diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 09da36a2bd..f45595fa70 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -203,7 +203,7 @@ export interface Config { } ``` -Source: [`packages/code-runtime/code-runtime-worker/src/index.ts:24`](../packages/code-runtime/code-runtime-worker/src/index.ts) +Source: [`packages/code-runtime/code-runtime-worker/src/index.ts:21`](../packages/code-runtime/code-runtime-worker/src/index.ts) ## `@deepseek-ai/dsh-compact-basic` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index fd831f459c..83888c02e1 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -82,7 +82,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:31`](../../packages/code-runtime/code-runtime/src/index.ts) +Source: [`packages/code-runtime/code-runtime/src/index.ts:30`](../../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/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl index 1962ff8d9e..779844067a 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 b251bacddd..b68703ad48 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 c49b9ff3fa..8dac383517 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"}}} diff --git a/packages/code-runtime/code-runtime-worker/README.md b/packages/code-runtime/code-runtime-worker/README.md index 0c69674e7a..342799dabb 100644 --- a/packages/code-runtime/code-runtime-worker/README.md +++ b/packages/code-runtime/code-runtime-worker/README.md @@ -23,7 +23,7 @@ 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. @@ -31,6 +31,8 @@ Every field is validated (positive numbers) and defaulted; there are no other tu Source mode loads erasable-only `src/worker.ts` through Node's native type stripping. Built mode passes the sibling `lib/worker.cjs` as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node. `tests/built-lib.e2e.ts` pins the real load path required by [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. + ## Model Experience Indirectly, through Code Mode in [`dsh-tools`](../../core/tools/README.md), which renders this worker's capped printed or returned data and exact `[dsh-code-runtime-worker] log capture truncated at bytes` and `… [truncated]` markers into a retained `run_code` result. Binding traffic and worker internals stay outside context. diff --git a/packages/code-runtime/code-runtime-worker/package.json b/packages/code-runtime/code-runtime-worker/package.json index c9d25ef4d8..77169f8eab 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 db78f9d72e..7f36364a7c 100644 --- a/packages/code-runtime/code-runtime-worker/src/bootstrap.ts +++ b/packages/code-runtime/code-runtime-worker/src/bootstrap.ts @@ -7,7 +7,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' @@ -28,12 +27,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 @@ -42,28 +41,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) } } @@ -84,7 +83,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 } @@ -98,17 +97,16 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS) * * @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( @@ -256,9 +254,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 aee20f9d71..4a13baa07c 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -12,14 +12,11 @@ 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' -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 { /** @@ -112,10 +109,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 @@ -134,20 +127,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 } : {} } @@ -291,33 +272,33 @@ 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 budget covers normal, forged, and stray-pipe log entries. The first // overflow emits the shared in-band marker and drops everything after it. 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) // Exactly one outcome wins. Every path cleans up, terminates, and awaits the worker; // logs captured before timeout, abort, or failure remain in the result. @@ -386,7 +367,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 739f1e4eb5..1ce108b7cc 100644 --- a/packages/code-runtime/code-runtime-worker/src/protocol.ts +++ b/packages/code-runtime/code-runtime-worker/src/protocol.ts @@ -5,8 +5,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. */ @@ -20,7 +18,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 @@ -32,10 +30,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 24006a7c04..ff68fd09fe 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 @@ -38,9 +38,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 a7386938c5..23c9a8ee60 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 () => { @@ -268,8 +263,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) }) @@ -304,11 +299,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); @@ -329,7 +322,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 (;;) {} `, @@ -341,10 +334,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 94c3bad61c..1f680741a1 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -16,7 +16,7 @@ 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. ## Model Experience diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index e629a855b5..bd8efe1377 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -10,7 +10,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 1cc6d363a0..5d16b5d496 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -609,10 +609,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}', @@ -623,7 +619,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 8156e21bb5..012182db1b 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -108,14 +108,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 } @@ -251,12 +250,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, @@ -278,7 +277,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 fe5ba5402a..54575403cf 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 () => { @@ -503,7 +503,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') @@ -627,7 +627,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. @@ -636,9 +636,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/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"}}} diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 42be996773..51d98c659e 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -101,7 +101,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" },