Merge remote-tracking branch 'origin/master' into session-fork
This commit is contained in:
@@ -92,6 +92,46 @@ function spawnAcpAgent(cwd: string, env: NodeJS.ProcessEnv = process.env): Spawn
|
||||
let spawned: Spawned | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
function hasStdoutLine(out: string[]): boolean {
|
||||
return out.join('').split('\n').some(line => line.trim().length > 0)
|
||||
}
|
||||
|
||||
async function waitForStdoutLine(child: ChildProcessWithoutNullStreams, out: string[], stderr: string[], timeoutMs: number): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
child.stdout.off('data', onData)
|
||||
child.off('exit', onExit)
|
||||
child.off('error', onError)
|
||||
}
|
||||
const pass = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
const fail = (reason: string) => {
|
||||
cleanup()
|
||||
reject(new Error(`${reason}; stderr: ${stderr.join('')}`))
|
||||
}
|
||||
const onData = () => {
|
||||
if (hasStdoutLine(out)) pass()
|
||||
}
|
||||
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
fail(`ACP child exited before emitting a stdout frame (code ${code ?? 'null'}, signal ${signal ?? 'null'})`)
|
||||
}
|
||||
const onError = (error: Error) => {
|
||||
fail(`ACP child failed before emitting a stdout frame: ${error.message}`)
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
fail(`ACP child did not emit a stdout frame within ${timeoutMs}ms`)
|
||||
}, timeoutMs)
|
||||
|
||||
child.stdout.on('data', onData)
|
||||
child.on('exit', onExit)
|
||||
child.on('error', onError)
|
||||
onData()
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
if (spawned) {
|
||||
spawned.child.kill('SIGKILL')
|
||||
@@ -114,16 +154,21 @@ describe('acp-agent over real stdio (no key required)', () => {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
const out: string[] = []
|
||||
const stderr: string[] = []
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stdout.on('data', (c: string) => out.push(c))
|
||||
child.stderr.on('data', (c: string) => stderr.push(c))
|
||||
|
||||
// Send a single initialize request as a newline-delimited JSON-RPC frame.
|
||||
const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} } })
|
||||
child.stdin.write(req + '\n')
|
||||
|
||||
// Give it a moment to boot + reply, then inspect stdout.
|
||||
await new Promise(r => setTimeout(r, 4000))
|
||||
child.kill('SIGKILL')
|
||||
try {
|
||||
await waitForStdoutLine(child, out, stderr, 15_000)
|
||||
} finally {
|
||||
child.kill('SIGKILL')
|
||||
}
|
||||
|
||||
const lines = out.join('').split('\n').filter(l => l.trim().length > 0)
|
||||
expect(lines.length).toBeGreaterThan(0)
|
||||
|
||||
@@ -43,8 +43,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
|
||||
// A handful of files for the model to read, so multiple bash steps
|
||||
// accumulate surface nodes (tool calls + results) and grow the history past
|
||||
// the (deliberately tiny) window.
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await writeFile(join(workdir, `file${i}.txt`), `This is file number ${i}. `.repeat(40))
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
await writeFile(join(workdir, `file${i}.txt`), `This is file number ${i}. `.repeat(50))
|
||||
}
|
||||
|
||||
// Tiny window so a couple of steps crosses the threshold. The generation
|
||||
@@ -55,21 +55,21 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
|
||||
ctx = await codingHarness(workdir, {
|
||||
persona: SYSTEM_PROMPT,
|
||||
compact: {
|
||||
contextWindow: 2400,
|
||||
contextWindow: 2000,
|
||||
thresholdRatio: 0.5,
|
||||
retainTokens: 500,
|
||||
retainTokens: 400,
|
||||
summarizationModel: '',
|
||||
maxTokens: 2048,
|
||||
maxTokens: 1024,
|
||||
compactionRetries: 1,
|
||||
},
|
||||
persistenceRoot: './.sessions',
|
||||
persistenceRoot: join(workdir, '.sessions'),
|
||||
})
|
||||
const agent = ctx.agentLoop.create(AgentId('e2e-compaction'), { model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.send([{
|
||||
type: 'text',
|
||||
text: 'Read file1.txt, file2.txt, file3.txt, file4.txt, file5.txt, and file6.txt one at a '
|
||||
+ 'time using cat (a separate bash command for each). After reading all six, tell me how '
|
||||
text: 'Read file1.txt, file2.txt, file3.txt, and file4.txt one at a '
|
||||
+ 'time using cat (a separate bash command for each). After reading all four, tell me how '
|
||||
+ 'many files you read and the number mentioned in file1.txt.',
|
||||
}])
|
||||
await waitForIdle(ctx, agent)
|
||||
@@ -98,9 +98,9 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
|
||||
expect(summaryData.shadowedSeqs.length).toBeGreaterThan(0)
|
||||
|
||||
// The conversation survived compaction: the agent produced a final answer
|
||||
// that reflects the work (it read six files).
|
||||
// that reflects the work (it read four files).
|
||||
const answer = finalText(events).toLowerCase()
|
||||
expect(answer.length).toBeGreaterThan(0)
|
||||
expect(answer).toMatch(/\b(6|six)\b/)
|
||||
expect(answer).toMatch(/\b(4|four)\b/)
|
||||
}, 240_000)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
@@ -9,6 +12,7 @@ import { codingHarness, finalText, SYSTEM_PROMPT, waitForIdle } from './harness.
|
||||
*/
|
||||
|
||||
let ctx: Context | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
// Always dispose the harness, even on failure/retry/timeout: agent-loop
|
||||
@@ -16,11 +20,14 @@ afterEach(async () => {
|
||||
// process the model left behind.
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bash tool', () => {
|
||||
it('runs a bash command on request and reports its output', async () => {
|
||||
ctx = await codingHarness(process.cwd(), { persona: SYSTEM_PROMPT })
|
||||
workdir = await mkdtemp(join(tmpdir(), 'dsh-full-loop-e2e-'))
|
||||
ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT })
|
||||
const agent = ctx.agentLoop.create(AgentId('e2e-loop'), { model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.send([{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }])
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
@@ -10,15 +13,19 @@ import { codingHarness, TODO_SYSTEM_PROMPT, waitForIdle } from './harness.ts'
|
||||
*/
|
||||
|
||||
let ctx: Context | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a plan', () => {
|
||||
it('appends a todo/write event with the model-produced task list', async () => {
|
||||
ctx = await codingHarness(process.cwd(), { persona: TODO_SYSTEM_PROMPT })
|
||||
workdir = await mkdtemp(join(tmpdir(), 'dsh-todo-write-e2e-'))
|
||||
ctx = await codingHarness(workdir, { persona: TODO_SYSTEM_PROMPT })
|
||||
const agent = ctx.agentLoop.create(AgentId('e2e-todo'), { model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.send([{ type: 'text', text:
|
||||
|
||||
Reference in New Issue
Block a user