@nylorun/harness 0.8.0-beta.1 → 0.11.0-beta
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 +64 -0
- package/README.md +5 -38
- package/dist/build/assemble.js +1 -0
- package/dist/build/bind-tool.js +4 -3
- package/dist/build/builder.js +26 -0
- package/dist/build/helpers.d.ts +2 -2
- package/dist/build/manifest.js +12 -1
- package/dist/build/schema.d.ts +11 -16
- package/dist/build/schema.js +141 -22
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +2 -0
- package/dist/model/adapters.d.ts +160 -0
- package/dist/model/adapters.js +545 -0
- package/dist/{model-normalize.d.ts → model/normalize.d.ts} +2 -2
- package/dist/{model-normalize.js → model/normalize.js} +35 -4
- package/dist/model/prepared.d.ts +16 -0
- package/dist/model/prepared.js +11 -0
- package/dist/session/event-log.d.ts +4 -3
- package/dist/session/input-queue.d.ts +7 -4
- package/dist/session/input-queue.js +12 -0
- package/dist/session/output-contract.d.ts +6 -0
- package/dist/session/output-contract.js +12 -0
- package/dist/session/scheduler.js +1 -1
- package/dist/session/seed.js +79 -5
- package/dist/session/session.d.ts +8 -5
- package/dist/session/session.js +38 -2
- package/dist/session/state.d.ts +1 -1
- package/dist/session/submission-stream.d.ts +5 -4
- package/dist/step/context-draft.js +1 -7
- package/dist/step/model-configuration.js +6 -20
- package/dist/step/project.js +25 -3
- package/dist/step/resolve.d.ts +1 -0
- package/dist/step/resolve.js +1 -0
- package/dist/step/run.d.ts +1 -0
- package/dist/step/run.js +25 -4
- package/dist/step/seal.d.ts +4 -2
- package/dist/step/seal.js +64 -13
- package/dist/step/step-context.js +1 -1
- package/dist/turn/plan-runner.js +38 -3
- package/dist/turn/runner.d.ts +6 -4
- package/dist/turn/runner.js +6 -4
- package/dist/types/manifest.d.ts +5 -4
- package/dist/types/middleware.d.ts +10 -1
- package/dist/types/model.d.ts +21 -13
- package/dist/types/session.d.ts +34 -13
- package/dist/types/shared.d.ts +16 -3
- package/dist/types/tool.d.ts +54 -17
- package/package.json +15 -12
- package/dist/utils/digest.d.ts +0 -1
- package/dist/utils/digest.js +0 -14
|
@@ -7,6 +7,18 @@ export function snapshotInput(event) {
|
|
|
7
7
|
switch (event.kind) {
|
|
8
8
|
case "user-message":
|
|
9
9
|
case "interrupt":
|
|
10
|
+
if ("content" in event)
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
kind: event.kind,
|
|
13
|
+
content: Object.freeze(event.content.map((part) => part.type === "text"
|
|
14
|
+
? Object.freeze({ type: "text", text: part.text })
|
|
15
|
+
: Object.freeze({
|
|
16
|
+
type: "media",
|
|
17
|
+
mediaType: part.mediaType,
|
|
18
|
+
reference: copyJson(part.reference),
|
|
19
|
+
}))),
|
|
20
|
+
...(event.metadata ? { metadata: copyJsonObject(event.metadata, "input metadata") } : {}),
|
|
21
|
+
});
|
|
10
22
|
return Object.freeze({
|
|
11
23
|
kind: event.kind,
|
|
12
24
|
text: event.text,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BoundToolSchema, ToolSchemaSource } from "../types/tool.js";
|
|
2
|
+
/** Runtime-only validator paired with the portable JSON Schema projected to models. */
|
|
3
|
+
export interface TurnOutputContract {
|
|
4
|
+
readonly schema: BoundToolSchema<unknown>;
|
|
5
|
+
}
|
|
6
|
+
export declare function bindOutputContract(source: ToolSchemaSource): TurnOutputContract;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HarnessError, isHarnessError } from "../errors.js";
|
|
2
|
+
import { normalizeSchema } from "../build/schema.js";
|
|
3
|
+
export function bindOutputContract(source) {
|
|
4
|
+
try {
|
|
5
|
+
return Object.freeze({ schema: normalizeSchema(source, "output") });
|
|
6
|
+
}
|
|
7
|
+
catch (cause) {
|
|
8
|
+
if (isHarnessError(cause))
|
|
9
|
+
throw new HarnessError("output.invalid-schema", cause.message, { cause });
|
|
10
|
+
throw new HarnessError("output.invalid-schema", String(cause), { cause });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -257,7 +257,7 @@ export class SessionScheduler {
|
|
|
257
257
|
? await this.resumeTurn(submission, context)
|
|
258
258
|
: submission.event.kind === "continue"
|
|
259
259
|
? await this.turns.continue(this.snapshotValue, context)
|
|
260
|
-
: await this.turns.start(this.snapshotValue, submission.event, context);
|
|
260
|
+
: await this.turns.start(this.snapshotValue, submission.event, context, submission.options?.output);
|
|
261
261
|
this.applyTurnOutcome(submission.stream, outcome);
|
|
262
262
|
}
|
|
263
263
|
catch (error) {
|
package/dist/session/seed.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HarnessError } from "../errors.js";
|
|
2
|
-
import { normalizeCandidate } from "../model
|
|
2
|
+
import { normalizeCandidate } from "../model/normalize.js";
|
|
3
3
|
import { assertJson, copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
4
4
|
export function normalizeSessionSeed(seed) {
|
|
5
5
|
try {
|
|
@@ -82,13 +82,12 @@ function entryAt(value, index) {
|
|
|
82
82
|
case "final":
|
|
83
83
|
exactKeys(entry, ["kind", "turnId", "stepId", "output"], `Transcript entry ${index}`);
|
|
84
84
|
requiredString(entry.stepId, `Transcript entry ${index} stepId`);
|
|
85
|
-
|
|
86
|
-
fail(`Transcript entry ${index} output must be a string`);
|
|
85
|
+
assertJson(entry.output, `Transcript entry ${index} output`);
|
|
87
86
|
return Object.freeze({
|
|
88
87
|
kind: "final",
|
|
89
88
|
turnId: entry.turnId,
|
|
90
89
|
stepId: entry.stepId,
|
|
91
|
-
output: entry.output,
|
|
90
|
+
output: copyJson(entry.output),
|
|
92
91
|
});
|
|
93
92
|
default:
|
|
94
93
|
fail(`Transcript entry ${index} has unknown kind '${String(entry.kind)}'`);
|
|
@@ -101,6 +100,18 @@ function inputEvent(value, index) {
|
|
|
101
100
|
switch (event.kind) {
|
|
102
101
|
case "user-message":
|
|
103
102
|
case "interrupt":
|
|
103
|
+
if ("content" in event) {
|
|
104
|
+
exactKeys(event, ["kind", "content", "metadata"], `Transcript input ${index}`);
|
|
105
|
+
if (!Array.isArray(event.content))
|
|
106
|
+
fail(`Transcript input ${index} content must be an array`);
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
kind: event.kind,
|
|
109
|
+
content: contentParts(event.content, index),
|
|
110
|
+
...(event.metadata === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { metadata: copyJsonObject(event.metadata, `transcript input ${index} metadata`) }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
104
115
|
exactKeys(event, ["kind", "text", "metadata"], `Transcript input ${index}`);
|
|
105
116
|
if (typeof event.text !== "string")
|
|
106
117
|
fail(`Transcript input ${index} text must be a string`);
|
|
@@ -134,6 +145,31 @@ function inputEvent(value, index) {
|
|
|
134
145
|
fail(`Transcript input ${index} has unknown kind '${String(event.kind)}'`);
|
|
135
146
|
}
|
|
136
147
|
}
|
|
148
|
+
function contentParts(value, inputIndex) {
|
|
149
|
+
return Object.freeze(value.map((value, partIndex) => {
|
|
150
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
151
|
+
fail(`Transcript input ${inputIndex} content part ${partIndex} must be an object`);
|
|
152
|
+
const part = value;
|
|
153
|
+
if (part.type === "text") {
|
|
154
|
+
exactKeys(part, ["type", "text"], `Transcript input ${inputIndex} content part ${partIndex}`);
|
|
155
|
+
if (typeof part.text !== "string")
|
|
156
|
+
fail(`Transcript input ${inputIndex} text part ${partIndex} must contain text`);
|
|
157
|
+
return Object.freeze({ type: "text", text: part.text });
|
|
158
|
+
}
|
|
159
|
+
if (part.type === "media") {
|
|
160
|
+
exactKeys(part, ["type", "mediaType", "reference"], `Transcript input ${inputIndex} content part ${partIndex}`);
|
|
161
|
+
if (typeof part.mediaType !== "string" || part.mediaType === "")
|
|
162
|
+
fail(`Transcript input ${inputIndex} media part ${partIndex} must contain mediaType`);
|
|
163
|
+
assertJson(part.reference, `transcript input ${inputIndex} media reference ${partIndex}`);
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
type: "media",
|
|
166
|
+
mediaType: part.mediaType,
|
|
167
|
+
reference: copyJson(part.reference),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
fail(`Transcript input ${inputIndex} content part ${partIndex} has unknown type`);
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
137
173
|
function toolResult(value, index) {
|
|
138
174
|
if (!value || typeof value !== "object")
|
|
139
175
|
fail(`Tool result ${index} must be an object`);
|
|
@@ -152,7 +188,7 @@ function toolResult(value, index) {
|
|
|
152
188
|
fail(`Tool result ${index} reason must be a string`);
|
|
153
189
|
return Object.freeze({ ...base, kind: "denied", reason: result.reason });
|
|
154
190
|
case "failed":
|
|
155
|
-
exactKeys(result, ["callId", "toolName", "kind", "code", "message"], `Tool result ${index}`);
|
|
191
|
+
exactKeys(result, ["callId", "toolName", "kind", "code", "message", "details"], `Tool result ${index}`);
|
|
156
192
|
if (typeof result.code !== "string" || typeof result.message !== "string")
|
|
157
193
|
fail(`Tool result ${index} code and message must be strings`);
|
|
158
194
|
return Object.freeze({
|
|
@@ -160,11 +196,44 @@ function toolResult(value, index) {
|
|
|
160
196
|
kind: "failed",
|
|
161
197
|
code: result.code,
|
|
162
198
|
message: result.message,
|
|
199
|
+
...(result.details === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { details: validationDetails(result.details, index) }),
|
|
163
202
|
});
|
|
164
203
|
default:
|
|
165
204
|
fail(`Tool result ${index} has unknown kind '${String(result.kind)}'`);
|
|
166
205
|
}
|
|
167
206
|
}
|
|
207
|
+
function validationDetails(value, index) {
|
|
208
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
209
|
+
fail(`Tool result ${index} details must be an object`);
|
|
210
|
+
const details = value;
|
|
211
|
+
exactKeys(details, ["phase", "issues"], `Tool result ${index} details`);
|
|
212
|
+
if (details.phase !== "input" && details.phase !== "output")
|
|
213
|
+
fail(`Tool result ${index} details phase must be input or output`);
|
|
214
|
+
if (!Array.isArray(details.issues))
|
|
215
|
+
fail(`Tool result ${index} details issues must be an array`);
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
phase: details.phase,
|
|
218
|
+
issues: Object.freeze(details.issues.map((issue, issueIndex) => {
|
|
219
|
+
if (!issue || typeof issue !== "object" || Array.isArray(issue))
|
|
220
|
+
fail(`Tool result ${index} details issue ${issueIndex} must be an object`);
|
|
221
|
+
const item = issue;
|
|
222
|
+
exactKeys(item, ["path", "code", "message"], `Tool result ${index} details issue ${issueIndex}`);
|
|
223
|
+
if (!Array.isArray(item.path))
|
|
224
|
+
fail(`Tool result ${index} details issue ${issueIndex} path must be an array`);
|
|
225
|
+
if (item.path.some((part) => typeof part !== "string" && typeof part !== "number"))
|
|
226
|
+
fail(`Tool result ${index} details issue ${issueIndex} path must contain strings or numbers`);
|
|
227
|
+
if (typeof item.code !== "string" || typeof item.message !== "string")
|
|
228
|
+
fail(`Tool result ${index} details issue ${issueIndex} code and message must be strings`);
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
path: Object.freeze([...item.path]),
|
|
231
|
+
code: item.code,
|
|
232
|
+
message: item.message,
|
|
233
|
+
});
|
|
234
|
+
})),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
168
237
|
function validateSeedCandidate(value, index) {
|
|
169
238
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
239
|
fail(`Candidate at transcript ${index} must be an object`);
|
|
@@ -182,6 +251,11 @@ function validateSeedCandidate(value, index) {
|
|
|
182
251
|
fail(`Candidate output ${blockIndex} text must be a string`);
|
|
183
252
|
return;
|
|
184
253
|
}
|
|
254
|
+
if (block.type === "json") {
|
|
255
|
+
exactKeys(block, ["type", "value"], `Candidate output ${blockIndex}`);
|
|
256
|
+
assertJson(block.value, `Candidate output ${blockIndex} value`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
185
259
|
if (block.type === "tool-call") {
|
|
186
260
|
exactKeys(block, ["type", "id", "name", "args", "raw"], `Candidate output ${blockIndex}`);
|
|
187
261
|
requiredString(block.id, `Candidate output ${blockIndex} id`);
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { InputHandle, InputOptions, MessageInput, Session, SessionEvent, SessionRunOptions, SessionSnapshot, SessionInput } from "../types/session.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ToolSchemaSource } from "../types/tool.js";
|
|
3
|
+
import type { JsonValue, Observer } from "../types/shared.js";
|
|
3
4
|
import type { LoopAgent } from "../build/agent.js";
|
|
4
5
|
export declare class LiveSession implements Session {
|
|
5
6
|
readonly id: string;
|
|
6
7
|
private readonly scheduler;
|
|
7
8
|
constructor(id: string, agent: LoopAgent, options: SessionRunOptions);
|
|
8
9
|
get state(): SessionSnapshot;
|
|
9
|
-
input(event: SessionInput, options?: InputOptions
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
input(event: SessionInput, options?: InputOptions & {
|
|
11
|
+
readonly outputSchema?: ToolSchemaSource;
|
|
12
|
+
}): InputHandle<any>;
|
|
13
|
+
interrupt(event: MessageInput, options?: InputOptions): InputHandle<any>;
|
|
14
|
+
continue(options?: InputOptions): InputHandle<any>;
|
|
15
|
+
stream(): AsyncIterable<SessionEvent<JsonValue>>;
|
|
13
16
|
observe(listener: Observer): () => void;
|
|
14
17
|
stop(reason?: string): Promise<void>;
|
|
15
18
|
}
|
package/dist/session/session.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { HarnessError } from "../errors.js";
|
|
1
2
|
import { SessionScheduler } from "./scheduler.js";
|
|
2
|
-
import { copyJsonObject } from "../utils/immutable.js";
|
|
3
|
+
import { copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
3
4
|
import { normalizeSessionSeed } from "./seed.js";
|
|
5
|
+
import { bindOutputContract } from "./output-contract.js";
|
|
4
6
|
export class LiveSession {
|
|
5
7
|
id;
|
|
6
8
|
scheduler;
|
|
@@ -24,7 +26,13 @@ export class LiveSession {
|
|
|
24
26
|
return this.scheduler.snapshot;
|
|
25
27
|
}
|
|
26
28
|
input(event, options) {
|
|
27
|
-
|
|
29
|
+
const output = options?.outputSchema === undefined ? undefined : bindOutputContract(options.outputSchema);
|
|
30
|
+
if (output && isInteractionReply(event))
|
|
31
|
+
throw new HarnessError("output.invalid-schema", "Interaction replies cannot define a turn outputSchema");
|
|
32
|
+
return this.scheduler.submit(normalizeInput(event), {
|
|
33
|
+
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
34
|
+
...(output === undefined ? {} : { output }),
|
|
35
|
+
});
|
|
28
36
|
}
|
|
29
37
|
interrupt(event, options) {
|
|
30
38
|
return this.scheduler.submit(normalizeMessage("interrupt", event), options);
|
|
@@ -50,6 +58,14 @@ function isInteractionReply(value) {
|
|
|
50
58
|
function normalizeMessage(kind, value) {
|
|
51
59
|
if (typeof value === "string")
|
|
52
60
|
return { kind, text: value };
|
|
61
|
+
if ("content" in value)
|
|
62
|
+
return {
|
|
63
|
+
kind,
|
|
64
|
+
content: snapshotContent(value.content),
|
|
65
|
+
...(value.metadata === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { metadata: copyJsonObject(value.metadata, "input metadata") }),
|
|
68
|
+
};
|
|
53
69
|
return {
|
|
54
70
|
kind,
|
|
55
71
|
text: value.text,
|
|
@@ -65,3 +81,23 @@ function normalizeInput(input) {
|
|
|
65
81
|
return input;
|
|
66
82
|
return normalizeMessage("user-message", input);
|
|
67
83
|
}
|
|
84
|
+
function snapshotContent(value) {
|
|
85
|
+
if (!Array.isArray(value))
|
|
86
|
+
throw new HarnessError("input.invalid-content", "Input content must be an array");
|
|
87
|
+
return Object.freeze(value.map((part, index) => {
|
|
88
|
+
if (!part || typeof part !== "object")
|
|
89
|
+
throw new HarnessError("input.invalid-content", `Input content part ${index} must be an object`);
|
|
90
|
+
if (part.type === "text") {
|
|
91
|
+
if (typeof part.text !== "string")
|
|
92
|
+
throw new HarnessError("input.invalid-content", `Input text part ${index} must contain text`);
|
|
93
|
+
return Object.freeze({ type: "text", text: part.text });
|
|
94
|
+
}
|
|
95
|
+
if (part.type !== "media" || typeof part.mediaType !== "string" || part.mediaType === "")
|
|
96
|
+
throw new HarnessError("input.invalid-content", `Input media part ${index} must contain a non-empty mediaType`);
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
type: "media",
|
|
99
|
+
mediaType: part.mediaType,
|
|
100
|
+
reference: copyJson(part.reference),
|
|
101
|
+
});
|
|
102
|
+
}));
|
|
103
|
+
}
|
package/dist/session/state.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ export declare function beginTurn(state: SessionSnapshot, turnId: string, event?
|
|
|
12
12
|
export declare const commitInput: (state: SessionSnapshot, turnId: string, event: InputEvent) => SessionSnapshot;
|
|
13
13
|
export declare const commitCandidate: (state: SessionSnapshot, turnId: string, stepId: string, candidate: ModelCandidate) => SessionSnapshot;
|
|
14
14
|
export declare const commitToolResults: (state: SessionSnapshot, turnId: string, stepId: string, results: readonly ToolResult[]) => SessionSnapshot;
|
|
15
|
-
export declare const commitFinal: (state: SessionSnapshot, turnId: string, stepId: string, output:
|
|
15
|
+
export declare const commitFinal: (state: SessionSnapshot, turnId: string, stepId: string, output: import("../types/shared.js").JsonValue) => SessionSnapshot;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { InputCompletion, InputHandle, SessionEvent } from "../types/session.js";
|
|
2
|
-
|
|
2
|
+
import type { JsonValue } from "../types/shared.js";
|
|
3
|
+
export declare class SubmissionStream implements InputHandle<JsonValue> {
|
|
3
4
|
readonly inputId: string;
|
|
4
|
-
readonly completed: Promise<InputCompletion
|
|
5
|
+
readonly completed: Promise<InputCompletion<JsonValue>>;
|
|
5
6
|
private resolveCompletion;
|
|
6
7
|
private rejectCompletion;
|
|
7
8
|
private readonly events;
|
|
8
9
|
private readonly cleanups;
|
|
9
10
|
private done;
|
|
10
11
|
constructor(inputId: string);
|
|
11
|
-
emit(event: SessionEvent): void;
|
|
12
|
-
finish(status: InputCompletion["status"]): void;
|
|
12
|
+
emit(event: SessionEvent<JsonValue>): void;
|
|
13
|
+
finish(status: InputCompletion<JsonValue>["status"]): void;
|
|
13
14
|
fail(error: unknown): void;
|
|
14
15
|
onFinish(cleanup: () => void): void;
|
|
15
16
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { HarnessError } from "../errors.js";
|
|
2
|
-
import { digest } from "../utils/digest.js";
|
|
3
2
|
import { copyJson } from "../utils/immutable.js";
|
|
4
3
|
import { SlotDraft } from "./slot-assembly.js";
|
|
5
4
|
const CONTEXT_TYPE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
@@ -36,7 +35,7 @@ export class ContextDraft {
|
|
|
36
35
|
...(this.hostContext === undefined ? [] : [hostContributor()]),
|
|
37
36
|
...slots.map((slot) => contributor(slot.owner, slot.reason)),
|
|
38
37
|
]);
|
|
39
|
-
return Object.freeze({ items, contributors
|
|
38
|
+
return Object.freeze({ items, contributors });
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
function normalizeItem(item) {
|
|
@@ -54,7 +53,6 @@ function hostContributor() {
|
|
|
54
53
|
middlewareId: "host",
|
|
55
54
|
slot: "session",
|
|
56
55
|
order: 0,
|
|
57
|
-
digest: digest({ middlewareId: "host", slot: "session", order: 0 }),
|
|
58
56
|
});
|
|
59
57
|
}
|
|
60
58
|
function contributor(owner, reason) {
|
|
@@ -62,10 +60,6 @@ function contributor(owner, reason) {
|
|
|
62
60
|
middlewareId: owner.middlewareId,
|
|
63
61
|
slot: owner.slot,
|
|
64
62
|
order: owner.order,
|
|
65
|
-
digest: digest({ middlewareId: owner.middlewareId, slot: owner.slot, order: owner.order }),
|
|
66
63
|
...(reason === undefined ? {} : { reason }),
|
|
67
64
|
});
|
|
68
65
|
}
|
|
69
|
-
function itemDigest(item) {
|
|
70
|
-
return item.type === undefined ? { value: item.value } : { type: item.type, value: item.value };
|
|
71
|
-
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { bindTool } from "../build/bind-tool.js";
|
|
2
2
|
import { HarnessError, isHarnessError } from "../errors.js";
|
|
3
|
-
import { normalizeDirective, sameDirective } from "../model
|
|
4
|
-
import { digest } from "../utils/digest.js";
|
|
3
|
+
import { normalizeDirective, sameDirective } from "../model/normalize.js";
|
|
5
4
|
import { copyJson } from "../utils/immutable.js";
|
|
6
5
|
import { checkedReason, slotOwner, SlotDraft } from "./slot-assembly.js";
|
|
7
6
|
/** Per-step, middleware-owned model configuration draft. It never persists past the call. */
|
|
@@ -80,7 +79,6 @@ export class ModelConfigurationDraft {
|
|
|
80
79
|
const toolSlots = this.#tools.values();
|
|
81
80
|
const instructions = instructionSlots.flatMap((slot) => slot.value.map((text) => Object.freeze({
|
|
82
81
|
text,
|
|
83
|
-
digest: digest(text),
|
|
84
82
|
contributor: contributor(slot.owner, slot.reason),
|
|
85
83
|
})));
|
|
86
84
|
const sourcedTools = toolSlots.flatMap((slot) => slot.value.map((tool) => Object.freeze({ tool, contributor: contributor(slot.owner, slot.reason) })));
|
|
@@ -95,15 +93,6 @@ export class ModelConfigurationDraft {
|
|
|
95
93
|
throw new HarnessError("configuration.duplicate-tool-name", `Duplicate Tool '${[...duplicate].sort().join("', '")}'`);
|
|
96
94
|
const toolContracts = sourcedTools.map(({ tool, contributor: source }) => toolContract(tool, source));
|
|
97
95
|
const model = this.#model?.directive;
|
|
98
|
-
const logical = digest({
|
|
99
|
-
instructions: instructions.map((item) => item.text),
|
|
100
|
-
tools: toolContracts.map(({ name, description, inputSchema }) => ({
|
|
101
|
-
name,
|
|
102
|
-
...(description === undefined ? {} : { description }),
|
|
103
|
-
inputSchema,
|
|
104
|
-
})),
|
|
105
|
-
});
|
|
106
|
-
const modelDigest = digest(model ?? null);
|
|
107
96
|
return Object.freeze({
|
|
108
97
|
version: 1,
|
|
109
98
|
...(model === undefined ? {} : { model }),
|
|
@@ -115,11 +104,6 @@ export class ModelConfigurationDraft {
|
|
|
115
104
|
...toolSlots.map((slot) => contributor(slot.owner, slot.reason)),
|
|
116
105
|
...(this.#model?.owner ? [contributor(this.#model.owner, this.#model.reason)] : []),
|
|
117
106
|
]),
|
|
118
|
-
digests: Object.freeze({
|
|
119
|
-
logical,
|
|
120
|
-
model: modelDigest,
|
|
121
|
-
request: digest({ logical, model: modelDigest }),
|
|
122
|
-
}),
|
|
123
107
|
});
|
|
124
108
|
}
|
|
125
109
|
}
|
|
@@ -134,7 +118,6 @@ function contributor(owner, reason) {
|
|
|
134
118
|
middlewareId: owner.middlewareId,
|
|
135
119
|
slot: owner.slot,
|
|
136
120
|
order: owner.order,
|
|
137
|
-
digest: digest({ middlewareId: owner.middlewareId, slot: owner.slot, order: owner.order }),
|
|
138
121
|
...(reason === undefined ? {} : { reason }),
|
|
139
122
|
});
|
|
140
123
|
}
|
|
@@ -142,10 +125,13 @@ function providerTool(tool) {
|
|
|
142
125
|
return {
|
|
143
126
|
name: tool.name,
|
|
144
127
|
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
145
|
-
inputSchema: copyJson(tool.
|
|
128
|
+
inputSchema: copyJson(tool.inputSchema.jsonSchema),
|
|
129
|
+
...(tool.outputSchema === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { outputSchema: copyJson(tool.outputSchema.jsonSchema) }),
|
|
146
132
|
};
|
|
147
133
|
}
|
|
148
134
|
function toolContract(tool, source) {
|
|
149
135
|
const value = providerTool(tool);
|
|
150
|
-
return Object.freeze({ ...value,
|
|
136
|
+
return Object.freeze({ ...value, contributor: source });
|
|
151
137
|
}
|
package/dist/step/project.js
CHANGED
|
@@ -9,9 +9,10 @@ export function projectModelCall(request) {
|
|
|
9
9
|
tools: Object.freeze(request.configuration.tools.map((tool) => Object.freeze({
|
|
10
10
|
name: tool.name,
|
|
11
11
|
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
12
|
-
inputSchema: copyJson(tool.
|
|
12
|
+
inputSchema: copyJson(tool.inputSchema.jsonSchema),
|
|
13
13
|
}))),
|
|
14
14
|
...(request.model === undefined ? {} : { model: copyJson(request.model) }),
|
|
15
|
+
...(request.outputSchema === undefined ? {} : { outputSchema: copyJson(request.outputSchema) }),
|
|
15
16
|
sessionId: request.sessionId,
|
|
16
17
|
});
|
|
17
18
|
}
|
|
@@ -45,12 +46,28 @@ function projectEntry(entry) {
|
|
|
45
46
|
if (entry.kind === "input") {
|
|
46
47
|
if (entry.event.kind !== "user-message" && entry.event.kind !== "interrupt")
|
|
47
48
|
return [];
|
|
48
|
-
return [
|
|
49
|
+
return [
|
|
50
|
+
freezeItem({
|
|
51
|
+
kind: "message",
|
|
52
|
+
role: "user",
|
|
53
|
+
content: "content" in entry.event
|
|
54
|
+
? entry.event.content.map((part) => part.type === "text"
|
|
55
|
+
? textPart(part.text)
|
|
56
|
+
: Object.freeze({
|
|
57
|
+
type: "media",
|
|
58
|
+
mediaType: part.mediaType,
|
|
59
|
+
reference: copyJson(part.reference),
|
|
60
|
+
}))
|
|
61
|
+
: [textPart(entry.event.text)],
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
49
64
|
}
|
|
50
65
|
if (entry.kind === "candidate") {
|
|
51
66
|
const content = entry.candidate.output.flatMap((block) => {
|
|
52
67
|
if (block.type === "text")
|
|
53
68
|
return [textPart(block.text)];
|
|
69
|
+
if (block.type === "json")
|
|
70
|
+
return [textPart(JSON.stringify(block.value))];
|
|
54
71
|
if (block.type === "tool-call")
|
|
55
72
|
return [
|
|
56
73
|
Object.freeze({
|
|
@@ -84,7 +101,12 @@ function toolResultPayload(result) {
|
|
|
84
101
|
return result.output;
|
|
85
102
|
if (result.kind === "denied")
|
|
86
103
|
return { kind: result.kind, reason: result.reason };
|
|
87
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
kind: result.kind,
|
|
106
|
+
code: result.code,
|
|
107
|
+
message: result.message,
|
|
108
|
+
...(result.details === undefined ? {} : { details: result.details }),
|
|
109
|
+
};
|
|
88
110
|
}
|
|
89
111
|
function textPart(text) {
|
|
90
112
|
return Object.freeze({ type: "text", text });
|
package/dist/step/resolve.d.ts
CHANGED
package/dist/step/resolve.js
CHANGED
|
@@ -12,5 +12,6 @@ export function resolveModelRequest(input) {
|
|
|
12
12
|
arrivals: Object.freeze([...input.arrivals]),
|
|
13
13
|
toolResults: Object.freeze([...input.toolResults]),
|
|
14
14
|
tools: Object.freeze([...ctx.offeredTools]),
|
|
15
|
+
...(input.output === undefined ? {} : { outputSchema: input.output.schema.jsonSchema }),
|
|
15
16
|
});
|
|
16
17
|
}
|
package/dist/step/run.d.ts
CHANGED
|
@@ -28,5 +28,6 @@ export declare function runStep(input: {
|
|
|
28
28
|
readonly context?: import("../types/shared.js").JsonObject;
|
|
29
29
|
}>;
|
|
30
30
|
states: CapabilityStateRegistry;
|
|
31
|
+
output?: import("../session/output-contract.js").TurnOutputContract;
|
|
31
32
|
recordModelRequested(state: SessionSnapshot, active: ActiveModelExecutionRecord): Promise<SessionSnapshot>;
|
|
32
33
|
}): Promise<StepRunResult>;
|
package/dist/step/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeCandidate } from "../model
|
|
1
|
+
import { normalizeCandidate } from "../model/normalize.js";
|
|
2
2
|
import { HarnessError, isHarnessError } from "../errors.js";
|
|
3
3
|
import { copyJson } from "../utils/immutable.js";
|
|
4
4
|
import { runMiddleware } from "./compose.js";
|
|
@@ -52,6 +52,7 @@ export async function runStep(input) {
|
|
|
52
52
|
context,
|
|
53
53
|
arrivals: input.arrivals,
|
|
54
54
|
toolResults: input.toolResults,
|
|
55
|
+
output: input.output,
|
|
55
56
|
});
|
|
56
57
|
requestedModelId = request.model?.id;
|
|
57
58
|
const call = projectModelCall(request);
|
|
@@ -76,10 +77,26 @@ export async function runStep(input) {
|
|
|
76
77
|
}),
|
|
77
78
|
}));
|
|
78
79
|
try {
|
|
80
|
+
let prepared = false;
|
|
79
81
|
const outcome = await input.agent.invoke(call, {
|
|
80
82
|
request,
|
|
81
83
|
invocationId,
|
|
82
84
|
signal: input.signal,
|
|
85
|
+
reportPreparedCall(value) {
|
|
86
|
+
if (prepared)
|
|
87
|
+
throw new HarnessError("model.adapter-invalid-options", "Model adapter may report only one prepared call per invocation");
|
|
88
|
+
if (typeof value.adapter !== "string" || value.adapter === "")
|
|
89
|
+
throw new HarnessError("model.adapter-invalid-options", "Prepared model call adapter must be a non-empty string");
|
|
90
|
+
prepared = true;
|
|
91
|
+
const reported = Object.freeze({ adapter: value.adapter, call: copyJson(value.call) });
|
|
92
|
+
input.observe(() => ({
|
|
93
|
+
type: "model.prepared",
|
|
94
|
+
turnId: input.turnId,
|
|
95
|
+
stepId: input.stepId,
|
|
96
|
+
...(request.model?.id === undefined ? {} : { requestedModelId: request.model.id }),
|
|
97
|
+
attributes: reported,
|
|
98
|
+
}));
|
|
99
|
+
},
|
|
83
100
|
});
|
|
84
101
|
if (input.signal.aborted)
|
|
85
102
|
throw input.signal.reason;
|
|
@@ -103,7 +120,7 @@ export async function runStep(input) {
|
|
|
103
120
|
});
|
|
104
121
|
}
|
|
105
122
|
}, input.observe);
|
|
106
|
-
const output = sealStep(context);
|
|
123
|
+
const output = sealStep(context, input.output);
|
|
107
124
|
const candidate = output.kind === "tools" ? output.plan.candidate : context.currentCandidate;
|
|
108
125
|
return Object.freeze({
|
|
109
126
|
stepId: input.stepId,
|
|
@@ -136,7 +153,6 @@ function snapshotConfiguration(configuration) {
|
|
|
136
153
|
tools: snapshotTools(configuration.tools),
|
|
137
154
|
toolContracts: copyJson(configuration.toolContracts),
|
|
138
155
|
contributors: copyJson(configuration.contributors),
|
|
139
|
-
digests: copyJson(configuration.digests),
|
|
140
156
|
});
|
|
141
157
|
}
|
|
142
158
|
function snapshotTools(tools) {
|
|
@@ -144,6 +160,11 @@ function snapshotTools(tools) {
|
|
|
144
160
|
name: tool.name,
|
|
145
161
|
...(tool.description === undefined ? {} : { description: tool.description }),
|
|
146
162
|
owner: tool.owner,
|
|
147
|
-
|
|
163
|
+
inputSchema: Object.freeze({ jsonSchema: copyJson(tool.inputSchema.jsonSchema) }),
|
|
164
|
+
...(tool.outputSchema === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: {
|
|
167
|
+
outputSchema: Object.freeze({ jsonSchema: copyJson(tool.outputSchema.jsonSchema) }),
|
|
168
|
+
}),
|
|
148
169
|
})));
|
|
149
170
|
}
|
package/dist/step/seal.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type { ModelCandidate } from "../types/model.js";
|
|
|
2
2
|
import type { Tripwire } from "../types/shared.js";
|
|
3
3
|
import type { BoundToolDefinition, RequiredInteraction, SealedToolCall, ToolOwner, ToolResult } from "../types/tool.js";
|
|
4
4
|
import type { StepContext } from "./step-context.js";
|
|
5
|
+
import type { TurnOutputContract } from "../session/output-contract.js";
|
|
5
6
|
export interface ExecutablePlanEntry {
|
|
6
7
|
readonly call: SealedToolCall;
|
|
7
8
|
readonly invocationId: string;
|
|
8
9
|
readonly owner: ToolOwner;
|
|
9
10
|
readonly execute: BoundToolDefinition["execute"];
|
|
11
|
+
readonly outputSchema?: BoundToolDefinition["outputSchema"];
|
|
10
12
|
readonly interaction?: RequiredInteraction;
|
|
11
13
|
}
|
|
12
14
|
export interface InternalToolPlan {
|
|
@@ -23,7 +25,7 @@ export type SealedStepOutput = {
|
|
|
23
25
|
readonly tripwire: Tripwire;
|
|
24
26
|
} | {
|
|
25
27
|
readonly kind: "final";
|
|
26
|
-
readonly output:
|
|
28
|
+
readonly output: import("../types/shared.js").JsonValue;
|
|
27
29
|
} | {
|
|
28
30
|
readonly kind: "deferred-model";
|
|
29
31
|
readonly active: import("../types/session.js").ActiveModelExecutionRecord;
|
|
@@ -32,4 +34,4 @@ export type SealedStepOutput = {
|
|
|
32
34
|
readonly plan: InternalToolPlan;
|
|
33
35
|
};
|
|
34
36
|
/** Converts the reviewed canonical candidate into either a final response or an executable tool plan. */
|
|
35
|
-
export declare function sealStep(context: StepContext): SealedStepOutput;
|
|
37
|
+
export declare function sealStep(context: StepContext, outputContract?: TurnOutputContract): SealedStepOutput;
|