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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Claude SDK chat-turn handler — natively emits `AgentEvent`s.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Drives the full lifecycle: prompt formatting, SDK query, native
|
|
5
|
+
* event emission per stream message, error recovery (session expired
|
|
6
|
+
* / context overflow / model fallback via `applyRetryDecisionStream`),
|
|
7
|
+
* token accounting, session persistence, and the flow-violation
|
|
8
|
+
* re-prompt loop.
|
|
9
|
+
*
|
|
10
|
+
* The exported async generator `runChatTurn` is what the factory wires
|
|
11
|
+
* onto `ChatBackend.runChatTurn` — no wrapper, no callback shim.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -11,27 +16,35 @@ import {
|
|
|
11
16
|
getSession,
|
|
12
17
|
incrementTurns,
|
|
13
18
|
recordUsage,
|
|
14
|
-
resetSession,
|
|
15
19
|
setSessionId,
|
|
16
20
|
setSessionName,
|
|
21
|
+
updateLiveTurn,
|
|
17
22
|
} from "../../storage/sessions.js";
|
|
18
|
-
import { getChatSettings, setChatModel } from "../../storage/chat-settings.js";
|
|
19
|
-
import { classify } from "../../core/errors.js";
|
|
20
23
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
21
24
|
import { traceMessage } from "../../util/trace.js";
|
|
22
|
-
import { incrementCounter
|
|
23
|
-
import { isTurnTerminator
|
|
25
|
+
import { incrementCounter } from "../../util/metrics.js";
|
|
26
|
+
import { isTurnTerminator } from "../../core/tools/index.js";
|
|
24
27
|
|
|
25
28
|
import type { Query } from "@anthropic-ai/claude-agent-sdk";
|
|
26
|
-
import
|
|
29
|
+
import {
|
|
30
|
+
type AgentEvent,
|
|
31
|
+
classifiedToAgentError,
|
|
32
|
+
} from "../../core/agent-runtime/events.js";
|
|
33
|
+
import type { ChatRunParams } from "../../core/agent-runtime/capabilities.js";
|
|
34
|
+
import { makeBareModelRef } from "../../core/agent-runtime/model-ref.js";
|
|
35
|
+
import { applyRetryDecisionStream } from "../shared/handle-retry.js";
|
|
27
36
|
import { getConfig } from "./state.js";
|
|
28
|
-
import { buildSdkOptions } from "./options.js";
|
|
37
|
+
import { buildSdkOptions, getActiveFrontends } from "./options.js";
|
|
38
|
+
import { waitForMcpServersReady } from "./mcp-ready.js";
|
|
39
|
+
import { frontendsForChat } from "../shared/frontends.js";
|
|
29
40
|
import {
|
|
30
41
|
createStreamState,
|
|
31
42
|
isSystemInit,
|
|
32
43
|
isStreamEvent,
|
|
33
44
|
isAssistant,
|
|
34
45
|
isResult,
|
|
46
|
+
isUserMessage,
|
|
47
|
+
extractToolResults,
|
|
35
48
|
processStreamDelta,
|
|
36
49
|
processAssistantMessage,
|
|
37
50
|
processResultMessage,
|
|
@@ -41,11 +54,46 @@ import {
|
|
|
41
54
|
prepareSystemPrompt,
|
|
42
55
|
extractSessionName,
|
|
43
56
|
detectFlowViolation,
|
|
57
|
+
FLOW_VIOLATION_MAX_RETRIES,
|
|
44
58
|
captureDeliveredText,
|
|
45
|
-
classifyRetry,
|
|
46
59
|
summarizeUsage,
|
|
60
|
+
buildDeliveryContract,
|
|
61
|
+
buildFlowViolationReminder,
|
|
62
|
+
buildFirstTurnReminder,
|
|
63
|
+
recordToolCall,
|
|
64
|
+
recordTurnMetrics,
|
|
65
|
+
recordFailedTurnAccounting,
|
|
66
|
+
recordFlowViolation,
|
|
47
67
|
} from "../shared/index.js";
|
|
48
68
|
|
|
69
|
+
// ── Post-result watchdog ────────────────────────────────────────────────────
|
|
70
|
+
// The SDK's PostToolBatch hook is the canonical loop-terminator — it returns
|
|
71
|
+
// `{ continue: false }` after `end_turn`/`send`, and the SDK is supposed to
|
|
72
|
+
// emit a `result` SDKMessage and close the async iterator immediately after.
|
|
73
|
+
// In practice (observed 2026-05-19 14:52Z, chat 352042062, contextTokens=251464,
|
|
74
|
+
// numApiCalls=50) the SDK can emit `result` and then ghost — the for-await loop
|
|
75
|
+
// stays parked forever, holding the dispatcher context and the typing-indicator
|
|
76
|
+
// pulse for hours until someone manually `/restart`s.
|
|
77
|
+
//
|
|
78
|
+
// Workaround: arm a short timer the moment `result` is processed. If the
|
|
79
|
+
// iterator hasn't closed by the grace deadline, abort the controller and
|
|
80
|
+
// force-close the generator via `qi.return()`. The clean-exit case clears the
|
|
81
|
+
// timer in the same turn and pays nothing.
|
|
82
|
+
|
|
83
|
+
const DEFAULT_SDK_POST_RESULT_GRACE_MS = 5_000;
|
|
84
|
+
|
|
85
|
+
function envMs(name: string, fallback: number): number {
|
|
86
|
+
const raw = process.env[name];
|
|
87
|
+
if (!raw) return fallback;
|
|
88
|
+
const n = Number(raw);
|
|
89
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SDK_POST_RESULT_GRACE_MS = envMs(
|
|
93
|
+
"TALON_SDK_POST_RESULT_GRACE_MS",
|
|
94
|
+
DEFAULT_SDK_POST_RESULT_GRACE_MS,
|
|
95
|
+
);
|
|
96
|
+
|
|
49
97
|
// ── Active query store ──────────────────────────────────────────────────────
|
|
50
98
|
// Holds the Query reference for each in-flight chat so gateway actions
|
|
51
99
|
// (e.g. reload_plugins) can call control methods like setMcpServers().
|
|
@@ -53,62 +101,179 @@ import {
|
|
|
53
101
|
const activeQueries = new Map<string, Query>();
|
|
54
102
|
|
|
55
103
|
/** Get the active Query for a chat, if one is in flight. */
|
|
104
|
+
/**
|
|
105
|
+
* Best-effort graceful interrupt of a chat's in-flight turn. Uses the SDK's
|
|
106
|
+
* native `Query.interrupt()`, which stops the agent loop and closes the stream
|
|
107
|
+
* with a `result` (subtype `interrupt`) — so the turn ends as a normal
|
|
108
|
+
* completion (turn_end + usage), NOT an error, and never trips the
|
|
109
|
+
* model-fallback retry path. No-op (returns false) when no turn is running.
|
|
110
|
+
*/
|
|
111
|
+
export async function interruptChatTurn(chatId: string): Promise<boolean> {
|
|
112
|
+
const qi = activeQueries.get(chatId);
|
|
113
|
+
if (!qi) return false;
|
|
114
|
+
try {
|
|
115
|
+
await qi.interrupt();
|
|
116
|
+
log("agent", `[${chatId}] turn interrupted by user`);
|
|
117
|
+
incrementCounter("sdk.turn_interrupted");
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logWarn(
|
|
121
|
+
"agent",
|
|
122
|
+
`[${chatId}] interrupt failed: ${err instanceof Error ? err.message : err}`,
|
|
123
|
+
);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
56
128
|
export function getActiveQuery(chatId: string): Query | undefined {
|
|
57
129
|
return activeQueries.get(chatId);
|
|
58
130
|
}
|
|
59
131
|
|
|
60
|
-
// ──
|
|
132
|
+
// ── Internal state passed across recursive retry calls ──────────────────────
|
|
133
|
+
|
|
134
|
+
type InternalState = { flowRetries?: number; errorRetried?: boolean };
|
|
61
135
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
136
|
+
// ── Main chat-turn generator ────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Native chat-turn generator. Yields the canonical
|
|
140
|
+
* `run_started → text_delta* → reasoning* → assistant_message* →
|
|
141
|
+
* tool_call* → usage → completed` sequence. On error: emits an
|
|
142
|
+
* `error` event (after running the shared retry decision, which may
|
|
143
|
+
* recurse via `yield*` and produce the retry's event stream
|
|
144
|
+
* transparently). On flow violation: `yield* runChatTurn(retry
|
|
145
|
+
* params)` — the recursive call owns its `incrementTurns`, the
|
|
146
|
+
* caller deliberately doesn't increment.
|
|
147
|
+
*/
|
|
148
|
+
export async function* runChatTurn(
|
|
149
|
+
params: ChatRunParams,
|
|
150
|
+
_internal: InternalState = {},
|
|
151
|
+
): AsyncIterable<AgentEvent> {
|
|
66
152
|
const config = getConfig();
|
|
67
153
|
|
|
68
|
-
const {
|
|
69
|
-
chatId,
|
|
70
|
-
text,
|
|
71
|
-
senderName,
|
|
72
|
-
isGroup,
|
|
73
|
-
onTextBlock,
|
|
74
|
-
onStreamDelta,
|
|
75
|
-
onToolUse,
|
|
76
|
-
} = params;
|
|
154
|
+
const { chatId, text, senderName, isGroup } = params;
|
|
77
155
|
const session = getSession(chatId);
|
|
78
156
|
const t0 = Date.now();
|
|
79
157
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
158
|
+
// The chat's OWNING messaging frontend (falling back to the primary
|
|
159
|
+
// for cross-surface chats). Drives the delivery-contract suffix and
|
|
160
|
+
// the frontend-aware flow-violation text — the tool NAMES differ per
|
|
161
|
+
// frontend (native's send tool is send_message, telegram's is send),
|
|
162
|
+
// and with tool servers scoped per chat the wrong contract would
|
|
163
|
+
// instruct a tool that doesn't exist. Empty in terminal mode, where
|
|
164
|
+
// no delivery tools exist and the strict tool-only contract must not
|
|
165
|
+
// be asserted.
|
|
166
|
+
const frontend: string | undefined = frontendsForChat(
|
|
167
|
+
chatId,
|
|
168
|
+
getActiveFrontends(),
|
|
169
|
+
)[0];
|
|
170
|
+
|
|
171
|
+
// Frozen per-session prompt (keyed by session epoch) — stable across
|
|
172
|
+
// turns so the provider's prompt-cache prefix survives other chats'
|
|
173
|
+
// session resets. See backend/shared/system-prompt.ts. The delivery
|
|
174
|
+
// contract joins as the backend suffix — the tail of the static
|
|
175
|
+
// prompt, the highest-salience spot — so models see the tool-only
|
|
176
|
+
// flow before their first turn instead of discovering it via a
|
|
177
|
+
// [FLOW VIOLATION] retry.
|
|
178
|
+
const preparedPrompt = prepareSystemPrompt({
|
|
179
|
+
config,
|
|
180
|
+
previousTurns: session.turns,
|
|
181
|
+
chatId,
|
|
182
|
+
sessionEpoch: session.createdAt,
|
|
183
|
+
backendSuffix: frontend
|
|
184
|
+
? buildDeliveryContract("tool-only", frontend)
|
|
185
|
+
: undefined,
|
|
186
|
+
});
|
|
86
187
|
|
|
87
|
-
const
|
|
188
|
+
const abortController = new AbortController();
|
|
189
|
+
const { options, activeModel } = buildSdkOptions(
|
|
190
|
+
chatId,
|
|
191
|
+
abortController,
|
|
192
|
+
params.model.id,
|
|
193
|
+
preparedPrompt,
|
|
194
|
+
);
|
|
88
195
|
|
|
89
|
-
|
|
196
|
+
let prompt = formatUserPrompt({
|
|
90
197
|
text,
|
|
91
198
|
senderName: senderName ?? "user",
|
|
92
199
|
isGroup,
|
|
93
200
|
messageId: params.messageId,
|
|
94
201
|
});
|
|
202
|
+
// First turn of a session is where flow violations cluster — the
|
|
203
|
+
// model hasn't seen the contract in action yet. One line appended to
|
|
204
|
+
// the turn-0 user message (never the system prompt, so the cached
|
|
205
|
+
// prefix is untouched) pre-empts the 2x-token violation retry.
|
|
206
|
+
// Skipped on flow retries: those already carry the full reminder.
|
|
207
|
+
if (frontend && session.turns === 0 && !_internal.flowRetries) {
|
|
208
|
+
prompt += `\n\n${buildFirstTurnReminder(frontend)}`;
|
|
209
|
+
}
|
|
95
210
|
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
96
211
|
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
97
212
|
|
|
213
|
+
yield { type: "run_started" };
|
|
214
|
+
|
|
98
215
|
const qi = query({ prompt, options });
|
|
99
216
|
activeQueries.set(chatId, qi);
|
|
217
|
+
|
|
218
|
+
// Cold-start delivery-tool race. The Claude SDK spawns a fresh subprocess
|
|
219
|
+
// per turn, and each reconnects to the hub's `${frontend}-tools` server.
|
|
220
|
+
// `alwaysLoad` is meant to block startup until that server connects, but on
|
|
221
|
+
// the FIRST turn of a freshly-opened chat the hub binding is created cold and
|
|
222
|
+
// can still be `pending` when the model builds its turn-1 tool list — so
|
|
223
|
+
// `end_turn`/`send` are absent and the reply silently fails (or the model
|
|
224
|
+
// burns a `tool_search` round-trip to recover it). After a close+reconnect
|
|
225
|
+
// the hub binding is warm, connects instantly, and the tools are present —
|
|
226
|
+
// which is exactly why the bug "heals" on reconnect. Explicitly wait (bounded,
|
|
227
|
+
// non-throwing) for the delivery server to leave `pending` before consuming
|
|
228
|
+
// the stream. This mirrors the proven `refreshTools` gate and returns
|
|
229
|
+
// immediately on warm turns, so it adds no steady-state latency.
|
|
230
|
+
if (frontend) {
|
|
231
|
+
await waitForMcpServersReady(qi, [`${frontend}-tools`], 5_000, 100);
|
|
232
|
+
}
|
|
233
|
+
|
|
100
234
|
const state = createStreamState();
|
|
235
|
+
// tool_use id → tool name for calls we've announced this turn; lets
|
|
236
|
+
// the tool_result emission name the tool without re-parsing blocks.
|
|
237
|
+
const pendingTools = new Map<string, string>();
|
|
238
|
+
|
|
239
|
+
// Per-API-call usage accumulator for live mid-turn stats. Each
|
|
240
|
+
// assistant message carries its API call's usage as it lands; the
|
|
241
|
+
// authoritative per-turn totals still come from the final result
|
|
242
|
+
// message (processResultMessage) — this only feeds the live-turn
|
|
243
|
+
// overlay so /status moves while a long agentic turn runs, and the
|
|
244
|
+
// failure path below so an errored turn's burn isn't lost.
|
|
245
|
+
const liveAcc = {
|
|
246
|
+
input: 0,
|
|
247
|
+
output: 0,
|
|
248
|
+
cacheRead: 0,
|
|
249
|
+
cacheWrite: 0,
|
|
250
|
+
calls: 0,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let postResultTimer: ReturnType<typeof setTimeout> | null = null;
|
|
254
|
+
let postResultForceClosed = false;
|
|
255
|
+
const armPostResultWatchdog = (): void => {
|
|
256
|
+
if (postResultTimer) return;
|
|
257
|
+
const t = setTimeout(() => {
|
|
258
|
+
postResultForceClosed = true;
|
|
259
|
+
logWarn(
|
|
260
|
+
"agent",
|
|
261
|
+
`[${chatId}] SDK iterator stuck ${SDK_POST_RESULT_GRACE_MS}ms after result — aborting`,
|
|
262
|
+
);
|
|
263
|
+
incrementCounter("sdk.iterator_force_close_after_result");
|
|
264
|
+
try {
|
|
265
|
+
abortController.abort();
|
|
266
|
+
} catch {
|
|
267
|
+
/* abort() can throw if already aborted — ignore */
|
|
268
|
+
}
|
|
269
|
+
qi.return(undefined).catch(() => {
|
|
270
|
+
/* the generator may already be in a terminal state — ignore */
|
|
271
|
+
});
|
|
272
|
+
}, SDK_POST_RESULT_GRACE_MS);
|
|
273
|
+
t.unref();
|
|
274
|
+
postResultTimer = t;
|
|
275
|
+
};
|
|
101
276
|
|
|
102
|
-
// Capture text args from delivery tools (`end_turn`, `send(type="text")`)
|
|
103
|
-
// so the end-of-turn trailing-text fallback can dedupe against content
|
|
104
|
-
// already delivered. Without this, a model that writes prose AND calls a
|
|
105
|
-
// delivery tool with similar text would surface twice in the chat.
|
|
106
|
-
//
|
|
107
|
-
// Tool names arrive MCP-prefixed (e.g. `mcp__telegram-tools__end_turn`)
|
|
108
|
-
// when routed through MCP — strip the prefix so equality checks match
|
|
109
|
-
// the registry's bare names.
|
|
110
|
-
// `captureDeliveredText` (from shared/) returns the normalized text
|
|
111
|
-
// norm — push it into state for the post-turn dedup check.
|
|
112
277
|
const captureIntoState = (
|
|
113
278
|
toolName: string,
|
|
114
279
|
input: Record<string, unknown>,
|
|
@@ -117,137 +282,223 @@ export async function handleMessage(
|
|
|
117
282
|
if (norm) state.deliveredTextNorms.push(norm);
|
|
118
283
|
};
|
|
119
284
|
|
|
285
|
+
let propagateError: AgentEvent | null = null;
|
|
120
286
|
try {
|
|
121
287
|
for await (const message of qi) {
|
|
122
|
-
// Session ID capture
|
|
123
288
|
if (isSystemInit(message)) {
|
|
124
289
|
state.newSessionId = message.session_id;
|
|
125
290
|
continue;
|
|
126
291
|
}
|
|
127
292
|
|
|
128
|
-
// Stream text deltas and thinking deltas
|
|
129
293
|
if (isStreamEvent(message)) {
|
|
130
|
-
processStreamDelta(message, state
|
|
294
|
+
const emit = processStreamDelta(message, state);
|
|
295
|
+
if (emit) {
|
|
296
|
+
if (emit.phase === "text") {
|
|
297
|
+
yield { type: "text_delta", text: emit.text };
|
|
298
|
+
} else {
|
|
299
|
+
yield { type: "reasoning", text: emit.text };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
131
302
|
continue;
|
|
132
303
|
}
|
|
133
304
|
|
|
134
|
-
// Complete assistant message — extract text blocks and tool calls
|
|
135
305
|
if (isAssistant(message)) {
|
|
136
306
|
const result = processAssistantMessage(message, state);
|
|
137
|
-
|
|
138
|
-
// Track the trailing text from this assistant message. Multiple
|
|
139
|
-
// assistant messages can fire per turn (one per tool-use round-trip);
|
|
140
|
-
// only the LAST one's trailingText is the user-facing final reply.
|
|
141
307
|
state.lastTrailingText = result.trailingText;
|
|
142
308
|
|
|
143
|
-
|
|
309
|
+
const u = message.message.usage;
|
|
310
|
+
if (u) {
|
|
311
|
+
liveAcc.input += u.input_tokens ?? 0;
|
|
312
|
+
liveAcc.output += u.output_tokens ?? 0;
|
|
313
|
+
liveAcc.cacheRead += u.cache_read_input_tokens ?? 0;
|
|
314
|
+
liveAcc.cacheWrite += u.cache_creation_input_tokens ?? 0;
|
|
315
|
+
liveAcc.calls += 1;
|
|
316
|
+
updateLiveTurn(chatId, {
|
|
317
|
+
inputTokens: liveAcc.input,
|
|
318
|
+
outputTokens: liveAcc.output,
|
|
319
|
+
cacheRead: liveAcc.cacheRead,
|
|
320
|
+
cacheWrite: liveAcc.cacheWrite,
|
|
321
|
+
// This call's full prompt = current context fill.
|
|
322
|
+
contextTokens:
|
|
323
|
+
(u.input_tokens ?? 0) +
|
|
324
|
+
(u.cache_read_input_tokens ?? 0) +
|
|
325
|
+
(u.cache_creation_input_tokens ?? 0),
|
|
326
|
+
contextWindow: state.contextWindow ?? 0,
|
|
327
|
+
numApiCalls: liveAcc.calls,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Emit progress text segments BEFORE the tool calls they
|
|
332
|
+
// precede, so a model that says "let me check…" then calls a
|
|
333
|
+
// tool delivers the explanatory text first.
|
|
334
|
+
for (const progress of result.progressTexts) {
|
|
335
|
+
yield { type: "assistant_message", text: progress };
|
|
336
|
+
}
|
|
337
|
+
|
|
144
338
|
for (const tool of result.tools) {
|
|
145
|
-
|
|
339
|
+
recordToolCall(tool.name, "claude");
|
|
146
340
|
captureIntoState(tool.name, tool.input);
|
|
147
|
-
// Pass tool.input so the soft-terminator opt-out (e.g. react
|
|
148
|
-
// with `end_turn: false`) keeps state.turnTerminated correctly
|
|
149
|
-
// false — otherwise the trailing-text dedup path mis-treats a
|
|
150
|
-
// mid-turn react as the final delivery.
|
|
151
341
|
if (isTurnTerminator(tool.name, tool.input)) {
|
|
152
342
|
state.turnTerminated = true;
|
|
153
343
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
344
|
+
// Use the SDK's tool_use block id so the later tool_result
|
|
345
|
+
// (same id) correlates — a UI spinner opened on this event
|
|
346
|
+
// can only be closed by an event carrying the same id.
|
|
347
|
+
const toolId =
|
|
348
|
+
tool.id ||
|
|
349
|
+
`${tool.name}-${Date.now()}-${Math.random()
|
|
350
|
+
.toString(36)
|
|
351
|
+
.slice(2, 8)}`;
|
|
352
|
+
pendingTools.set(toolId, tool.name);
|
|
353
|
+
yield {
|
|
354
|
+
type: "tool_call",
|
|
355
|
+
id: toolId,
|
|
356
|
+
name: tool.name,
|
|
357
|
+
input: tool.input,
|
|
358
|
+
};
|
|
161
359
|
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
162
362
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
363
|
+
// Tool executions come back as tool_result blocks on synthetic
|
|
364
|
+
// user messages. Emit the matching tool_result event — the other
|
|
365
|
+
// half of the lifecycle a tool_call opens (frontends show a
|
|
366
|
+
// spinner until it arrives).
|
|
367
|
+
if (isUserMessage(message)) {
|
|
368
|
+
for (const tr of extractToolResults(message)) {
|
|
369
|
+
const name = pendingTools.get(tr.toolUseId);
|
|
370
|
+
if (!name) continue; // not a tool we announced (subagent, replay)
|
|
371
|
+
pendingTools.delete(tr.toolUseId);
|
|
372
|
+
yield {
|
|
373
|
+
type: "tool_result",
|
|
374
|
+
id: tr.toolUseId,
|
|
375
|
+
name,
|
|
376
|
+
...(tr.error ? { error: tr.error } : {}),
|
|
377
|
+
};
|
|
172
378
|
}
|
|
173
|
-
|
|
174
|
-
// Turn-terminator detection happens here (sets `state.turnTerminated`
|
|
175
|
-
// for the flow-violation check below) but the actual SDK loop exit
|
|
176
|
-
// is owned by the `PostToolBatch` hook in `options.ts`. The hook
|
|
177
|
-
// fires after every tool in the batch has resolved, returns
|
|
178
|
-
// `{ continue: false }`, and the SDK exits with TerminalReason
|
|
179
|
-
// `'hook_stopped'` — no extra "wrap up after end_turn" round-trip,
|
|
180
|
-
// no phantom typing, no token spend on a stop_turn.
|
|
181
|
-
//
|
|
182
|
-
// Historical note: an earlier implementation called `qi.interrupt()`
|
|
183
|
-
// here directly. That raced with in-flight MCP tool dispatches in
|
|
184
|
-
// the same assistant message — `end_turn` itself is an MCP tool,
|
|
185
|
-
// and the model frequently emits sibling tool_use blocks alongside
|
|
186
|
-
// it. `interrupt()` cancelled their AbortController mid-flight,
|
|
187
|
-
// surfacing as `MCP error -32001: AbortError` and bubbling up as
|
|
188
|
-
// "Something went wrong". The `PostToolBatch` hook avoids the race
|
|
189
|
-
// by definition (it fires once the entire batch has resolved).
|
|
190
379
|
continue;
|
|
191
380
|
}
|
|
192
381
|
|
|
193
|
-
// Final result — read token counts and context info
|
|
194
382
|
if (isResult(message)) {
|
|
195
383
|
processResultMessage(message, state, options.model ?? activeModel);
|
|
384
|
+
armPostResultWatchdog();
|
|
196
385
|
}
|
|
197
386
|
}
|
|
198
|
-
} catch (err) {
|
|
199
|
-
const classified = classify(err);
|
|
200
|
-
incrementCounter(`errors.${classified.reason ?? "unknown"}`);
|
|
201
|
-
|
|
202
|
-
const decision = classifyRetry({
|
|
203
|
-
error: classified,
|
|
204
|
-
activeModel,
|
|
205
|
-
retried: _retried,
|
|
206
|
-
});
|
|
207
387
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
388
|
+
// The SDK doesn't throw on API errors — it converts them into a
|
|
389
|
+
// synthetic assistant message and finishes the turn with an error-
|
|
390
|
+
// flagged result (usage limits, 429s, auth failures all land here).
|
|
391
|
+
// Rethrow the captured error text so this turn takes the SAME path
|
|
392
|
+
// as a thrown SDK error: retry decision, failed-turn accounting, and
|
|
393
|
+
// an `error` event carrying the real message — instead of being
|
|
394
|
+
// treated as a normal reply (which, with no delivery tool call,
|
|
395
|
+
// would trip the flow-violation re-prompt loop and burn more turns
|
|
396
|
+
// against an already-exhausted limit).
|
|
397
|
+
if (state.resultErrorText) {
|
|
398
|
+
throw new Error(state.resultErrorText);
|
|
215
399
|
}
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (!postResultForceClosed) {
|
|
402
|
+
const buildRetryStream = (
|
|
403
|
+
fallbackModelId?: string,
|
|
404
|
+
): AsyncIterable<AgentEvent> =>
|
|
405
|
+
runChatTurn(
|
|
406
|
+
fallbackModelId
|
|
407
|
+
? {
|
|
408
|
+
...params,
|
|
409
|
+
model: makeBareModelRef(
|
|
410
|
+
params.model.backend,
|
|
411
|
+
fallbackModelId,
|
|
412
|
+
"fallback",
|
|
413
|
+
),
|
|
414
|
+
}
|
|
415
|
+
: params,
|
|
416
|
+
{ ..._internal, errorRetried: true },
|
|
417
|
+
);
|
|
418
|
+
const { retried, classified } = yield* applyRetryDecisionStream({
|
|
419
|
+
err,
|
|
420
|
+
chatId,
|
|
421
|
+
activeModel,
|
|
422
|
+
retried: _internal.errorRetried ?? false,
|
|
423
|
+
buildRetryStream,
|
|
424
|
+
// No backendLabel — historical claude-sdk log shape was un-prefixed.
|
|
425
|
+
});
|
|
426
|
+
if (retried) {
|
|
427
|
+
// The recursive stream already yielded its own usage + completed.
|
|
428
|
+
return;
|
|
229
429
|
}
|
|
430
|
+
logError("agent", `[${chatId}] SDK error: ${classified.message}`);
|
|
431
|
+
// Defer the actual yield until after the `finally` cleanup runs so the
|
|
432
|
+
// watchdog timer and activeQueries entry are released first.
|
|
433
|
+
propagateError = {
|
|
434
|
+
type: "error",
|
|
435
|
+
error: classifiedToAgentError(classified),
|
|
436
|
+
};
|
|
230
437
|
}
|
|
231
|
-
|
|
232
|
-
logError("agent", `[${chatId}] SDK error: ${classified.message}`);
|
|
233
|
-
throw classified;
|
|
234
438
|
} finally {
|
|
439
|
+
if (postResultTimer) {
|
|
440
|
+
clearTimeout(postResultTimer);
|
|
441
|
+
postResultTimer = null;
|
|
442
|
+
}
|
|
235
443
|
if (activeQueries.get(chatId) === qi) {
|
|
236
444
|
activeQueries.delete(chatId);
|
|
237
445
|
}
|
|
238
446
|
}
|
|
239
447
|
|
|
448
|
+
if (propagateError) {
|
|
449
|
+
// Terminal failure — account for whatever the turn consumed before
|
|
450
|
+
// dying (failed turns burn real tokens). The result message never
|
|
451
|
+
// arrived, so state.sdk* is usually empty; fall back to the per-call
|
|
452
|
+
// accumulator. Retried turns return earlier and never reach here.
|
|
453
|
+
const sawResultUsage =
|
|
454
|
+
state.sdkInputTokens +
|
|
455
|
+
state.sdkOutputTokens +
|
|
456
|
+
state.sdkCacheRead +
|
|
457
|
+
state.sdkCacheWrite >
|
|
458
|
+
0;
|
|
459
|
+
recordFailedTurnAccounting({
|
|
460
|
+
backend: "claude",
|
|
461
|
+
chatId,
|
|
462
|
+
durationMs: Date.now() - t0,
|
|
463
|
+
toolCalls: state.toolCalls,
|
|
464
|
+
apiCalls: state.numApiCalls || liveAcc.calls,
|
|
465
|
+
model: activeModel,
|
|
466
|
+
usage: sawResultUsage
|
|
467
|
+
? {
|
|
468
|
+
inputTokens: state.sdkInputTokens,
|
|
469
|
+
outputTokens: state.sdkOutputTokens,
|
|
470
|
+
cacheRead: state.sdkCacheRead,
|
|
471
|
+
cacheWrite: state.sdkCacheWrite,
|
|
472
|
+
}
|
|
473
|
+
: {
|
|
474
|
+
inputTokens: liveAcc.input,
|
|
475
|
+
outputTokens: liveAcc.output,
|
|
476
|
+
cacheRead: liveAcc.cacheRead,
|
|
477
|
+
cacheWrite: liveAcc.cacheWrite,
|
|
478
|
+
},
|
|
479
|
+
contextTokens: state.contextTokens,
|
|
480
|
+
contextWindow: state.contextWindow,
|
|
481
|
+
});
|
|
482
|
+
yield propagateError;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
240
486
|
// ── Persist session and usage ─────────────────────────────────────────────
|
|
241
487
|
|
|
242
488
|
const durationMs = Date.now() - t0;
|
|
243
|
-
|
|
244
|
-
|
|
489
|
+
recordTurnMetrics({
|
|
490
|
+
backend: "claude",
|
|
491
|
+
durationMs,
|
|
492
|
+
toolCalls: state.toolCalls,
|
|
493
|
+
apiCalls: state.numApiCalls,
|
|
494
|
+
usage: {
|
|
495
|
+
inputTokens: state.sdkInputTokens,
|
|
496
|
+
outputTokens: state.sdkOutputTokens,
|
|
497
|
+
cacheRead: state.sdkCacheRead,
|
|
498
|
+
cacheWrite: state.sdkCacheWrite,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
245
501
|
if (state.newSessionId) setSessionId(chatId, state.newSessionId);
|
|
246
|
-
// Token usage is recorded for THIS attempt unconditionally — the running
|
|
247
|
-
// session totals are additive, so a flow-violation retry that recurses
|
|
248
|
-
// through this same path will record its own tokens on top. The turn
|
|
249
|
-
// counter, in contrast, must only increment ONCE per user message (see
|
|
250
|
-
// the post-violation block below).
|
|
251
502
|
recordUsage(chatId, {
|
|
252
503
|
inputTokens: state.sdkInputTokens,
|
|
253
504
|
outputTokens: state.sdkOutputTokens,
|
|
@@ -260,53 +511,64 @@ export async function handleMessage(
|
|
|
260
511
|
numApiCalls: state.numApiCalls,
|
|
261
512
|
});
|
|
262
513
|
|
|
263
|
-
// Set a descriptive session name from the first message
|
|
264
|
-
|
|
514
|
+
// Set a descriptive session name from the first message.
|
|
515
|
+
// Guard against flow-violation retries, which pass the reminder text as
|
|
516
|
+
// `text` — we only want the original user message, not the synthetic prompt.
|
|
517
|
+
if (session.turns === 0 && text && !_internal.flowRetries) {
|
|
265
518
|
const name = extractSessionName(text);
|
|
266
519
|
if (name) setSessionName(chatId, name);
|
|
267
520
|
}
|
|
268
521
|
|
|
269
522
|
// ── Trailing-prose contract + flow-violation retry ──────────────────────
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
523
|
+
|
|
524
|
+
// Only messaging frontends have a tool-only delivery contract to enforce.
|
|
525
|
+
// In terminal mode (`frontend` undefined) there are no delivery tools and
|
|
526
|
+
// trailing prose IS the reply — the terminal renderer surfaces it via
|
|
527
|
+
// `result.text` when nothing came through the bridge (see
|
|
528
|
+
// frontend/terminal/index.ts). Running the check there would flag the
|
|
529
|
+
// model's natural prose as a violation and re-prompt it to call `end_turn`,
|
|
530
|
+
// a tool that mode doesn't even register. Mirrors the `frontend`-gated
|
|
531
|
+
// delivery-contract suffix above.
|
|
532
|
+
const violation = frontend
|
|
533
|
+
? detectFlowViolation({
|
|
534
|
+
trailingText: state.lastTrailingText,
|
|
535
|
+
turnTerminated: state.turnTerminated,
|
|
536
|
+
deliveredTextNorms: state.deliveredTextNorms,
|
|
537
|
+
toolCalls: state.toolCalls,
|
|
538
|
+
retried: (_internal.flowRetries ?? 0) > 0,
|
|
539
|
+
retryCount: _internal.flowRetries ?? 0,
|
|
540
|
+
maxRetries: FLOW_VIOLATION_MAX_RETRIES,
|
|
541
|
+
reminder: buildFlowViolationReminder(frontend),
|
|
542
|
+
})
|
|
543
|
+
: ({ violated: false } as const);
|
|
286
544
|
|
|
287
545
|
if (violation.violated) {
|
|
288
|
-
|
|
546
|
+
recordFlowViolation(violation.shouldRetry ? "retried" : "cap_exhausted");
|
|
289
547
|
log(
|
|
290
548
|
"agent",
|
|
291
|
-
`[${chatId}] flow violation:
|
|
549
|
+
`[${chatId}] flow violation: ${violation.reason}. ${
|
|
292
550
|
violation.shouldRetry
|
|
293
551
|
? "Re-prompting with reminder."
|
|
294
|
-
:
|
|
552
|
+
: `Retry cap (${FLOW_VIOLATION_MAX_RETRIES}) exhausted — accepting silent drop.`
|
|
295
553
|
}`,
|
|
296
554
|
);
|
|
297
555
|
|
|
298
556
|
if (violation.shouldRetry) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
557
|
+
yield* runChatTurn(
|
|
558
|
+
{ ...params, text: violation.reminder },
|
|
559
|
+
{
|
|
560
|
+
..._internal,
|
|
561
|
+
flowRetries: (_internal.flowRetries ?? 0) + 1,
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
303
565
|
}
|
|
304
566
|
}
|
|
305
567
|
|
|
306
568
|
// Reached the non-retry path — this turn counts as one user-visible turn.
|
|
307
569
|
incrementTurns(chatId);
|
|
308
570
|
|
|
309
|
-
// ── Build result
|
|
571
|
+
// ── Build result events ──────────────────────────────────────────────────
|
|
310
572
|
|
|
311
573
|
state.allResponseText += state.currentBlockText;
|
|
312
574
|
|
|
@@ -332,12 +594,21 @@ export async function handleMessage(
|
|
|
332
594
|
model: activeModel,
|
|
333
595
|
});
|
|
334
596
|
|
|
335
|
-
|
|
336
|
-
text: state.allResponseText.trim(),
|
|
337
|
-
durationMs,
|
|
597
|
+
const usage = {
|
|
338
598
|
inputTokens: state.sdkInputTokens,
|
|
339
599
|
outputTokens: state.sdkOutputTokens,
|
|
340
600
|
cacheRead: state.sdkCacheRead,
|
|
341
601
|
cacheWrite: state.sdkCacheWrite,
|
|
602
|
+
modelId: activeModel,
|
|
603
|
+
};
|
|
604
|
+
yield { type: "usage", usage };
|
|
605
|
+
yield {
|
|
606
|
+
type: "completed",
|
|
607
|
+
result: {
|
|
608
|
+
text: state.allResponseText.trim(),
|
|
609
|
+
durationMs,
|
|
610
|
+
usage,
|
|
611
|
+
modelId: activeModel,
|
|
612
|
+
},
|
|
342
613
|
};
|
|
343
614
|
}
|