@strav/brain 1.0.0-alpha.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +23 -7
  2. package/src/agent.ts +43 -5
  3. package/src/agent_generate_result.ts +32 -0
  4. package/src/agent_result.ts +7 -0
  5. package/src/agent_runner.ts +218 -14
  6. package/src/agent_stream_event.ts +100 -0
  7. package/src/brain_config.ts +218 -1
  8. package/src/brain_driver.ts +247 -0
  9. package/src/brain_error.ts +86 -10
  10. package/src/brain_manager.ts +359 -11
  11. package/src/brain_provider.ts +79 -9
  12. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  13. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  14. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  15. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  16. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  17. package/src/drivers/anthropic/index.ts +1 -0
  18. package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
  19. package/src/drivers/deepseek/index.ts +1 -0
  20. package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
  21. package/src/drivers/gemini/index.ts +1 -0
  22. package/src/drivers/minimax/index.ts +1 -0
  23. package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
  24. package/src/drivers/ollama/index.ts +1 -0
  25. package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
  26. package/src/drivers/openai/index.ts +1 -0
  27. package/src/drivers/openai/openai_brain_driver.ts +796 -0
  28. package/src/drivers/openai/openai_helpers.ts +58 -0
  29. package/src/drivers/openai/openai_message_builder.ts +187 -0
  30. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  31. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  32. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  33. package/src/drivers/openai_compat/index.ts +1 -0
  34. package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
  35. package/src/drivers/openai_responses/index.ts +1 -0
  36. package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
  37. package/src/drivers/openrouter/index.ts +1 -0
  38. package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
  39. package/src/drivers/qwen/index.ts +1 -0
  40. package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
  41. package/src/index.ts +75 -11
  42. package/src/mcp/client.ts +243 -0
  43. package/src/mcp/index.ts +23 -0
  44. package/src/mcp/oauth.ts +227 -0
  45. package/src/mcp/pool.ts +106 -0
  46. package/src/mcp/resolve_mcp_tools.ts +108 -0
  47. package/src/mcp_server.ts +63 -0
  48. package/src/output_schema.ts +72 -0
  49. package/src/persistence/brain_message.ts +34 -0
  50. package/src/persistence/brain_message_repository.ts +98 -0
  51. package/src/persistence/brain_store.ts +166 -0
  52. package/src/persistence/brain_suspended_run.ts +30 -0
  53. package/src/persistence/brain_suspended_run_repository.ts +59 -0
  54. package/src/persistence/brain_thread.ts +30 -0
  55. package/src/persistence/brain_thread_repository.ts +56 -0
  56. package/src/persistence/database_brain_store.ts +190 -0
  57. package/src/persistence/index.ts +48 -0
  58. package/src/persistence/schemas/brain_message_schema.ts +61 -0
  59. package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
  60. package/src/persistence/schemas/brain_thread_schema.ts +50 -0
  61. package/src/persistence/schemas/index.ts +3 -0
  62. package/src/suspended_run.ts +153 -0
  63. package/src/thread.ts +40 -1
  64. package/src/tool.ts +7 -0
  65. package/src/tool_runner.ts +81 -0
  66. package/src/translate/index.ts +19 -0
  67. package/src/translate/translate_cache.ts +78 -0
  68. package/src/translate/translate_provider.ts +46 -0
  69. package/src/translate/translator.ts +271 -0
  70. package/src/types.ts +398 -1
  71. package/src/zod/index.ts +121 -0
  72. package/src/provider.ts +0 -74
  73. package/src/providers/anthropic_provider.ts +0 -397
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `@strav/brain/zod` — Zod-flavored helpers on top of the
3
+ * schema-library-agnostic core.
4
+ *
5
+ * The default `@strav/brain` import deliberately doesn't depend on
6
+ * Zod — `Tool.inputSchema` and `OutputSchema.jsonSchema` are plain
7
+ * JSON Schema so apps stay free to pick Ajv, Valibot, ArkType, or
8
+ * nothing at all. This sub-path opt-in adds two thin wrappers for
9
+ * apps that already use Zod:
10
+ *
11
+ * - `outputSchema(z, opts?)` turns a Zod schema into an
12
+ * `OutputSchema<z.infer<typeof z>>` — `jsonSchema` is derived
13
+ * via Zod's built-in `z.toJSONSchema`, and `parse` is wired to
14
+ * `z.parse`. Apps then pass the result straight to
15
+ * `BrainManager.generate(input, schema)`.
16
+ *
17
+ * - `tool({ name, description, input, execute })` turns a Zod
18
+ * schema for the tool's input into a framework `Tool` — the
19
+ * wrapper validates the model's raw input through the Zod
20
+ * schema before calling the app's `execute`. Apps get inferred
21
+ * types on `execute(input)` for free.
22
+ *
23
+ * `zod` is an optional peer dependency. Apps that don't use Zod
24
+ * don't install it, don't bundle it, and never import this
25
+ * sub-path — they keep using `defineTool` / hand-written
26
+ * `OutputSchema` literals with raw JSON Schema.
27
+ */
28
+
29
+ import { z } from 'zod'
30
+ import type { OutputSchema } from '../output_schema.ts'
31
+ import type { Tool, ToolContext } from '../tool.ts'
32
+
33
+ /**
34
+ * Options for `outputSchema`. `name` defaults to `'output'` —
35
+ * apps that surface multiple schemas in logs or to OpenAI's wire
36
+ * format should pass a stable, descriptive identifier.
37
+ */
38
+ export interface OutputSchemaOptions {
39
+ /** Identifier — defaults to `'output'`. */
40
+ name?: string
41
+ /** Optional model-facing hint. Defaults to the Zod schema's `.describe(…)` if set. */
42
+ description?: string
43
+ }
44
+
45
+ /**
46
+ * Build an `OutputSchema<T>` from a Zod schema. The returned shape
47
+ * is ready to pass to `BrainManager.generate(...)`.
48
+ *
49
+ * ```ts
50
+ * const CityZ = z.object({ city: z.string(), population: z.number().int() })
51
+ * const { value } = await brain.generate('Capital of France?', outputSchema(CityZ, { name: 'city_answer' }))
52
+ * // ^? { city: string; population: number }
53
+ * ```
54
+ */
55
+ export function outputSchema<T>(
56
+ schema: z.ZodType<T>,
57
+ options: OutputSchemaOptions = {},
58
+ ): OutputSchema<T> {
59
+ const description = options.description ?? zodDescription(schema)
60
+ const result: OutputSchema<T> = {
61
+ name: options.name ?? 'output',
62
+ jsonSchema: z.toJSONSchema(schema) as Record<string, unknown>,
63
+ parse: (value) => schema.parse(value),
64
+ }
65
+ if (description !== undefined) result.description = description
66
+ return result
67
+ }
68
+
69
+ /**
70
+ * Spec passed to `tool(...)`. `execute` receives the model's input
71
+ * already validated + typed against `input` — no need to call
72
+ * `input.parse` manually.
73
+ */
74
+ export interface ZodToolSpec<TInput, TOutput> {
75
+ name: string
76
+ description: string
77
+ input: z.ZodType<TInput>
78
+ execute(input: TInput, ctx: ToolContext): Promise<TOutput>
79
+ }
80
+
81
+ /**
82
+ * Build a framework `Tool` from a Zod-typed spec. The wrapper
83
+ * derives `inputSchema` via `z.toJSONSchema` and validates the
84
+ * model's raw input through `input.parse` before delegating to
85
+ * `execute`. Validation failures propagate as `ZodError`; the
86
+ * agentic loop wraps that into a `ToolExecutionError`.
87
+ *
88
+ * ```ts
89
+ * const search = tool({
90
+ * name: 'search_orders',
91
+ * description: 'Look up an order by id.',
92
+ * input: z.object({ orderId: z.string() }),
93
+ * async execute({ orderId }, ctx) {
94
+ * // ^? { orderId: string }
95
+ * return await orders.find(orderId, ctx.context)
96
+ * },
97
+ * })
98
+ * ```
99
+ */
100
+ export function tool<TInput, TOutput>(
101
+ spec: ZodToolSpec<TInput, TOutput>,
102
+ ): Tool<TInput, TOutput> {
103
+ const jsonSchema = z.toJSONSchema(spec.input) as Record<string, unknown>
104
+ return {
105
+ name: spec.name,
106
+ description: spec.description,
107
+ inputSchema: jsonSchema,
108
+ async execute(raw: TInput, ctx: ToolContext): Promise<TOutput> {
109
+ const parsed = spec.input.parse(raw)
110
+ return spec.execute(parsed, ctx)
111
+ },
112
+ }
113
+ }
114
+
115
+ function zodDescription(schema: z.ZodType<unknown>): string | undefined {
116
+ // Zod stores `.describe(…)` on the schema's `_def`; surface it
117
+ // as the model-facing hint when callers don't pass one
118
+ // explicitly.
119
+ const def = (schema as unknown as { description?: string }).description
120
+ return typeof def === 'string' && def.length > 0 ? def : undefined
121
+ }
package/src/provider.ts DELETED
@@ -1,74 +0,0 @@
1
- /**
2
- * `Provider` — the contract every brain backend implements.
3
- *
4
- * Each concrete provider (Anthropic, OpenAI later, Gemini later,
5
- * DeepSeek later) wraps the vendor's SDK and translates the framework
6
- * shapes (`Message`, `ChatOptions`) into the vendor's native request,
7
- * then translates the response back into `ChatResult` / `StreamEvent`.
8
- *
9
- * Providers are values, not classes — apps use them via the
10
- * `BrainManager` facade. The interface is exported so apps that need
11
- * to plug in a custom provider (e.g. a local Ollama) can do so without
12
- * subclassing.
13
- */
14
-
15
- import type { AgentResult } from './agent_result.ts'
16
- import type { Tool } from './tool.ts'
17
- import type {
18
- ChatOptions,
19
- ChatResult,
20
- Message,
21
- StreamEvent,
22
- } from './types.ts'
23
-
24
- export interface RunWithToolsOptions extends ChatOptions {
25
- /** Safety ceiling on tool-use round-trips. Default `10`. */
26
- maxIterations?: number
27
- /** Free-form context bag passed to every tool's `execute(input, ctx)`. */
28
- context?: Record<string, unknown>
29
- }
30
-
31
- export interface Provider {
32
- /** Identifier — matches the `config.brain.providers` key. */
33
- readonly name: string
34
-
35
- /**
36
- * Generate a single reply. Awaits the full response; for
37
- * token-by-token rendering use `stream()`.
38
- */
39
- chat(messages: readonly Message[], options?: ChatOptions): Promise<ChatResult>
40
-
41
- /**
42
- * Stream the reply as it's generated. The async iterable yields
43
- * `text` events for each delta and a final `stop` event with usage
44
- * + stop-reason. Apps that want the full collected message at the
45
- * end pass the same `messages` to `chat()` instead; this surface is
46
- * for UI streaming, not for "make one call and get the message".
47
- */
48
- stream(messages: readonly Message[], options?: ChatOptions): AsyncIterable<StreamEvent>
49
-
50
- /**
51
- * Count input tokens for a given message set + options. Used by
52
- * apps that need to budget context before sending. Optional — not
53
- * every provider exposes a cheap token-count endpoint, so the
54
- * implementation may approximate.
55
- */
56
- countTokens?(messages: readonly Message[], options?: ChatOptions): Promise<number>
57
-
58
- /**
59
- * Agentic loop. Sends the `messages` + `tools` to the model;
60
- * detects tool-use blocks in the response; runs the matching
61
- * tool's `execute`; appends the result and re-asks. Loops until
62
- * the model returns `stop_reason: 'end_turn'` (or its
63
- * provider-specific equivalent) or `maxIterations` is hit.
64
- *
65
- * Optional on the interface so providers that don't (yet) support
66
- * tool use can omit it; `BrainManager.runTools` throws a
67
- * `BrainError` when the configured provider lacks the method.
68
- */
69
- runWithTools?(
70
- messages: readonly Message[],
71
- tools: readonly Tool[],
72
- options?: RunWithToolsOptions,
73
- ): Promise<AgentResult>
74
- }
@@ -1,397 +0,0 @@
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'
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'
45
-
46
- const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
47
-
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
- }
74
-
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)
79
- }
80
-
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
- }
102
-
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
- }
118
-
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,
142
- }
143
- let iterations = 0
144
- let lastStopReason: string | null = null
145
-
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
- }))
153
-
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
- }
173
- }
174
-
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
- )
191
- }
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)
200
- }
201
- const resultBlock: ToolResultBlock = {
202
- type: 'tool_result',
203
- toolUseId: block.id,
204
- content: typeof output === 'string' ? output : JSON.stringify(output),
205
- }
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,
218
- }
219
- }
220
- }
221
- }
222
-
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),
234
- }
235
-
236
- const system = toSystemParam(options.system)
237
- if (system !== undefined) params.system = system
238
-
239
- if (options.thinking === 'adaptive') {
240
- params.thinking = { type: 'adaptive' }
241
- } else if (options.thinking === 'disabled') {
242
- params.thinking = { type: 'disabled' }
243
- }
244
-
245
- if (options.effort !== undefined) {
246
- params.output_config = { effort: options.effort }
247
- }
248
-
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
253
- }
254
-
255
- const betas = mergeBetas(this.betas, options.betas)
256
- if (betas.length > 0) {
257
- ;(params as { betas?: readonly string[] }).betas = betas
258
- }
259
-
260
- return params
261
- }
262
-
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
- }
277
-
278
- // ─── Shape converters ─────────────────────────────────────────────────────
279
-
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
- }
288
-
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>,
302
- }
303
- }
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
- }),
320
- }
321
- }
322
-
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
- }
339
-
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
- }
359
-
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
- }
366
-
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
- }
373
-
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)
394
- }
395
- }
396
- return out
397
- }