amness 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Model } from "./harness.ts";
|
|
2
|
+
/** "1s", "6m0s", "20ms", "1.5s" -> milliseconds (the format of x-ratelimit-reset-* headers). */
|
|
3
|
+
export declare function parseDuration(s: string): number | undefined;
|
|
4
|
+
/** How long the server asked us to wait (Retry-After seconds or date, retry-after-ms, or "try again in 1.2s" in the message), if it said. */
|
|
5
|
+
export declare function retryAfterMs(e: any): number | undefined;
|
|
6
|
+
/** A 429 that waiting can fix. Quota, billing and spend-cap errors also arrive as 429 but never recover by retrying. */
|
|
7
|
+
export declare function isRateLimit(e: any): boolean;
|
|
8
|
+
export declare class Gate {
|
|
9
|
+
max: number;
|
|
10
|
+
readonly ceiling: number;
|
|
11
|
+
active: number;
|
|
12
|
+
pausedUntil: number;
|
|
13
|
+
private successes;
|
|
14
|
+
private waiters;
|
|
15
|
+
limited: number;
|
|
16
|
+
constructor(o?: {
|
|
17
|
+
ceiling?: number;
|
|
18
|
+
start?: number;
|
|
19
|
+
});
|
|
20
|
+
private wake;
|
|
21
|
+
acquire(signal?: AbortSignal): Promise<() => void>;
|
|
22
|
+
success(): void;
|
|
23
|
+
/** A 429 happened: pause everyone, halve the concurrency, and say how long. */
|
|
24
|
+
penalize(retryAfter: number | undefined, attempt: number): number;
|
|
25
|
+
}
|
|
26
|
+
/** The gate for one API endpoint and key: every model built for it shares it. */
|
|
27
|
+
export declare function gateFor(key: string, ceiling?: number): Gate;
|
|
28
|
+
export type RateLimitOptions = {
|
|
29
|
+
gate?: Gate; /** attempts after the first (default 6) */
|
|
30
|
+
retries?: number; /** give up when asked to wait longer than this (default 2 minutes) */
|
|
31
|
+
maxWaitMs?: number;
|
|
32
|
+
onWait?: (ms: number, attempt: number, gate: Gate) => void;
|
|
33
|
+
};
|
|
34
|
+
export declare function rateLimited(model: Model, o?: RateLimitOptions): Model;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const sleep = (ms, signal) => new Promise((res, rej) => {
|
|
2
|
+
if (signal?.aborted)
|
|
3
|
+
return rej(new DOMException("aborted", "AbortError"));
|
|
4
|
+
const t = setTimeout(() => { signal?.removeEventListener("abort", onAbort); res(); }, ms), onAbort = () => { clearTimeout(t); rej(new DOMException("aborted", "AbortError")); };
|
|
5
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6
|
+
});
|
|
7
|
+
/** "1s", "6m0s", "20ms", "1.5s" -> milliseconds (the format of x-ratelimit-reset-* headers). */
|
|
8
|
+
export function parseDuration(s) {
|
|
9
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h)/g;
|
|
10
|
+
let total = 0, any = false, m;
|
|
11
|
+
while ((m = re.exec(s))) {
|
|
12
|
+
any = true;
|
|
13
|
+
total += Number(m[1]) * { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[m[2]];
|
|
14
|
+
}
|
|
15
|
+
return any ? total : undefined;
|
|
16
|
+
}
|
|
17
|
+
const header = (e, name) => { const h = e?.headers; const v = typeof h?.get === "function" ? h.get(name) : h?.[name]; return v == null ? undefined : String(v); };
|
|
18
|
+
/** How long the server asked us to wait (Retry-After seconds or date, retry-after-ms, or "try again in 1.2s" in the message), if it said. */
|
|
19
|
+
export function retryAfterMs(e) {
|
|
20
|
+
if (typeof e?.retryAfterMs === "number")
|
|
21
|
+
return e.retryAfterMs;
|
|
22
|
+
const ms = header(e, "retry-after-ms");
|
|
23
|
+
if (ms && Number.isFinite(Number(ms)))
|
|
24
|
+
return Number(ms);
|
|
25
|
+
const ra = header(e, "retry-after");
|
|
26
|
+
if (ra) {
|
|
27
|
+
if (Number.isFinite(Number(ra)))
|
|
28
|
+
return Number(ra) * 1000;
|
|
29
|
+
const d = Date.parse(ra);
|
|
30
|
+
if (Number.isFinite(d))
|
|
31
|
+
return Math.max(0, d - Date.now());
|
|
32
|
+
}
|
|
33
|
+
const m = /try again in ([\d.]+\s*(?:ms|s|m))/i.exec(String(e?.message ?? ""));
|
|
34
|
+
return m ? parseDuration(m[1].replace(/\s+/g, "")) : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** A 429 that waiting can fix. Quota, billing and spend-cap errors also arrive as 429 but never recover by retrying. */
|
|
37
|
+
export function isRateLimit(e) {
|
|
38
|
+
const msg = String(e?.message ?? "");
|
|
39
|
+
if (/insufficient_quota|exceeded your current quota|billing|spend limit|usage limits|enforced_spend_limit/i.test(msg))
|
|
40
|
+
return false;
|
|
41
|
+
return e?.status === 429 || /\bHTTP 429\b|rate.?limit/i.test(msg);
|
|
42
|
+
}
|
|
43
|
+
export class Gate {
|
|
44
|
+
max;
|
|
45
|
+
ceiling;
|
|
46
|
+
active = 0;
|
|
47
|
+
pausedUntil = 0;
|
|
48
|
+
successes = 0;
|
|
49
|
+
waiters = [];
|
|
50
|
+
limited = 0;
|
|
51
|
+
constructor(o = {}) { this.ceiling = Math.max(1, o.ceiling ?? 4); this.max = Math.min(this.ceiling, Math.max(1, o.start ?? 2)); }
|
|
52
|
+
wake() { const w = this.waiters.shift(); if (w)
|
|
53
|
+
w(); }
|
|
54
|
+
async acquire(signal) {
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (signal?.aborted)
|
|
57
|
+
throw new DOMException("aborted", "AbortError");
|
|
58
|
+
const wait = this.pausedUntil - Date.now();
|
|
59
|
+
if (wait > 0) {
|
|
60
|
+
await sleep(wait, signal);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (this.active < this.max) {
|
|
64
|
+
this.active++;
|
|
65
|
+
let done = false;
|
|
66
|
+
return () => { if (done)
|
|
67
|
+
return; done = true; this.active--; this.wake(); };
|
|
68
|
+
}
|
|
69
|
+
await new Promise((res, rej) => { const w = () => { signal?.removeEventListener("abort", ab); res(); }, ab = () => { this.waiters = this.waiters.filter((x) => x !== w); rej(new DOMException("aborted", "AbortError")); }; this.waiters.push(w); signal?.addEventListener("abort", ab, { once: true }); });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
success() { this.successes++; if (this.successes >= 3 && this.max < this.ceiling) {
|
|
73
|
+
this.max++;
|
|
74
|
+
this.successes = 0;
|
|
75
|
+
this.wake();
|
|
76
|
+
} }
|
|
77
|
+
/** A 429 happened: pause everyone, halve the concurrency, and say how long. */
|
|
78
|
+
penalize(retryAfter, attempt) {
|
|
79
|
+
const backoff = Math.min(60_000, 1000 * 2 ** attempt), wait = Math.round((retryAfter ?? backoff) + Math.random() * Math.min(750, 100 + 0.25 * (retryAfter ?? backoff)));
|
|
80
|
+
this.pausedUntil = Math.max(this.pausedUntil, Date.now() + wait);
|
|
81
|
+
this.max = Math.max(1, Math.floor(this.max / 2));
|
|
82
|
+
this.successes = 0;
|
|
83
|
+
this.limited++;
|
|
84
|
+
return wait;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const gates = new Map();
|
|
88
|
+
/** The gate for one API endpoint and key: every model built for it shares it. */
|
|
89
|
+
export function gateFor(key, ceiling = 4) { let g = gates.get(key); if (!g)
|
|
90
|
+
gates.set(key, (g = new Gate({ ceiling }))); return g; }
|
|
91
|
+
export function rateLimited(model, o = {}) {
|
|
92
|
+
const gate = o.gate ?? new Gate(), retries = o.retries ?? 6, maxWait = o.maxWaitMs ?? 120_000;
|
|
93
|
+
const call = (async (messages, tools, opts) => {
|
|
94
|
+
for (let attempt = 0;; attempt++) {
|
|
95
|
+
const release = await gate.acquire(opts?.signal);
|
|
96
|
+
try {
|
|
97
|
+
const m = await model(messages, tools, opts);
|
|
98
|
+
gate.success();
|
|
99
|
+
return m;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (opts?.signal?.aborted || !isRateLimit(e) || attempt >= retries)
|
|
103
|
+
throw e;
|
|
104
|
+
const ra = retryAfterMs(e);
|
|
105
|
+
if (ra !== undefined && ra > maxWait)
|
|
106
|
+
throw e;
|
|
107
|
+
const wait = gate.penalize(ra, attempt);
|
|
108
|
+
o.onWait?.(wait, attempt + 1, gate);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
release();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return Object.assign(call, model);
|
|
116
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Message, type Options, type RunResult } from "./harness.ts";
|
|
2
|
+
import { type TraceLine } from "./trace.ts";
|
|
3
|
+
export type TraceRun = {
|
|
4
|
+
messages: Message[];
|
|
5
|
+
models: Message[];
|
|
6
|
+
results: Map<string, string[]>;
|
|
7
|
+
tools: string[];
|
|
8
|
+
lines: TraceLine[];
|
|
9
|
+
};
|
|
10
|
+
/** Split a JSONL trace into runs (one per `run` event). Events of subagents are ignored: they are not regenerated. */
|
|
11
|
+
export declare function readTrace(file: string): TraceRun[];
|
|
12
|
+
export type ReplayReport = {
|
|
13
|
+
run: number;
|
|
14
|
+
turns: number;
|
|
15
|
+
recorded: string[];
|
|
16
|
+
replayed: string[];
|
|
17
|
+
onlyRecorded: string[];
|
|
18
|
+
onlyReplayed: string[];
|
|
19
|
+
same: boolean;
|
|
20
|
+
result: RunResult;
|
|
21
|
+
};
|
|
22
|
+
/** Feed one recorded run back through the harness with `o` as its options (the thing you changed). */
|
|
23
|
+
export declare function replayRun(run: TraceRun, o?: Partial<Options>): Promise<ReplayReport>;
|
|
24
|
+
export declare function replayFile(file: string, o?: Partial<Options>): Promise<ReplayReport[]>;
|
|
25
|
+
export declare function formatReplay(reports: ReplayReport[]): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Replay a recorded trace through the CURRENT harness without calling any model or running any tool: the model's recorded messages and the tools' recorded results are fed back,
|
|
2
|
+
// so only the harness logic (guards, permissions, compaction, cache behaviour, hooks...) can differ. That makes harness changes testable offline, for free, on real sessions.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { canonicalCall, createHarness, defineTool } from "./harness.js";
|
|
5
|
+
import { Tracer } from "./trace.js";
|
|
6
|
+
const SIGNIFICANT = new Set(["nudge", "loop", "repair", "rejected", "denied", "hook", "trim", "cachebreak", "review", "audit", "finish", "aborted", "budget", "rollover", "tools", "steer"]);
|
|
7
|
+
/** Split a JSONL trace into runs (one per `run` event). Events of subagents are ignored: they are not regenerated. */
|
|
8
|
+
export function readTrace(file) {
|
|
9
|
+
const runs = [];
|
|
10
|
+
let cur;
|
|
11
|
+
const pending = new Map();
|
|
12
|
+
for (const l of readFileSync(file, "utf8").split("\n")) {
|
|
13
|
+
if (!l.trim())
|
|
14
|
+
continue;
|
|
15
|
+
let e;
|
|
16
|
+
try {
|
|
17
|
+
e = JSON.parse(l);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (e.agent)
|
|
23
|
+
continue;
|
|
24
|
+
if (e.kind === "run") {
|
|
25
|
+
cur = { messages: e.messages, models: [], results: new Map(), tools: [], lines: [] };
|
|
26
|
+
runs.push(cur);
|
|
27
|
+
pending.clear();
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!cur)
|
|
31
|
+
continue;
|
|
32
|
+
if (e.kind === "model") {
|
|
33
|
+
cur.models.push(e.message);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (e.kind === "toolstart") {
|
|
37
|
+
pending.set(String(e.id), { name: String(e.name), args: String(e.args) });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (e.kind === "tool") {
|
|
41
|
+
const p = pending.get(String(e.id));
|
|
42
|
+
if (!p)
|
|
43
|
+
continue;
|
|
44
|
+
pending.delete(String(e.id));
|
|
45
|
+
if (!cur.tools.includes(p.name))
|
|
46
|
+
cur.tools.push(p.name);
|
|
47
|
+
let args = {};
|
|
48
|
+
try {
|
|
49
|
+
args = JSON.parse(p.args || "{}");
|
|
50
|
+
}
|
|
51
|
+
catch { /* keep {} */ }
|
|
52
|
+
const key = canonicalCall(p.name, args), list = cur.results.get(key) ?? [];
|
|
53
|
+
list.push(String(e.result));
|
|
54
|
+
cur.results.set(key, list);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (SIGNIFICANT.has(e.kind))
|
|
58
|
+
cur.lines.push(e);
|
|
59
|
+
}
|
|
60
|
+
return runs;
|
|
61
|
+
}
|
|
62
|
+
const sig = (l) => { const d = String(l.detail ?? l.summary ?? l.reason ?? "").replace(/\s+/g, " ").slice(0, 80); return `${l.kind}${d ? " · " + d : ""}`; };
|
|
63
|
+
/** Feed one recorded run back through the harness with `o` as its options (the thing you changed). */
|
|
64
|
+
export async function replayRun(run, o = {}) {
|
|
65
|
+
const models = [...run.models], results = new Map([...run.results].map(([k, v]) => [k, [...v]]));
|
|
66
|
+
const model = async () => models.shift() ?? { role: "assistant", content: "(replay: the recorded trace ends here)" };
|
|
67
|
+
const tools = run.tools.map((name) => defineTool({ name, description: "", schema: { type: "object" }, readOnly: true, run: (args) => results.get(canonicalCall(name, args))?.shift() ?? "ERROR: (replay) no recorded result for this call: the harness diverged from the recording here" }));
|
|
68
|
+
const replayed = [], tracer = new Tracer({ sessionId: "replay", sink: (l) => replayed.push(l) });
|
|
69
|
+
const result = await createHarness({ maxSteps: 60, approve: () => true, ...o, model, tools, onEvent: (e) => tracer.event(e) }).run("", { resume: run.messages });
|
|
70
|
+
const A = run.lines.map(sig), B = replayed.filter((l) => SIGNIFICANT.has(l.kind)).map(sig);
|
|
71
|
+
const left = (xs, ys) => { const c = new Map(); for (const y of ys)
|
|
72
|
+
c.set(y, (c.get(y) ?? 0) + 1); return xs.filter((x) => { const n = c.get(x) ?? 0; if (n > 0) {
|
|
73
|
+
c.set(x, n - 1);
|
|
74
|
+
return false;
|
|
75
|
+
} return true; }); };
|
|
76
|
+
const onlyRecorded = left(A, B), onlyReplayed = left(B, A);
|
|
77
|
+
return { run: 0, turns: run.models.length, recorded: A, replayed: B, onlyRecorded, onlyReplayed, same: !onlyRecorded.length && !onlyReplayed.length, result };
|
|
78
|
+
}
|
|
79
|
+
export async function replayFile(file, o = {}) {
|
|
80
|
+
const runs = readTrace(file), out = [];
|
|
81
|
+
for (const [i, r] of runs.entries())
|
|
82
|
+
out.push({ ...(await replayRun(r, o)), run: i + 1 });
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export function formatReplay(reports) {
|
|
86
|
+
if (!reports.length)
|
|
87
|
+
return "no recorded runs in this trace (traces written before replay support, or with recording off, have no model messages)";
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const r of reports) {
|
|
90
|
+
out.push(`run ${r.run}: ${r.turns} model turn(s), ${r.same ? "identical harness behaviour" : `${r.onlyRecorded.length + r.onlyReplayed.length} difference(s)`}`);
|
|
91
|
+
for (const s of r.onlyRecorded)
|
|
92
|
+
out.push(` - recorded only: ${s}`);
|
|
93
|
+
for (const s of r.onlyReplayed)
|
|
94
|
+
out.push(` + replay only: ${s}`);
|
|
95
|
+
if (!r.same)
|
|
96
|
+
out.push(" (after the first difference the recording no longer matches what the model would have said, so later lines are indicative)");
|
|
97
|
+
}
|
|
98
|
+
return out.join("\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Budget, type Model } from "./harness.ts";
|
|
2
|
+
export type ReviewResult = {
|
|
3
|
+
pass: boolean;
|
|
4
|
+
findings: string[];
|
|
5
|
+
inconclusive?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Testable acceptance criteria for `task`. Returns [] when the model's reply cannot be used (the work simply proceeds without a contract). */
|
|
8
|
+
export declare function proposeContract(model: Model, task: string, budget?: Budget): Promise<string[]>;
|
|
9
|
+
export declare function reviewWork(o: {
|
|
10
|
+
model: Model;
|
|
11
|
+
cwd: string;
|
|
12
|
+
task: string;
|
|
13
|
+
contract: string[];
|
|
14
|
+
summary: string;
|
|
15
|
+
diffs: string;
|
|
16
|
+
commands: string;
|
|
17
|
+
verify?: string;
|
|
18
|
+
budget?: Budget;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}): Promise<ReviewResult>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// An independent, skeptical reviewer. Builders overpraise their own work, and a separate judge is easier to tune towards scepticism than a builder is to make self-critical.
|
|
2
|
+
// Two parts: a CONTRACT of testable acceptance criteria agreed before the work starts, and a REVIEW of the finished work against it, in a fresh context that cannot edit anything.
|
|
3
|
+
import { codeTools, runShell } from "./code.js";
|
|
4
|
+
import { clip, createHarness, defineTool } from "./harness.js";
|
|
5
|
+
import { BUILD_CMD, TEST_CMD } from "./ledger.js";
|
|
6
|
+
import { parseLooseJson } from "./schema.js";
|
|
7
|
+
const CONTRACT_SYSTEM = "You write acceptance criteria for a coding task before anyone starts it. Be concrete and checkable: each criterion must be verifiable by reading the diff or running a command. Do not describe how to implement. 3 to 7 criteria, each one sentence. Reply with ONLY a JSON array of strings.";
|
|
8
|
+
/** Testable acceptance criteria for `task`. Returns [] when the model's reply cannot be used (the work simply proceeds without a contract). */
|
|
9
|
+
export async function proposeContract(model, task, budget) {
|
|
10
|
+
try {
|
|
11
|
+
const m = await model([{ role: "system", content: CONTRACT_SYSTEM }, { role: "user", content: `Task:\n${clip(task, 6000)}` }], []);
|
|
12
|
+
budget?.add(m);
|
|
13
|
+
const j = parseLooseJson(m.content ?? "");
|
|
14
|
+
return Array.isArray(j) ? j.filter((x) => typeof x === "string" && x.trim().length > 3).map((x) => x.trim().slice(0, 300)).slice(0, 7) : [];
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const REVIEW_SYSTEM = `You are a skeptical code reviewer. Someone else did this work; assume it is wrong until the evidence shows otherwise.
|
|
21
|
+
- Do NOT trust their summary. Judge from the diff, the files, and commands you run yourself.
|
|
22
|
+
- Check every acceptance criterion. For each one decide: met (with the evidence you saw) or not met (say exactly what is wrong or missing).
|
|
23
|
+
- Look for what they did not do: untouched call sites, missing edge cases, weakened tests, unrelated changes.
|
|
24
|
+
- You can read files and run checks with the tools; you cannot change anything.
|
|
25
|
+
- When done, call finish with a summary that is ONLY this JSON: {"pass": boolean, "findings": ["one specific problem per entry, empty if none"]}.
|
|
26
|
+
Pass only if every criterion is met and you found no real problem. Style opinions are not findings.`;
|
|
27
|
+
/** Read-only tools plus a runner for the project's own checks (tests, build, lint); nothing else can execute. */
|
|
28
|
+
function reviewerTools(cwd, allow) {
|
|
29
|
+
const read = codeTools(cwd, { mode: () => "plan" }).filter((t) => ["list_files", "read_file", "grep"].includes(t.name));
|
|
30
|
+
const run = defineTool({ name: "run_check", description: "Run one of the project's own checks (tests, build, typecheck, lint) and see the result. Other commands are refused.", readOnly: true,
|
|
31
|
+
schema: { type: "object", required: ["cmd"], properties: { cmd: { type: "string" } } },
|
|
32
|
+
run: async ({ cmd }) => (!/[;&|<>`$()]/.test(cmd) && allow.some((re) => re.test(cmd)) ? clip(await runShell(cmd, cwd, 180_000), 4000) : "ERROR: only the project's test, build, typecheck and lint commands may be run here (one command, no chaining)") });
|
|
33
|
+
return [...read, run];
|
|
34
|
+
}
|
|
35
|
+
export async function reviewWork(o) {
|
|
36
|
+
const allow = [TEST_CMD, BUILD_CMD, ...(o.verify ? [new RegExp("^\\s*" + o.verify.trim().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+"))] : [])];
|
|
37
|
+
const prompt = `## Task\n${clip(o.task, 4000)}\n\n## Acceptance criteria\n${o.contract.length ? o.contract.map((c, i) => `${i + 1}. ${c}`).join("\n") : "(none were written; judge against the task itself)"}\n\n## What the author says they did\n${clip(o.summary, 2000)}\n\n## Changes (diffs)\n${o.diffs || "(no file was edited)"}\n\n## Commands the author ran\n${o.commands || "(none)"}`;
|
|
38
|
+
try {
|
|
39
|
+
const r = await createHarness({ model: o.model, system: REVIEW_SYSTEM, tools: reviewerTools(o.cwd, allow), maxSteps: 14, budget: o.budget, signal: o.signal }).run(prompt);
|
|
40
|
+
const j = r.finished ? parseLooseJson(r.text) : null;
|
|
41
|
+
if (!j || typeof j !== "object" || typeof j.pass !== "boolean")
|
|
42
|
+
return { pass: true, findings: [], inconclusive: true }; // an unusable verdict must not block the work
|
|
43
|
+
const findings = Array.isArray(j.findings) ? j.findings.filter((x) => typeof x === "string" && !!x.trim()).map((x) => x.slice(0, 500)).slice(0, 8) : [];
|
|
44
|
+
return { pass: j.pass && findings.length === 0, findings: j.pass ? findings : findings.length ? findings : ["the reviewer rejected the work without saying why"] };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { pass: true, findings: [], inconclusive: true };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Model } from "./harness.ts";
|
|
2
|
+
export type Roles = Partial<Record<"explore" | "compact" | "review" | "cheap", Model>>;
|
|
3
|
+
/** Errors worth trying another model for: rate limits, overload, server errors, timeouts, and network failures (the adapters already retried the same model). */
|
|
4
|
+
export declare function isRetryable(e: unknown): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Try `primary`; if it fails with a retryable error, answer from `fallback` instead (and say so through `onFallback`).
|
|
7
|
+
* The fallback model sees the same messages; adapters ignore reasoning blobs that belong to another provider.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withFallback(primary: Model, fallback: Model, onFallback?: (e: unknown) => void): Model;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Errors worth trying another model for: rate limits, overload, server errors, timeouts, and network failures (the adapters already retried the same model). */
|
|
2
|
+
export function isRetryable(e) {
|
|
3
|
+
const err = e;
|
|
4
|
+
if (err?.name === "AbortError")
|
|
5
|
+
return false;
|
|
6
|
+
if (typeof err?.status === "number")
|
|
7
|
+
return err.status === 408 || err.status === 429 || err.status >= 500;
|
|
8
|
+
return /\b(ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch failed|socket hang up|overloaded|HTTP (408|429|5\d\d))\b/i.test(`${err?.code ?? ""} ${err?.message ?? ""}`);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Try `primary`; if it fails with a retryable error, answer from `fallback` instead (and say so through `onFallback`).
|
|
12
|
+
* The fallback model sees the same messages; adapters ignore reasoning blobs that belong to another provider.
|
|
13
|
+
*/
|
|
14
|
+
export function withFallback(primary, fallback, onFallback) {
|
|
15
|
+
const m = async (messages, tools, opts) => {
|
|
16
|
+
try {
|
|
17
|
+
return await primary(messages, tools, opts);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (!isRetryable(e) || opts?.signal?.aborted)
|
|
21
|
+
throw e;
|
|
22
|
+
onFallback?.(e);
|
|
23
|
+
return fallback(messages, tools, opts);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
m.appendOnly = primary.appendOnly || fallback.appendOnly; // if either may be Claude, history must not be rewritten
|
|
27
|
+
if (primary.api)
|
|
28
|
+
m.api = primary.api;
|
|
29
|
+
return m;
|
|
30
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Message, Model } from "./harness.ts";
|
|
2
|
+
export type Tier = "strong" | "cheap";
|
|
3
|
+
export type Prices = {
|
|
4
|
+
input: number;
|
|
5
|
+
output: number; /** fraction of the input price charged for cached input (default 0.1) */
|
|
6
|
+
cachedRate?: number;
|
|
7
|
+
};
|
|
8
|
+
export type Decision = {
|
|
9
|
+
tier: Tier;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
export type RouterOptions = {
|
|
13
|
+
strong: Model;
|
|
14
|
+
cheap: Model;
|
|
15
|
+
prices?: {
|
|
16
|
+
strong: Prices;
|
|
17
|
+
cheap: Prices;
|
|
18
|
+
};
|
|
19
|
+
/** how long a provider keeps a prompt cache warm (ms); default 5 minutes */ ttlMs?: number;
|
|
20
|
+
onRoute?: (d: Decision & {
|
|
21
|
+
switched: boolean;
|
|
22
|
+
promptTokens: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
};
|
|
26
|
+
/** What the end of the conversation looks like: how many exploration-only turns in a row, and whether anything just went wrong or needs judgement. */
|
|
27
|
+
export declare function tailShape(messages: Message[]): {
|
|
28
|
+
streak: number;
|
|
29
|
+
trouble: boolean;
|
|
30
|
+
first: boolean;
|
|
31
|
+
steering: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** The pure decision, separated so it can be tested and tuned: cheap only when the next step is likely exploration AND switching pays for itself. */
|
|
34
|
+
export declare function decide(o: {
|
|
35
|
+
tail: ReturnType<typeof tailShape>;
|
|
36
|
+
promptTokens: number;
|
|
37
|
+
current: Tier;
|
|
38
|
+
cheapWarm: boolean;
|
|
39
|
+
prices: {
|
|
40
|
+
strong: Prices;
|
|
41
|
+
cheap: Prices;
|
|
42
|
+
};
|
|
43
|
+
}): Decision;
|
|
44
|
+
export declare function routedModel(o: RouterOptions): Model;
|
|
45
|
+
/** Route each request to a model chosen by your own function (cost, step kind, failures, budget...). `choose` returns a key of `models`; an unknown key falls back to the first model. */
|
|
46
|
+
export declare function choosingModel(models: Record<string, Model>, choose: (info: {
|
|
47
|
+
messages: Message[];
|
|
48
|
+
promptTokens: number;
|
|
49
|
+
tail: ReturnType<typeof tailShape>;
|
|
50
|
+
}) => string, o?: {
|
|
51
|
+
onChoose?: (key: string) => void;
|
|
52
|
+
}): Model;
|
|
53
|
+
export type RouteInfo = {
|
|
54
|
+
messages: Message[];
|
|
55
|
+
promptTokens: number;
|
|
56
|
+
tail: ReturnType<typeof tailShape>;
|
|
57
|
+
};
|
|
58
|
+
/** Send each request to the model id your function returns (or the base model when it returns nothing). Models are created once per id, on first use. */
|
|
59
|
+
export declare function routedByFunction(base: Model, modelById: (id: string) => Model, route: (info: RouteInfo) => string | undefined | void, o?: {
|
|
60
|
+
onRoute?: (id: string | undefined) => void;
|
|
61
|
+
}): Model;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const READ_ONLY = new Set(["read_file", "list_files", "grep", "repo_map", "find_refs", "impact", "skill", "sh_read", "sh_list", "memory_search", "web_search", "fetch_url", "task", "todo_write"]);
|
|
2
|
+
const failed = (s) => /^(ERROR|DENIED|REJECTED|BLOCKED)/.test(s) || /^exit=(?!0\b)/.test(s);
|
|
3
|
+
const tokensOf = (ms) => Math.ceil(ms.reduce((n, m) => n + (m.content?.length ?? 0) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0), 0) / 4);
|
|
4
|
+
/** What the end of the conversation looks like: how many exploration-only turns in a row, and whether anything just went wrong or needs judgement. */
|
|
5
|
+
export function tailShape(messages) {
|
|
6
|
+
const last = messages.at(-1), assistants = messages.filter((m) => m.role === "assistant");
|
|
7
|
+
const steering = last?.role === "user" && assistants.length > 0;
|
|
8
|
+
let streak = 0, trouble = false;
|
|
9
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10
|
+
const m = messages[i];
|
|
11
|
+
if (m.role !== "assistant")
|
|
12
|
+
continue;
|
|
13
|
+
if (!m.tool_calls?.length)
|
|
14
|
+
break;
|
|
15
|
+
const names = m.tool_calls.map((c) => c.function.name);
|
|
16
|
+
if (!names.every((n) => READ_ONLY.has(n)))
|
|
17
|
+
break;
|
|
18
|
+
streak++;
|
|
19
|
+
}
|
|
20
|
+
// results after the last assistant message
|
|
21
|
+
const lastA = messages.map((m) => m.role).lastIndexOf("assistant");
|
|
22
|
+
for (const m of messages.slice(lastA + 1))
|
|
23
|
+
if (m.role === "tool" && failed(m.content ?? ""))
|
|
24
|
+
trouble = true;
|
|
25
|
+
if (last?.role === "user" && /\b(REJECTED|nothing measurable has improved|going in circles|reflect)\b/i.test(last.content ?? ""))
|
|
26
|
+
trouble = true;
|
|
27
|
+
return { streak, trouble, first: assistants.length === 0, steering };
|
|
28
|
+
}
|
|
29
|
+
/** The pure decision, separated so it can be tested and tuned: cheap only when the next step is likely exploration AND switching pays for itself. */
|
|
30
|
+
export function decide(o) {
|
|
31
|
+
const { tail: t, current } = o;
|
|
32
|
+
if (t.first)
|
|
33
|
+
return { tier: "strong", reason: "first step: plan with the strong model" };
|
|
34
|
+
if (t.trouble)
|
|
35
|
+
return { tier: "strong", reason: "something just failed: escalate" };
|
|
36
|
+
if (t.steering)
|
|
37
|
+
return { tier: "strong", reason: "new instruction from the user" };
|
|
38
|
+
if (t.streak === 0)
|
|
39
|
+
return { tier: "strong", reason: "last step changed something or answered: judgement needed" };
|
|
40
|
+
if (current === "cheap")
|
|
41
|
+
return { tier: "cheap", reason: `exploration continues (streak ${t.streak}), staying on the cheap model` };
|
|
42
|
+
// currently strong, tempted to go cheap: price the cold read on the cheap model against the per-step saving
|
|
43
|
+
const cr = (p) => p.cachedRate ?? 0.1, N = o.promptTokens;
|
|
44
|
+
const coldRead = o.cheapWarm ? 0 : N * o.prices.cheap.input * (1 - cr(o.prices.cheap)); // extra cost of the first call (uncached input)
|
|
45
|
+
const savingPerStep = N * (o.prices.strong.input * cr(o.prices.strong) - o.prices.cheap.input * cr(o.prices.cheap)); // cached-read price difference on every cheap step
|
|
46
|
+
if (savingPerStep <= 0)
|
|
47
|
+
return { tier: "strong", reason: "the cheap model is not cheaper per cached read" };
|
|
48
|
+
const needed = coldRead / savingPerStep;
|
|
49
|
+
return t.streak + 1 >= needed ? { tier: "cheap", reason: `exploration streak ${t.streak}: the cold read (${Math.round(coldRead)} units) is repaid in ~${needed.toFixed(1)} steps` } : { tier: "strong", reason: `streak ${t.streak} is too short to repay a cold read on the cheap model (needs ~${needed.toFixed(1)} steps)` };
|
|
50
|
+
}
|
|
51
|
+
export function routedModel(o) {
|
|
52
|
+
const prices = o.prices ?? { strong: { input: 5, output: 25 }, cheap: { input: 1, output: 5 } }, now = o.now ?? Date.now, ttl = o.ttlMs ?? 300_000;
|
|
53
|
+
let current = "strong";
|
|
54
|
+
let cheapWarmUntil = 0;
|
|
55
|
+
const owner = new WeakMap();
|
|
56
|
+
const m = async (messages, tools, opts) => {
|
|
57
|
+
const tail = tailShape(messages), promptTokens = tokensOf(messages);
|
|
58
|
+
const d = decide({ tail, promptTokens, current, cheapWarm: now() < cheapWarmUntil, prices });
|
|
59
|
+
// reasoning blobs only make sense to the model that produced them: drop the other tier's
|
|
60
|
+
const clean = messages.map((x) => (x.role === "assistant" && x.raw !== undefined && owner.get(x.raw) !== d.tier ? { ...x, raw: undefined } : x));
|
|
61
|
+
const switched = d.tier !== current;
|
|
62
|
+
current = d.tier;
|
|
63
|
+
o.onRoute?.({ ...d, switched, promptTokens });
|
|
64
|
+
let r;
|
|
65
|
+
if (d.tier === "cheap") {
|
|
66
|
+
r = await o.cheap(clean, tools, opts ? { signal: opts.signal } : undefined); // not streamed: if the answer has to be redone, nothing half-shown must be left behind
|
|
67
|
+
cheapWarmUntil = now() + ttl;
|
|
68
|
+
const needsJudgement = !r.tool_calls?.length || r.tool_calls.some((c) => !READ_ONLY.has(c.function.name));
|
|
69
|
+
if (needsJudgement) { // the cheap model reached for an edit, a command, or the final answer: that is the strong model's job
|
|
70
|
+
o.onRoute?.({ tier: "strong", reason: "the cheap model proposed a change or an answer: redone by the strong model", switched: true, promptTokens });
|
|
71
|
+
current = "strong";
|
|
72
|
+
r = await o.strong(messages.map((x) => (x.role === "assistant" && x.raw !== undefined && owner.get(x.raw) === "cheap" ? { ...x, raw: undefined } : x)), tools, opts);
|
|
73
|
+
if (r.raw !== undefined && typeof r.raw === "object" && r.raw)
|
|
74
|
+
owner.set(r.raw, "strong");
|
|
75
|
+
return r;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
r = await o.strong(clean, tools, opts);
|
|
80
|
+
if (r.raw !== undefined && typeof r.raw === "object" && r.raw)
|
|
81
|
+
owner.set(r.raw, d.tier);
|
|
82
|
+
return r;
|
|
83
|
+
};
|
|
84
|
+
m.appendOnly = o.strong.appendOnly || o.cheap.appendOnly;
|
|
85
|
+
if (o.strong.api)
|
|
86
|
+
m.api = o.strong.api;
|
|
87
|
+
return m;
|
|
88
|
+
}
|
|
89
|
+
/** Route each request to a model chosen by your own function (cost, step kind, failures, budget...). `choose` returns a key of `models`; an unknown key falls back to the first model. */
|
|
90
|
+
export function choosingModel(models, choose, o = {}) {
|
|
91
|
+
const keys = Object.keys(models), call = (async (messages, tools, opts) => {
|
|
92
|
+
const k = (() => { try {
|
|
93
|
+
const x = choose({ messages, promptTokens: tokensOf(messages), tail: tailShape(messages) });
|
|
94
|
+
return x in models ? x : keys[0];
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return keys[0];
|
|
98
|
+
} })();
|
|
99
|
+
o.onChoose?.(k);
|
|
100
|
+
return models[k](messages, tools, opts);
|
|
101
|
+
});
|
|
102
|
+
return Object.assign(call, models[keys[0]]);
|
|
103
|
+
}
|
|
104
|
+
/** Send each request to the model id your function returns (or the base model when it returns nothing). Models are created once per id, on first use. */
|
|
105
|
+
export function routedByFunction(base, modelById, route, o = {}) {
|
|
106
|
+
const cache = new Map();
|
|
107
|
+
const call = (async (messages, tools, opts) => {
|
|
108
|
+
let id;
|
|
109
|
+
try {
|
|
110
|
+
id = route({ messages, promptTokens: tokensOf(messages), tail: tailShape(messages) }) || undefined;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
id = undefined;
|
|
114
|
+
}
|
|
115
|
+
o.onRoute?.(id);
|
|
116
|
+
if (!id)
|
|
117
|
+
return base(messages, tools, opts);
|
|
118
|
+
let m = cache.get(id);
|
|
119
|
+
if (!m) {
|
|
120
|
+
m = modelById(id);
|
|
121
|
+
cache.set(id, m);
|
|
122
|
+
}
|
|
123
|
+
return m(messages, tools, opts);
|
|
124
|
+
});
|
|
125
|
+
return Object.assign(call, base);
|
|
126
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type SandboxMode = "off" | "workspace" | "strict";
|
|
2
|
+
export type SandboxKind = "seatbelt" | "bwrap";
|
|
3
|
+
export declare const SANDBOX_MODES: SandboxMode[];
|
|
4
|
+
/** Which sandbox this machine can provide. */
|
|
5
|
+
export declare function sandboxKind(): SandboxKind | undefined;
|
|
6
|
+
export declare const sandboxHelp: () => "install bubblewrap (apt install bubblewrap); if it is installed, this kernel or container does not allow it to create namespaces" | "sandbox-exec was not found" | "OS sandboxing is only available on macOS and Linux";
|
|
7
|
+
/** The Seatbelt profile: everything allowed except writing outside the allowed folders (and inside .git), and, in strict mode, the network. */
|
|
8
|
+
export declare function seatbeltProfile(workspace: string, mode: SandboxMode): string;
|
|
9
|
+
/** `file` and `args` that run `cmd` under the sandbox, or undefined when mode is off. Throws when a sandbox was asked for and cannot be provided. */
|
|
10
|
+
export declare function wrapCommand(cmd: string, workspace: string, mode: SandboxMode): {
|
|
11
|
+
file: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
} | undefined;
|