@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 +17 -20
- package/src/agent.ts +42 -76
- package/src/agent_result.ts +32 -0
- package/src/agent_runner.ts +61 -0
- package/src/brain_config.ts +72 -0
- package/src/brain_error.ts +29 -0
- package/src/brain_manager.ts +170 -123
- package/src/brain_provider.ts +90 -6
- package/src/define_tool.ts +42 -0
- package/src/index.ts +40 -42
- package/src/provider.ts +74 -0
- package/src/providers/anthropic_provider.ts +347 -231
- package/src/thread.ts +99 -0
- package/src/tool.ts +28 -44
- package/src/tool_execution_error.ts +26 -0
- package/src/types.ts +129 -241
- 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_provider.ts +0 -569
- 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/package.json
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/brain",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
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.9",
|
|
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
|
}
|
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
|
-
*
|
|
2
|
+
* `Agent` — declarative base class for AI agents.
|
|
11
3
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
30
|
+
import type { ModelTier } from './types.ts'
|
|
31
|
+
import type { Tool } from './tool.ts'
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
37
|
+
/** Tools the agent can call. Empty array → the model answers without tools. */
|
|
38
|
+
readonly tools: readonly Tool[] = []
|
|
56
39
|
|
|
57
|
-
|
|
40
|
+
/** Override the configured default provider. Default = brain's default provider. */
|
|
41
|
+
readonly provider?: string
|
|
58
42
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
43
|
+
/** Explicit model ID. Wins over `tier`. */
|
|
44
|
+
readonly model?: string
|
|
61
45
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
46
|
+
/** Tier sugar. Default `'powerful'` for agentic work. */
|
|
47
|
+
readonly tier: ModelTier = 'powerful'
|
|
64
48
|
|
|
65
49
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
92
|
-
|
|
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
|
+
}
|