@strav/brain 0.4.31 → 1.0.0-alpha.10
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/package.json +17 -20
- package/src/agent.ts +50 -75
- package/src/agent_result.ts +32 -0
- package/src/agent_runner.ts +63 -0
- package/src/brain_config.ts +80 -0
- package/src/brain_error.ts +29 -0
- package/src/brain_manager.ts +186 -123
- package/src/brain_provider.ts +91 -6
- package/src/define_tool.ts +42 -0
- package/src/index.ts +43 -42
- package/src/mcp_server.ts +47 -0
- package/src/provider.ts +83 -0
- package/src/providers/anthropic_provider.ts +435 -232
- package/src/thread.ts +99 -0
- package/src/tool.ts +28 -44
- package/src/tool_execution_error.ts +26 -0
- package/src/types.ts +164 -237
- package/CHANGELOG.md +0 -44
- package/README.md +0 -121
- package/src/helpers.ts +0 -1082
- package/src/mcp_toolbox.ts +0 -62
- package/src/memory/context_budget.ts +0 -120
- package/src/memory/index.ts +0 -17
- package/src/memory/memory_manager.ts +0 -168
- package/src/memory/semantic_memory.ts +0 -89
- package/src/memory/strategies/sliding_window.ts +0 -20
- package/src/memory/strategies/summarize.ts +0 -157
- package/src/memory/thread_store.ts +0 -56
- package/src/memory/token_counter.ts +0 -101
- package/src/memory/types.ts +0 -68
- package/src/providers/google_provider.ts +0 -496
- package/src/providers/openai_provider.ts +0 -569
- package/src/providers/openai_responses_provider.ts +0 -321
- package/src/utils/error_scrub.ts +0 -5
- package/src/utils/prompt.ts +0 -65
- package/src/utils/retry.ts +0 -104
- package/src/utils/schema.ts +0 -27
- package/src/utils/sse_parser.ts +0 -62
- package/src/workflow.ts +0 -199
- package/tsconfig.json +0 -5
|
@@ -1,281 +1,484 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `AnthropicProvider` — implementation of `Provider` backed by the
|
|
3
|
+
* official `@anthropic-ai/sdk`.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Hold a singleton `Anthropic` client instance for the
|
|
7
|
+
* configured API key + base URL.
|
|
8
|
+
* 2. Translate the framework's `ChatOptions` / `Message` shapes
|
|
9
|
+
* into Anthropic's `MessageCreateParams` (system as `TextBlock[]`
|
|
10
|
+
* with `cache_control` when requested; messages with per-block
|
|
11
|
+
* cache flags translated likewise; `thinking` mapped to
|
|
12
|
+
* `ThinkingConfigParam`; `effort` placed under `output_config`).
|
|
13
|
+
* 3. Translate the response back to `ChatResult` — flatten the
|
|
14
|
+
* content blocks into a single `text` string, surface usage with
|
|
15
|
+
* cache-hit counters, and pass the raw `Message` through on `.raw`.
|
|
16
|
+
* 4. Stream via `client.messages.stream()` and yield the framework
|
|
17
|
+
* `StreamEvent` union — `text` deltas plus a terminal `stop`
|
|
18
|
+
* event with usage + stop reason.
|
|
19
|
+
*
|
|
20
|
+
* Errors from the SDK propagate; apps that want provider-specific
|
|
21
|
+
* recovery can `instanceof Anthropic.RateLimitError` etc. The brain
|
|
22
|
+
* facade wraps the call site in `BrainError` only for invariants the
|
|
23
|
+
* facade owns (e.g. "no provider configured").
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
27
|
+
import type { AgentResult } from '../agent_result.ts'
|
|
28
|
+
import type { AnthropicProviderConfig } from '../brain_config.ts'
|
|
29
|
+
import { DEFAULT_MODEL } from '../brain_config.ts'
|
|
30
|
+
import type { Provider, RunWithToolsOptions } from '../provider.ts'
|
|
31
|
+
import type { Tool } from '../tool.ts'
|
|
32
|
+
import { ToolExecutionError } from '../tool_execution_error.ts'
|
|
4
33
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
34
|
+
ChatOptions,
|
|
35
|
+
ChatResult,
|
|
36
|
+
ChatUsage,
|
|
37
|
+
ContentBlock,
|
|
38
|
+
MCPToolResultBlock,
|
|
39
|
+
MCPToolUseBlock,
|
|
10
40
|
Message,
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
StreamEvent,
|
|
42
|
+
SystemPrompt,
|
|
43
|
+
TextBlock,
|
|
44
|
+
ToolResultBlock,
|
|
45
|
+
ToolUseBlock,
|
|
13
46
|
} from '../types.ts'
|
|
14
47
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Translates the framework's normalized CompletionRequest/Response
|
|
19
|
-
* to/from the Anthropic wire format. Uses raw `fetch()`.
|
|
20
|
-
*/
|
|
21
|
-
export class AnthropicProvider implements AIProvider {
|
|
48
|
+
const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
|
|
49
|
+
|
|
50
|
+
export class AnthropicProvider implements Provider {
|
|
22
51
|
readonly name: string
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
private readonly client: Anthropic
|
|
53
|
+
private readonly defaultModel: string
|
|
54
|
+
private readonly defaultMaxTokens: number
|
|
55
|
+
private readonly betas: readonly string[]
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
name: string,
|
|
59
|
+
config: AnthropicProviderConfig,
|
|
60
|
+
options: { client?: Anthropic } = {},
|
|
61
|
+
) {
|
|
62
|
+
this.name = name
|
|
63
|
+
this.defaultModel = config.defaultModel ?? DEFAULT_MODEL
|
|
64
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 4096
|
|
65
|
+
this.betas = config.betas ?? []
|
|
66
|
+
// `client` injection point — tests pass a stub; apps that want a
|
|
67
|
+
// pre-configured SDK instance (custom retry, fetch transport, etc.)
|
|
68
|
+
// build their own and hand it over here.
|
|
69
|
+
this.client =
|
|
70
|
+
options.client ??
|
|
71
|
+
new Anthropic({
|
|
72
|
+
apiKey: config.apiKey,
|
|
73
|
+
...(config.baseUrl !== undefined ? { baseURL: config.baseUrl } : {}),
|
|
74
|
+
})
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
async
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'Anthropic',
|
|
46
|
-
`${this.baseUrl}/v1/messages`,
|
|
47
|
-
{ method: 'POST', headers: this.buildHeaders(), body: JSON.stringify(body) },
|
|
48
|
-
this.retryOptions
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
const data: any = await response.json()
|
|
52
|
-
return this.parseResponse(data)
|
|
77
|
+
async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
78
|
+
const params = this.buildParams(messages, options)
|
|
79
|
+
const response = await this.client.messages.create(params)
|
|
80
|
+
return this.toChatResult(response)
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
async *stream(
|
|
56
|
-
|
|
83
|
+
async *stream(
|
|
84
|
+
messages: readonly Message[],
|
|
85
|
+
options: ChatOptions = {},
|
|
86
|
+
): AsyncIterable<StreamEvent> {
|
|
87
|
+
const params = this.buildParams(messages, options)
|
|
88
|
+
const stream = this.client.messages.stream(params)
|
|
89
|
+
for await (const event of stream) {
|
|
90
|
+
if (
|
|
91
|
+
event.type === 'content_block_delta' &&
|
|
92
|
+
event.delta.type === 'text_delta'
|
|
93
|
+
) {
|
|
94
|
+
yield { type: 'text', delta: event.delta.text }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const final = await stream.finalMessage()
|
|
98
|
+
yield {
|
|
99
|
+
type: 'stop',
|
|
100
|
+
stopReason: final.stop_reason,
|
|
101
|
+
usage: toUsage(final.usage),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
57
104
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
async countTokens(
|
|
106
|
+
messages: readonly Message[],
|
|
107
|
+
options: ChatOptions = {},
|
|
108
|
+
): Promise<number> {
|
|
109
|
+
const base = this.buildParams(messages, options)
|
|
110
|
+
// count_tokens only accepts a subset of MessageCreateParams; build
|
|
111
|
+
// a focused payload that matches what apps actually need to budget.
|
|
112
|
+
const result = await this.client.messages.countTokens({
|
|
113
|
+
model: base.model,
|
|
114
|
+
messages: base.messages,
|
|
115
|
+
...(base.system !== undefined ? { system: base.system } : {}),
|
|
116
|
+
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
117
|
+
})
|
|
118
|
+
return result.input_tokens
|
|
119
|
+
}
|
|
64
120
|
|
|
65
|
-
|
|
66
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Agentic loop. Send → detect tool_use blocks → execute → append
|
|
123
|
+
* tool_result → re-send, until the model returns `end_turn` or
|
|
124
|
+
* the iteration ceiling is hit.
|
|
125
|
+
*
|
|
126
|
+
* Tools are passed once on every call — Anthropic doesn't carry
|
|
127
|
+
* tool state across requests; the model rediscovers them from the
|
|
128
|
+
* `tools` array each turn. Apps that care about cache hits keep
|
|
129
|
+
* the tool list stable across runs.
|
|
130
|
+
*/
|
|
131
|
+
async runWithTools(
|
|
132
|
+
messages: readonly Message[],
|
|
133
|
+
tools: readonly Tool[],
|
|
134
|
+
options: RunWithToolsOptions = {},
|
|
135
|
+
): Promise<AgentResult> {
|
|
136
|
+
const maxIterations = options.maxIterations ?? 10
|
|
137
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
138
|
+
const workingMessages: Message[] = [...messages]
|
|
139
|
+
const aggregated: ChatUsage = {
|
|
140
|
+
inputTokens: 0,
|
|
141
|
+
outputTokens: 0,
|
|
142
|
+
cacheReadTokens: 0,
|
|
143
|
+
cacheCreationTokens: 0,
|
|
67
144
|
}
|
|
145
|
+
let iterations = 0
|
|
146
|
+
let lastStopReason: string | null = null
|
|
68
147
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for await (const sse of parseSSE(response.body)) {
|
|
72
|
-
if (sse.data === '[DONE]') break
|
|
148
|
+
const mcpServers = options.mcpServers ?? []
|
|
149
|
+
const useMcpBeta = mcpServers.length > 0
|
|
73
150
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} catch {
|
|
78
|
-
continue
|
|
151
|
+
while (true) {
|
|
152
|
+
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
153
|
+
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
79
154
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
params.tools = [
|
|
156
|
+
...tools.map((t) => ({
|
|
157
|
+
name: t.name,
|
|
158
|
+
description: t.description,
|
|
159
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
160
|
+
})),
|
|
161
|
+
// MCP toolsets — one per declared server. The model sees the
|
|
162
|
+
// server's tools via Anthropic's connector, not via our local
|
|
163
|
+
// `tools` list.
|
|
164
|
+
...mcpServers
|
|
165
|
+
.filter((s) => s.tools?.enabled !== false)
|
|
166
|
+
.map((s) => ({
|
|
167
|
+
type: 'mcp_toolset' as const,
|
|
168
|
+
mcp_server_name: s.name,
|
|
169
|
+
...(s.tools?.allowedTools
|
|
170
|
+
? { allowed_tools: [...s.tools.allowedTools] }
|
|
171
|
+
: {}),
|
|
172
|
+
})),
|
|
173
|
+
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
174
|
+
|
|
175
|
+
// Declare MCP servers + flip to the beta surface when in use.
|
|
176
|
+
// Anthropic's MCP connector requires `mcp-client-2025-11-20`.
|
|
177
|
+
let response: Anthropic.Message
|
|
178
|
+
if (useMcpBeta) {
|
|
179
|
+
params.mcp_servers = mcpServers.map((s) => {
|
|
180
|
+
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
181
|
+
type: 'url',
|
|
182
|
+
name: s.name,
|
|
183
|
+
url: s.url,
|
|
91
184
|
}
|
|
185
|
+
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
186
|
+
return def
|
|
187
|
+
})
|
|
188
|
+
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
189
|
+
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
190
|
+
? [...baseBetas]
|
|
191
|
+
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
192
|
+
response = (await this.client.beta.messages.create(
|
|
193
|
+
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
194
|
+
)) as unknown as Anthropic.Message
|
|
195
|
+
} else {
|
|
196
|
+
response = await this.client.messages.create(params)
|
|
197
|
+
}
|
|
198
|
+
addUsage(aggregated, response.usage)
|
|
199
|
+
lastStopReason = response.stop_reason ?? null
|
|
200
|
+
|
|
201
|
+
// Append the assistant turn verbatim from the SDK shape so
|
|
202
|
+
// tool_use blocks survive to the next request unchanged.
|
|
203
|
+
workingMessages.push({
|
|
204
|
+
role: 'assistant',
|
|
205
|
+
content: fromAnthropicContent(response.content),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
if (response.stop_reason !== 'tool_use') {
|
|
209
|
+
return {
|
|
210
|
+
text: collectText(response.content),
|
|
211
|
+
messages: workingMessages,
|
|
212
|
+
iterations,
|
|
213
|
+
stopReason: lastStopReason ?? 'end_turn',
|
|
214
|
+
usage: aggregated,
|
|
92
215
|
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Execute every tool_use block in the response and append the
|
|
219
|
+
// results in a single user-role turn. The SDK's API expects all
|
|
220
|
+
// tool_result blocks for a given assistant turn to land in the
|
|
221
|
+
// same user message.
|
|
222
|
+
const toolUseBlocks = response.content.filter(
|
|
223
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
224
|
+
)
|
|
225
|
+
const resultBlocks: ContentBlock[] = []
|
|
226
|
+
for (const block of toolUseBlocks) {
|
|
227
|
+
const tool = toolMap.get(block.name)
|
|
228
|
+
if (!tool) {
|
|
229
|
+
throw new ToolExecutionError(
|
|
230
|
+
block.name,
|
|
231
|
+
block.id,
|
|
232
|
+
new Error(`Tool "${block.name}" is not registered.`),
|
|
233
|
+
)
|
|
103
234
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
235
|
+
let output: unknown
|
|
236
|
+
try {
|
|
237
|
+
output = await tool.execute(block.input, {
|
|
238
|
+
callId: block.id,
|
|
239
|
+
context: options.context ?? {},
|
|
240
|
+
})
|
|
241
|
+
} catch (cause) {
|
|
242
|
+
throw new ToolExecutionError(block.name, block.id, cause)
|
|
108
243
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
244
|
+
const resultBlock: ToolResultBlock = {
|
|
245
|
+
type: 'tool_result',
|
|
246
|
+
toolUseId: block.id,
|
|
247
|
+
content: typeof output === 'string' ? output : JSON.stringify(output),
|
|
248
|
+
}
|
|
249
|
+
resultBlocks.push(resultBlock)
|
|
250
|
+
}
|
|
251
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
252
|
+
|
|
253
|
+
iterations++
|
|
254
|
+
if (iterations >= maxIterations) {
|
|
255
|
+
return {
|
|
256
|
+
text: collectText(response.content),
|
|
257
|
+
messages: workingMessages,
|
|
258
|
+
iterations,
|
|
259
|
+
stopReason: 'max_iterations',
|
|
260
|
+
usage: aggregated,
|
|
120
261
|
}
|
|
121
|
-
} else if (type === 'message_stop') {
|
|
122
|
-
yield { type: 'done' }
|
|
123
262
|
}
|
|
124
263
|
}
|
|
125
264
|
}
|
|
126
265
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
266
|
+
// ─── Param translation ──────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
private buildParams(
|
|
269
|
+
messages: readonly Message[],
|
|
270
|
+
options: ChatOptions,
|
|
271
|
+
): Anthropic.MessageCreateParamsNonStreaming {
|
|
272
|
+
const model = options.model ?? this.defaultModel
|
|
273
|
+
const params: Anthropic.MessageCreateParamsNonStreaming = {
|
|
274
|
+
model,
|
|
275
|
+
max_tokens: options.maxTokens ?? this.defaultMaxTokens,
|
|
276
|
+
messages: messages.map(toMessageParam),
|
|
134
277
|
}
|
|
135
|
-
}
|
|
136
278
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
279
|
+
const system = toSystemParam(options.system)
|
|
280
|
+
if (system !== undefined) params.system = system
|
|
281
|
+
|
|
282
|
+
if (options.thinking === 'adaptive') {
|
|
283
|
+
params.thinking = { type: 'adaptive' }
|
|
284
|
+
} else if (options.thinking === 'disabled') {
|
|
285
|
+
params.thinking = { type: 'disabled' }
|
|
142
286
|
}
|
|
143
287
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
if (request.temperature !== undefined) body.temperature = request.temperature
|
|
147
|
-
if (request.stopSequences?.length) body.stop_sequences = request.stopSequences
|
|
148
|
-
|
|
149
|
-
// Tools
|
|
150
|
-
if (request.tools?.length) {
|
|
151
|
-
body.tools = request.tools.map(t => ({
|
|
152
|
-
name: t.name,
|
|
153
|
-
description: t.description,
|
|
154
|
-
input_schema: t.parameters,
|
|
155
|
-
}))
|
|
288
|
+
if (options.effort !== undefined) {
|
|
289
|
+
params.output_config = { effort: options.effort }
|
|
156
290
|
}
|
|
157
291
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} else if (request.toolChoice === 'required') {
|
|
163
|
-
body.tool_choice = { type: 'any' }
|
|
164
|
-
} else {
|
|
165
|
-
body.tool_choice = { type: 'tool', name: request.toolChoice.name }
|
|
166
|
-
}
|
|
292
|
+
if (options.cache === true) {
|
|
293
|
+
// Top-level auto-cache the last cacheable block. Maps to the
|
|
294
|
+
// SDK's `cache_control` shorthand on the request body.
|
|
295
|
+
;(params as { cache_control?: { type: 'ephemeral' } }).cache_control = EPHEMERAL_CACHE
|
|
167
296
|
}
|
|
168
297
|
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
format: {
|
|
173
|
-
type: 'json_schema',
|
|
174
|
-
schema: request.schema
|
|
175
|
-
}
|
|
176
|
-
}
|
|
298
|
+
const betas = mergeBetas(this.betas, options.betas)
|
|
299
|
+
if (betas.length > 0) {
|
|
300
|
+
;(params as { betas?: readonly string[] }).betas = betas
|
|
177
301
|
}
|
|
178
302
|
|
|
179
|
-
return
|
|
303
|
+
return params
|
|
180
304
|
}
|
|
181
305
|
|
|
182
|
-
private
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
tool_use_id: msg.toolCallId,
|
|
194
|
-
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
})
|
|
198
|
-
} else if (msg.role === 'assistant') {
|
|
199
|
-
const content: any[] = []
|
|
200
|
-
|
|
201
|
-
// Add text content if present
|
|
202
|
-
const text = typeof msg.content === 'string' ? msg.content : ''
|
|
203
|
-
if (text) {
|
|
204
|
-
content.push({ type: 'text', text })
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Add tool use blocks
|
|
208
|
-
if (msg.toolCalls?.length) {
|
|
209
|
-
for (const tc of msg.toolCalls) {
|
|
210
|
-
content.push({
|
|
211
|
-
type: 'tool_use',
|
|
212
|
-
id: tc.id,
|
|
213
|
-
name: tc.name,
|
|
214
|
-
input: tc.arguments,
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
result.push({
|
|
220
|
-
role: 'assistant',
|
|
221
|
-
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content,
|
|
222
|
-
})
|
|
223
|
-
} else {
|
|
224
|
-
// User messages
|
|
225
|
-
result.push({
|
|
226
|
-
role: 'user',
|
|
227
|
-
content: typeof msg.content === 'string' ? msg.content : msg.content,
|
|
228
|
-
})
|
|
229
|
-
}
|
|
306
|
+
private toChatResult(message: Anthropic.Message): ChatResult<Anthropic.Message> {
|
|
307
|
+
const text = message.content
|
|
308
|
+
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
309
|
+
.map((b) => b.text)
|
|
310
|
+
.join('')
|
|
311
|
+
return {
|
|
312
|
+
text,
|
|
313
|
+
model: message.model,
|
|
314
|
+
stopReason: message.stop_reason,
|
|
315
|
+
usage: toUsage(message.usage),
|
|
316
|
+
raw: message,
|
|
230
317
|
}
|
|
231
|
-
|
|
232
|
-
return result
|
|
233
318
|
}
|
|
319
|
+
}
|
|
234
320
|
|
|
235
|
-
|
|
236
|
-
let content = ''
|
|
237
|
-
const toolCalls: ToolCall[] = []
|
|
321
|
+
// ─── Shape converters ─────────────────────────────────────────────────────
|
|
238
322
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
323
|
+
function toUsage(u: Anthropic.Usage): ChatUsage {
|
|
324
|
+
return {
|
|
325
|
+
inputTokens: u.input_tokens,
|
|
326
|
+
outputTokens: u.output_tokens,
|
|
327
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
328
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function toMessageParam(message: Message): Anthropic.MessageParam {
|
|
333
|
+
if (typeof message.content === 'string') {
|
|
334
|
+
return { role: message.role, content: message.content }
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
role: message.role,
|
|
338
|
+
content: message.content
|
|
339
|
+
// MCP blocks are inbound-only — Anthropic produces them, we
|
|
340
|
+
// surface them on `result.messages` for observability, but we
|
|
341
|
+
// never echo them back to the model. The backend tracks MCP
|
|
342
|
+
// tool state on its side.
|
|
343
|
+
.filter(
|
|
344
|
+
(b): b is Exclude<ContentBlock, MCPToolUseBlock | MCPToolResultBlock> =>
|
|
345
|
+
b.type !== 'mcp_tool_use' && b.type !== 'mcp_tool_result',
|
|
346
|
+
)
|
|
347
|
+
.map((block): Anthropic.ContentBlockParam => {
|
|
348
|
+
if (block.type === 'tool_use') {
|
|
349
|
+
return {
|
|
350
|
+
type: 'tool_use',
|
|
245
351
|
id: block.id,
|
|
246
352
|
name: block.name,
|
|
247
|
-
|
|
248
|
-
}
|
|
353
|
+
input: block.input as Record<string, unknown>,
|
|
354
|
+
}
|
|
249
355
|
}
|
|
250
|
-
|
|
251
|
-
|
|
356
|
+
if (block.type === 'tool_result') {
|
|
357
|
+
const param: Anthropic.ToolResultBlockParam = {
|
|
358
|
+
type: 'tool_result',
|
|
359
|
+
tool_use_id: block.toolUseId,
|
|
360
|
+
content:
|
|
361
|
+
typeof block.content === 'string'
|
|
362
|
+
? block.content
|
|
363
|
+
: block.content.map(
|
|
364
|
+
(b) => ({ type: 'text', text: b.text }) as Anthropic.TextBlockParam,
|
|
365
|
+
),
|
|
366
|
+
}
|
|
367
|
+
if (block.isError) param.is_error = true
|
|
368
|
+
return param
|
|
369
|
+
}
|
|
370
|
+
const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
371
|
+
if (block.cache) text.cache_control = EPHEMERAL_CACHE
|
|
372
|
+
return text
|
|
373
|
+
}),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
252
376
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
377
|
+
function toSystemParam(
|
|
378
|
+
system: SystemPrompt | undefined,
|
|
379
|
+
): string | Anthropic.TextBlockParam[] | undefined {
|
|
380
|
+
if (system === undefined) return undefined
|
|
381
|
+
if (typeof system === 'string') return system
|
|
382
|
+
if (Array.isArray(system)) {
|
|
383
|
+
return system.map((block) => {
|
|
384
|
+
const param: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
385
|
+
if (block.cache) param.cache_control = EPHEMERAL_CACHE
|
|
386
|
+
return param
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
const param: Anthropic.TextBlockParam = { type: 'text', text: system.text }
|
|
390
|
+
if (system.cache) param.cache_control = EPHEMERAL_CACHE
|
|
391
|
+
return [param]
|
|
392
|
+
}
|
|
258
393
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
394
|
+
function mergeBetas(
|
|
395
|
+
providerBetas: readonly string[],
|
|
396
|
+
callBetas: readonly string[] | undefined,
|
|
397
|
+
): readonly string[] {
|
|
398
|
+
if (!callBetas || callBetas.length === 0) return providerBetas
|
|
399
|
+
const seen = new Set<string>()
|
|
400
|
+
const out: string[] = []
|
|
401
|
+
for (const b of providerBetas) {
|
|
402
|
+
if (seen.has(b)) continue
|
|
403
|
+
seen.add(b)
|
|
404
|
+
out.push(b)
|
|
405
|
+
}
|
|
406
|
+
for (const b of callBetas) {
|
|
407
|
+
if (seen.has(b)) continue
|
|
408
|
+
seen.add(b)
|
|
409
|
+
out.push(b)
|
|
410
|
+
}
|
|
411
|
+
return out
|
|
412
|
+
}
|
|
271
413
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
414
|
+
function addUsage(acc: ChatUsage, u: Anthropic.Usage): void {
|
|
415
|
+
acc.inputTokens += u.input_tokens
|
|
416
|
+
acc.outputTokens += u.output_tokens
|
|
417
|
+
acc.cacheReadTokens += u.cache_read_input_tokens ?? 0
|
|
418
|
+
acc.cacheCreationTokens += u.cache_creation_input_tokens ?? 0
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function collectText(content: Anthropic.ContentBlock[]): string {
|
|
422
|
+
return content
|
|
423
|
+
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
424
|
+
.map((b) => b.text)
|
|
425
|
+
.join('')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Translate the SDK's response content blocks back into framework
|
|
430
|
+
* `ContentBlock`s for storage in `workingMessages`. We preserve
|
|
431
|
+
* `text` and `tool_use` blocks verbatim; other server-side block
|
|
432
|
+
* types (thinking, server tool blocks) are dropped — V1 doesn't
|
|
433
|
+
* surface them, and re-sending them as part of the assistant turn
|
|
434
|
+
* could confuse the model.
|
|
435
|
+
*/
|
|
436
|
+
function fromAnthropicContent(
|
|
437
|
+
content: ReadonlyArray<Anthropic.ContentBlock | { type: string; [k: string]: unknown }>,
|
|
438
|
+
): ContentBlock[] {
|
|
439
|
+
const out: ContentBlock[] = []
|
|
440
|
+
for (const block of content) {
|
|
441
|
+
if (block.type === 'text') {
|
|
442
|
+
out.push({ type: 'text', text: (block as { text: string }).text } satisfies TextBlock)
|
|
443
|
+
} else if (block.type === 'tool_use') {
|
|
444
|
+
const u = block as { id: string; name: string; input: unknown }
|
|
445
|
+
out.push({
|
|
446
|
+
type: 'tool_use',
|
|
447
|
+
id: u.id,
|
|
448
|
+
name: u.name,
|
|
449
|
+
input: u.input,
|
|
450
|
+
} satisfies ToolUseBlock)
|
|
451
|
+
} else if (block.type === 'mcp_tool_use') {
|
|
452
|
+
const m = block as unknown as {
|
|
453
|
+
id: string
|
|
454
|
+
server_name: string
|
|
455
|
+
name: string
|
|
456
|
+
input: unknown
|
|
457
|
+
}
|
|
458
|
+
out.push({
|
|
459
|
+
type: 'mcp_tool_use',
|
|
460
|
+
id: m.id,
|
|
461
|
+
serverName: m.server_name,
|
|
462
|
+
name: m.name,
|
|
463
|
+
input: m.input,
|
|
464
|
+
} satisfies MCPToolUseBlock)
|
|
465
|
+
} else if (block.type === 'mcp_tool_result') {
|
|
466
|
+
const r = block as unknown as {
|
|
467
|
+
tool_use_id: string
|
|
468
|
+
content: string | Array<{ type: 'text'; text: string }>
|
|
469
|
+
is_error?: boolean
|
|
470
|
+
}
|
|
471
|
+
const result: MCPToolResultBlock = {
|
|
472
|
+
type: 'mcp_tool_result',
|
|
473
|
+
toolUseId: r.tool_use_id,
|
|
474
|
+
content:
|
|
475
|
+
typeof r.content === 'string'
|
|
476
|
+
? r.content
|
|
477
|
+
: r.content.map((c) => ({ type: 'text', text: c.text }) satisfies TextBlock),
|
|
478
|
+
}
|
|
479
|
+
if (r.is_error) result.isError = true
|
|
480
|
+
out.push(result)
|
|
279
481
|
}
|
|
280
482
|
}
|
|
483
|
+
return out
|
|
281
484
|
}
|