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

111 lines
4.0 KiB
Markdown

# 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.ts`](../packages/coding-agent/src/tools/resolve.ts)
- [`src/tools/pending-action.ts`](../packages/coding-agent/src/tools/pending-action.ts)
- [`src/tools/ast-edit.ts`](../packages/coding-agent/src/tools/ast-edit.ts)
- [`src/extensibility/custom-tools/types.ts`](../packages/coding-agent/src/extensibility/custom-tools/types.ts)
- [`src/extensibility/custom-tools/loader.ts`](../packages/coding-agent/src/extensibility/custom-tools/loader.ts)
- [`src/sdk.ts`](../packages/coding-agent/src/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.
- `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
```ts
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.