@agent-native/core 0.94.2 → 0.95.0
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 +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +20 -12
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +3 -0
- package/corpus/core/src/agent/engine/registry.ts +6 -1
- package/corpus/core/src/agent/engine/types.ts +2 -0
- package/corpus/core/src/cli/code-agent-executor.ts +61 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +262 -25
- package/corpus/core/src/client/settings/UsageSection.tsx +26 -5
- package/corpus/core/src/index.ts +2 -0
- 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/corpus/core/src/usage/store.ts +82 -20
- package/corpus/templates/plan/actions/record-recap-usage-cost.ts +49 -0
- package/corpus/templates/plan/actions/record-recap-usage.ts +15 -22
- 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/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +3 -0
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +2 -0
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/engine/types.d.ts +2 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/cli/code-agent-executor.d.ts +2 -0
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +52 -3
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +22 -2
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +199 -22
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/settings/UsageSection.d.ts.map +1 -1
- package/dist/client/settings/UsageSection.js +12 -3
- package/dist/client/settings/UsageSection.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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/dist/usage/store.d.ts +21 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +54 -19
- package/dist/usage/store.js.map +1 -1
- package/docs/content/pr-visual-recap.mdx +20 -12
- package/package.json +1 -1
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
type A2AContinuation,
|
|
19
19
|
} from "./a2a-continuations-store.js";
|
|
20
20
|
import { signInternalToken } from "./internal-token.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
OutgoingMessage,
|
|
23
|
+
PlatformAdapter,
|
|
24
|
+
PlatformRunProgress,
|
|
25
|
+
} from "./types.js";
|
|
22
26
|
|
|
23
27
|
const PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-a2a-continuation`;
|
|
24
28
|
const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
@@ -150,6 +154,8 @@ async function processClaimedContinuation(
|
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
const progress = await resumeA2AContinuationProgress(continuation, adapter);
|
|
158
|
+
|
|
153
159
|
const auth = await signContinuationToken(continuation);
|
|
154
160
|
const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
|
|
155
161
|
requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
@@ -162,6 +168,7 @@ async function processClaimedContinuation(
|
|
|
162
168
|
while (Date.now() < deadline) {
|
|
163
169
|
task = await client.getTask(continuation.a2aTaskId);
|
|
164
170
|
if (TERMINAL_STATES.has(task.status.state)) break;
|
|
171
|
+
await reportA2AContinuationProgress(continuation, progress, task);
|
|
165
172
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
166
173
|
}
|
|
167
174
|
} catch (err) {
|
|
@@ -171,6 +178,7 @@ async function processClaimedContinuation(
|
|
|
171
178
|
continuation,
|
|
172
179
|
adapter,
|
|
173
180
|
remotePollFailureReason(continuation),
|
|
181
|
+
progress,
|
|
174
182
|
);
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
@@ -182,6 +190,7 @@ async function processClaimedContinuation(
|
|
|
182
190
|
continuation,
|
|
183
191
|
adapter,
|
|
184
192
|
err instanceof Error ? err.message : String(err),
|
|
193
|
+
progress,
|
|
185
194
|
);
|
|
186
195
|
return;
|
|
187
196
|
}
|
|
@@ -190,24 +199,12 @@ async function processClaimedContinuation(
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
if (!task || !TERMINAL_STATES.has(task.status.state)) {
|
|
193
|
-
const recoverableArtifactText = extractRecoverableArtifactText(task);
|
|
194
|
-
if (recoverableArtifactText) {
|
|
195
|
-
await deliverAndCompleteA2AContinuation(
|
|
196
|
-
continuation,
|
|
197
|
-
adapter,
|
|
198
|
-
formatContinuationArtifactText(
|
|
199
|
-
recoverableArtifactText,
|
|
200
|
-
continuation.agentUrl,
|
|
201
|
-
),
|
|
202
|
-
);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
202
|
if (shouldStopPollingRemoteTask(continuation)) {
|
|
207
203
|
await notifyAndFailA2AContinuation(
|
|
208
204
|
continuation,
|
|
209
205
|
adapter,
|
|
210
206
|
remotePollFailureReason(continuation),
|
|
207
|
+
progress,
|
|
211
208
|
);
|
|
212
209
|
return;
|
|
213
210
|
}
|
|
@@ -219,7 +216,7 @@ async function processClaimedContinuation(
|
|
|
219
216
|
const reason =
|
|
220
217
|
extractTaskText(task) ||
|
|
221
218
|
`Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
|
|
222
|
-
await notifyAndFailA2AContinuation(continuation, adapter, reason);
|
|
219
|
+
await notifyAndFailA2AContinuation(continuation, adapter, reason, progress);
|
|
223
220
|
return;
|
|
224
221
|
}
|
|
225
222
|
|
|
@@ -232,11 +229,64 @@ async function processClaimedContinuation(
|
|
|
232
229
|
continuation,
|
|
233
230
|
adapter,
|
|
234
231
|
`Remote A2A task ${continuation.a2aTaskId} completed without text`,
|
|
232
|
+
progress,
|
|
235
233
|
);
|
|
236
234
|
return;
|
|
237
235
|
}
|
|
238
236
|
|
|
239
|
-
await deliverAndCompleteA2AContinuation(
|
|
237
|
+
await deliverAndCompleteA2AContinuation(
|
|
238
|
+
continuation,
|
|
239
|
+
adapter,
|
|
240
|
+
text,
|
|
241
|
+
progress,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function resumeA2AContinuationProgress(
|
|
246
|
+
continuation: A2AContinuation,
|
|
247
|
+
adapter: PlatformAdapter,
|
|
248
|
+
): Promise<PlatformRunProgress | null> {
|
|
249
|
+
if (!continuation.progressRef || !adapter.resumeRunProgress) return null;
|
|
250
|
+
try {
|
|
251
|
+
const progress = await adapter.resumeRunProgress(
|
|
252
|
+
continuation.incoming,
|
|
253
|
+
continuation.progressRef,
|
|
254
|
+
);
|
|
255
|
+
if (!progress) return null;
|
|
256
|
+
await progress.onEvent({
|
|
257
|
+
type: "agent_call_progress",
|
|
258
|
+
agent: continuation.agentName,
|
|
259
|
+
state: "working",
|
|
260
|
+
elapsedSeconds: Math.max(
|
|
261
|
+
0,
|
|
262
|
+
Math.round((Date.now() - continuation.createdAt) / 1_000),
|
|
263
|
+
),
|
|
264
|
+
detail: "Continuing in the background",
|
|
265
|
+
});
|
|
266
|
+
return progress;
|
|
267
|
+
} catch {
|
|
268
|
+
// A continuation still has a normal reply fallback. Do not log the
|
|
269
|
+
// opaque provider reference or the inbound message payload.
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function reportA2AContinuationProgress(
|
|
275
|
+
continuation: A2AContinuation,
|
|
276
|
+
progress: PlatformRunProgress | null,
|
|
277
|
+
task: Task,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
if (!progress) return;
|
|
280
|
+
await progress.onEvent({
|
|
281
|
+
type: "agent_call_progress",
|
|
282
|
+
agent: continuation.agentName,
|
|
283
|
+
state: task.status.state,
|
|
284
|
+
elapsedSeconds: Math.max(
|
|
285
|
+
0,
|
|
286
|
+
Math.round((Date.now() - continuation.createdAt) / 1_000),
|
|
287
|
+
),
|
|
288
|
+
detail: "Still working on the delegated request",
|
|
289
|
+
});
|
|
240
290
|
}
|
|
241
291
|
|
|
242
292
|
async function waitForContinuationDue(
|
|
@@ -262,6 +312,7 @@ async function notifyAndFailA2AContinuation(
|
|
|
262
312
|
continuation: A2AContinuation,
|
|
263
313
|
adapter: PlatformAdapter,
|
|
264
314
|
reason: string,
|
|
315
|
+
progress: PlatformRunProgress | null = null,
|
|
265
316
|
): Promise<void> {
|
|
266
317
|
const deliveryContinuation = await claimA2AContinuationDelivery(
|
|
267
318
|
continuation.id,
|
|
@@ -273,11 +324,14 @@ async function notifyAndFailA2AContinuation(
|
|
|
273
324
|
reason,
|
|
274
325
|
);
|
|
275
326
|
try {
|
|
327
|
+
const outgoing = adapter.formatAgentResponse(message);
|
|
276
328
|
await withTimeout(
|
|
277
|
-
|
|
278
|
-
adapter
|
|
279
|
-
deliveryContinuation
|
|
280
|
-
|
|
329
|
+
deliverA2AContinuationResponse(
|
|
330
|
+
adapter,
|
|
331
|
+
deliveryContinuation,
|
|
332
|
+
outgoing,
|
|
333
|
+
progress,
|
|
334
|
+
"error",
|
|
281
335
|
),
|
|
282
336
|
PLATFORM_SEND_TIMEOUT_MS,
|
|
283
337
|
`${deliveryContinuation.platform} failure notification timed out`,
|
|
@@ -296,6 +350,7 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
296
350
|
continuation: A2AContinuation,
|
|
297
351
|
adapter: PlatformAdapter,
|
|
298
352
|
text: string,
|
|
353
|
+
progress: PlatformRunProgress | null = null,
|
|
299
354
|
): Promise<void> {
|
|
300
355
|
const deliveryContinuation = await claimA2AContinuationDelivery(
|
|
301
356
|
continuation.id,
|
|
@@ -303,11 +358,14 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
303
358
|
if (!deliveryContinuation) return;
|
|
304
359
|
|
|
305
360
|
try {
|
|
361
|
+
const outgoing = adapter.formatAgentResponse(text);
|
|
306
362
|
await withTimeout(
|
|
307
|
-
|
|
308
|
-
adapter
|
|
309
|
-
deliveryContinuation
|
|
310
|
-
|
|
363
|
+
deliverA2AContinuationResponse(
|
|
364
|
+
adapter,
|
|
365
|
+
deliveryContinuation,
|
|
366
|
+
outgoing,
|
|
367
|
+
progress,
|
|
368
|
+
"done",
|
|
311
369
|
),
|
|
312
370
|
PLATFORM_SEND_TIMEOUT_MS,
|
|
313
371
|
`${deliveryContinuation.platform} response delivery timed out`,
|
|
@@ -327,6 +385,42 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
327
385
|
await completeAfterSuccessfulDelivery(deliveryContinuation);
|
|
328
386
|
}
|
|
329
387
|
|
|
388
|
+
async function deliverA2AContinuationResponse(
|
|
389
|
+
adapter: PlatformAdapter,
|
|
390
|
+
continuation: A2AContinuation,
|
|
391
|
+
message: OutgoingMessage,
|
|
392
|
+
progress: PlatformRunProgress | null,
|
|
393
|
+
status: "done" | "error",
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
if (progress) {
|
|
396
|
+
try {
|
|
397
|
+
await progress.onEvent({
|
|
398
|
+
type: "agent_call",
|
|
399
|
+
agent: continuation.agentName,
|
|
400
|
+
status,
|
|
401
|
+
});
|
|
402
|
+
await progress.complete(message);
|
|
403
|
+
return;
|
|
404
|
+
} catch {
|
|
405
|
+
// A resumed Slack stream can no longer be finalized (for example when
|
|
406
|
+
// chat.stopStream rejects). Preserve the final answer with the same
|
|
407
|
+
// thread reply fallback used by the initial webhook run. Also ask the
|
|
408
|
+
// adapter to terminate the native stream: otherwise Slack can keep the
|
|
409
|
+
// task card in its working state after the thread fallback succeeds.
|
|
410
|
+
try {
|
|
411
|
+
await progress.fail?.(
|
|
412
|
+
"I couldn't update the live response, but I posted the final result in this thread.",
|
|
413
|
+
);
|
|
414
|
+
} catch {
|
|
415
|
+
// The thread reply below is still the authoritative final answer.
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await adapter.sendResponse(message, continuation.incoming, {
|
|
420
|
+
placeholderRef: continuation.placeholderRef ?? undefined,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
330
424
|
async function rescheduleAndRedispatchA2AContinuation(
|
|
331
425
|
continuationId: string,
|
|
332
426
|
): Promise<void> {
|
|
@@ -524,13 +618,6 @@ function extractTaskText(task: Task): string {
|
|
|
524
618
|
.join("\n");
|
|
525
619
|
}
|
|
526
620
|
|
|
527
|
-
function extractRecoverableArtifactText(task: Task | null): string {
|
|
528
|
-
if (!task?.status.message?.metadata?.agentNativeRecoverableArtifacts) {
|
|
529
|
-
return "";
|
|
530
|
-
}
|
|
531
|
-
return extractTaskText(task);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
621
|
function formatContinuationArtifactText(
|
|
535
622
|
text: string,
|
|
536
623
|
agentUrl: string,
|
|
@@ -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();
|