Merge remote-tracking branch 'origin/master' into codex/simp-drop-unused-schema-default

This commit is contained in:
Tianyi Cui
2026-07-18 11:55:33 +08:00
678 changed files with 37644 additions and 8578 deletions
+24 -12
View File
@@ -5,6 +5,7 @@
* @module @deepseek-ai/dsh-tools/src/code-mode
*/
import { parse } from 'node:path'
import { inspect } from 'node:util'
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
@@ -16,11 +17,19 @@ import type { ToolDefinition, ToolRegistry } from './index.ts'
declare module '@deepseek-ai/dsh-session' {
interface SessionEventMap {
/**
* One bridged sub-dispatch from a `run_code` program: the parent `run_code` call id, the
* deterministic sub-call id (`<parent>:code:<n>`), the tool `name` with its
* JSON-normalized `arguments` — the exact value dispatched, normalized before dispatch, so
* this append can never fail on payload shape — whether the sub-call errored, and a
* bounded `resultSummary` of its model-facing text.
* One bridged sub-dispatch from a `run_code` program: the parent
* `run_code` call id, the deterministic sub-call id
* (`<parent>:code:<n>`), the tool `name` with its JSON-normalized
* `arguments` — the exact value dispatched, normalized BEFORE dispatch,
* so this append can never fail on payload shape — whether the sub-call
* errored, and a bounded `resultSummary` of its model-facing text. Before
* bounding, occurrences of a non-root session workspace path are
* normalized to `.` so host-specific absolute path lengths cannot change
* the summary.
* Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
* model context; persistence and UIs get every call. Appended inside the
* parent `run_code`'s execution (the bridge drains its queue before
* returning), so the turn-enclosure invariant holds by construction.
*/
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; resultSummary: string }
}
@@ -70,9 +79,12 @@ function textOf(content: ContentBlock[]): string {
.join('\n')
}
/** Bound a sub-call's model-facing text for the log event's `resultSummary`. */
function summarize(text: string): string {
return text.length > SUMMARY_MAX_CHARS ? `${text.slice(0, SUMMARY_MAX_CHARS)}` : text
/** Normalize workspace paths, then bound a sub-call's model-facing text for its durable log summary. */
function summarize(text: string, cwd: string | undefined): string {
const stableText = cwd === undefined || cwd === parse(cwd).root
? text
: text.replaceAll(cwd, '.')
return stableText.length > SUMMARY_MAX_CHARS ? `${stableText.slice(0, SUMMARY_MAX_CHARS)}` : stableText
}
/**
@@ -189,10 +201,10 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
parent: exec.token,
signal: runController.signal,
})
for (const context of result.additionalContexts ?? []) {
exec.deferContext(context)
}
const text = textOf(result.content)
// Sub-call `additionalContext` is deliberately DROPPED here: the loop's buffering
// (append after the step's tool/results) has no safe analogue from inside a running
// run_code — injecting now would break tool-call/result adjacency.
exec.agent?.session.append('tool/code-dispatch', {
parentCallId: exec.callId,
subCallId,
@@ -202,7 +214,7 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
// this record from what it actually received.
arguments: normalized.logged,
isError: result.isError,
resultSummary: summarize(text),
resultSummary: summarize(text, exec.agent.session.header.cwd),
})
return { text, isError: result.isError }
})
+50 -14
View File
@@ -125,7 +125,7 @@ export type ToolExecuteReturn = ContentBlock[] | { content: ContentBlock[]; meta
/** A registered tool: its schema plus the execution function. */
export interface ToolDefinition extends ToolSchema {
execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn>
execute(args: unknown, exec: ToolRunContext): Promise<ToolExecuteReturn>
/**
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
* Enforced by `@deepseek-ai/dsh-timeout-policy` (a `tools/execute` wrapper); it
@@ -207,6 +207,21 @@ export interface ToolExecution extends ToolExecutionInput {
readonly token: ToolExecutionToken
}
/**
* Runtime context handed to a tool implementation after the registry has
* accepted a {@link ToolExecution}. A composite tool uses
* {@link deferContext} to ferry context produced by nested dispatches back to
* the outer result; the loop appends it only after the outer `tool/result`.
*/
export interface ToolRunContext extends ToolExecution {
/**
* Defer one nested-dispatch context until this tool's final result reaches
* the agent loop. Contexts retain their individual source, envelope, and
* metadata and are emitted in call order.
*/
deferContext(context: HookContext): void
}
/** Structured error metadata for a failed tool call (alongside the model-facing text). */
export interface ToolErrorInfo {
name: string
@@ -240,7 +255,7 @@ export interface ToolExecutionResult {
* Model-facing context for the next request, separate from this tool result.
* The loop buffers it until all step results are logged, preserving pairing.
*/
additionalContext?: HookContext
additionalContexts?: HookContext[]
/**
* The tool-private presentation payload from a successful `execute` (the object
* return form). Threaded onto the `tool/result` session event and back into
@@ -266,8 +281,8 @@ export type PreToolDecision =
* request, or block by turning corrective feedback into an error result.
*/
export type PostToolDecision =
| { kind: 'accept'; content?: ContentBlock[]; additionalContext?: HookContext }
| { kind: 'block'; feedback: ContentBlock[]; additionalContext?: HookContext }
| { kind: 'accept'; content?: ContentBlock[]; additionalContexts?: HookContext[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: HookContext[] }
/**
* Best-effort human-readable message from an arbitrary thrown value: Error
@@ -677,6 +692,7 @@ export class ToolRegistry extends Service {
* @returns the materialized final result.
*/
async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult> {
const deferredContexts: HookContext[] = []
const token = createExecutionToken()
const callId = exec.callId
const name = exec.name
@@ -690,8 +706,11 @@ export class ToolRegistry extends Service {
...agent !== undefined ? { agent } : {},
...parent !== undefined ? { parent } : {},
...signal !== undefined ? { signal } : {},
deferContext(context: HookContext): void {
deferredContexts.push(context)
},
}
let execution: ToolExecution
let execution: ToolRunContext
try {
const detached = snapshotJsonValue(exec.arguments)
if (detached === undefined) {
@@ -709,7 +728,7 @@ export class ToolRegistry extends Service {
}
let result: ToolExecutionResult
try {
result = this.materializeFinalResult(await this.executePipeline(execution))
result = this.materializeFinalResult(await this.executePipeline(execution, deferredContexts))
} catch (error: unknown) {
// Outer backstop: a throwing pre/post-execute listener, guard, or the
// waterfall machinery becomes an isError result, never a turn failure.
@@ -720,7 +739,7 @@ export class ToolRegistry extends Service {
}
/** Run the transformable pipeline; {@link execute} owns final normalization and notification. */
private async executePipeline(exec: ToolExecution): Promise<ToolExecutionResult> {
private async executePipeline(exec: ToolRunContext, deferredContexts: HookContext[]): Promise<ToolExecutionResult> {
// --- Gate: tools/pre-execute. An `ask` resolves through the optional
// approval seam (or degrades to deny) before the monotonic guards run. The
// carrier keys dispatch by exec.agent, so an `agent.ctx` listener gates only
@@ -774,7 +793,16 @@ export class ToolRegistry extends Service {
}
},
)
return await this.postExecute(exec, result)
const resultWithDeferredContexts: ToolExecutionResult = deferredContexts.length === 0
? result
: {
...result,
additionalContexts: [
...deferredContexts,
...result.additionalContexts ?? [],
],
}
return await this.postExecute(exec, resultWithDeferredContexts)
}
/** Notify final-result observers without giving them a mutation/error channel into the outcome. */
@@ -836,8 +864,11 @@ export class ToolRegistry extends Service {
* Run the `tools/post-execute` waterfall over a dispatched `result` and apply
* its {@link PostToolDecision}: `accept` keeps the call successful (replacing
* `content` when given), `block` turns it into an `isError` whose content is
* the corrective `feedback`. Either decision may attach `additionalContext`,
* which is ferried on the returned result for the loop's per-step buffer.
* the corrective `feedback`. Either decision may attach `additionalContexts`,
* which are ferried on the returned result for the loop's per-step buffer.
* Context deferred by the tool body survives an accepted result but is
* discarded when the outer call is blocked; a block exposes only context the
* blocking decision explicitly supplied.
* Runs inside `execute`'s outer try/catch (a throwing listener → isError).
*/
private async postExecute(exec: ToolExecution, result: ToolExecutionResult): Promise<ToolExecutionResult> {
@@ -845,19 +876,24 @@ export class ToolRegistry extends Service {
scopeTarget(this, exec.agent), 'tools/post-execute', exec, result,
() => Promise.resolve<PostToolDecision>({ kind: 'accept' }),
)
const additionalContext = decision.additionalContext
const decisionContexts = decision.additionalContexts ?? []
if (decision.kind === 'block') {
return {
content: decision.feedback,
isError: true,
...additionalContext ? { additionalContext } : {},
...decisionContexts.length > 0 ? { additionalContexts: decisionContexts } : {},
}
}
// Accept: replace content if supplied and preserve the dispatched outcome.
// Accept: replace content if supplied, preserve the dispatched outcome, and
// append decision contexts after contexts deferred by the tool body.
const additionalContexts = [
...result.additionalContexts ?? [],
...decisionContexts,
]
return {
...result,
...decision.content ? { content: decision.content } : {},
...additionalContext ? { additionalContext } : {},
...additionalContexts.length > 0 ? { additionalContexts } : {},
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
/** Typed tool-parameter DSL with argument inference and JSON Schema output. @module dsh-tools/schema */
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ToolDefinition, ToolExecuteReturn, ToolExecution, ToolResult } from './index.ts'
import type { ToolDefinition, ToolExecuteReturn, ToolRunContext, ToolResult } from './index.ts'
import type { ToolCallView, ToolResultView } from './presentation.ts'
// ---------------------------------------------------------------------------
@@ -285,7 +285,7 @@ export interface DefineToolOptions<S extends SchemaSpec> {
* 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<ToolExecuteReturn>
execute(args: InferArgs<S>, exec: ToolRunContext): 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
@@ -329,7 +329,7 @@ export function defineTool<S extends SchemaSpec>(options: DefineToolOptions<S>):
description: options.description,
parameters: schemaSpecToJsonSchema(options.parameters) as unknown as Record<string, unknown>,
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
async execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn> {
async execute(args: unknown, exec: ToolRunContext): 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