Files
oh-my-pi/docs/resolve-tool-runtime.md
T
can1357 2867e1f4e3 feat(deps): added pi.zod exports and removed TypeBox package exports
- Added canonical `pi.zod` schema API exports and removed TypeBox package exports/imports.
- Migrated Tool schema typing from TypeBox to shared `TSchema`/Zod flow with legacy TypeBox compatibility.
- Updated AI provider adapters and MCP/agent builders to convert tool params through `toolWireSchema()`.
- Reworked schema validation from AJV to Zod-safe parsing with `fromTypeBox`, `toolWireSchema`, and meta schema checks.
2026-05-15 14:46:54 +02:00

5.3 KiB

Resolve tool runtime internals

This document explains how preview/apply workflows are modeled in coding-agent and how built-in or custom tools can participate via the tool-choice queue and pushPendingAction.

Scope and key files

What resolve does

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

  • action: "apply" executes the queued action's apply(reason) callback and returns that result with resolve metadata.
  • action: "discard" invokes reject(reason) if provided; otherwise returns Discarded: <label>. Reason: <reason>.

If no pending action exists, resolve fails with:

  • No pending action to resolve. Nothing to apply or discard.

Pending actions use the tool-choice queue

Preview producers call queueResolveHandler(...), which pushes a one-shot forced resolve directive onto the session tool-choice queue and adds a resolve-reminder steering message.

Runtime behavior:

  • the queued handler owns the pending apply/reject callbacks,
  • resolve looks up the current queue invoker with session.peekQueueInvoker(),
  • if the model rejects the forced tool choice, the queue directive is requeued,
  • resolve does not maintain a separate pending-action stack.

Multiple pending previews therefore follow the active tool-choice queue ordering, not an independent pending-action store.

Built-in producer example (ast_edit)

ast_edit previews structural replacements first. When the preview has replacements and is not applied yet, it queues a resolve handler 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: pushPendingAction

Custom tools can register resolve-compatible pending actions through CustomToolAPI.pushPendingAction(...). The custom tool loader forwards these actions to queueResolveHandler(...) when that hook is available.

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 exists on the public custom-tool type but is not currently forwarded by the loader into resolve metadata
  • 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.zod.object({
    files: pi.zod.array(pi.zod.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 through the active session's resolve queue hook.

If the runtime did not provide the resolve queue hook, pushPendingAction throws:

  • Pending action store unavailable for custom tools in this runtime.

Tool-choice behavior

When queueResolveHandler(...) registers a preview, the agent runtime forces a one-shot resolve tool choice 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(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 they are mediated by the tool-choice queue rather than a separate pending-action stack.