fix(subagents): scope live observations to turns
- commit
- 8fd3c0149a6fcc0e6fb82ea1578d9af79a4f705b
- author
- brice · · 2026-08-12 10:35 UTC
- committer
- brice · · 2026-08-12 12:33 UTC
- parent
- e1622b9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +88 -11
M packages/agent/modes/subagent_indicator.go +1 -1
| @@ -24,7 +24,7 @@ func renderSubagentActivityLines(th tui.Theme, spinnerGlyph string, snapshots [] | |||
| 24 | 24 | if operation == nil { | |
| 25 | 25 | continue | |
| 26 | 26 | } | |
| 27 | - | line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.LastObservation, width, now) | |
| 27 | + | line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.ObservationFor(*operation), width, now) | |
| 28 | 28 | if line != "" { | |
| 29 | 29 | lines = append(lines, line) | |
| 30 | 30 | } | |
| @@ -24,7 +24,7 @@ func renderSubagentActivityLines(th tui.Theme, spinnerGlyph string, snapshots [] | |||||
| 24 | if operation == nil { | 24 | if operation == nil { | ||
| 25 | continue | 25 | continue | ||
| 26 | } | 26 | } | ||
| 27 | - | line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.LastObservation, width, now) | 27 | + | line := renderSubagentActivityLine(th, spinnerGlyph, snapshot, *operation, view.ObservationFor(*operation), width, now) |
| 28 | if line != "" { | 28 | if line != "" { | ||
| 29 | lines = append(lines, line) | 29 | lines = append(lines, line) | ||
| 30 | } | 30 | } | ||
M packages/agent/modes/subagent_indicator_test.go +11 -1
| @@ -30,13 +30,23 @@ func TestRenderSubagentActivityLinesShowsOnlyTraceOpenOperations(t *testing.T) { | |||
| 30 | 30 | func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) { | |
| 31 | 31 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | |
| 32 | 32 | lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | |
| 33 | - | "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", At: now.Add(-time.Second)}}, | |
| 33 | + | "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", TurnID: "turn-1", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}}, | |
| 34 | 34 | }, 100, now)) | |
| 35 | 35 | if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") { | |
| 36 | 36 | t.Fatalf("activity lines = %#v", lines) | |
| 37 | 37 | } | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | + | func TestRenderSubagentActivityLinesOmitsPriorTurnObservation(t *testing.T) { | |
| 41 | + | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | |
| 42 | + | lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | |
| 43 | + | "review-123": {PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}}, | |
| 44 | + | }, 100, now)) | |
| 45 | + | if got := strings.Join(lines, "\n"); strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") { | |
| 46 | + | t.Fatalf("activity lines = %#v", lines) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 40 | 50 | func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) { | |
| 41 | 51 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | |
| 42 | 52 | lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | |
| @@ -30,13 +30,23 @@ func TestRenderSubagentActivityLinesShowsOnlyTraceOpenOperations(t *testing.T) { | |||||
| 30 | func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) { | 30 | func TestRenderSubagentActivityLinesShowsSafeLastObservation(t *testing.T) { | ||
| 31 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | 31 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | ||
| 32 | lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | 32 | lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | ||
| 33 | - | "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", At: now.Add(-time.Second)}}, | 33 | + | "review-123": {PrimaryOperation: &subagents.Operation{Type: "turn.started", TurnID: "turn-1", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}}, |
| 34 | }, 100, now)) | 34 | }, 100, now)) | ||
| 35 | if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") { | 35 | if got := strings.Join(lines, "\n"); !strings.Contains(got, "turn open · assistant streaming 1s ago") || strings.Contains(got, "delta") { | ||
| 36 | t.Fatalf("activity lines = %#v", lines) | 36 | t.Fatalf("activity lines = %#v", lines) | ||
| 37 | } | 37 | } | ||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | + | func TestRenderSubagentActivityLinesOmitsPriorTurnObservation(t *testing.T) { | |||
| 41 | + | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | |||
| 42 | + | lines := plainActivityLines(renderSubagentActivityLines(tui.Dark, "/", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | |||
| 43 | + | "review-123": {PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}}, | |||
| 44 | + | }, 100, now)) | |||
| 45 | + | if got := strings.Join(lines, "\n"); strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") { | |||
| 46 | + | t.Fatalf("activity lines = %#v", lines) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 40 | func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) { | 50 | func TestRenderSubagentActivityLinesKeepsNameAndAgeOnNarrowTerminals(t *testing.T) { | ||
| 41 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | 51 | now := time.Date(2026, time.August, 6, 12, 0, 0, 0, time.UTC) | ||
| 42 | lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | 52 | lines := renderSubagentActivityLines(tui.Dark, "|", []subagents.AgentSnapshot{{ID: "review-123", Subagent: "reviewer"}}, map[string]subagents.AgentTraceView{ | ||
M packages/agent/modes/subagents_dialog.go +5 -3
| @@ -1630,9 +1630,11 @@ func formatSupervisorRow(r subagents.AgentSnapshot, view subagents.AgentTraceVie | |||
| 1630 | 1630 | primary = &view.OpenOperations[0] | |
| 1631 | 1631 | } | |
| 1632 | 1632 | if primary != nil { | |
| 1633 | - | fact += " " + formatAge(primary.StartedAt) | |
| 1634 | - | } else if view.LastObservation != nil { | |
| 1635 | - | fact += " " + formatAge(view.LastObservation.At) | |
| 1633 | + | if observation := view.ObservationFor(*primary); observation != nil { | |
| 1634 | + | fact += " " + formatAge(observation.At) | |
| 1635 | + | } else { | |
| 1636 | + | fact += " " + formatAge(primary.StartedAt) | |
| 1637 | + | } | |
| 1636 | 1638 | } else if view.LastEvent.Type != "" { | |
| 1637 | 1639 | fact += " " + formatAge(view.LastEvent.Timestamp) | |
| 1638 | 1640 | } | |
| @@ -1630,9 +1630,11 @@ func formatSupervisorRow(r subagents.AgentSnapshot, view subagents.AgentTraceVie | |||||
| 1630 | primary = &view.OpenOperations[0] | 1630 | primary = &view.OpenOperations[0] | ||
| 1631 | } | 1631 | } | ||
| 1632 | if primary != nil { | 1632 | if primary != nil { | ||
| 1633 | - | fact += " " + formatAge(primary.StartedAt) | 1633 | + | if observation := view.ObservationFor(*primary); observation != nil { |
| 1634 | - | } else if view.LastObservation != nil { | 1634 | + | fact += " " + formatAge(observation.At) |
| 1635 | - | fact += " " + formatAge(view.LastObservation.At) | 1635 | + | } else { |
| 1636 | + | fact += " " + formatAge(primary.StartedAt) | |||
| 1637 | + | } | |||
| 1636 | } else if view.LastEvent.Type != "" { | 1638 | } else if view.LastEvent.Type != "" { | ||
| 1637 | fact += " " + formatAge(view.LastEvent.Timestamp) | 1639 | fact += " " + formatAge(view.LastEvent.Timestamp) | ||
| 1638 | } | 1640 | } | ||
M packages/agent/modes/subagents_dialog_test.go +11 -0
| @@ -35,6 +35,17 @@ func TestFormatSupervisorRowShowsTerminalResultDelivery(t *testing.T) { | |||
| 35 | 35 | } | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | + | func TestFormatSupervisorRowOmitsPriorTurnObservationAge(t *testing.T) { | |
| 39 | + | now := time.Now() | |
| 40 | + | got := formatSupervisorRow(subagents.AgentSnapshot{ID: "review-1", Started: now.Add(-time.Hour)}, subagents.AgentTraceView{ | |
| 41 | + | PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, | |
| 42 | + | LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}, | |
| 43 | + | }, 120) | |
| 44 | + | if strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") { | |
| 45 | + | t.Fatalf("row = %q", got) | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 38 | 49 | func TestSubagentsDialogEmptyState(t *testing.T) { | |
| 39 | 50 | d := newSubagentsDialog() | |
| 40 | 51 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | |
| @@ -35,6 +35,17 @@ func TestFormatSupervisorRowShowsTerminalResultDelivery(t *testing.T) { | |||||
| 35 | } | 35 | } | ||
| 36 | } | 36 | } | ||
| 37 | 37 | ||||
| 38 | + | func TestFormatSupervisorRowOmitsPriorTurnObservationAge(t *testing.T) { | |||
| 39 | + | now := time.Now() | |||
| 40 | + | got := formatSupervisorRow(subagents.AgentSnapshot{ID: "review-1", Started: now.Add(-time.Hour)}, subagents.AgentTraceView{ | |||
| 41 | + | PrimaryOperation: &subagents.Operation{Type: "tool.started", TurnID: "turn-2", StartedAt: now.Add(-time.Minute)}, | |||
| 42 | + | LastObservation: &subagents.LiveObservation{Type: "assistant.stream.observed", TurnID: "turn-1", At: now.Add(-time.Second)}, | |||
| 43 | + | }, 120) | |||
| 44 | + | if strings.Contains(got, "assistant streaming") || !strings.Contains(got, "tool open") || !strings.Contains(got, "1m") { | |||
| 45 | + | t.Fatalf("row = %q", got) | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 38 | func TestSubagentsDialogEmptyState(t *testing.T) { | 49 | func TestSubagentsDialogEmptyState(t *testing.T) { | ||
| 39 | d := newSubagentsDialog() | 50 | d := newSubagentsDialog() | ||
| 40 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | 51 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | ||
M packages/agent/subagents/projection.go +13 -6
| @@ -114,14 +114,11 @@ func (v AgentTraceView) Summary() string { | |||
| 114 | 114 | primary = &v.OpenOperations[0] | |
| 115 | 115 | } | |
| 116 | 116 | if primary != nil { | |
| 117 | - | if v.LastObservation != nil { | |
| 118 | - | return primary.Label() + " · " + v.LastObservation.Label() | |
| 117 | + | if observation := v.ObservationFor(*primary); observation != nil { | |
| 118 | + | return primary.Label() + " · " + observation.Label() | |
| 119 | 119 | } | |
| 120 | 120 | return primary.Label() | |
| 121 | 121 | } | |
| 122 | - | if v.LastObservation != nil { | |
| 123 | - | return v.LastObservation.Label() | |
| 124 | - | } | |
| 125 | 122 | if v.LastEvent.Type != "" { | |
| 126 | 123 | return "last event " + v.LastEvent.Type | |
| 127 | 124 | } | |
| @@ -195,6 +192,16 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView { | |||
| 195 | 192 | return views | |
| 196 | 193 | } | |
| 197 | 194 | ||
| 195 | + | // ObservationFor returns the last observation only when it belongs to this | |
| 196 | + | // operation's turn. A prior turn's stream is not live activity for a resumed | |
| 197 | + | // or subsequent turn. | |
| 198 | + | func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation { | |
| 199 | + | if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID { | |
| 200 | + | return nil | |
| 201 | + | } | |
| 202 | + | return v.LastObservation | |
| 203 | + | } | |
| 204 | + | ||
| 198 | 205 | func operationPriority(o Operation) int { | |
| 199 | 206 | switch o.Type { | |
| 200 | 207 | case "tool.started": | |
| @@ -236,7 +243,7 @@ func traceResultFact(event TraceEvent, previous *ResultFact) *ResultFact { | |||
| 236 | 243 | result.At = event.Timestamp | |
| 237 | 244 | switch event.Type { | |
| 238 | 245 | case "result.available": | |
| 239 | - | result.Available = true | |
| 246 | + | result.Available, result.Delivered, result.Failed = true, false, false | |
| 240 | 247 | case "result.delivered": | |
| 241 | 248 | result.Available, result.Delivered, result.Failed = true, true, false | |
| 242 | 249 | case "result.delivery.failed": | |
| @@ -114,14 +114,11 @@ func (v AgentTraceView) Summary() string { | |||||
| 114 | primary = &v.OpenOperations[0] | 114 | primary = &v.OpenOperations[0] | ||
| 115 | } | 115 | } | ||
| 116 | if primary != nil { | 116 | if primary != nil { | ||
| 117 | - | if v.LastObservation != nil { | 117 | + | if observation := v.ObservationFor(*primary); observation != nil { |
| 118 | - | return primary.Label() + " · " + v.LastObservation.Label() | 118 | + | return primary.Label() + " · " + observation.Label() |
| 119 | } | 119 | } | ||
| 120 | return primary.Label() | 120 | return primary.Label() | ||
| 121 | } | 121 | } | ||
| 122 | - | if v.LastObservation != nil { | |||
| 123 | - | return v.LastObservation.Label() | |||
| 124 | - | } | |||
| 125 | if v.LastEvent.Type != "" { | 122 | if v.LastEvent.Type != "" { | ||
| 126 | return "last event " + v.LastEvent.Type | 123 | return "last event " + v.LastEvent.Type | ||
| 127 | } | 124 | } | ||
| @@ -195,6 +192,16 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView { | |||||
| 195 | return views | 192 | return views | ||
| 196 | } | 193 | } | ||
| 197 | 194 | ||||
| 195 | + | // ObservationFor returns the last observation only when it belongs to this | |||
| 196 | + | // operation's turn. A prior turn's stream is not live activity for a resumed | |||
| 197 | + | // or subsequent turn. | |||
| 198 | + | func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation { | |||
| 199 | + | if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID { | |||
| 200 | + | return nil | |||
| 201 | + | } | |||
| 202 | + | return v.LastObservation | |||
| 203 | + | } | |||
| 204 | + | ||||
| 198 | func operationPriority(o Operation) int { | 205 | func operationPriority(o Operation) int { | ||
| 199 | switch o.Type { | 206 | switch o.Type { | ||
| 200 | case "tool.started": | 207 | case "tool.started": | ||
| @@ -236,7 +243,7 @@ func traceResultFact(event TraceEvent, previous *ResultFact) *ResultFact { | |||||
| 236 | result.At = event.Timestamp | 243 | result.At = event.Timestamp | ||
| 237 | switch event.Type { | 244 | switch event.Type { | ||
| 238 | case "result.available": | 245 | case "result.available": | ||
| 239 | - | result.Available = true | 246 | + | result.Available, result.Delivered, result.Failed = true, false, false |
| 240 | case "result.delivered": | 247 | case "result.delivered": | ||
| 241 | result.Available, result.Delivered, result.Failed = true, true, false | 248 | result.Available, result.Delivered, result.Failed = true, true, false | ||
| 242 | case "result.delivery.failed": | 249 | case "result.delivery.failed": | ||
M packages/agent/subagents/projection_test.go +30 -0
| @@ -79,6 +79,36 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing | |||
| 79 | 79 | } | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | + | func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) { | |
| 83 | + | started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC) | |
| 84 | + | view := ProjectTrace([]TraceEvent{ | |
| 85 | + | {Timestamp: started, Type: "assistant.stream.observed", AgentID: "agent-1", TurnID: "turn-1"}, | |
| 86 | + | {Timestamp: started.Add(time.Second), Type: "turn.started", AgentID: "agent-1", TurnID: "turn-2"}, | |
| 87 | + | })["agent-1"] | |
| 88 | + | if view.LastObservation == nil { | |
| 89 | + | t.Fatal("last observation missing") | |
| 90 | + | } | |
| 91 | + | if view.PrimaryOperation == nil { | |
| 92 | + | t.Fatal("primary operation missing") | |
| 93 | + | } | |
| 94 | + | if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil { | |
| 95 | + | t.Fatalf("prior turn observation = %#v, want omitted", observation) | |
| 96 | + | } | |
| 97 | + | if got, want := view.Summary(), "turn open"; got != want { | |
| 98 | + | t.Fatalf("summary = %q, want %q", got, want) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestProjectTraceNewAvailableResultResetsDelivery(t *testing.T) { | |
| 103 | + | view := ProjectTrace([]TraceEvent{ | |
| 104 | + | {Type: "result.delivered", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-1"}}, | |
| 105 | + | {Type: "result.available", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-2"}}, | |
| 106 | + | })["agent-1"] | |
| 107 | + | if view.Result == nil || !view.Result.Available || view.Result.Delivered || view.Result.Failed || view.Result.Ref != "subagent://agent-1/result-2" { | |
| 108 | + | t.Fatalf("result = %#v", view.Result) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 82 | 112 | func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) { | |
| 83 | 113 | view := ProjectTrace([]TraceEvent{ | |
| 84 | 114 | {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}}, | |
| @@ -79,6 +79,36 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing | |||||
| 79 | } | 79 | } | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | + | func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) { | |||
| 83 | + | started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC) | |||
| 84 | + | view := ProjectTrace([]TraceEvent{ | |||
| 85 | + | {Timestamp: started, Type: "assistant.stream.observed", AgentID: "agent-1", TurnID: "turn-1"}, | |||
| 86 | + | {Timestamp: started.Add(time.Second), Type: "turn.started", AgentID: "agent-1", TurnID: "turn-2"}, | |||
| 87 | + | })["agent-1"] | |||
| 88 | + | if view.LastObservation == nil { | |||
| 89 | + | t.Fatal("last observation missing") | |||
| 90 | + | } | |||
| 91 | + | if view.PrimaryOperation == nil { | |||
| 92 | + | t.Fatal("primary operation missing") | |||
| 93 | + | } | |||
| 94 | + | if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil { | |||
| 95 | + | t.Fatalf("prior turn observation = %#v, want omitted", observation) | |||
| 96 | + | } | |||
| 97 | + | if got, want := view.Summary(), "turn open"; got != want { | |||
| 98 | + | t.Fatalf("summary = %q, want %q", got, want) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func TestProjectTraceNewAvailableResultResetsDelivery(t *testing.T) { | |||
| 103 | + | view := ProjectTrace([]TraceEvent{ | |||
| 104 | + | {Type: "result.delivered", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-1"}}, | |||
| 105 | + | {Type: "result.available", AgentID: "agent-1", Data: map[string]any{"ref": "subagent://agent-1/result-2"}}, | |||
| 106 | + | })["agent-1"] | |||
| 107 | + | if view.Result == nil || !view.Result.Available || view.Result.Delivered || view.Result.Failed || view.Result.Ref != "subagent://agent-1/result-2" { | |||
| 108 | + | t.Fatalf("result = %#v", view.Result) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 82 | func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) { | 112 | func TestProjectTraceFailedAgentClosesOpenOperations(t *testing.T) { | ||
| 83 | view := ProjectTrace([]TraceEvent{ | 113 | view := ProjectTrace([]TraceEvent{ | ||
| 84 | {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}}, | 114 | {Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one"}}, | ||
M packages/agent/subagents/result_test.go +17 -0
| @@ -198,6 +198,23 @@ func TestEnsureResultRecordsAvailabilityAfterDurableWrite(t *testing.T) { | |||
| 198 | 198 | if _, err := os.Stat(resultPath(state)); err != nil { | |
| 199 | 199 | t.Fatalf("durable result missing: %v", err) | |
| 200 | 200 | } | |
| 201 | + | ||
| 202 | + | failedState := t.TempDir() | |
| 203 | + | if err := os.Mkdir(resultPath(failedState), 0o700); err != nil { | |
| 204 | + | t.Fatal(err) | |
| 205 | + | } | |
| 206 | + | failedTrace := NewMemoryTraceWriter() | |
| 207 | + | t.Cleanup(func() { _ = failedTrace.Close() }) | |
| 208 | + | failedAgent := &Agent{ID: "agent-2", stateDir: failedState, trace: failedTrace, maxOutputBytes: 1024, maxOutputLines: 10} | |
| 209 | + | (&Supervisor{}).ensureResult(failedAgent, StatusDone, nil) | |
| 210 | + | if err := failedTrace.Flush(); err != nil { | |
| 211 | + | t.Fatal(err) | |
| 212 | + | } | |
| 213 | + | for _, event := range failedTrace.Events() { | |
| 214 | + | if event.Type == "result.available" { | |
| 215 | + | t.Fatalf("result.available recorded after failed write: %#v", event) | |
| 216 | + | } | |
| 217 | + | } | |
| 201 | 218 | } | |
| 202 | 219 | ||
| 203 | 220 | func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) { | |
| @@ -198,6 +198,23 @@ func TestEnsureResultRecordsAvailabilityAfterDurableWrite(t *testing.T) { | |||||
| 198 | if _, err := os.Stat(resultPath(state)); err != nil { | 198 | if _, err := os.Stat(resultPath(state)); err != nil { | ||
| 199 | t.Fatalf("durable result missing: %v", err) | 199 | t.Fatalf("durable result missing: %v", err) | ||
| 200 | } | 200 | } | ||
| 201 | + | ||||
| 202 | + | failedState := t.TempDir() | |||
| 203 | + | if err := os.Mkdir(resultPath(failedState), 0o700); err != nil { | |||
| 204 | + | t.Fatal(err) | |||
| 205 | + | } | |||
| 206 | + | failedTrace := NewMemoryTraceWriter() | |||
| 207 | + | t.Cleanup(func() { _ = failedTrace.Close() }) | |||
| 208 | + | failedAgent := &Agent{ID: "agent-2", stateDir: failedState, trace: failedTrace, maxOutputBytes: 1024, maxOutputLines: 10} | |||
| 209 | + | (&Supervisor{}).ensureResult(failedAgent, StatusDone, nil) | |||
| 210 | + | if err := failedTrace.Flush(); err != nil { | |||
| 211 | + | t.Fatal(err) | |||
| 212 | + | } | |||
| 213 | + | for _, event := range failedTrace.Events() { | |||
| 214 | + | if event.Type == "result.available" { | |||
| 215 | + | t.Fatalf("result.available recorded after failed write: %#v", event) | |||
| 216 | + | } | |||
| 217 | + | } | |||
| 201 | } | 218 | } | ||
| 202 | 219 | ||||
| 203 | func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) { | 220 | func TestCaptureWorkspaceFailureIsDurableAndPrivate(t *testing.T) { | ||