a2acalling 0.6.72 → 0.6.74
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/.a2a-manifest.json +2 -2
- package/ARCHITECTURE.md +39 -16
- package/CONVENTIONS.md +43 -6
- package/biome.json +27 -0
- package/docs/assessments/2026-02-27-google-a2a-protocol-assessment.md +292 -0
- package/docs/plans/2026-03-01-a2a-68-openclaw-integration-tests.md +676 -0
- package/docs/plans/2026-03-01-a2a-77-invoke-security-tests.md +661 -0
- package/eslint.config.js +16 -0
- package/knip.json +17 -0
- package/package.json +11 -2
- package/scripts/install-openclaw.js +3 -5
- package/src/lib/agent-card.js +111 -0
- package/src/lib/client.js +290 -49
- package/src/lib/conversations.js +2 -0
- package/src/lib/local-request.js +69 -0
- package/src/lib/logger.js +2 -0
- package/src/lib/runtime-adapter.js +101 -7
- package/src/routes/a2a.js +393 -66
- package/src/routes/dashboard.js +1 -27
- package/src/server.js +19 -0
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* - openclaw: uses `openclaw` CLI for turn handling, summaries, notifications
|
|
6
6
|
* - claude: uses `claude` CLI as a real LLM subagent for conversations
|
|
7
7
|
*
|
|
8
|
+
* - test: minimal runtime for CI/headless — echoes messages or spawns A2A_AGENT_COMMAND
|
|
9
|
+
*
|
|
8
10
|
* Selection:
|
|
9
|
-
* - A2A_RUNTIME=openclaw|claude|auto (default: auto)
|
|
11
|
+
* - A2A_RUNTIME=openclaw|claude|test|auto (default: auto)
|
|
10
12
|
* - auto picks openclaw → claude → error (no supported CLI)
|
|
11
13
|
*/
|
|
12
14
|
|
|
@@ -43,6 +45,18 @@ function resolveRuntimeMode() {
|
|
|
43
45
|
const hasOpenClaw = commandExists('openclaw');
|
|
44
46
|
const hasClaude = commandExists('claude');
|
|
45
47
|
|
|
48
|
+
// A2A-66: test runtime for CI/headless environments — minimal runTurn with
|
|
49
|
+
// optional A2A_AGENT_COMMAND bridge support.
|
|
50
|
+
if (requested === 'test') {
|
|
51
|
+
return {
|
|
52
|
+
mode: 'test',
|
|
53
|
+
requested,
|
|
54
|
+
hasOpenClaw,
|
|
55
|
+
hasClaude,
|
|
56
|
+
reason: 'A2A_RUNTIME=test'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
if (requested === 'generic') {
|
|
47
61
|
return {
|
|
48
62
|
mode: 'none',
|
|
@@ -138,6 +152,11 @@ function normalizeOpenClawOutput(raw) {
|
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
|
|
155
|
+
function readPositiveIntEnv(name, fallback) {
|
|
156
|
+
const parsed = Number.parseInt(process.env[name] || '', 10);
|
|
157
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
158
|
+
}
|
|
159
|
+
|
|
141
160
|
function createRuntimeAdapter(options = {}) {
|
|
142
161
|
const workspaceDir = options.workspaceDir || process.cwd();
|
|
143
162
|
const modeInfo = resolveRuntimeMode();
|
|
@@ -158,6 +177,33 @@ function createRuntimeAdapter(options = {}) {
|
|
|
158
177
|
// Design decision (A2A-29): we keep per-conversation state for prompt/metadata
|
|
159
178
|
// continuity, but Claude execution itself is stateless (no `--resume`).
|
|
160
179
|
const claudeSessions = new Map();
|
|
180
|
+
const CLAUDE_SESSION_TTL_MS = readPositiveIntEnv('A2A_CLAUDE_SESSION_TTL_MS', 6 * 60 * 60 * 1000);
|
|
181
|
+
const MAX_CLAUDE_SESSIONS = readPositiveIntEnv('A2A_CLAUDE_MAX_SESSIONS', 500);
|
|
182
|
+
|
|
183
|
+
// A2A-69: TTL-based pruning for Claude session state.
|
|
184
|
+
// Follows the same pattern as pruneCollaborationSessions() in server.js:
|
|
185
|
+
// 1. Evict entries older than TTL
|
|
186
|
+
// 2. If still over max, evict oldest-first
|
|
187
|
+
function pruneClaudeSessions() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const [id, session] of claudeSessions.entries()) {
|
|
190
|
+
const updatedAt = Number(session?.updatedAt || 0);
|
|
191
|
+
if (!updatedAt || now - updatedAt > CLAUDE_SESSION_TTL_MS) {
|
|
192
|
+
claudeSessions.delete(id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (claudeSessions.size <= MAX_CLAUDE_SESSIONS) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const oldest = Array.from(claudeSessions.entries())
|
|
201
|
+
.sort((a, b) => (a[1]?.updatedAt || 0) - (b[1]?.updatedAt || 0));
|
|
202
|
+
const toDelete = claudeSessions.size - MAX_CLAUDE_SESSIONS;
|
|
203
|
+
for (let i = 0; i < toDelete; i++) {
|
|
204
|
+
claudeSessions.delete(oldest[i][0]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
161
207
|
|
|
162
208
|
async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
|
|
163
209
|
const traceId = context?.traceId || context?.trace_id;
|
|
@@ -165,6 +211,9 @@ function createRuntimeAdapter(options = {}) {
|
|
|
165
211
|
const conversationId = context?.conversationId || context?.conversation_id;
|
|
166
212
|
const startAt = Date.now();
|
|
167
213
|
|
|
214
|
+
// A2A-69: prune stale sessions before accessing/creating state
|
|
215
|
+
pruneClaudeSessions();
|
|
216
|
+
|
|
168
217
|
// Get or create session state
|
|
169
218
|
let session = claudeSessions.get(sessionId);
|
|
170
219
|
if (!session) {
|
|
@@ -177,6 +226,7 @@ function createRuntimeAdapter(options = {}) {
|
|
|
177
226
|
systemPrompt: '',
|
|
178
227
|
turnCount: 0,
|
|
179
228
|
lastMeta: null,
|
|
229
|
+
updatedAt: Date.now(),
|
|
180
230
|
// Keep a permission snapshot so summary runs with the same policy envelope.
|
|
181
231
|
permissionSnapshot: {
|
|
182
232
|
capabilities: Array.isArray(context?.capabilities) ? context.capabilities : [],
|
|
@@ -213,6 +263,7 @@ function createRuntimeAdapter(options = {}) {
|
|
|
213
263
|
claudeSessions.set(sessionId, session);
|
|
214
264
|
}
|
|
215
265
|
|
|
266
|
+
session.updatedAt = Date.now();
|
|
216
267
|
session.turnCount++;
|
|
217
268
|
|
|
218
269
|
logger.debug('Invoking Claude subagent turn', {
|
|
@@ -372,6 +423,39 @@ function createRuntimeAdapter(options = {}) {
|
|
|
372
423
|
}
|
|
373
424
|
}
|
|
374
425
|
|
|
426
|
+
// A2A-66: test runtime — spawn A2A_AGENT_COMMAND if set, otherwise echo.
|
|
427
|
+
// Uses shell: true so the command string is parsed by the shell (supports
|
|
428
|
+
// quoted args, paths with spaces, pipes, etc.).
|
|
429
|
+
if (modeInfo.mode === 'test') {
|
|
430
|
+
const agentCommand = process.env.A2A_AGENT_COMMAND;
|
|
431
|
+
if (agentCommand) {
|
|
432
|
+
const payload = JSON.stringify({ message, caller, context });
|
|
433
|
+
const result = spawnSync(agentCommand, {
|
|
434
|
+
input: payload,
|
|
435
|
+
encoding: 'utf8',
|
|
436
|
+
shell: true,
|
|
437
|
+
timeout: (timeoutMs || 65000) + 5000,
|
|
438
|
+
maxBuffer: 1024 * 1024,
|
|
439
|
+
cwd: workspaceDir,
|
|
440
|
+
env: process.env
|
|
441
|
+
});
|
|
442
|
+
if (result.error) {
|
|
443
|
+
throw result.error;
|
|
444
|
+
}
|
|
445
|
+
// A2A-66: check exit code — non-zero means the bridge command failed.
|
|
446
|
+
if (result.status !== 0) {
|
|
447
|
+
const stderr = String(result.stderr || '').trim();
|
|
448
|
+
throw new Error(
|
|
449
|
+
`A2A_AGENT_COMMAND exited with code ${result.status}` +
|
|
450
|
+
(stderr ? `: ${stderr.slice(0, 200)}` : '')
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return String(result.stdout || '').trim() || '[test-runtime] Empty command output';
|
|
454
|
+
}
|
|
455
|
+
const snippet = cleanText(message || prompt || '', 120);
|
|
456
|
+
return `[test-runtime] Echo: ${snippet}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
375
459
|
if (modeInfo.mode !== 'openclaw') {
|
|
376
460
|
throw new Error(
|
|
377
461
|
`No supported A2A runtime available (mode=${modeInfo.mode}). ` +
|
|
@@ -457,6 +541,12 @@ function createRuntimeAdapter(options = {}) {
|
|
|
457
541
|
throw new Error('Claude summary returned empty result');
|
|
458
542
|
}
|
|
459
543
|
|
|
544
|
+
// A2A-66: test runtime — return canned summary.
|
|
545
|
+
if (modeInfo.mode === 'test') {
|
|
546
|
+
const text = 'Test conversation concluded.';
|
|
547
|
+
return { summary: text, ownerSummary: text };
|
|
548
|
+
}
|
|
549
|
+
|
|
460
550
|
if (modeInfo.mode !== 'openclaw') {
|
|
461
551
|
throw new Error(
|
|
462
552
|
`No supported A2A runtime available for summarization (mode=${modeInfo.mode}). ` +
|
|
@@ -526,14 +616,15 @@ function createRuntimeAdapter(options = {}) {
|
|
|
526
616
|
data: { level }
|
|
527
617
|
});
|
|
528
618
|
|
|
529
|
-
if (modeInfo.mode === 'claude') {
|
|
530
|
-
// Claude mode: notifications are a no-op (no notification transport available)
|
|
531
|
-
logger.debug('Notification skipped (
|
|
532
|
-
event: '
|
|
619
|
+
if (modeInfo.mode === 'claude' || modeInfo.mode === 'test') {
|
|
620
|
+
// Claude/test mode: notifications are a no-op (no notification transport available)
|
|
621
|
+
logger.debug('Notification skipped (no notification transport in this mode)', {
|
|
622
|
+
event: 'notify_skipped',
|
|
533
623
|
traceId,
|
|
534
624
|
requestId,
|
|
535
625
|
conversationId,
|
|
536
|
-
tokenId: token?.id
|
|
626
|
+
tokenId: token?.id,
|
|
627
|
+
data: { mode: modeInfo.mode }
|
|
537
628
|
});
|
|
538
629
|
return;
|
|
539
630
|
}
|
|
@@ -597,7 +688,10 @@ function createRuntimeAdapter(options = {}) {
|
|
|
597
688
|
runTurn,
|
|
598
689
|
summarize,
|
|
599
690
|
notify,
|
|
600
|
-
getLastTurnMeta
|
|
691
|
+
getLastTurnMeta,
|
|
692
|
+
// A2A-69: exposed for testing
|
|
693
|
+
_claudeSessions: claudeSessions,
|
|
694
|
+
_pruneClaudeSessions: pruneClaudeSessions
|
|
601
695
|
};
|
|
602
696
|
}
|
|
603
697
|
|