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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { execSync, spawn } = require('child_process');
|
|
11
11
|
const { createLogger } = require('./logger');
|
|
12
|
+
const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
|
|
12
13
|
|
|
13
14
|
const logger = createLogger({ component: 'a2a.claude-subagent' });
|
|
14
15
|
|
|
@@ -216,7 +217,7 @@ function parseSubagentResponse(resultText) {
|
|
|
216
217
|
* @param {number} timeoutMs - Timeout in milliseconds
|
|
217
218
|
* @returns {Promise<{ stdout: string, stderr: string }>}
|
|
218
219
|
*/
|
|
219
|
-
function spawnClaude(args, timeoutMs =
|
|
220
|
+
function spawnClaude(args, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
|
|
220
221
|
return new Promise((resolve, reject) => {
|
|
221
222
|
const proc = spawn('claude', args, {
|
|
222
223
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -302,7 +303,7 @@ function extractResultFromJson(stdout) {
|
|
|
302
303
|
* @param {Array} options.activeThreads - Active conversation threads
|
|
303
304
|
* @param {Array} options.candidateCollaborations - Candidate collaboration ideas
|
|
304
305
|
* @param {boolean} options.closeSignal - Whether close has been signaled
|
|
305
|
-
* @param {number} [options.timeoutMs=
|
|
306
|
+
* @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
|
|
306
307
|
* @returns {Promise<{ message: string, statePatch: object|null, flags: array, sessionId: string }>}
|
|
307
308
|
*/
|
|
308
309
|
async function runClaudeTurn(options) {
|
|
@@ -317,7 +318,7 @@ async function runClaudeTurn(options) {
|
|
|
317
318
|
activeThreads = [],
|
|
318
319
|
candidateCollaborations = [],
|
|
319
320
|
closeSignal = false,
|
|
320
|
-
timeoutMs =
|
|
321
|
+
timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
321
322
|
} = options;
|
|
322
323
|
|
|
323
324
|
const turnPrompt = buildTurnPrompt({
|
|
@@ -396,10 +397,10 @@ async function runClaudeTurn(options) {
|
|
|
396
397
|
*
|
|
397
398
|
* @param {string} sessionId - Session ID to resume
|
|
398
399
|
* @param {string} reason - Why the conversation is ending
|
|
399
|
-
* @param {number} [timeoutMs=
|
|
400
|
+
* @param {number} [timeoutMs=300000] - Timeout in milliseconds
|
|
400
401
|
* @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
|
|
401
402
|
*/
|
|
402
|
-
async function runClaudeSummary(sessionId, reason, timeoutMs =
|
|
403
|
+
async function runClaudeSummary(sessionId, reason, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
|
|
403
404
|
if (!sessionId) {
|
|
404
405
|
throw new Error('Cannot summarize without a session ID');
|
|
405
406
|
}
|
package/src/lib/config.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
|
|
23
23
|
const { createLogger } = require('./logger');
|
|
24
24
|
const { buildUnifiedSummaryPrompt } = require('./summary-prompt');
|
|
25
|
+
const { resolveTokenTimeoutMs, resolveTurnTimeoutMs } = require('./turn-timeout');
|
|
25
26
|
|
|
26
27
|
const logger = createLogger({ component: 'a2a.conversation-driver' });
|
|
27
28
|
|
|
@@ -130,9 +131,16 @@ class ConversationDriver {
|
|
|
130
131
|
this.summarizer = options.summarizer || null;
|
|
131
132
|
this.ownerContext = options.ownerContext || {};
|
|
132
133
|
this.claudeMode = options.runtime?.mode === 'claude';
|
|
133
|
-
this.claudeTimeoutMs = options.claudeTimeoutMs || 180000;
|
|
134
134
|
|
|
135
|
-
const
|
|
135
|
+
const tokenTimeoutMs = options.tokenTimeoutMs
|
|
136
|
+
|| options.claudeTimeoutMs
|
|
137
|
+
|| resolveTokenTimeoutMs(options.token);
|
|
138
|
+
const configTimeoutMs = options.configTurnTimeoutMs;
|
|
139
|
+
this.claudeTimeoutMs = resolveTurnTimeoutMs({ tokenTimeoutMs, configTimeoutMs });
|
|
140
|
+
|
|
141
|
+
const clientTimeout = this.claudeMode
|
|
142
|
+
? Math.max(this.claudeTimeoutMs + 20000, 200000)
|
|
143
|
+
: 65000;
|
|
136
144
|
this.client = new A2AClient({ caller: this.caller, timeout: clientTimeout });
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -221,7 +229,8 @@ class ConversationDriver {
|
|
|
221
229
|
sessionId: `summary-${Date.now()}`,
|
|
222
230
|
prompt,
|
|
223
231
|
messages,
|
|
224
|
-
callerInfo: { name: agentContext.name, owner: agentContext.owner }
|
|
232
|
+
callerInfo: { name: agentContext.name, owner: agentContext.owner },
|
|
233
|
+
timeoutMs: this.claudeMode ? this.claudeTimeoutMs : 35000
|
|
225
234
|
});
|
|
226
235
|
} catch (err) {
|
|
227
236
|
logger.warn('Runtime summarizer failed, using default', {
|
package/src/lib/conversations.js
CHANGED
|
@@ -18,9 +18,10 @@ const DB_FILENAME = 'a2a-conversations.db';
|
|
|
18
18
|
const logger = createLogger({ component: 'a2a.conversations' });
|
|
19
19
|
|
|
20
20
|
class ConversationStore {
|
|
21
|
-
constructor(configDir = DEFAULT_CONFIG_DIR) {
|
|
21
|
+
constructor(configDir = DEFAULT_CONFIG_DIR, options = {}) {
|
|
22
22
|
this.configDir = configDir;
|
|
23
23
|
this.dbPath = path.join(configDir, DB_FILENAME);
|
|
24
|
+
this.eventStore = options.eventStore || null;
|
|
24
25
|
this.db = null;
|
|
25
26
|
this._ensureDir();
|
|
26
27
|
}
|
|
@@ -253,6 +254,19 @@ class ConversationStore {
|
|
|
253
254
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
|
254
255
|
`).run(id, contactId, contactName, tokenId, direction, now, now);
|
|
255
256
|
|
|
257
|
+
if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
|
|
258
|
+
this.eventStore.emitEvent('call.updated', {
|
|
259
|
+
conversation_id: id,
|
|
260
|
+
status: 'active',
|
|
261
|
+
direction,
|
|
262
|
+
contact_id: contactId || null,
|
|
263
|
+
contact_name: contactName || null
|
|
264
|
+
}, {
|
|
265
|
+
conversationId: id,
|
|
266
|
+
contactId: contactId || null
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
256
270
|
return { id, resumed: false };
|
|
257
271
|
}
|
|
258
272
|
|
|
@@ -284,6 +298,14 @@ class ConversationStore {
|
|
|
284
298
|
WHERE id = ?
|
|
285
299
|
`).run(now, conversationId);
|
|
286
300
|
|
|
301
|
+
if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
|
|
302
|
+
this.eventStore.emitEvent('call.updated', {
|
|
303
|
+
conversation_id: conversationId,
|
|
304
|
+
status: 'active',
|
|
305
|
+
direction
|
|
306
|
+
}, { conversationId });
|
|
307
|
+
}
|
|
308
|
+
|
|
287
309
|
return { id, timestamp: now };
|
|
288
310
|
}
|
|
289
311
|
|
|
@@ -450,6 +472,31 @@ class ConversationStore {
|
|
|
450
472
|
`).run(now, conversationId);
|
|
451
473
|
}
|
|
452
474
|
|
|
475
|
+
if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
|
|
476
|
+
this.eventStore.emitEvent('call.updated', {
|
|
477
|
+
conversation_id: conversationId,
|
|
478
|
+
status: 'concluded',
|
|
479
|
+
contact_id: conversation.contact_id || null,
|
|
480
|
+
contact_name: conversation.contact_name || null
|
|
481
|
+
}, {
|
|
482
|
+
conversationId,
|
|
483
|
+
contactId: conversation.contact_id || null
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (summary || ownerSummary) {
|
|
487
|
+
this.eventStore.emitEvent('summary.completed', {
|
|
488
|
+
conversation_id: conversationId,
|
|
489
|
+
contact_id: conversation.contact_id || null,
|
|
490
|
+
contact_name: conversation.contact_name || null,
|
|
491
|
+
has_summary: Boolean(summary),
|
|
492
|
+
has_owner_summary: Boolean(ownerSummary)
|
|
493
|
+
}, {
|
|
494
|
+
conversationId,
|
|
495
|
+
contactId: conversation.contact_id || null
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
453
500
|
return {
|
|
454
501
|
success: true,
|
|
455
502
|
conversationId,
|
|
@@ -472,6 +519,13 @@ class ConversationStore {
|
|
|
472
519
|
WHERE id = ?
|
|
473
520
|
`).run(now, conversationId);
|
|
474
521
|
|
|
522
|
+
if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
|
|
523
|
+
this.eventStore.emitEvent('call.updated', {
|
|
524
|
+
conversation_id: conversationId,
|
|
525
|
+
status: 'timeout'
|
|
526
|
+
}, { conversationId });
|
|
527
|
+
}
|
|
528
|
+
|
|
475
529
|
return { success: true };
|
|
476
530
|
}
|
|
477
531
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
6
|
+
process.env.OPENCLAW_CONFIG_DIR ||
|
|
7
|
+
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
8
|
+
|
|
9
|
+
const DB_FILENAME = 'a2a-events.db';
|
|
10
|
+
const DEFAULT_RETENTION_COUNT = 5000;
|
|
11
|
+
|
|
12
|
+
function nowIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class DashboardEventStore {
|
|
17
|
+
constructor(configDir = DEFAULT_CONFIG_DIR, options = {}) {
|
|
18
|
+
this.configDir = configDir;
|
|
19
|
+
this.dbPath = options.dbPath || path.join(configDir, DB_FILENAME);
|
|
20
|
+
this.retentionCount = Number.isFinite(options.retentionCount)
|
|
21
|
+
? Math.max(100, Math.floor(options.retentionCount))
|
|
22
|
+
: DEFAULT_RETENTION_COUNT;
|
|
23
|
+
this.db = null;
|
|
24
|
+
this._dbError = null;
|
|
25
|
+
this._stmts = null;
|
|
26
|
+
this._emitter = new EventEmitter();
|
|
27
|
+
this._ensureDir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_ensureDir() {
|
|
31
|
+
if (!fs.existsSync(this.configDir)) {
|
|
32
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_initDb() {
|
|
37
|
+
if (this.db) return this.db;
|
|
38
|
+
if (this._dbError) return null;
|
|
39
|
+
try {
|
|
40
|
+
const Database = require('better-sqlite3');
|
|
41
|
+
this.db = new Database(this.dbPath);
|
|
42
|
+
try {
|
|
43
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// Best effort.
|
|
46
|
+
}
|
|
47
|
+
this._migrate();
|
|
48
|
+
this._prepareStatements();
|
|
49
|
+
return this.db;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
this._dbError = err && err.message ? err.message : 'failed_to_initialize_event_db';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_migrate() {
|
|
57
|
+
this.db.exec(`
|
|
58
|
+
PRAGMA journal_mode = WAL;
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS dashboard_events (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
event_type TEXT NOT NULL,
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
payload_json TEXT NOT NULL,
|
|
65
|
+
conversation_id TEXT,
|
|
66
|
+
contact_id TEXT
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_events_created ON dashboard_events(created_at);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_events_type ON dashboard_events(event_type);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_events_conversation ON dashboard_events(conversation_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_dashboard_events_contact ON dashboard_events(contact_id);
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_prepareStatements() {
|
|
77
|
+
this._stmts = {
|
|
78
|
+
insertEvent: this.db.prepare(`
|
|
79
|
+
INSERT INTO dashboard_events (event_type, created_at, payload_json, conversation_id, contact_id)
|
|
80
|
+
VALUES (@event_type, @created_at, @payload_json, @conversation_id, @contact_id)
|
|
81
|
+
`),
|
|
82
|
+
listSince: this.db.prepare(`
|
|
83
|
+
SELECT id, event_type, created_at, payload_json, conversation_id, contact_id
|
|
84
|
+
FROM dashboard_events
|
|
85
|
+
WHERE id > @since_id
|
|
86
|
+
ORDER BY id ASC
|
|
87
|
+
LIMIT @limit
|
|
88
|
+
`),
|
|
89
|
+
listLatest: this.db.prepare(`
|
|
90
|
+
SELECT id, event_type, created_at, payload_json, conversation_id, contact_id
|
|
91
|
+
FROM dashboard_events
|
|
92
|
+
ORDER BY id DESC
|
|
93
|
+
LIMIT @limit
|
|
94
|
+
`),
|
|
95
|
+
prune: this.db.prepare(`
|
|
96
|
+
DELETE FROM dashboard_events
|
|
97
|
+
WHERE id NOT IN (
|
|
98
|
+
SELECT id FROM dashboard_events
|
|
99
|
+
ORDER BY id DESC
|
|
100
|
+
LIMIT @retention
|
|
101
|
+
)
|
|
102
|
+
`)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
isAvailable() {
|
|
107
|
+
return Boolean(this._initDb());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getDbError() {
|
|
111
|
+
this._initDb();
|
|
112
|
+
return this._dbError;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
emitEvent(eventType, payload = {}, meta = {}) {
|
|
116
|
+
const db = this._initDb();
|
|
117
|
+
if (!db) {
|
|
118
|
+
return { success: false, error: 'event_storage_unavailable', message: this._dbError };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const event_type = String(eventType || '').trim().slice(0, 80);
|
|
122
|
+
if (!event_type) {
|
|
123
|
+
return { success: false, error: 'event_type_required' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const created_at = nowIso();
|
|
127
|
+
const row = {
|
|
128
|
+
event_type,
|
|
129
|
+
created_at,
|
|
130
|
+
payload_json: JSON.stringify(payload || {}),
|
|
131
|
+
conversation_id: meta && meta.conversationId ? String(meta.conversationId).slice(0, 120) : null,
|
|
132
|
+
contact_id: meta && meta.contactId ? String(meta.contactId).slice(0, 120) : null
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const info = this._stmts.insertEvent.run(row);
|
|
136
|
+
const id = Number(info.lastInsertRowid);
|
|
137
|
+
const event = {
|
|
138
|
+
id,
|
|
139
|
+
type: event_type,
|
|
140
|
+
created_at,
|
|
141
|
+
conversation_id: row.conversation_id,
|
|
142
|
+
contact_id: row.contact_id,
|
|
143
|
+
payload: payload || {}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (id % 100 === 0) {
|
|
147
|
+
try {
|
|
148
|
+
this._stmts.prune.run({ retention: this.retentionCount });
|
|
149
|
+
} catch (_) {
|
|
150
|
+
// Best effort.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this._emitter.emit('event', event);
|
|
155
|
+
return { success: true, event };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
listSince(sinceId, options = {}) {
|
|
159
|
+
const db = this._initDb();
|
|
160
|
+
if (!db) return [];
|
|
161
|
+
const limit = Math.min(500, Math.max(1, Number.parseInt(String(options.limit || '200'), 10) || 200));
|
|
162
|
+
const parsedSince = Number.parseInt(String(sinceId || '0'), 10);
|
|
163
|
+
if (Number.isFinite(parsedSince) && parsedSince > 0) {
|
|
164
|
+
const rows = this._stmts.listSince.all({ since_id: parsedSince, limit });
|
|
165
|
+
return rows.map((row) => this._toEvent(row));
|
|
166
|
+
}
|
|
167
|
+
const rows = this._stmts.listLatest.all({ limit }).reverse();
|
|
168
|
+
return rows.map((row) => this._toEvent(row));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
subscribe(listener) {
|
|
172
|
+
const safe = (event) => {
|
|
173
|
+
try {
|
|
174
|
+
listener(event);
|
|
175
|
+
} catch (_) {
|
|
176
|
+
// Keep stream robust.
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
this._emitter.on('event', safe);
|
|
180
|
+
return () => {
|
|
181
|
+
this._emitter.off('event', safe);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_toEvent(row) {
|
|
186
|
+
let payload = {};
|
|
187
|
+
try {
|
|
188
|
+
payload = row.payload_json ? JSON.parse(row.payload_json) : {};
|
|
189
|
+
} catch (_) {
|
|
190
|
+
payload = {};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id: row.id,
|
|
194
|
+
type: row.event_type,
|
|
195
|
+
created_at: row.created_at,
|
|
196
|
+
conversation_id: row.conversation_id || null,
|
|
197
|
+
contact_id: row.contact_id || null,
|
|
198
|
+
payload
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
DashboardEventStore
|
|
205
|
+
};
|
|
@@ -14,6 +14,7 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
14
14
|
const { createLogger } = require('./logger');
|
|
15
15
|
const { runClaudeTurn: invokeClaudeTurn, buildSubagentSystemPrompt, runClaudeSummary } = require('./claude-subagent');
|
|
16
16
|
const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
|
|
17
|
+
const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
|
|
17
18
|
|
|
18
19
|
function commandExists(command) {
|
|
19
20
|
try {
|
|
@@ -208,7 +209,7 @@ function createRuntimeAdapter(options = {}) {
|
|
|
208
209
|
activeThreads: context?.activeThreads || [],
|
|
209
210
|
candidateCollaborations: context?.candidateCollaborations || [],
|
|
210
211
|
closeSignal: context?.closeSignal || false,
|
|
211
|
-
timeoutMs: timeoutMs ||
|
|
212
|
+
timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
// Store session ID from first turn for subsequent --resume
|
|
@@ -379,7 +380,7 @@ function createRuntimeAdapter(options = {}) {
|
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
382
|
|
|
382
|
-
async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId }) {
|
|
383
|
+
async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId, timeoutMs }) {
|
|
383
384
|
const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
|
|
384
385
|
const requestId = callerInfo?.request_id || callerInfo?.requestId;
|
|
385
386
|
const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
|
|
@@ -388,7 +389,11 @@ function createRuntimeAdapter(options = {}) {
|
|
|
388
389
|
if (modeInfo.mode === 'claude') {
|
|
389
390
|
const session = claudeSessions.get(sessionId);
|
|
390
391
|
if (session?.claudeSessionId) {
|
|
391
|
-
const result = await runClaudeSummary(
|
|
392
|
+
const result = await runClaudeSummary(
|
|
393
|
+
session.claudeSessionId,
|
|
394
|
+
'conversation ended',
|
|
395
|
+
timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
|
|
396
|
+
);
|
|
392
397
|
if (result && result.summary) {
|
|
393
398
|
return result;
|
|
394
399
|
}
|
package/src/lib/tokens.js
CHANGED
|
@@ -56,6 +56,11 @@ function sanitizeCustomFields(fields, options = {}) {
|
|
|
56
56
|
return cleaned;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function parsePositiveTimeoutMs(value) {
|
|
60
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
61
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
class TokenStore {
|
|
60
65
|
constructor(configDir = DEFAULT_CONFIG_DIR) {
|
|
61
66
|
this.configDir = configDir;
|
|
@@ -196,7 +201,8 @@ class TokenStore {
|
|
|
196
201
|
// Snapshot of actual capabilities at creation time
|
|
197
202
|
allowedTopics = null, // Array of topic strings, e.g. ['chat', 'calendar.read']
|
|
198
203
|
allowedGoals = null, // Array of goal strings, e.g. ['grow-network', 'find-collaborators']
|
|
199
|
-
tierSettings = null
|
|
204
|
+
tierSettings = null, // Object with tier-specific settings
|
|
205
|
+
timeoutMs = null
|
|
200
206
|
} = options;
|
|
201
207
|
|
|
202
208
|
const tier = String(permissions || 'public').trim() || 'public';
|
|
@@ -255,6 +261,7 @@ class TokenStore {
|
|
|
255
261
|
capabilities: capabilities || defaultCapabilities,
|
|
256
262
|
allowed_topics: allowedTopics || defaultTopics[tier] || ['chat'],
|
|
257
263
|
allowed_goals: allowedGoals || defaultGoals[tier] || [],
|
|
264
|
+
timeout_ms: parsePositiveTimeoutMs(timeoutMs),
|
|
258
265
|
tier_settings: tierSettings || {}, // Snapshot of settings at creation
|
|
259
266
|
disclosure,
|
|
260
267
|
notify,
|
|
@@ -327,6 +334,10 @@ class TokenStore {
|
|
|
327
334
|
|| TokenStore.DEFAULT_CAPABILITIES[tier]
|
|
328
335
|
|| ['context-read'];
|
|
329
336
|
|
|
337
|
+
const timeoutMs = parsePositiveTimeoutMs(record.timeout_ms)
|
|
338
|
+
|| parsePositiveTimeoutMs(record.tier_settings?.timeout_ms)
|
|
339
|
+
|| parsePositiveTimeoutMs(record.tier_settings?.timeoutMs);
|
|
340
|
+
|
|
330
341
|
return {
|
|
331
342
|
valid: true,
|
|
332
343
|
id: record.id,
|
|
@@ -335,6 +346,7 @@ class TokenStore {
|
|
|
335
346
|
capabilities,
|
|
336
347
|
allowed_topics: record.allowed_topics || ['chat'],
|
|
337
348
|
allowed_goals: record.allowed_goals || [],
|
|
349
|
+
timeout_ms: timeoutMs,
|
|
338
350
|
tier_settings: record.tier_settings || {},
|
|
339
351
|
disclosure: record.disclosure,
|
|
340
352
|
notify: record.notify,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const HARD_FALLBACK_TURN_TIMEOUT_MS = 300000;
|
|
2
|
+
|
|
3
|
+
function parsePositiveInt(value) {
|
|
4
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
5
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveTokenTimeoutMs(token) {
|
|
9
|
+
if (!token || typeof token !== 'object') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const topLevel = parsePositiveInt(token.timeout_ms ?? token.timeoutMs);
|
|
14
|
+
if (topLevel) {
|
|
15
|
+
return topLevel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const tierSettings = token.tier_settings || token.tierSettings;
|
|
19
|
+
if (!tierSettings || typeof tierSettings !== 'object') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsePositiveInt(tierSettings.timeout_ms ?? tierSettings.timeoutMs);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveTurnTimeoutMs(options = {}) {
|
|
26
|
+
const tokenTimeoutMs = parsePositiveInt(options.tokenTimeoutMs);
|
|
27
|
+
if (tokenTimeoutMs) {
|
|
28
|
+
return tokenTimeoutMs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const envTimeoutMs = parsePositiveInt(
|
|
32
|
+
options.envTimeoutMs !== undefined ? options.envTimeoutMs : process.env.A2A_TURN_TIMEOUT
|
|
33
|
+
);
|
|
34
|
+
if (envTimeoutMs) {
|
|
35
|
+
return envTimeoutMs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const configTimeoutMs = parsePositiveInt(options.configTimeoutMs);
|
|
39
|
+
if (configTimeoutMs) {
|
|
40
|
+
return configTimeoutMs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fallbackTimeoutMs = parsePositiveInt(options.hardFallbackMs);
|
|
44
|
+
return fallbackTimeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
HARD_FALLBACK_TURN_TIMEOUT_MS,
|
|
49
|
+
parsePositiveInt,
|
|
50
|
+
resolveTokenTimeoutMs,
|
|
51
|
+
resolveTurnTimeoutMs
|
|
52
|
+
};
|
package/src/routes/a2a.js
CHANGED
|
@@ -15,11 +15,14 @@ const { createLogger, createTraceId } = require('../lib/logger');
|
|
|
15
15
|
// Lazy-load conversation store (optional dependency)
|
|
16
16
|
let ConversationStore = null;
|
|
17
17
|
let conversationStore = null;
|
|
18
|
-
function getConversationStore() {
|
|
18
|
+
function getConversationStore(options = {}) {
|
|
19
19
|
if (!ConversationStore) {
|
|
20
20
|
try {
|
|
21
21
|
ConversationStore = require('../lib/conversations').ConversationStore;
|
|
22
|
-
|
|
22
|
+
const configDir = options.configDir || undefined;
|
|
23
|
+
conversationStore = new ConversationStore(configDir, {
|
|
24
|
+
eventStore: options.eventStore || null
|
|
25
|
+
});
|
|
23
26
|
if (!conversationStore.isAvailable()) {
|
|
24
27
|
conversationStore = null;
|
|
25
28
|
}
|
|
@@ -162,9 +165,13 @@ function createRoutes(options = {}) {
|
|
|
162
165
|
const notifyOwner = options.notifyOwner || (() => Promise.resolve());
|
|
163
166
|
const limits = options.rateLimits || { minute: 10, hour: 100, day: 1000 };
|
|
164
167
|
const logger = options.logger || createLogger({ component: 'a2a.routes' });
|
|
168
|
+
const eventStore = options.eventStore || null;
|
|
165
169
|
|
|
166
170
|
// Initialize conversation store and call monitor
|
|
167
|
-
const convStore = getConversationStore(
|
|
171
|
+
const convStore = getConversationStore({
|
|
172
|
+
eventStore,
|
|
173
|
+
configDir: tokenStore.configDir
|
|
174
|
+
});
|
|
168
175
|
const monitor = getCallMonitor({
|
|
169
176
|
convStore,
|
|
170
177
|
summarizer: options.summarizer,
|
|
@@ -340,6 +347,7 @@ function createRoutes(options = {}) {
|
|
|
340
347
|
tier: validation.tier,
|
|
341
348
|
capabilities: validation.capabilities,
|
|
342
349
|
allowed_topics: validation.allowed_topics,
|
|
350
|
+
timeout_ms: validation.timeout_ms,
|
|
343
351
|
disclosure: validation.disclosure,
|
|
344
352
|
caller: sanitizedCaller,
|
|
345
353
|
conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
|
|
@@ -366,6 +374,17 @@ function createRoutes(options = {}) {
|
|
|
366
374
|
tokenId: validation.id,
|
|
367
375
|
direction: 'inbound'
|
|
368
376
|
});
|
|
377
|
+
if (isNewConversation && eventStore && eventStore.isAvailable && eventStore.isAvailable()) {
|
|
378
|
+
eventStore.emitEvent('call.inbound', {
|
|
379
|
+
conversation_id: a2aContext.conversation_id,
|
|
380
|
+
token_id: validation.id,
|
|
381
|
+
caller_name: sanitizedCaller.name || validation.name || null,
|
|
382
|
+
caller_owner: sanitizedCaller.owner || null
|
|
383
|
+
}, {
|
|
384
|
+
conversationId: a2aContext.conversation_id,
|
|
385
|
+
contactId: ensuredContact?.id || validation.id
|
|
386
|
+
});
|
|
387
|
+
}
|
|
369
388
|
|
|
370
389
|
// Track activity for auto-conclude
|
|
371
390
|
if (monitor) {
|
|
@@ -562,7 +581,10 @@ function createRoutes(options = {}) {
|
|
|
562
581
|
}));
|
|
563
582
|
}
|
|
564
583
|
|
|
565
|
-
const convStore = getConversationStore(
|
|
584
|
+
const convStore = getConversationStore({
|
|
585
|
+
eventStore,
|
|
586
|
+
configDir: tokenStore.configDir
|
|
587
|
+
});
|
|
566
588
|
if (!convStore) {
|
|
567
589
|
return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
|
|
568
590
|
}
|