orbitx 0.2.1 → 0.2.4

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.
Files changed (113) hide show
  1. package/Readme.md +26 -0
  2. package/dist/core/ai-provider.d.ts +31 -1
  3. package/dist/core/ai-provider.d.ts.map +1 -1
  4. package/dist/core/ai-provider.js.map +1 -1
  5. package/dist/core/anthropic-provider.d.ts +1 -0
  6. package/dist/core/anthropic-provider.d.ts.map +1 -1
  7. package/dist/core/anthropic-provider.js +83 -3
  8. package/dist/core/anthropic-provider.js.map +1 -1
  9. package/dist/core/base-agent.d.ts +13 -0
  10. package/dist/core/base-agent.d.ts.map +1 -1
  11. package/dist/core/base-agent.js +77 -109
  12. package/dist/core/base-agent.js.map +1 -1
  13. package/dist/core/deepseek-provider.d.ts +2 -0
  14. package/dist/core/deepseek-provider.d.ts.map +1 -1
  15. package/dist/core/deepseek-provider.js +56 -2
  16. package/dist/core/deepseek-provider.js.map +1 -1
  17. package/dist/core/image-describer.d.ts +7 -1
  18. package/dist/core/image-describer.d.ts.map +1 -1
  19. package/dist/core/image-describer.js +5 -1
  20. package/dist/core/image-describer.js.map +1 -1
  21. package/dist/core/ollama-provider.d.ts +1 -0
  22. package/dist/core/ollama-provider.d.ts.map +1 -1
  23. package/dist/core/ollama-provider.js +45 -1
  24. package/dist/core/ollama-provider.js.map +1 -1
  25. package/dist/core/openai-provider.d.ts +1 -0
  26. package/dist/core/openai-provider.d.ts.map +1 -1
  27. package/dist/core/openai-provider.js +54 -1
  28. package/dist/core/openai-provider.js.map +1 -1
  29. package/dist/core/think-effort.d.ts +21 -0
  30. package/dist/core/think-effort.d.ts.map +1 -0
  31. package/dist/core/think-effort.js +19 -0
  32. package/dist/core/think-effort.js.map +1 -0
  33. package/dist/core/worker-agent.d.ts +43 -0
  34. package/dist/core/worker-agent.d.ts.map +1 -0
  35. package/dist/core/worker-agent.js +40 -0
  36. package/dist/core/worker-agent.js.map +1 -0
  37. package/dist/index.d.ts +8 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +4 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/templates/simple.d.ts +2 -0
  42. package/dist/templates/simple.d.ts.map +1 -1
  43. package/dist/templates/simple.js.map +1 -1
  44. package/dist/tests/index.d.ts +1 -0
  45. package/dist/tests/index.d.ts.map +1 -1
  46. package/dist/tests/index.js +1 -0
  47. package/dist/tests/index.js.map +1 -1
  48. package/dist/tests/tools/agent/agent-tools.test.d.ts +2 -0
  49. package/dist/tests/tools/agent/agent-tools.test.d.ts.map +1 -0
  50. package/dist/tests/tools/agent/agent-tools.test.js +156 -0
  51. package/dist/tests/tools/agent/agent-tools.test.js.map +1 -0
  52. package/dist/tools/agent/hire.d.ts +6 -0
  53. package/dist/tools/agent/hire.d.ts.map +1 -0
  54. package/dist/tools/agent/hire.js +32 -0
  55. package/dist/tools/agent/hire.js.map +1 -0
  56. package/dist/tools/agent/index.d.ts +27 -0
  57. package/dist/tools/agent/index.d.ts.map +1 -0
  58. package/dist/tools/agent/index.js +31 -0
  59. package/dist/tools/agent/index.js.map +1 -0
  60. package/dist/tools/agent/interaction.d.ts +25 -0
  61. package/dist/tools/agent/interaction.d.ts.map +1 -0
  62. package/dist/tools/agent/interaction.js +14 -0
  63. package/dist/tools/agent/interaction.js.map +1 -0
  64. package/dist/tools/agent/list.d.ts +5 -0
  65. package/dist/tools/agent/list.d.ts.map +1 -0
  66. package/dist/tools/agent/list.js +16 -0
  67. package/dist/tools/agent/list.js.map +1 -0
  68. package/dist/tools/agent/prompt.d.ts +6 -0
  69. package/dist/tools/agent/prompt.d.ts.map +1 -0
  70. package/dist/tools/agent/prompt.js +73 -0
  71. package/dist/tools/agent/prompt.js.map +1 -0
  72. package/dist/tools/agent/registry.d.ts +33 -0
  73. package/dist/tools/agent/registry.d.ts.map +1 -0
  74. package/dist/tools/agent/registry.js +68 -0
  75. package/dist/tools/agent/registry.js.map +1 -0
  76. package/dist/tools/agent/report.d.ts +12 -0
  77. package/dist/tools/agent/report.d.ts.map +1 -0
  78. package/dist/tools/agent/report.js +31 -0
  79. package/dist/tools/agent/report.js.map +1 -0
  80. package/dist/tools/fs/edit-file.d.ts.map +1 -1
  81. package/dist/tools/fs/edit-file.js +9 -8
  82. package/dist/tools/fs/edit-file.js.map +1 -1
  83. package/dist/tools/fs/list-dir.d.ts.map +1 -1
  84. package/dist/tools/fs/list-dir.js +0 -1
  85. package/dist/tools/fs/list-dir.js.map +1 -1
  86. package/dist/tools/fs/read-file.js +3 -3
  87. package/dist/tools/fs/read-file.js.map +1 -1
  88. package/dist/tools/fs/utils.d.ts +7 -0
  89. package/dist/tools/fs/utils.d.ts.map +1 -1
  90. package/dist/tools/fs/utils.js +9 -2
  91. package/dist/tools/fs/utils.js.map +1 -1
  92. package/dist/tools/utils/index.d.ts +1 -0
  93. package/dist/tools/utils/index.d.ts.map +1 -1
  94. package/dist/tools/utils/index.js +4 -1
  95. package/dist/tools/utils/index.js.map +1 -1
  96. package/dist/tools/utils/read-image.d.ts +12 -0
  97. package/dist/tools/utils/read-image.d.ts.map +1 -0
  98. package/dist/tools/utils/read-image.js +70 -0
  99. package/dist/tools/utils/read-image.js.map +1 -0
  100. package/docs/agents.md +181 -0
  101. package/docs/api-reference.md +54 -0
  102. package/docs/getting-started.md +78 -0
  103. package/docs/index.md +23 -0
  104. package/docs/mcp-architecture.md +103 -0
  105. package/docs/providers.md +151 -0
  106. package/docs/skills.md +68 -0
  107. package/docs/streaming.md +116 -0
  108. package/docs/tools.md +179 -0
  109. package/package.json +4 -3
  110. package/dist/tests/mcp.test.d.ts +0 -2
  111. package/dist/tests/mcp.test.d.ts.map +0 -1
  112. package/dist/tests/mcp.test.js +0 -3
  113. package/dist/tests/mcp.test.js.map +0 -1
@@ -0,0 +1,78 @@
1
+ # Getting Started
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install orbitx
7
+ ```
8
+
9
+ ## Your first agent
10
+
11
+ The fastest path is `SimpleAgent`: give it a provider, an instruction, and a list of tools/skills. It wires up an in-process MCP server/client for you automatically.
12
+
13
+ ```ts
14
+ import { OllamaProvider, SimpleAgent, GetCurrentTimeTool } from "orbitx";
15
+
16
+ const provider = new OllamaProvider("gemma4:e4b");
17
+
18
+ const agent = new SimpleAgent({
19
+ aiProvider: provider,
20
+ instruction: "You are a helpful assistant.",
21
+ tools: [GetCurrentTimeTool()],
22
+ });
23
+
24
+ await agent.run("what is current time?", (chunk) => {
25
+ process.stdout.write(chunk.content);
26
+ });
27
+ ```
28
+
29
+ `agent.run(prompt, streamCallback)` resolves once the model has stopped calling tools and produced a final answer. The `streamCallback` is optional — omit it if you only care about the eventual message history via `getCurrentAgentStates()`.
30
+
31
+ See [Streaming](./streaming.md) for the full chunk shape (roles, tool call/result chunks, `done` flag).
32
+
33
+ ## Choosing a provider
34
+
35
+ ```ts
36
+ import { OllamaProvider, DeepSeekProvider, AnthropicProvider, OpenAIProvider } from "orbitx";
37
+
38
+ new OllamaProvider("gemma4:e4b"); // local, via Ollama
39
+ new DeepSeekProvider("api-key", "deepseek-v4-flash"); // hosted
40
+ new AnthropicProvider("api-key", "claude-sonnet-5"); // hosted
41
+ new OpenAIProvider("api-key", "gpt-5"); // hosted
42
+ ```
43
+
44
+ Every provider implements the same `AIProvider` interface, so switching models is a one-line change to your agent's `aiProvider`. Details, capability flags, and the main/image role split are in [Providers](./providers.md).
45
+
46
+ ## Tools and skills up front
47
+
48
+ `SimpleAgent`'s `tools` and `skills` lists are baked into the system prompt once, at construction time — there's no per-turn tool selection/routing step deciding what the model is allowed to see. If you want the model to be able to call something, put it in `tools` (or bundle it inside a `Skill`). See [Tools](./tools.md) and [Skills](./skills.md).
49
+
50
+ ## Recovering an agent's state
51
+
52
+ `getCurrentAgentStates()` returns a plain JSON-serializable snapshot: message history, running memory summary, and token counters. Persist it anywhere; pass it back in as `initData` to resume the exact same agent later (after a process restart, in a new request handler, etc.):
53
+
54
+ ```ts
55
+ import { writeFileSync, existsSync, readFileSync } from "fs";
56
+
57
+ const initData = existsSync("./state.json")
58
+ ? JSON.parse(readFileSync("./state.json", "utf-8"))
59
+ : undefined;
60
+
61
+ const agent = new SimpleAgent({
62
+ aiProvider: provider,
63
+ instruction: "You are a helpful assistant.",
64
+ tools: [GetCurrentTimeTool()],
65
+ ...(initData ? { initData } : {}),
66
+ });
67
+
68
+ // after any run() call:
69
+ writeFileSync("./state.json", JSON.stringify(agent.getCurrentAgentStates()));
70
+ ```
71
+
72
+ Messages are only ever appended, never rewritten. When the conversation grows past the memory threshold, OrbitX keeps the full history on disk (`messagesFull`) but resets the model's *working* context from a running summary, so an agent can keep going indefinitely without losing the ability to act on what happened earlier. Mechanics are covered in [Agents](./agents.md#memory-compaction).
73
+
74
+ ## Next steps
75
+
76
+ - Full control over the MCP transport (custom server/client, IPC, WebSocket, sandboxed Docker execution): [MCP Architecture](./mcp-architecture.md).
77
+ - Writing your own tool or skill: [Tools](./tools.md#writing-a-custom-tool), [Skills](./skills.md#writing-a-custom-skill).
78
+ - Every public export: [API Reference](./api-reference.md).
package/docs/index.md ADDED
@@ -0,0 +1,23 @@
1
+ # OrbitX Docs
2
+
3
+ OrbitX is a TypeScript SDK for building AI agents: pluggable model providers, a tool/skill system, an MCP-based execution layer (in-process, IPC, or WebSocket), and a resumable agent loop with automatic memory compaction.
4
+
5
+ This directory is written for two audiences at once:
6
+
7
+ - **Humans** learning the library for the first time — read top to bottom.
8
+ - **AI coding agents** working inside a project that depends on `orbitx` — each page is self-contained enough to be read on its own when you only need one topic (e.g. "how do I define a tool").
9
+
10
+ ## Pages
11
+
12
+ 1. [Getting Started](./getting-started.md) — install, first agent, streaming output, resuming state.
13
+ 2. [Providers](./providers.md) — `AIProvider`, the four built-in providers, capabilities, main/image roles.
14
+ 3. [Agents](./agents.md) — `BaseAgent` vs `SimpleAgent`, the run loop, memory compaction, tokens, and multi-agent (`WorkerAgent`/`AgentTools`).
15
+ 4. [Tools](./tools.md) — the `MCPTool` shape, built-in tool catalog, writing your own tools.
16
+ 5. [Skills](./skills.md) — bundling instructions + tools, built-in skill catalog, writing your own.
17
+ 6. [MCP Architecture](./mcp-architecture.md) — `MCPServer`/`MCPClient`/connections, in-process vs IPC vs WS, `MCPComputer` sandbox.
18
+ 7. [Streaming](./streaming.md) — the `StreamCallback` chunk shape and how to consume it.
19
+ 8. [API Reference](./api-reference.md) — flat index of every public export from `orbitx`.
20
+
21
+ ## The shape of the library, in one paragraph
22
+
23
+ You construct one or more `AIProvider` instances (OpenAI, Anthropic, DeepSeek, or Ollama), a list of `MCPTool`s and/or `Skill`s (a skill is just instructions + tools bundled together), and hand them to `SimpleAgent` (or `BaseAgent` for full control over the underlying MCP transport). Calling `agent.run(prompt, streamCallback)` drives a loop: send the conversation + tool schemas to the provider, dispatch any tool calls the model requests, feed results back, repeat until the model stops calling tools. Everything the model can see (tools, skills, instructions) is decided once, up front — there's no dynamic tool routing/selection step. Long conversations are handled by summarizing into a running `memory` string instead of ever deleting messages; the full history is preserved in `getCurrentAgentStates()` for persistence/resume.
@@ -0,0 +1,103 @@
1
+ # MCP Architecture
2
+
3
+ OrbitX's tool execution layer is built around a small client/server protocol (called "MCP" in this codebase — not to be confused with Anthropic's separate Model Context Protocol spec; it's an internal naming choice). It exists to decouple *where tools run* from *where the agent loop runs* — the same `MCPTool` definitions work whether they execute in the same process, in a separate process over a Unix socket / named pipe, over a WebSocket, or inside a sandboxed Docker container.
4
+
5
+ `SimpleAgent` hides all of this behind an in-process connection. Reach for these primitives directly only when you need one of the other transports.
6
+
7
+ ## The three pieces
8
+
9
+ ```
10
+ MCPServer — owns the registered tools, actually executes them
11
+ MCPClient — what BaseAgent calls; sends tool invocations to a server, returns results
12
+ MCPConnection — the transport wiring a client to a server (in-process, IPC, or WS)
13
+ ```
14
+
15
+ ### `MCPServer`
16
+
17
+ ```ts
18
+ new MCPServer(connection: MCPConnection, storage?: MCPStorage, rng?: MCPRNG);
19
+ mcpServer.registerTool(tool: MCPTool<any>): void;
20
+ ```
21
+
22
+ Registers tools and listens on its `connection` for incoming tool-call requests, executing them and sending results back. Defaults to `MCPFSStorage` (filesystem-backed key/value storage — see below) if no `storage` is given.
23
+
24
+ ### `MCPClient`
25
+
26
+ ```ts
27
+ new MCPClient(envID: string, connection: MCPConnection | MCPConnection[], storage?: MCPStorage, rng?: MCPRNG, mcpFilter?: MCPFilter);
28
+ mcpClient.getTools(): Promise<ToolSchema[]>;
29
+ mcpClient.callTool(name: string, inputs: Record<string, any>): Promise<MCPToolOutput>;
30
+ ```
31
+
32
+ This is what you pass into `BaseAgent({ mcpClient, ... })`. `envID` scopes storage/RNG state per logical environment/session — pass a stable id (e.g. a user or conversation id) if you want isolated tool state per agent instance sharing a server. Accepting an array of connections lets one client fan out across multiple servers.
33
+
34
+ `mcpFilter` (an `MCPFilter`) redacts sensitive substrings/patterns from tool output before it reaches the model — see [`MCPFilter`](#mcpfilter-redacting-tool-output) below.
35
+
36
+ #### Registering tools directly on the client (bypassing the connection)
37
+
38
+ `MCPClient` also has its own `registerTool(tool: MCPTool<any>)`, separate from `MCPServer.registerTool()`. A tool registered on the client executes locally, on whatever machine/process the client itself is running in — it never crosses the connection to the remote server at all. `callTool()` checks the client's own tool list first, and only falls back to sending the call across the connection if no local match is found; `getTools()` merges both lists the same way (a client-registered tool shadows a same-named server tool).
39
+
40
+ This matters when you're using `MCPIPCConnection`/`MCPWSConnection`/`MCPComputer` to run most tools inside a separate process or sandbox: routing every single call across that boundary adds latency for tools that don't need the isolation. If a tool is cheap and can't do any harm (e.g. `GetCurrentTimeTool`, a pure computation like the `SumTool` example in [Tools](./tools.md#writing-a-custom-tool)), register it directly on the `MCPClient` instead of the sandboxed `MCPServer` — it still shows up to the model exactly like any other tool, it just answers immediately from the host process instead of round-tripping through IPC/WS/Docker. Keep anything that touches the filesystem, shell, or network on the sandboxed server side, where `MCPComputer`'s isolation actually protects you.
41
+
42
+ ### `MCPConnection` and its variants
43
+
44
+ - **`MCPConnection`** (base/in-process) — tools run in the same process as the agent. What `SimpleAgent` uses internally; you'd construct one directly only when composing `BaseAgent` by hand (see the README's "Building from the Base Agent" example).
45
+ - **`MCPIPCConnection`** — connects client and server across two processes over a named pipe (Windows) or Unix domain socket (`socketPath`), with `mode: "server" | "client"` on each side:
46
+
47
+ ```ts
48
+ import os from "os";
49
+ import { MCPIPCConnection } from "orbitx";
50
+
51
+ const path = os.platform() === "win32" ? `\\\\.\\pipe\\mcp_test` : `/tmp/mcp_test.sock`;
52
+ const ipcConnection = new MCPIPCConnection({ mode: "server", socketPath: path });
53
+ ```
54
+
55
+ - **`MCPWSConnection`** — connects over a WebSocket, optionally with a shared `token` for auth and `tls: { key, cert, ca? }` for WSS:
56
+
57
+ ```ts
58
+ import { MCPWSConnection } from "orbitx";
59
+
60
+ const wsConnection = new MCPWSConnection({ mode: "server", host: "0.0.0.0", port: 9257, token: "1234" });
61
+ ```
62
+
63
+ Client-mode options use `url` instead of `host`/`port`. Both IPC and WS auto-reconnect on disconnect (`reconnectDelay`, default 500ms).
64
+
65
+ ## `MCPComputer` — sandboxed execution *(experimental)*
66
+
67
+ Spins up a Docker container running an MCP server and hands you back a ready-to-use connection — no manual socket/port/token wiring. This is the recommended way to let an agent run filesystem/bash tools without risking the host machine.
68
+
69
+ ```ts
70
+ import { MCPComputer } from "orbitx";
71
+
72
+ const computer = new MCPComputer("/path/to/mount", [3000, 8080]);
73
+ await computer.start();
74
+
75
+ const connection = computer.getConnection();
76
+ // ... build MCPClient/MCPServer/BaseAgent around `connection` as usual ...
77
+ await computer.stop();
78
+ ```
79
+
80
+ - `mountPath` — host directory mounted into the container (this is what filesystem tools inside the sandbox actually touch).
81
+ - `ports` — ports to expose from the container, or `"*"` for host network mode (Linux only, or Windows via WSL2).
82
+ - `getConnection()` — returns a ready `MCPConnection`: IPC on Linux/macOS, WebSocket on Windows (auth token generated automatically).
83
+ - `stop()` — kills the container.
84
+
85
+ Requires the `aliafsordeh/orbitx-sandbox:0.1` image (pull it, or build it yourself from the `Dockerfile.sandbox` at the repo root).
86
+
87
+ ## Storage and RNG
88
+
89
+ - **`MCPStorage`** — abstract `{ get(key): Promise<string>; set(key, value): Promise<void> }`. `MCPFSStorage` is the default filesystem-backed implementation (`new MCPFSStorage(path?)`, defaults to a randomly-named folder under `./data`). `MCPStorage` and `MCPFSStorage` are both exported if you want to implement your own backend (Redis, a database, etc.) or point the default one at a specific path.
90
+ - **`MCPRNG`** — deterministic-ish id generator backed by an `MCPStorage` instance (used internally for things like `generateRefId()`, the helper behind ref ids in `BrowserReadTool`'s output).
91
+
92
+ ## `MCPFilter` — redacting tool output
93
+
94
+ ```ts
95
+ new MCPFilter(values: (string | RegExp)[]);
96
+ mcpFilter.filter(input: any): any; // replaces every match with "FILTERED_OUTPUT"
97
+ ```
98
+
99
+ Pass literal strings (e.g. an API key you never want echoed back to the model) or regexes (e.g. `/sk-[a-zA-Z0-9]{20,}/`) as the `mcpFilter` argument to `MCPClient` to scrub tool output before it's returned — useful when a tool might read a file or environment variable containing a secret.
100
+
101
+ ## Custom `MCP` subclasses
102
+
103
+ Both `MCPServer` and `MCPClient` extend the abstract `MCP` class (`getStorage()`, `getRNG()`). Tool `execute` functions receive the calling `MCP` instance as their third argument, so a custom `MCPCustomClass`-based tool (see [Tools](./tools.md#the-mcptool-shape)) can reach storage/RNG scoped to whichever client dispatched the call.
@@ -0,0 +1,151 @@
1
+ # Providers
2
+
3
+ An `AIProvider` is OrbitX's abstraction over a model API. All four built-in providers implement the same interface (`src/core/ai-provider.ts`):
4
+
5
+ ```ts
6
+ abstract class AIProvider {
7
+ abstract chat(
8
+ messages: Message[],
9
+ streamCallback?: StreamCallback,
10
+ tools?: ToolSchema[]
11
+ ): Promise<ChatResponse>;
12
+
13
+ abstract getCapabilities(): ProviderCapabilities;
14
+ }
15
+ ```
16
+
17
+ - `chat` sends the full message history (plus a provider-agnostic `tools` schema, when supplied) and returns a `ChatResponse` (`{ content, inputTokens, outputTokens, toolCalls? }`). If `streamCallback` is passed, text is streamed to it incrementally as it arrives — see [Streaming](./streaming.md).
18
+ - `getCapabilities()` returns a static description used by `BaseAgent` to decide things like when to trigger memory compaction — it is **not** re-queried per call:
19
+
20
+ ```ts
21
+ interface ProviderCapabilities {
22
+ supportsTools: boolean; // must be true — BaseAgent requires native tool-calling
23
+ supportsImages: boolean; // whether this provider/model accepts image content
24
+ contextWindow: number; // total context window, in tokens
25
+ safeUsageRatio?: number; // fraction of contextWindow safe to fill before compacting (default 0.5)
26
+ }
27
+ ```
28
+
29
+ `BaseAgent.run()` throws immediately if `getCapabilities().supportsTools` is `false` — the legacy JSON-in-text tool-call convention has been removed from the main loop. All providers default `supportsTools` to `true` except Ollama, which defaults it to `false` (many locally-hosted models don't support native function calling; pass `{ supportsTools: true }` explicitly if your model does).
30
+
31
+ ## Think effort
32
+
33
+ Reasoning/thinking models each expose a different native "how hard should the model think" knob — OpenAI and Ollama use `low`/`medium`/`high`, DeepSeek uses `none`/`low`/`high`/`max`, Anthropic uses a numeric thinking-token budget. OrbitX replaces all of that with one universal option on every provider's constructor:
34
+
35
+ ```ts
36
+ thinkEffort?: number; // 0-1, e.g. 0.5 or 0.8 — never a provider-specific string
37
+ ```
38
+
39
+ Each provider maps that 0-1 value onto whatever scale it actually accepts (`src/core/think-effort.ts`'s `resolveThinkEffortLevel()` buckets it evenly across a provider's supported levels; Anthropic instead scales it into a `budget_tokens` range). A provider or model that doesn't support thinking at all — or doesn't recognize the resulting native value — simply ignores it:
40
+
41
+ ```ts
42
+ import { AnthropicProvider, DeepSeekProvider } from "orbitx";
43
+
44
+ const anthropicProvider = new AnthropicProvider("api-key", "claude-sonnet-5", { thinkEffort: 0.7 });
45
+ const deepseekProvider = new DeepSeekProvider("api-key", "deepseek-v4-flash", { thinkEffort: 0.3 });
46
+ ```
47
+
48
+ Per-provider specifics:
49
+
50
+ | Provider | Native scale | Notes |
51
+ |---|---|---|
52
+ | Anthropic | `thinking.budget_tokens` (numeric) | Scales with `maxTokens`; `max_tokens` sent to the API is automatically bumped so it always exceeds the thinking budget. Signed thinking blocks are carried forward automatically via `Message.providerThinking` when a thinking turn also made a tool call — required by Anthropic's API, handled for you. |
53
+ | DeepSeek | `none` / `low` / `high` / `max` | Sent as `reasoning_effort` on reasoner models. |
54
+ | OpenAI | `low` / `medium` / `high` | Sent as `reasoning_effort`, and **only** for reasoning-capable model families (`o1`/`o3`/`o4`/`gpt-5*` prefixes) — silently ignored for every other model, since the Chat Completions API rejects the param on non-reasoning models. |
55
+ | Ollama | `false` / `low` / `medium` / `high` | `thinkEffort <= 0` maps to `false` (works as an on/off switch for models that only understand a boolean, e.g. deepseek-r1/qwen3); higher values map to a level string for models that support graded effort (e.g. gpt-oss). |
56
+
57
+ See [Streaming](./streaming.md#thinking-chunks) for how (and whether) a given provider's reasoning text is observable while it streams.
58
+
59
+ ## Built-in providers
60
+
61
+ ### Ollama
62
+
63
+ ```ts
64
+ import { OllamaProvider } from "orbitx";
65
+
66
+ new OllamaProvider(model: string, host = "http://localhost:11434", options?: {
67
+ supportsTools?: boolean; // default false
68
+ contextWindow?: number; // default 32_000
69
+ thinkEffort?: number; // 0-1, see "Think effort" above
70
+ });
71
+ ```
72
+
73
+ Wraps the `ollama` npm package, talking to a local (or remote) Ollama server. Always reports `supportsImages: true`.
74
+
75
+ ### DeepSeek
76
+
77
+ ```ts
78
+ import { DeepSeekProvider } from "orbitx";
79
+
80
+ new DeepSeekProvider(apiKey: string, model = "deepseek-v4-flash", options?: {
81
+ supportsTools?: boolean; // default true
82
+ supportsImages?: boolean; // default: true only for deepseek-v4-flash-vision-exp
83
+ contextWindow?: number; // default 1_000_000 (deepseek-v4-flash / deepseek-v4-flash-vision-exp / deepseek-v4-pro)
84
+ thinkEffort?: number; // 0-1, see "Think effort" above
85
+ });
86
+ ```
87
+
88
+ Uses the `openai` SDK pointed at `https://api.deepseek.com` (DeepSeek's API is OpenAI-compatible). `getCapabilities().supportsImages` is `false` for every model except `deepseek-v4-flash-vision-exp`, which is `deepseek-v4-flash` plus native image input (same context window, same reasoning support) — pass `supportsImages: true` explicitly if a future model adds vision support before this SDK's allowlist is updated.
89
+
90
+ Token usage (`inputTokens`/`outputTokens`) is only accurate on the non-streaming path (`chat()` called without `streamCallback`), which reads `response.usage` directly. On the streaming path this provider deliberately does not set `stream_options: { include_usage: true }`, so DeepSeek never sends a usage-bearing chunk and both counts report as `0`. This matters most for `ImageDescriber`, which always calls `chat()` without a `streamCallback` (see [Token accounting](./agents.md#token-accounting)) — so image-description usage is captured correctly even though live agent streaming isn't.
91
+
92
+ ### Anthropic
93
+
94
+ ```ts
95
+ import { AnthropicProvider } from "orbitx";
96
+
97
+ new AnthropicProvider(apiKey: string, model = "claude-sonnet-5", options?: {
98
+ supportsTools?: boolean; // default true
99
+ supportsImages?: boolean; // default true
100
+ contextWindow?: number; // default from a per-model table, else 200_000
101
+ maxTokens?: number; // output token cap, default 4096
102
+ thinkEffort?: number; // 0-1, see "Think effort" above
103
+ });
104
+ ```
105
+
106
+ Wraps `@anthropic-ai/sdk`. `maxTokens` is the only provider with an explicit output-length cap in its options today.
107
+
108
+ ### OpenAI
109
+
110
+ ```ts
111
+ import { OpenAIProvider } from "orbitx";
112
+
113
+ new OpenAIProvider(apiKey: string, model = "gpt-5", options?: {
114
+ supportsTools?: boolean; // default true
115
+ supportsImages?: boolean; // default true unless model is in a small denylist (e.g. o3-mini, o1-mini)
116
+ contextWindow?: number; // default from a per-model table, else 128_000
117
+ baseURL?: string; // point at an OpenAI-compatible endpoint
118
+ thinkEffort?: number; // 0-1, see "Think effort" above — ignored on non-reasoning models
119
+ });
120
+ ```
121
+
122
+ Uses the Chat Completions API (not the Responses API). `baseURL` makes this usable against any OpenAI-compatible provider, not just OpenAI itself.
123
+
124
+ ## Two provider roles: main + image
125
+
126
+ An agent has exactly two provider "roles":
127
+
128
+ - **`main`** — drives the agent loop itself (required).
129
+ - **`image`** — describes image tool output (e.g. a screenshot from `BrowserScreenshotTool`) into text, so the main conversation doesn't have to carry raw image bytes. Optional.
130
+
131
+ Pass a single provider to use it for both roles (if it supports images, it's also used as the image describer; otherwise images are dropped with an explanatory error message back to the model). Pass an array to split them:
132
+
133
+ ```ts
134
+ import { BaseAgent, DeepSeekProvider, OllamaProvider } from "orbitx";
135
+
136
+ const agent = new BaseAgent({
137
+ aiProvider: [
138
+ { type: "main", aiProvider: new DeepSeekProvider("api-key", "deepseek-v4-flash") },
139
+ { type: "image", aiProvider: new OllamaProvider("llava:latest") },
140
+ ],
141
+ instruction: "You are a helpful assistant.",
142
+ mcpClient,
143
+ allowedTools: [/* ... */],
144
+ });
145
+ ```
146
+
147
+ This resolution is done by `resolveAgentProviders()` (`AgentProvidersInput = AIProvider | AgentProviderEntry[]`), exported from `orbitx` if you need it directly. There is no third "utils" role — every tool/skill is included in the system prompt from the start, so there's nothing left for a separate pass to decide.
148
+
149
+ ## Writing a custom provider
150
+
151
+ Extend `AIProvider` and implement `chat`/`getCapabilities`. Follow an existing provider (e.g. `src/core/ollama-provider.ts`) as a template: translate `Message[]` to your API's wire format, stream text chunks to `streamCallback` as they arrive, accumulate tool-call deltas and emit them on the final chunk, and translate `ToolSchema[]` into your API's function-calling format (see `src/core/tool-schema-translator.ts` for existing OpenAI/Anthropic translators you can reuse or reference).
package/docs/skills.md ADDED
@@ -0,0 +1,68 @@
1
+ # Skills
2
+
3
+ A `Skill` bundles instructions with the tools those instructions depend on, so including the skill in an agent gets you both at once — the tools are auto-registered, and the instructions are folded into the system prompt under a `SKILLS:` block.
4
+
5
+ ```ts
6
+ class Skill {
7
+ constructor(skill: {
8
+ name: string;
9
+ description: string;
10
+ instructions: string;
11
+ tools: MCPTool<any>[];
12
+ });
13
+ getSkill(): { name; description; instructions; tools };
14
+ }
15
+ ```
16
+
17
+ `description` is shown alongside `instructions` in the system prompt (`${name} - ${description}` followed by the instructions text) — write it as a short "when to use this" pointer, since it's the model's first signal for whether the skill is relevant to the current task.
18
+
19
+ ## Built-in skills
20
+
21
+ | Skill | Purpose |
22
+ |---|---|
23
+ | `PlannerSkill` | Stands up a persistent, checkable todo list (via `TodoTools`) for any large/multi-step/vague job so progress survives long conversations and context resets. |
24
+ | `BigTaskSkill` | Keeps the agent from stopping short on large or ambitious jobs — no declaring victory on a partial slice, no handing a big job back half-finished. |
25
+ | `LongTaskEfficiencySkill` | Keeps a long job efficient — batching, avoiding redundant reads, checkpointing — without giving up on getting through the full scope. |
26
+ | `NodeBackendSkill` | Building, running, and debugging Node.js backends: APIs, servers, CLIs, workers, schedulers, DB access layers. |
27
+ | `ReactFrontendSkill` | Building, running, and debugging modern React (`.tsx`/`.jsx`) projects, typically Vite-scaffolded. |
28
+ | `UiUxDesignSkill` | Visual and interaction design quality for user-facing UI — layout, hierarchy, feedback states, accessibility, responsiveness. |
29
+ | `BackendSecuritySkill` | Hardening server-side code that touches auth, sessions, tokens, uploads, DB queries, payments, or any trust boundary. |
30
+ | `CodeVerificationSkill` | Running the right type checker/build/lint/test suite before handing code back, and telling real bugs apart from sandbox artifacts. |
31
+ | `WebEndToEndTestSkill` | Testing a live web app like a real user would (fill, click, navigate) and verifying via the rendered page, console, and network. |
32
+ | `ResearchSkill` | Answering questions that need real, current information from the live web — cross-checks multiple sources. |
33
+ | `ShoppingSkill` | Purchase-decision help — real current listings, prices, links, and review sentiment instead of recommending from memory. |
34
+ | `PresentSkill` | Deciding how to hand back a job's output as files (individually or zipped) and cleaning build artifacts out first — pairs with `PresentTools`. |
35
+ | `QuestionAnswerSkill` | Pausing to ask the user for missing information, a decision, or confirmation before continuing, instead of guessing — batches every pending question into one call and stops the turn immediately after asking (`QuestionAnswerTool` sets `stopIterationAfterUsingThisTool`). |
36
+ | `QuestionAnswerSkill` | Wraps `QuestionAnswerTool` with instructions for when it's appropriate to stop and ask the human operator a clarifying question. |
37
+
38
+ Each of these lives in its own file under `src/skills/` and is a plain factory function (`PlannerSkill()`), same convention as tools.
39
+
40
+ ## Writing a custom skill
41
+
42
+ ```ts
43
+ import { Skill, FsReadFileTool, FsWriteFileTool } from "orbitx";
44
+ import { SumTool } from "./sum-tool.js";
45
+
46
+ export const MathHelperSkill = () => new Skill({
47
+ name: "math-helper",
48
+ description:
49
+ "Use this skill whenever the user asks for arithmetic that should be computed exactly " +
50
+ "rather than estimated from the model's own reasoning — sums, running totals, or " +
51
+ "anything written to a file afterward.",
52
+ instructions: `
53
+ - Always use the math-sum tool for addition instead of computing it yourself.
54
+ - If asked to save a result, write it to disk with fs-write-file and confirm the path back to the user.
55
+ `,
56
+ tools: [SumTool(), FsReadFileTool(), FsWriteFileTool()],
57
+ });
58
+ ```
59
+
60
+ ```ts
61
+ const agent = new SimpleAgent({
62
+ aiProvider: provider,
63
+ instruction: "You are a helpful assistant.",
64
+ skills: [MathHelperSkill()],
65
+ });
66
+ ```
67
+
68
+ If a tool is already passed in `tools` *and* included inside a skill, `SimpleAgent` de-duplicates it — you won't get it registered twice. If you're building on `BaseAgent` directly instead, remember `allowedTools` and each skill's `tools` are two separate lists that both need to reach the same `MCPServer` registration — `SimpleAgent`'s constructor is the reference implementation for getting this wiring right.
@@ -0,0 +1,116 @@
1
+ # Streaming
2
+
3
+ `agent.run(prompt, streamCallback)` and `AIProvider.chat(messages, streamCallback, tools)` both accept an optional `StreamCallback`:
4
+
5
+ ```ts
6
+ type StreamCallback = (chunk: {
7
+ role: "assistant" | "tool" | "user";
8
+ content: string;
9
+ done: boolean;
10
+ toolCalls?: ToolCallRequest[]; // a single just-finished call, as a 1-element array — see "Incremental tool-call chunks" below
11
+ toolCallId?: string; // on "tool"-role chunks
12
+ toolName?: string; // on "tool"-role chunks
13
+ thinking?: string; // reasoning-text delta, mirroring `content` — see "Thinking chunks" below
14
+ }) => Promise<void> | void;
15
+ ```
16
+
17
+ ## What you actually receive, turn by turn
18
+
19
+ 1. **`role: "user"`** — one chunk, `done: true`, echoing the prompt that was just added to history (either the one you passed to `run()`, or a queued one — see [Agents](./agents.md#the-run-loop)).
20
+ 2. **`role: "assistant"`** — the model's reply, streamed incrementally: zero or more `done: false` chunks each carrying the next slice of `content` as it arrives from the provider, followed by one final `done: true` chunk. `content` is delta text, not the accumulated total — append it yourself if you need the running message.
21
+ - If the model requested tool calls this turn, each one is emitted individually, mid-stream, as soon as *that* call finishes generating — `toolCalls: [thatOneCall]` on a `done: false` chunk. See "Incremental tool-call chunks" below.
22
+ 3. **`role: "tool"`** — one chunk per dispatched tool call, `done: true`, with `content` set to the tool's result text and `toolCallId`/`toolName` identifying which call it answers. Emitted after the tool has actually finished executing (there is no streaming/progress signal for tool execution itself through this callback — see below for how to get that separately).
23
+
24
+ A full run of "call one tool, then answer" therefore looks like:
25
+
26
+ ```
27
+ { role: "user", content: "<prompt>", done: true }
28
+ { role: "assistant", content: "<partial>", done: false } // repeated
29
+ { role: "assistant", content: "", done: false, toolCalls: [{...}] }
30
+ { role: "assistant", content: "", done: true }
31
+ { role: "tool", content: "<result>", done: true, toolCallId, toolName }
32
+ { role: "assistant", content: "<partial>", done: false } // repeated
33
+ { role: "assistant", content: "", done: true }
34
+ ```
35
+
36
+ Note the final `done: true` chunk for the tool-call turn carries no `toolCalls` — every call this turn was already streamed individually by the time it arrives, so resending the full list would just be a duplicate. If you need the complete, aggregated list for a turn (e.g. you're driving `AIProvider.chat()` directly rather than going through `agent.run()`), it's on the returned `ChatResponse.toolCalls` instead — see below.
37
+
38
+ ## Incremental tool-call chunks
39
+
40
+ Deciding *whether* to call a tool and building its arguments is itself something the model streams token by token — so as soon as one call's arguments are fully generated, that call is emitted on its own as `toolCalls: [thatOneCall]`, without waiting for the rest of the turn (more tool calls, or trailing text) to finish too. This is purely observational: it exists so a consumer can show "the model just decided to call `X`" the moment it happens.
41
+
42
+ **It does not change when a tool actually runs.** Dispatch is still all-or-nothing at the end of the turn: `BaseAgent` only executes tool calls after `AIProvider.chat()` fully resolves and returns the complete `ChatResponse.toolCalls` array — nothing is ever called early just because its single-call chunk arrived first. If you want to reconcile the two: the incremental `toolCalls: [...]` chunks tell you a call was *decided*, in the order the model produced them; the eventual `role: "tool"` result chunks tell you a call was *executed*, in whatever order `BaseAgent` dispatches them.
43
+
44
+ Detection differs by provider, since not every API has an explicit "this call is done" signal:
45
+
46
+ | Provider | How completion is detected |
47
+ |---|---|
48
+ | Anthropic | Native `content_block_stop` event — exact and immediate. |
49
+ | OpenAI / DeepSeek | Inferred: the API streams one call's arguments at a time with no end marker, so a call is treated as finished the instant the *next* call's first delta (carrying a fresh `id`) arrives, or the stream ends (for the last/only call). |
50
+ | Ollama | Ollama sends each tool call already fully formed (not built up token by token), so it's emitted the moment it's seen — deduplicated in case the same call reappears in a later chunk. |
51
+
52
+ ## Minimal consumer
53
+
54
+ ```ts
55
+ let oldRole: "assistant" | "tool" | "user" | undefined;
56
+
57
+ agent.run("what is current time?", (chunk) => {
58
+ if (oldRole !== chunk.role) {
59
+ process.stdout.write(`\n${chunk.role}: `);
60
+ oldRole = chunk.role;
61
+ }
62
+
63
+ if (chunk.thinking) {
64
+ process.stdout.write(`\x1b[2m${chunk.thinking}\x1b[0m`); // dim, to visually separate it from the answer
65
+ } else {
66
+ process.stdout.write(chunk.content);
67
+ }
68
+
69
+ if (chunk.toolCalls?.length) {
70
+ console.log("decided to call:", chunk.toolCalls[0]);
71
+ }
72
+ if (chunk.role === "tool") {
73
+ console.log({ toolCallId: chunk.toolCallId, toolName: chunk.toolName });
74
+ }
75
+
76
+ if (chunk.done) {
77
+ process.stdout.write("\n\n");
78
+ oldRole = undefined;
79
+ }
80
+ });
81
+ ```
82
+
83
+ ## Thinking chunks
84
+
85
+ When a provider is constructed with `thinkEffort` (see [Providers](./providers.md#think-effort)) and it can actually surface reasoning text (`getCapabilities().supportsThinking`), that text streams incrementally too — as `role: "assistant"` chunks carrying their delta on `thinking` instead of `content`, interleaved *before* the model's normal answer chunks in the same turn:
86
+
87
+ ```
88
+ { role: "assistant", content: "", done: false, thinking: "<reasoning...>" } // repeated
89
+ { role: "assistant", content: "<answer...>", done: false } // thinking absent from here on
90
+ { role: "assistant", content: "", done: true }
91
+ ```
92
+
93
+ `thinking` mirrors `content`: a chunk carries text on exactly one of the two, never both, and `thinking` is only ever present on non-`done` chunks — the final `done: true` chunk never carries it. Once accumulated, the full reasoning text for the turn is also available non-streaming on `ChatResponse.thinking` / `Message.thinking` (e.g. for logging or persisting alongside the answer).
94
+
95
+ Support varies by provider — set `thinkEffort` and check `getCapabilities().supportsThinking` before assuming thinking chunks will actually arrive:
96
+
97
+ | Provider | Streams `thinking` chunks? |
98
+ |---|---|
99
+ | Anthropic | Yes — native `thinking_delta` events. |
100
+ | DeepSeek | Yes — `delta.reasoning_content` on reasoner models. |
101
+ | Ollama | Yes — `message.thinking` on models that support it. |
102
+ | OpenAI | **No.** `thinkEffort` is still honored server-side (via `reasoning_effort`) on reasoning-capable models, but the Chat Completions API this provider uses never returns the reasoning text itself — there's nothing to stream. |
103
+
104
+ ### `thinking` vs `providerThinking` on `Message`/`ChatResponse`
105
+
106
+ Once a turn finishes, its accumulated reasoning is available two ways on both `ChatResponse` (returned from `AIProvider.chat()`) and `Message` (as stored in an agent's history):
107
+
108
+ - **`thinking?: string`** — the plain reasoning text, for display or logging. Never required by any provider's API; read-only from your side.
109
+ - **`providerThinking?: any[]`** — opaque, provider-specific data (currently only Anthropic populates this: signed `thinking` content blocks). When a turn both thinks and calls a tool, Anthropic's API requires the exact signed thinking block(s) from that turn to be replayed verbatim on the next request or it rejects the call. `BaseAgent` carries this field through automatically when it builds the next turn's message history, so if you're using `BaseAgent`/`SimpleAgent` you never touch it directly. It only matters if you're driving an `AIProvider` yourself (see [Providers](./providers.md)) — in that case, copy `providerThinking` from the response straight onto the `Message` you push for that assistant turn, unchanged.
110
+
111
+ ## What's *not* in the stream today
112
+
113
+ - **No live per-tool progress channel through `StreamCallback`.** For domains that emit their own progress (browser navigation, long-running bash processes), subscribe to that domain's `*Interaction` `EventEmitter` directly instead (e.g. `BrowserInteraction`, `BashInteraction`) — see [Tools](./tools.md).
114
+ - **Tool calls still only appear once, fully assembled** — see above. `thinkEffort`/thinking chunks don't change that.
115
+
116
+ If your use case depends on either of these, check the project's issue tracker / recent changes before assuming they're unavailable — this area of the SDK is actively evolving.