Merge branch 'worktree-hooks-b-bash-seam' into worktree-hooks-c-interception
# Conflicts: # docs/architecture.md # docs/cordis-catalog/events-and-services.md # docs/core-data-structures/tools.md # packages/core/tools/README.md # packages/core/tools/src/index.ts
This commit is contained in:
@@ -13,6 +13,7 @@ import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { ToolCallView, ToolResultView } from './presentation.ts'
|
||||
|
||||
export {
|
||||
defineTool,
|
||||
@@ -27,6 +28,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
|
||||
@@ -73,147 +91,37 @@ declare module 'cordis' {
|
||||
// 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.
|
||||
* What a tool's `execute` returns. The bare {@link ContentBlock}`[]` form is the
|
||||
* common case (model-facing content only); the object form additionally attaches
|
||||
* a tool-private `meta` presentation payload that the registry threads onto the
|
||||
* `tool/result` session event and hands back to the tool's `presentResult`.
|
||||
* `meta` is opaque to the core (`unknown` — the tool owns and narrows its shape),
|
||||
* and MUST be JSON-serializable: it persists on the durable log (the session
|
||||
* enforces this at `append`), so replay reproduces the card.
|
||||
*/
|
||||
export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'
|
||||
|
||||
// FIXME(tool-presentation): the ToolCallPresentation / ToolResultPresentation /
|
||||
// ToolTerminal shapes need a rethink. They grew incrementally (title/kind/
|
||||
// rawInput, then a `content` block, then a `terminal` sub-shape carrying cwd/
|
||||
// output/exit) and the split of responsibility is now muddy: the call vs result
|
||||
// terminal fields overlap, the bridge has to reconcile a `content` block AND a
|
||||
// `terminal` block AND `rawInput` per call, and the "pending vs completed"
|
||||
// boundary doesn't cleanly map to how editors actually render (terminal card,
|
||||
// diff, generic card). Before more tools/UIs depend on this, redesign the type
|
||||
// so a tool declares its render INTENT once (e.g. a tagged union over card
|
||||
// kinds) rather than a bag of optional fields the bridge stitches together.
|
||||
// Pin the design in an RFC and migrate dsh-tool-bash + the ACP bridge together.
|
||||
|
||||
/**
|
||||
* 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. Provider-
|
||||
* neutral: a tool returns this from {@link ToolDefinition.presentCall} and a UI
|
||||
* plugin (e.g. the ACP bridge) maps it to its own wire shape. The tool owns its
|
||||
* own presentation — the UI must not special-case tool names.
|
||||
*/
|
||||
export interface ToolCallPresentation {
|
||||
/**
|
||||
* Human-readable, always-visible label describing what THIS call does (e.g.
|
||||
* the model-written one-line summary of a bash command). Keep it short — a UI
|
||||
* shows it as a card header / log line. Required: a presentation must have a
|
||||
* title (a UI falls back to the tool name only when `presentCall` is absent).
|
||||
*/
|
||||
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. the bash
|
||||
* COMMAND itself (as a string), so the title can stay a readable summary
|
||||
* while the exact command is still visible. Omit to show nothing; a string is
|
||||
* rendered 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 to show on the PENDING call alongside the title/card —
|
||||
* harness {@link ContentBlock}s, in render order. A terminal tool uses this to
|
||||
* surface its human-readable `description` as a text block ABOVE the terminal
|
||||
* card (the card itself is requested via {@link terminal} and labelled by the
|
||||
* command in `title`), since the card has no description slot. Omit to show no
|
||||
* extra content. A UI maps these to its own content blocks and renders a
|
||||
* {@link terminal} block (if any) as a terminal card.
|
||||
*/
|
||||
content?: ContentBlock[]
|
||||
/**
|
||||
* Ask a capable UI to render this call as a TERMINAL (a command running in a
|
||||
* working directory), not a generic tool card — set by a tool whose call IS a
|
||||
* shell command (e.g. `bash`). Provider-neutral; a UI bridge maps it to its
|
||||
* own terminal affordance and a UI that can't falls back to the normal card.
|
||||
* Pair with {@link ToolResultPresentation.terminal} for the output/exit.
|
||||
*/
|
||||
terminal?: ToolTerminal
|
||||
}
|
||||
|
||||
/**
|
||||
* A request to render a tool call as a terminal. The pending presentation
|
||||
* supplies the working directory; the result presentation (see
|
||||
* {@link ToolResultPresentation.terminal}) supplies the captured output and exit
|
||||
* status. Provider-neutral — no client-protocol types. A UI that supports
|
||||
* terminals shows a cwd-headed terminal card with the command, its output, and
|
||||
* an exit-status pill; a UI that does not ignores this and renders the ordinary
|
||||
* card/content.
|
||||
*/
|
||||
export interface ToolTerminal {
|
||||
/**
|
||||
* Working directory the command ran 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 tool presenter can't see the
|
||||
* session cwd). Omit entirely to let the bridge use the session workspace.
|
||||
*/
|
||||
cwd?: string
|
||||
/** Captured command output (stdout+stderr as the tool chooses to combine them). Result-state only. */
|
||||
output?: string
|
||||
/**
|
||||
* Process exit code, when the run ended by exiting (not a signal). Result-state
|
||||
* only; lets a capable UI show an exit-status pill on the terminal card. Omit
|
||||
* when the command was killed by a signal or the exit code is unknown.
|
||||
*/
|
||||
exitCode?: number
|
||||
/**
|
||||
* Signal name that killed the process (e.g. `SIGTERM`), when it died by signal
|
||||
* rather than exiting. Result-state only; mutually exclusive with `exitCode`.
|
||||
*/
|
||||
signal?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* How a tool wants the COMPLETED call shown — the *result* state, after
|
||||
* `execute` returns. Lets the tool reformat its result for a UI distinctly from
|
||||
* the model-facing text it returned from `execute` (e.g. wrap command output in
|
||||
* a fenced ```console block for monospace rendering, which the model-facing
|
||||
* result must NOT carry). All fields optional: a UI keeps the pending-state
|
||||
* title and renders the raw result content for anything left unset.
|
||||
*/
|
||||
export interface ToolResultPresentation {
|
||||
/** Replacement title for the completed call (e.g. append an exit status). 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.
|
||||
* Stays in harness vocabulary; the UI maps these to its own content blocks.
|
||||
*/
|
||||
content?: ContentBlock[]
|
||||
/**
|
||||
* Terminal output/exit for a call the pending presentation marked as a
|
||||
* terminal (see {@link ToolCallPresentation.terminal}). A capable UI renders
|
||||
* `output` in the terminal card and shows the exit status; an incapable UI
|
||||
* uses `content` (the tool should supply a text fallback there too).
|
||||
*/
|
||||
terminal?: ToolTerminal
|
||||
}
|
||||
export type ToolExecuteReturn = ContentBlock[] | { content: ContentBlock[]; meta?: unknown }
|
||||
|
||||
/** A registered tool: its schema plus the execution function. */
|
||||
export interface ToolDefinition extends ToolSchema {
|
||||
execute(args: unknown, exec: ToolExecution): Promise<ContentBlock[]>
|
||||
execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn>
|
||||
/**
|
||||
* Optional: how to present the PENDING state of one call in a UI, derived
|
||||
* from the call's `args` (parsed arguments, `unknown` — the tool validates/
|
||||
* narrows its own input). Returning `undefined` (or omitting the method) tells
|
||||
* a UI to fall back to a generic presentation (title = tool name, raw args as
|
||||
* input). Pure and side-effect-free: a UI may call it during live streaming
|
||||
* AND a session-log replay, so it must depend only on `args`.
|
||||
* Optional: how to present the PENDING state of one call in a UI, derived from
|
||||
* the call's `args` (parsed arguments, `unknown` — the tool validates/narrows
|
||||
* its own input). Returns a {@link ToolCallView} (a `card`-tagged render intent),
|
||||
* or `undefined` (or omit the method) to fall back to a generic presentation
|
||||
* (title = tool name, raw args as input). Pure and side-effect-free: a UI may
|
||||
* call it during live streaming AND a session-log replay, so it must depend
|
||||
* only on `args`.
|
||||
*/
|
||||
presentCall?(args: unknown): ToolCallPresentation | undefined
|
||||
presentCall?(args: unknown): ToolCallView | undefined
|
||||
/**
|
||||
* Optional: how to present the COMPLETED state, given the same `args` and the
|
||||
* `result` (`execute`'s content + whether it errored). Returning `undefined`
|
||||
* (or omitting the method) tells a UI to keep the pending title and render the
|
||||
* raw result content. Pure and side-effect-free for the same replay reason.
|
||||
* `result` (`execute`'s content + whether it errored). Returns a
|
||||
* {@link ToolResultView}, or `undefined` (or omit the method) to keep the
|
||||
* pending title and render the raw result content. Pure and side-effect-free
|
||||
* for the same replay reason.
|
||||
*/
|
||||
presentResult?(args: unknown, result: ToolResult): ToolResultPresentation | undefined
|
||||
presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined
|
||||
}
|
||||
|
||||
/** The completed outcome handed to {@link ToolDefinition.presentResult}. */
|
||||
@@ -222,6 +130,13 @@ export interface ToolResult {
|
||||
content: ContentBlock[]
|
||||
/** Whether the call failed. */
|
||||
isError: boolean
|
||||
/**
|
||||
* The tool-private presentation payload the tool attached from `execute` (via
|
||||
* the object return form), threaded verbatim from the `tool/result` event.
|
||||
* Opaque (`unknown`); the tool narrows it back to its own shape. Absent when
|
||||
* the tool attached none.
|
||||
*/
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
/** One pending tool call, as it flows through the execution pipeline (`tools/pre-execute` → dispatch → `tools/post-execute`). */
|
||||
@@ -265,6 +180,7 @@ export interface ToolExecutionResult {
|
||||
* text in `content` is always present; this is extra structure for code.
|
||||
*/
|
||||
error?: ToolErrorInfo
|
||||
/**
|
||||
/**
|
||||
* Extra model-facing context a `tools/post-execute` listener attached for the
|
||||
* NEXT request (Claude Code's PostToolUse `additionalContext`). It is NOT part
|
||||
@@ -276,6 +192,13 @@ export interface ToolExecutionResult {
|
||||
* from `execute()` up to the loop's per-step buffer.
|
||||
*/
|
||||
additionalContext?: HookContext
|
||||
/**
|
||||
* The tool-private presentation payload from a successful `execute` (the object
|
||||
* return form). Threaded onto the `tool/result` session event and back into
|
||||
* {@link ToolResult} for `presentResult`. Opaque (`unknown`); absent when the
|
||||
* tool attached none or the call failed.
|
||||
*/
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,8 +365,13 @@ export class ToolRegistry extends Service {
|
||||
try {
|
||||
const tool = this.store.get(exec.name)
|
||||
if (!tool) throw new ToolNotFoundError(exec.name)
|
||||
const content = await tool.execute(exec.arguments, exec)
|
||||
result = { callId: exec.callId, content, isError: false }
|
||||
// Normalize the two `execute` return shapes: a bare ContentBlock[] (no
|
||||
// meta) or a { content, meta } object (a tool attaching a private
|
||||
// presentation payload). An array IS the content; the object carries it.
|
||||
const returned = await tool.execute(exec.arguments, exec)
|
||||
const content = Array.isArray(returned) ? returned : returned.content
|
||||
const meta = Array.isArray(returned) ? undefined : returned.meta
|
||||
result = { callId: exec.callId, content, isError: false, ...meta !== undefined ? { meta } : {} }
|
||||
} catch (error: unknown) {
|
||||
result = toolErrorResult(exec.callId, error)
|
||||
}
|
||||
@@ -480,6 +408,7 @@ export class ToolRegistry extends Service {
|
||||
content: [...result.content],
|
||||
isError: result.isError,
|
||||
...result.error ? { error: result.error } : {},
|
||||
...result.meta !== undefined ? { meta: result.meta } : {},
|
||||
}
|
||||
const decision = await this.ctx.waterfall(
|
||||
this, 'tools/post-execute', exec, result,
|
||||
|
||||
206
packages/core/tools/src/presentation.ts
Normal file
206
packages/core/tools/src/presentation.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Tool render-intent vocabulary: the provider-neutral types a tool declares via
|
||||
* `ToolDefinition.presentCall`/`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 `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
|
||||
* `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[]
|
||||
}
|
||||
@@ -19,9 +19,9 @@
|
||||
* @module dsh-tools/schema
|
||||
*/
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolCallPresentation, ToolDefinition, ToolExecution, ToolResult, ToolResultPresentation } 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
|
||||
@@ -291,25 +291,27 @@ export interface DefineToolOptions<S extends SchemaSpec> {
|
||||
parameters: S
|
||||
/**
|
||||
* Tool execution function. `args` is typed as {@link InferArgs<S>} — zero
|
||||
* casts needed.
|
||||
* casts needed. Returns either a bare {@link ContentBlock}`[]` (model-facing
|
||||
* content only) or a `{ content, meta }` object to also attach a tool-private
|
||||
* presentation payload (see {@link ToolExecuteReturn}).
|
||||
*/
|
||||
execute(args: InferArgs<S>, exec: ToolExecution): Promise<ContentBlock[]>
|
||||
execute(args: InferArgs<S>, exec: ToolExecution): Promise<ToolExecuteReturn>
|
||||
/**
|
||||
* Optional: how to present the PENDING state of one call in a UI (an editor
|
||||
* tool-call card, a CLI log line). `args` is the typed, schema-validated
|
||||
* argument shape — zero casts. Pure and side-effect-free: a UI may call it
|
||||
* during live streaming AND a session-log replay, so depend only on `args`.
|
||||
* The tool owns its presentation so a UI never special-cases tool names. See
|
||||
* {@link ToolCallPresentation}.
|
||||
* {@link ToolCallView}.
|
||||
*/
|
||||
presentCall?(args: InferArgs<S>): ToolCallPresentation | undefined
|
||||
presentCall?(args: InferArgs<S>): ToolCallView | undefined
|
||||
/**
|
||||
* Optional: how to present the COMPLETED state, given the typed `args` and the
|
||||
* `result`. Use it to reformat result content for a UI distinctly from the
|
||||
* model-facing text (e.g. a fenced ```console block). Pure and side-effect-
|
||||
* free for the same replay reason. See {@link ToolResultPresentation}.
|
||||
* free for the same replay reason. See {@link ToolResultView}.
|
||||
*/
|
||||
presentResult?(args: InferArgs<S>, result: ToolResult): ToolResultPresentation | undefined
|
||||
presentResult?(args: InferArgs<S>, result: ToolResult): ToolResultView | undefined
|
||||
/** Whether the tool requires structured output (default false). */
|
||||
strict?: boolean
|
||||
}
|
||||
@@ -354,7 +356,7 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
|
||||
description: options.description,
|
||||
parameters: schemaSpecToJsonSchema(options.parameters) as unknown as Record<string, unknown>,
|
||||
...options.strict !== undefined ? { strict: options.strict } : {},
|
||||
async execute(args: unknown, exec: ToolExecution): Promise<ContentBlock[]> {
|
||||
async execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn> {
|
||||
// Validate the model-generated args before the typed body runs. On
|
||||
// mismatch we throw ToolArgsError; the registry turns it into an
|
||||
// isError result so the model can self-correct. After this guard, the
|
||||
@@ -369,13 +371,13 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
|
||||
// fall back to `undefined` (a generic UI presentation) on any mismatch, rather
|
||||
// than the hard `ToolArgsError` the execute path raises.
|
||||
if (userPresentCall) {
|
||||
tool.presentCall = (args: unknown): ToolCallPresentation | undefined => {
|
||||
tool.presentCall = (args: unknown): ToolCallView | undefined => {
|
||||
if (validateArgs(options.parameters, args).length > 0) return undefined
|
||||
return userPresentCall(args as InferArgs<S>)
|
||||
}
|
||||
}
|
||||
if (userPresentResult) {
|
||||
tool.presentResult = (args: unknown, result: ToolResult): ToolResultPresentation | undefined => {
|
||||
tool.presentResult = (args: unknown, result: ToolResult): ToolResultView | undefined => {
|
||||
if (validateArgs(options.parameters, args).length > 0) return undefined
|
||||
return userPresentResult(args as InferArgs<S>, result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user