refactor(core): simplify tools prompts and trusted services

This commit is contained in:
Tianyi Cui
2026-07-12 22:39:01 +08:00
parent 28e04ff4fb
commit 02ca71db57
24 changed files with 636 additions and 2695 deletions
+16 -37
View File
@@ -20,7 +20,6 @@
*/
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult } from './index.ts'
import type { ToolCallView, ToolResultView } from './presentation.ts'
@@ -288,21 +287,23 @@ export function validateArgs(spec: SchemaSpec, args: unknown): string[] {
/** Options for {@link defineTool}. */
export interface DefineToolOptions<S extends SchemaSpec> {
/** Tool name (must be unique). */
name: string
readonly name: string
/** Human-readable description sent to the model. */
description: string
readonly description: string
/**
* Parameter schema using the per-property-required DSL. Converted to
* standard JSON Schema at runtime.
*/
parameters: S
readonly parameters: S
/**
* Optional cooperative tool-call timeout budget in milliseconds. When given it
* must be a positive finite number; it is attached to the produced
* {@link ToolDefinition} for `@deepseek-ai/dsh-timeout-policy` to enforce and
* is never sent to the model.
*/
timeoutMs?: number
readonly timeoutMs?: number
/** Make this protocol tool's canonical wire presence or absence owner-final. */
readonly ownerFinal?: boolean
/**
* Tool execution function. `args` is typed as {@link InferArgs<S>} — zero
* casts needed. Returns either a bare {@link ContentBlock}`[]` (model-facing
@@ -355,11 +356,6 @@ export interface DefineToolOptions<S extends SchemaSpec> {
* by `ToolRegistry.register()` directly — `defineTool` is sugar for
* first-party plugin authors.
*
* Definition is an acceptance boundary: every top-level option is read once,
* and the parameter spec is detached before either the wire schema or the
* runtime validators are built. Later mutation of the caller's options or
* schema therefore cannot make the model-visible schema disagree with execute
* or presentation validation.
* @param options - the tool's name, description, typed parameter schema,
* execute body, and optional presenters.
* @returns a registry-ready {@link ToolDefinition}: its `execute` validates the
@@ -369,13 +365,6 @@ export interface DefineToolOptions<S extends SchemaSpec> {
* args).
*/
export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>): ToolDefinition {
// Capture every caller-owned top-level field before inspecting any nested
// schema value. Accessors may be stateful, so validation, presentation, and
// the returned definition must all derive from this one accepted record.
const name = options.name
const description = options.description
const inputParameters = options.parameters
const timeoutMs = options.timeoutMs
// Object-literal execute methods don't use `this`; the reference is safe.
// eslint-disable-next-line @typescript-eslint/unbound-method
const userExecute = options.execute
@@ -383,31 +372,21 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
const userPresentCall = options.presentCall
// eslint-disable-next-line @typescript-eslint/unbound-method
const userPresentResult = options.presentResult
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
throw new Error(`defineTool(${name}): timeoutMs must be a positive finite number`)
}
// The internal SchemaSpec and public wire schema must not share mutable
// subobjects. Each is materialized through the lossless one-pass boundary;
// structuredClone alone could sanitize an exotic default or nested getter.
const parameterSpec = snapshotJsonValue(inputParameters)
if (parameterSpec === undefined) {
throw new Error(`defineTool(${name}): parameters must be losslessly JSON-serializable`)
}
const wireParameters = snapshotJsonValue(schemaSpecToJsonSchema(parameterSpec))
if (wireParameters === undefined) {
throw new Error(`defineTool(${name}): generated parameters must be losslessly JSON-serializable`)
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`)
}
const tool: ToolDefinition = {
name,
description,
parameters: wireParameters as unknown as Record<string, unknown>,
...(timeoutMs !== undefined ? { timeoutMs } : {}),
name: options.name,
description: options.description,
parameters: schemaSpecToJsonSchema(options.parameters) as unknown as Record<string, unknown>,
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
...(options.ownerFinal === true ? { ownerFinal: true } : {}),
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
// cast to InferArgs<S> reflects the validated shape.
const violations = validateArgs(parameterSpec, args)
const violations = validateArgs(options.parameters, args)
if (violations.length > 0) throw new ToolArgsError(violations)
return userExecute(args as InferArgs<S>, exec)
},
@@ -418,13 +397,13 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
// than the hard `ToolArgsError` the execute path raises.
if (userPresentCall) {
tool.presentCall = (args: unknown): ToolCallView | undefined => {
if (validateArgs(parameterSpec, args).length > 0) return undefined
if (validateArgs(options.parameters, args).length > 0) return undefined
return userPresentCall(args as InferArgs<S>)
}
}
if (userPresentResult) {
tool.presentResult = (args: unknown, result: ToolResult): ToolResultView | undefined => {
if (validateArgs(parameterSpec, args).length > 0) return undefined
if (validateArgs(options.parameters, args).length > 0) return undefined
return userPresentResult(args as InferArgs<S>, result)
}
}