talon-agent 1.12.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -52
- package/package.json +39 -12
- package/prompts/README.md +112 -0
- package/prompts/base.md +12 -12
- package/prompts/discord.md +10 -47
- package/prompts/heartbeat.md +23 -8
- package/prompts/identity.md +12 -22
- package/prompts/native.md +23 -0
- package/prompts/system/contract-text-or-tools.md +8 -0
- package/prompts/system/contract-text-preferred.md +5 -0
- package/prompts/system/contract-tool-only.md +10 -0
- package/prompts/system/cron.md +3 -0
- package/prompts/system/daily-memory.md +3 -0
- package/prompts/system/goals.md +7 -0
- package/prompts/system/heartbeat-agent.md +44 -0
- package/prompts/system/persistent-memory.md +8 -0
- package/prompts/system/skills.md +9 -0
- package/prompts/system/triggers.md +5 -0
- package/prompts/system/workspace.md +9 -0
- package/prompts/teams.md +4 -14
- package/prompts/telegram.md +17 -89
- package/src/app.ts +278 -0
- package/src/backend/claude-sdk/constants.ts +22 -10
- package/src/backend/claude-sdk/factory.ts +79 -22
- package/src/backend/claude-sdk/handler.ts +431 -160
- package/src/backend/claude-sdk/index.ts +6 -2
- package/src/backend/claude-sdk/mcp-ready.ts +54 -0
- package/src/backend/claude-sdk/model-drift.ts +65 -0
- package/src/backend/claude-sdk/model-provider.ts +12 -6
- package/src/backend/claude-sdk/models/convert.ts +169 -0
- package/src/backend/claude-sdk/models/discovery.ts +203 -0
- package/src/backend/claude-sdk/models/index.ts +18 -0
- package/src/backend/claude-sdk/models/parsing.ts +264 -0
- package/src/backend/claude-sdk/models/static.ts +45 -0
- package/src/backend/claude-sdk/one-shot.ts +19 -13
- package/src/backend/claude-sdk/options.ts +228 -104
- package/src/backend/claude-sdk/state.ts +1 -1
- package/src/backend/claude-sdk/stream.ts +153 -45
- package/src/backend/claude-sdk/warm.ts +12 -5
- package/src/backend/codex/auth.ts +216 -40
- package/src/backend/codex/constants.ts +63 -22
- package/src/backend/codex/discovery.ts +427 -0
- package/src/backend/codex/factory.ts +63 -33
- package/src/backend/codex/handler/events.ts +239 -0
- package/src/backend/codex/handler/index.ts +15 -0
- package/src/backend/codex/handler/message.ts +669 -0
- package/src/backend/codex/handler/state.ts +14 -0
- package/src/backend/codex/handler/usage.ts +68 -0
- package/src/backend/codex/index.ts +2 -2
- package/src/backend/codex/init.ts +92 -11
- package/src/backend/codex/mcp-config.ts +89 -62
- package/src/backend/codex/models.ts +305 -54
- package/src/backend/codex/oauth-incompat.ts +302 -0
- package/src/backend/codex/one-shot.ts +91 -8
- package/src/backend/codex/state.ts +54 -3
- package/src/backend/codex/token-usage.ts +434 -0
- package/src/backend/kilo/factory.ts +60 -35
- package/src/backend/kilo/handler/index.ts +13 -0
- package/src/backend/kilo/handler/message.ts +317 -0
- package/src/backend/kilo/handler/state.ts +14 -0
- package/src/backend/kilo/handler/turn.ts +321 -0
- package/src/backend/kilo/index.ts +5 -5
- package/src/backend/kilo/model-provider.ts +26 -168
- package/src/backend/kilo/models/index.ts +52 -0
- package/src/backend/kilo/one-shot.ts +20 -142
- package/src/backend/kilo/server.ts +14 -21
- package/src/backend/openai-agents/builtins.ts +215 -114
- package/src/backend/openai-agents/constants.ts +41 -15
- package/src/backend/openai-agents/discovery.ts +332 -0
- package/src/backend/openai-agents/factory.ts +66 -28
- package/src/backend/openai-agents/handler/events.ts +170 -0
- package/src/backend/openai-agents/handler/index.ts +13 -0
- package/src/backend/openai-agents/handler/message.ts +527 -0
- package/src/backend/openai-agents/handler/state.ts +13 -0
- package/src/backend/openai-agents/index.ts +22 -4
- package/src/backend/openai-agents/init.ts +51 -88
- package/src/backend/openai-agents/mcp-pool.ts +252 -0
- package/src/backend/openai-agents/models.ts +118 -11
- package/src/backend/openai-agents/session.ts +277 -0
- package/src/backend/openai-agents/state.ts +82 -11
- package/src/backend/opencode/factory.ts +54 -25
- package/src/backend/opencode/handler/index.ts +13 -0
- package/src/backend/opencode/handler/message.ts +325 -0
- package/src/backend/opencode/handler/state.ts +14 -0
- package/src/backend/opencode/handler/turn.ts +304 -0
- package/src/backend/opencode/index.ts +2 -2
- package/src/backend/opencode/model-provider.ts +26 -168
- package/src/backend/opencode/models/index.ts +51 -0
- package/src/backend/opencode/one-shot.ts +20 -137
- package/src/backend/opencode/server.ts +12 -11
- package/src/backend/remote-server/client.ts +13 -6
- package/src/backend/remote-server/events.ts +59 -9
- package/src/backend/remote-server/lifecycle.ts +3 -1
- package/src/backend/remote-server/mcp.ts +52 -19
- package/src/backend/remote-server/model-catalog/catalog.ts +270 -0
- package/src/backend/remote-server/model-catalog/index.ts +100 -0
- package/src/backend/remote-server/model-catalog/presentation.ts +298 -0
- package/src/backend/remote-server/model-catalog/provider.ts +193 -0
- package/src/backend/remote-server/model-catalog/resolve.ts +225 -0
- package/src/backend/remote-server/model-catalog/types.ts +136 -0
- package/src/backend/remote-server/one-shot.ts +225 -0
- package/src/backend/remote-server/session-helpers.ts +3 -4
- package/src/backend/remote-server/state.ts +1 -1
- package/src/backend/shared/delivery-contract.ts +161 -0
- package/src/backend/shared/delivery.ts +49 -0
- package/src/backend/shared/flow-violation.ts +38 -7
- package/src/backend/shared/frontends.ts +66 -0
- package/src/backend/shared/handle-retry.ts +244 -0
- package/src/backend/shared/handler-to-events.ts +186 -0
- package/src/backend/shared/handler-types.ts +57 -0
- package/src/backend/shared/index.ts +51 -5
- package/src/backend/shared/metrics.ts +199 -0
- package/src/backend/shared/model-retry.ts +1 -1
- package/src/backend/shared/prompt-format.ts +69 -0
- package/src/backend/shared/session-name.ts +1 -1
- package/src/backend/shared/stream-state.ts +36 -2
- package/src/backend/shared/system-prompt.ts +200 -23
- package/src/bootstrap.ts +338 -38
- package/src/cli/chat.ts +49 -0
- package/src/cli/config-view.ts +100 -0
- package/src/cli/config.ts +105 -0
- package/src/cli/context.ts +23 -0
- package/src/cli/daemon.ts +95 -0
- package/src/cli/doctor.ts +53 -0
- package/src/cli/index.ts +139 -0
- package/src/cli/logs.ts +60 -0
- package/src/cli/menu.ts +104 -0
- package/src/cli/setup.ts +455 -0
- package/src/cli/status.ts +75 -0
- package/src/cli.ts +20 -1211
- package/src/core/agent-runtime/README.md +173 -0
- package/src/{backend/registry.ts → core/agent-runtime/backend-registry.ts} +15 -6
- package/src/core/agent-runtime/capabilities.ts +330 -0
- package/src/core/agent-runtime/contract-tests.ts +378 -0
- package/src/core/agent-runtime/events.ts +291 -0
- package/src/core/agent-runtime/index.ts +62 -0
- package/src/core/agent-runtime/model-ref.ts +130 -0
- package/src/core/background/cron.ts +469 -0
- package/src/core/{dream.ts → background/dream.ts} +118 -53
- package/src/core/background/heartbeat/agent.ts +333 -0
- package/src/core/background/heartbeat/index.ts +25 -0
- package/src/core/background/heartbeat/scheduler.ts +211 -0
- package/src/core/background/heartbeat/state.ts +153 -0
- package/src/core/background/index.ts +21 -0
- package/src/core/background/isolated-agent.ts +115 -0
- package/src/core/background/job-health.ts +140 -0
- package/src/core/background/job-oneshot.ts +165 -0
- package/src/core/background/job-prompt.ts +54 -0
- package/src/core/{pulse.ts → background/pulse.ts} +6 -5
- package/src/core/background/triggers/command.ts +60 -0
- package/src/core/background/triggers/exit.ts +202 -0
- package/src/core/background/triggers/index.ts +38 -0
- package/src/core/background/triggers/output.ts +142 -0
- package/src/core/background/triggers/resume.ts +127 -0
- package/src/core/background/triggers/spawn.ts +282 -0
- package/src/core/background/triggers/state.ts +52 -0
- package/src/core/constants.ts +57 -28
- package/src/core/daemon/control.ts +229 -0
- package/src/core/daemon/discovery.ts +169 -0
- package/src/core/daemon/pidfile.ts +98 -0
- package/src/core/doctor.ts +459 -0
- package/src/core/engine/backend-controller/holders.ts +17 -0
- package/src/core/engine/backend-controller/index.ts +71 -0
- package/src/core/engine/backend-controller/legacy.ts +111 -0
- package/src/core/engine/backend-controller/pool.ts +296 -0
- package/src/core/engine/backend-controller/rebind.ts +205 -0
- package/src/core/engine/backend-controller/state.ts +150 -0
- package/src/core/engine/backend-controller/types.ts +51 -0
- package/src/core/engine/dispatcher.ts +68 -0
- package/src/core/engine/gateway-actions/cron.ts +518 -0
- package/src/core/engine/gateway-actions/fetch-url.ts +120 -0
- package/src/core/engine/gateway-actions/goals.ts +178 -0
- package/src/core/engine/gateway-actions/history.ts +53 -0
- package/src/core/engine/gateway-actions/index.ts +60 -0
- package/src/core/engine/gateway-actions/models.ts +120 -0
- package/src/core/engine/gateway-actions/plugins.ts +83 -0
- package/src/core/engine/gateway-actions/scripts.ts +127 -0
- package/src/core/engine/gateway-actions/shared.ts +66 -0
- package/src/core/engine/gateway-actions/skills.ts +120 -0
- package/src/core/engine/gateway-actions/triggers.ts +237 -0
- package/src/core/engine/gateway-actions/types.ts +18 -0
- package/src/core/{gateway.ts → engine/gateway.ts} +223 -60
- package/src/core/engine/index.ts +20 -0
- package/src/core/engine/model-audit.ts +121 -0
- package/src/core/errors.ts +68 -6
- package/src/core/mcp-hub/children.ts +254 -0
- package/src/core/mcp-hub/index.ts +335 -0
- package/src/core/mcp-hub/proxy-server.ts +45 -0
- package/src/core/mcp-hub/talon-server.ts +71 -0
- package/src/core/memory/retrieval.ts +90 -0
- package/src/core/models/active-model.ts +441 -0
- package/src/core/{models.ts → models/catalog.ts} +6 -0
- package/src/core/models/index.ts +17 -0
- package/src/core/models/reasoning-levels.ts +57 -0
- package/src/core/plugin/actions.ts +32 -0
- package/src/core/plugin/builtins.ts +162 -0
- package/src/core/plugin/index.ts +37 -0
- package/src/core/plugin/loader.ts +289 -0
- package/src/core/plugin/mcp.ts +86 -0
- package/src/core/plugin/registry.ts +124 -0
- package/src/core/plugin/types.ts +126 -0
- package/src/core/prompt/assemble.ts +273 -0
- package/src/core/prompt/disk-prompts.ts +41 -0
- package/src/core/prompt/embed.ts +107 -0
- package/src/core/prompt/embedded-prompts.ts +85 -0
- package/src/core/prompt/index.ts +30 -0
- package/src/core/prompt/templates.ts +124 -0
- package/src/core/prompt/workspace-listing.ts +91 -0
- package/src/core/scripting/lua-runner.ts +210 -0
- package/src/core/scripts/runner.ts +125 -0
- package/src/core/soul/README.md +110 -0
- package/src/core/soul/RESEARCH.md +98 -0
- package/src/core/soul/associative.ts +98 -0
- package/src/core/soul/centrality.ts +98 -0
- package/src/core/soul/cluster.ts +83 -0
- package/src/core/soul/compiler.ts +207 -0
- package/src/core/soul/consolidate.ts +179 -0
- package/src/core/soul/critic.ts +159 -0
- package/src/core/soul/dag.ts +265 -0
- package/src/core/soul/delta.ts +123 -0
- package/src/core/soul/drift.ts +99 -0
- package/src/core/soul/embedder.ts +129 -0
- package/src/core/soul/emergent-critic.ts +96 -0
- package/src/core/soul/forgetting.ts +131 -0
- package/src/core/soul/governance.ts +93 -0
- package/src/core/soul/hash.ts +97 -0
- package/src/core/soul/hdc.ts +154 -0
- package/src/core/soul/index.ts +140 -0
- package/src/core/soul/kernel.ts +540 -0
- package/src/core/soul/lattice.ts +103 -0
- package/src/core/soul/lens.ts +110 -0
- package/src/core/soul/projector.ts +240 -0
- package/src/core/soul/reflect.ts +170 -0
- package/src/core/soul/reflex.ts +164 -0
- package/src/core/soul/retrieve.ts +146 -0
- package/src/core/soul/salience.ts +142 -0
- package/src/core/soul/service.ts +204 -0
- package/src/core/soul/settings.ts +47 -0
- package/src/core/soul/signals.ts +117 -0
- package/src/core/soul/talon-embedder.ts +80 -0
- package/src/core/soul/taps.ts +199 -0
- package/src/core/soul/types.ts +298 -0
- package/src/core/soul/valence.ts +83 -0
- package/src/core/tools/goals.ts +117 -0
- package/src/core/tools/index.ts +10 -0
- package/src/core/tools/mcp-env.ts +94 -0
- package/src/core/tools/mcp-server.ts +27 -4
- package/src/core/tools/messaging.ts +122 -29
- package/src/core/tools/models.ts +39 -0
- package/src/core/tools/scheduling.ts +133 -14
- package/src/core/tools/schemas.ts +16 -4
- package/src/core/tools/scripts.ts +78 -0
- package/src/core/tools/skills.ts +90 -0
- package/src/core/tools/stickers.ts +1 -1
- package/src/core/tools/triggers.ts +19 -3
- package/src/core/tools/types.ts +6 -6
- package/src/core/types.ts +62 -112
- package/src/core/update/self-update.ts +269 -0
- package/src/core/weaver/index.ts +10 -0
- package/src/core/weaver/loom.ts +125 -0
- package/src/core/weaver/thread-session.ts +51 -0
- package/src/core/weaver/thread.ts +196 -0
- package/src/core/weaver/weaver.ts +357 -0
- package/src/frontend/discord/actions/chat-info.ts +268 -0
- package/src/frontend/discord/actions/index.ts +77 -0
- package/src/frontend/discord/actions/media.ts +190 -0
- package/src/frontend/discord/actions/messaging.ts +277 -0
- package/src/frontend/discord/actions/shared.ts +100 -0
- package/src/frontend/discord/actions/types.ts +33 -0
- package/src/frontend/discord/admin.ts +13 -23
- package/src/frontend/discord/callbacks/autocomplete.ts +55 -0
- package/src/frontend/discord/{callbacks.ts → callbacks/components.ts} +150 -190
- package/src/frontend/discord/callbacks/index.ts +14 -0
- package/src/frontend/discord/callbacks/modals.ts +76 -0
- package/src/frontend/discord/callbacks/shared.ts +22 -0
- package/src/frontend/discord/commands/admin.ts +88 -0
- package/src/frontend/discord/commands/definitions.ts +200 -0
- package/src/frontend/discord/commands/index.ts +19 -0
- package/src/frontend/discord/commands/info.ts +106 -0
- package/src/frontend/discord/commands/router.ts +149 -0
- package/src/frontend/discord/commands/session.ts +89 -0
- package/src/frontend/discord/commands/settings.ts +405 -0
- package/src/frontend/discord/commands/shared.ts +50 -0
- package/src/frontend/discord/formatting.ts +8 -34
- package/src/frontend/discord/handlers/access.ts +248 -0
- package/src/frontend/discord/handlers/context.ts +161 -0
- package/src/frontend/discord/handlers/delivery.ts +94 -0
- package/src/frontend/discord/handlers/index.ts +34 -0
- package/src/frontend/discord/handlers/messages.ts +120 -0
- package/src/frontend/discord/handlers/queue.ts +135 -0
- package/src/frontend/discord/handlers/registry.ts +29 -0
- package/src/frontend/discord/handlers/state.ts +94 -0
- package/src/frontend/discord/helpers.ts +46 -57
- package/src/frontend/discord/index.ts +10 -7
- package/src/frontend/discord/middleware.ts +2 -2
- package/src/frontend/native/actions.ts +133 -0
- package/src/frontend/native/chats.ts +146 -0
- package/src/frontend/native/discovery.ts +50 -0
- package/src/frontend/native/index.ts +1227 -0
- package/src/frontend/native/logs.ts +95 -0
- package/src/frontend/native/protocol.ts +253 -0
- package/src/frontend/native/server.ts +612 -0
- package/src/frontend/native/settings.ts +205 -0
- package/src/frontend/native/turn-meta.ts +43 -0
- package/src/frontend/shared/format.ts +51 -0
- package/src/frontend/shared/reasoning-levels.ts +46 -0
- package/src/frontend/shared/session-status.ts +189 -0
- package/src/frontend/shared/status-context.ts +125 -0
- package/src/frontend/teams/actions.ts +7 -5
- package/src/frontend/teams/formatting.ts +7 -18
- package/src/frontend/teams/graph.ts +2 -1
- package/src/frontend/teams/index.ts +76 -54
- package/src/frontend/telegram/actions/chat-info.ts +323 -0
- package/src/frontend/telegram/actions/index.ts +68 -0
- package/src/frontend/telegram/actions/media.ts +224 -0
- package/src/frontend/telegram/actions/messaging.ts +295 -0
- package/src/frontend/telegram/actions/shared.ts +58 -0
- package/src/frontend/telegram/actions/types.ts +31 -0
- package/src/frontend/telegram/admin.ts +12 -22
- package/src/frontend/telegram/callbacks/effort.ts +68 -0
- package/src/frontend/telegram/callbacks/index.ts +68 -0
- package/src/frontend/telegram/callbacks/model.ts +311 -0
- package/src/frontend/telegram/callbacks/pulse.ts +50 -0
- package/src/frontend/telegram/callbacks/settings.ts +177 -0
- package/src/frontend/telegram/callbacks/shared.ts +71 -0
- package/src/frontend/telegram/commands/admin.ts +226 -0
- package/src/frontend/telegram/commands/definitions.ts +60 -0
- package/src/frontend/telegram/commands/index.ts +39 -0
- package/src/frontend/telegram/commands/info.ts +125 -0
- package/src/frontend/telegram/commands/session.ts +77 -0
- package/src/frontend/telegram/commands/settings.ts +345 -0
- package/src/frontend/telegram/commands/state.ts +34 -0
- package/src/frontend/telegram/formatting.ts +14 -25
- package/src/frontend/telegram/handlers/access.ts +287 -0
- package/src/frontend/telegram/handlers/context.ts +210 -0
- package/src/frontend/telegram/handlers/delivery.ts +236 -0
- package/src/frontend/telegram/handlers/index.ts +39 -0
- package/src/frontend/telegram/handlers/messages.ts +477 -0
- package/src/frontend/telegram/handlers/queue.ts +241 -0
- package/src/frontend/telegram/handlers/state.ts +75 -0
- package/src/frontend/telegram/helpers/diagnostics.ts +188 -0
- package/src/frontend/telegram/helpers/format.ts +41 -0
- package/src/frontend/telegram/helpers/index.ts +13 -0
- package/src/frontend/telegram/{helpers.ts → helpers/menu.ts} +209 -210
- package/src/frontend/telegram/index.ts +24 -27
- package/src/frontend/telegram/middleware.ts +16 -3
- package/src/frontend/telegram/model-callbacks.ts +14 -0
- package/src/frontend/telegram/model-menu.ts +317 -0
- package/src/frontend/telegram/sticker-library.ts +178 -0
- package/src/frontend/telegram/userbot.ts +2 -44
- package/src/frontend/terminal/commands.ts +47 -41
- package/src/frontend/terminal/index.ts +46 -31
- package/src/frontend/terminal/input.ts +5 -0
- package/src/frontend/terminal/renderer.ts +4 -2
- package/src/index.ts +13 -171
- package/src/login.ts +3 -2
- package/src/native/blake3-wasm-bytes.ts +7 -0
- package/src/native/blake3.ts +234 -0
- package/src/native/htmlents-wasm-bytes.ts +7 -0
- package/src/native/htmlents.ts +56 -0
- package/src/native/prelude.d.mts +168 -0
- package/src/native/prelude.mjs +1561 -0
- package/src/native/registry.ts +142 -0
- package/src/native/runtime.ts +164 -0
- package/src/native/scheduler-core/gleam.d.mts +2 -0
- package/src/native/scheduler-core/gleam.mjs +1 -0
- package/src/native/scheduler-core/scheduler_core.d.mts +76 -0
- package/src/native/scheduler-core/scheduler_core.mjs +252 -0
- package/src/native/scheduler-core.ts +124 -0
- package/src/native/sqlguard-wasm-bytes.ts +7 -0
- package/src/native/sqlguard.ts +107 -0
- package/src/native/strsim-wasm-bytes.ts +7 -0
- package/src/native/strsim.ts +98 -0
- package/src/native/textops-wasm-bytes.ts +7 -0
- package/src/native/textops.ts +83 -0
- package/src/native/warden.ts +287 -0
- package/src/plugins/github/index.ts +1 -1
- package/src/plugins/mempalace/index.ts +1 -1
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/chat-settings.ts +335 -85
- package/src/storage/cron-store.ts +266 -109
- package/src/storage/daily-log.ts +15 -3
- package/src/storage/db.ts +206 -0
- package/src/storage/goal-store.ts +186 -0
- package/src/storage/history.ts +137 -188
- package/src/storage/kv.ts +52 -0
- package/src/storage/legacy-import.ts +47 -0
- package/src/storage/media-index.ts +118 -61
- package/src/storage/repositories/chat-settings-repo.ts +60 -0
- package/src/storage/repositories/cron-repo.ts +128 -0
- package/src/storage/repositories/goals-repo.ts +122 -0
- package/src/storage/repositories/history-repo.ts +207 -0
- package/src/storage/repositories/kv-repo.ts +25 -0
- package/src/storage/repositories/media-index-repo.ts +147 -0
- package/src/storage/repositories/scripts-repo.ts +81 -0
- package/src/storage/repositories/sessions-repo.ts +129 -0
- package/src/storage/repositories/triggers-repo.ts +158 -0
- package/src/storage/repositories/turn-meta-repo.ts +32 -0
- package/src/storage/scheduled-store.ts +84 -0
- package/src/storage/script-store.ts +198 -0
- package/src/storage/sessions.ts +227 -102
- package/src/storage/skill-store.ts +325 -0
- package/src/storage/sql/README.md +18 -0
- package/src/storage/sql/chat-settings.sql +11 -0
- package/src/storage/sql/cron.sql +40 -0
- package/src/storage/sql/db.sql +11 -0
- package/src/storage/sql/embed.ts +118 -0
- package/src/storage/sql/goals.sql +41 -0
- package/src/storage/sql/history.sql +83 -0
- package/src/storage/sql/kv.sql +11 -0
- package/src/storage/sql/media-index.sql +45 -0
- package/src/storage/sql/schema.sql +233 -0
- package/src/storage/sql/scripts.sql +28 -0
- package/src/storage/sql/sessions.sql +22 -0
- package/src/storage/sql/statements.generated.ts +500 -0
- package/src/storage/sql/triggers.sql +69 -0
- package/src/storage/sql/turn-meta.sql +24 -0
- package/src/storage/sticker-store.ts +141 -0
- package/src/storage/trigger-store.ts +104 -126
- package/src/storage/turn-meta.ts +118 -0
- package/src/util/chat-id.ts +30 -0
- package/src/util/config.ts +313 -195
- package/src/util/fs-path.ts +32 -0
- package/src/util/log.ts +53 -31
- package/src/util/mcp-launcher.ts +351 -33
- package/src/util/paths.ts +45 -17
- package/src/util/tail-file.ts +24 -0
- package/src/util/workspace.ts +161 -10
- package/tsconfig.json +1 -0
- package/src/backend/claude-sdk/models.ts +0 -501
- package/src/backend/codex/handler.ts +0 -595
- package/src/backend/kilo/handler.ts +0 -707
- package/src/backend/kilo/models.ts +0 -762
- package/src/backend/openai-agents/handler.ts +0 -539
- package/src/backend/openai-agents/mcp.ts +0 -139
- package/src/backend/opencode/handler.ts +0 -663
- package/src/backend/opencode/models.ts +0 -741
- package/src/core/cron.ts +0 -184
- package/src/core/dispatcher.ts +0 -144
- package/src/core/gateway-actions.ts +0 -603
- package/src/core/heartbeat.ts +0 -575
- package/src/core/plugin.ts +0 -776
- package/src/core/triggers.ts +0 -640
- package/src/frontend/discord/actions.ts +0 -729
- package/src/frontend/discord/commands.ts +0 -1036
- package/src/frontend/discord/handlers.ts +0 -798
- package/src/frontend/telegram/actions.ts +0 -731
- package/src/frontend/telegram/callbacks.ts +0 -419
- package/src/frontend/telegram/commands.ts +0 -642
- package/src/frontend/telegram/handlers.ts +0 -1352
- package/src/util/mcp-launcher.mjs +0 -140
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool lifecycle + role bindings + observability: init/teardown, role
|
|
3
|
+
* accessors, availability queries, the `/model` snapshot, and change
|
|
4
|
+
* listeners.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Backend } from "../../agent-runtime/capabilities.js";
|
|
8
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
9
|
+
import {
|
|
10
|
+
listBackends,
|
|
11
|
+
type BackendInitContext,
|
|
12
|
+
} from "../../agent-runtime/backend-registry.js";
|
|
13
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
14
|
+
import { roleHolder } from "./holders.js";
|
|
15
|
+
import {
|
|
16
|
+
pool,
|
|
17
|
+
bindings,
|
|
18
|
+
listeners,
|
|
19
|
+
ctx,
|
|
20
|
+
ensurePoolEntry,
|
|
21
|
+
releaseHolderFromEntry,
|
|
22
|
+
resolveRoleBackendId,
|
|
23
|
+
hasBackendPool,
|
|
24
|
+
} from "./state.js";
|
|
25
|
+
import {
|
|
26
|
+
ALL_ROLES,
|
|
27
|
+
type BackendChangeListener,
|
|
28
|
+
type BackendHolder,
|
|
29
|
+
type BackendRole,
|
|
30
|
+
type PoolSnapshot,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
|
|
33
|
+
export { hasBackendPool };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialise the backend pool with bindings for every role.
|
|
37
|
+
*
|
|
38
|
+
* Reads `config.backend` (chat), `config.heartbeatBackend` (heartbeat,
|
|
39
|
+
* falls back to chat), `config.dreamBackend` (dream, falls back to
|
|
40
|
+
* chat). Identical ids share a pool instance — Talon does NOT spin up
|
|
41
|
+
* two Claude SDK runtimes just because chat and heartbeat both want
|
|
42
|
+
* Claude.
|
|
43
|
+
*
|
|
44
|
+
* If any role's init fails, previously-initialised roles are torn
|
|
45
|
+
* down before the throw bubbles up — so a partial-init state never
|
|
46
|
+
* leaks past bootstrap.
|
|
47
|
+
*/
|
|
48
|
+
export async function initBackendPool(
|
|
49
|
+
config: TalonConfig,
|
|
50
|
+
initContext: BackendInitContext,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
ctx.initCtx = initContext;
|
|
53
|
+
ctx.poolConfig = config;
|
|
54
|
+
const initialisedHolders: BackendHolder[] = [];
|
|
55
|
+
try {
|
|
56
|
+
for (const role of ALL_ROLES) {
|
|
57
|
+
const holder = roleHolder(role);
|
|
58
|
+
const id = resolveRoleBackendId(role, config);
|
|
59
|
+
const entry = await ensurePoolEntry(id, config);
|
|
60
|
+
entry.holders.add(holder);
|
|
61
|
+
bindings.set(holder, id);
|
|
62
|
+
initialisedHolders.push(holder);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Roll back partial init — release every holder that succeeded so
|
|
66
|
+
// the pool isn't left holding orphaned instances.
|
|
67
|
+
for (const holder of initialisedHolders) {
|
|
68
|
+
const id = bindings.get(holder);
|
|
69
|
+
bindings.delete(holder);
|
|
70
|
+
if (id) {
|
|
71
|
+
const entry = pool.get(id);
|
|
72
|
+
if (entry) await releaseHolderFromEntry(entry, holder);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
ctx.initCtx = null;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
const summary = ALL_ROLES.map(
|
|
79
|
+
(r) => `${r}=${bindings.get(roleHolder(r))}`,
|
|
80
|
+
).join(" ");
|
|
81
|
+
log("backend-controller", `Pool initialised — ${summary}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Backend instance for a role. Throws if the role isn't bound. */
|
|
85
|
+
export function getBackendForRole(role: BackendRole): Backend {
|
|
86
|
+
const holder = roleHolder(role);
|
|
87
|
+
const id = bindings.get(holder);
|
|
88
|
+
if (!id) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Backend role "${role}" not bound — initialise the pool or rebind first`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const entry = pool.get(id);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Backend role "${role}" bound to "${id}" but pool entry missing`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return entry.backend;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Backend id bound to a role. Throws if the role isn't bound. */
|
|
103
|
+
export function getBackendIdForRole(role: BackendRole): string {
|
|
104
|
+
const id = bindings.get(roleHolder(role));
|
|
105
|
+
if (!id) {
|
|
106
|
+
throw new Error(`Backend role "${role}" not bound`);
|
|
107
|
+
}
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Display label for the role's bound backend. */
|
|
112
|
+
export function getBackendLabelForRole(role: BackendRole): string {
|
|
113
|
+
const id = getBackendIdForRole(role);
|
|
114
|
+
const entry = pool.get(id);
|
|
115
|
+
if (!entry) {
|
|
116
|
+
throw new Error(`Pool entry for "${id}" missing`);
|
|
117
|
+
}
|
|
118
|
+
return entry.label;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* All registered backends, optionally filtered by `config.enabledBackends`.
|
|
123
|
+
*
|
|
124
|
+
* When `config.enabledBackends` is set, only ids on the whitelist are
|
|
125
|
+
* returned (in the whitelist's order). When unset, every registered
|
|
126
|
+
* backend is returned, sorted by id for deterministic output.
|
|
127
|
+
*/
|
|
128
|
+
export function listAvailableBackends(
|
|
129
|
+
config?: TalonConfig,
|
|
130
|
+
): { id: string; label: string }[] {
|
|
131
|
+
const all = listBackends().map((b) => ({ id: b.id, label: b.label }));
|
|
132
|
+
const enabled = config?.enabledBackends;
|
|
133
|
+
if (!enabled || enabled.length === 0) return all;
|
|
134
|
+
const byId = new Map(all.map((b) => [b.id, b]));
|
|
135
|
+
return enabled
|
|
136
|
+
.map((id) => byId.get(id))
|
|
137
|
+
.filter((b): b is { id: string; label: string } => Boolean(b));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Whether a backend id is registered and currently exposed by config. */
|
|
141
|
+
export function isBackendAvailable(id: string, config?: TalonConfig): boolean {
|
|
142
|
+
return listAvailableBackends(config).some((b) => b.id === id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Available backends using the config captured at init — convenience for
|
|
147
|
+
* runtime readers (e.g. the `list_backends` tool) that don't have config
|
|
148
|
+
* plumbed through. Falls back to all registered backends pre-init.
|
|
149
|
+
*/
|
|
150
|
+
export function getAvailableBackends(): { id: string; label: string }[] {
|
|
151
|
+
return listAvailableBackends(ctx.poolConfig ?? undefined);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The live pooled `Backend` instance for an id, or `null` if it isn't currently
|
|
156
|
+
* pooled. Unlike `getBackendForRole`/`getBackendForChat` this never initialises
|
|
157
|
+
* on demand — it only returns backends already spun up (role backends + chat
|
|
158
|
+
* overrides).
|
|
159
|
+
*/
|
|
160
|
+
export function getPooledBackend(id: string): Backend | null {
|
|
161
|
+
return pool.get(id)?.backend ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Monotonic counter for synthetic transient holders. */
|
|
165
|
+
let transientHolderSeq = 0;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Acquire a backend instance transiently — boots it on demand if it isn't
|
|
169
|
+
* already pooled, pinned by a synthetic `read:N` holder so a concurrent rebind
|
|
170
|
+
* can't tear it down mid-use. The caller MUST call the returned `release()`,
|
|
171
|
+
* which drops the holder and cleans the instance up iff nothing else holds it
|
|
172
|
+
* (so an already-active backend is left running).
|
|
173
|
+
*
|
|
174
|
+
* Used by read-only callers that need a non-current backend's catalog (e.g.
|
|
175
|
+
* `list_models backend=<other>`) without permanently switching the chat to it.
|
|
176
|
+
*/
|
|
177
|
+
export async function acquireBackendInstance(
|
|
178
|
+
id: string,
|
|
179
|
+
): Promise<{ backend: Backend; release: () => Promise<void> }> {
|
|
180
|
+
if (!ctx.poolConfig) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Backend pool not initialised — call initBackendPool first",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const entry = await ensurePoolEntry(id, ctx.poolConfig);
|
|
186
|
+
const holder = `read:${++transientHolderSeq}` as BackendHolder;
|
|
187
|
+
entry.holders.add(holder);
|
|
188
|
+
let released = false;
|
|
189
|
+
return {
|
|
190
|
+
backend: entry.backend,
|
|
191
|
+
release: async () => {
|
|
192
|
+
if (released) return;
|
|
193
|
+
released = true;
|
|
194
|
+
const e = pool.get(id);
|
|
195
|
+
if (e) await releaseHolderFromEntry(e, holder);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate a persisted model id against the backend that will serve
|
|
202
|
+
* it. Resolves through the catalog's `resolveModelInfo` — exact
|
|
203
|
+
* matches with `selectable !== false` are kept; everything else
|
|
204
|
+
* falls back. Backends without a catalog return `true` (we can't
|
|
205
|
+
* prove the value is stale, so we trust the chat-settings store).
|
|
206
|
+
*/
|
|
207
|
+
export async function isModelValidForBackend(
|
|
208
|
+
backend: Backend,
|
|
209
|
+
model: string,
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
const catalog = backend.models;
|
|
212
|
+
if (!catalog) return true;
|
|
213
|
+
const resolution = await catalog.resolveModelInfo(model);
|
|
214
|
+
return resolution.kind === "exact" && resolution.model.selectable !== false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Snapshot of the live pool + bindings for `/model` rendering. */
|
|
218
|
+
export function getPoolSnapshot(): PoolSnapshot {
|
|
219
|
+
const instances = [...pool.values()]
|
|
220
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
221
|
+
.map((entry) => {
|
|
222
|
+
const holdersList = [...entry.holders].sort();
|
|
223
|
+
const roles = holdersList
|
|
224
|
+
.filter((h) => h.startsWith("role:"))
|
|
225
|
+
.map((h) => h.slice(5) as BackendRole)
|
|
226
|
+
.sort();
|
|
227
|
+
const chats = holdersList
|
|
228
|
+
.filter((h) => h.startsWith("chat:"))
|
|
229
|
+
.map((h) => h.slice(5))
|
|
230
|
+
.sort();
|
|
231
|
+
return {
|
|
232
|
+
id: entry.id,
|
|
233
|
+
label: entry.label,
|
|
234
|
+
holders: holdersList,
|
|
235
|
+
roles,
|
|
236
|
+
chats,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
const bindingsObj: Partial<Record<BackendRole, string>> = {};
|
|
240
|
+
for (const role of ALL_ROLES) {
|
|
241
|
+
const id = bindings.get(roleHolder(role));
|
|
242
|
+
if (id) bindingsObj[role] = id;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
instances,
|
|
246
|
+
bindings: bindingsObj as Record<BackendRole, string>,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register a backend-change listener.
|
|
252
|
+
*
|
|
253
|
+
* Listener receives the holder that changed (`role:chat`, `chat:1234`,
|
|
254
|
+
* etc.), the new backend, and id/label. Fires AFTER the rebind is
|
|
255
|
+
* committed and the new entry is in the pool, but BEFORE the previous
|
|
256
|
+
* entry's cleanup runs (when applicable).
|
|
257
|
+
*/
|
|
258
|
+
export function onBackendChange(listener: BackendChangeListener): () => void {
|
|
259
|
+
listeners.add(listener);
|
|
260
|
+
return () => listeners.delete(listener);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Tear down the entire pool. Idempotent. */
|
|
264
|
+
export async function cleanupBackendPool(): Promise<void> {
|
|
265
|
+
const entries = [...pool.values()];
|
|
266
|
+
pool.clear();
|
|
267
|
+
bindings.clear();
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
if (entry.cleanup) {
|
|
270
|
+
try {
|
|
271
|
+
await entry.cleanup();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
logWarn(
|
|
274
|
+
"backend-controller",
|
|
275
|
+
`Pool shutdown cleanup of ${entry.label} failed: ${
|
|
276
|
+
err instanceof Error ? err.message : String(err)
|
|
277
|
+
}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
ctx.initCtx = null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Test-only state reset. */
|
|
286
|
+
export function resetBackendPoolForTest(): void {
|
|
287
|
+
pool.clear();
|
|
288
|
+
bindings.clear();
|
|
289
|
+
ctx.initCtx = null;
|
|
290
|
+
ctx.poolConfig = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Test-only: drop all listeners. */
|
|
294
|
+
export function clearBackendChangeListenersForTest(): void {
|
|
295
|
+
listeners.clear();
|
|
296
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebind / release holders, plus the per-chat accessors that resolve a chat
|
|
3
|
+
* to its override (or the chat-role default).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Backend } from "../../agent-runtime/capabilities.js";
|
|
7
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
8
|
+
import {
|
|
9
|
+
getBackend,
|
|
10
|
+
listBackends,
|
|
11
|
+
} from "../../agent-runtime/backend-registry.js";
|
|
12
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
13
|
+
import { chatHolder, roleHolder } from "./holders.js";
|
|
14
|
+
import {
|
|
15
|
+
pool,
|
|
16
|
+
bindings,
|
|
17
|
+
ctx,
|
|
18
|
+
ensurePoolEntry,
|
|
19
|
+
releaseHolderFromEntry,
|
|
20
|
+
notifyListeners,
|
|
21
|
+
hasBackendPool,
|
|
22
|
+
type PoolEntry,
|
|
23
|
+
} from "./state.js";
|
|
24
|
+
import { getBackendForRole, getBackendIdForRole } from "./pool.js";
|
|
25
|
+
import type { BackendHolder, BackendRole, RebindResult } from "./types.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Rebind any holder to a different backend id.
|
|
29
|
+
*
|
|
30
|
+
* Pool semantics:
|
|
31
|
+
* - If the target id is already pooled (another holder uses it),
|
|
32
|
+
* the existing instance is reused — no double init.
|
|
33
|
+
* - The previous binding's instance loses a refcount; cleanup fires
|
|
34
|
+
* only if no other holder still references it.
|
|
35
|
+
* - Failed init leaves the previous binding in place.
|
|
36
|
+
*
|
|
37
|
+
* Use `rebindRole` / `rebindChat` for typed wrappers.
|
|
38
|
+
*/
|
|
39
|
+
export async function rebindHolder(
|
|
40
|
+
holder: BackendHolder,
|
|
41
|
+
newId: string,
|
|
42
|
+
config: TalonConfig,
|
|
43
|
+
): Promise<RebindResult> {
|
|
44
|
+
if (!ctx.initCtx) {
|
|
45
|
+
return { ok: false, error: "Backend pool not initialised" };
|
|
46
|
+
}
|
|
47
|
+
const currentId = bindings.get(holder);
|
|
48
|
+
if (currentId === newId) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: `Holder "${holder}" already bound to "${newId}"`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const factory = getBackend(newId);
|
|
56
|
+
if (!factory) {
|
|
57
|
+
const known = listBackends()
|
|
58
|
+
.map((b) => `"${b.id}"`)
|
|
59
|
+
.join(", ");
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `Unknown backend "${newId}" — known: ${known}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const newReused = pool.has(newId);
|
|
67
|
+
let newEntry: PoolEntry;
|
|
68
|
+
try {
|
|
69
|
+
newEntry = await ensurePoolEntry(newId, config);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
logWarn(
|
|
73
|
+
"backend-controller",
|
|
74
|
+
`rebindHolder(${holder}, ${newId}) init failed: ${msg} — binding unchanged`,
|
|
75
|
+
);
|
|
76
|
+
return { ok: false, error: `Failed to init ${factory.label}: ${msg}` };
|
|
77
|
+
}
|
|
78
|
+
newEntry.holders.add(holder);
|
|
79
|
+
bindings.set(holder, newId);
|
|
80
|
+
|
|
81
|
+
let previousReused = false;
|
|
82
|
+
if (currentId) {
|
|
83
|
+
const oldEntry = pool.get(currentId);
|
|
84
|
+
if (oldEntry) {
|
|
85
|
+
previousReused = oldEntry.holders.size > 1;
|
|
86
|
+
await releaseHolderFromEntry(oldEntry, holder);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log(
|
|
91
|
+
"backend-controller",
|
|
92
|
+
`Rebound ${holder}: ${currentId ?? "(unbound)"} → ${newId}` +
|
|
93
|
+
(newReused ? " (reused pool instance)" : ""),
|
|
94
|
+
);
|
|
95
|
+
notifyListeners(holder, newEntry.backend, {
|
|
96
|
+
id: newId,
|
|
97
|
+
label: newEntry.label,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
from: currentId,
|
|
103
|
+
to: newId,
|
|
104
|
+
previousReused,
|
|
105
|
+
newReused,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Release a holder. If the entry it was holding has no other holders,
|
|
111
|
+
* the entry is cleaned up.
|
|
112
|
+
*
|
|
113
|
+
* No-op when the holder isn't bound — useful for "clear my override"
|
|
114
|
+
* flows that don't want to care whether the override was ever set.
|
|
115
|
+
*/
|
|
116
|
+
export async function releaseHolder(holder: BackendHolder): Promise<void> {
|
|
117
|
+
const id = bindings.get(holder);
|
|
118
|
+
if (!id) return;
|
|
119
|
+
bindings.delete(holder);
|
|
120
|
+
const entry = pool.get(id);
|
|
121
|
+
if (entry) await releaseHolderFromEntry(entry, holder);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Rebind a role. Convenience wrapper over `rebindHolder`. */
|
|
125
|
+
export function rebindRole(
|
|
126
|
+
role: BackendRole,
|
|
127
|
+
newId: string,
|
|
128
|
+
config: TalonConfig,
|
|
129
|
+
): Promise<RebindResult> {
|
|
130
|
+
return rebindHolder(roleHolder(role), newId, config);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rebind a per-chat override.
|
|
135
|
+
*
|
|
136
|
+
* Pool refcounts the new backend in addition to whatever the role
|
|
137
|
+
* holders already pin. Releasing the chat (`releaseChat`) reverts the
|
|
138
|
+
* chat to whichever backend the chat-role is bound to.
|
|
139
|
+
*/
|
|
140
|
+
export function rebindChat(
|
|
141
|
+
chatId: string,
|
|
142
|
+
newId: string,
|
|
143
|
+
config: TalonConfig,
|
|
144
|
+
): Promise<RebindResult> {
|
|
145
|
+
return rebindHolder(chatHolder(chatId), newId, config);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Release a per-chat override — the chat reverts to the global default. */
|
|
149
|
+
export function releaseChat(chatId: string): Promise<void> {
|
|
150
|
+
return releaseHolder(chatHolder(chatId));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the backend a chat should use right now.
|
|
155
|
+
*
|
|
156
|
+
* If the chat has an override pooled, return that backend; otherwise
|
|
157
|
+
* fall back to the global chat-role backend.
|
|
158
|
+
*/
|
|
159
|
+
export function getBackendForChat(chatId: string): Backend {
|
|
160
|
+
const overrideId = bindings.get(chatHolder(chatId));
|
|
161
|
+
if (overrideId) {
|
|
162
|
+
const entry = pool.get(overrideId);
|
|
163
|
+
if (entry) return entry.backend;
|
|
164
|
+
}
|
|
165
|
+
return getBackendForRole("chat");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Backend id the chat would resolve to (override → role default). */
|
|
169
|
+
export function getBackendIdForChat(chatId: string): string {
|
|
170
|
+
return bindings.get(chatHolder(chatId)) ?? getBackendIdForRole("chat");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Whether this chat has an override pinning a non-default backend. */
|
|
174
|
+
export function hasChatBackendOverride(chatId: string): boolean {
|
|
175
|
+
return bindings.has(chatHolder(chatId));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Safe variant of `getBackendForChat` that never throws — returns
|
|
180
|
+
* the per-chat / role:chat backend when the pool is initialised, or
|
|
181
|
+
* `fallback` (typically `gateway?.backend`) when it isn't.
|
|
182
|
+
*
|
|
183
|
+
* The pool is always initialised in production at bootstrap; this
|
|
184
|
+
* helper exists so unit tests and legacy code paths that wire the
|
|
185
|
+
* frontend before the pool can degrade gracefully instead of
|
|
186
|
+
* throwing on every `/model` command. Frontends should call this
|
|
187
|
+
* everywhere they previously read `gateway?.backend` for /model,
|
|
188
|
+
* /status, and warmSession — otherwise per-chat backend overrides
|
|
189
|
+
* silently lose effect.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveChatBackend(
|
|
192
|
+
chatId: string,
|
|
193
|
+
fallback?: Backend | null,
|
|
194
|
+
): Backend | null {
|
|
195
|
+
if (hasBackendPool()) {
|
|
196
|
+
try {
|
|
197
|
+
return getBackendForChat(chatId);
|
|
198
|
+
} catch {
|
|
199
|
+
// Pool initialised but the chat-role binding is somehow missing —
|
|
200
|
+
// shouldn't happen in practice, but degrade to fallback rather
|
|
201
|
+
// than crash a /model render.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return fallback ?? null;
|
|
205
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-pool shared state + internal helpers.
|
|
3
|
+
*
|
|
4
|
+
* All pool modules import the SAME Map/Set instances and the SAME `ctx`
|
|
5
|
+
* holder from here, so refcounting and rebinds stay coherent across files.
|
|
6
|
+
* `initCtx`/`poolConfig` are reassignable, so they live on the mutable `ctx`
|
|
7
|
+
* object rather than as module-level `let`s (which can't be reassigned from
|
|
8
|
+
* another module).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Backend } from "../../agent-runtime/capabilities.js";
|
|
12
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
13
|
+
import {
|
|
14
|
+
getBackend,
|
|
15
|
+
listBackends,
|
|
16
|
+
type BackendInitContext,
|
|
17
|
+
} from "../../agent-runtime/backend-registry.js";
|
|
18
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
19
|
+
import { roleHolder } from "./holders.js";
|
|
20
|
+
import type { BackendChangeListener, BackendHolder } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export interface PoolEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
backend: Backend;
|
|
26
|
+
cleanup?: () => Promise<void> | void;
|
|
27
|
+
holders: Set<BackendHolder>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** id → entry. */
|
|
31
|
+
export const pool = new Map<string, PoolEntry>();
|
|
32
|
+
/** holder → backend id (only present for holders currently bound). */
|
|
33
|
+
export const bindings = new Map<BackendHolder, string>();
|
|
34
|
+
|
|
35
|
+
export const listeners = new Set<BackendChangeListener>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reassignable init context, captured at first init so subsequent calls don't
|
|
39
|
+
* need ctx/config plumbed through. On a holder object so other modules can
|
|
40
|
+
* reassign it.
|
|
41
|
+
*/
|
|
42
|
+
export const ctx: {
|
|
43
|
+
initCtx: BackendInitContext | null;
|
|
44
|
+
poolConfig: TalonConfig | null;
|
|
45
|
+
} = {
|
|
46
|
+
initCtx: null,
|
|
47
|
+
poolConfig: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** Look up or initialise a pool entry for a backend id. */
|
|
53
|
+
export async function ensurePoolEntry(
|
|
54
|
+
id: string,
|
|
55
|
+
config: TalonConfig,
|
|
56
|
+
): Promise<PoolEntry> {
|
|
57
|
+
const existing = pool.get(id);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
|
|
60
|
+
const factory = getBackend(id);
|
|
61
|
+
if (!factory) {
|
|
62
|
+
const known = listBackends()
|
|
63
|
+
.map((b) => `"${b.id}"`)
|
|
64
|
+
.join(", ");
|
|
65
|
+
throw new Error(`Unknown backend "${id}" — known: ${known}`);
|
|
66
|
+
}
|
|
67
|
+
if (!ctx.initCtx) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"Backend pool not initialised — call initBackendPool first",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const instance = await factory.init(config, ctx.initCtx);
|
|
73
|
+
const entry: PoolEntry = {
|
|
74
|
+
id: factory.id,
|
|
75
|
+
label: factory.label,
|
|
76
|
+
backend: instance.backend,
|
|
77
|
+
cleanup: instance.cleanup,
|
|
78
|
+
holders: new Set(),
|
|
79
|
+
};
|
|
80
|
+
pool.set(factory.id, entry);
|
|
81
|
+
log("backend-controller", `Pool init: ${factory.label} (${factory.id})`);
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Drop a holder from an entry; clean up the entry if its refcount hits zero. */
|
|
86
|
+
export async function releaseHolderFromEntry(
|
|
87
|
+
entry: PoolEntry,
|
|
88
|
+
holder: BackendHolder,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
entry.holders.delete(holder);
|
|
91
|
+
if (entry.holders.size > 0) {
|
|
92
|
+
log(
|
|
93
|
+
"backend-controller",
|
|
94
|
+
`Released ${holder} from ${entry.label} (still held by: ${[...entry.holders].join(", ")})`,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
pool.delete(entry.id);
|
|
99
|
+
if (entry.cleanup) {
|
|
100
|
+
try {
|
|
101
|
+
await entry.cleanup();
|
|
102
|
+
log("backend-controller", `Pool cleanup: ${entry.label} (${entry.id})`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logWarn(
|
|
105
|
+
"backend-controller",
|
|
106
|
+
`Cleanup of ${entry.label} failed: ${
|
|
107
|
+
err instanceof Error ? err.message : String(err)
|
|
108
|
+
}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
log("backend-controller", `Pool cleanup: ${entry.label} (no cleanup hook)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function notifyListeners(
|
|
117
|
+
holder: BackendHolder,
|
|
118
|
+
backend: Backend,
|
|
119
|
+
ctxInfo: { id: string; label: string },
|
|
120
|
+
): void {
|
|
121
|
+
for (const listener of listeners) {
|
|
122
|
+
try {
|
|
123
|
+
listener(holder, backend, ctxInfo);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logWarn(
|
|
126
|
+
"backend-controller",
|
|
127
|
+
`Backend-change listener threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolveRoleBackendId(
|
|
134
|
+
role: "chat" | "heartbeat" | "dream",
|
|
135
|
+
config: TalonConfig,
|
|
136
|
+
): string {
|
|
137
|
+
switch (role) {
|
|
138
|
+
case "chat":
|
|
139
|
+
return config.backend;
|
|
140
|
+
case "heartbeat":
|
|
141
|
+
return config.heartbeatBackend ?? config.backend;
|
|
142
|
+
case "dream":
|
|
143
|
+
return config.dreamBackend ?? config.backend;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Has the pool been initialised? */
|
|
148
|
+
export function hasBackendPool(): boolean {
|
|
149
|
+
return bindings.has(roleHolder("chat"));
|
|
150
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the backend controller / pool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Backend } from "../../agent-runtime/capabilities.js";
|
|
6
|
+
|
|
7
|
+
/** Roles that map to global holders. */
|
|
8
|
+
export type BackendRole = "chat" | "heartbeat" | "dream";
|
|
9
|
+
|
|
10
|
+
export const ALL_ROLES: readonly BackendRole[] = ["chat", "heartbeat", "dream"];
|
|
11
|
+
|
|
12
|
+
/** Holder identifier — opaque string. Use `roleHolder()` / `chatHolder()`. */
|
|
13
|
+
export type BackendHolder = string;
|
|
14
|
+
|
|
15
|
+
export interface RebindResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** Backend id the holder was bound to before the rebind (omitted when none). */
|
|
18
|
+
from?: string;
|
|
19
|
+
/** Backend id the holder is now bound to. */
|
|
20
|
+
to?: string;
|
|
21
|
+
/** Whether the previous instance stays alive (still bound elsewhere). */
|
|
22
|
+
previousReused?: boolean;
|
|
23
|
+
/** Whether the new binding reuses an existing pool instance. */
|
|
24
|
+
newReused?: boolean;
|
|
25
|
+
/** Human-readable error on failure. */
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Snapshot of the pool for observability / `/model` rendering. */
|
|
30
|
+
export interface PoolSnapshot {
|
|
31
|
+
/** Active pool entries, sorted by id, with the holders pinning them. */
|
|
32
|
+
instances: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
/** Free-form holder strings — useful for diagnostics. */
|
|
36
|
+
holders: BackendHolder[];
|
|
37
|
+
/** Role-only subset, kept sorted, for the common case. */
|
|
38
|
+
roles: BackendRole[];
|
|
39
|
+
/** Per-chat overrides (chat ids only). */
|
|
40
|
+
chats: string[];
|
|
41
|
+
}>;
|
|
42
|
+
/** Current role bindings (always all three roles once initialised). */
|
|
43
|
+
bindings: Record<BackendRole, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Notified after a successful rebind. */
|
|
47
|
+
export type BackendChangeListener = (
|
|
48
|
+
holder: BackendHolder,
|
|
49
|
+
backend: Backend,
|
|
50
|
+
ctx: { id: string; label: string },
|
|
51
|
+
) => void;
|