Merge remote-tracking branch 'origin/master' into codex/fix-deepseek-keepalive
# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml # examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml # packages/llm/llm-deepseek/README.i18n.yaml # packages/util/timeout/README.i18n.yaml
This commit is contained in:
@@ -29,6 +29,10 @@ class CliMockAdapter extends LlmAdapter {
|
||||
}
|
||||
|
||||
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
if (process.env.DSH_CLI_MOCK_FAILURE === '1') {
|
||||
yield { type: 'finish', reason: { kind: 'error', failure: { code: 'SERVER', message: 'CLI mock provider failed' } } }
|
||||
return
|
||||
}
|
||||
const toolResult = options.messages.at(-1)?.content.find(block => block.type === 'tool-result')
|
||||
if (toolResult === undefined) {
|
||||
const args = JSON.stringify({ command: 'printf CLI_TOOL_ROUND_TRIP', description: 'Prove the CLI tool round trip.' })
|
||||
@@ -59,7 +63,7 @@ export const inject = ['llm']
|
||||
/** Register the keyless `cli-mock` adapter. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['cli-mock'], new CliMockAdapter())
|
||||
ctx.on('agent/request', async (_agent, _turn, step, _signal, next) => {
|
||||
ctx.on('agent/request', async ({ step }, next) => {
|
||||
const config = await next()
|
||||
return step === 2 ? { ...config, reasoningEffort: OFF } : config
|
||||
})
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
persistenceRoot: './.sessions'
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext: false
|
||||
dshHome: './.dsh-home'
|
||||
skills:
|
||||
local:
|
||||
agentsHome: './.agents-home'
|
||||
persona: 'Keyless headless-agent smoke.'
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
patches:
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
apiKey: snapshot-key
|
||||
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
|
||||
thinking: disabled
|
||||
streamIdleTimeoutMs: 150
|
||||
- id: cli-agent
|
||||
- id: agent-spine
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: './.sessions'
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext: false
|
||||
persona: 'Keyless DeepSeek adapter defaults snapshot.'
|
||||
- id: persistence
|
||||
config:
|
||||
root: './.sessions'
|
||||
|
||||
8
examples/headless-agent/tests/fixtures/dsh-run.cordis.yml
vendored
Normal file
8
examples/headless-agent/tests/fixtures/dsh-run.cordis.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
- id: agent-default-model
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
|
||||
- insert:
|
||||
- id: cli-mock-llm
|
||||
name: './snapshot-fixtures/cli-mock-llm.ts'
|
||||
202
examples/headless-agent/tests/fixtures/e2b/e2b/bin.ts
vendored
Normal file
202
examples/headless-agent/tests/fixtures/e2b/e2b/bin.ts
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { boot } from '@deepseek-ai/dsh-app-boot'
|
||||
import { Inbox } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-fs-e2b'
|
||||
import type {} from '@deepseek-ai/dsh-bash-local'
|
||||
import type {} from '@deepseek-ai/dsh-lsp-local'
|
||||
import type {} from '@deepseek-ai/dsh-pty-local'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('usage: bin.ts <cordis.yml>')
|
||||
|
||||
const ctx = await boot('e2b-composition', resolve(configPath))
|
||||
const ownerFiber = ctx.plugin(() => {})
|
||||
const ownerId = SessionId('e2b-live-owner')
|
||||
const session = Session.create(ownerId)
|
||||
const owner: Agent = {
|
||||
id: ownerId,
|
||||
options: {},
|
||||
session,
|
||||
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
||||
status: 'idle',
|
||||
ctx: ownerFiber.ctx,
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
inject() {},
|
||||
cancel() {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
const unregisterOwner = ctx.agents.register(owner)
|
||||
let terminalId: Awaited<ReturnType<typeof ctx.pty.spawn>>['sessionId'] | undefined
|
||||
try {
|
||||
const sandbox = await ctx.e2b.getSandbox()
|
||||
const fromFs = await ctx.fs.resolve('from-fs.txt')
|
||||
const written = await ctx.fs.writeText(fromFs, 'written-by-fs\n', { kind: 'createIfAbsent' })
|
||||
const observed = await ctx.fs.stat(fromFs)
|
||||
if (observed?.version !== written.version) {
|
||||
throw new Error(`E2B rename did not preserve version metadata: ${JSON.stringify({ written, observed })}`)
|
||||
}
|
||||
await ctx.fs.editText(
|
||||
fromFs,
|
||||
{ oldString: 'written-by-fs', newString: 'versioned-by-fs', replaceAll: false },
|
||||
{ version: observed.version },
|
||||
)
|
||||
const bashRead = await ctx.bash.run(ctx.bash.resolve({ command: 'cat from-fs.txt' }))
|
||||
if (bashRead.exitCode !== 0 || bashRead.stdout.text !== 'versioned-by-fs\n') {
|
||||
throw new Error(`E2B Bash could not read the FS write: ${JSON.stringify(bashRead)}`)
|
||||
}
|
||||
|
||||
const bashWrite = await ctx.bash.run(ctx.bash.resolve({ command: "printf 'written-by-bash\\n' > from-bash.txt" }))
|
||||
if (bashWrite.exitCode !== 0) {
|
||||
throw new Error(`E2B Bash could not write the shared filesystem: ${JSON.stringify(bashWrite)}`)
|
||||
}
|
||||
const fromBash = await ctx.fs.resolve('from-bash.txt')
|
||||
const fsRead = await ctx.fs.readText(fromBash)
|
||||
|
||||
const environmentHandle = ctx.subprocess.spawn({
|
||||
argv: ['env'],
|
||||
cwd: process.cwd(),
|
||||
stdio: { stdin: 'ignore', stdout: { maxBytes: 65_536 }, stderr: { maxBytes: 4_096 } },
|
||||
graceMs: 500,
|
||||
env: {
|
||||
'FOO-BAR': 'hyphen-value',
|
||||
DSH_EXPLICIT: 'managed-value',
|
||||
TOKEN_EXPLICIT: 'credential-value',
|
||||
},
|
||||
})
|
||||
const environmentOutcome = await environmentHandle.done
|
||||
const environmentText = environmentHandle.collected.stdout?.readFrom(0).text
|
||||
if (environmentOutcome.exitCode !== 0 || environmentText === undefined) {
|
||||
throw new Error(`E2B subprocess environment probe failed: ${JSON.stringify(environmentOutcome)}`)
|
||||
}
|
||||
const environmentLines = new Set(environmentText.trimEnd().split('\n'))
|
||||
const explicitEnvironment = [
|
||||
'FOO-BAR=hyphen-value',
|
||||
'DSH_EXPLICIT=managed-value',
|
||||
'TOKEN_EXPLICIT=credential-value',
|
||||
].every(entry => environmentLines.has(entry))
|
||||
if (!explicitEnvironment) throw new Error(`E2B subprocess dropped an explicit environment entry: ${environmentText}`)
|
||||
|
||||
const splitUtf8Handle = ctx.subprocess.spawn({
|
||||
argv: ['bash', '-c', "printf '\\344'; sleep 0.05; printf '\\275'; sleep 0.05; printf '\\240'; sleep 0.05; printf '\\345'; sleep 0.05; printf '\\245'; sleep 0.05; printf '\\275'"],
|
||||
cwd: process.cwd(),
|
||||
stdio: { stdin: 'ignore', stdout: { maxBytes: 32 }, stderr: { maxBytes: 4_096 } },
|
||||
graceMs: 500,
|
||||
env: {},
|
||||
})
|
||||
const splitUtf8Outcome = await splitUtf8Handle.done
|
||||
const splitUtf8Output = splitUtf8Handle.collected.stdout?.readFrom(0).text
|
||||
if (splitUtf8Outcome.exitCode !== 0 || splitUtf8Output !== '你好') {
|
||||
throw new Error(`E2B subprocess corrupted split UTF-8 output: ${JSON.stringify({ splitUtf8Outcome, splitUtf8Output })}`)
|
||||
}
|
||||
|
||||
const outputDrainStarted = Date.now()
|
||||
const outputDrainHandle = ctx.subprocess.spawn({
|
||||
argv: ['bash', '-c', "bash -c 'exec -a dsh-output-drain-descendant sleep 30' & printf 'leader-done\\n'"],
|
||||
cwd: process.cwd(),
|
||||
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 }, stderr: { maxBytes: 4_096 } },
|
||||
graceMs: 250,
|
||||
env: {},
|
||||
})
|
||||
const outputDrainOutcome = await outputDrainHandle.done
|
||||
const outputDrainText = outputDrainHandle.collected.stdout?.readFrom(0).text
|
||||
const outputDrainElapsedMs = Date.now() - outputDrainStarted
|
||||
outputDrainHandle.terminate()
|
||||
const outputDrainExited = await outputDrainHandle.waitForExit(AbortSignal.timeout(5_000))
|
||||
const outputDrainProcesses = await sandbox.commands.list()
|
||||
const outputDrainClean = !outputDrainProcesses.some(processInfo =>
|
||||
JSON.stringify([processInfo.cmd, processInfo.args]).includes('dsh-output-drain-descendant'),
|
||||
)
|
||||
if (outputDrainOutcome.exitCode !== 0 || outputDrainText !== 'leader-done\n'
|
||||
|| outputDrainElapsedMs >= 10_000 || !outputDrainExited || !outputDrainClean) {
|
||||
throw new Error(`E2B subprocess did not bound descendant-held output: ${JSON.stringify({
|
||||
outputDrainOutcome, outputDrainText, outputDrainElapsedMs, outputDrainExited, outputDrainClean,
|
||||
})}`)
|
||||
}
|
||||
|
||||
const lspFixture = await readFile(new URL('./fixture-lsp.mjs', import.meta.url), 'utf8')
|
||||
const remoteLspFixture = await ctx.fs.resolve('fixture-lsp.mjs')
|
||||
await ctx.fs.writeText(remoteLspFixture, lspFixture, { kind: 'createIfAbsent' })
|
||||
const remoteSource = await ctx.fs.resolve('multibyte # file.ts')
|
||||
await ctx.fs.writeText(remoteSource, 'const café = "你好"\nconsole.log(café)\n', { kind: 'createIfAbsent' })
|
||||
const hover = await ctx.lsp.query({
|
||||
operation: 'hover',
|
||||
filePath: 'multibyte # file.ts',
|
||||
position: { line: 0, character: 7 },
|
||||
workspaceRoot: process.cwd(),
|
||||
})
|
||||
const definition = await ctx.lsp.query({
|
||||
operation: 'goToDefinition',
|
||||
filePath: 'multibyte # file.ts',
|
||||
position: { line: 0, character: 7 },
|
||||
workspaceRoot: process.cwd(),
|
||||
})
|
||||
|
||||
const terminal = await ctx.pty.spawn(owner, { type: 'shell' })
|
||||
terminalId = terminal.sessionId
|
||||
const terminalEcho = await ctx.pty.startSend(owner, terminal.sessionId, {
|
||||
text: "printf 'PTY-你好\\n'",
|
||||
submit: true,
|
||||
}).done
|
||||
const sleeping = ctx.pty.startSend(owner, terminal.sessionId, {
|
||||
text: "printf 'DSH_SLEEP_%s\\n' READY; sleep 30",
|
||||
submit: true,
|
||||
})
|
||||
let sleepReadyOutput = ''
|
||||
const sleepReadyDeadline = Date.now() + 5_000
|
||||
while (!sleepReadyOutput.includes('DSH_SLEEP_READY\n')) {
|
||||
sleepReadyOutput += sleeping.readOutput().delta
|
||||
if (sleepReadyOutput.includes('DSH_SLEEP_READY\n')) break
|
||||
const settled = await Promise.race([
|
||||
sleeping.done.then(result => ({ result })),
|
||||
new Promise<undefined>(resolveDelay => setTimeout(() => { resolveDelay(undefined) }, 25)),
|
||||
])
|
||||
if (settled !== undefined) {
|
||||
throw new Error(`E2B PTY successor settled before executing: ${JSON.stringify(settled.result)}`)
|
||||
}
|
||||
if (Date.now() >= sleepReadyDeadline) throw new Error(`E2B PTY successor did not execute: ${sleepReadyOutput}`)
|
||||
}
|
||||
const terminalSignal = await ctx.pty.signal(owner, terminal.sessionId, 'SIGINT')
|
||||
const interrupted = await sleeping.done
|
||||
const stubborn = await ctx.pty.startSend(owner, terminal.sessionId, {
|
||||
text: "bash -c 'trap \"\" TERM; exec sleep 30' & printf 'DSH_STUBBORN_PID=%s\\n' \"$!\"",
|
||||
submit: true,
|
||||
}).done
|
||||
const stubbornMatch = /DSH_STUBBORN_PID=([1-9][0-9]*)/.exec(stubborn.viewport)
|
||||
if (stubbornMatch?.[1] === undefined) throw new Error(`E2B PTY did not report its stubborn child: ${stubborn.viewport}`)
|
||||
const stubbornPid = Number(stubbornMatch[1])
|
||||
const terminalScrollback = ctx.pty.read(owner, terminal.sessionId, { count: 50 })
|
||||
await ctx.pty.kill(owner, terminal.sessionId, 'live E2B composition complete')
|
||||
terminalId = undefined
|
||||
const stubbornProbe = await sandbox.commands.run(`if kill -0 ${stubbornPid} 2>/dev/null; then printf alive; else printf gone; fi`)
|
||||
const terminalTreeCleanup = stubbornProbe.stdout === 'gone'
|
||||
if (!terminalTreeCleanup) throw new Error(`E2B PTY left process ${stubbornPid} alive after close`)
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
sandboxId: (await ctx.e2b.getSandbox()).sandboxId,
|
||||
bashRead: bashRead.stdout.text,
|
||||
fsRead,
|
||||
explicitEnvironment,
|
||||
splitUtf8Output,
|
||||
hover,
|
||||
definition,
|
||||
terminal: {
|
||||
motd: terminal.motd,
|
||||
echo: terminalEcho,
|
||||
signal: terminalSignal,
|
||||
interrupted,
|
||||
treeCleanup: terminalTreeCleanup,
|
||||
scrollback: terminalScrollback.text,
|
||||
},
|
||||
})}\n`)
|
||||
} finally {
|
||||
if (terminalId !== undefined) await ctx.pty.kill(owner, terminalId, 'fixture cleanup').catch(() => false)
|
||||
unregisterOwner()
|
||||
await ownerFiber.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
57
examples/headless-agent/tests/fixtures/e2b/e2b/cordis.yml
vendored
Normal file
57
examples/headless-agent/tests/fixtures/e2b/e2b/cordis.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# One-world invariant (same pairing as examples/headless-agent/e2b.cordis.yml):
|
||||
# e2b.cwd and sandbox-policy.workspaceRoot must name the same remote directory,
|
||||
# which is also bash-local's implicit default workdir.
|
||||
- id: e2b
|
||||
name: '@deepseek-ai/dsh-e2b'
|
||||
config:
|
||||
cwd: !!js process.cwd()
|
||||
timeoutMs: 180000
|
||||
|
||||
- id: subprocess-e2b
|
||||
name: '@deepseek-ai/dsh-subprocess-e2b'
|
||||
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 30000
|
||||
|
||||
- id: fs-e2b
|
||||
name: '@deepseek-ai/dsh-fs-e2b'
|
||||
|
||||
- id: agents
|
||||
name: '@deepseek-ai/dsh-agent'
|
||||
|
||||
- id: sandbox-policy
|
||||
name: '@deepseek-ai/dsh-sandbox-policy'
|
||||
config:
|
||||
mode: danger-full-access
|
||||
workspaceRoot: !!js process.cwd()
|
||||
|
||||
- id: pty
|
||||
name: '@deepseek-ai/dsh-pty'
|
||||
|
||||
- id: pty-local
|
||||
name: '@deepseek-ai/dsh-pty-local'
|
||||
config:
|
||||
pollIntervalMs: 25
|
||||
exactProbeAfterMs: 150
|
||||
idleSilenceMs: 2000
|
||||
handoffGraceMs: 500
|
||||
timeoutMs: 5000
|
||||
disposeGraceMs: 1000
|
||||
|
||||
- id: lsp
|
||||
name: '@deepseek-ai/dsh-lsp'
|
||||
|
||||
- id: lsp-local
|
||||
name: '@deepseek-ai/dsh-lsp-local'
|
||||
config:
|
||||
servers:
|
||||
fixture:
|
||||
command: node
|
||||
args:
|
||||
- !!js process.cwd() + '/fixture-lsp.mjs'
|
||||
extensionToLanguage:
|
||||
.ts: typescript
|
||||
shutdownTimeoutMs: 1000
|
||||
killGraceMs: 500
|
||||
85
examples/headless-agent/tests/fixtures/e2b/e2b/fixture-lsp.mjs
vendored
Normal file
85
examples/headless-agent/tests/fixtures/e2b/e2b/fixture-lsp.mjs
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
let pending = Buffer.alloc(0)
|
||||
let source = ''
|
||||
let sourceUri = ''
|
||||
|
||||
function send(message) {
|
||||
const body = Buffer.from(JSON.stringify(message))
|
||||
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`)
|
||||
process.stdout.write(body)
|
||||
}
|
||||
|
||||
function respond(id, result) {
|
||||
send({ jsonrpc: '2.0', id, result })
|
||||
}
|
||||
|
||||
function dispatch(message) {
|
||||
switch (message.method) {
|
||||
case 'initialize':
|
||||
respond(message.id, {
|
||||
capabilities: {
|
||||
positionEncoding: 'utf-16',
|
||||
textDocumentSync: { openClose: true, change: 1 },
|
||||
definitionProvider: true,
|
||||
referencesProvider: true,
|
||||
implementationProvider: true,
|
||||
hoverProvider: true,
|
||||
},
|
||||
})
|
||||
return
|
||||
case 'textDocument/didOpen':
|
||||
source = message.params.textDocument.text
|
||||
sourceUri = message.params.textDocument.uri
|
||||
return
|
||||
case 'textDocument/didClose':
|
||||
source = ''
|
||||
sourceUri = ''
|
||||
return
|
||||
case 'textDocument/hover':
|
||||
if (!source.includes('const café = "你好"')) {
|
||||
send({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: 'multibyte source was corrupted' } })
|
||||
return
|
||||
}
|
||||
respond(message.id, {
|
||||
contents: { kind: 'markdown', value: '**remote hover** 你好 café' },
|
||||
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
|
||||
})
|
||||
return
|
||||
case 'textDocument/definition':
|
||||
case 'textDocument/references':
|
||||
case 'textDocument/implementation':
|
||||
respond(message.id, [{
|
||||
uri: sourceUri,
|
||||
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
|
||||
}])
|
||||
return
|
||||
case 'shutdown':
|
||||
respond(message.id, null)
|
||||
return
|
||||
case 'exit':
|
||||
process.exit(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function drain() {
|
||||
for (;;) {
|
||||
const headerEnd = pending.indexOf('\r\n\r\n')
|
||||
if (headerEnd < 0) return
|
||||
const header = pending.subarray(0, headerEnd).toString('ascii')
|
||||
const match = /(?:^|\r\n)Content-Length: ([0-9]+)(?:\r\n|$)/i.exec(header)
|
||||
if (!match) throw new Error('missing Content-Length')
|
||||
const length = Number(match[1])
|
||||
const bodyStart = headerEnd + 4
|
||||
if (pending.length < bodyStart + length) return
|
||||
const body = pending.subarray(bodyStart, bodyStart + length)
|
||||
pending = pending.subarray(bodyStart + length)
|
||||
dispatch(JSON.parse(body.toString('utf8')))
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
pending = Buffer.concat([pending, chunk])
|
||||
drain()
|
||||
})
|
||||
@@ -17,12 +17,22 @@
|
||||
- id: seed-goal
|
||||
name: './seed-goal.ts'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the persisted goal domain.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: none
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
|
||||
@@ -7,11 +7,13 @@ export const name = 'seed-goal'
|
||||
export const inject = ['goals']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.on('agent/step', (agent) => {
|
||||
if (ctx.goals.get(agent) !== undefined) return
|
||||
ctx.goals.create(agent, {
|
||||
objective: 'Prove the composed goal survives in the session log',
|
||||
maxGoalRounds: 7,
|
||||
})
|
||||
ctx.on('agent/pre-step', ({ agent }, next) => {
|
||||
if (ctx.goals.get(agent) === undefined) {
|
||||
ctx.goals.create(agent, {
|
||||
objective: 'Prove the composed goal survives in the session log',
|
||||
maxGoalRounds: 7,
|
||||
})
|
||||
}
|
||||
return next()
|
||||
})
|
||||
}
|
||||
|
||||
33
examples/headless-agent/tests/fixtures/headless-driver.ts
vendored
Normal file
33
examples/headless-agent/tests/fixtures/headless-driver.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/** Snapshot-only Loader driver: stream one fixture turn as canonical JSONL. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
const NAME = 'headless-test-driver'
|
||||
const [configPath, ...taskParts] = process.argv.slice(2)
|
||||
if (configPath === undefined || taskParts.length === 0 || taskParts.every(part => part.trim() === '')) {
|
||||
throw new Error(`${NAME}: expected <config-path> <task...>`)
|
||||
}
|
||||
|
||||
const uninstallFailLoud = installFailLoud(NAME)
|
||||
let ctx: Context | undefined
|
||||
try {
|
||||
loadEnv(NAME)
|
||||
ctx = await boot(NAME, resolveConfigPath(configPath, undefined))
|
||||
const result = await runFixtureTurn(ctx, {
|
||||
task: taskParts.join(' '),
|
||||
onEvent: (sessionId: string, event: SessionEvent) => {
|
||||
process.stdout.write(`${JSON.stringify({ type: 'session_event', sessionId, event })}\n`)
|
||||
},
|
||||
})
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`)
|
||||
} catch (error: unknown) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
await ctx?.fiber.dispose()
|
||||
uninstallFailLoud()
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
// Generated by dsh-plugin-prepare. Do not edit.
|
||||
const manifest = {"name":"headless-repository-fixture","skills":["dsh-plugin-assets/skills/0"]}
|
||||
// Value mirror: Cordis const enum FiberState.ACTIVE; keep aligned with dsh-repository-plugin source.ts.
|
||||
const FIBER_ACTIVE = 2
|
||||
export const name = "headless-repository-fixture"
|
||||
export const inject = ["loader","skills"]
|
||||
async function mount(ctx, plugin, label, config) {
|
||||
const fiber = ctx.plugin(plugin, config)
|
||||
await fiber
|
||||
if (fiber.state !== FIBER_ACTIVE) {
|
||||
const missing = Object.keys(fiber.inject).filter(service => fiber.ctx.get(service) === undefined)
|
||||
throw new Error(`${label} did not activate (waiting for services: ${missing.join(', ') || 'unknown'})`)
|
||||
}
|
||||
}
|
||||
export async function apply(ctx) {
|
||||
const runtime = ctx.loader.builtins["dsh-repository-plugin"]
|
||||
if (runtime === undefined) throw new Error("missing Cordis builtin dsh-repository-plugin")
|
||||
await ctx.plugin(runtime, { baseUrl: import.meta.url, manifest })
|
||||
await mount(ctx, runtime, 'repository Plugin runtime', { baseUrl: import.meta.url, manifest })
|
||||
}
|
||||
|
||||
26
examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
vendored
Normal file
26
examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Loader fixture that resumes the seeded diagnostic-scenario parent before
|
||||
* CLI dispatch, so `list_agents` runs against its pre-seeded cold child.
|
||||
* @module subagent-diagnostic-agent
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Fixture plugin name. */
|
||||
export const name = 'subagent-diagnostic-agent'
|
||||
/** Services that must exist before the fixture resumes its agent. */
|
||||
export const inject = ['agents', 'agentLoop', 'sessionPersistence']
|
||||
|
||||
/**
|
||||
* Resume the seeded session and bind its exact handle to this fixture's lifetime.
|
||||
* @param ctx - settled agent and persistence services from the Loader tree.
|
||||
* @returns after the resumed agent is published.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
const handle = await ctx.agents.resume({
|
||||
resumeSessionId: 'subagent-diagnostic-parent' as SessionId,
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
ctx.effect(() => () => handle.dispose(), 'subagent-diagnostic-agent.handle')
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import { writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { once } from 'node:events'
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
|
||||
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('telemetry-otel driver requires a config path')
|
||||
@@ -34,7 +35,16 @@ const ctx = await boot('telemetry-otel-e2e', resolveConfigPath(configPath, undef
|
||||
try {
|
||||
// The fixture credential rides the model-visible user message; the exported
|
||||
// copy must scrub it while the canonical log keeps the original bytes.
|
||||
await runOneShot(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' })
|
||||
await runFixtureTurn(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' })
|
||||
const mode = process.env.DSH_TELEMETRY_E2E_MODE ?? 'FULL'
|
||||
if (mode !== 'FULL') {
|
||||
const [agent] = ctx.get('agents')?.roots() ?? []
|
||||
if (agent === undefined) throw new Error('telemetry-otel driver requires one root agent')
|
||||
recordFeedback(agent.session, 'fixture feedback')
|
||||
if (mode === 'FEEDBACK_ONLY') {
|
||||
await runFixtureTurn(ctx, { task: 'post-feedback private suffix' })
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
# path, exporting to the mock OTLP collector the driver starts (url via env).
|
||||
# The redact-rule entry models a deployment mounting its own scrub rule on the
|
||||
# telemetry/record waterfall — the seam itself ships no rules.
|
||||
- id: logger-console
|
||||
name: '@cordisjs/plugin-logger-console'
|
||||
config:
|
||||
colors: false
|
||||
levels:
|
||||
default: 3
|
||||
showTime: ''
|
||||
|
||||
- id: cli-mock-llm
|
||||
name: './cli-mock-llm.ts'
|
||||
|
||||
@@ -18,15 +26,26 @@
|
||||
- id: telemetry-otel
|
||||
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
||||
config:
|
||||
mode: !!js process.env.DSH_TELEMETRY_E2E_MODE || 'FULL'
|
||||
exporter:
|
||||
url: !!js process.env.DSH_TELEMETRY_E2E_URL
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the session-telemetry-otel plugin.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
/** Test driver that sends two turns through one Headless Loader composition. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('time-context driver requires a config path')
|
||||
|
||||
const ctx = await boot('time-context-e2e', resolveConfigPath(configPath, undefined))
|
||||
try {
|
||||
await runOneShot(ctx, { task: 'first' })
|
||||
await runOneShot(ctx, { task: 'second' })
|
||||
await runFixtureTurn(ctx, { task: 'first' })
|
||||
await runFixtureTurn(ctx, { task: 'second' })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
|
||||
@@ -12,12 +12,22 @@
|
||||
- id: time-context
|
||||
name: '@deepseek-ai/dsh-time-context'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: time-context-mock
|
||||
model: time-context-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: time-context-mock
|
||||
model: time-context-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the time-context plugin.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
|
||||
25
examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts
vendored
Normal file
25
examples/headless-agent/tests/fixtures/workspace-context-resume-agent.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Loader fixture that resumes the seeded workspace-context session.
|
||||
* @module workspace-context-resume-agent
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Fixture plugin name. */
|
||||
export const name = 'workspace-context-resume-agent'
|
||||
/** Services that must exist before the fixture resumes its agent. */
|
||||
export const inject = ['agents', 'agentLoop', 'sessionPersistence']
|
||||
|
||||
/**
|
||||
* Resume the seeded session and bind its handle to this fixture's lifetime.
|
||||
* @param ctx - settled agent and persistence services from the Loader tree.
|
||||
* @returns after the resumed agent is published.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
const handle = await ctx.agents.resume({
|
||||
resumeSessionId: 'workspace-context-resume' as SessionId,
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
ctx.effect(() => () => handle.dispose(), 'workspace-context-resume-agent.handle')
|
||||
}
|
||||
Reference in New Issue
Block a user