@agent-native/core 0.101.6 → 0.101.7

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 (101) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/artifact-response.ts +255 -12
  5. package/corpus/core/src/agent/thread-data-builder.ts +84 -11
  6. package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
  7. package/corpus/core/src/integrations/adapters/discord.ts +3 -1
  8. package/corpus/core/src/integrations/adapters/email.ts +4 -1
  9. package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
  10. package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
  11. package/corpus/core/src/integrations/adapters/slack.ts +38 -9
  12. package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
  13. package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
  14. package/corpus/core/src/integrations/index.ts +1 -0
  15. package/corpus/core/src/integrations/types.ts +9 -2
  16. package/corpus/core/src/integrations/webhook-handler.ts +69 -45
  17. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
  18. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  19. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
  20. package/corpus/templates/content/AGENTS.md +29 -7
  21. package/corpus/templates/content/actions/_database-source-utils.ts +30 -16
  22. package/corpus/templates/content/actions/_database-utils.ts +15 -1
  23. package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
  24. package/corpus/templates/content/actions/_property-utils.ts +7 -1
  25. package/corpus/templates/content/actions/configure-document-property.ts +11 -0
  26. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  27. package/corpus/templates/content/actions/create-document.ts +9 -0
  28. package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
  29. package/corpus/templates/content/actions/export-content-source.ts +1 -0
  30. package/corpus/templates/content/actions/get-content-database.ts +13 -1
  31. package/corpus/templates/content/actions/get-document.ts +12 -2
  32. package/corpus/templates/content/actions/import-content-source.ts +6 -0
  33. package/corpus/templates/content/actions/list-documents.ts +3 -0
  34. package/corpus/templates/content/actions/pull-document.ts +2 -0
  35. package/corpus/templates/content/actions/search-documents.ts +5 -1
  36. package/corpus/templates/content/actions/update-document.ts +16 -1
  37. package/corpus/templates/content/actions/view-screen.ts +15 -2
  38. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
  39. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
  40. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +223 -34
  41. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +56 -8
  42. package/corpus/templates/content/app/i18n/zh-TW.ts +5 -0
  43. package/corpus/templates/content/app/i18n-data.ts +52 -0
  44. package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
  45. package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
  46. package/corpus/templates/content/server/db/schema.ts +4 -0
  47. package/corpus/templates/content/server/lib/document-context.ts +76 -0
  48. package/corpus/templates/content/server/lib/public-documents.ts +3 -0
  49. package/corpus/templates/content/server/plugins/db.ts +6 -0
  50. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
  51. package/corpus/templates/content/shared/api.ts +17 -0
  52. package/corpus/templates/content/shared/content-source.ts +7 -0
  53. package/corpus/templates/content/shared/properties.ts +2 -0
  54. package/dist/a2a/artifact-response.d.ts +15 -0
  55. package/dist/a2a/artifact-response.d.ts.map +1 -1
  56. package/dist/a2a/artifact-response.js +204 -5
  57. package/dist/a2a/artifact-response.js.map +1 -1
  58. package/dist/agent/thread-data-builder.d.ts +6 -0
  59. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  60. package/dist/agent/thread-data-builder.js +71 -8
  61. package/dist/agent/thread-data-builder.js.map +1 -1
  62. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  63. package/dist/integrations/a2a-continuation-processor.js +72 -6
  64. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  65. package/dist/integrations/adapters/discord.d.ts.map +1 -1
  66. package/dist/integrations/adapters/discord.js +1 -0
  67. package/dist/integrations/adapters/discord.js.map +1 -1
  68. package/dist/integrations/adapters/email.d.ts.map +1 -1
  69. package/dist/integrations/adapters/email.js +2 -0
  70. package/dist/integrations/adapters/email.js.map +1 -1
  71. package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
  72. package/dist/integrations/adapters/google-docs.js +2 -0
  73. package/dist/integrations/adapters/google-docs.js.map +1 -1
  74. package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
  75. package/dist/integrations/adapters/microsoft-teams.js +1 -0
  76. package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
  77. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  78. package/dist/integrations/adapters/slack.js +21 -6
  79. package/dist/integrations/adapters/slack.js.map +1 -1
  80. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  81. package/dist/integrations/adapters/telegram.js +11 -2
  82. package/dist/integrations/adapters/telegram.js.map +1 -1
  83. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  84. package/dist/integrations/adapters/whatsapp.js +3 -0
  85. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  86. package/dist/integrations/index.d.ts +1 -1
  87. package/dist/integrations/index.d.ts.map +1 -1
  88. package/dist/integrations/index.js.map +1 -1
  89. package/dist/integrations/types.d.ts +8 -2
  90. package/dist/integrations/types.d.ts.map +1 -1
  91. package/dist/integrations/types.js.map +1 -1
  92. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  93. package/dist/integrations/webhook-handler.js +40 -43
  94. package/dist/integrations/webhook-handler.js.map +1 -1
  95. package/dist/observability/routes.d.ts +1 -1
  96. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  97. package/dist/resources/handlers.d.ts +1 -1
  98. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  99. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  100. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  101. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2258
31
- - template files: 5635
31
+ - template files: 5639
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.101.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 70caa6f: Preserve participant-visible integration replies and verified artifact identities so threaded corrections can reliably target resources after renames.
8
+
3
9
  ## 0.101.6
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.101.6",
3
+ "version": "0.101.7",
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": {
@@ -1,3 +1,5 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+
1
3
  export interface A2AToolResultSummary {
2
4
  tool: string;
3
5
  result: string;
@@ -6,6 +8,121 @@ export interface A2AToolResultSummary {
6
8
  export interface A2AArtifactResponseOptions {
7
9
  baseUrl?: string;
8
10
  includeReferencedArtifacts?: boolean;
11
+ includePersistedArtifactMarker?: boolean;
12
+ }
13
+
14
+ export interface A2AArtifactIdentity {
15
+ resourceType:
16
+ | "document"
17
+ | "deck"
18
+ | "dashboard"
19
+ | "analysis"
20
+ | "image"
21
+ | "design"
22
+ | "monitor"
23
+ | "form";
24
+ id: string;
25
+ sourceAction: string;
26
+ titleAtAction?: string;
27
+ url?: string;
28
+ }
29
+
30
+ const ARTIFACT_IDENTITY_WRITE_TOOLS = new Set([
31
+ "save-monitor",
32
+ "create-form",
33
+ "submit-content-database-form",
34
+ "add-database-item",
35
+ "create-document",
36
+ "update-document",
37
+ "create-deck",
38
+ "duplicate-deck",
39
+ "add-slide",
40
+ "update-dashboard",
41
+ "rename-dashboard",
42
+ "save-analysis",
43
+ "generate-image",
44
+ "edit-image",
45
+ "refine-image",
46
+ "restyle-image",
47
+ "save-generated-image",
48
+ "save-generated-asset",
49
+ "export-image",
50
+ "export-asset",
51
+ "generate-image-batch",
52
+ "create-design",
53
+ "generate-design",
54
+ "create-file",
55
+ "duplicate-design",
56
+ ]);
57
+
58
+ const PERSISTED_ARTIFACT_MARKER = "agent-native:persisted-artifacts=";
59
+ const PERSISTED_ARTIFACT_MARKER_PATTERN =
60
+ /\s*<!--\s*agent-native:persisted-artifacts=[A-Za-z0-9_-]+\.[a-f0-9]{64}\s*-->/g;
61
+ const ARTIFACT_RESOURCE_TYPES = new Set<A2AArtifactIdentity["resourceType"]>([
62
+ "document",
63
+ "deck",
64
+ "dashboard",
65
+ "analysis",
66
+ "image",
67
+ "design",
68
+ "monitor",
69
+ "form",
70
+ ]);
71
+
72
+ function persistedArtifactIdentitiesFromMarker(
73
+ result: string,
74
+ ): A2AArtifactIdentity[] {
75
+ const secret = process.env.A2A_SECRET;
76
+ if (!secret) return [];
77
+ const match = result.match(
78
+ /<!--\s*agent-native:persisted-artifacts=([A-Za-z0-9_-]+)\.([a-f0-9]{64})\s*-->/,
79
+ );
80
+ if (!match) return [];
81
+ try {
82
+ const payload = match[1];
83
+ const expected = createHmac("sha256", secret).update(payload).digest();
84
+ const supplied = Buffer.from(match[2], "hex");
85
+ if (
86
+ supplied.length !== expected.length ||
87
+ !timingSafeEqual(supplied, expected)
88
+ ) {
89
+ return [];
90
+ }
91
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString());
92
+ if (!Array.isArray(parsed)) return [];
93
+ return parsed
94
+ .slice(0, 12)
95
+ .filter((identity): identity is A2AArtifactIdentity => {
96
+ const item = asRecord(identity);
97
+ return (
98
+ !!item &&
99
+ ARTIFACT_RESOURCE_TYPES.has(
100
+ item.resourceType as A2AArtifactIdentity["resourceType"],
101
+ ) &&
102
+ typeof item.id === "string" &&
103
+ typeof item.sourceAction === "string"
104
+ );
105
+ });
106
+ } catch {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ function withPersistedArtifactMarker(
112
+ text: string,
113
+ toolResults: A2AToolResultSummary[],
114
+ ): string {
115
+ const identities = extractA2AArtifactIdentities(toolResults).slice(0, 12);
116
+ const secret = process.env.A2A_SECRET;
117
+ if (identities.length === 0 || !secret) return text;
118
+ const payload = Buffer.from(JSON.stringify(identities)).toString("base64url");
119
+ const signature = createHmac("sha256", secret).update(payload).digest("hex");
120
+ const marker = `<!-- ${PERSISTED_ARTIFACT_MARKER}${payload}.${signature} -->`;
121
+ return text ? `${text}\n\n${marker}` : marker;
122
+ }
123
+
124
+ export function stripA2APersistedArtifactMarkers(text: string): string {
125
+ return text.replace(PERSISTED_ARTIFACT_MARKER_PATTERN, "").trim();
9
126
  }
10
127
 
11
128
  interface CreatedDocumentArtifact {
@@ -617,15 +734,25 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
617
734
 
618
735
  if (
619
736
  toolResult.tool === "generate-image" ||
737
+ toolResult.tool === "edit-image" ||
620
738
  toolResult.tool === "refine-image" ||
739
+ toolResult.tool === "restyle-image" ||
621
740
  toolResult.tool === "get-asset" ||
622
741
  toolResult.tool === "save-generated-image" ||
742
+ toolResult.tool === "save-generated-asset" ||
623
743
  toolResult.tool === "export-image"
624
744
  ) {
625
745
  addImageArtifact(images, parsed);
626
746
  continue;
627
747
  }
628
748
 
749
+ if (toolResult.tool === "export-asset") {
750
+ if (stringValue(parsed.artifactType) === "image") {
751
+ addImageArtifact(images, parsed);
752
+ }
753
+ continue;
754
+ }
755
+
629
756
  if (toolResult.tool === "generate-image-batch") {
630
757
  if (Array.isArray(parsed.images)) {
631
758
  for (const item of parsed.images) {
@@ -730,6 +857,114 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
730
857
  };
731
858
  }
732
859
 
860
+ /**
861
+ * Extract a compact, verified identity ledger from successful artifact tools.
862
+ * The ledger deliberately excludes raw tool results so it is safe to retain in
863
+ * long-lived thread context and stable even when a resource is later renamed.
864
+ */
865
+ export function extractA2AArtifactIdentities(
866
+ results: A2AToolResultSummary[],
867
+ ): A2AArtifactIdentity[] {
868
+ const identities = new Map<string, A2AArtifactIdentity>();
869
+
870
+ const remember = (identity: A2AArtifactIdentity) => {
871
+ identities.set(`${identity.resourceType}:${identity.id}`, identity);
872
+ };
873
+
874
+ for (const result of results) {
875
+ if (result.tool === "call-agent") {
876
+ for (const identity of persistedArtifactIdentitiesFromMarker(
877
+ result.result,
878
+ )) {
879
+ remember({ ...identity, sourceAction: "call-agent" });
880
+ }
881
+ continue;
882
+ }
883
+ if (!ARTIFACT_IDENTITY_WRITE_TOOLS.has(result.tool)) continue;
884
+ const artifacts = collectArtifacts([result]);
885
+ for (const document of artifacts.documents) {
886
+ remember({
887
+ resourceType: "document",
888
+ id: document.id,
889
+ sourceAction: result.tool,
890
+ titleAtAction: document.title,
891
+ url: document.url,
892
+ });
893
+ }
894
+ for (const deck of artifacts.decks) {
895
+ remember({
896
+ resourceType: "deck",
897
+ id: deck.id,
898
+ sourceAction: result.tool,
899
+ url: deck.url,
900
+ });
901
+ }
902
+ for (const dashboard of artifacts.dashboards) {
903
+ remember({
904
+ resourceType: "dashboard",
905
+ id: dashboard.id,
906
+ sourceAction: result.tool,
907
+ titleAtAction: dashboard.title,
908
+ url: dashboard.url,
909
+ });
910
+ }
911
+ for (const analysis of artifacts.analyses) {
912
+ remember({
913
+ resourceType: "analysis",
914
+ id: analysis.id,
915
+ sourceAction: result.tool,
916
+ titleAtAction: analysis.title,
917
+ url: analysis.url,
918
+ });
919
+ }
920
+ for (const image of artifacts.images) {
921
+ remember({
922
+ resourceType: "image",
923
+ id: image.id,
924
+ sourceAction: result.tool,
925
+ titleAtAction: image.title,
926
+ url: image.url,
927
+ });
928
+ }
929
+ for (const design of artifacts.designShells) {
930
+ remember({
931
+ resourceType: "design",
932
+ id: design.id,
933
+ sourceAction: result.tool,
934
+ titleAtAction: design.title,
935
+ });
936
+ }
937
+ for (const design of artifacts.generatedDesigns) {
938
+ remember({
939
+ resourceType: "design",
940
+ id: design.id,
941
+ sourceAction: result.tool,
942
+ url: design.url,
943
+ });
944
+ }
945
+ for (const monitor of artifacts.monitors) {
946
+ remember({
947
+ resourceType: "monitor",
948
+ id: monitor.id,
949
+ sourceAction: result.tool,
950
+ titleAtAction: monitor.name,
951
+ url: monitor.url,
952
+ });
953
+ }
954
+ for (const form of artifacts.forms) {
955
+ remember({
956
+ resourceType: "form",
957
+ id: form.id,
958
+ sourceAction: result.tool,
959
+ titleAtAction: form.title,
960
+ url: form.url,
961
+ });
962
+ }
963
+ }
964
+
965
+ return [...identities.values()];
966
+ }
967
+
733
968
  type DownstreamArtifact =
734
969
  | { kind: "deck"; id: string; url: string }
735
970
  | { kind: "document"; id: string; url: string; title?: string }
@@ -1131,6 +1366,10 @@ export function appendA2AArtifactLinks(
1131
1366
  const baseUrl = normalizeBaseUrl(options.baseUrl);
1132
1367
  const includeReferencedArtifacts =
1133
1368
  options.includeReferencedArtifacts ?? false;
1369
+ const finalize = (value: string) =>
1370
+ options.includePersistedArtifactMarker
1371
+ ? withPersistedArtifactMarker(value, toolResults)
1372
+ : value;
1134
1373
  const {
1135
1374
  documents,
1136
1375
  decks,
@@ -1160,7 +1399,7 @@ export function appendA2AArtifactLinks(
1160
1399
  ) ||
1161
1400
  /\b(?:done|created|ready|here(?:'s| is)|complete|finished)\b/i.test(text))
1162
1401
  ) {
1163
- return formatIncompleteDesignMessage(incompleteShells);
1402
+ return finalize(formatIncompleteDesignMessage(incompleteShells));
1164
1403
  }
1165
1404
 
1166
1405
  const unverifiedRefs = findUnverifiedArtifactReferences(
@@ -1174,15 +1413,17 @@ export function appendA2AArtifactLinks(
1174
1413
  generatedDesigns,
1175
1414
  );
1176
1415
  if (unverifiedRefs.length > 0) {
1177
- return formatUnverifiedArtifactMessage(
1178
- unverifiedRefs,
1179
- documents,
1180
- decks,
1181
- dashboards,
1182
- analyses,
1183
- images,
1184
- generatedDesigns,
1185
- baseUrl,
1416
+ return finalize(
1417
+ formatUnverifiedArtifactMessage(
1418
+ unverifiedRefs,
1419
+ documents,
1420
+ decks,
1421
+ dashboards,
1422
+ analyses,
1423
+ images,
1424
+ generatedDesigns,
1425
+ baseUrl,
1426
+ ),
1186
1427
  );
1187
1428
  }
1188
1429
 
@@ -1258,9 +1499,11 @@ export function appendA2AArtifactLinks(
1258
1499
  }
1259
1500
  }
1260
1501
 
1261
- if (missingLines.length === 0) return text;
1502
+ if (missingLines.length === 0) {
1503
+ return finalize(text);
1504
+ }
1262
1505
  const artifactBlock = `Artifacts:\n${missingLines.join("\n")}`;
1263
- return text ? `${text}\n\n${artifactBlock}` : artifactBlock;
1506
+ return finalize(text ? `${text}\n\n${artifactBlock}` : artifactBlock);
1264
1507
  }
1265
1508
 
1266
1509
  export function buildA2ARecoverableArtifactMessage(
@@ -637,23 +637,96 @@ export function threadDataToEngineMessages(
637
637
  for (const entry of data.messages) {
638
638
  const m = entry?.message ?? entry;
639
639
  if (!m || (m.role !== "user" && m.role !== "assistant")) continue;
640
- const text =
641
- typeof m.content === "string"
642
- ? m.content
643
- : Array.isArray(m.content)
644
- ? m.content
645
- .filter(
646
- (c: any) => c?.type === "text" && typeof c.text === "string",
647
- )
648
- .map((c: any) => c.text)
649
- .join("\n")
650
- : "";
640
+ const text = threadMessageTextForEngine(m);
651
641
  if (!text.trim()) continue;
652
642
  messages.push({ role: m.role, content: [{ type: "text", text }] });
653
643
  }
654
644
  return messages;
655
645
  }
656
646
 
647
+ const MAX_INTEGRATION_ARTIFACTS_IN_CONTEXT = 12;
648
+ const MAX_INTEGRATION_ARTIFACT_FIELD_CHARS = 500;
649
+
650
+ function boundedString(value: unknown): string | undefined {
651
+ if (typeof value !== "string") return undefined;
652
+ const trimmed = value.trim();
653
+ return trimmed
654
+ ? trimmed.slice(0, MAX_INTEGRATION_ARTIFACT_FIELD_CHARS)
655
+ : undefined;
656
+ }
657
+
658
+ function promptSafeJson(value: unknown): string {
659
+ return JSON.stringify(value)
660
+ .replaceAll("&", "\\u0026")
661
+ .replaceAll("<", "\\u003c")
662
+ .replaceAll(">", "\\u003e");
663
+ }
664
+
665
+ function messageTextContent(message: any): string {
666
+ if (typeof message?.content === "string") return message.content;
667
+ if (!Array.isArray(message?.content)) return "";
668
+ return message.content
669
+ .filter(
670
+ (part: any) => part?.type === "text" && typeof part.text === "string",
671
+ )
672
+ .map((part: any) => part.text)
673
+ .join("\n");
674
+ }
675
+
676
+ /**
677
+ * Select the participant-visible delivery for integration turns while keeping
678
+ * a compact, trusted resource ledger available to the agent. Raw tool results
679
+ * remain in thread_data for UI/audit use but are not replayed into the prompt.
680
+ */
681
+ export function threadMessageTextForEngine(message: any): string {
682
+ const delivery = message?.metadata?.integrationDelivery;
683
+ const deliveryAttempted =
684
+ message?.metadata?.integrationDeliveryAttempted === true;
685
+ const deliveredText =
686
+ message?.role === "assistant" &&
687
+ delivery?.status === "delivered" &&
688
+ typeof delivery.text === "string" &&
689
+ delivery.text.trim()
690
+ ? delivery.text
691
+ : undefined;
692
+ let text =
693
+ deliveredText ??
694
+ (message?.role === "assistant" && deliveryAttempted
695
+ ? ""
696
+ : messageTextContent(message));
697
+
698
+ if (message?.role !== "assistant") return text;
699
+ const storedArtifacts = message?.metadata?.integrationArtifacts;
700
+ if (!Array.isArray(storedArtifacts)) return text;
701
+
702
+ const artifacts = storedArtifacts
703
+ .slice(0, MAX_INTEGRATION_ARTIFACTS_IN_CONTEXT)
704
+ .map((artifact: any) => ({
705
+ resourceType: boundedString(artifact?.resourceType),
706
+ id: boundedString(artifact?.id),
707
+ sourceAction: boundedString(artifact?.sourceAction),
708
+ titleAtAction: boundedString(artifact?.titleAtAction),
709
+ url: boundedString(artifact?.url),
710
+ }))
711
+ .filter(
712
+ (artifact: {
713
+ resourceType?: string;
714
+ id?: string;
715
+ sourceAction?: string;
716
+ }) => artifact.resourceType && artifact.id && artifact.sourceAction,
717
+ );
718
+ if (artifacts.length === 0) return text;
719
+
720
+ const context = [
721
+ "<integration_artifact_context>",
722
+ "Trusted action history for this conversation. Resource IDs remain stable if participants rename the resource. Use these IDs when a follow-up refers to an earlier artifact, while still deciding from the request whether to update, add, supersede, or create.",
723
+ promptSafeJson(artifacts),
724
+ "</integration_artifact_context>",
725
+ ].join("\n");
726
+ text = text.trim() ? `${text.trim()}\n\n${context}` : context;
727
+ return text;
728
+ }
729
+
657
730
  export interface CodeAgentThreadTranscriptEvent {
658
731
  id: string;
659
732
  runId: string;
@@ -1,4 +1,8 @@
1
- import { appendA2AArtifactLinks } from "../a2a/artifact-response.js";
1
+ import {
2
+ appendA2AArtifactLinks,
3
+ extractA2AArtifactIdentities,
4
+ stripA2APersistedArtifactMarkers,
5
+ } from "../a2a/artifact-response.js";
2
6
  import { A2AClient, signA2AToken } from "../a2a/client.js";
3
7
  import type { Task } from "../a2a/types.js";
4
8
  import {
@@ -6,6 +10,8 @@ import {
6
10
  isLlmCredentialError,
7
11
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
8
12
  } from "../agent/engine/credential-errors.js";
13
+ import { extractThreadMeta } from "../agent/thread-data-builder.js";
14
+ import { getThread, updateThreadData } from "../chat-threads/store.js";
9
15
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
10
16
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
11
17
  import {
@@ -19,9 +25,11 @@ import {
19
25
  type A2AContinuation,
20
26
  } from "./a2a-continuations-store.js";
21
27
  import { signInternalToken } from "./internal-token.js";
28
+ import { getThreadMapping } from "./thread-mapping-store.js";
22
29
  import type {
23
30
  OutgoingMessage,
24
31
  PlatformAdapter,
32
+ PlatformDeliveryReceipt,
25
33
  PlatformRunProgress,
26
34
  } from "./types.js";
27
35
 
@@ -359,8 +367,10 @@ async function deliverAndCompleteA2AContinuation(
359
367
  if (!deliveryContinuation) return;
360
368
 
361
369
  try {
362
- const outgoing = adapter.formatAgentResponse(text);
363
- await withTimeout(
370
+ const outgoing = adapter.formatAgentResponse(
371
+ stripA2APersistedArtifactMarkers(text),
372
+ );
373
+ const deliveryReceipt = await withTimeout(
364
374
  deliverA2AContinuationResponse(
365
375
  adapter,
366
376
  deliveryContinuation,
@@ -371,6 +381,27 @@ async function deliverAndCompleteA2AContinuation(
371
381
  PLATFORM_SEND_TIMEOUT_MS,
372
382
  `${deliveryContinuation.platform} response delivery timed out`,
373
383
  );
384
+ let persistenceError: unknown;
385
+ for (let attempt = 0; attempt < 3; attempt += 1) {
386
+ try {
387
+ await persistA2AContinuationDelivery(
388
+ deliveryContinuation,
389
+ outgoing,
390
+ deliveryReceipt,
391
+ text,
392
+ );
393
+ persistenceError = undefined;
394
+ break;
395
+ } catch (err) {
396
+ persistenceError = err;
397
+ }
398
+ }
399
+ if (persistenceError) {
400
+ console.error(
401
+ `[integrations] Delivered A2A continuation ${deliveryContinuation.id} but could not persist its thread history:`,
402
+ persistenceError,
403
+ );
404
+ }
374
405
  } catch (err) {
375
406
  if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
376
407
  await failA2AContinuation(
@@ -392,7 +423,7 @@ async function deliverA2AContinuationResponse(
392
423
  message: OutgoingMessage,
393
424
  progress: PlatformRunProgress | null,
394
425
  status: "done" | "error",
395
- ): Promise<void> {
426
+ ): Promise<PlatformDeliveryReceipt> {
396
427
  if (progress) {
397
428
  try {
398
429
  await progress.onEvent({
@@ -400,8 +431,9 @@ async function deliverA2AContinuationResponse(
400
431
  agent: continuation.agentName,
401
432
  status,
402
433
  });
403
- await progress.complete(message);
404
- return;
434
+ const receipt = await progress.complete(message);
435
+ if (receipt?.status === "delivered") return receipt;
436
+ throw new Error("Continuation progress completed without delivery proof");
405
437
  } catch {
406
438
  // A resumed Slack stream can no longer be finalized (for example when
407
439
  // chat.stopStream rejects). Preserve the final answer with the same
@@ -417,9 +449,69 @@ async function deliverA2AContinuationResponse(
417
449
  }
418
450
  }
419
451
  }
420
- await adapter.sendResponse(message, continuation.incoming, {
452
+ const receipt = await adapter.sendResponse(message, continuation.incoming, {
421
453
  placeholderRef: continuation.placeholderRef ?? undefined,
422
454
  });
455
+ if (receipt?.status !== "delivered") {
456
+ throw new Error("Continuation response completed without delivery proof");
457
+ }
458
+ return receipt;
459
+ }
460
+
461
+ async function persistA2AContinuationDelivery(
462
+ continuation: A2AContinuation,
463
+ outgoing: OutgoingMessage,
464
+ receipt: PlatformDeliveryReceipt,
465
+ artifactText: string,
466
+ ): Promise<void> {
467
+ const mapping = await getThreadMapping(
468
+ continuation.platform,
469
+ continuation.externalThreadId,
470
+ );
471
+ if (!mapping) return;
472
+ const thread = await getThread(mapping.internalThreadId);
473
+ if (!thread) return;
474
+
475
+ let repo: any;
476
+ try {
477
+ repo = JSON.parse(thread.threadData || "{}");
478
+ } catch {
479
+ repo = {};
480
+ }
481
+ if (!Array.isArray(repo.messages)) repo.messages = [];
482
+
483
+ const artifacts = extractA2AArtifactIdentities([
484
+ { tool: "call-agent", result: artifactText },
485
+ ]);
486
+ const metadata: Record<string, unknown> = {
487
+ integrationDeliveryAttempted: true,
488
+ integrationDelivery: {
489
+ platform: continuation.platform,
490
+ status: "delivered",
491
+ text: outgoing.text,
492
+ deliveredAt: new Date().toISOString(),
493
+ ...(receipt.messageRefs?.length
494
+ ? { messageRefs: receipt.messageRefs }
495
+ : {}),
496
+ },
497
+ };
498
+ if (artifacts.length > 0) metadata.integrationArtifacts = artifacts;
499
+
500
+ repo.messages.push({
501
+ id: `msg-${Date.now()}-assistant-continuation`,
502
+ role: "assistant",
503
+ content: [{ type: "text", text: outgoing.text }],
504
+ createdAt: new Date().toISOString(),
505
+ metadata,
506
+ });
507
+ const meta = extractThreadMeta(repo);
508
+ await updateThreadData(
509
+ mapping.internalThreadId,
510
+ JSON.stringify(repo),
511
+ meta.title || thread.title || "Integration Chat",
512
+ meta.preview || thread.preview || "",
513
+ repo.messages.length,
514
+ );
423
515
  }
424
516
 
425
517
  async function rescheduleAndRedispatchA2AContinuation(
@@ -9,6 +9,7 @@ import type {
9
9
  IntegrationStatus,
10
10
  OutgoingMessage,
11
11
  PlatformAdapter,
12
+ PlatformDeliveryReceipt,
12
13
  } from "../types.js";
13
14
 
14
15
  const DISCORD_MAX_CONTENT_LENGTH = 2_000;
@@ -209,7 +210,7 @@ export function discordAdapter(): PlatformAdapter {
209
210
  async sendResponse(
210
211
  message: OutgoingMessage,
211
212
  context: IncomingMessage,
212
- ): Promise<void> {
213
+ ): Promise<void | PlatformDeliveryReceipt> {
213
214
  const applicationId = readString(context.platformContext.applicationId);
214
215
  const interactionToken = readString(
215
216
  context.responseContext?.interactionToken,
@@ -236,6 +237,7 @@ export function discordAdapter(): PlatformAdapter {
236
237
  chunk,
237
238
  );
238
239
  }
240
+ return { status: "delivered" };
239
241
  },
240
242
 
241
243
  formatAgentResponse(text: string): OutgoingMessage {
@@ -18,6 +18,7 @@ import type {
18
18
  OutgoingMessage,
19
19
  IntegrationStatus,
20
20
  OutboundTarget,
21
+ PlatformDeliveryReceipt,
21
22
  } from "../types.js";
22
23
 
23
24
  /** Max body length before truncation */
@@ -249,7 +250,7 @@ export function emailAdapter(): PlatformAdapter {
249
250
  async sendResponse(
250
251
  message: OutgoingMessage,
251
252
  context: IncomingMessage,
252
- ): Promise<void> {
253
+ ): Promise<void | PlatformDeliveryReceipt> {
253
254
  const agentAddress = await resolveSecret("EMAIL_AGENT_ADDRESS");
254
255
  if (!agentAddress) {
255
256
  console.error("[email] EMAIL_AGENT_ADDRESS not configured");
@@ -284,7 +285,9 @@ export function emailAdapter(): PlatformAdapter {
284
285
  });
285
286
  } catch (err) {
286
287
  console.error("[email] Failed to send response:", err);
288
+ throw err;
287
289
  }
290
+ return { status: "delivered" };
288
291
  },
289
292
 
290
293
  async sendMessageToTarget(