@strav/brain 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.6.0
4
+
5
+ ### Added
6
+
7
+ - **Memory management** — three-tier conversation memory system for long-running threads
8
+ - `thread.memory()` enables opt-in context window management
9
+ - **Working memory** — recent messages within token budget
10
+ - **Episodic memory** — LLM-generated summaries of compacted older messages
11
+ - **Semantic memory** — structured facts extracted from conversation, injected into system prompt
12
+ - `TokenCounter` — approximate token estimation per provider (~4 chars/token)
13
+ - `ContextBudget` — budget allocation across system prompt, summaries, facts, and working messages
14
+ - `MemoryManager` — orchestrates compaction and fact extraction
15
+ - `SemanticMemory` — in-memory fact store with `<known_facts>` prompt injection
16
+ - `SummarizeStrategy` — LLM-powered compaction with optional fact extraction
17
+ - `SlidingWindowStrategy` — drop oldest messages without summarization
18
+ - `InMemoryThreadStore` — default `ThreadStore` implementation for dev/testing
19
+ - `ThreadStore` interface — pluggable persistence (implement for database-backed storage)
20
+ - `BrainManager.useThreadStore()` — register a thread store for persistence
21
+ - `BrainManager.memoryConfig` / `BrainManager.threadStore` — accessors for memory configuration
22
+ - `thread.id()` — set thread identifier for persistence
23
+ - `thread.persist()` — enable auto-save to ThreadStore after each `send()`
24
+ - `thread.facts` / `thread.episodicSummary` — access memory state
25
+ - `thread.serializeMemory()` / `thread.restoreMemory()` — extended serialization with memory state
26
+ - `BrainConfig.memory` — optional `MemoryConfig` field for global memory settings
27
+
28
+ ## 0.1.1
29
+
30
+ ### Changed
31
+
32
+ - Applied consistent code formatting across all source files
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @stravigor/brain
2
+
3
+ AI module for the [Strav](https://www.npmjs.com/package/@stravigor/core) framework. Provides a unified interface for AI providers with support for agents, threads, tool use, and multi-step workflows.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @stravigor/brain
9
+ ```
10
+
11
+ Requires `@stravigor/core` as a peer dependency.
12
+
13
+ ## Providers
14
+
15
+ - **Anthropic** (Claude)
16
+ - **OpenAI** (GPT, also works with DeepSeek via custom `baseUrl`)
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { brain } from '@stravigor/brain'
22
+
23
+ // One-shot chat
24
+ const response = await brain.chat('Explain quantum computing')
25
+
26
+ // Streaming
27
+ for await (const chunk of brain.stream('Write a poem')) {
28
+ process.stdout.write(chunk.text)
29
+ }
30
+
31
+ // Structured output with Zod
32
+ import { z } from 'zod'
33
+ const result = await brain.generate('List 3 colors', {
34
+ schema: z.object({ colors: z.array(z.string()) }),
35
+ })
36
+
37
+ // Embeddings
38
+ const vectors = await brain.embed('Hello world')
39
+ ```
40
+
41
+ ## Agents
42
+
43
+ ```ts
44
+ import { Agent, defineTool } from '@stravigor/brain'
45
+
46
+ class ResearchAgent extends Agent {
47
+ provider = 'anthropic'
48
+ model = 'claude-sonnet-4-20250514'
49
+ instructions = 'You are a research assistant.'
50
+ tools = [searchTool, summarizeTool]
51
+ }
52
+
53
+ const result = await brain.agent(ResearchAgent).input('Find info on Bun').run()
54
+ ```
55
+
56
+ ## Threads
57
+
58
+ Multi-turn conversations with serialization support:
59
+
60
+ ```ts
61
+ const thread = brain.thread({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' })
62
+ await thread.send('Hello')
63
+ await thread.send('Tell me more')
64
+ const saved = thread.serialize() // persist and restore later
65
+ ```
66
+
67
+ ## Workflows
68
+
69
+ Orchestrate multi-agent pipelines:
70
+
71
+ ```ts
72
+ const workflow = brain.workflow()
73
+ .step('research', ResearchAgent)
74
+ .step('summarize', SummaryAgent)
75
+ .parallel('review', [FactCheckAgent, StyleAgent])
76
+
77
+ const result = await workflow.run('Analyze this topic')
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@strav/brain",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "AI module for the Strav framework",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./*": "./src/*.ts"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "package.json",
14
+ "tsconfig.json",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "peerDependencies": {
18
+ "@strav/kernel": "0.1.0",
19
+ "@strav/workflow": "0.1.0"
20
+ },
21
+ "dependencies": {
22
+ "zod": "^3.25 || ^4.0"
23
+ },
24
+ "scripts": {
25
+ "test": "bun test tests/",
26
+ "typecheck": "tsc --noEmit"
27
+ }
28
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type {
2
+ ToolDefinition,
3
+ ToolCall,
4
+ ToolCallRecord,
5
+ AgentResult,
6
+ OutputSchema,
7
+ } from './types.ts'
8
+
9
+ /**
10
+ * Base class for AI agents.
11
+ *
12
+ * Extend this class to define an agent with custom instructions,
13
+ * tools, structured output, and lifecycle hooks.
14
+ *
15
+ * @example
16
+ * class SupportAgent extends Agent {
17
+ * provider = 'anthropic'
18
+ * model = 'claude-sonnet-4-5-20250929'
19
+ * instructions = 'You are a customer support agent.'
20
+ * tools = [searchTool, lookupOrderTool]
21
+ *
22
+ * output = z.object({
23
+ * reply: z.string(),
24
+ * category: z.enum(['billing', 'shipping', 'product', 'other']),
25
+ * })
26
+ *
27
+ * onToolCall(call: ToolCall) {
28
+ * console.log(`Calling tool: ${call.name}`)
29
+ * }
30
+ * }
31
+ */
32
+ export abstract class Agent {
33
+ /** Provider name (e.g., 'anthropic', 'openai'). Falls back to config default. */
34
+ provider?: string
35
+
36
+ /** Model identifier. Falls back to the provider's configured default model. */
37
+ model?: string
38
+
39
+ /** System prompt / instructions for this agent. Supports `{{key}}` context interpolation. */
40
+ instructions: string = ''
41
+
42
+ /** Tools available to this agent during execution. */
43
+ tools?: ToolDefinition[]
44
+
45
+ /** Structured output schema (Zod or JSON Schema). When set, the final response is parsed and validated. */
46
+ output?: OutputSchema
47
+
48
+ /** Maximum tool-use loop iterations before forcing a stop. Falls back to config default (10). */
49
+ maxIterations?: number
50
+
51
+ /** Maximum tokens per completion request. Falls back to config default (4096). */
52
+ maxTokens?: number
53
+
54
+ /** Temperature for completion requests. Falls back to config default (0.7). */
55
+ temperature?: number
56
+
57
+ // ── Lifecycle hooks (optional) ───────────────────────────────────────────
58
+
59
+ /** Called before the first completion request. */
60
+ onStart?(input: string, context: Record<string, unknown>): void | Promise<void>
61
+
62
+ /** Called when the model requests a tool call, before execution. */
63
+ onToolCall?(call: ToolCall): void | Promise<void>
64
+
65
+ /** Called after a tool finishes execution. */
66
+ onToolResult?(call: ToolCallRecord): void | Promise<void>
67
+
68
+ /** Called when the agent run completes successfully. */
69
+ onComplete?(result: AgentResult): void | Promise<void>
70
+
71
+ /** Called when the agent run encounters an error. */
72
+ onError?(error: Error): void | Promise<void>
73
+ }
@@ -0,0 +1,136 @@
1
+ import { inject, ConfigurationError, Configuration } from '@stravigor/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
+ } from './types.ts'
13
+ import type { MemoryConfig, ThreadStore } from './memory/types.ts'
14
+
15
+ /**
16
+ * Central AI configuration hub.
17
+ *
18
+ * Resolved once via the DI container — reads the AI config
19
+ * and initializes the appropriate provider drivers.
20
+ *
21
+ * @example
22
+ * app.singleton(BrainManager)
23
+ * app.resolve(BrainManager)
24
+ *
25
+ * // Plug in a custom provider
26
+ * BrainManager.useProvider(new OllamaProvider())
27
+ */
28
+ @inject
29
+ export default class BrainManager {
30
+ private static _config: BrainConfig
31
+ private static _providers = new Map<string, AIProvider>()
32
+ private static _beforeHooks: BeforeHook[] = []
33
+ private static _afterHooks: AfterHook[] = []
34
+ private static _threadStore: ThreadStore | null = null
35
+ private static _memoryConfig: MemoryConfig = {}
36
+
37
+ constructor(config: Configuration) {
38
+ BrainManager._config = {
39
+ default: config.get('ai.default', 'anthropic') as string,
40
+ providers: config.get('ai.providers', {}) as Record<string, ProviderConfig>,
41
+ maxTokens: config.get('ai.maxTokens', 4096) as number,
42
+ temperature: config.get('ai.temperature', 0.7) as number,
43
+ maxIterations: config.get('ai.maxIterations', 10) as number,
44
+ }
45
+
46
+ BrainManager._memoryConfig = config.get('ai.memory', {}) as MemoryConfig
47
+
48
+ for (const [name, providerConfig] of Object.entries(BrainManager._config.providers)) {
49
+ BrainManager._providers.set(name, BrainManager.createProvider(name, providerConfig))
50
+ }
51
+ }
52
+
53
+ private static createProvider(name: string, config: ProviderConfig): AIProvider {
54
+ const driver = config.driver ?? name
55
+ switch (driver) {
56
+ case 'anthropic':
57
+ return new AnthropicProvider(config)
58
+ case 'openai':
59
+ return new OpenAIProvider(config, name)
60
+ default:
61
+ throw new ConfigurationError(
62
+ `Unknown AI provider driver: ${driver}. Use BrainManager.useProvider() for custom providers.`
63
+ )
64
+ }
65
+ }
66
+
67
+ static get config(): BrainConfig {
68
+ if (!BrainManager._config) {
69
+ throw new ConfigurationError(
70
+ 'BrainManager not configured. Resolve it through the container first.'
71
+ )
72
+ }
73
+ return BrainManager._config
74
+ }
75
+
76
+ /** Get a provider by name, or the default provider. */
77
+ static provider(name?: string): AIProvider {
78
+ const key = name ?? BrainManager._config.default
79
+ const p = BrainManager._providers.get(key)
80
+ if (!p) throw new ConfigurationError(`AI provider "${key}" not configured.`)
81
+ return p
82
+ }
83
+
84
+ /** Swap or add a provider at runtime (e.g., for testing or a custom provider). */
85
+ static useProvider(provider: AIProvider): void {
86
+ BrainManager._providers.set(provider.name, provider)
87
+ }
88
+
89
+ /** Get the configured memory settings. */
90
+ static get memoryConfig(): MemoryConfig {
91
+ return BrainManager._memoryConfig
92
+ }
93
+
94
+ /** Get the registered thread store, if any. */
95
+ static get threadStore(): ThreadStore | null {
96
+ return BrainManager._threadStore
97
+ }
98
+
99
+ /** Register a thread store for persistence (e.g., DatabaseThreadStore). */
100
+ static useThreadStore(store: ThreadStore): void {
101
+ BrainManager._threadStore = store
102
+ }
103
+
104
+ /** Register a hook that runs before every completion. */
105
+ static before(hook: BeforeHook): void {
106
+ BrainManager._beforeHooks.push(hook)
107
+ }
108
+
109
+ /** Register a hook that runs after every completion. */
110
+ static after(hook: AfterHook): void {
111
+ BrainManager._afterHooks.push(hook)
112
+ }
113
+
114
+ /**
115
+ * Run a completion through the named provider, with before/after hooks.
116
+ * Used internally by AgentRunner and the `brain` helper.
117
+ */
118
+ static async complete(
119
+ providerName: string | undefined,
120
+ request: CompletionRequest
121
+ ): Promise<CompletionResponse> {
122
+ for (const hook of BrainManager._beforeHooks) await hook(request)
123
+ const response = await BrainManager.provider(providerName).complete(request)
124
+ for (const hook of BrainManager._afterHooks) await hook(request, response)
125
+ return response
126
+ }
127
+
128
+ /** Clear all providers, hooks, and stores (for testing). */
129
+ static reset(): void {
130
+ BrainManager._providers.clear()
131
+ BrainManager._beforeHooks = []
132
+ BrainManager._afterHooks = []
133
+ BrainManager._threadStore = null
134
+ BrainManager._memoryConfig = {}
135
+ }
136
+ }
@@ -0,0 +1,16 @@
1
+ import { ServiceProvider } from '@stravigor/kernel'
2
+ import type { Application } from '@stravigor/kernel'
3
+ import BrainManager from './brain_manager.ts'
4
+
5
+ export default class BrainProvider extends ServiceProvider {
6
+ readonly name = 'brain'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(BrainManager)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(BrainManager)
15
+ }
16
+ }