@spendgraph/harness 0.1.0 → 0.2.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.
Files changed (119) hide show
  1. package/README.md +87 -160
  2. package/dist/cascade/cascade.d.ts +13 -0
  3. package/dist/cascade/cascade.js +91 -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 +53 -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 +58 -0
  11. package/dist/chain/gate.d.ts +16 -0
  12. package/dist/chain/gate.js +33 -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 +14 -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 +43 -0
  22. package/dist/loop/compact.d.ts +49 -0
  23. package/dist/loop/compact.js +59 -0
  24. package/dist/loop/hooks.d.ts +43 -0
  25. package/dist/loop/hooks.js +41 -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 +120 -0
  30. package/dist/loop/turn.d.ts +6 -0
  31. package/dist/loop/turn.js +17 -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 +68 -0
  38. package/dist/orchestrate/plan.d.ts +18 -0
  39. package/dist/orchestrate/plan.js +38 -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 +35 -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 +52 -0
  48. package/dist/parallel/parallel.d.ts +12 -0
  49. package/dist/parallel/parallel.js +66 -0
  50. package/dist/parallel/task.d.ts +13 -0
  51. package/dist/parallel/task.js +45 -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 +38 -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 +25 -0
  60. package/dist/refine/refine.d.ts +10 -0
  61. package/dist/refine/refine.js +67 -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 +57 -0
  66. package/dist/route/dispatch.d.ts +12 -0
  67. package/dist/route/dispatch.js +33 -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 +31 -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 +16 -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 +65 -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 +18 -14
  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
@@ -1,66 +0,0 @@
1
- /**
2
- * Substitutes `{name}` placeholders. An unknown one is left verbatim — silently
3
- * emptying it would send a prompt with a hole the caller cannot see.
4
- *
5
- * Mirrors `lib/prompt.ts` in the app, deliberately duplicated: this package is
6
- * published and depends on nothing. The two are kept in step by
7
- * `rendered` being recorded on every rollout, which makes a divergence visible
8
- * rather than theoretical.
9
- */
10
- export function applyVariables(text, values) {
11
- if (!text || !values)
12
- return text;
13
- return text.replace(/\{(\w+)\}/g, (whole, name) => name in values ? values[name] : whole);
14
- }
15
- /** Placeholder names used anywhere in the prompt, in first-seen order. */
16
- export function findVariables(blocks, question) {
17
- const seen = [];
18
- const scan = (text) => {
19
- for (const m of text.matchAll(/\{(\w+)\}/g)) {
20
- if (!seen.includes(m[1]))
21
- seen.push(m[1]);
22
- }
23
- };
24
- // titles too — a heading can carry a placeholder as readily as a body
25
- for (const b of blocks ?? []) {
26
- scan(b.title ?? "");
27
- scan(b.body ?? "");
28
- }
29
- scan(question);
30
- return seen;
31
- }
32
- /**
33
- * Blocks in the order they were arranged.
34
- *
35
- * An empty body drops the block entirely — a heading over nothing is noise in
36
- * the prompt. A body with no title is emitted bare rather than under a blank
37
- * `##`, so an unnamed block still says what it says.
38
- */
39
- export function compileSystemPrompt(blocks, values) {
40
- const out = [];
41
- for (const b of blocks ?? []) {
42
- const body = b.body?.trim();
43
- if (!body)
44
- continue;
45
- const title = b.title?.trim();
46
- const text = applyVariables(body, values);
47
- out.push(title ? `## ${applyVariables(title, values)}\n${text}` : text);
48
- }
49
- return out.join("\n\n");
50
- }
51
- /**
52
- * Every message the request will send, in order.
53
- *
54
- * An empty system message is a different request from no system message at all,
55
- * so a prompt with no blocks sends one message rather than two.
56
- */
57
- export function renderMessages(blocks, question, values) {
58
- const system = compileSystemPrompt(blocks, values);
59
- const user = applyVariables(question, values);
60
- return system
61
- ? [
62
- { role: "system", content: system },
63
- { role: "user", content: user },
64
- ]
65
- : [{ role: "user", content: user }];
66
- }
@@ -1,38 +0,0 @@
1
- import type { Budget } from "../core/budget.js";
2
- import type { Client } from "../core/client.js";
3
- import type { RunOptions, RunResult } from "./types.js";
4
- /** Generated per attempt so a retry of the same call lands on one row. */
5
- export declare function newRolloutId(): string;
6
- /**
7
- * Runs `fn` over `items` with at most `limit` in flight.
8
- *
9
- * Bounded on purpose. Firing a whole dataset at once turns most of it into 429s
10
- * that you still wait out, so the unbounded version finishes later and flakier
11
- * than the bounded one.
12
- */
13
- export declare function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
14
- export declare function runOnce(client: Client, promptId: string, values: Record<string, unknown>, opts?: RunOptions, budget?: Budget): Promise<RunResult>;
15
- /**
16
- * k repetitions of one case, so `pass^k` is computable.
17
- *
18
- * Always returns k results. A seed that failed is present with
19
- * `status: "failed"` rather than missing: returning three results for k = 5
20
- * would have the reliability metric compute over three and report better
21
- * consistency than the run actually showed.
22
- *
23
- * A budget stop is the exception, and it throws. Money running out says nothing
24
- * about how reliable the prompt is, so recording those seeds as failures would
25
- * push a number into pass^k that is about the wallet rather than the model.
26
- *
27
- * `seed` is metadata, not a provider parameter. Anthropic exposes none and
28
- * OpenAI's is best-effort; it records which repetition a row is, which is all
29
- * pass^k needs.
30
- */
31
- export declare function sampleRuns(client: Client, promptId: string, values: Record<string, unknown>, opts?: RunOptions & {
32
- k?: number;
33
- concurrency?: number;
34
- }, budget?: Budget): Promise<RunResult[]>;
35
- /** One rollout per model — the playground's comparison, headless. */
36
- export declare function runAcrossModels(client: Client, promptId: string, values: Record<string, unknown>, models: string[], opts?: RunOptions & {
37
- concurrency?: number;
38
- }, budget?: Budget): Promise<RunResult[]>;
@@ -1,96 +0,0 @@
1
- import { BudgetExceededError } from "../core/budget.js";
2
- /** Generated per attempt so a retry of the same call lands on one row. */
3
- export function newRolloutId() {
4
- return `ro_${crypto.randomUUID().replace(/-/g, "")}`;
5
- }
6
- /**
7
- * Runs `fn` over `items` with at most `limit` in flight.
8
- *
9
- * Bounded on purpose. Firing a whole dataset at once turns most of it into 429s
10
- * that you still wait out, so the unbounded version finishes later and flakier
11
- * than the bounded one.
12
- */
13
- export async function mapLimit(items, limit, fn) {
14
- const out = new Array(items.length);
15
- let next = 0;
16
- const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
17
- while (next < items.length) {
18
- const i = next++;
19
- out[i] = await fn(items[i], i);
20
- }
21
- });
22
- await Promise.all(workers);
23
- return out;
24
- }
25
- export async function runOnce(client, promptId, values, opts = {}, budget) {
26
- budget?.assertAffordable();
27
- const rolloutId = opts.rolloutId ?? newRolloutId();
28
- const res = await client.request(`/api/v1/prompts/${encodeURIComponent(promptId)}/run`, {
29
- method: "POST",
30
- body: JSON.stringify({
31
- rolloutId,
32
- values,
33
- model: opts.model,
34
- temperature: opts.temperature,
35
- maxTokens: opts.maxTokens,
36
- seed: opts.seed ?? 0,
37
- caseId: opts.caseId,
38
- candidateId: opts.candidateId,
39
- parentId: opts.parentId,
40
- generation: opts.generation,
41
- record: opts.record ?? true,
42
- }),
43
- });
44
- // A deduped reply costs nothing new, but its cost is already in the total from
45
- // the attempt that wrote it, so counting it again would overstate the spend.
46
- if (!res.deduped)
47
- budget?.add(res.rollout.costMicros ?? 0);
48
- return res.rollout;
49
- }
50
- /**
51
- * k repetitions of one case, so `pass^k` is computable.
52
- *
53
- * Always returns k results. A seed that failed is present with
54
- * `status: "failed"` rather than missing: returning three results for k = 5
55
- * would have the reliability metric compute over three and report better
56
- * consistency than the run actually showed.
57
- *
58
- * A budget stop is the exception, and it throws. Money running out says nothing
59
- * about how reliable the prompt is, so recording those seeds as failures would
60
- * push a number into pass^k that is about the wallet rather than the model.
61
- *
62
- * `seed` is metadata, not a provider parameter. Anthropic exposes none and
63
- * OpenAI's is best-effort; it records which repetition a row is, which is all
64
- * pass^k needs.
65
- */
66
- export async function sampleRuns(client, promptId, values, opts = {}, budget) {
67
- const k = Math.max(1, opts.k ?? 5);
68
- const seeds = Array.from({ length: k }, (_, i) => i);
69
- return mapLimit(seeds, opts.concurrency ?? 4, async (seed) => {
70
- try {
71
- return await runOnce(client, promptId, values, { ...opts, seed, rolloutId: undefined }, budget);
72
- }
73
- catch (err) {
74
- if (err instanceof BudgetExceededError)
75
- throw err;
76
- // A transport failure is still an outcome for this seed. Throwing would
77
- // discard the k-1 results that did come back.
78
- return {
79
- id: null,
80
- model: opts.model ?? "",
81
- status: "failed",
82
- output: "",
83
- error: err.message,
84
- costMicros: 0,
85
- latencyMs: 0,
86
- inputTokens: 0,
87
- outputTokens: 0,
88
- seed,
89
- };
90
- }
91
- });
92
- }
93
- /** One rollout per model — the playground's comparison, headless. */
94
- export async function runAcrossModels(client, promptId, values, models, opts = {}, budget) {
95
- return mapLimit(models, opts.concurrency ?? 4, (model) => runOnce(client, promptId, values, { ...opts, model, rolloutId: undefined }, budget));
96
- }
@@ -1,156 +0,0 @@
1
- import type { FieldSpec } from "../core/schema/types.js";
2
- import type { Block, Message } from "./render.js";
3
- /** What `GET /api/v1/prompts/:id` returns, narrowed to what this package uses. */
4
- export interface PromptPayload {
5
- id: string;
6
- name: string;
7
- /** The handle this prompt can also be pulled by. Absent before the server
8
- * that adds it; null on a row the backfill could not slug. */
9
- slug?: string | null;
10
- blocks: Block[];
11
- question: string;
12
- models: string[];
13
- temperature: number;
14
- maxTokens: number;
15
- fieldSpec: FieldSpec[];
16
- currentVersionId: string | null;
17
- }
18
- /** A pulled prompt: its declarations, its version, and how to render it. */
19
- export interface Prompt {
20
- id: string;
21
- name: string;
22
- /** What `pull` accepts besides the id. Null when the prompt has none. */
23
- slug: string | null;
24
- fields: FieldSpec[];
25
- /** What a rollout recorded from this prompt should reference. */
26
- versionId: string | null;
27
- models: string[];
28
- /** Validates and substitutes, throwing on a missing required field. */
29
- render(values?: Record<string, unknown>): Message[];
30
- /** The serialised field map that `render` used — recorded on the rollout. */
31
- serialize(values?: Record<string, unknown>): Record<string, string>;
32
- }
33
- /** What the caller hands back after running the prompt themselves. */
34
- export interface ReportInput {
35
- /** Dedupe key. Generated if omitted; supply your own to make a retry safe. */
36
- rolloutId?: string;
37
- versionId?: string | null;
38
- model: string;
39
- fields: Record<string, string>;
40
- rendered: Message[];
41
- output: string;
42
- status?: "completed" | "failed";
43
- error?: string;
44
- inputTokens?: number;
45
- outputTokens?: number;
46
- cacheReadTokens?: number;
47
- cacheWriteTokens?: number;
48
- latencyMs?: number;
49
- /** Set only for evaluation rollouts. Production traffic leaves it unset. */
50
- caseId?: string;
51
- seed?: number;
52
- candidateId?: string;
53
- parentId?: string;
54
- generation?: number;
55
- }
56
- export interface Rollout {
57
- rolloutId: string;
58
- status: "completed" | "failed";
59
- output: string;
60
- costMicros: number;
61
- latencyMs: number;
62
- inputTokens: number;
63
- outputTokens: number;
64
- }
65
- /** Run configuration. Field values are the second argument, not part of this. */
66
- export interface RunOptions {
67
- /** Overrides the prompt's first saved model. Never silently defaulted. */
68
- model?: string;
69
- temperature?: number;
70
- maxTokens?: number;
71
- /** Which repetition this is. Metadata for pass^k, not a provider parameter. */
72
- seed?: number;
73
- /** Links the run to a dataset case; the server resolves the split from it. */
74
- caseId?: string;
75
- candidateId?: string;
76
- parentId?: string;
77
- generation?: number;
78
- /** Supply to make your own retry idempotent. Generated when omitted. */
79
- rolloutId?: string;
80
- /** false runs the model but writes nothing — for debugging, not datasets. */
81
- record?: boolean;
82
- }
83
- /**
84
- * Prompt id → the values that prompt expects.
85
- *
86
- * Empty here and filled in by the file `harness codegen` writes, through
87
- * declaration merging. Left empty, every id falls back to a loose value map —
88
- * so the package works with no codegen step, and a stale generated file
89
- * degrades to permissive rather than wrong.
90
- */
91
- export interface Prompts {
92
- }
93
- /**
94
- * The values type for a known prompt id, or a loose map for anything else.
95
- *
96
- * A conditional rather than an overload: overloads would let a call with the
97
- * wrong values silently fall through to the permissive signature, which is
98
- * exactly the mistake codegen exists to catch.
99
- */
100
- export type ValuesFor<K extends string> = K extends keyof Prompts ? Prompts[K] : Record<string, unknown>;
101
- /** One wording a prompt has had. */
102
- export interface PromptVersion {
103
- id: string;
104
- promptId: string;
105
- hash: string;
106
- blocks: Block[];
107
- question: string;
108
- fieldSpec: FieldSpec[] | null;
109
- /** "user" is a wording a person wrote; "assay" is one the optimizer proposed. */
110
- origin: "user" | "assay";
111
- candidateId: string | null;
112
- parentVersionId: string | null;
113
- /** Last time this wording was put live; null means it never has been. */
114
- servedAt?: string | null;
115
- /** How many stints it has had live. */
116
- servedCount?: number;
117
- createdAt: string;
118
- isCurrent: boolean;
119
- }
120
- export type Split = "feedback" | "held_out";
121
- /** One input the prompt is evaluated against. */
122
- export interface DatasetCase {
123
- caseId: string;
124
- fieldValues: Record<string, string>;
125
- expected?: string | null;
126
- /**
127
- * Omit it. The server derives the split from the case id so a case never moves
128
- * between sides; supply one only when importing a set that already carries it.
129
- */
130
- split?: Split;
131
- }
132
- export interface DatasetSummary {
133
- cases: (DatasetCase & {
134
- split: Split;
135
- })[];
136
- counts: {
137
- total: number;
138
- feedback: number;
139
- held_out: number;
140
- };
141
- /** Why this dataset cannot be optimized against yet, or null. */
142
- problem: string | null;
143
- }
144
- /** A rollout as the run route returns it. `id` is null for a dry run. */
145
- export interface RunResult {
146
- id: string | null;
147
- model: string;
148
- status: "completed" | "failed";
149
- output: string;
150
- error?: string | null;
151
- costMicros: number;
152
- latencyMs: number;
153
- inputTokens: number;
154
- outputTokens: number;
155
- seed?: number;
156
- }
File without changes
File without changes