@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.
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
- package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
- package/corpus/core/src/integrations/adapters/slack.ts +77 -7
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +54 -18
- package/corpus/core/src/scripts/call-agent.ts +1 -0
- package/corpus/core/src/server/request-context.ts +2 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +127 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +76 -21
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +4 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +130 -6
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +56 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +54 -16
- package/dist/integrations/webhook-handler.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 +1 -0
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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)
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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;
|
|
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"}
|