Merge commit 'refs/codex-unblock/20260723/master' into worktree/pty-review-fixes

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/tools.md
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/tools/README.md
#	packages/core/tools/src/index.ts
#	packages/core/tools/src/schema.ts
#	packages/core/tools/tests/tools.spec.ts
#	packages/pty/tool-pty/README.md
#	packages/pty/tool-pty/src/index.ts
#	packages/pty/tool-pty/src/render.ts
#	packages/tasks/tool-tasks/README.md
#	packages/tasks/tool-tasks/src/index.ts
This commit is contained in:
Tianyi Cui
2026-07-23 20:50:45 +08:00
584 changed files with 27686 additions and 9816 deletions

View File

@@ -12,40 +12,60 @@ import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
import { assertNever, deepFreeze, HarnessError } from '@deepseek-ai/dsh-llm'
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import type { ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
import type { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
// Type-only: makes `ctx.get('approval')` resolve to the ApprovalService
// augmentation. The seam stays optional at runtime — see `serviceAsk`.
import type {} from '@deepseek-ai/dsh-user-approval'
import type { ToolCallView, ToolResultView } from './presentation.ts'
import { assertSupportedJsonSchema, validateJsonSchemaValue } from './json-schema.ts'
import type { JsonSchemaNode } from './json-schema.ts'
import { createRunCodeTool, RUN_CODE_NAME, SDK_SECTION_ORDER } from './code-mode.ts'
import { renderToolsSdk } from './ts-types.ts'
import type { ToolSdkSchema } from './ts-types.ts'
export {
defineTool,
schemaSpecToJsonSchema,
valueSchemaSpecToJsonSchema,
parameterSchemaSpecToJsonSchema,
validateArgs,
ToolArgsError,
type SchemaSpec,
type SchemaProp,
type SchemaType,
type ValueSchemaAnnotations,
type StringValueSchemaSpec,
type NumberValueSchemaSpec,
type IntegerValueSchemaSpec,
type BooleanValueSchemaSpec,
type NullValueSchemaSpec,
type ArrayValueSchemaSpec,
type ObjectValueSchemaSpec,
type JsonValueSchemaSpec,
type OneOfValueSchemaSpec,
type ValueSchemaSpec,
type ParameterPropertySpec,
type ParameterSchemaSpec,
type ParameterJsonSchema,
type InferValue,
type InferArgs,
type DefineToolOptions,
type JsonSchemaObject,
} from './schema.ts'
export {
assertSupportedOutputSchema,
validateStructuredValue,
OutputSchemaError,
type StructuredOutputSchema,
type StructuredSchemaNode,
type StructuredSchemaType,
type StructuredScalar,
assertSupportedJsonSchema,
assertObjectJsonSchema,
validateJsonSchemaValue,
JsonSchemaError,
type JsonSchemaNode,
type ObjectJsonSchema,
type JsonSchemaType,
type JsonSchemaScalar,
} from './json-schema.ts'
export type { JsonValue } from '@deepseek-ai/dsh-session'
export { CodeRunFailedError, RUN_CODE_NAME } from './code-mode.ts'
export { jsonSchemaToTs, renderToolsSdk } from './ts-types.ts'
export { defineContentToolFixture, type ContentToolFixtureOptions } from './testing.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`
@@ -124,21 +144,31 @@ declare module 'cordis' {
}
}
/** Tool output, optionally with lossless-JSON presentation metadata persisted for replay. */
export type ToolExecuteReturn = ContentBlock[] | { content: ContentBlock[]; meta?: unknown }
/** 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, computed only for surface 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. Async work must observe or forward `exec.signal` and
* settle only after its owned work reaches quiescence. The registry preserves
* caller cancellation through around-dispatch signal replacement and does
* not abandon this promise, but it cannot hard-kill same-process code.
* 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 registry 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 model-facing content plus optional private presentation metadata.
* @returns the canonical value declared by `output.schema`.
*/
execute(args: unknown, exec: ToolRunContext): Promise<ToolExecuteReturn>
execute(args: unknown, exec: ToolRunContext): Promise<unknown>
/**
* Synchronous last-mile transform for model-facing content. The registry
* snapshots this callback when execution starts and invokes it exactly once
@@ -185,7 +215,7 @@ export interface ToolDefinition extends ToolSchema {
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). Returns a
* 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.
@@ -195,17 +225,16 @@ export interface ToolDefinition extends ToolSchema {
/** The completed outcome handed to {@link ToolDefinition.presentResult}. */
export interface ToolResult {
/** The model-facing content `execute` returned (or the error text on failure). */
/** 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 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.
* The tool-private presentation payload projected by its output declaration
* and threaded verbatim from the `tool/result` event. Absent when the tool
* declared no projector or the call was nested under a composite transport.
*/
meta?: unknown
meta?: JsonValue
}
declare const toolExecutionTokenBrand: unique symbol
@@ -337,6 +366,14 @@ export interface ToolErrorInfo {
code: 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
@@ -350,30 +387,73 @@ export class ToolNotFoundError extends HarnessError {
}
}
/** The outcome of one tool call. */
export interface ToolExecutionResult {
content: ContentBlock[]
isError: boolean
/**
* Set when the call failed with a {@link HarnessError}: machine-routable
* `{ name, code }` for retry/sandbox plugins and replay. The model-facing
* text in `content` is always present; this is extra structure for code.
*/
error?: ToolErrorInfo
/**
* Model-facing context for the next request, separate from this tool result. The loop
* accepts it into the active-batch FIFO, then appends after recorded results even if interrupted.
*/
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
* {@link ToolResult} for `presentResult`. Opaque (`unknown`); absent when the
* tool attached none or the call failed.
*/
meta?: unknown
/** 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
}
}
/** Convert one projector exception into the canonical invalid-output failure. */
function projectionError(toolName: string, projector: 'render' | 'presentationMeta', error: unknown): ToolOutputError {
return new ToolOutputError(toolName, [`output.${projector} failed: ${errorMessage(error)}`])
}
/** Snapshot one projector result before later durable-result materialization. */
function snapshotProjection<T>(toolName: string, projector: 'render' | 'presentationMeta', candidate: T): T {
try {
const detached = snapshotJsonValue(candidate)
if (detached === undefined) {
throw new ToolOutputError(toolName, [`output.${projector} returned non-lossless JSON`])
}
return detached
} catch (error: unknown) {
if (error instanceof ToolOutputError) throw error
throw projectionError(toolName, projector, error)
}
}
/** Snapshot one body or policy value into the canonical invalid-output failure class. */
function snapshotToolValue(toolName: string, candidate: unknown): JsonValue {
try {
const detached = snapshotJsonValue(candidate)
if (detached === undefined) throw new ToolOutputError(toolName, ['value is not lossless JSON'])
return detached as JsonValue
} catch (error: unknown) {
if (error instanceof ToolOutputError) throw error
throw new ToolOutputError(toolName, [`value snapshot failed: ${errorMessage(error)}`])
}
}
/** 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?: HookContext[]
}
/** 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?: HookContext[]
}
/** The discriminated, execution-local outcome of one tool call. */
export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure
/**
* Pre-dispatch decision. `allow` runs the call; `deny` materializes an error;
* `ask` runs only after an approval service returns `allowed-once` and otherwise
@@ -386,11 +466,12 @@ export type PreToolDecision =
| { kind: 'ask'; reason?: string }
/**
* Post-dispatch decision: accept or replace content, attach context for the next
* request, or block by turning corrective feedback into an error result.
* 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[]; additionalContexts?: HookContext[] }
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: HookContext[] }
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: HookContext[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: HookContext[] }
/**
@@ -415,6 +496,23 @@ function errorMessage(error: unknown): string {
}
}
/** Derive one failure message from policy feedback without changing its rendered blocks. */
function failureMessageFromContent(content: ContentBlock[]): string {
const text = content
.map(block => block.type === 'text' ? block.text : `[${block.type} content]`)
.join('\n')
return text.length > 0 ? text : 'tool result blocked by post-execute policy'
}
/** Snapshot and freeze one durable tool-result projection or reject lossy data. */
function materializePresentation<T>(candidate: T): T {
const detached = snapshotJsonValue(candidate)
if (detached === undefined) {
throw new TypeError('tool result must be losslessly JSON-serializable')
}
return deepFreeze(detached)
}
/** Structured `{ name, code }` for a thrown HarnessError, else undefined. */
function errorInfo(error: unknown): ToolErrorInfo | undefined {
try {
@@ -583,7 +681,7 @@ export class ToolRegistry extends Service {
// Regenerate from the calling scope's visible tools in stable order.
text: (context) => {
this.requireCodeRuntime()
return renderToolsSdk(this.schemas(context.scope).filter(schema => schema.name !== RUN_CODE_NAME))
return renderToolsSdk(this.sdkSchemas(context.scope))
},
})
}
@@ -636,6 +734,13 @@ export class ToolRegistry extends Service {
*/
register(definition: ToolDefinition): () => void {
const name = definition.name
const output = (definition as Partial<ToolDefinition>).output
if (output === undefined || typeof output !== 'object'
|| typeof output.render !== 'function'
|| (output.presentationMeta !== undefined && typeof output.presentationMeta !== 'function')) {
throw new TypeError(`tool "${name}" must declare output { schema, render, presentationMeta? }`)
}
assertSupportedJsonSchema(output.schema)
const timeoutMs = definition.timeoutMs
if (timeoutMs !== undefined
&& (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
@@ -769,13 +874,34 @@ export class ToolRegistry extends Service {
return [...this.view(scope).visible.values()].map(definition => this.schemaOf(definition, true))
}
/** Project visible callable tools onto the generated Code Mode SDK contract. */
private sdkSchemas(scope?: ScopeKey): ToolSdkSchema[] {
return [...this.view(scope).visible.values()]
.filter(definition => definition.name !== RUN_CODE_NAME)
.map((definition): ToolSdkSchema => {
const output = snapshotJsonValue(definition.output.schema)
/* v8 ignore next -- registration already validated and retained this schema as lossless JSON. */
if (output === undefined) {
throw new Error(`tool "${definition.name}" output schema must be lossless JSON before SDK projection`)
}
return {
...this.schemaOf(definition, true),
output,
}
})
}
/** Project one definition onto the model-facing schema fields. */
private schemaOf(definition: ToolDefinition, detachParameters: boolean): ToolSchema {
const { name, description, parameters } = definition
const detached = detachParameters ? snapshotJsonValue(parameters) : parameters
if (detached === undefined) {
throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`)
}
return {
name,
description,
parameters: detachParameters ? structuredClone(parameters) : parameters,
parameters: detached,
}
}
@@ -914,10 +1040,11 @@ export class ToolRegistry extends Service {
return await next({
kind: 'post-result',
exec,
result: {
result: this.materializeFinalResult({
content: [{ type: 'text', text: `Error: ${denialReason}` }],
isError: true,
},
error: { message: denialReason },
}),
})
}
if (this.callerCancelled(exec)) {
@@ -970,13 +1097,7 @@ export class ToolRegistry extends Service {
if (!tool) throw new ToolNotFoundError(exec.name)
state.bodyInvoked = true
const returned = await tool.execute(exec.arguments, exec)
const content = Array.isArray(returned) ? returned : returned.content
const meta = Array.isArray(returned) ? undefined : returned.meta
const result: ToolExecutionResult = {
content,
isError: false,
...meta !== undefined ? { meta } : {},
}
const result = this.createSuccessResult(exec, tool, returned)
return isAborted(signal)
? toolAbortedResult(result)
: result
@@ -1003,18 +1124,19 @@ export class ToolRegistry extends Service {
carrier, 'tools/execute', mutableExec,
() => this.dispatchToolBody(mutableExec),
)
const normalized = this.normalizeDispatchResult(exec, result)
const deferredContexts = this.deferredContexts.get(exec)
/* v8 ignore next -- dispatch only receives executions minted by this registry's prepare stage */
if (deferredContexts === undefined) throw new Error('tool registry scheduler invariant violated: unprepared execution')
const resultWithDeferredContexts: ToolExecutionResult = deferredContexts.length === 0
? result
: {
...result,
? normalized
: this.markCanonical(exec, {
...normalized,
additionalContexts: [
...deferredContexts,
...result.additionalContexts ?? [],
...normalized.additionalContexts ?? [],
],
}
})
return {
kind: 'post-result',
result: this.callerCancelled(exec) && !resultWithDeferredContexts.isError
@@ -1049,23 +1171,23 @@ export class ToolRegistry extends Service {
}
/**
* Apply definition-owned content finalization, then materialize and notify a
* final result that must bypass post-execute.
* Materialize the candidate, apply definition-owned content finalization,
* then materialize and notify the authoritative result.
* @param exec - the prepared execution.
* @param result - final result.
* @returns the materialized final result.
* @internal
*/
private finishScheduledExecution(exec: ToolRunContext, result: ToolExecutionResult): ToolExecutionResult {
let snapshottedResult: ToolExecutionResult
let materializedResult: ToolExecutionResult
try {
snapshottedResult = this.snapshotFinalResult(result)
materializedResult = this.materializeFinalResult(result)
} catch (error: unknown) {
snapshottedResult = toolErrorResult(error)
materializedResult = this.materializeFinalResult(toolErrorResult(error))
}
let finalResult: ToolExecutionResult
try {
finalResult = this.materializeFinalResult(this.applyFinalContent(exec, snapshottedResult))
finalResult = this.materializeFinalResult(this.applyFinalContent(exec, materializedResult))
} catch (error: unknown) {
finalResult = this.materializeFinalResult(toolErrorResult(error))
}
@@ -1174,37 +1296,113 @@ export class ToolRegistry extends Service {
)
const decisionContexts = decision.additionalContexts ?? []
if (decision.kind === 'block') {
return {
const message = failureMessageFromContent(decision.feedback)
return this.markCanonical(exec, {
content: decision.feedback,
isError: true,
error: { message },
...decisionContexts.length > 0 ? { additionalContexts: decisionContexts } : {},
}
})
}
if (Object.hasOwn(decision, 'content') && Object.hasOwn(decision, 'value')) {
throw new TypeError('tools/post-execute accept decision cannot replace both value and content')
}
// 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 } : {},
...additionalContexts.length > 0 ? { additionalContexts } : {},
if (Object.hasOwn(decision, 'value')) {
if (result.isError) {
throw new TypeError('tools/post-execute cannot replace the value of a failed result')
}
const tool = this.get(exec.name, exec.agent)
if (tool === undefined) throw new ToolNotFoundError(exec.name)
const replaced = this.createSuccessResult(exec, tool, decision.value)
return this.markCanonical(exec, {
...replaced,
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
return this.markCanonical(exec, {
...result,
...decision.content !== undefined ? { content: decision.content } : {},
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
/** Validate and detach one candidate outcome before tool-owned final content. */
private snapshotFinalResult(result: ToolExecutionResult): ToolExecutionResult {
const detached = snapshotJsonValue(result)
if (detached === undefined) {
throw new TypeError('tool result must be losslessly JSON-serializable')
/** Registry-normalized results and the exact dispatch that validated each value. */
private readonly canonicalResults = new WeakMap<object, ToolExecutionToken>()
/** Mark one registry-normalized result as canonical only for its owning dispatch. */
private markCanonical<T extends ToolExecutionResult>(exec: ToolExecution, result: T): T {
this.canonicalResults.set(result, exec.token)
return result
}
/** Snapshot, validate, render, and optionally project one successful body value. */
private createSuccessResult(exec: ToolExecution, tool: ToolDefinition, candidate: unknown): ToolExecutionSuccess {
const detached = snapshotToolValue(tool.name, candidate)
const violations = validateJsonSchemaValue(tool.output.schema, detached, 'value')
if (violations.length > 0) throw new ToolOutputError(tool.name, violations)
const value = deepFreeze(detached)
let rendered: ContentBlock[]
try {
rendered = tool.output.render(exec.arguments, value)
} catch (error: unknown) {
throw projectionError(tool.name, 'render', error)
}
return detached
const content = snapshotProjection(tool.name, 'render', rendered)
let meta: JsonValue | undefined
if (exec.parent === undefined && tool.output.presentationMeta !== undefined) {
let projected: JsonValue
try {
projected = tool.output.presentationMeta(exec.arguments, value)
} catch (error: unknown) {
throw projectionError(tool.name, 'presentationMeta', error)
}
meta = snapshotProjection(tool.name, 'presentationMeta', projected)
}
return this.markCanonical(exec, this.materializeFinalResult({
isError: false,
value,
content,
...meta !== undefined ? { meta } : {},
}) as ToolExecutionSuccess)
}
/** Normalize an around-dispatch wrapper's authored result through the owning output contract. */
private normalizeDispatchResult(exec: ToolExecution, result: ToolExecutionResult): ToolExecutionResult {
if (this.canonicalResults.get(result) === exec.token) return result
if (result.isError) {
return this.markCanonical(exec, {
isError: true,
error: result.error,
content: result.content,
...result.meta !== undefined ? { meta: result.meta } : {},
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
})
}
const tool = this.get(exec.name, exec.agent)
if (tool === undefined) throw new ToolNotFoundError(exec.name)
const normalized = this.createSuccessResult(exec, tool, result.value)
return this.markCanonical(exec, {
...normalized,
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
})
}
/** Materialize the authoritative commit outcome once, immediately before `tools/result`. */
private materializeFinalResult(result: ToolExecutionResult): ToolExecutionResult {
return deepFreeze(this.snapshotFinalResult(result))
const presentation = {
content: result.content,
...result.meta !== undefined ? { meta: result.meta } : {},
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
}
if (result.isError) {
return materializePresentation({ isError: true as const, error: result.error, ...presentation })
}
const detached = materializePresentation({ isError: false as const, ...presentation })
return deepFreeze({ ...detached, value: result.value })
}
}
@@ -1215,10 +1413,11 @@ function createExecutionToken(): ToolExecutionToken {
function toolErrorResult(error: unknown): ToolExecutionResult {
const info = errorInfo(error)
const message = errorMessage(error)
return {
content: [{ type: 'text', text: `Error: ${errorMessage(error)}` }],
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
...info ? { error: info } : {},
error: { message, ...info ? { info } : {} },
}
}
@@ -1266,7 +1465,10 @@ function toolAbortedResult(prior?: ToolExecutionResult): ToolExecutionResult {
return {
content: [{ type: 'text', text: 'Error: tool call aborted' }],
isError: true,
error: { name: 'AbortError', code: TOOL_ABORTED },
error: {
message: 'tool call aborted',
info: { name: 'AbortError', code: TOOL_ABORTED },
},
...additionalContexts.length > 0 ? { additionalContexts } : {},
}
}
@@ -1277,7 +1479,10 @@ function toolAbortedBeforeDispatchResult(prior?: ToolExecutionResult): ToolExecu
return {
content: [{ type: 'text', text: 'Error: tool call aborted before dispatch' }],
isError: true,
error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
error: {
message: 'tool call aborted before dispatch',
info: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
},
...additionalContexts.length > 0 ? { additionalContexts } : {},
}
}