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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools.ts","../src/permissions.ts","../src/loop.ts","../src/pipeline.ts","../src/context.ts","../src/builtins.ts","../src/subagents.ts","../src/providers/mock.ts","../src/router.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/index.ts"],"sourcesContent":["import type {\n JSONSchema,\n SdkMcpServer,\n ToolContext,\n ToolDefinition,\n ToolResultPayload,\n ToolSchema,\n} from \"./types.ts\";\n\n/**\n * Define a custom tool. Mirrors `tool()` from the Claude Agent SDK.\n * tool(\"get_temp\", \"Get temperature\", { type:\"object\", ... }, async (input, ctx) => ...)\n */\nexport function tool(\n name: string,\n description: string,\n inputSchema: JSONSchema,\n handler: (input: any, ctx: ToolContext) => Promise<ToolResultPayload> | ToolResultPayload,\n): ToolDefinition {\n return { name, description, inputSchema, handler };\n}\n\n/**\n * Bundle tools into an in-process MCP server. Mirrors `createSdkMcpServer()`.\n * Tools become namespaced `mcp__<name>__<tool>` when registered.\n */\nexport function createSdkMcpServer(opts: {\n name: string;\n version?: string;\n tools: ToolDefinition[];\n}): SdkMcpServer {\n return { kind: \"sdk-mcp\", name: opts.name, version: opts.version ?? \"1.0.0\", tools: opts.tools };\n}\n\nexport interface RegisteredTool {\n fullName: string; // \"mcp__research__lookup\" or built-in \"read_file\"\n def: ToolDefinition;\n}\n\n/** Holds the model-facing tool catalog and executes calls by namespaced name. */\nexport class ToolRegistry {\n private map = new Map<string, ToolDefinition>();\n\n /** Built-ins register under their bare name (no namespace). */\n addBuiltins(defs: ToolDefinition[]): this {\n for (const d of defs) this.map.set(d.name, d);\n return this;\n }\n\n /** SDK MCP server tools register as mcp__<server>__<tool>. */\n addServer(server: SdkMcpServer): this {\n for (const t of server.tools) this.map.set(`mcp__${server.name}__${t.name}`, t);\n return this;\n }\n\n has(fullName: string): boolean {\n return this.map.has(fullName);\n }\n\n list(): RegisteredTool[] {\n return [...this.map.entries()].map(([fullName, def]) => ({ fullName, def }));\n }\n\n /** Tool schemas to hand the model, optionally filtered to a stage's allowlist. */\n schemas(filter?: (fullName: string) => boolean): ToolSchema[] {\n return this.list()\n .filter((t) => !filter || filter(t.fullName))\n .map((t) => ({ name: t.fullName, description: t.def.description, inputSchema: t.def.inputSchema }));\n }\n\n async execute(\n fullName: string,\n input: Record<string, unknown>,\n ctx: ToolContext,\n ): Promise<ToolResultPayload> {\n const def = this.map.get(fullName);\n if (!def) return { content: `Unknown tool: ${fullName}`, isError: true };\n try {\n return await def.handler(input, ctx);\n } catch (err) {\n return { content: `Tool ${fullName} threw: ${(err as Error).message}`, isError: true };\n }\n }\n}\n","import type { CanUseTool, PermissionDecision } from \"./types.ts\";\n\n/** Match a tool name against patterns supporting a trailing \"*\" wildcard. */\nexport function matchesAllow(name: string, patterns: string[]): boolean {\n return patterns.some((p) => {\n if (p === \"*\") return true;\n if (p.endsWith(\"*\")) return name.startsWith(p.slice(0, -1));\n return p === name;\n });\n}\n\nexport interface PermissionConfig {\n /** Allowlist (wildcards ok). If omitted, all tools allowed unless canUseTool vetoes. */\n allowedTools?: string[];\n /** Explicit denials, evaluated first. */\n disallowedTools?: string[];\n /** Final custom gate — can allow non-allowlisted tools, veto allowlisted ones, or rewrite input. */\n canUseTool?: CanUseTool;\n}\n\n/**\n * Evaluation order (mirrors the Agent SDK):\n * 1. disallowedTools → deny\n * 2. allowedTools → allow (if no canUseTool)\n * 3. canUseTool → final say\n * 4. default → allow when no allowlist, deny when allowlist set and unmatched\n */\nexport class PermissionEngine {\n private cfg: PermissionConfig;\n constructor(cfg: PermissionConfig = {}) {\n this.cfg = cfg;\n }\n\n async check(name: string, input: Record<string, unknown>): Promise<PermissionDecision> {\n const { allowedTools, disallowedTools, canUseTool } = this.cfg;\n\n if (disallowedTools && matchesAllow(name, disallowedTools)) {\n return { behavior: \"deny\", message: `${name} is in disallowedTools.` };\n }\n\n const onAllowlist = allowedTools ? matchesAllow(name, allowedTools) : true;\n\n if (canUseTool) return canUseTool(name, input); // callback has the final word\n\n if (onAllowlist) return { behavior: \"allow\" };\n return {\n behavior: \"deny\",\n message: `${name} is not in allowedTools and no canUseTool callback is set.`,\n };\n }\n}\n","import type { PermissionEngine } from \"./permissions.ts\";\nimport type { ToolRegistry } from \"./tools.ts\";\nimport type {\n AgentEvent,\n Message,\n ModelProvider,\n ToolContext,\n ToolResultBlock,\n} from \"./types.ts\";\n\n// The core agentic loop: gather context → call model → if it wants tools, run them\n// under the permission gate and feed results back → repeat until the model stops\n// asking for tools (or we hit maxTurns). This is the same contract the Claude Agent\n// SDK runs; everything else in this package just shapes what enters and leaves it.\n\nexport interface LoopOptions {\n provider: ModelProvider;\n registry: ToolRegistry;\n permissions: PermissionEngine;\n system: string;\n messages: Message[]; // seeded with the user turn\n toolContext: ToolContext;\n toolFilter?: (fullName: string) => boolean; // which tools to offer this run\n maxTurns?: number;\n stage?: string;\n}\n\nexport interface LoopResult {\n finalText: string;\n turns: number;\n messages: Message[];\n}\n\nlet counter = 0;\nconst genId = () => `tu_${++counter}`;\n\nexport async function* runLoop(opts: LoopOptions): AsyncGenerator<AgentEvent, LoopResult> {\n const { provider, registry, permissions, system, messages, toolContext } = opts;\n const maxTurns = opts.maxTurns ?? 8;\n const tools = registry.schemas(opts.toolFilter);\n\n let turns = 0;\n let finalText = \"\";\n\n while (turns < maxTurns) {\n turns++;\n const res = await provider.generate({ system, messages, tools });\n\n // Ensure every tool_use has an id (mock providers may omit it).\n for (const b of res.content) if (b.type === \"tool_use\" && !b.id) b.id = genId();\n messages.push({ role: \"assistant\", content: res.content });\n\n for (const b of res.content) {\n if (b.type === \"text\" && b.text.trim()) {\n yield { type: \"assistant_text\", text: b.text, stage: opts.stage };\n }\n }\n\n if (res.stopReason !== \"tool_use\") {\n finalText = res.content\n .filter((b): b is Extract<typeof b, { type: \"text\" }> => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\")\n .trim();\n return { finalText, turns, messages };\n }\n\n const toolResults: ToolResultBlock[] = [];\n for (const b of res.content) {\n if (b.type !== \"tool_use\") continue;\n yield { type: \"tool_use\", name: b.name, input: b.input, stage: opts.stage };\n\n const decision = await permissions.check(b.name, b.input);\n if (decision.behavior === \"deny\") {\n yield { type: \"permission_denied\", name: b.name, message: decision.message, stage: opts.stage };\n toolResults.push({\n type: \"tool_result\",\n toolUseId: b.id,\n content: `Permission denied: ${decision.message}`,\n isError: true,\n });\n continue;\n }\n\n const input = decision.updatedInput ?? b.input;\n const result = await registry.execute(b.name, input, toolContext);\n yield {\n type: \"tool_result\",\n name: b.name,\n content: result.content,\n isError: !!result.isError,\n stage: opts.stage,\n };\n toolResults.push({\n type: \"tool_result\",\n toolUseId: b.id,\n content: result.content,\n isError: result.isError,\n });\n }\n\n messages.push({ role: \"user\", content: toolResults });\n }\n\n return { finalText: finalText || \"[max turns reached]\", turns, messages };\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { loadStageContext } from \"./context.ts\";\nimport { runLoop } from \"./loop.ts\";\nimport { PermissionEngine, type PermissionConfig } from \"./permissions.ts\";\nimport { builtinTools } from \"./builtins.ts\";\nimport { loadStages, type StageContract } from \"./subagents.ts\";\nimport { ToolRegistry } from \"./tools.ts\";\nimport type { AgentEvent, Message, ModelProvider, SdkMcpServer } from \"./types.ts\";\n\n// The ICM pipeline runner: walk numbered stages in order, give each stage only the\n// context its contract names, run the agent loop, write the named artifact to the\n// stage's output/ (the handoff point), then pause at a human review gate.\n\nexport interface PipelineOptions {\n workspaceDir: string;\n provider: ModelProvider;\n mcpServers?: SdkMcpServer[];\n /** Global permission overlay. A stage's own \"## Tools\" list is the primary allowlist. */\n permissions?: Pick<PermissionConfig, \"canUseTool\" | \"disallowedTools\">;\n /** Called after each stage writes output. Return false to halt the pipeline. */\n reviewGate?: (\n stage: StageContract,\n outputs: { artifact: string; path: string; text: string }[],\n ) => Promise<boolean> | boolean;\n maxTurnsPerStage?: number;\n contextBudgetTokens?: number; // ICM target ceiling, default 8000\n}\n\nexport async function* runPipeline(opts: PipelineOptions): AsyncGenerator<AgentEvent, void> {\n const registry = new ToolRegistry().addBuiltins(builtinTools);\n for (const s of opts.mcpServers ?? []) registry.addServer(s);\n\n const budget = opts.contextBudgetTokens ?? 8000;\n const stages = await loadStages(opts.workspaceDir);\n\n for (const stage of stages) {\n yield { type: \"stage_start\", stage: stage.name };\n\n // ── Layered context (ICM Layers 0–4, scoped to the contract) ──\n const ctx = await loadStageContext(opts.workspaceDir, stage);\n yield { type: \"context_loaded\", stage: stage.name, tokensEstimated: ctx.tokensEstimated, files: ctx.files };\n if (ctx.tokensEstimated > budget) {\n yield {\n type: \"assistant_text\",\n stage: stage.name,\n text: `⚠ context ~${ctx.tokensEstimated} tok exceeds budget ${budget} — trim this stage's Inputs (ICM principle 3).`,\n };\n }\n\n // ── The contract's \"## Tools\" list is the source of truth for availability ──\n const perm = new PermissionEngine({\n allowedTools: stage.tools, // [] ⇒ a pure prose transform, no tools offered\n disallowedTools: opts.permissions?.disallowedTools,\n canUseTool: opts.permissions?.canUseTool,\n });\n const toolFilter = (n: string) =>\n stage.tools.some((p) => (p.endsWith(\"*\") ? n.startsWith(p.slice(0, -1)) : p === n));\n\n const userPrompt =\n `Execute this stage.\\n\\n## Process\\n${stage.process}\\n\\n` +\n `Produce: ${stage.outputs.join(\", \") || \"a single markdown artifact\"}.`;\n const messages: Message[] = [{ role: \"user\", content: [{ type: \"text\", text: userPrompt }] }];\n\n const result = yield* runLoop({\n provider: opts.provider,\n registry,\n permissions: perm,\n system: ctx.system,\n messages,\n toolFilter,\n toolContext: { workspaceDir: opts.workspaceDir, stageDir: stage.stageDir },\n maxTurns: opts.maxTurnsPerStage,\n stage: stage.name,\n });\n\n // ── Persist the deliverable to output/ per the Outputs contract (the handoff) ──\n const outDir = join(stage.stageDir, \"output\");\n await mkdir(outDir, { recursive: true });\n const primary = stage.outputs[0] ?? \"output.md\";\n const path = join(outDir, primary);\n await writeFile(path, result.finalText + \"\\n\", \"utf8\");\n yield { type: \"stage_output\", stage: stage.name, artifact: primary, path };\n yield { type: \"result\", stage: stage.name, finalText: result.finalText, turns: result.turns };\n\n // ── Review gate: every stage boundary is an edit surface (ICM principle 4) ──\n const proceed = opts.reviewGate\n ? await opts.reviewGate(stage, [{ artifact: primary, path, text: result.finalText }])\n : true;\n if (!proceed) return;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { StageContract } from \"./subagents.ts\";\n\n// ICM principle 3 — \"layered context loading\": assemble the system prompt from\n// exactly the layers a stage names, nothing more. The control point is the\n// contract's Inputs list; this loader never loads \"everything just in case\".\n\nexport interface LoadedContext {\n system: string;\n files: string[];\n tokensEstimated: number;\n}\n\nconst estimateTokens = (s: string) => Math.ceil(s.length / 4); // ~4 chars/token heuristic\n\nasync function readIfExists(path: string): Promise<string | null> {\n if (!existsSync(path)) return null;\n return readFile(path, \"utf8\");\n}\n\nfunction relativeName(root: string, path: string): string {\n return path.replace(root, \"\").replace(/^[\\\\/]/, \"\").replace(/\\\\/g, \"/\");\n}\n\n/**\n * Build a stage's system prompt from the ICM layer hierarchy:\n * Layer 0 AGENT.md (identity + map)\n * Layer 1 CONTEXT.md (routing)\n * Layer 2 stage CONTEXT.md (this stage's contract)\n * Layer 3 scoped references (constraints — only files the contract names)\n * Layer 4 scoped working (prior stage output — only files the contract names)\n */\nexport async function loadStageContext(\n workspaceDir: string,\n contract: StageContract,\n): Promise<LoadedContext> {\n const parts: string[] = [];\n const files: string[] = [];\n\n const push = async (label: string, path: string) => {\n const text = await readIfExists(path);\n if (text == null) return;\n const src = relativeName(workspaceDir, path);\n parts.push(`<context layer=\"${label}\" src=\"${src}\">\\n${text.trim()}\\n</context>`);\n files.push(src);\n };\n\n await push(\"0 identity\", join(workspaceDir, \"AGENT.md\"));\n await push(\"1 routing\", join(workspaceDir, \"CONTEXT.md\"));\n await push(\"2 contract\", contract.contractPath);\n\n for (const input of contract.inputs) {\n const abs = join(contract.stageDir, input.path);\n await push(input.layer === 3 ? \"3 reference\" : \"4 working\", abs);\n }\n\n const system = parts.join(\"\\n\\n\");\n return { system, files, tokensEstimated: estimateTokens(system) };\n}\n","import { mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, relative, resolve } from \"node:path\";\nimport { tool } from \"./tools.ts\";\nimport type { ToolDefinition } from \"./types.ts\";\n\n// Confine all built-in file access to the workspace directory (defence in depth;\n// the permission layer is the primary gate).\nfunction safeResolve(workspaceDir: string, p: string): string {\n const root = resolve(workspaceDir);\n const full = resolve(root, p);\n const rel = relative(root, full);\n if (rel.startsWith(\"..\") || resolve(root, rel) !== full) {\n throw new Error(`Path escapes workspace: ${p}`);\n }\n return full;\n}\n\nexport const readFileTool: ToolDefinition = tool(\n \"read_file\",\n \"Read a UTF-8 text file relative to the workspace root.\",\n { type: \"object\", properties: { path: { type: \"string\" } }, required: [\"path\"] },\n async (input: { path: string }, ctx) => {\n const full = safeResolve(ctx.workspaceDir, input.path);\n return { content: await readFile(full, \"utf8\") };\n },\n);\n\nexport const writeFileTool: ToolDefinition = tool(\n \"write_file\",\n \"Write a UTF-8 text file relative to the workspace root (creates parent dirs).\",\n {\n type: \"object\",\n properties: { path: { type: \"string\" }, content: { type: \"string\" } },\n required: [\"path\", \"content\"],\n },\n async (input: { path: string; content: string }, ctx) => {\n const full = safeResolve(ctx.workspaceDir, input.path);\n await mkdir(dirname(full), { recursive: true });\n await writeFile(full, input.content, \"utf8\");\n return { content: `Wrote ${input.content.length} chars to ${input.path}` };\n },\n);\n\nexport const listDirTool: ToolDefinition = tool(\n \"list_dir\",\n \"List entries of a directory relative to the workspace root.\",\n { type: \"object\", properties: { path: { type: \"string\" } }, required: [\"path\"] },\n async (input: { path: string }, ctx) => {\n const full = safeResolve(ctx.workspaceDir, input.path);\n const entries = await readdir(full, { withFileTypes: true });\n return { content: entries.map((e) => (e.isDirectory() ? e.name + \"/\" : e.name)).join(\"\\n\") };\n },\n);\n\nexport const builtinTools: ToolDefinition[] = [readFileTool, writeFileTool, listDirTool];\n","import { readFile, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n// A \"subagent\" in this SDK is a stage: a markdown contract (Layer 2 CONTEXT.md)\n// that declares its Inputs / Process / Outputs / Tools. Parsing the contract turns\n// folder structure into agent architecture — the whole ICM premise.\n\nexport interface StageInput {\n layer: 3 | 4; // 3 = reference (constraints), 4 = working (input to process)\n path: string; // relative to the stage dir, exactly as the contract names it\n note?: string;\n}\n\nexport interface StageContract {\n name: string; // \"01_research\"\n order: number;\n stageDir: string;\n contractPath: string;\n inputs: StageInput[];\n process: string;\n outputs: string[]; // artifact filenames, written to the stage's output/\n tools: string[]; // allowlist patterns from the optional \"## Tools\" section\n}\n\n/** Pull the body of a \"## <Name>\" markdown section (up to the next \"## \" or EOF). */\nfunction section(body: string, name: string): string {\n const re = new RegExp(`(?:^|\\\\n)##\\\\s+${name}\\\\s*\\\\n([\\\\s\\\\S]*?)(?=\\\\n##\\\\s|$)`, \"i\");\n const m = body.match(re);\n return m ? m[1].trim() : \"\";\n}\n\nexport function parseContract(\n name: string,\n stageDir: string,\n contractPath: string,\n body: string,\n): StageContract {\n const order = parseInt(name.slice(0, 2), 10) || 0;\n\n // Inputs: \"- Layer 3 (reference): ../../_config/voice.md # optional note\"\n const inputs: StageInput[] = [];\n for (const line of section(body, \"Inputs\").split(\"\\n\")) {\n const m = line.match(/Layer\\s+([34])\\b.*?:\\s*([^\\s#]+)\\s*(?:#\\s*(.*))?$/i);\n if (m) inputs.push({ layer: Number(m[1]) as 3 | 4, path: m[2], note: m[3]?.trim() });\n }\n\n // Outputs: \"- research-output.md -> output/\"\n const outputs: string[] = [];\n for (const line of section(body, \"Outputs\").split(\"\\n\")) {\n const m = line.match(/-\\s*([A-Za-z0-9._-]+\\.(?:md|json|txt))/);\n if (m) outputs.push(m[1]);\n }\n\n // Tools (optional): the stage declares exactly which tools it may use.\n const toolsRaw = section(body, \"Tools\");\n const tools = toolsRaw\n ? toolsRaw\n .split(/[\\n,]/)\n .map((s) => s.replace(/^[-*]\\s*/, \"\").trim())\n .filter(Boolean)\n : [];\n\n return {\n name,\n order,\n stageDir,\n contractPath,\n inputs,\n process: section(body, \"Process\"),\n outputs,\n tools,\n };\n}\n\n/** Discover and parse every numbered stage folder, in execution order. */\nexport async function loadStages(workspaceDir: string): Promise<StageContract[]> {\n const stagesRoot = join(workspaceDir, \"stages\");\n const entries = await readdir(stagesRoot, { withFileTypes: true });\n const dirs = entries\n .filter((e) => e.isDirectory() && /^\\d{2}_/.test(e.name))\n .map((e) => e.name)\n .sort();\n\n const contracts: StageContract[] = [];\n for (const name of dirs) {\n const stageDir = join(stagesRoot, name);\n const contractPath = join(stageDir, \"CONTEXT.md\");\n const body = await readFile(contractPath, \"utf8\");\n contracts.push(parseContract(name, stageDir, contractPath, body));\n }\n return contracts;\n}\n","import type {\n ModelProvider,\n ModelRequest,\n ModelResponse,\n ToolSchema,\n} from \"../types.ts\";\n\nexport interface MockOptions {\n /** Label stamped into outputs so mock-generated content is unmistakable. */\n label?: string;\n}\n\n/**\n * A deterministic, offline provider that exercises the full agent loop with no API\n * key. Strategy: if tools are offered and none have been used yet, call the first\n * tool once; otherwise synthesize a final answer from the system context + any tool\n * results. It is intentionally dumb — its job is to prove the harness wiring, not to\n * write good prose. Swap in AnthropicProvider for real output.\n */\nexport class MockProvider implements ModelProvider {\n readonly name = \"mock\";\n private opts: MockOptions;\n constructor(opts: MockOptions = {}) {\n this.opts = opts;\n }\n\n async generate(req: ModelRequest): Promise<ModelResponse> {\n const alreadyUsedTool = req.messages.some((m) =>\n m.content.some((b) => b.type === \"tool_use\"),\n );\n if (req.tools.length > 0 && !alreadyUsedTool) {\n const t = req.tools[0];\n return {\n stopReason: \"tool_use\",\n content: [{ type: \"tool_use\", id: \"\", name: t.name, input: this.sampleInput(t) }],\n };\n }\n return { stopReason: \"end_turn\", content: [{ type: \"text\", text: this.synthesize(req) }] };\n }\n\n private sampleInput(t: ToolSchema): Record<string, unknown> {\n const props = (t.inputSchema.properties ?? {}) as Record<string, { type?: string }>;\n const topic = \"the requested topic\";\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(props)) {\n out[k] =\n v.type === \"number\"\n ? 3\n : v.type === \"boolean\"\n ? true\n : k === \"path\"\n ? \"shared/notes.md\"\n : topic;\n }\n return out;\n }\n\n private synthesize(req: ModelRequest): string {\n const toolData = req.messages\n .flatMap((m) => m.content)\n .filter((b) => b.type === \"tool_result\")\n .map((b) => (b as { content: string }).content)\n .join(\"\\n\");\n\n const contract = extractLayer(req.system, \"2 contract\");\n const label = this.opts.label ? ` ${this.opts.label}` : \"\";\n\n const lines = [\n `<!-- mock model output${label} -->`,\n \"\",\n \"## Result\",\n \"\",\n \"Produced by the Torus MockProvider — proof that layered context →\",\n \"agent loop → output handoff works end to end. Replace with AnthropicProvider\",\n \"for real generation.\",\n ];\n if (toolData) {\n lines.push(\"\", \"### Tool-sourced material\", \"\", \"```\", toolData.slice(0, 600), \"```\");\n }\n if (contract) {\n lines.push(\"\", \"### Stage focus (read from the Layer 2 contract)\", \"\", firstLines(contract, 6));\n }\n return lines.join(\"\\n\");\n }\n}\n\nfunction extractLayer(system: string, layer: string): string {\n const m = system.match(new RegExp(`<context layer=\"${layer}\"[^>]*>([\\\\s\\\\S]*?)</context>`));\n return m ? m[1].trim() : \"\";\n}\nfunction firstLines(s: string, n: number): string {\n return s.split(\"\\n\").slice(0, n).join(\"\\n\");\n}\n","// ─────────────────────────────────────────────────────────────────────────\n// Intelligent LLM router — picks CHEAP vs EXPENSIVE per query to cut API cost.\n//\n// Hybrid strategy (provider-agnostic):\n// 1. Fast heuristics (no API call) for the obvious cases.\n// 2. Otherwise a structured \"judge\" call to the CHEAP model that classifies\n// the query as SIMPLE | COMPLEX.\n// SIMPLE → cheap model, COMPLEX → expensive model.\n//\n// The same mechanism is provided for two provider families:\n// - Anthropic: selectModel() (Claude Haiku judge → Haiku / Sonnet)\n// - Gemini: selectGeminiModel() (Gemini Flash-Lite judge → Flash-Lite / Pro)\n//\n// Safety: the select* functions never throw — on any failure they default to\n// the EXPENSIVE model so the user experience never breaks.\n// ─────────────────────────────────────────────────────────────────────────\n\nimport type { Message } from \"./types.ts\";\n\n// ── Target models ──────────────────────────────────────────────────────────\n// Change these in one place per provider.\nexport const CHEAP_MODEL = \"claude-haiku-4-5\"; // $1 / $5 per MTok\nexport const EXPENSIVE_MODEL = \"claude-sonnet-4-6\"; // current default — $3 / $15 per MTok\n\n// Gemini defaults to the stable 2.5 family. Swap to newer IDs (e.g.\n// \"gemini-3.1-flash-lite\" / \"gemini-3.1-pro-preview\") if your key has access.\nexport const GEMINI_CHEAP_MODEL = \"gemini-2.5-flash-lite\";\nexport const GEMINI_EXPENSIVE_MODEL = \"gemini-2.5-pro\";\n\nexport type Complexity = \"SIMPLE\" | \"COMPLEX\";\n\nexport interface RouterOptions {\n /** Reuse an existing provider SDK client (avoids a second client init). */\n client?: any;\n /** API key for a lazily-created client (defaults to the provider's env var). */\n apiKey?: string;\n /** Model used as the complexity judge. Defaults to the provider's cheap model. */\n judgeModel?: string;\n}\n\n// ── 1. Fast heuristics (no API call, provider-agnostic) ─────────────────────\n\nconst SIMPLE_KEYWORDS = [\n \"hello\", \"hi \", \"hey\", \"thanks\", \"thank you\", \"yes\", \"no\",\n \"format\", \"json\", \"yaml\", \"uppercase\", \"lowercase\", \"capitalize\",\n \"translate\", \"spell\", \"reverse\", \"echo\", \"greeting\",\n];\n\nconst estimateTokens = (s: string) => Math.ceil(s.length / 4); // ~4 chars/token\n\n/**\n * Cheap, deterministic pre-classification. Returns a verdict only when it's\n * confident; otherwise null (defer to the judge).\n */\nexport function fastHeuristic(prompt: string): Complexity | null {\n const tokens = estimateTokens(prompt);\n const lower = prompt.toLowerCase();\n\n // Short prompt that mentions a trivial operation → SIMPLE, route immediately.\n if (tokens <= 30 && SIMPLE_KEYWORDS.some((k) => lower.includes(k))) return \"SIMPLE\";\n\n // Very long prompts are almost always real work → skip the judge call.\n if (tokens >= 400) return \"COMPLEX\";\n\n return null;\n}\n\n// ── 2. LLM judges (structured output on each provider's cheap model) ─────────\n\nconst JUDGE_SYSTEM =\n \"You are a routing classifier. Decide whether a user query needs a powerful \" +\n \"model or can be handled by a small, fast one. Classify as SIMPLE (greetings, \" +\n \"formatting, short factual lookups, simple rewrites, single-step tasks) or \" +\n \"COMPLEX (multi-step reasoning, coding, analysis, planning, nuanced judgment). \" +\n 'Respond ONLY with the required JSON: {\"complexity\": \"SIMPLE\" | \"COMPLEX\"}.';\n\nconst COMPLEXITY_SCHEMA = {\n type: \"object\",\n properties: { complexity: { type: \"string\", enum: [\"SIMPLE\", \"COMPLEX\"] } },\n required: [\"complexity\"],\n additionalProperties: false,\n};\n\n/** Robustly extract SIMPLE/COMPLEX from a judge response. Throws if neither. */\nfunction parseComplexity(text: string): Complexity {\n try {\n const parsed = JSON.parse(text) as { complexity?: string };\n if (parsed.complexity === \"SIMPLE\" || parsed.complexity === \"COMPLEX\") return parsed.complexity;\n } catch {\n // fall through to text scan\n }\n const m = text.toUpperCase().match(/\\b(SIMPLE|COMPLEX)\\b/);\n if (m) return m[1] as Complexity;\n throw new Error(`judge returned unparseable complexity: ${text.slice(0, 80)}`);\n}\n\n// -- Anthropic judge --\nlet sharedAnthropic: any;\nasync function getAnthropic(opts: RouterOptions): Promise<any> {\n if (opts.client) return opts.client;\n if (sharedAnthropic) return sharedAnthropic;\n const mod = await import(\"@anthropic-ai/sdk\").catch(() => {\n throw new Error(\"Anthropic judge needs @anthropic-ai/sdk (npm i @anthropic-ai/sdk).\");\n });\n const Anthropic = (mod as any).default ?? (mod as any).Anthropic;\n sharedAnthropic = new Anthropic({ apiKey: opts.apiKey ?? process.env.ANTHROPIC_API_KEY });\n return sharedAnthropic;\n}\n\n/** Grade complexity with Claude (structured output). May throw. */\nexport async function judgeComplexity(prompt: string, opts: RouterOptions = {}): Promise<Complexity> {\n const client = await getAnthropic(opts);\n const res = await client.messages.create({\n model: opts.judgeModel ?? CHEAP_MODEL,\n max_tokens: 64,\n system: JUDGE_SYSTEM,\n output_config: { format: { type: \"json_schema\", schema: COMPLEXITY_SCHEMA } },\n messages: [{ role: \"user\", content: prompt }],\n });\n const text: string = res.content.find((b: any) => b.type === \"text\")?.text ?? \"\";\n return parseComplexity(text);\n}\n\n// -- Gemini judge --\nlet sharedGemini: any;\nasync function getGemini(opts: RouterOptions): Promise<any> {\n if (opts.client) return opts.client;\n if (sharedGemini) return sharedGemini;\n const mod = await import(\"@google/genai\").catch(() => {\n throw new Error(\"Gemini judge needs @google/genai (npm i @google/genai).\");\n });\n const GoogleGenAI = (mod as any).GoogleGenAI;\n sharedGemini = new GoogleGenAI({\n apiKey: opts.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY,\n });\n return sharedGemini;\n}\n\n/** Grade complexity with Gemini (JSON structured output). May throw. */\nexport async function judgeComplexityGemini(prompt: string, opts: RouterOptions = {}): Promise<Complexity> {\n const client = await getGemini(opts);\n const res = await client.models.generateContent({\n model: opts.judgeModel ?? GEMINI_CHEAP_MODEL,\n contents: prompt,\n config: {\n systemInstruction: JUDGE_SYSTEM,\n responseMimeType: \"application/json\",\n responseSchema: {\n type: \"object\",\n properties: { complexity: { type: \"string\", enum: [\"SIMPLE\", \"COMPLEX\"] } },\n required: [\"complexity\"],\n },\n },\n });\n return parseComplexity(res.text ?? \"\");\n}\n\n// ── 3. Classification + routing ─────────────────────────────────────────────\n\ntype Judge = (prompt: string, opts: RouterOptions) => Promise<Complexity>;\n\nasync function classifyWith(prompt: string, judge: Judge, opts: RouterOptions): Promise<Complexity> {\n return fastHeuristic(prompt) ?? judge(prompt, opts);\n}\n\n/** Heuristics first, Claude judge second. May throw. */\nexport function classifyComplexity(prompt: string, opts: RouterOptions = {}): Promise<Complexity> {\n return classifyWith(prompt, judgeComplexity, opts);\n}\n\n/** Heuristics first, Gemini judge second. May throw. */\nexport function classifyComplexityGemini(prompt: string, opts: RouterOptions = {}): Promise<Complexity> {\n return classifyWith(prompt, judgeComplexityGemini, opts);\n}\n\ninterface RouteConfig {\n cheapModel: string;\n expensiveModel: string;\n judge: Judge;\n}\n\nasync function routeWith(prompt: string, cfg: RouteConfig, opts: RouterOptions): Promise<string> {\n let model = cfg.expensiveModel;\n try {\n const complexity = await classifyWith(prompt, cfg.judge, opts);\n model = complexity === \"SIMPLE\" ? cfg.cheapModel : cfg.expensiveModel;\n } catch (err) {\n console.warn(\n `[router] classification failed — defaulting to expensive model. Reason: ${(err as Error).message}`,\n );\n model = cfg.expensiveModel;\n }\n record(model, model === cfg.cheapModel);\n return model;\n}\n\n/** Pick a Claude model for a prompt. Never throws (falls back to expensive). */\nexport function selectModel(prompt: string, opts: RouterOptions = {}): Promise<string> {\n return routeWith(prompt, { cheapModel: CHEAP_MODEL, expensiveModel: EXPENSIVE_MODEL, judge: judgeComplexity }, opts);\n}\n\n/** Pick a Gemini model for a prompt. Never throws (falls back to expensive). */\nexport function selectGeminiModel(prompt: string, opts: RouterOptions = {}): Promise<string> {\n return routeWith(\n prompt,\n { cheapModel: GEMINI_CHEAP_MODEL, expensiveModel: GEMINI_EXPENSIVE_MODEL, judge: judgeComplexityGemini },\n opts,\n );\n}\n\n// ── 4. Observability ─────────────────────────────────────────────────────────\n\nlet cheapCount = 0;\nlet expensiveCount = 0;\n\nfunction record(model: string, isCheap: boolean): void {\n if (isCheap) cheapCount++;\n else expensiveCount++;\n const total = cheapCount + expensiveCount;\n const pct = (n: number) => ((n / total) * 100).toFixed(0);\n console.log(\n `[router] → ${isCheap ? \"CHEAP\" : \"EXPENSIVE\"} (${model}) | cheap ${pct(cheapCount)}% / expensive ${pct(expensiveCount)}% (n=${total})`,\n );\n}\n\nexport interface RoutingStats {\n cheap: number;\n expensive: number;\n total: number;\n cheapPct: number;\n expensivePct: number;\n}\n\nexport function getRoutingStats(): RoutingStats {\n const total = cheapCount + expensiveCount;\n return {\n cheap: cheapCount,\n expensive: expensiveCount,\n total,\n cheapPct: total ? (cheapCount / total) * 100 : 0,\n expensivePct: total ? (expensiveCount / total) * 100 : 0,\n };\n}\n\n// ── Shared message util ──────────────────────────────────────────────────────\n\n/** Extract the most recent user turn's text — what the router classifies on. */\nexport function latestUserText(messages: Message[]): string {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m.role !== \"user\") continue;\n const text = m.content\n .filter((b): b is Extract<typeof b, { type: \"text\" }> => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\")\n .trim();\n if (text) return text;\n }\n return \"\";\n}\n","import { latestUserText, selectModel } from \"../router.ts\";\nimport type {\n ContentBlock,\n Message,\n ModelProvider,\n ModelRequest,\n ModelResponse,\n} from \"../types.ts\";\n\nexport interface AnthropicOptions {\n model?: string;\n apiKey?: string;\n maxTokens?: number;\n /**\n * When true, the model is chosen per-request by the cost router (cheap vs\n * expensive) based on query complexity, instead of using a fixed `model`.\n */\n route?: boolean;\n}\n\n/**\n * Real provider backed by the Anthropic Messages API. Requires the optional\n * `@anthropic-ai/sdk` dependency and an ANTHROPIC_API_KEY. The SDK is imported\n * lazily so the package (and the mock demo) work without it installed.\n */\nexport class AnthropicProvider implements ModelProvider {\n readonly name = \"anthropic\";\n private client: any;\n private model: string;\n private maxTokens: number;\n private apiKey?: string;\n private route: boolean;\n\n constructor(opts: AnthropicOptions = {}) {\n this.model = opts.model ?? \"claude-sonnet-4-6\";\n this.maxTokens = opts.maxTokens ?? 2048;\n this.apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;\n this.route = opts.route ?? false;\n }\n\n private async ensureClient(): Promise<void> {\n if (this.client) return;\n const mod = await import(\"@anthropic-ai/sdk\").catch(() => {\n throw new Error(\n \"AnthropicProvider needs the @anthropic-ai/sdk package: run `npm i @anthropic-ai/sdk`.\",\n );\n });\n const Anthropic = (mod as any).default ?? (mod as any).Anthropic;\n this.client = new Anthropic({ apiKey: this.apiKey });\n }\n\n async generate(req: ModelRequest): Promise<ModelResponse> {\n await this.ensureClient();\n\n // Cost routing: pick cheap vs expensive per request from the latest user\n // turn. selectModel never throws — it falls back to the expensive model.\n const model = this.route\n ? await selectModel(latestUserText(req.messages), {\n client: this.client,\n apiKey: this.apiKey,\n })\n : this.model;\n\n const res = await this.client.messages.create({\n model,\n max_tokens: this.maxTokens,\n system: req.system,\n tools: req.tools.map((t) => ({\n name: t.name,\n description: t.description,\n input_schema: t.inputSchema,\n })),\n messages: req.messages.map(toApiMessage),\n });\n\n const content: ContentBlock[] = res.content.map((b: any): ContentBlock => {\n if (b.type === \"tool_use\") return { type: \"tool_use\", id: b.id, name: b.name, input: b.input };\n return { type: \"text\", text: b.type === \"text\" ? b.text : \"\" };\n });\n const stopReason = res.stop_reason === \"tool_use\" ? \"tool_use\" : \"end_turn\";\n return { content, stopReason };\n }\n}\n\nfunction toApiMessage(m: Message): any {\n return {\n role: m.role,\n content: m.content.map((b) => {\n if (b.type === \"text\") return { type: \"text\", text: b.text };\n if (b.type === \"tool_use\") return { type: \"tool_use\", id: b.id, name: b.name, input: b.input };\n return { type: \"tool_result\", tool_use_id: b.toolUseId, content: b.content, is_error: b.isError };\n }),\n };\n}\n","import { latestUserText, selectGeminiModel } from \"../router.ts\";\nimport type {\n ContentBlock,\n Message,\n ModelProvider,\n ModelRequest,\n ModelResponse,\n} from \"../types.ts\";\n\nexport interface GeminiOptions {\n model?: string;\n apiKey?: string;\n /**\n * When true, the model is chosen per-request by the cost router (cheap vs\n * expensive Gemini) based on query complexity, instead of a fixed `model`.\n */\n route?: boolean;\n}\n\n/**\n * Provider backed by the Google Gemini API (@google/genai). Requires the\n * optional `@google/genai` dependency and a GOOGLE_API_KEY (or GEMINI_API_KEY).\n * The SDK is imported lazily so the package works without it installed.\n */\nexport class GeminiProvider implements ModelProvider {\n readonly name = \"gemini\";\n private client: any;\n private model: string;\n private apiKey?: string;\n private route: boolean;\n\n constructor(opts: GeminiOptions = {}) {\n this.model = opts.model ?? \"gemini-2.5-flash\";\n this.apiKey = opts.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;\n this.route = opts.route ?? false;\n }\n\n private async ensureClient(): Promise<void> {\n if (this.client) return;\n const mod = await import(\"@google/genai\").catch(() => {\n throw new Error(\"GeminiProvider needs the @google/genai package: run `npm i @google/genai`.\");\n });\n const GoogleGenAI = (mod as any).GoogleGenAI;\n this.client = new GoogleGenAI({ apiKey: this.apiKey });\n }\n\n async generate(req: ModelRequest): Promise<ModelResponse> {\n await this.ensureClient();\n\n const model = this.route\n ? await selectGeminiModel(latestUserText(req.messages), {\n client: this.client,\n apiKey: this.apiKey,\n })\n : this.model;\n\n const idToName = toolUseNames(req.messages);\n\n const config: any = {};\n if (req.system) config.systemInstruction = req.system;\n if (req.tools.length) {\n config.tools = [\n {\n functionDeclarations: req.tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.inputSchema,\n })),\n },\n ];\n }\n\n const res = await this.client.models.generateContent({\n model,\n contents: req.messages.map((m) => toGeminiContent(m, idToName)),\n config,\n });\n\n const content: ContentBlock[] = [];\n const text: string | undefined = res.text;\n if (text && text.trim()) content.push({ type: \"text\", text });\n\n const calls: any[] = res.functionCalls ?? [];\n for (const fc of calls) {\n content.push({ type: \"tool_use\", id: fc.id ?? \"\", name: fc.name, input: fc.args ?? {} });\n }\n if (content.length === 0) content.push({ type: \"text\", text: \"\" });\n\n return { content, stopReason: calls.length ? \"tool_use\" : \"end_turn\" };\n }\n}\n\n/** Map tool_use id -> tool name (Gemini matches function responses by name). */\nfunction toolUseNames(messages: Message[]): Map<string, string> {\n const map = new Map<string, string>();\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === \"tool_use\") map.set(b.id, b.name);\n }\n }\n return map;\n}\n\n/** Translate one of our Messages into a Gemini `Content` (role + parts). */\nfunction toGeminiContent(m: Message, idToName: Map<string, string>): any {\n const role = m.role === \"assistant\" ? \"model\" : \"user\";\n const parts = m.content.map((b) => {\n if (b.type === \"text\") return { text: b.text };\n if (b.type === \"tool_use\") return { functionCall: { id: b.id, name: b.name, args: b.input } };\n // tool_result -> functionResponse\n return {\n functionResponse: {\n id: b.toolUseId,\n name: idToName.get(b.toolUseId) ?? b.toolUseId,\n response: b.isError ? { error: b.content } : { result: b.content },\n },\n };\n });\n return { role, parts };\n}\n","// Public API for Torus.\n\nexport * from \"./types.ts\";\nexport { tool, createSdkMcpServer, ToolRegistry, type RegisteredTool } from \"./tools.ts\";\nexport { PermissionEngine, matchesAllow, type PermissionConfig } from \"./permissions.ts\";\nexport { runLoop, type LoopOptions, type LoopResult } from \"./loop.ts\";\nexport { runPipeline, type PipelineOptions } from \"./pipeline.ts\";\nexport { builtinTools, readFileTool, writeFileTool, listDirTool } from \"./builtins.ts\";\nexport { loadStages, parseContract, type StageContract, type StageInput } from \"./subagents.ts\";\nexport { loadStageContext, type LoadedContext } from \"./context.ts\";\nexport { MockProvider, type MockOptions } from \"./providers/mock.ts\";\nexport { AnthropicProvider, type AnthropicOptions } from \"./providers/anthropic.ts\";\nexport { GeminiProvider, type GeminiOptions } from \"./providers/gemini.ts\";\nexport {\n // Anthropic (Claude) routing\n CHEAP_MODEL,\n EXPENSIVE_MODEL,\n selectModel,\n classifyComplexity,\n judgeComplexity,\n // Gemini routing — same mechanism, Gemini models\n GEMINI_CHEAP_MODEL,\n GEMINI_EXPENSIVE_MODEL,\n selectGeminiModel,\n classifyComplexityGemini,\n judgeComplexityGemini,\n // Shared\n fastHeuristic,\n getRoutingStats,\n latestUserText,\n type Complexity,\n type RouterOptions,\n type RoutingStats,\n} from \"./router.ts\";\n\nimport { builtinTools } from \"./builtins.ts\";\nimport { runLoop } from \"./loop.ts\";\nimport { PermissionEngine, type PermissionConfig } from \"./permissions.ts\";\nimport { ToolRegistry } from \"./tools.ts\";\nimport type { AgentEvent, Message, ModelProvider, SdkMcpServer } from \"./types.ts\";\n\nexport interface QueryOptions {\n provider: ModelProvider;\n system?: string;\n mcpServers?: SdkMcpServer[];\n includeBuiltins?: boolean; // default true\n permissions?: PermissionConfig;\n workspaceDir?: string;\n maxTurns?: number;\n}\n\n/**\n * Single-shot agent run (no pipeline). Mirrors the Claude Agent SDK's streaming\n * `query()`: yields events as they happen and a final `result` event.\n *\n * for await (const ev of query(\"Summarize X\", { provider, mcpServers: [srv] })) { ... }\n */\nexport async function* query(\n prompt: string,\n options: QueryOptions,\n): AsyncGenerator<AgentEvent> {\n const registry = new ToolRegistry();\n if (options.includeBuiltins ?? true) registry.addBuiltins(builtinTools);\n for (const s of options.mcpServers ?? []) registry.addServer(s);\n\n const messages: Message[] = [{ role: \"user\", content: [{ type: \"text\", text: prompt }] }];\n const result = yield* runLoop({\n provider: options.provider,\n registry,\n permissions: new PermissionEngine(options.permissions ?? {}),\n system: options.system ?? \"You are a helpful agent.\",\n messages,\n toolContext: { workspaceDir: options.workspaceDir ?? process.cwd() },\n maxTurns: options.maxTurns,\n });\n yield { type: \"result\", finalText: result.finalText, turns: result.turns };\n}\n"],"mappings":"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","names":["mkdir","writeFile","join","readFile","readFile","readFile","readdir","join","join","mkdir","writeFile","estimateTokens"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "torus-ai",
3
+ "version": "0.1.0",
4
+ "description": "Torus — a minimal, ICM-structured Agent SDK: agent loop, tools, in-process MCP, markdown-contract subagents, layered context, permissions, and cost routing across Claude and Gemini.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "aenfr",
8
+ "repository": { "type": "git", "url": "git+https://github.com/aenfr/torus-ai.git" },
9
+ "homepage": "https://github.com/aenfr/torus-ai#readme",
10
+ "bugs": { "url": "https://github.com/aenfr/torus-ai/issues" },
11
+ "keywords": ["agent", "sdk", "llm", "mcp", "anthropic", "claude", "gemini", "google", "icm", "pipeline", "router"],
12
+ "engines": { "node": ">=22.6" },
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": ["dist", "src", "AGENT.md", "CONTEXT.md", "LICENSE", "README.md"],
23
+ "publishConfig": { "access": "public" },
24
+ "scripts": {
25
+ "demo": "node examples/blog-pipeline/run.ts",
26
+ "typecheck": "tsc --noEmit",
27
+ "build": "tsup",
28
+ "prepublishOnly": "npm run typecheck && npm run build"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.2",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.7.2"
34
+ },
35
+ "optionalDependencies": {
36
+ "@anthropic-ai/sdk": "^0.32.1",
37
+ "@google/genai": "^2.8.0"
38
+ }
39
+ }
@@ -0,0 +1,55 @@
1
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { tool } from "./tools.ts";
4
+ import type { ToolDefinition } from "./types.ts";
5
+
6
+ // Confine all built-in file access to the workspace directory (defence in depth;
7
+ // the permission layer is the primary gate).
8
+ function safeResolve(workspaceDir: string, p: string): string {
9
+ const root = resolve(workspaceDir);
10
+ const full = resolve(root, p);
11
+ const rel = relative(root, full);
12
+ if (rel.startsWith("..") || resolve(root, rel) !== full) {
13
+ throw new Error(`Path escapes workspace: ${p}`);
14
+ }
15
+ return full;
16
+ }
17
+
18
+ export const readFileTool: ToolDefinition = tool(
19
+ "read_file",
20
+ "Read a UTF-8 text file relative to the workspace root.",
21
+ { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
22
+ async (input: { path: string }, ctx) => {
23
+ const full = safeResolve(ctx.workspaceDir, input.path);
24
+ return { content: await readFile(full, "utf8") };
25
+ },
26
+ );
27
+
28
+ export const writeFileTool: ToolDefinition = tool(
29
+ "write_file",
30
+ "Write a UTF-8 text file relative to the workspace root (creates parent dirs).",
31
+ {
32
+ type: "object",
33
+ properties: { path: { type: "string" }, content: { type: "string" } },
34
+ required: ["path", "content"],
35
+ },
36
+ async (input: { path: string; content: string }, ctx) => {
37
+ const full = safeResolve(ctx.workspaceDir, input.path);
38
+ await mkdir(dirname(full), { recursive: true });
39
+ await writeFile(full, input.content, "utf8");
40
+ return { content: `Wrote ${input.content.length} chars to ${input.path}` };
41
+ },
42
+ );
43
+
44
+ export const listDirTool: ToolDefinition = tool(
45
+ "list_dir",
46
+ "List entries of a directory relative to the workspace root.",
47
+ { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
48
+ async (input: { path: string }, ctx) => {
49
+ const full = safeResolve(ctx.workspaceDir, input.path);
50
+ const entries = await readdir(full, { withFileTypes: true });
51
+ return { content: entries.map((e) => (e.isDirectory() ? e.name + "/" : e.name)).join("\n") };
52
+ },
53
+ );
54
+
55
+ export const builtinTools: ToolDefinition[] = [readFileTool, writeFileTool, listDirTool];
package/src/context.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { StageContract } from "./subagents.ts";
5
+
6
+ // ICM principle 3 — "layered context loading": assemble the system prompt from
7
+ // exactly the layers a stage names, nothing more. The control point is the
8
+ // contract's Inputs list; this loader never loads "everything just in case".
9
+
10
+ export interface LoadedContext {
11
+ system: string;
12
+ files: string[];
13
+ tokensEstimated: number;
14
+ }
15
+
16
+ const estimateTokens = (s: string) => Math.ceil(s.length / 4); // ~4 chars/token heuristic
17
+
18
+ async function readIfExists(path: string): Promise<string | null> {
19
+ if (!existsSync(path)) return null;
20
+ return readFile(path, "utf8");
21
+ }
22
+
23
+ function relativeName(root: string, path: string): string {
24
+ return path.replace(root, "").replace(/^[\\/]/, "").replace(/\\/g, "/");
25
+ }
26
+
27
+ /**
28
+ * Build a stage's system prompt from the ICM layer hierarchy:
29
+ * Layer 0 AGENT.md (identity + map)
30
+ * Layer 1 CONTEXT.md (routing)
31
+ * Layer 2 stage CONTEXT.md (this stage's contract)
32
+ * Layer 3 scoped references (constraints — only files the contract names)
33
+ * Layer 4 scoped working (prior stage output — only files the contract names)
34
+ */
35
+ export async function loadStageContext(
36
+ workspaceDir: string,
37
+ contract: StageContract,
38
+ ): Promise<LoadedContext> {
39
+ const parts: string[] = [];
40
+ const files: string[] = [];
41
+
42
+ const push = async (label: string, path: string) => {
43
+ const text = await readIfExists(path);
44
+ if (text == null) return;
45
+ const src = relativeName(workspaceDir, path);
46
+ parts.push(`<context layer="${label}" src="${src}">\n${text.trim()}\n</context>`);
47
+ files.push(src);
48
+ };
49
+
50
+ await push("0 identity", join(workspaceDir, "AGENT.md"));
51
+ await push("1 routing", join(workspaceDir, "CONTEXT.md"));
52
+ await push("2 contract", contract.contractPath);
53
+
54
+ for (const input of contract.inputs) {
55
+ const abs = join(contract.stageDir, input.path);
56
+ await push(input.layer === 3 ? "3 reference" : "4 working", abs);
57
+ }
58
+
59
+ const system = parts.join("\n\n");
60
+ return { system, files, tokensEstimated: estimateTokens(system) };
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ // Public API for Torus.
2
+
3
+ export * from "./types.ts";
4
+ export { tool, createSdkMcpServer, ToolRegistry, type RegisteredTool } from "./tools.ts";
5
+ export { PermissionEngine, matchesAllow, type PermissionConfig } from "./permissions.ts";
6
+ export { runLoop, type LoopOptions, type LoopResult } from "./loop.ts";
7
+ export { runPipeline, type PipelineOptions } from "./pipeline.ts";
8
+ export { builtinTools, readFileTool, writeFileTool, listDirTool } from "./builtins.ts";
9
+ export { loadStages, parseContract, type StageContract, type StageInput } from "./subagents.ts";
10
+ export { loadStageContext, type LoadedContext } from "./context.ts";
11
+ export { MockProvider, type MockOptions } from "./providers/mock.ts";
12
+ export { AnthropicProvider, type AnthropicOptions } from "./providers/anthropic.ts";
13
+ export { GeminiProvider, type GeminiOptions } from "./providers/gemini.ts";
14
+ export {
15
+ // Anthropic (Claude) routing
16
+ CHEAP_MODEL,
17
+ EXPENSIVE_MODEL,
18
+ selectModel,
19
+ classifyComplexity,
20
+ judgeComplexity,
21
+ // Gemini routing — same mechanism, Gemini models
22
+ GEMINI_CHEAP_MODEL,
23
+ GEMINI_EXPENSIVE_MODEL,
24
+ selectGeminiModel,
25
+ classifyComplexityGemini,
26
+ judgeComplexityGemini,
27
+ // Shared
28
+ fastHeuristic,
29
+ getRoutingStats,
30
+ latestUserText,
31
+ type Complexity,
32
+ type RouterOptions,
33
+ type RoutingStats,
34
+ } from "./router.ts";
35
+
36
+ import { builtinTools } from "./builtins.ts";
37
+ import { runLoop } from "./loop.ts";
38
+ import { PermissionEngine, type PermissionConfig } from "./permissions.ts";
39
+ import { ToolRegistry } from "./tools.ts";
40
+ import type { AgentEvent, Message, ModelProvider, SdkMcpServer } from "./types.ts";
41
+
42
+ export interface QueryOptions {
43
+ provider: ModelProvider;
44
+ system?: string;
45
+ mcpServers?: SdkMcpServer[];
46
+ includeBuiltins?: boolean; // default true
47
+ permissions?: PermissionConfig;
48
+ workspaceDir?: string;
49
+ maxTurns?: number;
50
+ }
51
+
52
+ /**
53
+ * Single-shot agent run (no pipeline). Mirrors the Claude Agent SDK's streaming
54
+ * `query()`: yields events as they happen and a final `result` event.
55
+ *
56
+ * for await (const ev of query("Summarize X", { provider, mcpServers: [srv] })) { ... }
57
+ */
58
+ export async function* query(
59
+ prompt: string,
60
+ options: QueryOptions,
61
+ ): AsyncGenerator<AgentEvent> {
62
+ const registry = new ToolRegistry();
63
+ if (options.includeBuiltins ?? true) registry.addBuiltins(builtinTools);
64
+ for (const s of options.mcpServers ?? []) registry.addServer(s);
65
+
66
+ const messages: Message[] = [{ role: "user", content: [{ type: "text", text: prompt }] }];
67
+ const result = yield* runLoop({
68
+ provider: options.provider,
69
+ registry,
70
+ permissions: new PermissionEngine(options.permissions ?? {}),
71
+ system: options.system ?? "You are a helpful agent.",
72
+ messages,
73
+ toolContext: { workspaceDir: options.workspaceDir ?? process.cwd() },
74
+ maxTurns: options.maxTurns,
75
+ });
76
+ yield { type: "result", finalText: result.finalText, turns: result.turns };
77
+ }
package/src/loop.ts ADDED
@@ -0,0 +1,106 @@
1
+ import type { PermissionEngine } from "./permissions.ts";
2
+ import type { ToolRegistry } from "./tools.ts";
3
+ import type {
4
+ AgentEvent,
5
+ Message,
6
+ ModelProvider,
7
+ ToolContext,
8
+ ToolResultBlock,
9
+ } from "./types.ts";
10
+
11
+ // The core agentic loop: gather context → call model → if it wants tools, run them
12
+ // under the permission gate and feed results back → repeat until the model stops
13
+ // asking for tools (or we hit maxTurns). This is the same contract the Claude Agent
14
+ // SDK runs; everything else in this package just shapes what enters and leaves it.
15
+
16
+ export interface LoopOptions {
17
+ provider: ModelProvider;
18
+ registry: ToolRegistry;
19
+ permissions: PermissionEngine;
20
+ system: string;
21
+ messages: Message[]; // seeded with the user turn
22
+ toolContext: ToolContext;
23
+ toolFilter?: (fullName: string) => boolean; // which tools to offer this run
24
+ maxTurns?: number;
25
+ stage?: string;
26
+ }
27
+
28
+ export interface LoopResult {
29
+ finalText: string;
30
+ turns: number;
31
+ messages: Message[];
32
+ }
33
+
34
+ let counter = 0;
35
+ const genId = () => `tu_${++counter}`;
36
+
37
+ export async function* runLoop(opts: LoopOptions): AsyncGenerator<AgentEvent, LoopResult> {
38
+ const { provider, registry, permissions, system, messages, toolContext } = opts;
39
+ const maxTurns = opts.maxTurns ?? 8;
40
+ const tools = registry.schemas(opts.toolFilter);
41
+
42
+ let turns = 0;
43
+ let finalText = "";
44
+
45
+ while (turns < maxTurns) {
46
+ turns++;
47
+ const res = await provider.generate({ system, messages, tools });
48
+
49
+ // Ensure every tool_use has an id (mock providers may omit it).
50
+ for (const b of res.content) if (b.type === "tool_use" && !b.id) b.id = genId();
51
+ messages.push({ role: "assistant", content: res.content });
52
+
53
+ for (const b of res.content) {
54
+ if (b.type === "text" && b.text.trim()) {
55
+ yield { type: "assistant_text", text: b.text, stage: opts.stage };
56
+ }
57
+ }
58
+
59
+ if (res.stopReason !== "tool_use") {
60
+ finalText = res.content
61
+ .filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
62
+ .map((b) => b.text)
63
+ .join("\n")
64
+ .trim();
65
+ return { finalText, turns, messages };
66
+ }
67
+
68
+ const toolResults: ToolResultBlock[] = [];
69
+ for (const b of res.content) {
70
+ if (b.type !== "tool_use") continue;
71
+ yield { type: "tool_use", name: b.name, input: b.input, stage: opts.stage };
72
+
73
+ const decision = await permissions.check(b.name, b.input);
74
+ if (decision.behavior === "deny") {
75
+ yield { type: "permission_denied", name: b.name, message: decision.message, stage: opts.stage };
76
+ toolResults.push({
77
+ type: "tool_result",
78
+ toolUseId: b.id,
79
+ content: `Permission denied: ${decision.message}`,
80
+ isError: true,
81
+ });
82
+ continue;
83
+ }
84
+
85
+ const input = decision.updatedInput ?? b.input;
86
+ const result = await registry.execute(b.name, input, toolContext);
87
+ yield {
88
+ type: "tool_result",
89
+ name: b.name,
90
+ content: result.content,
91
+ isError: !!result.isError,
92
+ stage: opts.stage,
93
+ };
94
+ toolResults.push({
95
+ type: "tool_result",
96
+ toolUseId: b.id,
97
+ content: result.content,
98
+ isError: result.isError,
99
+ });
100
+ }
101
+
102
+ messages.push({ role: "user", content: toolResults });
103
+ }
104
+
105
+ return { finalText: finalText || "[max turns reached]", turns, messages };
106
+ }
@@ -0,0 +1,51 @@
1
+ import type { CanUseTool, PermissionDecision } from "./types.ts";
2
+
3
+ /** Match a tool name against patterns supporting a trailing "*" wildcard. */
4
+ export function matchesAllow(name: string, patterns: string[]): boolean {
5
+ return patterns.some((p) => {
6
+ if (p === "*") return true;
7
+ if (p.endsWith("*")) return name.startsWith(p.slice(0, -1));
8
+ return p === name;
9
+ });
10
+ }
11
+
12
+ export interface PermissionConfig {
13
+ /** Allowlist (wildcards ok). If omitted, all tools allowed unless canUseTool vetoes. */
14
+ allowedTools?: string[];
15
+ /** Explicit denials, evaluated first. */
16
+ disallowedTools?: string[];
17
+ /** Final custom gate — can allow non-allowlisted tools, veto allowlisted ones, or rewrite input. */
18
+ canUseTool?: CanUseTool;
19
+ }
20
+
21
+ /**
22
+ * Evaluation order (mirrors the Agent SDK):
23
+ * 1. disallowedTools → deny
24
+ * 2. allowedTools → allow (if no canUseTool)
25
+ * 3. canUseTool → final say
26
+ * 4. default → allow when no allowlist, deny when allowlist set and unmatched
27
+ */
28
+ export class PermissionEngine {
29
+ private cfg: PermissionConfig;
30
+ constructor(cfg: PermissionConfig = {}) {
31
+ this.cfg = cfg;
32
+ }
33
+
34
+ async check(name: string, input: Record<string, unknown>): Promise<PermissionDecision> {
35
+ const { allowedTools, disallowedTools, canUseTool } = this.cfg;
36
+
37
+ if (disallowedTools && matchesAllow(name, disallowedTools)) {
38
+ return { behavior: "deny", message: `${name} is in disallowedTools.` };
39
+ }
40
+
41
+ const onAllowlist = allowedTools ? matchesAllow(name, allowedTools) : true;
42
+
43
+ if (canUseTool) return canUseTool(name, input); // callback has the final word
44
+
45
+ if (onAllowlist) return { behavior: "allow" };
46
+ return {
47
+ behavior: "deny",
48
+ message: `${name} is not in allowedTools and no canUseTool callback is set.`,
49
+ };
50
+ }
51
+ }
@@ -0,0 +1,92 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { loadStageContext } from "./context.ts";
4
+ import { runLoop } from "./loop.ts";
5
+ import { PermissionEngine, type PermissionConfig } from "./permissions.ts";
6
+ import { builtinTools } from "./builtins.ts";
7
+ import { loadStages, type StageContract } from "./subagents.ts";
8
+ import { ToolRegistry } from "./tools.ts";
9
+ import type { AgentEvent, Message, ModelProvider, SdkMcpServer } from "./types.ts";
10
+
11
+ // The ICM pipeline runner: walk numbered stages in order, give each stage only the
12
+ // context its contract names, run the agent loop, write the named artifact to the
13
+ // stage's output/ (the handoff point), then pause at a human review gate.
14
+
15
+ export interface PipelineOptions {
16
+ workspaceDir: string;
17
+ provider: ModelProvider;
18
+ mcpServers?: SdkMcpServer[];
19
+ /** Global permission overlay. A stage's own "## Tools" list is the primary allowlist. */
20
+ permissions?: Pick<PermissionConfig, "canUseTool" | "disallowedTools">;
21
+ /** Called after each stage writes output. Return false to halt the pipeline. */
22
+ reviewGate?: (
23
+ stage: StageContract,
24
+ outputs: { artifact: string; path: string; text: string }[],
25
+ ) => Promise<boolean> | boolean;
26
+ maxTurnsPerStage?: number;
27
+ contextBudgetTokens?: number; // ICM target ceiling, default 8000
28
+ }
29
+
30
+ export async function* runPipeline(opts: PipelineOptions): AsyncGenerator<AgentEvent, void> {
31
+ const registry = new ToolRegistry().addBuiltins(builtinTools);
32
+ for (const s of opts.mcpServers ?? []) registry.addServer(s);
33
+
34
+ const budget = opts.contextBudgetTokens ?? 8000;
35
+ const stages = await loadStages(opts.workspaceDir);
36
+
37
+ for (const stage of stages) {
38
+ yield { type: "stage_start", stage: stage.name };
39
+
40
+ // ── Layered context (ICM Layers 0–4, scoped to the contract) ──
41
+ const ctx = await loadStageContext(opts.workspaceDir, stage);
42
+ yield { type: "context_loaded", stage: stage.name, tokensEstimated: ctx.tokensEstimated, files: ctx.files };
43
+ if (ctx.tokensEstimated > budget) {
44
+ yield {
45
+ type: "assistant_text",
46
+ stage: stage.name,
47
+ text: `⚠ context ~${ctx.tokensEstimated} tok exceeds budget ${budget} — trim this stage's Inputs (ICM principle 3).`,
48
+ };
49
+ }
50
+
51
+ // ── The contract's "## Tools" list is the source of truth for availability ──
52
+ const perm = new PermissionEngine({
53
+ allowedTools: stage.tools, // [] ⇒ a pure prose transform, no tools offered
54
+ disallowedTools: opts.permissions?.disallowedTools,
55
+ canUseTool: opts.permissions?.canUseTool,
56
+ });
57
+ const toolFilter = (n: string) =>
58
+ stage.tools.some((p) => (p.endsWith("*") ? n.startsWith(p.slice(0, -1)) : p === n));
59
+
60
+ const userPrompt =
61
+ `Execute this stage.\n\n## Process\n${stage.process}\n\n` +
62
+ `Produce: ${stage.outputs.join(", ") || "a single markdown artifact"}.`;
63
+ const messages: Message[] = [{ role: "user", content: [{ type: "text", text: userPrompt }] }];
64
+
65
+ const result = yield* runLoop({
66
+ provider: opts.provider,
67
+ registry,
68
+ permissions: perm,
69
+ system: ctx.system,
70
+ messages,
71
+ toolFilter,
72
+ toolContext: { workspaceDir: opts.workspaceDir, stageDir: stage.stageDir },
73
+ maxTurns: opts.maxTurnsPerStage,
74
+ stage: stage.name,
75
+ });
76
+
77
+ // ── Persist the deliverable to output/ per the Outputs contract (the handoff) ──
78
+ const outDir = join(stage.stageDir, "output");
79
+ await mkdir(outDir, { recursive: true });
80
+ const primary = stage.outputs[0] ?? "output.md";
81
+ const path = join(outDir, primary);
82
+ await writeFile(path, result.finalText + "\n", "utf8");
83
+ yield { type: "stage_output", stage: stage.name, artifact: primary, path };
84
+ yield { type: "result", stage: stage.name, finalText: result.finalText, turns: result.turns };
85
+
86
+ // ── Review gate: every stage boundary is an edit surface (ICM principle 4) ──
87
+ const proceed = opts.reviewGate
88
+ ? await opts.reviewGate(stage, [{ artifact: primary, path, text: result.finalText }])
89
+ : true;
90
+ if (!proceed) return;
91
+ }
92
+ }