orbitx 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +478 -456
- package/dist/agents/backend.d.ts +4 -0
- package/dist/agents/backend.d.ts.map +1 -0
- package/dist/agents/backend.js +54 -0
- package/dist/agents/backend.js.map +1 -0
- package/dist/agents/frontend.d.ts +4 -0
- package/dist/agents/frontend.d.ts.map +1 -0
- package/dist/agents/frontend.js +50 -0
- package/dist/agents/frontend.js.map +1 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +22 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/planner.d.ts +4 -0
- package/dist/agents/planner.d.ts.map +1 -0
- package/dist/agents/planner.js +86 -0
- package/dist/agents/planner.js.map +1 -0
- package/dist/agents/protocol.d.ts +5 -0
- package/dist/agents/protocol.d.ts.map +1 -0
- package/dist/agents/protocol.js +70 -0
- package/dist/agents/protocol.js.map +1 -0
- package/dist/agents/reasoner.d.ts +5 -0
- package/dist/agents/reasoner.d.ts.map +1 -0
- package/dist/agents/reasoner.js +96 -0
- package/dist/agents/reasoner.js.map +1 -0
- package/dist/agents/research.d.ts +4 -0
- package/dist/agents/research.d.ts.map +1 -0
- package/dist/agents/research.js +28 -0
- package/dist/agents/research.js.map +1 -0
- package/dist/agents/tester.d.ts +4 -0
- package/dist/agents/tester.d.ts.map +1 -0
- package/dist/agents/tester.js +70 -0
- package/dist/agents/tester.js.map +1 -0
- package/dist/core/agent-definition.d.ts +61 -0
- package/dist/core/agent-definition.d.ts.map +1 -0
- package/dist/core/agent-definition.js +82 -0
- package/dist/core/agent-definition.js.map +1 -0
- package/dist/core/ai-provider.d.ts +3 -0
- package/dist/core/ai-provider.d.ts.map +1 -1
- package/dist/core/ai-provider.js.map +1 -1
- package/dist/core/anthropic-provider.d.ts +1 -0
- package/dist/core/anthropic-provider.d.ts.map +1 -1
- package/dist/core/anthropic-provider.js +3 -0
- package/dist/core/anthropic-provider.js.map +1 -1
- package/dist/core/base-agent.d.ts +51 -28
- package/dist/core/base-agent.d.ts.map +1 -1
- package/dist/core/base-agent.js +118 -26
- package/dist/core/base-agent.js.map +1 -1
- package/dist/core/deepseek-provider.d.ts +1 -0
- package/dist/core/deepseek-provider.d.ts.map +1 -1
- package/dist/core/deepseek-provider.js +3 -0
- package/dist/core/deepseek-provider.js.map +1 -1
- package/dist/core/mcp-computer.d.ts +14 -1
- package/dist/core/mcp-computer.d.ts.map +1 -1
- package/dist/core/mcp-computer.js +352 -71
- package/dist/core/mcp-computer.js.map +1 -1
- package/dist/core/ollama-provider.d.ts +1 -0
- package/dist/core/ollama-provider.d.ts.map +1 -1
- package/dist/core/ollama-provider.js +3 -0
- package/dist/core/ollama-provider.js.map +1 -1
- package/dist/core/openai-provider.d.ts +1 -0
- package/dist/core/openai-provider.d.ts.map +1 -1
- package/dist/core/openai-provider.js +3 -0
- package/dist/core/openai-provider.js.map +1 -1
- package/dist/core/swarm-base.d.ts +88 -0
- package/dist/core/swarm-base.d.ts.map +1 -0
- package/dist/core/swarm-base.js +427 -0
- package/dist/core/swarm-base.js.map +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/tests/agents/default-agents.test.d.ts +2 -0
- package/dist/tests/agents/default-agents.test.d.ts.map +1 -0
- package/dist/tests/agents/default-agents.test.js +106 -0
- package/dist/tests/agents/default-agents.test.js.map +1 -0
- package/dist/tests/core/agent-definition.test.d.ts +2 -0
- package/dist/tests/core/agent-definition.test.d.ts.map +1 -0
- package/dist/tests/core/agent-definition.test.js +99 -0
- package/dist/tests/core/agent-definition.test.js.map +1 -0
- package/dist/tests/core/scripted-provider.d.ts +38 -0
- package/dist/tests/core/scripted-provider.d.ts.map +1 -0
- package/dist/tests/core/scripted-provider.js +85 -0
- package/dist/tests/core/scripted-provider.js.map +1 -0
- package/dist/tests/core/swarm-base.test.d.ts +2 -0
- package/dist/tests/core/swarm-base.test.d.ts.map +1 -0
- package/dist/tests/core/swarm-base.test.js +387 -0
- package/dist/tests/core/swarm-base.test.js.map +1 -0
- package/dist/tests/index.d.ts +3 -0
- package/dist/tests/index.d.ts.map +1 -1
- package/dist/tests/index.js +3 -0
- package/dist/tests/index.js.map +1 -1
- package/dist/tests/tools/agent/agent-tools.test.js +225 -153
- package/dist/tests/tools/agent/agent-tools.test.js.map +1 -1
- package/dist/tools/agent/active.d.ts +5 -0
- package/dist/tools/agent/active.d.ts.map +1 -0
- package/dist/tools/agent/active.js +40 -0
- package/dist/tools/agent/active.js.map +1 -0
- package/dist/tools/agent/fire.d.ts +5 -0
- package/dist/tools/agent/fire.d.ts.map +1 -0
- package/dist/tools/agent/fire.js +38 -0
- package/dist/tools/agent/fire.js.map +1 -0
- package/dist/tools/agent/hire.d.ts +2 -2
- package/dist/tools/agent/hire.d.ts.map +1 -1
- package/dist/tools/agent/hire.js +43 -10
- package/dist/tools/agent/hire.js.map +1 -1
- package/dist/tools/agent/index.d.ts +22 -19
- package/dist/tools/agent/index.d.ts.map +1 -1
- package/dist/tools/agent/index.js +29 -21
- package/dist/tools/agent/index.js.map +1 -1
- package/dist/tools/agent/prompt.d.ts +2 -2
- package/dist/tools/agent/prompt.d.ts.map +1 -1
- package/dist/tools/agent/prompt.js +19 -53
- package/dist/tools/agent/prompt.js.map +1 -1
- package/dist/tools/agent/registry.d.ts +74 -24
- package/dist/tools/agent/registry.d.ts.map +1 -1
- package/dist/tools/agent/registry.js +93 -48
- package/dist/tools/agent/registry.js.map +1 -1
- package/dist/tools/agent/report-group.d.ts +5 -0
- package/dist/tools/agent/report-group.d.ts.map +1 -0
- package/dist/tools/agent/report-group.js +50 -0
- package/dist/tools/agent/report-group.js.map +1 -0
- package/dist/tools/agent/report-parent.d.ts +5 -0
- package/dist/tools/agent/report-parent.d.ts.map +1 -0
- package/dist/tools/agent/report-parent.js +36 -0
- package/dist/tools/agent/report-parent.js.map +1 -0
- package/dist/tools/agent/types.d.ts +5 -0
- package/dist/tools/agent/types.d.ts.map +1 -0
- package/dist/tools/agent/types.js +17 -0
- package/dist/tools/agent/types.js.map +1 -0
- package/docs/agents.md +173 -228
- package/docs/api-reference.md +9 -4
- package/docs/getting-started.md +78 -78
- package/docs/index.md +24 -23
- package/docs/mcp-architecture.md +29 -6
- package/docs/providers.md +258 -254
- package/docs/skills.md +0 -1
- package/docs/streaming.md +133 -133
- package/docs/swarm.md +503 -0
- package/docs/tools.md +214 -212
- package/package.json +1 -1
package/docs/agents.md
CHANGED
|
@@ -1,228 +1,173 @@
|
|
|
1
|
-
# Agents
|
|
2
|
-
|
|
3
|
-
## `BaseAgent` vs `SimpleAgent`
|
|
4
|
-
|
|
5
|
-
- **`BaseAgent`** (`src/core/base-agent.ts`) is the real engine — the run loop, memory compaction, tool dispatch, token accounting. It requires you to bring your own `MCPClient` (and therefore your own `MCPServer`/connection), which is what gives you the choice of in-process, IPC, or WebSocket execution (see [MCP Architecture](./mcp-architecture.md)).
|
|
6
|
-
- **`SimpleAgent`** (`src/templates/simple.ts`) is a thin subclass that wires up an in-process `MCPConnection` + `MCPServer` + `MCPClient` for you from a flat `tools`/`skills` list. Use this unless you specifically need a different transport.
|
|
7
|
-
- **`
|
|
8
|
-
|
|
9
|
-
## `BaseAgent` constructor
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
new BaseAgent({
|
|
13
|
-
instruction: string;
|
|
14
|
-
safetyPolicies?: string; // default: "" — see "Guardrail system prompt" below
|
|
15
|
-
allowedTools: MCPTool<any>[];
|
|
16
|
-
aiProvider: AIProvider | AgentProviderEntry[]; // see Providers doc
|
|
17
|
-
mcpClient: MCPClient;
|
|
18
|
-
skills?: Skill[];
|
|
19
|
-
maxMemorizeToken?: number; // default: derived from the main provider's contextWindow * safeUsageRatio
|
|
20
|
-
initData?: {
|
|
21
|
-
compactMemory: string;
|
|
22
|
-
retiredMessages: Message[]; // complete, ever-appended history of retired messages — each Message carries its own usage/timestamp
|
|
23
|
-
messagesCompact: Message[]; // the working context currently replayed to the provider every turn
|
|
24
|
-
};
|
|
25
|
-
features?: {
|
|
26
|
-
executeProviderFromMCPTool?: boolean; // default: false — see "executeProvider feature flag" below
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Guardrail system prompt
|
|
32
|
-
|
|
33
|
-
`safetyPolicies` is a separate `role: "system"` message, sent as its own leading entry ahead of the regular system prompt (see [Instructions, skills, and memory](#instructions-skills-and-memory-in-the-system-prompt)) on every main-provider `chat()` call in the run loop. Keep house rules/guardrails here rather than folding them into `instruction`, since it's a separate, cache-stable prefix ahead of the rest of the system prompt.
|
|
34
|
-
|
|
35
|
-
It is **not** prepended on an `executeProvider` call (see below) — stuffing a policy paragraph ahead of a narrow, single-purpose call (e.g. "describe this image") measurably degrades that call's output, and not every registered provider role even accepts a `system` message the way the main LLM does. A pre-dispatch content-safety check is the intended replacement there; not implemented yet.
|
|
36
|
-
|
|
37
|
-
### `executeProvider` feature flag
|
|
38
|
-
|
|
39
|
-
A tool's `execute()` can reach a registered `AIProvider` directly via `mcp.executeProvider(toolCallId, type, input)` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)) — a bigger trust surface than a tool that only returns data to the model, since it lets a tool spend real provider calls (and, per the point above, without a guardrail system prompt ahead of them). It's off by default: `BaseAgent#executeProvider` throws immediately unless `features.executeProviderFromMCPTool` was set `true` on construction. This gates every role uniformly (`image-describer` included) — enable it once you're ready to trust the tools you've allowed with direct provider access.
|
|
40
|
-
|
|
41
|
-
`SimpleAgent`'s constructor is the same shape minus `mcpClient`/`allowedTools` (it builds those from `tools`), plus `tools?: MCPTool<any>[]` and `maxMemorizeToken` defaulting to `16000` instead of being derived. `safetyPolicies` and `features` pass straight through to the underlying `BaseAgent`.
|
|
42
|
-
|
|
43
|
-
Token accounting no longer lives in `initData` as separate counters — every `Message` carries its own `usage` (see [Token accounting](#token-accounting)), so `retiredMessages`/`messagesCompact` alone are enough to resume with full token history intact.
|
|
44
|
-
|
|
45
|
-
## The run loop
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
await agent.run(prompt: string, streamCallback?: StreamCallback): Promise<boolean>;
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Each call to `run()`:
|
|
52
|
-
|
|
53
|
-
1. Pushes the user prompt onto the message history and emits it as a `role: "user"` stream chunk.
|
|
54
|
-
2. Sends `[system, ...history]` plus the full tool schema list to the main provider's `chat()`.
|
|
55
|
-
3. If the model requested tool calls, dispatches each one (via the `MCPClient` this agent was built with), pushes a `role: "tool"` message + stream chunk per result, and loops back to step 2.
|
|
56
|
-
4. Repeats until the model's turn produces no tool calls (or a tool marked `stopIterationAfterUsingThisTool` fired — see [Tools](./tools.md)).
|
|
57
|
-
|
|
58
|
-
If `run()` is called again while a previous call is still in flight, the new call is queued and resolves once the current run finishes (or times out after 4 minutes) — it does not run concurrently or interleave.
|
|
59
|
-
|
|
60
|
-
### Calling `run()` again mid-flight: prompt coalescing, not truncation
|
|
61
|
-
|
|
62
|
-
This is a deliberate design for absorbing multiple user prompts that arrive while the agent is still mid-turn (e.g. a chat UI where the user sends a couple of follow-up messages before the first reply lands) — not a bug, and nothing gets cut off mid-tool-call. The currently running loop iteration (one model turn plus all of that turn's tool dispatches) always finishes; only the *next* iteration is skipped once another `run()` call has queued:
|
|
63
|
-
|
|
64
|
-
- **One call queues up:** the in-flight run finishes its current iteration and stops there; the queued call then runs its own fresh loop from scratch, in a context that already contains everything the first run produced (including any pending tool results).
|
|
65
|
-
- **Multiple calls queue up:** every queued call except the last one has its prompt appended straight into history as a plain `role: "user"` message (with a matching stream chunk) and its own `run()` promise resolves immediately — no model turn is generated for that prompt specifically. The *last* queued call is the one that actually drives a real model turn, in a context that now includes every prompt that queued before it. The model's eventual reply addresses the whole accumulated batch at once, not each queued prompt individually.
|
|
66
|
-
|
|
67
|
-
Practical implication: don't treat an early queued call's resolved `run()` promise as "the model has now answered this prompt" — for anything but the last queued call, it only means the prompt was recorded. Drive replies off `streamCallback` if you need to know when the model has actually responded.
|
|
68
|
-
|
|
69
|
-
### The resolved boolean: "was my call the one that actually finished last?"
|
|
70
|
-
|
|
71
|
-
`run()` resolves to `true` only for the call whose own loop iteration was the one still executing when the agent finally went idle (no other `run()` call queued behind it in the meantime); every other call — including every non-last call in a queued batch (see above) and a call that itself got superseded by a later one arriving while it ran — resolves to `false`.
|
|
72
|
-
|
|
73
|
-
This matters for anything that should only happen exactly once — when the agent has truly gone idle — not once per overlapping `run()` call. A `console.log`/notification saying "agent finished" is a clear example: with several `run()` calls overlapping (a user firing off a couple of follow-ups before the first reply lands, or several callers hitting the same agent instance), every one of those calls eventually resolves — so logging on every resolution logs "finished" 2-3 times for what was really one continuous stretch of work, and the last one can land a noticeable delay after the others since it's the one that actually drove a full model turn. Gating on the resolved value being `true` instead fires exactly once, at the moment the agent actually has nothing left queued:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
if (await agent.run(prompt, streamCallback)) {
|
|
77
|
-
console.log("agent finished"); // fires exactly once, when the agent is genuinely idle
|
|
78
|
-
}
|
|
79
|
-
// without the check: could log "finished" 2-3 times, with the last one arriving
|
|
80
|
-
// minutes after the others if the final queued call drove a long tool-heavy turn
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
A `false` result isn't an error — it just means another `run()` call (either one already queued when yours was called, or one that queued while yours was executing) is the one that ended up driving the agent's final state, and that call is the one whose `true` you should be waiting on instead.
|
|
84
|
-
|
|
85
|
-
`safeStop()` requests the current run stop at the next safe point; it resolves once the loop has actually stopped, or rejects if it doesn't stop within 4 minutes. It does not interrupt an in-flight model call — that call is left to finish normally, and the loop stops right after.
|
|
86
|
-
|
|
87
|
-
`immediateStop()` requests the same stop but returns right away without waiting for confirmation, **and** aborts the in-flight `chat()` call to the main provider immediately — the underlying HTTP request is cancelled rather than left to run to completion, so no further output tokens get generated (and billed) past that point. Input tokens, and any output already generated before the abort landed, are already committed — cancelling can't retroactively make those free. This is the one to reach for when a call has to be killed regardless of how far through it is (e.g. a credit balance that just went negative). A tool dispatch already in progress isn't aborted (tools have no generic cancellation hook) — the loop stops as soon as that call returns. Abort support is real for Anthropic, OpenAI, and DeepSeek (both streaming and non-streaming); for Ollama it only takes effect on streaming calls — see the note in `ollama-provider.ts`.
|
|
88
|
-
|
|
89
|
-
If you're deciding *when* to call `immediateStop()` from outside — e.g. against an external credit/cost system — watch `usage` on the `StreamCallback` chunks (see [Streaming](./streaming.md)) rather than waiting for `run()` to resolve: it's the same callback passed into `run()`, forwarded straight through to the provider, so external code sees the same numbers `BaseAgent` itself uses to build `Message.usage`. Note this is only genuinely live for Anthropic — the other three providers only report usage once a turn is already complete, so for them there's no earlier signal to react to, cancellation-timing-wise, than the final chunk.
|
|
90
|
-
|
|
91
|
-
For a **streaming** call, an abort doesn't throw the turn away: `chat()` resolves normally with whatever text, thinking, and tool calls were already accumulated before the cutoff — the same as if the model had naturally stopped there — so anything already sent to `streamCallback` still ends up recorded in message history instead of vanishing. A tool call the model was still in the middle of generating when the abort hit is dropped even then (its arguments may be truncated JSON); only tool calls that had already fully finished are included — e.g. if the model requested 3 tool calls and the abort landed while the 3rd was still streaming, only the first 2 come back. For a **non-streaming** call there's nothing to salvage (the API only returns a response once, in full) — abort there still surfaces as a thrown error, and `BaseAgent` discards that turn entirely rather than recording a broken one.
|
|
92
|
-
|
|
93
|
-
`stop()` is a deprecated alias for `safeStop()` and will be removed in the 1.0.0 major release.
|
|
94
|
-
|
|
95
|
-
### Native tool-calling is required
|
|
96
|
-
|
|
97
|
-
`run()` throws immediately if the main provider's `getCapabilities().supportsTools` is `false`. There is no legacy JSON-in-text fallback in the main loop anymore — pick a provider/model combination (or pass `{ supportsTools: true }`) that supports native function calling.
|
|
98
|
-
|
|
99
|
-
## Memory compaction
|
|
100
|
-
|
|
101
|
-
OrbitX never truncates or deletes conversation history. Compaction happens through a real, always-available native tool — `compact_memory({ new_memory: string })` — that `BaseAgent` appends to every tool schema list alongside whatever MCP tools you allowed. This means the model can compact its own context whenever *it* judges the conversation has grown long, not only when forced to.
|
|
102
|
-
|
|
103
|
-
Calling `compact_memory`:
|
|
104
|
-
|
|
105
|
-
1. Replaces the running `compactMemory` string with `new_memory`, which is injected into every future system prompt (`MEMORY:` section). The cached system prompt is invalidated at the same time, so the very next turn's system prompt is rebuilt with the new summary rather than replaying the stale one.
|
|
106
|
-
2.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`
|
|
174
|
-
|
|
175
|
-
| Tool | Purpose |
|
|
176
|
-
|---|---|
|
|
177
|
-
| `agent-list` | List every worker in the roster — name, description, rating, and whether it's currently hired. Also returns `hiredCount` and (when set) `maxHired`. |
|
|
178
|
-
| `agent-hire` | Hire a worker by name, making it eligible for `agent-prompt`. Hiring an already-hired worker is a no-op. |
|
|
179
|
-
| `agent-prompt` | Send a prompt to a hired worker and wait for its response. |
|
|
180
|
-
|
|
181
|
-
`maxHired` caps how many workers can be hired at once — omit it for no limit. When set, it's woven directly into `agent-hire`'s own tool description (so the model learns the constraint from the tool itself, not out-of-band instructions) and exceeding it throws a clear error naming the current limit and who's currently hired. Re-hiring an already-hired worker is still a free no-op and never counts against the cap.
|
|
182
|
-
|
|
183
|
-
Give these to your **planner** agent (the one that decides which workers to use), not to the workers themselves:
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
import { AgentTools, SimpleAgent, WorkerAgent, AnthropicProvider, AgentReportTool, MCPConnection, MCPServer, MCPClient } from "orbitx";
|
|
187
|
-
|
|
188
|
-
function buildWorker(name: string, description: string, rating: Record<string, number>) {
|
|
189
|
-
const connection = new MCPConnection();
|
|
190
|
-
const server = new MCPServer(connection);
|
|
191
|
-
const reportTool = AgentReportTool();
|
|
192
|
-
server.registerTool(reportTool);
|
|
193
|
-
// ...register whatever other tools this worker needs on `server` too...
|
|
194
|
-
|
|
195
|
-
return new WorkerAgent({
|
|
196
|
-
name,
|
|
197
|
-
description,
|
|
198
|
-
rating,
|
|
199
|
-
instruction: `You are the ${name} specialist.`,
|
|
200
|
-
aiProvider: new AnthropicProvider("api-key", "claude-sonnet-5"),
|
|
201
|
-
mcpClient: new MCPClient(name, connection),
|
|
202
|
-
allowedTools: [reportTool /* , ...other tools registered above */],
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const backendWorker = buildWorker("backend-worker", "Node.js APIs, databases, servers.", { backend: 9, frontend: 2 });
|
|
207
|
-
const frontendWorker = buildWorker("frontend-worker", "React UI work.", { backend: 2, frontend: 9 });
|
|
208
|
-
|
|
209
|
-
const planner = new SimpleAgent({
|
|
210
|
-
aiProvider: new AnthropicProvider("api-key", "claude-sonnet-5"),
|
|
211
|
-
instruction: "You coordinate work across specialist worker agents.",
|
|
212
|
-
// At most 1 hired at a time here, just as an example constraint.
|
|
213
|
-
tools: AgentTools([backendWorker, frontendWorker], { maxHired: 1 }),
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### `agent-report` — for workers, not the planner
|
|
218
|
-
|
|
219
|
-
Include `AgentReportTool()` in each **worker's own** `allowedTools` (and register it on that worker's own `MCPServer`, as in the example above) — it's what lets a worker hand a result back once it's done. Calling it always ends the worker's current turn immediately (`stopIterationAfterUsingThisTool`), so a worker should call it and stop, not keep reasoning afterward.
|
|
220
|
-
|
|
221
|
-
### How a prompt/report round-trip actually works
|
|
222
|
-
|
|
223
|
-
1. Planner calls `agent-hire`, then `agent-prompt` with a task.
|
|
224
|
-
2. `agent-prompt`'s implementation calls `workerAgent.run(prompt)` and waits for it to resolve — the worker runs its own full turn (its own reasoning, its own tools), completely independently of the planner's own loop.
|
|
225
|
-
3. Once the worker's `run()` resolves, `agent-prompt` looks at only the messages that turn produced: if the worker called `agent-report`, that report text is returned (`{ report, reported: true }`); otherwise it falls back to the worker's last plain-text answer (`{ report, reported: false }`).
|
|
226
|
-
4. The planner sees this as a normal tool result. It's the planner's call what happens next — prompt the same worker again to continue (its message history persists across `agent-prompt` calls, same as any agent's `run()`), or move on and never call it again if the report shows the work is done. Nothing about the worker's lifecycle is automatic.
|
|
227
|
-
|
|
228
|
-
`AgentTools(...)` state (which agents are hired) is scoped to that one call — building a second, independent planner with its own `AgentTools([...])` call gets its own hire state, even in the same process.
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
## `BaseAgent` vs `SimpleAgent`
|
|
4
|
+
|
|
5
|
+
- **`BaseAgent`** (`src/core/base-agent.ts`) is the real engine — the run loop, memory compaction, tool dispatch, token accounting. It requires you to bring your own `MCPClient` (and therefore your own `MCPServer`/connection), which is what gives you the choice of in-process, IPC, or WebSocket execution (see [MCP Architecture](./mcp-architecture.md)).
|
|
6
|
+
- **`SimpleAgent`** (`src/templates/simple.ts`) is a thin subclass that wires up an in-process `MCPConnection` + `MCPServer` + `MCPClient` for you from a flat `tools`/`skills` list. Use this unless you specifically need a different transport.
|
|
7
|
+
- **`AgentDefinition`** (`src/core/agent-definition.ts`) isn't an agent at all — it's a reusable *description* of one kind of agent (persona, skills, tools) with no runtime wiring, which `SwarmBase` instantiates as many plain `BaseAgent`s as it needs. See [Multi-agent](#multi-agent) below.
|
|
8
|
+
|
|
9
|
+
## `BaseAgent` constructor
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
new BaseAgent({
|
|
13
|
+
instruction: string;
|
|
14
|
+
safetyPolicies?: string; // default: "" — see "Guardrail system prompt" below
|
|
15
|
+
allowedTools: MCPTool<any>[];
|
|
16
|
+
aiProvider: AIProvider | AgentProviderEntry[]; // see Providers doc
|
|
17
|
+
mcpClient: MCPClient;
|
|
18
|
+
skills?: Skill[];
|
|
19
|
+
maxMemorizeToken?: number; // default: derived from the main provider's contextWindow * safeUsageRatio
|
|
20
|
+
initData?: {
|
|
21
|
+
compactMemory: string;
|
|
22
|
+
retiredMessages: Message[]; // complete, ever-appended history of retired messages — each Message carries its own usage/timestamp
|
|
23
|
+
messagesCompact: Message[]; // the working context currently replayed to the provider every turn
|
|
24
|
+
};
|
|
25
|
+
features?: {
|
|
26
|
+
executeProviderFromMCPTool?: boolean; // default: false — see "executeProvider feature flag" below
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Guardrail system prompt
|
|
32
|
+
|
|
33
|
+
`safetyPolicies` is a separate `role: "system"` message, sent as its own leading entry ahead of the regular system prompt (see [Instructions, skills, and memory](#instructions-skills-and-memory-in-the-system-prompt)) on every main-provider `chat()` call in the run loop. Keep house rules/guardrails here rather than folding them into `instruction`, since it's a separate, cache-stable prefix ahead of the rest of the system prompt.
|
|
34
|
+
|
|
35
|
+
It is **not** prepended on an `executeProvider` call (see below) — stuffing a policy paragraph ahead of a narrow, single-purpose call (e.g. "describe this image") measurably degrades that call's output, and not every registered provider role even accepts a `system` message the way the main LLM does. A pre-dispatch content-safety check is the intended replacement there; not implemented yet.
|
|
36
|
+
|
|
37
|
+
### `executeProvider` feature flag
|
|
38
|
+
|
|
39
|
+
A tool's `execute()` can reach a registered `AIProvider` directly via `mcp.executeProvider(toolCallId, type, input)` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)) — a bigger trust surface than a tool that only returns data to the model, since it lets a tool spend real provider calls (and, per the point above, without a guardrail system prompt ahead of them). It's off by default: `BaseAgent#executeProvider` throws immediately unless `features.executeProviderFromMCPTool` was set `true` on construction. This gates every role uniformly (`image-describer` included) — enable it once you're ready to trust the tools you've allowed with direct provider access. **Inside a swarm this moves up a level:** the swarm answers these calls out of its own provider set, so the flag that counts is `SwarmBaseProps.features` and a per-agent one no longer opens or closes this door — see [Swarm](./swarm.md#providers-a-tool-reaches-for).
|
|
40
|
+
|
|
41
|
+
`SimpleAgent`'s constructor is the same shape minus `mcpClient`/`allowedTools` (it builds those from `tools`), plus `tools?: MCPTool<any>[]` and `maxMemorizeToken` defaulting to `16000` instead of being derived. `safetyPolicies` and `features` pass straight through to the underlying `BaseAgent`.
|
|
42
|
+
|
|
43
|
+
Token accounting no longer lives in `initData` as separate counters — every `Message` carries its own `usage` (see [Token accounting](#token-accounting)), so `retiredMessages`/`messagesCompact` alone are enough to resume with full token history intact.
|
|
44
|
+
|
|
45
|
+
## The run loop
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
await agent.run(prompt: string, streamCallback?: StreamCallback): Promise<boolean>;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each call to `run()`:
|
|
52
|
+
|
|
53
|
+
1. Pushes the user prompt onto the message history and emits it as a `role: "user"` stream chunk.
|
|
54
|
+
2. Sends `[system, ...history]` plus the full tool schema list to the main provider's `chat()`.
|
|
55
|
+
3. If the model requested tool calls, dispatches each one (via the `MCPClient` this agent was built with), pushes a `role: "tool"` message + stream chunk per result, and loops back to step 2.
|
|
56
|
+
4. Repeats until the model's turn produces no tool calls (or a tool marked `stopIterationAfterUsingThisTool` fired — see [Tools](./tools.md)).
|
|
57
|
+
|
|
58
|
+
If `run()` is called again while a previous call is still in flight, the new call is queued and resolves once the current run finishes (or times out after 4 minutes) — it does not run concurrently or interleave.
|
|
59
|
+
|
|
60
|
+
### Calling `run()` again mid-flight: prompt coalescing, not truncation
|
|
61
|
+
|
|
62
|
+
This is a deliberate design for absorbing multiple user prompts that arrive while the agent is still mid-turn (e.g. a chat UI where the user sends a couple of follow-up messages before the first reply lands) — not a bug, and nothing gets cut off mid-tool-call. The currently running loop iteration (one model turn plus all of that turn's tool dispatches) always finishes; only the *next* iteration is skipped once another `run()` call has queued:
|
|
63
|
+
|
|
64
|
+
- **One call queues up:** the in-flight run finishes its current iteration and stops there; the queued call then runs its own fresh loop from scratch, in a context that already contains everything the first run produced (including any pending tool results).
|
|
65
|
+
- **Multiple calls queue up:** every queued call except the last one has its prompt appended straight into history as a plain `role: "user"` message (with a matching stream chunk) and its own `run()` promise resolves immediately — no model turn is generated for that prompt specifically. The *last* queued call is the one that actually drives a real model turn, in a context that now includes every prompt that queued before it. The model's eventual reply addresses the whole accumulated batch at once, not each queued prompt individually.
|
|
66
|
+
|
|
67
|
+
Practical implication: don't treat an early queued call's resolved `run()` promise as "the model has now answered this prompt" — for anything but the last queued call, it only means the prompt was recorded. Drive replies off `streamCallback` if you need to know when the model has actually responded.
|
|
68
|
+
|
|
69
|
+
### The resolved boolean: "was my call the one that actually finished last?"
|
|
70
|
+
|
|
71
|
+
`run()` resolves to `true` only for the call whose own loop iteration was the one still executing when the agent finally went idle (no other `run()` call queued behind it in the meantime); every other call — including every non-last call in a queued batch (see above) and a call that itself got superseded by a later one arriving while it ran — resolves to `false`.
|
|
72
|
+
|
|
73
|
+
This matters for anything that should only happen exactly once — when the agent has truly gone idle — not once per overlapping `run()` call. A `console.log`/notification saying "agent finished" is a clear example: with several `run()` calls overlapping (a user firing off a couple of follow-ups before the first reply lands, or several callers hitting the same agent instance), every one of those calls eventually resolves — so logging on every resolution logs "finished" 2-3 times for what was really one continuous stretch of work, and the last one can land a noticeable delay after the others since it's the one that actually drove a full model turn. Gating on the resolved value being `true` instead fires exactly once, at the moment the agent actually has nothing left queued:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
if (await agent.run(prompt, streamCallback)) {
|
|
77
|
+
console.log("agent finished"); // fires exactly once, when the agent is genuinely idle
|
|
78
|
+
}
|
|
79
|
+
// without the check: could log "finished" 2-3 times, with the last one arriving
|
|
80
|
+
// minutes after the others if the final queued call drove a long tool-heavy turn
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A `false` result isn't an error — it just means another `run()` call (either one already queued when yours was called, or one that queued while yours was executing) is the one that ended up driving the agent's final state, and that call is the one whose `true` you should be waiting on instead.
|
|
84
|
+
|
|
85
|
+
`safeStop()` requests the current run stop at the next safe point; it resolves once the loop has actually stopped, or rejects if it doesn't stop within 4 minutes. It does not interrupt an in-flight model call — that call is left to finish normally, and the loop stops right after.
|
|
86
|
+
|
|
87
|
+
`immediateStop()` requests the same stop but returns right away without waiting for confirmation, **and** aborts the in-flight `chat()` call to the main provider immediately — the underlying HTTP request is cancelled rather than left to run to completion, so no further output tokens get generated (and billed) past that point. Input tokens, and any output already generated before the abort landed, are already committed — cancelling can't retroactively make those free. This is the one to reach for when a call has to be killed regardless of how far through it is (e.g. a credit balance that just went negative). A tool dispatch already in progress isn't aborted (tools have no generic cancellation hook) — the loop stops as soon as that call returns. Abort support is real for Anthropic, OpenAI, and DeepSeek (both streaming and non-streaming); for Ollama it only takes effect on streaming calls — see the note in `ollama-provider.ts`.
|
|
88
|
+
|
|
89
|
+
If you're deciding *when* to call `immediateStop()` from outside — e.g. against an external credit/cost system — watch `usage` on the `StreamCallback` chunks (see [Streaming](./streaming.md)) rather than waiting for `run()` to resolve: it's the same callback passed into `run()`, forwarded straight through to the provider, so external code sees the same numbers `BaseAgent` itself uses to build `Message.usage`. Note this is only genuinely live for Anthropic — the other three providers only report usage once a turn is already complete, so for them there's no earlier signal to react to, cancellation-timing-wise, than the final chunk.
|
|
90
|
+
|
|
91
|
+
For a **streaming** call, an abort doesn't throw the turn away: `chat()` resolves normally with whatever text, thinking, and tool calls were already accumulated before the cutoff — the same as if the model had naturally stopped there — so anything already sent to `streamCallback` still ends up recorded in message history instead of vanishing. A tool call the model was still in the middle of generating when the abort hit is dropped even then (its arguments may be truncated JSON); only tool calls that had already fully finished are included — e.g. if the model requested 3 tool calls and the abort landed while the 3rd was still streaming, only the first 2 come back. For a **non-streaming** call there's nothing to salvage (the API only returns a response once, in full) — abort there still surfaces as a thrown error, and `BaseAgent` discards that turn entirely rather than recording a broken one.
|
|
92
|
+
|
|
93
|
+
`stop()` is a deprecated alias for `safeStop()` and will be removed in the 1.0.0 major release.
|
|
94
|
+
|
|
95
|
+
### Native tool-calling is required
|
|
96
|
+
|
|
97
|
+
`run()` throws immediately if the main provider's `getCapabilities().supportsTools` is `false`. There is no legacy JSON-in-text fallback in the main loop anymore — pick a provider/model combination (or pass `{ supportsTools: true }`) that supports native function calling.
|
|
98
|
+
|
|
99
|
+
## Memory compaction
|
|
100
|
+
|
|
101
|
+
OrbitX never truncates or deletes conversation history. Compaction happens through a real, always-available native tool — `compact_memory({ new_memory: string, continue_iterating?: boolean })` — that `BaseAgent` appends to every tool schema list alongside whatever MCP tools you allowed. This means the model can compact its own context whenever *it* judges the conversation has grown long, not only when forced to.
|
|
102
|
+
|
|
103
|
+
Calling `compact_memory`:
|
|
104
|
+
|
|
105
|
+
1. Replaces the running `compactMemory` string with `new_memory`, which is injected into every future system prompt (`MEMORY:` section). The cached system prompt is invalidated at the same time, so the very next turn's system prompt is rebuilt with the new summary rather than replaying the stale one.
|
|
106
|
+
2. Once every tool call in that turn has been dispatched and had its normal tool-response message appended to `messagesCompact` — `compact_memory`'s own response included — the *entire* turn's `messagesCompact` (the assistant message, every tool-response message, all of it) is moved into `retiredMessages` and `messagesCompact` is cleared. Nothing is lost, it's just retired from what gets replayed to the model every turn. Nothing needs to be separately "rolled into a lifetime total" — every message already carries its own `usage`, so `getFullTotalToken()` (see [Token accounting](#token-accounting)) just sums `retiredMessages` + `messagesCompact` directly.
|
|
107
|
+
3. A single hidden `role: "user"` message — `"Memory compacted. Continue where you left off, using MEMORY above for context."` — is pushed onto the now-empty `messagesCompact`, but *only if `run()` is actually about to take another turn right now* (see `continue_iterating` below for when it isn't). With nothing left to respond to it this turn, an unconditional push would just leave it sitting in `messagesCompact` as a stray, unanswered `"user"` entry until some later, unrelated user message showed up after it. When it is added, the model's next turn already has the new summary via the system prompt's MEMORY section, so nothing about the compacted turn — including the `compact_memory` call/response pair itself — needs to stay in the replayed working context; the message is just the nudge to keep going.
|
|
108
|
+
|
|
109
|
+
Archiving is deliberately deferred until the whole turn is dispatched, rather than happening the instant `compact_memory` itself resolves: providers reject a `"tool"`-role message that isn't preceded, in the same request, by the assistant message carrying the `tool_calls` it's answering, so archiving mid-turn would strand the response of any *other* tool call made alongside `compact_memory` in the same turn. Doing it once, after every call in the turn already has its response in `messagesCompact`, avoids that regardless of how many other tools were called or in what order.
|
|
110
|
+
|
|
111
|
+
Like the forced-compaction nudge below, this confirmation message (and its matching `StreamCallback` chunk) is marked `hidden: true` — see the note two paragraphs down.
|
|
112
|
+
|
|
113
|
+
### `continue_iterating`: letting the model opt out of the next round-trip
|
|
114
|
+
|
|
115
|
+
Every other tool call makes `run()`'s loop take another turn (call the provider again) once it's dispatched — that's the right default, since a normal tool call usually means the model still has work left to do. `compact_memory` is different: a model will often call it *after* it's already produced its final answer to the user this turn, purely to tidy up context for next time. Without a way to say so, that trailing `compact_memory` call would look just like any other tool call and force one more, otherwise pointless, provider round-trip before `run()` returns.
|
|
116
|
+
|
|
117
|
+
`continue_iterating` (optional boolean, defaults to `true`) is how the model avoids that: pass `false` when the compaction is happening after the user already has their answer and there's nothing else to do this turn. When `compact_memory` is the *only* tool called in a turn and `continue_iterating` is `false`, that call no longer counts toward `run()`'s decision to keep looping, and (per the note above) the hidden confirmation message is skipped entirely rather than left dangling — the turn ends and control returns to the caller right after archiving. If any other tool was called in the same turn, that other call still triggers the normal continuation regardless of `continue_iterating`, since something else clearly still needed a response — and in that case the confirmation message is still added, since there's a next turn for it to be read by.
|
|
118
|
+
|
|
119
|
+
Before each turn, `BaseAgent` also checks the current context size: the summed `inputMissTokens` of every message's `"main"` usage entry across `messagesCompact` (see [Token accounting](#token-accounting)) — a cheap proxy for context size, not a precise replay-cost measurement. When that sum exceeds `maxMemorizeToken`, the agent doesn't compact on the model's behalf — instead it appends a `role: "user"` message telling the model to call `compact_memory` before doing anything else, and loops back for another turn. This keeps compaction on the same native tool-call path the model already uses for everything else, rather than a side-channel the model never sees; a model that ignores the nudge (rare, since the instruction is explicit) will simply see it repeated on the next turn until it complies.
|
|
120
|
+
|
|
121
|
+
This nudge message (and its matching `StreamCallback` chunk) is marked `hidden: true`, since it's `BaseAgent` talking to the model on the caller's behalf, not something a human said or the model produced — a chat UI built on this SDK should filter it out of what it shows an end user, the same way it wouldn't show raw system-prompt text. See [Streaming](./streaming.md) for the full `Message`/chunk field reference.
|
|
122
|
+
|
|
123
|
+
The nudge's text explicitly tells the model how to decide `continue_iterating` on the `compact_memory` call it triggers: `true` if there's still unfinished work left from before the nudge fired (e.g. the model is mid-way through a multi-step task the user asked for), `false` if the user already had everything they needed. `run()` trusts that decision either way — it does **not** force `continue_iterating` to `false` just because the call is answering this internal nudge rather than a real user message. A compaction that happens mid-task (say, partway through a long sequence of tool calls the user asked for) needs to keep going exactly as much as a voluntary one does; only the model, not `run()`, knows whether that's the case.
|
|
124
|
+
|
|
125
|
+
`maxMemorizeToken`, if not passed explicitly, is derived as `contextWindow * safeUsageRatio` (default ratio `0.5`) from the main provider's `getCapabilities()` — so it scales automatically with whatever model you plug in.
|
|
126
|
+
|
|
127
|
+
## Persisting and resuming state
|
|
128
|
+
|
|
129
|
+
`getCurrentAgentStates()` returns exactly the shape expected by the `initData` constructor option — `compactMemory`, `retiredMessages`, `messagesCompact`. Serialize it (it's plain JSON) after any `run()` call, and pass it back into a fresh `BaseAgent`/`SimpleAgent` to resume — same conversation, same memory, same token history, since every message already carries its own `usage`. See the example in [Getting Started](./getting-started.md#recovering-an-agents-state).
|
|
130
|
+
|
|
131
|
+
A snapshot taken with an older SDK version, before per-message `usage` existed, still loads fine — messages with no `usage` field just contribute `0` to every token total below.
|
|
132
|
+
|
|
133
|
+
### History repair on load
|
|
134
|
+
|
|
135
|
+
Both `retiredMessages` and `messagesCompact` passed via `initData` are run through an internal repair pass before `BaseAgent` stores them. It drops any assistant `toolCalls` entry that has no matching `"tool"`-role message right after it, and any `"tool"`-role message that has no preceding assistant `toolCalls` entry to answer — the shape a provider rejects with a 400 the moment it's replayed. This can happen to a snapshot saved mid-run (e.g. the process was killed between dispatching a tool call and receiving its result) or one saved with an older SDK version that had the `compact_memory` pairing bug described above. Repair only removes orphaned tool-call/tool-result pairs; every other message, and all of `compactMemory`, is preserved as-is.
|
|
136
|
+
|
|
137
|
+
## Token accounting
|
|
138
|
+
|
|
139
|
+
Token usage lives on `Message.usage` (`MessageUsage[]`), not on separate counters — `BaseAgent` populates it as `chat()` responses come back, and the four read methods below are pure derived sums over `retiredMessages`/`messagesCompact`. There's no rollover bookkeeping to keep in sync on compaction (see [Memory compaction](#memory-compaction)): a message lives in exactly one of `retiredMessages` (retired) or `messagesCompact` (active) at a time — they're disjoint, not overlapping — so "already compacted away" is simply `retiredMessages` itself, with no slicing required. The complete history at any point is the concatenation of the two, in that order, not `retiredMessages` alone.
|
|
140
|
+
|
|
141
|
+
Each entry in `Message.usage` is one of:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
{ type: ProviderType; model: string; unit: "tokens"; inputMissTokens: number; inputCacheTokens: number; outputTokens: number }
|
|
145
|
+
{ type: ProviderType; model: string; unit: "cost"; cost: number }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`type` is the provider role that produced the entry (see [Provider roles](./providers.md#provider-roles) — `"main"`, `"image-describer"`, or anything else registered/looked up). `model` is the concrete model that role resolved to for this particular call, exactly as the provider reports it from `getModel()` (e.g. `"deepseek-v4-flash"`, `"claude-sonnet-4-5"`) — the role says *what the call was for*, the model says *what actually answered it*, and one role can resolve to different models across a run (or across agents in a swarm), so a per-model breakdown is only possible because each entry carries its own. The read methods below still sum by `type`; filter `Message.usage` yourself when you want the split per model. `unit` distinguishes real token billing from anything that isn't naturally token-shaped — an image/audio/3D-generation call billed as a flat price reports `unit: "cost"` instead.
|
|
149
|
+
|
|
150
|
+
**Attribution** — a `type: "main"` entry is set only on the assistant message a `chat()` call produced, straight from that call's own `inputMissTokens`/`inputCacheTokens`/`outputTokens`. There's no splitting or attribution onto the user/tool messages that triggered the call, and no per-message context-size measurement — a user or tool message never carries a `"main"` entry. These are real, provider-reported numbers (Anthropic's `cache_read_input_tokens`/`cache_creation_input_tokens`, DeepSeek's `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens`, OpenAI's `prompt_tokens_details.cached_tokens`), not a client-side guess — see [Providers](./providers.md) for what each one actually reports and Ollama's lack of any cache concept (`inputCacheTokens` always `0` there).
|
|
151
|
+
|
|
152
|
+
The compaction trigger (see [Memory compaction](#memory-compaction)) sums `inputMissTokens` across every assistant message's `"main"` entry in `messagesCompact` as a cheap proxy for context size — not a precise measurement of what replaying `messagesCompact` right now would actually cost, since each entry reflects the call that produced that one assistant message, not the whole context at that point.
|
|
153
|
+
|
|
154
|
+
Every other `type` entry comes from a tool calling `mcp.executeProvider(toolCallId, type, input)` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)) — but the entry itself is recorded by `BaseAgent`, the instant the real provider call resolves, never by the tool's own return value. This is deliberate: a tool can't under-report (or hide) what it spent, because it never holds the usage number to begin with — `BaseAgent` keys it by `toolCallId` and moves it onto that call's result `Message.usage` only after `execute()` returns, regardless of what the tool itself returns. `read-image`/`browser-screenshot`, for example, trigger a `type: "image-describer", unit: "tokens"` entry with no hit/miss split (each call is a fresh, standalone chat issued directly against the resolved provider by `BaseAgent`, so every input token counts as `inputMissTokens`). In a swarm the provider call is *answered* by `SwarmBase` (out of the swarm's provider set) but the usage is still recorded on the agent that made the tool call, through `BaseAgent.recordToolCallUsage(toolCallId, usage)` — same message, same guarantee, so agent totals stay truthful and the swarm's totals pick it up from there instead of counting it twice. See [Swarm](./swarm.md#providers-a-tool-reaches-for).
|
|
155
|
+
|
|
156
|
+
`executeProvider` picks `unit: "tokens"` vs `unit: "cost"` dynamically from what the provider's `ChatResponse` actually reported, rather than assuming one or the other: if the response set `cost` (a flat, non-token price — see [`ChatResponse`](./providers.md#writing-a-custom-provider)), the entry is `unit: "cost"`; otherwise it falls back to the token fields (`inputMissTokens`/`inputCacheTokens`/`outputTokens`, each defaulting to `0` if the provider omitted them). Every built-in provider (Anthropic/OpenAI/DeepSeek/Ollama) is token-billed and always takes the token path — `cost` only comes into play for a custom provider registered under an image/video/audio-generation-style role that bills per-request instead of per-token.
|
|
157
|
+
|
|
158
|
+
**The four read methods:**
|
|
159
|
+
|
|
160
|
+
- `getCompactTotalTokens(type: ProviderType)` / `getFullTotalToken(type: ProviderType)` — return `{ total, inputHit, inputMiss, output }`, summing `unit: "tokens"` entries matching `type` over `messagesCompact` only, or `retiredMessages` + `messagesCompact` (this agent's whole lifetime), respectively.
|
|
161
|
+
- `getCompactTotalCost(type: ProviderType)` / `getFullTotalCost(type: ProviderType)` — return a plain `number`, summing `unit: "cost"` entries matching `type` over the same two scopes.
|
|
162
|
+
|
|
163
|
+
## Instructions, skills, and memory in the system prompt
|
|
164
|
+
|
|
165
|
+
The system prompt is built lazily on first use and cached — a stable prefix between compactions, which matters for provider-side prompt caching. It's assembled from: a fixed "you have tools" preamble, each skill's name/description/instructions (see [Skills](./skills.md)), the current `MEMORY:` block (sourced from `compactMemory`), and your `instruction` string, in that order. Each `compact_memory` call invalidates the cache (see [Memory compaction](#memory-compaction)), so the next turn's system prompt is rebuilt with the fresh `MEMORY:` block instead of the one that was current when the prompt was first built.
|
|
166
|
+
|
|
167
|
+
## Multi-agent
|
|
168
|
+
|
|
169
|
+
Multiple agents that hire, task, and report to each other live in [Swarm](./swarm.md) — `SwarmBase`, `getAgentTools()`, and `AgentDefinition`.
|
|
170
|
+
|
|
171
|
+
The short version, since it changes nothing here: every agent in a swarm is an ordinary `BaseAgent` with an ordinary `allowedTools` list. `SwarmBase` owns the live agents and the wiring between them; the `agent-*` tools from `getAgentTools()` are the only way an agent reaches that wiring; and `AgentDefinition` is the `Skill`-shaped abstraction you write once and hand to a swarm rather than assembling `BaseAgentProps` per agent.
|
|
172
|
+
|
|
173
|
+
The one behavior worth knowing from this page's perspective: a swarm's `run()` resolves `true` only when *every* agent in it is idle, not when the entrypoint agent stops talking. A single `BaseAgent.run()` resolving is one agent's turn ending; a swarm's is the whole system settling.
|
package/docs/api-reference.md
CHANGED
|
@@ -17,9 +17,14 @@ Flat index of every public export from `orbitx` (see `src/index.ts`), grouped by
|
|
|
17
17
|
|
|
18
18
|
- `BaseAgent`
|
|
19
19
|
- `SimpleAgent`
|
|
20
|
-
- `WorkerAgent` *(experimental)*, type `WorkerAgentRating`
|
|
21
20
|
- Types: `ExtractedSegment`, `ParsedToolCall`
|
|
22
21
|
|
|
22
|
+
## Swarm — see [Swarm](./swarm.md)
|
|
23
|
+
|
|
24
|
+
- `SwarmBase` — types `SwarmBaseProps`, `SwarmRunOptions`, `SwarmState`, `SwarmAgentState`, `SwarmStreamChunk`, `SwarmStreamCallback`, `SwarmAgentFailure`
|
|
25
|
+
- `AgentDefinition`, `selectTools()`, `applyTextOverride()` — types `AgentDefinitionProps`, `AgentOverrides`, `AgentFactory`, `AgentFactoryOptions`, `TextOverride`
|
|
26
|
+
- Ready-made definitions: `PlannerAgent`, `ReasonerAgent` (+ `REASONER_PANEL_MS`), `BackendAgent`, `FrontendAgent`, `TestAgent`, `ResearchAgent`, `DefaultAgents()`, and the shared instruction fragments `PLANNER_PROTOCOL` / `WORKER_PROTOCOL` / `TEAM_PROTOCOL` / `PRODUCTION_BAR`
|
|
27
|
+
|
|
23
28
|
## Tools — see [Tools](./tools.md)
|
|
24
29
|
|
|
25
30
|
- `MCPTool`, `generateRefId()`, `normalizeToolOutput()`
|
|
@@ -36,18 +41,18 @@ Built-in tool factories, grouped by domain (each domain also exports a `*Tools()
|
|
|
36
41
|
| Present | `PresentAddTool`, `PresentClearTool`, `PresentGetListTool`, `PresentTools`, `getPresentFolder()` |
|
|
37
42
|
| Question/Answer | `QuestionAnswerTool`, `QuestionAnswerTools` |
|
|
38
43
|
| Utility | `GetCurrentTimeTool`, `DelayTool`, `ReadImageTool`, `UtilTools` |
|
|
39
|
-
|
|
|
44
|
+
| Swarm | `getAgentTools(options?)`, `AgentTypesTool`, `AgentHireTool`, `AgentFireTool`, `AgentActiveTool`, `AgentPromptTool`, `AgentReportParentTool`, `AgentReportGroupTool`, `AgentToolsRegistry`, types `AgentToolsOptions`, `AgentToolsHandle`, `SwarmController`, `SwarmAgentTypeInfo`, `SwarmAgentInfo`, `AgentToolsEvent`, `AgentToolsAck`, `AgentToolsListener` — see [Swarm](./swarm.md#getagenttoolsoptions) |
|
|
40
45
|
|
|
41
46
|
## Skills — see [Skills](./skills.md)
|
|
42
47
|
|
|
43
48
|
- `Skill`
|
|
44
|
-
- `BackendSecuritySkill`, `BigTaskSkill`, `CodeVerificationSkill`, `LongTaskEfficiencySkill`, `NodeBackendSkill`, `PlannerSkill`, `PresentSkill`, `QuestionAnswerSkill`, `ReactFrontendSkill`, `ResearchSkill`, `
|
|
49
|
+
- `BackendSecuritySkill`, `BigTaskSkill`, `CodeVerificationSkill`, `LongTaskEfficiencySkill`, `NodeBackendSkill`, `PlannerSkill`, `PresentSkill`, `QuestionAnswerSkill`, `ReactFrontendSkill`, `ResearchSkill`, `UiUxDesignSkill`, `WebEndToEndTestSkill`
|
|
45
50
|
|
|
46
51
|
## MCP transport — see [MCP Architecture](./mcp-architecture.md)
|
|
47
52
|
|
|
48
53
|
- `MCPServer`, `MCPClient`
|
|
49
54
|
- `MCPConnection`, `MCPIPCConnection`, `MCPWSConnection`
|
|
50
|
-
- `MCPComputer` *(experimental)*
|
|
55
|
+
- `MCPComputer`, type `MCPComputerOptions` *(experimental)*
|
|
51
56
|
- `MCPStorage`, `MCPFSStorage`
|
|
52
57
|
- `MCPRNG`
|
|
53
58
|
- `MCPOutputFilter` (`MCPFilter` is a deprecated alias, removed in `1.0.0`)
|