refactor: simplify canonical tool result seams

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.
This commit is contained in:
Kit Langton
2026-07-22 17:22:58 -04:00
parent 162b70e28d
commit 0dc2509553
6 changed files with 16 additions and 17 deletions
+2 -2
View File
@@ -2,9 +2,9 @@
## Status
This document describes the agreed target design for V2 tools. It is an implementation plan, not documentation for the current API.
Implemented on this branch. This document is the historical plan kept for reviewer context; the authoritative semantic overview is [`specs/v2/tools.md`](../../../../specs/v2/tools.md), and where the two disagree the spec and code win. Delete this file at merge.
The current semantic overview remains in [`specs/v2/tools.md`](../../../../specs/v2/tools.md) until this plan is implemented. The plan's base is `origin/v2` at `b91dd78ab3`. Progress-event ephemerality already landed there (`5a9ed4d350`, `fix: make tool progress live-only`), including the failed-event partial snapshot this plan builds on. Delta-compressed progress is a later follow-up.
The plan's base was `origin/v2` at `b91dd78ab3`. Progress-event ephemerality already landed there (`5a9ed4d350`, `fix: make tool progress live-only`), including the failed-event partial snapshot this plan builds on. Delta-compressed progress is a later follow-up.
## Reader And Job
+1 -1
View File
@@ -8,7 +8,7 @@ import { FileSystem } from "../filesystem"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "../location"
import { Ripgrep } from "../ripgrep"
import { NonNegativeInt, RelativePath } from "../schema"
import { RelativePath } from "../schema"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
+1 -1
View File
@@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { Ripgrep } from "../ripgrep"
import { NonNegativeInt, RelativePath } from "../schema"
import { RelativePath } from "../schema"
import { Tool } from "./tool"
export const name = "grep"
+2 -5
View File
@@ -95,15 +95,12 @@ export type Definition<Input extends SchemaType<any>, Output extends SchemaType<
* Optional model projection. Receives the typed domain output. When absent, an
* encoded string becomes text and any other encoded JSON is serialized once.
*/
readonly toModelOutput?: (input: {
readonly toModelOutput?: (call: {
readonly input: InputValue<Input>
readonly output: OutputValue<Output>
}) => ModelOutput
/** Optional compact UI metadata. Receives the typed domain output. Absent means absent: no defaults. */
readonly toMetadata?: (input: {
readonly input: InputValue<Input>
readonly output: OutputValue<Output>
}) => Metadata
readonly toMetadata?: (call: { readonly input: InputValue<Input>; readonly output: OutputValue<Output> }) => Metadata
}
/** A dynamic tool's split result: a machine value for Code Mode and content for the model. */
+3
View File
@@ -414,6 +414,9 @@ export namespace Tool {
type: "session.tool.progress",
schema: {
...ToolBase,
// Deliberately looser than terminal metadata (JSON-only): live snapshots
// are ephemeral, and only the JSON-validated subset survives into a
// durable failure snapshot.
metadata: Schema.Record(Schema.String, Schema.Unknown),
content: Schema.Array(ToolContent),
},
+7 -8
View File
@@ -34,14 +34,13 @@ Every local tool receives the same concrete invocation context:
interface Tool.Context {
readonly sessionID: Session.ID
readonly agent: Agent.ID
readonly assistantMessageID: SessionMessage.ID
readonly toolCallID: string
readonly messageID: SessionMessage.ID
readonly callID: string
readonly progress: (update: Progress) => Effect.Effect<void>
}
```
`assistantMessageID` is the durable ID of the assistant message containing the call. The Session runner owns this association and supplies the complete context to the registry; the registry does not infer it.
Durable events call the invocation identifier `callID`; `Tool.Context.toolCallID` is the same value at the executor boundary.
`messageID` is the durable ID of the assistant message containing the call. The Session runner owns this association and supplies the complete context to the registry; the registry does not infer it. `callID` carries the same invocation identifier durable events use.
Decoded tool input is passed separately to `execute`. Raw provider input and domain services do not belong in the invocation context.
@@ -105,8 +104,8 @@ yield *
agent: context.agent,
source: {
type: "tool",
messageID: context.assistantMessageID,
callID: context.toolCallID,
messageID: context.messageID,
callID: context.callID,
},
action: "grep",
resources: [input.pattern],
@@ -160,7 +159,7 @@ Outcomes remain distinct:
- `ToolFailure` is an expected model-visible failure.
- Interruption cancels the invocation and is not a tool result.
- Unexpected typed errors and defects follow the runner's operational failure policy.
- Unknown and invalid calls become explicit model-visible settlement errors without invoking a handler.
- Unknown and invalid calls become explicit model-visible execution errors without invoking a handler.
Leaf tools translate only errors they deliberately classify as recoverable. Broad cause-catching around an executor is invalid because it consumes interruption and defects.