cascade-ai 0.68.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -59
- package/dist/cli.cjs +445 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +445 -61
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +431 -57
- package/dist/index.cjs +431 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -12
- package/dist/index.d.ts +231 -12
- package/dist/index.js +431 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -61,30 +61,69 @@ interface ResumeStoreOptions {
|
|
|
61
61
|
maxAgeMs?: number;
|
|
62
62
|
/** Injectable clock, for tests. */
|
|
63
63
|
now?: () => number;
|
|
64
|
+
/** How long a claim may be held before it is considered abandoned. */
|
|
65
|
+
claimTimeoutMs?: number;
|
|
64
66
|
}
|
|
65
67
|
declare class ResumeStore {
|
|
66
68
|
private readonly dir;
|
|
67
69
|
private readonly maxCheckpoints;
|
|
68
70
|
private readonly maxAgeMs;
|
|
69
71
|
private readonly now;
|
|
72
|
+
private readonly claimTimeoutMs;
|
|
70
73
|
constructor(options: ResumeStoreOptions);
|
|
71
74
|
/**
|
|
72
|
-
* Persist a checkpoint and prune.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
+
* Persist a checkpoint and prune.
|
|
76
|
+
*
|
|
77
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
78
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
79
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
80
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
81
|
+
* look alike to them.
|
|
82
|
+
*
|
|
83
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
75
84
|
*/
|
|
76
85
|
save(checkpoint: Omit<ResumeCheckpoint, 'version' | 'createdAt'> & {
|
|
77
86
|
createdAt?: string;
|
|
78
|
-
}): Promise<
|
|
87
|
+
}): Promise<boolean>;
|
|
79
88
|
/** Valid, unexpired checkpoints, newest first. */
|
|
80
89
|
list(): Promise<ResumeCheckpoint[]>;
|
|
81
90
|
/** The most recent resumable run, or null. */
|
|
82
91
|
latest(): Promise<ResumeCheckpoint | null>;
|
|
83
92
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
93
|
+
* Claim a checkpoint for a resume attempt.
|
|
94
|
+
*
|
|
95
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
96
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
97
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
98
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
99
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
100
|
+
* wrong trade for a recovery mechanism.
|
|
101
|
+
*
|
|
102
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
103
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
104
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
105
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
106
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
107
|
+
*/
|
|
108
|
+
claim(taskId?: string): Promise<{
|
|
109
|
+
checkpoint: ResumeCheckpoint;
|
|
110
|
+
claimId: string;
|
|
111
|
+
} | null>;
|
|
112
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
113
|
+
release(claimId: string): Promise<void>;
|
|
114
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
115
|
+
settle(claimId: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
118
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
86
119
|
*/
|
|
87
120
|
consume(taskId?: string): Promise<ResumeCheckpoint | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
123
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
124
|
+
* trading one loss window for another.
|
|
125
|
+
*/
|
|
126
|
+
reclaimStale(): Promise<number>;
|
|
88
127
|
/** Remove every checkpoint for a task id. */
|
|
89
128
|
delete(taskId: string): Promise<void>;
|
|
90
129
|
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
@@ -336,7 +375,17 @@ interface ToolExecuteOptions {
|
|
|
336
375
|
}>;
|
|
337
376
|
}
|
|
338
377
|
type TierRole = 'T1' | 'T2' | 'T3';
|
|
339
|
-
|
|
378
|
+
/**
|
|
379
|
+
* A tier's lifecycle state.
|
|
380
|
+
*
|
|
381
|
+
* BLOCKED is terminal like COMPLETED and FAILED — the tier will never run,
|
|
382
|
+
* because work it depended on failed first. It is NOT the same word as
|
|
383
|
+
* `MessageStatus.BLOCKED` below, which means "running but stuck": one describes
|
|
384
|
+
* a tier that never started, the other a tier that started and cannot finish.
|
|
385
|
+
* Anything rendering a status label should treat BLOCKED as "skipped", not as
|
|
386
|
+
* an error and not as in-progress.
|
|
387
|
+
*/
|
|
388
|
+
type TierStatus = 'IDLE' | 'ACTIVE' | 'COMPLETED' | 'FAILED' | 'ESCALATED' | 'BLOCKED';
|
|
340
389
|
type TaskComplexity = 'Simple' | 'Moderate' | 'Complex' | 'Highly Complex';
|
|
341
390
|
interface TierConfig {
|
|
342
391
|
role: TierRole;
|
|
@@ -409,10 +458,27 @@ interface StatusUpdate {
|
|
|
409
458
|
interface T2Result {
|
|
410
459
|
sectionId: string;
|
|
411
460
|
sectionTitle: string;
|
|
412
|
-
|
|
461
|
+
/**
|
|
462
|
+
* BLOCKED is not a kind of FAILED. A failed section was attempted and did not
|
|
463
|
+
* work; a blocked one was never attempted, because something it depended on
|
|
464
|
+
* failed first — so it spent no tokens and says nothing about whether it
|
|
465
|
+
* would have worked. Reporting it as FAILED (which is what happened until
|
|
466
|
+
* now) tells the user their work broke when in fact the scheduler protected
|
|
467
|
+
* them from paying for work that could not succeed yet.
|
|
468
|
+
*/
|
|
469
|
+
status: 'COMPLETED' | 'PARTIAL' | 'FAILED' | 'ESCALATED' | 'BLOCKED';
|
|
413
470
|
t3Results: T3Result[];
|
|
414
471
|
sectionSummary: string;
|
|
415
472
|
issues: string[];
|
|
473
|
+
/**
|
|
474
|
+
* Set only on BLOCKED sections: which upstream work stopped this one, by
|
|
475
|
+
* title and by id. Structured rather than flattened into `issues`, so a UI
|
|
476
|
+
* can link a skipped section back to its cause instead of parsing prose.
|
|
477
|
+
*/
|
|
478
|
+
blockedBy?: {
|
|
479
|
+
ids: string[];
|
|
480
|
+
titles: string[];
|
|
481
|
+
};
|
|
416
482
|
/**
|
|
417
483
|
* Set when this section reached PARTIAL because the user was asked about an
|
|
418
484
|
* escalation and chose "skip" — not because a T2/T3 pipeline shortfall left
|
|
@@ -1179,7 +1245,24 @@ declare class TaskAnalyzer {
|
|
|
1179
1245
|
private feedback?;
|
|
1180
1246
|
private bias;
|
|
1181
1247
|
private lastProfile;
|
|
1182
|
-
|
|
1248
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
1249
|
+
private currentRunSelections;
|
|
1250
|
+
/**
|
|
1251
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
1252
|
+
* type they were chosen for.
|
|
1253
|
+
*
|
|
1254
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
1255
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
1256
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
1257
|
+
* model is good at creative writing, from a rating that never said so.
|
|
1258
|
+
*
|
|
1259
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
1260
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
1261
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
1262
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
1263
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
1264
|
+
*/
|
|
1265
|
+
private lastCompletedRun?;
|
|
1183
1266
|
constructor(tracker?: ModelPerformanceTracker, bias?: AutoBias);
|
|
1184
1267
|
/**
|
|
1185
1268
|
* Supply per-model rating counts. Optional by design: a host that collects no
|
|
@@ -1211,9 +1294,17 @@ declare class TaskAnalyzer {
|
|
|
1211
1294
|
*/
|
|
1212
1295
|
recordRunOutcome(outcome: 'success' | 'failure', costByTier: Record<string, number>, contextTokens?: number): void;
|
|
1213
1296
|
/**
|
|
1214
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
1297
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
1215
1298
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
1216
|
-
*
|
|
1299
|
+
*
|
|
1300
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
1301
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
1302
|
+
* empty.
|
|
1303
|
+
*
|
|
1304
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
1305
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
1306
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
1307
|
+
* that reports false, which is what "rate this run" should mean.
|
|
1217
1308
|
*/
|
|
1218
1309
|
recordExplicitRating(rating: 'good' | 'bad'): boolean;
|
|
1219
1310
|
private scoreModel;
|
|
@@ -2150,9 +2241,49 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
2150
2241
|
* which node (Cockpit node panel / Why panel).
|
|
2151
2242
|
*/
|
|
2152
2243
|
protected servingModel?: string;
|
|
2244
|
+
/**
|
|
2245
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
2246
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
2247
|
+
*
|
|
2248
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
2249
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
2250
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
2251
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
2252
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
2253
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
2254
|
+
* appears in the stream.
|
|
2255
|
+
*/
|
|
2256
|
+
protected graphNodeId?: string;
|
|
2257
|
+
protected graphDependsOn?: readonly string[];
|
|
2258
|
+
protected graphWave?: number;
|
|
2153
2259
|
constructor(role: TierRole, id?: string, parentId?: string);
|
|
2154
2260
|
/** Mark this tier as the run's presenter (root tier). */
|
|
2155
2261
|
setPresenter(on?: boolean): void;
|
|
2262
|
+
/**
|
|
2263
|
+
* Record where this tier sits in the compiled task graph.
|
|
2264
|
+
*
|
|
2265
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
2266
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
2267
|
+
* are set independently rather than all at once.
|
|
2268
|
+
*/
|
|
2269
|
+
setGraphPosition(position: {
|
|
2270
|
+
nodeId?: string;
|
|
2271
|
+
dependsOn?: readonly string[];
|
|
2272
|
+
wave?: number;
|
|
2273
|
+
}): void;
|
|
2274
|
+
/**
|
|
2275
|
+
* The graph fields, for spreading into a status payload.
|
|
2276
|
+
*
|
|
2277
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
2278
|
+
* two different call sites with two different payload shapes; adding a field
|
|
2279
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
2280
|
+
* consumer than never arriving at all.
|
|
2281
|
+
*/
|
|
2282
|
+
protected graphFields(): {
|
|
2283
|
+
nodeId?: string;
|
|
2284
|
+
dependsOn?: string[];
|
|
2285
|
+
waveId?: number;
|
|
2286
|
+
};
|
|
2156
2287
|
getStatus(): TierStatus;
|
|
2157
2288
|
protected setStatus(status: TierStatus, output?: string): void;
|
|
2158
2289
|
/** Record the model serving this tier; future status events carry it. */
|
|
@@ -2532,6 +2663,25 @@ declare class Cascade extends EventEmitter {
|
|
|
2532
2663
|
private lastInterruptedRun?;
|
|
2533
2664
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
2534
2665
|
private resumeStore?;
|
|
2666
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
2667
|
+
private claimedResumeId?;
|
|
2668
|
+
/**
|
|
2669
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
2670
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
2671
|
+
*/
|
|
2672
|
+
private resumedFrom?;
|
|
2673
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
2674
|
+
private wroteCheckpointThisRun;
|
|
2675
|
+
/**
|
|
2676
|
+
* Whether the run reached the end of its normal path.
|
|
2677
|
+
*
|
|
2678
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
2679
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
2680
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
2681
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
2682
|
+
* the original claim and the finished sections with it.
|
|
2683
|
+
*/
|
|
2684
|
+
private completedNormally;
|
|
2535
2685
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
2536
2686
|
private currentRunCompleted;
|
|
2537
2687
|
private initPromise?;
|
|
@@ -2732,6 +2882,19 @@ declare class Cascade extends EventEmitter {
|
|
|
2732
2882
|
maxTokens?: number;
|
|
2733
2883
|
taskId?: string;
|
|
2734
2884
|
}): Promise<string | null>;
|
|
2885
|
+
/**
|
|
2886
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
2887
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
2888
|
+
* run loop; safe to call when nothing is claimed.
|
|
2889
|
+
*/
|
|
2890
|
+
private settleClaimedResume;
|
|
2891
|
+
/**
|
|
2892
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
2893
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
2894
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
2895
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
2896
|
+
*/
|
|
2897
|
+
releaseClaimedResume(): Promise<void>;
|
|
2735
2898
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
2736
2899
|
private raiseBudgetForResume;
|
|
2737
2900
|
/**
|
|
@@ -3215,17 +3378,67 @@ declare class TaskGraphValidationError extends Error {
|
|
|
3215
3378
|
declare function compileTaskGraph<TPayload>(inputNodes: readonly TaskGraphInputNode<TPayload>[], options?: CompileTaskGraphOptions): CompiledTaskGraph<TPayload>;
|
|
3216
3379
|
|
|
3217
3380
|
type SchedulerExecutionMode = 'parallel' | 'sequential';
|
|
3381
|
+
/**
|
|
3382
|
+
* How a finished node turned out.
|
|
3383
|
+
*
|
|
3384
|
+
* The scheduler used to know only "the promise resolved", which is not the same
|
|
3385
|
+
* question. T1 catches worker errors into an ordinary `T2Result { status:
|
|
3386
|
+
* 'FAILED' }` so one dead section cannot crash the run — a resolved value. The
|
|
3387
|
+
* scheduler saw success and released the dependents anyway, so "run the
|
|
3388
|
+
* integration tests" would start after "implement the API" had failed outright,
|
|
3389
|
+
* and then fail too, and bill for the privilege.
|
|
3390
|
+
*
|
|
3391
|
+
* `partial` deliberately does NOT block: partial output is usable, and treating
|
|
3392
|
+
* it as failure would cancel work that a degraded-but-real result can support.
|
|
3393
|
+
*/
|
|
3394
|
+
type NodeOutcome = 'succeeded' | 'partial' | 'failed';
|
|
3395
|
+
/** What to do with the dependents of a failed node. */
|
|
3396
|
+
type UpstreamFailurePolicy =
|
|
3397
|
+
/** Run them anyway — the historical behaviour, kept as the default. */
|
|
3398
|
+
'continue'
|
|
3399
|
+
/** Skip them, transitively, and report why. */
|
|
3400
|
+
| 'block';
|
|
3401
|
+
/** Why a node never ran. */
|
|
3402
|
+
interface BlockedNode {
|
|
3403
|
+
/**
|
|
3404
|
+
* Ids of the failed ancestors responsible, nearest first. Stable and
|
|
3405
|
+
* machine-readable — use these to link a blocked node back to its cause.
|
|
3406
|
+
*/
|
|
3407
|
+
blockedBy: string[];
|
|
3408
|
+
/**
|
|
3409
|
+
* The same ancestors by title, in the same order. Node ids are internal
|
|
3410
|
+
* ("s1", "s2"); anything a person reads should use these instead.
|
|
3411
|
+
*/
|
|
3412
|
+
blockedByTitles: string[];
|
|
3413
|
+
/** A sentence naming the cause, written with titles rather than ids. */
|
|
3414
|
+
reason: string;
|
|
3415
|
+
}
|
|
3218
3416
|
interface SchedulerHooks<TPayload, TResult> {
|
|
3219
3417
|
execute(node: TaskGraphNode<TPayload>): Promise<TResult>;
|
|
3220
3418
|
onWaveStart?(nodes: readonly TaskGraphNode<TPayload>[], wave: number): void | Promise<void>;
|
|
3221
3419
|
onNodeComplete?(node: TaskGraphNode<TPayload>, result: TResult): void | Promise<void>;
|
|
3420
|
+
/**
|
|
3421
|
+
* Decide whether a result counts as success. Omit it and every result counts
|
|
3422
|
+
* as `succeeded`, which is exactly the previous behaviour — so adding failure
|
|
3423
|
+
* awareness to a caller is opt-in and cannot silently change an existing one.
|
|
3424
|
+
*/
|
|
3425
|
+
classify?(node: TaskGraphNode<TPayload>, result: TResult): NodeOutcome;
|
|
3426
|
+
/**
|
|
3427
|
+
* A node that will never run because an ancestor failed. Callers use this to
|
|
3428
|
+
* synthesize a result for it, so downstream reporting still sees every node.
|
|
3429
|
+
*/
|
|
3430
|
+
onNodeBlocked?(node: TaskGraphNode<TPayload>, blocked: BlockedNode): void | Promise<void>;
|
|
3222
3431
|
}
|
|
3223
3432
|
interface SchedulerOptions {
|
|
3224
3433
|
mode?: SchedulerExecutionMode;
|
|
3434
|
+
/** Defaults to 'continue' so existing callers behave exactly as before. */
|
|
3435
|
+
onUpstreamFailure?: UpstreamFailurePolicy;
|
|
3225
3436
|
}
|
|
3226
3437
|
interface SchedulerResult<TResult> {
|
|
3227
3438
|
results: ReadonlyMap<string, TResult>;
|
|
3228
3439
|
waves: readonly (readonly string[])[];
|
|
3440
|
+
/** Nodes skipped because an ancestor failed. Empty under 'continue'. */
|
|
3441
|
+
blocked: ReadonlyMap<string, BlockedNode>;
|
|
3229
3442
|
}
|
|
3230
3443
|
/**
|
|
3231
3444
|
* Shared deterministic dependency scheduler for T1 sections and T2 subtasks.
|
|
@@ -3507,7 +3720,13 @@ declare class DashboardSocket {
|
|
|
3507
3720
|
broadcastToRoom(room: string, event: string, data: unknown): void;
|
|
3508
3721
|
broadcast(event: string, data: unknown): void;
|
|
3509
3722
|
emitCascadeEvent(ev: CascadeEvent): void;
|
|
3510
|
-
|
|
3723
|
+
/**
|
|
3724
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
3725
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
3726
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
3727
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
3728
|
+
*/
|
|
3729
|
+
emitTierStatus(tierId: string, role: string, status: string, sessionId: string, action?: string, extra?: Record<string, unknown>): void;
|
|
3511
3730
|
emitStreamToken(tierId: string, text: string, sessionId: string): void;
|
|
3512
3731
|
emitPeerMessage(event: PeerMessageEvent): void;
|
|
3513
3732
|
emitApprovalRequest(request: PermissionRequest): void;
|