torus-ai 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.js ADDED
@@ -0,0 +1,733 @@
1
+ // src/tools.ts
2
+ function tool(name, description, inputSchema, handler) {
3
+ return { name, description, inputSchema, handler };
4
+ }
5
+ function createSdkMcpServer(opts) {
6
+ return { kind: "sdk-mcp", name: opts.name, version: opts.version ?? "1.0.0", tools: opts.tools };
7
+ }
8
+ var ToolRegistry = class {
9
+ map = /* @__PURE__ */ new Map();
10
+ /** Built-ins register under their bare name (no namespace). */
11
+ addBuiltins(defs) {
12
+ for (const d of defs) this.map.set(d.name, d);
13
+ return this;
14
+ }
15
+ /** SDK MCP server tools register as mcp__<server>__<tool>. */
16
+ addServer(server) {
17
+ for (const t of server.tools) this.map.set(`mcp__${server.name}__${t.name}`, t);
18
+ return this;
19
+ }
20
+ has(fullName) {
21
+ return this.map.has(fullName);
22
+ }
23
+ list() {
24
+ return [...this.map.entries()].map(([fullName, def]) => ({ fullName, def }));
25
+ }
26
+ /** Tool schemas to hand the model, optionally filtered to a stage's allowlist. */
27
+ schemas(filter) {
28
+ return this.list().filter((t) => !filter || filter(t.fullName)).map((t) => ({ name: t.fullName, description: t.def.description, inputSchema: t.def.inputSchema }));
29
+ }
30
+ async execute(fullName, input, ctx) {
31
+ const def = this.map.get(fullName);
32
+ if (!def) return { content: `Unknown tool: ${fullName}`, isError: true };
33
+ try {
34
+ return await def.handler(input, ctx);
35
+ } catch (err) {
36
+ return { content: `Tool ${fullName} threw: ${err.message}`, isError: true };
37
+ }
38
+ }
39
+ };
40
+
41
+ // src/permissions.ts
42
+ function matchesAllow(name, patterns) {
43
+ return patterns.some((p) => {
44
+ if (p === "*") return true;
45
+ if (p.endsWith("*")) return name.startsWith(p.slice(0, -1));
46
+ return p === name;
47
+ });
48
+ }
49
+ var PermissionEngine = class {
50
+ cfg;
51
+ constructor(cfg = {}) {
52
+ this.cfg = cfg;
53
+ }
54
+ async check(name, input) {
55
+ const { allowedTools, disallowedTools, canUseTool } = this.cfg;
56
+ if (disallowedTools && matchesAllow(name, disallowedTools)) {
57
+ return { behavior: "deny", message: `${name} is in disallowedTools.` };
58
+ }
59
+ const onAllowlist = allowedTools ? matchesAllow(name, allowedTools) : true;
60
+ if (canUseTool) return canUseTool(name, input);
61
+ if (onAllowlist) return { behavior: "allow" };
62
+ return {
63
+ behavior: "deny",
64
+ message: `${name} is not in allowedTools and no canUseTool callback is set.`
65
+ };
66
+ }
67
+ };
68
+
69
+ // src/loop.ts
70
+ var counter = 0;
71
+ var genId = () => `tu_${++counter}`;
72
+ async function* runLoop(opts) {
73
+ const { provider, registry, permissions, system, messages, toolContext } = opts;
74
+ const maxTurns = opts.maxTurns ?? 8;
75
+ const tools = registry.schemas(opts.toolFilter);
76
+ let turns = 0;
77
+ let finalText = "";
78
+ while (turns < maxTurns) {
79
+ turns++;
80
+ const res = await provider.generate({ system, messages, tools });
81
+ for (const b of res.content) if (b.type === "tool_use" && !b.id) b.id = genId();
82
+ messages.push({ role: "assistant", content: res.content });
83
+ for (const b of res.content) {
84
+ if (b.type === "text" && b.text.trim()) {
85
+ yield { type: "assistant_text", text: b.text, stage: opts.stage };
86
+ }
87
+ }
88
+ if (res.stopReason !== "tool_use") {
89
+ finalText = res.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
90
+ return { finalText, turns, messages };
91
+ }
92
+ const toolResults = [];
93
+ for (const b of res.content) {
94
+ if (b.type !== "tool_use") continue;
95
+ yield { type: "tool_use", name: b.name, input: b.input, stage: opts.stage };
96
+ const decision = await permissions.check(b.name, b.input);
97
+ if (decision.behavior === "deny") {
98
+ yield { type: "permission_denied", name: b.name, message: decision.message, stage: opts.stage };
99
+ toolResults.push({
100
+ type: "tool_result",
101
+ toolUseId: b.id,
102
+ content: `Permission denied: ${decision.message}`,
103
+ isError: true
104
+ });
105
+ continue;
106
+ }
107
+ const input = decision.updatedInput ?? b.input;
108
+ const result = await registry.execute(b.name, input, toolContext);
109
+ yield {
110
+ type: "tool_result",
111
+ name: b.name,
112
+ content: result.content,
113
+ isError: !!result.isError,
114
+ stage: opts.stage
115
+ };
116
+ toolResults.push({
117
+ type: "tool_result",
118
+ toolUseId: b.id,
119
+ content: result.content,
120
+ isError: result.isError
121
+ });
122
+ }
123
+ messages.push({ role: "user", content: toolResults });
124
+ }
125
+ return { finalText: finalText || "[max turns reached]", turns, messages };
126
+ }
127
+
128
+ // src/pipeline.ts
129
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
130
+ import { join as join3 } from "path";
131
+
132
+ // src/context.ts
133
+ import { existsSync } from "fs";
134
+ import { readFile } from "fs/promises";
135
+ import { join } from "path";
136
+ var estimateTokens = (s) => Math.ceil(s.length / 4);
137
+ async function readIfExists(path) {
138
+ if (!existsSync(path)) return null;
139
+ return readFile(path, "utf8");
140
+ }
141
+ function relativeName(root, path) {
142
+ return path.replace(root, "").replace(/^[\\/]/, "").replace(/\\/g, "/");
143
+ }
144
+ async function loadStageContext(workspaceDir, contract) {
145
+ const parts = [];
146
+ const files = [];
147
+ const push = async (label, path) => {
148
+ const text = await readIfExists(path);
149
+ if (text == null) return;
150
+ const src = relativeName(workspaceDir, path);
151
+ parts.push(`<context layer="${label}" src="${src}">
152
+ ${text.trim()}
153
+ </context>`);
154
+ files.push(src);
155
+ };
156
+ await push("0 identity", join(workspaceDir, "AGENT.md"));
157
+ await push("1 routing", join(workspaceDir, "CONTEXT.md"));
158
+ await push("2 contract", contract.contractPath);
159
+ for (const input of contract.inputs) {
160
+ const abs = join(contract.stageDir, input.path);
161
+ await push(input.layer === 3 ? "3 reference" : "4 working", abs);
162
+ }
163
+ const system = parts.join("\n\n");
164
+ return { system, files, tokensEstimated: estimateTokens(system) };
165
+ }
166
+
167
+ // src/builtins.ts
168
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
169
+ import { dirname, relative, resolve } from "path";
170
+ function safeResolve(workspaceDir, p) {
171
+ const root = resolve(workspaceDir);
172
+ const full = resolve(root, p);
173
+ const rel = relative(root, full);
174
+ if (rel.startsWith("..") || resolve(root, rel) !== full) {
175
+ throw new Error(`Path escapes workspace: ${p}`);
176
+ }
177
+ return full;
178
+ }
179
+ var readFileTool = tool(
180
+ "read_file",
181
+ "Read a UTF-8 text file relative to the workspace root.",
182
+ { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
183
+ async (input, ctx) => {
184
+ const full = safeResolve(ctx.workspaceDir, input.path);
185
+ return { content: await readFile2(full, "utf8") };
186
+ }
187
+ );
188
+ var writeFileTool = tool(
189
+ "write_file",
190
+ "Write a UTF-8 text file relative to the workspace root (creates parent dirs).",
191
+ {
192
+ type: "object",
193
+ properties: { path: { type: "string" }, content: { type: "string" } },
194
+ required: ["path", "content"]
195
+ },
196
+ async (input, ctx) => {
197
+ const full = safeResolve(ctx.workspaceDir, input.path);
198
+ await mkdir(dirname(full), { recursive: true });
199
+ await writeFile(full, input.content, "utf8");
200
+ return { content: `Wrote ${input.content.length} chars to ${input.path}` };
201
+ }
202
+ );
203
+ var listDirTool = tool(
204
+ "list_dir",
205
+ "List entries of a directory relative to the workspace root.",
206
+ { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
207
+ async (input, ctx) => {
208
+ const full = safeResolve(ctx.workspaceDir, input.path);
209
+ const entries = await readdir(full, { withFileTypes: true });
210
+ return { content: entries.map((e) => e.isDirectory() ? e.name + "/" : e.name).join("\n") };
211
+ }
212
+ );
213
+ var builtinTools = [readFileTool, writeFileTool, listDirTool];
214
+
215
+ // src/subagents.ts
216
+ import { readFile as readFile3, readdir as readdir2 } from "fs/promises";
217
+ import { join as join2 } from "path";
218
+ function section(body, name) {
219
+ const re = new RegExp(`(?:^|\\n)##\\s+${name}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i");
220
+ const m = body.match(re);
221
+ return m ? m[1].trim() : "";
222
+ }
223
+ function parseContract(name, stageDir, contractPath, body) {
224
+ const order = parseInt(name.slice(0, 2), 10) || 0;
225
+ const inputs = [];
226
+ for (const line of section(body, "Inputs").split("\n")) {
227
+ const m = line.match(/Layer\s+([34])\b.*?:\s*([^\s#]+)\s*(?:#\s*(.*))?$/i);
228
+ if (m) inputs.push({ layer: Number(m[1]), path: m[2], note: m[3]?.trim() });
229
+ }
230
+ const outputs = [];
231
+ for (const line of section(body, "Outputs").split("\n")) {
232
+ const m = line.match(/-\s*([A-Za-z0-9._-]+\.(?:md|json|txt))/);
233
+ if (m) outputs.push(m[1]);
234
+ }
235
+ const toolsRaw = section(body, "Tools");
236
+ const tools = toolsRaw ? toolsRaw.split(/[\n,]/).map((s) => s.replace(/^[-*]\s*/, "").trim()).filter(Boolean) : [];
237
+ return {
238
+ name,
239
+ order,
240
+ stageDir,
241
+ contractPath,
242
+ inputs,
243
+ process: section(body, "Process"),
244
+ outputs,
245
+ tools
246
+ };
247
+ }
248
+ async function loadStages(workspaceDir) {
249
+ const stagesRoot = join2(workspaceDir, "stages");
250
+ const entries = await readdir2(stagesRoot, { withFileTypes: true });
251
+ const dirs = entries.filter((e) => e.isDirectory() && /^\d{2}_/.test(e.name)).map((e) => e.name).sort();
252
+ const contracts = [];
253
+ for (const name of dirs) {
254
+ const stageDir = join2(stagesRoot, name);
255
+ const contractPath = join2(stageDir, "CONTEXT.md");
256
+ const body = await readFile3(contractPath, "utf8");
257
+ contracts.push(parseContract(name, stageDir, contractPath, body));
258
+ }
259
+ return contracts;
260
+ }
261
+
262
+ // src/pipeline.ts
263
+ async function* runPipeline(opts) {
264
+ const registry = new ToolRegistry().addBuiltins(builtinTools);
265
+ for (const s of opts.mcpServers ?? []) registry.addServer(s);
266
+ const budget = opts.contextBudgetTokens ?? 8e3;
267
+ const stages = await loadStages(opts.workspaceDir);
268
+ for (const stage of stages) {
269
+ yield { type: "stage_start", stage: stage.name };
270
+ const ctx = await loadStageContext(opts.workspaceDir, stage);
271
+ yield { type: "context_loaded", stage: stage.name, tokensEstimated: ctx.tokensEstimated, files: ctx.files };
272
+ if (ctx.tokensEstimated > budget) {
273
+ yield {
274
+ type: "assistant_text",
275
+ stage: stage.name,
276
+ text: `\u26A0 context ~${ctx.tokensEstimated} tok exceeds budget ${budget} \u2014 trim this stage's Inputs (ICM principle 3).`
277
+ };
278
+ }
279
+ const perm = new PermissionEngine({
280
+ allowedTools: stage.tools,
281
+ // [] ⇒ a pure prose transform, no tools offered
282
+ disallowedTools: opts.permissions?.disallowedTools,
283
+ canUseTool: opts.permissions?.canUseTool
284
+ });
285
+ const toolFilter = (n) => stage.tools.some((p) => p.endsWith("*") ? n.startsWith(p.slice(0, -1)) : p === n);
286
+ const userPrompt = `Execute this stage.
287
+
288
+ ## Process
289
+ ${stage.process}
290
+
291
+ Produce: ${stage.outputs.join(", ") || "a single markdown artifact"}.`;
292
+ const messages = [{ role: "user", content: [{ type: "text", text: userPrompt }] }];
293
+ const result = yield* runLoop({
294
+ provider: opts.provider,
295
+ registry,
296
+ permissions: perm,
297
+ system: ctx.system,
298
+ messages,
299
+ toolFilter,
300
+ toolContext: { workspaceDir: opts.workspaceDir, stageDir: stage.stageDir },
301
+ maxTurns: opts.maxTurnsPerStage,
302
+ stage: stage.name
303
+ });
304
+ const outDir = join3(stage.stageDir, "output");
305
+ await mkdir2(outDir, { recursive: true });
306
+ const primary = stage.outputs[0] ?? "output.md";
307
+ const path = join3(outDir, primary);
308
+ await writeFile2(path, result.finalText + "\n", "utf8");
309
+ yield { type: "stage_output", stage: stage.name, artifact: primary, path };
310
+ yield { type: "result", stage: stage.name, finalText: result.finalText, turns: result.turns };
311
+ const proceed = opts.reviewGate ? await opts.reviewGate(stage, [{ artifact: primary, path, text: result.finalText }]) : true;
312
+ if (!proceed) return;
313
+ }
314
+ }
315
+
316
+ // src/providers/mock.ts
317
+ var MockProvider = class {
318
+ name = "mock";
319
+ opts;
320
+ constructor(opts = {}) {
321
+ this.opts = opts;
322
+ }
323
+ async generate(req) {
324
+ const alreadyUsedTool = req.messages.some(
325
+ (m) => m.content.some((b) => b.type === "tool_use")
326
+ );
327
+ if (req.tools.length > 0 && !alreadyUsedTool) {
328
+ const t = req.tools[0];
329
+ return {
330
+ stopReason: "tool_use",
331
+ content: [{ type: "tool_use", id: "", name: t.name, input: this.sampleInput(t) }]
332
+ };
333
+ }
334
+ return { stopReason: "end_turn", content: [{ type: "text", text: this.synthesize(req) }] };
335
+ }
336
+ sampleInput(t) {
337
+ const props = t.inputSchema.properties ?? {};
338
+ const topic = "the requested topic";
339
+ const out = {};
340
+ for (const [k, v] of Object.entries(props)) {
341
+ out[k] = v.type === "number" ? 3 : v.type === "boolean" ? true : k === "path" ? "shared/notes.md" : topic;
342
+ }
343
+ return out;
344
+ }
345
+ synthesize(req) {
346
+ const toolData = req.messages.flatMap((m) => m.content).filter((b) => b.type === "tool_result").map((b) => b.content).join("\n");
347
+ const contract = extractLayer(req.system, "2 contract");
348
+ const label = this.opts.label ? ` ${this.opts.label}` : "";
349
+ const lines = [
350
+ `<!-- mock model output${label} -->`,
351
+ "",
352
+ "## Result",
353
+ "",
354
+ "Produced by the Torus MockProvider \u2014 proof that layered context \u2192",
355
+ "agent loop \u2192 output handoff works end to end. Replace with AnthropicProvider",
356
+ "for real generation."
357
+ ];
358
+ if (toolData) {
359
+ lines.push("", "### Tool-sourced material", "", "```", toolData.slice(0, 600), "```");
360
+ }
361
+ if (contract) {
362
+ lines.push("", "### Stage focus (read from the Layer 2 contract)", "", firstLines(contract, 6));
363
+ }
364
+ return lines.join("\n");
365
+ }
366
+ };
367
+ function extractLayer(system, layer) {
368
+ const m = system.match(new RegExp(`<context layer="${layer}"[^>]*>([\\s\\S]*?)</context>`));
369
+ return m ? m[1].trim() : "";
370
+ }
371
+ function firstLines(s, n) {
372
+ return s.split("\n").slice(0, n).join("\n");
373
+ }
374
+
375
+ // src/router.ts
376
+ var CHEAP_MODEL = "claude-haiku-4-5";
377
+ var EXPENSIVE_MODEL = "claude-sonnet-4-6";
378
+ var GEMINI_CHEAP_MODEL = "gemini-2.5-flash-lite";
379
+ var GEMINI_EXPENSIVE_MODEL = "gemini-2.5-pro";
380
+ var SIMPLE_KEYWORDS = [
381
+ "hello",
382
+ "hi ",
383
+ "hey",
384
+ "thanks",
385
+ "thank you",
386
+ "yes",
387
+ "no",
388
+ "format",
389
+ "json",
390
+ "yaml",
391
+ "uppercase",
392
+ "lowercase",
393
+ "capitalize",
394
+ "translate",
395
+ "spell",
396
+ "reverse",
397
+ "echo",
398
+ "greeting"
399
+ ];
400
+ var estimateTokens2 = (s) => Math.ceil(s.length / 4);
401
+ function fastHeuristic(prompt) {
402
+ const tokens = estimateTokens2(prompt);
403
+ const lower = prompt.toLowerCase();
404
+ if (tokens <= 30 && SIMPLE_KEYWORDS.some((k) => lower.includes(k))) return "SIMPLE";
405
+ if (tokens >= 400) return "COMPLEX";
406
+ return null;
407
+ }
408
+ var JUDGE_SYSTEM = 'You are a routing classifier. Decide whether a user query needs a powerful model or can be handled by a small, fast one. Classify as SIMPLE (greetings, formatting, short factual lookups, simple rewrites, single-step tasks) or COMPLEX (multi-step reasoning, coding, analysis, planning, nuanced judgment). Respond ONLY with the required JSON: {"complexity": "SIMPLE" | "COMPLEX"}.';
409
+ var COMPLEXITY_SCHEMA = {
410
+ type: "object",
411
+ properties: { complexity: { type: "string", enum: ["SIMPLE", "COMPLEX"] } },
412
+ required: ["complexity"],
413
+ additionalProperties: false
414
+ };
415
+ function parseComplexity(text) {
416
+ try {
417
+ const parsed = JSON.parse(text);
418
+ if (parsed.complexity === "SIMPLE" || parsed.complexity === "COMPLEX") return parsed.complexity;
419
+ } catch {
420
+ }
421
+ const m = text.toUpperCase().match(/\b(SIMPLE|COMPLEX)\b/);
422
+ if (m) return m[1];
423
+ throw new Error(`judge returned unparseable complexity: ${text.slice(0, 80)}`);
424
+ }
425
+ var sharedAnthropic;
426
+ async function getAnthropic(opts) {
427
+ if (opts.client) return opts.client;
428
+ if (sharedAnthropic) return sharedAnthropic;
429
+ const mod = await import("@anthropic-ai/sdk").catch(() => {
430
+ throw new Error("Anthropic judge needs @anthropic-ai/sdk (npm i @anthropic-ai/sdk).");
431
+ });
432
+ const Anthropic = mod.default ?? mod.Anthropic;
433
+ sharedAnthropic = new Anthropic({ apiKey: opts.apiKey ?? process.env.ANTHROPIC_API_KEY });
434
+ return sharedAnthropic;
435
+ }
436
+ async function judgeComplexity(prompt, opts = {}) {
437
+ const client = await getAnthropic(opts);
438
+ const res = await client.messages.create({
439
+ model: opts.judgeModel ?? CHEAP_MODEL,
440
+ max_tokens: 64,
441
+ system: JUDGE_SYSTEM,
442
+ output_config: { format: { type: "json_schema", schema: COMPLEXITY_SCHEMA } },
443
+ messages: [{ role: "user", content: prompt }]
444
+ });
445
+ const text = res.content.find((b) => b.type === "text")?.text ?? "";
446
+ return parseComplexity(text);
447
+ }
448
+ var sharedGemini;
449
+ async function getGemini(opts) {
450
+ if (opts.client) return opts.client;
451
+ if (sharedGemini) return sharedGemini;
452
+ const mod = await import("@google/genai").catch(() => {
453
+ throw new Error("Gemini judge needs @google/genai (npm i @google/genai).");
454
+ });
455
+ const GoogleGenAI = mod.GoogleGenAI;
456
+ sharedGemini = new GoogleGenAI({
457
+ apiKey: opts.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY
458
+ });
459
+ return sharedGemini;
460
+ }
461
+ async function judgeComplexityGemini(prompt, opts = {}) {
462
+ const client = await getGemini(opts);
463
+ const res = await client.models.generateContent({
464
+ model: opts.judgeModel ?? GEMINI_CHEAP_MODEL,
465
+ contents: prompt,
466
+ config: {
467
+ systemInstruction: JUDGE_SYSTEM,
468
+ responseMimeType: "application/json",
469
+ responseSchema: {
470
+ type: "object",
471
+ properties: { complexity: { type: "string", enum: ["SIMPLE", "COMPLEX"] } },
472
+ required: ["complexity"]
473
+ }
474
+ }
475
+ });
476
+ return parseComplexity(res.text ?? "");
477
+ }
478
+ async function classifyWith(prompt, judge, opts) {
479
+ return fastHeuristic(prompt) ?? judge(prompt, opts);
480
+ }
481
+ function classifyComplexity(prompt, opts = {}) {
482
+ return classifyWith(prompt, judgeComplexity, opts);
483
+ }
484
+ function classifyComplexityGemini(prompt, opts = {}) {
485
+ return classifyWith(prompt, judgeComplexityGemini, opts);
486
+ }
487
+ async function routeWith(prompt, cfg, opts) {
488
+ let model = cfg.expensiveModel;
489
+ try {
490
+ const complexity = await classifyWith(prompt, cfg.judge, opts);
491
+ model = complexity === "SIMPLE" ? cfg.cheapModel : cfg.expensiveModel;
492
+ } catch (err) {
493
+ console.warn(
494
+ `[router] classification failed \u2014 defaulting to expensive model. Reason: ${err.message}`
495
+ );
496
+ model = cfg.expensiveModel;
497
+ }
498
+ record(model, model === cfg.cheapModel);
499
+ return model;
500
+ }
501
+ function selectModel(prompt, opts = {}) {
502
+ return routeWith(prompt, { cheapModel: CHEAP_MODEL, expensiveModel: EXPENSIVE_MODEL, judge: judgeComplexity }, opts);
503
+ }
504
+ function selectGeminiModel(prompt, opts = {}) {
505
+ return routeWith(
506
+ prompt,
507
+ { cheapModel: GEMINI_CHEAP_MODEL, expensiveModel: GEMINI_EXPENSIVE_MODEL, judge: judgeComplexityGemini },
508
+ opts
509
+ );
510
+ }
511
+ var cheapCount = 0;
512
+ var expensiveCount = 0;
513
+ function record(model, isCheap) {
514
+ if (isCheap) cheapCount++;
515
+ else expensiveCount++;
516
+ const total = cheapCount + expensiveCount;
517
+ const pct = (n) => (n / total * 100).toFixed(0);
518
+ console.log(
519
+ `[router] \u2192 ${isCheap ? "CHEAP" : "EXPENSIVE"} (${model}) | cheap ${pct(cheapCount)}% / expensive ${pct(expensiveCount)}% (n=${total})`
520
+ );
521
+ }
522
+ function getRoutingStats() {
523
+ const total = cheapCount + expensiveCount;
524
+ return {
525
+ cheap: cheapCount,
526
+ expensive: expensiveCount,
527
+ total,
528
+ cheapPct: total ? cheapCount / total * 100 : 0,
529
+ expensivePct: total ? expensiveCount / total * 100 : 0
530
+ };
531
+ }
532
+ function latestUserText(messages) {
533
+ for (let i = messages.length - 1; i >= 0; i--) {
534
+ const m = messages[i];
535
+ if (m.role !== "user") continue;
536
+ const text = m.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
537
+ if (text) return text;
538
+ }
539
+ return "";
540
+ }
541
+
542
+ // src/providers/anthropic.ts
543
+ var AnthropicProvider = class {
544
+ name = "anthropic";
545
+ client;
546
+ model;
547
+ maxTokens;
548
+ apiKey;
549
+ route;
550
+ constructor(opts = {}) {
551
+ this.model = opts.model ?? "claude-sonnet-4-6";
552
+ this.maxTokens = opts.maxTokens ?? 2048;
553
+ this.apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
554
+ this.route = opts.route ?? false;
555
+ }
556
+ async ensureClient() {
557
+ if (this.client) return;
558
+ const mod = await import("@anthropic-ai/sdk").catch(() => {
559
+ throw new Error(
560
+ "AnthropicProvider needs the @anthropic-ai/sdk package: run `npm i @anthropic-ai/sdk`."
561
+ );
562
+ });
563
+ const Anthropic = mod.default ?? mod.Anthropic;
564
+ this.client = new Anthropic({ apiKey: this.apiKey });
565
+ }
566
+ async generate(req) {
567
+ await this.ensureClient();
568
+ const model = this.route ? await selectModel(latestUserText(req.messages), {
569
+ client: this.client,
570
+ apiKey: this.apiKey
571
+ }) : this.model;
572
+ const res = await this.client.messages.create({
573
+ model,
574
+ max_tokens: this.maxTokens,
575
+ system: req.system,
576
+ tools: req.tools.map((t) => ({
577
+ name: t.name,
578
+ description: t.description,
579
+ input_schema: t.inputSchema
580
+ })),
581
+ messages: req.messages.map(toApiMessage)
582
+ });
583
+ const content = res.content.map((b) => {
584
+ if (b.type === "tool_use") return { type: "tool_use", id: b.id, name: b.name, input: b.input };
585
+ return { type: "text", text: b.type === "text" ? b.text : "" };
586
+ });
587
+ const stopReason = res.stop_reason === "tool_use" ? "tool_use" : "end_turn";
588
+ return { content, stopReason };
589
+ }
590
+ };
591
+ function toApiMessage(m) {
592
+ return {
593
+ role: m.role,
594
+ content: m.content.map((b) => {
595
+ if (b.type === "text") return { type: "text", text: b.text };
596
+ if (b.type === "tool_use") return { type: "tool_use", id: b.id, name: b.name, input: b.input };
597
+ return { type: "tool_result", tool_use_id: b.toolUseId, content: b.content, is_error: b.isError };
598
+ })
599
+ };
600
+ }
601
+
602
+ // src/providers/gemini.ts
603
+ var GeminiProvider = class {
604
+ name = "gemini";
605
+ client;
606
+ model;
607
+ apiKey;
608
+ route;
609
+ constructor(opts = {}) {
610
+ this.model = opts.model ?? "gemini-2.5-flash";
611
+ this.apiKey = opts.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
612
+ this.route = opts.route ?? false;
613
+ }
614
+ async ensureClient() {
615
+ if (this.client) return;
616
+ const mod = await import("@google/genai").catch(() => {
617
+ throw new Error("GeminiProvider needs the @google/genai package: run `npm i @google/genai`.");
618
+ });
619
+ const GoogleGenAI = mod.GoogleGenAI;
620
+ this.client = new GoogleGenAI({ apiKey: this.apiKey });
621
+ }
622
+ async generate(req) {
623
+ await this.ensureClient();
624
+ const model = this.route ? await selectGeminiModel(latestUserText(req.messages), {
625
+ client: this.client,
626
+ apiKey: this.apiKey
627
+ }) : this.model;
628
+ const idToName = toolUseNames(req.messages);
629
+ const config = {};
630
+ if (req.system) config.systemInstruction = req.system;
631
+ if (req.tools.length) {
632
+ config.tools = [
633
+ {
634
+ functionDeclarations: req.tools.map((t) => ({
635
+ name: t.name,
636
+ description: t.description,
637
+ parameters: t.inputSchema
638
+ }))
639
+ }
640
+ ];
641
+ }
642
+ const res = await this.client.models.generateContent({
643
+ model,
644
+ contents: req.messages.map((m) => toGeminiContent(m, idToName)),
645
+ config
646
+ });
647
+ const content = [];
648
+ const text = res.text;
649
+ if (text && text.trim()) content.push({ type: "text", text });
650
+ const calls = res.functionCalls ?? [];
651
+ for (const fc of calls) {
652
+ content.push({ type: "tool_use", id: fc.id ?? "", name: fc.name, input: fc.args ?? {} });
653
+ }
654
+ if (content.length === 0) content.push({ type: "text", text: "" });
655
+ return { content, stopReason: calls.length ? "tool_use" : "end_turn" };
656
+ }
657
+ };
658
+ function toolUseNames(messages) {
659
+ const map = /* @__PURE__ */ new Map();
660
+ for (const m of messages) {
661
+ for (const b of m.content) {
662
+ if (b.type === "tool_use") map.set(b.id, b.name);
663
+ }
664
+ }
665
+ return map;
666
+ }
667
+ function toGeminiContent(m, idToName) {
668
+ const role = m.role === "assistant" ? "model" : "user";
669
+ const parts = m.content.map((b) => {
670
+ if (b.type === "text") return { text: b.text };
671
+ if (b.type === "tool_use") return { functionCall: { id: b.id, name: b.name, args: b.input } };
672
+ return {
673
+ functionResponse: {
674
+ id: b.toolUseId,
675
+ name: idToName.get(b.toolUseId) ?? b.toolUseId,
676
+ response: b.isError ? { error: b.content } : { result: b.content }
677
+ }
678
+ };
679
+ });
680
+ return { role, parts };
681
+ }
682
+
683
+ // src/index.ts
684
+ async function* query(prompt, options) {
685
+ const registry = new ToolRegistry();
686
+ if (options.includeBuiltins ?? true) registry.addBuiltins(builtinTools);
687
+ for (const s of options.mcpServers ?? []) registry.addServer(s);
688
+ const messages = [{ role: "user", content: [{ type: "text", text: prompt }] }];
689
+ const result = yield* runLoop({
690
+ provider: options.provider,
691
+ registry,
692
+ permissions: new PermissionEngine(options.permissions ?? {}),
693
+ system: options.system ?? "You are a helpful agent.",
694
+ messages,
695
+ toolContext: { workspaceDir: options.workspaceDir ?? process.cwd() },
696
+ maxTurns: options.maxTurns
697
+ });
698
+ yield { type: "result", finalText: result.finalText, turns: result.turns };
699
+ }
700
+ export {
701
+ AnthropicProvider,
702
+ CHEAP_MODEL,
703
+ EXPENSIVE_MODEL,
704
+ GEMINI_CHEAP_MODEL,
705
+ GEMINI_EXPENSIVE_MODEL,
706
+ GeminiProvider,
707
+ MockProvider,
708
+ PermissionEngine,
709
+ ToolRegistry,
710
+ builtinTools,
711
+ classifyComplexity,
712
+ classifyComplexityGemini,
713
+ createSdkMcpServer,
714
+ fastHeuristic,
715
+ getRoutingStats,
716
+ judgeComplexity,
717
+ judgeComplexityGemini,
718
+ latestUserText,
719
+ listDirTool,
720
+ loadStageContext,
721
+ loadStages,
722
+ matchesAllow,
723
+ parseContract,
724
+ query,
725
+ readFileTool,
726
+ runLoop,
727
+ runPipeline,
728
+ selectGeminiModel,
729
+ selectModel,
730
+ tool,
731
+ writeFileTool
732
+ };
733
+ //# sourceMappingURL=index.js.map