@agent-native/core 0.94.2 → 0.94.3

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 (38) hide show
  1. package/corpus/core/CHANGELOG.md +8 -0
  2. package/corpus/core/package.json +1 -1
  3. package/corpus/core/src/a2a/artifact-response.ts +154 -1
  4. package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
  5. package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
  6. package/corpus/core/src/integrations/adapters/slack.ts +77 -7
  7. package/corpus/core/src/integrations/types.ts +30 -0
  8. package/corpus/core/src/integrations/webhook-handler.ts +54 -18
  9. package/corpus/core/src/scripts/call-agent.ts +1 -0
  10. package/corpus/core/src/server/request-context.ts +2 -0
  11. package/dist/a2a/artifact-response.d.ts.map +1 -1
  12. package/dist/a2a/artifact-response.js +127 -1
  13. package/dist/a2a/artifact-response.js.map +1 -1
  14. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  15. package/dist/integrations/a2a-continuation-processor.js +76 -21
  16. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  17. package/dist/integrations/a2a-continuations-store.d.ts +4 -1
  18. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  19. package/dist/integrations/a2a-continuations-store.js +130 -6
  20. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  21. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  22. package/dist/integrations/adapters/slack.js +56 -7
  23. package/dist/integrations/adapters/slack.js.map +1 -1
  24. package/dist/integrations/types.d.ts +25 -0
  25. package/dist/integrations/types.d.ts.map +1 -1
  26. package/dist/integrations/types.js.map +1 -1
  27. package/dist/integrations/webhook-handler.js +54 -16
  28. package/dist/integrations/webhook-handler.js.map +1 -1
  29. package/dist/resources/handlers.d.ts +2 -2
  30. package/dist/scripts/call-agent.d.ts.map +1 -1
  31. package/dist/scripts/call-agent.js +1 -0
  32. package/dist/scripts/call-agent.js.map +1 -1
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/dist/server/request-context.d.ts +2 -0
  35. package/dist/server/request-context.d.ts.map +1 -1
  36. package/dist/server/request-context.js.map +1 -1
  37. package/dist/server/transcribe-voice.d.ts +1 -1
  38. package/package.json +1 -1
@@ -10,7 +10,7 @@ import {
10
10
  ensureIndexExists,
11
11
  } from "../db/ddl-guard.js";
12
12
  import { isDuplicateColumnError } from "../db/migrations.js";
13
- import type { IncomingMessage } from "./types.js";
13
+ import type { IncomingMessage, PlatformRunProgressRef } from "./types.js";
14
14
 
15
15
  let _initPromise: Promise<void> | undefined;
16
16
  const PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
@@ -28,6 +28,8 @@ function buildCreateSql(): string {
28
28
  external_thread_id TEXT NOT NULL,
29
29
  incoming_payload TEXT NOT NULL,
30
30
  placeholder_ref TEXT,
31
+ progress_ref TEXT,
32
+ progress_ref_claimed ${intType()} NOT NULL DEFAULT 0,
31
33
  owner_email TEXT NOT NULL,
32
34
  org_id TEXT,
33
35
  agent_name TEXT NOT NULL,
@@ -76,10 +78,25 @@ async function ensureTable(): Promise<void> {
76
78
  "dedupe_key",
77
79
  `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS dedupe_key TEXT`,
78
80
  );
81
+ await ensureColumnExists(
82
+ "integration_a2a_continuations",
83
+ "progress_ref",
84
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref TEXT`,
85
+ );
86
+ await ensureColumnExists(
87
+ "integration_a2a_continuations",
88
+ "progress_ref_claimed",
89
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref_claimed ${intType()} NOT NULL DEFAULT 0`,
90
+ );
91
+ await backfillProgressRefOwners(client);
79
92
  await ensureIndexExists(
80
93
  "idx_a2a_continuations_dedupe_key",
81
94
  `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`,
82
95
  );
96
+ await ensureIndexExists(
97
+ "idx_a2a_continuations_one_progress_owner",
98
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`,
99
+ );
83
100
  return;
84
101
  }
85
102
  // SQLite (local dev): keep existing behavior
@@ -101,11 +118,22 @@ async function ensureTable(): Promise<void> {
101
118
  );
102
119
  await addColumnIfMissing("a2a_auth_token", "TEXT");
103
120
  await addColumnIfMissing("dedupe_key", "TEXT");
121
+ await addColumnIfMissing("progress_ref", "TEXT");
122
+ await addColumnIfMissing(
123
+ "progress_ref_claimed",
124
+ `${intType()} NOT NULL DEFAULT 0`,
125
+ );
126
+ await backfillProgressRefOwners(client);
104
127
  await retryOnDdlRace(() =>
105
128
  client.execute(
106
129
  `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`,
107
130
  ),
108
131
  );
132
+ await retryOnDdlRace(() =>
133
+ client.execute(
134
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`,
135
+ ),
136
+ );
109
137
  })().catch((err) => {
110
138
  // Retry init on the next call after a failed startup.
111
139
  _initPromise = undefined;
@@ -128,6 +156,33 @@ async function addColumnIfMissing(name: string, definition: string) {
128
156
  }
129
157
  }
130
158
 
159
+ async function backfillProgressRefOwners(
160
+ client: ReturnType<typeof getDbExec>,
161
+ ): Promise<void> {
162
+ await client.execute(`
163
+ UPDATE integration_a2a_continuations AS candidate
164
+ SET progress_ref_claimed = 1
165
+ WHERE candidate.progress_ref IS NOT NULL
166
+ AND candidate.status NOT IN ('completed', 'failed')
167
+ AND candidate.progress_ref_claimed = 0
168
+ AND NOT EXISTS (
169
+ SELECT 1
170
+ FROM integration_a2a_continuations AS owner
171
+ WHERE owner.integration_task_id = candidate.integration_task_id
172
+ AND owner.progress_ref_claimed = 1
173
+ )
174
+ AND candidate.id = (
175
+ SELECT selected.id
176
+ FROM integration_a2a_continuations AS selected
177
+ WHERE selected.integration_task_id = candidate.integration_task_id
178
+ AND selected.progress_ref IS NOT NULL
179
+ AND selected.status NOT IN ('completed', 'failed')
180
+ ORDER BY selected.created_at ASC, selected.id ASC
181
+ LIMIT 1
182
+ )
183
+ `);
184
+ }
185
+
131
186
  export type A2AContinuationStatus =
132
187
  | "pending"
133
188
  | "processing"
@@ -142,6 +197,8 @@ export interface A2AContinuation {
142
197
  externalThreadId: string;
143
198
  incoming: IncomingMessage;
144
199
  placeholderRef: string | null;
200
+ progressRef: PlatformRunProgressRef | null;
201
+ progressRefClaimed: boolean;
145
202
  ownerEmail: string;
146
203
  orgId: string | null;
147
204
  agentName: string;
@@ -158,6 +215,44 @@ export interface A2AContinuation {
158
215
  completedAt: number | null;
159
216
  }
160
217
 
218
+ const MAX_PROGRESS_REF_KIND_CHARS = 128;
219
+ const MAX_PROGRESS_REF_STREAM_TS_CHARS = 256;
220
+
221
+ /**
222
+ * Keep only the tiny, adapter-owned continuation reference. Invalid rows are
223
+ * treated as unavailable rather than throwing during a retry sweep.
224
+ */
225
+ function parseProgressRef(value: unknown): PlatformRunProgressRef | null {
226
+ if (typeof value !== "string" || value.length === 0) return null;
227
+ try {
228
+ const parsed = JSON.parse(value) as unknown;
229
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
230
+ return null;
231
+ }
232
+ const { kind, streamTs } = parsed as Record<string, unknown>;
233
+ if (
234
+ typeof kind !== "string" ||
235
+ typeof streamTs !== "string" ||
236
+ kind.length === 0 ||
237
+ streamTs.length === 0 ||
238
+ kind.length > MAX_PROGRESS_REF_KIND_CHARS ||
239
+ streamTs.length > MAX_PROGRESS_REF_STREAM_TS_CHARS
240
+ ) {
241
+ return null;
242
+ }
243
+ return { kind, streamTs };
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function serializeProgressRef(value: unknown): string | null {
250
+ const parsed = parseProgressRef(
251
+ typeof value === "string" ? value : JSON.stringify(value),
252
+ );
253
+ return parsed ? JSON.stringify(parsed) : null;
254
+ }
255
+
161
256
  function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
162
257
  return {
163
258
  id: row.id as string,
@@ -166,6 +261,8 @@ function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
166
261
  externalThreadId: row.external_thread_id as string,
167
262
  incoming: JSON.parse(row.incoming_payload as string) as IncomingMessage,
168
263
  placeholderRef: (row.placeholder_ref as string | null) ?? null,
264
+ progressRef: parseProgressRef(row.progress_ref),
265
+ progressRefClaimed: Number(row.progress_ref_claimed ?? 0) === 1,
169
266
  ownerEmail: row.owner_email as string,
170
267
  orgId: (row.org_id as string | null) ?? null,
171
268
  agentName: row.agent_name as string,
@@ -190,6 +287,7 @@ export async function insertA2AContinuation(input: {
190
287
  externalThreadId: string;
191
288
  incoming: IncomingMessage;
192
289
  placeholderRef?: string | null;
290
+ progressRef?: PlatformRunProgressRef | null;
193
291
  ownerEmail: string;
194
292
  orgId?: string | null;
195
293
  agentName: string;
@@ -203,14 +301,15 @@ export async function insertA2AContinuation(input: {
203
301
  const now = Date.now();
204
302
  const id = `a2a-cont-${now}-${Math.random().toString(36).slice(2, 8)}`;
205
303
  const payload = JSON.stringify(input.incoming);
304
+ const progressRef = serializeProgressRef(input.progressRef);
206
305
 
207
306
  try {
208
307
  await client.execute({
209
308
  sql: `INSERT INTO integration_a2a_continuations
210
309
  (id, integration_task_id, platform, external_thread_id, incoming_payload,
211
- placeholder_ref, owner_email, org_id, agent_name, agent_url, dedupe_key, a2a_task_id, a2a_auth_token,
310
+ placeholder_ref, progress_ref, progress_ref_claimed, owner_email, org_id, agent_name, agent_url, dedupe_key, a2a_task_id, a2a_auth_token,
212
311
  status, attempts, next_check_at, created_at, updated_at)
213
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
214
313
  args: [
215
314
  id,
216
315
  input.integrationTaskId,
@@ -218,6 +317,8 @@ export async function insertA2AContinuation(input: {
218
317
  input.externalThreadId,
219
318
  payload,
220
319
  input.placeholderRef ?? null,
320
+ null,
321
+ 0,
221
322
  input.ownerEmail,
222
323
  input.orgId ?? null,
223
324
  input.agentName,
@@ -232,7 +333,6 @@ export async function insertA2AContinuation(input: {
232
333
  now,
233
334
  ],
234
335
  });
235
- return (await getA2AContinuation(id))!;
236
336
  } catch (err: any) {
237
337
  if (!isDuplicateContinuationError(err)) throw err;
238
338
  const existing = await findA2AContinuation(
@@ -240,7 +340,65 @@ export async function insertA2AContinuation(input: {
240
340
  input.agentUrl,
241
341
  input.a2aTaskId,
242
342
  );
243
- if (existing) return existing;
343
+ if (existing) {
344
+ // A retry can reach this row after the original invocation created it
345
+ // without a resumable progress surface (or with one that has gone
346
+ // stale). Keep the most recent valid adapter reference for active work,
347
+ // but never resurrect short-lived delivery state after a terminal row
348
+ // has deliberately scrubbed it.
349
+ if (
350
+ progressRef &&
351
+ existing.status !== "completed" &&
352
+ existing.status !== "failed"
353
+ ) {
354
+ if (existing.progressRefClaimed) {
355
+ if (JSON.stringify(existing.progressRef) !== progressRef) {
356
+ await client.execute({
357
+ sql: `UPDATE integration_a2a_continuations
358
+ SET progress_ref = ?, updated_at = ?
359
+ WHERE id = ? AND status NOT IN ('completed', 'failed')
360
+ AND progress_ref_claimed = 1
361
+ AND (progress_ref IS NULL OR progress_ref <> ?)`,
362
+ args: [progressRef, now, existing.id, progressRef],
363
+ });
364
+ }
365
+ } else {
366
+ await claimA2AContinuationProgressRef(existing.id, progressRef);
367
+ }
368
+ return (await getA2AContinuation(existing.id)) ?? existing;
369
+ }
370
+ return existing;
371
+ }
372
+ throw err;
373
+ }
374
+
375
+ if (progressRef) {
376
+ await claimA2AContinuationProgressRef(id, progressRef);
377
+ }
378
+ return (await getA2AContinuation(id))!;
379
+ }
380
+
381
+ /**
382
+ * A native platform stream has one terminal completion. Claim it for a single
383
+ * downstream continuation, and retain the ownership marker after terminal
384
+ * cleanup scrubs the short-lived stream reference. The partial unique index
385
+ * makes concurrent downstream inserts safe across processes.
386
+ */
387
+ async function claimA2AContinuationProgressRef(
388
+ id: string,
389
+ progressRef: string,
390
+ ): Promise<void> {
391
+ try {
392
+ await getDbExec().execute({
393
+ sql: `UPDATE integration_a2a_continuations
394
+ SET progress_ref = ?, progress_ref_claimed = 1
395
+ WHERE id = ? AND progress_ref_claimed = 0`,
396
+ args: [progressRef, id],
397
+ });
398
+ } catch (err) {
399
+ // A sibling continuation already owns this stream and will finalize it.
400
+ // This continuation still delivers through the normal response path.
401
+ if (isDuplicateContinuationError(err)) return;
244
402
  throw err;
245
403
  }
246
404
  }
@@ -462,7 +620,7 @@ export async function completeA2AContinuation(id: string): Promise<void> {
462
620
  await client.execute({
463
621
  sql: `UPDATE integration_a2a_continuations
464
622
  SET status = ?, updated_at = ?, completed_at = ?,
465
- incoming_payload = ?, a2a_auth_token = NULL
623
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
466
624
  WHERE id = ? AND status IN ('processing', 'delivering', 'completed')`,
467
625
  args: ["completed", now, now, "{}", id],
468
626
  });
@@ -478,7 +636,7 @@ export async function failA2AContinuation(
478
636
  await client.execute({
479
637
  sql: `UPDATE integration_a2a_continuations
480
638
  SET status = ?, updated_at = ?, error_message = ?,
481
- incoming_payload = ?, a2a_auth_token = NULL
639
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
482
640
  WHERE id = ? AND status <> 'completed'`,
483
641
  args: ["failed", now, errorMessage.slice(0, 2000), "{}", id],
484
642
  });
@@ -22,6 +22,7 @@ import type {
22
22
  IntegrationStatus,
23
23
  OutboundTarget,
24
24
  PlatformRunProgress,
25
+ PlatformRunProgressRef,
25
26
  IntegrationContextMessage,
26
27
  IntegrationFileReference,
27
28
  } from "../types.js";
@@ -359,6 +360,16 @@ export function slackAdapter(
359
360
  return startSlackRunProgress(token, incoming);
360
361
  },
361
362
 
363
+ async resumeRunProgress(
364
+ incoming: IncomingMessage,
365
+ ref: PlatformRunProgressRef,
366
+ ): Promise<PlatformRunProgress | null> {
367
+ if (!isSlackStreamProgressRef(ref)) return null;
368
+ const token = await resolveBotToken(incoming);
369
+ if (!token) return null;
370
+ return resumeSlackRunProgress(token, incoming, ref.streamTs);
371
+ },
372
+
362
373
  async sendResponse(
363
374
  message: OutgoingMessage,
364
375
  context: IncomingMessage,
@@ -1273,6 +1284,21 @@ async function postSlackJson(
1273
1284
  return data;
1274
1285
  }
1275
1286
 
1287
+ function streamFailureCode(error: unknown): string {
1288
+ const message = error instanceof Error ? error.message.trim() : "";
1289
+ return /^[a-z0-9_:-]{1,80}$/i.test(message) ? message : "unknown";
1290
+ }
1291
+
1292
+ function streamChunkType(chunk: Record<string, unknown>): string {
1293
+ const type = chunk.type;
1294
+ return type === "task_update" ||
1295
+ type === "plan_update" ||
1296
+ type === "markdown_text" ||
1297
+ type === "blocks"
1298
+ ? type
1299
+ : "unknown";
1300
+ }
1301
+
1276
1302
  async function startSlackRunProgress(
1277
1303
  token: string,
1278
1304
  incoming: IncomingMessage,
@@ -1304,12 +1330,45 @@ async function startSlackRunProgress(
1304
1330
  },
1305
1331
  ],
1306
1332
  });
1307
- } catch {
1333
+ } catch (error) {
1334
+ console.warn("[slack] chat.startStream failed; using standard reply", {
1335
+ errorCode: streamFailureCode(error),
1336
+ isDirectMessage: incoming.conversationType === "dm",
1337
+ hasRecipientTeam: Boolean(incoming.tenantId),
1338
+ hasRecipientUser: Boolean(incoming.senderId),
1339
+ });
1308
1340
  return null;
1309
1341
  }
1310
1342
 
1311
1343
  const streamTs = started.ts;
1312
1344
  if (typeof streamTs !== "string") return null;
1345
+ return createSlackRunProgress(token, incoming, channel, threadTs, streamTs);
1346
+ }
1347
+
1348
+ function isSlackStreamProgressRef(ref: PlatformRunProgressRef): boolean {
1349
+ return (
1350
+ ref.kind === "slack-stream" && /^\d{1,20}\.\d{1,9}$/.test(ref.streamTs)
1351
+ );
1352
+ }
1353
+
1354
+ async function resumeSlackRunProgress(
1355
+ token: string,
1356
+ incoming: IncomingMessage,
1357
+ streamTs: string,
1358
+ ): Promise<PlatformRunProgress | null> {
1359
+ const channel = incoming.platformContext.channelId;
1360
+ const threadTs = incoming.platformContext.threadTs;
1361
+ if (typeof channel !== "string" || typeof threadTs !== "string") return null;
1362
+ return createSlackRunProgress(token, incoming, channel, threadTs, streamTs);
1363
+ }
1364
+
1365
+ function createSlackRunProgress(
1366
+ token: string,
1367
+ incoming: IncomingMessage,
1368
+ channel: string,
1369
+ threadTs: string,
1370
+ streamTs: string,
1371
+ ): PlatformRunProgress {
1313
1372
  const tasks = new Map<string, { title: string; status: string }>();
1314
1373
  const toolTaskIds = new Map<string, string>();
1315
1374
  const agentTaskIds = new Map<string, string>();
@@ -1328,12 +1387,22 @@ async function startSlackRunProgress(
1328
1387
  const now = Date.now();
1329
1388
  const write = async (value: Record<string, unknown>) => {
1330
1389
  lastWriteAt = Date.now();
1331
- await postSlackJson(token, "chat.appendStream", {
1332
- channel,
1333
- ts: streamTs,
1334
- markdown_text: "Progress updated.",
1335
- chunks: [value],
1336
- }).catch(() => {});
1390
+ try {
1391
+ await postSlackJson(token, "chat.appendStream", {
1392
+ channel,
1393
+ ts: streamTs,
1394
+ markdown_text: "Progress updated.",
1395
+ chunks: [value],
1396
+ });
1397
+ } catch (error) {
1398
+ console.warn(
1399
+ "[slack] chat.appendStream failed; progress may be stale",
1400
+ {
1401
+ chunkType: streamChunkType(value),
1402
+ errorCode: streamFailureCode(error),
1403
+ },
1404
+ );
1405
+ }
1337
1406
  };
1338
1407
  if (now - lastWriteAt >= 900) {
1339
1408
  await write(chunk);
@@ -1357,6 +1426,7 @@ async function startSlackRunProgress(
1357
1426
  `${prefix}:${explicit || ++sequence}`.slice(0, 240);
1358
1427
 
1359
1428
  return {
1429
+ ref: { kind: "slack-stream", streamTs },
1360
1430
  async onEvent(event) {
1361
1431
  if (!cancelControl) {
1362
1432
  const context = getIntegrationRequestContext();
@@ -181,6 +181,12 @@ export interface PlatformAdapterCapabilities {
181
181
  }
182
182
 
183
183
  export interface PlatformRunProgress {
184
+ /**
185
+ * Opaque, provider-owned reference for resuming this progress surface from a
186
+ * durable continuation. It deliberately contains no user content,
187
+ * credentials, or provider payload.
188
+ */
189
+ ref?: PlatformRunProgressRef;
184
190
  /** Receive normalized agent events. Implementations should throttle writes. */
185
191
  onEvent(event: AgentChatEvent): Promise<void> | void;
186
192
  /** Finalize the provider-native progress surface with the answer. */
@@ -189,6 +195,19 @@ export interface PlatformRunProgress {
189
195
  fail?(message: string): Promise<void>;
190
196
  }
191
197
 
198
+ /**
199
+ * Safe, minimal reference to a provider-native run-progress surface.
200
+ *
201
+ * The field values are opaque to the framework. Adapters may use `kind` to
202
+ * distinguish their own resume strategy and `streamTs` to identify the
203
+ * provider-side stream. No incoming message text, platform payload, or
204
+ * credential belongs here.
205
+ */
206
+ export interface PlatformRunProgressRef {
207
+ kind: string;
208
+ streamTs: string;
209
+ }
210
+
192
211
  export interface ImmediateWebhookResponse {
193
212
  status: number;
194
213
  body: unknown;
@@ -304,6 +323,17 @@ export interface PlatformAdapter {
304
323
  incoming: IncomingMessage,
305
324
  ): Promise<PlatformRunProgress | null>;
306
325
 
326
+ /**
327
+ * Reattach a durable continuation to a provider-native progress surface
328
+ * previously started by this adapter. Adapters that cannot resume a native
329
+ * surface should omit this and the continuation will use its normal reply
330
+ * path instead.
331
+ */
332
+ resumeRunProgress?(
333
+ incoming: IncomingMessage,
334
+ ref: PlatformRunProgressRef,
335
+ ): Promise<PlatformRunProgress | null>;
336
+
307
337
  /**
308
338
  * Send a proactive outbound message to a platform destination. Adapters that
309
339
  * only support direct replies can omit this.
@@ -796,6 +796,7 @@ async function processIncomingMessage(
796
796
  attempts: opts.attempts,
797
797
  incoming,
798
798
  placeholderRef: opts.placeholderRef,
799
+ progressRef: progress?.ref,
799
800
  scopeId: incoming.integrationScopeId,
800
801
  principalType: opts.principalType ?? "user",
801
802
  lineage: {
@@ -872,8 +873,9 @@ async function processIncomingMessage(
872
873
  },
873
874
  async (completedRun: ActiveRun) => {
874
875
  let keepSlackInputWindow = false;
876
+ let queuedA2AContinuation = false;
875
877
  try {
876
- const queuedA2AContinuation = hasQueuedA2AContinuation(completedRun);
878
+ queuedA2AContinuation = hasQueuedA2AContinuation(completedRun);
877
879
  const slackInputRequest =
878
880
  incoming.platform === "slack"
879
881
  ? extractSlackInputRequest(completedRun)
@@ -963,7 +965,15 @@ async function processIncomingMessage(
963
965
  threadDeepLinkUrl,
964
966
  });
965
967
  let delivered = false;
966
- if (progress) {
968
+ if (queuedA2AContinuation && progress?.ref) {
969
+ // Post substantive parent results as a normal thread reply while
970
+ // the one continuation that claimed this resumable stream keeps
971
+ // it open for its eventual terminal result.
972
+ await adapter.sendResponse(outgoing, incoming, {
973
+ placeholderRef: opts.placeholderRef,
974
+ });
975
+ delivered = true;
976
+ } else if (progress) {
967
977
  try {
968
978
  await progress.complete(outgoing);
969
979
  delivered = true;
@@ -988,24 +998,35 @@ async function processIncomingMessage(
988
998
  keepSlackInputWindow = true;
989
999
  }
990
1000
  } else if (progress) {
991
- // The downstream agent owns the eventual reply, but this parent
992
- // integration run owns the native progress stream it opened. End
993
- // that stream now so Slack does not leave an eternal task card;
994
- // the continuation processor will post the final result into the
995
- // same thread when the downstream task completes.
996
- const deferred = adapter.formatAgentResponse(
997
- "The delegated agent is still working. I’ll post its final result in this thread automatically.",
998
- );
999
- try {
1000
- await progress.complete(deferred);
1001
- } catch {
1002
- // A failed complete must still terminate a provider-native
1003
- // stream when the adapter offers a failure lifecycle. Do not
1004
- // duplicate the deferred text as a regular reply: a later
1005
- // continuation delivery is authoritative.
1006
- await progress.fail?.(
1001
+ // A continuation owns the eventual final response. If the adapter
1002
+ // supplied a durable progress reference, leave the same native
1003
+ // stream open for the continuation processor to update and close;
1004
+ // ending it here discards the plan/task UI before the delegated
1005
+ // work has actually finished.
1006
+ if (progress.ref) {
1007
+ await progress.onEvent({
1008
+ type: "agent_call_progress",
1009
+ agent:
1010
+ getQueuedA2AContinuationAgent(completedRun) ??
1011
+ "delegated agent",
1012
+ state: "working",
1013
+ elapsedSeconds: 0,
1014
+ detail: "Continuing in the background",
1015
+ });
1016
+ } else {
1017
+ // Older adapters have no resumable native surface. Close their
1018
+ // stream cleanly; the continuation will deliver one standard
1019
+ // final reply when the downstream task is terminal.
1020
+ const deferred = adapter.formatAgentResponse(
1007
1021
  "The delegated agent is still working. I’ll post its final result in this thread automatically.",
1008
1022
  );
1023
+ try {
1024
+ await progress.complete(deferred);
1025
+ } catch {
1026
+ await progress.fail?.(
1027
+ "The delegated agent is still working. I’ll post its final result in this thread automatically.",
1028
+ );
1029
+ }
1009
1030
  }
1010
1031
  }
1011
1032
 
@@ -1036,6 +1057,10 @@ async function processIncomingMessage(
1036
1057
  `[integrations] Error sending response to ${incoming.platform}:`,
1037
1058
  err,
1038
1059
  );
1060
+ // A queued continuation owns the final platform response. Later
1061
+ // bookkeeping failures (for example, persisting this parent run)
1062
+ // must not close its resumable native stream with a false failure.
1063
+ if (queuedA2AContinuation) return;
1039
1064
  // Last-ditch: try to post a brief apology so the thread isn't silent.
1040
1065
  try {
1041
1066
  await progress?.fail?.(
@@ -1322,6 +1347,17 @@ function hasQueuedA2AContinuation(completedRun: ActiveRun): boolean {
1322
1347
  });
1323
1348
  }
1324
1349
 
1350
+ function getQueuedA2AContinuationAgent(completedRun: ActiveRun): string | null {
1351
+ for (let i = completedRun.events.length - 1; i >= 0; i--) {
1352
+ const event = completedRun.events[i]!.event;
1353
+ if (event.type !== "agent_call") continue;
1354
+ if (typeof event.agent === "string" && event.agent.trim()) {
1355
+ return event.agent;
1356
+ }
1357
+ }
1358
+ return null;
1359
+ }
1360
+
1325
1361
  function extractSlackInputRequest(
1326
1362
  completedRun: ActiveRun,
1327
1363
  ): { text: string } | null {
@@ -427,6 +427,7 @@ async function enqueueIntegrationContinuationIfPossible(
427
427
  externalThreadId: integration.incoming.externalThreadId,
428
428
  incoming: integration.incoming,
429
429
  placeholderRef: integration.placeholderRef,
430
+ progressRef: integration.progressRef,
430
431
  ownerEmail,
431
432
  orgId: getRequestOrgId() ?? null,
432
433
  agentName: agent.name,
@@ -164,6 +164,8 @@ export interface RequestContext {
164
164
  attempts?: number;
165
165
  incoming: import("../integrations/types.js").IncomingMessage;
166
166
  placeholderRef?: string;
167
+ /** Opaque provider-native progress surface for a durable continuation. */
168
+ progressRef?: import("../integrations/types.js").PlatformRunProgressRef;
167
169
  installationId?: string;
168
170
  scopeId?: string;
169
171
  principalType?: "user" | "service";
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAo8BD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAsIR;AAED,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CA8Bf"}
1
+ {"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AA6lCD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAsIR;AAED,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CA8Bf"}