feat(plugin): add tool result content API
This commit is contained in:
parent
1ce607c230
commit
3e583a3f5a
25 changed files with 316 additions and 197 deletions
|
|
@ -45,8 +45,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(Tool.result({ output: { text }, content: [{ type: "text", text }] })),
|
||||
})
|
||||
return permission ? Tool.withPermission(tool, permission) : tool
|
||||
}
|
||||
|
|
@ -237,13 +236,21 @@ describe("ToolRegistry", () => {
|
|||
it.effect("passes complete invocation identity to the canonical handler", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
const contexts: Tool.Context[] = []
|
||||
const contexts: Array<Omit<Tool.Context, "progress">> = []
|
||||
yield* service.register({
|
||||
context: Tool.make({
|
||||
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({
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
assistantMessageID: context.assistantMessageID,
|
||||
toolCallID: context.toolCallID,
|
||||
})
|
||||
}).pipe(Effect.as({ ok: true })),
|
||||
}),
|
||||
})
|
||||
yield* executeTool(service, {
|
||||
|
|
@ -255,6 +262,62 @@ describe("ToolRegistry", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("emits bounded progress snapshots from the tool context", () =>
|
||||
Effect.gen(function* () {
|
||||
bounds.length = 0
|
||||
const service = yield* ToolRegistry.Service
|
||||
const progress: unknown[] = []
|
||||
yield* service.register({
|
||||
progress: Tool.make({
|
||||
description: "Progress",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ text: Schema.String }),
|
||||
execute: (_, context) =>
|
||||
context
|
||||
.progress({ output: { text: "loading" }, content: [{ type: "text", text: "loading" }] })
|
||||
.pipe(Effect.as(Tool.result({ output: { text: "done" }, content: [{ type: "text", text: "done" }] }))),
|
||||
}),
|
||||
})
|
||||
const settled = yield* settleTool(service, {
|
||||
sessionID,
|
||||
...identity,
|
||||
call: { type: "tool-call", id: "call-progress", name: "progress", input: {} },
|
||||
progress: (output) => Effect.sync(() => progress.push(output)),
|
||||
})
|
||||
|
||||
expect(progress).toEqual([{ structured: { text: "loading" }, content: [{ type: "text", text: "loading" }] }])
|
||||
expect(bounds.map((input) => input.toolCallID)).toEqual(["call-progress", "call-progress"])
|
||||
expect(settled).toMatchObject({
|
||||
result: { type: "text", value: "done" },
|
||||
output: { structured: { text: "done" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not treat plain output-content objects as result envelopes", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
yield* service.register({
|
||||
literal: Tool.make({
|
||||
description: "Literal output object",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ output: Schema.String, content: Schema.Array(Schema.String) }),
|
||||
execute: () => Effect.succeed({ output: "value", content: ["not model content"] }),
|
||||
}),
|
||||
})
|
||||
expect(
|
||||
yield* settleTool(service, {
|
||||
sessionID,
|
||||
...identity,
|
||||
call: { type: "tool-call", id: "call-literal", name: "literal", input: {} },
|
||||
}),
|
||||
).toMatchObject({
|
||||
result: { type: "json", value: { output: "value", content: ["not model content"] } },
|
||||
output: { structured: { output: "value", content: ["not model content"] }, content: [] },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("encodes output and applies generic settlement bounding", () =>
|
||||
Effect.gen(function* () {
|
||||
bounds.length = 0
|
||||
|
|
@ -290,8 +353,10 @@ 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(Tool.result({ output: { value }, content: [{ type: "text", text: String(value === "yes") }] })),
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
@ -410,8 +475,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(Tool.result({ output: { text }, content: [{ type: "text", text }] })),
|
||||
),
|
||||
}),
|
||||
})
|
||||
.pipe(Scope.provide(scope))
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ const recoveryModel = Model.make({
|
|||
provider: "fake",
|
||||
route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
|
||||
})
|
||||
const authorizations: Tool.Context[] = []
|
||||
const authorizations: Array<Omit<Tool.Context, "progress">> = []
|
||||
const executions: string[] = []
|
||||
const permission = Layer.succeed(
|
||||
PermissionV2.Service,
|
||||
|
|
@ -132,10 +132,14 @@ 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)
|
||||
authorizations.push({
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
assistantMessageID: context.assistantMessageID,
|
||||
toolCallID: context.toolCallID,
|
||||
})
|
||||
executions.push(text)
|
||||
activeToolExecutions++
|
||||
maxActiveToolExecutions = Math.max(maxActiveToolExecutions, activeToolExecutions)
|
||||
|
|
@ -143,7 +147,7 @@ const echo = Layer.effectDiscard(
|
|||
yield* Deferred.succeed(toolExecutionsStarted, undefined)
|
||||
}
|
||||
if (toolExecutionGate) yield* Deferred.await(toolExecutionGate)
|
||||
return { text }
|
||||
return Tool.result({ output: { text }, content: [{ type: "text", text }] })
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))),
|
||||
}),
|
||||
defect: Tool.make({
|
||||
|
|
@ -580,7 +584,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* setup
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const session = yield* SessionV2.Service
|
||||
const contexts: Tool.Context[] = []
|
||||
const contexts: Array<Omit<Tool.Context, "progress">> = []
|
||||
yield* registry.register({
|
||||
location_context: Tool.make({
|
||||
description: "Read application context",
|
||||
|
|
@ -588,7 +592,12 @@ describe("SessionRunnerLLM", () => {
|
|||
output: Schema.Struct({ answer: Schema.String }),
|
||||
execute: ({ query }, context) =>
|
||||
Effect.sync(() => {
|
||||
contexts.push(context)
|
||||
contexts.push({
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
assistantMessageID: context.assistantMessageID,
|
||||
toolCallID: context.toolCallID,
|
||||
})
|
||||
return { answer: query.toUpperCase() }
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ test("keeps locked deferred parity TODOs visible", async () => {
|
|||
"Replace token-based command-argument external-directory advisories with parser-based detection.",
|
||||
"Restore PowerShell and cmd-specific invocation/path handling on Windows.",
|
||||
"Add plugin shell.env environment augmentation once V2 plugin hooks exist.",
|
||||
"Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.",
|
||||
"Stream shell progress checkpoints without persisting every stdout/stderr chunk.",
|
||||
"Persist job status and define restart recovery before exposing remote observation.",
|
||||
"Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it.",
|
||||
"Revisit binary output handling if stdout/stderr decoding is text-only.",
|
||||
|
|
|
|||
|
|
@ -190,7 +190,8 @@ describe("SubagentTool", () => {
|
|||
},
|
||||
})
|
||||
|
||||
expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText })
|
||||
expect(settled.output?.structured).toMatchObject({ status: "completed" })
|
||||
expect(settled.output?.content).toEqual([{ type: "text", text: childText }])
|
||||
const child = yield* sessions.get(outputSessionID(settled.output?.structured))
|
||||
expect(child).toMatchObject({
|
||||
parentID: parent.id,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ describe("WebFetchTool registration", () => {
|
|||
expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
|
||||
result: { type: "text", value: "hello" },
|
||||
output: {
|
||||
structured: { url, contentType: "text/plain", format: "text", output: "hello" },
|
||||
structured: { url, contentType: "text/plain", format: "text" },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ describe("WebSearchTool registration", () => {
|
|||
expect(settled).toEqual({
|
||||
result: { type: "text", value: "parallel results" },
|
||||
output: {
|
||||
structured: { provider: "parallel", text: "parallel results" },
|
||||
structured: { provider: "parallel" },
|
||||
content: [{ type: "text", text: "parallel results" }],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue