@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
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2308
31
- - template files: 6135
30
+ - core files: 2309
31
+ - template files: 6143
@@ -1,5 +1,12 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.109.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d74a5a4: Return retryable responses for transient agent-chat database failures and back off active-run polling while the database recovers.
8
+ - d74a5a4: Add bounded per-tool generation telemetry, delegated trace linkage, direct-read telemetry, and idempotent A2A submissions.
9
+
3
10
  ## 0.109.0
4
11
 
5
12
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.109.0",
3
+ "version": "0.109.1",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,8 +1,10 @@
1
1
  import * as jose from "jose";
2
2
 
3
3
  import { ssrfSafeFetch } from "../extensions/url-safety.js";
4
+ import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
4
5
  import type {
5
6
  A2AApprovedAction,
7
+ A2ACorrelationMetadata,
6
8
  A2AReadOnlyActionResult,
7
9
  AgentCard,
8
10
  JsonRpcRequest,
@@ -239,6 +241,7 @@ export class A2AClient {
239
241
  opts?: {
240
242
  contextId?: string;
241
243
  metadata?: Record<string, unknown>;
244
+ idempotencyKey?: string;
242
245
  approvedActions?: A2AApprovedAction[];
243
246
  /**
244
247
  * If true, ask the server to return the task immediately in `working`
@@ -255,6 +258,7 @@ export class A2AClient {
255
258
  message,
256
259
  contextId: opts?.contextId,
257
260
  metadata: opts?.metadata,
261
+ ...(opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
258
262
  ...(opts?.approvedActions?.length
259
263
  ? { approvedActions: opts.approvedActions }
260
264
  : {}),
@@ -291,8 +295,14 @@ export class A2AClient {
291
295
  async invokeAction(
292
296
  action: string,
293
297
  input: Record<string, unknown> = {},
298
+ opts?: { metadata?: A2ACorrelationMetadata },
294
299
  ): Promise<A2AReadOnlyActionResult> {
295
- const response = await this.rpc("actions/invoke", { action, input });
300
+ const metadata = sanitizeA2ACorrelationMetadata(opts?.metadata);
301
+ const response = await this.rpc("actions/invoke", {
302
+ action,
303
+ input,
304
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
305
+ });
296
306
  if (response.error) {
297
307
  throw new Error(
298
308
  `A2A error (${response.error.code}): ${response.error.message}`,
@@ -315,6 +325,7 @@ export class A2AClient {
315
325
  opts?: {
316
326
  contextId?: string;
317
327
  metadata?: Record<string, unknown>;
328
+ idempotencyKey?: string;
318
329
  approvedActions?: A2AApprovedAction[];
319
330
  /** Total time to wait for completion. Default 5 min. */
320
331
  timeoutMs?: number;
@@ -327,6 +338,7 @@ export class A2AClient {
327
338
  const submitted = await this.send(message, {
328
339
  contextId: opts?.contextId,
329
340
  metadata: opts?.metadata,
341
+ idempotencyKey: opts?.idempotencyKey,
330
342
  ...(opts?.approvedActions?.length
331
343
  ? { approvedActions: opts.approvedActions }
332
344
  : {}),
@@ -620,6 +632,10 @@ export async function callAgent(
620
632
  requestOrigin?: string;
621
633
  /** Exact downstream actions explicitly authorized in the caller's chat. */
622
634
  approvedActions?: A2AApprovedAction[];
635
+ /** Bounded telemetry-only lineage forwarded to the receiving app. */
636
+ correlation?: A2ACorrelationMetadata;
637
+ /** Stable caller-generated key for one message submission. */
638
+ idempotencyKey?: string;
623
639
  /**
624
640
  * Use async/poll instead of a single blocking POST. Recommended for
625
641
  * cross-app calls that may exceed a synchronous serverless request budget.
@@ -657,6 +673,7 @@ export async function callAgent(
657
673
  if (opts?.userEmail) metadata.userEmail = opts.userEmail;
658
674
  if (opts?.orgDomain) metadata.orgDomain = opts.orgDomain;
659
675
  if (opts?.requestOrigin) metadata.requestOrigin = opts.requestOrigin;
676
+ Object.assign(metadata, sanitizeA2ACorrelationMetadata(opts?.correlation));
660
677
 
661
678
  // Default to async + poll. The receiving A2A server's `_process-task` route
662
679
  // runs the handler in a fresh function execution (cross-platform queue
@@ -691,6 +708,7 @@ export async function callAgent(
691
708
  : await client.sendAndWait(message, {
692
709
  contextId: opts?.contextId,
693
710
  metadata,
711
+ idempotencyKey: opts?.idempotencyKey,
694
712
  ...(opts?.approvedActions?.length
695
713
  ? { approvedActions: opts.approvedActions }
696
714
  : {}),
@@ -705,6 +723,7 @@ export async function callAgent(
705
723
  task = await client.send(message, {
706
724
  contextId: opts?.contextId,
707
725
  metadata,
726
+ idempotencyKey: opts?.idempotencyKey,
708
727
  ...(opts?.approvedActions?.length
709
728
  ? { approvedActions: opts.approvedActions }
710
729
  : {}),
@@ -756,6 +775,7 @@ export async function callAction(
756
775
  orgDomain?: string;
757
776
  orgSecret?: string;
758
777
  requestTimeoutMs?: number;
778
+ correlation?: A2ACorrelationMetadata;
759
779
  },
760
780
  ): Promise<A2AReadOnlyActionResult> {
761
781
  const actionName = action.trim();
@@ -775,7 +795,9 @@ export async function callAction(
775
795
  fallbackApiKeys,
776
796
  requestTimeoutMs: opts?.requestTimeoutMs,
777
797
  });
778
- return client.invokeAction(actionName, input);
798
+ return client.invokeAction(actionName, input, {
799
+ metadata: sanitizeA2ACorrelationMetadata(opts?.correlation),
800
+ });
779
801
  }
780
802
 
781
803
  async function buildA2AApiKeyAttempts(
@@ -0,0 +1,50 @@
1
+ import type { A2ACorrelationMetadata } from "./types.js";
2
+
3
+ export const MAX_A2A_CORRELATION_VALUE_CHARS = 200;
4
+
5
+ const APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
6
+ const CORRELATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
7
+
8
+ function boundedIdentifier(
9
+ value: unknown,
10
+ pattern: RegExp,
11
+ ): string | undefined {
12
+ if (typeof value !== "string") return undefined;
13
+ const trimmed = value.trim();
14
+ if (
15
+ !trimmed ||
16
+ trimmed.length > MAX_A2A_CORRELATION_VALUE_CHARS ||
17
+ !pattern.test(trimmed)
18
+ ) {
19
+ return undefined;
20
+ }
21
+ return trimmed;
22
+ }
23
+
24
+ export function sanitizeA2ACorrelationId(value: unknown): string | undefined {
25
+ return boundedIdentifier(value, CORRELATION_ID_PATTERN);
26
+ }
27
+
28
+ /**
29
+ * Keep only bounded, opaque ASCII correlation identifiers. These values
30
+ * remain telemetry hints; authentication continues to come exclusively from
31
+ * the verified A2A token/request context.
32
+ */
33
+ export function sanitizeA2ACorrelationMetadata(
34
+ value: unknown,
35
+ ): A2ACorrelationMetadata {
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
37
+ const metadata = value as Record<string, unknown>;
38
+ const callerApp = boundedIdentifier(metadata.callerApp, APP_ID_PATTERN);
39
+ const callerThreadId = sanitizeA2ACorrelationId(metadata.callerThreadId);
40
+ const parentRunId = sanitizeA2ACorrelationId(metadata.parentRunId);
41
+ const parentTurnId = sanitizeA2ACorrelationId(metadata.parentTurnId);
42
+ const invocationId = sanitizeA2ACorrelationId(metadata.invocationId);
43
+ return {
44
+ ...(callerApp ? { callerApp } : {}),
45
+ ...(callerThreadId ? { callerThreadId } : {}),
46
+ ...(parentRunId ? { parentRunId } : {}),
47
+ ...(parentTurnId ? { parentTurnId } : {}),
48
+ ...(invocationId ? { invocationId } : {}),
49
+ };
50
+ }
@@ -1,3 +1,5 @@
1
+ import crypto from "node:crypto";
2
+
1
3
  import { setResponseHeader, setResponseStatus } from "h3";
2
4
 
3
5
  import {
@@ -7,18 +9,22 @@ import {
7
9
  isAgentChatDurableBackgroundEnabled,
8
10
  resolveAgentChatProcessRunDispatchPath,
9
11
  } from "../agent/durable-background.js";
12
+ import { trackingIdentityProperties } from "../observability/tracking-identity.js";
10
13
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
11
14
  import { getOrigin, isConfiguredAppOrigin } from "../server/google-oauth.js";
12
15
  import { fireInternalDispatch } from "../server/self-dispatch.js";
13
16
  import { agentChat } from "../shared/agent-chat.js";
17
+ import { track } from "../tracking/registry.js";
14
18
  import {
15
19
  hasConfiguredA2ASecret,
16
20
  isA2AProductionRuntime,
17
21
  } from "./auth-policy.js";
22
+ import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
18
23
  import {
19
24
  createTask,
25
+ createOrReuseTask,
20
26
  getTask,
21
- getTaskOwner,
27
+ getTaskOwnership,
22
28
  updateTask,
23
29
  claimA2ATaskForProcessing,
24
30
  getA2ATaskDispatchState,
@@ -28,6 +34,8 @@ import {
28
34
  touchQueuedA2ATaskDispatch,
29
35
  touchProcessingA2ATask,
30
36
  pauseProcessingA2ATask,
37
+ MAX_A2A_IDEMPOTENCY_KEY_CHARS,
38
+ A2A_PERSONAL_OWNER_SCOPE,
31
39
  } from "./task-store.js";
32
40
  import type {
33
41
  A2AApprovedAction,
@@ -51,6 +59,7 @@ const A2A_PROCESSING_HEARTBEAT_MS = 30_000;
51
59
  const MAX_A2A_APPROVED_ACTIONS = 10;
52
60
  const MAX_A2A_DIRECT_ACTION_NAME_CHARS = 200;
53
61
  const MAX_A2A_DIRECT_ACTION_INPUT_BYTES = 64 * 1024;
62
+ const A2A_READ_INVOKE_EVENT = "$a2a_read_invoke";
54
63
 
55
64
  function trustedApprovedActions(
56
65
  value: unknown,
@@ -580,6 +589,22 @@ async function runHandlerAndPersist(
580
589
  }
581
590
  }
582
591
 
592
+ function verifiedTaskOwner(event?: any): {
593
+ ownerEmail: string | null;
594
+ ownerScope: string | null;
595
+ } {
596
+ const ownerEmail =
597
+ (event?.context?.__a2aVerifiedEmail as string | undefined) ?? null;
598
+ return {
599
+ ownerEmail,
600
+ ownerScope: ownerEmail
601
+ ? ((event?.context?.__a2aOrgDomain as string | undefined)
602
+ ?.trim()
603
+ .toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE)
604
+ : null,
605
+ };
606
+ }
607
+
583
608
  async function handleSend(
584
609
  params: Record<string, unknown>,
585
610
  config: A2AConfig,
@@ -606,8 +631,31 @@ async function handleSend(
606
631
  // on every subsequent tasks/get and tasks/cancel call. Caller-supplied
607
632
  // metadata.userEmail is NEVER used for ownership; that would re-introduce
608
633
  // the IDOR class fixed here.
609
- const ownerEmailForTask =
610
- (event?.context?.__a2aVerifiedEmail as string | undefined) ?? null;
634
+ const { ownerEmail: ownerEmailForTask, ownerScope: ownerScopeForTask } =
635
+ verifiedTaskOwner(event);
636
+ let idempotencyKey: string | undefined;
637
+ if (ownerEmailForTask && params.idempotencyKey !== undefined) {
638
+ if (typeof params.idempotencyKey !== "string") {
639
+ return {
640
+ ...jsonRpcError(
641
+ 0,
642
+ -32602,
643
+ "Invalid params: idempotencyKey must be a string",
644
+ ),
645
+ _id: 0,
646
+ };
647
+ }
648
+ idempotencyKey = params.idempotencyKey.trim();
649
+ if (
650
+ !idempotencyKey ||
651
+ idempotencyKey.length > MAX_A2A_IDEMPOTENCY_KEY_CHARS
652
+ ) {
653
+ return {
654
+ ...jsonRpcError(0, -32602, "Invalid params: idempotencyKey is invalid"),
655
+ _id: 0,
656
+ };
657
+ }
658
+ }
611
659
 
612
660
  // Async mode: return the task immediately in `working` state, run the
613
661
  // handler in the background, and let the caller poll `tasks/get`. This is
@@ -620,6 +668,7 @@ async function handleSend(
620
668
  // production — see the additional gate below.
621
669
  const asyncMode =
622
670
  params.async === true || (event && event.context?.__a2aForceAsync === true);
671
+ if (!asyncMode) idempotencyKey = undefined;
623
672
 
624
673
  if (asyncMode) {
625
674
  // Refuse async mode entirely when no auth is configured in production.
@@ -667,12 +716,20 @@ async function handleSend(
667
716
  approvedActions: approvedActions ?? null,
668
717
  },
669
718
  };
670
- const task = await createTask(
719
+ const { task, reused } = await createOrReuseTask(
671
720
  message,
672
721
  contextId,
673
722
  taskMetadata,
674
723
  ownerEmailForTask,
724
+ ownerScopeForTask,
725
+ idempotencyKey,
675
726
  );
727
+ if (reused) {
728
+ return {
729
+ ...jsonRpcResult(0, sanitizeTaskForResponse(task)),
730
+ _id: 0,
731
+ };
732
+ }
676
733
  const working = await updateTask(task.id, { state: "working" });
677
734
 
678
735
  // Awaited, not fire-and-forget: this handler is about to return, and a
@@ -695,12 +752,20 @@ async function handleSend(
695
752
  }
696
753
 
697
754
  return withA2ARequestContext(metadata, event, async () => {
698
- const task = await createTask(
755
+ const { task, reused } = await createOrReuseTask(
699
756
  message,
700
757
  contextId,
701
758
  undefined,
702
759
  ownerEmailForTask,
760
+ ownerScopeForTask,
761
+ idempotencyKey,
703
762
  );
763
+ if (reused) {
764
+ return {
765
+ ...jsonRpcResult(0, sanitizeTaskForResponse(task)),
766
+ _id: 0,
767
+ };
768
+ }
704
769
  await updateTask(task.id, { state: "working" });
705
770
 
706
771
  const ctx = makeHandlerContext(
@@ -790,8 +855,8 @@ async function handleStream(
790
855
  const contextId = params.contextId as string | undefined;
791
856
  const metadata = params.metadata as Record<string, unknown> | undefined;
792
857
  const approvedActions = trustedApprovedActions(params.approvedActions, event);
793
- const ownerEmailForTask =
794
- (event?.context?.__a2aVerifiedEmail as string | undefined) ?? null;
858
+ const { ownerEmail: ownerEmailForTask, ownerScope: ownerScopeForTask } =
859
+ verifiedTaskOwner(event);
795
860
 
796
861
  await withA2ARequestContext(metadata, event, async () => {
797
862
  const task = await createTask(
@@ -799,6 +864,7 @@ async function handleStream(
799
864
  contextId,
800
865
  undefined,
801
866
  ownerEmailForTask,
867
+ ownerScopeForTask,
802
868
  );
803
869
 
804
870
  await updateTask(task.id, { state: "working" });
@@ -879,16 +945,23 @@ const SENSITIVE_METADATA_KEYS = new Set([
879
945
 
880
946
  function sanitizeTaskForResponse(task: any): any {
881
947
  if (!task || typeof task !== "object") return task;
882
- if (!task.metadata || typeof task.metadata !== "object") return task;
948
+ const {
949
+ ownerEmail: _ownerEmail,
950
+ ownerScope: _ownerScope,
951
+ ...publicTask
952
+ } = task;
953
+ if (!publicTask.metadata || typeof publicTask.metadata !== "object") {
954
+ return publicTask;
955
+ }
883
956
 
884
- const meta = task.metadata as Record<string, unknown>;
957
+ const meta = publicTask.metadata as Record<string, unknown>;
885
958
  const publicMeta: Record<string, unknown> = {};
886
959
  for (const [k, v] of Object.entries(meta)) {
887
960
  if (k === "__a2a_processor") continue;
888
961
  if (SENSITIVE_METADATA_KEYS.has(k)) continue;
889
962
  publicMeta[k] = v;
890
963
  }
891
- return { ...task, metadata: publicMeta };
964
+ return { ...publicTask, metadata: publicMeta };
892
965
  }
893
966
 
894
967
  /**
@@ -907,6 +980,7 @@ function sanitizeTaskForResponse(task: any): any {
907
980
  */
908
981
  function authorizeTaskAccess(
909
982
  taskOwnerEmail: string | null,
983
+ taskOwnerScope: string | null,
910
984
  event: any,
911
985
  config: A2AConfig,
912
986
  ): JsonRpcResponse | null {
@@ -928,6 +1002,15 @@ function authorizeTaskAccess(
928
1002
  if (verifiedEmail.toLowerCase() !== taskOwnerEmail.toLowerCase()) {
929
1003
  return jsonRpcError(0, -32001, "Task not found");
930
1004
  }
1005
+ if (taskOwnerScope) {
1006
+ const verifiedScope =
1007
+ (event?.context?.__a2aOrgDomain as string | undefined)
1008
+ ?.trim()
1009
+ .toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE;
1010
+ if (verifiedScope !== taskOwnerScope.toLowerCase()) {
1011
+ return jsonRpcError(0, -32001, "Task not found");
1012
+ }
1013
+ }
931
1014
  }
932
1015
  // Legacy row (no owner_email recorded). The route-level auth gate is the
933
1016
  // only thing protecting it — fall through and serve.
@@ -943,8 +1026,13 @@ async function handleGet(
943
1026
  if (!id) {
944
1027
  return jsonRpcError(0, -32602, "Invalid params: id required");
945
1028
  }
946
- const ownerEmail = await getTaskOwner(id);
947
- const denied = authorizeTaskAccess(ownerEmail, event, config);
1029
+ const ownership = await getTaskOwnership(id);
1030
+ const denied = authorizeTaskAccess(
1031
+ ownership.ownerEmail,
1032
+ ownership.ownerScope,
1033
+ event,
1034
+ config,
1035
+ );
948
1036
  if (denied) return denied;
949
1037
 
950
1038
  const task = await getTask(id);
@@ -1038,8 +1126,13 @@ async function handleCancel(
1038
1126
  if (!id) {
1039
1127
  return jsonRpcError(0, -32602, "Invalid params: id required");
1040
1128
  }
1041
- const ownerEmail = await getTaskOwner(id);
1042
- const denied = authorizeTaskAccess(ownerEmail, event, config);
1129
+ const ownership = await getTaskOwnership(id);
1130
+ const denied = authorizeTaskAccess(
1131
+ ownership.ownerEmail,
1132
+ ownership.ownerScope,
1133
+ event,
1134
+ config,
1135
+ );
1043
1136
  if (denied) return denied;
1044
1137
 
1045
1138
  const task = await updateTask(id, { state: "canceled" });
@@ -1089,15 +1182,48 @@ async function handleInvokeReadOnlyAction(
1089
1182
  return jsonRpcError(0, -32601, "Direct action invocation not supported");
1090
1183
  }
1091
1184
 
1185
+ const startedAt = Date.now();
1186
+ const correlation = sanitizeA2ACorrelationMetadata(params.metadata);
1187
+ const invocationId = crypto.randomUUID();
1188
+ const verifiedEmail = event.context.__a2aVerifiedEmail as string;
1189
+ const emitTracking = (status: "completed" | "failed") => {
1190
+ const identity = trackingIdentityProperties();
1191
+ track(
1192
+ A2A_READ_INVOKE_EVENT,
1193
+ {
1194
+ ...identity,
1195
+ action,
1196
+ receiver_app: config.appId ?? identity.app ?? config.name,
1197
+ caller_app: correlation.callerApp ?? "unknown",
1198
+ duration_ms: Math.max(0, Date.now() - startedAt),
1199
+ status,
1200
+ invocation_id: invocationId,
1201
+ ...(correlation.parentRunId
1202
+ ? { parent_run_id: correlation.parentRunId }
1203
+ : {}),
1204
+ ...(correlation.parentTurnId
1205
+ ? { parent_turn_id: correlation.parentTurnId }
1206
+ : {}),
1207
+ ...(correlation.invocationId
1208
+ ? { caller_invocation_id: correlation.invocationId }
1209
+ : {}),
1210
+ },
1211
+ { userId: verifiedEmail },
1212
+ );
1213
+ };
1214
+
1092
1215
  try {
1093
1216
  const result = await withA2ARequestContext(undefined, event, () =>
1094
1217
  config.executeReadOnlyAction!({
1095
1218
  action,
1096
1219
  input: input as Record<string, unknown>,
1220
+ invocationId,
1097
1221
  }),
1098
1222
  );
1223
+ emitTracking(result.status);
1099
1224
  return jsonRpcResult(0, { action, ...result });
1100
1225
  } catch (error) {
1226
+ emitTracking("failed");
1101
1227
  console.error(`[a2a] Direct action ${action} failed:`, error);
1102
1228
  return jsonRpcError(0, -32000, "Direct action invocation failed");
1103
1229
  }
@@ -34,6 +34,7 @@ export type {
34
34
  Artifact,
35
35
  JsonRpcRequest,
36
36
  JsonRpcResponse,
37
+ A2ACorrelationMetadata,
37
38
  A2AReadOnlyActionInvocation,
38
39
  A2AReadOnlyActionResult,
39
40
  } from "./types.js";
@@ -7,7 +7,10 @@ import {
7
7
  callAction as defaultCallAction,
8
8
  callAgent as defaultCallAgent,
9
9
  } from "./client.js";
10
- import type { A2AReadOnlyActionResult } from "./types.js";
10
+ import type {
11
+ A2ACorrelationMetadata,
12
+ A2AReadOnlyActionResult,
13
+ } from "./types.js";
11
14
 
12
15
  export type AgentInvocationErrorCode =
13
16
  | "missing-target"
@@ -82,6 +85,8 @@ export interface InvokeAgentOptions extends ResolveAgentInvocationTargetOptions
82
85
  timeoutMs?: number;
83
86
  pollIntervalMs?: number;
84
87
  includeInvocationHint?: boolean;
88
+ correlation?: A2ACorrelationMetadata;
89
+ idempotencyKey?: string;
85
90
  runtime?: Partial<AgentInvocationRuntime>;
86
91
  }
87
92
 
@@ -94,6 +99,7 @@ export interface InvokeAgentActionOptions extends ResolveAgentInvocationTargetOp
94
99
  orgDomain?: string;
95
100
  orgSecret?: string;
96
101
  requestTimeoutMs?: number;
102
+ correlation?: A2ACorrelationMetadata;
97
103
  runtime?: Partial<AgentInvocationRuntime>;
98
104
  }
99
105
 
@@ -197,6 +203,8 @@ export async function invokeAgent(
197
203
  async: options.async,
198
204
  timeoutMs: options.timeoutMs,
199
205
  pollIntervalMs: options.pollIntervalMs,
206
+ correlation: options.correlation,
207
+ idempotencyKey: options.idempotencyKey,
200
208
  });
201
209
 
202
210
  return {
@@ -243,6 +251,7 @@ export async function invokeAgentAction(
243
251
  orgDomain: options.orgDomain,
244
252
  orgSecret: options.orgSecret,
245
253
  requestTimeoutMs: options.requestTimeoutMs,
254
+ correlation: options.correlation,
246
255
  });
247
256
 
248
257
  return { target, action, result };