@oisincoveney/pipeline 2.3.1 → 2.5.0

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.
@@ -0,0 +1,42 @@
1
+ //#region src/schedule/passes/candidates.ts
2
+ /**
3
+ * PIPE-83.7: best-of-N candidate generation. When config.best_of_n is enabled
4
+ * with n > 1, each agent node whose id carries a configured category (e.g.
5
+ * "green") is expanded into a kind:parallel node holding N candidate children
6
+ * (each a full copy with a fresh id and no inter-candidate deps). The wrapper
7
+ * keeps the original id + upstream needs, so downstream consumers and the
8
+ * PIPE-83.9 selector see a single dependency. Default off / n=1 is identity, so
9
+ * generated schedules and the PIPE-57 goldens are unchanged.
10
+ */
11
+ function expandBestOfNCandidates(config, artifact) {
12
+ const bestOfN = config.best_of_n;
13
+ if (!bestOfN?.enabled || bestOfN.n <= 1) return artifact;
14
+ return {
15
+ ...artifact,
16
+ workflows: Object.fromEntries(Object.entries(artifact.workflows).map(([id, workflow]) => [id, {
17
+ ...workflow,
18
+ nodes: workflow.nodes.flatMap((node) => expandNode(node, bestOfN.categories, bestOfN.n))
19
+ }]))
20
+ };
21
+ }
22
+ function expandNode(node, categories, n) {
23
+ if (node.kind !== "agent" || !categories.some((category) => node.id.includes(category))) return [node];
24
+ const candidatesId = `${node.id}--candidates`;
25
+ return [{
26
+ id: candidatesId,
27
+ kind: "parallel",
28
+ nodes: Array.from({ length: n }, (_, index) => ({
29
+ ...node,
30
+ id: `${node.id}--c${index + 1}`,
31
+ needs: []
32
+ })),
33
+ ...node.needs ? { needs: node.needs } : {}
34
+ }, {
35
+ builtin: "select-candidate",
36
+ id: node.id,
37
+ kind: "builtin",
38
+ needs: [candidatesId]
39
+ }];
40
+ }
41
+ //#endregion
42
+ export { expandBestOfNCandidates };
@@ -1,6 +1,7 @@
1
1
  //#region src/schedule/passes/index.ts
2
2
  const SCHEDULE_PASS_ORDER = [
3
3
  "coverage",
4
+ "candidates",
4
5
  "models",
5
6
  "ids",
6
7
  "references"
@@ -25,12 +25,16 @@ function applyNodeCatalogModelsToParallelNode(node, templates) {
25
25
  };
26
26
  }
27
27
  function applyNodeCatalogModelsToAgentNode(node, templates) {
28
- if (node.models?.length) return node;
29
28
  const template = nodeCatalogTemplateFor(node, templates);
30
- return template ? {
29
+ if (!template) return node;
30
+ return {
31
31
  ...node,
32
- models: template.models
33
- } : node;
32
+ category: node.category ?? template.category,
33
+ models: nodeModelsOrCatalog(node, template)
34
+ };
35
+ }
36
+ function nodeModelsOrCatalog(node, template) {
37
+ return node.models?.length ? node.models : template.models;
34
38
  }
35
39
  function nodeCatalogTemplateFor(node, templates) {
36
40
  return templates[node.id] ?? Object.values(templates).find((candidate) => node.id.includes(candidate.category)) ?? Object.values(templates).find((candidate) => candidate.profile === node.profile);
@@ -44,6 +44,9 @@ function plannerPrompt(entrypointId, task, baseline, config, planningContext) {
44
44
  "Scheduler node catalog:",
45
45
  schedulerCatalogPrompt(config, entrypointId),
46
46
  "",
47
+ "Token budget:",
48
+ tokenBudgetPrompt(config),
49
+ "",
47
50
  "Gate recipes:",
48
51
  "- Prefer preserving valid gates from the baseline workflows instead of recreating them.",
49
52
  "- RED/test coverage may use changed_files gates on test-writing nodes. A changed_files gate must include a changed_files object with allow and/or require_any glob arrays.",
@@ -84,6 +87,18 @@ function plannerRepairPrompt(inputs) {
84
87
  stringify(inputs.baseline)
85
88
  ].join("\n");
86
89
  }
90
+ function tokenBudgetPrompt(config) {
91
+ const budget = config.token_budget;
92
+ const windows = Object.entries(budget.model_context_windows);
93
+ const fanOut = Object.entries(budget.fan_out_width.by_category);
94
+ return [
95
+ `- Keep each node's assembled context under ${budget.max_context_pct}% of its model's context window; prefer the smallest-tier model whose window comfortably holds the node within that cap.`,
96
+ `- Assume ${budget.default_context_window} tokens of context window for a model with no declared window.`,
97
+ windows.length > 0 ? `- Known model context windows: ${windows.map(([id, size]) => `${id}=${size}`).join(", ")}.` : void 0,
98
+ `- Do not exceed the per-category fan-out width (max concurrent same-category nodes). Default width: ${budget.fan_out_width.default}.`,
99
+ fanOut.length > 0 ? `- Category fan-out caps: ${fanOut.map(([category, width]) => `${category}=${width}`).join(", ")}.` : void 0
100
+ ].filter((line) => Boolean(line)).join("\n");
101
+ }
87
102
  function allowedProfilePromptLine(config, id) {
88
103
  const profile = config.profiles[id];
89
104
  const runner = config.runners[profile.runner];
@@ -0,0 +1,23 @@
1
+ import { getEncoding } from "js-tiktoken";
2
+ //#region src/token-estimator.ts
3
+ /**
4
+ * Token estimation for node sizing. Uses the `o200k_base` BPE as a
5
+ * model-agnostic heuristic — NOT a guarantee of any specific model's tokenizer.
6
+ *
7
+ * This is a cross-model ESTIMATE, not a billing-accurate count: the pipeline
8
+ * routes nodes across OpenAI/Kimi/Qwen models whose exact tokenizers differ (and
9
+ * are not all known here), so treat the value as a sizing heuristic for
10
+ * budget/routing decisions only. For exact counts on Anthropic runners, use the
11
+ * Anthropic `count_tokens` API.
12
+ */
13
+ let encoder;
14
+ function encoding() {
15
+ encoder ??= getEncoding("o200k_base");
16
+ return encoder;
17
+ }
18
+ function estimateTokens(text) {
19
+ if (text.length === 0) return 0;
20
+ return encoding().encode(text).length;
21
+ }
22
+ //#endregion
23
+ export { estimateTokens };
package/package.json CHANGED
@@ -9,6 +9,7 @@
9
9
  "execa": "^9.5.2",
10
10
  "git-url-parse": "^16.1.0",
11
11
  "gray-matter": "^4.0.3",
12
+ "js-tiktoken": "^1.0.21",
12
13
  "jsonc-parser": "^3.3.1",
13
14
  "ky": "^2.0.2",
14
15
  "micromatch": "^4.0.8",
@@ -120,7 +121,7 @@
120
121
  "prepack": "bun run build:cli"
121
122
  },
122
123
  "type": "module",
123
- "version": "2.3.1",
124
+ "version": "2.5.0",
124
125
  "description": "Config-driven multi-agent pipeline runner for repository work",
125
126
  "main": "./dist/index.js",
126
127
  "types": "./dist/index.d.ts",