# AGENTS.md Orientation notes for AI coding assistants working in this repo. Read this first. ## What this is `llmorbiter` — a small (v0.1.0, 0BSD) **LLM tool-calling orchestrator with provider abstraction**. Pure ES modules (`"type": "module"`). Only runtime dependency is `dotenv`. No build step, no test runner, no README (this file is the orientation doc). ## Layout ``` src/ index.js # public entry — re-exports everything LLMOrbiter.js # core orchestrator (~364 lines) Tool.js # defineTool() + ToolError + DirectReturnError providers/ OpenRouterProvider.js # streaming OpenRouter SSE provider examples/ weather.js # minimal tool-calling demo tool-error-retry.js # tool-error + LLM self-correction demo (has PASS/FAIL asserts) pdf-pages.js # PDF -> per-page LLM processing (500 lines, largest) tools/weather.js # sample static-data weather tool ``` ## Public API (`src/index.js`) Exports: `LLMOrbiter`, `OpenRouterProvider`, `defineTool`, `ToolError`, `DirectReturnError`. - `defineTool({ name, description, parameters, strict=true, invoke })` -> `{ spec, invoke, name }`. `spec` is the OpenAI-compatible function schema sent to the LLM; `invoke(args, { signal })` is the local implementation. - `ToolError(message)` — throw from a tool to signal a recoverable error back to the LLM. - `DirectReturnError(data)` — throw from a tool to stop the loop immediately; `run()` resolves with `{ directReturn: true, data, ... }`. ### `new LLMOrbiter(provider, opts)` `opts` are lifecycle callbacks: `onLLMCall`, `onUsage`, `onToolCall`, `onToolError`, `onToolResult`, `onStatus`, `onContent`, `onReasoning`. ### `orbiter.run(opts)` `{ prompt, messages, model, tools, maxIterations=10, tool_choice, extraBody, signal }`. Loop: send messages + tool specs -> if `tool_calls` come back, invoke locals and feed results back -> repeat until no more tool calls or `maxIterations`. Returns `{ content, toolResults, usage, rawUsage, iterations, messages, ... }`; the returned `messages` can seed the next `run()` for conversation continuity (a leading `{role:'system'}` entry is the system prompt). `orbiter.abort(reason)` cancels the active run; aborts surface as `DOMException('...', 'AbortError')`. ### `OpenRouterProvider` `new OpenRouterProvider({ apiKey, apiUrl, siteUrl, siteName })`. Method: `chatCompletions({ model, messages, tools, tool_choice, signal, extraBody, onStatus, onContent, onReasoning })` — streams SSE, assembles `{ content, tool_calls, finish_reason, usage, reasoning_details, reasoning }`. `apiUrl` defaults to `https://openrouter.ai/api/v1/chat/completions`. ## Gotchas / notes - Examples need `OPENROUTER_API_KEY` (in `examples/.env` or via `--key`). Default model in the examples: `deepseek/deepseek-v4-flash-0731`. - `.gitignore` ignores `node_modules/`, `examples/.env`, `examples/invoice.pdf`. - `pdf-pages.js` shells out to `pdftotext -layout`, splits pages on `\f`, and appends prompts to `examples/prompts.log`. - `OpenRouterProvider._readStream` currently writes reasoning straight to `process.stdout` (debug-style side effect) rather than only via `onReasoning` — worth knowing if refactoring. ## Style conventions observed - 4-space indent, single quotes, semicolons, `'use strict';` header. - Comments use em dashes (`—`) and `───` section dividers in examples.