@ai-devkit/agent-manager 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
- package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
- package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
- package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
- package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
- package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +25 -28
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
- package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
- package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
- package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/database/connection.d.ts.map +1 -1
- package/dist/database/connection.js +5 -1
- package/dist/database/connection.js.map +1 -1
- package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/dist/durable/DurableAgent.d.ts +23 -3
- package/dist/durable/DurableAgent.d.ts.map +1 -1
- package/dist/durable/DurableAgent.js +12 -0
- package/dist/durable/DurableAgent.js.map +1 -1
- package/dist/durable/DurableAgentRepository.d.ts +6 -7
- package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
- package/dist/durable/DurableAgentRepository.js +73 -42
- package/dist/durable/DurableAgentRepository.js.map +1 -1
- package/dist/durable/process.d.ts +8 -0
- package/dist/durable/process.d.ts.map +1 -0
- package/dist/durable/process.js +40 -0
- package/dist/durable/process.js.map +1 -0
- package/dist/durable/run.d.ts +32 -0
- package/dist/durable/run.d.ts.map +1 -0
- package/dist/durable/run.js +46 -0
- package/dist/durable/run.js.map +1 -0
- package/dist/durable/utils.d.ts +19 -0
- package/dist/durable/utils.d.ts.map +1 -0
- package/dist/durable/utils.js +52 -0
- package/dist/durable/utils.js.map +1 -0
- package/dist/index.d.ts +17 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
- package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
- package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
- package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
- package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
- package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
- package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
- package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
- package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
- package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
- package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
- package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.js +51 -0
- package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
- package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.js +71 -0
- package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
- package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
- package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
- package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
- package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
- package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
- package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
- package/src/__tests__/fixtures/fake-codex.cjs +45 -0
- package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
- package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +11 -21
- package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
- package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
- package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
- package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
- package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
- package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
- package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
- package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
- package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
- package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
- package/src/adapters/index.ts +1 -1
- package/src/database/connection.ts +3 -1
- package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/src/durable/DurableAgent.ts +51 -3
- package/src/durable/DurableAgentRepository.ts +60 -31
- package/src/durable/process.ts +28 -0
- package/src/durable/run.ts +74 -0
- package/src/durable/utils.ts +61 -0
- package/src/index.ts +31 -15
- package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
- package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
- package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
- package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
- package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
- package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
- package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
- package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
- package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
- package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
- package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
- package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
- package/src/providers/pi/durable/PiCliProbe.ts +48 -0
- package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
- package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
- package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
- package/src/providers/pi/durable/PiStreamParser.ts +84 -0
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
- package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
- package/dist/durable/ClaudeCliProbe.js.map +0 -1
- package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
- package/dist/durable/ClaudePrintAgentService.js +0 -66
- package/dist/durable/ClaudePrintAgentService.js.map +0 -1
- package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
- package/dist/durable/ClaudePrintRunner.js +0 -128
- package/dist/durable/ClaudePrintRunner.js.map +0 -1
- package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
- package/dist/utils/ClaudeSessionParser.js.map +0 -1
- package/src/durable/ClaudePrintAgentService.ts +0 -94
- package/src/durable/ClaudePrintRunner.ts +0 -139
- /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { PassThrough, Writable } from 'node:stream';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { PiDurableAgent } from '../../../../index.js';
|
|
5
|
+
|
|
6
|
+
const SESSION = '22222222-2222-4222-8222-222222222222';
|
|
7
|
+
function agent(sessionHealth: PiDurableAgent['sessionHealth'] = 'uninitialized'): PiDurableAgent {
|
|
8
|
+
return { id: '11111111-1111-4111-8111-111111111111', name: 'reviewer', provider: 'pi', mode: 'durable',
|
|
9
|
+
cwd: '/project', providerSessionId: SESSION, state: 'running', sessionHealth, createdAt: '',
|
|
10
|
+
updatedAt: '', lastActiveAt: null, lastResult: null, activeRun: null };
|
|
11
|
+
}
|
|
12
|
+
function fakeSpawn(lines: string[], exitCode = 0) {
|
|
13
|
+
const promptChunks: Buffer[] = [];
|
|
14
|
+
const child = new EventEmitter() as any;
|
|
15
|
+
child.pid = 4242; child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = vi.fn();
|
|
16
|
+
child.stdin = new Writable({
|
|
17
|
+
write(chunk, _encoding, callback) { promptChunks.push(Buffer.from(chunk)); callback(); },
|
|
18
|
+
final(callback) { child.stdout.end(lines.join('\n')); queueMicrotask(() => child.emit('close', exitCode, null)); callback(); },
|
|
19
|
+
});
|
|
20
|
+
return { child, spawn: vi.fn(() => child), promptChunks };
|
|
21
|
+
}
|
|
22
|
+
function events(session = SESSION): string[] { return [
|
|
23
|
+
JSON.stringify({ type: 'session', version: 3, id: session, cwd: '/project' }),
|
|
24
|
+
JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'first' }] } }),
|
|
25
|
+
JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'thinking', thinking: 'hidden' }, { type: 'text', text: 'final' }] } }),
|
|
26
|
+
JSON.stringify({ type: 'agent_end', messages: [] }), '',
|
|
27
|
+
]; }
|
|
28
|
+
async function runner(fixture: ReturnType<typeof fakeSpawn>, maxLineBytes?: number) {
|
|
29
|
+
const api = await import('../../../../index.js') as Record<string, unknown>;
|
|
30
|
+
expect(api).toHaveProperty('PiPrintRunner');
|
|
31
|
+
const Runner = api.PiPrintRunner as new (options: unknown) => any;
|
|
32
|
+
return new Runner({ spawn: fixture.spawn, maxLineBytes, processInspector: { getIdentity: () => ({ pid: 4242, startedAt: 'start' }) } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('PiPrintRunner', () => {
|
|
36
|
+
it('binds a first session and returns the last completed assistant text', async () => {
|
|
37
|
+
const fixture = fakeSpawn(events()); const instance = await runner(fixture); const order: string[] = [];
|
|
38
|
+
const result = await instance.run({ agent: agent(), prompt: 'secret', executable: 'fake-pi',
|
|
39
|
+
onSpawn: async () => { expect(fixture.promptChunks).toHaveLength(0); order.push('spawn'); } });
|
|
40
|
+
expect(order).toEqual(['spawn']);
|
|
41
|
+
expect(fixture.spawn).toHaveBeenCalledWith('fake-pi', ['--mode', 'json', '--session-id', SESSION], expect.objectContaining({ cwd: '/project', shell: false }));
|
|
42
|
+
expect(Buffer.concat(fixture.promptChunks).toString()).toBe('secret');
|
|
43
|
+
expect(result).toEqual({ sessionId: SESSION, result: 'final', messages: ['first', 'final'], exitCode: 0 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('resumes the exact stored session and rejects mismatch', async () => {
|
|
47
|
+
const fixture = fakeSpawn(events('33333333-3333-4333-8333-333333333333'));
|
|
48
|
+
await expect((await runner(fixture)).run({ agent: agent('healthy'), prompt: 'later', onSpawn: vi.fn() }))
|
|
49
|
+
.rejects.toMatchObject({ code: 'PI_SESSION_MISMATCH' });
|
|
50
|
+
expect(fixture.spawn.mock.calls[0]![1]).toEqual(['--mode', 'json', '--session', SESSION]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
['malformed', ['{bad\n'], 'PI_PROTOCOL'], ['non-object', ['[]\n'], 'PI_PROTOCOL'],
|
|
55
|
+
['truncated', ['{}'], 'PI_PROTOCOL'], ['missing session', [JSON.stringify({ type: 'agent_end' }), ''], 'PI_PROTOCOL'],
|
|
56
|
+
['missing result', [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'agent_end' }), ''], 'PI_RESULT_MISSING'],
|
|
57
|
+
['missing end', [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'x' }] } }), ''], 'PI_PROTOCOL'],
|
|
58
|
+
])('rejects %s output', async (_name, lines, code) => {
|
|
59
|
+
await expect((await runner(fakeSpawn(lines as string[]))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
|
|
60
|
+
.rejects.toMatchObject({ code });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects oversized output, failed processes, and unverifiable identities', async () => {
|
|
64
|
+
await expect((await runner(fakeSpawn(['x'.repeat(20)]), 10)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
|
|
65
|
+
.rejects.toMatchObject({ code: 'PI_PROTOCOL' });
|
|
66
|
+
await expect((await runner(fakeSpawn(events(), 1))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
|
|
67
|
+
.rejects.toMatchObject({ code: 'PI_PROCESS' });
|
|
68
|
+
const fixture = fakeSpawn([]); const api = await import('../../../../index.js') as Record<string, unknown>;
|
|
69
|
+
const Runner = api.PiPrintRunner as new (options: unknown) => any;
|
|
70
|
+
await expect(new Runner({ spawn: fixture.spawn, processInspector: { getIdentity: () => null } }).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
|
|
71
|
+
.rejects.toMatchObject({ code: 'PI_PROCESS' });
|
|
72
|
+
expect(fixture.child.kill).toHaveBeenCalledOnce();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects invalid and duplicate session identities', async () => {
|
|
76
|
+
for (const lines of [
|
|
77
|
+
[JSON.stringify({ type: 'session', id: 'bad' }), ''],
|
|
78
|
+
[JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'session', id: SESSION }), ''],
|
|
79
|
+
]) await expect((await runner(fakeSpawn(lines))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() }))
|
|
80
|
+
.rejects.toMatchObject({ code: 'PI_PROTOCOL' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('kills on spawn persistence failure', async () => {
|
|
84
|
+
const spawnFailure = fakeSpawn([]); const failure = new Error('cannot persist');
|
|
85
|
+
await expect((await runner(spawnFailure)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn().mockRejectedValue(failure), onSession: vi.fn() })).rejects.toBe(failure);
|
|
86
|
+
expect(spawnFailure.child.kill).toHaveBeenCalledOnce();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('classifies spawn errors and a missing PID as process failures', async () => {
|
|
90
|
+
const errored = fakeSpawn([]);
|
|
91
|
+
errored.child.stdin = new Writable({ write(_chunk, _encoding, callback) { callback(); }, final(callback) { queueMicrotask(() => errored.child.emit('error', new Error('spawn'))); callback(); } });
|
|
92
|
+
await expect((await runner(errored)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() })).rejects.toMatchObject({ code: 'PI_PROCESS' });
|
|
93
|
+
const missing = fakeSpawn([]); missing.child.pid = undefined;
|
|
94
|
+
await expect((await runner(missing)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() })).rejects.toMatchObject({ code: 'PI_PROCESS' });
|
|
95
|
+
expect(missing.child.kill).toHaveBeenCalledOnce();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('accepts string assistant content and ignores empty or unrelated messages', async () => {
|
|
99
|
+
const lines = [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'future' }),
|
|
100
|
+
JSON.stringify({ type: 'message_end', message: { role: 'user', content: 'ignored' } }),
|
|
101
|
+
JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: ' ' } }),
|
|
102
|
+
JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: 'answer' } }),
|
|
103
|
+
JSON.stringify({ type: 'agent_end' }), ''];
|
|
104
|
+
await expect((await runner(fakeSpawn(lines))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() }))
|
|
105
|
+
.resolves.toMatchObject({ result: 'answer' });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { PiStreamParser } from '../../../../providers/pi/durable/PiStreamParser.js';
|
|
3
|
+
|
|
4
|
+
const SESSION = '22222222-2222-4222-8222-222222222222';
|
|
5
|
+
|
|
6
|
+
describe('PiStreamParser', () => {
|
|
7
|
+
it('ignores blank lines and normalizes unexpected parser failures', () => {
|
|
8
|
+
const parser = new PiStreamParser(SESSION, 1024);
|
|
9
|
+
parser.accept('\n');
|
|
10
|
+
parser.fail(new Error('unexpected'));
|
|
11
|
+
|
|
12
|
+
expect(parser.hasFailed()).toBe(true);
|
|
13
|
+
expect(() => parser.result({ code: 0, signal: null }))
|
|
14
|
+
.toThrowError(expect.objectContaining({ code: 'PI_PROTOCOL', message: 'Pi stream processing failed.' }));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('includes the terminating signal in process failures', () => {
|
|
18
|
+
const parser = new PiStreamParser(SESSION, 1024);
|
|
19
|
+
parser.accept(`${JSON.stringify({ type: 'session', id: SESSION })}\n`);
|
|
20
|
+
|
|
21
|
+
expect(() => parser.result({ code: null, signal: 'SIGTERM' }))
|
|
22
|
+
.toThrowError(expect.objectContaining({ code: 'PI_PROCESS', message: 'Pi print run failed (SIGTERM)' }));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
2
|
+
* Tests for providers/claude/ClaudeSessionParser.ts — focused on stripping
|
|
3
3
|
* harness-injected XML tags from conversation content.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
-
import { ClaudeSessionParser } from '../../
|
|
10
|
+
import { ClaudeSessionParser } from '../../providers/claude/ClaudeSessionParser.js';
|
|
11
11
|
|
|
12
12
|
interface JsonlEntry {
|
|
13
13
|
type: 'user' | 'assistant' | 'system';
|
package/src/adapters/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ClaudeCodeAdapter } from '
|
|
1
|
+
export { ClaudeCodeAdapter } from '../providers/claude/ClaudeCodeAdapter.js';
|
|
2
2
|
export { CodexAdapter } from './CodexAdapter.js';
|
|
3
3
|
export { CopilotAdapter } from './CopilotAdapter.js';
|
|
4
4
|
export { GeminiCliAdapter } from './GeminiCliAdapter.js';
|
|
@@ -32,6 +32,7 @@ export class DatabaseConnection {
|
|
|
32
32
|
|
|
33
33
|
this.db = new Database(this.dbPath, {
|
|
34
34
|
readonly: this.readonly,
|
|
35
|
+
timeout: 5000,
|
|
35
36
|
verbose: typeof options.verbose === 'function'
|
|
36
37
|
? options.verbose
|
|
37
38
|
: options.verbose ? console.log : undefined,
|
|
@@ -50,7 +51,8 @@ export class DatabaseConnection {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
private configure(): void {
|
|
53
|
-
this.db.pragma('journal_mode
|
|
54
|
+
const journalMode = this.db.pragma('journal_mode', { simple: true }) as string;
|
|
55
|
+
if (journalMode.toLowerCase() !== 'wal') this.db.pragma('journal_mode = WAL');
|
|
54
56
|
this.db.pragma('foreign_keys = ON');
|
|
55
57
|
this.db.pragma('synchronous = NORMAL');
|
|
56
58
|
this.db.pragma('busy_timeout = 5000');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE durable_agents ALTER COLUMN provider_session_id DROP NOT NULL;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export type DurableAgentState = 'ready' | 'running' | 'degraded';
|
|
2
2
|
export type DurableSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
|
|
3
3
|
export type DurableRunStatus = 'succeeded' | 'failed' | 'interrupted';
|
|
4
|
+
export type PiPrintErrorCode =
|
|
5
|
+
| 'PI_CLI_UNAVAILABLE'
|
|
6
|
+
| 'PI_CLI_UNSUPPORTED'
|
|
7
|
+
| 'PI_PROCESS'
|
|
8
|
+
| 'PI_PROTOCOL'
|
|
9
|
+
| 'PI_RESULT_MISSING'
|
|
10
|
+
| 'PI_SESSION_MISMATCH'
|
|
11
|
+
| 'PI_UNSUPPORTED';
|
|
4
12
|
|
|
5
13
|
export const AGENT_MODES = {
|
|
6
14
|
INTERACTIVE: 'interactive',
|
|
@@ -26,13 +34,13 @@ export interface DurableLastResult {
|
|
|
26
34
|
summary: string;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
export
|
|
37
|
+
export type DurableProvider = 'claude' | 'codex' | 'pi';
|
|
38
|
+
|
|
39
|
+
export interface DurableAgentBase {
|
|
30
40
|
id: string;
|
|
31
41
|
name: string;
|
|
32
|
-
provider: 'claude';
|
|
33
42
|
mode: typeof AGENT_MODES.DURABLE;
|
|
34
43
|
cwd: string;
|
|
35
|
-
providerSessionId: string;
|
|
36
44
|
state: DurableAgentState;
|
|
37
45
|
sessionHealth: DurableSessionHealth;
|
|
38
46
|
createdAt: string;
|
|
@@ -42,6 +50,23 @@ export interface DurableAgent {
|
|
|
42
50
|
activeRun: DurableActiveRun | null;
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
export interface ClaudeDurableAgent extends DurableAgentBase {
|
|
54
|
+
provider: 'claude';
|
|
55
|
+
providerSessionId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CodexDurableAgent extends DurableAgentBase {
|
|
59
|
+
provider: 'codex';
|
|
60
|
+
providerSessionId: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PiDurableAgent extends DurableAgentBase {
|
|
64
|
+
provider: 'pi';
|
|
65
|
+
providerSessionId: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type DurableAgent = ClaudeDurableAgent | CodexDurableAgent | PiDurableAgent;
|
|
69
|
+
|
|
45
70
|
export class DurableAgentError extends Error {
|
|
46
71
|
constructor(
|
|
47
72
|
message: string,
|
|
@@ -89,3 +114,26 @@ export class ClaudePrintError extends DurableAgentError {
|
|
|
89
114
|
this.name = 'ClaudePrintError';
|
|
90
115
|
}
|
|
91
116
|
}
|
|
117
|
+
|
|
118
|
+
export type CodexPrintErrorCode =
|
|
119
|
+
| 'CODEX_PROTOCOL'
|
|
120
|
+
| 'CODEX_PROCESS'
|
|
121
|
+
| 'CODEX_SESSION_MISMATCH'
|
|
122
|
+
| 'CODEX_UNSUPPORTED'
|
|
123
|
+
| 'CODEX_RESULT_MISSING'
|
|
124
|
+
| 'CODEX_CLI_UNSUPPORTED'
|
|
125
|
+
| 'CODEX_CLI_UNAVAILABLE';
|
|
126
|
+
|
|
127
|
+
export class CodexPrintError extends DurableAgentError {
|
|
128
|
+
constructor(message: string, code: CodexPrintErrorCode) {
|
|
129
|
+
super(message, code);
|
|
130
|
+
this.name = 'CodexPrintError';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class PiPrintError extends DurableAgentError {
|
|
135
|
+
constructor(message: string, code: PiPrintErrorCode = 'PI_PROCESS') {
|
|
136
|
+
super(message, code);
|
|
137
|
+
this.name = 'PiPrintError';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import { execFileSync } from 'child_process';
|
|
4
3
|
import { DatabaseConnection, DEFAULT_AGENT_REGISTRY_DB_PATH } from '../database/index.js';
|
|
5
|
-
import { AGENT_MODES, type DurableActiveRun, type DurableAgent, type ProcessIdentity, type DurableRunStatus, type DurableSessionHealth } from './DurableAgent.js';
|
|
4
|
+
import { AGENT_MODES, CodexPrintError, type DurableActiveRun, type DurableAgent, type DurableProvider, type ProcessIdentity, type DurableRunStatus, type DurableSessionHealth } from './DurableAgent.js';
|
|
5
|
+
import { LocalProcessInspector, type ProcessInspector } from './process.js';
|
|
6
|
+
import { isUuid } from './utils.js';
|
|
6
7
|
import {
|
|
7
8
|
DurableAgentBusyError,
|
|
8
9
|
DurableAgentNameConflictError,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
} from './DurableAgent.js';
|
|
12
13
|
|
|
13
14
|
interface DurableAgentRow {
|
|
14
|
-
id: string; name: string; provider:
|
|
15
|
+
id: string; name: string; provider: DurableProvider; mode: typeof AGENT_MODES.DURABLE; cwd: string; provider_session_id: string | null;
|
|
15
16
|
state: DurableAgent['state']; session_health: DurableSessionHealth; created_at: string; updated_at: string;
|
|
16
17
|
last_active_at: string | null; last_result_status: DurableRunStatus | null;
|
|
17
18
|
last_result_completed_at: string | null; last_result_exit_code: number | null; last_result_summary: string | null;
|
|
@@ -19,7 +20,7 @@ interface DurableAgentRow {
|
|
|
19
20
|
active_provider_pid: number | null; active_provider_started_at: string | null; active_run_started_at: string | null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export interface CreateDurableAgentInput { name: string; cwd: string }
|
|
23
|
+
export interface CreateDurableAgentInput { name: string; cwd: string; provider?: DurableProvider }
|
|
23
24
|
|
|
24
25
|
export interface DurableAgentRepositoryOptions {
|
|
25
26
|
dbPath?: string;
|
|
@@ -34,7 +35,8 @@ export interface DurableAgentRepositoryOptions {
|
|
|
34
35
|
mutationLockStaleMs?: number;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export
|
|
38
|
+
export { LocalProcessInspector } from './process.js';
|
|
39
|
+
export type { ProcessInspector } from './process.js';
|
|
38
40
|
export interface DurableRunCompletion {
|
|
39
41
|
status: DurableRunStatus; exitCode: number | null; summary: string; sessionHealth: DurableSessionHealth;
|
|
40
42
|
}
|
|
@@ -64,13 +66,14 @@ export class DurableAgentRepository {
|
|
|
64
66
|
const cwd = this.canonicalDirectory(input.cwd);
|
|
65
67
|
const timestamp = this.now().toISOString();
|
|
66
68
|
const id = randomUUID();
|
|
67
|
-
|
|
69
|
+
const provider = input.provider ?? 'claude';
|
|
70
|
+
let providerSessionId = provider === 'codex' ? null : randomUUID();
|
|
68
71
|
while (providerSessionId === id) providerSessionId = randomUUID();
|
|
69
72
|
try {
|
|
70
73
|
this.db.execute(`INSERT INTO durable_agents (
|
|
71
74
|
id, name, provider, mode, cwd, provider_session_id, state, session_health, created_at, updated_at
|
|
72
|
-
) VALUES (?, ?,
|
|
73
|
-
[id, input.name, AGENT_MODES.DURABLE, cwd, providerSessionId, timestamp, timestamp]);
|
|
75
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'ready', 'uninitialized', ?, ?)`,
|
|
76
|
+
[id, input.name, provider, AGENT_MODES.DURABLE, cwd, providerSessionId, timestamp, timestamp]);
|
|
74
77
|
} catch (error) {
|
|
75
78
|
if (/UNIQUE constraint failed: durable_agents\.name/i.test((error as Error).message)) {
|
|
76
79
|
throw new DurableAgentNameConflictError(input.name);
|
|
@@ -157,6 +160,42 @@ export class DurableAgentRepository {
|
|
|
157
160
|
if (changed.changes !== 1) throw new DurableAgentRepositoryError('Print run ownership changed.');
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
async bindProviderSession(id: string, token: string, providerSessionId: string): Promise<DurableAgent> {
|
|
164
|
+
this.assertWritable();
|
|
165
|
+
if (!isUuid(providerSessionId)) {
|
|
166
|
+
throw new DurableAgentRepositoryError('Invalid provider session id.');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
this.immediate(() => {
|
|
170
|
+
const agent = this.findById(id);
|
|
171
|
+
if (!agent) throw new DurableAgentNotFoundError(id);
|
|
172
|
+
if (agent.provider !== 'codex') {
|
|
173
|
+
throw new DurableAgentRepositoryError('Only Codex durable sessions can be bound after creation.');
|
|
174
|
+
}
|
|
175
|
+
if (agent.activeRun?.token !== token) {
|
|
176
|
+
throw new DurableAgentRepositoryError('Print run ownership changed.');
|
|
177
|
+
}
|
|
178
|
+
if (agent.providerSessionId !== null && agent.providerSessionId !== providerSessionId) {
|
|
179
|
+
throw new CodexPrintError('Durable agent provider session identity does not match.', 'CODEX_SESSION_MISMATCH');
|
|
180
|
+
}
|
|
181
|
+
if (agent.providerSessionId === providerSessionId) return;
|
|
182
|
+
const changed = this.db.execute(`UPDATE durable_agents SET provider_session_id = ?, updated_at = ?
|
|
183
|
+
WHERE id = ? AND provider = 'codex' AND state = 'running' AND active_run_token = ?
|
|
184
|
+
AND provider_session_id IS NULL`,
|
|
185
|
+
[providerSessionId, this.now().toISOString(), id, token]);
|
|
186
|
+
if (changed.changes !== 1) throw new DurableAgentRepositoryError('Print run ownership changed.');
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof DurableAgentRepositoryError || error instanceof DurableAgentNotFoundError
|
|
190
|
+
|| error instanceof CodexPrintError) throw error;
|
|
191
|
+
if (/UNIQUE constraint failed: durable_agents\.provider_session_id/i.test((error as Error).message)) {
|
|
192
|
+
throw new CodexPrintError('Provider session is already bound to another durable agent.', 'CODEX_SESSION_MISMATCH');
|
|
193
|
+
}
|
|
194
|
+
throw this.storageError('Failed to bind durable-agent provider session', error);
|
|
195
|
+
}
|
|
196
|
+
return this.requireById(id);
|
|
197
|
+
}
|
|
198
|
+
|
|
160
199
|
async completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent> {
|
|
161
200
|
this.assertWritable();
|
|
162
201
|
const completedAt = this.now().toISOString();
|
|
@@ -227,6 +266,10 @@ export class DurableAgentRepository {
|
|
|
227
266
|
}
|
|
228
267
|
|
|
229
268
|
private fromRow(row: DurableAgentRow): DurableAgent {
|
|
269
|
+
if (!['claude', 'codex', 'pi'].includes(row.provider)
|
|
270
|
+
|| (row.provider !== 'codex' && row.provider_session_id === null)) {
|
|
271
|
+
throw new DurableAgentRepositoryError(`Invalid durable-agent provider record: ${row.id}`);
|
|
272
|
+
}
|
|
230
273
|
const activeRun: DurableActiveRun | null = row.active_run_token === null ? null : {
|
|
231
274
|
token: row.active_run_token,
|
|
232
275
|
owner: { pid: row.active_owner_pid!, startedAt: row.active_owner_started_at! },
|
|
@@ -235,9 +278,9 @@ export class DurableAgentRepository {
|
|
|
235
278
|
},
|
|
236
279
|
startedAt: row.active_run_started_at!,
|
|
237
280
|
};
|
|
238
|
-
|
|
281
|
+
const base = {
|
|
239
282
|
id: row.id, name: row.name, provider: row.provider, mode: row.mode, cwd: row.cwd,
|
|
240
|
-
|
|
283
|
+
state: row.state, sessionHealth: row.session_health,
|
|
241
284
|
createdAt: row.created_at, updatedAt: row.updated_at, lastActiveAt: row.last_active_at,
|
|
242
285
|
lastResult: row.last_result_status === null ? null : {
|
|
243
286
|
status: row.last_result_status, completedAt: row.last_result_completed_at!,
|
|
@@ -245,6 +288,13 @@ export class DurableAgentRepository {
|
|
|
245
288
|
},
|
|
246
289
|
activeRun,
|
|
247
290
|
};
|
|
291
|
+
if (row.provider === 'claude') {
|
|
292
|
+
return { ...base, provider: 'claude', providerSessionId: row.provider_session_id! };
|
|
293
|
+
}
|
|
294
|
+
if (row.provider === 'pi') {
|
|
295
|
+
return { ...base, provider: 'pi', providerSessionId: row.provider_session_id! };
|
|
296
|
+
}
|
|
297
|
+
return { ...base, provider: 'codex', providerSessionId: row.provider_session_id };
|
|
248
298
|
}
|
|
249
299
|
|
|
250
300
|
private canonicalDirectory(input: string): string {
|
|
@@ -284,24 +334,3 @@ export class DurableAgentRepository {
|
|
|
284
334
|
: new DurableAgentRepositoryError(`${prefix}: ${(error as Error).message}`);
|
|
285
335
|
}
|
|
286
336
|
}
|
|
287
|
-
|
|
288
|
-
export class LocalProcessInspector implements ProcessInspector {
|
|
289
|
-
getIdentity(pid: number): ProcessIdentity | null {
|
|
290
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
291
|
-
try {
|
|
292
|
-
if (process.platform === 'linux') {
|
|
293
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
294
|
-
const close = stat.lastIndexOf(')');
|
|
295
|
-
const fields = stat.slice(close + 2).split(' ');
|
|
296
|
-
const startTicks = fields[19];
|
|
297
|
-
return startTicks ? { pid, startedAt: `linux:${startTicks}` } : null;
|
|
298
|
-
}
|
|
299
|
-
const startedAt = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
300
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
301
|
-
}).trim();
|
|
302
|
-
return startedAt ? { pid, startedAt } : null;
|
|
303
|
-
} catch {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import type { ProcessIdentity } from './DurableAgent.js';
|
|
4
|
+
|
|
5
|
+
export interface ProcessInspector {
|
|
6
|
+
getIdentity(pid: number): ProcessIdentity | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class LocalProcessInspector implements ProcessInspector {
|
|
10
|
+
getIdentity(pid: number): ProcessIdentity | null {
|
|
11
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
12
|
+
try {
|
|
13
|
+
if (process.platform === 'linux') {
|
|
14
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
15
|
+
const close = stat.lastIndexOf(')');
|
|
16
|
+
const fields = stat.slice(close + 2).split(' ');
|
|
17
|
+
const startTicks = fields[19];
|
|
18
|
+
return startTicks ? { pid, startedAt: `linux:${startTicks}` } : null;
|
|
19
|
+
}
|
|
20
|
+
const startedAt = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
21
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
22
|
+
}).trim();
|
|
23
|
+
return startedAt ? { pid, startedAt } : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DurableAgent, DurableProvider } from './DurableAgent.js';
|
|
2
|
+
import { DurableAgentNotFoundError } from './DurableAgent.js';
|
|
3
|
+
import type { DurableRunCompletion } from './DurableAgentRepository.js';
|
|
4
|
+
import { sanitizeText } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const MAX_RESULT_SUMMARY_LENGTH = 4096;
|
|
7
|
+
|
|
8
|
+
export interface DurableRunRepository {
|
|
9
|
+
resolve(reference: string): Promise<DurableAgent | DurableAgent[] | null>;
|
|
10
|
+
acquireRun(id: string): Promise<{ agent: DurableAgent; token: string }>;
|
|
11
|
+
completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ProviderAgent<Provider extends DurableProvider> = Extract<DurableAgent, { provider: Provider }>;
|
|
15
|
+
|
|
16
|
+
interface DurableRunResult {
|
|
17
|
+
result: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DurableRunOptions<Provider extends DurableProvider, Result extends DurableRunResult> {
|
|
22
|
+
reference: string;
|
|
23
|
+
provider: Provider;
|
|
24
|
+
repository: DurableRunRepository;
|
|
25
|
+
ambiguousError(): Error;
|
|
26
|
+
providerError(): Error;
|
|
27
|
+
execute(agent: ProviderAgent<Provider>, token: string): Promise<Result>;
|
|
28
|
+
isSessionMismatch(error: unknown): boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runDurableAgent<Provider extends DurableProvider, Result extends DurableRunResult>(
|
|
32
|
+
options: DurableRunOptions<Provider, Result>,
|
|
33
|
+
): Promise<{ agent: ProviderAgent<Provider>; result: Result }> {
|
|
34
|
+
const resolved = await options.repository.resolve(options.reference);
|
|
35
|
+
if (!resolved) throw new DurableAgentNotFoundError(options.reference);
|
|
36
|
+
if (Array.isArray(resolved)) throw options.ambiguousError();
|
|
37
|
+
if (resolved.provider !== options.provider) throw options.providerError();
|
|
38
|
+
|
|
39
|
+
const target = resolved as ProviderAgent<Provider>;
|
|
40
|
+
const acquired = await options.repository.acquireRun(target.id);
|
|
41
|
+
let result: Result;
|
|
42
|
+
try {
|
|
43
|
+
result = await options.execute(acquired.agent as ProviderAgent<Provider>, acquired.token);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
await options.repository.completeRun(
|
|
46
|
+
target.id,
|
|
47
|
+
acquired.token,
|
|
48
|
+
createFailedCompletion(error, options.isSessionMismatch(error)),
|
|
49
|
+
);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await options.repository.completeRun(target.id, acquired.token, createSucceededCompletion(result));
|
|
54
|
+
return { agent: target, result };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSucceededCompletion(result: DurableRunResult): DurableRunCompletion {
|
|
58
|
+
return {
|
|
59
|
+
status: 'succeeded',
|
|
60
|
+
exitCode: result.exitCode,
|
|
61
|
+
summary: sanitizeText(result.result, MAX_RESULT_SUMMARY_LENGTH, { preserveFormatting: true }),
|
|
62
|
+
sessionHealth: 'healthy',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createFailedCompletion(error: unknown, sessionMismatch: boolean): DurableRunCompletion {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
return {
|
|
69
|
+
status: 'failed',
|
|
70
|
+
exitCode: null,
|
|
71
|
+
summary: sanitizeText(message, MAX_RESULT_SUMMARY_LENGTH, { preserveFormatting: true }),
|
|
72
|
+
sessionHealth: sessionMismatch ? 'mismatch' : 'unknown',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export interface ChildCloseResult {
|
|
4
|
+
code: number | null;
|
|
5
|
+
signal: NodeJS.Signals | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function waitForChildClose(child: ChildProcessWithoutNullStreams): Promise<ChildCloseResult> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
child.once('error', reject);
|
|
11
|
+
child.once('close', (code, signal) => resolve({ code, signal }));
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sanitizeText(
|
|
16
|
+
value: string,
|
|
17
|
+
max: number,
|
|
18
|
+
options: { preserveFormatting?: boolean } = {},
|
|
19
|
+
): string {
|
|
20
|
+
return Array.from(value, (character) => {
|
|
21
|
+
const code = character.charCodeAt(0);
|
|
22
|
+
if (options.preserveFormatting && (code === 9 || code === 10 || code === 13)) return character;
|
|
23
|
+
return code <= 31 || code === 127 ? ' ' : character;
|
|
24
|
+
}).join('').trim().slice(0, max);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
28
|
+
|
|
29
|
+
export function isUuid(value: string): boolean {
|
|
30
|
+
return UUID_PATTERN.test(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class LineBuffer {
|
|
34
|
+
private buffer = Buffer.alloc(0);
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly maxLineBytes: number,
|
|
38
|
+
private readonly createOversizedLineError: () => Error,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
accept(chunk: Buffer | string): Buffer[] {
|
|
42
|
+
this.buffer = Buffer.concat([this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
43
|
+
if (this.buffer.length > this.maxLineBytes && this.buffer.indexOf(0x0a) < 0) {
|
|
44
|
+
throw this.createOversizedLineError();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines: Buffer[] = [];
|
|
48
|
+
let newline: number;
|
|
49
|
+
while ((newline = this.buffer.indexOf(0x0a)) >= 0) {
|
|
50
|
+
const line = this.buffer.subarray(0, newline);
|
|
51
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
52
|
+
if (line.length > this.maxLineBytes) throw this.createOversizedLineError();
|
|
53
|
+
lines.push(line);
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assertComplete(createIncompleteLineError: () => Error): void {
|
|
59
|
+
if (this.buffer.length > 0) throw createIncompleteLineError();
|
|
60
|
+
}
|
|
61
|
+
}
|