@ai-devkit/agent-manager 0.28.1 → 0.30.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.
Files changed (219) hide show
  1. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
  2. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  3. package/dist/__tests__/capacity/codex.test.js +343 -0
  4. package/dist/__tests__/capacity/codex.test.js.map +1 -0
  5. package/dist/__tests__/capacity/index.test.js +50 -0
  6. package/dist/__tests__/capacity/index.test.js.map +1 -0
  7. package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
  8. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
  9. package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
  10. package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
  11. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
  12. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
  13. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
  14. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
  15. package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
  16. package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
  17. package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
  18. package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
  19. package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
  20. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
  21. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +33 -27
  22. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  23. package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
  24. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  25. package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
  26. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  27. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
  28. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
  29. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
  30. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
  31. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
  32. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
  33. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
  34. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
  35. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
  36. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
  37. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
  38. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
  39. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
  40. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
  41. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
  42. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
  43. package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
  44. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
  45. package/dist/adapters/index.d.ts +1 -1
  46. package/dist/adapters/index.d.ts.map +1 -1
  47. package/dist/adapters/index.js +1 -1
  48. package/dist/adapters/index.js.map +1 -1
  49. package/dist/capacity/codex.d.ts +36 -0
  50. package/dist/capacity/codex.d.ts.map +1 -0
  51. package/dist/capacity/codex.js +317 -0
  52. package/dist/capacity/codex.js.map +1 -0
  53. package/dist/capacity/index.d.ts +11 -0
  54. package/dist/capacity/index.d.ts.map +1 -0
  55. package/dist/capacity/index.js +49 -0
  56. package/dist/capacity/index.js.map +1 -0
  57. package/dist/capacity/types.d.ts +17 -0
  58. package/dist/capacity/types.d.ts.map +1 -0
  59. package/dist/capacity/types.js +3 -0
  60. package/dist/capacity/types.js.map +1 -0
  61. package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
  62. package/dist/durable/DurableAgent.d.ts +23 -3
  63. package/dist/durable/DurableAgent.d.ts.map +1 -1
  64. package/dist/durable/DurableAgent.js +12 -0
  65. package/dist/durable/DurableAgent.js.map +1 -1
  66. package/dist/durable/DurableAgentRepository.d.ts +6 -7
  67. package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
  68. package/dist/durable/DurableAgentRepository.js +73 -42
  69. package/dist/durable/DurableAgentRepository.js.map +1 -1
  70. package/dist/durable/process.d.ts +8 -0
  71. package/dist/durable/process.d.ts.map +1 -0
  72. package/dist/durable/process.js +40 -0
  73. package/dist/durable/process.js.map +1 -0
  74. package/dist/durable/run.d.ts +32 -0
  75. package/dist/durable/run.d.ts.map +1 -0
  76. package/dist/durable/run.js +46 -0
  77. package/dist/durable/run.js.map +1 -0
  78. package/dist/durable/utils.d.ts +19 -0
  79. package/dist/durable/utils.d.ts.map +1 -0
  80. package/dist/durable/utils.js +52 -0
  81. package/dist/durable/utils.js.map +1 -0
  82. package/dist/index.d.ts +19 -13
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +10 -8
  85. package/dist/index.js.map +1 -1
  86. package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
  87. package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
  88. package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
  89. package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
  90. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
  91. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
  92. package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
  93. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
  94. package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
  95. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
  96. package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
  97. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
  98. package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
  99. package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
  100. package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
  101. package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
  102. package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
  103. package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
  104. package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
  105. package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
  106. package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
  107. package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
  108. package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
  109. package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
  110. package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
  111. package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
  112. package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
  113. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
  114. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
  115. package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
  116. package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
  117. package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
  118. package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
  119. package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
  120. package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
  121. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
  122. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
  123. package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
  124. package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
  125. package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
  126. package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
  127. package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
  128. package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
  129. package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
  130. package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
  131. package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
  132. package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
  133. package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
  134. package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
  135. package/dist/providers/pi/durable/PiCliProbe.js +51 -0
  136. package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
  137. package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
  138. package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
  139. package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
  140. package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
  141. package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
  142. package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
  143. package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
  144. package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
  145. package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
  146. package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
  147. package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
  148. package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
  149. package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
  150. package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
  151. package/dist/providers/pi/durable/PiStreamParser.js +71 -0
  152. package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
  153. package/package.json +1 -1
  154. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
  155. package/src/__tests__/capacity/codex.test.ts +184 -0
  156. package/src/__tests__/capacity/index.test.ts +38 -0
  157. package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
  158. package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
  159. package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
  160. package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
  161. package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
  162. package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
  163. package/src/__tests__/fixtures/fake-codex.cjs +45 -0
  164. package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
  165. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +19 -22
  166. package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
  167. package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
  168. package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
  169. package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
  170. package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
  171. package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
  172. package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
  173. package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
  174. package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
  175. package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
  176. package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
  177. package/src/adapters/index.ts +1 -1
  178. package/src/capacity/codex.ts +320 -0
  179. package/src/capacity/index.ts +57 -0
  180. package/src/capacity/types.ts +18 -0
  181. package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
  182. package/src/durable/DurableAgent.ts +51 -3
  183. package/src/durable/DurableAgentRepository.ts +60 -31
  184. package/src/durable/process.ts +28 -0
  185. package/src/durable/run.ts +74 -0
  186. package/src/durable/utils.ts +61 -0
  187. package/src/index.ts +37 -15
  188. package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
  189. package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
  190. package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
  191. package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
  192. package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
  193. package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
  194. package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
  195. package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
  196. package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
  197. package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
  198. package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
  199. package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
  200. package/src/providers/pi/durable/PiCliProbe.ts +48 -0
  201. package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
  202. package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
  203. package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
  204. package/src/providers/pi/durable/PiStreamParser.ts +84 -0
  205. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
  206. package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
  207. package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
  208. package/dist/durable/ClaudeCliProbe.js.map +0 -1
  209. package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
  210. package/dist/durable/ClaudePrintAgentService.js +0 -66
  211. package/dist/durable/ClaudePrintAgentService.js.map +0 -1
  212. package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
  213. package/dist/durable/ClaudePrintRunner.js +0 -128
  214. package/dist/durable/ClaudePrintRunner.js.map +0 -1
  215. package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
  216. package/dist/utils/ClaudeSessionParser.js.map +0 -1
  217. package/src/durable/ClaudePrintAgentService.ts +0 -94
  218. package/src/durable/ClaudePrintRunner.ts +0 -139
  219. /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
@@ -0,0 +1,92 @@
1
+ import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'child_process';
2
+ import type { ClaudeDurableAgent, ProcessIdentity } from '../../../durable/DurableAgent.js';
3
+ import { ClaudePrintError } from '../../../durable/DurableAgent.js';
4
+ import { LocalProcessInspector, type ProcessInspector } from '../../../durable/process.js';
5
+ import { waitForChildClose } from '../../../durable/utils.js';
6
+ import { ClaudeStreamParser } from './ClaudeStreamParser.js';
7
+
8
+ type Spawn = (
9
+ command: string,
10
+ args: readonly string[],
11
+ options: SpawnOptionsWithoutStdio & { stdio: ['pipe', 'pipe', 'pipe'] },
12
+ ) => ChildProcessWithoutNullStreams;
13
+
14
+ export interface ClaudePrintRunRequest {
15
+ agent: ClaudeDurableAgent;
16
+ prompt: string;
17
+ executable?: string;
18
+ firstRun: boolean;
19
+ onSpawn(identity: ProcessIdentity): Promise<void>;
20
+ }
21
+
22
+ export interface ClaudePrintRunResult {
23
+ sessionId: string;
24
+ result: string;
25
+ exitCode: number;
26
+ }
27
+
28
+ export interface ClaudePrintRunnerOptions {
29
+ spawn?: Spawn;
30
+ processInspector?: ProcessInspector;
31
+ maxLineBytes?: number;
32
+ }
33
+
34
+ export class ClaudePrintRunner {
35
+ private readonly spawn: Spawn;
36
+ private readonly processInspector: ProcessInspector;
37
+ private readonly maxLineBytes: number;
38
+
39
+ constructor(options: ClaudePrintRunnerOptions = {}) {
40
+ this.spawn = options.spawn ?? (nodeSpawn as Spawn);
41
+ this.processInspector = options.processInspector ?? new LocalProcessInspector();
42
+ this.maxLineBytes = options.maxLineBytes ?? 1024 * 1024;
43
+ }
44
+
45
+ async run(request: ClaudePrintRunRequest): Promise<ClaudePrintRunResult> {
46
+ const sessionArgs = request.firstRun
47
+ ? ['--session-id', request.agent.providerSessionId]
48
+ : ['--resume', request.agent.providerSessionId];
49
+ const args = ['-p', ...sessionArgs, '--output-format', 'stream-json', '--verbose'];
50
+ const child = this.spawn(request.executable ?? 'claude', args, {
51
+ cwd: request.agent.cwd,
52
+ shell: false,
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ });
55
+ if (!child.pid) {
56
+ child.kill();
57
+ throw new ClaudePrintError('Claude process did not provide a PID.', 'CLAUDE_PROCESS_IDENTITY');
58
+ }
59
+ const identity = this.processInspector.getIdentity(child.pid);
60
+ if (!identity) {
61
+ child.kill();
62
+ throw new ClaudePrintError('Cannot verify Claude process identity.', 'CLAUDE_PROCESS_IDENTITY');
63
+ }
64
+
65
+ const parser = new ClaudeStreamParser(request.agent.providerSessionId, this.maxLineBytes);
66
+
67
+ child.stdout.on('data', (chunk: Buffer | string) => {
68
+ if (parser.hasFailed()) return;
69
+ try {
70
+ parser.accept(chunk);
71
+ } catch (error) {
72
+ parser.fail(error);
73
+ }
74
+ });
75
+ // Drain provider diagnostics without reflecting potentially sensitive prompt/tool data.
76
+ child.stderr.resume();
77
+
78
+ const closed = waitForChildClose(child);
79
+
80
+ try {
81
+ await request.onSpawn(identity);
82
+ } catch (error) {
83
+ child.kill();
84
+ throw error;
85
+ }
86
+
87
+ child.stdin.end(request.prompt);
88
+ const { code, signal } = await closed;
89
+
90
+ return parser.result({ code, signal });
91
+ }
92
+ }
@@ -0,0 +1,79 @@
1
+ import { ClaudePrintError } from '../../../durable/DurableAgent.js';
2
+ import { LineBuffer, type ChildCloseResult } from '../../../durable/utils.js';
3
+ import type { ClaudePrintRunResult } from './ClaudePrintRunner.js';
4
+
5
+ export class ClaudeStreamParser {
6
+ private readonly lines: LineBuffer;
7
+ private terminal: ClaudePrintRunResult | null = null;
8
+ private protocolError: ClaudePrintError | null = null;
9
+
10
+ constructor(
11
+ private readonly expectedSessionId: string,
12
+ maxLineBytes: number,
13
+ ) {
14
+ this.lines = new LineBuffer(
15
+ maxLineBytes,
16
+ () => new ClaudePrintError('Claude stream line exceeded the safety limit.', 'CLAUDE_STREAM_OVERSIZED'),
17
+ );
18
+ }
19
+
20
+ hasFailed(): boolean {
21
+ return this.protocolError !== null;
22
+ }
23
+
24
+ accept(chunk: Buffer | string): void {
25
+ for (const line of this.lines.accept(chunk)) {
26
+ this.processLine(line);
27
+ }
28
+ }
29
+
30
+ fail(error: unknown): void {
31
+ this.protocolError = error instanceof ClaudePrintError
32
+ ? error
33
+ : new ClaudePrintError('Claude stream processing failed.', 'CLAUDE_STREAM_INVALID');
34
+ }
35
+
36
+ result(close: ChildCloseResult): ClaudePrintRunResult {
37
+ if (this.protocolError) throw this.protocolError;
38
+ this.lines.assertComplete(() => new ClaudePrintError('Claude stream ended with incomplete JSON.', 'CLAUDE_STREAM_INVALID'));
39
+ if (close.code !== 0) {
40
+ throw new ClaudePrintError(
41
+ `Claude print run failed${close.signal ? ` (${close.signal})` : '.'}`,
42
+ 'CLAUDE_PROCESS_FAILED',
43
+ );
44
+ }
45
+ if (!this.terminal) throw new ClaudePrintError('Claude stream ended without a terminal result.', 'CLAUDE_RESULT_MISSING');
46
+ return { sessionId: this.terminal.sessionId, result: this.terminal.result, exitCode: close.code };
47
+ }
48
+
49
+ private processLine(line: Buffer): void {
50
+ if (line.length === 0) return;
51
+
52
+ const event = this.parseEvent(line);
53
+ if (typeof event.session_id === 'string' && event.session_id !== this.expectedSessionId) {
54
+ throw new ClaudePrintError('Claude returned a different session identity.', 'CLAUDE_SESSION_MISMATCH');
55
+ }
56
+ if (event.type === 'result') this.recordTerminalResult(event);
57
+ }
58
+
59
+ private parseEvent(line: Buffer): Record<string, unknown> {
60
+ let value: unknown;
61
+ try {
62
+ value = JSON.parse(line.toString('utf8'));
63
+ } catch {
64
+ throw new ClaudePrintError('Claude emitted malformed stream JSON.', 'CLAUDE_STREAM_INVALID');
65
+ }
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
67
+ throw new ClaudePrintError('Claude emitted a non-object stream message.', 'CLAUDE_STREAM_INVALID');
68
+ }
69
+ return value as Record<string, unknown>;
70
+ }
71
+
72
+ private recordTerminalResult(event: Record<string, unknown>): void {
73
+ if (this.terminal) throw new ClaudePrintError('Claude emitted more than one terminal result.', 'CLAUDE_STREAM_INVALID');
74
+ if (typeof event.session_id !== 'string' || typeof event.result !== 'string') {
75
+ throw new ClaudePrintError('Claude emitted an invalid terminal result.', 'CLAUDE_STREAM_INVALID');
76
+ }
77
+ this.terminal = { sessionId: event.session_id, result: event.result, exitCode: 0 };
78
+ }
79
+ }
@@ -0,0 +1,60 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { CodexPrintError } from '../../../durable/DurableAgent.js';
4
+ import { sanitizeText } from '../../../durable/utils.js';
5
+
6
+ type ExecResult = { stdout: string; stderr: string };
7
+ type Exec = (file: string, args: string[]) => Promise<ExecResult>;
8
+
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ export interface CodexCliProbeOptions {
12
+ executable?: string;
13
+ exec?: Exec;
14
+ }
15
+
16
+ export class CodexCliProbe {
17
+ private readonly executable: string;
18
+ private readonly exec: Exec;
19
+
20
+ constructor(options: CodexCliProbeOptions = {}) {
21
+ this.executable = options.executable ?? 'codex';
22
+ this.exec = options.exec ?? (async (file, args) => {
23
+ const result = await execFileAsync(file, args, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
24
+ return { stdout: result.stdout, stderr: result.stderr };
25
+ });
26
+ }
27
+
28
+ async validate(): Promise<{ executable: string; version: string }> {
29
+ try {
30
+ const version = await this.exec(this.executable, ['--version']);
31
+ const execHelp = await this.exec(this.executable, ['exec', '--help']);
32
+ const resumeHelp = await this.exec(this.executable, ['exec', 'resume', '--help']);
33
+ const missing = [
34
+ !execHelp.stdout.includes('exec') && 'exec',
35
+ !execHelp.stdout.includes('--json') && '--json',
36
+ !hasStdinDash(execHelp.stdout) && 'stdin -',
37
+ !resumeHelp.stdout.includes('resume') && 'resume',
38
+ !resumeHelp.stdout.includes('--json') && 'resume --json',
39
+ !hasStdinDash(resumeHelp.stdout) && 'resume stdin -',
40
+ ].filter((value): value is string => typeof value === 'string');
41
+ if (missing.length > 0) {
42
+ throw new CodexPrintError(
43
+ `Codex CLI does not support required print-mode capabilities: ${missing.join(', ')}.`,
44
+ 'CODEX_CLI_UNSUPPORTED',
45
+ );
46
+ }
47
+ return { executable: this.executable, version: sanitizeText(version.stdout, 256) || 'unknown' };
48
+ } catch (error) {
49
+ if (error instanceof CodexPrintError) throw error;
50
+ throw new CodexPrintError(
51
+ `Codex CLI validation failed: ${sanitizeText((error as Error).message, 512)}`,
52
+ 'CODEX_CLI_UNAVAILABLE',
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ function hasStdinDash(help: string): boolean {
59
+ return /(?:^|\s)-(?:\s|$)/m.test(help);
60
+ }
@@ -0,0 +1,65 @@
1
+ import type { DurableAgent, ProcessIdentity } from '../../../durable/DurableAgent.js';
2
+ import { CodexPrintError } from '../../../durable/DurableAgent.js';
3
+ import { CodexCliProbe } from './CodexCliProbe.js';
4
+ import { CodexPrintRunner, type CodexPrintRunResult } from './CodexPrintRunner.js';
5
+ import { DurableAgentRepository, type CreateDurableAgentInput } from '../../../durable/DurableAgentRepository.js';
6
+ import { runDurableAgent, type DurableRunRepository } from '../../../durable/run.js';
7
+
8
+ interface RepositoryLike extends DurableRunRepository {
9
+ create(input: CreateDurableAgentInput): Promise<DurableAgent>;
10
+ list(): Promise<DurableAgent[]>;
11
+ recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
12
+ bindProviderSession(id: string, token: string, providerSessionId: string): Promise<DurableAgent>;
13
+ }
14
+
15
+ interface ProbeLike { validate(): Promise<{ executable: string; version: string }> }
16
+ interface RunnerLike { run(request: Parameters<CodexPrintRunner['run']>[0]): Promise<CodexPrintRunResult> }
17
+
18
+ export interface CodexPrintAgentServiceOptions {
19
+ repository?: RepositoryLike;
20
+ probe?: ProbeLike;
21
+ runner?: RunnerLike;
22
+ executable?: string;
23
+ }
24
+
25
+ export interface CodexPrintSendResult extends CodexPrintRunResult {
26
+ agentId: string;
27
+ agentName: string;
28
+ }
29
+
30
+ export class CodexPrintAgentService {
31
+ readonly repository: RepositoryLike;
32
+ private readonly probe: ProbeLike;
33
+ private readonly runner: RunnerLike;
34
+ private readonly executable?: string;
35
+
36
+ constructor(options: CodexPrintAgentServiceOptions = {}) {
37
+ this.repository = options.repository ?? new DurableAgentRepository();
38
+ this.probe = options.probe ?? new CodexCliProbe();
39
+ this.runner = options.runner ?? new CodexPrintRunner();
40
+ this.executable = options.executable;
41
+ }
42
+
43
+ async create(input: Omit<CreateDurableAgentInput, 'provider'>): Promise<DurableAgent> {
44
+ await this.probe.validate();
45
+ return this.repository.create({ ...input, provider: 'codex' });
46
+ }
47
+
48
+ async send(reference: string, prompt: string): Promise<CodexPrintSendResult> {
49
+ const completed = await runDurableAgent({
50
+ reference, provider: 'codex', repository: this.repository,
51
+ ambiguousError: () => new CodexPrintError('Multiple print agents match.', 'CODEX_UNSUPPORTED'),
52
+ providerError: () => new CodexPrintError('Print agent provider is not Codex.', 'CODEX_UNSUPPORTED'),
53
+ execute: (agent, token) => this.runner.run({
54
+ agent,
55
+ prompt,
56
+ executable: this.executable,
57
+ onSpawn: (identity) => this.repository.recordProviderProcess(agent.id, token, identity),
58
+ onSession: async (sessionId) => { await this.repository.bindProviderSession(agent.id, token, sessionId); },
59
+ }),
60
+ isSessionMismatch: (error) => error instanceof CodexPrintError
61
+ && error.code === 'CODEX_SESSION_MISMATCH',
62
+ });
63
+ return { ...completed.result, agentId: completed.agent.id, agentName: completed.agent.name };
64
+ }
65
+ }
@@ -0,0 +1,94 @@
1
+ import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'child_process';
2
+ import type { CodexDurableAgent, ProcessIdentity } from '../../../durable/DurableAgent.js';
3
+ import { CodexPrintError } from '../../../durable/DurableAgent.js';
4
+ import { LocalProcessInspector, type ProcessInspector } from '../../../durable/process.js';
5
+ import { waitForChildClose } from '../../../durable/utils.js';
6
+ import { CodexStreamParser } from './CodexStreamParser.js';
7
+
8
+ type Spawn = (
9
+ command: string,
10
+ args: readonly string[],
11
+ options: SpawnOptionsWithoutStdio & { stdio: ['pipe', 'pipe', 'pipe'] },
12
+ ) => ChildProcessWithoutNullStreams;
13
+
14
+ export interface CodexPrintRunRequest {
15
+ agent: CodexDurableAgent;
16
+ prompt: string;
17
+ executable?: string;
18
+ onSpawn(identity: ProcessIdentity): Promise<void>;
19
+ onSession(providerSessionId: string): Promise<void>;
20
+ }
21
+
22
+ export interface CodexPrintRunResult {
23
+ sessionId: string;
24
+ result: string;
25
+ messages: string[];
26
+ exitCode: number;
27
+ }
28
+
29
+ export interface CodexPrintRunnerOptions {
30
+ spawn?: Spawn;
31
+ processInspector?: ProcessInspector;
32
+ maxLineBytes?: number;
33
+ }
34
+
35
+ export class CodexPrintRunner {
36
+ private readonly spawn: Spawn;
37
+ private readonly processInspector: ProcessInspector;
38
+ private readonly maxLineBytes: number;
39
+
40
+ constructor(options: CodexPrintRunnerOptions = {}) {
41
+ this.spawn = options.spawn ?? (nodeSpawn as Spawn);
42
+ this.processInspector = options.processInspector ?? new LocalProcessInspector();
43
+ this.maxLineBytes = options.maxLineBytes ?? 1024 * 1024;
44
+ }
45
+
46
+ async run(request: CodexPrintRunRequest): Promise<CodexPrintRunResult> {
47
+ const args = buildCodexExecArgs(request.agent);
48
+ const child = this.spawn(request.executable ?? 'codex', args, {
49
+ cwd: request.agent.cwd, shell: false, stdio: ['pipe', 'pipe', 'pipe'],
50
+ });
51
+ const identity = this.verifyProcessIdentity(child);
52
+
53
+ const parser = new CodexStreamParser(request.agent.providerSessionId, request.onSession, this.maxLineBytes);
54
+ let processing = Promise.resolve();
55
+
56
+ child.stdout.on('data', (chunk: Buffer | string) => {
57
+ if (parser.hasFailed()) return;
58
+ processing = processing.then(() => parser.accept(chunk)).catch((error) => parser.fail(error));
59
+ });
60
+ child.stderr.resume();
61
+ const closed = waitForChildClose(child);
62
+ try {
63
+ await request.onSpawn(identity);
64
+ } catch (error) {
65
+ child.kill();
66
+ throw error;
67
+ }
68
+ child.stdin.end(request.prompt);
69
+ const { code, signal } = await closed.catch(() => {
70
+ throw new CodexPrintError('Codex process failed to start or communicate.', 'CODEX_PROCESS');
71
+ });
72
+ await processing;
73
+ return parser.result({ code, signal });
74
+ }
75
+
76
+ private verifyProcessIdentity(child: ChildProcessWithoutNullStreams): ProcessIdentity {
77
+ if (!child.pid) {
78
+ child.kill();
79
+ throw new CodexPrintError('Codex process did not provide a PID.', 'CODEX_PROCESS');
80
+ }
81
+ const identity = this.processInspector.getIdentity(child.pid);
82
+ if (!identity) {
83
+ child.kill();
84
+ throw new CodexPrintError('Cannot verify Codex process identity.', 'CODEX_PROCESS');
85
+ }
86
+ return identity;
87
+ }
88
+ }
89
+
90
+ function buildCodexExecArgs(agent: CodexDurableAgent): string[] {
91
+ return agent.providerSessionId === null
92
+ ? ['exec', '--json', '-']
93
+ : ['exec', 'resume', '--json', agent.providerSessionId, '-'];
94
+ }
@@ -0,0 +1,104 @@
1
+ import { CodexPrintError } from '../../../durable/DurableAgent.js';
2
+ import { isUuid, LineBuffer, type ChildCloseResult } from '../../../durable/utils.js';
3
+ import type { CodexPrintRunResult } from './CodexPrintRunner.js';
4
+
5
+ export class CodexStreamParser {
6
+ private readonly lines: LineBuffer;
7
+ private sessionId: string | null = null;
8
+ private turnCompleted = false;
9
+ private readonly messages: string[] = [];
10
+ private protocolError: CodexPrintError | null = null;
11
+
12
+ constructor(
13
+ private readonly expectedSessionId: string | null,
14
+ private readonly onSession: (providerSessionId: string) => Promise<void>,
15
+ maxLineBytes: number,
16
+ ) {
17
+ this.lines = new LineBuffer(
18
+ maxLineBytes,
19
+ () => new CodexPrintError('Codex stream line exceeded the safety limit.', 'CODEX_PROTOCOL'),
20
+ );
21
+ }
22
+
23
+ hasFailed(): boolean {
24
+ return this.protocolError !== null;
25
+ }
26
+
27
+ async accept(chunk: Buffer | string): Promise<void> {
28
+ for (const line of this.lines.accept(chunk)) {
29
+ await this.processLine(line);
30
+ }
31
+ }
32
+
33
+ fail(error: unknown): void {
34
+ this.protocolError = error instanceof CodexPrintError
35
+ ? error
36
+ : new CodexPrintError('Codex stream processing failed.', 'CODEX_PROTOCOL');
37
+ }
38
+
39
+ result(close: ChildCloseResult): CodexPrintRunResult {
40
+ if (this.protocolError) throw this.protocolError;
41
+ this.lines.assertComplete(
42
+ () => new CodexPrintError('Codex stream ended with incomplete JSON.', 'CODEX_PROTOCOL'),
43
+ );
44
+
45
+ if (close.code !== 0) {
46
+ throw new CodexPrintError(`Codex print run failed${close.signal ? ` (${close.signal})` : '.'}`, 'CODEX_PROCESS');
47
+ }
48
+ if (this.sessionId === null) throw new CodexPrintError('Codex stream ended without a thread identity.', 'CODEX_PROTOCOL');
49
+ if (!this.turnCompleted) throw new CodexPrintError('Codex stream ended before turn completion.', 'CODEX_PROTOCOL');
50
+ if (this.messages.length === 0) throw new CodexPrintError('Codex stream ended without an assistant result.', 'CODEX_RESULT_MISSING');
51
+ return {
52
+ sessionId: this.sessionId,
53
+ result: this.messages.at(-1)!,
54
+ messages: this.messages,
55
+ exitCode: close.code,
56
+ };
57
+ }
58
+
59
+ private async processLine(line: Buffer): Promise<void> {
60
+ if (this.protocolError || line.length === 0) return;
61
+
62
+ const event = this.parseEvent(line);
63
+ if (event.type === 'thread.started') {
64
+ await this.recordSession(event);
65
+ } else if (event.type === 'item.completed') {
66
+ this.recordAssistantMessage(event.item);
67
+ } else if (event.type === 'turn.completed') {
68
+ this.turnCompleted = true;
69
+ }
70
+ }
71
+
72
+ private parseEvent(line: Buffer): Record<string, unknown> {
73
+ let value: unknown;
74
+ try {
75
+ value = JSON.parse(line.toString('utf8'));
76
+ } catch {
77
+ throw new CodexPrintError('Codex emitted malformed stream JSON.', 'CODEX_PROTOCOL');
78
+ }
79
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
80
+ throw new CodexPrintError('Codex emitted a non-object stream message.', 'CODEX_PROTOCOL');
81
+ }
82
+ return value as Record<string, unknown>;
83
+ }
84
+
85
+ private async recordSession(event: Record<string, unknown>): Promise<void> {
86
+ const sessionId = String(event.thread_id ?? '');
87
+ if (this.sessionId !== null || !isUuid(sessionId)) {
88
+ throw new CodexPrintError('Codex emitted an invalid thread identity.', 'CODEX_PROTOCOL');
89
+ }
90
+ if (this.expectedSessionId !== null && this.expectedSessionId !== sessionId) {
91
+ throw new CodexPrintError('Codex returned a different session identity.', 'CODEX_SESSION_MISMATCH');
92
+ }
93
+ this.sessionId = sessionId;
94
+ await this.onSession(sessionId);
95
+ }
96
+
97
+ private recordAssistantMessage(item: unknown): void {
98
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return;
99
+ const record = item as Record<string, unknown>;
100
+ if (record.type === 'agent_message' && typeof record.text === 'string' && record.text.trim()) {
101
+ this.messages.push(record.text);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,48 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { PiPrintError } from '../../../durable/DurableAgent.js';
4
+ import { sanitizeText } from '../../../durable/utils.js';
5
+
6
+ type ExecResult = { stdout: string; stderr: string };
7
+ type Exec = (file: string, args: string[]) => Promise<ExecResult>;
8
+ const execFileAsync = promisify(execFile);
9
+ const REQUIRED = ['--mode', 'json', '--session-id', '--session'];
10
+
11
+ export interface PiCliProbeOptions {
12
+ executable?: string;
13
+ exec?: Exec;
14
+ }
15
+
16
+ export class PiCliProbe {
17
+ private readonly executable: string;
18
+ private readonly exec: Exec;
19
+
20
+ constructor(options: PiCliProbeOptions = {}) {
21
+ this.executable = options.executable ?? 'pi';
22
+ this.exec = options.exec ?? (async (file, args) => {
23
+ const result = await execFileAsync(file, args, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
24
+ return { stdout: result.stdout, stderr: result.stderr };
25
+ });
26
+ }
27
+
28
+ async validate(): Promise<{ executable: string; version: string }> {
29
+ try {
30
+ const version = await this.exec(this.executable, ['--version']);
31
+ const help = await this.exec(this.executable, ['--help']);
32
+ const missing = REQUIRED.filter((capability) => !help.stdout.includes(capability));
33
+ if (missing.length > 0) {
34
+ throw new PiPrintError(
35
+ `Pi CLI does not support required print-mode capabilities: ${missing.join(', ')}.`,
36
+ 'PI_CLI_UNSUPPORTED',
37
+ );
38
+ }
39
+ return { executable: this.executable, version: sanitizeText(version.stdout, 256) || 'unknown' };
40
+ } catch (error) {
41
+ if (error instanceof PiPrintError) throw error;
42
+ throw new PiPrintError(
43
+ `Pi CLI validation failed: ${sanitizeText((error as Error).message, 512)}`,
44
+ 'PI_CLI_UNAVAILABLE',
45
+ );
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,69 @@
1
+ import type { DurableAgent, ProcessIdentity } from '../../../durable/DurableAgent.js';
2
+ import { PiPrintError } from '../../../durable/DurableAgent.js';
3
+ import { PiCliProbe } from './PiCliProbe.js';
4
+ import { PiPrintRunner, type PiPrintRunResult } from './PiPrintRunner.js';
5
+ import { DurableAgentRepository, type CreateDurableAgentInput } from '../../../durable/DurableAgentRepository.js';
6
+ import { runDurableAgent, type DurableRunRepository } from '../../../durable/run.js';
7
+
8
+ interface RepositoryLike extends DurableRunRepository {
9
+ create(input: CreateDurableAgentInput): Promise<DurableAgent>;
10
+ list(): Promise<DurableAgent[]>;
11
+ recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
12
+ }
13
+
14
+ interface ProbeLike { validate(): Promise<{ executable: string; version: string }> }
15
+ interface RunnerLike { run(request: Parameters<PiPrintRunner['run']>[0]): Promise<PiPrintRunResult> }
16
+
17
+ export interface PiPrintAgentServiceOptions {
18
+ repository?: RepositoryLike;
19
+ probe?: ProbeLike;
20
+ runner?: RunnerLike;
21
+ executable?: string;
22
+ }
23
+
24
+ export interface PiPrintSendResult extends PiPrintRunResult {
25
+ agentId: string;
26
+ agentName: string;
27
+ }
28
+
29
+ export class PiPrintAgentService {
30
+ readonly repository: RepositoryLike;
31
+ private readonly probe: ProbeLike;
32
+ private readonly runner: RunnerLike;
33
+ private readonly executable?: string;
34
+
35
+ constructor(options: PiPrintAgentServiceOptions = {}) {
36
+ this.repository = options.repository ?? new DurableAgentRepository();
37
+ this.probe = options.probe ?? new PiCliProbe();
38
+ this.runner = options.runner ?? new PiPrintRunner();
39
+ this.executable = options.executable;
40
+ }
41
+
42
+ async create(input: Omit<CreateDurableAgentInput, 'provider'>): Promise<DurableAgent> {
43
+ await this.probe.validate();
44
+ return this.repository.create({ ...input, provider: 'pi' });
45
+ }
46
+
47
+ async send(reference: string, prompt: string): Promise<PiPrintSendResult> {
48
+ const completed = await runDurableAgent({
49
+ reference,
50
+ provider: 'pi',
51
+ repository: this.repository,
52
+ ambiguousError: () => new PiPrintError('Multiple print agents match.', 'PI_UNSUPPORTED'),
53
+ providerError: () => new PiPrintError('Print agent provider is not Pi.', 'PI_UNSUPPORTED'),
54
+ execute: (agent, token) => this.runner.run({
55
+ agent,
56
+ prompt,
57
+ executable: this.executable,
58
+ onSpawn: (identity) => this.repository.recordProviderProcess(agent.id, token, identity),
59
+ }),
60
+ isSessionMismatch: (error) => error instanceof PiPrintError
61
+ && error.code === 'PI_SESSION_MISMATCH',
62
+ });
63
+ return {
64
+ ...completed.result,
65
+ agentId: completed.agent.id,
66
+ agentName: completed.agent.name,
67
+ };
68
+ }
69
+ }
@@ -0,0 +1,39 @@
1
+ import { PiPrintError } from '../../../durable/DurableAgent.js';
2
+ import { isUuid } from '../../../durable/utils.js';
3
+
4
+ export function buildPiPrintArgs(providerSessionId: string, firstRun: boolean): string[] {
5
+ return firstRun
6
+ ? ['--mode', 'json', '--session-id', providerSessionId]
7
+ : ['--mode', 'json', '--session', providerSessionId];
8
+ }
9
+
10
+ export function readPiSessionId(
11
+ event: Record<string, unknown>,
12
+ currentSessionId: string | null,
13
+ expectedSessionId: string,
14
+ ): string {
15
+ if (currentSessionId !== null || typeof event.id !== 'string' || !isUuid(event.id)) {
16
+ throw new PiPrintError('Pi emitted an invalid session identity.', 'PI_PROTOCOL');
17
+ }
18
+ const sessionId = event.id;
19
+ if (expectedSessionId !== sessionId) {
20
+ throw new PiPrintError('Pi returned a different session identity.', 'PI_SESSION_MISMATCH');
21
+ }
22
+ return sessionId;
23
+ }
24
+
25
+ export function readPiAssistantText(event: Record<string, unknown>): string | null {
26
+ if (event.type !== 'message_end') return null;
27
+ const message = event.message;
28
+ if (!message || typeof message !== 'object' || Array.isArray(message)) return null;
29
+ const record = message as Record<string, unknown>;
30
+ if (record.role !== 'assistant') return null;
31
+ if (typeof record.content === 'string') return record.content.trim() ? record.content : null;
32
+ if (!Array.isArray(record.content)) return null;
33
+ const text = record.content.flatMap((part) => {
34
+ if (!part || typeof part !== 'object' || Array.isArray(part)) return [];
35
+ const block = part as Record<string, unknown>;
36
+ return block.type === 'text' && typeof block.text === 'string' ? [block.text] : [];
37
+ }).join('');
38
+ return text.trim() ? text : null;
39
+ }