@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +192 -0
  3. package/README.md +124 -0
  4. package/dist/build/adapters.d.ts +11 -0
  5. package/dist/build/adapters.js +91 -0
  6. package/dist/build/agent.d.ts +19 -0
  7. package/dist/build/agent.js +28 -0
  8. package/dist/build/assemble.d.ts +9 -0
  9. package/dist/build/assemble.js +76 -0
  10. package/dist/build/bind-tool.d.ts +4 -0
  11. package/dist/build/bind-tool.js +15 -0
  12. package/dist/build/builder.d.ts +31 -0
  13. package/dist/build/builder.js +74 -0
  14. package/dist/build/helpers.d.ts +7 -0
  15. package/dist/build/helpers.js +5 -0
  16. package/dist/build/manifest.d.ts +9 -0
  17. package/dist/build/manifest.js +15 -0
  18. package/dist/build/schema.d.ts +19 -0
  19. package/dist/build/schema.js +86 -0
  20. package/dist/errors.d.ts +17 -0
  21. package/dist/errors.js +17 -0
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.js +4 -0
  24. package/dist/model-normalize.d.ts +6 -0
  25. package/dist/model-normalize.js +211 -0
  26. package/dist/session/event-log.d.ts +11 -0
  27. package/dist/session/event-log.js +63 -0
  28. package/dist/session/input-queue.d.ts +29 -0
  29. package/dist/session/input-queue.js +78 -0
  30. package/dist/session/scheduler.d.ts +48 -0
  31. package/dist/session/scheduler.js +352 -0
  32. package/dist/session/session.d.ts +14 -0
  33. package/dist/session/session.js +55 -0
  34. package/dist/session/state.d.ts +10 -0
  35. package/dist/session/state.js +36 -0
  36. package/dist/session/submission-stream.d.ts +13 -0
  37. package/dist/session/submission-stream.js +36 -0
  38. package/dist/step/canonicalize.d.ts +21 -0
  39. package/dist/step/canonicalize.js +62 -0
  40. package/dist/step/compose.d.ts +4 -0
  41. package/dist/step/compose.js +106 -0
  42. package/dist/step/context-draft.d.ts +10 -0
  43. package/dist/step/context-draft.js +71 -0
  44. package/dist/step/model-configuration.d.ts +15 -0
  45. package/dist/step/model-configuration.js +153 -0
  46. package/dist/step/project.d.ts +2 -0
  47. package/dist/step/project.js +103 -0
  48. package/dist/step/resolve.d.ts +9 -0
  49. package/dist/step/resolve.js +16 -0
  50. package/dist/step/run.d.ts +27 -0
  51. package/dist/step/run.js +127 -0
  52. package/dist/step/seal.d.ts +31 -0
  53. package/dist/step/seal.js +108 -0
  54. package/dist/step/slot-assembly.d.ts +39 -0
  55. package/dist/step/slot-assembly.js +52 -0
  56. package/dist/step/step-context.d.ts +36 -0
  57. package/dist/step/step-context.js +255 -0
  58. package/dist/turn/plan-runner.d.ts +55 -0
  59. package/dist/turn/plan-runner.js +370 -0
  60. package/dist/turn/runner.d.ts +53 -0
  61. package/dist/turn/runner.js +128 -0
  62. package/dist/types/manifest.d.ts +22 -0
  63. package/dist/types/manifest.js +1 -0
  64. package/dist/types/middleware.d.ts +65 -0
  65. package/dist/types/middleware.js +1 -0
  66. package/dist/types/model.d.ts +166 -0
  67. package/dist/types/model.js +1 -0
  68. package/dist/types/session.d.ts +122 -0
  69. package/dist/types/session.js +1 -0
  70. package/dist/types/shared.d.ts +180 -0
  71. package/dist/types/shared.js +1 -0
  72. package/dist/types/tool.d.ts +101 -0
  73. package/dist/types/tool.js +1 -0
  74. package/dist/utils/digest.d.ts +1 -0
  75. package/dist/utils/digest.js +14 -0
  76. package/dist/utils/ids.d.ts +1 -0
  77. package/dist/utils/ids.js +3 -0
  78. package/dist/utils/immutable.d.ts +5 -0
  79. package/dist/utils/immutable.js +54 -0
  80. package/dist/utils/maps.d.ts +1 -0
  81. package/dist/utils/maps.js +37 -0
  82. package/dist/utils/observe.d.ts +8 -0
  83. package/dist/utils/observe.js +29 -0
  84. package/docs/loop.md +47 -0
  85. package/docs/model-call-projection.md +112 -0
  86. package/docs/reference.md +122 -0
  87. package/package.json +70 -0
@@ -0,0 +1,127 @@
1
+ import { normalizeCandidate } from "../model-normalize.js";
2
+ import { HarnessError, isHarnessError } from "../errors.js";
3
+ import { copyJson } from "../utils/immutable.js";
4
+ import { runMiddleware } from "./compose.js";
5
+ import { ContextDraft } from "./context-draft.js";
6
+ import { StepContext } from "./step-context.js";
7
+ import { projectModelCall } from "./project.js";
8
+ import { resolveModelRequest } from "./resolve.js";
9
+ import { sealStep } from "./seal.js";
10
+ import { ModelConfigurationDraft } from "./model-configuration.js";
11
+ export async function runStep(input) {
12
+ const stepInput = Object.freeze({
13
+ sessionId: input.sessionId,
14
+ turnId: input.turnId,
15
+ stepId: input.stepId,
16
+ turnNumber: input.turnNumber,
17
+ stepNumber: input.stepNumber,
18
+ session: input.session,
19
+ arrivals: Object.freeze([...input.arrivals]),
20
+ toolResults: Object.freeze([...input.toolResults]),
21
+ transcript: Object.freeze([...input.state.transcript]),
22
+ });
23
+ const configuration = new ModelConfigurationDraft(input.agent.adapters, input.agent.directive);
24
+ const runtimeContext = new ContextDraft(input.session.context);
25
+ const context = new StepContext(stepInput, input.observe, configuration, runtimeContext);
26
+ input.observe(() => ({
27
+ type: "step.started",
28
+ turnId: input.turnId,
29
+ stepId: input.stepId,
30
+ turnNumber: input.turnNumber,
31
+ stepNumber: input.stepNumber,
32
+ attributes: snapshotStepStart(stepInput),
33
+ }));
34
+ await runMiddleware(input.agent.middleware, context, async () => {
35
+ if (context.currentTripwire)
36
+ return context.tripwire(context.currentTripwire);
37
+ try {
38
+ context.sealConfiguration(configuration.snapshot());
39
+ context.sealContext(runtimeContext.snapshot());
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ return context.tripwire({
44
+ code: isHarnessError(error) ? error.code : "configuration.invalid",
45
+ message,
46
+ });
47
+ }
48
+ const request = resolveModelRequest({
49
+ context,
50
+ arrivals: input.arrivals,
51
+ toolResults: input.toolResults,
52
+ });
53
+ const call = projectModelCall(request);
54
+ input.observe(() => ({
55
+ type: "model.requested",
56
+ turnId: input.turnId,
57
+ stepId: input.stepId,
58
+ ...(request.model?.id === undefined ? {} : { requestedModelId: request.model.id }),
59
+ attributes: Object.freeze({
60
+ call,
61
+ configuration: snapshotConfiguration(request.configuration),
62
+ context: copyJson(request.context),
63
+ }),
64
+ }));
65
+ try {
66
+ const minted = context.mintFromModel(normalizeCandidate(await input.agent.invoke(call, {
67
+ request,
68
+ signal: input.signal,
69
+ })));
70
+ if (input.signal.aborted)
71
+ throw input.signal.reason;
72
+ const candidate = context.currentCandidate;
73
+ if (!candidate)
74
+ throw new HarnessError("model.candidate-missing", "Model candidate missing after mint");
75
+ input.observe(() => ({
76
+ type: "model.completed",
77
+ turnId: input.turnId,
78
+ stepId: input.stepId,
79
+ ...(request.model?.id === undefined ? {} : { requestedModelId: request.model.id }),
80
+ attributes: candidate,
81
+ }));
82
+ return minted;
83
+ }
84
+ catch (error) {
85
+ if (input.signal.aborted)
86
+ throw input.signal.reason;
87
+ return context.tripwire({
88
+ code: isHarnessError(error) ? error.code : "model.failed",
89
+ message: error instanceof Error ? error.message : String(error),
90
+ });
91
+ }
92
+ }, input.observe);
93
+ const output = sealStep(context);
94
+ const candidate = output.kind === "tools" ? output.plan.candidate : context.currentCandidate;
95
+ return Object.freeze({ stepId: input.stepId, ...(candidate ? { candidate } : {}), output });
96
+ }
97
+ function snapshotStepStart(input) {
98
+ const session = Object.freeze({
99
+ ...(input.session.userId === undefined ? {} : { userId: input.session.userId }),
100
+ ...(input.session.context === undefined ? {} : { context: copyJson(input.session.context) }),
101
+ });
102
+ return Object.freeze({
103
+ ...(Object.keys(session).length === 0 ? {} : { session }),
104
+ arrivals: copyJson(input.arrivals),
105
+ toolResults: copyJson(input.toolResults),
106
+ transcript: input.transcript,
107
+ });
108
+ }
109
+ function snapshotConfiguration(configuration) {
110
+ return Object.freeze({
111
+ version: configuration.version,
112
+ ...(configuration.model === undefined ? {} : { model: copyJson(configuration.model) }),
113
+ instructions: copyJson(configuration.instructions),
114
+ tools: snapshotTools(configuration.tools),
115
+ toolContracts: copyJson(configuration.toolContracts),
116
+ contributors: copyJson(configuration.contributors),
117
+ digests: copyJson(configuration.digests),
118
+ });
119
+ }
120
+ function snapshotTools(tools) {
121
+ return Object.freeze(tools.map((tool) => Object.freeze({
122
+ name: tool.name,
123
+ ...(tool.description === undefined ? {} : { description: tool.description }),
124
+ executeWith: tool.executeWith,
125
+ parameters: Object.freeze({ jsonSchema: copyJson(tool.parameters.jsonSchema) }),
126
+ })));
127
+ }
@@ -0,0 +1,31 @@
1
+ import type { ModelCandidate } from "../types/model.js";
2
+ import type { Tripwire } from "../types/shared.js";
3
+ import type { RequiredInteraction, SealedToolCall, ToolResult } from "../types/tool.js";
4
+ import type { StepContext } from "./step-context.js";
5
+ export interface ExecutablePlanEntry {
6
+ readonly call: SealedToolCall;
7
+ readonly invocationId: string;
8
+ readonly interaction?: RequiredInteraction;
9
+ readonly preflight?: "sandbox" | "validation";
10
+ }
11
+ export interface InternalToolPlan {
12
+ readonly candidate: ModelCandidate;
13
+ readonly order: readonly {
14
+ readonly callId: string;
15
+ readonly toolName: string;
16
+ }[];
17
+ readonly executable: readonly ExecutablePlanEntry[];
18
+ readonly immediateResults: readonly ToolResult[];
19
+ }
20
+ export type SealedStepOutput = {
21
+ readonly kind: "tripwire";
22
+ readonly tripwire: Tripwire;
23
+ } | {
24
+ readonly kind: "final";
25
+ readonly output: string;
26
+ } | {
27
+ readonly kind: "tools";
28
+ readonly plan: InternalToolPlan;
29
+ };
30
+ /** Converts the reviewed canonical candidate into either a final response or an executable tool plan. */
31
+ export declare function sealStep(context: StepContext): SealedStepOutput;
@@ -0,0 +1,108 @@
1
+ import { textFromOutput } from "../model-normalize.js";
2
+ import { createId } from "../utils/ids.js";
3
+ import { HarnessError, isHarnessError } from "../errors.js";
4
+ import { assertJson, copyJson } from "../utils/immutable.js";
5
+ const failed = (callId, toolName, code, message) => Object.freeze({ kind: "failed", callId, toolName, code, message });
6
+ /** Converts the reviewed canonical candidate into either a final response or an executable tool plan. */
7
+ export function sealStep(context) {
8
+ if (context.currentTripwire)
9
+ return Object.freeze({ kind: "tripwire", tripwire: context.currentTripwire });
10
+ const calls = context.canonicalCalls();
11
+ if (!calls.length)
12
+ return Object.freeze({
13
+ kind: "final",
14
+ output: textFromOutput(context.currentCandidate?.output ?? []),
15
+ });
16
+ const catalog = context.catalogByName;
17
+ const sealed = calls.map((call) => sealCall(context, catalog, call));
18
+ const canonicalCandidate = context.currentCandidate ?? Object.freeze({ output: Object.freeze([]) });
19
+ return Object.freeze({
20
+ kind: "tools",
21
+ plan: Object.freeze({
22
+ candidate: canonicalCandidate,
23
+ order: Object.freeze(sealed.map((item) => item.order)),
24
+ executable: Object.freeze(sealed.flatMap((item) => (item.executable ? [item.executable] : []))),
25
+ immediateResults: Object.freeze(sealed.flatMap((item) => (item.immediate ? [item.immediate] : []))),
26
+ }),
27
+ });
28
+ }
29
+ function sealCall(context, catalog, candidate) {
30
+ const order = Object.freeze({ callId: candidate.id, toolName: candidate.name });
31
+ const canonical = Object.freeze({
32
+ id: candidate.id,
33
+ name: candidate.name,
34
+ args: candidate.args,
35
+ });
36
+ if (candidate.missingId)
37
+ return {
38
+ order,
39
+ canonical,
40
+ immediate: failed(candidate.id, candidate.name, "tool.invalid-call-id", "Tool call id must not be empty"),
41
+ };
42
+ if (candidate.duplicateId)
43
+ return {
44
+ order,
45
+ canonical,
46
+ immediate: failed(candidate.id, candidate.name, "tool.duplicate-call-id", `Duplicate Tool call id '${candidate.duplicateId}'`),
47
+ };
48
+ const tool = catalog.get(candidate.name);
49
+ if (!tool)
50
+ return {
51
+ order,
52
+ canonical,
53
+ immediate: failed(candidate.id, candidate.name, "tool.unknown", `Unknown Tool '${candidate.name}'`),
54
+ };
55
+ const args = validatedArguments(tool, candidate);
56
+ if (isHarnessError(args))
57
+ return {
58
+ order,
59
+ canonical,
60
+ immediate: failed(candidate.id, candidate.name, args.code, args.message),
61
+ };
62
+ const denial = context.denialFor(candidate.id);
63
+ if (denial)
64
+ return {
65
+ order,
66
+ canonical,
67
+ immediate: Object.freeze({
68
+ kind: "denied",
69
+ callId: candidate.id,
70
+ toolName: candidate.name,
71
+ reason: denial,
72
+ }),
73
+ };
74
+ const requested = context.interactionFor(candidate.id);
75
+ const interaction = requested
76
+ ? Object.freeze({ ...requested, id: requested.id ?? createId("interaction") })
77
+ : undefined;
78
+ const preflight = context.preflightFor(candidate.id);
79
+ return {
80
+ order,
81
+ canonical,
82
+ executable: Object.freeze({
83
+ call: Object.freeze({
84
+ callId: candidate.id,
85
+ toolName: candidate.name,
86
+ args,
87
+ executeWith: tool.executeWith,
88
+ }),
89
+ invocationId: createId("invocation"),
90
+ ...(interaction ? { interaction } : {}),
91
+ ...(preflight ? { preflight } : {}),
92
+ }),
93
+ };
94
+ }
95
+ function validatedArguments(tool, candidate) {
96
+ const validation = tool.parameters.validate(candidate.args);
97
+ if (!validation.ok)
98
+ return new HarnessError("tool.invalid-arguments", validation.issues.join("; "));
99
+ try {
100
+ assertJson(validation.value, `arguments for '${candidate.name}'`);
101
+ return copyJson(validation.value);
102
+ }
103
+ catch (error) {
104
+ return isHarnessError(error)
105
+ ? error
106
+ : new HarnessError("tool.invalid-arguments", String(error), { cause: error });
107
+ }
108
+ }
@@ -0,0 +1,39 @@
1
+ import { type HarnessErrorCode } from "../errors.js";
2
+ export interface SlotOwner {
3
+ readonly middlewareId: string;
4
+ readonly middlewareOrder: number;
5
+ readonly slot: string;
6
+ readonly order: number;
7
+ }
8
+ export interface SlotDeclaration<Value> {
9
+ readonly owner: SlotOwner;
10
+ readonly value: Value;
11
+ readonly reason?: string;
12
+ }
13
+ /**
14
+ * Shared per-step slot store. It has no session lifetime: declarations are discarded with the
15
+ * draft that owns this instance. Configuration and context supply their typed values and snapshot
16
+ * them after this assembler has applied ownership, replacement, and canonical ordering.
17
+ */
18
+ export declare class SlotDraft<Value> {
19
+ #private;
20
+ set(input: {
21
+ readonly middlewareId: string;
22
+ readonly middlewareOrder: number;
23
+ readonly slot: string;
24
+ readonly value: Value;
25
+ readonly order?: number;
26
+ readonly reason?: string;
27
+ readonly invalidSlot: HarnessErrorCode;
28
+ readonly invalidOrder: HarnessErrorCode;
29
+ readonly invalidReason: HarnessErrorCode;
30
+ readonly label: string;
31
+ }): void;
32
+ values(): readonly SlotDeclaration<Value>[];
33
+ }
34
+ export declare function slotOwner(middlewareId: string, middlewareOrder: number, slot: string): SlotOwner;
35
+ export declare function slotKey(value: SlotOwner): string;
36
+ export declare function checkedSlot(value: string, code: HarnessErrorCode, label: string): void;
37
+ export declare function checkedReason(value: string, code: HarnessErrorCode, label: string): string;
38
+ export declare function withOrder(owner: SlotOwner, requested: number | undefined, previousOrder: number | undefined, code: HarnessErrorCode, label: string): SlotOwner;
39
+ export declare function compareSlotOwners(left: SlotOwner, right: SlotOwner): number;
@@ -0,0 +1,52 @@
1
+ import { HarnessError } from "../errors.js";
2
+ /**
3
+ * Shared per-step slot store. It has no session lifetime: declarations are discarded with the
4
+ * draft that owns this instance. Configuration and context supply their typed values and snapshot
5
+ * them after this assembler has applied ownership, replacement, and canonical ordering.
6
+ */
7
+ export class SlotDraft {
8
+ #slots = new Map();
9
+ set(input) {
10
+ checkedSlot(input.slot, input.invalidSlot, input.label);
11
+ const base = slotOwner(input.middlewareId, input.middlewareOrder, input.slot);
12
+ const key = slotKey(base);
13
+ const prior = this.#slots.get(key);
14
+ const owner = withOrder(base, input.order, prior?.owner.order, input.invalidOrder, input.label);
15
+ this.#slots.set(key, Object.freeze({
16
+ owner,
17
+ value: input.value,
18
+ ...(input.reason === undefined
19
+ ? {}
20
+ : { reason: checkedReason(input.reason, input.invalidReason, input.label) }),
21
+ }));
22
+ }
23
+ values() {
24
+ return Object.freeze([...this.#slots.values()].sort((left, right) => compareSlotOwners(left.owner, right.owner)));
25
+ }
26
+ }
27
+ export function slotOwner(middlewareId, middlewareOrder, slot) {
28
+ return Object.freeze({ middlewareId, middlewareOrder, slot, order: 0 });
29
+ }
30
+ export function slotKey(value) {
31
+ return `${value.middlewareId}\u0000${value.slot}`;
32
+ }
33
+ export function checkedSlot(value, code, label) {
34
+ if (typeof value !== "string" || value.length === 0)
35
+ throw new HarnessError(code, `${label} slot must be a non-empty string`);
36
+ }
37
+ export function checkedReason(value, code, label) {
38
+ if (typeof value !== "string" || value.length === 0)
39
+ throw new HarnessError(code, `${label} mutation reason must be a non-empty string`);
40
+ return value;
41
+ }
42
+ export function withOrder(owner, requested, previousOrder, code, label) {
43
+ const order = requested ?? previousOrder ?? 0;
44
+ if (!Number.isFinite(order))
45
+ throw new HarnessError(code, `${label} order must be a finite number`);
46
+ return Object.freeze({ ...owner, order });
47
+ }
48
+ export function compareSlotOwners(left, right) {
49
+ return (left.order - right.order ||
50
+ left.middlewareOrder - right.middlewareOrder ||
51
+ left.slot.localeCompare(right.slot));
52
+ }
@@ -0,0 +1,36 @@
1
+ import type { ContextSnapshot, ModelCandidate, ModelDirective, ModelConfigurationSnapshot } from "../types/model.js";
2
+ import type { StepInput, StepRequest, StepResponse } from "../types/middleware.js";
3
+ import type { Tripwire } from "../types/shared.js";
4
+ import type { ObserveEmit } from "../utils/observe.js";
5
+ import type { BoundToolDefinition, Interaction } from "../types/tool.js";
6
+ import { type CanonicalCall } from "./canonicalize.js";
7
+ import { ContextDraft } from "./context-draft.js";
8
+ import { ModelConfigurationDraft } from "./model-configuration.js";
9
+ export declare function isBrandedResponse(value: unknown): value is StepResponse;
10
+ /** Mutable Step state. Middleware receives leased request views and one branded response. */
11
+ export declare class StepContext {
12
+ #private;
13
+ readonly input: Readonly<StepInput>;
14
+ constructor(input: Readonly<StepInput>, observe: ObserveEmit, configuration: ModelConfigurationDraft, context: ContextDraft);
15
+ get currentTripwire(): Tripwire | undefined;
16
+ get currentCandidate(): Readonly<ModelCandidate> | undefined;
17
+ get selectedDirective(): ModelDirective | undefined;
18
+ get instructions(): readonly string[];
19
+ contextSnapshot(): ContextSnapshot;
20
+ get offeredTools(): readonly BoundToolDefinition[];
21
+ get catalogByName(): ReadonlyMap<string, BoundToolDefinition>;
22
+ configurationSnapshot(): ModelConfigurationSnapshot;
23
+ sealConfiguration(snapshot: ModelConfigurationSnapshot): void;
24
+ sealContext(snapshot: ContextSnapshot): void;
25
+ denialFor(callId: string): string | undefined;
26
+ interactionFor(callId: string): Interaction | undefined;
27
+ preflightFor(callId: string): "sandbox" | "validation" | undefined;
28
+ canonicalCalls(): readonly CanonicalCall[];
29
+ mintFromModel(candidate: Readonly<ModelCandidate>): StepResponse;
30
+ tripwire(error: Tripwire): StepResponse;
31
+ seal(): void;
32
+ requestFacade(middlewareId: string, middlewareOrder: number): {
33
+ readonly value: StepRequest;
34
+ revokeMutators(): void;
35
+ };
36
+ }
@@ -0,0 +1,255 @@
1
+ import { HarnessError, isHarnessError } from "../errors.js";
2
+ import { callsFromCanonical, candidateFromCanonical, canonicalizeOutput, identityKey, } from "./canonicalize.js";
3
+ import { normalizeCandidate } from "../model-normalize.js";
4
+ import { ContextDraft } from "./context-draft.js";
5
+ import { ModelConfigurationDraft } from "./model-configuration.js";
6
+ const branded = new WeakSet();
7
+ export function isBrandedResponse(value) {
8
+ return typeof value === "object" && value !== null && branded.has(value);
9
+ }
10
+ /** Mutable Step state. Middleware receives leased request views and one branded response. */
11
+ export class StepContext {
12
+ input;
13
+ #observe;
14
+ #context;
15
+ #configuration;
16
+ #denials = new Map();
17
+ #interactions = new Map();
18
+ #preflights = new Map();
19
+ #identities = new Map();
20
+ #canonical = Object.freeze([]);
21
+ #candidate;
22
+ #tripwire;
23
+ #response;
24
+ #sealed = false;
25
+ #configurationSnapshot;
26
+ #contextSnapshot;
27
+ constructor(input, observe, configuration, context) {
28
+ this.input = input;
29
+ this.#observe = observe;
30
+ this.#configuration = configuration;
31
+ this.#context = context;
32
+ }
33
+ get currentTripwire() {
34
+ return this.#tripwire;
35
+ }
36
+ get currentCandidate() {
37
+ return this.#candidate;
38
+ }
39
+ get selectedDirective() {
40
+ return this.configurationSnapshot().model;
41
+ }
42
+ get instructions() {
43
+ return Object.freeze(this.configurationSnapshot().instructions.map((item) => item.text));
44
+ }
45
+ contextSnapshot() {
46
+ return this.#contextSnapshot ?? this.#context.snapshot();
47
+ }
48
+ get offeredTools() {
49
+ return this.configurationSnapshot().tools;
50
+ }
51
+ get catalogByName() {
52
+ return new Map(this.offeredTools.map((tool) => [tool.name, tool]));
53
+ }
54
+ configurationSnapshot() {
55
+ return this.#configurationSnapshot ?? this.#configuration.snapshot();
56
+ }
57
+ sealConfiguration(snapshot) {
58
+ this.#configurationSnapshot = snapshot;
59
+ }
60
+ sealContext(snapshot) {
61
+ this.#contextSnapshot = snapshot;
62
+ }
63
+ denialFor(callId) {
64
+ return this.#denials.get(callId);
65
+ }
66
+ interactionFor(callId) {
67
+ return this.#interactions.get(callId);
68
+ }
69
+ preflightFor(callId) {
70
+ return this.#preflights.get(callId);
71
+ }
72
+ canonicalCalls() {
73
+ return this.#canonical;
74
+ }
75
+ mintFromModel(candidate) {
76
+ const { output, calls } = canonicalizeOutput(candidate.output);
77
+ this.#canonical = calls;
78
+ this.#candidate = candidateFromCanonical(candidate, output);
79
+ this.#identities.clear();
80
+ for (const call of calls)
81
+ this.#identities.set(call.id, identityKey(call.name, call.args));
82
+ return this.#ensureResponse();
83
+ }
84
+ tripwire(error) {
85
+ this.#tripwire ??= Object.freeze({ ...error });
86
+ return this.#ensureResponse();
87
+ }
88
+ seal() {
89
+ this.#sealed = true;
90
+ }
91
+ requestFacade(middlewareId, middlewareOrder) {
92
+ let mutatorsActive = true;
93
+ const mutate = (action) => {
94
+ if (!mutatorsActive || this.#sealed) {
95
+ this.#observe({
96
+ type: "middleware.lease-violation",
97
+ turnId: this.input.turnId,
98
+ stepId: this.input.stepId,
99
+ middlewareId,
100
+ reason: this.#sealed ? "step-sealed" : "mutators-revoked",
101
+ });
102
+ throw new HarnessError("middleware.request-mutators-revoked", "Middleware request mutators are no longer active", { details: { middlewareId } });
103
+ }
104
+ action();
105
+ };
106
+ const mutateConfiguration = (kind, action) => mutate(() => {
107
+ try {
108
+ action();
109
+ }
110
+ catch (cause) {
111
+ const message = cause instanceof Error ? cause.message : String(cause);
112
+ const code = isHarnessError(cause)
113
+ ? cause.code
114
+ : kind === "tools"
115
+ ? "tool.invalid-schema"
116
+ : kind === "model"
117
+ ? "model.invalid-directive"
118
+ : "configuration.invalid-instructions";
119
+ this.#tripwire ??= Object.freeze({ code, message });
120
+ }
121
+ });
122
+ const mutateContext = (action) => mutate(() => {
123
+ try {
124
+ action();
125
+ }
126
+ catch (cause) {
127
+ const message = cause instanceof Error ? cause.message : String(cause);
128
+ this.#tripwire ??= Object.freeze({
129
+ code: isHarnessError(cause) ? cause.code : "context.invalid-item",
130
+ message,
131
+ });
132
+ }
133
+ });
134
+ const value = Object.freeze({
135
+ sessionId: this.input.sessionId,
136
+ turnId: this.input.turnId,
137
+ stepId: this.input.stepId,
138
+ session: this.input.session,
139
+ turnNumber: this.input.turnNumber,
140
+ stepNumber: this.input.stepNumber,
141
+ arrivals: this.input.arrivals,
142
+ toolResults: this.input.toolResults,
143
+ transcript: this.input.transcript,
144
+ context: Object.freeze({
145
+ set: (slot, items, options) => mutateContext(() => this.#context.set(middlewareId, middlewareOrder, slot, items, options)),
146
+ }),
147
+ configuration: Object.freeze({
148
+ instructions: Object.freeze({
149
+ set: (slot, items, options) => mutateConfiguration("instructions", () => this.#configuration.setInstructions(middlewareId, middlewareOrder, slot, items, options)),
150
+ }),
151
+ tools: Object.freeze({
152
+ set: (slot, tools, options) => mutateConfiguration("tools", () => this.#configuration.setTools(middlewareId, middlewareOrder, slot, tools, options)),
153
+ }),
154
+ model: Object.freeze({
155
+ select: (directive, options) => mutateConfiguration("model", () => this.#configuration.select(middlewareId, middlewareOrder, directive, options)),
156
+ replace: (directive, options) => mutateConfiguration("model", () => this.#configuration.replace(middlewareId, middlewareOrder, directive, options)),
157
+ clear: (options) => mutateConfiguration("model", () => this.#configuration.clear(options)),
158
+ }),
159
+ }),
160
+ tripwire: (error) => {
161
+ let minted;
162
+ mutate(() => {
163
+ minted = this.tripwire(error);
164
+ });
165
+ return minted;
166
+ },
167
+ });
168
+ return Object.freeze({
169
+ value,
170
+ revokeMutators: () => {
171
+ mutatorsActive = false;
172
+ },
173
+ });
174
+ }
175
+ #ensureResponse() {
176
+ if (this.#response)
177
+ return this.#response;
178
+ const value = Object.freeze({
179
+ candidate: () => this.currentCandidate,
180
+ toolCalls: () => callsFromCanonical(this.#canonical),
181
+ replace: (candidate) => this.#replace(candidate),
182
+ deny: (callId, reason) => {
183
+ if (!this.#denials.has(callId))
184
+ this.#denials.set(callId, reason);
185
+ },
186
+ requireInteraction: (callId, interaction) => {
187
+ if (!this.#interactions.has(callId))
188
+ this.#interactions.set(callId, freezeGraph({ ...interaction }));
189
+ },
190
+ requirePreflight: (callId, kind) => {
191
+ if (!this.#preflights.has(callId))
192
+ this.#preflights.set(callId, kind);
193
+ },
194
+ tripwire: (error) => {
195
+ this.#tripwire ??= Object.freeze({ ...error });
196
+ return value;
197
+ },
198
+ });
199
+ branded.add(value);
200
+ this.#response = value;
201
+ return value;
202
+ }
203
+ #replace(candidate) {
204
+ let normalized;
205
+ try {
206
+ normalized = normalizeCandidate(candidate);
207
+ }
208
+ catch (error) {
209
+ this.#tripwire ??= Object.freeze({
210
+ code: isHarnessError(error) ? error.code : "response.invalid-replacement",
211
+ message: error instanceof Error ? error.message : String(error),
212
+ });
213
+ return;
214
+ }
215
+ const { output, calls: next } = canonicalizeOutput(normalized.output);
216
+ for (const call of next) {
217
+ const original = this.#identities.get(call.id);
218
+ if (original === undefined) {
219
+ this.#tripwire ??= Object.freeze({
220
+ code: "response.invalid-replacement",
221
+ message: `replace() cannot add Tool call '${call.id}'`,
222
+ });
223
+ return;
224
+ }
225
+ if (original !== identityKey(call.name, call.args)) {
226
+ this.#tripwire ??= Object.freeze({
227
+ code: "response.invalid-replacement",
228
+ message: `replace() cannot change identity of Tool call '${call.id}'`,
229
+ });
230
+ return;
231
+ }
232
+ }
233
+ const current = this.#candidate;
234
+ this.#canonical = next;
235
+ this.#candidate = Object.freeze({
236
+ output,
237
+ ...pickSidecar("finishReason", candidate, normalized, current),
238
+ ...pickSidecar("usage", candidate, normalized, current),
239
+ ...pickSidecar("evidence", candidate, normalized, current),
240
+ });
241
+ }
242
+ }
243
+ function pickSidecar(key, replacement, normalized, current) {
244
+ const source = Object.hasOwn(replacement, key) ? normalized : current;
245
+ const value = source?.[key];
246
+ return value === undefined ? {} : { [key]: value };
247
+ }
248
+ function freezeGraph(value, seen = new WeakSet()) {
249
+ if (!value || typeof value !== "object" || seen.has(value))
250
+ return value;
251
+ seen.add(value);
252
+ for (const item of Object.values(value))
253
+ freezeGraph(item, seen);
254
+ return Object.freeze(value);
255
+ }