a2acalling 0.6.43 → 0.6.45

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.
@@ -3,20 +3,17 @@
3
3
  *
4
4
  * Modes:
5
5
  * - openclaw: uses `openclaw` CLI for turn handling, summaries, notifications
6
- * - generic: platform-agnostic fallback that never hard-fails calls
6
+ * - claude: uses `claude` CLI as a real LLM subagent for conversations
7
7
  *
8
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
9
+ * - A2A_RUNTIME=openclaw|claude|auto (default: auto)
10
+ * - auto picks openclaw claude error (no supported CLI)
16
11
  */
17
12
 
18
13
  const { execSync, spawnSync } = require('child_process');
19
14
  const { createLogger } = require('./logger');
15
+ const { runClaudeTurn: invokeClaudeTurn, buildSubagentSystemPrompt, runClaudeSummary } = require('./claude-subagent');
16
+ const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
20
17
 
21
18
  function commandExists(command) {
22
19
  try {
@@ -34,33 +31,40 @@ function cleanText(value, maxLength = 300) {
34
31
  .slice(0, maxLength);
35
32
  }
36
33
 
37
- function normalizeCommandText(command) {
38
- return String(command || '').trim().slice(0, 160);
39
- }
40
-
41
- function payloadAuditLength(payload) {
42
- const raw = JSON.stringify(payload || {});
43
- return Number.isFinite(raw?.length) ? raw.length : 0;
44
- }
45
-
46
- function toBool(value, fallback = true) {
47
- if (value === undefined || value === null || value === '') {
48
- return fallback;
49
- }
50
- const normalized = String(value).trim().toLowerCase();
51
- return !(normalized === '0' || normalized === 'false' || normalized === 'no');
52
- }
53
34
 
54
35
  function resolveRuntimeMode() {
55
36
  const requested = String(process.env.A2A_RUNTIME || 'auto').trim().toLowerCase();
56
37
  const hasOpenClaw = commandExists('openclaw');
38
+ const hasClaude = commandExists('claude');
57
39
 
58
40
  if (requested === 'generic') {
59
41
  return {
60
- mode: 'generic',
42
+ mode: 'none',
43
+ requested,
44
+ hasOpenClaw,
45
+ hasClaude,
46
+ warning: 'A2A_RUNTIME=generic is no longer supported. Use openclaw or claude runtime.',
47
+ reason: 'unsupported-generic-mode'
48
+ };
49
+ }
50
+
51
+ if (requested === 'claude') {
52
+ if (hasClaude) {
53
+ return {
54
+ mode: 'claude',
55
+ requested,
56
+ hasOpenClaw,
57
+ hasClaude,
58
+ reason: 'A2A_RUNTIME=claude'
59
+ };
60
+ }
61
+ return {
62
+ mode: 'none',
61
63
  requested,
62
64
  hasOpenClaw,
63
- reason: 'A2A_RUNTIME=generic'
65
+ hasClaude,
66
+ warning: 'A2A_RUNTIME=claude but claude CLI not found; install claude CLI or switch to openclaw',
67
+ reason: 'forced-claude-missing'
64
68
  };
65
69
  }
66
70
 
@@ -70,32 +74,48 @@ function resolveRuntimeMode() {
70
74
  mode: 'openclaw',
71
75
  requested,
72
76
  hasOpenClaw,
77
+ hasClaude,
73
78
  reason: 'A2A_RUNTIME=openclaw'
74
79
  };
75
80
  }
76
81
  return {
77
- mode: 'generic',
82
+ mode: 'none',
78
83
  requested,
79
84
  hasOpenClaw,
80
- warning: 'A2A_RUNTIME=openclaw but openclaw CLI not found, falling back to generic runtime',
85
+ hasClaude,
86
+ warning: 'A2A_RUNTIME=openclaw but openclaw CLI not found; install openclaw CLI or switch to claude',
81
87
  reason: 'forced-openclaw-missing'
82
88
  };
83
89
  }
84
90
 
91
+ // Auto detection chain: openclaw → claude → none
85
92
  if (hasOpenClaw) {
86
93
  return {
87
94
  mode: 'openclaw',
88
95
  requested: 'auto',
89
96
  hasOpenClaw,
97
+ hasClaude,
90
98
  reason: 'openclaw CLI detected'
91
99
  };
92
100
  }
93
101
 
102
+ if (hasClaude) {
103
+ return {
104
+ mode: 'claude',
105
+ requested: 'auto',
106
+ hasOpenClaw,
107
+ hasClaude,
108
+ reason: 'claude CLI detected'
109
+ };
110
+ }
111
+
94
112
  return {
95
- mode: 'generic',
113
+ mode: 'none',
96
114
  requested: 'auto',
97
115
  hasOpenClaw,
98
- reason: 'openclaw CLI not detected'
116
+ hasClaude,
117
+ warning: 'No supported runtime CLI found. Install openclaw or claude CLI.',
118
+ reason: 'no supported CLI detected'
99
119
  };
100
120
  }
101
121
 
@@ -111,152 +131,12 @@ function normalizeOpenClawOutput(raw) {
111
131
  return lines.join('\n').trim();
112
132
  }
113
133
 
114
- function parseCommandTextOutput(rawOutput, keys = ['response', 'text', 'message']) {
115
- const output = String(rawOutput || '').trim();
116
- if (!output) {
117
- return '';
118
- }
119
-
120
- try {
121
- const parsed = JSON.parse(output);
122
- if (parsed && typeof parsed === 'object') {
123
- for (const key of keys) {
124
- if (typeof parsed[key] === 'string' && parsed[key].trim()) {
125
- return parsed[key].trim();
126
- }
127
- }
128
- }
129
- } catch (err) {
130
- // Plain text output is valid for bridge commands.
131
- }
132
-
133
- return output;
134
- }
135
-
136
- function parseSummaryOutput(rawOutput) {
137
- const output = String(rawOutput || '').trim();
138
- if (!output) {
139
- return null;
140
- }
141
-
142
- try {
143
- const parsed = JSON.parse(output);
144
- if (parsed && typeof parsed === 'object') {
145
- const summary = cleanText(parsed.summary || parsed.text || parsed.message, 1500);
146
- return {
147
- ...parsed,
148
- summary: summary || null,
149
- ownerSummary: cleanText(
150
- parsed.ownerSummary || parsed.owner_summary || summary || '',
151
- 1500
152
- ) || null
153
- };
154
- }
155
- } catch (err) {
156
- // Plain text is also acceptable.
157
- }
158
-
159
- const summary = cleanText(output, 1500);
160
- return {
161
- summary,
162
- ownerSummary: summary
163
- };
164
- }
165
-
166
- function runCommand(command, payload, options = {}) {
167
- const payloadJson = JSON.stringify(payload || {});
168
- const timeoutMs = options.timeoutMs || 60000;
169
- return execSync(command, {
170
- encoding: 'utf8',
171
- timeout: timeoutMs,
172
- stdio: ['pipe', 'pipe', 'pipe'],
173
- input: payloadJson,
174
- cwd: options.cwd || process.cwd(),
175
- env: {
176
- ...process.env,
177
- A2A_PAYLOAD_JSON: payloadJson
178
- }
179
- });
180
- }
181
-
182
- function escapeCliValue(value) {
183
- return String(value || '')
184
- .replace(/\\/g, '\\\\') // Backslashes first
185
- .replace(/"/g, '\\"') // Double quotes
186
- .replace(/\$/g, '\\$') // Dollar signs (variable expansion)
187
- .replace(/`/g, '\\`') // Backticks (command substitution)
188
- .replace(/!/g, '\\!') // History expansion in some shells
189
- .replace(/\n/g, '\\n') // Newlines
190
- .replace(/\r/g, ''); // Carriage returns
191
- }
192
-
193
- function buildFallbackResponse(message, context = {}, reason = null) {
194
- const callerName = cleanText(context.callerName || context.caller?.name || 'caller');
195
- const ownerName = cleanText(context.ownerName || 'the owner');
196
- const allowedTopics = Array.isArray(context.allowedTopics)
197
- ? context.allowedTopics.filter(Boolean).slice(0, 4)
198
- : [];
199
- const topicText = allowedTopics.length > 0
200
- ? allowedTopics.join(', ')
201
- : 'permitted topics';
202
- const excerpt = cleanText(message, 220) || 'No message content provided.';
203
-
204
- let prefix = `I am running in generic A2A mode for ${ownerName}.`;
205
- if (reason) {
206
- prefix = `I switched to generic fallback mode (${cleanText(reason, 120)}).`;
207
- }
208
-
209
- return `${prefix} I received from ${callerName}: "${excerpt}". ` +
210
- `We can still work through concrete overlap on ${topicText} and line up actionable next steps. ` +
211
- `What outcome should we target first?`;
212
- }
213
-
214
- function buildFallbackSummary(messages = [], callerInfo = {}, reason = null) {
215
- const inbound = messages.filter(m => m.direction === 'inbound');
216
- const outbound = messages.filter(m => m.direction !== 'inbound');
217
- const caller = cleanText(callerInfo?.name || 'Unknown caller');
218
- const lastInbound = inbound.length > 0
219
- ? cleanText(inbound[inbound.length - 1].content, 220)
220
- : '';
221
-
222
- const summary = [
223
- `Call concluded with ${caller}.`,
224
- `Inbound turns: ${inbound.length}. Outbound turns: ${outbound.length}.`,
225
- lastInbound ? `Latest caller request: "${lastInbound}".` : '',
226
- reason ? `Summary mode: ${cleanText(reason, 140)}.` : 'Summary mode: generic fallback.'
227
- ].filter(Boolean).join(' ');
228
-
229
- return {
230
- summary,
231
- ownerSummary: summary,
232
- relevance: 'unknown',
233
- goalsTouched: [],
234
- ownerActionItems: [],
235
- callerActionItems: [],
236
- jointActionItems: [],
237
- collaborationOpportunity: {
238
- found: false,
239
- rationale: 'Generic fallback summary (no platform-specific summarizer configured)'
240
- },
241
- followUp: lastInbound
242
- ? `Clarify the next concrete step for: ${lastInbound}`
243
- : 'Ask both owners to confirm desired follow-up scope.',
244
- notes: reason
245
- ? `Fallback summary generated after runtime issue: ${cleanText(reason, 180)}`
246
- : 'Fallback summary generated by generic runtime.'
247
- };
248
- }
249
134
 
250
135
  function createRuntimeAdapter(options = {}) {
251
136
  const workspaceDir = options.workspaceDir || process.cwd();
252
137
  const modeInfo = resolveRuntimeMode();
253
- const failoverEnabled = toBool(process.env.A2A_RUNTIME_FAILOVER, true);
254
138
  const logger = options.logger || createLogger({ component: 'a2a.runtime' });
255
139
 
256
- const genericAgentCommand = process.env.A2A_AGENT_COMMAND || '';
257
- const genericSummaryCommand = process.env.A2A_SUMMARY_COMMAND || '';
258
- const genericNotifyCommand = process.env.A2A_NOTIFY_COMMAND || '';
259
-
260
140
  logger.info('Runtime adapter initialized', {
261
141
  event: 'runtime_initialized',
262
142
  data: {
@@ -264,10 +144,107 @@ function createRuntimeAdapter(options = {}) {
264
144
  requested_mode: modeInfo.requested,
265
145
  reason: modeInfo.reason,
266
146
  has_openclaw: modeInfo.hasOpenClaw,
267
- failover_enabled: failoverEnabled
147
+ has_claude: modeInfo.hasClaude
268
148
  }
269
149
  });
270
150
 
151
+ // Claude subagent session tracking
152
+ const claudeSessions = new Map();
153
+
154
+ async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
155
+ const traceId = context?.traceId || context?.trace_id;
156
+ const requestId = context?.requestId || context?.request_id;
157
+ const conversationId = context?.conversationId || context?.conversation_id;
158
+ const startAt = Date.now();
159
+
160
+ // Get or create session state
161
+ let session = claudeSessions.get(sessionId);
162
+ if (!session) {
163
+ // First turn: build the system prompt from disclosure context
164
+ const manifest = loadManifest();
165
+ const tierTopics = getTopicsForTier(context?.tier || 'public');
166
+ const formatted = formatTopicsForPrompt(tierTopics);
167
+
168
+ const systemPrompt = buildSubagentSystemPrompt({
169
+ agentName: context?.agentName || 'Agent',
170
+ ownerName: context?.ownerName || 'the owner',
171
+ otherAgentName: caller?.name || 'Remote Agent',
172
+ otherOwnerName: caller?.owner || 'their owner',
173
+ accessTier: context?.tier || 'public',
174
+ tierTopics: formatted.topics,
175
+ tierObjectives: formatted.objectives,
176
+ doNotDiscuss: formatted.doNotDiscuss,
177
+ neverDisclose: formatted.neverDisclose,
178
+ personalityNotes: manifest.personality_notes || '',
179
+ roleContext: context?.roleContext || ''
180
+ });
181
+
182
+ session = { claudeSessionId: null, systemPrompt, turnCount: 0, lastMeta: null };
183
+ claudeSessions.set(sessionId, session);
184
+ }
185
+
186
+ session.turnCount++;
187
+
188
+ logger.debug('Invoking Claude subagent turn', {
189
+ event: 'claude_turn_start',
190
+ traceId,
191
+ requestId,
192
+ conversationId,
193
+ data: {
194
+ session_id: sessionId,
195
+ turn: session.turnCount,
196
+ timeout_ms: timeoutMs
197
+ }
198
+ });
199
+
200
+ const result = await invokeClaudeTurn({
201
+ sessionId: session.claudeSessionId,
202
+ systemPrompt: session.systemPrompt,
203
+ turnMessage: message,
204
+ turn: session.turnCount,
205
+ maxTurns: context?.maxTurns || 30,
206
+ phase: context?.phase || 'handshake',
207
+ overlapScore: context?.overlapScore || 0.15,
208
+ activeThreads: context?.activeThreads || [],
209
+ candidateCollaborations: context?.candidateCollaborations || [],
210
+ closeSignal: context?.closeSignal || false,
211
+ timeoutMs: timeoutMs || 180000
212
+ });
213
+
214
+ // Store session ID from first turn for subsequent --resume
215
+ if (result.sessionId) {
216
+ session.claudeSessionId = result.sessionId;
217
+ }
218
+
219
+ // Store flags/state for retrieval via getLastTurnMeta
220
+ session.lastMeta = {
221
+ statePatch: result.statePatch,
222
+ flags: result.flags
223
+ };
224
+
225
+ logger.debug('Claude subagent turn completed', {
226
+ event: 'claude_turn_complete',
227
+ traceId,
228
+ requestId,
229
+ conversationId,
230
+ data: {
231
+ session_id: sessionId,
232
+ turn: session.turnCount,
233
+ duration_ms: Date.now() - startAt,
234
+ message_length: result.message.length,
235
+ has_state_patch: Boolean(result.statePatch),
236
+ flag_count: result.flags.length
237
+ }
238
+ });
239
+
240
+ return result.message;
241
+ }
242
+
243
+ function getLastTurnMeta(sessionId) {
244
+ const session = claudeSessions.get(sessionId);
245
+ return session?.lastMeta || null;
246
+ }
247
+
271
248
  async function runOpenClawTurn({ sessionId, prompt, timeoutMs }) {
272
249
  const timeoutSeconds = Math.max(5, Math.min(300, Math.round((timeoutMs || 65000) / 1000)));
273
250
  // Use spawnSync with stdin to avoid shell escaping issues with complex prompts
@@ -328,185 +305,34 @@ function createRuntimeAdapter(options = {}) {
328
305
  ], { timeout: 10000, stdio: 'pipe' });
329
306
  }
330
307
 
331
- async function runGenericTurn({ message, caller, context, runtimeError }) {
332
- const payload = {
333
- mode: 'a2a-turn',
334
- message,
335
- caller: caller || {},
336
- context: context || {}
337
- };
308
+ async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
338
309
  const traceId = context?.traceId || context?.trace_id;
339
310
  const requestId = context?.requestId || context?.request_id;
340
311
  const conversationId = context?.conversationId || context?.conversation_id;
341
- const startAt = Date.now();
342
312
 
343
- logger.debug('Invoking generic agent command', {
344
- event: 'generic_agent_command_start',
345
- traceId,
346
- requestId,
347
- conversationId,
348
- data: {
349
- command: normalizeCommandText(genericAgentCommand),
350
- payload_bytes: payloadAuditLength(payload)
351
- }
352
- });
353
-
354
- if (genericAgentCommand) {
313
+ if (modeInfo.mode === 'claude') {
355
314
  try {
356
- const output = runCommand(genericAgentCommand, payload, {
357
- timeoutMs: context?.timeoutMs || 65000
358
- });
359
- const text = parseCommandTextOutput(output);
360
- logger.debug('Generic agent command completed', {
361
- event: 'generic_agent_command_complete',
362
- traceId,
363
- requestId,
364
- conversationId,
365
- data: {
366
- command: normalizeCommandText(genericAgentCommand),
367
- duration_ms: Date.now() - startAt,
368
- output_length: String(output || '').length
369
- }
370
- });
371
- if (text) {
372
- return text;
373
- }
315
+ return await runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs });
374
316
  } catch (err) {
375
- runtimeError = err.message;
376
- logger.error('Generic agent command failed', {
377
- event: 'generic_agent_command_failed',
317
+ logger.error('Claude subagent turn failed', {
318
+ event: 'claude_turn_failed',
378
319
  traceId,
379
320
  requestId,
380
321
  conversationId,
381
- error_code: 'GENERIC_AGENT_COMMAND_FAILED',
382
- hint: 'Verify A2A_AGENT_COMMAND exits 0 and returns valid text/JSON response.',
322
+ error_code: 'CLAUDE_TURN_FAILED',
323
+ hint: 'Inspect Claude CLI availability, timeout settings, and CLAUDECODE env var.',
383
324
  error: err,
384
- data: {
385
- command_present: Boolean(genericAgentCommand),
386
- command: normalizeCommandText(genericAgentCommand),
387
- payload_bytes: payloadAuditLength(payload),
388
- duration_ms: Date.now() - startAt
389
- }
325
+ data: { session_id: sessionId, timeout_ms: timeoutMs }
390
326
  });
327
+ throw err;
391
328
  }
392
329
  }
393
330
 
394
- return buildFallbackResponse(message, {
395
- caller,
396
- callerName: caller?.name,
397
- ownerName: context?.ownerName,
398
- allowedTopics: context?.allowedTopics
399
- }, runtimeError);
400
- }
401
-
402
- async function runGenericSummary({ messages, callerInfo, reason }) {
403
- const payload = {
404
- mode: 'a2a-summary',
405
- messages,
406
- caller: callerInfo || {}
407
- };
408
- const traceId = callerInfo?.trace_id || callerInfo?.traceId;
409
- const requestId = callerInfo?.request_id || callerInfo?.requestId;
410
- const conversationId = callerInfo?.conversation_id || callerInfo?.conversationId;
411
- const startAt = Date.now();
412
-
413
- if (genericSummaryCommand) {
414
- try {
415
- const output = runCommand(genericSummaryCommand, payload, { timeoutMs: 35000 });
416
- const parsed = parseSummaryOutput(output);
417
- logger.debug('Generic summary command completed', {
418
- event: 'generic_summary_command_complete',
419
- traceId,
420
- requestId,
421
- conversationId,
422
- data: {
423
- command: normalizeCommandText(genericSummaryCommand),
424
- payload_bytes: payloadAuditLength(payload),
425
- output_length: String(output || '').length
426
- }
427
- });
428
- if (parsed && parsed.summary) {
429
- return parsed;
430
- }
431
- } catch (err) {
432
- reason = err.message;
433
- logger.error('Generic summary command failed', {
434
- event: 'generic_summary_command_failed',
435
- traceId,
436
- requestId,
437
- conversationId,
438
- error_code: 'GENERIC_SUMMARY_COMMAND_FAILED',
439
- hint: 'Verify A2A_SUMMARY_COMMAND returns JSON with summary field or plain text.',
440
- error: err,
441
- data: {
442
- command_present: Boolean(genericSummaryCommand),
443
- command: normalizeCommandText(genericSummaryCommand),
444
- payload_bytes: payloadAuditLength(payload),
445
- duration_ms: Date.now() - startAt
446
- }
447
- });
448
- }
449
- }
450
-
451
- return buildFallbackSummary(messages, callerInfo, reason);
452
- }
453
-
454
- async function runGenericNotify(payload) {
455
- if (!genericNotifyCommand) {
456
- return;
457
- }
458
- const traceId = payload?.trace_id || payload?.traceId;
459
- const requestId = payload?.request_id || payload?.requestId;
460
- const conversationId = payload?.conversationId;
461
- const startAt = Date.now();
462
- logger.debug('Invoking generic notify command', {
463
- event: 'generic_notify_command_start',
464
- traceId,
465
- requestId,
466
- conversationId,
467
- data: {
468
- command: normalizeCommandText(genericNotifyCommand),
469
- payload_bytes: payloadAuditLength(payload)
470
- }
471
- });
472
- try {
473
- runCommand(genericNotifyCommand, payload, { timeoutMs: 10000 });
474
- logger.debug('Generic notify command completed', {
475
- event: 'generic_notify_command_complete',
476
- traceId,
477
- requestId,
478
- conversationId,
479
- data: {
480
- command: normalizeCommandText(genericNotifyCommand),
481
- duration_ms: Date.now() - startAt
482
- }
483
- });
484
- } catch (err) {
485
- logger.error('Generic notify command failed', {
486
- event: 'generic_notify_command_failed',
487
- traceId,
488
- requestId,
489
- conversationId,
490
- tokenId: payload?.token?.id,
491
- error_code: 'GENERIC_NOTIFY_COMMAND_FAILED',
492
- hint: 'Validate A2A_NOTIFY_COMMAND and downstream notifier transport availability.',
493
- error: err,
494
- data: {
495
- command_present: Boolean(genericNotifyCommand),
496
- command: normalizeCommandText(genericNotifyCommand),
497
- payload_bytes: payloadAuditLength(payload),
498
- duration_ms: Date.now() - startAt
499
- }
500
- });
501
- }
502
- }
503
-
504
- async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
505
- const traceId = context?.traceId || context?.trace_id;
506
- const requestId = context?.requestId || context?.request_id;
507
- const conversationId = context?.conversationId || context?.conversation_id;
508
331
  if (modeInfo.mode !== 'openclaw') {
509
- return runGenericTurn({ message, caller, context });
332
+ throw new Error(
333
+ `No supported A2A runtime available (mode=${modeInfo.mode}). ` +
334
+ 'Install the openclaw or claude CLI and set A2A_RUNTIME accordingly.'
335
+ );
510
336
  }
511
337
 
512
338
  const startAt = Date.now();
@@ -535,42 +361,21 @@ function createRuntimeAdapter(options = {}) {
535
361
  });
536
362
  return response;
537
363
  } catch (err) {
538
- if (!failoverEnabled) {
539
- logger.error('OpenClaw turn failed', {
540
- event: 'openclaw_turn_failed',
541
- traceId,
542
- requestId,
543
- conversationId,
544
- error_code: 'OPENCLAW_TURN_FAILED',
545
- hint: 'Inspect OpenClaw CLI output, timeout settings, and environment PATH.',
546
- error: err,
547
- data: {
548
- session_id: sessionId,
549
- timeout_ms: timeoutMs,
550
- duration_ms: Date.now() - startAt
551
- }
552
- });
553
- throw err;
554
- }
555
- logger.warn('OpenClaw runtime failed, switching to generic fallback', {
556
- event: 'openclaw_turn_failed_fallback',
364
+ logger.error('OpenClaw turn failed', {
365
+ event: 'openclaw_turn_failed',
557
366
  traceId,
558
367
  requestId,
559
368
  conversationId,
560
- error_code: 'OPENCLAW_TURN_FAILED_FALLBACK',
561
- hint: 'Inspect OpenClaw CLI health or set A2A_RUNTIME=generic for explicit fallback mode.',
369
+ error_code: 'OPENCLAW_TURN_FAILED',
370
+ hint: 'Inspect OpenClaw CLI output, timeout settings, and environment PATH.',
562
371
  error: err,
563
372
  data: {
564
- duration_ms: Date.now() - startAt,
565
- failover_enabled: failoverEnabled
373
+ session_id: sessionId,
374
+ timeout_ms: timeoutMs,
375
+ duration_ms: Date.now() - startAt
566
376
  }
567
377
  });
568
- return runGenericTurn({
569
- message,
570
- caller,
571
- context,
572
- runtimeError: `openclaw runtime unavailable: ${err.message}`
573
- });
378
+ throw err;
574
379
  }
575
380
  }
576
381
 
@@ -578,9 +383,26 @@ function createRuntimeAdapter(options = {}) {
578
383
  const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
579
384
  const requestId = callerInfo?.request_id || callerInfo?.requestId;
580
385
  const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
386
+
387
+ // Claude mode: use the subagent session for summarization
388
+ if (modeInfo.mode === 'claude') {
389
+ const session = claudeSessions.get(sessionId);
390
+ if (session?.claudeSessionId) {
391
+ const result = await runClaudeSummary(session.claudeSessionId, 'conversation ended');
392
+ if (result && result.summary) {
393
+ return result;
394
+ }
395
+ }
396
+ throw new Error('Claude summary session not available or returned empty result');
397
+ }
398
+
581
399
  if (modeInfo.mode !== 'openclaw') {
582
- return runGenericSummary({ messages, callerInfo });
400
+ throw new Error(
401
+ `No supported A2A runtime available for summarization (mode=${modeInfo.mode}). ` +
402
+ 'Install the openclaw or claude CLI and set A2A_RUNTIME accordingly.'
403
+ );
583
404
  }
405
+
584
406
  const startAt = Date.now();
585
407
  logger.debug('Invoking openclaw summary', {
586
408
  event: 'openclaw_summary_start',
@@ -612,72 +434,27 @@ function createRuntimeAdapter(options = {}) {
612
434
  });
613
435
  return result;
614
436
  }
615
- logger.warn('OpenClaw summary returned empty output; using generic fallback', {
616
- event: 'openclaw_summary_empty',
617
- traceId: effectiveTraceId,
618
- requestId,
619
- conversationId: effectiveConversationId,
620
- data: {
621
- session_id: sessionId,
622
- duration_ms: Date.now() - startAt
623
- }
624
- });
625
- return runGenericSummary({
626
- messages,
627
- callerInfo,
628
- reason: 'empty summary from openclaw runtime'
629
- });
437
+ throw new Error('OpenClaw summary returned empty output');
630
438
  } catch (err) {
631
- if (!failoverEnabled) {
632
- logger.error('OpenClaw summary failed', {
633
- event: 'openclaw_summary_failed',
634
- traceId: effectiveTraceId,
635
- requestId,
636
- conversationId: effectiveConversationId,
637
- error_code: 'OPENCLAW_SUMMARY_FAILED',
638
- hint: 'Inspect summary message length, timeout configuration, and CLI stderr output.',
639
- error: err,
640
- data: {
641
- session_id: sessionId,
642
- duration_ms: Date.now() - startAt
643
- }
644
- });
645
- throw err;
646
- }
647
- logger.warn('OpenClaw summary failed, using generic fallback', {
648
- event: 'openclaw_summary_failed_fallback',
439
+ logger.error('OpenClaw summary failed', {
440
+ event: 'openclaw_summary_failed',
649
441
  traceId: effectiveTraceId,
650
442
  requestId,
651
443
  conversationId: effectiveConversationId,
652
- error_code: 'OPENCLAW_SUMMARY_FAILED_FALLBACK',
653
- hint: 'Inspect OpenClaw summary session output and summarizer prompt input.',
444
+ error_code: 'OPENCLAW_SUMMARY_FAILED',
445
+ hint: 'Inspect summary message length, timeout configuration, and CLI stderr output.',
654
446
  error: err,
655
447
  data: {
656
448
  session_id: sessionId,
657
- duration_ms: Date.now() - startAt,
658
- failover_enabled: failoverEnabled
449
+ duration_ms: Date.now() - startAt
659
450
  }
660
451
  });
661
- return runGenericSummary({
662
- messages,
663
- callerInfo,
664
- reason: `openclaw summary unavailable: ${err.message}`
665
- });
452
+ throw err;
666
453
  }
667
454
  }
668
455
 
669
456
  async function notify({ level, token, caller, message, conversationId, traceId }) {
670
457
  const requestId = token?.request_id || token?.requestId || null;
671
- const payload = {
672
- mode: 'a2a-notify',
673
- level,
674
- token: token || null,
675
- caller: caller || null,
676
- message,
677
- conversationId,
678
- traceId,
679
- requestId
680
- };
681
458
 
682
459
  logger.debug('Owner notify requested', {
683
460
  event: 'notify_requested',
@@ -688,8 +465,27 @@ function createRuntimeAdapter(options = {}) {
688
465
  data: { level }
689
466
  });
690
467
 
468
+ if (modeInfo.mode === 'claude') {
469
+ // Claude mode: notifications are a no-op (no notification transport available)
470
+ logger.debug('Notification skipped (claude mode has no notification transport)', {
471
+ event: 'notify_skipped_claude',
472
+ traceId,
473
+ requestId,
474
+ conversationId,
475
+ tokenId: token?.id
476
+ });
477
+ return;
478
+ }
479
+
691
480
  if (modeInfo.mode !== 'openclaw') {
692
- return runGenericNotify(payload);
481
+ logger.debug('Notification skipped (no supported runtime)', {
482
+ event: 'notify_skipped_no_runtime',
483
+ traceId,
484
+ requestId,
485
+ conversationId,
486
+ tokenId: token?.id
487
+ });
488
+ return;
693
489
  }
694
490
 
695
491
  if (level !== 'all') {
@@ -713,31 +509,20 @@ function createRuntimeAdapter(options = {}) {
713
509
  }
714
510
  });
715
511
  } catch (err) {
716
- if (!failoverEnabled) {
717
- throw err;
718
- }
719
- logger.warn('OpenClaw notify failed, running generic notifier', {
720
- event: 'openclaw_notify_failed_fallback',
512
+ // Notifications are best-effort; log and swallow
513
+ logger.warn('OpenClaw notify failed', {
514
+ event: 'openclaw_notify_failed',
721
515
  traceId,
722
516
  requestId,
723
517
  conversationId,
724
518
  tokenId: token?.id,
725
- error_code: 'OPENCLAW_NOTIFY_FAILED_FALLBACK',
519
+ error_code: 'OPENCLAW_NOTIFY_FAILED',
726
520
  hint: 'Check OpenClaw messaging channel config and notify permissions.',
727
521
  error: err,
728
522
  data: {
729
- failover_enabled: failoverEnabled,
730
523
  duration_ms: Date.now() - notifyStart
731
524
  }
732
525
  });
733
- logger.debug('OpenClaw notify fallback to generic notifier', {
734
- event: 'openclaw_notify_generic_fallback',
735
- traceId,
736
- requestId,
737
- conversationId,
738
- tokenId: token?.id
739
- });
740
- await runGenericNotify(payload);
741
526
  }
742
527
  }
743
528
 
@@ -745,18 +530,17 @@ function createRuntimeAdapter(options = {}) {
745
530
  mode: modeInfo.mode,
746
531
  requestedMode: modeInfo.requested,
747
532
  hasOpenClaw: modeInfo.hasOpenClaw,
533
+ hasClaude: modeInfo.hasClaude,
748
534
  reason: modeInfo.reason,
749
535
  warning: modeInfo.warning || null,
750
- failoverEnabled,
751
536
  runTurn,
752
537
  summarize,
753
538
  notify,
754
- buildFallbackResponse
539
+ getLastTurnMeta
755
540
  };
756
541
  }
757
542
 
758
543
  module.exports = {
759
544
  createRuntimeAdapter,
760
- resolveRuntimeMode,
761
- buildFallbackResponse
545
+ resolveRuntimeMode
762
546
  };