@agent-native/core 0.161.22 → 0.162.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 (125) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
  4. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  5. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  6. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  7. package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
  8. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
  10. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
  11. package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
  12. package/corpus/templates/clips/desktop/src/app.tsx +30 -40
  13. package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
  14. package/corpus/templates/clips/desktop/src/styles.css +110 -0
  15. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
  16. package/corpus/templates/clips/shared/recording-capture.ts +18 -13
  17. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
  20. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
  21. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
  22. package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
  23. package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
  24. package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
  25. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +13 -7
  26. package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
  27. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
  28. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
  29. package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
  30. package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
  31. package/corpus/templates/design/playwright.config.ts +19 -2
  32. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  33. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  34. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  35. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  36. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  37. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  38. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  39. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  40. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  41. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  42. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  43. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  44. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  45. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  46. package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
  47. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  48. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  49. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  50. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  51. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  52. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  53. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  54. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  55. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  56. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  57. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  58. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  59. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  60. package/corpus/templates/slides/shared/api.ts +7 -1
  61. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  62. package/dist/agent/engine/builder-engine.js +15 -8
  63. package/dist/agent/engine/translate-anthropic.js +27 -2
  64. package/dist/agent/production-agent.d.ts +17 -2
  65. package/dist/agent/production-agent.js +48 -17
  66. package/dist/agent/thread-data-builder.d.ts +1 -0
  67. package/dist/agent/thread-data-builder.js +6 -4
  68. package/dist/agent/tool-approval-store.d.ts +10 -1
  69. package/dist/agent/tool-approval-store.js +19 -2
  70. package/dist/agent/types.d.ts +9 -0
  71. package/dist/client/AssistantChat.js +14 -6
  72. package/dist/client/EnvironmentBadge.d.ts +15 -0
  73. package/dist/client/EnvironmentBadge.js +79 -0
  74. package/dist/client/MultiTabAssistantChat.js +73 -17
  75. package/dist/client/agent-chat-adapter.js +11 -1
  76. package/dist/client/api-path.js +14 -1
  77. package/dist/client/app-providers.js +2 -1
  78. package/dist/client/chat/tool-call-display.d.ts +12 -2
  79. package/dist/client/chat/tool-call-display.js +17 -7
  80. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  81. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  82. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  83. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  84. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  85. package/dist/client/onboarding/use-onboarding.js +22 -7
  86. package/dist/client/session-replay.js +58 -15
  87. package/dist/client/settings/SettingsTabsPage.js +19 -6
  88. package/dist/client/sse-event-processor.d.ts +7 -0
  89. package/dist/client/sse-event-processor.js +4 -1
  90. package/dist/client/ui/index.d.ts +1 -0
  91. package/dist/client/ui/index.js +1 -0
  92. package/dist/client/use-action.js +26 -0
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/config.d.ts +15 -0
  96. package/dist/config.js +53 -0
  97. package/dist/extensions/fetch-tool.js +8 -2
  98. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  99. package/dist/mcp/embed-app.js +1 -1
  100. package/dist/observability/routes.d.ts +5 -5
  101. package/dist/progress/routes.d.ts +1 -1
  102. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  103. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  104. package/dist/provider-api/index.d.ts +10 -0
  105. package/dist/provider-api/index.js +45 -9
  106. package/dist/secrets/routes.d.ts +9 -9
  107. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  108. package/dist/server/auth.js +10 -3
  109. package/dist/server/poll.js +7 -3
  110. package/dist/server/sentry.d.ts +1 -1
  111. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  112. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  113. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  114. package/dist/vite/client.d.ts +5 -0
  115. package/dist/vite/client.js +64 -2
  116. package/dist/workspace-files/actions.d.ts +1 -8
  117. package/dist/workspace-files/actions.js +5 -21
  118. package/dist/workspace-files/index.d.ts +1 -1
  119. package/dist/workspace-files/index.js +1 -1
  120. package/dist/workspace-files/store.d.ts +18 -0
  121. package/dist/workspace-files/store.js +24 -3
  122. package/package.json +3 -3
  123. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  124. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  125. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  FeatureNotConfiguredError,
3
- getSession,
4
3
  indexBuilderDesignSystem,
5
4
  } from "@agent-native/core/server";
6
5
  import { defineEventHandler, readBody, setResponseStatus } from "h3";
7
6
 
8
7
  import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
9
- import { withSlidesRequestContext } from "./request-auth-context.js";
8
+ import {
9
+ resolveSlidesRequestAuth,
10
+ withSlidesRequestContext,
11
+ } from "./request-auth-context.js";
10
12
 
11
13
  /**
12
14
  * Finalizes Builder DSI indexing from upload tokens produced by the
@@ -14,8 +16,14 @@ import { withSlidesRequestContext } from "./request-auth-context.js";
14
16
  * storage; this endpoint only forwards the opaque tokens.
15
17
  */
16
18
  export const indexDesignSystemSources = defineEventHandler(async (event) => {
17
- const session = await getSession(event).catch(() => null);
18
- if (!session?.email) {
19
+ const auth = await resolveSlidesRequestAuth(event);
20
+ if (!auth.ok) {
21
+ setResponseStatus(event, auth.statusCode);
22
+ return { error: auth.error };
23
+ }
24
+ const session = auth.context;
25
+ const sessionEmail = session.email;
26
+ if (!sessionEmail) {
19
27
  setResponseStatus(event, 401);
20
28
  return { error: "Unauthorized" };
21
29
  }
@@ -45,21 +53,28 @@ export const indexDesignSystemSources = defineEventHandler(async (event) => {
45
53
  }));
46
54
 
47
55
  try {
48
- return await withSlidesRequestContext(event, async ({ email, orgId }) => {
49
- const result = await indexBuilderDesignSystem({ sources, projectName });
50
- const proxy = await upsertBuilderProxyDesignSystem({
51
- result,
52
- ownerEmail: email ?? session.email,
53
- orgId: orgId ?? null,
54
- projectName,
55
- sourceKind: "figma",
56
- });
57
- return {
58
- ...result,
59
- ...proxy,
60
- uploadedFileCount: uploadTokens.length,
61
- };
62
- });
56
+ return await withSlidesRequestContext(
57
+ event,
58
+ async ({ email, orgId }) => {
59
+ const result = await indexBuilderDesignSystem({
60
+ sources,
61
+ projectName,
62
+ });
63
+ const proxy = await upsertBuilderProxyDesignSystem({
64
+ result,
65
+ ownerEmail: email ?? sessionEmail,
66
+ orgId: orgId ?? null,
67
+ projectName,
68
+ sourceKind: "figma",
69
+ });
70
+ return {
71
+ ...result,
72
+ ...proxy,
73
+ uploadedFileCount: uploadTokens.length,
74
+ };
75
+ },
76
+ session,
77
+ );
63
78
  } catch (err) {
64
79
  if (err instanceof FeatureNotConfiguredError) {
65
80
  setResponseStatus(event, 412);
@@ -116,7 +116,7 @@ async function createShareLink(event: any, deckId: string) {
116
116
  }
117
117
 
118
118
  const slides = storedDeck.slides.map((slide: unknown, index: number) =>
119
- toSharedDeckSlide(slide, index),
119
+ toSharedDeckSlide(slide, index, { includeNotes: false }),
120
120
  );
121
121
 
122
122
  await db.insert(schema.deckShareLinks).values({
@@ -57,7 +57,7 @@ export default createAgentChatPlugin({
57
57
  return authContext.orgId === undefined ? null : authContext.orgId;
58
58
  },
59
59
  prepareRequest: prepareSlidesChatAttachments,
60
- systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow.
60
+ systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow. When speaker notes are requested, keep presenter-only text in each slide's notes field rather than the slide HTML, and preserve notes during source-preserving edits.
61
61
 
62
62
  When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck with compact=true for deck orientation. If you need slide markup, call get-deck with compact=false explicitly and only when the edit requires the full HTML. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true and one patch-slide operation with fields.content for every source slide ID; the action rejects partial coverage. Do not split a full-deck restyle into arbitrary batches or use one-by-one update-slide calls - reserve update-slide for targeted one-slide edits. After the patch succeeds, verify with get-deck using compact=true so the verification does not retransmit every slide's HTML. Do not claim a partial or initial pass. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
63
63
  For a targeted one-slide edit, prefer get-deck with the stable slideId from view-screen or compact get-deck. It returns only that slide's full HTML by default, so do not load the full deck when one slide is enough. For code-style edits, request compact=false and format=true, then call update-slide with that slide's contentHash as baseContentHash and an ordered edits list. Use exact replace, insert-before/after, replace-between, or regex-replace edits; include expectedMatches for ambiguous markers. The action applies the whole list atomically under the deck lock, so a failed required match writes nothing. Set format=true on update-slide when readable line breaks should be persisted. Use fullContent only for an intentional full rewrite. Pass compact=true when you only need a lightweight check of that slide.
@@ -77,7 +77,7 @@ export default defineEventHandler(async (event) => {
77
77
  const data = parseDeckData(deck.data);
78
78
  const slides = Array.isArray(data.slides)
79
79
  ? data.slides.map((slide: unknown, index: number) =>
80
- toSharedDeckSlide(slide, index),
80
+ toSharedDeckSlide(slide, index, { includeNotes: false }),
81
81
  )
82
82
  : [];
83
83
 
@@ -2,9 +2,10 @@ import { createReadStream } from "fs";
2
2
  import { stat } from "fs/promises";
3
3
  import path from "path";
4
4
 
5
- import { getSession, streamFile } from "@agent-native/core/server";
5
+ import { streamFile } from "@agent-native/core/server";
6
6
  import { defineEventHandler, getRouterParam, setResponseStatus } from "h3";
7
7
 
8
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
8
9
  import { tenantExportDir } from "../../../lib/tenant-files.js";
9
10
 
10
11
  const CONTENT_TYPES: Record<string, string> = {
@@ -15,8 +16,13 @@ const CONTENT_TYPES: Record<string, string> = {
15
16
  };
16
17
 
17
18
  export default defineEventHandler(async (event) => {
18
- const session = await getSession(event).catch(() => null);
19
- if (!session?.email) {
19
+ const auth = await resolveSlidesRequestAuth(event);
20
+ if (!auth.ok) {
21
+ setResponseStatus(event, auth.statusCode);
22
+ return { error: auth.error };
23
+ }
24
+ const session = auth.context;
25
+ if (!session.email) {
20
26
  setResponseStatus(event, 401);
21
27
  return { error: "Unauthorized" };
22
28
  }
@@ -1,10 +1,11 @@
1
- import { getSession, runWithRequestContext } from "@agent-native/core/server";
1
+ import { runWithRequestContext } from "@agent-native/core/server";
2
2
  import {
3
3
  defineEventHandler,
4
4
  readMultipartFormData,
5
5
  setResponseStatus,
6
6
  } from "h3";
7
7
 
8
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
8
9
  import { getGoogleDocsAccessToken } from "../../../lib/google-docs-oauth.js";
9
10
 
10
11
  const PPTX_CONTENT_TYPE =
@@ -28,8 +29,14 @@ const UPLOAD_URL =
28
29
  * See "Export Behavior" in `templates/slides/AGENTS.md` for the routing rule.
29
30
  */
30
31
  export default defineEventHandler(async (event) => {
31
- const session = await getSession(event).catch(() => null);
32
- if (!session?.email) {
32
+ const auth = await resolveSlidesRequestAuth(event);
33
+ if (!auth.ok) {
34
+ setResponseStatus(event, auth.statusCode);
35
+ return { error: auth.error };
36
+ }
37
+ const session = auth.context;
38
+ const sessionEmail = session.email;
39
+ if (!sessionEmail) {
33
40
  setResponseStatus(event, 401);
34
41
  return { error: "Unauthorized" };
35
42
  }
@@ -50,8 +57,8 @@ export default defineEventHandler(async (event) => {
50
57
  // be org-scoped vault secrets, and resolving them without the org reports the
51
58
  // integration as unconfigured.
52
59
  const account = await runWithRequestContext(
53
- { userEmail: session.email, orgId: session.orgId },
54
- () => getGoogleDocsAccessToken(session.email),
60
+ { userEmail: sessionEmail, orgId: session.orgId },
61
+ () => getGoogleDocsAccessToken(sessionEmail),
55
62
  );
56
63
  if (!account) {
57
64
  setResponseStatus(event, 409);
@@ -1,17 +1,19 @@
1
1
  import path from "path";
2
2
 
3
- import {
4
- getSession,
5
- readBody,
6
- runWithRequestContext,
7
- } from "@agent-native/core/server";
3
+ import { readBody, runWithRequestContext } from "@agent-native/core/server";
8
4
  import { defineEventHandler, setResponseStatus } from "h3";
9
5
 
10
6
  import exportHtmlAction from "../../../../actions/export-html.js";
7
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
11
8
 
12
9
  export default defineEventHandler(async (event) => {
13
- const session = await getSession(event).catch(() => null);
14
- if (!session?.email) {
10
+ const auth = await resolveSlidesRequestAuth(event);
11
+ if (!auth.ok) {
12
+ setResponseStatus(event, auth.statusCode);
13
+ return { error: auth.error };
14
+ }
15
+ const session = auth.context;
16
+ if (!session.email) {
15
17
  setResponseStatus(event, 401);
16
18
  return { error: "Unauthorized" };
17
19
  }
@@ -1,20 +1,22 @@
1
1
  import path from "path";
2
2
 
3
- import {
4
- getSession,
5
- readBody,
6
- runWithRequestContext,
7
- } from "@agent-native/core/server";
3
+ import { readBody, runWithRequestContext } from "@agent-native/core/server";
8
4
  import { defineEventHandler, setResponseStatus } from "h3";
9
5
 
10
6
  import exportPptxAction from "../../../../actions/export-pptx.js";
7
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
11
8
 
12
9
  const PPTX_CONTENT_TYPE =
13
10
  "application/vnd.openxmlformats-officedocument.presentationml.presentation";
14
11
 
15
12
  export default defineEventHandler(async (event) => {
16
- const session = await getSession(event).catch(() => null);
17
- if (!session?.email) {
13
+ const auth = await resolveSlidesRequestAuth(event);
14
+ if (!auth.ok) {
15
+ setResponseStatus(event, auth.statusCode);
16
+ return { error: auth.error };
17
+ }
18
+ const session = auth.context;
19
+ if (!session.email) {
18
20
  setResponseStatus(event, 401);
19
21
  return { error: "Unauthorized" };
20
22
  }
@@ -126,6 +126,11 @@ export interface SharedDeckSlide {
126
126
  splitByParagraph?: boolean;
127
127
  }
128
128
 
129
+ export interface SharedDeckSlideOptions {
130
+ /** Include presenter-only notes for an authenticated internal read. */
131
+ includeNotes?: boolean;
132
+ }
133
+
129
134
  const SHARED_SLIDE_TRANSITIONS = new Set<SharedSlideTransition>([
130
135
  "instant",
131
136
  "none",
@@ -216,12 +221,13 @@ function normalizeSlideAnimation(
216
221
  export function toSharedDeckSlide(
217
222
  value: unknown,
218
223
  index: number,
224
+ options: SharedDeckSlideOptions = {},
219
225
  ): SharedDeckSlide {
220
226
  const slide = isRecord(value) ? value : {};
221
227
  const shared: SharedDeckSlide = {
222
228
  id: normalizeString(slide.id, `slide-${index + 1}`),
223
229
  content: normalizeString(slide.content, ""),
224
- notes: "",
230
+ notes: options.includeNotes ? normalizeString(slide.notes, "") : "",
225
231
  layout: normalizeString(slide.layout, "content"),
226
232
  };
227
233
 
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
50
50
 
51
51
  For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
52
52
 
53
- 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
53
+ 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
54
54
 
55
55
  5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
56
56
 
@@ -138,10 +138,17 @@ class BuilderEngine {
138
138
  }, creditsLane);
139
139
  return;
140
140
  }
141
+ // The Builder gateway has an "auto" fallback mode, but Agent Native owns
142
+ // model selection. Always send a concrete model so the gateway cannot
143
+ // select an organization-level override or another fallback model.
144
+ const requestedModel = opts.model.trim();
145
+ const model = requestedModel.length === 0 || requestedModel === "auto"
146
+ ? BUILDER_DEFAULT_MODEL
147
+ : requestedModel;
141
148
  const messages = engineMessagesToBuilderGatewayAnthropic(opts.messages);
142
149
  const tools = engineToolsToAnthropic(opts.tools);
143
150
  const thinkingBudget = opts.providerOptions?.anthropic?.thinking?.budgetTokens;
144
- const reasoningEffort = normalizeReasoningEffortForModel(opts.model, opts.reasoningEffort ??
151
+ const reasoningEffort = normalizeReasoningEffortForModel(model, opts.reasoningEffort ??
145
152
  (typeof thinkingBudget === "number"
146
153
  ? mapReasoningEffort(thinkingBudget)
147
154
  : undefined));
@@ -200,13 +207,13 @@ class BuilderEngine {
200
207
  }
201
208
  }
202
209
  }
203
- const gptToolsRequireExplicitNoReasoning = cachedTools.length > 0 && isGPTReasoningModel(opts.model);
210
+ const gptToolsRequireExplicitNoReasoning = cachedTools.length > 0 && isGPTReasoningModel(model);
204
211
  const body = {
205
- model: opts.model,
212
+ model,
206
213
  messages: cachedMessages,
207
214
  ...(systemValue !== undefined ? { system: systemValue } : {}),
208
215
  ...(cachedTools.length > 0 ? { tools: cachedTools } : {}),
209
- max_tokens: resolveMaxOutputTokensForEngine(this.name, opts.maxOutputTokens, opts.model),
216
+ max_tokens: resolveMaxOutputTokensForEngine(this.name, opts.maxOutputTokens, model),
210
217
  ...(typeof opts.temperature === "number"
211
218
  ? { temperature: opts.temperature }
212
219
  : {}),
@@ -232,7 +239,7 @@ class BuilderEngine {
232
239
  // gateway outage are the same capture.
233
240
  const payload = JSON.stringify(body);
234
241
  const requestShape = {
235
- model: opts.model,
242
+ model,
236
243
  payloadBytes: new TextEncoder().encode(payload).length,
237
244
  toolCount: cachedTools.length,
238
245
  messageCount: cachedMessages.length,
@@ -242,7 +249,7 @@ class BuilderEngine {
242
249
  gatewayUrl.searchParams.set("apiKey", spaceId);
243
250
  const orgLabel = creds.orgName || "unknown-org";
244
251
  const tStart = Date.now();
245
- console.log(`[builder-engine] → POST ${gatewayUrl.origin}${gatewayUrl.pathname} model=${opts.model} tools=${tools.length} org=${orgLabel}`);
252
+ console.log(`[builder-engine] → POST ${gatewayUrl.origin}${gatewayUrl.pathname} model=${model} tools=${tools.length} org=${orgLabel}`);
246
253
  const gatewayTimeoutMs = getBuilderGatewayTimeoutMs();
247
254
  const gatewayAbort = createGatewayAbortSignal(opts.abortSignal, gatewayTimeoutMs);
248
255
  try {
@@ -269,7 +276,7 @@ class BuilderEngine {
269
276
  if (timedOut || isBuilderGatewayNetworkError(err)) {
270
277
  captureBuilderGatewayTransportError(err, {
271
278
  phase: "request",
272
- model: opts.model,
279
+ model,
273
280
  gatewayUrl,
274
281
  timeoutMs: gatewayAbort.effectiveTimeoutMs(),
275
282
  timedOut,
@@ -325,7 +332,7 @@ class BuilderEngine {
325
332
  }, creditsLane, requestShape);
326
333
  return;
327
334
  }
328
- yield* parseJsonlStream(reader, opts.model, {
335
+ yield* parseJsonlStream(reader, model, {
329
336
  creditsLane,
330
337
  abortSignal: gatewayAbort.signal,
331
338
  didGatewayTimeout: gatewayAbort.didTimeout,
@@ -261,17 +261,42 @@ export function backfillEngineMessagesToolResults(messages) {
261
261
  // ---------------------------------------------------------------------------
262
262
  // EngineMessage → Anthropic.MessageParam
263
263
  // ---------------------------------------------------------------------------
264
+ /**
265
+ * A thinking block replays only with the signature Anthropic issued for it, or
266
+ * (redacted) with its encrypted payload. An empty signature is not a fallback:
267
+ * the native API rejects the whole request, so a turn that streamed fine dies
268
+ * with a provider error pointing nowhere near the cause. Drop the unsendable
269
+ * block and say so instead of coercing it into a guaranteed 400.
270
+ *
271
+ * Not applied to the Builder gateway: its tolerance for an unsigned thinking
272
+ * block is unverified, so that path keeps its existing behavior rather than
273
+ * trading a working request for an untested one.
274
+ */
275
+ function replayableAnthropicPart(part) {
276
+ if (part.type !== "thinking")
277
+ return true;
278
+ if (part.redactedData || part.signature)
279
+ return true;
280
+ console.warn("[anthropic-engine] dropping a thinking block with no signature; it cannot be replayed");
281
+ return false;
282
+ }
264
283
  export function engineMessageToAnthropic(msg, opts) {
265
284
  const builderGateway = opts?.builderGateway === true;
285
+ const content = builderGateway
286
+ ? msg.content
287
+ : msg.content.filter(replayableAnthropicPart);
266
288
  return {
267
289
  role: msg.role,
268
- content: msg.content.map((p) => enginePartToAnthropic(p, builderGateway)),
290
+ content: content.map((p) => enginePartToAnthropic(p, builderGateway)),
269
291
  };
270
292
  }
271
293
  /** Messages for the Anthropic HTTP API (strict schema — no extra tool_result fields). */
272
294
  export function engineMessagesToAnthropic(messages) {
273
295
  const normalized = backfillEngineMessagesToolResults(messages);
274
- return normalized.map((m) => engineMessageToAnthropic(m));
296
+ return normalized.flatMap((m) => {
297
+ const translated = engineMessageToAnthropic(m);
298
+ return translated.content.length > 0 ? [translated] : [];
299
+ });
275
300
  }
276
301
  /**
277
302
  * Messages for the Builder LLM gateway (Gemini-backed). Same Anthropic-shaped
@@ -700,7 +700,7 @@ export interface ExecuteAgentToolCallOptions {
700
700
  threadId?: string;
701
701
  turnId?: string;
702
702
  approvedToolCalls?: string[];
703
- onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
703
+ onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
704
704
  consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
705
705
  send?: (event: AgentChatEvent) => void;
706
706
  }
@@ -877,7 +877,7 @@ export declare function runAgentLoop(opts: {
877
877
  * so client-supplied history is never the authorization source; direct loop
878
878
  * callers may omit them when they own the surrounding approval boundary.
879
879
  */
880
- onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
880
+ onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
881
881
  consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
882
882
  /**
883
883
  * In-loop processor seam (see `processors.ts`). Each processor can observe
@@ -1351,5 +1351,20 @@ export declare function resolveAgentRequestReasoningEffort({ model, requestEffor
1351
1351
  requestEffort?: unknown;
1352
1352
  configuredEffort?: ReasoningEffort;
1353
1353
  }): ReasoningEffort | undefined;
1354
+ export type AgentModelSelectionSource = "request" | "configured" | "stored" | "default";
1355
+ /**
1356
+ * Resolve the model before the engine boundary. `auto` is a UI sentinel, not
1357
+ * a model to forward to the gateway, so it gives way to an AN org/user
1358
+ * default, then the configured global engine default.
1359
+ */
1360
+ export declare function resolveAgentModelSelection(options: {
1361
+ requestModel?: string | null;
1362
+ configuredModel?: string | null;
1363
+ storedModel?: string | null;
1364
+ defaultModel: string;
1365
+ }): {
1366
+ model: string;
1367
+ source: AgentModelSelectionSource;
1368
+ };
1354
1369
  export declare function createProductionAgentHandler(options: ProductionAgentOptions): H3EventHandler;
1355
1370
  export { getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, subscribeToRun, };
@@ -4331,7 +4331,14 @@ export async function runAgentLoop(opts) {
4331
4331
  mustApprove = true;
4332
4332
  }
4333
4333
  if (mustApprove) {
4334
- await opts.onApprovalRequired?.(approvalBinding);
4334
+ // `askId` identifies THIS gate hit, distinct from any earlier ask
4335
+ // for the same approvalKey/toolCallId. A failed resume (expired
4336
+ // grant, turn-id mismatch) re-enters this branch and mints a new
4337
+ // askId, which is how the client tells "an already-approved card
4338
+ // re-rendered" apart from "a fresh ask after the grant didn't land"
4339
+ // — without it, a stale client-side "approved" mark permanently
4340
+ // hides Approve/Deny with no way to retry.
4341
+ const askId = (await opts.onApprovalRequired?.(approvalBinding)) || undefined;
4335
4342
  send({
4336
4343
  type: "tool_start",
4337
4344
  id: toolCall.id,
@@ -4343,6 +4350,7 @@ export async function runAgentLoop(opts) {
4343
4350
  tool: toolCall.name,
4344
4351
  input: toolCall.input,
4345
4352
  approvalKey,
4353
+ ...(askId ? { askId } : {}),
4346
4354
  ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
4347
4355
  });
4348
4356
  // Audit the blocked attempt: the action did NOT run, but "the agent
@@ -6275,6 +6283,27 @@ function progressStepFromAgentChatEvent(event) {
6275
6283
  export function resolveAgentRequestReasoningEffort({ model, requestEffort, configuredEffort, }) {
6276
6284
  return normalizeReasoningEffortForRequest(model, isReasoningEffort(requestEffort) ? requestEffort : configuredEffort);
6277
6285
  }
6286
+ function isConcreteModelSelection(model) {
6287
+ const normalized = typeof model === "string" ? model.trim() : "";
6288
+ return normalized.length > 0 && normalized !== "auto";
6289
+ }
6290
+ /**
6291
+ * Resolve the model before the engine boundary. `auto` is a UI sentinel, not
6292
+ * a model to forward to the gateway, so it gives way to an AN org/user
6293
+ * default, then the configured global engine default.
6294
+ */
6295
+ export function resolveAgentModelSelection(options) {
6296
+ if (isConcreteModelSelection(options.requestModel)) {
6297
+ return { model: options.requestModel, source: "request" };
6298
+ }
6299
+ if (isConcreteModelSelection(options.configuredModel)) {
6300
+ return { model: options.configuredModel, source: "configured" };
6301
+ }
6302
+ if (isConcreteModelSelection(options.storedModel)) {
6303
+ return { model: options.storedModel, source: "stored" };
6304
+ }
6305
+ return { model: options.defaultModel, source: "default" };
6306
+ }
6278
6307
  export function createProductionAgentHandler(options) {
6279
6308
  if (options.resolveActionSurface) {
6280
6309
  assertRequestActionSurfaceIsolation();
@@ -6644,21 +6673,23 @@ export function createProductionAgentHandler(options) {
6644
6673
  // DIAGNOSTIC-ONLY: bracket stored-model resolution (getStoredModelForEngine
6645
6674
  // settings read).
6646
6675
  workerStep("model_start");
6647
- const storedModel = requestModel == null && configuredModel == null
6676
+ const requestModelIsExplicit = isConcreteModelSelection(requestModel);
6677
+ const configuredModelIsExplicit = isConcreteModelSelection(configuredModel);
6678
+ const storedModel = !requestModelIsExplicit && !configuredModelIsExplicit
6648
6679
  ? await getStoredModelForEngine(engine, { appId: options.appId })
6649
6680
  : undefined;
6650
- const modelCandidate = requestModel ?? configuredModel ?? storedModel ?? engine.defaultModel;
6681
+ const modelSelection = resolveAgentModelSelection({
6682
+ requestModel,
6683
+ configuredModel,
6684
+ storedModel,
6685
+ defaultModel: engine.defaultModel,
6686
+ });
6687
+ const modelCandidate = modelSelection.model;
6651
6688
  // DIAGNOSTIC-ONLY: stored-model resolution finished.
6652
6689
  workerStep("model_done");
6653
6690
  const model = normalizeModelForEngine(engine, modelCandidate);
6654
6691
  let effectiveModel = model;
6655
- let modelSelectionSource = requestModel != null
6656
- ? "request"
6657
- : configuredModel != null
6658
- ? "configured"
6659
- : storedModel != null
6660
- ? "stored"
6661
- : "default";
6692
+ let modelSelectionSource = modelSelection.source;
6662
6693
  let experimentAssignments = [];
6663
6694
  // Database-backed experiments remain available to app operators.
6664
6695
  try {
@@ -7171,7 +7202,7 @@ export function createProductionAgentHandler(options) {
7171
7202
  };
7172
7203
  const approvalHooks = {
7173
7204
  onApprovalRequired: async (binding) => {
7174
- await createAgentToolApproval(approvalStoreBinding(binding));
7205
+ return createAgentToolApproval(approvalStoreBinding(binding));
7175
7206
  },
7176
7207
  consumeApproval: async (binding) => {
7177
7208
  if (!ownerEmail)
@@ -7211,12 +7242,12 @@ export function createProductionAgentHandler(options) {
7211
7242
  const { threadDataToEngineMessages } = await import("./thread-data-builder.js");
7212
7243
  const priorThreadData = (await getThread(effectiveThreadId))
7213
7244
  ?.threadData;
7214
- // `threadDataToEngineMessages` takes one argument and flattens tool
7215
- // calls to text by design. The second argument never existed anywhere
7216
- // in the repo, so this failed `tsc` and broke every production build
7217
- // between 16:29 and now -- it reached main only because admin merges
7218
- // do not wait for the typecheck that would have caught it.
7219
- const resumed = threadDataToEngineMessages(priorThreadData);
7245
+ // Replay what earlier chunks DID, not just what they said: the default
7246
+ // text-only rebuild drops every tool call and result, so the next chunk
7247
+ // re-runs work already committed and cannot see its output.
7248
+ const resumed = threadDataToEngineMessages(priorThreadData, {
7249
+ includeToolCalls: true,
7250
+ });
7220
7251
  if (resumed.length > 0) {
7221
7252
  const actionPreparationTool = typeof backgroundRunMarker?.actionPreparationTool === "string" &&
7222
7253
  backgroundRunMarker.actionPreparationTool.trim()
@@ -21,6 +21,7 @@ interface ContentPart {
21
21
  approval?: {
22
22
  approvalKey: string;
23
23
  dismissed?: boolean;
24
+ askId?: string;
24
25
  };
25
26
  }
26
27
  interface BuildAssistantMessageOptions {
@@ -144,7 +144,10 @@ export function buildAssistantMessage(events, runId, options = {}) {
144
144
  const matchingIndex = findApprovalToolCallIndex(content, event.tool ?? "unknown", event.toolCallId);
145
145
  const part = content[matchingIndex];
146
146
  if (part?.type === "tool-call") {
147
- part.approval = { approvalKey: event.approvalKey };
147
+ part.approval = {
148
+ approvalKey: event.approvalKey,
149
+ ...(event.askId ? { askId: event.askId } : {}),
150
+ };
148
151
  }
149
152
  continue;
150
153
  }
@@ -744,14 +747,13 @@ function assistantReplayContent(message, text) {
744
747
  ? part.args
745
748
  : {};
746
749
  assistant.push({ type: "tool-call", id, name, input });
747
- if (part.result === undefined)
748
- continue;
750
+ const result = part.result === undefined ? INTERRUPTED_TOOL_RESULT : part.result;
749
751
  results.push({
750
752
  type: "tool-result",
751
753
  toolCallId: id,
752
754
  toolName: name,
753
755
  toolInput: stringifyToolUseInputForGateway(input),
754
- content: replayedToolResultContent(part.result),
756
+ content: replayedToolResultContent(result),
755
757
  ...(part.isError === true ? { isError: true } : {}),
756
758
  });
757
759
  }
@@ -21,7 +21,16 @@ export declare function resolveAgentToolApprovalTurnId(binding: {
21
21
  requestedTurnId?: string | null;
22
22
  approvalKeys: readonly string[];
23
23
  }): Promise<string | null>;
24
- export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<void>;
24
+ /**
25
+ * Creates the durable pending-approval row and returns its id as the "ask"
26
+ * identity for this specific gate hit. Every call is a fresh row (a fresh
27
+ * id), including a second gate hit for the same `approvalKey` after a prior
28
+ * grant failed to be consumed — so the id doubles as the signal the client
29
+ * needs to tell "this approval_required is the one I already approved,
30
+ * re-rendered" apart from "this is a NEW ask for the same tool call". See
31
+ * `ApprovalAffordance` in client/chat/tool-call-display.tsx.
32
+ */
33
+ export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<string>;
25
34
  /**
26
35
  * Atomically consume the server-created approval for one logical tool call.
27
36
  * The model's call id is transport metadata and can change when Dispatch