orbitx 0.2.9 → 0.3.2
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-ask.d.ts +28 -0
- package/dist/core/ai-ask.d.ts.map +1 -0
- package/dist/core/ai-ask.js +80 -0
- package/dist/core/ai-ask.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-auto-execution-policy.d.ts +22 -0
- package/dist/core/mcp-auto-execution-policy.d.ts.map +1 -0
- package/dist/core/mcp-auto-execution-policy.js +85 -0
- package/dist/core/mcp-auto-execution-policy.js.map +1 -0
- 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/stateless-agent.d.ts +30 -0
- package/dist/core/stateless-agent.d.ts.map +1 -0
- package/dist/core/stateless-agent.js +79 -0
- package/dist/core/stateless-agent.js.map +1 -0
- 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 +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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/ai-ask.test.d.ts +2 -0
- package/dist/tests/core/ai-ask.test.d.ts.map +1 -0
- package/dist/tests/core/ai-ask.test.js +101 -0
- package/dist/tests/core/ai-ask.test.js.map +1 -0
- package/dist/tests/core/mcp-auto-execution-policy.test.d.ts +2 -0
- package/dist/tests/core/mcp-auto-execution-policy.test.d.ts.map +1 -0
- package/dist/tests/core/mcp-auto-execution-policy.test.js +107 -0
- package/dist/tests/core/mcp-auto-execution-policy.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/stateless-agent.test.d.ts +2 -0
- package/dist/tests/core/stateless-agent.test.d.ts.map +1 -0
- package/dist/tests/core/stateless-agent.test.js +73 -0
- package/dist/tests/core/stateless-agent.test.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 +6 -0
- package/dist/tests/index.d.ts.map +1 -1
- package/dist/tests/index.js +6 -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 +249 -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 +71 -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/dist/core/image-describer.d.ts +0 -19
- package/dist/core/image-describer.d.ts.map +0 -1
- package/dist/core/image-describer.js +0 -37
- package/dist/core/image-describer.js.map +0 -1
- package/dist/core/worker-agent.d.ts +0 -43
- package/dist/core/worker-agent.d.ts.map +0 -1
- package/dist/core/worker-agent.js +0 -40
- package/dist/core/worker-agent.js.map +0 -1
- package/dist/skills/shopping.d.ts +0 -4
- package/dist/skills/shopping.d.ts.map +0 -1
- package/dist/skills/shopping.js +0 -71
- package/dist/skills/shopping.js.map +0 -1
- package/dist/tools/agent/interaction.d.ts +0 -25
- package/dist/tools/agent/interaction.d.ts.map +0 -1
- package/dist/tools/agent/interaction.js +0 -14
- package/dist/tools/agent/interaction.js.map +0 -1
- package/dist/tools/agent/list.d.ts +0 -5
- package/dist/tools/agent/list.d.ts.map +0 -1
- package/dist/tools/agent/list.js +0 -16
- package/dist/tools/agent/list.js.map +0 -1
- package/dist/tools/agent/report.d.ts +0 -12
- package/dist/tools/agent/report.d.ts.map +0 -1
- package/dist/tools/agent/report.js +0 -31
- package/dist/tools/agent/report.js.map +0 -1
- package/dist/tools/bash/interaction.d.ts +0 -29
- package/dist/tools/bash/interaction.d.ts.map +0 -1
- package/dist/tools/bash/interaction.js +0 -13
- package/dist/tools/bash/interaction.js.map +0 -1
- package/dist/tools/browser/interaction.d.ts +0 -49
- package/dist/tools/browser/interaction.d.ts.map +0 -1
- package/dist/tools/browser/interaction.js +0 -14
- package/dist/tools/browser/interaction.js.map +0 -1
- package/dist/tools/fs/interaction.d.ts +0 -37
- package/dist/tools/fs/interaction.d.ts.map +0 -1
- package/dist/tools/fs/interaction.js +0 -13
- package/dist/tools/fs/interaction.js.map +0 -1
- package/dist/tools/present/interaction.d.ts +0 -17
- package/dist/tools/present/interaction.d.ts.map +0 -1
- package/dist/tools/present/interaction.js +0 -15
- package/dist/tools/present/interaction.js.map +0 -1
- package/dist/tools/question-answer/interaction.d.ts +0 -14
- package/dist/tools/question-answer/interaction.d.ts.map +0 -1
- package/dist/tools/question-answer/interaction.js +0 -13
- package/dist/tools/question-answer/interaction.js.map +0 -1
- package/dist/tools/todo/interaction.d.ts +0 -30
- package/dist/tools/todo/interaction.d.ts.map +0 -1
- package/dist/tools/todo/interaction.js +0 -13
- package/dist/tools/todo/interaction.js.map +0 -1
package/docs/agents.md
CHANGED
|
@@ -1,228 +1,249 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- **
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
## `BaseAgent` vs `SimpleAgent` vs `StatelessAgent`
|
|
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
|
+
- **`StatelessAgent`** (`src/core/stateless-agent.ts`) is not a loop at all — one `input` in, one `chat()` call out (see [`StatelessAgent`](#statelessagent) below). Use this for input checking, classification, or validation, where `BaseAgent`'s multi-turn loop and memory compaction are unnecessary overhead.
|
|
8
|
+
- **`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.
|
|
9
|
+
|
|
10
|
+
## `BaseAgent` constructor
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
new BaseAgent({
|
|
14
|
+
instruction: string;
|
|
15
|
+
safetyPolicies?: string; // default: "" — see "Guardrail system prompt" below
|
|
16
|
+
allowedTools: MCPTool<any>[];
|
|
17
|
+
aiProvider: AIProvider | AgentProviderEntry[]; // see Providers doc
|
|
18
|
+
mcpClient: MCPClient;
|
|
19
|
+
skills?: Skill[];
|
|
20
|
+
maxMemorizeToken?: number; // default: derived from the main provider's contextWindow * safeUsageRatio
|
|
21
|
+
initData?: {
|
|
22
|
+
compactMemory: string;
|
|
23
|
+
retiredMessages: Message[]; // complete, ever-appended history of retired messages — each Message carries its own usage/timestamp
|
|
24
|
+
messagesCompact: Message[]; // the working context currently replayed to the provider every turn
|
|
25
|
+
};
|
|
26
|
+
features?: {
|
|
27
|
+
executeProviderFromMCPTool?: boolean; // default: false — see "executeProvider feature flag" below
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Guardrail system prompt
|
|
33
|
+
|
|
34
|
+
`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.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
### `executeProvider` feature flag
|
|
39
|
+
|
|
40
|
+
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).
|
|
41
|
+
|
|
42
|
+
`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`.
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
## The run loop
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await agent.run(prompt: string, streamCallback?: StreamCallback): Promise<boolean>;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each call to `run()`:
|
|
53
|
+
|
|
54
|
+
1. Pushes the user prompt onto the message history and emits it as a `role: "user"` stream chunk.
|
|
55
|
+
2. Sends `[system, ...history]` plus the full tool schema list to the main provider's `chat()`.
|
|
56
|
+
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.
|
|
57
|
+
4. Repeats until the model's turn produces no tool calls (or a tool marked `stopIterationAfterUsingThisTool` fired — see [Tools](./tools.md)).
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
### Calling `run()` again mid-flight: prompt coalescing, not truncation
|
|
62
|
+
|
|
63
|
+
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:
|
|
64
|
+
|
|
65
|
+
- **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).
|
|
66
|
+
- **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.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
### The resolved boolean: "was my call the one that actually finished last?"
|
|
71
|
+
|
|
72
|
+
`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`.
|
|
73
|
+
|
|
74
|
+
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:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
if (await agent.run(prompt, streamCallback)) {
|
|
78
|
+
console.log("agent finished"); // fires exactly once, when the agent is genuinely idle
|
|
79
|
+
}
|
|
80
|
+
// without the check: could log "finished" 2-3 times, with the last one arriving
|
|
81
|
+
// minutes after the others if the final queued call drove a long tool-heavy turn
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
`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.
|
|
87
|
+
|
|
88
|
+
`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`.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
`stop()` is a deprecated alias for `safeStop()` and will be removed in the 1.0.0 major release.
|
|
95
|
+
|
|
96
|
+
### Native tool-calling is required
|
|
97
|
+
|
|
98
|
+
`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.
|
|
99
|
+
|
|
100
|
+
## Memory compaction
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
Calling `compact_memory`:
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
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.
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
Like the forced-compaction nudge below, this confirmation message (and its matching `StreamCallback` chunk) is marked `hidden: true` — see the note two paragraphs down.
|
|
113
|
+
|
|
114
|
+
### `continue_iterating`: letting the model opt out of the next round-trip
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
`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.
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
`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.
|
|
127
|
+
|
|
128
|
+
## Persisting and resuming state
|
|
129
|
+
|
|
130
|
+
`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).
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+
### History repair on load
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
138
|
+
## Token accounting
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
Each entry in `Message.usage` is one of:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{ type: ProviderType; model: string; unit: "tokens"; inputMissTokens: number; inputCacheTokens: number; outputTokens: number }
|
|
146
|
+
{ type: ProviderType; model: string; unit: "cost"; cost: number }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`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.
|
|
150
|
+
|
|
151
|
+
**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).
|
|
152
|
+
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
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).
|
|
156
|
+
|
|
157
|
+
`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.
|
|
158
|
+
|
|
159
|
+
**The four read methods:**
|
|
160
|
+
|
|
161
|
+
- `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.
|
|
162
|
+
- `getCompactTotalCost(type: ProviderType)` / `getFullTotalCost(type: ProviderType)` — return a plain `number`, summing `unit: "cost"` entries matching `type` over the same two scopes.
|
|
163
|
+
|
|
164
|
+
## Instructions, skills, and memory in the system prompt
|
|
165
|
+
|
|
166
|
+
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.
|
|
167
|
+
|
|
168
|
+
## `StatelessAgent`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
new StatelessAgent({
|
|
172
|
+
instruction: string;
|
|
173
|
+
safetyPolicies?: string; // default: "" — sent as its own leading system message, same convention as BaseAgent
|
|
174
|
+
aiProvider: AIProvider | AgentProviderEntry[];
|
|
175
|
+
mcpClient?: MCPClient; // required only if allowedTools is non-empty — bring your own, same as BaseAgent
|
|
176
|
+
allowedTools?: MCPTool<any>[]; // default: []
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Unlike `BaseAgent`, there is no `run` loop, no `messagesCompact`/`retiredMessages`, no `compact_memory` tool, and no `getCurrentAgentStates()` — an instance holds only its fixed configuration (instruction, provider, allowed tools), never conversation state. Because of that, one instance is meant to be constructed once and reused: `run()` can be called any number of times, including concurrently, and calls never see each other's input or output — each builds its own message array from scratch (or continues one you hand back in — see below).
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const result = await agent.run(input: string, streamCallback?: StreamCallback, previousMessages?: Message[]): Promise<StatelessAgentResult>;
|
|
184
|
+
// { content: string; toolCalls: DispatchedToolCall[]; usage: MessageUsage[]; messages: Message[] }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`run()` makes exactly **one** `chat()` call — never a second, even when the model requests tool calls:
|
|
188
|
+
|
|
189
|
+
1. `previousMessages` omitted (the default): the message array is built fresh as `[system(safetyPolicies), system(instruction), user(input)]`. Given instead, `input` is appended as one more `user` message onto it — this is how you continue an exchange without StatelessAgent retaining any state of its own; see `result.messages` below.
|
|
190
|
+
2. That one `chat()` call is made. If the model requested tool calls, every one of them is dispatched right away (via the `MCPClient`/`allowedTools` this agent was built with — same dispatch semantics as `BaseAgent#dispatchTool`: an error from a tool, or a tool name that isn't allowed, comes back as an `Error: ...` result string rather than throwing), and their `role: "tool"` result messages are appended to `messages`. `run()` then returns immediately — there is no automatic follow-up `chat()` call to fold those results into a final answer.
|
|
191
|
+
|
|
192
|
+
If the model's first turn requests no tools, `run()` still resolves after that one call, with `toolCalls: []`.
|
|
193
|
+
|
|
194
|
+
### Continuing an exchange: `result.messages` / `previousMessages`
|
|
195
|
+
|
|
196
|
+
Since `run()` never makes a second `chat()` call on its own, `result.messages` is how you pick up where it left off: it's the complete array built for that call — the two leading system messages, the `user` turn, the resulting `assistant` turn, and any `tool` result messages — in the order the provider needs them replayed. Call `run()` again with it as `previousMessages` (and whatever `input` makes sense next — often just a nudge like `"continue"`, or nothing new to add beyond letting the model see the tool results) to get the model's next turn, e.g. its synthesized answer after seeing what a tool returned. Each such call is still exactly one `chat()` call; a caller that wants BaseAgent-style looping-until-no-more-tool-calls behavior builds that loop itself by chaining `run()` calls this way, rather than getting it for free.
|
|
197
|
+
|
|
198
|
+
### Token accounting
|
|
199
|
+
|
|
200
|
+
`result.usage` is exactly one `MessageUsage` entry per `run()` call — there's no running total to query since there's no persisted state between calls; sum `usage` yourself across calls (e.g. across a chain of `previousMessages`-linked calls) if you need a lifetime total.
|
|
201
|
+
|
|
202
|
+
## `AIASK`
|
|
203
|
+
|
|
204
|
+
`AIASK` (`src/core/ai-ask.ts`) asks the model for exactly one answer in a validated JSON shape. It's built on `StatelessAgent`, not a replacement for it — internally it registers a single `submit_answer` tool (built from `outputStructure`) on its own private, in-process `MCPClient` (same `MCPConnection` + `MCPServer` wiring `SimpleAgent` uses) and drives a `StatelessAgent` with it. There's nothing to wire up from outside:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
new AIASK({
|
|
208
|
+
instruction: string;
|
|
209
|
+
aiModel: AIProvider | AgentProviderEntry[];
|
|
210
|
+
outputStructure: { name: string; type: "number" | "string" | "boolean" | "object" | "array"; description: string; required?: boolean }[];
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
No `mcpClient`, no `allowedTools`, and deliberately no `safetyPolicies` field — for a single narrow structured-answer call, `instruction` *is* the whole system prompt (fold any guardrail text straight into it) rather than a separate cache-stable prefix, which only earns its keep across many turns.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const result = await ask.run(input: string): Promise<AIAskResult>;
|
|
218
|
+
// { output: Record<string, any>; valid: boolean; errors?: string[]; attempts: number; usage: MessageUsage[] }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Each field in `outputStructure` becomes one input on the `submit_answer` tool (`required` defaults to `true`). After the model calls it (or fails to), the returned fields are validated against `outputStructure` — every `required` field present, and each field's JS type (`typeof`, with `Array.isArray` for `"array"` and a plain-object check for `"object"`) matching what was declared. Replying in plain text instead of calling the tool at all counts as a failed attempt too.
|
|
222
|
+
|
|
223
|
+
A failed attempt (validation errors, or no tool call) triggers a retry: `run()` calls the underlying `StatelessAgent` again, up to **3 attempts total**, with the validation errors folded into the next prompt so the model can see exactly what was wrong and self-correct. The first attempt that validates returns immediately with `valid: true` and `errors` unset. If all 3 attempts fail, `run()` returns rather than throwing — `valid: false`, `errors` from the last attempt, `attempts: 3`, and `output` holding whatever fields the last attempt did manage to submit (possibly `{}` if it never called the tool at all).
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const ask = new AIASK({
|
|
227
|
+
instruction: "Classify the input as spam or not.",
|
|
228
|
+
aiModel,
|
|
229
|
+
outputStructure: [
|
|
230
|
+
{ name: "spam", type: "boolean", description: "true if this is spam" },
|
|
231
|
+
{ name: "reason", type: "string", description: "one-sentence justification" },
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const { output, valid, attempts } = await ask.run(userMessage);
|
|
236
|
+
if (valid) {
|
|
237
|
+
// output is { spam: boolean, reason: string }
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`result.usage` accumulates every `chat()` call across every attempt (up to 3, one per `StatelessAgent.run()` call), not just the winning one.
|
|
242
|
+
|
|
243
|
+
## Multi-agent
|
|
244
|
+
|
|
245
|
+
Multiple agents that hire, task, and report to each other live in [Swarm](./swarm.md) — `SwarmBase`, `getAgentTools()`, and `AgentDefinition`.
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
|
|
249
|
+
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.
|