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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import EventEmitter from 'events';
2
2
  import crypto, { randomUUID, timingSafeEqual } from 'crypto';
3
+ import { glob } from 'glob';
3
4
  import Anthropic from '@anthropic-ai/sdk';
4
5
  import OpenAI from 'openai';
5
6
  import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
@@ -8,7 +9,7 @@ import fs2 from 'fs/promises';
8
9
  import path13 from 'path';
9
10
  import * as ignoreFactory from 'ignore';
10
11
  import ignoreFactory__default from 'ignore';
11
- import { exec, execFile } from 'child_process';
12
+ import { exec, execFile, execSync } from 'child_process';
12
13
  import { promisify } from 'util';
13
14
  import { simpleGit } from 'simple-git';
14
15
  import fs11 from 'fs';
@@ -17,7 +18,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
17
18
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
18
19
  import { z } from 'zod';
19
20
  import { createContext, runInContext } from 'vm';
20
- import os from 'os';
21
+ import os2 from 'os';
21
22
  import Database from 'better-sqlite3';
22
23
  import { createServer } from 'http';
23
24
  import { fileURLToPath } from 'url';
@@ -123,7 +124,7 @@ var require_keytar2 = __commonJS({
123
124
  });
124
125
 
125
126
  // src/constants.ts
126
- var CASCADE_VERSION = "0.2.2";
127
+ var CASCADE_VERSION = "0.2.11";
127
128
  var CASCADE_CONFIG_DIR = ".cascade";
128
129
  var CASCADE_MD_FILE = "CASCADE.md";
129
130
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -426,7 +427,8 @@ var TOOL_NAMES = {
426
427
  IMAGE_ANALYZE: "image_analyze",
427
428
  PDF_CREATE: "pdf_create",
428
429
  RUN_CODE: "run_code",
429
- PEER_MESSAGE: "peer_message"
430
+ PEER_MESSAGE: "peer_message",
431
+ WEB_SEARCH: "web_search"
430
432
  };
431
433
  var DEFAULT_APPROVAL_REQUIRED = [
432
434
  TOOL_NAMES.SHELL,
@@ -575,33 +577,61 @@ var AnthropicProvider = class extends BaseProvider {
575
577
  }
576
578
  }
577
579
  convertMessages(messages) {
578
- return messages.filter((m) => m.role !== "system").map((m) => {
579
- if (typeof m.content === "string") {
580
- return { role: m.role, content: m.content };
580
+ const result = [];
581
+ for (const m of messages) {
582
+ if (m.role === "system") continue;
583
+ if (m.role === "tool") {
584
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
585
+ result.push({
586
+ role: "user",
587
+ content: [{
588
+ type: "tool_result",
589
+ tool_use_id: m.toolCallId ?? "",
590
+ content: toolContent
591
+ }]
592
+ });
593
+ continue;
581
594
  }
582
- const content = m.content.map((block) => {
583
- if (block.type === "text") return { type: "text", text: block.text };
584
- if (block.type === "image") {
585
- const img = block.image;
586
- if (img.type === "base64") {
587
- return {
588
- type: "image",
589
- source: {
590
- type: "base64",
591
- media_type: img.mimeType,
592
- data: img.data
595
+ if (m.role === "assistant") {
596
+ const content = [];
597
+ const text = typeof m.content === "string" ? m.content : "";
598
+ if (text) content.push({ type: "text", text });
599
+ for (const tc of m.toolCalls ?? []) {
600
+ content.push({
601
+ type: "tool_use",
602
+ id: tc.id,
603
+ name: tc.name,
604
+ input: tc.input
605
+ });
606
+ }
607
+ if (content.length > 0) {
608
+ result.push({ role: "assistant", content });
609
+ }
610
+ continue;
611
+ }
612
+ if (m.role === "user") {
613
+ if (typeof m.content === "string") {
614
+ result.push({ role: "user", content: m.content });
615
+ } else {
616
+ const content = m.content.map((block) => {
617
+ if (block.type === "text") return { type: "text", text: block.text };
618
+ if (block.type === "image") {
619
+ const img = block.image;
620
+ if (img.type === "base64") {
621
+ return {
622
+ type: "image",
623
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
624
+ };
593
625
  }
594
- };
595
- }
596
- return {
597
- type: "image",
598
- source: { type: "url", url: img.data }
599
- };
626
+ return { type: "image", source: { type: "url", url: img.data } };
627
+ }
628
+ return { type: "text", text: "" };
629
+ });
630
+ result.push({ role: "user", content });
600
631
  }
601
- return { type: "text", text: "" };
602
- });
603
- return { role: m.role, content };
604
- });
632
+ }
633
+ }
634
+ return result;
605
635
  }
606
636
  };
607
637
  var OpenAIProvider = class extends BaseProvider {
@@ -862,7 +892,7 @@ var GeminiProvider = class extends BaseProvider {
862
892
  for (const part of candidate?.content?.parts ?? []) {
863
893
  if (part.functionCall) {
864
894
  toolCalls.push({
865
- id: `gemini-tool-${Date.now()}-${toolCalls.length}`,
895
+ id: part.functionCall.name,
866
896
  name: part.functionCall.name,
867
897
  input: part.functionCall.args ?? {}
868
898
  });
@@ -950,10 +980,70 @@ var GeminiProvider = class extends BaseProvider {
950
980
  }
951
981
  // ── Private ──────────────────────────────────
952
982
  buildContents(messages, extraImages) {
953
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
954
- role: m.role === "assistant" ? "model" : "user",
955
- parts: typeof m.content === "string" ? [{ text: m.content }] : this.convertMessageContent(m, extraImages)
956
- }));
983
+ const contents = [];
984
+ for (const m of messages) {
985
+ if (m.role === "system") {
986
+ const text = typeof m.content === "string" ? m.content : "";
987
+ if (!text.trim()) continue;
988
+ const prev = contents[contents.length - 1];
989
+ if (prev?.role === "user") {
990
+ prev.parts.unshift({ text: `[System context]: ${text}
991
+
992
+ ` });
993
+ } else {
994
+ contents.push({ role: "user", parts: [{ text: `[System context]: ${text}` }] });
995
+ }
996
+ continue;
997
+ }
998
+ if (m.role === "tool") {
999
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1000
+ const functionName = m.toolCallId ?? "unknown_function";
1001
+ contents.push({
1002
+ role: "user",
1003
+ parts: [{
1004
+ functionResponse: {
1005
+ name: functionName,
1006
+ response: { output: toolContent }
1007
+ }
1008
+ }]
1009
+ });
1010
+ continue;
1011
+ }
1012
+ if (m.role === "assistant") {
1013
+ const parts = [];
1014
+ const textContent = typeof m.content === "string" ? m.content : "";
1015
+ if (textContent) parts.push({ text: textContent });
1016
+ for (const tc of m.toolCalls ?? []) {
1017
+ parts.push({
1018
+ functionCall: {
1019
+ name: tc.name,
1020
+ args: tc.input
1021
+ }
1022
+ });
1023
+ }
1024
+ if (parts.length > 0) {
1025
+ contents.push({ role: "model", parts });
1026
+ }
1027
+ continue;
1028
+ }
1029
+ if (m.role === "user") {
1030
+ const parts = this.convertMessageContent(m, contents.length === 0 ? extraImages : void 0);
1031
+ if (extraImages?.length && contents.length > 0) {
1032
+ const isLastUser = !messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
1033
+ if (isLastUser) {
1034
+ for (const img of extraImages) {
1035
+ if (img.type === "base64") {
1036
+ parts.push({ inlineData: { mimeType: img.mimeType, data: img.data } });
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ if (parts.length > 0) {
1042
+ contents.push({ role: "user", parts });
1043
+ }
1044
+ }
1045
+ }
1046
+ return contents;
957
1047
  }
958
1048
  convertMessageContent(msg, extraImages) {
959
1049
  const parts = [];
@@ -1820,6 +1910,29 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
1820
1910
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
1821
1911
  }
1822
1912
  };
1913
+
1914
+ // src/utils/retry.ts
1915
+ var CascadeCancelledError = class extends Error {
1916
+ constructor(reason) {
1917
+ super(reason ?? "Run was cancelled via AbortSignal");
1918
+ this.name = "CascadeCancelledError";
1919
+ }
1920
+ };
1921
+ var CascadeToolError = class extends Error {
1922
+ /** A friendly message to show the user / T3 */
1923
+ userMessage;
1924
+ /** Whether this error class is retryable by default */
1925
+ retryable;
1926
+ constructor(userMessage, cause, retryable = false) {
1927
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
1928
+ super(`${userMessage}: ${causeMsg}`);
1929
+ this.name = "CascadeToolError";
1930
+ this.userMessage = userMessage;
1931
+ this.retryable = retryable;
1932
+ }
1933
+ };
1934
+
1935
+ // src/core/tiers/base.ts
1823
1936
  var BaseTier = class extends EventEmitter {
1824
1937
  id;
1825
1938
  role;
@@ -1829,6 +1942,8 @@ var BaseTier = class extends EventEmitter {
1829
1942
  label;
1830
1943
  systemPromptOverride = "";
1831
1944
  hierarchyContext = "";
1945
+ /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
1946
+ signal;
1832
1947
  constructor(role, id, parentId) {
1833
1948
  super();
1834
1949
  this.role = role;
@@ -1891,6 +2006,18 @@ var BaseTier = class extends EventEmitter {
1891
2006
  log(message, data) {
1892
2007
  this.emit("log", { tierId: this.id, role: this.role, message, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1893
2008
  }
2009
+ /**
2010
+ * Throws `CascadeCancelledError` if the run's `AbortSignal` has fired.
2011
+ * Call this at safe checkpoints (before LLM calls, between T3 dispatches)
2012
+ * to provide a fast, clean cancellation path.
2013
+ */
2014
+ throwIfCancelled() {
2015
+ if (this.signal?.aborted) {
2016
+ throw new CascadeCancelledError(
2017
+ typeof this.signal.reason === "string" ? this.signal.reason : "Run cancelled by caller"
2018
+ );
2019
+ }
2020
+ }
1894
2021
  };
1895
2022
 
1896
2023
  // src/core/context/manager.ts
@@ -2091,6 +2218,7 @@ Rules:
2091
2218
  - Execute the subtask completely \u2014 do not stop partway through.
2092
2219
  - Use tools when needed. Ask for approval only when the tool registry requires it.
2093
2220
  - 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.
2221
+ - Use the "web_search" tool to find current information, documentation, news, or general web data.
2094
2222
  - Use the "pdf_create" tool for PDF requests.
2095
2223
  - 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.
2096
2224
  - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
@@ -2134,7 +2262,8 @@ var T3Worker = class extends BaseTier {
2134
2262
  this.store = store;
2135
2263
  this.audit = new AuditLogger(store, sessionId);
2136
2264
  }
2137
- async execute(assignment, taskId) {
2265
+ async execute(assignment, taskId, signal) {
2266
+ this.signal = signal;
2138
2267
  this.assignment = assignment;
2139
2268
  this.taskId = taskId;
2140
2269
  this.setLabel(assignment.subtaskTitle);
@@ -2284,6 +2413,7 @@ Now execute your subtask using this context where relevant.`
2284
2413
  tools = [...tools];
2285
2414
  while (iterations < MAX_ITERATIONS) {
2286
2415
  iterations++;
2416
+ this.throwIfCancelled();
2287
2417
  const options = {
2288
2418
  messages: this.context.getMessages(),
2289
2419
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -2304,21 +2434,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
2304
2434
  if (requiresArtifact) {
2305
2435
  stalledArtifactIterations += 1;
2306
2436
  if (stalledArtifactIterations >= 2) {
2307
- if (this.toolCreator && stalledArtifactIterations === 2) {
2308
- const toolName = await this.toolCreator.createTool(
2309
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
2310
- this.assignment?.description ?? ""
2311
- );
2312
- if (toolName) {
2313
- tools = this.toolRegistry.getToolDefinitions();
2314
- this.sendStatusUpdate({
2315
- progressPct: 50,
2316
- currentAction: `Dynamic tool created: ${toolName}`,
2317
- status: "IN_PROGRESS"
2318
- });
2319
- this.emit("tool:created", { tierId: this.id, toolName });
2320
- continue;
2321
- }
2437
+ if (stalledArtifactIterations === 2) {
2438
+ throw new Error(`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`);
2322
2439
  }
2323
2440
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
2324
2441
  }
@@ -2478,6 +2595,9 @@ ${assignment.expectedOutput}`;
2478
2595
  const artifactPaths = this.extractArtifactPaths(assignment);
2479
2596
  if (!artifactPaths.length) return { ok: true, issues: [] };
2480
2597
  const issues = [];
2598
+ const { exec: exec3 } = await import('child_process');
2599
+ const { promisify: promisify3 } = await import('util');
2600
+ const execAsync2 = promisify3(exec3);
2481
2601
  for (const artifactPath of artifactPaths) {
2482
2602
  const absolutePath = path13.resolve(process.cwd(), artifactPath);
2483
2603
  try {
@@ -2494,9 +2614,27 @@ ${assignment.expectedOutput}`;
2494
2614
  const content = await fs2.readFile(absolutePath, "utf-8");
2495
2615
  if (!content.trim()) {
2496
2616
  issues.push(`Artifact content is empty: ${artifactPath}`);
2617
+ continue;
2497
2618
  }
2498
2619
  } else if (stat.size < 100) {
2499
2620
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
2621
+ continue;
2622
+ }
2623
+ const ext = path13.extname(absolutePath).toLowerCase();
2624
+ try {
2625
+ if (ext === ".ts" || ext === ".tsx") {
2626
+ await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
2627
+ } else if (ext === ".js" || ext === ".jsx") {
2628
+ await execAsync2(`node --check ${absolutePath}`, { timeout: 1e4 });
2629
+ } else if (ext === ".py") {
2630
+ await execAsync2(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
2631
+ }
2632
+ } catch (err) {
2633
+ const stderr = err?.stderr || String(err);
2634
+ const stdout = err?.stdout || "";
2635
+ issues.push(`Semantic error in ${artifactPath}:
2636
+ ${stderr}
2637
+ ${stdout}`);
2500
2638
  }
2501
2639
  } catch {
2502
2640
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -2893,7 +3031,8 @@ var T2Manager = class extends BaseTier {
2893
3031
  });
2894
3032
  this.emit("peer-sync-received", { fromId, content });
2895
3033
  }
2896
- async execute(assignment, taskId) {
3034
+ async execute(assignment, taskId, signal) {
3035
+ this.signal = signal;
2897
3036
  this.assignment = assignment;
2898
3037
  this.taskId = taskId;
2899
3038
  this.setLabel(assignment.sectionTitle);
@@ -2905,12 +3044,14 @@ var T2Manager = class extends BaseTier {
2905
3044
  });
2906
3045
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
2907
3046
  try {
3047
+ this.throwIfCancelled();
2908
3048
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
2909
3049
  this.sendStatusUpdate({
2910
3050
  progressPct: 20,
2911
3051
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
2912
3052
  status: "IN_PROGRESS"
2913
3053
  });
3054
+ this.throwIfCancelled();
2914
3055
  const t3Results = await this.executeSubtasks(subtasks, taskId);
2915
3056
  this.sendStatusUpdate({
2916
3057
  progressPct: 90,
@@ -3077,11 +3218,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3077
3218
  ).join(", ")}`,
3078
3219
  status: "IN_PROGRESS"
3079
3220
  });
3221
+ this.throwIfCancelled();
3080
3222
  const waveResults = await Promise.allSettled(
3081
3223
  runnableIds.map(async (id) => {
3082
3224
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3083
3225
  const worker = workerMap.get(id);
3084
- const result = await worker.execute(assignment, taskId);
3226
+ const result = await worker.execute(assignment, taskId, this.signal);
3085
3227
  resultMap.set(id, result);
3086
3228
  return result;
3087
3229
  })
@@ -3095,6 +3237,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3095
3237
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3096
3238
  const retried = await this.retryT3(assignment, taskId);
3097
3239
  resultMap.set(id, retried);
3240
+ } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
3241
+ const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
3242
+ if (this.toolCreator) {
3243
+ this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
3244
+ this.sendStatusUpdate({
3245
+ progressPct: 50,
3246
+ currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
3247
+ status: "IN_PROGRESS"
3248
+ });
3249
+ const toolName = await this.toolCreator.createTool(
3250
+ `Help complete: ${assignment.subtaskTitle}`,
3251
+ assignment.description
3252
+ );
3253
+ if (toolName) {
3254
+ this.log(`T2 verifying new tool: ${toolName}`);
3255
+ this.sendStatusUpdate({
3256
+ progressPct: 60,
3257
+ currentAction: `T2 Verifying new tool: ${toolName}`,
3258
+ status: "IN_PROGRESS"
3259
+ });
3260
+ try {
3261
+ const verifyResult = await this.router.generate("T2", {
3262
+ 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".` }],
3263
+ systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
3264
+ maxTokens: 50
3265
+ });
3266
+ if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
3267
+ this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
3268
+ const retried = await this.retryT3({
3269
+ ...assignment,
3270
+ description: `${assignment.description}
3271
+
3272
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
3273
+ }, taskId);
3274
+ resultMap.set(id, retried);
3275
+ } else {
3276
+ this.log(`T2 rejected the dynamic tool: ${toolName}`);
3277
+ resultMap.set(id, r.value);
3278
+ }
3279
+ } catch {
3280
+ const retried = await this.retryT3({
3281
+ ...assignment,
3282
+ description: `${assignment.description}
3283
+
3284
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
3285
+ }, taskId);
3286
+ resultMap.set(id, retried);
3287
+ }
3288
+ } else {
3289
+ resultMap.set(id, r.value);
3290
+ }
3291
+ } else {
3292
+ resultMap.set(id, r.value);
3293
+ }
3098
3294
  }
3099
3295
  for (const dependent of adj.get(id) ?? []) {
3100
3296
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -3158,7 +3354,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3158
3354
  }));
3159
3355
  return worker.execute(
3160
3356
  { ...assignment, description: `[RETRY] ${assignment.description}` },
3161
- taskId
3357
+ taskId,
3358
+ this.signal
3162
3359
  );
3163
3360
  }
3164
3361
  publishSectionOutput(result) {
@@ -3172,29 +3369,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3172
3369
  async aggregateResults(assignment, results) {
3173
3370
  const completed = results.filter((r) => r.status === "COMPLETED");
3174
3371
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
3175
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
3176
3372
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
3177
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
3178
-
3179
- ${outputs}
3180
- ${peerOutputs ? `
3373
+ const peerContext = peerOutputs ? `
3181
3374
 
3182
3375
  Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
3183
- ${peerOutputs}` : ""}`;
3184
- const messages = [{ role: "user", content: prompt }];
3185
- try {
3186
- const result = await this.router.generate("T2", {
3187
- messages,
3188
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
3376
+ ${peerOutputs}` : "";
3377
+ const MAX_CHUNK_LENGTH = 15e3;
3378
+ let currentSummary = "";
3379
+ let i = 0;
3380
+ while (i < completed.length) {
3381
+ let chunkText = "";
3382
+ let chunkEnd = i;
3383
+ while (chunkEnd < completed.length) {
3384
+ const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
3385
+
3386
+ `;
3387
+ if (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
3388
+ break;
3389
+ }
3390
+ chunkText += nextOutput;
3391
+ chunkEnd++;
3392
+ }
3393
+ i = chunkEnd;
3394
+ const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
3395
+ ${currentSummary ? `
3396
+ PREVIOUS SUMMARY SO FAR:
3397
+ ${currentSummary}
3398
+
3399
+ NEW OUTPUTS TO INTEGRATE:
3400
+ ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
3401
+ const messages = [{ role: "user", content: prompt }];
3402
+ try {
3403
+ const result = await this.router.generate("T2", {
3404
+ messages,
3405
+ systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
3189
3406
 
3190
3407
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3191
- maxTokens: 300
3192
- });
3193
- return result.content;
3194
- } catch (err) {
3195
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
3196
- return outputs;
3408
+ maxTokens: 500
3409
+ });
3410
+ currentSummary = result.content;
3411
+ } catch (err) {
3412
+ this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
3413
+ return currentSummary + "\n\n" + chunkText;
3414
+ }
3197
3415
  }
3416
+ return currentSummary;
3198
3417
  }
3199
3418
  determineStatus(results) {
3200
3419
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -3319,10 +3538,10 @@ Rules:
3319
3538
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
3320
3539
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
3321
3540
 
3322
- EXECUTION MODE GUIDANCE:
3323
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
3324
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
3325
- - Prefer parallel execution: it is significantly faster and reduces total wall-clock time.
3541
+ DEPENDENCY GUIDANCE:
3542
+ - Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
3543
+ - 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).
3544
+ - Prefer empty dependencies (parallel execution): it is significantly faster and reduces total wall-clock time.
3326
3545
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
3327
3546
 
3328
3547
  QUALITY RULES:
@@ -3361,7 +3580,8 @@ var T1Administrator = class extends BaseTier {
3361
3580
  setToolCreator(creator) {
3362
3581
  this.toolCreator = creator;
3363
3582
  }
3364
- async execute(userPrompt, images, systemContext) {
3583
+ async execute(userPrompt, images, systemContext, signal) {
3584
+ this.signal = signal;
3365
3585
  this.taskId = randomUUID();
3366
3586
  this.setLabel("Administrator");
3367
3587
  this.setStatus("ACTIVE");
@@ -3372,10 +3592,12 @@ var T1Administrator = class extends BaseTier {
3372
3592
  status: "IN_PROGRESS"
3373
3593
  });
3374
3594
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
3595
+ this.throwIfCancelled();
3375
3596
  let enrichedPrompt = userPrompt;
3376
3597
  if (images?.length) {
3377
3598
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
3378
3599
  }
3600
+ this.throwIfCancelled();
3379
3601
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
3380
3602
  this.sendStatusUpdate({
3381
3603
  progressPct: 10,
@@ -3383,21 +3605,83 @@ var T1Administrator = class extends BaseTier {
3383
3605
  status: "IN_PROGRESS"
3384
3606
  });
3385
3607
  this.emit("plan", { taskId: this.taskId, plan });
3386
- const t2Results = await this.dispatchT2Managers(plan.sections);
3608
+ this.throwIfCancelled();
3609
+ let allT2Results = await this.dispatchT2Managers(plan.sections);
3610
+ let pass = 1;
3611
+ const MAX_REPLAN_PASSES = 2;
3612
+ while (pass <= MAX_REPLAN_PASSES) {
3613
+ const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
3614
+ if (reviewResult.approved) {
3615
+ this.log("T1 Review passed.");
3616
+ break;
3617
+ }
3618
+ this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
3619
+ this.sendStatusUpdate({
3620
+ progressPct: 80 + pass * 5,
3621
+ currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
3622
+ status: "IN_PROGRESS"
3623
+ });
3624
+ const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
3625
+ Review reason: ${reviewResult.reason}
3626
+
3627
+ Original goal: ${enrichedPrompt}
3628
+
3629
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
3630
+ const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
3631
+ allT2Results = [...allT2Results, ...correctionResults];
3632
+ pass++;
3633
+ }
3387
3634
  this.sendStatusUpdate({
3388
3635
  progressPct: 95,
3389
3636
  currentAction: "Compiling final output",
3390
3637
  status: "IN_PROGRESS"
3391
3638
  });
3392
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
3639
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
3393
3640
  this.setStatus("COMPLETED");
3394
3641
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
3395
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
3642
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
3396
3643
  }
3397
3644
  getEscalations() {
3398
3645
  return [...this.escalations];
3399
3646
  }
3400
3647
  // ── Private ──────────────────────────────────
3648
+ async reviewT2Outputs(originalPrompt, plan, t2Results) {
3649
+ const failedSections = t2Results.filter((r) => r.status === "FAILED");
3650
+ if (failedSections.length > 0) {
3651
+ return {
3652
+ approved: false,
3653
+ reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
3654
+ };
3655
+ }
3656
+ const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
3657
+ ${r.sectionSummary}`).join("\n\n");
3658
+ const prompt = `You are a strict QA Reviewer for the Cascade AI system.
3659
+ Review the following execution outputs against the original user prompt.
3660
+
3661
+ Original Request: ${originalPrompt}
3662
+
3663
+ T2 Manager Summaries:
3664
+ ${sectionsText}
3665
+
3666
+ Does the current state of the workspace and the outputs fully satisfy the user's request?
3667
+ If yes, reply with exactly: "APPROVED".
3668
+ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
3669
+ try {
3670
+ const result = await this.router.generate("T1", {
3671
+ messages: [{ role: "user", content: prompt }],
3672
+ systemPrompt: this.systemPromptOverride + "You are a QA Reviewer.",
3673
+ maxTokens: 500,
3674
+ temperature: 0
3675
+ });
3676
+ const response = result.content.trim();
3677
+ if (response.toUpperCase().startsWith("APPROVED")) {
3678
+ return { approved: true };
3679
+ }
3680
+ return { approved: false, reason: response.replace(/^REJECTED:\s*/i, "") };
3681
+ } catch {
3682
+ return { approved: true };
3683
+ }
3684
+ }
3401
3685
  async analyzeImages(prompt, images) {
3402
3686
  const visionModel = this.router.getModelForTier("T1");
3403
3687
  if (!visionModel?.isVisionCapable) return prompt;
@@ -3426,29 +3710,35 @@ ${systemContext}` : "";
3426
3710
  Example: if asked to create files "inside python_exclusive", every subtask that
3427
3711
  creates a file must use "python_exclusive/filename.ext" as the path.
3428
3712
 
3429
- Return JSON where subtasks can declare dependencies:
3713
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
3430
3714
  {
3431
3715
  "sections": [{
3716
+ "sectionId": "s1",
3717
+ "sectionTitle": "Setup Project",
3718
+ "description": "Initialize the project",
3719
+ "expectedOutput": "Basic structure created",
3720
+ "constraints": [],
3721
+ "dependsOn": [], // \u2190 empty = runs immediately
3432
3722
  "t3Subtasks": [{
3433
3723
  "subtaskId": "t1",
3434
- "subtaskTitle": "Generate Source Code",
3435
- "dependsOn": [], // \u2190 empty = runs immediately
3436
- "executionMode": "parallel"
3437
- }, {
3438
- "subtaskId": "t2",
3439
- "subtaskTitle": "Save Code to File",
3440
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
3441
- "executionMode": "parallel"
3442
- }, {
3443
- "subtaskId": "t3",
3444
- "subtaskTitle": "Execute and Verify",
3445
- "dependsOn": ["t2"], // \u2190 waits for t2
3446
- "executionMode": "parallel"
3724
+ "subtaskTitle": "Init NPM",
3725
+ "description": "Run npm init",
3726
+ "expectedOutput": "package.json created",
3727
+ "constraints": [],
3728
+ "dependsOn": []
3447
3729
  }]
3730
+ }, {
3731
+ "sectionId": "s2",
3732
+ "sectionTitle": "Write Tests",
3733
+ "description": "Write tests for the project",
3734
+ "expectedOutput": "Tests passing",
3735
+ "constraints": [],
3736
+ "dependsOn": ["s1"], // \u2190 waits for section s1 to complete first
3737
+ "t3Subtasks": [...]
3448
3738
  }]
3449
3739
  }
3450
- Use dependsOn when a subtask needs the output of a previous one.
3451
- Leave dependsOn empty for subtasks that can run immediately in parallel.`;
3740
+ Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
3741
+ Leave dependsOn empty for sections that can run immediately in parallel.`;
3452
3742
  const messages = [{ role: "user", content: decompositionPrompt }];
3453
3743
  const result = await this.router.generate("T1", {
3454
3744
  messages,
@@ -3576,92 +3866,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
3576
3866
  ].filter(Boolean).join(" ");
3577
3867
  m.setHierarchyContext(context);
3578
3868
  });
3579
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
3580
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
3581
- for (const section of sections) {
3582
- if (overlapSections.has(section.sectionId)) {
3583
- section.executionMode = "sequential";
3869
+ if (overlapSections.size > 0) {
3870
+ this.log("Overlap detected \u2014 adding sequential dependencies for conflicting sections to prevent race conditions");
3871
+ const overlapArray = Array.from(overlapSections);
3872
+ for (let i = 1; i < overlapArray.length; i++) {
3873
+ const section = sections.find((s) => s.sectionId === overlapArray[i]);
3874
+ if (section) {
3875
+ section.dependsOn = [...section.dependsOn || [], overlapArray[i - 1]];
3584
3876
  }
3585
3877
  }
3586
3878
  }
3587
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
3588
- const isSequential = sections.some((s) => s.executionMode === "sequential");
3589
3879
  const t2Results = [];
3590
3880
  try {
3591
- if (isSequential) {
3592
- this.log("Dispatching T2 managers sequentially");
3593
- for (let i = 0; i < managers.length; i++) {
3594
- const m = managers[i];
3595
- this.sendStatusUpdate({
3596
- progressPct: pct(i),
3597
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
3598
- status: "IN_PROGRESS"
3599
- });
3600
- try {
3601
- const result = await m.execute(sections[i], this.taskId);
3602
- t2Results.push(result);
3603
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
3604
- if (result.status === "ESCALATED") {
3605
- this.escalations.push({
3606
- raisedBy: `T2_${sections[i].sectionId}`,
3607
- sectionId: sections[i].sectionId,
3608
- attempted: result.issues,
3609
- blocker: result.issues.join("; "),
3610
- needs: "Human review required"
3611
- });
3612
- }
3613
- } catch (err) {
3614
- t2Results.push({
3615
- sectionId: sections[i].sectionId,
3616
- sectionTitle: sections[i].sectionTitle,
3617
- status: "FAILED",
3618
- t3Results: [],
3619
- sectionSummary: "",
3620
- issues: [err instanceof Error ? err.message : String(err)]
3621
- });
3881
+ t2Results.push(...await this.runT2sWithDependencies(sections, managers, this.taskId));
3882
+ } finally {
3883
+ cleanup();
3884
+ }
3885
+ return t2Results;
3886
+ }
3887
+ /**
3888
+ * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
3889
+ */
3890
+ async runT2sWithDependencies(sections, managers, taskId) {
3891
+ const adj = /* @__PURE__ */ new Map();
3892
+ const inDegree = /* @__PURE__ */ new Map();
3893
+ const resultMap = /* @__PURE__ */ new Map();
3894
+ const allKeys = new Set(sections.map((s) => s.sectionId));
3895
+ for (const s of sections) {
3896
+ if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
3897
+ inDegree.set(s.sectionId, 0);
3898
+ s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
3899
+ }
3900
+ for (const s of sections) {
3901
+ for (const dep of s.dependsOn ?? []) {
3902
+ adj.get(dep).add(s.sectionId);
3903
+ inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
3904
+ }
3905
+ }
3906
+ const queue = [];
3907
+ const degree = new Map(inDegree);
3908
+ for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
3909
+ const visited = /* @__PURE__ */ new Set();
3910
+ while (queue.length > 0) {
3911
+ const u = queue.shift();
3912
+ visited.add(u);
3913
+ for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
3914
+ const newDeg = (degree.get(v) ?? 1) - 1;
3915
+ degree.set(v, newDeg);
3916
+ if (newDeg === 0) queue.push(v);
3917
+ }
3918
+ }
3919
+ const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
3920
+ if (cycleNodes.length > 0) {
3921
+ this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
3922
+ for (const s of sections) {
3923
+ if (cycleNodes.includes(s.sectionId)) {
3924
+ const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
3925
+ for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
3926
+ inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
3927
+ adj.get(removed)?.delete(s.sectionId);
3622
3928
  }
3929
+ s.dependsOn = safeDeps;
3623
3930
  }
3624
- } else {
3625
- const results = await Promise.allSettled(
3626
- managers.map((m, i) => {
3627
- this.sendStatusUpdate({
3628
- progressPct: pct(i),
3629
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
3630
- status: "IN_PROGRESS"
3631
- });
3632
- return m.execute(sections[i], this.taskId);
3633
- })
3634
- );
3635
- for (let i = 0; i < results.length; i++) {
3636
- const r = results[i];
3637
- if (r.status === "fulfilled") {
3638
- t2Results.push(r.value);
3639
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
3640
- if (r.value.status === "ESCALATED") {
3641
- this.escalations.push({
3642
- raisedBy: `T2_${sections[i].sectionId}`,
3643
- sectionId: sections[i].sectionId,
3644
- attempted: r.value.issues,
3645
- blocker: r.value.issues.join("; "),
3646
- needs: "Human review required"
3647
- });
3648
- }
3649
- } else {
3650
- t2Results.push({
3651
- sectionId: sections[i].sectionId,
3652
- sectionTitle: sections[i].sectionTitle,
3653
- status: "FAILED",
3654
- t3Results: [],
3655
- sectionSummary: "",
3656
- issues: [r.reason instanceof Error ? r.reason.message : String(r.reason)]
3931
+ }
3932
+ }
3933
+ const totalSections = sections.length;
3934
+ let completedSections = 0;
3935
+ const executeWave = async () => {
3936
+ const readyIds = [];
3937
+ for (const [id, deg] of inDegree.entries()) {
3938
+ if (deg === 0 && !resultMap.has(id)) {
3939
+ readyIds.push(id);
3940
+ }
3941
+ }
3942
+ if (readyIds.length === 0) return;
3943
+ await Promise.all(readyIds.map(async (id) => {
3944
+ resultMap.set(id, null);
3945
+ const index = sections.findIndex((s) => s.sectionId === id);
3946
+ const section = sections[index];
3947
+ const manager = managers[index];
3948
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
3949
+ this.sendStatusUpdate({
3950
+ progressPct,
3951
+ currentAction: `T2 working on: ${section.sectionTitle}`,
3952
+ status: "IN_PROGRESS"
3953
+ });
3954
+ this.throwIfCancelled();
3955
+ let result;
3956
+ try {
3957
+ result = await manager.execute(section, taskId, this.signal);
3958
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
3959
+ if (result.status === "ESCALATED") {
3960
+ this.escalations.push({
3961
+ raisedBy: `T2_${section.sectionId}`,
3962
+ sectionId: section.sectionId,
3963
+ attempted: result.issues,
3964
+ blocker: result.issues.join("; "),
3965
+ needs: "Human review required"
3657
3966
  });
3658
3967
  }
3968
+ } catch (err) {
3969
+ result = {
3970
+ sectionId: section.sectionId,
3971
+ sectionTitle: section.sectionTitle,
3972
+ status: "FAILED",
3973
+ t3Results: [],
3974
+ sectionSummary: "",
3975
+ issues: [err instanceof Error ? err.message : String(err)]
3976
+ };
3977
+ }
3978
+ resultMap.set(id, result);
3979
+ completedSections++;
3980
+ for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
3981
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
3659
3982
  }
3983
+ }));
3984
+ if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
3985
+ await executeWave();
3660
3986
  }
3661
- } finally {
3662
- cleanup();
3663
- }
3664
- return t2Results;
3987
+ };
3988
+ await executeWave();
3989
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
3665
3990
  }
3666
3991
  async compileFinalOutput(originalPrompt, plan, t2Results) {
3667
3992
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -4108,13 +4433,47 @@ var GitHubTool = class extends BaseTool {
4108
4433
  }
4109
4434
  async execute(input, _options) {
4110
4435
  const platform = input["platform"] ?? "github";
4111
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
4112
4436
  const operation = input["operation"];
4113
4437
  const repo = input["repo"];
4114
- if (platform === "github") {
4115
- return this.executeGitHub(operation, repo, token, input);
4438
+ let token = input["token"];
4439
+ if (!token) {
4440
+ if (platform === "github") {
4441
+ token = process.env["GITHUB_TOKEN"];
4442
+ } else {
4443
+ token = process.env["GITLAB_TOKEN"];
4444
+ }
4445
+ }
4446
+ if (!token) {
4447
+ const envName = platform === "github" ? "GITHUB_TOKEN" : "GITLAB_TOKEN";
4448
+ return `Error: No ${platform} token provided. Set the ${envName} environment variable or pass a "token" field in the input.`;
4449
+ }
4450
+ try {
4451
+ if (platform === "github") {
4452
+ return await this.executeGitHub(operation, repo, token, input);
4453
+ }
4454
+ return await this.executeGitLab(operation, repo, token, input);
4455
+ } catch (err) {
4456
+ const axiosErr = err;
4457
+ if (axiosErr?.response?.status) {
4458
+ const status = axiosErr.response.status;
4459
+ const msg = axiosErr.response.data?.message ?? "";
4460
+ switch (status) {
4461
+ case 401:
4462
+ return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
4463
+ case 403:
4464
+ return `Permission denied: Your ${platform} token lacks the required scopes for this operation. Needed: repo or workflow.`;
4465
+ case 404:
4466
+ return `Not found: Repository "${repo}" does not exist, or your token cannot access it.`;
4467
+ case 422:
4468
+ return `Validation error from ${platform}: ${msg || "Check your input parameters (branch names, base/head refs, etc.)."}`;
4469
+ case 429:
4470
+ return `Rate limited by ${platform}. Please wait a moment before trying again.`;
4471
+ default:
4472
+ return `${platform} API error (${status}): ${msg || (axiosErr.message ?? "Unknown error")}`;
4473
+ }
4474
+ }
4475
+ return `${platform} request failed: ${axiosErr.message ?? String(err)}`;
4116
4476
  }
4117
- return this.executeGitLab(operation, repo, token, input);
4118
4477
  }
4119
4478
  async executeGitHub(operation, repo, token, input) {
4120
4479
  const headers = {
@@ -4201,6 +4560,7 @@ ${response.data.description}`;
4201
4560
  };
4202
4561
 
4203
4562
  // src/tools/browser.ts
4563
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
4204
4564
  var BrowserTool = class extends BaseTool {
4205
4565
  name = "browser";
4206
4566
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -4209,7 +4569,7 @@ var BrowserTool = class extends BaseTool {
4209
4569
  properties: {
4210
4570
  action: {
4211
4571
  type: "string",
4212
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
4572
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
4213
4573
  },
4214
4574
  url: { type: "string", description: "URL to navigate to" },
4215
4575
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -4229,53 +4589,86 @@ var BrowserTool = class extends BaseTool {
4229
4589
  try {
4230
4590
  playwright = await import('playwright');
4231
4591
  } catch {
4232
- throw new Error("Playwright is not installed. Run: npm install playwright && npx playwright install chromium");
4233
- }
4234
- if (!this.browser) {
4235
- const pw = playwright;
4236
- this.browser = await pw.chromium.launch({ headless: true });
4237
- const b = this.browser;
4238
- this.page = await b.newPage();
4592
+ return "Error: Playwright is not installed. Run: npm install playwright && npx playwright install chromium";
4239
4593
  }
4240
- const page = this.page;
4241
4594
  const action = input["action"];
4242
4595
  const timeout = input["timeout"] ?? 1e4;
4243
- switch (action) {
4244
- case "navigate": {
4245
- await page.goto(input["url"], { timeout });
4246
- return `Navigated to ${input["url"]}`;
4247
- }
4248
- case "click": {
4249
- await page.click(input["selector"], { timeout });
4250
- return `Clicked ${input["selector"]}`;
4251
- }
4252
- case "fill": {
4253
- await page.fill(input["selector"], input["value"]);
4254
- return `Filled ${input["selector"]} with value`;
4255
- }
4256
- case "screenshot": {
4257
- const buf = await page.screenshot({ type: "png" });
4258
- return `data:image/png;base64,${buf.toString("base64")}`;
4259
- }
4260
- case "evaluate": {
4261
- const result = await page.evaluate(input["script"]);
4262
- return JSON.stringify(result);
4596
+ if (action === "close") {
4597
+ await this.close();
4598
+ return "Browser closed.";
4599
+ }
4600
+ if (!this.browser || !this.page) {
4601
+ await this.close();
4602
+ const launchPromise = playwright.chromium.launch({ headless: true });
4603
+ const timeoutPromise = new Promise(
4604
+ (_, 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)
4605
+ );
4606
+ try {
4607
+ this.browser = await Promise.race([launchPromise, timeoutPromise]);
4608
+ this.page = await this.browser.newPage();
4609
+ } catch (err) {
4610
+ this.browser = null;
4611
+ this.page = null;
4612
+ return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
4263
4613
  }
4264
- case "extract_text": {
4265
- const text = await page.locator("body").innerText();
4266
- return text.slice(0, 1e4);
4614
+ }
4615
+ const page = this.page;
4616
+ try {
4617
+ switch (action) {
4618
+ case "navigate": {
4619
+ await page.goto(input["url"], { timeout });
4620
+ const title = await page.title();
4621
+ return `Navigated to ${input["url"]} (title: "${title}")`;
4622
+ }
4623
+ case "click": {
4624
+ await page.click(input["selector"], { timeout });
4625
+ return `Clicked ${input["selector"]}`;
4626
+ }
4627
+ case "fill": {
4628
+ await page.fill(input["selector"], input["value"]);
4629
+ return `Filled ${input["selector"]} with value`;
4630
+ }
4631
+ case "screenshot": {
4632
+ const buf = await page.screenshot({ type: "png" });
4633
+ return `data:image/png;base64,${buf.toString("base64")}`;
4634
+ }
4635
+ case "evaluate": {
4636
+ const result = await page.evaluate(input["script"]);
4637
+ return JSON.stringify(result);
4638
+ }
4639
+ case "extract_text": {
4640
+ const text = await page.locator("body").innerText();
4641
+ return text.slice(0, 1e4);
4642
+ }
4643
+ case "wait": {
4644
+ await page.waitForTimeout(timeout);
4645
+ return `Waited ${timeout}ms`;
4646
+ }
4647
+ default:
4648
+ return `Unknown browser action: ${action}. Supported: navigate, click, fill, screenshot, evaluate, extract_text, wait, close`;
4267
4649
  }
4268
- case "wait": {
4269
- await page.waitForTimeout(timeout);
4270
- return `Waited ${timeout}ms`;
4650
+ } catch (err) {
4651
+ const errMsg = err instanceof Error ? err.message : String(err);
4652
+ if (/Target closed|Page crashed|Navigation failed/i.test(errMsg)) {
4653
+ await this.close();
4654
+ return `Browser error (page reset): ${errMsg}`;
4271
4655
  }
4272
- default:
4273
- throw new Error(`Unknown browser action: ${action}`);
4656
+ return `Browser action "${action}" failed: ${errMsg}`;
4274
4657
  }
4275
4658
  }
4276
4659
  async close() {
4277
- if (this.browser) {
4278
- await this.browser.close();
4660
+ try {
4661
+ if (this.page) {
4662
+ await this.page.close().catch(() => {
4663
+ });
4664
+ this.page = null;
4665
+ }
4666
+ if (this.browser) {
4667
+ await this.browser.close().catch(() => {
4668
+ });
4669
+ this.browser = null;
4670
+ }
4671
+ } catch {
4279
4672
  this.browser = null;
4280
4673
  this.page = null;
4281
4674
  }
@@ -4372,6 +4765,19 @@ var PDFCreateTool = class extends BaseTool {
4372
4765
  });
4373
4766
  }
4374
4767
  };
4768
+ function detectCommand(candidates) {
4769
+ for (const cmd of candidates) {
4770
+ try {
4771
+ const which = process.platform === "win32" ? "where" : "which";
4772
+ execSync(`${which} ${cmd}`, { stdio: "ignore" });
4773
+ return cmd;
4774
+ } catch {
4775
+ }
4776
+ }
4777
+ return null;
4778
+ }
4779
+ var PYTHON_CMD = detectCommand(["python3", "python"]);
4780
+ var NODE_CMD = detectCommand(["node"]);
4375
4781
  var CodeInterpreterTool = class extends BaseTool {
4376
4782
  name = "run_code";
4377
4783
  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.";
@@ -4387,10 +4793,30 @@ var CodeInterpreterTool = class extends BaseTool {
4387
4793
  isDangerous() {
4388
4794
  return true;
4389
4795
  }
4390
- async execute(input, options) {
4796
+ async execute(input, _options) {
4391
4797
  const language = input["language"];
4392
4798
  const code = input["code"];
4393
4799
  const args = input["args"] ?? [];
4800
+ let cmdPrefix;
4801
+ if (language === "python") {
4802
+ if (!PYTHON_CMD) {
4803
+ return [
4804
+ "Error: Python interpreter not found.",
4805
+ "Please install Python and ensure it is in your PATH.",
4806
+ "Tried: python3, python"
4807
+ ].join("\n");
4808
+ }
4809
+ cmdPrefix = PYTHON_CMD;
4810
+ } else {
4811
+ if (!NODE_CMD) {
4812
+ return [
4813
+ "Error: Node.js interpreter not found.",
4814
+ "Please install Node.js and ensure it is in your PATH.",
4815
+ "Tried: node"
4816
+ ].join("\n");
4817
+ }
4818
+ cmdPrefix = NODE_CMD;
4819
+ }
4394
4820
  const tmpDir = path13.join(process.cwd(), ".cascade", "tmp");
4395
4821
  if (!fs11.existsSync(tmpDir)) {
4396
4822
  fs11.mkdirSync(tmpDir, { recursive: true });
@@ -4399,8 +4825,9 @@ var CodeInterpreterTool = class extends BaseTool {
4399
4825
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
4400
4826
  const filePath = path13.join(tmpDir, fileName);
4401
4827
  fs11.writeFileSync(filePath, code, "utf-8");
4402
- const cmdPrefix = language === "python" ? "python3" : "node";
4403
- const fullCmd = `${cmdPrefix} "${filePath}" ${args.map((a) => `"${a}"`).join(" ")}`;
4828
+ const quotedPath = `"${filePath}"`;
4829
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
4830
+ const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
4404
4831
  return new Promise((resolve) => {
4405
4832
  const startMs = Date.now();
4406
4833
  exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -4413,10 +4840,17 @@ var CodeInterpreterTool = class extends BaseTool {
4413
4840
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
4414
4841
  }
4415
4842
  if (error) {
4416
- resolve(`Execution failed (${duration}ms):
4843
+ const timedOut = error.killed && duration >= 3e4;
4844
+ if (timedOut) {
4845
+ resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
4846
+ Partial stdout: ${stdout}
4847
+ Stderr: ${stderr}`);
4848
+ } else {
4849
+ resolve(`Execution failed (${duration}ms):
4417
4850
  Error: ${error.message}
4418
4851
  Stderr: ${stderr}
4419
4852
  Stdout: ${stdout}`);
4853
+ }
4420
4854
  } else {
4421
4855
  resolve(`Execution successful (${duration}ms):
4422
4856
  Stdout: ${stdout}
@@ -4481,6 +4915,186 @@ ${formatted}`;
4481
4915
  }
4482
4916
  };
4483
4917
 
4918
+ // src/tools/web-search.ts
4919
+ async function searchSearXNG(query, baseUrl, maxResults) {
4920
+ const url = new URL("/search", baseUrl);
4921
+ url.searchParams.set("q", query);
4922
+ url.searchParams.set("format", "json");
4923
+ url.searchParams.set("categories", "general");
4924
+ url.searchParams.set("engines", "google,bing,duckduckgo");
4925
+ const resp = await fetch(url.toString(), {
4926
+ headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
4927
+ signal: AbortSignal.timeout(1e4)
4928
+ });
4929
+ if (!resp.ok) {
4930
+ throw new Error(`SearXNG returned HTTP ${resp.status}`);
4931
+ }
4932
+ const data = await resp.json();
4933
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
4934
+ title: r.title ?? "",
4935
+ url: r.url ?? "",
4936
+ snippet: r.content ?? "",
4937
+ engine: `searxng(${r.engine ?? "unknown"})`
4938
+ }));
4939
+ }
4940
+ async function searchBrave(query, apiKey, maxResults) {
4941
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}&safesearch=off`;
4942
+ const resp = await fetch(url, {
4943
+ headers: {
4944
+ "Accept": "application/json",
4945
+ "Accept-Encoding": "gzip",
4946
+ "X-Subscription-Token": apiKey
4947
+ },
4948
+ signal: AbortSignal.timeout(1e4)
4949
+ });
4950
+ if (!resp.ok) {
4951
+ throw new Error(`Brave Search returned HTTP ${resp.status}`);
4952
+ }
4953
+ const data = await resp.json();
4954
+ return (data.web?.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
4955
+ title: r.title ?? "",
4956
+ url: r.url ?? "",
4957
+ snippet: r.description ?? "",
4958
+ engine: "brave"
4959
+ }));
4960
+ }
4961
+ async function searchTavily(query, apiKey, maxResults) {
4962
+ const resp = await fetch("https://api.tavily.com/search", {
4963
+ method: "POST",
4964
+ headers: {
4965
+ "Content-Type": "application/json",
4966
+ "Authorization": `Bearer ${apiKey}`
4967
+ },
4968
+ body: JSON.stringify({
4969
+ query,
4970
+ max_results: maxResults,
4971
+ search_depth: "basic",
4972
+ include_answer: false,
4973
+ include_raw_content: false
4974
+ }),
4975
+ signal: AbortSignal.timeout(15e3)
4976
+ });
4977
+ if (!resp.ok) {
4978
+ throw new Error(`Tavily returned HTTP ${resp.status}`);
4979
+ }
4980
+ const data = await resp.json();
4981
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
4982
+ title: r.title ?? "",
4983
+ url: r.url ?? "",
4984
+ snippet: r.content ?? "",
4985
+ engine: "tavily"
4986
+ }));
4987
+ }
4988
+ async function searchDuckDuckGoLite(query, maxResults) {
4989
+ const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
4990
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
4991
+ signal: AbortSignal.timeout(1e4)
4992
+ });
4993
+ if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
4994
+ const html = await resp.text();
4995
+ const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
4996
+ const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
4997
+ const links = [];
4998
+ const snippets = [];
4999
+ let m;
5000
+ while ((m = linkPattern.exec(html)) !== null) {
5001
+ links.push({ url: m[1], title: m[2].trim() });
5002
+ }
5003
+ while ((m = snippetPattern.exec(html)) !== null) {
5004
+ snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
5005
+ }
5006
+ return links.slice(0, maxResults).map((link, i) => ({
5007
+ title: link.title,
5008
+ url: link.url,
5009
+ snippet: snippets[i] ?? "",
5010
+ engine: "duckduckgo-lite"
5011
+ }));
5012
+ }
5013
+ var WebSearchTool = class extends BaseTool {
5014
+ name = "web_search";
5015
+ description = "Search the web for current information, news, documentation, or any topic. Returns a list of relevant results with titles, URLs, and snippets.";
5016
+ inputSchema = {
5017
+ type: "object",
5018
+ properties: {
5019
+ query: { type: "string", description: "The search query" },
5020
+ maxResults: { type: "number", description: "Number of results to return (default: 5, max: 10)" }
5021
+ },
5022
+ required: ["query"]
5023
+ };
5024
+ config;
5025
+ constructor(config = {}) {
5026
+ super();
5027
+ this.config = {
5028
+ searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
5029
+ braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
5030
+ tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
5031
+ maxResults: config.maxResults ?? 5
5032
+ };
5033
+ }
5034
+ async execute(input, _options) {
5035
+ const query = input["query"];
5036
+ if (!query?.trim()) return "Error: query is required and must be non-empty.";
5037
+ const maxResults = Math.min(
5038
+ input["maxResults"] ?? this.config.maxResults ?? 5,
5039
+ 10
5040
+ );
5041
+ const errors = [];
5042
+ let results = [];
5043
+ if (this.config.searxngUrl) {
5044
+ try {
5045
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
5046
+ if (results.length > 0) return this.formatResults(query, results);
5047
+ errors.push("SearXNG: returned 0 results");
5048
+ } catch (err) {
5049
+ errors.push(`SearXNG: ${err instanceof Error ? err.message : String(err)}`);
5050
+ }
5051
+ }
5052
+ if (this.config.braveApiKey) {
5053
+ try {
5054
+ results = await searchBrave(query, this.config.braveApiKey, maxResults);
5055
+ if (results.length > 0) return this.formatResults(query, results);
5056
+ errors.push("Brave: returned 0 results");
5057
+ } catch (err) {
5058
+ errors.push(`Brave: ${err instanceof Error ? err.message : String(err)}`);
5059
+ }
5060
+ }
5061
+ if (this.config.tavilyApiKey) {
5062
+ try {
5063
+ results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
5064
+ if (results.length > 0) return this.formatResults(query, results);
5065
+ errors.push("Tavily: returned 0 results");
5066
+ } catch (err) {
5067
+ errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
5068
+ }
5069
+ }
5070
+ try {
5071
+ results = await searchDuckDuckGoLite(query, maxResults);
5072
+ if (results.length > 0) return this.formatResults(query, results);
5073
+ errors.push("DuckDuckGo Lite: returned 0 results");
5074
+ } catch (err) {
5075
+ errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
5076
+ }
5077
+ 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" : "";
5078
+ return [
5079
+ `Web search for "${query}" failed across all backends:`,
5080
+ ...errors.map((e) => ` \u2022 ${e}`),
5081
+ configHint
5082
+ ].join("\n");
5083
+ }
5084
+ formatResults(query, results) {
5085
+ const lines = [`Web search results for: "${query}"`, ""];
5086
+ for (let i = 0; i < results.length; i++) {
5087
+ const r = results[i];
5088
+ lines.push(`[${i + 1}] ${r.title}`);
5089
+ lines.push(` URL: ${r.url}`);
5090
+ if (r.snippet) lines.push(` ${r.snippet.slice(0, 300)}`);
5091
+ if (r.engine) lines.push(` Source: ${r.engine}`);
5092
+ lines.push("");
5093
+ }
5094
+ return lines.join("\n");
5095
+ }
5096
+ };
5097
+
4484
5098
  // src/tools/mcp.ts
4485
5099
  var McpToolWrapper = class extends BaseTool {
4486
5100
  name;
@@ -4602,7 +5216,8 @@ var ToolRegistry = class {
4602
5216
  new ImageAnalyzeTool(),
4603
5217
  new PDFCreateTool(),
4604
5218
  new CodeInterpreterTool(),
4605
- new PeerCommunicationTool()
5219
+ new PeerCommunicationTool(),
5220
+ new WebSearchTool(this.config.webSearch)
4606
5221
  ];
4607
5222
  for (const tool of tools) {
4608
5223
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -4626,8 +5241,23 @@ var ToolRegistry = class {
4626
5241
  return this.ignoreMatcher.ignores(posixRel);
4627
5242
  }
4628
5243
  };
4629
- var McpClient = class {
5244
+ var McpClient = class _McpClient {
5245
+ static activeProcessPids = /* @__PURE__ */ new Set();
5246
+ /**
5247
+ * Forcefully kills all known MCP child processes.
5248
+ * Call this from global process exit handlers to prevent zombie processes.
5249
+ */
5250
+ static killAllProcesses() {
5251
+ for (const pid of _McpClient.activeProcessPids) {
5252
+ try {
5253
+ process.kill(pid, "SIGKILL");
5254
+ } catch {
5255
+ }
5256
+ }
5257
+ _McpClient.activeProcessPids.clear();
5258
+ }
4630
5259
  clients = /* @__PURE__ */ new Map();
5260
+ transports = /* @__PURE__ */ new Map();
4631
5261
  tools = /* @__PURE__ */ new Map();
4632
5262
  trustedServers;
4633
5263
  approvalCallback;
@@ -4656,6 +5286,8 @@ var McpClient = class {
4656
5286
  );
4657
5287
  await client.connect(transport);
4658
5288
  this.clients.set(server.name, client);
5289
+ this.transports.set(server.name, transport);
5290
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
4659
5291
  const toolsResult = await client.listTools();
4660
5292
  for (const tool of toolsResult.tools) {
4661
5293
  for (const existing of this.tools.values()) {
@@ -4677,8 +5309,11 @@ var McpClient = class {
4677
5309
  async disconnect(serverName) {
4678
5310
  const client = this.clients.get(serverName);
4679
5311
  if (client) {
5312
+ const transport = this.transports.get(serverName);
5313
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
4680
5314
  await client.close();
4681
5315
  this.clients.delete(serverName);
5316
+ this.transports.delete(serverName);
4682
5317
  for (const key of this.tools.keys()) {
4683
5318
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
4684
5319
  }
@@ -4706,6 +5341,13 @@ var McpClient = class {
4706
5341
  getConnectedServers() {
4707
5342
  return Array.from(this.clients.keys());
4708
5343
  }
5344
+ getActivePids() {
5345
+ const pids = [];
5346
+ for (const transport of this.transports.values()) {
5347
+ if (transport.pid) pids.push(transport.pid);
5348
+ }
5349
+ return pids;
5350
+ }
4709
5351
  isConnected(serverName) {
4710
5352
  return this.clients.has(serverName);
4711
5353
  }
@@ -4844,12 +5486,24 @@ var McpServerConfigSchema = z.object({
4844
5486
  args: z.array(z.string()).optional(),
4845
5487
  env: z.record(z.string()).optional()
4846
5488
  });
5489
+ var WebSearchConfigSchema = z.object({
5490
+ /** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
5491
+ searxngUrl: z.string().optional(),
5492
+ /** Brave Search API key — get one at https://api.search.brave.com */
5493
+ braveApiKey: z.string().optional(),
5494
+ /** Tavily API key — get one at https://tavily.com */
5495
+ tavilyApiKey: z.string().optional(),
5496
+ /** Max results per search (default 5) */
5497
+ maxResults: z.number().default(5)
5498
+ });
4847
5499
  var ToolsConfigSchema = z.object({
4848
5500
  shellAllowlist: z.array(z.string()).default([]),
4849
5501
  shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
4850
5502
  requireApprovalFor: z.array(z.string()).default([]),
4851
5503
  browserEnabled: z.boolean().default(false),
4852
- mcpServers: z.array(McpServerConfigSchema).optional()
5504
+ mcpServers: z.array(McpServerConfigSchema).optional(),
5505
+ /** Web search backends — at least one should be configured for best results */
5506
+ webSearch: WebSearchConfigSchema.optional()
4853
5507
  });
4854
5508
  var HookDefinitionSchema = z.object({
4855
5509
  command: z.string(),
@@ -5391,12 +6045,25 @@ var Cascade = class extends EventEmitter {
5391
6045
  looksLikeSimpleArtifactTask(prompt) {
5392
6046
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
5393
6047
  }
5394
- async determineComplexity(prompt, conversationHistory = []) {
6048
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
5395
6049
  if (this.looksLikeSimpleArtifactTask(prompt)) {
5396
6050
  return "Simple";
5397
6051
  }
6052
+ let workspaceContext = "";
6053
+ try {
6054
+ const files = await glob("**/*.*", {
6055
+ cwd: workspacePath,
6056
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
6057
+ nodir: true
6058
+ });
6059
+ workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
6060
+ } catch {
6061
+ workspaceContext = "Workspace Scout: Could not scan workspace.";
6062
+ }
5398
6063
  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.
5399
6064
 
6065
+ ${workspaceContext}
6066
+
5400
6067
  Classification:
5401
6068
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
5402
6069
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -5471,7 +6138,7 @@ ${prompt}` : prompt;
5471
6138
  }
5472
6139
  escalator.resolveUserDecision(req.id, approved, always);
5473
6140
  });
5474
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
6141
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
5475
6142
  this.telemetry.capture("cascade:session_start", {
5476
6143
  complexity,
5477
6144
  providerCount: this.config.providers.length,
@@ -5551,7 +6218,7 @@ ${prompt}` : prompt;
5551
6218
  peerT3Ids: [],
5552
6219
  parentT2: "root"
5553
6220
  };
5554
- const t3Result = await t3.execute(assignment, taskId);
6221
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
5555
6222
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
5556
6223
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
5557
6224
  } else if (complexity === "Moderate") {
@@ -5574,7 +6241,7 @@ ${prompt}` : prompt;
5574
6241
  constraints: [],
5575
6242
  t3Subtasks: []
5576
6243
  };
5577
- const t2Result = await t2.execute(assignment, taskId);
6244
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
5578
6245
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
5579
6246
  t2Results = [t2Result];
5580
6247
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -5596,13 +6263,22 @@ ${prompt}` : prompt;
5596
6263
  if (toolCreator) t1.setToolCreator(toolCreator);
5597
6264
  bindTierEvents(t1);
5598
6265
  t1.on("plan", (e) => this.emit("plan", e));
5599
- const result = await t1.execute(options.prompt, options.images);
6266
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
5600
6267
  finalOutput = result.output;
5601
6268
  t2Results = result.t2Results;
5602
6269
  }
5603
6270
  } catch (err) {
5604
- runError = err;
5605
- throw err;
6271
+ if (err instanceof CascadeCancelledError) {
6272
+ this.emit("run:cancelled", {
6273
+ taskId,
6274
+ reason: err.message,
6275
+ partialOutput: finalOutput || ""
6276
+ });
6277
+ runError = null;
6278
+ } else {
6279
+ runError = err;
6280
+ throw err;
6281
+ }
5606
6282
  } finally {
5607
6283
  try {
5608
6284
  escalator.cancelAllPending();
@@ -5924,9 +6600,10 @@ var MemoryStore = class _MemoryStore {
5924
6600
  constructor(dbPath) {
5925
6601
  fs11.mkdirSync(path13.dirname(dbPath), { recursive: true });
5926
6602
  try {
5927
- this.db = new Database(dbPath);
6603
+ this.db = new Database(dbPath, { timeout: 5e3 });
5928
6604
  this.db.pragma("journal_mode = WAL");
5929
6605
  this.db.pragma("foreign_keys = ON");
6606
+ this.db.pragma("synchronous = NORMAL");
5930
6607
  this.migrate();
5931
6608
  } catch (err) {
5932
6609
  if (err instanceof Error && err.message.includes("Could not locate the bindings file")) {
@@ -5940,6 +6617,38 @@ Original error: ${err.message}`
5940
6617
  throw err;
5941
6618
  }
5942
6619
  }
6620
+ // ── Async Write Queue ─────────────────────────
6621
+ writeQueue = [];
6622
+ isProcessingQueue = false;
6623
+ async processQueue() {
6624
+ if (this.isProcessingQueue) return;
6625
+ this.isProcessingQueue = true;
6626
+ while (this.writeQueue.length > 0) {
6627
+ const op = this.writeQueue.shift();
6628
+ if (op) {
6629
+ let attempts = 0;
6630
+ while (attempts < 5) {
6631
+ try {
6632
+ op();
6633
+ break;
6634
+ } catch (err) {
6635
+ if (err instanceof Error && err.code === "SQLITE_BUSY") {
6636
+ attempts++;
6637
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempts)));
6638
+ } else {
6639
+ console.error("Cascade AI: DB Write Error:", err);
6640
+ break;
6641
+ }
6642
+ }
6643
+ }
6644
+ }
6645
+ }
6646
+ this.isProcessingQueue = false;
6647
+ }
6648
+ enqueueWrite(op) {
6649
+ this.writeQueue.push(op);
6650
+ this.processQueue().catch(console.error);
6651
+ }
5943
6652
  // ── Sessions ──────────────────────────────────
5944
6653
  createSession(session) {
5945
6654
  this.db.prepare(`
@@ -6026,26 +6735,28 @@ Original error: ${err.message}`
6026
6735
  }
6027
6736
  // ── Runtime Sessions / Nodes ─────────────────
6028
6737
  upsertRuntimeSession(session) {
6029
- this.db.prepare(`
6030
- INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
6031
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6032
- ON CONFLICT(session_id) DO UPDATE SET
6033
- title = excluded.title,
6034
- workspace_path = excluded.workspace_path,
6035
- status = excluded.status,
6036
- updated_at = excluded.updated_at,
6037
- latest_prompt = excluded.latest_prompt,
6038
- is_global = excluded.is_global
6039
- `).run(
6040
- session.sessionId,
6041
- session.title,
6042
- session.workspacePath,
6043
- session.status,
6044
- session.startedAt,
6045
- session.updatedAt,
6046
- session.latestPrompt ?? null,
6047
- session.isGlobal ? 1 : 0
6048
- );
6738
+ this.enqueueWrite(() => {
6739
+ this.db.prepare(`
6740
+ INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
6741
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6742
+ ON CONFLICT(session_id) DO UPDATE SET
6743
+ title = excluded.title,
6744
+ workspace_path = excluded.workspace_path,
6745
+ status = excluded.status,
6746
+ updated_at = excluded.updated_at,
6747
+ latest_prompt = excluded.latest_prompt,
6748
+ is_global = excluded.is_global
6749
+ `).run(
6750
+ session.sessionId,
6751
+ session.title,
6752
+ session.workspacePath,
6753
+ session.status,
6754
+ session.startedAt,
6755
+ session.updatedAt,
6756
+ session.latestPrompt ?? null,
6757
+ session.isGlobal ? 1 : 0
6758
+ );
6759
+ });
6049
6760
  }
6050
6761
  listRuntimeSessions(limit = 100) {
6051
6762
  const rows = this.db.prepare(`
@@ -6063,33 +6774,35 @@ Original error: ${err.message}`
6063
6774
  }));
6064
6775
  }
6065
6776
  upsertRuntimeNode(node) {
6066
- this.db.prepare(`
6067
- INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
6068
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6069
- ON CONFLICT(tier_id) DO UPDATE SET
6070
- session_id = excluded.session_id,
6071
- parent_id = excluded.parent_id,
6072
- role = excluded.role,
6073
- label = excluded.label,
6074
- status = excluded.status,
6075
- current_action = excluded.current_action,
6076
- progress_pct = excluded.progress_pct,
6077
- updated_at = excluded.updated_at,
6078
- workspace_path = excluded.workspace_path,
6079
- is_global = excluded.is_global
6080
- `).run(
6081
- node.tierId,
6082
- node.sessionId,
6083
- node.parentId ?? null,
6084
- node.role,
6085
- node.label,
6086
- node.status,
6087
- node.currentAction ?? null,
6088
- node.progressPct ?? null,
6089
- node.updatedAt,
6090
- node.workspacePath ?? null,
6091
- node.isGlobal ? 1 : 0
6092
- );
6777
+ this.enqueueWrite(() => {
6778
+ this.db.prepare(`
6779
+ INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
6780
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6781
+ ON CONFLICT(tier_id) DO UPDATE SET
6782
+ session_id = excluded.session_id,
6783
+ parent_id = excluded.parent_id,
6784
+ role = excluded.role,
6785
+ label = excluded.label,
6786
+ status = excluded.status,
6787
+ current_action = excluded.current_action,
6788
+ progress_pct = excluded.progress_pct,
6789
+ updated_at = excluded.updated_at,
6790
+ workspace_path = excluded.workspace_path,
6791
+ is_global = excluded.is_global
6792
+ `).run(
6793
+ node.tierId,
6794
+ node.sessionId,
6795
+ node.parentId ?? null,
6796
+ node.role,
6797
+ node.label,
6798
+ node.status,
6799
+ node.currentAction ?? null,
6800
+ node.progressPct ?? null,
6801
+ node.updatedAt,
6802
+ node.workspacePath ?? null,
6803
+ node.isGlobal ? 1 : 0
6804
+ );
6805
+ });
6093
6806
  }
6094
6807
  listRuntimeNodes(sessionId, limit = 500) {
6095
6808
  const rows = sessionId ? this.db.prepare(`
@@ -6112,30 +6825,32 @@ Original error: ${err.message}`
6112
6825
  }));
6113
6826
  }
6114
6827
  addRuntimeNodeLog(log) {
6115
- this.db.prepare(`
6116
- INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
6117
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6118
- `).run(
6119
- log.id,
6120
- log.sessionId,
6121
- log.tierId,
6122
- log.role,
6123
- log.label,
6124
- log.status,
6125
- log.currentAction ?? null,
6126
- log.progressPct ?? null,
6127
- log.timestamp,
6128
- log.workspacePath ?? null,
6129
- log.isGlobal ? 1 : 0
6130
- );
6131
- this.db.prepare(`
6132
- DELETE FROM runtime_node_logs
6133
- WHERE id NOT IN (
6134
- SELECT id FROM runtime_node_logs
6135
- ORDER BY timestamp DESC
6136
- LIMIT 2000
6137
- )
6138
- `).run();
6828
+ this.enqueueWrite(() => {
6829
+ this.db.prepare(`
6830
+ INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
6831
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6832
+ `).run(
6833
+ log.id,
6834
+ log.sessionId,
6835
+ log.tierId,
6836
+ log.role,
6837
+ log.label,
6838
+ log.status,
6839
+ log.currentAction ?? null,
6840
+ log.progressPct ?? null,
6841
+ log.timestamp,
6842
+ log.workspacePath ?? null,
6843
+ log.isGlobal ? 1 : 0
6844
+ );
6845
+ this.db.prepare(`
6846
+ DELETE FROM runtime_node_logs
6847
+ WHERE id NOT IN (
6848
+ SELECT id FROM runtime_node_logs
6849
+ ORDER BY timestamp DESC
6850
+ LIMIT 2000
6851
+ )
6852
+ `).run();
6853
+ });
6139
6854
  }
6140
6855
  listRuntimeNodeLogs(sessionId, tierId, limit = 200) {
6141
6856
  let rows;
@@ -6173,19 +6888,21 @@ Original error: ${err.message}`
6173
6888
  }
6174
6889
  // ── Messages ──────────────────────────────────
6175
6890
  addMessage(message) {
6176
- this.db.prepare(`
6177
- INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
6178
- VALUES (?, ?, ?, ?, ?, ?, ?)
6179
- `).run(
6180
- message.id,
6181
- message.sessionId,
6182
- message.role,
6183
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
6184
- message.timestamp,
6185
- message.tokens ? JSON.stringify(message.tokens) : null,
6186
- message.agentMessages ? JSON.stringify(message.agentMessages) : null
6187
- );
6188
- this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
6891
+ this.enqueueWrite(() => {
6892
+ this.db.prepare(`
6893
+ INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
6894
+ VALUES (?, ?, ?, ?, ?, ?, ?)
6895
+ `).run(
6896
+ message.id,
6897
+ message.sessionId,
6898
+ message.role,
6899
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
6900
+ message.timestamp,
6901
+ message.tokens ? JSON.stringify(message.tokens) : null,
6902
+ message.agentMessages ? JSON.stringify(message.agentMessages) : null
6903
+ );
6904
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
6905
+ });
6189
6906
  }
6190
6907
  getSessionMessages(sessionId) {
6191
6908
  const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
@@ -6282,10 +6999,12 @@ Original error: ${err.message}`
6282
6999
  }
6283
7000
  // ── Audit Log ─────────────────────────────────
6284
7001
  addAuditEntry(entry) {
6285
- this.db.prepare(`
6286
- INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
6287
- VALUES (?, ?, ?, ?, ?, ?)
6288
- `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
7002
+ this.enqueueWrite(() => {
7003
+ this.db.prepare(`
7004
+ INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
7005
+ VALUES (?, ?, ?, ?, ?, ?)
7006
+ `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
7007
+ });
6289
7008
  }
6290
7009
  getAuditLog(sessionId, limit = 100) {
6291
7010
  const rows = this.db.prepare("SELECT * FROM audit_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
@@ -6300,10 +7019,12 @@ Original error: ${err.message}`
6300
7019
  }
6301
7020
  // ── File Snapshots ────────────────────────────
6302
7021
  addFileSnapshot(sessionId, filePath, content) {
6303
- this.db.prepare(`
6304
- INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
6305
- VALUES (?, ?, ?, ?, ?)
6306
- `).run(randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
7022
+ this.enqueueWrite(() => {
7023
+ this.db.prepare(`
7024
+ INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
7025
+ VALUES (?, ?, ?, ?, ?)
7026
+ `).run(randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
7027
+ });
6307
7028
  }
6308
7029
  getLatestFileSnapshots(sessionId) {
6309
7030
  const rows = this.db.prepare(`
@@ -6599,7 +7320,7 @@ var ConfigManager = class {
6599
7320
  globalDir;
6600
7321
  constructor(workspacePath = process.cwd()) {
6601
7322
  this.workspacePath = workspacePath;
6602
- this.globalDir = path13.join(os.homedir(), GLOBAL_CONFIG_DIR);
7323
+ this.globalDir = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR);
6603
7324
  }
6604
7325
  async load() {
6605
7326
  this.config = await this.loadConfig();
@@ -6969,7 +7690,7 @@ var DashboardServer = class {
6969
7690
  // ── Setup ─────────────────────────────────────
6970
7691
  getGlobalStore() {
6971
7692
  if (!this.globalStore) {
6972
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7693
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
6973
7694
  this.globalStore = new MemoryStore(globalDbPath);
6974
7695
  }
6975
7696
  return this.globalStore;
@@ -7031,7 +7752,7 @@ var DashboardServer = class {
7031
7752
  }
7032
7753
  watchRuntimeChanges() {
7033
7754
  const workspaceDbPath = path13.join(this.workspacePath, CASCADE_DB_FILE);
7034
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7755
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7035
7756
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
7036
7757
  for (const watchPath of watchPaths) {
7037
7758
  if (!fs11.existsSync(watchPath)) continue;
@@ -7139,7 +7860,7 @@ var DashboardServer = class {
7139
7860
  const sessionId = req.params.id;
7140
7861
  this.store.deleteSession(sessionId);
7141
7862
  this.store.deleteRuntimeSession(sessionId);
7142
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7863
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7143
7864
  const globalStore = new MemoryStore(globalDbPath);
7144
7865
  try {
7145
7866
  globalStore.deleteRuntimeSession(sessionId);
@@ -7153,7 +7874,7 @@ var DashboardServer = class {
7153
7874
  });
7154
7875
  this.app.delete("/api/sessions", auth, (req, res) => {
7155
7876
  const body = req.body;
7156
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7877
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7157
7878
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
7158
7879
  const globalStore = new MemoryStore(globalDbPath);
7159
7880
  try {
@@ -7176,7 +7897,7 @@ var DashboardServer = class {
7176
7897
  });
7177
7898
  this.app.delete("/api/runtime", auth, (_req, res) => {
7178
7899
  this.store.deleteAllRuntimeNodes();
7179
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7900
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7180
7901
  const globalStore = new MemoryStore(globalDbPath);
7181
7902
  try {
7182
7903
  globalStore.deleteAllRuntimeNodes();
@@ -7272,7 +7993,7 @@ var DashboardServer = class {
7272
7993
  this.app.get("/api/runtime", auth, (req, res) => {
7273
7994
  const scope = req.query["scope"] ?? "workspace";
7274
7995
  if (scope === "global") {
7275
- const globalDbPath = path13.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7996
+ const globalDbPath = path13.join(os2.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
7276
7997
  const globalStore = new MemoryStore(globalDbPath);
7277
7998
  try {
7278
7999
  res.json({
@@ -7483,6 +8204,6 @@ var HooksRunner = class {
7483
8204
  }
7484
8205
  };
7485
8206
 
7486
- export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeIgnore, CascadeRouter, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, runCascade, streamCascade };
8207
+ export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, runCascade, streamCascade };
7487
8208
  //# sourceMappingURL=index.js.map
7488
8209
  //# sourceMappingURL=index.js.map