cascade-ai 0.2.2 → 0.2.11

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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var EventEmitter = require('events');
4
4
  var crypto = require('crypto');
5
+ var glob = require('glob');
5
6
  var Anthropic = require('@anthropic-ai/sdk');
6
7
  var OpenAI = require('openai');
7
8
  var genai = require('@google/genai');
@@ -18,7 +19,7 @@ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
18
19
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
19
20
  var zod = require('zod');
20
21
  var vm = require('vm');
21
- var os = require('os');
22
+ var os2 = require('os');
22
23
  var Database = require('better-sqlite3');
23
24
  var http = require('http');
24
25
  var url = require('url');
@@ -61,7 +62,7 @@ var path13__default = /*#__PURE__*/_interopDefault(path13);
61
62
  var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
62
63
  var fs11__default = /*#__PURE__*/_interopDefault(fs11);
63
64
  var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
64
- var os__default = /*#__PURE__*/_interopDefault(os);
65
+ var os2__default = /*#__PURE__*/_interopDefault(os2);
65
66
  var Database__default = /*#__PURE__*/_interopDefault(Database);
66
67
  var express__default = /*#__PURE__*/_interopDefault(express);
67
68
  var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
@@ -164,7 +165,7 @@ var require_keytar2 = __commonJS({
164
165
  });
165
166
 
166
167
  // src/constants.ts
167
- var CASCADE_VERSION = "0.2.2";
168
+ var CASCADE_VERSION = "0.2.11";
168
169
  var CASCADE_CONFIG_DIR = ".cascade";
169
170
  var CASCADE_MD_FILE = "CASCADE.md";
170
171
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -467,7 +468,8 @@ var TOOL_NAMES = {
467
468
  IMAGE_ANALYZE: "image_analyze",
468
469
  PDF_CREATE: "pdf_create",
469
470
  RUN_CODE: "run_code",
470
- PEER_MESSAGE: "peer_message"
471
+ PEER_MESSAGE: "peer_message",
472
+ WEB_SEARCH: "web_search"
471
473
  };
472
474
  var DEFAULT_APPROVAL_REQUIRED = [
473
475
  TOOL_NAMES.SHELL,
@@ -616,33 +618,61 @@ var AnthropicProvider = class extends BaseProvider {
616
618
  }
617
619
  }
618
620
  convertMessages(messages) {
619
- return messages.filter((m) => m.role !== "system").map((m) => {
620
- if (typeof m.content === "string") {
621
- return { role: m.role, content: m.content };
621
+ const result = [];
622
+ for (const m of messages) {
623
+ if (m.role === "system") continue;
624
+ if (m.role === "tool") {
625
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
626
+ result.push({
627
+ role: "user",
628
+ content: [{
629
+ type: "tool_result",
630
+ tool_use_id: m.toolCallId ?? "",
631
+ content: toolContent
632
+ }]
633
+ });
634
+ continue;
622
635
  }
623
- const content = m.content.map((block) => {
624
- if (block.type === "text") return { type: "text", text: block.text };
625
- if (block.type === "image") {
626
- const img = block.image;
627
- if (img.type === "base64") {
628
- return {
629
- type: "image",
630
- source: {
631
- type: "base64",
632
- media_type: img.mimeType,
633
- data: img.data
636
+ if (m.role === "assistant") {
637
+ const content = [];
638
+ const text = typeof m.content === "string" ? m.content : "";
639
+ if (text) content.push({ type: "text", text });
640
+ for (const tc of m.toolCalls ?? []) {
641
+ content.push({
642
+ type: "tool_use",
643
+ id: tc.id,
644
+ name: tc.name,
645
+ input: tc.input
646
+ });
647
+ }
648
+ if (content.length > 0) {
649
+ result.push({ role: "assistant", content });
650
+ }
651
+ continue;
652
+ }
653
+ if (m.role === "user") {
654
+ if (typeof m.content === "string") {
655
+ result.push({ role: "user", content: m.content });
656
+ } else {
657
+ const content = m.content.map((block) => {
658
+ if (block.type === "text") return { type: "text", text: block.text };
659
+ if (block.type === "image") {
660
+ const img = block.image;
661
+ if (img.type === "base64") {
662
+ return {
663
+ type: "image",
664
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
665
+ };
634
666
  }
635
- };
636
- }
637
- return {
638
- type: "image",
639
- source: { type: "url", url: img.data }
640
- };
667
+ return { type: "image", source: { type: "url", url: img.data } };
668
+ }
669
+ return { type: "text", text: "" };
670
+ });
671
+ result.push({ role: "user", content });
641
672
  }
642
- return { type: "text", text: "" };
643
- });
644
- return { role: m.role, content };
645
- });
673
+ }
674
+ }
675
+ return result;
646
676
  }
647
677
  };
648
678
  var OpenAIProvider = class extends BaseProvider {
@@ -903,7 +933,7 @@ var GeminiProvider = class extends BaseProvider {
903
933
  for (const part of candidate?.content?.parts ?? []) {
904
934
  if (part.functionCall) {
905
935
  toolCalls.push({
906
- id: `gemini-tool-${Date.now()}-${toolCalls.length}`,
936
+ id: part.functionCall.name,
907
937
  name: part.functionCall.name,
908
938
  input: part.functionCall.args ?? {}
909
939
  });
@@ -991,10 +1021,70 @@ var GeminiProvider = class extends BaseProvider {
991
1021
  }
992
1022
  // ── Private ──────────────────────────────────
993
1023
  buildContents(messages, extraImages) {
994
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
995
- role: m.role === "assistant" ? "model" : "user",
996
- parts: typeof m.content === "string" ? [{ text: m.content }] : this.convertMessageContent(m, extraImages)
997
- }));
1024
+ const contents = [];
1025
+ for (const m of messages) {
1026
+ if (m.role === "system") {
1027
+ const text = typeof m.content === "string" ? m.content : "";
1028
+ if (!text.trim()) continue;
1029
+ const prev = contents[contents.length - 1];
1030
+ if (prev?.role === "user") {
1031
+ prev.parts.unshift({ text: `[System context]: ${text}
1032
+
1033
+ ` });
1034
+ } else {
1035
+ contents.push({ role: "user", parts: [{ text: `[System context]: ${text}` }] });
1036
+ }
1037
+ continue;
1038
+ }
1039
+ if (m.role === "tool") {
1040
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1041
+ const functionName = m.toolCallId ?? "unknown_function";
1042
+ contents.push({
1043
+ role: "user",
1044
+ parts: [{
1045
+ functionResponse: {
1046
+ name: functionName,
1047
+ response: { output: toolContent }
1048
+ }
1049
+ }]
1050
+ });
1051
+ continue;
1052
+ }
1053
+ if (m.role === "assistant") {
1054
+ const parts = [];
1055
+ const textContent = typeof m.content === "string" ? m.content : "";
1056
+ if (textContent) parts.push({ text: textContent });
1057
+ for (const tc of m.toolCalls ?? []) {
1058
+ parts.push({
1059
+ functionCall: {
1060
+ name: tc.name,
1061
+ args: tc.input
1062
+ }
1063
+ });
1064
+ }
1065
+ if (parts.length > 0) {
1066
+ contents.push({ role: "model", parts });
1067
+ }
1068
+ continue;
1069
+ }
1070
+ if (m.role === "user") {
1071
+ const parts = this.convertMessageContent(m, contents.length === 0 ? extraImages : void 0);
1072
+ if (extraImages?.length && contents.length > 0) {
1073
+ const isLastUser = !messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
1074
+ if (isLastUser) {
1075
+ for (const img of extraImages) {
1076
+ if (img.type === "base64") {
1077
+ parts.push({ inlineData: { mimeType: img.mimeType, data: img.data } });
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ if (parts.length > 0) {
1083
+ contents.push({ role: "user", parts });
1084
+ }
1085
+ }
1086
+ }
1087
+ return contents;
998
1088
  }
999
1089
  convertMessageContent(msg, extraImages) {
1000
1090
  const parts = [];
@@ -1861,6 +1951,29 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
1861
1951
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
1862
1952
  }
1863
1953
  };
1954
+
1955
+ // src/utils/retry.ts
1956
+ var CascadeCancelledError = class extends Error {
1957
+ constructor(reason) {
1958
+ super(reason ?? "Run was cancelled via AbortSignal");
1959
+ this.name = "CascadeCancelledError";
1960
+ }
1961
+ };
1962
+ var CascadeToolError = class extends Error {
1963
+ /** A friendly message to show the user / T3 */
1964
+ userMessage;
1965
+ /** Whether this error class is retryable by default */
1966
+ retryable;
1967
+ constructor(userMessage, cause, retryable = false) {
1968
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
1969
+ super(`${userMessage}: ${causeMsg}`);
1970
+ this.name = "CascadeToolError";
1971
+ this.userMessage = userMessage;
1972
+ this.retryable = retryable;
1973
+ }
1974
+ };
1975
+
1976
+ // src/core/tiers/base.ts
1864
1977
  var BaseTier = class extends EventEmitter__default.default {
1865
1978
  id;
1866
1979
  role;
@@ -1870,6 +1983,8 @@ var BaseTier = class extends EventEmitter__default.default {
1870
1983
  label;
1871
1984
  systemPromptOverride = "";
1872
1985
  hierarchyContext = "";
1986
+ /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
1987
+ signal;
1873
1988
  constructor(role, id, parentId) {
1874
1989
  super();
1875
1990
  this.role = role;
@@ -1932,6 +2047,18 @@ var BaseTier = class extends EventEmitter__default.default {
1932
2047
  log(message, data) {
1933
2048
  this.emit("log", { tierId: this.id, role: this.role, message, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1934
2049
  }
2050
+ /**
2051
+ * Throws `CascadeCancelledError` if the run's `AbortSignal` has fired.
2052
+ * Call this at safe checkpoints (before LLM calls, between T3 dispatches)
2053
+ * to provide a fast, clean cancellation path.
2054
+ */
2055
+ throwIfCancelled() {
2056
+ if (this.signal?.aborted) {
2057
+ throw new CascadeCancelledError(
2058
+ typeof this.signal.reason === "string" ? this.signal.reason : "Run cancelled by caller"
2059
+ );
2060
+ }
2061
+ }
1935
2062
  };
1936
2063
 
1937
2064
  // src/core/context/manager.ts
@@ -2132,6 +2259,7 @@ Rules:
2132
2259
  - Execute the subtask completely \u2014 do not stop partway through.
2133
2260
  - Use tools when needed. Ask for approval only when the tool registry requires it.
2134
2261
  - If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.
2262
+ - Use the "web_search" tool to find current information, documentation, news, or general web data.
2135
2263
  - Use the "pdf_create" tool for PDF requests.
2136
2264
  - Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.
2137
2265
  - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
@@ -2175,7 +2303,8 @@ var T3Worker = class extends BaseTier {
2175
2303
  this.store = store;
2176
2304
  this.audit = new AuditLogger(store, sessionId);
2177
2305
  }
2178
- async execute(assignment, taskId) {
2306
+ async execute(assignment, taskId, signal) {
2307
+ this.signal = signal;
2179
2308
  this.assignment = assignment;
2180
2309
  this.taskId = taskId;
2181
2310
  this.setLabel(assignment.subtaskTitle);
@@ -2325,6 +2454,7 @@ Now execute your subtask using this context where relevant.`
2325
2454
  tools = [...tools];
2326
2455
  while (iterations < MAX_ITERATIONS) {
2327
2456
  iterations++;
2457
+ this.throwIfCancelled();
2328
2458
  const options = {
2329
2459
  messages: this.context.getMessages(),
2330
2460
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -2345,21 +2475,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2345
2475
  if (requiresArtifact) {
2346
2476
  stalledArtifactIterations += 1;
2347
2477
  if (stalledArtifactIterations >= 2) {
2348
- if (this.toolCreator && stalledArtifactIterations === 2) {
2349
- const toolName = await this.toolCreator.createTool(
2350
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
2351
- this.assignment?.description ?? ""
2352
- );
2353
- if (toolName) {
2354
- tools = this.toolRegistry.getToolDefinitions();
2355
- this.sendStatusUpdate({
2356
- progressPct: 50,
2357
- currentAction: `Dynamic tool created: ${toolName}`,
2358
- status: "IN_PROGRESS"
2359
- });
2360
- this.emit("tool:created", { tierId: this.id, toolName });
2361
- continue;
2362
- }
2478
+ if (stalledArtifactIterations === 2) {
2479
+ throw new Error(`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`);
2363
2480
  }
2364
2481
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
2365
2482
  }
@@ -2519,6 +2636,9 @@ ${assignment.expectedOutput}`;
2519
2636
  const artifactPaths = this.extractArtifactPaths(assignment);
2520
2637
  if (!artifactPaths.length) return { ok: true, issues: [] };
2521
2638
  const issues = [];
2639
+ const { exec: exec3 } = await import('child_process');
2640
+ const { promisify: promisify3 } = await import('util');
2641
+ const execAsync2 = promisify3(exec3);
2522
2642
  for (const artifactPath of artifactPaths) {
2523
2643
  const absolutePath = path13__default.default.resolve(process.cwd(), artifactPath);
2524
2644
  try {
@@ -2535,9 +2655,27 @@ ${assignment.expectedOutput}`;
2535
2655
  const content = await fs2__default.default.readFile(absolutePath, "utf-8");
2536
2656
  if (!content.trim()) {
2537
2657
  issues.push(`Artifact content is empty: ${artifactPath}`);
2658
+ continue;
2538
2659
  }
2539
2660
  } else if (stat.size < 100) {
2540
2661
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
2662
+ continue;
2663
+ }
2664
+ const ext = path13__default.default.extname(absolutePath).toLowerCase();
2665
+ try {
2666
+ if (ext === ".ts" || ext === ".tsx") {
2667
+ await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
2668
+ } else if (ext === ".js" || ext === ".jsx") {
2669
+ await execAsync2(`node --check ${absolutePath}`, { timeout: 1e4 });
2670
+ } else if (ext === ".py") {
2671
+ await execAsync2(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
2672
+ }
2673
+ } catch (err) {
2674
+ const stderr = err?.stderr || String(err);
2675
+ const stdout = err?.stdout || "";
2676
+ issues.push(`Semantic error in ${artifactPath}:
2677
+ ${stderr}
2678
+ ${stdout}`);
2541
2679
  }
2542
2680
  } catch {
2543
2681
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -2934,7 +3072,8 @@ var T2Manager = class extends BaseTier {
2934
3072
  });
2935
3073
  this.emit("peer-sync-received", { fromId, content });
2936
3074
  }
2937
- async execute(assignment, taskId) {
3075
+ async execute(assignment, taskId, signal) {
3076
+ this.signal = signal;
2938
3077
  this.assignment = assignment;
2939
3078
  this.taskId = taskId;
2940
3079
  this.setLabel(assignment.sectionTitle);
@@ -2946,12 +3085,14 @@ var T2Manager = class extends BaseTier {
2946
3085
  });
2947
3086
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
2948
3087
  try {
3088
+ this.throwIfCancelled();
2949
3089
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
2950
3090
  this.sendStatusUpdate({
2951
3091
  progressPct: 20,
2952
3092
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
2953
3093
  status: "IN_PROGRESS"
2954
3094
  });
3095
+ this.throwIfCancelled();
2955
3096
  const t3Results = await this.executeSubtasks(subtasks, taskId);
2956
3097
  this.sendStatusUpdate({
2957
3098
  progressPct: 90,
@@ -3118,11 +3259,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3118
3259
  ).join(", ")}`,
3119
3260
  status: "IN_PROGRESS"
3120
3261
  });
3262
+ this.throwIfCancelled();
3121
3263
  const waveResults = await Promise.allSettled(
3122
3264
  runnableIds.map(async (id) => {
3123
3265
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3124
3266
  const worker = workerMap.get(id);
3125
- const result = await worker.execute(assignment, taskId);
3267
+ const result = await worker.execute(assignment, taskId, this.signal);
3126
3268
  resultMap.set(id, result);
3127
3269
  return result;
3128
3270
  })
@@ -3136,6 +3278,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3136
3278
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3137
3279
  const retried = await this.retryT3(assignment, taskId);
3138
3280
  resultMap.set(id, retried);
3281
+ } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
3282
+ const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3283
+ if (this.toolCreator) {
3284
+ this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
3285
+ this.sendStatusUpdate({
3286
+ progressPct: 50,
3287
+ currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
3288
+ status: "IN_PROGRESS"
3289
+ });
3290
+ const toolName = await this.toolCreator.createTool(
3291
+ `Help complete: ${assignment.subtaskTitle}`,
3292
+ assignment.description
3293
+ );
3294
+ if (toolName) {
3295
+ this.log(`T2 verifying new tool: ${toolName}`);
3296
+ this.sendStatusUpdate({
3297
+ progressPct: 60,
3298
+ currentAction: `T2 Verifying new tool: ${toolName}`,
3299
+ status: "IN_PROGRESS"
3300
+ });
3301
+ try {
3302
+ const verifyResult = await this.router.generate("T2", {
3303
+ messages: [{ role: "user", content: `A new tool named "${toolName}" was just created dynamically to help with: ${assignment.description}. Based on its name and purpose, does this seem like a valid addition? Reply "VERIFIED" or "REJECTED".` }],
3304
+ systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
3305
+ maxTokens: 50
3306
+ });
3307
+ if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
3308
+ this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
3309
+ const retried = await this.retryT3({
3310
+ ...assignment,
3311
+ description: `${assignment.description}
3312
+
3313
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
3314
+ }, taskId);
3315
+ resultMap.set(id, retried);
3316
+ } else {
3317
+ this.log(`T2 rejected the dynamic tool: ${toolName}`);
3318
+ resultMap.set(id, r.value);
3319
+ }
3320
+ } catch {
3321
+ const retried = await this.retryT3({
3322
+ ...assignment,
3323
+ description: `${assignment.description}
3324
+
3325
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
3326
+ }, taskId);
3327
+ resultMap.set(id, retried);
3328
+ }
3329
+ } else {
3330
+ resultMap.set(id, r.value);
3331
+ }
3332
+ } else {
3333
+ resultMap.set(id, r.value);
3334
+ }
3139
3335
  }
3140
3336
  for (const dependent of adj.get(id) ?? []) {
3141
3337
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -3199,7 +3395,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3199
3395
  }));
3200
3396
  return worker.execute(
3201
3397
  { ...assignment, description: `[RETRY] ${assignment.description}` },
3202
- taskId
3398
+ taskId,
3399
+ this.signal
3203
3400
  );
3204
3401
  }
3205
3402
  publishSectionOutput(result) {
@@ -3213,29 +3410,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3213
3410
  async aggregateResults(assignment, results) {
3214
3411
  const completed = results.filter((r) => r.status === "COMPLETED");
3215
3412
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
3216
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
3217
3413
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
3218
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
3219
-
3220
- ${outputs}
3221
- ${peerOutputs ? `
3414
+ const peerContext = peerOutputs ? `
3222
3415
 
3223
3416
  Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
3224
- ${peerOutputs}` : ""}`;
3225
- const messages = [{ role: "user", content: prompt }];
3226
- try {
3227
- const result = await this.router.generate("T2", {
3228
- messages,
3229
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
3417
+ ${peerOutputs}` : "";
3418
+ const MAX_CHUNK_LENGTH = 15e3;
3419
+ let currentSummary = "";
3420
+ let i = 0;
3421
+ while (i < completed.length) {
3422
+ let chunkText = "";
3423
+ let chunkEnd = i;
3424
+ while (chunkEnd < completed.length) {
3425
+ const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
3426
+
3427
+ `;
3428
+ if (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
3429
+ break;
3430
+ }
3431
+ chunkText += nextOutput;
3432
+ chunkEnd++;
3433
+ }
3434
+ i = chunkEnd;
3435
+ const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
3436
+ ${currentSummary ? `
3437
+ PREVIOUS SUMMARY SO FAR:
3438
+ ${currentSummary}
3439
+
3440
+ NEW OUTPUTS TO INTEGRATE:
3441
+ ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
3442
+ const messages = [{ role: "user", content: prompt }];
3443
+ try {
3444
+ const result = await this.router.generate("T2", {
3445
+ messages,
3446
+ systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
3230
3447
 
3231
3448
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3232
- maxTokens: 300
3233
- });
3234
- return result.content;
3235
- } catch (err) {
3236
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
3237
- return outputs;
3449
+ maxTokens: 500
3450
+ });
3451
+ currentSummary = result.content;
3452
+ } catch (err) {
3453
+ this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
3454
+ return currentSummary + "\n\n" + chunkText;
3455
+ }
3238
3456
  }
3457
+ return currentSummary;
3239
3458
  }
3240
3459
  determineStatus(results) {
3241
3460
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -3360,10 +3579,10 @@ Rules:
3360
3579
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
3361
3580
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
3362
3581
 
3363
- EXECUTION MODE GUIDANCE:
3364
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
3365
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
3366
- - Prefer parallel execution: it is significantly faster and reduces total wall-clock time.
3582
+ DEPENDENCY GUIDANCE:
3583
+ - Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
3584
+ - Populate "dependsOn" with section IDs ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
3585
+ - Prefer empty dependencies (parallel execution): it is significantly faster and reduces total wall-clock time.
3367
3586
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
3368
3587
 
3369
3588
  QUALITY RULES:
@@ -3402,7 +3621,8 @@ var T1Administrator = class extends BaseTier {
3402
3621
  setToolCreator(creator) {
3403
3622
  this.toolCreator = creator;
3404
3623
  }
3405
- async execute(userPrompt, images, systemContext) {
3624
+ async execute(userPrompt, images, systemContext, signal) {
3625
+ this.signal = signal;
3406
3626
  this.taskId = crypto.randomUUID();
3407
3627
  this.setLabel("Administrator");
3408
3628
  this.setStatus("ACTIVE");
@@ -3413,10 +3633,12 @@ var T1Administrator = class extends BaseTier {
3413
3633
  status: "IN_PROGRESS"
3414
3634
  });
3415
3635
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
3636
+ this.throwIfCancelled();
3416
3637
  let enrichedPrompt = userPrompt;
3417
3638
  if (images?.length) {
3418
3639
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
3419
3640
  }
3641
+ this.throwIfCancelled();
3420
3642
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
3421
3643
  this.sendStatusUpdate({
3422
3644
  progressPct: 10,
@@ -3424,21 +3646,83 @@ var T1Administrator = class extends BaseTier {
3424
3646
  status: "IN_PROGRESS"
3425
3647
  });
3426
3648
  this.emit("plan", { taskId: this.taskId, plan });
3427
- const t2Results = await this.dispatchT2Managers(plan.sections);
3649
+ this.throwIfCancelled();
3650
+ let allT2Results = await this.dispatchT2Managers(plan.sections);
3651
+ let pass = 1;
3652
+ const MAX_REPLAN_PASSES = 2;
3653
+ while (pass <= MAX_REPLAN_PASSES) {
3654
+ const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
3655
+ if (reviewResult.approved) {
3656
+ this.log("T1 Review passed.");
3657
+ break;
3658
+ }
3659
+ this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
3660
+ this.sendStatusUpdate({
3661
+ progressPct: 80 + pass * 5,
3662
+ currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
3663
+ status: "IN_PROGRESS"
3664
+ });
3665
+ const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
3666
+ Review reason: ${reviewResult.reason}
3667
+
3668
+ Original goal: ${enrichedPrompt}
3669
+
3670
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
3671
+ const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
3672
+ allT2Results = [...allT2Results, ...correctionResults];
3673
+ pass++;
3674
+ }
3428
3675
  this.sendStatusUpdate({
3429
3676
  progressPct: 95,
3430
3677
  currentAction: "Compiling final output",
3431
3678
  status: "IN_PROGRESS"
3432
3679
  });
3433
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
3680
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
3434
3681
  this.setStatus("COMPLETED");
3435
3682
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
3436
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
3683
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
3437
3684
  }
3438
3685
  getEscalations() {
3439
3686
  return [...this.escalations];
3440
3687
  }
3441
3688
  // ── Private ──────────────────────────────────
3689
+ async reviewT2Outputs(originalPrompt, plan, t2Results) {
3690
+ const failedSections = t2Results.filter((r) => r.status === "FAILED");
3691
+ if (failedSections.length > 0) {
3692
+ return {
3693
+ approved: false,
3694
+ reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
3695
+ };
3696
+ }
3697
+ const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
3698
+ ${r.sectionSummary}`).join("\n\n");
3699
+ const prompt = `You are a strict QA Reviewer for the Cascade AI system.
3700
+ Review the following execution outputs against the original user prompt.
3701
+
3702
+ Original Request: ${originalPrompt}
3703
+
3704
+ T2 Manager Summaries:
3705
+ ${sectionsText}
3706
+
3707
+ Does the current state of the workspace and the outputs fully satisfy the user's request?
3708
+ If yes, reply with exactly: "APPROVED".
3709
+ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
3710
+ try {
3711
+ const result = await this.router.generate("T1", {
3712
+ messages: [{ role: "user", content: prompt }],
3713
+ systemPrompt: this.systemPromptOverride + "You are a QA Reviewer.",
3714
+ maxTokens: 500,
3715
+ temperature: 0
3716
+ });
3717
+ const response = result.content.trim();
3718
+ if (response.toUpperCase().startsWith("APPROVED")) {
3719
+ return { approved: true };
3720
+ }
3721
+ return { approved: false, reason: response.replace(/^REJECTED:\s*/i, "") };
3722
+ } catch {
3723
+ return { approved: true };
3724
+ }
3725
+ }
3442
3726
  async analyzeImages(prompt, images) {
3443
3727
  const visionModel = this.router.getModelForTier("T1");
3444
3728
  if (!visionModel?.isVisionCapable) return prompt;
@@ -3467,29 +3751,35 @@ ${systemContext}` : "";
3467
3751
  Example: if asked to create files "inside python_exclusive", every subtask that
3468
3752
  creates a file must use "python_exclusive/filename.ext" as the path.
3469
3753
 
3470
- Return JSON where subtasks can declare dependencies:
3754
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
3471
3755
  {
3472
3756
  "sections": [{
3757
+ "sectionId": "s1",
3758
+ "sectionTitle": "Setup Project",
3759
+ "description": "Initialize the project",
3760
+ "expectedOutput": "Basic structure created",
3761
+ "constraints": [],
3762
+ "dependsOn": [], // \u2190 empty = runs immediately
3473
3763
  "t3Subtasks": [{
3474
3764
  "subtaskId": "t1",
3475
- "subtaskTitle": "Generate Source Code",
3476
- "dependsOn": [], // \u2190 empty = runs immediately
3477
- "executionMode": "parallel"
3478
- }, {
3479
- "subtaskId": "t2",
3480
- "subtaskTitle": "Save Code to File",
3481
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
3482
- "executionMode": "parallel"
3483
- }, {
3484
- "subtaskId": "t3",
3485
- "subtaskTitle": "Execute and Verify",
3486
- "dependsOn": ["t2"], // \u2190 waits for t2
3487
- "executionMode": "parallel"
3765
+ "subtaskTitle": "Init NPM",
3766
+ "description": "Run npm init",
3767
+ "expectedOutput": "package.json created",
3768
+ "constraints": [],
3769
+ "dependsOn": []
3488
3770
  }]
3771
+ }, {
3772
+ "sectionId": "s2",
3773
+ "sectionTitle": "Write Tests",
3774
+ "description": "Write tests for the project",
3775
+ "expectedOutput": "Tests passing",
3776
+ "constraints": [],
3777
+ "dependsOn": ["s1"], // \u2190 waits for section s1 to complete first
3778
+ "t3Subtasks": [...]
3489
3779
  }]
3490
3780
  }
3491
- Use dependsOn when a subtask needs the output of a previous one.
3492
- Leave dependsOn empty for subtasks that can run immediately in parallel.`;
3781
+ Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
3782
+ Leave dependsOn empty for sections that can run immediately in parallel.`;
3493
3783
  const messages = [{ role: "user", content: decompositionPrompt }];
3494
3784
  const result = await this.router.generate("T1", {
3495
3785
  messages,
@@ -3617,92 +3907,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
3617
3907
  ].filter(Boolean).join(" ");
3618
3908
  m.setHierarchyContext(context);
3619
3909
  });
3620
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
3621
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
3622
- for (const section of sections) {
3623
- if (overlapSections.has(section.sectionId)) {
3624
- section.executionMode = "sequential";
3910
+ if (overlapSections.size > 0) {
3911
+ this.log("Overlap detected \u2014 adding sequential dependencies for conflicting sections to prevent race conditions");
3912
+ const overlapArray = Array.from(overlapSections);
3913
+ for (let i = 1; i < overlapArray.length; i++) {
3914
+ const section = sections.find((s) => s.sectionId === overlapArray[i]);
3915
+ if (section) {
3916
+ section.dependsOn = [...section.dependsOn || [], overlapArray[i - 1]];
3625
3917
  }
3626
3918
  }
3627
3919
  }
3628
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
3629
- const isSequential = sections.some((s) => s.executionMode === "sequential");
3630
3920
  const t2Results = [];
3631
3921
  try {
3632
- if (isSequential) {
3633
- this.log("Dispatching T2 managers sequentially");
3634
- for (let i = 0; i < managers.length; i++) {
3635
- const m = managers[i];
3636
- this.sendStatusUpdate({
3637
- progressPct: pct(i),
3638
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
3639
- status: "IN_PROGRESS"
3640
- });
3641
- try {
3642
- const result = await m.execute(sections[i], this.taskId);
3643
- t2Results.push(result);
3644
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
3645
- if (result.status === "ESCALATED") {
3646
- this.escalations.push({
3647
- raisedBy: `T2_${sections[i].sectionId}`,
3648
- sectionId: sections[i].sectionId,
3649
- attempted: result.issues,
3650
- blocker: result.issues.join("; "),
3651
- needs: "Human review required"
3652
- });
3653
- }
3654
- } catch (err) {
3655
- t2Results.push({
3656
- sectionId: sections[i].sectionId,
3657
- sectionTitle: sections[i].sectionTitle,
3658
- status: "FAILED",
3659
- t3Results: [],
3660
- sectionSummary: "",
3661
- issues: [err instanceof Error ? err.message : String(err)]
3662
- });
3922
+ t2Results.push(...await this.runT2sWithDependencies(sections, managers, this.taskId));
3923
+ } finally {
3924
+ cleanup();
3925
+ }
3926
+ return t2Results;
3927
+ }
3928
+ /**
3929
+ * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
3930
+ */
3931
+ async runT2sWithDependencies(sections, managers, taskId) {
3932
+ const adj = /* @__PURE__ */ new Map();
3933
+ const inDegree = /* @__PURE__ */ new Map();
3934
+ const resultMap = /* @__PURE__ */ new Map();
3935
+ const allKeys = new Set(sections.map((s) => s.sectionId));
3936
+ for (const s of sections) {
3937
+ if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
3938
+ inDegree.set(s.sectionId, 0);
3939
+ s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
3940
+ }
3941
+ for (const s of sections) {
3942
+ for (const dep of s.dependsOn ?? []) {
3943
+ adj.get(dep).add(s.sectionId);
3944
+ inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
3945
+ }
3946
+ }
3947
+ const queue = [];
3948
+ const degree = new Map(inDegree);
3949
+ for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
3950
+ const visited = /* @__PURE__ */ new Set();
3951
+ while (queue.length > 0) {
3952
+ const u = queue.shift();
3953
+ visited.add(u);
3954
+ for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
3955
+ const newDeg = (degree.get(v) ?? 1) - 1;
3956
+ degree.set(v, newDeg);
3957
+ if (newDeg === 0) queue.push(v);
3958
+ }
3959
+ }
3960
+ const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
3961
+ if (cycleNodes.length > 0) {
3962
+ this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
3963
+ for (const s of sections) {
3964
+ if (cycleNodes.includes(s.sectionId)) {
3965
+ const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
3966
+ for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
3967
+ inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
3968
+ adj.get(removed)?.delete(s.sectionId);
3663
3969
  }
3970
+ s.dependsOn = safeDeps;
3664
3971
  }
3665
- } else {
3666
- const results = await Promise.allSettled(
3667
- managers.map((m, i) => {
3668
- this.sendStatusUpdate({
3669
- progressPct: pct(i),
3670
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
3671
- status: "IN_PROGRESS"
3672
- });
3673
- return m.execute(sections[i], this.taskId);
3674
- })
3675
- );
3676
- for (let i = 0; i < results.length; i++) {
3677
- const r = results[i];
3678
- if (r.status === "fulfilled") {
3679
- t2Results.push(r.value);
3680
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
3681
- if (r.value.status === "ESCALATED") {
3682
- this.escalations.push({
3683
- raisedBy: `T2_${sections[i].sectionId}`,
3684
- sectionId: sections[i].sectionId,
3685
- attempted: r.value.issues,
3686
- blocker: r.value.issues.join("; "),
3687
- needs: "Human review required"
3688
- });
3689
- }
3690
- } else {
3691
- t2Results.push({
3692
- sectionId: sections[i].sectionId,
3693
- sectionTitle: sections[i].sectionTitle,
3694
- status: "FAILED",
3695
- t3Results: [],
3696
- sectionSummary: "",
3697
- issues: [r.reason instanceof Error ? r.reason.message : String(r.reason)]
3972
+ }
3973
+ }
3974
+ const totalSections = sections.length;
3975
+ let completedSections = 0;
3976
+ const executeWave = async () => {
3977
+ const readyIds = [];
3978
+ for (const [id, deg] of inDegree.entries()) {
3979
+ if (deg === 0 && !resultMap.has(id)) {
3980
+ readyIds.push(id);
3981
+ }
3982
+ }
3983
+ if (readyIds.length === 0) return;
3984
+ await Promise.all(readyIds.map(async (id) => {
3985
+ resultMap.set(id, null);
3986
+ const index = sections.findIndex((s) => s.sectionId === id);
3987
+ const section = sections[index];
3988
+ const manager = managers[index];
3989
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
3990
+ this.sendStatusUpdate({
3991
+ progressPct,
3992
+ currentAction: `T2 working on: ${section.sectionTitle}`,
3993
+ status: "IN_PROGRESS"
3994
+ });
3995
+ this.throwIfCancelled();
3996
+ let result;
3997
+ try {
3998
+ result = await manager.execute(section, taskId, this.signal);
3999
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
4000
+ if (result.status === "ESCALATED") {
4001
+ this.escalations.push({
4002
+ raisedBy: `T2_${section.sectionId}`,
4003
+ sectionId: section.sectionId,
4004
+ attempted: result.issues,
4005
+ blocker: result.issues.join("; "),
4006
+ needs: "Human review required"
3698
4007
  });
3699
4008
  }
4009
+ } catch (err) {
4010
+ result = {
4011
+ sectionId: section.sectionId,
4012
+ sectionTitle: section.sectionTitle,
4013
+ status: "FAILED",
4014
+ t3Results: [],
4015
+ sectionSummary: "",
4016
+ issues: [err instanceof Error ? err.message : String(err)]
4017
+ };
4018
+ }
4019
+ resultMap.set(id, result);
4020
+ completedSections++;
4021
+ for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
4022
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
3700
4023
  }
4024
+ }));
4025
+ if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
4026
+ await executeWave();
3701
4027
  }
3702
- } finally {
3703
- cleanup();
3704
- }
3705
- return t2Results;
4028
+ };
4029
+ await executeWave();
4030
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
3706
4031
  }
3707
4032
  async compileFinalOutput(originalPrompt, plan, t2Results) {
3708
4033
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -4149,13 +4474,47 @@ var GitHubTool = class extends BaseTool {
4149
4474
  }
4150
4475
  async execute(input, _options) {
4151
4476
  const platform = input["platform"] ?? "github";
4152
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
4153
4477
  const operation = input["operation"];
4154
4478
  const repo = input["repo"];
4155
- if (platform === "github") {
4156
- return this.executeGitHub(operation, repo, token, input);
4479
+ let token = input["token"];
4480
+ if (!token) {
4481
+ if (platform === "github") {
4482
+ token = process.env["GITHUB_TOKEN"];
4483
+ } else {
4484
+ token = process.env["GITLAB_TOKEN"];
4485
+ }
4486
+ }
4487
+ if (!token) {
4488
+ const envName = platform === "github" ? "GITHUB_TOKEN" : "GITLAB_TOKEN";
4489
+ return `Error: No ${platform} token provided. Set the ${envName} environment variable or pass a "token" field in the input.`;
4490
+ }
4491
+ try {
4492
+ if (platform === "github") {
4493
+ return await this.executeGitHub(operation, repo, token, input);
4494
+ }
4495
+ return await this.executeGitLab(operation, repo, token, input);
4496
+ } catch (err) {
4497
+ const axiosErr = err;
4498
+ if (axiosErr?.response?.status) {
4499
+ const status = axiosErr.response.status;
4500
+ const msg = axiosErr.response.data?.message ?? "";
4501
+ switch (status) {
4502
+ case 401:
4503
+ return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
4504
+ case 403:
4505
+ return `Permission denied: Your ${platform} token lacks the required scopes for this operation. Needed: repo or workflow.`;
4506
+ case 404:
4507
+ return `Not found: Repository "${repo}" does not exist, or your token cannot access it.`;
4508
+ case 422:
4509
+ return `Validation error from ${platform}: ${msg || "Check your input parameters (branch names, base/head refs, etc.)."}`;
4510
+ case 429:
4511
+ return `Rate limited by ${platform}. Please wait a moment before trying again.`;
4512
+ default:
4513
+ return `${platform} API error (${status}): ${msg || (axiosErr.message ?? "Unknown error")}`;
4514
+ }
4515
+ }
4516
+ return `${platform} request failed: ${axiosErr.message ?? String(err)}`;
4157
4517
  }
4158
- return this.executeGitLab(operation, repo, token, input);
4159
4518
  }
4160
4519
  async executeGitHub(operation, repo, token, input) {
4161
4520
  const headers = {
@@ -4242,6 +4601,7 @@ ${response.data.description}`;
4242
4601
  };
4243
4602
 
4244
4603
  // src/tools/browser.ts
4604
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
4245
4605
  var BrowserTool = class extends BaseTool {
4246
4606
  name = "browser";
4247
4607
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -4250,7 +4610,7 @@ var BrowserTool = class extends BaseTool {
4250
4610
  properties: {
4251
4611
  action: {
4252
4612
  type: "string",
4253
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
4613
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
4254
4614
  },
4255
4615
  url: { type: "string", description: "URL to navigate to" },
4256
4616
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -4270,53 +4630,86 @@ var BrowserTool = class extends BaseTool {
4270
4630
  try {
4271
4631
  playwright = await import('playwright');
4272
4632
  } catch {
4273
- throw new Error("Playwright is not installed. Run: npm install playwright && npx playwright install chromium");
4274
- }
4275
- if (!this.browser) {
4276
- const pw = playwright;
4277
- this.browser = await pw.chromium.launch({ headless: true });
4278
- const b = this.browser;
4279
- this.page = await b.newPage();
4633
+ return "Error: Playwright is not installed. Run: npm install playwright && npx playwright install chromium";
4280
4634
  }
4281
- const page = this.page;
4282
4635
  const action = input["action"];
4283
4636
  const timeout = input["timeout"] ?? 1e4;
4284
- switch (action) {
4285
- case "navigate": {
4286
- await page.goto(input["url"], { timeout });
4287
- return `Navigated to ${input["url"]}`;
4288
- }
4289
- case "click": {
4290
- await page.click(input["selector"], { timeout });
4291
- return `Clicked ${input["selector"]}`;
4292
- }
4293
- case "fill": {
4294
- await page.fill(input["selector"], input["value"]);
4295
- return `Filled ${input["selector"]} with value`;
4296
- }
4297
- case "screenshot": {
4298
- const buf = await page.screenshot({ type: "png" });
4299
- return `data:image/png;base64,${buf.toString("base64")}`;
4300
- }
4301
- case "evaluate": {
4302
- const result = await page.evaluate(input["script"]);
4303
- return JSON.stringify(result);
4637
+ if (action === "close") {
4638
+ await this.close();
4639
+ return "Browser closed.";
4640
+ }
4641
+ if (!this.browser || !this.page) {
4642
+ await this.close();
4643
+ const launchPromise = playwright.chromium.launch({ headless: true });
4644
+ const timeoutPromise = new Promise(
4645
+ (_, reject) => setTimeout(() => reject(new Error(`Browser launch timed out after ${BROWSER_LAUNCH_TIMEOUT_MS}ms. Is Chromium installed? Run: npx playwright install chromium`)), BROWSER_LAUNCH_TIMEOUT_MS)
4646
+ );
4647
+ try {
4648
+ this.browser = await Promise.race([launchPromise, timeoutPromise]);
4649
+ this.page = await this.browser.newPage();
4650
+ } catch (err) {
4651
+ this.browser = null;
4652
+ this.page = null;
4653
+ return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
4304
4654
  }
4305
- case "extract_text": {
4306
- const text = await page.locator("body").innerText();
4307
- return text.slice(0, 1e4);
4655
+ }
4656
+ const page = this.page;
4657
+ try {
4658
+ switch (action) {
4659
+ case "navigate": {
4660
+ await page.goto(input["url"], { timeout });
4661
+ const title = await page.title();
4662
+ return `Navigated to ${input["url"]} (title: "${title}")`;
4663
+ }
4664
+ case "click": {
4665
+ await page.click(input["selector"], { timeout });
4666
+ return `Clicked ${input["selector"]}`;
4667
+ }
4668
+ case "fill": {
4669
+ await page.fill(input["selector"], input["value"]);
4670
+ return `Filled ${input["selector"]} with value`;
4671
+ }
4672
+ case "screenshot": {
4673
+ const buf = await page.screenshot({ type: "png" });
4674
+ return `data:image/png;base64,${buf.toString("base64")}`;
4675
+ }
4676
+ case "evaluate": {
4677
+ const result = await page.evaluate(input["script"]);
4678
+ return JSON.stringify(result);
4679
+ }
4680
+ case "extract_text": {
4681
+ const text = await page.locator("body").innerText();
4682
+ return text.slice(0, 1e4);
4683
+ }
4684
+ case "wait": {
4685
+ await page.waitForTimeout(timeout);
4686
+ return `Waited ${timeout}ms`;
4687
+ }
4688
+ default:
4689
+ return `Unknown browser action: ${action}. Supported: navigate, click, fill, screenshot, evaluate, extract_text, wait, close`;
4308
4690
  }
4309
- case "wait": {
4310
- await page.waitForTimeout(timeout);
4311
- return `Waited ${timeout}ms`;
4691
+ } catch (err) {
4692
+ const errMsg = err instanceof Error ? err.message : String(err);
4693
+ if (/Target closed|Page crashed|Navigation failed/i.test(errMsg)) {
4694
+ await this.close();
4695
+ return `Browser error (page reset): ${errMsg}`;
4312
4696
  }
4313
- default:
4314
- throw new Error(`Unknown browser action: ${action}`);
4697
+ return `Browser action "${action}" failed: ${errMsg}`;
4315
4698
  }
4316
4699
  }
4317
4700
  async close() {
4318
- if (this.browser) {
4319
- await this.browser.close();
4701
+ try {
4702
+ if (this.page) {
4703
+ await this.page.close().catch(() => {
4704
+ });
4705
+ this.page = null;
4706
+ }
4707
+ if (this.browser) {
4708
+ await this.browser.close().catch(() => {
4709
+ });
4710
+ this.browser = null;
4711
+ }
4712
+ } catch {
4320
4713
  this.browser = null;
4321
4714
  this.page = null;
4322
4715
  }
@@ -4413,6 +4806,19 @@ var PDFCreateTool = class extends BaseTool {
4413
4806
  });
4414
4807
  }
4415
4808
  };
4809
+ function detectCommand(candidates) {
4810
+ for (const cmd of candidates) {
4811
+ try {
4812
+ const which = process.platform === "win32" ? "where" : "which";
4813
+ child_process.execSync(`${which} ${cmd}`, { stdio: "ignore" });
4814
+ return cmd;
4815
+ } catch {
4816
+ }
4817
+ }
4818
+ return null;
4819
+ }
4820
+ var PYTHON_CMD = detectCommand(["python3", "python"]);
4821
+ var NODE_CMD = detectCommand(["node"]);
4416
4822
  var CodeInterpreterTool = class extends BaseTool {
4417
4823
  name = "run_code";
4418
4824
  description = "Execute a Python or Node.js script to perform complex tasks (data processing, file conversion, etc.). The script is automatically cleaned up after execution.";
@@ -4428,10 +4834,30 @@ var CodeInterpreterTool = class extends BaseTool {
4428
4834
  isDangerous() {
4429
4835
  return true;
4430
4836
  }
4431
- async execute(input, options) {
4837
+ async execute(input, _options) {
4432
4838
  const language = input["language"];
4433
4839
  const code = input["code"];
4434
4840
  const args = input["args"] ?? [];
4841
+ let cmdPrefix;
4842
+ if (language === "python") {
4843
+ if (!PYTHON_CMD) {
4844
+ return [
4845
+ "Error: Python interpreter not found.",
4846
+ "Please install Python and ensure it is in your PATH.",
4847
+ "Tried: python3, python"
4848
+ ].join("\n");
4849
+ }
4850
+ cmdPrefix = PYTHON_CMD;
4851
+ } else {
4852
+ if (!NODE_CMD) {
4853
+ return [
4854
+ "Error: Node.js interpreter not found.",
4855
+ "Please install Node.js and ensure it is in your PATH.",
4856
+ "Tried: node"
4857
+ ].join("\n");
4858
+ }
4859
+ cmdPrefix = NODE_CMD;
4860
+ }
4435
4861
  const tmpDir = path13__default.default.join(process.cwd(), ".cascade", "tmp");
4436
4862
  if (!fs11__default.default.existsSync(tmpDir)) {
4437
4863
  fs11__default.default.mkdirSync(tmpDir, { recursive: true });
@@ -4440,8 +4866,9 @@ var CodeInterpreterTool = class extends BaseTool {
4440
4866
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
4441
4867
  const filePath = path13__default.default.join(tmpDir, fileName);
4442
4868
  fs11__default.default.writeFileSync(filePath, code, "utf-8");
4443
- const cmdPrefix = language === "python" ? "python3" : "node";
4444
- const fullCmd = `${cmdPrefix} "${filePath}" ${args.map((a) => `"${a}"`).join(" ")}`;
4869
+ const quotedPath = `"${filePath}"`;
4870
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
4871
+ const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
4445
4872
  return new Promise((resolve) => {
4446
4873
  const startMs = Date.now();
4447
4874
  child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -4454,10 +4881,17 @@ var CodeInterpreterTool = class extends BaseTool {
4454
4881
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
4455
4882
  }
4456
4883
  if (error) {
4457
- resolve(`Execution failed (${duration}ms):
4884
+ const timedOut = error.killed && duration >= 3e4;
4885
+ if (timedOut) {
4886
+ resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
4887
+ Partial stdout: ${stdout}
4888
+ Stderr: ${stderr}`);
4889
+ } else {
4890
+ resolve(`Execution failed (${duration}ms):
4458
4891
  Error: ${error.message}
4459
4892
  Stderr: ${stderr}
4460
4893
  Stdout: ${stdout}`);
4894
+ }
4461
4895
  } else {
4462
4896
  resolve(`Execution successful (${duration}ms):
4463
4897
  Stdout: ${stdout}
@@ -4522,6 +4956,186 @@ ${formatted}`;
4522
4956
  }
4523
4957
  };
4524
4958
 
4959
+ // src/tools/web-search.ts
4960
+ async function searchSearXNG(query, baseUrl, maxResults) {
4961
+ const url = new URL("/search", baseUrl);
4962
+ url.searchParams.set("q", query);
4963
+ url.searchParams.set("format", "json");
4964
+ url.searchParams.set("categories", "general");
4965
+ url.searchParams.set("engines", "google,bing,duckduckgo");
4966
+ const resp = await fetch(url.toString(), {
4967
+ headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
4968
+ signal: AbortSignal.timeout(1e4)
4969
+ });
4970
+ if (!resp.ok) {
4971
+ throw new Error(`SearXNG returned HTTP ${resp.status}`);
4972
+ }
4973
+ const data = await resp.json();
4974
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
4975
+ title: r.title ?? "",
4976
+ url: r.url ?? "",
4977
+ snippet: r.content ?? "",
4978
+ engine: `searxng(${r.engine ?? "unknown"})`
4979
+ }));
4980
+ }
4981
+ async function searchBrave(query, apiKey, maxResults) {
4982
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}&safesearch=off`;
4983
+ const resp = await fetch(url, {
4984
+ headers: {
4985
+ "Accept": "application/json",
4986
+ "Accept-Encoding": "gzip",
4987
+ "X-Subscription-Token": apiKey
4988
+ },
4989
+ signal: AbortSignal.timeout(1e4)
4990
+ });
4991
+ if (!resp.ok) {
4992
+ throw new Error(`Brave Search returned HTTP ${resp.status}`);
4993
+ }
4994
+ const data = await resp.json();
4995
+ return (data.web?.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
4996
+ title: r.title ?? "",
4997
+ url: r.url ?? "",
4998
+ snippet: r.description ?? "",
4999
+ engine: "brave"
5000
+ }));
5001
+ }
5002
+ async function searchTavily(query, apiKey, maxResults) {
5003
+ const resp = await fetch("https://api.tavily.com/search", {
5004
+ method: "POST",
5005
+ headers: {
5006
+ "Content-Type": "application/json",
5007
+ "Authorization": `Bearer ${apiKey}`
5008
+ },
5009
+ body: JSON.stringify({
5010
+ query,
5011
+ max_results: maxResults,
5012
+ search_depth: "basic",
5013
+ include_answer: false,
5014
+ include_raw_content: false
5015
+ }),
5016
+ signal: AbortSignal.timeout(15e3)
5017
+ });
5018
+ if (!resp.ok) {
5019
+ throw new Error(`Tavily returned HTTP ${resp.status}`);
5020
+ }
5021
+ const data = await resp.json();
5022
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
5023
+ title: r.title ?? "",
5024
+ url: r.url ?? "",
5025
+ snippet: r.content ?? "",
5026
+ engine: "tavily"
5027
+ }));
5028
+ }
5029
+ async function searchDuckDuckGoLite(query, maxResults) {
5030
+ const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
5031
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
5032
+ signal: AbortSignal.timeout(1e4)
5033
+ });
5034
+ if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
5035
+ const html = await resp.text();
5036
+ const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
5037
+ const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
5038
+ const links = [];
5039
+ const snippets = [];
5040
+ let m;
5041
+ while ((m = linkPattern.exec(html)) !== null) {
5042
+ links.push({ url: m[1], title: m[2].trim() });
5043
+ }
5044
+ while ((m = snippetPattern.exec(html)) !== null) {
5045
+ snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
5046
+ }
5047
+ return links.slice(0, maxResults).map((link, i) => ({
5048
+ title: link.title,
5049
+ url: link.url,
5050
+ snippet: snippets[i] ?? "",
5051
+ engine: "duckduckgo-lite"
5052
+ }));
5053
+ }
5054
+ var WebSearchTool = class extends BaseTool {
5055
+ name = "web_search";
5056
+ description = "Search the web for current information, news, documentation, or any topic. Returns a list of relevant results with titles, URLs, and snippets.";
5057
+ inputSchema = {
5058
+ type: "object",
5059
+ properties: {
5060
+ query: { type: "string", description: "The search query" },
5061
+ maxResults: { type: "number", description: "Number of results to return (default: 5, max: 10)" }
5062
+ },
5063
+ required: ["query"]
5064
+ };
5065
+ config;
5066
+ constructor(config = {}) {
5067
+ super();
5068
+ this.config = {
5069
+ searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
5070
+ braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
5071
+ tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
5072
+ maxResults: config.maxResults ?? 5
5073
+ };
5074
+ }
5075
+ async execute(input, _options) {
5076
+ const query = input["query"];
5077
+ if (!query?.trim()) return "Error: query is required and must be non-empty.";
5078
+ const maxResults = Math.min(
5079
+ input["maxResults"] ?? this.config.maxResults ?? 5,
5080
+ 10
5081
+ );
5082
+ const errors = [];
5083
+ let results = [];
5084
+ if (this.config.searxngUrl) {
5085
+ try {
5086
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
5087
+ if (results.length > 0) return this.formatResults(query, results);
5088
+ errors.push("SearXNG: returned 0 results");
5089
+ } catch (err) {
5090
+ errors.push(`SearXNG: ${err instanceof Error ? err.message : String(err)}`);
5091
+ }
5092
+ }
5093
+ if (this.config.braveApiKey) {
5094
+ try {
5095
+ results = await searchBrave(query, this.config.braveApiKey, maxResults);
5096
+ if (results.length > 0) return this.formatResults(query, results);
5097
+ errors.push("Brave: returned 0 results");
5098
+ } catch (err) {
5099
+ errors.push(`Brave: ${err instanceof Error ? err.message : String(err)}`);
5100
+ }
5101
+ }
5102
+ if (this.config.tavilyApiKey) {
5103
+ try {
5104
+ results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
5105
+ if (results.length > 0) return this.formatResults(query, results);
5106
+ errors.push("Tavily: returned 0 results");
5107
+ } catch (err) {
5108
+ errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
5109
+ }
5110
+ }
5111
+ try {
5112
+ results = await searchDuckDuckGoLite(query, maxResults);
5113
+ if (results.length > 0) return this.formatResults(query, results);
5114
+ errors.push("DuckDuckGo Lite: returned 0 results");
5115
+ } catch (err) {
5116
+ errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
5117
+ }
5118
+ const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
5119
+ return [
5120
+ `Web search for "${query}" failed across all backends:`,
5121
+ ...errors.map((e) => ` \u2022 ${e}`),
5122
+ configHint
5123
+ ].join("\n");
5124
+ }
5125
+ formatResults(query, results) {
5126
+ const lines = [`Web search results for: "${query}"`, ""];
5127
+ for (let i = 0; i < results.length; i++) {
5128
+ const r = results[i];
5129
+ lines.push(`[${i + 1}] ${r.title}`);
5130
+ lines.push(` URL: ${r.url}`);
5131
+ if (r.snippet) lines.push(` ${r.snippet.slice(0, 300)}`);
5132
+ if (r.engine) lines.push(` Source: ${r.engine}`);
5133
+ lines.push("");
5134
+ }
5135
+ return lines.join("\n");
5136
+ }
5137
+ };
5138
+
4525
5139
  // src/tools/mcp.ts
4526
5140
  var McpToolWrapper = class extends BaseTool {
4527
5141
  name;
@@ -4643,7 +5257,8 @@ var ToolRegistry = class {
4643
5257
  new ImageAnalyzeTool(),
4644
5258
  new PDFCreateTool(),
4645
5259
  new CodeInterpreterTool(),
4646
- new PeerCommunicationTool()
5260
+ new PeerCommunicationTool(),
5261
+ new WebSearchTool(this.config.webSearch)
4647
5262
  ];
4648
5263
  for (const tool of tools) {
4649
5264
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -4667,8 +5282,23 @@ var ToolRegistry = class {
4667
5282
  return this.ignoreMatcher.ignores(posixRel);
4668
5283
  }
4669
5284
  };
4670
- var McpClient = class {
5285
+ var McpClient = class _McpClient {
5286
+ static activeProcessPids = /* @__PURE__ */ new Set();
5287
+ /**
5288
+ * Forcefully kills all known MCP child processes.
5289
+ * Call this from global process exit handlers to prevent zombie processes.
5290
+ */
5291
+ static killAllProcesses() {
5292
+ for (const pid of _McpClient.activeProcessPids) {
5293
+ try {
5294
+ process.kill(pid, "SIGKILL");
5295
+ } catch {
5296
+ }
5297
+ }
5298
+ _McpClient.activeProcessPids.clear();
5299
+ }
4671
5300
  clients = /* @__PURE__ */ new Map();
5301
+ transports = /* @__PURE__ */ new Map();
4672
5302
  tools = /* @__PURE__ */ new Map();
4673
5303
  trustedServers;
4674
5304
  approvalCallback;
@@ -4697,6 +5327,8 @@ var McpClient = class {
4697
5327
  );
4698
5328
  await client.connect(transport);
4699
5329
  this.clients.set(server.name, client);
5330
+ this.transports.set(server.name, transport);
5331
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
4700
5332
  const toolsResult = await client.listTools();
4701
5333
  for (const tool of toolsResult.tools) {
4702
5334
  for (const existing of this.tools.values()) {
@@ -4718,8 +5350,11 @@ var McpClient = class {
4718
5350
  async disconnect(serverName) {
4719
5351
  const client = this.clients.get(serverName);
4720
5352
  if (client) {
5353
+ const transport = this.transports.get(serverName);
5354
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
4721
5355
  await client.close();
4722
5356
  this.clients.delete(serverName);
5357
+ this.transports.delete(serverName);
4723
5358
  for (const key of this.tools.keys()) {
4724
5359
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
4725
5360
  }
@@ -4747,6 +5382,13 @@ var McpClient = class {
4747
5382
  getConnectedServers() {
4748
5383
  return Array.from(this.clients.keys());
4749
5384
  }
5385
+ getActivePids() {
5386
+ const pids = [];
5387
+ for (const transport of this.transports.values()) {
5388
+ if (transport.pid) pids.push(transport.pid);
5389
+ }
5390
+ return pids;
5391
+ }
4750
5392
  isConnected(serverName) {
4751
5393
  return this.clients.has(serverName);
4752
5394
  }
@@ -4885,12 +5527,24 @@ var McpServerConfigSchema = zod.z.object({
4885
5527
  args: zod.z.array(zod.z.string()).optional(),
4886
5528
  env: zod.z.record(zod.z.string()).optional()
4887
5529
  });
5530
+ var WebSearchConfigSchema = zod.z.object({
5531
+ /** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
5532
+ searxngUrl: zod.z.string().optional(),
5533
+ /** Brave Search API key — get one at https://api.search.brave.com */
5534
+ braveApiKey: zod.z.string().optional(),
5535
+ /** Tavily API key — get one at https://tavily.com */
5536
+ tavilyApiKey: zod.z.string().optional(),
5537
+ /** Max results per search (default 5) */
5538
+ maxResults: zod.z.number().default(5)
5539
+ });
4888
5540
  var ToolsConfigSchema = zod.z.object({
4889
5541
  shellAllowlist: zod.z.array(zod.z.string()).default([]),
4890
5542
  shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
4891
5543
  requireApprovalFor: zod.z.array(zod.z.string()).default([]),
4892
5544
  browserEnabled: zod.z.boolean().default(false),
4893
- mcpServers: zod.z.array(McpServerConfigSchema).optional()
5545
+ mcpServers: zod.z.array(McpServerConfigSchema).optional(),
5546
+ /** Web search backends — at least one should be configured for best results */
5547
+ webSearch: WebSearchConfigSchema.optional()
4894
5548
  });
4895
5549
  var HookDefinitionSchema = zod.z.object({
4896
5550
  command: zod.z.string(),
@@ -5432,12 +6086,25 @@ var Cascade = class extends EventEmitter__default.default {
5432
6086
  looksLikeSimpleArtifactTask(prompt) {
5433
6087
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
5434
6088
  }
5435
- async determineComplexity(prompt, conversationHistory = []) {
6089
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
5436
6090
  if (this.looksLikeSimpleArtifactTask(prompt)) {
5437
6091
  return "Simple";
5438
6092
  }
6093
+ let workspaceContext = "";
6094
+ try {
6095
+ const files = await glob.glob("**/*.*", {
6096
+ cwd: workspacePath,
6097
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
6098
+ nodir: true
6099
+ });
6100
+ workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
6101
+ } catch {
6102
+ workspaceContext = "Workspace Scout: Could not scan workspace.";
6103
+ }
5439
6104
  const sysPrompt = `You are a routing classifier for a hierarchical AI system. Determine task complexity using BOTH the latest user message and the recent conversation context.
5440
6105
 
6106
+ ${workspaceContext}
6107
+
5441
6108
  Classification:
5442
6109
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
5443
6110
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -5512,7 +6179,7 @@ ${prompt}` : prompt;
5512
6179
  }
5513
6180
  escalator.resolveUserDecision(req.id, approved, always);
5514
6181
  });
5515
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
6182
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
5516
6183
  this.telemetry.capture("cascade:session_start", {
5517
6184
  complexity,
5518
6185
  providerCount: this.config.providers.length,
@@ -5592,7 +6259,7 @@ ${prompt}` : prompt;
5592
6259
  peerT3Ids: [],
5593
6260
  parentT2: "root"
5594
6261
  };
5595
- const t3Result = await t3.execute(assignment, taskId);
6262
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
5596
6263
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
5597
6264
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
5598
6265
  } else if (complexity === "Moderate") {
@@ -5615,7 +6282,7 @@ ${prompt}` : prompt;
5615
6282
  constraints: [],
5616
6283
  t3Subtasks: []
5617
6284
  };
5618
- const t2Result = await t2.execute(assignment, taskId);
6285
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
5619
6286
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
5620
6287
  t2Results = [t2Result];
5621
6288
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -5637,13 +6304,22 @@ ${prompt}` : prompt;
5637
6304
  if (toolCreator) t1.setToolCreator(toolCreator);
5638
6305
  bindTierEvents(t1);
5639
6306
  t1.on("plan", (e) => this.emit("plan", e));
5640
- const result = await t1.execute(options.prompt, options.images);
6307
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
5641
6308
  finalOutput = result.output;
5642
6309
  t2Results = result.t2Results;
5643
6310
  }
5644
6311
  } catch (err) {
5645
- runError = err;
5646
- throw err;
6312
+ if (err instanceof CascadeCancelledError) {
6313
+ this.emit("run:cancelled", {
6314
+ taskId,
6315
+ reason: err.message,
6316
+ partialOutput: finalOutput || ""
6317
+ });
6318
+ runError = null;
6319
+ } else {
6320
+ runError = err;
6321
+ throw err;
6322
+ }
5647
6323
  } finally {
5648
6324
  try {
5649
6325
  escalator.cancelAllPending();
@@ -5965,9 +6641,10 @@ var MemoryStore = class _MemoryStore {
5965
6641
  constructor(dbPath) {
5966
6642
  fs11__default.default.mkdirSync(path13__default.default.dirname(dbPath), { recursive: true });
5967
6643
  try {
5968
- this.db = new Database__default.default(dbPath);
6644
+ this.db = new Database__default.default(dbPath, { timeout: 5e3 });
5969
6645
  this.db.pragma("journal_mode = WAL");
5970
6646
  this.db.pragma("foreign_keys = ON");
6647
+ this.db.pragma("synchronous = NORMAL");
5971
6648
  this.migrate();
5972
6649
  } catch (err) {
5973
6650
  if (err instanceof Error && err.message.includes("Could not locate the bindings file")) {
@@ -5981,6 +6658,38 @@ Original error: ${err.message}`
5981
6658
  throw err;
5982
6659
  }
5983
6660
  }
6661
+ // ── Async Write Queue ─────────────────────────
6662
+ writeQueue = [];
6663
+ isProcessingQueue = false;
6664
+ async processQueue() {
6665
+ if (this.isProcessingQueue) return;
6666
+ this.isProcessingQueue = true;
6667
+ while (this.writeQueue.length > 0) {
6668
+ const op = this.writeQueue.shift();
6669
+ if (op) {
6670
+ let attempts = 0;
6671
+ while (attempts < 5) {
6672
+ try {
6673
+ op();
6674
+ break;
6675
+ } catch (err) {
6676
+ if (err instanceof Error && err.code === "SQLITE_BUSY") {
6677
+ attempts++;
6678
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempts)));
6679
+ } else {
6680
+ console.error("Cascade AI: DB Write Error:", err);
6681
+ break;
6682
+ }
6683
+ }
6684
+ }
6685
+ }
6686
+ }
6687
+ this.isProcessingQueue = false;
6688
+ }
6689
+ enqueueWrite(op) {
6690
+ this.writeQueue.push(op);
6691
+ this.processQueue().catch(console.error);
6692
+ }
5984
6693
  // ── Sessions ──────────────────────────────────
5985
6694
  createSession(session) {
5986
6695
  this.db.prepare(`
@@ -6067,26 +6776,28 @@ Original error: ${err.message}`
6067
6776
  }
6068
6777
  // ── Runtime Sessions / Nodes ─────────────────
6069
6778
  upsertRuntimeSession(session) {
6070
- this.db.prepare(`
6071
- INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
6072
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6073
- ON CONFLICT(session_id) DO UPDATE SET
6074
- title = excluded.title,
6075
- workspace_path = excluded.workspace_path,
6076
- status = excluded.status,
6077
- updated_at = excluded.updated_at,
6078
- latest_prompt = excluded.latest_prompt,
6079
- is_global = excluded.is_global
6080
- `).run(
6081
- session.sessionId,
6082
- session.title,
6083
- session.workspacePath,
6084
- session.status,
6085
- session.startedAt,
6086
- session.updatedAt,
6087
- session.latestPrompt ?? null,
6088
- session.isGlobal ? 1 : 0
6089
- );
6779
+ this.enqueueWrite(() => {
6780
+ this.db.prepare(`
6781
+ INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
6782
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6783
+ ON CONFLICT(session_id) DO UPDATE SET
6784
+ title = excluded.title,
6785
+ workspace_path = excluded.workspace_path,
6786
+ status = excluded.status,
6787
+ updated_at = excluded.updated_at,
6788
+ latest_prompt = excluded.latest_prompt,
6789
+ is_global = excluded.is_global
6790
+ `).run(
6791
+ session.sessionId,
6792
+ session.title,
6793
+ session.workspacePath,
6794
+ session.status,
6795
+ session.startedAt,
6796
+ session.updatedAt,
6797
+ session.latestPrompt ?? null,
6798
+ session.isGlobal ? 1 : 0
6799
+ );
6800
+ });
6090
6801
  }
6091
6802
  listRuntimeSessions(limit = 100) {
6092
6803
  const rows = this.db.prepare(`
@@ -6104,33 +6815,35 @@ Original error: ${err.message}`
6104
6815
  }));
6105
6816
  }
6106
6817
  upsertRuntimeNode(node) {
6107
- this.db.prepare(`
6108
- INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
6109
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6110
- ON CONFLICT(tier_id) DO UPDATE SET
6111
- session_id = excluded.session_id,
6112
- parent_id = excluded.parent_id,
6113
- role = excluded.role,
6114
- label = excluded.label,
6115
- status = excluded.status,
6116
- current_action = excluded.current_action,
6117
- progress_pct = excluded.progress_pct,
6118
- updated_at = excluded.updated_at,
6119
- workspace_path = excluded.workspace_path,
6120
- is_global = excluded.is_global
6121
- `).run(
6122
- node.tierId,
6123
- node.sessionId,
6124
- node.parentId ?? null,
6125
- node.role,
6126
- node.label,
6127
- node.status,
6128
- node.currentAction ?? null,
6129
- node.progressPct ?? null,
6130
- node.updatedAt,
6131
- node.workspacePath ?? null,
6132
- node.isGlobal ? 1 : 0
6133
- );
6818
+ this.enqueueWrite(() => {
6819
+ this.db.prepare(`
6820
+ INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
6821
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6822
+ ON CONFLICT(tier_id) DO UPDATE SET
6823
+ session_id = excluded.session_id,
6824
+ parent_id = excluded.parent_id,
6825
+ role = excluded.role,
6826
+ label = excluded.label,
6827
+ status = excluded.status,
6828
+ current_action = excluded.current_action,
6829
+ progress_pct = excluded.progress_pct,
6830
+ updated_at = excluded.updated_at,
6831
+ workspace_path = excluded.workspace_path,
6832
+ is_global = excluded.is_global
6833
+ `).run(
6834
+ node.tierId,
6835
+ node.sessionId,
6836
+ node.parentId ?? null,
6837
+ node.role,
6838
+ node.label,
6839
+ node.status,
6840
+ node.currentAction ?? null,
6841
+ node.progressPct ?? null,
6842
+ node.updatedAt,
6843
+ node.workspacePath ?? null,
6844
+ node.isGlobal ? 1 : 0
6845
+ );
6846
+ });
6134
6847
  }
6135
6848
  listRuntimeNodes(sessionId, limit = 500) {
6136
6849
  const rows = sessionId ? this.db.prepare(`
@@ -6153,30 +6866,32 @@ Original error: ${err.message}`
6153
6866
  }));
6154
6867
  }
6155
6868
  addRuntimeNodeLog(log) {
6156
- this.db.prepare(`
6157
- INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
6158
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6159
- `).run(
6160
- log.id,
6161
- log.sessionId,
6162
- log.tierId,
6163
- log.role,
6164
- log.label,
6165
- log.status,
6166
- log.currentAction ?? null,
6167
- log.progressPct ?? null,
6168
- log.timestamp,
6169
- log.workspacePath ?? null,
6170
- log.isGlobal ? 1 : 0
6171
- );
6172
- this.db.prepare(`
6173
- DELETE FROM runtime_node_logs
6174
- WHERE id NOT IN (
6175
- SELECT id FROM runtime_node_logs
6176
- ORDER BY timestamp DESC
6177
- LIMIT 2000
6178
- )
6179
- `).run();
6869
+ this.enqueueWrite(() => {
6870
+ this.db.prepare(`
6871
+ INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
6872
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6873
+ `).run(
6874
+ log.id,
6875
+ log.sessionId,
6876
+ log.tierId,
6877
+ log.role,
6878
+ log.label,
6879
+ log.status,
6880
+ log.currentAction ?? null,
6881
+ log.progressPct ?? null,
6882
+ log.timestamp,
6883
+ log.workspacePath ?? null,
6884
+ log.isGlobal ? 1 : 0
6885
+ );
6886
+ this.db.prepare(`
6887
+ DELETE FROM runtime_node_logs
6888
+ WHERE id NOT IN (
6889
+ SELECT id FROM runtime_node_logs
6890
+ ORDER BY timestamp DESC
6891
+ LIMIT 2000
6892
+ )
6893
+ `).run();
6894
+ });
6180
6895
  }
6181
6896
  listRuntimeNodeLogs(sessionId, tierId, limit = 200) {
6182
6897
  let rows;
@@ -6214,19 +6929,21 @@ Original error: ${err.message}`
6214
6929
  }
6215
6930
  // ── Messages ──────────────────────────────────
6216
6931
  addMessage(message) {
6217
- this.db.prepare(`
6218
- INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
6219
- VALUES (?, ?, ?, ?, ?, ?, ?)
6220
- `).run(
6221
- message.id,
6222
- message.sessionId,
6223
- message.role,
6224
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
6225
- message.timestamp,
6226
- message.tokens ? JSON.stringify(message.tokens) : null,
6227
- message.agentMessages ? JSON.stringify(message.agentMessages) : null
6228
- );
6229
- this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
6932
+ this.enqueueWrite(() => {
6933
+ this.db.prepare(`
6934
+ INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
6935
+ VALUES (?, ?, ?, ?, ?, ?, ?)
6936
+ `).run(
6937
+ message.id,
6938
+ message.sessionId,
6939
+ message.role,
6940
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
6941
+ message.timestamp,
6942
+ message.tokens ? JSON.stringify(message.tokens) : null,
6943
+ message.agentMessages ? JSON.stringify(message.agentMessages) : null
6944
+ );
6945
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
6946
+ });
6230
6947
  }
6231
6948
  getSessionMessages(sessionId) {
6232
6949
  const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
@@ -6323,10 +7040,12 @@ Original error: ${err.message}`
6323
7040
  }
6324
7041
  // ── Audit Log ─────────────────────────────────
6325
7042
  addAuditEntry(entry) {
6326
- this.db.prepare(`
6327
- INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
6328
- VALUES (?, ?, ?, ?, ?, ?)
6329
- `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
7043
+ this.enqueueWrite(() => {
7044
+ this.db.prepare(`
7045
+ INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
7046
+ VALUES (?, ?, ?, ?, ?, ?)
7047
+ `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
7048
+ });
6330
7049
  }
6331
7050
  getAuditLog(sessionId, limit = 100) {
6332
7051
  const rows = this.db.prepare("SELECT * FROM audit_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
@@ -6341,10 +7060,12 @@ Original error: ${err.message}`
6341
7060
  }
6342
7061
  // ── File Snapshots ────────────────────────────
6343
7062
  addFileSnapshot(sessionId, filePath, content) {
6344
- this.db.prepare(`
6345
- INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
6346
- VALUES (?, ?, ?, ?, ?)
6347
- `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
7063
+ this.enqueueWrite(() => {
7064
+ this.db.prepare(`
7065
+ INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
7066
+ VALUES (?, ?, ?, ?, ?)
7067
+ `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
7068
+ });
6348
7069
  }
6349
7070
  getLatestFileSnapshots(sessionId) {
6350
7071
  const rows = this.db.prepare(`
@@ -6640,7 +7361,7 @@ var ConfigManager = class {
6640
7361
  globalDir;
6641
7362
  constructor(workspacePath = process.cwd()) {
6642
7363
  this.workspacePath = workspacePath;
6643
- this.globalDir = path13__default.default.join(os__default.default.homedir(), GLOBAL_CONFIG_DIR);
7364
+ this.globalDir = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR);
6644
7365
  }
6645
7366
  async load() {
6646
7367
  this.config = await this.loadConfig();
@@ -7010,7 +7731,7 @@ var DashboardServer = class {
7010
7731
  // ── Setup ─────────────────────────────────────
7011
7732
  getGlobalStore() {
7012
7733
  if (!this.globalStore) {
7013
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7734
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7014
7735
  this.globalStore = new MemoryStore(globalDbPath);
7015
7736
  }
7016
7737
  return this.globalStore;
@@ -7072,7 +7793,7 @@ var DashboardServer = class {
7072
7793
  }
7073
7794
  watchRuntimeChanges() {
7074
7795
  const workspaceDbPath = path13__default.default.join(this.workspacePath, CASCADE_DB_FILE);
7075
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7796
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7076
7797
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
7077
7798
  for (const watchPath of watchPaths) {
7078
7799
  if (!fs11__default.default.existsSync(watchPath)) continue;
@@ -7180,7 +7901,7 @@ var DashboardServer = class {
7180
7901
  const sessionId = req.params.id;
7181
7902
  this.store.deleteSession(sessionId);
7182
7903
  this.store.deleteRuntimeSession(sessionId);
7183
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7904
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7184
7905
  const globalStore = new MemoryStore(globalDbPath);
7185
7906
  try {
7186
7907
  globalStore.deleteRuntimeSession(sessionId);
@@ -7194,7 +7915,7 @@ var DashboardServer = class {
7194
7915
  });
7195
7916
  this.app.delete("/api/sessions", auth, (req, res) => {
7196
7917
  const body = req.body;
7197
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7918
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7198
7919
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
7199
7920
  const globalStore = new MemoryStore(globalDbPath);
7200
7921
  try {
@@ -7217,7 +7938,7 @@ var DashboardServer = class {
7217
7938
  });
7218
7939
  this.app.delete("/api/runtime", auth, (_req, res) => {
7219
7940
  this.store.deleteAllRuntimeNodes();
7220
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7941
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7221
7942
  const globalStore = new MemoryStore(globalDbPath);
7222
7943
  try {
7223
7944
  globalStore.deleteAllRuntimeNodes();
@@ -7313,7 +8034,7 @@ var DashboardServer = class {
7313
8034
  this.app.get("/api/runtime", auth, (req, res) => {
7314
8035
  const scope = req.query["scope"] ?? "workspace";
7315
8036
  if (scope === "global") {
7316
- const globalDbPath = path13__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
8037
+ const globalDbPath = path13__default.default.join(os2__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7317
8038
  const globalStore = new MemoryStore(globalDbPath);
7318
8039
  try {
7319
8040
  res.json({
@@ -7537,8 +8258,10 @@ exports.CASCADE_MD_FILE = CASCADE_MD_FILE;
7537
8258
  exports.CASCADE_VERSION = CASCADE_VERSION;
7538
8259
  exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
7539
8260
  exports.Cascade = Cascade;
8261
+ exports.CascadeCancelledError = CascadeCancelledError;
7540
8262
  exports.CascadeIgnore = CascadeIgnore;
7541
8263
  exports.CascadeRouter = CascadeRouter;
8264
+ exports.CascadeToolError = CascadeToolError;
7542
8265
  exports.ConfigManager = ConfigManager;
7543
8266
  exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
7544
8267
  exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;