@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 +17 -20
- package/src/brain_config.ts +72 -0
- package/src/brain_error.ts +29 -0
- package/src/brain_manager.ts +113 -132
- package/src/brain_provider.ts +81 -6
- package/src/index.ts +27 -43
- package/src/provider.ts +48 -0
- package/src/providers/anthropic_provider.ts +192 -246
- package/src/thread.ts +99 -0
- package/src/types.ts +101 -246
- package/CHANGELOG.md +0 -44
- package/README.md +0 -121
- package/src/agent.ts +0 -93
- package/src/helpers.ts +0 -1082
- package/src/mcp_toolbox.ts +0 -62
- package/src/memory/context_budget.ts +0 -120
- package/src/memory/index.ts +0 -17
- package/src/memory/memory_manager.ts +0 -168
- package/src/memory/semantic_memory.ts +0 -89
- package/src/memory/strategies/sliding_window.ts +0 -20
- package/src/memory/strategies/summarize.ts +0 -157
- package/src/memory/thread_store.ts +0 -56
- package/src/memory/token_counter.ts +0 -101
- package/src/memory/types.ts +0 -68
- package/src/providers/google_provider.ts +0 -496
- package/src/providers/openai_provider.ts +0 -569
- package/src/providers/openai_responses_provider.ts +0 -321
- package/src/tool.ts +0 -51
- package/src/utils/error_scrub.ts +0 -5
- package/src/utils/prompt.ts +0 -65
- package/src/utils/retry.ts +0 -104
- package/src/utils/schema.ts +0 -27
- package/src/utils/sse_parser.ts +0 -62
- package/src/workflow.ts +0 -199
- package/tsconfig.json +0 -5
package/package.json
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/brain",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
6
|
-
"
|
|
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
|
-
"
|
|
14
|
-
"tsconfig.json",
|
|
15
|
-
"CHANGELOG.md"
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
16
14
|
],
|
|
17
|
-
"
|
|
18
|
-
"
|
|
15
|
+
"engines": {
|
|
16
|
+
"bun": ">=1.3.14"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"@strav/
|
|
22
|
-
"@
|
|
23
|
-
"zod": "^3.25 || ^4.0"
|
|
22
|
+
"@strav/kernel": "1.0.0-alpha.8",
|
|
23
|
+
"@anthropic-ai/sdk": "^0.100.0"
|
|
24
24
|
},
|
|
25
|
-
"
|
|
26
|
-
"@
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@types/bun": ">=1.3.14"
|
|
27
27
|
},
|
|
28
|
-
"
|
|
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
|
+
}
|
package/src/brain_manager.ts
CHANGED
|
@@ -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
|
-
*
|
|
2
|
+
* `BrainManager` — the per-app facade apps inject and call.
|
|
19
3
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* app.resolve(BrainManager)
|
|
11
|
+
* Constructed by `BrainProvider` at boot from `config.brain`. Apps
|
|
12
|
+
* also build one inline for tests:
|
|
26
13
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
*
|
|
118
|
-
*
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
):
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
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
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* (
|
|
134
|
-
*
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
): Promise<
|
|
140
|
-
const provider =
|
|
141
|
-
if (!provider.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
127
|
+
if (resolved.cache === undefined && this.defaultCache) {
|
|
128
|
+
resolved.cache = true
|
|
129
|
+
}
|
|
130
|
+
return resolved
|
|
148
131
|
}
|
|
132
|
+
}
|
|
149
133
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
}
|
package/src/brain_provider.ts
CHANGED
|
@@ -1,16 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 {
|
|
13
|
-
export {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
CompletionRequest,
|
|
22
|
-
CompletionResponse,
|
|
23
|
-
Message,
|
|
23
|
+
ChatOptions,
|
|
24
|
+
ChatResult,
|
|
25
|
+
ChatUsage,
|
|
24
26
|
ContentBlock,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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'
|
package/src/provider.ts
ADDED
|
@@ -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
|
+
}
|