whale-code 6.4.0 → 6.5.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/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
|
@@ -13,6 +13,7 @@ import { processStreamWithCallbacks } from "../../shared/sse-parser.js";
|
|
|
13
13
|
import { MODELS } from "../../shared/constants.js";
|
|
14
14
|
import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
|
|
15
15
|
import { getCachedToolDefs, getFullToolSchemas } from "../tool-router.js";
|
|
16
|
+
import { queueSpan, auditRowToSpan } from "./clickhouse-buffer.js";
|
|
16
17
|
import { DELEGATE_TASK_TOOL_DEF, runServerSubagent, } from "./server-subagent.js";
|
|
17
18
|
import { handleTranscribe } from "../handlers/transcription.js";
|
|
18
19
|
import { preCompact } from "./compaction-service.js";
|
|
@@ -49,7 +50,7 @@ function mapToolChoiceForAnthropic(tc) {
|
|
|
49
50
|
// UNIFIED AGENT LOOP
|
|
50
51
|
// ============================================================================
|
|
51
52
|
export async function runServerAgentLoop(opts) {
|
|
52
|
-
const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd = DEFAULT_SESSION_COST_BUDGET_USD, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs =
|
|
53
|
+
const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd = DEFAULT_SESSION_COST_BUDGET_USD, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
|
|
53
54
|
// Auto-inject delegate_task for all models (subagents always use Claude Haiku/Sonnet)
|
|
54
55
|
// activeTools is mutable — discover_tools adds to it during the session
|
|
55
56
|
const activeTools = [...inputTools];
|
|
@@ -92,10 +93,12 @@ export async function runServerAgentLoop(opts) {
|
|
|
92
93
|
let sessionCostUsd = 0;
|
|
93
94
|
let compactionCount = 0;
|
|
94
95
|
let finalResponse = "";
|
|
96
|
+
let lastStopReason = "end_turn";
|
|
95
97
|
const allTextResponses = [];
|
|
96
98
|
const allToolNames = [];
|
|
97
99
|
const allCitations = [];
|
|
98
100
|
const turnMetrics = [];
|
|
101
|
+
const costWarningsEmitted = new Set();
|
|
99
102
|
while (turnCount < maxTurns) {
|
|
100
103
|
// Abort checks
|
|
101
104
|
if (clientDisconnected.value) {
|
|
@@ -164,13 +167,17 @@ export async function runServerAgentLoop(opts) {
|
|
|
164
167
|
];
|
|
165
168
|
// Resolve tool_choice for this turn
|
|
166
169
|
const recentToolUses = turnMetrics.slice(-3).flatMap(t => t.toolsUsed);
|
|
167
|
-
|
|
170
|
+
let resolvedToolChoice = resolveToolChoice({
|
|
168
171
|
toolChoice: opts.toolChoice,
|
|
169
172
|
turnCount,
|
|
170
173
|
recentToolUses,
|
|
171
174
|
availableToolNames: tools.map(t => t.name),
|
|
172
175
|
userMessage: firstUserText,
|
|
173
176
|
});
|
|
177
|
+
// Anthropic API: forced tool_choice ("any" or specific tool) is incompatible with thinking — downgrade to "auto"
|
|
178
|
+
if (thinkingCfg.thinking.type !== "disabled" && resolvedToolChoice !== "auto" && resolvedToolChoice !== "none") {
|
|
179
|
+
resolvedToolChoice = "auto";
|
|
180
|
+
}
|
|
174
181
|
const { toolChoice: anthropicToolChoice, omitTools } = mapToolChoiceForAnthropic(resolvedToolChoice);
|
|
175
182
|
if (omitTools) {
|
|
176
183
|
log.info({ turn: turnCount, resolvedToolChoice }, "tool_choice=none — omitting tools");
|
|
@@ -254,6 +261,15 @@ export async function runServerAgentLoop(opts) {
|
|
|
254
261
|
cacheReadTokens += turnCacheRead;
|
|
255
262
|
// Update cost (include cache tokens for accurate pricing)
|
|
256
263
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
|
|
264
|
+
// Graduated cost warnings — give the LLM visibility into spend
|
|
265
|
+
if (isFinite(maxCostUsd)) {
|
|
266
|
+
for (const pct of [25, 50, 75]) {
|
|
267
|
+
if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
|
|
268
|
+
costWarningsEmitted.add(pct);
|
|
269
|
+
onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
257
273
|
// Record per-turn metrics for observability
|
|
258
274
|
const turnToolNames = toolUseBlocks.map(b => b.name);
|
|
259
275
|
turnMetrics.push({
|
|
@@ -271,6 +287,7 @@ export async function runServerAgentLoop(opts) {
|
|
|
271
287
|
// Compaction handling — API paused after generating summary.
|
|
272
288
|
// Preserve last 2 messages (1 user + 1 assistant turn) for continuity,
|
|
273
289
|
// then resume. This is NOT a new turn — just context compression.
|
|
290
|
+
lastStopReason = streamResult.stopReason || "end_turn";
|
|
274
291
|
if (streamResult.stopReason === "compaction" && compactionContent) {
|
|
275
292
|
compactionCount++;
|
|
276
293
|
log.info({ compactionCount }, "compaction — preserving last 2 messages, resuming");
|
|
@@ -331,6 +348,15 @@ export async function runServerAgentLoop(opts) {
|
|
|
331
348
|
totalIn += subagentTokens.input;
|
|
332
349
|
totalOut += subagentTokens.output;
|
|
333
350
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + subagentTokens.costUsd;
|
|
351
|
+
// Cost warnings after subagent aggregation (subagents can be expensive)
|
|
352
|
+
if (isFinite(maxCostUsd)) {
|
|
353
|
+
for (const pct of [25, 50, 75]) {
|
|
354
|
+
if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
|
|
355
|
+
costWarningsEmitted.add(pct);
|
|
356
|
+
onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
334
360
|
const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks, compactionContent });
|
|
335
361
|
messages.push({ role: "assistant", content: assistantContent });
|
|
336
362
|
messages.push({ role: "user", content: toolResults });
|
|
@@ -426,6 +452,15 @@ export async function runServerAgentLoop(opts) {
|
|
|
426
452
|
}
|
|
427
453
|
}
|
|
428
454
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
|
|
455
|
+
// Graduated cost warnings (non-streaming path)
|
|
456
|
+
if (isFinite(maxCostUsd)) {
|
|
457
|
+
for (const pct of [25, 50, 75]) {
|
|
458
|
+
if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
|
|
459
|
+
costWarningsEmitted.add(pct);
|
|
460
|
+
onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
429
464
|
// Record per-turn metrics (non-streaming)
|
|
430
465
|
const nsTurnToolNames = toolUseBlocks.map(b => b.name);
|
|
431
466
|
turnMetrics.push({
|
|
@@ -440,6 +475,7 @@ export async function runServerAgentLoop(opts) {
|
|
|
440
475
|
});
|
|
441
476
|
if (currentText)
|
|
442
477
|
allTextResponses.push(currentText);
|
|
478
|
+
lastStopReason = response.stop_reason || "end_turn";
|
|
443
479
|
// Compaction handling (non-streaming) — same logic as streaming path
|
|
444
480
|
if (response.stop_reason === "compaction" && nsCompactionContent !== null) {
|
|
445
481
|
compactionCount++;
|
|
@@ -488,6 +524,15 @@ export async function runServerAgentLoop(opts) {
|
|
|
488
524
|
totalIn += nonStreamSubTokens.input;
|
|
489
525
|
totalOut += nonStreamSubTokens.output;
|
|
490
526
|
sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + nonStreamSubTokens.costUsd;
|
|
527
|
+
// Cost warnings after subagent aggregation (non-streaming)
|
|
528
|
+
if (isFinite(maxCostUsd)) {
|
|
529
|
+
for (const pct of [25, 50, 75]) {
|
|
530
|
+
if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
|
|
531
|
+
costWarningsEmitted.add(pct);
|
|
532
|
+
onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
491
536
|
const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks });
|
|
492
537
|
messages.push({ role: "assistant", content: assistantContent });
|
|
493
538
|
messages.push({ role: "user", content: toolResults });
|
|
@@ -514,21 +559,22 @@ export async function runServerAgentLoop(opts) {
|
|
|
514
559
|
loopDetectorStats: loopDetector.getSessionStats(),
|
|
515
560
|
turns: turnMetrics,
|
|
516
561
|
citations: allCitations,
|
|
562
|
+
stopReason: lastStopReason,
|
|
517
563
|
};
|
|
518
564
|
}
|
|
519
565
|
// ============================================================================
|
|
520
566
|
// TOOL EXECUTOR FACTORY — creates executor for dispatchTools with delegation
|
|
521
567
|
// ============================================================================
|
|
522
568
|
function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredToolNames) {
|
|
523
|
-
const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs =
|
|
569
|
+
const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
|
|
524
570
|
return async (name, input) => {
|
|
525
571
|
allToolNames.push(name);
|
|
526
|
-
// Subagent delegation
|
|
572
|
+
// Subagent delegation — demote models to control cost (sub-agents should never run Opus)
|
|
527
573
|
if (name === "delegate_task") {
|
|
528
574
|
const subPrompt = String(input.prompt || "");
|
|
529
575
|
const subModelInput = String(input.model || "haiku");
|
|
530
|
-
const subModel = (subModelInput === "opus" ? "
|
|
531
|
-
subModelInput === "sonnet" ? "
|
|
576
|
+
const subModel = (subModelInput === "opus" ? "sonnet" :
|
|
577
|
+
subModelInput === "sonnet" ? "haiku" : "haiku");
|
|
532
578
|
const subMaxTurns = Math.min(Math.max(1, Number(input.max_turns) || 6), 12);
|
|
533
579
|
const subTools = tools.filter((t) => t.name !== "delegate_task");
|
|
534
580
|
const subId = `sub-${Date.now().toString(36)}`;
|
|
@@ -547,37 +593,37 @@ function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredT
|
|
|
547
593
|
: subModel === "sonnet" ? MODELS.SONNET : MODELS.HAIKU;
|
|
548
594
|
try {
|
|
549
595
|
const subEndTime = Date.now();
|
|
550
|
-
|
|
551
|
-
crypto.getRandomValues(subBytes);
|
|
552
|
-
const subSpanId = Array.from(subBytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
553
|
-
await supabase.from("audit_logs").insert({
|
|
596
|
+
queueSpan(auditRowToSpan({
|
|
554
597
|
action: "chat.subagent_complete", severity: "info",
|
|
555
598
|
store_id: storeId || null, resource_type: "chat_subagent",
|
|
556
599
|
resource_id: agentId || null, request_id: traceId || null,
|
|
557
600
|
conversation_id: conversationId || null, source: "server_subagent",
|
|
558
|
-
user_id: userId || null,
|
|
601
|
+
user_id: userId || null,
|
|
602
|
+
user_email: userEmail || null,
|
|
559
603
|
input_tokens: subResult.tokensUsed.input, output_tokens: subResult.tokensUsed.output,
|
|
560
604
|
total_cost: subResult.costUsd, model: subModelId, duration_ms: subDurationMs,
|
|
561
|
-
// OTEL fields
|
|
562
605
|
trace_id: traceId || null,
|
|
563
|
-
span_id: subSpanId,
|
|
564
606
|
span_kind: "INTERNAL",
|
|
565
607
|
service_name: "agent-server",
|
|
566
608
|
status_code: subResult.success ? "OK" : "ERROR",
|
|
567
609
|
start_time: new Date(subEndTime - subDurationMs).toISOString(),
|
|
568
610
|
end_time: new Date(subEndTime).toISOString(),
|
|
611
|
+
stop_reason: subResult.stopReason || undefined,
|
|
612
|
+
turn_number: subResult.turnCount || 1,
|
|
613
|
+
parent_conversation_id: conversationId || undefined,
|
|
569
614
|
details: {
|
|
570
615
|
subagent_model: subModel, turn_count: subResult.turnCount,
|
|
571
616
|
tool_calls: subResult.toolsUsed.length, tool_names: subResult.toolsUsed,
|
|
572
617
|
cost_usd: subResult.costUsd, success: subResult.success,
|
|
573
618
|
prompt_preview: subPrompt.substring(0, 200),
|
|
574
|
-
// gen_ai fields for SwiftUI cost display
|
|
575
619
|
"gen_ai.request.model": subModelId,
|
|
576
620
|
"gen_ai.usage.input_tokens": subResult.tokensUsed.input,
|
|
577
621
|
"gen_ai.usage.output_tokens": subResult.tokensUsed.output,
|
|
622
|
+
"gen_ai.usage.cache_read_tokens": subResult.tokensUsed.cacheRead || 0,
|
|
623
|
+
"gen_ai.usage.cache_creation_tokens": subResult.tokensUsed.cacheCreation || 0,
|
|
578
624
|
"gen_ai.usage.cost": subResult.costUsd,
|
|
579
625
|
},
|
|
580
|
-
});
|
|
626
|
+
}));
|
|
581
627
|
}
|
|
582
628
|
catch (err) {
|
|
583
629
|
log.error({ err: err.message }, "failed to log subagent delegation audit");
|
|
@@ -12,10 +12,13 @@ export interface SubagentResult {
|
|
|
12
12
|
tokensUsed: {
|
|
13
13
|
input: number;
|
|
14
14
|
output: number;
|
|
15
|
+
cacheRead: number;
|
|
16
|
+
cacheCreation: number;
|
|
15
17
|
};
|
|
16
18
|
costUsd: number;
|
|
17
19
|
toolsUsed: string[];
|
|
18
20
|
turnCount: number;
|
|
21
|
+
stopReason: string;
|
|
19
22
|
}
|
|
20
23
|
export interface SubagentProgressEvent {
|
|
21
24
|
subagentId: string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* from index.ts (avoids circular deps) and is fully testable in isolation.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { LoopDetector, estimateCostUsd, sanitizeError, isRetryableError, } from "../../shared/agent-core.js";
|
|
8
|
+
import { LoopDetector, estimateCostUsd, sanitizeError, isRetryableError, addPromptCaching, } from "../../shared/agent-core.js";
|
|
9
9
|
import { buildAPIRequest } from "../../shared/api-client.js";
|
|
10
10
|
import { MODELS, MODEL_MAP } from "../../shared/constants.js";
|
|
11
11
|
import { dispatchTools } from "../../shared/tool-dispatch.js";
|
|
@@ -90,6 +90,8 @@ export async function runServerSubagent(opts) {
|
|
|
90
90
|
turn++;
|
|
91
91
|
loopDetector.resetTurn();
|
|
92
92
|
onProgress?.({ subagentId, event: "turn", turn, maxTurns });
|
|
93
|
+
// Apply prompt caching to tools and messages for cache hits on repeated context
|
|
94
|
+
const { tools: cachedTools, messages: cachedMessages } = addPromptCaching(tools, messages);
|
|
93
95
|
// Non-streaming API call with retry
|
|
94
96
|
let response;
|
|
95
97
|
try {
|
|
@@ -99,8 +101,8 @@ export async function runServerSubagent(opts) {
|
|
|
99
101
|
max_tokens: apiConfig.maxTokens,
|
|
100
102
|
temperature: shouldThink ? 1 : 0.3, // Anthropic requires temp=1 with thinking
|
|
101
103
|
system,
|
|
102
|
-
tools:
|
|
103
|
-
messages:
|
|
104
|
+
tools: cachedTools,
|
|
105
|
+
messages: cachedMessages,
|
|
104
106
|
betas: apiConfig.betas,
|
|
105
107
|
context_management: apiConfig.contextManagement,
|
|
106
108
|
...(apiConfig.thinking ? { thinking: apiConfig.thinking } : {}),
|
|
@@ -178,10 +180,11 @@ function makeResult(success, output, inputTokens, outputTokens, modelId, toolsUs
|
|
|
178
180
|
return {
|
|
179
181
|
success,
|
|
180
182
|
output,
|
|
181
|
-
tokensUsed: { input: inputTokens, output: outputTokens },
|
|
183
|
+
tokensUsed: { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheCreation: cacheCreationTokens },
|
|
182
184
|
costUsd: estimateCostUsd(inputTokens, outputTokens, modelId, 0, cacheReadTokens, cacheCreationTokens),
|
|
183
185
|
toolsUsed: [...new Set(toolsUsed)],
|
|
184
186
|
turnCount,
|
|
187
|
+
stopReason: "end_turn", // subagent always runs to completion or error
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
async function withSubagentRetry(fn, maxRetries = 2) {
|
|
@@ -1,13 +1,53 @@
|
|
|
1
|
-
// lib/supabase-client.ts — Resilient Supabase client with
|
|
1
|
+
// lib/supabase-client.ts — Resilient Supabase client with circuit breaker
|
|
2
2
|
// Fixes intermittent 520 errors from Cloudflare by retrying failed requests
|
|
3
3
|
// and reusing client instances instead of creating new ones per request.
|
|
4
|
+
// Circuit breaker prevents retry storms when Supabase is down.
|
|
4
5
|
import { createClient } from "@supabase/supabase-js";
|
|
5
6
|
const MAX_RETRIES = 3;
|
|
6
7
|
const INITIAL_BACKOFF_MS = 500;
|
|
7
8
|
const MAX_BACKOFF_MS = 5_000;
|
|
8
|
-
|
|
9
|
+
// ── Circuit breaker ──
|
|
10
|
+
// When Supabase is consistently failing, stop retrying to prevent:
|
|
11
|
+
// 1. Retry storms that make Supabase worse
|
|
12
|
+
// 2. Event loop blocking that causes health check failures
|
|
13
|
+
// 3. SSE stream stalls that freeze the CLI
|
|
14
|
+
const CIRCUIT_FAILURE_THRESHOLD = 10; // Open circuit after 10 consecutive failures
|
|
15
|
+
const CIRCUIT_RESET_MS = 30_000; // Try again after 30s
|
|
16
|
+
let circuitFailures = 0;
|
|
17
|
+
let circuitOpenUntil = 0;
|
|
18
|
+
/** Check if circuit breaker allows the request */
|
|
19
|
+
function isCircuitOpen() {
|
|
20
|
+
if (circuitFailures < CIRCUIT_FAILURE_THRESHOLD)
|
|
21
|
+
return false;
|
|
22
|
+
if (Date.now() >= circuitOpenUntil) {
|
|
23
|
+
// Half-open: allow one probe request
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function recordCircuitFailure() {
|
|
29
|
+
circuitFailures++;
|
|
30
|
+
if (circuitFailures >= CIRCUIT_FAILURE_THRESHOLD) {
|
|
31
|
+
circuitOpenUntil = Date.now() + CIRCUIT_RESET_MS;
|
|
32
|
+
if (circuitFailures === CIRCUIT_FAILURE_THRESHOLD) {
|
|
33
|
+
console.warn(`[supabase] Circuit breaker OPEN — ${circuitFailures} consecutive failures, pausing for ${CIRCUIT_RESET_MS / 1000}s`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function recordCircuitSuccess() {
|
|
38
|
+
if (circuitFailures > 0) {
|
|
39
|
+
console.info(`[supabase] Circuit breaker reset — connection recovered`);
|
|
40
|
+
}
|
|
41
|
+
circuitFailures = 0;
|
|
42
|
+
circuitOpenUntil = 0;
|
|
43
|
+
}
|
|
44
|
+
/** Custom fetch with retry for 5xx errors + circuit breaker */
|
|
9
45
|
function createRetryFetch(maxRetries = MAX_RETRIES) {
|
|
10
46
|
return async (input, init) => {
|
|
47
|
+
// Circuit breaker: fail fast when Supabase is known to be down
|
|
48
|
+
if (isCircuitOpen()) {
|
|
49
|
+
throw new Error("Supabase circuit breaker open — skipping request");
|
|
50
|
+
}
|
|
11
51
|
let lastError = null;
|
|
12
52
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
13
53
|
try {
|
|
@@ -18,17 +58,25 @@ function createRetryFetch(maxRetries = MAX_RETRIES) {
|
|
|
18
58
|
});
|
|
19
59
|
// Retry on 5xx errors (Cloudflare 520 = origin error, 522 = timeout, 524 = timeout)
|
|
20
60
|
if (res.status >= 500 && attempt < maxRetries) {
|
|
61
|
+
recordCircuitFailure();
|
|
62
|
+
if (isCircuitOpen()) {
|
|
63
|
+
return res; // Don't retry if circuit just opened
|
|
64
|
+
}
|
|
21
65
|
const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
22
66
|
console.warn(`[supabase] ${res.status} on ${typeof input === 'string' ? input.split('?')[0] : 'request'}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
|
|
23
67
|
await new Promise(r => setTimeout(r, backoff));
|
|
24
68
|
continue;
|
|
25
69
|
}
|
|
70
|
+
if (res.status < 500) {
|
|
71
|
+
recordCircuitSuccess();
|
|
72
|
+
}
|
|
26
73
|
return res;
|
|
27
74
|
}
|
|
28
75
|
catch (err) {
|
|
29
76
|
lastError = err;
|
|
77
|
+
recordCircuitFailure();
|
|
30
78
|
// Retry on network errors (ECONNRESET, ETIMEDOUT, etc.)
|
|
31
|
-
if (attempt < maxRetries) {
|
|
79
|
+
if (attempt < maxRetries && !isCircuitOpen()) {
|
|
32
80
|
const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
33
81
|
console.warn(`[supabase] Network error: ${lastError.message}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
|
|
34
82
|
await new Promise(r => setTimeout(r, backoff));
|
|
@@ -159,14 +159,24 @@ export function evaluateCondition(expression, ctx) {
|
|
|
159
159
|
[/\scontains\s/, "contains"],
|
|
160
160
|
[/\s!contains\s/, "!contains"],
|
|
161
161
|
];
|
|
162
|
+
// Identify quoted regions to avoid matching operators inside them
|
|
163
|
+
const quoteRegions = [];
|
|
164
|
+
const quoteRe = /(['"])(?:(?!\1).)*\1/g;
|
|
165
|
+
let qm;
|
|
166
|
+
while ((qm = quoteRe.exec(expr)) !== null) {
|
|
167
|
+
quoteRegions.push([qm.index, qm.index + qm[0].length]);
|
|
168
|
+
}
|
|
162
169
|
for (const [pattern, op] of operatorPatterns) {
|
|
163
|
-
// Use the LAST match
|
|
164
|
-
// operator-like strings, but the actual operator is the rightmost one.
|
|
170
|
+
// Use the LAST match that is NOT inside a quoted string.
|
|
165
171
|
const globalRe = new RegExp(pattern.source, "g");
|
|
166
172
|
let lastMatch = null;
|
|
167
173
|
let m;
|
|
168
|
-
while ((m = globalRe.exec(expr)) !== null)
|
|
169
|
-
|
|
174
|
+
while ((m = globalRe.exec(expr)) !== null) {
|
|
175
|
+
const idx = m.index;
|
|
176
|
+
const insideQuotes = quoteRegions.some(([s, e]) => idx > s && idx < e);
|
|
177
|
+
if (!insideQuotes)
|
|
178
|
+
lastMatch = m;
|
|
179
|
+
}
|
|
170
180
|
if (!lastMatch || lastMatch.index === undefined)
|
|
171
181
|
continue;
|
|
172
182
|
let left = expr.substring(0, lastMatch.index).trim();
|
package/dist/server/lib/utils.js
CHANGED
|
@@ -114,6 +114,21 @@ export function summarizeResult(toolName, action, data) {
|
|
|
114
114
|
if (action === "browse")
|
|
115
115
|
return { categories: d.data ? d.data.categories?.length || 0 : 0, status_summary: d.data ? d.data.product_status_summary : undefined };
|
|
116
116
|
return { action, count: Array.isArray(d) ? d.length : (d.count ?? (d.products ? d.products.length : (d.id ? 1 : 0))) };
|
|
117
|
+
case "media": {
|
|
118
|
+
if (action === "list" || action === "search")
|
|
119
|
+
return { count: d.count, total: d.total };
|
|
120
|
+
if (action === "analytics")
|
|
121
|
+
return { total_items: d.total_items, orphan_count: d.orphan_count, storage_mb: d.total_storage_mb };
|
|
122
|
+
if (action === "upload")
|
|
123
|
+
return { media_id: d.media_id, file_name: d.file_name };
|
|
124
|
+
if (action === "bulk_upload")
|
|
125
|
+
return { uploaded_count: d.uploaded_count, failed_count: d.failed_count };
|
|
126
|
+
if (action === "bulk_update")
|
|
127
|
+
return { updated_count: d.updated_count, failed_count: d.failed_count };
|
|
128
|
+
if (action === "usage")
|
|
129
|
+
return { reference_count: d.reference_count };
|
|
130
|
+
return { action };
|
|
131
|
+
}
|
|
117
132
|
case "audit_trail":
|
|
118
133
|
return { count: d.count, days: d.days, actions: d.summary ? Object.keys(d.summary).length : 0 };
|
|
119
134
|
case "telemetry":
|
|
@@ -16,6 +16,7 @@ export interface LocalAgent {
|
|
|
16
16
|
ws: WebSocket;
|
|
17
17
|
storeId: string;
|
|
18
18
|
userId: string | null;
|
|
19
|
+
nodeId: string | null;
|
|
19
20
|
capabilities: string[];
|
|
20
21
|
connectedAt: Date;
|
|
21
22
|
lastPong: number;
|
|
@@ -50,6 +51,7 @@ export declare function getAgentInfo(storeId: string): Array<{
|
|
|
50
51
|
capabilities: string[];
|
|
51
52
|
connected_at: string;
|
|
52
53
|
uptime_seconds: number;
|
|
54
|
+
node_id: string | null;
|
|
53
55
|
}>;
|
|
54
56
|
/**
|
|
55
57
|
* Execute a command on the user's local machine via their connected agent.
|
|
@@ -79,4 +81,46 @@ export declare function getGatewayStats(): {
|
|
|
79
81
|
pending_requests: number;
|
|
80
82
|
agents_by_store: Record<string, number>;
|
|
81
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Get active remote desktop sessions.
|
|
86
|
+
*/
|
|
87
|
+
export declare function getRemoteDesktopSessions(): Array<{
|
|
88
|
+
session_id: string;
|
|
89
|
+
store_id: string;
|
|
90
|
+
user_id: string;
|
|
91
|
+
agent_id: string;
|
|
92
|
+
started_at: string;
|
|
93
|
+
frames_relayed: number;
|
|
94
|
+
bytes_relayed: number;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Get active portal sessions.
|
|
98
|
+
*/
|
|
99
|
+
export declare function getPortalSessions(): Array<{
|
|
100
|
+
session_id: string;
|
|
101
|
+
store_id: string;
|
|
102
|
+
initiator_agent_id: string;
|
|
103
|
+
target_agent_id: string;
|
|
104
|
+
capabilities: string[];
|
|
105
|
+
started_at: string;
|
|
106
|
+
bytes_relayed: number;
|
|
107
|
+
}>;
|
|
108
|
+
export interface ClusterCommandArgs {
|
|
109
|
+
action: string;
|
|
110
|
+
task?: string;
|
|
111
|
+
max_cells?: number;
|
|
112
|
+
working_directory?: string;
|
|
113
|
+
cluster_id?: string;
|
|
114
|
+
channel_id?: string;
|
|
115
|
+
conversation_id?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Execute a cluster command on the connected whale-node.
|
|
119
|
+
* Sends via WebSocket and waits for the result.
|
|
120
|
+
*/
|
|
121
|
+
export declare function executeClusterCommand(storeId: string, args: ClusterCommandArgs, options?: {
|
|
122
|
+
timeout?: number;
|
|
123
|
+
agent_id?: string;
|
|
124
|
+
node_id?: string;
|
|
125
|
+
}): Promise<AgentResult>;
|
|
82
126
|
export declare function shutdownGateway(): void;
|