Merge remote-tracking branch 'origin/master' into codex/skill-system
# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/module-graph.md # docs/rfc/INDEX.md # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # packages/core/agent-core/src/index.ts # packages/core/tools/tests/gen-tool-catalog.spec.ts # packages/support/acp-snapshot/src/suite.ts # packages/ui/acp-agent/src/index.ts
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Keyless Loader-path smoke for the Code Mode overlay: boot the REAL
|
||||
* example through the `@deepseek-ai/dsh-stdio-agent` bin against
|
||||
* `code-mode.cordis.yml` (the cordis Loader, `unwrapExports`, the include
|
||||
* patches over ./cordis.yml, the worker-thread code runtime, and the
|
||||
* registry in `mode: code`), then close stdin with no prompt and assert
|
||||
* the Code Mode banner + a clean exit.
|
||||
*
|
||||
* No prompt is ever sent, so the model is NEVER called and no `run_code`
|
||||
* turn happens — a dummy key lets `llm-deepseek`'s key-PRESENT check boot
|
||||
* the tree. This is the export-shape guard (postmortem 0001) for the Code
|
||||
* Mode composition; the with-key proof lives in `code-mode.e2e.ts`.
|
||||
*/
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../code-mode.cordis.yml', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig
|
||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||
// the repo, so point it at the repo tsconfig.
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (child !== undefined && child.exitCode === null) child.kill('SIGKILL')
|
||||
child = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'code-mode-smoke-'))
|
||||
const cwd = workdir
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
process.execPath,
|
||||
// --expose-internals: the included cordis.yml loads the HMR plugin (mirrors demo:code-mode).
|
||||
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||
// A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots.
|
||||
// No prompt is sent, so the adapter never streams — no network call.
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
child = proc
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
proc.stderr.setEncoding('utf8')
|
||||
proc.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`code-mode overlay did not exit within 10s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 10_000)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (code === 0) resolve({ stdout, code })
|
||||
else reject(new Error(`code-mode overlay exited ${code}. stderr:\n${stderr}`))
|
||||
})
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
|
||||
// No prompt — just EOF, so the stdio UI exits without ever running a turn.
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
|
||||
describe('code-mode overlay keyless smoke (real code-mode.cordis.yml via the Loader)', () => {
|
||||
it('boots the Code Mode plugin tree, prints its banner, and exits cleanly on EOF', async () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('code-mode agent ready.')
|
||||
}, 15_000)
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
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'
|
||||
|
||||
/**
|
||||
* The Code Mode with-key proof (the RFC's e2e tier): a REAL model under
|
||||
* `mode: 'code'`, a task that requires composing two tool calls, verified
|
||||
* against the WORLD — the persisted request header carried exactly
|
||||
* `[run_code]` as the wire tool list, each sub-call landed as a
|
||||
* `tool/code-dispatch` event, the file the program wrote exists on disk, and
|
||||
* the final answer is the program's curated output. Key-gated (see
|
||||
* vitest.e2e.config.ts); the keyless Loader-path smoke of the overlay lives
|
||||
* in `code-mode-keyless-smoke.e2e.ts`.
|
||||
*/
|
||||
|
||||
const PERSONA = 'You are coding-agent. You work by writing TypeScript programs for run_code: '
|
||||
+ '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: [] })
|
||||
await harness.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
|
||||
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)
|
||||
const agent = ctx.agentLoop.create(AgentId('e2e-code-mode'), { model: 'deepseek-v4-flash' })
|
||||
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user