From 01da49a3aba3ee285784262af847988f70935ad9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:07:41 +0800 Subject: [PATCH 1/5] refactor: prune code runtime surface --- docs/cordis-catalog/services.md | 2 +- docs/core-data-structures/code-runtime.md | 17 ++----- .../feature/2026-06-15-code-mode.md | 5 +- .../code-runtime-worker/README.md | 4 +- .../code-runtime-worker/package.json | 1 - .../code-runtime-worker/src/bootstrap.ts | 40 +++++++-------- .../code-runtime-worker/src/index.ts | 44 ++++++---------- .../code-runtime-worker/src/protocol.ts | 8 ++- .../tests/bootstrap.spec.ts | 50 +++++++++---------- .../tests/built-lib.e2e.ts | 4 +- .../code-runtime-worker/tests/runtime.spec.ts | 36 ++++++------- packages/code-runtime/code-runtime/README.md | 2 +- .../code-runtime/code-runtime/src/index.ts | 1 - .../code-runtime/code-runtime/src/types.ts | 18 +------ .../code-runtime/tests/service.spec.ts | 2 +- .../cordis/tool-cordis/src/api-catalog.ts | 6 +-- packages/core/tools/src/code-mode.ts | 11 ++-- packages/core/tools/tests/code-mode.spec.ts | 11 ++-- scripts/type-equiv.manifest.json | 1 - 19 files changed, 100 insertions(+), 163 deletions(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 28d1904ecf..e95ad6af70 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -98,7 +98,7 @@ abstract run(request: CodeRunRequest): Promise Types: [CodeRunRequest](../core-data-structures/code-runtime.md) · [CodeRunResult](../core-data-structures/code-runtime.md) -Source: [`packages/code-runtime/code-runtime/src/index.ts:59`](../../packages/code-runtime/code-runtime/src/index.ts) +Source: [`packages/code-runtime/code-runtime/src/index.ts:58`](../../packages/code-runtime/code-runtime/src/index.ts) ## `ctx.compact` — `CompactService` (abstract seam) diff --git a/docs/core-data-structures/code-runtime.md b/docs/core-data-structures/code-runtime.md index cb1661e02f..9237a3cce9 100644 --- a/docs/core-data-structures/code-runtime.md +++ b/docs/core-data-structures/code-runtime.md @@ -39,8 +39,8 @@ interface CodeRunResult { * or value-less run leaves this absent. */ value?: unknown - /** Everything the program emitted, in order (capped by the implementation). */ - logs: CodeLogEntry[] + /** Text the program emitted, in order (capped by the implementation). */ + logs: string[] /** Present iff the run failed; see {@link CodeRunFailure} for the taxonomy. */ error?: CodeRunFailure } @@ -65,18 +65,7 @@ type CodeBindingFunction = (args: unknown) => Promise ## Captured output and the failure taxonomy -Logs arrive in emission order, attributed to their channel (the runtime's `console` shim, or stray writes to the underlying streams): - -```ts type-equiv -interface CodeLogEntry { - /** Which channel produced the text. */ - source: 'console' | 'stdout' | 'stderr' - /** The console method used; present only when `source` is `'console'`. */ - level?: 'log' | 'info' | 'warn' | 'error' | 'debug' - /** The captured text (possibly truncated by the implementation's caps, marked in-band). */ - text: string -} -``` +Logs are plain strings in emission order. The runtime captures the program's console and stream output, but channel and console-method metadata are not part of the seam because consumers render only the text. Implementations cap the aggregate output and mark truncation in-band. Failure kinds are **orthogonal outcomes reported independently** (per [defensive-patterns](../defensive-patterns.md)): a budget expiry is not an exception, an abort is not a timeout, and a substrate death (e.g. OOM) is neither: diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.md b/docs/rfc/implemented/feature/2026-06-15-code-mode.md index 60acfa7c88..32d60aafcf 100644 --- a/docs/rfc/implemented/feature/2026-06-15-code-mode.md +++ b/docs/rfc/implemented/feature/2026-06-15-code-mode.md @@ -40,7 +40,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat 1. **Builds the bindings**: the bridge owns a **run-scoped `AbortController`** whose signal follows `exec.signal` (an outer cancel propagates in) and which the bridge itself fires the moment the run settles for any reason — completion, program exception, `computeMs`/`maxWallMs` expiry, worker exit. For every visible capability tool, the binding is an async function that (a) checks the run signal before and after, (b) **JSON-normalizes the argument** — a `JSON.parse(JSON.stringify(args))` round-trip, rejecting that one call with a descriptive `Error` when the value does not survive (`BigInt`, circular structures) — because the seam's structured-clone boundary is wider than JSON while the session log accepts only JSON, (c) awaits its turn on the **per-run serialization queue** (below), (d) calls `this.execute({ callId, name, arguments, agent: exec.agent, parent: exec.token, signal: runSignal })` with a deterministic sub-id `` CallId(`${exec.callId}:code:${n}`) ``, (e) appends a `tool/code-dispatch` session event, and (f) maps the result: success → the text-block contents joined as a `string` (non-text blocks become placeholders), `isError` → **the binding rejects** with an `Error` carrying the result text. The child's readonly `parent` is only the outer execution's frozen, property-free token, so commit-style observers can correlate outcomes without receiving a mutation path into the live `run_code` wrapper. Every sub-call still traverses the full pipeline under its own immutable identity and registry-assigned token. The run signal, rather than the bare outer one, lets budget expiry abort an in-flight sub-tool instead of orphaning it. Rejection gives programs ordinary `try/catch` and `Promise.all` failure semantics rather than a bespoke result envelope. 2. **Runs the program**: `ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`. The runtime receives the run-scoped signal, not only the caller's outer signal, so any way the outer run settles also aborts work inside the runtime. -3. **Surfaces the outcome — after reaching quiescence.** When `ctx.codeRuntime.run()` settles, whether by fulfillment or rejection, the bridge fires the run-scoped abort (cancelling any in-flight sub-dispatch and abandoning queued-unstarted ones), then **awaits the dispatch queue's drain before returning or propagating**, per the dispose-to-quiescence rule in [defensive patterns](../../../defensive-patterns.md): an aborted in-flight sub-call still settles and logs its `isError` `tool/code-dispatch` event *inside* the open turn, and nothing can append after `run_code` settles. A successful result then returns one text block — the captured console/stdout output followed by the rendered return value (if any) — plus a `meta` payload (capped logs, dispatch count) for presentation. A fulfilled run with `result.error` throws a `CodeRunFailedError extends HarnessError` (`code: 'CODE_RUN_FAILED'`, message = the error kind and text plus captured logs so the model can self-correct); a backend rejection propagates through the same registry error boundary. Both become structured `isError` tool results. +3. **Surfaces the outcome — after reaching quiescence.** When `ctx.codeRuntime.run()` settles, whether by fulfillment or rejection, the bridge fires the run-scoped abort (cancelling any in-flight sub-dispatch and abandoning queued-unstarted ones), then **awaits the dispatch queue's drain before returning or propagating**, per the dispose-to-quiescence rule in [defensive patterns](../../../defensive-patterns.md): an aborted in-flight sub-call still settles and logs its `isError` `tool/code-dispatch` event *inside* the open turn, and nothing can append after `run_code` settles. A successful result then returns one text block — the captured console/stdout output followed by the rendered return value (if any) — plus the capped log strings as presentation metadata. A fulfilled run with `result.error` throws a `CodeRunFailedError extends HarnessError` (`code: 'CODE_RUN_FAILED'`, message = the error kind and text plus captured logs so the model can self-correct); a backend rejection propagates through the same registry error boundary. Both become structured `isError` tool results. **Sub-call `additionalContext` is suppressed, deliberately.** A `tools/post-execute` hook may attach `additionalContext` to a call; for loop-dispatched calls the loop buffers those and appends each as a `context/message` only after the step's `tool/result`s, preserving call/result adjacency. A sub-dispatch result's `additionalContext` has no such safe outlet from inside a running `run_code`: injecting immediately would land a `context/message` between the parent's `tool/call` and its `tool/result` (breaking the adjacency the buffering exists to protect), and `PostToolDecision.additionalContext` is singular where a program may produce many. The MVP therefore drops sub-call `additionalContext`, pinned by a test and stated in the hooks bridge's docs; the follow-up (a plural context channel or loop-level sub-dispatch buffering) is deferred until a real hook needs it through Code Mode. @@ -58,8 +58,7 @@ Each sub-dispatch appends one session event, declared by `dsh-tools` via `Sessio - `CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }` - `CodeBindingNamespace = { global: string; functions: Record Promise> }` — the runtime exposes each namespace as a global object of async functions inside the program; binding arguments and resolutions must be structured-cloneable (a runtime may cross a serialization boundary; ours does). -- `CodeRunResult = { value?: unknown; logs: CodeLogEntry[]; error?: CodeRunFailure }` — program execution outcomes, including exception, timeout, abort, and worker exit, resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary. -- `CodeLogEntry = { source: 'console' | 'stdout' | 'stderr'; level?: 'log' | 'info' | 'warn' | 'error' | 'debug'; text: string }` +- `CodeRunResult = { value?: unknown; logs: string[]; error?: CodeRunFailure }` — program execution outcomes, including exception, timeout, abort, and worker exit, resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary. - `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout. - Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for the shipped backend; a Python backend would say so, and pair with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all). diff --git a/packages/code-runtime/code-runtime-worker/README.md b/packages/code-runtime/code-runtime-worker/README.md index 8138f01539..705174d0e5 100644 --- a/packages/code-runtime/code-runtime-worker/README.md +++ b/packages/code-runtime/code-runtime-worker/README.md @@ -23,10 +23,12 @@ Every field is validated (positive numbers) and defaulted; there are no other tu - **Type-strip host-side, in execution context** — the program is wrapped in an async-function shell, stripped with `node:module`'s `stripTypeScriptTypes` (erasable syntax only — `enum`/namespaces are rejected as a program `exception` and no worker spawns), and sliced back out byte-positioned; it then executes as the body of an `AsyncFunction`, so top-level `await`/`return` work. - **The port assumes a hostile peer** — model code can reach `parentPort` and forge traffic, so every inbound message is shape-validated and REBUILT before anything reads it (`null`, primitives, junk types, and malformed payloads drop without a throw; forged extra fields never ride along), the host answers each call id at most once, resolves binding names as OWN properties only (a forged `constructor` cannot walk a prototype chain), drops post-settlement replies, and converts a non-cloneable binding resolution into an error reply. Forged `log`/`done` messages cannot bypass the caps: one host-side ledger bounds everything that lands in `logs`, and the completion value is re-capped host-side. Worker-side namespaces are null-prototype with `defineProperty`, so `__proto__`-shaped binding names are ordinary keys. - **Two independent budgets, because the peer is hostile** — `computeMs` meters the worker's MEASURED busy time (`worker.performance.eventLoopUtilization()` polling): a hot loop cannot hide behind a pending decoy dispatch, and a program awaiting a slow tool accrues nothing. `maxWallMs` backstops what busy time cannot see (awaiting a promise nobody resolves). Both funnel into `worker.terminate()`, which ends hot synchronous loops too; heap overflow surfaces as the worker's OOM exit (`kind: 'worker-exit'`). -- **Logs stream eagerly** — console/stdout/stderr entries cross the port as they happen, so a timed-out or killed program still shows what it printed. ONE shared `maxLogBytes` ledger bounds everything: streamed entries, forged port traffic, and pipe bytes that bypass the patched streams (appended after), with the overflow marked in-band once. +- **Logs stream eagerly** — console/stdout/stderr text crosses the port in emission order, so a timed-out or killed program still shows what it printed. ONE shared `maxLogBytes` ledger bounds everything: streamed text, forged port traffic, and pipe bytes that bypass the patched streams (appended after), with the overflow marked in-band once. - **Empty environment** — the worker gets `env: {}` and `execArgv: []`: no ambient credentials (stronger than the scrubbed-env rule for spawned commands) and no inherited loader flags. - **Dispose to quiescence** — teardown fails in-flight runs as `abort` and AWAITS each worker's exit before resolving. ## The worker entry, unbuilt and built `worker.ts` is deliberately erasable-only TypeScript with type-only cross-package imports: unbuilt (vitest/tsx), the host spawns `src/worker.ts` directly and Node's native type stripping loads it; built, the entry ships as the sibling CommonJS bundle `lib/worker.cjs` (its own tsdown entry). The CommonJS format is required because pkg's VFS Worker hook compiles filesystem-string entries as CommonJS. The host converts either entry URL to a filesystem string before constructing `Worker`, which works through both ordinary Node resolution and that pkg hook. The built path is pinned by `tests/built-lib.e2e.ts`, the real-load-path guard from [docs/testing.md](../../../docs/testing.md). + +The SDK surface is the default/named `WorkerCodeRuntime` class plus `Config`. The operational `./worker` subpath exists only as the packaged spawn entry; the wire protocol and bootstrap helpers are source-private implementation details. diff --git a/packages/code-runtime/code-runtime-worker/package.json b/packages/code-runtime/code-runtime-worker/package.json index 91075243d2..3c4ee1d931 100644 --- a/packages/code-runtime/code-runtime-worker/package.json +++ b/packages/code-runtime/code-runtime-worker/package.json @@ -15,7 +15,6 @@ "types": "./lib/types/worker.d.ts", "default": "./lib/worker.cjs" }, - "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ diff --git a/packages/code-runtime/code-runtime-worker/src/bootstrap.ts b/packages/code-runtime/code-runtime-worker/src/bootstrap.ts index f2e0d343f3..a152b6b278 100644 --- a/packages/code-runtime/code-runtime-worker/src/bootstrap.ts +++ b/packages/code-runtime/code-runtime-worker/src/bootstrap.ts @@ -12,7 +12,6 @@ import { inspect } from 'node:util' import { serialize } from 'node:v8' -import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime' import { logTruncationMarker } from './protocol.ts' import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' @@ -33,12 +32,12 @@ export interface PatchableStream { } /** - * Ordered log capture under one shared byte budget, delivered to a sink as - * each entry lands (the real sink streams entries over the port eagerly, so + * Ordered text capture under one shared byte budget, delivered to a sink as + * each item lands (the real sink streams text over the port eagerly, so * captured output survives a mid-run termination). Once the budget is - * exhausted it emits exactly one in-band marker entry (on the `stderr` - * diagnostics channel) and silently drops everything after — the cap is a - * blast-radius bound, so "how much was lost" intentionally stays unmeasured. + * exhausted it emits exactly one in-band marker and silently drops everything + * after. The cap is a blast-radius bound, so "how much was lost" intentionally + * stays unmeasured. */ export class LogBuffer { private remaining: number @@ -47,28 +46,28 @@ export class LogBuffer { // under Node's native strip-only mode, which rejects non-erasable syntax — // and parameter properties are non-erasable. private readonly maxBytes: number - private readonly sink: (entry: CodeLogEntry) => void + private readonly sink: (text: string) => void - constructor(maxBytes: number, sink: (entry: CodeLogEntry) => void) { + constructor(maxBytes: number, sink: (text: string) => void) { this.maxBytes = maxBytes this.sink = sink this.remaining = maxBytes } /** - * Emit one entry to the sink, charging its text against the budget (drops + marks once exhausted). - * @param entry - the log entry to deliver. + * Emit text to the sink, charging it against the budget (drops + marks once exhausted). + * @param text - the captured text to deliver. */ - push(entry: CodeLogEntry): void { + push(text: string): void { if (this.truncated) return - const cost = Buffer.byteLength(entry.text, 'utf8') + const cost = Buffer.byteLength(text, 'utf8') if (cost > this.remaining) { this.truncated = true - this.sink({ source: 'stderr', text: logTruncationMarker(this.maxBytes) }) + this.sink(logTruncationMarker(this.maxBytes)) return } this.remaining -= cost - this.sink(entry) + this.sink(text) } } @@ -89,7 +88,7 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS) args.map(arg => typeof arg === 'string' ? arg : inspect(arg, INSPECT_OPTIONS)).join(' ') const shim = Object.create(null) as Record<(typeof CONSOLE_LEVELS)[number], (...args: unknown[]) => void> for (const level of CONSOLE_LEVELS) { - shim[level] = (...args: unknown[]) => { logs.push({ source: 'console', level, text: render(args) }) } + shim[level] = (...args: unknown[]) => { logs.push(render(args)) } } return shim } @@ -104,17 +103,16 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS) * even for writes the exhausted budget drops. * @param logs - the buffer captured writes are pushed into. * @param stream - the stream whose `write` slot is patched. - * @param source - the log source the captured writes are attributed to. * @returns the restore function (the in-process tests un-patch; the real * worker never needs to). */ -export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream, source: 'stdout' | 'stderr'): () => void { +export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream): () => void { // The slot's VALUE is stored for restore and reassigned — never invoked // detached, so the unbound-method concern does not apply. // eslint-disable-next-line @typescript-eslint/unbound-method const original = stream.write stream.write = (chunk: unknown, ...rest: unknown[]): boolean => { - logs.push({ source, text: typeof chunk === 'string' ? chunk : String(chunk) }) + logs.push(typeof chunk === 'string' ? chunk : String(chunk)) // Node's optional-encoding shape: the callback is whichever of the next // two positions holds a function (a non-function there is the encoding). const callback = [rest[0], rest[1]].find( @@ -273,9 +271,9 @@ export async function runWorkerMain( data: WorkerBootData, streams: { stdout: PatchableStream; stderr: PatchableStream }, ): Promise { - const logs = new LogBuffer(data.maxLogBytes, (entry) => { port.postMessage({ type: 'log', entry }) }) - captureStreamWrites(logs, streams.stdout, 'stdout') - captureStreamWrites(logs, streams.stderr, 'stderr') + const logs = new LogBuffer(data.maxLogBytes, (text) => { port.postMessage({ type: 'log', text }) }) + captureStreamWrites(logs, streams.stdout) + captureStreamWrites(logs, streams.stderr) const pending = new Map() wireReplies(port, pending) diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index 7a8024cb3d..1f659ad769 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url' import { Context } from 'cordis' import z from 'schemastery' import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' -import type { CodeBindingFunction, CodeLogEntry, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' +import type { CodeBindingFunction, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' import { prepareValue, truncateUtf8Bytes } from './bootstrap.ts' import { logTruncationMarker } from './protocol.ts' import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' @@ -118,10 +118,6 @@ function messageOf(error: unknown): string { return error instanceof Error ? error.message : String(error) } -/** The log sources / console levels the seam vocabulary admits, as runtime sets for inbound-message validation. */ -const LOG_SOURCES = new Set(['console', 'stdout', 'stderr']) -const LOG_LEVELS = new Set(['log', 'info', 'warn', 'error', 'debug']) - /** * Runtime shape gate for inbound port traffic. The peer runs MODEL CODE and * can post anything — `null`, primitives, objects with poisoned fields — so @@ -140,20 +136,8 @@ function parseWorkerMessage(raw: unknown): WorkerToHost | undefined { return { type: 'call', id: m.id, global: m.global, name: m.name, args: m.args } } case 'log': { - const entry = m.entry - if (typeof entry !== 'object' || entry === null) return undefined - const e = entry as Record - if (typeof e.text !== 'string') return undefined - if (typeof e.source !== 'string' || !LOG_SOURCES.has(e.source)) return undefined - if (e.level !== undefined && (typeof e.level !== 'string' || !LOG_LEVELS.has(e.level))) return undefined - return { - type: 'log', - entry: { - source: e.source as CodeLogEntry['source'], - ...e.level !== undefined ? { level: e.level as Exclude } : {}, - text: e.text, - }, - } + if (typeof m.text !== 'string') return undefined + return { type: 'log', text: m.text } } case 'done': { if (m.error === undefined) return { type: 'done', ...m.value !== undefined ? { value: m.value } : {} } @@ -299,8 +283,8 @@ export class WorkerCodeRuntime extends CodeRuntime { return new Promise((resolve) => { let settled = false const answered = new Set() - const logs: CodeLogEntry[] = [] - const strayLogs: CodeLogEntry[] = [] + const logs: string[] = [] + const strayLogs: string[] = [] // ONE host-side ledger for everything that lands in `logs`/`strayLogs`, // whatever the path: honest port entries, FORGED port entries (model @@ -310,26 +294,26 @@ export class WorkerCodeRuntime extends CodeRuntime { // so the documented cap is one shared `maxLogBytes` however it is hit. let logBudget = this.config.maxLogBytes let logsTruncated = false - const admit = (entry: CodeLogEntry, sink: CodeLogEntry[]): void => { + const admit = (text: string, sink: string[]): void => { if (logsTruncated) return - const cost = Buffer.byteLength(entry.text, 'utf8') + const cost = Buffer.byteLength(text, 'utf8') if (cost > logBudget) { logsTruncated = true - sink.push({ source: 'stderr', text: logTruncationMarker(this.config.maxLogBytes) }) + sink.push(logTruncationMarker(this.config.maxLogBytes)) return } logBudget -= cost - sink.push(entry) + sink.push(text) } // No settled guard: `finish` snapshots the arrays when it resolves, so // a chunk flushing after settlement mutates only the discarded buffers, // and the ledger bounds that growth until the pipes close. - const captureStray = (source: 'stdout' | 'stderr') => (chunk: Buffer) => { - admit({ source, text: chunk.toString('utf8') }, strayLogs) + const captureStray = (chunk: Buffer): void => { + admit(chunk.toString('utf8'), strayLogs) } - worker.stdout.on('data', captureStray('stdout')) - worker.stderr.on('data', captureStray('stderr')) + worker.stdout.on('data', captureStray) + worker.stderr.on('data', captureStray) // Settlement: exactly one outcome wins; every path funnels through // here, cleans up the timers/listeners, terminates the worker, and @@ -404,7 +388,7 @@ export class WorkerCodeRuntime extends CodeRuntime { // this listener would crash the host process. Junk drops silently. const message = parseWorkerMessage(raw) if (!message) return - if (message.type === 'log' && !settled) admit(message.entry, logs) + if (message.type === 'log' && !settled) admit(message.text, logs) onCall(message) onDone(message) }) diff --git a/packages/code-runtime/code-runtime-worker/src/protocol.ts b/packages/code-runtime/code-runtime-worker/src/protocol.ts index b8ea122c5b..65e6a0d60e 100644 --- a/packages/code-runtime/code-runtime-worker/src/protocol.ts +++ b/packages/code-runtime/code-runtime-worker/src/protocol.ts @@ -9,8 +9,6 @@ * @module @deepseek-ai/dsh-code-runtime-worker/src/protocol */ -import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime' - /** What the host hands the worker at spawn, via `workerData`. */ export interface WorkerBootData { /** The type-stripped (plain JS) program body. */ @@ -36,10 +34,10 @@ export interface CallMessage { args: unknown } -/** Worker → host: one captured log entry, streamed eagerly so output survives a mid-run termination (timeout, abort, OOM). */ -export interface LogMessage { +/** Worker → host: captured text, streamed eagerly so output survives a mid-run termination (timeout, abort, OOM). */ +interface LogMessage { type: 'log' - entry: CodeLogEntry + text: string } /** diff --git a/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts b/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts index e41f4455bb..111aa4f15f 100644 --- a/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from 'vitest' import { EventEmitter } from 'node:events' -import { LogBuffer, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareValue, runWorkerMain, truncateUtf8Bytes, wireReplies } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts' -import type { BootstrapPort, PatchableStream, PendingCall } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts' -import type { ReplyMessage, WorkerToHost } from '@deepseek-ai/dsh-code-runtime-worker/src/protocol.ts' -import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime' +import { LogBuffer, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareValue, runWorkerMain, truncateUtf8Bytes, wireReplies } from '../src/bootstrap.ts' +import type { BootstrapPort, PatchableStream, PendingCall } from '../src/bootstrap.ts' +import type { ReplyMessage, WorkerToHost } from '../src/protocol.ts' /** * An in-process stand-in for the worker's parentPort: the test plays the @@ -31,8 +30,8 @@ class FakePort implements BootstrapPort { this.emitter.emit('message', message) } - logs(): CodeLogEntry[] { - return this.sent.filter(message => message.type === 'log').map(message => message.entry) + logs(): string[] { + return this.sent.filter(message => message.type === 'log').map(message => message.text) } done(): WorkerToHost | undefined { @@ -48,12 +47,12 @@ const BOOT = { maxLogBytes: 65_536, maxValueBytes: 32_768 } describe('LogBuffer', () => { it('streams entries to the sink until the byte budget, then emits one marker and drops the rest', () => { - const seen: CodeLogEntry[] = [] - const buffer = new LogBuffer(10, entry => seen.push(entry)) - buffer.push({ source: 'console', level: 'log', text: '12345' }) - buffer.push({ source: 'console', level: 'log', text: '123456' }) - buffer.push({ source: 'console', level: 'log', text: 'dropped' }) - expect(seen.map(entry => entry.text)).toEqual([ + const seen: string[] = [] + const buffer = new LogBuffer(10, text => seen.push(text)) + buffer.push('12345') + buffer.push('123456') + buffer.push('dropped') + expect(seen).toEqual([ '12345', '[dsh-code-runtime-worker] log capture truncated at 10 bytes', ]) @@ -61,40 +60,37 @@ describe('LogBuffer', () => { }) describe('makeConsoleShim', () => { - it('captures the five levels and renders non-strings inspect-style', () => { - const seen: CodeLogEntry[] = [] - const shim = makeConsoleShim(new LogBuffer(1_000, entry => seen.push(entry))) + it('captures the five methods and renders non-strings inspect-style', () => { + const seen: string[] = [] + const shim = makeConsoleShim(new LogBuffer(1_000, text => seen.push(text))) shim.log('plain', { a: 1 }) shim.info('i') shim.warn('w') shim.error('e') shim.debug('d') - expect(seen.map(entry => entry.level)).toEqual(['log', 'info', 'warn', 'error', 'debug']) - expect(seen[0]?.text).toBe('plain { a: 1 }') - expect(seen.every(entry => entry.source === 'console')).toBe(true) + expect(seen).toEqual(['plain { a: 1 }', 'i', 'w', 'e', 'd']) }) }) describe('captureStreamWrites', () => { it('redirects writes into the buffer and restores on request', () => { - const seen: CodeLogEntry[] = [] - const buffer = new LogBuffer(1_000, entry => seen.push(entry)) + const seen: string[] = [] + const buffer = new LogBuffer(1_000, text => seen.push(text)) let underlying = '' const stream: PatchableStream = { write: (chunk: unknown) => { underlying += String(chunk); return true } } - const restore = captureStreamWrites(buffer, stream, 'stdout') + const restore = captureStreamWrites(buffer, stream) stream.write('captured', 'utf8') stream.write(Buffer.from('bytes')) restore() stream.write('after') - expect(seen.map(entry => entry.text)).toEqual(['captured', 'bytes']) - expect(seen[0]).toMatchObject({ source: 'stdout' }) + expect(seen).toEqual(['captured', 'bytes']) expect(underlying).toBe('after') }) it('invokes the write callback asynchronously, in both optional-encoding shapes', async () => { const buffer = new LogBuffer(1_000, () => {}) const stream: PatchableStream = { write: () => true } - captureStreamWrites(buffer, stream, 'stdout') + captureStreamWrites(buffer, stream) const calls: (Error | null | undefined)[] = [] stream.write('two-arg', (error?: Error | null) => calls.push(error)) stream.write('three-arg', 'utf8', (error?: Error | null) => calls.push(error)) @@ -107,7 +103,7 @@ describe('captureStreamWrites', () => { it('still fires the callback for a write the exhausted budget drops', async () => { const buffer = new LogBuffer(4, () => {}) const stream: PatchableStream = { write: () => true } - captureStreamWrites(buffer, stream, 'stdout') + captureStreamWrites(buffer, stream) stream.write('this write overflows the budget and is dropped') await new Promise(resolve => stream.write('also dropped', resolve)) }) @@ -210,7 +206,7 @@ describe('runWorkerMain', () => { code: 'const doubled = await tools.double({ n: 21 }); console.log("got", doubled); return { doubled };', namespaces: [{ global: 'tools', names: ['double'] }], }, fakeStreams()) - expect(port.logs()).toEqual([{ source: 'console', level: 'log', text: 'got 42' }]) + expect(port.logs()).toEqual(['got 42']) expect(port.done()).toEqual({ type: 'done', value: { doubled: 42 } }) }) @@ -268,6 +264,6 @@ describe('runWorkerMain', () => { // The patch stays installed for the worker's lifetime; writes during the // program landed in order. Here the program wrote nothing via streams, so // only the post-run write above went through the patched slot. - expect(port.logs().at(-1)).toMatchObject({ source: 'stdout' }) + expect(port.logs().at(-1)).toBe('never seen — already restored? no: patch persists in worker') }) }) diff --git a/packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts b/packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts index aac0ece8a1..2870bf8d4a 100644 --- a/packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts +++ b/packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts @@ -47,9 +47,9 @@ describe.skipIf(!built)('built lib real load path (plain node)', () => { expect(exitCode, `stderr:\n${stderr}`).toBe(0) const lastLine = stdout.trim().split('\n').at(-1) ?? '' - const result = JSON.parse(lastLine) as { value?: unknown; logs: { source: string; level?: string; text: string }[]; error?: unknown } + const result = JSON.parse(lastLine) as { value?: unknown; logs: string[]; error?: unknown } expect(result.error).toBeUndefined() expect(result.value).toBe(42) - expect(result.logs).toContainEqual({ source: 'console', level: 'log', text: 'halfway 42' }) + expect(result.logs).toContain('halfway 42') }) }) diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index edc2bd1271..9754564702 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -28,7 +28,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => { expect(runtime.isolation).toBe('worker-thread') }) - it('runs TypeScript (erasable syntax), captures console/stdout in order, returns the value', async () => { + it('runs TypeScript (erasable syntax), captures output in order, returns the value', async () => { const { runtime } = await setup() const result = await runtime.run({ program: ` @@ -43,12 +43,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => { }) expect(result.error).toBeUndefined() expect(result.value).toBe(3) - expect(result.logs.map(entry => [entry.source, entry.level ?? null])).toEqual([ - ['console', 'log'], - ['stdout', null], - ['console', 'warn'], - ]) - expect(result.logs[0]?.text).toBe('point { x: 1, y: 2 }') + expect(result.logs).toEqual(['point { x: 1, y: 2 }', 'raw-out\n', 'careful']) }) it('bridges binding calls both ways and rejects the program-side call on a host rejection', async () => { @@ -115,7 +110,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => { bindings: [], }) expect(result.error?.kind).toBe('exception') - expect(result.logs.map(entry => entry.text)).toContain('before') + expect(result.logs).toContain('before') }) }) @@ -210,8 +205,8 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => { program: 'for (let i = 0; i < 1000; i++) console.log("spam line", i); return 1', bindings: [], }) - expect(result.logs.at(-1)?.text).toContain('truncated at 300 bytes') - const total = result.logs.reduce((sum, entry) => sum + Buffer.byteLength(entry.text, 'utf8'), 0) + expect(result.logs.at(-1)).toContain('truncated at 300 bytes') + const total = result.logs.reduce((sum, text) => sum + Buffer.byteLength(text, 'utf8'), 0) expect(total).toBeLessThan(1_000) }) @@ -241,7 +236,7 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => { }) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toContainEqual({ source: 'stdout', text: 'flushed' }) + expect(result.logs).toContain('flushed') }) it('caps a huge container whose bounded rendering is small (wire size, not rendering, is what counts)', async () => { @@ -270,8 +265,8 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => { bindings: [], }) expect(result.error).toBeUndefined() - expect(result.logs).toContainEqual({ source: 'stdout', text: 'abcd' }) - expect(result.logs.map(entry => entry.text)).not.toContain('ef') + expect(result.logs).toContain('abcd') + expect(result.logs).not.toContain('ef') }, 15_000) }) @@ -306,11 +301,9 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => { { type: 'call', id: 1e9, global: 7, name: 'real', args: {} }, { type: 'call', id: 1e9, global: 'tools', name: 7, args: {} }, { type: 'log' }, - { type: 'log', entry: null }, - { type: 'log', entry: { source: 'stdout', text: 7 } }, - { type: 'log', entry: { source: 'nope', text: 'x' } }, - { type: 'log', entry: { source: 'console', level: 'nope', text: 'x' } }, - { type: 'log', entry: { source: 'console', level: 7, text: 'x' } }, + { type: 'log', text: null }, + { type: 'log', text: 7 }, + { type: 'log', text: {} }, { type: 'done', error: 5 }, { type: 'done', error: { message: 5 } }, ]) parentPort.postMessage(junk); @@ -331,7 +324,7 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => { // code and an unbounded result. program: ` const { parentPort } = await import('node:worker_threads'); - for (let i = 0; i < 50; i++) parentPort.postMessage({ type: 'log', entry: { source: 'stdout', text: 'F'.repeat(100), forged: true } }); + for (let i = 0; i < 50; i++) parentPort.postMessage({ type: 'log', text: 'F'.repeat(100), forged: true }); parentPort.postMessage({ type: 'done', value: 'V'.repeat(100000) }); for (;;) {} `, @@ -343,10 +336,9 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => { expect(value.endsWith('… [truncated]')).toBe(true) expect(value.length).toBeLessThan(120) const marker = '[dsh-code-runtime-worker] log capture truncated at 200 bytes' - const total = result.logs.reduce((sum, entry) => sum + Buffer.byteLength(entry.text, 'utf8'), 0) + const total = result.logs.reduce((sum, text) => sum + Buffer.byteLength(text, 'utf8'), 0) expect(total).toBeLessThanOrEqual(200 + Buffer.byteLength(marker, 'utf8')) - expect(result.logs.at(-1)?.text).toBe(marker) - expect(result.logs.every(entry => !('forged' in entry))).toBe(true) + expect(result.logs.at(-1)).toBe(marker) }) it('accepts a forged done carrying both value and error (self-sabotage, contained)', async () => { diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index 20c9274b9c..20a92ed526 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -16,4 +16,4 @@ Semantics every implementation must honor (contract details in the class JSDoc): ## Vocabulary -`CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on — defaulting (time budgets, output caps) is the implementation's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions`), each exposed to the program as one global object of async callables. `CodeRunResult` reports the completion `value?`, the ordered `logs` (`CodeLogEntry`: `console`/`stdout`/`stderr` source, console `level`, capped text), and the `error?` (`CodeRunFailure`: `kind` + model-feedable `message`). See `src/types.ts` for the full contracts. +`CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on — defaulting (time budgets, output caps) is the implementation's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions`), each exposed to the program as one global object of async callables. `CodeRunResult` reports the completion `value?`, ordered capped `logs: string[]`, and the `error?` (`CodeRunFailure`: `kind` + model-feedable `message`). See `src/types.ts` for the full contracts. diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index 5595469afe..8548b4adb2 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -22,7 +22,6 @@ import type { CodeRunRequest, CodeRunResult } from './types.ts' export type { CodeBindingFunction, CodeBindingNamespace, - CodeLogEntry, CodeRunFailure, CodeRunRequest, CodeRunResult, diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts index 8278f33a39..d7669a4785 100644 --- a/packages/code-runtime/code-runtime/src/types.ts +++ b/packages/code-runtime/code-runtime/src/types.ts @@ -54,20 +54,6 @@ export interface CodeRunRequest { signal?: AbortSignal } -/** - * One captured output entry, in emission order. `source` says which channel - * produced it: the program's `console` (shimmed by the runtime), or a stray - * write to the underlying stdout/stderr streams. - */ -export interface CodeLogEntry { - /** Which channel produced the text. */ - source: 'console' | 'stdout' | 'stderr' - /** The console method used; present only when `source` is `'console'`. */ - level?: 'log' | 'info' | 'warn' | 'error' | 'debug' - /** The captured text (possibly truncated by the implementation's caps, marked in-band). */ - text: string -} - /** * Why a run failed. The kinds are orthogonal outcomes reported independently * (per docs/defensive-patterns.md): a budget expiry is not an exception, an @@ -98,8 +84,8 @@ export interface CodeRunResult { * or value-less run leaves this absent. */ value?: unknown - /** Everything the program emitted, in order (capped by the implementation). */ - logs: CodeLogEntry[] + /** Text the program emitted, in order (capped by the implementation). */ + logs: string[] /** Present iff the run failed; see {@link CodeRunFailure} for the taxonomy. */ error?: CodeRunFailure } diff --git a/packages/code-runtime/code-runtime/tests/service.spec.ts b/packages/code-runtime/code-runtime/tests/service.spec.ts index 4ff6d8f313..7811ef0531 100644 --- a/packages/code-runtime/code-runtime/tests/service.spec.ts +++ b/packages/code-runtime/code-runtime/tests/service.spec.ts @@ -55,7 +55,7 @@ describe('CodeRuntime service seam', () => { it('reports a failed run as an error field on a resolved result, never a rejection', async () => { const { runtime } = await setup() runtime.nextResult = { - logs: [{ source: 'console', level: 'error', text: 'boom' }], + logs: ['boom'], error: { kind: 'exception', message: 'boom' }, } const result = await runtime.run({ program: 'throw new Error("boom")', bindings: [] }) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 51a8f52082..2df7290a3f 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -598,10 +598,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'CodeBindingNamespace', declaration: 'export interface CodeBindingNamespace {\n global: string;\n functions: Record;\n}', }, - { - name: 'CodeLogEntry', - declaration: 'export interface CodeLogEntry {\n source: \'console\' | \'stdout\' | \'stderr\';\n level?: \'log\' | \'info\' | \'warn\' | \'error\' | \'debug\';\n text: string;\n}', - }, { name: 'CodeRunFailure', declaration: 'export interface CodeRunFailure {\n kind: \'exception\' | \'timeout\' | \'abort\' | \'worker-exit\';\n message: string;\n}', @@ -612,7 +608,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'CodeRunResult', - declaration: 'export interface CodeRunResult {\n value?: unknown;\n logs: CodeLogEntry[];\n error?: CodeRunFailure;\n}', + declaration: 'export interface CodeRunResult {\n value?: unknown;\n logs: string[];\n error?: CodeRunFailure;\n}', }, { name: 'CollectedOutput', diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 323fbeed2b..8ed73383c1 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -126,14 +126,13 @@ function renderValue(value: unknown): string { /** The run_code result's `meta` payload (JSON-serializable; `presentResult` narrows it back). */ interface RunCodeMeta { logs: CodeRunResult['logs'] - dispatches: number } /** Soft-narrow a result `meta` back to {@link RunCodeMeta} (replay may carry older shapes; presentation must not throw). */ function asRunCodeMeta(meta: unknown): RunCodeMeta | undefined { if (typeof meta !== 'object' || meta === null) return undefined const m = meta as Record - if (!Array.isArray(m.logs) || typeof m.dispatches !== 'number') return undefined + if (!Array.isArray(m.logs) || !m.logs.every(log => typeof log === 'string')) return undefined return m as unknown as RunCodeMeta } @@ -283,12 +282,12 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => } if (result.error) { - const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.map(entry => entry.text).join('\n')}` : '' + const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : '' throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`) } const rendered = renderValue(result.value) - const parts = [result.logs.map(entry => entry.text).join('\n'), rendered].filter(part => part.length > 0) - const meta: RunCodeMeta = { logs: result.logs, dispatches } + const parts = [result.logs.join('\n'), rendered].filter(part => part.length > 0) + const meta: RunCodeMeta = { logs: result.logs } return { content: [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }], meta, @@ -316,7 +315,7 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => presentResult: (_args, result) => { const meta = asRunCodeMeta(result.meta) if (!meta) return undefined - const output = meta.logs.map(entry => entry.text).join('\n') + const output = meta.logs.join('\n') return { card: 'generic', ...output.length > 0 ? { content: [{ type: 'text' as const, text: output }] } : {}, diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index f9d84aadbf..1ae7aae4ec 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -328,7 +328,7 @@ describe('the run_code dispatch bridge', () => { const tools = request.bindings[0]!.functions const first = await tools.echo!({ value: 'one' }) const second = await tools.echo!({ value: 'two' }) - return { logs: [{ source: 'console', level: 'log', text: `saw ${String(first)}` }], value: second } + return { logs: [`saw ${String(first)}`], value: second } } const result = await runCode(ctx, 'const …: string = …', { agent }) expect(result.isError).toBe(false) @@ -339,7 +339,7 @@ describe('the run_code dispatch bridge', () => { { parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo', arguments: { value: 'one' }, isError: false, resultSummary: 'echo:one' }, { parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo', arguments: { value: 'two' }, isError: false, resultSummary: 'echo:two' }, ]) - expect(result.meta).toEqual({ logs: [{ source: 'console', level: 'log', text: 'saw echo:one' }], dispatches: 2 }) + expect(result.meta).toEqual({ logs: ['saw echo:one'] }) }) it('exposes only an opaque parent token to nested result observers', async () => { @@ -505,7 +505,7 @@ describe('the run_code dispatch bridge', () => { it('converts a failed run into a structured isError result carrying kind, message, and captured logs', async () => { const { ctx, runtime } = await setup({ mode: 'code' }) runtime.behavior = () => Promise.resolve({ - logs: [{ source: 'console', level: 'log', text: 'got this far' }], + logs: ['got this far'], error: { kind: 'timeout', message: 'compute budget exhausted (300ms busy)' }, }) const result = await runCode(ctx, 'program') @@ -632,7 +632,7 @@ describe('the run_code dispatch bridge', () => { const view = tool.presentResult?.({ code: 'return 1' }, { content: [{ type: 'text', text: 'model-facing' }], isError: false, - meta: { logs: [{ source: 'console', level: 'log', text: 'printed' }], dispatches: 1 }, + meta: { logs: ['printed'] }, }) // The result omits the title — an update replaces only provided fields, // so the pending card's program title persists through completion. @@ -641,9 +641,10 @@ describe('the run_code dispatch bridge', () => { content: [{ type: 'text', text: 'printed' }], }) // No captured output → no content either; everything pending persists. - expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [], dispatches: 2 } })) + expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [] } })) .toEqual({ card: 'generic' }) // Replay with an unrecognizable meta falls back to the generic rendering. + expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [{ text: 'legacy' }], dispatches: 1 } })).toBeUndefined() expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { other: true } })).toBeUndefined() expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false })).toBeUndefined() }) diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index a219ea1b1d..d3572387aa 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -92,7 +92,6 @@ { "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeRunResult", "source": "packages/code-runtime/code-runtime/src/types.ts" }, { "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeBindingNamespace", "source": "packages/code-runtime/code-runtime/src/types.ts" }, { "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeBindingFunction", "source": "packages/code-runtime/code-runtime/src/types.ts" }, - { "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeLogEntry", "source": "packages/code-runtime/code-runtime/src/types.ts" }, { "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeRunFailure", "source": "packages/code-runtime/code-runtime/src/types.ts" }, { "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsTarget", "source": "packages/fs/fs/src/types.ts" }, From a11396e43cb57fd1523784dcfbe621f68f908967 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:09:53 +0800 Subject: [PATCH 2/5] test: refresh code runtime snapshots --- .../acp-agent/tests/snapshots/advanced-toolchain/session.jsonl | 2 +- examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl | 2 +- examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl index bb74382b22..ac833c32d8 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -21,7 +21,7 @@ {"type":"assistant/message","seq":19,"time":1783957884490,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}],"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} {"type":"tool/call","seq":20,"time":1783957884490,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"return await tools.cordis_inspect({ what: 'dynamic' })\"}"}} {"type":"tool/code-dispatch","seq":21,"time":1783957884560,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_inspect","arguments":{"what":"dynamic"},"isError":false,"resultSummary":"## dynamic\n- dyn-1: snapshot-marker [active]"}} -{"type":"tool/result","seq":22,"time":1783957884561,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"## dynamic\n- dyn-1: snapshot-marker [active]"}],"isError":false,"meta":{"logs":[],"dispatches":1}},"sourceEventSeqs":[20],"surfaceOp":"append"} +{"type":"tool/result","seq":22,"time":1783957884561,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"## dynamic\n- dyn-1: snapshot-marker [active]"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[20],"surfaceOp":"append"} {"type":"step/end","seq":23,"time":1783957884561,"data":{"turn":1,"step":2}} {"type":"step/start","seq":24,"time":1783957884562,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":25,"time":1783950000026,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl index b49097188e..926b1e3389 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl @@ -87,7 +87,7 @@ {"type":"assistant/message","seq":85,"time":1783611775503,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the `run_code` tool to execute a program that calls `tools.bash` with the command `echo BOTH_OK` and returns its output."},{"type":"tool-call","id":"call_00_AZFzvUwuC4vAUoICrfke5147","name":"run_code","arguments":"{\"code\": \"const result = await tools.bash({ command: \\\"echo BOTH_OK\\\", description: \\\"Echo BOTH_OK\\\" });\\nreturn result;\"}"}],"usage":{"inputTokens":5530,"outputTokens":108,"cacheReadTokens":0,"reasoningTokens":37}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84],"surfaceOp":"append"} {"type":"tool/call","seq":86,"time":1783611775504,"data":{"turn":1,"step":1,"callId":"call_00_AZFzvUwuC4vAUoICrfke5147","name":"run_code","arguments":"{\"code\": \"const result = await tools.bash({ command: \\\"echo BOTH_OK\\\", description: \\\"Echo BOTH_OK\\\" });\\nreturn result;\"}"}} {"type":"tool/code-dispatch","seq":87,"time":1783611775590,"data":{"parentCallId":"call_00_AZFzvUwuC4vAUoICrfke5147","subCallId":"call_00_AZFzvUwuC4vAUoICrfke5147:code:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Echo BOTH_OK"},"isError":false,"resultSummary":"BOTH_OK\n"}} -{"type":"tool/result","seq":88,"time":1783611775592,"data":{"turn":1,"step":1,"callId":"call_00_AZFzvUwuC4vAUoICrfke5147","content":[{"type":"text","text":"BOTH_OK\n"}],"isError":false,"meta":{"logs":[],"dispatches":1}},"sourceEventSeqs":[86],"surfaceOp":"append"} +{"type":"tool/result","seq":88,"time":1783611775592,"data":{"turn":1,"step":1,"callId":"call_00_AZFzvUwuC4vAUoICrfke5147","content":[{"type":"text","text":"BOTH_OK\n"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[86],"surfaceOp":"append"} {"type":"step/end","seq":89,"time":1783611775592,"data":{"turn":1,"step":1}} {"type":"step/start","seq":90,"time":1783611775592,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":91,"time":1783611776183,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl index 9367e2deb0..7b79bd07cb 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl @@ -112,7 +112,7 @@ {"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.trim() + \\\"+\\\" + out2.trim();\"}"}} {"type":"tool/code-dispatch","seq":111,"time":1783611772933,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"First echo"},"isError":false,"resultSummary":"CODE_ONE\n"}} {"type":"tool/code-dispatch","seq":112,"time":1783611772936,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Second echo"},"isError":false,"resultSummary":"CODE_TWO\n"}} -{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"CODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":[],"dispatches":2}},"sourceEventSeqs":[110],"surfaceOp":"append"} +{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"CODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[110],"surfaceOp":"append"} {"type":"step/end","seq":114,"time":1783611772938,"data":{"turn":1,"step":1}} {"type":"step/start","seq":115,"time":1783611772938,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":116,"time":1783611773376,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} From 4f40197e169c6f8dadf756ca514b824a9050f58d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:18:16 +0800 Subject: [PATCH 3/5] fix: complete code runtime surface pruning --- packages/code-runtime/code-runtime-worker/src/index.ts | 3 --- .../snapshots/python-sdk-single-exe/advanced/result.json | 6 ++---- .../snapshots/python-sdk-single-exe/advanced/session.jsonl | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index 1f659ad769..96477cf793 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -23,9 +23,6 @@ import { prepareValue, truncateUtf8Bytes } from './bootstrap.ts' import { logTruncationMarker } from './protocol.ts' import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' -export type { BootstrapPort, PatchableStream } from './bootstrap.ts' -export type { CallMessage, DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' - /** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */ export interface Config { /** diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 2ecfc76a2e..ca89b2d9a9 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -408,8 +408,7 @@ ], "isError": false, "meta": { - "logs": [], - "dispatches": 1 + "logs": [] } }, "sourceEventSeqs": [ @@ -1606,8 +1605,7 @@ ], "isError": false, "meta": { - "logs": [], - "dispatches": 1 + "logs": [] } }, "sourceEventSeqs": [ diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl index bb0ee1d4d0..82fb190727 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -22,7 +22,7 @@ {"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\"}"}],"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"} {"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\"}"}} {"type":"tool/code-dispatch","seq":22,"time":0,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"resultSummary":"42"}} -{"type":"tool/result","seq":23,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false,"meta":{"logs":[],"dispatches":1}},"sourceEventSeqs":[21],"surfaceOp":"append"} +{"type":"tool/result","seq":23,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","seq":24,"time":0,"data":{"turn":1,"step":2}} {"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} From 5e9131e824a86dc2fcdaf910569b1fbf7d4cc72c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:19:33 +0800 Subject: [PATCH 4/5] chore: sync code runtime surface catalog --- docs/config-catalog.md | 2 +- packages/code-runtime/code-runtime-worker/src/protocol.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f785fdf337..d117c13498 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -212,7 +212,7 @@ export interface Config { } ``` -Source: [`packages/code-runtime/code-runtime-worker/src/index.ts:30`](../packages/code-runtime/code-runtime-worker/src/index.ts) +Source: [`packages/code-runtime/code-runtime-worker/src/index.ts:27`](../packages/code-runtime/code-runtime-worker/src/index.ts) ## `@deepseek-ai/dsh-compact-basic` diff --git a/packages/code-runtime/code-runtime-worker/src/protocol.ts b/packages/code-runtime/code-runtime-worker/src/protocol.ts index 65e6a0d60e..663e407400 100644 --- a/packages/code-runtime/code-runtime-worker/src/protocol.ts +++ b/packages/code-runtime/code-runtime-worker/src/protocol.ts @@ -22,7 +22,7 @@ export interface WorkerBootData { } /** Worker → host: one bridged binding call. */ -export interface CallMessage { +interface CallMessage { type: 'call' /** Worker-issued correlation id; the host answers each id at most once and ignores duplicates. */ id: number From 26e2fe9356bf1c65a8022dac8492be257ebb722a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:41:23 +0800 Subject: [PATCH 5/5] refactor: drop assembled section order echo --- docs/config-catalog.md | 2 +- docs/cordis-catalog/services.md | 2 +- packages/bash/tool-bash/tests/tools.spec.ts | 10 ++++++- .../cordis/tool-cordis/src/api-catalog.ts | 2 +- packages/core/system-prompt/src/index.ts | 9 ++----- .../core/system-prompt/tests/scoped.spec.ts | 2 +- .../system-prompt/tests/system-prompt.spec.ts | 26 +++++++++---------- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f785fdf337..674970f703 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -813,7 +813,7 @@ export interface Config { } ``` -Source: [`packages/core/system-prompt/src/index.ts:227`](../packages/core/system-prompt/src/index.ts) +Source: [`packages/core/system-prompt/src/index.ts:223`](../packages/core/system-prompt/src/index.ts) ## `@deepseek-ai/dsh-tool-cordis` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 28d1904ecf..c236baf23e 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -257,7 +257,7 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine async assemble(context: AssembleContext = {}): Promise ``` -Source: [`packages/core/system-prompt/src/index.ts:342`](../../packages/core/system-prompt/src/index.ts) +Source: [`packages/core/system-prompt/src/index.ts:338`](../../packages/core/system-prompt/src/index.ts) ## `ctx.tools` — `ToolRegistry` diff --git a/packages/bash/tool-bash/tests/tools.spec.ts b/packages/bash/tool-bash/tests/tools.spec.ts index 12b4a53958..6041033314 100644 --- a/packages/bash/tool-bash/tests/tools.spec.ts +++ b/packages/bash/tool-bash/tests/tools.spec.ts @@ -293,9 +293,17 @@ describe('bash tool', () => { it('contributes the exit-code habit as its prompt section (guidance the descriptions cannot carry)', async () => { const ctx = await setup() + ctx.systemPrompt.section({ name: 'test:before-bash', order: 104, text: 'before' }) + ctx.systemPrompt.section({ name: 'test:after-bash', order: 106, text: 'after' }) const assembly = await ctx.systemPrompt.assemble() const section = assembly.sections.find(s => s.name === 'tool:bash') - expect(section?.order).toBe(105) + expect(assembly.sections.map(s => s.name)).toEqual([ + 'harness:identity', + 'deployment:persona', + 'test:before-bash', + 'tool:bash', + 'test:after-bash', + ]) expect(section?.text).toContain('[exit code: N]') }) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 51a8f52082..9b8f016ca5 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -544,7 +544,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'AssembledSection', - declaration: 'export interface AssembledSection {\n name: string;\n order: number;\n text: string;\n}', + declaration: 'export interface AssembledSection {\n name: string;\n text: string;\n}', }, { name: 'BashExecRequest', diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index b886b4c774..ad1fba9f1e 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -103,10 +103,6 @@ export interface PromptSection { export interface AssembledSection { /** The contributing section's unique name. */ name: string - // TODO(assembled-section-order): drop this output field; registry order has - // already sorted the array, and no production renderer/listener reads it. - /** The contributing section's order (sections arrive sorted ascending). */ - order: number /** The resolved (but not yet interpolated) section text. */ text: string } @@ -619,12 +615,11 @@ export class SystemPrompt extends Service { } const assembly: PromptAssembly = { sections: [...sectionByName.values()] + .sort((a, b) => a.order - b.order) .map(section => ({ name: section.name, - order: section.order, text: typeof section.text === 'function' ? section.text(context) : section.text, - })) - .sort((a, b) => a.order - b.order), + })), tools: orderTools(collected, this.toolOrder, knownNames), variables, } diff --git a/packages/core/system-prompt/tests/scoped.spec.ts b/packages/core/system-prompt/tests/scoped.spec.ts index a452bb6797..aac3f79e88 100644 --- a/packages/core/system-prompt/tests/scoped.spec.ts +++ b/packages/core/system-prompt/tests/scoped.spec.ts @@ -139,7 +139,7 @@ describe('scoped assemble dispatch', () => { scope.ctx.on('system-prompt/assemble', async (_assembly: PromptAssembly, context, next: () => Promise) => { shaped.push(context.scope) const result = await next() - result.sections.push({ name: 'listener:extra', order: 999, text: 'listener text' }) + result.sections.push({ name: 'listener:extra', text: 'listener text' }) return result }) diff --git a/packages/core/system-prompt/tests/system-prompt.spec.ts b/packages/core/system-prompt/tests/system-prompt.spec.ts index c7436711fa..b084104d22 100644 --- a/packages/core/system-prompt/tests/system-prompt.spec.ts +++ b/packages/core/system-prompt/tests/system-prompt.spec.ts @@ -21,9 +21,9 @@ describe('SystemPrompt', () => { await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness SDK.' }) const assembly = await ctx.systemPrompt.assemble() - expect(assembly.sections.map(s => [s.name, s.order])).toEqual([ - ['harness:identity', -100], - ['deployment:persona', 0], + expect(assembly.sections.map(s => s.name)).toEqual([ + 'harness:identity', + 'deployment:persona', ]) expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness SDK.`) // The names are reserved by the plugin — one owner per section. @@ -183,7 +183,7 @@ describe('SystemPrompt', () => { const contexts: AssembleContext[] = [] ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, context, next) => { contexts.push(context) - assembly.sections.push({ name: 'from-a', order: 100, text: 'a' }) + assembly.sections.push({ name: 'from-a', text: 'a' }) return next() }) // Listener B (registered later, runs after A) sees A's contribution. @@ -235,8 +235,8 @@ describe('SystemPrompt', () => { it('filters out empty section text from renderPrompt', () => { const result = renderPrompt({ sections: [ - { name: 'empty', order: 0, text: '' }, - { name: 'real', order: 1, text: 'content' }, + { name: 'empty', text: '' }, + { name: 'real', text: 'content' }, ], tools: [], variables: {}, @@ -356,13 +356,13 @@ describe('SystemPrompt', () => { }) it('names "(none)" when no variables are registered at all', () => { - expect(() => renderPrompt({ sections: [{ name: 's', order: 0, text: '{{x}}' }], tools: [], variables: {} })) + expect(() => renderPrompt({ sections: [{ name: 's', text: '{{x}}' }], tools: [], variables: {} })) .toThrow('unknown prompt variable "{{x}}" in section "s"; registered variables: (none)') }) it('throws when a referenced variable has no value for this assembly', () => { expect(() => renderPrompt({ - sections: [{ name: 'persona', order: 0, text: 'in {{cwd}}' }], + sections: [{ name: 'persona', text: 'in {{cwd}}' }], tools: [], variables: { cwd: undefined }, })).toThrow('prompt variable "{{cwd}}" has no value for this assembly (section "persona")') @@ -370,7 +370,7 @@ describe('SystemPrompt', () => { it('throws on a malformed complete reference, e.g. inner spaces', () => { expect(() => renderPrompt({ - sections: [{ name: 's', order: 0, text: 'on {{ model }}' }], + sections: [{ name: 's', text: 'on {{ model }}' }], tools: [], variables: { model: 'm' }, })).toThrow('malformed prompt variable reference "{{ model }}" in section "s"') @@ -378,7 +378,7 @@ describe('SystemPrompt', () => { it('leaves a lone {{ verbatim only when NO }} follows anywhere after it', () => { const text = renderPrompt({ - sections: [{ name: 's', order: 0, text: 'shell ${X:-{{fallback} stays' }], + sections: [{ name: 's', text: 'shell ${X:-{{fallback} stays' }], tools: [], variables: {}, }) @@ -390,7 +390,7 @@ describe('SystemPrompt', () => { { text: 'x {{a{b}} y {{model}}', label: 'nested brace inside a would-be group' }, ])('throws on a mangled reference with a }} still following ($label)', ({ text }) => { expect(() => renderPrompt({ - sections: [{ name: 's', order: 0, text }], + sections: [{ name: 's', text }], tools: [], variables: { model: 'm' }, })).toThrow('malformed prompt variable reference at') @@ -400,7 +400,7 @@ describe('SystemPrompt', () => { // `in` would find Object.prototype.constructor and splice function // source into the prompt; Object.hasOwn must reject it instead. expect(() => renderPrompt({ - sections: [{ name: 's', order: 0, text: 'on {{constructor}}' }], + sections: [{ name: 's', text: 'on {{constructor}}' }], tools: [], variables: { model: 'm' }, })).toThrow('unknown prompt variable "{{constructor}}"') @@ -416,7 +416,7 @@ describe('SystemPrompt', () => { it('never re-scans substituted values (a value containing {{sneaky}} stays literal)', () => { const text = renderPrompt({ - sections: [{ name: 's', order: 0, text: 'v = {{model}}!' }], + sections: [{ name: 's', text: 'v = {{model}}!' }], tools: [], variables: { model: 'literal {{sneaky}} inside' }, })