@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,55 @@
|
|
|
1
|
+
import type { AdapterRegistry } from "../build/adapters.js";
|
|
2
|
+
import type { ObserveEmit } from "../utils/observe.js";
|
|
3
|
+
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";
|
|
7
|
+
export type ToolPlanProgress = {
|
|
8
|
+
readonly kind: "completed";
|
|
9
|
+
readonly results: readonly ToolResult[];
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "interaction-required";
|
|
12
|
+
readonly interaction: RequiredInteraction;
|
|
13
|
+
};
|
|
14
|
+
export interface ToolPlanRunContext {
|
|
15
|
+
readonly adapters: AdapterRegistry;
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
readonly observe: ObserveEmit;
|
|
18
|
+
readonly ids: {
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
readonly turnId: string;
|
|
21
|
+
readonly stepId: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Owns the mutable progress of one sealed tool plan across interaction resumes. */
|
|
25
|
+
export declare class ToolPlanRunner {
|
|
26
|
+
private readonly plan;
|
|
27
|
+
private readonly results;
|
|
28
|
+
private readonly resumes;
|
|
29
|
+
private phase;
|
|
30
|
+
private index;
|
|
31
|
+
private readonly pending;
|
|
32
|
+
private executeStarted;
|
|
33
|
+
private resumeExecute?;
|
|
34
|
+
constructor(plan: InternalToolPlan);
|
|
35
|
+
get interactionId(): string | undefined;
|
|
36
|
+
get interactionKind(): RequiredInteraction["kind"] | undefined;
|
|
37
|
+
get interactionCallId(): string | undefined;
|
|
38
|
+
get interactionToolName(): string | undefined;
|
|
39
|
+
get interactionPhase(): PlanPhase | undefined;
|
|
40
|
+
cancelledResults(reason: string): readonly ToolResult[];
|
|
41
|
+
run(context: ToolPlanRunContext, resume?: InputEvent): Promise<ToolPlanProgress>;
|
|
42
|
+
private acceptResume;
|
|
43
|
+
private runExecute;
|
|
44
|
+
private preflight;
|
|
45
|
+
private executeBatch;
|
|
46
|
+
private execute;
|
|
47
|
+
private takeResume;
|
|
48
|
+
private requireInteraction;
|
|
49
|
+
private enqueueInteraction;
|
|
50
|
+
private interactionRequired;
|
|
51
|
+
private orderedResults;
|
|
52
|
+
private resultsInOrder;
|
|
53
|
+
private throwIfAborted;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { HarnessError, isHarnessError } from "../errors.js";
|
|
2
|
+
import { createId } from "../utils/ids.js";
|
|
3
|
+
import { copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
4
|
+
/** Owns the mutable progress of one sealed tool plan across interaction resumes. */
|
|
5
|
+
export class ToolPlanRunner {
|
|
6
|
+
plan;
|
|
7
|
+
results;
|
|
8
|
+
resumes = new Map();
|
|
9
|
+
phase = "interaction";
|
|
10
|
+
index = 0;
|
|
11
|
+
pending = [];
|
|
12
|
+
executeStarted = false;
|
|
13
|
+
resumeExecute;
|
|
14
|
+
constructor(plan) {
|
|
15
|
+
this.plan = plan;
|
|
16
|
+
this.results = new Map(plan.immediateResults.map((item) => [item.callId, item]));
|
|
17
|
+
}
|
|
18
|
+
get interactionId() {
|
|
19
|
+
return this.pending[0]?.interaction.id;
|
|
20
|
+
}
|
|
21
|
+
get interactionKind() {
|
|
22
|
+
return this.pending[0]?.interaction.kind;
|
|
23
|
+
}
|
|
24
|
+
get interactionCallId() {
|
|
25
|
+
const pending = this.pending[0];
|
|
26
|
+
if (!pending)
|
|
27
|
+
return undefined;
|
|
28
|
+
return this.plan.executable[pending.index]?.call.callId;
|
|
29
|
+
}
|
|
30
|
+
get interactionToolName() {
|
|
31
|
+
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;
|
|
38
|
+
}
|
|
39
|
+
cancelledResults(reason) {
|
|
40
|
+
return this.resultsInOrder("tool.cancelled", reason);
|
|
41
|
+
}
|
|
42
|
+
async run(context, resume) {
|
|
43
|
+
this.acceptResume(resume);
|
|
44
|
+
while (true) {
|
|
45
|
+
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];
|
|
62
|
+
if (this.results.has(entry.call.callId)) {
|
|
63
|
+
this.index += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
this.index += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
acceptResume(resume) {
|
|
87
|
+
const pending = this.pending.shift();
|
|
88
|
+
if (!pending)
|
|
89
|
+
return;
|
|
90
|
+
if (!resume)
|
|
91
|
+
throw new HarnessError("interaction.missing-resume", "A pending tool plan requires a correlated interaction input");
|
|
92
|
+
const value = resumeValue(resume, pending);
|
|
93
|
+
const entry = this.plan.executable[pending.index];
|
|
94
|
+
if (value.kind === "approval" && value.approved === false) {
|
|
95
|
+
this.results.set(entry.call.callId, Object.freeze({
|
|
96
|
+
callId: entry.call.callId,
|
|
97
|
+
toolName: entry.call.toolName,
|
|
98
|
+
kind: "denied",
|
|
99
|
+
reason: "Approval rejected",
|
|
100
|
+
}));
|
|
101
|
+
this.index += 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (pending.phase === "interaction") {
|
|
105
|
+
this.resumes.set(entry.call.callId, value);
|
|
106
|
+
this.index += 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.resumes.set(`${pending.phase}:${entry.call.callId}`, value);
|
|
110
|
+
if (pending.phase === "execute")
|
|
111
|
+
this.resumeExecute = entry;
|
|
112
|
+
}
|
|
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);
|
|
122
|
+
}
|
|
123
|
+
return this.pending.length
|
|
124
|
+
? this.interactionRequired()
|
|
125
|
+
: { kind: "completed", results: this.orderedResults() };
|
|
126
|
+
}
|
|
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
|
+
}
|
|
134
|
+
context.observe({
|
|
135
|
+
type: "adapter.preflight.started",
|
|
136
|
+
turnId: context.ids.turnId,
|
|
137
|
+
stepId: context.ids.stepId,
|
|
138
|
+
adapterId: adapter.id,
|
|
139
|
+
toolName: entry.call.toolName,
|
|
140
|
+
callId: entry.call.callId,
|
|
141
|
+
invocationId: entry.invocationId,
|
|
142
|
+
attributes: { args: copyJson(entry.call.args) },
|
|
143
|
+
});
|
|
144
|
+
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",
|
|
158
|
+
turnId: context.ids.turnId,
|
|
159
|
+
stepId: context.ids.stepId,
|
|
160
|
+
adapterId: adapter.id,
|
|
161
|
+
toolName: entry.call.toolName,
|
|
162
|
+
callId: entry.call.callId,
|
|
163
|
+
outcome: outcome.kind,
|
|
164
|
+
...adapterCompleted(this.results.get(entry.call.callId)),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
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,
|
|
205
|
+
callId: entry.call.callId,
|
|
206
|
+
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),
|
|
213
|
+
});
|
|
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",
|
|
220
|
+
turnId: context.ids.turnId,
|
|
221
|
+
stepId: context.ids.stepId,
|
|
222
|
+
adapterId: adapter.id,
|
|
223
|
+
toolName: entry.call.toolName,
|
|
224
|
+
callId: entry.call.callId,
|
|
225
|
+
outcome: outcome.kind,
|
|
226
|
+
...adapterCompleted(this.results.get(entry.call.callId)),
|
|
227
|
+
});
|
|
228
|
+
return undefined;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
this.throwIfAborted(context.signal);
|
|
233
|
+
const result = failed(entry, isHarnessError(error) ? error.code : "tool.execution-failed", error);
|
|
234
|
+
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
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
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();
|
|
257
|
+
}
|
|
258
|
+
enqueueInteraction(pending) {
|
|
259
|
+
const position = this.pending.findIndex((item) => item.index > pending.index);
|
|
260
|
+
if (position === -1)
|
|
261
|
+
this.pending.push(pending);
|
|
262
|
+
else
|
|
263
|
+
this.pending.splice(position, 0, pending);
|
|
264
|
+
}
|
|
265
|
+
interactionRequired() {
|
|
266
|
+
const pending = this.pending[0];
|
|
267
|
+
if (!pending)
|
|
268
|
+
throw new HarnessError("interaction.invalid", "Missing pending interaction");
|
|
269
|
+
return { kind: "interaction-required", interaction: pending.interaction };
|
|
270
|
+
}
|
|
271
|
+
orderedResults() {
|
|
272
|
+
return this.resultsInOrder("tool.missing-result", "Tool call did not produce a result");
|
|
273
|
+
}
|
|
274
|
+
resultsInOrder(code, message) {
|
|
275
|
+
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
|
+
})));
|
|
283
|
+
}
|
|
284
|
+
throwIfAborted(signal) {
|
|
285
|
+
if (signal.aborted)
|
|
286
|
+
throw signal.reason;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function interactionBarrier(phase, index, outcome) {
|
|
290
|
+
return {
|
|
291
|
+
phase,
|
|
292
|
+
index,
|
|
293
|
+
interaction: normalizeInteraction(outcome.interaction),
|
|
294
|
+
...(outcome.token === undefined ? {} : { token: copyJson(outcome.token) }),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function normalizeInteraction(value) {
|
|
298
|
+
if (!value || typeof value !== "object")
|
|
299
|
+
throw new HarnessError("interaction.invalid", "Interaction must be an object");
|
|
300
|
+
if (value.kind !== "approval" && value.kind !== "response")
|
|
301
|
+
throw new HarnessError("interaction.invalid", "Interaction kind must be approval or response");
|
|
302
|
+
if (typeof value.prompt !== "string")
|
|
303
|
+
throw new HarnessError("interaction.invalid", "Interaction prompt must be a string");
|
|
304
|
+
if (value.id !== undefined && (typeof value.id !== "string" || !value.id))
|
|
305
|
+
throw new HarnessError("interaction.invalid", "Interaction id must be a non-empty string");
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
id: value.id ?? createId("interaction"),
|
|
308
|
+
kind: value.kind,
|
|
309
|
+
prompt: value.prompt,
|
|
310
|
+
...(value.metadata === undefined
|
|
311
|
+
? {}
|
|
312
|
+
: { metadata: copyJsonObject(value.metadata, "interaction metadata") }),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function resumeValue(event, pending) {
|
|
316
|
+
if (event.kind === "approve")
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
interactionId: event.interactionId,
|
|
319
|
+
kind: "approval",
|
|
320
|
+
approved: event.approved,
|
|
321
|
+
...(pending.token === undefined ? {} : { token: pending.token }),
|
|
322
|
+
});
|
|
323
|
+
if (event.kind === "respond")
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
interactionId: event.interactionId,
|
|
326
|
+
kind: "response",
|
|
327
|
+
value: event.value,
|
|
328
|
+
...(pending.token === undefined ? {} : { token: pending.token }),
|
|
329
|
+
});
|
|
330
|
+
throw new HarnessError("interaction.uncorrelated-resume", "Only correlated interaction input can resume a plan");
|
|
331
|
+
}
|
|
332
|
+
function resultFrom(entry, outcome) {
|
|
333
|
+
const base = { callId: entry.call.callId, toolName: entry.call.toolName };
|
|
334
|
+
switch (outcome.kind) {
|
|
335
|
+
case "completed":
|
|
336
|
+
return Object.freeze({ ...base, kind: "completed", output: copyJson(outcome.output) });
|
|
337
|
+
case "denied":
|
|
338
|
+
if (typeof outcome.reason !== "string")
|
|
339
|
+
throw new HarnessError("tool.invalid-tool-result", "Tool denial reason must be a string");
|
|
340
|
+
return Object.freeze({ ...base, kind: "denied", reason: outcome.reason });
|
|
341
|
+
case "failed":
|
|
342
|
+
if (typeof outcome.code !== "string" || typeof outcome.message !== "string")
|
|
343
|
+
throw new HarnessError("tool.invalid-tool-result", "Tool failure code and message must be strings");
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
...base,
|
|
346
|
+
kind: "failed",
|
|
347
|
+
code: outcome.code,
|
|
348
|
+
message: outcome.message,
|
|
349
|
+
});
|
|
350
|
+
default:
|
|
351
|
+
throw new HarnessError("adapter.invalid-outcome", "Tool adapter returned an invalid outcome");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function adapterCompleted(result) {
|
|
355
|
+
if (!result)
|
|
356
|
+
return {};
|
|
357
|
+
return {
|
|
358
|
+
...(result.code === undefined ? {} : { code: result.code }),
|
|
359
|
+
attributes: copyJson(result),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function failed(entry, code, error) {
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
callId: entry.call.callId,
|
|
365
|
+
toolName: entry.call.toolName,
|
|
366
|
+
kind: "failed",
|
|
367
|
+
code,
|
|
368
|
+
message: error instanceof Error ? error.message : String(error),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { InputEvent, SessionEvent, SessionSnapshot } from "../types/session.js";
|
|
2
|
+
import type { JsonObject, Tripwire } from "../types/shared.js";
|
|
3
|
+
import type { RequiredInteraction } from "../types/tool.js";
|
|
4
|
+
import type { LoopAgent } from "../build/agent.js";
|
|
5
|
+
import type { ObserveEmit } from "../utils/observe.js";
|
|
6
|
+
import { ToolPlanRunner } from "./plan-runner.js";
|
|
7
|
+
export interface PendingTurn {
|
|
8
|
+
readonly plan: ToolPlanRunner;
|
|
9
|
+
readonly turnId: string;
|
|
10
|
+
readonly stepId: string;
|
|
11
|
+
readonly stepNumber: number;
|
|
12
|
+
}
|
|
13
|
+
export type TurnProgress = {
|
|
14
|
+
readonly kind: "final";
|
|
15
|
+
readonly state: SessionSnapshot;
|
|
16
|
+
readonly turnId: string;
|
|
17
|
+
readonly stepId: string;
|
|
18
|
+
readonly output: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "interaction-required";
|
|
21
|
+
readonly state: SessionSnapshot;
|
|
22
|
+
readonly pending: PendingTurn;
|
|
23
|
+
readonly interaction: RequiredInteraction;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "tripwire";
|
|
26
|
+
readonly state: SessionSnapshot;
|
|
27
|
+
readonly turnId: string;
|
|
28
|
+
readonly stepId?: string;
|
|
29
|
+
readonly tripwire: Tripwire;
|
|
30
|
+
};
|
|
31
|
+
export interface TurnRunContext {
|
|
32
|
+
readonly signal: AbortSignal;
|
|
33
|
+
readonly observe: ObserveEmit;
|
|
34
|
+
readonly assertCurrent: () => void;
|
|
35
|
+
readonly onPlanActive: (pending: PendingTurn | undefined) => void;
|
|
36
|
+
readonly onState: (state: SessionSnapshot) => void;
|
|
37
|
+
readonly onConversation: (event: SessionEvent) => void;
|
|
38
|
+
readonly claimInterrupts: (turnId: string) => readonly InputEvent[];
|
|
39
|
+
}
|
|
40
|
+
/** Advances one Turn until it finalizes, trips a policy, or pauses for an interaction. */
|
|
41
|
+
export declare class TurnRunner {
|
|
42
|
+
private readonly agent;
|
|
43
|
+
private readonly sessionId;
|
|
44
|
+
private readonly session;
|
|
45
|
+
constructor(agent: LoopAgent, sessionId: string, session: Readonly<{
|
|
46
|
+
readonly userId?: string;
|
|
47
|
+
readonly context?: JsonObject;
|
|
48
|
+
}>);
|
|
49
|
+
start(state: SessionSnapshot, event: InputEvent, context: TurnRunContext): Promise<TurnProgress>;
|
|
50
|
+
resume(state: SessionSnapshot, pending: PendingTurn, event: InputEvent, context: TurnRunContext): Promise<TurnProgress>;
|
|
51
|
+
private advance;
|
|
52
|
+
private runPlan;
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { beginTurn, commitCandidate, commitFinal, commitInput, commitToolResults, } from "../session/state.js";
|
|
2
|
+
import { runStep } from "../step/run.js";
|
|
3
|
+
import { createId } from "../utils/ids.js";
|
|
4
|
+
import { copyJson } from "../utils/immutable.js";
|
|
5
|
+
import { ToolPlanRunner } from "./plan-runner.js";
|
|
6
|
+
/** Advances one Turn until it finalizes, trips a policy, or pauses for an interaction. */
|
|
7
|
+
export class TurnRunner {
|
|
8
|
+
agent;
|
|
9
|
+
sessionId;
|
|
10
|
+
session;
|
|
11
|
+
constructor(agent, sessionId, session) {
|
|
12
|
+
this.agent = agent;
|
|
13
|
+
this.sessionId = sessionId;
|
|
14
|
+
this.session = session;
|
|
15
|
+
}
|
|
16
|
+
async start(state, event, context) {
|
|
17
|
+
const turnId = createId("turn");
|
|
18
|
+
const started = beginTurn(state, turnId, event);
|
|
19
|
+
context.onState(started);
|
|
20
|
+
context.onConversation({ type: "input", event, turnId });
|
|
21
|
+
return this.advance(started, turnId, 1, [event], [], context);
|
|
22
|
+
}
|
|
23
|
+
async resume(state, pending, event, context) {
|
|
24
|
+
context.onConversation({ type: "input", event, turnId: pending.turnId });
|
|
25
|
+
const progress = await this.runPlan(pending, context, event);
|
|
26
|
+
if (progress.kind === "interaction-required")
|
|
27
|
+
return { kind: "interaction-required", state, pending, interaction: progress.interaction };
|
|
28
|
+
const afterTools = commitToolResults(state, pending.turnId, pending.stepId, progress.results);
|
|
29
|
+
context.onState(afterTools);
|
|
30
|
+
const claimed = claimAndRecord(afterTools, pending.turnId, context);
|
|
31
|
+
return this.advance(claimed.state, pending.turnId, pending.stepNumber + 1, claimed.arrivals, progress.results, context);
|
|
32
|
+
}
|
|
33
|
+
async advance(initialState, turnId, firstStep, arrivals, initialResults, context) {
|
|
34
|
+
let state = initialState;
|
|
35
|
+
let stepArrivals = arrivals;
|
|
36
|
+
let toolResults = initialResults;
|
|
37
|
+
for (let stepNumber = firstStep;; stepNumber += 1) {
|
|
38
|
+
const stepId = createId("step");
|
|
39
|
+
const run = await runStep({
|
|
40
|
+
agent: this.agent,
|
|
41
|
+
observe: context.observe,
|
|
42
|
+
state,
|
|
43
|
+
sessionId: this.sessionId,
|
|
44
|
+
turnId,
|
|
45
|
+
stepId,
|
|
46
|
+
turnNumber: state.turnCount,
|
|
47
|
+
stepNumber,
|
|
48
|
+
arrivals: stepArrivals,
|
|
49
|
+
toolResults,
|
|
50
|
+
signal: context.signal,
|
|
51
|
+
session: this.session,
|
|
52
|
+
});
|
|
53
|
+
context.assertCurrent();
|
|
54
|
+
if (run.candidate) {
|
|
55
|
+
state = commitCandidate(state, turnId, stepId, run.candidate);
|
|
56
|
+
context.onState(state);
|
|
57
|
+
context.onConversation({ type: "candidate", turnId, stepId, candidate: run.candidate });
|
|
58
|
+
}
|
|
59
|
+
if (run.output.kind === "tripwire")
|
|
60
|
+
return { kind: "tripwire", state, turnId, stepId, tripwire: run.output.tripwire };
|
|
61
|
+
if (run.output.kind === "final")
|
|
62
|
+
return {
|
|
63
|
+
kind: "final",
|
|
64
|
+
state: commitFinal(state, turnId, stepId, run.output.output),
|
|
65
|
+
turnId,
|
|
66
|
+
stepId,
|
|
67
|
+
output: run.output.output,
|
|
68
|
+
};
|
|
69
|
+
const plan = run.output.plan;
|
|
70
|
+
context.observe(() => ({
|
|
71
|
+
type: "tool.sealed",
|
|
72
|
+
turnId,
|
|
73
|
+
stepId,
|
|
74
|
+
attributes: {
|
|
75
|
+
executable: Object.freeze(plan.executable.map((entry) => Object.freeze({
|
|
76
|
+
callId: entry.call.callId,
|
|
77
|
+
toolName: entry.call.toolName,
|
|
78
|
+
args: copyJson(entry.call.args),
|
|
79
|
+
executeWith: entry.call.executeWith,
|
|
80
|
+
invocationId: entry.invocationId,
|
|
81
|
+
...(entry.preflight === undefined ? {} : { preflight: entry.preflight }),
|
|
82
|
+
...(entry.interaction === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: { interaction: copyJson(entry.interaction) }),
|
|
85
|
+
}))),
|
|
86
|
+
immediate: copyJson(plan.immediateResults),
|
|
87
|
+
},
|
|
88
|
+
}));
|
|
89
|
+
const pending = {
|
|
90
|
+
plan: new ToolPlanRunner(run.output.plan),
|
|
91
|
+
turnId,
|
|
92
|
+
stepId,
|
|
93
|
+
stepNumber,
|
|
94
|
+
};
|
|
95
|
+
const progress = await this.runPlan(pending, context);
|
|
96
|
+
if (progress.kind === "interaction-required")
|
|
97
|
+
return { kind: "interaction-required", state, pending, interaction: progress.interaction };
|
|
98
|
+
state = commitToolResults(state, turnId, stepId, progress.results);
|
|
99
|
+
context.onState(state);
|
|
100
|
+
const claimed = claimAndRecord(state, turnId, context);
|
|
101
|
+
state = claimed.state;
|
|
102
|
+
stepArrivals = claimed.arrivals;
|
|
103
|
+
toolResults = progress.results;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async runPlan(pending, context, resume) {
|
|
107
|
+
context.onPlanActive(pending);
|
|
108
|
+
const progress = await pending.plan.run({
|
|
109
|
+
adapters: this.agent.adapters,
|
|
110
|
+
signal: context.signal,
|
|
111
|
+
observe: context.observe,
|
|
112
|
+
ids: { sessionId: this.sessionId, turnId: pending.turnId, stepId: pending.stepId },
|
|
113
|
+
}, resume);
|
|
114
|
+
context.assertCurrent();
|
|
115
|
+
context.onPlanActive(undefined);
|
|
116
|
+
return progress;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function claimAndRecord(state, turnId, context) {
|
|
120
|
+
const arrivals = context.claimInterrupts(turnId);
|
|
121
|
+
if (arrivals.length === 0)
|
|
122
|
+
return { state, arrivals };
|
|
123
|
+
let next = state;
|
|
124
|
+
for (const event of arrivals)
|
|
125
|
+
next = commitInput(next, turnId, event);
|
|
126
|
+
context.onState(next);
|
|
127
|
+
return { state: next, arrivals };
|
|
128
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BoundMiddleware } from "./middleware.js";
|
|
2
|
+
import type { ModelDirective } from "./model.js";
|
|
3
|
+
import type { BuildDiagnostic } from "./shared.js";
|
|
4
|
+
export interface AgentManifest {
|
|
5
|
+
readonly middleware: readonly {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
}[];
|
|
8
|
+
readonly model?: ModelDirective;
|
|
9
|
+
readonly adapters: readonly {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly maxConcurrentCalls?: number;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
export type BuildResult<Agent> = {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly agent: Agent;
|
|
17
|
+
readonly manifest: AgentManifest;
|
|
18
|
+
} | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly diagnostics: readonly BuildDiagnostic[];
|
|
21
|
+
};
|
|
22
|
+
export type { BoundMiddleware };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|