openclaw-a2a 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/index.js +952 -0
  4. package/package.json +78 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomas Grasl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # openclaw-a2a
2
+
3
+ > What happens when AI agents start talking to each other?
4
+ > Let's find out.
5
+
6
+ [![CI](https://github.com/freema/openclaw-a2a/actions/workflows/ci.yml/badge.svg)](https://github.com/freema/openclaw-a2a/actions/workflows/ci.yml)
7
+ [![npm](https://img.shields.io/npm/v/openclaw-a2a)](https://www.npmjs.com/package/openclaw-a2a)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ ![Status: Beta](https://img.shields.io/badge/status-beta-orange)
10
+ ![A2A: v1.0](https://img.shields.io/badge/A2A-v1.0-blue)
11
+
12
+ **openclaw-a2a** is a bridge that lets any A2A-compatible agent chat with your
13
+ self-hosted [OpenClaw](https://openclaw.ai) assistant. Think of it as giving your
14
+ OpenClaw a phone number that other agents can call.
15
+
16
+ This is a **beta experiment**. We're exploring the bleeding edge of agent-to-agent
17
+ communication using Google's [A2A protocol](https://google.github.io/A2A/) v1.0.
18
+ The JS SDK is still on v0.3, so we implemented v1.0 from scratch. YOLO.
19
+
20
+ ## The Idea
21
+
22
+ You already have OpenClaw running. Maybe on your laptop, maybe on a server.
23
+ It's smart, it has access to your tools, and it talks to your LLM.
24
+
25
+ Now imagine another agent — running somewhere else, built by someone else —
26
+ wants to ask your OpenClaw something. That's what A2A is for.
27
+
28
+ ```
29
+ Google ADK Agent ──┐
30
+ CrewAI Agent ──────┤── A2A Protocol ──> openclaw-a2a ──> OpenClaw Gateway
31
+ Your Custom Agent ─┤ |
32
+ Claude.ai* ────────┘ (yes, streaming!)
33
+
34
+ * via A2A-MCP bridge (because Claude speaks MCP, not A2A... yet)
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ### npm
40
+
41
+ ```bash
42
+ npm install -g openclaw-a2a@beta
43
+
44
+ # Start the bridge
45
+ openclaw-a2a --openclaw-url http://127.0.0.1:18789 --token your-token
46
+ ```
47
+
48
+ ### Docker
49
+
50
+ ```bash
51
+ docker run --rm -p 3100:3100 \
52
+ -e OPENCLAW_URL=http://host.docker.internal:18789 \
53
+ -e OPENCLAW_GATEWAY_TOKEN=your-token \
54
+ ghcr.io/freema/openclaw-a2a:beta
55
+ ```
56
+
57
+ ### From source
58
+
59
+ ```bash
60
+ git clone https://github.com/freema/openclaw-a2a.git
61
+ cd openclaw-a2a
62
+ npm install
63
+ cp .env.example .env # edit with your settings
64
+ npm run dev
65
+ ```
66
+
67
+ ## Try It
68
+
69
+ ```bash
70
+ # Discover the agent
71
+ curl http://localhost:3100/.well-known/agent-card.json | jq .
72
+
73
+ # Send a message (A2A v1.0)
74
+ curl -X POST http://localhost:3100/a2a \
75
+ -H "Content-Type: application/json" \
76
+ -H "A2A-Version: 1.0" \
77
+ -d '{
78
+ "jsonrpc": "2.0", "id": "1", "method": "SendMessage",
79
+ "params": {
80
+ "message": {
81
+ "messageId": "test-1",
82
+ "role": "ROLE_USER",
83
+ "parts": [{ "text": "Hello from A2A v1.0!" }]
84
+ }
85
+ }
86
+ }'
87
+
88
+ # Stream a response
89
+ curl -N -X POST http://localhost:3100/a2a \
90
+ -H "Content-Type: application/json" \
91
+ -H "A2A-Version: 1.0" \
92
+ -d '{
93
+ "jsonrpc": "2.0", "id": "2", "method": "SendStreamingMessage",
94
+ "params": {
95
+ "message": {
96
+ "messageId": "test-2",
97
+ "role": "ROLE_USER",
98
+ "parts": [{ "text": "Stream me something cool!" }]
99
+ }
100
+ }
101
+ }'
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ | Env Variable | Default | Description |
107
+ |---|---|---|
108
+ | `OPENCLAW_URL` | — | OpenClaw Gateway URL (required) |
109
+ | `OPENCLAW_GATEWAY_TOKEN` | — | Bearer token for gateway auth |
110
+ | `OPENCLAW_INSTANCES` | — | JSON array for multi-instance routing |
111
+ | `PORT` | `3100` | Server port |
112
+ | `HOST` | `0.0.0.0` | Server host |
113
+ | `PUBLIC_URL` | `http://localhost:3100` | Public URL for Agent Card |
114
+ | `DEBUG` | `false` | Enable debug logging |
115
+
116
+ ### Multi-instance
117
+
118
+ Route requests to different OpenClaw instances based on message metadata:
119
+
120
+ ```bash
121
+ export OPENCLAW_INSTANCES='[
122
+ {"name":"prod","url":"http://prod:18789","token":"t1","default":true},
123
+ {"name":"staging","url":"http://staging:18789","token":"t2"}
124
+ ]'
125
+ ```
126
+
127
+ Then send with `"metadata": { "instance": "staging" }` in your A2A message.
128
+
129
+ ### CLI Options
130
+
131
+ ```bash
132
+ openclaw-a2a --help
133
+
134
+ Options:
135
+ --port, -p Server port [number]
136
+ --host Server host [string]
137
+ --openclaw-url OpenClaw Gateway URL [string]
138
+ --token OpenClaw Gateway token [string]
139
+ --debug Enable debug logging [boolean]
140
+ ```
141
+
142
+ ## What's Inside
143
+
144
+ This implements the **A2A v1.0 specification** — the full protocol, not a subset:
145
+
146
+ | Feature | Status |
147
+ |---|---|
148
+ | Agent Card discovery (`.well-known/agent-card.json`) | Done |
149
+ | SendMessage (sync) | Done |
150
+ | SendStreamingMessage (SSE) | Done |
151
+ | GetTask / ListTasks | Done |
152
+ | CancelTask | Done |
153
+ | Multi-turn conversations (INPUT_REQUIRED) | Done |
154
+ | Multi-instance routing | Done |
155
+ | A2A-Version header validation | Done |
156
+ | Push notifications | Stub (returns UNSUPPORTED) |
157
+ | Extended Agent Card | Stub (returns UNSUPPORTED) |
158
+ | SubscribeToTask | Stub (returns UNSUPPORTED) |
159
+
160
+ ### v1.0 Spec Compliance
161
+
162
+ We implement A2A v1.0 directly from the [proto spec](https://github.com/a2aproject/A2A):
163
+
164
+ - PascalCase method names (`SendMessage`, not `message/send`)
165
+ - SCREAMING_SNAKE enum values (`TASK_STATE_COMPLETED`, not `completed`)
166
+ - Part discrimination by field presence (not `kind`)
167
+ - `Task.id` (not `taskId`)
168
+ - `A2A-Version` header with fallback to `0.3` (rejected — we only support 1.0)
169
+ - Cursor-based pagination for ListTasks
170
+ - `supportedInterfaces[]` in Agent Card (not top-level `url`)
171
+
172
+ ## Who Can Talk To This?
173
+
174
+ ### Native A2A clients
175
+
176
+ | Client | Notes |
177
+ |---|---|
178
+ | [Google ADK](https://google.github.io/adk-docs/) | `RemoteA2aAgent` — most mature |
179
+ | [CrewAI](https://crewai.com/) >= v1.10 | Built-in A2A support |
180
+ | [Microsoft Agent Framework](https://github.com/microsoft/agents) | .NET + Python |
181
+ | [BeeAI](https://beeai.dev/) | A2A adapter |
182
+ | [LangGraph](https://langchain-ai.github.io/langgraph/) | Server + client |
183
+ | curl / any HTTP client | JSON-RPC over HTTP |
184
+
185
+ ### Via A2A-MCP bridge (for Claude)
186
+
187
+ Claude doesn't speak A2A natively (it speaks MCP). But there's a bridge:
188
+
189
+ 1. Run `openclaw-a2a` (this project) — gives OpenClaw an A2A interface
190
+ 2. Add an [A2A-MCP bridge](https://github.com/GongRzhe/A2A-MCP-Server) to Claude
191
+ 3. Claude discovers and chats with your OpenClaw through A2A
192
+
193
+ Is this useful? Maybe. Is it cool? Definitely.
194
+
195
+ ## Development
196
+
197
+ ```bash
198
+ # Dev server (hot reload)
199
+ npm run dev
200
+
201
+ # Run tests (watch mode)
202
+ npm run test
203
+
204
+ # Full quality check
205
+ npm run check:all # lint + typecheck + test + build
206
+
207
+ # Format
208
+ npm run format
209
+ ```
210
+
211
+ ### Testing
212
+
213
+ - **84 unit + integration tests** — run without external dependencies
214
+ - **E2E tests** — require a real OpenClaw Gateway (optional)
215
+
216
+ ```bash
217
+ # Unit + integration (no gateway needed)
218
+ npm run test:run
219
+
220
+ # E2E (requires gateway on localhost:18789)
221
+ export OPENCLAW_URL=http://127.0.0.1:18789
222
+ export OPENCLAW_GATEWAY_TOKEN=your-token
223
+ npm run test:e2e
224
+ ```
225
+
226
+ ## Docker Deployment
227
+
228
+ ```bash
229
+ # Build locally
230
+ docker build -t openclaw-a2a .
231
+
232
+ # Or use Docker Compose
233
+ cp .env.example .env
234
+ docker compose up -d
235
+
236
+ # Dev mode (build from source)
237
+ docker compose -f docker-compose.yml -f docker-compose.dev.yml up
238
+ ```
239
+
240
+ ## Sister Project
241
+
242
+ This is the A2A sibling of [openclaw-mcp](https://github.com/freema/openclaw-mcp),
243
+ which bridges OpenClaw to Claude via MCP. Different protocols, same OpenClaw.
244
+
245
+ | | openclaw-mcp | openclaw-a2a |
246
+ |---|---|---|
247
+ | Protocol | MCP (Anthropic) | A2A v1.0 (Google/Linux Foundation) |
248
+ | For | Claude.ai, Claude Desktop | Any A2A agent |
249
+ | Port | 3000 | 3100 |
250
+ | Status | Stable | Beta |
251
+
252
+ ```
253
+ MCP = how an agent talks to TOOLS (vertical: agent → tools)
254
+ A2A = how AGENTS talk to EACH OTHER (horizontal: agent ↔ agent)
255
+ ```
256
+
257
+ They're complementary, not competing.
258
+
259
+ ## License
260
+
261
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,952 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import yargs from "yargs";
5
+ import { hideBin } from "yargs/helpers";
6
+
7
+ // src/config/index.ts
8
+ function parseInstances() {
9
+ const raw = process.env.OPENCLAW_INSTANCES;
10
+ if (raw) {
11
+ try {
12
+ const parsed = JSON.parse(raw);
13
+ if (!Array.isArray(parsed) || parsed.length === 0) {
14
+ throw new Error("OPENCLAW_INSTANCES must be a non-empty JSON array");
15
+ }
16
+ for (const inst of parsed) {
17
+ if (!inst.name || !inst.url) {
18
+ throw new Error(`Each instance must have "name" and "url". Got: ${JSON.stringify(inst)}`);
19
+ }
20
+ }
21
+ const defaults = parsed.filter((i) => i.default);
22
+ if (defaults.length === 0) {
23
+ parsed[0].default = true;
24
+ }
25
+ return parsed;
26
+ } catch (e) {
27
+ if (e.message.includes("OPENCLAW_INSTANCES")) throw e;
28
+ throw new Error(`Failed to parse OPENCLAW_INSTANCES: ${e.message}`);
29
+ }
30
+ }
31
+ const url = process.env.OPENCLAW_URL;
32
+ if (!url) {
33
+ throw new Error("OPENCLAW_URL or OPENCLAW_INSTANCES must be set");
34
+ }
35
+ return [
36
+ {
37
+ name: "default",
38
+ url: url.replace(/\/+$/, ""),
39
+ token: process.env.OPENCLAW_GATEWAY_TOKEN ?? "",
40
+ default: true
41
+ }
42
+ ];
43
+ }
44
+ function loadConfig() {
45
+ const port = parseInt(process.env.PORT ?? "3100", 10);
46
+ const host = process.env.HOST ?? "0.0.0.0";
47
+ const debug = process.env.DEBUG === "true";
48
+ const publicUrl = (process.env.PUBLIC_URL ?? `http://localhost:${port}`).replace(/\/+$/, "");
49
+ const instances = parseInstances();
50
+ return { port, host, debug, publicUrl, instances };
51
+ }
52
+ function getDefaultInstance(config2) {
53
+ return config2.instances.find((i) => i.default) ?? config2.instances[0];
54
+ }
55
+ function getInstanceByName(config2, name) {
56
+ return config2.instances.find((i) => i.name === name);
57
+ }
58
+
59
+ // src/server/index.ts
60
+ import express from "express";
61
+
62
+ // src/a2a/executor.ts
63
+ import { v4 as uuid } from "uuid";
64
+
65
+ // src/utils/logger.ts
66
+ var debugEnabled = false;
67
+ function setDebug(enabled) {
68
+ debugEnabled = enabled;
69
+ }
70
+ function formatLog(level, message, data) {
71
+ const entry = {
72
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
73
+ level,
74
+ message
75
+ };
76
+ if (data) Object.assign(entry, data);
77
+ return JSON.stringify(entry);
78
+ }
79
+ function log(message, data) {
80
+ console.log(formatLog("info", message, data));
81
+ }
82
+ function logError(message, error, data) {
83
+ const extra = { ...data };
84
+ if (error instanceof Error) {
85
+ extra.error = error.message;
86
+ extra.stack = error.stack;
87
+ } else if (error !== void 0) {
88
+ extra.error = String(error);
89
+ }
90
+ console.error(formatLog("error", message, extra));
91
+ }
92
+ function logDebug(message, data) {
93
+ if (debugEnabled) {
94
+ console.log(formatLog("debug", message, data));
95
+ }
96
+ }
97
+
98
+ // src/openclaw/client.ts
99
+ var DEFAULT_TIMEOUT = 12e4;
100
+ var MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
101
+ var OpenClawError = class extends Error {
102
+ statusCode;
103
+ constructor(message, statusCode) {
104
+ super(message);
105
+ this.name = "OpenClawError";
106
+ this.statusCode = statusCode;
107
+ }
108
+ };
109
+ var OpenClawClient = class {
110
+ baseUrl;
111
+ token;
112
+ timeout;
113
+ constructor(instance, timeout = DEFAULT_TIMEOUT) {
114
+ this.baseUrl = instance.url.replace(/\/+$/, "");
115
+ this.token = instance.token;
116
+ this.timeout = timeout;
117
+ }
118
+ async health() {
119
+ const res = await fetch(`${this.baseUrl}/health`, {
120
+ signal: AbortSignal.timeout(5e3)
121
+ });
122
+ if (!res.ok) {
123
+ throw new OpenClawError(`Health check failed: ${res.status}`, res.status);
124
+ }
125
+ return await res.json();
126
+ }
127
+ async chat(message) {
128
+ const url = `${this.baseUrl}/v1/chat/completions`;
129
+ const body = {
130
+ model: "openclaw",
131
+ messages: [{ role: "user", content: message }],
132
+ stream: false
133
+ };
134
+ logDebug("OpenClaw chat request", { url, messageLength: message.length });
135
+ const controller = new AbortController();
136
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
137
+ try {
138
+ const res = await fetch(url, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
143
+ },
144
+ body: JSON.stringify(body),
145
+ signal: controller.signal
146
+ });
147
+ if (!res.ok) {
148
+ const text = await res.text().catch(() => "");
149
+ throw new OpenClawError(
150
+ `OpenClaw API error: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`,
151
+ res.status
152
+ );
153
+ }
154
+ const contentLength = res.headers.get("content-length");
155
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
156
+ throw new OpenClawError(
157
+ `Response too large: ${contentLength} bytes (max ${MAX_RESPONSE_SIZE})`
158
+ );
159
+ }
160
+ const data = await res.json();
161
+ logDebug("OpenClaw chat response", { id: data.id, choices: data.choices?.length });
162
+ return data;
163
+ } catch (e) {
164
+ if (e instanceof OpenClawError) throw e;
165
+ if (e.name === "AbortError") {
166
+ throw new OpenClawError(`Request timeout after ${this.timeout}ms`);
167
+ }
168
+ throw new OpenClawError(`Connection error: ${e.message}`);
169
+ } finally {
170
+ clearTimeout(timeoutId);
171
+ }
172
+ }
173
+ async *chatStream(message, abortSignal) {
174
+ const url = `${this.baseUrl}/v1/chat/completions`;
175
+ const body = {
176
+ model: "openclaw",
177
+ messages: [{ role: "user", content: message }],
178
+ stream: true
179
+ };
180
+ logDebug("OpenClaw stream request", { url, messageLength: message.length });
181
+ const controller = new AbortController();
182
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
183
+ if (abortSignal) {
184
+ abortSignal.addEventListener("abort", () => controller.abort(), { once: true });
185
+ }
186
+ try {
187
+ const res = await fetch(url, {
188
+ method: "POST",
189
+ headers: {
190
+ "Content-Type": "application/json",
191
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
192
+ },
193
+ body: JSON.stringify(body),
194
+ signal: controller.signal
195
+ });
196
+ if (!res.ok) {
197
+ const text = await res.text().catch(() => "");
198
+ throw new OpenClawError(
199
+ `OpenClaw API error: ${res.status} ${res.statusText}${text ? ` \u2014 ${text}` : ""}`,
200
+ res.status
201
+ );
202
+ }
203
+ if (!res.body) {
204
+ throw new OpenClawError("Response body is null \u2014 streaming not supported?");
205
+ }
206
+ const reader = res.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ while (true) {
210
+ const { done, value } = await reader.read();
211
+ if (done) break;
212
+ buffer += decoder.decode(value, { stream: true });
213
+ const lines = buffer.split("\n");
214
+ buffer = lines.pop() ?? "";
215
+ for (const line of lines) {
216
+ if (!line.startsWith("data: ")) continue;
217
+ const data = line.slice(6).trim();
218
+ if (data === "[DONE]") return;
219
+ if (!data) continue;
220
+ try {
221
+ const chunk = JSON.parse(data);
222
+ const content = chunk.choices?.[0]?.delta?.content;
223
+ if (content) yield content;
224
+ } catch {
225
+ logError("Failed to parse SSE chunk", void 0, { data });
226
+ }
227
+ }
228
+ }
229
+ } catch (e) {
230
+ if (e instanceof OpenClawError) throw e;
231
+ if (e.name === "AbortError") {
232
+ logDebug("Stream aborted");
233
+ return;
234
+ }
235
+ throw new OpenClawError(`Stream error: ${e.message}`);
236
+ } finally {
237
+ clearTimeout(timeoutId);
238
+ }
239
+ }
240
+ };
241
+
242
+ // src/a2a/errors.ts
243
+ var A2A_ERROR_CODES = {
244
+ // Standard JSON-RPC
245
+ PARSE_ERROR: -32700,
246
+ INVALID_REQUEST: -32600,
247
+ METHOD_NOT_FOUND: -32601,
248
+ INVALID_PARAMS: -32602,
249
+ INTERNAL_ERROR: -32603,
250
+ // A2A specific
251
+ TASK_NOT_FOUND: -32001,
252
+ TASK_NOT_CANCELABLE: -32002,
253
+ PUSH_NOTIFICATION_NOT_SUPPORTED: -32003,
254
+ UNSUPPORTED_OPERATION: -32004,
255
+ CONTENT_TYPE_NOT_SUPPORTED: -32005,
256
+ EXTENSION_SUPPORT_REQUIRED: -32008,
257
+ VERSION_NOT_SUPPORTED: -32009
258
+ };
259
+ var A2AError = class extends Error {
260
+ code;
261
+ data;
262
+ constructor(code, message, data) {
263
+ super(message);
264
+ this.name = "A2AError";
265
+ this.code = code;
266
+ this.data = data;
267
+ }
268
+ toJSON() {
269
+ return {
270
+ code: this.code,
271
+ message: this.message,
272
+ ...this.data !== void 0 ? { data: this.data } : {}
273
+ };
274
+ }
275
+ };
276
+
277
+ // src/a2a/types/enums.ts
278
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
279
+ "TASK_STATE_COMPLETED" /* COMPLETED */,
280
+ "TASK_STATE_FAILED" /* FAILED */,
281
+ "TASK_STATE_CANCELED" /* CANCELED */,
282
+ "TASK_STATE_REJECTED" /* REJECTED */
283
+ ]);
284
+
285
+ // src/a2a/executor.ts
286
+ var HEARTBEAT_INTERVAL = 15e3;
287
+ var OpenClawExecutor = class {
288
+ constructor(config2, taskStore) {
289
+ this.config = config2;
290
+ this.taskStore = taskStore;
291
+ }
292
+ clients = /* @__PURE__ */ new Map();
293
+ canceledTasks = /* @__PURE__ */ new Set();
294
+ getClient(instance) {
295
+ let client = this.clients.get(instance.name);
296
+ if (!client) {
297
+ client = new OpenClawClient(instance);
298
+ this.clients.set(instance.name, client);
299
+ }
300
+ return client;
301
+ }
302
+ resolveInstance(metadata) {
303
+ const instanceName = metadata?.instance;
304
+ if (instanceName) {
305
+ const inst = getInstanceByName(this.config, instanceName);
306
+ if (!inst) {
307
+ throw new A2AError(A2A_ERROR_CODES.INVALID_PARAMS, `Unknown instance: "${instanceName}"`);
308
+ }
309
+ return inst;
310
+ }
311
+ return getDefaultInstance(this.config);
312
+ }
313
+ extractText(message) {
314
+ return message.parts.filter((p) => p.text !== void 0).map((p) => p.text).join("\n");
315
+ }
316
+ cancelTask(taskId) {
317
+ this.canceledTasks.add(taskId);
318
+ }
319
+ isCanceled(taskId) {
320
+ return this.canceledTasks.has(taskId);
321
+ }
322
+ async execute(context, eventBus) {
323
+ const { task, contextId, userMessage } = context;
324
+ const text = this.extractText(userMessage);
325
+ if (!text) {
326
+ this.publishFailed(eventBus, task.id, contextId, "Empty message \u2014 no text parts found");
327
+ return;
328
+ }
329
+ if (this.isCanceled(task.id)) {
330
+ this.publishCanceled(eventBus, task.id, contextId);
331
+ return;
332
+ }
333
+ try {
334
+ const instance = this.resolveInstance(userMessage.metadata);
335
+ const client = this.getClient(instance);
336
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_WORKING" /* WORKING */);
337
+ const response = await client.chat(text);
338
+ const content = response.choices[0]?.message?.content ?? "";
339
+ if (this.isCanceled(task.id)) {
340
+ this.publishCanceled(eventBus, task.id, contextId);
341
+ return;
342
+ }
343
+ eventBus.publish({
344
+ artifactUpdate: {
345
+ taskId: task.id,
346
+ contextId,
347
+ artifact: {
348
+ artifactId: uuid(),
349
+ parts: [{ text: content }]
350
+ }
351
+ }
352
+ });
353
+ task.artifacts = [{ artifactId: uuid(), parts: [{ text: content }] }];
354
+ if (this.isInputRequired(content)) {
355
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_INPUT_REQUIRED" /* INPUT_REQUIRED */, {
356
+ messageId: uuid(),
357
+ role: "ROLE_AGENT" /* AGENT */,
358
+ parts: [{ text: content }]
359
+ });
360
+ task.status = { state: "TASK_STATE_INPUT_REQUIRED" /* INPUT_REQUIRED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
361
+ this.taskStore.set(task);
362
+ return;
363
+ }
364
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_COMPLETED" /* COMPLETED */);
365
+ task.status = { state: "TASK_STATE_COMPLETED" /* COMPLETED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
366
+ this.taskStore.set(task);
367
+ } catch (e) {
368
+ logError("Executor error", e, { taskId: task.id });
369
+ const msg = e instanceof OpenClawError ? e.message : "Internal execution error";
370
+ this.publishFailed(eventBus, task.id, contextId, msg);
371
+ task.status = { state: "TASK_STATE_FAILED" /* FAILED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
372
+ this.taskStore.set(task);
373
+ } finally {
374
+ eventBus.finish();
375
+ }
376
+ }
377
+ async executeStreaming(context, eventBus) {
378
+ const { task, contextId, userMessage } = context;
379
+ const text = this.extractText(userMessage);
380
+ if (!text) {
381
+ this.publishFailed(eventBus, task.id, contextId, "Empty message \u2014 no text parts found");
382
+ eventBus.finish();
383
+ return;
384
+ }
385
+ if (this.isCanceled(task.id)) {
386
+ this.publishCanceled(eventBus, task.id, contextId);
387
+ eventBus.finish();
388
+ return;
389
+ }
390
+ const abortController = new AbortController();
391
+ const heartbeat = setInterval(() => {
392
+ if (!eventBus.finished) {
393
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_WORKING" /* WORKING */);
394
+ }
395
+ }, HEARTBEAT_INTERVAL);
396
+ try {
397
+ const instance = this.resolveInstance(userMessage.metadata);
398
+ const client = this.getClient(instance);
399
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_WORKING" /* WORKING */);
400
+ const artifactId = uuid();
401
+ let fullContent = "";
402
+ for await (const chunk of client.chatStream(text, abortController.signal)) {
403
+ if (this.isCanceled(task.id)) {
404
+ abortController.abort();
405
+ this.publishCanceled(eventBus, task.id, contextId);
406
+ break;
407
+ }
408
+ fullContent += chunk;
409
+ eventBus.publish({
410
+ artifactUpdate: {
411
+ taskId: task.id,
412
+ contextId,
413
+ artifact: { artifactId, parts: [{ text: chunk }] },
414
+ append: true
415
+ }
416
+ });
417
+ }
418
+ if (!this.isCanceled(task.id)) {
419
+ eventBus.publish({
420
+ artifactUpdate: {
421
+ taskId: task.id,
422
+ contextId,
423
+ artifact: { artifactId, parts: [{ text: "" }] },
424
+ lastChunk: true
425
+ }
426
+ });
427
+ task.artifacts = [{ artifactId, parts: [{ text: fullContent }] }];
428
+ if (this.isInputRequired(fullContent)) {
429
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_INPUT_REQUIRED" /* INPUT_REQUIRED */, {
430
+ messageId: uuid(),
431
+ role: "ROLE_AGENT" /* AGENT */,
432
+ parts: [{ text: fullContent }]
433
+ });
434
+ task.status = { state: "TASK_STATE_INPUT_REQUIRED" /* INPUT_REQUIRED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
435
+ this.taskStore.set(task);
436
+ } else {
437
+ this.publishStatus(eventBus, task.id, contextId, "TASK_STATE_COMPLETED" /* COMPLETED */);
438
+ task.status = { state: "TASK_STATE_COMPLETED" /* COMPLETED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
439
+ this.taskStore.set(task);
440
+ }
441
+ }
442
+ } catch (e) {
443
+ logError("Streaming executor error", e, { taskId: task.id });
444
+ const msg = e instanceof OpenClawError ? e.message : "Internal execution error";
445
+ this.publishFailed(eventBus, task.id, contextId, msg);
446
+ task.status = { state: "TASK_STATE_FAILED" /* FAILED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
447
+ this.taskStore.set(task);
448
+ } finally {
449
+ clearInterval(heartbeat);
450
+ eventBus.finish();
451
+ }
452
+ }
453
+ // Multi-turn detection heuristic
454
+ // Returns true if the response indicates more user input is needed
455
+ isInputRequired(content) {
456
+ if (!content) return false;
457
+ if (content.includes("[INPUT_REQUIRED]") || content.includes("[NEEDS_INPUT]")) {
458
+ return false;
459
+ }
460
+ const trimmed = content.trim();
461
+ const lastSentence = trimmed.split(/[.!]\s/).pop()?.trim() ?? "";
462
+ if (lastSentence.endsWith("?") && trimmed.length < 500) {
463
+ return false;
464
+ }
465
+ return false;
466
+ }
467
+ publishStatus(eventBus, taskId, contextId, state, message) {
468
+ const status = {
469
+ state,
470
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
471
+ ...message ? { message } : {}
472
+ };
473
+ eventBus.publish({ statusUpdate: { taskId, contextId, status } });
474
+ logDebug("Status update", { taskId, state });
475
+ }
476
+ publishFailed(eventBus, taskId, contextId, errorMsg) {
477
+ this.publishStatus(eventBus, taskId, contextId, "TASK_STATE_FAILED" /* FAILED */, {
478
+ messageId: uuid(),
479
+ role: "ROLE_AGENT" /* AGENT */,
480
+ parts: [{ text: errorMsg }]
481
+ });
482
+ }
483
+ publishCanceled(eventBus, taskId, contextId) {
484
+ this.publishStatus(eventBus, taskId, contextId, "TASK_STATE_CANCELED" /* CANCELED */);
485
+ const task = this.taskStore.get(taskId);
486
+ if (task) {
487
+ task.status = { state: "TASK_STATE_CANCELED" /* CANCELED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
488
+ this.taskStore.set(task);
489
+ }
490
+ this.canceledTasks.delete(taskId);
491
+ }
492
+ };
493
+
494
+ // src/a2a/router.ts
495
+ import { Router } from "express";
496
+
497
+ // src/a2a/agent-card.ts
498
+ function buildAgentCard(config2) {
499
+ return {
500
+ name: "OpenClaw A2A Bridge",
501
+ description: "A2A v1.0 bridge to OpenClaw AI assistant gateway",
502
+ version: true ? "0.1.0-beta.1" : "0.1.0-beta.1",
503
+ provider: {
504
+ organization: "OpenClaw",
505
+ url: "https://github.com/freema/openclaw-a2a"
506
+ },
507
+ supportedInterfaces: [
508
+ {
509
+ url: config2.publicUrl,
510
+ protocolBinding: "JSONRPC",
511
+ protocolVersion: "1.0"
512
+ }
513
+ ],
514
+ capabilities: {
515
+ streaming: true,
516
+ pushNotifications: false,
517
+ stateTransitionHistory: false,
518
+ extendedAgentCard: false
519
+ },
520
+ defaultInputModes: ["text/plain"],
521
+ defaultOutputModes: ["text/plain"],
522
+ skills: [
523
+ {
524
+ id: "openclaw-chat",
525
+ name: "OpenClaw Chat",
526
+ description: "Chat with OpenClaw AI assistant",
527
+ tags: ["chat", "ai", "assistant"],
528
+ examples: ["Hello!", "What can you help me with?"]
529
+ }
530
+ ]
531
+ };
532
+ }
533
+
534
+ // src/a2a/request-handler.ts
535
+ import { v4 as uuid2 } from "uuid";
536
+
537
+ // src/a2a/event-bus.ts
538
+ var ExecutionEventBus = class {
539
+ listeners = [];
540
+ finishListeners = [];
541
+ _finished = false;
542
+ get finished() {
543
+ return this._finished;
544
+ }
545
+ on(listener) {
546
+ this.listeners.push(listener);
547
+ return () => {
548
+ this.listeners = this.listeners.filter((l) => l !== listener);
549
+ };
550
+ }
551
+ onFinish(listener) {
552
+ if (this._finished) {
553
+ listener();
554
+ return () => {
555
+ };
556
+ }
557
+ this.finishListeners.push(listener);
558
+ return () => {
559
+ this.finishListeners = this.finishListeners.filter((l) => l !== listener);
560
+ };
561
+ }
562
+ publish(event) {
563
+ for (const listener of this.listeners) {
564
+ listener(event);
565
+ }
566
+ }
567
+ finish() {
568
+ this._finished = true;
569
+ for (const listener of this.finishListeners) {
570
+ listener();
571
+ }
572
+ this.listeners = [];
573
+ this.finishListeners = [];
574
+ }
575
+ };
576
+
577
+ // src/a2a/sse.ts
578
+ var SSE_HEADERS = {
579
+ "Content-Type": "text/event-stream",
580
+ "Cache-Control": "no-cache",
581
+ Connection: "keep-alive",
582
+ "X-Accel-Buffering": "no"
583
+ };
584
+ function formatSSEEvent(data) {
585
+ return `data: ${JSON.stringify(data)}
586
+
587
+ `;
588
+ }
589
+
590
+ // src/a2a/request-handler.ts
591
+ function createRequestHandler(config2, taskStore, executor) {
592
+ return async (req, res) => {
593
+ try {
594
+ resolveAndValidateVersion(req);
595
+ const rpcReq = parseJsonRpcRequest(req.body);
596
+ logDebug("JSON-RPC request", { method: rpcReq.method, id: rpcReq.id });
597
+ const method = rpcReq.method;
598
+ switch (method) {
599
+ case "SendMessage":
600
+ return await handleSendMessage(rpcReq, res, config2, taskStore, executor);
601
+ case "SendStreamingMessage":
602
+ return await handleSendStreamingMessage(rpcReq, res, config2, taskStore, executor);
603
+ case "GetTask":
604
+ return handleGetTask(rpcReq, res, taskStore);
605
+ case "ListTasks":
606
+ return handleListTasks(rpcReq, res, taskStore);
607
+ case "CancelTask":
608
+ return handleCancelTask(rpcReq, res, taskStore, executor);
609
+ case "SubscribeToTask":
610
+ return handleSubscribeToTask(rpcReq, res);
611
+ case "CreateTaskPushNotificationConfig":
612
+ case "GetTaskPushNotificationConfig":
613
+ case "ListTaskPushNotificationConfigs":
614
+ case "DeleteTaskPushNotificationConfig":
615
+ return sendJsonRpcError(
616
+ res,
617
+ rpcReq.id,
618
+ A2A_ERROR_CODES.PUSH_NOTIFICATION_NOT_SUPPORTED,
619
+ "Push notifications are not supported"
620
+ );
621
+ case "GetExtendedAgentCard":
622
+ return sendJsonRpcError(
623
+ res,
624
+ rpcReq.id,
625
+ A2A_ERROR_CODES.UNSUPPORTED_OPERATION,
626
+ "Extended agent card is not supported"
627
+ );
628
+ default:
629
+ return sendJsonRpcError(
630
+ res,
631
+ rpcReq.id,
632
+ A2A_ERROR_CODES.METHOD_NOT_FOUND,
633
+ `Unknown method: ${method}`
634
+ );
635
+ }
636
+ } catch (e) {
637
+ if (e instanceof A2AError) {
638
+ return sendJsonRpcError(res, req.body?.id ?? null, e.code, e.message, e.data);
639
+ }
640
+ logError("Unhandled request error", e);
641
+ return sendJsonRpcError(
642
+ res,
643
+ req.body?.id ?? null,
644
+ A2A_ERROR_CODES.INTERNAL_ERROR,
645
+ "Internal server error"
646
+ );
647
+ }
648
+ };
649
+ }
650
+ function resolveAndValidateVersion(req) {
651
+ const raw = req.header("A2A-Version")?.trim() || req.query["A2A-Version"];
652
+ const version = raw || "0.3";
653
+ if (version !== "1.0") {
654
+ throw new A2AError(
655
+ A2A_ERROR_CODES.VERSION_NOT_SUPPORTED,
656
+ `A2A version "${version}" is not supported. Supported versions: ["1.0"]`,
657
+ { supportedVersions: ["1.0"] }
658
+ );
659
+ }
660
+ return version;
661
+ }
662
+ function parseJsonRpcRequest(body) {
663
+ if (!body || typeof body !== "object") {
664
+ throw new A2AError(A2A_ERROR_CODES.PARSE_ERROR, "Invalid JSON");
665
+ }
666
+ if (body.jsonrpc !== "2.0") {
667
+ throw new A2AError(
668
+ A2A_ERROR_CODES.INVALID_REQUEST,
669
+ 'Missing or invalid jsonrpc version (must be "2.0")'
670
+ );
671
+ }
672
+ if (!body.method || typeof body.method !== "string") {
673
+ throw new A2AError(A2A_ERROR_CODES.INVALID_REQUEST, "Missing or invalid method");
674
+ }
675
+ return body;
676
+ }
677
+ async function handleSendMessage(rpcReq, res, config2, taskStore, executor) {
678
+ const params = rpcReq.params;
679
+ if (!params?.message) {
680
+ return sendJsonRpcError(
681
+ res,
682
+ rpcReq.id,
683
+ A2A_ERROR_CODES.INVALID_PARAMS,
684
+ "Missing message in params"
685
+ );
686
+ }
687
+ const context = createTaskContext(params, taskStore);
688
+ const eventBus = new ExecutionEventBus();
689
+ await executor.execute(context, eventBus);
690
+ const task = taskStore.get(context.task.id) ?? context.task;
691
+ return sendJsonRpcResult(res, rpcReq.id, task);
692
+ }
693
+ async function handleSendStreamingMessage(rpcReq, res, config2, taskStore, executor) {
694
+ const params = rpcReq.params;
695
+ if (!params?.message) {
696
+ return sendJsonRpcError(
697
+ res,
698
+ rpcReq.id,
699
+ A2A_ERROR_CODES.INVALID_PARAMS,
700
+ "Missing message in params"
701
+ );
702
+ }
703
+ const context = createTaskContext(params, taskStore);
704
+ const eventBus = new ExecutionEventBus();
705
+ res.writeHead(200, SSE_HEADERS);
706
+ eventBus.on((event) => {
707
+ const rpcResponse = {
708
+ jsonrpc: "2.0",
709
+ id: rpcReq.id,
710
+ result: event
711
+ };
712
+ res.write(formatSSEEvent(rpcResponse));
713
+ });
714
+ eventBus.onFinish(() => {
715
+ res.end();
716
+ });
717
+ executor.executeStreaming(context, eventBus).catch((e) => {
718
+ logError("Streaming execution error", e);
719
+ if (!res.writableEnded) {
720
+ res.end();
721
+ }
722
+ });
723
+ }
724
+ function handleGetTask(rpcReq, res, taskStore) {
725
+ const params = rpcReq.params;
726
+ if (!params?.id) {
727
+ return sendJsonRpcError(res, rpcReq.id, A2A_ERROR_CODES.INVALID_PARAMS, "Missing task id");
728
+ }
729
+ const task = taskStore.get(params.id);
730
+ if (!task) {
731
+ return sendJsonRpcError(
732
+ res,
733
+ rpcReq.id,
734
+ A2A_ERROR_CODES.TASK_NOT_FOUND,
735
+ `Task not found: ${params.id}`
736
+ );
737
+ }
738
+ if (params.historyLength !== void 0 && task.history) {
739
+ task.history = task.history.slice(-params.historyLength);
740
+ }
741
+ return sendJsonRpcResult(res, rpcReq.id, task);
742
+ }
743
+ function handleListTasks(rpcReq, res, taskStore) {
744
+ const params = rpcReq.params ?? {};
745
+ const result = taskStore.list({
746
+ contextId: params.contextId,
747
+ taskStates: params.taskStates,
748
+ cursor: params.cursor,
749
+ pageSize: params.pageSize
750
+ });
751
+ return sendJsonRpcResult(res, rpcReq.id, result);
752
+ }
753
+ function handleCancelTask(rpcReq, res, taskStore, executor) {
754
+ const params = rpcReq.params;
755
+ if (!params?.id) {
756
+ return sendJsonRpcError(res, rpcReq.id, A2A_ERROR_CODES.INVALID_PARAMS, "Missing task id");
757
+ }
758
+ const task = taskStore.get(params.id);
759
+ if (!task) {
760
+ return sendJsonRpcError(
761
+ res,
762
+ rpcReq.id,
763
+ A2A_ERROR_CODES.TASK_NOT_FOUND,
764
+ `Task not found: ${params.id}`
765
+ );
766
+ }
767
+ if (TERMINAL_STATES.has(task.status.state)) {
768
+ return sendJsonRpcError(
769
+ res,
770
+ rpcReq.id,
771
+ A2A_ERROR_CODES.TASK_NOT_CANCELABLE,
772
+ `Task is in terminal state: ${task.status.state}`
773
+ );
774
+ }
775
+ executor.cancelTask(params.id);
776
+ task.status = { state: "TASK_STATE_CANCELED" /* CANCELED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
777
+ taskStore.set(task);
778
+ return sendJsonRpcResult(res, rpcReq.id, task);
779
+ }
780
+ function handleSubscribeToTask(rpcReq, res) {
781
+ return sendJsonRpcError(
782
+ res,
783
+ rpcReq.id,
784
+ A2A_ERROR_CODES.UNSUPPORTED_OPERATION,
785
+ "SubscribeToTask is not yet supported"
786
+ );
787
+ }
788
+ function createTaskContext(params, taskStore) {
789
+ const message = params.message;
790
+ let contextId;
791
+ let existingTask;
792
+ if (message.taskId) {
793
+ existingTask = taskStore.get(message.taskId);
794
+ if (existingTask) {
795
+ contextId = message.contextId ?? existingTask.contextId;
796
+ } else {
797
+ contextId = message.contextId ?? uuid2();
798
+ }
799
+ } else {
800
+ contextId = message.contextId ?? uuid2();
801
+ }
802
+ const task = existingTask ?? {
803
+ id: uuid2(),
804
+ contextId,
805
+ status: { state: "TASK_STATE_SUBMITTED" /* SUBMITTED */, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
806
+ history: [],
807
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
808
+ };
809
+ if (!task.history) task.history = [];
810
+ task.history.push(message);
811
+ taskStore.set(task);
812
+ return { task, contextId, userMessage: message };
813
+ }
814
+ function sendJsonRpcResult(res, id, result) {
815
+ const response = { jsonrpc: "2.0", id, result };
816
+ res.json(response);
817
+ }
818
+ function sendJsonRpcError(res, id, code, message, data) {
819
+ const response = {
820
+ jsonrpc: "2.0",
821
+ id: id ?? 0,
822
+ error: { code, message, ...data !== void 0 ? { data } : {} }
823
+ };
824
+ res.json(response);
825
+ }
826
+
827
+ // src/a2a/router.ts
828
+ function createA2ARouter(config2, taskStore, executor) {
829
+ const router = Router();
830
+ const handler = createRequestHandler(config2, taskStore, executor);
831
+ router.get("/.well-known/agent-card.json", (_req, res) => {
832
+ res.json(buildAgentCard({ publicUrl: config2.publicUrl }));
833
+ });
834
+ router.post("/a2a", handler);
835
+ return router;
836
+ }
837
+
838
+ // src/a2a/task-store.ts
839
+ var InMemoryTaskStore = class {
840
+ tasks = /* @__PURE__ */ new Map();
841
+ get(id) {
842
+ return this.tasks.get(id);
843
+ }
844
+ set(task) {
845
+ task.lastModified = (/* @__PURE__ */ new Date()).toISOString();
846
+ this.tasks.set(task.id, task);
847
+ }
848
+ list(options) {
849
+ let all = Array.from(this.tasks.values());
850
+ if (options?.contextId) {
851
+ all = all.filter((t) => t.contextId === options.contextId);
852
+ }
853
+ if (options?.taskStates && options.taskStates.length > 0) {
854
+ const states = new Set(options.taskStates);
855
+ all = all.filter((t) => states.has(t.status.state));
856
+ }
857
+ all.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
858
+ const pageSize = options?.pageSize ?? 50;
859
+ let startIndex = 0;
860
+ if (options?.cursor) {
861
+ const cursorIndex = all.findIndex((t) => t.id === options.cursor);
862
+ if (cursorIndex >= 0) startIndex = cursorIndex + 1;
863
+ }
864
+ const page = all.slice(startIndex, startIndex + pageSize);
865
+ const nextCursor = startIndex + pageSize < all.length ? all[startIndex + pageSize - 1]?.id : void 0;
866
+ return { tasks: page, nextCursor };
867
+ }
868
+ delete(id) {
869
+ return this.tasks.delete(id);
870
+ }
871
+ };
872
+
873
+ // src/server/index.ts
874
+ function createApp(config2) {
875
+ const app = express();
876
+ const taskStore = new InMemoryTaskStore();
877
+ const executor = new OpenClawExecutor(config2, taskStore);
878
+ app.use(express.json({ limit: "10mb" }));
879
+ app.get("/health", (_req, res) => {
880
+ res.json({
881
+ status: "ok",
882
+ version: "0.1.0-beta.1",
883
+ a2aVersion: "1.0",
884
+ uptime: process.uptime()
885
+ });
886
+ });
887
+ app.get("/instances", (_req, res) => {
888
+ res.json(
889
+ config2.instances.map((i) => ({
890
+ name: i.name,
891
+ url: i.url,
892
+ default: i.default ?? false
893
+ }))
894
+ );
895
+ });
896
+ app.use(createA2ARouter(config2, taskStore, executor));
897
+ return { app, taskStore, executor };
898
+ }
899
+ function startServer(config2) {
900
+ const { app } = createApp(config2);
901
+ const server = app.listen(config2.port, config2.host, () => {
902
+ log("Server started", {
903
+ port: config2.port,
904
+ host: config2.host,
905
+ publicUrl: config2.publicUrl,
906
+ instances: config2.instances.length
907
+ });
908
+ log(`Agent Card: ${config2.publicUrl}/.well-known/agent-card.json`);
909
+ log(`A2A endpoint: ${config2.publicUrl}/a2a`);
910
+ });
911
+ const shutdown = (signal) => {
912
+ log(`Received ${signal}, shutting down...`);
913
+ server.close(() => {
914
+ log("Server closed");
915
+ process.exit(0);
916
+ });
917
+ setTimeout(() => {
918
+ log("Forced shutdown");
919
+ process.exit(1);
920
+ }, 1e4);
921
+ };
922
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
923
+ process.on("SIGINT", () => shutdown("SIGINT"));
924
+ return server;
925
+ }
926
+
927
+ // src/index.ts
928
+ var argv = yargs(hideBin(process.argv)).scriptName("openclaw-a2a").version(true ? "0.1.0-beta.1" : "0.1.0-beta.1").option("port", {
929
+ alias: "p",
930
+ type: "number",
931
+ description: "Server port"
932
+ }).option("host", {
933
+ type: "string",
934
+ description: "Server host"
935
+ }).option("openclaw-url", {
936
+ type: "string",
937
+ description: "OpenClaw Gateway URL"
938
+ }).option("token", {
939
+ type: "string",
940
+ description: "OpenClaw Gateway token"
941
+ }).option("debug", {
942
+ type: "boolean",
943
+ description: "Enable debug logging"
944
+ }).parseSync();
945
+ if (argv.port) process.env.PORT = String(argv.port);
946
+ if (argv.host) process.env.HOST = argv.host;
947
+ if (argv["openclaw-url"]) process.env.OPENCLAW_URL = argv["openclaw-url"];
948
+ if (argv.token) process.env.OPENCLAW_GATEWAY_TOKEN = argv.token;
949
+ if (argv.debug) process.env.DEBUG = "true";
950
+ var config = loadConfig();
951
+ setDebug(config.debug);
952
+ startServer(config);
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "openclaw-a2a",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "A2A v1.0 bridge for OpenClaw AI assistant — agent-to-agent communication",
5
+ "author": "Tomas Grasl <https://www.tomasgrasl.cz/>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "openclaw-a2a": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "dev": "tsx watch src/index.ts",
14
+ "build": "tsup",
15
+ "start": "node dist/index.js",
16
+ "clean": "rm -rf dist",
17
+ "typecheck": "tsc --noEmit",
18
+ "lint": "eslint src --ext .ts",
19
+ "lint:fix": "eslint src --ext .ts --fix",
20
+ "format": "prettier --write \"src/**/*.ts\"",
21
+ "format:check": "prettier --check \"src/**/*.ts\"",
22
+ "check": "npm run lint:fix && npm run typecheck",
23
+ "check:all": "npm run check && npm run test:run && npm run build",
24
+ "prepublishOnly": "npm run clean && npm run build",
25
+ "test": "vitest",
26
+ "test:run": "vitest run",
27
+ "test:e2e": "vitest run --config tests/e2e/vitest.config.ts"
28
+ },
29
+ "keywords": [
30
+ "a2a",
31
+ "a2a-protocol",
32
+ "agent-to-agent",
33
+ "openclaw",
34
+ "ai-agent",
35
+ "bridge",
36
+ "google-a2a"
37
+ ],
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "dependencies": {
42
+ "express": "^4.21.2",
43
+ "uuid": "^11.1.0",
44
+ "yargs": "^17.7.2"
45
+ },
46
+ "devDependencies": {
47
+ "@types/express": "^4.17.21",
48
+ "@types/node": "^20.11.0",
49
+ "@types/supertest": "^6.0.2",
50
+ "@types/uuid": "^10.0.0",
51
+ "@types/yargs": "^17.0.32",
52
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
53
+ "@typescript-eslint/parser": "^6.21.0",
54
+ "eslint": "^8.57.1",
55
+ "eslint-config-prettier": "^9.1.0",
56
+ "eslint-plugin-prettier": "^5.2.1",
57
+ "prettier": "^3.3.3",
58
+ "supertest": "^7.1.0",
59
+ "tsup": "^8.0.0",
60
+ "tsx": "^4.7.0",
61
+ "typescript": "^5.3.3",
62
+ "vitest": "^2.0.0"
63
+ },
64
+ "files": [
65
+ "dist",
66
+ "README.md",
67
+ "LICENSE"
68
+ ],
69
+ "homepage": "https://github.com/freema/openclaw-a2a#readme",
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/freema/openclaw-a2a.git"
73
+ },
74
+ "publishConfig": {
75
+ "access": "public",
76
+ "tag": "beta"
77
+ }
78
+ }