@agent-native/core 0.79.7 → 0.79.10

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 (94) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-manager.ts +9 -1
  5. package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
  6. package/corpus/core/src/client/session-replay.ts +67 -13
  7. package/corpus/core/src/onboarding/default-steps.ts +3 -3
  8. package/corpus/core/src/server/action-routes.ts +6 -2
  9. package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
  10. package/corpus/core/src/server/agent-run-context.ts +204 -0
  11. package/corpus/core/src/server/credential-provider.ts +20 -4
  12. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
  13. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
  14. package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
  15. package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
  16. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  17. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
  18. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
  19. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
  20. package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
  21. package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
  22. package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
  23. package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
  24. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
  25. package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
  26. package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
  27. package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
  28. package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
  29. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
  30. package/corpus/templates/clips/app/routes/record.tsx +2 -2
  31. package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
  34. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  35. package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
  36. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
  37. package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
  38. package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
  39. package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
  40. package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
  41. package/corpus/templates/clips/desktop/src/app.tsx +85 -18
  42. package/corpus/templates/clips/desktop/src/styles.css +55 -0
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
  45. package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
  46. package/corpus/templates/mail/app/hooks/use-emails.ts +28 -4
  47. package/corpus/templates/mail/app/root.tsx +13 -7
  48. package/corpus/templates/mail/changelog/2026-06-26-mail-avoids-unnecessary-message-list-reloads-after-backgroun.md +6 -0
  49. package/dist/agent/run-manager.d.ts.map +1 -1
  50. package/dist/agent/run-manager.js +12 -1
  51. package/dist/agent/run-manager.js.map +1 -1
  52. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  53. package/dist/client/agent-chat-adapter.js +15 -8
  54. package/dist/client/agent-chat-adapter.js.map +1 -1
  55. package/dist/client/session-replay.d.ts.map +1 -1
  56. package/dist/client/session-replay.js +55 -14
  57. package/dist/client/session-replay.js.map +1 -1
  58. package/dist/collab/awareness.d.ts +2 -2
  59. package/dist/collab/awareness.d.ts.map +1 -1
  60. package/dist/collab/routes.d.ts +2 -2
  61. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  62. package/dist/notifications/routes.d.ts +2 -2
  63. package/dist/observability/routes.d.ts +6 -6
  64. package/dist/onboarding/default-steps.js +3 -3
  65. package/dist/onboarding/default-steps.js.map +1 -1
  66. package/dist/resources/handlers.d.ts +3 -3
  67. package/dist/server/action-routes.d.ts.map +1 -1
  68. package/dist/server/action-routes.js +6 -2
  69. package/dist/server/action-routes.js.map +1 -1
  70. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  71. package/dist/server/agent-chat-plugin.js +69 -154
  72. package/dist/server/agent-chat-plugin.js.map +1 -1
  73. package/dist/server/agent-run-context.d.ts +35 -0
  74. package/dist/server/agent-run-context.d.ts.map +1 -0
  75. package/dist/server/agent-run-context.js +137 -0
  76. package/dist/server/agent-run-context.js.map +1 -0
  77. package/dist/server/credential-provider.d.ts.map +1 -1
  78. package/dist/server/credential-provider.js +15 -4
  79. package/dist/server/credential-provider.js.map +1 -1
  80. package/dist/server/transcribe-voice.d.ts +1 -1
  81. package/package.json +1 -1
  82. package/corpus/templates/.retired/README.md +0 -9
  83. package/corpus/templates/.retired/calls/package.json +0 -8
  84. package/corpus/templates/.retired/code/package.json +0 -8
  85. package/corpus/templates/.retired/contracts/package.json +0 -8
  86. package/corpus/templates/.retired/images/package.json +0 -8
  87. package/corpus/templates/.retired/issues/package.json +0 -8
  88. package/corpus/templates/.retired/meeting-notes/package.json +0 -8
  89. package/corpus/templates/.retired/migration/package.json +0 -8
  90. package/corpus/templates/.retired/recruiting/package.json +0 -8
  91. package/corpus/templates/.retired/scheduling/package.json +0 -8
  92. package/corpus/templates/.retired/visual-plans/package.json +0 -8
  93. package/corpus/templates/.retired/voice/package.json +0 -8
  94. package/corpus/templates/.retired/workbench/package.json +0 -8
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2028
31
- - template files: 4381
30
+ - core files: 2029
31
+ - template files: 4374
@@ -1,5 +1,24 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.79.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 7e19ed6: Return plain string action results with a JSON content type so browser action clients can parse successful responses.
8
+
9
+ ## 0.79.9
10
+
11
+ ### Patch Changes
12
+
13
+ - ca677a0: Reduce noisy error capture for expected provider connection failures and transient chat recovery probes.
14
+
15
+ ## 0.79.8
16
+
17
+ ### Patch Changes
18
+
19
+ - 4984f2b: Fail A2A agent tasks that end on terminal run errors without a final response, and keep app-native artifact actions ahead of generic extension creation in agent prompts.
20
+ - 4984f2b: Centralize agent chat request context resolution so foreground and durable background workers share owner, org, timezone, and background-run setup, harden hosted credential fallback detection, and support compressed session replay uploads.
21
+
3
22
  ## 0.79.7
4
23
 
5
24
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.79.7",
3
+ "version": "0.79.10",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,4 +1,5 @@
1
1
  import { captureError } from "../server/capture-error.js";
2
+ import { isLlmCredentialError } from "./engine/credential-errors.js";
2
3
  import { EngineError } from "./engine/types.js";
3
4
  import {
4
5
  insertRun,
@@ -149,10 +150,15 @@ function getEngineRunErrorDetails(err: EngineError): string | undefined {
149
150
  function shouldCaptureRunError(err: unknown): boolean {
150
151
  if (!(err instanceof EngineError)) return true;
151
152
  const errorCode = getEngineRunErrorCode(err);
153
+ if (isLlmCredentialError(err, errorCode)) return false;
154
+ if (err.statusCode === 401 || err.statusCode === 403) return false;
155
+ if (/^40[13] status code\b/i.test(err.message)) return false;
156
+ if (err.message.trim().toLowerCase() === "connection error.") return false;
152
157
  if (!errorCode) return true;
153
158
  const normalizedCode = errorCode.toLowerCase();
154
159
  return (
155
160
  !normalizedCode.startsWith("credits-limit") &&
161
+ normalizedCode !== "builder_gateway_network_error" &&
156
162
  normalizedCode !== "provider_rate_limited" &&
157
163
  normalizedCode !== "rate_limit_exceeded"
158
164
  );
@@ -423,6 +429,8 @@ export function startRun(
423
429
  let pendingTerminalEvent: RunEvent | null = null;
424
430
 
425
431
  const captureRunError = (error: unknown, phase: "run" | "completion") => {
432
+ const errorCode =
433
+ error instanceof EngineError ? getEngineRunErrorCode(error) : undefined;
426
434
  captureError(error, {
427
435
  route: "/_agent-native/agent-chat",
428
436
  tags: {
@@ -431,7 +439,7 @@ export function startRun(
431
439
  runStatus: run.status,
432
440
  softTimedOut: softTimedOut ? "true" : "false",
433
441
  abortReason: run.abortReason,
434
- errorCode: error instanceof EngineError ? error.errorCode : undefined,
442
+ errorCode,
435
443
  },
436
444
  extra: {
437
445
  runId,
@@ -717,7 +717,7 @@ function continuationRepeatSignature(content: ContentPart[]): string {
717
717
  .map((segment) => segment.replace(/[^a-z0-9]+/g, " ").trim())
718
718
  .filter((segment) => segment.length > 0);
719
719
  if (segments.length === 0) return "";
720
- return Array.from(new Set(segments)).sort().join("");
720
+ return Array.from(new Set(segments)).sort().join("\u0000");
721
721
  }
722
722
 
723
723
  /**
@@ -771,7 +771,7 @@ function inFlightToolInputSignature(
771
771
  part: Extract<ContentPart, { type: "tool-call" }>,
772
772
  ): string {
773
773
  const raw = part.argsText ?? stableJson(part.args);
774
- return `${raw.length}${raw.slice(0, 256)}`;
774
+ return `${raw.length}\u0000${raw.slice(0, 256)}`;
775
775
  }
776
776
 
777
777
  function toolContinuationKey(
@@ -964,6 +964,10 @@ function retryDelay(attempt: number, abortSignal: AbortSignal): Promise<void> {
964
964
  return delay(ms, abortSignal);
965
965
  }
966
966
 
967
+ function shouldCaptureRecoveryHttpStatus(status: number): boolean {
968
+ return status < 500 || status >= 600;
969
+ }
970
+
967
971
  function generateTurnId(): string {
968
972
  if (
969
973
  typeof crypto !== "undefined" &&
@@ -1518,15 +1522,17 @@ export function createAgentChatAdapter(
1518
1522
  lastReconnectError = new Error(
1519
1523
  `Reconnect failed: ${reconnectRes.status}`,
1520
1524
  );
1521
- captureChatClientError(
1522
- lastReconnectError,
1523
- "reconnect-current-response",
1524
- {
1525
- status: reconnectRes.status,
1526
- hasBody: Boolean(reconnectRes.body),
1527
- attempt,
1528
- },
1529
- );
1525
+ if (shouldCaptureRecoveryHttpStatus(reconnectRes.status)) {
1526
+ captureChatClientError(
1527
+ lastReconnectError,
1528
+ "reconnect-current-response",
1529
+ {
1530
+ status: reconnectRes.status,
1531
+ hasBody: Boolean(reconnectRes.body),
1532
+ attempt,
1533
+ },
1534
+ );
1535
+ }
1530
1536
  reconnectErrorCaptured = true;
1531
1537
  break;
1532
1538
  }
@@ -1610,11 +1616,13 @@ export function createAgentChatAdapter(
1610
1616
  lastActiveRunError = new Error(
1611
1617
  `Active run lookup failed: ${activeRes.status}`,
1612
1618
  );
1613
- captureChatClientError(
1614
- lastActiveRunError,
1615
- "reconnect-active-response",
1616
- { status: activeRes.status, attempt },
1617
- );
1619
+ if (shouldCaptureRecoveryHttpStatus(activeRes.status)) {
1620
+ captureChatClientError(
1621
+ lastActiveRunError,
1622
+ "reconnect-active-response",
1623
+ { status: activeRes.status, attempt },
1624
+ );
1625
+ }
1618
1626
  return false;
1619
1627
  }
1620
1628
  const active = await activeRes.json();
@@ -630,6 +630,72 @@ function buildReplayBody(
630
630
  return `${envelopeJson.slice(0, -1)},"events":[${eventJsonParts.join(",")}]}`;
631
631
  }
632
632
 
633
+ interface ReplayUploadBody {
634
+ body: BodyInit;
635
+ headers: Record<string, string>;
636
+ compressed: boolean;
637
+ }
638
+
639
+ async function gzipReplayBody(body: string): Promise<Blob | null> {
640
+ if (
641
+ typeof CompressionStream === "undefined" ||
642
+ typeof Blob === "undefined" ||
643
+ typeof Response === "undefined"
644
+ ) {
645
+ return null;
646
+ }
647
+ try {
648
+ const stream = new Blob([body], { type: "application/json" })
649
+ .stream()
650
+ .pipeThrough(new CompressionStream("gzip"));
651
+ const compressed = await new Response(stream).arrayBuffer();
652
+ return new Blob([compressed], { type: "application/json" });
653
+ } catch {
654
+ return null;
655
+ }
656
+ }
657
+
658
+ async function buildReplayUploadBody(body: string): Promise<ReplayUploadBody> {
659
+ const compressed = await gzipReplayBody(body);
660
+ if (compressed) {
661
+ return {
662
+ body: compressed,
663
+ compressed: true,
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ "Content-Encoding": "gzip",
667
+ },
668
+ };
669
+ }
670
+ return {
671
+ body,
672
+ compressed: false,
673
+ headers: {
674
+ "Content-Type": "text/plain;charset=UTF-8",
675
+ },
676
+ };
677
+ }
678
+
679
+ async function sendReplayUpload(
680
+ options: NormalizedSessionReplayOptions,
681
+ body: string,
682
+ ): Promise<void> {
683
+ const upload = await buildReplayUploadBody(body);
684
+ if (!upload.compressed && navigator.sendBeacon) {
685
+ const sent = navigator.sendBeacon(options.endpoint, body);
686
+ if (sent) return;
687
+ }
688
+ await fetch(options.endpoint, {
689
+ method: "POST",
690
+ body: upload.body,
691
+ keepalive: true,
692
+ headers: {
693
+ ...upload.headers,
694
+ "X-Agent-Native-Analytics-Key": options.publicKey,
695
+ },
696
+ }).catch(() => {});
697
+ }
698
+
633
699
  function isFinalFlushReason(reason: string): boolean {
634
700
  return [
635
701
  "manual",
@@ -649,19 +715,7 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
649
715
  if (!body || !state.options) return;
650
716
  state.flushing = true;
651
717
  try {
652
- if (navigator.sendBeacon) {
653
- const sent = navigator.sendBeacon(state.options.endpoint, body);
654
- if (sent) return;
655
- }
656
- await fetch(state.options.endpoint, {
657
- method: "POST",
658
- body,
659
- keepalive: true,
660
- headers: {
661
- "Content-Type": "text/plain;charset=UTF-8",
662
- "X-Agent-Native-Analytics-Key": state.options.publicKey,
663
- },
664
- }).catch(() => {});
718
+ await sendReplayUpload(state.options, body);
665
719
  } finally {
666
720
  state.flushing = false;
667
721
  }
@@ -121,9 +121,9 @@ const llmStep: OnboardingStep = {
121
121
  await import("../server/credential-provider.js");
122
122
  if (await resolveHasCompleteBuilderConnection()) return true;
123
123
  } catch {
124
- if (process.env.BUILDER_PRIVATE_KEY && process.env.BUILDER_PUBLIC_KEY) {
125
- return true;
126
- }
124
+ // Credential storage may be unavailable during early boot. Do not fall
125
+ // back to deployment-level Builder env here; the scoped resolver owns the
126
+ // policy for when that is safe.
127
127
  }
128
128
  try {
129
129
  if (await detectEngineFromUserSecrets()) return true;
@@ -356,12 +356,16 @@ export function mountActionRoutes(
356
356
  }
357
357
  }
358
358
 
359
- // If the action returned a string, try to parse as JSON for a clean response
359
+ // If the action returned a string, try to parse as JSON for a
360
+ // clean response. Plain strings still need to go over the HTTP
361
+ // action transport as JSON, otherwise H3 sends text/plain and the
362
+ // browser action client rejects the successful 2xx response.
360
363
  if (typeof result === "string") {
361
364
  try {
362
365
  return JSON.parse(result);
363
366
  } catch {
364
- return result;
367
+ setResponseHeader(event, "Content-Type", "application/json");
368
+ return JSON.stringify(result);
365
369
  }
366
370
  }
367
371
 
@@ -147,6 +147,12 @@ import {
147
147
  import { dbExecToolParameters } from "../scripts/db/tool-schemas.js";
148
148
  import { getSetting, putSetting } from "../settings/store.js";
149
149
  import { discoverAgents } from "./agent-discovery.js";
150
+ import {
151
+ resolveAgentRunOwnerContext,
152
+ runWithAgentRunContext,
153
+ seedBackgroundAgentRunOwnerContext,
154
+ type AgentRunOwnerContext,
155
+ } from "./agent-run-context.js";
150
156
  import {
151
157
  AGENT_TEAM_PROCESS_RUN_PATH,
152
158
  getCurrentDelegationDepth,
@@ -159,6 +165,7 @@ import {
159
165
  getBuilderBrowserConnectUrlForOwner,
160
166
  resolveBuilderBranchProjectId,
161
167
  } from "./builder-browser.js";
168
+ import { captureError } from "./capture-error.js";
162
169
  import { captureCliOutput } from "./cli-capture.js";
163
170
  import {
164
171
  getH3App,
@@ -871,14 +878,51 @@ export function assembleA2AFinalResponse(
871
878
  toolResults: readonly A2AToolResultSummary[],
872
879
  options: A2AArtifactResponseOptions & { event?: any } = {},
873
880
  ): { responseText: string; finalText: string } {
874
- const responseText = collectFinalResponseTextFromAgentEvents(events);
881
+ const terminalError = getA2ATerminalErrorEvent(events);
882
+ const responseText = collectFinalResponseTextFromAgentEvents(events, {
883
+ fallbackToPreToolText: !terminalError,
884
+ });
875
885
  const finalText = appendA2AArtifactLinks(responseText, [...toolResults], {
876
886
  baseUrl: options.baseUrl ?? resolveArtifactBaseUrl(options.event),
877
887
  includeReferencedArtifacts: true,
878
888
  });
889
+ if (terminalError && !finalText.trim()) {
890
+ throw new Error(formatA2ATerminalError(terminalError));
891
+ }
879
892
  return { responseText, finalText };
880
893
  }
881
894
 
895
+ function getA2ATerminalErrorEvent(
896
+ events: readonly AgentChatEvent[],
897
+ ): Extract<AgentChatEvent, { type: "error" }> | null {
898
+ for (let i = events.length - 1; i >= 0; i--) {
899
+ const event = events[i];
900
+ if (event.type === "clear") continue;
901
+ if (event.type === "done") return null;
902
+ if (event.type === "error") return event;
903
+ if (event.type === "auto_continue") {
904
+ return {
905
+ type: "error",
906
+ error: `Agent stopped before finishing (${event.reason}).`,
907
+ errorCode: event.reason,
908
+ recoverable: true,
909
+ };
910
+ }
911
+ }
912
+ return null;
913
+ }
914
+
915
+ function formatA2ATerminalError(
916
+ event: Extract<AgentChatEvent, { type: "error" }>,
917
+ ): string {
918
+ const parts = [
919
+ event.error || "Agent failed before producing a final response.",
920
+ event.errorCode ? `code: ${event.errorCode}` : "",
921
+ event.details ? `details: ${event.details}` : "",
922
+ ].filter(Boolean);
923
+ return parts.join("\n");
924
+ }
925
+
882
926
  /**
883
927
  * Creates the `get-framework-context` tool. Returns detailed instructions
884
928
  * for framework capabilities that are summarized in the compact prompt.
@@ -3052,6 +3096,8 @@ In Act mode, if the user asks for generated interactive UI in chat, choose the s
3052
3096
 
3053
3097
  These are **NOT** source-code changes and do **NOT** go through \`connect-builder\`. Extensions are sandboxed mini-apps — no source files are touched, no PR is opened, no build is required. Saved extensions can be edited later via \`update-extension\`.
3054
3098
 
3099
+ If the app exposes native actions or instructions for dashboards, reports, analyses, charts, documents, decks, or other domain artifacts, use those app-native actions first. Choose an extension only when the user explicitly asks for an extension/custom mini-app, or when the app's native artifact format cannot faithfully express the requested interaction.
3100
+
3055
3101
  Keep \`create-extension\` payloads compact enough to finish quickly. For complex extensions, create a useful working v1 first, then call \`update-extension\` with focused edits for refinements instead of trying to assemble one enormous initial tool input.
3056
3102
 
3057
3103
  Generated UI content can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send(...)/sendToAgentChat(...). It can receive chat inputs through slotContext/window.onSlotContext. Use agentNative.ui.output for passive current values from knobs, sliders, selections, and controls; it writes application state at \`inline-ui:<extensionId>:output\` scoped to the inline extension id returned by \`render-inline-extension\` or \`show-extension-inline\`. When the user later says "use that value", "apply the current setting", or similar, read it with \`readAppState("inline-ui:<id>:output")\` instead of asking them to send it again. Use agentNative.chat.send for visible submit/apply actions that should put a message into chat. Transient extensionData is browser-local and not agent-readable, synced, promoted, or garbage-collected; use application_state/appFetch, appAction, ui.output, or chat.send for anything the agent or app must observe. Use semantic Tailwind classes like bg-background, text-foreground, bg-primary, border-border, and text-muted-foreground so the UI inherits the parent app theme.
@@ -3079,7 +3125,7 @@ Route by what the request changes, not how it is phrased. Extensions render in t
3079
3125
  | Ambiguous, satisfiable either way (e.g. "give me an unread view") | \`render-inline-extension\` for chat-only, \`create-extension\` for reusable |
3080
3126
  </routing>
3081
3127
 
3082
- Worked examples: "a widget showing unread emails grouped by sender", "a dashboard summarizing my pipeline", "a tracker for my newsletter subscriptions" → \`create-extension\`. "Add an Unread tab to the left navigation", "make the subject lines wrap", "change the inbox grouping logic", "add a field to the compose form" → \`connect-builder\`.`
3128
+ Worked examples: "a widget showing unread emails grouped by sender", "a tracker for my newsletter subscriptions", "a custom kanban board with drag-and-drop rules the app does not have" → \`create-extension\`. "Add an Unread tab to the left navigation", "make the subject lines wrap", "change the inbox grouping logic", "add a field to the compose form" → \`connect-builder\`.`
3083
3129
  : `### Extensions Disabled
3084
3130
 
3085
3131
  Extension creation and management tools are disabled for this app. Do not claim you can create, edit, hide, or delete Agent-Native extensions unless the template exposes its own typed action for that workflow. For requests that would otherwise be handled as an extension/widget/dashboard/calculator mini-app, explain that this app has disabled extension tools and use the app's available actions instead.`;
@@ -3088,6 +3134,8 @@ Extension creation and management tools are disabled for this app. Do not claim
3088
3134
 
3089
3135
  In Act mode, if the user asks for generated interactive UI in chat, call \`render-inline-extension\` for one-time inline controls/knobs/calculators/visualizers that do not need saving. If the user asks for an **extension**, **widget**, **dashboard**, **calculator**, or **mini-app** that should be reusable or saved, call \`create-extension\` with a self-contained Alpine.js HTML body. To load a saved extension inline, call \`show-extension-inline\`. These are NOT code changes — extensions are sandboxed mini-apps. Do not preface with "let me build…" — just call the right extension action.
3090
3136
 
3137
+ Use app-native artifact actions first when they exist for dashboards, reports, analyses, charts, documents, decks, or similar domain artifacts. Pick \`create-extension\` only for explicit extension/custom mini-app requests or for behavior the native artifact format cannot support.
3138
+
3091
3139
  Keep the first \`create-extension\` call compact and working. If the request is complex, create the v1 first and then refine with focused \`update-extension\` edits.
3092
3140
 
3093
3141
  Generated UI can read chat inputs from slotContext/window.onSlotContext, see/update app state through appFetch/appAction, use extensionData, record passive current values through agentNative.ui.output(value, opts?), and send visible results through agentNative.chat.send(...) or sendToAgentChat(...). ui.output writes \`inline-ui:<extensionId>:output\` in application state; when the user asks to use the current slider/selection/value, read \`readAppState("inline-ui:<id>:output")\`. Transient extensionData is browser-local only, so do not rely on it for values the agent or app must observe. Use semantic Tailwind theme classes.
@@ -4994,45 +5042,13 @@ export function createAgentChatPlugin(
4994
5042
  });
4995
5043
  }
4996
5044
 
4997
- type OwnerContext = {
4998
- owner: string;
4999
- anonymous: boolean;
5000
- name?: string;
5001
- };
5002
- const OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
5003
-
5004
5045
  // Resolve owner from the H3 event's session, with an optional
5005
5046
  // template-provided anonymous owner for public read-only surfaces.
5006
- const resolveOwnerContext = async (event: any): Promise<OwnerContext> => {
5007
- const eventContext = event?.context as
5008
- | (Record<string, unknown> & { [OWNER_CONTEXT_KEY]?: OwnerContext })
5009
- | undefined;
5010
- if (eventContext?.[OWNER_CONTEXT_KEY]) {
5011
- return eventContext[OWNER_CONTEXT_KEY];
5012
- }
5013
-
5014
- const session = await getSession(event);
5015
- if (session?.email) {
5016
- const resolved = {
5017
- owner: session.email,
5018
- anonymous: false,
5019
- name: session.name,
5020
- };
5021
- if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
5022
- return resolved;
5023
- }
5024
-
5025
- const anonymousOwner = await options?.anonymousOwner?.(event);
5026
- if (anonymousOwner) {
5027
- const resolved = { owner: anonymousOwner, anonymous: true };
5028
- if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
5029
- return resolved;
5030
- }
5031
-
5032
- const { createError } = await import("h3");
5033
- throw createError({
5034
- statusCode: 401,
5035
- statusMessage: "Unauthenticated",
5047
+ const resolveOwnerContext = async (
5048
+ event: any,
5049
+ ): Promise<AgentRunOwnerContext> => {
5050
+ return resolveAgentRunOwnerContext(event, {
5051
+ anonymousOwner: options?.anonymousOwner,
5036
5052
  });
5037
5053
  };
5038
5054
 
@@ -7749,92 +7765,17 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7749
7765
  // the background worker), so both go through identical context + handler
7750
7766
  // selection.
7751
7767
  const invokeAgentChatHandler = async (event: any) => {
7752
- // Resolve per-request auth context
7768
+ // Resolve per-request auth context.
7753
7769
  const ownerContext = await resolveOwnerContext(event);
7754
- const owner = ownerContext.owner;
7755
-
7756
- // Resolve org ID: explicit callback > session.orgId from Better Auth
7757
- // > implicit org membership. Better Auth leaves session.orgId null
7758
- // until the user explicitly switches orgs, so a fresh signup with
7759
- // implicit membership (e.g. domain-matched org) would otherwise see
7760
- // no org-scoped credentials. getOrgContext() does the same DB lookup
7761
- // the /builder/status endpoint uses to decide "Connected".
7762
- let resolvedOrgId: string | undefined;
7763
- if (options?.resolveOrgId) {
7764
- resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
7765
- } else {
7766
- try {
7767
- const session = await getSession(event);
7768
- resolvedOrgId = session?.orgId ?? undefined;
7769
- } catch {
7770
- // Session not available
7771
- }
7772
- if (!resolvedOrgId) {
7773
- try {
7774
- const { getOrgContext } = await import("../org/context.js");
7775
- const ctx = await getOrgContext(event);
7776
- resolvedOrgId = ctx.orgId ?? undefined;
7777
- } catch {
7778
- // org_members table may not exist yet on first boot
7779
- }
7780
- }
7781
- }
7782
- // Cookieless durable background worker: getSession()/getOrgContext()
7783
- // resolve from the (absent) session, so org-scoped credential resolution
7784
- // (the Builder engine) would miss the owner's org and resolveEngine would
7785
- // fall back to the anthropic default — bailing on a missing key before
7786
- // the worker claims its run. The owner IS known here (seeded via
7787
- // OWNER_CONTEXT_KEY from the run's thread), so resolve the org directly
7788
- // from the owner email, the same active-org logic the foreground session
7789
- // would yield.
7790
- if (!resolvedOrgId && owner) {
7791
- try {
7792
- const { resolveOrgIdForEmail } = await import("../org/context.js");
7793
- resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
7794
- } catch {
7795
- // org tables unavailable — proceed without org (foreground-equivalent)
7796
- }
7797
- }
7798
-
7799
- // DURABLE OWNER CONTEXT, PART 2: ORG.
7800
- // The cookieless `_process-run` self-dispatch (and any other background
7801
- // re-entry) resolves the owner from the run row, but has NO session —
7802
- // every org resolver above ultimately derives identity from
7803
- // getSession(event), so they all come back null here. That silently
7804
- // scopes the agent to `org_id IS NULL` rows only: it sees and writes
7805
- // resources outside the user's org, while the foreground UI — which
7806
- // carries the session cookie — resolves the real org. The result is the
7807
- // observed split where the agent can't list/read the same resources the
7808
- // UI shows and writes new rows with a null org. Resolve the org from the
7809
- // now-known owner email (the same active-org / membership logic
7810
- // getOrgContext uses, minus the session) so the agent and the UI operate
7811
- // over the same org-scoped data.
7812
- if (!resolvedOrgId && owner && !ownerContext.anonymous) {
7813
- try {
7814
- const { resolveOrgIdForEmail } = await import("../org/context.js");
7815
- resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
7816
- } catch {
7817
- // org tables unavailable (first boot) — leave org unscoped
7818
- }
7819
- }
7820
7770
 
7821
- // Propagate the caller's IANA timezone from `x-user-timezone` so that
7822
- // tool calls made by the agent (e.g. log-meal with no explicit date)
7823
- // resolve "today" in the user's local timezone instead of server UTC.
7824
- const tzRaw = getHeader(event, "x-user-timezone");
7825
- const timezone =
7826
- typeof tzRaw === "string" &&
7827
- tzRaw.trim().length > 0 &&
7828
- tzRaw.trim().length < 64
7829
- ? tzRaw.trim()
7830
- : undefined;
7831
-
7832
- return runWithRequestContext(
7771
+ return runWithAgentRunContext(
7833
7772
  {
7834
- userEmail: owner,
7835
- userName: ownerContext.name,
7836
- orgId: resolvedOrgId,
7837
- timezone,
7773
+ event,
7774
+ ownerContext,
7775
+ resolveOrgId: options?.resolveOrgId,
7776
+ isBackgroundWorker: Boolean(
7777
+ (event as any).context?.__agentChatBackgroundBody,
7778
+ ),
7838
7779
  },
7839
7780
  () => {
7840
7781
  // App-rendered chat can't host direct code edits — HMR/full
@@ -7949,50 +7890,29 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7949
7890
  (event as any).context = (event as any).context ?? {};
7950
7891
  (event as any).context.__agentChatBackgroundBody = prepared.body;
7951
7892
 
7952
- // DURABLE OWNER CONTEXT: this self-dispatch is cookieless (HMAC-only
7953
- // the foreground POST carried the user's session, but this background-
7954
- // function invocation does not). Without a session, resolveOwnerContext()
7955
- // falls through to its 401 "Unauthenticated" throw and the worker dies
7956
- // before it can even claim the run (observed in prod as the `route_threw`
7957
- // diag stage — every durable run only completed via the foreground
7958
- // circuit-breaker's inline recovery). The run's chat thread already
7959
- // records the authenticated owner (written by the foreground under the
7960
- // signed-in user), so resolve the owner from the run row — NOT from the
7961
- // request body, which the HMAC does not cover and a caller could forge —
7962
- // and pre-seed the owner context the handler reads (resolveOwnerContext
7963
- // returns it before attempting any session lookup).
7964
- let backgroundOwner: string | null = null;
7965
- try {
7966
- const { getRunOwnerEmail } = await import("../agent/run-store.js");
7967
- backgroundOwner = await getRunOwnerEmail(prepared.runId);
7968
- if (backgroundOwner) {
7969
- (event as any).context[OWNER_CONTEXT_KEY] = {
7970
- owner: backgroundOwner,
7971
- anonymous: false,
7972
- };
7973
- }
7974
- } catch {
7975
- // Best-effort: if the lookup fails, resolveOwnerContext runs as before
7976
- // and throws its normal 401, surfaced via the route_threw diagnostic.
7977
- }
7893
+ // Durable owner context: this self-dispatch is cookieless (HMAC-only).
7894
+ // Resolve the owner from the persisted run row, never the request body,
7895
+ // then invoke the normal handler. The shared agent-run context helper
7896
+ // expands that owner into the same user/org AsyncLocalStorage context the
7897
+ // foreground request uses, so credential and data scoping stay aligned.
7898
+ await seedBackgroundAgentRunOwnerContext(event, prepared.runId);
7978
7899
 
7979
7900
  try {
7980
- // Run the worker INSIDE the owner's AsyncLocalStorage request context.
7981
- // Seeding OWNER_CONTEXT_KEY (above) only feeds getOwnerFromEvent; the
7982
- // engine-resolution path (detectEngineFromUserSecrets) and other
7983
- // owner-scoped reads use getRequestUserEmail()/getRequestOrgId(),
7984
- // which a cookieless background worker otherwise leaves empty. Without
7985
- // this, resolveEngine can't see the owner's Builder credential, falls
7986
- // back to the anthropic default, and — when the owner has no stored
7987
- // anthropic key and deploy-credential fallback is blocked — bails at
7988
- // the API-key check BEFORE claiming its run (the analytics durable
7989
- // worker's "stalls at post_model, never claims" bug).
7990
- const invoke = () => invokeAgentChatHandler(event);
7991
- return await (backgroundOwner
7992
- ? runWithRequestContext({ userEmail: backgroundOwner }, invoke)
7993
- : invoke());
7901
+ return await invokeAgentChatHandler(event);
7994
7902
  } catch (err: any) {
7995
7903
  console.error("[agent-chat] _process-run failed:", err);
7904
+ captureError(err, {
7905
+ route: AGENT_CHAT_PROCESS_RUN_PATH,
7906
+ method: getMethod(event),
7907
+ userAgent: getHeader(event, "user-agent"),
7908
+ tags: {
7909
+ source: "agent-chat-bg-worker",
7910
+ phase: "process-run",
7911
+ },
7912
+ extra: {
7913
+ runId: prepared.runId,
7914
+ },
7915
+ });
7996
7916
  // DIAGNOSTIC: the worker invocation threw at the route boundary —
7997
7917
  // record the message so the failure cause is readable client-side.
7998
7918
  if (diag) {