taskflow-core 0.2.8 → 0.2.10

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 (56) hide show
  1. package/README.md +53 -12
  2. package/dist/build-info.json +2 -2
  3. package/dist/detached-runner.js +4 -1
  4. package/dist/detached-runner.js.map +1 -1
  5. package/dist/exec/driver.d.ts +6 -0
  6. package/dist/exec/driver.d.ts.map +1 -1
  7. package/dist/exec/driver.js +4 -0
  8. package/dist/exec/driver.js.map +1 -1
  9. package/dist/exec/step-kinds.d.ts.map +1 -1
  10. package/dist/exec/step-kinds.js +10 -3
  11. package/dist/exec/step-kinds.js.map +1 -1
  12. package/dist/exec/step.d.ts +9 -0
  13. package/dist/exec/step.d.ts.map +1 -1
  14. package/dist/exec/step.js +23 -4
  15. package/dist/exec/step.js.map +1 -1
  16. package/dist/final-output.d.ts +6 -5
  17. package/dist/final-output.d.ts.map +1 -1
  18. package/dist/final-output.js +14 -10
  19. package/dist/final-output.js.map +1 -1
  20. package/dist/flowir/compile.d.ts.map +1 -1
  21. package/dist/flowir/compile.js +1 -0
  22. package/dist/flowir/compile.js.map +1 -1
  23. package/dist/flowir/phasefp.d.ts.map +1 -1
  24. package/dist/flowir/phasefp.js +1 -0
  25. package/dist/flowir/phasefp.js.map +1 -1
  26. package/dist/library/search.d.ts.map +1 -1
  27. package/dist/library/search.js +2 -2
  28. package/dist/library/search.js.map +1 -1
  29. package/dist/resources/baseline.d.ts +1 -1
  30. package/dist/resources/baseline.d.ts.map +1 -1
  31. package/dist/resources/baseline.js +1 -1
  32. package/dist/resources/baseline.js.map +1 -1
  33. package/dist/resume.d.ts.map +1 -1
  34. package/dist/resume.js +2 -0
  35. package/dist/resume.js.map +1 -1
  36. package/dist/runner-core.d.ts +17 -1
  37. package/dist/runner-core.d.ts.map +1 -1
  38. package/dist/runner-core.js +33 -12
  39. package/dist/runner-core.js.map +1 -1
  40. package/dist/runtime/phases/script.d.ts +13 -0
  41. package/dist/runtime/phases/script.d.ts.map +1 -1
  42. package/dist/runtime/phases/script.js +25 -1
  43. package/dist/runtime/phases/script.js.map +1 -1
  44. package/dist/runtime.d.ts +17 -5
  45. package/dist/runtime.d.ts.map +1 -1
  46. package/dist/runtime.js +60 -22
  47. package/dist/runtime.js.map +1 -1
  48. package/dist/schema.d.ts +1 -0
  49. package/dist/schema.d.ts.map +1 -1
  50. package/dist/schema.js +4 -0
  51. package/dist/schema.js.map +1 -1
  52. package/dist/store.d.ts +24 -14
  53. package/dist/store.d.ts.map +1 -1
  54. package/dist/store.js +606 -93
  55. package/dist/store.js.map +1 -1
  56. package/package.json +2 -2
package/dist/runtime.js CHANGED
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import * as fs from "node:fs";
14
14
  import { coerceArray, evaluateCondition, interpolate, interpolateValue, safeParse, tryEvaluateCondition } from "./interpolate.js";
15
15
  import { contractViolations } from "./contract.js";
16
- import { isFailed, isTransientError, mapWithConcurrencyLimit, PHASE_TIMEOUT_ABORT_GRACE_MS, sanitizeErrorMessage } from "./runner-core.js";
16
+ import { failedResultDisplayOutput, hasMeaningfulFailedOutput, isFailed, isTransientError, mapWithConcurrencyLimit, PHASE_TIMEOUT_ABORT_GRACE_MS, sanitizeErrorMessage } from "./runner-core.js";
17
17
  /** Default runner used when no host injected one: fail loudly rather than
18
18
  * silently spawn anything (core is host-neutral and cannot spawn pi/codex). */
19
19
  const noRunnerInjected = async (_cwd, _agents, agentName, task) => ({
@@ -105,16 +105,14 @@ function buildInterpolationContext(state, previousOutput, locals, onRead, reflex
105
105
  function resultToPhaseState(id, r, inputHash, parseJson) {
106
106
  const failed = isFailed(r);
107
107
  const attempts = attemptsOf(r);
108
- // For failed phases, embed the error info in the output so downstream
109
- // phases (and the user) can see what went wrong. The raw r.output is
110
- // often a useless placeholder like "(upstream error: subagent failed)".
111
108
  const output = failed
112
- ? r.errorMessage || r.stderr || r.output
109
+ ? failedResultDisplayOutput(r)
113
110
  : r.output;
114
111
  return {
115
112
  id,
116
113
  status: failed ? "failed" : "done",
117
114
  output,
115
+ partialOutput: failed && hasMeaningfulFailedOutput(r) ? true : undefined,
118
116
  json: parseJson && !failed ? safeParse(r.output) : undefined,
119
117
  usage: r.usage,
120
118
  model: r.model,
@@ -304,7 +302,8 @@ function mergePhaseState(id, results, inputHash, parseJson) {
304
302
  // which model produced the merged output.
305
303
  const model = ran.find((r) => r.model !== undefined)?.model;
306
304
  // Combine outputs as a labelled list; also expose a JSON array of outputs.
307
- // For failed items, use the error message instead of the useless placeholder.
305
+ // For failed items, preserve a real partial body; replace only the shared
306
+ // transport placeholder with the concise diagnostic.
308
307
  // Labels are positionally aligned to the ORIGINAL `over` array: we iterate
309
308
  // over ALL results (including budget-skipped, which are filtered to null) and
310
309
  // use `results.length` as N, so item k's label reads `[k/N]` matching its
@@ -316,7 +315,7 @@ function mergePhaseState(id, results, inputHash, parseJson) {
316
315
  if (r.stopReason === "budget-skipped")
317
316
  return null;
318
317
  const label = `### [${i + 1}/${results.length}] ${r.agent}${isFailed(r) ? " (failed)" : ""}`;
319
- const content = isFailed(r) ? (r.errorMessage || r.stderr || r.output) : r.output;
318
+ const content = isFailed(r) ? failedResultDisplayOutput(r) : r.output;
320
319
  return `${label}\n\n${content}`;
321
320
  })
322
321
  .filter((x) => x !== null)
@@ -332,6 +331,7 @@ function mergePhaseState(id, results, inputHash, parseJson) {
332
331
  id,
333
332
  status: anyFailed ? "failed" : "done",
334
333
  output: combinedText,
334
+ partialOutput: ran.some((r) => isFailed(r) && hasMeaningfulFailedOutput(r)) ? true : undefined,
335
335
  json: jsonArray,
336
336
  usage,
337
337
  model,
@@ -600,18 +600,21 @@ function flowTreeUsesCwdBridge(def, loadFlow, seenUses = new Set()) {
600
600
  * after the root binding and cache policy were decided.
601
601
  */
602
602
  function snapshotFlowLoader(deps) {
603
- if (!deps.loadFlow || deps._flowLoaderSnapshot)
603
+ if ((!deps.loadSavedFlow && !deps.loadFlow) || deps._flowLoaderSnapshot)
604
604
  return deps;
605
- const source = deps.loadFlow;
605
+ const source = deps.loadSavedFlow
606
+ ?? ((name) => {
607
+ const def = deps.loadFlow?.(name);
608
+ return def === undefined ? undefined : { def };
609
+ });
606
610
  const snapshot = new Map();
607
- const loadFlow = (name) => {
611
+ const loadSavedFlow = (name) => {
608
612
  let entry = snapshot.get(name);
609
613
  if (!entry) {
610
614
  try {
611
615
  const loaded = source(name);
612
- // Loader-owned objects may be mutated asynchronously. Capability scan
613
- // and execution operate on a detached structured snapshot, never the
614
- // loader's live reference.
616
+ // Definition and provenance are detached and cached as one value, so
617
+ // mutable discovery cannot pair data from one flow with another path.
615
618
  entry = { ok: true, value: loaded === undefined ? undefined : structuredClone(loaded) };
616
619
  }
617
620
  catch (error) {
@@ -623,7 +626,8 @@ function snapshotFlowLoader(deps) {
623
626
  throw entry.error;
624
627
  return entry.value;
625
628
  };
626
- return { ...deps, loadFlow, _flowLoaderSnapshot: snapshot };
629
+ const loadFlow = (name) => loadSavedFlow(name)?.def;
630
+ return { ...deps, loadFlow, loadSavedFlow, _flowLoaderSnapshot: snapshot };
627
631
  }
628
632
  function sameDirectoryIdentity(a, b) {
629
633
  return !!a && !!b && a.canonicalPath === b.canonicalPath && a.device === b.device && a.inode === b.inode;
@@ -2154,7 +2158,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2154
2158
  }
2155
2159
  // script — zero-token shell (spawn/timeout/size caps in runtime/phases/script.ts)
2156
2160
  if (type === "script") {
2157
- const { runScriptCommand, scriptResultToPhaseState, scriptSpawnErrorToPhaseState } = await import("./runtime/phases/script.js");
2161
+ const { resolveScriptCwd, runScriptCommand, scriptResultToPhaseState, scriptSpawnErrorToPhaseState } = await import("./runtime/phases/script.js");
2158
2162
  const cmd = phase.run;
2159
2163
  if (!cmd) {
2160
2164
  return {
@@ -2179,18 +2183,44 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2179
2183
  if (stdinInterp?.missing.length)
2180
2184
  warnUnresolvedRefs(phase.id, stdinInterp.missing);
2181
2185
  const stdinInput = stdinInterp?.text;
2182
- const ck = cacheKeys(cc, [phase.id, JSON.stringify(interpRunText), stdinInput ?? ""]);
2186
+ const reads = readRefs.length ? readRefsToReads(readRefs, state) : undefined;
2187
+ let scriptCwd;
2188
+ const hasExplicitScriptCwd = deps._cwdOverride !== undefined || phase.cwd !== undefined;
2189
+ try {
2190
+ scriptCwd = resolveScriptCwd({
2191
+ executionCwd: effCwd,
2192
+ hasExplicitCwd: hasExplicitScriptCwd,
2193
+ scriptCwd: state.def.scriptCwd,
2194
+ flowSourceFile: state.flowSourceFile,
2195
+ flowSourceDirIdentity: state.flowSourceDirIdentity,
2196
+ });
2197
+ }
2198
+ catch (err) {
2199
+ return scriptSpawnErrorToPhaseState(phase.id, err, {
2200
+ inputHash: hashInput(phase.id, JSON.stringify(interpRunText), stdinInput ?? "", "script-cwd-unresolved"),
2201
+ reads,
2202
+ });
2203
+ }
2204
+ if (deps._cwdBoundary && !isPathWithin(deps._cwdBoundary, scriptCwd)) {
2205
+ return scriptSpawnErrorToPhaseState(phase.id, new Error("TF_CWD_BOUNDARY_ESCAPE: script cwd must remain inside the inherited cwd boundary"), {
2206
+ inputHash: hashInput(phase.id, JSON.stringify(interpRunText), stdinInput ?? "", "script-cwd-boundary-escape"),
2207
+ reads,
2208
+ });
2209
+ }
2210
+ const ck = cacheKeys(cc, [phase.id, JSON.stringify(interpRunText), stdinInput ?? "", `cwd:${scriptCwd}`]);
2183
2211
  const inputHash = ck.key;
2184
2212
  const cached = cachedPhase(cc, ck);
2185
2213
  if (cached)
2186
2214
  return cached;
2187
2215
  const SCRIPT_TIMEOUT_MS = phase.timeout ?? 60_000;
2188
- const reads = readRefs.length ? readRefsToReads(readRefs, state) : undefined;
2189
2216
  try {
2190
2217
  const invoke = () => runScriptCommand({
2191
2218
  interpRunText,
2192
2219
  arrayForm: Array.isArray(cmd),
2193
- cwd: effCwd,
2220
+ cwd: scriptCwd,
2221
+ cwdIdentity: !hasExplicitScriptCwd && state.def.scriptCwd === "flow"
2222
+ ? state.flowSourceDirIdentity
2223
+ : undefined,
2194
2224
  signal: deps.signal,
2195
2225
  stdinInput,
2196
2226
  timeoutMs: SCRIPT_TIMEOUT_MS,
@@ -2420,6 +2450,8 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2420
2450
  }
2421
2451
  }
2422
2452
  let subDef;
2453
+ let subFlowSourceFile;
2454
+ let subFlowSourceDirIdentity;
2423
2455
  let name;
2424
2456
  let recursionKey; // identity used for cache key + recursion guard
2425
2457
  if (type === "expand" && !hasDef) {
@@ -2526,11 +2558,14 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2526
2558
  const useName = phase.use;
2527
2559
  if (!useName)
2528
2560
  return failPhase(phase.id, `flow phase '${phase.id}' requires 'use' or 'def'`);
2529
- if (!deps.loadFlow)
2561
+ if (!deps.loadSavedFlow)
2530
2562
  return failPhase(phase.id, `flow phase '${phase.id}': no sub-flow loader available`);
2531
- subDef = deps.loadFlow(useName);
2532
- if (!subDef)
2563
+ const loaded = deps.loadSavedFlow(useName);
2564
+ if (!loaded)
2533
2565
  return failPhase(phase.id, `flow phase '${phase.id}': saved flow not found: '${useName}'`);
2566
+ subDef = loaded.def;
2567
+ subFlowSourceFile = loaded.filePath;
2568
+ subFlowSourceDirIdentity = loaded.sourceDirIdentity;
2534
2569
  name = useName;
2535
2570
  recursionKey = useName;
2536
2571
  }
@@ -2575,7 +2610,7 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2575
2610
  // Every sub-flow cache identity includes the resolved definition. A saved
2576
2611
  // flow's name alone is insufficient: its contents can change without the
2577
2612
  // parent definition moving.
2578
- const flowIdentity = `${hasDef ? "def" : "flow"}:${name}:${JSON.stringify(subDef)}`;
2613
+ const flowIdentity = `${hasDef ? "def" : "flow"}:${name}:${JSON.stringify(subDef)}:source:${subFlowSourceFile ?? ""}:source-dir:${JSON.stringify(subFlowSourceDirIdentity ?? null)}`;
2579
2614
  const ck = cacheKeys(flowCc, [phase.id, flowIdentity, preRead, JSON.stringify(subArgs)]);
2580
2615
  const inputHash = ck.key;
2581
2616
  const cached = cachedPhase(flowCc, ck);
@@ -2606,6 +2641,8 @@ async function executePhaseInner(phase, state, deps, prior, emitProgress, _retry
2606
2641
  createdAt: Date.now(),
2607
2642
  updatedAt: Date.now(),
2608
2643
  cwd: effCwd,
2644
+ flowSourceFile: subFlowSourceFile,
2645
+ flowSourceDirIdentity: subFlowSourceDirIdentity,
2609
2646
  };
2610
2647
  // B8: pass this flow phase's preRead content to every sub-flow phase by
2611
2648
  // wrapping runTask — sub-phase preRead still gets prepended on top of it.
@@ -3840,6 +3877,7 @@ export async function executeTaskflow(state, deps) {
3840
3877
  verifiers: deps.verifiers,
3841
3878
  requestApproval: deps.requestApproval,
3842
3879
  loadFlow: deps.loadFlow,
3880
+ loadSavedFlow: deps.loadSavedFlow,
3843
3881
  _stack: deps._stack,
3844
3882
  _dynamic: deps._dynamic,
3845
3883
  });