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/streaming.md
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
# Streaming
|
|
2
|
-
|
|
3
|
-
`agent.run(prompt, streamCallback)` and `AIProvider.chat(messages, streamCallback, tools)` both accept an optional `StreamCallback`:
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
type StreamCallback = (chunk: {
|
|
7
|
-
role: "assistant" | "tool" | "user";
|
|
8
|
-
content: string;
|
|
9
|
-
done: boolean;
|
|
10
|
-
toolCalls?: ToolCallRequest[]; // a single just-finished call, as a 1-element array — see "Incremental tool-call chunks" below
|
|
11
|
-
toolCallId?: string; // on "tool"-role chunks
|
|
12
|
-
toolName?: string; // on "tool"-role chunks
|
|
13
|
-
thinking?: string; // reasoning-text delta, mirroring `content` — see "Thinking chunks" below
|
|
14
|
-
usage?: { inputMissTokens: number; inputCacheTokens: number; outputTokens: number }; // see "Usage chunks" below
|
|
15
|
-
hidden?: boolean; // true on messages BaseAgent injected itself
|
|
16
|
-
}) => Promise<void> | void;
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## What you actually receive, turn by turn
|
|
20
|
-
|
|
21
|
-
1. **`role: "user"`** — one chunk, `done: true`, echoing the prompt that was just added to history (either the one you passed to `run()`, or a queued one — see [Agents](./agents.md#the-run-loop)). One exception: the forced-compaction nudge (see [Agents](./agents.md#memory-compaction)) is also a `role: "user"` chunk, but carries `hidden: true` since it's `BaseAgent` talking to the model, not the actual user — check that flag before showing a `"user"` chunk to an end user.
|
|
22
|
-
2. **`role: "assistant"`** — the model's reply, streamed incrementally: zero or more `done: false` chunks each carrying the next slice of `content` as it arrives from the provider, followed by one final `done: true` chunk. `content` is delta text, not the accumulated total — append it yourself if you need the running message.
|
|
23
|
-
- If the model requested tool calls this turn, each one is emitted individually, mid-stream, as soon as *that* call finishes generating — `toolCalls: [thatOneCall]` on a `done: false` chunk. See "Incremental tool-call chunks" below.
|
|
24
|
-
3. **`role: "tool"`** — one chunk per dispatched tool call, `done: true`, with `content` set to the tool's result text and `toolCallId`/`toolName` identifying which call it answers. Emitted after the tool has actually finished executing (there is no streaming/progress signal for tool execution itself through this callback — see below for how to get that separately).
|
|
25
|
-
|
|
26
|
-
A full run of "call one tool, then answer" therefore looks like:
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
{ role: "user", content: "<prompt>", done: true }
|
|
30
|
-
{ role: "assistant", content: "<partial>", done: false } // repeated
|
|
31
|
-
{ role: "assistant", content: "", done: false, toolCalls: [{...}] }
|
|
32
|
-
{ role: "assistant", content: "", done: true }
|
|
33
|
-
{ role: "tool", content: "<result>", done: true, toolCallId, toolName }
|
|
34
|
-
{ role: "assistant", content: "<partial>", done: false } // repeated
|
|
35
|
-
{ role: "assistant", content: "", done: true }
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Note the final `done: true` chunk for the tool-call turn carries no `toolCalls` — every call this turn was already streamed individually by the time it arrives, so resending the full list would just be a duplicate. If you need the complete, aggregated list for a turn (e.g. you're driving `AIProvider.chat()` directly rather than going through `agent.run()`), it's on the returned `ChatResponse.toolCalls` instead — see below.
|
|
39
|
-
|
|
40
|
-
## Incremental tool-call chunks
|
|
41
|
-
|
|
42
|
-
Deciding *whether* to call a tool and building its arguments is itself something the model streams token by token — so as soon as one call's arguments are fully generated, that call is emitted on its own as `toolCalls: [thatOneCall]`, without waiting for the rest of the turn (more tool calls, or trailing text) to finish too. This is purely observational: it exists so a consumer can show "the model just decided to call `X`" the moment it happens.
|
|
43
|
-
|
|
44
|
-
**It does not change when a tool actually runs.** Dispatch is still all-or-nothing at the end of the turn: `BaseAgent` only executes tool calls after `AIProvider.chat()` fully resolves and returns the complete `ChatResponse.toolCalls` array — nothing is ever called early just because its single-call chunk arrived first. If you want to reconcile the two: the incremental `toolCalls: [...]` chunks tell you a call was *decided*, in the order the model produced them; the eventual `role: "tool"` result chunks tell you a call was *executed*, in whatever order `BaseAgent` dispatches them.
|
|
45
|
-
|
|
46
|
-
Detection differs by provider, since not every API has an explicit "this call is done" signal:
|
|
47
|
-
|
|
48
|
-
| Provider | How completion is detected |
|
|
49
|
-
|---|---|
|
|
50
|
-
| Anthropic | Native `content_block_stop` event — exact and immediate. |
|
|
51
|
-
| OpenAI / DeepSeek | Inferred: the API streams one call's arguments at a time with no end marker, so a call is treated as finished the instant the *next* call's first delta (carrying a fresh `id`) arrives, or the stream ends (for the last/only call). |
|
|
52
|
-
| Ollama | Ollama sends each tool call already fully formed (not built up token by token), so it's emitted the moment it's seen — deduplicated in case the same call reappears in a later chunk. |
|
|
53
|
-
|
|
54
|
-
## Usage chunks
|
|
55
|
-
|
|
56
|
-
Every `assistant`-role chunk carries `usage` — not just the ones with a fresh number to report. Where a value isn't known yet, it's an explicit `0`, never an absent/undefined field, so consuming code can always read `chunk.usage.outputTokens` etc. without separately guarding for `usage` or its fields being missing. This is the same `streamCallback` passed into `run()`, forwarded straight through to the provider, so external code (e.g. something watching an external credit/cost budget, deciding when to call `immediateStop()`) sees it too — no separate channel.
|
|
57
|
-
|
|
58
|
-
Every number here is real, straight from whatever the provider reports — nothing in this SDK estimates or guesses a token count. What differs is *when* each field stops being a placeholder `0` and starts reflecting the API's real number:
|
|
59
|
-
|
|
60
|
-
| Provider | Mid-stream `usage` | Final `usage` (on `done: true`) |
|
|
61
|
-
|---|---|---|
|
|
62
|
-
| Anthropic | `inputMissTokens`/`inputCacheTokens` are real from the very first chunk (before any content) — that's simply when its protocol first has them, not something computed early on purpose. `outputTokens` updates on every delta. | Same numbers, just the last update. |
|
|
63
|
-
| OpenAI | All three fields are `0` on every chunk before the last one (`include_usage` is honored correctly, but only delivers on completion — there's nothing to report early). | Real, billed numbers for all three fields. If the call is cut off before this chunk arrives, `inputMissTokens`/`inputCacheTokens`/`outputTokens` come back `0` on `ChatResponse` too — the API never computed them, so there's nothing to report. |
|
|
64
|
-
| DeepSeek | Same as OpenAI — `0` on every chunk before the last one. | Real, billed numbers, including its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` for the cache split. Same `0`-on-abort behavior as OpenAI if the call is cut off first. |
|
|
65
|
-
| Ollama | All three fields are `0` on every chunk before the last one — no live signal exists for this provider at all. | Real. |
|
|
66
|
-
|
|
67
|
-
Practical implication: for Anthropic and DeepSeek you get a live, reactable signal before a turn finishes (immediately for Anthropic's input side, throughout for either provider's output side); for OpenAI there's no live signal at all today — you only find out after the turn finishes, or get a hard `0` if it was cut off first.
|
|
68
|
-
|
|
69
|
-
## Minimal consumer
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
let oldRole: "assistant" | "tool" | "user" | undefined;
|
|
73
|
-
|
|
74
|
-
agent.run("what is current time?", (chunk) => {
|
|
75
|
-
if (oldRole !== chunk.role) {
|
|
76
|
-
process.stdout.write(`\n${chunk.role}: `);
|
|
77
|
-
oldRole = chunk.role;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (chunk.thinking) {
|
|
81
|
-
process.stdout.write(`\x1b[2m${chunk.thinking}\x1b[0m`); // dim, to visually separate it from the answer
|
|
82
|
-
} else {
|
|
83
|
-
process.stdout.write(chunk.content);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (chunk.toolCalls?.length) {
|
|
87
|
-
console.log("decided to call:", chunk.toolCalls[0]);
|
|
88
|
-
}
|
|
89
|
-
if (chunk.role === "tool") {
|
|
90
|
-
console.log({ toolCallId: chunk.toolCallId, toolName: chunk.toolName });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (chunk.done) {
|
|
94
|
-
process.stdout.write("\n\n");
|
|
95
|
-
oldRole = undefined;
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Thinking chunks
|
|
101
|
-
|
|
102
|
-
When a provider is constructed with `thinkEffort` (see [Providers](./providers.md#think-effort)) and it can actually surface reasoning text (`getCapabilities().supportsThinking`), that text streams incrementally too — as `role: "assistant"` chunks carrying their delta on `thinking` instead of `content`, interleaved *before* the model's normal answer chunks in the same turn:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
{ role: "assistant", content: "", done: false, thinking: "<reasoning...>" } // repeated
|
|
106
|
-
{ role: "assistant", content: "<answer...>", done: false } // thinking absent from here on
|
|
107
|
-
{ role: "assistant", content: "", done: true }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
`thinking` mirrors `content`: a chunk carries text on exactly one of the two, never both, and `thinking` is only ever present on non-`done` chunks — the final `done: true` chunk never carries it. Once accumulated, the full reasoning text for the turn is also available non-streaming on `ChatResponse.thinking` / `Message.thinking` (e.g. for logging or persisting alongside the answer).
|
|
111
|
-
|
|
112
|
-
Support varies by provider — set `thinkEffort` and check `getCapabilities().supportsThinking` before assuming thinking chunks will actually arrive:
|
|
113
|
-
|
|
114
|
-
| Provider | Streams `thinking` chunks? |
|
|
115
|
-
|---|---|
|
|
116
|
-
| Anthropic | Yes — native `thinking_delta` events. |
|
|
117
|
-
| DeepSeek | Yes — `delta.reasoning_content` on reasoner models. |
|
|
118
|
-
| Ollama | Yes — `message.thinking` on models that support it. |
|
|
119
|
-
| OpenAI | **No.** `thinkEffort` is still honored server-side (via `reasoning_effort`) on reasoning-capable models, but the Chat Completions API this provider uses never returns the reasoning text itself — there's nothing to stream. |
|
|
120
|
-
|
|
121
|
-
### `thinking` vs `providerThinking` on `Message`/`ChatResponse`
|
|
122
|
-
|
|
123
|
-
Once a turn finishes, its accumulated reasoning is available two ways on both `ChatResponse` (returned from `AIProvider.chat()`) and `Message` (as stored in an agent's history):
|
|
124
|
-
|
|
125
|
-
- **`thinking?: string`** — the plain reasoning text, for display or logging. Never required by any provider's API; read-only from your side.
|
|
126
|
-
- **`providerThinking?: any[]`** — opaque, provider-specific data (currently only Anthropic populates this: signed `thinking` content blocks). When a turn both thinks and calls a tool, Anthropic's API requires the exact signed thinking block(s) from that turn to be replayed verbatim on the next request or it rejects the call. `BaseAgent` carries this field through automatically when it builds the next turn's message history, so if you're using `BaseAgent`/`SimpleAgent` you never touch it directly. It only matters if you're driving an `AIProvider` yourself (see [Providers](./providers.md)) — in that case, copy `providerThinking` from the response straight onto the `Message` you push for that assistant turn, unchanged.
|
|
127
|
-
|
|
128
|
-
## What's *not* in the stream today
|
|
129
|
-
|
|
130
|
-
- **No live per-tool progress channel through `StreamCallback`.** There's no built-in way to observe a long-running tool call (browser navigation, a bash process) mid-flight — `execute()` runs to completion and its return value is the only thing surfaced.
|
|
131
|
-
- **Tool calls still only appear once, fully assembled** — see above. `thinkEffort`/thinking chunks don't change that.
|
|
132
|
-
|
|
133
|
-
If your use case depends on either of these, check the project's issue tracker / recent changes before assuming they're unavailable — this area of the SDK is actively evolving.
|
|
1
|
+
# Streaming
|
|
2
|
+
|
|
3
|
+
`agent.run(prompt, streamCallback)` and `AIProvider.chat(messages, streamCallback, tools)` both accept an optional `StreamCallback`:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
type StreamCallback = (chunk: {
|
|
7
|
+
role: "assistant" | "tool" | "user";
|
|
8
|
+
content: string;
|
|
9
|
+
done: boolean;
|
|
10
|
+
toolCalls?: ToolCallRequest[]; // a single just-finished call, as a 1-element array — see "Incremental tool-call chunks" below
|
|
11
|
+
toolCallId?: string; // on "tool"-role chunks
|
|
12
|
+
toolName?: string; // on "tool"-role chunks
|
|
13
|
+
thinking?: string; // reasoning-text delta, mirroring `content` — see "Thinking chunks" below
|
|
14
|
+
usage?: { inputMissTokens: number; inputCacheTokens: number; outputTokens: number }; // see "Usage chunks" below
|
|
15
|
+
hidden?: boolean; // true on messages BaseAgent injected itself
|
|
16
|
+
}) => Promise<void> | void;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What you actually receive, turn by turn
|
|
20
|
+
|
|
21
|
+
1. **`role: "user"`** — one chunk, `done: true`, echoing the prompt that was just added to history (either the one you passed to `run()`, or a queued one — see [Agents](./agents.md#the-run-loop)). One exception: the forced-compaction nudge (see [Agents](./agents.md#memory-compaction)) is also a `role: "user"` chunk, but carries `hidden: true` since it's `BaseAgent` talking to the model, not the actual user — check that flag before showing a `"user"` chunk to an end user.
|
|
22
|
+
2. **`role: "assistant"`** — the model's reply, streamed incrementally: zero or more `done: false` chunks each carrying the next slice of `content` as it arrives from the provider, followed by one final `done: true` chunk. `content` is delta text, not the accumulated total — append it yourself if you need the running message.
|
|
23
|
+
- If the model requested tool calls this turn, each one is emitted individually, mid-stream, as soon as *that* call finishes generating — `toolCalls: [thatOneCall]` on a `done: false` chunk. See "Incremental tool-call chunks" below.
|
|
24
|
+
3. **`role: "tool"`** — one chunk per dispatched tool call, `done: true`, with `content` set to the tool's result text and `toolCallId`/`toolName` identifying which call it answers. Emitted after the tool has actually finished executing (there is no streaming/progress signal for tool execution itself through this callback — see below for how to get that separately).
|
|
25
|
+
|
|
26
|
+
A full run of "call one tool, then answer" therefore looks like:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
{ role: "user", content: "<prompt>", done: true }
|
|
30
|
+
{ role: "assistant", content: "<partial>", done: false } // repeated
|
|
31
|
+
{ role: "assistant", content: "", done: false, toolCalls: [{...}] }
|
|
32
|
+
{ role: "assistant", content: "", done: true }
|
|
33
|
+
{ role: "tool", content: "<result>", done: true, toolCallId, toolName }
|
|
34
|
+
{ role: "assistant", content: "<partial>", done: false } // repeated
|
|
35
|
+
{ role: "assistant", content: "", done: true }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Note the final `done: true` chunk for the tool-call turn carries no `toolCalls` — every call this turn was already streamed individually by the time it arrives, so resending the full list would just be a duplicate. If you need the complete, aggregated list for a turn (e.g. you're driving `AIProvider.chat()` directly rather than going through `agent.run()`), it's on the returned `ChatResponse.toolCalls` instead — see below.
|
|
39
|
+
|
|
40
|
+
## Incremental tool-call chunks
|
|
41
|
+
|
|
42
|
+
Deciding *whether* to call a tool and building its arguments is itself something the model streams token by token — so as soon as one call's arguments are fully generated, that call is emitted on its own as `toolCalls: [thatOneCall]`, without waiting for the rest of the turn (more tool calls, or trailing text) to finish too. This is purely observational: it exists so a consumer can show "the model just decided to call `X`" the moment it happens.
|
|
43
|
+
|
|
44
|
+
**It does not change when a tool actually runs.** Dispatch is still all-or-nothing at the end of the turn: `BaseAgent` only executes tool calls after `AIProvider.chat()` fully resolves and returns the complete `ChatResponse.toolCalls` array — nothing is ever called early just because its single-call chunk arrived first. If you want to reconcile the two: the incremental `toolCalls: [...]` chunks tell you a call was *decided*, in the order the model produced them; the eventual `role: "tool"` result chunks tell you a call was *executed*, in whatever order `BaseAgent` dispatches them.
|
|
45
|
+
|
|
46
|
+
Detection differs by provider, since not every API has an explicit "this call is done" signal:
|
|
47
|
+
|
|
48
|
+
| Provider | How completion is detected |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Anthropic | Native `content_block_stop` event — exact and immediate. |
|
|
51
|
+
| OpenAI / DeepSeek | Inferred: the API streams one call's arguments at a time with no end marker, so a call is treated as finished the instant the *next* call's first delta (carrying a fresh `id`) arrives, or the stream ends (for the last/only call). |
|
|
52
|
+
| Ollama | Ollama sends each tool call already fully formed (not built up token by token), so it's emitted the moment it's seen — deduplicated in case the same call reappears in a later chunk. |
|
|
53
|
+
|
|
54
|
+
## Usage chunks
|
|
55
|
+
|
|
56
|
+
Every `assistant`-role chunk carries `usage` — not just the ones with a fresh number to report. Where a value isn't known yet, it's an explicit `0`, never an absent/undefined field, so consuming code can always read `chunk.usage.outputTokens` etc. without separately guarding for `usage` or its fields being missing. This is the same `streamCallback` passed into `run()`, forwarded straight through to the provider, so external code (e.g. something watching an external credit/cost budget, deciding when to call `immediateStop()`) sees it too — no separate channel.
|
|
57
|
+
|
|
58
|
+
Every number here is real, straight from whatever the provider reports — nothing in this SDK estimates or guesses a token count. What differs is *when* each field stops being a placeholder `0` and starts reflecting the API's real number:
|
|
59
|
+
|
|
60
|
+
| Provider | Mid-stream `usage` | Final `usage` (on `done: true`) |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Anthropic | `inputMissTokens`/`inputCacheTokens` are real from the very first chunk (before any content) — that's simply when its protocol first has them, not something computed early on purpose. `outputTokens` updates on every delta. | Same numbers, just the last update. |
|
|
63
|
+
| OpenAI | All three fields are `0` on every chunk before the last one (`include_usage` is honored correctly, but only delivers on completion — there's nothing to report early). | Real, billed numbers for all three fields. If the call is cut off before this chunk arrives, `inputMissTokens`/`inputCacheTokens`/`outputTokens` come back `0` on `ChatResponse` too — the API never computed them, so there's nothing to report. |
|
|
64
|
+
| DeepSeek | Same as OpenAI — `0` on every chunk before the last one. | Real, billed numbers, including its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` for the cache split. Same `0`-on-abort behavior as OpenAI if the call is cut off first. |
|
|
65
|
+
| Ollama | All three fields are `0` on every chunk before the last one — no live signal exists for this provider at all. | Real. |
|
|
66
|
+
|
|
67
|
+
Practical implication: for Anthropic and DeepSeek you get a live, reactable signal before a turn finishes (immediately for Anthropic's input side, throughout for either provider's output side); for OpenAI there's no live signal at all today — you only find out after the turn finishes, or get a hard `0` if it was cut off first.
|
|
68
|
+
|
|
69
|
+
## Minimal consumer
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
let oldRole: "assistant" | "tool" | "user" | undefined;
|
|
73
|
+
|
|
74
|
+
agent.run("what is current time?", (chunk) => {
|
|
75
|
+
if (oldRole !== chunk.role) {
|
|
76
|
+
process.stdout.write(`\n${chunk.role}: `);
|
|
77
|
+
oldRole = chunk.role;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (chunk.thinking) {
|
|
81
|
+
process.stdout.write(`\x1b[2m${chunk.thinking}\x1b[0m`); // dim, to visually separate it from the answer
|
|
82
|
+
} else {
|
|
83
|
+
process.stdout.write(chunk.content);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (chunk.toolCalls?.length) {
|
|
87
|
+
console.log("decided to call:", chunk.toolCalls[0]);
|
|
88
|
+
}
|
|
89
|
+
if (chunk.role === "tool") {
|
|
90
|
+
console.log({ toolCallId: chunk.toolCallId, toolName: chunk.toolName });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (chunk.done) {
|
|
94
|
+
process.stdout.write("\n\n");
|
|
95
|
+
oldRole = undefined;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Thinking chunks
|
|
101
|
+
|
|
102
|
+
When a provider is constructed with `thinkEffort` (see [Providers](./providers.md#think-effort)) and it can actually surface reasoning text (`getCapabilities().supportsThinking`), that text streams incrementally too — as `role: "assistant"` chunks carrying their delta on `thinking` instead of `content`, interleaved *before* the model's normal answer chunks in the same turn:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
{ role: "assistant", content: "", done: false, thinking: "<reasoning...>" } // repeated
|
|
106
|
+
{ role: "assistant", content: "<answer...>", done: false } // thinking absent from here on
|
|
107
|
+
{ role: "assistant", content: "", done: true }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`thinking` mirrors `content`: a chunk carries text on exactly one of the two, never both, and `thinking` is only ever present on non-`done` chunks — the final `done: true` chunk never carries it. Once accumulated, the full reasoning text for the turn is also available non-streaming on `ChatResponse.thinking` / `Message.thinking` (e.g. for logging or persisting alongside the answer).
|
|
111
|
+
|
|
112
|
+
Support varies by provider — set `thinkEffort` and check `getCapabilities().supportsThinking` before assuming thinking chunks will actually arrive:
|
|
113
|
+
|
|
114
|
+
| Provider | Streams `thinking` chunks? |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Anthropic | Yes — native `thinking_delta` events. |
|
|
117
|
+
| DeepSeek | Yes — `delta.reasoning_content` on reasoner models. |
|
|
118
|
+
| Ollama | Yes — `message.thinking` on models that support it. |
|
|
119
|
+
| OpenAI | **No.** `thinkEffort` is still honored server-side (via `reasoning_effort`) on reasoning-capable models, but the Chat Completions API this provider uses never returns the reasoning text itself — there's nothing to stream. |
|
|
120
|
+
|
|
121
|
+
### `thinking` vs `providerThinking` on `Message`/`ChatResponse`
|
|
122
|
+
|
|
123
|
+
Once a turn finishes, its accumulated reasoning is available two ways on both `ChatResponse` (returned from `AIProvider.chat()`) and `Message` (as stored in an agent's history):
|
|
124
|
+
|
|
125
|
+
- **`thinking?: string`** — the plain reasoning text, for display or logging. Never required by any provider's API; read-only from your side.
|
|
126
|
+
- **`providerThinking?: any[]`** — opaque, provider-specific data (currently only Anthropic populates this: signed `thinking` content blocks). When a turn both thinks and calls a tool, Anthropic's API requires the exact signed thinking block(s) from that turn to be replayed verbatim on the next request or it rejects the call. `BaseAgent` carries this field through automatically when it builds the next turn's message history, so if you're using `BaseAgent`/`SimpleAgent` you never touch it directly. It only matters if you're driving an `AIProvider` yourself (see [Providers](./providers.md)) — in that case, copy `providerThinking` from the response straight onto the `Message` you push for that assistant turn, unchanged.
|
|
127
|
+
|
|
128
|
+
## What's *not* in the stream today
|
|
129
|
+
|
|
130
|
+
- **No live per-tool progress channel through `StreamCallback`.** There's no built-in way to observe a long-running tool call (browser navigation, a bash process) mid-flight — `execute()` runs to completion and its return value is the only thing surfaced.
|
|
131
|
+
- **Tool calls still only appear once, fully assembled** — see above. `thinkEffort`/thinking chunks don't change that.
|
|
132
|
+
|
|
133
|
+
If your use case depends on either of these, check the project's issue tracker / recent changes before assuming they're unavailable — this area of the SDK is actively evolving.
|