@ai-devkit/agent-manager 0.27.0 → 0.28.1
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/README.md +12 -4
- package/dist/__tests__/adapters/CodexAdapter.test.js +199 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/database/DurableAgentsDatabase.test.js +100 -0
- package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -0
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +7 -7
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgentService.test.js +7 -7
- package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
- package/dist/__tests__/print/DurableAgent.test.js +17 -0
- package/dist/__tests__/print/DurableAgent.test.js.map +1 -0
- package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js +186 -0
- package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js.map +1 -0
- package/dist/__tests__/print/{PrintAgentStore.test.js → DurableAgentRepository.test.js} +63 -110
- package/dist/__tests__/print/DurableAgentRepository.test.js.map +1 -0
- package/dist/adapters/CodexAdapter.d.ts +1 -0
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +16 -6
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/database/connection.d.ts.map +1 -1
- package/dist/database/connection.js +17 -9
- package/dist/database/connection.js.map +1 -1
- package/dist/database/migrations/003_durable_agents.sql +43 -0
- package/dist/durable/ClaudeCliProbe.d.ts.map +1 -0
- package/dist/{print → durable}/ClaudeCliProbe.js +1 -1
- package/dist/durable/ClaudeCliProbe.js.map +1 -0
- package/dist/{print → durable}/ClaudePrintAgentService.d.ts +11 -11
- package/dist/durable/ClaudePrintAgentService.d.ts.map +1 -0
- package/dist/{print → durable}/ClaudePrintAgentService.js +12 -12
- package/dist/durable/ClaudePrintAgentService.js.map +1 -0
- package/dist/{print → durable}/ClaudePrintRunner.d.ts +3 -3
- package/dist/durable/ClaudePrintRunner.d.ts.map +1 -0
- package/dist/{print → durable}/ClaudePrintRunner.js +2 -2
- package/dist/durable/ClaudePrintRunner.js.map +1 -0
- package/dist/durable/DurableAgent.d.ts +61 -0
- package/dist/durable/DurableAgent.d.ts.map +1 -0
- package/dist/durable/DurableAgent.js +46 -0
- package/dist/durable/DurableAgent.js.map +1 -0
- package/dist/durable/DurableAgentRepository.d.ts +60 -0
- package/dist/durable/DurableAgentRepository.d.ts.map +1 -0
- package/dist/durable/DurableAgentRepository.js +325 -0
- package/dist/durable/DurableAgentRepository.js.map +1 -0
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/CodexAdapter.test.ts +132 -0
- package/src/__tests__/database/DurableAgentsDatabase.test.ts +77 -0
- package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +6 -6
- package/src/__tests__/print/ClaudePrintAgentService.test.ts +7 -7
- package/src/__tests__/print/ClaudePrintRunner.test.ts +3 -3
- package/src/__tests__/print/{PrintAgent.test.ts → DurableAgent.test.ts} +6 -6
- package/src/__tests__/print/DurableAgentRepository.sqlite.test.ts +105 -0
- package/src/__tests__/print/DurableAgentRepository.test.ts +163 -0
- package/src/adapters/CodexAdapter.ts +14 -11
- package/src/database/connection.ts +15 -9
- package/src/database/migrations/003_durable_agents.sql +43 -0
- package/src/{print → durable}/ClaudeCliProbe.ts +1 -1
- package/src/{print → durable}/ClaudePrintAgentService.ts +21 -21
- package/src/{print → durable}/ClaudePrintRunner.ts +4 -4
- package/src/durable/DurableAgent.ts +91 -0
- package/src/durable/DurableAgentRepository.ts +307 -0
- package/src/index.ts +26 -25
- package/dist/__tests__/print/PrintAgent.test.js +0 -17
- package/dist/__tests__/print/PrintAgent.test.js.map +0 -1
- package/dist/__tests__/print/PrintAgentStore.test.js.map +0 -1
- package/dist/print/ClaudeCliProbe.d.ts.map +0 -1
- package/dist/print/ClaudeCliProbe.js.map +0 -1
- package/dist/print/ClaudePrintAgentService.d.ts.map +0 -1
- package/dist/print/ClaudePrintAgentService.js.map +0 -1
- package/dist/print/ClaudePrintRunner.d.ts.map +0 -1
- package/dist/print/ClaudePrintRunner.js.map +0 -1
- package/dist/print/PrintAgent.d.ts +0 -57
- package/dist/print/PrintAgent.d.ts.map +0 -1
- package/dist/print/PrintAgent.js +0 -42
- package/dist/print/PrintAgent.js.map +0 -1
- package/dist/print/PrintAgentStore.d.ts +0 -69
- package/dist/print/PrintAgentStore.d.ts.map +0 -1
- package/dist/print/PrintAgentStore.js +0 -484
- package/dist/print/PrintAgentStore.js.map +0 -1
- package/src/__tests__/print/PrintAgentStore.test.ts +0 -192
- package/src/print/PrintAgent.ts +0 -86
- package/src/print/PrintAgentStore.ts +0 -503
- /package/dist/{print → durable}/ClaudeCliProbe.d.ts +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const tempDirs: string[] = [];
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
async function loadStore(): Promise<any> {
|
|
13
|
+
const api = await import('../../index.js') as Record<string, unknown>;
|
|
14
|
+
expect(api).toHaveProperty('DurableAgentRepository');
|
|
15
|
+
return api.DurableAgentRepository;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fixture(): { root: string; cwd: string; dbPath: string } {
|
|
19
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'durable-agent-store-'));
|
|
20
|
+
tempDirs.push(root);
|
|
21
|
+
const cwd = path.join(root, 'project');
|
|
22
|
+
fs.mkdirSync(cwd);
|
|
23
|
+
return { root, cwd, dbPath: path.join(root, 'state', 'agents.db') };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('DurableAgentRepository create/list/resolve', () => {
|
|
27
|
+
it('creates distinct durable identities with a canonical cwd and lists them', async () => {
|
|
28
|
+
const DurableAgentRepository = await loadStore();
|
|
29
|
+
const { cwd, dbPath } = fixture();
|
|
30
|
+
const repository = new DurableAgentRepository({ dbPath, now: () => new Date('2026-08-07T09:00:00Z') });
|
|
31
|
+
|
|
32
|
+
const agent = await repository.create({ name: 'reviewer', cwd });
|
|
33
|
+
|
|
34
|
+
expect(agent).toMatchObject({
|
|
35
|
+
name: 'reviewer',
|
|
36
|
+
provider: 'claude',
|
|
37
|
+
mode: 'durable',
|
|
38
|
+
cwd: fs.realpathSync(cwd),
|
|
39
|
+
state: 'ready',
|
|
40
|
+
sessionHealth: 'uninitialized',
|
|
41
|
+
activeRun: null,
|
|
42
|
+
});
|
|
43
|
+
expect(agent.id).toMatch(/^[0-9a-f-]{36}$/);
|
|
44
|
+
expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);
|
|
45
|
+
expect(agent.id).not.toBe(agent.providerSessionId);
|
|
46
|
+
expect(await repository.list()).toEqual([agent]);
|
|
47
|
+
expect(fs.existsSync(dbPath)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('resolves exact ids and names and rejects duplicate names', async () => {
|
|
51
|
+
const DurableAgentRepository = await loadStore();
|
|
52
|
+
const { cwd, dbPath } = fixture();
|
|
53
|
+
const repository = new DurableAgentRepository({ dbPath });
|
|
54
|
+
const agent = await repository.create({ name: 'Reviewer', cwd });
|
|
55
|
+
|
|
56
|
+
expect(await repository.resolve(agent.id)).toMatchObject({ id: agent.id });
|
|
57
|
+
expect(await repository.resolve('reviewer')).toMatchObject({ id: agent.id });
|
|
58
|
+
expect(await repository.resolve('view')).toBeNull();
|
|
59
|
+
await expect(repository.create({ name: 'reviewer', cwd })).rejects.toMatchObject({
|
|
60
|
+
code: 'DURABLE_AGENT_NAME_CONFLICT',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects a missing cwd', async () => {
|
|
65
|
+
const DurableAgentRepository = await loadStore();
|
|
66
|
+
const { root, dbPath } = fixture();
|
|
67
|
+
const repository = new DurableAgentRepository({ dbPath });
|
|
68
|
+
|
|
69
|
+
await expect(repository.create({ name: 'missing', cwd: path.join(root, 'missing') }))
|
|
70
|
+
.rejects.toMatchObject({ code: 'DURABLE_AGENT_REPOSITORY' });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('DurableAgentRepository run ownership', () => {
|
|
75
|
+
it('fails fast when another exact owner is live and completes only for its token', async () => {
|
|
76
|
+
const DurableAgentRepository = await loadStore();
|
|
77
|
+
const { cwd, dbPath } = fixture();
|
|
78
|
+
const live = new Map<number, string>([[process.pid, 'owner-start']]);
|
|
79
|
+
const processInspector = { getIdentity: (pid: number) => {
|
|
80
|
+
const startedAt = live.get(pid);
|
|
81
|
+
return startedAt ? { pid, startedAt } : null;
|
|
82
|
+
} };
|
|
83
|
+
const repository = new DurableAgentRepository({ dbPath, processInspector });
|
|
84
|
+
const agent = await repository.create({ name: 'runner', cwd });
|
|
85
|
+
|
|
86
|
+
const acquired = await repository.acquireRun(agent.id);
|
|
87
|
+
await expect(repository.acquireRun(agent.id)).rejects.toMatchObject({ code: 'DURABLE_AGENT_BUSY' });
|
|
88
|
+
await expect(repository.completeRun(agent.id, 'wrong-token', {
|
|
89
|
+
status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',
|
|
90
|
+
})).rejects.toMatchObject({ code: 'DURABLE_AGENT_REPOSITORY' });
|
|
91
|
+
|
|
92
|
+
const completed = await repository.completeRun(agent.id, acquired.token, {
|
|
93
|
+
status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',
|
|
94
|
+
});
|
|
95
|
+
expect(completed).toMatchObject({ state: 'ready', sessionHealth: 'healthy', activeRun: null });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('retains busy for a live provider then recovers a dead run without signaling it', async () => {
|
|
99
|
+
const DurableAgentRepository = await loadStore();
|
|
100
|
+
const { cwd, dbPath } = fixture();
|
|
101
|
+
const live = new Map<number, string>([[process.pid, 'owner-start'], [4242, 'provider-start']]);
|
|
102
|
+
const processInspector = { getIdentity: (pid: number) => {
|
|
103
|
+
const startedAt = live.get(pid);
|
|
104
|
+
return startedAt ? { pid, startedAt } : null;
|
|
105
|
+
} };
|
|
106
|
+
const first = new DurableAgentRepository({ dbPath, processInspector });
|
|
107
|
+
const agent = await first.create({ name: 'recoverable', cwd });
|
|
108
|
+
const run = await first.acquireRun(agent.id);
|
|
109
|
+
await first.recordProviderProcess(agent.id, run.token, { pid: 4242, startedAt: 'provider-start' });
|
|
110
|
+
|
|
111
|
+
live.delete(process.pid);
|
|
112
|
+
await expect(first.acquireRun(agent.id)).rejects.toMatchObject({ code: 'DURABLE_AGENT_BUSY' });
|
|
113
|
+
|
|
114
|
+
live.delete(4242);
|
|
115
|
+
live.set(process.pid, 'replacement-owner-start');
|
|
116
|
+
const recovered = await first.acquireRun(agent.id);
|
|
117
|
+
expect(recovered.agent).toMatchObject({
|
|
118
|
+
state: 'running',
|
|
119
|
+
lastResult: { status: 'interrupted' },
|
|
120
|
+
});
|
|
121
|
+
await first.completeRun(agent.id, recovered.token, {
|
|
122
|
+
status: 'failed', exitCode: 1, summary: 'failed', sessionHealth: 'unknown',
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reconciles an interrupted run to degraded during list', async () => {
|
|
127
|
+
const DurableAgentRepository = await loadStore();
|
|
128
|
+
const { cwd, dbPath } = fixture();
|
|
129
|
+
const live = new Map<number, string>([[process.pid, 'owner-start']]);
|
|
130
|
+
const repository = new DurableAgentRepository({ dbPath, incompleteLockGraceMs: 10, processInspector: {
|
|
131
|
+
getIdentity: (pid: number) => {
|
|
132
|
+
const startedAt = live.get(pid);
|
|
133
|
+
return startedAt ? { pid, startedAt } : null;
|
|
134
|
+
},
|
|
135
|
+
} });
|
|
136
|
+
const agent = await repository.create({ name: 'crashed', cwd });
|
|
137
|
+
await repository.acquireRun(agent.id);
|
|
138
|
+
live.clear();
|
|
139
|
+
|
|
140
|
+
const listed = await repository.list();
|
|
141
|
+
|
|
142
|
+
expect(listed[0]).toMatchObject({
|
|
143
|
+
state: 'degraded',
|
|
144
|
+
sessionHealth: 'unknown',
|
|
145
|
+
activeRun: null,
|
|
146
|
+
lastResult: { status: 'interrupted' },
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('rejects send acquisition when the bound cwd is replaced by a symlink', async () => {
|
|
151
|
+
const DurableAgentRepository = await loadStore();
|
|
152
|
+
const { root, cwd, dbPath } = fixture();
|
|
153
|
+
const repository = new DurableAgentRepository({ dbPath });
|
|
154
|
+
const agent = await repository.create({ name: 'bound', cwd });
|
|
155
|
+
const moved = path.join(root, 'moved-project');
|
|
156
|
+
const other = path.join(root, 'other-project');
|
|
157
|
+
fs.renameSync(cwd, moved);
|
|
158
|
+
fs.mkdirSync(other);
|
|
159
|
+
fs.symlinkSync(other, cwd);
|
|
160
|
+
|
|
161
|
+
await expect(repository.acquireRun(agent.id)).rejects.toMatchObject({ code: 'DURABLE_AGENT_REPOSITORY' });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -119,11 +119,11 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
119
119
|
const processes = relevant.filter((process) => this.canHandle(process));
|
|
120
120
|
if (processes.length === 0) return [];
|
|
121
121
|
|
|
122
|
-
const
|
|
123
|
-
|
|
122
|
+
const mappingResult = this.mapSessionMappingMatches(processes);
|
|
123
|
+
const { cachedAgents, remaining } = this.tryRegistryCache(mappingResult.fallback);
|
|
124
|
+
if (remaining.length === 0) return [...mappingResult.agents, ...cachedAgents];
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
const { direct, fallback } = this.tryResumeMatching(mappingResult.fallback);
|
|
126
|
+
const { direct, fallback } = this.tryResumeMatching(remaining);
|
|
127
127
|
const directResult = this.mapDirectMatches(direct);
|
|
128
128
|
const { sessions, contentCache } = this.discoverSessions(fallback);
|
|
129
129
|
if (sessions.length === 0) {
|
|
@@ -887,13 +887,12 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
887
887
|
const ts = this.parseTimestamp(entry.timestamp);
|
|
888
888
|
if (ts) lastTimestamp = ts;
|
|
889
889
|
|
|
890
|
-
if (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
firstUserMessage = entry.payload.message.trim();
|
|
890
|
+
if (!firstUserMessage) {
|
|
891
|
+
const message = this.toConversationMessage(entry, false);
|
|
892
|
+
const content = message?.content.trim() ?? '';
|
|
893
|
+
if (message?.role === 'user' && content.length > 0 && !this.isSyntheticUserMessage(content)) {
|
|
894
|
+
firstUserMessage = content;
|
|
895
|
+
}
|
|
897
896
|
}
|
|
898
897
|
}
|
|
899
898
|
|
|
@@ -917,4 +916,8 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
917
916
|
sessionFilePath: filePath,
|
|
918
917
|
};
|
|
919
918
|
}
|
|
919
|
+
|
|
920
|
+
private isSyntheticUserMessage(content: string): boolean {
|
|
921
|
+
return content.startsWith('<environment_context>') && content.includes('</environment_context>');
|
|
922
|
+
}
|
|
920
923
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import { mkdirSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { initializeSchema } from './schema.js';
|
|
@@ -25,7 +25,10 @@ export class DatabaseConnection {
|
|
|
25
25
|
constructor(options: DatabaseOptions = {}) {
|
|
26
26
|
this.dbPath = options.dbPath ?? DEFAULT_AGENT_REGISTRY_DB_PATH;
|
|
27
27
|
this.readonly = options.readonly ?? false;
|
|
28
|
-
|
|
28
|
+
if (this.readonly && !existsSync(this.dbPath)) {
|
|
29
|
+
throw new Error(`Cannot open readonly agent database because it does not exist: ${this.dbPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!this.readonly) mkdirSync(dirname(this.dbPath), { recursive: true });
|
|
29
32
|
|
|
30
33
|
this.db = new Database(this.dbPath, {
|
|
31
34
|
readonly: this.readonly,
|
|
@@ -34,16 +37,19 @@ export class DatabaseConnection {
|
|
|
34
37
|
: options.verbose ? console.log : undefined,
|
|
35
38
|
});
|
|
36
39
|
|
|
37
|
-
this.
|
|
38
|
-
|
|
40
|
+
if (this.readonly) {
|
|
41
|
+
const version = this.db.pragma('user_version', { simple: true }) as number;
|
|
42
|
+
if (version < 3) {
|
|
43
|
+
this.db.close();
|
|
44
|
+
throw new Error(`Readonly agent database requires schema version 3 (found ${version}).`);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
this.configure();
|
|
48
|
+
initializeSchema(this);
|
|
49
|
+
}
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
private configure(): void {
|
|
42
|
-
if (this.readonly) {
|
|
43
|
-
this.db.pragma('foreign_keys = ON');
|
|
44
|
-
this.db.pragma('busy_timeout = 5000');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
53
|
this.db.pragma('journal_mode = WAL');
|
|
48
54
|
this.db.pragma('foreign_keys = ON');
|
|
49
55
|
this.db.pragma('synchronous = NORMAL');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
CREATE TABLE durable_agents (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
|
4
|
+
provider TEXT NOT NULL,
|
|
5
|
+
mode TEXT NOT NULL DEFAULT 'durable',
|
|
6
|
+
cwd TEXT NOT NULL,
|
|
7
|
+
provider_session_id TEXT NOT NULL UNIQUE,
|
|
8
|
+
state TEXT NOT NULL CHECK (state IN ('ready','running','degraded')),
|
|
9
|
+
session_health TEXT NOT NULL CHECK (session_health IN ('uninitialized','healthy','unknown','mismatch')),
|
|
10
|
+
created_at TEXT NOT NULL,
|
|
11
|
+
updated_at TEXT NOT NULL,
|
|
12
|
+
last_active_at TEXT NULL,
|
|
13
|
+
last_result_status TEXT NULL CHECK (last_result_status IS NULL OR last_result_status IN ('succeeded','failed','interrupted')),
|
|
14
|
+
last_result_completed_at TEXT NULL,
|
|
15
|
+
last_result_exit_code INTEGER NULL,
|
|
16
|
+
last_result_summary TEXT NULL,
|
|
17
|
+
active_run_token TEXT UNIQUE,
|
|
18
|
+
active_owner_pid INTEGER NULL,
|
|
19
|
+
active_owner_started_at TEXT NULL,
|
|
20
|
+
active_provider_pid INTEGER NULL,
|
|
21
|
+
active_provider_started_at TEXT NULL,
|
|
22
|
+
active_run_started_at TEXT NULL,
|
|
23
|
+
CHECK (
|
|
24
|
+
(state = 'running'
|
|
25
|
+
AND active_run_token IS NOT NULL
|
|
26
|
+
AND active_owner_pid IS NOT NULL
|
|
27
|
+
AND active_owner_started_at IS NOT NULL
|
|
28
|
+
AND active_run_started_at IS NOT NULL)
|
|
29
|
+
OR
|
|
30
|
+
(state <> 'running'
|
|
31
|
+
AND active_run_token IS NULL
|
|
32
|
+
AND active_owner_pid IS NULL
|
|
33
|
+
AND active_owner_started_at IS NULL
|
|
34
|
+
AND active_provider_pid IS NULL
|
|
35
|
+
AND active_provider_started_at IS NULL
|
|
36
|
+
AND active_run_started_at IS NULL)
|
|
37
|
+
),
|
|
38
|
+
CHECK ((active_provider_pid IS NULL) = (active_provider_started_at IS NULL)),
|
|
39
|
+
CHECK ((last_result_status IS NULL) = (last_result_completed_at IS NULL))
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX idx_durable_agents_state ON durable_agents(state);
|
|
43
|
+
CREATE INDEX idx_durable_agents_list ON durable_agents(updated_at DESC, name COLLATE NOCASE);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import { ClaudePrintError } from './
|
|
3
|
+
import { ClaudePrintError } from './DurableAgent.js';
|
|
4
4
|
|
|
5
5
|
type ExecResult = { stdout: string; stderr: string };
|
|
6
6
|
type Exec = (file: string, args: string[]) => Promise<ExecResult>;
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { ClaudePrintError,
|
|
1
|
+
import type { DurableAgent, ProcessIdentity } from './DurableAgent.js';
|
|
2
|
+
import { ClaudePrintError, DurableAgentNotFoundError } from './DurableAgent.js';
|
|
3
3
|
import { ClaudeCliProbe } from './ClaudeCliProbe.js';
|
|
4
4
|
import { ClaudePrintRunner, type ClaudePrintRunResult } from './ClaudePrintRunner.js';
|
|
5
|
-
import {
|
|
5
|
+
import { DurableAgentRepository, type CreateDurableAgentInput, type DurableRunCompletion } from './DurableAgentRepository.js';
|
|
6
6
|
|
|
7
|
-
interface
|
|
8
|
-
create(input:
|
|
9
|
-
list(): Promise<
|
|
10
|
-
resolve(reference: string): Promise<
|
|
11
|
-
acquireRun(id: string): Promise<{ agent:
|
|
7
|
+
interface RepositoryLike {
|
|
8
|
+
create(input: CreateDurableAgentInput): Promise<DurableAgent>;
|
|
9
|
+
list(): Promise<DurableAgent[]>;
|
|
10
|
+
resolve(reference: string): Promise<DurableAgent | DurableAgent[] | null>;
|
|
11
|
+
acquireRun(id: string): Promise<{ agent: DurableAgent; token: string }>;
|
|
12
12
|
recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
|
|
13
|
-
completeRun(id: string, token: string, result:
|
|
13
|
+
completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface ProbeLike { validate(): Promise<{ executable: string; version: string }> }
|
|
17
17
|
interface RunnerLike { run(request: Parameters<ClaudePrintRunner['run']>[0]): Promise<ClaudePrintRunResult> }
|
|
18
18
|
|
|
19
19
|
export interface ClaudePrintAgentServiceOptions {
|
|
20
|
-
|
|
20
|
+
repository?: RepositoryLike;
|
|
21
21
|
probe?: ProbeLike;
|
|
22
22
|
runner?: RunnerLike;
|
|
23
23
|
executable?: string;
|
|
@@ -29,39 +29,39 @@ export interface ClaudePrintSendResult extends ClaudePrintRunResult {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class ClaudePrintAgentService {
|
|
32
|
-
readonly
|
|
32
|
+
readonly repository: RepositoryLike;
|
|
33
33
|
private readonly probe: ProbeLike;
|
|
34
34
|
private readonly runner: RunnerLike;
|
|
35
35
|
private readonly executable?: string;
|
|
36
36
|
|
|
37
37
|
constructor(options: ClaudePrintAgentServiceOptions = {}) {
|
|
38
|
-
this.
|
|
38
|
+
this.repository = options.repository ?? new DurableAgentRepository();
|
|
39
39
|
this.probe = options.probe ?? new ClaudeCliProbe();
|
|
40
40
|
this.runner = options.runner ?? new ClaudePrintRunner();
|
|
41
41
|
this.executable = options.executable;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async create(input:
|
|
44
|
+
async create(input: CreateDurableAgentInput): Promise<DurableAgent> {
|
|
45
45
|
await this.probe.validate();
|
|
46
|
-
return this.
|
|
46
|
+
return this.repository.create(input);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async send(reference: string, prompt: string): Promise<ClaudePrintSendResult> {
|
|
50
|
-
const resolved = await this.
|
|
51
|
-
if (!resolved) throw new
|
|
50
|
+
const resolved = await this.repository.resolve(reference);
|
|
51
|
+
if (!resolved) throw new DurableAgentNotFoundError(reference);
|
|
52
52
|
if (Array.isArray(resolved)) {
|
|
53
|
-
throw new ClaudePrintError(`Multiple
|
|
53
|
+
throw new ClaudePrintError(`Multiple durable agents match "${reference}".`, 'DURABLE_AGENT_AMBIGUOUS');
|
|
54
54
|
}
|
|
55
|
-
const acquired = await this.
|
|
55
|
+
const acquired = await this.repository.acquireRun(resolved.id);
|
|
56
56
|
try {
|
|
57
57
|
const result = await this.runner.run({
|
|
58
58
|
agent: acquired.agent,
|
|
59
59
|
prompt,
|
|
60
60
|
executable: this.executable,
|
|
61
61
|
firstRun: acquired.agent.sessionHealth === 'uninitialized',
|
|
62
|
-
onSpawn: (identity) => this.
|
|
62
|
+
onSpawn: (identity) => this.repository.recordProviderProcess(resolved.id, acquired.token, identity),
|
|
63
63
|
});
|
|
64
|
-
await this.
|
|
64
|
+
await this.repository.completeRun(resolved.id, acquired.token, {
|
|
65
65
|
status: 'succeeded',
|
|
66
66
|
exitCode: result.exitCode,
|
|
67
67
|
summary: sanitize(result.result, 4096),
|
|
@@ -73,7 +73,7 @@ export class ClaudePrintAgentService {
|
|
|
73
73
|
const sessionHealth = error instanceof ClaudePrintError && error.code === 'CLAUDE_SESSION_MISMATCH'
|
|
74
74
|
? 'mismatch' as const
|
|
75
75
|
: 'unknown' as const;
|
|
76
|
-
await this.
|
|
76
|
+
await this.repository.completeRun(resolved.id, acquired.token, {
|
|
77
77
|
status: 'failed',
|
|
78
78
|
exitCode: null,
|
|
79
79
|
summary: sanitize(failure.message, 4096),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'child_process';
|
|
2
|
-
import type {
|
|
3
|
-
import { ClaudePrintError } from './
|
|
4
|
-
import { LocalProcessInspector, type ProcessInspector } from './
|
|
2
|
+
import type { DurableAgent, ProcessIdentity } from './DurableAgent.js';
|
|
3
|
+
import { ClaudePrintError } from './DurableAgent.js';
|
|
4
|
+
import { LocalProcessInspector, type ProcessInspector } from './DurableAgentRepository.js';
|
|
5
5
|
|
|
6
6
|
type Spawn = (
|
|
7
7
|
command: string,
|
|
@@ -10,7 +10,7 @@ type Spawn = (
|
|
|
10
10
|
) => ChildProcessWithoutNullStreams;
|
|
11
11
|
|
|
12
12
|
export interface ClaudePrintRunRequest {
|
|
13
|
-
agent:
|
|
13
|
+
agent: DurableAgent;
|
|
14
14
|
prompt: string;
|
|
15
15
|
executable?: string;
|
|
16
16
|
firstRun: boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type DurableAgentState = 'ready' | 'running' | 'degraded';
|
|
2
|
+
export type DurableSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
|
|
3
|
+
export type DurableRunStatus = 'succeeded' | 'failed' | 'interrupted';
|
|
4
|
+
|
|
5
|
+
export const AGENT_MODES = {
|
|
6
|
+
INTERACTIVE: 'interactive',
|
|
7
|
+
DURABLE: 'durable',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export interface ProcessIdentity {
|
|
11
|
+
pid: number;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DurableActiveRun {
|
|
16
|
+
token: string;
|
|
17
|
+
owner: ProcessIdentity;
|
|
18
|
+
provider: ProcessIdentity | null;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DurableLastResult {
|
|
23
|
+
status: DurableRunStatus;
|
|
24
|
+
completedAt: string;
|
|
25
|
+
exitCode: number | null;
|
|
26
|
+
summary: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DurableAgent {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
provider: 'claude';
|
|
33
|
+
mode: typeof AGENT_MODES.DURABLE;
|
|
34
|
+
cwd: string;
|
|
35
|
+
providerSessionId: string;
|
|
36
|
+
state: DurableAgentState;
|
|
37
|
+
sessionHealth: DurableSessionHealth;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
lastActiveAt: string | null;
|
|
41
|
+
lastResult: DurableLastResult | null;
|
|
42
|
+
activeRun: DurableActiveRun | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class DurableAgentError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public readonly code: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'DurableAgentError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class DurableAgentBusyError extends DurableAgentError {
|
|
56
|
+
constructor(
|
|
57
|
+
public readonly agentId: string,
|
|
58
|
+
agentName: string,
|
|
59
|
+
) {
|
|
60
|
+
super(`Durable agent "${agentName}" is busy.`, 'DURABLE_AGENT_BUSY');
|
|
61
|
+
this.name = 'DurableAgentBusyError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class DurableAgentNotFoundError extends DurableAgentError {
|
|
66
|
+
constructor(public readonly reference: string) {
|
|
67
|
+
super(`Durable agent "${reference}" was not found.`, 'DURABLE_AGENT_NOT_FOUND');
|
|
68
|
+
this.name = 'DurableAgentNotFoundError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class DurableAgentRepositoryError extends DurableAgentError {
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super(message, 'DURABLE_AGENT_REPOSITORY');
|
|
75
|
+
this.name = 'DurableAgentRepositoryError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class DurableAgentNameConflictError extends DurableAgentError {
|
|
80
|
+
constructor(public readonly agentName: string) {
|
|
81
|
+
super(`Durable agent name "${agentName}" is already in use.`, 'DURABLE_AGENT_NAME_CONFLICT');
|
|
82
|
+
this.name = 'DurableAgentNameConflictError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class ClaudePrintError extends DurableAgentError {
|
|
87
|
+
constructor(message: string, code = 'CLAUDE_PRINT_FAILED') {
|
|
88
|
+
super(message, code);
|
|
89
|
+
this.name = 'ClaudePrintError';
|
|
90
|
+
}
|
|
91
|
+
}
|