refactor(plugin): return tool output directly

This commit is contained in:
Dax Raad 2026-07-04 17:09:51 +00:00 committed by 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
commit 8f7c6c33d9
21 changed files with 235 additions and 172 deletions

View file

@ -108,7 +108,7 @@ describe("PluginV2", () => {
description: "Plugin tool",
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ ok: true }),
execute: () => Effect.succeed({ structured: { ok: true }, content: [] }),
}),
})
.pipe(Effect.orDie),
@ -146,7 +146,8 @@ describe("PluginV2", () => {
description: "Echo",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })),
execute: ({ text }) =>
Effect.sync(() => executed.push({ text })).pipe(Effect.as({ structured: { text }, content: [] })),
}),
})
.pipe(Effect.orDie)

View file

@ -46,8 +46,7 @@ const make = (permission?: string) => {
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
execute: ({ text }) => Effect.succeed({ text }),
toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
execute: ({ text }) => Effect.succeed({ structured: { text }, content: [{ type: "text" as const, text }] }),
})
return permission ? Tool.withPermission(tool, permission) : tool
}
@ -244,7 +243,8 @@ describe("ToolRegistry", () => {
description: "Context",
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: (_, context) => Effect.sync(() => contexts.push(context)).pipe(Effect.as({ ok: true })),
execute: (_, context) =>
Effect.sync(() => contexts.push(context)).pipe(Effect.as({ structured: { ok: true }, content: [] })),
}),
})
yield* executeTool(service, {
@ -276,7 +276,7 @@ describe("ToolRegistry", () => {
}),
)
it.effect("enforces transformed codecs at execution and projection boundaries", () =>
it.effect("validates structured output while preserving executor-provided model content", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
const executed: string[] = []
@ -291,18 +291,23 @@ describe("ToolRegistry", () => {
description: "Transform values",
input: Schema.Struct({ value: Transformed }),
output: Schema.Struct({ value: Transformed }),
execute: ({ value }) => Effect.sync(() => executed.push(value)).pipe(Effect.as({ value })),
toModelOutput: ({ output }) => [{ type: "text", text: String(output.value) }],
execute: ({ value }) =>
Effect.sync(() => executed.push(value)).pipe(
Effect.as({ structured: { value }, content: [{ type: "text" as const, text: value }] }),
),
}),
})
expect(
yield* executeTool(service, {
yield* settleTool(service, {
sessionID,
...identity,
call: { type: "tool-call", id: "transformed", name: "transformed", input: { value: true } },
}),
).toEqual({ type: "text", value: "true" })
).toEqual({
result: { type: "text", value: "yes" },
output: { structured: { value: true }, content: [{ type: "text", text: "yes" }] },
})
expect(executed).toEqual(["yes"])
expect(
yield* executeTool(service, {
@ -329,7 +334,7 @@ describe("ToolRegistry", () => {
}),
),
}),
execute: () => Effect.succeed({ value: "invalid" }),
execute: () => Effect.succeed({ structured: { value: "invalid" }, content: [] }),
}),
})
expect(
@ -411,8 +416,10 @@ describe("ToolRegistry", () => {
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
execute: ({ text }) =>
Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(release)), Effect.as({ text })),
toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
Deferred.succeed(started, undefined).pipe(
Effect.andThen(Deferred.await(release)),
Effect.as({ structured: { text }, content: [{ type: "text" as const, text }] }),
),
}),
})
.pipe(Scope.provide(scope))

View file

@ -135,7 +135,6 @@ const echo = Layer.effectDiscard(
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
execute: ({ text }, context) =>
Effect.gen(function* () {
authorizations.push(context)
@ -146,7 +145,7 @@ const echo = Layer.effectDiscard(
yield* Deferred.succeed(toolExecutionsStarted, undefined)
}
if (toolExecutionGate) yield* Deferred.await(toolExecutionGate)
return { text }
return { structured: { text }, content: [{ type: "text" as const, text }] }
}).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))),
}),
defect: Tool.make({
@ -161,7 +160,7 @@ const echo = Layer.effectDiscard(
description: "Produce output that cannot be persisted",
input: Schema.Struct({}),
output: Schema.Any,
execute: () => Effect.succeed({ big: 1n }),
execute: () => Effect.succeed({ structured: { big: 1n }, content: [] }),
}),
}),
),
@ -608,7 +607,7 @@ describe("SessionRunnerLLM", () => {
execute: ({ query }, context) =>
Effect.sync(() => {
contexts.push(context)
return { answer: query.toUpperCase() }
return { structured: { answer: query.toUpperCase() }, content: [] }
}),
}),
})
@ -2834,7 +2833,9 @@ describe("SessionRunnerLLM", () => {
input: Schema.Struct({}),
output: Schema.Struct({}),
execute: (_, context) =>
questions.ask({ sessionID: context.sessionID, questions: [] }).pipe(Effect.as({}), Effect.orDie),
questions
.ask({ sessionID: context.sessionID, questions: [] })
.pipe(Effect.as({ structured: {}, content: [] }), Effect.orDie),
}),
})
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Ask then stop" }), resume: false })