@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.
- package/CHANGELOG.md +45 -0
- package/README.md +22 -108
- package/dist/build/agent.d.ts +4 -7
- package/dist/build/agent.js +12 -7
- package/dist/build/assemble.d.ts +2 -6
- package/dist/build/assemble.js +4 -47
- package/dist/build/bind-tool.d.ts +2 -3
- package/dist/build/bind-tool.js +6 -4
- package/dist/build/builder.d.ts +5 -8
- package/dist/build/builder.js +34 -13
- package/dist/build/helpers.d.ts +2 -3
- package/dist/build/helpers.js +0 -1
- package/dist/build/manifest.d.ts +0 -4
- package/dist/build/manifest.js +0 -8
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/session/capability-state.d.ts +14 -0
- package/dist/session/capability-state.js +67 -0
- package/dist/session/input-queue.d.ts +11 -2
- package/dist/session/input-queue.js +5 -1
- package/dist/session/record.d.ts +11 -0
- package/dist/session/record.js +26 -0
- package/dist/session/scheduler.d.ts +17 -3
- package/dist/session/scheduler.js +181 -48
- package/dist/session/seed.d.ts +10 -0
- package/dist/session/seed.js +219 -0
- package/dist/session/session.d.ts +3 -2
- package/dist/session/session.js +15 -3
- package/dist/session/state.d.ts +8 -3
- package/dist/session/state.js +16 -4
- package/dist/session/submission-stream.d.ts +2 -0
- package/dist/session/submission-stream.js +11 -1
- package/dist/step/model-configuration.d.ts +1 -3
- package/dist/step/model-configuration.js +2 -4
- package/dist/step/project.js +4 -2
- package/dist/step/run.d.ts +6 -1
- package/dist/step/run.js +35 -13
- package/dist/step/seal.d.ts +6 -2
- package/dist/step/seal.js +4 -3
- package/dist/step/step-context.d.ts +5 -2
- package/dist/step/step-context.js +19 -11
- package/dist/turn/plan-runner.d.ts +28 -13
- package/dist/turn/plan-runner.js +165 -182
- package/dist/turn/runner.d.ts +18 -4
- package/dist/turn/runner.js +73 -45
- package/dist/types/manifest.d.ts +0 -6
- package/dist/types/middleware.d.ts +25 -4
- package/dist/types/model.d.ts +3 -2
- package/dist/types/session.d.ts +86 -2
- package/dist/types/shared.d.ts +65 -9
- package/dist/types/tool.d.ts +37 -39
- package/dist/utils/immutable.js +16 -2
- package/package.json +1 -2
- package/dist/build/adapters.d.ts +0 -11
- package/dist/build/adapters.js +0 -91
- package/docs/loop.md +0 -47
- package/docs/model-call-projection.md +0 -112
- package/docs/reference.md +0 -122
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { HarnessError } from "../errors.js";
|
|
2
|
+
import { normalizeCandidate } from "../model-normalize.js";
|
|
3
|
+
import { assertJson, copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
4
|
+
export function normalizeSessionSeed(seed) {
|
|
5
|
+
try {
|
|
6
|
+
return normalizeSeed(seed);
|
|
7
|
+
}
|
|
8
|
+
catch (cause) {
|
|
9
|
+
if (cause instanceof HarnessError && cause.code === "session.invalid-seed")
|
|
10
|
+
throw cause;
|
|
11
|
+
throw new HarnessError("session.invalid-seed", "Session seed contains invalid typed JSON", {
|
|
12
|
+
cause,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function normalizeSeed(seed) {
|
|
17
|
+
if (!seed || typeof seed !== "object")
|
|
18
|
+
fail("Session seed must be an object");
|
|
19
|
+
if (!Array.isArray(seed.transcript))
|
|
20
|
+
fail("Session seed transcript must be an array");
|
|
21
|
+
optionalString(seed.id, "Session seed id");
|
|
22
|
+
optionalString(seed.userId, "Session seed userId");
|
|
23
|
+
const turnCount = count(seed.turnCount, "turnCount");
|
|
24
|
+
const revision = count(seed.revision, "revision");
|
|
25
|
+
const transcript = Object.freeze(seed.transcript.map((entry, index) => entryAt(entry, index)));
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
...(seed.id === undefined ? {} : { id: seed.id }),
|
|
28
|
+
...(seed.userId === undefined ? {} : { userId: seed.userId }),
|
|
29
|
+
...(seed.context === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { context: copyJsonObject(seed.context, "session seed context") }),
|
|
32
|
+
turnCount,
|
|
33
|
+
revision,
|
|
34
|
+
transcript,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function entryAt(value, index) {
|
|
38
|
+
if (!value || typeof value !== "object")
|
|
39
|
+
fail(`Transcript entry ${index} must be an object`);
|
|
40
|
+
const entry = value;
|
|
41
|
+
requiredString(entry.kind, `Transcript entry ${index} kind`);
|
|
42
|
+
requiredString(entry.turnId, `Transcript entry ${index} turnId`);
|
|
43
|
+
switch (entry.kind) {
|
|
44
|
+
case "input":
|
|
45
|
+
exactKeys(entry, ["kind", "turnId", "event"], `Transcript entry ${index}`);
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
kind: "input",
|
|
48
|
+
turnId: entry.turnId,
|
|
49
|
+
event: inputEvent(entry.event, index),
|
|
50
|
+
});
|
|
51
|
+
case "candidate": {
|
|
52
|
+
exactKeys(entry, ["kind", "turnId", "stepId", "candidate"], `Transcript entry ${index}`);
|
|
53
|
+
requiredString(entry.stepId, `Transcript entry ${index} stepId`);
|
|
54
|
+
try {
|
|
55
|
+
validateSeedCandidate(entry.candidate, index);
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: "candidate",
|
|
58
|
+
turnId: entry.turnId,
|
|
59
|
+
stepId: entry.stepId,
|
|
60
|
+
candidate: normalizeCandidate(entry.candidate),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
throw new HarnessError("session.invalid-seed", `Invalid candidate at transcript ${index}`, {
|
|
65
|
+
cause,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
case "tool-results": {
|
|
70
|
+
exactKeys(entry, ["kind", "turnId", "stepId", "results"], `Transcript entry ${index}`);
|
|
71
|
+
requiredString(entry.stepId, `Transcript entry ${index} stepId`);
|
|
72
|
+
if (!Array.isArray(entry.results))
|
|
73
|
+
fail(`Transcript entry ${index} tool results must be an array`);
|
|
74
|
+
const result = Object.freeze({
|
|
75
|
+
kind: "tool-results",
|
|
76
|
+
turnId: entry.turnId,
|
|
77
|
+
stepId: entry.stepId,
|
|
78
|
+
results: Object.freeze(entry.results.map((item, resultIndex) => toolResult(item, resultIndex))),
|
|
79
|
+
});
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
case "final":
|
|
83
|
+
exactKeys(entry, ["kind", "turnId", "stepId", "output"], `Transcript entry ${index}`);
|
|
84
|
+
requiredString(entry.stepId, `Transcript entry ${index} stepId`);
|
|
85
|
+
if (typeof entry.output !== "string")
|
|
86
|
+
fail(`Transcript entry ${index} output must be a string`);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
kind: "final",
|
|
89
|
+
turnId: entry.turnId,
|
|
90
|
+
stepId: entry.stepId,
|
|
91
|
+
output: entry.output,
|
|
92
|
+
});
|
|
93
|
+
default:
|
|
94
|
+
fail(`Transcript entry ${index} has unknown kind '${String(entry.kind)}'`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function inputEvent(value, index) {
|
|
98
|
+
if (!value || typeof value !== "object")
|
|
99
|
+
fail(`Transcript input ${index} must be an object`);
|
|
100
|
+
const event = value;
|
|
101
|
+
switch (event.kind) {
|
|
102
|
+
case "user-message":
|
|
103
|
+
case "interrupt":
|
|
104
|
+
exactKeys(event, ["kind", "text", "metadata"], `Transcript input ${index}`);
|
|
105
|
+
if (typeof event.text !== "string")
|
|
106
|
+
fail(`Transcript input ${index} text must be a string`);
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
kind: event.kind,
|
|
109
|
+
text: event.text,
|
|
110
|
+
...(event.metadata === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { metadata: copyJsonObject(event.metadata, `transcript input ${index} metadata`) }),
|
|
113
|
+
});
|
|
114
|
+
case "approve":
|
|
115
|
+
exactKeys(event, ["kind", "interactionId", "approved"], `Transcript input ${index}`);
|
|
116
|
+
requiredString(event.interactionId, `Transcript input ${index} interactionId`);
|
|
117
|
+
if (typeof event.approved !== "boolean")
|
|
118
|
+
fail(`Transcript input ${index} approved must be a boolean`);
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
kind: "approve",
|
|
121
|
+
interactionId: event.interactionId,
|
|
122
|
+
approved: event.approved,
|
|
123
|
+
});
|
|
124
|
+
case "respond":
|
|
125
|
+
exactKeys(event, ["kind", "interactionId", "value"], `Transcript input ${index}`);
|
|
126
|
+
requiredString(event.interactionId, `Transcript input ${index} interactionId`);
|
|
127
|
+
assertJson(event.value, `transcript input ${index} value`);
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
kind: "respond",
|
|
130
|
+
interactionId: event.interactionId,
|
|
131
|
+
value: copyJson(event.value),
|
|
132
|
+
});
|
|
133
|
+
default:
|
|
134
|
+
fail(`Transcript input ${index} has unknown kind '${String(event.kind)}'`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function toolResult(value, index) {
|
|
138
|
+
if (!value || typeof value !== "object")
|
|
139
|
+
fail(`Tool result ${index} must be an object`);
|
|
140
|
+
const result = value;
|
|
141
|
+
requiredString(result.callId, `Tool result ${index} callId`);
|
|
142
|
+
requiredString(result.toolName, `Tool result ${index} toolName`);
|
|
143
|
+
const base = { callId: result.callId, toolName: result.toolName };
|
|
144
|
+
switch (result.kind) {
|
|
145
|
+
case "completed":
|
|
146
|
+
exactKeys(result, ["callId", "toolName", "kind", "output"], `Tool result ${index}`);
|
|
147
|
+
assertJson(result.output, `tool result ${index} output`);
|
|
148
|
+
return Object.freeze({ ...base, kind: "completed", output: copyJson(result.output) });
|
|
149
|
+
case "denied":
|
|
150
|
+
exactKeys(result, ["callId", "toolName", "kind", "reason"], `Tool result ${index}`);
|
|
151
|
+
if (typeof result.reason !== "string")
|
|
152
|
+
fail(`Tool result ${index} reason must be a string`);
|
|
153
|
+
return Object.freeze({ ...base, kind: "denied", reason: result.reason });
|
|
154
|
+
case "failed":
|
|
155
|
+
exactKeys(result, ["callId", "toolName", "kind", "code", "message"], `Tool result ${index}`);
|
|
156
|
+
if (typeof result.code !== "string" || typeof result.message !== "string")
|
|
157
|
+
fail(`Tool result ${index} code and message must be strings`);
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
...base,
|
|
160
|
+
kind: "failed",
|
|
161
|
+
code: result.code,
|
|
162
|
+
message: result.message,
|
|
163
|
+
});
|
|
164
|
+
default:
|
|
165
|
+
fail(`Tool result ${index} has unknown kind '${String(result.kind)}'`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function validateSeedCandidate(value, index) {
|
|
169
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
|
+
fail(`Candidate at transcript ${index} must be an object`);
|
|
171
|
+
const candidate = value;
|
|
172
|
+
exactKeys(candidate, ["output", "finishReason", "usage", "evidence"], `Candidate at transcript ${index}`);
|
|
173
|
+
if (!Array.isArray(candidate.output))
|
|
174
|
+
fail(`Candidate at transcript ${index} output must be an array`);
|
|
175
|
+
candidate.output.forEach((value, blockIndex) => {
|
|
176
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
177
|
+
fail(`Candidate output ${blockIndex} at transcript ${index} must be an object`);
|
|
178
|
+
const block = value;
|
|
179
|
+
if (block.type === "text" || block.type === "reasoning") {
|
|
180
|
+
exactKeys(block, ["type", "text"], `Candidate output ${blockIndex}`);
|
|
181
|
+
if (typeof block.text !== "string")
|
|
182
|
+
fail(`Candidate output ${blockIndex} text must be a string`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (block.type === "tool-call") {
|
|
186
|
+
exactKeys(block, ["type", "id", "name", "args", "raw"], `Candidate output ${blockIndex}`);
|
|
187
|
+
requiredString(block.id, `Candidate output ${blockIndex} id`);
|
|
188
|
+
requiredString(block.name, `Candidate output ${blockIndex} name`);
|
|
189
|
+
if (block.raw !== undefined && typeof block.raw !== "string")
|
|
190
|
+
fail(`Candidate output ${blockIndex} raw must be a string`);
|
|
191
|
+
copyJsonObject(block.args, `candidate output ${blockIndex} args`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
fail(`Candidate output ${blockIndex} has unknown kind '${String(block.type)}'`);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function exactKeys(value, allowed, label) {
|
|
198
|
+
for (const key of Object.keys(value))
|
|
199
|
+
if (!allowed.includes(key))
|
|
200
|
+
fail(`${label} has unknown field '${key}'`);
|
|
201
|
+
}
|
|
202
|
+
function count(value, label) {
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
return 0;
|
|
205
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
206
|
+
fail(`Session seed ${label} must be a non-negative safe integer`);
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function optionalString(value, label) {
|
|
210
|
+
if (value !== undefined)
|
|
211
|
+
requiredString(value, label);
|
|
212
|
+
}
|
|
213
|
+
function requiredString(value, label) {
|
|
214
|
+
if (typeof value !== "string" || value.length === 0)
|
|
215
|
+
fail(`${label} must be a non-empty string`);
|
|
216
|
+
}
|
|
217
|
+
function fail(message) {
|
|
218
|
+
throw new HarnessError("session.invalid-seed", message);
|
|
219
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { InputHandle, InputOptions, MessageInput, Session, SessionEvent,
|
|
1
|
+
import type { InputHandle, InputOptions, MessageInput, Session, SessionEvent, SessionRunOptions, SessionSnapshot, SessionInput } from "../types/session.js";
|
|
2
2
|
import type { Observer } from "../types/shared.js";
|
|
3
3
|
import type { LoopAgent } from "../build/agent.js";
|
|
4
4
|
export declare class LiveSession implements Session {
|
|
5
5
|
readonly id: string;
|
|
6
6
|
private readonly scheduler;
|
|
7
|
-
constructor(id: string, agent: LoopAgent, options:
|
|
7
|
+
constructor(id: string, agent: LoopAgent, options: SessionRunOptions);
|
|
8
8
|
get state(): SessionSnapshot;
|
|
9
9
|
input(event: SessionInput, options?: InputOptions): InputHandle;
|
|
10
10
|
interrupt(event: MessageInput, options?: InputOptions): InputHandle;
|
|
11
|
+
continue(options?: InputOptions): InputHandle;
|
|
11
12
|
stream(): AsyncIterable<SessionEvent>;
|
|
12
13
|
observe(listener: Observer): () => void;
|
|
13
14
|
stop(reason?: string): Promise<void>;
|
package/dist/session/session.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { SessionScheduler } from "./scheduler.js";
|
|
2
2
|
import { copyJsonObject } from "../utils/immutable.js";
|
|
3
|
+
import { normalizeSessionSeed } from "./seed.js";
|
|
3
4
|
export class LiveSession {
|
|
4
5
|
id;
|
|
5
6
|
scheduler;
|
|
6
7
|
constructor(id, agent, options) {
|
|
7
8
|
this.id = id;
|
|
9
|
+
const seed = "seed" in options && options.seed ? normalizeSessionSeed(options.seed) : undefined;
|
|
10
|
+
const userId = seed?.userId ?? ("userId" in options ? options.userId : undefined);
|
|
11
|
+
const suppliedContext = seed?.context ?? ("context" in options ? options.context : undefined);
|
|
8
12
|
const session = Object.freeze({
|
|
9
|
-
...(
|
|
10
|
-
...(
|
|
13
|
+
...(userId === undefined ? {} : { userId }),
|
|
14
|
+
...(suppliedContext === undefined
|
|
15
|
+
? {}
|
|
16
|
+
: { context: copyJsonObject(suppliedContext, "session context") }),
|
|
17
|
+
});
|
|
18
|
+
this.scheduler = new SessionScheduler(id, agent, session, {
|
|
19
|
+
...(seed === undefined ? {} : { seed }),
|
|
20
|
+
...(options.recorder === undefined ? {} : { recorder: options.recorder }),
|
|
11
21
|
});
|
|
12
|
-
this.scheduler = new SessionScheduler(id, agent, session);
|
|
13
22
|
}
|
|
14
23
|
get state() {
|
|
15
24
|
return this.scheduler.snapshot;
|
|
@@ -20,6 +29,9 @@ export class LiveSession {
|
|
|
20
29
|
interrupt(event, options) {
|
|
21
30
|
return this.scheduler.submit(normalizeMessage("interrupt", event), options);
|
|
22
31
|
}
|
|
32
|
+
continue(options) {
|
|
33
|
+
return this.scheduler.continue(options);
|
|
34
|
+
}
|
|
23
35
|
stream() {
|
|
24
36
|
return this.scheduler.events;
|
|
25
37
|
}
|
package/dist/session/state.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { InputEvent, SessionSnapshot } from "../types/session.js";
|
|
1
|
+
import type { ActiveExecutionRecord, InputEvent, SessionSnapshot, TranscriptEntry } from "../types/session.js";
|
|
2
2
|
import type { ModelCandidate } from "../types/model.js";
|
|
3
3
|
import type { RequiredInteraction, ToolResult } from "../types/tool.js";
|
|
4
|
-
export declare function initialState(id: string
|
|
5
|
-
|
|
4
|
+
export declare function initialState(id: string, input?: {
|
|
5
|
+
readonly turnCount?: number;
|
|
6
|
+
readonly revision?: number;
|
|
7
|
+
readonly transcript?: readonly TranscriptEntry[];
|
|
8
|
+
}): SessionSnapshot;
|
|
9
|
+
export declare function withStatus(state: SessionSnapshot, status: SessionSnapshot["status"], pendingInteraction?: RequiredInteraction, active?: ActiveExecutionRecord): SessionSnapshot;
|
|
10
|
+
export declare function withRevision(state: SessionSnapshot, revision: number): SessionSnapshot;
|
|
6
11
|
export declare function beginTurn(state: SessionSnapshot, turnId: string, event?: InputEvent): SessionSnapshot;
|
|
7
12
|
export declare const commitInput: (state: SessionSnapshot, turnId: string, event: InputEvent) => SessionSnapshot;
|
|
8
13
|
export declare const commitCandidate: (state: SessionSnapshot, turnId: string, stepId: string, candidate: ModelCandidate) => SessionSnapshot;
|
package/dist/session/state.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
export function initialState(id) {
|
|
2
|
-
return Object.freeze({
|
|
1
|
+
export function initialState(id, input = {}) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
id,
|
|
4
|
+
status: "idle",
|
|
5
|
+
turnCount: input.turnCount ?? 0,
|
|
6
|
+
revision: input.revision ?? 0,
|
|
7
|
+
transcript: Object.freeze([...(input.transcript ?? [])]),
|
|
8
|
+
});
|
|
3
9
|
}
|
|
4
|
-
export function withStatus(state, status, pendingInteraction) {
|
|
10
|
+
export function withStatus(state, status, pendingInteraction, active) {
|
|
5
11
|
return Object.freeze({
|
|
6
12
|
id: state.id,
|
|
7
13
|
status,
|
|
8
14
|
turnCount: state.turnCount,
|
|
15
|
+
revision: state.revision,
|
|
9
16
|
transcript: state.transcript,
|
|
10
17
|
...(pendingInteraction ? { pendingInteraction } : {}),
|
|
18
|
+
...(active ? { active } : {}),
|
|
11
19
|
});
|
|
12
20
|
}
|
|
21
|
+
export function withRevision(state, revision) {
|
|
22
|
+
return Object.freeze({ ...state, revision });
|
|
23
|
+
}
|
|
13
24
|
export function beginTurn(state, turnId, event) {
|
|
14
25
|
const transcript = event
|
|
15
26
|
? [
|
|
@@ -20,7 +31,8 @@ export function beginTurn(state, turnId, event) {
|
|
|
20
31
|
return Object.freeze({
|
|
21
32
|
id: state.id,
|
|
22
33
|
status: "running",
|
|
23
|
-
turnCount: state.turnCount +
|
|
34
|
+
turnCount: state.turnCount + 1,
|
|
35
|
+
revision: state.revision,
|
|
24
36
|
transcript: Object.freeze(transcript),
|
|
25
37
|
});
|
|
26
38
|
}
|
|
@@ -3,11 +3,13 @@ export declare class SubmissionStream implements InputHandle {
|
|
|
3
3
|
readonly inputId: string;
|
|
4
4
|
readonly completed: Promise<InputCompletion>;
|
|
5
5
|
private resolveCompletion;
|
|
6
|
+
private rejectCompletion;
|
|
6
7
|
private readonly events;
|
|
7
8
|
private readonly cleanups;
|
|
8
9
|
private done;
|
|
9
10
|
constructor(inputId: string);
|
|
10
11
|
emit(event: SessionEvent): void;
|
|
11
12
|
finish(status: InputCompletion["status"]): void;
|
|
13
|
+
fail(error: unknown): void;
|
|
12
14
|
onFinish(cleanup: () => void): void;
|
|
13
15
|
}
|
|
@@ -2,13 +2,15 @@ export class SubmissionStream {
|
|
|
2
2
|
inputId;
|
|
3
3
|
completed;
|
|
4
4
|
resolveCompletion;
|
|
5
|
+
rejectCompletion;
|
|
5
6
|
events = [];
|
|
6
7
|
cleanups = [];
|
|
7
8
|
done = false;
|
|
8
9
|
constructor(inputId) {
|
|
9
10
|
this.inputId = inputId;
|
|
10
|
-
this.completed = new Promise((resolve) => {
|
|
11
|
+
this.completed = new Promise((resolve, reject) => {
|
|
11
12
|
this.resolveCompletion = resolve;
|
|
13
|
+
this.rejectCompletion = reject;
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
emit(event) {
|
|
@@ -27,6 +29,14 @@ export class SubmissionStream {
|
|
|
27
29
|
events: Object.freeze([...this.events]),
|
|
28
30
|
}));
|
|
29
31
|
}
|
|
32
|
+
fail(error) {
|
|
33
|
+
if (this.done)
|
|
34
|
+
return;
|
|
35
|
+
this.done = true;
|
|
36
|
+
while (this.cleanups.length)
|
|
37
|
+
this.cleanups.pop()();
|
|
38
|
+
this.rejectCompletion(error);
|
|
39
|
+
}
|
|
30
40
|
onFinish(cleanup) {
|
|
31
41
|
if (this.done)
|
|
32
42
|
cleanup();
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { ModelConfigurationMutationOptions, ModelConfigurationSnapshot, ModelDirective } from "../types/model.js";
|
|
2
2
|
import type { ToolDefinition } from "../types/tool.js";
|
|
3
|
-
import type { AdapterRegistry } from "../build/adapters.js";
|
|
4
3
|
/** Per-step, middleware-owned model configuration draft. It never persists past the call. */
|
|
5
4
|
export declare class ModelConfigurationDraft {
|
|
6
5
|
#private;
|
|
7
|
-
|
|
8
|
-
constructor(adapters: AdapterRegistry, directive?: ModelDirective);
|
|
6
|
+
constructor(directive?: ModelDirective);
|
|
9
7
|
setInstructions(middlewareId: string, middlewareOrder: number, slot: string, items: readonly string[], options?: ModelConfigurationMutationOptions): void;
|
|
10
8
|
setTools(middlewareId: string, middlewareOrder: number, slot: string, tools: readonly ToolDefinition[], options?: ModelConfigurationMutationOptions): void;
|
|
11
9
|
select(middlewareId: string, middlewareOrder: number, directive: ModelDirective, options?: Omit<ModelConfigurationMutationOptions, "order">): void;
|
|
@@ -6,12 +6,10 @@ import { copyJson } from "../utils/immutable.js";
|
|
|
6
6
|
import { checkedReason, slotOwner, SlotDraft } from "./slot-assembly.js";
|
|
7
7
|
/** Per-step, middleware-owned model configuration draft. It never persists past the call. */
|
|
8
8
|
export class ModelConfigurationDraft {
|
|
9
|
-
adapters;
|
|
10
9
|
#instructions = new SlotDraft();
|
|
11
10
|
#tools = new SlotDraft();
|
|
12
11
|
#model;
|
|
13
|
-
constructor(
|
|
14
|
-
this.adapters = adapters;
|
|
12
|
+
constructor(directive) {
|
|
15
13
|
if (directive !== undefined)
|
|
16
14
|
this.#model = Object.freeze({ directive: checkedDirective(directive) });
|
|
17
15
|
}
|
|
@@ -38,7 +36,7 @@ export class ModelConfigurationDraft {
|
|
|
38
36
|
middlewareId,
|
|
39
37
|
middlewareOrder,
|
|
40
38
|
slot,
|
|
41
|
-
value: Object.freeze(tools.map((tool) => bindTool(tool,
|
|
39
|
+
value: Object.freeze(tools.map((tool) => bindTool(tool, { middlewareId, slot }))),
|
|
42
40
|
order: options?.order,
|
|
43
41
|
reason: options?.reason,
|
|
44
42
|
invalidSlot: "configuration.invalid-slot",
|
package/dist/step/project.js
CHANGED
|
@@ -81,8 +81,10 @@ function projectToolResult(result) {
|
|
|
81
81
|
}
|
|
82
82
|
function toolResultPayload(result) {
|
|
83
83
|
if (result.kind === "completed")
|
|
84
|
-
return result.output
|
|
85
|
-
|
|
84
|
+
return result.output;
|
|
85
|
+
if (result.kind === "denied")
|
|
86
|
+
return { kind: result.kind, reason: result.reason };
|
|
87
|
+
return { kind: result.kind, code: result.code, message: result.message };
|
|
86
88
|
}
|
|
87
89
|
function textPart(text) {
|
|
88
90
|
return Object.freeze({ type: "text", text });
|
package/dist/step/run.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ModelCandidate } from "../types/model.js";
|
|
2
|
-
import type { InputEvent, SessionSnapshot } from "../types/session.js";
|
|
2
|
+
import type { ActiveModelExecutionRecord, InputEvent, SessionSnapshot } from "../types/session.js";
|
|
3
3
|
import type { LoopAgent } from "../build/agent.js";
|
|
4
4
|
import type { ObserveEmit } from "../utils/observe.js";
|
|
5
5
|
import { type SealedStepOutput } from "./seal.js";
|
|
6
|
+
import type { CapabilityStateRegistry } from "../session/capability-state.js";
|
|
6
7
|
export interface StepRunResult {
|
|
7
8
|
readonly stepId: string;
|
|
9
|
+
readonly state: SessionSnapshot;
|
|
8
10
|
readonly candidate?: ModelCandidate;
|
|
9
11
|
readonly output: SealedStepOutput;
|
|
12
|
+
readonly requestedModelId?: string;
|
|
10
13
|
}
|
|
11
14
|
export declare function runStep(input: {
|
|
12
15
|
agent: LoopAgent;
|
|
@@ -24,4 +27,6 @@ export declare function runStep(input: {
|
|
|
24
27
|
readonly userId?: string;
|
|
25
28
|
readonly context?: import("../types/shared.js").JsonObject;
|
|
26
29
|
}>;
|
|
30
|
+
states: CapabilityStateRegistry;
|
|
31
|
+
recordModelRequested(state: SessionSnapshot, active: ActiveModelExecutionRecord): Promise<SessionSnapshot>;
|
|
27
32
|
}): Promise<StepRunResult>;
|
package/dist/step/run.js
CHANGED
|
@@ -8,7 +8,10 @@ import { projectModelCall } from "./project.js";
|
|
|
8
8
|
import { resolveModelRequest } from "./resolve.js";
|
|
9
9
|
import { sealStep } from "./seal.js";
|
|
10
10
|
import { ModelConfigurationDraft } from "./model-configuration.js";
|
|
11
|
+
import { createId } from "../utils/ids.js";
|
|
11
12
|
export async function runStep(input) {
|
|
13
|
+
let state = input.state;
|
|
14
|
+
let requestedModelId;
|
|
12
15
|
const stepInput = Object.freeze({
|
|
13
16
|
sessionId: input.sessionId,
|
|
14
17
|
turnId: input.turnId,
|
|
@@ -20,9 +23,9 @@ export async function runStep(input) {
|
|
|
20
23
|
toolResults: Object.freeze([...input.toolResults]),
|
|
21
24
|
transcript: Object.freeze([...input.state.transcript]),
|
|
22
25
|
});
|
|
23
|
-
const configuration = new ModelConfigurationDraft(
|
|
26
|
+
const configuration = new ModelConfigurationDraft();
|
|
24
27
|
const runtimeContext = new ContextDraft(input.session.context);
|
|
25
|
-
const context = new StepContext(stepInput, input.observe, configuration, runtimeContext);
|
|
28
|
+
const context = new StepContext(stepInput, input.observe, configuration, runtimeContext, input.states);
|
|
26
29
|
input.observe(() => ({
|
|
27
30
|
type: "step.started",
|
|
28
31
|
turnId: input.turnId,
|
|
@@ -50,7 +53,17 @@ export async function runStep(input) {
|
|
|
50
53
|
arrivals: input.arrivals,
|
|
51
54
|
toolResults: input.toolResults,
|
|
52
55
|
});
|
|
56
|
+
requestedModelId = request.model?.id;
|
|
53
57
|
const call = projectModelCall(request);
|
|
58
|
+
const invocationId = createId("invocation");
|
|
59
|
+
const active = Object.freeze({
|
|
60
|
+
kind: "model",
|
|
61
|
+
turnId: input.turnId,
|
|
62
|
+
stepId: input.stepId,
|
|
63
|
+
invocationId,
|
|
64
|
+
call,
|
|
65
|
+
});
|
|
66
|
+
state = await input.recordModelRequested(state, active);
|
|
54
67
|
input.observe(() => ({
|
|
55
68
|
type: "model.requested",
|
|
56
69
|
turnId: input.turnId,
|
|
@@ -63,22 +76,22 @@ export async function runStep(input) {
|
|
|
63
76
|
}),
|
|
64
77
|
}));
|
|
65
78
|
try {
|
|
66
|
-
const
|
|
79
|
+
const outcome = await input.agent.invoke(call, {
|
|
67
80
|
request,
|
|
81
|
+
invocationId,
|
|
68
82
|
signal: input.signal,
|
|
69
|
-
})
|
|
83
|
+
});
|
|
70
84
|
if (input.signal.aborted)
|
|
71
85
|
throw input.signal.reason;
|
|
86
|
+
if (isDeferred(outcome))
|
|
87
|
+
return context.deferModel(Object.freeze({
|
|
88
|
+
...active,
|
|
89
|
+
...(outcome.token === undefined ? {} : { token: copyJson(outcome.token) }),
|
|
90
|
+
}));
|
|
91
|
+
const minted = context.mintFromModel(normalizeCandidate(outcome));
|
|
72
92
|
const candidate = context.currentCandidate;
|
|
73
93
|
if (!candidate)
|
|
74
94
|
throw new HarnessError("model.candidate-missing", "Model candidate missing after mint");
|
|
75
|
-
input.observe(() => ({
|
|
76
|
-
type: "model.completed",
|
|
77
|
-
turnId: input.turnId,
|
|
78
|
-
stepId: input.stepId,
|
|
79
|
-
...(request.model?.id === undefined ? {} : { requestedModelId: request.model.id }),
|
|
80
|
-
attributes: candidate,
|
|
81
|
-
}));
|
|
82
95
|
return minted;
|
|
83
96
|
}
|
|
84
97
|
catch (error) {
|
|
@@ -92,7 +105,16 @@ export async function runStep(input) {
|
|
|
92
105
|
}, input.observe);
|
|
93
106
|
const output = sealStep(context);
|
|
94
107
|
const candidate = output.kind === "tools" ? output.plan.candidate : context.currentCandidate;
|
|
95
|
-
return Object.freeze({
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
stepId: input.stepId,
|
|
110
|
+
state,
|
|
111
|
+
...(candidate ? { candidate } : {}),
|
|
112
|
+
...(requestedModelId === undefined ? {} : { requestedModelId }),
|
|
113
|
+
output,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function isDeferred(value) {
|
|
117
|
+
return (typeof value === "object" && value !== null && "kind" in value && value.kind === "deferred");
|
|
96
118
|
}
|
|
97
119
|
function snapshotStepStart(input) {
|
|
98
120
|
const session = Object.freeze({
|
|
@@ -121,7 +143,7 @@ function snapshotTools(tools) {
|
|
|
121
143
|
return Object.freeze(tools.map((tool) => Object.freeze({
|
|
122
144
|
name: tool.name,
|
|
123
145
|
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
124
|
-
|
|
146
|
+
owner: tool.owner,
|
|
125
147
|
parameters: Object.freeze({ jsonSchema: copyJson(tool.parameters.jsonSchema) }),
|
|
126
148
|
})));
|
|
127
149
|
}
|
package/dist/step/seal.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ModelCandidate } from "../types/model.js";
|
|
2
2
|
import type { Tripwire } from "../types/shared.js";
|
|
3
|
-
import type { RequiredInteraction, SealedToolCall, ToolResult } from "../types/tool.js";
|
|
3
|
+
import type { BoundToolDefinition, RequiredInteraction, SealedToolCall, ToolOwner, ToolResult } from "../types/tool.js";
|
|
4
4
|
import type { StepContext } from "./step-context.js";
|
|
5
5
|
export interface ExecutablePlanEntry {
|
|
6
6
|
readonly call: SealedToolCall;
|
|
7
7
|
readonly invocationId: string;
|
|
8
|
+
readonly owner: ToolOwner;
|
|
9
|
+
readonly execute: BoundToolDefinition["execute"];
|
|
8
10
|
readonly interaction?: RequiredInteraction;
|
|
9
|
-
readonly preflight?: "sandbox" | "validation";
|
|
10
11
|
}
|
|
11
12
|
export interface InternalToolPlan {
|
|
12
13
|
readonly candidate: ModelCandidate;
|
|
@@ -23,6 +24,9 @@ export type SealedStepOutput = {
|
|
|
23
24
|
} | {
|
|
24
25
|
readonly kind: "final";
|
|
25
26
|
readonly output: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "deferred-model";
|
|
29
|
+
readonly active: import("../types/session.js").ActiveModelExecutionRecord;
|
|
26
30
|
} | {
|
|
27
31
|
readonly kind: "tools";
|
|
28
32
|
readonly plan: InternalToolPlan;
|
package/dist/step/seal.js
CHANGED
|
@@ -7,6 +7,8 @@ const failed = (callId, toolName, code, message) => Object.freeze({ kind: "faile
|
|
|
7
7
|
export function sealStep(context) {
|
|
8
8
|
if (context.currentTripwire)
|
|
9
9
|
return Object.freeze({ kind: "tripwire", tripwire: context.currentTripwire });
|
|
10
|
+
if (context.currentModelDeferred)
|
|
11
|
+
return Object.freeze({ kind: "deferred-model", active: context.currentModelDeferred });
|
|
10
12
|
const calls = context.canonicalCalls();
|
|
11
13
|
if (!calls.length)
|
|
12
14
|
return Object.freeze({
|
|
@@ -75,7 +77,6 @@ function sealCall(context, catalog, candidate) {
|
|
|
75
77
|
const interaction = requested
|
|
76
78
|
? Object.freeze({ ...requested, id: requested.id ?? createId("interaction") })
|
|
77
79
|
: undefined;
|
|
78
|
-
const preflight = context.preflightFor(candidate.id);
|
|
79
80
|
return {
|
|
80
81
|
order,
|
|
81
82
|
canonical,
|
|
@@ -84,11 +85,11 @@ function sealCall(context, catalog, candidate) {
|
|
|
84
85
|
callId: candidate.id,
|
|
85
86
|
toolName: candidate.name,
|
|
86
87
|
args,
|
|
87
|
-
executeWith: tool.executeWith,
|
|
88
88
|
}),
|
|
89
89
|
invocationId: createId("invocation"),
|
|
90
|
+
owner: tool.owner,
|
|
91
|
+
execute: tool.execute,
|
|
90
92
|
...(interaction ? { interaction } : {}),
|
|
91
|
-
...(preflight ? { preflight } : {}),
|
|
92
93
|
}),
|
|
93
94
|
};
|
|
94
95
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import type { ContextSnapshot, ModelCandidate, ModelDirective, ModelConfigurationSnapshot } from "../types/model.js";
|
|
2
2
|
import type { StepInput, StepRequest, StepResponse } from "../types/middleware.js";
|
|
3
3
|
import type { Tripwire } from "../types/shared.js";
|
|
4
|
+
import type { ActiveModelExecutionRecord } from "../types/session.js";
|
|
4
5
|
import type { ObserveEmit } from "../utils/observe.js";
|
|
5
6
|
import type { BoundToolDefinition, Interaction } from "../types/tool.js";
|
|
6
7
|
import { type CanonicalCall } from "./canonicalize.js";
|
|
7
8
|
import { ContextDraft } from "./context-draft.js";
|
|
8
9
|
import { ModelConfigurationDraft } from "./model-configuration.js";
|
|
10
|
+
import type { CapabilityStateRegistry } from "../session/capability-state.js";
|
|
9
11
|
export declare function isBrandedResponse(value: unknown): value is StepResponse;
|
|
10
12
|
/** Mutable Step state. Middleware receives leased request views and one branded response. */
|
|
11
13
|
export declare class StepContext {
|
|
12
14
|
#private;
|
|
13
15
|
readonly input: Readonly<StepInput>;
|
|
14
|
-
constructor(input: Readonly<StepInput>, observe: ObserveEmit, configuration: ModelConfigurationDraft, context: ContextDraft);
|
|
16
|
+
constructor(input: Readonly<StepInput>, observe: ObserveEmit, configuration: ModelConfigurationDraft, context: ContextDraft, states?: CapabilityStateRegistry);
|
|
15
17
|
get currentTripwire(): Tripwire | undefined;
|
|
16
18
|
get currentCandidate(): Readonly<ModelCandidate> | undefined;
|
|
19
|
+
get currentModelDeferred(): ActiveModelExecutionRecord | undefined;
|
|
17
20
|
get selectedDirective(): ModelDirective | undefined;
|
|
18
21
|
get instructions(): readonly string[];
|
|
19
22
|
contextSnapshot(): ContextSnapshot;
|
|
@@ -24,9 +27,9 @@ export declare class StepContext {
|
|
|
24
27
|
sealContext(snapshot: ContextSnapshot): void;
|
|
25
28
|
denialFor(callId: string): string | undefined;
|
|
26
29
|
interactionFor(callId: string): Interaction | undefined;
|
|
27
|
-
preflightFor(callId: string): "sandbox" | "validation" | undefined;
|
|
28
30
|
canonicalCalls(): readonly CanonicalCall[];
|
|
29
31
|
mintFromModel(candidate: Readonly<ModelCandidate>): StepResponse;
|
|
32
|
+
deferModel(active: ActiveModelExecutionRecord): StepResponse;
|
|
30
33
|
tripwire(error: Tripwire): StepResponse;
|
|
31
34
|
seal(): void;
|
|
32
35
|
requestFacade(middlewareId: string, middlewareOrder: number): {
|