- Introduce `deferrable?: boolean` on AgentTool, CustomTool, and ToolDefinition. AstEditTool sets it to true; resolve is now injected only when at least one active tool is deferrable (previously unconditional). - Replace single-slot PendingActionStore (set/get/clear) with a LIFO stack (push/peek/pop/clear). Multiple deferrable tools can stage independent preview actions; resolve always consumes the topmost one first. - Wire pendingActionStore through discoverAndLoadCustomTools / loadCustomTools / CustomToolLoader so custom tools can call pushPendingAction(action) to register a resolve-compatible pending action with label, apply callback, optional details, and optional sourceToolName. - Export HIDDEN_TOOLS and ResolveTool from the SDK for manual tool composition. - Add CustomToolPendingAction type and pushPendingAction to CustomToolAPI. - Update createAgentSession to re-inject or remove resolve after the deferrable audit, consistent with createTools behavior. - Add LIFO resolve test, update existing tests (set -> push, get -> peek). - Add docs/resolve-tool-runtime.md covering PendingActionStore internals, built-in producer example, and custom tool usage guide.
4.0 KiB
Resolve tool runtime internals
This document explains how preview/apply workflows are modeled in coding-agent and how custom tools can participate via pushPendingAction.
Scope and key files
src/tools/resolve.tssrc/tools/pending-action.tssrc/tools/ast-edit.tssrc/extensibility/custom-tools/types.tssrc/extensibility/custom-tools/loader.tssrc/sdk.ts
What resolve does
resolve is a hidden tool that finalizes a pending preview action.
action: "apply"executes the pending action callback and persists changes.action: "discard"drops the pending action without applying.
If no pending action exists, resolve fails with:
No pending action to resolve. Nothing to apply or discard.
Pending actions are a stack (LIFO)
Pending actions are stored in PendingActionStore as a push/pop stack:
push(action)adds a new pending action on top.peek()inspects the current top action.pop()removes and returns the top action.hasPendingindicates whether the stack is non-empty.
resolve always consumes the topmost pending action first (pop()), so multiple preview-producing tools resolve in reverse order of registration.
Built-in producer example (ast_edit)
ast_edit previews structural replacements first. When the preview has replacements and is not applied yet, it pushes a pending action that contains:
- label (human-readable summary)
sourceToolName(ast_edit)apply()callback that reruns AST edit withdryRun: false
resolve(action="apply") later executes this callback.
Custom tools: pushPendingAction
Custom tools can register resolve-compatible pending actions through CustomToolAPI.pushPendingAction(...).
CustomToolPendingAction:
label: string(required)apply(): Promise<AgentToolResult<unknown>>(required)details?: unknown(optional)sourceToolName?: string(optional, defaults to"custom_tool")
Minimal usage example
import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
const factory: CustomToolFactory = pi => ({
name: "batch_rename_preview",
label: "Batch Rename Preview",
description: "Previews renames and defers commit to resolve",
parameters: pi.typebox.Type.Object({
files: pi.typebox.Type.Array(pi.typebox.Type.String()),
}),
async execute(_toolCallId, params) {
const previewSummary = `Prepared rename plan for ${params.files.length} files`;
pi.pushPendingAction({
label: `Batch rename: ${params.files.length} files`,
sourceToolName: "batch_rename_preview",
apply: async () => {
// apply writes here
return {
content: [{ type: "text", text: "Applied batch rename." }],
};
},
});
return {
content: [{ type: "text", text: `${previewSummary}. Call resolve to apply or discard.` }],
};
},
});
export default factory;
Runtime availability and failures
pushPendingAction is wired by the custom tool loader using the active session PendingActionStore.
If the runtime has no pending-action store, pushPendingAction throws:
Pending action store unavailable for custom tools in this runtime.
Tool-choice behavior
When PendingActionStore.hasPending is true, the agent runtime biases tool choice to resolve so pending previews are explicitly finalized before normal tool flow continues.
Developer guidance
- Use pending actions only for destructive or high-impact operations that should support explicit apply/discard.
- Keep
labelconcise and specific; it is shown in resolve renderer output. - Ensure
apply()is deterministic and idempotent enough for one-shot execution. - If your tool can stage multiple previews, remember LIFO semantics: latest pushed action resolves first.