@noetaris/harness 0.1.0 → 0.2.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/README.md +201 -0
- package/dist/index.d.ts +555 -40
- package/dist/index.js +405 -73
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @noetaris/harness
|
|
2
|
+
|
|
3
|
+
Agent execution framework — the harness implementation.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
agent = llm + harness
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The harness is the reusable artifact: execution loop, state management, routing, and provider abstraction. The LLM is a swappable commodity component.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add @noetaris/harness
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js ≥ 22.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createHarness, createAgent, field, required, runtime } from '@noetaris/harness'
|
|
23
|
+
|
|
24
|
+
// 1. Define the dependency interface your steps will need
|
|
25
|
+
interface Ctx {
|
|
26
|
+
model: { invoke(messages: string[], opts: any): Promise<{ text: string; toolCalls: any[] }> }
|
|
27
|
+
tools: Record<string, any>
|
|
28
|
+
prompts: { system: string }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Create the harness — fixes Ctx and infers State from the field schema
|
|
32
|
+
const h = createHarness<Ctx>()({
|
|
33
|
+
messages: field<string[]>({ default: () => [], reduce: (a, b) => [...a, ...b] }),
|
|
34
|
+
toolCalls: field<any[]> ({ default: () => [] }),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// 3. Define the loop — validated immediately at call time
|
|
38
|
+
h.loop(l =>
|
|
39
|
+
l.start()
|
|
40
|
+
.step('think', {
|
|
41
|
+
run: async (state, ctx) => {
|
|
42
|
+
const result = await ctx.model.invoke(state.messages, {
|
|
43
|
+
tools: Object.values(ctx.tools),
|
|
44
|
+
})
|
|
45
|
+
return { messages: [result.text], toolCalls: result.toolCalls }
|
|
46
|
+
},
|
|
47
|
+
route: (state) => state.toolCalls.length > 0 ? 'call_tools' : 'complete',
|
|
48
|
+
})
|
|
49
|
+
.on('call_tools').to('action')
|
|
50
|
+
.on('complete').end()
|
|
51
|
+
.step('action', {
|
|
52
|
+
run: async (state, ctx) => ({
|
|
53
|
+
toolResults: await runTools(state.toolCalls, ctx.tools),
|
|
54
|
+
}),
|
|
55
|
+
})
|
|
56
|
+
.next('think')
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// 4. Declare providers
|
|
60
|
+
h.provide('tools', { search: new MySearchTool() }) // hard-coded
|
|
61
|
+
h.provide('prompts', required()) // must be supplied at createAgent()
|
|
62
|
+
h.provide('model', runtime()) // must be supplied at agent.run()
|
|
63
|
+
|
|
64
|
+
// 5. Create an agent — assigns an ID and fills required() slots
|
|
65
|
+
const agent = createAgent('my-agent', h, {
|
|
66
|
+
prompts: { system: 'You are a helpful assistant.' },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// 6. Run
|
|
70
|
+
const run = agent.run(
|
|
71
|
+
{ messages: ['What is the weather in Paris?'] },
|
|
72
|
+
{ model: new MyLLMAdapter() },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const outcome = await run
|
|
76
|
+
console.log(outcome.signal, outcome.state)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Concepts
|
|
80
|
+
|
|
81
|
+
### The Loop
|
|
82
|
+
|
|
83
|
+
A harness loop is a directed graph. Steps are nodes; transitions are edges.
|
|
84
|
+
|
|
85
|
+
- **`run`** — transforms state. The only place state changes.
|
|
86
|
+
- **`route`** — reads post-run state, emits a named signal. Pure — no `ctx`, no mutation.
|
|
87
|
+
|
|
88
|
+
Three step patterns:
|
|
89
|
+
|
|
90
|
+
| Pattern | `run` | `route` | Transition |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| Transform + route | ✅ | ✅ | `.on(signal).to(step)` |
|
|
93
|
+
| Transform + next | ✅ | ❌ | `.next(name)` or implicit |
|
|
94
|
+
| Decision node | ❌ | ✅ | `.on(signal).to(step)` |
|
|
95
|
+
|
|
96
|
+
The loop structure is **validated at `h.loop()` call time**. Violations are thrown together as a `LoopValidationError` with a `violations: readonly string[]` property.
|
|
97
|
+
|
|
98
|
+
### State
|
|
99
|
+
|
|
100
|
+
State is defined as a schema of `field<T>()` declarations. The framework infers the `State` type — no separate interface needed.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const h = createHarness<Ctx>()({
|
|
104
|
+
messages: field<string[]>({
|
|
105
|
+
default: () => [],
|
|
106
|
+
reduce: (accumulated, update) => [...accumulated, ...update],
|
|
107
|
+
}),
|
|
108
|
+
count: field<number>({ default: () => 0 }),
|
|
109
|
+
})
|
|
110
|
+
// State = { messages: string[], count: number }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Steps return a `Partial<State>` update. The `reduce` function merges accumulating fields; absent fields are replaced directly.
|
|
114
|
+
|
|
115
|
+
### Providers
|
|
116
|
+
|
|
117
|
+
`h.provide()` is the single extension point — everything on `ctx` comes through it.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
h.provide('tools', { search: myTool }) // hard-coded — shared by all agents
|
|
121
|
+
h.provide('prompts', required()) // build-time — supplied at createAgent()
|
|
122
|
+
h.provide('model', runtime()) // per-run — supplied at agent.run()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Session Store
|
|
126
|
+
|
|
127
|
+
`h.store()` adds persistence. The reserved `session` key is used by the framework to save and restore state across runs; all other keys are surfaced as `ctx.store.<name>`.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { InMemorySessionStore } from '@noetaris/harness-store'
|
|
131
|
+
|
|
132
|
+
h.store({
|
|
133
|
+
session: new InMemorySessionStore(), // framework-managed lifecycle
|
|
134
|
+
knowledge: new MyKnowledgeGraph(), // available as ctx.store.knowledge
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The framework injects `ctx.sessionId` automatically on every run — no declaration in `Ctx` needed.
|
|
139
|
+
|
|
140
|
+
### Interrupts
|
|
141
|
+
|
|
142
|
+
A run can be stopped or resumed:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const run = agent.run(initialState, slots)
|
|
146
|
+
run.stop() // request graceful stop at the next step boundary
|
|
147
|
+
|
|
148
|
+
// When a step calls ctx.interrupt(), the run settles with signal "$interrupt".
|
|
149
|
+
// Resume in the same process:
|
|
150
|
+
const resumed = run.resume(response, interruptId)
|
|
151
|
+
|
|
152
|
+
// Or cross-process (requires a session store):
|
|
153
|
+
const resumed = agent.resume(response, sessionId, interruptId)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API
|
|
157
|
+
|
|
158
|
+
| Export | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `createHarness<Ctx>()(schema)` | Creates a harness. Fixes `Ctx`, infers `State` from schema. |
|
|
161
|
+
| `createAgent(id, h, slots)` | Assigns the agent an ID and fills `required()` slots. Returns an `Agent`. |
|
|
162
|
+
| `field<T>(opts)` | Declares a state field with a default and optional reduce function. |
|
|
163
|
+
| `required()` | Marks a provider slot as required at `createAgent()`. |
|
|
164
|
+
| `runtime()` | Marks a provider slot as required at `agent.run()`. |
|
|
165
|
+
| `composeObservers(...observers)` | Merges multiple `Observer` instances into one fan-out observer. |
|
|
166
|
+
| `SessionStore` | Interface for session persistence backends. |
|
|
167
|
+
| `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, and state. |
|
|
168
|
+
| `Observer` | Interface for telemetry hooks on run and step lifecycle events. |
|
|
169
|
+
| `ObserverAware` | Interface for provider objects that accept an `Observer` binding via `bindObserver()`. |
|
|
170
|
+
| `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`. |
|
|
171
|
+
| `StepContext` | Context passed to step-level observer hooks — `agentId`, `sessionId`, `stepName`. |
|
|
172
|
+
| `NoInterruptError` | Thrown when `resume()` is called but the session is not paused on a matching interrupt. |
|
|
173
|
+
| `SessionInFlightError` | Thrown when a session is already running. |
|
|
174
|
+
| `SessionPendingInterruptError` | Thrown when a session is paused on a pending interrupt — use `agent.resume()` instead of `agent.run()`. |
|
|
175
|
+
| `StoreLoadError` | Thrown when the session store fails to load state. |
|
|
176
|
+
|
|
177
|
+
## Design Principles
|
|
178
|
+
|
|
179
|
+
- **The loop is a contract, not optional.** Every agent is a loop with defined entry, routing, and exit. Structure is validated at definition time.
|
|
180
|
+
- **State is the only spine.** Steps do not call each other — they write to state; the loop routes based on state.
|
|
181
|
+
- **The framework owns what is universal.** Loop lifecycle, state management, session store lifecycle. Never prompt content, tool behaviour, or provider APIs.
|
|
182
|
+
- **Built-ins are sugar, not magic.** `h.store()` is `h.provide()` plus lifecycle hooks — no hidden mechanism.
|
|
183
|
+
|
|
184
|
+
## Requirements
|
|
185
|
+
|
|
186
|
+
- Node.js ≥ 22
|
|
187
|
+
- ESM only (`"type": "module"`)
|
|
188
|
+
- Zero runtime dependencies
|
|
189
|
+
|
|
190
|
+
## Related Packages
|
|
191
|
+
|
|
192
|
+
- [`@noetaris/harness-store`](https://github.com/noetaris-lab/harness-store) — session store implementations (`InMemorySessionStore`, `LocalFileSessionStore`, etc.)
|
|
193
|
+
- [`@noetaris/harness-types`](https://github.com/noetaris-lab/harness-types) — shared LLM type contract (`LLM`, `Message`, `Tool`, `ToolCall`, `LLMResponse`)
|
|
194
|
+
- [`@noetaris/harness-anthropic`](https://github.com/noetaris-lab/harness-anthropic) — Anthropic Claude adapter
|
|
195
|
+
- [`@noetaris/harness-openai`](https://github.com/noetaris-lab/harness-openai) — OpenAI adapter
|
|
196
|
+
- [`@noetaris/harness-google`](https://github.com/noetaris-lab/harness-google) — Google Gemini adapter
|
|
197
|
+
- [`@noetaris/harness-otel`](https://github.com/noetaris-lab/harness-otel) — OpenTelemetry observer bridge
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|