@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +24 -2
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +140 -14
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/invoke.ts +10 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +16 -0
- package/corpus/core/src/action.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +23 -0
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/scripts/call-agent.ts +53 -2
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +18 -6
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +45 -18
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
- package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/dist/a2a/client.d.ts +11 -2
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +16 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +4 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +3 -0
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +15 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +2 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +35 -3
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +35 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +93 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
|
|
3
3
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ensureTableExists,
|
|
6
|
+
ensureColumnExists,
|
|
7
|
+
ensureIndexExists,
|
|
8
|
+
} from "../db/ddl-guard.js";
|
|
5
9
|
import type { Task, Message, TaskState, Artifact } from "./types.js";
|
|
6
10
|
|
|
7
11
|
let _initPromise: Promise<void> | undefined;
|
|
12
|
+
export const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
|
|
13
|
+
const A2A_IDEMPOTENCY_INDEX = "idx_a2a_tasks_owner_scope_idempotency";
|
|
14
|
+
export const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
|
|
8
15
|
|
|
9
16
|
async function ensureTable(): Promise<void> {
|
|
10
17
|
if (!_initPromise) {
|
|
@@ -20,10 +27,16 @@ async function ensureTable(): Promise<void> {
|
|
|
20
27
|
history TEXT NOT NULL DEFAULT '[]',
|
|
21
28
|
artifacts TEXT NOT NULL DEFAULT '[]',
|
|
22
29
|
metadata TEXT,
|
|
30
|
+
owner_email TEXT,
|
|
31
|
+
owner_scope TEXT NOT NULL DEFAULT '',
|
|
32
|
+
idempotency_key TEXT,
|
|
23
33
|
created_at ${intType()} NOT NULL,
|
|
24
34
|
updated_at ${intType()} NOT NULL
|
|
25
35
|
)
|
|
26
36
|
`;
|
|
37
|
+
const createIdempotencyIndexSql =
|
|
38
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS ${A2A_IDEMPOTENCY_INDEX} ` +
|
|
39
|
+
`ON a2a_tasks(owner_email, owner_scope, idempotency_key)`;
|
|
27
40
|
const createApprovalsSql = `
|
|
28
41
|
CREATE TABLE IF NOT EXISTS a2a_approvals (
|
|
29
42
|
id TEXT PRIMARY KEY,
|
|
@@ -57,6 +70,20 @@ async function ensureTable(): Promise<void> {
|
|
|
57
70
|
"owner_email",
|
|
58
71
|
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_email TEXT`,
|
|
59
72
|
);
|
|
73
|
+
await ensureColumnExists(
|
|
74
|
+
"a2a_tasks",
|
|
75
|
+
"owner_scope",
|
|
76
|
+
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_scope TEXT NOT NULL DEFAULT ''`,
|
|
77
|
+
);
|
|
78
|
+
await ensureColumnExists(
|
|
79
|
+
"a2a_tasks",
|
|
80
|
+
"idempotency_key",
|
|
81
|
+
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS idempotency_key TEXT`,
|
|
82
|
+
);
|
|
83
|
+
await ensureIndexExists(
|
|
84
|
+
A2A_IDEMPOTENCY_INDEX,
|
|
85
|
+
createIdempotencyIndexSql,
|
|
86
|
+
);
|
|
60
87
|
await ensureTableExists("a2a_approvals", createApprovalsSql);
|
|
61
88
|
return;
|
|
62
89
|
}
|
|
@@ -69,6 +96,13 @@ async function ensureTable(): Promise<void> {
|
|
|
69
96
|
// have NULL owner_email and remain accessible to legacy callers via
|
|
70
97
|
// the legacy-token apiKeyEnv path; new rows are scoped from this point
|
|
71
98
|
// forward.
|
|
99
|
+
try {
|
|
100
|
+
await client.execute(
|
|
101
|
+
`ALTER TABLE a2a_tasks ADD COLUMN owner_scope TEXT NOT NULL DEFAULT ''`,
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
// Column already exists — expected on every restart after first run.
|
|
105
|
+
}
|
|
72
106
|
try {
|
|
73
107
|
await client.execute(
|
|
74
108
|
`ALTER TABLE a2a_tasks ADD COLUMN owner_email TEXT`,
|
|
@@ -76,6 +110,14 @@ async function ensureTable(): Promise<void> {
|
|
|
76
110
|
} catch {
|
|
77
111
|
// Column already exists — expected on every restart after first run.
|
|
78
112
|
}
|
|
113
|
+
try {
|
|
114
|
+
await client.execute(
|
|
115
|
+
`ALTER TABLE a2a_tasks ADD COLUMN idempotency_key TEXT`,
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// Column already exists — expected on every restart after first run.
|
|
119
|
+
}
|
|
120
|
+
await client.execute(createIdempotencyIndexSql);
|
|
79
121
|
await client.execute(createApprovalsSql);
|
|
80
122
|
})().catch((err) => {
|
|
81
123
|
// Retry init on the next call after a failed startup.
|
|
@@ -320,7 +362,10 @@ export async function pauseProcessingA2ATask(
|
|
|
320
362
|
return task;
|
|
321
363
|
}
|
|
322
364
|
|
|
323
|
-
function taskFromRow(row: any): Task & {
|
|
365
|
+
function taskFromRow(row: any): Task & {
|
|
366
|
+
ownerEmail?: string | null;
|
|
367
|
+
ownerScope?: string | null;
|
|
368
|
+
} {
|
|
324
369
|
return {
|
|
325
370
|
id: row.id as string,
|
|
326
371
|
contextId: (row.context_id as string) || undefined,
|
|
@@ -335,6 +380,7 @@ function taskFromRow(row: any): Task & { ownerEmail?: string | null } {
|
|
|
335
380
|
artifacts: JSON.parse(row.artifacts as string),
|
|
336
381
|
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
|
|
337
382
|
ownerEmail: (row.owner_email as string | null) ?? null,
|
|
383
|
+
ownerScope: (row.owner_scope as string | null) ?? null,
|
|
338
384
|
};
|
|
339
385
|
}
|
|
340
386
|
|
|
@@ -356,6 +402,7 @@ export async function createTask(
|
|
|
356
402
|
contextId?: string,
|
|
357
403
|
metadata?: Record<string, unknown>,
|
|
358
404
|
ownerEmail?: string | null,
|
|
405
|
+
ownerScope?: string | null,
|
|
359
406
|
): Promise<Task> {
|
|
360
407
|
await ensureTable();
|
|
361
408
|
const client = getDbExec();
|
|
@@ -373,7 +420,7 @@ export async function createTask(
|
|
|
373
420
|
};
|
|
374
421
|
|
|
375
422
|
await client.execute({
|
|
376
|
-
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
423
|
+
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, owner_scope, idempotency_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
377
424
|
args: [
|
|
378
425
|
id,
|
|
379
426
|
contextId ?? null,
|
|
@@ -383,6 +430,8 @@ export async function createTask(
|
|
|
383
430
|
"[]",
|
|
384
431
|
metadata ? JSON.stringify(metadata) : null,
|
|
385
432
|
ownerEmail ?? null,
|
|
433
|
+
ownerScope ?? "",
|
|
434
|
+
null,
|
|
386
435
|
now,
|
|
387
436
|
now,
|
|
388
437
|
],
|
|
@@ -391,6 +440,87 @@ export async function createTask(
|
|
|
391
440
|
return task;
|
|
392
441
|
}
|
|
393
442
|
|
|
443
|
+
export async function createOrReuseTask(
|
|
444
|
+
message: Message,
|
|
445
|
+
contextId: string | undefined,
|
|
446
|
+
metadata: Record<string, unknown> | undefined,
|
|
447
|
+
ownerEmail: string | null,
|
|
448
|
+
ownerScope: string | null,
|
|
449
|
+
idempotencyKey: string | undefined,
|
|
450
|
+
): Promise<{ task: Task; reused: boolean }> {
|
|
451
|
+
if (!ownerEmail || !idempotencyKey) {
|
|
452
|
+
return {
|
|
453
|
+
task: await createTask(
|
|
454
|
+
message,
|
|
455
|
+
contextId,
|
|
456
|
+
metadata,
|
|
457
|
+
ownerEmail,
|
|
458
|
+
ownerScope,
|
|
459
|
+
),
|
|
460
|
+
reused: false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (idempotencyKey.length > MAX_A2A_IDEMPOTENCY_KEY_CHARS) {
|
|
464
|
+
throw new Error("A2A idempotency key is too long");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await ensureTable();
|
|
468
|
+
const client = getDbExec();
|
|
469
|
+
const normalizedOwner = ownerEmail.trim().toLowerCase();
|
|
470
|
+
const normalizedScope =
|
|
471
|
+
ownerScope?.trim().toLowerCase() || A2A_PERSONAL_OWNER_SCOPE;
|
|
472
|
+
|
|
473
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
474
|
+
const id = crypto.randomUUID();
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
const timestamp = new Date().toISOString();
|
|
477
|
+
await client.execute({
|
|
478
|
+
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, owner_scope, idempotency_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(owner_email, owner_scope, idempotency_key) DO NOTHING`,
|
|
479
|
+
args: [
|
|
480
|
+
id,
|
|
481
|
+
contextId ?? null,
|
|
482
|
+
"submitted",
|
|
483
|
+
timestamp,
|
|
484
|
+
JSON.stringify([message]),
|
|
485
|
+
"[]",
|
|
486
|
+
metadata ? JSON.stringify(metadata) : null,
|
|
487
|
+
normalizedOwner,
|
|
488
|
+
normalizedScope,
|
|
489
|
+
idempotencyKey,
|
|
490
|
+
now,
|
|
491
|
+
now,
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const { rows } = await client.execute({
|
|
496
|
+
sql: `SELECT * FROM a2a_tasks WHERE owner_email = ? AND owner_scope = ? AND idempotency_key = ?`,
|
|
497
|
+
args: [normalizedOwner, normalizedScope, idempotencyKey],
|
|
498
|
+
});
|
|
499
|
+
if (rows.length === 0) {
|
|
500
|
+
throw new Error("A2A idempotent task insert could not be verified");
|
|
501
|
+
}
|
|
502
|
+
const task = taskFromRow(rows[0]);
|
|
503
|
+
const reused = task.id !== id;
|
|
504
|
+
if (
|
|
505
|
+
reused &&
|
|
506
|
+
(task.status.state === "failed" || task.status.state === "canceled")
|
|
507
|
+
) {
|
|
508
|
+
await client.execute({
|
|
509
|
+
sql: `UPDATE a2a_tasks SET idempotency_key = NULL, updated_at = ? WHERE id = ? AND idempotency_key = ? AND status_state IN ('failed', 'canceled')`,
|
|
510
|
+
args: [Date.now(), task.id, idempotencyKey],
|
|
511
|
+
});
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
return { task, reused };
|
|
515
|
+
}
|
|
516
|
+
throw new Error("A2A terminal task idempotency key could not be released");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export interface A2ATaskOwnership {
|
|
520
|
+
ownerEmail: string | null;
|
|
521
|
+
ownerScope: string | null;
|
|
522
|
+
}
|
|
523
|
+
|
|
394
524
|
/**
|
|
395
525
|
* Fetch the verified owner email recorded against a task at creation time.
|
|
396
526
|
* Returns null when the task has no owner (legacy rows or unauthenticated
|
|
@@ -400,15 +530,25 @@ export async function createTask(
|
|
|
400
530
|
* verified caller's email must match `owner_email` to read or cancel.
|
|
401
531
|
*/
|
|
402
532
|
export async function getTaskOwner(id: string): Promise<string | null> {
|
|
533
|
+
return (await getTaskOwnership(id)).ownerEmail;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function getTaskOwnership(id: string): Promise<A2ATaskOwnership> {
|
|
403
537
|
await ensureTable();
|
|
404
538
|
const client = getDbExec();
|
|
405
539
|
const { rows } = await client.execute({
|
|
406
|
-
sql: `SELECT owner_email FROM a2a_tasks WHERE id = ?`,
|
|
540
|
+
sql: `SELECT owner_email, owner_scope FROM a2a_tasks WHERE id = ?`,
|
|
407
541
|
args: [id],
|
|
408
542
|
});
|
|
409
|
-
if (rows.length === 0) return null;
|
|
543
|
+
if (rows.length === 0) return { ownerEmail: null, ownerScope: null };
|
|
410
544
|
const ownerEmail = (rows[0] as any).owner_email;
|
|
411
|
-
|
|
545
|
+
const ownerScope = (rows[0] as any).owner_scope;
|
|
546
|
+
return {
|
|
547
|
+
ownerEmail:
|
|
548
|
+
typeof ownerEmail === "string" && ownerEmail ? ownerEmail : null,
|
|
549
|
+
ownerScope:
|
|
550
|
+
typeof ownerScope === "string" && ownerScope ? ownerScope : null,
|
|
551
|
+
};
|
|
412
552
|
}
|
|
413
553
|
|
|
414
554
|
/**
|
|
@@ -132,6 +132,19 @@ export interface A2AApprovedAction {
|
|
|
132
132
|
input: unknown;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Telemetry-only cross-app correlation. Receivers must never use these
|
|
137
|
+
* caller-supplied values for identity, ownership, org scoping, access, or
|
|
138
|
+
* approval decisions.
|
|
139
|
+
*/
|
|
140
|
+
export interface A2ACorrelationMetadata {
|
|
141
|
+
callerApp?: string;
|
|
142
|
+
callerThreadId?: string;
|
|
143
|
+
parentRunId?: string;
|
|
144
|
+
parentTurnId?: string;
|
|
145
|
+
invocationId?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
// --- Framework config ---
|
|
136
149
|
|
|
137
150
|
export interface A2AHandlerContext {
|
|
@@ -168,6 +181,7 @@ export interface A2AApprovalExecution {
|
|
|
168
181
|
export interface A2AReadOnlyActionInvocation {
|
|
169
182
|
action: string;
|
|
170
183
|
input: Record<string, unknown>;
|
|
184
|
+
invocationId: string;
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
export interface A2AReadOnlyActionResult {
|
|
@@ -183,6 +197,8 @@ export type A2AHandler = (
|
|
|
183
197
|
|
|
184
198
|
export interface A2AConfig {
|
|
185
199
|
name: string;
|
|
200
|
+
/** Canonical receiver app id used only for telemetry attribution. */
|
|
201
|
+
appId?: string;
|
|
186
202
|
description: string;
|
|
187
203
|
version?: string;
|
|
188
204
|
skills: AgentSkill[];
|
|
@@ -2481,6 +2481,9 @@ export interface ExecuteAgentToolCallOptions {
|
|
|
2481
2481
|
orgId?: string | null;
|
|
2482
2482
|
/** Audit/action attribution for this externally selected call. */
|
|
2483
2483
|
caller?: ActionCaller;
|
|
2484
|
+
networkProtocol?: "a2a" | "mcp" | "provider-api";
|
|
2485
|
+
networkId?: string;
|
|
2486
|
+
networkPeer?: string;
|
|
2484
2487
|
threadId?: string;
|
|
2485
2488
|
turnId?: string;
|
|
2486
2489
|
approvedToolCalls?: string[];
|
|
@@ -2576,6 +2579,9 @@ export async function executeAgentToolCall(
|
|
|
2576
2579
|
ownerEmail: options.ownerEmail,
|
|
2577
2580
|
orgId: options.orgId,
|
|
2578
2581
|
actionCaller: options.caller,
|
|
2582
|
+
networkProtocol: options.networkProtocol,
|
|
2583
|
+
networkId: options.networkId,
|
|
2584
|
+
networkPeer: options.networkPeer,
|
|
2579
2585
|
executionMode: "act",
|
|
2580
2586
|
maxIterations: 2,
|
|
2581
2587
|
threadId: options.threadId,
|
|
@@ -3157,6 +3163,12 @@ export async function runAgentLoop(opts: {
|
|
|
3157
3163
|
orgId?: string | null;
|
|
3158
3164
|
/** Action invocation attribution. Defaults to the normal agent tool loop. */
|
|
3159
3165
|
actionCaller?: ActionCaller;
|
|
3166
|
+
/** Concrete execution id used for cross-app trace correlation. */
|
|
3167
|
+
runId?: string;
|
|
3168
|
+
/** Verified/telemetry-only delegated lineage supplied by the transport. */
|
|
3169
|
+
networkProtocol?: "a2a" | "mcp" | "provider-api";
|
|
3170
|
+
networkId?: string;
|
|
3171
|
+
networkPeer?: string;
|
|
3160
3172
|
/**
|
|
3161
3173
|
* Attachments submitted with this turn (pasted text, files, images), passed
|
|
3162
3174
|
* through to each tool's `ActionRunContext.attachments` so actions can
|
|
@@ -4215,6 +4227,9 @@ export async function runAgentLoop(opts: {
|
|
|
4215
4227
|
userEmail: getRequestUserEmail(),
|
|
4216
4228
|
orgId: getRequestOrgId() ?? null,
|
|
4217
4229
|
caller: opts.actionCaller ?? "tool",
|
|
4230
|
+
networkProtocol: opts.networkProtocol,
|
|
4231
|
+
networkId: opts.networkId,
|
|
4232
|
+
networkPeer: opts.networkPeer,
|
|
4218
4233
|
}),
|
|
4219
4234
|
)
|
|
4220
4235
|
: actionEntry.needsApproval === true;
|
|
@@ -4249,6 +4264,9 @@ export async function runAgentLoop(opts: {
|
|
|
4249
4264
|
ctx: {
|
|
4250
4265
|
actionName: toolCall.name,
|
|
4251
4266
|
caller: opts.actionCaller ?? "tool",
|
|
4267
|
+
networkProtocol: opts.networkProtocol,
|
|
4268
|
+
networkId: opts.networkId,
|
|
4269
|
+
networkPeer: opts.networkPeer,
|
|
4252
4270
|
userEmail: getRequestUserEmail(),
|
|
4253
4271
|
orgId: getRequestOrgId() ?? null,
|
|
4254
4272
|
...(opts.threadId ? { threadId: opts.threadId } : {}),
|
|
@@ -4645,12 +4663,16 @@ export async function runAgentLoop(opts: {
|
|
|
4645
4663
|
userEmail: actionUserEmail ?? undefined,
|
|
4646
4664
|
orgId: actionOrgId,
|
|
4647
4665
|
caller: opts.actionCaller ?? "tool",
|
|
4666
|
+
networkProtocol: opts.networkProtocol,
|
|
4667
|
+
networkId: opts.networkId,
|
|
4668
|
+
networkPeer: opts.networkPeer,
|
|
4648
4669
|
attachments: opts.attachments,
|
|
4649
4670
|
signal,
|
|
4650
4671
|
// Audit attribution: the action name + the agent thread/turn that
|
|
4651
4672
|
// triggered this call, so a mutation can be traced to its run.
|
|
4652
4673
|
actionName: toolCall.name,
|
|
4653
4674
|
...(opts.threadId ? { threadId: opts.threadId } : {}),
|
|
4675
|
+
...(opts.runId ? { runId: opts.runId } : {}),
|
|
4654
4676
|
...(opts.turnId ? { turnId: opts.turnId } : {}),
|
|
4655
4677
|
};
|
|
4656
4678
|
const requestContext = getRequestContext();
|
|
@@ -8004,6 +8026,7 @@ export function createProductionAgentHandler(
|
|
|
8004
8026
|
const agentLoopOpts = {
|
|
8005
8027
|
engine,
|
|
8006
8028
|
model: effectiveModel,
|
|
8029
|
+
runId,
|
|
8007
8030
|
systemPrompt: requestSystemPrompt,
|
|
8008
8031
|
tools: requestTools,
|
|
8009
8032
|
availableTools: availableRequestTools,
|
|
@@ -80,6 +80,7 @@ export const DEFAULT_BACKGROUND_STUCK_THRESHOLD_MS = 180_000;
|
|
|
80
80
|
export const DEFAULT_LIVE_BACKGROUND_STUCK_THRESHOLD_MS = 13 * 60_000;
|
|
81
81
|
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
82
82
|
const IDLE_BACKOFF_INTERVAL_MS = 15_000;
|
|
83
|
+
const MAX_POLL_ERROR_BACKOFF_MS = 30_000;
|
|
83
84
|
const FRESH_BACKGROUND_HEARTBEAT_MS = 30_000;
|
|
84
85
|
|
|
85
86
|
interface ActiveRunResponse {
|
|
@@ -129,6 +130,22 @@ export function useRunStuckDetection({
|
|
|
129
130
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
130
131
|
let snapshotTransitionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
131
132
|
let snapshotVersion = 0;
|
|
133
|
+
let consecutivePollFailures = 0;
|
|
134
|
+
|
|
135
|
+
const pollFailureDelay = () => {
|
|
136
|
+
consecutivePollFailures += 1;
|
|
137
|
+
return Math.min(
|
|
138
|
+
Math.max(pollIntervalMs, 1) * 2 ** consecutivePollFailures,
|
|
139
|
+
MAX_POLL_ERROR_BACKOFF_MS,
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const clearObservedRun = () => {
|
|
144
|
+
snapshotVersion += 1;
|
|
145
|
+
if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
|
|
146
|
+
snapshotTransitionTimer = null;
|
|
147
|
+
setState(EMPTY_STATE);
|
|
148
|
+
};
|
|
132
149
|
|
|
133
150
|
type RunHealthSnapshot = {
|
|
134
151
|
active: boolean;
|
|
@@ -269,6 +286,7 @@ export function useRunStuckDetection({
|
|
|
269
286
|
);
|
|
270
287
|
if (cancelled) return;
|
|
271
288
|
if (res.ok) {
|
|
289
|
+
consecutivePollFailures = 0;
|
|
272
290
|
const data = (await res.json()) as ActiveRunResponse;
|
|
273
291
|
const lastProgressAt = data.lastProgressAt ?? null;
|
|
274
292
|
// Measure elapsed against the SERVER clock (serverNow) rather than the
|
|
@@ -336,9 +354,17 @@ export function useRunStuckDetection({
|
|
|
336
354
|
if (!data.active || data.status !== "running") {
|
|
337
355
|
nextDelay = IDLE_BACKOFF_INTERVAL_MS;
|
|
338
356
|
}
|
|
357
|
+
} else if (res.status === 429 || res.status >= 500) {
|
|
358
|
+
nextDelay = pollFailureDelay();
|
|
359
|
+
} else {
|
|
360
|
+
consecutivePollFailures = 0;
|
|
361
|
+
clearObservedRun();
|
|
362
|
+
nextDelay = MAX_POLL_ERROR_BACKOFF_MS;
|
|
339
363
|
}
|
|
340
364
|
} catch {
|
|
341
|
-
// Network blip — leave previous state
|
|
365
|
+
// Network/503 blip — leave previous state and ease polling pressure
|
|
366
|
+
// while the server or database recovers.
|
|
367
|
+
nextDelay = pollFailureDelay();
|
|
342
368
|
}
|
|
343
369
|
if (!cancelled) {
|
|
344
370
|
timer = setTimeout(poll, nextDelay);
|
|
@@ -669,6 +669,61 @@ export function isConnectionError(err: any): boolean {
|
|
|
669
669
|
);
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Classify database failures that should temporarily shed request load.
|
|
674
|
+
* Statement timeouts are not included in isConnectionError() because retrying
|
|
675
|
+
* every timed-out mutation would not be safe, but request handlers can still
|
|
676
|
+
* return a retryable service-unavailable response for them.
|
|
677
|
+
*/
|
|
678
|
+
export function isTransientDatabaseError(err: unknown): boolean {
|
|
679
|
+
const error = err as {
|
|
680
|
+
code?: unknown;
|
|
681
|
+
name?: unknown;
|
|
682
|
+
message?: unknown;
|
|
683
|
+
stack?: unknown;
|
|
684
|
+
cause?: {
|
|
685
|
+
code?: unknown;
|
|
686
|
+
name?: unknown;
|
|
687
|
+
message?: unknown;
|
|
688
|
+
stack?: unknown;
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
const code = String(error?.code ?? error?.cause?.code ?? "");
|
|
692
|
+
if (
|
|
693
|
+
code === "ECHECKOUTTIMEOUT" ||
|
|
694
|
+
code === "53300" ||
|
|
695
|
+
code === "57014" ||
|
|
696
|
+
/^08/.test(code) ||
|
|
697
|
+
/^57P0[123]$/.test(code)
|
|
698
|
+
) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const message = [error?.message, error?.cause?.message]
|
|
703
|
+
.filter((value): value is string => typeof value === "string")
|
|
704
|
+
.join(" ");
|
|
705
|
+
if (
|
|
706
|
+
/\bstatement timeout\b|\bdb (?:query|connect) timed out\b/i.test(message)
|
|
707
|
+
) {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const databaseSurface = [
|
|
712
|
+
error?.name,
|
|
713
|
+
error?.cause?.name,
|
|
714
|
+
error?.stack,
|
|
715
|
+
error?.cause?.stack,
|
|
716
|
+
]
|
|
717
|
+
.filter((value): value is string => typeof value === "string")
|
|
718
|
+
.join(" ");
|
|
719
|
+
return (
|
|
720
|
+
isConnectionError(error) &&
|
|
721
|
+
/@neondatabase|@libsql|\bpostgres(?:ql)?\b|\bpg-pool\b|drizzle-orm|\/db\/client\.[cm]?[jt]s/i.test(
|
|
722
|
+
databaseSurface,
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
672
727
|
export async function retryOnConnectionError<T>(
|
|
673
728
|
fn: () => Promise<T>,
|
|
674
729
|
maxAttempts = 3,
|