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,224 @@
|
|
|
1
|
+
// server/lib/code-worker-pool.ts — Persistent pre-forked worker pool for code steps
|
|
2
|
+
// Eliminates fork-per-execution overhead. Workers are recycled after MAX_USES executions.
|
|
3
|
+
import { fork } from "node:child_process";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname_pool = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const WORKER_PATH = join(__dirname_pool, "code-worker.js");
|
|
8
|
+
const POOL_SIZE = parseInt(process.env.CODE_WORKER_POOL_SIZE || "4", 10);
|
|
9
|
+
const MAX_USES = 50; // Recycle worker after N executions (prevent memory leaks)
|
|
10
|
+
const ACQUIRE_TIMEOUT_MS = 10_000; // Max wait to acquire a worker
|
|
11
|
+
const pool = [];
|
|
12
|
+
const waitQueue = [];
|
|
13
|
+
let initialized = false;
|
|
14
|
+
// Restricted env for worker processes — strip server secrets (API keys, DB URLs, etc.)
|
|
15
|
+
const WORKER_ENV = {
|
|
16
|
+
NODE_ENV: process.env.NODE_ENV || "production",
|
|
17
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
18
|
+
};
|
|
19
|
+
function spawnWorker() {
|
|
20
|
+
const child = fork(WORKER_PATH, [], {
|
|
21
|
+
stdio: "pipe",
|
|
22
|
+
serialization: "json",
|
|
23
|
+
env: WORKER_ENV,
|
|
24
|
+
});
|
|
25
|
+
const worker = { child, busy: false, uses: 0, ready: false };
|
|
26
|
+
child.on("message", (msg) => {
|
|
27
|
+
if (msg.type === "ready") {
|
|
28
|
+
worker.ready = true;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
child.on("exit", () => {
|
|
32
|
+
// Remove dead worker from pool and replace it
|
|
33
|
+
const idx = pool.indexOf(worker);
|
|
34
|
+
if (idx >= 0) {
|
|
35
|
+
pool.splice(idx, 1);
|
|
36
|
+
try {
|
|
37
|
+
pool.push(spawnWorker());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Can't respawn — pool shrinks
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
child.on("error", () => {
|
|
45
|
+
// Will trigger exit event above
|
|
46
|
+
});
|
|
47
|
+
return worker;
|
|
48
|
+
}
|
|
49
|
+
/** Initialize the worker pool. Safe to call multiple times. */
|
|
50
|
+
export function initWorkerPool() {
|
|
51
|
+
if (initialized)
|
|
52
|
+
return;
|
|
53
|
+
initialized = true;
|
|
54
|
+
for (let i = 0; i < POOL_SIZE; i++) {
|
|
55
|
+
try {
|
|
56
|
+
pool.push(spawnWorker());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error(`[worker-pool] Failed to spawn worker ${i + 1}/${POOL_SIZE}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log(`[worker-pool] Initialized with ${pool.length} workers`);
|
|
63
|
+
}
|
|
64
|
+
/** Acquire an available worker, or wait in queue. */
|
|
65
|
+
function acquireWorker() {
|
|
66
|
+
// Try to find an idle worker
|
|
67
|
+
const idle = pool.find(w => !w.busy && w.ready);
|
|
68
|
+
if (idle) {
|
|
69
|
+
idle.busy = true;
|
|
70
|
+
return Promise.resolve(idle);
|
|
71
|
+
}
|
|
72
|
+
// No idle worker — wait in queue
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
const idx = waitQueue.findIndex(q => q.resolve === resolve);
|
|
76
|
+
if (idx >= 0)
|
|
77
|
+
waitQueue.splice(idx, 1);
|
|
78
|
+
reject(new Error("Timed out waiting for available code worker"));
|
|
79
|
+
}, ACQUIRE_TIMEOUT_MS);
|
|
80
|
+
waitQueue.push({ resolve, timer });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Release a worker back to the pool. Recycle if MAX_USES reached. */
|
|
84
|
+
function releaseWorker(worker) {
|
|
85
|
+
worker.busy = false;
|
|
86
|
+
worker.uses++;
|
|
87
|
+
// Recycle if too many uses (prevent memory leaks in vm)
|
|
88
|
+
if (worker.uses >= MAX_USES) {
|
|
89
|
+
const idx = pool.indexOf(worker);
|
|
90
|
+
if (idx >= 0)
|
|
91
|
+
pool.splice(idx, 1);
|
|
92
|
+
try {
|
|
93
|
+
worker.child.kill("SIGTERM");
|
|
94
|
+
}
|
|
95
|
+
catch { /* process already dead */ }
|
|
96
|
+
try {
|
|
97
|
+
pool.push(spawnWorker());
|
|
98
|
+
}
|
|
99
|
+
catch { /* spawn failure handled by pool size checks */ }
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Service waiting queue
|
|
103
|
+
if (waitQueue.length > 0) {
|
|
104
|
+
const next = waitQueue.shift();
|
|
105
|
+
clearTimeout(next.timer);
|
|
106
|
+
worker.busy = true;
|
|
107
|
+
next.resolve(worker);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Execute code using the worker pool. Falls back to direct fork if pool is unavailable.
|
|
112
|
+
*/
|
|
113
|
+
export async function executeWithPool(req) {
|
|
114
|
+
if (!initialized || pool.length === 0) {
|
|
115
|
+
// Pool not available — fall back to one-shot fork
|
|
116
|
+
return executeDirectFork(req);
|
|
117
|
+
}
|
|
118
|
+
let worker;
|
|
119
|
+
try {
|
|
120
|
+
worker = await acquireWorker();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return executeDirectFork(req);
|
|
124
|
+
}
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let resolved = false;
|
|
127
|
+
const finish = (result) => {
|
|
128
|
+
if (resolved)
|
|
129
|
+
return;
|
|
130
|
+
resolved = true;
|
|
131
|
+
clearTimeout(killTimer);
|
|
132
|
+
releaseWorker(worker);
|
|
133
|
+
resolve(result);
|
|
134
|
+
};
|
|
135
|
+
const killTimer = setTimeout(() => {
|
|
136
|
+
finish({ success: false, error: `Code execution timed out after ${req.timeoutMs}ms` });
|
|
137
|
+
// Kill and replace the stuck worker
|
|
138
|
+
const idx = pool.indexOf(worker);
|
|
139
|
+
if (idx >= 0)
|
|
140
|
+
pool.splice(idx, 1);
|
|
141
|
+
try {
|
|
142
|
+
worker.child.kill("SIGKILL");
|
|
143
|
+
}
|
|
144
|
+
catch { /* process already dead */ }
|
|
145
|
+
try {
|
|
146
|
+
pool.push(spawnWorker());
|
|
147
|
+
}
|
|
148
|
+
catch { /* spawn failure handled by pool size checks */ }
|
|
149
|
+
}, req.timeoutMs + 3000);
|
|
150
|
+
// Listen for result (one-shot)
|
|
151
|
+
const onMessage = (msg) => {
|
|
152
|
+
if (msg.type === "result") {
|
|
153
|
+
worker.child.removeListener("message", onMessage);
|
|
154
|
+
finish(msg.success
|
|
155
|
+
? { success: true, output: msg.output, logs: msg.logs }
|
|
156
|
+
: { success: false, error: msg.error, logs: msg.logs });
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
worker.child.on("message", onMessage);
|
|
160
|
+
// Send code
|
|
161
|
+
worker.child.send({
|
|
162
|
+
type: "execute",
|
|
163
|
+
code: req.code,
|
|
164
|
+
context: req.context,
|
|
165
|
+
timeoutMs: req.timeoutMs,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** One-shot fork fallback (same as original) */
|
|
170
|
+
function executeDirectFork(req) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
let resolved = false;
|
|
173
|
+
let child;
|
|
174
|
+
const finish = (result) => {
|
|
175
|
+
if (resolved)
|
|
176
|
+
return;
|
|
177
|
+
resolved = true;
|
|
178
|
+
try {
|
|
179
|
+
child?.kill("SIGKILL");
|
|
180
|
+
}
|
|
181
|
+
catch { /* process already dead */ }
|
|
182
|
+
resolve(result);
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
child = fork(WORKER_PATH, [], { timeout: req.timeoutMs + 2000, stdio: "pipe", serialization: "json", env: WORKER_ENV });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return resolve({ success: false, error: "Failed to fork code worker" });
|
|
189
|
+
}
|
|
190
|
+
const killTimer = setTimeout(() => {
|
|
191
|
+
finish({ success: false, error: `Code execution timed out after ${req.timeoutMs}ms` });
|
|
192
|
+
}, req.timeoutMs + 3000);
|
|
193
|
+
child.on("message", (msg) => {
|
|
194
|
+
if (msg.type === "result") {
|
|
195
|
+
clearTimeout(killTimer);
|
|
196
|
+
finish(msg.success ? { success: true, output: msg.output } : { success: false, error: msg.error });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
child.on("error", (err) => { clearTimeout(killTimer); finish({ success: false, error: `Worker error: ${err.message}` }); });
|
|
200
|
+
child.on("exit", (code) => { clearTimeout(killTimer); if (!resolved)
|
|
201
|
+
finish({ success: false, error: `Worker crashed (exit ${code})` }); });
|
|
202
|
+
child.send({ type: "execute", code: req.code, context: req.context, timeoutMs: req.timeoutMs });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Gracefully shutdown all workers */
|
|
206
|
+
export function shutdownPool() {
|
|
207
|
+
for (const w of pool) {
|
|
208
|
+
try {
|
|
209
|
+
w.child.kill("SIGTERM");
|
|
210
|
+
}
|
|
211
|
+
catch { /* process already dead */ }
|
|
212
|
+
}
|
|
213
|
+
pool.length = 0;
|
|
214
|
+
initialized = false;
|
|
215
|
+
}
|
|
216
|
+
/** Pool stats for monitoring */
|
|
217
|
+
export function getPoolStats() {
|
|
218
|
+
return {
|
|
219
|
+
total: pool.length,
|
|
220
|
+
busy: pool.filter(w => w.busy).length,
|
|
221
|
+
idle: pool.filter(w => !w.busy && w.ready).length,
|
|
222
|
+
queue: waitQueue.length,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// server/lib/code-worker.ts — Isolated code execution worker
|
|
2
|
+
// Runs as a child process via fork(). Receives code + context via IPC,
|
|
3
|
+
// executes in a fresh vm context, returns result. Crash here cannot take down the server.
|
|
4
|
+
import { runInNewContext } from "node:vm";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* Create a safe wrapper function that cannot be used to reach the host Function constructor.
|
|
8
|
+
* Wraps a host function in a proxy whose prototype chain is severed — .constructor returns
|
|
9
|
+
* undefined instead of the host Function constructor.
|
|
10
|
+
*/
|
|
11
|
+
function safeFunction(fn) {
|
|
12
|
+
// Create a wrapper function whose .constructor and .prototype are severed.
|
|
13
|
+
// We can't make a callable null-prototype object directly, so we override them on a regular function.
|
|
14
|
+
const safe = function (...args) {
|
|
15
|
+
return fn.apply(this, args);
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(safe, "constructor", { value: undefined, writable: false, configurable: false });
|
|
18
|
+
Object.defineProperty(safe, "prototype", { value: undefined, writable: false, configurable: false });
|
|
19
|
+
// Freeze to prevent re-assignment of constructor
|
|
20
|
+
Object.freeze(safe);
|
|
21
|
+
return safe;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a safe namespace object (like Math, JSON, Buffer) whose properties
|
|
25
|
+
* cannot reach the host Function constructor through any prototype chain.
|
|
26
|
+
* All function-valued properties are wrapped via safeFunction().
|
|
27
|
+
*/
|
|
28
|
+
function safeNamespace(source, keys) {
|
|
29
|
+
const ns = Object.create(null);
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
const val = source[key];
|
|
32
|
+
if (typeof val === "function") {
|
|
33
|
+
ns[key] = safeFunction(val);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
ns[key] = val;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze(ns);
|
|
40
|
+
}
|
|
41
|
+
// Only run when forked as a child process
|
|
42
|
+
if (process.send) {
|
|
43
|
+
process.on("message", (msg) => {
|
|
44
|
+
if (msg.type !== "execute")
|
|
45
|
+
return;
|
|
46
|
+
const logs = [];
|
|
47
|
+
// Validate code for dangerous patterns (defense-in-depth — sandbox isolation is primary defense)
|
|
48
|
+
const dangerousPatterns = [
|
|
49
|
+
/constructor\s*\[/i,
|
|
50
|
+
/constructor\s*\(/i,
|
|
51
|
+
/\.constructor/i,
|
|
52
|
+
/__proto__/i,
|
|
53
|
+
/prototype\s*\[/i,
|
|
54
|
+
/\bprocess\b/,
|
|
55
|
+
/\brequire\b/,
|
|
56
|
+
/\bimport\b/,
|
|
57
|
+
/\bglobalThis\b/,
|
|
58
|
+
/\bglobal\b/,
|
|
59
|
+
/\bFunction\b/,
|
|
60
|
+
/\beval\b/,
|
|
61
|
+
/\bReflect\b/,
|
|
62
|
+
/\bProxy\b/,
|
|
63
|
+
/\bSymbol\b/,
|
|
64
|
+
/\bWeakRef\b/,
|
|
65
|
+
];
|
|
66
|
+
for (const pattern of dangerousPatterns) {
|
|
67
|
+
if (pattern.test(msg.code)) {
|
|
68
|
+
const result = {
|
|
69
|
+
type: "result",
|
|
70
|
+
success: false,
|
|
71
|
+
error: `Code contains blocked pattern: ${pattern.source}`,
|
|
72
|
+
};
|
|
73
|
+
process.send(result);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Build hardened sandbox — NO host constructors or direct function references.
|
|
78
|
+
// vm.runInNewContext provides its own Array, Object, String, Number, Boolean in the
|
|
79
|
+
// sandbox context. We do NOT pass host realm constructors, which would allow
|
|
80
|
+
// escaping via .constructor.constructor('return process')().
|
|
81
|
+
const sandbox = Object.create(null);
|
|
82
|
+
sandbox.steps = JSON.parse(JSON.stringify(msg.context.steps));
|
|
83
|
+
sandbox.trigger = JSON.parse(JSON.stringify(msg.context.trigger));
|
|
84
|
+
sandbox.input = msg.context.input != null ? JSON.parse(JSON.stringify(msg.context.input)) : undefined;
|
|
85
|
+
sandbox.output = undefined;
|
|
86
|
+
// console — safe: null-prototype object with wrapped log function
|
|
87
|
+
const logFn = safeFunction((...args) => logs.push(args.map(String).join(" ")));
|
|
88
|
+
const consoleObj = Object.create(null);
|
|
89
|
+
consoleObj.log = logFn;
|
|
90
|
+
sandbox.console = Object.freeze(consoleObj);
|
|
91
|
+
// JSON — safe namespace with only parse/stringify
|
|
92
|
+
sandbox.JSON = safeNamespace(JSON, ["parse", "stringify"]);
|
|
93
|
+
// Math — safe namespace (all methods wrapped, numeric constants preserved)
|
|
94
|
+
const mathKeys = [
|
|
95
|
+
"abs", "ceil", "floor", "round", "max", "min", "pow", "sqrt", "log", "log2", "log10",
|
|
96
|
+
"random", "sign", "trunc", "cbrt", "hypot", "clz32", "imul", "fround",
|
|
97
|
+
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh",
|
|
98
|
+
"PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2",
|
|
99
|
+
];
|
|
100
|
+
sandbox.Math = safeNamespace(Math, mathKeys);
|
|
101
|
+
// Date — only expose Date.now() as a safe function, not the Date constructor itself.
|
|
102
|
+
// The Date constructor is a host function whose .constructor leads to Function.
|
|
103
|
+
const dateNs = Object.create(null);
|
|
104
|
+
dateNs.now = safeFunction(Date.now);
|
|
105
|
+
sandbox.Date = Object.freeze(dateNs);
|
|
106
|
+
// Utility functions — each wrapped to sever .constructor chain
|
|
107
|
+
sandbox.parseInt = safeFunction(parseInt);
|
|
108
|
+
sandbox.parseFloat = safeFunction(parseFloat);
|
|
109
|
+
sandbox.isNaN = safeFunction(isNaN);
|
|
110
|
+
sandbox.isFinite = safeFunction(isFinite);
|
|
111
|
+
sandbox.encodeURIComponent = safeFunction(encodeURIComponent);
|
|
112
|
+
sandbox.decodeURIComponent = safeFunction(decodeURIComponent);
|
|
113
|
+
// URL — safe namespace with only parse method (no constructor exposure)
|
|
114
|
+
const urlNs = Object.create(null);
|
|
115
|
+
urlNs.parse = safeFunction((input) => {
|
|
116
|
+
try {
|
|
117
|
+
const u = new URL(input);
|
|
118
|
+
return { href: u.href, protocol: u.protocol, host: u.host, hostname: u.hostname,
|
|
119
|
+
port: u.port, pathname: u.pathname, search: u.search, hash: u.hash,
|
|
120
|
+
origin: u.origin, searchParams: Object.fromEntries(u.searchParams) };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
urlNs.format = safeFunction((parts) => {
|
|
127
|
+
try {
|
|
128
|
+
return new URL(`${parts.protocol || "https:"}//${parts.host || parts.hostname || ""}${parts.pathname || ""}${parts.search || ""}`).href;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
sandbox.URL = Object.freeze(urlNs);
|
|
135
|
+
// Buffer — safe namespace with wrapped from/alloc
|
|
136
|
+
const bufferNs = Object.create(null);
|
|
137
|
+
bufferNs.from = safeFunction((...args) => Buffer.from(args[0], args[1]));
|
|
138
|
+
bufferNs.alloc = safeFunction((size) => Buffer.alloc(size));
|
|
139
|
+
sandbox.Buffer = Object.freeze(bufferNs);
|
|
140
|
+
// crypto — safe namespace with wrapped randomUUID
|
|
141
|
+
const cryptoNs = Object.create(null);
|
|
142
|
+
cryptoNs.randomUUID = safeFunction(randomUUID);
|
|
143
|
+
sandbox.crypto = Object.freeze(cryptoNs);
|
|
144
|
+
// DO NOT expose host constructors: Array, Object, String, Number, Boolean.
|
|
145
|
+
// vm.runInNewContext creates its own versions of these in the sandbox context.
|
|
146
|
+
// Passing host constructors would allow: [].constructor.constructor('return process')()
|
|
147
|
+
// Block dangerous globals explicitly
|
|
148
|
+
sandbox.process = undefined;
|
|
149
|
+
sandbox.require = undefined;
|
|
150
|
+
sandbox.global = undefined;
|
|
151
|
+
sandbox.globalThis = undefined;
|
|
152
|
+
sandbox.Function = undefined;
|
|
153
|
+
sandbox.eval = undefined;
|
|
154
|
+
sandbox.setTimeout = undefined;
|
|
155
|
+
sandbox.setInterval = undefined;
|
|
156
|
+
sandbox.Reflect = undefined;
|
|
157
|
+
sandbox.Proxy = undefined;
|
|
158
|
+
sandbox.Symbol = undefined;
|
|
159
|
+
sandbox.WeakRef = undefined;
|
|
160
|
+
try {
|
|
161
|
+
const result = runInNewContext(msg.code, sandbox, {
|
|
162
|
+
timeout: msg.timeoutMs,
|
|
163
|
+
filename: "workflow-code-step",
|
|
164
|
+
breakOnSigint: true,
|
|
165
|
+
microtaskMode: "afterEvaluate",
|
|
166
|
+
});
|
|
167
|
+
const workerResult = {
|
|
168
|
+
type: "result",
|
|
169
|
+
success: true,
|
|
170
|
+
output: { result: sandbox.output ?? result, logs },
|
|
171
|
+
};
|
|
172
|
+
process.send(workerResult);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const workerResult = {
|
|
176
|
+
type: "result",
|
|
177
|
+
success: false,
|
|
178
|
+
error: err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT"
|
|
179
|
+
? `Code execution timed out after ${msg.timeoutMs}ms`
|
|
180
|
+
: `Code error: ${err.message}`,
|
|
181
|
+
logs,
|
|
182
|
+
};
|
|
183
|
+
process.send(workerResult);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
// Signal ready
|
|
187
|
+
process.send({ type: "ready" });
|
|
188
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
export interface CompactionResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
content?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PreCompactResult {
|
|
8
|
+
messages: Array<Record<string, unknown>>;
|
|
9
|
+
bytesRemoved: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pre-compaction pass: strip redundant data before calling LLM summarizer.
|
|
13
|
+
* Mechanically removes large tool results already processed by the agent
|
|
14
|
+
* and thinking blocks from older turns. Returns modified messages array
|
|
15
|
+
* with reduced content so the LLM summarizer (or API context_management)
|
|
16
|
+
* has less to work with.
|
|
17
|
+
*/
|
|
18
|
+
export declare function preCompact(messages: Array<Record<string, unknown>>): PreCompactResult;
|
|
19
|
+
/**
|
|
20
|
+
* Generate a compaction summary using Haiku.
|
|
21
|
+
*
|
|
22
|
+
* Takes the full conversation history and produces a concise summary
|
|
23
|
+
* that preserves all state needed to continue the conversation.
|
|
24
|
+
* Falls back gracefully — returns null content on failure so the
|
|
25
|
+
* caller can keep the unsummarized history.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateCompaction(opts: {
|
|
28
|
+
anthropic: Anthropic;
|
|
29
|
+
messages: Array<Record<string, unknown>>;
|
|
30
|
+
systemPrompt: string;
|
|
31
|
+
instructions?: string;
|
|
32
|
+
}): Promise<CompactionResult>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// server/lib/compaction-service.ts — Haiku-based conversation compaction
|
|
2
|
+
//
|
|
3
|
+
// Used for non-Anthropic providers (OpenAI, Gemini) that lack native
|
|
4
|
+
// server-side compaction. Calls Haiku to produce a transparent summary
|
|
5
|
+
// using the same instructions as compact_20260112.
|
|
6
|
+
const COMPACTION_MODEL = "claude-haiku-4-5-20251001";
|
|
7
|
+
const COMPACTION_MAX_TOKENS = 4096;
|
|
8
|
+
const COMPACTION_TIMEOUT_MS = 30_000;
|
|
9
|
+
const COMPACTION_INSTRUCTIONS = "Summarize the conversation preserving: (1) task goals and constraints, " +
|
|
10
|
+
"(2) files created/modified with paths, (3) decisions made and rationale, " +
|
|
11
|
+
"(4) errors encountered and resolutions, (5) exact next steps. " +
|
|
12
|
+
"Be concise but preserve all state needed to continue work without repeating mistakes.";
|
|
13
|
+
/**
|
|
14
|
+
* Pre-compaction pass: strip redundant data before calling LLM summarizer.
|
|
15
|
+
* Mechanically removes large tool results already processed by the agent
|
|
16
|
+
* and thinking blocks from older turns. Returns modified messages array
|
|
17
|
+
* with reduced content so the LLM summarizer (or API context_management)
|
|
18
|
+
* has less to work with.
|
|
19
|
+
*/
|
|
20
|
+
export function preCompact(messages) {
|
|
21
|
+
let bytesRemoved = 0;
|
|
22
|
+
const compacted = messages.map((msg, idx) => {
|
|
23
|
+
if (msg.role !== "user")
|
|
24
|
+
return msg;
|
|
25
|
+
const content = msg.content;
|
|
26
|
+
if (!Array.isArray(content))
|
|
27
|
+
return msg;
|
|
28
|
+
const newContent = content.map((block) => {
|
|
29
|
+
// 1. Replace large tool results (>2KB) that the agent already acted on
|
|
30
|
+
if (block.type === "tool_result") {
|
|
31
|
+
const rc = typeof block.content === "string"
|
|
32
|
+
? block.content
|
|
33
|
+
: JSON.stringify(block.content);
|
|
34
|
+
if (rc.length > 2048) {
|
|
35
|
+
// Check if there's a subsequent assistant message (meaning agent processed this)
|
|
36
|
+
const hasFollowup = messages
|
|
37
|
+
.slice(idx + 1)
|
|
38
|
+
.some((m) => m.role === "assistant");
|
|
39
|
+
if (hasFollowup) {
|
|
40
|
+
const originalSize = rc.length;
|
|
41
|
+
bytesRemoved += originalSize - 100;
|
|
42
|
+
return {
|
|
43
|
+
...block,
|
|
44
|
+
content: `[Tool result: ${block.tool_use_id || "unknown"} returned ${Math.round(originalSize / 1024)}KB — agent processed and continued]`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return block;
|
|
50
|
+
});
|
|
51
|
+
return { ...msg, content: newContent };
|
|
52
|
+
});
|
|
53
|
+
// 2. Remove thinking blocks older than 3 turns from the end
|
|
54
|
+
const thinkingCutoff = Math.max(0, compacted.length - 6); // 3 turns = ~6 messages
|
|
55
|
+
const result = compacted.map((msg, idx) => {
|
|
56
|
+
if (idx >= thinkingCutoff)
|
|
57
|
+
return msg;
|
|
58
|
+
if (msg.role !== "assistant")
|
|
59
|
+
return msg;
|
|
60
|
+
const content = msg.content;
|
|
61
|
+
if (!Array.isArray(content))
|
|
62
|
+
return msg;
|
|
63
|
+
const filtered = content.filter((block) => {
|
|
64
|
+
if (block.type === "thinking") {
|
|
65
|
+
bytesRemoved += String(block.thinking || "").length;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
if (filtered.length === 0)
|
|
71
|
+
return msg; // Keep at least something
|
|
72
|
+
return { ...msg, content: filtered };
|
|
73
|
+
});
|
|
74
|
+
return { messages: result, bytesRemoved };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate a compaction summary using Haiku.
|
|
78
|
+
*
|
|
79
|
+
* Takes the full conversation history and produces a concise summary
|
|
80
|
+
* that preserves all state needed to continue the conversation.
|
|
81
|
+
* Falls back gracefully — returns null content on failure so the
|
|
82
|
+
* caller can keep the unsummarized history.
|
|
83
|
+
*/
|
|
84
|
+
export async function generateCompaction(opts) {
|
|
85
|
+
const { anthropic, messages, systemPrompt, instructions } = opts;
|
|
86
|
+
// Build a text representation of the conversation for summarization
|
|
87
|
+
const conversationText = messages
|
|
88
|
+
.map((msg) => {
|
|
89
|
+
const role = msg.role;
|
|
90
|
+
let content;
|
|
91
|
+
if (typeof msg.content === "string") {
|
|
92
|
+
content = msg.content;
|
|
93
|
+
}
|
|
94
|
+
else if (Array.isArray(msg.content)) {
|
|
95
|
+
content = msg.content
|
|
96
|
+
.map((block) => {
|
|
97
|
+
if (block.type === "text")
|
|
98
|
+
return block.text;
|
|
99
|
+
if (block.type === "tool_use")
|
|
100
|
+
return `[Tool: ${block.name}]`;
|
|
101
|
+
if (block.type === "tool_result") {
|
|
102
|
+
const rc = block.content;
|
|
103
|
+
if (typeof rc === "string")
|
|
104
|
+
return rc.slice(0, 500);
|
|
105
|
+
if (Array.isArray(rc)) {
|
|
106
|
+
return rc
|
|
107
|
+
.map((b) => (b.type === "text" ? b.text.slice(0, 500) : `[${b.type}]`))
|
|
108
|
+
.join(" ");
|
|
109
|
+
}
|
|
110
|
+
return "[tool_result]";
|
|
111
|
+
}
|
|
112
|
+
if (block.type === "compaction")
|
|
113
|
+
return `[Previous summary: ${block.content.slice(0, 200)}...]`;
|
|
114
|
+
return `[${block.type}]`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
content = String(msg.content);
|
|
120
|
+
}
|
|
121
|
+
return `${role.toUpperCase()}:\n${content}`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n\n---\n\n");
|
|
124
|
+
// Truncate if conversation is very long (Haiku has 200K context)
|
|
125
|
+
const maxInputChars = 400_000; // ~100K tokens
|
|
126
|
+
const truncatedConversation = conversationText.length > maxInputChars
|
|
127
|
+
? conversationText.slice(-maxInputChars) + "\n\n... (earlier messages truncated)"
|
|
128
|
+
: conversationText;
|
|
129
|
+
const summaryInstructions = instructions || COMPACTION_INSTRUCTIONS;
|
|
130
|
+
try {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timeout = setTimeout(() => controller.abort(), COMPACTION_TIMEOUT_MS);
|
|
133
|
+
try {
|
|
134
|
+
const response = await anthropic.messages.create({
|
|
135
|
+
model: COMPACTION_MODEL,
|
|
136
|
+
max_tokens: COMPACTION_MAX_TOKENS,
|
|
137
|
+
system: `You are summarizing an AI agent conversation for context compaction. ${summaryInstructions}`,
|
|
138
|
+
messages: [
|
|
139
|
+
{
|
|
140
|
+
role: "user",
|
|
141
|
+
content: `Summarize this conversation. The agent's system prompt was:\n\n${systemPrompt.slice(0, 2000)}\n\n---\n\nConversation:\n\n${truncatedConversation}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
}, { signal: controller.signal });
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
147
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
148
|
+
return { success: false, error: "No text in compaction response" };
|
|
149
|
+
}
|
|
150
|
+
return { success: true, content: textBlock.text };
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: `Compaction failed: ${err.message}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging with pino — Phase 6.1
|
|
3
|
+
*
|
|
4
|
+
* JSON output in production, optional pino-pretty in dev.
|
|
5
|
+
* Provides child loggers with context and correlation IDs.
|
|
6
|
+
*/
|
|
7
|
+
import pino from "pino";
|
|
8
|
+
declare const logger: pino.Logger<never, boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Create a child logger bound to a specific context (module name).
|
|
11
|
+
* Usage: const log = createLogger('agent-loop');
|
|
12
|
+
*/
|
|
13
|
+
export declare function createLogger(context: string): pino.Logger;
|
|
14
|
+
/**
|
|
15
|
+
* Create a child logger with correlation IDs for request tracing.
|
|
16
|
+
* Usage: const log = withTrace(traceId, conversationId);
|
|
17
|
+
*/
|
|
18
|
+
export declare function withTrace(traceId: string, conversationId?: string): pino.Logger;
|
|
19
|
+
export default logger;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging with pino — Phase 6.1
|
|
3
|
+
*
|
|
4
|
+
* JSON output in production, optional pino-pretty in dev.
|
|
5
|
+
* Provides child loggers with context and correlation IDs.
|
|
6
|
+
*/
|
|
7
|
+
import pino from "pino";
|
|
8
|
+
const logger = pino({
|
|
9
|
+
level: process.env.LOG_LEVEL || "info",
|
|
10
|
+
formatters: {
|
|
11
|
+
level(label) {
|
|
12
|
+
return { level: label };
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
// Pretty print in dev if pino-pretty is installed, JSON in production
|
|
16
|
+
transport: process.env.NODE_ENV !== "production"
|
|
17
|
+
? (() => {
|
|
18
|
+
try {
|
|
19
|
+
// pino-pretty is optional — if not installed, fall back to JSON
|
|
20
|
+
require.resolve("pino-pretty");
|
|
21
|
+
return { target: "pino-pretty", options: { colorize: true } };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
})()
|
|
27
|
+
: undefined,
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Create a child logger bound to a specific context (module name).
|
|
31
|
+
* Usage: const log = createLogger('agent-loop');
|
|
32
|
+
*/
|
|
33
|
+
export function createLogger(context) {
|
|
34
|
+
return logger.child({ context });
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a child logger with correlation IDs for request tracing.
|
|
38
|
+
* Usage: const log = withTrace(traceId, conversationId);
|
|
39
|
+
*/
|
|
40
|
+
export function withTrace(traceId, conversationId) {
|
|
41
|
+
return logger.child({
|
|
42
|
+
traceId,
|
|
43
|
+
...(conversationId ? { conversationId } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export default logger;
|