cascade-ai 0.13.1 → 0.13.2

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.d.cts CHANGED
@@ -429,6 +429,14 @@ interface CascadeConfig {
429
429
  policy: 'local-only';
430
430
  }>;
431
431
  };
432
+ /**
433
+ * Routing controls. `forceTier` pins the run's root tier — 'T1' (full
434
+ * hierarchy), 'T2' (manager + workers), 'T3' (single worker) — bypassing the
435
+ * complexity classifier. 'auto' (default) uses classification.
436
+ */
437
+ routing?: {
438
+ forceTier?: 'auto' | 'T1' | 'T2' | 'T3';
439
+ };
432
440
  /** Render the TUI in the alternate screen buffer (vim-style). Default: false. */
433
441
  altScreen?: boolean;
434
442
  }
@@ -595,6 +603,19 @@ interface PermissionRequest {
595
603
  * auto-approve a later dangerous action.
596
604
  */
597
605
  forceReprompt?: boolean;
606
+ /**
607
+ * Escalation trail — each engaged tier's non-binding recommendation as the
608
+ * request rose toward the user. Dangerous tools never get a terminal
609
+ * decision from a tier; the tier records its advice here instead, so the
610
+ * user sees who asked and what T2/T1 thought.
611
+ */
612
+ trail?: PermissionTrailEntry[];
613
+ }
614
+ /** One tier's advisory verdict recorded while a request escalated upward. */
615
+ interface PermissionTrailEntry {
616
+ tier: 'T2' | 'T1';
617
+ verdict: 'approve' | 'deny' | 'unsure';
618
+ reason?: string;
598
619
  }
599
620
  /**
600
621
  * A decision made at any tier (T2, T1, or USER) about a PermissionRequest.
@@ -1374,7 +1395,16 @@ declare abstract class BaseTier extends EventEmitter {
1374
1395
  protected hierarchyContext: string;
1375
1396
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
1376
1397
  protected signal?: AbortSignal;
1398
+ /**
1399
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
1400
+ * Complex). Its own synthesis stream is the user-facing answer and is
1401
+ * tagged `primary` so the desktop renders it live — background workers,
1402
+ * which would interleave, are not tagged.
1403
+ */
1404
+ protected isPresenter: boolean;
1377
1405
  constructor(role: TierRole, id?: string, parentId?: string);
1406
+ /** Mark this tier as the run's presenter (root tier). */
1407
+ setPresenter(on?: boolean): void;
1378
1408
  getStatus(): TierStatus;
1379
1409
  protected setStatus(status: TierStatus, output?: string): void;
1380
1410
  protected setLabel(label: string): void;
@@ -1698,6 +1728,14 @@ declare class Cascade extends EventEmitter {
1698
1728
  * do?" requests from being mis-routed into a multi-agent, multi-thousand-token run.
1699
1729
  */
1700
1730
  private looksLikeReadOnlyInquiry;
1731
+ /**
1732
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
1733
+ * multiple sections/workers). Deliberately conservative: requires a
1734
+ * build/implementation verb AND either an app/system-scale noun or an
1735
+ * explicit multi-part structure, so ordinary single-file asks (handled as
1736
+ * Simple/Moderate) don't get over-escalated.
1737
+ */
1738
+ private looksClearlyComplex;
1701
1739
  private static globCache;
1702
1740
  private countWorkspaceFiles;
1703
1741
  private determineComplexity;
@@ -2161,7 +2199,7 @@ declare class DashboardSocket {
2161
2199
  }) => void): void;
2162
2200
  emitToSocket(socketId: string, event: string, data: unknown): void;
2163
2201
  onConfigGet(callback: (socketId: string) => void): void;
2164
- onCascadeRun(callback: (prompt: string, model: string, socketId: string, sessionId?: string) => void): void;
2202
+ onCascadeRun(callback: (prompt: string, model: string, socketId: string, sessionId?: string, forceTier?: string) => void): void;
2165
2203
  close(): void;
2166
2204
  }
2167
2205
 
@@ -2182,6 +2220,13 @@ declare class DashboardServer {
2182
2220
  * of runs the session performed in this server's lifetime.
2183
2221
  */
2184
2222
  private sessionTaskIds;
2223
+ /**
2224
+ * Tool-approval requests awaiting a user decision from a connected client,
2225
+ * keyed by the request's uuid. The desktop shows a modal on
2226
+ * `permission:user-required` and answers with `permission:decision`; this
2227
+ * map is how that answer reaches the run that's blocked on it.
2228
+ */
2229
+ private pendingApprovals;
2185
2230
  private port;
2186
2231
  private host;
2187
2232
  private workspacePath;
@@ -2221,6 +2266,18 @@ declare class DashboardServer {
2221
2266
  */
2222
2267
  private steerSessions;
2223
2268
  private recordSessionTask;
2269
+ /**
2270
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
2271
+ * user. The request itself was already pushed to the client via the
2272
+ * `permission:user-required` forward; here we just park a resolver keyed by
2273
+ * the request id and wait for the client's `permission:decision` (handled in
2274
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
2275
+ * pending until the client answers, the run ends, or the escalator's own
2276
+ * timeout denies it.
2277
+ */
2278
+ private makeApprovalCallback;
2279
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
2280
+ private denyPendingApprovals;
2224
2281
  private persistRuntimeRow;
2225
2282
  /**
2226
2283
  * Continuing an existing session: the Cascade orchestrator is per-run and
@@ -2462,4 +2519,4 @@ declare class CascadeToolError extends Error {
2462
2519
  declare function preferIpv4Host(url: string | undefined): string | undefined;
2463
2520
  declare function nodeHttpFetch(input: string | URL | Request, init?: RequestInit, redirectCount?: number): Promise<Response>;
2464
2521
 
2465
- export { AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, ConfigManager, type ConversationMessage, 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, type DashboardConfig, DashboardServer, type EscalationPayload, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, McpClient, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type StatusUpdate, type StoredMessage, type StreamChunk, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
2522
+ export { AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, ConfigManager, type ConversationMessage, 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, type DashboardConfig, DashboardServer, type EscalationPayload, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, McpClient, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type StatusUpdate, type StoredMessage, type StreamChunk, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
package/dist/index.d.ts CHANGED
@@ -429,6 +429,14 @@ interface CascadeConfig {
429
429
  policy: 'local-only';
430
430
  }>;
431
431
  };
432
+ /**
433
+ * Routing controls. `forceTier` pins the run's root tier — 'T1' (full
434
+ * hierarchy), 'T2' (manager + workers), 'T3' (single worker) — bypassing the
435
+ * complexity classifier. 'auto' (default) uses classification.
436
+ */
437
+ routing?: {
438
+ forceTier?: 'auto' | 'T1' | 'T2' | 'T3';
439
+ };
432
440
  /** Render the TUI in the alternate screen buffer (vim-style). Default: false. */
433
441
  altScreen?: boolean;
434
442
  }
@@ -595,6 +603,19 @@ interface PermissionRequest {
595
603
  * auto-approve a later dangerous action.
596
604
  */
597
605
  forceReprompt?: boolean;
606
+ /**
607
+ * Escalation trail — each engaged tier's non-binding recommendation as the
608
+ * request rose toward the user. Dangerous tools never get a terminal
609
+ * decision from a tier; the tier records its advice here instead, so the
610
+ * user sees who asked and what T2/T1 thought.
611
+ */
612
+ trail?: PermissionTrailEntry[];
613
+ }
614
+ /** One tier's advisory verdict recorded while a request escalated upward. */
615
+ interface PermissionTrailEntry {
616
+ tier: 'T2' | 'T1';
617
+ verdict: 'approve' | 'deny' | 'unsure';
618
+ reason?: string;
598
619
  }
599
620
  /**
600
621
  * A decision made at any tier (T2, T1, or USER) about a PermissionRequest.
@@ -1374,7 +1395,16 @@ declare abstract class BaseTier extends EventEmitter {
1374
1395
  protected hierarchyContext: string;
1375
1396
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
1376
1397
  protected signal?: AbortSignal;
1398
+ /**
1399
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
1400
+ * Complex). Its own synthesis stream is the user-facing answer and is
1401
+ * tagged `primary` so the desktop renders it live — background workers,
1402
+ * which would interleave, are not tagged.
1403
+ */
1404
+ protected isPresenter: boolean;
1377
1405
  constructor(role: TierRole, id?: string, parentId?: string);
1406
+ /** Mark this tier as the run's presenter (root tier). */
1407
+ setPresenter(on?: boolean): void;
1378
1408
  getStatus(): TierStatus;
1379
1409
  protected setStatus(status: TierStatus, output?: string): void;
1380
1410
  protected setLabel(label: string): void;
@@ -1698,6 +1728,14 @@ declare class Cascade extends EventEmitter {
1698
1728
  * do?" requests from being mis-routed into a multi-agent, multi-thousand-token run.
1699
1729
  */
1700
1730
  private looksLikeReadOnlyInquiry;
1731
+ /**
1732
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
1733
+ * multiple sections/workers). Deliberately conservative: requires a
1734
+ * build/implementation verb AND either an app/system-scale noun or an
1735
+ * explicit multi-part structure, so ordinary single-file asks (handled as
1736
+ * Simple/Moderate) don't get over-escalated.
1737
+ */
1738
+ private looksClearlyComplex;
1701
1739
  private static globCache;
1702
1740
  private countWorkspaceFiles;
1703
1741
  private determineComplexity;
@@ -2161,7 +2199,7 @@ declare class DashboardSocket {
2161
2199
  }) => void): void;
2162
2200
  emitToSocket(socketId: string, event: string, data: unknown): void;
2163
2201
  onConfigGet(callback: (socketId: string) => void): void;
2164
- onCascadeRun(callback: (prompt: string, model: string, socketId: string, sessionId?: string) => void): void;
2202
+ onCascadeRun(callback: (prompt: string, model: string, socketId: string, sessionId?: string, forceTier?: string) => void): void;
2165
2203
  close(): void;
2166
2204
  }
2167
2205
 
@@ -2182,6 +2220,13 @@ declare class DashboardServer {
2182
2220
  * of runs the session performed in this server's lifetime.
2183
2221
  */
2184
2222
  private sessionTaskIds;
2223
+ /**
2224
+ * Tool-approval requests awaiting a user decision from a connected client,
2225
+ * keyed by the request's uuid. The desktop shows a modal on
2226
+ * `permission:user-required` and answers with `permission:decision`; this
2227
+ * map is how that answer reaches the run that's blocked on it.
2228
+ */
2229
+ private pendingApprovals;
2185
2230
  private port;
2186
2231
  private host;
2187
2232
  private workspacePath;
@@ -2221,6 +2266,18 @@ declare class DashboardServer {
2221
2266
  */
2222
2267
  private steerSessions;
2223
2268
  private recordSessionTask;
2269
+ /**
2270
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
2271
+ * user. The request itself was already pushed to the client via the
2272
+ * `permission:user-required` forward; here we just park a resolver keyed by
2273
+ * the request id and wait for the client's `permission:decision` (handled in
2274
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
2275
+ * pending until the client answers, the run ends, or the escalator's own
2276
+ * timeout denies it.
2277
+ */
2278
+ private makeApprovalCallback;
2279
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
2280
+ private denyPendingApprovals;
2224
2281
  private persistRuntimeRow;
2225
2282
  /**
2226
2283
  * Continuing an existing session: the Cascade orchestrator is per-run and
@@ -2462,4 +2519,4 @@ declare class CascadeToolError extends Error {
2462
2519
  declare function preferIpv4Host(url: string | undefined): string | undefined;
2463
2520
  declare function nodeHttpFetch(input: string | URL | Request, init?: RequestInit, redirectCount?: number): Promise<Response>;
2464
2521
 
2465
- export { AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, ConfigManager, type ConversationMessage, 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, type DashboardConfig, DashboardServer, type EscalationPayload, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, McpClient, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type StatusUpdate, type StoredMessage, type StreamChunk, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
2522
+ export { AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, ConfigManager, type ConversationMessage, 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, type DashboardConfig, DashboardServer, type EscalationPayload, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, McpClient, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type StatusUpdate, type StoredMessage, type StreamChunk, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
package/dist/index.js CHANGED
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
190
190
  });
191
191
 
192
192
  // src/constants.ts
193
- var CASCADE_VERSION = "0.13.1";
193
+ var CASCADE_VERSION = "0.13.2";
194
194
  var CASCADE_CONFIG_DIR = ".cascade";
195
195
  var CASCADE_MD_FILE = "CASCADE.md";
196
196
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -3123,6 +3123,13 @@ var BaseTier = class extends EventEmitter {
3123
3123
  hierarchyContext = "";
3124
3124
  /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
3125
3125
  signal;
3126
+ /**
3127
+ * True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
3128
+ * Complex). Its own synthesis stream is the user-facing answer and is
3129
+ * tagged `primary` so the desktop renders it live — background workers,
3130
+ * which would interleave, are not tagged.
3131
+ */
3132
+ isPresenter = false;
3126
3133
  constructor(role, id, parentId) {
3127
3134
  super();
3128
3135
  this.role = role;
@@ -3130,6 +3137,10 @@ var BaseTier = class extends EventEmitter {
3130
3137
  this.parentId = parentId;
3131
3138
  this.label = this.id;
3132
3139
  }
3140
+ /** Mark this tier as the run's presenter (root tier). */
3141
+ setPresenter(on = true) {
3142
+ this.isPresenter = on;
3143
+ }
3133
3144
  getStatus() {
3134
3145
  return this.status;
3135
3146
  }
@@ -3864,7 +3875,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
3864
3875
  "T3",
3865
3876
  options,
3866
3877
  (chunk) => {
3867
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
3878
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
3868
3879
  }
3869
3880
  );
3870
3881
  let effectiveToolCalls = result.toolCalls ?? [];
@@ -5325,6 +5336,7 @@ ${peerOutputs}` : "";
5325
5336
  chunkEnd++;
5326
5337
  }
5327
5338
  i = chunkEnd;
5339
+ const isLastChunk = chunkEnd >= completed.length;
5328
5340
  const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
5329
5341
  ${currentSummary ? `
5330
5342
  PREVIOUS SUMMARY SO FAR:
@@ -5334,6 +5346,7 @@ NEW OUTPUTS TO INTEGRATE:
5334
5346
  ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
5335
5347
  const messages = [{ role: "user", content: prompt }];
5336
5348
  try {
5349
+ const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
5337
5350
  const result = await this.router.generate("T2", {
5338
5351
  messages,
5339
5352
  systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
@@ -5341,7 +5354,7 @@ NEW OUTPUTS TO INTEGRATE:
5341
5354
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
5342
5355
  maxTokens: 500,
5343
5356
  ...this.sectionModel ? { model: this.sectionModel } : {}
5344
- });
5357
+ }, streamFinal);
5345
5358
  currentSummary = result.content;
5346
5359
  } catch (err) {
5347
5360
  this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -5393,14 +5406,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
5393
5406
  ...this.sectionModel ? { model: this.sectionModel } : {}
5394
5407
  });
5395
5408
  const answer = result.content.trim().toUpperCase();
5396
- if (answer.includes("YES")) {
5397
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
5398
- }
5399
- if (answer.includes("NO")) {
5400
- return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
5401
- }
5409
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
5410
+ (req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
5402
5411
  return null;
5403
5412
  } catch {
5413
+ (req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
5404
5414
  return null;
5405
5415
  }
5406
5416
  }
@@ -6079,7 +6089,7 @@ Instructions:
6079
6089
  systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
6080
6090
  maxTokens: 8e3
6081
6091
  }, (chunk) => {
6082
- this.emit("stream:token", { tierId: this.id, text: chunk.text });
6092
+ this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
6083
6093
  });
6084
6094
  return result.content;
6085
6095
  }
@@ -6108,14 +6118,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
6108
6118
  temperature: 0
6109
6119
  });
6110
6120
  const answer = result.content.trim().toUpperCase();
6111
- if (answer.includes("YES")) {
6112
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
6113
- }
6114
- if (answer.includes("NO")) {
6115
- return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
6116
- }
6121
+ const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
6122
+ (req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
6117
6123
  return null;
6118
6124
  } catch {
6125
+ (req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
6119
6126
  return null;
6120
6127
  }
6121
6128
  }
@@ -8222,6 +8229,8 @@ var CascadeConfigSchema = z.object({
8222
8229
  privacy: z.object({
8223
8230
  paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
8224
8231
  }).optional(),
8232
+ /** Routing controls — forceTier pins the root tier, bypassing the classifier. */
8233
+ routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
8225
8234
  /**
8226
8235
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
8227
8236
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -9496,6 +9505,21 @@ ${last.partialOutput}` : "");
9496
9505
  const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
9497
9506
  return inquiry && !producesArtifact;
9498
9507
  }
9508
+ /**
9509
+ * Strong, explicit signals that a task needs the full hierarchy (planning +
9510
+ * multiple sections/workers). Deliberately conservative: requires a
9511
+ * build/implementation verb AND either an app/system-scale noun or an
9512
+ * explicit multi-part structure, so ordinary single-file asks (handled as
9513
+ * Simple/Moderate) don't get over-escalated.
9514
+ */
9515
+ looksClearlyComplex(prompt) {
9516
+ const p = prompt.trim();
9517
+ if (p.length < 24) return false;
9518
+ const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
9519
+ const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
9520
+ const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
9521
+ return buildVerb && (scaleNoun || multiPart);
9522
+ }
9499
9523
  // Cache glob scan results per workspace path to avoid repeated I/O.
9500
9524
  static globCache = /* @__PURE__ */ new Map();
9501
9525
  async countWorkspaceFiles(workspacePath) {
@@ -9579,10 +9603,16 @@ ${prompt}` : prompt;
9579
9603
  let verdict;
9580
9604
  if (match) {
9581
9605
  verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
9582
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
9606
+ if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
9607
+ this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
9608
+ verdict = "Complex";
9609
+ } else {
9610
+ this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
9611
+ }
9583
9612
  } else {
9584
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
9585
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
9613
+ const words = prompt.trim().split(/\s+/).length;
9614
+ verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
9615
+ this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
9586
9616
  }
9587
9617
  return verdict;
9588
9618
  } catch {
@@ -9634,7 +9664,15 @@ ${prompt}` : prompt;
9634
9664
  }
9635
9665
  escalator.resolveUserDecision(req.id, approved, always);
9636
9666
  });
9637
- const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
9667
+ const forceTier = this.config.routing?.forceTier;
9668
+ const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
9669
+ let complexity;
9670
+ if (forced) {
9671
+ complexity = forced;
9672
+ this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
9673
+ } else {
9674
+ complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
9675
+ }
9638
9676
  this.telemetry.capture("cascade:session_start", {
9639
9677
  complexity,
9640
9678
  providerCount: this.config.providers.length,
@@ -9714,6 +9752,7 @@ ${prompt}` : prompt;
9714
9752
  try {
9715
9753
  if (complexity === "Simple") {
9716
9754
  const t3 = new T3Worker(this.router, this.toolRegistry, "root");
9755
+ t3.setPresenter(true);
9717
9756
  t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
9718
9757
  if (identityPrompt) {
9719
9758
  t3.setSystemPromptOverride(identityPrompt);
@@ -9738,6 +9777,7 @@ ${prompt}` : prompt;
9738
9777
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
9739
9778
  } else if (complexity === "Moderate") {
9740
9779
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
9780
+ t2.setPresenter(true);
9741
9781
  t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
9742
9782
  if (identityPrompt) {
9743
9783
  t2.setSystemPromptOverride(identityPrompt);
@@ -9787,6 +9827,7 @@ ${prompt}` : prompt;
9787
9827
  }
9788
9828
  } else {
9789
9829
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
9830
+ t1.setPresenter(true);
9790
9831
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
9791
9832
  if (identityPrompt) {
9792
9833
  t1.setSystemPromptOverride(identityPrompt);
@@ -11244,7 +11285,8 @@ var DashboardSocket = class {
11244
11285
  socket.on("cascade:run", (payload) => {
11245
11286
  if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
11246
11287
  const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
11247
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
11288
+ const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
11289
+ callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
11248
11290
  }
11249
11291
  });
11250
11292
  });
@@ -11271,6 +11313,13 @@ var DashboardServer = class {
11271
11313
  * of runs the session performed in this server's lifetime.
11272
11314
  */
11273
11315
  sessionTaskIds = /* @__PURE__ */ new Map();
11316
+ /**
11317
+ * Tool-approval requests awaiting a user decision from a connected client,
11318
+ * keyed by the request's uuid. The desktop shows a modal on
11319
+ * `permission:user-required` and answers with `permission:decision`; this
11320
+ * map is how that answer reaches the run that's blocked on it.
11321
+ */
11322
+ pendingApprovals = /* @__PURE__ */ new Map();
11274
11323
  port;
11275
11324
  host;
11276
11325
  workspacePath;
@@ -11329,17 +11378,18 @@ var DashboardServer = class {
11329
11378
  }
11330
11379
  this.persistConfig();
11331
11380
  });
11332
- this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
11381
+ this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
11333
11382
  const sessionId = requestedSessionId ?? randomUUID();
11334
11383
  const abortController = new AbortController();
11335
11384
  this.activeControllers.set(sessionId, abortController);
11336
11385
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
11337
11386
  const title = this.persistRunStart(sessionId, prompt);
11338
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
11387
+ let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
11388
+ if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
11339
11389
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
11340
11390
  this.activeSessions.set(sessionId, cascade);
11341
11391
  cascade.on("stream:token", (e) => {
11342
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
11392
+ this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
11343
11393
  });
11344
11394
  cascade.on("tier:status", (e) => {
11345
11395
  this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
@@ -11351,7 +11401,11 @@ var DashboardServer = class {
11351
11401
  this.socket.emitPeerMessage(e);
11352
11402
  });
11353
11403
  try {
11354
- const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
11404
+ const result = await cascade.run({
11405
+ prompt: runPrompt,
11406
+ signal: abortController.signal,
11407
+ approvalCallback: this.makeApprovalCallback(sessionId)
11408
+ });
11355
11409
  this.recordSessionTask(sessionId, result.taskId);
11356
11410
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
11357
11411
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
@@ -11370,10 +11424,18 @@ var DashboardServer = class {
11370
11424
  } finally {
11371
11425
  this.activeSessions.delete(sessionId);
11372
11426
  this.activeControllers.delete(sessionId);
11427
+ this.denyPendingApprovals(sessionId);
11373
11428
  }
11374
11429
  });
11375
11430
  this.socket.onSessionHalt((sessionId) => {
11376
11431
  this.activeControllers.get(sessionId)?.abort();
11432
+ this.denyPendingApprovals(sessionId);
11433
+ });
11434
+ this.socket.onApprovalResponse(({ requestId, approved, always }) => {
11435
+ const pending = this.pendingApprovals.get(requestId);
11436
+ if (!pending) return;
11437
+ this.pendingApprovals.delete(requestId);
11438
+ pending.resolve({ approved: !!approved, always: !!always });
11377
11439
  });
11378
11440
  this.socket.onSessionSteer((message, sessionId, nodeId) => {
11379
11441
  const steered = this.steerSessions(message, sessionId, nodeId);
@@ -11541,6 +11603,29 @@ var DashboardServer = class {
11541
11603
  list.push(taskId);
11542
11604
  this.sessionTaskIds.set(sessionId, list);
11543
11605
  }
11606
+ /**
11607
+ * Approval bridge: cascade calls this when a dangerous tool escalates to the
11608
+ * user. The request itself was already pushed to the client via the
11609
+ * `permission:user-required` forward; here we just park a resolver keyed by
11610
+ * the request id and wait for the client's `permission:decision` (handled in
11611
+ * onApprovalResponse). Never auto-approves — an unanswered request stays
11612
+ * pending until the client answers, the run ends, or the escalator's own
11613
+ * timeout denies it.
11614
+ */
11615
+ makeApprovalCallback(sessionId) {
11616
+ return (request) => new Promise((resolve) => {
11617
+ this.pendingApprovals.set(request.id, { resolve, sessionId });
11618
+ });
11619
+ }
11620
+ /** Deny + clear any approvals still pending for a session (run end / abort). */
11621
+ denyPendingApprovals(sessionId) {
11622
+ for (const [id, pending] of this.pendingApprovals) {
11623
+ if (pending.sessionId === sessionId) {
11624
+ this.pendingApprovals.delete(id);
11625
+ pending.resolve({ approved: false, always: false });
11626
+ }
11627
+ }
11628
+ }
11544
11629
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
11545
11630
  const now = (/* @__PURE__ */ new Date()).toISOString();
11546
11631
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -11729,15 +11814,6 @@ ${prompt}`;
11729
11814
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
11730
11815
  res.json({ success: true, ...payload });
11731
11816
  });
11732
- this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
11733
- const body = req.body;
11734
- const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
11735
- const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
11736
- const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
11737
- this.socket.broadcast("session:approve", payload);
11738
- if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
11739
- res.json({ success: true, ...payload });
11740
- });
11741
11817
  this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
11742
11818
  const body = req.body;
11743
11819
  const message = typeof body["message"] === "string" ? body["message"] : void 0;
@@ -11988,7 +12064,7 @@ ${prompt}`;
11988
12064
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
11989
12065
  this.activeSessions.set(sessionId, cascade);
11990
12066
  cascade.on("stream:token", (e) => {
11991
- this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
12067
+ this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
11992
12068
  });
11993
12069
  cascade.on("tier:status", (e) => {
11994
12070
  this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
@@ -12000,7 +12076,11 @@ ${prompt}`;
12000
12076
  this.socket.emitPeerMessage(e);
12001
12077
  });
12002
12078
  try {
12003
- const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
12079
+ const result = await cascade.run({
12080
+ prompt: runPrompt,
12081
+ identityId: body.identityId,
12082
+ approvalCallback: this.makeApprovalCallback(sessionId)
12083
+ });
12004
12084
  this.recordSessionTask(sessionId, result.taskId);
12005
12085
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
12006
12086
  this.socket.broadcast("cost:update", {
@@ -12018,6 +12098,7 @@ ${prompt}`;
12018
12098
  });
12019
12099
  } finally {
12020
12100
  this.activeSessions.delete(sessionId);
12101
+ this.denyPendingApprovals(sessionId);
12021
12102
  }
12022
12103
  })();
12023
12104
  });