@nylorun/harness 0.5.0-beta.1 → 0.8.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 +63 -0
- package/README.md +27 -108
- package/dist/build/agent.d.ts +6 -7
- package/dist/build/agent.js +16 -7
- package/dist/build/assemble.d.ts +5 -6
- package/dist/build/assemble.js +10 -43
- package/dist/build/bind-tool.d.ts +2 -3
- package/dist/build/bind-tool.js +6 -4
- package/dist/build/builder.d.ts +30 -15
- package/dist/build/builder.js +89 -38
- package/dist/build/helpers.d.ts +2 -3
- package/dist/build/helpers.js +0 -1
- package/dist/build/manifest.d.ts +2 -4
- package/dist/build/manifest.js +2 -8
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +2 -2
- package/dist/session/capability-state.d.ts +14 -0
- package/dist/session/capability-state.js +67 -0
- package/dist/session/input-queue.d.ts +11 -2
- package/dist/session/input-queue.js +5 -1
- package/dist/session/record.d.ts +11 -0
- package/dist/session/record.js +26 -0
- package/dist/session/scheduler.d.ts +17 -3
- package/dist/session/scheduler.js +181 -48
- package/dist/session/seed.d.ts +10 -0
- package/dist/session/seed.js +219 -0
- package/dist/session/session.d.ts +3 -2
- package/dist/session/session.js +15 -3
- package/dist/session/state.d.ts +8 -3
- package/dist/session/state.js +16 -4
- package/dist/session/submission-stream.d.ts +2 -0
- package/dist/session/submission-stream.js +11 -1
- package/dist/step/model-configuration.d.ts +1 -3
- package/dist/step/model-configuration.js +2 -4
- package/dist/step/project.js +4 -2
- package/dist/step/run.d.ts +6 -1
- package/dist/step/run.js +35 -13
- package/dist/step/seal.d.ts +6 -2
- package/dist/step/seal.js +4 -3
- package/dist/step/step-context.d.ts +5 -2
- package/dist/step/step-context.js +19 -11
- package/dist/turn/plan-runner.d.ts +28 -13
- package/dist/turn/plan-runner.js +165 -182
- package/dist/turn/runner.d.ts +18 -4
- package/dist/turn/runner.js +73 -45
- package/dist/types/manifest.d.ts +2 -6
- package/dist/types/middleware.d.ts +25 -4
- package/dist/types/model.d.ts +3 -2
- package/dist/types/session.d.ts +86 -2
- package/dist/types/shared.d.ts +65 -9
- package/dist/types/tool.d.ts +37 -39
- package/dist/utils/immutable.js +16 -2
- package/package.json +1 -2
- package/dist/build/adapters.d.ts +0 -11
- package/dist/build/adapters.js +0 -91
- package/docs/loop.md +0 -47
- package/docs/model-call-projection.md +0 -112
- package/docs/reference.md +0 -122
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,66 @@ All notable changes to `@nylorun/harness` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The project follows [Semantic Versioning](https://semver.org/). Before 1.0, the public API is experimental: breaking changes may occur in minor releases, while patch releases are reserved for compatible fixes.
|
|
6
6
|
|
|
7
|
+
## [0.8.0-beta.1] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Breaking changes
|
|
10
|
+
|
|
11
|
+
- `Agent()` now takes identity options instead of a model adapter. Migrate
|
|
12
|
+
`Agent(adapter).use(...).build()` to
|
|
13
|
+
`Agent({ id, name, instructions }).use(...).with(adapter).build()`.
|
|
14
|
+
- `.build()` exists only on `BoundAgentBuilder`, the type returned by a single `.with(onModelCall)`.
|
|
15
|
+
`AgentBuilder` has `.use()` and `.with()` only.
|
|
16
|
+
- `AgentManifest` now includes `id` and `name`. Built agents expose the same fields as `agent.id`
|
|
17
|
+
and `agent.name`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Optional constructor `instructions` compile as reserved `agent` middleware. A string is
|
|
22
|
+
normalized to one instruction. Capability-specific instructions still go through `.use()`.
|
|
23
|
+
|
|
24
|
+
## [0.7.0-beta.1] - 2026-08-30
|
|
25
|
+
|
|
26
|
+
### Breaking changes
|
|
27
|
+
|
|
28
|
+
- Removed the bind-time directive argument: migrate `Agent(adapter, directive)` to
|
|
29
|
+
`Agent(adapter).use({ id: "model", model: directive })`. `AgentManifest.model` is removed.
|
|
30
|
+
- Added `CapabilityDeclaration` support to `.use()`. A declaration owns one capability id, static
|
|
31
|
+
tool/instruction/model contributions, and optional inline middleware.
|
|
32
|
+
- `ToolExecutionContext` now includes session, turn, step, and call identities.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Declarations may own typed lazy session state through `CapabilityState`. State is shared only by
|
|
37
|
+
that declaration's middleware and tools, is cold after seed recovery, and is disposed on
|
|
38
|
+
`session.stop()` or recording failure.
|
|
39
|
+
- Added `capability.state.dispose.failed` observations for best-effort disposal failures.
|
|
40
|
+
- Added `capability.state.undeclared`, raised when session state is requested for a capability that
|
|
41
|
+
declared none.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- Added concise package guidance for direct agent composition and the capability/service/host/core model.
|
|
46
|
+
|
|
47
|
+
## [0.6.0-beta.1] - 2026-08-29
|
|
48
|
+
|
|
49
|
+
### Breaking changes since 0.5.0-beta.1
|
|
50
|
+
|
|
51
|
+
- Removed tool adapters, `.with()`, `executeWith`, preflight, adapter concurrency controls, custom scheduling, adapter manifest fields, and adapter observations/errors.
|
|
52
|
+
- Tools now own their implementation through `execute(args, context)`. Harness centrally executes eligible siblings concurrently and commits normalized results in model-call order.
|
|
53
|
+
- Tool results are explicit `completed`, `denied`, and `failed` discriminated unions. Tool and model implementations may return `deferred` for a runtime handoff.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- Structural `SessionSeed` import through `agent.run({ seed })` and no-input `session.continue()`. Core validates typed JSON but deliberately leaves historical semantics and provider protocol validation to the host and model adapter.
|
|
58
|
+
- Optional awaited `SessionRecorder`, immutable full-state `SessionRecord` values, monotonic revisions, and effect barriers around input, model requests, candidates, tool results, waiting, final, and stop transitions.
|
|
59
|
+
- JSON-safe active model/tool/interaction records with stable invocation identities, tool ownership provenance, settled/deferred state, and opaque handoff tokens.
|
|
60
|
+
- `model.deferred`, `tool.started`, `tool.completed`, `tool.deferred`, `session.seeded`, `session.continued`, and `session.record.failed` observations.
|
|
61
|
+
|
|
62
|
+
### Reliability
|
|
63
|
+
|
|
64
|
+
- Recorder failure now fences later model/tool effects, quarantines late results, stops queued work, preserves the last successfully recorded revision, and exposes `session.record-failed` with the storage error as its cause.
|
|
65
|
+
- Deferred sibling batches settle fully without committing a partial model-facing `tool-results` entry.
|
|
66
|
+
|
|
7
67
|
## [0.5.0-beta.1] - 2026-08-28
|
|
8
68
|
|
|
9
69
|
### Breaking changes since 0.4.0-rc.1
|
|
@@ -30,3 +90,6 @@ The project follows [Semantic Versioning](https://semver.org/). Before 1.0, the
|
|
|
30
90
|
- Observe attributes are materialized only when a listener is registered. Transcript snapshots on `step.started` share the already-frozen step transcript rather than deep-copying it on every step.
|
|
31
91
|
|
|
32
92
|
[0.5.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
|
|
93
|
+
[0.6.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
|
|
94
|
+
[0.7.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
|
|
95
|
+
[0.8.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
|
package/README.md
CHANGED
|
@@ -1,124 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@nylorun/harness`
|
|
2
2
|
|
|
3
|
-
`@nylorun/harness` is a small, provider-neutral
|
|
3
|
+
`@nylorun/harness` is a small, provider-neutral TypeScript loop for model calls and tools. Compose
|
|
4
|
+
an agent from a model adapter and named capabilities; keep provider clients, stores, HTTP, and
|
|
5
|
+
credentials in ordinary application code.
|
|
4
6
|
|
|
5
|
-
> **Experimental beta.** Install
|
|
7
|
+
> **Experimental beta.** Install with `npm install @nylorun/harness@beta`.
|
|
6
8
|
|
|
7
9
|
```ts
|
|
8
|
-
import { Agent,
|
|
10
|
+
import { Agent, model, tool } from "@nylorun/harness";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
|
|
11
|
-
const
|
|
12
|
-
id: "local",
|
|
13
|
-
async execute(call) {
|
|
14
|
-
return { kind: "completed" as const, output: { echoed: call.args } };
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
13
|
+
const adapter = model(async (call) => provider.complete(call));
|
|
18
14
|
const echo = tool({
|
|
19
15
|
name: "echo",
|
|
20
|
-
description: "Echo
|
|
16
|
+
description: "Echo text",
|
|
21
17
|
parameters: z.object({ text: z.string() }),
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const example = model(async (call, { request }) => {
|
|
26
|
-
return request.toolResults.length === 0
|
|
27
|
-
? {
|
|
28
|
-
output: [{ type: "tool-call", id: "call_1", name: "echo", args: { text: "hello" } }],
|
|
29
|
-
}
|
|
30
|
-
: `Completed with ${JSON.stringify(request.toolResults[0]?.output)}`;
|
|
18
|
+
execute: async ({ text }) => ({ kind: "completed", output: text }),
|
|
31
19
|
});
|
|
32
20
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
if (request.turnNumber > 8) {
|
|
42
|
-
return request.tripwire({ code: "turn.limit", message: "Limit reached" });
|
|
43
|
-
}
|
|
44
|
-
return next();
|
|
45
|
-
})
|
|
46
|
-
.build()
|
|
47
|
-
.run();
|
|
48
|
-
session.observe((event) => {
|
|
49
|
-
if (event.type === "model.requested") console.log("model", event.attributes);
|
|
50
|
-
});
|
|
51
|
-
session.input("Echo hello");
|
|
52
|
-
for await (const event of session.stream()) {
|
|
53
|
-
if (event.type === "final") {
|
|
54
|
-
console.log(event.output);
|
|
55
|
-
await session.stop();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`Agent(model, directive?)` returns a builder. The first argument is the model callback `(call, { request, signal })`; the optional directive (`{ id?, controls?, config? }`) is seeded into every model step and can be replaced or cleared for that call. `.with(adapter, { maxConcurrentCalls? })` registers an adapter by `adapter.id` (required for tool dispatch via `executeWith`); omitted `maxConcurrentCalls` dispatches sibling `execute()` calls in parallel without a Harness cap, while a positive safe integer is a FIFO limit shared by all Sessions from that `BuiltAgent`. Adapter authors remain responsible for concurrent safety when no limit is supplied. Call order has no onion effect. `.use(middleware)` or `.use(id, middleware)` appends middleware (later is more inward); omitted ids become `middleware-1`, `middleware-2`, …. `.build()` validates ids and seals a `BuiltAgent`, or throws `AgentBuildError`. `BuiltAgent.run(options?)` creates an in-memory Session. `session.input("hello")` or `session.input({ text, metadata? })` queues a user message and returns a completion handle; while a turn is running or waiting, that message stays queued until the current turn finals, then a new turn starts. Approve and respond stay on `session.input({ kind: "approve" | "respond", ... })` — those are interaction replies, not messages. `session.interrupt("...")` queues barge-in text: if the current turn still has another step, the interrupt is claimed as that step’s arrivals (same turn, no abort of the in-flight model or tool work); if the turn already finalized, it starts a new turn like ordinary input. `session.stream()` yields the conversation (`input`, per-step `candidate`, then settlement events) — not observe events or tool results. `session.observe(listener)` adds a live, fail-open observer and returns an idempotent unsubscribe function; it does not replay earlier events. `session.stop()` ends that Session only.
|
|
21
|
+
const agent = Agent({
|
|
22
|
+
id: "echo",
|
|
23
|
+
name: "Echo",
|
|
24
|
+
instructions: "Use echo when asked.",
|
|
25
|
+
})
|
|
26
|
+
.use({ id: "echo", tools: [echo] })
|
|
27
|
+
.with(adapter)
|
|
28
|
+
.build();
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Middleware is the Model lifecycle:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
type StepMiddleware = (
|
|
68
|
-
request: StepRequest,
|
|
69
|
-
next: () => Promise<StepResponse>,
|
|
70
|
-
) => Promise<StepResponse>;
|
|
30
|
+
const result = await agent.run().input("Echo hello").completed;
|
|
71
31
|
```
|
|
72
32
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Configuration and context are fresh assemblies for each model call. A repeated `set` for the same middleware/slot replaces that middleware’s earlier declaration in that step; nothing is retained for the next step. A middleware can declare only its own slot. Cross-cutting policy belongs in the tool-owning middleware or the host's final assembly decision; `deny()` remains available to reject a specific requested call after inspecting its arguments. Effective instruction and visible-tool order is canonical: explicit `order`, middleware registration order, slot name, then declaration order. A model directive is `{ id?, controls?, config? }`; `select` is idempotent, `replace` is the explicit override, and `clear` suppresses the Agent directive only for the current call. Duplicate tool names and invalid schemas tripwire before the Model runs. `reason` is attribution emitted to observers; Harness does not enforce drift policy.
|
|
76
|
-
|
|
77
|
-
Context is separate from the configuration digest. `context.set(slot, items, { order?, reason? })` contributes only to the current call. `run({ context })` is freshly projected as a host-attributed `type: "session"` item on every call. Applications that need state across calls own it and explicitly re-declare the model-visible portion using the request identities.
|
|
78
|
-
|
|
79
|
-
`ModelAdapter` receives a projected `ModelCall` first (`prompt`, `tools`, optional `model`, `sessionId`), then `{ request, signal }`: the structured `ModelRequest` escape hatch and its cancellation signal. `prompt` is an ordered typed list: `instructions` (system), then transcript `message` / `tool-result` items, then one `context` item when the committed snapshot is non-empty. Context is a user-role envelope around a JSON array of `{ type?, value }` — structural serialization so a value cannot mint a sibling prompt item, not a sandbox against prompt injection. Tool-result items keep `toolName` and `status`. Arrivals and toolResults stay on `request` — they are already on the transcript before invoke, so appending them would duplicate history. `turnId` and `stepId` stay on `ModelRequest` and observe events, not on `ModelCall`.
|
|
80
|
-
|
|
81
|
-
`ModelRequest.configuration` carries the selected model directive, ordered instruction text, visible provider tool contracts, contributor provenance, and logical/model/combined digests. `ModelRequest.context` is the current-step snapshot (items, contributors, digest). Immediately before invoking the adapter, Harness emits one `model.requested` event. Its `attributes.call` is the exact deeply immutable `ModelCall` supplied to the adapter; `attributes.configuration` and `attributes.context` are JSON-safe attributed snapshots. This is the immutable logical adapter-input boundary, not a provider-wire trace. Harness does not retain a baseline or audit drift; an observer such as Studio can compare these events and apply its own policy.
|
|
82
|
-
|
|
83
|
-
A successful Model return is a `ModelCandidate`: an ordered `output` of `text`, `reasoning`, and `tool-call` blocks, plus optional `finishReason`, `usage`, and `evidence`. A string return becomes one text block. Session `final` joins text blocks with `""`; reasoning is stored on the transcript candidate for observability and is never part of that string or of the tool plan. Harness projects its transcript into the portable `ModelCall` and omits reasoning when replaying assistant content. Tool-call `args` are a JSON object; `{}` is valid and is not a stand-in for parse failure. `finishReason` is `stop`, `length`, `tool-calls`, `content-filter`, or `other` — never `error` or `aborted`. Thrown `invoke` or an aborted signal stays on the `model.failed` / abort path. `evidence.extras` is for small safe fields, not raw request or response bodies. Token counts that are present must be non-negative integers; `costUsd` is optional and only when the provider supplied it.
|
|
84
|
-
|
|
85
|
-
The harness canonicalizes tool-call ids when it mints the response; `sealStep` reuses those ids. `replace` may drop calls and rewrite text or reasoning but cannot change a retained call’s name or arguments or undo an inner denial. A replace that omits `finishReason`, `usage`, or `evidence` keeps the current values.
|
|
86
|
-
|
|
87
|
-
A tripwire, invalid return, or bind failure on one Session does not stop sibling Sessions. Harness contract breaks are session-scoped. Late `setTimeout` mutations throw and emit observe events; they do not stop a settled Session.
|
|
88
|
-
|
|
89
|
-
The harness has no automatic turn, Step, or wall-clock timeout. Applications own budgets through middleware.
|
|
90
|
-
|
|
91
|
-
## Structured errors and beta migration
|
|
92
|
-
|
|
93
|
-
Harness-created errors are `HarnessError` instances. Their `code` is stable and machine-readable; their `message` is explanatory and is not a compatibility contract. `details` contains only safe identifiers and paths, and `cause` preserves a foreign underlying error when there is one. Middleware-, model-, and adapter-originated errors remain foreign causes; application-owned tripwire and tool-result codes remain unrestricted strings.
|
|
94
|
-
|
|
95
|
-
Version 0.5.0-beta.1 replaces the following built-in outcome codes without aliases:
|
|
96
|
-
|
|
97
|
-
| Previous code / condition | 0.5 code |
|
|
98
|
-
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
-
| `tool.duplicate-name` | `configuration.duplicate-tool-name` |
|
|
100
|
-
| `model.selection-conflict` | `configuration.model-selection-conflict` |
|
|
101
|
-
| `tool.invalid` | `tool.invalid-name`, `tool.invalid-schema`, or `adapter.not-registered` |
|
|
102
|
-
| `prefix.invalid` | `configuration.invalid-instructions`, `configuration.invalid-tools`, `configuration.invalid-slot`, `configuration.invalid-reason`, or `configuration.invalid-order` |
|
|
103
|
-
| prefix status / drift policy | Removed; compare `model.requested` events in the observing application. |
|
|
104
|
-
| `response.replace-invalid` | `response.invalid-replacement` |
|
|
105
|
-
| `model.failed` for invalid Harness-normalized candidates | `model.invalid-candidate` |
|
|
106
|
-
| `middleware.failed` for Harness middleware-protocol faults | `middleware.request-mutators-revoked`, `middleware.next-after-return`, or `middleware.next-called-twice` |
|
|
107
|
-
| `tool.execution-failed` / `tool.preflight-failed` for malformed Harness-validated adapter data | `adapter.invalid-outcome`, `tool.invalid-tool-result`, or `interaction.invalid` |
|
|
108
|
-
| `harness.invalid-directive` build diagnostic | `model.invalid-directive` |
|
|
109
|
-
|
|
110
|
-
Foreign model invocation failures still report `model.failed`; this distinguishes them from Harness validation failures.
|
|
111
|
-
|
|
112
|
-
## Zod-only tool schemas
|
|
113
|
-
|
|
114
|
-
Harness `0.5.0-beta.1` accepts synchronous `z.object(...)` schemas only. Import `z` from `zod`; Harness does not re-export it. `tool()` validates and converts the schema to immutable JSON Schema when the definition is created; raw object literals are prepared on their first declaration.
|
|
115
|
-
|
|
116
|
-
## Manifest
|
|
117
|
-
|
|
118
|
-
`agent.manifest` is a frozen description of the sealed onion, the optional default model directive, and adapters (`id` plus an optional `maxConcurrentCalls`). It has no session configuration state. Observe `model.requested` for the per-call, JSON-safe attributed assembly; executable tool validators are omitted.
|
|
119
|
-
|
|
120
|
-
The package intentionally does not own persistence, restart recovery, credentials, transports, provider-specific wire conversion, background jobs, or detached Tool work.
|
|
33
|
+
## Design boundaries
|
|
121
34
|
|
|
122
|
-
|
|
35
|
+
- **Capabilities** declare model-visible tools, instructions, context, and candidate review through
|
|
36
|
+
`.use()`.
|
|
37
|
+
- **Services** are dependencies you construct and pass to capabilities, such as a store, MCP client,
|
|
38
|
+
or container runner.
|
|
39
|
+
- **Host code** owns transport, persistence, credentials, and background work.
|
|
40
|
+
- **Harness core** owns the loop, validation, scheduling, and recording invariants.
|
|
123
41
|
|
|
124
|
-
|
|
42
|
+
See [Examples](../examples/README.md) for complete agents and [CHANGELOG.md](./CHANGELOG.md) for
|
|
43
|
+
release notes.
|
package/dist/build/agent.d.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { AgentManifest } from "../types/manifest.js";
|
|
2
2
|
import type { BoundMiddleware } from "../types/middleware.js";
|
|
3
|
-
import type { ModelAdapter
|
|
4
|
-
import type { Session,
|
|
5
|
-
import type { AdapterRegistry } from "./adapters.js";
|
|
3
|
+
import type { ModelAdapter } from "../types/model.js";
|
|
4
|
+
import type { Session, SessionRunOptions } from "../types/session.js";
|
|
6
5
|
export interface LoopAgent {
|
|
7
6
|
readonly middleware: readonly BoundMiddleware[];
|
|
8
7
|
readonly invoke: ModelAdapter;
|
|
9
|
-
readonly directive?: ModelDirective;
|
|
10
|
-
readonly adapters: AdapterRegistry;
|
|
11
8
|
}
|
|
12
9
|
export declare class BuiltAgent {
|
|
13
10
|
#private;
|
|
14
11
|
readonly middleware: readonly BoundMiddleware[];
|
|
15
12
|
readonly manifest: AgentManifest;
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: string;
|
|
16
15
|
private constructor();
|
|
17
|
-
run(options?:
|
|
16
|
+
run(options?: SessionRunOptions): Session;
|
|
18
17
|
}
|
|
19
|
-
export declare function bindAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter,
|
|
18
|
+
export declare function bindAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, manifest: AgentManifest): BuiltAgent;
|
package/dist/build/agent.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import { LiveSession } from "../session/session.js";
|
|
2
2
|
import { createId } from "../utils/ids.js";
|
|
3
|
+
import { HarnessError } from "../errors.js";
|
|
3
4
|
let createBoundAgent;
|
|
4
5
|
export class BuiltAgent {
|
|
5
6
|
middleware;
|
|
6
7
|
manifest;
|
|
8
|
+
id;
|
|
9
|
+
name;
|
|
7
10
|
#loopAgent;
|
|
8
|
-
constructor(middleware, invoke,
|
|
11
|
+
constructor(middleware, invoke, manifest) {
|
|
9
12
|
this.middleware = middleware;
|
|
10
13
|
this.manifest = manifest;
|
|
14
|
+
this.id = manifest.id;
|
|
15
|
+
this.name = manifest.name;
|
|
11
16
|
this.#loopAgent = Object.freeze({
|
|
12
17
|
middleware,
|
|
13
18
|
invoke,
|
|
14
|
-
adapters,
|
|
15
|
-
...(directive === undefined ? {} : { directive }),
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
21
|
static {
|
|
19
|
-
createBoundAgent = (middleware, invoke,
|
|
22
|
+
createBoundAgent = (middleware, invoke, manifest) => new BuiltAgent(middleware, invoke, manifest);
|
|
20
23
|
}
|
|
21
24
|
run(options = {}) {
|
|
22
|
-
const
|
|
25
|
+
const seeded = "seed" in options && options.seed !== undefined;
|
|
26
|
+
if (seeded &&
|
|
27
|
+
(("id" in options && options.id !== undefined) ||
|
|
28
|
+
("userId" in options && options.userId !== undefined) ||
|
|
29
|
+
("context" in options && options.context !== undefined)))
|
|
30
|
+
throw new HarnessError("session.invalid-seed", "Seeded run options cannot include id, userId, or context outside the seed");
|
|
31
|
+
const id = (seeded ? options.seed.id : "id" in options ? options.id : undefined) ?? createId("session");
|
|
23
32
|
return new LiveSession(id, this.#loopAgent, options);
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
|
-
export function bindAgent(middleware, invoke,
|
|
27
|
-
return createBoundAgent(middleware, invoke,
|
|
35
|
+
export function bindAgent(middleware, invoke, manifest) {
|
|
36
|
+
return createBoundAgent(middleware, invoke, manifest);
|
|
28
37
|
}
|
package/dist/build/assemble.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { BuildResult } from "../types/manifest.js";
|
|
2
2
|
import type { BoundMiddleware } from "../types/middleware.js";
|
|
3
|
-
import type { ModelAdapter
|
|
4
|
-
import type { ToolAdapter } from "../types/tool.js";
|
|
3
|
+
import type { ModelAdapter } from "../types/model.js";
|
|
5
4
|
import { type BuiltAgent } from "./agent.js";
|
|
6
|
-
export declare function assembleAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
5
|
+
export declare function assembleAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, identity: Readonly<{
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
}>): BuildResult<BuiltAgent>;
|
package/dist/build/assemble.js
CHANGED
|
@@ -1,50 +1,17 @@
|
|
|
1
|
-
import { normalizeDirective } from "../model-normalize.js";
|
|
2
|
-
import { isHarnessError } from "../errors.js";
|
|
3
1
|
import { bindAgent } from "./agent.js";
|
|
4
|
-
import { createAdapterRegistry } from "./adapters.js";
|
|
5
2
|
import { createManifest } from "./manifest.js";
|
|
6
3
|
const diagnostic = (code, message, extra = {}) => Object.freeze({ code, message, ...extra });
|
|
7
|
-
export function assembleAgent(middleware, invoke,
|
|
4
|
+
export function assembleAgent(middleware, invoke, identity) {
|
|
8
5
|
const diagnostics = [];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!id)
|
|
15
|
-
diagnostics.push(diagnostic("adapter.invalid-id", "Adapter id must not be empty"));
|
|
16
|
-
else if (adapterIds.has(id))
|
|
17
|
-
diagnostics.push(diagnostic("adapter.duplicate-id", `Duplicate adapter id '${id}'`));
|
|
18
|
-
else if (typeof adapter.execute !== "function")
|
|
19
|
-
diagnostics.push(diagnostic("adapter.invalid", `Adapter '${id}' must provide execute()`));
|
|
20
|
-
else {
|
|
21
|
-
const limit = entry.options?.maxConcurrentCalls;
|
|
22
|
-
if (limit !== undefined &&
|
|
23
|
-
(!Number.isSafeInteger(limit) || !Number.isFinite(limit) || limit < 1)) {
|
|
24
|
-
diagnostics.push(diagnostic("adapter.invalid-max-concurrent-calls", `Adapter '${id}' maxConcurrentCalls must be a positive safe integer`));
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
adapterIds.add(id);
|
|
28
|
-
validAdapters.push(Object.freeze({
|
|
29
|
-
adapter,
|
|
30
|
-
options: Object.freeze(limit === undefined ? {} : { maxConcurrentCalls: limit }),
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
6
|
+
if (typeof identity.id !== "string" || identity.id.length === 0) {
|
|
7
|
+
diagnostics.push(diagnostic("agent.invalid-id", "Agent id must be a non-empty string"));
|
|
8
|
+
}
|
|
9
|
+
if (typeof identity.name !== "string" || identity.name.length === 0) {
|
|
10
|
+
diagnostics.push(diagnostic("agent.invalid-name", "Agent name must be a non-empty string"));
|
|
33
11
|
}
|
|
34
12
|
if (typeof invoke !== "function") {
|
|
35
13
|
diagnostics.push(diagnostic("harness.invalid-model", "A model invoke function is required"));
|
|
36
14
|
}
|
|
37
|
-
let frozenDirective;
|
|
38
|
-
if (directive !== undefined) {
|
|
39
|
-
const normalized = normalizeDirective(directive);
|
|
40
|
-
if (isHarnessError(normalized))
|
|
41
|
-
diagnostics.push(diagnostic(normalized.code, normalized.message, {
|
|
42
|
-
...(Object.keys(normalized.details).length ? { details: normalized.details } : {}),
|
|
43
|
-
...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
|
|
44
|
-
}));
|
|
45
|
-
else
|
|
46
|
-
frozenDirective = normalized;
|
|
47
|
-
}
|
|
48
15
|
const middlewareIds = new Set();
|
|
49
16
|
const frozen = [];
|
|
50
17
|
for (const item of middleware) {
|
|
@@ -59,18 +26,18 @@ export function assembleAgent(middleware, invoke, adapters, directive) {
|
|
|
59
26
|
frozen.push(Object.freeze({
|
|
60
27
|
id: item.id,
|
|
61
28
|
handle: item.handle,
|
|
29
|
+
...(item.state === undefined ? {} : { state: item.state }),
|
|
62
30
|
}));
|
|
63
31
|
}
|
|
64
32
|
}
|
|
65
33
|
if (diagnostics.length)
|
|
66
34
|
return Object.freeze({ ok: false, diagnostics: Object.freeze(diagnostics) });
|
|
67
35
|
const frozenMiddleware = Object.freeze(frozen);
|
|
68
|
-
const registry = createAdapterRegistry(Object.freeze(validAdapters));
|
|
69
36
|
const manifest = createManifest({
|
|
37
|
+
id: identity.id,
|
|
38
|
+
name: identity.name,
|
|
70
39
|
middleware: frozenMiddleware,
|
|
71
|
-
...(frozenDirective === undefined ? {} : { directive: frozenDirective }),
|
|
72
|
-
adapters: registry,
|
|
73
40
|
});
|
|
74
|
-
const agent = bindAgent(frozenMiddleware, invoke,
|
|
41
|
+
const agent = bindAgent(frozenMiddleware, invoke, manifest);
|
|
75
42
|
return Object.freeze({ ok: true, agent, manifest });
|
|
76
43
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { BoundToolDefinition, ToolDefinition } from "../types/tool.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare function bindTool(item: ToolDefinition, adapters: AdapterRegistry): BoundToolDefinition;
|
|
2
|
+
/** Prepares an executable Tool and records its middleware slot provenance. */
|
|
3
|
+
export declare function bindTool(item: ToolDefinition, owner: BoundToolDefinition["owner"]): BoundToolDefinition;
|
package/dist/build/bind-tool.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { HarnessError } from "../errors.js";
|
|
2
2
|
import { normalizedSchemaFor } from "./schema.js";
|
|
3
|
-
/**
|
|
4
|
-
export function bindTool(item,
|
|
3
|
+
/** Prepares an executable Tool and records its middleware slot provenance. */
|
|
4
|
+
export function bindTool(item, owner) {
|
|
5
5
|
if (!item.name)
|
|
6
6
|
throw new HarnessError("tool.invalid-name", "Tool name must not be empty");
|
|
7
|
-
|
|
7
|
+
if (typeof item.execute !== "function")
|
|
8
|
+
throw new HarnessError("tool.invalid", `Tool '${item.name}' must provide execute()`);
|
|
8
9
|
const parameters = normalizedSchemaFor(item);
|
|
9
10
|
return Object.freeze({
|
|
10
11
|
name: item.name,
|
|
11
12
|
...(item.description ? { description: item.description } : {}),
|
|
12
13
|
parameters: parameters,
|
|
13
|
-
|
|
14
|
+
execute: item.execute.bind(item),
|
|
15
|
+
owner: Object.freeze({ ...owner }),
|
|
14
16
|
});
|
|
15
17
|
}
|
package/dist/build/builder.d.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import type { StepMiddleware } from "../types/middleware.js";
|
|
2
|
-
import type { ModelAdapter
|
|
1
|
+
import type { BoundMiddleware, CapabilityDeclaration, StepMiddleware } from "../types/middleware.js";
|
|
2
|
+
import type { ModelAdapter } from "../types/model.js";
|
|
3
3
|
import type { BuildDiagnostic } from "../types/shared.js";
|
|
4
|
-
import type { AdapterExecutionOptions, ToolAdapter } from "../types/tool.js";
|
|
5
4
|
import { HarnessError } from "../errors.js";
|
|
6
5
|
import type { BuiltAgent } from "./agent.js";
|
|
6
|
+
export interface AgentOptions {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly instructions?: string | readonly string[];
|
|
10
|
+
}
|
|
11
|
+
interface BuilderState {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly middleware: BoundMiddleware[];
|
|
15
|
+
invoke?: ModelAdapter;
|
|
16
|
+
bound: boolean;
|
|
17
|
+
sealed: boolean;
|
|
18
|
+
agent?: BuiltAgent;
|
|
19
|
+
error?: AgentBuildError;
|
|
20
|
+
middlewareSeq: number;
|
|
21
|
+
}
|
|
7
22
|
export declare class AgentBuildError extends HarnessError {
|
|
8
23
|
readonly diagnostics: readonly BuildDiagnostic[];
|
|
9
24
|
constructor(diagnostics: readonly BuildDiagnostic[]);
|
|
@@ -11,21 +26,21 @@ export declare class AgentBuildError extends HarnessError {
|
|
|
11
26
|
export declare class AgentLifecycleError extends HarnessError {
|
|
12
27
|
constructor(message: string);
|
|
13
28
|
}
|
|
14
|
-
export declare function Agent(
|
|
29
|
+
export declare function Agent(options: AgentOptions): AgentBuilder;
|
|
15
30
|
export declare class AgentBuilder {
|
|
16
|
-
private readonly
|
|
17
|
-
|
|
18
|
-
private readonly middleware;
|
|
19
|
-
private readonly adapters;
|
|
20
|
-
private sealed;
|
|
21
|
-
private agent?;
|
|
22
|
-
private error?;
|
|
23
|
-
private middlewareSeq;
|
|
24
|
-
constructor(invoke: ModelAdapter, directive?: ModelDirective | undefined);
|
|
25
|
-
with(adapter: ToolAdapter, options?: AdapterExecutionOptions): this;
|
|
31
|
+
private readonly state;
|
|
32
|
+
constructor(state: BuilderState);
|
|
26
33
|
use(middleware: StepMiddleware): this;
|
|
27
34
|
use(id: string, middleware: StepMiddleware): this;
|
|
28
|
-
|
|
35
|
+
use<State>(declaration: CapabilityDeclaration<State>): this;
|
|
36
|
+
with(onModelCall: ModelAdapter): BoundAgentBuilder;
|
|
29
37
|
private nextMiddlewareId;
|
|
30
38
|
private push;
|
|
39
|
+
private assertOpen;
|
|
40
|
+
}
|
|
41
|
+
export declare class BoundAgentBuilder {
|
|
42
|
+
private readonly state;
|
|
43
|
+
constructor(state: BuilderState);
|
|
44
|
+
build(): BuiltAgent;
|
|
31
45
|
}
|
|
46
|
+
export {};
|
package/dist/build/builder.js
CHANGED
|
@@ -14,61 +14,112 @@ export class AgentLifecycleError extends HarnessError {
|
|
|
14
14
|
this.name = "AgentLifecycleError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
export function Agent(
|
|
18
|
-
return new AgentBuilder(
|
|
17
|
+
export function Agent(options) {
|
|
18
|
+
return new AgentBuilder(createState(options));
|
|
19
19
|
}
|
|
20
20
|
export class AgentBuilder {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
adapters = [];
|
|
25
|
-
sealed = false;
|
|
26
|
-
agent;
|
|
27
|
-
error;
|
|
28
|
-
middlewareSeq = 0;
|
|
29
|
-
constructor(invoke, directive) {
|
|
30
|
-
this.invoke = invoke;
|
|
31
|
-
this.directive = directive;
|
|
32
|
-
}
|
|
33
|
-
with(adapter, options) {
|
|
34
|
-
if (this.sealed)
|
|
35
|
-
throw new AgentLifecycleError("AgentBuilder cannot be changed after build()");
|
|
36
|
-
this.adapters.push({ adapter, ...(options === undefined ? {} : { options }) });
|
|
37
|
-
return this;
|
|
21
|
+
state;
|
|
22
|
+
constructor(state) {
|
|
23
|
+
this.state = state;
|
|
38
24
|
}
|
|
39
25
|
use(idOrMiddleware, middleware) {
|
|
40
26
|
if (typeof idOrMiddleware === "function") {
|
|
41
27
|
return this.push({ id: this.nextMiddlewareId(), handle: idOrMiddleware });
|
|
42
28
|
}
|
|
29
|
+
if (typeof idOrMiddleware === "object")
|
|
30
|
+
return this.push(compileDeclaration(idOrMiddleware));
|
|
43
31
|
return this.push({ id: idOrMiddleware, handle: middleware });
|
|
44
32
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.sealed = true;
|
|
51
|
-
const result = assembleAgent(this.middleware, this.invoke, this.adapters, this.directive);
|
|
52
|
-
if (!result.ok) {
|
|
53
|
-
this.error = new AgentBuildError(result.diagnostics);
|
|
54
|
-
throw this.error;
|
|
55
|
-
}
|
|
56
|
-
this.agent = result.agent;
|
|
57
|
-
return this.agent;
|
|
33
|
+
with(onModelCall) {
|
|
34
|
+
this.assertOpen("with()");
|
|
35
|
+
this.state.bound = true;
|
|
36
|
+
this.state.invoke = onModelCall;
|
|
37
|
+
return new BoundAgentBuilder(this.state);
|
|
58
38
|
}
|
|
59
39
|
nextMiddlewareId() {
|
|
60
|
-
const taken = new Set(this.middleware.map((item) => item.id));
|
|
40
|
+
const taken = new Set(this.state.middleware.map((item) => item.id));
|
|
61
41
|
let id;
|
|
62
42
|
do {
|
|
63
|
-
this.middlewareSeq += 1;
|
|
64
|
-
id = `middleware-${this.middlewareSeq}`;
|
|
43
|
+
this.state.middlewareSeq += 1;
|
|
44
|
+
id = `middleware-${this.state.middlewareSeq}`;
|
|
65
45
|
} while (taken.has(id));
|
|
66
46
|
return id;
|
|
67
47
|
}
|
|
68
48
|
push(entry) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.middleware.push(entry);
|
|
49
|
+
this.assertOpen("build()");
|
|
50
|
+
this.state.middleware.push(entry);
|
|
72
51
|
return this;
|
|
73
52
|
}
|
|
53
|
+
assertOpen(after) {
|
|
54
|
+
if (this.state.bound)
|
|
55
|
+
throw new AgentLifecycleError("AgentBuilder cannot be changed after with()");
|
|
56
|
+
if (this.state.sealed)
|
|
57
|
+
throw new AgentLifecycleError(`AgentBuilder cannot be changed after ${after}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class BoundAgentBuilder {
|
|
61
|
+
state;
|
|
62
|
+
constructor(state) {
|
|
63
|
+
this.state = state;
|
|
64
|
+
}
|
|
65
|
+
build() {
|
|
66
|
+
if (this.state.agent)
|
|
67
|
+
return this.state.agent;
|
|
68
|
+
if (this.state.error)
|
|
69
|
+
throw this.state.error;
|
|
70
|
+
this.state.sealed = true;
|
|
71
|
+
const result = assembleAgent(this.state.middleware, this.state.invoke, {
|
|
72
|
+
id: this.state.id,
|
|
73
|
+
name: this.state.name,
|
|
74
|
+
});
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
this.state.error = new AgentBuildError(result.diagnostics);
|
|
77
|
+
throw this.state.error;
|
|
78
|
+
}
|
|
79
|
+
this.state.agent = result.agent;
|
|
80
|
+
return this.state.agent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function createState(options) {
|
|
84
|
+
const middleware = [];
|
|
85
|
+
if (options.instructions !== undefined) {
|
|
86
|
+
const instructions = typeof options.instructions === "string" ? [options.instructions] : options.instructions;
|
|
87
|
+
middleware.push(compileDeclaration({ id: "agent", instructions }));
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
id: options.id,
|
|
91
|
+
name: options.name,
|
|
92
|
+
middleware,
|
|
93
|
+
bound: false,
|
|
94
|
+
sealed: false,
|
|
95
|
+
middlewareSeq: 0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function compileDeclaration(declaration) {
|
|
99
|
+
const tools = copyItems(declaration.tools, declaration.id);
|
|
100
|
+
const instructions = copyItems(declaration.instructions, declaration.id);
|
|
101
|
+
const model = declaration.model;
|
|
102
|
+
const handle = async (request, next) => {
|
|
103
|
+
if (tools)
|
|
104
|
+
request.configuration.tools.set(tools.slot, tools.items);
|
|
105
|
+
if (instructions)
|
|
106
|
+
request.configuration.instructions.set(instructions.slot, instructions.items);
|
|
107
|
+
if (model)
|
|
108
|
+
request.configuration.model.select(model);
|
|
109
|
+
return declaration.middleware ? declaration.middleware(request, next) : next();
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
id: declaration.id,
|
|
113
|
+
handle,
|
|
114
|
+
...(declaration.state === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { state: declaration.state }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function copyItems(value, defaultSlot) {
|
|
120
|
+
if (value === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
if (!("items" in value))
|
|
123
|
+
return Object.freeze({ slot: defaultSlot, items: Object.freeze([...value]) });
|
|
124
|
+
return Object.freeze({ slot: value.slot, items: Object.freeze([...value.items]) });
|
|
74
125
|
}
|