@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +27 -108
  3. package/dist/build/agent.d.ts +6 -7
  4. package/dist/build/agent.js +16 -7
  5. package/dist/build/assemble.d.ts +5 -6
  6. package/dist/build/assemble.js +10 -43
  7. package/dist/build/bind-tool.d.ts +2 -3
  8. package/dist/build/bind-tool.js +6 -4
  9. package/dist/build/builder.d.ts +30 -15
  10. package/dist/build/builder.js +89 -38
  11. package/dist/build/helpers.d.ts +2 -3
  12. package/dist/build/helpers.js +0 -1
  13. package/dist/build/manifest.d.ts +2 -4
  14. package/dist/build/manifest.js +2 -8
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/index.d.ts +7 -6
  17. package/dist/index.js +2 -2
  18. package/dist/session/capability-state.d.ts +14 -0
  19. package/dist/session/capability-state.js +67 -0
  20. package/dist/session/input-queue.d.ts +11 -2
  21. package/dist/session/input-queue.js +5 -1
  22. package/dist/session/record.d.ts +11 -0
  23. package/dist/session/record.js +26 -0
  24. package/dist/session/scheduler.d.ts +17 -3
  25. package/dist/session/scheduler.js +181 -48
  26. package/dist/session/seed.d.ts +10 -0
  27. package/dist/session/seed.js +219 -0
  28. package/dist/session/session.d.ts +3 -2
  29. package/dist/session/session.js +15 -3
  30. package/dist/session/state.d.ts +8 -3
  31. package/dist/session/state.js +16 -4
  32. package/dist/session/submission-stream.d.ts +2 -0
  33. package/dist/session/submission-stream.js +11 -1
  34. package/dist/step/model-configuration.d.ts +1 -3
  35. package/dist/step/model-configuration.js +2 -4
  36. package/dist/step/project.js +4 -2
  37. package/dist/step/run.d.ts +6 -1
  38. package/dist/step/run.js +35 -13
  39. package/dist/step/seal.d.ts +6 -2
  40. package/dist/step/seal.js +4 -3
  41. package/dist/step/step-context.d.ts +5 -2
  42. package/dist/step/step-context.js +19 -11
  43. package/dist/turn/plan-runner.d.ts +28 -13
  44. package/dist/turn/plan-runner.js +165 -182
  45. package/dist/turn/runner.d.ts +18 -4
  46. package/dist/turn/runner.js +73 -45
  47. package/dist/types/manifest.d.ts +2 -6
  48. package/dist/types/middleware.d.ts +25 -4
  49. package/dist/types/model.d.ts +3 -2
  50. package/dist/types/session.d.ts +86 -2
  51. package/dist/types/shared.d.ts +65 -9
  52. package/dist/types/tool.d.ts +37 -39
  53. package/dist/utils/immutable.js +16 -2
  54. package/package.json +1 -2
  55. package/dist/build/adapters.d.ts +0 -11
  56. package/dist/build/adapters.js +0 -91
  57. package/docs/loop.md +0 -47
  58. package/docs/model-call-projection.md +0 -112
  59. 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
- # @nylorun/harness
1
+ # `@nylorun/harness`
2
2
 
3
- `@nylorun/harness` is a small, provider-neutral agent loop. It seals one model adapter, an optional default model directive, tool adapters, and an ordered middleware onion into a fixed Agent, then runs independent in-memory Sessions. Middleware stages named changes against a session-scoped prompt-prefix state; each Model call receives an immutable canonical prefix, then its candidate is reviewed and its tool plan sealed before any adapter runs.
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 the current candidate with `npm install @nylorun/harness@beta`. Until 1.0, this API is intentionally experimental: minor releases may contain breaking changes, while patch releases are reserved for compatible fixes. See [CHANGELOG.md](./CHANGELOG.md) for migration notes.
7
+ > **Experimental beta.** Install with `npm install @nylorun/harness@beta`.
6
8
 
7
9
  ```ts
8
- import { Agent, adapter, model, tool } from "@nylorun/harness";
10
+ import { Agent, model, tool } from "@nylorun/harness";
9
11
  import { z } from "zod";
10
12
 
11
- const local = adapter({
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 a message",
16
+ description: "Echo text",
21
17
  parameters: z.object({ text: z.string() }),
22
- executeWith: "local",
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 session = Agent(example, { id: "example" })
34
- .with(local)
35
- .use("echo", async (request, next) => {
36
- request.configuration.tools.set("echo-tools", [echo], { order: 100 });
37
- request.configuration.instructions.set("echo-policy", ["Echo the user text."], { order: 100 });
38
- return next();
39
- })
40
- .use("turn-budget", async (request, next) => {
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
- ## Middleware
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
- Each `StepRequest` includes opaque `sessionId`, `turnId`, and `stepId` identities, so middleware can associate application-owned state with the current execution. Draft the current call on `request.configuration`: named `instructions.set`, `tools.set`, and `model.select` / `replace` / `clear`; draft current-call tail data on `request.context.set`; or tripwire. `await next()` runs the inner onion and one Model call, then returns a branded `StepResponse`. Review that candidate (`deny`, `requireInteraction`, `requirePreflight`, `replace`, `tripwire`) and return it. Skipping `next()` is legal only by returning `request.tripwire(...)`. `void next()` is not supported.
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
- ## Internals
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
- The in-memory loop is **build → session → turn → step**. Start with [docs/loop.md](./docs/loop.md). The complete [error-code and observe-event reference](./docs/reference.md) documents the stable machine-readable surfaces.
42
+ See [Examples](../examples/README.md) for complete agents and [CHANGELOG.md](./CHANGELOG.md) for
43
+ release notes.
@@ -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, ModelDirective } from "../types/model.js";
4
- import type { Session, SessionOptions } from "../types/session.js";
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?: SessionOptions): Session;
16
+ run(options?: SessionRunOptions): Session;
18
17
  }
19
- export declare function bindAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, adapters: AdapterRegistry, manifest: AgentManifest, directive?: ModelDirective): BuiltAgent;
18
+ export declare function bindAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, manifest: AgentManifest): BuiltAgent;
@@ -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, adapters, manifest, directive) {
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, adapters, manifest, directive) => new BuiltAgent(middleware, invoke, adapters, manifest, directive);
22
+ createBoundAgent = (middleware, invoke, manifest) => new BuiltAgent(middleware, invoke, manifest);
20
23
  }
21
24
  run(options = {}) {
22
- const id = options.id ?? createId("session");
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, adapters, manifest, directive) {
27
- return createBoundAgent(middleware, invoke, adapters, manifest, directive);
35
+ export function bindAgent(middleware, invoke, manifest) {
36
+ return createBoundAgent(middleware, invoke, manifest);
28
37
  }
@@ -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, ModelDirective } from "../types/model.js";
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, adapters: readonly {
7
- readonly adapter: ToolAdapter;
8
- readonly options?: import("../types/tool.js").AdapterExecutionOptions;
9
- }[], directive?: ModelDirective): BuildResult<BuiltAgent>;
5
+ export declare function assembleAgent(middleware: readonly BoundMiddleware[], invoke: ModelAdapter, identity: Readonly<{
6
+ id: string;
7
+ name: string;
8
+ }>): BuildResult<BuiltAgent>;
@@ -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, adapters, directive) {
4
+ export function assembleAgent(middleware, invoke, identity) {
8
5
  const diagnostics = [];
9
- const adapterIds = new Set();
10
- const validAdapters = [];
11
- for (const entry of adapters) {
12
- const adapter = entry?.adapter;
13
- const id = adapter?.id;
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, registry, manifest, frozenDirective);
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
- import type { AdapterRegistry } from "./adapters.js";
3
- /** Binds one Tool definition against the Agent's adapter registry. */
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;
@@ -1,15 +1,17 @@
1
1
  import { HarnessError } from "../errors.js";
2
2
  import { normalizedSchemaFor } from "./schema.js";
3
- /** Binds one Tool definition against the Agent's adapter registry. */
4
- export function bindTool(item, adapters) {
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
- adapters.require(item.executeWith);
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
- executeWith: item.executeWith,
14
+ execute: item.execute.bind(item),
15
+ owner: Object.freeze({ ...owner }),
14
16
  });
15
17
  }
@@ -1,9 +1,24 @@
1
- import type { StepMiddleware } from "../types/middleware.js";
2
- import type { ModelAdapter, ModelDirective } from "../types/model.js";
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(model: ModelAdapter, directive?: ModelDirective): AgentBuilder;
29
+ export declare function Agent(options: AgentOptions): AgentBuilder;
15
30
  export declare class AgentBuilder {
16
- private readonly invoke;
17
- private readonly directive?;
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
- build(): BuiltAgent;
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 {};
@@ -14,61 +14,112 @@ export class AgentLifecycleError extends HarnessError {
14
14
  this.name = "AgentLifecycleError";
15
15
  }
16
16
  }
17
- export function Agent(model, directive) {
18
- return new AgentBuilder(model, directive);
17
+ export function Agent(options) {
18
+ return new AgentBuilder(createState(options));
19
19
  }
20
20
  export class AgentBuilder {
21
- invoke;
22
- directive;
23
- middleware = [];
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
- build() {
46
- if (this.agent)
47
- return this.agent;
48
- if (this.error)
49
- throw this.error;
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
- if (this.sealed)
70
- throw new AgentLifecycleError("AgentBuilder cannot be changed after build()");
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
  }