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 +12 -3
- package/package.json +1 -1
- package/scripts/install-openclaw.js +159 -14
- package/src/index.js +5 -0
- package/src/lib/runtime-adapter.js +460 -0
- package/src/server.js +56 -61
package/README.md
CHANGED
|
@@ -58,10 +58,10 @@ npm install -g a2acalling
|
|
|
58
58
|
npm install a2acalling
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
###
|
|
61
|
+
### Setup (Auto-Detect Runtime)
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
# Auto setup (detects gateway
|
|
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
|
|
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,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* A2A Calling
|
|
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
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
652
|
+
${bold('A2A Calling Setup')}
|
|
510
653
|
|
|
511
654
|
Usage:
|
|
512
|
-
npx a2acalling install [options] Install A2A
|
|
513
|
-
npx a2acalling setup [options] Alias for install (auto
|
|
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
|
|
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 ||
|
|
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 = {
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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]
|
|
553
|
-
return
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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:
|
|
667
|
+
console.log(`[a2a] Features: adaptive collaboration, auto-contacts, summaries, dashboard`);
|
|
673
668
|
});
|