@axiom-lattice/cli-a2a 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +51 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +290 -0
- package/__tests__/opencode-executor.test.ts +159 -0
- package/agents/opencode-example/config.json +64 -0
- package/dist/bridge-7ZUDKCZT.mjs +271 -0
- package/dist/bridge-7ZUDKCZT.mjs.map +1 -0
- package/dist/chunk-35NFMGMS.mjs +27 -0
- package/dist/chunk-35NFMGMS.mjs.map +1 -0
- package/dist/chunk-G7AGL2QA.mjs +284 -0
- package/dist/chunk-G7AGL2QA.mjs.map +1 -0
- package/dist/chunk-LXL47XMZ.mjs +43 -0
- package/dist/chunk-LXL47XMZ.mjs.map +1 -0
- package/dist/chunk-NQIDRU47.mjs +178 -0
- package/dist/chunk-NQIDRU47.mjs.map +1 -0
- package/dist/chunk-VSZ3DACI.mjs +179 -0
- package/dist/chunk-VSZ3DACI.mjs.map +1 -0
- package/dist/chunk-VZEH3EPJ.mjs +56 -0
- package/dist/chunk-VZEH3EPJ.mjs.map +1 -0
- package/dist/chunk-WNCDOYZS.mjs +187 -0
- package/dist/chunk-WNCDOYZS.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1562 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +293 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/executor-OWPVDUXH.mjs +11 -0
- package/dist/executor-OWPVDUXH.mjs.map +1 -0
- package/dist/executor-RVBGAWUF.mjs +11 -0
- package/dist/executor-RVBGAWUF.mjs.map +1 -0
- package/dist/executor-XWHWUVQ3.mjs +11 -0
- package/dist/executor-XWHWUVQ3.mjs.map +1 -0
- package/dist/executors-QIIKBUMJ.mjs +15 -0
- package/dist/executors-QIIKBUMJ.mjs.map +1 -0
- package/dist/index.d.mts +295 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.js +942 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +44 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.js +16 -0
- package/package.json +63 -0
- package/src/bridge.ts +355 -0
- package/src/cli.ts +384 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +16 -0
- package/src/config/loader.ts +121 -0
- package/src/config/types.ts +163 -0
- package/src/executors/claude/client.ts +138 -0
- package/src/executors/claude/executor.ts +111 -0
- package/src/executors/codex/client.ts +139 -0
- package/src/executors/codex/executor.ts +118 -0
- package/src/executors/events.ts +117 -0
- package/src/executors/index.ts +54 -0
- package/src/executors/opencode/client.ts +149 -0
- package/src/executors/opencode/executor.ts +112 -0
- package/src/index.ts +54 -0
- package/src/logger.ts +78 -0
- package/src/server/agent-card.ts +51 -0
- package/src/server/index.ts +124 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified configuration types for the CLI A2A gateway.
|
|
3
|
+
*
|
|
4
|
+
* Each provider (OpenCode, Codex, Claude Code) extends the base config
|
|
5
|
+
* with its own connection settings. A single config file can declare
|
|
6
|
+
* multiple providers, and the CLI selects one at startup via --provider.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Agent Card ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface AgentCardConfig {
|
|
12
|
+
/** Human-readable agent name */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Agent description shown to orchestrators / callers */
|
|
15
|
+
description: string;
|
|
16
|
+
/** Protocol version (default: "0.3.0") */
|
|
17
|
+
protocolVersion?: string;
|
|
18
|
+
/** Agent version string */
|
|
19
|
+
version?: string;
|
|
20
|
+
/** Skills advertised on the agent card */
|
|
21
|
+
skills?: SkillConfig[];
|
|
22
|
+
/** Supported input modes */
|
|
23
|
+
defaultInputModes?: string[];
|
|
24
|
+
/** Supported output modes */
|
|
25
|
+
defaultOutputModes?: string[];
|
|
26
|
+
/** Enable streaming */
|
|
27
|
+
streaming?: boolean;
|
|
28
|
+
/** Enable push notifications */
|
|
29
|
+
pushNotifications?: boolean;
|
|
30
|
+
/** Provider info */
|
|
31
|
+
provider?: { organization: string; url?: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SkillConfig {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
examples?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Server ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface ServerConfig {
|
|
45
|
+
/** A2A server port */
|
|
46
|
+
port?: number;
|
|
47
|
+
/** Bind address */
|
|
48
|
+
hostname?: string;
|
|
49
|
+
/** Hostname advertised in agent card URLs */
|
|
50
|
+
advertiseHost?: string;
|
|
51
|
+
/** Protocol for advertised URLs */
|
|
52
|
+
advertiseProtocol?: 'http' | 'https';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Session ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface SessionConfig {
|
|
58
|
+
/** Session title prefix */
|
|
59
|
+
titlePrefix?: string;
|
|
60
|
+
/** Reuse sessions by A2A contextId */
|
|
61
|
+
reuseByContext?: boolean;
|
|
62
|
+
/** Session TTL in ms */
|
|
63
|
+
ttl?: number;
|
|
64
|
+
/** Cleanup interval in ms */
|
|
65
|
+
cleanupInterval?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Features / Timeouts / Logging ─────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export interface FeatureFlags {
|
|
71
|
+
/** Auto-approve all permission requests */
|
|
72
|
+
autoApprovePermissions?: boolean;
|
|
73
|
+
/** Auto-answer question prompts */
|
|
74
|
+
autoAnswerQuestions?: boolean;
|
|
75
|
+
/** Stream artifact chunks individually vs buffer */
|
|
76
|
+
streamArtifactChunks?: boolean;
|
|
77
|
+
/** Enable polling fallback on SSE failure */
|
|
78
|
+
enablePollingFallback?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface TimeoutConfig {
|
|
82
|
+
/** Timeout for a single prompt in ms */
|
|
83
|
+
prompt?: number;
|
|
84
|
+
/** Polling interval in ms */
|
|
85
|
+
pollingInterval?: number;
|
|
86
|
+
/** Health check interval in ms */
|
|
87
|
+
healthCheck?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LoggingConfig {
|
|
91
|
+
/** Log level */
|
|
92
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Provider Configs ──────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface OpenCodeConfig {
|
|
98
|
+
/** OpenCode server base URL */
|
|
99
|
+
baseUrl?: string;
|
|
100
|
+
/** Target project directory */
|
|
101
|
+
projectDirectory?: string;
|
|
102
|
+
/** Model identifier (e.g. "anthropic/claude-sonnet-4-20250514") */
|
|
103
|
+
model?: string;
|
|
104
|
+
/** Agent preset name */
|
|
105
|
+
agent?: string;
|
|
106
|
+
/** System prompt */
|
|
107
|
+
systemPrompt?: string;
|
|
108
|
+
/** How system prompt is applied */
|
|
109
|
+
systemPromptMode?: 'append' | 'replace';
|
|
110
|
+
/** Context file name */
|
|
111
|
+
contextFile?: string;
|
|
112
|
+
/** Prompt for building context */
|
|
113
|
+
contextPrompt?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface CodexConfig {
|
|
117
|
+
/** Codex CLI path */
|
|
118
|
+
cliPath?: string;
|
|
119
|
+
/** Working directory */
|
|
120
|
+
workdir?: string;
|
|
121
|
+
/** Model identifier */
|
|
122
|
+
model?: string;
|
|
123
|
+
/** API key for OpenAI */
|
|
124
|
+
apiKey?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ClaudeCodeConfig {
|
|
128
|
+
/** Claude Code CLI path */
|
|
129
|
+
cliPath?: string;
|
|
130
|
+
/** Working directory */
|
|
131
|
+
workdir?: string;
|
|
132
|
+
/** Model identifier */
|
|
133
|
+
model?: string;
|
|
134
|
+
/** API key for Anthropic */
|
|
135
|
+
apiKey?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Root Config ───────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export type ProviderType = 'opencode' | 'codex' | 'claude';
|
|
141
|
+
|
|
142
|
+
export interface AgentConfig {
|
|
143
|
+
/** Active provider to use */
|
|
144
|
+
provider: ProviderType;
|
|
145
|
+
/** Agent card identity */
|
|
146
|
+
agentCard: AgentCardConfig;
|
|
147
|
+
/** Server settings */
|
|
148
|
+
server?: ServerConfig;
|
|
149
|
+
/** Session management */
|
|
150
|
+
session?: SessionConfig;
|
|
151
|
+
/** Feature flags */
|
|
152
|
+
features?: FeatureFlags;
|
|
153
|
+
/** Timeout settings */
|
|
154
|
+
timeouts?: TimeoutConfig;
|
|
155
|
+
/** Logging settings */
|
|
156
|
+
logging?: LoggingConfig;
|
|
157
|
+
/** Provider-specific connection configs */
|
|
158
|
+
opencode?: OpenCodeConfig;
|
|
159
|
+
codex?: CodexConfig;
|
|
160
|
+
claude?: ClaudeCodeConfig;
|
|
161
|
+
/** Directory containing config file (populated at runtime) */
|
|
162
|
+
configDir?: string;
|
|
163
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code CLI client wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the Anthropic Claude Code CLI as a child process in non-interactive
|
|
5
|
+
* print mode (`-p`). Sends prompts, captures stdout as the response.
|
|
6
|
+
* Supports configurable CLI path, working directory, model, and timeout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { logger } from '../../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = logger.child('claude:client');
|
|
13
|
+
|
|
14
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface ClaudeClientConfig {
|
|
17
|
+
/** Path to the claude binary (default: "claude") */
|
|
18
|
+
cliPath: string;
|
|
19
|
+
/** Working directory for the process */
|
|
20
|
+
workdir: string;
|
|
21
|
+
/** Model identifier (e.g. "claude-sonnet-4-20250514") — passed via --model */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Anthropic API key — passed as ANTHROPIC_API_KEY env */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
/** Timeout in ms (default: 300_000 = 5 min) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export class ClaudeClient {
|
|
32
|
+
private currentChild: ReturnType<typeof spawn> | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(private config: ClaudeClientConfig) {}
|
|
35
|
+
|
|
36
|
+
abort(): void {
|
|
37
|
+
if (this.currentChild) {
|
|
38
|
+
this.currentChild.kill('SIGTERM');
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (this.currentChild?.exitCode === null) {
|
|
41
|
+
this.currentChild.kill('SIGKILL');
|
|
42
|
+
}
|
|
43
|
+
}, 5000);
|
|
44
|
+
this.currentChild = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute a prompt via the Claude Code CLI and return the response text.
|
|
50
|
+
*/
|
|
51
|
+
async execute(prompt: string): Promise<string> {
|
|
52
|
+
const cliPath = this.config.cliPath || 'claude';
|
|
53
|
+
const timeout = this.config.timeout ?? 300_000;
|
|
54
|
+
const MAX_PROMPT = 100_000;
|
|
55
|
+
|
|
56
|
+
if (prompt.length > MAX_PROMPT) {
|
|
57
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Only pass relevant env vars to child process
|
|
61
|
+
const env: Record<string, string> = {
|
|
62
|
+
PATH: process.env.PATH ?? '',
|
|
63
|
+
HOME: process.env.HOME ?? '',
|
|
64
|
+
};
|
|
65
|
+
// Forward common proxy/no-proxy settings
|
|
66
|
+
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
67
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.config.apiKey) {
|
|
71
|
+
env['ANTHROPIC_API_KEY'] = this.config.apiKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const args: string[] = ['-p'];
|
|
75
|
+
if (this.config.model) {
|
|
76
|
+
args.push('--model', this.config.model);
|
|
77
|
+
}
|
|
78
|
+
args.push(prompt);
|
|
79
|
+
|
|
80
|
+
log.info('Spawning claude', { cliPath, workdir: this.config.workdir, model: this.config.model });
|
|
81
|
+
|
|
82
|
+
return new Promise<string>((resolve, reject) => {
|
|
83
|
+
const child = spawn(cliPath, args, {
|
|
84
|
+
cwd: this.config.workdir || process.cwd(),
|
|
85
|
+
env,
|
|
86
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.currentChild = child;
|
|
90
|
+
|
|
91
|
+
let stdout = '';
|
|
92
|
+
let stderr = '';
|
|
93
|
+
const MAX_OUTPUT = 10_000_000;
|
|
94
|
+
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
child.kill('SIGTERM');
|
|
97
|
+
// Force kill after 5s grace period
|
|
98
|
+
const forceTimer = setTimeout(() => {
|
|
99
|
+
if (child.exitCode === null) child.kill('SIGKILL');
|
|
100
|
+
}, 5000);
|
|
101
|
+
child.on('close', () => clearTimeout(forceTimer));
|
|
102
|
+
reject(new Error(`Claude Code execution timed out after ${timeout}ms`));
|
|
103
|
+
}, timeout);
|
|
104
|
+
|
|
105
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
106
|
+
stdout += chunk.toString();
|
|
107
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
108
|
+
child.kill('SIGTERM');
|
|
109
|
+
setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
114
|
+
stderr += chunk.toString();
|
|
115
|
+
log.debug('Claude stderr', { text: chunk.toString().trim() });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.on('close', (code) => {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
this.currentChild = null;
|
|
121
|
+
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve(stdout.trim());
|
|
124
|
+
} else {
|
|
125
|
+
const errMsg = stderr.trim() || stdout.trim() || `Claude Code exited with code ${code}`;
|
|
126
|
+
log.warn('Claude non-zero exit', { code, stderr: errMsg });
|
|
127
|
+
reject(new Error(errMsg));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.on('error', (err) => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
log.error('Claude spawn failed', { error: err.message });
|
|
134
|
+
reject(new Error(`Failed to start claude: ${err.message}. Install: npm i -g @anthropic-ai/claude-code`));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code A2A Executor
|
|
3
|
+
*
|
|
4
|
+
* Bridges the local Claude Code CLI to the A2A protocol. Spawns Claude Code
|
|
5
|
+
* in non-interactive print mode (`-p`) for each request and captures stdout
|
|
6
|
+
* as the response.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { A2AExecutor } from '../index.js';
|
|
10
|
+
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
11
|
+
import type { Message as A2AMessage } from '@a2a-js/sdk';
|
|
12
|
+
|
|
13
|
+
import type { AgentConfig } from '../../config/types.js';
|
|
14
|
+
import { logger } from '../../logger.js';
|
|
15
|
+
import { ClaudeClient } from './client.js';
|
|
16
|
+
import {
|
|
17
|
+
publishTask,
|
|
18
|
+
publishStatus,
|
|
19
|
+
publishFinalArtifact,
|
|
20
|
+
} from '../events.js';
|
|
21
|
+
|
|
22
|
+
const log = logger.child('claude:executor');
|
|
23
|
+
|
|
24
|
+
// ─── Executor ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export class ClaudeExecutor implements A2AExecutor {
|
|
27
|
+
private config: Required<AgentConfig>;
|
|
28
|
+
private client: ClaudeClient | null = null;
|
|
29
|
+
private initialized = false;
|
|
30
|
+
|
|
31
|
+
constructor(config: Required<AgentConfig>) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(): Promise<void> {
|
|
36
|
+
if (this.initialized) return;
|
|
37
|
+
|
|
38
|
+
const cc = this.config.claude;
|
|
39
|
+
this.client = new ClaudeClient({
|
|
40
|
+
cliPath: cc.cliPath!,
|
|
41
|
+
workdir: cc.workdir!,
|
|
42
|
+
model: cc.model || undefined,
|
|
43
|
+
apiKey: cc.apiKey || undefined,
|
|
44
|
+
timeout: this.config.timeouts.prompt ?? 600_000,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.initialized = true;
|
|
48
|
+
log.info('Executor initialized', { cliPath: cc.cliPath, workdir: cc.workdir });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async shutdown(): Promise<void> {
|
|
52
|
+
this.client = null;
|
|
53
|
+
this.initialized = false;
|
|
54
|
+
log.info('Executor shut down');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
|
|
58
|
+
const { taskId, contextId, userMessage, task } = ctx;
|
|
59
|
+
await this.initialize();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (!task) {
|
|
63
|
+
publishTask(bus, taskId, contextId);
|
|
64
|
+
publishStatus(bus, taskId, contextId, 'submitted');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
publishStatus(bus, taskId, contextId, 'working', 'Processing request...');
|
|
68
|
+
|
|
69
|
+
const promptText = this.extractText(userMessage);
|
|
70
|
+
log.info('Sending prompt to Claude Code', { taskId, len: promptText.length });
|
|
71
|
+
|
|
72
|
+
const response = await this.client!.execute(promptText);
|
|
73
|
+
|
|
74
|
+
const finalText = response || 'No response from Claude Code.';
|
|
75
|
+
publishFinalArtifact(bus, taskId, contextId, finalText);
|
|
76
|
+
publishStatus(bus, taskId, contextId, 'completed', undefined, true);
|
|
77
|
+
bus.finished();
|
|
78
|
+
log.info('Task completed', { taskId, len: finalText.length });
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const msg = (error as Error).message ?? String(error);
|
|
82
|
+
log.error('Execution failed', { taskId, error: msg });
|
|
83
|
+
publishStatus(bus, taskId, contextId, 'failed', `Error: ${msg}`, true);
|
|
84
|
+
bus.finished();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async cancelTask(taskId: string, bus: ExecutionEventBus): Promise<void> {
|
|
89
|
+
log.info('Cancel requested for Claude Code task', { taskId });
|
|
90
|
+
this.client?.abort();
|
|
91
|
+
publishStatus(bus, taskId, '', 'canceled', 'Claude Code task cancelled', true);
|
|
92
|
+
bus.finished();
|
|
93
|
+
}
|
|
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
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export function createClaudeExecutor(config: Required<AgentConfig>): A2AExecutor {
|
|
110
|
+
return new ClaudeExecutor(config);
|
|
111
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI client wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the OpenAI Codex CLI as a child process, sends prompts via stdin,
|
|
5
|
+
* captures stdout as the response. Supports configurable CLI path, working
|
|
6
|
+
* directory, model selection, and timeout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { logger } from '../../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = logger.child('codex:client');
|
|
13
|
+
|
|
14
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface CodexClientConfig {
|
|
17
|
+
/** Path to the codex binary (default: "codex") */
|
|
18
|
+
cliPath: string;
|
|
19
|
+
/** Working directory for the codex process */
|
|
20
|
+
workdir: string;
|
|
21
|
+
/** Model identifier (e.g. "gpt-4o") — passed as CODELLM env or --model flag */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** OpenAI API key — passed as OPENAI_API_KEY env */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
/** Timeout in ms (default: 300_000 = 5 min) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export class CodexClient {
|
|
32
|
+
private currentChild: ReturnType<typeof spawn> | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(private config: CodexClientConfig) {}
|
|
35
|
+
|
|
36
|
+
abort(): void {
|
|
37
|
+
if (this.currentChild) {
|
|
38
|
+
this.currentChild.kill('SIGTERM');
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (this.currentChild?.exitCode === null) {
|
|
41
|
+
this.currentChild.kill('SIGKILL');
|
|
42
|
+
}
|
|
43
|
+
}, 5000);
|
|
44
|
+
this.currentChild = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute a prompt via the Codex CLI and return the response text.
|
|
50
|
+
*/
|
|
51
|
+
async execute(prompt: string): Promise<string> {
|
|
52
|
+
const cliPath = this.config.cliPath || 'codex';
|
|
53
|
+
const timeout = this.config.timeout ?? 300_000;
|
|
54
|
+
const MAX_PROMPT = 100_000;
|
|
55
|
+
|
|
56
|
+
if (prompt.length > MAX_PROMPT) {
|
|
57
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Only pass relevant env vars to child process
|
|
61
|
+
const env: Record<string, string> = {
|
|
62
|
+
PATH: process.env.PATH ?? '',
|
|
63
|
+
HOME: process.env.HOME ?? '',
|
|
64
|
+
};
|
|
65
|
+
// Forward common proxy/no-proxy settings
|
|
66
|
+
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
67
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.config.apiKey) {
|
|
71
|
+
env['OPENAI_API_KEY'] = this.config.apiKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const args: string[] = [];
|
|
75
|
+
// Some codex versions support --model flag
|
|
76
|
+
if (this.config.model) {
|
|
77
|
+
args.push('--model', this.config.model);
|
|
78
|
+
}
|
|
79
|
+
args.push(prompt);
|
|
80
|
+
|
|
81
|
+
log.info('Spawning codex', { cliPath, workdir: this.config.workdir, model: this.config.model });
|
|
82
|
+
|
|
83
|
+
return new Promise<string>((resolve, reject) => {
|
|
84
|
+
const child = spawn(cliPath, args, {
|
|
85
|
+
cwd: this.config.workdir || process.cwd(),
|
|
86
|
+
env,
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.currentChild = child;
|
|
91
|
+
|
|
92
|
+
let stdout = '';
|
|
93
|
+
let stderr = '';
|
|
94
|
+
const MAX_OUTPUT = 10_000_000;
|
|
95
|
+
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
child.kill('SIGTERM');
|
|
98
|
+
// Force kill after 5s grace period
|
|
99
|
+
const forceTimer = setTimeout(() => {
|
|
100
|
+
if (child.exitCode === null) child.kill('SIGKILL');
|
|
101
|
+
}, 5000);
|
|
102
|
+
child.on('close', () => clearTimeout(forceTimer));
|
|
103
|
+
reject(new Error(`Codex execution timed out after ${timeout}ms`));
|
|
104
|
+
}, timeout);
|
|
105
|
+
|
|
106
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
107
|
+
stdout += chunk.toString();
|
|
108
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
109
|
+
child.kill('SIGTERM');
|
|
110
|
+
setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
115
|
+
stderr += chunk.toString();
|
|
116
|
+
log.debug('Codex stderr', { text: chunk.toString().trim() });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
child.on('close', (code) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
this.currentChild = null;
|
|
122
|
+
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
resolve(stdout.trim());
|
|
125
|
+
} else {
|
|
126
|
+
const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
|
|
127
|
+
log.warn('Codex non-zero exit', { code, stderr: errMsg });
|
|
128
|
+
reject(new Error(errMsg));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
child.on('error', (err) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
log.error('Codex spawn failed', { error: err.message });
|
|
135
|
+
reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex A2A Executor
|
|
3
|
+
*
|
|
4
|
+
* Bridges the local Codex CLI to the A2A protocol. Spawns Codex as a
|
|
5
|
+
* child process for each request and captures stdout as the response.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { A2AExecutor } from '../index.js';
|
|
9
|
+
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
10
|
+
import type { Message as A2AMessage } from '@a2a-js/sdk';
|
|
11
|
+
|
|
12
|
+
import type { AgentConfig } from '../../config/types.js';
|
|
13
|
+
import { logger } from '../../logger.js';
|
|
14
|
+
import { CodexClient } from './client.js';
|
|
15
|
+
import {
|
|
16
|
+
publishTask,
|
|
17
|
+
publishStatus,
|
|
18
|
+
publishFinalArtifact,
|
|
19
|
+
} from '../events.js';
|
|
20
|
+
|
|
21
|
+
const log = logger.child('codex:executor');
|
|
22
|
+
|
|
23
|
+
// ─── Executor ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export class CodexExecutor implements A2AExecutor {
|
|
26
|
+
private config: Required<AgentConfig>;
|
|
27
|
+
private client: CodexClient | null = null;
|
|
28
|
+
private initialized = false;
|
|
29
|
+
|
|
30
|
+
constructor(config: Required<AgentConfig>) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async initialize(): Promise<void> {
|
|
35
|
+
if (this.initialized) return;
|
|
36
|
+
|
|
37
|
+
const cx = this.config.codex;
|
|
38
|
+
this.client = new CodexClient({
|
|
39
|
+
cliPath: cx.cliPath!,
|
|
40
|
+
workdir: cx.workdir!,
|
|
41
|
+
model: cx.model || undefined,
|
|
42
|
+
apiKey: cx.apiKey || undefined,
|
|
43
|
+
timeout: this.config.timeouts.prompt ?? 600_000,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.initialized = true;
|
|
47
|
+
log.info('Executor initialized', { cliPath: cx.cliPath, workdir: cx.workdir });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async shutdown(): Promise<void> {
|
|
51
|
+
this.client = null;
|
|
52
|
+
this.initialized = false;
|
|
53
|
+
log.info('Executor shut down');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
|
|
57
|
+
const { taskId, contextId, userMessage, task } = ctx;
|
|
58
|
+
await this.initialize();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 1. Register task
|
|
62
|
+
if (!task) {
|
|
63
|
+
publishTask(bus, taskId, contextId);
|
|
64
|
+
publishStatus(bus, taskId, contextId, 'submitted');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Working
|
|
68
|
+
publishStatus(bus, taskId, contextId, 'working', 'Processing request...');
|
|
69
|
+
|
|
70
|
+
// 3. Extract prompt text
|
|
71
|
+
const promptText = this.extractText(userMessage);
|
|
72
|
+
|
|
73
|
+
log.info('Sending prompt to Codex', { taskId, len: promptText.length });
|
|
74
|
+
|
|
75
|
+
// 4. Execute
|
|
76
|
+
const response = await this.client!.execute(promptText);
|
|
77
|
+
|
|
78
|
+
// 5. Finalize
|
|
79
|
+
const finalText = response || 'No response from Codex.';
|
|
80
|
+
publishFinalArtifact(bus, taskId, contextId, finalText);
|
|
81
|
+
publishStatus(bus, taskId, contextId, 'completed', undefined, true);
|
|
82
|
+
bus.finished();
|
|
83
|
+
log.info('Task completed', { taskId, len: finalText.length });
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const msg = (error as Error).message ?? String(error);
|
|
87
|
+
log.error('Execution failed', { taskId, error: msg });
|
|
88
|
+
publishStatus(bus, taskId, contextId, 'failed', `Error: ${msg}`, true);
|
|
89
|
+
bus.finished();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async cancelTask(taskId: string, bus: ExecutionEventBus): Promise<void> {
|
|
94
|
+
log.info('Cancel requested for Codex task', { taskId });
|
|
95
|
+
this.client?.abort();
|
|
96
|
+
publishStatus(bus, taskId, '', 'canceled', 'Codex task cancelled', true);
|
|
97
|
+
bus.finished();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
private extractText(message: A2AMessage): string {
|
|
103
|
+
return message.parts
|
|
104
|
+
.filter((p) => {
|
|
105
|
+
const part = p as unknown as Record<string, unknown>;
|
|
106
|
+
const text = part.text as string | undefined;
|
|
107
|
+
return text !== undefined && text !== null;
|
|
108
|
+
})
|
|
109
|
+
.map((p) => (p as unknown as { text: string }).text)
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function createCodexExecutor(config: Required<AgentConfig>): A2AExecutor {
|
|
117
|
+
return new CodexExecutor(config);
|
|
118
|
+
}
|