a2acalling 0.6.49 → 0.6.51
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/bin/cli.js +228 -2
- package/docs/protocol.md +42 -0
- package/native/macos/index.html +5 -10
- package/native/macos/src-tauri/Cargo.lock +5875 -0
- package/native/macos/src-tauri/Cargo.toml +3 -2
- package/native/macos/src-tauri/icons/128x128.png +0 -0
- package/native/macos/src-tauri/icons/128x128@2x.png +0 -0
- package/native/macos/src-tauri/icons/32x32.png +0 -0
- package/native/macos/src-tauri/icons/tray-connected.png +0 -0
- package/native/macos/src-tauri/icons/tray-disconnected.png +0 -0
- package/native/macos/src-tauri/src/lib.rs +2 -2
- package/native/macos/src-tauri/src/notifications.rs +147 -68
- package/native/macos/src-tauri/tauri.conf.json +2 -9
- package/package.json +1 -1
- package/scripts/install-skills.js +80 -0
- package/scripts/postinstall.js +8 -45
- package/src/dashboard/public/app.js +147 -1
- package/src/lib/claude-subagent.js +6 -5
- package/src/lib/config.js +1 -0
- package/src/lib/conversation-driver.js +12 -3
- package/src/lib/conversations.js +55 -1
- package/src/lib/dashboard-events.js +205 -0
- package/src/lib/runtime-adapter.js +8 -3
- package/src/lib/tokens.js +13 -1
- package/src/lib/turn-timeout.js +52 -0
- package/src/routes/a2a.js +26 -4
- package/src/routes/dashboard.js +114 -1
- package/src/server.js +20 -1
package/src/routes/dashboard.js
CHANGED
|
@@ -18,6 +18,7 @@ const { A2AConfig } = require('../lib/config');
|
|
|
18
18
|
const { loadManifest, saveManifest } = require('../lib/disclosure');
|
|
19
19
|
const { resolveInviteHost } = require('../lib/invite-host');
|
|
20
20
|
const { CallbookStore } = require('../lib/callbook');
|
|
21
|
+
const { DashboardEventStore } = require('../lib/dashboard-events');
|
|
21
22
|
const { createLogger } = require('../lib/logger');
|
|
22
23
|
|
|
23
24
|
const DASHBOARD_STATIC_DIR = path.join(__dirname, '..', 'dashboard', 'public');
|
|
@@ -192,11 +193,12 @@ function buildContext(options = {}) {
|
|
|
192
193
|
const config = options.config || new A2AConfig();
|
|
193
194
|
const logger = options.logger || createLogger({ component: 'a2a.dashboard' });
|
|
194
195
|
const callbookStore = options.callbookStore || new CallbookStore(tokenStore.configDir);
|
|
196
|
+
const eventStore = options.eventStore || new DashboardEventStore(tokenStore.configDir);
|
|
195
197
|
const agentContext = resolveAgentContext(options);
|
|
196
198
|
let convStore = options.convStore || null;
|
|
197
199
|
if (!convStore) {
|
|
198
200
|
try {
|
|
199
|
-
convStore = new ConversationStore();
|
|
201
|
+
convStore = new ConversationStore(tokenStore.configDir, { eventStore });
|
|
200
202
|
if (!convStore.isAvailable()) {
|
|
201
203
|
convStore = null;
|
|
202
204
|
}
|
|
@@ -210,6 +212,7 @@ function buildContext(options = {}) {
|
|
|
210
212
|
config,
|
|
211
213
|
convStore,
|
|
212
214
|
callbookStore,
|
|
215
|
+
eventStore,
|
|
213
216
|
getUpdateManager: typeof options.getUpdateManager === 'function'
|
|
214
217
|
? options.getUpdateManager
|
|
215
218
|
: (() => null),
|
|
@@ -444,6 +447,23 @@ function createDashboardApiRouter(options = {}) {
|
|
|
444
447
|
const context = buildContext(options);
|
|
445
448
|
router.use(express.json());
|
|
446
449
|
const ensureDashboardAccess = makeEnsureDashboardAccess(context);
|
|
450
|
+
const writeSseEvent = (res, event) => {
|
|
451
|
+
const eventName = sanitizeString(event?.type || 'message', 80) || 'message';
|
|
452
|
+
const eventId = Number.parseInt(String(event?.id || ''), 10);
|
|
453
|
+
const payload = {
|
|
454
|
+
id: eventId || null,
|
|
455
|
+
type: eventName,
|
|
456
|
+
created_at: event?.created_at || new Date().toISOString(),
|
|
457
|
+
conversation_id: event?.conversation_id || null,
|
|
458
|
+
contact_id: event?.contact_id || null,
|
|
459
|
+
payload: event?.payload || {}
|
|
460
|
+
};
|
|
461
|
+
if (Number.isFinite(eventId) && eventId > 0) {
|
|
462
|
+
res.write(`id: ${eventId}\n`);
|
|
463
|
+
}
|
|
464
|
+
res.write(`event: ${eventName}\n`);
|
|
465
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
466
|
+
};
|
|
447
467
|
|
|
448
468
|
// Callbook Remote: exchange a short-lived provisioning code for a long-lived session cookie.
|
|
449
469
|
// This route must be reachable BEFORE dashboard access is established.
|
|
@@ -481,6 +501,14 @@ function createDashboardApiRouter(options = {}) {
|
|
|
481
501
|
});
|
|
482
502
|
res.setHeader('Set-Cookie', cookie);
|
|
483
503
|
|
|
504
|
+
if (context.eventStore && context.eventStore.isAvailable()) {
|
|
505
|
+
context.eventStore.emitEvent('invite.used', {
|
|
506
|
+
device_id: result.device?.id || null,
|
|
507
|
+
device_label: result.device?.label || null,
|
|
508
|
+
source: 'callbook_exchange'
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
484
512
|
return res.json({
|
|
485
513
|
success: true,
|
|
486
514
|
device: result.device,
|
|
@@ -491,6 +519,72 @@ function createDashboardApiRouter(options = {}) {
|
|
|
491
519
|
// All other dashboard API routes require owner access.
|
|
492
520
|
router.use(ensureDashboardAccess);
|
|
493
521
|
|
|
522
|
+
router.get('/events', (req, res) => {
|
|
523
|
+
if (!context.eventStore || !context.eventStore.isAvailable()) {
|
|
524
|
+
return res.status(503).json({
|
|
525
|
+
success: false,
|
|
526
|
+
error: 'event_stream_unavailable',
|
|
527
|
+
message: context.eventStore ? context.eventStore.getDbError() : 'missing_event_store'
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const lastIdHeader = req.headers['last-event-id'];
|
|
532
|
+
const since = sanitizeString(
|
|
533
|
+
req.query.since || lastIdHeader || req.query.last_event_id || '',
|
|
534
|
+
32
|
|
535
|
+
);
|
|
536
|
+
const replayLimit = Math.min(500, Math.max(1, Number.parseInt(req.query.replay || '200', 10) || 200));
|
|
537
|
+
|
|
538
|
+
res.status(200);
|
|
539
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
540
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
541
|
+
res.setHeader('Connection', 'keep-alive');
|
|
542
|
+
res.flushHeaders?.();
|
|
543
|
+
|
|
544
|
+
let lastSentId = Number.parseInt(String(since || '0'), 10);
|
|
545
|
+
if (!Number.isFinite(lastSentId) || lastSentId < 0) {
|
|
546
|
+
lastSentId = 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const pendingLive = [];
|
|
550
|
+
const unsubscribe = context.eventStore.subscribe((event) => {
|
|
551
|
+
if (!event || !Number.isFinite(event.id)) return;
|
|
552
|
+
if (event.id <= lastSentId) return;
|
|
553
|
+
pendingLive.push(event);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const replay = context.eventStore.listSince(since, { limit: replayLimit });
|
|
557
|
+
for (const row of replay) {
|
|
558
|
+
writeSseEvent(res, row);
|
|
559
|
+
if (Number.isFinite(row.id) && row.id > lastSentId) {
|
|
560
|
+
lastSentId = row.id;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
while (pendingLive.length > 0) {
|
|
564
|
+
const row = pendingLive.shift();
|
|
565
|
+
if (!row || !Number.isFinite(row.id) || row.id <= lastSentId) continue;
|
|
566
|
+
writeSseEvent(res, row);
|
|
567
|
+
lastSentId = row.id;
|
|
568
|
+
}
|
|
569
|
+
res.write(': connected\n\n');
|
|
570
|
+
|
|
571
|
+
const liveUnsubscribe = context.eventStore.subscribe((event) => {
|
|
572
|
+
if (!event || !Number.isFinite(event.id) || event.id <= lastSentId) return;
|
|
573
|
+
writeSseEvent(res, event);
|
|
574
|
+
lastSentId = event.id;
|
|
575
|
+
});
|
|
576
|
+
const heartbeat = setInterval(() => {
|
|
577
|
+
res.write(`: heartbeat ${Date.now()}\n\n`);
|
|
578
|
+
}, 15000);
|
|
579
|
+
|
|
580
|
+
req.on('close', () => {
|
|
581
|
+
clearInterval(heartbeat);
|
|
582
|
+
unsubscribe();
|
|
583
|
+
liveUnsubscribe();
|
|
584
|
+
res.end();
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
494
588
|
router.get('/status', async (req, res) => {
|
|
495
589
|
context.logger.debug('Dashboard status requested', { event: 'dashboard_status' });
|
|
496
590
|
const refreshIp = String(req.query.refresh_ip || 'false') === 'true';
|
|
@@ -1028,7 +1122,16 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1028
1122
|
const url = `a2a://${contact.host}/${contact.token}`;
|
|
1029
1123
|
try {
|
|
1030
1124
|
const result = await client.call(url, message, { conversationId, timeoutSeconds });
|
|
1125
|
+
const previousStatus = String(contact.status || '');
|
|
1031
1126
|
context.tokenStore.updateContactStatus(contact.id, 'online');
|
|
1127
|
+
if (context.eventStore && context.eventStore.isAvailable() && previousStatus !== 'online') {
|
|
1128
|
+
context.eventStore.emitEvent('contact.status.changed', {
|
|
1129
|
+
contact_id: contact.id,
|
|
1130
|
+
contact_name: contact.name || contact.host || null,
|
|
1131
|
+
previous_status: previousStatus || null,
|
|
1132
|
+
status: 'online'
|
|
1133
|
+
}, { contactId: contact.id });
|
|
1134
|
+
}
|
|
1032
1135
|
|
|
1033
1136
|
if (context.convStore) {
|
|
1034
1137
|
try {
|
|
@@ -1053,7 +1156,17 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1053
1156
|
can_continue: result?.can_continue !== false
|
|
1054
1157
|
});
|
|
1055
1158
|
} catch (err) {
|
|
1159
|
+
const previousStatus = String(contact.status || '');
|
|
1056
1160
|
context.tokenStore.updateContactStatus(contact.id, 'offline', err.message);
|
|
1161
|
+
if (context.eventStore && context.eventStore.isAvailable() && previousStatus !== 'offline') {
|
|
1162
|
+
context.eventStore.emitEvent('contact.status.changed', {
|
|
1163
|
+
contact_id: contact.id,
|
|
1164
|
+
contact_name: contact.name || contact.host || null,
|
|
1165
|
+
previous_status: previousStatus || null,
|
|
1166
|
+
status: 'offline',
|
|
1167
|
+
reason: err.message || null
|
|
1168
|
+
}, { contactId: contact.id });
|
|
1169
|
+
}
|
|
1057
1170
|
return res.status(502).json({
|
|
1058
1171
|
success: false,
|
|
1059
1172
|
error: 'contact_call_failed',
|
package/src/server.js
CHANGED
|
@@ -26,7 +26,9 @@ const { writePidFile, removePidFile } = require('./lib/pid-file');
|
|
|
26
26
|
const { buildUnifiedSummaryPrompt } = require('./lib/summary-prompt');
|
|
27
27
|
const { A2AConfig } = require('./lib/config');
|
|
28
28
|
const { UpdateManager } = require('./lib/update-manager');
|
|
29
|
+
const { DashboardEventStore } = require('./lib/dashboard-events');
|
|
29
30
|
const { spawn } = require('child_process');
|
|
31
|
+
const { resolveTurnTimeoutMs } = require('./lib/turn-timeout');
|
|
30
32
|
|
|
31
33
|
const DEFAULT_PORTS = [80, 3001, 8080, 8443, 9001];
|
|
32
34
|
const requestedPort = process.env.PORT ? parseInt(process.env.PORT, 10)
|
|
@@ -66,6 +68,7 @@ function loadAgentContext() {
|
|
|
66
68
|
const agentContext = loadAgentContext();
|
|
67
69
|
const tokenStore = new TokenStore();
|
|
68
70
|
const config = new A2AConfig();
|
|
71
|
+
const eventStore = new DashboardEventStore(tokenStore.configDir);
|
|
69
72
|
const runtime = createRuntimeAdapter({
|
|
70
73
|
workspaceDir,
|
|
71
74
|
agentContext,
|
|
@@ -120,6 +123,15 @@ function readPositiveIntEnv(name, fallback) {
|
|
|
120
123
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
function resolveConfiguredTurnTimeoutMs() {
|
|
127
|
+
try {
|
|
128
|
+
const defaults = config.getDefaults?.() || {};
|
|
129
|
+
return defaults.turnTimeoutMs ?? defaults.turn_timeout_ms ?? null;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
function resolveCollabMode() {
|
|
124
136
|
const raw = String(process.env.A2A_COLLAB_MODE || 'adaptive').trim().toLowerCase();
|
|
125
137
|
if (raw === 'deep_dive' || raw === 'deep-dive') {
|
|
@@ -583,6 +595,10 @@ async function callAgent(message, a2aContext) {
|
|
|
583
595
|
: buildConnectionPrompt(promptOptions);
|
|
584
596
|
|
|
585
597
|
const sessionId = `a2a-${conversationId}`;
|
|
598
|
+
const claudeTurnTimeoutMs = resolveTurnTimeoutMs({
|
|
599
|
+
tokenTimeoutMs: a2aContext.timeout_ms,
|
|
600
|
+
configTimeoutMs: resolveConfiguredTurnTimeoutMs()
|
|
601
|
+
});
|
|
586
602
|
|
|
587
603
|
try {
|
|
588
604
|
callLogger.info('Handling inbound call turn', {
|
|
@@ -600,12 +616,13 @@ async function callAgent(message, a2aContext) {
|
|
|
600
616
|
prompt,
|
|
601
617
|
message,
|
|
602
618
|
caller: a2aContext.caller || {},
|
|
603
|
-
timeoutMs: 65000,
|
|
619
|
+
timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
|
|
604
620
|
context: {
|
|
605
621
|
conversationId,
|
|
606
622
|
tier: tierInfo,
|
|
607
623
|
ownerName: agentContext.owner,
|
|
608
624
|
allowedTopics: a2aContext.allowed_topics || [],
|
|
625
|
+
timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
|
|
609
626
|
traceId,
|
|
610
627
|
requestId
|
|
611
628
|
}
|
|
@@ -818,6 +835,7 @@ app.use('/api/a2a/dashboard', createDashboardApiRouter({
|
|
|
818
835
|
tokenStore,
|
|
819
836
|
agentContext,
|
|
820
837
|
config,
|
|
838
|
+
eventStore,
|
|
821
839
|
getUpdateManager: () => updateManager,
|
|
822
840
|
logger: logger.child({ component: 'a2a.dashboard' })
|
|
823
841
|
}));
|
|
@@ -843,6 +861,7 @@ app.use('/callbook', createCallbookRouter());
|
|
|
843
861
|
|
|
844
862
|
app.use('/api/a2a', createRoutes({
|
|
845
863
|
tokenStore,
|
|
864
|
+
eventStore,
|
|
846
865
|
logger: logger.child({ component: 'a2a.routes' }),
|
|
847
866
|
onCallMonitor: (monitor) => {
|
|
848
867
|
activeCallMonitor = monitor;
|