Files
oh-my-pi/docs/resolve-tool-runtime.md
T
can1357 c3d0bd9773 feat(coding-agent): deferrable tools, LIFO pending-action stack, custom tool pushPendingAction
- 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.
2026-03-01 02:57:31 +01:00

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

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.
  • 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.

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 with dryRun: 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 label concise 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.