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.
Files changed (141) hide show
  1. package/Readme.md +478 -456
  2. package/dist/agents/backend.d.ts +4 -0
  3. package/dist/agents/backend.d.ts.map +1 -0
  4. package/dist/agents/backend.js +54 -0
  5. package/dist/agents/backend.js.map +1 -0
  6. package/dist/agents/frontend.d.ts +4 -0
  7. package/dist/agents/frontend.d.ts.map +1 -0
  8. package/dist/agents/frontend.js +50 -0
  9. package/dist/agents/frontend.js.map +1 -0
  10. package/dist/agents/index.d.ts +10 -0
  11. package/dist/agents/index.d.ts.map +1 -0
  12. package/dist/agents/index.js +22 -0
  13. package/dist/agents/index.js.map +1 -0
  14. package/dist/agents/planner.d.ts +4 -0
  15. package/dist/agents/planner.d.ts.map +1 -0
  16. package/dist/agents/planner.js +86 -0
  17. package/dist/agents/planner.js.map +1 -0
  18. package/dist/agents/protocol.d.ts +5 -0
  19. package/dist/agents/protocol.d.ts.map +1 -0
  20. package/dist/agents/protocol.js +70 -0
  21. package/dist/agents/protocol.js.map +1 -0
  22. package/dist/agents/reasoner.d.ts +5 -0
  23. package/dist/agents/reasoner.d.ts.map +1 -0
  24. package/dist/agents/reasoner.js +96 -0
  25. package/dist/agents/reasoner.js.map +1 -0
  26. package/dist/agents/research.d.ts +4 -0
  27. package/dist/agents/research.d.ts.map +1 -0
  28. package/dist/agents/research.js +28 -0
  29. package/dist/agents/research.js.map +1 -0
  30. package/dist/agents/tester.d.ts +4 -0
  31. package/dist/agents/tester.d.ts.map +1 -0
  32. package/dist/agents/tester.js +70 -0
  33. package/dist/agents/tester.js.map +1 -0
  34. package/dist/core/agent-definition.d.ts +61 -0
  35. package/dist/core/agent-definition.d.ts.map +1 -0
  36. package/dist/core/agent-definition.js +82 -0
  37. package/dist/core/agent-definition.js.map +1 -0
  38. package/dist/core/ai-provider.d.ts +3 -0
  39. package/dist/core/ai-provider.d.ts.map +1 -1
  40. package/dist/core/ai-provider.js.map +1 -1
  41. package/dist/core/anthropic-provider.d.ts +1 -0
  42. package/dist/core/anthropic-provider.d.ts.map +1 -1
  43. package/dist/core/anthropic-provider.js +3 -0
  44. package/dist/core/anthropic-provider.js.map +1 -1
  45. package/dist/core/base-agent.d.ts +51 -28
  46. package/dist/core/base-agent.d.ts.map +1 -1
  47. package/dist/core/base-agent.js +118 -26
  48. package/dist/core/base-agent.js.map +1 -1
  49. package/dist/core/deepseek-provider.d.ts +1 -0
  50. package/dist/core/deepseek-provider.d.ts.map +1 -1
  51. package/dist/core/deepseek-provider.js +3 -0
  52. package/dist/core/deepseek-provider.js.map +1 -1
  53. package/dist/core/mcp-computer.d.ts +14 -1
  54. package/dist/core/mcp-computer.d.ts.map +1 -1
  55. package/dist/core/mcp-computer.js +352 -71
  56. package/dist/core/mcp-computer.js.map +1 -1
  57. package/dist/core/ollama-provider.d.ts +1 -0
  58. package/dist/core/ollama-provider.d.ts.map +1 -1
  59. package/dist/core/ollama-provider.js +3 -0
  60. package/dist/core/ollama-provider.js.map +1 -1
  61. package/dist/core/openai-provider.d.ts +1 -0
  62. package/dist/core/openai-provider.d.ts.map +1 -1
  63. package/dist/core/openai-provider.js +3 -0
  64. package/dist/core/openai-provider.js.map +1 -1
  65. package/dist/core/swarm-base.d.ts +88 -0
  66. package/dist/core/swarm-base.d.ts.map +1 -0
  67. package/dist/core/swarm-base.js +427 -0
  68. package/dist/core/swarm-base.js.map +1 -0
  69. package/dist/index.d.ts +7 -5
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -3
  72. package/dist/index.js.map +1 -1
  73. package/dist/tests/agents/default-agents.test.d.ts +2 -0
  74. package/dist/tests/agents/default-agents.test.d.ts.map +1 -0
  75. package/dist/tests/agents/default-agents.test.js +106 -0
  76. package/dist/tests/agents/default-agents.test.js.map +1 -0
  77. package/dist/tests/core/agent-definition.test.d.ts +2 -0
  78. package/dist/tests/core/agent-definition.test.d.ts.map +1 -0
  79. package/dist/tests/core/agent-definition.test.js +99 -0
  80. package/dist/tests/core/agent-definition.test.js.map +1 -0
  81. package/dist/tests/core/scripted-provider.d.ts +38 -0
  82. package/dist/tests/core/scripted-provider.d.ts.map +1 -0
  83. package/dist/tests/core/scripted-provider.js +85 -0
  84. package/dist/tests/core/scripted-provider.js.map +1 -0
  85. package/dist/tests/core/swarm-base.test.d.ts +2 -0
  86. package/dist/tests/core/swarm-base.test.d.ts.map +1 -0
  87. package/dist/tests/core/swarm-base.test.js +387 -0
  88. package/dist/tests/core/swarm-base.test.js.map +1 -0
  89. package/dist/tests/index.d.ts +3 -0
  90. package/dist/tests/index.d.ts.map +1 -1
  91. package/dist/tests/index.js +3 -0
  92. package/dist/tests/index.js.map +1 -1
  93. package/dist/tests/tools/agent/agent-tools.test.js +225 -153
  94. package/dist/tests/tools/agent/agent-tools.test.js.map +1 -1
  95. package/dist/tools/agent/active.d.ts +5 -0
  96. package/dist/tools/agent/active.d.ts.map +1 -0
  97. package/dist/tools/agent/active.js +40 -0
  98. package/dist/tools/agent/active.js.map +1 -0
  99. package/dist/tools/agent/fire.d.ts +5 -0
  100. package/dist/tools/agent/fire.d.ts.map +1 -0
  101. package/dist/tools/agent/fire.js +38 -0
  102. package/dist/tools/agent/fire.js.map +1 -0
  103. package/dist/tools/agent/hire.d.ts +2 -2
  104. package/dist/tools/agent/hire.d.ts.map +1 -1
  105. package/dist/tools/agent/hire.js +43 -10
  106. package/dist/tools/agent/hire.js.map +1 -1
  107. package/dist/tools/agent/index.d.ts +22 -19
  108. package/dist/tools/agent/index.d.ts.map +1 -1
  109. package/dist/tools/agent/index.js +29 -21
  110. package/dist/tools/agent/index.js.map +1 -1
  111. package/dist/tools/agent/prompt.d.ts +2 -2
  112. package/dist/tools/agent/prompt.d.ts.map +1 -1
  113. package/dist/tools/agent/prompt.js +19 -53
  114. package/dist/tools/agent/prompt.js.map +1 -1
  115. package/dist/tools/agent/registry.d.ts +74 -24
  116. package/dist/tools/agent/registry.d.ts.map +1 -1
  117. package/dist/tools/agent/registry.js +93 -48
  118. package/dist/tools/agent/registry.js.map +1 -1
  119. package/dist/tools/agent/report-group.d.ts +5 -0
  120. package/dist/tools/agent/report-group.d.ts.map +1 -0
  121. package/dist/tools/agent/report-group.js +50 -0
  122. package/dist/tools/agent/report-group.js.map +1 -0
  123. package/dist/tools/agent/report-parent.d.ts +5 -0
  124. package/dist/tools/agent/report-parent.d.ts.map +1 -0
  125. package/dist/tools/agent/report-parent.js +36 -0
  126. package/dist/tools/agent/report-parent.js.map +1 -0
  127. package/dist/tools/agent/types.d.ts +5 -0
  128. package/dist/tools/agent/types.d.ts.map +1 -0
  129. package/dist/tools/agent/types.js +17 -0
  130. package/dist/tools/agent/types.js.map +1 -0
  131. package/docs/agents.md +173 -228
  132. package/docs/api-reference.md +9 -4
  133. package/docs/getting-started.md +78 -78
  134. package/docs/index.md +24 -23
  135. package/docs/mcp-architecture.md +29 -6
  136. package/docs/providers.md +258 -254
  137. package/docs/skills.md +0 -1
  138. package/docs/streaming.md +133 -133
  139. package/docs/swarm.md +503 -0
  140. package/docs/tools.md +214 -212
  141. package/package.json +1 -1
package/docs/swarm.md ADDED
@@ -0,0 +1,503 @@
1
+ # Swarm
2
+
3
+ A **swarm** is a group of `BaseAgent`s that hire, task, and report to each other, behind a single entrypoint agent you talk to.
4
+
5
+ Nothing about `BaseAgent` changes to make this work. Every agent in a swarm is an ordinary `BaseAgent` with an ordinary `allowedTools` list. What makes it a swarm is two pieces:
6
+
7
+ - **`SwarmBase`** (`src/core/swarm-base.ts`) owns the live agents and the wiring between them.
8
+ - **`getAgentTools()`** (`src/tools/agent/`) builds the tools that are the only way an agent can reach that wiring.
9
+
10
+ Plus one supporting abstraction: **`AgentDefinition`** (`src/core/agent-definition.ts`), which is to agents what `Skill` is to instructions — a reusable, importable description of one kind of agent that you hand to a swarm instead of hand-writing a `BaseAgentProps` block per agent.
11
+
12
+ ## Quick start
13
+
14
+ ```ts
15
+ import {
16
+ SwarmBase, getAgentTools, DefaultAgents,
17
+ FsTools, BashTools, BrowserTools, TodoTools, UtilTools,
18
+ AnthropicProvider,
19
+ } from "orbitx";
20
+
21
+ // Built once. The tools and the swarm share the state minted here.
22
+ const agentTools = getAgentTools({ maxHired: 5 });
23
+
24
+ // Everything this app can do. Each agent definition picks what its role needs.
25
+ const tools = [
26
+ ...FsTools(), ...BashTools(), ...BrowserTools(), ...TodoTools(), ...UtilTools(),
27
+ ...agentTools.tools,
28
+ ];
29
+
30
+ const swarm = new SwarmBase({
31
+ tools,
32
+ agents: DefaultAgents(), // [PlannerAgent, BackendAgent, FrontendAgent, ResearchAgent]
33
+ default: "planner", // which type is the entrypoint
34
+ agentTools,
35
+ aiProvider: new AnthropicProvider("claude-sonnet-4-5", process.env.ANTHROPIC_API_KEY!),
36
+ });
37
+
38
+ const done = await swarm.run("Build the checkout flow", chunk => {
39
+ process.stdout.write(`[${chunk.agentId}] ${chunk.content}`);
40
+ });
41
+ // done === true → the planner is finished AND every agent it hired is idle
42
+ ```
43
+
44
+ `SwarmBase` built its own `MCPConnection` + `MCPServer` + `MCPClient` here and registered every tool it knows about — the `SimpleAgent` treatment. Pass your own `mcpClient` instead if you need a different transport; then registering tools on it is your job.
45
+
46
+ ## `AgentDefinition`
47
+
48
+ A definition is data, not a live agent: a swarm may instantiate the same definition three times. It carries the persona and the tools, and **no runtime wiring** — provider and MCP client come from the swarm.
49
+
50
+ ```ts
51
+ new AgentDefinition({
52
+ type: string; // unique in a roster; the name a model passes to agent-hire
53
+ description: string; // what this agent is good at — written for the hiring model, not for humans
54
+ rating?: Record<string, number>; // per-task fit score, e.g. { backend: 10, frontend: 2 }
55
+ instruction: string;
56
+ safetyPolicies?: string;
57
+ tools: MCPTool[]; // already narrowed out of the app's pool — see selectTools()
58
+ skills?: Skill[]; // their tools are merged into allowedTools automatically
59
+ aiProvider?: AgentProvidersInput; // optional: pins a provider for this type only
60
+ maxMemorizeToken?: number;
61
+ features?: { executeProviderFromMCPTool?: boolean };
62
+ })
63
+ ```
64
+
65
+ Methods: `getDefinition()`, `getType()`, `with(overrides)` (returns a **new** definition — never mutates), `getAllTools()` (own tools + skill tools), and `buildProps(runtime)` (the `BaseAgentProps` the swarm constructs from).
66
+
67
+ Note there is no `default` flag here. Which type is the entrypoint is the *swarm's* decision (`SwarmBaseProps.default`), because the same imported definition is an entrypoint in one swarm and a hireable worker in another.
68
+
69
+ ### `selectTools(pool, names)`
70
+
71
+ A definition picks its tools out of the pool the app constructed, by exact name or trailing-`*` prefix:
72
+
73
+ ```ts
74
+ tools: selectTools(pool, ["fs-*", "bash-*", "get-current-time", "agent-report-parent"])
75
+ ```
76
+
77
+ Order follows `names`, duplicates are dropped, and a name with no match is **skipped rather than throwing** — the pool is the source of truth for what this app enabled, so an agent asking for a tool you don't ship simply doesn't get it.
78
+
79
+ This is also the capability gate. Since the whole swarm shares one MCP client, what an agent can do is decided entirely by what its definition picked: a definition that lists `agent-hire` can hire, one that lists `agent-report-parent` can report.
80
+
81
+ ### Overrides
82
+
83
+ `AgentOverrides` can be applied by a factory's caller, or by the swarm — same merge either way:
84
+
85
+ ```ts
86
+ type TextOverride =
87
+ | string // replace
88
+ | { replace: string }
89
+ | { append: string }
90
+ | { prepend: string };
91
+
92
+ interface AgentOverrides {
93
+ instruction?: TextOverride;
94
+ safetyPolicies?: TextOverride;
95
+ aiProvider?: AgentProvidersInput;
96
+ maxMemorizeToken?: number;
97
+ features?: { executeProviderFromMCPTool?: boolean };
98
+ extraTools?: MCPTool[]; // appended to what the definition picked, deduped
99
+ }
100
+ ```
101
+
102
+ A bare string replaces — predictable default. Instructions are usually appended (a definition is written once and reused, so a swarm normally adds project context rather than discarding the persona); safety policies are usually replaced.
103
+
104
+ ```ts
105
+ new SwarmBase({
106
+ // ...
107
+ safetyPolicies: "Never touch files outside /workspace.", // every agent
108
+ instruction: { append: "This repo is a pnpm monorepo." }, // every agent
109
+ overrides: {
110
+ backend: { // this type only
111
+ instruction: { append: "Postgres, not MySQL." },
112
+ aiProvider: strongerProvider,
113
+ },
114
+ },
115
+ });
116
+ ```
117
+
118
+ Merge order is **definition → swarm-wide → per-type**, so appends stack in that order.
119
+
120
+ ## Ready-made agents (`src/agents/`)
121
+
122
+ | Export | Type | What it gets |
123
+ |---|---|---|
124
+ | `PlannerAgent` | `planner` | The hiring tools, todo tools, read-only filesystem. No write tools — a planner that can edit files starts doing the work itself. |
125
+ | `ReasonerAgent` | `reasoner` | `get-current-time`, `question-answer`, read-only filesystem, reporting tools. A six-persona panel that interrogates the user, then argues for a measured five minutes before answering. |
126
+ | `BackendAgent` | `backend` | `fs-*`, `bash-*`, reporting tools. Owns the API contract as a deliverable. Skills: node-backend, backend-security, code-verification. |
127
+ | `FrontendAgent` | `frontend` | `fs-*`, `bash-*`, `browser-*`, `read-image`, reporting tools. Built to work as one of several in a group. Skills: react-frontend, ui-ux-design, code-verification. |
128
+ | `TestAgent` | `tester` | `fs-*`, `bash-*`, `browser-*`, reporting tools. Verifies and reports; never fixes. Skills: code-verification, web-end-to-end-test. |
129
+ | `ResearchAgent` | `research` | `browser-*`, read-only filesystem, reporting tools. No write tools at all. Skill: research. |
130
+ | `DefaultAgents()` | — | All six, as a roster. |
131
+
132
+ They're ordinary factories with nothing privileged about them — copy one and change it. Each takes the app's pool and grabs what its role needs:
133
+
134
+ ```ts
135
+ agents: [
136
+ PlannerAgent, // swarm calls it with its own `tools`
137
+ BackendAgent({ tools, instruction: { append: "…" } }), // or wire it yourself
138
+ MyOwnAgent,
139
+ ]
140
+ ```
141
+
142
+ `PLANNER_PROTOCOL`, `WORKER_PROTOCOL`, `TEAM_PROTOCOL`, and `PRODUCTION_BAR` (`src/agents/protocol.ts`) are the shared text those definitions append to their own personas: how work arrives and where answers go, how several agents share one codebase without overwriting each other (`TEAM_PROTOCOL`), and what "finished" has to mean when the result is a real application rather than a demo (`PRODUCTION_BAR` — configuration from the environment, state that survives a restart, defined behaviour for every failure, nothing left stubbed, and the real build actually run). Import them into your own definitions, or ignore them. Nothing in `SwarmBase` depends on them.
143
+
144
+ ### The build pipeline
145
+
146
+ `PlannerAgent`'s instruction carries an ordered pipeline, because the ordering is what keeps parallel agents from building against something nobody checked:
147
+
148
+ 1. **Think.** Hire a `reasoner`, hand it the request, wait for its verdict. Its report is what the plan is made of. Fire it afterwards.
149
+ 2. **Backend.** Hire a `backend` agent. Its task ends with two deliverables: working code, and an **API contract** written to a file — endpoints, request/response shapes, auth, errors.
150
+ 3. **Verify the contract.** Hire a `tester` with that contract. It builds the backend, runs it, exercises every documented endpoint, and reports divergences. **The API docs do not reach any frontend until this passes** — a frontend built against a wrong contract is work paid for twice. Defects go back to the backend agent, then re-verify.
151
+ 4. **Frontends, sized to the work.** One agent for a single screen, two for a handful, three for a large surface with independent sections. All hired into **one group**, each given the verified contract and its own slice.
152
+ 5. **They scaffold by talking to each other.** Before writing code the group agrees on folder layout, routing, where shared code lives, and each agent claims its files by path — "I own `src/features/cart/`" — and never edits a file another agent claimed. That conversation runs over `agent-report-group`, without the planner in it. It's what stops three agents from creating three different project structures.
153
+ 6. **Verify the whole thing.** A `tester` drives the finished UI in a real browser and checks that frontend and backend actually agree in practice.
154
+ 7. **The planner answers the user** in its own words.
155
+
156
+ The reasoner's five minutes are wall clock, not vibes. It has no waiting tool — nothing to sleep on — so the time is filled with argument and measured as it goes: `get-current-time` before anyone speaks is T0, then **one persona speaks and one `get-current-time` follows it**, over and over, and the synthesis may not start until 300 seconds have actually passed.
157
+
158
+ That is one persona *per turn*: the persona's argument and the `get-current-time` call that ends it are a single assistant turn, and the next voice does not open its mouth until the timestamp has come back. The instruction says so in those terms because the tempting shortcut — writing all six personas in one burst and reading the clock once at the end — measures nothing. Six voices and one timestamp is a single block of text with a clock reading taped to it, not five minutes of argument, so the panel would report a number it never actually spent.
159
+
160
+ Timestamping after every persona rather than at the end of a round is what makes the pacing work. How many rounds fit inside five minutes is not a number the panel picks — it falls out of how fast the provider is answering, so a fast model gets more rounds and a slow one fewer, and both spend the same five minutes thinking. Checking the clock between voices tells the panel whether it has four rounds left to fill or is on its last one, so it argues at the right depth instead of discovering at the end that it rushed or overran. A ten-round cap keeps a very fast provider from looping past the point where rounds repeat rather than deepen.
161
+
162
+ Two things are deliberately excluded from that clock: the question phase before it (`question-answer`, no time limit — the panel interrogates the user until every persona can reason properly) and any mid-debate pause for the user, which is timestamped on both sides and subtracted, so waiting on a human never eats the five minutes.
163
+
164
+ Early agreement is explicitly treated as a signal to attack the consensus rather than permission to stop — that's the failure mode a panel exists to prevent. `REASONER_PANEL_MS` is exported if you want the number.
165
+
166
+ None of this is enforced by `SwarmBase` — it's instruction text in one definition. Replace `PlannerAgent` with your own and the swarm follows whatever pipeline you write instead.
167
+
168
+ ### Telling a paused agent from a dead one
169
+
170
+ An agent's turn ends every time it stops generating, and that is not the same as being finished. The clearest case is a tool with `stopIterationAfterUsingThisTool` that waits on something outside the swarm — `question-answer` is exactly this: the agent asks, its turn ends on purpose, and it sits there until the answer arrives.
171
+
172
+ From a parent's side that used to be indistinguishable from a child that had crashed, because `busy` alone cannot tell them apart. The parent's usual reaction — re-task it, or fire it and route the work elsewhere — is the worst possible one: a second task lands on top of the first and comes back instead of the work you wanted, and firing throws away everything the agent has done *including the question it is waiting on*, which then never gets answered.
173
+
174
+ So `SwarmAgentInfo` carries a second flag. `awaitingReport` is set the moment a parent tasks an agent and cleared when that agent reports back (or fails, or is fired). Group messages are peer chatter and leave it alone. Read together:
175
+
176
+ | `busy` | `awaitingReport` | What it means |
177
+ |---|---|---|
178
+ | `true` | `true` | Working on your task right now. |
179
+ | `false` | `true` | **Tasked, paused, still owes you an answer** — blocked on something outside the swarm, most often a question put to the user. Leave it alone. |
180
+ | `false` | `false` | Idle, owes you nothing. |
181
+ | `true` | `false` | Running something that did not come from you — its own follow-up, or a group message. |
182
+
183
+ `agent-active` surfaces both, and `PLANNER_PROTOCOL` spells out the rule they exist to support: when an agent is paused, do not re-prompt it, do not fire it, do not poll it, and do not decide it failed — a real failure arrives as an explicit failure message naming the agent, so silence is never failure. Dispatch anything that doesn't depend on it, then end your turn; its report will wake you.
184
+
185
+ ### Answering an agent's questions from your app
186
+
187
+ `question-answer` deliberately does nothing. Its `execute` returns immediately and its only real effect is `stopIterationAfterUsingThisTool`, which parks the agent. Actually reaching a human is the **host application's** job, not the library's — the library has no idea whether "the user" is a terminal, a web session, or a Slack thread.
188
+
189
+ Register it on the **client**, not in the sandbox: an agent inside a container has nobody to ask.
190
+
191
+ ```ts
192
+ const hostTools = [...QuestionAnswerTools()];
193
+ const tools = [...sandboxTools, ...agentTools.tools, ...hostTools];
194
+
195
+ const mcpClient = new MCPClient("SWARM_ENV", computer.getConnection());
196
+ [...agentTools.tools, ...hostTools].forEach(tool => mcpClient.registerTool(tool));
197
+ ```
198
+
199
+ Then watch the stream for the call, and use `run(answers, { agentId })` to wake exactly the agent that asked:
200
+
201
+ ```ts
202
+ const pendingAsks: { agentId: string; questions: any[] }[] = [];
203
+
204
+ const render: SwarmStreamCallback = chunk => {
205
+ for (const call of chunk.toolCalls ?? []) {
206
+ if (call.name === "question-answer") {
207
+ pendingAsks.push({ agentId: chunk.agentId, questions: call.inputs.questions ?? [] });
208
+ }
209
+ }
210
+ // …your normal rendering…
211
+ };
212
+
213
+ let done = await swarm.run(task, render);
214
+
215
+ // `run` resolves when nothing is generating — which is also true of a swarm parked
216
+ // on a question nobody answered. "Idle" only means finished once there is nothing
217
+ // left to ask, and an answer can produce more questions.
218
+ while (pendingAsks.length) {
219
+ const ask = pendingAsks.shift()!;
220
+ const answers = await askYourUserSomehow(ask); // terminal, HTTP, socket…
221
+ done = await swarm.run(answers, { agentId: ask.agentId, stream: render });
222
+ }
223
+ ```
224
+
225
+ Two things worth keeping in mind. `run()` returning `true` means *nothing is generating*, which a parked swarm also satisfies — so treat a non-empty question queue as "not finished" regardless of what `run` returned. And answer one agent at a time: waking an agent runs its work, and anything that work sets off, to quiet again before you look at the queue afresh.
226
+
227
+ See [`src/test.ts`](../src/test.ts) for a complete terminal implementation, including numbered `predefinedAnswer` shortcuts.
228
+
229
+ ## `getAgentTools(options?)`
230
+
231
+ Returns a **handle**, not an `MCPTool[]`:
232
+
233
+ ```ts
234
+ interface AgentToolsHandle {
235
+ readonly tools: MCPTool[]; // into your pool, and onto your MCP server
236
+ readonly id: string;
237
+ getMaxHired(): number | undefined;
238
+ attach(swarm: SwarmController): void; // lets the tools read the roster
239
+ on(event, listener): void; // what the tools ask the swarm to do
240
+ runAs(agentId, fn): Promise<T>; // labels an agent's turn
241
+ }
242
+ ```
243
+
244
+ The handle goes two places: `handle.tools` into the tool pool, and the handle itself into `new SwarmBase({ agentTools })`. Both halves share the state minted at construction, which is what makes `maxHired` a single pool rather than a per-agent allowance. Every call builds an independent handle, so two swarms in one process never share hire state — and attaching one handle to two swarms throws.
245
+
246
+ The last three are the seam between the tools and the swarm, and `SwarmBase` is the only thing that calls them — see [How it works under the hood](#how-it-works-under-the-hood).
247
+
248
+ | Tool | Who holds it | What it does |
249
+ |---|---|---|
250
+ | `agent-types` | hiring agents | Lists hireable types with description, ratings, how many of each are working, and remaining hire slots. |
251
+ | `agent-hire` | hiring agents | Spawns an agent of a type. Optional `group` and `briefing` (appended to that instance's standing instruction). Caller becomes its parent. |
252
+ | `agent-fire` | hiring agents | Releases one of your own hires and frees its slot. Stops it if it's mid-run. |
253
+ | `agent-active` | hiring agents | Every hired agent in the swarm — id, type, groups, parent, `busy`, `awaitingReport` — with `yours` marking your own. See [Telling a paused agent from a dead one](#telling-a-paused-agent-from-a-dead-one). |
254
+ | `agent-prompt` | hiring agents | Hands a task to one of your hires and **returns immediately**. |
255
+ | `agent-report-parent` | workers | Sends a result to the agent that hired you. Ends your turn (`stopIterationAfterUsingThisTool`). |
256
+ | `agent-report-group` | workers | Sends a message to every other agent in your group. Does **not** end your turn. |
257
+
258
+ When `maxHired` is set it's woven into `agent-hire`'s own description, so the model learns the constraint from the tool rather than out-of-band instructions.
259
+
260
+ ## `SwarmBase`
261
+
262
+ ```ts
263
+ new SwarmBase({
264
+ tools?: MCPTool[]; // the pool; handed to bare factories in `agents`
265
+ agents: (AgentDefinition | AgentFactory)[];
266
+ default: string; // type name of the entrypoint
267
+ agentTools?: AgentToolsHandle;
268
+
269
+ aiProvider: AgentProvidersInput;
270
+ mcpClient?: MCPClient; // shared by every agent; omit and the swarm builds one
271
+ envID?: string; // for the client the swarm builds; default "SWARM"
272
+
273
+ safetyPolicies?: TextOverride; // every agent
274
+ instruction?: TextOverride; // every agent
275
+ maxMemorizeToken?: number;
276
+ features?: { executeProviderFromMCPTool?: boolean }; // gates executeProvider for the whole swarm
277
+ overrides?: Record<string, AgentOverrides>; // per type
278
+
279
+ initData?: SwarmState; // resume — see below
280
+ })
281
+ ```
282
+
283
+ ### `run(prompt, options?)` — finishes when the *swarm* is idle
284
+
285
+ This is the part that differs most from a single agent.
286
+
287
+ Concurrency follows `BaseAgent`: several `run()` calls may be in flight at once, extra prompts are absorbed into the entrypoint's current turn, and those absorbed calls resolve `false` as soon as that turn ends. The newest call stays live.
288
+
289
+ Unlike a single agent, the live call **does not resolve when the planner stops talking**. It keeps waiting while any hired agent is working, while any report or group message is in flight, and while those deliveries wake the planner for more turns. It resolves `true` only when the entire swarm is idle: nothing running, nothing queued.
290
+
291
+ ```ts
292
+ const done = await swarm.run("build the checkout flow");
293
+ // true → the whole swarm settled
294
+ // false → a newer run() call superseded this one and now owns the finish
295
+ ```
296
+
297
+ The second argument is either a stream callback or an options object:
298
+
299
+ ```ts
300
+ interface SwarmRunOptions {
301
+ agentId?: string; // who receives the prompt; default: the entrypoint agent
302
+ stream?: SwarmStreamCallback;
303
+ }
304
+
305
+ await swarm.run("build the checkout flow"); // → the planner
306
+ await swarm.run("build it", chunk => { /* … */ }); // → the planner, streamed
307
+ await swarm.run("why did you pick Redis?", { agentId: "backend-1" }); // → straight to that agent
308
+ ```
309
+
310
+ `agentId` is how you talk to one agent inside the swarm without its parent in the middle. The prompt arrives as a plain **user** message — no `[task from …]` wrapper, because it isn't from another agent — and the call still waits for the whole swarm to settle, including anything that agent sets off (a report waking its parent, for instance). Naming an agent that was fired, or one that never existed, throws.
311
+
312
+ Which call is "newest" is tracked by `SwarmBase` itself rather than read off `BaseAgent`'s return value, because the entrypoint also gets *internal* runs — every report delivered to it is one. Those must not demote the caller waiting on the swarm; only another `run()` does.
313
+
314
+ Practical consequence for planner instructions: **after dispatching work, the planner should stop and end its turn.** It isn't finished — reports arrive as new messages and wake it back up. Waiting in place just burns tokens. This is what `PLANNER_PROTOCOL` tells it.
315
+
316
+ ### Streaming
317
+
318
+ Every chunk is a normal `StreamCallback` chunk plus `agentId`:
319
+
320
+ ```ts
321
+ await swarm.run(prompt, chunk => {
322
+ console.log(chunk.agentId, chunk.role, chunk.content);
323
+ });
324
+ ```
325
+
326
+ Subscribers are live for the duration of a `run()` call and receive chunks from **every** agent, including ones a previous call hired — so a hired agent's output reaches whoever is listening now, not just the call that happened to start it.
327
+
328
+ ### Groups and reports
329
+
330
+ - **Report to parent** (`agent-report-parent`) is the upward channel: a hire's answer to the agent that hired it. It ends the reporting agent's turn and lands in the parent's conversation as a new message, waking it if idle.
331
+ - **Report to group** (`agent-report-group`) is the sideways channel: one message to every other agent in the sender's group, and to nobody else. The parent does not see it. It does *not* end the sender's turn — sharing a finding with peers is something an agent does while working.
332
+
333
+ An agent joins a group when it's hired (`agent-hire`'s `group` input). Delivery in both directions is a push: the swarm calls `run()` on each target, and `BaseAgent`'s own queue merges the message if that agent is mid-turn.
334
+
335
+ Authority rules, enforced by the tools and surfaced as errors the model can act on:
336
+
337
+ - only the agent that hired someone can prompt or fire them
338
+ - the entrypoint agent can't be fired and holds no hire slot
339
+ - a fired agent can't be prompted, and its parent is told if a hire dies with an error instead of reporting
340
+
341
+ ### Stopping
342
+
343
+ - `safeStop()` asks every **running** agent to stop at its next safe point, then waits for the swarm to go idle.
344
+ - `immediateStop()` aborts every in-flight provider call at once.
345
+
346
+ Both deliberately skip idle agents: `BaseAgent`'s stop signal is only cleared by its run loop, so signalling an agent that isn't running would either hang (`safeStop`) or silently swallow that agent's next run (`immediateStop`).
347
+
348
+ ### Providers a tool reaches for
349
+
350
+ Some tools want a model of their own mid-call — `read-image` describing a picture, a browser screenshot being read. They ask for one by *role* through `mcp.executeProvider(toolCallId, type, input)`, and in a swarm **the swarm answers**, out of the whole provider set you gave it:
351
+
352
+ ```ts
353
+ const swarm = new SwarmBase({
354
+ // ...
355
+ aiProvider: [
356
+ { type: "main", provider: writer },
357
+ { type: "image-describer", provider: vision },
358
+ ],
359
+ features: { executeProviderFromMCPTool: true }, // off by default
360
+ });
361
+ ```
362
+
363
+ A standalone `BaseAgent` answers this itself, from its own providers. Inside a swarm it never does: the swarm takes the MCP client's handler back after building each agent, so every one of these calls lands on the swarm instead. That is deliberate, and it is what makes the whole thing simple — a tool asking for an image describer wants a model with that capability, not the model belonging to whichever agent happened to call it, so nothing has to work out who was calling.
364
+
365
+ Three things follow, and they are the trade:
366
+
367
+ - **`features.executeProviderFromMCPTool` is decided at swarm level.** Setting it on a definition or in `overrides` still governs that agent's own chat, but no longer whether its tools can reach a provider. The swarm's flag is the whole gate — there is no longer a "is this call yours" check behind it, because that check needed to know the caller.
368
+ - **A definition's own `aiProvider` doesn't apply here either.** Pinning a stronger model for the `backend` type changes what that agent thinks with, not what its tools reach for.
369
+ - **The usage still lands on the calling agent.** The swarm answers the call, but the tokens were spent for one agent's tool call, so `SwarmBase` records them there — `BaseAgent.recordToolCallUsage(toolCallId, usage)` puts the entry on that call's tool-result message, exactly where a standalone agent would put it. So `agent.getFullTotalToken(type)` includes it, and `swarm.getFullTotalToken(type)` / `getFullTotalCost(type)` pick it up by summing the agents rather than counting it a second time. The usage is written from the real provider result by the swarm, never from what the tool returned, so a tool still can't fake or hide what it spent. Only a call whose tool call is already gone (it outlived it) falls into the swarm's own bucket, so nothing is ever lost.
370
+
371
+ An unknown role, or one you never registered, comes back to the tool as `this swarm has no provider for type "…"` rather than silently falling back to `main`.
372
+
373
+ ### State and resume
374
+
375
+ `getCurrentSwarmStates()` returns a `SwarmState` that round-trips straight back into `initData`:
376
+
377
+ ```ts
378
+ interface SwarmState {
379
+ default: string; // id of the entrypoint agent
380
+ agents: {
381
+ id: string;
382
+ type: string;
383
+ parentId?: string; // absent for the entrypoint
384
+ groups: string[];
385
+ state: AgentState; // the BaseAgent state you already know
386
+ }[];
387
+ }
388
+ ```
389
+
390
+ ```ts
391
+ const saved = swarm.getCurrentSwarmStates();
392
+ // …later, same roster, fresh getAgentTools() handle…
393
+ const resumed = new SwarmBase({ tools, agents, default: "planner", agentTools, aiProvider, initData: saved });
394
+ ```
395
+
396
+ With `initData`, `initData.default` (an agent **id**) names the entrypoint instead of `default` (a type name). Agent ids are readable and stable (`backend-1`, `backend-2`) so a model can type them back into `agent-prompt`; the per-type counter continues from what was restored rather than colliding with it. A saved type that isn't in the roster, or a `default` that isn't in the saved agents, throws.
397
+
398
+ **Who was fired is deliberately not saved.** `active` is a fact about a running process, not about a conversation: a swarm that died left its agents mid-task with nobody waiting on them, so carrying "this one was let go" across a restart preserves a decision whose context is gone. What is worth keeping is what each agent said and remembered. Everything therefore comes back hired, and the entrypoint agent prompts or fires again from there.
399
+
400
+ ### Reading the roster
401
+
402
+ | Method | Returns |
403
+ |---|---|
404
+ | `listAgents()` | Every agent — `agentId`, `type`, `parentId`, `groups`, `active`, `busy`, `awaitingReport`. Fired agents stay listed, with `active: false`. |
405
+ | `listTypes()` | Every hireable type with its description, ratings, and how many are working. |
406
+ | `getAgentInfo(id)` / `isEntrypoint(id)` | One row of the above, or whether an id is the entrypoint. |
407
+ | `getMainAgent()` / `getMainAgentId()` | The entrypoint `BaseAgent`, or its id. |
408
+ | `getAgent(id)` / `getAgentIds()` | One live agent, or every id. |
409
+ | `getFailures()` | Background failures — a hired agent that threw mid-task never surfaces through `run()`'s return value. |
410
+ | `getFullTotalToken(type)` / `getFullTotalCost(type)` | Usage summed across every agent, including fired ones — the swarm's whole bill for that provider role. |
411
+ | `getMCPClient()` / `ownsMCP()` | The shared client, and whether the swarm built it. |
412
+ | `close()` | Closes the connection the swarm built. No-op when you supplied the client. |
413
+
414
+ `createAgent`, `deactivateAgent` and `sendTo` are public too, but they are only what the event handlers call — raw state changes with no permission checks, no hire limit, and no message framing. Prefer the tools; reach for these only if you are writing your own tool layer.
415
+
416
+ There is deliberately **no** `hire()`, `fire()`, `prompt()` or `report()` on `SwarmBase`. Hiring and firing are decisions an agent makes, and the only way to make them is to call the matching tool — which is why every rule about who may do what lives in `src/tools/agent/` and not in the swarm.
417
+
418
+ ## Running a swarm in a sandbox
419
+
420
+ Agents that write files and run shell commands are exactly what [`MCPComputer`](./mcp-architecture.md#mcpcomputer-sandboxed-execution) is for. Because the whole swarm shares one client, sandboxing all of it is just a matter of pointing that client at the container:
421
+
422
+ ```ts
423
+ const computer = new MCPComputer("./data/test_swarm", [3000, 5173]);
424
+ await computer.start();
425
+
426
+ const agentTools = getAgentTools({ maxHired: 5 });
427
+
428
+ // The container has its own copies of these registered on the MCPServer running
429
+ // inside it. Locally these instances only carry names, for selectTools()/allowedTools.
430
+ const tools = [
431
+ ...FsTools(), ...BashTools(), ...BrowserTools(), ...TodoTools(), ...UtilTools(),
432
+ ...agentTools.tools,
433
+ ];
434
+
435
+ const mcpClient = new MCPClient("SWARM_ENV", computer.getConnection());
436
+ // The agent tools drive this swarm, so they run here — not across the sandbox boundary.
437
+ agentTools.tools.forEach(tool => mcpClient.registerTool(tool));
438
+
439
+ const swarm = new SwarmBase({
440
+ tools,
441
+ agents: DefaultAgents(),
442
+ default: "planner",
443
+ agentTools,
444
+ mcpClient,
445
+ aiProvider,
446
+ instruction: { append: computer.getInstructions() },
447
+ });
448
+
449
+ // …later
450
+ swarm.close();
451
+ await computer.stop();
452
+ ```
453
+
454
+ Four things make that work:
455
+
456
+ - **Tools route by name.** `MCPClient.callTool` checks its own registered tools first and otherwise asks its connections, so `fs-write-file` lands inside the container while `agent-hire` runs in this process. The local `FsTools()` instances are never executed here.
457
+ - **Agent tools must stay client-side.** They drive the live swarm — spawning agents, delivering messages — which only exists in the host process. Registering them on the client is also the cheap path for anything that doesn't need isolation (see [`MCPClient`](./mcp-architecture.md#mcpclient)).
458
+ - **You register the tools yourself.** Passing your own `mcpClient` means `SwarmBase` registers nothing — that's the trade for controlling the transport.
459
+ - **`computer.getInstructions()` appended swarm-wide** tells every agent where the workspace, presents, and open ports are, without any definition needing to know it might be sandboxed. This is what swarm-level `instruction: { append }` is for.
460
+
461
+ Needs Docker running and the `orbitx-sandbox:0.1` image built from `Dockerfile.sandbox`.
462
+
463
+ ## How it works under the hood
464
+
465
+ Two implementation details are worth knowing, because they're what let all of this happen without touching `BaseAgent` or `MCPClient`, and without the swarm keeping a ledger of tool calls.
466
+
467
+ **The caller comes from the turn, not from the tool call.** One `MCPClient` is shared by the whole swarm, so a tool's `execute(envID, inputs, toolCallId, mcp)` gets nothing that says who called it — `envID` and `mcp` are the same for everyone. `SwarmBase` therefore runs each agent's turn inside an `AsyncLocalStorage` context holding that agent's id (`handle.runAs(agentId, …)`), and the tools read the caller straight off it. Parallel agents each get their own store, so two reports in flight at once can't be confused. Called outside any turn, a tool refuses instead of guessing.
468
+
469
+ **Tools decide, the swarm carries out.** The handle is an `EventEmitter`. A tool checks what its caller may do — parentage, the shared hire cap, group membership — writes the message the receiving agent will see, then asks the swarm for the one thing it cannot do itself:
470
+
471
+ ```ts
472
+ // in the tool
473
+ await registry.request("agent-report-parent", agentId, { to: [parentId], message });
474
+
475
+ // in SwarmBase
476
+ agentTools.on("agent-report-parent", (agentId, { to, message }, done) => {
477
+ for (const target of to) this.sendTo(target, message);
478
+ done(); // done(error) fails the tool call instead
479
+ });
480
+ ```
481
+
482
+ `request()` resolves when the swarm acks, rejects with whatever error the swarm passes to `done`, and times out rather than leaving an agent waiting forever on a listener nobody registered. That is why `SwarmBase` has no `hire()`/`fire()`/`report()` of its own: it never decides any of it, it only spawns, deactivates, and delivers.
483
+
484
+ **The swarm takes the provider handler back.** `BaseAgent`'s constructor claims the shared client's `executeProvider` handler, which is right for an agent on its own and wrong in a swarm, so `SwarmBase` re-claims it immediately after every spawn. That is the whole mechanism — `BaseAgent`'s own handler is simply never reached while it belongs to a swarm. The one thing `BaseAgent` adds for this is `recordToolCallUsage(toolCallId, usage)` (with `hasActiveToolCall(toolCallId)` alongside it), which lets the swarm write the usage back onto the tool call that spent it. See [Providers a tool reaches for](#providers-a-tool-reaches-for).
485
+
486
+ ## Writing your own definition
487
+
488
+ ```ts
489
+ import { AgentDefinition, selectTools, WORKER_PROTOCOL } from "orbitx";
490
+ import type { AgentFactory, AgentFactoryOptions } from "orbitx";
491
+
492
+ export const DataAgent: AgentFactory = ({ tools = [], ...overrides }: AgentFactoryOptions = {}) =>
493
+ new AgentDefinition({
494
+ type: "data",
495
+ description: "Analytics and data modelling: schemas, migrations, query performance, reporting pipelines.",
496
+ rating: { data: 10, backend: 6, frontend: 1 },
497
+ skills: [MyDataSkill()],
498
+ tools: selectTools(tools, ["fs-*", "bash-*", "agent-report-parent", "agent-report-group"]),
499
+ instruction: `You are a data engineer working inside a swarm.\n\n…\n\n${WORKER_PROTOCOL}`,
500
+ }).with(overrides);
501
+ ```
502
+
503
+ Then drop it in the roster: `agents: [PlannerAgent, DataAgent]`. The `.with(overrides)` at the end is what makes your factory accept the same instruction/policy/provider overrides as the shipped ones.