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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery-contract text — single source of truth for "how a reply
|
|
3
|
+
* reaches the user" documentation.
|
|
4
|
+
*
|
|
5
|
+
* The response-flow contract is a property of the BACKEND (claude-sdk
|
|
6
|
+
* and openai-agents enforce strict tool-only delivery; codex / kilo /
|
|
7
|
+
* opencode accept plain assistant text), while the delivery TOOL NAMES
|
|
8
|
+
* are a property of the FRONTEND (telegram/discord ship `end_turn` /
|
|
9
|
+
* `send` / `react`; native ships `end_turn` / `send_message` /
|
|
10
|
+
* `react`; teams ships `end_turn` / `send_message`). Before
|
|
11
|
+
* this module, the strict contract was hardcoded into the frontend
|
|
12
|
+
* prompt files (prompts/telegram.md etc.) — buried mid-prompt where
|
|
13
|
+
* models routinely missed it on the first turn of a session, and
|
|
14
|
+
* contradicted by the text-mode backends' own suffixes.
|
|
15
|
+
*
|
|
16
|
+
* Backends now build their suffix from here and append it via
|
|
17
|
+
* `prepareSystemPrompt({ backendSuffix })`, which places the contract
|
|
18
|
+
* at the END of the static prompt — the highest-salience position —
|
|
19
|
+
* without touching the prompt-cache-friendly static/dynamic split.
|
|
20
|
+
*
|
|
21
|
+
* Also here:
|
|
22
|
+
* - `buildFlowViolationReminder` — frontend-aware [FLOW VIOLATION]
|
|
23
|
+
* re-prompt text (the shared constant used to hardcode telegram's
|
|
24
|
+
* tool names, which made the reminder wrong on Teams).
|
|
25
|
+
* - `buildFirstTurnReminder` — a one-line nudge appended to the
|
|
26
|
+
* formatted user prompt on the FIRST turn of a session. First
|
|
27
|
+
* turns are where flow violations cluster: the model hasn't seen
|
|
28
|
+
* the contract in action yet. One short line on turn 0 is far
|
|
29
|
+
* cheaper than the 2x-token violation retry it prevents.
|
|
30
|
+
*
|
|
31
|
+
* The contract bodies live in `prompts/system/contract-*.md`
|
|
32
|
+
* (package-owned templates, see core/prompt/templates.ts). The
|
|
33
|
+
* one-line runtime reminders below stay in code: they are
|
|
34
|
+
* logic-adjacent strings, like log messages.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { loadSystemTemplate } from "../../core/prompt/templates.js";
|
|
38
|
+
|
|
39
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How a backend delivers replies to the user.
|
|
43
|
+
*
|
|
44
|
+
* - `tool-only` — output stream is private scratchpad; replies MUST
|
|
45
|
+
* go through a delivery tool (claude-sdk, openai-agents).
|
|
46
|
+
* - `text-or-tools` — plain assistant text is delivered as the
|
|
47
|
+
* reply; delivery tools add rich content / targeting (codex, kilo).
|
|
48
|
+
* - `text-preferred` — plain text is the normal route; tools only
|
|
49
|
+
* for genuine side effects (opencode).
|
|
50
|
+
*/
|
|
51
|
+
export type DeliveryMode = "tool-only" | "text-or-tools" | "text-preferred";
|
|
52
|
+
|
|
53
|
+
/** Frontend-specific delivery tool names. */
|
|
54
|
+
export type DeliveryToolNames = {
|
|
55
|
+
/** Turn-closing final-reply tool. */
|
|
56
|
+
endTurn: string;
|
|
57
|
+
/** Mid-turn / rich-content send tool. */
|
|
58
|
+
send: string;
|
|
59
|
+
/** Emoji-reaction tool, when the frontend supports reactions. */
|
|
60
|
+
react?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ── Frontend tool-name registry ─────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const FRONTEND_TOOLS: Record<string, DeliveryToolNames> = {
|
|
66
|
+
telegram: { endTurn: "end_turn", send: "send", react: "react" },
|
|
67
|
+
discord: { endTurn: "end_turn", send: "send", react: "react" },
|
|
68
|
+
teams: { endTurn: "end_turn", send: "send_message" },
|
|
69
|
+
native: { endTurn: "end_turn", send: "send_message", react: "react" },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const DEFAULT_TOOLS: DeliveryToolNames = {
|
|
73
|
+
endTurn: "end_turn",
|
|
74
|
+
send: "send",
|
|
75
|
+
react: "react",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delivery tool names for a frontend. Unknown frontends (and
|
|
80
|
+
* `terminal`, which has no outbound messaging surface) fall back to
|
|
81
|
+
* the telegram-shaped defaults — harmless, since backends only enforce
|
|
82
|
+
* the contract when a frontend MCP server is actually wired.
|
|
83
|
+
*/
|
|
84
|
+
export function deliveryToolsForFrontend(frontend: string): DeliveryToolNames {
|
|
85
|
+
return FRONTEND_TOOLS[frontend] ?? DEFAULT_TOOLS;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register (or override) the delivery tool names for a frontend.
|
|
90
|
+
* Extension seam: a new frontend declares its tool names here at
|
|
91
|
+
* registration time and every backend's contract, flow-violation
|
|
92
|
+
* reminder, and first-turn nudge pick them up — no edits to this
|
|
93
|
+
* module or to any backend.
|
|
94
|
+
*/
|
|
95
|
+
export function registerFrontendDeliveryTools(
|
|
96
|
+
frontend: string,
|
|
97
|
+
tools: DeliveryToolNames,
|
|
98
|
+
): void {
|
|
99
|
+
FRONTEND_TOOLS[frontend] = tools;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Contract suffix ─────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the response-flow contract section a backend appends to the
|
|
106
|
+
* static system prompt. The prose lives in package-owned templates
|
|
107
|
+
* (`prompts/system/contract-*.md`) — kept deliberately tight: this is
|
|
108
|
+
* read on every session, so every sentence must earn its tokens. The
|
|
109
|
+
* per-tool details (parameter shapes, examples) live in the MCP tool
|
|
110
|
+
* descriptions, which the model also has in context.
|
|
111
|
+
*/
|
|
112
|
+
export function buildDeliveryContract(
|
|
113
|
+
mode: DeliveryMode,
|
|
114
|
+
frontend: string,
|
|
115
|
+
): string {
|
|
116
|
+
const t = deliveryToolsForFrontend(frontend);
|
|
117
|
+
return loadSystemTemplate(`contract-${mode}`, {
|
|
118
|
+
end_turn: t.endTurn,
|
|
119
|
+
send: t.send,
|
|
120
|
+
react: t.react,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Flow-violation reminder ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Frontend-aware [FLOW VIOLATION] reminder. Same shape as the legacy
|
|
128
|
+
* shared `FLOW_VIOLATION_REMINDER`, but with the frontend's actual
|
|
129
|
+
* tool names (the constant hardcoded \`send\`/\`react\`, which don't
|
|
130
|
+
* exist on Teams).
|
|
131
|
+
*/
|
|
132
|
+
export function buildFlowViolationReminder(frontend: string): string {
|
|
133
|
+
const t = deliveryToolsForFrontend(frontend);
|
|
134
|
+
const reactPart = t.react
|
|
135
|
+
? `, or \`${t.react}(emoji=...)\` for an emoji acknowledgement`
|
|
136
|
+
: "";
|
|
137
|
+
return (
|
|
138
|
+
"[FLOW VIOLATION] Your previous turn ended without a delivery tool, so the " +
|
|
139
|
+
"user saw nothing. Replies must go through a tool call: " +
|
|
140
|
+
`\`${t.endTurn}(text=...)\` for a final reply, ` +
|
|
141
|
+
`\`${t.endTurn}()\` (no args) to close silently, ` +
|
|
142
|
+
`\`${t.send}(...)\` for mid-turn rich content${reactPart}. ` +
|
|
143
|
+
"Tool calls alone do not close the turn. Retry now using the correct tool call."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── First-turn nudge ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One-line reminder appended to the formatted user prompt on the first
|
|
151
|
+
* turn of a session (strict backends only). Lives in the user message,
|
|
152
|
+
* not the system prompt, so it costs nothing on later turns and never
|
|
153
|
+
* perturbs the cached static prefix.
|
|
154
|
+
*/
|
|
155
|
+
export function buildFirstTurnReminder(frontend: string): string {
|
|
156
|
+
const t = deliveryToolsForFrontend(frontend);
|
|
157
|
+
return (
|
|
158
|
+
`[New session — reminder: your prose output is never shown to the user. ` +
|
|
159
|
+
`Deliver your reply via ${t.endTurn}(text=...), or ${t.endTurn}() to stay silent.]`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -35,6 +35,32 @@ import { incrementCounter } from "../../util/metrics.js";
|
|
|
35
35
|
/** Route the delivery decision selected. */
|
|
36
36
|
export type DeliveryRoute = "tool" | "text-part" | "synthetic-error" | "empty";
|
|
37
37
|
|
|
38
|
+
export class TextBlockDeliveryError extends Error {
|
|
39
|
+
readonly route: DeliveryRoute;
|
|
40
|
+
readonly chars: number;
|
|
41
|
+
|
|
42
|
+
constructor(route: DeliveryRoute, chars: number, cause: unknown) {
|
|
43
|
+
super(
|
|
44
|
+
`text-block delivery failed (${route}, ${chars} chars): ${errMsg(cause)}`,
|
|
45
|
+
);
|
|
46
|
+
this.name = "TextBlockDeliveryError";
|
|
47
|
+
this.route = route;
|
|
48
|
+
this.chars = chars;
|
|
49
|
+
this.cause = cause;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildDeliveryFailureReminder(error: TextBlockDeliveryError) {
|
|
54
|
+
return (
|
|
55
|
+
"[DELIVERY FAILURE] Your previous reply was not shown to the user because " +
|
|
56
|
+
`Talon failed while delivering a ${error.route} response (${error.chars} chars): ${errMsg(error.cause)}. ` +
|
|
57
|
+
"Retry now using delivery tools. If the answer is long, split it across " +
|
|
58
|
+
'multiple `send(type="text", text=...)` calls and then call `end_turn()`; ' +
|
|
59
|
+
"or shorten the answer and call `end_turn(text=...)`. Do not write the final " +
|
|
60
|
+
"answer as plain assistant text."
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
/** Outcome of `routeDelivery` — `route` + char count for the log line. */
|
|
39
65
|
export interface DeliveryDecision {
|
|
40
66
|
route: DeliveryRoute;
|
|
@@ -58,6 +84,11 @@ export interface RouteDeliveryInputs {
|
|
|
58
84
|
* `kilo.synthetic_error`). Defaults to a lower-cased backendLabel.
|
|
59
85
|
*/
|
|
60
86
|
metricNamespace?: string;
|
|
87
|
+
/**
|
|
88
|
+
* When true, failed `onTextBlock` delivery is thrown back to the backend so
|
|
89
|
+
* it can re-prompt the model. Default preserves older non-fatal behaviour.
|
|
90
|
+
*/
|
|
91
|
+
propagateDeliveryFailure?: boolean;
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
/**
|
|
@@ -78,6 +109,7 @@ export async function routeDelivery(
|
|
|
78
109
|
responseText,
|
|
79
110
|
onTextBlock,
|
|
80
111
|
metricNamespace = backendLabel.toLowerCase(),
|
|
112
|
+
propagateDeliveryFailure = false,
|
|
81
113
|
} = inputs;
|
|
82
114
|
|
|
83
115
|
// Route 1 — bridge delivery already happened.
|
|
@@ -116,6 +148,13 @@ export async function routeDelivery(
|
|
|
116
148
|
"agent",
|
|
117
149
|
`[${chatId}] onTextBlock (synthetic-error) failed: ${errMsg(err)}`,
|
|
118
150
|
);
|
|
151
|
+
if (propagateDeliveryFailure) {
|
|
152
|
+
throw new TextBlockDeliveryError(
|
|
153
|
+
"synthetic-error",
|
|
154
|
+
state.syntheticError.length,
|
|
155
|
+
err,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
119
158
|
}
|
|
120
159
|
}
|
|
121
160
|
return {
|
|
@@ -131,6 +170,13 @@ export async function routeDelivery(
|
|
|
131
170
|
await onTextBlock(responseText);
|
|
132
171
|
} catch (err) {
|
|
133
172
|
logWarn("agent", `[${chatId}] onTextBlock failed: ${errMsg(err)}`);
|
|
173
|
+
if (propagateDeliveryFailure) {
|
|
174
|
+
throw new TextBlockDeliveryError(
|
|
175
|
+
"text-part",
|
|
176
|
+
responseText.length,
|
|
177
|
+
err,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
134
180
|
}
|
|
135
181
|
}
|
|
136
182
|
return { route: "text-part", chars: responseText.length };
|
|
@@ -155,6 +201,9 @@ export async function routeDelivery(
|
|
|
155
201
|
"agent",
|
|
156
202
|
`[${chatId}] onTextBlock (empty-turn error) failed: ${errMsg(err)}`,
|
|
157
203
|
);
|
|
204
|
+
if (propagateDeliveryFailure) {
|
|
205
|
+
throw new TextBlockDeliveryError("empty", 0, err);
|
|
206
|
+
}
|
|
158
207
|
}
|
|
159
208
|
}
|
|
160
209
|
return { route: "empty", chars: 0 };
|
|
@@ -29,7 +29,9 @@ export const FLOW_VIOLATION_REMINDER =
|
|
|
29
29
|
"`end_turn()` (no args) to close silently, " +
|
|
30
30
|
"`send(...)` for mid-turn rich content (photos, polls, etc.), or " +
|
|
31
31
|
"`react(emoji=...)` for an emoji acknowledgement. " +
|
|
32
|
-
"Retry now using the correct tool call.";
|
|
32
|
+
"Tool calls alone do not close the turn. Retry now using the correct tool call.";
|
|
33
|
+
|
|
34
|
+
export const FLOW_VIOLATION_MAX_RETRIES = 3;
|
|
33
35
|
|
|
34
36
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
35
37
|
|
|
@@ -41,8 +43,21 @@ export type FlowViolationInputs = {
|
|
|
41
43
|
turnTerminated: boolean;
|
|
42
44
|
/** Already-delivered text norms used for dedup. */
|
|
43
45
|
deliveredTextNorms: readonly string[];
|
|
46
|
+
/** Number of tool calls observed in this turn. */
|
|
47
|
+
toolCalls?: number;
|
|
44
48
|
/** Whether this is already a retry (prevents looping). */
|
|
45
49
|
retried: boolean;
|
|
50
|
+
/** Number of synthetic flow-violation retries already attempted. */
|
|
51
|
+
retryCount?: number;
|
|
52
|
+
/** Maximum synthetic flow-violation retries before accepting the drop. */
|
|
53
|
+
maxRetries?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Reminder text to ship on retry. Defaults to the legacy
|
|
56
|
+
* telegram-shaped `FLOW_VIOLATION_REMINDER`; backends should pass
|
|
57
|
+
* `buildFlowViolationReminder(frontend)` so the tool names match
|
|
58
|
+
* what's actually registered (Teams has `send_message`, not `send`).
|
|
59
|
+
*/
|
|
60
|
+
reminder?: string;
|
|
46
61
|
};
|
|
47
62
|
|
|
48
63
|
/** Outcome of the flow-violation check. */
|
|
@@ -52,6 +67,8 @@ export type FlowViolationResult =
|
|
|
52
67
|
violated: true;
|
|
53
68
|
/** Trimmed scratchpad prose that would be dropped. */
|
|
54
69
|
trailing: string;
|
|
70
|
+
/** Human-readable reason logged by handlers. */
|
|
71
|
+
reason: string;
|
|
55
72
|
/** Whether the handler should re-prompt with `reminder`. False when retried. */
|
|
56
73
|
shouldRetry: boolean;
|
|
57
74
|
/** Reminder string to send as the next user prompt (when retrying). */
|
|
@@ -65,9 +82,9 @@ export type FlowViolationResult =
|
|
|
65
82
|
* Classify a stream-loop outcome against the delivery contract.
|
|
66
83
|
*
|
|
67
84
|
* A turn is in violation when ALL of the following hold:
|
|
68
|
-
* - The trailing text (after trim) is non-empty.
|
|
69
85
|
* - No turn-terminator tool fired (e.g. `end_turn`).
|
|
70
|
-
* - The
|
|
86
|
+
* - The model produced either trailing prose or tool calls.
|
|
87
|
+
* - Any trailing text isn't a duplicate of content already delivered
|
|
71
88
|
* through a tool call in the same turn.
|
|
72
89
|
*
|
|
73
90
|
* The third condition handles the legitimate "model wrote text AND called
|
|
@@ -79,14 +96,28 @@ export function detectFlowViolation(
|
|
|
79
96
|
inputs: FlowViolationInputs,
|
|
80
97
|
): FlowViolationResult {
|
|
81
98
|
const trailing = inputs.trailingText.trim();
|
|
82
|
-
if (trailing.length === 0) return { violated: false };
|
|
83
99
|
if (inputs.turnTerminated) return { violated: false };
|
|
84
|
-
|
|
100
|
+
const toolCalls = inputs.toolCalls ?? 0;
|
|
101
|
+
if (trailing.length === 0 && toolCalls === 0) return { violated: false };
|
|
102
|
+
if (
|
|
103
|
+
trailing.length > 0 &&
|
|
104
|
+
isDuplicateOfDelivered(trailing, inputs.deliveredTextNorms)
|
|
105
|
+
) {
|
|
85
106
|
return { violated: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const retryCount = inputs.retryCount ?? (inputs.retried ? 1 : 0);
|
|
110
|
+
const maxRetries = inputs.maxRetries ?? FLOW_VIOLATION_MAX_RETRIES;
|
|
111
|
+
const reason =
|
|
112
|
+
trailing.length > 0
|
|
113
|
+
? `trailing prose (${trailing.length} chars)`
|
|
114
|
+
: `${toolCalls} tool call${toolCalls === 1 ? "" : "s"} with no terminator`;
|
|
115
|
+
|
|
86
116
|
return {
|
|
87
117
|
violated: true,
|
|
88
118
|
trailing,
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
reason,
|
|
120
|
+
shouldRetry: retryCount < maxRetries,
|
|
121
|
+
reminder: inputs.reminder ?? FLOW_VIOLATION_REMINDER,
|
|
91
122
|
};
|
|
92
123
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend-list helpers shared across backends.
|
|
3
|
+
*
|
|
4
|
+
* Every backend needs "which frontends get an MCP tool server?" at
|
|
5
|
+
* spawn time; before this helper, claude-sdk, openai-agents, and codex
|
|
6
|
+
* each carried their own copy of the same normalise-and-filter logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isNativeChatId,
|
|
11
|
+
isTeamsChatId,
|
|
12
|
+
isDiscordChatId,
|
|
13
|
+
isTelegramChatId,
|
|
14
|
+
} from "../../util/chat-id.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalise a config `frontend` value (string or array, possibly
|
|
18
|
+
* undefined) to the list of messaging frontends — i.e. those that
|
|
19
|
+
* need an MCP tool server spawned. `terminal` is excluded: it has no
|
|
20
|
+
* outbound messaging surface (the agent runs to stdout).
|
|
21
|
+
*/
|
|
22
|
+
export function nonTerminalFrontends(
|
|
23
|
+
frontend: string | readonly string[] | undefined,
|
|
24
|
+
): readonly string[] {
|
|
25
|
+
if (!frontend) return [];
|
|
26
|
+
const all = Array.isArray(frontend) ? frontend : [frontend as string];
|
|
27
|
+
return all.filter((f) => f !== "terminal");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The messaging frontend that owns a chat, inferred from the chat-id
|
|
32
|
+
* shape (the same convention the gateway uses to route actions):
|
|
33
|
+
* native `d_*`, teams `teams_chat_*`, discord `discord_*`, telegram
|
|
34
|
+
* numeric. Returns null for cross-surface contexts — the heartbeat
|
|
35
|
+
* sentinel, isolated one-shots, terminal sessions.
|
|
36
|
+
*/
|
|
37
|
+
export function frontendForChatId(chatId: string): string | null {
|
|
38
|
+
if (isNativeChatId(chatId)) return "native";
|
|
39
|
+
if (isTeamsChatId(chatId)) return "teams";
|
|
40
|
+
if (isDiscordChatId(chatId)) return "discord";
|
|
41
|
+
if (isTelegramChatId(chatId)) return "telegram";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which frontends' tool servers a chat's turn should register.
|
|
47
|
+
*
|
|
48
|
+
* A chat owned by one messaging frontend gets exactly that frontend's
|
|
49
|
+
* tools: a native-app conversation must see `native-tools`, not a
|
|
50
|
+
* `telegram-tools` server that happens to share the daemon — the
|
|
51
|
+
* model reads surface identity out of tool names, and stray
|
|
52
|
+
* `mcp__telegram-tools__*` entries in a native chat are both confusing
|
|
53
|
+
* UI and an invitation to deliver the reply to the wrong surface.
|
|
54
|
+
*
|
|
55
|
+
* Chats with no owning frontend (heartbeat sentinel, one-shot cron,
|
|
56
|
+
* terminal sessions) keep every configured messaging frontend: those
|
|
57
|
+
* contexts legitimately reach across surfaces via explicit chat_id.
|
|
58
|
+
*/
|
|
59
|
+
export function frontendsForChat(
|
|
60
|
+
chatId: string,
|
|
61
|
+
configured: readonly string[],
|
|
62
|
+
): readonly string[] {
|
|
63
|
+
const owner = frontendForChatId(chatId);
|
|
64
|
+
if (owner && configured.includes(owner)) return [owner];
|
|
65
|
+
return configured;
|
|
66
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error-recovery dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Every backend's handler ends its `try { ... } catch (err) { ... }`
|
|
5
|
+
* block with the same `classifyRetry` decision tree:
|
|
6
|
+
*
|
|
7
|
+
* 1. `reset_and_retry` (session_expired / context_length) — reset
|
|
8
|
+
* the session and recurse.
|
|
9
|
+
* 2. `fallback_model` (retryable + fallback configured) — recurse
|
|
10
|
+
* with the fallback model threaded through the retry's params.
|
|
11
|
+
* 3. `propagate` — let the caller throw.
|
|
12
|
+
*
|
|
13
|
+
* The four backends each duplicated ~30 lines of this — same logic,
|
|
14
|
+
* different log-line prefix. Centralising it here means a future
|
|
15
|
+
* tweak (e.g. adding a third decision kind, changing the
|
|
16
|
+
* transient-model-flip semantics) lands once instead of four times,
|
|
17
|
+
* and the per-backend handlers reduce to a single function call.
|
|
18
|
+
*
|
|
19
|
+
* Caveat: this is the GENERIC recovery path. Backend-specific
|
|
20
|
+
* fallbacks (e.g. `maybeFallbackForChatGptMismatch` in codex) still
|
|
21
|
+
* live in the backend handler — they're a different decision and run
|
|
22
|
+
* BEFORE this one.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { QueryParams, QueryResult } from "./handler-types.js";
|
|
26
|
+
import { classify, type TalonError } from "../../core/errors.js";
|
|
27
|
+
import { logWarn } from "../../util/log.js";
|
|
28
|
+
import { incrementCounter } from "../../util/metrics.js";
|
|
29
|
+
import { resetSession } from "../../storage/sessions.js";
|
|
30
|
+
import { classifyRetry } from "./model-retry.js";
|
|
31
|
+
import type { AgentEvent } from "../../core/agent-runtime/events.js";
|
|
32
|
+
|
|
33
|
+
/** Inputs for `applyRetryDecision`. */
|
|
34
|
+
export interface ApplyRetryDecisionInputs {
|
|
35
|
+
/** The error caught by the backend's handler. */
|
|
36
|
+
err: unknown;
|
|
37
|
+
/** Active chat id (for log lines + session reset). */
|
|
38
|
+
chatId: string;
|
|
39
|
+
/** Model that was active when the error fired. */
|
|
40
|
+
activeModel: string;
|
|
41
|
+
/** True when the current call is already a retry (short-circuits). */
|
|
42
|
+
retried: boolean;
|
|
43
|
+
/** Original `handleMessage` params — used for the recursive retry. */
|
|
44
|
+
params: QueryParams;
|
|
45
|
+
/**
|
|
46
|
+
* Backend's recursive entry — passed as a callback so we don't have
|
|
47
|
+
* to depend on each backend module. Each backend calls this with its
|
|
48
|
+
* own `handleMessage`.
|
|
49
|
+
*/
|
|
50
|
+
recurseWithRetried: (params: QueryParams) => Promise<QueryResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Backend label for log-line prefixes (`"Kilo"`, `"OpenCode"`,
|
|
53
|
+
* `"Codex"`, `"Claude"`). When omitted or empty the prefix is
|
|
54
|
+
* dropped — matches the claude-sdk historical behaviour.
|
|
55
|
+
*/
|
|
56
|
+
backendLabel?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Word used in the reset_and_retry log line. Codex resets a "thread"
|
|
59
|
+
* while the remote-server / claude backends reset a "session"; the
|
|
60
|
+
* helper takes whatever the backend prefers.
|
|
61
|
+
*/
|
|
62
|
+
resetNoun?: "session" | "thread";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Outcome — undefined means the caller should propagate the classified error. */
|
|
66
|
+
export interface ApplyRetryDecisionResult {
|
|
67
|
+
/**
|
|
68
|
+
* Set when the helper retried — caller `return retry`s this value
|
|
69
|
+
* directly from its handler.
|
|
70
|
+
*/
|
|
71
|
+
retry?: QueryResult;
|
|
72
|
+
/**
|
|
73
|
+
* Set when the helper decided NOT to retry — caller should
|
|
74
|
+
* `throw classified` to propagate.
|
|
75
|
+
*/
|
|
76
|
+
classified: TalonError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run the shared retry-decision logic.
|
|
81
|
+
*
|
|
82
|
+
* Side effects (when a retry fires):
|
|
83
|
+
* - `incrementCounter('errors.<reason>')` exactly once per call.
|
|
84
|
+
* - `resetSession(chatId)` before recursion.
|
|
85
|
+
* - For `fallback_model`: the fallback model id is spread into the
|
|
86
|
+
* recursion's params (`params.model` outranks chat settings).
|
|
87
|
+
*
|
|
88
|
+
* When the decision is `propagate`, returns `{classified}` only — the
|
|
89
|
+
* caller throws.
|
|
90
|
+
*/
|
|
91
|
+
export async function applyRetryDecision(
|
|
92
|
+
inputs: ApplyRetryDecisionInputs,
|
|
93
|
+
): Promise<ApplyRetryDecisionResult> {
|
|
94
|
+
const {
|
|
95
|
+
err,
|
|
96
|
+
chatId,
|
|
97
|
+
activeModel,
|
|
98
|
+
retried,
|
|
99
|
+
params,
|
|
100
|
+
recurseWithRetried,
|
|
101
|
+
backendLabel,
|
|
102
|
+
resetNoun = "session",
|
|
103
|
+
} = inputs;
|
|
104
|
+
|
|
105
|
+
const classified = classify(err);
|
|
106
|
+
incrementCounter(`errors.${classified.reason ?? "unknown"}`);
|
|
107
|
+
|
|
108
|
+
const decision = classifyRetry({
|
|
109
|
+
error: classified,
|
|
110
|
+
activeModel,
|
|
111
|
+
retried,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const prefix = backendLabel ? `${backendLabel} ` : "";
|
|
115
|
+
|
|
116
|
+
if (decision.kind === "reset_and_retry") {
|
|
117
|
+
logWarn(
|
|
118
|
+
"agent",
|
|
119
|
+
`[${chatId}] ${prefix}${decision.reason}, resetting ${resetNoun} and retrying`,
|
|
120
|
+
);
|
|
121
|
+
resetSession(chatId);
|
|
122
|
+
return { retry: await recurseWithRetried(params), classified };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (decision.kind === "fallback_model") {
|
|
126
|
+
logWarn(
|
|
127
|
+
"agent",
|
|
128
|
+
`[${chatId}] ${classified.reason}, falling back to ${decision.fallbackModelId}`,
|
|
129
|
+
);
|
|
130
|
+
resetSession(chatId);
|
|
131
|
+
return {
|
|
132
|
+
retry: await recurseWithRetried({
|
|
133
|
+
...params,
|
|
134
|
+
model: decision.fallbackModelId,
|
|
135
|
+
}),
|
|
136
|
+
classified,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// `propagate` — caller throws `classified`.
|
|
141
|
+
return { classified };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Generator-shaped variant ────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/** Inputs for `applyRetryDecisionStream`. */
|
|
147
|
+
export interface StreamRetryInputs {
|
|
148
|
+
/** Error caught inside the caller's generator. */
|
|
149
|
+
err: unknown;
|
|
150
|
+
/** Active chat id (for log lines + session reset). */
|
|
151
|
+
chatId: string;
|
|
152
|
+
/** Model that was active when the error fired. */
|
|
153
|
+
activeModel: string;
|
|
154
|
+
/** True when the current call is already a retry (short-circuits). */
|
|
155
|
+
retried: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Builds the recursive event stream. Called once when the helper
|
|
158
|
+
* decides to retry — yielded via `yield*` so the caller's generator
|
|
159
|
+
* transparently delegates to it. For `fallback_model` decisions the
|
|
160
|
+
* fallback model id is passed through; the builder must thread it
|
|
161
|
+
* into the retry's params (`params.model` outranks chat settings,
|
|
162
|
+
* so a transient `setChatModel` flip would be a silent no-op).
|
|
163
|
+
*/
|
|
164
|
+
buildRetryStream: (fallbackModelId?: string) => AsyncIterable<AgentEvent>;
|
|
165
|
+
/** Backend label for log-line prefixes (`"Kilo"`, `"Codex"` …). */
|
|
166
|
+
backendLabel?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Word used in the reset_and_retry log line. Codex resets a "thread"
|
|
169
|
+
* while remote-server / claude backends reset a "session".
|
|
170
|
+
*/
|
|
171
|
+
resetNoun?: "session" | "thread";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Outcome `applyRetryDecisionStream` returns from its generator. */
|
|
175
|
+
export interface StreamRetryResult {
|
|
176
|
+
/**
|
|
177
|
+
* True when the helper recursed (the recursive stream was already
|
|
178
|
+
* yielded into the caller). Caller may finish its generator.
|
|
179
|
+
*/
|
|
180
|
+
retried: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* The classified error. When `retried` is false the caller should
|
|
183
|
+
* yield an `error` event derived from this and return.
|
|
184
|
+
*/
|
|
185
|
+
classified: TalonError;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generator-shaped equivalent of `applyRetryDecision`. The caller
|
|
190
|
+
* uses `yield* applyRetryDecisionStream({...})` so the recursive
|
|
191
|
+
* retry's events flow into the outer stream transparently. Side
|
|
192
|
+
* effects (counter increment, session reset, fallback model threaded
|
|
193
|
+
* through the retry) match the callback-shaped helper exactly.
|
|
194
|
+
*
|
|
195
|
+
* The terminating value (`AsyncGenerator` 2nd type param) tells the
|
|
196
|
+
* caller whether to emit a final `error` event or just return.
|
|
197
|
+
*/
|
|
198
|
+
export async function* applyRetryDecisionStream(
|
|
199
|
+
inputs: StreamRetryInputs,
|
|
200
|
+
): AsyncGenerator<AgentEvent, StreamRetryResult, void> {
|
|
201
|
+
const {
|
|
202
|
+
err,
|
|
203
|
+
chatId,
|
|
204
|
+
activeModel,
|
|
205
|
+
retried,
|
|
206
|
+
buildRetryStream,
|
|
207
|
+
backendLabel,
|
|
208
|
+
resetNoun = "session",
|
|
209
|
+
} = inputs;
|
|
210
|
+
|
|
211
|
+
const classified = classify(err);
|
|
212
|
+
incrementCounter(`errors.${classified.reason ?? "unknown"}`);
|
|
213
|
+
|
|
214
|
+
const decision = classifyRetry({
|
|
215
|
+
error: classified,
|
|
216
|
+
activeModel,
|
|
217
|
+
retried,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const prefix = backendLabel ? `${backendLabel} ` : "";
|
|
221
|
+
|
|
222
|
+
if (decision.kind === "reset_and_retry") {
|
|
223
|
+
logWarn(
|
|
224
|
+
"agent",
|
|
225
|
+
`[${chatId}] ${prefix}${decision.reason}, resetting ${resetNoun} and retrying`,
|
|
226
|
+
);
|
|
227
|
+
resetSession(chatId);
|
|
228
|
+
yield* buildRetryStream();
|
|
229
|
+
return { retried: true, classified };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (decision.kind === "fallback_model") {
|
|
233
|
+
logWarn(
|
|
234
|
+
"agent",
|
|
235
|
+
`[${chatId}] ${classified.reason}, falling back to ${decision.fallbackModelId}`,
|
|
236
|
+
);
|
|
237
|
+
resetSession(chatId);
|
|
238
|
+
yield* buildRetryStream(decision.fallbackModelId);
|
|
239
|
+
return { retried: true, classified };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// `propagate` — caller should yield an `error` event and return.
|
|
243
|
+
return { retried: false, classified };
|
|
244
|
+
}
|