@agent-native/core 0.126.0 → 0.127.3
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 +83 -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/cli/template-sync.ts +96 -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/builder-design-systems.ts +1 -1
- 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/chat/_gitignore +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
- package/corpus/templates/design/actions/create-file.ts +15 -0
- package/corpus/templates/design/actions/generate-design.ts +40 -0
- package/corpus/templates/design/actions/present-design-variants.ts +16 -0
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
- package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
- 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/design/server/source-workspace.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +608 -18
- 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/deck/DeckCard.tsx +2 -6
- 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/cli/template-sync.d.ts.map +1 -1
- package/dist/cli/template-sync.js +88 -0
- package/dist/cli/template-sync.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 +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.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 +3 -3
- package/dist/progress/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/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- 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/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- 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/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +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/chat/_gitignore +2 -0
- 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/cli/template-sync.ts +96 -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/builder-design-systems.ts +1 -1
- package/src/server/core-routes-plugin.ts +67 -0
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/templates/chat/_gitignore +2 -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,88 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.127.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 750e90e: Add a `materialize --out <dir>` subcommand to `agent-native template` that writes the post-processed standalone template tree (e.g. `--template chat`, with the template's own identity, `_gitignore`→`.gitignore`, resolved deps, standalone `netlify.toml`) to a directory — no existing app required. This is the monorepo half of the vendor-branch starter mirror: the public monorepo materializes `templates/chat` and pushes it to the private starter's `template` branch, which the starter merges into `main` with git. Reuses the existing `materializeTemplate` engine.
|
|
8
|
+
|
|
9
|
+
## 0.127.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c6ca76a: Fix the Builder Design Systems API base URL fallback incorrectly including an `agent-native/` prefix. The real route is registered as `/design-systems/v1/...` with no `agent-native/` prefix, so requests using the fallback base URL (when `BUILDER_DESIGN_SYSTEMS_BASE_URL` is unset) were hitting the wrong path.
|
|
14
|
+
|
|
15
|
+
## 0.127.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- bbd202b: Fix the model picker offering a model the app cannot route, and the chat bridge dropping submitted model overrides.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- `[agent-chat] resolved …` now logs `requestModel` and `turnId`, making a server-side model substitution visible.
|
|
23
|
+
|
|
24
|
+
## 0.127.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- cbc6936: Make connecting one agent-native app to another a guided flow instead of three
|
|
29
|
+
blank text fields.
|
|
30
|
+
- New `GET /_agent-native/agents/probe` reads a peer's agent card and makes one
|
|
31
|
+
authenticated no-op call, reporting `reachable` and `authorized` as
|
|
32
|
+
independent fields. A peer that answers but rejects the caller's token is the
|
|
33
|
+
failure local dev hides — the receiver runs unauthenticated on localhost, so a
|
|
34
|
+
mismatched secret previously surfaced only after deploy.
|
|
35
|
+
- Settings → Manage agent → Connected Agents is URL-first: paste a peer URL,
|
|
36
|
+
press Check, and the name and description come from its card. Unreachable
|
|
37
|
+
never blocks the save. Rows carry a liveness dot from one batched probe.
|
|
38
|
+
- The section now shows shared-secret state and a Sync to apps action inline,
|
|
39
|
+
reusing the existing org hooks. A caller who cannot see the secret is told so
|
|
40
|
+
rather than being shown "not set".
|
|
41
|
+
- After an add, the UI states that registration is one-directional and deep
|
|
42
|
+
links to the peer's own settings with the values prefilled.
|
|
43
|
+
- The Connected Agents list collapses a remote agent that still has its
|
|
44
|
+
pre-migration `agents/*.json` row alongside the canonical
|
|
45
|
+
`remote-agents/*.json` one, instead of listing it twice with the same URL.
|
|
46
|
+
- `list-connected-agents` keys custom manifests by the normalized agent id, so
|
|
47
|
+
an agent registered as `images`/`asset` no longer appears once as a discovered
|
|
48
|
+
agent and again as a custom one.
|
|
49
|
+
- Export `resolveA2ACallerAuth` from `@agent-native/core/a2a` so app code can
|
|
50
|
+
authenticate outbound A2A calls without reimplementing org-secret lookup.
|
|
51
|
+
- The `a2a-protocol` skill documents the real setup path — A2A is auto-mounted,
|
|
52
|
+
peers are `remote-agents/*.json` resources, and auth is a JWT signed with
|
|
53
|
+
`A2A_SECRET` or the per-org secret — replacing the `mountA2A` + per-peer
|
|
54
|
+
`apiKeyEnv` flow the framework no longer wires up.
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- 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.
|
|
59
|
+
- cbc6936: Hide pending workspace apps by default and expose app ownership metadata from each card's overflow menu.
|
|
60
|
+
- cbc6936: Show the provider's brand logo on agent tool rows for catalog-backed MCP tools, instead of a generic code icon.
|
|
61
|
+
- cbc6936: Settings → Connections: the "New" key menu is now a searchable, scrolling combobox instead of a cropped dropdown, so every registered key is reachable.
|
|
62
|
+
- cbc6936: Send `X-Content-Type-Options: nosniff` on CDN-served static assets.
|
|
63
|
+
|
|
64
|
+
`/assets/**`, `/favicon.*`, `/manifest.json`, `/icon-*.svg`, and
|
|
65
|
+
`/library-presets/**` are served straight off the CDN, so the security-headers
|
|
66
|
+
h3 middleware never runs for them and they shipped without the `nosniff` header
|
|
67
|
+
that every function-served response already carries.
|
|
68
|
+
|
|
69
|
+
- cbc6936: Make the chat Stop button actually stop the turn.
|
|
70
|
+
|
|
71
|
+
A turn runs as a chain of runs: every `loop_limit` / `auto_continue` boundary
|
|
72
|
+
and every background handoff starts a successor under a new run id but the same
|
|
73
|
+
turn id. Stop only aborted the run id the client happened to hold, so the
|
|
74
|
+
successor claimed itself and the agent kept looping — the turn-abort marker that
|
|
75
|
+
`isTurnAborted` consults was only ever written on the pre-run path, which is
|
|
76
|
+
dead as soon as a run id exists.
|
|
77
|
+
|
|
78
|
+
`POST /runs/:id/abort` now escalates user-intent reasons (`user`, `abort`,
|
|
79
|
+
`user_stuck_cancel`, `user_stuck_retry`) to a turn-wide abort. Watchdog reasons
|
|
80
|
+
(`no_progress`, `auto_stuck_retry`) keep single-run semantics so they cannot
|
|
81
|
+
kill a server-side continuation chain that is still making progress.
|
|
82
|
+
|
|
83
|
+
- Updated dependencies [cbc6936]
|
|
84
|
+
- @agent-native/toolkit@0.10.9
|
|
85
|
+
|
|
3
86
|
## 0.126.0
|
|
4
87
|
|
|
5
88
|
### 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.3",
|
|
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,
|
|
@@ -476,6 +476,18 @@ export async function runTemplate(
|
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
const rest = args.slice(1);
|
|
479
|
+
|
|
480
|
+
// `materialize` produces a fresh post-processed template tree at --out; it has
|
|
481
|
+
// no existing app to resolve, so it runs before target resolution.
|
|
482
|
+
if (command === "materialize") {
|
|
483
|
+
try {
|
|
484
|
+
return await materializeCommand(rest, io);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
479
491
|
const appArg = positionalArgs(rest)[0];
|
|
480
492
|
const flags = {
|
|
481
493
|
to: flagValue(rest, "--to"),
|
|
@@ -540,6 +552,87 @@ async function runForTarget(
|
|
|
540
552
|
}
|
|
541
553
|
}
|
|
542
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Write the post-processed template tree to a directory — the pristine,
|
|
557
|
+
* template-derived output `create` would produce (no install/git/skills).
|
|
558
|
+
* `--to` defaults to the local template (walked up from the package); pass a ref
|
|
559
|
+
* to fetch from GitHub instead.
|
|
560
|
+
*/
|
|
561
|
+
async function materializeCommand(
|
|
562
|
+
rest: string[],
|
|
563
|
+
io: TemplateIO,
|
|
564
|
+
): Promise<number> {
|
|
565
|
+
const outDir = flagValue(rest, "--out");
|
|
566
|
+
const template = flagValue(rest, "--template");
|
|
567
|
+
const name = flagValue(rest, "--name");
|
|
568
|
+
const ref = flagValue(rest, "--to") ?? null;
|
|
569
|
+
if (!outDir) {
|
|
570
|
+
io.err("template materialize requires --out <dir>.");
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
if (!template) {
|
|
574
|
+
io.err("template materialize requires --template <name>.");
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
// Stage into a unique sibling dir, then swap it in crash-safely: move the old
|
|
578
|
+
// tree aside, move the staged tree in, and restore the old one if that fails.
|
|
579
|
+
// Directory replacement isn't atomic on POSIX (you can't rename onto a non-empty
|
|
580
|
+
// dir), so the old tree is only ever moved — never deleted before the new one is
|
|
581
|
+
// in place — and a crash between the two renames leaves it recoverable under
|
|
582
|
+
// `backup`. Unique same-filesystem siblings keep each rename atomic and
|
|
583
|
+
// collision-free.
|
|
584
|
+
const parent = path.dirname(path.resolve(outDir));
|
|
585
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
586
|
+
const tmp = fs.mkdtempSync(path.join(parent, ".template-materialize-"));
|
|
587
|
+
// `backup` is a unique dir too, so a concurrent process can't occupy the path;
|
|
588
|
+
// renaming a directory onto an existing *empty* dir is a valid replace on POSIX,
|
|
589
|
+
// so the `hadOld` rename below still lands cleanly on it.
|
|
590
|
+
const backup = fs.mkdtempSync(
|
|
591
|
+
path.join(parent, ".template-materialize-backup-"),
|
|
592
|
+
);
|
|
593
|
+
let backupHoldsOriginal = false;
|
|
594
|
+
try {
|
|
595
|
+
const result = await materializeTemplate({
|
|
596
|
+
appName: name ?? template,
|
|
597
|
+
template,
|
|
598
|
+
ref,
|
|
599
|
+
shape: "standalone",
|
|
600
|
+
destDir: tmp,
|
|
601
|
+
});
|
|
602
|
+
const hadOld = fs.existsSync(outDir);
|
|
603
|
+
if (hadOld) {
|
|
604
|
+
fs.renameSync(outDir, backup);
|
|
605
|
+
backupHoldsOriginal = true;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
fs.renameSync(tmp, outDir);
|
|
609
|
+
backupHoldsOriginal = false;
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (hadOld) {
|
|
612
|
+
fs.renameSync(backup, outDir);
|
|
613
|
+
backupHoldsOriginal = false;
|
|
614
|
+
}
|
|
615
|
+
throw err;
|
|
616
|
+
}
|
|
617
|
+
io.out(
|
|
618
|
+
`Materialized "${template}" (${result.source}@${result.ref}) to ${outDir}.`,
|
|
619
|
+
);
|
|
620
|
+
return 0;
|
|
621
|
+
} finally {
|
|
622
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
623
|
+
// If restoring from `backup` above itself threw, `backupHoldsOriginal` is still
|
|
624
|
+
// true and `backup` is the only remaining copy of the pre-existing output —
|
|
625
|
+
// leave it on disk instead of deleting the last copy of the user's data.
|
|
626
|
+
if (!backupHoldsOriginal) {
|
|
627
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
628
|
+
} else {
|
|
629
|
+
io.err(
|
|
630
|
+
`Failed to restore ${outDir}; the previous contents were preserved at ${backup}.`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
543
636
|
function templateName(target: AppTarget, flags: TemplateFlags): string {
|
|
544
637
|
const name = flags.template ?? target.provenance.template;
|
|
545
638
|
if (!name) {
|
|
@@ -995,6 +1088,9 @@ function templateUsage(): string {
|
|
|
995
1088
|
" baseline [app] Record a baseline for an app scaffolded before",
|
|
996
1089
|
" provenance existed [--ref <ref>] [--template <name>]",
|
|
997
1090
|
" accept [app] Advance the baseline after resolving conflicts",
|
|
1091
|
+
" materialize --template <name> --out <dir>",
|
|
1092
|
+
" Write the post-processed template tree to a dir",
|
|
1093
|
+
" (no app needed) [--name <appName>] [--to <ref>]",
|
|
998
1094
|
"",
|
|
999
1095
|
"With no [app], operates on the current app, or on every app in a workspace",
|
|
1000
1096
|
"when run from the workspace root. --to defaults to the ref matching the",
|
|
@@ -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:
|