Skip to content

refactor(tools): unify tool APIs and result handling - #38367

Merged
kitlangton merged 27 commits into
v2from
canonical-tool-output
Jul 23, 2026
Merged

kitlangton merged 27 commits into
v2from
canonical-tool-output

Conversation

@kitlangton

@kitlangton kitlangton commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

What

Unifies the V2 tool stack from authored tools through provider schemas, local execution, hooks, clients, and persisted terminal results.

The canonical vocabulary is now:

  • Tool.Tool for an authored tool and Tool.Any for its erased form.
  • tool for runtime values, including registration.tool.
  • execute as the sole local execution verb.
  • LLM.ToolDefinition for the inert model-facing schema.

A terminal response separates schema-validated machine output, model-visible content, and compact UI metadata. One registry boundary validates and bounds those values, runs hooks, and returns a canonical completed | error result consumed by Code Mode, MCP, plugins, the runner, CLI, ACP, TUI, simulation, and durable session state.

Shell follows the same split: progress publishes compact metadata such as shellID; paginated shell output remains outside durable progress events; terminal completion or failure owns model content.

How

  • Plugin API: aligns Effect and Promise tools around Tool.make({ description, input, output?, execute }), Tool.Tool, and Tool.Any. The standalone CLI exposes the same public tool facade to both plugin runtimes.
  • Registration: keeps reusable tools separate from effective names, namespaces, Code Mode placement, and permission identity. Registrations are normalized, collision-checked, scoped, and captured per model request.
  • Execution: validates decoded input and encoded output, converts responses into non-empty model content, validates compact JSON metadata, bounds stored content, and revalidates hook mutations.
  • Code Mode: receives declared, schema-validated machine output instead of reconstructing values from model content or UI projections.
  • Persistence: stores canonical terminal content, metadata, and errors in V2 session messages and events. The migration rewrites projected session_message rows only and leaves legacy durable events untouched.
  • Bedrock: final steps retain tool definitions in toolConfig.tools while omitting unsupported toolChoice: none, preserving the tools -> system -> messages prompt-cache prefix.
  • MCP: reconciles one registration batch per server while retaining the same namespaced permission identity per tool.

Scope

  • Legacy session.tool.success.1 and session.tool.failed.1 durable rows are intentionally unchanged. Cleanup before V2 stable is tracked in V2: remove V1 event surface after cutover (tracking) #35054 (comment).
  • Bedrock Converse still has no native none tool choice; this PR omits only that unsupported field rather than removing tool definitions.
  • Broader resumable migration, binary payload bounding, and tool-display performance work are not included.

Testing

  • Monorepo push-hook typecheck: 33 packages passed.
  • Package typechecks: AI, Plugin, Code Mode, Core, CLI, SDK Next, and Simulation.
  • Plugin declaration build: tsc -p tsconfig.build.json.
  • Plugin tests: 8 passed.
  • Code Mode tests: 1,086 passed.
  • Focused Core tool, MCP, registry, and migration tests: 101 passed.
  • CLI import-boundary tests: 4 passed.
  • Bedrock Converse tests: 34 passed.
  • Provider tests: 84 passed.
  • Shell tool stress rerun: 300 passed across 20 reruns.
  • Standalone Node build completed Linux arm64, Linux x64, and macOS arm64 bundles before the remaining target matrix exceeded the local command timeout.

Flow

flowchart LR
  A[Tool.make] --> B[Scoped registration]
  B --> C[Registry snapshot]
  C --> D[LLM.ToolDefinition]
  D --> E[Provider tool call]
  E --> F[Registry execute]
  F --> G[Validated output]
  F --> H[Bounded content]
  F --> I[Compact metadata]
  G --> J[Code Mode]
  H --> K[Model and durable state]
  I --> L[UI and clients]
Loading

@kitlangton
kitlangton force-pushed the canonical-tool-output branch from 977a1f0 to ed35983 Compare July 23, 2026 03:32
@kitlangton kitlangton changed the title feat(core): canonical tool results refactor(core): canonicalize tool outcomes Jul 23, 2026
@kitlangton
kitlangton force-pushed the canonical-tool-output branch from 2db0991 to e22395b Compare July 23, 2026 16:59
Materializing every assistant row at once was measured at ~4.9GB RSS and 105s on an 8.8GB production database. Keyset-paginated batches of 1000 inside the same transaction cut this to 843MB and 37s.
edit and patch never declared toMetadata, so the TUI diff renderer had nothing to draw for new calls, and the row migration dropped historical structured payloads (including subagent session joins). Both tools now emit { files }, migrated terminal states carry old structured objects as canonical metadata, and undecodable rows are skipped instead of failing the migration on every startup.
…tches

Hosted provider result payloads are provider-format state, not model state. Gating providerResultState reuse on exact model identity broke Anthropic hosted replay after a model switch within the provider; the fallback result value is derived display content, not the typed block payload. Result-state reuse now requires only provider identity.
An untyped JS plugin after-hook could clear failed-call content to an empty array or set non-JSON metadata, producing an unencodable durable event that wedged the tool part in running. The JS boundary now revalidates through one shared Tool.jsonMetadata guard, which also applies the byte bound to failure-snapshot metadata that only the success path enforced before. Consolidates the duplicated stringify and metadata-validation helpers into tool.ts.
Read's canonical model content is the JSON page envelope, so ACP clients and text displays showed raw JSON instead of file text. toolOutputText and the ACP tool update now unwrap the envelope back to the clean content, replacing the dead metadata-based extraction.
Review cleanups: dead imports, contentFrom happy-path flattening, codemodeTool shadow rename, call-site param naming for toModelOutput/toMetadata, spec Context fields matching the code, progress-metadata leniency comment, and PLAN.md marked implemented.
@kitlangton
kitlangton force-pushed the canonical-tool-output branch from 664407f to 34ae3e3 Compare July 23, 2026 20:05
@kitlangton kitlangton changed the title refactor(core): canonicalize tool outcomes refactor(tools): unify tool APIs and result handling Jul 23, 2026
@kitlangton
kitlangton enabled auto-merge (squash) July 23, 2026 20:56
@kitlangton
kitlangton merged commit 79c1544 into v2 Jul 23, 2026
9 checks passed
@kitlangton
kitlangton deleted the canonical-tool-output branch July 23, 2026 21:13
jinhuang712 pushed a commit to jinhuang712/opencode that referenced this pull request Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant