@xandout/libra-harness 0.1.8
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/LICENSE +21 -0
- package/README.md +486 -0
- package/dist/agent.d.ts +174 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +575 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai-sdk-model.d.ts +26 -0
- package/dist/ai-sdk-model.d.ts.map +1 -0
- package/dist/ai-sdk-model.js +260 -0
- package/dist/ai-sdk-model.js.map +1 -0
- package/dist/context.d.ts +75 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension.d.ts +63 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +2 -0
- package/dist/extension.js.map +1 -0
- package/dist/extras/extension-loader.d.ts +205 -0
- package/dist/extras/extension-loader.d.ts.map +1 -0
- package/dist/extras/extension-loader.js +315 -0
- package/dist/extras/extension-loader.js.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.d.ts +46 -0
- package/dist/extras/extensions/auto-steer/extension.d.ts.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.js +52 -0
- package/dist/extras/extensions/auto-steer/extension.js.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.json +7 -0
- package/dist/extras/extensions/auto-steer/index.d.ts +3 -0
- package/dist/extras/extensions/auto-steer/index.d.ts.map +1 -0
- package/dist/extras/extensions/auto-steer/index.js +2 -0
- package/dist/extras/extensions/auto-steer/index.js.map +1 -0
- package/dist/extras/extensions/disk-session/extension.d.ts +167 -0
- package/dist/extras/extensions/disk-session/extension.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/extension.js +492 -0
- package/dist/extras/extensions/disk-session/extension.js.map +1 -0
- package/dist/extras/extensions/disk-session/extension.json +6 -0
- package/dist/extras/extensions/disk-session/extension.test.d.ts +2 -0
- package/dist/extras/extensions/disk-session/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/extension.test.js +672 -0
- package/dist/extras/extensions/disk-session/extension.test.js.map +1 -0
- package/dist/extras/extensions/disk-session/index.d.ts +3 -0
- package/dist/extras/extensions/disk-session/index.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/index.js +2 -0
- package/dist/extras/extensions/disk-session/index.js.map +1 -0
- package/dist/extras/extensions/emoji/extension.d.ts +20 -0
- package/dist/extras/extensions/emoji/extension.d.ts.map +1 -0
- package/dist/extras/extensions/emoji/extension.js +22 -0
- package/dist/extras/extensions/emoji/extension.js.map +1 -0
- package/dist/extras/extensions/emoji/extension.json +7 -0
- package/dist/extras/extensions/emoji/index.d.ts +3 -0
- package/dist/extras/extensions/emoji/index.d.ts.map +1 -0
- package/dist/extras/extensions/emoji/index.js +2 -0
- package/dist/extras/extensions/emoji/index.js.map +1 -0
- package/dist/extras/extensions/filesystem/extension.d.ts +53 -0
- package/dist/extras/extensions/filesystem/extension.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/extension.js +350 -0
- package/dist/extras/extensions/filesystem/extension.js.map +1 -0
- package/dist/extras/extensions/filesystem/extension.json +6 -0
- package/dist/extras/extensions/filesystem/extension.test.d.ts +2 -0
- package/dist/extras/extensions/filesystem/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/extension.test.js +387 -0
- package/dist/extras/extensions/filesystem/extension.test.js.map +1 -0
- package/dist/extras/extensions/filesystem/index.d.ts +2 -0
- package/dist/extras/extensions/filesystem/index.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/index.js +2 -0
- package/dist/extras/extensions/filesystem/index.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.d.ts +53 -0
- package/dist/extras/extensions/keyword-extractor/extension.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.js +61 -0
- package/dist/extras/extensions/keyword-extractor/extension.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.json +6 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.d.ts +2 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.js +105 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/index.d.ts +5 -0
- package/dist/extras/extensions/keyword-extractor/index.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/index.js +3 -0
- package/dist/extras/extensions/keyword-extractor/index.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.d.ts +68 -0
- package/dist/extras/extensions/keyword-extractor/nlp.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.js +156 -0
- package/dist/extras/extensions/keyword-extractor/nlp.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts +2 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.js +176 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.js.map +1 -0
- package/dist/extras/extensions/logger/extension.d.ts +52 -0
- package/dist/extras/extensions/logger/extension.d.ts.map +1 -0
- package/dist/extras/extensions/logger/extension.js +68 -0
- package/dist/extras/extensions/logger/extension.js.map +1 -0
- package/dist/extras/extensions/logger/extension.json +7 -0
- package/dist/extras/extensions/logger/index.d.ts +3 -0
- package/dist/extras/extensions/logger/index.d.ts.map +1 -0
- package/dist/extras/extensions/logger/index.js +2 -0
- package/dist/extras/extensions/logger/index.js.map +1 -0
- package/dist/extras/extensions/mcp/extension.d.ts +63 -0
- package/dist/extras/extensions/mcp/extension.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/extension.js +437 -0
- package/dist/extras/extensions/mcp/extension.js.map +1 -0
- package/dist/extras/extensions/mcp/extension.json +7 -0
- package/dist/extras/extensions/mcp/extension.test.d.ts +2 -0
- package/dist/extras/extensions/mcp/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/extension.test.js +286 -0
- package/dist/extras/extensions/mcp/extension.test.js.map +1 -0
- package/dist/extras/extensions/mcp/index.d.ts +3 -0
- package/dist/extras/extensions/mcp/index.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/index.js +2 -0
- package/dist/extras/extensions/mcp/index.js.map +1 -0
- package/dist/extras/extensions/mem-session/extension.d.ts +24 -0
- package/dist/extras/extensions/mem-session/extension.d.ts.map +1 -0
- package/dist/extras/extensions/mem-session/extension.js +49 -0
- package/dist/extras/extensions/mem-session/extension.js.map +1 -0
- package/dist/extras/extensions/mem-session/extension.json +6 -0
- package/dist/extras/extensions/mem-session/index.d.ts +2 -0
- package/dist/extras/extensions/mem-session/index.d.ts.map +1 -0
- package/dist/extras/extensions/mem-session/index.js +2 -0
- package/dist/extras/extensions/mem-session/index.js.map +1 -0
- package/dist/extras/extensions/memory/extension.d.ts +54 -0
- package/dist/extras/extensions/memory/extension.d.ts.map +1 -0
- package/dist/extras/extensions/memory/extension.js +143 -0
- package/dist/extras/extensions/memory/extension.js.map +1 -0
- package/dist/extras/extensions/memory/extension.json +7 -0
- package/dist/extras/extensions/memory/index.d.ts +5 -0
- package/dist/extras/extensions/memory/index.d.ts.map +1 -0
- package/dist/extras/extensions/memory/index.js +3 -0
- package/dist/extras/extensions/memory/index.js.map +1 -0
- package/dist/extras/extensions/memory/llm-extractor.d.ts +42 -0
- package/dist/extras/extensions/memory/llm-extractor.d.ts.map +1 -0
- package/dist/extras/extensions/memory/llm-extractor.js +89 -0
- package/dist/extras/extensions/memory/llm-extractor.js.map +1 -0
- package/dist/extras/extensions/memory/types.d.ts +142 -0
- package/dist/extras/extensions/memory/types.d.ts.map +1 -0
- package/dist/extras/extensions/memory/types.js +2 -0
- package/dist/extras/extensions/memory/types.js.map +1 -0
- package/dist/extras/extensions/otel/extension.d.ts +57 -0
- package/dist/extras/extensions/otel/extension.d.ts.map +1 -0
- package/dist/extras/extensions/otel/extension.js +253 -0
- package/dist/extras/extensions/otel/extension.js.map +1 -0
- package/dist/extras/extensions/otel/extension.json +7 -0
- package/dist/extras/extensions/otel/extension.test.d.ts +2 -0
- package/dist/extras/extensions/otel/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/otel/extension.test.js +399 -0
- package/dist/extras/extensions/otel/extension.test.js.map +1 -0
- package/dist/extras/extensions/otel/index.d.ts +4 -0
- package/dist/extras/extensions/otel/index.d.ts.map +1 -0
- package/dist/extras/extensions/otel/index.js +3 -0
- package/dist/extras/extensions/otel/index.js.map +1 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts +60 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts.map +1 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.js +134 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.js.map +1 -0
- package/dist/extras/extensions/scripts/extension.d.ts +180 -0
- package/dist/extras/extensions/scripts/extension.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/extension.js +618 -0
- package/dist/extras/extensions/scripts/extension.js.map +1 -0
- package/dist/extras/extensions/scripts/extension.json +7 -0
- package/dist/extras/extensions/scripts/extension.test.d.ts +2 -0
- package/dist/extras/extensions/scripts/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/extension.test.js +552 -0
- package/dist/extras/extensions/scripts/extension.test.js.map +1 -0
- package/dist/extras/extensions/scripts/index.d.ts +3 -0
- package/dist/extras/extensions/scripts/index.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/index.js +2 -0
- package/dist/extras/extensions/scripts/index.js.map +1 -0
- package/dist/extras/extensions/skills/extension.d.ts +105 -0
- package/dist/extras/extensions/skills/extension.d.ts.map +1 -0
- package/dist/extras/extensions/skills/extension.js +419 -0
- package/dist/extras/extensions/skills/extension.js.map +1 -0
- package/dist/extras/extensions/skills/extension.json +7 -0
- package/dist/extras/extensions/skills/extension.test.d.ts +2 -0
- package/dist/extras/extensions/skills/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/skills/extension.test.js +184 -0
- package/dist/extras/extensions/skills/extension.test.js.map +1 -0
- package/dist/extras/extensions/skills/index.d.ts +4 -0
- package/dist/extras/extensions/skills/index.d.ts.map +1 -0
- package/dist/extras/extensions/skills/index.js +3 -0
- package/dist/extras/extensions/skills/index.js.map +1 -0
- package/dist/extras/extensions/streaming/extension.d.ts +15 -0
- package/dist/extras/extensions/streaming/extension.d.ts.map +1 -0
- package/dist/extras/extensions/streaming/extension.js +41 -0
- package/dist/extras/extensions/streaming/extension.js.map +1 -0
- package/dist/extras/extensions/streaming/extension.json +6 -0
- package/dist/extras/extensions/streaming/index.d.ts +2 -0
- package/dist/extras/extensions/streaming/index.d.ts.map +1 -0
- package/dist/extras/extensions/streaming/index.js +2 -0
- package/dist/extras/extensions/streaming/index.js.map +1 -0
- package/dist/extras/extensions/structured-output/extension.d.ts +11 -0
- package/dist/extras/extensions/structured-output/extension.d.ts.map +1 -0
- package/dist/extras/extensions/structured-output/extension.js +102 -0
- package/dist/extras/extensions/structured-output/extension.js.map +1 -0
- package/dist/extras/extensions/structured-output/extension.json +7 -0
- package/dist/extras/extensions/structured-output/index.d.ts +3 -0
- package/dist/extras/extensions/structured-output/index.d.ts.map +1 -0
- package/dist/extras/extensions/structured-output/index.js +2 -0
- package/dist/extras/extensions/structured-output/index.js.map +1 -0
- package/dist/extras/extensions/timestamp/extension.d.ts +11 -0
- package/dist/extras/extensions/timestamp/extension.d.ts.map +1 -0
- package/dist/extras/extensions/timestamp/extension.js +21 -0
- package/dist/extras/extensions/timestamp/extension.js.map +1 -0
- package/dist/extras/extensions/timestamp/extension.json +6 -0
- package/dist/extras/extensions/timestamp/index.d.ts +2 -0
- package/dist/extras/extensions/timestamp/index.d.ts.map +1 -0
- package/dist/extras/extensions/timestamp/index.js +2 -0
- package/dist/extras/extensions/timestamp/index.js.map +1 -0
- package/dist/extras/extensions/token-stats/extension.d.ts +50 -0
- package/dist/extras/extensions/token-stats/extension.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/extension.js +99 -0
- package/dist/extras/extensions/token-stats/extension.js.map +1 -0
- package/dist/extras/extensions/token-stats/extension.json +7 -0
- package/dist/extras/extensions/token-stats/extension.test.d.ts +2 -0
- package/dist/extras/extensions/token-stats/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/extension.test.js +197 -0
- package/dist/extras/extensions/token-stats/extension.test.js.map +1 -0
- package/dist/extras/extensions/token-stats/index.d.ts +3 -0
- package/dist/extras/extensions/token-stats/index.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/index.js +2 -0
- package/dist/extras/extensions/token-stats/index.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.d.ts +89 -0
- package/dist/extras/extensions/tool-buffer/extension.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.js +182 -0
- package/dist/extras/extensions/tool-buffer/extension.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.json +7 -0
- package/dist/extras/extensions/tool-buffer/extension.test.d.ts +2 -0
- package/dist/extras/extensions/tool-buffer/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.test.js +549 -0
- package/dist/extras/extensions/tool-buffer/extension.test.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/index.d.ts +3 -0
- package/dist/extras/extensions/tool-buffer/index.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/index.js +2 -0
- package/dist/extras/extensions/tool-buffer/index.js.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.d.ts +22 -0
- package/dist/extras/extensions/weather-tool/extension.d.ts.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.js +33 -0
- package/dist/extras/extensions/weather-tool/extension.js.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.json +7 -0
- package/dist/extras/extensions/weather-tool/index.d.ts +3 -0
- package/dist/extras/extensions/weather-tool/index.d.ts.map +1 -0
- package/dist/extras/extensions/weather-tool/index.js +2 -0
- package/dist/extras/extensions/weather-tool/index.js.map +1 -0
- package/dist/extras/models/ai-sdk-resolver.d.ts +15 -0
- package/dist/extras/models/ai-sdk-resolver.d.ts.map +1 -0
- package/dist/extras/models/ai-sdk-resolver.js +57 -0
- package/dist/extras/models/ai-sdk-resolver.js.map +1 -0
- package/dist/extras/models/index.d.ts +5 -0
- package/dist/extras/models/index.d.ts.map +1 -0
- package/dist/extras/models/index.js +3 -0
- package/dist/extras/models/index.js.map +1 -0
- package/dist/extras/models/routing-model.d.ts +18 -0
- package/dist/extras/models/routing-model.d.ts.map +1 -0
- package/dist/extras/models/routing-model.js +24 -0
- package/dist/extras/models/routing-model.js.map +1 -0
- package/dist/extras/openai-provider/index.d.ts +3 -0
- package/dist/extras/openai-provider/index.d.ts.map +1 -0
- package/dist/extras/openai-provider/index.js +2 -0
- package/dist/extras/openai-provider/index.js.map +1 -0
- package/dist/extras/openai-provider/server.d.ts +34 -0
- package/dist/extras/openai-provider/server.d.ts.map +1 -0
- package/dist/extras/openai-provider/server.js +335 -0
- package/dist/extras/openai-provider/server.js.map +1 -0
- package/dist/handle.d.ts +56 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +2 -0
- package/dist/handle.js.map +1 -0
- package/dist/hooks.d.ts +98 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +53 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +75 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -0
- package/dist/tool.d.ts +78 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +78 -0
- package/dist/tool.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Libra contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# libra
|
|
2
|
+
|
|
3
|
+
A small, composable, hookable agent harness. Library-first.
|
|
4
|
+
|
|
5
|
+
The core agent system is responsible for the actual LLM interaction. Everything else is an extension.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @xandout/libra-harness
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Agent } from '@xandout/libra-harness'
|
|
17
|
+
import { resolveModel } from '@xandout/libra-harness/extras/models'
|
|
18
|
+
|
|
19
|
+
// Resolve a model from environment variables (DEEPSEEK_API_KEY, OPENAI_API_KEY, etc.)
|
|
20
|
+
const model = await resolveModel('deepseek/deepseek-v4-flash')
|
|
21
|
+
|
|
22
|
+
const agent = new Agent({
|
|
23
|
+
model,
|
|
24
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const result = await agent.run({ message: 'Hello!' })
|
|
28
|
+
|
|
29
|
+
console.log(result.message)
|
|
30
|
+
console.log(result.finishReason) // 'stop'
|
|
31
|
+
console.log(result.iterations) // 1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const agent = new Agent({
|
|
38
|
+
model,
|
|
39
|
+
tools: [
|
|
40
|
+
{
|
|
41
|
+
name: 'search',
|
|
42
|
+
description: 'Search the web',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { query: { type: 'string' } },
|
|
46
|
+
required: ['query'],
|
|
47
|
+
},
|
|
48
|
+
async execute(args) {
|
|
49
|
+
return { toolCallId: '', content: `results for ${args.query}` }
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const result = await agent.run({ message: 'search for cats' })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The harness automatically continues the turn after tool calls — tool results are fed back to the model until it produces a final response.
|
|
59
|
+
|
|
60
|
+
### External tools
|
|
61
|
+
|
|
62
|
+
Set `external: true` on a tool to return its call to the caller instead of executing it internally. This enables the standard OpenAI tool-calling round-trip: the agent returns `finishReason: 'tool_calls'` with `pendingToolCalls`, the caller executes the tool and sends the result back as a `tool` message in a follow-up request.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const agent = new Agent({
|
|
66
|
+
model,
|
|
67
|
+
tools: [
|
|
68
|
+
{
|
|
69
|
+
name: 'get_weather',
|
|
70
|
+
description: 'Get weather for a city',
|
|
71
|
+
parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
|
|
72
|
+
external: true,
|
|
73
|
+
async execute() { return { toolCallId: '', content: '' } }, // never called
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const result = await agent.run({ message: 'Weather in SF?' })
|
|
79
|
+
// result.finishReason === 'tool_calls'
|
|
80
|
+
// result.pendingToolCalls === [{ id: '...', name: 'get_weather', arguments: '{"city":"SF"}' }]
|
|
81
|
+
|
|
82
|
+
// Caller executes the tool, then resumes:
|
|
83
|
+
const result2 = await agent.run({
|
|
84
|
+
message: 'Weather in SF?',
|
|
85
|
+
metadata: {
|
|
86
|
+
myMessages: [
|
|
87
|
+
{ role: 'user', content: 'Weather in SF?' },
|
|
88
|
+
{ role: 'assistant', content: '', toolCalls: result.pendingToolCalls },
|
|
89
|
+
{ role: 'tool', content: 'Sunny, 72F', toolCallId: '...', name: 'get_weather' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
// result2.finishReason === 'stop'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Multimodal Messages
|
|
97
|
+
|
|
98
|
+
Libra supports text, images, documents, audio, and video in message content:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const result = await agent.run({
|
|
102
|
+
message: [
|
|
103
|
+
{ type: 'text', text: 'Describe this image' },
|
|
104
|
+
{ type: 'file', mediaType: 'image/png', data: { type: 'url', url: 'https://example.com/photo.png' } },
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
File content can be a URL, base64 data, or text:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// URL
|
|
113
|
+
{ type: 'file', mediaType: 'image/png', data: { type: 'url', url: 'https://...' } }
|
|
114
|
+
|
|
115
|
+
// Base64
|
|
116
|
+
{ type: 'file', mediaType: 'image/jpeg', data: { type: 'data', data: '/9j/4AAQ...' } }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Extensions
|
|
120
|
+
|
|
121
|
+
Extensions add behavior without requiring the core to understand it.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import type { Extension } from '@xandout/libra-harness'
|
|
125
|
+
|
|
126
|
+
const loggingExtension: Extension = {
|
|
127
|
+
name: 'logging',
|
|
128
|
+
install(agent) {
|
|
129
|
+
agent.hook('beforeLLM', 'logging', async (ctx) => {
|
|
130
|
+
console.log(`LLM call with ${ctx.turn.messages.length} messages`)
|
|
131
|
+
})
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
agent.use(loggingExtension)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Built-in extensions (`@xandout/libra-harness/extras`)
|
|
139
|
+
|
|
140
|
+
Libra ships with a set of optional extensions under `@xandout/libra-harness/extras`. Each is importable via its own subpath — import only what you need:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { createLoggerExtension } from '@xandout/libra-harness/extras/logger'
|
|
144
|
+
import { createDiskSessionExtension } from '@xandout/libra-harness/extras/disk-session'
|
|
145
|
+
|
|
146
|
+
agent.use(createLoggerExtension())
|
|
147
|
+
agent.use(createDiskSessionExtension({ dir: './sessions' }))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Extension | Import | Description |
|
|
151
|
+
|-----------|--------|-------------|
|
|
152
|
+
| logger | `@xandout/libra-harness/extras/logger` | Logs each lifecycle stage |
|
|
153
|
+
| streaming | `@xandout/libra-harness/extras/streaming` | Streams text/reasoning/tool-input deltas |
|
|
154
|
+
| otel | `@xandout/libra-harness/extras/otel` | OpenTelemetry tracing (JSONL or OTLP export) |
|
|
155
|
+
| weather-tool | `@xandout/libra-harness/extras/weather-tool` | Registers a `get_weather` tool |
|
|
156
|
+
| structured-output | `@xandout/libra-harness/extras/structured-output` | Validates LLM output against a JSON schema |
|
|
157
|
+
| mcp | `@xandout/libra-harness/extras/mcp` | Connects to MCP servers, registers tools |
|
|
158
|
+
| skills | `@xandout/libra-harness/extras/skills` | Loads Agent Skills from directories |
|
|
159
|
+
| filesystem | `@xandout/libra-harness/extras/filesystem` | File read/write/list tools |
|
|
160
|
+
| scripts | `@xandout/libra-harness/extras/scripts` | Runs shell scripts in pipeline stages |
|
|
161
|
+
| keyword-extractor | `@xandout/libra-harness/extras/keyword-extractor` | Extracts keywords from messages (local NLP) |
|
|
162
|
+
| token-stats | `@xandout/libra-harness/extras/token-stats` | Tracks token usage per turn |
|
|
163
|
+
| tool-buffer | `@xandout/libra-harness/extras/tool-buffer` | Buffers and replays tool results |
|
|
164
|
+
| auto-steer | `@xandout/libra-harness/extras/auto-steer` | Auto-injects steering messages based on conditions |
|
|
165
|
+
| emoji | `@xandout/libra-harness/extras/emoji` | Decorates responses with an emoji prefix |
|
|
166
|
+
| timestamp | `@xandout/libra-harness/extras/timestamp` | Records start/finish timestamps in metadata |
|
|
167
|
+
| disk-session | `@xandout/libra-harness/extras/disk-session` | Disk-backed session history per session ID |
|
|
168
|
+
| mem-session | `@xandout/libra-harness/extras/mem-session` | In-memory session history per session ID |
|
|
169
|
+
| memory | `@xandout/libra-harness/extras/memory` | Long-term memory with LLM-based extraction |
|
|
170
|
+
|
|
171
|
+
**Priority** controls hook execution order within each lifecycle stage (higher = runs first, ties keep registration order). Set `priority` on any extension whose hooks must run before or after another extension's hooks.
|
|
172
|
+
|
|
173
|
+
See [`src/extras/README.md`](src/extras/README.md) for full API docs.
|
|
174
|
+
|
|
175
|
+
### Extension loader
|
|
176
|
+
|
|
177
|
+
For larger setups, `loadExtensions` accepts a mix of factory functions, `Extension` objects, and directory paths. It passes a shared config object to each factory, sorts by priority, and handles cleanup:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { loadExtensions, installExtensions, closeExtensions } from '@xandout/libra-harness/extras'
|
|
181
|
+
import { createLoggerExtension } from '@xandout/libra-harness/extras/logger'
|
|
182
|
+
import { createMcpExtension } from '@xandout/libra-harness/extras/mcp'
|
|
183
|
+
|
|
184
|
+
const loaded = await loadExtensions(
|
|
185
|
+
[
|
|
186
|
+
createLoggerExtension, // factory — config passed automatically
|
|
187
|
+
createMcpExtension, // factory — opts out if no mcpConfigPaths
|
|
188
|
+
'./extensions', // directory — discovers extensions by extension.json
|
|
189
|
+
],
|
|
190
|
+
{ mcpConfigPaths: './mcpServers.json' },
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
installExtensions(loaded, agent)
|
|
194
|
+
|
|
195
|
+
// ... run turns ...
|
|
196
|
+
|
|
197
|
+
await closeExtensions(loaded) // calls close() on extensions that have one (e.g. MCP)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Model Providers
|
|
201
|
+
|
|
202
|
+
### Native resolver
|
|
203
|
+
|
|
204
|
+
The easiest way to get a model is `resolveModel` from `@xandout/libra-harness/extras/models`. It reads API keys from environment variables and loads the appropriate AI SDK provider package dynamically:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { resolveModel } from '@xandout/libra-harness/extras/models'
|
|
208
|
+
|
|
209
|
+
// Reads DEEPSEEK_API_KEY from env, loads @ai-sdk/deepseek
|
|
210
|
+
const model = await resolveModel('deepseek/deepseek-v4-flash')
|
|
211
|
+
|
|
212
|
+
// Reads OPENAI_API_KEY from env, loads @ai-sdk/openai
|
|
213
|
+
const model = await resolveModel('openai/gpt-4.1-mini')
|
|
214
|
+
|
|
215
|
+
// Reads ANTHROPIC_API_KEY from env, loads @ai-sdk/anthropic
|
|
216
|
+
const model = await resolveModel('anthropic/claude-sonnet-4-20250514')
|
|
217
|
+
|
|
218
|
+
// Reads GOOGLE_GENERATIVE_AI_API_KEY from env, loads @ai-sdk/google
|
|
219
|
+
const model = await resolveModel('google/gemini-2.0-flash')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Model IDs use the format `provider/model`. Supported providers:
|
|
223
|
+
|
|
224
|
+
| Provider | Environment variable | Package |
|
|
225
|
+
|----------|---------------------|---------|
|
|
226
|
+
| `openai` | `OPENAI_API_KEY` | `@ai-sdk/openai` |
|
|
227
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | `@ai-sdk/anthropic` |
|
|
228
|
+
| `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | `@ai-sdk/google` |
|
|
229
|
+
| `deepseek` | `DEEPSEEK_API_KEY` | `@ai-sdk/deepseek` |
|
|
230
|
+
|
|
231
|
+
### Direct AISdkModel
|
|
232
|
+
|
|
233
|
+
You can also wrap any AI SDK `LanguageModelV4` directly:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { AISdkModel } from '@xandout/libra-harness'
|
|
237
|
+
import { openai } from '@ai-sdk/openai'
|
|
238
|
+
|
|
239
|
+
const model = new AISdkModel(openai('gpt-4.1-mini'))
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Routing model
|
|
243
|
+
|
|
244
|
+
Route requests to different models based on input content — e.g. send images to a vision model:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { createRoutingModel, hasImageInput } from '@xandout/libra-harness/extras/models'
|
|
248
|
+
|
|
249
|
+
const model = createRoutingModel({
|
|
250
|
+
default: await resolveModel('deepseek/deepseek-v4-flash'),
|
|
251
|
+
routes: [
|
|
252
|
+
{ when: hasImageInput, model: await resolveModel('deepseek/deepseek-v4-flash-vision-exp') },
|
|
253
|
+
],
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Custom Model interface
|
|
258
|
+
|
|
259
|
+
Implement the `Model` interface directly for custom providers:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import type { Model, ModelRequest, ModelResponse } from '@xandout/libra-harness'
|
|
263
|
+
|
|
264
|
+
class MyModel implements Model {
|
|
265
|
+
async generate(request: ModelRequest): Promise<ModelResponse> {
|
|
266
|
+
// translate to your API, call it, translate back
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Virtual Models (OpenAI-compatible provider)
|
|
272
|
+
|
|
273
|
+
Expose Libra agents as OpenAI-compatible models. Any framework that supports a custom OpenAI base URL can use your agents as models — with their own context, tools, extensions, and policy controls.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Agent } from '@xandout/libra-harness'
|
|
277
|
+
import { resolveModel } from '@xandout/libra-harness/extras/models'
|
|
278
|
+
import { createOpenAICompatibleServer } from '@xandout/libra-harness/extras/openai-provider'
|
|
279
|
+
|
|
280
|
+
const model = await resolveModel('deepseek/deepseek-v4-flash')
|
|
281
|
+
|
|
282
|
+
const server = createOpenAICompatibleServer({
|
|
283
|
+
agents: {
|
|
284
|
+
'research-agent': new Agent({ model, systemPrompt: 'You are a research assistant.' }),
|
|
285
|
+
'coding-agent': new Agent({ model, systemPrompt: 'You are a coding assistant.' }),
|
|
286
|
+
},
|
|
287
|
+
apiKeys: ['your-provider-key'],
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
server.listen(8787, '127.0.0.1')
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Now any OpenAI-compatible client can call these agents as models:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="your-provider-key")
|
|
297
|
+
response = client.chat.completions.create(
|
|
298
|
+
model="research-agent",
|
|
299
|
+
messages=[{"role": "user", "content": "Research quantum computing"}],
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Features:
|
|
304
|
+
- `GET /v1/models` and `POST /v1/chat/completions`
|
|
305
|
+
- Bearer and `x-api-key` authentication
|
|
306
|
+
- Text, image, system, developer, assistant, and tool messages
|
|
307
|
+
- JSON and SSE streaming responses
|
|
308
|
+
- Client-defined tools (external tool calling with round-trip)
|
|
309
|
+
- Agent's own tools run internally (invisible to the caller)
|
|
310
|
+
- Per-agent hooks for moderation, context injection, output filtering
|
|
311
|
+
|
|
312
|
+
See [`docs/virtual-models.md`](docs/virtual-models.md) and [`docs/virtual-models-pii-dlp.md`](docs/virtual-models-pii-dlp.md) for concepts and the PII/DLP pattern.
|
|
313
|
+
|
|
314
|
+
## Hook Lifecycle
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
beforeTurn → beforeContext → [beforeLLM → afterLLM → (beforeTool → afterTool)*]* → beforeResponse → afterTurn
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`onError` fires when any error is thrown during the turn. An `onError` hook can observe the error or recover by returning `{ skip: true, value: AgentResponse }`.
|
|
321
|
+
|
|
322
|
+
| Hook | When | Can Mutate |
|
|
323
|
+
|---|---|---|
|
|
324
|
+
| `beforeTurn` | Turn starts | messages, tools, metadata |
|
|
325
|
+
| `beforeContext` | Before LLM loop | messages |
|
|
326
|
+
| `beforeLLM` | Before each model call | modelRequest; can short-circuit with `{ skip: true, value: ModelResponse }` |
|
|
327
|
+
| `afterLLM` | After each model response | modelResponse |
|
|
328
|
+
| `beforeTool` | Before each tool call | can short-circuit with `{ skip: true, value: ToolResult }` |
|
|
329
|
+
| `afterTool` | After each tool result | toolResult |
|
|
330
|
+
| `beforeResponse` | Before final response | turn.response |
|
|
331
|
+
| `afterTurn` | Turn completes | turn (observe/persist) |
|
|
332
|
+
| `onError` | Any error thrown | can recover with `{ skip: true, value: AgentResponse }` |
|
|
333
|
+
|
|
334
|
+
## Steering & Halting
|
|
335
|
+
|
|
336
|
+
Each `agent.run()` returns a `RunHandle` that is thenable and exposes `steer()` and `halt()`:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
const handle = agent.run({ message: 'Research this topic' })
|
|
340
|
+
|
|
341
|
+
// Redirect mid-turn
|
|
342
|
+
handle.steer('Focus on the financial aspects')
|
|
343
|
+
|
|
344
|
+
// Cancel mid-turn
|
|
345
|
+
handle.halt('user cancelled')
|
|
346
|
+
|
|
347
|
+
// Or just await
|
|
348
|
+
const result = await handle
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Hooks can also steer/halt via `ctx.turn.steer()` and `ctx.turn.halt()` — these target only the current turn, even with concurrent turns running.
|
|
352
|
+
|
|
353
|
+
## Error Handling
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const fallbackExtension: Extension = {
|
|
357
|
+
name: 'fallback',
|
|
358
|
+
install(agent) {
|
|
359
|
+
agent.hook('onError', 'fallback', async (ctx) => {
|
|
360
|
+
if (ctx.error instanceof ModelError) {
|
|
361
|
+
return {
|
|
362
|
+
skip: true,
|
|
363
|
+
value: {
|
|
364
|
+
role: 'assistant',
|
|
365
|
+
message: 'The model is temporarily unavailable.',
|
|
366
|
+
finishReason: 'stop',
|
|
367
|
+
iterations: 0,
|
|
368
|
+
metadata: ctx.turn.metadata,
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The `errorPolicy` config controls what happens when no `onError` hook recovers:
|
|
378
|
+
|
|
379
|
+
| Policy | Behavior |
|
|
380
|
+
|--------|----------|
|
|
381
|
+
| `'fallback'` (default) | Returns a graceful response with `finishReason: 'error'` |
|
|
382
|
+
| `'throw'` | Rethrows the error |
|
|
383
|
+
| `function` | Custom recovery — return an `AgentResponse` or `undefined` to rethrow |
|
|
384
|
+
|
|
385
|
+
## Multiple Agents
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const researchAgent = new Agent({ model, systemPrompt: 'You are a research agent.' })
|
|
389
|
+
const codingAgent = new Agent({ model, systemPrompt: 'You are a coding agent.' })
|
|
390
|
+
|
|
391
|
+
// Agents are fully independent — different tools, extensions, models
|
|
392
|
+
await researchAgent.run({ message: 'Research this customer' })
|
|
393
|
+
await codingAgent.run({ message: 'Write a function' })
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Agent-as-Tool
|
|
397
|
+
|
|
398
|
+
Use `createAgentTool` to wrap an agent as a tool for an outer agent. Signal and metadata are automatically chained — if the outer turn is halted, the inner agent is also halted.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { Agent, createAgentTool } from '@xandout/libra-harness'
|
|
402
|
+
|
|
403
|
+
const researchAgent = new Agent({ model, systemPrompt: 'You are a research agent.' })
|
|
404
|
+
|
|
405
|
+
const outerAgent = new Agent({
|
|
406
|
+
model: outerModel,
|
|
407
|
+
tools: [
|
|
408
|
+
createAgentTool(researchAgent, {
|
|
409
|
+
name: 'research',
|
|
410
|
+
description: 'Delegate a research question to a research agent',
|
|
411
|
+
}),
|
|
412
|
+
],
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const result = await outerAgent.run({ message: 'Research this topic' })
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Streaming & Thinking Deltas
|
|
419
|
+
|
|
420
|
+
Libra supports streaming model output via an optional `onDelta` callback on `ModelRequest`. When set, `AISdkModel` uses its streaming path and emits text, reasoning, and tool-input deltas in real time. The final assembled `ModelResponse` is still returned.
|
|
421
|
+
|
|
422
|
+
Extensions enable streaming by setting `onDelta` on `ctx.modelRequest` in a `beforeLLM` hook:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import type { Extension, ModelDelta } from '@xandout/libra-harness'
|
|
426
|
+
|
|
427
|
+
const streamingExtension: Extension = {
|
|
428
|
+
name: 'streaming',
|
|
429
|
+
install(agent) {
|
|
430
|
+
agent.hook('beforeLLM', 'streaming', async (ctx) => {
|
|
431
|
+
if (!ctx.modelRequest) return
|
|
432
|
+
ctx.modelRequest.onDelta = (delta: ModelDelta) => {
|
|
433
|
+
if (delta.type === 'text') {
|
|
434
|
+
process.stdout.write(delta.content)
|
|
435
|
+
} else if (delta.type === 'reasoning') {
|
|
436
|
+
console.log('[thinking]', delta.content)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
agent.use(streamingExtension)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
When `onDelta` is not set, `AISdkModel` uses `doGenerate` (no streaming overhead). The core never interprets deltas — it simply passes the callback through.
|
|
447
|
+
|
|
448
|
+
## Examples
|
|
449
|
+
|
|
450
|
+
The `examples/` directory includes reference implementations:
|
|
451
|
+
|
|
452
|
+
- **`full-agent/`** — all built-in extensions via the loader, plus a local search-replace extension. Multi-turn session memory, MCP tools, skill loader, weather tool, streaming.
|
|
453
|
+
- **`basic-agent-concurrent/`** — single agent handling many concurrent users with session isolation and per-turn halt.
|
|
454
|
+
- **`subagents/`** — orchestrator agent delegating to specialized subagents via `createAgentTool`, with signal chaining and halt propagation.
|
|
455
|
+
- **`subagents-concurrent/`** — orchestrator fanning out to multiple subagents in parallel via `Promise.all`.
|
|
456
|
+
- **`structured-output/`** — `beforeResponse` hook that validates LLM output against a JSON schema.
|
|
457
|
+
- **`streaming/`** — `beforeLLM` hook that streams text/reasoning/tool-input deltas.
|
|
458
|
+
- **`openai-compatible-provider/`** — exposes multiple independent Libra agents as authenticated OpenAI-compatible models. Supports text, images, SSE streaming, and client-defined external tools.
|
|
459
|
+
- **`pii-dlp-provider/`** — proves the virtual model PII/DLP pattern: the LLM never sees real PII, the consumer never sees placeholders. Uses a CSV datasource with tool calling and full lifecycle logging.
|
|
460
|
+
- **`slack-bot/`** — full Slack bot with Socket Mode, block kit rendering, session persistence, MCP, skills, and OpenTelemetry tracing.
|
|
461
|
+
- **`large-document-mapper/`** — processes large documents in chunks with mapping and reduction.
|
|
462
|
+
|
|
463
|
+
## Architecture
|
|
464
|
+
|
|
465
|
+
- **Library-first** — no server, daemon, database, or queue required
|
|
466
|
+
- **Hookable** — 9 lifecycle hooks with observation and mutation
|
|
467
|
+
- **Extensible** — extensions register hooks/tools without core conditional logic
|
|
468
|
+
- **Composable** — multiple independent agents, agent-as-tool, all in-process
|
|
469
|
+
- **Provider-independent** — `Model` interface with AI SDK v4 integration and native resolver
|
|
470
|
+
- **Multimodal** — text, images, documents, audio, and video in message content
|
|
471
|
+
- **Steerable & haltable** — per-turn controls via `RunHandle` or `ctx.turn`
|
|
472
|
+
- **Streamable** — text, reasoning, and tool-input deltas via `onDelta` callback
|
|
473
|
+
- **Virtual models** — expose agents as OpenAI-compatible models with full provider-side control
|
|
474
|
+
- **Testable** — 354 tests with mock model
|
|
475
|
+
|
|
476
|
+
### What the core owns
|
|
477
|
+
|
|
478
|
+
Agent configuration, model interaction, messages, tools, turn execution, hooks, errors, streaming delta forwarding.
|
|
479
|
+
|
|
480
|
+
### What the core does NOT own
|
|
481
|
+
|
|
482
|
+
Sessions, memory, databases, MCP, HTTP servers, auth, logging, metrics, scheduling — all of these are extensions or host-application concerns.
|
|
483
|
+
|
|
484
|
+
## License
|
|
485
|
+
|
|
486
|
+
MIT
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Model } from './model.js';
|
|
2
|
+
import type { Tool } from './tool.js';
|
|
3
|
+
import type { Extension } from './extension.js';
|
|
4
|
+
import type { HookHandler, HookName } from './hooks.js';
|
|
5
|
+
import type { AgentRequest, AgentResponse, TurnContext } from './context.js';
|
|
6
|
+
import type { RunHandle } from './handle.js';
|
|
7
|
+
/**
|
|
8
|
+
* Context passed to a custom {@link ErrorPolicy} function.
|
|
9
|
+
*/
|
|
10
|
+
export interface ErrorPolicyContext {
|
|
11
|
+
/** The error that was thrown during the turn. */
|
|
12
|
+
error: unknown;
|
|
13
|
+
/** The mutable turn state (includes metadata, messages so far, etc.). */
|
|
14
|
+
turn: TurnContext;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How the agent handles an error that no `onError` hook recovered from.
|
|
18
|
+
*
|
|
19
|
+
* - `'fallback'` (default) — return a graceful fallback response with
|
|
20
|
+
* `finishReason: 'error'`. The error is attached to
|
|
21
|
+
* `response.metadata.error` so observability extensions can see it.
|
|
22
|
+
* - `'throw'` — rethrow the error. Use this when you want strict
|
|
23
|
+
* fail-fast behavior and prefer to handle errors at the call site.
|
|
24
|
+
* - `function` — custom policy. Return an {@link AgentResponse} to
|
|
25
|
+
* recover, or `undefined` to rethrow. This runs *after* all `onError`
|
|
26
|
+
* hooks, so it only fires when no extension recovered.
|
|
27
|
+
*
|
|
28
|
+
* `onError` hooks always run first and take precedence — if a hook
|
|
29
|
+
* returns `{ skip: true, value: AgentResponse }`, the error policy is
|
|
30
|
+
* not consulted.
|
|
31
|
+
*/
|
|
32
|
+
export type ErrorPolicy = 'throw' | 'fallback' | ((ctx: ErrorPolicyContext) => AgentResponse | undefined | Promise<AgentResponse | undefined>);
|
|
33
|
+
/** Configuration for constructing an {@link Agent}. */
|
|
34
|
+
export interface AgentConfig {
|
|
35
|
+
model: Model;
|
|
36
|
+
systemPrompt?: string;
|
|
37
|
+
tools?: Tool[];
|
|
38
|
+
/** Default max LLM iterations per turn. Default: 25. */
|
|
39
|
+
maxIterations?: number;
|
|
40
|
+
/** Default temperature. */
|
|
41
|
+
temperature?: number;
|
|
42
|
+
/** Default max tokens. */
|
|
43
|
+
maxTokens?: number;
|
|
44
|
+
/**
|
|
45
|
+
* How to handle errors that no `onError` hook recovered from.
|
|
46
|
+
*
|
|
47
|
+
* Default: `'fallback'` — returns a graceful response instead of
|
|
48
|
+
* throwing. Set to `'throw'` for strict fail-fast behavior, or pass
|
|
49
|
+
* a function for custom recovery logic.
|
|
50
|
+
*
|
|
51
|
+
* `onError` hooks always run first and take precedence.
|
|
52
|
+
*/
|
|
53
|
+
errorPolicy?: ErrorPolicy;
|
|
54
|
+
/**
|
|
55
|
+
* Message used by the built-in `'fallback'` error policy.
|
|
56
|
+
* Default: `'Sorry, I encountered an error. Please try again.'`
|
|
57
|
+
*/
|
|
58
|
+
fallbackMessage?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The core agent harness.
|
|
62
|
+
*
|
|
63
|
+
* Executes turns with a model, supports tool-call continuation, and is
|
|
64
|
+
* extended via hooks and extensions.
|
|
65
|
+
*
|
|
66
|
+
* Agents are **steerable** and **haltable** — {@link Agent.run} returns a
|
|
67
|
+
* {@link RunHandle} that exposes `steer()` and `halt()` controls tied to
|
|
68
|
+
* that specific turn.
|
|
69
|
+
*/
|
|
70
|
+
export declare class Agent {
|
|
71
|
+
private readonly registry;
|
|
72
|
+
private readonly tools;
|
|
73
|
+
/** Maps tool name → extension that registered it (for unload). */
|
|
74
|
+
private readonly toolOwners;
|
|
75
|
+
private readonly extensions;
|
|
76
|
+
private config;
|
|
77
|
+
/** Name of the extension currently being installed (for tool ownership tracking). */
|
|
78
|
+
private installingExtension?;
|
|
79
|
+
private readonly activeHandles;
|
|
80
|
+
constructor(config: AgentConfig);
|
|
81
|
+
/**
|
|
82
|
+
* Append content to the system prompt. This is a one-time mutation
|
|
83
|
+
* typically called by extensions at install time (e.g. the skills
|
|
84
|
+
* extension appends preloaded skill content so it's always active).
|
|
85
|
+
*/
|
|
86
|
+
appendSystemPrompt(content: string): void;
|
|
87
|
+
/** Install an extension. The extension's `install()` is called immediately. */
|
|
88
|
+
use(extension: Extension): this;
|
|
89
|
+
/**
|
|
90
|
+
* Uninstall an extension by name — removes its hooks, tools, and
|
|
91
|
+
* registration. If the extension has a `close()` method, it is called
|
|
92
|
+
* (awaited) so resources like MCP client connections are cleaned up.
|
|
93
|
+
*/
|
|
94
|
+
unload(name: string): Promise<this>;
|
|
95
|
+
/**
|
|
96
|
+
* Register a hook at a lifecycle stage.
|
|
97
|
+
*
|
|
98
|
+
* The hook's priority is derived from the extension that registers it:
|
|
99
|
+
* - During `install()` (inside `use()`), the installing extension's
|
|
100
|
+
* `priority` field is used.
|
|
101
|
+
* - Outside `install()`, the priority of an installed extension with
|
|
102
|
+
* a matching `extensionName` is used.
|
|
103
|
+
* - Otherwise, priority defaults to 0.
|
|
104
|
+
*
|
|
105
|
+
* Higher priority = runs first. Ties keep registration order.
|
|
106
|
+
*/
|
|
107
|
+
hook(stage: HookName, extensionName: string, handler: HookHandler): this;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the priority for a hook registered by `extensionName`.
|
|
110
|
+
* During install, the installing extension's priority wins. Otherwise
|
|
111
|
+
* look up the extension by name. Default: 0.
|
|
112
|
+
*/
|
|
113
|
+
private resolveExtensionPriority;
|
|
114
|
+
/** Register a tool. Ownership is tracked for the currently-installing extension. */
|
|
115
|
+
tool(tool: Tool): this;
|
|
116
|
+
/** List all registered tool names. */
|
|
117
|
+
getTools(): string[];
|
|
118
|
+
/**
|
|
119
|
+
* Inject a steering message into all active turns.
|
|
120
|
+
*
|
|
121
|
+
* Convenience method — operates on every currently-running turn. If you
|
|
122
|
+
* have the {@link RunHandle} from `run()`, prefer calling
|
|
123
|
+
* `handle.steer()` directly. Hooks should use `ctx.turn.steer()` to
|
|
124
|
+
* target only their own turn.
|
|
125
|
+
*/
|
|
126
|
+
steer(message: string): void;
|
|
127
|
+
/**
|
|
128
|
+
* Halt all active turns.
|
|
129
|
+
*
|
|
130
|
+
* Convenience method — halts every currently-running turn. If you have
|
|
131
|
+
* the {@link RunHandle} from `run()`, prefer calling `handle.halt()`
|
|
132
|
+
* directly. Hooks should use `ctx.turn.halt()` to target only their own
|
|
133
|
+
* turn.
|
|
134
|
+
*/
|
|
135
|
+
halt(reason?: string): void;
|
|
136
|
+
/** Whether any turn is currently active. */
|
|
137
|
+
get isRunning(): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Execute an agent turn.
|
|
140
|
+
*
|
|
141
|
+
* Returns a {@link RunHandle} that is thenable (so `await agent.run(req)`
|
|
142
|
+
* still works) and exposes `steer()` and `halt()` controls for this
|
|
143
|
+
* specific turn. Multiple turns can run concurrently — each gets its
|
|
144
|
+
* own independent handle.
|
|
145
|
+
*/
|
|
146
|
+
run(request: AgentRequest): RunHandle;
|
|
147
|
+
private mergeTools;
|
|
148
|
+
private executeTurn;
|
|
149
|
+
/**
|
|
150
|
+
* Build the response, run beforeResponse + afterTurn hooks, and return.
|
|
151
|
+
* All turn exit paths go through here so extensions (session, memory,
|
|
152
|
+
* observability) always get a chance to persist/observe.
|
|
153
|
+
*/
|
|
154
|
+
private finishTurn;
|
|
155
|
+
/**
|
|
156
|
+
* Build a fallback error response, run afterTurn hooks, and return.
|
|
157
|
+
*
|
|
158
|
+
* Used by the default `'fallback'` error policy when no `onError` hook
|
|
159
|
+
* recovered. The error is attached to `response.metadata.error` so
|
|
160
|
+
* observability extensions can inspect it via `afterTurn`.
|
|
161
|
+
*/
|
|
162
|
+
private finishWithError;
|
|
163
|
+
private executeToolCall;
|
|
164
|
+
/**
|
|
165
|
+
* Race a promise against an abort signal. Returns `undefined` if the
|
|
166
|
+
* signal fires first (meaning "halted"). The original promise is NOT
|
|
167
|
+
* cancelled — it may continue running in the background.
|
|
168
|
+
*/
|
|
169
|
+
private raceWithAbort;
|
|
170
|
+
private drainSteering;
|
|
171
|
+
private buildResponse;
|
|
172
|
+
private runHooks;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA+B,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,IAAI,EAAe,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAe,WAAW,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAEjF,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,OAAO,CAAC;IACf,yEAAyE;IACzE,IAAI,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,UAAU,GACV,CAAC,CAAC,GAAG,EAAE,kBAAkB,KAAK,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;AAKlG,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD;;;;;;;;;GASG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IACjD,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAc;IAE5B,qFAAqF;IACrF,OAAO,CAAC,mBAAmB,CAAC,CAAS;IAIrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgC;IAE9D,YAAY,MAAM,EAAE,WAAW,EAK9B;IAID;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMxC;IAID,+EAA+E;IAC/E,GAAG,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAY9B;IAED;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAexC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAIvE;IAED;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAUhC,oFAAoF;IACpF,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAMrB;IAED,sCAAsC;IACtC,QAAQ,IAAI,MAAM,EAAE,CAEnB;IAID;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE3B;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,SAAW,GAAG,IAAI,CAE5B;IAED,4CAA4C;IAC5C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAID;;;;;;;OAOG;IACH,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAwCpC;IAED,OAAO,CAAC,UAAU;YAMJ,WAAW;IA2MzB;;;;OAIG;YACW,UAAU;IAmBxB;;;;;;OAMG;YACW,eAAe;YAef,eAAe;IAoE7B;;;;OAIG;YACW,aAAa;IAa3B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;YAmBP,QAAQ;CAuBvB"}
|