a2acalling 0.5.0 → 0.5.3
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 +29 -1
- package/SKILL.md +18 -0
- package/bin/cli.js +123 -0
- package/docs/protocol.md +19 -0
- package/package.json +1 -1
- package/scripts/install-openclaw.js +26 -4
- package/src/dashboard/public/app.js +195 -7
- package/src/dashboard/public/index.html +51 -0
- package/src/dashboard/public/style.css +27 -0
- package/src/index.js +5 -0
- package/src/lib/call-monitor.js +68 -6
- package/src/lib/config.js +11 -1
- package/src/lib/conversations.js +13 -1
- package/src/lib/disclosure.js +11 -1
- package/src/lib/invite-host.js +17 -7
- package/src/lib/logger.js +566 -0
- package/src/lib/openclaw-integration.js +53 -6
- package/src/lib/runtime-adapter.js +133 -39
- package/src/lib/summarizer.js +21 -2
- package/src/lib/tokens.js +12 -1
- package/src/routes/a2a.js +170 -11
- package/src/routes/dashboard.js +41 -0
- package/src/server.js +169 -32
package/src/routes/dashboard.js
CHANGED
|
@@ -16,6 +16,7 @@ const { ConversationStore } = require('../lib/conversations');
|
|
|
16
16
|
const { A2AConfig } = require('../lib/config');
|
|
17
17
|
const { loadManifest, saveManifest } = require('../lib/disclosure');
|
|
18
18
|
const { resolveInviteHost } = require('../lib/invite-host');
|
|
19
|
+
const { createLogger } = require('../lib/logger');
|
|
19
20
|
|
|
20
21
|
const DASHBOARD_STATIC_DIR = path.join(__dirname, '..', 'dashboard', 'public');
|
|
21
22
|
|
|
@@ -106,6 +107,7 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
106
107
|
function buildContext(options = {}) {
|
|
107
108
|
const tokenStore = options.tokenStore || new TokenStore();
|
|
108
109
|
const config = options.config || new A2AConfig();
|
|
110
|
+
const logger = options.logger || createLogger({ component: 'a2a.dashboard' });
|
|
109
111
|
let convStore = options.convStore || null;
|
|
110
112
|
if (!convStore) {
|
|
111
113
|
try {
|
|
@@ -122,6 +124,7 @@ function buildContext(options = {}) {
|
|
|
122
124
|
tokenStore,
|
|
123
125
|
config,
|
|
124
126
|
convStore,
|
|
127
|
+
logger,
|
|
125
128
|
staticDir: DASHBOARD_STATIC_DIR
|
|
126
129
|
};
|
|
127
130
|
}
|
|
@@ -189,6 +192,7 @@ function createDashboardApiRouter(options = {}) {
|
|
|
189
192
|
router.use(ensureDashboardAccess);
|
|
190
193
|
|
|
191
194
|
router.get('/status', (req, res) => {
|
|
195
|
+
context.logger.debug('Dashboard status requested', { event: 'dashboard_status' });
|
|
192
196
|
res.json({
|
|
193
197
|
success: true,
|
|
194
198
|
dashboard: true,
|
|
@@ -198,6 +202,43 @@ function createDashboardApiRouter(options = {}) {
|
|
|
198
202
|
});
|
|
199
203
|
});
|
|
200
204
|
|
|
205
|
+
router.get('/logs', (req, res) => {
|
|
206
|
+
const limit = Math.min(1000, Math.max(1, Number.parseInt(req.query.limit || '200', 10) || 200));
|
|
207
|
+
const logs = context.logger.list({
|
|
208
|
+
limit,
|
|
209
|
+
level: req.query.level || null,
|
|
210
|
+
component: req.query.component || null,
|
|
211
|
+
event: req.query.event || null,
|
|
212
|
+
errorCode: req.query.error_code || req.query.errorCode || null,
|
|
213
|
+
statusCode: req.query.status_code || req.query.statusCode || null,
|
|
214
|
+
traceId: req.query.trace_id || req.query.traceId || null,
|
|
215
|
+
conversationId: req.query.conversation_id || req.query.conversationId || null,
|
|
216
|
+
tokenId: req.query.token_id || req.query.tokenId || null,
|
|
217
|
+
search: req.query.search || null,
|
|
218
|
+
from: req.query.from || null,
|
|
219
|
+
to: req.query.to || null
|
|
220
|
+
});
|
|
221
|
+
return res.json({ success: true, logs });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
router.get('/logs/trace/:traceId', (req, res) => {
|
|
225
|
+
const traceId = sanitizeString(req.params.traceId, 120);
|
|
226
|
+
if (!traceId) {
|
|
227
|
+
return res.status(400).json({ success: false, error: 'trace_id_required' });
|
|
228
|
+
}
|
|
229
|
+
const limit = Math.min(1000, Math.max(1, Number.parseInt(req.query.limit || '500', 10) || 500));
|
|
230
|
+
const logs = context.logger.getTrace(traceId, { limit });
|
|
231
|
+
return res.json({ success: true, trace_id: traceId, logs });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
router.get('/logs/stats', (req, res) => {
|
|
235
|
+
const stats = context.logger.stats({
|
|
236
|
+
from: req.query.from || null,
|
|
237
|
+
to: req.query.to || null
|
|
238
|
+
});
|
|
239
|
+
return res.json({ success: true, stats });
|
|
240
|
+
});
|
|
241
|
+
|
|
201
242
|
router.get('/contacts', (req, res) => {
|
|
202
243
|
const contacts = context.tokenStore.listRemotes();
|
|
203
244
|
const contactIndex = buildContactIndex(contacts);
|
package/src/server.js
CHANGED
|
@@ -20,12 +20,14 @@ const {
|
|
|
20
20
|
extractCollaborationState
|
|
21
21
|
} = require('./lib/prompt-template');
|
|
22
22
|
const { findAvailablePort } = require('./lib/port-scanner');
|
|
23
|
+
const { createLogger } = require('./lib/logger');
|
|
23
24
|
|
|
24
25
|
const DEFAULT_PORTS = [80, 3001, 8080, 8443, 9001];
|
|
25
26
|
const requestedPort = process.env.PORT ? parseInt(process.env.PORT, 10)
|
|
26
27
|
: process.argv[2] ? parseInt(process.argv[2], 10)
|
|
27
28
|
: null;
|
|
28
29
|
const workspaceDir = process.env.A2A_WORKSPACE || process.env.OPENCLAW_WORKSPACE || process.cwd();
|
|
30
|
+
const logger = createLogger({ component: 'a2a.server' });
|
|
29
31
|
|
|
30
32
|
// Load workspace context for agent identity
|
|
31
33
|
function loadAgentContext() {
|
|
@@ -59,17 +61,30 @@ const agentContext = loadAgentContext();
|
|
|
59
61
|
const tokenStore = new TokenStore();
|
|
60
62
|
const runtime = createRuntimeAdapter({
|
|
61
63
|
workspaceDir,
|
|
62
|
-
agentContext
|
|
64
|
+
agentContext,
|
|
65
|
+
logger: logger.child({ component: 'a2a.runtime' })
|
|
63
66
|
});
|
|
64
67
|
const VALID_PHASES = new Set(['handshake', 'explore', 'deep_dive', 'synthesize', 'close']);
|
|
65
68
|
const collaborationSessions = new Map();
|
|
66
69
|
const COLLAB_STATE_TTL_MS = readPositiveIntEnv('A2A_COLLAB_STATE_TTL_MS', 6 * 60 * 60 * 1000);
|
|
67
70
|
const MAX_COLLAB_SESSIONS = readPositiveIntEnv('A2A_COLLAB_MAX_SESSIONS', 500);
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
logger.info('A2A server bootstrapping', {
|
|
73
|
+
event: 'server_bootstrap',
|
|
74
|
+
data: {
|
|
75
|
+
agent_name: agentContext.name,
|
|
76
|
+
owner_name: agentContext.owner,
|
|
77
|
+
runtime_mode: runtime.mode,
|
|
78
|
+
runtime_reason: runtime.reason
|
|
79
|
+
}
|
|
80
|
+
});
|
|
71
81
|
if (runtime.warning) {
|
|
72
|
-
|
|
82
|
+
logger.warn('Runtime warning', {
|
|
83
|
+
event: 'runtime_warning',
|
|
84
|
+
data: {
|
|
85
|
+
warning: runtime.warning
|
|
86
|
+
}
|
|
87
|
+
});
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
function readPositiveIntEnv(name, fallback) {
|
|
@@ -405,11 +420,27 @@ function ensureContact(caller, tokenId) {
|
|
|
405
420
|
try {
|
|
406
421
|
const contact = tokenStore.ensureInboundContact(caller, tokenId);
|
|
407
422
|
if (contact) {
|
|
408
|
-
|
|
423
|
+
logger.info('Contact ensured from inbound call', {
|
|
424
|
+
event: 'contact_ensured',
|
|
425
|
+
tokenId,
|
|
426
|
+
data: {
|
|
427
|
+
caller_name: caller.name,
|
|
428
|
+
caller_owner: caller.owner || null
|
|
429
|
+
}
|
|
430
|
+
});
|
|
409
431
|
}
|
|
410
432
|
return contact;
|
|
411
433
|
} catch (err) {
|
|
412
|
-
|
|
434
|
+
logger.error('Failed to ensure contact', {
|
|
435
|
+
event: 'contact_ensure_failed',
|
|
436
|
+
tokenId,
|
|
437
|
+
error_code: 'CONTACT_ENSURE_FAILED',
|
|
438
|
+
hint: 'Validate token/contact store files and write permissions.',
|
|
439
|
+
error: err,
|
|
440
|
+
data: {
|
|
441
|
+
caller_name: caller?.name || null
|
|
442
|
+
}
|
|
443
|
+
});
|
|
413
444
|
return null;
|
|
414
445
|
}
|
|
415
446
|
}
|
|
@@ -422,6 +453,12 @@ async function callAgent(message, a2aContext) {
|
|
|
422
453
|
const callerOwner = a2aContext.caller?.owner || '';
|
|
423
454
|
const tierInfo = a2aContext.tier || 'public';
|
|
424
455
|
const conversationId = a2aContext.conversation_id || `conv_${Date.now()}`;
|
|
456
|
+
const traceId = a2aContext.trace_id || null;
|
|
457
|
+
const callLogger = logger.child({
|
|
458
|
+
traceId,
|
|
459
|
+
conversationId,
|
|
460
|
+
tokenId: a2aContext.token_id
|
|
461
|
+
});
|
|
425
462
|
const collabMode = resolveCollabMode();
|
|
426
463
|
const collabState = getOrCreateCollaborationState(conversationId, {
|
|
427
464
|
callerName,
|
|
@@ -481,6 +518,16 @@ async function callAgent(message, a2aContext) {
|
|
|
481
518
|
const sessionId = `a2a-${conversationId}`;
|
|
482
519
|
|
|
483
520
|
try {
|
|
521
|
+
callLogger.info('Handling inbound call turn', {
|
|
522
|
+
event: 'call_turn_start',
|
|
523
|
+
data: {
|
|
524
|
+
caller_name: callerName,
|
|
525
|
+
caller_owner: callerOwner || null,
|
|
526
|
+
tier: tierInfo,
|
|
527
|
+
collab_mode: collabMode
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
484
531
|
const rawResponse = await runtime.runTurn({
|
|
485
532
|
sessionId,
|
|
486
533
|
prompt,
|
|
@@ -491,7 +538,8 @@ async function callAgent(message, a2aContext) {
|
|
|
491
538
|
conversationId,
|
|
492
539
|
tier: tierInfo,
|
|
493
540
|
ownerName: agentContext.owner,
|
|
494
|
-
allowedTopics: a2aContext.allowed_topics || []
|
|
541
|
+
allowedTopics: a2aContext.allowed_topics || [],
|
|
542
|
+
traceId
|
|
495
543
|
}
|
|
496
544
|
});
|
|
497
545
|
|
|
@@ -507,10 +555,21 @@ async function callAgent(message, a2aContext) {
|
|
|
507
555
|
if (parsed.hasState && parsed.statePatch) {
|
|
508
556
|
usedMetadata = applyCollaborationPatch(collabState, parsed.statePatch);
|
|
509
557
|
if (!usedMetadata) {
|
|
510
|
-
|
|
558
|
+
callLogger.warn('Invalid collaboration patch; applying fallback heuristics', {
|
|
559
|
+
event: 'collaboration_patch_invalid',
|
|
560
|
+
error_code: 'COLLABORATION_PATCH_INVALID',
|
|
561
|
+
hint: 'Ensure assistant emits valid collaboration metadata JSON block.'
|
|
562
|
+
});
|
|
511
563
|
}
|
|
512
564
|
} else if (parsed.parseError) {
|
|
513
|
-
|
|
565
|
+
callLogger.warn('Could not parse collaboration metadata; applying fallback heuristics', {
|
|
566
|
+
event: 'collaboration_metadata_parse_failed',
|
|
567
|
+
error_code: 'COLLABORATION_METADATA_PARSE_FAILED',
|
|
568
|
+
hint: 'Inspect response format and ensure metadata wrapper markers are intact.',
|
|
569
|
+
data: {
|
|
570
|
+
parse_error: parsed.parseError
|
|
571
|
+
}
|
|
572
|
+
});
|
|
514
573
|
}
|
|
515
574
|
|
|
516
575
|
if (!usedMetadata) {
|
|
@@ -527,10 +586,27 @@ async function callAgent(message, a2aContext) {
|
|
|
527
586
|
collabState.updatedAt = Date.now();
|
|
528
587
|
collaborationSessions.set(conversationId, collabState);
|
|
529
588
|
|
|
589
|
+
callLogger.info('Call turn completed', {
|
|
590
|
+
event: 'call_turn_complete',
|
|
591
|
+
data: {
|
|
592
|
+
phase: collabState.phase,
|
|
593
|
+
overlap_score: collabState.overlapScore,
|
|
594
|
+
turn_count: collabState.turnCount
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
530
598
|
return cleanResponse || '[Sub-agent returned empty response]';
|
|
531
599
|
|
|
532
600
|
} catch (err) {
|
|
533
|
-
|
|
601
|
+
callLogger.error('Runtime turn handling failed; using fallback response', {
|
|
602
|
+
event: 'call_turn_failed_fallback',
|
|
603
|
+
error_code: 'RUNTIME_TURN_FAILED',
|
|
604
|
+
hint: 'Inspect runtime adapter logs in this trace to identify CLI/bridge failure.',
|
|
605
|
+
error: err,
|
|
606
|
+
data: {
|
|
607
|
+
phase: 'runtime_turn'
|
|
608
|
+
}
|
|
609
|
+
});
|
|
534
610
|
return runtime.buildFallbackResponse(message, {
|
|
535
611
|
caller: a2aContext.caller,
|
|
536
612
|
ownerName: agentContext.owner,
|
|
@@ -573,10 +649,22 @@ Be concise but specific. No filler.`;
|
|
|
573
649
|
sessionId: `summary-${Date.now()}`,
|
|
574
650
|
prompt,
|
|
575
651
|
messages,
|
|
576
|
-
callerInfo
|
|
652
|
+
callerInfo,
|
|
653
|
+
traceId: callerInfo?.trace_id || callerInfo?.traceId,
|
|
654
|
+
conversationId: callerInfo?.conversation_id || callerInfo?.conversationId
|
|
577
655
|
});
|
|
578
656
|
} catch (err) {
|
|
579
|
-
|
|
657
|
+
logger.error('Summary generation failed', {
|
|
658
|
+
event: 'summary_generation_failed',
|
|
659
|
+
traceId: callerInfo?.trace_id || callerInfo?.traceId,
|
|
660
|
+
conversationId: callerInfo?.conversation_id || callerInfo?.conversationId,
|
|
661
|
+
error_code: 'SUMMARY_GENERATION_FAILED',
|
|
662
|
+
hint: 'Check summarizer runtime and command configuration for summary stage.',
|
|
663
|
+
error: err,
|
|
664
|
+
data: {
|
|
665
|
+
phase: 'summary'
|
|
666
|
+
}
|
|
667
|
+
});
|
|
580
668
|
return null;
|
|
581
669
|
}
|
|
582
670
|
}
|
|
@@ -584,23 +672,30 @@ Be concise but specific. No filler.`;
|
|
|
584
672
|
/**
|
|
585
673
|
* Notify owner via Telegram
|
|
586
674
|
*/
|
|
587
|
-
async function notifyOwner({ level, token, caller, message, conversation_id }) {
|
|
675
|
+
async function notifyOwner({ level, token, caller, message, conversation_id, trace_id }) {
|
|
588
676
|
const callerName = caller?.name || 'Unknown';
|
|
589
677
|
const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
|
|
590
678
|
const messageText = String(message || '');
|
|
591
679
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
680
|
+
logger.info('Owner notification requested', {
|
|
681
|
+
event: 'owner_notify_requested',
|
|
682
|
+
conversationId: conversation_id,
|
|
683
|
+
tokenId: token?.id,
|
|
684
|
+
data: {
|
|
685
|
+
caller_name: callerName,
|
|
686
|
+
caller_owner: caller?.owner || null,
|
|
687
|
+
token_name: token?.name || 'unknown',
|
|
688
|
+
level
|
|
689
|
+
}
|
|
690
|
+
});
|
|
597
691
|
|
|
598
692
|
await runtime.notify({
|
|
599
693
|
level,
|
|
600
694
|
token,
|
|
601
695
|
caller,
|
|
602
696
|
message: messageText,
|
|
603
|
-
conversationId: conversation_id
|
|
697
|
+
conversationId: conversation_id,
|
|
698
|
+
traceId: trace_id || null
|
|
604
699
|
});
|
|
605
700
|
}
|
|
606
701
|
|
|
@@ -609,21 +704,40 @@ app.use(express.json());
|
|
|
609
704
|
|
|
610
705
|
// Minimal owner dashboard (local by default unless A2A_ADMIN_TOKEN is provided)
|
|
611
706
|
app.use('/api/a2a/dashboard', createDashboardApiRouter({
|
|
612
|
-
tokenStore
|
|
707
|
+
tokenStore,
|
|
708
|
+
logger: logger.child({ component: 'a2a.dashboard' })
|
|
613
709
|
}));
|
|
614
710
|
app.use('/dashboard', createDashboardUiRouter({
|
|
615
|
-
tokenStore
|
|
711
|
+
tokenStore,
|
|
712
|
+
logger: logger.child({ component: 'a2a.dashboard' })
|
|
616
713
|
}));
|
|
617
714
|
|
|
618
715
|
app.use('/api/a2a', createRoutes({
|
|
619
716
|
tokenStore,
|
|
717
|
+
logger: logger.child({ component: 'a2a.routes' }),
|
|
620
718
|
|
|
621
719
|
async handleMessage(message, context, options) {
|
|
622
|
-
|
|
720
|
+
const traceId = context.trace_id || null;
|
|
721
|
+
const requestLogger = logger.child({
|
|
722
|
+
traceId,
|
|
723
|
+
conversationId: context.conversation_id,
|
|
724
|
+
tokenId: context.token_id
|
|
725
|
+
});
|
|
726
|
+
requestLogger.info('Inbound message accepted for handling', {
|
|
727
|
+
event: 'handle_message_start',
|
|
728
|
+
data: {
|
|
729
|
+
caller_name: context.caller?.name || 'unknown'
|
|
730
|
+
}
|
|
731
|
+
});
|
|
623
732
|
|
|
624
733
|
const response = await callAgent(message, context);
|
|
625
734
|
|
|
626
|
-
|
|
735
|
+
requestLogger.info('Outbound response generated', {
|
|
736
|
+
event: 'handle_message_complete',
|
|
737
|
+
data: {
|
|
738
|
+
response_length: String(response || '').length
|
|
739
|
+
}
|
|
740
|
+
});
|
|
627
741
|
|
|
628
742
|
return { text: response, canContinue: true };
|
|
629
743
|
},
|
|
@@ -645,7 +759,12 @@ async function startServer() {
|
|
|
645
759
|
if (await isPortAvailable(requestedPort)) {
|
|
646
760
|
port = requestedPort;
|
|
647
761
|
} else {
|
|
648
|
-
|
|
762
|
+
logger.warn('Requested port is in use, scanning for alternatives', {
|
|
763
|
+
event: 'requested_port_in_use',
|
|
764
|
+
data: {
|
|
765
|
+
requested_port: requestedPort
|
|
766
|
+
}
|
|
767
|
+
});
|
|
649
768
|
port = await findAvailablePort(DEFAULT_PORTS);
|
|
650
769
|
}
|
|
651
770
|
} else {
|
|
@@ -653,22 +772,40 @@ async function startServer() {
|
|
|
653
772
|
}
|
|
654
773
|
|
|
655
774
|
if (!port) {
|
|
656
|
-
|
|
657
|
-
|
|
775
|
+
logger.error('No available port found', {
|
|
776
|
+
event: 'port_unavailable',
|
|
777
|
+
data: {
|
|
778
|
+
tried_ports: requestedPort ? [requestedPort, ...DEFAULT_PORTS] : DEFAULT_PORTS
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
logger.error('Set PORT env or free one of the default ports.', {
|
|
782
|
+
event: 'port_unavailable_hint'
|
|
783
|
+
});
|
|
658
784
|
process.exit(1);
|
|
659
785
|
}
|
|
660
786
|
|
|
661
787
|
const server = app.listen(port, () => {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
788
|
+
logger.info('A2A server listening', {
|
|
789
|
+
event: 'server_started',
|
|
790
|
+
data: {
|
|
791
|
+
port,
|
|
792
|
+
agent_name: agentContext.name,
|
|
793
|
+
runtime_mode: runtime.mode,
|
|
794
|
+
failover_enabled: runtime.failoverEnabled,
|
|
795
|
+
collaboration_mode: resolveCollabMode(),
|
|
796
|
+
features: ['adaptive collaboration', 'auto-contacts', 'summaries', 'dashboard']
|
|
797
|
+
}
|
|
798
|
+
});
|
|
667
799
|
});
|
|
668
800
|
|
|
669
801
|
server.on('error', (err) => {
|
|
670
802
|
if (err.code === 'EADDRINUSE') {
|
|
671
|
-
|
|
803
|
+
logger.error('Bound port became unavailable (EADDRINUSE)', {
|
|
804
|
+
event: 'server_port_lost',
|
|
805
|
+
data: {
|
|
806
|
+
port
|
|
807
|
+
}
|
|
808
|
+
});
|
|
672
809
|
process.exit(1);
|
|
673
810
|
}
|
|
674
811
|
throw err;
|