@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.
@@ -1,158 +1,205 @@
1
- import { inject, ConfigurationError, Configuration } from '@strav/kernel'
2
- import { AnthropicProvider } from './providers/anthropic_provider.ts'
3
- import { OpenAIProvider } from './providers/openai_provider.ts'
4
- import type {
5
- AIProvider,
6
- BrainConfig,
7
- ProviderConfig,
8
- CompletionRequest,
9
- CompletionResponse,
10
- BeforeHook,
11
- AfterHook,
12
- TranscribeRequest,
13
- TranscriptionResponse,
14
- } from './types.ts'
15
- import type { MemoryConfig, ThreadStore } from './memory/types.ts'
16
-
17
1
  /**
18
- * Central AI configuration hub.
2
+ * `BrainManager` the per-app facade apps inject and call.
19
3
  *
20
- * Resolved once via the DI container reads the AI config
21
- * and initializes the appropriate provider drivers.
4
+ * Holds the configured `Provider` registry + the default-provider key
5
+ * + the tier-to-model map. Apps call `chat / stream / countTokens`
6
+ * with framework-native types; the manager resolves which provider
7
+ * runs the call (default unless `options.provider` overrides),
8
+ * applies tier sugar (`options.tier` → concrete `model`), and
9
+ * delegates.
22
10
  *
23
- * @example
24
- * app.singleton(BrainManager)
25
- * app.resolve(BrainManager)
11
+ * Constructed by `BrainProvider` at boot from `config.brain`. Apps
12
+ * also build one inline for tests:
26
13
  *
27
- * // Plug in a custom provider
28
- * BrainManager.useProvider(new OllamaProvider())
14
+ * ```ts
15
+ * const brain = new BrainManager({
16
+ * default: 'anthropic',
17
+ * providers: { anthropic: stubProvider },
18
+ * })
19
+ * ```
29
20
  */
30
- @inject
31
- export default class BrainManager {
32
- private static _config: BrainConfig
33
- private static _providers = new Map<string, AIProvider>()
34
- private static _beforeHooks: BeforeHook[] = []
35
- private static _afterHooks: AfterHook[] = []
36
- private static _threadStore: ThreadStore | null = null
37
- private static _memoryConfig: MemoryConfig = {}
38
-
39
- constructor(config: Configuration) {
40
- BrainManager._config = {
41
- default: config.get('ai.default', 'anthropic') as string,
42
- providers: config.get('ai.providers', {}) as Record<string, ProviderConfig>,
43
- maxTokens: config.get('ai.maxTokens', 4096) as number,
44
- temperature: config.get('ai.temperature', 0.7) as number,
45
- maxIterations: config.get('ai.maxIterations', 10) as number,
46
- }
47
21
 
48
- BrainManager._memoryConfig = config.get('ai.memory', {}) as MemoryConfig
49
-
50
- for (const [name, providerConfig] of Object.entries(BrainManager._config.providers)) {
51
- BrainManager._providers.set(name, BrainManager.createProvider(name, providerConfig))
52
- }
53
- }
22
+ import type { Agent } from './agent.ts'
23
+ import type { AgentResult } from './agent_result.ts'
24
+ import { AgentRunner } from './agent_runner.ts'
25
+ import { BrainError } from './brain_error.ts'
26
+ import type { ModelTier } from './types.ts'
27
+ import type {
28
+ ChatOptions,
29
+ ChatResult,
30
+ Message,
31
+ StreamEvent,
32
+ } from './types.ts'
33
+ import type { Provider, RunWithToolsOptions } from './provider.ts'
34
+ import type { Tool } from './tool.ts'
35
+ import { DEFAULT_TIERS } from './brain_config.ts'
36
+
37
+ /** Container-aware Agent constructor resolver — `BrainProvider` installs one wired to `app.resolve(...)`. */
38
+ export type AgentResolver = <A extends Agent>(cls: new (...args: never[]) => A) => A
39
+
40
+ export interface BrainManagerOptions {
41
+ /** Name of the default provider — must exist in `providers`. */
42
+ default: string
43
+ /** Provider registry keyed by name. */
44
+ providers: Record<string, Provider>
45
+ /** Tier-to-model overrides; merged on top of the framework defaults. */
46
+ tiers?: Partial<Record<ModelTier, string>>
47
+ /** Default for `ChatOptions.cache` when the call site doesn't pass one. */
48
+ defaultCache?: boolean
49
+ }
54
50
 
55
- private static createProvider(name: string, config: ProviderConfig): AIProvider {
56
- const driver = config.driver ?? name
57
- switch (driver) {
58
- case 'anthropic':
59
- return new AnthropicProvider(config)
60
- case 'openai':
61
- return new OpenAIProvider(config, name)
62
- default:
63
- throw new ConfigurationError(
64
- `Unknown AI provider driver: ${driver}. Use BrainManager.useProvider() for custom providers.`
65
- )
51
+ export class BrainManager {
52
+ readonly defaultProvider: string
53
+ private readonly providers: Map<string, Provider>
54
+ private readonly tiers: Record<ModelTier, string>
55
+ private readonly defaultCache: boolean
56
+
57
+ constructor(options: BrainManagerOptions) {
58
+ if (!options.providers[options.default]) {
59
+ throw new BrainError(
60
+ `BrainManager: default provider "${options.default}" is not registered.`,
61
+ { context: { default: options.default, available: Object.keys(options.providers) } },
62
+ )
66
63
  }
64
+ this.defaultProvider = options.default
65
+ this.providers = new Map(Object.entries(options.providers))
66
+ this.tiers = { ...DEFAULT_TIERS, ...(options.tiers ?? {}) }
67
+ this.defaultCache = options.defaultCache ?? false
67
68
  }
68
69
 
69
- static get config(): BrainConfig {
70
- if (!BrainManager._config) {
71
- throw new ConfigurationError(
72
- 'BrainManager not configured. Resolve it through the container first.'
73
- )
70
+ /** Resolve a provider by name. Default when no name passed. Throws when unknown. */
71
+ provider(name?: string): Provider {
72
+ const key = name ?? this.defaultProvider
73
+ const provider = this.providers.get(key)
74
+ if (!provider) {
75
+ throw new BrainError(`BrainManager: no provider registered under "${key}".`, {
76
+ context: { requested: key, available: [...this.providers.keys()] },
77
+ })
74
78
  }
75
- return BrainManager._config
79
+ return provider
76
80
  }
77
81
 
78
- /** Get a provider by name, or the default provider. */
79
- static provider(name?: string): AIProvider {
80
- const key = name ?? BrainManager._config.default
81
- const p = BrainManager._providers.get(key)
82
- if (!p) throw new ConfigurationError(`AI provider "${key}" not configured.`)
83
- return p
82
+ /**
83
+ * One-shot chat: send the messages, await the full reply.
84
+ *
85
+ * Accepts either a bare prompt string (treated as a single
86
+ * user-role message) or a typed `Message[]` for multi-turn /
87
+ * pre-built conversations.
88
+ */
89
+ async chat(input: string | readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
90
+ const messages = normalizeInput(input)
91
+ const resolved = this.applyDefaults(options)
92
+ return this.provider(options.provider).chat(messages, resolved)
84
93
  }
85
94
 
86
- /** Swap or add a provider at runtime (e.g., for testing or a custom provider). */
87
- static useProvider(provider: AIProvider): void {
88
- BrainManager._providers.set(provider.name, provider)
95
+ /**
96
+ * Stream the reply. Yields a `text` event per delta and a single
97
+ * terminal `stop` event with usage + stop-reason. Apps that want
98
+ * just the final message use `chat()` instead — this surface is
99
+ * for UI streaming.
100
+ */
101
+ stream(
102
+ input: string | readonly Message[],
103
+ options: ChatOptions = {},
104
+ ): AsyncIterable<StreamEvent> {
105
+ const messages = normalizeInput(input)
106
+ const resolved = this.applyDefaults(options)
107
+ return this.provider(options.provider).stream(messages, resolved)
89
108
  }
90
109
 
91
- /** Get the configured memory settings. */
92
- static get memoryConfig(): MemoryConfig {
93
- return BrainManager._memoryConfig
110
+ /**
111
+ * Count input tokens for the given messages + options. Returns
112
+ * `null` when the configured provider doesn't expose a token count
113
+ * helper (no `countTokens` method) — apps can fall back to a local
114
+ * estimator at the call site.
115
+ */
116
+ async countTokens(
117
+ input: string | readonly Message[],
118
+ options: ChatOptions = {},
119
+ ): Promise<number | null> {
120
+ const provider = this.provider(options.provider)
121
+ if (!provider.countTokens) return null
122
+ const messages = normalizeInput(input)
123
+ const resolved = this.applyDefaults(options)
124
+ return provider.countTokens(messages, resolved)
94
125
  }
95
126
 
96
- /** Get the registered thread store, if any. */
97
- static get threadStore(): ThreadStore | null {
98
- return BrainManager._threadStore
127
+ /**
128
+ * Run an agentic loop: send `messages` + `tools` to the model;
129
+ * execute any tool the model calls; loop until the model returns
130
+ * a terminal `stop_reason` (`'end_turn'`) or `maxIterations` is hit.
131
+ *
132
+ * Throws `BrainError` when the configured provider doesn't
133
+ * implement `runWithTools` (V1: OpenAI / Gemini / DeepSeek providers
134
+ * don't yet — only `AnthropicProvider`).
135
+ */
136
+ async runTools(
137
+ input: string | readonly Message[],
138
+ tools: readonly Tool[],
139
+ options: RunWithToolsOptions = {},
140
+ ): Promise<AgentResult> {
141
+ const provider = this.provider(options.provider)
142
+ if (!provider.runWithTools) {
143
+ throw new BrainError(
144
+ `BrainManager.runTools: provider "${provider.name}" does not implement runWithTools. Use a provider that supports tool use (V1: Anthropic).`,
145
+ { context: { provider: provider.name } },
146
+ )
147
+ }
148
+ const messages = normalizeInput(input)
149
+ const resolved = this.applyDefaults(options) as RunWithToolsOptions
150
+ return provider.runWithTools(messages, tools, resolved)
99
151
  }
100
152
 
101
- /** Register a thread store for persistence (e.g., DatabaseThreadStore). */
102
- static useThreadStore(store: ThreadStore): void {
103
- BrainManager._threadStore = store
153
+ /**
154
+ * Resolve an `Agent` subclass from the container and return an
155
+ * `AgentRunner` ready to receive `input(...)` and `run()`. Apps
156
+ * `@inject()`-decorate their Agent subclass so constructor
157
+ * injection of dependencies (Repositories, services, etc.) flows
158
+ * through normally.
159
+ */
160
+ agent<A extends Agent>(AgentClass: new (...args: never[]) => A, instance?: A): AgentRunner {
161
+ const agent = instance ?? this.resolveAgent(AgentClass)
162
+ return new AgentRunner(this, agent)
104
163
  }
105
164
 
106
- /** Register a hook that runs before every completion. */
107
- static before(hook: BeforeHook): void {
108
- BrainManager._beforeHooks.push(hook)
109
- }
165
+ // ─── Internal ────────────────────────────────────────────────────────────
110
166
 
111
- /** Register a hook that runs after every completion. */
112
- static after(hook: AfterHook): void {
113
- BrainManager._afterHooks.push(hook)
167
+ private resolveAgent<A extends Agent>(AgentClass: new (...args: never[]) => A): A {
168
+ if (this.agentResolver) return this.agentResolver(AgentClass)
169
+ // Fallback: assume the Agent class is constructible without args.
170
+ // Apps that need DI on the agent register a resolver via
171
+ // `setAgentResolver` (BrainProvider wires this to the container).
172
+ return new (AgentClass as unknown as new () => A)()
114
173
  }
115
174
 
116
175
  /**
117
- * Run a completion through the named provider, with before/after hooks.
118
- * Used internally by AgentRunner and the `brain` helper.
176
+ * Internal `BrainProvider` calls this at boot to plug in the
177
+ * container's resolution function so `brain.agent(MyAgent)` runs
178
+ * `app.resolve(MyAgent)` under the hood. Apps that build a
179
+ * `BrainManager` by hand for tests can leave this unset and pass
180
+ * a pre-constructed agent to `brain.agent(_, instance)`.
119
181
  */
120
- static async complete(
121
- providerName: string | undefined,
122
- request: CompletionRequest
123
- ): Promise<CompletionResponse> {
124
- for (const hook of BrainManager._beforeHooks) await hook(request)
125
- const response = await BrainManager.provider(providerName).complete(request)
126
- for (const hook of BrainManager._afterHooks) await hook(request, response)
127
- return response
182
+ setAgentResolver(resolver: AgentResolver): void {
183
+ this.agentResolver = resolver
128
184
  }
129
185
 
130
- /**
131
- * Transcribe audio through the named provider. Throws a clear
132
- * ConfigurationError if the provider doesn't implement `transcribe()`
133
- * (Anthropic, at time of writing). Hooks are not invoked — they're
134
- * shaped for chat completions and don't carry an audio analogue.
135
- */
136
- static async transcribe(
137
- providerName: string | undefined,
138
- request: TranscribeRequest
139
- ): Promise<TranscriptionResponse> {
140
- const provider = BrainManager.provider(providerName)
141
- if (!provider.transcribe) {
142
- throw new ConfigurationError(
143
- `AI provider "${provider.name}" does not support transcribe(). ` +
144
- `Use the OpenAI or Google providers, or register a custom one via BrainManager.useProvider().`
145
- )
186
+ private agentResolver: AgentResolver | undefined
187
+
188
+ private applyDefaults(options: ChatOptions): ChatOptions {
189
+ const resolved: ChatOptions = { ...options }
190
+ if (resolved.model === undefined && resolved.tier !== undefined) {
191
+ resolved.model = this.tiers[resolved.tier]
192
+ }
193
+ if (resolved.cache === undefined && this.defaultCache) {
194
+ resolved.cache = true
146
195
  }
147
- return provider.transcribe(request)
196
+ return resolved
148
197
  }
198
+ }
149
199
 
150
- /** Clear all providers, hooks, and stores (for testing). */
151
- static reset(): void {
152
- BrainManager._providers.clear()
153
- BrainManager._beforeHooks = []
154
- BrainManager._afterHooks = []
155
- BrainManager._threadStore = null
156
- BrainManager._memoryConfig = {}
200
+ function normalizeInput(input: string | readonly Message[]): readonly Message[] {
201
+ if (typeof input === 'string') {
202
+ return [{ role: 'user', content: input }]
157
203
  }
204
+ return input
158
205
  }
@@ -1,16 +1,100 @@
1
- import { ServiceProvider } from '@strav/kernel'
2
- import type { Application } from '@strav/kernel'
3
- import BrainManager from './brain_manager.ts'
1
+ /**
2
+ * `BrainProvider` `ServiceProvider` that wires `BrainManager` into
3
+ * the container from `config.brain`.
4
+ *
5
+ * Reads the brain config at register time, instantiates every
6
+ * configured provider (today: just Anthropic), and binds a
7
+ * `BrainManager` singleton. Apps inject it the standard way:
8
+ *
9
+ * ```ts
10
+ * @inject()
11
+ * class GreetingService {
12
+ * constructor(private readonly brain: BrainManager) {}
13
+ *
14
+ * async greet(name: string): Promise<string> {
15
+ * const { text } = await this.brain.chat(`Greet ${name} warmly.`)
16
+ * return text
17
+ * }
18
+ * }
19
+ * ```
20
+ *
21
+ * Eager construction is on purpose — a missing API key or unknown
22
+ * driver should fail at boot, not at the first call. The `boot()`
23
+ * step resolves the manager so `ConfigError`s surface before any
24
+ * request hits.
25
+ */
4
26
 
5
- export default class BrainProvider extends ServiceProvider {
6
- readonly name = 'brain'
27
+ import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
28
+ import { BrainManager } from './brain_manager.ts'
29
+ import type { BrainConfigShape, ProviderConfig } from './brain_config.ts'
30
+ import { AnthropicProvider } from './providers/anthropic_provider.ts'
31
+ import type { Provider } from './provider.ts'
32
+
33
+ export class BrainProvider extends ServiceProvider {
34
+ override readonly name = 'brain'
7
35
  override readonly dependencies = ['config']
8
36
 
9
37
  override register(app: Application): void {
10
- app.singleton(BrainManager)
38
+ app.singleton(BrainManager, (c) => {
39
+ const config = c.resolve(ConfigRepository).get('brain') as BrainConfigShape | undefined
40
+ if (!config) {
41
+ throw new ConfigError(
42
+ 'BrainProvider: `config.brain` is missing. Add a `config/brain.ts` with at least `default` + `providers`.',
43
+ )
44
+ }
45
+ if (!config.providers || Object.keys(config.providers).length === 0) {
46
+ throw new ConfigError(
47
+ 'BrainProvider: `config.brain.providers` must have at least one entry.',
48
+ )
49
+ }
50
+ if (!config.providers[config.default]) {
51
+ throw new ConfigError(
52
+ `BrainProvider: default provider "${config.default}" is not declared in config.brain.providers.`,
53
+ )
54
+ }
55
+
56
+ const providers: Record<string, Provider> = {}
57
+ for (const [name, entry] of Object.entries(config.providers)) {
58
+ providers[name] = buildProvider(name, entry)
59
+ }
60
+
61
+ const options: ConstructorParameters<typeof BrainManager>[0] = {
62
+ default: config.default,
63
+ providers,
64
+ }
65
+ if (config.tiers !== undefined) options.tiers = config.tiers
66
+ if (config.cache?.auto !== undefined) options.defaultCache = config.cache.auto
67
+ const manager = new BrainManager(options)
68
+ // Plug in the container so `brain.agent(MyAgent)` resolves
69
+ // its constructor deps through `@inject()` like every other
70
+ // injected class. The variance widening at the boundary
71
+ // (`never[]` ↔ `any[]`) is purely a TS typing artifact — the
72
+ // container call is identical to a direct `c.resolve(MyAgent)`.
73
+ manager.setAgentResolver(<A>(cls: new (...args: never[]) => A) =>
74
+ c.resolve(cls as unknown as new (...args: unknown[]) => A),
75
+ )
76
+ return manager
77
+ })
11
78
  }
12
79
 
13
80
  override boot(app: Application): void {
81
+ // Force-resolve so config errors surface at boot, not on first call.
14
82
  app.resolve(BrainManager)
15
83
  }
16
84
  }
85
+
86
+ function buildProvider(name: string, config: ProviderConfig): Provider {
87
+ switch (config.driver) {
88
+ case 'anthropic':
89
+ if (!config.apiKey) {
90
+ throw new ConfigError(
91
+ `BrainProvider: anthropic provider "${name}" is missing apiKey. Source from env('ANTHROPIC_API_KEY').`,
92
+ )
93
+ }
94
+ return new AnthropicProvider(name, config)
95
+ default:
96
+ throw new ConfigError(
97
+ `BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic.`,
98
+ )
99
+ }
100
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `defineTool({ name, description, inputSchema, execute })` — typed
3
+ * factory mirroring `defineWorkflow` / `defineMachine` / `defineDurable`.
4
+ *
5
+ * ```ts
6
+ * const getWeather = defineTool({
7
+ * name: 'get_weather',
8
+ * description: 'Get current weather for a location.',
9
+ * inputSchema: {
10
+ * type: 'object',
11
+ * properties: { city: { type: 'string' } },
12
+ * required: ['city'],
13
+ * },
14
+ * execute: async ({ city }: { city: string }, ctx) => {
15
+ * return weatherService.lookup(city, ctx.context.userId as string)
16
+ * },
17
+ * })
18
+ * ```
19
+ *
20
+ * The generic parameters are usually inferred from `execute`'s first
21
+ * arg + return type; apps that want explicit typing pass them.
22
+ */
23
+
24
+ import type { Tool, ToolContext } from './tool.ts'
25
+
26
+ export interface DefineToolSpec<TInput, TOutput> {
27
+ name: string
28
+ description: string
29
+ inputSchema: Record<string, unknown>
30
+ execute(input: TInput, ctx: ToolContext): Promise<TOutput>
31
+ }
32
+
33
+ export function defineTool<TInput = unknown, TOutput = unknown>(
34
+ spec: DefineToolSpec<TInput, TOutput>,
35
+ ): Tool<TInput, TOutput> {
36
+ return {
37
+ name: spec.name,
38
+ description: spec.description,
39
+ inputSchema: spec.inputSchema,
40
+ execute: spec.execute,
41
+ }
42
+ }
package/src/index.ts CHANGED
@@ -1,48 +1,46 @@
1
- export { default, default as BrainManager } from './brain_manager.ts'
1
+ // Public API of @strav/brain.
2
+ //
3
+ // V1: Provider interface + AnthropicProvider, BrainManager, Thread,
4
+ // BrainProvider service-wiring, prompt caching.
5
+ // V2 (this slice): tools + agents — defineTool, Agent base + AgentRunner,
6
+ // BrainManager.runTools / .agent(Class), Provider.runWithTools.
7
+ // Still deferred: MCP, embeddings, streaming agent loops, server-side
8
+ // tools, structured outputs, other providers.
2
9
 
3
- // Provider
4
- export { default as BrainProvider } from './brain_provider.ts'
5
- export { brain, AgentRunner, Thread } from './helpers.ts'
6
10
  export { Agent } from './agent.ts'
7
- export { defineTool, defineToolbox } from './tool.ts'
8
- export { defineMcpToolbox } from './mcp_toolbox.ts'
9
- export type { McpToolboxOptions } from './mcp_toolbox.ts'
10
- export { Workflow } from './workflow.ts'
11
+ export type { AgentResult } from './agent_result.ts'
12
+ export { AgentRunner } from './agent_runner.ts'
13
+ export {
14
+ type AnthropicProviderConfig,
15
+ type BrainCacheConfig,
16
+ type BrainConfigShape,
17
+ DEFAULT_MODEL,
18
+ DEFAULT_TIERS,
19
+ type ProviderConfig,
20
+ } from './brain_config.ts'
21
+ export { BrainError } from './brain_error.ts'
22
+ export {
23
+ type AgentResolver,
24
+ BrainManager,
25
+ type BrainManagerOptions,
26
+ } from './brain_manager.ts'
27
+ export { BrainProvider } from './brain_provider.ts'
28
+ export { defineTool, type DefineToolSpec } from './define_tool.ts'
11
29
  export { AnthropicProvider } from './providers/anthropic_provider.ts'
12
- export { GoogleProvider } from './providers/google_provider.ts'
13
- export { OpenAIProvider } from './providers/openai_provider.ts'
14
- export { OpenAIResponsesProvider } from './providers/openai_responses_provider.ts'
15
- export { parseSSE } from './utils/sse_parser.ts'
16
- export { zodToJsonSchema } from './utils/schema.ts'
30
+ export type { Provider, RunWithToolsOptions } from './provider.ts'
31
+ export { Thread, type ThreadOptions, type ThreadState } from './thread.ts'
32
+ export type { Tool, ToolContext } from './tool.ts'
33
+ export { ToolExecutionError } from './tool_execution_error.ts'
17
34
  export type {
18
- AIProvider,
19
- BrainConfig,
20
- ProviderConfig,
21
- CompletionRequest,
22
- CompletionResponse,
23
- Message,
35
+ ChatOptions,
36
+ ChatResult,
37
+ ChatUsage,
24
38
  ContentBlock,
25
- ToolCall,
26
- ToolDefinition,
27
- StreamChunk,
28
- Usage,
29
- AgentResult,
30
- ToolCallRecord,
31
- AgentEvent,
32
- WorkflowResult,
33
- EmbeddingResponse,
34
- JsonSchema,
35
- SSEEvent,
36
- BeforeHook,
37
- AfterHook,
38
- SerializedThread,
39
- SerializedAgentState,
40
- SuspendedRun,
41
- ToolCallResult,
42
- OutputSchema,
39
+ Message,
40
+ ModelTier,
41
+ StreamEvent,
42
+ SystemPrompt,
43
+ TextBlock,
44
+ ToolResultBlock,
45
+ ToolUseBlock,
43
46
  } from './types.ts'
44
- export type { ChatOptions, GenerateOptions, GenerateResult, EmbedOptions } from './helpers.ts'
45
- export type { WorkflowContext } from './workflow.ts'
46
-
47
- // Memory
48
- export * from './memory/index.ts'
@@ -0,0 +1,74 @@
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
+ }