@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.
Files changed (87) hide show
  1. package/README.md +12 -4
  2. package/dist/__tests__/adapters/CodexAdapter.test.js +199 -0
  3. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  4. package/dist/__tests__/database/DurableAgentsDatabase.test.js +100 -0
  5. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -0
  6. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +7 -7
  7. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  8. package/dist/__tests__/print/ClaudePrintAgentService.test.js +7 -7
  9. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  10. package/dist/__tests__/print/ClaudePrintRunner.test.js +1 -1
  11. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  12. package/dist/__tests__/print/DurableAgent.test.js +17 -0
  13. package/dist/__tests__/print/DurableAgent.test.js.map +1 -0
  14. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js +186 -0
  15. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js.map +1 -0
  16. package/dist/__tests__/print/{PrintAgentStore.test.js → DurableAgentRepository.test.js} +63 -110
  17. package/dist/__tests__/print/DurableAgentRepository.test.js.map +1 -0
  18. package/dist/adapters/CodexAdapter.d.ts +1 -0
  19. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  20. package/dist/adapters/CodexAdapter.js +16 -6
  21. package/dist/adapters/CodexAdapter.js.map +1 -1
  22. package/dist/database/connection.d.ts.map +1 -1
  23. package/dist/database/connection.js +17 -9
  24. package/dist/database/connection.js.map +1 -1
  25. package/dist/database/migrations/003_durable_agents.sql +43 -0
  26. package/dist/durable/ClaudeCliProbe.d.ts.map +1 -0
  27. package/dist/{print → durable}/ClaudeCliProbe.js +1 -1
  28. package/dist/durable/ClaudeCliProbe.js.map +1 -0
  29. package/dist/{print → durable}/ClaudePrintAgentService.d.ts +11 -11
  30. package/dist/durable/ClaudePrintAgentService.d.ts.map +1 -0
  31. package/dist/{print → durable}/ClaudePrintAgentService.js +12 -12
  32. package/dist/durable/ClaudePrintAgentService.js.map +1 -0
  33. package/dist/{print → durable}/ClaudePrintRunner.d.ts +3 -3
  34. package/dist/durable/ClaudePrintRunner.d.ts.map +1 -0
  35. package/dist/{print → durable}/ClaudePrintRunner.js +2 -2
  36. package/dist/durable/ClaudePrintRunner.js.map +1 -0
  37. package/dist/durable/DurableAgent.d.ts +61 -0
  38. package/dist/durable/DurableAgent.d.ts.map +1 -0
  39. package/dist/durable/DurableAgent.js +46 -0
  40. package/dist/durable/DurableAgent.js.map +1 -0
  41. package/dist/durable/DurableAgentRepository.d.ts +60 -0
  42. package/dist/durable/DurableAgentRepository.d.ts.map +1 -0
  43. package/dist/durable/DurableAgentRepository.js +325 -0
  44. package/dist/durable/DurableAgentRepository.js.map +1 -0
  45. package/dist/index.d.ts +11 -11
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +6 -6
  48. package/dist/index.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/__tests__/adapters/CodexAdapter.test.ts +132 -0
  51. package/src/__tests__/database/DurableAgentsDatabase.test.ts +77 -0
  52. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +6 -6
  53. package/src/__tests__/print/ClaudePrintAgentService.test.ts +7 -7
  54. package/src/__tests__/print/ClaudePrintRunner.test.ts +3 -3
  55. package/src/__tests__/print/{PrintAgent.test.ts → DurableAgent.test.ts} +6 -6
  56. package/src/__tests__/print/DurableAgentRepository.sqlite.test.ts +105 -0
  57. package/src/__tests__/print/DurableAgentRepository.test.ts +163 -0
  58. package/src/adapters/CodexAdapter.ts +14 -11
  59. package/src/database/connection.ts +15 -9
  60. package/src/database/migrations/003_durable_agents.sql +43 -0
  61. package/src/{print → durable}/ClaudeCliProbe.ts +1 -1
  62. package/src/{print → durable}/ClaudePrintAgentService.ts +21 -21
  63. package/src/{print → durable}/ClaudePrintRunner.ts +4 -4
  64. package/src/durable/DurableAgent.ts +91 -0
  65. package/src/durable/DurableAgentRepository.ts +307 -0
  66. package/src/index.ts +26 -25
  67. package/dist/__tests__/print/PrintAgent.test.js +0 -17
  68. package/dist/__tests__/print/PrintAgent.test.js.map +0 -1
  69. package/dist/__tests__/print/PrintAgentStore.test.js.map +0 -1
  70. package/dist/print/ClaudeCliProbe.d.ts.map +0 -1
  71. package/dist/print/ClaudeCliProbe.js.map +0 -1
  72. package/dist/print/ClaudePrintAgentService.d.ts.map +0 -1
  73. package/dist/print/ClaudePrintAgentService.js.map +0 -1
  74. package/dist/print/ClaudePrintRunner.d.ts.map +0 -1
  75. package/dist/print/ClaudePrintRunner.js.map +0 -1
  76. package/dist/print/PrintAgent.d.ts +0 -57
  77. package/dist/print/PrintAgent.d.ts.map +0 -1
  78. package/dist/print/PrintAgent.js +0 -42
  79. package/dist/print/PrintAgent.js.map +0 -1
  80. package/dist/print/PrintAgentStore.d.ts +0 -69
  81. package/dist/print/PrintAgentStore.d.ts.map +0 -1
  82. package/dist/print/PrintAgentStore.js +0 -484
  83. package/dist/print/PrintAgentStore.js.map +0 -1
  84. package/src/__tests__/print/PrintAgentStore.test.ts +0 -192
  85. package/src/print/PrintAgent.ts +0 -86
  86. package/src/print/PrintAgentStore.ts +0 -503
  87. /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 { cachedAgents, remaining } = this.tryRegistryCache(processes);
123
- if (remaining.length === 0) return cachedAgents;
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 mappingResult = this.mapSessionMappingMatches(remaining);
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
- !firstUserMessage &&
892
- entry.payload?.type === 'user_message' &&
893
- typeof entry.payload.message === 'string' &&
894
- entry.payload.message.trim().length > 0
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
- mkdirSync(dirname(this.dbPath), { recursive: true });
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.configure();
38
- if (!this.readonly) initializeSchema(this);
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 './PrintAgent.js';
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 { PrintAgent, ProcessIdentity } from './PrintAgent.js';
2
- import { ClaudePrintError, PrintAgentNotFoundError } from './PrintAgent.js';
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 { PrintAgentStore, type CreatePrintAgentInput, type PrintRunCompletion } from './PrintAgentStore.js';
5
+ import { DurableAgentRepository, type CreateDurableAgentInput, type DurableRunCompletion } from './DurableAgentRepository.js';
6
6
 
7
- interface StoreLike {
8
- create(input: CreatePrintAgentInput): Promise<PrintAgent>;
9
- list(): Promise<PrintAgent[]>;
10
- resolve(reference: string): Promise<PrintAgent | PrintAgent[] | null>;
11
- acquireRun(id: string): Promise<{ agent: PrintAgent; token: string }>;
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: PrintRunCompletion): Promise<PrintAgent>;
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
- store?: StoreLike;
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 store: StoreLike;
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.store = options.store ?? new PrintAgentStore();
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: CreatePrintAgentInput): Promise<PrintAgent> {
44
+ async create(input: CreateDurableAgentInput): Promise<DurableAgent> {
45
45
  await this.probe.validate();
46
- return this.store.create(input);
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.store.resolve(reference);
51
- if (!resolved) throw new PrintAgentNotFoundError(reference);
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 print agents match "${reference}".`, 'PRINT_AGENT_AMBIGUOUS');
53
+ throw new ClaudePrintError(`Multiple durable agents match "${reference}".`, 'DURABLE_AGENT_AMBIGUOUS');
54
54
  }
55
- const acquired = await this.store.acquireRun(resolved.id);
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.store.recordProviderProcess(resolved.id, acquired.token, identity),
62
+ onSpawn: (identity) => this.repository.recordProviderProcess(resolved.id, acquired.token, identity),
63
63
  });
64
- await this.store.completeRun(resolved.id, acquired.token, {
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.store.completeRun(resolved.id, acquired.token, {
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 { PrintAgent, ProcessIdentity } from './PrintAgent.js';
3
- import { ClaudePrintError } from './PrintAgent.js';
4
- import { LocalProcessInspector, type ProcessInspector } from './PrintAgentStore.js';
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: PrintAgent;
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
+ }