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,50 @@
|
|
|
1
|
+
import type { Message, Model, ModelOpts } from "./harness.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for OpenAI and OpenAI-compatible servers. It speaks two wire APIs:
|
|
4
|
+
* - Responses (/v1/responses): what current OpenAI models need. Since GPT-5.4, Chat Completions rejects function tools combined with
|
|
5
|
+
* reasoning ("Function tools with reasoning_effort are not supported ... use /v1/responses"), and the GPT-6 family is Responses-first.
|
|
6
|
+
* - Chat Completions (/v1/chat/completions): everything else (gpt-4.x, Ollama, vLLM, OpenRouter, most gateways).
|
|
7
|
+
* `api: "auto"` picks Responses for reasoning models on api.openai.com and Chat elsewhere, and it self-corrects: if a server says to use
|
|
8
|
+
* /v1/responses it switches, if /v1/responses does not exist it switches back, and if a reasoning option is refused it drops that option.
|
|
9
|
+
*/
|
|
10
|
+
export type Effort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
11
|
+
export type OpenAIOptions = {
|
|
12
|
+
/** Let the caller handle 429 responses (the shared rate limiter does); the adapter still retries timeouts and 5xx. */
|
|
13
|
+
externalRetry?: boolean;
|
|
14
|
+
/** Server-side compaction (Responses API): once the prompt passes this many tokens the server compacts the conversation into an opaque item, and we stop re-sending what came before it. Off by default; ignored on servers that do not know the parameter. */
|
|
15
|
+
compactAt?: number;
|
|
16
|
+
model?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
retries?: number;
|
|
20
|
+
/** extra request fields merged into every body */
|
|
21
|
+
extra?: object;
|
|
22
|
+
api?: "auto" | "responses" | "chat";
|
|
23
|
+
/** reasoning effort for reasoning models; omit for the model's default */
|
|
24
|
+
effort?: Effort;
|
|
25
|
+
/** ask for reasoning summaries (streamed to `onThinking`). Default on; dropped automatically if the org may not use them. */
|
|
26
|
+
summary?: boolean;
|
|
27
|
+
/** let OpenAI keep the response server-side. Default false: stateless, reasoning is replayed by us (encrypted), nothing is retained. */
|
|
28
|
+
store?: boolean;
|
|
29
|
+
/** stable key that improves prompt-cache hits across requests (OpenAI only) */
|
|
30
|
+
promptCacheKey?: string;
|
|
31
|
+
};
|
|
32
|
+
export declare class ApiError extends Error {
|
|
33
|
+
status: number; /** the server's Retry-After, in milliseconds, when it sent one */
|
|
34
|
+
retryAfterMs?: number;
|
|
35
|
+
constructor(status: number, message: string, retryAfterMs?: number);
|
|
36
|
+
}
|
|
37
|
+
export declare const isReasoningModel: (m: string) => boolean;
|
|
38
|
+
export declare const isOpenAIHost: (base: string) => boolean;
|
|
39
|
+
/** Reasoning-effort values a model accepts. Unknown reasoning models get the broad list; the adapter drops a value the server refuses. */
|
|
40
|
+
export declare function effortsFor(model: string): Effort[];
|
|
41
|
+
/** Our transcript -> Responses `instructions` + `input` items. Reasoning/function-call items from the same model are replayed verbatim. */
|
|
42
|
+
export declare function toResponsesInput(msgs: Message[], model: string): {
|
|
43
|
+
instructions?: string;
|
|
44
|
+
input: any[];
|
|
45
|
+
};
|
|
46
|
+
/** A finished Responses object -> our Message. `raw` keeps every output item so the next request can replay reasoning. */
|
|
47
|
+
export declare function fromResponse(resp: any, model: string): Message;
|
|
48
|
+
/** Read a streamed Responses call: forward text/reasoning deltas, return the final response object (completed/incomplete). */
|
|
49
|
+
export declare function readResponsesStream(r: Response, h: Pick<ModelOpts, "onText" | "onThinking">): Promise<any>;
|
|
50
|
+
export declare function openai(opts?: OpenAIOptions): Model;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status; /** the server's Retry-After, in milliseconds, when it sent one */
|
|
3
|
+
retryAfterMs;
|
|
4
|
+
constructor(status, message, retryAfterMs) { super(message); this.status = status; this.retryAfterMs = retryAfterMs; }
|
|
5
|
+
}
|
|
6
|
+
export const isReasoningModel = (m) => /^(o\d|gpt-([5-9]|\d{2,}))/i.test(m);
|
|
7
|
+
export const isOpenAIHost = (base) => { try {
|
|
8
|
+
return new URL(base).hostname === "api.openai.com";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
} };
|
|
13
|
+
/** Reasoning-effort values a model accepts. Unknown reasoning models get the broad list; the adapter drops a value the server refuses. */
|
|
14
|
+
export function effortsFor(model) {
|
|
15
|
+
if (!isReasoningModel(model))
|
|
16
|
+
return [];
|
|
17
|
+
if (/gpt-6(\.\d+)?-(astra|sol)/i.test(model))
|
|
18
|
+
return ["low", "medium", "high", "xhigh", "max"]; // no `none` / `minimal` on the big models
|
|
19
|
+
return ["none", "low", "medium", "high", "xhigh", "max"];
|
|
20
|
+
}
|
|
21
|
+
// ------------------------------------------------------------------ Responses API: request side
|
|
22
|
+
/** Our transcript -> Responses `instructions` + `input` items. Reasoning/function-call items from the same model are replayed verbatim. */
|
|
23
|
+
export function toResponsesInput(msgs, model) {
|
|
24
|
+
let instructions;
|
|
25
|
+
const input = [];
|
|
26
|
+
for (const m of msgs) {
|
|
27
|
+
if (m.role === "system")
|
|
28
|
+
instructions = (instructions ? instructions + "\n\n" : "") + (m.content ?? "");
|
|
29
|
+
else if (m.role === "user")
|
|
30
|
+
input.push({ role: "user", content: m.images?.length ? [{ type: "input_text", text: m.content || "(continue)" }, ...m.images.map((i) => ({ type: "input_image", image_url: `data:${i.mime};base64,${i.data}` }))] : m.content || "(continue)" });
|
|
31
|
+
else if (m.role === "tool")
|
|
32
|
+
input.push({ type: "function_call_output", call_id: m.tool_call_id, output: m.content ?? "" });
|
|
33
|
+
else {
|
|
34
|
+
const raw = m.raw;
|
|
35
|
+
if (raw?.api === "responses" && raw.model === model && Array.isArray(raw.items)) {
|
|
36
|
+
input.push(...raw.items);
|
|
37
|
+
continue;
|
|
38
|
+
} // keeps encrypted reasoning
|
|
39
|
+
if (m.content)
|
|
40
|
+
input.push({ role: "assistant", content: m.content });
|
|
41
|
+
for (const c of m.tool_calls ?? [])
|
|
42
|
+
input.push({ type: "function_call", call_id: c.id, name: c.function.name, arguments: c.function.arguments || "{}" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Everything before the newest compaction item is already inside it: sending it again would defeat the point.
|
|
46
|
+
let cut = -1;
|
|
47
|
+
input.forEach((it, i) => { if (it?.type === "compaction")
|
|
48
|
+
cut = i; });
|
|
49
|
+
return { instructions, input: cut > 0 ? input.slice(cut) : input };
|
|
50
|
+
}
|
|
51
|
+
const toResponsesTool = (t) => ({
|
|
52
|
+
type: "function", name: t.name, description: t.description || undefined,
|
|
53
|
+
parameters: t.parameters.type === "object" ? t.parameters : { type: "object", properties: {} },
|
|
54
|
+
strict: false, // our schemas are not strict-mode shaped; be explicit instead of letting the API guess
|
|
55
|
+
});
|
|
56
|
+
// ------------------------------------------------------------------ Responses API: response side
|
|
57
|
+
/** A finished Responses object -> our Message. `raw` keeps every output item so the next request can replay reasoning. */
|
|
58
|
+
export function fromResponse(resp, model) {
|
|
59
|
+
const items = resp.output ?? [];
|
|
60
|
+
let text = "", calls = [], keep = items;
|
|
61
|
+
for (const it of items) {
|
|
62
|
+
if (it.type === "message")
|
|
63
|
+
for (const c of it.content ?? [])
|
|
64
|
+
text += c.type === "output_text" ? c.text : c.type === "refusal" ? `[declined] ${c.refusal}` : "";
|
|
65
|
+
else if (it.type === "function_call")
|
|
66
|
+
calls.push({ id: it.call_id, type: "function", function: { name: it.name, arguments: it.arguments || "{}" } });
|
|
67
|
+
}
|
|
68
|
+
if (resp.status === "incomplete" && resp.incomplete_details?.reason === "max_output_tokens" && calls.length) {
|
|
69
|
+
// a tool call cut off mid-JSON must never run; drop it (and reasoning, which is invalid without its follow-up item) and nudge a smaller step
|
|
70
|
+
calls = [];
|
|
71
|
+
keep = items.filter((i) => i.type === "message");
|
|
72
|
+
text = `${text}\n[output hit the max_output_tokens limit; I'll redo that in smaller steps]`.trim();
|
|
73
|
+
}
|
|
74
|
+
const u = resp.usage ?? {};
|
|
75
|
+
return {
|
|
76
|
+
role: "assistant", content: text || null, ...(calls.length && { tool_calls: calls }), raw: { api: "responses", model, items: keep },
|
|
77
|
+
usage: {
|
|
78
|
+
prompt_tokens: u.input_tokens ?? 0, completion_tokens: u.output_tokens ?? 0, total_tokens: u.total_tokens ?? (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
|
|
79
|
+
cached_tokens: u.input_tokens_details?.cached_tokens ?? 0, reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? 0,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Read a streamed Responses call: forward text/reasoning deltas, return the final response object (completed/incomplete). */
|
|
84
|
+
export async function readResponsesStream(r, h) {
|
|
85
|
+
const items = [];
|
|
86
|
+
let final, buf = "";
|
|
87
|
+
const dec = new TextDecoder();
|
|
88
|
+
const handle = (j) => {
|
|
89
|
+
switch (j.type) {
|
|
90
|
+
case "response.output_text.delta":
|
|
91
|
+
if (j.delta)
|
|
92
|
+
h.onText?.(j.delta);
|
|
93
|
+
break;
|
|
94
|
+
case "response.reasoning_summary_text.delta":
|
|
95
|
+
if (j.delta)
|
|
96
|
+
h.onThinking?.(j.delta);
|
|
97
|
+
break;
|
|
98
|
+
case "response.output_item.done":
|
|
99
|
+
items[j.output_index ?? items.length] = j.item;
|
|
100
|
+
break;
|
|
101
|
+
case "response.completed":
|
|
102
|
+
case "response.incomplete":
|
|
103
|
+
final = j.response;
|
|
104
|
+
break;
|
|
105
|
+
case "response.failed": throw new Error(`response failed: ${j.response?.error?.message ?? "unknown error"}`);
|
|
106
|
+
case "error": throw new Error(j.message ?? j.error?.message ?? "stream error");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
for await (const chunk of r.body) {
|
|
110
|
+
buf += dec.decode(chunk, { stream: true });
|
|
111
|
+
for (let i; (i = buf.indexOf("\n")) >= 0;) {
|
|
112
|
+
const line = buf.slice(0, i).trim();
|
|
113
|
+
buf = buf.slice(i + 1);
|
|
114
|
+
if (!line.startsWith("data:") || line.includes("[DONE]"))
|
|
115
|
+
continue;
|
|
116
|
+
let j;
|
|
117
|
+
try {
|
|
118
|
+
j = JSON.parse(line.slice(5));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
handle(j);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (final) {
|
|
127
|
+
if (!final.output?.length && items.length)
|
|
128
|
+
final.output = items.filter(Boolean);
|
|
129
|
+
return final;
|
|
130
|
+
}
|
|
131
|
+
if (items.length)
|
|
132
|
+
return { output: items.filter(Boolean), status: "incomplete", incomplete_details: { reason: "stream_ended" } };
|
|
133
|
+
throw new Error("the response stream ended before it completed");
|
|
134
|
+
}
|
|
135
|
+
// ------------------------------------------------------------------ Chat Completions: streaming reader
|
|
136
|
+
async function readChatStream(r, onText) {
|
|
137
|
+
const calls = [];
|
|
138
|
+
let content = "", usage, buf = "";
|
|
139
|
+
const dec = new TextDecoder();
|
|
140
|
+
for await (const chunk of r.body) {
|
|
141
|
+
buf += dec.decode(chunk, { stream: true });
|
|
142
|
+
for (let i; (i = buf.indexOf("\n")) >= 0;) {
|
|
143
|
+
const line = buf.slice(0, i).trim();
|
|
144
|
+
buf = buf.slice(i + 1);
|
|
145
|
+
if (!line.startsWith("data:") || line.includes("[DONE]"))
|
|
146
|
+
continue;
|
|
147
|
+
let j;
|
|
148
|
+
try {
|
|
149
|
+
j = JSON.parse(line.slice(5));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (j.usage)
|
|
155
|
+
usage = { prompt_tokens: j.usage.prompt_tokens, completion_tokens: j.usage.completion_tokens, total_tokens: j.usage.total_tokens, cached_tokens: j.usage.prompt_tokens_details?.cached_tokens, reasoning_tokens: j.usage.completion_tokens_details?.reasoning_tokens };
|
|
156
|
+
const d = j.choices?.[0]?.delta;
|
|
157
|
+
if (!d)
|
|
158
|
+
continue;
|
|
159
|
+
if (d.content) {
|
|
160
|
+
content += d.content;
|
|
161
|
+
onText(d.content);
|
|
162
|
+
}
|
|
163
|
+
for (const tc of d.tool_calls ?? []) {
|
|
164
|
+
const c = (calls[tc.index ?? 0] ??= { id: "", type: "function", function: { name: "", arguments: "" } });
|
|
165
|
+
if (tc.id)
|
|
166
|
+
c.id = tc.id;
|
|
167
|
+
if (tc.function?.name)
|
|
168
|
+
c.function.name += tc.function.name;
|
|
169
|
+
if (tc.function?.arguments)
|
|
170
|
+
c.function.arguments += tc.function.arguments;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const tool_calls = calls.filter(Boolean);
|
|
175
|
+
return { role: "assistant", content: content || null, ...(tool_calls.length && { tool_calls }), usage };
|
|
176
|
+
}
|
|
177
|
+
// ------------------------------------------------------------------ the adapter
|
|
178
|
+
export function openai(opts = {}) {
|
|
179
|
+
const base = (opts.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
180
|
+
const key = opts.apiKey ?? process.env.OPENAI_API_KEY ?? "";
|
|
181
|
+
const model = opts.model ?? process.env.AMNESS_MODEL ?? "gpt-6.1-sol";
|
|
182
|
+
const retries = opts.retries ?? 4;
|
|
183
|
+
const official = isOpenAIHost(base);
|
|
184
|
+
const mode = opts.api ?? "auto";
|
|
185
|
+
// adapter state that self-corrects at runtime
|
|
186
|
+
// optional request fields a server may not know; if it says so we stop sending that one field
|
|
187
|
+
const OPTIONAL = ["prompt_cache_key", "prompt_cache_retention", "include", "stream_options", "parallel_tool_calls", "context_management"], dropped = new Set();
|
|
188
|
+
let useResponses = mode === "responses" || (mode === "auto" && official && isReasoningModel(model));
|
|
189
|
+
let sendSummary = opts.summary ?? true, sendEffort = opts.effort !== undefined, chatNoReasoning = false, flips = 0, chatRefused = false;
|
|
190
|
+
/** POST with retry/backoff on 408/429/5xx and network errors; other 4xx become ApiError immediately. */
|
|
191
|
+
async function post(path, body, signal) {
|
|
192
|
+
const payload = JSON.stringify(body);
|
|
193
|
+
for (let i = 0;; i++) {
|
|
194
|
+
let err;
|
|
195
|
+
try {
|
|
196
|
+
const r = await fetch(base + path, { method: "POST", body: payload, signal, headers: { "content-type": "application/json", authorization: `Bearer ${key}` } });
|
|
197
|
+
if (r.ok)
|
|
198
|
+
return r;
|
|
199
|
+
const status = r.status, text = await r.text();
|
|
200
|
+
const ra = r.headers.get("retry-after-ms") ? Number(r.headers.get("retry-after-ms")) : r.headers.get("retry-after") && Number.isFinite(Number(r.headers.get("retry-after"))) ? Number(r.headers.get("retry-after")) * 1000 : undefined;
|
|
201
|
+
err = new ApiError(status, `HTTP ${status}: ${text}`, ra);
|
|
202
|
+
if (status !== 408 && status !== 429 && status < 500)
|
|
203
|
+
throw err;
|
|
204
|
+
if (status === 429 && opts.externalRetry)
|
|
205
|
+
throw err; // a shared rate limiter owns 429s: retrying here would bypass it
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
if (e instanceof ApiError && e.status !== 408 && e.status !== 429 && e.status < 500)
|
|
209
|
+
throw e;
|
|
210
|
+
if (signal?.aborted)
|
|
211
|
+
throw e;
|
|
212
|
+
err = e;
|
|
213
|
+
}
|
|
214
|
+
if (i >= retries - 1)
|
|
215
|
+
throw err;
|
|
216
|
+
await new Promise((res) => setTimeout(res, Math.max(2 ** i * 1000, err.retryAfterMs ?? 0) + Math.random() * 500));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function viaResponses(messages, tools, o) {
|
|
220
|
+
const { instructions, input } = toResponsesInput(messages, model);
|
|
221
|
+
const reasoning = isReasoningModel(model) ? { ...(sendEffort && opts.effort ? { effort: opts.effort } : {}), ...(sendSummary ? { summary: "auto" } : {}) } : undefined;
|
|
222
|
+
const store = opts.store ?? false;
|
|
223
|
+
const body = {
|
|
224
|
+
model, input, ...(instructions && { instructions }), store, stream: !!o.onText,
|
|
225
|
+
...(tools.length && { tools: tools.map(toResponsesTool), tool_choice: "auto" }),
|
|
226
|
+
...(reasoning && Object.keys(reasoning).length && { reasoning }),
|
|
227
|
+
...(reasoning && !store && { include: ["reasoning.encrypted_content"] }), // older reasoning models only return the replayable blob when asked; newer ones always do
|
|
228
|
+
...(opts.promptCacheKey && official && { prompt_cache_key: opts.promptCacheKey, prompt_cache_retention: "24h" }), // keep the cached prefix alive across idle gaps; dropped automatically if a model refuses it
|
|
229
|
+
...(opts.compactAt && official && { context_management: [{ type: "compaction", compact_threshold: opts.compactAt }] }),
|
|
230
|
+
...opts.extra,
|
|
231
|
+
};
|
|
232
|
+
for (const k of dropped)
|
|
233
|
+
delete body[k];
|
|
234
|
+
const r = await post("/responses", body, o.signal);
|
|
235
|
+
const resp = (r.headers.get("content-type") ?? "").includes("text/event-stream") ? await readResponsesStream(r, o) : await r.json();
|
|
236
|
+
if (resp.error)
|
|
237
|
+
throw new Error(`response failed: ${resp.error.message ?? JSON.stringify(resp.error)}`);
|
|
238
|
+
return fromResponse(resp, model);
|
|
239
|
+
}
|
|
240
|
+
async function viaChat(messages, tools, o) {
|
|
241
|
+
const stream = !!o.onText;
|
|
242
|
+
const body = {
|
|
243
|
+
model, // only wire fields: `raw` / `usage` belong to other adapters, and replayed tool calls need an explicit type
|
|
244
|
+
messages: messages.map(({ role, content, tool_calls, tool_call_id, images }) => ({ role, content: images?.length ? [{ type: "text", text: content || "(continue)" }, ...images.map((i) => ({ type: "image_url", image_url: { url: `data:${i.mime};base64,${i.data}` } }))] : content, ...(tool_calls && { tool_calls: tool_calls.map((c) => ({ id: c.id, type: "function", function: c.function })) }), ...(tool_call_id && { tool_call_id }) })),
|
|
245
|
+
...(tools.length && { tools: tools.map((t) => ({ type: "function", function: t })) }),
|
|
246
|
+
...(stream && { stream: true, stream_options: { include_usage: true } }),
|
|
247
|
+
...(chatNoReasoning && isReasoningModel(model) ? { reasoning_effort: "none" } : opts.effort && isReasoningModel(model) && sendEffort ? { reasoning_effort: opts.effort } : {}),
|
|
248
|
+
...opts.extra,
|
|
249
|
+
};
|
|
250
|
+
for (const k of dropped)
|
|
251
|
+
delete body[k];
|
|
252
|
+
const r = await post("/chat/completions", body, o.signal);
|
|
253
|
+
if ((r.headers.get("content-type") ?? "").includes("text/event-stream"))
|
|
254
|
+
return readChatStream(r, o.onText ?? (() => { }));
|
|
255
|
+
const j = await r.json(), m = j.choices[0].message; // server ignored `stream`, or we did not ask
|
|
256
|
+
if (stream && m.content)
|
|
257
|
+
o.onText(m.content);
|
|
258
|
+
const u = j.usage ?? {};
|
|
259
|
+
return { ...m, usage: { prompt_tokens: u.prompt_tokens, completion_tokens: u.completion_tokens, total_tokens: u.total_tokens, cached_tokens: u.prompt_tokens_details?.cached_tokens, reasoning_tokens: u.completion_tokens_details?.reasoning_tokens } };
|
|
260
|
+
}
|
|
261
|
+
const call = async (messages, tools, o2) => {
|
|
262
|
+
const o = o2 ?? {};
|
|
263
|
+
for (let step = 0;; step++) {
|
|
264
|
+
try {
|
|
265
|
+
return useResponses ? await viaResponses(messages, tools, o) : await viaChat(messages, tools, o);
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
if (!(e instanceof ApiError) || step >= 6)
|
|
269
|
+
throw e;
|
|
270
|
+
const msg = e.message;
|
|
271
|
+
const refused = e.status === 400 && /unknown|unsupported|unrecognized|not permitted|invalid/i.test(msg) ? OPTIONAL.find((k) => msg.includes(k) && !dropped.has(k)) : undefined;
|
|
272
|
+
if (refused) {
|
|
273
|
+
dropped.add(refused);
|
|
274
|
+
continue;
|
|
275
|
+
} // e.g. a gateway that has never heard of prompt_cache_key
|
|
276
|
+
// the server tells us which endpoint to use
|
|
277
|
+
if (!useResponses && e.status === 400 && /\/v1\/responses|responses api/i.test(msg) && mode !== "chat" && flips++ < 2) {
|
|
278
|
+
useResponses = true;
|
|
279
|
+
chatRefused = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!useResponses && e.status === 400 && /reasoning_effort/i.test(msg) && !chatNoReasoning) {
|
|
283
|
+
chatNoReasoning = true;
|
|
284
|
+
continue;
|
|
285
|
+
} // documented workaround: tools + reasoning_effort "none"
|
|
286
|
+
if (useResponses && (e.status === 404 || e.status === 405) && mode !== "responses" && flips++ < 2) {
|
|
287
|
+
useResponses = false;
|
|
288
|
+
if (chatRefused)
|
|
289
|
+
chatNoReasoning = true;
|
|
290
|
+
continue;
|
|
291
|
+
} // gateway has no /responses: back to chat, already using the workaround
|
|
292
|
+
// a reasoning option this account/model refuses: drop just that option and carry on
|
|
293
|
+
if (useResponses && e.status === 400 && sendSummary && /summary|verif/i.test(msg)) {
|
|
294
|
+
sendSummary = false;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (useResponses && e.status === 400 && sendEffort && /effort|reasoning/i.test(msg)) {
|
|
298
|
+
sendEffort = false;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
call.api = () => (useResponses ? "responses" : "chat");
|
|
306
|
+
return call;
|
|
307
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type Attempt = {
|
|
2
|
+
summary?: string;
|
|
3
|
+
tokens?: number;
|
|
4
|
+
};
|
|
5
|
+
export type Candidate = {
|
|
6
|
+
index: number;
|
|
7
|
+
diff: string;
|
|
8
|
+
tokens: number;
|
|
9
|
+
summary?: string;
|
|
10
|
+
passed: boolean;
|
|
11
|
+
extraPassed: number;
|
|
12
|
+
verifyOutput: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
export type BestOfNOptions = {
|
|
16
|
+
repo: string;
|
|
17
|
+
n: number;
|
|
18
|
+
/** Do the work in `dir` (a fresh worktree of HEAD). `index` is 0-based; use it to vary the attempt if you like. */
|
|
19
|
+
attempt: (dir: string, index: number) => Promise<Attempt>;
|
|
20
|
+
/** The user's verify command: a candidate that fails it is never chosen. */
|
|
21
|
+
verify?: string;
|
|
22
|
+
/** Extra commands (for example generated tests): among passing candidates, the one passing most of these wins. */
|
|
23
|
+
extraChecks?: string[];
|
|
24
|
+
/** Optional last-resort judge over the passing candidates that are still tied; returns the index of its pick. */
|
|
25
|
+
judge?: (c: Candidate[]) => Promise<number>;
|
|
26
|
+
/** Stop starting attempts after this many tokens in total. */
|
|
27
|
+
maxTokens?: number;
|
|
28
|
+
/** Run all n at once instead of one by one until one passes. Costs n attempts every time. */
|
|
29
|
+
parallel?: boolean;
|
|
30
|
+
/** Apply the winner's diff to `repo`. */
|
|
31
|
+
apply?: boolean;
|
|
32
|
+
onCandidate?: (c: Candidate) => void;
|
|
33
|
+
};
|
|
34
|
+
export type BestOfNResult = {
|
|
35
|
+
winner?: Candidate;
|
|
36
|
+
candidates: Candidate[];
|
|
37
|
+
tokens: number;
|
|
38
|
+
stoppedForBudget: boolean;
|
|
39
|
+
applied?: boolean;
|
|
40
|
+
applyError?: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function bestOfN(o: BestOfNOptions): Promise<BestOfNResult>;
|
|
43
|
+
export type Part = {
|
|
44
|
+
name: string;
|
|
45
|
+
task: string;
|
|
46
|
+
};
|
|
47
|
+
export type PartResult = {
|
|
48
|
+
part: Part;
|
|
49
|
+
status: "merged" | "failed-verify" | "conflict" | "broke-verify" | "no-change" | "error" | "skipped-budget";
|
|
50
|
+
tokens: number;
|
|
51
|
+
summary?: string;
|
|
52
|
+
detail?: string;
|
|
53
|
+
};
|
|
54
|
+
export type ParallelOptions = {
|
|
55
|
+
repo: string;
|
|
56
|
+
parts: Part[];
|
|
57
|
+
attempt: (part: Part, dir: string) => Promise<Attempt>;
|
|
58
|
+
verify?: string;
|
|
59
|
+
maxTokens?: number;
|
|
60
|
+
concurrency?: number;
|
|
61
|
+
onPart?: (r: PartResult) => void;
|
|
62
|
+
};
|
|
63
|
+
export declare function parallelParts(o: ParallelOptions): Promise<{
|
|
64
|
+
results: PartResult[];
|
|
65
|
+
tokens: number;
|
|
66
|
+
}>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Several agents at once, each in its own git worktree so they cannot disturb each other or your working tree. Two uses:
|
|
2
|
+
// bestOfN the same task attempted up to n times; a candidate must pass your verify command, then ties are broken by extra checks, an optional judge, and diff size.
|
|
3
|
+
// By default attempts run one at a time and stop at the first pass (escalation), so the extra cost is only paid for tasks that need it.
|
|
4
|
+
// parallelParts independent parts of a task run concurrently; each part must pass verify in its own worktree, and parts are then merged one at a time through verify again.
|
|
5
|
+
// A part that conflicts or breaks verify after merging is reverted and reported, never left half-applied.
|
|
6
|
+
// Budgets are explicit: every attempt reports its tokens, and no new attempt starts once the cap is reached.
|
|
7
|
+
import { runShell } from "./code.js";
|
|
8
|
+
import { applyDiff, createWorktree, removeWorktree, revertDiff, worktreeDiff } from "./worktree.js";
|
|
9
|
+
const passes = async (cmd, dir) => { if (!cmd)
|
|
10
|
+
return { ok: true, out: "" }; const out = await runShell(cmd, dir, 300_000); return { ok: out.startsWith("exit=0"), out }; };
|
|
11
|
+
const lines = (d) => d.split("\n").length;
|
|
12
|
+
export async function bestOfN(o) {
|
|
13
|
+
const candidates = [];
|
|
14
|
+
let tokens = 0, stoppedForBudget = false;
|
|
15
|
+
const one = async (index) => {
|
|
16
|
+
const w = await createWorktree(o.repo);
|
|
17
|
+
const c = { index, diff: "", tokens: 0, passed: false, extraPassed: 0, verifyOutput: "" };
|
|
18
|
+
try {
|
|
19
|
+
const a = await o.attempt(w.dir, index);
|
|
20
|
+
c.tokens = a.tokens ?? 0;
|
|
21
|
+
c.summary = a.summary;
|
|
22
|
+
c.diff = await worktreeDiff(w);
|
|
23
|
+
if (c.diff) {
|
|
24
|
+
const v = await passes(o.verify, w.dir);
|
|
25
|
+
c.passed = v.ok;
|
|
26
|
+
c.verifyOutput = v.out.slice(0, 1500);
|
|
27
|
+
if (c.passed)
|
|
28
|
+
for (const x of o.extraChecks ?? [])
|
|
29
|
+
if ((await passes(x, w.dir)).ok)
|
|
30
|
+
c.extraPassed++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
c.error = e.message;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await removeWorktree(w);
|
|
38
|
+
}
|
|
39
|
+
tokens += c.tokens;
|
|
40
|
+
candidates.push(c);
|
|
41
|
+
o.onCandidate?.(c);
|
|
42
|
+
return c;
|
|
43
|
+
};
|
|
44
|
+
if (o.parallel)
|
|
45
|
+
await Promise.all(Array.from({ length: o.n }, (_, i) => one(i)));
|
|
46
|
+
else
|
|
47
|
+
for (let i = 0; i < o.n; i++) {
|
|
48
|
+
if (o.maxTokens !== undefined && tokens >= o.maxTokens) {
|
|
49
|
+
stoppedForBudget = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const c = await one(i);
|
|
53
|
+
if (c.passed && !(o.extraChecks?.length))
|
|
54
|
+
break; // with extra checks, a first pass is still compared against later ones only if it missed some
|
|
55
|
+
if (c.passed && c.extraPassed === (o.extraChecks?.length ?? 0))
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
candidates.sort((a, b) => a.index - b.index);
|
|
59
|
+
let pool = candidates.filter((c) => c.passed);
|
|
60
|
+
const best = Math.max(0, ...pool.map((c) => c.extraPassed));
|
|
61
|
+
pool = pool.filter((c) => c.extraPassed === best);
|
|
62
|
+
if (pool.length > 1 && o.judge) {
|
|
63
|
+
const pick = await o.judge(pool).catch(() => -1);
|
|
64
|
+
const w = pool.find((c) => c.index === pick);
|
|
65
|
+
if (w)
|
|
66
|
+
pool = [w];
|
|
67
|
+
}
|
|
68
|
+
const winner = pool.sort((a, b) => lines(a.diff) - lines(b.diff) || a.index - b.index)[0];
|
|
69
|
+
const res = { winner, candidates, tokens, stoppedForBudget };
|
|
70
|
+
if (o.apply && winner) {
|
|
71
|
+
const err = await applyDiff(o.repo, winner.diff);
|
|
72
|
+
res.applied = !err;
|
|
73
|
+
res.applyError = err;
|
|
74
|
+
}
|
|
75
|
+
return res;
|
|
76
|
+
}
|
|
77
|
+
export async function parallelParts(o) {
|
|
78
|
+
const done = [];
|
|
79
|
+
let tokens = 0, next = 0;
|
|
80
|
+
const worker = async () => {
|
|
81
|
+
while (next < o.parts.length) {
|
|
82
|
+
const part = o.parts[next++];
|
|
83
|
+
if (o.maxTokens !== undefined && tokens >= o.maxTokens) {
|
|
84
|
+
done.push({ part, diff: "", tokens: 0, res: { part, status: "skipped-budget", tokens: 0, detail: "token cap reached before this part started" } });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const w = await createWorktree(o.repo);
|
|
88
|
+
const d = { part, diff: "", tokens: 0 };
|
|
89
|
+
try {
|
|
90
|
+
const a = await o.attempt(part, w.dir);
|
|
91
|
+
d.tokens = a.tokens ?? 0;
|
|
92
|
+
d.summary = a.summary;
|
|
93
|
+
tokens += d.tokens;
|
|
94
|
+
d.diff = await worktreeDiff(w);
|
|
95
|
+
if (!d.diff)
|
|
96
|
+
d.res = { part, status: "no-change", tokens: d.tokens, summary: a.summary };
|
|
97
|
+
else {
|
|
98
|
+
const v = await passes(o.verify, w.dir);
|
|
99
|
+
if (!v.ok)
|
|
100
|
+
d.res = { part, status: "failed-verify", tokens: d.tokens, summary: a.summary, detail: v.out.slice(0, 800) };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
d.res = { part, status: "error", tokens: d.tokens, detail: e.message };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await removeWorktree(w);
|
|
108
|
+
}
|
|
109
|
+
done.push(d);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(o.concurrency ?? o.parts.length, o.parts.length)) }, worker));
|
|
113
|
+
done.sort((a, b) => o.parts.indexOf(a.part) - o.parts.indexOf(b.part));
|
|
114
|
+
const results = [];
|
|
115
|
+
for (const d of done) { // merge strictly one at a time, in the order given
|
|
116
|
+
let r = d.res;
|
|
117
|
+
if (!r) {
|
|
118
|
+
const err = await applyDiff(o.repo, d.diff);
|
|
119
|
+
if (err)
|
|
120
|
+
r = { part: d.part, status: "conflict", tokens: d.tokens, summary: d.summary, detail: err.slice(0, 400) };
|
|
121
|
+
else {
|
|
122
|
+
const v = await passes(o.verify, o.repo);
|
|
123
|
+
if (v.ok)
|
|
124
|
+
r = { part: d.part, status: "merged", tokens: d.tokens, summary: d.summary };
|
|
125
|
+
else {
|
|
126
|
+
await revertDiff(o.repo, d.diff);
|
|
127
|
+
r = { part: d.part, status: "broke-verify", tokens: d.tokens, summary: d.summary, detail: v.out.slice(0, 800) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
results.push(r);
|
|
132
|
+
o.onPart?.(r);
|
|
133
|
+
}
|
|
134
|
+
return { results, tokens };
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type Line = {
|
|
2
|
+
t: " " | "-" | "+";
|
|
3
|
+
s: string;
|
|
4
|
+
};
|
|
5
|
+
export type Hunk = {
|
|
6
|
+
anchor?: string;
|
|
7
|
+
lines: Line[]; /** line number hint from a unified diff (1-based) */
|
|
8
|
+
hint?: number;
|
|
9
|
+
};
|
|
10
|
+
export type FileOp = {
|
|
11
|
+
path: string;
|
|
12
|
+
op: "update" | "add" | "delete";
|
|
13
|
+
moveTo?: string;
|
|
14
|
+
hunks: Hunk[];
|
|
15
|
+
};
|
|
16
|
+
export declare function parsePatch(text: string): FileOp[];
|
|
17
|
+
/** Every path a patch touches (for permission checks). */
|
|
18
|
+
export declare function patchPaths(text: string): string[];
|
|
19
|
+
export type Applied = {
|
|
20
|
+
path: string;
|
|
21
|
+
before: string | null;
|
|
22
|
+
after: string | null;
|
|
23
|
+
fuzzy: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function applyOps(ops: FileOp[], read: (path: string) => string | undefined): {
|
|
26
|
+
applied: Applied[];
|
|
27
|
+
errors: string[];
|
|
28
|
+
};
|
|
29
|
+
/** Replace `old` by `nw` in `content`: exactly once. When there is no exact match and `old` spans whole lines, a match that differs only in indentation or trailing space is accepted if it is unique (and reported). */
|
|
30
|
+
export declare function replaceOnce(content: string, old: string, nw: string): {
|
|
31
|
+
out: string;
|
|
32
|
+
fuzzy?: boolean;
|
|
33
|
+
} | {
|
|
34
|
+
error: string;
|
|
35
|
+
};
|
|
36
|
+
/** Apply several edits in order to one file's text; all-or-nothing. */
|
|
37
|
+
export declare function replaceMany(content: string, edits: {
|
|
38
|
+
old: string;
|
|
39
|
+
new: string;
|
|
40
|
+
}[]): {
|
|
41
|
+
out: string;
|
|
42
|
+
fuzzy: number;
|
|
43
|
+
} | {
|
|
44
|
+
error: string;
|
|
45
|
+
};
|