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"executesapply(reason)on the pending action and persists changes.action: "discard"invokesreject(reason)if provided; otherwise drops the action with a default "Discarded" message.
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(reason: string)callback that reruns AST edit withdryRun: false
resolve(action="apply", reason="...") passes reason into this callback.
Custom tools: pushPendingAction
Custom tools can register resolve-compatible pending actions through CustomToolAPI.pushPendingAction(...).
CustomToolPendingAction:
label: string(required)apply(reason: string): Promise<AgentToolResult<unknown>>(required) — invoked on apply;reasonis the string passed toresolvereject?(reason: string): Promise<AgentToolResult<unknown> | undefined>(optional) — invoked on discard; return value replaces the default "Discarded" message if provideddetails?: 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 (reason) => {
// apply writes here
return {
content: [{ type: "text", text: `Applied batch rename. Reason: ${reason}` }],
};
},
reject: async (reason) => {
// optional: cleanup or notify on discard
return {
content: [{ type: "text", text: `Discarded batch rename. Reason: ${reason}` }],
};
},
});
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(reason)is deterministic and idempotent enough for one-shot execution;reasonis informational and should not change behavior. - Implement
reject(reason)when the discard needs cleanup (temp state, locks, notifications); omit it for stateless previews where the default message suffices. - If your tool can stage multiple previews, remember LIFO semantics: latest pushed action resolves first.