@agent-native/core 0.109.0 → 0.109.1

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.
Files changed (125) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +24 -2
  5. package/corpus/core/src/a2a/correlation.ts +50 -0
  6. package/corpus/core/src/a2a/handlers.ts +140 -14
  7. package/corpus/core/src/a2a/index.ts +1 -0
  8. package/corpus/core/src/a2a/invoke.ts +10 -1
  9. package/corpus/core/src/a2a/task-store.ts +146 -6
  10. package/corpus/core/src/a2a/types.ts +16 -0
  11. package/corpus/core/src/action.ts +2 -0
  12. package/corpus/core/src/agent/production-agent.ts +23 -0
  13. package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
  14. package/corpus/core/src/db/client.ts +55 -0
  15. package/corpus/core/src/observability/traces.ts +172 -19
  16. package/corpus/core/src/scripts/call-agent.ts +53 -2
  17. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
  18. package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  21. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  22. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  23. package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
  24. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
  25. package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
  26. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
  27. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
  28. package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
  29. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
  30. package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
  31. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
  32. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
  33. package/corpus/templates/clips/app/routes/record.tsx +3 -3
  34. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
  35. package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
  38. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
  39. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
  40. package/corpus/templates/clips/desktop/src/app.tsx +18 -6
  41. package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
  42. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
  43. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -4
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
  45. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +45 -18
  46. package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
  47. package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
  48. package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -0
  49. package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
  50. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
  51. package/corpus/templates/clips/server/plugins/auth.ts +5 -5
  52. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  53. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -1
  55. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
  59. package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
  60. package/corpus/templates/clips/shared/share-meta.ts +1 -1
  61. package/dist/a2a/client.d.ts +11 -2
  62. package/dist/a2a/client.d.ts.map +1 -1
  63. package/dist/a2a/client.js +16 -3
  64. package/dist/a2a/client.js.map +1 -1
  65. package/dist/a2a/correlation.d.ts +10 -0
  66. package/dist/a2a/correlation.d.ts.map +1 -0
  67. package/dist/a2a/correlation.js +40 -0
  68. package/dist/a2a/correlation.js.map +1 -0
  69. package/dist/a2a/handlers.d.ts.map +1 -1
  70. package/dist/a2a/handlers.js +100 -15
  71. package/dist/a2a/handlers.js.map +1 -1
  72. package/dist/a2a/index.d.ts +1 -1
  73. package/dist/a2a/index.d.ts.map +1 -1
  74. package/dist/a2a/index.js.map +1 -1
  75. package/dist/a2a/invoke.d.ts +4 -1
  76. package/dist/a2a/invoke.d.ts.map +1 -1
  77. package/dist/a2a/invoke.js +3 -0
  78. package/dist/a2a/invoke.js.map +1 -1
  79. package/dist/a2a/task-store.d.ts +12 -1
  80. package/dist/a2a/task-store.d.ts.map +1 -1
  81. package/dist/a2a/task-store.js +96 -6
  82. package/dist/a2a/task-store.js.map +1 -1
  83. package/dist/a2a/types.d.ts +15 -0
  84. package/dist/a2a/types.d.ts.map +1 -1
  85. package/dist/a2a/types.js.map +1 -1
  86. package/dist/action.d.ts +2 -0
  87. package/dist/action.d.ts.map +1 -1
  88. package/dist/action.js.map +1 -1
  89. package/dist/agent/production-agent.d.ts +9 -0
  90. package/dist/agent/production-agent.d.ts.map +1 -1
  91. package/dist/agent/production-agent.js +14 -0
  92. package/dist/agent/production-agent.js.map +1 -1
  93. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  94. package/dist/client/use-run-stuck-detection.js +25 -1
  95. package/dist/client/use-run-stuck-detection.js.map +1 -1
  96. package/dist/collab/routes.d.ts +1 -1
  97. package/dist/collab/struct-routes.d.ts +1 -1
  98. package/dist/db/client.d.ts +7 -0
  99. package/dist/db/client.d.ts.map +1 -1
  100. package/dist/db/client.js +33 -0
  101. package/dist/db/client.js.map +1 -1
  102. package/dist/observability/routes.d.ts +5 -5
  103. package/dist/observability/traces.d.ts +9 -0
  104. package/dist/observability/traces.d.ts.map +1 -1
  105. package/dist/observability/traces.js +137 -19
  106. package/dist/observability/traces.js.map +1 -1
  107. package/dist/resources/handlers.d.ts +2 -2
  108. package/dist/scripts/call-agent.d.ts.map +1 -1
  109. package/dist/scripts/call-agent.js +35 -3
  110. package/dist/scripts/call-agent.js.map +1 -1
  111. package/dist/secrets/routes.d.ts +9 -9
  112. package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
  113. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  114. package/dist/server/agent-chat/action-filters-a2a.js +35 -7
  115. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  116. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  117. package/dist/server/agent-chat-plugin.js +93 -6
  118. package/dist/server/agent-chat-plugin.js.map +1 -1
  119. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  120. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  121. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  122. package/package.json +1 -1
  123. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  124. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  125. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -26,6 +26,16 @@ function costUsdFromCenticents(value: number): number {
26
26
  return Math.round((value / 10_000) * 1_000_000) / 1_000_000;
27
27
  }
28
28
 
29
+ const MAX_TRACKED_GENERATION_TOOL_CALLS = 50;
30
+
31
+ type GenerationToolCall = {
32
+ name: string;
33
+ started_offset_ms: number;
34
+ duration_ms: number;
35
+ status: "success" | "error";
36
+ error_class: "tool_error" | "legacy_inferred_error" | "interrupted" | null;
37
+ };
38
+
29
39
  function emitLlmGenerationTrackingEvent(args: {
30
40
  runId: string;
31
41
  threadId: string | null;
@@ -45,6 +55,15 @@ function emitLlmGenerationTrackingEvent(args: {
45
55
  toolCalls: number;
46
56
  successfulTools: number;
47
57
  failedTools: number;
58
+ tools: GenerationToolCall[];
59
+ toolsTruncated: boolean;
60
+ delegation?: {
61
+ protocol: "a2a" | "mcp";
62
+ callerApp?: string;
63
+ taskId?: string;
64
+ parentRunId?: string;
65
+ parentTurnId?: string;
66
+ };
48
67
  createdAt: number;
49
68
  experimentAssignments?: Array<{
50
69
  experimentId: string;
@@ -77,6 +96,14 @@ function emitLlmGenerationTrackingEvent(args: {
77
96
  tool_calls: args.toolCalls,
78
97
  successful_tools: args.successfulTools,
79
98
  failed_tools: args.failedTools,
99
+ tools: args.tools,
100
+ tools_truncated: args.toolsTruncated,
101
+ delegated: args.delegation ? true : undefined,
102
+ delegation_protocol: args.delegation?.protocol,
103
+ caller_app: args.delegation?.callerApp,
104
+ a2a_task_id: args.delegation?.taskId,
105
+ parent_run_id: args.delegation?.parentRunId,
106
+ parent_turn_id: args.delegation?.parentTurnId,
80
107
  model_selection_source: args.modelSelectionSource,
81
108
  created_at: new Date(args.createdAt).toISOString(),
82
109
  created_at_ms: args.createdAt,
@@ -191,6 +218,7 @@ export async function instrumentAgentLoop(opts: {
191
218
  send: (event: AgentChatEvent) => void;
192
219
  signal: AbortSignal;
193
220
  providerOptions?: any;
221
+ runId?: string;
194
222
  }) => Promise<AgentLoopUsage>;
195
223
  loopOpts: {
196
224
  engine: any;
@@ -202,6 +230,7 @@ export async function instrumentAgentLoop(opts: {
202
230
  send: (event: AgentChatEvent) => void;
203
231
  signal: AbortSignal;
204
232
  providerOptions?: any;
233
+ runId?: string;
205
234
  };
206
235
  runId: string;
207
236
  threadId: string | null;
@@ -217,6 +246,13 @@ export async function instrumentAgentLoop(opts: {
217
246
  variantId: string;
218
247
  }>;
219
248
  modelSelectionSource?: string;
249
+ delegation?: {
250
+ protocol: "a2a" | "mcp";
251
+ callerApp?: string;
252
+ taskId?: string;
253
+ parentRunId?: string;
254
+ parentTurnId?: string;
255
+ };
220
256
  /** Raw user-authored message before prompt/context enrichment. */
221
257
  sentimentInput?: string;
222
258
  classifyError?: (error: unknown) =>
@@ -271,6 +307,7 @@ export async function instrumentAgentLoop(opts: {
271
307
  number,
272
308
  {
273
309
  spanId: string;
310
+ callId?: string;
274
311
  startMs: number;
275
312
  toolName: string;
276
313
  input: AgentToolInput;
@@ -278,12 +315,11 @@ export async function instrumentAgentLoop(opts: {
278
315
  endResult?: { status: "success" | "error"; errorMessage: string | null };
279
316
  }
280
317
  >();
281
- // Secondary index: tool name FIFO queue of pending invocation counters.
282
- // tool_start/tool_done events carry only the tool name (no call id), so to
283
- // pair starts and dones correctly when the agent runs concurrent calls to the
284
- // same tool name (read-only / parallelSafe batches via Promise.all), we keep a
285
- // queue per name and match each done to the OLDEST still-pending start.
318
+ // Secondary index for legacy emitters without call ids. Current tool events
319
+ // are paired by id first; same-name FIFO remains as a compatibility fallback.
286
320
  const toolNameToCounters = new Map<string, number[]>();
321
+ const toolCallIdToCounter = new Map<string, number>();
322
+ const generationToolCalls = new Map<number, GenerationToolCall>();
287
323
 
288
324
  let toolCallCount = 0;
289
325
  let successfulTools = 0;
@@ -304,6 +340,7 @@ export async function instrumentAgentLoop(opts: {
304
340
  // microtask gap by recording the span on the pending entry when ready.
305
341
  const entry: {
306
342
  spanId: string;
343
+ callId?: string;
307
344
  startMs: number;
308
345
  toolName: string;
309
346
  input: AgentToolInput;
@@ -316,12 +353,14 @@ export async function instrumentAgentLoop(opts: {
316
353
  };
317
354
  } = {
318
355
  spanId: sid,
356
+ ...(event.id ? { callId: event.id } : {}),
319
357
  startMs: Date.now(),
320
358
  toolName: event.tool,
321
359
  input: event.input,
322
360
  otelSpan: null,
323
361
  };
324
362
  pendingTools.set(counter, entry);
363
+ if (event.id) toolCallIdToCounter.set(event.id, counter);
325
364
  void startAgentSpan("tool.call", {
326
365
  "tool.name": event.tool,
327
366
  }).then((span) => {
@@ -343,16 +382,39 @@ export async function instrumentAgentLoop(opts: {
343
382
  else toolNameToCounters.set(event.tool, [counter]);
344
383
  } else if (event.type === "tool_done") {
345
384
  const queue = toolNameToCounters.get(event.tool);
346
- const counter = queue?.shift();
385
+ const counterFromId = event.id
386
+ ? toolCallIdToCounter.get(event.id)
387
+ : undefined;
388
+ const legacyQueueIndex =
389
+ event.id && counterFromId === undefined && queue
390
+ ? queue.findIndex(
391
+ (candidate) => !pendingTools.get(candidate)?.callId,
392
+ )
393
+ : -1;
394
+ const counter =
395
+ counterFromId ??
396
+ (event.id
397
+ ? legacyQueueIndex >= 0
398
+ ? queue?.[legacyQueueIndex]
399
+ : undefined
400
+ : queue?.shift());
347
401
  const pending =
348
402
  counter !== undefined ? pendingTools.get(counter) : undefined;
349
403
  if (counter !== undefined) {
350
404
  pendingTools.delete(counter);
405
+ if (pending?.callId) toolCallIdToCounter.delete(pending.callId);
406
+ if ((counterFromId !== undefined || legacyQueueIndex >= 0) && queue) {
407
+ const queueIndex = queue.indexOf(counter);
408
+ if (queueIndex >= 0) queue.splice(queueIndex, 1);
409
+ }
351
410
  if (queue && queue.length === 0)
352
411
  toolNameToCounters.delete(event.tool);
353
412
  }
354
413
  toolCallCount++;
355
414
 
415
+ const finishedAt = Date.now();
416
+
417
+ const explicitError = event.isError === true;
356
418
  const isError =
357
419
  typeof event.isError === "boolean"
358
420
  ? event.isError
@@ -362,6 +424,24 @@ export async function instrumentAgentLoop(opts: {
362
424
  if (isError) failedTools++;
363
425
  else successfulTools++;
364
426
 
427
+ if (
428
+ counter !== undefined &&
429
+ counter < MAX_TRACKED_GENERATION_TOOL_CALLS &&
430
+ pending
431
+ ) {
432
+ generationToolCalls.set(counter, {
433
+ name: pending.toolName,
434
+ started_offset_ms: Math.max(0, pending.startMs - runStart),
435
+ duration_ms: Math.max(0, finishedAt - pending.startMs),
436
+ status: isError ? "error" : "success",
437
+ error_class: !isError
438
+ ? null
439
+ : explicitError
440
+ ? "tool_error"
441
+ : "legacy_inferred_error",
442
+ });
443
+ }
444
+
365
445
  // Finalize the OTel tool span. If the span promise hasn't resolved yet
366
446
  // we record the result on the entry so its `.then` handler ends it.
367
447
  const otelEndResult = {
@@ -392,7 +472,7 @@ export async function instrumentAgentLoop(opts: {
392
472
  cacheReadTokens: 0,
393
473
  cacheWriteTokens: 0,
394
474
  costCentsX100: 0,
395
- durationMs: pending ? Date.now() - pending.startMs : 0,
475
+ durationMs: pending ? Math.max(0, finishedAt - pending.startMs) : 0,
396
476
  status: isError ? "error" : "success",
397
477
  errorMessage: isError ? event.result : null,
398
478
  metadata:
@@ -422,7 +502,11 @@ export async function instrumentAgentLoop(opts: {
422
502
  let errorMessage: string | null = null;
423
503
  let runMetadata: Record<string, unknown> | null = opts.metadata ?? null;
424
504
  try {
425
- usage = await runAgentLoop({ ...loopOpts, send: instrumentedSend });
505
+ usage = await runAgentLoop({
506
+ ...loopOpts,
507
+ runId,
508
+ send: instrumentedSend,
509
+ });
426
510
  } catch (err: any) {
427
511
  const classification = opts.classifyError?.(err) ?? null;
428
512
  runStatus = classification?.status ?? "error";
@@ -440,6 +524,62 @@ export async function instrumentAgentLoop(opts: {
440
524
  const runEnd = Date.now();
441
525
  const totalDurationMs = runEnd - runStart;
442
526
 
527
+ if (pendingTools.size > 0) {
528
+ if (runStatus === "success") {
529
+ runStatus = "error";
530
+ errorMessage ??= "Agent run ended with interrupted tool calls";
531
+ }
532
+ for (const [counter, pending] of pendingTools) {
533
+ toolCallCount += 1;
534
+ failedTools += 1;
535
+ if (counter < MAX_TRACKED_GENERATION_TOOL_CALLS) {
536
+ generationToolCalls.set(counter, {
537
+ name: pending.toolName,
538
+ started_offset_ms: Math.max(0, pending.startMs - runStart),
539
+ duration_ms: Math.max(0, runEnd - pending.startMs),
540
+ status: "error",
541
+ error_class: "interrupted",
542
+ });
543
+ }
544
+ const interruptedMessage = "Tool call interrupted before completion";
545
+ if (pending.otelSpan) {
546
+ openOtelToolSpans.delete(pending.otelSpan);
547
+ endAgentSpan(pending.otelSpan, {
548
+ status: "error",
549
+ errorMessage: interruptedMessage,
550
+ attributes: { "tool.name": pending.toolName },
551
+ });
552
+ } else {
553
+ pending.endResult = {
554
+ status: "error",
555
+ errorMessage: interruptedMessage,
556
+ };
557
+ }
558
+ spans.push({
559
+ id: pending.spanId,
560
+ runId,
561
+ threadId,
562
+ userId,
563
+ parentSpanId,
564
+ spanType: "tool_call",
565
+ name: pending.toolName,
566
+ inputTokens: 0,
567
+ outputTokens: 0,
568
+ cacheReadTokens: 0,
569
+ cacheWriteTokens: 0,
570
+ costCentsX100: 0,
571
+ durationMs: Math.max(0, runEnd - pending.startMs),
572
+ status: "error",
573
+ errorMessage: interruptedMessage,
574
+ metadata: null,
575
+ createdAt: runEnd,
576
+ });
577
+ }
578
+ pendingTools.clear();
579
+ toolNameToCounters.clear();
580
+ toolCallIdToCounter.clear();
581
+ }
582
+
443
583
  let costCentsX100 = 0;
444
584
  try {
445
585
  const { calculateCost } = await import("../usage/store.js");
@@ -455,8 +595,15 @@ export async function instrumentAgentLoop(opts: {
455
595
  } catch {}
456
596
 
457
597
  let llmCallCount = 0;
458
- if (usage) {
598
+ if (usage || runStatus === "error") {
459
599
  llmCallCount = 1;
600
+ const generationUsage = usage ?? {
601
+ inputTokens: 0,
602
+ outputTokens: 0,
603
+ cacheReadTokens: 0,
604
+ cacheWriteTokens: 0,
605
+ model: loopOpts.model,
606
+ };
460
607
  const llmSpanId = spanId();
461
608
  const llmSpan: TraceSpan = {
462
609
  id: llmSpanId,
@@ -465,11 +612,11 @@ export async function instrumentAgentLoop(opts: {
465
612
  userId,
466
613
  parentSpanId,
467
614
  spanType: "llm_call",
468
- name: usage.model,
469
- inputTokens: usage.inputTokens,
470
- outputTokens: usage.outputTokens,
471
- cacheReadTokens: usage.cacheReadTokens,
472
- cacheWriteTokens: usage.cacheWriteTokens,
615
+ name: generationUsage.model,
616
+ inputTokens: generationUsage.inputTokens,
617
+ outputTokens: generationUsage.outputTokens,
618
+ cacheReadTokens: generationUsage.cacheReadTokens,
619
+ cacheWriteTokens: generationUsage.cacheWriteTokens,
473
620
  costCentsX100,
474
621
  durationMs: totalDurationMs,
475
622
  status: runStatus,
@@ -488,11 +635,11 @@ export async function instrumentAgentLoop(opts: {
488
635
  typeof loopOpts.engine?.name === "string"
489
636
  ? loopOpts.engine.name
490
637
  : undefined,
491
- model: usage.model,
492
- inputTokens: usage.inputTokens,
493
- outputTokens: usage.outputTokens,
494
- cacheReadTokens: usage.cacheReadTokens,
495
- cacheWriteTokens: usage.cacheWriteTokens,
638
+ model: generationUsage.model,
639
+ inputTokens: generationUsage.inputTokens,
640
+ outputTokens: generationUsage.outputTokens,
641
+ cacheReadTokens: generationUsage.cacheReadTokens,
642
+ cacheWriteTokens: generationUsage.cacheWriteTokens,
496
643
  costCentsX100,
497
644
  durationMs: totalDurationMs,
498
645
  status: runStatus,
@@ -500,6 +647,12 @@ export async function instrumentAgentLoop(opts: {
500
647
  toolCalls: toolCallCount,
501
648
  successfulTools,
502
649
  failedTools,
650
+ tools: [...generationToolCalls.entries()]
651
+ .sort(([a], [b]) => a - b)
652
+ .map(([, detail]) => detail),
653
+ toolsTruncated:
654
+ toolInvocationCounter > MAX_TRACKED_GENERATION_TOOL_CALLS,
655
+ delegation: opts.delegation,
503
656
  createdAt: runStart,
504
657
  experimentAssignments: opts.experimentAssignments,
505
658
  modelSelectionSource: opts.modelSelectionSource,
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
 
3
3
  import {
4
4
  A2ATaskTimeoutError,
@@ -7,7 +7,11 @@ import {
7
7
  signA2AToken,
8
8
  } from "../a2a/client.js";
9
9
  import { invokeAgentAction } from "../a2a/invoke.js";
10
- import type { A2AApprovedAction, Task } from "../a2a/types.js";
10
+ import type {
11
+ A2AApprovedAction,
12
+ A2ACorrelationMetadata,
13
+ Task,
14
+ } from "../a2a/types.js";
11
15
  import {
12
16
  formatLlmCredentialErrorMessage,
13
17
  isLlmCredentialError,
@@ -28,6 +32,38 @@ const DEFAULT_SERVERLESS_INTEGRATION_A2A_TIMEOUT_MS = 18_000;
28
32
  const NETLIFY_INTEGRATION_A2A_TIMEOUT_MS = 2_000;
29
33
  const INTEGRATION_A2A_TOKEN_TTL = "30m";
30
34
 
35
+ function buildDelegationCorrelation(
36
+ context: ActionRunContext | undefined,
37
+ selfAppId: string | undefined,
38
+ invocationId?: string,
39
+ ): A2ACorrelationMetadata {
40
+ return {
41
+ ...(selfAppId?.trim() ? { callerApp: selfAppId.trim() } : {}),
42
+ ...(context?.threadId ? { callerThreadId: context.threadId } : {}),
43
+ ...(context?.runId ? { parentRunId: context.runId } : {}),
44
+ ...(context?.turnId ? { parentTurnId: context.turnId } : {}),
45
+ ...(invocationId ? { invocationId } : {}),
46
+ };
47
+ }
48
+
49
+ function buildMessageIdempotencyKey(
50
+ originatingTurnId: string | undefined,
51
+ target: string,
52
+ exactMessage: string,
53
+ ): string | undefined {
54
+ if (!originatingTurnId) return undefined;
55
+ const digest = createHash("sha256")
56
+ .update(
57
+ JSON.stringify({
58
+ originatingTurnId,
59
+ target,
60
+ message: exactMessage,
61
+ }),
62
+ )
63
+ .digest("hex");
64
+ return `v1:${digest}`;
65
+ }
66
+
31
67
  function parseTimeoutMs(value: string | undefined): number | undefined {
32
68
  if (!value) return undefined;
33
69
  const parsed = Number(value);
@@ -187,6 +223,12 @@ export async function run(
187
223
  return `Error: Agent "${agentIdOrName}" not found. Available agents: ${available || "(none)"}`;
188
224
  }
189
225
 
226
+ const correlation = buildDelegationCorrelation(context, selfAppId);
227
+ const idempotencyKey =
228
+ message && !taskId
229
+ ? buildMessageIdempotencyKey(context?.turnId, agent.url, message)
230
+ : undefined;
231
+
190
232
  if (action) {
191
233
  if (context?.send) {
192
234
  context.send({ type: "agent_call", agent: agent.name, status: "start" });
@@ -196,6 +238,7 @@ export async function run(
196
238
  agent,
197
239
  action,
198
240
  input as Record<string, unknown>,
241
+ buildDelegationCorrelation(context, selfAppId, randomUUID()),
199
242
  );
200
243
  return output;
201
244
  } finally {
@@ -389,6 +432,9 @@ export async function run(
389
432
  orgDomain: callerOrgDomain,
390
433
  orgSecret: callerOrgSecret,
391
434
  approvedActions,
435
+ contextId: context.threadId,
436
+ correlation,
437
+ idempotencyKey,
392
438
  ...(taskId ? { taskId } : {}),
393
439
  onUpdate: onRemotePollUpdate,
394
440
  returnRecoverableArtifactsOnTimeout: false,
@@ -480,6 +526,9 @@ export async function run(
480
526
  orgDomain: domain,
481
527
  orgSecret,
482
528
  approvedActions,
529
+ contextId: context?.threadId,
530
+ correlation,
531
+ idempotencyKey,
483
532
  ...(taskId ? { taskId } : {}),
484
533
  returnRecoverableArtifactsOnTimeout: false,
485
534
  });
@@ -514,6 +563,7 @@ async function invokeReadOnlyAppAction(
514
563
  agent: { name: string; url: string },
515
564
  action: string,
516
565
  input: Record<string, unknown>,
566
+ correlation: A2ACorrelationMetadata,
517
567
  ): Promise<string> {
518
568
  const callerEmail = getRequestUserEmail();
519
569
  if (!callerEmail) {
@@ -544,6 +594,7 @@ async function invokeReadOnlyAppAction(
544
594
  userEmail: callerEmail,
545
595
  orgDomain: callerOrgDomain,
546
596
  orgSecret: callerOrgSecret,
597
+ correlation,
547
598
  });
548
599
  return invocation.result.status === "completed"
549
600
  ? invocation.result.output
@@ -191,34 +191,81 @@ function formatA2ATerminalError(
191
191
 
192
192
  type A2AAgentLoopRunner = typeof runAgentLoopDirectWithSoftTimeout;
193
193
 
194
- function runDelegatedAgentLoop(
194
+ export interface DelegatedAgentLoopTelemetry {
195
+ runId: string;
196
+ threadId: string | null;
197
+ userId: string | null;
198
+ delegation: {
199
+ protocol: "a2a" | "mcp";
200
+ callerApp?: string;
201
+ taskId?: string;
202
+ parentRunId?: string;
203
+ parentTurnId?: string;
204
+ };
205
+ }
206
+
207
+ interface DelegatedAgentLoopOptions {
208
+ runner?: A2AAgentLoopRunner;
209
+ telemetry?: DelegatedAgentLoopTelemetry;
210
+ }
211
+
212
+ async function runDelegatedAgentLoop(
195
213
  runOptions: Parameters<A2AAgentLoopRunner>[0],
196
214
  pluginOptions: Pick<
197
215
  AgentChatPluginOptions,
198
216
  "finalResponseGuard" | "runSoftTimeoutMs"
199
217
  >,
200
218
  timeoutOptions: Parameters<A2AAgentLoopRunner>[2],
201
- runner: A2AAgentLoopRunner,
219
+ options: DelegatedAgentLoopOptions,
202
220
  ) {
203
- return runner(
204
- {
205
- ...runOptions,
206
- // Delegated runs resolve their own model and do not pass through the
207
- // interactive request handler's output-token setup. Use the same
208
- // model-aware headroom here so reasoning models (notably GPT-5.x) do
209
- // not spend the small internal default entirely on reasoning before
210
- // emitting a tool call or answer. Preserve explicit test/caller values.
211
- maxOutputTokens:
212
- runOptions.maxOutputTokens ??
213
- resolveMainChatMaxOutputTokens(runOptions.model),
214
- reasoningEffort:
215
- runOptions.reasoningEffort ??
216
- resolveAgentRequestReasoningEffort({ model: runOptions.model }),
217
- finalResponseGuard: pluginOptions.finalResponseGuard,
218
- },
219
- pluginOptions.runSoftTimeoutMs,
220
- timeoutOptions,
221
- );
221
+ const runner = options.runner ?? runAgentLoopDirectWithSoftTimeout;
222
+ const resolvedRunOptions = {
223
+ ...runOptions,
224
+ // Delegated runs resolve their own model and do not pass through the
225
+ // interactive request handler's output-token setup. Use the same
226
+ // model-aware headroom here so reasoning models (notably GPT-5.x) do
227
+ // not spend the small internal default entirely on reasoning before
228
+ // emitting a tool call or answer. Preserve explicit test/caller values.
229
+ maxOutputTokens:
230
+ runOptions.maxOutputTokens ??
231
+ resolveMainChatMaxOutputTokens(runOptions.model),
232
+ reasoningEffort:
233
+ runOptions.reasoningEffort ??
234
+ resolveAgentRequestReasoningEffort({ model: runOptions.model }),
235
+ finalResponseGuard: pluginOptions.finalResponseGuard,
236
+ };
237
+ const execute = (loopOptions = resolvedRunOptions) =>
238
+ runner(loopOptions, pluginOptions.runSoftTimeoutMs, timeoutOptions);
239
+
240
+ if (!options.telemetry) return execute();
241
+
242
+ let instrumented = false;
243
+ try {
244
+ const { getObservabilityConfig, instrumentAgentLoop } =
245
+ await import("../../observability/traces.js");
246
+ const config = await getObservabilityConfig();
247
+ if (config.enabled) {
248
+ instrumented = true;
249
+ return await instrumentAgentLoop({
250
+ runAgentLoop: (loopOptions) =>
251
+ execute(
252
+ loopOptions as Parameters<A2AAgentLoopRunner>[0] &
253
+ typeof resolvedRunOptions,
254
+ ),
255
+ loopOpts: resolvedRunOptions,
256
+ runId: options.telemetry.runId,
257
+ threadId: options.telemetry.threadId,
258
+ userId: options.telemetry.userId,
259
+ config,
260
+ delegation: options.telemetry.delegation,
261
+ });
262
+ }
263
+ } catch (error) {
264
+ // Match interactive chat: setup failures fall through, but a failure from
265
+ // inside an instrumented agent loop is the real run failure and rethrows.
266
+ if (instrumented) throw error;
267
+ }
268
+ return execute();
222
269
  }
223
270
 
224
271
  /**
@@ -236,13 +283,13 @@ export function runA2AAgentLoop(
236
283
  "finalResponseGuard" | "runSoftTimeoutMs"
237
284
  >,
238
285
  timeoutOptions: Parameters<A2AAgentLoopRunner>[2],
239
- runner: A2AAgentLoopRunner = runAgentLoopDirectWithSoftTimeout,
286
+ options: DelegatedAgentLoopOptions = {},
240
287
  ) {
241
288
  return runDelegatedAgentLoop(
242
289
  runOptions,
243
290
  pluginOptions,
244
291
  timeoutOptions,
245
- runner,
292
+ options,
246
293
  );
247
294
  }
248
295
 
@@ -258,13 +305,13 @@ export function runMCPAgentLoop(
258
305
  "finalResponseGuard" | "runSoftTimeoutMs"
259
306
  >,
260
307
  timeoutOptions: Parameters<A2AAgentLoopRunner>[2],
261
- runner: A2AAgentLoopRunner = runAgentLoopDirectWithSoftTimeout,
308
+ options: DelegatedAgentLoopOptions = {},
262
309
  ) {
263
310
  return runDelegatedAgentLoop(
264
311
  runOptions,
265
312
  pluginOptions,
266
313
  timeoutOptions,
267
- runner,
314
+ options,
268
315
  );
269
316
  }
270
317