oh-my-fable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,920 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AnthropicProvider: () => AnthropicProvider,
24
+ ContextManager: () => ContextManager,
25
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
26
+ Executor: () => Executor,
27
+ FileStore: () => FileStore,
28
+ MemoryStore: () => MemoryStore,
29
+ Planner: () => Planner,
30
+ Reflector: () => Reflector,
31
+ ScriptedProvider: () => ScriptedProvider,
32
+ ToolRegistry: () => ToolRegistry,
33
+ checkBudget: () => checkBudget,
34
+ createContext: () => createContext,
35
+ defineTool: () => defineTool,
36
+ estimateTokens: () => estimateTokens,
37
+ genId: () => genId,
38
+ nextPendingStep: () => nextPendingStep,
39
+ reply: () => reply,
40
+ resolveSerializable: () => resolveSerializable,
41
+ resume: () => resume,
42
+ run: () => run,
43
+ runLoop: () => runLoop,
44
+ runWith: () => runWith,
45
+ withRetry: () => withRetry
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/config/defaults.ts
50
+ var DEFAULT_CONFIG = {
51
+ maxSteps: 50,
52
+ maxTokens: 2e6,
53
+ maxWallClockMs: 30 * 60 * 1e3,
54
+ maxStepAttempts: 3,
55
+ maxReplans: 12,
56
+ contextTokenLimit: 1e5,
57
+ keepRecent: 8,
58
+ temperature: 0.2,
59
+ maxStepTokens: 4096
60
+ };
61
+ function resolveSerializable(config) {
62
+ return {
63
+ maxSteps: config.maxSteps ?? DEFAULT_CONFIG.maxSteps,
64
+ maxTokens: config.maxTokens ?? DEFAULT_CONFIG.maxTokens,
65
+ maxWallClockMs: config.maxWallClockMs ?? DEFAULT_CONFIG.maxWallClockMs,
66
+ maxStepAttempts: config.maxStepAttempts ?? DEFAULT_CONFIG.maxStepAttempts,
67
+ maxReplans: config.maxReplans ?? DEFAULT_CONFIG.maxReplans,
68
+ contextTokenLimit: config.contextTokenLimit ?? DEFAULT_CONFIG.contextTokenLimit,
69
+ keepRecent: config.keepRecent ?? DEFAULT_CONFIG.keepRecent,
70
+ temperature: config.temperature ?? DEFAULT_CONFIG.temperature,
71
+ maxStepTokens: config.maxStepTokens ?? DEFAULT_CONFIG.maxStepTokens
72
+ };
73
+ }
74
+
75
+ // src/run/context.ts
76
+ function genId(prefix = "run") {
77
+ const t = Date.now().toString(36);
78
+ const r = Math.floor(Math.random() * 36 ** 5).toString(36).padStart(5, "0");
79
+ return `${prefix}_${t}${r}`;
80
+ }
81
+ function nowIso() {
82
+ return (/* @__PURE__ */ new Date()).toISOString();
83
+ }
84
+ function createContext(goal, config) {
85
+ const ts = nowIso();
86
+ return {
87
+ runId: genId(),
88
+ goal,
89
+ plan: { goal: goal.description, steps: [], status: "active", revision: 0 },
90
+ history: [],
91
+ digests: [],
92
+ budget: { steps: 0, tokens: 0, startedAtMs: Date.now(), replans: 0 },
93
+ config,
94
+ createdAt: ts,
95
+ updatedAt: ts,
96
+ meta: {}
97
+ };
98
+ }
99
+ function touch(ctx) {
100
+ ctx.updatedAt = nowIso();
101
+ }
102
+ function nextPendingStep(ctx) {
103
+ const done = new Set(ctx.plan.steps.filter((s) => s.status === "done").map((s) => s.id));
104
+ for (const step of ctx.plan.steps) {
105
+ if (step.status !== "pending") continue;
106
+ const deps = step.dependsOn ?? [];
107
+ if (deps.every((d) => done.has(d))) return step;
108
+ }
109
+ return null;
110
+ }
111
+ function findStep(ctx, id) {
112
+ return ctx.plan.steps.find((s) => s.id === id);
113
+ }
114
+
115
+ // src/memory/store.ts
116
+ var import_promises = require("fs/promises");
117
+ var import_node_fs = require("fs");
118
+ var import_node_path = require("path");
119
+ var FileStore = class {
120
+ constructor(dir = "runs") {
121
+ this.dir = dir;
122
+ }
123
+ dir;
124
+ async ensureDir() {
125
+ if (!(0, import_node_fs.existsSync)(this.dir)) await (0, import_promises.mkdir)(this.dir, { recursive: true });
126
+ }
127
+ path(runId) {
128
+ return (0, import_node_path.join)(this.dir, `${runId.replace(/[^\w.-]/g, "_")}.json`);
129
+ }
130
+ async save(ctx) {
131
+ await this.ensureDir();
132
+ const file = this.path(ctx.runId);
133
+ const tmp = `${file}.tmp`;
134
+ await (0, import_promises.writeFile)(tmp, JSON.stringify(ctx, null, 2), "utf8");
135
+ await (0, import_promises.rename)(tmp, file);
136
+ }
137
+ async load(runId) {
138
+ const file = this.path(runId);
139
+ if (!(0, import_node_fs.existsSync)(file)) return null;
140
+ try {
141
+ return JSON.parse(await (0, import_promises.readFile)(file, "utf8"));
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+ async list() {
147
+ if (!(0, import_node_fs.existsSync)(this.dir)) return [];
148
+ const files = (await (0, import_promises.readdir)(this.dir)).filter((f) => f.endsWith(".json"));
149
+ const out = [];
150
+ for (const f of files) {
151
+ try {
152
+ const ctx = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(this.dir, f), "utf8"));
153
+ out.push({
154
+ runId: ctx.runId,
155
+ goal: ctx.goal.description,
156
+ planStatus: ctx.plan.status,
157
+ steps: ctx.plan.steps.length,
158
+ updatedAt: ctx.updatedAt
159
+ });
160
+ } catch {
161
+ }
162
+ }
163
+ return out.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
164
+ }
165
+ };
166
+ var MemoryStore = class {
167
+ map = /* @__PURE__ */ new Map();
168
+ async save(ctx) {
169
+ this.map.set(ctx.runId, JSON.stringify(ctx));
170
+ }
171
+ async load(runId) {
172
+ const raw = this.map.get(runId);
173
+ return raw ? JSON.parse(raw) : null;
174
+ }
175
+ async list() {
176
+ return [...this.map.values()].map((raw) => {
177
+ const ctx = JSON.parse(raw);
178
+ return { runId: ctx.runId, goal: ctx.goal.description, planStatus: ctx.plan.status, steps: ctx.plan.steps.length, updatedAt: ctx.updatedAt };
179
+ });
180
+ }
181
+ };
182
+
183
+ // src/executor/tools.ts
184
+ var ToolRegistry = class {
185
+ map = /* @__PURE__ */ new Map();
186
+ constructor(tools = []) {
187
+ for (const t of tools) this.register(t);
188
+ }
189
+ register(tool) {
190
+ this.map.set(tool.name, tool);
191
+ }
192
+ get(name) {
193
+ return this.map.get(name);
194
+ }
195
+ schemas() {
196
+ return [...this.map.values()].map((t) => t.schema);
197
+ }
198
+ get size() {
199
+ return this.map.size;
200
+ }
201
+ /** Run a tool, turning any thrown error into a ToolResult — the loop never dies on a tool. */
202
+ async run(name, input) {
203
+ const tool = this.map.get(name);
204
+ if (!tool) return { ok: false, output: "", error: `unknown tool: ${name}` };
205
+ try {
206
+ return await tool.handler(input);
207
+ } catch (err) {
208
+ return { ok: false, output: "", error: err.message ?? "tool threw" };
209
+ }
210
+ }
211
+ };
212
+ function defineTool(name, description, parameters, handler) {
213
+ return { name, description, schema: { name, description, parameters }, handler };
214
+ }
215
+
216
+ // src/core/json.ts
217
+ function extractJson(raw) {
218
+ let s = raw.trim();
219
+ const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
220
+ if (fence) s = fence[1].trim();
221
+ const start = s.search(/[[{]/);
222
+ if (start > 0) s = s.slice(start);
223
+ const open = s[0];
224
+ if (open === "{" || open === "[") {
225
+ const close = open === "{" ? "}" : "]";
226
+ let depth = 0;
227
+ let inStr = false;
228
+ let esc = false;
229
+ for (let i = 0; i < s.length; i++) {
230
+ const c = s[i];
231
+ if (inStr) {
232
+ if (esc) esc = false;
233
+ else if (c === "\\") esc = true;
234
+ else if (c === '"') inStr = false;
235
+ } else if (c === '"') inStr = true;
236
+ else if (c === open) depth++;
237
+ else if (c === close) {
238
+ depth--;
239
+ if (depth === 0) return s.slice(0, i + 1);
240
+ }
241
+ }
242
+ }
243
+ return s;
244
+ }
245
+ function tryParse(raw) {
246
+ try {
247
+ return JSON.parse(extractJson(raw));
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ async function parseWithRepair(raw, provider, validate) {
253
+ const first = tryParse(raw);
254
+ if (first !== null && (!validate || validate(first))) return first;
255
+ const repaired = await provider.complete({
256
+ responseFormat: "json",
257
+ temperature: 0,
258
+ messages: [
259
+ { role: "system", content: "You convert a malformed response into valid JSON. Output ONLY the JSON \u2014 no prose, no code fences." },
260
+ { role: "user", content: raw }
261
+ ]
262
+ });
263
+ const second = tryParse(repaired.content);
264
+ if (second !== null && (!validate || validate(second))) return second;
265
+ return null;
266
+ }
267
+
268
+ // src/planner/prompts.ts
269
+ var PLAN_SYSTEM = `You are a planning module. Decompose a goal into an ordered list of executable steps.
270
+
271
+ Rules:
272
+ - Each step has a single, verifiable intent.
273
+ - If a step is too big, split it further.
274
+ - Never create a step that violates a constraint.
275
+ - Order matters; use dependsOn (step ids) when a step needs an earlier one.
276
+ - Respond with ONLY this JSON. No prose, no code fences:
277
+ { "steps": [ { "id": "s1", "intent": "...", "dependsOn": [] } ] }`;
278
+ function planPrompt(goal) {
279
+ return [
280
+ { role: "system", content: PLAN_SYSTEM },
281
+ {
282
+ role: "user",
283
+ content: [
284
+ `Goal: ${goal.description}`,
285
+ goal.constraints?.length ? `Constraints: ${goal.constraints.join("; ")}` : "",
286
+ goal.successCriteria?.length ? `Done when: ${goal.successCriteria.join("; ")}` : ""
287
+ ].filter(Boolean).join("\n")
288
+ }
289
+ ];
290
+ }
291
+ var REPLAN_SYSTEM = `You are a planning module revising a plan that hit an obstacle.
292
+
293
+ CRITICAL: steps already completed are DONE \u2014 do not redo them. Their results are given.
294
+ Produce ONLY the steps still needed to reach the goal from the current state.
295
+ Account for the obstacle so the new steps actually get unblocked.
296
+ Respond with ONLY this JSON. No prose, no code fences:
297
+ { "steps": [ { "id": "n1", "intent": "...", "dependsOn": [] } ] }`;
298
+ function replanPrompt(plan, obs, notes, goal) {
299
+ const done = plan.steps.filter((s) => s.status === "done");
300
+ const blocked = plan.steps.find((s) => s.id === obs.stepId);
301
+ return [
302
+ { role: "system", content: REPLAN_SYSTEM },
303
+ {
304
+ role: "user",
305
+ content: [
306
+ `Goal: ${goal.description}`,
307
+ goal.successCriteria?.length ? `Done when: ${goal.successCriteria.join("; ")}` : "",
308
+ "",
309
+ "Already completed (do NOT redo):",
310
+ done.length ? done.map((s) => ` \u2713 [${s.id}] ${s.intent}${s.result ? ` \u2192 ${s.result}` : ""}`).join("\n") : " (nothing yet)",
311
+ "",
312
+ `The step that got stuck: ${blocked ? blocked.intent : obs.stepId}`,
313
+ `What happened: ${obs.error ?? obs.output}`,
314
+ `Reflection: ${notes}`,
315
+ "",
316
+ "Give the remaining steps from here."
317
+ ].join("\n")
318
+ }
319
+ ];
320
+ }
321
+
322
+ // src/planner/planner.ts
323
+ function coerceSteps(raw, idPrefix) {
324
+ const steps = [];
325
+ const seen = /* @__PURE__ */ new Set();
326
+ let n = 0;
327
+ for (const s of raw?.steps ?? []) {
328
+ if (typeof s?.intent !== "string" || s.intent.trim() === "") continue;
329
+ let id = typeof s.id === "string" && s.id.trim() ? s.id.trim() : `${idPrefix}${++n}`;
330
+ while (seen.has(id)) id = `${idPrefix}${++n}`;
331
+ seen.add(id);
332
+ const dependsOn = Array.isArray(s.dependsOn) ? s.dependsOn.filter((d) => typeof d === "string") : void 0;
333
+ steps.push({ id, intent: s.intent.trim(), dependsOn, status: "pending", attempts: 0 });
334
+ }
335
+ return steps;
336
+ }
337
+ var Planner = class {
338
+ constructor(provider, temperature) {
339
+ this.provider = provider;
340
+ this.temperature = temperature;
341
+ }
342
+ provider;
343
+ temperature;
344
+ async plan(goal) {
345
+ const res = await this.provider.complete({ messages: planPrompt(goal), responseFormat: "json", temperature: this.temperature });
346
+ const raw = await parseWithRepair(res.content, this.provider, (v) => Array.isArray(v.steps));
347
+ let steps = coerceSteps(raw, "s");
348
+ if (steps.length === 0) {
349
+ steps = [{ id: "s1", intent: goal.description, status: "pending", attempts: 0 }];
350
+ }
351
+ return { goal: goal.description, steps, status: "active", revision: 0 };
352
+ }
353
+ /**
354
+ * Accumulate, never reset: completed steps are preserved verbatim; only the
355
+ * remaining work is regenerated from the point of failure. This is what lets
356
+ * a long task make forward progress instead of restarting forever.
357
+ */
358
+ async replan(plan, obs, ctx) {
359
+ const notes = String(ctx.meta["lastReflectionNotes"] ?? "");
360
+ const res = await this.provider.complete({
361
+ messages: replanPrompt(plan, obs, notes, ctx.goal),
362
+ responseFormat: "json",
363
+ temperature: this.temperature
364
+ });
365
+ const raw = await parseWithRepair(res.content, this.provider, (v) => Array.isArray(v.steps));
366
+ const revision = plan.revision + 1;
367
+ const done = plan.steps.filter((s) => s.status === "done");
368
+ const doneIds = new Set(done.map((s) => s.id));
369
+ let fresh = coerceSteps(raw, `r${revision}s`).filter((s) => !doneIds.has(s.id));
370
+ if (fresh.length === 0) {
371
+ fresh = [{ id: `r${revision}s1`, intent: `Make progress toward: ${ctx.goal.description}`, status: "pending", attempts: 0 }];
372
+ }
373
+ return { goal: plan.goal, steps: [...done, ...fresh], status: "active", revision };
374
+ }
375
+ };
376
+
377
+ // src/executor/executor.ts
378
+ var EXEC_SYSTEM = `You are the execution module of an autonomous agent. You are given the overall goal for context and exactly ONE step to perform now.
379
+
380
+ - Do only this step. Don't get ahead of the plan.
381
+ - If a tool would help and tools are available, call it.
382
+ - Respond with the concrete result of the step \u2014 what you produced, found, or decided. Be specific and concise; this becomes the step's recorded result.`;
383
+ var MAX_TOOL_HOPS = 8;
384
+ function background(ctx) {
385
+ const g = ctx.goal;
386
+ const done = ctx.plan.steps.filter((s) => s.status === "done");
387
+ const pending = ctx.plan.steps.filter((s) => s.status === "pending");
388
+ const lines = [`Goal: ${g.description}`];
389
+ if (g.constraints?.length) lines.push(`Constraints: ${g.constraints.join("; ")}`);
390
+ if (g.successCriteria?.length) lines.push(`Done when: ${g.successCriteria.join("; ")}`);
391
+ if (ctx.digests.length) {
392
+ lines.push("", "Summary of earlier work:");
393
+ for (const d of ctx.digests) lines.push(` ${d.summary}`);
394
+ }
395
+ if (done.length) {
396
+ lines.push("", "Completed steps:");
397
+ for (const s of done) lines.push(` \u2713 ${s.intent}${s.result ? ` \u2192 ${s.result}` : ""}`);
398
+ }
399
+ if (pending.length) {
400
+ lines.push("", "Still to do:");
401
+ for (const s of pending) lines.push(` \u2022 ${s.intent}`);
402
+ }
403
+ return lines.join("\n");
404
+ }
405
+ var Executor = class {
406
+ constructor(provider, registry, opts) {
407
+ this.provider = provider;
408
+ this.opts = opts;
409
+ this.registry = registry;
410
+ }
411
+ provider;
412
+ opts;
413
+ registry;
414
+ async execute(step, ctx) {
415
+ const tools = this.registry.size > 0 ? this.registry.schemas() : void 0;
416
+ const local = [
417
+ { role: "system", content: EXEC_SYSTEM },
418
+ { role: "user", content: background(ctx) },
419
+ ...ctx.history,
420
+ { role: "user", content: `Now do this step:
421
+ [${step.id}] ${step.intent}` }
422
+ ];
423
+ let tokensUsed = 0;
424
+ const allToolCalls = [];
425
+ let toolError;
426
+ try {
427
+ let result = await this.provider.complete({
428
+ messages: local,
429
+ tools,
430
+ temperature: this.opts.temperature,
431
+ maxTokens: this.opts.maxStepTokens
432
+ });
433
+ tokensUsed += result.tokensIn + result.tokensOut;
434
+ let hops = 0;
435
+ while (result.stopReason === "tool_use" && result.toolCalls?.length && hops < MAX_TOOL_HOPS) {
436
+ hops++;
437
+ const resultsText = [];
438
+ for (const call of result.toolCalls) {
439
+ allToolCalls.push(call);
440
+ const out = await this.registry.run(call.name, call.input);
441
+ if (!out.ok) toolError = out.error;
442
+ resultsText.push(`- ${call.name}: ${out.ok ? out.output : `ERROR: ${out.error}`}`);
443
+ }
444
+ local.push({ role: "assistant", content: result.content || `(requested: ${result.toolCalls.map((c) => c.name).join(", ")})` });
445
+ local.push({ role: "user", content: `Tool results:
446
+ ${resultsText.join("\n")}
447
+
448
+ Continue the step with these.` });
449
+ result = await this.provider.complete({ messages: local, tools, temperature: this.opts.temperature, maxTokens: this.opts.maxStepTokens });
450
+ tokensUsed += result.tokensIn + result.tokensOut;
451
+ }
452
+ const ok = result.stopReason !== "error";
453
+ const output = result.content.trim() || (ok ? "(step produced no text output)" : "");
454
+ ctx.history.push({ role: "user", content: `Step [${step.id}]: ${step.intent}` });
455
+ ctx.history.push({ role: "assistant", content: output });
456
+ return {
457
+ stepId: step.id,
458
+ ok,
459
+ output,
460
+ toolCalls: allToolCalls.length ? allToolCalls : void 0,
461
+ error: ok ? toolError : `model error (stopReason=${result.stopReason})`,
462
+ tokensUsed
463
+ };
464
+ } catch (err) {
465
+ return {
466
+ stepId: step.id,
467
+ ok: false,
468
+ output: "",
469
+ error: err.message ?? "execution failed",
470
+ tokensUsed
471
+ };
472
+ }
473
+ }
474
+ };
475
+
476
+ // src/reflector/prompts.ts
477
+ var REFLECT_SYSTEM = `You are the progress supervisor of an autonomous agent. You just saw the result of one step. Judge what should happen next \u2014 pick exactly ONE:
478
+
479
+ - goal_met \u2014 the goal's success criteria are ALL satisfied. (Allowed even if planned steps remain \u2014 don't pad work that's already done.)
480
+ - needs_replan \u2014 the step worked, but its result changed the assumptions the remaining plan was built on. The plan needs revising.
481
+ - blocked \u2014 the same obstacle keeps recurring, or there is no path forward with the current plan.
482
+ - on_track \u2014 normal forward progress; continue to the next step.
483
+
484
+ Respond with ONLY this JSON. No prose, no code fences:
485
+ { "progress": "on_track" | "needs_replan" | "blocked" | "goal_met", "notes": "1-2 sentence reason", "confidence": 0.0 }`;
486
+ function planSummary(plan) {
487
+ const done = plan.steps.filter((s) => s.status === "done");
488
+ const pending = plan.steps.filter((s) => s.status === "pending");
489
+ const lines = [];
490
+ if (done.length) lines.push("Done:", ...done.map((s) => ` \u2713 ${s.intent}`));
491
+ if (pending.length) lines.push("Remaining:", ...pending.map((s) => ` \u2022 ${s.intent}`));
492
+ return lines.join("\n") || "(no steps)";
493
+ }
494
+ function reflectPrompt(plan, obs, goal, step) {
495
+ return [
496
+ { role: "system", content: REFLECT_SYSTEM },
497
+ {
498
+ role: "user",
499
+ content: [
500
+ `Goal: ${goal.description}`,
501
+ goal.successCriteria?.length ? `Done when: ${goal.successCriteria.join("; ")}` : "",
502
+ "",
503
+ "Current plan:",
504
+ planSummary(plan),
505
+ "",
506
+ `Step just run: ${step ? step.intent : obs.stepId}`,
507
+ `Succeeded: ${obs.ok}`,
508
+ `Result: ${obs.error ? `ERROR \u2014 ${obs.error}` : obs.output}`
509
+ ].filter(Boolean).join("\n")
510
+ }
511
+ ];
512
+ }
513
+
514
+ // src/reflector/reflector.ts
515
+ var PROGRESS_VALUES = ["on_track", "needs_replan", "blocked", "goal_met"];
516
+ var Reflector = class {
517
+ constructor(provider) {
518
+ this.provider = provider;
519
+ }
520
+ provider;
521
+ async reflect(plan, obs, ctx) {
522
+ const step = findStep(ctx, obs.stepId);
523
+ if (!obs.ok && step) {
524
+ step.attempts += 1;
525
+ if (step.attempts >= ctx.config.maxStepAttempts) {
526
+ return {
527
+ progress: "blocked",
528
+ notes: `Step failed ${step.attempts} times in a row (last error: ${obs.error ?? "unknown"}). No path with the current plan.`,
529
+ confidence: 0.95
530
+ };
531
+ }
532
+ }
533
+ const res = await this.provider.complete({
534
+ messages: reflectPrompt(plan, obs, ctx.goal, step),
535
+ responseFormat: "json",
536
+ temperature: 0
537
+ });
538
+ const raw = await parseWithRepair(res.content, this.provider, (v) => typeof v.progress === "string");
539
+ if (!raw || typeof raw.progress !== "string" || !PROGRESS_VALUES.includes(raw.progress)) {
540
+ return { progress: "needs_replan", notes: "Could not parse a verdict; replanning conservatively." };
541
+ }
542
+ return {
543
+ progress: raw.progress,
544
+ notes: typeof raw.notes === "string" ? raw.notes : "",
545
+ confidence: typeof raw.confidence === "number" ? raw.confidence : void 0
546
+ };
547
+ }
548
+ };
549
+
550
+ // src/memory/context.ts
551
+ var FOLD_SYSTEM = `You compress an agent's conversation log. Keep ONLY: key decisions made, concrete results/outputs produced, and any open or unresolved items. Drop chit-chat and restating. Be terse \u2014 this summary replaces the raw log.`;
552
+ var DIGEST_CHAR_LIMIT = 6e3;
553
+ var ContextManager = class {
554
+ constructor(provider, config) {
555
+ this.provider = provider;
556
+ this.config = config;
557
+ }
558
+ provider;
559
+ config;
560
+ overBudget(ctx) {
561
+ return this.provider.estimateTokens(ctx.history) >= this.config.contextTokenLimit * 0.8;
562
+ }
563
+ /** Fold all but the most recent K messages into a digest; return the new (short) history. */
564
+ async compact(ctx) {
565
+ const keep = this.config.keepRecent;
566
+ if (ctx.history.length <= keep) return ctx.history;
567
+ const recent = ctx.history.slice(-keep);
568
+ const toFold = ctx.history.slice(0, -keep);
569
+ const folded = await this.provider.complete({
570
+ temperature: 0,
571
+ messages: [
572
+ { role: "system", content: FOLD_SYSTEM },
573
+ { role: "user", content: toFold.map((m) => `[${m.role}] ${m.content}`).join("\n\n") }
574
+ ]
575
+ });
576
+ ctx.digests.push({ summary: folded.content.trim(), coversUntil: nowIso() });
577
+ const totalDigestChars = ctx.digests.reduce((n, d) => n + d.summary.length, 0);
578
+ if (totalDigestChars > DIGEST_CHAR_LIMIT && ctx.digests.length > 1) {
579
+ const merged = await this.provider.complete({
580
+ temperature: 0,
581
+ messages: [
582
+ { role: "system", content: FOLD_SYSTEM },
583
+ { role: "user", content: ctx.digests.map((d) => d.summary).join("\n\n") }
584
+ ]
585
+ });
586
+ ctx.digests = [{ summary: merged.content.trim(), coversUntil: nowIso() }];
587
+ }
588
+ return recent;
589
+ }
590
+ };
591
+
592
+ // src/run/budget.ts
593
+ function checkBudget(ctx) {
594
+ const c = ctx.config;
595
+ const b = ctx.budget;
596
+ if (b.steps >= c.maxSteps) {
597
+ return { exceeded: true, reason: `step budget exhausted (${b.steps}/${c.maxSteps} steps)` };
598
+ }
599
+ if (b.tokens >= c.maxTokens) {
600
+ return { exceeded: true, reason: `token budget exhausted (${b.tokens}/${c.maxTokens} tokens)` };
601
+ }
602
+ const elapsed = Date.now() - b.startedAtMs;
603
+ if (elapsed >= c.maxWallClockMs) {
604
+ return { exceeded: true, reason: `wall-clock budget exhausted (${Math.round(elapsed / 1e3)}s/${Math.round(c.maxWallClockMs / 1e3)}s)` };
605
+ }
606
+ if (b.replans >= c.maxReplans) {
607
+ return { exceeded: true, reason: `replan budget exhausted (${b.replans}/${c.maxReplans} replans)` };
608
+ }
609
+ return { exceeded: false };
610
+ }
611
+
612
+ // src/core/loop.ts
613
+ function applyReflection(ctx, step, obs, reflection) {
614
+ ctx.meta["lastReflectionNotes"] = reflection.notes;
615
+ ctx.budget.tokens += obs.tokensUsed;
616
+ ctx.budget.steps += 1;
617
+ if (obs.ok) {
618
+ step.status = "done";
619
+ step.result = obs.output.slice(0, 600);
620
+ } else if (reflection.progress === "on_track") {
621
+ step.status = "pending";
622
+ } else {
623
+ step.status = "failed";
624
+ }
625
+ }
626
+ async function runLoop(ctx, deps) {
627
+ const { planner, executor, reflector, contextManager, store, onEvent } = deps;
628
+ if (ctx.plan.steps.length === 0) {
629
+ ctx.plan = await planner.plan(ctx.goal);
630
+ touch(ctx);
631
+ await store.save(ctx);
632
+ onEvent({ type: "plan_created", plan: ctx.plan });
633
+ }
634
+ while (true) {
635
+ const guard = checkBudget(ctx);
636
+ if (guard.exceeded) {
637
+ ctx.plan.status = "failed";
638
+ touch(ctx);
639
+ await store.save(ctx);
640
+ onEvent({ type: "halted", reason: guard.reason });
641
+ return { status: "halted", reason: guard.reason, ctx };
642
+ }
643
+ const step = nextPendingStep(ctx);
644
+ if (step === null) {
645
+ ctx.plan.status = "done";
646
+ touch(ctx);
647
+ await store.save(ctx);
648
+ onEvent({ type: "done", reason: "all steps complete" });
649
+ return { status: "done", reason: "all steps complete", ctx };
650
+ }
651
+ if (contextManager.overBudget(ctx)) {
652
+ const before = ctx.history.length;
653
+ ctx.history = await contextManager.compact(ctx);
654
+ onEvent({
655
+ type: "compaction",
656
+ foldedMessages: before - ctx.history.length,
657
+ digestChars: ctx.digests.reduce((n, d) => n + d.summary.length, 0)
658
+ });
659
+ }
660
+ step.status = "running";
661
+ onEvent({ type: "step_start", step });
662
+ const obs = await executor.execute(step, ctx);
663
+ const reflection = await reflector.reflect(ctx.plan, obs, ctx);
664
+ applyReflection(ctx, step, obs, reflection);
665
+ onEvent({ type: "step_done", step, observation: obs });
666
+ onEvent({ type: "reflection", reflection, step });
667
+ touch(ctx);
668
+ await store.save(ctx);
669
+ onEvent({ type: "checkpoint", runId: ctx.runId });
670
+ switch (reflection.progress) {
671
+ case "goal_met": {
672
+ ctx.plan.status = "done";
673
+ touch(ctx);
674
+ await store.save(ctx);
675
+ onEvent({ type: "done", reason: reflection.notes || "goal met" });
676
+ return { status: "done", reason: reflection.notes || "goal met", ctx };
677
+ }
678
+ case "needs_replan": {
679
+ ctx.budget.replans += 1;
680
+ ctx.plan = await planner.replan(ctx.plan, obs, ctx);
681
+ touch(ctx);
682
+ await store.save(ctx);
683
+ onEvent({ type: "replan", revision: ctx.plan.revision, reason: reflection.notes });
684
+ break;
685
+ }
686
+ case "blocked": {
687
+ onEvent({ type: "escalation", step, notes: reflection.notes });
688
+ ctx.budget.replans += 1;
689
+ ctx.plan = await planner.replan(ctx.plan, obs, ctx);
690
+ touch(ctx);
691
+ await store.save(ctx);
692
+ onEvent({ type: "replan", revision: ctx.plan.revision, reason: `recovering from blocked: ${reflection.notes}` });
693
+ break;
694
+ }
695
+ case "on_track":
696
+ default:
697
+ break;
698
+ }
699
+ }
700
+ }
701
+
702
+ // src/providers/provider.ts
703
+ function estimateTokens(messages) {
704
+ let chars = 0;
705
+ for (const m of messages) chars += m.content.length;
706
+ return Math.ceil(chars / 4);
707
+ }
708
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
709
+ async function withRetry(fn, opts = {}) {
710
+ const retries = opts.retries ?? 4;
711
+ const base = opts.baseDelayMs ?? 500;
712
+ const sleep = opts.sleep ?? defaultSleep;
713
+ const retryable = opts.isRetryable ?? (() => true);
714
+ let lastErr;
715
+ for (let attempt = 0; attempt <= retries; attempt++) {
716
+ try {
717
+ return await fn();
718
+ } catch (err) {
719
+ lastErr = err;
720
+ if (attempt === retries || !retryable(err)) break;
721
+ await sleep(base * 2 ** attempt);
722
+ }
723
+ }
724
+ throw lastErr;
725
+ }
726
+ function fill(partial, req) {
727
+ const content = partial.content ?? "";
728
+ return {
729
+ content,
730
+ toolCalls: partial.toolCalls,
731
+ tokensIn: partial.tokensIn ?? estimateTokens(req.messages),
732
+ tokensOut: partial.tokensOut ?? Math.ceil(content.length / 4),
733
+ stopReason: partial.stopReason ?? (partial.toolCalls && partial.toolCalls.length > 0 ? "tool_use" : "end")
734
+ };
735
+ }
736
+ var ScriptedProvider = class {
737
+ constructor(responses) {
738
+ this.responses = responses;
739
+ }
740
+ responses;
741
+ name = "scripted";
742
+ index = 0;
743
+ /** Every request the loop made — handy for assertions. */
744
+ requests = [];
745
+ async complete(req) {
746
+ this.requests.push(req);
747
+ const r = this.responses[this.index];
748
+ if (this.index < this.responses.length) this.index++;
749
+ const resolved = typeof r === "function" ? r(req) : r ?? { content: "", stopReason: "end" };
750
+ return fill(resolved, req);
751
+ }
752
+ estimateTokens(messages) {
753
+ return estimateTokens(messages);
754
+ }
755
+ };
756
+ var reply = {
757
+ plan(steps) {
758
+ return { content: JSON.stringify({ steps }), stopReason: "end" };
759
+ },
760
+ reflection(progress, notes = "", confidence = 0.9) {
761
+ return { content: JSON.stringify({ progress, notes, confidence }), stopReason: "end" };
762
+ },
763
+ text(content) {
764
+ return { content, stopReason: "end" };
765
+ },
766
+ toolUse(calls, content = "") {
767
+ return { content, toolCalls: calls, stopReason: "tool_use" };
768
+ }
769
+ };
770
+
771
+ // src/providers/anthropic.ts
772
+ function coalesce(messages) {
773
+ const out = [];
774
+ for (const m of messages) {
775
+ const last = out[out.length - 1];
776
+ if (last && last.role === m.role) last.content += "\n\n" + m.content;
777
+ else out.push({ ...m });
778
+ }
779
+ if (out.length === 0 || out[0].role !== "user") out.unshift({ role: "user", content: "(begin)" });
780
+ return out;
781
+ }
782
+ var AnthropicProvider = class {
783
+ name = "anthropic";
784
+ apiKey;
785
+ model;
786
+ baseUrl;
787
+ version;
788
+ maxRetries;
789
+ defaultMaxTokens;
790
+ constructor(opts = {}) {
791
+ this.apiKey = opts.apiKey ?? process.env["ANTHROPIC_API_KEY"] ?? "";
792
+ this.model = opts.model ?? "claude-sonnet-4-6";
793
+ this.baseUrl = opts.baseUrl ?? "https://api.anthropic.com";
794
+ this.version = opts.version ?? "2023-06-01";
795
+ this.maxRetries = opts.maxRetries ?? 4;
796
+ this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
797
+ if (!this.apiKey) {
798
+ throw new Error("AnthropicProvider needs an API key (pass { apiKey } or set ANTHROPIC_API_KEY).");
799
+ }
800
+ }
801
+ estimateTokens(messages) {
802
+ return estimateTokens(messages);
803
+ }
804
+ async complete(req) {
805
+ const system = req.messages.filter((m) => m.role === "system").map((m) => m.content).join("\n\n");
806
+ const convo = coalesce(
807
+ req.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }))
808
+ );
809
+ const body = {
810
+ model: this.model,
811
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
812
+ temperature: req.temperature ?? 1,
813
+ messages: convo
814
+ };
815
+ let sys = system;
816
+ if (req.responseFormat === "json") sys = (sys ? sys + "\n\n" : "") + "Output ONLY valid JSON. No prose, no code fences.";
817
+ if (sys) body["system"] = sys;
818
+ if (req.tools?.length) {
819
+ body["tools"] = req.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
820
+ }
821
+ const data = await withRetry(
822
+ async () => {
823
+ const res = await fetch(`${this.baseUrl}/v1/messages`, {
824
+ method: "POST",
825
+ headers: {
826
+ "content-type": "application/json",
827
+ "x-api-key": this.apiKey,
828
+ "anthropic-version": this.version
829
+ },
830
+ body: JSON.stringify(body)
831
+ });
832
+ if (!res.ok) {
833
+ const text = await res.text().catch(() => "");
834
+ const err = new Error(`Anthropic ${res.status}: ${text.slice(0, 300)}`);
835
+ err.status = res.status;
836
+ throw err;
837
+ }
838
+ return await res.json();
839
+ },
840
+ {
841
+ retries: this.maxRetries,
842
+ isRetryable: (e) => {
843
+ const status = e.status;
844
+ return status === void 0 || status === 429 || status >= 500 && status < 600;
845
+ }
846
+ }
847
+ );
848
+ let content = "";
849
+ const toolCalls = [];
850
+ for (const block of data.content ?? []) {
851
+ if (block.type === "text" && block.text) content += block.text;
852
+ else if (block.type === "tool_use" && block.name) toolCalls.push({ id: block.id ?? block.name, name: block.name, input: block.input });
853
+ }
854
+ const map = { end_turn: "end", tool_use: "tool_use", max_tokens: "max_tokens", stop_sequence: "end" };
855
+ return {
856
+ content,
857
+ toolCalls: toolCalls.length ? toolCalls : void 0,
858
+ tokensIn: data.usage?.input_tokens ?? 0,
859
+ tokensOut: data.usage?.output_tokens ?? 0,
860
+ stopReason: map[data.stop_reason ?? ""] ?? "end"
861
+ };
862
+ }
863
+ };
864
+
865
+ // src/index.ts
866
+ function buildDeps(config, serializable) {
867
+ const provider = config.provider;
868
+ const store = config.store ?? new FileStore(config.runsDir);
869
+ const registry = new ToolRegistry(config.tools ?? []);
870
+ return {
871
+ planner: new Planner(provider, serializable.temperature),
872
+ executor: new Executor(provider, registry, { temperature: serializable.temperature, maxStepTokens: serializable.maxStepTokens }),
873
+ reflector: new Reflector(provider),
874
+ contextManager: new ContextManager(provider, serializable),
875
+ store,
876
+ onEvent: config.onEvent ?? (() => {
877
+ })
878
+ };
879
+ }
880
+ async function run(goal, config) {
881
+ const g = typeof goal === "string" ? { description: goal } : goal;
882
+ const serializable = resolveSerializable(config);
883
+ const ctx = createContext(g, serializable);
884
+ return runLoop(ctx, buildDeps(config, serializable));
885
+ }
886
+ async function resume(runId, config) {
887
+ const store = config.store ?? new FileStore(config.runsDir);
888
+ const ctx = await store.load(runId);
889
+ if (!ctx) throw new Error(`No saved run found for "${runId}".`);
890
+ return runLoop(ctx, buildDeps({ ...config, store }, ctx.config));
891
+ }
892
+ async function runWith(ctx, config) {
893
+ return runLoop(ctx, buildDeps(config, ctx.config));
894
+ }
895
+ // Annotate the CommonJS export names for ESM import in node:
896
+ 0 && (module.exports = {
897
+ AnthropicProvider,
898
+ ContextManager,
899
+ DEFAULT_CONFIG,
900
+ Executor,
901
+ FileStore,
902
+ MemoryStore,
903
+ Planner,
904
+ Reflector,
905
+ ScriptedProvider,
906
+ ToolRegistry,
907
+ checkBudget,
908
+ createContext,
909
+ defineTool,
910
+ estimateTokens,
911
+ genId,
912
+ nextPendingStep,
913
+ reply,
914
+ resolveSerializable,
915
+ resume,
916
+ run,
917
+ runLoop,
918
+ runWith,
919
+ withRetry
920
+ });