@axiom-lattice/cli-a2a 0.1.5 → 0.1.7
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/.turbo/turbo-build.log +34 -32
- package/CHANGELOG.md +27 -0
- package/README.md +85 -2
- package/__tests__/opencode-executor.test.ts +631 -31
- package/__tests__/session-store-path.test.ts +40 -0
- package/dist/{chunk-NQIDRU47.mjs → chunk-54SKDK3D.mjs} +78 -32
- package/dist/chunk-54SKDK3D.mjs.map +1 -0
- package/dist/chunk-BSNQOP2W.mjs +350 -0
- package/dist/chunk-BSNQOP2W.mjs.map +1 -0
- package/dist/chunk-DRFFQJII.mjs +195 -0
- package/dist/chunk-DRFFQJII.mjs.map +1 -0
- package/dist/chunk-URJDRXJW.mjs +378 -0
- package/dist/chunk-URJDRXJW.mjs.map +1 -0
- package/dist/{chunk-G7AGL2QA.mjs → chunk-WZB7FBSX.mjs} +23 -5
- package/dist/chunk-WZB7FBSX.mjs.map +1 -0
- package/dist/{chunk-LXL47XMZ.mjs → chunk-YGUPSPZO.mjs} +9 -1
- package/dist/chunk-YGUPSPZO.mjs.map +1 -0
- package/dist/{chunk-35NFMGMS.mjs → chunk-ZP3JNJQJ.mjs} +3 -3
- package/dist/chunk-ZP3JNJQJ.mjs.map +1 -0
- package/dist/cli.js +769 -156
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +7 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/executor-LLE5VEFJ.mjs +12 -0
- package/dist/executor-LO7P3KUW.mjs +12 -0
- package/dist/{executor-RVBGAWUF.mjs → executor-OOLGGXHS.mjs} +4 -3
- package/dist/{executors-QIIKBUMJ.mjs → executors-AQHOPGSR.mjs} +2 -2
- package/dist/index.d.mts +92 -21
- package/dist/index.d.ts +92 -21
- package/dist/index.js +731 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -7
- package/package.json +10 -3
- package/runtime/cli-path.cjs +3 -0
- package/runtime/cli-path.d.ts +1 -0
- package/runtime/cli-path.mjs +5 -0
- package/src/config/defaults.ts +3 -1
- package/src/config/types.ts +8 -3
- package/src/executors/claude/client.ts +44 -14
- package/src/executors/claude/executor.ts +65 -30
- package/src/executors/codex/client.ts +239 -16
- package/src/executors/codex/executor.ts +70 -37
- package/src/executors/events.ts +15 -1
- package/src/executors/index.ts +13 -6
- package/src/executors/opencode/client.ts +192 -18
- package/src/executors/opencode/executor.ts +65 -30
- package/src/index.ts +8 -0
- package/src/local-state.ts +53 -0
- package/src/server/agent-card.ts +1 -0
- package/src/server/index.ts +22 -7
- package/src/session-store.ts +262 -0
- package/dist/chunk-35NFMGMS.mjs.map +0 -1
- package/dist/chunk-G7AGL2QA.mjs.map +0 -1
- package/dist/chunk-LXL47XMZ.mjs.map +0 -1
- package/dist/chunk-NQIDRU47.mjs.map +0 -1
- package/dist/chunk-VSZ3DACI.mjs +0 -179
- package/dist/chunk-VSZ3DACI.mjs.map +0 -1
- package/dist/chunk-WNCDOYZS.mjs +0 -187
- package/dist/chunk-WNCDOYZS.mjs.map +0 -1
- package/dist/executor-OWPVDUXH.mjs +0 -11
- package/dist/executor-XWHWUVQ3.mjs +0 -11
- /package/dist/{executor-OWPVDUXH.mjs.map → executor-LLE5VEFJ.mjs.map} +0 -0
- /package/dist/{executor-RVBGAWUF.mjs.map → executor-LO7P3KUW.mjs.map} +0 -0
- /package/dist/{executor-XWHWUVQ3.mjs.map → executor-OOLGGXHS.mjs.map} +0 -0
- /package/dist/{executors-QIIKBUMJ.mjs.map → executors-AQHOPGSR.mjs.map} +0 -0
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Spawns the Anthropic Claude Code CLI as a child process in non-interactive
|
|
5
5
|
* print mode (`-p`). Sends prompts, captures stdout as the response.
|
|
6
|
-
* Supports
|
|
6
|
+
* Supports session persistence: first call uses `--session-id <uuid>` to
|
|
7
|
+
* create a named session; subsequent calls use `--resume <uuid>` to continue.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
11
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
12
|
import { logger } from '../../logger.js';
|
|
11
13
|
|
|
12
14
|
const log = logger.child('claude:client');
|
|
@@ -26,6 +28,11 @@ export interface ClaudeClientConfig {
|
|
|
26
28
|
timeout?: number;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export interface ClaudeExecuteResult {
|
|
32
|
+
text: string;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
30
37
|
|
|
31
38
|
export class ClaudeClient {
|
|
@@ -46,15 +53,36 @@ export class ClaudeClient {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/**
|
|
49
|
-
* Execute a prompt via the Claude Code CLI
|
|
56
|
+
* Execute a prompt via the Claude Code CLI.
|
|
57
|
+
*
|
|
58
|
+
* @param prompt - The user prompt text.
|
|
59
|
+
* @param sessionId - If provided, resumes the session with `--resume`.
|
|
60
|
+
* Otherwise creates a new session with `--session-id <new-uuid>`.
|
|
61
|
+
* @param workingDirectory - Optional per-request working directory override.
|
|
62
|
+
* @returns The response text and the session ID (new or existing).
|
|
50
63
|
*/
|
|
51
|
-
async execute(prompt: string): Promise<
|
|
64
|
+
async execute(prompt: string, sessionId?: string, workingDirectory?: string): Promise<ClaudeExecuteResult> {
|
|
65
|
+
if (sessionId) {
|
|
66
|
+
const text = await this.run(['-p', '--resume', sessionId, prompt], workingDirectory);
|
|
67
|
+
return { text, sessionId };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const newSessionId = uuidv4();
|
|
71
|
+
const text = await this.run(['-p', '--session-id', newSessionId, prompt], workingDirectory);
|
|
72
|
+
return { text, sessionId: newSessionId };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
private async run(args: string[], workingDirectory?: string): Promise<string> {
|
|
52
78
|
const cliPath = this.config.cliPath || 'claude';
|
|
53
79
|
const timeout = this.config.timeout ?? 300_000;
|
|
54
80
|
const MAX_PROMPT = 100_000;
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
// Validate prompt size
|
|
83
|
+
const promptArg = args[args.length - 1];
|
|
84
|
+
if (promptArg && promptArg.length > MAX_PROMPT) {
|
|
85
|
+
throw new Error(`Prompt too large (${promptArg.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
// Only pass relevant env vars to child process
|
|
@@ -62,26 +90,29 @@ export class ClaudeClient {
|
|
|
62
90
|
PATH: process.env.PATH ?? '',
|
|
63
91
|
HOME: process.env.HOME ?? '',
|
|
64
92
|
};
|
|
65
|
-
// Forward common proxy/no-proxy settings
|
|
66
93
|
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
67
94
|
if (process.env[key]) env[key] = process.env[key];
|
|
68
95
|
}
|
|
69
|
-
|
|
70
96
|
if (this.config.apiKey) {
|
|
71
97
|
env['ANTHROPIC_API_KEY'] = this.config.apiKey;
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
|
|
100
|
+
// Build full args: base args + model + prompt
|
|
101
|
+
const fullArgs: string[] = [];
|
|
102
|
+
for (const arg of args.slice(0, -1)) {
|
|
103
|
+
fullArgs.push(arg);
|
|
104
|
+
}
|
|
75
105
|
if (this.config.model) {
|
|
76
|
-
|
|
106
|
+
fullArgs.push('--model', this.config.model);
|
|
77
107
|
}
|
|
78
|
-
|
|
108
|
+
fullArgs.push(promptArg);
|
|
79
109
|
|
|
80
|
-
|
|
110
|
+
const workdir = workingDirectory || this.config.workdir || process.cwd();
|
|
111
|
+
log.info('Spawning claude', { cliPath, workdir, model: this.config.model });
|
|
81
112
|
|
|
82
113
|
return new Promise<string>((resolve, reject) => {
|
|
83
|
-
const child = spawn(cliPath,
|
|
84
|
-
cwd:
|
|
114
|
+
const child = spawn(cliPath, fullArgs, {
|
|
115
|
+
cwd: workdir,
|
|
85
116
|
env,
|
|
86
117
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
118
|
});
|
|
@@ -94,7 +125,6 @@ export class ClaudeClient {
|
|
|
94
125
|
|
|
95
126
|
const timer = setTimeout(() => {
|
|
96
127
|
child.kill('SIGTERM');
|
|
97
|
-
// Force kill after 5s grace period
|
|
98
128
|
const forceTimer = setTimeout(() => {
|
|
99
129
|
if (child.exitCode === null) child.kill('SIGKILL');
|
|
100
130
|
}, 5000);
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* Claude Code A2A Executor
|
|
3
3
|
*
|
|
4
4
|
* Bridges the local Claude Code CLI to the A2A protocol. Spawns Claude Code
|
|
5
|
-
* in non-interactive print mode (`-p`) for each request
|
|
6
|
-
*
|
|
5
|
+
* in non-interactive print mode (`-p`) for each request. Supports multi-turn
|
|
6
|
+
* conversation via contextId → sessionId binding through SessionBindingStore.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { A2AExecutor } from '../index.js';
|
|
10
10
|
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
11
|
-
import type { Message as A2AMessage } from '@a2a-js/sdk';
|
|
12
11
|
|
|
13
12
|
import type { AgentConfig } from '../../config/types.js';
|
|
13
|
+
import type { SessionBindingStore } from '../../session-store.js';
|
|
14
|
+
import { generateContextId } from '../../session-store.js';
|
|
14
15
|
import { logger } from '../../logger.js';
|
|
15
16
|
import { ClaudeClient } from './client.js';
|
|
16
17
|
import {
|
|
17
18
|
publishTask,
|
|
18
19
|
publishStatus,
|
|
19
20
|
publishFinalArtifact,
|
|
21
|
+
extractText,
|
|
20
22
|
} from '../events.js';
|
|
21
23
|
|
|
22
24
|
const log = logger.child('claude:executor');
|
|
@@ -27,9 +29,11 @@ export class ClaudeExecutor implements A2AExecutor {
|
|
|
27
29
|
private config: Required<AgentConfig>;
|
|
28
30
|
private client: ClaudeClient | null = null;
|
|
29
31
|
private initialized = false;
|
|
32
|
+
private sessionStore: SessionBindingStore;
|
|
30
33
|
|
|
31
|
-
constructor(config: Required<AgentConfig
|
|
34
|
+
constructor(config: Required<AgentConfig>, sessionStore: SessionBindingStore) {
|
|
32
35
|
this.config = config;
|
|
36
|
+
this.sessionStore = sessionStore;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
async initialize(): Promise<void> {
|
|
@@ -55,31 +59,70 @@ export class ClaudeExecutor implements A2AExecutor {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
|
|
58
|
-
const { taskId,
|
|
62
|
+
const { taskId, userMessage } = ctx;
|
|
59
63
|
await this.initialize();
|
|
60
64
|
|
|
65
|
+
const reuseByContext = this.config.session?.reuseByContext ?? true;
|
|
66
|
+
|
|
67
|
+
let contextId = ctx.contextId;
|
|
68
|
+
if (!contextId) {
|
|
69
|
+
contextId = generateContextId();
|
|
70
|
+
log.info('Generated new contextId', { contextId, taskId });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const binding = reuseByContext
|
|
74
|
+
? this.sessionStore.get(contextId, 'claude')
|
|
75
|
+
: null;
|
|
76
|
+
|
|
77
|
+
const sessionId = binding?.sessionId;
|
|
78
|
+
|
|
61
79
|
try {
|
|
62
|
-
|
|
80
|
+
const existingTask = ctx.task;
|
|
81
|
+
if (!existingTask) {
|
|
63
82
|
publishTask(bus, taskId, contextId);
|
|
64
83
|
publishStatus(bus, taskId, contextId, 'submitted');
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
publishStatus(bus, taskId, contextId, 'working'
|
|
68
|
-
|
|
69
|
-
const promptText =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
publishStatus(bus, taskId, contextId, 'working');
|
|
87
|
+
|
|
88
|
+
const promptText = extractText(userMessage);
|
|
89
|
+
|
|
90
|
+
// Extract per-request workingDirectory from A2A message metadata
|
|
91
|
+
const userMsg = userMessage as unknown as Record<string, unknown>;
|
|
92
|
+
const workingDirectory = userMsg.metadata &&
|
|
93
|
+
typeof userMsg.metadata === 'object'
|
|
94
|
+
? (userMsg.metadata as Record<string, unknown>).workingDirectory as string | undefined
|
|
95
|
+
: undefined;
|
|
96
|
+
|
|
97
|
+
log.info('Sending prompt to Claude Code', {
|
|
98
|
+
taskId,
|
|
99
|
+
contextId,
|
|
100
|
+
sessionId: sessionId || '(new)',
|
|
101
|
+
len: promptText.length,
|
|
102
|
+
workingDirectory: workingDirectory || '(default)',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await this.client!.execute(promptText, sessionId, workingDirectory);
|
|
106
|
+
|
|
107
|
+
this.sessionStore.set({
|
|
108
|
+
provider: 'claude',
|
|
109
|
+
contextId,
|
|
110
|
+
sessionId: result.sessionId,
|
|
111
|
+
activeTaskId: undefined,
|
|
112
|
+
activeTaskState: undefined,
|
|
113
|
+
createdAt: binding?.createdAt ?? new Date().toISOString(),
|
|
114
|
+
updatedAt: new Date().toISOString(),
|
|
115
|
+
expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1000)).toISOString(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
publishFinalArtifact(bus, taskId, contextId, result.text || 'No response from Claude Code.');
|
|
76
119
|
publishStatus(bus, taskId, contextId, 'completed', undefined, true);
|
|
77
120
|
bus.finished();
|
|
78
|
-
log.info('Task completed', { taskId,
|
|
121
|
+
log.info('Task completed', { taskId, contextId, sessionId: result.sessionId });
|
|
79
122
|
|
|
80
123
|
} catch (error) {
|
|
81
124
|
const msg = (error as Error).message ?? String(error);
|
|
82
|
-
log.error('Execution failed', { taskId, error: msg });
|
|
125
|
+
log.error('Execution failed', { taskId, contextId, error: msg });
|
|
83
126
|
publishStatus(bus, taskId, contextId, 'failed', `Error: ${msg}`, true);
|
|
84
127
|
bus.finished();
|
|
85
128
|
}
|
|
@@ -91,21 +134,13 @@ export class ClaudeExecutor implements A2AExecutor {
|
|
|
91
134
|
publishStatus(bus, taskId, '', 'canceled', 'Claude Code task cancelled', true);
|
|
92
135
|
bus.finished();
|
|
93
136
|
}
|
|
94
|
-
|
|
95
|
-
private extractText(message: A2AMessage): string {
|
|
96
|
-
return message.parts
|
|
97
|
-
.filter((p) => {
|
|
98
|
-
const part = p as unknown as Record<string, unknown>;
|
|
99
|
-
const text = part.text as string | undefined;
|
|
100
|
-
return text !== undefined && text !== null;
|
|
101
|
-
})
|
|
102
|
-
.map((p) => (p as unknown as { text: string }).text)
|
|
103
|
-
.join('\n');
|
|
104
|
-
}
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
108
140
|
|
|
109
|
-
export function createClaudeExecutor(
|
|
110
|
-
|
|
141
|
+
export function createClaudeExecutor(
|
|
142
|
+
config: Required<AgentConfig>,
|
|
143
|
+
sessionStore: SessionBindingStore,
|
|
144
|
+
): A2AExecutor {
|
|
145
|
+
return new ClaudeExecutor(config, sessionStore);
|
|
111
146
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Codex CLI client wrapper.
|
|
3
3
|
*
|
|
4
4
|
* Spawns the OpenAI Codex CLI as a child process, sends prompts via stdin,
|
|
5
|
-
* captures stdout as the response. Supports
|
|
6
|
-
*
|
|
5
|
+
* captures stdout as the response. Supports session persistence: on first
|
|
6
|
+
* call the thread_id is extracted from JSONL output, and subsequent calls
|
|
7
|
+
* use `codex exec resume <sessionId>` for multi-turn continuation.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
@@ -26,6 +27,11 @@ export interface CodexClientConfig {
|
|
|
26
27
|
timeout?: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export interface CodexExecuteResult {
|
|
31
|
+
text: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
30
36
|
|
|
31
37
|
export class CodexClient {
|
|
@@ -46,9 +52,27 @@ export class CodexClient {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
|
-
* Execute a prompt via the Codex CLI
|
|
55
|
+
* Execute a prompt via the Codex CLI.
|
|
56
|
+
*
|
|
57
|
+
* @param prompt - The user prompt text.
|
|
58
|
+
* @param sessionId - If provided, resumes an existing session.
|
|
59
|
+
* Otherwise creates a new session and extracts the thread_id from JSONL.
|
|
60
|
+
* @param workingDirectory - Optional per-request working directory override.
|
|
50
61
|
*/
|
|
51
|
-
async execute(prompt: string): Promise<
|
|
62
|
+
async execute(prompt: string, sessionId?: string, workingDirectory?: string): Promise<CodexExecuteResult> {
|
|
63
|
+
if (sessionId) {
|
|
64
|
+
return this.executeResume(prompt, sessionId, workingDirectory);
|
|
65
|
+
}
|
|
66
|
+
return this.executeNew(prompt, workingDirectory);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private resolveWorkdir(override?: string): string {
|
|
70
|
+
return override || this.config.workdir || process.cwd();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── New session (first turn) ──────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
private async executeNew(prompt: string, workingDirectory?: string): Promise<CodexExecuteResult> {
|
|
52
76
|
const cliPath = this.config.cliPath || 'codex';
|
|
53
77
|
const timeout = this.config.timeout ?? 300_000;
|
|
54
78
|
const MAX_PROMPT = 100_000;
|
|
@@ -57,32 +81,130 @@ export class CodexClient {
|
|
|
57
81
|
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
// Only pass relevant env vars to child process
|
|
61
84
|
const env: Record<string, string> = {
|
|
62
85
|
PATH: process.env.PATH ?? '',
|
|
63
86
|
HOME: process.env.HOME ?? '',
|
|
64
87
|
};
|
|
65
|
-
// Forward common proxy/no-proxy settings
|
|
66
88
|
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
67
89
|
if (process.env[key]) env[key] = process.env[key];
|
|
68
90
|
}
|
|
91
|
+
if (this.config.apiKey) {
|
|
92
|
+
env['OPENAI_API_KEY'] = this.config.apiKey;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const args: string[] = ['exec', '--json'];
|
|
96
|
+
if (this.config.model) {
|
|
97
|
+
args.push('--model', this.config.model);
|
|
98
|
+
}
|
|
99
|
+
args.push(
|
|
100
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
101
|
+
'--skip-git-repo-check',
|
|
102
|
+
prompt,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
106
|
+
log.info('Spawning codex (new session)', { cliPath, workdir, model: this.config.model });
|
|
107
|
+
|
|
108
|
+
return new Promise<CodexExecuteResult>((resolve, reject) => {
|
|
109
|
+
const child = spawn(cliPath, args, {
|
|
110
|
+
cwd: workdir,
|
|
111
|
+
env,
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.currentChild = child;
|
|
116
|
+
|
|
117
|
+
let stdout = '';
|
|
118
|
+
let stderr = '';
|
|
119
|
+
const MAX_OUTPUT = 10_000_000;
|
|
120
|
+
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
child.kill('SIGTERM');
|
|
123
|
+
const forceTimer = setTimeout(() => {
|
|
124
|
+
if (child.exitCode === null) child.kill('SIGKILL');
|
|
125
|
+
}, 5000);
|
|
126
|
+
child.on('close', () => clearTimeout(forceTimer));
|
|
127
|
+
reject(new Error(`Codex execution timed out after ${timeout}ms`));
|
|
128
|
+
}, timeout);
|
|
129
|
+
|
|
130
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
131
|
+
stdout += chunk.toString();
|
|
132
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
133
|
+
child.kill('SIGTERM');
|
|
134
|
+
setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
139
|
+
stderr += chunk.toString();
|
|
140
|
+
log.debug('Codex stderr', { text: chunk.toString().trim() });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
child.on('close', (code) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
this.currentChild = null;
|
|
146
|
+
|
|
147
|
+
if (code !== 0) {
|
|
148
|
+
const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
|
|
149
|
+
log.warn('Codex non-zero exit', { code, stderr: errMsg });
|
|
150
|
+
reject(new Error(errMsg));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const { text, sessionId } = this.parseJsonl(stdout);
|
|
156
|
+
resolve({ text, sessionId });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
reject(new Error(`Failed to parse Codex JSONL output: ${(err as Error).message}`));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
child.on('error', (err) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
log.error('Codex spawn failed', { error: err.message });
|
|
165
|
+
reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Resume session (subsequent turns) ─────────────────────────────────
|
|
171
|
+
|
|
172
|
+
private async executeResume(prompt: string, sessionId: string, workingDirectory?: string): Promise<CodexExecuteResult> {
|
|
173
|
+
const cliPath = this.config.cliPath || 'codex';
|
|
174
|
+
const timeout = this.config.timeout ?? 300_000;
|
|
175
|
+
const MAX_PROMPT = 100_000;
|
|
176
|
+
|
|
177
|
+
if (prompt.length > MAX_PROMPT) {
|
|
178
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
179
|
+
}
|
|
69
180
|
|
|
181
|
+
const env: Record<string, string> = {
|
|
182
|
+
PATH: process.env.PATH ?? '',
|
|
183
|
+
HOME: process.env.HOME ?? '',
|
|
184
|
+
};
|
|
185
|
+
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
186
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
187
|
+
}
|
|
70
188
|
if (this.config.apiKey) {
|
|
71
189
|
env['OPENAI_API_KEY'] = this.config.apiKey;
|
|
72
190
|
}
|
|
73
191
|
|
|
74
|
-
const args: string[] = [];
|
|
75
|
-
// Some codex versions support --model flag
|
|
192
|
+
const args: string[] = ['exec', 'resume', sessionId];
|
|
76
193
|
if (this.config.model) {
|
|
77
194
|
args.push('--model', this.config.model);
|
|
78
195
|
}
|
|
79
|
-
args.push(
|
|
196
|
+
args.push(
|
|
197
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
198
|
+
'--skip-git-repo-check',
|
|
199
|
+
prompt,
|
|
200
|
+
);
|
|
80
201
|
|
|
81
|
-
|
|
202
|
+
const workdir = this.resolveWorkdir(workingDirectory);
|
|
203
|
+
log.info('Spawning codex (resume)', { sessionId, workdir });
|
|
82
204
|
|
|
83
|
-
return new Promise<
|
|
205
|
+
return new Promise<CodexExecuteResult>((resolve, reject) => {
|
|
84
206
|
const child = spawn(cliPath, args, {
|
|
85
|
-
cwd:
|
|
207
|
+
cwd: workdir,
|
|
86
208
|
env,
|
|
87
209
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
210
|
});
|
|
@@ -95,7 +217,6 @@ export class CodexClient {
|
|
|
95
217
|
|
|
96
218
|
const timer = setTimeout(() => {
|
|
97
219
|
child.kill('SIGTERM');
|
|
98
|
-
// Force kill after 5s grace period
|
|
99
220
|
const forceTimer = setTimeout(() => {
|
|
100
221
|
if (child.exitCode === null) child.kill('SIGKILL');
|
|
101
222
|
}, 5000);
|
|
@@ -120,13 +241,27 @@ export class CodexClient {
|
|
|
120
241
|
clearTimeout(timer);
|
|
121
242
|
this.currentChild = null;
|
|
122
243
|
|
|
123
|
-
if (code
|
|
124
|
-
resolve(stdout.trim());
|
|
125
|
-
} else {
|
|
244
|
+
if (code !== 0) {
|
|
126
245
|
const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
|
|
127
246
|
log.warn('Codex non-zero exit', { code, stderr: errMsg });
|
|
128
247
|
reject(new Error(errMsg));
|
|
248
|
+
return;
|
|
129
249
|
}
|
|
250
|
+
|
|
251
|
+
// Resume output: the CLI prints session metadata header + agent response.
|
|
252
|
+
// Strip the header lines (first 2+ lines before the actual output).
|
|
253
|
+
// The header looks like:
|
|
254
|
+
// OpenAI Codex v0.142.0
|
|
255
|
+
// --------
|
|
256
|
+
// workdir: ...
|
|
257
|
+
// ...
|
|
258
|
+
// --------
|
|
259
|
+
// user
|
|
260
|
+
// ...
|
|
261
|
+
// codex
|
|
262
|
+
// <actual response>
|
|
263
|
+
const text = this.extractResumeText(stdout);
|
|
264
|
+
resolve({ text, sessionId });
|
|
130
265
|
});
|
|
131
266
|
|
|
132
267
|
child.on('error', (err) => {
|
|
@@ -136,4 +271,92 @@ export class CodexClient {
|
|
|
136
271
|
});
|
|
137
272
|
});
|
|
138
273
|
}
|
|
274
|
+
|
|
275
|
+
// ── Parsers ───────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Parse JSONL output to extract session thread_id and agent response text.
|
|
279
|
+
*/
|
|
280
|
+
private parseJsonl(stdout: string): { text: string; sessionId: string } {
|
|
281
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
282
|
+
let sessionId = '';
|
|
283
|
+
const textParts: string[] = [];
|
|
284
|
+
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
let event: Record<string, unknown>;
|
|
287
|
+
try {
|
|
288
|
+
event = JSON.parse(line);
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Extract thread_id from thread.started event
|
|
294
|
+
if (event.type === 'thread.started' && event.thread_id) {
|
|
295
|
+
sessionId = event.thread_id as string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Extract text from agent messages in item.completed events
|
|
299
|
+
if (event.type === 'item.completed') {
|
|
300
|
+
const item = event.item as Record<string, unknown> | undefined;
|
|
301
|
+
if (item && item.type === 'agent_message' && item.text) {
|
|
302
|
+
textParts.push(item.text as string);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!sessionId) {
|
|
308
|
+
throw new Error('No thread_id found in Codex JSONL output');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
text: textParts.join('\n').trim() || 'No response from Codex.',
|
|
313
|
+
sessionId,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Extract the agent response text from the resume mode output.
|
|
319
|
+
*
|
|
320
|
+
* The resume output includes a CLI header followed by the conversation.
|
|
321
|
+
* We extract everything after the last "codex" marker line as the
|
|
322
|
+
* latest agent response.
|
|
323
|
+
*/
|
|
324
|
+
private extractResumeText(stdout: string): string {
|
|
325
|
+
const lines = stdout.split('\n');
|
|
326
|
+
|
|
327
|
+
// Find the separator: the last "codex" line marks the start of the response
|
|
328
|
+
let responseStart = -1;
|
|
329
|
+
for (let i = 0; i < lines.length; i++) {
|
|
330
|
+
if (lines[i].trim() === 'codex') {
|
|
331
|
+
responseStart = i;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (responseStart >= 0 && responseStart + 1 < lines.length) {
|
|
336
|
+
return lines.slice(responseStart + 1).join('\n').trim();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Fallback: strip known header lines
|
|
340
|
+
let skipHeader = true;
|
|
341
|
+
const resultLines: string[] = [];
|
|
342
|
+
for (const line of lines) {
|
|
343
|
+
const trimmed = line.trim();
|
|
344
|
+
if (skipHeader) {
|
|
345
|
+
if (trimmed === '--------' || trimmed === 'codex') {
|
|
346
|
+
skipHeader = false;
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (trimmed === 'user' || trimmed === 'codex') {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (trimmed.startsWith('tokens used')) {
|
|
354
|
+
if (resultLines.length > 0) resultLines.pop();
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
resultLines.push(line);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return resultLines.join('\n').trim() || stdout.trim();
|
|
361
|
+
}
|
|
139
362
|
}
|