@agent-native/core 0.125.0 → 0.127.1

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 (188) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +79 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  11. package/corpus/core/src/client/agent-chat.ts +19 -1
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  14. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  15. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  16. package/corpus/core/src/client/use-chat-models.ts +54 -37
  17. package/corpus/core/src/localization/default-messages.ts +11 -0
  18. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  19. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  20. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  21. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  22. package/corpus/core/src/server/agent-discovery.ts +1 -1
  23. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  24. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  25. package/corpus/core/src/server/email-template.ts +13 -0
  26. package/corpus/core/src/server/email.ts +40 -4
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  28. package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
  29. package/corpus/core/src/sharing/registry.ts +41 -1
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  31. package/corpus/templates/assets/actions/edit-image.ts +31 -18
  32. package/corpus/templates/assets/actions/refine-image.ts +43 -29
  33. package/corpus/templates/assets/actions/restyle-image.ts +30 -19
  34. package/corpus/templates/assets/actions/variant-slots.ts +26 -0
  35. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
  36. package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
  37. package/corpus/templates/clips/server/db/index.ts +28 -6
  38. package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
  39. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
  41. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  43. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  44. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  45. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  46. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  48. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  49. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  50. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  51. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  52. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  53. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  54. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  55. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  56. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  57. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  60. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  61. package/corpus/toolkit/CHANGELOG.md +6 -0
  62. package/corpus/toolkit/package.json +1 -1
  63. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  64. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  65. package/dist/a2a/index.d.ts +2 -0
  66. package/dist/a2a/index.d.ts.map +1 -1
  67. package/dist/a2a/index.js +1 -0
  68. package/dist/a2a/index.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +2 -2
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +6 -3
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +11 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +34 -1
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +8 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +16 -0
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  82. package/dist/client/MultiTabAssistantChat.js +60 -31
  83. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  84. package/dist/client/agent-chat.d.ts +6 -0
  85. package/dist/client/agent-chat.d.ts.map +1 -1
  86. package/dist/client/agent-chat.js +13 -1
  87. package/dist/client/agent-chat.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  89. package/dist/client/chat/tool-call-display.js +14 -0
  90. package/dist/client/chat/tool-call-display.js.map +1 -1
  91. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  92. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  93. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  94. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  95. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  96. package/dist/client/settings/AgentsSection.js +250 -30
  97. package/dist/client/settings/AgentsSection.js.map +1 -1
  98. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  99. package/dist/client/settings/SecretsSection.js +16 -2
  100. package/dist/client/settings/SecretsSection.js.map +1 -1
  101. package/dist/client/use-chat-models.d.ts.map +1 -1
  102. package/dist/client/use-chat-models.js +46 -27
  103. package/dist/client/use-chat-models.js.map +1 -1
  104. package/dist/collab/routes.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  107. package/dist/localization/default-messages.d.ts +9 -0
  108. package/dist/localization/default-messages.d.ts.map +1 -1
  109. package/dist/localization/default-messages.js +9 -0
  110. package/dist/localization/default-messages.js.map +1 -1
  111. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  112. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  113. package/dist/mcp/ask-app-task-handle.js +136 -0
  114. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  115. package/dist/mcp/builtin-tools.d.ts +3 -3
  116. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  117. package/dist/mcp/builtin-tools.js +115 -38
  118. package/dist/mcp/builtin-tools.js.map +1 -1
  119. package/dist/observability/routes.d.ts +1 -1
  120. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  121. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  122. package/dist/secrets/routes.d.ts +9 -9
  123. package/dist/server/agent-capabilities.d.ts +7 -1
  124. package/dist/server/agent-capabilities.d.ts.map +1 -1
  125. package/dist/server/agent-capabilities.js +1 -0
  126. package/dist/server/agent-capabilities.js.map +1 -1
  127. package/dist/server/agent-chat-plugin.d.ts +7 -0
  128. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  129. package/dist/server/agent-chat-plugin.js +16 -1
  130. package/dist/server/agent-chat-plugin.js.map +1 -1
  131. package/dist/server/agent-discovery.d.ts +1 -0
  132. package/dist/server/agent-discovery.d.ts.map +1 -1
  133. package/dist/server/agent-discovery.js +1 -1
  134. package/dist/server/agent-discovery.js.map +1 -1
  135. package/dist/server/agent-peer-probe.d.ts +37 -0
  136. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  137. package/dist/server/agent-peer-probe.js +90 -0
  138. package/dist/server/agent-peer-probe.js.map +1 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +53 -0
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/dist/server/email-template.d.ts +6 -0
  143. package/dist/server/email-template.d.ts.map +1 -1
  144. package/dist/server/email-template.js +6 -0
  145. package/dist/server/email-template.js.map +1 -1
  146. package/dist/server/email.d.ts +7 -0
  147. package/dist/server/email.d.ts.map +1 -1
  148. package/dist/server/email.js +31 -6
  149. package/dist/server/email.js.map +1 -1
  150. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  151. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  152. package/dist/shared/mcp-embed-headers.js +4 -0
  153. package/dist/shared/mcp-embed-headers.js.map +1 -1
  154. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  155. package/dist/sharing/actions/share-resource.js +49 -5
  156. package/dist/sharing/actions/share-resource.js.map +1 -1
  157. package/dist/sharing/registry.d.ts +40 -1
  158. package/dist/sharing/registry.d.ts.map +1 -1
  159. package/dist/sharing/registry.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  161. package/docs/content/external-agents-catalog.mdx +14 -11
  162. package/docs/content/mcp-protocol.mdx +9 -4
  163. package/package.json +2 -2
  164. package/src/a2a/index.ts +2 -0
  165. package/src/agent/production-agent.ts +6 -1
  166. package/src/agent/run-manager.ts +37 -0
  167. package/src/agent/run-store.ts +18 -0
  168. package/src/client/MultiTabAssistantChat.tsx +75 -43
  169. package/src/client/agent-chat.ts +19 -1
  170. package/src/client/chat/tool-call-display.tsx +25 -0
  171. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  172. package/src/client/settings/AgentsSection.tsx +565 -71
  173. package/src/client/settings/SecretsSection.tsx +71 -36
  174. package/src/client/use-chat-models.ts +54 -37
  175. package/src/localization/default-messages.ts +11 -0
  176. package/src/mcp/ask-app-task-handle.ts +183 -0
  177. package/src/mcp/builtin-tools.ts +146 -34
  178. package/src/server/agent-capabilities.ts +8 -1
  179. package/src/server/agent-chat-plugin.ts +17 -0
  180. package/src/server/agent-discovery.ts +1 -1
  181. package/src/server/agent-peer-probe.ts +133 -0
  182. package/src/server/core-routes-plugin.ts +67 -0
  183. package/src/server/email-template.ts +13 -0
  184. package/src/server/email.ts +40 -4
  185. package/src/shared/mcp-embed-headers.ts +4 -0
  186. package/src/sharing/actions/share-resource.ts +55 -5
  187. package/src/sharing/registry.ts +41 -1
  188. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1737
31
+ - core files: 1739
32
32
  - toolkit files: 160
33
- - template files: 6804
33
+ - template files: 6815
@@ -1,5 +1,84 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.127.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bbd202b: Fix the model picker offering a model the app cannot route, and the chat bridge dropping submitted model overrides.
8
+ - The picker now defaults only to a configured engine group, and shows nothing when none is configured. `DEFAULT_MODEL` is a builder-gateway id that no group carries unless Builder is connected, so the old `?? DEFAULT_MODEL` / `?? groups[0]` fallbacks produced a selection the server silently replaced with its own default.
9
+ - A submitted `model`/`engine` pair is now applied regardless of whether the engine list has loaded, travels with cold-start queued sends, keeps the sender's engine, and treats a blank engine as absent. Previously it was honored only when the model already appeared in the (initially empty) engine list, so app-initiated first turns lost it.
10
+ - `[agent-chat] resolved …` now logs `requestModel` and `turnId`, making a server-side model substitution visible.
11
+
12
+ ## 0.127.0
13
+
14
+ ### Minor Changes
15
+
16
+ - cbc6936: Make connecting one agent-native app to another a guided flow instead of three
17
+ blank text fields.
18
+ - New `GET /_agent-native/agents/probe` reads a peer's agent card and makes one
19
+ authenticated no-op call, reporting `reachable` and `authorized` as
20
+ independent fields. A peer that answers but rejects the caller's token is the
21
+ failure local dev hides — the receiver runs unauthenticated on localhost, so a
22
+ mismatched secret previously surfaced only after deploy.
23
+ - Settings → Manage agent → Connected Agents is URL-first: paste a peer URL,
24
+ press Check, and the name and description come from its card. Unreachable
25
+ never blocks the save. Rows carry a liveness dot from one batched probe.
26
+ - The section now shows shared-secret state and a Sync to apps action inline,
27
+ reusing the existing org hooks. A caller who cannot see the secret is told so
28
+ rather than being shown "not set".
29
+ - After an add, the UI states that registration is one-directional and deep
30
+ links to the peer's own settings with the values prefilled.
31
+ - The Connected Agents list collapses a remote agent that still has its
32
+ pre-migration `agents/*.json` row alongside the canonical
33
+ `remote-agents/*.json` one, instead of listing it twice with the same URL.
34
+ - `list-connected-agents` keys custom manifests by the normalized agent id, so
35
+ an agent registered as `images`/`asset` no longer appears once as a discovered
36
+ agent and again as a custom one.
37
+ - Export `resolveA2ACallerAuth` from `@agent-native/core/a2a` so app code can
38
+ authenticate outbound A2A calls without reimplementing org-secret lookup.
39
+ - The `a2a-protocol` skill documents the real setup path — A2A is auto-mounted,
40
+ peers are `remote-agents/*.json` resources, and auth is a JWT signed with
41
+ `A2A_SECRET` or the per-org secret — replacing the `mountA2A` + per-peer
42
+ `apiKeyEnv` flow the framework no longer wires up.
43
+
44
+ ### Patch Changes
45
+
46
+ - f8df095: Keep cross-app `ask_app_status` polling attached to the original task route with an encrypted task handle, report the configured app identity in standalone MCP discovery, and reject unknown cross-app targets instead of running them locally.
47
+ - cbc6936: Hide pending workspace apps by default and expose app ownership metadata from each card's overflow menu.
48
+ - cbc6936: Show the provider's brand logo on agent tool rows for catalog-backed MCP tools, instead of a generic code icon.
49
+ - cbc6936: Settings → Connections: the "New" key menu is now a searchable, scrolling combobox instead of a cropped dropdown, so every registered key is reachable.
50
+ - cbc6936: Send `X-Content-Type-Options: nosniff` on CDN-served static assets.
51
+
52
+ `/assets/**`, `/favicon.*`, `/manifest.json`, `/icon-*.svg`, and
53
+ `/library-presets/**` are served straight off the CDN, so the security-headers
54
+ h3 middleware never runs for them and they shipped without the `nosniff` header
55
+ that every function-served response already carries.
56
+
57
+ - cbc6936: Make the chat Stop button actually stop the turn.
58
+
59
+ A turn runs as a chain of runs: every `loop_limit` / `auto_continue` boundary
60
+ and every background handoff starts a successor under a new run id but the same
61
+ turn id. Stop only aborted the run id the client happened to hold, so the
62
+ successor claimed itself and the agent kept looping — the turn-abort marker that
63
+ `isTurnAborted` consults was only ever written on the pre-run path, which is
64
+ dead as soon as a run id exists.
65
+
66
+ `POST /runs/:id/abort` now escalates user-intent reasons (`user`, `abort`,
67
+ `user_stuck_cancel`, `user_stuck_retry`) to a turn-wide abort. Watchdog reasons
68
+ (`no_progress`, `auto_stuck_retry`) keep single-run semantics so they cannot
69
+ kill a server-side continuation chain that is still making progress.
70
+
71
+ - Updated dependencies [cbc6936]
72
+ - @agent-native/toolkit@0.10.9
73
+
74
+ ## 0.126.0
75
+
76
+ ### Minor Changes
77
+
78
+ - b99b899: **Breaking:** rename the `registerShareableResource` resolver `getShareEmailLogoUrl` (added in 0.125.0) to `getLogoUrl`. Update any registration that used the old name.
79
+
80
+ Add `getBrandName`, `getSender`, and `getHeroHtml` resolvers to `registerShareableResource`, plus an optional `heroHtml` on `renderEmail` and `fromName` on `sendEmail`. Share-notification emails can now render a template-owned preview block above the CTA (injected verbatim, so a template supplies its own markup — e.g. a video thumbnail with a play badge), override the brand name shown beside the logo, and appear to come from the sharing user ("Alice via Clips") with replies routed to them while keeping the configured domain-verified sending address so SPF/DKIM still pass.
81
+
3
82
  ## 0.125.0
4
83
 
5
84
  ### Minor Changes
@@ -225,13 +225,13 @@ In practice, every agent-native app should be authored with both: MCP Apps for i
225
225
 
226
226
  On top of the per-action tools the MCP server exposes a stable verb set, so an external agent has a predictable surface without guessing per-app action names:
227
227
 
228
- | Tool | Side effects | Returns |
229
- | -------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
230
- | `list_apps` | none | workspace apps + their URLs / running state |
231
- | `open_app({ app, view?, path?, params?, embed? })` | none | a deep link or same-origin route; `embed: true` renders the full app inline where supported |
232
- | `ask_app({ app, message, async?, maxWaitMs? })` | agent loop | routes a natural-language task to that app's in-app agent; long work returns a durable `taskId` for `ask_app_status` polling |
233
- | `create_workspace_app({ name, template })` | scaffolds | a new app booted via the workspace path, plus its running URL + deep link |
234
- | `list_templates` | none | the allow-listed templates only |
228
+ | Tool | Side effects | Returns |
229
+ | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
230
+ | `list_apps` | none | workspace apps + their URLs / running state |
231
+ | `open_app({ app, view?, path?, params?, embed? })` | none | a deep link or same-origin route; `embed: true` renders the full app inline where supported |
232
+ | `ask_app({ app, message, async?, maxWaitMs? })` | agent loop | routes a natural-language task to that app's in-app agent; long work returns a durable `taskHandle` plus a legacy `taskId` for `ask_app_status` polling |
233
+ | `create_workspace_app({ name, template })` | scaffolds | a new app booted via the workspace path, plus its running URL + deep link |
234
+ | `list_templates` | none | the allow-listed templates only |
235
235
 
236
236
  `create_workspace_app` rejects any non-allow-listed template — the public template allow-list in `packages/shared-app-config/templates.ts` is authoritative and CI-guarded; an external agent cannot widen it. A same-named template action overrides a builtin (template-over-core precedence). Disable the whole set with `MCPConfig.builtinCrossAppTools: false`.
237
237
 
@@ -244,9 +244,10 @@ For fast ChatGPT/Claude handoffs, the ideal path is direct: call the action that
244
244
  MCP hosts and gateways do not share one long tool-call timeout; some enforce a
245
245
  five-minute ceiling and others are configurable. Do not design an external
246
246
  agent integration around a ten-minute blocking `tools/call`. `ask-agent` and
247
- `ask_app` acknowledge hosted work quickly, then return a durable `taskId` when
248
- the inline wait expires. Poll with `ask_app_status` using the returned `app`
249
- and `taskId`; the result includes `pollAfterMs` as a host-friendly interval.
247
+ `ask_app` acknowledge hosted work quickly, then return a durable `taskHandle`
248
+ plus a legacy `taskId` when the inline wait expires. Poll with `ask_app_status`
249
+ by replaying the returned `poll.arguments` unchanged, especially the
250
+ `taskHandle`; the result includes `pollAfterMs` as a host-friendly interval.
250
251
  The task must be created before the first response can time out, so a polling
251
252
  URL returned only after a long call is not a reliable recovery contract.
252
253
 
@@ -254,7 +255,9 @@ If a bounded transient status read is unavailable, `ask_app_status` can return
254
255
  `status: "unknown"`, `statusRead: "unavailable"`, `retryable: true`, the
255
256
  original `app` and `taskId`, and `pollAfterMs` / `poll`. This means the status
256
257
  read is unavailable—not that the durable task failed. Retry `ask_app_status`
257
- with that exact `app` and `taskId`; never resubmit `ask_app` to recover, because
258
+ with the exact returned `poll.arguments`, including `taskHandle` when present;
259
+ legacy results without a handle must reuse the same `app` and `taskId`. Never
260
+ resubmit `ask_app` to recover, because
258
261
  submission can duplicate work. Permanent authentication, not-found, and
259
262
  protocol failures remain errors.
260
263
 
@@ -235,7 +235,11 @@ The agent runs the same loop as the interactive chat — it can call multiple to
235
235
  {
236
236
  "poll": {
237
237
  "tool": "ask_app_status",
238
- "arguments": { "app": "mail", "taskId": "task_123" }
238
+ "arguments": {
239
+ "app": "mail",
240
+ "taskId": "task_123",
241
+ "taskHandle": "opaque_handle"
242
+ }
239
243
  }
240
244
  }
241
245
  ```
@@ -248,9 +252,10 @@ If a bounded transient status read is unavailable, `ask_app_status` can return
248
252
  `status: "unknown"`, `statusRead: "unavailable"`, `retryable: true`, the
249
253
  original `app` and `taskId`, and `pollAfterMs` / `poll`. This means the status
250
254
  read is unavailable—not that the durable task failed. Retry `ask_app_status`
251
- with that exact `app` and `taskId`; never resubmit `ask_app` to recover, because
252
- submission can duplicate work. Permanent authentication, not-found, and
253
- protocol failures remain errors.
255
+ with the returned `poll.arguments` unchanged, including `taskHandle` when
256
+ present; legacy results without a handle must reuse the same `app` and `taskId`.
257
+ Never resubmit `ask_app` to recover, because submission can duplicate work.
258
+ Permanent authentication, not-found, and protocol failures remain errors.
254
259
 
255
260
  ## Authentication {#authentication}
256
261
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.125.0",
3
+ "version": "0.127.1",
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": {
@@ -21,6 +21,8 @@ export {
21
21
 
22
22
  // Client
23
23
  export { A2AClient, callAction, callAgent, signA2AToken } from "./client.js";
24
+ export { resolveA2ACallerAuth } from "./caller-auth.js";
25
+ export type { A2ACallerAuth } from "./caller-auth.js";
24
26
  export {
25
27
  AgentInvocationError,
26
28
  buildAgentInvocationPrompt,
@@ -141,6 +141,7 @@ import {
141
141
  getRun,
142
142
  abortRun,
143
143
  abortRunDurably,
144
+ abortTurnDurably,
144
145
  tryClaimRunSlot,
145
146
  isHostedRuntime,
146
147
  resolveRunSoftTimeoutMs,
@@ -7409,8 +7410,11 @@ export function createProductionAgentHandler(
7409
7410
  // sent from the model picker; `engine.name` is what resolveEngine picked.
7410
7411
  // Divergence between them is the usual cause of "status says builder but
7411
7412
  // no [builder-engine] log lines appear" confusion.
7413
+ // `requestModel` differing from `model` means normalizeModelForEngine
7414
+ // substituted the engine default — otherwise indistinguishable from the
7415
+ // client never asking for one.
7412
7416
  console.log(
7413
- `[agent-chat] resolved engine=${engine.name} model=${effectiveModel} requestEngine=${requestEngine ?? "(none)"} modelSource=${modelSelectionSource}`,
7417
+ `[agent-chat] resolved engine=${engine.name} model=${effectiveModel} requestModel=${requestModel ?? "(none)"} requestEngine=${requestEngine ?? "(none)"} modelSource=${modelSelectionSource} turnId=${requestTurnId ?? "(none)"}`,
7414
7418
  );
7415
7419
 
7416
7420
  if (
@@ -9020,5 +9024,6 @@ export {
9020
9024
  getRun,
9021
9025
  abortRun,
9022
9026
  abortRunDurably,
9027
+ abortTurnDurably,
9023
9028
  subscribeToRun,
9024
9029
  };
@@ -15,6 +15,8 @@ import {
15
15
  getRunEventsSince,
16
16
  getRunById,
17
17
  getRunByThread,
18
+ getRunTurnRef,
19
+ markTurnAborted,
18
20
  cleanupOldRuns,
19
21
  updateRunHeartbeat,
20
22
  bumpRunProgress,
@@ -2095,5 +2097,40 @@ export async function abortRunDurably(
2095
2097
  return abortedInMemory;
2096
2098
  }
2097
2099
 
2100
+ /**
2101
+ * Stop the whole turn `runId` belongs to, not just that run.
2102
+ *
2103
+ * A turn is executed as a chain of runs: every `loop_limit` / `auto_continue`
2104
+ * boundary and every background handoff starts a successor run with a NEW run
2105
+ * id under the same turn id. Aborting one run therefore only ends the current
2106
+ * chunk — the successor claims itself and the turn keeps going, which is what
2107
+ * users see as "Stop didn't stop it". The durable turn-abort marker is the only
2108
+ * thing the successor-claim path (`isTurnAborted`) consults.
2109
+ */
2110
+ export async function abortTurnDurably(
2111
+ runId: string,
2112
+ reason: string = "user",
2113
+ ): Promise<void> {
2114
+ const memRun = activeRuns.get(runId);
2115
+ // In-memory first: a foreground run's SQL insert is async, so the row may not
2116
+ // exist yet when Stop lands moments after send.
2117
+ const ref = memRun
2118
+ ? { threadId: memRun.threadId, turnId: memRun.turnId }
2119
+ : await getRunTurnRef(runId).catch(() => null);
2120
+ if (!ref) return;
2121
+ try {
2122
+ await markTurnAborted(ref.threadId, ref.turnId, reason);
2123
+ } catch (error) {
2124
+ // The current run is already stopped; a failed marker write must not turn
2125
+ // Stop into a 500. Successors will keep running — capture it so that is
2126
+ // visible rather than silent.
2127
+ captureError(error, {
2128
+ route: "/_agent-native/agent-chat/runs/:id/abort",
2129
+ tags: { source: "agent-run-manager", phase: "abort-turn" },
2130
+ extra: { runId, reason, ...ref },
2131
+ });
2132
+ }
2133
+ }
2134
+
2098
2135
  // Re-export so callers can avoid importing from run-store directly.
2099
2136
  export { tryClaimRunSlot } from "./run-store.js";
@@ -2053,6 +2053,24 @@ export async function markTurnAborted(
2053
2053
  );
2054
2054
  }
2055
2055
 
2056
+ /**
2057
+ * Turn identity for a run, or null when the row is unknown. `turn_id` is null
2058
+ * for rows written before the column existed; those runs are their own turn.
2059
+ */
2060
+ export async function getRunTurnRef(
2061
+ runId: string,
2062
+ ): Promise<{ threadId: string; turnId: string } | null> {
2063
+ await ensureRunTables();
2064
+ const { rows } = await getDbExec().execute({
2065
+ sql: `SELECT thread_id, turn_id FROM agent_runs WHERE id = ?`,
2066
+ args: [runId],
2067
+ });
2068
+ const row = rows[0] as { thread_id?: unknown; turn_id?: unknown } | undefined;
2069
+ const threadId = row?.thread_id ? String(row.thread_id) : "";
2070
+ if (!threadId) return null;
2071
+ return { threadId, turnId: row?.turn_id ? String(row.turn_id) : runId };
2072
+ }
2073
+
2056
2074
  export async function isTurnAborted(
2057
2075
  threadId: string,
2058
2076
  turnId: string,
@@ -88,6 +88,8 @@ interface PendingSend {
88
88
  interface PendingDelivery {
89
89
  threadId: string | null;
90
90
  send: PendingSend;
91
+ /** Applied to whichever thread this send lands on; a queued send outlives the handler that parsed it. */
92
+ modelOverride?: ModelSelection;
91
93
  }
92
94
 
93
95
  /** The single path that hands a queued send to a mounted chat ref. */
@@ -154,28 +156,28 @@ function resolveModelSelection(
154
156
  groups: EngineModelGroup[],
155
157
  ): ModelSelection | undefined {
156
158
  if (!selection?.model) return undefined;
157
- if (groups.length === 0) {
158
- return {
159
- model: selection.model,
160
- effort: resolveReasoningEffortSelection(
161
- selection.model,
162
- selection.effort,
163
- ),
164
- };
165
- }
166
- const preferredGroup = groups.find(
167
- (group) =>
168
- group.engine === selection.engine &&
169
- group.models.includes(selection.model),
170
- );
171
- const fallbackGroup = groups.find((group) =>
172
- group.models.includes(selection.model),
173
- );
174
- if (groups.length > 0 && !preferredGroup && !fallbackGroup) {
175
- return undefined;
176
- }
159
+ // Engine precedence turns on whether the catalog OFFERS the supplied engine,
160
+ // not on whether that engine advertises this model:
161
+ // offered honor it. A gateway's advertised list is its built-in
162
+ // catalog, not what the endpoint serves, and one model id
163
+ // sits under several groups (claude-sonnet-5 under both
164
+ // anthropic and builder) — so a model-only match would
165
+ // reroute the turn to a different provider and bill it there.
166
+ // not offered → heal to a group that serves the model. A selection left
167
+ // on `builder` after disconnecting it must still run on the
168
+ // user's own key; the same model through another configured
169
+ // provider is the point of bring-your-own-key.
170
+ // `builder` is in the catalog exactly when Builder is connected, so this is a
171
+ // real availability signal rather than a guess.
172
+ const suppliedEngine = selection.engine?.trim()
173
+ ? selection.engine
174
+ : undefined;
177
175
  const engine =
178
- preferredGroup?.engine ?? fallbackGroup?.engine ?? selection.engine;
176
+ (groups.some((group) => group.engine === suppliedEngine)
177
+ ? suppliedEngine
178
+ : undefined) ??
179
+ groups.find((group) => group.models.includes(selection.model))?.engine ??
180
+ suppliedEngine;
179
181
  if (!engine && groups.length > 0) return undefined;
180
182
 
181
183
  const effort = resolveReasoningEffortSelection(
@@ -1120,7 +1122,14 @@ export function MultiTabAssistantChat({
1120
1122
  .catch(() => null),
1121
1123
  ])
1122
1124
  .then(([enginesData, envKeys, builderStatus]) => {
1123
- if (!enginesData?.engines) return;
1125
+ if (!enginesData?.engines) {
1126
+ // Leaves `availableModels` empty for the session, so an override with
1127
+ // no engine of its own has nothing to resolve against.
1128
+ console.warn(
1129
+ "[agent-chat] no engine list; model overrides cannot be catalog-resolved",
1130
+ );
1131
+ return;
1132
+ }
1124
1133
  const configuredKeys = new Set(
1125
1134
  (envKeys as Array<{ key: string; configured: boolean }>)
1126
1135
  .filter((k) => k.configured)
@@ -1598,6 +1607,7 @@ export function MultiTabAssistantChat({
1598
1607
  context,
1599
1608
  openSidebar,
1600
1609
  model,
1610
+ engine,
1601
1611
  effort,
1602
1612
  newTab,
1603
1613
  background,
@@ -1638,32 +1648,44 @@ export function MultiTabAssistantChat({
1638
1648
  ...(submitMessageId ? { submitMessageId } : {}),
1639
1649
  };
1640
1650
 
1651
+ // Resolved once, up front, and carried with the send until a thread
1652
+ // exists to key it by. Applying it only when `model` matched
1653
+ // `availableModels` dropped every override that arrived before the
1654
+ // engines fetch resolved — and the cold-start replay below runs at mount,
1655
+ // when that list is always still empty.
1656
+ let modelOverride: ModelSelection | undefined;
1657
+ if (model) {
1658
+ const catalogEngine = availableModels.find((g) =>
1659
+ g.models.includes(model),
1660
+ )?.engine;
1661
+ const overrideEngine = engine ?? catalogEngine;
1662
+ if (!overrideEngine && availableModels.length > 0) {
1663
+ console.warn(
1664
+ `[agent-chat] model override "${model}" is in no engine group and carries no engine; the server will substitute its default`,
1665
+ );
1666
+ }
1667
+ modelOverride = {
1668
+ model,
1669
+ ...(overrideEngine ? { engine: overrideEngine } : {}),
1670
+ effort: resolveReasoningEffortSelection(
1671
+ model,
1672
+ isReasoningEffort(effort) ? effort : undefined,
1673
+ ),
1674
+ };
1675
+ }
1676
+
1641
1677
  const sendToTab = (threadId: string) => {
1642
1678
  if (isAgentChatSubmitCancelled(submitMessageId)) return;
1643
- // If a model override was specified, apply it only if we recognize it
1644
- if (model) {
1645
- const matchedGroup = availableModels.find((g) =>
1646
- g.models.includes(model),
1647
- );
1648
- if (matchedGroup) {
1649
- const selectedEffort = resolveReasoningEffortSelection(
1650
- model,
1651
- isReasoningEffort(effort) ? effort : undefined,
1652
- );
1653
- threadModelRef.current.set(threadId, {
1654
- model,
1655
- engine: matchedGroup.engine,
1656
- effort: selectedEffort,
1657
- });
1658
- bumpModelSelectionVersion();
1659
- }
1679
+ if (modelOverride) {
1680
+ threadModelRef.current.set(threadId, modelOverride);
1681
+ bumpModelSelectionVersion();
1660
1682
  }
1661
1683
 
1662
1684
  const ref = chatRefs.current.get(threadId);
1663
1685
  if (ref) {
1664
1686
  deliverPendingSend(ref, send);
1665
1687
  } else {
1666
- pendingDeliveries.current.push({ threadId, send });
1688
+ pendingDeliveries.current.push({ threadId, send, modelOverride });
1667
1689
  }
1668
1690
  };
1669
1691
 
@@ -1709,7 +1731,11 @@ export function MultiTabAssistantChat({
1709
1731
  } else {
1710
1732
  // Cold start: no thread yet. Queue for the first active thread (the
1711
1733
  // bootstrap effect creates it) rather than racing a second create.
1712
- pendingDeliveries.current.push({ threadId: null, send });
1734
+ pendingDeliveries.current.push({
1735
+ threadId: null,
1736
+ send,
1737
+ modelOverride,
1738
+ });
1713
1739
  }
1714
1740
  }
1715
1741
  };
@@ -1762,16 +1788,22 @@ export function MultiTabAssistantChat({
1762
1788
  if (isAgentChatSubmitCancelled(delivery.send.submitMessageId)) continue;
1763
1789
  const threadId = delivery.threadId ?? active ?? null;
1764
1790
  const ref = threadId ? chatRefs.current.get(threadId) : null;
1791
+ if (threadId && delivery.modelOverride) {
1792
+ threadModelRef.current.set(threadId, delivery.modelOverride);
1793
+ bumpModelSelectionVersion();
1794
+ }
1765
1795
  if (threadId && ref) {
1766
1796
  const { send } = delivery;
1767
1797
  setTimeout(() => deliverPendingSend(ref, send), 50);
1768
1798
  } else {
1769
1799
  // Not ready — keep it, pinning the resolved threadId once known.
1770
- remaining.push(threadId ? { threadId, send: delivery.send } : delivery);
1800
+ remaining.push(
1801
+ threadId ? { ...delivery, threadId, send: delivery.send } : delivery,
1802
+ );
1771
1803
  }
1772
1804
  }
1773
1805
  pendingDeliveries.current = remaining;
1774
- }, [openTabIds, activeThreadId]);
1806
+ }, [openTabIds, activeThreadId, bumpModelSelectionVersion]);
1775
1807
 
1776
1808
  // Listen for chatRunning completion events
1777
1809
  useEffect(() => {
@@ -858,6 +858,17 @@ function normalizeAgentChatRequestMode(
858
858
  return value === "act" || value === "plan" ? value : undefined;
859
859
  }
860
860
 
861
+ /**
862
+ * Composers submit `engine: ""` whenever the engines list failed to load. An
863
+ * empty string is not nullish, so it survives `??` yet reads as falsy — one
864
+ * value meaning both "specified" and "absent". Absent is decided here, once.
865
+ */
866
+ function nonEmptyString(value: unknown): string | undefined {
867
+ if (typeof value !== "string") return undefined;
868
+ const trimmed = value.trim();
869
+ return trimmed ? trimmed : undefined;
870
+ }
871
+
861
872
  /** A normalized `agentNative.submitChat` payload — decode via {@link parseSubmitChatMessage}. */
862
873
  export interface ParsedSubmitChat {
863
874
  /** Visible prompt text (non-empty). */
@@ -867,6 +878,12 @@ export interface ParsedSubmitChat {
867
878
  submit: boolean;
868
879
  openSidebar?: boolean;
869
880
  model?: string;
881
+ /**
882
+ * Engine paired with `model`. The receiver cannot re-derive it for an id the
883
+ * catalog omits, and a model sent without one is normalized to the resolved
884
+ * engine's default server-side.
885
+ */
886
+ engine?: string;
870
887
  /** Raw effort hint; the receiver validates it against the model. */
871
888
  effort?: unknown;
872
889
  newTab?: boolean;
@@ -907,7 +924,8 @@ export function parseSubmitChatMessage(
907
924
  submit: raw.submit !== false,
908
925
  openSidebar:
909
926
  typeof raw.openSidebar === "boolean" ? raw.openSidebar : undefined,
910
- model: typeof raw.model === "string" ? raw.model : undefined,
927
+ model: nonEmptyString(raw.model),
928
+ engine: nonEmptyString(raw.engine),
911
929
  effort: raw.effort,
912
930
  newTab: typeof raw.newTab === "boolean" ? raw.newTab : undefined,
913
931
  background:
@@ -41,6 +41,7 @@ import {
41
41
  import { ConnectBuilderCard } from "../ConnectBuilderCard.js";
42
42
  import { useT } from "../i18n.js";
43
43
  import { McpAppRenderer } from "../mcp-apps/McpAppRenderer.js";
44
+ import { findMcpIntegrationForToolName } from "../resources/mcp-integration-catalog.js";
44
45
  import type { AgentCallProgress, ContentPart } from "../sse-event-processor.js";
45
46
  import {
46
47
  BashCell,
@@ -279,7 +280,31 @@ type ToolIconComponent = React.ComponentType<{
279
280
  size?: number | string;
280
281
  }>;
281
282
 
283
+ const brandIcons = new Map<string, ToolIconComponent>();
284
+
285
+ function brandToolIcon(logoUrl: string, name: string): ToolIconComponent {
286
+ const cached = brandIcons.get(logoUrl);
287
+ if (cached) return cached;
288
+ const Icon: ToolIconComponent = ({ className, size }) => (
289
+ <img
290
+ src={logoUrl}
291
+ alt=""
292
+ aria-hidden
293
+ width={size}
294
+ height={size}
295
+ title={name}
296
+ className={cn("rounded-[3px] object-contain", className)}
297
+ />
298
+ );
299
+ brandIcons.set(logoUrl, Icon);
300
+ return Icon;
301
+ }
302
+
282
303
  function resolveToolIcon(toolName: string): ToolIconComponent {
304
+ const integration = findMcpIntegrationForToolName(toolName);
305
+ if (integration) {
306
+ return brandToolIcon(integration.logoUrl, integration.name);
307
+ }
283
308
  const name = toolName.toLowerCase();
284
309
  if (name.includes("slack")) return IconBrandSlack;
285
310
  if (
@@ -803,6 +803,29 @@ export function findMcpIntegrationForText(
803
803
  return null;
804
804
  }
805
805
 
806
+ /**
807
+ * Matches the server segment of an `mcp__<server>__<tool>` name, not prose, so
808
+ * ambiguous brand words ("box", "monday", "linear") are safe here in a way they
809
+ * are not in `findMcpIntegrationForText`.
810
+ */
811
+ export function findMcpIntegrationForToolName(
812
+ toolName: string,
813
+ integrations: readonly DefaultMcpIntegration[] = DEFAULT_MCP_INTEGRATIONS,
814
+ ): DefaultMcpIntegration | null {
815
+ const server = /^mcp__(.+?)__/.exec(toolName.toLowerCase())?.[1];
816
+ if (!server) return null;
817
+ return (
818
+ integrations.find((integration) =>
819
+ [
820
+ integration.id,
821
+ integration.provider,
822
+ ...(integration.brandAliases ?? []),
823
+ ...(integration.aliases ?? []),
824
+ ].some((alias) => textContainsTerm(server, alias)),
825
+ ) ?? null
826
+ );
827
+ }
828
+
806
829
  export function isMcpConnectionFailureText(text: string): boolean {
807
830
  return /\b(?:can(?:not|'t|’t)|could(?: not|n't|n’t)|unable|failed|don't have access|don’t have access|not connected|not able)\b[\s\S]{0,80}\b(?:read|access|open|see|fetch|connect)\b/i.test(
808
831
  text,