Skip to content

Resolve Tool Runtime Internals

This document explains how preview/apply workflows are modeled in coding-agent and how custom tools can participate via pushPendingAction.

resolve is a hidden tool that finalizes a pending preview action.

  • action: "apply" executes apply(reason) on the pending action and persists changes.
  • action: "discard" invokes reject(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 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.
  • hasPending indicates 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.

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 with dryRun: false

resolve(action="apply", reason="...") passes reason into this callback.

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; reason is the string passed to resolve
  • reject?(reason: string): Promise<AgentToolResult<unknown> | undefined> (optional) — invoked on discard; return value replaces the default “Discarded” message if provided
  • details?: unknown (optional)
  • sourceToolName?: string (optional, defaults to "custom_tool")
import type { CustomToolFactory } from "@f5xc-salesdemos/xcsh";
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;

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.

When PendingActionStore.hasPending is true, the agent runtime biases tool choice to resolve so pending previews are explicitly finalized before normal tool flow continues.

  • Use pending actions only for destructive or high-impact operations that should support explicit apply/discard.
  • Keep label concise and specific; it is shown in resolve renderer output.
  • Ensure apply(reason) is deterministic and idempotent enough for one-shot execution; reason is 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.