whale-code 6.4.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 +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Loop — local-first agentic CLI with server tool support
|
|
3
|
+
*
|
|
4
|
+
* LLM calls proxy through the `agent-proxy` edge function (server holds API key).
|
|
5
|
+
* User authenticates via Supabase JWT. Local tools execute on the client.
|
|
6
|
+
* Server tools execute via direct import of executeTool() (same codebase).
|
|
7
|
+
*
|
|
8
|
+
* Fallback: if proxy is unavailable and ANTHROPIC_API_KEY is set, calls directly.
|
|
9
|
+
*
|
|
10
|
+
* This file is the thin orchestrator + re-export facade. Domain logic lives in:
|
|
11
|
+
* - memory-manager.ts (loadMemory, addMemory, removeMemory, listMemories)
|
|
12
|
+
* - git-context.ts (gatherGitContext, refreshGitContext)
|
|
13
|
+
* - claude-md-loader.ts (loadClaudeMd, reloadClaudeMd)
|
|
14
|
+
* - permission-modes.ts (PermissionMode, set/get/isAllowed)
|
|
15
|
+
* - model-manager.ts (setModel, getModel, getModelShortName, estimateCostUsd)
|
|
16
|
+
* - session-persistence.ts (SessionMeta, save/load/list/find sessions)
|
|
17
|
+
* - system-prompt.ts (buildSystemPrompt)
|
|
18
|
+
*/
|
|
19
|
+
import { LOCAL_TOOL_DEFINITIONS, executeLocalTool, isLocalTool, } from "./local-tools.js";
|
|
20
|
+
import { INTERACTIVE_TOOL_DEFINITIONS, executeInteractiveTool, } from "./interactive-tools.js";
|
|
21
|
+
import { loadConfig, resolveConfig, getProxyUrl } from "./config-store.js";
|
|
22
|
+
import { getValidToken, refreshSession } from "./auth-service.js";
|
|
23
|
+
import { isServerTool, loadServerToolDefinitions, executeServerTool, getServerStatus, setServerToolContext, } from "./server-tools.js";
|
|
24
|
+
import { nextTurn, createTurnContext, logSpan, generateSpanId, } from "./telemetry.js";
|
|
25
|
+
import { captureError, addBreadcrumb } from "./error-logger.js";
|
|
26
|
+
import { setGlobalEmitter, clearGlobalEmitter, } from "./agent-events.js";
|
|
27
|
+
import { mcpClientManager } from "./mcp-client.js";
|
|
28
|
+
import { loadHooks, runBeforeToolHook, runAfterToolHook, runSessionHook } from "./hooks.js";
|
|
29
|
+
import { LoopDetector, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, getCompactionConfig } from "../../shared/agent-core.js";
|
|
30
|
+
import { parseSSEStream, processStreamWithCallbacks, collectStreamResult } from "../../shared/sse-parser.js";
|
|
31
|
+
import { callServerProxy, callTranscribe, buildAPIRequest, buildSystemBlocks, prepareWithCaching, trimGeminiContext, trimOpenAIContext, requestProviderCompaction } from "../../shared/api-client.js";
|
|
32
|
+
import { getProvider, MODELS } from "../../shared/constants.js";
|
|
33
|
+
import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
|
|
34
|
+
// ── Extracted modules (re-exported below for backward compatibility) ──
|
|
35
|
+
import { loadMemory, addMemory, removeMemory, listMemories } from "./memory-manager.js";
|
|
36
|
+
import { refreshGitContext, resetGitContext } from "./git-context.js";
|
|
37
|
+
import { loadClaudeMd, reloadClaudeMd, resetClaudeMdCache } from "./claude-md-loader.js";
|
|
38
|
+
import { setPermissionMode, getPermissionMode, isToolAllowedByPermission } from "./permission-modes.js";
|
|
39
|
+
import { setModel, setModelById, getModel, getModelShortName, estimateCostUsd } from "./model-manager.js";
|
|
40
|
+
import { saveSession, loadSession, listSessions, findLatestSessionForCwd } from "./session-persistence.js";
|
|
41
|
+
import { buildSystemPrompt } from "./system-prompt.js";
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// RE-EXPORTS — all consumers keep importing from agent-loop.ts
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Memory
|
|
46
|
+
export { loadMemory, addMemory, removeMemory, listMemories };
|
|
47
|
+
// Git context
|
|
48
|
+
export { refreshGitContext };
|
|
49
|
+
// CLAUDE.md
|
|
50
|
+
export { loadClaudeMd, reloadClaudeMd };
|
|
51
|
+
// Permission modes
|
|
52
|
+
export { setPermissionMode, getPermissionMode, isToolAllowedByPermission };
|
|
53
|
+
// Model management
|
|
54
|
+
export { setModel, getModel, getModelShortName, estimateCostUsd };
|
|
55
|
+
// Session persistence
|
|
56
|
+
export { saveSession, loadSession, listSessions, findLatestSessionForCwd };
|
|
57
|
+
// Server status (pass-through)
|
|
58
|
+
export { getServerStatus };
|
|
59
|
+
// MCP client manager
|
|
60
|
+
export { mcpClientManager };
|
|
61
|
+
// Re-export background process listing for /tasks command
|
|
62
|
+
export { listProcesses, listBackgroundAgents } from "./background-processes.js";
|
|
63
|
+
// Re-export event emitter for ChatApp
|
|
64
|
+
export { AgentEventEmitter } from "./agent-events.js";
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// SESSION STATE
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// CLI-only: Session-wide token tracking (actual counts from API responses).
|
|
69
|
+
// Reset via resetSessionState() when starting a new conversation.
|
|
70
|
+
let sessionInputTokens = 0;
|
|
71
|
+
let sessionOutputTokens = 0;
|
|
72
|
+
export function getSessionTokens() {
|
|
73
|
+
return { input: sessionInputTokens, output: sessionOutputTokens };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reset all CLI-only session state. Call when starting a new conversation
|
|
77
|
+
* (e.g., /clear command, new print-mode run) to prevent stale token counts,
|
|
78
|
+
* loop detector state, and caches from leaking across sessions.
|
|
79
|
+
*
|
|
80
|
+
* Does NOT reset activeModel or permissionMode — those are user preferences
|
|
81
|
+
* that persist intentionally until explicitly changed.
|
|
82
|
+
*/
|
|
83
|
+
export function resetSessionState() {
|
|
84
|
+
sessionInputTokens = 0;
|
|
85
|
+
sessionOutputTokens = 0;
|
|
86
|
+
sessionLoopDetector = null;
|
|
87
|
+
resetGitContext();
|
|
88
|
+
resetClaudeMdCache();
|
|
89
|
+
}
|
|
90
|
+
/** CLI-only: loop detector — persists session error state across turns (reset by resetSessionState) */
|
|
91
|
+
let sessionLoopDetector = null;
|
|
92
|
+
const MAX_TURNS = 200; // Match Claude Code — effectively unlimited within a session
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// SHELL OUTPUT SUMMARIZATION
|
|
95
|
+
// ============================================================================
|
|
96
|
+
const SHELL_SUMMARIZE_LINE_THRESHOLD = 800;
|
|
97
|
+
const SHELL_SUMMARIZE_SIZE_THRESHOLD = 200_000; // 200KB — only summarize truly huge outputs
|
|
98
|
+
const SHELL_SUMMARIZE_MAX_INPUT = 300_000; // 300KB max to summarizer
|
|
99
|
+
const SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES = 20;
|
|
100
|
+
/**
|
|
101
|
+
* Check if shell output should be summarized based on line count or size.
|
|
102
|
+
*/
|
|
103
|
+
function shouldSummarizeShellOutput(output) {
|
|
104
|
+
if (output.length > SHELL_SUMMARIZE_SIZE_THRESHOLD)
|
|
105
|
+
return true;
|
|
106
|
+
const lineCount = output.split("\n").length;
|
|
107
|
+
return lineCount > SHELL_SUMMARIZE_LINE_THRESHOLD;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Summarize long shell output using Haiku via server proxy.
|
|
111
|
+
* Returns summarized output or original if summarization fails.
|
|
112
|
+
*/
|
|
113
|
+
async function summarizeShellOutput(output, proxyUrl, token, storeId) {
|
|
114
|
+
const lineCount = output.split("\n").length;
|
|
115
|
+
const truncatedForSummary = output.length > SHELL_SUMMARIZE_MAX_INPUT
|
|
116
|
+
? output.slice(0, SHELL_SUMMARIZE_MAX_INPUT) + "\n... (truncated for summarization)"
|
|
117
|
+
: output;
|
|
118
|
+
try {
|
|
119
|
+
const summaryConfig = buildAPIRequest({
|
|
120
|
+
model: MODELS.HAIKU,
|
|
121
|
+
contextProfile: "subagent",
|
|
122
|
+
});
|
|
123
|
+
const stream = await callServerProxy({
|
|
124
|
+
proxyUrl,
|
|
125
|
+
token,
|
|
126
|
+
model: MODELS.HAIKU,
|
|
127
|
+
system: [{ type: "text", text: "You are a concise technical summarizer. Summarize shell/command output preserving key information, errors, warnings, file paths, and actionable items. Be brief but thorough." }],
|
|
128
|
+
messages: [{ role: "user", content: `Summarize this shell output concisely, preserving key information, errors, and actionable items:\n\n${truncatedForSummary}` }],
|
|
129
|
+
tools: [],
|
|
130
|
+
apiConfig: summaryConfig,
|
|
131
|
+
storeId,
|
|
132
|
+
timeoutMs: 15_000,
|
|
133
|
+
});
|
|
134
|
+
const result = await collectStreamResult(parseSSEStream(stream));
|
|
135
|
+
const summary = result.text.trim();
|
|
136
|
+
if (!summary)
|
|
137
|
+
return output; // Summarization failed, return original
|
|
138
|
+
// Build first N lines preview
|
|
139
|
+
const originalLines = output.split("\n");
|
|
140
|
+
const preview = originalLines.slice(0, SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES).join("\n");
|
|
141
|
+
return `[Summarized from ${lineCount} lines]\n\n${summary}\n\n[First ${SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES} lines of original output]\n${preview}`;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Summarization failed silently — return original output
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Post-process tool results to summarize long bash output.
|
|
150
|
+
* Only affects bash tool results that exceed size/line thresholds.
|
|
151
|
+
*/
|
|
152
|
+
async function summarizeLongToolResults(toolResults, toolNames, proxyUrl, token, shellSummarization, storeId) {
|
|
153
|
+
if (!shellSummarization)
|
|
154
|
+
return toolResults;
|
|
155
|
+
const tasks = toolResults.map(async (result) => {
|
|
156
|
+
// Only summarize bash tool string results
|
|
157
|
+
const toolName = toolNames.get(result.tool_use_id);
|
|
158
|
+
if (toolName !== "bash" || typeof result.content !== "string")
|
|
159
|
+
return result;
|
|
160
|
+
// Check thresholds
|
|
161
|
+
if (!shouldSummarizeShellOutput(result.content))
|
|
162
|
+
return result;
|
|
163
|
+
const summarized = await summarizeShellOutput(result.content, proxyUrl, token, storeId);
|
|
164
|
+
return { ...result, content: summarized };
|
|
165
|
+
});
|
|
166
|
+
return Promise.all(tasks);
|
|
167
|
+
}
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// TOOL DEFINITIONS
|
|
170
|
+
// ============================================================================
|
|
171
|
+
async function getTools(allowedTools, disallowedTools) {
|
|
172
|
+
const localTools = LOCAL_TOOL_DEFINITIONS.map((t) => ({
|
|
173
|
+
name: t.name,
|
|
174
|
+
description: t.description,
|
|
175
|
+
input_schema: t.input_schema,
|
|
176
|
+
}));
|
|
177
|
+
// Add interactive tools (ask_user_question, enter_plan_mode, exit_plan_mode)
|
|
178
|
+
const interactiveTools = INTERACTIVE_TOOL_DEFINITIONS.map((t) => ({
|
|
179
|
+
name: t.name,
|
|
180
|
+
description: t.description,
|
|
181
|
+
input_schema: t.input_schema,
|
|
182
|
+
}));
|
|
183
|
+
localTools.push(...interactiveTools);
|
|
184
|
+
let serverTools = [];
|
|
185
|
+
try {
|
|
186
|
+
serverTools = await loadServerToolDefinitions();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Server tools silently unavailable
|
|
190
|
+
}
|
|
191
|
+
// Deduplicate: local tools take priority over server tools with the same name
|
|
192
|
+
const localNames = new Set(localTools.map(t => t.name));
|
|
193
|
+
const uniqueServerTools = serverTools.filter(t => !localNames.has(t.name));
|
|
194
|
+
// MCP tools from external servers
|
|
195
|
+
const mcpTools = mcpClientManager.getTools();
|
|
196
|
+
let allTools = [...localTools, ...uniqueServerTools, ...mcpTools];
|
|
197
|
+
// Apply tool filtering
|
|
198
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
199
|
+
const allowed = new Set(allowedTools);
|
|
200
|
+
allTools = allTools.filter(t => allowed.has(t.name));
|
|
201
|
+
}
|
|
202
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
203
|
+
const disallowed = new Set(disallowedTools);
|
|
204
|
+
allTools = allTools.filter(t => !disallowed.has(t.name));
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
tools: allTools,
|
|
208
|
+
serverToolCount: uniqueServerTools.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Exposed for /status command */
|
|
212
|
+
export async function getServerToolCount() {
|
|
213
|
+
try {
|
|
214
|
+
const defs = await loadServerToolDefinitions();
|
|
215
|
+
return defs.length;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// MAIN LOOP
|
|
223
|
+
// ============================================================================
|
|
224
|
+
export async function runAgentLoop(opts) {
|
|
225
|
+
const { message, conversationHistory, callbacks, abortSignal, emitter } = opts;
|
|
226
|
+
if (opts.model)
|
|
227
|
+
setModel(opts.model);
|
|
228
|
+
// Set global emitter for subagents to use
|
|
229
|
+
if (emitter) {
|
|
230
|
+
setGlobalEmitter(emitter);
|
|
231
|
+
}
|
|
232
|
+
const effectiveMaxTurns = opts.maxTurns || MAX_TURNS;
|
|
233
|
+
// Load hooks from project and user config
|
|
234
|
+
const hooksCwd = opts.cwd || process.cwd();
|
|
235
|
+
const hooks = loadHooks(hooksCwd);
|
|
236
|
+
// Fire SessionStart hook (non-blocking)
|
|
237
|
+
if (hooks.length > 0) {
|
|
238
|
+
runSessionHook(hooks, "SessionStart", { session_id: `turn-${Date.now()}` }).catch(() => { });
|
|
239
|
+
}
|
|
240
|
+
// Shell summarization config (default: true)
|
|
241
|
+
const shellSummarization = opts.shellSummarization !== false;
|
|
242
|
+
const { tools, serverToolCount } = await getTools(opts.allowedTools, opts.disallowedTools);
|
|
243
|
+
const systemPrompt = await buildSystemPrompt(serverToolCount > 0, opts.effort);
|
|
244
|
+
// Build user content — text-only string or content blocks array with images
|
|
245
|
+
let userContent;
|
|
246
|
+
if (opts.images && opts.images.length > 0) {
|
|
247
|
+
const blocks = [];
|
|
248
|
+
for (const img of opts.images) {
|
|
249
|
+
blocks.push({
|
|
250
|
+
type: "image",
|
|
251
|
+
source: {
|
|
252
|
+
type: "base64",
|
|
253
|
+
media_type: img.mediaType,
|
|
254
|
+
data: img.base64,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
blocks.push({ type: "text", text: message || "(see attached images)" });
|
|
259
|
+
userContent = blocks;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
userContent = message;
|
|
263
|
+
}
|
|
264
|
+
const messages = [
|
|
265
|
+
...conversationHistory,
|
|
266
|
+
{ role: "user", content: userContent },
|
|
267
|
+
];
|
|
268
|
+
// Session-level loop detector: persists failed strategies across turns.
|
|
269
|
+
// Created once per conversation, reset only when user starts a new conversation.
|
|
270
|
+
if (!sessionLoopDetector || conversationHistory.length === 0) {
|
|
271
|
+
sessionLoopDetector = new LoopDetector();
|
|
272
|
+
}
|
|
273
|
+
const loopDetector = sessionLoopDetector;
|
|
274
|
+
loopDetector.resetTurn();
|
|
275
|
+
let totalIn = 0;
|
|
276
|
+
let totalOut = 0;
|
|
277
|
+
let totalCacheCreation = 0;
|
|
278
|
+
let totalCacheRead = 0;
|
|
279
|
+
let totalThinking = 0;
|
|
280
|
+
let allAssistantText = [];
|
|
281
|
+
let prevIterationInputTokens = 0; // Tracks actual context size from last API call
|
|
282
|
+
// Telemetry: one turn per user message (not per API call)
|
|
283
|
+
const sessionStart = Date.now();
|
|
284
|
+
const { storeId } = resolveConfig();
|
|
285
|
+
const turnNum = nextTurn(); // ONCE per user message
|
|
286
|
+
const turnCtx = createTurnContext({ model: getModel(), turnNumber: turnNum });
|
|
287
|
+
addBreadcrumb("agent", `Turn ${turnNum}: ${message.slice(0, 100)}`, "info");
|
|
288
|
+
// Set server tool context so tool calls carry trace/user identity to Fly.io server
|
|
289
|
+
setServerToolContext({
|
|
290
|
+
traceId: turnCtx.traceId,
|
|
291
|
+
conversationId: turnCtx.conversationId,
|
|
292
|
+
userId: turnCtx.userId,
|
|
293
|
+
userEmail: turnCtx.userEmail,
|
|
294
|
+
source: "whale-code",
|
|
295
|
+
});
|
|
296
|
+
logSpan({
|
|
297
|
+
action: "chat.user_message",
|
|
298
|
+
durationMs: 0,
|
|
299
|
+
context: turnCtx,
|
|
300
|
+
storeId: storeId || undefined,
|
|
301
|
+
details: {
|
|
302
|
+
message: message,
|
|
303
|
+
conversation_history_length: conversationHistory.length,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
let sessionCostUsd = 0;
|
|
307
|
+
let compactionCount = 0;
|
|
308
|
+
const activeModel = getModel();
|
|
309
|
+
// Tool executor — routes to interactive, local, server, or MCP tools.
|
|
310
|
+
// Wraps execution with before/after hooks when hooks are loaded.
|
|
311
|
+
const INTERACTIVE_TOOL_NAMES = new Set(INTERACTIVE_TOOL_DEFINITIONS.map(t => t.name));
|
|
312
|
+
const toolExecutor = async (name, input) => {
|
|
313
|
+
if (!name) {
|
|
314
|
+
return { success: false, output: "Tool call missing name — skipping." };
|
|
315
|
+
}
|
|
316
|
+
// Permission mode enforcement
|
|
317
|
+
if (!isToolAllowedByPermission(name)) {
|
|
318
|
+
return { success: false, output: `Tool "${name}" blocked by ${getPermissionMode()} mode. Switch modes with /mode.` };
|
|
319
|
+
}
|
|
320
|
+
// BeforeTool hook — may block or modify input
|
|
321
|
+
let effectiveInput = input;
|
|
322
|
+
if (hooks.length > 0) {
|
|
323
|
+
const hookResult = await runBeforeToolHook(hooks, name, input);
|
|
324
|
+
if (!hookResult.allow) {
|
|
325
|
+
return { success: false, output: hookResult.message || "Blocked by hook" };
|
|
326
|
+
}
|
|
327
|
+
if (hookResult.modifiedInput) {
|
|
328
|
+
effectiveInput = hookResult.modifiedInput;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
let result;
|
|
332
|
+
if (INTERACTIVE_TOOL_NAMES.has(name)) {
|
|
333
|
+
result = await executeInteractiveTool(name, effectiveInput);
|
|
334
|
+
}
|
|
335
|
+
else if (isLocalTool(name)) {
|
|
336
|
+
result = await executeLocalTool(name, effectiveInput);
|
|
337
|
+
}
|
|
338
|
+
else if (isServerTool(name)) {
|
|
339
|
+
result = await executeServerTool(name, effectiveInput, emitter);
|
|
340
|
+
}
|
|
341
|
+
else if (mcpClientManager.isMcpTool(name)) {
|
|
342
|
+
result = await mcpClientManager.callTool(name, effectiveInput);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
result = { success: false, output: `Unknown tool: ${name}` };
|
|
346
|
+
}
|
|
347
|
+
// AfterTool hook — may modify output
|
|
348
|
+
if (hooks.length > 0) {
|
|
349
|
+
const afterResult = await runAfterToolHook(hooks, name, result.output, result.success);
|
|
350
|
+
if (afterResult.modifiedOutput !== undefined) {
|
|
351
|
+
result = { ...result, output: afterResult.modifiedOutput };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
};
|
|
356
|
+
// Deprecated: Anthropic context_management handles limits via clear_tool_uses + compaction.
|
|
357
|
+
// tool-dispatch.ts uses SAFETY_MAX_CHARS (500K) — this value is ignored.
|
|
358
|
+
const maxResultChars = undefined;
|
|
359
|
+
try {
|
|
360
|
+
for (let iteration = 0; iteration < effectiveMaxTurns; iteration++) {
|
|
361
|
+
if (abortSignal?.aborted) {
|
|
362
|
+
logSpan({ action: "chat.cancelled", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, details: { iteration, reason: "user_abort" } });
|
|
363
|
+
callbacks.onError("Cancelled", messages);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Budget enforcement
|
|
367
|
+
if (opts.maxBudgetUsd && sessionCostUsd >= opts.maxBudgetUsd) {
|
|
368
|
+
logSpan({ action: "chat.budget_exceeded", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { session_cost_usd: sessionCostUsd, max_budget_usd: opts.maxBudgetUsd, iteration } });
|
|
369
|
+
callbacks.onError(`Budget exceeded: $${sessionCostUsd.toFixed(4)} >= $${opts.maxBudgetUsd}`, messages);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const apiStart = Date.now();
|
|
373
|
+
const apiSpanId = generateSpanId();
|
|
374
|
+
const apiRowId = crypto.randomUUID(); // UUID for this span's row — children reference via parent_id
|
|
375
|
+
const costContext = `Session cost: $${sessionCostUsd.toFixed(2)}${opts.maxBudgetUsd ? ` | Budget remaining: $${(opts.maxBudgetUsd - sessionCostUsd).toFixed(2)}` : ""}`;
|
|
376
|
+
// Build API request config
|
|
377
|
+
const currentModel = getModel();
|
|
378
|
+
const apiConfig = buildAPIRequest({
|
|
379
|
+
model: currentModel,
|
|
380
|
+
contextProfile: "main",
|
|
381
|
+
thinkingEnabled: opts.thinking,
|
|
382
|
+
});
|
|
383
|
+
// Prepare with prompt caching
|
|
384
|
+
let { tools: cachedTools, messages: cachedMessages } = prepareWithCaching(tools, messages);
|
|
385
|
+
const system = buildSystemBlocks(systemPrompt, costContext);
|
|
386
|
+
// Client-side context trimming for non-Anthropic providers.
|
|
387
|
+
// Uses prevIterationInputTokens (actual context size from last API call) — NOT
|
|
388
|
+
// cumulative sessionInputTokens, which grows quadratically and would trigger too early.
|
|
389
|
+
const provider = getProvider(currentModel);
|
|
390
|
+
if (provider === "gemini" || provider === "openai") {
|
|
391
|
+
const preTrimMessages = cachedMessages;
|
|
392
|
+
if (provider === "gemini") {
|
|
393
|
+
// Emergency fallback only — compaction fires at 700K first; trim at 950K catches failures
|
|
394
|
+
cachedMessages = trimGeminiContext(cachedMessages, prevIterationInputTokens);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Emergency fallback only — compaction fires at 120K first; trim at 190K catches failures
|
|
398
|
+
// GPT-4o: 128K context → 96K threshold (no compaction headroom)
|
|
399
|
+
const threshold = currentModel === "gpt-4o" ? 96_000 : 190_000;
|
|
400
|
+
cachedMessages = trimOpenAIContext(cachedMessages, prevIterationInputTokens, threshold);
|
|
401
|
+
}
|
|
402
|
+
// Notify UI when trimming actually occurred (trim returns same ref if no-op)
|
|
403
|
+
if (cachedMessages !== preTrimMessages) {
|
|
404
|
+
// Count tool results before/after to report meaningful numbers
|
|
405
|
+
const countToolResults = (msgs) => msgs.reduce((sum, m) => sum + (Array.isArray(m.content)
|
|
406
|
+
? m.content.filter(b => b.type === "tool_result" && b.content !== "[trimmed]").length
|
|
407
|
+
: 0), 0);
|
|
408
|
+
const activeBefore = countToolResults(preTrimMessages);
|
|
409
|
+
const activeAfter = countToolResults(cachedMessages);
|
|
410
|
+
const estimatedSaved = Math.round(prevIterationInputTokens * ((activeBefore - activeAfter) / Math.max(activeBefore, 1)));
|
|
411
|
+
callbacks.onAutoCompact?.(activeBefore, activeAfter, estimatedSaved);
|
|
412
|
+
emitter?.emitCompact(activeBefore, activeAfter, estimatedSaved);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Get auth token
|
|
416
|
+
const token = await getValidToken();
|
|
417
|
+
if (!token) {
|
|
418
|
+
throw new Error("Not logged in. Run: whale login");
|
|
419
|
+
}
|
|
420
|
+
// Call server proxy with built-in retry
|
|
421
|
+
const originalModel = currentModel;
|
|
422
|
+
const stream = await callServerProxy({
|
|
423
|
+
proxyUrl: getProxyUrl(),
|
|
424
|
+
token,
|
|
425
|
+
model: currentModel,
|
|
426
|
+
system,
|
|
427
|
+
messages: cachedMessages,
|
|
428
|
+
tools: cachedTools,
|
|
429
|
+
apiConfig,
|
|
430
|
+
signal: abortSignal,
|
|
431
|
+
fallbackModel: opts.fallbackModel,
|
|
432
|
+
storeId: storeId || undefined,
|
|
433
|
+
onFallback: (from, to) => {
|
|
434
|
+
setModel(to);
|
|
435
|
+
logSpan({ action: "claude_api_fallback", durationMs: 0, context: { ...turnCtx, spanId: apiSpanId }, storeId: storeId || undefined, details: { from_model: from, to_model: to } });
|
|
436
|
+
},
|
|
437
|
+
onRetry: (attempt, max, err) => {
|
|
438
|
+
const msg = `\n\x1b[33m\u21BB Retrying (${attempt}/${max})... ${err.slice(0, 80)}\x1b[0m\n`;
|
|
439
|
+
if (emitter) {
|
|
440
|
+
emitter.emitText(msg);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
callbacks.onText(msg);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
onTokenRefresh: async () => {
|
|
447
|
+
const result = await refreshSession();
|
|
448
|
+
return result.success ? result.config.access_token : null;
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
// Process stream events with UI callbacks
|
|
452
|
+
const result = await processStreamWithCallbacks(parseSSEStream(stream, abortSignal), {
|
|
453
|
+
onText: (text) => {
|
|
454
|
+
if (emitter) {
|
|
455
|
+
emitter.emitText(text);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
callbacks.onText(text);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
onToolStart: (name, input) => {
|
|
462
|
+
// NOTE: Do NOT call callbacks.onToolStart here — dispatchTools.onStart
|
|
463
|
+
// fires it once per tool at execution time. Calling it here too would
|
|
464
|
+
// send duplicate tool_start events (stale spinners in WhaleChat).
|
|
465
|
+
if (input) {
|
|
466
|
+
// Tool block complete — emit structured start for CLI TUI only
|
|
467
|
+
emitter?.emitToolStart("", name);
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
}, abortSignal);
|
|
471
|
+
// Flush buffered text
|
|
472
|
+
emitter?.flushText();
|
|
473
|
+
// Restore original model after transient fallback
|
|
474
|
+
if (getModel() !== originalModel && opts.fallbackModel) {
|
|
475
|
+
setModelById(originalModel);
|
|
476
|
+
}
|
|
477
|
+
// Update session token tracking
|
|
478
|
+
sessionInputTokens += result.usage.inputTokens;
|
|
479
|
+
sessionOutputTokens += result.usage.outputTokens;
|
|
480
|
+
prevIterationInputTokens = result.usage.inputTokens; // Actual context size for next trim check
|
|
481
|
+
// Emit usage with model + cost context for all providers
|
|
482
|
+
if (emitter && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
|
|
483
|
+
const iterCost = estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
484
|
+
emitter.emitUsage(result.usage.inputTokens, result.usage.outputTokens, currentModel, iterCost, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
485
|
+
}
|
|
486
|
+
totalIn += result.usage.inputTokens;
|
|
487
|
+
totalOut += result.usage.outputTokens;
|
|
488
|
+
totalCacheCreation += result.usage.cacheCreationTokens;
|
|
489
|
+
totalCacheRead += result.usage.cacheReadTokens;
|
|
490
|
+
totalThinking += result.thinkingTokens;
|
|
491
|
+
sessionCostUsd += estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
492
|
+
// Server-side context management notification
|
|
493
|
+
if (result.contextManagementApplied) {
|
|
494
|
+
callbacks.onAutoCompact?.(messages.length, messages.length, 0);
|
|
495
|
+
emitter?.emitCompact(messages.length, messages.length, 0);
|
|
496
|
+
logSpan({ action: "chat.api_compaction", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { type: "server_side", has_compaction_content: result.compactionContent !== null, iteration } });
|
|
497
|
+
}
|
|
498
|
+
if (result.text)
|
|
499
|
+
allAssistantText.push(result.text);
|
|
500
|
+
// Telemetry: API call span
|
|
501
|
+
const iterCostUsd = estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
502
|
+
logSpan({
|
|
503
|
+
action: "claude_api_request",
|
|
504
|
+
durationMs: Date.now() - apiStart,
|
|
505
|
+
context: { ...turnCtx, spanId: apiSpanId, rowId: apiRowId, inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens, totalCost: iterCostUsd, model: currentModel },
|
|
506
|
+
storeId: storeId || undefined,
|
|
507
|
+
details: {
|
|
508
|
+
"gen_ai.request.model": currentModel,
|
|
509
|
+
"gen_ai.usage.input_tokens": result.usage.inputTokens,
|
|
510
|
+
"gen_ai.usage.output_tokens": result.usage.outputTokens,
|
|
511
|
+
"gen_ai.usage.cache_creation_tokens": result.usage.cacheCreationTokens,
|
|
512
|
+
"gen_ai.usage.cache_read_tokens": result.usage.cacheReadTokens,
|
|
513
|
+
"gen_ai.usage.cost": iterCostUsd,
|
|
514
|
+
stop_reason: result.stopReason === "compaction" ? "compaction" : result.toolUseBlocks.length > 0 ? "tool_use" : "end_turn",
|
|
515
|
+
iteration,
|
|
516
|
+
tool_count: result.toolUseBlocks.length,
|
|
517
|
+
tool_names: result.toolUseBlocks.map(t => t.name),
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
// Compaction handling — API paused after generating summary.
|
|
521
|
+
// Preserve last 2 messages for continuity, then resume.
|
|
522
|
+
if (result.stopReason === "compaction" && result.compactionContent) {
|
|
523
|
+
compactionCount++;
|
|
524
|
+
logSpan({ action: "chat.compaction_pause", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, messages_before: messages.length } });
|
|
525
|
+
// Budget enforcement
|
|
526
|
+
if (compactionCount * COMPACTION_TRIGGER_TOKENS >= COMPACTION_TOTAL_BUDGET) {
|
|
527
|
+
const budgetMsg = "\n[Context budget reached — wrapping up.]";
|
|
528
|
+
if (emitter) {
|
|
529
|
+
emitter.emitText(budgetMsg);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
callbacks.onText(budgetMsg);
|
|
533
|
+
}
|
|
534
|
+
const compactedMessages = [
|
|
535
|
+
{ role: "assistant", content: [{ type: "compaction", content: result.compactionContent }] },
|
|
536
|
+
{ role: "user", content: [{ type: "text", text: "You have reached the context budget. Please wrap up your current work and provide a final summary of what was accomplished and what remains." }] },
|
|
537
|
+
];
|
|
538
|
+
messages.length = 0;
|
|
539
|
+
messages.push(...compactedMessages);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
// Normal compaction: preserve last 2 messages for continuity
|
|
543
|
+
const preserved = messages.slice(-2);
|
|
544
|
+
const compactedMessages = [
|
|
545
|
+
{ role: "assistant", content: [{ type: "compaction", content: result.compactionContent }] },
|
|
546
|
+
...preserved,
|
|
547
|
+
];
|
|
548
|
+
messages.length = 0;
|
|
549
|
+
messages.push(...compactedMessages);
|
|
550
|
+
iteration--; // Don't count compaction as an iteration
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
// No tool calls — check if we should continue or stop
|
|
554
|
+
if (result.toolUseBlocks.length === 0) {
|
|
555
|
+
// If model hit max_tokens, it was truncated mid-response — continue so it can finish
|
|
556
|
+
if (result.stopReason === "max_tokens") {
|
|
557
|
+
const truncatedText = result.text || "";
|
|
558
|
+
const assistantContent = buildAssistantContent({
|
|
559
|
+
text: truncatedText,
|
|
560
|
+
toolUseBlocks: [],
|
|
561
|
+
thinkingBlocks: result.thinkingBlocks,
|
|
562
|
+
compactionContent: result.compactionContent,
|
|
563
|
+
});
|
|
564
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
565
|
+
messages.push({ role: "user", content: [{ type: "text", text: "[Your response was truncated due to length. Please continue where you left off.]" }] });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
// Execute tools via shared dispatch
|
|
571
|
+
const { results: toolResults, bailOut, bailMessage } = await dispatchTools(result.toolUseBlocks, toolExecutor, {
|
|
572
|
+
loopDetector,
|
|
573
|
+
maxConcurrent: 7,
|
|
574
|
+
maxResultChars,
|
|
575
|
+
onStart: (name, input) => {
|
|
576
|
+
callbacks.onToolStart(name, input);
|
|
577
|
+
},
|
|
578
|
+
onResult: (name, success, output, durationMs) => {
|
|
579
|
+
callbacks.onToolResult(name, success, output, undefined, durationMs);
|
|
580
|
+
logSpan({
|
|
581
|
+
action: `tool.${name}`,
|
|
582
|
+
durationMs,
|
|
583
|
+
context: { ...turnCtx, spanId: generateSpanId(), parentSpanId: apiSpanId, parentId: apiRowId },
|
|
584
|
+
storeId: storeId || undefined,
|
|
585
|
+
error: success ? undefined : output,
|
|
586
|
+
details: {
|
|
587
|
+
tool_input: {},
|
|
588
|
+
tool_result: truncateResult(output, 2000),
|
|
589
|
+
error_type: success ? undefined : classifyToolError(output),
|
|
590
|
+
iteration,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
signal: abortSignal,
|
|
595
|
+
transcribeAudio: storeId
|
|
596
|
+
? async (base64, mediaType) => callTranscribe({
|
|
597
|
+
proxyUrl: getProxyUrl(),
|
|
598
|
+
token,
|
|
599
|
+
storeId: storeId,
|
|
600
|
+
audioBase64: base64,
|
|
601
|
+
mediaType,
|
|
602
|
+
})
|
|
603
|
+
: undefined,
|
|
604
|
+
});
|
|
605
|
+
if (bailOut) {
|
|
606
|
+
logSpan({ action: "chat.bail_out", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { ...loopDetector.getSessionStats(), message: bailMessage, iteration } });
|
|
607
|
+
}
|
|
608
|
+
// Shell output summarization — summarize long bash output to save context window
|
|
609
|
+
const toolNameMap = new Map(result.toolUseBlocks.map(t => [t.id, t.name]));
|
|
610
|
+
const finalToolResults = await summarizeLongToolResults(toolResults, toolNameMap, getProxyUrl(), token, shellSummarization, storeId || undefined);
|
|
611
|
+
// Build assistant content and append to conversation
|
|
612
|
+
const assistantContent = buildAssistantContent({
|
|
613
|
+
text: result.text,
|
|
614
|
+
toolUseBlocks: result.toolUseBlocks,
|
|
615
|
+
thinkingBlocks: result.thinkingBlocks,
|
|
616
|
+
compactionContent: result.compactionContent,
|
|
617
|
+
});
|
|
618
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
619
|
+
messages.push({ role: "user", content: finalToolResults });
|
|
620
|
+
// Non-native compaction for OpenAI/Gemini — fires after tool results appended
|
|
621
|
+
const compactionCfg = getCompactionConfig(currentModel);
|
|
622
|
+
if (!compactionCfg.isNative && result.usage.inputTokens >= compactionCfg.triggerTokens) {
|
|
623
|
+
compactionCount++;
|
|
624
|
+
if (compactionCount * compactionCfg.triggerTokens >= compactionCfg.totalBudget) {
|
|
625
|
+
// Budget exhaustion — force wrap-up (same as native compaction budget logic)
|
|
626
|
+
const budgetMsg = "\n[Context budget reached — wrapping up.]";
|
|
627
|
+
if (emitter) {
|
|
628
|
+
emitter.emitText(budgetMsg);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
callbacks.onText(budgetMsg);
|
|
632
|
+
}
|
|
633
|
+
const summary = await requestProviderCompaction({
|
|
634
|
+
proxyUrl: getProxyUrl(),
|
|
635
|
+
token,
|
|
636
|
+
messages: messages,
|
|
637
|
+
systemPrompt,
|
|
638
|
+
});
|
|
639
|
+
const compactedMessages = [
|
|
640
|
+
...(summary
|
|
641
|
+
? [{ role: "assistant", content: [{ type: "compaction", content: summary }] }]
|
|
642
|
+
: []),
|
|
643
|
+
{ role: "user", content: [{ type: "text", text: "You have reached the context budget. Please wrap up your current work and provide a final summary of what was accomplished and what remains." }] },
|
|
644
|
+
];
|
|
645
|
+
messages.length = 0;
|
|
646
|
+
messages.push(...compactedMessages);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
// Normal compaction — summarize and preserve last 2 messages
|
|
650
|
+
const summary = await requestProviderCompaction({
|
|
651
|
+
proxyUrl: getProxyUrl(),
|
|
652
|
+
token,
|
|
653
|
+
messages: messages,
|
|
654
|
+
systemPrompt,
|
|
655
|
+
});
|
|
656
|
+
if (summary) {
|
|
657
|
+
const preserved = messages.slice(-2);
|
|
658
|
+
const compactedMessages = [
|
|
659
|
+
{ role: "assistant", content: [{ type: "compaction", content: summary }] },
|
|
660
|
+
...preserved,
|
|
661
|
+
];
|
|
662
|
+
messages.length = 0;
|
|
663
|
+
messages.push(...compactedMessages);
|
|
664
|
+
iteration--; // Don't count compaction as an iteration
|
|
665
|
+
callbacks.onAutoCompact?.(messages.length + preserved.length, messages.length, Math.round(result.usage.inputTokens * 0.7));
|
|
666
|
+
emitter?.emitCompact(messages.length + preserved.length, messages.length, Math.round(result.usage.inputTokens * 0.7));
|
|
667
|
+
logSpan({ action: "chat.provider_compaction", durationMs: 0, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, provider, model: currentModel, input_tokens: result.usage.inputTokens, trigger_tokens: compactionCfg.triggerTokens } });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Telemetry: session summary
|
|
672
|
+
logSpan({
|
|
673
|
+
action: "chat.session_complete",
|
|
674
|
+
durationMs: Date.now() - sessionStart,
|
|
675
|
+
context: { ...turnCtx, inputTokens: totalIn, outputTokens: totalOut, model: activeModel },
|
|
676
|
+
storeId: storeId || undefined,
|
|
677
|
+
details: {
|
|
678
|
+
input_tokens: totalIn, output_tokens: totalOut, total_tokens: totalIn + totalOut,
|
|
679
|
+
cache_creation_tokens: totalCacheCreation, cache_read_tokens: totalCacheRead,
|
|
680
|
+
session_input_tokens: sessionInputTokens, session_output_tokens: sessionOutputTokens,
|
|
681
|
+
model: activeModel,
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
const turnCostUsd = estimateCostUsd(totalIn, totalOut, activeModel, totalThinking, totalCacheRead, totalCacheCreation);
|
|
685
|
+
callbacks.onUsage(totalIn, totalOut, totalThinking, activeModel, turnCostUsd, totalCacheRead, totalCacheCreation);
|
|
686
|
+
// Fire SessionEnd hook (non-blocking)
|
|
687
|
+
if (hooks.length > 0) {
|
|
688
|
+
runSessionHook(hooks, "SessionEnd", { session_id: `turn-${sessionStart}` }).catch(() => { });
|
|
689
|
+
}
|
|
690
|
+
const finalText = allAssistantText.length > 0 ? allAssistantText[allAssistantText.length - 1] : "";
|
|
691
|
+
emitter?.emitDone(finalText, messages);
|
|
692
|
+
if (emitter)
|
|
693
|
+
clearGlobalEmitter();
|
|
694
|
+
callbacks.onDone(messages);
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
const errorMsg = abortSignal?.aborted || err?.message === "Cancelled"
|
|
698
|
+
? "Cancelled"
|
|
699
|
+
: String(err?.message || err);
|
|
700
|
+
logSpan({
|
|
701
|
+
action: errorMsg === "Cancelled" ? "chat.cancelled" : "chat.fatal_error",
|
|
702
|
+
durationMs: Date.now() - sessionStart,
|
|
703
|
+
context: { ...turnCtx, inputTokens: totalIn, outputTokens: totalOut, model: activeModel },
|
|
704
|
+
storeId: storeId || undefined,
|
|
705
|
+
severity: errorMsg === "Cancelled" ? "info" : "error",
|
|
706
|
+
error: errorMsg === "Cancelled" ? undefined : errorMsg,
|
|
707
|
+
details: { input_tokens: totalIn, output_tokens: totalOut, session_cost_usd: sessionCostUsd, model: activeModel },
|
|
708
|
+
});
|
|
709
|
+
// Capture to error_events (not just audit_logs) for non-cancellations
|
|
710
|
+
if (errorMsg !== "Cancelled") {
|
|
711
|
+
captureError({
|
|
712
|
+
error: err instanceof Error ? err : undefined,
|
|
713
|
+
errorType: "AgentLoopError",
|
|
714
|
+
errorMessage: errorMsg,
|
|
715
|
+
severity: "error",
|
|
716
|
+
traceId: turnCtx.traceId,
|
|
717
|
+
spanId: turnCtx.spanId,
|
|
718
|
+
storeId: storeId || undefined,
|
|
719
|
+
tags: { model: activeModel, turn: String(turnNum) },
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
emitter?.emitError(errorMsg);
|
|
723
|
+
if (emitter)
|
|
724
|
+
clearGlobalEmitter();
|
|
725
|
+
callbacks.onError(errorMsg, messages);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// ============================================================================
|
|
729
|
+
// TELEMETRY HELPERS
|
|
730
|
+
// ============================================================================
|
|
731
|
+
export function truncateResult(output, maxLen) {
|
|
732
|
+
if (output.length <= maxLen)
|
|
733
|
+
return output;
|
|
734
|
+
return output.slice(0, maxLen) + `... (${output.length} chars total)`;
|
|
735
|
+
}
|
|
736
|
+
export function classifyToolError(output) {
|
|
737
|
+
const lower = output.toLowerCase();
|
|
738
|
+
if (lower.includes("timed out") || lower.includes("timeout"))
|
|
739
|
+
return "timeout";
|
|
740
|
+
if (lower.includes("permission denied") || lower.includes("eacces"))
|
|
741
|
+
return "permission";
|
|
742
|
+
if (lower.includes("not found") || lower.includes("no such file"))
|
|
743
|
+
return "not_found";
|
|
744
|
+
if (lower.includes("command not found") || lower.includes("exit code 127"))
|
|
745
|
+
return "command_not_found";
|
|
746
|
+
if (lower.includes("import") && lower.includes("error"))
|
|
747
|
+
return "import_error";
|
|
748
|
+
if (lower.includes("syntax") || lower.includes("parse"))
|
|
749
|
+
return "syntax_error";
|
|
750
|
+
if (lower.includes("externally-managed"))
|
|
751
|
+
return "env_managed";
|
|
752
|
+
return "unknown";
|
|
753
|
+
}
|
|
754
|
+
// Convenience: check if user can use the agent (logged in OR has API key)
|
|
755
|
+
export function canUseAgent() {
|
|
756
|
+
const config = loadConfig();
|
|
757
|
+
const hasToken = !!(config.access_token && config.refresh_token);
|
|
758
|
+
const hasApiKey = !!(process.env.ANTHROPIC_API_KEY || config.anthropic_api_key);
|
|
759
|
+
if (hasToken || hasApiKey)
|
|
760
|
+
return { ready: true };
|
|
761
|
+
return { ready: false, reason: "Run `whale login` to authenticate." };
|
|
762
|
+
}
|