@lchase/yak 0.4.1 → 0.6.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 +3 -1
- package/dist/index.js +387 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ bug) for a guided walkthrough — also live at
|
|
|
42
42
|
rather than failing it. `yak pending` lists every run across the repo
|
|
43
43
|
awaiting a human answer; `yak resume <run-id>` replays and continues;
|
|
44
44
|
`--interactive` prompts through open gates inline instead of exiting to
|
|
45
|
-
resume later.
|
|
45
|
+
resume later. `yak cancel <run-id>` terminates a live run's process tree
|
|
46
|
+
and journals a clean `failed`/`cancelled` terminal state, so any
|
|
47
|
+
out-of-process supervisor has a supported kill.
|
|
46
48
|
- **Worktree isolation:** `yak run --isolation worktree` runs the whole
|
|
47
49
|
workflow inside a fresh git worktree; `map` steps can additionally
|
|
48
50
|
isolate each item into its own sibling worktree.
|
package/dist/index.js
CHANGED
|
@@ -100,6 +100,16 @@ function inputNamesOf(step) {
|
|
|
100
100
|
function dependenciesOf(step, producerOf) {
|
|
101
101
|
return inputNamesOf(step).map((need) => producerOf.get(need)).filter((id) => id !== void 0);
|
|
102
102
|
}
|
|
103
|
+
function earlierProducerIds(step, posIndex, producerOf, order) {
|
|
104
|
+
const deps = [];
|
|
105
|
+
for (const name of inputNamesOf(step)) {
|
|
106
|
+
const producerId = producerOf.get(name);
|
|
107
|
+
if (producerId === void 0) continue;
|
|
108
|
+
const producerPos = order.get(producerId);
|
|
109
|
+
if (producerPos !== void 0 && producerPos < posIndex) deps.push(producerId);
|
|
110
|
+
}
|
|
111
|
+
return deps;
|
|
112
|
+
}
|
|
103
113
|
function flattenSteps(steps) {
|
|
104
114
|
const result = [];
|
|
105
115
|
for (const step of steps) {
|
|
@@ -191,6 +201,7 @@ var rawStepSchema = z2.lazy(
|
|
|
191
201
|
loop: z2.object({
|
|
192
202
|
until: rawExprSchema,
|
|
193
203
|
budget: rawBudgetSchema,
|
|
204
|
+
concurrency: z2.number().int().positive().optional(),
|
|
194
205
|
onExhausted: z2.enum(["suspend", "fail", "continue"]).optional(),
|
|
195
206
|
freshContext: z2.boolean().optional(),
|
|
196
207
|
body: z2.array(rawStepSchema)
|
|
@@ -281,6 +292,7 @@ function normalizeStep(raw) {
|
|
|
281
292
|
body: raw.loop.body.map(normalizeStep),
|
|
282
293
|
until: raw.loop.until,
|
|
283
294
|
budget: raw.loop.budget,
|
|
295
|
+
concurrency: raw.loop.concurrency ?? 1,
|
|
284
296
|
onExhausted: raw.loop.onExhausted ?? "suspend",
|
|
285
297
|
freshContext: raw.loop.freshContext ?? true,
|
|
286
298
|
...raw.produces !== void 0 ? { produces: raw.produces } : {}
|
|
@@ -331,14 +343,76 @@ import { promisify } from "util";
|
|
|
331
343
|
// src/expr/template.ts
|
|
332
344
|
import jexl from "jexl";
|
|
333
345
|
var PLACEHOLDER_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
346
|
+
var IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/y;
|
|
347
|
+
var NOT_A_ROOT = /* @__PURE__ */ new Set(["true", "false", "in"]);
|
|
334
348
|
function extractTemplateRoots(template) {
|
|
335
|
-
const roots =
|
|
349
|
+
const roots = [];
|
|
350
|
+
const seen = /* @__PURE__ */ new Set();
|
|
336
351
|
for (const match of template.matchAll(PLACEHOLDER_RE)) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
for (const ident of identifiersInExpr(match[1])) {
|
|
353
|
+
if (seen.has(ident)) continue;
|
|
354
|
+
seen.add(ident);
|
|
355
|
+
roots.push(ident);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return roots;
|
|
359
|
+
}
|
|
360
|
+
function identifiersInExpr(expr) {
|
|
361
|
+
const found = [];
|
|
362
|
+
let prev = "other";
|
|
363
|
+
let i = 0;
|
|
364
|
+
while (i < expr.length) {
|
|
365
|
+
const c = expr[i];
|
|
366
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
367
|
+
i++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (c === '"' || c === "'") {
|
|
371
|
+
i = skipString(expr, i);
|
|
372
|
+
prev = "other";
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (c === "|" && expr[i + 1] === "|") {
|
|
376
|
+
i += 2;
|
|
377
|
+
prev = "other";
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (c === "|") {
|
|
381
|
+
i++;
|
|
382
|
+
prev = "pipe";
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (c === ".") {
|
|
386
|
+
i++;
|
|
387
|
+
prev = "dot";
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
IDENTIFIER.lastIndex = i;
|
|
391
|
+
const ident = IDENTIFIER.exec(expr);
|
|
392
|
+
if (ident && ident.index === i) {
|
|
393
|
+
const name = ident[0];
|
|
394
|
+
if (prev !== "dot" && prev !== "pipe" && !NOT_A_ROOT.has(name)) found.push(name);
|
|
395
|
+
i += name.length;
|
|
396
|
+
prev = "other";
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
i++;
|
|
400
|
+
prev = "other";
|
|
340
401
|
}
|
|
341
|
-
return
|
|
402
|
+
return found;
|
|
403
|
+
}
|
|
404
|
+
function skipString(expr, start) {
|
|
405
|
+
const quote = expr[start];
|
|
406
|
+
let i = start + 1;
|
|
407
|
+
while (i < expr.length) {
|
|
408
|
+
if (expr[i] === "\\") {
|
|
409
|
+
i += 2;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (expr[i] === quote) return i + 1;
|
|
413
|
+
i++;
|
|
414
|
+
}
|
|
415
|
+
return i;
|
|
342
416
|
}
|
|
343
417
|
async function renderTemplate(template, inputs) {
|
|
344
418
|
let result = "";
|
|
@@ -483,6 +557,7 @@ var KNOWN_AGENT_TOOLS = /* @__PURE__ */ new Set([
|
|
|
483
557
|
async function validateWorkflow(workflow, cwd = process.cwd(), runIsolation = "none") {
|
|
484
558
|
const flatSteps = flattenSteps(workflow.steps);
|
|
485
559
|
checkKnownKinds(flatSteps);
|
|
560
|
+
checkLoopBodyKinds(workflow.steps);
|
|
486
561
|
checkDuplicateIds(flatSteps);
|
|
487
562
|
const producerOf = buildProducerMap(workflow.steps);
|
|
488
563
|
checkReservedArtifactNames(flatSteps);
|
|
@@ -495,6 +570,7 @@ async function validateWorkflow(workflow, cwd = process.cwd(), runIsolation = "n
|
|
|
495
570
|
await checkAgentPromptPlaceholders(flatSteps, cwd);
|
|
496
571
|
checkSessionChainDepth(flatSteps);
|
|
497
572
|
checkMapIsolation(workflow.steps, runIsolation);
|
|
573
|
+
checkLoopParallelWriters(workflow.steps);
|
|
498
574
|
await checkGateSchemaKeys(flatSteps, cwd);
|
|
499
575
|
await checkGateRenderPlaceholders(flatSteps, cwd);
|
|
500
576
|
checkSandboxImage(flatSteps);
|
|
@@ -547,6 +623,83 @@ function checkMapIsolation(steps, runIsolation) {
|
|
|
547
623
|
checkMapIsolation([step.step], runIsolation);
|
|
548
624
|
}
|
|
549
625
|
}
|
|
626
|
+
var UNSUPPORTED_LOOP_BODY_KINDS = /* @__PURE__ */ new Set(["map", "loop", "gate"]);
|
|
627
|
+
function checkLoopBodyKinds(steps) {
|
|
628
|
+
for (const step of steps) {
|
|
629
|
+
if (step.kind === "map") {
|
|
630
|
+
checkLoopBodyKinds([step.step]);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (step.kind !== "loop") continue;
|
|
634
|
+
for (const bodyStep of step.body) {
|
|
635
|
+
if (UNSUPPORTED_LOOP_BODY_KINDS.has(bodyStep.kind)) {
|
|
636
|
+
throw new WorkflowValidationError(
|
|
637
|
+
`step "${bodyStep.id}": kind "${bodyStep.kind}" not supported inside a loop body in M3`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
checkLoopBodyKinds(step.body);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function checkLoopParallelWriters(steps) {
|
|
645
|
+
for (const step of steps) {
|
|
646
|
+
if (step.kind === "loop") {
|
|
647
|
+
warnParallelWriteCapableBody(step);
|
|
648
|
+
checkLoopParallelWriters(step.body);
|
|
649
|
+
} else if (step.kind === "map") {
|
|
650
|
+
checkLoopParallelWriters([step.step]);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function writeCapableTool(step) {
|
|
655
|
+
if (step.kind !== "agent") return void 0;
|
|
656
|
+
return (step.tools ?? []).find((tool) => WRITE_CAPABLE_TOOLS.has(tool));
|
|
657
|
+
}
|
|
658
|
+
function waitsOn(step, targetId, stepsById, producerOf, order) {
|
|
659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
660
|
+
const startPos = order.get(step.id);
|
|
661
|
+
if (startPos === void 0) return false;
|
|
662
|
+
const stack = earlierProducerIds(step, startPos, producerOf, order);
|
|
663
|
+
while (stack.length > 0) {
|
|
664
|
+
const id = stack.pop();
|
|
665
|
+
if (id === void 0 || seen.has(id)) continue;
|
|
666
|
+
if (id === targetId) return true;
|
|
667
|
+
seen.add(id);
|
|
668
|
+
const pred = stepsById.get(id);
|
|
669
|
+
const predPos = order.get(id);
|
|
670
|
+
if (!pred || predPos === void 0) continue;
|
|
671
|
+
stack.push(...earlierProducerIds(pred, predPos, producerOf, order));
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
function warnParallelWriteCapableBody(loop) {
|
|
676
|
+
const concurrency = loop.concurrency ?? 1;
|
|
677
|
+
if (concurrency <= 1 || loop.body.length < 2) return;
|
|
678
|
+
const order = new Map(loop.body.map((bodyStep, index) => [bodyStep.id, index]));
|
|
679
|
+
const stepsById = new Map(loop.body.map((bodyStep) => [bodyStep.id, bodyStep]));
|
|
680
|
+
const producerOf = buildProducerMap(loop.body);
|
|
681
|
+
const warned = /* @__PURE__ */ new Set();
|
|
682
|
+
for (let i = 0; i < loop.body.length; i++) {
|
|
683
|
+
for (let j = i + 1; j < loop.body.length; j++) {
|
|
684
|
+
const earlier = loop.body[i];
|
|
685
|
+
const later = loop.body[j];
|
|
686
|
+
if (!earlier || !later) continue;
|
|
687
|
+
if (waitsOn(later, earlier.id, stepsById, producerOf, order)) continue;
|
|
688
|
+
for (const bodyStep of [earlier, later]) {
|
|
689
|
+
if (warned.has(bodyStep.id)) continue;
|
|
690
|
+
const tool = writeCapableTool(bodyStep);
|
|
691
|
+
const command = bodyStep.kind === "command";
|
|
692
|
+
if (!tool && !command) continue;
|
|
693
|
+
const why = command ? "is a command step" : `declares write-capable tool "${tool}"`;
|
|
694
|
+
const fix = command ? "use concurrency: 1" : "use concurrency: 1 or drop the write-capable tool";
|
|
695
|
+
console.warn(
|
|
696
|
+
`step "${loop.id}": concurrency ${concurrency} runs body steps "${earlier.id}" and "${later.id}" in parallel, and "${bodyStep.id}" ${why} \u2014 concurrent body steps share one cwd and may race; ${fix}`
|
|
697
|
+
);
|
|
698
|
+
warned.add(bodyStep.id);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
550
703
|
function checkAgentToolNames(steps) {
|
|
551
704
|
for (const step of steps) {
|
|
552
705
|
if (step.kind !== "agent") continue;
|
|
@@ -606,17 +759,17 @@ function checkNeedsSatisfied(steps, producerOf) {
|
|
|
606
759
|
function checkNoCycles(steps, producerOf) {
|
|
607
760
|
const stepsById = new Map(steps.map((s) => [s.id, s]));
|
|
608
761
|
const state = /* @__PURE__ */ new Map();
|
|
609
|
-
function visit(step,
|
|
762
|
+
function visit(step, path22) {
|
|
610
763
|
const mark = state.get(step.id);
|
|
611
764
|
if (mark === "done") return;
|
|
612
765
|
if (mark === "visiting") {
|
|
613
|
-
const cycle = [...
|
|
766
|
+
const cycle = [...path22, step.id].join(" -> ");
|
|
614
767
|
throw new WorkflowValidationError(`cycle detected: ${cycle}`);
|
|
615
768
|
}
|
|
616
769
|
state.set(step.id, "visiting");
|
|
617
770
|
for (const depId of dependenciesOf(step, producerOf)) {
|
|
618
771
|
const dep = stepsById.get(depId);
|
|
619
|
-
if (dep) visit(dep, [...
|
|
772
|
+
if (dep) visit(dep, [...path22, step.id]);
|
|
620
773
|
}
|
|
621
774
|
state.set(step.id, "done");
|
|
622
775
|
}
|
|
@@ -1038,7 +1191,7 @@ async function createWorktree(repoRoot, branchName, baseRef, worktreePath) {
|
|
|
1038
1191
|
|
|
1039
1192
|
// src/engine/scheduler.ts
|
|
1040
1193
|
import path14 from "path";
|
|
1041
|
-
import
|
|
1194
|
+
import pLimit3 from "p-limit";
|
|
1042
1195
|
import { z as z8 } from "zod";
|
|
1043
1196
|
|
|
1044
1197
|
// src/adapters/claude-code.ts
|
|
@@ -1534,6 +1687,8 @@ function runCommandStep(step, cwd, extraEnv) {
|
|
|
1534
1687
|
}
|
|
1535
1688
|
|
|
1536
1689
|
// src/steps/loop.ts
|
|
1690
|
+
import assert from "assert/strict";
|
|
1691
|
+
import pLimit from "p-limit";
|
|
1537
1692
|
import { z as z6 } from "zod";
|
|
1538
1693
|
|
|
1539
1694
|
// src/engine/cache.ts
|
|
@@ -1652,11 +1807,28 @@ async function runLoopStep(step, ctx) {
|
|
|
1652
1807
|
return "failed";
|
|
1653
1808
|
}
|
|
1654
1809
|
const resumeFrom = ctx.loopContinuation?.action === "continue" ? await buildResumeFrom(step, ctx) : void 0;
|
|
1655
|
-
const
|
|
1656
|
-
if (status === "ok") {
|
|
1657
|
-
|
|
1810
|
+
const result = await runLoopBody(step, ctx, resumeFrom);
|
|
1811
|
+
if (result.status === "ok") {
|
|
1812
|
+
let artifact;
|
|
1813
|
+
if (step.produces) {
|
|
1814
|
+
const written = await writeArtifact(ctx.runDir, step.produces, result.untilContext, z6.unknown());
|
|
1815
|
+
artifact = { name: step.produces, hash: written.hash };
|
|
1816
|
+
ctx.outerArtifactHashes.set(step.produces, written.hash);
|
|
1817
|
+
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
1818
|
+
t: "artifact.written",
|
|
1819
|
+
name: step.produces,
|
|
1820
|
+
hash: written.hash,
|
|
1821
|
+
bytes: written.bytes
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
1825
|
+
t: "step.completed",
|
|
1826
|
+
stepId: step.id,
|
|
1827
|
+
...artifact ? { artifact: artifact.name, artifactHash: artifact.hash } : {},
|
|
1828
|
+
cached: false
|
|
1829
|
+
});
|
|
1658
1830
|
}
|
|
1659
|
-
return status;
|
|
1831
|
+
return result.status;
|
|
1660
1832
|
}
|
|
1661
1833
|
async function buildResumeFrom(step, ctx) {
|
|
1662
1834
|
const events = await readJournal(ctx.runDir);
|
|
@@ -1692,22 +1864,18 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1692
1864
|
const thisValues = /* @__PURE__ */ new Map();
|
|
1693
1865
|
const thisHashes = /* @__PURE__ */ new Map();
|
|
1694
1866
|
try {
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
freshContext: step.freshContext ?? true,
|
|
1708
|
-
bodySessionIds
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1867
|
+
await runLoopRound({
|
|
1868
|
+
step,
|
|
1869
|
+
iteration,
|
|
1870
|
+
ctx,
|
|
1871
|
+
localProducerOf,
|
|
1872
|
+
bodyOrderIndex,
|
|
1873
|
+
priorValues,
|
|
1874
|
+
priorHashes,
|
|
1875
|
+
thisValues,
|
|
1876
|
+
thisHashes,
|
|
1877
|
+
bodySessionIds
|
|
1878
|
+
});
|
|
1711
1879
|
} catch (err) {
|
|
1712
1880
|
if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
|
|
1713
1881
|
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
@@ -1719,12 +1887,12 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1719
1887
|
recoverable: false
|
|
1720
1888
|
}
|
|
1721
1889
|
});
|
|
1722
|
-
return "failed";
|
|
1890
|
+
return { status: "failed" };
|
|
1723
1891
|
}
|
|
1724
1892
|
throw err;
|
|
1725
1893
|
}
|
|
1726
1894
|
const untilContext = Object.fromEntries(thisValues);
|
|
1727
|
-
if (await evalExpr(step.until, untilContext, ctx.cwd)) return "ok";
|
|
1895
|
+
if (await evalExpr(step.until, untilContext, ctx.cwd)) return { status: "ok", untilContext };
|
|
1728
1896
|
let tripped;
|
|
1729
1897
|
if (step.budget.noProgress) {
|
|
1730
1898
|
lastSignal = await evalExpr(step.budget.noProgress.signal, untilContext, ctx.cwd);
|
|
@@ -1743,7 +1911,10 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1743
1911
|
const tokensTotal = await sumBodyTokens(ctx.runDir, bodyStepIds);
|
|
1744
1912
|
if (tokensTotal >= step.budget.maxTokens) tripped = "maxTokens";
|
|
1745
1913
|
}
|
|
1746
|
-
if (tripped)
|
|
1914
|
+
if (tripped) {
|
|
1915
|
+
const outcome = await handleExhausted(step, ctx, iteration, tripped);
|
|
1916
|
+
return outcome === "ok" ? { status: "ok", untilContext } : { status: outcome };
|
|
1917
|
+
}
|
|
1747
1918
|
priorValues = thisValues;
|
|
1748
1919
|
priorHashes = thisHashes;
|
|
1749
1920
|
}
|
|
@@ -1788,6 +1959,80 @@ async function handleExhausted(step, ctx, iteration, tripped) {
|
|
|
1788
1959
|
await writeLoopExhaustedRequest(ctx.runDir, ctx.runId, step.id, iteration, tripped);
|
|
1789
1960
|
return "suspended";
|
|
1790
1961
|
}
|
|
1962
|
+
async function runLoopRound(args) {
|
|
1963
|
+
const {
|
|
1964
|
+
step,
|
|
1965
|
+
iteration,
|
|
1966
|
+
ctx,
|
|
1967
|
+
localProducerOf,
|
|
1968
|
+
bodyOrderIndex,
|
|
1969
|
+
priorValues,
|
|
1970
|
+
priorHashes,
|
|
1971
|
+
thisValues,
|
|
1972
|
+
thisHashes,
|
|
1973
|
+
bodySessionIds
|
|
1974
|
+
} = args;
|
|
1975
|
+
const cap = Math.min(step.concurrency ?? 1, ctx.globalConcurrency);
|
|
1976
|
+
const limit = pLimit(cap);
|
|
1977
|
+
const remaining = new Set(step.body.map((bodyStep) => bodyStep.id));
|
|
1978
|
+
const completed = /* @__PURE__ */ new Set();
|
|
1979
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1980
|
+
let firstError;
|
|
1981
|
+
const thisRoundDeps = (bodyStep, posIndex) => earlierProducerIds(bodyStep, posIndex, localProducerOf, bodyOrderIndex);
|
|
1982
|
+
const launchEligible = () => {
|
|
1983
|
+
if (firstError !== void 0) return;
|
|
1984
|
+
for (const [posIndex, bodyStep] of step.body.entries()) {
|
|
1985
|
+
if (!remaining.has(bodyStep.id) || inFlight.has(bodyStep.id)) continue;
|
|
1986
|
+
const ready = thisRoundDeps(bodyStep, posIndex).every((dep) => completed.has(dep));
|
|
1987
|
+
if (!ready) {
|
|
1988
|
+
if (cap === 1) break;
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
inFlight.set(
|
|
1992
|
+
bodyStep.id,
|
|
1993
|
+
limit(async () => {
|
|
1994
|
+
if (firstError !== void 0) return "skipped";
|
|
1995
|
+
try {
|
|
1996
|
+
await runOneBodyStep({
|
|
1997
|
+
bodyStep,
|
|
1998
|
+
posIndex,
|
|
1999
|
+
iteration,
|
|
2000
|
+
ctx,
|
|
2001
|
+
localProducerOf,
|
|
2002
|
+
bodyOrderIndex,
|
|
2003
|
+
priorValues,
|
|
2004
|
+
priorHashes,
|
|
2005
|
+
thisValues,
|
|
2006
|
+
thisHashes,
|
|
2007
|
+
freshContext: step.freshContext ?? true,
|
|
2008
|
+
bodySessionIds
|
|
2009
|
+
});
|
|
2010
|
+
return "ok";
|
|
2011
|
+
} catch (err) {
|
|
2012
|
+
if (firstError === void 0) firstError = err;
|
|
2013
|
+
return "error";
|
|
2014
|
+
}
|
|
2015
|
+
})
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
launchEligible();
|
|
2020
|
+
while (inFlight.size > 0) {
|
|
2021
|
+
const [doneId, status] = await Promise.race(
|
|
2022
|
+
[...inFlight.entries()].map(async ([id, p]) => [id, await p])
|
|
2023
|
+
);
|
|
2024
|
+
inFlight.delete(doneId);
|
|
2025
|
+
remaining.delete(doneId);
|
|
2026
|
+
if (status === "ok") completed.add(doneId);
|
|
2027
|
+
launchEligible();
|
|
2028
|
+
}
|
|
2029
|
+
if (firstError !== void 0) throw firstError;
|
|
2030
|
+
assert.equal(
|
|
2031
|
+
remaining.size,
|
|
2032
|
+
0,
|
|
2033
|
+
`loop "${step.id}" stalled: body step(s) [${[...remaining].join(", ")}] never became eligible`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
1791
2036
|
async function runOneBodyStep(args) {
|
|
1792
2037
|
const {
|
|
1793
2038
|
bodyStep,
|
|
@@ -1903,7 +2148,7 @@ async function runBodyStepKind(bodyStep, inputs, ctx, iteration, freshContext, b
|
|
|
1903
2148
|
|
|
1904
2149
|
// src/steps/map.ts
|
|
1905
2150
|
import path13 from "path";
|
|
1906
|
-
import
|
|
2151
|
+
import pLimit2 from "p-limit";
|
|
1907
2152
|
import { z as z7 } from "zod";
|
|
1908
2153
|
var RETRY_ATTEMPTS = 2;
|
|
1909
2154
|
async function runMapStep(step, ctx) {
|
|
@@ -1917,7 +2162,7 @@ async function runMapStep(step, ctx) {
|
|
|
1917
2162
|
await commitAllIfDirty(ctx.cwd, `yak: checkpoint before map ${step.id}`);
|
|
1918
2163
|
}
|
|
1919
2164
|
const items = await readArtifactRaw(ctx.runDir, step.over);
|
|
1920
|
-
const limit =
|
|
2165
|
+
const limit = pLimit2(Math.min(step.concurrency ?? 4, ctx.globalConcurrency));
|
|
1921
2166
|
const results = new Array(items.length);
|
|
1922
2167
|
let anyFailed = false;
|
|
1923
2168
|
await Promise.all(
|
|
@@ -2122,7 +2367,7 @@ function trustResumedSteps(remaining, producerOf, resumeState, completed, artifa
|
|
|
2122
2367
|
}
|
|
2123
2368
|
}
|
|
2124
2369
|
async function runEligibleSteps(workflow, ctx, resumeState) {
|
|
2125
|
-
const limit =
|
|
2370
|
+
const limit = pLimit3(ctx.concurrency ?? DEFAULT_CONCURRENCY);
|
|
2126
2371
|
const producerOf = buildProducerMap(workflow.steps);
|
|
2127
2372
|
const remaining = new Map(workflow.steps.map((s) => [s.id, s]));
|
|
2128
2373
|
const completed = /* @__PURE__ */ new Set();
|
|
@@ -2188,6 +2433,7 @@ async function runStep(step, ctx, artifactHashes, workflowName, agentSessionIds)
|
|
|
2188
2433
|
semanticKey: semanticKey2,
|
|
2189
2434
|
definitionKey: definitionKey2,
|
|
2190
2435
|
outerArtifactHashes: artifactHashes,
|
|
2436
|
+
globalConcurrency: ctx.concurrency ?? DEFAULT_CONCURRENCY,
|
|
2191
2437
|
buildAdapter: (step2, iteration) => buildAdapter(ctx.adapter, ctx, workflowName, step2, iteration),
|
|
2192
2438
|
loopContinuation: ctx.loopContinuations?.get(step.id)
|
|
2193
2439
|
});
|
|
@@ -2398,6 +2644,7 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2398
2644
|
inputHash: inputWritten?.hash ?? sha256(JSON.stringify(workflow)),
|
|
2399
2645
|
adapter,
|
|
2400
2646
|
isolation,
|
|
2647
|
+
pid: process.pid,
|
|
2401
2648
|
...opts.tag !== void 0 ? { tag: opts.tag } : {}
|
|
2402
2649
|
});
|
|
2403
2650
|
if (inputWritten) {
|
|
@@ -2486,6 +2733,7 @@ async function resumeRun(runId, opts = {}) {
|
|
|
2486
2733
|
const { runsDir, cwd, cacheDir } = resolveDirs(opts.runsDir, opts.cwd, opts.cacheDir);
|
|
2487
2734
|
const runDir = path15.join(runsDir, runId);
|
|
2488
2735
|
const workflow = await readRunWorkflow(runDir);
|
|
2736
|
+
await appendJournalEvent(runDir, runId, { t: "run.resumed", pid: process.pid });
|
|
2489
2737
|
let events = await readJournal(runDir);
|
|
2490
2738
|
const loopContinuations = await resolveOpenRequests(runId, runDir, workflow, cwd, events);
|
|
2491
2739
|
events = await readJournal(runDir);
|
|
@@ -2549,6 +2797,99 @@ async function artifactsCommand(runId, opts = {}) {
|
|
|
2549
2797
|
return 0;
|
|
2550
2798
|
}
|
|
2551
2799
|
|
|
2800
|
+
// src/engine/cancel.ts
|
|
2801
|
+
import path17 from "path";
|
|
2802
|
+
var SIGKILL_GRACE_MS = 5e3;
|
|
2803
|
+
var LIVENESS_POLL_MS = 100;
|
|
2804
|
+
var LIVENESS_POLLS = Math.ceil(SIGKILL_GRACE_MS / LIVENESS_POLL_MS);
|
|
2805
|
+
var realSignal = (pid, signal) => {
|
|
2806
|
+
process.kill(pid, signal);
|
|
2807
|
+
};
|
|
2808
|
+
function livePidFromJournal(events) {
|
|
2809
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2810
|
+
const event = events[i];
|
|
2811
|
+
if (event?.t === "run.resumed") return event.pid;
|
|
2812
|
+
}
|
|
2813
|
+
const started = events.find((e) => e.t === "run.started");
|
|
2814
|
+
return started?.t === "run.started" ? started.pid : void 0;
|
|
2815
|
+
}
|
|
2816
|
+
function isAlive(signal, pid) {
|
|
2817
|
+
try {
|
|
2818
|
+
signal(pid, 0);
|
|
2819
|
+
return true;
|
|
2820
|
+
} catch (err) {
|
|
2821
|
+
return err.code === "EPERM";
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
async function terminateProcessTree(signal, sleep, pid) {
|
|
2825
|
+
if (!isAlive(signal, pid)) return "not-running";
|
|
2826
|
+
try {
|
|
2827
|
+
signal(-pid, "SIGTERM");
|
|
2828
|
+
} catch {
|
|
2829
|
+
try {
|
|
2830
|
+
signal(pid, "SIGTERM");
|
|
2831
|
+
} catch {
|
|
2832
|
+
return "sigterm";
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
for (let poll = 0; poll < LIVENESS_POLLS; poll++) {
|
|
2836
|
+
if (!isAlive(signal, pid)) return "sigterm";
|
|
2837
|
+
await sleep(LIVENESS_POLL_MS);
|
|
2838
|
+
}
|
|
2839
|
+
if (!isAlive(signal, pid)) return "sigterm";
|
|
2840
|
+
try {
|
|
2841
|
+
signal(-pid, "SIGKILL");
|
|
2842
|
+
} catch {
|
|
2843
|
+
try {
|
|
2844
|
+
signal(pid, "SIGKILL");
|
|
2845
|
+
} catch {
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return "sigkill";
|
|
2849
|
+
}
|
|
2850
|
+
async function cancelRun(runId, opts = {}) {
|
|
2851
|
+
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2852
|
+
const signal = opts.signal ?? realSignal;
|
|
2853
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2854
|
+
const runDir = path17.join(runsDir, runId);
|
|
2855
|
+
const events = await readJournal(runDir);
|
|
2856
|
+
if (!events.some((e) => e.t === "run.started")) return { status: "not-found", runId };
|
|
2857
|
+
const last = events.at(-1);
|
|
2858
|
+
if (last?.t === "run.finished") {
|
|
2859
|
+
return { status: "already-terminal", runId, finishedStatus: last.status };
|
|
2860
|
+
}
|
|
2861
|
+
const pid = livePidFromJournal(events);
|
|
2862
|
+
const signalled = pid === void 0 ? "no-pid" : await terminateProcessTree(signal, sleep, pid);
|
|
2863
|
+
const settled = (await readJournal(runDir)).at(-1);
|
|
2864
|
+
if (settled?.t === "run.finished") {
|
|
2865
|
+
return { status: "already-terminal", runId, finishedStatus: settled.status };
|
|
2866
|
+
}
|
|
2867
|
+
await appendJournalEvent(runDir, runId, {
|
|
2868
|
+
t: "run.finished",
|
|
2869
|
+
status: "failed",
|
|
2870
|
+
reason: "cancelled"
|
|
2871
|
+
});
|
|
2872
|
+
return { status: "cancelled", runId, signalled };
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// src/cli/commands/cancel.ts
|
|
2876
|
+
async function cancelCommand(runId, opts = {}) {
|
|
2877
|
+
const outcome = await cancelRun(runId, { runsDir: opts.runsDir });
|
|
2878
|
+
switch (outcome.status) {
|
|
2879
|
+
case "not-found":
|
|
2880
|
+
console.error(`run ${runId} not found`);
|
|
2881
|
+
return 1;
|
|
2882
|
+
case "already-terminal":
|
|
2883
|
+
console.log(`run ${runId} already finished: ${outcome.finishedStatus} \u2014 nothing to cancel`);
|
|
2884
|
+
return 0;
|
|
2885
|
+
case "cancelled": {
|
|
2886
|
+
const note = outcome.signalled === "no-pid" ? " (no pid recorded \u2014 marked cancelled without signalling)" : outcome.signalled === "not-running" ? " (process already gone \u2014 marked cancelled)" : outcome.signalled === "sigkill" ? " (SIGKILL after grace period)" : "";
|
|
2887
|
+
console.log(`run ${runId} cancelled${note}`);
|
|
2888
|
+
return 0;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2552
2893
|
// src/cli/commands/graph.ts
|
|
2553
2894
|
async function graphCommand(workflowPath) {
|
|
2554
2895
|
const raw = await loadWorkflowYaml(workflowPath);
|
|
@@ -2559,13 +2900,13 @@ async function graphCommand(workflowPath) {
|
|
|
2559
2900
|
|
|
2560
2901
|
// src/cli/commands/pending.ts
|
|
2561
2902
|
import { readdir as readdir3 } from "fs/promises";
|
|
2562
|
-
import
|
|
2903
|
+
import path18 from "path";
|
|
2563
2904
|
async function pendingCommand(opts = {}) {
|
|
2564
2905
|
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2565
2906
|
const runIds = (await readdir3(runsDir).catch(() => [])).sort();
|
|
2566
2907
|
let anyPending = false;
|
|
2567
2908
|
for (const runId of runIds) {
|
|
2568
|
-
const runDir =
|
|
2909
|
+
const runDir = path18.join(runsDir, runId);
|
|
2569
2910
|
const events = await readJournal(runDir);
|
|
2570
2911
|
const last = events.at(-1);
|
|
2571
2912
|
if (!last || last.t !== "run.finished" || last.status !== "suspended") continue;
|
|
@@ -2669,7 +3010,7 @@ async function resumeCommand(runId, adapter) {
|
|
|
2669
3010
|
}
|
|
2670
3011
|
|
|
2671
3012
|
// src/cli/commands/run.ts
|
|
2672
|
-
import
|
|
3013
|
+
import path19 from "path";
|
|
2673
3014
|
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
2674
3015
|
|
|
2675
3016
|
// src/cli/interactive.ts
|
|
@@ -2799,7 +3140,7 @@ ${text2}
|
|
|
2799
3140
|
async function runCommand(workflowPath, opts = {}) {
|
|
2800
3141
|
let result;
|
|
2801
3142
|
try {
|
|
2802
|
-
result = await executeWorkflowFile(
|
|
3143
|
+
result = await executeWorkflowFile(path19.resolve(workflowPath), {
|
|
2803
3144
|
adapter: opts.adapter,
|
|
2804
3145
|
isolation: opts.isolation,
|
|
2805
3146
|
input: opts.input,
|
|
@@ -2839,7 +3180,7 @@ async function runCommand(workflowPath, opts = {}) {
|
|
|
2839
3180
|
}
|
|
2840
3181
|
|
|
2841
3182
|
// src/cli/commands/status.ts
|
|
2842
|
-
import
|
|
3183
|
+
import path20 from "path";
|
|
2843
3184
|
async function statusCommand(runId, opts = {}) {
|
|
2844
3185
|
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2845
3186
|
const resolvedRunId = runId ?? await findLatestRunId(runsDir);
|
|
@@ -2847,7 +3188,7 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2847
3188
|
console.error("no runs found");
|
|
2848
3189
|
return 1;
|
|
2849
3190
|
}
|
|
2850
|
-
const runDir =
|
|
3191
|
+
const runDir = path20.join(runsDir, resolvedRunId);
|
|
2851
3192
|
const workflow = await readRunWorkflow(runDir).catch(() => void 0);
|
|
2852
3193
|
if (!workflow) {
|
|
2853
3194
|
console.error(`run ${resolvedRunId} not found`);
|
|
@@ -2873,7 +3214,7 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2873
3214
|
}
|
|
2874
3215
|
|
|
2875
3216
|
// src/cli/commands/watch.ts
|
|
2876
|
-
import
|
|
3217
|
+
import path21 from "path";
|
|
2877
3218
|
import { render } from "ink";
|
|
2878
3219
|
import React2 from "react";
|
|
2879
3220
|
|
|
@@ -2933,7 +3274,7 @@ async function watchCommand(runId, opts = {}) {
|
|
|
2933
3274
|
console.error("no runs found");
|
|
2934
3275
|
return 1;
|
|
2935
3276
|
}
|
|
2936
|
-
const runDir =
|
|
3277
|
+
const runDir = path21.join(runsDir, resolvedRunId);
|
|
2937
3278
|
const workflow = await readRunWorkflow(runDir).catch(() => void 0);
|
|
2938
3279
|
if (!workflow) {
|
|
2939
3280
|
console.error(`run ${resolvedRunId} not found`);
|
|
@@ -3010,6 +3351,9 @@ program.command("run").description("Run a workflow YAML file").argument("<workfl
|
|
|
3010
3351
|
program.command("resume").description("Resume an interrupted run").argument("<run-id>", "id of the run to resume").option("--adapter <adapter>", "must match the adapter the run started with, if given").action(async (runId, options) => {
|
|
3011
3352
|
process.exitCode = await resumeCommand(runId, options.adapter ? requireAdapterId(options.adapter) : void 0);
|
|
3012
3353
|
});
|
|
3354
|
+
program.command("cancel").description("Terminate a live run and journal a clean 'cancelled' terminal state").argument("<run-id>", "id of the run to cancel").action(async (runId) => {
|
|
3355
|
+
process.exitCode = await cancelCommand(runId);
|
|
3356
|
+
});
|
|
3013
3357
|
program.command("status").description("Report a run's per-step status").argument("[run-id]", "id of the run to report on (default: most recent)").action(async (runId) => {
|
|
3014
3358
|
process.exitCode = await statusCommand(runId);
|
|
3015
3359
|
});
|