diff --git a/docs/architecture.md b/docs/architecture.md index 371b5df579..02dac4a2dd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,6 +26,7 @@ The default distribution is a composition, not a hierarchy. `packages/core/` is |---|---|---| | `ctx.llm` | [`llm/`](../packages/llm/README.md) | adapter registry and streaming model calls | | `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution | +| `ctx.codeRuntime` | [`code-runtime/`](../packages/code-runtime/README.md) | model-written program execution | | `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events | | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | | `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-surface compaction | diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 414ee898d1..339954c00f 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -42,6 +42,8 @@ flowchart LR pkg_bash_local["bash-local"] pkg_hooks_claude["hooks-claude"] pkg_hooks_codex["hooks-codex"] + pkg_code_runtime["code-runtime"] + svc_codeRuntime["ctx.codeRuntime
Code-execution seam"] pkg_fs["fs"] svc_fs["ctx.fs
Filesystem provider seam"] pkg_fs_local["fs-local"] @@ -64,6 +66,7 @@ flowchart LR pkg_agent_loop --> svc_agentLoop pkg_bash --> svc_bash pkg_bash_local --> svc_bash + pkg_code_runtime --> svc_codeRuntime pkg_compact --> svc_compact pkg_compact_basic --> svc_compact pkg_fs --> svc_fs @@ -134,6 +137,7 @@ flowchart LR | `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-agent`](../packages/ui/stdio-agent), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-core`](../packages/core/agent-core) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local. | +| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | - | - | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the Code Mode RFC specifies the worker-thread backend and the tool-registry consumer). | | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. | | `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-mock`](../packages/support/subagent-mock) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 2c101e318e..73ab7d45dd 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -802,6 +802,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)). - `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts)) +- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts)) - `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts)) - `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)) - `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts)) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index f7f2734a9f..c941192381 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -67,6 +67,23 @@ Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../c Source: [`packages/bash/bash/src/index.ts:59`](../../packages/bash/bash/src/index.ts) +## `ctx.codeRuntime` — `CodeRuntime` (abstract seam) + +Abstract code-execution service. Subclass, implement run and the two descriptors, and load the subclass as a plugin — it registers as `ctx.codeRuntime` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). + +Semantics every implementation must honor: + +- run resolves with an error FIELD for every program outcome — parse/transform failures, thrown exceptions, budget expiry, abort, substrate death (CodeRunFailure's taxonomy). It REJECTS only for caller misuse of the seam itself (e.g. a run submitted after disposal). +- Binding calls bridge to the caller's CodeBindingFunctions verbatim; arguments and resolutions must be structured-cloneable, and the runtime treats the program as a hostile peer (arbitrary binding names are own properties, malformed traffic is rejected or ignored, never crashes the host). +- Runs are isolated from each other: no state survives from one run to the next through the runtime. +- Disposal reaches quiescence: in-flight runs are terminated AND awaited before the service's own teardown completes (no orphan substrate survives `fiber.dispose()`). + +```ts cordis-catalog +abstract run(request: CodeRunRequest): Promise +``` + +Source: [`packages/code-runtime/code-runtime/src/index.ts:59`](../../packages/code-runtime/code-runtime/src/index.ts) + ## `ctx.compact` — `CompactService` (abstract seam) Abstract compaction service. Subclass implement the two abstract methods, and load the subclass as a plugin — it registers as `ctx.compact` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). diff --git a/docs/module-graph.md b/docs/module-graph.md index 8283a8b756..b12250d484 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -78,6 +78,9 @@ flowchart TD pkg_app_boot["app-boot"] pkg_stdio_agent["stdio-agent"] end + subgraph group_code_runtime["packages/code-runtime"] + pkg_code_runtime["code-runtime"] + end pkg_llm --> pkg_brand pkg_bash --> pkg_brand pkg_llm_deepseek --> pkg_llm @@ -208,6 +211,7 @@ flowchart TD | --- | --- | --- | | [`brand`](../packages/util/brand) | `util` | — | | [`app-boot`](../packages/ui/app-boot) | `ui` | — | +| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — | | [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) | | [`bash`](../packages/bash/bash) | `bash` | [`brand`](../packages/util/brand) | | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`llm`](../packages/llm/llm) | diff --git a/docs/rfc/proposed/feature/2026-06-15-code-mode.md b/docs/rfc/proposed/feature/2026-06-15-code-mode.md index 30f4d71319..a5f1563126 100644 --- a/docs/rfc/proposed/feature/2026-06-15-code-mode.md +++ b/docs/rfc/proposed/feature/2026-06-15-code-mode.md @@ -65,7 +65,7 @@ Per explicit-over-implicit at seams, the request spells out everything the runti ### The worker-thread runtime -`packages/code-runtime/code-runtime-worker/` — `@deepseek-ai/dsh-code-runtime-worker`. Per `run()`: +`@deepseek-ai/dsh-code-runtime-worker`, the second package of the `packages/code-runtime/` group. Per `run()`: 1. **Type-strip host-side** with Node's built-in `stripTypeScriptTypes` (`node:module`; present across the repo's whole engines range, `^22.19.0 || >=24.0.0`, and position-preserving, so runtime error line numbers match the model's source). Strip-only mode rejects non-erasable syntax (`enum`, namespaces) — that rejection returns as `error.kind: 'exception'` with Node's message, the SDK instructions say "erasable TypeScript only", and the model self-corrects like any other program error. A syntax-level failure never spawns a worker. 2. **Spawn one fresh `Worker` per run** from the package's own bootstrap module: `env: {}` (truly empty — stronger than the scrubbed-env rule for spawned commands), `resourceLimits` from config, `stdout`/`stderr` captured into `logs` rather than inherited. No pooling and no cross-run state: a program's world dies with its worker, which keeps runs reconstructable from the log alone and makes state bleed unrepresentable. diff --git a/packages/README.md b/packages/README.md index f07e0d5a36..da75f740e8 100644 --- a/packages/README.md +++ b/packages/README.md @@ -11,6 +11,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`core/`](core/README.md) | Product API spine: session, system-prompt, tools, agent, and the concrete loop | Product — stable surface | | [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface | | [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface | +| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs | Product — stable surface | | [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, and the model-facing file tools | Product — stable surface | | [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface | | [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface | diff --git a/packages/code-runtime/README.md b/packages/code-runtime/README.md new file mode 100644 index 0000000000..578f3179c1 --- /dev/null +++ b/packages/code-runtime/README.md @@ -0,0 +1,9 @@ +# code-runtime/ — code-execution capability family + +The code-execution capability seam (see [capability seams](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's Code Mode, and the first implementation (a Node worker-thread backend) is specified alongside it in the [Code Mode RFC](../../docs/rfc/proposed/feature/2026-06-15-code-mode.md). **Product** packages. + +| Package | Role | ctx key | +|---|---|---| +| `code-runtime/` | Abstract code-execution seam (interface + vocabulary) | `ctx.codeRuntime` | + +The interface lives at `code-runtime/code-runtime/`. Backends differ by execution substrate (worker thread, process, container) and by source language — both readonly descriptors on the service — and register `ctx.codeRuntime` without touching the interface or its consumer; that split is what makes a hardened backend a drop-in later. diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md new file mode 100644 index 0000000000..2d7b12add1 --- /dev/null +++ b/packages/code-runtime/code-runtime/README.md @@ -0,0 +1,19 @@ +# @deepseek-ai/dsh-code-runtime + +The **code-execution seam**: an abstract `CodeRuntime` service (`ctx.codeRuntime`) defining WHAT a code runtime does — run one model-written program against a set of host-provided async bindings and report `{ value, logs, error? }` — without saying HOW. + +This package is the interface third of the capability (the bash trio is the template — see [capability seams](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): implementations subclass `CodeRuntime` and register the service; the consumer is the tool registry's Code Mode, which generates the model-facing SDK and bridges tool dispatch — both specified in the [Code Mode RFC](../../../docs/rfc/proposed/feature/2026-06-15-code-mode.md), whose first implementation is a Node worker-thread backend. The runtime knows nothing about tools or sessions: it is handed named async functions and a program string, and everything tool-shaped stays with the consumer. + +## Service API (`ctx.codeRuntime`) + +| Member | Semantics | +|---|---| +| `run(request)` | Execute one program against the request's bindings. **Resolves with an error FIELD for every program outcome** — parse/transform failure, thrown exception, budget expiry, abort, substrate death (`CodeRunFailure`'s orthogonal `kind` taxonomy); it rejects only for caller misuse of the seam itself (e.g. a run submitted after disposal). The program runs as the body of an async function: top-level `await`/`return` work, and the completion value becomes `result.value` when it survives the serialization boundary. | +| `language` | Readonly descriptor: the source language `run` expects (`'typescript'` is the well-known value). Informational, not gating — a consumer that generates language-specific presentation switches on it and fails loud on a language it cannot present. | +| `isolation` | Readonly descriptor: the execution substrate (`'worker-thread'`, `'process'`, `'container'`). A label for deployments and diagnostics, **not a security claim**. | + +Semantics every implementation must honor (contract details in the class JSDoc): binding calls bridge to the caller's functions verbatim with structured-cloneable arguments/resolutions; the program is treated as a hostile peer (arbitrary binding names are own properties, malformed traffic never crashes the host); no state survives between runs; disposal terminates in-flight runs AND awaits their exit before completing. + +## 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. diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json new file mode 100644 index 0000000000..0fe24bb15c --- /dev/null +++ b/packages/code-runtime/code-runtime/package.json @@ -0,0 +1,30 @@ +{ + "name": "@deepseek-ai/dsh-code-runtime", + "description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "cordis": "^4.0.0-rc.6" + }, + "devDependencies": { + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts new file mode 100644 index 0000000000..af967da61d --- /dev/null +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -0,0 +1,93 @@ +/** + * The code-execution seam (`ctx.codeRuntime`): an abstract service defining + * WHAT a code runtime does — run one model-written program against a set of + * host-provided async bindings and report `{ value, logs, error? }` — without + * saying HOW. Implementations subclass {@link CodeRuntime} and register + * themselves as the `codeRuntime` service; backends may differ by execution + * substrate (worker thread, separate process, container) and by source + * language, both declared as readonly descriptors. The design and its + * consumer (the tool registry's Code Mode) are specified in the Code Mode RFC + * (docs/rfc/proposed/feature/2026-06-15-code-mode.md). + * + * The split mirrors the bash seam (`BashExecutor`): the runtime knows nothing + * about tools or sessions — it is handed named async functions and a program, + * and everything tool-shaped stays with the consumer. + * + * @module @deepseek-ai/dsh-code-runtime + */ + +import { Context, Service } from 'cordis' +import type { CodeRunRequest, CodeRunResult } from './types.ts' + +export type { + CodeBindingFunction, + CodeBindingNamespace, + CodeLogEntry, + CodeRunFailure, + CodeRunRequest, + CodeRunResult, +} from './types.ts' + +declare module 'cordis' { + interface Context { + codeRuntime: CodeRuntime + } +} + +/** + * Abstract code-execution service. Subclass, implement {@link run} and the + * two descriptors, and load the subclass as a plugin — it registers as + * `ctx.codeRuntime` (one implementation per context; loading a second throws, + * cordis' standard duplicate-service behavior). + * + * Semantics every implementation must honor: + * - {@link run} resolves with an error FIELD for every program outcome — + * parse/transform failures, thrown exceptions, budget expiry, abort, + * substrate death ({@link CodeRunFailure}'s taxonomy). It REJECTS only for + * caller misuse of the seam itself (e.g. a run submitted after disposal). + * - Binding calls bridge to the caller's {@link CodeBindingFunction}s + * verbatim; arguments and resolutions must be structured-cloneable, and the + * runtime treats the program as a hostile peer (arbitrary binding names are + * own properties, malformed traffic is rejected or ignored, never crashes + * the host). + * - Runs are isolated from each other: no state survives from one run to the + * next through the runtime. + * - Disposal reaches quiescence: in-flight runs are terminated AND awaited + * before the service's own teardown completes (no orphan substrate survives + * `fiber.dispose()`). + */ +export abstract class CodeRuntime extends Service { + /** + * The source language {@link run} expects `program` to be written in, as a + * lowercase identifier. Informational, not gating — a consumer that + * generates language-specific presentation (typed SDK stubs, usage + * instructions) switches on it and fails loud on a language it cannot + * present. Well-known value: `'typescript'`. + */ + abstract readonly language: string + + /** + * The execution substrate, as a lowercase identifier. Informational, not + * gating — a descriptor so deployments and diagnostics can tell backends + * apart, not a security claim. Well-known values: `'worker-thread'`, + * `'process'`, `'container'`. + */ + abstract readonly isolation: string + + constructor(ctx: Context) { + super(ctx, 'codeRuntime') + } + + /** + * Execute one program against the request's bindings and capture what it + * emitted. See the class doc for the resolution contract (error is a result + * field; rejection means seam misuse only). + * @param request - the program, its bindings, and the abort signal; the + * request carries everything the runtime acts on, with no hidden defaults. + * @returns the run's outcome: completion value (when transferable), the + * ordered log capture, and the failure (if any). + */ + abstract run(request: CodeRunRequest): Promise +} + +export default CodeRuntime diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts new file mode 100644 index 0000000000..8278f33a39 --- /dev/null +++ b/packages/code-runtime/code-runtime/src/types.ts @@ -0,0 +1,105 @@ +/** + * Vocabulary types for the code-execution seam: what a caller hands a + * {@link ../index.ts | CodeRuntime} and what it gets back. Pure types — no + * runtime code lives here. + * + * @module @deepseek-ai/dsh-code-runtime/src/types + */ + +/** + * One host-side function exposed to the program as an async callable. The + * runtime bridges calls to it (possibly across a serialization boundary), so + * `args` and the resolution value MUST be structured-cloneable; a runtime + * rejects a non-cloneable value with a descriptive error rather than + * corrupting the run. A rejection of this function surfaces inside the + * program as a rejection of the corresponding call. + */ +export type CodeBindingFunction = (args: unknown) => Promise + +/** + * A named group of {@link CodeBindingFunction}s the runtime exposes to the + * program as one global object (e.g. `tools`). Function names are arbitrary + * strings — a runtime must treat names like `__proto__` or `constructor` as + * ordinary own properties (null-prototype construction), never as prototype + * collisions. + */ +export interface CodeBindingNamespace { + /** The global identifier the program sees (must be a valid JS identifier). */ + global: string + /** The callable members, keyed by the exact name the program calls. */ + functions: Record +} + +/** + * One run: the program source plus everything the runtime acts on. Per the + * explicit-over-implicit convention, defaulting (time budgets, output caps) + * is the implementation's validated config — a request carries no optional + * tuning knobs for a hidden `??` to fill in. + */ +export interface CodeRunRequest { + /** + * The program source, in the runtime's {@link ../index.ts | language}. It + * runs as the body of an async function: top-level `await` and `return` + * are available, and the completion value becomes + * {@link CodeRunResult.value}. + */ + program: string + /** Host functions exposed to the program, one global object per namespace. */ + bindings: CodeBindingNamespace[] + /** + * Abort the run: the runtime stops the program (hard, even mid-loop) and + * resolves with a {@link CodeRunFailure} of kind `'abort'`. In-flight + * binding calls are the CALLER's to settle — the runtime only stops asking. + */ + 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 + * abort is not a timeout, and a substrate death is neither. + * + * - `'exception'` — the program threw or failed to parse/transform. + * - `'timeout'` — an implementation-owned budget expired; the message says which. + * - `'abort'` — {@link CodeRunRequest.signal} fired. + * - `'worker-exit'` — the execution substrate died without settling (e.g. OOM). + */ +export interface CodeRunFailure { + /** The failure class (see the interface doc for each kind's meaning). */ + kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' + /** Human-readable detail, suitable for feeding back to a model to self-correct. */ + message: string +} + +/** + * The outcome of one run. An error is a FIELD on a resolved result, never a + * rejection of `run()` — reporting a failed program is the caller's job, not + * an exception path. + */ +export interface CodeRunResult { + /** + * The program's completion value (its top-level `return`), when it ran to + * completion and the value survived the runtime's serialization boundary; + * a non-transferable value is replaced by a string rendering, and a failed + * or value-less run leaves this absent. + */ + value?: unknown + /** Everything the program emitted, in order (capped by the implementation). */ + logs: CodeLogEntry[] + /** 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 new file mode 100644 index 0000000000..4ff6d8f313 --- /dev/null +++ b/packages/code-runtime/code-runtime/tests/service.spec.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' +import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' + +/** + * Minimal concrete runtime: records requests, "executes" by invoking every + * binding once in declaration order, and lets tests script the outcome. The + * seam package ships no implementation, so the contract is exercised through + * the smallest subclass that honors it. + */ +class StubRuntime extends CodeRuntime { + readonly language = 'typescript' + readonly isolation = 'in-process-stub' + requests: CodeRunRequest[] = [] + nextResult: CodeRunResult = { logs: [] } + + async run(request: CodeRunRequest): Promise { + this.requests.push(request) + if (request.signal?.aborted) { + return { logs: [], error: { kind: 'abort', message: String(request.signal.reason) } } + } + for (const namespace of request.bindings) { + for (const fn of Object.values(namespace.functions)) { + await fn({ from: 'stub' }) + } + } + return this.nextResult + } +} + +async function setup() { + const ctx = new Context() + await ctx.plugin(StubRuntime) + const runtime = ctx.codeRuntime as StubRuntime + return { ctx, runtime } +} + +describe('CodeRuntime service seam', () => { + it('registers as ctx.codeRuntime and serves the abstract API', async () => { + const { runtime } = await setup() + expect(runtime.language).toBe('typescript') + expect(runtime.isolation).toBe('in-process-stub') + + const calls: unknown[] = [] + const result = await runtime.run({ + program: 'return 1', + bindings: [{ global: 'tools', functions: { probe: async args => void calls.push(args) } }], + }) + expect(result).toEqual({ logs: [] }) + expect(calls).toEqual([{ from: 'stub' }]) + expect(runtime.requests).toHaveLength(1) + }) + + 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' }], + error: { kind: 'exception', message: 'boom' }, + } + const result = await runtime.run({ program: 'throw new Error("boom")', bindings: [] }) + expect(result.error).toEqual({ kind: 'exception', message: 'boom' }) + expect(result.value).toBeUndefined() + }) + + it('reports a pre-aborted signal as an abort failure', async () => { + const { runtime } = await setup() + const controller = new AbortController() + controller.abort('cancelled') + const result = await runtime.run({ program: 'return 1', bindings: [], signal: controller.signal }) + expect(result.error).toEqual({ kind: 'abort', message: 'cancelled' }) + }) + + it('is removed from the context when the providing fiber disposes (HMR safety)', async () => { + const ctx = new Context() + const fiber = await ctx.plugin(StubRuntime) + expect(ctx.get('codeRuntime')).toBeInstanceOf(StubRuntime) + + await fiber.dispose() + expect(ctx.get('codeRuntime')).toBeUndefined() + }) + + it('rejects a second implementation in the same context (duplicate service)', async () => { + const { ctx } = await setup() + await expect(ctx.plugin(StubRuntime)).rejects.toThrow(/registered/) + }) +}) diff --git a/packages/code-runtime/code-runtime/tsconfig.json b/packages/code-runtime/code-runtime/tsconfig.json new file mode 100644 index 0000000000..754725418e --- /dev/null +++ b/packages/code-runtime/code-runtime/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b501bc21ee..ccf2f6978d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,12 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/code-runtime/code-runtime: + devDependencies: + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/compact/compact: devDependencies: '@deepseek-ai/dsh-llm': diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json index b23811e3d0..fc2b9d12c2 100644 --- a/scripts/doc-budgets.manifest.json +++ b/scripts/doc-budgets.manifest.json @@ -1,7 +1,7 @@ { "AGENTS.md": 1691, "docs/AGENTS.md": 1315, - "docs/architecture.md": 1630, + "docs/architecture.md": 1640, "docs/cordis-primer.md": 550, "docs/defensive-patterns.md": 550, "docs/testing.md": 800, diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 66cb102b00..6440a672be 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -149,6 +149,15 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'], note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local.', }, + { + key: 'codeRuntime', + pkg: 'code-runtime', + title: 'Code-execution seam', + mode: 'seam', + implementations: [], + consumers: [], + note: 'Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the Code Mode RFC specifies the worker-thread backend and the tool-registry consumer).', + }, { key: 'fs', pkg: 'fs', diff --git a/tsconfig.base.json b/tsconfig.base.json index 40e4dbe728..ea1d804ba5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -43,6 +43,7 @@ "./packages/core/*/src", "./packages/llm/*/src", "./packages/bash/*/src", + "./packages/code-runtime/*/src", "./packages/fs/*/src", "./packages/compact/*/src", "./packages/subagent/*/src", diff --git a/tsconfig.build.json b/tsconfig.build.json index b6ba7901f2..00739a361d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -22,6 +22,7 @@ { "path": "./packages/core/agent-loop" }, { "path": "./packages/core/agent-core" }, { "path": "./packages/bash/bash" }, + { "path": "./packages/code-runtime/code-runtime" }, { "path": "./packages/compact/compact" }, { "path": "./packages/compact/compact-basic" }, { "path": "./packages/llm/llm-deepseek" }, diff --git a/tsconfig.json b/tsconfig.json index 9cd7aa8a6d..5d90943c1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ { "path": "./packages/core/agent-loop" }, { "path": "./packages/core/agent-core" }, { "path": "./packages/bash/bash" }, + { "path": "./packages/code-runtime/code-runtime" }, { "path": "./packages/llm/llm-deepseek" }, { "path": "./packages/llm/llm-pi-ai" }, { "path": "./packages/bash/bash-local" },