@spendgraph/harness 0.1.0 → 0.2.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 (119) hide show
  1. package/README.md +141 -111
  2. package/dist/cascade/cascade.d.ts +13 -0
  3. package/dist/cascade/cascade.js +80 -0
  4. package/dist/cascade/index.d.ts +3 -0
  5. package/dist/cascade/index.js +2 -0
  6. package/dist/cascade/tier.d.ts +31 -0
  7. package/dist/cascade/tier.js +32 -0
  8. package/dist/cascade/types.d.ts +98 -0
  9. package/dist/chain/chain.d.ts +9 -0
  10. package/dist/chain/chain.js +50 -0
  11. package/dist/chain/gate.d.ts +16 -0
  12. package/dist/chain/gate.js +25 -0
  13. package/dist/chain/index.d.ts +3 -0
  14. package/dist/chain/index.js +2 -0
  15. package/dist/chain/step.d.ts +9 -0
  16. package/dist/chain/step.js +7 -0
  17. package/dist/chain/types.d.ts +50 -0
  18. package/dist/index.d.ts +16 -9
  19. package/dist/index.js +8 -7
  20. package/dist/loop/act.d.ts +21 -0
  21. package/dist/loop/act.js +29 -0
  22. package/dist/loop/compact.d.ts +49 -0
  23. package/dist/loop/compact.js +41 -0
  24. package/dist/loop/hooks.d.ts +43 -0
  25. package/dist/loop/hooks.js +31 -0
  26. package/dist/loop/index.d.ts +6 -0
  27. package/dist/loop/index.js +5 -0
  28. package/dist/loop/loop.d.ts +13 -0
  29. package/dist/loop/loop.js +99 -0
  30. package/dist/loop/turn.d.ts +6 -0
  31. package/dist/loop/turn.js +15 -0
  32. package/dist/loop/types.d.ts +140 -0
  33. package/dist/loop/types.js +1 -0
  34. package/dist/orchestrate/index.d.ts +4 -0
  35. package/dist/orchestrate/index.js +3 -0
  36. package/dist/orchestrate/orchestrate.d.ts +12 -0
  37. package/dist/orchestrate/orchestrate.js +56 -0
  38. package/dist/orchestrate/plan.d.ts +18 -0
  39. package/dist/orchestrate/plan.js +27 -0
  40. package/dist/orchestrate/types.d.ts +75 -0
  41. package/dist/orchestrate/types.js +1 -0
  42. package/dist/orchestrate/work.d.ts +13 -0
  43. package/dist/orchestrate/work.js +25 -0
  44. package/dist/parallel/index.d.ts +4 -0
  45. package/dist/parallel/index.js +3 -0
  46. package/dist/parallel/merge.d.ts +26 -0
  47. package/dist/parallel/merge.js +35 -0
  48. package/dist/parallel/parallel.d.ts +12 -0
  49. package/dist/parallel/parallel.js +56 -0
  50. package/dist/parallel/task.d.ts +13 -0
  51. package/dist/parallel/task.js +37 -0
  52. package/dist/parallel/types.d.ts +61 -0
  53. package/dist/parallel/types.js +1 -0
  54. package/dist/refine/attempt.d.ts +22 -0
  55. package/dist/refine/attempt.js +24 -0
  56. package/dist/refine/index.d.ts +4 -0
  57. package/dist/refine/index.js +3 -0
  58. package/dist/refine/judge.d.ts +15 -0
  59. package/dist/refine/judge.js +14 -0
  60. package/dist/refine/refine.d.ts +10 -0
  61. package/dist/refine/refine.js +59 -0
  62. package/dist/refine/types.d.ts +90 -0
  63. package/dist/refine/types.js +1 -0
  64. package/dist/route/classify.d.ts +52 -0
  65. package/dist/route/classify.js +33 -0
  66. package/dist/route/dispatch.d.ts +12 -0
  67. package/dist/route/dispatch.js +25 -0
  68. package/dist/route/index.d.ts +3 -0
  69. package/dist/route/index.js +2 -0
  70. package/dist/route/route.d.ts +11 -0
  71. package/dist/route/route.js +22 -0
  72. package/dist/route/types.d.ts +53 -0
  73. package/dist/route/types.js +1 -0
  74. package/dist/stream/graph.d.ts +9 -0
  75. package/dist/stream/graph.js +10 -0
  76. package/dist/stream/index.d.ts +2 -0
  77. package/dist/stream/index.js +1 -0
  78. package/dist/stream/stream.d.ts +19 -0
  79. package/dist/stream/stream.js +48 -0
  80. package/dist/stream/types.d.ts +31 -0
  81. package/dist/stream/types.js +1 -0
  82. package/docs/README.md +41 -0
  83. package/docs/cascade.md +165 -0
  84. package/docs/chain.md +134 -0
  85. package/docs/loop.md +318 -0
  86. package/docs/orchestrate.md +128 -0
  87. package/docs/parallel.md +139 -0
  88. package/docs/refine.md +206 -0
  89. package/docs/route.md +212 -0
  90. package/package.json +19 -15
  91. package/bin/harness.mjs +0 -76
  92. package/dist/core/budget.d.ts +0 -46
  93. package/dist/core/budget.js +0 -72
  94. package/dist/core/cache.d.ts +0 -119
  95. package/dist/core/cache.js +0 -206
  96. package/dist/core/client.d.ts +0 -33
  97. package/dist/core/client.js +0 -98
  98. package/dist/core/errors.d.ts +0 -22
  99. package/dist/core/errors.js +0 -28
  100. package/dist/core/index.d.ts +0 -5
  101. package/dist/core/index.js +0 -5
  102. package/dist/core/pull.d.ts +0 -17
  103. package/dist/core/pull.js +0 -38
  104. package/dist/core/schema/serialize.d.ts +0 -12
  105. package/dist/core/schema/serialize.js +0 -42
  106. package/dist/core/schema/types.d.ts +0 -58
  107. package/dist/core/schema/validate.d.ts +0 -28
  108. package/dist/core/schema/validate.js +0 -98
  109. package/dist/harness.d.ts +0 -128
  110. package/dist/harness.js +0 -187
  111. package/dist/prompt/codegen.d.ts +0 -37
  112. package/dist/prompt/codegen.js +0 -97
  113. package/dist/prompt/render.d.ts +0 -35
  114. package/dist/prompt/render.js +0 -66
  115. package/dist/prompt/run.d.ts +0 -38
  116. package/dist/prompt/run.js +0 -96
  117. package/dist/prompt/types.d.ts +0 -156
  118. /package/dist/{core/schema → cascade}/types.js +0 -0
  119. /package/dist/{prompt → chain}/types.js +0 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export { orchestrate } from "./orchestrate.js";
2
+ export { capped, planStep } from "./plan.js";
3
+ export type { OrchestrateEnd, OrchestrateOptions, OrchestrateResult, Outcome, Plan, Subtask, } from "./types.js";
4
+ export { synthesisStep, work } from "./work.js";
@@ -0,0 +1,3 @@
1
+ export { orchestrate } from "./orchestrate.js";
2
+ export { capped, planStep } from "./plan.js";
3
+ export { synthesisStep, work } from "./work.js";
@@ -0,0 +1,12 @@
1
+ import type { OrchestrateOptions, OrchestrateResult } from "./types.js";
2
+ /**
3
+ * A lead decomposes, workers do the pieces, the lead puts it back together.
4
+ *
5
+ * The workflow for work whose shape is not known until the input is read — the
6
+ * difference from `chain`, where the stages are named in advance.
7
+ *
8
+ * It is also the one that can spend the most: the lead decides how many calls
9
+ * to make, so `maxWorkers` is a cap on something a model chose rather than on
10
+ * something you did.
11
+ */
12
+ export declare function orchestrate(values: Record<string, unknown>, opts: OrchestrateOptions): Promise<OrchestrateResult>;
@@ -0,0 +1,56 @@
1
+ import { capped, planStep } from "./plan.js";
2
+ import { synthesisStep, work } from "./work.js";
3
+ function tokens(steps) {
4
+ let inputTokens = 0;
5
+ let outputTokens = 0;
6
+ for (const step of steps) {
7
+ inputTokens += step.inputTokens ?? 0;
8
+ outputTokens += step.outputTokens ?? 0;
9
+ }
10
+ return { inputTokens, outputTokens };
11
+ }
12
+ export async function orchestrate(values, opts) {
13
+ const startedAt = Date.now();
14
+ const steps = [];
15
+ let subtasks = [];
16
+ let dropped = [];
17
+ let results = [];
18
+ const finish = (status, stoppedBy, output, error) => ({
19
+ status,
20
+ output,
21
+ error,
22
+ steps,
23
+ outputs: { subtasks, results },
24
+ latencyMs: Date.now() - startedAt,
25
+ ...tokens(steps),
26
+ subtasks,
27
+ dropped,
28
+ ran: results.filter((r) => r.status === "completed").map((r) => r.name),
29
+ failed: results.filter((r) => r.status === "failed").map((r) => r.name),
30
+ stoppedBy,
31
+ });
32
+ const plan = await opts.plan(values);
33
+ ({ subtasks, dropped } = capped(plan, opts.maxWorkers));
34
+ steps.push(planStep(steps.length, plan, dropped));
35
+ opts.onPlan?.(plan, dropped);
36
+ if (subtasks.length === 0) {
37
+ return finish("completed", "planned-nothing", plan.output ?? "");
38
+ }
39
+ const spentOnPlan = (plan.inputTokens ?? 0) + (plan.outputTokens ?? 0);
40
+ if (opts.maxTokens !== undefined && spentOnPlan >= opts.maxTokens) {
41
+ return finish("failed", "tokens", "", "The plan alone reached the token ceiling.");
42
+ }
43
+ const done = await work(subtasks, values, opts);
44
+ steps.push(...done.steps.map((step, at) => ({ ...step, index: steps.length + at })));
45
+ results = done.results;
46
+ if (done.status === "failed") {
47
+ return finish("failed", "workers-failed", "", done.error);
48
+ }
49
+ const { inputTokens, outputTokens } = tokens(steps);
50
+ if (opts.maxTokens !== undefined && inputTokens + outputTokens >= opts.maxTokens) {
51
+ return finish("failed", "tokens", "", "The workers reached the token ceiling before synthesis.");
52
+ }
53
+ const synthesis = await opts.synthesise(results.filter((r) => r.status === "completed"), values, (text, stage) => opts.emit?.(text, stage ?? "synthesis"));
54
+ steps.push(synthesisStep(steps.length, synthesis));
55
+ return finish("completed", "completed", synthesis.output);
56
+ }
@@ -0,0 +1,18 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { Plan, Subtask } from "./types.js";
3
+ /**
4
+ * The subtasks that will run, and the ones the cap refused.
5
+ *
6
+ * Dropping the tail rather than the head: a lead asked for the most important
7
+ * piece first usually gives it first, and truncating from the front would throw
8
+ * away exactly what it thought mattered.
9
+ *
10
+ * Duplicated names are made unique rather than rejected — a lead naming two
11
+ * sections "summary" is a wording problem, not a reason to lose the run.
12
+ */
13
+ export declare function capped(plan: Plan, max?: number): {
14
+ subtasks: Subtask[];
15
+ dropped: Subtask[];
16
+ };
17
+ /** The lead's own call as a step, because deciding is not free. */
18
+ export declare function planStep(index: number, plan: Plan, dropped: Subtask[]): RolloutStep;
@@ -0,0 +1,27 @@
1
+ const DEFAULT_MAX_WORKERS = 5;
2
+ export function capped(plan, max = DEFAULT_MAX_WORKERS) {
3
+ const limit = Math.max(1, max);
4
+ const seen = new Map();
5
+ const named = plan.subtasks.map((subtask) => {
6
+ const count = seen.get(subtask.name) ?? 0;
7
+ seen.set(subtask.name, count + 1);
8
+ return count === 0 ? subtask : { ...subtask, name: `${subtask.name}_${count}` };
9
+ });
10
+ return { subtasks: named.slice(0, limit), dropped: named.slice(limit) };
11
+ }
12
+ export function planStep(index, plan, dropped) {
13
+ return {
14
+ index,
15
+ source: "plan",
16
+ model: plan.model,
17
+ output: plan.output ?? JSON.stringify(plan.subtasks.map((s) => s.name)),
18
+ status: "completed",
19
+ inputTokens: plan.inputTokens,
20
+ outputTokens: plan.outputTokens,
21
+ ...(dropped.length > 0
22
+ ? {
23
+ error: `${dropped.length} subtask(s) past the worker cap were dropped: ${dropped.map((s) => s.name).join(", ")}`,
24
+ }
25
+ : {}),
26
+ };
27
+ }
@@ -0,0 +1,75 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { TaskResult } from "../parallel/index.js";
3
+ import type { Emit } from "../stream/index.js";
4
+ /** One piece of work the lead invented. Anything else on it is yours. */
5
+ export interface Subtask {
6
+ /** Unique in the run, and the name its step is recorded under. */
7
+ name: string;
8
+ [key: string]: unknown;
9
+ }
10
+ /** What the lead decided to do, and what deciding cost. */
11
+ export interface Plan {
12
+ subtasks: Subtask[];
13
+ output?: string;
14
+ model?: string;
15
+ inputTokens?: number;
16
+ outputTokens?: number;
17
+ }
18
+ /** What a worker or the synthesis produced. */
19
+ export interface Outcome {
20
+ output: string;
21
+ model?: string;
22
+ inputTokens?: number;
23
+ outputTokens?: number;
24
+ }
25
+ export interface OrchestrateOptions {
26
+ /** The lead reads the input and says what the pieces are. */
27
+ plan(values: Record<string, unknown>): Promise<Plan> | Plan;
28
+ /** One piece of work. */
29
+ work(subtask: Subtask, values: Record<string, unknown>, emit: Emit): Promise<Outcome> | Outcome;
30
+ /** The lead turns what came back into the answer. */
31
+ synthesise(results: TaskResult[], values: Record<string, unknown>, emit: Emit): Promise<Outcome> | Outcome;
32
+ /**
33
+ * Where the tokens go while this runs.
34
+ *
35
+ * Passed down to whatever makes the model call; omit it and nothing is
36
+ * emitted. `streamed()` supplies one.
37
+ */
38
+ emit?: Emit;
39
+ /**
40
+ * Most workers to run. Default 5.
41
+ *
42
+ * The failure this exists for: a lead asked to decompose a large input
43
+ * invents twelve subtasks, and twelve model calls leave before anyone has
44
+ * decided that was reasonable. Anything past the cap is dropped and **said**,
45
+ * never dropped quietly.
46
+ */
47
+ maxWorkers?: number;
48
+ /** Most workers in flight at once. Default 4. */
49
+ concurrency?: number;
50
+ /** How many must succeed before synthesising. Defaults to all that ran. */
51
+ minSuccess?: number;
52
+ /** Checked before the workers and before the synthesis. */
53
+ maxTokens?: number;
54
+ onPlan?: (plan: Plan, dropped: Subtask[]) => void;
55
+ onWork?: (result: TaskResult) => void;
56
+ }
57
+ /** Why it ended. `planned-nothing` is the lead finding no work to do. */
58
+ export type OrchestrateEnd = "completed" | "planned-nothing" | "workers-failed" | "tokens" | "failed";
59
+ export interface OrchestrateResult {
60
+ status: "completed" | "failed";
61
+ output: string;
62
+ error?: string;
63
+ steps: RolloutStep[];
64
+ outputs: Record<string, unknown>;
65
+ latencyMs: number;
66
+ inputTokens: number;
67
+ outputTokens: number;
68
+ /** What the lead asked for, after the cap. */
69
+ subtasks: Subtask[];
70
+ /** What the cap refused. Empty when the lead stayed within it. */
71
+ dropped: Subtask[];
72
+ ran: string[];
73
+ failed: string[];
74
+ stoppedBy: OrchestrateEnd;
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import { type ParallelResult } from "../parallel/index.js";
3
+ import type { OrchestrateOptions, Outcome, Subtask } from "./types.js";
4
+ /**
5
+ * Every worker at once.
6
+ *
7
+ * Delegated to `parallel` rather than fanned out again here: it already caps the
8
+ * concurrency, already turns a thrown worker into a result so the others land,
9
+ * and already numbers the steps by declaration so two runs can be compared.
10
+ */
11
+ export declare function work(subtasks: Subtask[], values: Record<string, unknown>, opts: OrchestrateOptions): Promise<ParallelResult>;
12
+ /** The lead's second call as a step. */
13
+ export declare function synthesisStep(index: number, outcome: Outcome): RolloutStep;
@@ -0,0 +1,25 @@
1
+ import { parallel } from "../parallel/index.js";
2
+ export function work(subtasks, values, opts) {
3
+ return parallel(values, {
4
+ tasks: subtasks.map((subtask) => ({
5
+ name: subtask.name,
6
+ run: (_values, emit) => opts.work(subtask, values, emit),
7
+ })),
8
+ emit: opts.emit,
9
+ concurrency: opts.concurrency,
10
+ minSuccess: opts.minSuccess ?? subtasks.length,
11
+ onTask: opts.onWork,
12
+ merge: (results) => results,
13
+ });
14
+ }
15
+ export function synthesisStep(index, outcome) {
16
+ return {
17
+ index,
18
+ source: "synthesise",
19
+ model: outcome.model,
20
+ output: outcome.output,
21
+ status: "completed",
22
+ inputTokens: outcome.inputTokens,
23
+ outputTokens: outcome.outputTokens,
24
+ };
25
+ }
@@ -0,0 +1,4 @@
1
+ export { majority, repeat, type Vote } from "./merge.js";
2
+ export { parallel } from "./parallel.js";
3
+ export { runTask, taskStep } from "./task.js";
4
+ export type { ParallelOptions, ParallelResult, Task, TaskResult } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { majority, repeat } from "./merge.js";
2
+ export { parallel } from "./parallel.js";
3
+ export { runTask, taskStep } from "./task.js";
@@ -0,0 +1,26 @@
1
+ import type { Task, TaskResult } from "./types.js";
2
+ /**
3
+ * The same task, N times over, for voting.
4
+ *
5
+ * An odd N on purpose — an even one ties, and a tie has no answer that is not
6
+ * arbitrary. Ask for an even number and you get the next one up.
7
+ */
8
+ export declare function repeat(name: string, times: number, run: (values: Record<string, unknown>, at: number) => Promise<unknown> | unknown): Task[];
9
+ export interface Vote<T = unknown> {
10
+ value: T;
11
+ /** How many of the tasks agreed on it. */
12
+ count: number;
13
+ /** Of the ones that succeeded. */
14
+ of: number;
15
+ }
16
+ /**
17
+ * The answer most of them gave.
18
+ *
19
+ * Voting is worth the tokens where a model is right most of the time and wrong
20
+ * differently each time — the wrong answers scatter and the right one repeats.
21
+ * It buys nothing where the model is confidently wrong the same way every time,
22
+ * which is the failure it is easiest to mistake it for a fix for.
23
+ *
24
+ * Ties go to the first declared, so the result is the same on every run.
25
+ */
26
+ export declare function majority<T = unknown>(results: TaskResult[]): Vote<T> | undefined;
@@ -0,0 +1,35 @@
1
+ export function repeat(name, times, run) {
2
+ const odd = Math.max(1, times % 2 === 0 ? times + 1 : times);
3
+ return Array.from({ length: odd }, (_, at) => ({
4
+ name: `${name}_${at}`,
5
+ run: (values) => run(values, at),
6
+ }));
7
+ }
8
+ function ballot(value) {
9
+ if (value &&
10
+ typeof value === "object" &&
11
+ typeof value.output === "string") {
12
+ return value.output;
13
+ }
14
+ return typeof value === "string" ? value : JSON.stringify(value);
15
+ }
16
+ export function majority(results) {
17
+ const done = results.filter((r) => r.status === "completed");
18
+ if (done.length === 0)
19
+ return undefined;
20
+ const tally = new Map();
21
+ for (const result of done) {
22
+ const key = ballot(result.value);
23
+ const seen = tally.get(key);
24
+ if (seen)
25
+ seen.count++;
26
+ else
27
+ tally.set(key, { value: result.value, count: 1 });
28
+ }
29
+ let best = { value: done[0].value, count: 0 };
30
+ for (const entry of tally.values()) {
31
+ if (entry.count > best.count)
32
+ best = entry;
33
+ }
34
+ return { value: best.value, count: best.count, of: done.length };
35
+ }
@@ -0,0 +1,12 @@
1
+ import type { ParallelOptions, ParallelResult } from "./types.js";
2
+ /**
3
+ * Every task at once, then one answer out of what came back.
4
+ *
5
+ * Not compiled to a graph like `route` and `chain`: `@spendgraph/graph` walks one
6
+ * node at a time and picks a single edge, so a fan-out is not a shape it can
7
+ * express. The concurrency comes from `mapLimit`, which already exists.
8
+ *
9
+ * A task that throws is a result, not an exception — the point of running five
10
+ * is that the other four still land.
11
+ */
12
+ export declare function parallel(values: Record<string, unknown>, opts: ParallelOptions): Promise<ParallelResult>;
@@ -0,0 +1,56 @@
1
+ import { mapLimit } from "@spendgraph/prompt/internals";
2
+ import { runTask, taskStep } from "./task.js";
3
+ const DEFAULT_CONCURRENCY = 4;
4
+ function totals(results) {
5
+ let inputTokens = 0;
6
+ let outputTokens = 0;
7
+ for (const result of results) {
8
+ inputTokens += result.inputTokens;
9
+ outputTokens += result.outputTokens;
10
+ }
11
+ return { inputTokens, outputTokens };
12
+ }
13
+ function stringify(value) {
14
+ if (value === undefined || value === null)
15
+ return "";
16
+ return typeof value === "string" ? value : JSON.stringify(value);
17
+ }
18
+ export async function parallel(values, opts) {
19
+ if (opts.tasks.length === 0)
20
+ throw new Error("A parallel run needs at least one task.");
21
+ const names = new Set();
22
+ for (const task of opts.tasks) {
23
+ if (names.has(task.name))
24
+ throw new Error(`Two tasks are called "${task.name}".`);
25
+ names.add(task.name);
26
+ }
27
+ const startedAt = Date.now();
28
+ const results = await mapLimit(opts.tasks, opts.concurrency ?? DEFAULT_CONCURRENCY, async (task) => {
29
+ const result = await runTask(task, values, opts.emit);
30
+ opts.onTask?.(result);
31
+ return result;
32
+ });
33
+ const steps = results.map((result, at) => taskStep(at, result));
34
+ const succeeded = results.filter((r) => r.status === "completed");
35
+ const needed = opts.minSuccess ?? opts.tasks.length;
36
+ const outputs = Object.fromEntries(results.map((r) => [r.name, r.value]));
37
+ const base = {
38
+ steps,
39
+ outputs,
40
+ latencyMs: Date.now() - startedAt,
41
+ ...totals(results),
42
+ results,
43
+ succeeded: succeeded.map((r) => r.name),
44
+ failed: results.filter((r) => r.status === "failed").map((r) => r.name),
45
+ };
46
+ if (succeeded.length < needed) {
47
+ return {
48
+ ...base,
49
+ status: "failed",
50
+ output: "",
51
+ error: `${succeeded.length} of ${opts.tasks.length} tasks succeeded, and ${needed} were needed.`,
52
+ };
53
+ }
54
+ const merged = opts.merge(succeeded, values);
55
+ return { ...base, status: "completed", output: stringify(merged), merged };
56
+ }
@@ -0,0 +1,13 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { Emit } from "../stream/index.js";
3
+ import type { Task, TaskResult } from "./types.js";
4
+ /** Runs one task. Never throws — a failure is a result, so the others still land. */
5
+ export declare function runTask(task: Task, values: Record<string, unknown>, emit?: Emit): Promise<TaskResult>;
6
+ /**
7
+ * One task as a step.
8
+ *
9
+ * Indexed by where it was declared rather than when it landed: a fan-out that
10
+ * numbered its steps by completion order would record a different run every
11
+ * time for the same work, which makes two runs impossible to compare.
12
+ */
13
+ export declare function taskStep(index: number, result: TaskResult): RolloutStep;
@@ -0,0 +1,37 @@
1
+ function counted(value) {
2
+ return value && typeof value === "object" ? value : {};
3
+ }
4
+ export async function runTask(task, values, emit) {
5
+ try {
6
+ const value = await task.run(values, (text, stage) => emit?.(text, stage ?? task.name));
7
+ const { inputTokens = 0, outputTokens = 0 } = counted(value);
8
+ return { name: task.name, status: "completed", value, inputTokens, outputTokens };
9
+ }
10
+ catch (err) {
11
+ return {
12
+ name: task.name,
13
+ status: "failed",
14
+ error: err instanceof Error ? err.message : String(err),
15
+ inputTokens: 0,
16
+ outputTokens: 0,
17
+ };
18
+ }
19
+ }
20
+ export function taskStep(index, result) {
21
+ const { output, model } = counted(result.value);
22
+ return {
23
+ index,
24
+ source: result.name,
25
+ model,
26
+ output: result.status === "completed" ? (output ?? stringify(result.value)) : "",
27
+ status: result.status,
28
+ error: result.error,
29
+ inputTokens: result.inputTokens,
30
+ outputTokens: result.outputTokens,
31
+ };
32
+ }
33
+ function stringify(value) {
34
+ if (value === undefined || value === null)
35
+ return "";
36
+ return typeof value === "string" ? value : JSON.stringify(value);
37
+ }
@@ -0,0 +1,61 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { Emit } from "../stream/index.js";
3
+ /** One branch of the fan-out. */
4
+ export interface Task {
5
+ /** Unique in the run, and the name its step is recorded under. */
6
+ name: string;
7
+ run(values: Record<string, unknown>, emit: Emit): Promise<unknown> | unknown;
8
+ }
9
+ /** What one task produced, or why it did not. */
10
+ export interface TaskResult {
11
+ name: string;
12
+ status: "completed" | "failed";
13
+ value?: unknown;
14
+ error?: string;
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ }
18
+ export interface ParallelOptions {
19
+ tasks: Task[];
20
+ /**
21
+ * Combines what came back into the answer.
22
+ *
23
+ * Only the tasks that succeeded are passed. Sectioning usually joins them;
24
+ * voting usually counts them — `majority` is exported for that.
25
+ */
26
+ merge(results: TaskResult[], values: Record<string, unknown>): unknown;
27
+ /**
28
+ * Where the tokens go while this runs.
29
+ *
30
+ * Passed down to whatever makes the model call; omit it and nothing is
31
+ * emitted. `streamed()` supplies one.
32
+ */
33
+ emit?: Emit;
34
+ /** Most tasks in flight at once. Default 4. */
35
+ concurrency?: number;
36
+ /**
37
+ * How many must succeed before merging is worth doing. Defaults to all.
38
+ *
39
+ * Sectioning wants all of them: a missing section is a hole in the answer.
40
+ * Voting wants a majority, and is right to ignore the one call that timed out.
41
+ */
42
+ minSuccess?: number;
43
+ /** Called as each task lands, so a slow fan-out is not silent. */
44
+ onTask?: (result: TaskResult) => void;
45
+ }
46
+ export interface ParallelResult {
47
+ status: "completed" | "failed";
48
+ output: string;
49
+ error?: string;
50
+ steps: RolloutStep[];
51
+ outputs: Record<string, unknown>;
52
+ latencyMs: number;
53
+ inputTokens: number;
54
+ outputTokens: number;
55
+ /** Every task, in the order they were declared — never the order they landed. */
56
+ results: TaskResult[];
57
+ succeeded: string[];
58
+ failed: string[];
59
+ /** What `merge` returned. Undefined when too few tasks succeeded. */
60
+ merged?: unknown;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { Attempt } from "./types.js";
3
+ /**
4
+ * What one attempt consumed.
5
+ *
6
+ * Both halves, because a refine loop's cost is dominated by the input it resends
7
+ * every round — the draft, the critique and the instruction to try again.
8
+ */
9
+ export declare function tokensOf(attempt: Attempt): number;
10
+ /**
11
+ * One attempt as a step.
12
+ *
13
+ * Recorded whether it was accepted or not: the rejected drafts are the evidence
14
+ * for whether refining paid at all, and a loop that keeps only the winner cannot
15
+ * say what the answer cost.
16
+ */
17
+ export declare function attemptStep(index: number, round: number, attempt: Attempt): RolloutStep;
18
+ /** Both halves of every attempt so far, for the result's totals. */
19
+ export declare function totalTokens(steps: RolloutStep[]): {
20
+ inputTokens: number;
21
+ outputTokens: number;
22
+ };
@@ -0,0 +1,24 @@
1
+ export function tokensOf(attempt) {
2
+ return (attempt.inputTokens ?? 0) + (attempt.outputTokens ?? 0);
3
+ }
4
+ export function attemptStep(index, round, attempt) {
5
+ return {
6
+ index,
7
+ source: `attempt_${round}`,
8
+ model: attempt.model,
9
+ output: attempt.output,
10
+ status: attempt.status ?? "completed",
11
+ error: attempt.error,
12
+ inputTokens: attempt.inputTokens,
13
+ outputTokens: attempt.outputTokens,
14
+ };
15
+ }
16
+ export function totalTokens(steps) {
17
+ let inputTokens = 0;
18
+ let outputTokens = 0;
19
+ for (const step of steps) {
20
+ inputTokens += step.inputTokens ?? 0;
21
+ outputTokens += step.outputTokens ?? 0;
22
+ }
23
+ return { inputTokens, outputTokens };
24
+ }
@@ -0,0 +1,4 @@
1
+ export { attemptStep, tokensOf } from "./attempt.js";
2
+ export { best, judgeStep } from "./judge.js";
3
+ export { refine } from "./refine.js";
4
+ export type { Attempt, RefineOptions, RefineResult, RoundRecord, Verdict } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { attemptStep, tokensOf } from "./attempt.js";
2
+ export { best, judgeStep } from "./judge.js";
3
+ export { refine } from "./refine.js";
@@ -0,0 +1,15 @@
1
+ import type { RolloutStep } from "@spendgraph/sdk";
2
+ import type { RoundRecord, Verdict } from "./types.js";
3
+ /** One verdict as a step, so a run says why each draft was turned down. */
4
+ export declare function judgeStep(index: number, round: number, verdict: Verdict): RolloutStep;
5
+ /**
6
+ * The attempt to hand back when nothing was accepted.
7
+ *
8
+ * Highest score wins, and the last attempt wins when nothing was scored — a loop
9
+ * that improves each round has its best work at the end, and returning the first
10
+ * draft after three revisions would throw the work away.
11
+ *
12
+ * Here rather than in the loop because the choice is the judge's: it is made on
13
+ * the verdicts, and a judge that never scores gets the fallback.
14
+ */
15
+ export declare function best(history: RoundRecord[]): RoundRecord | undefined;
@@ -0,0 +1,14 @@
1
+ export function judgeStep(index, round, verdict) {
2
+ return {
3
+ index,
4
+ source: `judge_${round}`,
5
+ output: JSON.stringify(verdict),
6
+ status: "completed",
7
+ };
8
+ }
9
+ export function best(history) {
10
+ const scored = history.filter((r) => typeof r.verdict.score === "number");
11
+ if (scored.length === 0)
12
+ return history.at(-1);
13
+ return scored.reduce((a, b) => ((b.verdict.score ?? 0) > (a.verdict.score ?? 0) ? b : a));
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { RefineOptions, RefineResult } from "./types.js";
2
+ /**
3
+ * Draft, judge, revise, until it is good enough or the budget is out.
4
+ *
5
+ * The pattern worth naming when there is a clear criterion and a first draft
6
+ * rarely meets it. Every attempt and every judgement is recorded, the rejected
7
+ * ones included: a loop that keeps only the winner cannot say what the answer
8
+ * cost, and the rejected drafts are the evidence for whether refining paid.
9
+ */
10
+ export declare function refine(opts: RefineOptions): Promise<RefineResult>;