@strav/brain 0.4.31 → 1.0.0-alpha.11
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 +18 -20
- package/src/agent.ts +50 -75
- package/src/agent_result.ts +32 -0
- package/src/agent_runner.ts +63 -0
- package/src/brain_config.ts +95 -0
- package/src/brain_error.ts +29 -0
- package/src/brain_manager.ts +186 -123
- package/src/brain_provider.ts +104 -6
- package/src/define_tool.ts +42 -0
- package/src/index.ts +44 -41
- package/src/mcp_server.ts +47 -0
- package/src/provider.ts +83 -0
- package/src/providers/anthropic_provider.ts +435 -232
- package/src/providers/openai_provider.ts +350 -503
- package/src/thread.ts +99 -0
- package/src/tool.ts +28 -44
- package/src/tool_execution_error.ts +26 -0
- package/src/types.ts +164 -237
- package/CHANGELOG.md +0 -44
- package/README.md +0 -121
- 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_responses_provider.ts +0 -321
- 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/src/brain_manager.ts
CHANGED
|
@@ -1,158 +1,221 @@
|
|
|
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
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
import type { Agent } from './agent.ts'
|
|
23
|
+
import type { AgentResult } from './agent_result.ts'
|
|
24
|
+
import type { MCPServer } from './mcp_server.ts'
|
|
25
|
+
import { AgentRunner } from './agent_runner.ts'
|
|
26
|
+
import { BrainError } from './brain_error.ts'
|
|
27
|
+
import type { ModelTier } from './types.ts'
|
|
28
|
+
import type {
|
|
29
|
+
ChatOptions,
|
|
30
|
+
ChatResult,
|
|
31
|
+
Message,
|
|
32
|
+
StreamEvent,
|
|
33
|
+
} from './types.ts'
|
|
34
|
+
import type { Provider, RunWithToolsOptions } from './provider.ts'
|
|
35
|
+
import type { Tool } from './tool.ts'
|
|
36
|
+
import { DEFAULT_TIERS } from './brain_config.ts'
|
|
37
|
+
|
|
38
|
+
/** Container-aware Agent constructor resolver — `BrainProvider` installs one wired to `app.resolve(...)`. */
|
|
39
|
+
export type AgentResolver = <A extends Agent>(cls: new (...args: never[]) => A) => A
|
|
40
|
+
|
|
41
|
+
export interface BrainManagerOptions {
|
|
42
|
+
/** Name of the default provider — must exist in `providers`. */
|
|
43
|
+
default: string
|
|
44
|
+
/** Provider registry keyed by name. */
|
|
45
|
+
providers: Record<string, Provider>
|
|
46
|
+
/** Tier-to-model overrides; merged on top of the framework defaults. */
|
|
47
|
+
tiers?: Partial<Record<ModelTier, string>>
|
|
48
|
+
/** Default for `ChatOptions.cache` when the call site doesn't pass one. */
|
|
49
|
+
defaultCache?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Default MCP servers used on every `runTools` call when the per-call
|
|
52
|
+
* options don't specify them. Per-call `mcpServers` replaces the
|
|
53
|
+
* default outright (no merge) — apps that want additive behavior
|
|
54
|
+
* concat at the call site.
|
|
55
|
+
*/
|
|
56
|
+
defaultMcpServers?: readonly MCPServer[]
|
|
57
|
+
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
export class BrainManager {
|
|
60
|
+
readonly defaultProvider: string
|
|
61
|
+
private readonly providers: Map<string, Provider>
|
|
62
|
+
private readonly tiers: Record<ModelTier, string>
|
|
63
|
+
private readonly defaultCache: boolean
|
|
64
|
+
private readonly defaultMcpServers: readonly MCPServer[]
|
|
65
|
+
|
|
66
|
+
constructor(options: BrainManagerOptions) {
|
|
67
|
+
if (!options.providers[options.default]) {
|
|
68
|
+
throw new BrainError(
|
|
69
|
+
`BrainManager: default provider "${options.default}" is not registered.`,
|
|
70
|
+
{ context: { default: options.default, available: Object.keys(options.providers) } },
|
|
71
|
+
)
|
|
66
72
|
}
|
|
73
|
+
this.defaultProvider = options.default
|
|
74
|
+
this.providers = new Map(Object.entries(options.providers))
|
|
75
|
+
this.tiers = { ...DEFAULT_TIERS, ...(options.tiers ?? {}) }
|
|
76
|
+
this.defaultCache = options.defaultCache ?? false
|
|
77
|
+
this.defaultMcpServers = options.defaultMcpServers ?? []
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
/** Resolve a provider by name. Default when no name passed. Throws when unknown. */
|
|
81
|
+
provider(name?: string): Provider {
|
|
82
|
+
const key = name ?? this.defaultProvider
|
|
83
|
+
const provider = this.providers.get(key)
|
|
84
|
+
if (!provider) {
|
|
85
|
+
throw new BrainError(`BrainManager: no provider registered under "${key}".`, {
|
|
86
|
+
context: { requested: key, available: [...this.providers.keys()] },
|
|
87
|
+
})
|
|
74
88
|
}
|
|
75
|
-
return
|
|
89
|
+
return provider
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
/**
|
|
93
|
+
* One-shot chat: send the messages, await the full reply.
|
|
94
|
+
*
|
|
95
|
+
* Accepts either a bare prompt string (treated as a single
|
|
96
|
+
* user-role message) or a typed `Message[]` for multi-turn /
|
|
97
|
+
* pre-built conversations.
|
|
98
|
+
*/
|
|
99
|
+
async chat(input: string | readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
100
|
+
const messages = normalizeInput(input)
|
|
101
|
+
const resolved = this.applyDefaults(options)
|
|
102
|
+
return this.provider(options.provider).chat(messages, resolved)
|
|
84
103
|
}
|
|
85
104
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Stream the reply. Yields a `text` event per delta and a single
|
|
107
|
+
* terminal `stop` event with usage + stop-reason. Apps that want
|
|
108
|
+
* just the final message use `chat()` instead — this surface is
|
|
109
|
+
* for UI streaming.
|
|
110
|
+
*/
|
|
111
|
+
stream(
|
|
112
|
+
input: string | readonly Message[],
|
|
113
|
+
options: ChatOptions = {},
|
|
114
|
+
): AsyncIterable<StreamEvent> {
|
|
115
|
+
const messages = normalizeInput(input)
|
|
116
|
+
const resolved = this.applyDefaults(options)
|
|
117
|
+
return this.provider(options.provider).stream(messages, resolved)
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Count input tokens for the given messages + options. Returns
|
|
122
|
+
* `null` when the configured provider doesn't expose a token count
|
|
123
|
+
* helper (no `countTokens` method) — apps can fall back to a local
|
|
124
|
+
* estimator at the call site.
|
|
125
|
+
*/
|
|
126
|
+
async countTokens(
|
|
127
|
+
input: string | readonly Message[],
|
|
128
|
+
options: ChatOptions = {},
|
|
129
|
+
): Promise<number | null> {
|
|
130
|
+
const provider = this.provider(options.provider)
|
|
131
|
+
if (!provider.countTokens) return null
|
|
132
|
+
const messages = normalizeInput(input)
|
|
133
|
+
const resolved = this.applyDefaults(options)
|
|
134
|
+
return provider.countTokens(messages, resolved)
|
|
94
135
|
}
|
|
95
136
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Run an agentic loop: send `messages` + `tools` to the model;
|
|
139
|
+
* execute any tool the model calls; loop until the model returns
|
|
140
|
+
* a terminal `stop_reason` (`'end_turn'`) or `maxIterations` is hit.
|
|
141
|
+
*
|
|
142
|
+
* Throws `BrainError` when the configured provider doesn't
|
|
143
|
+
* implement `runWithTools` (V1: OpenAI / Gemini / DeepSeek providers
|
|
144
|
+
* don't yet — only `AnthropicProvider`).
|
|
145
|
+
*/
|
|
146
|
+
async runTools(
|
|
147
|
+
input: string | readonly Message[],
|
|
148
|
+
tools: readonly Tool[],
|
|
149
|
+
options: RunWithToolsOptions = {},
|
|
150
|
+
): Promise<AgentResult> {
|
|
151
|
+
const provider = this.provider(options.provider)
|
|
152
|
+
if (!provider.runWithTools) {
|
|
153
|
+
throw new BrainError(
|
|
154
|
+
`BrainManager.runTools: provider "${provider.name}" does not implement runWithTools. Use a provider that supports tool use (V1: Anthropic).`,
|
|
155
|
+
{ context: { provider: provider.name } },
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
const messages = normalizeInput(input)
|
|
159
|
+
const resolved = this.applyDefaults(options) as RunWithToolsOptions
|
|
160
|
+
// MCP defaults — per-call override (when present) replaces the
|
|
161
|
+
// configured list outright; apps that want concat behavior
|
|
162
|
+
// construct the merged array themselves and pass it in.
|
|
163
|
+
if (resolved.mcpServers === undefined && this.defaultMcpServers.length > 0) {
|
|
164
|
+
resolved.mcpServers = this.defaultMcpServers
|
|
165
|
+
}
|
|
166
|
+
return provider.runWithTools(messages, tools, resolved)
|
|
99
167
|
}
|
|
100
168
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Resolve an `Agent` subclass from the container and return an
|
|
171
|
+
* `AgentRunner` ready to receive `input(...)` and `run()`. Apps
|
|
172
|
+
* `@inject()`-decorate their Agent subclass so constructor
|
|
173
|
+
* injection of dependencies (Repositories, services, etc.) flows
|
|
174
|
+
* through normally.
|
|
175
|
+
*/
|
|
176
|
+
agent<A extends Agent>(AgentClass: new (...args: never[]) => A, instance?: A): AgentRunner {
|
|
177
|
+
const agent = instance ?? this.resolveAgent(AgentClass)
|
|
178
|
+
return new AgentRunner(this, agent)
|
|
104
179
|
}
|
|
105
180
|
|
|
106
|
-
|
|
107
|
-
static before(hook: BeforeHook): void {
|
|
108
|
-
BrainManager._beforeHooks.push(hook)
|
|
109
|
-
}
|
|
181
|
+
// ─── Internal ────────────────────────────────────────────────────────────
|
|
110
182
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
183
|
+
private resolveAgent<A extends Agent>(AgentClass: new (...args: never[]) => A): A {
|
|
184
|
+
if (this.agentResolver) return this.agentResolver(AgentClass)
|
|
185
|
+
// Fallback: assume the Agent class is constructible without args.
|
|
186
|
+
// Apps that need DI on the agent register a resolver via
|
|
187
|
+
// `setAgentResolver` (BrainProvider wires this to the container).
|
|
188
|
+
return new (AgentClass as unknown as new () => A)()
|
|
114
189
|
}
|
|
115
190
|
|
|
116
191
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
192
|
+
* Internal — `BrainProvider` calls this at boot to plug in the
|
|
193
|
+
* container's resolution function so `brain.agent(MyAgent)` runs
|
|
194
|
+
* `app.resolve(MyAgent)` under the hood. Apps that build a
|
|
195
|
+
* `BrainManager` by hand for tests can leave this unset and pass
|
|
196
|
+
* a pre-constructed agent to `brain.agent(_, instance)`.
|
|
119
197
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
198
|
+
setAgentResolver(resolver: AgentResolver): void {
|
|
199
|
+
this.agentResolver = resolver
|
|
128
200
|
}
|
|
129
201
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
)
|
|
202
|
+
private agentResolver: AgentResolver | undefined
|
|
203
|
+
|
|
204
|
+
private applyDefaults(options: ChatOptions): ChatOptions {
|
|
205
|
+
const resolved: ChatOptions = { ...options }
|
|
206
|
+
if (resolved.model === undefined && resolved.tier !== undefined) {
|
|
207
|
+
resolved.model = this.tiers[resolved.tier]
|
|
146
208
|
}
|
|
147
|
-
|
|
209
|
+
if (resolved.cache === undefined && this.defaultCache) {
|
|
210
|
+
resolved.cache = true
|
|
211
|
+
}
|
|
212
|
+
return resolved
|
|
148
213
|
}
|
|
214
|
+
}
|
|
149
215
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
BrainManager._beforeHooks = []
|
|
154
|
-
BrainManager._afterHooks = []
|
|
155
|
-
BrainManager._threadStore = null
|
|
156
|
-
BrainManager._memoryConfig = {}
|
|
216
|
+
function normalizeInput(input: string | readonly Message[]): readonly Message[] {
|
|
217
|
+
if (typeof input === 'string') {
|
|
218
|
+
return [{ role: 'user', content: input }]
|
|
157
219
|
}
|
|
220
|
+
return input
|
|
158
221
|
}
|
package/src/brain_provider.ts
CHANGED
|
@@ -1,16 +1,114 @@
|
|
|
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 { OpenAIProvider } from './providers/openai_provider.ts'
|
|
32
|
+
import type { Provider } from './provider.ts'
|
|
33
|
+
|
|
34
|
+
export class BrainProvider extends ServiceProvider {
|
|
35
|
+
override readonly name = 'brain'
|
|
7
36
|
override readonly dependencies = ['config']
|
|
8
37
|
|
|
9
38
|
override register(app: Application): void {
|
|
10
|
-
app.singleton(BrainManager)
|
|
39
|
+
app.singleton(BrainManager, (c) => {
|
|
40
|
+
const config = c.resolve(ConfigRepository).get('brain') as BrainConfigShape | undefined
|
|
41
|
+
if (!config) {
|
|
42
|
+
throw new ConfigError(
|
|
43
|
+
'BrainProvider: `config.brain` is missing. Add a `config/brain.ts` with at least `default` + `providers`.',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
47
|
+
throw new ConfigError(
|
|
48
|
+
'BrainProvider: `config.brain.providers` must have at least one entry.',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
if (!config.providers[config.default]) {
|
|
52
|
+
throw new ConfigError(
|
|
53
|
+
`BrainProvider: default provider "${config.default}" is not declared in config.brain.providers.`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const providers: Record<string, Provider> = {}
|
|
58
|
+
for (const [name, entry] of Object.entries(config.providers)) {
|
|
59
|
+
providers[name] = buildProvider(name, entry)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const options: ConstructorParameters<typeof BrainManager>[0] = {
|
|
63
|
+
default: config.default,
|
|
64
|
+
providers,
|
|
65
|
+
}
|
|
66
|
+
if (config.tiers !== undefined) options.tiers = config.tiers
|
|
67
|
+
if (config.cache?.auto !== undefined) options.defaultCache = config.cache.auto
|
|
68
|
+
if (config.mcpServers !== undefined) options.defaultMcpServers = config.mcpServers
|
|
69
|
+
const manager = new BrainManager(options)
|
|
70
|
+
// Plug in the container so `brain.agent(MyAgent)` resolves
|
|
71
|
+
// its constructor deps through `@inject()` like every other
|
|
72
|
+
// injected class. The variance widening at the boundary
|
|
73
|
+
// (`never[]` ↔ `any[]`) is purely a TS typing artifact — the
|
|
74
|
+
// container call is identical to a direct `c.resolve(MyAgent)`.
|
|
75
|
+
manager.setAgentResolver(<A>(cls: new (...args: never[]) => A) =>
|
|
76
|
+
c.resolve(cls as unknown as new (...args: unknown[]) => A),
|
|
77
|
+
)
|
|
78
|
+
return manager
|
|
79
|
+
})
|
|
11
80
|
}
|
|
12
81
|
|
|
13
82
|
override boot(app: Application): void {
|
|
83
|
+
// Force-resolve so config errors surface at boot, not on first call.
|
|
14
84
|
app.resolve(BrainManager)
|
|
15
85
|
}
|
|
16
86
|
}
|
|
87
|
+
|
|
88
|
+
function buildProvider(name: string, config: ProviderConfig): Provider {
|
|
89
|
+
switch (config.driver) {
|
|
90
|
+
case 'anthropic':
|
|
91
|
+
if (!config.apiKey) {
|
|
92
|
+
throw new ConfigError(
|
|
93
|
+
`BrainProvider: anthropic provider "${name}" is missing apiKey. Source from env('ANTHROPIC_API_KEY').`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
return new AnthropicProvider(name, config)
|
|
97
|
+
case 'openai':
|
|
98
|
+
if (!config.apiKey) {
|
|
99
|
+
throw new ConfigError(
|
|
100
|
+
`BrainProvider: openai provider "${name}" is missing apiKey. Source from env('OPENAI_API_KEY').`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return new OpenAIProvider(name, config)
|
|
104
|
+
default: {
|
|
105
|
+
const exhaustiveCheck: never = config
|
|
106
|
+
throw new ConfigError(
|
|
107
|
+
`BrainProvider: unknown driver for provider "${name}". Known drivers: anthropic, openai.`,
|
|
108
|
+
)
|
|
109
|
+
// (unreachable — kept for the exhaustive check to fire when a new driver lands)
|
|
110
|
+
// biome-ignore lint/correctness/noUnreachable: kept for the exhaustive-check above
|
|
111
|
+
return exhaustiveCheck
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -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,51 @@
|
|
|
1
|
-
|
|
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 {
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
10
|
-
|
|
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 OpenAIProviderConfig,
|
|
20
|
+
type ProviderConfig,
|
|
21
|
+
} from './brain_config.ts'
|
|
22
|
+
export { BrainError } from './brain_error.ts'
|
|
23
|
+
export {
|
|
24
|
+
type AgentResolver,
|
|
25
|
+
BrainManager,
|
|
26
|
+
type BrainManagerOptions,
|
|
27
|
+
} from './brain_manager.ts'
|
|
28
|
+
export { BrainProvider } from './brain_provider.ts'
|
|
29
|
+
export { defineTool, type DefineToolSpec } from './define_tool.ts'
|
|
30
|
+
export type { MCPServer, MCPServerToolConfig } from './mcp_server.ts'
|
|
11
31
|
export { AnthropicProvider } from './providers/anthropic_provider.ts'
|
|
12
|
-
export { GoogleProvider } from './providers/google_provider.ts'
|
|
13
32
|
export { OpenAIProvider } from './providers/openai_provider.ts'
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
33
|
+
export type { Provider, RunWithToolsOptions } from './provider.ts'
|
|
34
|
+
export { Thread, type ThreadOptions, type ThreadState } from './thread.ts'
|
|
35
|
+
export type { Tool, ToolContext } from './tool.ts'
|
|
36
|
+
export { ToolExecutionError } from './tool_execution_error.ts'
|
|
17
37
|
export type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
CompletionRequest,
|
|
22
|
-
CompletionResponse,
|
|
23
|
-
Message,
|
|
38
|
+
ChatOptions,
|
|
39
|
+
ChatResult,
|
|
40
|
+
ChatUsage,
|
|
24
41
|
ContentBlock,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
JsonSchema,
|
|
35
|
-
SSEEvent,
|
|
36
|
-
BeforeHook,
|
|
37
|
-
AfterHook,
|
|
38
|
-
SerializedThread,
|
|
39
|
-
SerializedAgentState,
|
|
40
|
-
SuspendedRun,
|
|
41
|
-
ToolCallResult,
|
|
42
|
-
OutputSchema,
|
|
42
|
+
MCPToolResultBlock,
|
|
43
|
+
MCPToolUseBlock,
|
|
44
|
+
Message,
|
|
45
|
+
ModelTier,
|
|
46
|
+
StreamEvent,
|
|
47
|
+
SystemPrompt,
|
|
48
|
+
TextBlock,
|
|
49
|
+
ToolResultBlock,
|
|
50
|
+
ToolUseBlock,
|
|
43
51
|
} 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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MCPServer` — declarative configuration for a remote MCP server
|
|
3
|
+
* that Anthropic's backend invokes on behalf of the model.
|
|
4
|
+
*
|
|
5
|
+
* V1 leverages Anthropic's server-side MCP support: apps declare
|
|
6
|
+
* server URLs (with optional bearer auth) on the request; the
|
|
7
|
+
* backend connects to them, discovers their tools, surfaces them to
|
|
8
|
+
* the model, and runs the tool calls itself. The agentic loop here
|
|
9
|
+
* doesn't intercept MCP tool calls — they appear in the response as
|
|
10
|
+
* `MCPToolUseBlock` / `MCPToolResultBlock` content blocks for
|
|
11
|
+
* observability and audit-trail rendering.
|
|
12
|
+
*
|
|
13
|
+
* For OpenAI / Gemini / DeepSeek providers (later slices), a local
|
|
14
|
+
* MCP client implementation will live alongside this to translate
|
|
15
|
+
* MCP-discovered tools into `Tool` records and let the framework run
|
|
16
|
+
* the loop. The V1 contract stays the same; the per-provider
|
|
17
|
+
* implementation differs.
|
|
18
|
+
*
|
|
19
|
+
* `allowedTools` opts into a subset of the server's exposed tools —
|
|
20
|
+
* useful for narrowing surface area when the MCP server exposes more
|
|
21
|
+
* capabilities than the agent should be able to invoke. `enabled`
|
|
22
|
+
* defaults to `true`; set to `false` to declare the server without
|
|
23
|
+
* routing model calls to it (rare, but handy for temporary
|
|
24
|
+
* disablement without re-deploying config).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface MCPServerToolConfig {
|
|
28
|
+
/** Whitelist of tool names the agent can call. Omit for "all tools the server exposes." */
|
|
29
|
+
allowedTools?: readonly string[]
|
|
30
|
+
/** Default `true`. Set `false` to declare-but-disable. */
|
|
31
|
+
enabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MCPServer {
|
|
35
|
+
/** Server identifier — used in MCPToolUseBlock.serverName + logging. */
|
|
36
|
+
name: string
|
|
37
|
+
/** HTTPS URL of the MCP server. */
|
|
38
|
+
url: string
|
|
39
|
+
/**
|
|
40
|
+
* Optional bearer token. Apps source from env vars / secrets
|
|
41
|
+
* managers — never hardcode. The framework forwards this verbatim
|
|
42
|
+
* to the provider's `authorization_token` field.
|
|
43
|
+
*/
|
|
44
|
+
authorizationToken?: string
|
|
45
|
+
/** Per-server tool config (allowlist / enable flag). */
|
|
46
|
+
tools?: MCPServerToolConfig
|
|
47
|
+
}
|