@lchase/yak 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +251 -25
- package/package.json +1 -1
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", "null", "undefined"]);
|
|
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";
|
|
401
|
+
}
|
|
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++;
|
|
340
414
|
}
|
|
341
|
-
return
|
|
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;
|
|
@@ -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
|
|
@@ -1709,22 +1864,18 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1709
1864
|
const thisValues = /* @__PURE__ */ new Map();
|
|
1710
1865
|
const thisHashes = /* @__PURE__ */ new Map();
|
|
1711
1866
|
try {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
freshContext: step.freshContext ?? true,
|
|
1725
|
-
bodySessionIds
|
|
1726
|
-
});
|
|
1727
|
-
}
|
|
1867
|
+
await runLoopRound({
|
|
1868
|
+
step,
|
|
1869
|
+
iteration,
|
|
1870
|
+
ctx,
|
|
1871
|
+
localProducerOf,
|
|
1872
|
+
bodyOrderIndex,
|
|
1873
|
+
priorValues,
|
|
1874
|
+
priorHashes,
|
|
1875
|
+
thisValues,
|
|
1876
|
+
thisHashes,
|
|
1877
|
+
bodySessionIds
|
|
1878
|
+
});
|
|
1728
1879
|
} catch (err) {
|
|
1729
1880
|
if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
|
|
1730
1881
|
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
@@ -1808,6 +1959,80 @@ async function handleExhausted(step, ctx, iteration, tripped) {
|
|
|
1808
1959
|
await writeLoopExhaustedRequest(ctx.runDir, ctx.runId, step.id, iteration, tripped);
|
|
1809
1960
|
return "suspended";
|
|
1810
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
|
+
}
|
|
1811
2036
|
async function runOneBodyStep(args) {
|
|
1812
2037
|
const {
|
|
1813
2038
|
bodyStep,
|
|
@@ -1923,7 +2148,7 @@ async function runBodyStepKind(bodyStep, inputs, ctx, iteration, freshContext, b
|
|
|
1923
2148
|
|
|
1924
2149
|
// src/steps/map.ts
|
|
1925
2150
|
import path13 from "path";
|
|
1926
|
-
import
|
|
2151
|
+
import pLimit2 from "p-limit";
|
|
1927
2152
|
import { z as z7 } from "zod";
|
|
1928
2153
|
var RETRY_ATTEMPTS = 2;
|
|
1929
2154
|
async function runMapStep(step, ctx) {
|
|
@@ -1937,7 +2162,7 @@ async function runMapStep(step, ctx) {
|
|
|
1937
2162
|
await commitAllIfDirty(ctx.cwd, `yak: checkpoint before map ${step.id}`);
|
|
1938
2163
|
}
|
|
1939
2164
|
const items = await readArtifactRaw(ctx.runDir, step.over);
|
|
1940
|
-
const limit =
|
|
2165
|
+
const limit = pLimit2(Math.min(step.concurrency ?? 4, ctx.globalConcurrency));
|
|
1941
2166
|
const results = new Array(items.length);
|
|
1942
2167
|
let anyFailed = false;
|
|
1943
2168
|
await Promise.all(
|
|
@@ -2142,7 +2367,7 @@ function trustResumedSteps(remaining, producerOf, resumeState, completed, artifa
|
|
|
2142
2367
|
}
|
|
2143
2368
|
}
|
|
2144
2369
|
async function runEligibleSteps(workflow, ctx, resumeState) {
|
|
2145
|
-
const limit =
|
|
2370
|
+
const limit = pLimit3(ctx.concurrency ?? DEFAULT_CONCURRENCY);
|
|
2146
2371
|
const producerOf = buildProducerMap(workflow.steps);
|
|
2147
2372
|
const remaining = new Map(workflow.steps.map((s) => [s.id, s]));
|
|
2148
2373
|
const completed = /* @__PURE__ */ new Set();
|
|
@@ -2208,6 +2433,7 @@ async function runStep(step, ctx, artifactHashes, workflowName, agentSessionIds)
|
|
|
2208
2433
|
semanticKey: semanticKey2,
|
|
2209
2434
|
definitionKey: definitionKey2,
|
|
2210
2435
|
outerArtifactHashes: artifactHashes,
|
|
2436
|
+
globalConcurrency: ctx.concurrency ?? DEFAULT_CONCURRENCY,
|
|
2211
2437
|
buildAdapter: (step2, iteration) => buildAdapter(ctx.adapter, ctx, workflowName, step2, iteration),
|
|
2212
2438
|
loopContinuation: ctx.loopContinuations?.get(step.id)
|
|
2213
2439
|
});
|