fix(coding-agent): document mutable tool_call input closes #2611
This commit is contained in:
@@ -307,6 +307,68 @@ describe("agentLoop with AgentMessage", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should execute mutated beforeToolCall args without revalidation", async () => {
|
||||||
|
const toolSchema = Type.Object({ value: Type.String() });
|
||||||
|
const executed: Array<string | number> = [];
|
||||||
|
const tool: AgentTool<typeof toolSchema, { value: string | number }> = {
|
||||||
|
name: "echo",
|
||||||
|
label: "Echo",
|
||||||
|
description: "Echo tool",
|
||||||
|
parameters: toolSchema,
|
||||||
|
async execute(_toolCallId, params) {
|
||||||
|
executed.push(params.value as string | number);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `echoed: ${String(params.value)}` }],
|
||||||
|
details: { value: params.value as string | number },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context: AgentContext = {
|
||||||
|
systemPrompt: "",
|
||||||
|
messages: [],
|
||||||
|
tools: [tool],
|
||||||
|
};
|
||||||
|
|
||||||
|
const userPrompt: AgentMessage = createUserMessage("echo something");
|
||||||
|
|
||||||
|
const config: AgentLoopConfig = {
|
||||||
|
model: createModel(),
|
||||||
|
convertToLlm: identityConverter,
|
||||||
|
beforeToolCall: async ({ args }) => {
|
||||||
|
const mutableArgs = args as { value: string | number };
|
||||||
|
mutableArgs.value = 123;
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let callIndex = 0;
|
||||||
|
const streamFn = () => {
|
||||||
|
const stream = new MockAssistantStream();
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (callIndex === 0) {
|
||||||
|
const message = createAssistantMessage(
|
||||||
|
[{ type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "hello" } }],
|
||||||
|
"toolUse",
|
||||||
|
);
|
||||||
|
stream.push({ type: "done", reason: "toolUse", message });
|
||||||
|
} else {
|
||||||
|
const message = createAssistantMessage([{ type: "text", text: "done" }]);
|
||||||
|
stream.push({ type: "done", reason: "stop", message });
|
||||||
|
}
|
||||||
|
callIndex++;
|
||||||
|
});
|
||||||
|
return stream;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = agentLoop([userPrompt], context, config, undefined, streamFn);
|
||||||
|
for await (const _event of stream) {
|
||||||
|
// consume
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(executed).toEqual([123]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should execute tool calls in parallel and emit tool results in source order", async () => {
|
it("should execute tool calls in parallel and emit tool results in source order", async () => {
|
||||||
const toolSchema = Type.Object({ value: Type.String() });
|
const toolSchema = Type.Object({ value: Type.String() });
|
||||||
let firstResolved = false;
|
let firstResolved = false;
|
||||||
|
|||||||
@@ -565,17 +565,27 @@ Before `tool_call` runs, pi waits for previously emitted Agent events to finish
|
|||||||
|
|
||||||
In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
|
In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
|
||||||
|
|
||||||
|
`event.input` is mutable. Mutate it in place to patch tool arguments before execution.
|
||||||
|
|
||||||
|
Behavior guarantees:
|
||||||
|
- Mutations to `event.input` affect the actual tool execution
|
||||||
|
- Later `tool_call` handlers see mutations made by earlier handlers
|
||||||
|
- No re-validation is performed after your mutation
|
||||||
|
- Return values from `tool_call` only control blocking via `{ block: true, reason?: string }`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
pi.on("tool_call", async (event, ctx) => {
|
pi.on("tool_call", async (event, ctx) => {
|
||||||
// event.toolName - "bash", "read", "write", "edit", etc.
|
// event.toolName - "bash", "read", "write", "edit", etc.
|
||||||
// event.toolCallId
|
// event.toolCallId
|
||||||
// event.input - tool parameters
|
// event.input - tool parameters (mutable)
|
||||||
|
|
||||||
// Built-in tools: no type params needed
|
// Built-in tools: no type params needed
|
||||||
if (isToolCallEventType("bash", event)) {
|
if (isToolCallEventType("bash", event)) {
|
||||||
// event.input is { command: string; timeout?: number }
|
// event.input is { command: string; timeout?: number }
|
||||||
|
event.input.command = `source ~/.profile\n${event.input.command}`;
|
||||||
|
|
||||||
if (event.input.command.includes("rm -rf")) {
|
if (event.input.command.includes("rm -rf")) {
|
||||||
return { block: true, reason: "Dangerous command" };
|
return { block: true, reason: "Dangerous command" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -717,7 +717,12 @@ export interface CustomToolCallEvent extends ToolCallEventBase {
|
|||||||
input: Record<string, unknown>;
|
input: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fired before a tool executes. Can block. */
|
/**
|
||||||
|
* Fired before a tool executes. Can block.
|
||||||
|
*
|
||||||
|
* `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
|
||||||
|
* Later `tool_call` handlers see earlier mutations. No re-validation is performed after mutation.
|
||||||
|
*/
|
||||||
export type ToolCallEvent =
|
export type ToolCallEvent =
|
||||||
| BashToolCallEvent
|
| BashToolCallEvent
|
||||||
| ReadToolCallEvent
|
| ReadToolCallEvent
|
||||||
@@ -879,6 +884,7 @@ export interface ContextEventResult {
|
|||||||
export type BeforeProviderRequestEventResult = unknown;
|
export type BeforeProviderRequestEventResult = unknown;
|
||||||
|
|
||||||
export interface ToolCallEventResult {
|
export interface ToolCallEventResult {
|
||||||
|
/** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user