# Conflicts: # docs/config-catalog.md # docs/config-catalog.zh.md # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # docs/persistence-catalog.i18n.yaml # docs/persistence-catalog.md # docs/persistence-catalog.zh.md # docs/persistence-schema.json # packages/core/agent-loop/src/agent.ts # packages/core/agent-loop/src/tool-calls.ts # packages/core/agent-loop/tests/tool-updates.spec.ts # packages/core/system-prompt/README.i18n.yaml # packages/core/system-prompt/README.md # packages/core/system-prompt/README.zh.md # packages/core/tool-executor-ptc/src/ptc.ts # packages/core/tool-executor-ptc/tests/ptc.spec.ts # packages/core/tools/README.i18n.yaml # packages/core/tools/README.md # packages/core/tools/README.zh.md # packages/core/tools/src/index.ts # packages/extensions/tool-cordis/src/api-catalog.ts # packages/plan/plan-mode/tests/plan-mode.spec.ts # snapshots/session/cordis-inspect-jsdoc/session.v4.jsonl
392 lines
18 KiB
TypeScript
392 lines
18 KiB
TypeScript
/** Tool definitions and invocation data shared by registries and executors. @module */
|
|
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
|
import type { ToolCallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
|
|
import type { Agent, ModelSelection } from '@deepseek-ai/dsh-agent'
|
|
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
|
|
import type { UserMessage } from '@deepseek-ai/dsh-session'
|
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
import type { JsonSchemaNode } from './json-schema.ts'
|
|
import type { ToolCallView, ToolResultView } from './presentation.ts'
|
|
|
|
/** Reserved model-facing program transport name. */
|
|
export const RUN_CODE_NAME = 'run_code'
|
|
declare const toolExecutionTokenBrand: unique symbol
|
|
|
|
/** Tool-owned canonical output contract used after the body returns a JSON value. */
|
|
export interface ToolOutputDefinition {
|
|
/** Raw supported JSON Schema enforced against every successful canonical value. */
|
|
readonly schema: JsonSchemaNode
|
|
/** Pure projection from validated arguments and value to Native/model content. */
|
|
render(args: unknown, value: JsonValue): ContentBlock[]
|
|
/** Pure replayable presentation projection for native and nested calls. */
|
|
presentationMeta?(args: unknown, value: JsonValue): JsonValue
|
|
}
|
|
|
|
/** A registered tool: its schema plus the execution function. */
|
|
export interface ToolDefinition extends ToolSchema {
|
|
/** Mandatory canonical output declaration. */
|
|
readonly output: ToolOutputDefinition
|
|
/**
|
|
* Run one accepted call and return only its canonical lossless-JSON value.
|
|
* Async work must observe or forward `exec.signal` and settle only after its
|
|
* owned work reaches quiescence. The executor preserves caller cancellation
|
|
* through around-dispatch signal replacement and does not abandon this
|
|
* promise, but it cannot hard-kill same-process code.
|
|
* @param args - losslessly snapshotted, frozen model arguments.
|
|
* @param exec - execution identity, cancellation signal, and context deferral.
|
|
* @returns the canonical value declared by `output.schema`.
|
|
*/
|
|
execute(args: unknown, exec: ToolRunContext): Promise<unknown>
|
|
/**
|
|
* Install execution-prepared content before `tools/post-execute` policies.
|
|
* The callback is captured when the call starts and runs once for a
|
|
* normalized outcome entering post-execute. Policy replacements remain
|
|
* authoritative; pipeline failures that bypass post-execute skip projection.
|
|
* @param exec - immutable execution identity and arguments.
|
|
* @param result - normalized result before post-execute policy.
|
|
* @returns replacement content, or undefined to preserve the renderer output.
|
|
*/
|
|
projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
|
|
/**
|
|
* Synchronous last-mile transform for model-facing content. The executor
|
|
* snapshots this callback when execution starts and invokes it exactly once
|
|
* for every normalized outcome, including pipeline failures that bypass
|
|
* `tools/post-execute`, immediately before lossless materialization.
|
|
* Returning `undefined` preserves the content; every other result field
|
|
* remains executor-owned. The callback must be total and must not throw.
|
|
* @param exec - immutable execution identity and arguments.
|
|
* @param result - complete normalized outcome before materialization.
|
|
* @returns replacement content, or `undefined` to preserve it.
|
|
*/
|
|
finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
|
|
/**
|
|
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
|
|
* Enforced by `@deepseek-ai/dsh-tool-call-timeout-policy` (a `tools/execute` wrapper); it
|
|
* is NEVER sent to the model — `schemas()` whitelists only name/description/
|
|
* parameters. Declaring it asserts this tool forwards `exec.signal` to a
|
|
* cooperative implementation that can reach quiescence when the signal aborts.
|
|
*/
|
|
timeoutMs?: number
|
|
/**
|
|
* Pure synchronous classifier for overlap with sibling tool calls. Only
|
|
* `true` opts in; omission, exceptions, non-`true` returns, and invalid
|
|
* `defineTool` arguments are exclusive. This metadata is never model-visible.
|
|
*
|
|
* Opted-in executions must not mutate parent-owned state. Shared state must
|
|
* tolerate concurrent dispatch; recorder races are permitted only when they
|
|
* commute or fail closed. See the
|
|
* [parallel-tool-call Agent Note](../../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)
|
|
* for the full contract.
|
|
* @param args - parsed arguments; `defineTool` validates before calling.
|
|
* @returns Whether this call may join a parallel group.
|
|
*/
|
|
isConcurrencySafe?(args: unknown): boolean
|
|
/**
|
|
* 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): ToolCallView | undefined
|
|
/**
|
|
* Optional: how to present the COMPLETED state, given the same `args` and the
|
|
* durable result projection (`content`, failure state, and optional `meta`). 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): ToolResultView | undefined
|
|
}
|
|
|
|
/** Inputs for a tool's step-time activation condition. */
|
|
export interface ToolActivationContext {
|
|
/** Agent preparing the step. */
|
|
readonly agent: Agent
|
|
/** Provider, model, and optional reasoning effort captured for the step. */
|
|
readonly modelSelection: Readonly<ModelSelection>
|
|
/** Cancellation of step preparation; asynchronous conditions must observe it. */
|
|
readonly signal: AbortSignal
|
|
}
|
|
|
|
/** Lifecycle-owned conditions for a registered tool. */
|
|
export interface ToolRegistrationOptions {
|
|
/**
|
|
* Decide whether the step's requests may call the tool. Omission enables
|
|
* it. False excludes it from both declarations and dispatch; rejection fails
|
|
* step preparation. Conditions must not mutate registrations or start tool work.
|
|
* @param context - preparing agent, captured model selection, and cancellation signal.
|
|
* @returns whether the tool participates in this step.
|
|
*/
|
|
readonly when?: (context: ToolActivationContext) => boolean | Promise<boolean>
|
|
}
|
|
|
|
/** One step's admitted registrations, also used by nested PTC calls. */
|
|
export interface ResolvedToolSet {
|
|
/** Close admission after the step's tool batch settles. */
|
|
dispose(): void
|
|
/**
|
|
* Read admitted registrations that remain visible in their original scope.
|
|
* @param scope - exact requesting agent; another scope is never admitted.
|
|
* @returns original definitions excluding removed or restricted registrations.
|
|
*/
|
|
view(scope: ScopeKey | undefined): ReadonlyMap<string, ToolDefinition>
|
|
}
|
|
|
|
/** Failure to determine a tool's step-time availability. */
|
|
export class ToolActivationError extends HarnessError {
|
|
/**
|
|
* Preserve a condition failure without treating it as normal unavailability.
|
|
* @param name - tool whose condition rejected.
|
|
* @param cause - underlying condition failure.
|
|
*/
|
|
constructor(name: string, cause: unknown) {
|
|
super(`cannot resolve availability of tool "${name}"`, 'TOOL_ACTIVATION_FAILED', { cause })
|
|
this.name = 'ToolActivationError'
|
|
}
|
|
}
|
|
|
|
/** The completed outcome handed to {@link ToolDefinition.presentResult}. */
|
|
export interface ToolResult {
|
|
/** The final model-facing content (or the rendered error text on failure). */
|
|
content: ContentBlock[]
|
|
/** Whether the call failed. */
|
|
isError: boolean
|
|
/**
|
|
* The tool-private presentation payload projected by its output declaration.
|
|
* It is persisted on `tool/result` or `tool/ptc-dispatch` for Host presenters
|
|
* and Client renderers to narrow independently. Absent when the tool
|
|
* declared no projector.
|
|
*/
|
|
meta?: JsonValue
|
|
}
|
|
|
|
/** Opaque call identity that permits correlation without exposing mutable execution state. */
|
|
export type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
|
|
|
|
/**
|
|
* Parsed input to the shared invocation pipeline. The pipeline
|
|
* adds its own token to form a {@link ToolExecution};
|
|
* callers do not choose that token.
|
|
*/
|
|
export interface ToolExecutionInput {
|
|
readonly callId: ToolCallId
|
|
/**
|
|
* Root model-requested call owning this execution tree. Callers omit it for
|
|
* a root execution; nested dispatchers propagate the enclosing value.
|
|
*/
|
|
readonly rootCallId?: ToolCallId
|
|
readonly name: string
|
|
/** Binding-time tool schema for a PTC inner call; frozen by its producer and never logged. */
|
|
readonly schema?: ToolSchema
|
|
/** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */
|
|
readonly arguments: unknown
|
|
/** The agent on whose behalf the call runs (set by the agent loop). */
|
|
readonly agent?: Agent
|
|
/** Step admission shared by Native and PTC dispatch. Conditional tools require it. */
|
|
readonly toolSet?: ResolvedToolSet
|
|
/**
|
|
* Opaque token of the enclosing transport execution, when one exists. PTC
|
|
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
|
|
* the outer `run_code` outcome without receiving its live mutable execution.
|
|
* The token also marks the call as a transport sub-dispatch rather than a
|
|
* model-direct call: with the PTC executor selected, only calls with a parent may
|
|
* execute a native tool name — a model-direct call (no parent) is denied as
|
|
* `UNKNOWN_TOOL` before the policy pipeline.
|
|
*/
|
|
readonly parent?: ToolExecutionToken
|
|
/** Required caller-owned cancellation for this invocation. */
|
|
readonly signal: AbortSignal
|
|
}
|
|
|
|
/**
|
|
* Scheduling mode for one pending call. `parallel` may overlap with siblings;
|
|
* `exclusive` runs alone and forms an ordering barrier.
|
|
*/
|
|
export type ToolExecutionMode =
|
|
| { kind: 'parallel' }
|
|
| { kind: 'exclusive' }
|
|
|
|
/**
|
|
* One pending tool call inside the executor pipeline. Parsed arguments cross
|
|
* one lossless-JSON materialization boundary before policy and are deep-frozen;
|
|
* call identity, the caller signal, and the executor-assigned {@link token} are
|
|
* readonly. The executor freezes the complete object before `tools/result`
|
|
* observers run.
|
|
*/
|
|
export interface ToolExecution extends ToolExecutionInput {
|
|
/** Root model-requested call, resolved for every root and nested execution. */
|
|
readonly rootCallId: ToolCallId
|
|
/** Executor-assigned identity shared with nested calls only as their opaque `parent` token. */
|
|
readonly token: ToolExecutionToken
|
|
}
|
|
|
|
/**
|
|
* Around-dispatch view of a {@link ToolExecution}. A `tools/execute` wrapper
|
|
* may replace the signal for its delegated lifetime, but it cannot remove it.
|
|
* The executor fuses every replacement with the captured caller signal.
|
|
*/
|
|
export interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
|
|
/** Cancellation signal visible to the next wrapper or tool body. */
|
|
signal: AbortSignal
|
|
}
|
|
|
|
/**
|
|
* Runtime context handed to a tool implementation after the executor has
|
|
* accepted a {@link ToolExecution}. {@link deferContext} attaches context to
|
|
* this execution's own result — a composite tool ferries nested-dispatch
|
|
* context back to the outer result, and a leaf tool may mint a fresh
|
|
* plugin-sourced instruction; the loop appends it only after the
|
|
* `tool/result`.
|
|
*/
|
|
export interface ToolRunContext extends ToolExecution {
|
|
/**
|
|
* Defer one context — typically a nested-dispatch context ferried by a
|
|
* composite tool, or a fresh plugin-sourced instruction — until this tool's
|
|
* final result reaches the agent loop. Contexts retain their individual
|
|
* source and metadata and are emitted in call order.
|
|
*/
|
|
deferContext(context: UserMessage): void
|
|
/**
|
|
* Mark a successful final result as terminal for the current agent turn.
|
|
* The marker rides this execution's own result (`concludesTurn` exists only
|
|
* on {@link ToolExecutionSuccess}); a composite that dispatches nested
|
|
* calls forwards it from the nested result, exactly like
|
|
* `additionalContexts`, so only an authoritative nested success can
|
|
* conclude the enclosing run.
|
|
*/
|
|
concludeTurn(): void
|
|
}
|
|
|
|
/** Canonical error code for cancellation after a tool body was invoked. */
|
|
export const TOOL_ABORTED = 'ABORTED'
|
|
|
|
/** Canonical error code for cancellation before a tool body was invoked. */
|
|
export const TOOL_ABORTED_BEFORE_DISPATCH = 'ABORTED_BEFORE_DISPATCH'
|
|
|
|
/** Structured error metadata for a failed tool call (alongside the model-facing text). */
|
|
export interface ToolErrorInfo {
|
|
name: string
|
|
code: string
|
|
/** Optional raw user-facing detail; durable projections preserve it but model-facing content does not include it. */
|
|
reason?: string
|
|
}
|
|
|
|
/** Canonical failure detail; internal routing information remains optional. */
|
|
export interface ToolFailure {
|
|
/** Human-readable failure message without the Native `Error: ` envelope. */
|
|
message: string
|
|
/** Internal error class/code used by policy and durable diagnostics. */
|
|
info?: ToolErrorInfo
|
|
}
|
|
|
|
/**
|
|
* Thrown (internally) when the model requests a tool that isn't registered.
|
|
* Extends {@link HarnessError} (`code: 'UNKNOWN_TOOL'`) so an unknown-tool
|
|
* failure is as routable as a tool-thrown one — retry/sandbox/replay code can
|
|
* distinguish it from a tool body's own error.
|
|
*/
|
|
export class ToolNotFoundError extends HarnessError {
|
|
/**
|
|
* @param toolName - the name the caller asked for.
|
|
* @param reachableFrom - how the model reaches this tool instead, when the
|
|
* name IS visible and only the presentation denies calling it directly.
|
|
* Omitted for a name that is registered nowhere.
|
|
*/
|
|
constructor(toolName: string, reachableFrom?: string) {
|
|
super(
|
|
reachableFrom === undefined
|
|
? `unknown tool "${toolName}"`
|
|
: `unknown tool "${toolName}": ${reachableFrom}`,
|
|
'UNKNOWN_TOOL',
|
|
)
|
|
this.name = 'ToolNotFoundError'
|
|
}
|
|
}
|
|
|
|
/** Thrown when a tool body or post-policy value violates its declared output. */
|
|
export class ToolOutputError extends HarnessError {
|
|
/** Schema/value violations in validation order. */
|
|
readonly violations: string[]
|
|
|
|
constructor(toolName: string, violations: string[]) {
|
|
super(`tool "${toolName}" returned invalid output: ${violations.join('; ')}`, 'INVALID_TOOL_OUTPUT')
|
|
this.name = 'ToolOutputError'
|
|
this.violations = violations
|
|
}
|
|
}
|
|
|
|
/** Successful canonical tool execution, including its Native/model projection. */
|
|
export interface ToolExecutionSuccess {
|
|
readonly isError: false
|
|
/** Execution-local canonical value; deliberately omitted from durable events. */
|
|
readonly value: JsonValue
|
|
readonly content: ContentBlock[]
|
|
readonly error?: never
|
|
readonly meta?: JsonValue
|
|
readonly additionalContexts?: UserMessage[]
|
|
/** The agent loop stops after committing this successful result batch. */
|
|
readonly concludesTurn?: true
|
|
}
|
|
|
|
/** Failed canonical tool execution; failures never carry a successful value. */
|
|
export interface ToolExecutionFailure {
|
|
readonly isError: true
|
|
readonly error: ToolFailure
|
|
readonly value?: never
|
|
readonly content: ContentBlock[]
|
|
readonly meta?: JsonValue
|
|
readonly additionalContexts?: UserMessage[]
|
|
readonly concludesTurn?: never
|
|
}
|
|
|
|
/** The discriminated, execution-local outcome of one tool call. */
|
|
export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure
|
|
|
|
/**
|
|
* Pre-dispatch decision. `allow` runs the call; `deny` materializes its
|
|
* model-facing reason and optional structured error identity; `cancel` selects
|
|
* the canonical cancellation result without presenting a policy denial; `ask`
|
|
* runs only after an approval service returns `allowed-once` and otherwise
|
|
* denies; its `reason` is the audited approval reason and its optional
|
|
* `displayReason` is the localized prompt text. Input rewriting is excluded because arguments are already logged and
|
|
* presented.
|
|
*/
|
|
export type PreToolDecision =
|
|
| { kind: 'allow' }
|
|
| { kind: 'deny'; reason: string; info?: ToolErrorInfo }
|
|
| { kind: 'cancel' }
|
|
| { kind: 'ask'; reason?: string; displayReason?: { readonly en: string; readonly [locale: string]: string } }
|
|
|
|
/**
|
|
* Post-dispatch decision: accept, replace one projection, attach context for the
|
|
* next request, or block by turning corrective feedback into an error result.
|
|
*/
|
|
export type PostToolDecision =
|
|
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessage[] }
|
|
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessage[] }
|
|
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessage[] }
|
|
|
|
/**
|
|
* Per-scope filter over global tools. Restrictions intersect and do not affect
|
|
* scoped registrations or the reserved PTC mode transport.
|
|
*/
|
|
export interface ToolRestriction {
|
|
/** Global tool names that stay visible; everything else is removed. */
|
|
readonly allow?: readonly string[]
|
|
/** Global tool names removed from visibility. */
|
|
readonly deny?: readonly string[]
|
|
}
|
|
|
|
/**
|
|
* A monotonic execution guard evaluated after every `tools/pre-execute`
|
|
* listener and before the tool body. Returning a reason denies the call;
|
|
* returning `undefined` leaves it unchanged. Because guards have no allow
|
|
* result, listener ordering cannot turn a denial back into permission.
|
|
* @param execution - the identity-protected call after extensible pre-execute policy completed.
|
|
* @returns a final denial reason, or `undefined` to leave the call allowed.
|
|
*/
|
|
export type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
|