From 3841955fbc7a898066445fe203105352902f253c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 1 Jul 2026 00:41:45 -0400 Subject: [PATCH] refactor(plugin): simplify tool result projection --- packages/core/src/tool/registry.ts | 19 ++++++------ packages/plugin/src/v2/effect/tool.ts | 44 ++++++++++++++++----------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts index 7bbc7d189a..f1c9ee543c 100644 --- a/packages/core/src/tool/registry.ts +++ b/packages/core/src/tool/registry.ts @@ -62,20 +62,21 @@ const registryLayer = Layer.effect( } if (advertised && registration.identity !== advertised) return { result: { type: "error" as const, value: `Stale tool call: ${input.call.name}` } } - const progress = (output: ToolOutput) => { - const emit = input.progress - if (!emit) return Effect.void - return resources.bound({ sessionID: input.sessionID, toolCallID: input.call.id, output }).pipe( - Effect.flatMap((bounded) => emit(bounded.output)), - Effect.ignore, - ) - } + const emitProgress = input.progress const pending = yield* settle(registration.tool, input.call, { sessionID: input.sessionID, agent: input.agent, assistantMessageID: input.assistantMessageID, toolCallID: input.call.id, - progress, + ...(emitProgress + ? { + progress: (output: ToolOutput) => + resources.bound({ sessionID: input.sessionID, toolCallID: input.call.id, output }).pipe( + Effect.flatMap((bounded) => emitProgress(bounded.output)), + Effect.ignore, + ), + } + : {}), }).pipe( Effect.map((output) => ({ output })), Effect.catchTag("LLM.ToolFailure", (failure) => diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts index 340ff45ade..07e455c4d4 100644 --- a/packages/plugin/src/v2/effect/tool.ts +++ b/packages/plugin/src/v2/effect/tool.ts @@ -37,11 +37,24 @@ export interface State { } export interface Result extends State { - readonly [ResultTypeId]: Output + readonly [ResultTypeId]: true } -export const result = (state: State): Result => - Object.freeze({ ...state, [ResultTypeId]: state.output }) +class ResultValue implements Result { + readonly output: Output + readonly content?: ReadonlyArray + + get [ResultTypeId]() { + return true as const + } + + constructor(state: State) { + this.output = state.output + this.content = state.content + } +} + +export const result = (state: State): Result => Object.freeze(new ResultValue(state)) export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { name: Schema.String, @@ -62,8 +75,6 @@ type Config, Output extends SchemaType> = { ) => Effect.Effect | Result>, ToolFailure> } -export type DynamicOutput = Result - /** * Config for a tool whose input shape is a raw JSON Schema not known at compile * time (MCP servers, plugin manifests). Input is passed through as `unknown`; @@ -103,11 +114,8 @@ function makeTyped, Output extends SchemaType const tool = Object.freeze({}) as Definition const definitions = new Map() - const project = ( - value: Schema.Schema.Type | Result>, - ): Effect.Effect => { - const state = stateOf(value) - return Schema.encodeEffect(config.output)(state.output).pipe( + const projectState = (state: State>): Effect.Effect => + Schema.encodeEffect(config.output)(state.output).pipe( Effect.map((output) => ToolOutput.make(output, contentOf(output, state.content))), Effect.mapError( (error) => @@ -116,7 +124,9 @@ function makeTyped, Output extends SchemaType }), ), ) - } + + const project = (value: Schema.Schema.Type | Result>) => + projectState(stateOf(value)) runtimes.set(tool, { definition: (name) => { @@ -139,7 +149,9 @@ function makeTyped, Output extends SchemaType .execute(input, { ...context, progress: (state) => - project(result(state)).pipe(Effect.flatMap(context.progress ?? (() => Effect.void)), Effect.ignore), + context.progress + ? projectState(state).pipe(Effect.flatMap(context.progress), Effect.ignore) + : Effect.void, }) .pipe(Effect.flatMap(project)), ), @@ -151,10 +163,8 @@ function makeTyped, Output extends SchemaType function makeDynamic(config: DynamicConfig): AnyTool { const tool = Object.freeze({}) as AnyTool const definitions = new Map() - const project = (value: unknown) => { - const state = stateOf(value) - return ToolOutput.make(state.output, contentOf(state.output, state.content)) - } + const projectState = (state: State) => ToolOutput.make(state.output, contentOf(state.output, state.content)) + const project = (value: unknown) => projectState(stateOf(value)) runtimes.set(tool, { definition: (name) => { const cached = definitions.get(name) @@ -172,7 +182,7 @@ function makeDynamic(config: DynamicConfig): AnyTool { config .execute(call.input, { ...context, - progress: (state) => context.progress?.(project(result(state))).pipe(Effect.ignore) ?? Effect.void, + progress: (state) => context.progress?.(projectState(state)).pipe(Effect.ignore) ?? Effect.void, }) .pipe(Effect.map(project)), })