a2acalling 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -58,10 +58,10 @@ npm install -g a2acalling
58
58
  npm install a2acalling
59
59
  ```
60
60
 
61
- ### For OpenClaw Users
61
+ ### Setup (Auto-Detect Runtime)
62
62
 
63
63
  ```bash
64
- # Auto setup (detects gateway and configures dashboard location)
64
+ # Auto setup (detects OpenClaw gateway/runtime or configures standalone mode)
65
65
  npx a2acalling setup
66
66
 
67
67
  # Or clone and install
@@ -72,8 +72,9 @@ node scripts/install-openclaw.js setup
72
72
  ```
73
73
 
74
74
  Setup behavior:
75
+ - Runtime auto-detects OpenClaw when available and falls back to generic mode if unavailable.
75
76
  - If OpenClaw gateway is detected, dashboard is exposed on gateway at `/a2a` (proxied to A2A backend).
76
- - If gateway is not detected, dashboard is served directly by A2A server at `/dashboard`.
77
+ - If OpenClaw is not detected, setup bootstraps standalone config + bridge templates and serves dashboard at `/dashboard`.
77
78
  - Setup prints the exact dashboard URL at the end.
78
79
 
79
80
  Before the first `a2a call`, the owner must set permissions and disclosure tiers. Run onboarding first:
@@ -325,6 +326,14 @@ app.listen(3001);
325
326
  | `A2A_HOSTNAME` | Hostname for invite URLs (required for creates) |
326
327
  | `A2A_PORT` | Server port (default: 3001) |
327
328
  | `A2A_CONFIG_DIR` | Config directory (default: `~/.config/openclaw`) |
329
+ | `A2A_WORKSPACE` | Workspace root for context files like `USER.md` (default: current directory) |
330
+ | `A2A_RUNTIME` | Runtime mode: `auto` (default), `openclaw`, or `generic` |
331
+ | `A2A_RUNTIME_FAILOVER` | Fallback to generic runtime if OpenClaw runtime errors (default: `true`) |
332
+ | `A2A_AGENT_COMMAND` | Generic runtime command for inbound turn handling (reads JSON from stdin) |
333
+ | `A2A_SUMMARY_COMMAND` | Generic runtime command for call summaries (reads JSON from stdin) |
334
+ | `A2A_NOTIFY_COMMAND` | Generic runtime command for owner notifications (reads JSON from stdin) |
335
+ | `A2A_AGENT_NAME` | Override local agent display name |
336
+ | `A2A_OWNER_NAME` | Override owner display name |
328
337
  | `A2A_COLLAB_MODE` | Conversation style: `adaptive` (default) or `deep_dive` |
329
338
  | `A2A_ADMIN_TOKEN` | Protect dashboard/conversation admin routes for non-local access |
330
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * A2A Calling - OpenClaw Integration Installer
3
+ * A2A Calling Setup Installer
4
4
  *
5
5
  * Supports automatic setup:
6
6
  * - If OpenClaw gateway is detected, install a gateway HTTP proxy plugin
7
7
  * so dashboard is accessible at /a2a on gateway.
8
8
  * - If gateway is not detected, dashboard runs on standalone A2A server.
9
+ * - If OpenClaw is not installed, bootstrap standalone runtime templates.
9
10
  *
10
11
  * Usage:
11
12
  * npx a2acalling install
@@ -16,6 +17,7 @@
16
17
 
17
18
  const fs = require('fs');
18
19
  const path = require('path');
20
+ const crypto = require('crypto');
19
21
  const { execSync } = require('child_process');
20
22
 
21
23
  // Paths
@@ -111,6 +113,112 @@ function resolveGatewayBaseUrl() {
111
113
  return `http://127.0.0.1:${fallbackPort}`;
112
114
  }
113
115
 
116
+ function resolveA2AConfigDir() {
117
+ return process.env.A2A_CONFIG_DIR ||
118
+ process.env.OPENCLAW_CONFIG_DIR ||
119
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
120
+ }
121
+
122
+ function safeRead(filePath) {
123
+ try {
124
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
125
+ } catch (err) {
126
+ return '';
127
+ }
128
+ }
129
+
130
+ function writeExecutableFile(filePath, content) {
131
+ fs.writeFileSync(filePath, content, { mode: 0o755 });
132
+ try {
133
+ fs.chmodSync(filePath, 0o755);
134
+ } catch (err) {
135
+ // Non-fatal on platforms without chmod support.
136
+ }
137
+ }
138
+
139
+ function ensureStandaloneBootstrap(hostname, port) {
140
+ const configDir = resolveA2AConfigDir();
141
+ ensureDir(configDir);
142
+
143
+ const configFile = path.join(configDir, 'a2a-config.json');
144
+ const manifestFile = path.join(configDir, 'a2a-disclosure.json');
145
+
146
+ try {
147
+ const { A2AConfig } = require('../src/lib/config');
148
+ const { loadManifest, saveManifest, generateDefaultManifest } = require('../src/lib/disclosure');
149
+
150
+ const config = new A2AConfig();
151
+ const defaults = config.getDefaults() || {};
152
+ config.setDefaults(defaults);
153
+
154
+ const agent = config.getAgent() || {};
155
+ if (!agent.hostname) {
156
+ config.setAgent({
157
+ hostname: `${hostname}:${port}`
158
+ });
159
+ }
160
+
161
+ const manifest = loadManifest();
162
+ if (!manifest || Object.keys(manifest).length === 0) {
163
+ const generated = generateDefaultManifest({
164
+ user: safeRead(path.join(process.cwd(), 'USER.md')),
165
+ heartbeat: safeRead(path.join(process.cwd(), 'HEARTBEAT.md')),
166
+ soul: safeRead(path.join(process.cwd(), 'SOUL.md'))
167
+ });
168
+ saveManifest(generated);
169
+ log(`Generated default disclosure manifest: ${manifestFile}`);
170
+ }
171
+ } catch (err) {
172
+ warn(`Standalone config bootstrap failed: ${err.message}`);
173
+ }
174
+
175
+ const bridgeDir = path.join(configDir, 'runtime-bridge');
176
+ ensureDir(bridgeDir);
177
+ const turnScript = path.join(bridgeDir, 'a2a-turn.sh');
178
+ const summaryScript = path.join(bridgeDir, 'a2a-summary.sh');
179
+ const notifyScript = path.join(bridgeDir, 'a2a-notify.sh');
180
+
181
+ if (!fs.existsSync(turnScript)) {
182
+ writeExecutableFile(turnScript, `#!/usr/bin/env bash
183
+ set -euo pipefail
184
+ payload="$(cat || true)"
185
+ echo '{"response":"Generic bridge placeholder: your agent bridge is active. I received your message and can continue the call. What collaboration outcome should we target?"}'
186
+ `);
187
+ }
188
+
189
+ if (!fs.existsSync(summaryScript)) {
190
+ writeExecutableFile(summaryScript, `#!/usr/bin/env bash
191
+ set -euo pipefail
192
+ payload="$(cat || true)"
193
+ echo '{"summary":"Generic summary placeholder generated by standalone bridge.","ownerSummary":"Generic summary placeholder generated by standalone bridge."}'
194
+ `);
195
+ }
196
+
197
+ if (!fs.existsSync(notifyScript)) {
198
+ writeExecutableFile(notifyScript, `#!/usr/bin/env bash
199
+ set -euo pipefail
200
+ payload="$(cat || true)"
201
+ echo "a2a notify: $payload" >&2
202
+ `);
203
+ }
204
+
205
+ let generatedAdminToken = null;
206
+ if (!process.env.A2A_ADMIN_TOKEN) {
207
+ generatedAdminToken = crypto.randomBytes(24).toString('base64url');
208
+ }
209
+
210
+ return {
211
+ configDir,
212
+ configFile,
213
+ manifestFile,
214
+ bridgeDir,
215
+ turnScript,
216
+ summaryScript,
217
+ notifyScript,
218
+ generatedAdminToken
219
+ };
220
+ }
221
+
114
222
  const DASHBOARD_PLUGIN_MANIFEST = {
115
223
  id: DASHBOARD_PLUGIN_ID,
116
224
  name: 'A2A Dashboard Proxy',
@@ -378,20 +486,30 @@ a2a server --port 3001
378
486
  `;
379
487
 
380
488
  function install() {
381
- log('Installing A2A Calling for OpenClaw...\n');
489
+ log('Installing A2A Calling...\n');
382
490
 
383
491
  const hostname = flags.hostname || process.env.HOSTNAME || 'localhost';
384
492
  const port = String(flags.port || process.env.A2A_PORT || '3001');
385
493
  const backendUrl = flags['dashboard-backend'] || `http://127.0.0.1:${port}`;
386
-
387
- // 1. Create skills directory if needed
388
- ensureDir(OPENCLAW_SKILLS);
389
-
390
- // 2. Install skill
391
- const skillDir = path.join(OPENCLAW_SKILLS, SKILL_NAME);
392
- ensureDir(skillDir);
393
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), SKILL_MD);
394
- log(`Installed skill to: ${skillDir}`);
494
+ const forceStandalone = Boolean(flags.standalone) || String(process.env.A2A_FORCE_STANDALONE || '').toLowerCase() === 'true';
495
+ const hasOpenClawBinary = commandExists('openclaw');
496
+ const hasOpenClawConfig = fs.existsSync(OPENCLAW_CONFIG);
497
+ const hasOpenClaw = !forceStandalone && (hasOpenClawBinary || hasOpenClawConfig);
498
+ let standaloneBootstrap = null;
499
+
500
+ if (hasOpenClaw) {
501
+ // 1. Create skills directory if needed
502
+ ensureDir(OPENCLAW_SKILLS);
503
+
504
+ // 2. Install skill
505
+ const skillDir = path.join(OPENCLAW_SKILLS, SKILL_NAME);
506
+ ensureDir(skillDir);
507
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), SKILL_MD);
508
+ log(`Installed skill to: ${skillDir}`);
509
+ } else {
510
+ warn('OpenClaw not detected. Enabling standalone A2A bootstrap.');
511
+ standaloneBootstrap = ensureStandaloneBootstrap(hostname, port);
512
+ }
395
513
 
396
514
  // 3. Update OpenClaw config + gateway plugin setup (if available)
397
515
  let config = loadOpenClawConfig();
@@ -450,6 +568,12 @@ function install() {
450
568
  warn('Skipping OpenClaw command/plugin config updates');
451
569
  }
452
570
 
571
+ const runtimeLine = forceStandalone
572
+ ? 'Runtime forced to standalone mode for this setup run.'
573
+ : hasOpenClawBinary
574
+ ? 'Runtime auto-selects OpenClaw when available and falls back to generic if needed.'
575
+ : 'Runtime defaults to generic fallback (no OpenClaw dependency required).';
576
+
453
577
  console.log(`
454
578
  ${bold('━━━ Server Setup ━━━')}
455
579
 
@@ -466,6 +590,25 @@ ${dashboardMode === 'gateway'
466
590
  ? `Gateway path /a2a is now proxied to ${backendUrl}.`
467
591
  : 'No gateway detected. Dashboard is served directly from the A2A server.'}
468
592
 
593
+ ${bold('━━━ Runtime Setup ━━━')}
594
+
595
+ ${runtimeLine}
596
+ ${standaloneBootstrap
597
+ ? `Standalone bridge templates:
598
+ ${green(standaloneBootstrap.turnScript)}
599
+ ${green(standaloneBootstrap.summaryScript)}
600
+ ${green(standaloneBootstrap.notifyScript)}
601
+
602
+ Optional bridge wiring:
603
+ export A2A_RUNTIME=generic
604
+ export A2A_AGENT_COMMAND="${standaloneBootstrap.turnScript}"
605
+ export A2A_SUMMARY_COMMAND="${standaloneBootstrap.summaryScript}"
606
+ export A2A_NOTIFY_COMMAND="${standaloneBootstrap.notifyScript}"
607
+ ${standaloneBootstrap.generatedAdminToken ? `
608
+ Suggested dashboard admin token (set in env, do not commit):
609
+ export A2A_ADMIN_TOKEN="${standaloneBootstrap.generatedAdminToken}"` : ''}`
610
+ : 'No standalone bridge templates were created because OpenClaw was detected.'}
611
+
469
612
  ${bold('━━━ Usage ━━━')}
470
613
 
471
614
  In your chat app, use:
@@ -506,11 +649,11 @@ function uninstall() {
506
649
 
507
650
  function showHelp() {
508
651
  console.log(`
509
- ${bold('A2A Calling - OpenClaw Integration')}
652
+ ${bold('A2A Calling Setup')}
510
653
 
511
654
  Usage:
512
- npx a2acalling install [options] Install A2A for OpenClaw
513
- npx a2acalling setup [options] Alias for install (auto gateway/standalone)
655
+ npx a2acalling install [options] Install A2A (OpenClaw-aware + standalone)
656
+ npx a2acalling setup [options] Alias for install (auto runtime detection)
514
657
  npx a2acalling uninstall Remove A2A skill + dashboard plugin
515
658
  npx a2acalling server Start A2A server
516
659
 
@@ -519,10 +662,12 @@ Install Options:
519
662
  --port <port> A2A server port (default: 3001)
520
663
  --gateway-url <url> Force gateway base URL for printed dashboard link
521
664
  --dashboard-backend <url> Backend URL used by gateway dashboard proxy
665
+ --standalone Force standalone bootstrap (ignore OpenClaw detection)
522
666
 
523
667
  Examples:
524
668
  npx a2acalling install --hostname myserver.com --port 3001
525
669
  npx a2acalling setup --dashboard-backend http://127.0.0.1:3001
670
+ npx a2acalling setup --standalone
526
671
  npx a2acalling uninstall
527
672
  `);
528
673
  }
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ const { TokenStore } = require('./lib/tokens');
18
18
  const { A2AClient, A2AError } = require('./lib/client');
19
19
  const { createRoutes } = require('./routes/a2a');
20
20
  const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
21
+ const { createRuntimeAdapter, resolveRuntimeMode } = require('./lib/runtime-adapter');
21
22
 
22
23
  // Lazy load optional dependencies
23
24
  let ConversationStore = null;
@@ -48,6 +49,10 @@ module.exports = {
48
49
  // Dashboard routes
49
50
  createDashboardApiRouter,
50
51
  createDashboardUiRouter,
52
+
53
+ // Runtime adapter
54
+ createRuntimeAdapter,
55
+ resolveRuntimeMode,
51
56
 
52
57
  // Conversation storage (requires better-sqlite3)
53
58
  ConversationStore,
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Runtime adapter for inbound A2A calls.
3
+ *
4
+ * Modes:
5
+ * - openclaw: uses `openclaw` CLI for turn handling, summaries, notifications
6
+ * - generic: platform-agnostic fallback that never hard-fails calls
7
+ *
8
+ * Selection:
9
+ * - A2A_RUNTIME=openclaw|generic|auto (default: auto)
10
+ * - auto picks openclaw if CLI exists, otherwise generic
11
+ *
12
+ * Generic bridge hooks:
13
+ * - A2A_AGENT_COMMAND command that receives JSON payload on stdin and returns text or JSON
14
+ * - A2A_SUMMARY_COMMAND command that receives JSON payload on stdin and returns summary text/JSON
15
+ * - A2A_NOTIFY_COMMAND command that receives JSON payload on stdin for owner notifications
16
+ */
17
+
18
+ const { execSync } = require('child_process');
19
+
20
+ function commandExists(command) {
21
+ try {
22
+ execSync(`command -v ${command}`, { stdio: 'ignore' });
23
+ return true;
24
+ } catch (err) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function cleanText(value, maxLength = 300) {
30
+ return String(value || '')
31
+ .replace(/\s+/g, ' ')
32
+ .trim()
33
+ .slice(0, maxLength);
34
+ }
35
+
36
+ function toBool(value, fallback = true) {
37
+ if (value === undefined || value === null || value === '') {
38
+ return fallback;
39
+ }
40
+ const normalized = String(value).trim().toLowerCase();
41
+ return !(normalized === '0' || normalized === 'false' || normalized === 'no');
42
+ }
43
+
44
+ function resolveRuntimeMode() {
45
+ const requested = String(process.env.A2A_RUNTIME || 'auto').trim().toLowerCase();
46
+ const hasOpenClaw = commandExists('openclaw');
47
+
48
+ if (requested === 'generic') {
49
+ return {
50
+ mode: 'generic',
51
+ requested,
52
+ hasOpenClaw,
53
+ reason: 'A2A_RUNTIME=generic'
54
+ };
55
+ }
56
+
57
+ if (requested === 'openclaw') {
58
+ if (hasOpenClaw) {
59
+ return {
60
+ mode: 'openclaw',
61
+ requested,
62
+ hasOpenClaw,
63
+ reason: 'A2A_RUNTIME=openclaw'
64
+ };
65
+ }
66
+ return {
67
+ mode: 'generic',
68
+ requested,
69
+ hasOpenClaw,
70
+ warning: 'A2A_RUNTIME=openclaw but openclaw CLI not found, falling back to generic runtime',
71
+ reason: 'forced-openclaw-missing'
72
+ };
73
+ }
74
+
75
+ if (hasOpenClaw) {
76
+ return {
77
+ mode: 'openclaw',
78
+ requested: 'auto',
79
+ hasOpenClaw,
80
+ reason: 'openclaw CLI detected'
81
+ };
82
+ }
83
+
84
+ return {
85
+ mode: 'generic',
86
+ requested: 'auto',
87
+ hasOpenClaw,
88
+ reason: 'openclaw CLI not detected'
89
+ };
90
+ }
91
+
92
+ function normalizeOpenClawOutput(raw) {
93
+ const lines = String(raw || '')
94
+ .split('\n')
95
+ .filter(line => {
96
+ if (!line.trim()) return false;
97
+ if (line.includes('[telegram-topic-tracker]')) return false;
98
+ if (line.includes('Plugin registered')) return false;
99
+ return true;
100
+ });
101
+ return lines.join('\n').trim();
102
+ }
103
+
104
+ function parseCommandTextOutput(rawOutput, keys = ['response', 'text', 'message']) {
105
+ const output = String(rawOutput || '').trim();
106
+ if (!output) {
107
+ return '';
108
+ }
109
+
110
+ try {
111
+ const parsed = JSON.parse(output);
112
+ if (parsed && typeof parsed === 'object') {
113
+ for (const key of keys) {
114
+ if (typeof parsed[key] === 'string' && parsed[key].trim()) {
115
+ return parsed[key].trim();
116
+ }
117
+ }
118
+ }
119
+ } catch (err) {
120
+ // Plain text output is valid for bridge commands.
121
+ }
122
+
123
+ return output;
124
+ }
125
+
126
+ function parseSummaryOutput(rawOutput) {
127
+ const output = String(rawOutput || '').trim();
128
+ if (!output) {
129
+ return null;
130
+ }
131
+
132
+ try {
133
+ const parsed = JSON.parse(output);
134
+ if (parsed && typeof parsed === 'object') {
135
+ const summary = cleanText(parsed.summary || parsed.text || parsed.message, 1500);
136
+ return {
137
+ ...parsed,
138
+ summary: summary || null,
139
+ ownerSummary: cleanText(
140
+ parsed.ownerSummary || parsed.owner_summary || summary || '',
141
+ 1500
142
+ ) || null
143
+ };
144
+ }
145
+ } catch (err) {
146
+ // Plain text is also acceptable.
147
+ }
148
+
149
+ const summary = cleanText(output, 1500);
150
+ return {
151
+ summary,
152
+ ownerSummary: summary
153
+ };
154
+ }
155
+
156
+ function runCommand(command, payload, options = {}) {
157
+ const payloadJson = JSON.stringify(payload || {});
158
+ const timeoutMs = options.timeoutMs || 60000;
159
+ return execSync(command, {
160
+ encoding: 'utf8',
161
+ timeout: timeoutMs,
162
+ stdio: ['pipe', 'pipe', 'pipe'],
163
+ input: payloadJson,
164
+ cwd: options.cwd || process.cwd(),
165
+ env: {
166
+ ...process.env,
167
+ A2A_PAYLOAD_JSON: payloadJson
168
+ }
169
+ });
170
+ }
171
+
172
+ function escapeCliValue(value) {
173
+ return String(value || '')
174
+ .replace(/\\/g, '\\\\')
175
+ .replace(/"/g, '\\"')
176
+ .replace(/\n/g, '\\n')
177
+ .replace(/\r/g, '');
178
+ }
179
+
180
+ function buildFallbackResponse(message, context = {}, reason = null) {
181
+ const callerName = cleanText(context.callerName || context.caller?.name || 'caller');
182
+ const ownerName = cleanText(context.ownerName || 'the owner');
183
+ const allowedTopics = Array.isArray(context.allowedTopics)
184
+ ? context.allowedTopics.filter(Boolean).slice(0, 4)
185
+ : [];
186
+ const topicText = allowedTopics.length > 0
187
+ ? allowedTopics.join(', ')
188
+ : 'permitted topics';
189
+ const excerpt = cleanText(message, 220) || 'No message content provided.';
190
+
191
+ let prefix = `I am running in generic A2A mode for ${ownerName}.`;
192
+ if (reason) {
193
+ prefix = `I switched to generic fallback mode (${cleanText(reason, 120)}).`;
194
+ }
195
+
196
+ return `${prefix} I received from ${callerName}: "${excerpt}". ` +
197
+ `We can still work through concrete overlap on ${topicText} and line up actionable next steps. ` +
198
+ `What outcome should we target first?`;
199
+ }
200
+
201
+ function buildFallbackSummary(messages = [], callerInfo = {}, reason = null) {
202
+ const inbound = messages.filter(m => m.direction === 'inbound');
203
+ const outbound = messages.filter(m => m.direction !== 'inbound');
204
+ const caller = cleanText(callerInfo?.name || 'Unknown caller');
205
+ const lastInbound = inbound.length > 0
206
+ ? cleanText(inbound[inbound.length - 1].content, 220)
207
+ : '';
208
+
209
+ const summary = [
210
+ `Call concluded with ${caller}.`,
211
+ `Inbound turns: ${inbound.length}. Outbound turns: ${outbound.length}.`,
212
+ lastInbound ? `Latest caller request: "${lastInbound}".` : '',
213
+ reason ? `Summary mode: ${cleanText(reason, 140)}.` : 'Summary mode: generic fallback.'
214
+ ].filter(Boolean).join(' ');
215
+
216
+ return {
217
+ summary,
218
+ ownerSummary: summary,
219
+ relevance: 'unknown',
220
+ goalsTouched: [],
221
+ ownerActionItems: [],
222
+ callerActionItems: [],
223
+ jointActionItems: [],
224
+ collaborationOpportunity: {
225
+ found: false,
226
+ rationale: 'Generic fallback summary (no platform-specific summarizer configured)'
227
+ },
228
+ followUp: lastInbound
229
+ ? `Clarify the next concrete step for: ${lastInbound}`
230
+ : 'Ask both owners to confirm desired follow-up scope.',
231
+ notes: reason
232
+ ? `Fallback summary generated after runtime issue: ${cleanText(reason, 180)}`
233
+ : 'Fallback summary generated by generic runtime.'
234
+ };
235
+ }
236
+
237
+ function createRuntimeAdapter(options = {}) {
238
+ const workspaceDir = options.workspaceDir || process.cwd();
239
+ const modeInfo = resolveRuntimeMode();
240
+ const failoverEnabled = toBool(process.env.A2A_RUNTIME_FAILOVER, true);
241
+
242
+ const genericAgentCommand = process.env.A2A_AGENT_COMMAND || '';
243
+ const genericSummaryCommand = process.env.A2A_SUMMARY_COMMAND || '';
244
+ const genericNotifyCommand = process.env.A2A_NOTIFY_COMMAND || '';
245
+
246
+ async function runOpenClawTurn({ sessionId, prompt, timeoutMs }) {
247
+ const timeoutSeconds = Math.max(5, Math.min(300, Math.round((timeoutMs || 65000) / 1000)));
248
+ const escapedPrompt = escapeCliValue(prompt);
249
+ const output = execSync(
250
+ `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout ${timeoutSeconds} 2>&1`,
251
+ {
252
+ encoding: 'utf8',
253
+ timeout: (timeoutMs || 65000) + 5000,
254
+ maxBuffer: 1024 * 1024,
255
+ cwd: workspaceDir,
256
+ env: { ...process.env, FORCE_COLOR: '0' }
257
+ }
258
+ );
259
+ return normalizeOpenClawOutput(output) || '[Sub-agent returned empty response]';
260
+ }
261
+
262
+ async function runOpenClawSummary({ sessionId, prompt, timeoutMs }) {
263
+ const timeoutSeconds = Math.max(5, Math.min(120, Math.round((timeoutMs || 35000) / 1000)));
264
+ const escapedPrompt = escapeCliValue(prompt);
265
+ const output = execSync(
266
+ `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout ${timeoutSeconds} 2>&1`,
267
+ {
268
+ encoding: 'utf8',
269
+ timeout: (timeoutMs || 35000) + 5000,
270
+ cwd: workspaceDir,
271
+ env: { ...process.env, FORCE_COLOR: '0' }
272
+ }
273
+ );
274
+ const summaryText = cleanText(normalizeOpenClawOutput(output), 1500);
275
+ if (!summaryText) {
276
+ return null;
277
+ }
278
+ return {
279
+ summary: summaryText,
280
+ ownerSummary: summaryText
281
+ };
282
+ }
283
+
284
+ async function runOpenClawNotify({ callerName, callerOwner, message }) {
285
+ const notification = `🤝 **A2A Call**\nFrom: ${callerName}${callerOwner}\n> ${message.slice(0, 150)}...`;
286
+ execSync(
287
+ `openclaw message send --channel telegram --message "${escapeCliValue(notification)}"`,
288
+ { timeout: 10000, stdio: 'pipe' }
289
+ );
290
+ }
291
+
292
+ async function runGenericTurn({ message, caller, context, runtimeError }) {
293
+ const payload = {
294
+ mode: 'a2a-turn',
295
+ message,
296
+ caller: caller || {},
297
+ context: context || {}
298
+ };
299
+
300
+ if (genericAgentCommand) {
301
+ try {
302
+ const output = runCommand(genericAgentCommand, payload, {
303
+ timeoutMs: context?.timeoutMs || 65000
304
+ });
305
+ const text = parseCommandTextOutput(output);
306
+ if (text) {
307
+ return text;
308
+ }
309
+ } catch (err) {
310
+ runtimeError = err.message;
311
+ console.error(`[a2a] Generic agent command failed: ${err.message}`);
312
+ }
313
+ }
314
+
315
+ return buildFallbackResponse(message, {
316
+ caller,
317
+ callerName: caller?.name,
318
+ ownerName: context?.ownerName,
319
+ allowedTopics: context?.allowedTopics
320
+ }, runtimeError);
321
+ }
322
+
323
+ async function runGenericSummary({ messages, callerInfo, reason }) {
324
+ const payload = {
325
+ mode: 'a2a-summary',
326
+ messages,
327
+ caller: callerInfo || {}
328
+ };
329
+
330
+ if (genericSummaryCommand) {
331
+ try {
332
+ const output = runCommand(genericSummaryCommand, payload, { timeoutMs: 35000 });
333
+ const parsed = parseSummaryOutput(output);
334
+ if (parsed && parsed.summary) {
335
+ return parsed;
336
+ }
337
+ } catch (err) {
338
+ reason = err.message;
339
+ console.error(`[a2a] Generic summary command failed: ${err.message}`);
340
+ }
341
+ }
342
+
343
+ return buildFallbackSummary(messages, callerInfo, reason);
344
+ }
345
+
346
+ async function runGenericNotify(payload) {
347
+ if (!genericNotifyCommand) {
348
+ return;
349
+ }
350
+ try {
351
+ runCommand(genericNotifyCommand, payload, { timeoutMs: 10000 });
352
+ } catch (err) {
353
+ console.error(`[a2a] Generic notify command failed: ${err.message}`);
354
+ }
355
+ }
356
+
357
+ async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
358
+ if (modeInfo.mode !== 'openclaw') {
359
+ return runGenericTurn({ message, caller, context });
360
+ }
361
+
362
+ try {
363
+ return await runOpenClawTurn({ sessionId, prompt, timeoutMs });
364
+ } catch (err) {
365
+ if (!failoverEnabled) {
366
+ throw err;
367
+ }
368
+ console.error(`[a2a] OpenClaw runtime failed, switching to generic fallback: ${err.message}`);
369
+ return runGenericTurn({
370
+ message,
371
+ caller,
372
+ context,
373
+ runtimeError: `openclaw runtime unavailable: ${err.message}`
374
+ });
375
+ }
376
+ }
377
+
378
+ async function summarize({ sessionId, prompt, messages, callerInfo }) {
379
+ if (modeInfo.mode !== 'openclaw') {
380
+ return runGenericSummary({ messages, callerInfo });
381
+ }
382
+
383
+ try {
384
+ const result = await runOpenClawSummary({
385
+ sessionId,
386
+ prompt,
387
+ timeoutMs: 35000
388
+ });
389
+ if (result && result.summary) {
390
+ return result;
391
+ }
392
+ return runGenericSummary({
393
+ messages,
394
+ callerInfo,
395
+ reason: 'empty summary from openclaw runtime'
396
+ });
397
+ } catch (err) {
398
+ if (!failoverEnabled) {
399
+ throw err;
400
+ }
401
+ console.error(`[a2a] OpenClaw summary failed, using generic fallback: ${err.message}`);
402
+ return runGenericSummary({
403
+ messages,
404
+ callerInfo,
405
+ reason: `openclaw summary unavailable: ${err.message}`
406
+ });
407
+ }
408
+ }
409
+
410
+ async function notify({ level, token, caller, message, conversationId }) {
411
+ const payload = {
412
+ mode: 'a2a-notify',
413
+ level,
414
+ token: token || null,
415
+ caller: caller || null,
416
+ message,
417
+ conversationId
418
+ };
419
+
420
+ if (modeInfo.mode !== 'openclaw') {
421
+ return runGenericNotify(payload);
422
+ }
423
+
424
+ if (level !== 'all') {
425
+ return;
426
+ }
427
+
428
+ const callerName = caller?.name || 'Unknown';
429
+ const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
430
+
431
+ try {
432
+ await runOpenClawNotify({ callerName, callerOwner, message: message || '' });
433
+ } catch (err) {
434
+ if (!failoverEnabled) {
435
+ throw err;
436
+ }
437
+ console.error(`[a2a] OpenClaw notify failed, running generic notifier: ${err.message}`);
438
+ await runGenericNotify(payload);
439
+ }
440
+ }
441
+
442
+ return {
443
+ mode: modeInfo.mode,
444
+ requestedMode: modeInfo.requested,
445
+ hasOpenClaw: modeInfo.hasOpenClaw,
446
+ reason: modeInfo.reason,
447
+ warning: modeInfo.warning || null,
448
+ failoverEnabled,
449
+ runTurn,
450
+ summarize,
451
+ notify,
452
+ buildFallbackResponse
453
+ };
454
+ }
455
+
456
+ module.exports = {
457
+ createRuntimeAdapter,
458
+ resolveRuntimeMode,
459
+ buildFallbackResponse
460
+ };
package/src/server.js CHANGED
@@ -2,17 +2,17 @@
2
2
  /**
3
3
  * A2A Server
4
4
  *
5
- * Routes A2A calls to OpenClaw sub-agents.
5
+ * Routes A2A calls through a runtime adapter (OpenClaw or generic fallback).
6
6
  * Auto-adds contacts, generates summaries, notifies owner.
7
7
  */
8
8
 
9
9
  const express = require('express');
10
- const { execSync } = require('child_process');
11
10
  const fs = require('fs');
12
11
  const path = require('path');
13
12
  const { createRoutes } = require('./routes/a2a');
14
13
  const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
15
14
  const { TokenStore } = require('./lib/tokens');
15
+ const { createRuntimeAdapter } = require('./lib/runtime-adapter');
16
16
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./lib/disclosure');
17
17
  const {
18
18
  buildConnectionPrompt,
@@ -21,16 +21,23 @@ const {
21
21
  } = require('./lib/prompt-template');
22
22
 
23
23
  const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
24
- const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/clawd';
24
+ const workspaceDir = process.env.A2A_WORKSPACE || process.env.OPENCLAW_WORKSPACE || process.cwd();
25
25
 
26
26
  // Load workspace context for agent identity
27
27
  function loadAgentContext() {
28
- let context = { name: 'bappybot', owner: 'Ben Pollack' };
28
+ let context = {
29
+ name: process.env.A2A_AGENT_NAME || process.env.AGENT_NAME || 'a2a-agent',
30
+ owner: process.env.A2A_OWNER_NAME || process.env.USER || 'Agent Owner'
31
+ };
29
32
 
30
33
  try {
31
34
  const userPath = path.join(workspaceDir, 'USER.md');
32
35
  if (fs.existsSync(userPath)) {
33
36
  const content = fs.readFileSync(userPath, 'utf8');
37
+ const agentMatch = content.match(/\*\*Agent:\*\*\s*([^\n]+)/i);
38
+ if (agentMatch && agentMatch[1]) {
39
+ context.name = agentMatch[1].trim().slice(0, 80) || context.name;
40
+ }
34
41
  const nameMatch = content.match(/\*\*Name:\*\*\s*([^\n]+)/);
35
42
  if (nameMatch) {
36
43
  const name = nameMatch[1].trim();
@@ -46,12 +53,20 @@ function loadAgentContext() {
46
53
 
47
54
  const agentContext = loadAgentContext();
48
55
  const tokenStore = new TokenStore();
56
+ const runtime = createRuntimeAdapter({
57
+ workspaceDir,
58
+ agentContext
59
+ });
49
60
  const VALID_PHASES = new Set(['handshake', 'explore', 'deep_dive', 'synthesize', 'close']);
50
61
  const collaborationSessions = new Map();
51
62
  const COLLAB_STATE_TTL_MS = readPositiveIntEnv('A2A_COLLAB_STATE_TTL_MS', 6 * 60 * 60 * 1000);
52
63
  const MAX_COLLAB_SESSIONS = readPositiveIntEnv('A2A_COLLAB_MAX_SESSIONS', 500);
53
64
 
54
65
  console.log(`[a2a] Agent: ${agentContext.name} (${agentContext.owner}'s agent)`);
66
+ console.log(`[a2a] Runtime: ${runtime.mode} (${runtime.reason})`);
67
+ if (runtime.warning) {
68
+ console.warn(`[a2a] Runtime warning: ${runtime.warning}`);
69
+ }
55
70
 
56
71
  function readPositiveIntEnv(name, fallback) {
57
72
  const parsed = Number.parseInt(process.env[name] || '', 10);
@@ -489,30 +504,19 @@ async function callAgent(message, a2aContext) {
489
504
  const sessionId = `a2a-${conversationId}`;
490
505
 
491
506
  try {
492
- const escapedPrompt = prompt
493
- .replace(/\\/g, '\\\\')
494
- .replace(/"/g, '\\"')
495
- .replace(/\n/g, '\\n')
496
- .replace(/\r/g, '');
497
-
498
- const result = execSync(
499
- `openclaw agent --session-id "${sessionId}" --message "${escapedPrompt}" --timeout 55 2>&1`,
500
- {
501
- encoding: 'utf8',
502
- timeout: 65000,
503
- maxBuffer: 1024 * 1024,
504
- cwd: workspaceDir,
505
- env: { ...process.env, FORCE_COLOR: '0' }
507
+ const rawResponse = await runtime.runTurn({
508
+ sessionId,
509
+ prompt,
510
+ message,
511
+ caller: a2aContext.caller || {},
512
+ timeoutMs: 65000,
513
+ context: {
514
+ conversationId,
515
+ tier: tierInfo,
516
+ ownerName: agentContext.owner,
517
+ allowedTopics: a2aContext.allowed_topics || []
506
518
  }
507
- );
508
-
509
- const lines = result.split('\n').filter(line =>
510
- !line.includes('[telegram-topic-tracker]') &&
511
- !line.includes('Plugin registered') &&
512
- line.trim()
513
- );
514
-
515
- const rawResponse = lines.join('\n').trim() || '[Sub-agent returned empty response]';
519
+ });
516
520
 
517
521
  if (collabMode !== 'adaptive') {
518
522
  return rawResponse;
@@ -549,8 +553,12 @@ async function callAgent(message, a2aContext) {
549
553
  return cleanResponse || '[Sub-agent returned empty response]';
550
554
 
551
555
  } catch (err) {
552
- console.error('[a2a] Sub-agent spawn failed:', err.message);
553
- return `[Sub-agent error: ${err.message}]`;
556
+ console.error('[a2a] Runtime turn handling failed:', err.message);
557
+ return runtime.buildFallbackResponse(message, {
558
+ caller: a2aContext.caller,
559
+ ownerName: agentContext.owner,
560
+ allowedTopics: a2aContext.allowed_topics || []
561
+ }, err.message);
554
562
  }
555
563
  }
556
564
 
@@ -584,25 +592,12 @@ Structure your summary with these sections:
584
592
  Be concise but specific. No filler.`;
585
593
 
586
594
  try {
587
- const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
588
- const result = execSync(
589
- `openclaw agent --session-id "summary-${Date.now()}" --message "${escapedPrompt}" --timeout 30 2>&1`,
590
- { encoding: 'utf8', timeout: 35000, cwd: workspaceDir, env: { ...process.env, FORCE_COLOR: '0' } }
591
- );
592
-
593
- // Filter out noise and get the summary
594
- const lines = result.split('\n').filter(line =>
595
- !line.includes('[telegram-topic-tracker]') &&
596
- !line.includes('Plugin registered') &&
597
- line.trim()
598
- );
599
-
600
- const summaryText = lines.join(' ').trim().slice(0, 1000);
601
-
602
- return {
603
- summary: summaryText,
604
- ownerSummary: summaryText
605
- };
595
+ return await runtime.summarize({
596
+ sessionId: `summary-${Date.now()}`,
597
+ prompt,
598
+ messages,
599
+ callerInfo
600
+ });
606
601
  } catch (err) {
607
602
  console.error('[a2a] Summary generation failed:', err.message);
608
603
  return null;
@@ -615,22 +610,21 @@ Be concise but specific. No filler.`;
615
610
  async function notifyOwner({ level, token, caller, message, conversation_id }) {
616
611
  const callerName = caller?.name || 'Unknown';
617
612
  const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
613
+ const messageText = String(message || '');
618
614
 
619
615
  console.log(`[a2a] 📞 Call from ${callerName}${callerOwner}`);
620
616
  console.log(`[a2a] Token: ${token?.name || 'unknown'}`);
621
- console.log(`[a2a] Message: ${message.slice(0, 100)}...`);
622
-
623
- // Try to notify via Telegram
624
- if (level === 'all') {
625
- try {
626
- const notification = `🤝 **A2A Call**\nFrom: ${callerName}${callerOwner}\n> ${message.slice(0, 150)}...`;
627
- execSync(`openclaw message send --channel telegram --message "${notification.replace(/"/g, '\\"')}"`, {
628
- timeout: 10000, stdio: 'pipe'
629
- });
630
- } catch (e) {
631
- // Notification failed, continue anyway
632
- }
617
+ if (messageText) {
618
+ console.log(`[a2a] Message: ${messageText.slice(0, 100)}...`);
633
619
  }
620
+
621
+ await runtime.notify({
622
+ level,
623
+ token,
624
+ caller,
625
+ message: messageText,
626
+ conversationId: conversation_id
627
+ });
634
628
  }
635
629
 
636
630
  const app = express();
@@ -668,6 +662,7 @@ app.get('/', (req, res) => {
668
662
  app.listen(port, () => {
669
663
  console.log(`[a2a] A2A server listening on port ${port}`);
670
664
  console.log(`[a2a] Agent: ${agentContext.name} - LIVE`);
665
+ console.log(`[a2a] Runtime mode: ${runtime.mode}${runtime.failoverEnabled ? ' (failover enabled)' : ''}`);
671
666
  console.log(`[a2a] Collaboration mode: ${resolveCollabMode()}`);
672
- console.log(`[a2a] Features: sub-agents, auto-contacts, summaries`);
667
+ console.log(`[a2a] Features: adaptive collaboration, auto-contacts, summaries, dashboard`);
673
668
  });