Merge remote-tracking branch 'origin/master' into codex/enforce-tool-cancellation
This commit is contained in:
240 files changed
+3844
-322
No files matched your search
@@ -33,6 +33,10 @@ The full-screen terminal sibling of `repl-agent`: it reuses the same coding back
|
||||
|
||||
Run with: `pnpm run demo:tui` (needs `DEEPSEEK_API_KEY`). See [tui-agent/README.md](tui-agent/README.md) for controls and composition.
|
||||
|
||||
## jsonrpc-agent
|
||||
|
||||
An unattended coding agent driven through the Python SDK: JSON-RPC stdio, foreground-only `bash`, `read` / `write` / `edit`, one foreground `subagent`, `todo_write`, JSONL persistence, and compaction. It excludes terminal UI, stdout logging, approvals, skills, and background task controls. See [jsonrpc-agent/README.md](jsonrpc-agent/README.md).
|
||||
|
||||
## cordis-agent
|
||||
|
||||
The **self-referential** demo: the coding spine plus [`@deepseek-ai/dsh-tool-cordis`](../packages/cordis/tool-cordis), whose three tools (`cordis_inspect` / `cordis_mount` / `cordis_unmount`) let the agent inspect the live cordis runtime it runs inside, mount model-written plugins into it (an event listener, a brand-new tool for itself, or a service another mount injects), and dispose them again — all dynamic mounts grouped under one `cordis-dynamic` fiber subtree. The `ctx.fs`/`ctx.web` services ride along provider-only, as the capabilities those plugins build on.
|
||||
|
||||
@@ -5,10 +5,10 @@ import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from
|
||||
/**
|
||||
* The acp-agent example's snapshot suite: the scenario table for
|
||||
* `dsh-acp-snapshot`'s suite factory, which owns every compare/guard mechanic
|
||||
* (golden + re-persisted-log diffs, record/refresh write-back, the pinned-header
|
||||
* (expected-output + re-persisted-log diffs, record/refresh write-back, the pinned-header
|
||||
* uniformity guard, the fixture guards). Fixtures live under `snapshots/<name>/`;
|
||||
* `pnpm run test:snapshot:record` re-records model transcripts against the real
|
||||
* API; `pnpm run test:snapshot:refresh` rewrites current replay goldens keyless.
|
||||
* API; `pnpm run test:snapshot:refresh` rewrites current replay expected outputs keyless.
|
||||
* See the package README (packages/support/acp-snapshot) and the snapshot RFC,
|
||||
* docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md.
|
||||
*/
|
||||
@@ -137,7 +137,7 @@ const SCENARIOS: Scenario[] = [
|
||||
// The mid-turn seams fire during a real model turn, so each is recorded with its hook active
|
||||
// (the model's reaction to a deny/block/force-continue is part of the captured transcript).
|
||||
// SessionStart/SubagentStart are excluded because detached injection races log
|
||||
// order; SubagentStop writes no transcript, so a golden could not prove it ran.
|
||||
// order; SubagentStop writes no transcript, so an expected output could not prove it ran.
|
||||
// Unit tests cover those points; the hook-snapshot-matrix RFC owns the rationale.
|
||||
{ name: 'hook-cc-promptsubmit-context', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-pretool-deny', hasModelTurn: true, recorded: true },
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -13,7 +13,7 @@ import { describe, expect, it } from 'vitest'
|
||||
const snapshotsDir = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
|
||||
const scenarioDir = join(snapshotsDir, 'advanced-toolchain')
|
||||
const sessionFixture = join(scenarioDir, 'session.jsonl')
|
||||
const streamGolden = join(scenarioDir, 'stream-json.golden.jsonl')
|
||||
const streamExpected = join(scenarioDir, 'stream-json.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../advanced.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
@@ -134,7 +134,7 @@ describe('headless stream-json snapshots', () => {
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
|
||||
if (refreshing) await writeFile(streamGolden, normalized)
|
||||
expect(normalized).toBe(await readFile(streamGolden, 'utf8'))
|
||||
if (refreshing) await writeFile(streamExpected, normalized)
|
||||
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
File renamed without changes.
@@ -0,0 +1,24 @@
|
||||
# jsonrpc-agent
|
||||
|
||||
The unattended coding-agent composition for the Python SDK's bundled JSON-RPC runtime. It intentionally loads no terminal UI, console logger, approval surface, or user-interaction tool because stdout belongs to the SDK protocol and turns are driven by the SDK.
|
||||
|
||||
The model-facing tools are:
|
||||
|
||||
- `bash`, foreground only
|
||||
- `read`, `write`, and `edit`
|
||||
- `subagent`, using one foreground in-process spawn provider
|
||||
- `todo_write`
|
||||
|
||||
The surrounding runtime also loads JSONL session persistence and automatic context compaction. `maxTokensAsSuccess` keeps a token-limited model turn as an accepted evaluation result while preserving its `max-tokens` reason.
|
||||
|
||||
## Runtime environment
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `DEEPSEEK_API_KEY` | Credential passed to the OpenAI-compatible host endpoint |
|
||||
| `DEEPSEEK_BASE_URL` | Host endpoint used by `dsh-llm-deepseek` |
|
||||
| `DSH_CWD` | Agent workspace for bash and filesystem tools |
|
||||
| `DSH_SESSION_ROOT` | JSONL trajectory directory |
|
||||
| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona |
|
||||
|
||||
Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Unattended coding-agent deployment for the bundled dsh-jsonrpc-agent runtime.
|
||||
# stdout is reserved for JSON-RPC; do not add a console logger or terminal UI.
|
||||
|
||||
- id: jsonrpc
|
||||
name: '@deepseek-ai/dsh-jsonrpc'
|
||||
config:
|
||||
maxTokensAsSuccess: true
|
||||
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
||||
timeoutMs: 60000
|
||||
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a coding agent.'
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash:
|
||||
enableRunInBackground: false
|
||||
toolTasks: false
|
||||
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
config:
|
||||
providerName: spawn
|
||||
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
enableRunInBackground: false
|
||||
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
||||
|
||||
- id: fs-policy
|
||||
name: '@deepseek-ai/dsh-fs-policy'
|
||||
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
config:
|
||||
contextWindow: 128000
|
||||
thresholdRatio: 0.8
|
||||
retainTokens: 20480
|
||||
summarizationModel: ''
|
||||
maxTokens: 8192
|
||||
compactionRetries: 1
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "jsonrpc-agent-example",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Unattended JSON-RPC coding-agent composition"
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
function waitForLine(
|
||||
lines: string[],
|
||||
predicate: (value: Record<string, unknown>) => boolean,
|
||||
stderr: () => string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + 30_000
|
||||
const poll = (): void => {
|
||||
while (lines.length > 0) {
|
||||
const line = lines.shift()!
|
||||
if (!line.trim()) continue
|
||||
try {
|
||||
const value = JSON.parse(line) as Record<string, unknown>
|
||||
if (predicate(value)) {
|
||||
resolve(value)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
reject(new Error(`non-JSON stdout from JSON-RPC agent runtime: ${line}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
reject(new Error(`timed out waiting for JSON-RPC response; stderr=${stderr()}`))
|
||||
return
|
||||
}
|
||||
setTimeout(poll, 10)
|
||||
}
|
||||
poll()
|
||||
})
|
||||
}
|
||||
|
||||
describe('jsonrpc-agent keyless smoke', () => {
|
||||
it('boots the real Cordis tree and serves initialize/shutdown over clean stdout', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-agent-smoke-'))
|
||||
const modelRequests: Record<string, unknown>[] = []
|
||||
const modelServer = createServer((request, response) => {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk: string) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
modelRequests.push(JSON.parse(body) as Record<string, unknown>)
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n')
|
||||
response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
|
||||
response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n')
|
||||
response.end('data: [DONE]\n\n')
|
||||
})
|
||||
})
|
||||
await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve))
|
||||
const address = modelServer.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('model server did not bind a TCP port')
|
||||
const child = spawn(process.execPath, [
|
||||
'--expose-internals',
|
||||
'--import',
|
||||
'tsx',
|
||||
binScript,
|
||||
configPath,
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
DSH_CWD: root,
|
||||
DSH_SESSION_ROOT: join(root, '.sessions'),
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
const lines: string[] = []
|
||||
let stdoutBuffer = ''
|
||||
let stderr = ''
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stdout.on('data', (chunk: string) => {
|
||||
stdoutBuffer += chunk
|
||||
const parts = stdoutBuffer.split('\n')
|
||||
stdoutBuffer = parts.pop() ?? ''
|
||||
lines.push(...parts)
|
||||
})
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
try {
|
||||
child.stdin.write(`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: { cwd: root, provider: 'deepseek', model: 'deepseek-v4-pro' },
|
||||
})}\n`)
|
||||
const initialized = await waitForLine(lines, value => value.id === 1, () => stderr)
|
||||
expect(initialized).toMatchObject({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
result: { serverInfo: { name: 'deepseek-harness-sdk-runtime' } },
|
||||
})
|
||||
|
||||
child.stdin.write(`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'session/prompt',
|
||||
params: { sessionId: 'main', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
|
||||
})}\n`)
|
||||
const prompt = await waitForLine(lines, value => value.id === 2, () => stderr)
|
||||
expect(prompt).toMatchObject({ jsonrpc: '2.0', id: 2, result: { accepted: true } })
|
||||
const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
|
||||
expect(tools.map(tool => tool.function?.name).sort()).toEqual([
|
||||
'bash',
|
||||
'edit',
|
||||
'read',
|
||||
'subagent',
|
||||
'todo_write',
|
||||
'write',
|
||||
])
|
||||
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
|
||||
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
|
||||
expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} })
|
||||
if (child.exitCode === null) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.once('exit', (code) => {
|
||||
if (code === 0) resolve()
|
||||
else reject(new Error(`runtime exited ${code}; stderr=${stderr}`))
|
||||
})
|
||||
})
|
||||
} else {
|
||||
expect(child.exitCode, stderr).toBe(0)
|
||||
}
|
||||
} finally {
|
||||
if (child.exitCode === null) child.kill('SIGKILL')
|
||||
await new Promise<void>(resolve => modelServer.close(() => { resolve() }))
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 40_000)
|
||||
})
|
||||
@@ -8,6 +8,7 @@
|
||||
"@cordisjs/plugin-hmr": "workspace:*",
|
||||
"@cordisjs/plugin-include": "workspace:*",
|
||||
"@deepseek-ai/dsh-acp-demo": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-cli-demo": "workspace:*",
|
||||
@@ -17,12 +18,14 @@
|
||||
"@deepseek-ai/dsh-fs-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-hooks-claude": "workspace:*",
|
||||
"@deepseek-ai/dsh-hooks-codex": "workspace:*",
|
||||
"@deepseek-ai/dsh-jsonrpc": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-replay": "workspace:*",
|
||||
"@deepseek-ai/dsh-permission": "workspace:*",
|
||||
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:*",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
|
||||
"@deepseek-ai/dsh-spill-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-stdio-demo": "workspace:*",
|
||||
|
||||
@@ -20,4 +20,4 @@ Run `pnpm run demo:code-mode tui` for the sibling Code Mode overlay.
|
||||
|
||||
## Snapshot tests
|
||||
|
||||
`tests/snapshots/<scenario>/session.jsonl` supplies recorded user prompts and model chunks; sibling child logs drive subagents and workflows. The keyless suite executes those scripts through the real loop and tool implementations, then compares readable terminal cell/style goldens. Use `pnpm run test:snapshot:refresh` for presentation-only changes and `pnpm run test:snapshot:record` with a DeepSeek key when a recorded model journey changes. The implemented [TUI snapshot RFC](../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns the scenario matrix and the split between recorded journeys, transient package snapshots, and PTY coverage.
|
||||
`tests/snapshots/<scenario>/session.jsonl` supplies recorded user prompts and model chunks; sibling child logs drive subagents and workflows. The keyless suite executes those scripts through the real loop and tool implementations, then compares readable expected terminal cell/style output. Use `pnpm run test:snapshot:refresh` for presentation-only changes and `pnpm run test:snapshot:record` with a DeepSeek key when a recorded model journey changes. The implemented [TUI snapshot RFC](../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns the scenario matrix and the split between recorded journeys, transient package snapshots, and PTY coverage.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -296,7 +296,7 @@ describe('TUI recorded-session terminal snapshots', () => {
|
||||
it(scenario.name, async () => {
|
||||
observedScenarios.add(scenario.name)
|
||||
const result = await runScenario(scenario)
|
||||
const terminalFile = join(scenarioDir(scenario), 'terminal.golden.txt')
|
||||
const terminalFile = join(scenarioDir(scenario), 'terminal.expected.txt')
|
||||
if (MODE === 'record' || MODE === 'refresh') {
|
||||
await mkdir(scenarioDir(scenario), { recursive: true })
|
||||
await writeFile(terminalFile, result.terminal)
|
||||
@@ -317,7 +317,7 @@ afterAll(async () => {
|
||||
for (const scenario of SCENARIOS) {
|
||||
const expected = [
|
||||
'session.jsonl',
|
||||
'terminal.golden.txt',
|
||||
'terminal.expected.txt',
|
||||
...scenario.seedWorkspace === true ? ['workspace'] : [],
|
||||
...Array.from({ length: scenario.childSessions ?? 0 }, (_, index) => `session.${index + 1}.jsonl`),
|
||||
].sort()
|
||||
|
||||
Reference in New Issue
Block a user