Move questions and reviews into the transcript; agents answer findings

This commit is contained in:
Ian Gulliver
2026-10-19 09:14:21 -07:00
parent eb7e0a13c4
commit 27ff0f457b
21 changed files with 903 additions and 801 deletions
+50 -10
View File
@@ -51,6 +51,8 @@ enum RowKind {
case turnEnd(error: String?)
case note(Note)
case review(note: Note, md: [MDBlock])
case finding(index: Int)
case findingResponse(snapshot: Int, index: Int, response: String, reason: String)
case ready(when: Date, md: [MDBlock])
case handoff
case snapshot(Snapshot)
@@ -84,6 +86,7 @@ final class Row: Identifiable {
var calls: [Call] = []
var summary: Note?
var review: Review?
var reviewOf: Int?
var question: InboxItem?
var text = ""
var md: [MDBlock] = []
@@ -175,6 +178,8 @@ private final class Ctx {
var subagents: [String: Subagent] = [:]
var reviewers: Set<String> = []
var reviewButtons: [Int: Row] = [:]
var reviews: [Int: Review] = [:]
var reviewRows: [String: [Row]] = [:]
var questions: [String: Row] = [:]
var run: Run?
var held: RowList?
@@ -471,6 +476,17 @@ final class TranscriptBuilder {
if ev.kind == "review.started" || ev.kind == "review.finished" || ev.kind == "review.finding" {
let r = try! decodePayload(ev.payload, as: Review.self)
ctx.reviewButtons[r.snapshotN]?.review = r
ctx.reviews[r.snapshotN] = r
for row in ctx.reviewRows[r.id] ?? [] {
row.review = r
}
if ev.kind == "review.finished" {
ctx.run = nil
for row in renderReview(r, ev.createdAt) {
put(row)
}
continue
}
}
if ev.kind == "inbox.updated" {
let item = try! decodePayload(ev.payload, as: InboxItem.self)
@@ -490,6 +506,20 @@ final class TranscriptBuilder {
put(row, keepHeld: true)
continue
}
if ev.kind == "agent.tool_call" && sub == nil && str(p, "name") == "mcp__byre__respond_to_finding" {
let input = dict(p, "input") ?? [:]
let n = int(input, "snapshot") ?? 0
let row = Row(.findingResponse(snapshot: n, index: (int(input, "finding") ?? 0) - 1, response: str(input, "response") ?? "", reason: str(input, "reason") ?? ""), taskId: taskId)
row.started = ev.createdAt
ctx.tools[jsString(p["id"])] = row
if let r = ctx.reviews[n], r.state == .done {
row.review = r
ctx.reviewRows[r.id, default: []].append(row)
}
ctx.run = nil
put(row, keepHeld: true)
continue
}
if ev.kind == "agent.tool_call" {
let name = toolLabel(jsString(p["name"]))
let row = renderToolCall(p)
@@ -605,6 +635,22 @@ final class TranscriptBuilder {
return row
}
private func renderReview(_ r: Review, _ when: Date) -> [Row] {
let outcome = r.state == .failed ? "failed: \(r.error)" : r.state == .skipped ? "skipped: no changes" : reviewCounts(r)
let parts: [NotePart] = [.text("review of snapshot #\(r.snapshotN): \(outcome)")]
let fs = findings(r)
let head = fs.isEmpty
? Row(.note(Note(parts: parts, when: when, style: r.state == .failed ? .error : .plain)), taskId: taskId)
: Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: taskId)
let rows = [head] + fs.indices.map { Row(.finding(index: $0), taskId: taskId) }
for row in rows {
row.review = r
row.reviewOf = r.snapshotN
}
ctx.reviewRows[r.id, default: []] += rows
return rows
}
private func renderEvent(_ ev: ByreEvent) -> Row? {
let p = ev.payload.value
let when = ev.createdAt
@@ -654,6 +700,9 @@ final class TranscriptBuilder {
return Row(.orphan(body), taskId: taskId)
}
target.result = body
if case .findingResponse = target.kind, bool(p, "is_error") {
target.hidden = true
}
return nil
case "turn.ended":
@@ -709,15 +758,6 @@ final class TranscriptBuilder {
case "task.merge_title":
return note([.text("\(ev.actor) set the merge commit title to \"\(jsString(p["merge_title"]))\"")])
case "review.finished":
let r = try! decodePayload(ev.payload, as: Review.self)
let outcome = r.state == .failed ? "failed: \(r.error)" : r.state == .skipped ? "skipped: no changes" : reviewCounts(r)
let parts: [NotePart] = [.text("review of snapshot #\(r.snapshotN): \(outcome)")]
if findings(r).isEmpty {
return note(parts, r.state == .failed ? .error : .plain)
}
return Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: taskId)
case "task.merge_queued":
return note([.text("\(ev.actor) asked to merge; held until the merge in progress in this project is done")])
@@ -778,7 +818,7 @@ func findings(_ r: Review) -> [JSONDict] {
}
func findingCount(_ r: Review) -> Int {
r.state == .done ? findings(r).indices.filter { r.findingStates.additionalProperties[String($0)] == nil }.count : 0
r.state == .done ? findings(r).indices.filter { findingOpen(r, $0) }.count : 0
}
func latestReview(_ events: [ByreEvent]) -> Review? {