feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
|
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
|
|
|
import { tmpdir } from 'node:os'
|
|
|
|
|
import { join } from 'node:path'
|
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
|
|
|
import { Context } from 'cordis'
|
|
|
|
|
import LlmService from '@deepseek-ai/dsh-llm'
|
|
|
|
|
import SessionStore from '@deepseek-ai/dsh-session'
|
|
|
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
|
|
|
import ToolRegistry, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
|
|
|
|
|
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
|
|
|
|
|
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
|
|
|
|
|
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
|
|
|
|
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
|
|
|
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
|
|
|
|
import { WorkerCodeRuntime } from '@deepseek-ai/dsh-code-runtime-worker'
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-13 23:27:00 +08:00
|
|
|
* With-key Code Mode proof: a real model receives only `run_code`, composes two
|
|
|
|
|
* sub-calls, writes a file, and returns curated output while the log records
|
|
|
|
|
* each `tool/code-dispatch`. The keyless Loader smoke is in the sibling test.
|
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
|
|
|
*/
|
|
|
|
|
|
refactor: unify the Code Mode demos on base-plus-overlay
Both demo:code-mode UIs now share one mechanism: the base example plus a
same-shaped code-mode.cordis.yml include overlay (insert the worker
runtime, flip tools.mode). Previously the REPL side was a hand-forked
example (examples/code-agent) that had also silently diverged — it
dropped compaction and the subagent stack — so the demo's UI argument
switched agents, not just surfaces. The fork is retired: coding-agent
gains the overlay, a Code Mode README section absorbing code-agent's,
and both of its tests (the keyless boot guard, retargeted at the
overlay; the with-key RFC proof, which hand-mounts its own harness and
moves untouched). The RFC's composed-surface and e2e-tier lines, the
examples index, the AGENTS.md smoke table, and the dsh-tools README
link now describe the overlay shape.
Verified live: overlay keyless smoke, with-key code-mode e2e from its
new home, demo:code-mode banner + EOF exit, and the acp handshake.
2026-07-09 13:02:19 +08:00
|
|
|
const PERSONA = 'You are coding-agent. You work by writing TypeScript programs for run_code: '
|
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
|
|
|
+ 'batch related tool work into one program and print or return ONLY the findings that matter.'
|
|
|
|
|
|
|
|
|
|
let ctx: Context | undefined
|
|
|
|
|
let workdir: string | undefined
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
// Always dispose, even on failure/retry/timeout: agent-loop teardown stops
|
|
|
|
|
// the loop, the executor kills stray processes, and the code runtime's
|
|
|
|
|
// dispose awaits worker exits.
|
|
|
|
|
await ctx?.fiber.dispose()
|
|
|
|
|
ctx = undefined
|
|
|
|
|
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
|
|
|
|
workdir = undefined
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function codeModeHarness(cwd: string): Promise<Context> {
|
|
|
|
|
const harness = new Context()
|
|
|
|
|
await harness.plugin(LlmService)
|
|
|
|
|
await harness.plugin(SessionStore)
|
|
|
|
|
await harness.plugin(SystemPrompt, { persona: PERSONA })
|
|
|
|
|
await harness.plugin(ToolRegistry, { mode: 'code' })
|
|
|
|
|
await harness.plugin(AgentRegistry)
|
|
|
|
|
await harness.plugin(AgentLoop, { agents: [] })
|
2026-07-14 21:57:52 +08:00
|
|
|
await harness.plugin(LlmDeepSeek)
|
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
|
|
|
await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
|
|
|
|
|
await harness.plugin(ToolBash)
|
|
|
|
|
await harness.plugin(WorkerCodeRuntime, {})
|
|
|
|
|
return harness
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function waitForIdle(harness: Context, agent: ReactLoopAgent): Promise<void> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const dispose = harness.on('agent/status', (subject, status) => {
|
|
|
|
|
if (subject === agent && status === 'idle') {
|
|
|
|
|
dispose()
|
|
|
|
|
resolve()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a program over real tools', () => {
|
|
|
|
|
it('collapses the wire tool list to [run_code], bridges sub-calls, and returns curated output', async () => {
|
|
|
|
|
workdir = await mkdtemp(join(tmpdir(), 'dsh-code-mode-e2e-'))
|
|
|
|
|
ctx = await codeModeHarness(workdir)
|
2026-07-14 21:57:52 +08:00
|
|
|
const agent = ctx.agentLoop.create(AgentId('e2e-code-mode'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
|
|
|
|
|
|
|
|
agent.send([{
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, '
|
|
|
|
|
+ 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), '
|
|
|
|
|
+ 'and return only the joined string.',
|
|
|
|
|
}])
|
|
|
|
|
await waitForIdle(ctx, agent)
|
|
|
|
|
const events: SessionEvent[] = [...agent.session.events]
|
|
|
|
|
|
|
|
|
|
// The wire contract: every request this session made offered EXACTLY ONE
|
|
|
|
|
// tool — run_code (the logged header snapshots the assembled list).
|
|
|
|
|
const headers = events.filter(event => event.type === 'request/header')
|
|
|
|
|
expect(headers.length).toBeGreaterThan(0)
|
|
|
|
|
for (const header of headers) {
|
|
|
|
|
expect(header.data.header.tools?.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
|
|
|
|
|
}
|
|
|
|
|
// The model actually went through run_code…
|
|
|
|
|
const calls = events.filter(event => event.type === 'tool/call')
|
|
|
|
|
expect(calls.length).toBeGreaterThan(0)
|
|
|
|
|
expect(calls.every(event => event.data.name === RUN_CODE_NAME)).toBe(true)
|
|
|
|
|
// …and the program's tool calls landed as dispatch events under it.
|
|
|
|
|
const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
|
|
|
|
|
expect(dispatches.length).toBeGreaterThanOrEqual(2)
|
|
|
|
|
expect(dispatches.every(event => event.data.name === 'bash')).toBe(true)
|
|
|
|
|
const parents = new Set(calls.map(event => event.data.callId))
|
|
|
|
|
expect(dispatches.every(event => parents.has(event.data.parentCallId))).toBe(true)
|
|
|
|
|
|
|
|
|
|
// World verification: the file the program wrote, and the curated answer.
|
|
|
|
|
const combined = await readFile(join(workdir, 'combined.txt'), 'utf8')
|
|
|
|
|
expect(combined).toContain('alpha-7')
|
|
|
|
|
expect(combined).toContain('beta-9')
|
|
|
|
|
const finalMessage = events.findLast(event => event.type === 'assistant/message')
|
|
|
|
|
const finalText = finalMessage !== undefined
|
|
|
|
|
? finalMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
|
|
|
|
: ''
|
|
|
|
|
expect(finalText).toContain('alpha-7')
|
|
|
|
|
expect(finalText).toContain('beta-9')
|
|
|
|
|
}, 180_000)
|
|
|
|
|
})
|