@strav/brain 1.0.0-alpha.14 → 1.0.0-alpha.16
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 +14 -5
- package/src/agent_generate_result.ts +30 -0
- package/src/agent_runner.ts +96 -13
- package/src/index.ts +2 -1
- package/src/zod/index.ts +121 -0
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/brain",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.16",
|
|
4
4
|
"description": "Strav AI module — unified Provider interface, BrainManager, threads, prompt caching, tools / agents / MCP. Anthropic + OpenAI providers; Gemini / DeepSeek follow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
-
"./mcp": "./src/mcp/index.ts"
|
|
10
|
+
"./mcp": "./src/mcp/index.ts",
|
|
11
|
+
"./zod": "./src/zod/index.ts"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src",
|
|
@@ -23,11 +24,19 @@
|
|
|
23
24
|
"@anthropic-ai/sdk": "^0.100.0",
|
|
24
25
|
"@google/genai": "^2.7.0",
|
|
25
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
26
|
-
"@strav/kernel": "1.0.0-alpha.
|
|
27
|
+
"@strav/kernel": "1.0.0-alpha.16",
|
|
27
28
|
"openai": "^6.0.0"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
|
-
"@types/bun": ">=1.3.14"
|
|
31
|
+
"@types/bun": ">=1.3.14",
|
|
32
|
+
"zod": "^4.0.0"
|
|
31
33
|
},
|
|
32
|
-
"
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"zod": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"zod": "^4.4.3"
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AgentGenerateResult<T>` — what an Agent run returns when the
|
|
3
|
+
* runner was switched into structured-output mode via
|
|
4
|
+
* `.output(schema)`.
|
|
5
|
+
*
|
|
6
|
+
* Combines the structured-output payload (`value` + raw `text`) with
|
|
7
|
+
* the agent-loop bookkeeping (`messages`, `iterations`, `stopReason`,
|
|
8
|
+
* `usage`) so apps can still render the trace + report token spend
|
|
9
|
+
* the same way they do for `AgentResult`. `iterations` is always `0`
|
|
10
|
+
* in V1 because the structured-output path doesn't engage the
|
|
11
|
+
* tool-use loop — see the docs for the (deferred) "tools + schema"
|
|
12
|
+
* combined slice.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ChatUsage, Message } from './types.ts'
|
|
16
|
+
|
|
17
|
+
export interface AgentGenerateResult<T = unknown> {
|
|
18
|
+
/** Parsed structured value matching the supplied `OutputSchema<T>`. */
|
|
19
|
+
value: T
|
|
20
|
+
/** Raw JSON text the model produced — handy for logging when `parse` rejects. */
|
|
21
|
+
text: string
|
|
22
|
+
/** Full message history of the run (single user → assistant turn in V1). */
|
|
23
|
+
messages: Message[]
|
|
24
|
+
/** Always `0` in V1 — the schema path doesn't engage the tool-use loop. */
|
|
25
|
+
iterations: number
|
|
26
|
+
/** Provider-reported terminal stop reason. */
|
|
27
|
+
stopReason: string
|
|
28
|
+
/** Token usage from the single underlying `generate` call. */
|
|
29
|
+
usage: ChatUsage
|
|
30
|
+
}
|
package/src/agent_runner.ts
CHANGED
|
@@ -2,24 +2,49 @@
|
|
|
2
2
|
* `AgentRunner` — fluent builder returned by `BrainManager.agent(Class)`.
|
|
3
3
|
*
|
|
4
4
|
* Carries the agent instance + an input message + an optional
|
|
5
|
-
* per-run context bag
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
5
|
+
* per-run context bag + an optional structured-output schema.
|
|
6
|
+
* `run()` translates the agent's declarative configuration into
|
|
7
|
+
* either a `runWithTools` call (default) or a `generate` call (when
|
|
8
|
+
* `.output(schema)` was used) and returns the matching result type.
|
|
9
|
+
*
|
|
10
|
+
* Designed to chain:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* brain.agent(R).input(text).context({...}).run()
|
|
14
|
+
* brain.agent(R).input(text).output(schema).run() // → AgentGenerateResult<T>
|
|
15
|
+
* ```
|
|
8
16
|
*
|
|
9
|
-
* Designed to chain: `brain.agent(R).input(text).context({...}).run()`.
|
|
10
17
|
* Apps that need the full Message-array surface bypass the runner
|
|
11
|
-
* and call `BrainManager.runTools(messages, tools, options)`
|
|
18
|
+
* and call `BrainManager.runTools(messages, tools, options)` or
|
|
19
|
+
* `BrainManager.generate(input, schema, options)` directly.
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
22
|
import type { Agent } from './agent.ts'
|
|
23
|
+
import type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
15
24
|
import type { AgentResult } from './agent_result.ts'
|
|
16
25
|
import type { BrainManager } from './brain_manager.ts'
|
|
26
|
+
import { BrainError } from './brain_error.ts'
|
|
27
|
+
import type { OutputSchema } from './output_schema.ts'
|
|
28
|
+
import type { ChatOptions, Message } from './types.ts'
|
|
17
29
|
import type { RunWithToolsOptions } from './provider.ts'
|
|
18
|
-
import type { Message } from './types.ts'
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Conditional return shape for `AgentRunner.run()`. With the default
|
|
33
|
+
* generic (`T = never`), `run()` returns `AgentResult` — the
|
|
34
|
+
* tool-loop shape. When the runner has been switched into
|
|
35
|
+
* structured-output mode via `.output(schema)`, `T` carries the
|
|
36
|
+
* inferred type and `run()` returns `AgentGenerateResult<T>`.
|
|
37
|
+
*
|
|
38
|
+
* The `[T] extends [never]` form is the standard "is this still the
|
|
39
|
+
* default never?" check — `T extends never` would distribute over
|
|
40
|
+
* union types and break.
|
|
41
|
+
*/
|
|
42
|
+
export type AgentRunResult<T> = [T] extends [never] ? AgentResult : AgentGenerateResult<T>
|
|
43
|
+
|
|
44
|
+
export class AgentRunner<T = never> {
|
|
21
45
|
private prompt: string | undefined
|
|
22
46
|
private contextBag: Record<string, unknown> = {}
|
|
47
|
+
private schema: OutputSchema<T> | undefined
|
|
23
48
|
|
|
24
49
|
constructor(
|
|
25
50
|
private readonly brain: BrainManager,
|
|
@@ -43,21 +68,79 @@ export class AgentRunner {
|
|
|
43
68
|
return this
|
|
44
69
|
}
|
|
45
70
|
|
|
46
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Switch the runner into structured-output mode. `run()` then
|
|
73
|
+
* delegates to `BrainManager.generate(...)` and returns an
|
|
74
|
+
* `AgentGenerateResult<U>` shaped to the supplied schema.
|
|
75
|
+
*
|
|
76
|
+
* V1 caveat: structured output and tool use can't be combined yet.
|
|
77
|
+
* Agents that declare `tools` or `mcpServers` AND call `.output()`
|
|
78
|
+
* throw a `BrainError` at `run()` with a clear "this combination is
|
|
79
|
+
* deferred" message. Apps that need both today run them in two
|
|
80
|
+
* steps — `runTools(...)` for the loop, then `generate(...)` for
|
|
81
|
+
* the structured summary.
|
|
82
|
+
*/
|
|
83
|
+
output<U>(schema: OutputSchema<U>): AgentRunner<U> {
|
|
84
|
+
// Mutate in place + cast — the runtime state is a single object;
|
|
85
|
+
// the generic narrows only the static return type. This avoids
|
|
86
|
+
// cloning the prompt + contextBag fields.
|
|
87
|
+
this.schema = schema as unknown as OutputSchema<T>
|
|
88
|
+
return this as unknown as AgentRunner<U>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async run(): Promise<AgentRunResult<T>> {
|
|
47
92
|
if (this.prompt === undefined) {
|
|
48
|
-
throw new
|
|
93
|
+
throw new BrainError('AgentRunner.run: input() must be called before run().')
|
|
49
94
|
}
|
|
50
95
|
const messages: Message[] = [{ role: 'user', content: this.prompt }]
|
|
96
|
+
|
|
97
|
+
if (this.schema !== undefined) {
|
|
98
|
+
if (this.agent.tools.length > 0 || this.agent.mcpServers.length > 0) {
|
|
99
|
+
throw new BrainError(
|
|
100
|
+
'AgentRunner.output() does not yet support tool use. The agent declares tools or mcpServers — drop them on the agent, or run runTools(...) and generate(...) as two separate calls. Combined tool + schema lands in a later slice.',
|
|
101
|
+
{
|
|
102
|
+
context: {
|
|
103
|
+
agent: this.agent.constructor.name,
|
|
104
|
+
tools: this.agent.tools.length,
|
|
105
|
+
mcpServers: this.agent.mcpServers.length,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
const generateOptions = this.buildChatOptions()
|
|
111
|
+
const result = await this.brain.generate<T>(messages, this.schema, generateOptions)
|
|
112
|
+
const generateResult: AgentGenerateResult<T> = {
|
|
113
|
+
value: result.value,
|
|
114
|
+
text: result.text,
|
|
115
|
+
messages: [
|
|
116
|
+
...messages,
|
|
117
|
+
{ role: 'assistant', content: result.text },
|
|
118
|
+
],
|
|
119
|
+
iterations: 0,
|
|
120
|
+
stopReason: result.stopReason ?? 'stop',
|
|
121
|
+
usage: result.usage,
|
|
122
|
+
}
|
|
123
|
+
return generateResult as AgentRunResult<T>
|
|
124
|
+
}
|
|
125
|
+
|
|
51
126
|
const options: RunWithToolsOptions = {
|
|
127
|
+
...this.buildChatOptions(),
|
|
128
|
+
maxIterations: this.agent.maxIterations,
|
|
129
|
+
context: this.contextBag,
|
|
130
|
+
}
|
|
131
|
+
if (this.agent.mcpServers.length > 0) options.mcpServers = this.agent.mcpServers
|
|
132
|
+
const result = await this.brain.runTools(messages, this.agent.tools, options)
|
|
133
|
+
return result as AgentRunResult<T>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private buildChatOptions(): ChatOptions {
|
|
137
|
+
const options: ChatOptions = {
|
|
52
138
|
tier: this.agent.tier,
|
|
53
139
|
maxTokens: this.agent.maxTokens,
|
|
54
140
|
system: this.agent.instructions,
|
|
55
|
-
maxIterations: this.agent.maxIterations,
|
|
56
|
-
context: this.contextBag,
|
|
57
141
|
}
|
|
58
142
|
if (this.agent.model !== undefined) options.model = this.agent.model
|
|
59
143
|
if (this.agent.provider !== undefined) options.provider = this.agent.provider
|
|
60
|
-
|
|
61
|
-
return this.brain.runTools(messages, this.agent.tools, options)
|
|
144
|
+
return options
|
|
62
145
|
}
|
|
63
146
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// tools, structured outputs, other providers.
|
|
9
9
|
|
|
10
10
|
export { Agent } from './agent.ts'
|
|
11
|
+
export type { AgentGenerateResult } from './agent_generate_result.ts'
|
|
11
12
|
export type { AgentResult } from './agent_result.ts'
|
|
12
|
-
export { AgentRunner } from './agent_runner.ts'
|
|
13
|
+
export { AgentRunner, type AgentRunResult } from './agent_runner.ts'
|
|
13
14
|
export {
|
|
14
15
|
type AnthropicProviderConfig,
|
|
15
16
|
type BrainCacheConfig,
|
package/src/zod/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@strav/brain/zod` — Zod-flavored helpers on top of the
|
|
3
|
+
* schema-library-agnostic core.
|
|
4
|
+
*
|
|
5
|
+
* The default `@strav/brain` import deliberately doesn't depend on
|
|
6
|
+
* Zod — `Tool.inputSchema` and `OutputSchema.jsonSchema` are plain
|
|
7
|
+
* JSON Schema so apps stay free to pick Ajv, Valibot, ArkType, or
|
|
8
|
+
* nothing at all. This sub-path opt-in adds two thin wrappers for
|
|
9
|
+
* apps that already use Zod:
|
|
10
|
+
*
|
|
11
|
+
* - `outputSchema(z, opts?)` turns a Zod schema into an
|
|
12
|
+
* `OutputSchema<z.infer<typeof z>>` — `jsonSchema` is derived
|
|
13
|
+
* via Zod's built-in `z.toJSONSchema`, and `parse` is wired to
|
|
14
|
+
* `z.parse`. Apps then pass the result straight to
|
|
15
|
+
* `BrainManager.generate(input, schema)`.
|
|
16
|
+
*
|
|
17
|
+
* - `tool({ name, description, input, execute })` turns a Zod
|
|
18
|
+
* schema for the tool's input into a framework `Tool` — the
|
|
19
|
+
* wrapper validates the model's raw input through the Zod
|
|
20
|
+
* schema before calling the app's `execute`. Apps get inferred
|
|
21
|
+
* types on `execute(input)` for free.
|
|
22
|
+
*
|
|
23
|
+
* `zod` is an optional peer dependency. Apps that don't use Zod
|
|
24
|
+
* don't install it, don't bundle it, and never import this
|
|
25
|
+
* sub-path — they keep using `defineTool` / hand-written
|
|
26
|
+
* `OutputSchema` literals with raw JSON Schema.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { z } from 'zod'
|
|
30
|
+
import type { OutputSchema } from '../output_schema.ts'
|
|
31
|
+
import type { Tool, ToolContext } from '../tool.ts'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for `outputSchema`. `name` defaults to `'output'` —
|
|
35
|
+
* apps that surface multiple schemas in logs or to OpenAI's wire
|
|
36
|
+
* format should pass a stable, descriptive identifier.
|
|
37
|
+
*/
|
|
38
|
+
export interface OutputSchemaOptions {
|
|
39
|
+
/** Identifier — defaults to `'output'`. */
|
|
40
|
+
name?: string
|
|
41
|
+
/** Optional model-facing hint. Defaults to the Zod schema's `.describe(…)` if set. */
|
|
42
|
+
description?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build an `OutputSchema<T>` from a Zod schema. The returned shape
|
|
47
|
+
* is ready to pass to `BrainManager.generate(...)`.
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* const CityZ = z.object({ city: z.string(), population: z.number().int() })
|
|
51
|
+
* const { value } = await brain.generate('Capital of France?', outputSchema(CityZ, { name: 'city_answer' }))
|
|
52
|
+
* // ^? { city: string; population: number }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function outputSchema<T>(
|
|
56
|
+
schema: z.ZodType<T>,
|
|
57
|
+
options: OutputSchemaOptions = {},
|
|
58
|
+
): OutputSchema<T> {
|
|
59
|
+
const description = options.description ?? zodDescription(schema)
|
|
60
|
+
const result: OutputSchema<T> = {
|
|
61
|
+
name: options.name ?? 'output',
|
|
62
|
+
jsonSchema: z.toJSONSchema(schema) as Record<string, unknown>,
|
|
63
|
+
parse: (value) => schema.parse(value),
|
|
64
|
+
}
|
|
65
|
+
if (description !== undefined) result.description = description
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Spec passed to `tool(...)`. `execute` receives the model's input
|
|
71
|
+
* already validated + typed against `input` — no need to call
|
|
72
|
+
* `input.parse` manually.
|
|
73
|
+
*/
|
|
74
|
+
export interface ZodToolSpec<TInput, TOutput> {
|
|
75
|
+
name: string
|
|
76
|
+
description: string
|
|
77
|
+
input: z.ZodType<TInput>
|
|
78
|
+
execute(input: TInput, ctx: ToolContext): Promise<TOutput>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a framework `Tool` from a Zod-typed spec. The wrapper
|
|
83
|
+
* derives `inputSchema` via `z.toJSONSchema` and validates the
|
|
84
|
+
* model's raw input through `input.parse` before delegating to
|
|
85
|
+
* `execute`. Validation failures propagate as `ZodError`; the
|
|
86
|
+
* agentic loop wraps that into a `ToolExecutionError`.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const search = tool({
|
|
90
|
+
* name: 'search_orders',
|
|
91
|
+
* description: 'Look up an order by id.',
|
|
92
|
+
* input: z.object({ orderId: z.string() }),
|
|
93
|
+
* async execute({ orderId }, ctx) {
|
|
94
|
+
* // ^? { orderId: string }
|
|
95
|
+
* return await orders.find(orderId, ctx.context)
|
|
96
|
+
* },
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function tool<TInput, TOutput>(
|
|
101
|
+
spec: ZodToolSpec<TInput, TOutput>,
|
|
102
|
+
): Tool<TInput, TOutput> {
|
|
103
|
+
const jsonSchema = z.toJSONSchema(spec.input) as Record<string, unknown>
|
|
104
|
+
return {
|
|
105
|
+
name: spec.name,
|
|
106
|
+
description: spec.description,
|
|
107
|
+
inputSchema: jsonSchema,
|
|
108
|
+
async execute(raw: TInput, ctx: ToolContext): Promise<TOutput> {
|
|
109
|
+
const parsed = spec.input.parse(raw)
|
|
110
|
+
return spec.execute(parsed, ctx)
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function zodDescription(schema: z.ZodType<unknown>): string | undefined {
|
|
116
|
+
// Zod stores `.describe(…)` on the schema's `_def`; surface it
|
|
117
|
+
// as the model-facing hint when callers don't pass one
|
|
118
|
+
// explicitly.
|
|
119
|
+
const def = (schema as unknown as { description?: string }).description
|
|
120
|
+
return typeof def === 'string' && def.length > 0 ? def : undefined
|
|
121
|
+
}
|