@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
@@ -100,6 +100,11 @@ function emitLlmGenerationTrackingEvent(args: {
100
100
  successfulTools: number;
101
101
  failedTools: number;
102
102
  createdAt: number;
103
+ experimentAssignments?: Array<{
104
+ experimentId: string;
105
+ variantId: string;
106
+ }>;
107
+ modelSelectionSource?: string;
103
108
  }): void {
104
109
  const provider = llmProviderFromEngine(args.engineName, args.model);
105
110
  const costUsd = costUsdFromCenticents(args.costCentsX100);
@@ -126,6 +131,7 @@ function emitLlmGenerationTrackingEvent(args: {
126
131
  tool_calls: args.toolCalls,
127
132
  successful_tools: args.successfulTools,
128
133
  failed_tools: args.failedTools,
134
+ model_selection_source: args.modelSelectionSource,
129
135
  created_at: new Date(args.createdAt).toISOString(),
130
136
  created_at_ms: args.createdAt,
131
137
  $ai_trace_id: args.runId,
@@ -145,6 +151,18 @@ function emitLlmGenerationTrackingEvent(args: {
145
151
  $ai_request_count: 1,
146
152
  $ai_total_cost_usd: costUsd,
147
153
  };
154
+ if (args.experimentAssignments?.length) {
155
+ properties.experiment_ids = args.experimentAssignments
156
+ .map((assignment) => assignment.experimentId)
157
+ .join(",");
158
+ properties.experiment_variants = args.experimentAssignments
159
+ .map((assignment) => assignment.variantId)
160
+ .join(",");
161
+ if (args.experimentAssignments.length === 1) {
162
+ properties.experiment_id = args.experimentAssignments[0].experimentId;
163
+ properties.experiment_variant = args.experimentAssignments[0].variantId;
164
+ }
165
+ }
148
166
  if (error) properties.error_message = error;
149
167
 
150
168
  for (const key of Object.keys(properties)) {
@@ -245,6 +263,12 @@ export async function instrumentAgentLoop(opts: {
245
263
  * reads. */
246
264
  userId: string | null;
247
265
  config: ObservabilityConfig;
266
+ metadata?: Record<string, unknown> | null;
267
+ experimentAssignments?: Array<{
268
+ experimentId: string;
269
+ variantId: string;
270
+ }>;
271
+ modelSelectionSource?: string;
248
272
  classifyError?: (error: unknown) =>
249
273
  | {
250
274
  status?: "success" | "error";
@@ -268,6 +292,15 @@ export async function instrumentAgentLoop(opts: {
268
292
  "agent.thread_id": threadId ?? undefined,
269
293
  "agent.user_id": userId ?? undefined,
270
294
  "agent.model": loopOpts.model,
295
+ "agent.model_selection_source": opts.modelSelectionSource,
296
+ "agent.experiment_id":
297
+ opts.experimentAssignments?.length === 1
298
+ ? opts.experimentAssignments[0].experimentId
299
+ : undefined,
300
+ "agent.experiment_variant":
301
+ opts.experimentAssignments?.length === 1
302
+ ? opts.experimentAssignments[0].variantId
303
+ : undefined,
271
304
  });
272
305
 
273
306
  const spans: TraceSpan[] = [];
@@ -424,7 +457,7 @@ export async function instrumentAgentLoop(opts: {
424
457
  let usage: AgentLoopUsage | undefined;
425
458
  let runStatus: "success" | "error" = "success";
426
459
  let errorMessage: string | null = null;
427
- let runMetadata: Record<string, unknown> | null = null;
460
+ let runMetadata: Record<string, unknown> | null = opts.metadata ?? null;
428
461
  try {
429
462
  usage = await runAgentLoop({ ...loopOpts, send: instrumentedSend });
430
463
  } catch (err: any) {
@@ -434,7 +467,11 @@ export async function instrumentAgentLoop(opts: {
434
467
  classification?.errorMessage === undefined
435
468
  ? (err?.message ?? String(err))
436
469
  : classification.errorMessage;
437
- runMetadata = classification?.metadata ?? null;
470
+ const errorMetadata = classification?.metadata ?? null;
471
+ runMetadata =
472
+ runMetadata || errorMetadata
473
+ ? { ...(runMetadata ?? {}), ...(errorMetadata ?? {}) }
474
+ : null;
438
475
  throw err;
439
476
  } finally {
440
477
  const runEnd = Date.now();
@@ -501,6 +538,8 @@ export async function instrumentAgentLoop(opts: {
501
538
  successfulTools,
502
539
  failedTools,
503
540
  createdAt: runStart,
541
+ experimentAssignments: opts.experimentAssignments,
542
+ modelSelectionSource: opts.modelSelectionSource,
504
543
  });
505
544
  }
506
545
 
@@ -15,6 +15,8 @@ import {
15
15
  normalizeModelForEngine,
16
16
  } from "../../agent/engine/index.js";
17
17
  import type { ActionTool } from "../../agent/types.js";
18
+ import { resolveHostedDefaultModelExperiment } from "../../observability/hosted-model-experiment.js";
19
+ import { getRequestUserEmail } from "../../server/request-context.js";
18
20
  import { getSetting } from "../../settings/index.js";
19
21
 
20
22
  export const tool: ActionTool = {
@@ -82,13 +84,23 @@ export async function run(args: Record<string, string> = {}): Promise<string> {
82
84
  ? current?.model
83
85
  : undefined;
84
86
  const currentEngineName = currentEntry?.name ?? "anthropic";
85
- const currentModel =
87
+ let currentModel =
86
88
  currentEntry && !envUnavailable
87
89
  ? normalizeModelForEngine(
88
90
  currentEntry,
89
91
  currentModelCandidate ?? currentEntry.defaultModel,
90
92
  )
91
93
  : (currentModelCandidate ?? DEFAULT_MODEL);
94
+ const hostedExperiment =
95
+ currentEntry && !currentModelCandidate
96
+ ? resolveHostedDefaultModelExperiment({
97
+ userId: getRequestUserEmail(),
98
+ engineName: currentEntry.name,
99
+ isDefaultModelSelection: true,
100
+ supportedModels: currentEntry.supportedModels,
101
+ })
102
+ : null;
103
+ if (hostedExperiment) currentModel = hostedExperiment.model;
92
104
 
93
105
  const result = {
94
106
  engines: engines.map((e) => ({
@@ -107,6 +119,12 @@ export async function run(args: Record<string, string> = {}): Promise<string> {
107
119
  : {
108
120
  engine: currentEngineName,
109
121
  model: currentModel,
122
+ ...(hostedExperiment
123
+ ? {
124
+ modelSelectionSource: "experiment",
125
+ experimentAssignment: hostedExperiment.assignment,
126
+ }
127
+ : {}),
110
128
  },
111
129
  };
112
130
 
@@ -321,7 +321,14 @@ export async function run(
321
321
  orgDomain: callerOrgDomain,
322
322
  orgSecret: callerOrgSecret,
323
323
  onUpdate: onRemotePollUpdate,
324
- ...(callTimeoutMs ? { timeoutMs: callTimeoutMs } : {}),
324
+ ...(callTimeoutMs
325
+ ? {
326
+ timeoutMs: callTimeoutMs,
327
+ // Integration callers must keep the timeout task id so the
328
+ // catch below can enqueue durable continuation polling.
329
+ returnRecoverableArtifactsOnTimeout: false,
330
+ }
331
+ : {}),
325
332
  });
326
333
  responseText =
327
334
  formatDownstreamLlmCredentialFailure(agent.name, responseText) ??
@@ -348,8 +355,23 @@ export async function run(
348
355
  `The ${agent.name} agent accepted this delegated subtask and will post its own final result to the originating integration thread automatically. ` +
349
356
  `Do not call ${agent.name} again for this same subtask. Continue any other requested work, then answer with the completed results you have; if needed, mention that ${agent.name} is posting its result separately.`;
350
357
  } else {
351
- const reason = pollErr?.message ?? "unknown error";
352
- responseText = `The ${agent.name} agent is taking longer than expected and didn't reply in time. (${reason})`;
358
+ // The normal integration path must preserve the timeout task id so
359
+ // it can enqueue a durable continuation. If that enqueue fails,
360
+ // do not hide receiver-verified artifacts that were already
361
+ // returned with the last poll; this mirrors callAgent's default
362
+ // timeout behavior without treating arbitrary remote status text
363
+ // as a completed response.
364
+ const recoverableArtifactText =
365
+ extractRecoverableTimeoutArtifactText(pollErr);
366
+ if (recoverableArtifactText) {
367
+ responseText = expandRelativeUrls(
368
+ recoverableArtifactText,
369
+ agent.url,
370
+ );
371
+ } else {
372
+ const reason = pollErr?.message ?? "unknown error";
373
+ responseText = `The ${agent.name} agent is taking longer than expected and didn't reply in time. (${reason})`;
374
+ }
353
375
  }
354
376
  } else {
355
377
  const reason = pollErr?.message ?? "unknown error";
@@ -490,6 +512,34 @@ function getA2ATaskTimeoutTaskId(err: unknown): string | null {
490
512
  return match?.[1] ?? null;
491
513
  }
492
514
 
515
+ /**
516
+ * Mirrors the A2A client's default timeout recovery for the exceptional case
517
+ * where an integration cannot enqueue a durable continuation. Only an
518
+ * explicitly receiver-marked task message is safe to surface as a completed
519
+ * partial result; ordinary working-state text remains a timeout failure.
520
+ */
521
+ function extractRecoverableTimeoutArtifactText(err: unknown): string {
522
+ const candidate = err as
523
+ | { lastTask?: Task | unknown; name?: unknown }
524
+ | null
525
+ | undefined;
526
+ const lastTask =
527
+ err instanceof A2ATaskTimeoutError
528
+ ? err.lastTask
529
+ : candidate?.name === "A2ATaskTimeoutError"
530
+ ? (candidate.lastTask as Task | undefined)
531
+ : undefined;
532
+ const message = lastTask?.status?.message;
533
+ if (!message?.metadata?.agentNativeRecoverableArtifacts) return "";
534
+
535
+ return message.parts
536
+ .filter(
537
+ (part): part is { type: "text"; text: string } => part.type === "text",
538
+ )
539
+ .map((part) => part.text)
540
+ .join("\n");
541
+ }
542
+
493
543
  async function formatExistingIntegrationContinuationIfRetry(
494
544
  agent: {
495
545
  name: string;
@@ -4,7 +4,7 @@
4
4
  * Search or list past agent chat threads.
5
5
  *
6
6
  * Usage:
7
- * pnpm action search-chats [--query "search term"] [--limit N] [--format json]
7
+ * pnpm action search-chats [--query "search term"] [--limit N] [--format json] [--includeArchived]
8
8
  */
9
9
 
10
10
  import { searchThreads, listThreads } from "../../chat-threads/store.js";
@@ -40,15 +40,17 @@ export default async function searchChats(args: string[]): Promise<void> {
40
40
  console.log(`Usage: pnpm action search-chats [options]
41
41
 
42
42
  Options:
43
- --query <text> Search chats by title, preview, or content
44
- --limit N Max results (default: 20)
45
- --format json Output as JSON
46
- --help Show this help message
43
+ --query <text> Search chats by title, preview, or content
44
+ --limit N Max results (default: 20)
45
+ --format json Output as JSON
46
+ --includeArchived Also include archived chats (excluded by default)
47
+ --help Show this help message
47
48
 
48
49
  Examples:
49
50
  pnpm action search-chats --query "email setup"
50
51
  pnpm action search-chats --limit 5
51
- pnpm action search-chats --format json`);
52
+ pnpm action search-chats --format json
53
+ pnpm action search-chats --includeArchived`);
52
54
  return;
53
55
  }
54
56
 
@@ -56,10 +58,11 @@ Examples:
56
58
  const limit = parsed.limit ? parseInt(parsed.limit, 10) : 20;
57
59
  if (isNaN(limit) || limit < 1) fail("--limit must be a positive integer");
58
60
 
61
+ const includeArchived = parsed.includeArchived === "true";
59
62
  const query = parsed.query;
60
63
  const threads = query
61
- ? await searchThreads(owner, query, limit)
62
- : await listThreads(owner, limit, 0);
64
+ ? await searchThreads(owner, query, limit, { includeArchived })
65
+ : await listThreads(owner, { limit, offset: 0, includeArchived });
63
66
 
64
67
  if (parsed.format === "json") {
65
68
  console.log(
@@ -22,7 +22,7 @@ export function registerFrameworkSecrets(): void {
22
22
  key: "OPENAI_API_KEY",
23
23
  label: "OpenAI API key",
24
24
  description:
25
- "Enables realtime voice conversations with your app and optional OpenAI transcription.",
25
+ "Optional fallback for realtime voice when Builder is not connected, and for OpenAI transcription.",
26
26
  docsUrl: "https://platform.openai.com/api-keys",
27
27
  scope: "user",
28
28
  kind: "api-key",
@@ -54,11 +54,16 @@ import {
54
54
  listAgentEngines,
55
55
  registerBuiltinEngines,
56
56
  } from "../agent/engine/index.js";
57
- import type { AgentEngine, EngineMessage } from "../agent/engine/types.js";
57
+ import type {
58
+ AgentEngine,
59
+ EngineMessage,
60
+ EngineTool,
61
+ } from "../agent/engine/types.js";
58
62
  import {
59
63
  createProductionAgentHandler,
60
64
  actionsToEngineTools,
61
65
  executeAgentToolCall,
66
+ filterInitialEngineTools,
62
67
  getActiveRunForThreadAsync,
63
68
  abortRunDurably,
64
69
  subscribeToRun,
@@ -1903,6 +1908,11 @@ async function createChatScriptEntries(): Promise<Record<string, ActionEntry>> {
1903
1908
  description: "Output format",
1904
1909
  enum: ["json", "text"],
1905
1910
  },
1911
+ includeArchived: {
1912
+ type: "boolean",
1913
+ description:
1914
+ "Also include archived chats in the results. Archived chats are excluded by default.",
1915
+ },
1906
1916
  },
1907
1917
  },
1908
1918
  },
@@ -1953,6 +1963,11 @@ async function createChatScriptEntries(): Promise<Record<string, ActionEntry>> {
1953
1963
  description: "(search) Output format",
1954
1964
  enum: ["json", "text"],
1955
1965
  },
1966
+ includeArchived: {
1967
+ type: "boolean",
1968
+ description:
1969
+ "(search) Also include archived chats in the results. Archived chats are excluded by default, matching the 'archive' action's effect on the chat list.",
1970
+ },
1956
1971
  id: {
1957
1972
  type: "string",
1958
1973
  description:
@@ -2940,6 +2955,22 @@ export function runA2AAgentLoop(
2940
2955
  );
2941
2956
  }
2942
2957
 
2958
+ /**
2959
+ * Keep delegated A2A turns on the same compact initial tool surface as
2960
+ * interactive chat. `tool-search` remains in the initial set, while the
2961
+ * complete registry is supplied separately so the run loop can load a matched
2962
+ * schema after a tool-search result.
2963
+ */
2964
+ export function createA2AEngineToolSurface(
2965
+ availableTools: EngineTool[],
2966
+ initialToolNames?: string[],
2967
+ ): { tools: EngineTool[]; availableTools: EngineTool[] } {
2968
+ return {
2969
+ tools: filterInitialEngineTools(availableTools, initialToolNames),
2970
+ availableTools,
2971
+ };
2972
+ }
2973
+
2943
2974
  /**
2944
2975
  * Verbose framework sections returned by the `get-framework-context` tool.
2945
2976
  * Keyed by topic so the agent can request specific sections.
@@ -2971,9 +3002,9 @@ Use for charts, visualizations, previews. Don't use for simple text/tables or ex
2971
3002
  "chat-history": `### Chat History
2972
3003
 
2973
3004
  You can search and restore previous chat conversations using \`chat-history\`:
2974
- - \`chat-history\` (action: "search") — Search or list past chat threads by keyword
3005
+ - \`chat-history\` (action: "search") — Search or list past chat threads by keyword. Archived threads are excluded by default; pass \`includeArchived: true\` to also see them.
2975
3006
  - \`chat-history\` (action: "open") — Open a chat thread in the UI as a new tab and focus it
2976
- - \`chat-history\` (actions: "rename", "pin", "unpin", "archive") — Organize a known chat thread by ID
3007
+ - \`chat-history\` (actions: "rename", "pin", "unpin", "archive") — Organize a known chat thread by ID. Archiving a thread hides it from the default chat list and search.
2977
3008
 
2978
3009
  When the user asks to find a previous conversation, use \`chat-history\` with action "search" first to find matching threads, then action "open" to restore the one they want.`,
2979
3010
 
@@ -5077,7 +5108,10 @@ export function createAgentChatPlugin(
5077
5108
  },
5078
5109
  );
5079
5110
 
5080
- const a2aTools = actionsToEngineTools(a2aActions);
5111
+ const a2aToolSurface = createA2AEngineToolSurface(
5112
+ actionsToEngineTools(a2aActions),
5113
+ options?.initialToolNames,
5114
+ );
5081
5115
 
5082
5116
  // Precise current time rides the user message (not the cached
5083
5117
  // system-prompt prefix) — same pattern as the interactive handler.
@@ -5098,7 +5132,7 @@ export function createAgentChatPlugin(
5098
5132
  const controller = new AbortController();
5099
5133
 
5100
5134
  console.log(
5101
- `[A2A] Starting agent loop: ${a2aTools.length} tools, prompt ${systemPrompt.length} chars`,
5135
+ `[A2A] Starting agent loop: ${a2aToolSurface.tools.length}/${a2aToolSurface.availableTools.length} initial tools, prompt ${systemPrompt.length} chars`,
5102
5136
  );
5103
5137
 
5104
5138
  await runA2AAgentLoop(
@@ -5106,7 +5140,8 @@ export function createAgentChatPlugin(
5106
5140
  engine: a2aEngine,
5107
5141
  model,
5108
5142
  systemPrompt,
5109
- tools: a2aTools,
5143
+ tools: a2aToolSurface.tools,
5144
+ availableTools: a2aToolSurface.availableTools,
5110
5145
  messages: a2aMessages,
5111
5146
  actions: a2aActions,
5112
5147
  send: (event) => {
@@ -298,6 +298,7 @@ const BUILDER_WAITLIST_USE_CASES = new Set([
298
298
  BUILDER_WAITLIST_DEFAULT_USE_CASE,
299
299
  "design_publish_app",
300
300
  "docs_build_online_waitlist",
301
+ "docs_edit_online_waitlist",
301
302
  ]);
302
303
  const BUILDER_WAITLIST_FORM_TIMEOUT_MS = 8000;
303
304
  const BUILDER_WAITLIST_TEXT_LIMIT = 4000;
@@ -320,6 +321,7 @@ export interface BuilderWaitlistBody {
320
321
  appUrl?: unknown;
321
322
  pageUrl?: unknown;
322
323
  source?: unknown;
324
+ template?: unknown;
323
325
  useCase?: unknown;
324
326
  }
325
327
 
@@ -352,6 +354,13 @@ function normalizeBuilderWaitlistUseCase(value: unknown): string {
352
354
  : BUILDER_WAITLIST_DEFAULT_USE_CASE;
353
355
  }
354
356
 
357
+ function normalizeBuilderWaitlistTemplate(value: unknown): string | undefined {
358
+ const template = cleanBuilderWaitlistText(value, 100);
359
+ return template && /^[a-z0-9][a-z0-9-]{0,99}$/.test(template)
360
+ ? template
361
+ : undefined;
362
+ }
363
+
355
364
  function isValidWaitlistEmail(email: string): boolean {
356
365
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
357
366
  }
@@ -506,6 +515,7 @@ export function buildBuilderWaitlistFormPayload(
506
515
  getOrigin(event);
507
516
  const source =
508
517
  cleanBuilderWaitlistText(body.source, 100) ?? BUILDER_WAITLIST_FORM_SOURCE;
518
+ const template = normalizeBuilderWaitlistTemplate(body.template);
509
519
  const useCase = normalizeBuilderWaitlistUseCase(body.useCase);
510
520
 
511
521
  return {
@@ -515,6 +525,7 @@ export function buildBuilderWaitlistFormPayload(
515
525
  appUrl,
516
526
  prompt: cleanBuilderWaitlistText(body.prompt),
517
527
  source,
528
+ template,
518
529
  useCase,
519
530
  },
520
531
  _hp: "",
@@ -522,6 +533,7 @@ export function buildBuilderWaitlistFormPayload(
522
533
  submitterEmail: sessionEmail,
523
534
  pageUrl: appUrl,
524
535
  source,
536
+ template,
525
537
  useCase,
526
538
  },
527
539
  };
@@ -2021,6 +2033,7 @@ export function createCoreRoutesPlugin(
2021
2033
  body,
2022
2034
  );
2023
2035
  const waitlistSource = waitlistPayload.data.source;
2036
+ const waitlistTemplate = waitlistPayload.data.template;
2024
2037
  const waitlistUseCase = waitlistPayload.data.useCase;
2025
2038
  let formSubmission: { submitted: boolean; formId?: string };
2026
2039
  try {
@@ -2039,6 +2052,7 @@ export function createCoreRoutesPlugin(
2039
2052
  err instanceof Error ? err.message : "unknown_waitlist_error",
2040
2053
  source: waitlistSource,
2041
2054
  stage: "waitlist",
2055
+ template: waitlistTemplate ?? null,
2042
2056
  useCase: waitlistUseCase,
2043
2057
  },
2044
2058
  );
@@ -2057,6 +2071,7 @@ export function createCoreRoutesPlugin(
2057
2071
  formSubmitted: formSubmission.submitted,
2058
2072
  source: waitlistSource,
2059
2073
  stage: "waitlist",
2074
+ template: waitlistTemplate ?? null,
2060
2075
  useCase: waitlistUseCase,
2061
2076
  },
2062
2077
  );
@@ -8,6 +8,7 @@ import {
8
8
  type H3Event,
9
9
  } from "h3";
10
10
 
11
+ import { getBuilderGatewayRequestHeaders } from "../agent/engine/builder-gateway-headers.js";
11
12
  import {
12
13
  actionsToEngineTools,
13
14
  type ActionEntry,
@@ -18,7 +19,11 @@ import {
18
19
  sanitizeToolErrorValue,
19
20
  } from "../agent/tool-error-redaction.js";
20
21
  import { getSession } from "./auth.js";
21
- import { resolveSecret } from "./credential-provider.js";
22
+ import {
23
+ getBuilderGatewayBaseUrl,
24
+ resolveBuilderCredentials,
25
+ resolveSecret,
26
+ } from "./credential-provider.js";
22
27
  import { getH3App } from "./framework-request-handler.js";
23
28
  import { runWithRequestContext } from "./request-context.js";
24
29
 
@@ -336,12 +341,20 @@ function createSessionHandler(
336
341
  : undefined,
337
342
  },
338
343
  async () => {
339
- const apiKey = (await resolveSecret("OPENAI_API_KEY"))?.trim();
340
- if (!apiKey) {
341
- setResponseStatus(event, 400);
344
+ const builderCredentials = await resolveBuilderCredentials();
345
+ const builderConfigured = Boolean(
346
+ builderCredentials.privateKey?.trim() &&
347
+ builderCredentials.publicKey?.trim(),
348
+ );
349
+ const apiKey = builderConfigured
350
+ ? null
351
+ : (await resolveSecret("OPENAI_API_KEY"))?.trim();
352
+ if (!builderConfigured && !apiKey) {
353
+ setResponseStatus(event, 409);
342
354
  return {
343
355
  error:
344
- "Configure OPENAI_API_KEY in Settings to use realtime voice.",
356
+ "Connect Builder or configure an OpenAI API key to use realtime voice.",
357
+ code: "realtime_voice_setup_required",
345
358
  };
346
359
  }
347
360
 
@@ -369,39 +382,73 @@ function createSessionHandler(
369
382
  tool_choice: "auto",
370
383
  };
371
384
 
372
- const form = new FormData();
373
- form.set("sdp", sdp);
374
- form.set("session", JSON.stringify(session));
375
-
376
385
  let upstream: Response;
377
386
  try {
378
- upstream = await fetch(OPENAI_REALTIME_CALLS_URL, {
379
- method: "POST",
380
- headers: {
381
- Authorization: `Bearer ${apiKey}`,
382
- "OpenAI-Safety-Identifier": await realtimeVoiceSafetyIdentifier(
383
- auth.userEmail,
384
- ),
385
- },
386
- body: form,
387
- });
387
+ if (builderConfigured) {
388
+ const gatewayUrl = new URL(
389
+ "realtime/calls",
390
+ getBuilderGatewayBaseUrl().endsWith("/")
391
+ ? getBuilderGatewayBaseUrl()
392
+ : `${getBuilderGatewayBaseUrl()}/`,
393
+ );
394
+ gatewayUrl.searchParams.set(
395
+ "apiKey",
396
+ builderCredentials.publicKey!.trim(),
397
+ );
398
+ upstream = await fetch(gatewayUrl.toString(), {
399
+ method: "POST",
400
+ headers: {
401
+ "Content-Type": "application/json",
402
+ Authorization: `Bearer ${builderCredentials.privateKey!.trim()}`,
403
+ "x-builder-api-key": builderCredentials.publicKey!.trim(),
404
+ ...getBuilderGatewayRequestHeaders(),
405
+ ...(builderCredentials.userId
406
+ ? { "x-builder-user-id": builderCredentials.userId }
407
+ : {}),
408
+ },
409
+ body: JSON.stringify({ sdp, session }),
410
+ });
411
+ } else {
412
+ const form = new FormData();
413
+ form.set("sdp", sdp);
414
+ form.set("session", JSON.stringify(session));
415
+ upstream = await fetch(OPENAI_REALTIME_CALLS_URL, {
416
+ method: "POST",
417
+ headers: {
418
+ Authorization: `Bearer ${apiKey}`,
419
+ "OpenAI-Safety-Identifier": await realtimeVoiceSafetyIdentifier(
420
+ auth.userEmail,
421
+ ),
422
+ },
423
+ body: form,
424
+ });
425
+ }
388
426
  } catch {
389
427
  setResponseStatus(event, 502);
390
- return { error: "Could not reach the OpenAI Realtime API" };
428
+ return {
429
+ error: builderConfigured
430
+ ? "Could not reach the Builder realtime voice gateway"
431
+ : "Could not reach the OpenAI Realtime API",
432
+ };
391
433
  }
392
434
 
393
435
  if (!upstream.ok) {
394
- const detail = await safeOpenAiErrorDetail(upstream, apiKey);
395
- setResponseStatus(event, 502);
436
+ const detail = await safeOpenAiErrorDetail(
437
+ upstream,
438
+ builderConfigured ? builderCredentials.privateKey! : apiKey!,
439
+ );
440
+ setResponseStatus(event, builderConfigured ? upstream.status : 502);
396
441
  return {
397
- error: `OpenAI rejected the realtime session (${upstream.status})${detail ? `: ${detail}` : ""}`,
442
+ error: `${builderConfigured ? "Builder" : "OpenAI"} rejected the realtime session (${upstream.status})${detail ? `: ${detail}` : ""}`,
398
443
  };
399
444
  }
400
445
 
401
446
  const answerSdp = await upstream.text().catch(() => "");
402
447
  if (!answerSdp.trim()) {
403
448
  setResponseStatus(event, 502);
404
- return { error: "OpenAI returned an empty realtime session answer" };
449
+ return {
450
+ error: `${builderConfigured ? "Builder" : "OpenAI"} returned an empty realtime session answer`,
451
+ };
405
452
  }
406
453
 
407
454
  setResponseStatus(event, upstream.status);
@@ -22,7 +22,7 @@ import {
22
22
  import { getOrgContext } from "../org/context.js";
23
23
  import { getSession } from "./auth.js";
24
24
  import {
25
- resolveHasBuilderPrivateKey,
25
+ resolveHasCompleteBuilderConnection,
26
26
  resolveSecret,
27
27
  } from "./credential-provider.js";
28
28
  import { resolveGoogleRealtimeCredentials } from "./google-realtime-session.js";
@@ -90,8 +90,9 @@ export function createVoiceProvidersStatusHandler() {
90
90
  let builder = false;
91
91
  try {
92
92
  builder =
93
- (await withRequestContext(() => resolveHasBuilderPrivateKey())) ===
94
- true;
93
+ (await withRequestContext(() =>
94
+ resolveHasCompleteBuilderConnection(),
95
+ )) === true;
95
96
  } catch {
96
97
  builder = false;
97
98
  }
@@ -1,21 +1,15 @@
1
1
  export const DEFAULT_SSR_CACHE_CONTROL = "public, max-age=0, must-revalidate";
2
2
 
3
3
  export const DEFAULT_SSR_CDN_CACHE_CONTROL =
4
- "public, s-maxage=3600, stale-while-revalidate=604800, stale-if-error=3600";
4
+ "public, s-maxage=600, stale-while-revalidate=604800, stale-if-error=3600";
5
5
 
6
6
  export const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL =
7
- "public, durable, s-maxage=3600, stale-while-revalidate=604800, stale-if-error=3600";
8
-
9
- // React Router data requests can select a subset of route loaders with
10
- // `?_routes=...`. Ignore every other query parameter (UTM tags, click ids,
11
- // and similar tracking noise) so they cannot fragment Netlify's SSR cache.
12
- export const DEFAULT_SSR_NETLIFY_VARY = "query=_routes";
7
+ "public, durable, s-maxage=600, stale-while-revalidate=604800, stale-if-error=3600";
13
8
 
14
9
  export const DEFAULT_SSR_CACHE_HEADERS = {
15
10
  "cache-control": DEFAULT_SSR_CACHE_CONTROL,
16
11
  "cdn-cache-control": DEFAULT_SSR_CDN_CACHE_CONTROL,
17
12
  "netlify-cdn-cache-control": DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL,
18
- "netlify-vary": DEFAULT_SSR_NETLIFY_VARY,
19
13
  } as const;
20
14
 
21
15
  export const DEFAULT_SPECULATION_RULES_PATH =
@@ -25,6 +25,7 @@
25
25
  @import "./agent-conversation.css";
26
26
  @import "./rich-markdown-editor.css";
27
27
  @import "./blocks.css";
28
+ @import "./chat-history-list.css";
28
29
 
29
30
  /* Scan @agent-native/core's compiled client output for Tailwind classes.
30
31
  Path is resolved relative to THIS file. After build, this CSS lives at