@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
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.94.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 48d8471: Keep Slack agent task cards active through deferred cross-agent work, show durable progress, and deliver the true final result instead of treating interim artifacts as complete.
8
+ - 48d8471: Keep completed realtime voice turns in their originating chat, restore that chat safely when voice ends, and expose live audio activity to the voice dock.
9
+ - 48d8471: Accept canonical Content document links returned by successful read-only actions as A2A artifact proof while continuing to reject mismatched or unproven URLs.
10
+
3
11
  ## 0.94.2
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.94.2",
3
+ "version": "0.94.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -228,6 +228,132 @@ function contentDatabaseSubmissionArtifact(
228
228
  };
229
229
  }
230
230
 
231
+ function documentUrlForId(
232
+ parsed: Record<string, unknown>,
233
+ id: string,
234
+ additionalCandidates: Array<string | undefined> = [],
235
+ options: { requireContentOrigin?: boolean } = {},
236
+ ): string | undefined {
237
+ const candidates = [
238
+ stringValue(parsed.url),
239
+ stringValue(parsed.urlPath),
240
+ stringValue(parsed.deepLink),
241
+ stringValue(parsed.pageUrl),
242
+ stringValue(parsed.documentUrl),
243
+ ...additionalCandidates,
244
+ ].filter((value): value is string => !!value);
245
+
246
+ return candidates.find((candidate) => {
247
+ if (!artifactUrlReferencesId(candidate, "document", id)) return false;
248
+ return !options.requireContentOrigin || isContentDocumentUrl(candidate);
249
+ });
250
+ }
251
+
252
+ function isContentDocumentUrl(rawUrl: string): boolean {
253
+ try {
254
+ return new URL(rawUrl).origin === "https://content.agent-native.com";
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+
260
+ function addDocumentReadArtifact(
261
+ documents: Map<string, CreatedDocumentArtifact>,
262
+ parsed: Record<string, unknown>,
263
+ options: {
264
+ allowWithoutUrl: boolean;
265
+ additionalUrlCandidates?: Array<string | undefined>;
266
+ requireContentOrigin?: boolean;
267
+ },
268
+ ): void {
269
+ const id = stringValue(parsed.documentId) ?? stringValue(parsed.id);
270
+ if (!id) return;
271
+
272
+ const url = documentUrlForId(parsed, id, options.additionalUrlCandidates, {
273
+ requireContentOrigin: options.requireContentOrigin,
274
+ });
275
+ if (!url && !options.allowWithoutUrl) return;
276
+
277
+ documents.set(id, {
278
+ id,
279
+ title: stringValue(parsed.title) ?? stringValue(parsed.name),
280
+ url,
281
+ });
282
+ }
283
+
284
+ function addContentDatabaseReadArtifacts(
285
+ documents: Map<string, CreatedDocumentArtifact>,
286
+ parsed: Record<string, unknown>,
287
+ ): void {
288
+ const resultUrls = [
289
+ stringValue(parsed.url),
290
+ stringValue(parsed.urlPath),
291
+ stringValue(parsed.deepLink),
292
+ ];
293
+ const database = asRecord(parsed.database);
294
+ if (database) {
295
+ addDocumentReadArtifact(documents, database, {
296
+ allowWithoutUrl: true,
297
+ requireContentOrigin: true,
298
+ additionalUrlCandidates: resultUrls,
299
+ });
300
+ } else {
301
+ // Unavailable database reads still return a documentId, but they do not
302
+ // prove that the page exists and must not authorize an artifact URL.
303
+ if (parsed.available !== false) {
304
+ addDocumentReadArtifact(documents, parsed, {
305
+ allowWithoutUrl: true,
306
+ requireContentOrigin: true,
307
+ });
308
+ }
309
+ }
310
+
311
+ if (!Array.isArray(parsed.items)) return;
312
+ for (const item of parsed.items) {
313
+ const itemRecord = asRecord(item);
314
+ const document = asRecord(itemRecord?.document);
315
+ if (!document) continue;
316
+ addDocumentReadArtifact(documents, document, {
317
+ allowWithoutUrl: true,
318
+ requireContentOrigin: true,
319
+ additionalUrlCandidates: [
320
+ stringValue(itemRecord?.url),
321
+ stringValue(itemRecord?.urlPath),
322
+ stringValue(itemRecord?.deepLink),
323
+ ],
324
+ });
325
+ }
326
+ }
327
+
328
+ function isGenericReadTool(tool: string): boolean {
329
+ return /^(?:find|get|list|query|read|search)-/i.test(tool);
330
+ }
331
+
332
+ function addGenericDocumentReadArtifact(
333
+ documents: Map<string, CreatedDocumentArtifact>,
334
+ parsed: Record<string, unknown>,
335
+ ): void {
336
+ // Unknown read actions are accepted only when their result pairs a document
337
+ // ID with a canonical page URL containing that exact ID. An ID by itself is
338
+ // insufficient, preserving the fabrication guard for unrelated actions.
339
+ addDocumentReadArtifact(documents, parsed, {
340
+ allowWithoutUrl: false,
341
+ requireContentOrigin: true,
342
+ });
343
+
344
+ const document = asRecord(parsed.document);
345
+ if (!document) return;
346
+ addDocumentReadArtifact(documents, document, {
347
+ allowWithoutUrl: false,
348
+ requireContentOrigin: true,
349
+ additionalUrlCandidates: [
350
+ stringValue(parsed.url),
351
+ stringValue(parsed.urlPath),
352
+ stringValue(parsed.deepLink),
353
+ ],
354
+ });
355
+ }
356
+
231
357
  function addImageArtifact(
232
358
  images: Map<string, CreatedImageArtifact>,
233
359
  parsed: Record<string, unknown>,
@@ -387,7 +513,6 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
387
513
 
388
514
  if (
389
515
  toolResult.tool === "create-document" ||
390
- toolResult.tool === "get-document" ||
391
516
  toolResult.tool === "update-document"
392
517
  ) {
393
518
  const id = stringValue(parsed.id);
@@ -401,6 +526,34 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
401
526
  continue;
402
527
  }
403
528
 
529
+ if (
530
+ toolResult.tool === "get-document" ||
531
+ toolResult.tool === "get-content-document"
532
+ ) {
533
+ const document = asRecord(parsed.document);
534
+ addDocumentReadArtifact(documents, document ?? parsed, {
535
+ allowWithoutUrl: true,
536
+ requireContentOrigin: true,
537
+ additionalUrlCandidates: document
538
+ ? [
539
+ stringValue(parsed.url),
540
+ stringValue(parsed.urlPath),
541
+ stringValue(parsed.deepLink),
542
+ ]
543
+ : [],
544
+ });
545
+ continue;
546
+ }
547
+
548
+ if (toolResult.tool === "get-content-database") {
549
+ addContentDatabaseReadArtifacts(documents, parsed);
550
+ continue;
551
+ }
552
+
553
+ if (isGenericReadTool(toolResult.tool)) {
554
+ addGenericDocumentReadArtifact(documents, parsed);
555
+ }
556
+
404
557
  if (
405
558
  toolResult.tool === "create-deck" ||
406
559
  toolResult.tool === "duplicate-deck"
@@ -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 { PlatformAdapter } from "./types.js";
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(continuation, adapter, text);
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
- adapter.sendResponse(
278
- adapter.formatAgentResponse(message),
279
- deliveryContinuation.incoming,
280
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
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
- adapter.sendResponse(
308
- adapter.formatAgentResponse(text),
309
- deliveryContinuation.incoming,
310
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
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,