@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.
- package/package.json +23 -7
- package/src/agent.ts +43 -5
- package/src/agent_generate_result.ts +32 -0
- package/src/agent_result.ts +7 -0
- package/src/agent_runner.ts +218 -14
- package/src/agent_stream_event.ts +100 -0
- package/src/brain_config.ts +218 -1
- package/src/brain_driver.ts +247 -0
- package/src/brain_error.ts +86 -10
- package/src/brain_manager.ts +359 -11
- package/src/brain_provider.ts +79 -9
- package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
- package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
- package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
- package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
- package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
- package/src/drivers/anthropic/index.ts +1 -0
- package/src/drivers/deepseek/deepseek_brain_driver.ts +117 -0
- package/src/drivers/deepseek/index.ts +1 -0
- package/src/drivers/gemini/gemini_brain_driver.ts +1064 -0
- package/src/drivers/gemini/index.ts +1 -0
- package/src/drivers/minimax/index.ts +1 -0
- package/src/drivers/minimax/minimax_brain_driver.ts +84 -0
- package/src/drivers/ollama/index.ts +1 -0
- package/src/drivers/ollama/ollama_brain_driver.ts +86 -0
- package/src/drivers/openai/index.ts +1 -0
- package/src/drivers/openai/openai_brain_driver.ts +796 -0
- package/src/drivers/openai/openai_helpers.ts +58 -0
- package/src/drivers/openai/openai_message_builder.ts +187 -0
- package/src/drivers/openai/openai_response_mapper.ts +70 -0
- package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
- package/src/drivers/openai/openai_tool_loop.ts +191 -0
- package/src/drivers/openai_compat/index.ts +1 -0
- package/src/drivers/openai_compat/openai_compat_brain_driver.ts +616 -0
- package/src/drivers/openai_responses/index.ts +1 -0
- package/src/drivers/openai_responses/openai_responses_brain_driver.ts +1015 -0
- package/src/drivers/openrouter/index.ts +1 -0
- package/src/drivers/openrouter/openrouter_brain_driver.ts +137 -0
- package/src/drivers/qwen/index.ts +1 -0
- package/src/drivers/qwen/qwen_brain_driver.ts +103 -0
- package/src/index.ts +75 -11
- package/src/mcp/client.ts +243 -0
- package/src/mcp/index.ts +23 -0
- package/src/mcp/oauth.ts +227 -0
- package/src/mcp/pool.ts +106 -0
- package/src/mcp/resolve_mcp_tools.ts +108 -0
- package/src/mcp_server.ts +63 -0
- package/src/output_schema.ts +72 -0
- package/src/persistence/brain_message.ts +34 -0
- package/src/persistence/brain_message_repository.ts +98 -0
- package/src/persistence/brain_store.ts +166 -0
- package/src/persistence/brain_suspended_run.ts +30 -0
- package/src/persistence/brain_suspended_run_repository.ts +59 -0
- package/src/persistence/brain_thread.ts +30 -0
- package/src/persistence/brain_thread_repository.ts +56 -0
- package/src/persistence/database_brain_store.ts +190 -0
- package/src/persistence/index.ts +48 -0
- package/src/persistence/schemas/brain_message_schema.ts +61 -0
- package/src/persistence/schemas/brain_suspended_run_schema.ts +58 -0
- package/src/persistence/schemas/brain_thread_schema.ts +50 -0
- package/src/persistence/schemas/index.ts +3 -0
- package/src/suspended_run.ts +153 -0
- package/src/thread.ts +40 -1
- package/src/tool.ts +7 -0
- package/src/tool_runner.ts +81 -0
- package/src/translate/index.ts +19 -0
- package/src/translate/translate_cache.ts +78 -0
- package/src/translate/translate_provider.ts +46 -0
- package/src/translate/translator.ts +271 -0
- package/src/types.ts +398 -1
- package/src/zod/index.ts +121 -0
- package/src/provider.ts +0 -74
- package/src/providers/anthropic_provider.ts +0 -397
package/src/zod/index.ts
ADDED
|
@@ -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
|
-
}
|