diff --git a/.gitignore b/.gitignore index 8a2a435..e1f70c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ examples/.env -examples/invoice.pdf \ No newline at end of file +examples/invoice.pdf +examples/prompts.log diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e163da4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +# 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. diff --git a/src/Tool.js b/src/Tool.js index 31de367..267ed3a 100644 --- a/src/Tool.js +++ b/src/Tool.js @@ -21,7 +21,7 @@ * Create a tool from a definition. * Returns { spec, invoke } where spec is the wire-format object sent to the LLM. */ -export function defineTool({ name, description, parameters, strict = true, invoke }) { +export function defineTool({ name, description, parameters, strict = false, invoke }) { if (!name) throw new Error('Tool must have a name'); if (typeof invoke !== 'function') throw new Error(`Tool "${name}" must have an invoke function`);