@oisincoveney/pipeline 3.11.13 → 3.11.15

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.
@@ -509,8 +509,8 @@ declare const configSchema: z.ZodObject<{
509
509
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
510
510
  description: z.ZodOptional<z.ZodString>;
511
511
  baseline: z.ZodEnum<{
512
- execute: "execute";
513
512
  quick: "quick";
513
+ execute: "execute";
514
514
  }>;
515
515
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
516
516
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -1,27 +1,37 @@
1
1
  //#region src/model-resolver.ts
2
2
  const DISABLED_MODELS_ENV = "PIPELINE_DISABLED_MODELS";
3
- function selectNodeModel(node, options) {
4
- return fallbackModelSelection(node.models ?? [], options);
5
- }
6
- function fallbackModelSelection(models, options) {
3
+ function selectNodeModelCandidates(node, options) {
4
+ const models = node.models ?? [];
7
5
  if (models.length === 0) return {
6
+ models: [],
8
7
  reason: "node declares no model fallback array",
9
8
  skipped: []
10
9
  };
11
10
  const disabled = disabledModels();
12
11
  const available = options?.available;
13
12
  const enabled = models.filter((candidate) => !disabled.has(candidate) && isAvailable(candidate, available));
14
- const skipped = models.filter((candidate) => disabled.has(candidate) || !isAvailable(candidate, available));
13
+ const baseSkipped = models.filter((candidate) => disabled.has(candidate) || !isAvailable(candidate, available));
15
14
  const sizing = sizingFromOptions(options);
16
- if (!sizing) {
17
- const model = enabled[0];
18
- return {
19
- model,
20
- reason: selectionReason(model),
21
- skipped
22
- };
23
- }
24
- return sizedSelection(enabled, skipped, sizing);
15
+ if (!sizing) return {
16
+ models: enabled,
17
+ reason: selectionReason(enabled[0]),
18
+ skipped: baseSkipped
19
+ };
20
+ return sizedCandidates(enabled, baseSkipped, sizing);
21
+ }
22
+ function sizedCandidates(enabled, baseSkipped, options) {
23
+ const { estimatedTokens, budget } = options;
24
+ const required = estimatedTokens / (budget.max_context_pct / 100);
25
+ const fits = [];
26
+ const tooSmall = [];
27
+ for (const candidate of enabled) if ((budget.model_context_windows[candidate] ?? budget.default_context_window) >= required) fits.push(candidate);
28
+ else tooSmall.push(candidate);
29
+ const head = fits[0];
30
+ return {
31
+ models: fits,
32
+ reason: head ? `selected '${head}' (window ${budget.model_context_windows[head] ?? budget.default_context_window}) — holds estimated ${estimatedTokens} tokens within the ${budget.max_context_pct}% context cap` : `estimated context ${estimatedTokens} tokens exceeds ${budget.max_context_pct}% of every available model window`,
33
+ skipped: [...baseSkipped, ...tooSmall]
34
+ };
25
35
  }
26
36
  function isAvailable(candidate, available) {
27
37
  return available ? available.has(candidate) : true;
@@ -32,24 +42,6 @@ function sizingFromOptions(options) {
32
42
  estimatedTokens: options.estimatedTokens
33
43
  };
34
44
  }
35
- function sizedSelection(enabled, disabledSkipped, options) {
36
- const { estimatedTokens, budget } = options;
37
- const required = estimatedTokens / (budget.max_context_pct / 100);
38
- const tooSmall = [];
39
- for (const candidate of enabled) {
40
- const window = budget.model_context_windows[candidate] ?? budget.default_context_window;
41
- if (window >= required) return {
42
- model: candidate,
43
- reason: `selected '${candidate}' (window ${window}) — holds estimated ${estimatedTokens} tokens within the ${budget.max_context_pct}% context cap`,
44
- skipped: [...disabledSkipped, ...tooSmall]
45
- };
46
- tooSmall.push(candidate);
47
- }
48
- return {
49
- reason: `estimated context ${estimatedTokens} tokens exceeds ${budget.max_context_pct}% of every available model window`,
50
- skipped: [...disabledSkipped, ...tooSmall]
51
- };
52
- }
53
45
  function selectionReason(model) {
54
46
  if (model) return "selected first enabled model from node fallback array";
55
47
  return `all configured node models are disabled by ${DISABLED_MODELS_ENV}`;
@@ -58,4 +50,4 @@ function disabledModels() {
58
50
  return new Set((process.env[DISABLED_MODELS_ENV] ?? "").split(",").map((value) => value.trim()).filter(Boolean));
59
51
  }
60
52
  //#endregion
61
- export { selectNodeModel };
53
+ export { selectNodeModelCandidates };
@@ -5,13 +5,13 @@ import { z } from "zod";
5
5
  //#region src/moka-submit.d.ts
6
6
  declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
7
7
  "workflow.start": "workflow.start";
8
+ "node.finish": "node.finish";
9
+ "node.start": "node.start";
8
10
  "workflow.success": "workflow.success";
9
11
  "workflow.failure": "workflow.failure";
10
12
  "workflow.complete": "workflow.complete";
11
- "node.start": "node.start";
12
13
  "node.success": "node.success";
13
14
  "node.error": "node.error";
14
- "node.finish": "node.finish";
15
15
  "gate.failure": "gate.failure";
16
16
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
17
17
  failure: z.ZodDefault<z.ZodEnum<{
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
94
94
  }, z.core.$strict>>;
95
95
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
96
96
  "workflow.start": "workflow.start";
97
+ "node.finish": "node.finish";
98
+ "node.start": "node.start";
97
99
  "workflow.success": "workflow.success";
98
100
  "workflow.failure": "workflow.failure";
99
101
  "workflow.complete": "workflow.complete";
100
- "node.start": "node.start";
101
102
  "node.success": "node.success";
102
103
  "node.error": "node.error";
103
- "node.finish": "node.finish";
104
104
  "gate.failure": "gate.failure";
105
105
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
106
106
  failure: z.ZodDefault<z.ZodEnum<{
@@ -161,8 +161,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
161
161
  }, z.core.$strict>>;
162
162
  serviceAccountName: z.ZodOptional<z.ZodString>;
163
163
  mode: z.ZodEnum<{
164
- quick: "quick";
165
164
  full: "full";
165
+ quick: "quick";
166
166
  }>;
167
167
  schedulePath: z.ZodOptional<z.ZodString>;
168
168
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -207,13 +207,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
207
207
  }, z.core.$strict>>;
208
208
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
209
209
  "workflow.start": "workflow.start";
210
+ "node.finish": "node.finish";
211
+ "node.start": "node.start";
210
212
  "workflow.success": "workflow.success";
211
213
  "workflow.failure": "workflow.failure";
212
214
  "workflow.complete": "workflow.complete";
213
- "node.start": "node.start";
214
215
  "node.success": "node.success";
215
216
  "node.error": "node.error";
216
- "node.finish": "node.finish";
217
217
  "gate.failure": "gate.failure";
218
218
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
219
219
  failure: z.ZodDefault<z.ZodEnum<{
@@ -109,6 +109,9 @@ function promoteFinalRefEffect(input) {
109
109
  function headSha(worktreePath) {
110
110
  return Effect.map(runGit(worktreePath, ["rev-parse", "HEAD"]), (sha) => sha.trim());
111
111
  }
112
+ function runnerCommitMessage(nodeId) {
113
+ return `chore(pipeline): ${nodeId}`;
114
+ }
112
115
  function commitChangesIfNeeded(worktreePath, nodeId, committer) {
113
116
  return Effect.gen(function* () {
114
117
  if ((yield* runGit(worktreePath, [
@@ -121,7 +124,7 @@ function commitChangesIfNeeded(worktreePath, nodeId, committer) {
121
124
  yield* runGit(worktreePath, [
122
125
  "commit",
123
126
  "-m",
124
- `pipeline: ${nodeId}`
127
+ runnerCommitMessage(nodeId)
125
128
  ]);
126
129
  });
127
130
  }
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- quick: "quick";
47
46
  full: "full";
47
+ quick: "quick";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- quick: "quick";
108
107
  full: "full";
108
+ quick: "quick";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;
@@ -11,8 +11,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
11
11
  runId: z.ZodString;
12
12
  sequence: z.ZodNumber;
13
13
  type: z.ZodEnum<{
14
- "workflow.start": "workflow.start";
15
14
  "workflow.planned": "workflow.planned";
15
+ "workflow.start": "workflow.start";
16
16
  }>;
17
17
  workflowPlan: z.ZodObject<{
18
18
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -58,10 +58,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
58
58
  }>;
59
59
  }, z.core.$strip>;
60
60
  type: z.ZodEnum<{
61
- "node.start": "node.start";
62
- "node.finish": "node.finish";
63
61
  "agent.finish": "agent.finish";
64
62
  "agent.start": "agent.start";
63
+ "node.finish": "node.finish";
64
+ "node.start": "node.start";
65
65
  }>;
66
66
  }, z.core.$strip>, z.ZodObject<{
67
67
  at: z.ZodOptional<z.ZodString>;
@@ -189,8 +189,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
189
189
  runId: z.ZodString;
190
190
  sequence: z.ZodNumber;
191
191
  type: z.ZodEnum<{
192
- "workflow.start": "workflow.start";
193
192
  "workflow.planned": "workflow.planned";
193
+ "workflow.start": "workflow.start";
194
194
  }>;
195
195
  workflowPlan: z.ZodObject<{
196
196
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -236,10 +236,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
236
236
  }>;
237
237
  }, z.core.$strip>;
238
238
  type: z.ZodEnum<{
239
- "node.start": "node.start";
240
- "node.finish": "node.finish";
241
239
  "agent.finish": "agent.finish";
242
240
  "agent.start": "agent.start";
241
+ "node.finish": "node.finish";
242
+ "node.start": "node.start";
243
243
  }>;
244
244
  }, z.core.$strip>, z.ZodObject<{
245
245
  at: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import { resolvePackageAssetPath } from "../../package-assets.js";
2
2
  import { resolveFileReference } from "../../path-refs.js";
3
3
  import { gatewayServerForProfile } from "../../mcp/gateway.js";
4
- import { selectNodeModel } from "../../model-resolver.js";
4
+ import { selectNodeModelCandidates } from "../../model-resolver.js";
5
5
  import { createRunnerLaunchPlan } from "../../runner.js";
6
6
  import { normalizeRunnerOutput, runnerTextCandidates } from "../../runner-output.js";
7
7
  import { estimateTokens } from "../../token-estimator.js";
@@ -29,17 +29,72 @@ function executeAgentNodeEffect(node, context, attempt) {
29
29
  if (decision.overBudget) return {
30
30
  evidence: [
31
31
  `agent boundary node=${node.id} profile=${node.profile}`,
32
- `over token budget: ${decision.selection.reason}`,
33
- ...decision.selection.skipped.length ? [`model fallbacks skipped: ${decision.selection.skipped.join(", ")}`] : []
32
+ `over token budget: ${decision.reason}`,
33
+ ...decision.skipped.length ? [`model fallbacks skipped: ${decision.skipped.join(", ")}`] : []
34
34
  ],
35
35
  exitCode: 1,
36
36
  output: ""
37
37
  };
38
- const modelSelection = decision.selection;
38
+ const profileId = node.profile;
39
+ const fallbackEvidence = [];
40
+ const lastIndex = decision.candidates.length - 1;
41
+ for (let index = 0; index < lastIndex; index += 1) {
42
+ const model = decision.candidates[index];
43
+ const attemptOutcome = yield* runModelAttemptEffect({
44
+ attempt,
45
+ context,
46
+ model,
47
+ node,
48
+ profileId,
49
+ prompt
50
+ });
51
+ if (attemptOutcome.result.exitCode !== 70) return yield* buildAgentAttemptResultEffect({
52
+ attempt,
53
+ context,
54
+ decision,
55
+ fallbackEvidence,
56
+ model,
57
+ node,
58
+ outcome: attemptOutcome,
59
+ profileId
60
+ });
61
+ fallbackEvidence.push(fallbackNote(model, decision.candidates[index + 1], attemptOutcome.result));
62
+ }
63
+ const lastModel = decision.candidates[lastIndex];
64
+ return yield* buildAgentAttemptResultEffect({
65
+ attempt,
66
+ context,
67
+ decision,
68
+ fallbackEvidence,
69
+ model: lastModel,
70
+ node,
71
+ outcome: yield* runModelAttemptEffect({
72
+ attempt,
73
+ context,
74
+ model: lastModel,
75
+ node,
76
+ profileId,
77
+ prompt
78
+ }),
79
+ profileId
80
+ });
81
+ });
82
+ }
83
+ function modelLabel(model) {
84
+ return model ?? "profile/default";
85
+ }
86
+ function fallbackNote(failed, next, result) {
87
+ const detail = result.stderr ? `: ${result.stderr}` : "";
88
+ return `model ${modelLabel(failed)} failed (infra exit ${result.exitCode}${detail}); falling back to ${modelLabel(next)}`;
89
+ }
90
+ function runModelAttemptEffect(inputs) {
91
+ return Effect.gen(function* () {
92
+ const { attempt, context, model, node, profileId, prompt } = inputs;
93
+ const service = yield* AgentNodeRuntimeService;
39
94
  const plan = createRunnerLaunchPlan(context.config, {
40
- model: modelSelection.model,
95
+ model,
41
96
  nodeId: node.id,
42
- profileId: node.profile,
97
+ profileId,
43
98
  prompt,
44
99
  reasoningEffort: node.reasoning_effort,
45
100
  worktreePath: context.worktreePath
@@ -47,12 +102,22 @@ function executeAgentNodeEffect(node, context, attempt) {
47
102
  if (node.timeoutMs) plan.timeoutMs = node.timeoutMs;
48
103
  context.agentInvocations.push(plan);
49
104
  emitAgentStart(context, plan, attempt);
50
- const result = yield* (yield* AgentNodeRuntimeService).executeRunner(context.executor, plan, {
105
+ const result = yield* service.executeRunner(context.executor, plan, {
51
106
  onOutput: agentOutputRecorder(context, node, attempt),
52
107
  signal: context.signal
53
108
  });
54
109
  emitAgentFinish(context, plan, attempt, result);
55
110
  if (result.sessionId) context.nodeStateStore.recordSessionId(node.id, result.sessionId);
111
+ return {
112
+ plan,
113
+ result
114
+ };
115
+ });
116
+ }
117
+ function buildAgentAttemptResultEffect(inputs) {
118
+ return Effect.gen(function* () {
119
+ const { attempt, context, decision, fallbackEvidence, model, node, outcome, profileId } = inputs;
120
+ const { plan, result } = outcome;
56
121
  const finalized = yield* finalizeAgentOutputEffect({
57
122
  context,
58
123
  node,
@@ -64,10 +129,11 @@ function executeAgentNodeEffect(node, context, attempt) {
64
129
  const handoff = yield* maybeDeriveHandoffEffect(context, node, finalized.output, attempt);
65
130
  return withOptionalHandoff({
66
131
  evidence: [
67
- `agent boundary node=${node.id} profile=${node.profile} runner=${plan.runnerId}`,
132
+ `agent boundary node=${node.id} profile=${profileId} runner=${plan.runnerId}`,
68
133
  `estimated context tokens: ${decision.estimatedTokens}`,
69
- `model selection: ${modelSelection.model ?? "profile/default"} (${modelSelection.reason})`,
70
- ...modelSelection.skipped.length ? [`model fallbacks skipped: ${modelSelection.skipped.join(", ")}`] : [],
134
+ `model selection: ${modelLabel(model)} (${decision.reason})`,
135
+ ...decision.skipped.length ? [`model fallbacks skipped: ${decision.skipped.join(", ")}`] : [],
136
+ ...fallbackEvidence,
71
137
  ...finalized.evidence,
72
138
  ...result.stderr ? [`stderr: ${result.stderr}`] : [],
73
139
  ...result.timedOut ? ["agent timed out"] : []
@@ -151,27 +217,28 @@ function createHandoffFinalizerPlan(context, node, runner, rawOutput) {
151
217
  }
152
218
  /**
153
219
  * Pure model-routing decision for a node: estimate the assembled prompt size and
154
- * pick the smallest fallback model whose window holds it within the context cap.
155
- * A node with no fallback array keeps the legacy (size-unaware) selection. A node
156
- * with a fallback array but no fitting model is `overBudget` the caller fails
157
- * it fast rather than truncating.
220
+ * resolve the ordered fallback set of models whose window holds it within the
221
+ * context cap. A node with no fallback array (or none surviving the filters)
222
+ * falls back to the profile default (`undefined`). A node with a fallback array
223
+ * but no fitting model under budget is `overBudget` — the caller fails it fast
224
+ * rather than truncating.
158
225
  */
159
226
  function decideNodeModel(prompt, node, budget, availableModels) {
160
227
  const estimatedTokens = estimateTokens(prompt);
161
- if (!(budget && node.models?.length)) return {
162
- estimatedTokens,
163
- overBudget: false,
164
- selection: selectNodeModel(node, { available: availableModels })
165
- };
166
- const selection = selectNodeModel(node, {
228
+ const candidates = selectNodeModelCandidates(node, {
167
229
  available: availableModels,
168
- budget,
169
- estimatedTokens
230
+ ...budget ? {
231
+ budget,
232
+ estimatedTokens
233
+ } : {}
170
234
  });
235
+ const overBudget = Boolean(budget && node.models?.length) && candidates.models.length === 0;
171
236
  return {
237
+ candidates: candidates.models.length ? candidates.models : [void 0],
172
238
  estimatedTokens,
173
- overBudget: !selection.model,
174
- selection
239
+ overBudget,
240
+ reason: candidates.reason,
241
+ skipped: candidates.skipped
175
242
  };
176
243
  }
177
244
  function finalizeAgentOutputEffect(inputs) {
@@ -7,15 +7,6 @@ function createOpencodeSessionRegistry() {
7
7
  return { sessions: /* @__PURE__ */ new Map() };
8
8
  }
9
9
  /**
10
- * Distinguish infra failure (server/session error -> retry-eligible exit 70)
11
- * from a normal agent completion (the agent may still have produced a wrong
12
- * answer; gates decide that, exit 0). This mirrors the EXIT_STARTUP convention
13
- * in runner-command/run.ts and feeds retry.ts via the node's retry policy.
14
- */
15
- const EXIT_OK = 0;
16
- const EXIT_AGENT_ERROR = 1;
17
- const EXIT_INFRA = 70;
18
- /**
19
10
  * SDK-backed replacement for the subprocess `runLaunchPlan`. Conforms to the
20
11
  * RuntimeContext.executor seam so agent-node never learns the transport.
21
12
  */
@@ -288,7 +279,7 @@ function successResult(plan, drive) {
288
279
  };
289
280
  return {
290
281
  argv: plan.args,
291
- exitCode: EXIT_OK,
282
+ exitCode: 0,
292
283
  sessionId: drive.sessionId,
293
284
  stdout
294
285
  };
@@ -296,7 +287,7 @@ function successResult(plan, drive) {
296
287
  function failureResult(plan, error) {
297
288
  return {
298
289
  argv: plan.args,
299
- exitCode: EXIT_INFRA,
290
+ exitCode: 70,
300
291
  stderr: `opencode session failed: ${errorMessage(error)}`,
301
292
  stdout: ""
302
293
  };
@@ -309,8 +300,8 @@ function failureResult(plan, error) {
309
300
  function infraErrorExitCode(error) {
310
301
  switch (error.name) {
311
302
  case "MessageOutputLengthError":
312
- case "MessageAbortedError": return EXIT_AGENT_ERROR;
313
- default: return EXIT_INFRA;
303
+ case "MessageAbortedError": return 1;
304
+ default: return 70;
314
305
  }
315
306
  }
316
307
  function describeMessageError(error) {
package/package.json CHANGED
@@ -128,7 +128,7 @@
128
128
  "prepack": "bun run build:cli"
129
129
  },
130
130
  "type": "module",
131
- "version": "3.11.13",
131
+ "version": "3.11.15",
132
132
  "description": "Config-driven multi-agent pipeline runner for repository work",
133
133
  "main": "./dist/index.js",
134
134
  "types": "./dist/index.d.ts",