@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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +192 -0
- package/README.md +124 -0
- package/dist/build/adapters.d.ts +11 -0
- package/dist/build/adapters.js +91 -0
- package/dist/build/agent.d.ts +19 -0
- package/dist/build/agent.js +28 -0
- package/dist/build/assemble.d.ts +9 -0
- package/dist/build/assemble.js +76 -0
- package/dist/build/bind-tool.d.ts +4 -0
- package/dist/build/bind-tool.js +15 -0
- package/dist/build/builder.d.ts +31 -0
- package/dist/build/builder.js +74 -0
- package/dist/build/helpers.d.ts +7 -0
- package/dist/build/helpers.js +5 -0
- package/dist/build/manifest.d.ts +9 -0
- package/dist/build/manifest.js +15 -0
- package/dist/build/schema.d.ts +19 -0
- package/dist/build/schema.js +86 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4 -0
- package/dist/model-normalize.d.ts +6 -0
- package/dist/model-normalize.js +211 -0
- package/dist/session/event-log.d.ts +11 -0
- package/dist/session/event-log.js +63 -0
- package/dist/session/input-queue.d.ts +29 -0
- package/dist/session/input-queue.js +78 -0
- package/dist/session/scheduler.d.ts +48 -0
- package/dist/session/scheduler.js +352 -0
- package/dist/session/session.d.ts +14 -0
- package/dist/session/session.js +55 -0
- package/dist/session/state.d.ts +10 -0
- package/dist/session/state.js +36 -0
- package/dist/session/submission-stream.d.ts +13 -0
- package/dist/session/submission-stream.js +36 -0
- package/dist/step/canonicalize.d.ts +21 -0
- package/dist/step/canonicalize.js +62 -0
- package/dist/step/compose.d.ts +4 -0
- package/dist/step/compose.js +106 -0
- package/dist/step/context-draft.d.ts +10 -0
- package/dist/step/context-draft.js +71 -0
- package/dist/step/model-configuration.d.ts +15 -0
- package/dist/step/model-configuration.js +153 -0
- package/dist/step/project.d.ts +2 -0
- package/dist/step/project.js +103 -0
- package/dist/step/resolve.d.ts +9 -0
- package/dist/step/resolve.js +16 -0
- package/dist/step/run.d.ts +27 -0
- package/dist/step/run.js +127 -0
- package/dist/step/seal.d.ts +31 -0
- package/dist/step/seal.js +108 -0
- package/dist/step/slot-assembly.d.ts +39 -0
- package/dist/step/slot-assembly.js +52 -0
- package/dist/step/step-context.d.ts +36 -0
- package/dist/step/step-context.js +255 -0
- package/dist/turn/plan-runner.d.ts +55 -0
- package/dist/turn/plan-runner.js +370 -0
- package/dist/turn/runner.d.ts +53 -0
- package/dist/turn/runner.js +128 -0
- package/dist/types/manifest.d.ts +22 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/middleware.d.ts +65 -0
- package/dist/types/middleware.js +1 -0
- package/dist/types/model.d.ts +166 -0
- package/dist/types/model.js +1 -0
- package/dist/types/session.d.ts +122 -0
- package/dist/types/session.js +1 -0
- package/dist/types/shared.d.ts +180 -0
- package/dist/types/shared.js +1 -0
- package/dist/types/tool.d.ts +101 -0
- package/dist/types/tool.js +1 -0
- package/dist/utils/digest.d.ts +1 -0
- package/dist/utils/digest.js +14 -0
- package/dist/utils/ids.d.ts +1 -0
- package/dist/utils/ids.js +3 -0
- package/dist/utils/immutable.d.ts +5 -0
- package/dist/utils/immutable.js +54 -0
- package/dist/utils/maps.d.ts +1 -0
- package/dist/utils/maps.js +37 -0
- package/dist/utils/observe.d.ts +8 -0
- package/dist/utils/observe.js +29 -0
- package/docs/loop.md +47 -0
- package/docs/model-call-projection.md +112 -0
- package/docs/reference.md +122 -0
- 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;
|