@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.
Files changed (2) hide show
  1. package/dist/index.js +251 -25
  2. 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 = /* @__PURE__ */ new Set();
349
+ const roots = [];
350
+ const seen = /* @__PURE__ */ new Set();
336
351
  for (const match of template.matchAll(PLACEHOLDER_RE)) {
337
- const expr = match[1].trim();
338
- const root = expr.split(/[.[]/, 1)[0];
339
- if (root) roots.add(root);
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 [...roots];
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 pLimit2 from "p-limit";
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
- for (const [posIndex, bodyStep] of step.body.entries()) {
1713
- await runOneBodyStep({
1714
- bodyStep,
1715
- posIndex,
1716
- iteration,
1717
- ctx,
1718
- localProducerOf,
1719
- bodyOrderIndex,
1720
- priorValues,
1721
- priorHashes,
1722
- thisValues,
1723
- thisHashes,
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 pLimit from "p-limit";
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 = pLimit(Math.min(step.concurrency ?? 4, ctx.globalConcurrency));
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 = pLimit2(ctx.concurrency ?? DEFAULT_CONCURRENCY);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/yak",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "An agentic workflow engine",
5
5
  "license": "MIT",
6
6
  "type": "module",