@spendgraph/prompt 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 (72) hide show
  1. package/README.md +240 -0
  2. package/dist/budget/budget.d.ts +31 -0
  3. package/dist/budget/budget.js +53 -0
  4. package/dist/budget/errors.d.ts +12 -0
  5. package/dist/budget/errors.js +18 -0
  6. package/dist/budget/index.d.ts +2 -0
  7. package/dist/budget/index.js +2 -0
  8. package/dist/build/build.d.ts +19 -0
  9. package/dist/build/build.js +73 -0
  10. package/dist/build/index.d.ts +2 -0
  11. package/dist/build/index.js +1 -0
  12. package/dist/cache/cache.d.ts +118 -0
  13. package/dist/cache/cache.js +198 -0
  14. package/dist/cache/index.d.ts +1 -0
  15. package/dist/cache/index.js +1 -0
  16. package/dist/client.d.ts +147 -0
  17. package/dist/client.js +230 -0
  18. package/dist/index.d.ts +11 -0
  19. package/dist/index.js +5 -0
  20. package/dist/internals.d.ts +17 -0
  21. package/dist/internals.js +13 -0
  22. package/dist/prompt/bind.d.ts +60 -0
  23. package/dist/prompt/bind.js +21 -0
  24. package/dist/prompt/factory.d.ts +49 -0
  25. package/dist/prompt/factory.js +118 -0
  26. package/dist/prompt/index.d.ts +4 -0
  27. package/dist/prompt/index.js +2 -0
  28. package/dist/pull/index.d.ts +1 -0
  29. package/dist/pull/index.js +1 -0
  30. package/dist/pull/pull.d.ts +12 -0
  31. package/dist/pull/pull.js +31 -0
  32. package/dist/render/index.d.ts +4 -0
  33. package/dist/render/index.js +3 -0
  34. package/dist/render/messages.d.ts +12 -0
  35. package/dist/render/messages.js +24 -0
  36. package/dist/render/system.d.ts +8 -0
  37. package/dist/render/system.js +19 -0
  38. package/dist/render/types.d.ts +15 -0
  39. package/dist/render/types.js +1 -0
  40. package/dist/render/variables.d.ts +11 -0
  41. package/dist/render/variables.js +29 -0
  42. package/dist/run/across-models.d.ts +7 -0
  43. package/dist/run/across-models.js +6 -0
  44. package/dist/run/concurrency.d.ts +7 -0
  45. package/dist/run/concurrency.js +18 -0
  46. package/dist/run/id.d.ts +2 -0
  47. package/dist/run/id.js +4 -0
  48. package/dist/run/index.d.ts +5 -0
  49. package/dist/run/index.js +5 -0
  50. package/dist/run/once.d.ts +4 -0
  51. package/dist/run/once.js +22 -0
  52. package/dist/run/sample.d.ts +17 -0
  53. package/dist/run/sample.js +39 -0
  54. package/dist/types/dataset.d.ts +22 -0
  55. package/dist/types/dataset.js +1 -0
  56. package/dist/types/index.d.ts +8 -0
  57. package/dist/types/index.js +1 -0
  58. package/dist/types/payload.d.ts +16 -0
  59. package/dist/types/payload.js +1 -0
  60. package/dist/types/prompt.d.ts +73 -0
  61. package/dist/types/prompt.js +1 -0
  62. package/dist/types/result.d.ts +13 -0
  63. package/dist/types/result.js +1 -0
  64. package/dist/types/rollout.d.ts +8 -0
  65. package/dist/types/rollout.js +1 -0
  66. package/dist/types/run.d.ts +18 -0
  67. package/dist/types/run.js +1 -0
  68. package/dist/types/trace.d.ts +71 -0
  69. package/dist/types/trace.js +1 -0
  70. package/dist/types/version.d.ts +21 -0
  71. package/dist/types/version.js +1 -0
  72. package/package.json +63 -0
@@ -0,0 +1,60 @@
1
+ import type { History, Message } from "../render/index.js";
2
+ import type { Prompt, TraceOutcome } from "../types/index.js";
3
+ /**
4
+ * Anything that can answer a list of messages.
5
+ *
6
+ * Structural on purpose: `Llm` from `@spendgraph/llms` satisfies it because its
7
+ * `LlmReply` is a `TraceOutcome`, so binding one costs neither package an
8
+ * import of the other.
9
+ */
10
+ export interface Model {
11
+ call(messages: Message[], opts?: ModelOptions): Promise<TraceOutcome> | TraceOutcome;
12
+ /** Optional. `chain.stream` falls back to `call` when a model cannot stream. */
13
+ stream?(messages: Message[], opts?: ModelOptions): Promise<TraceOutcome> | TraceOutcome;
14
+ }
15
+ export interface ModelOptions {
16
+ /** The turn selected for this call — structurally a tool bus. */
17
+ tools?: unknown;
18
+ onText?: (delta: string) => void;
19
+ }
20
+ export interface InvokeOptions {
21
+ /** Prior turns, oldest first. */
22
+ history?: History;
23
+ /** A bus to select from. The turn is passed to the model and to the rollout. */
24
+ tools?: {
25
+ trace(query?: string, limit?: number): {
26
+ record(): unknown;
27
+ };
28
+ };
29
+ /** What to select tools on. Defaults to the rendered question. */
30
+ query?: string;
31
+ /** False runs the model and records nothing. */
32
+ report?: boolean;
33
+ /** Supply your own to make a retry safe. */
34
+ rolloutId?: string;
35
+ }
36
+ export interface BatchOptions extends InvokeOptions {
37
+ /** Max calls in flight. Default 4. */
38
+ concurrency?: number;
39
+ }
40
+ export type Answer = TraceOutcome & {
41
+ rolloutId: string;
42
+ };
43
+ /** A prompt with a model attached. */
44
+ export interface Chain {
45
+ /** Render, call the model, record. The one-liner. */
46
+ invoke(values?: Record<string, unknown>, opts?: InvokeOptions): Promise<Answer>;
47
+ /** The same, streamed. `onText` receives each delta as it arrives. */
48
+ stream(values?: Record<string, unknown>, opts?: InvokeOptions & {
49
+ onText?: (delta: string) => void;
50
+ }): Promise<Answer>;
51
+ /** One call per set of values, concurrency-capped. */
52
+ batch(values: Record<string, unknown>[], opts?: BatchOptions): Promise<Answer[]>;
53
+ }
54
+ /**
55
+ * Attaches a model, so the prompt can run itself.
56
+ *
57
+ * Without this you hand the call to `prompt.call`; with it the call is already
58
+ * known and `invoke` is the whole thing.
59
+ */
60
+ export declare function bind(prompt: Prompt, model: Model): Chain;
@@ -0,0 +1,21 @@
1
+ import { mapLimit } from "../run/concurrency.js";
2
+ const DEFAULT_CONCURRENCY = 4;
3
+ /**
4
+ * Attaches a model, so the prompt can run itself.
5
+ *
6
+ * Without this you hand the call to `prompt.call`; with it the call is already
7
+ * known and `invoke` is the whole thing.
8
+ */
9
+ export function bind(prompt, model) {
10
+ const once = (values, opts, streaming) => prompt.call(values, ({ messages, turn }) => {
11
+ const modelOpts = { tools: turn, onText: opts.onText };
12
+ return streaming && model.stream
13
+ ? model.stream(messages, modelOpts)
14
+ : model.call(messages, modelOpts);
15
+ }, opts);
16
+ return {
17
+ invoke: (values = {}, opts = {}) => once(values, opts, false),
18
+ stream: (values = {}, opts = {}) => once(values, opts, true),
19
+ batch: (values, opts = {}) => mapLimit(values, opts.concurrency ?? DEFAULT_CONCURRENCY, (v) => once(v, opts, false)),
20
+ };
21
+ }
@@ -0,0 +1,49 @@
1
+ import type { FieldSpec } from "@spendgraph/sdk";
2
+ import type { Block } from "../render/index.js";
3
+ import type { Prompt, PromptSource, ReportInput } from "../types/index.js";
4
+ /** Everything the factory needs about the wording, however it was obtained. */
5
+ export interface PromptShape {
6
+ id: string;
7
+ name: string;
8
+ slug: string | null;
9
+ blocks: Block[];
10
+ question: string;
11
+ fields: FieldSpec[];
12
+ versionId: string | null;
13
+ models: string[];
14
+ }
15
+ /**
16
+ * Where a finished call goes.
17
+ *
18
+ * A server prompt writes a rollout; a custom one has no row to hang one on and
19
+ * writes usage instead. Injecting it is what lets both constructors share one
20
+ * `trace`, so the two can never drift on what "recorded" means.
21
+ */
22
+ export interface Recorder {
23
+ enabled: boolean;
24
+ /**
25
+ * Records one rollout.
26
+ *
27
+ * May return what the server priced it at, in micro-USD. Callers that do not
28
+ * wait for it are unaffected — the write is still fire-and-forget, and the
29
+ * promise is only there for `call({ awaitCost: true })` to take.
30
+ */
31
+ write(promptId: string, entry: ReportInput): void | Promise<number | undefined>;
32
+ }
33
+ export declare const noRecorder: Recorder;
34
+ /**
35
+ * What the rollout route accepts in one record.
36
+ *
37
+ * A conversation longer than this still runs — only its rollout is refused, and
38
+ * that refusal reaches `onReportError` rather than the caller, so it is said
39
+ * here instead while there is still somebody watching.
40
+ */
41
+ export declare const MAX_RECORDED_MESSAGES = 50;
42
+ /**
43
+ * The `Prompt` both constructors return.
44
+ *
45
+ * `pullPrompt` and `buildCustomPrompt` differ only in where the wording came
46
+ * from and what the recorder does with the result — everything a caller
47
+ * touches is built here, once.
48
+ */
49
+ export declare function makePrompt(shape: PromptShape, source: PromptSource, recorder?: Recorder): Prompt;
@@ -0,0 +1,118 @@
1
+ import { serializeFields } from "@spendgraph/sdk";
2
+ import { renderMessages } from "../render/index.js";
3
+ import { newRolloutId } from "../run/id.js";
4
+ import { bind } from "./bind.js";
5
+ export const noRecorder = {
6
+ enabled: false,
7
+ write: () => { },
8
+ };
9
+ /**
10
+ * What the rollout route accepts in one record.
11
+ *
12
+ * A conversation longer than this still runs — only its rollout is refused, and
13
+ * that refusal reaches `onReportError` rather than the caller, so it is said
14
+ * here instead while there is still somebody watching.
15
+ */
16
+ export const MAX_RECORDED_MESSAGES = 50;
17
+ let warnedAboutLength = false;
18
+ function warnIfUnrecordable(messages) {
19
+ if (warnedAboutLength || messages.length <= MAX_RECORDED_MESSAGES)
20
+ return;
21
+ warnedAboutLength = true;
22
+ console.warn(`[prompt] ${messages.length} messages is over the ${MAX_RECORDED_MESSAGES} a rollout ` +
23
+ "can record. The call still runs; the rollout will be rejected. Trim the history you pass.");
24
+ }
25
+ function lastUserMessage(messages) {
26
+ for (let i = messages.length - 1; i >= 0; i--) {
27
+ if (messages[i].role === "user")
28
+ return messages[i].content;
29
+ }
30
+ return "";
31
+ }
32
+ /**
33
+ * The `Prompt` both constructors return.
34
+ *
35
+ * `pullPrompt` and `buildCustomPrompt` differ only in where the wording came
36
+ * from and what the recorder does with the result — everything a caller
37
+ * touches is built here, once.
38
+ */
39
+ export function makePrompt(shape, source, recorder = noRecorder) {
40
+ const serialize = (values = {}) => serializeFields(values, shape.fields);
41
+ const format = (values = {}, opts = {}) => renderMessages(shape.blocks, shape.question, serialize(values), opts.history);
42
+ const built = {
43
+ id: shape.id,
44
+ name: shape.name,
45
+ slug: shape.slug,
46
+ fields: shape.fields,
47
+ versionId: shape.versionId,
48
+ models: shape.models,
49
+ source,
50
+ serialize,
51
+ format,
52
+ async call(values, run, opts = {}) {
53
+ const messages = format(values, { history: opts.history });
54
+ warnIfUnrecordable(messages);
55
+ const turn = opts.tools?.trace(opts.query ?? lastUserMessage(messages));
56
+ const rolloutId = opts.rolloutId ?? newRolloutId();
57
+ const recording = recorder.enabled && (opts.report ?? true);
58
+ const startedAt = Date.now();
59
+ const sent = {
60
+ rolloutId,
61
+ versionId: shape.versionId,
62
+ fields: serialize(values),
63
+ rendered: messages,
64
+ };
65
+ let outcome;
66
+ try {
67
+ outcome = await run({ messages, turn });
68
+ }
69
+ catch (err) {
70
+ if (recording) {
71
+ const recorded = turn?.record();
72
+ recorder.write(shape.id, {
73
+ ...sent,
74
+ model: "unknown",
75
+ output: "",
76
+ status: "failed",
77
+ error: err instanceof Error ? err.message : String(err),
78
+ latencyMs: Date.now() - startedAt,
79
+ offeredTools: recorded?.offeredTools,
80
+ steps: recorded?.steps,
81
+ });
82
+ }
83
+ throw err;
84
+ }
85
+ const latencyMs = Date.now() - startedAt;
86
+ let pricing;
87
+ if (recording) {
88
+ const recorded = turn?.record();
89
+ const written = recorder.write(shape.id, {
90
+ ...sent,
91
+ model: outcome.model,
92
+ output: outcome.output,
93
+ status: outcome.status ?? "completed",
94
+ error: outcome.error,
95
+ inputTokens: outcome.inputTokens,
96
+ outputTokens: outcome.outputTokens,
97
+ cacheReadTokens: outcome.cacheReadTokens,
98
+ cacheWriteTokens: outcome.cacheWriteTokens,
99
+ citationTokens: outcome.citationTokens,
100
+ reasoningTokens: outcome.reasoningTokens,
101
+ latencyMs,
102
+ caseId: outcome.caseId,
103
+ seed: outcome.seed,
104
+ offeredTools: outcome.offeredTools ?? recorded?.offeredTools,
105
+ steps: outcome.steps ?? recorded?.steps,
106
+ });
107
+ // Not awaited. The write stays fire-and-forget — a stage that stalled
108
+ // on a report round trip would pay it fifteen times over a run — and
109
+ // the promise is handed back so a caller can settle every price at the
110
+ // end, when the total is what it needs.
111
+ pricing = written instanceof Promise ? written : undefined;
112
+ }
113
+ return { ...outcome, rolloutId, ...(pricing ? { pricing } : {}) };
114
+ },
115
+ bind: (model) => bind(built, model),
116
+ };
117
+ return built;
118
+ }
@@ -0,0 +1,4 @@
1
+ export type { Answer, BatchOptions, Chain, InvokeOptions, Model, ModelOptions, } from "./bind.js";
2
+ export { bind } from "./bind.js";
3
+ export type { PromptShape, Recorder } from "./factory.js";
4
+ export { MAX_RECORDED_MESSAGES, makePrompt, noRecorder } from "./factory.js";
@@ -0,0 +1,2 @@
1
+ export { bind } from "./bind.js";
2
+ export { MAX_RECORDED_MESSAGES, makePrompt, noRecorder } from "./factory.js";
@@ -0,0 +1 @@
1
+ export { pullWithCache } from "./pull.js";
@@ -0,0 +1 @@
1
+ export { pullWithCache } from "./pull.js";
@@ -0,0 +1,12 @@
1
+ import type { PullCache } from "../cache/index.js";
2
+ /**
3
+ * Fetch-by-id with stale-while-revalidate.
4
+ *
5
+ * fresh → return it, no request
6
+ * stale → return it now, refresh behind the caller
7
+ * miss → fetch, and only then answer
8
+ *
9
+ * The middle path is why a prompt edit takes effect within a TTL without any
10
+ * request paying for the fetch. Generic because tools and graphs pull the same.
11
+ */
12
+ export declare function pullWithCache<T>(cache: PullCache<T>, key: string, fetchOne: () => Promise<T>): Promise<T>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Fetch-by-id with stale-while-revalidate.
3
+ *
4
+ * fresh → return it, no request
5
+ * stale → return it now, refresh behind the caller
6
+ * miss → fetch, and only then answer
7
+ *
8
+ * The middle path is why a prompt edit takes effect within a TTL without any
9
+ * request paying for the fetch. Generic because tools and graphs pull the same.
10
+ */
11
+ export async function pullWithCache(cache, key, fetchOne) {
12
+ const hit = cache.get(key);
13
+ if (hit && !hit.stale)
14
+ return hit.value;
15
+ if (hit) {
16
+ cache.refreshInBackground(key, fetchOne);
17
+ return hit.value;
18
+ }
19
+ // Guarded like the background refresh: an invalidation can land mid-fetch,
20
+ // and storing afterwards marks pre-invalidation state fresh for a whole TTL.
21
+ const startedAt = cache.beginFetch();
22
+ try {
23
+ const value = await fetchOne();
24
+ cache.settle(key, value, startedAt);
25
+ return value;
26
+ }
27
+ catch (err) {
28
+ cache.abandon(key);
29
+ throw err;
30
+ }
31
+ }
@@ -0,0 +1,4 @@
1
+ export { renderMessages } from "./messages.js";
2
+ export { compileSystemPrompt } from "./system.js";
3
+ export type { Block, History, Message } from "./types.js";
4
+ export { applyVariables, findVariables } from "./variables.js";
@@ -0,0 +1,3 @@
1
+ export { renderMessages } from "./messages.js";
2
+ export { compileSystemPrompt } from "./system.js";
3
+ export { applyVariables, findVariables } from "./variables.js";
@@ -0,0 +1,12 @@
1
+ import type { Block, History, Message } from "./types.js";
2
+ /**
3
+ * The messages a request will send.
4
+ *
5
+ * A prompt with no blocks sends one message, not two: an empty system message is
6
+ * a different request from no system message at all.
7
+ *
8
+ * `history` is placed between the system turn and the question, oldest first,
9
+ * and is passed through verbatim — it is a record of what was already said, not
10
+ * a template, so `{placeholders}` inside it are left alone.
11
+ */
12
+ export declare function renderMessages(blocks: Block[], question: string, values?: Record<string, string>, history?: History): Message[];
@@ -0,0 +1,24 @@
1
+ import { compileSystemPrompt } from "./system.js";
2
+ import { applyVariables } from "./variables.js";
3
+ /**
4
+ * The messages a request will send.
5
+ *
6
+ * A prompt with no blocks sends one message, not two: an empty system message is
7
+ * a different request from no system message at all.
8
+ *
9
+ * `history` is placed between the system turn and the question, oldest first,
10
+ * and is passed through verbatim — it is a record of what was already said, not
11
+ * a template, so `{placeholders}` inside it are left alone.
12
+ */
13
+ export function renderMessages(blocks, question, values, history = []) {
14
+ const system = compileSystemPrompt(blocks, values);
15
+ const user = applyVariables(question, values);
16
+ const turns = history.filter((m) => m.role !== "system");
17
+ return system
18
+ ? [
19
+ { role: "system", content: system },
20
+ ...turns,
21
+ { role: "user", content: user },
22
+ ]
23
+ : [...turns, { role: "user", content: user }];
24
+ }
@@ -0,0 +1,8 @@
1
+ import type { Block } from "./types.js";
2
+ /**
3
+ * Blocks in order, each under its title.
4
+ *
5
+ * An empty body drops the block — a heading over nothing is noise. An untitled
6
+ * block is emitted bare rather than under a blank `##`.
7
+ */
8
+ export declare function compileSystemPrompt(blocks: Block[], values?: Record<string, string>): string;
@@ -0,0 +1,19 @@
1
+ import { applyVariables } from "./variables.js";
2
+ /**
3
+ * Blocks in order, each under its title.
4
+ *
5
+ * An empty body drops the block — a heading over nothing is noise. An untitled
6
+ * block is emitted bare rather than under a blank `##`.
7
+ */
8
+ export function compileSystemPrompt(blocks, values) {
9
+ const out = [];
10
+ for (const b of blocks ?? []) {
11
+ const body = b.body?.trim();
12
+ if (!body)
13
+ continue;
14
+ const title = b.title?.trim();
15
+ const text = applyVariables(body, values);
16
+ out.push(title ? `## ${applyVariables(title, values)}\n${text}` : text);
17
+ }
18
+ return out.join("\n\n");
19
+ }
@@ -0,0 +1,15 @@
1
+ /** One titled section of a prompt. */
2
+ export type Block = {
3
+ title: string;
4
+ body: string;
5
+ };
6
+ /**
7
+ * One turn. `assistant` appears only in conversation history — a prompt's own
8
+ * wording still renders a system turn and a user turn, never an answer.
9
+ */
10
+ export type Message = {
11
+ role: "system" | "user" | "assistant";
12
+ content: string;
13
+ };
14
+ /** Prior turns, oldest first, placed between the system turn and the question. */
15
+ export type History = Message[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { Block } from "./types.js";
2
+ /**
3
+ * Substitutes `{name}` placeholders, leaving unknown ones verbatim — emptying
4
+ * one would send a prompt with a hole the caller cannot see.
5
+ *
6
+ * Mirrors `lib/prompt.ts` in the app. Duplicated because this package depends on
7
+ * nothing; `rendered` on every rollout makes a divergence visible.
8
+ */
9
+ export declare function applyVariables(text: string, values: Record<string, string> | undefined): string;
10
+ /** Placeholder names used anywhere in the prompt, in first-seen order. */
11
+ export declare function findVariables(blocks: Block[], question: string): string[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Substitutes `{name}` placeholders, leaving unknown ones verbatim — emptying
3
+ * one would send a prompt with a hole the caller cannot see.
4
+ *
5
+ * Mirrors `lib/prompt.ts` in the app. Duplicated because this package depends on
6
+ * nothing; `rendered` on every rollout makes a divergence visible.
7
+ */
8
+ export function applyVariables(text, values) {
9
+ if (!text || !values)
10
+ return text;
11
+ return text.replace(/\{(\w+)\}/g, (whole, name) => name in values ? values[name] : whole);
12
+ }
13
+ /** Placeholder names used anywhere in the prompt, in first-seen order. */
14
+ export function findVariables(blocks, question) {
15
+ const seen = [];
16
+ const scan = (text) => {
17
+ for (const m of text.matchAll(/\{(\w+)\}/g)) {
18
+ if (!seen.includes(m[1]))
19
+ seen.push(m[1]);
20
+ }
21
+ };
22
+ // titles too — a heading can carry a placeholder as readily as a body
23
+ for (const b of blocks ?? []) {
24
+ scan(b.title ?? "");
25
+ scan(b.body ?? "");
26
+ }
27
+ scan(question);
28
+ return seen;
29
+ }
@@ -0,0 +1,7 @@
1
+ import type { Prompts } from "@spendgraph/sdk";
2
+ import type { Budget } from "../budget/index.js";
3
+ import type { RunOptions, RunResult } from "../types/index.js";
4
+ /** One rollout per model — the playground's comparison, headless. */
5
+ export declare function runAcrossModels(prompts: Prompts, promptId: string, values: Record<string, unknown>, models: string[], opts?: RunOptions & {
6
+ concurrency?: number;
7
+ }, budget?: Budget): Promise<RunResult[]>;
@@ -0,0 +1,6 @@
1
+ import { mapLimit } from "./concurrency.js";
2
+ import { runOnce } from "./once.js";
3
+ /** One rollout per model — the playground's comparison, headless. */
4
+ export async function runAcrossModels(prompts, promptId, values, models, opts = {}, budget) {
5
+ return mapLimit(models, opts.concurrency ?? 4, (model) => runOnce(prompts, promptId, values, { ...opts, model, rolloutId: undefined }, budget));
6
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs `fn` over `items`, at most `limit` in flight.
3
+ *
4
+ * Bounded on purpose: a whole dataset at once becomes 429s you still wait out,
5
+ * so the unbounded version finishes later and flakier.
6
+ */
7
+ export declare function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Runs `fn` over `items`, at most `limit` in flight.
3
+ *
4
+ * Bounded on purpose: a whole dataset at once becomes 429s you still wait out,
5
+ * so the unbounded version finishes later and flakier.
6
+ */
7
+ export async function mapLimit(items, limit, fn) {
8
+ const out = new Array(items.length);
9
+ let next = 0;
10
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
11
+ while (next < items.length) {
12
+ const i = next++;
13
+ out[i] = await fn(items[i], i);
14
+ }
15
+ });
16
+ await Promise.all(workers);
17
+ return out;
18
+ }
@@ -0,0 +1,2 @@
1
+ /** Generated per attempt so a retry of the same call lands on one row. */
2
+ export declare function newRolloutId(): string;
package/dist/run/id.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Generated per attempt so a retry of the same call lands on one row. */
2
+ export function newRolloutId() {
3
+ return `ro_${crypto.randomUUID().replace(/-/g, "")}`;
4
+ }
@@ -0,0 +1,5 @@
1
+ export { runAcrossModels } from "./across-models.js";
2
+ export { mapLimit } from "./concurrency.js";
3
+ export { newRolloutId } from "./id.js";
4
+ export { runOnce } from "./once.js";
5
+ export { sampleRuns } from "./sample.js";
@@ -0,0 +1,5 @@
1
+ export { runAcrossModels } from "./across-models.js";
2
+ export { mapLimit } from "./concurrency.js";
3
+ export { newRolloutId } from "./id.js";
4
+ export { runOnce } from "./once.js";
5
+ export { sampleRuns } from "./sample.js";
@@ -0,0 +1,4 @@
1
+ import type { Prompts } from "@spendgraph/sdk";
2
+ import type { Budget } from "../budget/index.js";
3
+ import type { RunOptions, RunResult } from "../types/index.js";
4
+ export declare function runOnce(prompts: Prompts, promptId: string, values: Record<string, unknown>, opts?: RunOptions, budget?: Budget): Promise<RunResult>;
@@ -0,0 +1,22 @@
1
+ import { newRolloutId } from "./id.js";
2
+ export async function runOnce(prompts, promptId, values, opts = {}, budget) {
3
+ budget?.assertAffordable();
4
+ const rolloutId = opts.rolloutId ?? newRolloutId();
5
+ const res = await prompts.run(promptId, {
6
+ rolloutId,
7
+ values,
8
+ model: opts.model,
9
+ temperature: opts.temperature,
10
+ maxTokens: opts.maxTokens,
11
+ seed: opts.seed ?? 0,
12
+ caseId: opts.caseId,
13
+ candidateId: opts.candidateId,
14
+ parentId: opts.parentId,
15
+ generation: opts.generation,
16
+ record: opts.record ?? true,
17
+ });
18
+ // A deduped reply costs nothing new — the attempt that wrote it already paid.
19
+ if (!res.deduped)
20
+ budget?.add(res.rollout.costMicros ?? 0);
21
+ return res.rollout;
22
+ }
@@ -0,0 +1,17 @@
1
+ import type { Prompts } from "@spendgraph/sdk";
2
+ import type { Budget } from "../budget/index.js";
3
+ import type { RunOptions, RunResult } from "../types/index.js";
4
+ /**
5
+ * k repetitions of one case, so `pass^k` is computable.
6
+ *
7
+ * Always k results. A failed seed is present with `status: "failed"` rather than
8
+ * missing — three results for k = 5 would have the metric compute over three and
9
+ * report better consistency than the run showed.
10
+ *
11
+ * A budget stop throws instead. Money running out says nothing about the prompt,
12
+ * and recording those seeds as failures would put the wallet into pass^k.
13
+ */
14
+ export declare function sampleRuns(prompts: Prompts, promptId: string, values: Record<string, unknown>, opts?: RunOptions & {
15
+ k?: number;
16
+ concurrency?: number;
17
+ }, budget?: Budget): Promise<RunResult[]>;
@@ -0,0 +1,39 @@
1
+ import { BudgetExceededError } from "../budget/index.js";
2
+ import { mapLimit } from "./concurrency.js";
3
+ import { runOnce } from "./once.js";
4
+ /**
5
+ * k repetitions of one case, so `pass^k` is computable.
6
+ *
7
+ * Always k results. A failed seed is present with `status: "failed"` rather than
8
+ * missing — three results for k = 5 would have the metric compute over three and
9
+ * report better consistency than the run showed.
10
+ *
11
+ * A budget stop throws instead. Money running out says nothing about the prompt,
12
+ * and recording those seeds as failures would put the wallet into pass^k.
13
+ */
14
+ export async function sampleRuns(prompts, promptId, values, opts = {}, budget) {
15
+ const k = Math.max(1, opts.k ?? 5);
16
+ const seeds = Array.from({ length: k }, (_, i) => i);
17
+ return mapLimit(seeds, opts.concurrency ?? 4, async (seed) => {
18
+ try {
19
+ return await runOnce(prompts, promptId, values, { ...opts, seed, rolloutId: undefined }, budget);
20
+ }
21
+ catch (err) {
22
+ if (err instanceof BudgetExceededError)
23
+ throw err;
24
+ // Still an outcome for this seed; throwing discards the k-1 that landed.
25
+ return {
26
+ id: null,
27
+ model: opts.model ?? "",
28
+ status: "failed",
29
+ output: "",
30
+ error: err.message,
31
+ costMicros: 0,
32
+ latencyMs: 0,
33
+ inputTokens: 0,
34
+ outputTokens: 0,
35
+ seed,
36
+ };
37
+ }
38
+ });
39
+ }
@@ -0,0 +1,22 @@
1
+ /** Which side of the wall a case sits on. The server decides, never the client. */
2
+ export type Split = "feedback" | "held_out";
3
+ /** One input the prompt is evaluated against. */
4
+ export interface DatasetCase {
5
+ caseId: string;
6
+ fieldValues: Record<string, string>;
7
+ expected?: string | null;
8
+ /** Omit it. The server derives the split, so a case never moves sides. */
9
+ split?: Split;
10
+ }
11
+ export interface DatasetSummary {
12
+ cases: (DatasetCase & {
13
+ split: Split;
14
+ })[];
15
+ counts: {
16
+ total: number;
17
+ feedback: number;
18
+ held_out: number;
19
+ };
20
+ /** Why this dataset cannot be optimized against yet, or null. */
21
+ problem: string | null;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export type { DatasetCase, DatasetSummary, Split } from "./dataset.js";
2
+ export type { PromptPayload } from "./payload.js";
3
+ export type { Prompt, PromptSource, PromptSpec, RenderOptions } from "./prompt.js";
4
+ export type { RunResult } from "./result.js";
5
+ export type { ReportInput, Rollout } from "./rollout.js";
6
+ export type { RunOptions } from "./run.js";
7
+ export type { Recordable, TraceHandle, TraceOptions, TraceOutcome, TurnRecord, TurnSource, } from "./trace.js";
8
+ export type { PromptVersion } from "./version.js";
@@ -0,0 +1 @@
1
+ export {};