@strav/brain 0.4.31 → 1.0.0-alpha.8

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 CHANGED
@@ -1,32 +1,29 @@
1
1
  {
2
2
  "name": "@strav/brain",
3
- "version": "0.4.31",
3
+ "version": "1.0.0-alpha.8",
4
+ "description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching. Anthropic provider in V1; OpenAI / Gemini / DeepSeek follow.",
4
5
  "type": "module",
5
- "description": "AI module for the Strav framework",
6
- "license": "MIT",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
7
8
  "exports": {
8
- ".": "./src/index.ts",
9
- "./*": "./src/*.ts"
9
+ ".": "./src/index.ts"
10
10
  },
11
11
  "files": [
12
- "src/",
13
- "package.json",
14
- "tsconfig.json",
15
- "CHANGELOG.md"
12
+ "src",
13
+ "README.md"
16
14
  ],
17
- "peerDependencies": {
18
- "@strav/kernel": "0.4.31"
15
+ "engines": {
16
+ "bun": ">=1.3.14"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
19
20
  },
20
21
  "dependencies": {
21
- "@strav/mcp": "0.4.31",
22
- "@strav/workflow": "0.4.31",
23
- "zod": "^3.25 || ^4.0"
22
+ "@strav/kernel": "1.0.0-alpha.8",
23
+ "@anthropic-ai/sdk": "^0.100.0"
24
24
  },
25
- "devDependencies": {
26
- "@strav/http": "0.4.31"
25
+ "peerDependencies": {
26
+ "@types/bun": ">=1.3.14"
27
27
  },
28
- "scripts": {
29
- "test": "bun test tests/",
30
- "typecheck": "tsc --noEmit"
31
- }
28
+ "devDependencies": null
32
29
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Brain configuration shape — what `config.brain` looks like.
3
+ *
4
+ * Mirrors the manager-pattern config used by other Strav packages
5
+ * (auth.guards, mail.transports, database.connections): a `default`
6
+ * provider key + a `providers` map keyed by name. Each provider entry
7
+ * carries its driver and driver-specific options.
8
+ *
9
+ * `tiers` map model-tier sugar (`fast` / `balanced` / `powerful`) to
10
+ * concrete model IDs. The `'fast' → claude-haiku-4-5` etc. defaults
11
+ * apply when this section is omitted; apps can rewire to point at,
12
+ * e.g., self-hosted Llama for the `fast` tier.
13
+ *
14
+ * `cache.auto` is the default for `ChatOptions.cache` when the call
15
+ * site doesn't pass one. Prompt caching is opt-in by default — apps
16
+ * that want every long request to cache flip this to `true`.
17
+ */
18
+
19
+ import type { ModelTier } from './types.ts'
20
+
21
+ /** Anthropic-specific driver config. */
22
+ export interface AnthropicProviderConfig {
23
+ driver: 'anthropic'
24
+ /** API key. Required. Most apps source from `env('ANTHROPIC_API_KEY')`. */
25
+ apiKey: string
26
+ /** Optional override of the SDK's base URL — useful for proxies or test doubles. */
27
+ baseUrl?: string
28
+ /** Default model when neither `options.model` nor `options.tier` is passed. */
29
+ defaultModel?: string
30
+ /** Default `max_tokens` for `chat()` calls that don't specify one. */
31
+ defaultMaxTokens?: number
32
+ /** Optional beta headers added to every request from this provider. */
33
+ betas?: readonly string[]
34
+ }
35
+
36
+ export type ProviderConfig = AnthropicProviderConfig // | OpenAIProviderConfig | … (later slices)
37
+
38
+ /** Cache-shape defaults applied when `ChatOptions.cache` is omitted. */
39
+ export interface BrainCacheConfig {
40
+ /** Set `cache_control` on the last cacheable block on every request. Default `false`. */
41
+ auto?: boolean
42
+ }
43
+
44
+ export interface BrainConfigShape {
45
+ /** Name of the default provider; must exist in `providers`. */
46
+ default: string
47
+ /** Provider registry. Each entry is one configured backend. */
48
+ providers: Record<string, ProviderConfig>
49
+ /**
50
+ * Model-tier sugar. When omitted, the framework defaults apply:
51
+ * - fast: 'claude-haiku-4-5'
52
+ * - balanced: 'claude-sonnet-4-6'
53
+ * - powerful: 'claude-opus-4-7'
54
+ */
55
+ tiers?: Partial<Record<ModelTier, string>>
56
+ /** Prompt-cache defaults. */
57
+ cache?: BrainCacheConfig
58
+ }
59
+
60
+ /**
61
+ * Framework-level tier defaults. Apps that don't override
62
+ * `config.brain.tiers` get these. Lives here so `BrainManager` and
63
+ * the docs both pull from one source.
64
+ */
65
+ export const DEFAULT_TIERS: Record<ModelTier, string> = {
66
+ fast: 'claude-haiku-4-5',
67
+ balanced: 'claude-sonnet-4-6',
68
+ powerful: 'claude-opus-4-7',
69
+ }
70
+
71
+ /** The model the framework reaches for when nothing else is specified. */
72
+ export const DEFAULT_MODEL = DEFAULT_TIERS.powerful
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `BrainError` — typed wrapper for failures originating in the brain
3
+ * stack. Provider-native errors (e.g. `Anthropic.RateLimitError`) are
4
+ * preserved on `.cause` so apps can `instanceof`-check them when they
5
+ * need provider-specific recovery; the wrapping just gives the
6
+ * framework a consistent `StravError` to render through the standard
7
+ * exception handler.
8
+ *
9
+ * Subclassing surface deferred — V1 has one error type. When a real
10
+ * use case appears for distinguishing "model refused" vs "rate
11
+ * limited" at the framework level (rather than `instanceof
12
+ * Anthropic.RateLimitError` at the call site), a typed hierarchy
13
+ * lands.
14
+ */
15
+
16
+ import { StravError } from '@strav/kernel'
17
+
18
+ export class BrainError extends StravError {
19
+ constructor(
20
+ message: string,
21
+ options: { context?: Record<string, unknown>; cause?: unknown } = {},
22
+ ) {
23
+ super(
24
+ message,
25
+ { code: 'brain.error', status: 500 },
26
+ { ...options },
27
+ )
28
+ }
29
+ }
@@ -1,158 +1,139 @@
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
-
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
- }
54
21
 
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
- )
66
- }
67
- }
22
+ import { BrainError } from './brain_error.ts'
23
+ import type { ModelTier } from './types.ts'
24
+ import type {
25
+ ChatOptions,
26
+ ChatResult,
27
+ Message,
28
+ StreamEvent,
29
+ } from './types.ts'
30
+ import type { Provider } from './provider.ts'
31
+ import { DEFAULT_TIERS } from './brain_config.ts'
32
+
33
+ export interface BrainManagerOptions {
34
+ /** Name of the default provider — must exist in `providers`. */
35
+ default: string
36
+ /** Provider registry keyed by name. */
37
+ providers: Record<string, Provider>
38
+ /** Tier-to-model overrides; merged on top of the framework defaults. */
39
+ tiers?: Partial<Record<ModelTier, string>>
40
+ /** Default for `ChatOptions.cache` when the call site doesn't pass one. */
41
+ defaultCache?: boolean
42
+ }
68
43
 
69
- static get config(): BrainConfig {
70
- if (!BrainManager._config) {
71
- throw new ConfigurationError(
72
- 'BrainManager not configured. Resolve it through the container first.'
44
+ export class BrainManager {
45
+ readonly defaultProvider: string
46
+ private readonly providers: Map<string, Provider>
47
+ private readonly tiers: Record<ModelTier, string>
48
+ private readonly defaultCache: boolean
49
+
50
+ constructor(options: BrainManagerOptions) {
51
+ if (!options.providers[options.default]) {
52
+ throw new BrainError(
53
+ `BrainManager: default provider "${options.default}" is not registered.`,
54
+ { context: { default: options.default, available: Object.keys(options.providers) } },
73
55
  )
74
56
  }
75
- return BrainManager._config
76
- }
77
-
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
57
+ this.defaultProvider = options.default
58
+ this.providers = new Map(Object.entries(options.providers))
59
+ this.tiers = { ...DEFAULT_TIERS, ...(options.tiers ?? {}) }
60
+ this.defaultCache = options.defaultCache ?? false
84
61
  }
85
62
 
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)
89
- }
90
-
91
- /** Get the configured memory settings. */
92
- static get memoryConfig(): MemoryConfig {
93
- return BrainManager._memoryConfig
94
- }
95
-
96
- /** Get the registered thread store, if any. */
97
- static get threadStore(): ThreadStore | null {
98
- return BrainManager._threadStore
99
- }
100
-
101
- /** Register a thread store for persistence (e.g., DatabaseThreadStore). */
102
- static useThreadStore(store: ThreadStore): void {
103
- BrainManager._threadStore = store
104
- }
105
-
106
- /** Register a hook that runs before every completion. */
107
- static before(hook: BeforeHook): void {
108
- BrainManager._beforeHooks.push(hook)
63
+ /** Resolve a provider by name. Default when no name passed. Throws when unknown. */
64
+ provider(name?: string): Provider {
65
+ const key = name ?? this.defaultProvider
66
+ const provider = this.providers.get(key)
67
+ if (!provider) {
68
+ throw new BrainError(`BrainManager: no provider registered under "${key}".`, {
69
+ context: { requested: key, available: [...this.providers.keys()] },
70
+ })
71
+ }
72
+ return provider
109
73
  }
110
74
 
111
- /** Register a hook that runs after every completion. */
112
- static after(hook: AfterHook): void {
113
- BrainManager._afterHooks.push(hook)
75
+ /**
76
+ * One-shot chat: send the messages, await the full reply.
77
+ *
78
+ * Accepts either a bare prompt string (treated as a single
79
+ * user-role message) or a typed `Message[]` for multi-turn /
80
+ * pre-built conversations.
81
+ */
82
+ async chat(input: string | readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
83
+ const messages = normalizeInput(input)
84
+ const resolved = this.applyDefaults(options)
85
+ return this.provider(options.provider).chat(messages, resolved)
114
86
  }
115
87
 
116
88
  /**
117
- * Run a completion through the named provider, with before/after hooks.
118
- * Used internally by AgentRunner and the `brain` helper.
89
+ * Stream the reply. Yields a `text` event per delta and a single
90
+ * terminal `stop` event with usage + stop-reason. Apps that want
91
+ * just the final message use `chat()` instead — this surface is
92
+ * for UI streaming.
119
93
  */
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
94
+ stream(
95
+ input: string | readonly Message[],
96
+ options: ChatOptions = {},
97
+ ): AsyncIterable<StreamEvent> {
98
+ const messages = normalizeInput(input)
99
+ const resolved = this.applyDefaults(options)
100
+ return this.provider(options.provider).stream(messages, resolved)
128
101
  }
129
102
 
130
103
  /**
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.
104
+ * Count input tokens for the given messages + options. Returns
105
+ * `null` when the configured provider doesn't expose a token count
106
+ * helper (no `countTokens` method) apps can fall back to a local
107
+ * estimator at the call site.
135
108
  */
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
- )
109
+ async countTokens(
110
+ input: string | readonly Message[],
111
+ options: ChatOptions = {},
112
+ ): Promise<number | null> {
113
+ const provider = this.provider(options.provider)
114
+ if (!provider.countTokens) return null
115
+ const messages = normalizeInput(input)
116
+ const resolved = this.applyDefaults(options)
117
+ return provider.countTokens(messages, resolved)
118
+ }
119
+
120
+ // ─── Internal ────────────────────────────────────────────────────────────
121
+
122
+ private applyDefaults(options: ChatOptions): ChatOptions {
123
+ const resolved: ChatOptions = { ...options }
124
+ if (resolved.model === undefined && resolved.tier !== undefined) {
125
+ resolved.model = this.tiers[resolved.tier]
146
126
  }
147
- return provider.transcribe(request)
127
+ if (resolved.cache === undefined && this.defaultCache) {
128
+ resolved.cache = true
129
+ }
130
+ return resolved
148
131
  }
132
+ }
149
133
 
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 = {}
134
+ function normalizeInput(input: string | readonly Message[]): readonly Message[] {
135
+ if (typeof input === 'string') {
136
+ return [{ role: 'user', content: input }]
157
137
  }
138
+ return input
158
139
  }
@@ -1,16 +1,91 @@
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
+ return new BrainManager(options)
68
+ })
11
69
  }
12
70
 
13
71
  override boot(app: Application): void {
72
+ // Force-resolve so config errors surface at boot, not on first call.
14
73
  app.resolve(BrainManager)
15
74
  }
16
75
  }
76
+
77
+ function buildProvider(name: string, config: ProviderConfig): Provider {
78
+ switch (config.driver) {
79
+ case 'anthropic':
80
+ if (!config.apiKey) {
81
+ throw new ConfigError(
82
+ `BrainProvider: anthropic provider "${name}" is missing apiKey. Source from env('ANTHROPIC_API_KEY').`,
83
+ )
84
+ }
85
+ return new AnthropicProvider(name, config)
86
+ default:
87
+ throw new ConfigError(
88
+ `BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic.`,
89
+ )
90
+ }
91
+ }
package/src/index.ts CHANGED
@@ -1,48 +1,32 @@
1
- export { default, default as BrainManager } from './brain_manager.ts'
1
+ // Public API of @strav/brain.
2
+ //
3
+ // Foundation slice: Provider interface + AnthropicProvider, BrainManager,
4
+ // Thread, BrainProvider service-wiring, prompt caching. Tools / agents /
5
+ // MCP / embeddings / other providers (OpenAI/Google/DeepSeek) land in
6
+ // follow-up slices.
2
7
 
3
- // Provider
4
- export { default as BrainProvider } from './brain_provider.ts'
5
- export { brain, AgentRunner, Thread } from './helpers.ts'
6
- 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'
8
+ export {
9
+ type AnthropicProviderConfig,
10
+ type BrainCacheConfig,
11
+ type BrainConfigShape,
12
+ DEFAULT_MODEL,
13
+ DEFAULT_TIERS,
14
+ type ProviderConfig,
15
+ } from './brain_config.ts'
16
+ export { BrainError } from './brain_error.ts'
17
+ export { BrainManager, type BrainManagerOptions } from './brain_manager.ts'
18
+ export { BrainProvider } from './brain_provider.ts'
11
19
  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'
20
+ export type { Provider } from './provider.ts'
21
+ export { Thread, type ThreadOptions, type ThreadState } from './thread.ts'
17
22
  export type {
18
- AIProvider,
19
- BrainConfig,
20
- ProviderConfig,
21
- CompletionRequest,
22
- CompletionResponse,
23
- Message,
23
+ ChatOptions,
24
+ ChatResult,
25
+ ChatUsage,
24
26
  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,
27
+ Message,
28
+ ModelTier,
29
+ StreamEvent,
30
+ SystemPrompt,
31
+ TextBlock,
43
32
  } 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,48 @@
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 {
16
+ ChatOptions,
17
+ ChatResult,
18
+ Message,
19
+ StreamEvent,
20
+ } from './types.ts'
21
+
22
+ export interface Provider {
23
+ /** Identifier — matches the `config.brain.providers` key. */
24
+ readonly name: string
25
+
26
+ /**
27
+ * Generate a single reply. Awaits the full response; for
28
+ * token-by-token rendering use `stream()`.
29
+ */
30
+ chat(messages: readonly Message[], options?: ChatOptions): Promise<ChatResult>
31
+
32
+ /**
33
+ * Stream the reply as it's generated. The async iterable yields
34
+ * `text` events for each delta and a final `stop` event with usage
35
+ * + stop-reason. Apps that want the full collected message at the
36
+ * end pass the same `messages` to `chat()` instead; this surface is
37
+ * for UI streaming, not for "make one call and get the message".
38
+ */
39
+ stream(messages: readonly Message[], options?: ChatOptions): AsyncIterable<StreamEvent>
40
+
41
+ /**
42
+ * Count input tokens for a given message set + options. Used by
43
+ * apps that need to budget context before sending. Optional — not
44
+ * every provider exposes a cheap token-count endpoint, so the
45
+ * implementation may approximate.
46
+ */
47
+ countTokens?(messages: readonly Message[], options?: ChatOptions): Promise<number>
48
+ }