a2acalling 0.5.3 → 0.5.4
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 +19 -0
- package/docs/protocol.md +7 -0
- package/package.json +1 -1
- package/scripts/install-openclaw.js +57 -13
- package/src/lib/runtime-adapter.js +223 -15
- package/src/routes/a2a.js +86 -27
- package/src/routes/dashboard.js +122 -0
- package/src/server.js +18 -15
package/README.md
CHANGED
|
@@ -208,6 +208,7 @@ Dashboard/API log routes:
|
|
|
208
208
|
- `GET /api/a2a/dashboard/logs`
|
|
209
209
|
- `GET /api/a2a/dashboard/logs/trace/:traceId`
|
|
210
210
|
- `GET /api/a2a/dashboard/logs/stats`
|
|
211
|
+
- `GET /api/a2a/dashboard/debug/call?trace_id=<id>` (or `conversation_id=<id>`)
|
|
211
212
|
|
|
212
213
|
Useful filters for `/api/a2a/dashboard/logs`:
|
|
213
214
|
|
|
@@ -221,6 +222,24 @@ Example:
|
|
|
221
222
|
curl "http://localhost:3001/api/a2a/dashboard/logs?trace_id=trace_abc123&error_code=TOKEN_INVALID_OR_EXPIRED"
|
|
222
223
|
```
|
|
223
224
|
|
|
225
|
+
### Incoming Call Debug
|
|
226
|
+
|
|
227
|
+
Every `/api/a2a/invoke` and `/api/a2a/end` response now returns:
|
|
228
|
+
- `trace_id` (generated when caller does not send one)
|
|
229
|
+
- `request_id` (generated when caller does not send one)
|
|
230
|
+
|
|
231
|
+
To inspect one call, use the dashboard debug endpoint:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
curl -H "x-admin-token: $A2A_ADMIN_TOKEN" \
|
|
235
|
+
"http://localhost:3001/api/a2a/dashboard/debug/call?trace_id=<trace_id>"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For each call you get:
|
|
239
|
+
- `summary` (event count, first/last seen, duration, and IDs involved)
|
|
240
|
+
- `errors` and `error_codes` for fast triage
|
|
241
|
+
- `logs` (ordered timeline events from that trace)
|
|
242
|
+
|
|
224
243
|
## 📡 Protocol
|
|
225
244
|
|
|
226
245
|
Tokens use the `a2a://` URI scheme:
|
package/docs/protocol.md
CHANGED
|
@@ -56,6 +56,8 @@ Headers:
|
|
|
56
56
|
```
|
|
57
57
|
Authorization: Bearer fed_abc123xyz
|
|
58
58
|
Content-Type: application/json
|
|
59
|
+
x-trace-id: trace_... (optional)
|
|
60
|
+
x-request-id: req_... (optional)
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Request body:
|
|
@@ -76,6 +78,8 @@ Success response:
|
|
|
76
78
|
```json
|
|
77
79
|
{
|
|
78
80
|
"success": true,
|
|
81
|
+
"trace_id": "trace_...",
|
|
82
|
+
"request_id": "req_...",
|
|
79
83
|
"conversation_id": "conv_123456",
|
|
80
84
|
"response": "The agent's response text",
|
|
81
85
|
"can_continue": true,
|
|
@@ -114,6 +118,8 @@ Success response:
|
|
|
114
118
|
```json
|
|
115
119
|
{
|
|
116
120
|
"success": true,
|
|
121
|
+
"trace_id": "trace_...",
|
|
122
|
+
"request_id": "req_...",
|
|
117
123
|
"conversation_id": "conv_123456",
|
|
118
124
|
"status": "concluded",
|
|
119
125
|
"summary": "Optional summary text"
|
|
@@ -143,6 +149,7 @@ Dashboard API endpoints:
|
|
|
143
149
|
- `GET /api/a2a/dashboard/logs`
|
|
144
150
|
- `GET /api/a2a/dashboard/logs/trace/:traceId`
|
|
145
151
|
- `GET /api/a2a/dashboard/logs/stats`
|
|
152
|
+
- `GET /api/a2a/dashboard/debug/call?trace_id=<id>`
|
|
146
153
|
|
|
147
154
|
Example filters for `/logs`: `trace_id`, `conversation_id`, `token_id`, `error_code`, `status_code`, `component`, `event`, `level`, `search`, `from`, `to`.
|
|
148
155
|
|
package/package.json
CHANGED
|
@@ -77,6 +77,53 @@ function loadOpenClawConfig() {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function normalizeDashboardPluginEntry(rawEntry, backendUrl) {
|
|
81
|
+
const issues = [];
|
|
82
|
+
const normalized = {
|
|
83
|
+
enabled: true,
|
|
84
|
+
config: {}
|
|
85
|
+
};
|
|
86
|
+
const entry = (rawEntry && typeof rawEntry === 'object') ? rawEntry : {};
|
|
87
|
+
|
|
88
|
+
const legacyBackendUrl = (typeof entry.backendUrl === 'string' && entry.backendUrl.trim())
|
|
89
|
+
? entry.backendUrl.trim()
|
|
90
|
+
: null;
|
|
91
|
+
const rawConfig = (entry.config && typeof entry.config === 'object') ? entry.config : null;
|
|
92
|
+
|
|
93
|
+
if (!entry || typeof entry !== 'object') {
|
|
94
|
+
issues.push('plugin entry is missing or invalid');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (entry && typeof entry.enabled === 'boolean') {
|
|
98
|
+
normalized.enabled = entry.enabled;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (rawConfig) {
|
|
102
|
+
normalized.config = { ...rawConfig };
|
|
103
|
+
} else if (entry.config !== undefined) {
|
|
104
|
+
issues.push('plugin entry has non-object config; replacing with empty object');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (legacyBackendUrl) {
|
|
108
|
+
issues.push(`legacy key detected: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl (using backendUrl migration)`);
|
|
109
|
+
normalized.config.backendUrl = backendUrl || legacyBackendUrl;
|
|
110
|
+
} else if (typeof normalized.config.backendUrl === 'string' && normalized.config.backendUrl.trim()) {
|
|
111
|
+
normalized.config.backendUrl = normalized.config.backendUrl.trim();
|
|
112
|
+
} else if (typeof backendUrl === 'string' && backendUrl.trim()) {
|
|
113
|
+
normalized.config.backendUrl = backendUrl.trim();
|
|
114
|
+
} else {
|
|
115
|
+
issues.push('backendUrl could not be determined; plugin may fail to route dashboard traffic');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
normalized,
|
|
120
|
+
issues,
|
|
121
|
+
changed: issues.length > 0,
|
|
122
|
+
legacyBackendUrl,
|
|
123
|
+
summary: `a2a-dashboard-proxy config => ${normalized.enabled ? 'enabled' : 'disabled'}, backendUrl=${normalized.config.backendUrl || 'missing'}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
function writeOpenClawConfig(config) {
|
|
81
128
|
const backupPath = `${OPENCLAW_CONFIG}.backup.${Date.now()}`;
|
|
82
129
|
fs.copyFileSync(OPENCLAW_CONFIG, backupPath);
|
|
@@ -573,20 +620,17 @@ async function install() {
|
|
|
573
620
|
config.plugins = config.plugins || {};
|
|
574
621
|
config.plugins.entries = config.plugins.entries || {};
|
|
575
622
|
const rawEntry = config.plugins.entries[DASHBOARD_PLUGIN_ID];
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
: {};
|
|
580
|
-
if (typeof existingEntry.backendUrl === 'string' && existingEntry.backendUrl) {
|
|
581
|
-
log(`Migrated legacy plugin key plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl -> plugins.entries.${DASHBOARD_PLUGIN_ID}.config.backendUrl`);
|
|
623
|
+
const audit = normalizeDashboardPluginEntry(rawEntry, backendUrl);
|
|
624
|
+
for (const issue of audit.issues) {
|
|
625
|
+
warn(`a2a-dashboard-proxy config issue: ${issue}`);
|
|
582
626
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
627
|
+
if (audit.legacyBackendUrl) {
|
|
628
|
+
warn(`Auto-fixing legacy key: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl`);
|
|
629
|
+
}
|
|
630
|
+
if (audit.changed) {
|
|
631
|
+
log(`Migrated dashboard plugin config: ${audit.summary}`);
|
|
632
|
+
}
|
|
633
|
+
config.plugins.entries[DASHBOARD_PLUGIN_ID] = audit.normalized;
|
|
590
634
|
configUpdated = true;
|
|
591
635
|
log(`Configured gateway plugin entry: ${DASHBOARD_PLUGIN_ID}`);
|
|
592
636
|
}
|
|
@@ -34,6 +34,15 @@ function cleanText(value, maxLength = 300) {
|
|
|
34
34
|
.slice(0, maxLength);
|
|
35
35
|
}
|
|
36
36
|
|
|
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
|
+
|
|
37
46
|
function toBool(value, fallback = true) {
|
|
38
47
|
if (value === undefined || value === null || value === '') {
|
|
39
48
|
return fallback;
|
|
@@ -326,6 +335,21 @@ function createRuntimeAdapter(options = {}) {
|
|
|
326
335
|
caller: caller || {},
|
|
327
336
|
context: context || {}
|
|
328
337
|
};
|
|
338
|
+
const traceId = context?.traceId || context?.trace_id;
|
|
339
|
+
const requestId = context?.requestId || context?.request_id;
|
|
340
|
+
const conversationId = context?.conversationId || context?.conversation_id;
|
|
341
|
+
const startAt = Date.now();
|
|
342
|
+
|
|
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
|
+
});
|
|
329
353
|
|
|
330
354
|
if (genericAgentCommand) {
|
|
331
355
|
try {
|
|
@@ -333,6 +357,17 @@ function createRuntimeAdapter(options = {}) {
|
|
|
333
357
|
timeoutMs: context?.timeoutMs || 65000
|
|
334
358
|
});
|
|
335
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
|
+
});
|
|
336
371
|
if (text) {
|
|
337
372
|
return text;
|
|
338
373
|
}
|
|
@@ -340,13 +375,17 @@ function createRuntimeAdapter(options = {}) {
|
|
|
340
375
|
runtimeError = err.message;
|
|
341
376
|
logger.error('Generic agent command failed', {
|
|
342
377
|
event: 'generic_agent_command_failed',
|
|
343
|
-
traceId
|
|
344
|
-
|
|
378
|
+
traceId,
|
|
379
|
+
requestId,
|
|
380
|
+
conversationId,
|
|
345
381
|
error_code: 'GENERIC_AGENT_COMMAND_FAILED',
|
|
346
382
|
hint: 'Verify A2A_AGENT_COMMAND exits 0 and returns valid text/JSON response.',
|
|
347
383
|
error: err,
|
|
348
384
|
data: {
|
|
349
|
-
command_present: Boolean(genericAgentCommand)
|
|
385
|
+
command_present: Boolean(genericAgentCommand),
|
|
386
|
+
command: normalizeCommandText(genericAgentCommand),
|
|
387
|
+
payload_bytes: payloadAuditLength(payload),
|
|
388
|
+
duration_ms: Date.now() - startAt
|
|
350
389
|
}
|
|
351
390
|
});
|
|
352
391
|
}
|
|
@@ -366,11 +405,26 @@ function createRuntimeAdapter(options = {}) {
|
|
|
366
405
|
messages,
|
|
367
406
|
caller: callerInfo || {}
|
|
368
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();
|
|
369
412
|
|
|
370
413
|
if (genericSummaryCommand) {
|
|
371
414
|
try {
|
|
372
415
|
const output = runCommand(genericSummaryCommand, payload, { timeoutMs: 35000 });
|
|
373
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
|
+
});
|
|
374
428
|
if (parsed && parsed.summary) {
|
|
375
429
|
return parsed;
|
|
376
430
|
}
|
|
@@ -378,13 +432,17 @@ function createRuntimeAdapter(options = {}) {
|
|
|
378
432
|
reason = err.message;
|
|
379
433
|
logger.error('Generic summary command failed', {
|
|
380
434
|
event: 'generic_summary_command_failed',
|
|
381
|
-
traceId
|
|
382
|
-
|
|
435
|
+
traceId,
|
|
436
|
+
requestId,
|
|
437
|
+
conversationId,
|
|
383
438
|
error_code: 'GENERIC_SUMMARY_COMMAND_FAILED',
|
|
384
439
|
hint: 'Verify A2A_SUMMARY_COMMAND returns JSON with summary field or plain text.',
|
|
385
440
|
error: err,
|
|
386
441
|
data: {
|
|
387
|
-
command_present: Boolean(genericSummaryCommand)
|
|
442
|
+
command_present: Boolean(genericSummaryCommand),
|
|
443
|
+
command: normalizeCommandText(genericSummaryCommand),
|
|
444
|
+
payload_bytes: payloadAuditLength(payload),
|
|
445
|
+
duration_ms: Date.now() - startAt
|
|
388
446
|
}
|
|
389
447
|
});
|
|
390
448
|
}
|
|
@@ -397,19 +455,47 @@ function createRuntimeAdapter(options = {}) {
|
|
|
397
455
|
if (!genericNotifyCommand) {
|
|
398
456
|
return;
|
|
399
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
|
+
});
|
|
400
472
|
try {
|
|
401
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
|
+
});
|
|
402
484
|
} catch (err) {
|
|
403
485
|
logger.error('Generic notify command failed', {
|
|
404
486
|
event: 'generic_notify_command_failed',
|
|
405
|
-
traceId
|
|
406
|
-
|
|
487
|
+
traceId,
|
|
488
|
+
requestId,
|
|
489
|
+
conversationId,
|
|
407
490
|
tokenId: payload?.token?.id,
|
|
408
491
|
error_code: 'GENERIC_NOTIFY_COMMAND_FAILED',
|
|
409
492
|
hint: 'Validate A2A_NOTIFY_COMMAND and downstream notifier transport availability.',
|
|
410
493
|
error: err,
|
|
411
494
|
data: {
|
|
412
|
-
command_present: Boolean(genericNotifyCommand)
|
|
495
|
+
command_present: Boolean(genericNotifyCommand),
|
|
496
|
+
command: normalizeCommandText(genericNotifyCommand),
|
|
497
|
+
payload_bytes: payloadAuditLength(payload),
|
|
498
|
+
duration_ms: Date.now() - startAt
|
|
413
499
|
}
|
|
414
500
|
});
|
|
415
501
|
}
|
|
@@ -417,24 +503,65 @@ function createRuntimeAdapter(options = {}) {
|
|
|
417
503
|
|
|
418
504
|
async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
|
|
419
505
|
const traceId = context?.traceId || context?.trace_id;
|
|
506
|
+
const requestId = context?.requestId || context?.request_id;
|
|
507
|
+
const conversationId = context?.conversationId || context?.conversation_id;
|
|
420
508
|
if (modeInfo.mode !== 'openclaw') {
|
|
421
509
|
return runGenericTurn({ message, caller, context });
|
|
422
510
|
}
|
|
423
511
|
|
|
512
|
+
const startAt = Date.now();
|
|
513
|
+
logger.debug('Invoking openclaw turn', {
|
|
514
|
+
event: 'openclaw_turn_start',
|
|
515
|
+
traceId,
|
|
516
|
+
requestId,
|
|
517
|
+
conversationId,
|
|
518
|
+
data: {
|
|
519
|
+
session_id: sessionId,
|
|
520
|
+
timeout_ms: timeoutMs
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
424
524
|
try {
|
|
425
|
-
|
|
525
|
+
const response = await runOpenClawTurn({ sessionId, prompt, timeoutMs });
|
|
526
|
+
logger.debug('OpenClaw turn completed', {
|
|
527
|
+
event: 'openclaw_turn_complete',
|
|
528
|
+
traceId,
|
|
529
|
+
requestId,
|
|
530
|
+
conversationId,
|
|
531
|
+
data: {
|
|
532
|
+
session_id: sessionId,
|
|
533
|
+
duration_ms: Date.now() - startAt
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
return response;
|
|
426
537
|
} catch (err) {
|
|
427
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
|
+
});
|
|
428
553
|
throw err;
|
|
429
554
|
}
|
|
430
555
|
logger.warn('OpenClaw runtime failed, switching to generic fallback', {
|
|
431
556
|
event: 'openclaw_turn_failed_fallback',
|
|
432
557
|
traceId,
|
|
433
|
-
|
|
558
|
+
requestId,
|
|
559
|
+
conversationId,
|
|
434
560
|
error_code: 'OPENCLAW_TURN_FAILED_FALLBACK',
|
|
435
561
|
hint: 'Inspect OpenClaw CLI health or set A2A_RUNTIME=generic for explicit fallback mode.',
|
|
436
562
|
error: err,
|
|
437
563
|
data: {
|
|
564
|
+
duration_ms: Date.now() - startAt,
|
|
438
565
|
failover_enabled: failoverEnabled
|
|
439
566
|
}
|
|
440
567
|
});
|
|
@@ -448,9 +575,23 @@ function createRuntimeAdapter(options = {}) {
|
|
|
448
575
|
}
|
|
449
576
|
|
|
450
577
|
async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId }) {
|
|
578
|
+
const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
|
|
579
|
+
const requestId = callerInfo?.request_id || callerInfo?.requestId;
|
|
580
|
+
const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
|
|
451
581
|
if (modeInfo.mode !== 'openclaw') {
|
|
452
582
|
return runGenericSummary({ messages, callerInfo });
|
|
453
583
|
}
|
|
584
|
+
const startAt = Date.now();
|
|
585
|
+
logger.debug('Invoking openclaw summary', {
|
|
586
|
+
event: 'openclaw_summary_start',
|
|
587
|
+
traceId: effectiveTraceId,
|
|
588
|
+
requestId,
|
|
589
|
+
conversationId: effectiveConversationId,
|
|
590
|
+
data: {
|
|
591
|
+
session_id: sessionId,
|
|
592
|
+
message_count: Array.isArray(messages) ? messages.length : 0
|
|
593
|
+
}
|
|
594
|
+
});
|
|
454
595
|
|
|
455
596
|
try {
|
|
456
597
|
const result = await runOpenClawSummary({
|
|
@@ -459,8 +600,28 @@ function createRuntimeAdapter(options = {}) {
|
|
|
459
600
|
timeoutMs: 35000
|
|
460
601
|
});
|
|
461
602
|
if (result && result.summary) {
|
|
603
|
+
logger.debug('OpenClaw summary completed', {
|
|
604
|
+
event: 'openclaw_summary_complete',
|
|
605
|
+
traceId: effectiveTraceId,
|
|
606
|
+
requestId,
|
|
607
|
+
conversationId: effectiveConversationId,
|
|
608
|
+
data: {
|
|
609
|
+
session_id: sessionId,
|
|
610
|
+
duration_ms: Date.now() - startAt
|
|
611
|
+
}
|
|
612
|
+
});
|
|
462
613
|
return result;
|
|
463
614
|
}
|
|
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
|
+
});
|
|
464
625
|
return runGenericSummary({
|
|
465
626
|
messages,
|
|
466
627
|
callerInfo,
|
|
@@ -468,16 +629,32 @@ function createRuntimeAdapter(options = {}) {
|
|
|
468
629
|
});
|
|
469
630
|
} catch (err) {
|
|
470
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
|
+
});
|
|
471
645
|
throw err;
|
|
472
646
|
}
|
|
473
647
|
logger.warn('OpenClaw summary failed, using generic fallback', {
|
|
474
648
|
event: 'openclaw_summary_failed_fallback',
|
|
475
|
-
traceId:
|
|
476
|
-
|
|
649
|
+
traceId: effectiveTraceId,
|
|
650
|
+
requestId,
|
|
651
|
+
conversationId: effectiveConversationId,
|
|
477
652
|
error_code: 'OPENCLAW_SUMMARY_FAILED_FALLBACK',
|
|
478
653
|
hint: 'Inspect OpenClaw summary session output and summarizer prompt input.',
|
|
479
654
|
error: err,
|
|
480
655
|
data: {
|
|
656
|
+
session_id: sessionId,
|
|
657
|
+
duration_ms: Date.now() - startAt,
|
|
481
658
|
failover_enabled: failoverEnabled
|
|
482
659
|
}
|
|
483
660
|
});
|
|
@@ -490,6 +667,7 @@ function createRuntimeAdapter(options = {}) {
|
|
|
490
667
|
}
|
|
491
668
|
|
|
492
669
|
async function notify({ level, token, caller, message, conversationId, traceId }) {
|
|
670
|
+
const requestId = token?.request_id || token?.requestId || null;
|
|
493
671
|
const payload = {
|
|
494
672
|
mode: 'a2a-notify',
|
|
495
673
|
level,
|
|
@@ -497,9 +675,19 @@ function createRuntimeAdapter(options = {}) {
|
|
|
497
675
|
caller: caller || null,
|
|
498
676
|
message,
|
|
499
677
|
conversationId,
|
|
500
|
-
traceId
|
|
678
|
+
traceId,
|
|
679
|
+
requestId
|
|
501
680
|
};
|
|
502
681
|
|
|
682
|
+
logger.debug('Owner notify requested', {
|
|
683
|
+
event: 'notify_requested',
|
|
684
|
+
traceId,
|
|
685
|
+
requestId,
|
|
686
|
+
conversationId,
|
|
687
|
+
tokenId: token?.id,
|
|
688
|
+
data: { level }
|
|
689
|
+
});
|
|
690
|
+
|
|
503
691
|
if (modeInfo.mode !== 'openclaw') {
|
|
504
692
|
return runGenericNotify(payload);
|
|
505
693
|
}
|
|
@@ -510,9 +698,20 @@ function createRuntimeAdapter(options = {}) {
|
|
|
510
698
|
|
|
511
699
|
const callerName = caller?.name || 'Unknown';
|
|
512
700
|
const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
|
|
701
|
+
const notifyStart = Date.now();
|
|
513
702
|
|
|
514
703
|
try {
|
|
515
704
|
await runOpenClawNotify({ callerName, callerOwner, message: message || '' });
|
|
705
|
+
logger.debug('OpenClaw notify completed', {
|
|
706
|
+
event: 'openclaw_notify_complete',
|
|
707
|
+
traceId,
|
|
708
|
+
requestId,
|
|
709
|
+
conversationId,
|
|
710
|
+
tokenId: token?.id,
|
|
711
|
+
data: {
|
|
712
|
+
duration_ms: Date.now() - notifyStart
|
|
713
|
+
}
|
|
714
|
+
});
|
|
516
715
|
} catch (err) {
|
|
517
716
|
if (!failoverEnabled) {
|
|
518
717
|
throw err;
|
|
@@ -520,15 +719,24 @@ function createRuntimeAdapter(options = {}) {
|
|
|
520
719
|
logger.warn('OpenClaw notify failed, running generic notifier', {
|
|
521
720
|
event: 'openclaw_notify_failed_fallback',
|
|
522
721
|
traceId,
|
|
722
|
+
requestId,
|
|
523
723
|
conversationId,
|
|
524
724
|
tokenId: token?.id,
|
|
525
725
|
error_code: 'OPENCLAW_NOTIFY_FAILED_FALLBACK',
|
|
526
726
|
hint: 'Check OpenClaw messaging channel config and notify permissions.',
|
|
527
727
|
error: err,
|
|
528
728
|
data: {
|
|
529
|
-
failover_enabled: failoverEnabled
|
|
729
|
+
failover_enabled: failoverEnabled,
|
|
730
|
+
duration_ms: Date.now() - notifyStart
|
|
530
731
|
}
|
|
531
732
|
});
|
|
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
|
+
});
|
|
532
740
|
await runGenericNotify(payload);
|
|
533
741
|
}
|
|
534
742
|
}
|
package/src/routes/a2a.js
CHANGED
|
@@ -74,6 +74,32 @@ function resolveTraceId(req) {
|
|
|
74
74
|
return createTraceId('a2a');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function resolveRequestId(req) {
|
|
78
|
+
const headerRequestId = req.headers['x-request-id'];
|
|
79
|
+
if (typeof headerRequestId === 'string' && headerRequestId.trim()) {
|
|
80
|
+
return headerRequestId.trim().slice(0, 120);
|
|
81
|
+
}
|
|
82
|
+
return createTraceId('req');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractClientHost(req) {
|
|
86
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
87
|
+
if (typeof forwarded === 'string' && forwarded.trim()) {
|
|
88
|
+
return forwarded.split(',')[0].trim();
|
|
89
|
+
}
|
|
90
|
+
return req.ip || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeRequestMetadata(req) {
|
|
94
|
+
const body = req && typeof req.body === 'object' && req.body ? req.body : {};
|
|
95
|
+
return {
|
|
96
|
+
has_message: typeof body.message === 'string',
|
|
97
|
+
has_caller: Boolean(body.caller && typeof body.caller === 'object'),
|
|
98
|
+
has_context: Boolean(body.context && typeof body.context === 'object'),
|
|
99
|
+
timeout_seconds: body.timeout_seconds
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 }) {
|
|
78
104
|
const now = Date.now();
|
|
79
105
|
const minute = Math.floor(now / 60000);
|
|
@@ -177,14 +203,25 @@ function createRoutes(options = {}) {
|
|
|
177
203
|
router.post('/invoke', async (req, res) => {
|
|
178
204
|
const startedAt = Date.now();
|
|
179
205
|
const traceId = resolveTraceId(req);
|
|
180
|
-
const
|
|
206
|
+
const requestId = resolveRequestId(req);
|
|
207
|
+
const reqLogger = logger.child({ traceId, requestId, event: 'invoke' });
|
|
208
|
+
const withTracePayload = (payload) => ({ ...payload, trace_id: traceId, request_id: requestId });
|
|
181
209
|
res.set('x-trace-id', traceId);
|
|
210
|
+
res.set('x-request-id', requestId);
|
|
182
211
|
reqLogger.info('Received invoke request', {
|
|
183
212
|
data: {
|
|
184
213
|
ip: req.ip,
|
|
214
|
+
request_id: requestId,
|
|
215
|
+
client_host: extractClientHost(req),
|
|
216
|
+
forwarded_for: req.headers['x-forwarded-for'] || null,
|
|
217
|
+
user_agent: req.headers['user-agent'] || null,
|
|
185
218
|
has_auth_header: Boolean(req.headers.authorization)
|
|
186
219
|
}
|
|
187
220
|
});
|
|
221
|
+
reqLogger.debug('Invoke request metadata', {
|
|
222
|
+
event: 'invoke_request_metadata',
|
|
223
|
+
data: normalizeRequestMetadata(req)
|
|
224
|
+
});
|
|
188
225
|
|
|
189
226
|
// Extract token
|
|
190
227
|
const authHeader = req.headers.authorization;
|
|
@@ -194,11 +231,11 @@ function createRoutes(options = {}) {
|
|
|
194
231
|
status_code: 401,
|
|
195
232
|
hint: 'Send Authorization: Bearer <a2a_token>.'
|
|
196
233
|
});
|
|
197
|
-
return res.status(401).json({
|
|
234
|
+
return res.status(401).json(withTracePayload({
|
|
198
235
|
success: false,
|
|
199
236
|
error: 'missing_token',
|
|
200
237
|
message: 'Authorization header required'
|
|
201
|
-
});
|
|
238
|
+
}));
|
|
202
239
|
}
|
|
203
240
|
|
|
204
241
|
const token = authHeader.slice(7);
|
|
@@ -213,11 +250,11 @@ function createRoutes(options = {}) {
|
|
|
213
250
|
status_code: 401,
|
|
214
251
|
hint: 'Create a fresh invite token and retry with the new bearer token.'
|
|
215
252
|
});
|
|
216
|
-
return res.status(401).json({
|
|
253
|
+
return res.status(401).json(withTracePayload({
|
|
217
254
|
success: false,
|
|
218
255
|
error: 'unauthorized',
|
|
219
256
|
message: 'Invalid or expired token'
|
|
220
|
-
});
|
|
257
|
+
}));
|
|
221
258
|
}
|
|
222
259
|
|
|
223
260
|
// Check rate limit
|
|
@@ -233,11 +270,11 @@ function createRoutes(options = {}) {
|
|
|
233
270
|
}
|
|
234
271
|
});
|
|
235
272
|
res.set('Retry-After', rateCheck.retryAfter);
|
|
236
|
-
return res.status(429).json({
|
|
273
|
+
return res.status(429).json(withTracePayload({
|
|
237
274
|
success: false,
|
|
238
275
|
error: rateCheck.error,
|
|
239
276
|
message: rateCheck.message
|
|
240
|
-
});
|
|
277
|
+
}));
|
|
241
278
|
}
|
|
242
279
|
|
|
243
280
|
// Extract and validate request
|
|
@@ -250,11 +287,11 @@ function createRoutes(options = {}) {
|
|
|
250
287
|
status_code: 400,
|
|
251
288
|
hint: 'Include a non-empty string field `message` in the request body.'
|
|
252
289
|
});
|
|
253
|
-
return res.status(400).json({
|
|
290
|
+
return res.status(400).json(withTracePayload({
|
|
254
291
|
success: false,
|
|
255
292
|
error: 'missing_message',
|
|
256
293
|
message: 'Message is required'
|
|
257
|
-
});
|
|
294
|
+
}));
|
|
258
295
|
}
|
|
259
296
|
|
|
260
297
|
// Validate message length
|
|
@@ -269,11 +306,11 @@ function createRoutes(options = {}) {
|
|
|
269
306
|
message_length: typeof message === 'string' ? message.length : null
|
|
270
307
|
}
|
|
271
308
|
});
|
|
272
|
-
return res.status(400).json({
|
|
309
|
+
return res.status(400).json(withTracePayload({
|
|
273
310
|
success: false,
|
|
274
311
|
error: 'invalid_message',
|
|
275
312
|
message: `Message must be a string under ${MAX_MESSAGE_LENGTH} characters`
|
|
276
|
-
});
|
|
313
|
+
}));
|
|
277
314
|
}
|
|
278
315
|
|
|
279
316
|
// Validate and bound timeout
|
|
@@ -299,7 +336,8 @@ function createRoutes(options = {}) {
|
|
|
299
336
|
disclosure: validation.disclosure,
|
|
300
337
|
caller: sanitizedCaller,
|
|
301
338
|
conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
|
|
302
|
-
trace_id: traceId
|
|
339
|
+
trace_id: traceId,
|
|
340
|
+
request_id: requestId
|
|
303
341
|
};
|
|
304
342
|
|
|
305
343
|
// Track conversation if store available
|
|
@@ -317,7 +355,8 @@ function createRoutes(options = {}) {
|
|
|
317
355
|
if (monitor) {
|
|
318
356
|
monitor.trackActivity(a2aContext.conversation_id, {
|
|
319
357
|
...sanitizedCaller,
|
|
320
|
-
trace_id: traceId
|
|
358
|
+
trace_id: traceId,
|
|
359
|
+
request_id: requestId
|
|
321
360
|
});
|
|
322
361
|
}
|
|
323
362
|
|
|
@@ -377,7 +416,8 @@ function createRoutes(options = {}) {
|
|
|
377
416
|
message,
|
|
378
417
|
response: response.text,
|
|
379
418
|
conversation_id: a2aContext.conversation_id,
|
|
380
|
-
trace_id: traceId
|
|
419
|
+
trace_id: traceId,
|
|
420
|
+
request_id: requestId
|
|
381
421
|
}).catch(err => {
|
|
382
422
|
reqLogger.error('Failed to notify owner', {
|
|
383
423
|
conversationId: a2aContext.conversation_id,
|
|
@@ -395,6 +435,7 @@ function createRoutes(options = {}) {
|
|
|
395
435
|
reqLogger.info('Invoke request completed', {
|
|
396
436
|
conversationId: a2aContext.conversation_id,
|
|
397
437
|
tokenId: validation.id,
|
|
438
|
+
requestId,
|
|
398
439
|
data: {
|
|
399
440
|
duration_ms: Date.now() - startedAt,
|
|
400
441
|
message_length: message.length,
|
|
@@ -404,6 +445,8 @@ function createRoutes(options = {}) {
|
|
|
404
445
|
|
|
405
446
|
res.json({
|
|
406
447
|
success: true,
|
|
448
|
+
trace_id: traceId,
|
|
449
|
+
request_id: requestId,
|
|
407
450
|
conversation_id: a2aContext.conversation_id,
|
|
408
451
|
response: response.text,
|
|
409
452
|
can_continue: response.canContinue !== false,
|
|
@@ -422,11 +465,11 @@ function createRoutes(options = {}) {
|
|
|
422
465
|
duration_ms: Date.now() - startedAt
|
|
423
466
|
}
|
|
424
467
|
});
|
|
425
|
-
res.status(500).json({
|
|
468
|
+
res.status(500).json(withTracePayload({
|
|
426
469
|
success: false,
|
|
427
470
|
error: 'internal_error',
|
|
428
471
|
message: 'Failed to process message'
|
|
429
|
-
});
|
|
472
|
+
}));
|
|
430
473
|
}
|
|
431
474
|
});
|
|
432
475
|
|
|
@@ -437,8 +480,20 @@ function createRoutes(options = {}) {
|
|
|
437
480
|
router.post('/end', async (req, res) => {
|
|
438
481
|
const startedAt = Date.now();
|
|
439
482
|
const traceId = resolveTraceId(req);
|
|
440
|
-
const
|
|
483
|
+
const requestId = resolveRequestId(req);
|
|
484
|
+
const reqLogger = logger.child({ traceId, requestId, event: 'end' });
|
|
485
|
+
const withTracePayload = (payload) => ({ ...payload, trace_id: traceId, request_id: requestId });
|
|
441
486
|
res.set('x-trace-id', traceId);
|
|
487
|
+
res.set('x-request-id', requestId);
|
|
488
|
+
reqLogger.info('Received end request', {
|
|
489
|
+
data: {
|
|
490
|
+
request_id: requestId,
|
|
491
|
+
ip: req.ip,
|
|
492
|
+
client_host: extractClientHost(req),
|
|
493
|
+
has_auth_header: Boolean(req.headers.authorization),
|
|
494
|
+
has_conversation_id: Boolean(req.body && req.body.conversation_id)
|
|
495
|
+
}
|
|
496
|
+
});
|
|
442
497
|
|
|
443
498
|
// Extract token
|
|
444
499
|
const authHeader = req.headers.authorization;
|
|
@@ -448,11 +503,11 @@ function createRoutes(options = {}) {
|
|
|
448
503
|
status_code: 401,
|
|
449
504
|
hint: 'Send Authorization: Bearer <a2a_token>.'
|
|
450
505
|
});
|
|
451
|
-
return res.status(401).json({
|
|
506
|
+
return res.status(401).json(withTracePayload({
|
|
452
507
|
success: false,
|
|
453
508
|
error: 'unauthorized',
|
|
454
509
|
message: 'Authorization header required'
|
|
455
|
-
});
|
|
510
|
+
}));
|
|
456
511
|
}
|
|
457
512
|
|
|
458
513
|
const token = authHeader.slice(7);
|
|
@@ -463,11 +518,11 @@ function createRoutes(options = {}) {
|
|
|
463
518
|
status_code: 401,
|
|
464
519
|
hint: 'Use a currently valid invite token for conversation end calls.'
|
|
465
520
|
});
|
|
466
|
-
return res.status(401).json({
|
|
521
|
+
return res.status(401).json(withTracePayload({
|
|
467
522
|
success: false,
|
|
468
523
|
error: 'unauthorized',
|
|
469
524
|
message: 'Invalid or expired token'
|
|
470
|
-
});
|
|
525
|
+
}));
|
|
471
526
|
}
|
|
472
527
|
|
|
473
528
|
const { conversation_id } = req.body;
|
|
@@ -478,16 +533,16 @@ function createRoutes(options = {}) {
|
|
|
478
533
|
status_code: 400,
|
|
479
534
|
hint: 'Provide `conversation_id` returned from /invoke.'
|
|
480
535
|
});
|
|
481
|
-
return res.status(400).json({
|
|
536
|
+
return res.status(400).json(withTracePayload({
|
|
482
537
|
success: false,
|
|
483
538
|
error: 'missing_conversation_id',
|
|
484
539
|
message: 'conversation_id is required'
|
|
485
|
-
});
|
|
540
|
+
}));
|
|
486
541
|
}
|
|
487
542
|
|
|
488
543
|
const convStore = getConversationStore();
|
|
489
544
|
if (!convStore) {
|
|
490
|
-
return res.json({ success: true, message: 'Conversation storage not enabled' });
|
|
545
|
+
return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
|
|
491
546
|
}
|
|
492
547
|
|
|
493
548
|
try {
|
|
@@ -508,7 +563,8 @@ function createRoutes(options = {}) {
|
|
|
508
563
|
type: 'conversation_concluded',
|
|
509
564
|
token: validation,
|
|
510
565
|
conversation: conv,
|
|
511
|
-
trace_id: traceId
|
|
566
|
+
trace_id: traceId,
|
|
567
|
+
request_id: requestId
|
|
512
568
|
}).catch(err => {
|
|
513
569
|
reqLogger.error('Failed to notify owner after conversation end', {
|
|
514
570
|
conversationId: conversation_id,
|
|
@@ -526,6 +582,7 @@ function createRoutes(options = {}) {
|
|
|
526
582
|
reqLogger.info('End request completed', {
|
|
527
583
|
conversationId: conversation_id,
|
|
528
584
|
tokenId: validation.id,
|
|
585
|
+
requestId,
|
|
529
586
|
data: {
|
|
530
587
|
duration_ms: Date.now() - startedAt,
|
|
531
588
|
status: result.success ? 'concluded' : 'unchanged'
|
|
@@ -534,6 +591,8 @@ function createRoutes(options = {}) {
|
|
|
534
591
|
|
|
535
592
|
res.json({
|
|
536
593
|
success: true,
|
|
594
|
+
trace_id: traceId,
|
|
595
|
+
request_id: requestId,
|
|
537
596
|
conversation_id,
|
|
538
597
|
status: 'concluded',
|
|
539
598
|
summary: result.summary
|
|
@@ -550,11 +609,11 @@ function createRoutes(options = {}) {
|
|
|
550
609
|
duration_ms: Date.now() - startedAt
|
|
551
610
|
}
|
|
552
611
|
});
|
|
553
|
-
res.status(500).json({
|
|
612
|
+
res.status(500).json(withTracePayload({
|
|
554
613
|
success: false,
|
|
555
614
|
error: 'internal_error',
|
|
556
615
|
message: 'Failed to end conversation'
|
|
557
|
-
});
|
|
616
|
+
}));
|
|
558
617
|
}
|
|
559
618
|
});
|
|
560
619
|
|
package/src/routes/dashboard.js
CHANGED
|
@@ -185,6 +185,69 @@ function ensureDashboardAccess(req, res, next) {
|
|
|
185
185
|
return res.status(401).json({ success: false, error: 'unauthorized', message: 'Admin token required' });
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
function summarizeDebugLogs(logs) {
|
|
189
|
+
if (!Array.isArray(logs) || logs.length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
event_count: 0,
|
|
192
|
+
events: [],
|
|
193
|
+
timestamps: [],
|
|
194
|
+
error_count: 0,
|
|
195
|
+
warning_count: 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const normalized = logs.slice().sort((a, b) => {
|
|
200
|
+
const aTime = Number(new Date(a.timestamp));
|
|
201
|
+
const bTime = Number(new Date(b.timestamp));
|
|
202
|
+
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
|
|
203
|
+
if (Number.isNaN(aTime)) return 1;
|
|
204
|
+
if (Number.isNaN(bTime)) return -1;
|
|
205
|
+
return aTime - bTime;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const firstTimestamp = normalized[0]?.timestamp || null;
|
|
209
|
+
const lastTimestamp = normalized[normalized.length - 1]?.timestamp || null;
|
|
210
|
+
const firstMs = firstTimestamp ? Number(new Date(firstTimestamp)) : null;
|
|
211
|
+
const lastMs = lastTimestamp ? Number(new Date(lastTimestamp)) : null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
event_count: normalized.length,
|
|
215
|
+
events: normalized.map(row => row.event).filter(Boolean),
|
|
216
|
+
timestamps: normalized.map(row => row.timestamp),
|
|
217
|
+
first_seen: firstTimestamp,
|
|
218
|
+
last_seen: lastTimestamp,
|
|
219
|
+
timeline_ms: Number.isFinite(firstMs) && Number.isFinite(lastMs) && lastMs >= firstMs
|
|
220
|
+
? lastMs - firstMs
|
|
221
|
+
: null,
|
|
222
|
+
trace_ids: [...new Set(normalized.map(row => row.trace_id).filter(Boolean))],
|
|
223
|
+
conversation_ids: [...new Set(normalized.map(row => row.conversation_id).filter(Boolean))],
|
|
224
|
+
token_ids: [...new Set(normalized.map(row => row.token_id).filter(Boolean))],
|
|
225
|
+
request_ids: [...new Set(normalized.map(row => row.request_id).filter(Boolean))],
|
|
226
|
+
error_count: normalized.filter(row => row.level === 'error').length,
|
|
227
|
+
warning_count: normalized.filter(row => row.level === 'warn').length,
|
|
228
|
+
hints: [...new Set(normalized
|
|
229
|
+
.filter(row => row.hint)
|
|
230
|
+
.map(row => String(row.hint)))]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findErrorHints(logs) {
|
|
235
|
+
const hints = new Map();
|
|
236
|
+
for (const row of logs) {
|
|
237
|
+
if (!row.error_code) continue;
|
|
238
|
+
if (!hints.has(row.error_code)) {
|
|
239
|
+
hints.set(row.error_code, {
|
|
240
|
+
error_code: row.error_code,
|
|
241
|
+
status_code: row.status_code,
|
|
242
|
+
count: 0
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const existing = hints.get(row.error_code);
|
|
246
|
+
existing.count += 1;
|
|
247
|
+
}
|
|
248
|
+
return Array.from(hints.values());
|
|
249
|
+
}
|
|
250
|
+
|
|
188
251
|
function createDashboardApiRouter(options = {}) {
|
|
189
252
|
const router = express.Router();
|
|
190
253
|
const context = buildContext(options);
|
|
@@ -239,6 +302,65 @@ function createDashboardApiRouter(options = {}) {
|
|
|
239
302
|
return res.json({ success: true, stats });
|
|
240
303
|
});
|
|
241
304
|
|
|
305
|
+
router.get('/debug/call', (req, res) => {
|
|
306
|
+
const traceId = sanitizeString(req.query.trace_id || req.query.traceId || '', 120);
|
|
307
|
+
const conversationId = sanitizeString(req.query.conversation_id || req.query.conversationId || '', 120);
|
|
308
|
+
const limit = Math.min(1000, Math.max(1, Number.parseInt(req.query.limit || '500', 10) || 500));
|
|
309
|
+
|
|
310
|
+
if (!traceId && !conversationId) {
|
|
311
|
+
return res.status(400).json({
|
|
312
|
+
success: false,
|
|
313
|
+
error: 'missing_scope',
|
|
314
|
+
message: 'Provide trace_id or conversation_id.'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const logs = traceId
|
|
319
|
+
? context.logger.getTrace(traceId, { limit })
|
|
320
|
+
: context.logger.list({
|
|
321
|
+
limit,
|
|
322
|
+
conversationId,
|
|
323
|
+
sort_desc: false
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!Array.isArray(logs) || logs.length === 0) {
|
|
327
|
+
return res.status(404).json({
|
|
328
|
+
success: false,
|
|
329
|
+
error: 'no_logs_found',
|
|
330
|
+
message: traceId ? 'No logs for trace_id' : 'No logs for conversation_id'
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const summary = summarizeDebugLogs(logs);
|
|
335
|
+
const errors = logs.filter(row => row.level === 'error' || row.level === 'warn').slice(0, 20);
|
|
336
|
+
const callSignature = {
|
|
337
|
+
trace_ids: summary.trace_ids,
|
|
338
|
+
conversation_ids: summary.conversation_ids,
|
|
339
|
+
token_ids: summary.token_ids,
|
|
340
|
+
request_ids: summary.request_ids
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return res.json({
|
|
344
|
+
success: true,
|
|
345
|
+
summary: {
|
|
346
|
+
...summary,
|
|
347
|
+
...callSignature,
|
|
348
|
+
errors: errors.map(row => ({
|
|
349
|
+
id: row.id,
|
|
350
|
+
timestamp: row.timestamp,
|
|
351
|
+
level: row.level,
|
|
352
|
+
event: row.event,
|
|
353
|
+
message: row.message,
|
|
354
|
+
error_code: row.error_code,
|
|
355
|
+
status_code: row.status_code,
|
|
356
|
+
hint: row.hint
|
|
357
|
+
})),
|
|
358
|
+
error_codes: findErrorHints(logs)
|
|
359
|
+
},
|
|
360
|
+
logs: summary.timeline_ms === null ? logs : logs
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
242
364
|
router.get('/contacts', (req, res) => {
|
|
243
365
|
const contacts = context.tokenStore.listRemotes();
|
|
244
366
|
const contactIndex = buildContactIndex(contacts);
|
package/src/server.js
CHANGED
|
@@ -454,8 +454,10 @@ async function callAgent(message, a2aContext) {
|
|
|
454
454
|
const tierInfo = a2aContext.tier || 'public';
|
|
455
455
|
const conversationId = a2aContext.conversation_id || `conv_${Date.now()}`;
|
|
456
456
|
const traceId = a2aContext.trace_id || null;
|
|
457
|
+
const requestId = a2aContext.request_id || null;
|
|
457
458
|
const callLogger = logger.child({
|
|
458
459
|
traceId,
|
|
460
|
+
requestId,
|
|
459
461
|
conversationId,
|
|
460
462
|
tokenId: a2aContext.token_id
|
|
461
463
|
});
|
|
@@ -528,20 +530,21 @@ async function callAgent(message, a2aContext) {
|
|
|
528
530
|
}
|
|
529
531
|
});
|
|
530
532
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
533
|
+
const rawResponse = await runtime.runTurn({
|
|
534
|
+
sessionId,
|
|
535
|
+
prompt,
|
|
536
|
+
message,
|
|
537
|
+
caller: a2aContext.caller || {},
|
|
538
|
+
timeoutMs: 65000,
|
|
539
|
+
context: {
|
|
540
|
+
conversationId,
|
|
541
|
+
tier: tierInfo,
|
|
542
|
+
ownerName: agentContext.owner,
|
|
543
|
+
allowedTopics: a2aContext.allowed_topics || [],
|
|
544
|
+
traceId,
|
|
545
|
+
requestId
|
|
546
|
+
}
|
|
547
|
+
});
|
|
545
548
|
|
|
546
549
|
if (collabMode !== 'adaptive') {
|
|
547
550
|
return rawResponse;
|
|
@@ -595,7 +598,7 @@ async function callAgent(message, a2aContext) {
|
|
|
595
598
|
}
|
|
596
599
|
});
|
|
597
600
|
|
|
598
|
-
|
|
601
|
+
return cleanResponse || '[Sub-agent returned empty response]';
|
|
599
602
|
|
|
600
603
|
} catch (err) {
|
|
601
604
|
callLogger.error('Runtime turn handling failed; using fallback response', {
|