anpord 0.1.3 → 0.1.4

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.
package/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # anpord
2
2
 
3
- TypeScript SDK for [Anpord](https://anpord.com) customer configuration for AI products.
4
-
5
- > **Placeholder release.** This version reserves the package name. There is no
6
- > client yet — the API surface lands in a future release. Watch this space.
3
+ TypeScript SDK for [Anpord](https://anpord.com). Run coding agent evals across harnesses, models, and sandboxes, and manage versioned prompts from the same client.
7
4
 
8
5
  ## Install
9
6
 
@@ -11,6 +8,74 @@ TypeScript SDK for [Anpord](https://anpord.com) — customer configuration for A
11
8
  npm install anpord
12
9
  ```
13
10
 
11
+ ## Run an eval
12
+
13
+ ```ts
14
+ import { Anpord } from "anpord";
15
+
16
+ const anpord = new Anpord({ apiKey: process.env.ANPORD_API_KEY });
17
+
18
+ const run = await anpord.evals.startAndWait({
19
+ cases: [
20
+ {
21
+ name: "writes the requested file",
22
+ variables: { task: "Create hello.txt containing exactly hello" },
23
+ verify: "test \"$(cat hello.txt)\" = hello",
24
+ },
25
+ ],
26
+ prompt: "{{task}}",
27
+ tasks: [{ harness: "codex", model: "gpt-5.6-sol", provider: "daytona" }],
28
+ trials: 3,
29
+ });
30
+
31
+ for (const cell of run.cells) {
32
+ console.log(cell.caseName, cell.distribution?.passRate);
33
+ }
34
+ ```
35
+
36
+ For a TypeScript validator, export the function from its own file:
37
+
38
+ ```ts
39
+ import type { Validator } from "anpord";
40
+
41
+ export const hasGreeting: Validator = async ({ readText }) =>
42
+ (await readText("hello.txt")) === "hello";
43
+ ```
44
+
45
+ Reference it directly from `anpord.eval.ts`:
46
+
47
+ ```ts
48
+ import { defineEval } from "anpord";
49
+ import { hasGreeting } from "./validators/greeting";
50
+
51
+ export default defineEval({
52
+ name: "greeting",
53
+ cases: [
54
+ {
55
+ name: "greeting",
56
+ variables: { task: "Write hello.txt" },
57
+ validate: hasGreeting,
58
+ },
59
+ ],
60
+ prompt: "{{task}}",
61
+ tasks: [{ harness: "codex", model: "gpt-5.6-sol", provider: "daytona" }],
62
+ trials: 3,
63
+ });
64
+ ```
65
+
66
+ ```sh
67
+ npx anpord eval
68
+ ```
69
+
70
+ ## Resolve a prompt
71
+
72
+ ```ts
73
+ const prompt = await anpord.prompts.get({ id: "support-reply" });
74
+ console.log(prompt.content, prompt.version);
75
+ ```
76
+
77
+ See the [documentation](https://docs.anpord.com) for eval concepts, prompt releases, SDK methods, and the API reference.
78
+
14
79
  ## License
15
80
 
16
81
  MIT
package/dist/cli.cjs CHANGED
@@ -1,20 +1,200 @@
1
1
  #!/usr/bin/env node
2
- const require_client = require("./client-Cno4LwRq.cjs");
2
+ const require_client = require("./client-BCZEcfVE.cjs");
3
3
  const require_config = require("./config.cjs");
4
- const require_errors = require("./errors-C0E5A8fN.cjs");
4
+ const require_errors = require("./errors-Boum34f5.cjs");
5
+ const require_compiler = require("./compiler-CZoX4jkx.cjs");
5
6
  let _effect_platform = require("@effect/platform");
6
7
  let effect = require("effect");
7
8
  let _effect_cli = require("@effect/cli");
8
9
  let _effect_platform_node = require("@effect/platform-node");
10
+ //#region package.json
11
+ var version$1 = "0.1.4";
12
+ //#endregion
13
+ //#region ../template/src/extract.ts
14
+ /**
15
+ * Distinct variable names in source order, so callers can count or list them.
16
+ *
17
+ * Escaped braces are read here too, so a name the renderer will treat as
18
+ * literal text is never reported as a variable the editor should draw.
19
+ */
20
+ function extractVariables(template) {
21
+ const names = [];
22
+ for (const [, open, close, name] of template.matchAll(require_errors.tokenMatcher())) if (open === void 0 && close === void 0 && name !== void 0) names.push(name);
23
+ return [...new Set(names)];
24
+ }
25
+ //#endregion
26
+ //#region src/cli/declarations.ts
27
+ /**
28
+ * The generated file has to open by importing the module it augments.
29
+ *
30
+ * Without it `declare module` shadows the module rather than adding to it, and
31
+ * every import of the SDK stops resolving with an error naming the missing
32
+ * export rather than the file that hid it.
33
+ */
34
+ const PREAMBLE = [
35
+ "// Generated by `anpord generate`. Do not edit.",
36
+ "// Run it again after changing which variables a prompt uses.",
37
+ "import \"anpord\";",
38
+ "",
39
+ "declare module \"anpord\" {",
40
+ " interface AnpordPromptVariables {"
41
+ ].join("\n");
42
+ /** Quoted, because an id may carry characters an identifier cannot. */
43
+ const entry = (id, names) => {
44
+ return ` "${id}": ${names.length === 0 ? "Record<string, never>" : `{ ${names.map((name) => `"${name}": string`).join("; ")} }`};`;
45
+ };
46
+ const declarationFile = (prompts) => [
47
+ PREAMBLE,
48
+ ...[...prompts].sort(([left], [right]) => left.localeCompare(right)).map(([id, names]) => entry(id, names)),
49
+ " }",
50
+ "}",
51
+ ""
52
+ ].join("\n");
53
+ //#endregion
54
+ //#region src/cli/eval-files.ts
55
+ const SUFFIX = ".eval.ts";
56
+ const SKIPPED = /* @__PURE__ */ new Set([
57
+ "node_modules",
58
+ "dist",
59
+ "build",
60
+ ".git"
61
+ ]);
62
+ const hidden = (segment) => segment.startsWith(".");
63
+ const wanted = (path) => {
64
+ const segments = path.split("/");
65
+ return path.endsWith(SUFFIX) && segments.every((segment) => !(SKIPPED.has(segment) || hidden(segment)));
66
+ };
67
+ const evalFilesIn = (directory) => effect.Effect.gen(function* () {
68
+ return (yield* (yield* _effect_platform.FileSystem.FileSystem).readDirectory(directory, { recursive: true })).filter(wanted).sort();
69
+ }).pipe(effect.Effect.withSpan("Cli.evalFilesIn"));
70
+ //#endregion
71
+ //#region src/cli/eval-gate.ts
72
+ const regressions = (run) => run.cells.filter((cell) => cell.comparison?.verdict === "regressed");
73
+ const unscored = (run) => run.cells.filter((cell) => (cell.distribution?.scored ?? 0) === 0);
74
+ const problemsWith = (run, failOn) => {
75
+ if (run.status === "failed") return [run.failure ?? "The run failed."];
76
+ if (failOn === "never") return [];
77
+ const found = regressions(run).map((cell) => `${cell.caseName} regressed against its baseline.`);
78
+ return failOn === "unscored" ? [...found, ...unscored(run).map((cell) => `${cell.caseName} produced no scored trials.`)] : found;
79
+ };
80
+ const failWhen = (problems) => problems.length === 0 ? effect.Effect.void : effect.Effect.fail(new EvalGateFailed({ problems }));
81
+ var EvalGateFailed = class extends effect.Data.TaggedError("EvalGateFailed") {
82
+ get message() {
83
+ return this.problems.join("\n");
84
+ }
85
+ };
86
+ var NoEvalFiles = class extends effect.Data.TaggedError("NoEvalFiles") {
87
+ get message() {
88
+ return "No *.eval.ts file here. Name one, or pass a file to run.";
89
+ }
90
+ };
91
+ //#endregion
9
92
  //#region src/cli/render.ts
10
93
  const json = (value) => effect.Console.log(JSON.stringify(value, null, 2));
11
94
  const promptContent = (prompt) => effect.Effect.sync(() => {
12
95
  process.stdout.write(prompt.content);
13
96
  if (!prompt.content.endsWith("\n")) process.stdout.write("\n");
14
97
  });
98
+ /** A result the caller may pipe into another tool, so it belongs on stdout
99
+ * alongside the prompt content rather than beside the status messages. */
100
+ const row = (line) => effect.Effect.sync(() => {
101
+ process.stdout.write(`${line}\n`);
102
+ });
15
103
  const note = (message) => effect.Effect.sync(() => {
16
104
  process.stderr.write(`${message}\n`);
17
105
  });
106
+ const attended = effect.Effect.sync(() => globalThis.process?.stdout?.isTTY === true);
107
+ //#endregion
108
+ //#region src/cli/eval-grid.ts
109
+ const DIM = "\x1B[2m";
110
+ const BOLD = "\x1B[1m";
111
+ const GREEN = "\x1B[32m";
112
+ const RED = "\x1B[31m";
113
+ const YELLOW = "\x1B[33m";
114
+ const RESET = "\x1B[0m";
115
+ const DONE = "●";
116
+ const RUNNING = "◐";
117
+ const FILLED = "▰";
118
+ const HOLLOW = "▱";
119
+ const PERCENT = 100;
120
+ const SECONDS = 1e3;
121
+ const MINUTE = 60;
122
+ const paint = (colour, text) => `${colour}${text}${RESET}`;
123
+ const elapsedOf = (ms) => {
124
+ const total = Math.floor(ms / SECONDS);
125
+ const minutes = Math.floor(total / MINUTE);
126
+ return minutes === 0 ? `${total}s` : `${minutes}m${String(total % MINUTE).padStart(2, "0")}s`;
127
+ };
128
+ const markOf = (cell) => {
129
+ if (cell.status === "finished") return paint(GREEN, DONE);
130
+ return cell.status === "failed" ? paint(RED, DONE) : paint(YELLOW, RUNNING);
131
+ };
132
+ const trialsOf = (cell, trials) => {
133
+ const settled = cell.trials.filter((trial) => trial.status !== "queued" && trial.status !== "running").length;
134
+ return `${FILLED.repeat(settled)}${paint(DIM, HOLLOW.repeat(Math.max(0, trials - settled)))}`;
135
+ };
136
+ const rateOf = (cell) => {
137
+ const rate = cell.distribution?.passRate;
138
+ if (rate === void 0 || cell.distribution?.scored === 0) return paint(DIM, "—");
139
+ const shown = `${Math.round(rate * PERCENT)}%`;
140
+ return paint(rate === 1 ? GREEN : RED, shown);
141
+ };
142
+ const variantOf = (run, cell) => {
143
+ const task = run.tasks[cell.taskIndex];
144
+ return task === void 0 ? "?" : `${task.harness}/${task.model}`;
145
+ };
146
+ const widest = (run) => run.cells.reduce((width, cell) => Math.max(width, variantOf(run, cell).length), 0);
147
+ const gridOf = (run, trials, elapsedMs) => {
148
+ const width = widest(run);
149
+ const lines = [];
150
+ for (const caseName of run.cases) {
151
+ lines.push(` ${BOLD}${caseName}${RESET}`);
152
+ for (const cell of run.cells.filter((one) => one.caseName === caseName)) lines.push(` ${markOf(cell)} ${variantOf(run, cell).padEnd(width)} ${trialsOf(cell, trials)} ${rateOf(cell)}`);
153
+ lines.push("");
154
+ }
155
+ lines.push(paint(DIM, ` ${elapsedOf(elapsedMs)} elapsed`));
156
+ return lines;
157
+ };
158
+ const up = (rows) => `[${rows}A`;
159
+ const liveGrid = (trials, interactive) => effect.Effect.gen(function* () {
160
+ const drawn = yield* effect.Ref.make(0);
161
+ return (run, elapsedMs) => effect.Effect.gen(function* () {
162
+ if (!interactive) return;
163
+ const rows = yield* effect.Ref.getAndSet(drawn, 0);
164
+ const lines = gridOf(run, trials, elapsedMs);
165
+ yield* note(`${rows === 0 ? "" : up(rows)}${lines.join("\n")}`);
166
+ yield* effect.Ref.set(drawn, lines.length);
167
+ });
168
+ });
169
+ const summaryOf = (run, trials, drawn) => drawn ? "" : gridOf(run, trials, 0).join("\n");
170
+ //#endregion
171
+ //#region src/cli/eval-run.ts
172
+ const FIRST_POLL = 2e3;
173
+ const SLOWEST_POLL = 1e4;
174
+ const WIDENING = 1.5;
175
+ const running = ({ run }) => run === null || run.status === "running";
176
+ const widen = (gap) => Math.min(Math.round(gap * WIDENING), SLOWEST_POLL);
177
+ const waitForRun = (id, onProgress) => effect.Effect.gen(function* () {
178
+ const api = yield* require_client.AnpordApi;
179
+ const startedAt = yield* effect.Clock.currentTimeMillis;
180
+ const step = ({ gap, run }) => effect.Effect.gen(function* () {
181
+ if (run !== null) yield* effect.Effect.sleep(effect.Duration.millis(gap));
182
+ const next = yield* api.evals.get({ payload: { id } });
183
+ yield* onProgress(next, (yield* effect.Clock.currentTimeMillis) - startedAt);
184
+ return {
185
+ gap: widen(gap),
186
+ run: next
187
+ };
188
+ });
189
+ const { run } = yield* effect.Effect.iterate({
190
+ gap: FIRST_POLL,
191
+ run: null
192
+ }, {
193
+ body: step,
194
+ while: running
195
+ });
196
+ return run ?? (yield* api.evals.get({ payload: { id } }));
197
+ }).pipe(effect.Effect.withSpan("Cli.waitForRun", { attributes: { runId: id } }));
18
198
  //#endregion
19
199
  //#region src/cli/commands.ts
20
200
  const promptId = _effect_cli.Args.text({ name: "id" }).pipe(_effect_cli.Args.withDescription("The prompt's id, such as support-reply"), _effect_cli.Args.withSchema(require_client.PromptId));
@@ -38,7 +218,7 @@ const get = _effect_cli.Command.make("get", {
38
218
  const list = _effect_cli.Command.make("list", { asJson }, ({ asJson: wantsJson }) => effect.Effect.gen(function* () {
39
219
  const { data } = yield* (yield* require_client.AnpordApi).prompts.list({ payload: {} });
40
220
  if (wantsJson) return yield* json(data);
41
- return yield* effect.Effect.forEach(data, (row) => note(`${row.id}\tv${row.latestVersion ?? "-"}\t${row.name}`));
221
+ return yield* effect.Effect.forEach(data, (summary) => row(`${summary.id}\tv${summary.latestVersion ?? "-"}\t${summary.name}`));
42
222
  })).pipe(_effect_cli.Command.withDescription("List every prompt"));
43
223
  const versions = _effect_cli.Command.make("versions", { promptId }, ({ promptId: id }) => effect.Effect.gen(function* () {
44
224
  const prompt = yield* (yield* require_client.AnpordApi).prompts.get({ payload: {
@@ -59,25 +239,100 @@ const promote = _effect_cli.Command.make("promote", {
59
239
  } });
60
240
  return yield* note(`${id} v${pin} is now ${to}`);
61
241
  })).pipe(_effect_cli.Command.withDescription("Point a channel at a version"));
62
- const readStdin = effect.Effect.flatMap(_effect_platform.FileSystem.FileSystem, (fs) => fs.readFileString("/dev/stdin"));
242
+ /**
243
+ * Read from the stream rather than by opening `/dev/stdin` as a file. The path
244
+ * only names the pipe, so reading it races whoever is writing: a body arriving
245
+ * in more than one chunk, which is what a pipe does under load, was read as
246
+ * whatever had landed by then.
247
+ */
248
+ const readStdin = effect.Effect.async((resume) => {
249
+ let body = "";
250
+ process.stdin.setEncoding("utf8");
251
+ process.stdin.on("data", (chunk) => {
252
+ body += chunk;
253
+ });
254
+ process.stdin.on("end", () => resume(effect.Effect.succeed(body)));
255
+ process.stdin.on("error", (cause) => resume(effect.Effect.fail(new Error("Could not read the content from stdin", { cause }))));
256
+ });
63
257
  const target = _effect_cli.Args.all([promptId, _effect_cli.Args.text({ name: "content" }).pipe(_effect_cli.Args.withDescription("The new content, or - to read stdin"))]);
258
+ const push = _effect_cli.Command.make("push", {
259
+ message,
260
+ target
261
+ }, ({ message: why, target: [id, content] }) => effect.Effect.gen(function* () {
262
+ const api = yield* require_client.AnpordApi;
263
+ const body = content === "-" ? yield* readStdin : content;
264
+ const prompt = yield* api.prompts.update({ payload: {
265
+ content: body,
266
+ id,
267
+ message: effect.Option.getOrUndefined(why)
268
+ } });
269
+ return yield* note(`${id} is now v${prompt.version}`);
270
+ })).pipe(_effect_cli.Command.withDescription("Add a version to a prompt"));
271
+ const out = _effect_cli.Options.file("out").pipe(_effect_cli.Options.withDescription("Where to write the declarations"), _effect_cli.Options.withDefault("anpord-env.d.ts"));
272
+ /** Reading one prompt at a time because the list carries no content, bounded
273
+ * so a large organisation does not open a connection per prompt. */
274
+ const READ_AT_ONCE = 8;
275
+ const writeDeclarations = ({ out: path }) => effect.Effect.gen(function* () {
276
+ const api = yield* require_client.AnpordApi;
277
+ const fs = yield* _effect_platform.FileSystem.FileSystem;
278
+ const { data } = yield* api.prompts.list({ payload: {} });
279
+ const prompts = yield* effect.Effect.forEach(data, (summary) => api.prompts.get({ payload: { id: summary.id } }).pipe(effect.Effect.map((prompt) => [prompt.id, extractVariables(prompt.content)])), { concurrency: READ_AT_ONCE });
280
+ yield* fs.writeFileString(path, declarationFile(prompts));
281
+ return yield* note(`Wrote ${prompts.length} ${prompts.length === 1 ? "prompt" : "prompts"} to ${path}`);
282
+ });
283
+ const DESCRIPTION = "Write TypeScript declarations for prompt variables";
284
+ const generate = _effect_cli.Command.make("generate", { out }, writeDeclarations).pipe(_effect_cli.Command.withDescription(DESCRIPTION));
285
+ /** A second command rather than an alias, because a command carries one name
286
+ * and the shorter one is what anybody types twice. */
287
+ const gen = _effect_cli.Command.make("gen", { out }, writeDeclarations).pipe(_effect_cli.Command.withDescription(DESCRIPTION));
288
+ const evalFile = _effect_cli.Args.text({ name: "file" }).pipe(_effect_cli.Args.withDescription("A TypeScript file that default exports defineEval(...); every *.eval.ts is run when omitted"), _effect_cli.Args.optional);
289
+ const noWait = _effect_cli.Options.boolean("no-wait").pipe(_effect_cli.Options.withDescription("Start the run and print its id, without waiting"));
290
+ const failOn = _effect_cli.Options.choice("fail-on", [
291
+ "never",
292
+ "regressed",
293
+ "unscored"
294
+ ]).pipe(_effect_cli.Options.withDescription("What makes the command exit nonzero"), _effect_cli.Options.withDefault("regressed"));
295
+ const runOneEval = (file, options) => effect.Effect.gen(function* () {
296
+ const api = yield* require_client.AnpordApi;
297
+ const payload = yield* require_compiler.compileEvalEffect(file);
298
+ const started = yield* api.evals.start({ payload });
299
+ if (options.skipWait) {
300
+ yield* json(started);
301
+ return [];
302
+ }
303
+ const live = !options.wantsJson && (yield* attended);
304
+ if (live || options.label) yield* note(`${file} · run ${started.id}`);
305
+ const draw = yield* liveGrid(payload.trials, live);
306
+ const run = yield* waitForRun(started.id, draw);
307
+ yield* options.wantsJson ? json(run) : note(summaryOf(run, payload.trials, live));
308
+ return problemsWith(run, options.gate);
309
+ });
64
310
  const commands = [
311
+ _effect_cli.Command.make("eval", {
312
+ asJson,
313
+ evalFile,
314
+ failOn,
315
+ noWait
316
+ }, ({ asJson: wantsJson, evalFile: file, failOn: gate, noWait: skipWait }) => effect.Effect.gen(function* () {
317
+ const files = yield* effect.Option.match(file, {
318
+ onNone: () => evalFilesIn("."),
319
+ onSome: (one) => effect.Effect.succeed([one])
320
+ });
321
+ if (files.length === 0) return yield* effect.Effect.fail(new NoEvalFiles());
322
+ const found = yield* effect.Effect.forEach(files, (one) => runOneEval(one, {
323
+ gate,
324
+ label: files.length > 1,
325
+ skipWait,
326
+ wantsJson
327
+ }));
328
+ return yield* failWhen(found.flat());
329
+ })).pipe(_effect_cli.Command.withDescription("Compile and run an eval from TypeScript")),
330
+ gen,
331
+ generate,
65
332
  get,
66
333
  list,
67
334
  promote,
68
- _effect_cli.Command.make("push", {
69
- message,
70
- target
71
- }, ({ message: why, target: [id, content] }) => effect.Effect.gen(function* () {
72
- const api = yield* require_client.AnpordApi;
73
- const body = content === "-" ? yield* readStdin : content;
74
- const prompt = yield* api.prompts.update({ payload: {
75
- content: body,
76
- id,
77
- message: effect.Option.getOrUndefined(why)
78
- } });
79
- return yield* note(`${id} is now v${prompt.version}`);
80
- })).pipe(_effect_cli.Command.withDescription("Add a version to a prompt")),
335
+ push,
81
336
  versions
82
337
  ];
83
338
  //#endregion
@@ -94,10 +349,10 @@ const reportFailure = (error) => effect.Effect.sync(() => {
94
349
  });
95
350
  //#endregion
96
351
  //#region src/cli/main.ts
97
- const anpord = _effect_cli.Command.make("anpord").pipe(_effect_cli.Command.withDescription("Read and publish prompts from the terminal"), _effect_cli.Command.withSubcommands(commands));
352
+ const anpord = _effect_cli.Command.make("anpord").pipe(_effect_cli.Command.withDescription("Run evals and manage prompts from the terminal"), _effect_cli.Command.withSubcommands(commands));
98
353
  const ClientLayer = effect.Layer.unwrapEffect(effect.Effect.map(require_config.clientOptionsConfig, require_client.layer));
99
354
  _effect_cli.Command.run(anpord, {
100
355
  name: "Anpord",
101
- version: "0.1.0"
356
+ version: version$1
102
357
  })(process.argv).pipe(effect.Effect.provide(effect.Layer.mergeAll(ClientLayer, _effect_platform_node.NodeContext.layer)), effect.Effect.catchAllCause((cause) => effect.Cause.isInterruptedOnly(cause) ? effect.Effect.void : reportFailure(effect.Cause.failureOption(cause).pipe(effect.Option.getOrElse(() => effect.Cause.squash(cause))))), _effect_platform_node.NodeRuntime.runMain({ disableErrorReporting: true }));
103
358
  //#endregion