diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml
index d106745e70..b81d96ba2a 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-20-canonical-tool-output-contract.md: 4099568de5dcc21a89b7873d4a6d4c7e9c62f8e4
-2026-07-20-canonical-tool-output-contract.zh.md: 01b50ef7493ea6548cd238f55e445a702e4d78b3
+2026-07-20-canonical-tool-output-contract.md: 9cc7c97c1f0c9e0753ee826c1e20a3a425e2caf3
+2026-07-20-canonical-tool-output-contract.zh.md: 183a7e6bf212e63dffca1a346aa907f66006444a
diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
index 4099568de5..9cc7c97c1f 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
@@ -24,7 +24,7 @@ output: {
`defineTool` infers the body return and both projectors from the unified `ValueSchemaSpec`. Raw and dynamic definitions provide the compiled `JsonSchemaNode` form. Registration rejects a missing declaration or unsupported raw schema; there is no content-return compatibility path.
-For each successful dispatch the registry snapshots the returned value as lossless `JsonValue`, validates it against `output.schema`, deep-freezes it, then invokes the pure renderer and, for a direct surface call, the optional metadata projector. Renderer, projector, schema, or lossless-JSON failures are contained as ordinary `ToolOutputError` results. An around `tools/execute` wrapper receives and returns the canonical success/failure union; a wrapper-authored success is normalized again through the resolved tool's output declaration instead of trusting independently authored content.
+For each successful dispatch the registry snapshots the returned value as lossless `JsonValue`, validates it against `output.schema`, deep-freezes it, then invokes the pure renderer and, for a direct surface call, the optional metadata projector. Renderer, projector, schema, or lossless-JSON failures are contained as ordinary `ToolOutputError` results. An around `tools/execute` wrapper receives and returns the canonical success/failure union; a wrapper-authored success is normalized again through the resolved tool's output declaration instead of trusting independently authored content. Canonical-result provenance is scoped to the immutable dispatch token, so returning a cached result from another call or tool triggers normalization under the active declaration rather than bypassing it.
```ts ignore-check
type ToolExecutionResult =
diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md
index 01b50ef749..183a7e6bf2 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md
@@ -24,7 +24,7 @@ output: {
`defineTool` 从统一的 `ValueSchemaSpec` 推导工具主体返回值和两个投影器的类型。原始定义和动态定义则提供编译后的 `JsonSchemaNode` 形式。注册时会拒绝缺失输出声明或采用不受支持原始 schema 的定义,不提供兼容旧式内容返回值的路径。
-每次成功分发时,注册表会将返回值快照为无损 `JsonValue`,依据 `output.schema` 校验并深度冻结,然后调用纯渲染器;对于直接的外层调用,还会调用可选的元数据投影器。渲染器、投影器、schema 或无损 JSON 处理失败都会被收敛为普通 `ToolOutputError` 结果。around `tools/execute` 包装层接收并返回规范的成功/失败联合;包装层自行产生的成功结果会再次通过已解析工具的输出声明完成归一化,而不会信任其独立编写的内容。
+每次成功分发时,注册表会将返回值快照为无损 `JsonValue`,依据 `output.schema` 校验并深度冻结,然后调用纯渲染器;对于直接的外层调用,还会调用可选的元数据投影器。渲染器、投影器、schema 或无损 JSON 处理失败都会被收敛为普通 `ToolOutputError` 结果。around `tools/execute` 包装层接收并返回规范的成功/失败联合;包装层自行产生的成功结果会再次通过已解析工具的输出声明完成归一化,而不会信任其独立编写的内容。每个规范结果只归属于一个不可变的分发 token;因此,如果包装层返回来自其他调用或工具的缓存结果,系统会依据当前生效的输出声明重新执行归一化,而不会绕过这一步。
```ts ignore-check
type ToolExecutionResult =
diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
index 2f7a4b1adb..a5273c9e3d 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-20-unified-json-value-schema-dsl.md: e735472ee0ac6a696462aa7598d57fce04aa9c3e
-2026-07-20-unified-json-value-schema-dsl.zh.md: dd34817a6d047b14346b35ba6c4bce0290ad7feb
+2026-07-20-unified-json-value-schema-dsl.md: 3e35bce6eb48afeb31c564e9b5d7b84ff91a7b1f
+2026-07-20-unified-json-value-schema-dsl.zh.md: 77a20d17aab61e759de6e490510b14b5bc408726
diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
index e735472ee0..3e35bce6eb 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
@@ -12,7 +12,9 @@ Tool parameters used a small author DSL while subagent/workflow structured outpu
`dsh-tools` owns one JSON-value schema vocabulary with two representations. `ValueSchemaSpec` is the author form for any JSON root; `ParameterSchemaSpec` is its implicit object-property-map form with per-property `required: true`. `JsonSchemaNode` is the raw wire form. Both support string, finite number, integer, boolean, null, array, object, type-correct scalar `enum`/`const`, and exact-one `oneOf`; `{ type: 'json' }` is author-only sugar for an annotation-only unconstrained raw node.
-An explicit author object must declare `additionalProperties: true | false`. The implicit parameter root and raw JSON Schema preserve the standard open default. `InferValue` and `InferArgs
` derive TypeScript values from the same declarations that `valueSchemaSpecToJsonSchema()` and `parameterSchemaSpecToJsonSchema()` compile. `assertSupportedJsonSchema()` rejects unsupported or misplaced keywords, and `validateJsonSchemaValue()` enforces the accepted subset against the lossless `JsonValue` boundary: no `undefined`, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Intrinsic plain Object and Array containers remain plain across JavaScript realms; subclasses remain exotic. Author compilation, raw-schema assertion, value validation, schema-to-TypeScript rendering, registry detachment, and dynamic Cordis cross-realm normalization and cloning use explicit work stacks, so valid nesting is limited by available memory rather than the JavaScript call stack.
+An explicit author object must declare `additionalProperties: true | false`. The implicit parameter root and raw JSON Schema preserve the standard open default. Schema records contain only own enumerable string keys, schema arrays are dense intrinsic arrays, and supported keywords are read as own properties; custom prototypes, inherited constraints, symbols, and JSON-invisible decorations therefore cannot make compilation, projection, and validation observe different declarations. Intrinsic plain Object and Array containers remain plain across JavaScript realms, while subclasses and forged constructor prototypes remain exotic.
+
+`InferValue` and `InferArgs
` derive TypeScript values from the same declarations that `valueSchemaSpecToJsonSchema()` and `parameterSchemaSpecToJsonSchema()` compile. Exact inference is bounded to 16 container levels and then uses `JsonValue`, preventing TypeScript's type-instantiation stack from becoming the authoring limit. `assertSupportedJsonSchema()` rejects unsupported or misplaced keywords, and `validateJsonSchemaValue()` enforces the accepted subset against the lossless `JsonValue` boundary: no `undefined`, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Author compilation, raw-schema assertion, value validation, schema-to-TypeScript rendering, registry detachment, and dynamic Cordis cross-realm normalization and cloning use explicit work stacks, so runtime nesting is limited by available memory rather than the JavaScript call stack.
Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent and workflow caller-defined structured outputs use `assertObjectJsonSchema()` and `ObjectJsonSchema`; tool outputs may use any root. Dynamic Cordis registrations rebuild realm-foreign schemas into host-owned JSON, preserve raw-wrapper openness, and require direct-DSL object openness before calling the same compiler.
@@ -28,5 +30,6 @@ Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent
- Parameter validation, output validation, schema-to-TypeScript generation, subagent/workflow guards, and dynamic registration share one enforced vocabulary.
- Output declarations can infer object, array, scalar, or null roots; subagent/workflow structured outputs remain object-rooted at their existing seams.
- Explicit object openness and type-correct literal constraints make malformed declarations fail during authoring or registration rather than during a later model call.
+- Bounded type inference retains useful exact types for ordinary declarations and degrades unusually deep tails to `JsonValue`; runtime schema enforcement remains exact at every depth.
- Raw tools may still register broader JSON Schema directly, but unified code generation treats unsupported schemas as unknown instead of pretending to enforce them.
- Runtime and compile-time tests cover every root, exact-one overlap/no-match behavior, raw open defaults, explicit openness, lossy JSON values, inference, and deep nesting across core and dynamic projections.
diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
index dd34817a6d..77a20d17aa 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
@@ -12,7 +12,9 @@ Status: implemented
`dsh-tools` 以两种表示形式统一管理一套 JSON 值 schema 词汇。`ValueSchemaSpec` 是可描述任意 JSON 根类型的作者侧形式;`ParameterSchemaSpec` 是其隐式对象属性映射形式,每个属性可标记 `required: true`。`JsonSchemaNode` 是原始协议表示。两种形式都支持字符串、有限数值、整数、布尔值、null、数组、对象、类型正确的标量 `enum`/`const`,以及要求恰好匹配一个分支的 `oneOf`;`{ type: 'json' }` 仅是作者侧语法糖,会编译为仅含注解、不施加约束的原始节点。
-显式的作者侧对象必须声明 `additionalProperties: true | false`。隐式参数根对象和原始 JSON Schema 保留标准的默认开放语义。`InferValue` 和 `InferArgs
` turns per-property requiredness into required and optional keys:
+`{ type: 'json' }` infers `JsonValue` and compiles to an annotation-only unconstrained raw schema. Output roots can be objects, arrays, scalars, or null. `InferValue` honors literal constraints and object openness through 16 container levels, then falls back to `JsonValue` instead of exhausting TypeScript's type-instantiation stack. `InferArgs
` turns per-property requiredness into required and optional string keys:
```ts type-equiv
/**
- * Infer the TypeScript value accepted by an author-facing value schema.
- * Output schemas may therefore infer object, array, scalar, or null roots.
+ * Infer the TypeScript value accepted by an author-facing value schema. Exact
+ * inference is bounded to 16 container levels, then falls back to `JsonValue`.
*/
-type InferValue =
- D['length'] extends 12 ? JsonValue :
- S extends StringValueSchemaSpec ? InferScalar :
- S extends NumberValueSchemaSpec | IntegerValueSchemaSpec ? InferScalar :
- S extends BooleanValueSchemaSpec ? InferScalar :
- S extends NullValueSchemaSpec ? null :
- S extends ArrayValueSchemaSpec
- ? S extends { items: infer I extends ValueSchemaSpec } ? InferValue>[] : JsonValue[]
- : S extends ObjectValueSchemaSpec ? InferObject> :
- S extends JsonValueSchemaSpec ? JsonValue :
- S extends OneOfValueSchemaSpec ? InferValue> :
- never
+type InferValue = InferValueAt
```
```ts type-equiv
/** Infer the TypeScript argument object for an implicit parameter schema. */
-type InferArgs = InferProperties
+type InferArgs = InferProperties
```
-`defineTool({ name, description, parameters, output, execute, … })` ties parameter inference to `parameterSchemaSpecToJsonSchema()` and `validateArgs()`, and ties `execute`/`render`/`presentationMeta` to `InferValue`. Inference widens to `JsonValue` after twelve nested nodes so large schemas remain compilable; runtime validation keeps walking the complete schema. `valueSchemaSpecToJsonSchema()` compiles output declarations through the same enforced raw subset. A parameter mismatch throws `ToolArgsError` (`INVALID_ARGS`); an invalid body or post-policy value throws `ToolOutputError` (`INVALID_TOOL_OUTPUT`). Both use the normal tool-error path. Raw JSON Schema remains open by default; unsupported keywords reject instead of being accepted without enforcement.
+`defineTool({ name, description, parameters, output, execute, … })` ties parameter inference to `parameterSchemaSpecToJsonSchema()` and `validateArgs()`, and ties `execute`/`render`/`presentationMeta` to `InferValue`. Schema records contain only own enumerable string keys, and schema arrays are dense intrinsic arrays, so inference, compilation, and validation observe the same declaration. Inference stays exact through 16 container levels and then widens to `JsonValue`; runtime validation keeps walking the complete schema. `valueSchemaSpecToJsonSchema()` compiles output declarations through the same enforced raw subset. A parameter mismatch throws `ToolArgsError` (`INVALID_ARGS`); an invalid body or post-policy value throws `ToolOutputError` (`INVALID_TOOL_OUTPUT`). Both use the normal tool-error path. Raw JSON Schema remains open by default; unsupported keywords reject instead of being accepted without enforcement.
Registration is a trusted same-process contract. The registry borrows the typed definition as readonly input, requires `output`, validates its raw schema, and checks semantic requirements such as a positive finite `timeoutMs`; `schemas()` materializes the explicit model-facing projection at the model boundary so execution and presentation share one resolved definition without leaking callbacks onto the wire.
diff --git a/packages/cordis/tool-cordis/src/guard.ts b/packages/cordis/tool-cordis/src/guard.ts
index 60c2b8d0dd..ba22b76c8f 100644
--- a/packages/cordis/tool-cordis/src/guard.ts
+++ b/packages/cordis/tool-cordis/src/guard.ts
@@ -38,13 +38,18 @@ function isPlainRecord(value: unknown): value is Record {
}
/* jscpd:ignore-start -- this VM boundary mirrors the session-owned realm-safe intrinsic test */
-/** Whether a realm-owned intrinsic prototype names and points back to its constructor. */
+/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
const constructor: unknown = descriptor?.value
- return typeof constructor === 'function'
- && constructor.name === name
- && constructor.prototype === prototype
+ if (typeof constructor !== 'function') return false
+ try {
+ return constructor.name === name
+ && constructor.prototype === prototype
+ && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
+ } catch {
+ return false
+ }
}
/** Whether an array uses one realm's intrinsic Array prototype rather than a subclass. */
diff --git a/packages/cordis/tool-cordis/tests/mount.spec.ts b/packages/cordis/tool-cordis/tests/mount.spec.ts
index d2a9f83cf9..4f361a1ef1 100644
--- a/packages/cordis/tool-cordis/tests/mount.spec.ts
+++ b/packages/cordis/tool-cordis/tests/mount.spec.ts
@@ -452,6 +452,9 @@ describe('cordis_mount', () => {
['parameters: { value: { type: \'json\', default: new (class DefaultValue { constructor() { this.ok = true } })() } }', 'parameters.value.default must be lossless JSON data'],
['parameters: { value: { type: \'json\', default: new (class DefaultList extends Array {})() } }', 'parameters.value.default must be lossless JSON data'],
['parameters: { value: { type: \'json\', default: new Date(0) } }', 'parameters.value.default must be lossless JSON data'],
+ ['parameters: (() => { const p = Object.create(null); const C = function C() {}; Object.defineProperty(C, \'name\', { value: \'Object\' }); C.prototype = p; Object.defineProperty(p, \'constructor\', { value: C }); return Object.create(p) })()', 'must be a ParameterSchemaSpec object'],
+ ['parameters: (() => { const p = Object.create(null); const C = function C() {}; Object.defineProperty(C, \'name\', { value: \'Object\' }); C.prototype = p; const r = Proxy.revocable(C, {}); Object.defineProperty(p, \'constructor\', { value: r.proxy }); r.revoke(); return Object.create(p) })()', 'must be a ParameterSchemaSpec object'],
+ ['parameters: Object.create(Object.create(null))', 'must be a ParameterSchemaSpec object'],
])('rejects a malformed ParameterSchemaSpec (%s) with a teaching error', async (parameters, message) => {
const ctx = await setup()
const result = await call(ctx, 'cordis_mount', {
diff --git a/packages/core/session/src/json.ts b/packages/core/session/src/json.ts
index e1452457fd..43e9f0625f 100644
--- a/packages/core/session/src/json.ts
+++ b/packages/core/session/src/json.ts
@@ -12,13 +12,18 @@
*/
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
-/** Whether a realm-owned intrinsic prototype names and points back to its constructor. */
+/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
const constructor: unknown = descriptor?.value
- return typeof constructor === 'function'
- && constructor.name === name
- && constructor.prototype === prototype
+ if (typeof constructor !== 'function') return false
+ try {
+ return constructor.name === name
+ && constructor.prototype === prototype
+ && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
+ } catch {
+ return false
+ }
}
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
diff --git a/packages/core/session/tests/json.spec.ts b/packages/core/session/tests/json.spec.ts
index e8680142f3..d81266440e 100644
--- a/packages/core/session/tests/json.spec.ts
+++ b/packages/core/session/tests/json.spec.ts
@@ -2,6 +2,17 @@ import { runInNewContext } from 'node:vm'
import { describe, expect, it } from 'vitest'
import { isJsonValue, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-session'
+function objectWithForgedIntrinsicPrototype(revoked = false): Record {
+ const prototype = Object.create(null) as Record
+ const ForgedObject = function ForgedObject(): void {}
+ Object.defineProperty(ForgedObject, 'name', { value: 'Object' })
+ ForgedObject.prototype = prototype
+ const constructor = revoked ? Proxy.revocable(ForgedObject, {}) : undefined
+ if (constructor !== undefined) constructor.revoke()
+ Object.defineProperty(prototype, 'constructor', { value: constructor?.proxy ?? ForgedObject })
+ return Object.assign(Object.create(prototype) as Record, { value: 1 })
+}
+
describe('snapshotJsonValue', () => {
it('copies the complete JSON scalar vocabulary and rejects unsupported scalars', () => {
const unsupportedFunction = (): void => {}
@@ -109,6 +120,8 @@ describe('snapshotJsonValue', () => {
const symbolObject = { [Symbol('extra')]: true }
const customPrototype = Object.create(null) as Record
const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record, { value: 1 })
+ const forgedIntrinsicObject = objectWithForgedIntrinsicPrototype()
+ const revokedIntrinsicObject = objectWithForgedIntrinsicPrototype(true)
const forgedPrototype: unknown[] = []
Object.setPrototypeOf(forgedPrototype, null)
const forgedArray = [1]
@@ -133,6 +146,8 @@ describe('snapshotJsonValue', () => {
expect(snapshotJsonValue(hiddenObject)).toBeUndefined()
expect(snapshotJsonValue(symbolObject)).toBeUndefined()
expect(snapshotJsonValue(customPrototypeObject)).toBeUndefined()
+ expect(snapshotJsonValue(forgedIntrinsicObject)).toBeUndefined()
+ expect(snapshotJsonValue(revokedIntrinsicObject)).toBeUndefined()
expect(snapshotJsonValue(forgedArray)).toBeUndefined()
expect(snapshotJsonValue(cyclic)).toBeUndefined()
expect(snapshotJsonValue([undefined])).toBeUndefined()
@@ -206,6 +221,8 @@ describe('isJsonValue', () => {
const symbolObject = { [Symbol('extra')]: true }
const customPrototype = Object.create(null) as Record
const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record, { value: 1 })
+ const forgedIntrinsicObject = objectWithForgedIntrinsicPrototype()
+ const revokedIntrinsicObject = objectWithForgedIntrinsicPrototype(true)
const forgedPrototype: unknown[] = []
Object.setPrototypeOf(forgedPrototype, null)
const forgedArray = [1]
@@ -220,6 +237,8 @@ describe('isJsonValue', () => {
expect(isJsonValue(hiddenObject)).toBe(false)
expect(isJsonValue(symbolObject)).toBe(false)
expect(isJsonValue(customPrototypeObject)).toBe(false)
+ expect(isJsonValue(forgedIntrinsicObject)).toBe(false)
+ expect(isJsonValue(revokedIntrinsicObject)).toBe(false)
expect(isJsonValue(forgedArray)).toBe(false)
expect(isJsonValue(new ExoticArray(1))).toBe(false)
expect(isJsonValue([undefined])).toBe(false)
diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md
index 225b14ea75..d752a84bba 100644
--- a/packages/core/tools/README.md
+++ b/packages/core/tools/README.md
@@ -52,7 +52,7 @@ The live registry pipeline has three transformable waterfalls followed by the ob
- Tool plugins call `ctx.tools.register()` — schemas flow into the assembly automatically.
- `tools/pre-execute` is the reorderable allow/deny/ask gate; `ctx.tools.guard()` adds monotonic owner policy after it.
-- `tools/execute` wraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration.
+- `tools/execute` wraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration. Canonical-result provenance belongs to one immutable dispatch token, so a cached result from another call or tool is revalidated under the active declaration.
- `tools/post-execute` may replace presentation content, replace the canonical value, block with feedback, or attach ordered contexts; `tools/result` observes the immutable final outcome. Content replacement is not a confidentiality boundary: block or replace the value when programmatic consumers must not receive it.
- Exact signatures and ordering live in the generated [event catalog](../../../docs/cordis-catalog/events.md) and [pipeline](../../../docs/tool-execution-pipeline.md).
- MCP servers: one plugin per server, discover tools, call `ctx.tools.register()` with the server's schemas.
@@ -87,7 +87,7 @@ ctx.tools.register(defineTool({
}))
```
-The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default. Compilation, validation, registry detachment, and schema-to-TypeScript rendering use explicit work stacks, so valid deep schemas are memory-bounded rather than call-stack-bounded.
+The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default. Schema records accept only own enumerable string keys, and schema arrays must be dense ordinary arrays. Compilation, validation, registry detachment, and schema-to-TypeScript rendering use explicit work stacks, so runtime processing of valid deep schemas is memory-bounded rather than call-stack-bounded; `InferValue` preserves exact types through 16 container levels and then falls back to `JsonValue` so TypeScript itself remains stack-safe.
A `defineTool` definition validates model arguments before execution and turns missing required values, wrong primitives, invalid enum members, and nested violations into `ToolArgsError` (`INVALID_ARGS`) for the normal error-result path. It also infers the body return and pure output projectors from `output.schema`; the registry snapshots and validates the returned lossless JSON before presentation. The implicit parameter root is open; an explicit object accepts extra keys only with `additionalProperties: true`, and a closed object with no declared properties accepts only `{}`. Raw JSON Schema objects remain open unless they explicitly set `additionalProperties: false`. Defaults are not applied; open objects without `properties` and arrays without `items` receive only a container type check. Raw-registered tools own input validation but still declare and receive registry-enforced output.
diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts
index 4b712b1d07..ebefeb2cf2 100644
--- a/packages/core/tools/src/index.ts
+++ b/packages/core/tools/src/index.ts
@@ -1111,7 +1111,7 @@ export class ToolRegistry extends Service {
if (deferredContexts === undefined) throw new Error('tool registry scheduler invariant violated: unprepared execution')
const resultWithDeferredContexts: ToolExecutionResult = deferredContexts.length === 0
? normalized
- : this.markCanonical({
+ : this.markCanonical(exec, {
...normalized,
additionalContexts: [
...deferredContexts,
@@ -1262,7 +1262,7 @@ export class ToolRegistry extends Service {
const decisionContexts = decision.additionalContexts ?? []
if (decision.kind === 'block') {
const message = failureMessageFromContent(decision.feedback)
- return this.markCanonical({
+ return this.markCanonical(exec, {
content: decision.feedback,
isError: true,
error: { message },
@@ -1283,24 +1283,24 @@ export class ToolRegistry extends Service {
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({
+ return this.markCanonical(exec, {
...replaced,
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
- return this.markCanonical({
+ return this.markCanonical(exec, {
...result,
...decision.content !== undefined ? { content: decision.content } : {},
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
- /** Results created by the registry already own a validated, frozen canonical value. */
- private readonly canonicalResults = new WeakSet