@strav/brain 0.4.31 → 1.0.0-alpha.9

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.9",
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.9",
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
  }
package/src/agent.ts CHANGED
@@ -1,93 +1,59 @@
1
- import type {
2
- ToolDefinition,
3
- ToolCall,
4
- ToolCallRecord,
5
- AgentResult,
6
- OutputSchema,
7
- } from './types.ts'
8
-
9
1
  /**
10
- * Base class for AI agents.
2
+ * `Agent` — declarative base class for AI agents.
11
3
  *
12
- * Extend this class to define an agent with custom instructions,
13
- * tools, structured output, and lifecycle hooks.
4
+ * Apps subclass and set the static-ish properties: which model to
5
+ * use, what the agent's persona is, which tools it has access to,
6
+ * and an optional iteration ceiling. The `BrainManager.agent(Class)`
7
+ * call resolves an instance via the container, builds an
8
+ * `AgentRunner`, and lets the app stream input + context into it.
14
9
  *
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]
10
+ * ```ts
11
+ * @inject()
12
+ * class ResearchAgent extends Agent {
13
+ * override readonly instructions = 'You are a meticulous research assistant.'
14
+ * override readonly tools = [searchTool, summarizeTool]
15
+ * override readonly tier: ModelTier = 'powerful'
16
+ * }
21
17
  *
22
- * output = z.object({
23
- * reply: z.string(),
24
- * category: z.enum(['billing', 'shipping', 'product', 'other']),
25
- * })
18
+ * const result = await brain.agent(ResearchAgent)
19
+ * .input('What is the current state of bun.sql?')
20
+ * .context({ userId: '01ABC...' })
21
+ * .run()
22
+ * ```
26
23
  *
27
- * onToolCall(call: ToolCall) {
28
- * console.log(`Calling tool: ${call.name}`)
29
- * }
30
- * }
24
+ * V1 makes the configuration declarative-only — apps that need
25
+ * runtime knobs (per-request model overrides, dynamic tool sets)
26
+ * use `BrainManager.runTools(...)` directly. Adding per-instance
27
+ * overrides on the Agent class is a future ergonomic slice.
31
28
  */
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
29
 
42
- /** Tools available to this agent during execution. */
43
- tools?: ToolDefinition[]
30
+ import type { ModelTier } from './types.ts'
31
+ import type { Tool } from './tool.ts'
44
32
 
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
33
+ export abstract class Agent {
34
+ /** System prompt — the persona / instructions Claude sees on every turn. */
35
+ abstract readonly instructions: string
53
36
 
54
- /** Temperature for completion requests. Falls back to config default (0.7). */
55
- temperature?: number
37
+ /** Tools the agent can call. Empty array the model answers without tools. */
38
+ readonly tools: readonly Tool[] = []
56
39
 
57
- // ── Lifecycle hooks (optional) ───────────────────────────────────────────
40
+ /** Override the configured default provider. Default = brain's default provider. */
41
+ readonly provider?: string
58
42
 
59
- /** Called before the first completion request. */
60
- onStart?(input: string, context: Record<string, unknown>): void | Promise<void>
43
+ /** Explicit model ID. Wins over `tier`. */
44
+ readonly model?: string
61
45
 
62
- /** Called when the model requests a tool call, before execution. */
63
- onToolCall?(call: ToolCall): void | Promise<void>
46
+ /** Tier sugar. Default `'powerful'` for agentic work. */
47
+ readonly tier: ModelTier = 'powerful'
64
48
 
65
49
  /**
66
- * Called before a tool is executed. Return `true` to suspend the agent loop
67
- * before running this tool call; the runner will return a `SuspendedRun`
68
- * with a JSON-serializable snapshot of the loop state. Resume later via
69
- * `AgentRunner.resume(state, toolResults)` once the tool result is known.
70
- *
71
- * This is a policy-free primitive: the framework does not attach meaning
72
- * to suspension. Integrators can use it to gate mutating tools on human
73
- * approval, dispatch a tool to an external worker, rate-limit, etc.
74
- *
75
- * When suspension occurs mid-batch, the triggering call and any remaining
76
- * unprocessed calls in the same batch are captured together in
77
- * `pendingToolCalls` so the provider's tool_use/tool_result contract stays
78
- * balanced on resume.
50
+ * Safety ceiling on the agentic loop. Default `10`. Hitting it
51
+ * returns a result with `stopReason: 'max_iterations'`; the loop
52
+ * doesn't throw because partial progress (assistant messages, tool
53
+ * results) is usually still useful to surface.
79
54
  */
80
- shouldSuspend?(
81
- call: ToolCall,
82
- context: Record<string, unknown>
83
- ): boolean | Promise<boolean>
84
-
85
- /** Called after a tool finishes execution. */
86
- onToolResult?(call: ToolCallRecord): void | Promise<void>
87
-
88
- /** Called when the agent run completes successfully. */
89
- onComplete?(result: AgentResult): void | Promise<void>
55
+ readonly maxIterations: number = 10
90
56
 
91
- /** Called when the agent run encounters an error. */
92
- onError?(error: Error): void | Promise<void>
57
+ /** Hard cap on per-call response tokens. Default `4096`. */
58
+ readonly maxTokens: number = 4096
93
59
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `AgentResult` — what an agentic loop returns when it ends. Combines
3
+ * the final assistant `text`, the full message history (including
4
+ * tool calls + results so apps can render the trace), the total
5
+ * iteration count (how many tool-use round-trips the loop made),
6
+ * and aggregated token usage across every model call inside the
7
+ * loop.
8
+ *
9
+ * `stopReason` is the provider's terminal stop reason (typically
10
+ * `'end_turn'`). When the loop exits because it hit `maxIterations`,
11
+ * `stopReason` is `'max_iterations'` — distinct from the provider
12
+ * value so apps can detect "the model would have kept going."
13
+ */
14
+
15
+ import type { ChatUsage, Message } from './types.ts'
16
+
17
+ export interface AgentResult {
18
+ /** Concatenated text from the final assistant turn. */
19
+ text: string
20
+ /** Full message history of the loop, including tool_use / tool_result blocks. */
21
+ messages: Message[]
22
+ /** Number of tool-use rounds. `0` when the model answered without tools. */
23
+ iterations: number
24
+ /**
25
+ * Terminal stop reason. Either the provider's stop_reason (typically
26
+ * `'end_turn'`) or the framework-specific `'max_iterations'` when
27
+ * the loop hit its iteration ceiling.
28
+ */
29
+ stopReason: string
30
+ /** Token usage summed across every model call in the loop. */
31
+ usage: ChatUsage
32
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `AgentRunner` — fluent builder returned by `BrainManager.agent(Class)`.
3
+ *
4
+ * Carries the agent instance + an input message + an optional
5
+ * per-run context bag. `run()` translates the agent's declarative
6
+ * configuration into a `runWithTools` call and returns the
7
+ * `AgentResult`.
8
+ *
9
+ * Designed to chain: `brain.agent(R).input(text).context({...}).run()`.
10
+ * Apps that need the full Message-array surface bypass the runner
11
+ * and call `BrainManager.runTools(messages, tools, options)` directly.
12
+ */
13
+
14
+ import type { Agent } from './agent.ts'
15
+ import type { AgentResult } from './agent_result.ts'
16
+ import type { BrainManager } from './brain_manager.ts'
17
+ import type { ChatOptions, Message } from './types.ts'
18
+
19
+ export class AgentRunner {
20
+ private prompt: string | undefined
21
+ private contextBag: Record<string, unknown> = {}
22
+
23
+ constructor(
24
+ private readonly brain: BrainManager,
25
+ private readonly agent: Agent,
26
+ ) {}
27
+
28
+ /** Set the user input. Required before `run()`. */
29
+ input(text: string): this {
30
+ this.prompt = text
31
+ return this
32
+ }
33
+
34
+ /**
35
+ * Attach context that every tool's `execute(input, ctx)` will see
36
+ * on `ctx.context`. Useful for per-request data the agent's tools
37
+ * need but the model shouldn't see directly (auth identity,
38
+ * tenant id, request-id for tracing).
39
+ */
40
+ context(data: Record<string, unknown>): this {
41
+ this.contextBag = { ...this.contextBag, ...data }
42
+ return this
43
+ }
44
+
45
+ async run(): Promise<AgentResult> {
46
+ if (this.prompt === undefined) {
47
+ throw new Error('AgentRunner.run: input() must be called before run().')
48
+ }
49
+ const messages: Message[] = [{ role: 'user', content: this.prompt }]
50
+ const options: ChatOptions & { maxIterations?: number; context?: Record<string, unknown> } = {
51
+ tier: this.agent.tier,
52
+ maxTokens: this.agent.maxTokens,
53
+ system: this.agent.instructions,
54
+ maxIterations: this.agent.maxIterations,
55
+ context: this.contextBag,
56
+ }
57
+ if (this.agent.model !== undefined) options.model = this.agent.model
58
+ if (this.agent.provider !== undefined) options.provider = this.agent.provider
59
+ return this.brain.runTools(messages, this.agent.tools, options)
60
+ }
61
+ }
@@ -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
+ }