From ac09224ec339fbf5dfce43b19814557bd551404e Mon Sep 17 00:00:00 2001 From: Nomadcxx Date: Tue, 16 Jun 2026 07:04:49 +1000 Subject: [PATCH 1/2] feat(mcp): surface tool progress --- packages/opencode/src/mcp/catalog.ts | 6 ++- packages/opencode/src/session/prompt.ts | 9 +++- packages/opencode/src/session/tools.ts | 36 ++++++++++++- packages/opencode/test/mcp/lifecycle.test.ts | 57 ++++++++++++++++++++ packages/opencode/test/session/tools.test.ts | 26 +++++++++ 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/test/session/tools.test.ts diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 6d4b985dd2b8..d549b7a5f98f 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -2,6 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { CallToolResultSchema, ListToolsResultSchema, + type Progress, ToolSchema, type Tool as MCPToolDef, } from "@modelcontextprotocol/sdk/types.js" @@ -11,6 +12,9 @@ import { Effect } from "effect" const DEFAULT_TIMEOUT = 30_000 const MAX_LIST_PAGES = 1_000 +export type ProgressCallback = (progress: Progress) => void +type ToolExecutionOptionsWithProgress = { onprogress?: ProgressCallback } + const TolerantListToolsResultSchema = ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array(), }) @@ -62,7 +66,7 @@ export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: numbe signal: options.abortSignal, timeout, // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. - onprogress: () => {}, + onprogress: (progress) => (options as ToolExecutionOptionsWithProgress).onprogress?.(progress), }, ) if (result.isError) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b616df6e598e..f0f1fc9b0750 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1131,7 +1131,8 @@ export const layer = Layer.effect( throw new Error("Impossible") }) - const runLoop: (sessionID: SessionID) => Effect.Effect = Effect.fn("SessionPrompt.run")( + const runLoop: (sessionID: SessionID) => Effect.Effect = + Effect.fn("SessionPrompt.run")( function* (sessionID: SessionID) { const ctx = yield* InstanceState.context let structured: unknown @@ -1386,7 +1387,11 @@ export const layer = Layer.effect( const loop: (input: LoopInput) => Effect.Effect = Effect.fn("SessionPrompt.loop")(function* ( input: LoopInput, ) { - return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID)) + return yield* state.ensureRunning( + input.sessionID, + lastAssistant(input.sessionID), + runLoop(input.sessionID).pipe(Effect.provideService(EventV2Bridge.Service, events)), + ) }) const shell: (input: ShellInput) => Effect.Effect = Effect.fn( diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 87582ce0751e..0cbd20d63e3a 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -3,6 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session" import { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" import { MCP } from "@/mcp" +import type { ProgressCallback } from "@/mcp/catalog" import { Permission } from "@/permission" import { Tool } from "@/tool/tool" import { ToolJsonSchema } from "@/tool/json-schema" @@ -12,7 +13,7 @@ import { Truncate } from "@/tool/truncate" import { Plugin } from "@/plugin" import type { TaskPromptOps } from "@/tool/task" import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" -import { Effect } from "effect" +import { DateTime, Effect } from "effect" import { MessageV2 } from "./message-v2" import { Session } from "./session" import { SessionProcessor } from "./processor" @@ -20,6 +21,22 @@ import { PartID } from "./schema" import { EffectBridge } from "@/effect/bridge" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { EventV2Bridge } from "@/event-v2-bridge" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionMessage } from "@opencode-ai/core/session/message" + +export function mcpProgressToToolProgress(progress: Parameters[0]) { + const structured: Record = { + source: "mcp", + progress: progress.progress, + ...(progress.total !== undefined ? { total: progress.total } : {}), + ...(progress.message !== undefined ? { message: progress.message } : {}), + } + return { + structured, + content: progress.message ? [{ type: "text" as const, text: progress.message }] : [], + } +} export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { agent: Agent.Info @@ -37,6 +54,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { const registry = yield* ToolRegistry.Service const mcp = yield* MCP.Service const truncate = yield* Truncate.Service + const events = yield* EventV2Bridge.Service const context = (args: Record, options: ToolExecutionOptions): Tool.Context => ({ sessionID: input.session.id, @@ -132,7 +150,21 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { ) const result: Awaited>> = yield* Effect.gen(function* () { yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) - return yield* Effect.promise(() => execute(args, opts)) + const onprogress: ProgressCallback = (progress) => { + const state = mcpProgressToToolProgress(progress) + void run.promise( + events + .publish(SessionEvent.Tool.Progress, { + sessionID: ctx.sessionID, + assistantMessageID: SessionMessage.ID.make(input.processor.message.id), + callID: opts.toolCallId, + timestamp: DateTime.makeUnsafe(Date.now()), + ...state, + }) + .pipe(Effect.ignore), + ) + } + return yield* Effect.promise(() => execute(args, { ...opts, onprogress } as never)) }).pipe( Effect.withSpan("Tool.execute", { attributes: { diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 34304624e680..e2168ec3bef5 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -19,6 +19,12 @@ interface MockClientState { listResourcesCalls: number getPromptTimeout?: number readResourceTimeout?: number + callToolOptions?: { + resetTimeoutOnProgress?: boolean + signal?: AbortSignal + timeout?: number + onprogress?: (progress: { progress: number; total?: number; message?: string }) => void + } requestCalls: number listToolsShouldFail: boolean listToolsError: string @@ -234,6 +240,12 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ return { contents: [{ uri: params.uri, text: "test" }] } } + async callTool(_params: unknown, _schema: unknown, options?: MockClientState["callToolOptions"]) { + if (this._state) this._state.callToolOptions = options + options?.onprogress?.({ progress: 2, total: 4, message: "halfway" }) + return { content: [{ type: "text", text: "ok" }] } + } + async close() { if (this._state) this._state.closed = true } @@ -923,6 +935,51 @@ it.instance( { config: { mcp: {} } }, ) +it.instance( + "MCP tool calls include a progress handler so progress can reset timeouts", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "progress-server" + const serverState = getOrCreateClientState("progress-server") + serverState.capabilities = { tools: {} } + + yield* mcp.add("progress-server", { + type: "local", + command: ["echo", "test"], + timeout: 2500, + }) + + const tools = yield* mcp.tools() + const tool = tools["progress-server_test_tool"] + expect(tool).toBeDefined() + + const abort = new AbortController() + let progress: unknown + yield* Effect.promise(() => + tool.execute?.( + {}, + { + toolCallId: "call_1", + messages: [], + abortSignal: abort.signal, + onprogress: (value: unknown) => { + progress = value + }, + } as never, + ), + ) + + expect(serverState.callToolOptions?.resetTimeoutOnProgress).toBe(true) + expect(serverState.callToolOptions?.signal).toBe(abort.signal) + expect(serverState.callToolOptions?.timeout).toBe(2500) + expect(typeof serverState.callToolOptions?.onprogress).toBe("function") + expect(progress).toEqual({ progress: 2, total: 4, message: "halfway" }) + }), + ), + { config: { mcp: {} } }, +) + it.instance( "prompts() skips disconnected servers", () => diff --git a/packages/opencode/test/session/tools.test.ts b/packages/opencode/test/session/tools.test.ts new file mode 100644 index 000000000000..d9be1bc018af --- /dev/null +++ b/packages/opencode/test/session/tools.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "bun:test" +import { mcpProgressToToolProgress } from "../../src/session/tools" + +describe("SessionTools MCP progress", () => { + it("maps MCP progress into standard running tool progress", () => { + expect(mcpProgressToToolProgress({ progress: 2, total: 4, message: "halfway" })).toEqual({ + structured: { + source: "mcp", + progress: 2, + total: 4, + message: "halfway", + }, + content: [{ type: "text", text: "halfway" }], + }) + }) + + it("keeps progress display bounded when no message is provided", () => { + expect(mcpProgressToToolProgress({ progress: 2 })).toEqual({ + structured: { + source: "mcp", + progress: 2, + }, + content: [], + }) + }) +}) From e3a4862b280ed8692afee770f3357bb294589b22 Mon Sep 17 00:00:00 2001 From: Nomadcxx Date: Fri, 19 Jun 2026 14:07:47 +1000 Subject: [PATCH 2/2] fix(mcp): separate progress reports --- packages/core/src/session/event.ts | 17 + packages/core/src/session/message-updater.ts | 8 + packages/core/src/session/projector.ts | 1 + .../core/test/session-tool-progress.test.ts | 17 + packages/opencode/src/session/prompt.ts | 6 +- packages/opencode/src/session/tools.ts | 13 +- packages/opencode/test/mcp/lifecycle.test.ts | 19 +- packages/opencode/test/session/tools.test.ts | 19 +- packages/sdk/js/src/v2/gen/types.gen.ts | 67 ++++ packages/sdk/openapi.json | 313 ++++++++++++++++++ packages/tui/src/context/data.tsx | 10 + 11 files changed, 459 insertions(+), 31 deletions(-) diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 3472cc114a50..2cb1be5e577d 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -365,6 +365,22 @@ export namespace Tool { }) export type Progress = typeof Progress.Type + export const ProgressReport = EventV2.define({ + type: "session.next.tool.progress.report", + ...options, + schema: { + ...ToolBase, + report: Schema.Struct({ + progress: Schema.Number, + total: Schema.Number.pipe(Schema.optional), + message: Schema.String.pipe(Schema.optional), + source: Schema.String.pipe(Schema.optional), + }), + structured: Schema.Record(Schema.String, Schema.Any), + }, + }) + export type ProgressReport = typeof ProgressReport.Type + export const Success = EventV2.define({ type: "session.next.tool.success", ...options, @@ -489,6 +505,7 @@ const DurableDefinitions = [ Tool.Input.Ended, Tool.Called, Tool.Progress, + Tool.ProgressReport, Tool.Success, Tool.Failed, Reasoning.Started, diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index cf1eb2cedfda..f341e2d280a8 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -291,6 +291,14 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { } }) }, + "session.next.tool.progress.report": (event) => { + return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.state.structured = event.data.structured + } + }) + }, "session.next.tool.success": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestTool(draft, event.data.callID) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index caf63de78ac8..9a5c69b4e4f8 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -430,6 +430,7 @@ export const layer = Layer.effectDiscard( yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Progress, (event) => run(db, event)) + yield* events.project(SessionEvent.Tool.ProgressReport, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 09cc159a20ef..cabc5c5536fa 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -104,6 +104,22 @@ describe("Tool.Progress", () => { state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") }, }) + yield* service.publish(SessionEvent.Tool.ProgressReport, { + sessionID, + timestamp, + assistantMessageID, + callID: "call-success", + report: { progress: 2, total: 4, message: "halfway", source: "mcp" }, + structured: { source: "mcp", progress: 2, total: 4, message: "halfway" }, + }) + expect((yield* readAssistant).content[0]).toMatchObject({ + state: { + status: "running", + structured: { source: "mcp", progress: 2, total: 4, message: "halfway" }, + content: content("saved"), + }, + }) + const success = yield* service.publish(SessionEvent.Tool.Success, { sessionID, timestamp, @@ -153,6 +169,7 @@ describe("Tool.Progress", () => { .all() .pipe(Effect.orDie) expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) + expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.ProgressReport.type, 1)) expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1)) expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1)) }), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f0f1fc9b0750..d5d27b3685e8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1132,8 +1132,7 @@ export const layer = Layer.effect( }) const runLoop: (sessionID: SessionID) => Effect.Effect = - Effect.fn("SessionPrompt.run")( - function* (sessionID: SessionID) { + Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID) { const ctx = yield* InstanceState.context let structured: unknown let step = 0 @@ -1381,8 +1380,7 @@ export const layer = Layer.effect( yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope)) return yield* lastAssistant(sessionID) - }, - ) + }) const loop: (input: LoopInput) => Effect.Effect = Effect.fn("SessionPrompt.loop")(function* ( input: LoopInput, diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 0cbd20d63e3a..01c2f0ee6272 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -25,7 +25,7 @@ import { EventV2Bridge } from "@/event-v2-bridge" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" -export function mcpProgressToToolProgress(progress: Parameters[0]) { +export function mcpProgressToToolProgressReport(progress: Parameters[0]) { const structured: Record = { source: "mcp", progress: progress.progress, @@ -33,8 +33,13 @@ export function mcpProgressToToolProgress(progress: Parameters ...(progress.message !== undefined ? { message: progress.message } : {}), } return { + report: { + progress: progress.progress, + ...(progress.total !== undefined ? { total: progress.total } : {}), + ...(progress.message !== undefined ? { message: progress.message } : {}), + source: "mcp", + }, structured, - content: progress.message ? [{ type: "text" as const, text: progress.message }] : [], } } @@ -151,10 +156,10 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { const result: Awaited>> = yield* Effect.gen(function* () { yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) const onprogress: ProgressCallback = (progress) => { - const state = mcpProgressToToolProgress(progress) + const state = mcpProgressToToolProgressReport(progress) void run.promise( events - .publish(SessionEvent.Tool.Progress, { + .publish(SessionEvent.Tool.ProgressReport, { sessionID: ctx.sessionID, assistantMessageID: SessionMessage.ID.make(input.processor.message.id), callID: opts.toolCallId, diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index e2168ec3bef5..2a51fa71212b 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -957,17 +957,14 @@ it.instance( const abort = new AbortController() let progress: unknown yield* Effect.promise(() => - tool.execute?.( - {}, - { - toolCallId: "call_1", - messages: [], - abortSignal: abort.signal, - onprogress: (value: unknown) => { - progress = value - }, - } as never, - ), + tool.execute?.({}, { + toolCallId: "call_1", + messages: [], + abortSignal: abort.signal, + onprogress: (value: unknown) => { + progress = value + }, + } as never), ) expect(serverState.callToolOptions?.resetTimeoutOnProgress).toBe(true) diff --git a/packages/opencode/test/session/tools.test.ts b/packages/opencode/test/session/tools.test.ts index d9be1bc018af..5fc2b659af9b 100644 --- a/packages/opencode/test/session/tools.test.ts +++ b/packages/opencode/test/session/tools.test.ts @@ -1,26 +1,21 @@ import { describe, expect, it } from "bun:test" -import { mcpProgressToToolProgress } from "../../src/session/tools" +import { mcpProgressToToolProgressReport } from "../../src/session/tools" describe("SessionTools MCP progress", () => { - it("maps MCP progress into standard running tool progress", () => { - expect(mcpProgressToToolProgress({ progress: 2, total: 4, message: "halfway" })).toEqual({ - structured: { - source: "mcp", + it("maps MCP progress into a status report, not partial tool output", () => { + expect(mcpProgressToToolProgressReport({ progress: 2, total: 4, message: "halfway" })).toEqual({ + report: { progress: 2, total: 4, message: "halfway", + source: "mcp", }, - content: [{ type: "text", text: "halfway" }], - }) - }) - - it("keeps progress display bounded when no message is provided", () => { - expect(mcpProgressToToolProgress({ progress: 2 })).toEqual({ structured: { source: "mcp", progress: 2, + total: 4, + message: "halfway", }, - content: [], }) }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e2add116e0e5..bd81200f0de3 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -41,6 +41,7 @@ export type Event = | EventSessionNextToolInputEnded | EventSessionNextToolCalled | EventSessionNextToolProgress + | EventSessionNextToolProgressReport | EventSessionNextToolSuccess | EventSessionNextToolFailed | EventSessionNextRetried @@ -1122,6 +1123,25 @@ export type GlobalEvent = { content: Array } } + | { + id: string + type: "session.next.tool.progress.report" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + report: { + progress: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + total?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + message?: string + source?: string + } + structured: { + [key: string]: unknown + } + } + } | { id: string type: "session.next.tool.success" @@ -1654,6 +1674,7 @@ export type GlobalEvent = { | SyncEventSessionNextToolInputEnded | SyncEventSessionNextToolCalled | SyncEventSessionNextToolProgress + | SyncEventSessionNextToolProgressReport | SyncEventSessionNextToolSuccess | SyncEventSessionNextToolFailed | SyncEventSessionNextRetried @@ -3479,6 +3500,32 @@ export type SyncEventSessionNextToolProgress = { } } +export type SyncEventSessionNextToolProgressReport = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.progress.report.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + report: { + progress: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + total?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + message?: string + source?: string + } + structured: { + [key: string]: unknown + } + } + } +} + export type SyncEventSessionNextToolSuccess = { type: "sync" id: string @@ -4648,6 +4695,26 @@ export type EventSessionNextToolProgress = { } } +export type EventSessionNextToolProgressReport = { + id: string + type: "session.next.tool.progress.report" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + report: { + progress: number | "NaN" | "Infinity" | "-Infinity" + total?: number | "NaN" | "Infinity" | "-Infinity" + message?: string + source?: string + } + structured: { + [key: string]: unknown + } + } +} + export type EventSessionNextToolSuccess = { id: string type: "session.next.tool.success" diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b0cf1678c780..1fdcf235eafc 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -14586,6 +14586,9 @@ { "$ref": "#/components/schemas/EventSessionNextToolProgress" }, + { + "$ref": "#/components/schemas/EventSessionNextToolProgressReport" + }, { "$ref": "#/components/schemas/EventSessionNextToolSuccess" }, @@ -17963,6 +17966,104 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^evt_" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.progress.report"] + }, + "properties": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "assistantMessageID": { + "type": "string", + "pattern": "^msg_" + }, + "callID": { + "type": "string" + }, + "report": { + "type": "object", + "properties": { + "progress": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "message": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["progress"], + "additionalProperties": false + }, + "structured": { + "type": "object" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "report", "structured"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -19752,6 +19853,9 @@ { "$ref": "#/components/schemas/SyncEventSessionNextToolProgress" }, + { + "$ref": "#/components/schemas/SyncEventSessionNextToolProgressReport" + }, { "$ref": "#/components/schemas/SyncEventSessionNextToolSuccess" }, @@ -25093,6 +25197,125 @@ "required": ["type", "id", "syncEvent"], "additionalProperties": false }, + "SyncEventSessionNextToolProgressReport": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["sync"] + }, + "id": { + "type": "string", + "pattern": "^evt_" + }, + "syncEvent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["session.next.tool.progress.report.1"] + }, + "id": { + "type": "string", + "pattern": "^evt_" + }, + "seq": { + "type": "number" + }, + "aggregateID": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "assistantMessageID": { + "type": "string", + "pattern": "^msg_" + }, + "callID": { + "type": "string" + }, + "report": { + "type": "object", + "properties": { + "progress": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + }, + { + "type": "string", + "enum": ["Infinity", "-Infinity", "NaN"] + } + ] + }, + "message": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["progress"], + "additionalProperties": false + }, + "structured": { + "type": "object" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "report", "structured"], + "additionalProperties": false + } + }, + "required": ["type", "id", "seq", "aggregateID", "data"], + "additionalProperties": false + } + }, + "required": ["type", "id", "syncEvent"], + "additionalProperties": false + }, "SyncEventSessionNextToolSuccess": { "type": "object", "properties": { @@ -28961,6 +29184,96 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventSessionNextToolProgressReport": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^evt_" + }, + "type": { + "type": "string", + "enum": ["session.next.tool.progress.report"] + }, + "properties": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "assistantMessageID": { + "type": "string", + "pattern": "^msg_" + }, + "callID": { + "type": "string" + }, + "report": { + "type": "object", + "properties": { + "progress": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": ["NaN"] + }, + { + "type": "string", + "enum": ["Infinity"] + }, + { + "type": "string", + "enum": ["-Infinity"] + } + ] + }, + "message": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["progress"], + "additionalProperties": false + }, + "structured": { + "type": "object" + } + }, + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "report", "structured"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSessionNextToolSuccess": { "type": "object", "properties": { diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index bea6e001a43b..7e1a4a6bb398 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -333,6 +333,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ match.state.content = [...event.properties.content] }) break + case "session.next.tool.progress.report": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (match?.state.status !== "running") return + match.state.structured = event.properties.structured + }) + break case "session.next.tool.success": message.update(event.properties.sessionID, (draft) => { const match = message.latestTool(