@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,78 @@
1
+ import { copyJson, copyJsonObject } from "../utils/immutable.js";
2
+ import { SubmissionStream } from "./submission-stream.js";
3
+ export function isInteractionReply(event) {
4
+ return event.kind === "approve" || event.kind === "respond";
5
+ }
6
+ export function snapshotInput(event) {
7
+ switch (event.kind) {
8
+ case "user-message":
9
+ case "interrupt":
10
+ return Object.freeze({
11
+ kind: event.kind,
12
+ text: event.text,
13
+ ...(event.metadata ? { metadata: copyJsonObject(event.metadata, "input metadata") } : {}),
14
+ });
15
+ case "approve":
16
+ return Object.freeze({ ...event });
17
+ case "respond":
18
+ return Object.freeze({ ...event, value: copyJson(event.value) });
19
+ }
20
+ }
21
+ /** Serializes ordinary input while allowing a matching interaction reply to resume immediately. */
22
+ export class InputQueue {
23
+ values = [];
24
+ get size() {
25
+ return this.values.length;
26
+ }
27
+ add(input, priority = false) {
28
+ if (priority)
29
+ this.values.unshift(input);
30
+ else
31
+ this.values.push(input);
32
+ }
33
+ take(waitingForInteraction) {
34
+ const next = this.values[0];
35
+ if (!next || (waitingForInteraction && !isInteractionReply(next.event)))
36
+ return undefined;
37
+ return this.values.shift();
38
+ }
39
+ takeInterrupts() {
40
+ const claimed = [];
41
+ for (let index = 0; index < this.values.length;) {
42
+ const item = this.values[index];
43
+ if (item.event.kind !== "interrupt") {
44
+ index += 1;
45
+ continue;
46
+ }
47
+ this.values.splice(index, 1);
48
+ if (!item.cancelled)
49
+ claimed.push(item);
50
+ }
51
+ return claimed;
52
+ }
53
+ remove(input) {
54
+ const index = this.values.indexOf(input);
55
+ if (index < 0)
56
+ return false;
57
+ this.values.splice(index, 1);
58
+ return true;
59
+ }
60
+ drain() {
61
+ return this.values.splice(0);
62
+ }
63
+ }
64
+ /** Binds one caller-provided signal to its queue entry and always removes the listener at completion. */
65
+ export function watchInputAbort(input, handlers) {
66
+ const signal = input.options?.signal;
67
+ if (!signal)
68
+ return;
69
+ const onAbort = () => {
70
+ input.cancelled = true;
71
+ if (handlers.isActive())
72
+ handlers.abortActive(signal.reason);
73
+ else
74
+ handlers.cancelQueued();
75
+ };
76
+ signal.addEventListener("abort", onAbort, { once: true });
77
+ input.stream.onFinish(() => signal.removeEventListener("abort", onAbort));
78
+ }
@@ -0,0 +1,48 @@
1
+ import type { InputEvent, InputOptions, SessionSnapshot } from "../types/session.js";
2
+ import type { Observer } from "../types/shared.js";
3
+ import { SessionEventLog } from "./event-log.js";
4
+ import { SubmissionStream } from "./submission-stream.js";
5
+ import { type LoopAgent } from "../build/agent.js";
6
+ /** Coordinates one active turn at a time; TurnRunner owns the turn's internal state machine. */
7
+ export declare class SessionScheduler {
8
+ readonly id: string;
9
+ readonly events: SessionEventLog;
10
+ private snapshotValue;
11
+ private readonly queue;
12
+ private readonly sessionController;
13
+ private activeController?;
14
+ private activeSubmission?;
15
+ private activeWork?;
16
+ private pending?;
17
+ private inFlightPlan?;
18
+ private running;
19
+ private stopped;
20
+ private generation;
21
+ private stopPromise?;
22
+ private readonly observers;
23
+ private readonly turns;
24
+ constructor(id: string, agent: LoopAgent, session: Readonly<{
25
+ readonly userId?: string;
26
+ readonly context?: import("../types/shared.js").JsonObject;
27
+ }>);
28
+ get snapshot(): SessionSnapshot;
29
+ observe(listener: Observer): () => void;
30
+ submit(event: InputEvent, options?: InputOptions): SubmissionStream;
31
+ stop(reason?: string): Promise<void>;
32
+ private enqueueReply;
33
+ private enqueueOrdinaryInput;
34
+ private watchAbort;
35
+ private reject;
36
+ private finishCancelled;
37
+ private finishStopped;
38
+ private publish;
39
+ private pump;
40
+ private run;
41
+ private resumeTurn;
42
+ private applyTurnOutcome;
43
+ private beginStop;
44
+ private emitObserve;
45
+ private commitCancelledPlan;
46
+ private claimInterrupts;
47
+ private assertCurrent;
48
+ }
@@ -0,0 +1,352 @@
1
+ import { HarnessError } from "../errors.js";
2
+ import { createId } from "../utils/ids.js";
3
+ import { createObserverRegistry } from "../utils/observe.js";
4
+ import { InputQueue, isInteractionReply, snapshotInput, watchInputAbort, } from "./input-queue.js";
5
+ import { SessionEventLog } from "./event-log.js";
6
+ import { SubmissionStream } from "./submission-stream.js";
7
+ import { commitToolResults, initialState, withStatus } from "./state.js";
8
+ import {} from "../build/agent.js";
9
+ import { TurnRunner } from "../turn/runner.js";
10
+ /** Coordinates one active turn at a time; TurnRunner owns the turn's internal state machine. */
11
+ export class SessionScheduler {
12
+ id;
13
+ events = new SessionEventLog();
14
+ snapshotValue;
15
+ queue = new InputQueue();
16
+ sessionController = new AbortController();
17
+ activeController;
18
+ activeSubmission;
19
+ activeWork;
20
+ pending;
21
+ inFlightPlan;
22
+ running = false;
23
+ stopped = false;
24
+ generation = 0;
25
+ stopPromise;
26
+ observers = createObserverRegistry();
27
+ turns;
28
+ constructor(id, agent, session) {
29
+ this.id = id;
30
+ this.snapshotValue = initialState(id);
31
+ this.turns = new TurnRunner(agent, id, session);
32
+ }
33
+ get snapshot() {
34
+ return this.snapshotValue;
35
+ }
36
+ observe(listener) {
37
+ if (this.stopped)
38
+ return () => undefined;
39
+ return this.observers.observe(listener);
40
+ }
41
+ submit(event, options) {
42
+ const stream = new SubmissionStream(createId("input"));
43
+ const submission = {
44
+ event: snapshotInput(event),
45
+ options,
46
+ stream,
47
+ cancelled: false,
48
+ };
49
+ if (this.stopped)
50
+ return this.finishStopped(stream);
51
+ if (options?.signal?.aborted)
52
+ return this.finishCancelled(stream, cancellationMessage(options.signal));
53
+ if (isInteractionReply(event)) {
54
+ if (!this.enqueueReply(submission, event))
55
+ return stream;
56
+ }
57
+ else
58
+ this.enqueueOrdinaryInput(submission);
59
+ this.watchAbort(submission);
60
+ this.emitObserve({
61
+ type: "input.received",
62
+ inputId: stream.inputId,
63
+ kind: event.kind,
64
+ });
65
+ this.pump();
66
+ return stream;
67
+ }
68
+ stop(reason = "Session stopped") {
69
+ if (this.stopPromise)
70
+ return this.stopPromise;
71
+ this.beginStop(reason);
72
+ const activeWork = this.activeWork;
73
+ this.stopPromise = (async () => {
74
+ if (activeWork)
75
+ await activeWork;
76
+ })();
77
+ return this.stopPromise;
78
+ }
79
+ enqueueReply(submission, reply) {
80
+ if (!this.pending) {
81
+ this.reject(submission.stream, "No interaction is pending");
82
+ return false;
83
+ }
84
+ const expected = this.pending.plan.interactionId;
85
+ if (expected !== reply.interactionId) {
86
+ this.reject(submission.stream, "Interaction id does not match the pending interaction");
87
+ return false;
88
+ }
89
+ if ((this.pending.plan.interactionKind === "approval") !== (reply.kind === "approve")) {
90
+ this.reject(submission.stream, "Interaction input kind does not match");
91
+ return false;
92
+ }
93
+ this.queue.add(submission, true);
94
+ return true;
95
+ }
96
+ enqueueOrdinaryInput(submission) {
97
+ if (this.running || this.pending || this.queue.size) {
98
+ this.publish(submission.stream, {
99
+ type: "input.queued",
100
+ inputId: submission.stream.inputId,
101
+ });
102
+ this.emitObserve({
103
+ type: "input.queued",
104
+ inputId: submission.stream.inputId,
105
+ });
106
+ }
107
+ this.queue.add(submission);
108
+ }
109
+ watchAbort(submission) {
110
+ watchInputAbort(submission, {
111
+ isActive: () => this.activeSubmission === submission,
112
+ abortActive: (reason) => this.activeController?.abort(reason),
113
+ cancelQueued: () => {
114
+ const signal = submission.options?.signal;
115
+ if (signal && this.queue.remove(submission))
116
+ this.finishCancelled(submission.stream, cancellationMessage(signal));
117
+ },
118
+ });
119
+ }
120
+ reject(stream, reason) {
121
+ this.publish(stream, { type: "input.rejected", inputId: stream.inputId, reason });
122
+ this.emitObserve({
123
+ type: "input.rejected",
124
+ inputId: stream.inputId,
125
+ reason,
126
+ });
127
+ stream.finish("rejected");
128
+ return stream;
129
+ }
130
+ finishCancelled(stream, reason) {
131
+ this.publish(stream, { type: "input.cancelled", inputId: stream.inputId, reason });
132
+ this.emitObserve({
133
+ type: "input.cancelled",
134
+ inputId: stream.inputId,
135
+ reason,
136
+ });
137
+ stream.finish("cancelled");
138
+ return stream;
139
+ }
140
+ finishStopped(stream) {
141
+ this.publish(stream, { type: "session.stopped", sessionId: this.id });
142
+ stream.finish("stopped");
143
+ return stream;
144
+ }
145
+ publish(stream, event) {
146
+ stream.emit(event);
147
+ this.events.emit(event);
148
+ }
149
+ pump() {
150
+ if (this.running || this.stopped)
151
+ return;
152
+ const next = this.queue.take(Boolean(this.pending));
153
+ if (!next)
154
+ return;
155
+ if (next.cancelled) {
156
+ this.pump();
157
+ return;
158
+ }
159
+ // This Scheduler is the sole serializing point: only one turn can mutate a Session at once.
160
+ this.running = true;
161
+ this.activeSubmission = next;
162
+ this.activeController = new AbortController();
163
+ const generation = ++this.generation;
164
+ const signal = AbortSignal.any([
165
+ this.sessionController.signal,
166
+ this.activeController.signal,
167
+ ...(next.options?.signal ? [next.options.signal] : []),
168
+ ]);
169
+ const work = this.run(next, signal, generation).finally(() => {
170
+ this.running = false;
171
+ this.activeSubmission = undefined;
172
+ this.activeController = undefined;
173
+ if (this.activeWork === work)
174
+ this.activeWork = undefined;
175
+ if (!this.stopped && !this.pending)
176
+ this.snapshotValue = withStatus(this.snapshotValue, "idle");
177
+ this.pump();
178
+ });
179
+ this.activeWork = work;
180
+ void work;
181
+ }
182
+ async run(submission, signal, generation) {
183
+ try {
184
+ const context = {
185
+ signal,
186
+ observe: ((event) => this.emitObserve(() => withInputId(typeof event === "function" ? event() : event, submission.stream.inputId))),
187
+ assertCurrent: () => this.assertCurrent(generation, signal),
188
+ onPlanActive: (plan) => {
189
+ this.inFlightPlan = plan;
190
+ },
191
+ onState: (state) => {
192
+ this.snapshotValue = state;
193
+ },
194
+ onConversation: (event) => this.publish(submission.stream, event),
195
+ claimInterrupts: (turnId) => this.claimInterrupts(turnId),
196
+ };
197
+ const outcome = this.pending
198
+ ? await this.resumeTurn(submission, context)
199
+ : await this.turns.start(this.snapshotValue, submission.event, context);
200
+ this.applyTurnOutcome(submission.stream, outcome);
201
+ }
202
+ catch (error) {
203
+ if (this.inFlightPlan) {
204
+ this.commitCancelledPlan(this.inFlightPlan, message(error));
205
+ this.inFlightPlan = undefined;
206
+ }
207
+ if (this.stopped)
208
+ this.finishStopped(submission.stream);
209
+ else {
210
+ if (!this.pending)
211
+ this.snapshotValue = withStatus(this.snapshotValue, "idle");
212
+ this.finishCancelled(submission.stream, message(error));
213
+ }
214
+ }
215
+ }
216
+ async resumeTurn(submission, context) {
217
+ const pending = this.pending;
218
+ this.pending = undefined;
219
+ this.snapshotValue = withStatus(this.snapshotValue, "running");
220
+ return this.turns.resume(this.snapshotValue, pending, submission.event, context);
221
+ }
222
+ applyTurnOutcome(stream, outcome) {
223
+ switch (outcome.kind) {
224
+ case "final":
225
+ this.snapshotValue = withStatus(outcome.state, "idle");
226
+ this.publish(stream, { type: "final", output: outcome.output, turnId: outcome.turnId });
227
+ this.emitObserve({
228
+ type: "turn.completed",
229
+ turnId: outcome.turnId,
230
+ stepId: outcome.stepId,
231
+ inputId: stream.inputId,
232
+ attributes: { output: outcome.output },
233
+ });
234
+ stream.finish("completed");
235
+ return;
236
+ case "interaction-required":
237
+ this.pending = outcome.pending;
238
+ this.snapshotValue = withStatus(outcome.state, "waiting", outcome.interaction);
239
+ this.emitObserve({
240
+ type: "interaction.required",
241
+ turnId: outcome.pending.turnId,
242
+ stepId: outcome.pending.stepId,
243
+ inputId: stream.inputId,
244
+ interactionId: outcome.interaction.id,
245
+ kind: outcome.interaction.kind,
246
+ ...(outcome.pending.plan.interactionCallId === undefined
247
+ ? {}
248
+ : { callId: outcome.pending.plan.interactionCallId }),
249
+ ...(outcome.pending.plan.interactionToolName === undefined
250
+ ? {}
251
+ : { toolName: outcome.pending.plan.interactionToolName }),
252
+ ...(outcome.pending.plan.interactionPhase === undefined
253
+ ? {}
254
+ : { phase: outcome.pending.plan.interactionPhase }),
255
+ attributes: {
256
+ prompt: outcome.interaction.prompt,
257
+ ...(outcome.interaction.metadata === undefined
258
+ ? {}
259
+ : { metadata: outcome.interaction.metadata }),
260
+ },
261
+ });
262
+ this.publish(stream, {
263
+ type: "interaction.required",
264
+ interaction: outcome.interaction,
265
+ turnId: outcome.pending.turnId,
266
+ });
267
+ stream.finish("waiting");
268
+ return;
269
+ case "tripwire":
270
+ this.snapshotValue = outcome.state;
271
+ this.publish(stream, {
272
+ type: "tripwire",
273
+ tripwire: outcome.tripwire,
274
+ turnId: outcome.turnId,
275
+ });
276
+ this.emitObserve({
277
+ type: "tripwire",
278
+ turnId: outcome.turnId,
279
+ inputId: stream.inputId,
280
+ ...(outcome.stepId ? { stepId: outcome.stepId } : {}),
281
+ code: outcome.tripwire.code,
282
+ scope: outcome.tripwire.scope ?? "step",
283
+ attributes: { message: outcome.tripwire.message },
284
+ });
285
+ if (outcome.tripwire.scope === "session")
286
+ this.beginStop("Session policy tripwire");
287
+ else
288
+ this.snapshotValue = withStatus(this.snapshotValue, "idle");
289
+ stream.finish("completed");
290
+ }
291
+ }
292
+ beginStop(reason) {
293
+ if (this.stopped)
294
+ return;
295
+ this.stopped = true;
296
+ this.generation += 1;
297
+ this.sessionController.abort(new HarnessError("session.stale-result", reason));
298
+ this.activeController?.abort(new HarnessError("session.stale-result", reason));
299
+ if (this.pending)
300
+ this.commitCancelledPlan(this.pending, reason);
301
+ this.pending = undefined;
302
+ this.snapshotValue = withStatus(this.snapshotValue, "stopped");
303
+ this.events.emit({ type: "session.stopped", sessionId: this.id });
304
+ this.events.finish();
305
+ this.emitObserve({ type: "session.stopped", reason });
306
+ this.observers.clear();
307
+ for (const item of this.queue.drain())
308
+ this.finishStopped(item.stream);
309
+ }
310
+ emitObserve(event) {
311
+ this.observers.emit(event);
312
+ }
313
+ commitCancelledPlan(pending, reason) {
314
+ this.snapshotValue = commitToolResults(this.snapshotValue, pending.turnId, pending.stepId, pending.plan.cancelledResults(reason));
315
+ }
316
+ claimInterrupts(turnId) {
317
+ const claimed = this.queue.takeInterrupts();
318
+ const events = [];
319
+ for (const item of claimed) {
320
+ const conversation = Object.freeze({
321
+ type: "input",
322
+ event: item.event,
323
+ turnId,
324
+ });
325
+ this.events.emit(conversation);
326
+ this.activeSubmission?.stream.emit(conversation);
327
+ item.stream.emit(conversation);
328
+ item.stream.finish("completed");
329
+ events.push(item.event);
330
+ }
331
+ return Object.freeze(events);
332
+ }
333
+ assertCurrent(generation, signal) {
334
+ // An abort-ignoring adapter may resolve late; its state must never re-enter this Session.
335
+ if (this.stopped || generation !== this.generation || signal.aborted)
336
+ throw (signal.reason ??
337
+ new HarnessError("session.stale-result", "Stale Session result quarantined"));
338
+ }
339
+ }
340
+ function withInputId(event, inputId) {
341
+ return "turnId" in event ? { ...event, inputId } : event;
342
+ }
343
+ function message(error) {
344
+ return error instanceof Error ? error.message : String(error);
345
+ }
346
+ function cancellationMessage(signal) {
347
+ return signal.reason instanceof Error
348
+ ? signal.reason.message
349
+ : signal.reason
350
+ ? String(signal.reason)
351
+ : "Input cancelled";
352
+ }
@@ -0,0 +1,14 @@
1
+ import type { InputHandle, InputOptions, MessageInput, Session, SessionEvent, SessionOptions, SessionSnapshot, SessionInput } from "../types/session.js";
2
+ import type { Observer } from "../types/shared.js";
3
+ import type { LoopAgent } from "../build/agent.js";
4
+ export declare class LiveSession implements Session {
5
+ readonly id: string;
6
+ private readonly scheduler;
7
+ constructor(id: string, agent: LoopAgent, options: SessionOptions);
8
+ get state(): SessionSnapshot;
9
+ input(event: SessionInput, options?: InputOptions): InputHandle;
10
+ interrupt(event: MessageInput, options?: InputOptions): InputHandle;
11
+ stream(): AsyncIterable<SessionEvent>;
12
+ observe(listener: Observer): () => void;
13
+ stop(reason?: string): Promise<void>;
14
+ }
@@ -0,0 +1,55 @@
1
+ import { SessionScheduler } from "./scheduler.js";
2
+ import { copyJsonObject } from "../utils/immutable.js";
3
+ export class LiveSession {
4
+ id;
5
+ scheduler;
6
+ constructor(id, agent, options) {
7
+ this.id = id;
8
+ const session = Object.freeze({
9
+ ...(options.userId ? { userId: options.userId } : {}),
10
+ ...(options.context ? { context: copyJsonObject(options.context, "session context") } : {}),
11
+ });
12
+ this.scheduler = new SessionScheduler(id, agent, session);
13
+ }
14
+ get state() {
15
+ return this.scheduler.snapshot;
16
+ }
17
+ input(event, options) {
18
+ return this.scheduler.submit(normalizeInput(event), options);
19
+ }
20
+ interrupt(event, options) {
21
+ return this.scheduler.submit(normalizeMessage("interrupt", event), options);
22
+ }
23
+ stream() {
24
+ return this.scheduler.events;
25
+ }
26
+ observe(listener) {
27
+ return this.scheduler.observe(listener);
28
+ }
29
+ stop(reason) {
30
+ return this.scheduler.stop(reason);
31
+ }
32
+ }
33
+ function isInteractionReply(value) {
34
+ return (typeof value === "object" &&
35
+ "kind" in value &&
36
+ (value.kind === "approve" || value.kind === "respond"));
37
+ }
38
+ function normalizeMessage(kind, value) {
39
+ if (typeof value === "string")
40
+ return { kind, text: value };
41
+ return {
42
+ kind,
43
+ text: value.text,
44
+ ...(value.metadata === undefined
45
+ ? {}
46
+ : { metadata: copyJsonObject(value.metadata, "input metadata") }),
47
+ };
48
+ }
49
+ function normalizeInput(input) {
50
+ if (typeof input === "string")
51
+ return { kind: "user-message", text: input };
52
+ if (isInteractionReply(input))
53
+ return input;
54
+ return normalizeMessage("user-message", input);
55
+ }
@@ -0,0 +1,10 @@
1
+ import type { InputEvent, SessionSnapshot } from "../types/session.js";
2
+ import type { ModelCandidate } from "../types/model.js";
3
+ import type { RequiredInteraction, ToolResult } from "../types/tool.js";
4
+ export declare function initialState(id: string): SessionSnapshot;
5
+ export declare function withStatus(state: SessionSnapshot, status: SessionSnapshot["status"], pendingInteraction?: RequiredInteraction): SessionSnapshot;
6
+ export declare function beginTurn(state: SessionSnapshot, turnId: string, event?: InputEvent): SessionSnapshot;
7
+ export declare const commitInput: (state: SessionSnapshot, turnId: string, event: InputEvent) => SessionSnapshot;
8
+ export declare const commitCandidate: (state: SessionSnapshot, turnId: string, stepId: string, candidate: ModelCandidate) => SessionSnapshot;
9
+ export declare const commitToolResults: (state: SessionSnapshot, turnId: string, stepId: string, results: readonly ToolResult[]) => SessionSnapshot;
10
+ export declare const commitFinal: (state: SessionSnapshot, turnId: string, stepId: string, output: string) => SessionSnapshot;
@@ -0,0 +1,36 @@
1
+ export function initialState(id) {
2
+ return Object.freeze({ id, status: "idle", turnCount: 0, transcript: Object.freeze([]) });
3
+ }
4
+ export function withStatus(state, status, pendingInteraction) {
5
+ return Object.freeze({
6
+ id: state.id,
7
+ status,
8
+ turnCount: state.turnCount,
9
+ transcript: state.transcript,
10
+ ...(pendingInteraction ? { pendingInteraction } : {}),
11
+ });
12
+ }
13
+ export function beginTurn(state, turnId, event) {
14
+ const transcript = event
15
+ ? [
16
+ ...state.transcript,
17
+ Object.freeze({ kind: "input", turnId, event: Object.freeze({ ...event }) }),
18
+ ]
19
+ : [...state.transcript];
20
+ return Object.freeze({
21
+ id: state.id,
22
+ status: "running",
23
+ turnCount: state.turnCount + (event ? 1 : 0),
24
+ transcript: Object.freeze(transcript),
25
+ });
26
+ }
27
+ function append(state, entry) {
28
+ return Object.freeze({
29
+ ...state,
30
+ transcript: Object.freeze([...state.transcript, Object.freeze(entry)]),
31
+ });
32
+ }
33
+ export const commitInput = (state, turnId, event) => append(state, { kind: "input", turnId, event: Object.freeze({ ...event }) });
34
+ export const commitCandidate = (state, turnId, stepId, candidate) => append(state, { kind: "candidate", turnId, stepId, candidate });
35
+ export const commitToolResults = (state, turnId, stepId, results) => append(state, { kind: "tool-results", turnId, stepId, results: Object.freeze([...results]) });
36
+ export const commitFinal = (state, turnId, stepId, output) => append(state, { kind: "final", turnId, stepId, output });
@@ -0,0 +1,13 @@
1
+ import type { InputCompletion, InputHandle, SessionEvent } from "../types/session.js";
2
+ export declare class SubmissionStream implements InputHandle {
3
+ readonly inputId: string;
4
+ readonly completed: Promise<InputCompletion>;
5
+ private resolveCompletion;
6
+ private readonly events;
7
+ private readonly cleanups;
8
+ private done;
9
+ constructor(inputId: string);
10
+ emit(event: SessionEvent): void;
11
+ finish(status: InputCompletion["status"]): void;
12
+ onFinish(cleanup: () => void): void;
13
+ }
@@ -0,0 +1,36 @@
1
+ export class SubmissionStream {
2
+ inputId;
3
+ completed;
4
+ resolveCompletion;
5
+ events = [];
6
+ cleanups = [];
7
+ done = false;
8
+ constructor(inputId) {
9
+ this.inputId = inputId;
10
+ this.completed = new Promise((resolve) => {
11
+ this.resolveCompletion = resolve;
12
+ });
13
+ }
14
+ emit(event) {
15
+ if (!this.done)
16
+ this.events.push(event);
17
+ }
18
+ finish(status) {
19
+ if (this.done)
20
+ return;
21
+ this.done = true;
22
+ while (this.cleanups.length)
23
+ this.cleanups.pop()();
24
+ this.resolveCompletion(Object.freeze({
25
+ inputId: this.inputId,
26
+ status,
27
+ events: Object.freeze([...this.events]),
28
+ }));
29
+ }
30
+ onFinish(cleanup) {
31
+ if (this.done)
32
+ cleanup();
33
+ else
34
+ this.cleanups.push(cleanup);
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ import type { ModelCandidate, ModelOutputBlock, ModelToolCall } from "../types/model.js";
2
+ import type { JsonObject } from "../types/shared.js";
3
+ export interface CanonicalCall {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly args: JsonObject;
7
+ readonly missingId: boolean;
8
+ readonly duplicateId?: string;
9
+ }
10
+ export declare function canonicalizeCalls(calls: readonly {
11
+ readonly id?: string;
12
+ readonly name?: string;
13
+ readonly args: JsonObject;
14
+ }[]): readonly CanonicalCall[];
15
+ export declare function callsFromCanonical(calls: readonly CanonicalCall[]): readonly ModelToolCall[];
16
+ export declare function canonicalizeOutput(output: readonly ModelOutputBlock[]): {
17
+ readonly output: readonly ModelOutputBlock[];
18
+ readonly calls: readonly CanonicalCall[];
19
+ };
20
+ export declare function candidateFromCanonical(candidate: ModelCandidate, output: readonly ModelOutputBlock[]): ModelCandidate;
21
+ export declare function identityKey(name: string, args: unknown): string;