diff --git a/docs/cordis-catalog/events-and-services.md b/docs/cordis-catalog/events-and-services.md index b3d784e6fa..6cf03aefe4 100644 --- a/docs/cordis-catalog/events-and-services.md +++ b/docs/cordis-catalog/events-and-services.md @@ -337,7 +337,7 @@ A tool was registered or unregistered (the available tool set changed). 'tools/change'(): void ``` -Source: [`packages/core/tools/src/index.ts:48`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:66`](../../packages/core/tools/src/index.ts) #### `tools/execute` — waterfall @@ -349,7 +349,7 @@ Waterfall around every tool execution — the single seam where sandbox, permiss Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) -Source: [`packages/core/tools/src/index.ts:43`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:61`](../../packages/core/tools/src/index.ts) ### `web/*` @@ -559,7 +559,7 @@ async execute(exec: ToolExecution): Promise Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) -Source: [`packages/core/tools/src/index.ts:366`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:199`](../../packages/core/tools/src/index.ts) ### `ctx.web` — `WebService` diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 355eefc2e7..f534ea1cdb 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -121,4 +121,4 @@ How a tool wants its call shown in a UI (an editor tool-call card, a CLI log lin `ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`) and `FileDiff` (`{ path, oldText, newText }`) are the shared file-card vocabulary. The design is pinned in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); the ACP bridge maps a `diff` card to a `{ type: 'diff' }` content block, a `terminal` card to the `_meta` terminal convention, and relativizes a file card's title against the session cwd. -The full presentation field docs live in [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts). The bash tool's own schemas (`bash`/`bash_output`/`bash_kill`) and the executor they drive are on [bash.md](bash.md). +The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The bash tool's own schemas (`bash`/`bash_output`/`bash_kill`) and the executor they drive are on [bash.md](bash.md). diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 76f67291ed..1a0e132e3a 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -12,6 +12,7 @@ import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' import { HarnessError } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-system-prompt' +import type { ToolCallView, ToolResultView } from './presentation.ts' export { defineTool, @@ -26,6 +27,23 @@ export { type JsonSchemaObject, } from './schema.ts' +// The render-intent vocabulary a tool declares via `presentCall`/`presentResult` +// lives in its own UI-facing module; re-export it so `@deepseek-ai/dsh-tools` +// stays the single public surface for consumers (producers + the ACP bridge). +export type { + ToolCallKind, + FileLocation, + FileDiff, + ToolCallView, + GenericCallView, + TerminalCallView, + DiffCallView, + ToolResultView, + GenericResultView, + TerminalResultView, + DiffResultView, +} from './presentation.ts' + declare module 'cordis' { interface Context { tools: ToolRegistry @@ -54,191 +72,6 @@ declare module 'cordis' { // parallel execution — Claude Code partitions read-only tools; phase 1 // executes sequentially). -/** - * Category of a tool call, used by a UI to pick an icon / treatment. A neutral - * vocabulary owned here (NOT an ACP type) so tools describe themselves without - * depending on any client protocol; a UI bridge maps it to its own enum. The - * member set mirrors the common ACP `ToolKind` values; `other` is the default. - */ -export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other' - -/** - * A file location a tool reads or modifies, so a capable UI can "follow along" — - * highlight or jump to the file (and line) as the tool runs. Provider-neutral; - * a UI bridge maps it to its own affordance (the ACP bridge forwards it as - * `tool_call.locations`). `path` is what the tool operated on (the model-facing - * path); `line` is an optional 1-based line to focus (e.g. a read's offset). - */ -export interface FileLocation { - path: string - line?: number -} - -/** - * A single-file change a tool is about to make, for a UI that renders inline - * diffs (an editor's diff card). Provider-neutral; the ACP bridge forwards it as - * a `{ type: 'diff' }` tool-call content block. `oldText` is `null` for a - * new-file create (nothing to diff against); an overwrite also uses `null`, - * because a call-time presenter has no access to the file's prior content. - */ -export interface FileDiff { - path: string - /** Prior content, or `null` for a new file / an overwrite (no prior content available at call time). */ - oldText: string | null - /** Content after the change. */ - newText: string -} - -/** - * How a tool wants ONE of its calls shown in a UI (an editor's tool-call card, a - * CLI log line) BEFORE the result is known — the *pending* state. A `card`-tagged - * discriminated union: a tool declares its render INTENT once and a UI bridge - * switches on `card` to map it to the bridge's own wire shape. Provider-neutral — - * the tool owns its presentation, so a UI never special-cases tool names. - * - * Returned by {@link ToolDefinition.presentCall}. See the render-intent-union - * RFC (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). - */ -export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView - -/** - * The default card: a titled tool-call row with an optional category icon, a - * salient raw input, extra content blocks, and follow-along file locations. Any - * tool whose call is not a terminal or a diff uses this. - */ -export interface GenericCallView { - card: 'generic' - /** - * Human-readable, always-visible label describing what THIS call does. Keep it - * short — a UI shows it as a card header / log line. - */ - title: string - /** Category for icon/treatment; defaults to `other` when omitted. */ - kind?: ToolCallKind - /** - * The salient input to surface in a detail/expanded view (e.g. a background - * task id). Omit to show nothing; a string renders as-is, an object as pretty - * JSON. NOT the full raw args object unless that is genuinely what a reader wants. - */ - rawInput?: unknown - /** - * UI-facing content blocks to show on the pending call alongside the title. - * Omit to show none. A UI maps these to its own content blocks. - */ - content?: ContentBlock[] - /** Files this call reads/modifies, for editor follow-along. Omit for a call that touches no file. */ - locations?: FileLocation[] -} - -/** - * A call that IS a shell command running in a working directory: a capable UI - * renders it as a terminal card (cwd-headed, with the command as the title and - * live/afterward output from the {@link TerminalResultView}); an incapable UI - * falls back to a generic card whose body is the fenced command output. Set by a - * tool whose call is a foreground command (e.g. `bash`). - */ -export interface TerminalCallView { - card: 'terminal' - /** The command, shown as the terminal card's title / header line. */ - title: string - /** - * A human-readable one-line summary of what the command does, rendered ABOVE - * the terminal card (the card itself has no description slot). Omit for none. - */ - description?: string - /** - * Working directory the command runs in, shown as the terminal header. An - * ABSOLUTE path is used as-is; a RELATIVE path is resolved by the UI bridge - * against the session workspace (the pure presenter can't see the session cwd). - * Omit entirely to let the bridge use the session workspace. - */ - cwd?: string -} - -/** - * A call that creates or modifies files, rendered as an inline diff card by a - * capable UI. Set by a tool whose call writes/edits a file (e.g. `write`, - * `edit`). The diffs are derived from the call ARGUMENTS (a create's `oldText` is - * `null`); the tool emits a separate {@link DiffResultView} after `execute` — the - * applied change (an edit/overwrite hunk with context, or a whole-file diff for a - * create). - */ -export interface DiffCallView { - card: 'diff' - /** Card header (e.g. `Write foo.txt`). */ - title: string - /** One entry per file the call changes. */ - diffs: FileDiff[] - /** Files this call modifies, for editor follow-along (usually the diffs' paths). */ - locations?: FileLocation[] -} - -/** - * How a tool wants the COMPLETED call shown — the *result* state, after `execute` - * returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on - * `card`. Lets the tool reformat its result for a UI distinctly from the - * model-facing text it returned from `execute`. Returned by - * {@link ToolDefinition.presentResult}; omitting the method keeps the pending - * title and renders the raw result content. - */ -export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView - -/** - * The default completed card: an optional replacement title and reformatted - * content. Omit a field to keep the pending title / render the raw result content. - */ -export interface GenericResultView { - card: 'generic' - /** Replacement title for the completed call. Omit to keep the pending-state title. */ - title?: string - /** - * UI-facing result content (harness {@link ContentBlock}s), reformatted from - * the model-facing result. Omit to let the UI render the raw result content. - */ - content?: ContentBlock[] -} - -/** - * The completed state of a {@link TerminalCallView}: the captured output and exit - * status. A capable UI renders `output` in the terminal card and shows an - * exit-status pill; an incapable UI gets a fenced ```console fallback the BRIDGE - * derives from `output` (the tool does not double-encode it). - */ -export interface TerminalResultView { - card: 'terminal' - /** Replacement title for the completed call. Omit to keep the pending-state title. */ - title?: string - /** Captured command output (stdout+stderr as the tool chooses to combine them). */ - output?: string - /** - * Process exit code, when the run ended by exiting (not a signal). Lets a - * capable UI show an exit-status pill. Omit when killed by a signal or unknown. - */ - exitCode?: number - /** Signal name that killed the process (e.g. `SIGTERM`). Mutually exclusive with `exitCode`. */ - signal?: string -} - -/** - * A completed file mutation rendered as an inline diff card, the *result-time* - * analogue of {@link DiffCallView}. Set by a tool whose `execute` applied a file - * change (e.g. `write`, `edit`): `diffs` are the change to show — typically the - * APPLIED hunks computed from the before/after content (one entry per hunk, each - * with surrounding context lines), so the editor shows the real change in place; - * a tool with no before-image (e.g. a file create) may instead give a whole-file - * diff (`oldText: null`). A `tool_call_update`'s content REPLACES the call's - * content in an editor, so a mutation tool returns this even when it duplicates - * the call-time snippet — otherwise the model-facing result text would replace - * (clobber) the pending diff card. - */ -export interface DiffResultView { - card: 'diff' - /** Replacement title for the completed call. Omit to keep the pending-state title. */ - title?: string - /** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */ - diffs: FileDiff[] -} - /** * What a tool's `execute` returns. The bare {@link ContentBlock}`[]` form is the * common case (model-facing content only); the object form additionally attaches diff --git a/packages/core/tools/src/presentation.ts b/packages/core/tools/src/presentation.ts new file mode 100644 index 0000000000..a64fe71eec --- /dev/null +++ b/packages/core/tools/src/presentation.ts @@ -0,0 +1,206 @@ +/** + * Tool render-intent vocabulary: the provider-neutral types a tool declares via + * {@link ToolDefinition.presentCall}/{@link ToolDefinition.presentResult} to say + * how ONE of its calls renders in a UI (an editor's tool-call card, a CLI log + * line). A UI bridge switches on the `card` tag to map each intent to its own + * wire shape, so a UI never special-cases tool names. + * + * This is the UI-facing surface of `dsh-tools`, kept separate from the registry + * and execution core in `index.ts`: this module owns ONLY presentation + * vocabulary and references none of the execution types, so the dependency runs + * one way (`index.ts` imports these views for the `ToolDefinition` method + * signatures). The opaque `meta` presentation channel is execution plumbing and + * lives with the registry in `index.ts`, not here. + * + * See the render-intent-union RFC + * (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). + * + * @module @deepseek-ai/dsh-tools/src/presentation + */ + +import type { ContentBlock } from '@deepseek-ai/dsh-llm' + +/** + * Category of a tool call, used by a UI to pick an icon / treatment. A neutral + * vocabulary owned here (NOT an ACP type) so tools describe themselves without + * depending on any client protocol; a UI bridge maps it to its own enum. The + * member set mirrors the common ACP `ToolKind` values; `other` is the default. + */ +export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other' + +/** + * A file location a tool reads or modifies, so a capable UI can "follow along" — + * highlight or jump to the file (and line) as the tool runs. Provider-neutral; + * a UI bridge maps it to its own affordance (the ACP bridge forwards it as + * `tool_call.locations`). `path` is what the tool operated on (the model-facing + * path); `line` is an optional 1-based line to focus (e.g. a read's offset). + */ +export interface FileLocation { + path: string + line?: number +} + +/** + * A single-file change a tool is about to make, for a UI that renders inline + * diffs (an editor's diff card). Provider-neutral; the ACP bridge forwards it as + * a `{ type: 'diff' }` tool-call content block. `oldText` is `null` for a + * new-file create (nothing to diff against); an overwrite also uses `null`, + * because a call-time presenter has no access to the file's prior content. + */ +export interface FileDiff { + path: string + /** Prior content, or `null` for a new file / an overwrite (no prior content available at call time). */ + oldText: string | null + /** Content after the change. */ + newText: string +} + +/** + * How a tool wants ONE of its calls shown in a UI (an editor's tool-call card, a + * CLI log line) BEFORE the result is known — the *pending* state. A `card`-tagged + * discriminated union: a tool declares its render INTENT once and a UI bridge + * switches on `card` to map it to the bridge's own wire shape. Provider-neutral — + * the tool owns its presentation, so a UI never special-cases tool names. + * + * Returned by {@link ToolDefinition.presentCall}. See the render-intent-union + * RFC (docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). + */ +export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView + +/** + * The default card: a titled tool-call row with an optional category icon, a + * salient raw input, extra content blocks, and follow-along file locations. Any + * tool whose call is not a terminal or a diff uses this. + */ +export interface GenericCallView { + card: 'generic' + /** + * Human-readable, always-visible label describing what THIS call does. Keep it + * short — a UI shows it as a card header / log line. + */ + title: string + /** Category for icon/treatment; defaults to `other` when omitted. */ + kind?: ToolCallKind + /** + * The salient input to surface in a detail/expanded view (e.g. a background + * task id). Omit to show nothing; a string renders as-is, an object as pretty + * JSON. NOT the full raw args object unless that is genuinely what a reader wants. + */ + rawInput?: unknown + /** + * UI-facing content blocks to show on the pending call alongside the title. + * Omit to show none. A UI maps these to its own content blocks. + */ + content?: ContentBlock[] + /** Files this call reads/modifies, for editor follow-along. Omit for a call that touches no file. */ + locations?: FileLocation[] +} + +/** + * A call that IS a shell command running in a working directory: a capable UI + * renders it as a terminal card (cwd-headed, with the command as the title and + * live/afterward output from the {@link TerminalResultView}); an incapable UI + * falls back to a generic card whose body is the fenced command output. Set by a + * tool whose call is a foreground command (e.g. `bash`). + */ +export interface TerminalCallView { + card: 'terminal' + /** The command, shown as the terminal card's title / header line. */ + title: string + /** + * A human-readable one-line summary of what the command does, rendered ABOVE + * the terminal card (the card itself has no description slot). Omit for none. + */ + description?: string + /** + * Working directory the command runs in, shown as the terminal header. An + * ABSOLUTE path is used as-is; a RELATIVE path is resolved by the UI bridge + * against the session workspace (the pure presenter can't see the session cwd). + * Omit entirely to let the bridge use the session workspace. + */ + cwd?: string +} + +/** + * A call that creates or modifies files, rendered as an inline diff card by a + * capable UI. Set by a tool whose call writes/edits a file (e.g. `write`, + * `edit`). The diffs are derived from the call ARGUMENTS (a create's `oldText` is + * `null`); the tool emits a separate {@link DiffResultView} after `execute` — the + * applied change (an edit/overwrite hunk with context, or a whole-file diff for a + * create). + */ +export interface DiffCallView { + card: 'diff' + /** Card header (e.g. `Write foo.txt`). */ + title: string + /** One entry per file the call changes. */ + diffs: FileDiff[] + /** Files this call modifies, for editor follow-along (usually the diffs' paths). */ + locations?: FileLocation[] +} + +/** + * How a tool wants the COMPLETED call shown — the *result* state, after `execute` + * returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on + * `card`. Lets the tool reformat its result for a UI distinctly from the + * model-facing text it returned from `execute`. Returned by + * {@link ToolDefinition.presentResult}; omitting the method keeps the pending + * title and renders the raw result content. + */ +export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView + +/** + * The default completed card: an optional replacement title and reformatted + * content. Omit a field to keep the pending title / render the raw result content. + */ +export interface GenericResultView { + card: 'generic' + /** Replacement title for the completed call. Omit to keep the pending-state title. */ + title?: string + /** + * UI-facing result content (harness {@link ContentBlock}s), reformatted from + * the model-facing result. Omit to let the UI render the raw result content. + */ + content?: ContentBlock[] +} + +/** + * The completed state of a {@link TerminalCallView}: the captured output and exit + * status. A capable UI renders `output` in the terminal card and shows an + * exit-status pill; an incapable UI gets a fenced ```console fallback the BRIDGE + * derives from `output` (the tool does not double-encode it). + */ +export interface TerminalResultView { + card: 'terminal' + /** Replacement title for the completed call. Omit to keep the pending-state title. */ + title?: string + /** Captured command output (stdout+stderr as the tool chooses to combine them). */ + output?: string + /** + * Process exit code, when the run ended by exiting (not a signal). Lets a + * capable UI show an exit-status pill. Omit when killed by a signal or unknown. + */ + exitCode?: number + /** Signal name that killed the process (e.g. `SIGTERM`). Mutually exclusive with `exitCode`. */ + signal?: string +} + +/** + * A completed file mutation rendered as an inline diff card, the *result-time* + * analogue of {@link DiffCallView}. Set by a tool whose `execute` applied a file + * change (e.g. `write`, `edit`): `diffs` are the change to show — typically the + * APPLIED hunks computed from the before/after content (one entry per hunk, each + * with surrounding context lines), so the editor shows the real change in place; + * a tool with no before-image (e.g. a file create) may instead give a whole-file + * diff (`oldText: null`). A `tool_call_update`'s content REPLACES the call's + * content in an editor, so a mutation tool returns this even when it duplicates + * the call-time snippet — otherwise the model-facing result text would replace + * (clobber) the pending diff card. + */ +export interface DiffResultView { + card: 'diff' + /** Replacement title for the completed call. Omit to keep the pending-state title. */ + title?: string + /** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */ + diffs: FileDiff[] +} diff --git a/packages/core/tools/src/schema.ts b/packages/core/tools/src/schema.ts index 592ff4df21..78b3c91538 100644 --- a/packages/core/tools/src/schema.ts +++ b/packages/core/tools/src/schema.ts @@ -20,7 +20,8 @@ */ import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm' -import type { ToolCallView, ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult, ToolResultView } from './index.ts' +import type { ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult } from './index.ts' +import type { ToolCallView, ToolResultView } from './presentation.ts' // --------------------------------------------------------------------------- // SchemaSpec — the author-facing per-property type