orbitx 0.2.1 → 0.2.3

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 (84) 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.map +1 -1
  10. package/dist/core/base-agent.js +7 -3
  11. package/dist/core/base-agent.js.map +1 -1
  12. package/dist/core/deepseek-provider.d.ts +1 -0
  13. package/dist/core/deepseek-provider.d.ts.map +1 -1
  14. package/dist/core/deepseek-provider.js +51 -1
  15. package/dist/core/deepseek-provider.js.map +1 -1
  16. package/dist/core/ollama-provider.d.ts +1 -0
  17. package/dist/core/ollama-provider.d.ts.map +1 -1
  18. package/dist/core/ollama-provider.js +45 -1
  19. package/dist/core/ollama-provider.js.map +1 -1
  20. package/dist/core/openai-provider.d.ts +1 -0
  21. package/dist/core/openai-provider.d.ts.map +1 -1
  22. package/dist/core/openai-provider.js +54 -1
  23. package/dist/core/openai-provider.js.map +1 -1
  24. package/dist/core/think-effort.d.ts +21 -0
  25. package/dist/core/think-effort.d.ts.map +1 -0
  26. package/dist/core/think-effort.js +19 -0
  27. package/dist/core/think-effort.js.map +1 -0
  28. package/dist/core/worker-agent.d.ts +43 -0
  29. package/dist/core/worker-agent.d.ts.map +1 -0
  30. package/dist/core/worker-agent.js +40 -0
  31. package/dist/core/worker-agent.js.map +1 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/tests/index.d.ts +1 -0
  37. package/dist/tests/index.d.ts.map +1 -1
  38. package/dist/tests/index.js +1 -0
  39. package/dist/tests/index.js.map +1 -1
  40. package/dist/tests/tools/agent/agent-tools.test.d.ts +2 -0
  41. package/dist/tests/tools/agent/agent-tools.test.d.ts.map +1 -0
  42. package/dist/tests/tools/agent/agent-tools.test.js +156 -0
  43. package/dist/tests/tools/agent/agent-tools.test.js.map +1 -0
  44. package/dist/tools/agent/hire.d.ts +6 -0
  45. package/dist/tools/agent/hire.d.ts.map +1 -0
  46. package/dist/tools/agent/hire.js +32 -0
  47. package/dist/tools/agent/hire.js.map +1 -0
  48. package/dist/tools/agent/index.d.ts +27 -0
  49. package/dist/tools/agent/index.d.ts.map +1 -0
  50. package/dist/tools/agent/index.js +31 -0
  51. package/dist/tools/agent/index.js.map +1 -0
  52. package/dist/tools/agent/interaction.d.ts +25 -0
  53. package/dist/tools/agent/interaction.d.ts.map +1 -0
  54. package/dist/tools/agent/interaction.js +14 -0
  55. package/dist/tools/agent/interaction.js.map +1 -0
  56. package/dist/tools/agent/list.d.ts +5 -0
  57. package/dist/tools/agent/list.d.ts.map +1 -0
  58. package/dist/tools/agent/list.js +16 -0
  59. package/dist/tools/agent/list.js.map +1 -0
  60. package/dist/tools/agent/prompt.d.ts +6 -0
  61. package/dist/tools/agent/prompt.d.ts.map +1 -0
  62. package/dist/tools/agent/prompt.js +73 -0
  63. package/dist/tools/agent/prompt.js.map +1 -0
  64. package/dist/tools/agent/registry.d.ts +33 -0
  65. package/dist/tools/agent/registry.d.ts.map +1 -0
  66. package/dist/tools/agent/registry.js +68 -0
  67. package/dist/tools/agent/registry.js.map +1 -0
  68. package/dist/tools/agent/report.d.ts +12 -0
  69. package/dist/tools/agent/report.d.ts.map +1 -0
  70. package/dist/tools/agent/report.js +31 -0
  71. package/dist/tools/agent/report.js.map +1 -0
  72. package/dist/tools/fs/list-dir.d.ts.map +1 -1
  73. package/dist/tools/fs/list-dir.js +0 -1
  74. package/dist/tools/fs/list-dir.js.map +1 -1
  75. package/docs/agents.md +163 -0
  76. package/docs/api-reference.md +54 -0
  77. package/docs/getting-started.md +78 -0
  78. package/docs/index.md +23 -0
  79. package/docs/mcp-architecture.md +103 -0
  80. package/docs/providers.md +148 -0
  81. package/docs/skills.md +68 -0
  82. package/docs/streaming.md +116 -0
  83. package/docs/tools.md +174 -0
  84. package/package.json +4 -3
@@ -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,148 @@
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
+ contextWindow?: number; // default 1_000_000 (deepseek-v4-flash / deepseek-v4-pro)
83
+ thinkEffort?: number; // 0-1, see "Think effort" above
84
+ });
85
+ ```
86
+
87
+ Uses the `openai` SDK pointed at `https://api.deepseek.com` (DeepSeek's API is OpenAI-compatible). Always reports `supportsImages: false`.
88
+
89
+ ### Anthropic
90
+
91
+ ```ts
92
+ import { AnthropicProvider } from "orbitx";
93
+
94
+ new AnthropicProvider(apiKey: string, model = "claude-sonnet-5", options?: {
95
+ supportsTools?: boolean; // default true
96
+ supportsImages?: boolean; // default true
97
+ contextWindow?: number; // default from a per-model table, else 200_000
98
+ maxTokens?: number; // output token cap, default 4096
99
+ thinkEffort?: number; // 0-1, see "Think effort" above
100
+ });
101
+ ```
102
+
103
+ Wraps `@anthropic-ai/sdk`. `maxTokens` is the only provider with an explicit output-length cap in its options today.
104
+
105
+ ### OpenAI
106
+
107
+ ```ts
108
+ import { OpenAIProvider } from "orbitx";
109
+
110
+ new OpenAIProvider(apiKey: string, model = "gpt-5", options?: {
111
+ supportsTools?: boolean; // default true
112
+ supportsImages?: boolean; // default true unless model is in a small denylist (e.g. o3-mini, o1-mini)
113
+ contextWindow?: number; // default from a per-model table, else 128_000
114
+ baseURL?: string; // point at an OpenAI-compatible endpoint
115
+ thinkEffort?: number; // 0-1, see "Think effort" above — ignored on non-reasoning models
116
+ });
117
+ ```
118
+
119
+ Uses the Chat Completions API (not the Responses API). `baseURL` makes this usable against any OpenAI-compatible provider, not just OpenAI itself.
120
+
121
+ ## Two provider roles: main + image
122
+
123
+ An agent has exactly two provider "roles":
124
+
125
+ - **`main`** — drives the agent loop itself (required).
126
+ - **`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.
127
+
128
+ 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:
129
+
130
+ ```ts
131
+ import { BaseAgent, DeepSeekProvider, OllamaProvider } from "orbitx";
132
+
133
+ const agent = new BaseAgent({
134
+ aiProvider: [
135
+ { type: "main", aiProvider: new DeepSeekProvider("api-key", "deepseek-v4-flash") },
136
+ { type: "image", aiProvider: new OllamaProvider("llava:latest") },
137
+ ],
138
+ instruction: "You are a helpful assistant.",
139
+ mcpClient,
140
+ allowedTools: [/* ... */],
141
+ });
142
+ ```
143
+
144
+ 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.
145
+
146
+ ## Writing a custom provider
147
+
148
+ 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.
package/docs/tools.md ADDED
@@ -0,0 +1,174 @@
1
+ # Tools
2
+
3
+ ## The `MCPTool` shape
4
+
5
+ Every tool — built-in or custom — is an `MCPTool` instance (`src/core/mcp.ts`):
6
+
7
+ ```ts
8
+ new MCPTool({
9
+ name: string;
10
+ description: string;
11
+ inputs: {
12
+ name: string;
13
+ type: "number" | "string" | "boolean" | "object" | "array";
14
+ description: string;
15
+ required?: boolean;
16
+ default?: any;
17
+ }[];
18
+ stopIterationAfterUsingThisTool?: boolean; // default false
19
+ customClass?: MCPCustomClass; // optional stateful helper, see below
20
+ execute: (
21
+ envID: string,
22
+ inputs: Record<string, any>,
23
+ mcp?: MCP,
24
+ customClass?: typeof customClass
25
+ ) => Promise<any>;
26
+ });
27
+ ```
28
+
29
+ - `inputs` doubles as both the JSON-schema-like description sent to the model (translated per-provider by `src/core/tool-schema-translator.ts`) and the shape `execute` should expect on `inputs`.
30
+ - `stopIterationAfterUsingThisTool: true` ends the agent's run loop immediately after this tool fires, even if the model didn't naturally stop calling tools — useful for a "final answer" or "hand off" style tool.
31
+ - `execute`'s return value is normalized via `normalizeToolOutput()` into either `{ type: "text", output: {...} }` or `{ type: "image", output: { image, mimeType, ... } }`. Return `{ type: "image", output: {...} }` explicitly to hand back an image (it gets routed through the agent's image provider — see [Providers](./providers.md#two-provider-roles-main--image)); anything else is treated as a plain JSON text result.
32
+ - `envID` identifies which "environment" (in-process client id, IPC/WS session) the call came from — most tools ignore it unless they need per-environment isolated state (see `MCPStorage`/`MCPFSStorage` in [MCP Architecture](./mcp-architecture.md)).
33
+ - `customClass` (a subclass of `MCPCustomClass`) is a way to give a tool its own persistent helper object with access to the owning `MCP` instance (`getMCP()`) and an `EventEmitter` (`getEvents()`) for progress/status events — see how the built-in `*Interaction` classes are used (e.g. `FsInteraction`, `BrowserInteraction`) if you want to expose live progress to a UI. **Experimental:** `customClass` is still settling — its name and shape may change (or it may be removed) in a future release, so avoid depending on it for anything beyond the built-in `*Interaction` pattern until it stabilizes.
34
+
35
+ Tools are registered on an `MCPServer` and invoked through an `MCPClient`; `SimpleAgent` does this wiring for you from a flat `tools` array.
36
+
37
+ ## Built-in tool catalog
38
+
39
+ Import a whole domain's tools at once (e.g. `FsTools()`), or import individual tools by name. Every domain also exports a `*Interaction` `EventEmitter` for subscribing to that domain's live progress events (e.g. `FsInteraction`, `BashInteraction`).
40
+
41
+ ### Filesystem — `FsTools()`
42
+
43
+ > ⚠️ `FsWriteFileTool` and `FsDeleteTool` can permanently overwrite, delete, or corrupt existing files — no undo. Prefer routing filesystem access through `MCPComputer`'s sandbox (see [MCP Architecture](./mcp-architecture.md#mcpcomputer-sandboxed-execution)) to protect the host system.
44
+
45
+ | Tool | Purpose |
46
+ |---|---|
47
+ | `FsReadFileTool` | Read a text file, paging through large files by line. |
48
+ | `FsWriteFileTool` | Write/overwrite a file's full content (creates parent dirs as needed). ⚠️ No undo. |
49
+ | `FsEditFileTool` | Replace a specific line range in an existing file without rewriting the whole thing. |
50
+ | `FsListDirTool` | List a directory's entries, paged by index for large directories. |
51
+ | `FsCreateDirTool` | Create a directory, including missing parents. |
52
+ | `FsDeleteTool` | Permanently delete a file or directory (recursive optional). ⚠️ No undo. |
53
+ | `FsMoveTool` | Move or rename a file or directory. |
54
+ | `FsStatTool` | Check whether a path exists and get its metadata (type, size, modified time). |
55
+
56
+ ### Bash / processes — `BashTools()`
57
+
58
+ > ⚠️ Shell commands can permanently delete files, corrupt data, modify system settings, or execute harmful operations — no undo. Same sandboxing recommendation as above.
59
+
60
+ | Tool | Purpose |
61
+ |---|---|
62
+ | `BashRunTool` | Launch a shell command as a background process; returns immediately if it's still running after `waitMs` (e.g. a dev server). |
63
+ | `BashWaitTool` | Wait again on a previously started process and get its latest status/output. |
64
+ | `BashLogsTool` | Read a process's combined stdout/stderr, paged by line. |
65
+ | `BashListTool` | List every process launched so far with its status. |
66
+ | `BashWriteInputTool` | Send text to a running process's stdin (answer an interactive prompt like `(y/n)`). |
67
+ | `BashTerminateTool` | Terminate a running process. |
68
+
69
+ ### Browser — `BrowserTools()`
70
+
71
+ Drives a real headless browser session.
72
+
73
+ | Tool | Purpose |
74
+ |---|---|
75
+ | `BrowserCreateSessionTool` | Open a new headless browser session at a URL, returns its `sessionId`. |
76
+ | `BrowserRemoveSessionTool` | Close and remove a browser session. |
77
+ | `BrowserGetSessionsTool` | List all currently open session ids. |
78
+ | `BrowserNavigateTool` | Navigate an existing session to a new URL. |
79
+ | `BrowserReadTool` | Read the page as a text outline (headings, tables, links, forms), assigning ref ids to clickable/fillable elements. |
80
+ | `BrowserClickTool` | Click a `[CLICKABLE]` element by ref id. |
81
+ | `BrowserFillTool` | Type into a `[FILLABLE]` element by ref id; can submit via Enter if the field's form has no visible submit button. |
82
+ | `BrowserSubmitFormTool` | Submit a `[FORM]` ref directly — for forms with no visible submit button. |
83
+ | `BrowserScrollInfoTool` | Get total scrollable length and current scroll position. |
84
+ | `BrowserScrollTool` | Scroll to a given vertical pixel position. |
85
+ | `BrowserConsoleTool` | Read captured browser console log messages. |
86
+ | `BrowserNetworkTool` | Read captured network request/response activity. |
87
+ | `BrowserNetworkStatusTool` | Check whether the page currently has in-flight network activity. |
88
+ | `BrowserInjectTool` | Evaluate arbitrary JS in the page and return the result. |
89
+ | `BrowserScreenshotTool` | Capture a screenshot; can be described by the `image` provider instead of passed raw to the main model. |
90
+
91
+ ### Todo lists — `TodoTools()`
92
+
93
+ Backs the `PlannerSkill` (see [Skills](./skills.md)), but usable standalone.
94
+
95
+ | Tool | Purpose |
96
+ |---|---|
97
+ | `TodoCreateListTool` | Create a new todo list, returns its `todoListId`. |
98
+ | `TodoRemoveListTool` | Remove one or more todo lists (and their tasks). |
99
+ | `TodoGetListsTool` | Get all existing todo list ids. |
100
+ | `TodoGetListTool` | Get all tasks belonging to a list. |
101
+ | `TodoCreateTaskTool` | Create one or more tasks inside a list. |
102
+ | `TodoRemoveTaskTool` | Remove one or more tasks by id. |
103
+ | `TodoCheckTaskTool` | Set the checked/unchecked state of one or more tasks. |
104
+
105
+ ### Present / deliverables — `PresentTools()`
106
+
107
+ | Tool | Purpose |
108
+ |---|---|
109
+ | `PresentAddTool` | Present a file to the user by copying it into the present folder (zip folders first — this only accepts single files). |
110
+ | `PresentClearTool` | Clear out every file currently presented. |
111
+ | `PresentGetListTool` | Get the list of files currently presented. |
112
+
113
+ ### Question / answer — `QuestionAnswerTools()`
114
+
115
+ | Tool | Purpose |
116
+ |---|---|
117
+ | `QuestionAnswerTool` | Ask the human operator a clarifying question and wait for their answer. |
118
+
119
+ ### Utility — `UtilTools()`
120
+
121
+ | Tool | Purpose |
122
+ |---|---|
123
+ | `GetCurrentTimeTool` | Get the current date/time (ISO string, unix timestamp, timezone). |
124
+ | `DelayTool` | Wait a given number of milliseconds (max 60000ms) before continuing. |
125
+
126
+ ### Multi-agent — `AgentTools(availableAgents, options?)` *(experimental)*
127
+
128
+ **Experimental** — see the note in [Agents](./agents.md#multi-agent-workeragent-experimental).
129
+
130
+ Unlike every other domain above, this one takes parameters: a fixed roster of `WorkerAgent` instances (see [Agents](./agents.md#multi-agent-workeragent-experimental)) you build ahead of time, and an optional `{ maxHired?: number }` cap on how many can be hired at once. Give the result to your **planner** agent.
131
+
132
+ | Tool | Purpose |
133
+ |---|---|
134
+ | `agent-list` | List every worker in the roster — name, description, rating, and hired status. Also returns `hiredCount` and (when set) `maxHired`. |
135
+ | `agent-hire` | Hire a worker by name, making it eligible for `agent-prompt`. |
136
+ | `agent-prompt` | Send a prompt to a hired worker and return its response once it's done. |
137
+
138
+ `AgentReportTool` is a separate, standalone factory in the same module — put it on each **worker's own** tool list instead (not the planner's), so a worker can hand a result back and end its turn. See [Agents](./agents.md#agent-report--for-workers-not-the-planner) for the full walkthrough.
139
+
140
+ ## Writing a custom tool
141
+
142
+ ```ts
143
+ import { MCPTool } from "orbitx";
144
+
145
+ export const SumTool = () => new MCPTool({
146
+ name: "math-sum",
147
+ description: "get two numbers and sum them",
148
+ inputs: [
149
+ { name: "first", type: "number", description: "first number", required: true },
150
+ { name: "second", type: "number", description: "second number", required: true },
151
+ ],
152
+ execute: async (_envID: string, inputs: Record<string, any>) => {
153
+ const { first, second } = inputs;
154
+ if (typeof first !== "number") throw new Error("first must be a number");
155
+ if (typeof second !== "number") throw new Error("second must be a number");
156
+ return { output: first + second };
157
+ },
158
+ });
159
+ ```
160
+
161
+ Use it exactly like a built-in tool:
162
+
163
+ ```ts
164
+ const agent = new SimpleAgent({
165
+ aiProvider: provider,
166
+ instruction: "You are a helpful assistant.",
167
+ tools: [SumTool()],
168
+ });
169
+ ```
170
+
171
+ Conventions worth following, based on the built-in tools:
172
+ - Export a **factory function** (`SumTool()`), not the `MCPTool` instance itself — this keeps each agent's tool list made of fresh instances.
173
+ - Throw inside `execute` for invalid input rather than returning an error object — thrown errors are caught by `BaseAgent` and turned into a `Error: ...` result the model sees, which is usually what you want.
174
+ - Keep `description` and each input's `description` written for the model, not for a human reader of your source — they go straight into the tool schema.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orbitx",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "An AI SDK providing agents, swarms, planners, skills, MCP tools, workflows, and multi-provider AI integrations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,8 @@
22
22
  }
23
23
  },
24
24
  "files": [
25
- "dist"
25
+ "dist",
26
+ "docs"
26
27
  ],
27
28
  "scripts": {
28
29
  "build": "tsc",
@@ -54,4 +55,4 @@
54
55
  "tsx": "^4.22.4",
55
56
  "typescript": "^6.0.3"
56
57
  }
57
- }
58
+ }