@nylorun/harness 0.11.1-beta → 0.13.0-beta

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 (131) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +124 -5
  3. package/dist/definition/agent-definition.d.ts +11 -0
  4. package/dist/definition/agent-definition.js +12 -0
  5. package/dist/definition/assemble.d.ts +22 -0
  6. package/dist/{build → definition}/assemble.js +6 -7
  7. package/dist/definition/bind-agent.d.ts +9 -0
  8. package/dist/definition/bind-agent.js +21 -0
  9. package/dist/{build → definition}/bind-tool.d.ts +2 -1
  10. package/dist/{build → definition}/bind-tool.js +1 -0
  11. package/dist/definition/bound.d.ts +16 -0
  12. package/dist/definition/builder.d.ts +30 -0
  13. package/dist/definition/builder.js +94 -0
  14. package/dist/definition/declaration.d.ts +3 -0
  15. package/dist/definition/declaration.js +53 -0
  16. package/dist/{build → definition}/helpers.d.ts +1 -1
  17. package/dist/{build → definition}/manifest.d.ts +3 -1
  18. package/dist/definition/manifest.js +45 -0
  19. package/dist/{session → definition}/output-contract.d.ts +2 -2
  20. package/dist/{session → definition}/output-contract.js +1 -1
  21. package/dist/definition/registry.d.ts +10 -0
  22. package/dist/definition/registry.js +48 -0
  23. package/dist/{build → definition}/schema.d.ts +4 -4
  24. package/dist/errors.d.ts +1 -1
  25. package/dist/execution/dispatch.d.ts +2 -0
  26. package/dist/execution/dispatch.js +164 -0
  27. package/dist/execution/initial-state.d.ts +3 -0
  28. package/dist/execution/initial-state.js +14 -0
  29. package/dist/execution/invocation.d.ts +21 -0
  30. package/dist/execution/invocation.js +32 -0
  31. package/dist/{model → execution/model}/adapters.d.ts +2 -2
  32. package/dist/{model → execution/model}/adapters.js +2 -2
  33. package/dist/{model → execution/model}/normalize.d.ts +2 -2
  34. package/dist/{model → execution/model}/normalize.js +2 -2
  35. package/dist/{model → execution/model}/prepared.d.ts +3 -3
  36. package/dist/{step → execution/model}/project.d.ts +1 -1
  37. package/dist/{step → execution/model}/project.js +2 -10
  38. package/dist/execution/observe.d.ts +17 -0
  39. package/dist/execution/observe.js +53 -0
  40. package/dist/execution/options.d.ts +10 -0
  41. package/dist/execution/options.js +49 -0
  42. package/dist/execution/resume.d.ts +4 -0
  43. package/dist/execution/resume.js +52 -0
  44. package/dist/execution/run.d.ts +5 -0
  45. package/dist/execution/run.js +256 -0
  46. package/dist/execution/state.d.ts +8 -0
  47. package/dist/execution/state.js +208 -0
  48. package/dist/{step → execution/step}/canonicalize.d.ts +2 -2
  49. package/dist/{step → execution/step}/canonicalize.js +2 -2
  50. package/dist/{step → execution/step}/compose.d.ts +3 -2
  51. package/dist/{step → execution/step}/compose.js +5 -5
  52. package/dist/{step → execution/step}/context-draft.d.ts +3 -5
  53. package/dist/{step → execution/step}/context-draft.js +5 -22
  54. package/dist/execution/step/describe.d.ts +9 -0
  55. package/dist/execution/step/describe.js +22 -0
  56. package/dist/{step → execution/step}/model-configuration.d.ts +4 -3
  57. package/dist/{step → execution/step}/model-configuration.js +3 -3
  58. package/dist/execution/step/resolve.d.ts +11 -0
  59. package/dist/{step → execution/step}/resolve.js +5 -3
  60. package/dist/execution/step/run.d.ts +29 -0
  61. package/dist/{step → execution/step}/run.js +19 -53
  62. package/dist/execution/step/runtime.d.ts +32 -0
  63. package/dist/execution/step/runtime.js +1 -0
  64. package/dist/{step → execution/step}/seal.d.ts +13 -8
  65. package/dist/{step → execution/step}/seal.js +8 -6
  66. package/dist/{step → execution/step}/slot-assembly.d.ts +4 -4
  67. package/dist/{step → execution/step}/slot-assembly.js +4 -4
  68. package/dist/{step → execution/step}/step-context.d.ts +11 -12
  69. package/dist/{step → execution/step}/step-context.js +5 -27
  70. package/dist/execution/tool-result.d.ts +4 -0
  71. package/dist/execution/tool-result.js +30 -0
  72. package/dist/execution/transcript.d.ts +2 -0
  73. package/dist/{session/seed.js → execution/transcript.js} +9 -39
  74. package/dist/index.d.ts +15 -12
  75. package/dist/index.js +5 -5
  76. package/dist/types/agent.d.ts +9 -0
  77. package/dist/types/agent.js +1 -0
  78. package/dist/types/execution.d.ts +97 -0
  79. package/dist/types/execution.js +1 -0
  80. package/dist/types/manifest.d.ts +16 -13
  81. package/dist/types/middleware.d.ts +11 -40
  82. package/dist/types/model.d.ts +10 -10
  83. package/dist/types/observe.d.ts +165 -0
  84. package/dist/types/observe.js +1 -0
  85. package/dist/types/shared.d.ts +1 -225
  86. package/dist/types/tool.d.ts +21 -19
  87. package/dist/types/transcript.d.ts +65 -0
  88. package/dist/types/transcript.js +1 -0
  89. package/dist/utils/canonical.d.ts +1 -0
  90. package/dist/utils/canonical.js +10 -0
  91. package/dist/utils/immutable.d.ts +1 -0
  92. package/dist/utils/immutable.js +8 -0
  93. package/package.json +5 -5
  94. package/dist/build/agent.d.ts +0 -18
  95. package/dist/build/agent.js +0 -37
  96. package/dist/build/assemble.d.ts +0 -8
  97. package/dist/build/builder.d.ts +0 -46
  98. package/dist/build/builder.js +0 -151
  99. package/dist/build/manifest.js +0 -20
  100. package/dist/session/capability-state.d.ts +0 -14
  101. package/dist/session/capability-state.js +0 -67
  102. package/dist/session/event-log.d.ts +0 -12
  103. package/dist/session/event-log.js +0 -63
  104. package/dist/session/input-queue.d.ts +0 -41
  105. package/dist/session/input-queue.js +0 -94
  106. package/dist/session/record.d.ts +0 -11
  107. package/dist/session/record.js +0 -26
  108. package/dist/session/scheduler.d.ts +0 -62
  109. package/dist/session/scheduler.js +0 -485
  110. package/dist/session/seed.d.ts +0 -10
  111. package/dist/session/session.d.ts +0 -18
  112. package/dist/session/session.js +0 -103
  113. package/dist/session/state.d.ts +0 -15
  114. package/dist/session/state.js +0 -48
  115. package/dist/session/submission-stream.d.ts +0 -16
  116. package/dist/session/submission-stream.js +0 -46
  117. package/dist/step/resolve.d.ts +0 -10
  118. package/dist/step/run.d.ts +0 -33
  119. package/dist/turn/plan-runner.d.ts +0 -70
  120. package/dist/turn/plan-runner.js +0 -388
  121. package/dist/turn/runner.d.ts +0 -69
  122. package/dist/turn/runner.js +0 -158
  123. package/dist/types/session.d.ts +0 -227
  124. package/dist/utils/maps.d.ts +0 -1
  125. package/dist/utils/maps.js +0 -37
  126. package/dist/utils/observe.d.ts +0 -8
  127. package/dist/utils/observe.js +0 -29
  128. /package/dist/{types/session.js → definition/bound.js} +0 -0
  129. /package/dist/{build → definition}/helpers.js +0 -0
  130. /package/dist/{build → definition}/schema.js +0 -0
  131. /package/dist/{model → execution/model}/prepared.js +0 -0
@@ -0,0 +1,48 @@
1
+ import { bindTool } from "./bind-tool.js";
2
+ import { HarnessError } from "../errors.js";
3
+ import { canonical } from "../utils/canonical.js";
4
+ import { copyJson } from "../utils/immutable.js";
5
+ /** Definition-only lookup; no per-run selections or progress live here. */
6
+ export class ToolRegistry {
7
+ tools = new Map();
8
+ owners = new WeakMap();
9
+ constructor(middleware) {
10
+ for (const capability of middleware) {
11
+ for (const tool of capability.tools ?? []) {
12
+ const key = JSON.stringify([capability.id, tool.name]);
13
+ if (this.tools.has(key) || this.owners.has(tool))
14
+ throw new HarnessError("tool.invalid", `Duplicate registered tool '${tool.name}' in '${capability.id}'`);
15
+ this.tools.set(key, bindTool(tool, { middlewareId: capability.id, slot: capability.id }));
16
+ this.owners.set(tool, capability.id);
17
+ }
18
+ }
19
+ }
20
+ reference(tool) {
21
+ const capabilityId = this.owners.get(tool.source);
22
+ if (!capabilityId)
23
+ throw new HarnessError("tool.unregistered", `Tool '${tool.name}' is not registered. Declare it in capability.tools.`);
24
+ const reference = copyJson({
25
+ capabilityId,
26
+ toolName: tool.source.name,
27
+ descriptor: descriptor(tool),
28
+ });
29
+ this.restore(reference);
30
+ return reference;
31
+ }
32
+ restore(reference) {
33
+ if ("familyId" in reference || "binding" in reference || "version" in reference)
34
+ throw new HarnessError("execution.incompatible", "Saved state uses a removed tool-family reference. Register an ordinary tool and start a new execution.");
35
+ const registered = this.tools.get(JSON.stringify([reference.capabilityId, reference.toolName]));
36
+ if (!registered || canonical(descriptor(registered)) !== canonical(reference.descriptor))
37
+ throw new HarnessError("execution.incompatible", `Missing or changed registered tool '${reference.toolName}'`);
38
+ return registered;
39
+ }
40
+ }
41
+ function descriptor(tool) {
42
+ return {
43
+ name: tool.name,
44
+ ...(tool.description === undefined ? {} : { description: tool.description }),
45
+ inputSchema: tool.inputSchema.jsonSchema,
46
+ ...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema.jsonSchema }),
47
+ };
48
+ }
@@ -1,8 +1,8 @@
1
- import type { BoundToolSchema, ToolDefinition, ToolSchema, ToolSchemaSource } from "../types/tool.js";
1
+ import type { ToolDefinition, ToolSchema, ToolSchemaSource } from "../types/tool.js";
2
2
  export type { SchemaValidation } from "../types/tool.js";
3
3
  interface NormalizedToolSchemas {
4
- readonly inputSchema: BoundToolSchema<unknown>;
5
- readonly outputSchema?: BoundToolSchema<unknown>;
4
+ readonly inputSchema: ToolSchema<unknown>;
5
+ readonly outputSchema?: ToolSchema<unknown>;
6
6
  }
7
7
  /** Creates a portable, explicitly validated schema source from raw JSON Schema. */
8
8
  export declare function defineSchema<T>(value: ToolSchema<T>): ToolSchema<T>;
@@ -11,4 +11,4 @@ export declare function prepareTool<T extends ToolDefinition>(definition: T): T;
11
11
  /** Returns cached normalized contracts, preparing raw definitions on first bind. */
12
12
  export declare function normalizedSchemasFor(definition: ToolDefinition): NormalizedToolSchemas;
13
13
  /** Converts an accepted tool schema source into Harness's immutable runtime representation. */
14
- export declare function normalizeSchema<T>(source: ToolSchemaSource<T>, role: "input" | "output"): BoundToolSchema<T>;
14
+ export declare function normalizeSchema<T>(source: ToolSchemaSource<T>, role: "input" | "output"): ToolSchema<T>;
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Machine-readable error raised by Harness-owned code. */
2
- export type HarnessErrorCode = "agent.build-failed" | "agent.lifecycle-sealed" | "capability.state.create-failed" | "capability.state.undeclared" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "input.invalid-content" | "output.invalid" | "output.invalid-schema" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.adapter-invalid-options" | "model.adapter-invalid-response" | "model.invalid-candidate" | "model.invalid-directive" | "model.unsupported-content" | "model.unsupported-output-schema" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "session.invalid-seed" | "session.record-failed" | "session.stale-result" | "tool.invalid" | "tool.invalid-arguments" | "tool.invalid-output" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
2
+ export type HarnessErrorCode = "execution.invalid-state" | "execution.invalid-input" | "execution.incompatible" | "execution.record-failed" | "tool.unregistered" | "tool.invalid-binding" | "agent.build-failed" | "agent.lifecycle-sealed" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "input.invalid-content" | "output.invalid" | "output.invalid-schema" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.adapter-invalid-options" | "model.adapter-invalid-response" | "model.invalid-candidate" | "model.invalid-directive" | "model.unsupported-content" | "model.unsupported-output-schema" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "tool.invalid" | "tool.invalid-arguments" | "tool.invalid-output" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
3
3
  export type HarnessErrorDetails = Readonly<Record<string, string | number | boolean>>;
4
4
  export interface HarnessErrorOptions {
5
5
  readonly cause?: unknown;
@@ -0,0 +1,2 @@
1
+ import type { Invocation } from "./invocation.js";
2
+ export declare function dispatchPlan(invocation: Invocation): Promise<"paused" | undefined>;
@@ -0,0 +1,164 @@
1
+ import { HarnessError, isHarnessError } from "../errors.js";
2
+ import { createId } from "../utils/ids.js";
3
+ import { copyJson } from "../utils/immutable.js";
4
+ import { toolResult } from "./tool-result.js";
5
+ export async function dispatchPlan(invocation) {
6
+ const { signal, definitions, options, observe, record } = invocation;
7
+ let plan = invocation.state.plan;
8
+ if (plan.calls.some((call) => call.status === "interaction" && call.interactionPhase === "before")) {
9
+ for (const call of plan.calls)
10
+ if (call.status === "interaction")
11
+ observe({
12
+ type: "interaction.required",
13
+ turnId: plan.turnId,
14
+ stepId: plan.stepId,
15
+ interactionId: call.interaction.id,
16
+ kind: call.interaction.kind,
17
+ callId: call.callId,
18
+ toolName: call.toolName,
19
+ phase: "interaction",
20
+ attributes: {
21
+ prompt: call.interaction.prompt,
22
+ ...(call.interaction.metadata ? { metadata: call.interaction.metadata } : {}),
23
+ },
24
+ });
25
+ return "paused";
26
+ }
27
+ signal.throwIfAborted();
28
+ const pending = plan.calls.filter((call) => call.status === "pending");
29
+ if (pending.length) {
30
+ invocation.state = {
31
+ ...invocation.state,
32
+ plan: {
33
+ ...plan,
34
+ calls: plan.calls.map((call) => call.status === "pending" ? { ...call, status: "active" } : call),
35
+ },
36
+ };
37
+ await record();
38
+ signal.throwIfAborted();
39
+ const settled = await Promise.all(pending.map(async (call) => {
40
+ const definition = definitions.get(call.invocationId);
41
+ const eventIds = {
42
+ executionId: invocation.state.executionId,
43
+ turnId: plan.turnId,
44
+ stepId: plan.stepId,
45
+ callId: call.callId,
46
+ invocationId: call.invocationId,
47
+ toolName: call.toolName,
48
+ middlewareId: call.reference.capabilityId,
49
+ slot: definition.owner.slot,
50
+ };
51
+ observe({ type: "tool.started", ...eventIds, attributes: { args: call.args } });
52
+ let outcome;
53
+ try {
54
+ signal.throwIfAborted();
55
+ outcome = await definition.execute(call.args, {
56
+ executionId: invocation.state.executionId,
57
+ turnId: plan.turnId,
58
+ stepId: plan.stepId,
59
+ callId: call.callId,
60
+ invocationId: call.invocationId,
61
+ signal,
62
+ info: options.info,
63
+ onModelCall: options.onModelCall,
64
+ ...(call.resume ? { resume: call.resume } : {}),
65
+ });
66
+ if (!outcome || typeof outcome !== "object")
67
+ throw new HarnessError("tool.invalid-tool-result", "Tool returned an invalid outcome");
68
+ if (outcome.kind === "interaction-required") {
69
+ if (!["approval", "response"].includes(outcome.interaction.kind) ||
70
+ typeof outcome.interaction.prompt !== "string")
71
+ throw new HarnessError("tool.invalid-tool-result", "Tool returned an invalid interaction");
72
+ return copyJson({
73
+ ...call,
74
+ status: "interaction",
75
+ interaction: {
76
+ ...outcome.interaction,
77
+ id: outcome.interaction.id ?? createId("interaction"),
78
+ },
79
+ interactionPhase: "execute",
80
+ ...(outcome.token === undefined ? {} : { token: outcome.token }),
81
+ });
82
+ }
83
+ if (outcome.kind === "deferred") {
84
+ observe({
85
+ type: "tool.deferred",
86
+ ...eventIds,
87
+ attributes: outcome.token === undefined ? {} : { token: outcome.token },
88
+ });
89
+ return copyJson({
90
+ ...call,
91
+ status: "deferred",
92
+ ...(outcome.token === undefined ? {} : { token: outcome.token }),
93
+ });
94
+ }
95
+ const result = toolResult(call, definition, outcome);
96
+ observe({
97
+ type: "tool.completed",
98
+ ...eventIds,
99
+ outcome: result.kind,
100
+ attributes: result,
101
+ });
102
+ return copyJson({ ...call, status: "settled", result });
103
+ }
104
+ catch (cause) {
105
+ const result = {
106
+ callId: call.callId,
107
+ toolName: call.toolName,
108
+ kind: "failed",
109
+ code: signal.aborted
110
+ ? "tool.cancelled"
111
+ : isHarnessError(cause)
112
+ ? cause.code
113
+ : "tool.execution-failed",
114
+ message: cause instanceof Error ? cause.message : String(cause),
115
+ };
116
+ observe({
117
+ type: "tool.completed",
118
+ ...eventIds,
119
+ outcome: result.kind,
120
+ attributes: result,
121
+ });
122
+ return copyJson({ ...call, status: "settled", result });
123
+ }
124
+ }));
125
+ const byId = new Map(settled.map((call) => [call.invocationId, call]));
126
+ plan = { ...plan, calls: plan.calls.map((call) => byId.get(call.invocationId) ?? call) };
127
+ invocation.state = { ...invocation.state, plan };
128
+ await record();
129
+ }
130
+ signal.throwIfAborted();
131
+ if (plan.calls.some((call) => call.status === "interaction" || call.status === "deferred")) {
132
+ for (const call of plan.calls)
133
+ if (call.status === "interaction")
134
+ observe({
135
+ type: "interaction.required",
136
+ turnId: plan.turnId,
137
+ stepId: plan.stepId,
138
+ interactionId: call.interaction.id,
139
+ kind: call.interaction.kind,
140
+ callId: call.callId,
141
+ toolName: call.toolName,
142
+ phase: call.interactionPhase === "before" ? "interaction" : "execute",
143
+ attributes: {
144
+ prompt: call.interaction.prompt,
145
+ ...(call.interaction.metadata ? { metadata: call.interaction.metadata } : {}),
146
+ },
147
+ });
148
+ return "paused";
149
+ }
150
+ const results = [
151
+ ...plan.immediate,
152
+ ...plan.calls.flatMap((call) => (call.result ? [call.result] : [])),
153
+ ];
154
+ const ordered = plan.order.map((item) => results.find((result) => result.callId === item.callId && result.toolName === item.toolName));
155
+ const { plan: _, ...rest } = invocation.state;
156
+ invocation.state = {
157
+ ...rest,
158
+ transcript: [
159
+ ...invocation.state.transcript,
160
+ { kind: "tool-results", turnId: plan.turnId, stepId: plan.stepId, results: ordered },
161
+ ],
162
+ };
163
+ await record();
164
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentDefinition } from "../definition/agent-definition.js";
2
+ import type { ExecutionState } from "../types/execution.js";
3
+ export declare function initializeExecutionState(agent: AgentDefinition): ExecutionState;
@@ -0,0 +1,14 @@
1
+ import { copyJson } from "../utils/immutable.js";
2
+ import { createId } from "../utils/ids.js";
3
+ export function initializeExecutionState(agent) {
4
+ return copyJson({
5
+ version: 1,
6
+ agentId: agent.id,
7
+ executionId: createId("execution"),
8
+ revision: 0,
9
+ turnCount: 0,
10
+ transcript: [],
11
+ status: "ready",
12
+ ...(agent.output ? { outputContract: agent.output.schema.jsonSchema } : {}),
13
+ });
14
+ }
@@ -0,0 +1,21 @@
1
+ import type { AgentDefinition } from "../definition/agent-definition.js";
2
+ import type { BoundToolDefinition } from "../definition/bound.js";
3
+ import type { ExecutionState, RunOptions } from "../types/execution.js";
4
+ import type { ObserveEvent } from "../types/observe.js";
5
+ import { type ExecutionOutcomeEvent, type ObserveEmit } from "./observe.js";
6
+ export interface Invocation {
7
+ readonly agent: AgentDefinition;
8
+ readonly options: RunOptions<any>;
9
+ readonly signal: AbortSignal;
10
+ readonly definitions: Map<string, BoundToolDefinition>;
11
+ state: ExecutionState;
12
+ observe: ObserveEmit;
13
+ emit(event: ObserveEvent | ExecutionOutcomeEvent): void;
14
+ record(): Promise<void>;
15
+ }
16
+ export declare function createInvocation(input: {
17
+ readonly agent: AgentDefinition;
18
+ readonly options: RunOptions<any>;
19
+ readonly state: ExecutionState;
20
+ readonly definitions: Map<string, BoundToolDefinition>;
21
+ }): Invocation;
@@ -0,0 +1,32 @@
1
+ import { HarnessError } from "../errors.js";
2
+ import { copyJson } from "../utils/immutable.js";
3
+ import { createId } from "../utils/ids.js";
4
+ import { createObservationSink } from "./observe.js";
5
+ export function createInvocation(input) {
6
+ const invocation = {
7
+ agent: input.agent,
8
+ options: input.options,
9
+ signal: input.options.signal ?? new AbortController().signal,
10
+ definitions: input.definitions,
11
+ state: input.state,
12
+ };
13
+ const sink = createObservationSink({
14
+ listener: input.options.onEvent,
15
+ executionId: () => invocation.state.executionId,
16
+ runId: createId("run"),
17
+ });
18
+ invocation.observe = sink.observe;
19
+ invocation.emit = sink.emit;
20
+ invocation.record = async () => {
21
+ invocation.state = copyJson({ ...invocation.state, revision: invocation.state.revision + 1 });
22
+ if (!input.options.record)
23
+ return;
24
+ try {
25
+ await input.options.record(copyJson(invocation.state));
26
+ }
27
+ catch (cause) {
28
+ throw new HarnessError("execution.record-failed", "Recording failed. Execution stopped; reconcile any external effects before retrying.", { cause });
29
+ }
30
+ };
31
+ return invocation;
32
+ }
@@ -1,5 +1,5 @@
1
- import type { ModelAdapter, ModelAdapterContext, ModelCall, ModelCandidate } from "../types/model.js";
2
- import type { JsonObject } from "../types/shared.js";
1
+ import type { ModelAdapter, ModelAdapterContext, ModelCall, ModelCandidate } from "../../types/model.js";
2
+ import type { JsonObject } from "../../types/shared.js";
3
3
  export type ChatCompletionsMessage = {
4
4
  readonly role: "system";
5
5
  readonly content: string;
@@ -1,5 +1,5 @@
1
- import { HarnessError } from "../errors.js";
2
- import { copyJsonObject } from "../utils/immutable.js";
1
+ import { HarnessError } from "../../errors.js";
2
+ import { copyJsonObject } from "../../utils/immutable.js";
3
3
  import { preparedModel } from "./prepared.js";
4
4
  /** Translate a Harness call to the OpenAI Chat Completions request shape. */
5
5
  export function toChatCompletions(call) {
@@ -1,5 +1,5 @@
1
- import { HarnessError } from "../errors.js";
2
- import type { ModelCandidate, ModelDirective, ModelOutputBlock } from "../types/model.js";
1
+ import { HarnessError } from "../../errors.js";
2
+ import type { ModelCandidate, ModelDirective, ModelOutputBlock } from "../../types/model.js";
3
3
  export declare function normalizeDirective(value: unknown): ModelDirective | HarnessError;
4
4
  export declare function sameDirective(left: ModelDirective, right: ModelDirective): boolean;
5
5
  export declare function textFromOutput(output: readonly ModelOutputBlock[]): string;
@@ -1,5 +1,5 @@
1
- import { HarnessError, isHarnessError } from "../errors.js";
2
- import { assertJson, copyJson, copyJsonObject } from "../utils/immutable.js";
1
+ import { HarnessError, isHarnessError } from "../../errors.js";
2
+ import { assertJson, copyJson, copyJsonObject } from "../../utils/immutable.js";
3
3
  const FINISH_REASONS = new Set([
4
4
  "stop",
5
5
  "length",
@@ -1,5 +1,5 @@
1
- import type { ModelAdapter, ModelAdapterContext, ModelCandidate, ModelCall } from "../types/model.js";
2
- import type { DeferredOutcome, JsonValue } from "../types/shared.js";
1
+ import type { ModelAdapter, ModelAdapterContext, ModelCandidate, ModelCall } from "../../types/model.js";
2
+ import type { JsonValue } from "../../types/shared.js";
3
3
  export interface PreparedModelOptions<Wire> {
4
4
  readonly adapter: string;
5
5
  prepare(call: ModelCall, context: ModelAdapterContext): Promise<{
@@ -7,7 +7,7 @@ export interface PreparedModelOptions<Wire> {
7
7
  readonly observed: JsonValue;
8
8
  }>;
9
9
  send(request: Wire, call: ModelCall, context: ModelAdapterContext): Promise<unknown>;
10
- decode(response: unknown, call: ModelCall): ModelCandidate | string | DeferredOutcome;
10
+ decode(response: unknown, call: ModelCall): ModelCandidate | string;
11
11
  }
12
12
  /**
13
13
  * Builds a ModelAdapter that reports the provider request derived from Harness's canonical call.
@@ -1,2 +1,2 @@
1
- import type { ModelCall, ModelRequest } from "../types/model.js";
1
+ import type { ModelCall, ModelRequest } from "../../types/model.js";
2
2
  export declare function projectModelCall(request: ModelRequest): ModelCall;
@@ -1,4 +1,4 @@
1
- import { copyJson } from "../utils/immutable.js";
1
+ import { copyJson, freezeGraph } from "../../utils/immutable.js";
2
2
  export function projectModelCall(request) {
3
3
  return freezeGraph({
4
4
  prompt: Object.freeze([
@@ -13,7 +13,7 @@ export function projectModelCall(request) {
13
13
  }))),
14
14
  ...(request.model === undefined ? {} : { model: copyJson(request.model) }),
15
15
  ...(request.outputSchema === undefined ? {} : { outputSchema: copyJson(request.outputSchema) }),
16
- sessionId: request.sessionId,
16
+ executionId: request.executionId,
17
17
  });
18
18
  }
19
19
  function projectInstructions(instructions) {
@@ -121,11 +121,3 @@ function freezeItem(item) {
121
121
  content: Object.freeze(item.content.map((part) => Object.freeze(part))),
122
122
  });
123
123
  }
124
- function freezeGraph(value, seen = new WeakSet()) {
125
- if (!value || typeof value !== "object" || seen.has(value))
126
- return value;
127
- seen.add(value);
128
- for (const item of Object.values(value))
129
- freezeGraph(item, seen);
130
- return Object.freeze(value);
131
- }
@@ -0,0 +1,17 @@
1
+ import type { ExecutionEvent } from "../types/execution.js";
2
+ import type { ObserveEvent, Observer } from "../types/observe.js";
3
+ import type { JsonValue } from "../types/shared.js";
4
+ export type ObserveEmit = (event: ObserveEvent | (() => ObserveEvent)) => void;
5
+ export type ExecutionOutcomeEvent = {
6
+ type: "execution.completed" | "execution.paused" | "execution.cancelled" | "execution.failed" | "observation.failed";
7
+ attributes?: Record<string, JsonValue>;
8
+ };
9
+ export declare function createObservationSink(input: {
10
+ readonly listener?: (event: ExecutionEvent) => void | Promise<void>;
11
+ readonly executionId: () => string;
12
+ readonly runId: string;
13
+ }): {
14
+ readonly observe: ObserveEmit;
15
+ readonly emit: (event: ObserveEvent | ExecutionOutcomeEvent) => void;
16
+ };
17
+ export declare function emitObserve(listener: Observer | undefined, event: ObserveEvent | (() => ObserveEvent)): void;
@@ -0,0 +1,53 @@
1
+ import { copyJson } from "../utils/immutable.js";
2
+ export function createObservationSink(input) {
3
+ let sequence = 0;
4
+ let reportingFailure = false;
5
+ const emit = (event) => {
6
+ if (!input.listener)
7
+ return;
8
+ const value = copyJson({
9
+ ...event,
10
+ executionId: input.executionId(),
11
+ runId: input.runId,
12
+ sequence: ++sequence,
13
+ });
14
+ const failed = () => {
15
+ if (reportingFailure || event.type === "observation.failed")
16
+ return;
17
+ reportingFailure = true;
18
+ emit({
19
+ type: "observation.failed",
20
+ attributes: { message: "Execution observer failed; execution is continuing" },
21
+ });
22
+ reportingFailure = false;
23
+ };
24
+ try {
25
+ const result = input.listener(value);
26
+ if (result)
27
+ void Promise.resolve(result).catch(failed);
28
+ }
29
+ catch {
30
+ failed();
31
+ }
32
+ };
33
+ return {
34
+ emit,
35
+ observe(source) {
36
+ if (!input.listener)
37
+ return;
38
+ emit(typeof source === "function" ? source() : source);
39
+ },
40
+ };
41
+ }
42
+ export function emitObserve(listener, event) {
43
+ if (!listener)
44
+ return;
45
+ try {
46
+ const result = listener(typeof event === "function" ? event() : event);
47
+ if (result)
48
+ void Promise.resolve(result).catch(() => undefined);
49
+ }
50
+ catch {
51
+ // Observation is deliberately fail-open for ObserveEvent-only callers.
52
+ }
53
+ }
@@ -0,0 +1,10 @@
1
+ import type { AgentDefinition } from "../definition/agent-definition.js";
2
+ import type { BoundToolDefinition } from "../definition/bound.js";
3
+ import type { ExecutionState, RunOptions } from "../types/execution.js";
4
+ import { normalizeInput } from "./state.js";
5
+ export declare function validateRunOptions(options: RunOptions<any>): void;
6
+ export declare function openExecution(agent: AgentDefinition, options: RunOptions<any>): {
7
+ readonly state: ExecutionState;
8
+ readonly input: ReturnType<typeof normalizeInput>;
9
+ readonly definitions: Map<string, BoundToolDefinition>;
10
+ };
@@ -0,0 +1,49 @@
1
+ import { initializeExecutionState } from "./initial-state.js";
2
+ import { canonical } from "../utils/canonical.js";
3
+ import { HarnessError } from "../errors.js";
4
+ import { normalizeInput, validateExecutionState } from "./state.js";
5
+ const acceptedOptions = new Set([
6
+ "state",
7
+ "input",
8
+ "onModelCall",
9
+ "info",
10
+ "signal",
11
+ "onEvent",
12
+ "record",
13
+ ]);
14
+ export function validateRunOptions(options) {
15
+ if (!options || typeof options.onModelCall !== "function")
16
+ throw new HarnessError("execution.invalid-input", "run() requires input and onModelCall");
17
+ for (const key of Object.keys(options))
18
+ if (!acceptedOptions.has(key))
19
+ throw new HarnessError("execution.invalid-input", `Unknown run option '${key}'`);
20
+ for (const key of ["record", "onEvent"])
21
+ if (options[key] !== undefined && typeof options[key] !== "function")
22
+ throw new HarnessError("execution.invalid-input", `${key} must be a function`);
23
+ if (options.signal !== undefined &&
24
+ (!options.signal ||
25
+ typeof options.signal.throwIfAborted !== "function" ||
26
+ typeof options.signal.addEventListener !== "function"))
27
+ throw new HarnessError("execution.invalid-input", "signal must be an AbortSignal");
28
+ }
29
+ export function openExecution(agent, options) {
30
+ const state = options.state === undefined
31
+ ? initializeExecutionState(agent)
32
+ : validateExecutionState(options.state);
33
+ if (state.agentId !== agent.id ||
34
+ canonical(state.outputContract) !== canonical(agent.output?.schema.jsonSchema))
35
+ throw new HarnessError("execution.incompatible", "Saved state requires the original compatible agent and outputSchema");
36
+ if ((state.status !== "paused" && state.plan !== undefined) ||
37
+ state.status === "active" ||
38
+ state.plan?.calls.some((call) => call.status === "active"))
39
+ throw new HarnessError("execution.invalid-state", "Execution was interrupted with possibly active effects. Reconcile it before starting another run; automatic replay is not supported.");
40
+ const definitions = new Map();
41
+ for (const call of state.plan?.calls ?? []) {
42
+ const definition = agent.registry.restore(call.reference);
43
+ const validation = definition.inputSchema.validate(call.rawArgs);
44
+ if (!validation.ok || canonical(validation.value) !== canonical(call.args))
45
+ throw new HarnessError("execution.incompatible", "Saved tool arguments differ from their accepted contract");
46
+ definitions.set(call.invocationId, definition);
47
+ }
48
+ return { state, input: normalizeInput(options.input), definitions };
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { BoundToolDefinition } from "../definition/bound.js";
2
+ import type { ExecutionState } from "../types/execution.js";
3
+ import type { normalizeInput } from "./state.js";
4
+ export declare function applyResume(state: ExecutionState, input: ReturnType<typeof normalizeInput>, definitions: Map<string, BoundToolDefinition>): ExecutionState;
@@ -0,0 +1,52 @@
1
+ import { HarnessError } from "../errors.js";
2
+ import { copyJson } from "../utils/immutable.js";
3
+ import { toolResult } from "./tool-result.js";
4
+ export function applyResume(state, input, definitions) {
5
+ const plan = state.plan;
6
+ let matched = false;
7
+ const calls = plan.calls.map((call) => {
8
+ if (input.kind === "settle" &&
9
+ call.status === "deferred" &&
10
+ call.invocationId === input.invocationId) {
11
+ matched = true;
12
+ return {
13
+ ...call,
14
+ status: "settled",
15
+ result: toolResult(call, definitions.get(call.invocationId), input.outcome),
16
+ };
17
+ }
18
+ if ((input.kind === "approve" || input.kind === "respond") &&
19
+ call.status === "interaction" &&
20
+ call.interaction.id === input.interactionId) {
21
+ if ((input.kind === "approve") !== (call.interaction.kind === "approval"))
22
+ throw new HarnessError("execution.invalid-input", "Interaction response kind does not match the saved request");
23
+ matched = true;
24
+ const { interaction: _, interactionPhase: __, resume: ___, ...rest } = call;
25
+ if (input.kind === "approve" && !input.approved)
26
+ return {
27
+ ...rest,
28
+ status: "settled",
29
+ result: {
30
+ kind: "denied",
31
+ callId: call.callId,
32
+ toolName: call.toolName,
33
+ reason: "Approval rejected",
34
+ },
35
+ };
36
+ return {
37
+ ...rest,
38
+ status: "pending",
39
+ resume: {
40
+ interactionId: input.interactionId,
41
+ kind: input.kind === "approve" ? "approval" : "response",
42
+ ...(input.kind === "approve" ? { approved: input.approved } : { value: input.value }),
43
+ ...(call.token === undefined ? {} : { token: call.token }),
44
+ },
45
+ };
46
+ }
47
+ return call;
48
+ });
49
+ if (!matched)
50
+ throw new HarnessError("execution.invalid-input", "Input does not match a pending approval, response, or deferred invocation");
51
+ return copyJson({ ...state, plan: { ...plan, calls } });
52
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgentDefinition } from "../definition/agent-definition.js";
2
+ import type { RunOptions, RunResult } from "../types/execution.js";
3
+ import type { JsonValue } from "../types/shared.js";
4
+ /** All mutable progress belongs to this invocation and is discarded when it settles. */
5
+ export declare function execute(agent: AgentDefinition, options: RunOptions<any>): Promise<RunResult<JsonValue>>;