feat(agent,coding-agent): add prepareArguments hook for pre-validation argument preparation

Add AgentTool.prepareArguments and ToolDefinition.prepareArguments hook
that runs before schema validation in the agent loop. This lets tools
silently accept legacy argument shapes from resumed old sessions without
polluting the public schema.

The built-in edit tool uses this to fold legacy top-level oldText/newText
into edits[] when resuming sessions that predate the edits-only schema.

- AgentTool/ToolDefinition: typed prepareArguments returning Static<TParameters>
- agent-loop: prepareToolCallArguments() runs before validateToolArguments()
- edit tool: prepareEditArguments folds legacy fields, validateEditInput is strict
- Documented in extensions.md with edit-tool example
This commit is contained in:
Mario Zechner
2026-03-29 21:06:12 +02:00
parent fa890e3f94
commit b5f425ad15
10 changed files with 287 additions and 8 deletions

View File

@@ -44,6 +44,10 @@ const editSchema = Type.Object(
);
export type EditToolInput = Static<typeof editSchema>;
type LegacyEditToolInput = EditToolInput & {
oldText?: unknown;
newText?: unknown;
};
export interface EditToolDetails {
/** Unified diff of the changes made */
@@ -76,13 +80,24 @@ export interface EditToolOptions {
operations?: EditOperations;
}
function validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {
if (!Array.isArray(input.edits)) {
throw new Error(
"Edit tool input is invalid. edits must be an array of replacements in the form { oldText: string, newText: string }.",
);
function prepareEditArguments(input: unknown): EditToolInput {
if (!input || typeof input !== "object") {
return input as EditToolInput;
}
if (input.edits.length === 0) {
const args = input as LegacyEditToolInput;
if (typeof args.oldText !== "string" || typeof args.newText !== "string") {
return input as EditToolInput;
}
const edits = Array.isArray(args.edits) ? [...args.edits] : [];
edits.push({ oldText: args.oldText, newText: args.newText });
const { oldText: _oldText, newText: _newText, ...rest } = args;
return { ...rest, edits } as EditToolInput;
}
function validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {
if (!Array.isArray(input.edits) || input.edits.length === 0) {
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
}
return { path: input.path, edits: input.edits };
@@ -154,6 +169,7 @@ export function createEditToolDefinition(
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
],
parameters: editSchema,
prepareArguments: prepareEditArguments,
async execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, _ctx?) {
const { path, edits } = validateEditInput(input);
const absolutePath = resolveToCwd(path, cwd);