@strav/brain 1.0.0-alpha.22 → 1.0.0-alpha.24

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 (45) hide show
  1. package/package.json +3 -3
  2. package/src/agent_runner.ts +1 -1
  3. package/src/{provider.ts → brain_driver.ts} +11 -10
  4. package/src/brain_error.ts +86 -10
  5. package/src/brain_manager.ts +30 -7
  6. package/src/brain_provider.ts +16 -16
  7. package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
  8. package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
  9. package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
  10. package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
  11. package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
  12. package/src/drivers/anthropic/index.ts +1 -0
  13. package/src/{providers/deepseek_provider.ts → drivers/deepseek/deepseek_brain_driver.ts} +10 -10
  14. package/src/drivers/deepseek/index.ts +1 -0
  15. package/src/{providers/gemini_provider.ts → drivers/gemini/gemini_brain_driver.ts} +21 -21
  16. package/src/drivers/gemini/index.ts +1 -0
  17. package/src/drivers/ollama/index.ts +1 -0
  18. package/src/{providers/ollama_provider.ts → drivers/ollama/ollama_brain_driver.ts} +5 -5
  19. package/src/drivers/openai/index.ts +1 -0
  20. package/src/{providers/openai_provider.ts → drivers/openai/openai_brain_driver.ts} +152 -591
  21. package/src/drivers/openai/openai_helpers.ts +58 -0
  22. package/src/drivers/openai/openai_message_builder.ts +187 -0
  23. package/src/drivers/openai/openai_response_mapper.ts +70 -0
  24. package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
  25. package/src/drivers/openai/openai_tool_loop.ts +191 -0
  26. package/src/drivers/openai_compat/index.ts +1 -0
  27. package/src/{providers/openai_compat_provider.ts → drivers/openai_compat/openai_compat_brain_driver.ts} +16 -16
  28. package/src/drivers/openai_responses/index.ts +1 -0
  29. package/src/{providers/openai_responses_provider.ts → drivers/openai_responses/openai_responses_brain_driver.ts} +24 -24
  30. package/src/index.ts +18 -12
  31. package/src/mcp/pool.ts +1 -1
  32. package/src/persistence/brain_message.ts +1 -1
  33. package/src/persistence/brain_message_repository.ts +3 -11
  34. package/src/persistence/brain_suspended_run.ts +1 -1
  35. package/src/persistence/brain_suspended_run_repository.ts +2 -11
  36. package/src/persistence/brain_thread.ts +1 -1
  37. package/src/persistence/brain_thread_repository.ts +2 -11
  38. package/src/persistence/index.ts +1 -1
  39. package/src/tool_runner.ts +1 -1
  40. package/src/types.ts +2 -2
  41. package/src/providers/anthropic_provider.ts +0 -1194
  42. /package/src/persistence/{schema → schemas}/brain_message_schema.ts +0 -0
  43. /package/src/persistence/{schema → schemas}/brain_suspended_run_schema.ts +0 -0
  44. /package/src/persistence/{schema → schemas}/brain_thread_schema.ts +0 -0
  45. /package/src/persistence/{schema → schemas}/index.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strav/brain",
3
- "version": "1.0.0-alpha.22",
3
+ "version": "1.0.0-alpha.24",
4
4
  "description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching, tools / agents / MCP. Anthropic + OpenAI providers; Gemini / DeepSeek follow.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -25,8 +25,8 @@
25
25
  "@anthropic-ai/sdk": "^0.100.0",
26
26
  "@google/genai": "^2.7.0",
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@strav/database": "1.0.0-alpha.22",
29
- "@strav/kernel": "1.0.0-alpha.22",
28
+ "@strav/database": "1.0.0-alpha.24",
29
+ "@strav/kernel": "1.0.0-alpha.24",
30
30
  "openai": "^6.0.0"
31
31
  },
32
32
  "peerDependencies": {
@@ -31,7 +31,7 @@ import type {
31
31
  Message,
32
32
  ToolUseBlock,
33
33
  } from './types.ts'
34
- import type { RunWithToolsOptions } from './provider.ts'
34
+ import type { RunWithToolsOptions } from './brain_driver.ts'
35
35
  import type {
36
36
  SuspendedRun,
37
37
  SuspendedState,
@@ -1,15 +1,16 @@
1
1
  /**
2
- * `Provider` — the contract every brain backend implements.
2
+ * `BrainDriver` — the contract every brain backend implements.
3
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`.
4
+ * Each concrete driver (`AnthropicBrainDriver`, `OpenAIBrainDriver`,
5
+ * `GeminiBrainDriver`, `DeepSeekBrainDriver`, …) wraps the vendor's
6
+ * SDK and translates the framework shapes (`Message`, `ChatOptions`)
7
+ * into the vendor's native request, then translates the response back
8
+ * into `ChatResult` / `StreamEvent`.
8
9
  *
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.
10
+ * Drivers are values, not classes the app instantiates by name — apps
11
+ * use them via the `BrainManager` facade. The interface is exported so
12
+ * apps that need to plug in a custom backend (e.g. a local Ollama
13
+ * variant, or a private gateway) can do so without subclassing.
13
14
  */
14
15
 
15
16
  import type { AgentGenerateResult } from './agent_generate_result.ts'
@@ -106,7 +107,7 @@ export type RunWithToolsOptionsWithSuspend = RunWithToolsOptions & {
106
107
  shouldSuspend: NonNullable<RunWithToolsOptions['shouldSuspend']>
107
108
  }
108
109
 
109
- export interface Provider {
110
+ export interface BrainDriver {
110
111
  /** Identifier — matches the `config.brain.providers` key. */
111
112
  readonly name: string
112
113
 
@@ -1,16 +1,32 @@
1
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
2
+ * `BrainError` hierarchy — typed wrappers for failures originating
3
+ * in the brain stack.
4
+ *
5
+ * Provider-native errors (e.g. `Anthropic.RateLimitError`) are
6
+ * preserved on `.cause` so apps can `instanceof`-check them when
7
+ * they need vendor-specific recovery; the framework wrapping gives
8
+ * a consistent `StravError` to render through the standard
7
9
  * exception handler.
8
10
  *
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.
11
+ * Subclasses ship in v1 for the boot / lookup / usage paths.
12
+ * Vendor-side runtime errors use `BrainProviderError` as the
13
+ * generic wrapper. Granular vendor classes (rate-limit, content
14
+ * filter, etc.) land when apps actually need to branch on them at
15
+ * the framework level — until then, `instanceof Anthropic.RateLimitError`
16
+ * on `.cause` is the call-site pattern.
17
+ *
18
+ * - `BrainConfigError` — boot-time misconfiguration (missing
19
+ * provider in `config.brain.providers`, default key absent).
20
+ *
21
+ * - `UnknownProviderError` — `brain.provider(name)` for a name
22
+ * that wasn't registered.
23
+ *
24
+ * - `BrainUsageError` — pre-condition violations from the
25
+ * framework's own API contract (e.g. `AgentRunner.run` called
26
+ * before `input()`).
27
+ *
28
+ * - `BrainProviderError` — wraps a vendor exception. `cause` is
29
+ * preserved; default status 502.
14
30
  */
15
31
 
16
32
  import { StravError } from '@strav/kernel'
@@ -27,3 +43,63 @@ export class BrainError extends StravError {
27
43
  )
28
44
  }
29
45
  }
46
+
47
+ export class BrainConfigError extends BrainError {
48
+ constructor(
49
+ message: string,
50
+ options: { context?: Record<string, unknown> } = {},
51
+ ) {
52
+ super(message, options)
53
+ // Reassign code/status via the underlying StravError props (the
54
+ // base constructor froze them with `brain.error`); we read them
55
+ // back through getters so subclass-specific overrides surface in
56
+ // logs.
57
+ Object.defineProperty(this, 'code', { value: 'brain.config' })
58
+ Object.defineProperty(this, 'status', { value: 500 })
59
+ }
60
+ }
61
+
62
+ export class UnknownProviderError extends BrainError {
63
+ constructor(name: string, available: readonly string[]) {
64
+ super(
65
+ `Brain provider "${name}" is not registered. Available: ${available.join(', ') || '<none>'}.`,
66
+ { context: { requested: name, available } },
67
+ )
68
+ Object.defineProperty(this, 'code', { value: 'brain.unknown_provider' })
69
+ Object.defineProperty(this, 'status', { value: 400 })
70
+ }
71
+ }
72
+
73
+ export class BrainUsageError extends BrainError {
74
+ constructor(
75
+ message: string,
76
+ options: { context?: Record<string, unknown> } = {},
77
+ ) {
78
+ super(message, options)
79
+ Object.defineProperty(this, 'code', { value: 'brain.usage' })
80
+ Object.defineProperty(this, 'status', { value: 500 })
81
+ }
82
+ }
83
+
84
+ export class BrainProviderError extends BrainError {
85
+ constructor(
86
+ message: string,
87
+ options: {
88
+ provider: string
89
+ operation: string
90
+ context?: Record<string, unknown>
91
+ cause?: unknown
92
+ },
93
+ ) {
94
+ super(message, {
95
+ context: {
96
+ provider: options.provider,
97
+ operation: options.operation,
98
+ ...(options.context ?? {}),
99
+ },
100
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
101
+ })
102
+ Object.defineProperty(this, 'code', { value: 'brain.provider_error' })
103
+ Object.defineProperty(this, 'status', { value: 502 })
104
+ }
105
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `BrainManager` — the per-app facade apps inject and call.
3
3
  *
4
- * Holds the configured `Provider` registry + the default-provider key
4
+ * Holds the configured `BrainDriver` registry + the default-provider key
5
5
  * + the tier-to-model map. Apps call `chat / stream / countTokens`
6
6
  * with framework-native types; the manager resolves which provider
7
7
  * runs the call (default unless `options.provider` overrides),
@@ -41,10 +41,10 @@ import type {
41
41
  TranscribeResult,
42
42
  } from './types.ts'
43
43
  import type {
44
- Provider,
44
+ BrainDriver,
45
45
  RunWithToolsOptions,
46
46
  RunWithToolsOptionsWithSuspend,
47
- } from './provider.ts'
47
+ } from './brain_driver.ts'
48
48
  import { appendResumeResults, type SuspendedRun, type SuspendedState, type ToolResultInput } from './suspended_run.ts'
49
49
  import type { Tool } from './tool.ts'
50
50
  import { DEFAULT_TIERS } from './brain_config.ts'
@@ -56,7 +56,7 @@ export interface BrainManagerOptions {
56
56
  /** Name of the default provider — must exist in `providers`. */
57
57
  default: string
58
58
  /** Provider registry keyed by name. */
59
- providers: Record<string, Provider>
59
+ providers: Record<string, BrainDriver>
60
60
  /** Tier-to-model overrides; merged on top of the framework defaults. */
61
61
  tiers?: Partial<Record<ModelTier, string>>
62
62
  /** Default for `ChatOptions.cache` when the call site doesn't pass one. */
@@ -72,7 +72,7 @@ export interface BrainManagerOptions {
72
72
 
73
73
  export class BrainManager {
74
74
  readonly defaultProvider: string
75
- private readonly providers: Map<string, Provider>
75
+ private readonly providers: Map<string, BrainDriver>
76
76
  private readonly tiers: Record<ModelTier, string>
77
77
  private readonly defaultCache: boolean
78
78
  private readonly defaultMcpServers: readonly MCPServer[]
@@ -92,7 +92,7 @@ export class BrainManager {
92
92
  }
93
93
 
94
94
  /** Resolve a provider by name. Default when no name passed. Throws when unknown. */
95
- provider(name?: string): Provider {
95
+ provider(name?: string): BrainDriver {
96
96
  const key = name ?? this.defaultProvider
97
97
  const provider = this.providers.get(key)
98
98
  if (!provider) {
@@ -103,6 +103,29 @@ export class BrainManager {
103
103
  return provider
104
104
  }
105
105
 
106
+ /**
107
+ * Register an additional provider after construction. Apps that
108
+ * wire a custom adapter (a fine-tuned model server, a fork of
109
+ * Anthropic with extra knobs, an internal LLM) use this to add it
110
+ * to the registry without going through `config.brain.providers`.
111
+ *
112
+ * Overwrites any existing provider under the same name.
113
+ *
114
+ * ```ts
115
+ * brain.extend('internal', new InternalLlmProvider({ baseUrl }))
116
+ * const reply = await brain.chat(messages, { provider: 'internal' })
117
+ * ```
118
+ *
119
+ * Mirrors `RagManager.extend(name, factory)` / `PaymentManager.extend(name, factory)`
120
+ * — the OCP escape hatch every multi-driver Strav manager exposes.
121
+ */
122
+ extend(name: string, provider: BrainDriver): void {
123
+ if (!name) {
124
+ throw new BrainError('BrainManager.extend: provider name must be a non-empty string.')
125
+ }
126
+ this.providers.set(name, provider)
127
+ }
128
+
106
129
  /**
107
130
  * One-shot chat: send the messages, await the full reply.
108
131
  *
@@ -155,7 +178,7 @@ export class BrainManager {
155
178
  *
156
179
  * Throws `BrainError` when the configured provider doesn't
157
180
  * implement `runWithTools` (V1: OpenAI / Gemini / DeepSeek providers
158
- * don't yet — only `AnthropicProvider`).
181
+ * don't yet — only `AnthropicBrainDriver`).
159
182
  */
160
183
  runTools(
161
184
  input: string | readonly Message[],
@@ -27,13 +27,13 @@
27
27
  import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
28
28
  import { BrainManager } from './brain_manager.ts'
29
29
  import type { BrainConfigShape, ProviderConfig } from './brain_config.ts'
30
- import { AnthropicProvider } from './providers/anthropic_provider.ts'
31
- import { DeepSeekProvider } from './providers/deepseek_provider.ts'
32
- import { GeminiProvider } from './providers/gemini_provider.ts'
33
- import { OllamaProvider } from './providers/ollama_provider.ts'
34
- import { OpenAIProvider } from './providers/openai_provider.ts'
35
- import { OpenAIResponsesProvider } from './providers/openai_responses_provider.ts'
36
- import type { Provider } from './provider.ts'
30
+ import { AnthropicBrainDriver } from './drivers/anthropic/anthropic_brain_driver.ts'
31
+ import { DeepSeekBrainDriver } from './drivers/deepseek/deepseek_brain_driver.ts'
32
+ import { GeminiBrainDriver } from './drivers/gemini/gemini_brain_driver.ts'
33
+ import { OllamaBrainDriver } from './drivers/ollama/ollama_brain_driver.ts'
34
+ import { OpenAIBrainDriver } from './drivers/openai/openai_brain_driver.ts'
35
+ import { OpenAIResponsesBrainDriver } from './drivers/openai_responses/openai_responses_brain_driver.ts'
36
+ import type { BrainDriver } from './brain_driver.ts'
37
37
 
38
38
  export class BrainProvider extends ServiceProvider {
39
39
  override readonly name = 'brain'
@@ -58,9 +58,9 @@ export class BrainProvider extends ServiceProvider {
58
58
  )
59
59
  }
60
60
 
61
- const providers: Record<string, Provider> = {}
61
+ const providers: Record<string, BrainDriver> = {}
62
62
  for (const [name, entry] of Object.entries(config.providers)) {
63
- providers[name] = buildProvider(name, entry)
63
+ providers[name] = buildBrainDriver(name, entry)
64
64
  }
65
65
 
66
66
  const options: ConstructorParameters<typeof BrainManager>[0] = {
@@ -89,7 +89,7 @@ export class BrainProvider extends ServiceProvider {
89
89
  }
90
90
  }
91
91
 
92
- function buildProvider(name: string, config: ProviderConfig): Provider {
92
+ function buildBrainDriver(name: string, config: ProviderConfig): BrainDriver {
93
93
  switch (config.driver) {
94
94
  case 'anthropic':
95
95
  if (!config.apiKey) {
@@ -97,42 +97,42 @@ function buildProvider(name: string, config: ProviderConfig): Provider {
97
97
  `BrainProvider: anthropic provider "${name}" is missing apiKey. Source from env('ANTHROPIC_API_KEY').`,
98
98
  )
99
99
  }
100
- return new AnthropicProvider(name, config)
100
+ return new AnthropicBrainDriver(name, config)
101
101
  case 'openai':
102
102
  if (!config.apiKey) {
103
103
  throw new ConfigError(
104
104
  `BrainProvider: openai provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
105
105
  )
106
106
  }
107
- return new OpenAIProvider(name, config)
107
+ return new OpenAIBrainDriver(name, config)
108
108
  case 'openai-responses':
109
109
  if (!config.apiKey) {
110
110
  throw new ConfigError(
111
111
  `BrainProvider: openai-responses provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
112
112
  )
113
113
  }
114
- return new OpenAIResponsesProvider(name, config)
114
+ return new OpenAIResponsesBrainDriver(name, config)
115
115
  case 'google':
116
116
  if (!config.apiKey) {
117
117
  throw new ConfigError(
118
118
  `BrainProvider: google provider "${name}" is missing apiKey. Source from env('GOOGLE_API_KEY').`,
119
119
  )
120
120
  }
121
- return new GeminiProvider(name, config)
121
+ return new GeminiBrainDriver(name, config)
122
122
  case 'deepseek':
123
123
  if (!config.apiKey) {
124
124
  throw new ConfigError(
125
125
  `BrainProvider: deepseek provider "${name}" is missing apiKey. Source from env('DEEPSEEK_API_KEY').`,
126
126
  )
127
127
  }
128
- return new DeepSeekProvider(name, config)
128
+ return new DeepSeekBrainDriver(name, config)
129
129
  case 'ollama':
130
130
  if (!config.defaultModel) {
131
131
  throw new ConfigError(
132
132
  `BrainProvider: ollama provider "${name}" is missing defaultModel. Ollama models are user-installed — pick one you've pulled (e.g. 'llama3.2').`,
133
133
  )
134
134
  }
135
- return new OllamaProvider(name, config)
135
+ return new OllamaBrainDriver(name, config)
136
136
  default: {
137
137
  const exhaustiveCheck: never = config
138
138
  throw new ConfigError(