a2acalling 0.6.49 → 0.6.50

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 CHANGED
@@ -37,7 +37,8 @@ const ONBOARDING_EXEMPT = new Set([
37
37
  'dashboard',
38
38
  'server',
39
39
  'setup',
40
- 'install'
40
+ 'install',
41
+ 'skills'
41
42
  ]);
42
43
 
43
44
  function isOnboarded() {
@@ -710,6 +711,8 @@ const commands = {
710
711
 
711
712
  // Get objectives from disclosure
712
713
  const objectives = tierTopics.objectives || [];
714
+ const timeoutMsRaw = args.flags['timeout-ms'] || args.flags.timeout_ms;
715
+ const timeoutMs = timeoutMsRaw ? Number.parseInt(String(timeoutMsRaw), 10) : null;
713
716
 
714
717
  const { token, record } = store.create({
715
718
  name: args.flags.name || args.flags.n || 'unnamed',
@@ -720,7 +723,8 @@ const commands = {
720
723
  notify: args.flags.notify || 'all',
721
724
  maxCalls,
722
725
  allowedTopics,
723
- allowedGoals: objectives.map(o => o.objective || o)
726
+ allowedGoals: objectives.map(o => o.objective || o),
727
+ timeoutMs
724
728
  });
725
729
 
726
730
  const resolvedHost = await resolveInviteHostname();
@@ -759,6 +763,7 @@ const commands = {
759
763
  console.log(`Disclosure: ${record.disclosure}`);
760
764
  console.log(`Notify: ${record.notify}`);
761
765
  console.log(`Max calls: ${record.max_calls || 'unlimited'}`);
766
+ if (record.timeout_ms) console.log(`Turn timeout: ${record.timeout_ms}ms`);
762
767
  if (linkContact) console.log(`Linked to: ${linkContact}`);
763
768
  console.log(`\nTo revoke: a2a revoke ${record.id}`);
764
769
  console.log(`\n${'─'.repeat(50)}`);
@@ -1355,11 +1360,15 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1355
1360
 
1356
1361
  // Build owner context from config for summarizer
1357
1362
  let ownerContext = {};
1363
+ let configTurnTimeoutMs = null;
1358
1364
  try {
1359
1365
  const { A2AConfig } = require('../src/lib/config');
1360
1366
  const config = new A2AConfig();
1361
1367
  const configAll = config.getAll();
1362
1368
  const tierGoals = configAll.tiers?.public?.goals || [];
1369
+ configTurnTimeoutMs = configAll.defaults?.turnTimeoutMs
1370
+ || configAll.defaults?.turn_timeout_ms
1371
+ || null;
1363
1372
  ownerContext = {
1364
1373
  goals: tierGoals,
1365
1374
  agentName: agentContext.name,
@@ -1378,6 +1387,7 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1378
1387
  disclosure,
1379
1388
  minTurns,
1380
1389
  maxTurns,
1390
+ configTurnTimeoutMs,
1381
1391
  ownerContext,
1382
1392
  onTurn: (info) => {
1383
1393
  const preview = info.messagePreview.length >= 80
@@ -2625,6 +2635,46 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
2625
2635
  return commands.quickstart(args);
2626
2636
  },
2627
2637
 
2638
+ skills: (args) => {
2639
+ const { installSkills, SKILL_FILES } = require('../scripts/install-skills');
2640
+ const check = args.flags.check || args.flags.c;
2641
+ const force = args.flags.force;
2642
+ const targetDir = process.cwd();
2643
+
2644
+ if (check) {
2645
+ console.log('A2A skills for this project:\n');
2646
+ for (const file of SKILL_FILES) {
2647
+ const destPath = path.join(targetDir, file.dest);
2648
+ const exists = fs.existsSync(destPath);
2649
+ const icon = exists ? ' \u2713' : ' \u2717';
2650
+ console.log(`${icon} ${file.dest}${exists ? ' (installed)' : ' (not installed)'}`);
2651
+ }
2652
+ console.log(`\nRun "a2a skills" to install missing files.`);
2653
+ return;
2654
+ }
2655
+
2656
+ const result = installSkills(targetDir, { force });
2657
+
2658
+ if (result.installed.length) {
2659
+ console.log(`\n Installed ${result.installed.length} A2A skill file(s):\n`);
2660
+ result.installed.forEach(f => console.log(` + ${f}`));
2661
+ }
2662
+ if (result.skipped.length) {
2663
+ console.log(`\n Skipped ${result.skipped.length} unchanged file(s)`);
2664
+ }
2665
+ if (result.errors.length) {
2666
+ console.error(`\n Errors:`);
2667
+ result.errors.forEach(e => console.error(` ! ${e.file}: ${e.error}`));
2668
+ }
2669
+
2670
+ if (result.installed.length === 0 && result.skipped.length > 0) {
2671
+ console.log('\n All skills already installed. Use --force to overwrite.\n');
2672
+ } else if (result.installed.length > 0) {
2673
+ console.log('\n Skills ready. In Claude Code, type /a2a- to see available commands.');
2674
+ console.log(' In Codex CLI, A2A instructions are in .codex/AGENTS.md\n');
2675
+ }
2676
+ },
2677
+
2628
2678
  version: () => {
2629
2679
  const pkg = require('../package.json');
2630
2680
  console.log(pkg.version);
@@ -2645,6 +2695,7 @@ Commands:
2645
2695
  --disclosure, -d Disclosure level (public, minimal, none)
2646
2696
  --notify Owner notification (all, summary, none)
2647
2697
  --max-calls Maximum invocations (default: 100)
2698
+ --timeout-ms Per-token Claude turn timeout in milliseconds
2648
2699
  --link, -l Auto-link to contact name
2649
2700
 
2650
2701
  list List active tokens
@@ -2710,6 +2761,9 @@ Server:
2710
2761
  uninstall Stop server and remove local config/DB
2711
2762
  --keep-config Preserve config/DB (for reinstall)
2712
2763
  --force Skip confirmation prompt
2764
+ skills Install Claude Code + Codex CLI skills
2765
+ --check, -c Show what would be installed
2766
+ --force Overwrite existing files
2713
2767
  version Show installed package version
2714
2768
 
2715
2769
  Examples:
package/docs/protocol.md CHANGED
@@ -388,6 +388,48 @@ module.exports = function (test, assert, helpers) {
388
388
  };
389
389
  ```
390
390
 
391
+ ## CLI Skills (Claude Code & Codex)
392
+
393
+ A2A ships with slash commands for Claude Code and agent instructions for Codex CLI.
394
+
395
+ ### Installation
396
+
397
+ ```bash
398
+ a2a skills # Install into current project
399
+ a2a skills --check # See what would be installed
400
+ a2a skills --force # Overwrite existing files
401
+ ```
402
+
403
+ Skills are also installed automatically on `npm install -g a2acalling`.
404
+
405
+ ### Claude Code Commands
406
+
407
+ | Command | Description |
408
+ |---------|-------------|
409
+ | `/a2a-call <contact> <msg>` | Call another agent (multi-turn) |
410
+ | `/a2a-invite [name] [--tier]` | Create invite token |
411
+ | `/a2a-contacts [add\|show\|ping\|rm]` | Manage contacts |
412
+ | `/a2a-status` | Server and agent health dashboard |
413
+ | `/a2a-setup` | First-time setup and onboarding |
414
+
415
+ Files installed to: `.claude/commands/a2a-*.md`
416
+
417
+ ### Codex CLI
418
+
419
+ A2A agent instructions are installed to `.codex/AGENTS.md`. Codex reads this file automatically to understand available A2A commands, permission tiers, and workflows.
420
+
421
+ ### Manual Installation
422
+
423
+ If the automatic install didn't work, copy the files manually:
424
+
425
+ ```bash
426
+ # Claude Code commands
427
+ cp node_modules/a2acalling/.claude/commands/a2a-*.md .claude/commands/
428
+
429
+ # Codex instructions
430
+ cp node_modules/a2acalling/.codex/AGENTS.md .codex/AGENTS.md
431
+ ```
432
+
391
433
  ## Future Protocol Extensions (v1+)
392
434
 
393
435
  - **Capability advertisement**: Agents declare what they can help with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.49",
3
+ "version": "0.6.50",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A2A Skill Installer
3
+ *
4
+ * Copies Claude Code commands and Codex AGENTS.md into a target project directory.
5
+ * Idempotent: skips files that already exist with identical content.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const PACKAGE_ROOT = path.join(__dirname, '..');
12
+
13
+ const SKILL_FILES = [
14
+ { src: '.claude/commands/a2a-call.md', dest: '.claude/commands/a2a-call.md' },
15
+ { src: '.claude/commands/a2a-invite.md', dest: '.claude/commands/a2a-invite.md' },
16
+ { src: '.claude/commands/a2a-contacts.md', dest: '.claude/commands/a2a-contacts.md' },
17
+ { src: '.claude/commands/a2a-status.md', dest: '.claude/commands/a2a-status.md' },
18
+ { src: '.claude/commands/a2a-setup.md', dest: '.claude/commands/a2a-setup.md' },
19
+ { src: '.codex/AGENTS.md', dest: '.codex/AGENTS.md' }
20
+ ];
21
+
22
+ function installSkills(targetDir, options = {}) {
23
+ const result = { installed: [], skipped: [], errors: [] };
24
+
25
+ for (const file of SKILL_FILES) {
26
+ const srcPath = path.join(PACKAGE_ROOT, file.src);
27
+ const destPath = path.join(targetDir, file.dest);
28
+
29
+ try {
30
+ if (!fs.existsSync(srcPath)) {
31
+ result.errors.push({ file: file.src, error: 'Source file not found' });
32
+ continue;
33
+ }
34
+
35
+ const srcContent = fs.readFileSync(srcPath, 'utf8');
36
+
37
+ // Check if identical file already exists
38
+ if (!options.force && fs.existsSync(destPath)) {
39
+ const existing = fs.readFileSync(destPath, 'utf8');
40
+ if (existing === srcContent) {
41
+ result.skipped.push(file.dest);
42
+ continue;
43
+ }
44
+ }
45
+
46
+ // Create directory and write file
47
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
48
+ fs.writeFileSync(destPath, srcContent);
49
+ result.installed.push(file.dest);
50
+ } catch (err) {
51
+ result.errors.push({ file: file.dest, error: err.message });
52
+ }
53
+ }
54
+
55
+ return result;
56
+ }
57
+
58
+ // CLI mode: node scripts/install-skills.js [targetDir] [--force]
59
+ if (require.main === module) {
60
+ const args = process.argv.slice(2);
61
+ const force = args.includes('--force');
62
+ const targetDir = args.find(a => !a.startsWith('-')) || process.cwd();
63
+
64
+ const result = installSkills(targetDir, { force });
65
+
66
+ if (result.installed.length) {
67
+ console.log(`Installed ${result.installed.length} A2A skill file(s):`);
68
+ result.installed.forEach(f => console.log(` + ${f}`));
69
+ }
70
+ if (result.skipped.length) {
71
+ console.log(`Skipped ${result.skipped.length} unchanged file(s)`);
72
+ }
73
+ if (result.errors.length) {
74
+ console.error(`Errors: ${result.errors.length}`);
75
+ result.errors.forEach(e => console.error(` ! ${e.file}: ${e.error}`));
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ module.exports = { installSkills, SKILL_FILES };
@@ -43,13 +43,25 @@ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
43
43
 
44
44
  if (result.error) {
45
45
  // Don't fail the install — the agent will get onboarding when it runs `a2a`.
46
+ installSkillFiles();
46
47
  installMacOSApp();
47
48
  process.exit(0);
48
49
  }
49
50
 
51
+ installSkillFiles();
50
52
  installMacOSApp();
51
53
  process.exit(result.status || 0);
52
54
 
55
+ // Best-effort: install Claude Code + Codex skills into the workspace
56
+ function installSkillFiles() {
57
+ try {
58
+ const { installSkills } = require('./install-skills');
59
+ installSkills(initCwd);
60
+ } catch (e) {
61
+ // Silent — skills can be installed later with `a2a skills`
62
+ }
63
+ }
64
+
53
65
  // Download and install the native macOS app from GitHub Releases
54
66
  function installMacOSApp() {
55
67
  const os = require('os');
@@ -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 = 180000) {
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=180000] - Timeout in milliseconds
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 = 180000
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=120000] - Timeout in milliseconds
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 = 120000) {
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
@@ -224,6 +224,7 @@ const DEFAULT_CONFIG = {
224
224
  perHour: 100,
225
225
  perDay: 1000
226
226
  },
227
+ turnTimeoutMs: 300000, // default Claude turn timeout
227
228
  maxPendingRequests: 5 // max connection requests per hour
228
229
  },
229
230
 
@@ -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 clientTimeout = this.claudeMode ? 200000 : 65000;
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', {
@@ -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 || 180000
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(session.claudeSessionId, 'conversation ended');
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 // Object with tier-specific settings
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
@@ -340,6 +340,7 @@ function createRoutes(options = {}) {
340
340
  tier: validation.tier,
341
341
  capabilities: validation.capabilities,
342
342
  allowed_topics: validation.allowed_topics,
343
+ timeout_ms: validation.timeout_ms,
343
344
  disclosure: validation.disclosure,
344
345
  caller: sanitizedCaller,
345
346
  conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
package/src/server.js CHANGED
@@ -27,6 +27,7 @@ const { buildUnifiedSummaryPrompt } = require('./lib/summary-prompt');
27
27
  const { A2AConfig } = require('./lib/config');
28
28
  const { UpdateManager } = require('./lib/update-manager');
29
29
  const { spawn } = require('child_process');
30
+ const { resolveTurnTimeoutMs } = require('./lib/turn-timeout');
30
31
 
31
32
  const DEFAULT_PORTS = [80, 3001, 8080, 8443, 9001];
32
33
  const requestedPort = process.env.PORT ? parseInt(process.env.PORT, 10)
@@ -120,6 +121,15 @@ function readPositiveIntEnv(name, fallback) {
120
121
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
121
122
  }
122
123
 
124
+ function resolveConfiguredTurnTimeoutMs() {
125
+ try {
126
+ const defaults = config.getDefaults?.() || {};
127
+ return defaults.turnTimeoutMs ?? defaults.turn_timeout_ms ?? null;
128
+ } catch (err) {
129
+ return null;
130
+ }
131
+ }
132
+
123
133
  function resolveCollabMode() {
124
134
  const raw = String(process.env.A2A_COLLAB_MODE || 'adaptive').trim().toLowerCase();
125
135
  if (raw === 'deep_dive' || raw === 'deep-dive') {
@@ -583,6 +593,10 @@ async function callAgent(message, a2aContext) {
583
593
  : buildConnectionPrompt(promptOptions);
584
594
 
585
595
  const sessionId = `a2a-${conversationId}`;
596
+ const claudeTurnTimeoutMs = resolveTurnTimeoutMs({
597
+ tokenTimeoutMs: a2aContext.timeout_ms,
598
+ configTimeoutMs: resolveConfiguredTurnTimeoutMs()
599
+ });
586
600
 
587
601
  try {
588
602
  callLogger.info('Handling inbound call turn', {
@@ -600,12 +614,13 @@ async function callAgent(message, a2aContext) {
600
614
  prompt,
601
615
  message,
602
616
  caller: a2aContext.caller || {},
603
- timeoutMs: 65000,
617
+ timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
604
618
  context: {
605
619
  conversationId,
606
620
  tier: tierInfo,
607
621
  ownerName: agentContext.owner,
608
622
  allowedTopics: a2aContext.allowed_topics || [],
623
+ timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
609
624
  traceId,
610
625
  requestId
611
626
  }