Merge remote-tracking branch 'origin/master' into parallel-tool-call
# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/rfc/INDEX.md # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/index.ts # packages/core/agent-loop/src/loop.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # packages/subagent/subagent/src/types.ts # packages/subagent/tool-subagent/README.md # scripts/gen-cordis-catalog.ts # scripts/type-equiv.manifest.json
This commit is contained in:
@@ -19,7 +19,7 @@ A REPL agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + th
|
||||
|
||||
Run with: `pnpm run demo:repl` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details.
|
||||
|
||||
Its `code-mode.cordis.yml` overlay flips the same tree to **Code Mode**: the worker-thread code runtime is loaded and the tool registry runs `mode: code`, so the model gets exactly one wire tool — `run_code` — plus a generated TypeScript SDK section, and composes the other tools by writing a program whose output it curates. Run with: `pnpm run demo:code-mode` (the REPL is the default UI; `acp` as the argument serves the acp-agent example's same-shaped overlay instead) — see the [Code Mode section](coding-agent/README.md#code-mode) for what to try.
|
||||
Its `code-mode.cordis.yml` overlay flips the same tree to **Code Mode**: the worker-thread code runtime is loaded and the tool registry runs `mode: code`, so its registry contribution is the reserved `run_code` transport plus a generated TypeScript SDK section, and the model composes the other tools by writing a program whose output it curates. Run with: `pnpm run demo:code-mode` (the REPL is the default UI; `acp` as the argument serves the acp-agent example's same-shaped overlay instead) — see the [Code Mode section](coding-agent/README.md#code-mode) for what to try.
|
||||
|
||||
## cordis-agent
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_
|
||||
|
||||
## Code Mode
|
||||
|
||||
[`code-mode.cordis.yml`](code-mode.cordis.yml) is this same tree flipped to [Code Mode](../../docs/rfc/implemented/feature/2026-06-15-code-mode.md): an include overlay over `./cordis.yml` whose two patches insert the worker-thread code runtime (`@deepseek-ai/dsh-code-runtime-worker`, registering `ctx.codeRuntime`) and set `tools: { mode: code }` on the app. The model is then offered exactly ONE wire tool — `run_code` — plus a generated TypeScript SDK section declaring every other registered tool; it composes them by writing a program, each program tool call bridges back through the ordinary `tools/pre-execute`/`post-execute` pipeline one at a time and is logged as a `tool/code-dispatch` session event, and ONLY what the program prints or returns re-enters its context. (Flip the mode to `both` to offer native calls AND `run_code` side by side.)
|
||||
[`code-mode.cordis.yml`](code-mode.cordis.yml) is this same tree flipped to [Code Mode](../../docs/rfc/implemented/feature/2026-06-15-code-mode.md): an include overlay over `./cordis.yml` whose two patches insert the worker-thread code runtime (`@deepseek-ai/dsh-code-runtime-worker`, registering `ctx.codeRuntime`) and set `tools: { mode: code }` on the app. The registry contributes exactly one reserved wire transport — `run_code` — plus a generated TypeScript SDK section declaring the visible end-capability tools. The model composes those capabilities by writing a program; each program call carries an immutable link to its enclosing transport, bridges back through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation one at a time, and is logged as a `tool/code-dispatch` session event. Only what the program prints or returns re-enters model context. (Flip the mode to `both` to offer native calls and `run_code` side by side.)
|
||||
|
||||
```sh
|
||||
pnpm run demo:code-mode # this overlay under the REPL (default UI)
|
||||
|
||||
@@ -26,6 +26,14 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// `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))
|
||||
// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader
|
||||
// startup can therefore outlive a tight smoke-test deadline before the child
|
||||
// emits any output; 30s still detects a wedged process without confusing
|
||||
// bounded CI contention with a lifecycle failure.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
@@ -67,8 +75,8 @@ async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`code-mode overlay did not exit within 30s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 30_000)
|
||||
reject(new Error(`code-mode overlay did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
@@ -87,5 +95,5 @@ describe('code-mode overlay keyless smoke (real code-mode.cordis.yml via the Loa
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('code-mode agent ready.')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
@@ -35,6 +35,14 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||
// the repo, so point it at the repo tsconfig (root is four levels up).
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader
|
||||
// startup can therefore outlive a tight smoke-test deadline before the child
|
||||
// emits any output; 30s still detects a wedged process without confusing
|
||||
// bounded CI contention with a lifecycle failure.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
@@ -78,8 +86,8 @@ async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`coding-agent did not exit within 30s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 30_000)
|
||||
reject(new Error(`coding-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
@@ -98,5 +106,5 @@ describe('coding-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('agent REPL ready.')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
@@ -39,11 +39,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
|
||||
// dispose the whole context (simulating process exit) so only the JSONL
|
||||
// log on disk survives.
|
||||
ctx = await codingHarness(process.cwd(), { persona: SYSTEM_PROMPT, persistenceRoot: root })
|
||||
const first = ctx.agents.create({
|
||||
const first = (await ctx.agents.create({
|
||||
agentId: AgentId('resume-1'),
|
||||
sessionId: SESSION_ID,
|
||||
agentOptions: { model: 'deepseek-v4-flash' },
|
||||
}).agent as ReactLoopAgent
|
||||
})).agent as ReactLoopAgent
|
||||
first.send([{ type: 'text', text: `Remember this code for later: ${SECRET}. Just acknowledge it.` }])
|
||||
await waitForIdle(ctx, first)
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
@@ -29,6 +29,14 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||
// the repo, so point it at the repo tsconfig (root is three levels up).
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader
|
||||
// startup can therefore outlive a tight smoke-test deadline before the child
|
||||
// emits any output; 30s still detects a wedged process without confusing
|
||||
// bounded CI contention with a lifecycle failure.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
@@ -70,8 +78,8 @@ async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`cordis-agent did not exit within 30s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 30_000)
|
||||
reject(new Error(`cordis-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
@@ -90,5 +98,5 @@ describe('cordis-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('cordis-agent ready.')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
@@ -37,6 +37,14 @@ const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// a temp cwd OUTSIDE the repo, so point tsx at the repo tsconfig explicitly
|
||||
// (repo root is four levels up from examples/echo-agent/tests).
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// The real-API workflow runs up to 14 e2e files at once. Cold tsx/Loader
|
||||
// startup can therefore outlive a tight smoke-test deadline before the child
|
||||
// emits any output; 30s still detects a wedged process without confusing
|
||||
// bounded CI contention with a lifecycle failure.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
@@ -51,7 +59,7 @@ afterEach(async () => {
|
||||
/**
|
||||
* Boot echo-agent, write `lines` to its stdin, close stdin, and resolve with
|
||||
* the full stdout once the process exits (the stdio UI exits on EOF after the
|
||||
* agent settles). Rejects on a non-zero exit or a 30s timeout.
|
||||
* agent settles). Rejects on a non-zero exit or the process deadline.
|
||||
*/
|
||||
async function runEcho(lines: string[]): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'echo-smoke-'))
|
||||
@@ -84,8 +92,8 @@ async function runEcho(lines: string[]): Promise<{ stdout: string; code: number
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`echo-agent did not exit within 30s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 30_000)
|
||||
reject(new Error(`echo-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
@@ -105,19 +113,19 @@ describe('echo-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
const { stdout, code } = await runEcho([])
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('echo-agent ready.')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
|
||||
it('runs the echo tool round-trip for an "echo …" line', async () => {
|
||||
const { stdout } = await runEcho(['echo hello world'])
|
||||
// mock-llm.ts emits a tool-call for the echo tool; echo-tool.ts uppercases.
|
||||
expect(stdout).toContain('[tool call] echo')
|
||||
expect(stdout).toContain('[tool result] ECHO: HELLO WORLD')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
|
||||
it('streams a direct canned reply for a non-echo line', async () => {
|
||||
const { stdout } = await runEcho(['just chatting'])
|
||||
// The direct-response branch of mock-llm.ts quotes the input back.
|
||||
expect(stdout).toContain('just chatting')
|
||||
expect(stdout).not.toContain('[tool call]')
|
||||
}, 45_000)
|
||||
}, TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user