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.
@@ -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 (claude mode has no notification transport)', {
532
- event: 'notify_skipped_claude',
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