@agent-native/core 0.126.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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +71 -0
- package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
- package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/index.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +6 -1
- package/corpus/core/src/agent/run-manager.ts +37 -0
- package/corpus/core/src/agent/run-store.ts +18 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
- package/corpus/core/src/client/agent-chat.ts +19 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
- package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
- package/corpus/core/src/client/use-chat-models.ts +54 -37
- package/corpus/core/src/localization/default-messages.ts +11 -0
- package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
- package/corpus/core/src/mcp/builtin-tools.ts +146 -34
- package/corpus/core/src/server/agent-capabilities.ts +8 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
- package/corpus/core/src/server/agent-discovery.ts +1 -1
- package/corpus/core/src/server/agent-peer-probe.ts +133 -0
- package/corpus/core/src/server/core-routes-plugin.ts +67 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
- package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
- package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
- package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +11 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +34 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +8 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +16 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +60 -31
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +16 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/AgentsSection.d.ts.map +1 -1
- package/dist/client/settings/AgentsSection.js +250 -30
- package/dist/client/settings/AgentsSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +16 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +46 -27
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +9 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +9 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/ask-app-task-handle.d.ts +25 -0
- package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
- package/dist/mcp/ask-app-task-handle.js +136 -0
- package/dist/mcp/ask-app-task-handle.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +3 -3
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -38
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/agent-capabilities.d.ts +7 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +1 -0
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +7 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +16 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts +1 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +1 -1
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/agent-peer-probe.d.ts +37 -0
- package/dist/server/agent-peer-probe.d.ts.map +1 -0
- package/dist/server/agent-peer-probe.js +90 -0
- package/dist/server/agent-peer-probe.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +53 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -0
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +4 -0
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/docs/content/external-agents-catalog.mdx +14 -11
- package/docs/content/mcp-protocol.mdx +9 -4
- package/package.json +2 -2
- package/src/a2a/index.ts +2 -0
- package/src/agent/production-agent.ts +6 -1
- package/src/agent/run-manager.ts +37 -0
- package/src/agent/run-store.ts +18 -0
- package/src/client/MultiTabAssistantChat.tsx +75 -43
- package/src/client/agent-chat.ts +19 -1
- package/src/client/chat/tool-call-display.tsx +25 -0
- package/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/src/client/settings/AgentsSection.tsx +565 -71
- package/src/client/settings/SecretsSection.tsx +71 -36
- package/src/client/use-chat-models.ts +54 -37
- package/src/localization/default-messages.ts +11 -0
- package/src/mcp/ask-app-task-handle.ts +183 -0
- package/src/mcp/builtin-tools.ts +146 -34
- package/src/server/agent-capabilities.ts +8 -1
- package/src/server/agent-chat-plugin.ts +17 -0
- package/src/server/agent-discovery.ts +1 -1
- package/src/server/agent-peer-probe.ts +133 -0
- package/src/server/core-routes-plugin.ts +67 -0
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
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
|
+
|
|
3
74
|
## 0.126.0
|
|
4
75
|
|
|
5
76
|
### 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 `
|
|
248
|
-
the inline wait expires. Poll with `ask_app_status`
|
|
249
|
-
|
|
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
|
|
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": {
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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({
|
|
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(
|
|
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:
|
|
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,
|