@agent-native/core 0.95.1 → 0.96.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 (224) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/a2a/client.ts +11 -1
  5. package/corpus/core/src/agent/production-agent.ts +87 -27
  6. package/corpus/core/src/agent/thread-data-builder.ts +7 -5
  7. package/corpus/core/src/chat-threads/store.ts +20 -1
  8. package/corpus/core/src/cli/code-agent-executor.ts +1 -0
  9. package/corpus/core/src/cli/code-agent-runs.ts +14 -0
  10. package/corpus/core/src/client/AgentPanel.tsx +32 -25
  11. package/corpus/core/src/client/AssistantChat.tsx +36 -20
  12. package/corpus/core/src/client/MultiTabAssistantChat.tsx +141 -90
  13. package/corpus/core/src/client/chat/ChatHistoryList.tsx +404 -0
  14. package/corpus/core/src/client/chat/index.ts +6 -0
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +37 -3
  16. package/corpus/core/src/client/code-agent-chat-adapter.ts +72 -4
  17. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +52 -11
  18. package/corpus/core/src/client/composer/TiptapComposer.tsx +157 -20
  19. package/corpus/core/src/client/composer/VoiceButton.tsx +28 -0
  20. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +5 -0
  21. package/corpus/core/src/client/conversation/code-agent-transcript.ts +4 -5
  22. package/corpus/core/src/client/guided-questions.tsx +2 -1
  23. package/corpus/core/src/client/index.ts +11 -0
  24. package/corpus/core/src/client/use-chat-threads.ts +10 -1
  25. package/corpus/core/src/client/voice-provider-status.ts +44 -0
  26. package/corpus/core/src/code-agents/transcript-normalizer.ts +76 -0
  27. package/corpus/core/src/deploy/build.ts +12 -11
  28. package/corpus/core/src/integrations/a2a-continuation-processor.ts +35 -5
  29. package/corpus/core/src/integrations/adapters/slack.ts +74 -21
  30. package/corpus/core/src/integrations/webhook-handler.ts +14 -0
  31. package/corpus/core/src/localization/default-messages.ts +6 -0
  32. package/corpus/core/src/observability/hosted-model-experiment.ts +118 -0
  33. package/corpus/core/src/observability/routes.ts +17 -0
  34. package/corpus/core/src/observability/store.ts +4 -1
  35. package/corpus/core/src/observability/traces.ts +41 -2
  36. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +19 -1
  37. package/corpus/core/src/scripts/call-agent.ts +53 -3
  38. package/corpus/core/src/scripts/chat/search-chats.ts +11 -8
  39. package/corpus/core/src/secrets/register-framework-secrets.ts +1 -1
  40. package/corpus/core/src/server/agent-chat-plugin.ts +41 -6
  41. package/corpus/core/src/server/core-routes-plugin.ts +15 -0
  42. package/corpus/core/src/server/realtime-voice.ts +71 -24
  43. package/corpus/core/src/server/voice-providers-status.ts +4 -3
  44. package/corpus/core/src/shared/cache-control.ts +2 -8
  45. package/corpus/core/src/styles/agent-native.css +1 -0
  46. package/corpus/core/src/styles/chat-history-list.css +313 -0
  47. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +20 -0
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +8 -4
  49. package/corpus/core/src/vite/client.ts +1 -0
  50. package/corpus/templates/analytics/AGENTS.md +7 -0
  51. package/corpus/templates/analytics/actions/bigquery.ts +26 -2
  52. package/corpus/templates/analytics/actions/data-source-status.ts +18 -0
  53. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +5 -4
  54. package/corpus/templates/analytics/app/global.css +10 -0
  55. package/corpus/templates/analytics/app/pages/Ask.tsx +25 -2
  56. package/corpus/templates/analytics/changelog/2026-07-10-analytics-answers-simple-time-bounded-metrics-without-unnece.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-07-10-analytics-chat-reliably-uses-connected-data-sources-across-s.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-07-10-chat-threads-now-stay-clear-of-top-actions-and-fade-smoothly.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-07-10-context-chips-can-now-be-selected-and-removed-one-at-a-time-.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-07-10-the-ask-composer-no-longer-carries-dashboard-or-panel-contex.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-07-10-the-ask-section-stays-highlighted-while-its-chat-history-is-.md +6 -0
  62. package/corpus/templates/analytics/server/lib/bigquery.ts +90 -14
  63. package/corpus/templates/analytics/server/plugins/agent-chat.ts +82 -7
  64. package/dist/a2a/client.d.ts +7 -0
  65. package/dist/a2a/client.d.ts.map +1 -1
  66. package/dist/a2a/client.js +2 -1
  67. package/dist/a2a/client.js.map +1 -1
  68. package/dist/agent/production-agent.d.ts +6 -0
  69. package/dist/agent/production-agent.d.ts.map +1 -1
  70. package/dist/agent/production-agent.js +63 -26
  71. package/dist/agent/production-agent.js.map +1 -1
  72. package/dist/agent/thread-data-builder.d.ts +1 -0
  73. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  74. package/dist/agent/thread-data-builder.js +6 -5
  75. package/dist/agent/thread-data-builder.js.map +1 -1
  76. package/dist/chat-threads/store.d.ts +10 -0
  77. package/dist/chat-threads/store.d.ts.map +1 -1
  78. package/dist/chat-threads/store.js +6 -0
  79. package/dist/chat-threads/store.js.map +1 -1
  80. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  81. package/dist/cli/code-agent-executor.js +1 -0
  82. package/dist/cli/code-agent-executor.js.map +1 -1
  83. package/dist/cli/code-agent-runs.d.ts +12 -0
  84. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  85. package/dist/cli/code-agent-runs.js +1 -0
  86. package/dist/cli/code-agent-runs.js.map +1 -1
  87. package/dist/client/AgentPanel.d.ts.map +1 -1
  88. package/dist/client/AgentPanel.js +3 -3
  89. package/dist/client/AgentPanel.js.map +1 -1
  90. package/dist/client/AssistantChat.d.ts +11 -0
  91. package/dist/client/AssistantChat.d.ts.map +1 -1
  92. package/dist/client/AssistantChat.js +7 -4
  93. package/dist/client/AssistantChat.js.map +1 -1
  94. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  95. package/dist/client/MultiTabAssistantChat.js +83 -10
  96. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  97. package/dist/client/chat/ChatHistoryList.d.ts +88 -0
  98. package/dist/client/chat/ChatHistoryList.d.ts.map +1 -0
  99. package/dist/client/chat/ChatHistoryList.js +83 -0
  100. package/dist/client/chat/ChatHistoryList.js.map +1 -0
  101. package/dist/client/chat/index.d.ts +1 -0
  102. package/dist/client/chat/index.d.ts.map +1 -1
  103. package/dist/client/chat/index.js +1 -0
  104. package/dist/client/chat/index.js.map +1 -1
  105. package/dist/client/chat/tool-call-display.d.ts +13 -1
  106. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  107. package/dist/client/chat/tool-call-display.js +10 -2
  108. package/dist/client/chat/tool-call-display.js.map +1 -1
  109. package/dist/client/code-agent-chat-adapter.d.ts +17 -1
  110. package/dist/client/code-agent-chat-adapter.d.ts.map +1 -1
  111. package/dist/client/code-agent-chat-adapter.js +47 -3
  112. package/dist/client/code-agent-chat-adapter.js.map +1 -1
  113. package/dist/client/composer/RealtimeVoiceMode.d.ts +11 -1
  114. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  115. package/dist/client/composer/RealtimeVoiceMode.js +5 -3
  116. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  117. package/dist/client/composer/TiptapComposer.d.ts +25 -2
  118. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  119. package/dist/client/composer/TiptapComposer.js +96 -5
  120. package/dist/client/composer/TiptapComposer.js.map +1 -1
  121. package/dist/client/composer/VoiceButton.d.ts.map +1 -1
  122. package/dist/client/composer/VoiceButton.js +21 -1
  123. package/dist/client/composer/VoiceButton.js.map +1 -1
  124. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  125. package/dist/client/composer/useRealtimeVoiceMode.js +5 -0
  126. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  127. package/dist/client/conversation/code-agent-transcript.d.ts +2 -0
  128. package/dist/client/conversation/code-agent-transcript.d.ts.map +1 -1
  129. package/dist/client/conversation/code-agent-transcript.js +3 -5
  130. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  131. package/dist/client/guided-questions.d.ts.map +1 -1
  132. package/dist/client/guided-questions.js +2 -1
  133. package/dist/client/guided-questions.js.map +1 -1
  134. package/dist/client/index.d.ts +3 -1
  135. package/dist/client/index.d.ts.map +1 -1
  136. package/dist/client/index.js +3 -1
  137. package/dist/client/index.js.map +1 -1
  138. package/dist/client/use-chat-threads.d.ts.map +1 -1
  139. package/dist/client/use-chat-threads.js +9 -1
  140. package/dist/client/use-chat-threads.js.map +1 -1
  141. package/dist/client/voice-provider-status.d.ts +10 -0
  142. package/dist/client/voice-provider-status.d.ts.map +1 -0
  143. package/dist/client/voice-provider-status.js +32 -0
  144. package/dist/client/voice-provider-status.js.map +1 -0
  145. package/dist/code-agents/transcript-normalizer.d.ts +29 -0
  146. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  147. package/dist/code-agents/transcript-normalizer.js +55 -0
  148. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  149. package/dist/collab/routes.d.ts +1 -1
  150. package/dist/collab/struct-routes.d.ts +1 -1
  151. package/dist/deploy/build.d.ts.map +1 -1
  152. package/dist/deploy/build.js +13 -11
  153. package/dist/deploy/build.js.map +1 -1
  154. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  155. package/dist/integrations/a2a-continuation-processor.js +25 -5
  156. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  157. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  158. package/dist/integrations/adapters/slack.js +55 -21
  159. package/dist/integrations/adapters/slack.js.map +1 -1
  160. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  161. package/dist/integrations/webhook-handler.js +11 -0
  162. package/dist/integrations/webhook-handler.js.map +1 -1
  163. package/dist/localization/default-messages.d.ts +5 -0
  164. package/dist/localization/default-messages.d.ts.map +1 -1
  165. package/dist/localization/default-messages.js +5 -0
  166. package/dist/localization/default-messages.js.map +1 -1
  167. package/dist/notifications/routes.d.ts +2 -2
  168. package/dist/observability/hosted-model-experiment.d.ts +39 -0
  169. package/dist/observability/hosted-model-experiment.d.ts.map +1 -0
  170. package/dist/observability/hosted-model-experiment.js +90 -0
  171. package/dist/observability/hosted-model-experiment.js.map +1 -0
  172. package/dist/observability/routes.d.ts +1 -1
  173. package/dist/observability/routes.d.ts.map +1 -1
  174. package/dist/observability/routes.js +16 -0
  175. package/dist/observability/routes.js.map +1 -1
  176. package/dist/observability/store.d.ts.map +1 -1
  177. package/dist/observability/store.js +4 -1
  178. package/dist/observability/store.js.map +1 -1
  179. package/dist/observability/traces.d.ts +6 -0
  180. package/dist/observability/traces.d.ts.map +1 -1
  181. package/dist/observability/traces.js +28 -2
  182. package/dist/observability/traces.js.map +1 -1
  183. package/dist/resources/handlers.d.ts +1 -1
  184. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  185. package/dist/scripts/agent-engines/list-agent-engines.js +19 -1
  186. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  187. package/dist/scripts/call-agent.d.ts.map +1 -1
  188. package/dist/scripts/call-agent.js +43 -3
  189. package/dist/scripts/call-agent.js.map +1 -1
  190. package/dist/scripts/chat/search-chats.d.ts +1 -1
  191. package/dist/scripts/chat/search-chats.d.ts.map +1 -1
  192. package/dist/scripts/chat/search-chats.js +11 -8
  193. package/dist/scripts/chat/search-chats.js.map +1 -1
  194. package/dist/secrets/register-framework-secrets.js +1 -1
  195. package/dist/secrets/register-framework-secrets.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts +11 -0
  197. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  198. package/dist/server/agent-chat-plugin.js +27 -6
  199. package/dist/server/agent-chat-plugin.js.map +1 -1
  200. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  201. package/dist/server/core-routes-plugin.d.ts +3 -0
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/realtime-voice.d.ts.map +1 -1
  206. package/dist/server/realtime-voice.js +55 -21
  207. package/dist/server/realtime-voice.js.map +1 -1
  208. package/dist/server/voice-providers-status.d.ts.map +1 -1
  209. package/dist/server/voice-providers-status.js +2 -3
  210. package/dist/server/voice-providers-status.js.map +1 -1
  211. package/dist/shared/cache-control.d.ts +4 -6
  212. package/dist/shared/cache-control.d.ts.map +1 -1
  213. package/dist/shared/cache-control.js +2 -7
  214. package/dist/shared/cache-control.js.map +1 -1
  215. package/dist/styles/agent-native.css +1 -0
  216. package/dist/styles/chat-history-list.css +313 -0
  217. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +20 -0
  218. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +8 -4
  219. package/dist/vite/client.d.ts.map +1 -1
  220. package/dist/vite/client.js +1 -0
  221. package/dist/vite/client.js.map +1 -1
  222. package/package.json +3 -1
  223. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +20 -0
  224. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +8 -4
@@ -61,6 +61,16 @@ export interface NormalizedCodeAgentToolEvent extends NormalizedCodeAgentTranscr
61
61
  * it. Absent on old transcript events — UI must handle both cases.
62
62
  */
63
63
  structuredMeta?: Record<string, unknown>;
64
+ /**
65
+ * Stable approval id extracted from the synthetic "Approval required..."
66
+ * bash result (see `requestCodeAgentApproval` in `cli/code-agent-executor.ts`)
67
+ * when this exact approval has not yet been resolved elsewhere in the
68
+ * transcript (approved / denied / allowlisted / forbidden). Consumers attach
69
+ * this as `approval: { approvalKey }` on the rendered tool-call content part
70
+ * so the shared `ApprovalAffordance` can render inline. Absent once a later
71
+ * transcript event records a resolution for this approval id.
72
+ */
73
+ pendingApprovalKey?: string;
64
74
  }
65
75
 
66
76
  export interface NormalizedCodeAgentStatusEvent extends NormalizedCodeAgentTranscriptBase {
@@ -70,6 +80,7 @@ export interface NormalizedCodeAgentStatusEvent extends NormalizedCodeAgentTrans
70
80
  statusKind: CodeAgentTranscriptEvent["kind"];
71
81
  status?: string;
72
82
  phase?: string;
83
+ signal?: CodeAgentTranscriptEvent["signal"];
73
84
  metadata?: Record<string, unknown>;
74
85
  }
75
86
 
@@ -82,6 +93,35 @@ export interface NormalizedCodeAgentThinkingEvent extends NormalizedCodeAgentTra
82
93
  text: string;
83
94
  }
84
95
 
96
+ /** Structured signal value stamped on the "no LLM provider key" status event. */
97
+ export const CREDENTIAL_GAP_SIGNAL: NonNullable<
98
+ CodeAgentTranscriptEvent["signal"]
99
+ > = "credential-gap";
100
+
101
+ /**
102
+ * Shared "credential gap" detection for code-agent transcript events and the
103
+ * normalized status items built from them. Prefers the structured `signal`
104
+ * field the executor stamps on the event (see `code-agent-executor.ts`); only
105
+ * falls back to matching the legacy hint text for transcripts persisted
106
+ * before the structured signal existed. Accepts either a raw
107
+ * `CodeAgentTranscriptEvent` (`message`) or a `NormalizedCodeAgentStatusEvent`
108
+ * (`text`), and any of the other UI-facing transcript event shapes that carry
109
+ * the same field names, so every consumer can share one implementation
110
+ * instead of re-implementing the regex.
111
+ */
112
+ export function isCredentialGapCodeAgentEvent(event: {
113
+ signal?: string;
114
+ text?: string;
115
+ message?: string;
116
+ }): boolean {
117
+ if (event.signal === CREDENTIAL_GAP_SIGNAL) return true;
118
+ return isLegacyCredentialGapHintText(event.text ?? event.message ?? "");
119
+ }
120
+
121
+ function isLegacyCredentialGapHintText(value: string): boolean {
122
+ return /No LLM provider key was found|Missing credentials/i.test(value);
123
+ }
124
+
85
125
  export function normalizeCodeAgentTranscript(
86
126
  events: readonly CodeAgentTranscriptEvent[],
87
127
  ): NormalizedCodeAgentTranscript {
@@ -148,6 +188,7 @@ export function normalizeCodeAgentTranscript(
148
188
  hiddenEvents.sort(
149
189
  (a, b) => (eventOrder.get(a.id) ?? 0) - (eventOrder.get(b.id) ?? 0),
150
190
  );
191
+ applyPendingCodeAgentApprovalKeys(dedupedItems, events);
151
192
 
152
193
  return {
153
194
  items: dedupedItems,
@@ -156,6 +197,40 @@ export function normalizeCodeAgentTranscript(
156
197
  };
157
198
  }
158
199
 
200
+ /**
201
+ * Stamp `pendingApprovalKey` onto completed bash tool events whose synthetic
202
+ * result carries an "Approval id: {id}" marker (see `requestCodeAgentApproval`
203
+ * in `cli/code-agent-executor.ts`), unless a later raw event already recorded
204
+ * a resolution for that same id (approved / denied / allowlisted-and-run /
205
+ * forbidden — all stamp `metadata.approvalId`).
206
+ *
207
+ * Resolution lookup scans the *raw* event stream rather than the normalized
208
+ * items: resolution status events are intentionally low-signal (they read as
209
+ * "status: running") and get folded into `hiddenEvents` by
210
+ * `isLowSignalLifecycleEvent`, so they would not otherwise be visible here.
211
+ */
212
+ function applyPendingCodeAgentApprovalKeys(
213
+ items: NormalizedCodeAgentTranscriptItem[],
214
+ events: readonly CodeAgentTranscriptEvent[],
215
+ ): void {
216
+ const resolvedApprovalIds = new Set<string>();
217
+ for (const event of events) {
218
+ const approvalId = stringMetadata(event.metadata, "approvalId");
219
+ if (approvalId) resolvedApprovalIds.add(approvalId);
220
+ }
221
+ for (const item of items) {
222
+ if (item.type !== "tool" || item.state !== "completed") continue;
223
+ if (typeof item.result !== "string") continue;
224
+ const match = CODE_AGENT_APPROVAL_ID_RESULT_PATTERN.exec(item.result);
225
+ const id = match?.[1];
226
+ if (id && !resolvedApprovalIds.has(id)) {
227
+ item.pendingApprovalKey = id;
228
+ }
229
+ }
230
+ }
231
+
232
+ const CODE_AGENT_APPROVAL_ID_RESULT_PATTERN = /Approval id:\s*(\S+)/;
233
+
159
234
  function createUserTurn(
160
235
  event: CodeAgentTranscriptEvent,
161
236
  turnIndex: number,
@@ -253,6 +328,7 @@ function createStatusEvent(
253
328
  statusKind: event.kind,
254
329
  status: stringMetadata(metadata, "status"),
255
330
  phase: stringMetadata(metadata, "phase"),
331
+ signal: event.signal,
256
332
  metadata,
257
333
  createdAt: event.createdAt,
258
334
  updatedAt: event.createdAt,
@@ -32,7 +32,6 @@ import { normalizeAppBasePath } from "../server/app-base-path.js";
32
32
  import {
33
33
  DEFAULT_SSR_CDN_CACHE_CONTROL,
34
34
  DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL,
35
- DEFAULT_SSR_NETLIFY_VARY,
36
35
  DEFAULT_SPECULATION_RULES_PATH,
37
36
  DEFAULT_SSR_CACHE_CONTROL,
38
37
  } from "../shared/cache-control.js";
@@ -909,7 +908,6 @@ function injectHeadScript(html, script) {
909
908
  const DEFAULT_SSR_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CACHE_CONTROL)};
910
909
  const DEFAULT_SSR_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CDN_CACHE_CONTROL)};
911
910
  const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL)};
912
- const DEFAULT_SSR_NETLIFY_VARY = ${JSON.stringify(DEFAULT_SSR_NETLIFY_VARY)};
913
911
  const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
914
912
  const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
915
913
  const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify(
@@ -986,17 +984,21 @@ function isSsrHtmlOrDataResponse(headers, status, pathname) {
986
984
  /**
987
985
  * Apply the SSR cache policy to the response headers.
988
986
  *
989
- * SSR IS A PUBLIC, HARD-CDN-CACHED SHELL SERVED IDENTICALLY TO EVERYONE.
990
- * Every SSR HTML / React Router .data response gets the same public
991
- * stale-while-revalidate policy for ALL visitors, authenticated or not. The SSR
992
- * output is impersonal (the handler never reads the request's session/cookies),
993
- * so it is safe to hard-cache one shared copy at the edge. Do NOT reintroduce
994
- * per-user / cookie-based cache variation here (no private, no no-store, no
995
- * "authenticated then don't cache" branch) — that makes every logged-in
996
- * visitor's pages uncacheable. Per-user state is resolved client-side instead.
987
+ * Unannotated SSR HTML and React Router .data responses share a public
988
+ * stale-while-revalidate cache policy. Preserve an explicit route policy,
989
+ * though: token-gated and personalized routes can opt out with private or
990
+ * no-store policies.
997
991
  */
998
992
  function applyDefaultSsrCacheHeader(headers, status, pathname) {
999
993
  if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
994
+ // React Router marks every .data response as no-cache, but those loader
995
+ // payloads are safe to cache at the shared edge. Preserve only explicit
996
+ // private/no-store policies, which token-gated and personalized routes use
997
+ // to opt out of shared caching.
998
+ const cacheControl = headers.get("cache-control")?.toLowerCase() ?? "";
999
+ if (cacheControl.includes("private") || cacheControl.includes("no-store")) {
1000
+ return;
1001
+ }
1000
1002
 
1001
1003
  headers.set("cache-control", DEFAULT_SSR_CACHE_CONTROL);
1002
1004
  headers.set("cdn-cache-control", DEFAULT_SSR_CDN_CACHE_CONTROL);
@@ -1005,7 +1007,6 @@ function applyDefaultSsrCacheHeader(headers, status, pathname) {
1005
1007
  // Netlify-specific header so SSR HTML/.data are served from the shared
1006
1008
  // durable CDN cache instead of stampeding origin — for every visitor.
1007
1009
  headers.set("netlify-cdn-cache-control", DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL);
1008
- headers.set("netlify-vary", DEFAULT_SSR_NETLIFY_VARY);
1009
1010
  }
1010
1011
 
1011
1012
  function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
@@ -4,6 +4,7 @@ import type { Task } from "../a2a/types.js";
4
4
  import {
5
5
  formatLlmCredentialErrorMessage,
6
6
  isLlmCredentialError,
7
+ LLM_MISSING_CREDENTIALS_ERROR_CODE,
7
8
  } from "../agent/engine/credential-errors.js";
8
9
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
9
10
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
@@ -457,15 +458,44 @@ function formatContinuationFailureMessage(
457
458
  continuation: A2AContinuation,
458
459
  reason: string,
459
460
  ): string {
460
- if (isLlmCredentialError(reason)) {
461
- return formatLlmCredentialErrorMessage({
462
- agentName: continuation.agentName,
463
- });
461
+ const explicitCode = extractFailureCode(reason);
462
+ const diagnostics = formatContinuationFailureDiagnostics(
463
+ continuation,
464
+ reason,
465
+ );
466
+ if (isLlmCredentialError(reason, explicitCode)) {
467
+ return (
468
+ formatLlmCredentialErrorMessage({
469
+ agentName: continuation.agentName,
470
+ }) + diagnostics
471
+ );
464
472
  }
465
473
 
466
474
  return `The ${continuation.agentName} agent could not finish this request: ${sanitizeFailureReason(
467
475
  reason,
468
- )}`;
476
+ )}${diagnostics}`;
477
+ }
478
+
479
+ function formatContinuationFailureDiagnostics(
480
+ continuation: A2AContinuation,
481
+ reason: string,
482
+ ): string {
483
+ return `\n\nError code: \`${continuationFailureCode(reason)}\`\nRequest ID: \`${continuation.integrationTaskId}\`\nContinuation ID: \`${continuation.id}\`\nDownstream task ID: \`${continuation.a2aTaskId}\``;
484
+ }
485
+
486
+ function continuationFailureCode(reason: string): string {
487
+ const explicitCode = extractFailureCode(reason);
488
+ if (explicitCode) return explicitCode;
489
+ if (isLlmCredentialError(reason, explicitCode)) {
490
+ return LLM_MISSING_CREDENTIALS_ERROR_CODE;
491
+ }
492
+ if (/\btimed out polling\b/i.test(reason)) return "a2a_remote_timeout";
493
+ return "a2a_downstream_error";
494
+ }
495
+
496
+ function extractFailureCode(reason: string): string | null {
497
+ const match = /\bcode\s*[:=]\s*[`"']?([a-z][a-z0-9_]{0,79})\b/i.exec(reason);
498
+ return match?.[1]?.toLowerCase() ?? null;
469
499
  }
470
500
 
471
501
  function isRemoteWorkExpired(continuation: A2AContinuation): boolean {
@@ -37,6 +37,7 @@ const SLACK_API_TIMEOUT_MS = 10_000;
37
37
  const SLACK_PERMALINK_TIMEOUT_MS = 1_000;
38
38
  const SLACK_CONTEXT_MESSAGE_LIMIT = 15;
39
39
  const SLACK_CONTEXT_TEXT_LIMIT = 2_000;
40
+ const SLACK_CALM_PROGRESS_THRESHOLD_SECONDS = 30;
40
41
 
41
42
  export interface SlackAdapterOptions {
42
43
  /** Resolve the bot token for the exact Slack installation. */
@@ -323,12 +324,10 @@ export function slackAdapter(
323
324
  async postProcessingPlaceholder(
324
325
  incoming: IncomingMessage,
325
326
  ): Promise<{ placeholderRef: string } | null> {
326
- // No placeholder reply in the thread — Slack's native assistant
327
- // status bar ("agent-native is thinking…", below the composer) is the
328
- // loading affordance. A second visible "Working on it…" reply was
329
- // redundant and added an extra chunk that we then had to overwrite.
330
- // We just set the native status and return null so sendResponse posts
331
- // the final reply as a fresh message.
327
+ // No placeholder reply in the thread — Slack's native assistant status
328
+ // bar and the task stream are the loading affordance. Keep the status
329
+ // specific about intent instead of presenting the generic thinking
330
+ // state while the native plan is opening.
332
331
  const token = await resolveBotToken(incoming);
333
332
  if (!token) return null;
334
333
 
@@ -336,11 +335,16 @@ export function slackAdapter(
336
335
  const threadTs = incoming.platformContext.threadTs as string;
337
336
  if (!channelId || !threadTs) return null;
338
337
 
339
- // Best-effort: flip the native Agent "is thinking…" status bar in the
338
+ // Best-effort: flip the native Agent status bar in the
340
339
  // channel input area. Slack accepts chat:write for this method. The
341
340
  // canonical manifest separately requests assistant:write for Agent View
342
341
  // and app_context_changed events.
343
- setSlackAssistantStatus(token, channelId, threadTs, "is thinking…");
342
+ setSlackAssistantStatus(
343
+ token,
344
+ channelId,
345
+ threadTs,
346
+ "I’m looking into this now…",
347
+ );
344
348
  return null;
345
349
  },
346
350
 
@@ -1266,6 +1270,42 @@ function shortTaskTitle(value: unknown): string {
1266
1270
  return text.replace(/[-_]/g, " ").slice(0, 200);
1267
1271
  }
1268
1272
 
1273
+ function delegatedTaskTitle(agent: unknown): string {
1274
+ return `Contact ${shortTaskTitle(agent)}`;
1275
+ }
1276
+
1277
+ function formatElapsedSeconds(elapsedSeconds: number): string {
1278
+ const totalSeconds = Math.max(0, Math.round(elapsedSeconds));
1279
+ const minutes = Math.floor(totalSeconds / 60);
1280
+ const seconds = totalSeconds % 60;
1281
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
1282
+ }
1283
+
1284
+ function formatProgressState(state: string): string {
1285
+ if (state === "submitted") return "Queued";
1286
+ if (state === "working") return "Working";
1287
+ return shortTaskTitle(state);
1288
+ }
1289
+
1290
+ function delegatedProgressDetails(
1291
+ agent: unknown,
1292
+ state: string,
1293
+ elapsedSeconds: number,
1294
+ detail?: string,
1295
+ ): string {
1296
+ const elapsed = formatElapsedSeconds(elapsedSeconds);
1297
+ const progress = `${formatProgressState(state)} · ${elapsed}`;
1298
+ const update = detail ? shortTaskTitle(detail) : "";
1299
+
1300
+ if (elapsedSeconds >= SLACK_CALM_PROGRESS_THRESHOLD_SECONDS) {
1301
+ const agentName = shortTaskTitle(agent);
1302
+ const context = update ? ` — ${update}.` : ".";
1303
+ return `${progress}${context} This is taking longer than usual, but ${agentName} is still working. I’ll post the result here.`;
1304
+ }
1305
+
1306
+ return update ? `${progress} — ${update}` : progress;
1307
+ }
1308
+
1269
1309
  async function postSlackJson(
1270
1310
  token: string,
1271
1311
  method: string,
@@ -1315,18 +1355,18 @@ async function startSlackRunProgress(
1315
1355
  ...(incoming.tenantId ? { recipient_team_id: incoming.tenantId } : {}),
1316
1356
  ...(incoming.senderId ? { recipient_user_id: incoming.senderId } : {}),
1317
1357
  task_display_mode: "plan",
1318
- markdown_text: "Working on your request…",
1358
+ markdown_text: "I’m looking into this for you.",
1319
1359
  chunks: [
1320
1360
  {
1321
1361
  type: "plan_update",
1322
- title: "Working on your request",
1362
+ title: "I’m looking into this for you",
1323
1363
  },
1324
1364
  {
1325
1365
  type: "task_update",
1326
1366
  id: "agent-native:context",
1327
- title: "Understand the request",
1367
+ title: "Review the request",
1328
1368
  status: "in_progress",
1329
- details: "Loading relevant context",
1369
+ details: "Finding the information needed for an answer",
1330
1370
  },
1331
1371
  ],
1332
1372
  });
@@ -1373,7 +1413,7 @@ function createSlackRunProgress(
1373
1413
  const toolTaskIds = new Map<string, string>();
1374
1414
  const agentTaskIds = new Map<string, string>();
1375
1415
  tasks.set("agent-native:context", {
1376
- title: "Understand the request",
1416
+ title: "Review the request",
1377
1417
  status: "in_progress",
1378
1418
  });
1379
1419
  let sequence = 0;
@@ -1540,9 +1580,19 @@ function createSlackRunProgress(
1540
1580
  : event.status === "done"
1541
1581
  ? "complete"
1542
1582
  : "error";
1543
- const title = `Ask ${shortTaskTitle(event.agent)}`;
1583
+ const title = delegatedTaskTitle(event.agent);
1544
1584
  tasks.set(id, { title, status });
1545
- await append({ type: "task_update", id, title, status });
1585
+ await append({
1586
+ type: "task_update",
1587
+ id,
1588
+ title,
1589
+ status,
1590
+ ...(event.status === "start"
1591
+ ? {
1592
+ details: `I’m contacting ${shortTaskTitle(event.agent)} for an answer.`,
1593
+ }
1594
+ : {}),
1595
+ });
1546
1596
  } else if (event.type === "agent_call_progress") {
1547
1597
  // A2A calls can stay healthy for minutes. Keep the same native Slack
1548
1598
  // task card alive with each real downstream poll rather than creating
@@ -1550,10 +1600,13 @@ function createSlackRunProgress(
1550
1600
  const id =
1551
1601
  agentTaskIds.get(event.agent) ?? taskId("agent", event.agent);
1552
1602
  agentTaskIds.set(event.agent, id);
1553
- const title = `Ask ${shortTaskTitle(event.agent)}`;
1554
- const details = event.detail
1555
- ? shortTaskTitle(event.detail)
1556
- : `Still ${event.state} after ${event.elapsedSeconds}s`;
1603
+ const title = delegatedTaskTitle(event.agent);
1604
+ const details = delegatedProgressDetails(
1605
+ event.agent,
1606
+ event.state,
1607
+ event.elapsedSeconds,
1608
+ event.detail,
1609
+ );
1557
1610
  tasks.set(id, { title, status: "in_progress" });
1558
1611
  await append({
1559
1612
  type: "task_update",
@@ -1566,9 +1619,9 @@ function createSlackRunProgress(
1566
1619
  await append({
1567
1620
  type: "task_update",
1568
1621
  id: "agent-native:context",
1569
- title: "Understand the request",
1622
+ title: "Review the request",
1570
1623
  status: "in_progress",
1571
- details: shortTaskTitle(event.label),
1624
+ details: `Working · ${shortTaskTitle(event.label)}`,
1572
1625
  });
1573
1626
  }
1574
1627
  },
@@ -15,6 +15,7 @@ import {
15
15
  normalizeModelForEngine,
16
16
  resolveEngine,
17
17
  } from "../agent/engine/index.js";
18
+ import { resolveMainChatMaxOutputTokens } from "../agent/engine/output-tokens.js";
18
19
  import { PROVIDER_TO_ENV } from "../agent/engine/provider-env-vars.js";
19
20
  import type { AgentEngine, EngineMessage } from "../agent/engine/types.js";
20
21
  import {
@@ -45,6 +46,7 @@ import {
45
46
  readDeployCredentialEnv,
46
47
  } from "../server/credential-provider.js";
47
48
  import { runWithRequestContext } from "../server/request-context.js";
49
+ import { normalizeReasoningEffortForRequest } from "../shared/reasoning-effort.js";
48
50
  import { A2A_CONTINUATION_QUEUED_MARKER } from "./a2a-continuation-marker.js";
49
51
  import {
50
52
  clearIntegrationAwaitingInput,
@@ -866,6 +868,18 @@ async function processIncomingMessage(
866
868
  signal,
867
869
  threadId,
868
870
  approvedToolCalls: incoming.approvedToolCalls,
871
+ // Messaging integrations are interactive chat surfaces. They
872
+ // need the same initial completion headroom as web chat so
873
+ // reasoning cannot consume the small per-engine default and
874
+ // leave a user-facing Slack reply empty.
875
+ maxOutputTokens: resolveMainChatMaxOutputTokens(resolvedModel),
876
+ // Explicitly resolve the normal chat default so an empty-final
877
+ // retry can step its reasoning effort down rather than
878
+ // repeatedly letting the engine choose Medium.
879
+ reasoningEffort: normalizeReasoningEffortForRequest(
880
+ resolvedModel,
881
+ undefined,
882
+ ),
869
883
  });
870
884
  return usage;
871
885
  },
@@ -216,6 +216,12 @@ const messages = {
216
216
  promptTitle: "Talk to your app",
217
217
  promptDescription:
218
218
  "Voice mode keeps listening while the agent navigates and takes actions.",
219
+ setupTitle: "Set up voice mode",
220
+ setupDescription:
221
+ "Connect Builder to use managed voice with credits, or use your own OpenAI API key.",
222
+ connectBuilder: "Connect Builder",
223
+ useOpenAiKey: "Use OpenAI API key",
224
+ startWithOpenAiKey: "Start with OpenAI key",
219
225
  start: "Start voice mode",
220
226
  keepDictating: "Keep dictating",
221
227
  showChat: "Show chat",
@@ -0,0 +1,118 @@
1
+ export const HOSTED_DEFAULT_MODEL_EXPERIMENT_ID =
2
+ "hosted-default-model-sonnet-5-vs-gpt-5-6-luna-2026-07";
3
+
4
+ export const HOSTED_DEFAULT_MODEL_CONTROL = {
5
+ id: "sonnet-5",
6
+ model: "claude-sonnet-5",
7
+ weight: 80,
8
+ } as const;
9
+
10
+ export const HOSTED_DEFAULT_MODEL_TREATMENT = {
11
+ id: "gpt-5-6-luna",
12
+ model: "gpt-5-6-luna",
13
+ weight: 20,
14
+ } as const;
15
+
16
+ export interface HostedDefaultModelExperimentAssignment {
17
+ experimentId: string;
18
+ variantId: string;
19
+ }
20
+
21
+ export interface HostedDefaultModelExperimentResolution {
22
+ model: string;
23
+ assignment: HostedDefaultModelExperimentAssignment;
24
+ }
25
+
26
+ type HostedExperimentEnv = Record<string, string | undefined>;
27
+
28
+ const FIRST_PARTY_HOST_SUFFIX = ".agent-native.com";
29
+ const ENABLED_VALUES = new Set(["1", "true", "on", "yes"]);
30
+ const DISABLED_VALUES = new Set(["0", "false", "off", "no"]);
31
+
32
+ function hostnameFromUrl(value: string | undefined): string | null {
33
+ const trimmed = value?.trim();
34
+ if (!trimmed) return null;
35
+ try {
36
+ const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
37
+ ? new URL(trimmed)
38
+ : new URL(`https://${trimmed}`);
39
+ return url.hostname.toLowerCase();
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * The rollout is automatic only on first-party hosted app domains. Operators
47
+ * can explicitly enable or disable it for preview and emergency rollback.
48
+ */
49
+ export function isHostedDefaultModelExperimentEnabled(
50
+ env: HostedExperimentEnv = process.env,
51
+ ): boolean {
52
+ const override =
53
+ env.AGENT_NATIVE_HOSTED_MODEL_EXPERIMENT?.trim().toLowerCase();
54
+ if (override && DISABLED_VALUES.has(override)) return false;
55
+ if (override && ENABLED_VALUES.has(override)) return true;
56
+
57
+ const hostnames = [
58
+ env.APP_URL,
59
+ env.BETTER_AUTH_URL,
60
+ env.URL,
61
+ env.DEPLOY_URL,
62
+ env.VERCEL_PROJECT_PRODUCTION_URL,
63
+ env.VERCEL_URL,
64
+ ]
65
+ .map(hostnameFromUrl)
66
+ .filter((hostname): hostname is string => Boolean(hostname));
67
+
68
+ return hostnames.some(
69
+ (hostname) =>
70
+ hostname === "agent-native.com" ||
71
+ hostname.endsWith(FIRST_PARTY_HOST_SUFFIX),
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Stable cross-app bucket: the same authenticated user receives the same
77
+ * variant in every hosted template for the life of this experiment id.
78
+ */
79
+ export function hostedDefaultModelExperimentBucket(userId: string): number {
80
+ const input = `${HOSTED_DEFAULT_MODEL_EXPERIMENT_ID}:${userId.trim().toLowerCase()}`;
81
+ let hash = 0x811c9dc5;
82
+ for (let index = 0; index < input.length; index++) {
83
+ hash ^= input.charCodeAt(index);
84
+ hash = Math.imul(hash, 0x01000193);
85
+ }
86
+ return (hash >>> 0) % 100;
87
+ }
88
+
89
+ export function resolveHostedDefaultModelExperiment(args: {
90
+ userId: string | null | undefined;
91
+ engineName: string;
92
+ isDefaultModelSelection: boolean;
93
+ supportedModels?: readonly string[];
94
+ env?: HostedExperimentEnv;
95
+ }): HostedDefaultModelExperimentResolution | null {
96
+ const userId = args.userId?.trim();
97
+ if (!userId) return null;
98
+ if (args.engineName !== "builder") return null;
99
+ if (!args.isDefaultModelSelection) return null;
100
+ if (!isHostedDefaultModelExperimentEnabled(args.env)) return null;
101
+
102
+ const variant =
103
+ hostedDefaultModelExperimentBucket(userId) <
104
+ HOSTED_DEFAULT_MODEL_TREATMENT.weight
105
+ ? HOSTED_DEFAULT_MODEL_TREATMENT
106
+ : HOSTED_DEFAULT_MODEL_CONTROL;
107
+ if (args.supportedModels && !args.supportedModels.includes(variant.model)) {
108
+ return null;
109
+ }
110
+
111
+ return {
112
+ model: variant.model,
113
+ assignment: {
114
+ experimentId: HOSTED_DEFAULT_MODEL_EXPERIMENT_ID,
115
+ variantId: variant.id,
116
+ },
117
+ };
118
+ }
@@ -69,6 +69,18 @@ async function resolveOwner(event: H3Event): Promise<string> {
69
69
  return session.email;
70
70
  }
71
71
 
72
+ function canManageExperiments(ownerEmail: string): boolean {
73
+ // Local development keeps the built-in dashboard usable without additional
74
+ // setup. Hosted deployments fail closed unless the operator supplies an
75
+ // explicit allowlist, because experiments affect every user in the app.
76
+ if (process.env.NODE_ENV !== "production") return true;
77
+ const admins = (process.env.AGENT_NATIVE_EXPERIMENT_ADMIN_EMAILS ?? "")
78
+ .split(",")
79
+ .map((email) => email.trim().toLowerCase())
80
+ .filter(Boolean);
81
+ return admins.includes(ownerEmail.trim().toLowerCase());
82
+ }
83
+
72
84
  function parseSince(q: Record<string, any>): number {
73
85
  const raw = q.since;
74
86
  if (typeof raw === "string" && raw.length > 0) {
@@ -236,6 +248,11 @@ export function createObservabilityHandler() {
236
248
  return getEvalStats(parseSince(q), { userId: owner });
237
249
  }
238
250
 
251
+ if (parts[0] === "experiments" && !canManageExperiments(owner)) {
252
+ setResponseStatus(event, 403);
253
+ return { error: "Experiment administrator access required" };
254
+ }
255
+
239
256
  // POST /experiments — create experiment. Records the calling user as
240
257
  // the owner so subsequent PUT / POST results require the same caller.
241
258
  if (method === "POST" && parts.length === 1 && parts[0] === "experiments") {
@@ -972,7 +972,10 @@ export async function updateExperiment(
972
972
  sets.push("status = ?");
973
973
  args.push(updates.status);
974
974
  if (updates.status === "running" && !updates.endedAt) {
975
- sets.push("started_at = ?");
975
+ // Preserve the original exposure window when a paused experiment
976
+ // resumes; resetting it would silently discard the first run period from
977
+ // the results.
978
+ sets.push("started_at = COALESCE(started_at, ?)");
976
979
  args.push(Date.now());
977
980
  }
978
981
  }