synabun 2.1.0 → 2.2.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/CHANGELOG.md +139 -0
- package/README.md +1 -1
- package/docs/whatsapp.md +11 -11
- package/lib/client-config-repair.js +13 -9
- package/lib/external-tools.js +6 -5
- package/lib/update-snapshot.js +77 -3
- package/lib/virustotal.mjs +12 -1
- package/mcp-server/dist/index.js +1 -1
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +2 -1
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +2 -1
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.js +1 -1
- package/mcp-server/dist/services/tool-catalog.js.map +1 -1
- package/mcp-server/dist/services/tool-list.d.ts +1 -1
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.js +1 -1
- package/mcp-server/dist/services/tool-runtime.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +8 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +10 -9
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/skill-tools.js +11 -8
- package/mcp-server/dist/tools/skill-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/index.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +2 -1
- package/mcp-server/src/services/server-instructions.ts +2 -1
- package/mcp-server/src/services/tool-catalog.ts +1 -1
- package/mcp-server/src/services/tool-list.ts +1 -1
- package/mcp-server/src/services/tool-runtime.ts +2 -2
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/tools/agents.ts +10 -9
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +11 -8
- package/neural-interface/i18n/en.json +504 -39
- package/neural-interface/i18n/pt-BR.json +504 -39
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +1 -0
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-brains/codex.js +6 -1
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +76 -9
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +211 -50
- package/neural-interface/lib/assistant-envelope.js +153 -1
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +16 -11
- package/neural-interface/lib/assistant-plan-permissions.js +15 -0
- package/neural-interface/lib/assistant-router.js +237 -35
- package/neural-interface/lib/assistant-runtime.js +158 -12
- package/neural-interface/lib/assistant-task-prompt.js +10 -8
- package/neural-interface/lib/assistant-tools.js +2 -2
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +6 -1
- package/neural-interface/lib/browser-guard-cdp.js +5 -4
- package/neural-interface/lib/browser-network-guard.js +274 -82
- package/neural-interface/lib/browser-tool-policy.js +12 -7
- package/neural-interface/lib/claude-agent-bridge.js +7 -0
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +160 -31
- package/neural-interface/lib/cli-resolver.js +9 -2
- package/neural-interface/lib/cli-update-planner.js +25 -15
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +25 -6
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +9 -2
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/mcp-client-config.js +37 -6
- package/neural-interface/lib/mcp-external-servers.js +40 -7
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +147 -25
- package/neural-interface/lib/native-loop-providers.js +423 -23
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +165 -22
- package/neural-interface/lib/owner-authority.js +1 -1
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +80 -6
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +3 -0
- package/neural-interface/lib/rulesets/api.js +6 -5
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +89 -10
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +64 -1
- package/neural-interface/lib/skills/catalog.js +5 -1
- package/neural-interface/lib/skills/creation-brief.js +1 -1
- package/neural-interface/lib/skills/creation-sources.js +19 -1
- package/neural-interface/lib/skills/deploy.js +9 -4
- package/neural-interface/lib/skills/draft-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-trace.js +16 -0
- package/neural-interface/lib/skills/export.js +5 -2
- package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
- package/neural-interface/lib/skills/ownership.js +1 -1
- package/neural-interface/lib/skills/roots.js +10 -3
- package/neural-interface/lib/skills/runtime-checks.js +6 -3
- package/neural-interface/lib/skills/service.js +15 -7
- package/neural-interface/lib/skills/telemetry.js +6 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +77 -1
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-resume.js +13 -4
- package/neural-interface/lib/worker-sandbox.js +46 -16
- package/neural-interface/package-lock.json +18 -2
- package/neural-interface/package.json +2 -1
- package/neural-interface/public/onboarding.html +1 -1
- package/neural-interface/public/shared/agent-runtime-options.js +89 -11
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +4 -4
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
- package/neural-interface/public/shared/assistant/asst-api.js +15 -3
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
- package/neural-interface/public/shared/assistant/asst-control.js +48 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
- package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
- package/neural-interface/public/shared/assistant/asst-render.js +112 -18
- package/neural-interface/public/shared/assistant/asst-route.js +237 -57
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
- package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
- package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
- package/neural-interface/public/shared/assistant/asst-state.js +76 -9
- package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
- package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +7 -4
- package/neural-interface/public/shared/cli-status.js +9 -8
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/context-menu-model.js +1 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +1 -0
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +21 -4
- package/neural-interface/public/shared/loading-teardown.js +34 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
- package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-creation.js +6 -3
- package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
- package/neural-interface/public/shared/skills/ss-lint.js +9 -1
- package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
- package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
- package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
- package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
- package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
- package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/storage-retry.js +47 -0
- package/neural-interface/public/shared/storage.js +25 -4
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +362 -35
- package/neural-interface/public/shared/synabun-mascot.js +27 -8
- package/neural-interface/public/shared/synabun-ticker.js +147 -12
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +2 -2
- package/neural-interface/public/shared/tool-list-view.js +2 -2
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +20 -12
- package/neural-interface/public/shared/ui-claude-panel.js +374 -134
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +8 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-loading.js +5 -4
- package/neural-interface/public/shared/ui-menubar.js +2 -2
- package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
- package/neural-interface/public/shared/ui-navbar.js +45 -0
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +22 -12
- package/neural-interface/public/shared/ui-settings.js +90 -65
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
- package/neural-interface/public/shared/ui-terminal.js +146 -10
- package/neural-interface/public/shared/ui-tool-updates.js +3 -3
- package/neural-interface/public/shared/ui-tooltip.js +71 -46
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1 -1
- package/neural-interface/public/shared/ui-worker-end.js +1 -1
- package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/server.js +731 -466
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
- package/neural-interface/templates/rulesets/manifest.json +1 -1
- package/npm-shrinkwrap.json +18 -2
- package/package.json +2 -1
- package/skills/skill-forge/SKILL.md +4 -4
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/schedule.md +3 -3
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -37,7 +37,7 @@ import { effectiveRouting, normalizeRouteMode } from './assistant-config.js';
|
|
|
37
37
|
import { normalizeEffort } from './effort-levels.js';
|
|
38
38
|
import { hasRouteAccounts, routeAccountText } from './assistant-route-accounts.js';
|
|
39
39
|
import { chooseUsageAccount } from './account-usage.js';
|
|
40
|
-
import { PROVIDER_LABELS, coversWork, dryProviders, isFreePrice, listPriceOf, rowOf } from './assistant-budget-split.js';
|
|
40
|
+
import { PROVIDER_LABELS, coversWork, dryProviders, isFreePrice, isUnpricedProvider, listPriceOf, rowOf } from './assistant-budget-split.js';
|
|
41
41
|
|
|
42
42
|
export const TASK_CLASS_META = Object.freeze({
|
|
43
43
|
chat: { label: 'Chat', description: 'Conversation and questions answered from knowledge or memory', defaultKind: 'direct' },
|
|
@@ -75,6 +75,8 @@ export function requiresVision(taskClass) { return TASK_CLASS_META[taskClass]?.v
|
|
|
75
75
|
export const CANNOT_SEE_TEXT = "This model can't see images.";
|
|
76
76
|
export const UNKNOWN_SIGHT_TEXT = "This model isn't known to see images.";
|
|
77
77
|
export const UNAVAILABLE_TEXT = "This model isn't available right now.";
|
|
78
|
+
/** A "here" on an account other than this conversation's: provider histories belong to their account. */
|
|
79
|
+
export const WORKER_FOR_ACCOUNT_TEXT = 'Runs on a worker because this conversation belongs to another account.';
|
|
78
80
|
/** The card / answer text for a model design cannot use (a designFit() verdict). */
|
|
79
81
|
function fitText(fit) { return fit === 'blind' ? CANNOT_SEE_TEXT : fit === 'unavailable' ? UNAVAILABLE_TEXT : UNKNOWN_SIGHT_TEXT; }
|
|
80
82
|
/** What a run of the class collects into its media: the medium it makes, else what it gathers (Design's screenshots). */
|
|
@@ -185,7 +187,8 @@ export function normalizeConfidence(value) {
|
|
|
185
187
|
|
|
186
188
|
/**
|
|
187
189
|
* Normalize the brain's proposal (agent_route body, or a dispatch spec reshaped
|
|
188
|
-
* into one). A "direct" proposal on another provider is really a dispatch
|
|
190
|
+
* into one). A "direct" proposal on another provider is really a dispatch
|
|
191
|
+
* (`coercedProvider` records the first, so the result can say so); for a
|
|
189
192
|
* dispatch-only class (image / video creation, design) every proposal is a dispatch,
|
|
190
193
|
* a "here" one on the brain's own model (`coercedDirect` records the first).
|
|
191
194
|
* Design (`visionOnly`) also always needs a model that can see.
|
|
@@ -201,14 +204,17 @@ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
|
|
|
201
204
|
const dispatchOnly = meta.dispatchOnly === true;
|
|
202
205
|
const proposals = [];
|
|
203
206
|
let coercedDirect = false;
|
|
207
|
+
let coercedProvider = null;
|
|
204
208
|
for (const raw of Array.isArray(body.proposals) ? body.proposals.slice(0, 3) : []) {
|
|
205
209
|
if (!raw || typeof raw !== 'object') continue;
|
|
206
210
|
let kind = raw.kind === 'direct' ? 'direct' : 'dispatch';
|
|
207
211
|
let provider = CATALOG_PROVIDERS.includes(raw.provider) ? raw.provider : null;
|
|
208
212
|
let model = raw.model ? String(raw.model).trim().slice(0, 200) : null;
|
|
209
213
|
if (kind === 'direct') {
|
|
210
|
-
if (provider && provider !== brain.provider)
|
|
211
|
-
|
|
214
|
+
if (provider && provider !== brain.provider) {
|
|
215
|
+
kind = 'dispatch';
|
|
216
|
+
if (!proposals.length && !dispatchOnly) coercedProvider = provider;
|
|
217
|
+
} else provider = brain.provider || provider;
|
|
212
218
|
}
|
|
213
219
|
if (!provider) continue;
|
|
214
220
|
if (dispatchOnly && kind === 'direct') {
|
|
@@ -230,9 +236,18 @@ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
|
|
|
230
236
|
const request = { taskClass, taskClassLabel: meta.label, summary: clip(body.summary || '', 400), confidence, needsVision, proposals };
|
|
231
237
|
if (needsOutput || dispatchOnly) Object.assign(request, { needsOutput, dispatchOnly, coercedDirect });
|
|
232
238
|
if (visionOnly) request.visionOnly = true;
|
|
239
|
+
if (coercedProvider) request.coercedProvider = coercedProvider;
|
|
233
240
|
return request;
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
/** Why a "here" on `provider` runs on a worker: only the brain's own provider runs in this conversation. */
|
|
244
|
+
function otherProviderCorrection(provider, brain = {}) {
|
|
245
|
+
return {
|
|
246
|
+
field: 'kind', from: 'direct', to: 'dispatch', reason: 'other_provider', provider,
|
|
247
|
+
text: `${providerLabel(provider)} is not this conversation's provider${brain.provider ? ` (${providerLabel(brain.provider)})` : ''}, so it runs on a worker`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
236
251
|
/** Catalog-backed description of a target: label, tier, vision, outputs, price, context. */
|
|
237
252
|
export function describeTarget(target, { catalog = null, brain = {}, brainInfo = null } = {}) {
|
|
238
253
|
if (!target) return null;
|
|
@@ -499,6 +514,8 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
|
|
|
499
514
|
if (request.coercedDirect) {
|
|
500
515
|
corrections.push({ field: 'kind', from: 'direct', to: 'dispatch', reason: 'dispatch_only', text: `${label} always runs on a worker, not here` });
|
|
501
516
|
}
|
|
517
|
+
// "Here" on another provider was always a worker: the result now says so instead of changing the kind silently.
|
|
518
|
+
if (request.coercedProvider) corrections.push(otherProviderCorrection(request.coercedProvider, brain));
|
|
502
519
|
const sees = (target) => describeTarget(target, { catalog, brain, brainInfo })?.vision;
|
|
503
520
|
const fit = (target) => designFit(catalog, target, { brain, brainInfo });
|
|
504
521
|
// Too blind for this task: design needs a confirmed true (null never counts) on an available row; other vision work refuses a confirmed false.
|
|
@@ -643,7 +660,8 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
|
|
|
643
660
|
if (dispatchOnly && auto.kind !== 'dispatch') auto = { ...auto, kind: 'dispatch' };
|
|
644
661
|
// A corrected model or provider re-checks the effort.
|
|
645
662
|
auto = effortChecked(auto, catalog, corrections);
|
|
646
|
-
|
|
663
|
+
// The provider rule only names what always happened (a "here" elsewhere is a worker): it corrects nothing.
|
|
664
|
+
if (!bound && corrections.some((c) => c.reason !== 'remembered_disabled' && c.reason !== 'remembered_incapable' && c.reason !== 'other_provider')) source = 'corrected';
|
|
647
665
|
// With a budget the saved route yields: a provider with no share left moves the task on.
|
|
648
666
|
const budgeted = applyBudget(auto);
|
|
649
667
|
if (budgeted.declined) return budgetDeclined(budgeted);
|
|
@@ -671,11 +689,13 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
|
|
|
671
689
|
else if (fit !== 'ok') { option.disabled = true; option.disabledReason = fitText(fit); }
|
|
672
690
|
else if (needsVision && d.vision === false) { option.disabled = true; option.disabledReason = 'This model cannot see images, and this task needs to.'; }
|
|
673
691
|
else if (needsVision && d.vision === null) option.warning = 'Image support unknown for this model.';
|
|
674
|
-
// The session budget: a provider with no share left cannot take the task (a free model still can)
|
|
692
|
+
// The session budget: a provider with no share left cannot take the task (a free model still can), and one
|
|
693
|
+
// that reports no prices cannot run under a budget at all.
|
|
675
694
|
if (!option.disabled && coverage && !budgetCovers(coverage, catalog, target)) {
|
|
695
|
+
const unpriced = coverage.leftUsd < 0.01 ? null : unpricedText(target.provider);
|
|
676
696
|
option.disabled = true;
|
|
677
|
-
option.budget = 'share_used_up';
|
|
678
|
-
option.disabledReason = coverage.leftUsd < 0.01 ? "Budget: this session's budget is spent." : `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up
|
|
697
|
+
option.budget = unpriced ? 'unpriced' : 'share_used_up';
|
|
698
|
+
option.disabledReason = unpriced || (coverage.leftUsd < 0.01 ? "Budget: this session's budget is spent." : `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up.`);
|
|
679
699
|
}
|
|
680
700
|
if (here && brainInfo?.contextWindow && d.contextWindow && d.contextWindow < brainInfo.contextWindow) {
|
|
681
701
|
option.warning = `Smaller context (${Math.round(d.contextWindow / 1000)}k) than the current model; a long conversation may not fit.`;
|
|
@@ -684,7 +704,12 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
|
|
|
684
704
|
}
|
|
685
705
|
|
|
686
706
|
export function providerLabel(provider) {
|
|
687
|
-
return provider === 'claude-code' ? 'Claude Code' : provider === 'codex' ? 'Codex' : provider === 'opencode' ? 'OpenCode' : String(provider || 'Assistant');
|
|
707
|
+
return provider === 'claude-code' ? 'Claude Code' : provider === 'codex' ? 'Codex' : provider === 'opencode' ? 'OpenCode' : provider === 'antigravity' ? 'Antigravity' : String(provider || 'Assistant');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Why a session with a budget cannot run `provider` at all (it reports tokens, never prices), else null. */
|
|
711
|
+
export function unpricedText(provider) {
|
|
712
|
+
return isUnpricedProvider(provider) ? `Budget: ${providerLabel(provider)} reports tokens, not prices, so a session with a budget cannot run it. Pick another option, or switch the budget to No limit.` : null;
|
|
688
713
|
}
|
|
689
714
|
|
|
690
715
|
/** `seeingOnly` (design): only rows with a confirmed vision: true, where `needsVision` only drops a confirmed false. */
|
|
@@ -725,9 +750,17 @@ function strongestClaude(catalog) {
|
|
|
725
750
|
* Image / video creation: no "here" (always a worker), models that cannot make
|
|
726
751
|
* the medium disabled, and — when the suggestion cannot — the model that would
|
|
727
752
|
* run it instead (badge "capable"). Design: the same with "can see".
|
|
753
|
+
*
|
|
754
|
+
* Both sides, always: the card switches between Here (kind "direct": this
|
|
755
|
+
* conversation, on its model or another of its provider) and Dispatch (a worker).
|
|
756
|
+
* A side the options leave without one that can run gets one: a worker on the
|
|
757
|
+
* brain's own model ("w1", badge "current"), else a capable model of its provider,
|
|
758
|
+
* else elsewhere; a "here" on a model of the brain's provider that can see when the
|
|
759
|
+
* brain cannot ("h1", badge "capable"). Worker-only classes get no "here".
|
|
760
|
+
* → { options, defaultOptionId, sides, defaults } (routeSides).
|
|
728
761
|
*/
|
|
729
762
|
export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo = null, catalog = null, routing = {}, coverage = null }) {
|
|
730
|
-
|
|
763
|
+
const alwaysAsk = evaluation.reasons?.includes('always-ask') === true;
|
|
731
764
|
const medium = request.needsOutput || null;
|
|
732
765
|
const dispatchOnly = request.dispatchOnly === true;
|
|
733
766
|
const visionOnly = request.visionOnly === true;
|
|
@@ -735,16 +768,26 @@ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo =
|
|
|
735
768
|
const options = [];
|
|
736
769
|
const push = (option) => { if (!options.some((o) => sameTarget(o, option))) options.push(option); };
|
|
737
770
|
const hereTarget = { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null };
|
|
771
|
+
// A direct pick on the brain's own model with no effort is "here" as the brain runs now: one row, not two.
|
|
772
|
+
const onBrainModel = (t) => t.kind === 'direct' && t.provider === brain.provider && String(t.model || '') === String(brain.model || '');
|
|
738
773
|
request.proposals.forEach((p, index) => {
|
|
739
774
|
const target = { kind: p.kind, provider: p.provider, model: p.model || (index === 0 ? evaluation.proposal?.model || null : null), effort: p.effort };
|
|
740
775
|
if (index === 0 && evaluation.proposal) Object.assign(target, evaluation.proposal);
|
|
776
|
+
if (onBrainModel(target) && !target.effort && brain.effort) target.effort = brain.effort;
|
|
741
777
|
push(optionFor(`s${index + 1}`, target, 'suggested', { ...ctx, reason: p.reason }));
|
|
742
778
|
});
|
|
743
779
|
// The session budget moved the suggestion: the brain's own pick stays on the card, disabled with why.
|
|
744
780
|
if (evaluation.budgetFrom && coverage) push(optionFor('b0', evaluation.budgetFrom, 'suggested', { ...ctx, reason: request.proposals[0]?.reason || null }));
|
|
745
781
|
if (!dispatchOnly) push(optionFor('here', hereTarget, 'current', ctx));
|
|
746
|
-
const pref =
|
|
747
|
-
|
|
782
|
+
const pref = preferenceTarget(routing.preferences?.[request.taskClass], brain, catalog, { dispatchOnly });
|
|
783
|
+
// Always ask: the saved route is offered and its side starts on it (never binding: the card still asks).
|
|
784
|
+
if (pref) {
|
|
785
|
+
const saved = optionFor('r1', pref, 'remembered', ctx);
|
|
786
|
+
// A proposal of the same target keeps its row and carries the mark.
|
|
787
|
+
const row = options.find((o) => sameTarget(o, saved)) || saved;
|
|
788
|
+
if (alwaysAsk && !saved.disabled) Object.assign(row, { preferred: true, saved: true });
|
|
789
|
+
push(saved);
|
|
790
|
+
}
|
|
748
791
|
const anchor = evaluation.proposal?.kind === 'dispatch' || dispatchOnly ? evaluation.proposal || request.proposals[0] : hereTarget;
|
|
749
792
|
const cheaper = neighbour(catalog, anchor, 'cheaper', { needsVision: request.needsVision, seeingOnly: visionOnly, needsOutput: medium });
|
|
750
793
|
if (cheaper) push(optionFor('c1', { kind: anchor.provider === brain.provider && anchor.kind === 'direct' ? 'direct' : 'dispatch', provider: anchor.provider, model: cheaper.id, effort: null }, 'cheaper', ctx));
|
|
@@ -766,11 +809,103 @@ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo =
|
|
|
766
809
|
const option = seer ? optionFor('g1', seer, 'capable', ctx) : null;
|
|
767
810
|
if (option && !option.disabled) push(option);
|
|
768
811
|
}
|
|
812
|
+
// Both sides, always: a side with nothing that can run it gets the first candidate that can
|
|
813
|
+
// (or, with none, the first candidate disabled with why, so the card can say it).
|
|
814
|
+
// A saved route on an account is checked later (it may be signed out): it does not count, so its side keeps a fallback.
|
|
815
|
+
const runnable = (kind) => options.some((o) => o.kind === kind && !o.disabled && !(o.saved && o.accountId));
|
|
816
|
+
const offer = (kind, id, candidates) => {
|
|
817
|
+
let first = null;
|
|
818
|
+
for (const [target, badge] of candidates) {
|
|
819
|
+
if (!target?.provider) continue;
|
|
820
|
+
const option = optionFor(id, target, badge, ctx);
|
|
821
|
+
if (options.some((o) => sameTarget(o, option))) continue;
|
|
822
|
+
if (!option.disabled) { push(option); return; }
|
|
823
|
+
first = first || option;
|
|
824
|
+
}
|
|
825
|
+
if (first && !options.some((o) => o.kind === kind)) push(first);
|
|
826
|
+
};
|
|
827
|
+
if (request.taskClass !== 'chat' && !dispatchOnly && !runnable('direct')) offer('direct', 'h1', hereCandidates({ request, catalog, brain }));
|
|
828
|
+
if (request.taskClass !== 'chat' && !runnable('dispatch')) offer('dispatch', 'w1', workerCandidates({ request, catalog, brain, brainInfo }));
|
|
769
829
|
// Order: suggested first, then here, remembered, a capable stand-in, cheaper, stronger.
|
|
770
830
|
const order = { suggested: 0, current: 1, remembered: 2, capable: 3, cheaper: 4, stronger: 5 };
|
|
771
831
|
options.sort((a, b) => (order[a.badge] ?? 9) - (order[b.badge] ?? 9) || String(a.id).localeCompare(String(b.id)));
|
|
772
|
-
|
|
773
|
-
|
|
832
|
+
return { options, ...routeSides({ options, request }) };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* "Here" on another model of the brain's provider when "here" itself cannot run the task
|
|
837
|
+
* (the brain cannot see a computer task's screen, or the user hid its model): the
|
|
838
|
+
* cheapest one of that provider that can (one that can see first for vision work).
|
|
839
|
+
*/
|
|
840
|
+
function hereCandidates({ request, catalog, brain }) {
|
|
841
|
+
if (!brain.provider) return [];
|
|
842
|
+
const rows = visibleRows(catalog, brain.provider)
|
|
843
|
+
.filter((row) => row.status !== 'unavailable' && row.toolcall !== false && row.id !== brain.model && (!request.needsVision || row.vision === true))
|
|
844
|
+
.sort((a, b) => outPrice(a) - outPrice(b) || tierRank(a.tier) - tierRank(b.tier));
|
|
845
|
+
return rows.map((row) => [{ kind: 'direct', provider: brain.provider, model: row.id, effort: null }, request.needsVision ? 'capable' : null]);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* A worker for a card whose options have none that can run: the brain's own model on a
|
|
850
|
+
* worker (the same model, off this conversation), else a model of its provider that can
|
|
851
|
+
* (the brain's tier first, one that can see for vision work), else elsewhere (one that
|
|
852
|
+
* can see, or the strongest Claude model).
|
|
853
|
+
*/
|
|
854
|
+
function workerCandidates({ request, catalog, brain, brainInfo }) {
|
|
855
|
+
const candidates = [];
|
|
856
|
+
if (brain.provider) candidates.push([{ kind: 'dispatch', provider: brain.provider, model: brain.model || null, effort: brain.effort || null }, 'current']);
|
|
857
|
+
const brainTier = brainInfo?.tier || (brain.model ? findModel(catalog, brain.provider, brain.model)?.row?.tier : null) || null;
|
|
858
|
+
const fits = (row) => row.status !== 'unavailable' && row.toolcall !== false && (!request.needsVision || row.vision === true);
|
|
859
|
+
const closeness = (row) => (brainTier ? Math.abs(tierRank(row.tier) - tierRank(brainTier)) : 0);
|
|
860
|
+
const badge = request.needsVision ? 'capable' : null;
|
|
861
|
+
if (brain.provider) {
|
|
862
|
+
visibleRows(catalog, brain.provider).filter(fits).filter((row) => row.id !== brain.model)
|
|
863
|
+
.sort((a, b) => closeness(a) - closeness(b) || outPrice(a) - outPrice(b))
|
|
864
|
+
.forEach((row) => candidates.push([{ kind: 'dispatch', provider: brain.provider, model: row.id, effort: null }, badge]));
|
|
865
|
+
}
|
|
866
|
+
if (request.needsVision) {
|
|
867
|
+
CATALOG_PROVIDERS.filter((p) => p !== brain.provider)
|
|
868
|
+
.flatMap((p) => visibleRows(catalog, p).filter(fits).map((row) => ({ provider: p, row })))
|
|
869
|
+
.sort((a, b) => outPrice(a.row) - outPrice(b.row) || tierRank(a.row.tier) - tierRank(b.row.tier))
|
|
870
|
+
.forEach(({ provider, row }) => candidates.push([{ kind: 'dispatch', provider, model: row.id, effort: null }, 'capable']));
|
|
871
|
+
} else if (brain.provider !== 'claude-code') {
|
|
872
|
+
const opus = strongestClaude(catalog);
|
|
873
|
+
if (opus) candidates.push([{ kind: 'dispatch', provider: 'claude-code', model: opus.id, effort: null }, null]);
|
|
874
|
+
}
|
|
875
|
+
return candidates;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Which side of the card's Here / Dispatch switch can run the task, and the option each side
|
|
880
|
+
* starts on: { sides: { direct, dispatch }, defaults: { direct, dispatch }, defaultOptionId }.
|
|
881
|
+
* A side is { available, reason, code } (code: worker_only, account_worker, disabled, none).
|
|
882
|
+
* Read from the options as they are sent, after the account check (a "here" that moved to a
|
|
883
|
+
* worker belongs to the dispatch side: its kind is "dispatch").
|
|
884
|
+
*/
|
|
885
|
+
export function routeSides({ options = [], request = {}, defaultOptionId = null } = {}) {
|
|
886
|
+
const usable = (o) => o && !o.disabled;
|
|
887
|
+
const chosen = options.find((o) => o.id === defaultOptionId && usable(o))
|
|
888
|
+
|| options.find((o) => o.badge === 'suggested' && usable(o)) || options.find(usable) || options[0] || null;
|
|
889
|
+
// A side starts on the saved route when Always ask marked it (preferred), else on the chosen option, else its first.
|
|
890
|
+
const firstOf = (kind) => options.find((o) => o.preferred === true && o.kind === kind && usable(o))
|
|
891
|
+
|| (chosen?.kind === kind && usable(chosen) ? chosen : null) || options.find((o) => o.kind === kind && usable(o)) || null;
|
|
892
|
+
const label = request.taskClassLabel || TASK_CLASS_META[request.taskClass]?.label || 'This task';
|
|
893
|
+
const side = (kind) => {
|
|
894
|
+
if (firstOf(kind)) return { available: true, reason: null, code: null };
|
|
895
|
+
if (kind === 'direct' && request.dispatchOnly) return { available: false, code: 'worker_only', reason: `${label} always runs on a worker.` };
|
|
896
|
+
if (kind === 'direct' && options.some((o) => o.kind === 'dispatch' && o.accountKind === 'direct')) {
|
|
897
|
+
return { available: false, code: 'account_worker', reason: "This conversation's account cannot take it, so it runs on a worker." };
|
|
898
|
+
}
|
|
899
|
+
const blocked = options.find((o) => o.kind === kind && o.disabled);
|
|
900
|
+
if (blocked) return { available: false, code: 'disabled', reason: blocked.disabledReason || null };
|
|
901
|
+
return { available: false, code: 'none', reason: kind === 'direct' ? 'Nothing in this conversation can run this task.' : 'No worker model can run this task.' };
|
|
902
|
+
};
|
|
903
|
+
return {
|
|
904
|
+
// The card opens on the chosen option's side, on that side's default.
|
|
905
|
+
defaultOptionId: (chosen && usable(chosen) ? firstOf(chosen.kind)?.id : null) || chosen?.id || 'here',
|
|
906
|
+
sides: { direct: side('direct'), dispatch: side('dispatch') },
|
|
907
|
+
defaults: { direct: firstOf('direct')?.id || null, dispatch: firstOf('dispatch')?.id || null },
|
|
908
|
+
};
|
|
774
909
|
}
|
|
775
910
|
|
|
776
911
|
/** Validate a route-card answer; returns { declined } or { target, optionId, remember }. */
|
|
@@ -779,14 +914,19 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
|
|
|
779
914
|
const optionId = String(response.optionId || response.option_id || '').trim();
|
|
780
915
|
const remember = response.remember === true || (typeof response.remember === 'string' && response.remember.length > 0);
|
|
781
916
|
const option = optionId && optionId !== 'other' ? (card.options || []).find((o) => o.id === optionId) : null;
|
|
917
|
+
// The model this conversation already runs on needs no catalog entry to stay on (an OpenCode model
|
|
918
|
+
// no longer listed as connected, an id the list spells otherwise): "here" on it changes nothing.
|
|
919
|
+
// A hidden one is still refused, as everywhere.
|
|
920
|
+
const onBrain = (target) => target.kind === 'direct' && !!brain.model && target.provider === brain.provider && String(target.model || '') === String(brain.model);
|
|
782
921
|
// A card can remain open while the catalog or hidden-model list changes.
|
|
783
922
|
// Check the chosen target again instead of trusting the option's old state.
|
|
784
923
|
const ensureAvailable = (target) => {
|
|
785
|
-
|
|
924
|
+
const own = onBrain(target);
|
|
925
|
+
if (target.model && (own ? isDisabled(catalog, target) : findModel(catalog, target.provider, target.model) === null)) {
|
|
786
926
|
if (isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `${target.model} is disabled in the Assistant's Models list. Pick an enabled model.`);
|
|
787
927
|
throw routeError('ROUTE_INVALID', `Unknown model ${target.model} for ${target.provider}.`);
|
|
788
928
|
}
|
|
789
|
-
if (findModel(catalog, target.provider, target.model)?.row?.status === 'unavailable') throw routeError('ROUTE_INVALID', UNAVAILABLE_TEXT);
|
|
929
|
+
if (!own && findModel(catalog, target.provider, target.model)?.row?.status === 'unavailable') throw routeError('ROUTE_INVALID', UNAVAILABLE_TEXT);
|
|
790
930
|
if (!target.model && isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `Every ${target.provider} model is disabled in the Assistant's Models list. Pick another provider.`);
|
|
791
931
|
// Design: only a confirmed vision: true on an available row (unknown or unavailable is refused too);
|
|
792
932
|
// other vision work: a confirmed false.
|
|
@@ -800,10 +940,16 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
|
|
|
800
940
|
};
|
|
801
941
|
// Image / video creation and design always run on a worker: a "here" pick is a dispatch onto that model.
|
|
802
942
|
const dispatchOnly = card.dispatchOnly === true;
|
|
943
|
+
// Only the brain's own provider runs in this conversation: a "here" on another one (an option made
|
|
944
|
+
// before the brain changed provider, or an "other" pick) is a worker, and the answer says so.
|
|
945
|
+
const elsewhere = (provider) => !!brain.provider && provider !== brain.provider;
|
|
803
946
|
if (option) {
|
|
804
947
|
if (option.disabled && !option.accountError) throw routeError('ROUTE_INVALID', option.disabledReason || 'That option is not available for this task.');
|
|
805
948
|
const corrections = [];
|
|
806
|
-
const
|
|
949
|
+
const moved = !dispatchOnly && option.kind === 'direct' && elsewhere(option.provider);
|
|
950
|
+
if (moved) corrections.push(otherProviderCorrection(option.provider, brain));
|
|
951
|
+
const kind = dispatchOnly || moved ? 'dispatch' : option.kind;
|
|
952
|
+
const target = effortChecked({ kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort, ...(option.accountId ? { accountId: option.accountChoice || option.accountId, ...(option.accountKind ? { accountKind: dispatchOnly || moved ? 'dispatch' : option.accountKind } : {}) } : {}) }, catalog, corrections);
|
|
807
953
|
ensureAvailable(target);
|
|
808
954
|
return { target, optionId, remember, corrections };
|
|
809
955
|
}
|
|
@@ -811,6 +957,11 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
|
|
|
811
957
|
if (!t || typeof t !== 'object' || !CATALOG_PROVIDERS.includes(t.provider)) throw routeError('ROUTE_INVALID', 'Pick one of the options or a catalog model.');
|
|
812
958
|
const kind = t.kind === 'direct' && t.provider === brain.provider && !dispatchOnly ? 'direct' : 'dispatch';
|
|
813
959
|
const corrections = [];
|
|
960
|
+
if (t.kind === 'direct' && kind === 'dispatch') {
|
|
961
|
+
corrections.push(dispatchOnly
|
|
962
|
+
? { field: 'kind', from: 'direct', to: 'dispatch', reason: 'dispatch_only', text: `${TASK_CLASS_META[card.taskClass]?.label || 'This task'} always runs on a worker, not here` }
|
|
963
|
+
: otherProviderCorrection(t.provider, brain));
|
|
964
|
+
}
|
|
814
965
|
const target = effortChecked({ kind, provider: t.provider, model: t.model ? String(t.model) : null, effort: t.effort ? String(t.effort).trim().slice(0, 40) : null, accountId: t.accountId ? String(t.accountId).trim() : null }, catalog, corrections);
|
|
815
966
|
ensureAvailable(target);
|
|
816
967
|
return { target, optionId: 'other', remember, corrections };
|
|
@@ -1017,9 +1168,13 @@ export function createAssistantRouter({
|
|
|
1017
1168
|
: row.signedIn === false ? `Account ${row.label || row.id} (${row.id}) is signed out. Sign it in from the ${providerLabel(target.provider)} account menu, or save another Account in Model routes.` : '';
|
|
1018
1169
|
const accountError = message ? { code: row ? 'ACCOUNT_SIGNED_OUT' : choice === 'auto' ? 'ACCOUNT_NO_SUBSCRIPTION' : 'ACCOUNT_NOT_FOUND', message } : null;
|
|
1019
1170
|
// Provider histories are account-owned. Another account can do the task on
|
|
1020
|
-
// a worker; it cannot resume this account's conversation.
|
|
1171
|
+
// a worker; it cannot resume this account's conversation. That move is always
|
|
1172
|
+
// said (a pinned account the user picked carries no usage reason of its own).
|
|
1021
1173
|
const moved = target.kind === 'direct' && accountId !== (brain.accountId || 'default');
|
|
1022
|
-
const accountCorrection = why
|
|
1174
|
+
const accountCorrection = why
|
|
1175
|
+
? { reason, from: before?.label || choice, to: row?.label || accountId, usage, workerForAccount: moved, message: why + (moved ? ` ${WORKER_FOR_ACCOUNT_TEXT}` : '') }
|
|
1176
|
+
: moved ? { field: 'kind', reason: 'account_worker', from: rows.find(a => a.id === (brain.accountId || 'default'))?.label || brain.accountId || 'default', to: row?.label || accountId, usage, workerForAccount: true, message: `${WORKER_FOR_ACCOUNT_TEXT.replace(/\.$/, '')} (${row?.label || accountId}).` }
|
|
1177
|
+
: null;
|
|
1023
1178
|
const enriched = { ...target, accountKind: target.accountKind || target.kind, ...(moved ? { kind: 'dispatch' } : {}), accountId, account, accountError, accountChoice: target.accountChoice || choice, accountCorrection };
|
|
1024
1179
|
if (check && accountError) throw routeError(accountError.code, message, 409, { accountId, target: enriched });
|
|
1025
1180
|
return enriched;
|
|
@@ -1085,8 +1240,14 @@ export function createAssistantRouter({
|
|
|
1085
1240
|
return call('continueDirect', sessionId, { target, route: source, text: source.summary || '' }) !== false;
|
|
1086
1241
|
}
|
|
1087
1242
|
|
|
1088
|
-
function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false, sessionId = null } = {}) {
|
|
1243
|
+
function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false, sessionId = null, switched = null, droppedRuns = [] } = {}) {
|
|
1089
1244
|
const described = target ? { ...describeTarget(target, { catalog: cat, brain, brainInfo }), continuation } : null;
|
|
1245
|
+
// The user's pick was the other kind than the brain proposed; a "here" pick on another account runs on a worker.
|
|
1246
|
+
const kindLead = source !== 'user' ? ''
|
|
1247
|
+
: target?.kind === 'dispatch' && target.accountKind === 'direct'
|
|
1248
|
+
? 'The user chose to do this here, but on another account than this conversation\'s, so it runs on a worker. '
|
|
1249
|
+
: switched === 'direct' ? `The user chose to do this here, not on a worker${droppedRuns.length ? ` (the held run${droppedRuns.length === 1 ? '' : 's'} ${droppedRuns.join(', ')} ${droppedRuns.length === 1 ? 'was' : 'were'} not started)` : ''}. `
|
|
1250
|
+
: switched === 'dispatch' ? 'The user chose a worker for this, not here. ' : '';
|
|
1090
1251
|
// A saved route that replaced the brain's pick (never mode) says so first.
|
|
1091
1252
|
const binding = (corrections || []).find((c) => c?.reason === 'remembered_route');
|
|
1092
1253
|
const skipped = (corrections || []).find((c) => c?.reason === 'remembered_disabled');
|
|
@@ -1103,7 +1264,7 @@ export function createAssistantRouter({
|
|
|
1103
1264
|
ok: true, status, routeId, mode, source, target: described, remembered, continuation,
|
|
1104
1265
|
corrections, runIds, expiresAt: card?.expiresAt ? new Date(card.expiresAt).toISOString() : null,
|
|
1105
1266
|
...(shares ? { budget: shares } : {}),
|
|
1106
|
-
next: `${lead}${nextText(status, described, { routeId, label: described?.label })}`,
|
|
1267
|
+
next: `${lead}${status === 'approved' ? kindLead : ''}${nextText(status, described, { routeId, label: described?.label })}`,
|
|
1107
1268
|
};
|
|
1108
1269
|
}
|
|
1109
1270
|
|
|
@@ -1155,13 +1316,23 @@ export function createAssistantRouter({
|
|
|
1155
1316
|
const routeId = `route-${randomId()}`;
|
|
1156
1317
|
const requestId = routeId;
|
|
1157
1318
|
const r = routing();
|
|
1158
|
-
const
|
|
1319
|
+
const built = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r, coverage: evaluation.coverage || null });
|
|
1320
|
+
const { options } = built;
|
|
1159
1321
|
for (const option of options) {
|
|
1160
1322
|
const first = request.proposals[0];
|
|
1161
1323
|
const fallback = origin === 'dispatch' && option.badge !== 'remembered' && option.provider === first?.provider ? first.accountId : null;
|
|
1162
1324
|
Object.assign(option, accountTarget(option, brain, cat, { check: false, fallback }));
|
|
1163
1325
|
if (option.accountError) { option.disabled = true; option.disabledReason = option.accountError.message; }
|
|
1326
|
+
// A "here" whose account cannot take it (this conversation's is out of usage) runs on a worker
|
|
1327
|
+
// on another account: the card names it as a worker and says why, never "Do it here".
|
|
1328
|
+
if (option.kind === 'dispatch' && option.accountKind === 'direct') {
|
|
1329
|
+
const described = describeTarget(option, { catalog: cat, brain, brainInfo });
|
|
1330
|
+
option.label = `${providerLabel(option.provider)} · ${described?.label || option.model || 'default'}${option.effort ? ` · ${option.effort}` : ''}`;
|
|
1331
|
+
option.warning = option.accountCorrection?.message || WORKER_FOR_ACCOUNT_TEXT;
|
|
1332
|
+
}
|
|
1164
1333
|
}
|
|
1334
|
+
// The sides and defaults as the options are sent (the account check can disable or move one).
|
|
1335
|
+
const { defaultOptionId, sides, defaults } = routeSides({ options, request, defaultOptionId: built.defaultOptionId });
|
|
1165
1336
|
const computer = computerConsent({ session, request, options, defaultOptionId, brain, origin });
|
|
1166
1337
|
const expiresAt = now() + r.cardTimeoutMinutes * 60_000;
|
|
1167
1338
|
const pref = r.preferences?.[request.taskClass];
|
|
@@ -1169,7 +1340,7 @@ export function createAssistantRouter({
|
|
|
1169
1340
|
routeId, requestId, sessionId, origin, status: 'pending', mode, workflowId,
|
|
1170
1341
|
taskClass: request.taskClass, summary: request.summary, confidence: request.confidence, needsVision: request.needsVision,
|
|
1171
1342
|
needsOutput: request.needsOutput || null, dispatchOnly: request.dispatchOnly === true, visionOnly: request.visionOnly === true,
|
|
1172
|
-
reasons: evaluation.reasons, request, options, defaultOptionId, runIds: new Set(runIds), waiters: new Set(),
|
|
1343
|
+
reasons: evaluation.reasons, request, options, defaultOptionId, sides, defaults, runIds: new Set(runIds), waiters: new Set(),
|
|
1173
1344
|
createdAt: now(), expiresAt, timer: null, mailboxOnDecision: origin === 'dispatch', brain,
|
|
1174
1345
|
// The option whose approval is also the computer approval (computerConsent), as this card was sent.
|
|
1175
1346
|
computer,
|
|
@@ -1188,6 +1359,8 @@ export function createAssistantRouter({
|
|
|
1188
1359
|
visionOnly: request.visionOnly === true,
|
|
1189
1360
|
brain: { provider: brain.provider, model: brain.model || null, label: brainInfo?.label || brain.model || providerLabel(brain.provider), vision: brainInfo?.vision ?? null },
|
|
1190
1361
|
options, defaultOptionId,
|
|
1362
|
+
// The card's Here / Dispatch switch: which side can run the task (and why not), and the option each side starts on.
|
|
1363
|
+
sides, defaults,
|
|
1191
1364
|
other: { allowed: true, providers: CATALOG_PROVIDERS, catalogPath: '/api/assistant/catalog' },
|
|
1192
1365
|
remember: { available: request.taskClass !== 'chat', taskClass: request.taskClass, label: request.taskClassLabel, current: pref ? (pref.label || [pref.provider, pref.model].filter(Boolean).join('/')) : null },
|
|
1193
1366
|
decline: { allowed: true, label: 'Cancel' },
|
|
@@ -1396,21 +1569,28 @@ export function createAssistantRouter({
|
|
|
1396
1569
|
try { await budget.ensureCovered(card.sessionId, { provider: target.provider, forBrain: target.kind === 'direct', free: targetIsFree(cat, target) }); } catch { /* decided on the shares as they are */ }
|
|
1397
1570
|
if (cards.get(routeId) !== card || card.status !== 'pending') openCard(routeId, { sessionId });
|
|
1398
1571
|
if (!budgetCovers(coverageOf(card.sessionId), cat, target)) {
|
|
1572
|
+
const unpriced = unpricedText(target.provider);
|
|
1573
|
+
if (unpriced) throw routeError('BUDGET_UNPRICED', unpriced, 409, { provider: target.provider });
|
|
1399
1574
|
throw routeError('BUDGET_SHARE_EXHAUSTED', `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up. Pick another option (or a free model), or raise the budget.`, 409, { provider: target.provider });
|
|
1400
1575
|
}
|
|
1401
1576
|
}
|
|
1577
|
+
// "Here" on a held dispatch's card: the brain does the task in this conversation, so the held
|
|
1578
|
+
// worker runs are not started (they stop, route_here) and the brain is told to do it itself.
|
|
1579
|
+
const keptHere = card.runIds.size > 0 && target.kind === 'direct';
|
|
1402
1580
|
// Every held run must be able to start on the pick (the dispatcher's checkHeld: a design run's
|
|
1403
1581
|
// sight and browser settings on that provider). A pick one would refuse is not approved, saved
|
|
1404
|
-
// or reported started: the card stays open with the reason.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1582
|
+
// or reported started: the card stays open with the reason. A "here" pick starts none.
|
|
1583
|
+
if (!keptHere) {
|
|
1584
|
+
for (const runId of card.runIds) {
|
|
1585
|
+
const refusal = call('checkHeld', runId, target);
|
|
1586
|
+
if (refusal) throw routeError(refusal.code || 'ROUTE_INVALID', refusal.message || 'The held run cannot start on that model.', Number(refusal.status) || 400);
|
|
1587
|
+
}
|
|
1408
1588
|
}
|
|
1409
1589
|
// "Do it here" on another model continues in the turn the card was raised in,
|
|
1410
1590
|
// handed over first. The runtime refuses a pick that turn no longer owns (a
|
|
1411
1591
|
// newer prompt, a Stop or the session closing came after it): then nothing is
|
|
1412
1592
|
// approved or remembered, and the card expires on every host.
|
|
1413
|
-
const continuation = !card.runIds.size && continuesHere(target, brain);
|
|
1593
|
+
const continuation = (keptHere || !card.runIds.size) && continuesHere(target, brain);
|
|
1414
1594
|
// The decision is claimed here, before the first side effect (the runtime's continuation, the
|
|
1415
1595
|
// held runs, the mailbox): nothing a sink does can close or decide this card again meanwhile.
|
|
1416
1596
|
card.status = 'approved';
|
|
@@ -1428,21 +1608,23 @@ export function createAssistantRouter({
|
|
|
1428
1608
|
remembered = true;
|
|
1429
1609
|
} catch (error) { log('router:remember-error', error?.message || String(error)); }
|
|
1430
1610
|
}
|
|
1611
|
+
const heldRuns = keptHere ? [] : [...card.runIds];
|
|
1612
|
+
const droppedRuns = keptHere ? [...card.runIds] : [];
|
|
1431
1613
|
const route = {
|
|
1432
1614
|
routeId, status: 'approved', sessionId: card.sessionId, taskClass: card.taskClass, summary: card.summary, confidence: card.confidence,
|
|
1433
|
-
target, decidedBy: 'user', remembered, optionId, corrections, mode: card.mode, runIds:
|
|
1615
|
+
target, decidedBy: 'user', remembered, optionId, corrections, mode: card.mode, runIds: heldRuns,
|
|
1434
1616
|
reason: card.options.find((o) => o.id === optionId)?.reason || null, alternatives: card.alternatives, expiresAt: now() + DECIDED_TTL_MS,
|
|
1435
1617
|
};
|
|
1436
1618
|
decided.set(routeId, route);
|
|
1437
1619
|
noteBudgetTask(card.sessionId, { taskClass: card.taskClass, summary: card.summary }, target);
|
|
1438
|
-
const heldRuns = [...card.runIds];
|
|
1439
1620
|
// A remote session: the held run this card was for is its one dispatch.
|
|
1440
1621
|
if (heldRuns.length && session.remote === true) route.consumed = true;
|
|
1622
|
+
for (const runId of droppedRuns) call('declineHeld', runId, 'route_here');
|
|
1441
1623
|
for (const runId of heldRuns) call('startHeld', runId, target, { routeId, decidedBy: 'user', remembered, optionId });
|
|
1442
1624
|
const view = routeView(route, { catalog: cat, brain, brainInfo });
|
|
1443
1625
|
call('routeEvent', card.sessionId, 'decided', view);
|
|
1444
1626
|
const inTurnWaiter = card.waiters.size > 0;
|
|
1445
|
-
const viaMailbox = !inTurnWaiter && !continuation && (card.mailboxOnDecision ||
|
|
1627
|
+
const viaMailbox = !inTurnWaiter && !continuation && (card.mailboxOnDecision || card.runIds.size > 0);
|
|
1446
1628
|
// The card said the Mac will be controlled, and this is the option it said it for: the yes is
|
|
1447
1629
|
// also the computer approval of the turn that carries the route out (this one, or the mailbox
|
|
1448
1630
|
// turn that executes the decided route). From what the card recorded when it was sent; the
|
|
@@ -1451,9 +1633,19 @@ export function createAssistantRouter({
|
|
|
1451
1633
|
// `authority` is passed on untouched and never kept: the runtime alone can tell whose it is.
|
|
1452
1634
|
call('computerApproved', card.sessionId, { routeId, origin, viaMailbox, authority });
|
|
1453
1635
|
}
|
|
1454
|
-
|
|
1636
|
+
// The brain proposed one kind and the user picked the other (the card's Here / Dispatch switch):
|
|
1637
|
+
// the result says so first, so the brain follows the pick, not its proposal.
|
|
1638
|
+
const proposed = card.request?.proposals?.[0]?.kind || null;
|
|
1639
|
+
const picked = target.accountKind || target.kind;
|
|
1640
|
+
const switched = proposed && proposed !== picked ? picked : null;
|
|
1641
|
+
const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation, sessionId: card.sessionId, switched, droppedRuns });
|
|
1455
1642
|
if (viaMailbox) {
|
|
1456
|
-
|
|
1643
|
+
const label = view.target?.label || 'a model';
|
|
1644
|
+
const what = card.summary ? `"${card.summary}"` : 'this task';
|
|
1645
|
+
const text = heldRuns.length ? `The user picked ${label}; the held run${heldRuns.length === 1 ? '' : 's'} started.`
|
|
1646
|
+
: droppedRuns.length ? `The user chose to do ${what} here with ${label} instead of on a worker: the held run${droppedRuns.length === 1 ? '' : 's'} ${droppedRuns.join(', ')} ${droppedRuns.length === 1 ? 'was' : 'were'} not started. Do that task here yourself now; do not dispatch it again.`
|
|
1647
|
+
: `The user picked ${label}. ${result.next}`;
|
|
1648
|
+
call('mailbox', card.sessionId, { kind: 'route_decided', route: view, runIds: heldRuns, ...(droppedRuns.length ? { droppedRunIds: droppedRuns } : {}), text });
|
|
1457
1649
|
}
|
|
1458
1650
|
settle(card, { status: 'approved', result });
|
|
1459
1651
|
closeCard(card);
|
|
@@ -1483,6 +1675,12 @@ export function createAssistantRouter({
|
|
|
1483
1675
|
const remote = session.remote === true;
|
|
1484
1676
|
if (routeId) {
|
|
1485
1677
|
const approved = decided.get(routeId);
|
|
1678
|
+
// The user picked "here" on this route's card: it runs in this conversation, never on a worker
|
|
1679
|
+
// (a brain's own direct route, or a saved one in Never ask, keeps its old dispatch behaviour).
|
|
1680
|
+
if (approved && approved.sessionId === sessionId && approved.target?.kind === 'direct' && approved.decidedBy === 'user') {
|
|
1681
|
+
const where = approved.target.model ? ` on ${approved.target.model}` : '';
|
|
1682
|
+
throw routeError('ROUTE_IS_DIRECT', `Route ${routeId} was decided by the user to run here, in this conversation${where}, not on a worker. Do the task yourself now; do not dispatch it. If it should go to a worker after all, ask the user, then call agent_route again.`, 409, { routeId, target: approved.target });
|
|
1683
|
+
}
|
|
1486
1684
|
if (approved && approved.sessionId === sessionId && approved.target && !approved.consumed) {
|
|
1487
1685
|
// A remote session's route is claimed here, so a second dispatch cannot take it meanwhile.
|
|
1488
1686
|
// `claimed` tells the dispatcher to give it back (releaseRoute) if it refuses the launch
|
|
@@ -1495,7 +1693,11 @@ export function createAssistantRouter({
|
|
|
1495
1693
|
let runTarget = t;
|
|
1496
1694
|
let budgetCorrection = null;
|
|
1497
1695
|
if (budgeted() && !budgetCovers(coverageOf(sessionId), cat, t)) {
|
|
1498
|
-
if (approved.mode === 'always-ask' && approved.decidedBy === 'user')
|
|
1696
|
+
if (approved.mode === 'always-ask' && approved.decidedBy === 'user') {
|
|
1697
|
+
const unpriced = unpricedText(t.provider);
|
|
1698
|
+
if (unpriced) throw routeError('BUDGET_UNPRICED', `${unpriced} Choose a new task route.`, 409, { routeId, provider: t.provider });
|
|
1699
|
+
throw routeError('BUDGET_SHARE_EXHAUSTED', 'The selected model has no session budget left. Choose a new task route or raise the budget.', 409, { routeId, provider: t.provider });
|
|
1700
|
+
}
|
|
1499
1701
|
try { await budget.ensureCovered(sessionId, { provider: t.provider, forBrain: false, free: targetIsFree(cat, t), task: { taskClass: approved.taskClass, summary: approved.summary || null, provider: t.provider, model: t.model || null } }); } catch { /* decided on the shares as they are */ }
|
|
1500
1702
|
const request = normalizeRouteRequest({ task_class: approved.taskClass, summary: approved.summary || '', confidence: 1, needs_vision: !!spec.usesComputer, proposals: [{ kind: 'dispatch', provider: t.provider, model: t.model || null, effort: t.effort || null }] }, { brain });
|
|
1501
1703
|
const moved = budgetMove({ target: { ...t, kind: 'dispatch' }, request, catalog: cat, brain, brainInfo, routing: r, coverage: coverageOf(sessionId) });
|
|
@@ -1635,7 +1837,7 @@ export function createAssistantRouter({
|
|
|
1635
1837
|
const mode = modeFor(sessionId);
|
|
1636
1838
|
const prefs = Object.entries(r.preferences || {}).filter(([, p]) => p?.provider)
|
|
1637
1839
|
.map(([cls, p]) => `${cls} → ${p.kind === 'direct' ? 'here' : p.provider}${p.model ? `/${p.model}` : ''}${p.effort ? ` (${p.effort})` : ''}${hasRouteAccounts(p.provider) ? ` as ${routeAccountText(p, { accounts: liveAccounts(catalog.peek?.()), brain: getSession(sessionId)?.brain, workers: { codex: configStore?.workers?.()?.codexAccount, 'claude-code': configStore?.workers?.()?.claudeAccount } })}` : ''}`);
|
|
1638
|
-
const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model)'
|
|
1840
|
+
const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model, and whether it runs here or on a worker)'
|
|
1639
1841
|
: mode === 'never' ? 'never ask (agent_route approves at once; the saved routes below are binding and agent_route applies them over your pick, so classify the task correctly)'
|
|
1640
1842
|
: `ask when unsure (agent_route asks the user below ${Math.round(r.askBelow * 100)}% confidence or for unknown/missing models)`;
|
|
1641
1843
|
const text = `[SynaBun Router] Route mode: ${modeLine}.${prefs.length ? ` ${mode === 'never' ? 'Saved routes (binding)' : 'Remembered routes'}: ${prefs.join('; ')}.` : ''}`;
|