Skip to content

Commit e263664

Browse files
committed
fix(cli): simplify live event tracking
1 parent 47fdf60 commit e263664

7 files changed

Lines changed: 127 additions & 133 deletions

File tree

packages/core/src/session/runner/publish-llm-event.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
6262
called: boolean
6363
settled: boolean
6464
providerExecuted: boolean
65-
providerState?: Record<string, unknown>
6665
}
6766
>()
6867
let assistantMessageID: SessionMessage.ID | undefined
@@ -307,14 +306,14 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
307306
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
308307
tool.called = true
309308
tool.providerExecuted = event.providerExecuted === true
310-
tool.providerState = providerState(event.providerMetadata)
309+
const state = providerState(event.providerMetadata)
311310
yield* events.publish(SessionEvent.Tool.Called, {
312311
sessionID: input.sessionID,
313312
assistantMessageID: tool.assistantMessageID,
314313
callID: event.id,
315314
input: record(event.input),
316315
executed: tool.providerExecuted,
317-
state: tool.providerState,
316+
state,
318317
})
319318
return
320319
}

packages/llm/src/protocols/openai-responses.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -815,10 +815,7 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
815815
const item = event.item
816816
if (!item) return [state, NO_EVENTS] satisfies StepResult
817817

818-
if (item.type === "message" && item.id) {
819-
const events: LLMEvent[] = []
820-
return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, item.id) }, events] satisfies StepResult
821-
}
818+
if (item.type === "message" && item.id) return onOutputTextDone(state, { ...event, item_id: item.id })
822819

823820
if (item.type === "function_call") {
824821
if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult

packages/opencode/src/cli/cmd/run/stream-v2.subagent.ts

Lines changed: 57 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -38,86 +38,91 @@ export function outputText(content: ReadonlyArray<{ type: string; text?: string
3838
export function legacyTool(input: {
3939
sessionID: string
4040
messageID: string
41-
callID: string
42-
name: string
43-
state: SessionMessageAssistantTool["state"]
44-
time: SessionMessageAssistantTool["time"]
45-
executed?: boolean
46-
providerState?: Record<string, unknown>
47-
providerResultState?: Record<string, unknown>
41+
tool: SessionMessageAssistantTool
4842
}): ToolPart {
43+
const tool = input.tool
4944
const providerCall =
50-
input.executed === undefined && input.providerState === undefined
45+
tool.executed === undefined && tool.providerState === undefined
5146
? undefined
52-
: { executed: input.executed, state: input.providerState }
47+
: { executed: tool.executed, state: tool.providerState }
5348
const providerResult =
54-
input.executed === undefined && input.providerResultState === undefined
49+
tool.executed === undefined && tool.providerResultState === undefined
5550
? undefined
56-
: { executed: input.executed, state: input.providerResultState }
51+
: { executed: tool.executed, state: tool.providerResultState }
5752
const base = {
58-
id: `prt_${input.callID}`,
53+
id: `prt_${tool.id}`,
5954
sessionID: input.sessionID,
6055
messageID: input.messageID,
6156
type: "tool" as const,
62-
callID: input.callID,
63-
tool: input.name,
57+
callID: tool.id,
58+
tool: tool.name,
6459
}
65-
if (input.state.status === "pending") {
60+
if (tool.state.status === "pending") {
6661
return {
6762
...base,
68-
state: { status: "pending", input: {}, raw: input.state.input },
63+
state: { status: "pending", input: {}, raw: tool.state.input },
6964
}
7065
}
71-
if (input.state.status === "running") {
66+
if (tool.state.status === "running") {
7267
return {
7368
...base,
7469
state: {
7570
status: "running",
76-
input: input.state.input,
77-
title: input.name,
78-
metadata: { structured: input.state.structured, content: input.state.content, providerCall },
79-
time: { start: input.time.ran ?? input.time.created },
71+
input: tool.state.input,
72+
title: tool.name,
73+
metadata: { structured: tool.state.structured, content: tool.state.content, providerCall },
74+
time: { start: tool.time.ran ?? tool.time.created },
8075
},
8176
}
8277
}
83-
if (input.state.status === "completed") {
78+
if (tool.state.status === "completed") {
8479
return {
8580
...base,
8681
state: {
8782
status: "completed",
88-
input: input.state.input,
89-
output: outputText(input.state.content),
90-
title: input.name,
83+
input: tool.state.input,
84+
output: outputText(tool.state.content),
85+
title: tool.name,
9186
metadata: {
92-
structured: input.state.structured,
93-
content: input.state.content,
94-
outputPaths: input.state.outputPaths,
95-
result: input.state.result,
87+
structured: tool.state.structured,
88+
content: tool.state.content,
89+
outputPaths: tool.state.outputPaths,
90+
result: tool.state.result,
9691
providerCall,
9792
providerResult,
9893
},
99-
time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
94+
time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created },
10095
},
10196
}
10297
}
10398
return {
10499
...base,
105100
state: {
106101
status: "error",
107-
input: input.state.input,
108-
error: input.state.error.message,
102+
input: tool.state.input,
103+
error: tool.state.error.message,
109104
metadata: {
110-
structured: input.state.structured,
111-
content: input.state.content,
112-
result: input.state.result,
105+
structured: tool.state.structured,
106+
content: tool.state.content,
107+
result: tool.state.result,
113108
providerCall,
114109
providerResult,
115110
},
116-
time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
111+
time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created },
117112
},
118113
}
119114
}
120115

116+
export function nextFragmentID(kind: "text" | "reasoning", ordinals: Map<string, number>, messageID: string) {
117+
const ordinal = ordinals.get(messageID) ?? 0
118+
ordinals.set(messageID, ordinal + 1)
119+
return `${kind}:${ordinal}`
120+
}
121+
122+
export function currentFragmentID(kind: "text" | "reasoning", ordinals: Map<string, number>, messageID: string) {
123+
return `${kind}:${Math.max(0, (ordinals.get(messageID) ?? 1) - 1)}`
124+
}
125+
121126
export function toolCommit(part: ToolPart, phase: "start" | "progress" | "final"): StreamCommit {
122127
const status = part.state.status
123128
const text =
@@ -153,7 +158,6 @@ type ToolTrack = {
153158
name: string
154159
input: Record<string, unknown>
155160
started: number
156-
executed?: boolean
157161
providerState?: Record<string, unknown>
158162
}
159163

@@ -173,8 +177,6 @@ type ChildState = {
173177
projectedReasoning: Map<string, string>
174178
textOrdinals: Map<string, number>
175179
reasoningOrdinals: Map<string, number>
176-
activeText: Map<string, string>
177-
activeReasoning: Map<string, string>
178180
tools: Map<string, ToolTrack>
179181
finishedTools: Set<string>
180182
messageIDs: Set<string>
@@ -235,7 +237,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
235237
// Live subagent tool calls in the parent, so tool.success structured output
236238
// can be joined with the call's input metadata.
237239
const pendingCalls = new Map<string, Record<string, unknown>>()
238-
const subagentCalls = new Set<string>()
239240
// Foreign sessions already resolved through session.get. Non-children stay
240241
// cached so unrelated concurrent sessions are checked at most once.
241242
const checked = new Set<string>()
@@ -263,8 +264,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
263264
projectedReasoning: new Map(),
264265
textOrdinals: new Map(),
265266
reasoningOrdinals: new Map(),
266-
activeText: new Map(),
267-
activeReasoning: new Map(),
268267
tools: new Map(),
269268
finishedTools: new Set(),
270269
messageIDs: new Set(),
@@ -328,13 +327,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
328327
const part = legacyTool({
329328
sessionID: child.sessionID,
330329
messageID,
331-
callID: item.id,
332-
name: item.name,
333-
state: item.state,
334-
time: item.time,
335-
executed: item.executed,
336-
providerState: item.providerState,
337-
providerResultState: item.providerResultState,
330+
tool: item,
338331
})
339332
if (item.state.status === "pending") return
340333
child.callIDs.add(item.id)
@@ -355,8 +348,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
355348
child.projectedReasoning.clear()
356349
child.textOrdinals.clear()
357350
child.reasoningOrdinals.clear()
358-
child.activeText.clear()
359-
child.activeReasoning.clear()
360351
child.finishedTools.clear()
361352
child.messageIDs.clear()
362353
child.callIDs.clear()
@@ -477,15 +468,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
477468
return
478469
}
479470
if (event.type === "session.text.started") {
480-
const ordinal = child.textOrdinals.get(event.data.assistantMessageID) ?? 0
481-
child.textOrdinals.set(event.data.assistantMessageID, ordinal + 1)
482-
child.activeText.set(event.data.assistantMessageID, `text:${ordinal}`)
471+
nextFragmentID("text", child.textOrdinals, event.data.assistantMessageID)
483472
return
484473
}
485474
if (event.type === "session.text.delta") {
486-
const id =
487-
child.activeText.get(event.data.assistantMessageID) ??
488-
`text:${Math.max(0, (child.textOrdinals.get(event.data.assistantMessageID) ?? 1) - 1)}`
475+
const id = currentFragmentID("text", child.textOrdinals, event.data.assistantMessageID)
489476
const key = fragmentKey(event.data.assistantMessageID, id)
490477
const projected = child.projectedText.get(key)
491478
const covered = projected?.indexOf(event.data.delta) ?? -1
@@ -508,13 +495,10 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
508495
return
509496
}
510497
if (event.type === "session.text.ended") {
511-
const id =
512-
child.activeText.get(event.data.assistantMessageID) ??
513-
`text:${Math.max(0, (child.textOrdinals.get(event.data.assistantMessageID) ?? 1) - 1)}`
498+
const id = currentFragmentID("text", child.textOrdinals, event.data.assistantMessageID)
514499
const key = fragmentKey(event.data.assistantMessageID, id)
515500
child.text.set(key, event.data.text)
516501
child.projectedText.delete(key)
517-
child.activeText.delete(event.data.assistantMessageID)
518502
setFrame(child, key, {
519503
kind: "assistant",
520504
source: "assistant",
@@ -528,15 +512,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
528512
return
529513
}
530514
if (event.type === "session.reasoning.started") {
531-
const ordinal = child.reasoningOrdinals.get(event.data.assistantMessageID) ?? 0
532-
child.reasoningOrdinals.set(event.data.assistantMessageID, ordinal + 1)
533-
child.activeReasoning.set(event.data.assistantMessageID, `reasoning:${ordinal}`)
515+
nextFragmentID("reasoning", child.reasoningOrdinals, event.data.assistantMessageID)
534516
return
535517
}
536518
if (event.type === "session.reasoning.delta") {
537-
const id =
538-
child.activeReasoning.get(event.data.assistantMessageID) ??
539-
`reasoning:${Math.max(0, (child.reasoningOrdinals.get(event.data.assistantMessageID) ?? 1) - 1)}`
519+
const id = currentFragmentID("reasoning", child.reasoningOrdinals, event.data.assistantMessageID)
540520
const key = fragmentKey(event.data.assistantMessageID, id)
541521
const projected = child.projectedReasoning.get(key)
542522
const covered = projected?.indexOf(event.data.delta) ?? -1
@@ -559,13 +539,10 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
559539
return
560540
}
561541
if (event.type === "session.reasoning.ended") {
562-
const id =
563-
child.activeReasoning.get(event.data.assistantMessageID) ??
564-
`reasoning:${Math.max(0, (child.reasoningOrdinals.get(event.data.assistantMessageID) ?? 1) - 1)}`
542+
const id = currentFragmentID("reasoning", child.reasoningOrdinals, event.data.assistantMessageID)
565543
const key = fragmentKey(event.data.assistantMessageID, id)
566544
child.reasoning.set(key, event.data.text)
567545
child.projectedReasoning.delete(key)
568-
child.activeReasoning.delete(event.data.assistantMessageID)
569546
if (!input.thinking) return
570547
setFrame(child, key, {
571548
kind: "reasoning",
@@ -588,7 +565,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
588565
name: current?.name ?? "tool",
589566
input: event.data.input,
590567
started: current?.started ?? event.created,
591-
executed: event.data.executed,
592568
providerState: event.data.state,
593569
})
594570
childTool(
@@ -650,7 +626,14 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
650626
notifyDetail(child)
651627
return
652628
}
629+
if (event.type === "session.step.ended") {
630+
child.textOrdinals.delete(event.data.assistantMessageID)
631+
child.reasoningOrdinals.delete(event.data.assistantMessageID)
632+
return
633+
}
653634
if (event.type === "session.step.failed") {
635+
child.textOrdinals.delete(event.data.assistantMessageID)
636+
child.reasoningOrdinals.delete(event.data.assistantMessageID)
654637
setFrame(child, `error:step:${event.data.assistantMessageID}`, {
655638
kind: "error",
656639
source: "system",
@@ -687,22 +670,20 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
687670
return {
688671
main(event) {
689672
if (event.type === "session.tool.input.started") {
690-
if (event.data.name === "subagent") subagentCalls.add(event.data.callID)
673+
if (event.data.name === "subagent") pendingCalls.set(event.data.callID, {})
691674
return
692675
}
693676
if (event.type === "session.tool.called") {
694-
if (subagentCalls.has(event.data.callID)) pendingCalls.set(event.data.callID, event.data.input)
677+
if (pendingCalls.has(event.data.callID)) pendingCalls.set(event.data.callID, event.data.input)
695678
return
696679
}
697680
if (event.type === "session.tool.failed") {
698681
pendingCalls.delete(event.data.callID)
699-
subagentCalls.delete(event.data.callID)
700682
return
701683
}
702684
if (event.type !== "session.tool.success") return
703685
const pending = pendingCalls.get(event.data.callID)
704686
pendingCalls.delete(event.data.callID)
705-
subagentCalls.delete(event.data.callID)
706687
const found = childSessionID(record(event.data.structured))
707688
if (!found) return
708689
const child = ensureChild(found.sessionID)

0 commit comments

Comments
 (0)