@nylorun/harness 0.5.0-beta.1 → 0.7.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 +45 -0
  2. package/README.md +22 -108
  3. package/dist/build/agent.d.ts +4 -7
  4. package/dist/build/agent.js +12 -7
  5. package/dist/build/assemble.d.ts +2 -6
  6. package/dist/build/assemble.js +4 -47
  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 +5 -8
  10. package/dist/build/builder.js +34 -13
  11. package/dist/build/helpers.d.ts +2 -3
  12. package/dist/build/helpers.js +0 -1
  13. package/dist/build/manifest.d.ts +0 -4
  14. package/dist/build/manifest.js +0 -8
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/index.d.ts +5 -5
  17. package/dist/index.js +1 -1
  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 +0 -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
@@ -13,22 +13,24 @@ export class StepContext {
13
13
  #observe;
14
14
  #context;
15
15
  #configuration;
16
+ #states;
16
17
  #denials = new Map();
17
18
  #interactions = new Map();
18
- #preflights = new Map();
19
19
  #identities = new Map();
20
20
  #canonical = Object.freeze([]);
21
21
  #candidate;
22
22
  #tripwire;
23
+ #modelDeferred;
23
24
  #response;
24
25
  #sealed = false;
25
26
  #configurationSnapshot;
26
27
  #contextSnapshot;
27
- constructor(input, observe, configuration, context) {
28
+ constructor(input, observe, configuration, context, states) {
28
29
  this.input = input;
29
30
  this.#observe = observe;
30
31
  this.#configuration = configuration;
31
32
  this.#context = context;
33
+ this.#states = states;
32
34
  }
33
35
  get currentTripwire() {
34
36
  return this.#tripwire;
@@ -36,6 +38,9 @@ export class StepContext {
36
38
  get currentCandidate() {
37
39
  return this.#candidate;
38
40
  }
41
+ get currentModelDeferred() {
42
+ return this.#modelDeferred;
43
+ }
39
44
  get selectedDirective() {
40
45
  return this.configurationSnapshot().model;
41
46
  }
@@ -66,9 +71,6 @@ export class StepContext {
66
71
  interactionFor(callId) {
67
72
  return this.#interactions.get(callId);
68
73
  }
69
- preflightFor(callId) {
70
- return this.#preflights.get(callId);
71
- }
72
74
  canonicalCalls() {
73
75
  return this.#canonical;
74
76
  }
@@ -81,6 +83,10 @@ export class StepContext {
81
83
  this.#identities.set(call.id, identityKey(call.name, call.args));
82
84
  return this.#ensureResponse();
83
85
  }
86
+ deferModel(active) {
87
+ this.#modelDeferred = active;
88
+ return this.#ensureResponse();
89
+ }
84
90
  tripwire(error) {
85
91
  this.#tripwire ??= Object.freeze({ ...error });
86
92
  return this.#ensureResponse();
@@ -131,7 +137,7 @@ export class StepContext {
131
137
  });
132
138
  }
133
139
  });
134
- const value = Object.freeze({
140
+ const request = {
135
141
  sessionId: this.input.sessionId,
136
142
  turnId: this.input.turnId,
137
143
  stepId: this.input.stepId,
@@ -164,7 +170,13 @@ export class StepContext {
164
170
  });
165
171
  return minted;
166
172
  },
167
- });
173
+ };
174
+ if (this.#states?.has(middlewareId))
175
+ Object.defineProperty(request, "state", {
176
+ enumerable: true,
177
+ get: () => this.#states.get(middlewareId),
178
+ });
179
+ const value = Object.freeze(request);
168
180
  return Object.freeze({
169
181
  value,
170
182
  revokeMutators: () => {
@@ -187,10 +199,6 @@ export class StepContext {
187
199
  if (!this.#interactions.has(callId))
188
200
  this.#interactions.set(callId, freezeGraph({ ...interaction }));
189
201
  },
190
- requirePreflight: (callId, kind) => {
191
- if (!this.#preflights.has(callId))
192
- this.#preflights.set(callId, kind);
193
- },
194
202
  tripwire: (error) => {
195
203
  this.#tripwire ??= Object.freeze({ ...error });
196
204
  return value;
@@ -1,18 +1,30 @@
1
- import type { AdapterRegistry } from "../build/adapters.js";
2
1
  import type { ObserveEmit } from "../utils/observe.js";
3
2
  import type { InputEvent } from "../types/session.js";
4
- import type { RequiredInteraction, ToolResult } from "../types/tool.js";
5
- import type { InternalToolPlan } from "../step/seal.js";
6
- type PlanPhase = "interaction" | "preflight" | "execute";
3
+ import type { ActiveInteractionExecutionRecord, ActiveToolsExecutionRecord } from "../types/session.js";
4
+ import type { RequiredInteraction, ToolExecutionResume, ToolResult } from "../types/tool.js";
5
+ import type { JsonValue } from "../types/shared.js";
6
+ import type { CapabilityStateRegistry } from "../session/capability-state.js";
7
+ import type { ExecutablePlanEntry, InternalToolPlan } from "../step/seal.js";
8
+ export interface DeferredToolCall {
9
+ readonly callId: string;
10
+ readonly toolName: string;
11
+ readonly args: JsonValue;
12
+ readonly invocationId: string;
13
+ readonly owner: ExecutablePlanEntry["owner"];
14
+ readonly token?: JsonValue;
15
+ }
7
16
  export type ToolPlanProgress = {
8
17
  readonly kind: "completed";
9
18
  readonly results: readonly ToolResult[];
10
19
  } | {
11
20
  readonly kind: "interaction-required";
12
21
  readonly interaction: RequiredInteraction;
22
+ } | {
23
+ readonly kind: "deferred";
24
+ readonly settled: readonly ToolResult[];
25
+ readonly deferred: readonly DeferredToolCall[];
13
26
  };
14
27
  export interface ToolPlanRunContext {
15
- readonly adapters: AdapterRegistry;
16
28
  readonly signal: AbortSignal;
17
29
  readonly observe: ObserveEmit;
18
30
  readonly ids: {
@@ -20,15 +32,17 @@ export interface ToolPlanRunContext {
20
32
  readonly turnId: string;
21
33
  readonly stepId: string;
22
34
  };
35
+ readonly states?: CapabilityStateRegistry;
23
36
  }
24
- /** Owns the mutable progress of one sealed tool plan across interaction resumes. */
37
+ /** Owns one sealed plan's deterministic interaction and concurrent execution progress. */
25
38
  export declare class ToolPlanRunner {
26
39
  private readonly plan;
27
40
  private readonly results;
41
+ private readonly deferred;
28
42
  private readonly resumes;
29
- private phase;
30
- private index;
31
43
  private readonly pending;
44
+ private readonly settlementEvents;
45
+ private interactionIndex;
32
46
  private executeStarted;
33
47
  private resumeExecute?;
34
48
  constructor(plan: InternalToolPlan);
@@ -36,20 +50,21 @@ export declare class ToolPlanRunner {
36
50
  get interactionKind(): RequiredInteraction["kind"] | undefined;
37
51
  get interactionCallId(): string | undefined;
38
52
  get interactionToolName(): string | undefined;
39
- get interactionPhase(): PlanPhase | undefined;
40
53
  cancelledResults(reason: string): readonly ToolResult[];
54
+ /** Publishes settlement facts only after their authoritative state was recorded. */
55
+ publishSettlements(observe: ObserveEmit): void;
56
+ activeToolsRecord(turnId: string, stepId: string): ActiveToolsExecutionRecord;
57
+ activeInteractionRecord(turnId: string, stepId: string, resume?: ToolExecutionResume): ActiveInteractionExecutionRecord;
58
+ interactionResume(event: InputEvent): ToolExecutionResume;
41
59
  run(context: ToolPlanRunContext, resume?: InputEvent): Promise<ToolPlanProgress>;
42
60
  private acceptResume;
43
- private runExecute;
44
- private preflight;
45
61
  private executeBatch;
46
62
  private execute;
47
63
  private takeResume;
48
- private requireInteraction;
49
64
  private enqueueInteraction;
50
65
  private interactionRequired;
51
66
  private orderedResults;
67
+ private orderedSettledResults;
52
68
  private resultsInOrder;
53
69
  private throwIfAborted;
54
70
  }
55
- export {};
@@ -1,14 +1,15 @@
1
1
  import { HarnessError, isHarnessError } from "../errors.js";
2
2
  import { createId } from "../utils/ids.js";
3
3
  import { copyJson, copyJsonObject } from "../utils/immutable.js";
4
- /** Owns the mutable progress of one sealed tool plan across interaction resumes. */
4
+ /** Owns one sealed plan's deterministic interaction and concurrent execution progress. */
5
5
  export class ToolPlanRunner {
6
6
  plan;
7
7
  results;
8
+ deferred = new Map();
8
9
  resumes = new Map();
9
- phase = "interaction";
10
- index = 0;
11
10
  pending = [];
11
+ settlementEvents = [];
12
+ interactionIndex = 0;
12
13
  executeStarted = false;
13
14
  resumeExecute;
14
15
  constructor(plan) {
@@ -23,65 +24,104 @@ export class ToolPlanRunner {
23
24
  }
24
25
  get interactionCallId() {
25
26
  const pending = this.pending[0];
26
- if (!pending)
27
- return undefined;
28
- return this.plan.executable[pending.index]?.call.callId;
27
+ return pending ? this.plan.executable[pending.index]?.call.callId : undefined;
29
28
  }
30
29
  get interactionToolName() {
31
30
  const pending = this.pending[0];
32
- if (!pending)
33
- return undefined;
34
- return this.plan.executable[pending.index]?.call.toolName;
35
- }
36
- get interactionPhase() {
37
- return this.pending[0]?.phase;
31
+ return pending ? this.plan.executable[pending.index]?.call.toolName : undefined;
38
32
  }
39
33
  cancelledResults(reason) {
40
34
  return this.resultsInOrder("tool.cancelled", reason);
41
35
  }
36
+ /** Publishes settlement facts only after their authoritative state was recorded. */
37
+ publishSettlements(observe) {
38
+ for (const event of this.settlementEvents.splice(0))
39
+ observe(event);
40
+ }
41
+ activeToolsRecord(turnId, stepId) {
42
+ return Object.freeze({
43
+ kind: "tools",
44
+ turnId,
45
+ stepId,
46
+ calls: Object.freeze(this.plan.executable.map((entry) => {
47
+ const result = this.results.get(entry.call.callId);
48
+ const deferred = this.deferred.get(entry.call.callId);
49
+ return Object.freeze({
50
+ callId: entry.call.callId,
51
+ toolName: entry.call.toolName,
52
+ args: copyJson(entry.call.args),
53
+ invocationId: entry.invocationId,
54
+ owner: entry.owner,
55
+ status: result
56
+ ? "settled"
57
+ : deferred
58
+ ? "deferred"
59
+ : "pending",
60
+ ...(result ? { result } : {}),
61
+ ...(deferred?.token === undefined ? {} : { token: deferred.token }),
62
+ });
63
+ })),
64
+ });
65
+ }
66
+ activeInteractionRecord(turnId, stepId, resume) {
67
+ const pending = this.pending[0];
68
+ const entry = pending ? this.plan.executable[pending.index] : undefined;
69
+ if (!pending || !entry)
70
+ throw new HarnessError("interaction.invalid", "Missing pending interaction state");
71
+ return Object.freeze({
72
+ kind: "interaction",
73
+ turnId,
74
+ stepId,
75
+ callId: entry.call.callId,
76
+ toolName: entry.call.toolName,
77
+ interaction: pending.interaction,
78
+ ...(pending.token === undefined ? {} : { token: pending.token }),
79
+ ...(resume === undefined ? {} : { resume }),
80
+ tools: this.activeToolsRecord(turnId, stepId),
81
+ });
82
+ }
83
+ interactionResume(event) {
84
+ const pending = this.pending[0];
85
+ if (!pending)
86
+ throw new HarnessError("interaction.invalid", "Missing pending interaction state");
87
+ return resumeValue(event, pending);
88
+ }
42
89
  async run(context, resume) {
43
90
  this.acceptResume(resume);
44
- while (true) {
91
+ while (this.interactionIndex < this.plan.executable.length) {
45
92
  this.throwIfAborted(context.signal);
46
- if (this.phase === "execute")
47
- return this.runExecute(context);
48
- if (this.index >= this.plan.executable.length) {
49
- if (this.phase === "interaction") {
50
- this.phase = "preflight";
51
- this.index = 0;
52
- continue;
53
- }
54
- if (this.phase === "preflight") {
55
- this.phase = "execute";
56
- this.index = 0;
57
- continue;
58
- }
59
- return { kind: "completed", results: this.orderedResults() };
60
- }
61
- const entry = this.plan.executable[this.index];
93
+ const entry = this.plan.executable[this.interactionIndex];
62
94
  if (this.results.has(entry.call.callId)) {
63
- this.index += 1;
95
+ this.interactionIndex += 1;
64
96
  continue;
65
97
  }
66
- if (this.phase === "interaction") {
67
- if (!entry.interaction) {
68
- this.index += 1;
69
- continue;
70
- }
71
- return this.requireInteraction({
72
- phase: this.phase,
73
- index: this.index,
74
- interaction: entry.interaction,
75
- });
76
- }
77
- const adapter = context.adapters.require(entry.call.executeWith);
78
- if (this.phase === "preflight") {
79
- const progress = await this.preflight(entry, adapter, context);
80
- if (progress)
81
- return progress;
98
+ if (entry.interaction) {
99
+ this.enqueueInteraction({ index: this.interactionIndex, interaction: entry.interaction });
100
+ return this.interactionRequired();
82
101
  }
83
- this.index += 1;
102
+ this.interactionIndex += 1;
84
103
  }
104
+ if (this.resumeExecute) {
105
+ const entry = this.resumeExecute;
106
+ this.resumeExecute = undefined;
107
+ await this.executeBatch([entry], context);
108
+ }
109
+ else if (!this.executeStarted) {
110
+ this.executeStarted = true;
111
+ await this.executeBatch(this.plan.executable.filter((entry) => !this.results.has(entry.call.callId)), context);
112
+ }
113
+ if (this.pending.length)
114
+ return this.interactionRequired();
115
+ if (this.deferred.size)
116
+ return {
117
+ kind: "deferred",
118
+ settled: this.orderedSettledResults(),
119
+ deferred: Object.freeze(this.plan.executable.flatMap((entry) => {
120
+ const value = this.deferred.get(entry.call.callId);
121
+ return value ? [value] : [];
122
+ })),
123
+ };
124
+ return { kind: "completed", results: this.orderedResults() };
85
125
  }
86
126
  acceptResume(resume) {
87
127
  const pending = this.pending.shift();
@@ -98,162 +138,100 @@ export class ToolPlanRunner {
98
138
  kind: "denied",
99
139
  reason: "Approval rejected",
100
140
  }));
101
- this.index += 1;
102
- return;
103
- }
104
- if (pending.phase === "interaction") {
105
- this.resumes.set(entry.call.callId, value);
106
- this.index += 1;
141
+ this.interactionIndex = Math.max(this.interactionIndex, pending.index + 1);
107
142
  return;
108
143
  }
109
- this.resumes.set(`${pending.phase}:${entry.call.callId}`, value);
110
- if (pending.phase === "execute")
144
+ this.resumes.set(entry.call.callId, value);
145
+ if (this.executeStarted)
111
146
  this.resumeExecute = entry;
147
+ else
148
+ this.interactionIndex = Math.max(this.interactionIndex, pending.index + 1);
112
149
  }
113
- async runExecute(context) {
114
- if (this.resumeExecute) {
115
- const entry = this.resumeExecute;
116
- this.resumeExecute = undefined;
117
- await this.executeBatch([entry], context);
118
- }
119
- else if (!this.executeStarted) {
120
- this.executeStarted = true;
121
- await this.executeBatch(this.plan.executable.filter((entry) => !this.results.has(entry.call.callId)), context);
150
+ async executeBatch(entries, context) {
151
+ const settled = await Promise.allSettled(entries.map((entry) => this.execute(entry, context)));
152
+ if (context.signal.aborted)
153
+ throw context.signal.reason;
154
+ for (const outcome of settled) {
155
+ if (outcome.status === "rejected")
156
+ throw outcome.reason;
157
+ if (outcome.value)
158
+ this.enqueueInteraction(outcome.value);
122
159
  }
123
- return this.pending.length
124
- ? this.interactionRequired()
125
- : { kind: "completed", results: this.orderedResults() };
126
160
  }
127
- async preflight(entry, adapter, context) {
128
- if (!entry.preflight)
129
- return undefined;
130
- if (!adapter.preflight) {
131
- this.results.set(entry.call.callId, failed(entry, "tool.preflight-missing", `Adapter '${entry.call.executeWith}' does not implement preflight`));
132
- return undefined;
133
- }
161
+ async execute(entry, context) {
134
162
  context.observe({
135
- type: "adapter.preflight.started",
163
+ type: "tool.started",
164
+ sessionId: context.ids.sessionId,
136
165
  turnId: context.ids.turnId,
137
166
  stepId: context.ids.stepId,
138
- adapterId: adapter.id,
139
167
  toolName: entry.call.toolName,
140
168
  callId: entry.call.callId,
141
169
  invocationId: entry.invocationId,
170
+ middlewareId: entry.owner.middlewareId,
171
+ slot: entry.owner.slot,
142
172
  attributes: { args: copyJson(entry.call.args) },
143
173
  });
144
174
  try {
145
- const outcome = await adapter.preflight(entry.call, {
146
- kind: entry.preflight,
147
- invocationId: entry.invocationId,
148
- signal: context.signal,
149
- resume: this.takeResume("preflight", entry.call.callId),
150
- });
151
- this.throwIfAborted(context.signal);
152
- if (outcome.kind === "interaction-required")
153
- return this.requireInteraction(interactionBarrier(this.phase, this.index, outcome));
154
- if (outcome.kind !== "completed")
155
- this.results.set(entry.call.callId, resultFrom(entry, outcome));
156
- context.observe({
157
- type: "adapter.preflight.completed",
175
+ const toolContext = {
176
+ sessionId: context.ids.sessionId,
158
177
  turnId: context.ids.turnId,
159
178
  stepId: context.ids.stepId,
160
- adapterId: adapter.id,
161
- toolName: entry.call.toolName,
162
179
  callId: entry.call.callId,
163
- outcome: outcome.kind,
164
- ...adapterCompleted(this.results.get(entry.call.callId)),
165
- });
166
- }
167
- catch (error) {
180
+ invocationId: entry.invocationId,
181
+ signal: context.signal,
182
+ resume: this.takeResume(entry.call.callId),
183
+ };
184
+ if (context.states?.has(entry.owner.middlewareId))
185
+ Object.defineProperty(toolContext, "state", {
186
+ enumerable: true,
187
+ get: () => context.states.get(entry.owner.middlewareId),
188
+ });
189
+ const outcome = await entry.execute(entry.call.args, Object.freeze(toolContext));
168
190
  this.throwIfAborted(context.signal);
169
- const result = failed(entry, isHarnessError(error) ? error.code : "tool.preflight-failed", error);
170
- this.results.set(entry.call.callId, result);
171
- context.observe({
172
- type: "adapter.preflight.completed",
173
- turnId: context.ids.turnId,
174
- stepId: context.ids.stepId,
175
- adapterId: adapter.id,
176
- toolName: entry.call.toolName,
177
- callId: entry.call.callId,
178
- outcome: "failed",
179
- ...adapterCompleted(result),
180
- });
181
- }
182
- return undefined;
183
- }
184
- async executeBatch(entries, context) {
185
- const settled = await Promise.allSettled(entries.map((entry) => this.execute(entry, context)));
186
- if (context.signal.aborted)
187
- throw context.signal.reason;
188
- for (const outcome of settled) {
189
- if (outcome.status === "rejected")
190
- throw outcome.reason;
191
- if (outcome.value)
192
- this.enqueueInteraction(outcome.value);
193
- }
194
- }
195
- async execute(entry, context) {
196
- const adapter = context.adapters.require(entry.call.executeWith);
197
- try {
198
- return await context.adapters.execute(adapter.id, context.signal, async () => {
199
- context.observe({
200
- type: "adapter.started",
201
- turnId: context.ids.turnId,
202
- stepId: context.ids.stepId,
203
- adapterId: adapter.id,
204
- toolName: entry.call.toolName,
191
+ if (!outcome || typeof outcome !== "object" || typeof outcome.kind !== "string")
192
+ throw new HarnessError("tool.invalid-tool-result", "Tool returned an invalid outcome");
193
+ if (outcome.kind === "interaction-required")
194
+ return interactionBarrier(this.plan.executable.indexOf(entry), outcome);
195
+ if (outcome.kind === "deferred") {
196
+ const value = Object.freeze({
205
197
  callId: entry.call.callId,
198
+ toolName: entry.call.toolName,
199
+ args: copyJson(entry.call.args),
206
200
  invocationId: entry.invocationId,
207
- attributes: { args: copyJson(entry.call.args) },
208
- });
209
- const outcome = await adapter.execute(entry.call, {
210
- invocationId: entry.invocationId,
211
- signal: context.signal,
212
- resume: this.takeResume("execute", entry.call.callId),
201
+ owner: entry.owner,
202
+ ...(outcome.token === undefined ? {} : { token: copyJson(outcome.token) }),
213
203
  });
214
- this.throwIfAborted(context.signal);
215
- if (outcome.kind === "interaction-required")
216
- return interactionBarrier("execute", this.plan.executable.indexOf(entry), outcome);
217
- this.results.set(entry.call.callId, resultFrom(entry, outcome));
218
- context.observe({
219
- type: "adapter.completed",
204
+ this.deferred.set(entry.call.callId, value);
205
+ this.settlementEvents.push({
206
+ type: "tool.deferred",
207
+ sessionId: context.ids.sessionId,
220
208
  turnId: context.ids.turnId,
221
209
  stepId: context.ids.stepId,
222
- adapterId: adapter.id,
223
210
  toolName: entry.call.toolName,
224
211
  callId: entry.call.callId,
225
- outcome: outcome.kind,
226
- ...adapterCompleted(this.results.get(entry.call.callId)),
212
+ invocationId: entry.invocationId,
213
+ middlewareId: entry.owner.middlewareId,
214
+ slot: entry.owner.slot,
215
+ attributes: outcome.token === undefined ? {} : { token: copyJson(outcome.token) },
227
216
  });
228
217
  return undefined;
229
- });
218
+ }
219
+ const result = resultFrom(entry, outcome);
220
+ this.results.set(entry.call.callId, result);
221
+ this.settlementEvents.push(completedEvent(entry, context, result));
230
222
  }
231
223
  catch (error) {
232
224
  this.throwIfAborted(context.signal);
233
225
  const result = failed(entry, isHarnessError(error) ? error.code : "tool.execution-failed", error);
234
226
  this.results.set(entry.call.callId, result);
235
- context.observe({
236
- type: "adapter.completed",
237
- turnId: context.ids.turnId,
238
- stepId: context.ids.stepId,
239
- adapterId: adapter.id,
240
- toolName: entry.call.toolName,
241
- callId: entry.call.callId,
242
- outcome: "failed",
243
- ...adapterCompleted(result),
244
- });
227
+ this.settlementEvents.push(completedEvent(entry, context, result));
245
228
  }
246
229
  return undefined;
247
230
  }
248
- takeResume(phase, callId) {
249
- const phased = `${phase}:${callId}`;
250
- const resume = this.resumes.get(phased) ?? this.resumes.get(callId);
251
- this.resumes.delete(phased);
252
- return resume;
253
- }
254
- requireInteraction(pending) {
255
- this.enqueueInteraction(pending);
256
- return this.interactionRequired();
231
+ takeResume(callId) {
232
+ const value = this.resumes.get(callId);
233
+ this.resumes.delete(callId);
234
+ return value;
257
235
  }
258
236
  enqueueInteraction(pending) {
259
237
  const position = this.pending.findIndex((item) => item.index > pending.index);
@@ -271,24 +249,23 @@ export class ToolPlanRunner {
271
249
  orderedResults() {
272
250
  return this.resultsInOrder("tool.missing-result", "Tool call did not produce a result");
273
251
  }
252
+ orderedSettledResults() {
253
+ return Object.freeze(this.plan.order.flatMap(({ callId }) => {
254
+ const result = this.results.get(callId);
255
+ return result ? [result] : [];
256
+ }));
257
+ }
274
258
  resultsInOrder(code, message) {
275
259
  return Object.freeze(this.plan.order.map(({ callId, toolName }) => this.results.get(callId) ??
276
- Object.freeze({
277
- callId,
278
- toolName,
279
- kind: "failed",
280
- code,
281
- message,
282
- })));
260
+ Object.freeze({ kind: "failed", callId, toolName, code, message })));
283
261
  }
284
262
  throwIfAborted(signal) {
285
263
  if (signal.aborted)
286
264
  throw signal.reason;
287
265
  }
288
266
  }
289
- function interactionBarrier(phase, index, outcome) {
267
+ function interactionBarrier(index, outcome) {
290
268
  return {
291
- phase,
292
269
  index,
293
270
  interaction: normalizeInteraction(outcome.interaction),
294
271
  ...(outcome.token === undefined ? {} : { token: copyJson(outcome.token) }),
@@ -347,15 +324,21 @@ function resultFrom(entry, outcome) {
347
324
  code: outcome.code,
348
325
  message: outcome.message,
349
326
  });
350
- default:
351
- throw new HarnessError("adapter.invalid-outcome", "Tool adapter returned an invalid outcome");
352
327
  }
353
328
  }
354
- function adapterCompleted(result) {
355
- if (!result)
356
- return {};
329
+ function completedEvent(entry, context, result) {
357
330
  return {
358
- ...(result.code === undefined ? {} : { code: result.code }),
331
+ type: "tool.completed",
332
+ sessionId: context.ids.sessionId,
333
+ turnId: context.ids.turnId,
334
+ stepId: context.ids.stepId,
335
+ toolName: entry.call.toolName,
336
+ callId: entry.call.callId,
337
+ invocationId: entry.invocationId,
338
+ middlewareId: entry.owner.middlewareId,
339
+ slot: entry.owner.slot,
340
+ outcome: result.kind,
341
+ ...(result.kind === "failed" ? { code: result.code } : {}),
359
342
  attributes: copyJson(result),
360
343
  };
361
344
  }