patchwork-os 1.2.0-beta.2.canary.836 → 1.2.0-beta.2.canary.838

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.
@@ -67,7 +67,7 @@ import { resolveLocalEndpoint, resolveLocalModel } from "./localSettings.js";
67
67
  import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "./migrations/index.js";
68
68
  import { costRouter } from "./pricing/costRouter.js";
69
69
  import { loadPriceTable, costUsd as priceCostUsd, } from "./pricing/priceTable.js";
70
- import { ReplayIncompleteError } from "./replayBoundary.js";
70
+ import { ReplayIncompleteError, } from "./replayBoundary.js";
71
71
  import { resolveRecipePath } from "./resolveRecipePath.js";
72
72
  import { RunBudget } from "./runBudget.js";
73
73
  import { recordAttemptRun } from "./runLedgers.js";
@@ -961,6 +961,9 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
961
961
  // agent step would. It returns `{ value, ok }`; `ok: false` signals a
962
962
  // failed / silent-fail / empty agent response — the caller stops the loop
963
963
  // and treats it as exhausted (we don't re-judge a non-result).
964
+ // The agent step currently executing, for replay refusals raised from the
965
+ // refine-loop / nested-agent helpers below (they do not take a step id).
966
+ let currentAgentStepId;
964
967
  const runAgentText = async (prompt, driver, model, mcpAccess, downshift, providerOptions,
965
968
  // P0-5: carry the reviewed/judge step's opt-in tool sandbox into refine-loop
966
969
  // re-runs so a sandboxed step STAYS sandboxed across revisions/re-judges.
@@ -995,7 +998,7 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
995
998
  ...(deps.agentDisallowedTools?.length && { enforceSandbox: true }),
996
999
  ...(providerOptions && { providerOptions }),
997
1000
  ...(dataPolicy !== undefined && { boundary: { dataPolicy } }),
998
- }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId));
1001
+ }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId, currentAgentStepId));
999
1002
  runBudget.reconcile(
1000
1003
  // Prefer the driver executeAgent actually resolved+ran; fall back to
1001
1004
  // the routed value only when servedBy is absent (non-executeAgent
@@ -1076,7 +1079,7 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
1076
1079
  ...(input.dataPolicy !== undefined && {
1077
1080
  boundary: { dataPolicy: input.dataPolicy },
1078
1081
  }),
1079
- }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId));
1082
+ }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId, currentAgentStepId));
1080
1083
  runBudget.reconcile(agentReturn.servedBy?.driver ?? input.driver ?? "auto", agentReturn.usage, agentReturn.servedBy?.model, {
1081
1084
  inputChars: input.prompt.length,
1082
1085
  outputChars: agentReturn.text.length,
@@ -1317,6 +1320,7 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
1317
1320
  // cap is set; never disturbs injected (unit-test) price tables.
1318
1321
  runBudget.refreshPrices();
1319
1322
  const stepIdForEmit = step.into ?? step.agent?.into ?? `step_${stepsRun}`;
1323
+ currentAgentStepId = stepIdForEmit;
1320
1324
  const stepTs = Date.now();
1321
1325
  stepStartTs.set(stepIdForEmit, stepTs);
1322
1326
  emit("recipe_step_start", {
@@ -1714,6 +1718,12 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
1714
1718
  // capture no `output`, so under replay there is never a mock for
1715
1719
  // one — refuse here rather than let the catch below be the first
1716
1720
  // to hear about it from the seam.
1721
+ // A recorded agent failure replays as that failure (thrown so the
1722
+ // catch below records it exactly as the original halt did).
1723
+ const replayedAgentFailure = deps.mockedFailures?.get(stepId);
1724
+ if (replayedAgentFailure !== undefined) {
1725
+ throw new Error(replayedAgentFailure.error);
1726
+ }
1717
1727
  if (deps.replayOnly === true) {
1718
1728
  throw new ReplayIncompleteError([stepId], "agent step");
1719
1729
  }
@@ -1764,7 +1774,7 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
1764
1774
  ...(agentCfg.data_policy !== undefined && {
1765
1775
  boundary: { dataPolicy: agentCfg.data_policy },
1766
1776
  }),
1767
- }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId));
1777
+ }, buildAgentExecutorDeps(stepDeps, deps, undefined, runTaskId, stepId));
1768
1778
  agentResult = agentReturn.text;
1769
1779
  runBudget.reconcile(
1770
1780
  // Prefer the driver executeAgent actually resolved+ran; the routed
@@ -2007,7 +2017,16 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
2007
2017
  // below (driven by `result`), so a replay shows how the recipe's
2008
2018
  // wiring behaves against captured evidence — only the tool call
2009
2019
  // itself is skipped. See RunnerDeps.mockedOutputs's doc comment.
2010
- if (deps.mockedOutputs?.has(stepId)) {
2020
+ const replayedFailure = deps.mockedFailures?.get(stepId);
2021
+ if (replayedFailure !== undefined) {
2022
+ // The original run recorded this step as FAILED with no output. That
2023
+ // record is the evidence: fail the same way, dispatch nothing, and do
2024
+ // not retry — there is nothing to retry against.
2025
+ thrownError = replayedFailure.error;
2026
+ thrownErrorCode = replayedFailure.errorCode;
2027
+ result = null;
2028
+ }
2029
+ else if (deps.mockedOutputs?.has(stepId)) {
2011
2030
  result = deps.mockedOutputs.get(stepId) ?? null;
2012
2031
  // A replayed http.post response is interpreted exactly as a live one,
2013
2032
  // including a response captured before httpOutcome existed: it reads
@@ -3121,12 +3140,17 @@ function buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride,
3121
3140
  * the `runTaskIdRef` cell on its own deps at dispatch time, by which point
3122
3141
  * the run has started; the flat caller passes its local const directly.
3123
3142
  */
3124
- runTaskId) {
3143
+ runTaskId,
3144
+ /**
3145
+ * The step this dispatch belongs to, so a replay refusal names it. Falls
3146
+ * back to "agent" only when a caller genuinely does not know.
3147
+ */
3148
+ stepId) {
3125
3149
  // Replay boundary (replayBoundary.ts, layer 3) — the single place agent
3126
3150
  // executor deps are built for every dispatch site in this runner, so a
3127
3151
  // replay's StepDeps can never hand a driver to an agent step.
3128
3152
  if (stepDeps.replayOnly === true) {
3129
- throw new ReplayIncompleteError(["agent"], "agent step");
3153
+ throw new ReplayIncompleteError([stepId ?? "agent"], "agent step");
3130
3154
  }
3131
3155
  const claudeCliFn = claudeCodeFnOverride ?? stepDeps.claudeCodeFn;
3132
3156
  return {
@@ -3820,7 +3844,7 @@ recipeName) {
3820
3844
  }, buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride,
3821
3845
  // Read at DISPATCH time, not build time — by now `runChainedRecipe` has
3822
3846
  // computed and published the run's `taskId`.
3823
- runTaskIdRef.current));
3847
+ runTaskIdRef.current, opts?.stepId));
3824
3848
  };
3825
3849
  // ---------------------------------------------------------------------
3826
3850
  // BEGIN A-PR2 EDIT BLOCK — `loadNestedRecipe` jail (dogfood F-04).
@@ -4004,6 +4028,7 @@ export async function dispatchRecipe(recipe, deps, seedContext = {}) {
4004
4028
  runLog: deps.chainedOptions?.runLog,
4005
4029
  activityLog: deps.chainedOptions?.activityLog,
4006
4030
  mockedOutputs: deps.chainedOptions?.mockedOutputs,
4031
+ mockedFailures: deps.chainedOptions?.mockedFailures,
4007
4032
  ...((deps.replayOnly === true ||
4008
4033
  deps.chainedOptions?.replayOnly === true) && { replayOnly: true }),
4009
4034
  taskIdPrefix: deps.chainedOptions?.taskIdPrefix,