feat(plugin): add tool progress reporting
This commit is contained in:
parent
6039f54126
commit
915be9b54d
36 changed files with 196 additions and 92 deletions
|
|
@ -333,8 +333,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
messageID: event.messageID,
|
||||
callID: event.callID,
|
||||
input: event.input,
|
||||
}
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
|
|
@ -347,8 +347,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
messageID: event.messageID,
|
||||
callID: event.callID,
|
||||
input: event.input,
|
||||
result: event.result,
|
||||
output: event.output,
|
||||
|
|
|
|||
|
|
@ -256,10 +256,22 @@ function fromPromiseTool(tool: AnyTool) {
|
|||
if ("jsonSchema" in tool)
|
||||
return Tool.make({
|
||||
...tool,
|
||||
execute: (input, context) => Effect.promise(() => tool.execute(input, context)),
|
||||
execute: (input, context) =>
|
||||
Effect.promise(() =>
|
||||
tool.execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.runPromise(context.progress(update)),
|
||||
}),
|
||||
),
|
||||
})
|
||||
return Tool.make({
|
||||
...tool,
|
||||
execute: (input, context) => Effect.promise(() => tool.execute(input, context)),
|
||||
execute: (input, context) =>
|
||||
Effect.promise(() =>
|
||||
tool.execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.runPromise(context.progress(update)),
|
||||
}),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,8 +265,18 @@ const layer = Layer.effect(
|
|||
toolMaterialization.settle({
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
assistantMessageID,
|
||||
messageID: assistantMessageID,
|
||||
call: event,
|
||||
progress: (update) =>
|
||||
serialized(
|
||||
events.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID: session.id,
|
||||
assistantMessageID,
|
||||
callID: event.id,
|
||||
structured: { ...update.structured },
|
||||
content: [...update.content],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.flatMap((settlement) =>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const MANAGED_DIRECTORY = "tool-output"
|
|||
|
||||
export interface BoundInput {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly toolCallID: string
|
||||
readonly callID: string
|
||||
readonly output: ToolOutput
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ export const Plugin = {
|
|||
return Effect.gen(function* () {
|
||||
const permissionSource = {
|
||||
type: "tool" as const,
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
}
|
||||
if (input.oldString === input.newString) {
|
||||
return yield* new ToolFailure({
|
||||
|
|
|
|||
|
|
@ -113,12 +113,13 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
|
|||
const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1)
|
||||
const output = yield* settle(
|
||||
registration.tool,
|
||||
{ type: "tool-call", id: context.toolCallID, name, input },
|
||||
{ type: "tool-call", id: context.callID, name, input },
|
||||
{
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
assistantMessageID: context.assistantMessageID,
|
||||
toolCallID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
progress: context.progress,
|
||||
},
|
||||
).pipe(Effect.mapError((failure) => toolError(failure.message, failure)))
|
||||
const outputFileParts = outputFiles(output)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const Plugin = {
|
|||
},
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
const cwd = path.resolve(location.directory, input.path ?? ".")
|
||||
yield* fs
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const Plugin = {
|
|||
},
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
const target = path.resolve(location.directory, input.path ?? ".")
|
||||
const info = yield* fs
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export interface BeforeEvent {
|
|||
readonly tool: string
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly toolCallID: string
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
input: unknown
|
||||
}
|
||||
|
||||
|
|
@ -21,8 +21,8 @@ export interface AfterEvent {
|
|||
readonly tool: string
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly toolCallID: string
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly input: unknown
|
||||
result: ToolResultValue
|
||||
output?: ToolOutput
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ export const layer = Layer.effectDiscard(
|
|||
agent: context.agent,
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
},
|
||||
})
|
||||
const result = yield* mcp
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ export const Plugin = {
|
|||
return Effect.gen(function* () {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
}
|
||||
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
||||
const hunks = yield* Effect.try({
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const Plugin = {
|
|||
resources: ["*"],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError((error) => new ToolFailure({ message: "Permission denied: question", error })),
|
||||
|
|
@ -84,7 +84,7 @@ export const Plugin = {
|
|||
title: "Questions",
|
||||
metadata: {
|
||||
kind: "question",
|
||||
tool: { messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
tool: { messageID: context.messageID, callID: context.callID },
|
||||
},
|
||||
fields: [
|
||||
toField(input.questions[0], 0),
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ export const Plugin = {
|
|||
return Effect.gen(function* () {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.path, kind: "directory" })
|
||||
const external = target.externalDirectory
|
||||
|
|
|
|||
|
|
@ -19,8 +19,14 @@ import { toSessionError } from "../session/to-session-error"
|
|||
export type ExecuteInput = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly agent: AgentV2.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly call: ToolCall
|
||||
readonly progress?: (update: Progress) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Progress {
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
readonly content: ToolOutput["content"]
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
|
@ -65,8 +71,8 @@ const registryLayer = Layer.effect(
|
|||
tool: input.call.name,
|
||||
sessionID: input.sessionID,
|
||||
agent: input.agent,
|
||||
assistantMessageID: input.assistantMessageID,
|
||||
toolCallID: input.call.id,
|
||||
messageID: input.messageID,
|
||||
callID: input.call.id,
|
||||
input: input.call.input,
|
||||
}
|
||||
yield* toolHooks.runBefore(beforeEvent)
|
||||
|
|
@ -76,8 +82,22 @@ const registryLayer = Layer.effect(
|
|||
{
|
||||
sessionID: input.sessionID,
|
||||
agent: input.agent,
|
||||
assistantMessageID: input.assistantMessageID,
|
||||
toolCallID: input.call.id,
|
||||
messageID: input.messageID,
|
||||
callID: input.call.id,
|
||||
progress: (update) =>
|
||||
input.progress?.({
|
||||
structured: update.structured,
|
||||
content: (update.content ?? []).map((part) =>
|
||||
part.type === "text"
|
||||
? { type: "text" as const, text: part.text }
|
||||
: {
|
||||
type: "file" as const,
|
||||
uri: `data:${part.mime};base64,${part.data}`,
|
||||
mime: part.mime,
|
||||
name: part.name,
|
||||
},
|
||||
),
|
||||
}) ?? Effect.void,
|
||||
},
|
||||
).pipe(
|
||||
Effect.map((output) => ({ output })),
|
||||
|
|
@ -94,7 +114,7 @@ const registryLayer = Layer.effect(
|
|||
} else {
|
||||
const bounded = yield* resources.bound({
|
||||
sessionID: input.sessionID,
|
||||
toolCallID: input.call.id,
|
||||
callID: input.call.id,
|
||||
output: pending.output,
|
||||
})
|
||||
const result = ToolOutput.toResultValue(bounded.output)
|
||||
|
|
@ -111,8 +131,8 @@ const registryLayer = Layer.effect(
|
|||
tool: input.call.name,
|
||||
sessionID: input.sessionID,
|
||||
agent: input.agent,
|
||||
assistantMessageID: input.assistantMessageID,
|
||||
toolCallID: input.call.id,
|
||||
messageID: input.messageID,
|
||||
callID: input.call.id,
|
||||
input: beforeEvent.input,
|
||||
result: settlement.result,
|
||||
output: settlement.output,
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@ export const Plugin = {
|
|||
Effect.gen(function* () {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.workdir ?? ".", kind: "directory" })
|
||||
const external = target.externalDirectory
|
||||
|
|
@ -231,7 +231,7 @@ export const Plugin = {
|
|||
Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)),
|
||||
)
|
||||
const job = yield* runtime.job.start({
|
||||
id: context.toolCallID,
|
||||
id: context.callID,
|
||||
type: name,
|
||||
title: input.command,
|
||||
metadata: { sessionID: context.sessionID, shellID: info.id },
|
||||
|
|
@ -240,7 +240,7 @@ export const Plugin = {
|
|||
|
||||
if (input.background === true) {
|
||||
yield* runtime.job.background(job.id)
|
||||
yield* notifyWhenDone(context.sessionID, context.toolCallID, input.command)
|
||||
yield* notifyWhenDone(context.sessionID, context.callID, input.command)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
|
|
@ -255,7 +255,7 @@ export const Plugin = {
|
|||
.pipe(Effect.onInterrupt(() => runtime.job.cancel(job.id).pipe(Effect.ignore)))
|
||||
if (result?.type === "backgrounded") {
|
||||
yield* shell.timeout(info.id, 0)
|
||||
yield* notifyWhenDone(context.sessionID, context.toolCallID, input.command)
|
||||
yield* notifyWhenDone(context.sessionID, context.callID, input.command)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const Plugin = {
|
|||
save: [skill.id],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
const directory = path.dirname(skill.location)
|
||||
const files =
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ export const Plugin = {
|
|||
agent: context.agent,
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
},
|
||||
})
|
||||
.pipe(Effect.mapError((error) => new ToolFailure({ message: `Subagent denied: ${agent.id}`, error })))
|
||||
|
|
@ -160,6 +160,9 @@ export const Plugin = {
|
|||
)
|
||||
|
||||
const background = input.background === true
|
||||
yield* context.progress({
|
||||
structured: { sessionID: child.id, status: "running" },
|
||||
})
|
||||
|
||||
const run = Effect.gen(function* () {
|
||||
// The child session owns its agent/model (set at create); prompt only admits input.
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export const Plugin = {
|
|||
metadata: input,
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
|
||||
const { body, contentType } = yield* Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export const Plugin = {
|
|||
metadata: { ...input, provider },
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
})
|
||||
|
||||
const text =
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ export const Plugin = {
|
|||
Effect.gen(function* () {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.assistantMessageID,
|
||||
callID: context.toolCallID,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.path, kind: "file" })
|
||||
const external = target.externalDirectory
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue