@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 +32 -0
- package/README.md +82 -0
- package/package.json +28 -0
- package/src/agent.ts +73 -0
- package/src/brain_manager.ts +136 -0
- package/src/brain_provider.ts +16 -0
- package/src/helpers.ts +903 -0
- package/src/index.ts +42 -0
- package/src/memory/context_budget.ts +120 -0
- package/src/memory/index.ts +17 -0
- package/src/memory/memory_manager.ts +168 -0
- package/src/memory/semantic_memory.ts +89 -0
- package/src/memory/strategies/sliding_window.ts +20 -0
- package/src/memory/strategies/summarize.ts +157 -0
- package/src/memory/thread_store.ts +56 -0
- package/src/memory/token_counter.ts +101 -0
- package/src/memory/types.ts +68 -0
- package/src/providers/anthropic_provider.ts +276 -0
- package/src/providers/openai_provider.ts +509 -0
- package/src/providers/openai_responses_provider.ts +319 -0
- package/src/tool.ts +50 -0
- package/src/types.ts +182 -0
- package/src/utils/retry.ts +100 -0
- package/src/utils/schema.ts +27 -0
- package/src/utils/sse_parser.ts +62 -0
- package/src/workflow.ts +180 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SSEEvent } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a Server-Sent Events stream into structured events.
|
|
5
|
+
*
|
|
6
|
+
* Handles:
|
|
7
|
+
* - Chunks split at arbitrary byte boundaries
|
|
8
|
+
* - Multi-line `data:` fields (concatenated with newlines)
|
|
9
|
+
* - Optional `event:` field
|
|
10
|
+
* - Empty lines / keepalive comments
|
|
11
|
+
*/
|
|
12
|
+
export async function* parseSSE(stream: ReadableStream<Uint8Array>): AsyncIterable<SSEEvent> {
|
|
13
|
+
const reader = stream.getReader()
|
|
14
|
+
const decoder = new TextDecoder()
|
|
15
|
+
let buffer = ''
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
while (true) {
|
|
19
|
+
const { done, value } = await reader.read()
|
|
20
|
+
if (done) break
|
|
21
|
+
|
|
22
|
+
buffer += decoder.decode(value, { stream: true })
|
|
23
|
+
|
|
24
|
+
const events = buffer.split('\n\n')
|
|
25
|
+
// Last element is either empty (if buffer ended with \n\n) or incomplete
|
|
26
|
+
buffer = events.pop()!
|
|
27
|
+
|
|
28
|
+
for (const block of events) {
|
|
29
|
+
const parsed = parseBlock(block)
|
|
30
|
+
if (parsed) yield parsed
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Flush any remaining data in buffer
|
|
35
|
+
if (buffer.trim()) {
|
|
36
|
+
const parsed = parseBlock(buffer)
|
|
37
|
+
if (parsed) yield parsed
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
reader.releaseLock()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseBlock(block: string): SSEEvent | null {
|
|
45
|
+
let event: string | undefined
|
|
46
|
+
const dataLines: string[] = []
|
|
47
|
+
|
|
48
|
+
for (const line of block.split('\n')) {
|
|
49
|
+
if (line.startsWith('event: ')) {
|
|
50
|
+
event = line.slice(7)
|
|
51
|
+
} else if (line.startsWith('data: ')) {
|
|
52
|
+
dataLines.push(line.slice(6))
|
|
53
|
+
} else if (line === 'data:') {
|
|
54
|
+
dataLines.push('')
|
|
55
|
+
}
|
|
56
|
+
// Skip comments (lines starting with ':') and other fields
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (dataLines.length === 0) return null
|
|
60
|
+
|
|
61
|
+
return { event, data: dataLines.join('\n') }
|
|
62
|
+
}
|
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Workflow as BaseWorkflow } from '@stravigor/workflow'
|
|
2
|
+
import type { WorkflowContext as BaseContext } from '@stravigor/workflow'
|
|
3
|
+
import { AgentRunner } from './helpers.ts'
|
|
4
|
+
import type { Agent } from './agent.ts'
|
|
5
|
+
import type { AgentResult, WorkflowResult, Usage } from './types.ts'
|
|
6
|
+
|
|
7
|
+
// ── AI Workflow Context ─────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface WorkflowContext {
|
|
10
|
+
input: Record<string, unknown>
|
|
11
|
+
results: Record<string, AgentResult>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type StepMapInput = (ctx: WorkflowContext) => Record<string, unknown> | string
|
|
15
|
+
|
|
16
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function resolveInput(mapInput: StepMapInput | undefined, ctx: BaseContext): string {
|
|
19
|
+
if (!mapInput) return JSON.stringify(ctx.input)
|
|
20
|
+
const mapped = mapInput(ctx as unknown as WorkflowContext)
|
|
21
|
+
return typeof mapped === 'string' ? mapped : JSON.stringify(mapped)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function addUsage(total: Usage, add: Usage): void {
|
|
25
|
+
total.inputTokens += add.inputTokens
|
|
26
|
+
total.outputTokens += add.outputTokens
|
|
27
|
+
total.totalTokens += add.totalTokens
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Workflow Builder ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Multi-agent workflow orchestrator built on `@stravigor/workflow`.
|
|
34
|
+
*
|
|
35
|
+
* Supports sequential steps, parallel fan-out, routing, and loops.
|
|
36
|
+
* Each step wraps an Agent execution through the general-purpose workflow engine.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const result = await brain.workflow('content-pipeline')
|
|
40
|
+
* .step('research', ResearchAgent)
|
|
41
|
+
* .step('write', WriterAgent, (ctx) => ({
|
|
42
|
+
* topic: ctx.results.research.data.summary,
|
|
43
|
+
* }))
|
|
44
|
+
* .step('review', ReviewerAgent)
|
|
45
|
+
* .run({ topic: 'AI in healthcare' })
|
|
46
|
+
*/
|
|
47
|
+
export class Workflow {
|
|
48
|
+
private pipeline: BaseWorkflow
|
|
49
|
+
private totalUsage: Usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
50
|
+
|
|
51
|
+
constructor(name: string) {
|
|
52
|
+
this.pipeline = new BaseWorkflow(name)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a sequential step. Runs after all previous steps complete.
|
|
57
|
+
* Use `mapInput` to transform context into the agent's input.
|
|
58
|
+
*/
|
|
59
|
+
step(name: string, agent: new () => Agent, mapInput?: StepMapInput): this {
|
|
60
|
+
this.pipeline.step(name, async (ctx: BaseContext) => {
|
|
61
|
+
const inputText = resolveInput(mapInput, ctx)
|
|
62
|
+
const result = await new AgentRunner(agent).input(inputText).run()
|
|
63
|
+
addUsage(this.totalUsage, result.usage)
|
|
64
|
+
return result
|
|
65
|
+
})
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run multiple agents in parallel. All agents receive the same context.
|
|
71
|
+
* Each agent's result is stored under its name in the workflow results.
|
|
72
|
+
*/
|
|
73
|
+
parallel(
|
|
74
|
+
name: string,
|
|
75
|
+
agents: { name: string; agent: new () => Agent; mapInput?: StepMapInput }[]
|
|
76
|
+
): this {
|
|
77
|
+
this.pipeline.parallel(
|
|
78
|
+
name,
|
|
79
|
+
agents.map(a => ({
|
|
80
|
+
name: a.name,
|
|
81
|
+
handler: async (ctx: BaseContext) => {
|
|
82
|
+
const inputText = resolveInput(a.mapInput, ctx)
|
|
83
|
+
const result = await new AgentRunner(a.agent).input(inputText).run()
|
|
84
|
+
addUsage(this.totalUsage, result.usage)
|
|
85
|
+
return result
|
|
86
|
+
},
|
|
87
|
+
}))
|
|
88
|
+
)
|
|
89
|
+
return this
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Route to a specialized agent based on a router agent's output.
|
|
94
|
+
* The router agent should return structured output with a `route` field
|
|
95
|
+
* that matches one of the branch keys.
|
|
96
|
+
*/
|
|
97
|
+
route(
|
|
98
|
+
name: string,
|
|
99
|
+
router: new () => Agent,
|
|
100
|
+
branches: Record<string, new () => Agent>,
|
|
101
|
+
mapInput?: StepMapInput
|
|
102
|
+
): this {
|
|
103
|
+
// Router step: run the router agent, store as `${name}:router`
|
|
104
|
+
this.pipeline.step(`${name}:router`, async (ctx: BaseContext) => {
|
|
105
|
+
const inputText = resolveInput(mapInput, ctx)
|
|
106
|
+
const result = await new AgentRunner(router).input(inputText).run()
|
|
107
|
+
addUsage(this.totalUsage, result.usage)
|
|
108
|
+
return result
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Branch step: dispatch to the matching branch agent
|
|
112
|
+
this.pipeline.route(
|
|
113
|
+
name,
|
|
114
|
+
(ctx: BaseContext) => {
|
|
115
|
+
const routerResult = ctx.results[`${name}:router`] as AgentResult
|
|
116
|
+
return routerResult.data?.route ?? routerResult.text?.trim() ?? ''
|
|
117
|
+
},
|
|
118
|
+
Object.fromEntries(
|
|
119
|
+
Object.entries(branches).map(([key, BranchAgent]) => [
|
|
120
|
+
key,
|
|
121
|
+
async (ctx: BaseContext) => {
|
|
122
|
+
const inputText = resolveInput(mapInput, ctx)
|
|
123
|
+
const result = await new AgentRunner(BranchAgent).input(inputText).run()
|
|
124
|
+
addUsage(this.totalUsage, result.usage)
|
|
125
|
+
return result
|
|
126
|
+
},
|
|
127
|
+
])
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return this
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Run an agent in a loop until a condition is met or max iterations reached.
|
|
135
|
+
* Use `feedback` to transform the result into the next iteration's input.
|
|
136
|
+
*/
|
|
137
|
+
loop(
|
|
138
|
+
name: string,
|
|
139
|
+
agent: new () => Agent,
|
|
140
|
+
options: {
|
|
141
|
+
maxIterations: number
|
|
142
|
+
until?: (result: AgentResult, iteration: number) => boolean
|
|
143
|
+
feedback?: (result: AgentResult) => string
|
|
144
|
+
mapInput?: StepMapInput
|
|
145
|
+
}
|
|
146
|
+
): this {
|
|
147
|
+
this.pipeline.loop(
|
|
148
|
+
name,
|
|
149
|
+
async (input: unknown, _ctx: BaseContext) => {
|
|
150
|
+
const result = await new AgentRunner(agent).input(String(input)).run()
|
|
151
|
+
addUsage(this.totalUsage, result.usage)
|
|
152
|
+
return result
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
maxIterations: options.maxIterations,
|
|
156
|
+
until: options.until
|
|
157
|
+
? (result: unknown, iteration: number) => options.until!(result as AgentResult, iteration)
|
|
158
|
+
: undefined,
|
|
159
|
+
feedback: options.feedback
|
|
160
|
+
? (result: unknown) => options.feedback!(result as AgentResult)
|
|
161
|
+
: undefined,
|
|
162
|
+
mapInput: options.mapInput
|
|
163
|
+
? (ctx: BaseContext) => resolveInput(options.mapInput, ctx)
|
|
164
|
+
: (ctx: BaseContext) => JSON.stringify(ctx.input),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Execute the workflow. */
|
|
171
|
+
async run(input: Record<string, unknown>): Promise<WorkflowResult> {
|
|
172
|
+
this.totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
173
|
+
const result = await this.pipeline.run(input)
|
|
174
|
+
return {
|
|
175
|
+
results: result.results as Record<string, AgentResult>,
|
|
176
|
+
usage: this.totalUsage,
|
|
177
|
+
duration: result.duration,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|