@strav/brain 0.4.31 → 1.0.0-alpha.9
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 +42 -76
- package/src/agent_result.ts +32 -0
- package/src/agent_runner.ts +61 -0
- package/src/brain_config.ts +72 -0
- package/src/brain_error.ts +29 -0
- package/src/brain_manager.ts +170 -123
- package/src/brain_provider.ts +90 -6
- package/src/define_tool.ts +42 -0
- package/src/index.ts +40 -42
- package/src/provider.ts +74 -0
- package/src/providers/anthropic_provider.ts +347 -231
- package/src/thread.ts +99 -0
- package/src/tool.ts +28 -44
- package/src/tool_execution_error.ts +26 -0
- package/src/types.ts +129 -241
- 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,397 @@
|
|
|
1
|
-
import { parseSSE } from '../utils/sse_parser.ts'
|
|
2
|
-
import { retryableFetch, type RetryOptions } from '../utils/retry.ts'
|
|
3
|
-
import { ExternalServiceError } from '@strav/kernel'
|
|
4
|
-
import type {
|
|
5
|
-
AIProvider,
|
|
6
|
-
CompletionRequest,
|
|
7
|
-
CompletionResponse,
|
|
8
|
-
StreamChunk,
|
|
9
|
-
ProviderConfig,
|
|
10
|
-
Message,
|
|
11
|
-
ToolCall,
|
|
12
|
-
Usage,
|
|
13
|
-
} from '../types.ts'
|
|
14
|
-
|
|
15
1
|
/**
|
|
16
|
-
*
|
|
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.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
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").
|
|
20
24
|
*/
|
|
21
|
-
export class AnthropicProvider implements AIProvider {
|
|
22
|
-
readonly name: string
|
|
23
|
-
private apiKey: string
|
|
24
|
-
private baseUrl: string
|
|
25
|
-
private defaultModel: string
|
|
26
|
-
private defaultMaxTokens: number
|
|
27
|
-
private retryOptions: RetryOptions
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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'
|
|
33
|
+
import type {
|
|
34
|
+
ChatOptions,
|
|
35
|
+
ChatResult,
|
|
36
|
+
ChatUsage,
|
|
37
|
+
ContentBlock,
|
|
38
|
+
Message,
|
|
39
|
+
StreamEvent,
|
|
40
|
+
SystemPrompt,
|
|
41
|
+
TextBlock,
|
|
42
|
+
ToolResultBlock,
|
|
43
|
+
ToolUseBlock,
|
|
44
|
+
} from '../types.ts'
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
const body = this.buildRequestBody(request, false)
|
|
46
|
+
const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
export class AnthropicProvider implements Provider {
|
|
49
|
+
readonly name: string
|
|
50
|
+
private readonly client: Anthropic
|
|
51
|
+
private readonly defaultModel: string
|
|
52
|
+
private readonly defaultMaxTokens: number
|
|
53
|
+
private readonly betas: readonly string[]
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
name: string,
|
|
57
|
+
config: AnthropicProviderConfig,
|
|
58
|
+
options: { client?: Anthropic } = {},
|
|
59
|
+
) {
|
|
60
|
+
this.name = name
|
|
61
|
+
this.defaultModel = config.defaultModel ?? DEFAULT_MODEL
|
|
62
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 4096
|
|
63
|
+
this.betas = config.betas ?? []
|
|
64
|
+
// `client` injection point — tests pass a stub; apps that want a
|
|
65
|
+
// pre-configured SDK instance (custom retry, fetch transport, etc.)
|
|
66
|
+
// build their own and hand it over here.
|
|
67
|
+
this.client =
|
|
68
|
+
options.client ??
|
|
69
|
+
new Anthropic({
|
|
70
|
+
apiKey: config.apiKey,
|
|
71
|
+
...(config.baseUrl !== undefined ? { baseURL: config.baseUrl } : {}),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
76
|
+
const params = this.buildParams(messages, options)
|
|
77
|
+
const response = await this.client.messages.create(params)
|
|
78
|
+
return this.toChatResult(response)
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
async *stream(
|
|
56
|
-
|
|
81
|
+
async *stream(
|
|
82
|
+
messages: readonly Message[],
|
|
83
|
+
options: ChatOptions = {},
|
|
84
|
+
): AsyncIterable<StreamEvent> {
|
|
85
|
+
const params = this.buildParams(messages, options)
|
|
86
|
+
const stream = this.client.messages.stream(params)
|
|
87
|
+
for await (const event of stream) {
|
|
88
|
+
if (
|
|
89
|
+
event.type === 'content_block_delta' &&
|
|
90
|
+
event.delta.type === 'text_delta'
|
|
91
|
+
) {
|
|
92
|
+
yield { type: 'text', delta: event.delta.text }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const final = await stream.finalMessage()
|
|
96
|
+
yield {
|
|
97
|
+
type: 'stop',
|
|
98
|
+
stopReason: final.stop_reason,
|
|
99
|
+
usage: toUsage(final.usage),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
async countTokens(
|
|
104
|
+
messages: readonly Message[],
|
|
105
|
+
options: ChatOptions = {},
|
|
106
|
+
): Promise<number> {
|
|
107
|
+
const base = this.buildParams(messages, options)
|
|
108
|
+
// count_tokens only accepts a subset of MessageCreateParams; build
|
|
109
|
+
// a focused payload that matches what apps actually need to budget.
|
|
110
|
+
const result = await this.client.messages.countTokens({
|
|
111
|
+
model: base.model,
|
|
112
|
+
messages: base.messages,
|
|
113
|
+
...(base.system !== undefined ? { system: base.system } : {}),
|
|
114
|
+
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
115
|
+
})
|
|
116
|
+
return result.input_tokens
|
|
117
|
+
}
|
|
64
118
|
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Agentic loop. Send → detect tool_use blocks → execute → append
|
|
121
|
+
* tool_result → re-send, until the model returns `end_turn` or
|
|
122
|
+
* the iteration ceiling is hit.
|
|
123
|
+
*
|
|
124
|
+
* Tools are passed once on every call — Anthropic doesn't carry
|
|
125
|
+
* tool state across requests; the model rediscovers them from the
|
|
126
|
+
* `tools` array each turn. Apps that care about cache hits keep
|
|
127
|
+
* the tool list stable across runs.
|
|
128
|
+
*/
|
|
129
|
+
async runWithTools(
|
|
130
|
+
messages: readonly Message[],
|
|
131
|
+
tools: readonly Tool[],
|
|
132
|
+
options: RunWithToolsOptions = {},
|
|
133
|
+
): Promise<AgentResult> {
|
|
134
|
+
const maxIterations = options.maxIterations ?? 10
|
|
135
|
+
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
136
|
+
const workingMessages: Message[] = [...messages]
|
|
137
|
+
const aggregated: ChatUsage = {
|
|
138
|
+
inputTokens: 0,
|
|
139
|
+
outputTokens: 0,
|
|
140
|
+
cacheReadTokens: 0,
|
|
141
|
+
cacheCreationTokens: 0,
|
|
67
142
|
}
|
|
143
|
+
let iterations = 0
|
|
144
|
+
let lastStopReason: string | null = null
|
|
68
145
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
146
|
+
while (true) {
|
|
147
|
+
const params = this.buildParams(workingMessages, options)
|
|
148
|
+
params.tools = tools.map((t) => ({
|
|
149
|
+
name: t.name,
|
|
150
|
+
description: t.description,
|
|
151
|
+
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
152
|
+
}))
|
|
73
153
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
154
|
+
const response = await this.client.messages.create(params)
|
|
155
|
+
addUsage(aggregated, response.usage)
|
|
156
|
+
lastStopReason = response.stop_reason ?? null
|
|
157
|
+
|
|
158
|
+
// Append the assistant turn verbatim from the SDK shape so
|
|
159
|
+
// tool_use blocks survive to the next request unchanged.
|
|
160
|
+
workingMessages.push({
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
content: fromAnthropicContent(response.content),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (response.stop_reason !== 'tool_use') {
|
|
166
|
+
return {
|
|
167
|
+
text: collectText(response.content),
|
|
168
|
+
messages: workingMessages,
|
|
169
|
+
iterations,
|
|
170
|
+
stopReason: lastStopReason ?? 'end_turn',
|
|
171
|
+
usage: aggregated,
|
|
172
|
+
}
|
|
79
173
|
}
|
|
80
174
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
175
|
+
// Execute every tool_use block in the response and append the
|
|
176
|
+
// results in a single user-role turn. The SDK's API expects all
|
|
177
|
+
// tool_result blocks for a given assistant turn to land in the
|
|
178
|
+
// same user message.
|
|
179
|
+
const toolUseBlocks = response.content.filter(
|
|
180
|
+
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
181
|
+
)
|
|
182
|
+
const resultBlocks: ContentBlock[] = []
|
|
183
|
+
for (const block of toolUseBlocks) {
|
|
184
|
+
const tool = toolMap.get(block.name)
|
|
185
|
+
if (!tool) {
|
|
186
|
+
throw new ToolExecutionError(
|
|
187
|
+
block.name,
|
|
188
|
+
block.id,
|
|
189
|
+
new Error(`Tool "${block.name}" is not registered.`),
|
|
190
|
+
)
|
|
92
191
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
toolIndex: parsed.index ?? currentBlockIndex,
|
|
102
|
-
}
|
|
192
|
+
let output: unknown
|
|
193
|
+
try {
|
|
194
|
+
output = await tool.execute(block.input, {
|
|
195
|
+
callId: block.id,
|
|
196
|
+
context: options.context ?? {},
|
|
197
|
+
})
|
|
198
|
+
} catch (cause) {
|
|
199
|
+
throw new ToolExecutionError(block.name, block.id, cause)
|
|
103
200
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
const resultBlock: ToolResultBlock = {
|
|
202
|
+
type: 'tool_result',
|
|
203
|
+
toolUseId: block.id,
|
|
204
|
+
content: typeof output === 'string' ? output : JSON.stringify(output),
|
|
108
205
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
206
|
+
resultBlocks.push(resultBlock)
|
|
207
|
+
}
|
|
208
|
+
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
209
|
+
|
|
210
|
+
iterations++
|
|
211
|
+
if (iterations >= maxIterations) {
|
|
212
|
+
return {
|
|
213
|
+
text: collectText(response.content),
|
|
214
|
+
messages: workingMessages,
|
|
215
|
+
iterations,
|
|
216
|
+
stopReason: 'max_iterations',
|
|
217
|
+
usage: aggregated,
|
|
120
218
|
}
|
|
121
|
-
} else if (type === 'message_stop') {
|
|
122
|
-
yield { type: 'done' }
|
|
123
219
|
}
|
|
124
220
|
}
|
|
125
221
|
}
|
|
126
222
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
223
|
+
// ─── Param translation ──────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
private buildParams(
|
|
226
|
+
messages: readonly Message[],
|
|
227
|
+
options: ChatOptions,
|
|
228
|
+
): Anthropic.MessageCreateParamsNonStreaming {
|
|
229
|
+
const model = options.model ?? this.defaultModel
|
|
230
|
+
const params: Anthropic.MessageCreateParamsNonStreaming = {
|
|
231
|
+
model,
|
|
232
|
+
max_tokens: options.maxTokens ?? this.defaultMaxTokens,
|
|
233
|
+
messages: messages.map(toMessageParam),
|
|
134
234
|
}
|
|
135
|
-
}
|
|
136
235
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
model: request.model ?? this.defaultModel,
|
|
140
|
-
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
|
|
141
|
-
messages: this.mapMessages(request.messages),
|
|
142
|
-
}
|
|
236
|
+
const system = toSystemParam(options.system)
|
|
237
|
+
if (system !== undefined) params.system = system
|
|
143
238
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
239
|
+
if (options.thinking === 'adaptive') {
|
|
240
|
+
params.thinking = { type: 'adaptive' }
|
|
241
|
+
} else if (options.thinking === 'disabled') {
|
|
242
|
+
params.thinking = { type: 'disabled' }
|
|
243
|
+
}
|
|
148
244
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
body.tools = request.tools.map(t => ({
|
|
152
|
-
name: t.name,
|
|
153
|
-
description: t.description,
|
|
154
|
-
input_schema: t.parameters,
|
|
155
|
-
}))
|
|
245
|
+
if (options.effort !== undefined) {
|
|
246
|
+
params.output_config = { effort: options.effort }
|
|
156
247
|
}
|
|
157
248
|
|
|
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
|
-
}
|
|
249
|
+
if (options.cache === true) {
|
|
250
|
+
// Top-level auto-cache the last cacheable block. Maps to the
|
|
251
|
+
// SDK's `cache_control` shorthand on the request body.
|
|
252
|
+
;(params as { cache_control?: { type: 'ephemeral' } }).cache_control = EPHEMERAL_CACHE
|
|
167
253
|
}
|
|
168
254
|
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
format: {
|
|
173
|
-
type: 'json_schema',
|
|
174
|
-
schema: request.schema
|
|
175
|
-
}
|
|
176
|
-
}
|
|
255
|
+
const betas = mergeBetas(this.betas, options.betas)
|
|
256
|
+
if (betas.length > 0) {
|
|
257
|
+
;(params as { betas?: readonly string[] }).betas = betas
|
|
177
258
|
}
|
|
178
259
|
|
|
179
|
-
return
|
|
260
|
+
return params
|
|
180
261
|
}
|
|
181
262
|
|
|
182
|
-
private
|
|
183
|
-
const
|
|
263
|
+
private toChatResult(message: Anthropic.Message): ChatResult<Anthropic.Message> {
|
|
264
|
+
const text = message.content
|
|
265
|
+
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
266
|
+
.map((b) => b.text)
|
|
267
|
+
.join('')
|
|
268
|
+
return {
|
|
269
|
+
text,
|
|
270
|
+
model: message.model,
|
|
271
|
+
stopReason: message.stop_reason,
|
|
272
|
+
usage: toUsage(message.usage),
|
|
273
|
+
raw: message,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
184
277
|
|
|
185
|
-
|
|
186
|
-
if (msg.role === 'tool') {
|
|
187
|
-
// Tool results go as user messages with tool_result content blocks
|
|
188
|
-
result.push({
|
|
189
|
-
role: 'user',
|
|
190
|
-
content: [
|
|
191
|
-
{
|
|
192
|
-
type: 'tool_result',
|
|
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[] = []
|
|
278
|
+
// ─── Shape converters ─────────────────────────────────────────────────────
|
|
200
279
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
280
|
+
function toUsage(u: Anthropic.Usage): ChatUsage {
|
|
281
|
+
return {
|
|
282
|
+
inputTokens: u.input_tokens,
|
|
283
|
+
outputTokens: u.output_tokens,
|
|
284
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
285
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
206
288
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
289
|
+
function toMessageParam(message: Message): Anthropic.MessageParam {
|
|
290
|
+
if (typeof message.content === 'string') {
|
|
291
|
+
return { role: message.role, content: message.content }
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
role: message.role,
|
|
295
|
+
content: message.content.map((block): Anthropic.ContentBlockParam => {
|
|
296
|
+
if (block.type === 'tool_use') {
|
|
297
|
+
return {
|
|
298
|
+
type: 'tool_use',
|
|
299
|
+
id: block.id,
|
|
300
|
+
name: block.name,
|
|
301
|
+
input: block.input as Record<string, unknown>,
|
|
217
302
|
}
|
|
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
303
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
304
|
+
if (block.type === 'tool_result') {
|
|
305
|
+
const param: Anthropic.ToolResultBlockParam = {
|
|
306
|
+
type: 'tool_result',
|
|
307
|
+
tool_use_id: block.toolUseId,
|
|
308
|
+
content:
|
|
309
|
+
typeof block.content === 'string'
|
|
310
|
+
? block.content
|
|
311
|
+
: block.content.map((b) => ({ type: 'text', text: b.text }) as Anthropic.TextBlockParam),
|
|
312
|
+
}
|
|
313
|
+
if (block.isError) param.is_error = true
|
|
314
|
+
return param
|
|
315
|
+
}
|
|
316
|
+
const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
317
|
+
if (block.cache) text.cache_control = EPHEMERAL_CACHE
|
|
318
|
+
return text
|
|
319
|
+
}),
|
|
233
320
|
}
|
|
321
|
+
}
|
|
234
322
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
323
|
+
function toSystemParam(
|
|
324
|
+
system: SystemPrompt | undefined,
|
|
325
|
+
): string | Anthropic.TextBlockParam[] | undefined {
|
|
326
|
+
if (system === undefined) return undefined
|
|
327
|
+
if (typeof system === 'string') return system
|
|
328
|
+
if (Array.isArray(system)) {
|
|
329
|
+
return system.map((block) => {
|
|
330
|
+
const param: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
331
|
+
if (block.cache) param.cache_control = EPHEMERAL_CACHE
|
|
332
|
+
return param
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
const param: Anthropic.TextBlockParam = { type: 'text', text: system.text }
|
|
336
|
+
if (system.cache) param.cache_control = EPHEMERAL_CACHE
|
|
337
|
+
return [param]
|
|
338
|
+
}
|
|
238
339
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
340
|
+
function mergeBetas(
|
|
341
|
+
providerBetas: readonly string[],
|
|
342
|
+
callBetas: readonly string[] | undefined,
|
|
343
|
+
): readonly string[] {
|
|
344
|
+
if (!callBetas || callBetas.length === 0) return providerBetas
|
|
345
|
+
const seen = new Set<string>()
|
|
346
|
+
const out: string[] = []
|
|
347
|
+
for (const b of providerBetas) {
|
|
348
|
+
if (seen.has(b)) continue
|
|
349
|
+
seen.add(b)
|
|
350
|
+
out.push(b)
|
|
351
|
+
}
|
|
352
|
+
for (const b of callBetas) {
|
|
353
|
+
if (seen.has(b)) continue
|
|
354
|
+
seen.add(b)
|
|
355
|
+
out.push(b)
|
|
356
|
+
}
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
252
359
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
360
|
+
function addUsage(acc: ChatUsage, u: Anthropic.Usage): void {
|
|
361
|
+
acc.inputTokens += u.input_tokens
|
|
362
|
+
acc.outputTokens += u.output_tokens
|
|
363
|
+
acc.cacheReadTokens += u.cache_read_input_tokens ?? 0
|
|
364
|
+
acc.cacheCreationTokens += u.cache_creation_input_tokens ?? 0
|
|
365
|
+
}
|
|
258
366
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
stopReason = 'max_tokens'
|
|
266
|
-
break
|
|
267
|
-
case 'stop_sequence':
|
|
268
|
-
stopReason = 'stop_sequence'
|
|
269
|
-
break
|
|
270
|
-
}
|
|
367
|
+
function collectText(content: Anthropic.ContentBlock[]): string {
|
|
368
|
+
return content
|
|
369
|
+
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
370
|
+
.map((b) => b.text)
|
|
371
|
+
.join('')
|
|
372
|
+
}
|
|
271
373
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Translate the SDK's response content blocks back into framework
|
|
376
|
+
* `ContentBlock`s for storage in `workingMessages`. We preserve
|
|
377
|
+
* `text` and `tool_use` blocks verbatim; other server-side block
|
|
378
|
+
* types (thinking, server tool blocks) are dropped — V1 doesn't
|
|
379
|
+
* surface them, and re-sending them as part of the assistant turn
|
|
380
|
+
* could confuse the model.
|
|
381
|
+
*/
|
|
382
|
+
function fromAnthropicContent(content: Anthropic.ContentBlock[]): ContentBlock[] {
|
|
383
|
+
const out: ContentBlock[] = []
|
|
384
|
+
for (const block of content) {
|
|
385
|
+
if (block.type === 'text') {
|
|
386
|
+
out.push({ type: 'text', text: block.text } satisfies TextBlock)
|
|
387
|
+
} else if (block.type === 'tool_use') {
|
|
388
|
+
out.push({
|
|
389
|
+
type: 'tool_use',
|
|
390
|
+
id: block.id,
|
|
391
|
+
name: block.name,
|
|
392
|
+
input: block.input,
|
|
393
|
+
} satisfies ToolUseBlock)
|
|
279
394
|
}
|
|
280
395
|
}
|
|
396
|
+
return out
|
|
281
397
|
}
|