@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A event publishing helpers.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around ExecutionEventBus.publish() that construct
|
|
5
|
+
* spec-compliant A2A task status and artifact update events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TaskState } from '@a2a-js/sdk';
|
|
9
|
+
import type { TaskStatusUpdateEvent, TaskArtifactUpdateEvent } from '@a2a-js/sdk';
|
|
10
|
+
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
11
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a task with the execution event bus before publishing any events.
|
|
15
|
+
*/
|
|
16
|
+
export function publishTask(bus: ExecutionEventBus, taskId: string, contextId: string): void {
|
|
17
|
+
bus.publish({
|
|
18
|
+
kind: 'task',
|
|
19
|
+
taskId,
|
|
20
|
+
contextId,
|
|
21
|
+
} as unknown as TaskStatusUpdateEvent);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Publish a task status update.
|
|
26
|
+
*/
|
|
27
|
+
export function publishStatus(
|
|
28
|
+
bus: ExecutionEventBus,
|
|
29
|
+
taskId: string,
|
|
30
|
+
contextId: string,
|
|
31
|
+
state: TaskState | string,
|
|
32
|
+
messageText?: string,
|
|
33
|
+
final?: boolean,
|
|
34
|
+
): void {
|
|
35
|
+
const event: TaskStatusUpdateEvent = {
|
|
36
|
+
kind: 'task-status-update',
|
|
37
|
+
taskId,
|
|
38
|
+
contextId,
|
|
39
|
+
status: {
|
|
40
|
+
state: state as TaskState,
|
|
41
|
+
message: messageText
|
|
42
|
+
? { role: 'agent', parts: [{ kind: 'text', text: messageText }] }
|
|
43
|
+
: undefined,
|
|
44
|
+
},
|
|
45
|
+
final: final ?? false,
|
|
46
|
+
} as unknown as TaskStatusUpdateEvent;
|
|
47
|
+
bus.publish(event);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Publish a complete artifact (non-streaming).
|
|
52
|
+
*/
|
|
53
|
+
export function publishFinalArtifact(
|
|
54
|
+
bus: ExecutionEventBus,
|
|
55
|
+
taskId: string,
|
|
56
|
+
contextId: string,
|
|
57
|
+
text: string,
|
|
58
|
+
): void {
|
|
59
|
+
const artifactId = uuidv4();
|
|
60
|
+
const event: TaskArtifactUpdateEvent = {
|
|
61
|
+
kind: 'task-artifact-update',
|
|
62
|
+
taskId,
|
|
63
|
+
contextId,
|
|
64
|
+
artifact: {
|
|
65
|
+
artifactId,
|
|
66
|
+
parts: [{ kind: 'text', text }],
|
|
67
|
+
},
|
|
68
|
+
lastChunk: true,
|
|
69
|
+
} as unknown as TaskArtifactUpdateEvent;
|
|
70
|
+
bus.publish(event);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Publish a streaming artifact chunk.
|
|
75
|
+
*/
|
|
76
|
+
export function publishStreamingChunk(
|
|
77
|
+
bus: ExecutionEventBus,
|
|
78
|
+
taskId: string,
|
|
79
|
+
contextId: string,
|
|
80
|
+
artifactId: string,
|
|
81
|
+
chunkText: string,
|
|
82
|
+
): void {
|
|
83
|
+
const event: TaskArtifactUpdateEvent = {
|
|
84
|
+
kind: 'task-artifact-update',
|
|
85
|
+
taskId,
|
|
86
|
+
contextId,
|
|
87
|
+
artifact: {
|
|
88
|
+
artifactId,
|
|
89
|
+
parts: [{ kind: 'text', text: chunkText }],
|
|
90
|
+
},
|
|
91
|
+
lastChunk: false,
|
|
92
|
+
} as unknown as TaskArtifactUpdateEvent;
|
|
93
|
+
bus.publish(event);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Publish the final streaming chunk with full accumulated text.
|
|
98
|
+
*/
|
|
99
|
+
export function publishLastChunkMarker(
|
|
100
|
+
bus: ExecutionEventBus,
|
|
101
|
+
taskId: string,
|
|
102
|
+
contextId: string,
|
|
103
|
+
artifactId: string,
|
|
104
|
+
fullText: string,
|
|
105
|
+
): void {
|
|
106
|
+
const event: TaskArtifactUpdateEvent = {
|
|
107
|
+
kind: 'task-artifact-update',
|
|
108
|
+
taskId,
|
|
109
|
+
contextId,
|
|
110
|
+
artifact: {
|
|
111
|
+
artifactId,
|
|
112
|
+
parts: [{ kind: 'text', text: fullText }],
|
|
113
|
+
},
|
|
114
|
+
lastChunk: true,
|
|
115
|
+
} as unknown as TaskArtifactUpdateEvent;
|
|
116
|
+
bus.publish(event);
|
|
117
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor types and common interfaces for CLI A2A providers.
|
|
3
|
+
*
|
|
4
|
+
* Each provider (OpenCode, Codex, Claude) implements its own executor
|
|
5
|
+
* conforming to the A2AExecutor interface which extends the SDK's
|
|
6
|
+
* AgentExecutor with lifecycle methods (initialize, shutdown).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentExecutor } from '@a2a-js/sdk/server';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extended executor interface with lifecycle methods.
|
|
13
|
+
*
|
|
14
|
+
* The base AgentExecutor from @a2a-js/sdk only defines execute() and
|
|
15
|
+
* cancelTask(). We add initialize() for server setup and shutdown()
|
|
16
|
+
* for graceful cleanup.
|
|
17
|
+
*/
|
|
18
|
+
export interface A2AExecutor extends AgentExecutor {
|
|
19
|
+
initialize(): Promise<void>;
|
|
20
|
+
shutdown(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type { AgentExecutor, RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
24
|
+
export { TaskState } from '@a2a-js/sdk';
|
|
25
|
+
export { v4 as uuidv4 } from 'uuid';
|
|
26
|
+
|
|
27
|
+
import type { AgentConfig, ProviderType } from '../config/types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Factory function type: given resolved config, produce an executor.
|
|
31
|
+
*/
|
|
32
|
+
export type ExecutorFactory = (config: Required<AgentConfig>) => A2AExecutor;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Map provider type to executor factory.
|
|
36
|
+
*/
|
|
37
|
+
const registry = new Map<ProviderType, ExecutorFactory>();
|
|
38
|
+
|
|
39
|
+
export function registerExecutor(provider: ProviderType, factory: ExecutorFactory): void {
|
|
40
|
+
registry.set(provider, factory);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getExecutor(provider: ProviderType): ExecutorFactory | undefined {
|
|
44
|
+
return registry.get(provider);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createExecutor(config: Required<AgentConfig>): A2AExecutor {
|
|
48
|
+
const factory = registry.get(config.provider);
|
|
49
|
+
if (!factory) {
|
|
50
|
+
const available = Array.from(registry.keys()).join(', ');
|
|
51
|
+
throw new Error(`Unknown provider: ${config.provider}. Available: ${available}`);
|
|
52
|
+
}
|
|
53
|
+
return factory(config);
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode CLI client wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the OpenCode CLI as a child process. Uses `opencode run` with
|
|
5
|
+
* optional `--attach` to connect to an existing server. Captures stdout
|
|
6
|
+
* as the response. Same pattern as Codex and Claude Code clients.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { logger } from '../../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = logger.child('opencode:client');
|
|
13
|
+
|
|
14
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface OpenCodeClientConfig {
|
|
17
|
+
/** Path to the opencode binary (default: "opencode") */
|
|
18
|
+
cliPath: string;
|
|
19
|
+
/** Working directory for the process */
|
|
20
|
+
workdir: string;
|
|
21
|
+
/** Model identifier (e.g. "anthropic/claude-sonnet-4") */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Agent preset name */
|
|
24
|
+
agent?: string;
|
|
25
|
+
/** Optional URL of a running OpenCode server to attach to */
|
|
26
|
+
attachUrl?: string;
|
|
27
|
+
/** Timeout in ms (default: 600_000 = 10 min) */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export class OpenCodeClient {
|
|
34
|
+
private currentChild: ReturnType<typeof spawn> | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(private config: OpenCodeClientConfig) {}
|
|
37
|
+
|
|
38
|
+
/** Abort the currently running child process (if any). */
|
|
39
|
+
abort(): void {
|
|
40
|
+
if (this.currentChild) {
|
|
41
|
+
this.currentChild.kill('SIGTERM');
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
if (this.currentChild?.exitCode === null) {
|
|
44
|
+
this.currentChild.kill('SIGKILL');
|
|
45
|
+
}
|
|
46
|
+
}, 5000);
|
|
47
|
+
this.currentChild = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute a prompt via the OpenCode CLI and return the response text.
|
|
53
|
+
*/
|
|
54
|
+
async execute(prompt: string): Promise<string> {
|
|
55
|
+
const cliPath = this.config.cliPath || 'opencode';
|
|
56
|
+
const timeout = this.config.timeout ?? 600_000;
|
|
57
|
+
const MAX_PROMPT = 100_000; // safe below typical ARG_MAX
|
|
58
|
+
|
|
59
|
+
if (prompt.length > MAX_PROMPT) {
|
|
60
|
+
throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages or use a provider with HTTP API transport.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Only pass essential env vars
|
|
64
|
+
const env: Record<string, string> = {
|
|
65
|
+
PATH: process.env.PATH ?? '',
|
|
66
|
+
HOME: process.env.HOME ?? '',
|
|
67
|
+
};
|
|
68
|
+
for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']) {
|
|
69
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
70
|
+
}
|
|
71
|
+
// Forward auth env vars if present
|
|
72
|
+
for (const key of ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENCODE_SERVER_PASSWORD', 'OPENCODE_SERVER_USERNAME']) {
|
|
73
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const args: string[] = ['run', prompt];
|
|
77
|
+
|
|
78
|
+
if (this.config.model) {
|
|
79
|
+
args.push('--model', this.config.model);
|
|
80
|
+
}
|
|
81
|
+
if (this.config.agent) {
|
|
82
|
+
args.push('--agent', this.config.agent);
|
|
83
|
+
}
|
|
84
|
+
if (this.config.attachUrl) {
|
|
85
|
+
args.push('--attach', this.config.attachUrl);
|
|
86
|
+
}
|
|
87
|
+
// Default format captures plain text output
|
|
88
|
+
args.push('--format', 'default');
|
|
89
|
+
// Never go interactive
|
|
90
|
+
args.push('--dangerously-skip-permissions');
|
|
91
|
+
|
|
92
|
+
log.info('Spawning opencode', { cliPath, workdir: this.config.workdir, model: this.config.model });
|
|
93
|
+
|
|
94
|
+
return new Promise<string>((resolve, reject) => {
|
|
95
|
+
const child = spawn(cliPath, args, {
|
|
96
|
+
cwd: this.config.workdir || process.cwd(),
|
|
97
|
+
env,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.currentChild = child;
|
|
102
|
+
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
const MAX_OUTPUT = 10_000_000; // 10MB
|
|
106
|
+
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
child.kill('SIGTERM');
|
|
109
|
+
const forceTimer = setTimeout(() => {
|
|
110
|
+
if (child.exitCode === null) child.kill('SIGKILL');
|
|
111
|
+
}, 5000);
|
|
112
|
+
child.on('close', () => clearTimeout(forceTimer));
|
|
113
|
+
reject(new Error(`OpenCode execution timed out after ${timeout}ms`));
|
|
114
|
+
}, timeout);
|
|
115
|
+
|
|
116
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
117
|
+
stdout += chunk.toString();
|
|
118
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
119
|
+
child.kill('SIGTERM');
|
|
120
|
+
setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); }, 5000);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
125
|
+
stderr += chunk.toString();
|
|
126
|
+
log.debug('OpenCode stderr', { text: chunk.toString().trim() });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
child.on('close', (code) => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
this.currentChild = null;
|
|
132
|
+
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
resolve(stdout.trim());
|
|
135
|
+
} else {
|
|
136
|
+
const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
|
|
137
|
+
log.warn('OpenCode non-zero exit', { code, stderr: errMsg });
|
|
138
|
+
reject(new Error(errMsg));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
child.on('error', (err) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
log.error('OpenCode spawn failed', { error: err.message });
|
|
145
|
+
reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode A2A Executor
|
|
3
|
+
*
|
|
4
|
+
* Bridges the local OpenCode CLI to the A2A protocol. Spawns `opencode run`
|
|
5
|
+
* as a child process for each request and captures stdout as the response.
|
|
6
|
+
* Same spawn pattern as Codex and Claude Code executors.
|
|
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 { OpenCodeClient } from './client.js';
|
|
16
|
+
import {
|
|
17
|
+
publishTask,
|
|
18
|
+
publishStatus,
|
|
19
|
+
publishFinalArtifact,
|
|
20
|
+
} from '../events.js';
|
|
21
|
+
|
|
22
|
+
const log = logger.child('opencode:executor');
|
|
23
|
+
|
|
24
|
+
// ─── Executor ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export class OpenCodeExecutor implements A2AExecutor {
|
|
27
|
+
private config: Required<AgentConfig>;
|
|
28
|
+
private client: OpenCodeClient | 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 oc = this.config.opencode;
|
|
39
|
+
this.client = new OpenCodeClient({
|
|
40
|
+
cliPath: 'opencode',
|
|
41
|
+
workdir: oc.projectDirectory || process.cwd(),
|
|
42
|
+
model: oc.model || undefined,
|
|
43
|
+
agent: oc.agent || undefined,
|
|
44
|
+
attachUrl: oc.baseUrl || undefined,
|
|
45
|
+
timeout: this.config.timeouts.prompt ?? 600_000,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
log.info('Executor initialized', { workdir: oc.projectDirectory, model: oc.model });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async shutdown(): Promise<void> {
|
|
53
|
+
this.client = null;
|
|
54
|
+
this.initialized = false;
|
|
55
|
+
log.info('Executor shut down');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
|
|
59
|
+
const { taskId, contextId, userMessage, task } = ctx;
|
|
60
|
+
await this.initialize();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (!task) {
|
|
64
|
+
publishTask(bus, taskId, contextId);
|
|
65
|
+
publishStatus(bus, taskId, contextId, 'submitted');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
publishStatus(bus, taskId, contextId, 'working', 'Processing request...');
|
|
69
|
+
|
|
70
|
+
const promptText = this.extractText(userMessage);
|
|
71
|
+
log.info('Sending prompt to OpenCode', { taskId, len: promptText.length });
|
|
72
|
+
|
|
73
|
+
const response = await this.client!.execute(promptText);
|
|
74
|
+
|
|
75
|
+
const finalText = response || 'No response from OpenCode.';
|
|
76
|
+
publishFinalArtifact(bus, taskId, contextId, finalText);
|
|
77
|
+
publishStatus(bus, taskId, contextId, 'completed', undefined, true);
|
|
78
|
+
bus.finished();
|
|
79
|
+
log.info('Task completed', { taskId, len: finalText.length });
|
|
80
|
+
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const msg = (error as Error).message ?? String(error);
|
|
83
|
+
log.error('Execution failed', { taskId, error: msg });
|
|
84
|
+
publishStatus(bus, taskId, contextId, 'failed', `Error: ${msg}`, true);
|
|
85
|
+
bus.finished();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async cancelTask(taskId: string, bus: ExecutionEventBus): Promise<void> {
|
|
90
|
+
log.info('Cancel requested for OpenCode task', { taskId });
|
|
91
|
+
this.client?.abort();
|
|
92
|
+
publishStatus(bus, taskId, '', 'canceled', 'OpenCode task cancelled', true);
|
|
93
|
+
bus.finished();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private extractText(message: A2AMessage): string {
|
|
97
|
+
return message.parts
|
|
98
|
+
.filter((p) => {
|
|
99
|
+
const part = p as unknown as Record<string, unknown>;
|
|
100
|
+
const text = part.text as string | undefined;
|
|
101
|
+
return text !== undefined && text !== null;
|
|
102
|
+
})
|
|
103
|
+
.map((p) => (p as unknown as { text: string }).text)
|
|
104
|
+
.join('\n');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function createOpenCodeExecutor(config: Required<AgentConfig>): A2AExecutor {
|
|
111
|
+
return new OpenCodeExecutor(config);
|
|
112
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @axiom-lattice/cli-a2a
|
|
3
|
+
*
|
|
4
|
+
* Multi-provider A2A (Agent-to-Agent) CLI gateway.
|
|
5
|
+
* Wraps local coding agents as spec-compliant A2A servers.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* npx @axiom-lattice/cli-a2a --provider opencode --config agents/example.json
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createA2AServer, resolveConfig } from '@axiom-lattice/cli-a2a';
|
|
15
|
+
* const config = resolveConfig('path/to/config.json');
|
|
16
|
+
* const server = await createA2AServer(config);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Config
|
|
21
|
+
export { resolveConfig, loadConfigFile } from './config/index.js';
|
|
22
|
+
export type {
|
|
23
|
+
AgentConfig,
|
|
24
|
+
ProviderType,
|
|
25
|
+
AgentCardConfig,
|
|
26
|
+
SkillConfig,
|
|
27
|
+
ServerConfig,
|
|
28
|
+
SessionConfig,
|
|
29
|
+
FeatureFlags,
|
|
30
|
+
TimeoutConfig,
|
|
31
|
+
LoggingConfig,
|
|
32
|
+
OpenCodeConfig,
|
|
33
|
+
CodexConfig,
|
|
34
|
+
ClaudeCodeConfig,
|
|
35
|
+
} from './config/index.js';
|
|
36
|
+
|
|
37
|
+
// Server
|
|
38
|
+
export { createA2AServer } from './server/index.js';
|
|
39
|
+
export type { ServerHandle } from './server/index.js';
|
|
40
|
+
|
|
41
|
+
// Executors
|
|
42
|
+
export {
|
|
43
|
+
registerExecutor,
|
|
44
|
+
getExecutor,
|
|
45
|
+
createExecutor,
|
|
46
|
+
} from './executors/index.js';
|
|
47
|
+
export type { A2AExecutor, ExecutorFactory } from './executors/index.js';
|
|
48
|
+
export { createOpenCodeExecutor, OpenCodeExecutor } from './executors/opencode/executor.js';
|
|
49
|
+
export { createCodexExecutor, CodexExecutor } from './executors/codex/executor.js';
|
|
50
|
+
export { createClaudeExecutor, ClaudeExecutor } from './executors/claude/executor.js';
|
|
51
|
+
export type { AgentExecutor, RequestContext, ExecutionEventBus } from './executors/index.js';
|
|
52
|
+
|
|
53
|
+
// Logger
|
|
54
|
+
export { logger, LogLevel } from './logger.js';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight structured logger for the CLI A2A gateway.
|
|
3
|
+
*
|
|
4
|
+
* Uses pino-compatible structured logging. In production mode the output is
|
|
5
|
+
* JSON; in dev mode it is pretty-printed via pino-pretty.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export enum LogLevel {
|
|
9
|
+
DEBUG = 10,
|
|
10
|
+
INFO = 20,
|
|
11
|
+
WARN = 30,
|
|
12
|
+
ERROR = 40,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LogEntry {
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
msg: string;
|
|
18
|
+
ctx: Record<string, unknown>;
|
|
19
|
+
ts: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class Logger {
|
|
23
|
+
private level: LogLevel = LogLevel.INFO;
|
|
24
|
+
private name: string;
|
|
25
|
+
|
|
26
|
+
constructor(name: string) {
|
|
27
|
+
this.name = name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setLevel(level: LogLevel): void {
|
|
31
|
+
this.level = level;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
child(name: string): Logger {
|
|
35
|
+
const childLogger = new Logger(`${this.name}:${name}`);
|
|
36
|
+
childLogger.level = this.level;
|
|
37
|
+
return childLogger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debug(msg: string, ctx?: Record<string, unknown>): void {
|
|
41
|
+
this.log(LogLevel.DEBUG, msg, ctx);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
info(msg: string, ctx?: Record<string, unknown>): void {
|
|
45
|
+
this.log(LogLevel.INFO, msg, ctx);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warn(msg: string, ctx?: Record<string, unknown>): void {
|
|
49
|
+
this.log(LogLevel.WARN, msg, ctx);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
error(msg: string, ctx?: Record<string, unknown>): void {
|
|
53
|
+
this.log(LogLevel.ERROR, msg, ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private log(level: LogLevel, msg: string, ctx?: Record<string, unknown>): void {
|
|
57
|
+
if (level < this.level) return;
|
|
58
|
+
|
|
59
|
+
const entry: LogEntry = {
|
|
60
|
+
level,
|
|
61
|
+
msg,
|
|
62
|
+
ctx: { name: this.name, ...ctx },
|
|
63
|
+
ts: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const output = process.env.NODE_ENV === 'production'
|
|
67
|
+
? JSON.stringify(entry)
|
|
68
|
+
: `${entry.ts} [${LogLevel[entry.level]}] ${entry.ctx.name}: ${msg}${ctx && Object.keys(ctx).length > 1 ? ' ' + JSON.stringify(ctx) : ''}`;
|
|
69
|
+
|
|
70
|
+
if (level >= LogLevel.WARN) {
|
|
71
|
+
process.stderr.write(output + '\n');
|
|
72
|
+
} else {
|
|
73
|
+
process.stdout.write(output + '\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const logger = new Logger('cli-a2a');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Card builder.
|
|
3
|
+
*
|
|
4
|
+
* Builds an A2A-compliant AgentCard from the resolved config.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentCard, AgentSkill } from '@a2a-js/sdk';
|
|
8
|
+
import type { AgentConfig } from '../config/types.js';
|
|
9
|
+
import { logger } from '../logger.js';
|
|
10
|
+
|
|
11
|
+
const log = logger.child('agent-card');
|
|
12
|
+
|
|
13
|
+
export function buildAgentCard(config: Required<AgentConfig>): AgentCard {
|
|
14
|
+
const { agentCard, server } = config;
|
|
15
|
+
const proto = server.advertiseProtocol ?? 'http';
|
|
16
|
+
const host = server.advertiseHost ?? 'localhost';
|
|
17
|
+
const port = server.port ?? 3000;
|
|
18
|
+
const baseUrl = `${proto}://${host}:${port}`;
|
|
19
|
+
|
|
20
|
+
const skills: AgentSkill[] = (agentCard.skills ?? []).map((s) => ({
|
|
21
|
+
id: s.id,
|
|
22
|
+
name: s.name,
|
|
23
|
+
description: s.description,
|
|
24
|
+
tags: s.tags ?? [],
|
|
25
|
+
examples: s.examples ?? [],
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const card: AgentCard = {
|
|
29
|
+
name: agentCard.name,
|
|
30
|
+
description: agentCard.description,
|
|
31
|
+
protocolVersion: agentCard.protocolVersion ?? '0.3.0',
|
|
32
|
+
version: agentCard.version ?? '1.0.0',
|
|
33
|
+
url: `${baseUrl}/a2a/jsonrpc`,
|
|
34
|
+
// Default input/output modes (A2A v1.0 normalized names)
|
|
35
|
+
defaultInputModes: (agentCard.defaultInputModes as Array<'text' | 'file' | 'image' | 'audio'>) ?? ['text'],
|
|
36
|
+
defaultOutputModes: (agentCard.defaultOutputModes as Array<'text' | 'file' | 'image' | 'audio'>) ?? ['text'],
|
|
37
|
+
skills,
|
|
38
|
+
capabilities: {
|
|
39
|
+
streaming: agentCard.streaming ?? true,
|
|
40
|
+
pushNotifications: agentCard.pushNotifications ?? false,
|
|
41
|
+
},
|
|
42
|
+
// Additional interfaces advertised to orchestrators
|
|
43
|
+
additionalInterfaces: [
|
|
44
|
+
{ transport: 'JSONRPC', url: `${baseUrl}/a2a/jsonrpc` },
|
|
45
|
+
{ transport: 'REST', url: `${baseUrl}/a2a/rest` },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
log.info('Agent card built', { name: card.name, url: card.url, skills: card.skills?.length ?? 0 });
|
|
50
|
+
return card;
|
|
51
|
+
}
|