@tjamescouch/gro 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/drivers/anthropic.js +256 -0
  2. package/dist/drivers/index.js +2 -0
  3. package/dist/drivers/streaming-openai.js +262 -0
  4. package/dist/drivers/types.js +1 -0
  5. package/dist/errors.js +79 -0
  6. package/dist/logger.js +30 -0
  7. package/dist/main.js +867 -0
  8. package/dist/mcp/client.js +130 -0
  9. package/dist/mcp/index.js +1 -0
  10. package/dist/memory/advanced-memory.js +210 -0
  11. package/dist/memory/agent-memory.js +52 -0
  12. package/dist/memory/agenthnsw.js +86 -0
  13. package/{src/memory/index.ts → dist/memory/index.js} +0 -1
  14. package/dist/memory/simple-memory.js +34 -0
  15. package/dist/memory/vector-index.js +7 -0
  16. package/dist/package.json +22 -0
  17. package/dist/session.js +110 -0
  18. package/dist/tools/agentpatch.js +91 -0
  19. package/dist/tools/bash.js +61 -0
  20. package/dist/tools/version.js +76 -0
  21. package/dist/utils/rate-limiter.js +46 -0
  22. package/{src/utils/retry.ts → dist/utils/retry.js} +8 -12
  23. package/dist/utils/timed-fetch.js +25 -0
  24. package/gro +0 -0
  25. package/package.json +13 -2
  26. package/.github/workflows/ci.yml +0 -20
  27. package/src/drivers/anthropic.ts +0 -281
  28. package/src/drivers/index.ts +0 -5
  29. package/src/drivers/streaming-openai.ts +0 -258
  30. package/src/drivers/types.ts +0 -39
  31. package/src/errors.ts +0 -97
  32. package/src/logger.ts +0 -28
  33. package/src/main.ts +0 -905
  34. package/src/mcp/client.ts +0 -163
  35. package/src/mcp/index.ts +0 -2
  36. package/src/memory/advanced-memory.ts +0 -263
  37. package/src/memory/agent-memory.ts +0 -61
  38. package/src/memory/agenthnsw.ts +0 -122
  39. package/src/memory/simple-memory.ts +0 -41
  40. package/src/memory/vector-index.ts +0 -30
  41. package/src/session.ts +0 -150
  42. package/src/tools/agentpatch.ts +0 -89
  43. package/src/tools/bash.ts +0 -61
  44. package/src/tools/version.ts +0 -98
  45. package/src/utils/rate-limiter.ts +0 -60
  46. package/src/utils/timed-fetch.ts +0 -29
  47. package/tests/errors.test.ts +0 -246
  48. package/tests/memory.test.ts +0 -186
  49. package/tests/rate-limiter.test.ts +0 -76
  50. package/tests/retry.test.ts +0 -138
  51. package/tests/timed-fetch.test.ts +0 -104
  52. package/tsconfig.json +0 -13
@@ -0,0 +1,91 @@
1
+ /**
2
+ * agentpatch tool integration for gro.
3
+ *
4
+ * Exposes a first-class file editor: `apply_patch`.
5
+ *
6
+ * This wraps the agentpatch patch grammar and applies patches via the
7
+ * `agentpatch/bin/apply_patch` script.
8
+ */
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { Logger } from "../logger.js";
13
+ const DEFAULT_TIMEOUT = 120_000;
14
+ const MAX_OUTPUT = 30_000;
15
+ export function agentpatchToolDefinition() {
16
+ return {
17
+ type: "function",
18
+ function: {
19
+ name: "apply_patch",
20
+ description: "Apply a unified agentpatch-style patch to the working tree (safe, idempotent).",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {
24
+ patch: { type: "string", description: "The patch text (agentpatch grammar)." },
25
+ dry_run: { type: "boolean", description: "If true, validate/preview without writing." },
26
+ verbose: { type: "boolean", description: "If true, emit debug logs from applier." },
27
+ allow_delete: { type: "boolean", description: "Allow *** Delete File ops." },
28
+ allow_rename: { type: "boolean", description: "Allow *** Rename File ops." },
29
+ timeout: { type: "number", description: "Timeout in ms (default 120000)." },
30
+ },
31
+ required: ["patch"],
32
+ },
33
+ },
34
+ };
35
+ }
36
+ function truncate(s) {
37
+ if (s.length <= MAX_OUTPUT)
38
+ return s;
39
+ const half = Math.floor(MAX_OUTPUT / 2);
40
+ return s.slice(0, half) + `\n\n... (truncated ${s.length - MAX_OUTPUT} chars) ...\n\n` + s.slice(-half);
41
+ }
42
+ export function executeAgentpatch(args) {
43
+ const patch = args.patch || "";
44
+ if (!patch.trim())
45
+ return "Error: empty patch";
46
+ const timeout = args.timeout || DEFAULT_TIMEOUT;
47
+ const dryRun = args.dry_run === true;
48
+ const verbose = args.verbose === true;
49
+ const allowDelete = args.allow_delete === true;
50
+ const allowRename = args.allow_rename === true;
51
+ // Expected layout in this monorepo-ish runner: /home/agent/agentpatch
52
+ // If not present, instruct user to clone it.
53
+ const agentpatchPath = process.env.AGENTPATCH_PATH || join(process.env.HOME || "", "agentpatch");
54
+ const bin = join(agentpatchPath, "bin", "apply_patch");
55
+ if (!existsSync(bin)) {
56
+ return `Error: agentpatch not found at ${bin}. Set AGENTPATCH_PATH or clone agentpatch to ~/agentpatch.`;
57
+ }
58
+ const cmd = [bin];
59
+ if (dryRun)
60
+ cmd.push("--dry-run");
61
+ if (verbose)
62
+ cmd.push("--verbose");
63
+ if (allowDelete)
64
+ cmd.push("--allow-delete");
65
+ if (allowRename)
66
+ cmd.push("--allow-rename");
67
+ Logger.debug(`apply_patch: ${cmd.join(" ")}`);
68
+ try {
69
+ const out = execSync(cmd.join(" "), {
70
+ shell: "/bin/bash",
71
+ input: patch,
72
+ encoding: "utf-8",
73
+ timeout,
74
+ maxBuffer: 10 * 1024 * 1024,
75
+ stdio: ["pipe", "pipe", "pipe"],
76
+ });
77
+ return truncate(out || "ok");
78
+ }
79
+ catch (e) {
80
+ let result = "";
81
+ if (e.stdout)
82
+ result += e.stdout;
83
+ if (e.stderr)
84
+ result += (result ? "\n" : "") + e.stderr;
85
+ if (!result)
86
+ result = e.message || "Command failed";
87
+ if (e.status != null)
88
+ result += `\n[exit code: ${e.status}]`;
89
+ return truncate(result);
90
+ }
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Built-in bash tool for gro — executes shell commands and returns output.
3
+ * Gated behind --bash flag. Not enabled by default.
4
+ */
5
+ import { execSync } from "node:child_process";
6
+ import { Logger } from "../logger.js";
7
+ const MAX_OUTPUT = 30_000;
8
+ const DEFAULT_TIMEOUT = 120_000;
9
+ export function bashToolDefinition() {
10
+ return {
11
+ type: "function",
12
+ function: {
13
+ name: "bash",
14
+ description: "Execute a bash command and return its output (stdout + stderr).",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ command: { type: "string", description: "The bash command to execute" },
19
+ timeout: { type: "number", description: "Timeout in milliseconds (default: 120000)" },
20
+ },
21
+ required: ["command"],
22
+ },
23
+ },
24
+ };
25
+ }
26
+ export function executeBash(args) {
27
+ const command = args.command;
28
+ if (!command)
29
+ return "Error: no command provided";
30
+ const timeout = args.timeout || DEFAULT_TIMEOUT;
31
+ Logger.debug(`bash: ${command}`);
32
+ try {
33
+ const output = execSync(command, {
34
+ shell: "/bin/bash",
35
+ encoding: "utf-8",
36
+ timeout,
37
+ maxBuffer: 10 * 1024 * 1024,
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ });
40
+ return truncate(output);
41
+ }
42
+ catch (e) {
43
+ // execSync throws on non-zero exit — capture stdout + stderr
44
+ let result = "";
45
+ if (e.stdout)
46
+ result += e.stdout;
47
+ if (e.stderr)
48
+ result += (result ? "\n" : "") + e.stderr;
49
+ if (!result)
50
+ result = e.message || "Command failed";
51
+ if (e.status != null)
52
+ result += `\n[exit code: ${e.status}]`;
53
+ return truncate(result);
54
+ }
55
+ }
56
+ function truncate(s) {
57
+ if (s.length <= MAX_OUTPUT)
58
+ return s;
59
+ const half = Math.floor(MAX_OUTPUT / 2);
60
+ return s.slice(0, half) + `\n\n... (truncated ${s.length - MAX_OUTPUT} chars) ...\n\n` + s.slice(-half);
61
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Built-in version/identity tool for gro.
3
+ *
4
+ * Lets agents (and humans) introspect the gro runtime — version, provider,
5
+ * model, uptime, process info. This is the canonical way to confirm an
6
+ * agent is running on gro.
7
+ */
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ const startTime = Date.now();
12
+ /** Read version from package.json — single source of truth. */
13
+ function readVersion() {
14
+ // In ESM, __dirname isn't available — derive from import.meta.url
15
+ let selfDir;
16
+ try {
17
+ selfDir = dirname(fileURLToPath(import.meta.url));
18
+ }
19
+ catch {
20
+ selfDir = process.cwd();
21
+ }
22
+ const candidates = [
23
+ join(selfDir, "..", "package.json"), // from dist/tools/ or src/tools/
24
+ join(selfDir, "..", "..", "package.json"), // from deeper nesting
25
+ join(process.cwd(), "package.json"),
26
+ ];
27
+ for (const p of candidates) {
28
+ if (existsSync(p)) {
29
+ try {
30
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
31
+ if (pkg.name === "@tjamescouch/gro" && pkg.version) {
32
+ return pkg.version;
33
+ }
34
+ }
35
+ catch {
36
+ // try next candidate
37
+ }
38
+ }
39
+ }
40
+ return "unknown";
41
+ }
42
+ // Cache version at module load
43
+ const GRO_VERSION = readVersion();
44
+ export function getGroVersion() {
45
+ return GRO_VERSION;
46
+ }
47
+ export function groVersionToolDefinition() {
48
+ return {
49
+ type: "function",
50
+ function: {
51
+ name: "gro_version",
52
+ description: "Report gro runtime identity and version. Returns runtime name, version, provider, model, uptime, and process info. Use this to confirm an agent is running on gro.",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {},
56
+ },
57
+ },
58
+ };
59
+ }
60
+ /**
61
+ * Execute the version tool. Requires runtime config to report provider/model.
62
+ */
63
+ export function executeGroVersion(cfg) {
64
+ const info = {
65
+ runtime: "gro",
66
+ version: GRO_VERSION,
67
+ provider: cfg.provider,
68
+ model: cfg.model,
69
+ pid: process.pid,
70
+ uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
71
+ node_version: process.version,
72
+ platform: process.platform,
73
+ persistent: cfg.persistent,
74
+ };
75
+ return JSON.stringify(info, null, 2);
76
+ }
@@ -0,0 +1,46 @@
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
3
+ }
4
+ class RateLimiter {
5
+ constructor(now) {
6
+ this.states = new Map();
7
+ this.now =
8
+ now ??
9
+ (() => typeof performance !== "undefined" && typeof performance.now === "function"
10
+ ? performance.now()
11
+ : Date.now());
12
+ }
13
+ async limit(name, throughputPerSecond) {
14
+ if (!Number.isFinite(throughputPerSecond) || throughputPerSecond <= 0) {
15
+ throw new RangeError(`throughputPerSecond must be a positive finite number; got ${throughputPerSecond}`);
16
+ }
17
+ const key = name || "default";
18
+ const state = this.getState(key);
19
+ const waitPromise = state.tail.then(async () => {
20
+ const intervalMs = 1000 / throughputPerSecond;
21
+ const now = this.now();
22
+ const scheduledAt = Math.max(now, state.nextAvailableMs);
23
+ state.nextAvailableMs = scheduledAt + intervalMs;
24
+ const delay = scheduledAt - now;
25
+ if (delay > 0)
26
+ await sleep(delay);
27
+ });
28
+ state.tail = waitPromise.catch(() => { });
29
+ return waitPromise;
30
+ }
31
+ reset(name) {
32
+ if (name)
33
+ this.states.delete(name);
34
+ else
35
+ this.states.clear();
36
+ }
37
+ getState(name) {
38
+ let s = this.states.get(name);
39
+ if (!s) {
40
+ s = { nextAvailableMs: this.now(), tail: Promise.resolve() };
41
+ this.states.set(name, s);
42
+ }
43
+ return s;
44
+ }
45
+ }
46
+ export const rateLimiter = new RateLimiter();
@@ -3,30 +3,26 @@
3
3
  *
4
4
  * Shared between Anthropic and OpenAI drivers to avoid duplication.
5
5
  */
6
-
7
6
  export const MAX_RETRIES = 3;
8
7
  export const RETRY_BASE_MS = 1000;
9
-
10
8
  /**
11
9
  * Check if an HTTP status code is retryable.
12
10
  * 429 = rate limited, 502/503 = upstream error, 529 = overloaded.
13
11
  */
14
- export function isRetryable(status: number): boolean {
15
- return status === 429 || status === 502 || status === 503 || status === 529;
12
+ export function isRetryable(status) {
13
+ return status === 429 || status === 502 || status === 503 || status === 529;
16
14
  }
17
-
18
15
  /**
19
16
  * Calculate retry delay with exponential backoff + jitter.
20
17
  * attempt 0 → base 1s + 0-0.5s jitter
21
18
  * attempt 1 → base 2s + 0-1s jitter
22
19
  * attempt 2 → base 4s + 0-2s jitter
23
20
  */
24
- export function retryDelay(attempt: number): number {
25
- const base = RETRY_BASE_MS * Math.pow(2, attempt);
26
- const jitter = Math.random() * base * 0.5;
27
- return base + jitter;
21
+ export function retryDelay(attempt) {
22
+ const base = RETRY_BASE_MS * Math.pow(2, attempt);
23
+ const jitter = Math.random() * base * 0.5;
24
+ return base + jitter;
28
25
  }
29
-
30
- export function sleep(ms: number): Promise<void> {
31
- return new Promise((resolve) => setTimeout(resolve, ms));
26
+ export function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
32
28
  }
@@ -0,0 +1,25 @@
1
+ import { asError } from "../errors.js";
2
+ /** Wrap fetch with timeout and location context for debugging. */
3
+ export async function timedFetch(url, init = {}) {
4
+ const { timeoutMs, where, ...rest } = init;
5
+ let controller = null;
6
+ let timer = null;
7
+ try {
8
+ if (timeoutMs && timeoutMs > 0) {
9
+ controller = new AbortController();
10
+ rest.signal = controller.signal;
11
+ timer = setTimeout(() => controller.abort(), timeoutMs);
12
+ }
13
+ return await fetch(url, rest);
14
+ }
15
+ catch (e) {
16
+ const wrapped = asError(e);
17
+ const err = new Error(`[fetch timeout] ${where ?? ""} ${url} -> ${wrapped.name}: ${wrapped.message}`);
18
+ err.cause = e;
19
+ throw err;
20
+ }
21
+ finally {
22
+ if (timer)
23
+ clearTimeout(timer);
24
+ }
25
+ }
package/gro CHANGED
File without changes
package/package.json CHANGED
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "name": "@tjamescouch/gro",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Provider-agnostic LLM runtime with context management",
5
+ "bin": {
6
+ "gro": "./dist/main.js"
7
+ },
8
+ "files": [
9
+ "dist/",
10
+ "gro",
11
+ "providers/",
12
+ "_base.md",
13
+ "owl"
14
+ ],
5
15
  "type": "module",
6
16
  "scripts": {
7
17
  "start": "npx tsx src/main.ts",
8
- "build": "npx tsc && cp package.json dist/",
18
+ "build": "npx tsc && chmod +x dist/main.js",
19
+ "prepublishOnly": "npm run build",
9
20
  "build:bun": "bun build src/main.ts --outdir dist --target bun",
10
21
  "test": "npx tsx --test tests/*.test.ts",
11
22
  "test:bun": "bun test"
@@ -1,20 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [main]
6
- push:
7
- branches: [main]
8
-
9
- jobs:
10
- ci:
11
- name: Build & Test
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: oven-sh/setup-bun@v2
16
- with:
17
- bun-version: latest
18
- - run: bun install
19
- - run: bun run build
20
- - run: bun test
@@ -1,281 +0,0 @@
1
- /**
2
- * Anthropic Messages API driver.
3
- * Direct HTTP — no SDK dependency.
4
- */
5
- import { Logger } from "../logger.js";
6
- import { rateLimiter } from "../utils/rate-limiter.js";
7
- import { timedFetch } from "../utils/timed-fetch.js";
8
- import { MAX_RETRIES, isRetryable, retryDelay, sleep } from "../utils/retry.js";
9
- import { groError, asError, isGroError, errorLogFields } from "../errors.js";
10
- import type { ChatDriver, ChatMessage, ChatOutput, ChatToolCall, TokenUsage } from "./types.js";
11
-
12
- export interface AnthropicDriverConfig {
13
- apiKey: string;
14
- baseUrl?: string;
15
- model?: string;
16
- maxTokens?: number;
17
- timeoutMs?: number;
18
- }
19
-
20
- /**
21
- * Convert tool definitions from OpenAI format to Anthropic format.
22
- * OpenAI: { type: "function", function: { name, description, parameters } }
23
- * Anthropic: { name, description, input_schema }
24
- */
25
- function convertToolDefs(tools: any[]): any[] {
26
- return tools.map(t => {
27
- if (t.type === "function" && t.function) {
28
- return {
29
- type: "custom",
30
- name: t.function.name,
31
- description: t.function.description || "",
32
- input_schema: t.function.parameters || { type: "object", properties: {} },
33
- };
34
- }
35
- // Already in Anthropic format — ensure type is set
36
- if (!t.type) return { type: "custom", ...t };
37
- return t;
38
- });
39
- }
40
-
41
- /**
42
- * Convert internal messages (OpenAI-style) to Anthropic Messages API format.
43
- *
44
- * Key differences:
45
- * - Assistant tool calls become content blocks with type "tool_use"
46
- * - Tool result messages become user messages with type "tool_result" content blocks
47
- * - Anthropic requires strictly alternating user/assistant roles
48
- */
49
- function convertMessages(messages: ChatMessage[]): { system: string | undefined; apiMessages: any[] } {
50
- let systemPrompt: string | undefined;
51
- const apiMessages: any[] = [];
52
-
53
- for (const m of messages) {
54
- if (m.role === "system") {
55
- systemPrompt = systemPrompt ? systemPrompt + "\n" + m.content : m.content;
56
- continue;
57
- }
58
-
59
- if (m.role === "assistant") {
60
- const content: any[] = [];
61
- if (m.content) content.push({ type: "text", text: m.content });
62
-
63
- // Convert OpenAI-style tool_calls to Anthropic tool_use blocks
64
- const toolCalls = (m as any).tool_calls;
65
- if (Array.isArray(toolCalls)) {
66
- for (const tc of toolCalls) {
67
- let input: any;
68
- try { input = JSON.parse(tc.function.arguments || "{}"); } catch { input = {}; }
69
- content.push({
70
- type: "tool_use",
71
- id: tc.id,
72
- name: tc.function.name,
73
- input,
74
- });
75
- }
76
- }
77
-
78
- if (content.length > 0) {
79
- apiMessages.push({ role: "assistant", content });
80
- }
81
- continue;
82
- }
83
-
84
- if (m.role === "tool") {
85
- // Tool results must be in a user message with tool_result content blocks
86
- const block = {
87
- type: "tool_result",
88
- tool_use_id: m.tool_call_id,
89
- content: m.content,
90
- };
91
-
92
- // Group consecutive tool results into a single user message
93
- const last = apiMessages[apiMessages.length - 1];
94
- if (last && last.role === "user" && Array.isArray(last.content) &&
95
- last.content.length > 0 && last.content[0].type === "tool_result") {
96
- last.content.push(block);
97
- } else {
98
- apiMessages.push({ role: "user", content: [block] });
99
- }
100
- continue;
101
- }
102
-
103
- // Regular user messages
104
- apiMessages.push({ role: "user", content: m.content });
105
- }
106
-
107
- return { system: systemPrompt, apiMessages };
108
- }
109
-
110
- /** Pattern matching transient network errors that should be retried */
111
- const TRANSIENT_ERROR_RE = /fetch timeout|fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|socket hang up/i;
112
-
113
- /** Parse response content blocks into text + tool calls + token usage */
114
- function parseResponseContent(data: any, onToken?: (t: string) => void): ChatOutput {
115
- let text = "";
116
- const toolCalls: ChatToolCall[] = [];
117
-
118
- for (const block of data.content ?? []) {
119
- if (block.type === "text") {
120
- text += block.text;
121
- if (onToken) {
122
- try { onToken(block.text); } catch {}
123
- }
124
- } else if (block.type === "tool_use") {
125
- toolCalls.push({
126
- id: block.id,
127
- type: "custom",
128
- function: {
129
- name: block.name,
130
- arguments: JSON.stringify(block.input),
131
- },
132
- });
133
- }
134
- }
135
-
136
- const usage: TokenUsage | undefined = data.usage ? {
137
- inputTokens: data.usage.input_tokens ?? 0,
138
- outputTokens: data.usage.output_tokens ?? 0,
139
- } : undefined;
140
-
141
- return { text, toolCalls, usage };
142
- }
143
-
144
- export function makeAnthropicDriver(cfg: AnthropicDriverConfig): ChatDriver {
145
- const base = (cfg.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
146
- const endpoint = `${base}/v1/messages`;
147
- const model = cfg.model ?? "claude-sonnet-4-20250514";
148
- const maxTokens = cfg.maxTokens ?? 4096;
149
- const timeoutMs = cfg.timeoutMs ?? 2 * 60 * 60 * 1000;
150
-
151
- async function chat(messages: ChatMessage[], opts?: any): Promise<ChatOutput> {
152
- await rateLimiter.limit("llm-ask", 1);
153
-
154
- const onToken: ((t: string) => void) | undefined = opts?.onToken;
155
- const resolvedModel = opts?.model ?? model;
156
-
157
- const { system: systemPrompt, apiMessages } = convertMessages(messages);
158
-
159
- const body: any = {
160
- model: resolvedModel,
161
- thinking: {
162
- type: "adaptive"
163
- },
164
- max_tokens: maxTokens,
165
- messages: apiMessages,
166
- };
167
- if (systemPrompt) body.system = systemPrompt;
168
-
169
- // Tools support — convert from OpenAI format to Anthropic format
170
- if (Array.isArray(opts?.tools) && opts.tools.length) {
171
- body.tools = convertToolDefs(opts.tools);
172
- }
173
-
174
- const headers: Record<string, string> = {
175
- "Content-Type": "application/json",
176
- "x-api-key": cfg.apiKey,
177
- "anthropic-version": "2023-06-01",
178
- };
179
-
180
- const RETRYABLE_STATUS = new Set([429, 503, 529]);
181
- let requestId: string | undefined;
182
-
183
- try {
184
- let res!: Response;
185
- for (let attempt = 0; ; attempt++) {
186
- res = await timedFetch(endpoint, {
187
- method: "POST",
188
- headers,
189
- body: JSON.stringify(body),
190
- where: "driver:anthropic",
191
- timeoutMs,
192
- });
193
-
194
- if (res.ok) break;
195
-
196
- if (isRetryable(res.status) && attempt < MAX_RETRIES) {
197
- const delay = retryDelay(attempt);
198
- Logger.warn(`Anthropic ${res.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
199
- await sleep(delay);
200
- continue;
201
- }
202
-
203
- const text = await res.text().catch(() => "");
204
- const ge = groError("provider_error", `Anthropic API failed (${res.status}): ${text}`, {
205
- provider: "anthropic",
206
- model: resolvedModel,
207
- request_id: requestId,
208
- retryable: RETRYABLE_STATUS.has(res.status),
209
- cause: new Error(text),
210
- });
211
- Logger.error("Anthropic driver error:", errorLogFields(ge));
212
- throw ge;
213
- }
214
-
215
- const data = await res.json() as any;
216
- return parseResponseContent(data, onToken);
217
- } catch (e: unknown) {
218
- if (isGroError(e)) throw e; // already wrapped above
219
-
220
- // Classify the error: fetch timeouts and network errors are transient
221
- const errMsg = asError(e).message;
222
- const isTransient = TRANSIENT_ERROR_RE.test(errMsg);
223
-
224
- if (isTransient) {
225
- // Retry transient network errors (e.g. auth proxy down during container restart)
226
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
227
- const delay = retryDelay(attempt);
228
- Logger.warn(`Transient error: ${errMsg.substring(0, 120)}, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
229
- await sleep(delay);
230
-
231
- try {
232
- const retryRes = await timedFetch(endpoint, {
233
- method: "POST",
234
- headers,
235
- body: JSON.stringify(body),
236
- where: "driver:anthropic",
237
- timeoutMs,
238
- });
239
-
240
- if (!retryRes.ok) {
241
- const text = await retryRes.text().catch(() => "");
242
- if (isRetryable(retryRes.status) && attempt < MAX_RETRIES - 1) continue;
243
- throw groError("provider_error", `Anthropic API failed (${retryRes.status}): ${text}`, {
244
- provider: "anthropic", model: resolvedModel, retryable: false, cause: new Error(text),
245
- });
246
- }
247
-
248
- // Success on retry — parse and return
249
- const data = await retryRes.json() as any;
250
- Logger.info(`Recovered from transient error after ${attempt + 1} retries`);
251
- return parseResponseContent(data, onToken);
252
- } catch (retryErr: unknown) {
253
- if (isGroError(retryErr)) throw retryErr;
254
- if (attempt === MAX_RETRIES - 1) {
255
- // Exhausted retries — throw with context
256
- const ge = groError("provider_error", `Anthropic driver error (after ${MAX_RETRIES} retries): ${errMsg}`, {
257
- provider: "anthropic", model: resolvedModel, request_id: requestId,
258
- retryable: false, cause: e,
259
- });
260
- Logger.error("Anthropic driver error (retries exhausted):", errorLogFields(ge));
261
- throw ge;
262
- }
263
- }
264
- }
265
- }
266
-
267
- // Non-transient error — throw immediately
268
- const ge = groError("provider_error", `Anthropic driver error: ${errMsg}`, {
269
- provider: "anthropic",
270
- model: resolvedModel,
271
- request_id: requestId,
272
- retryable: false,
273
- cause: e,
274
- });
275
- Logger.error("Anthropic driver error:", errorLogFields(ge));
276
- throw ge;
277
- }
278
- }
279
-
280
- return { chat };
281
- }
@@ -1,5 +0,0 @@
1
- export type { ChatDriver, ChatMessage, ChatOutput, ChatToolCall, TokenUsage } from "./types.js";
2
- export { makeStreamingOpenAiDriver } from "./streaming-openai.js";
3
- export type { OpenAiDriverConfig } from "./streaming-openai.js";
4
- export { makeAnthropicDriver } from "./anthropic.js";
5
- export type { AnthropicDriverConfig } from "./anthropic.js";