@nylorun/harness 0.5.0-beta.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/CHANGELOG.md +32 -0
- package/LICENSE +192 -0
- package/README.md +124 -0
- package/dist/build/adapters.d.ts +11 -0
- package/dist/build/adapters.js +91 -0
- package/dist/build/agent.d.ts +19 -0
- package/dist/build/agent.js +28 -0
- package/dist/build/assemble.d.ts +9 -0
- package/dist/build/assemble.js +76 -0
- package/dist/build/bind-tool.d.ts +4 -0
- package/dist/build/bind-tool.js +15 -0
- package/dist/build/builder.d.ts +31 -0
- package/dist/build/builder.js +74 -0
- package/dist/build/helpers.d.ts +7 -0
- package/dist/build/helpers.js +5 -0
- package/dist/build/manifest.d.ts +9 -0
- package/dist/build/manifest.js +15 -0
- package/dist/build/schema.d.ts +19 -0
- package/dist/build/schema.js +86 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4 -0
- package/dist/model-normalize.d.ts +6 -0
- package/dist/model-normalize.js +211 -0
- package/dist/session/event-log.d.ts +11 -0
- package/dist/session/event-log.js +63 -0
- package/dist/session/input-queue.d.ts +29 -0
- package/dist/session/input-queue.js +78 -0
- package/dist/session/scheduler.d.ts +48 -0
- package/dist/session/scheduler.js +352 -0
- package/dist/session/session.d.ts +14 -0
- package/dist/session/session.js +55 -0
- package/dist/session/state.d.ts +10 -0
- package/dist/session/state.js +36 -0
- package/dist/session/submission-stream.d.ts +13 -0
- package/dist/session/submission-stream.js +36 -0
- package/dist/step/canonicalize.d.ts +21 -0
- package/dist/step/canonicalize.js +62 -0
- package/dist/step/compose.d.ts +4 -0
- package/dist/step/compose.js +106 -0
- package/dist/step/context-draft.d.ts +10 -0
- package/dist/step/context-draft.js +71 -0
- package/dist/step/model-configuration.d.ts +15 -0
- package/dist/step/model-configuration.js +153 -0
- package/dist/step/project.d.ts +2 -0
- package/dist/step/project.js +103 -0
- package/dist/step/resolve.d.ts +9 -0
- package/dist/step/resolve.js +16 -0
- package/dist/step/run.d.ts +27 -0
- package/dist/step/run.js +127 -0
- package/dist/step/seal.d.ts +31 -0
- package/dist/step/seal.js +108 -0
- package/dist/step/slot-assembly.d.ts +39 -0
- package/dist/step/slot-assembly.js +52 -0
- package/dist/step/step-context.d.ts +36 -0
- package/dist/step/step-context.js +255 -0
- package/dist/turn/plan-runner.d.ts +55 -0
- package/dist/turn/plan-runner.js +370 -0
- package/dist/turn/runner.d.ts +53 -0
- package/dist/turn/runner.js +128 -0
- package/dist/types/manifest.d.ts +22 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/middleware.d.ts +65 -0
- package/dist/types/middleware.js +1 -0
- package/dist/types/model.d.ts +166 -0
- package/dist/types/model.js +1 -0
- package/dist/types/session.d.ts +122 -0
- package/dist/types/session.js +1 -0
- package/dist/types/shared.d.ts +180 -0
- package/dist/types/shared.js +1 -0
- package/dist/types/tool.d.ts +101 -0
- package/dist/types/tool.js +1 -0
- package/dist/utils/digest.d.ts +1 -0
- package/dist/utils/digest.js +14 -0
- package/dist/utils/ids.d.ts +1 -0
- package/dist/utils/ids.js +3 -0
- package/dist/utils/immutable.d.ts +5 -0
- package/dist/utils/immutable.js +54 -0
- package/dist/utils/maps.d.ts +1 -0
- package/dist/utils/maps.js +37 -0
- package/dist/utils/observe.d.ts +8 -0
- package/dist/utils/observe.js +29 -0
- package/docs/loop.md +47 -0
- package/docs/model-call-projection.md +112 -0
- package/docs/reference.md +122 -0
- package/package.json +70 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { HarnessError } from "../errors.js";
|
|
2
|
+
export function assertJson(value, path = "value") {
|
|
3
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
4
|
+
return;
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
6
|
+
return;
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
value.forEach((item, index) => assertJson(item, `${path}[${index}]`));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "object") {
|
|
12
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
14
|
+
throw new HarnessError("json.invalid-data", `${path} must be plain JSON data`, {
|
|
15
|
+
details: { path },
|
|
16
|
+
});
|
|
17
|
+
for (const [key, item] of Object.entries(value)) {
|
|
18
|
+
if (item === undefined)
|
|
19
|
+
throw new HarnessError("json.invalid-data", `${path}.${key} cannot be undefined`, {
|
|
20
|
+
details: { path: `${path}.${key}` },
|
|
21
|
+
});
|
|
22
|
+
assertJson(item, `${path}.${key}`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new HarnessError("json.invalid-data", `${path} must be JSON-serializable`, {
|
|
27
|
+
details: { path },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function copyJson(value) {
|
|
31
|
+
assertJson(value);
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return Object.freeze(value.map((item) => copyJson(item)));
|
|
34
|
+
if (value !== null && typeof value === "object") {
|
|
35
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyJson(item)])));
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function copyJsonObject(value, path) {
|
|
40
|
+
assertJson(value, path);
|
|
41
|
+
if (value === null || Array.isArray(value) || typeof value !== "object")
|
|
42
|
+
throw new HarnessError("json.invalid-object", `${path} must be a JSON object`, {
|
|
43
|
+
details: { path },
|
|
44
|
+
});
|
|
45
|
+
return copyJson(value);
|
|
46
|
+
}
|
|
47
|
+
export function deepFreeze(value) {
|
|
48
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
49
|
+
Object.freeze(value);
|
|
50
|
+
for (const item of Object.values(value))
|
|
51
|
+
deepFreeze(item);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createFixedMap<K, V>(values: Iterable<readonly [K, V]>): ReadonlyMap<K, V>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class FixedReadonlyMap {
|
|
2
|
+
#values;
|
|
3
|
+
constructor(values) {
|
|
4
|
+
this.#values = new Map(values);
|
|
5
|
+
Object.freeze(this);
|
|
6
|
+
}
|
|
7
|
+
get size() {
|
|
8
|
+
return this.#values.size;
|
|
9
|
+
}
|
|
10
|
+
get(key) {
|
|
11
|
+
return this.#values.get(key);
|
|
12
|
+
}
|
|
13
|
+
has(key) {
|
|
14
|
+
return this.#values.has(key);
|
|
15
|
+
}
|
|
16
|
+
entries() {
|
|
17
|
+
return this.#values.entries();
|
|
18
|
+
}
|
|
19
|
+
keys() {
|
|
20
|
+
return this.#values.keys();
|
|
21
|
+
}
|
|
22
|
+
values() {
|
|
23
|
+
return this.#values.values();
|
|
24
|
+
}
|
|
25
|
+
forEach(callbackfn, thisArg) {
|
|
26
|
+
this.#values.forEach((value, key) => callbackfn.call(thisArg, value, key, this));
|
|
27
|
+
}
|
|
28
|
+
[Symbol.iterator]() {
|
|
29
|
+
return this.#values[Symbol.iterator]();
|
|
30
|
+
}
|
|
31
|
+
get [Symbol.toStringTag]() {
|
|
32
|
+
return "FixedReadonlyMap";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function createFixedMap(values) {
|
|
36
|
+
return new FixedReadonlyMap(values);
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ObserveEvent, Observer } from "../types/shared.js";
|
|
2
|
+
export type ObserveEmit = (event: ObserveEvent | (() => ObserveEvent)) => void;
|
|
3
|
+
export interface ObserverRegistry {
|
|
4
|
+
observe(listener: Observer): () => void;
|
|
5
|
+
emit(event: ObserveEvent | (() => ObserveEvent)): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createObserverRegistry(): ObserverRegistry;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function createObserverRegistry() {
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
return {
|
|
4
|
+
observe(listener) {
|
|
5
|
+
listeners.add(listener);
|
|
6
|
+
return () => listeners.delete(listener);
|
|
7
|
+
},
|
|
8
|
+
emit(event) {
|
|
9
|
+
if (listeners.size === 0)
|
|
10
|
+
return;
|
|
11
|
+
const resolved = typeof event === "function" ? event() : event;
|
|
12
|
+
const snapshot = Object.freeze({ ...resolved });
|
|
13
|
+
for (const listener of [...listeners]) {
|
|
14
|
+
try {
|
|
15
|
+
const result = listener(snapshot);
|
|
16
|
+
if (result && typeof result.then === "function") {
|
|
17
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Observation is deliberately fail-open.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
clear() {
|
|
26
|
+
listeners.clear();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/docs/loop.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# How the harness loop is laid out
|
|
2
|
+
|
|
3
|
+
The public seam is `Agent` → `BuiltAgent.run` → `Session`. Everything below that is the in-memory loop. Open these four files first:
|
|
4
|
+
|
|
5
|
+
| Layer | First file | Owns |
|
|
6
|
+
| ------- | -------------------------- | ------------------------------------------------------------------------------- |
|
|
7
|
+
| Build | `src/build/builder.ts` | Seal one model, adapters, and middleware into a `BuiltAgent` |
|
|
8
|
+
| Session | `src/session/scheduler.ts` | One session: queue, observe, stream, stop; one active turn at a time |
|
|
9
|
+
| Turn | `src/turn/runner.ts` | One user turn: a model step, then the tool plan |
|
|
10
|
+
| Step | `src/step/run.ts` | One model call: fresh configuration/context assembly, middleware, project, seal |
|
|
11
|
+
|
|
12
|
+
`BuiltAgent.run` constructs a `LiveSession`. The scheduler drives `TurnRunner`, which calls `runStep` and then `ToolPlanRunner`. How a committed step becomes the portable `ModelCall` is in [model-call-projection.md](./model-call-projection.md).
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
AgentBuilder.build
|
|
16
|
+
→ BuiltAgent.run
|
|
17
|
+
→ LiveSession / SessionScheduler
|
|
18
|
+
→ TurnRunner
|
|
19
|
+
→ runStep (middleware onion, model, seal)
|
|
20
|
+
→ ToolPlanRunner (interaction, preflight, execute)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tool execution
|
|
24
|
+
|
|
25
|
+
Declared interactions and preflights remain model-ordered. Once they pass, sibling `execute()` calls fan out in parallel. `.with(adapter)` is unbounded; `.with(adapter, { maxConcurrentCalls })` adds a FIFO limit shared by every Session from the same `BuiltAgent`. Results are committed in model order even when adapter completion events arrive in a different order. An adapter that returns several interaction requests has them presented and resumed in model order.
|
|
26
|
+
|
|
27
|
+
## Glossary
|
|
28
|
+
|
|
29
|
+
**Configuration.** Middleware-owned, named slots for instructions, visible tools, and the selected model directive. They are assembled only for the current model call, in canonical order, and are digested for observation. Harness does not retain a baseline or apply drift policy.
|
|
30
|
+
|
|
31
|
+
**Context.** Per-step runtime context, separate from the configuration digest. `request.context.set` writes it; `run({ context })` is freshly seeded on each call. Do not confuse it with `StepContext` in `src/step/step-context.ts`, which is the mutable lease middleware sees for one step.
|
|
32
|
+
|
|
33
|
+
**Candidate.** The model’s successful return: ordered `text` / `reasoning` / `tool-call` blocks, plus optional `finishReason`, `usage`, and `evidence`. A string return becomes one text block. Session `final` joins text blocks only.
|
|
34
|
+
|
|
35
|
+
**Seal.** After the onion returns, `sealStep` turns the reviewed candidate into a tool plan (or a final / tripwire). Retained tool-call ids, names, and arguments cannot change.
|
|
36
|
+
|
|
37
|
+
**Tripwire.** A structured stop for this step or session. Middleware may return `request.tripwire(...)` instead of calling `next()`. One session’s tripwire does not stop sibling sessions.
|
|
38
|
+
|
|
39
|
+
**Arrivals.** New input claimed for this step (user message, interrupt, or an interaction reply). They are already on the transcript before invoke, so `projectModelCall` does not append them again.
|
|
40
|
+
|
|
41
|
+
## Where to add a test
|
|
42
|
+
|
|
43
|
+
Most tests go through `Agent` from `src/index.ts`. Use that unless you are testing a private helper.
|
|
44
|
+
|
|
45
|
+
**Public seam** — behavior through `Agent` / `Session`: `session-loop`, `pipeline`, `middleware`, `prompt-prefix`, `context-ledger`, `candidate`, `sealing`, `interaction`, `observation`, `lifecycle`, `conversation-stream`, `isolation`, `schema`, `binding`, `errors`, `public-api`.
|
|
46
|
+
|
|
47
|
+
**Internal seam** — a private module: `input-queue`, `event-log`, `turn-runner`, `tool-plan-runner`, `project-call`, `utils`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# How a step becomes a ModelCall
|
|
2
|
+
|
|
3
|
+
The four-layer loop (build → session → turn → step) is in [loop.md](./loop.md). This page describes the fresh, per-step model assembly.
|
|
4
|
+
|
|
5
|
+
Each model call is assembled from the current middleware execution, transcript, step deltas, Agent model directive, and host `run({ context })`. Harness owns no cross-step configuration or context store.
|
|
6
|
+
|
|
7
|
+
## What the model sees
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
ModelCall
|
|
11
|
+
├── tools[] provider contracts from configuration.tools.set
|
|
12
|
+
└── prompt[]
|
|
13
|
+
├── instructions configuration.instructions.set (system)
|
|
14
|
+
├── message/tool-result transcript
|
|
15
|
+
└── context current-step context envelope (user role, if non-empty)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Piece | Source | ModelCall projection |
|
|
19
|
+
| ------------ | ---------------------------------------------------- | ----------------------------------------------- |
|
|
20
|
+
| Tools | `request.configuration.tools.set` | `tools` with name, description, and JSON Schema |
|
|
21
|
+
| Instructions | `request.configuration.instructions.set` | one system `instructions` item |
|
|
22
|
+
| Model | Agent directive or `configuration.model` declaration | optional `model` field |
|
|
23
|
+
| Transcript | Session history | message and tool-result prompt items |
|
|
24
|
+
| Context | `run({ context })` plus `request.context.set` | trailing runtime-context envelope |
|
|
25
|
+
|
|
26
|
+
`turnId` and `stepId` are available to middleware on `StepRequest` and remain on `ModelRequest`; they are intentionally not provider input. `arrivals` and `toolResults` are also on `ModelRequest` but omitted from the prompt because they are already committed onto the transcript.
|
|
27
|
+
|
|
28
|
+
## Assembly and invocation boundary
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
committed transcript + current middleware declarations
|
|
32
|
+
→ ModelConfigurationDraft / ContextDraft
|
|
33
|
+
→ immutable ModelRequest
|
|
34
|
+
→ immutable ModelCall
|
|
35
|
+
→ model.requested event
|
|
36
|
+
→ ModelAdapter.invoke(call, { request, signal })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`configuration` and `context` use named, middleware-owned slots only to ensure same-step replacement and deterministic ordering. A later `set` from the same middleware and slot replaces its earlier declaration for that call. Every draft is discarded after invocation: a value is absent on the next model step unless middleware declares it again.
|
|
40
|
+
|
|
41
|
+
The Agent directive and `run({ context })` are re-seeded on each step. `configuration.model.clear()` suppresses the Agent directive for that call only. Applications that want durable or turn-scoped state keep it outside Harness and re-declare the portion they want model-visible, using `sessionId`, `turnId`, and `stepId`.
|
|
42
|
+
|
|
43
|
+
Canonical order is explicit `order`, middleware registration order, slot name, and declaration order. The configuration snapshot has logical, model, and combined request digests; context has its own digest. These describe logical content only. Harness keeps no baseline, status, strict policy, or drift comparison.
|
|
44
|
+
|
|
45
|
+
Immediately before adapter invocation, Harness emits exactly one `model.requested` event. `attributes.call` is the exact deeply immutable `ModelCall` passed to the adapter. `attributes.configuration` records ordered sources, tool routes, and digests; `attributes.context` records the attributed runtime context. The event is a logical adapter-input boundary, not a provider-wire trace. Observers such as Studio can compare these events across calls and apply audit or drift policy.
|
|
46
|
+
|
|
47
|
+
## Verified two-step echo redraw
|
|
48
|
+
|
|
49
|
+
The executable source for this walkthrough is [`project-call.test.ts`](../test/project-call.test.ts), test **“runs the documented two-step echo redraw example.”** It runs under `npm run check`.
|
|
50
|
+
|
|
51
|
+
The Agent has `{ id: "haiku" }`, `run({ id: "durable-session", context: { user: "ada" } })`, and an `echo` tool routed to `local`. Its middleware declares everything needed by each model call:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
.use("echo", async (step, next) => {
|
|
55
|
+
step.configuration.instructions.set("echo-policy", ["Echo the user text."]);
|
|
56
|
+
step.configuration.tools.set("echo-tools", [echo]);
|
|
57
|
+
step.context.set("example", [{ type: "example", value: { step: step.stepNumber } }]);
|
|
58
|
+
return next();
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This unconditional declaration is the pattern for a tool that remains available throughout a tool loop. The following conditional form is also valid, but intentionally withdraws `echo` from the next model call:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
if (step.stepNumber === 1) step.configuration.tools.set("echo-tools", [echo]);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The first model call returns `echo({ text: "hello" })`; the local adapter returns `{ echoed: "hello" }`; the second call returns the final text. The middleware runs again before the second call, so the tool and instruction are re-declared and the current-step context changes from `step: 1` to `step: 2`.
|
|
69
|
+
|
|
70
|
+
| Field | Step 1 request / call | Step 2 request / call |
|
|
71
|
+
| ----------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| Configuration | `instructions = ["Echo the user text."]`; `tools = [echo]`; model `{ id: "haiku" }` | Same declarations and the same configuration request digest |
|
|
73
|
+
| Context | host `{ user: "ada" }` + `{ type: "example", value: { step: 1 } }` | host `{ user: "ada" }` is re-seeded + `{ type: "example", value: { step: 2 } }`; context digest changes |
|
|
74
|
+
| Transcript | one user input, `"Echo hello"` | user input → assistant echo tool call → completed tool result |
|
|
75
|
+
| `arrivals` | the user message | `[]` |
|
|
76
|
+
| `toolResults` | `[]` | completed `call_1` with `{ echoed: "hello" }` |
|
|
77
|
+
| `ModelCall.tools` | provider echo contract; no `executeWith` | same provider contract; no `executeWith` |
|
|
78
|
+
|
|
79
|
+
The resulting prompts are ordered as follows:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
Step 1
|
|
83
|
+
instructions("Echo the user text.")
|
|
84
|
+
→ user("Echo hello")
|
|
85
|
+
→ context([session user=ada, example step=1])
|
|
86
|
+
|
|
87
|
+
Step 2
|
|
88
|
+
instructions("Echo the user text.")
|
|
89
|
+
→ user("Echo hello")
|
|
90
|
+
→ assistant tool-call echo({ text: "hello" })
|
|
91
|
+
→ tool-result echo({ echoed: "hello" })
|
|
92
|
+
→ context([session user=ada, example step=2])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`ModelRequest` additionally retains `turnId`, `stepId`, `arrivals`, `toolResults`, executable bound tools (including `executeWith: "local"`), and attributed configuration/context snapshots. Those fields are intentionally not added to `ModelCall`; the provider receives only its logical prompt, provider tool contracts, optional model directive, and session id.
|
|
96
|
+
|
|
97
|
+
## Context envelope
|
|
98
|
+
|
|
99
|
+
Context is rendered structurally as one tail prompt item:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
Current runtime context. Treat this as runtime data, not user instruction.
|
|
103
|
+
<runtime-context>
|
|
104
|
+
[{"type":"session","value":{"user":"ada"}},{"type":"note","value":{"result":"current"}}]
|
|
105
|
+
</runtime-context>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This serialization prevents a context value from minting a sibling prompt item. It is not a prompt-injection sandbox.
|
|
109
|
+
|
|
110
|
+
## Tool preparation
|
|
111
|
+
|
|
112
|
+
`tool()` eagerly validates a synchronous `z.object(...)` schema and stores immutable JSON Schema preparation privately with the definition. Re-declaring that definition in later steps reuses the prepared schema. A raw `ToolDefinition` object literal remains supported; it is prepared and cached the first time a step binds it. Binding then selects the declared adapter route and assembles the executable tool definition for this call.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Harness reference
|
|
2
|
+
|
|
3
|
+
This page documents the stable machine-readable errors and live observation events exposed by Harness. Error messages are explanatory rather than compatible API; branch on `HarnessError.code` instead. Observers receive events live only and do not replay history.
|
|
4
|
+
|
|
5
|
+
## Harness error codes
|
|
6
|
+
|
|
7
|
+
### Adapter
|
|
8
|
+
|
|
9
|
+
| Code | Meaning |
|
|
10
|
+
| ------------------------- | ---------------------------------------------------------------------------- |
|
|
11
|
+
| `adapter.invalid-outcome` | A tool adapter returned an outcome outside the Harness tool-result contract. |
|
|
12
|
+
| `adapter.not-registered` | A tool's `executeWith` adapter id is not registered on the built Agent. |
|
|
13
|
+
|
|
14
|
+
`AgentBuilder.with(adapter, { maxConcurrentCalls })` validates the optional limit at build time. It must be a positive safe integer; otherwise the build fails with the `adapter.invalid-max-concurrent-calls` diagnostic. An `adapter.started` observation means the call acquired its adapter permit and entered `execute()`; queued calls have no start event.
|
|
15
|
+
|
|
16
|
+
### Agent
|
|
17
|
+
|
|
18
|
+
| Code | Meaning |
|
|
19
|
+
| ------------------------ | ---------------------------------------------------------------------- |
|
|
20
|
+
| `agent.build-failed` | Agent construction produced one or more build diagnostics. |
|
|
21
|
+
| `agent.lifecycle-sealed` | A mutating builder operation was attempted after the Agent was sealed. |
|
|
22
|
+
|
|
23
|
+
### Context
|
|
24
|
+
|
|
25
|
+
| Code | Meaning |
|
|
26
|
+
| --------------------------- | ----------------------------------------------------------------------------- |
|
|
27
|
+
| `context.invalid-item` | Runtime context items are not an array of valid item objects. |
|
|
28
|
+
| `context.invalid-item-type` | A runtime context item `type` does not match the supported identifier format. |
|
|
29
|
+
| `context.invalid-order` | A context declaration's `order` is not a finite number. |
|
|
30
|
+
| `context.invalid-reason` | A context declaration's `reason` is not a non-empty string. |
|
|
31
|
+
| `context.invalid-slot` | A context declaration's slot is not a non-empty string. |
|
|
32
|
+
|
|
33
|
+
### Interaction
|
|
34
|
+
|
|
35
|
+
| Code | Meaning |
|
|
36
|
+
| --------------------------------- | --------------------------------------------------------------------- |
|
|
37
|
+
| `interaction.invalid` | An interaction request is not a valid approval or response shape. |
|
|
38
|
+
| `interaction.missing-resume` | A pending tool plan did not receive its correlated interaction input. |
|
|
39
|
+
| `interaction.uncorrelated-resume` | An interaction input attempted to resume a plan other than its own. |
|
|
40
|
+
|
|
41
|
+
### JSON
|
|
42
|
+
|
|
43
|
+
| Code | Meaning |
|
|
44
|
+
| --------------------- | --------------------------------------------------------- |
|
|
45
|
+
| `json.invalid-data` | A value that must be JSON-safe contains unsupported data. |
|
|
46
|
+
| `json.invalid-object` | A value that must be a JSON object is not one. |
|
|
47
|
+
|
|
48
|
+
### Middleware
|
|
49
|
+
|
|
50
|
+
| Code | Meaning |
|
|
51
|
+
| ------------------------------------- | -------------------------------------------------------------------------- |
|
|
52
|
+
| `middleware.next-after-return` | Middleware called `next()` after its handler had returned. |
|
|
53
|
+
| `middleware.next-called-twice` | Middleware called `next()` more than once. |
|
|
54
|
+
| `middleware.request-mutators-revoked` | Middleware attempted to mutate its request after its mutation lease ended. |
|
|
55
|
+
|
|
56
|
+
### Model
|
|
57
|
+
|
|
58
|
+
| Code | Meaning |
|
|
59
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
60
|
+
| `model.candidate-missing` | The model path completed without a candidate to mint. |
|
|
61
|
+
| `model.invalid-candidate` | The model return value does not satisfy the normalized candidate contract. |
|
|
62
|
+
| `model.invalid-directive` | A model directive is malformed or contains unsupported JSON data. |
|
|
63
|
+
|
|
64
|
+
### Configuration
|
|
65
|
+
|
|
66
|
+
| Code | Meaning |
|
|
67
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
68
|
+
| `configuration.duplicate-tool-name` | More than one visible tool has the same name for a model call. |
|
|
69
|
+
| `configuration.invalid` | Configuration assembly failed without a more specific Harness code. |
|
|
70
|
+
| `configuration.invalid-instructions` | A configuration instruction declaration contains a non-string item. |
|
|
71
|
+
| `configuration.invalid-order` | A configuration declaration's `order` is not a finite number. |
|
|
72
|
+
| `configuration.invalid-reason` | A configuration declaration's `reason` is not a non-empty string. |
|
|
73
|
+
| `configuration.invalid-slot` | A configuration declaration's slot is not a non-empty string. |
|
|
74
|
+
| `configuration.invalid-tools` | A configuration tool declaration is not an array. |
|
|
75
|
+
| `configuration.model-selection-conflict` | `model.select()` conflicts with an already selected model; use `replace()` to override it. |
|
|
76
|
+
|
|
77
|
+
### Response
|
|
78
|
+
|
|
79
|
+
| Code | Meaning |
|
|
80
|
+
| ------------------------------ | -------------------------------------------------------------------------------------- |
|
|
81
|
+
| `response.invalid-replacement` | Middleware replacement attempted an invalid candidate or changed a retained tool call. |
|
|
82
|
+
|
|
83
|
+
### Session
|
|
84
|
+
|
|
85
|
+
| Code | Meaning |
|
|
86
|
+
| ---------------------- | ------------------------------------------------------- |
|
|
87
|
+
| `session.stale-result` | A stopped or superseded Session result was quarantined. |
|
|
88
|
+
|
|
89
|
+
### Tool
|
|
90
|
+
|
|
91
|
+
| Code | Meaning |
|
|
92
|
+
| -------------------------- | ---------------------------------------------------------------------------- |
|
|
93
|
+
| `tool.invalid-arguments` | Tool-call arguments failed the declared tool schema. |
|
|
94
|
+
| `tool.invalid-name` | A tool name is empty. |
|
|
95
|
+
| `tool.invalid-schema` | A tool schema is unsupported or does not have an object root. |
|
|
96
|
+
| `tool.invalid-tool-result` | A normalized tool result has an invalid denial, failure, or completed shape. |
|
|
97
|
+
|
|
98
|
+
## Observe events
|
|
99
|
+
|
|
100
|
+
Every `ObserveEvent` has a distinct `type`. The TypeScript union groups some variants with the same field shape, but each name in the table below is a separate event. Fields marked optional may be absent.
|
|
101
|
+
|
|
102
|
+
| Event | Required identifiers and payload |
|
|
103
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `session.stopped` | `reason` |
|
|
105
|
+
| `input.received` | `inputId`, `kind` |
|
|
106
|
+
| `input.queued` | `inputId` |
|
|
107
|
+
| `input.rejected` | `inputId`, `reason` |
|
|
108
|
+
| `input.cancelled` | `inputId`, `reason` |
|
|
109
|
+
| `model.requested` | `turnId`, `stepId`; optional `inputId`, `requestedModelId`; `attributes.call`, `attributes.configuration`, and `attributes.context` |
|
|
110
|
+
| `model.completed` | `turnId`, `stepId`; optional `inputId`, `requestedModelId`; `attributes` is the `ModelCandidate` |
|
|
111
|
+
| `step.started` | `turnId`, `stepId`, `turnNumber`, `stepNumber`; optional `inputId`; `attributes` contains session metadata, arrivals, tool results, and transcript |
|
|
112
|
+
| `middleware.entered` | `turnId`, `stepId`, `middlewareId`; optional `inputId` |
|
|
113
|
+
| `middleware.completed` | `turnId`, `stepId`, `middlewareId`; optional `inputId` |
|
|
114
|
+
| `middleware.lease-violation` | `turnId`, `stepId`, `middlewareId`, `reason`; optional `inputId` |
|
|
115
|
+
| `tool.sealed` | `turnId`, `stepId`; optional `inputId`; `attributes.executable` and `attributes.immediate` tool results |
|
|
116
|
+
| `adapter.preflight.started` | `turnId`, `stepId`, `adapterId`, `toolName`, `callId`; optional `inputId`, `invocationId`; `attributes.args` |
|
|
117
|
+
| `adapter.started` | `turnId`, `stepId`, `adapterId`, `toolName`, `callId`; optional `inputId`, `invocationId`; `attributes.args` |
|
|
118
|
+
| `adapter.preflight.completed` | `turnId`, `stepId`, `adapterId`, `toolName`, `callId`, `outcome`; optional `inputId`, `code`, `attributes` tool result |
|
|
119
|
+
| `adapter.completed` | `turnId`, `stepId`, `adapterId`, `toolName`, `callId`, `outcome`; optional `inputId`, `code`, `attributes` tool result |
|
|
120
|
+
| `turn.completed` | `turnId`, `stepId`; optional `inputId`; `attributes.output` |
|
|
121
|
+
| `interaction.required` | `turnId`, `stepId`, `interactionId`, `kind`; optional `inputId`, `callId`, `toolName`, `phase`; `attributes.prompt` and optional metadata |
|
|
122
|
+
| `tripwire` | `turnId`, `code`, `scope`; optional `stepId`, `inputId`; `attributes.message` |
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nylorun/harness",
|
|
3
|
+
"version": "0.5.0-beta.1",
|
|
4
|
+
"description": "A provider-neutral, in-memory agent loop for TypeScript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nylorun/harness.git",
|
|
10
|
+
"directory": "harness"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/nylorun/harness/tree/main/harness#readme",
|
|
13
|
+
"bugs": "https://github.com/nylorun/harness/issues",
|
|
14
|
+
"author": "Nylo",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"agent",
|
|
17
|
+
"agent-loop",
|
|
18
|
+
"ai",
|
|
19
|
+
"llm",
|
|
20
|
+
"tool-calling",
|
|
21
|
+
"typescript",
|
|
22
|
+
"zod"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"docs",
|
|
35
|
+
"README.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": "^22.14.0 || ^24.0.0 || >=26.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"clean": "node -e \"import('node:fs').then(({rmSync})=>rmSync('dist',{recursive:true,force:true}))\"",
|
|
49
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"format": "prettier --write .",
|
|
52
|
+
"format:check": "prettier --check .",
|
|
53
|
+
"test:types": "tsc -p test/types/tsconfig.json",
|
|
54
|
+
"check": "npm run format:check && npm run build && npm run test:types && npm test && node scripts/check-package.mjs",
|
|
55
|
+
"prepack": "npm run build"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^22.18.0",
|
|
59
|
+
"prettier": "^3.9.6",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"vitest": "^3.2.4",
|
|
62
|
+
"zod": "^4.1.12"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"zod": "^4.1.12"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@noble/hashes": "^2.3.0"
|
|
69
|
+
}
|
|
70
|
+
}
|