@agent-native/core 0.107.1 → 0.108.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.
Files changed (257) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/actions.mdx +48 -0
  4. package/corpus/core/docs/content/locales/ar-SA/actions.mdx +38 -0
  5. package/corpus/core/docs/content/locales/de-DE/actions.mdx +38 -0
  6. package/corpus/core/docs/content/locales/es-ES/actions.mdx +38 -0
  7. package/corpus/core/docs/content/locales/fr-FR/actions.mdx +38 -0
  8. package/corpus/core/docs/content/locales/hi-IN/actions.mdx +38 -0
  9. package/corpus/core/docs/content/locales/ja-JP/actions.mdx +38 -0
  10. package/corpus/core/docs/content/locales/ko-KR/actions.mdx +38 -0
  11. package/corpus/core/docs/content/locales/pt-BR/actions.mdx +38 -0
  12. package/corpus/core/docs/content/locales/zh-CN/actions.mdx +38 -0
  13. package/corpus/core/docs/content/locales/zh-TW/actions.mdx +38 -0
  14. package/corpus/core/package.json +5 -1
  15. package/corpus/core/src/a2a/artifact-response.ts +32 -10
  16. package/corpus/core/src/a2a-claims.ts +52 -0
  17. package/corpus/core/src/action.ts +6 -2
  18. package/corpus/core/src/audit/record.ts +6 -0
  19. package/corpus/core/src/client/feature-flags/FeatureFlagsPanel.tsx +519 -0
  20. package/corpus/core/src/client/feature-flags/helpers.ts +77 -0
  21. package/corpus/core/src/client/feature-flags/index.ts +13 -0
  22. package/corpus/core/src/client/feature-flags/types.ts +29 -0
  23. package/corpus/core/src/client/feature-flags/use-feature-flag.ts +26 -0
  24. package/corpus/core/src/client/index.ts +11 -0
  25. package/corpus/core/src/extensions/routes.ts +1 -1
  26. package/corpus/core/src/feature-flags/a2a-action-route.ts +56 -0
  27. package/corpus/core/src/feature-flags/actions/get-feature-flags.ts +24 -0
  28. package/corpus/core/src/feature-flags/actions/list-feature-flags.ts +69 -0
  29. package/corpus/core/src/feature-flags/actions/set-feature-flag.ts +89 -0
  30. package/corpus/core/src/feature-flags/index.ts +21 -0
  31. package/corpus/core/src/feature-flags/permissions.ts +53 -0
  32. package/corpus/core/src/feature-flags/plugin.ts +44 -0
  33. package/corpus/core/src/feature-flags/registry.ts +96 -0
  34. package/corpus/core/src/feature-flags/store.ts +176 -0
  35. package/corpus/core/src/index.ts +15 -0
  36. package/corpus/core/src/integrations/a2a-continuation-processor.ts +107 -3
  37. package/corpus/core/src/integrations/pending-tasks-store.ts +98 -1
  38. package/corpus/core/src/integrations/plugin.ts +129 -4
  39. package/corpus/core/src/integrations/webhook-handler.ts +339 -36
  40. package/corpus/core/src/localization/default-messages.ts +47 -0
  41. package/corpus/core/src/server/action-discovery.ts +12 -0
  42. package/corpus/core/src/server/action-routes.ts +47 -5
  43. package/corpus/core/src/server/agent-chat-plugin.ts +93 -8
  44. package/corpus/core/src/server/index.ts +1 -0
  45. package/corpus/core/src/settings/org-settings.ts +13 -0
  46. package/corpus/core/src/settings/store.ts +57 -0
  47. package/corpus/core/src/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  48. package/corpus/core/src/templates/default/AGENTS.md +1 -0
  49. package/corpus/core/src/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  50. package/corpus/core/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  51. package/corpus/core/src/templates/workspace-core/AGENTS.md +1 -1
  52. package/corpus/core/src/templates/workspace-root/AGENTS.md +1 -1
  53. package/corpus/core/src/vite/action-types-plugin.ts +12 -0
  54. package/corpus/templates/analytics/.agents/skills/feature-flags/SKILL.md +169 -0
  55. package/corpus/templates/analytics/AGENTS.md +15 -3
  56. package/corpus/templates/analytics/actions/list-workspace-feature-flags.ts +15 -0
  57. package/corpus/templates/analytics/actions/navigate.ts +2 -2
  58. package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +45 -0
  59. package/corpus/templates/analytics/actions/view-screen.ts +13 -0
  60. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +243 -0
  61. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +11 -3
  62. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +3 -1
  63. package/corpus/templates/analytics/app/i18n/zh-TW.ts +17 -0
  64. package/corpus/templates/analytics/app/i18n-data.ts +251 -0
  65. package/corpus/templates/analytics/app/pages/Agents.tsx +22 -2
  66. package/corpus/templates/analytics/changelog/2026-07-17-manage-feature-flag-rollouts-across-your-organization-s-apps.md +6 -0
  67. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +13 -5
  68. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +347 -0
  69. package/corpus/templates/assets/.agents/skills/feature-flags/SKILL.md +169 -0
  70. package/corpus/templates/brain/.agents/skills/feature-flags/SKILL.md +169 -0
  71. package/corpus/templates/calendar/.agents/skills/feature-flags/SKILL.md +169 -0
  72. package/corpus/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  73. package/corpus/templates/clips/.agents/skills/feature-flags/SKILL.md +169 -0
  74. package/corpus/templates/clips/server/plugins/feature-flags.ts +11 -0
  75. package/corpus/templates/clips/shared/feature-flags.ts +30 -37
  76. package/corpus/templates/content/.agents/skills/feature-flags/SKILL.md +169 -0
  77. package/corpus/templates/design/.agents/skills/feature-flags/SKILL.md +169 -0
  78. package/corpus/templates/design/AGENTS.md +5 -3
  79. package/corpus/templates/design/actions/add-fusion-screens.ts +4 -6
  80. package/corpus/templates/design/actions/apply-fusion-edits.ts +4 -6
  81. package/corpus/templates/design/actions/create-fusion-app.ts +5 -4
  82. package/corpus/templates/design/actions/deploy-fusion-app.ts +4 -3
  83. package/corpus/templates/design/actions/get-fusion-deploy-status.ts +4 -6
  84. package/corpus/templates/design/actions/list-fusion-edits.ts +4 -3
  85. package/corpus/templates/design/actions/push-fusion-app.ts +4 -6
  86. package/corpus/templates/design/actions/queue-fusion-edit.ts +4 -3
  87. package/corpus/templates/design/actions/send-fusion-message.ts +4 -6
  88. package/corpus/templates/design/actions/sync-fusion-app.ts +4 -3
  89. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  90. package/corpus/templates/design/app/pages/Index.tsx +8 -6
  91. package/corpus/templates/design/server/plugins/feature-flags.ts +5 -0
  92. package/corpus/templates/design/shared/full-app.ts +10 -7
  93. package/corpus/templates/dispatch/.agents/skills/feature-flags/SKILL.md +169 -0
  94. package/corpus/templates/forms/.agents/skills/feature-flags/SKILL.md +169 -0
  95. package/corpus/templates/macros/.agents/skills/feature-flags/SKILL.md +169 -0
  96. package/corpus/templates/mail/.agents/skills/feature-flags/SKILL.md +169 -0
  97. package/corpus/templates/plan/.agents/skills/feature-flags/SKILL.md +169 -0
  98. package/corpus/templates/slides/.agents/skills/feature-flags/SKILL.md +169 -0
  99. package/corpus/templates/tasks/.agents/skills/feature-flags/SKILL.md +169 -0
  100. package/dist/a2a/artifact-response.d.ts +5 -1
  101. package/dist/a2a/artifact-response.d.ts.map +1 -1
  102. package/dist/a2a/artifact-response.js +18 -12
  103. package/dist/a2a/artifact-response.js.map +1 -1
  104. package/dist/a2a-claims.d.ts +10 -0
  105. package/dist/a2a-claims.d.ts.map +1 -0
  106. package/dist/a2a-claims.js +41 -0
  107. package/dist/a2a-claims.js.map +1 -0
  108. package/dist/action.d.ts +6 -2
  109. package/dist/action.d.ts.map +1 -1
  110. package/dist/action.js.map +1 -1
  111. package/dist/audit/record.d.ts +3 -0
  112. package/dist/audit/record.d.ts.map +1 -1
  113. package/dist/audit/record.js +3 -0
  114. package/dist/audit/record.js.map +1 -1
  115. package/dist/client/feature-flags/FeatureFlagsPanel.d.ts +10 -0
  116. package/dist/client/feature-flags/FeatureFlagsPanel.d.ts.map +1 -0
  117. package/dist/client/feature-flags/FeatureFlagsPanel.js +143 -0
  118. package/dist/client/feature-flags/FeatureFlagsPanel.js.map +1 -0
  119. package/dist/client/feature-flags/helpers.d.ts +16 -0
  120. package/dist/client/feature-flags/helpers.d.ts.map +1 -0
  121. package/dist/client/feature-flags/helpers.js +50 -0
  122. package/dist/client/feature-flags/helpers.js.map +1 -0
  123. package/dist/client/feature-flags/index.d.ts +5 -0
  124. package/dist/client/feature-flags/index.d.ts.map +1 -0
  125. package/dist/client/feature-flags/index.js +4 -0
  126. package/dist/client/feature-flags/index.js.map +1 -0
  127. package/dist/client/feature-flags/types.d.ts +27 -0
  128. package/dist/client/feature-flags/types.d.ts.map +1 -0
  129. package/dist/client/feature-flags/types.js +2 -0
  130. package/dist/client/feature-flags/types.js.map +1 -0
  131. package/dist/client/feature-flags/use-feature-flag.d.ts +8 -0
  132. package/dist/client/feature-flags/use-feature-flag.d.ts.map +1 -0
  133. package/dist/client/feature-flags/use-feature-flag.js +15 -0
  134. package/dist/client/feature-flags/use-feature-flag.js.map +1 -0
  135. package/dist/client/index.d.ts +1 -0
  136. package/dist/client/index.d.ts.map +1 -1
  137. package/dist/client/index.js +1 -0
  138. package/dist/client/index.js.map +1 -1
  139. package/dist/collab/routes.d.ts +1 -1
  140. package/dist/collab/struct-routes.d.ts +1 -1
  141. package/dist/extensions/routes.d.ts.map +1 -1
  142. package/dist/extensions/routes.js +1 -1
  143. package/dist/extensions/routes.js.map +1 -1
  144. package/dist/feature-flags/a2a-action-route.d.ts +14 -0
  145. package/dist/feature-flags/a2a-action-route.d.ts.map +1 -0
  146. package/dist/feature-flags/a2a-action-route.js +50 -0
  147. package/dist/feature-flags/a2a-action-route.js.map +1 -0
  148. package/dist/feature-flags/actions/get-feature-flags.d.ts +3 -0
  149. package/dist/feature-flags/actions/get-feature-flags.d.ts.map +1 -0
  150. package/dist/feature-flags/actions/get-feature-flags.js +18 -0
  151. package/dist/feature-flags/actions/get-feature-flags.js.map +1 -0
  152. package/dist/feature-flags/actions/list-feature-flags.d.ts +28 -0
  153. package/dist/feature-flags/actions/list-feature-flags.d.ts.map +1 -0
  154. package/dist/feature-flags/actions/list-feature-flags.js +60 -0
  155. package/dist/feature-flags/actions/list-feature-flags.js.map +1 -0
  156. package/dist/feature-flags/actions/set-feature-flag.d.ts +26 -0
  157. package/dist/feature-flags/actions/set-feature-flag.d.ts.map +1 -0
  158. package/dist/feature-flags/actions/set-feature-flag.js +78 -0
  159. package/dist/feature-flags/actions/set-feature-flag.js.map +1 -0
  160. package/dist/feature-flags/index.d.ts +5 -0
  161. package/dist/feature-flags/index.d.ts.map +1 -0
  162. package/dist/feature-flags/index.js +5 -0
  163. package/dist/feature-flags/index.js.map +1 -0
  164. package/dist/feature-flags/permissions.d.ts +13 -0
  165. package/dist/feature-flags/permissions.d.ts.map +1 -0
  166. package/dist/feature-flags/permissions.js +37 -0
  167. package/dist/feature-flags/permissions.js.map +1 -0
  168. package/dist/feature-flags/plugin.d.ts +17 -0
  169. package/dist/feature-flags/plugin.d.ts.map +1 -0
  170. package/dist/feature-flags/plugin.js +29 -0
  171. package/dist/feature-flags/plugin.js.map +1 -0
  172. package/dist/feature-flags/registry.d.ts +23 -0
  173. package/dist/feature-flags/registry.d.ts.map +1 -0
  174. package/dist/feature-flags/registry.js +59 -0
  175. package/dist/feature-flags/registry.js.map +1 -0
  176. package/dist/feature-flags/store.d.ts +31 -0
  177. package/dist/feature-flags/store.d.ts.map +1 -0
  178. package/dist/feature-flags/store.js +124 -0
  179. package/dist/feature-flags/store.js.map +1 -0
  180. package/dist/index.d.ts +1 -0
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +1 -0
  183. package/dist/index.js.map +1 -1
  184. package/dist/integrations/a2a-continuation-processor.js +62 -5
  185. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  186. package/dist/integrations/pending-tasks-store.d.ts +3 -0
  187. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  188. package/dist/integrations/pending-tasks-store.js +75 -1
  189. package/dist/integrations/pending-tasks-store.js.map +1 -1
  190. package/dist/integrations/plugin.d.ts.map +1 -1
  191. package/dist/integrations/plugin.js +81 -5
  192. package/dist/integrations/plugin.js.map +1 -1
  193. package/dist/integrations/webhook-handler.d.ts +23 -2
  194. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  195. package/dist/integrations/webhook-handler.js +257 -35
  196. package/dist/integrations/webhook-handler.js.map +1 -1
  197. package/dist/localization/default-messages.d.ts +42 -0
  198. package/dist/localization/default-messages.d.ts.map +1 -1
  199. package/dist/localization/default-messages.js +42 -0
  200. package/dist/localization/default-messages.js.map +1 -1
  201. package/dist/notifications/routes.d.ts +2 -2
  202. package/dist/observability/routes.d.ts +1 -1
  203. package/dist/progress/routes.d.ts +1 -1
  204. package/dist/server/action-discovery.d.ts.map +1 -1
  205. package/dist/server/action-discovery.js +12 -0
  206. package/dist/server/action-discovery.js.map +1 -1
  207. package/dist/server/action-routes.d.ts +3 -0
  208. package/dist/server/action-routes.d.ts.map +1 -1
  209. package/dist/server/action-routes.js +45 -5
  210. package/dist/server/action-routes.js.map +1 -1
  211. package/dist/server/agent-chat-plugin.d.ts +6 -0
  212. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  213. package/dist/server/agent-chat-plugin.js +71 -6
  214. package/dist/server/agent-chat-plugin.js.map +1 -1
  215. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  216. package/dist/server/index.d.ts +1 -0
  217. package/dist/server/index.d.ts.map +1 -1
  218. package/dist/server/index.js +1 -0
  219. package/dist/server/index.js.map +1 -1
  220. package/dist/settings/org-settings.d.ts +2 -0
  221. package/dist/settings/org-settings.d.ts.map +1 -1
  222. package/dist/settings/org-settings.js +5 -1
  223. package/dist/settings/org-settings.js.map +1 -1
  224. package/dist/settings/store.d.ts +8 -0
  225. package/dist/settings/store.d.ts.map +1 -1
  226. package/dist/settings/store.js +48 -0
  227. package/dist/settings/store.js.map +1 -1
  228. package/dist/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  229. package/dist/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  230. package/dist/templates/default/AGENTS.md +1 -0
  231. package/dist/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  232. package/dist/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  233. package/dist/templates/workspace-core/AGENTS.md +1 -1
  234. package/dist/templates/workspace-root/AGENTS.md +1 -1
  235. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  236. package/dist/vite/action-types-plugin.js +12 -0
  237. package/dist/vite/action-types-plugin.js.map +1 -1
  238. package/docs/content/actions.mdx +48 -0
  239. package/docs/content/locales/ar-SA/actions.mdx +38 -0
  240. package/docs/content/locales/de-DE/actions.mdx +38 -0
  241. package/docs/content/locales/es-ES/actions.mdx +38 -0
  242. package/docs/content/locales/fr-FR/actions.mdx +38 -0
  243. package/docs/content/locales/hi-IN/actions.mdx +38 -0
  244. package/docs/content/locales/ja-JP/actions.mdx +38 -0
  245. package/docs/content/locales/ko-KR/actions.mdx +38 -0
  246. package/docs/content/locales/pt-BR/actions.mdx +38 -0
  247. package/docs/content/locales/zh-CN/actions.mdx +38 -0
  248. package/docs/content/locales/zh-TW/actions.mdx +38 -0
  249. package/package.json +5 -1
  250. package/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  251. package/src/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  252. package/src/templates/default/AGENTS.md +1 -0
  253. package/src/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  254. package/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  255. package/src/templates/workspace-core/AGENTS.md +1 -1
  256. package/src/templates/workspace-root/AGENTS.md +1 -1
  257. package/corpus/templates/clips/actions/get-feature-flags.ts +0 -30
@@ -0,0 +1,176 @@
1
+ import { getOrgSetting, mutateOrgSetting } from "../settings/org-settings.js";
2
+ import { getSetting, mutateSetting } from "../settings/store.js";
3
+ import {
4
+ getFeatureFlagDefinition,
5
+ type FeatureFlagDefinition,
6
+ } from "./registry.js";
7
+
8
+ export type FeatureFlagMode = "off" | "on" | "rules";
9
+
10
+ export interface FeatureFlagRules {
11
+ version: 1;
12
+ mode: FeatureFlagMode;
13
+ emails: string[];
14
+ orgIds: string[];
15
+ percentage: number;
16
+ updatedAt: number | null;
17
+ updatedBy: string | null;
18
+ }
19
+
20
+ export interface FeatureFlagScope {
21
+ userEmail?: string;
22
+ /** Canonical authenticated identity. V1 callers use normalized email. */
23
+ userKey?: string;
24
+ orgId?: string | null;
25
+ }
26
+
27
+ export const FEATURE_FLAG_SETTINGS_PREFIX = "feature-flag:";
28
+
29
+ function settingKey(key: string): string {
30
+ return `${FEATURE_FLAG_SETTINGS_PREFIX}${key}`;
31
+ }
32
+
33
+ export function defaultFeatureFlagRules(): FeatureFlagRules {
34
+ return {
35
+ version: 1,
36
+ mode: "off",
37
+ emails: [],
38
+ orgIds: [],
39
+ percentage: 0,
40
+ updatedAt: null,
41
+ updatedBy: null,
42
+ };
43
+ }
44
+
45
+ function stringList(value: unknown): string[] {
46
+ if (!Array.isArray(value)) return [];
47
+ return [
48
+ ...new Set(
49
+ value
50
+ .filter((item): item is string => typeof item === "string")
51
+ .map((item) => item.trim())
52
+ .filter(Boolean),
53
+ ),
54
+ ].sort();
55
+ }
56
+
57
+ export function normalizeFeatureFlagRules(value: unknown): FeatureFlagRules {
58
+ if (!value || typeof value !== "object") return defaultFeatureFlagRules();
59
+ const raw = value as Record<string, unknown>;
60
+ const mode: FeatureFlagMode =
61
+ raw.mode === "on" || raw.mode === "rules" || raw.mode === "off"
62
+ ? raw.mode
63
+ : "off";
64
+ const percentage =
65
+ typeof raw.percentage === "number" && Number.isFinite(raw.percentage)
66
+ ? Math.max(0, Math.min(100, Math.floor(raw.percentage)))
67
+ : 0;
68
+ return {
69
+ version: 1,
70
+ mode,
71
+ emails: stringList(raw.emails).map((email) => email.toLowerCase()),
72
+ orgIds: stringList(raw.orgIds),
73
+ percentage,
74
+ updatedAt:
75
+ typeof raw.updatedAt === "number" && Number.isSafeInteger(raw.updatedAt)
76
+ ? raw.updatedAt
77
+ : null,
78
+ updatedBy:
79
+ typeof raw.updatedBy === "string" && raw.updatedBy.trim()
80
+ ? raw.updatedBy.trim().toLowerCase()
81
+ : null,
82
+ };
83
+ }
84
+
85
+ export async function getFeatureFlagRules(
86
+ key: string,
87
+ scope: Pick<FeatureFlagScope, "orgId">,
88
+ ): Promise<FeatureFlagRules> {
89
+ if (!getFeatureFlagDefinition(key)) return defaultFeatureFlagRules();
90
+ // An organization-specific rule overrides the global rule. The fallback is
91
+ // what makes global exact-org targeting meaningful for callers in an org.
92
+ const stored = scope.orgId?.trim()
93
+ ? ((await getOrgSetting(scope.orgId, settingKey(key))) ??
94
+ (await getSetting(settingKey(key))))
95
+ : await getSetting(settingKey(key));
96
+ return normalizeFeatureFlagRules(stored);
97
+ }
98
+
99
+ /**
100
+ * Atomically derive one flag's scoped rules. An org's first override starts
101
+ * from the global fallback, then becomes independently CAS-protected.
102
+ */
103
+ export async function mutateFeatureFlagRules(
104
+ key: string,
105
+ scope: Pick<FeatureFlagScope, "orgId">,
106
+ updater: (
107
+ current: FeatureFlagRules,
108
+ ) => FeatureFlagRules | Promise<FeatureFlagRules>,
109
+ ): Promise<FeatureFlagRules> {
110
+ if (!getFeatureFlagDefinition(key)) {
111
+ throw new Error(`Unknown feature flag: ${key}`);
112
+ }
113
+ const mutate = async (stored: Record<string, unknown> | null) => {
114
+ const fallback =
115
+ stored == null && scope.orgId?.trim()
116
+ ? await getSetting(settingKey(key))
117
+ : null;
118
+ return {
119
+ ...(await updater(normalizeFeatureFlagRules(stored ?? fallback))),
120
+ };
121
+ };
122
+ const persisted = scope.orgId?.trim()
123
+ ? await mutateOrgSetting(scope.orgId, settingKey(key), mutate)
124
+ : await mutateSetting(settingKey(key), mutate);
125
+ return normalizeFeatureFlagRules(persisted);
126
+ }
127
+
128
+ function rolloutBucket(input: string): number {
129
+ // FNV-1a is deliberately tiny, deterministic, and independent of runtime.
130
+ let hash = 0x811c9dc5;
131
+ for (let index = 0; index < input.length; index += 1) {
132
+ hash ^= input.charCodeAt(index);
133
+ hash = Math.imul(hash, 0x01000193);
134
+ }
135
+ return (hash >>> 0) % 100;
136
+ }
137
+
138
+ export function evaluateFeatureFlagRules(
139
+ key: string,
140
+ rules: FeatureFlagRules,
141
+ scope: FeatureFlagScope,
142
+ ): boolean {
143
+ if (rules.mode === "off") return false;
144
+ if (rules.mode === "on") return true;
145
+ const email = scope.userEmail?.trim().toLowerCase();
146
+ if (email && rules.emails.includes(email)) return true;
147
+ if (scope.orgId && rules.orgIds.includes(scope.orgId)) return true;
148
+ const userKey = scope.userKey?.trim() || email;
149
+ if (!userKey || rules.percentage <= 0) return false;
150
+ return rolloutBucket(`${key}:${userKey}`) < rules.percentage;
151
+ }
152
+
153
+ export async function evaluateFeatureFlag(
154
+ key: string,
155
+ scope: FeatureFlagScope = {},
156
+ ): Promise<boolean> {
157
+ if (!getFeatureFlagDefinition(key)) return false;
158
+ try {
159
+ return evaluateFeatureFlagRules(
160
+ key,
161
+ await getFeatureFlagRules(key, scope),
162
+ scope,
163
+ );
164
+ } catch {
165
+ // A feature flag must never become an availability dependency.
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /** Ergonomic app-action guard. Accepts either a registered definition or its key. */
171
+ export async function isFeatureFlagEnabled(
172
+ flag: string | FeatureFlagDefinition,
173
+ scope: FeatureFlagScope = {},
174
+ ): Promise<boolean> {
175
+ return evaluateFeatureFlag(typeof flag === "string" ? flag : flag.key, scope);
176
+ }
@@ -142,6 +142,21 @@ export {
142
142
  type AutomationWorkflowDefinition,
143
143
  type JsonSchema,
144
144
  } from "./automation/index.js";
145
+ export {
146
+ createFeatureFlagsPlugin,
147
+ defineFeatureFlag,
148
+ defineFeatureFlags,
149
+ evaluateFeatureFlag,
150
+ getFeatureFlagDefinition,
151
+ getFeatureFlagRules,
152
+ isFeatureFlagEnabled,
153
+ listFeatureFlags,
154
+ registerFeatureFlags,
155
+ type FeatureFlagDefinition,
156
+ type FeatureFlagMode,
157
+ type FeatureFlagRules,
158
+ type FeatureFlagScope,
159
+ } from "./feature-flags/index.js";
145
160
 
146
161
  // Server
147
162
  export {
@@ -171,11 +171,22 @@ async function processClaimedContinuation(
171
171
  ...(auth.apiKeyFallbacks ? { fallbackApiKeys: auth.apiKeyFallbacks } : {}),
172
172
  });
173
173
  const deadline = Date.now() + PROCESSOR_WAIT_MS;
174
+ const recoverableArtifactSecrets =
175
+ await resolveContinuationArtifactSecrets(continuation);
174
176
  let task: Task | null = null;
177
+ let latestRecoverableArtifactText: string | null = null;
175
178
 
176
179
  try {
177
180
  while (Date.now() < deadline) {
178
181
  task = await client.getTask(continuation.a2aTaskId);
182
+ const recoverableArtifactText = extractVerifiedRecoverableArtifactText(
183
+ task,
184
+ continuation.agentUrl,
185
+ recoverableArtifactSecrets,
186
+ );
187
+ if (recoverableArtifactText) {
188
+ latestRecoverableArtifactText = recoverableArtifactText;
189
+ }
179
190
  if (TERMINAL_STATES.has(task.status.state)) break;
180
191
  await reportA2AContinuationProgress(continuation, progress, task);
181
192
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
@@ -183,6 +194,17 @@ async function processClaimedContinuation(
183
194
  } catch (err) {
184
195
  if (isTransientA2APollError(err)) {
185
196
  if (shouldStopPollingRemoteTask(continuation)) {
197
+ if (latestRecoverableArtifactText) {
198
+ await deliverAndCompleteA2AContinuation(
199
+ continuation,
200
+ adapter,
201
+ formatRecoverableArtifactFallbackText(
202
+ latestRecoverableArtifactText,
203
+ ),
204
+ progress,
205
+ );
206
+ return;
207
+ }
186
208
  await notifyAndFailA2AContinuation(
187
209
  continuation,
188
210
  adapter,
@@ -209,6 +231,15 @@ async function processClaimedContinuation(
209
231
 
210
232
  if (!task || !TERMINAL_STATES.has(task.status.state)) {
211
233
  if (shouldStopPollingRemoteTask(continuation)) {
234
+ if (latestRecoverableArtifactText) {
235
+ await deliverAndCompleteA2AContinuation(
236
+ continuation,
237
+ adapter,
238
+ formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
239
+ progress,
240
+ );
241
+ return;
242
+ }
212
243
  await notifyAndFailA2AContinuation(
213
244
  continuation,
214
245
  adapter,
@@ -222,6 +253,15 @@ async function processClaimedContinuation(
222
253
  }
223
254
 
224
255
  if (task.status.state !== "completed") {
256
+ if (latestRecoverableArtifactText) {
257
+ await deliverAndCompleteA2AContinuation(
258
+ continuation,
259
+ adapter,
260
+ formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
261
+ progress,
262
+ );
263
+ return;
264
+ }
225
265
  const reason =
226
266
  extractTaskText(task) ||
227
267
  `Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
@@ -234,6 +274,15 @@ async function processClaimedContinuation(
234
274
  continuation.agentUrl,
235
275
  );
236
276
  if (!text.trim()) {
277
+ if (latestRecoverableArtifactText) {
278
+ await deliverAndCompleteA2AContinuation(
279
+ continuation,
280
+ adapter,
281
+ formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
282
+ progress,
283
+ );
284
+ return;
285
+ }
237
286
  await notifyAndFailA2AContinuation(
238
287
  continuation,
239
288
  adapter,
@@ -382,6 +431,8 @@ async function deliverAndCompleteA2AContinuation(
382
431
  `${deliveryContinuation.platform} response delivery timed out`,
383
432
  );
384
433
  let persistenceError: unknown;
434
+ const artifactSecrets =
435
+ await resolveContinuationArtifactSecrets(deliveryContinuation);
385
436
  for (let attempt = 0; attempt < 3; attempt += 1) {
386
437
  try {
387
438
  await persistA2AContinuationDelivery(
@@ -389,6 +440,7 @@ async function deliverAndCompleteA2AContinuation(
389
440
  outgoing,
390
441
  deliveryReceipt,
391
442
  text,
443
+ artifactSecrets,
392
444
  );
393
445
  persistenceError = undefined;
394
446
  break;
@@ -463,6 +515,7 @@ async function persistA2AContinuationDelivery(
463
515
  outgoing: OutgoingMessage,
464
516
  receipt: PlatformDeliveryReceipt,
465
517
  artifactText: string,
518
+ artifactSecrets: readonly string[],
466
519
  ): Promise<void> {
467
520
  const mapping = await getThreadMapping(
468
521
  continuation.platform,
@@ -480,9 +533,12 @@ async function persistA2AContinuationDelivery(
480
533
  }
481
534
  if (!Array.isArray(repo.messages)) repo.messages = [];
482
535
 
483
- const artifacts = extractA2AArtifactIdentities([
484
- { tool: "call-agent", result: artifactText },
485
- ]);
536
+ const artifacts = extractA2AArtifactIdentities(
537
+ [{ tool: "call-agent", result: artifactText }],
538
+ {
539
+ persistedArtifactSecrets: artifactSecrets,
540
+ },
541
+ );
486
542
  const metadata: Record<string, unknown> = {
487
543
  integrationDeliveryAttempted: true,
488
544
  integrationDelivery: {
@@ -726,6 +782,24 @@ async function signFreshContinuationTokens(
726
782
  return tokens;
727
783
  }
728
784
 
785
+ async function resolveContinuationArtifactSecrets(
786
+ continuation: A2AContinuation,
787
+ ): Promise<string[]> {
788
+ const secrets: string[] = [];
789
+ const add = (secret: string | null | undefined) => {
790
+ const value = secret?.trim();
791
+ if (value && !secrets.includes(value)) secrets.push(value);
792
+ };
793
+ add(process.env.A2A_SECRET);
794
+ if (continuation.orgId) {
795
+ try {
796
+ const { getOrgA2ASecret } = await import("../org/context.js");
797
+ add(await getOrgA2ASecret(continuation.orgId));
798
+ } catch {}
799
+ }
800
+ return secrets;
801
+ }
802
+
729
803
  function isLikelyJwt(token: string): boolean {
730
804
  return token.split(".").length === 3;
731
805
  }
@@ -740,6 +814,36 @@ function extractTaskText(task: Task): string {
740
814
  .join("\n");
741
815
  }
742
816
 
817
+ function extractVerifiedRecoverableArtifactText(
818
+ task: Task,
819
+ agentUrl: string,
820
+ artifactSecrets: readonly string[],
821
+ ): string | null {
822
+ if (task.status.message?.metadata?.agentNativeRecoverableArtifacts !== true) {
823
+ return null;
824
+ }
825
+
826
+ const text = formatContinuationArtifactText(extractTaskText(task), agentUrl);
827
+ if (!text.trim()) return null;
828
+
829
+ // Require the signed identity ledger so arbitrary peer progress prose cannot
830
+ // prematurely complete the continuation.
831
+ const artifacts = extractA2AArtifactIdentities(
832
+ [{ tool: "call-agent", result: text }],
833
+ {
834
+ persistedArtifactSecrets: artifactSecrets,
835
+ },
836
+ );
837
+ return artifacts.length > 0 ? text : null;
838
+ }
839
+
840
+ function formatRecoverableArtifactFallbackText(text: string): string {
841
+ return text.replace(
842
+ "The agent is still working on the full response, but these verified artifacts already exist:",
843
+ "The downstream agent did not finish its full response, but these verified artifacts already exist:",
844
+ );
845
+ }
846
+
743
847
  function formatContinuationArtifactText(
744
848
  text: string,
745
849
  agentUrl: string,
@@ -272,6 +272,19 @@ export async function claimPendingTask(
272
272
  AND active.status = 'processing'
273
273
  AND active.id <> integration_pending_tasks.id
274
274
  )
275
+ AND NOT EXISTS (
276
+ SELECT 1 FROM integration_pending_tasks earlier
277
+ WHERE earlier.platform = integration_pending_tasks.platform
278
+ AND earlier.external_thread_id = integration_pending_tasks.external_thread_id
279
+ AND earlier.status = 'pending'
280
+ AND (
281
+ earlier.created_at < integration_pending_tasks.created_at
282
+ OR (
283
+ earlier.created_at = integration_pending_tasks.created_at
284
+ AND earlier.id < integration_pending_tasks.id
285
+ )
286
+ )
287
+ )
275
288
  RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at`
276
289
  : `UPDATE integration_pending_tasks
277
290
  SET status = ?, attempts = attempts + 1, updated_at = ?
@@ -282,6 +295,19 @@ export async function claimPendingTask(
282
295
  AND active.external_thread_id = integration_pending_tasks.external_thread_id
283
296
  AND active.status = 'processing'
284
297
  AND active.id <> integration_pending_tasks.id
298
+ )
299
+ AND NOT EXISTS (
300
+ SELECT 1 FROM integration_pending_tasks earlier
301
+ WHERE earlier.platform = integration_pending_tasks.platform
302
+ AND earlier.external_thread_id = integration_pending_tasks.external_thread_id
303
+ AND earlier.status = 'pending'
304
+ AND (
305
+ earlier.created_at < integration_pending_tasks.created_at
306
+ OR (
307
+ earlier.created_at = integration_pending_tasks.created_at
308
+ AND earlier.id < integration_pending_tasks.id
309
+ )
310
+ )
285
311
  )`,
286
312
  args: ["processing", now, id],
287
313
  });
@@ -312,7 +338,7 @@ export async function getNextPendingTaskIdForThread(
312
338
  const { rows } = await getDbExec().execute({
313
339
  sql: `SELECT id FROM integration_pending_tasks
314
340
  WHERE platform = ? AND external_thread_id = ? AND status = 'pending'
315
- ORDER BY created_at ASC LIMIT 1`,
341
+ ORDER BY created_at ASC, id ASC LIMIT 1`,
316
342
  args: [platform, externalThreadId],
317
343
  });
318
344
  return rows[0]?.id ? String(rows[0].id) : null;
@@ -354,6 +380,77 @@ export async function markTaskRetryable(
354
380
  });
355
381
  }
356
382
 
383
+ export async function stageTaskDeliveryPayload(
384
+ id: string,
385
+ payload: string,
386
+ ): Promise<void> {
387
+ await ensureTable();
388
+ const client = getDbExec();
389
+ const now = Date.now();
390
+ const result = await client.execute({
391
+ sql: `UPDATE integration_pending_tasks
392
+ SET payload = ?, updated_at = ?
393
+ WHERE id = ? AND status = 'processing'`,
394
+ args: [payload, now, id],
395
+ });
396
+ const affected = Number(
397
+ (result as { rowsAffected?: number }).rowsAffected ??
398
+ (result as { rowCount?: number }).rowCount ??
399
+ 0,
400
+ );
401
+ if (affected === 0) {
402
+ throw new Error("Integration task is no longer claimable for delivery");
403
+ }
404
+ }
405
+
406
+ export async function markTaskDeliveryRetryable(
407
+ id: string,
408
+ payload: string,
409
+ errorMessage: string,
410
+ ): Promise<void> {
411
+ await ensureTable();
412
+ const client = getDbExec();
413
+ const result = await client.execute({
414
+ sql: `UPDATE integration_pending_tasks
415
+ SET status = ?, payload = ?, updated_at = ?, error_message = ?
416
+ WHERE id = ? AND status = 'processing'`,
417
+ args: ["pending", payload, Date.now(), errorMessage.slice(0, 2000), id],
418
+ });
419
+ const affected = Number(
420
+ (result as { rowsAffected?: number }).rowsAffected ??
421
+ (result as { rowCount?: number }).rowCount ??
422
+ 0,
423
+ );
424
+ if (affected === 0) {
425
+ throw new Error(
426
+ "Integration task is no longer claimable for delivery retry",
427
+ );
428
+ }
429
+ }
430
+
431
+ export async function failTaskDeliveryTransition(
432
+ id: string,
433
+ errorMessage: string,
434
+ ): Promise<void> {
435
+ await ensureTable();
436
+ const result = await getDbExec().execute({
437
+ sql: `UPDATE integration_pending_tasks
438
+ SET status = ?, updated_at = ?, error_message = ?, payload = ?
439
+ WHERE id = ? AND status = 'processing'`,
440
+ args: ["failed", Date.now(), errorMessage.slice(0, 2000), "{}", id],
441
+ });
442
+ const affected = Number(
443
+ (result as { rowsAffected?: number }).rowsAffected ??
444
+ (result as { rowCount?: number }).rowCount ??
445
+ 0,
446
+ );
447
+ if (affected === 0) {
448
+ throw new Error(
449
+ "Integration task delivery failure transition lost its race",
450
+ );
451
+ }
452
+ }
453
+
357
454
  /** Mark a task as failed and stash an error message. */
358
455
  export async function markTaskFailed(
359
456
  id: string,