From 97685d5ed1ee845c1a3c048b3a606800a950cc16 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Apr 2026 18:55:59 -0400 Subject: [PATCH] core: enable real-time tool progress updates during execution Add session.next.tool.progress event so users can see live status from long-running tools instead of waiting for completion. Consolidate tool state metadata into a unified 'details' field for consistent display. --- packages/opencode/src/session/processor.ts | 3 +- .../opencode/src/v2/session-entry-stepper.ts | 14 +++- packages/opencode/src/v2/session-entry.ts | 8 +- packages/opencode/src/v2/session-event.ts | 73 +++++++++---------- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 0ec6400e26..0efb9697ea 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -380,7 +380,6 @@ export const layer: Layer.Layer< SyncEvent.run(SessionEvent.Tool.Success.Sync, { sessionID: ctx.sessionID, callID: value.toolCallId, - title: value.output.title, output: value.output.output, attachments: value.output.attachments?.map((item: MessageV2.FilePart) => ({ uri: item.url, @@ -396,9 +395,9 @@ export const layer: Layer.Layer< } : {}), })), + details: value.output.metadata, provider: { executed: toolCall?.part.metadata?.providerExecuted === true, - metadata: value.output.metadata, }, timestamp: DateTime.makeUnsafe(Date.now()), }) diff --git a/packages/opencode/src/v2/session-entry-stepper.ts b/packages/opencode/src/v2/session-entry-stepper.ts index 5abefabe30..745024e05f 100644 --- a/packages/opencode/src/v2/session-entry-stepper.ts +++ b/packages/opencode/src/v2/session-entry-stepper.ts @@ -169,6 +169,16 @@ export function stepWith(adapter: Adapter, event: SessionEvent.E ) } }, + "session.next.tool.progress": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") match.state.details = event.data.details + }), + ) + } + }, "session.next.tool.success": (event) => { if (currentAssistant) { adapter.updateAssistant( @@ -179,8 +189,7 @@ export function stepWith(adapter: Adapter, event: SessionEvent.E status: "completed", input: match.state.input, output: event.data.output ?? "", - title: event.data.title, - metadata: event.metadata ?? {}, + details: event.data.details, attachments: [...(event.data.attachments ?? [])], } } @@ -198,7 +207,6 @@ export function stepWith(adapter: Adapter, event: SessionEvent.E status: "error", error: event.data.error, input: match.state.input, - metadata: event.metadata ?? {}, } } }), diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts index 42a020e05a..e7681f3954 100644 --- a/packages/opencode/src/v2/session-entry.ts +++ b/packages/opencode/src/v2/session-entry.ts @@ -62,16 +62,14 @@ export class ToolStatePending extends Schema.Class("Session.En export class ToolStateRunning extends Schema.Class("Session.Entry.ToolState.Running")({ status: Schema.Literal("running"), input: Schema.Record(Schema.String, Schema.Unknown), - title: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + details: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} export class ToolStateCompleted extends Schema.Class("Session.Entry.ToolState.Completed")({ status: Schema.Literal("completed"), input: Schema.Record(Schema.String, Schema.Unknown), output: Schema.String, - title: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown), + details: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), }) {} @@ -79,7 +77,7 @@ export class ToolStateError extends Schema.Class("Session.Entry. status: Schema.Literal("error"), input: Schema.Record(Schema.String, Schema.Unknown), error: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + details: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 0ea69c68f7..c0f4003fe5 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -16,12 +16,16 @@ export const Source = Schema.Struct({ }) export type Source = Schema.Schema.Type +const Base = { + timestamp: Schema.DateTimeUtcFromMillis, + sessionID: SessionID, +} + export const Prompted = Event.define({ type: "session.next.prompted", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, prompt: Prompt, }, }) @@ -31,8 +35,7 @@ export const Synthetic = Event.define({ type: "session.next.synthetic", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, text: Schema.String, }, }) @@ -43,8 +46,7 @@ export namespace Step { type: "session.next.step.started", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, model: Schema.Struct({ id: Schema.String, providerID: Schema.String, @@ -58,8 +60,7 @@ export namespace Step { type: "session.next.step.ended", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, reason: Schema.String, cost: Schema.Number, tokens: Schema.Struct({ @@ -81,8 +82,7 @@ export namespace Text { type: "session.next.text.started", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, }, }) export type Started = Schema.Schema.Type @@ -91,8 +91,7 @@ export namespace Text { type: "session.next.text.delta", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, delta: Schema.String, }, }) @@ -102,8 +101,7 @@ export namespace Text { type: "session.next.text.ended", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, text: Schema.String, }, }) @@ -115,8 +113,7 @@ export namespace Reasoning { type: "session.next.reasoning.started", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, reasoningID: Schema.String, }, }) @@ -126,8 +123,7 @@ export namespace Reasoning { type: "session.next.reasoning.delta", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, reasoningID: Schema.String, delta: Schema.String, }, @@ -138,8 +134,7 @@ export namespace Reasoning { type: "session.next.reasoning.ended", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, reasoningID: Schema.String, text: Schema.String, }, @@ -153,8 +148,7 @@ export namespace Tool { type: "session.next.tool.input.started", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, name: Schema.String, }, @@ -165,8 +159,7 @@ export namespace Tool { type: "session.next.tool.input.delta", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, delta: Schema.String, }, @@ -177,8 +170,7 @@ export namespace Tool { type: "session.next.tool.input.ended", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, text: Schema.String, }, @@ -190,8 +182,7 @@ export namespace Tool { type: "session.next.tool.called", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, tool: Schema.String, input: Schema.Record(Schema.String, Schema.Unknown), @@ -203,16 +194,26 @@ export namespace Tool { }) export type Called = Schema.Schema.Type + export const Progress = Event.define({ + type: "session.next.tool.progress", + aggregate: "sessionID", + schema: { + ...Base, + callID: Schema.String, + details: Schema.Record(Schema.String, Schema.Unknown), + }, + }) + export type Progress = Schema.Schema.Type + export const Success = Event.define({ type: "session.next.tool.success", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, - title: Schema.String, output: Schema.String.pipe(Schema.optional), attachments: Schema.Array(FileAttachment).pipe(Schema.optional), + details: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), provider: Schema.Struct({ executed: Schema.Boolean, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), @@ -225,8 +226,7 @@ export namespace Tool { type: "session.next.tool.error", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, callID: Schema.String, error: Schema.String, provider: Schema.Struct({ @@ -254,8 +254,7 @@ export const Retried = Event.define({ type: "session.next.retried", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, attempt: Schema.Number, error: RetryError, }, @@ -266,8 +265,7 @@ export const Compacted = Event.define({ type: "session.next.compacted", aggregate: "sessionID", schema: { - timestamp: Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + ...Base, auto: Schema.Boolean, overflow: Schema.Boolean.pipe(Schema.optional), }, @@ -287,6 +285,7 @@ export const All = Schema.Union( Tool.Input.Delta, Tool.Input.Ended, Tool.Called, + Tool.Progress, Tool.Success, Tool.Error, Reasoning.Started,