@rarebit-one/harness-kernel 0.5.1
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 +286 -0
- package/dist/agent.d.ts +71 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +258 -0
- package/dist/agent.js.map +1 -0
- package/dist/connectors/mcpClient.d.ts +22 -0
- package/dist/connectors/mcpClient.d.ts.map +1 -0
- package/dist/connectors/mcpClient.js +63 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/context/types.d.ts +68 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +43 -0
- package/dist/context/types.js.map +1 -0
- package/dist/engines/capability.d.ts +67 -0
- package/dist/engines/capability.d.ts.map +1 -0
- package/dist/engines/capability.js +73 -0
- package/dist/engines/capability.js.map +1 -0
- package/dist/engines/capabilityEmissions.d.ts +27 -0
- package/dist/engines/capabilityEmissions.d.ts.map +1 -0
- package/dist/engines/capabilityEmissions.js +37 -0
- package/dist/engines/capabilityEmissions.js.map +1 -0
- package/dist/engines/capabilityMcp.d.ts +23 -0
- package/dist/engines/capabilityMcp.d.ts.map +1 -0
- package/dist/engines/capabilityMcp.js +36 -0
- package/dist/engines/capabilityMcp.js.map +1 -0
- package/dist/engines/capabilityStdio.d.ts +46 -0
- package/dist/engines/capabilityStdio.d.ts.map +1 -0
- package/dist/engines/capabilityStdio.js +39 -0
- package/dist/engines/capabilityStdio.js.map +1 -0
- package/dist/engines/claudeCode.d.ts +73 -0
- package/dist/engines/claudeCode.d.ts.map +1 -0
- package/dist/engines/claudeCode.js +100 -0
- package/dist/engines/claudeCode.js.map +1 -0
- package/dist/engines/claudeCodeDriver.d.ts +14 -0
- package/dist/engines/claudeCodeDriver.d.ts.map +1 -0
- package/dist/engines/claudeCodeDriver.js +90 -0
- package/dist/engines/claudeCodeDriver.js.map +1 -0
- package/dist/engines/codex.d.ts +72 -0
- package/dist/engines/codex.d.ts.map +1 -0
- package/dist/engines/codex.js +157 -0
- package/dist/engines/codex.js.map +1 -0
- package/dist/engines/codexDriver.d.ts +43 -0
- package/dist/engines/codexDriver.d.ts.map +1 -0
- package/dist/engines/codexDriver.js +110 -0
- package/dist/engines/codexDriver.js.map +1 -0
- package/dist/engines/connectorMcp.d.ts +60 -0
- package/dist/engines/connectorMcp.d.ts.map +1 -0
- package/dist/engines/connectorMcp.js +137 -0
- package/dist/engines/connectorMcp.js.map +1 -0
- package/dist/engines/index.d.ts +33 -0
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +29 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/native.d.ts +55 -0
- package/dist/engines/native.d.ts.map +1 -0
- package/dist/engines/native.js +117 -0
- package/dist/engines/native.js.map +1 -0
- package/dist/engines/types.d.ts +107 -0
- package/dist/engines/types.d.ts.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/events.d.ts +211 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +69 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/loop.d.ts +132 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +77 -0
- package/dist/loop.js.map +1 -0
- package/dist/models/chat.d.ts +65 -0
- package/dist/models/chat.d.ts.map +1 -0
- package/dist/models/chat.js +66 -0
- package/dist/models/chat.js.map +1 -0
- package/dist/models/middleware.d.ts +63 -0
- package/dist/models/middleware.d.ts.map +1 -0
- package/dist/models/middleware.js +139 -0
- package/dist/models/middleware.js.map +1 -0
- package/dist/models/registry.d.ts +78 -0
- package/dist/models/registry.d.ts.map +1 -0
- package/dist/models/registry.js +0 -0
- package/dist/models/registry.js.map +1 -0
- package/dist/models/types.d.ts +149 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +64 -0
- package/dist/models/types.js.map +1 -0
- package/dist/primitives/codeExec.d.ts +25 -0
- package/dist/primitives/codeExec.d.ts.map +1 -0
- package/dist/primitives/codeExec.js +69 -0
- package/dist/primitives/codeExec.js.map +1 -0
- package/dist/primitives/download.d.ts +19 -0
- package/dist/primitives/download.d.ts.map +1 -0
- package/dist/primitives/download.js +113 -0
- package/dist/primitives/download.js.map +1 -0
- package/dist/primitives/fs.d.ts +5 -0
- package/dist/primitives/fs.d.ts.map +1 -0
- package/dist/primitives/fs.js +59 -0
- package/dist/primitives/fs.js.map +1 -0
- package/dist/primitives/http.d.ts +100 -0
- package/dist/primitives/http.d.ts.map +1 -0
- package/dist/primitives/http.js +257 -0
- package/dist/primitives/http.js.map +1 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +90 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/clientOptions.d.ts +14 -0
- package/dist/providers/clientOptions.d.ts.map +1 -0
- package/dist/providers/clientOptions.js +26 -0
- package/dist/providers/clientOptions.js.map +1 -0
- package/dist/providers/index.d.ts +24 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +51 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mock.d.ts +25 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +85 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/openai.d.ts +32 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +118 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +29 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +35 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/types.d.ts +60 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routing/staticResolver.d.ts +41 -0
- package/dist/routing/staticResolver.d.ts.map +1 -0
- package/dist/routing/staticResolver.js +67 -0
- package/dist/routing/staticResolver.js.map +1 -0
- package/dist/routing/types.d.ts +72 -0
- package/dist/routing/types.d.ts.map +1 -0
- package/dist/routing/types.js +11 -0
- package/dist/routing/types.js.map +1 -0
- package/dist/secrets.d.ts +8 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +14 -0
- package/dist/secrets.js.map +1 -0
- package/dist/tools/metadata.d.ts +93 -0
- package/dist/tools/metadata.d.ts.map +1 -0
- package/dist/tools/metadata.js +49 -0
- package/dist/tools/metadata.js.map +1 -0
- package/dist/tools/modelTool.d.ts +35 -0
- package/dist/tools/modelTool.d.ts.map +1 -0
- package/dist/tools/modelTool.js +37 -0
- package/dist/tools/modelTool.js.map +1 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +166 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rarebit One
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# harness-kernel
|
|
2
|
+
|
|
3
|
+
A **provider-neutral AI-harness kernel** in TypeScript: LLM adapters, a tool-use
|
|
4
|
+
loop, MCP connectors, sandbox primitives, and a pluggable agent-engine seam.
|
|
5
|
+
|
|
6
|
+
It is deliberately a *kernel*, not a framework. It holds the commodity mechanics
|
|
7
|
+
every AI harness needs and nothing else — applications are the userland. The
|
|
8
|
+
kernel never knows an application exists; dependencies point **one way only**
|
|
9
|
+
(`app layer → kernel`, never the reverse).
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
harness-kernel
|
|
13
|
+
▲ ▲ ▲
|
|
14
|
+
app harness app harness app harness (one-way deps)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
**v0 — private, unpublished.** The package is built OSS-ready (MIT licensed, npm
|
|
20
|
+
OIDC trusted-publishing workflow wired) but the repo is private and nothing has
|
|
21
|
+
been published to npm. `publish.yml` fires on a GitHub Release for **any** tag,
|
|
22
|
+
so it ships **inert**: the one mutating step is skipped unless the repo variable
|
|
23
|
+
`PUBLISH_LIVE` is `"true"`, and an unarmed run reports what it would have
|
|
24
|
+
published. "No Release exists yet" was never a safety property — it was an
|
|
25
|
+
absence.
|
|
26
|
+
|
|
27
|
+
v0 is a mechanical extraction of a proven, already-provider-neutral agent core
|
|
28
|
+
into a standalone package — the mechanics are battle-tested, the packaging is
|
|
29
|
+
new. Seam-widening (a general `ModelInvocation` seam, streaming, multimodal
|
|
30
|
+
input, middleware, context providers) is planned as an additive v0.2.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @rarebit-one/harness-kernel # once published
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
ESM-only, Node >= 22.
|
|
39
|
+
|
|
40
|
+
## What's in it
|
|
41
|
+
|
|
42
|
+
| Area | Exports |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| **Model seam** | `ModelInvocation` — one arrow for every model kind, with declared capabilities, a uniform result envelope and `probe()` |
|
|
45
|
+
| **Providers** | `Provider` interface + `selectProvider()` over Anthropic, OpenAI, OpenRouter, and an offline `mock`; `chatModel()` puts any of them on the seam |
|
|
46
|
+
| **Agent loop** | `runAgent()` — a provider-neutral tool-use loop with step and wall-clock budgets |
|
|
47
|
+
| **Loop** | `Loop` + `nativeLoop` — the control loop as a seam; the kernel ships one and it is the loop `runAgent` has always run |
|
|
48
|
+
| **Run events** | `RunEvent` — an ordered, structured account of a run (turns, tool calls, budgets, outcome), emitted to an optional sink |
|
|
49
|
+
| **Tools** | `Tool`, `primitiveTools()` (`run_code`, `read_file`, `list_files`, `http_fetch`), `connectorTools()`, `modelAsTool()`, metadata + projections |
|
|
50
|
+
| **Connectors** | `connectMcp()` — MCP clients over stdio / streamable HTTP / SSE |
|
|
51
|
+
| **Engines** | The `AgentEngine` seam + `native`, `claude-code`, and `codex` harnesses, plus the capability *mechanism* (scope guards, a generic `write_file`, and MCP/stdio transports) — the capability *set* is yours to inject |
|
|
52
|
+
| **Routing** | `RouteResolver` + `StaticRouteResolver` — capability → model, prompt, tools |
|
|
53
|
+
| **Context** | `ContextProvider` chain, assembled in parallel and rendered into the prompt |
|
|
54
|
+
| **Secrets** | `secretsToEnv()` — resolved secret values into an exec environment |
|
|
55
|
+
|
|
56
|
+
## Extension points
|
|
57
|
+
|
|
58
|
+
The kernel is meant to be extended from outside, never patched from inside.
|
|
59
|
+
There are seven seams, and each ships with the in-process default that makes it
|
|
60
|
+
usable on its own — no database, no service, no infrastructure.
|
|
61
|
+
|
|
62
|
+
**1. Model kinds.** A chat LLM is one kind among several. Register any other and
|
|
63
|
+
resolve it back through the same call:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { ModelRegistry, chatModel, selectProvider } from "@rarebit-one/harness-kernel"
|
|
67
|
+
|
|
68
|
+
const registry = new ModelRegistry()
|
|
69
|
+
.registerInstance(chatModel(selectProvider("anthropic")))
|
|
70
|
+
.register({ kind: "vision.detect", id: "yolo" }, () => myDetector)
|
|
71
|
+
|
|
72
|
+
const detector = registry.resolve<DetectRequest, DetectionResult>({
|
|
73
|
+
kind: "vision.detect",
|
|
74
|
+
id: "yolo",
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`Req` and `Res` stay typed per kind — the seam unifies the arrow and the
|
|
79
|
+
envelope (`ModelResult`, `InvokeContext`, `ModelCaps`, `probe`), never the
|
|
80
|
+
payload. An unresolved ref throws rather than returning `undefined`.
|
|
81
|
+
|
|
82
|
+
**2. Route resolution.** One layer up: which model, prompt and tools serve a
|
|
83
|
+
named capability. `StaticRouteResolver` is capabilities-as-code; a dynamic
|
|
84
|
+
resolver is your adapter behind the same interface.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const resolver = new StaticRouteResolver([
|
|
88
|
+
{ name: "summarise", model: { kind: "chat", id: "anthropic" }, prompt: "Summarise." },
|
|
89
|
+
])
|
|
90
|
+
const route = await resolver.resolve("summarise")
|
|
91
|
+
const model = registry.resolve(route.model) // the resolver picks, the registry binds
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**3. Middleware.** Payload-blind, so one implementation covers every kind:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const model = withMiddleware(anyModel, [correlationMiddleware(), loggingMiddleware()])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**4. Engines.** The `AgentEngine` seam — `native`, `claude-code`, `codex`, or
|
|
101
|
+
your own. File mutations stay out of band.
|
|
102
|
+
|
|
103
|
+
**5. Context providers.** Replace a single pre-baked context string with a
|
|
104
|
+
chain, so a live source can inject fragments:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
new NativeEngine({ contextProviders: [myLiveFeed] })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Providers run in parallel; one that fails is logged and skipped rather than
|
|
111
|
+
failing the run.
|
|
112
|
+
|
|
113
|
+
**6. Tools.** Rich metadata (capability scoping, invocation-mode gating,
|
|
114
|
+
confirmation, reversibility, retention, client visibility) with lossy
|
|
115
|
+
projections down to what a provider actually needs — and `modelAsTool()`, which
|
|
116
|
+
surfaces any model invocation as a callable tool. That last one is the cleanest
|
|
117
|
+
form of extension: a perception model becomes something the chat loop can call
|
|
118
|
+
without the loop, the engines, or any kernel code learning perception exists.
|
|
119
|
+
|
|
120
|
+
**7. The loop.** The control flow itself. Everything above answers "which
|
|
121
|
+
implementation?" for something the loop *uses*; this is the loop.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const confirmFirst: Loop = {
|
|
125
|
+
name: "confirm-first",
|
|
126
|
+
run: async (req, ctx) => {
|
|
127
|
+
const gated = req.tools.filter((t) => t.meta?.requiresConfirmation)
|
|
128
|
+
if (gated.length > 0) return { text: "awaiting confirmation", steps: 0, outcome: "completed" }
|
|
129
|
+
return nativeLoop.run(req, ctx)
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
new NativeEngine({ loop: confirmFirst })
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A loop is handed **resolved** materials — a bound model, built prompts, a
|
|
136
|
+
projected tool surface, budgets as numbers rather than optionals (via
|
|
137
|
+
`resolveLoopLimits`, the one place defaults live). It decides control flow and
|
|
138
|
+
nothing else, which is what keeps a second implementation small enough to be
|
|
139
|
+
worth writing. `runAgent` and `nativeLoop` are the same code path, so they
|
|
140
|
+
cannot drift.
|
|
141
|
+
|
|
142
|
+
Capabilities are **checked, not assumed**: asking a model to stream when it
|
|
143
|
+
declared `streaming: false` throws a `CapabilityError` instead of quietly
|
|
144
|
+
returning a buffered response.
|
|
145
|
+
|
|
146
|
+
### Run events
|
|
147
|
+
|
|
148
|
+
`log` is prose for a human. The event stream is the same run, machine-readable:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { runAgent, recordRunEvents } from "@rarebit-one/harness-kernel"
|
|
152
|
+
|
|
153
|
+
const { sink, events } = recordRunEvents()
|
|
154
|
+
await runAgent({ provider, system, userPrompt, tools, emit: sink })
|
|
155
|
+
// events: run.started -> model.turn -> tool.called -> tool.succeeded -> ... -> run.finished
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The **bookends** — `run.started` and `run.finished` — are emitted by
|
|
159
|
+
`runWithEvents`, which wraps whichever `Loop` runs. A loop emits only what
|
|
160
|
+
happens inside it, so a custom loop that emits nothing still produces a run that
|
|
161
|
+
is visibly a run. A guarantee that depends on every future implementor
|
|
162
|
+
remembering it is not a guarantee.
|
|
163
|
+
|
|
164
|
+
On the failure path `steps` and `text` are **omitted**, not zeroed: the loop
|
|
165
|
+
threw and never returned a result, so any number there would be invented. The
|
|
166
|
+
`model.turn` events already in the stream are the authoritative record of how
|
|
167
|
+
far it got.
|
|
168
|
+
|
|
169
|
+
Engines take the same sink through `EngineContext`:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await engine.run(spec, { log, emit: sink })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Both are **optional** — omit them and the loop behaves exactly as it did before
|
|
176
|
+
events existed. Every event carries a monotonic `seq` from a single per-run
|
|
177
|
+
counter, so a gap is detectable: a dropped event must never look like an event
|
|
178
|
+
that never happened. A sink that throws is reported through `log` and skipped,
|
|
179
|
+
never allowed to fail the run.
|
|
180
|
+
|
|
181
|
+
A sink is an observer and can never be a participant: `tool.called.input` is a
|
|
182
|
+
detached copy, so a sink that redacts in place cannot change what the tool
|
|
183
|
+
receives. The flip side is a disclosure surface worth knowing about — that field
|
|
184
|
+
carries the **raw tool arguments**, so any secret or PII a model passed as an
|
|
185
|
+
argument now reaches every attached sink. Nothing outside the model conversation
|
|
186
|
+
captured these before; a sink that persists or forwards events should redact.
|
|
187
|
+
|
|
188
|
+
`run.finished` is always the last event, including when the loop throws — the
|
|
189
|
+
error propagates unchanged, but `outcome: "failed"` closes the stream first, so
|
|
190
|
+
a crashed run is never mistaken for one still in flight.
|
|
191
|
+
|
|
192
|
+
**The kernel emits this stream; it does not store it.** Resume, fork, search and
|
|
193
|
+
replay all want persistence and a schema — infrastructure a kernel must not
|
|
194
|
+
ship. `recordRunEvents()` is an in-memory recorder for tests and the reference
|
|
195
|
+
shape for a real store, not a session store itself.
|
|
196
|
+
|
|
197
|
+
`tool.succeeded` carries the tool's own `reversible` / `undoToolName` /
|
|
198
|
+
`undoWindowSeconds` from `ToolMetadata`, copied at call time. That is the
|
|
199
|
+
missing half of a bargain the metadata already made: it could declare a tool
|
|
200
|
+
undoable but nothing recorded that the tool *ran*, so an application had nothing
|
|
201
|
+
concrete to undo. Deciding whether to undo remains policy, and stays in the
|
|
202
|
+
application.
|
|
203
|
+
|
|
204
|
+
## Usage
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { selectProvider, runAgent, primitiveTools } from "@rarebit-one/harness-kernel"
|
|
208
|
+
|
|
209
|
+
const provider = selectProvider("anthropic", { model: "claude-sonnet-5" })
|
|
210
|
+
|
|
211
|
+
const text = await runAgent({
|
|
212
|
+
provider,
|
|
213
|
+
system: "You are a helpful autonomous agent.",
|
|
214
|
+
userPrompt: "Summarise the repo's README.",
|
|
215
|
+
tools: primitiveTools("/path/to/sandbox", process.env),
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
With no provider API key configured, `selectProvider` falls back to the offline
|
|
220
|
+
`mock` provider — so the whole loop runs end to end with no keys and no network.
|
|
221
|
+
That is deliberate: the test suite depends on it, and so can yours.
|
|
222
|
+
|
|
223
|
+
### Engines
|
|
224
|
+
|
|
225
|
+
An engine is a pluggable agent harness. `native` is the in-process tool-use loop
|
|
226
|
+
above; `claude-code` and `codex` shell out to those CLIs/SDKs. All three share
|
|
227
|
+
one capability surface (`open_issue`, `write_file`, `promote_knowledge`) scoped
|
|
228
|
+
to a single run's workspace, and all three mutate files **out of band** — the
|
|
229
|
+
caller diffs the working tree afterwards, so the engine contract never has to
|
|
230
|
+
model "an edit" uniformly.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { selectEngine } from "@rarebit-one/harness-kernel"
|
|
234
|
+
|
|
235
|
+
const engine = selectEngine("native", { domainTools }) // "claude-code" | "codex" | unknown → native
|
|
236
|
+
const support = engine.supports(spec)
|
|
237
|
+
if (!support.ok) throw new Error(support.reason) // fail loud, never silently degrade
|
|
238
|
+
const { text } = await engine.run(spec, { log: console.error })
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### The kernel defines no domain tools
|
|
242
|
+
|
|
243
|
+
`primitiveTools()` is strictly generic — sandboxed shell/file/HTTP access. Tools
|
|
244
|
+
that collect **application** output don't exist here at all: what a run may emit
|
|
245
|
+
is your vocabulary, so you define it and inject it.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { primitiveTools, writeFileCapability, denyCrossWorkspace } from "@rarebit-one/harness-kernel"
|
|
249
|
+
|
|
250
|
+
// Your sinks, your tools — the kernel never sees them.
|
|
251
|
+
const knowledge: MyNote[] = []
|
|
252
|
+
const domainTools = (allowed?: string[]) => myEmissionTools({ knowledge }, allowed)
|
|
253
|
+
|
|
254
|
+
const engine = selectEngine("native", { domainTools })
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The same holds for the workspace-scoped **capabilities** the external engines
|
|
258
|
+
mount over MCP. The kernel ships exactly one — `writeFileCapability`, a sandbox
|
|
259
|
+
write with no product semantics — plus the guards (`denyCrossWorkspace`,
|
|
260
|
+
`resolveWithin`) so your own capabilities inherit identical security properties:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
selectEngine("claude-code", {
|
|
264
|
+
capabilityTools: (ctx) => [writeFileCapability(ctx), ...myCapabilities(ctx)],
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// codex runs out-of-process, so it needs a script it can spawn — a child
|
|
268
|
+
// process can't be handed a closure. Omitting it throws rather than serving an
|
|
269
|
+
// empty surface.
|
|
270
|
+
selectEngine("codex", { capabilityServerScript: "/app/dist/my-capability-server.js" })
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Development
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm ci
|
|
277
|
+
npm run typecheck # tsc --noEmit
|
|
278
|
+
npm run lint # eslint . (lint:fix to autofix)
|
|
279
|
+
npm run format # prettier --write . (format:check to verify)
|
|
280
|
+
npm test # vitest run
|
|
281
|
+
npm run build # tsc -p tsconfig.build.json → dist/ + .d.ts
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT © Rarebit One
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type RunEventSink } from "./events.js";
|
|
2
|
+
import { type LoopContext, type LoopRequest, type LoopResult } from "./loop.js";
|
|
3
|
+
import { type ChatModel } from "./models/chat.js";
|
|
4
|
+
import type { Provider } from "./providers/types.js";
|
|
5
|
+
import { type Tool } from "./tools/registry.js";
|
|
6
|
+
export interface RunAgentOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The chat model driving the loop. A bare {@link Provider} is accepted and
|
|
9
|
+
* adapted onto the model seam internally, so existing callers are unaffected;
|
|
10
|
+
* pass a {@link ChatModel} to drive the loop through a registry-resolved,
|
|
11
|
+
* middleware-wrapped invocation instead.
|
|
12
|
+
*/
|
|
13
|
+
provider: Provider | ChatModel;
|
|
14
|
+
system: string;
|
|
15
|
+
userPrompt: string;
|
|
16
|
+
tools: Tool[];
|
|
17
|
+
maxSteps?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Wall-clock budget for the whole loop in ms. Checked between steps; when
|
|
20
|
+
* exceeded the loop stops cleanly and returns what's accumulated (with a note)
|
|
21
|
+
* rather than throwing. Defaults to `RUNNER_RUN_TIMEOUT_MS` if set, else 10min.
|
|
22
|
+
*/
|
|
23
|
+
maxDurationMs?: number;
|
|
24
|
+
log?: (line: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Structured event sink. Absent (the default) means the loop behaves exactly
|
|
27
|
+
* as it did before events existed — `log` is unaffected either way, and the
|
|
28
|
+
* return value is unchanged. See `events.ts` for why the kernel emits this
|
|
29
|
+
* stream but never stores it.
|
|
30
|
+
*/
|
|
31
|
+
emit?: RunEventSink;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a partial budget to the concrete numbers a {@link LoopRequest} needs.
|
|
35
|
+
*
|
|
36
|
+
* One place, deliberately. A {@link Loop} is handed numbers rather than
|
|
37
|
+
* optionals, so if each caller resolved its own defaults a second loop
|
|
38
|
+
* implementation could silently run to a different ceiling than the one the
|
|
39
|
+
* kernel ships. `runAgent` and `NativeEngine` both come here.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveLoopLimits(limits?: {
|
|
42
|
+
maxSteps?: number | undefined;
|
|
43
|
+
maxDurationMs?: number | undefined;
|
|
44
|
+
}): {
|
|
45
|
+
maxSteps: number;
|
|
46
|
+
maxDurationMs: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Cap on tool output fed back into the conversation, independent of the sandbox
|
|
50
|
+
* buffer caps — an 8 MB stdout would otherwise blow the context window.
|
|
51
|
+
* Env-overridable (`RUNNER_MAX_TOOL_RESULT_BYTES`, default 64 KiB) and resolved
|
|
52
|
+
* per call so a changed env takes effect without a reload.
|
|
53
|
+
*/
|
|
54
|
+
export declare function maxToolResultBytes(): number;
|
|
55
|
+
/**
|
|
56
|
+
* Drives a provider-agnostic tool-use loop: the model converses, the runner
|
|
57
|
+
* executes any requested tools in the sandbox, feeds the results back, and
|
|
58
|
+
* repeats until the model stops calling tools (or the step budget is hit). The
|
|
59
|
+
* model orchestrates; deterministic work happens in the tools.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runAgent(options: RunAgentOptions): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* The native tool-use loop, as a {@link Loop}.
|
|
64
|
+
*
|
|
65
|
+
* Exported for `loop.ts` to wrap; applications reach it through `nativeLoop`
|
|
66
|
+
* rather than calling this directly. It is the whole of the kernel's control
|
|
67
|
+
* flow, and the only place any of it lives — `runAgent` and `nativeLoop` both
|
|
68
|
+
* come here, so the two cannot drift.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runNativeLoop(req: LoopRequest, loopCtx: LoopContext): Promise<LoopResult>;
|
|
71
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAA;AACpB,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,KAAK,EAAgB,QAAQ,EAAc,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAG5D,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,IAAI,EAAE,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;CACpB;AAYD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE;IACzC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAK9C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AA0CD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBxE;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAkK/F"}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { runEventEmitter, } from "./events.js";
|
|
2
|
+
import { nativeLoop, runWithEvents, } from "./loop.js";
|
|
3
|
+
import { asChatModel } from "./models/chat.js";
|
|
4
|
+
import { executeTool } from "./tools/registry.js";
|
|
5
|
+
import { toToolSpecs } from "./tools/metadata.js";
|
|
6
|
+
const DEFAULT_MAX_STEPS = 12;
|
|
7
|
+
const FALLBACK_MAX_DURATION_MS = 10 * 60 * 1000;
|
|
8
|
+
/** Resolve the wall-clock run budget from env, falling back to 10 minutes. */
|
|
9
|
+
function defaultMaxDurationMs() {
|
|
10
|
+
const raw = Number(process.env.RUNNER_RUN_TIMEOUT_MS);
|
|
11
|
+
return Number.isFinite(raw) && raw > 0 ? raw : FALLBACK_MAX_DURATION_MS;
|
|
12
|
+
}
|
|
13
|
+
const FALLBACK_MAX_TOOL_RESULT_BYTES = 64 * 1024;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a partial budget to the concrete numbers a {@link LoopRequest} needs.
|
|
16
|
+
*
|
|
17
|
+
* One place, deliberately. A {@link Loop} is handed numbers rather than
|
|
18
|
+
* optionals, so if each caller resolved its own defaults a second loop
|
|
19
|
+
* implementation could silently run to a different ceiling than the one the
|
|
20
|
+
* kernel ships. `runAgent` and `NativeEngine` both come here.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveLoopLimits(limits) {
|
|
23
|
+
return {
|
|
24
|
+
maxSteps: limits?.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
25
|
+
maxDurationMs: limits?.maxDurationMs ?? defaultMaxDurationMs(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cap on tool output fed back into the conversation, independent of the sandbox
|
|
30
|
+
* buffer caps — an 8 MB stdout would otherwise blow the context window.
|
|
31
|
+
* Env-overridable (`RUNNER_MAX_TOOL_RESULT_BYTES`, default 64 KiB) and resolved
|
|
32
|
+
* per call so a changed env takes effect without a reload.
|
|
33
|
+
*/
|
|
34
|
+
export function maxToolResultBytes() {
|
|
35
|
+
const raw = Number(process.env.RUNNER_MAX_TOOL_RESULT_BYTES);
|
|
36
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : FALLBACK_MAX_TOOL_RESULT_BYTES;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The reversibility fields a tool declares, copied at call time.
|
|
40
|
+
*
|
|
41
|
+
* Each is spread only when declared: absent means "makes no claim", which is a
|
|
42
|
+
* different fact from `reversible: false` ("declared irreversible") and a
|
|
43
|
+
* rollback consumer must be able to tell them apart.
|
|
44
|
+
*/
|
|
45
|
+
function undoFields(meta) {
|
|
46
|
+
return {
|
|
47
|
+
...(meta?.reversible !== undefined ? { reversible: meta.reversible } : {}),
|
|
48
|
+
...(meta?.undoToolName !== undefined ? { undoToolName: meta.undoToolName } : {}),
|
|
49
|
+
...(meta?.undoWindowSeconds !== undefined ? { undoWindowSeconds: meta.undoWindowSeconds } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A detached copy of a tool's arguments for the event stream.
|
|
54
|
+
*
|
|
55
|
+
* The sink must never be able to change what the tool receives: emitting is
|
|
56
|
+
* observation, and an observer that mutates `input` in place would make merely
|
|
57
|
+
* *attaching* a sink alter the run's side effects. `structuredClone` handles
|
|
58
|
+
* the nested case; the shallow fallback covers a value it refuses (a function,
|
|
59
|
+
* say) rather than letting a clone failure take the run down.
|
|
60
|
+
*/
|
|
61
|
+
function detach(input) {
|
|
62
|
+
try {
|
|
63
|
+
return structuredClone(input);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { ...input };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function truncate(text, max = maxToolResultBytes()) {
|
|
70
|
+
return text.length <= max
|
|
71
|
+
? text
|
|
72
|
+
: `${text.slice(0, max)}\n…[truncated ${text.length - max} bytes]`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Drives a provider-agnostic tool-use loop: the model converses, the runner
|
|
76
|
+
* executes any requested tools in the sandbox, feeds the results back, and
|
|
77
|
+
* repeats until the model stops calling tools (or the step budget is hit). The
|
|
78
|
+
* model orchestrates; deterministic work happens in the tools.
|
|
79
|
+
*/
|
|
80
|
+
export async function runAgent(options) {
|
|
81
|
+
// A `Provider` is adapted onto the model seam; a `ChatModel` passes straight
|
|
82
|
+
// through. Either way the loop talks to one interface, so a
|
|
83
|
+
// middleware-wrapped or registry-resolved model drives it identically.
|
|
84
|
+
const log = options.log ?? (() => { });
|
|
85
|
+
const result = await runWithEvents(nativeLoop, {
|
|
86
|
+
model: asChatModel(options.provider),
|
|
87
|
+
system: options.system,
|
|
88
|
+
userPrompt: options.userPrompt,
|
|
89
|
+
tools: options.tools,
|
|
90
|
+
limits: resolveLoopLimits(options),
|
|
91
|
+
}, { log, events: runEventEmitter(options.emit, log) });
|
|
92
|
+
return result.text;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The native tool-use loop, as a {@link Loop}.
|
|
96
|
+
*
|
|
97
|
+
* Exported for `loop.ts` to wrap; applications reach it through `nativeLoop`
|
|
98
|
+
* rather than calling this directly. It is the whole of the kernel's control
|
|
99
|
+
* flow, and the only place any of it lives — `runAgent` and `nativeLoop` both
|
|
100
|
+
* come here, so the two cannot drift.
|
|
101
|
+
*/
|
|
102
|
+
export async function runNativeLoop(req, loopCtx) {
|
|
103
|
+
const { model, system, userPrompt, tools } = req;
|
|
104
|
+
const { maxSteps, maxDurationMs } = req.limits;
|
|
105
|
+
const log = loopCtx.log;
|
|
106
|
+
const ctx = { log, budget: { maxDurationMs } };
|
|
107
|
+
// The emitter is the caller's: `seq` is per RUN, not per loop, so a loop that
|
|
108
|
+
// made its own would restart the numbering mid-stream.
|
|
109
|
+
const events = loopCtx.events;
|
|
110
|
+
const byName = new Map(tools.map((t) => [t.spec.name, t]));
|
|
111
|
+
const specs = toToolSpecs(tools);
|
|
112
|
+
const messages = [{ role: "user", text: userPrompt }];
|
|
113
|
+
const deadline = Date.now() + maxDurationMs;
|
|
114
|
+
let finalText = "";
|
|
115
|
+
let lastAssistantText = "";
|
|
116
|
+
let exhausted = false;
|
|
117
|
+
let timedOut = false;
|
|
118
|
+
let stepsTaken = 0;
|
|
119
|
+
// A throw propagates untouched; `runWithEvents` closes the stream around it,
|
|
120
|
+
// so every loop gets that property rather than only this one.
|
|
121
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
122
|
+
// Wall-clock budget: enforced between steps so an in-flight provider/tool
|
|
123
|
+
// call isn't interrupted mid-flight, mirroring how maxSteps stops cleanly.
|
|
124
|
+
// Because the check is only between steps, a single step can overshoot the
|
|
125
|
+
// budget by its entire duration: up to (provider timeout × maxRetries) for the
|
|
126
|
+
// converse call plus the time to execute all of that step's tool calls. So the
|
|
127
|
+
// effective ceiling is `maxDurationMs + one full step`, not `maxDurationMs`.
|
|
128
|
+
if (Date.now() >= deadline) {
|
|
129
|
+
timedOut = true;
|
|
130
|
+
log(`reached run time budget (${maxDurationMs}ms) after ${step} step(s)`);
|
|
131
|
+
events.emit({ type: "run.budget_exhausted", kind: "duration", step });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const { value: turn } = await model.invoke({ system, messages, tools: specs }, ctx);
|
|
135
|
+
stepsTaken = step + 1;
|
|
136
|
+
messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls });
|
|
137
|
+
if (turn.text)
|
|
138
|
+
lastAssistantText = turn.text;
|
|
139
|
+
events.emit({
|
|
140
|
+
type: "model.turn",
|
|
141
|
+
step,
|
|
142
|
+
text: turn.text,
|
|
143
|
+
toolCalls: turn.toolCalls.map((c) => ({ id: c.id, name: c.name })),
|
|
144
|
+
});
|
|
145
|
+
if (turn.toolCalls.length === 0) {
|
|
146
|
+
finalText = turn.text;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const results = [];
|
|
150
|
+
for (const call of turn.toolCalls) {
|
|
151
|
+
const tool = byName.get(call.name);
|
|
152
|
+
if (!tool) {
|
|
153
|
+
results.push({
|
|
154
|
+
toolCallId: call.id,
|
|
155
|
+
content: `unknown tool: ${call.name}`,
|
|
156
|
+
isError: true,
|
|
157
|
+
});
|
|
158
|
+
log(`tool ${call.name} (unknown)`);
|
|
159
|
+
events.emit({
|
|
160
|
+
type: "tool.failed",
|
|
161
|
+
step,
|
|
162
|
+
callId: call.id,
|
|
163
|
+
name: call.name,
|
|
164
|
+
reason: "unknown_tool",
|
|
165
|
+
error: `unknown tool: ${call.name}`,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Emitted BEFORE execution: a process that dies mid-tool still leaves
|
|
170
|
+
// evidence that the effect may have started, which is the one case a
|
|
171
|
+
// rollback consumer cannot afford to infer from silence.
|
|
172
|
+
events.emit({
|
|
173
|
+
type: "tool.called",
|
|
174
|
+
step,
|
|
175
|
+
callId: call.id,
|
|
176
|
+
name: call.name,
|
|
177
|
+
input: detach(call.input),
|
|
178
|
+
...undoFields(tool.meta),
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
// Uses the tool's structured executor when it has one, so a typed
|
|
182
|
+
// payload survives alongside the string the model sees. Tools without
|
|
183
|
+
// one take the original string path unchanged.
|
|
184
|
+
const output = await executeTool(tool, call.input);
|
|
185
|
+
results.push({
|
|
186
|
+
toolCallId: call.id,
|
|
187
|
+
content: truncate(output.content),
|
|
188
|
+
...(output.structured !== undefined ? { structured: output.structured } : {}),
|
|
189
|
+
});
|
|
190
|
+
log(`tool ${call.name} ok (${output.content.length} bytes)`);
|
|
191
|
+
// Reversibility is copied from the tool's own metadata at call time —
|
|
192
|
+
// the tool set can differ per run, so a consumer reading the stream
|
|
193
|
+
// later must not have to reconstruct which registry was in play.
|
|
194
|
+
events.emit({
|
|
195
|
+
type: "tool.succeeded",
|
|
196
|
+
step,
|
|
197
|
+
callId: call.id,
|
|
198
|
+
name: call.name,
|
|
199
|
+
bytes: Buffer.byteLength(output.content, "utf8"),
|
|
200
|
+
...undoFields(tool.meta),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
205
|
+
results.push({ toolCallId: call.id, content: message, isError: true });
|
|
206
|
+
log(`tool ${call.name} error: ${message}`);
|
|
207
|
+
events.emit({
|
|
208
|
+
type: "tool.failed",
|
|
209
|
+
step,
|
|
210
|
+
callId: call.id,
|
|
211
|
+
name: call.name,
|
|
212
|
+
reason: "threw",
|
|
213
|
+
error: message,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
messages.push({ role: "tool", results });
|
|
218
|
+
if (step === maxSteps - 1) {
|
|
219
|
+
exhausted = true;
|
|
220
|
+
events.emit({ type: "run.budget_exhausted", kind: "steps", step });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Ran out of time mid-loop: don't spend more of the (already-blown) budget on
|
|
224
|
+
// a summary call — just return whatever prose we accumulated, clearly noted.
|
|
225
|
+
if (!finalText && timedOut) {
|
|
226
|
+
const accumulated = lastAssistantText || "(no output before time budget exhausted)";
|
|
227
|
+
const text = `${accumulated}\n\n[run stopped: wall-clock budget of ${maxDurationMs}ms exceeded]`;
|
|
228
|
+
return { text, steps: stepsTaken, outcome: "timed_out" };
|
|
229
|
+
}
|
|
230
|
+
// Hit the step budget mid-tool-use: ask once more with no tools so the model
|
|
231
|
+
// can summarize, falling back to its last prose rather than losing the work.
|
|
232
|
+
if (!finalText && exhausted) {
|
|
233
|
+
log(`reached max steps (${maxSteps}); requesting a final summary`);
|
|
234
|
+
try {
|
|
235
|
+
const summary = (await model.invoke({ system, messages, tools: [] }, ctx)).value;
|
|
236
|
+
// Emitted like any other turn: `run.finished.text` can come from this
|
|
237
|
+
// call, and a terminal event whose text traces back to no turn in the
|
|
238
|
+
// transcript would break the stream's one promise.
|
|
239
|
+
events.emit({ type: "model.turn", step: stepsTaken, text: summary.text, toolCalls: [] });
|
|
240
|
+
finalText = summary.text || lastAssistantText;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
log(`final summary failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
|
+
finalText = lastAssistantText;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const text = finalText || "(no output)";
|
|
248
|
+
// `completed` means the model stopped calling tools of its own accord. A run
|
|
249
|
+
// that hit the step budget and was asked for a summary still reports
|
|
250
|
+
// `steps_exhausted`, because the distinction is exactly what a consumer needs
|
|
251
|
+
// to know about the trustworthiness of the answer.
|
|
252
|
+
const outcome = timedOut ? "timed_out" : exhausted ? "steps_exhausted" : "completed";
|
|
253
|
+
// `runWithEvents` builds the terminal event from exactly this result, so the
|
|
254
|
+
// return value and the stream cannot disagree — there is one source now, not
|
|
255
|
+
// two that happen to be written from the same variables.
|
|
256
|
+
return { text, steps: stepsTaken, outcome };
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|