@nylorun/harness 0.9.0-beta.1 → 0.11.1-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 +60 -0
- package/README.md +5 -79
- package/dist/build/bind-tool.js +4 -3
- package/dist/build/helpers.d.ts +2 -2
- 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 +6 -3
- package/dist/index.js +2 -0
- package/dist/model/adapters.d.ts +48 -5
- package/dist/model/adapters.js +142 -17
- package/dist/model/normalize.js +49 -6
- 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 +80 -6
- 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/canonicalize.js +3 -0
- package/dist/step/context-draft.js +1 -7
- package/dist/step/model-configuration.js +5 -19
- package/dist/step/project.js +31 -5
- 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 +24 -3
- package/dist/step/seal.d.ts +4 -2
- package/dist/step/seal.js +63 -12
- 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/middleware.d.ts +1 -1
- package/dist/types/model.d.ts +30 -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 +11 -12
- package/dist/utils/digest.d.ts +0 -1
- package/dist/utils/digest.js +0 -14
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
|
}
|
|
@@ -38,6 +38,9 @@ export function canonicalizeOutput(output) {
|
|
|
38
38
|
id: call.id,
|
|
39
39
|
name: call.name,
|
|
40
40
|
args: call.args,
|
|
41
|
+
...(block.providerMetadata === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { providerMetadata: block.providerMetadata }),
|
|
41
44
|
...(block.raw === undefined ? {} : { raw: block.raw }),
|
|
42
45
|
});
|
|
43
46
|
})),
|
|
@@ -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
3
|
import { normalizeDirective, sameDirective } from "../model/normalize.js";
|
|
4
|
-
import { digest } from "../utils/digest.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,29 @@ 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
|
-
if (block.type === "text"
|
|
53
|
-
|
|
67
|
+
if (block.type === "text" ||
|
|
68
|
+
(block.type === "reasoning" && block.providerMetadata !== undefined))
|
|
69
|
+
return [Object.freeze({ ...block })];
|
|
70
|
+
if (block.type === "json")
|
|
71
|
+
return [textPart(JSON.stringify(block.value))];
|
|
54
72
|
if (block.type === "tool-call")
|
|
55
73
|
return [
|
|
56
74
|
Object.freeze({
|
|
@@ -58,6 +76,9 @@ function projectEntry(entry) {
|
|
|
58
76
|
id: block.id,
|
|
59
77
|
name: block.name,
|
|
60
78
|
args: copyJson(block.args),
|
|
79
|
+
...(block.providerMetadata === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { providerMetadata: copyJson(block.providerMetadata) }),
|
|
61
82
|
}),
|
|
62
83
|
];
|
|
63
84
|
return [];
|
|
@@ -84,7 +105,12 @@ function toolResultPayload(result) {
|
|
|
84
105
|
return result.output;
|
|
85
106
|
if (result.kind === "denied")
|
|
86
107
|
return { kind: result.kind, reason: result.reason };
|
|
87
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
kind: result.kind,
|
|
110
|
+
code: result.code,
|
|
111
|
+
message: result.message,
|
|
112
|
+
...(result.details === undefined ? {} : { details: result.details }),
|
|
113
|
+
};
|
|
88
114
|
}
|
|
89
115
|
function textPart(text) {
|
|
90
116
|
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
|
@@ -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;
|
package/dist/step/seal.js
CHANGED
|
@@ -2,19 +2,29 @@ import { textFromOutput } from "../model/normalize.js";
|
|
|
2
2
|
import { createId } from "../utils/ids.js";
|
|
3
3
|
import { HarnessError, isHarnessError } from "../errors.js";
|
|
4
4
|
import { assertJson, copyJson } from "../utils/immutable.js";
|
|
5
|
-
const failed = (callId, toolName, code, message) => Object.freeze({
|
|
5
|
+
const failed = (callId, toolName, code, message, details) => Object.freeze({
|
|
6
|
+
kind: "failed",
|
|
7
|
+
callId,
|
|
8
|
+
toolName,
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
...(details ? { details } : {}),
|
|
12
|
+
});
|
|
6
13
|
/** Converts the reviewed canonical candidate into either a final response or an executable tool plan. */
|
|
7
|
-
export function sealStep(context) {
|
|
14
|
+
export function sealStep(context, outputContract) {
|
|
8
15
|
if (context.currentTripwire)
|
|
9
16
|
return Object.freeze({ kind: "tripwire", tripwire: context.currentTripwire });
|
|
10
17
|
if (context.currentModelDeferred)
|
|
11
18
|
return Object.freeze({ kind: "deferred-model", active: context.currentModelDeferred });
|
|
12
19
|
const calls = context.canonicalCalls();
|
|
13
|
-
if (!calls.length)
|
|
20
|
+
if (!calls.length) {
|
|
21
|
+
if (outputContract)
|
|
22
|
+
return sealStructuredOutput(context, outputContract);
|
|
14
23
|
return Object.freeze({
|
|
15
24
|
kind: "final",
|
|
16
25
|
output: textFromOutput(context.currentCandidate?.output ?? []),
|
|
17
26
|
});
|
|
27
|
+
}
|
|
18
28
|
const catalog = context.catalogByName;
|
|
19
29
|
const sealed = calls.map((call) => sealCall(context, catalog, call));
|
|
20
30
|
const canonicalCandidate = context.currentCandidate ?? Object.freeze({ output: Object.freeze([]) });
|
|
@@ -28,6 +38,28 @@ export function sealStep(context) {
|
|
|
28
38
|
}),
|
|
29
39
|
});
|
|
30
40
|
}
|
|
41
|
+
function sealStructuredOutput(context, contract) {
|
|
42
|
+
const output = context.currentCandidate?.output ?? [];
|
|
43
|
+
const json = output.filter((block) => block.type === "json");
|
|
44
|
+
if (json.length !== 1 || output.some((block) => block.type === "text"))
|
|
45
|
+
return invalidOutput("Structured terminal output must contain exactly one JSON block and no text blocks");
|
|
46
|
+
const validation = contract.schema.validate(json[0].value);
|
|
47
|
+
if (!validation.ok)
|
|
48
|
+
return invalidOutput(`Structured terminal output failed validation: ${validation.issues.map(renderIssue).join("; ")}`);
|
|
49
|
+
try {
|
|
50
|
+
assertJson(validation.value, "structured terminal output");
|
|
51
|
+
return Object.freeze({ kind: "final", output: copyJson(validation.value) });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return invalidOutput(error instanceof Error ? error.message : String(error));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function invalidOutput(message) {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
kind: "tripwire",
|
|
60
|
+
tripwire: Object.freeze({ code: "output.invalid", message }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
31
63
|
function sealCall(context, catalog, candidate) {
|
|
32
64
|
const order = Object.freeze({ callId: candidate.id, toolName: candidate.name });
|
|
33
65
|
const canonical = Object.freeze({
|
|
@@ -55,11 +87,11 @@ function sealCall(context, catalog, candidate) {
|
|
|
55
87
|
immediate: failed(candidate.id, candidate.name, "tool.unknown", `Unknown Tool '${candidate.name}'`),
|
|
56
88
|
};
|
|
57
89
|
const args = validatedArguments(tool, candidate);
|
|
58
|
-
if (
|
|
90
|
+
if (!args.ok)
|
|
59
91
|
return {
|
|
60
92
|
order,
|
|
61
93
|
canonical,
|
|
62
|
-
immediate: failed(candidate.id, candidate.name, args.
|
|
94
|
+
immediate: failed(candidate.id, candidate.name, "tool.invalid-arguments", args.message, args.details),
|
|
63
95
|
};
|
|
64
96
|
const denial = context.denialFor(candidate.id);
|
|
65
97
|
if (denial)
|
|
@@ -84,26 +116,45 @@ function sealCall(context, catalog, candidate) {
|
|
|
84
116
|
call: Object.freeze({
|
|
85
117
|
callId: candidate.id,
|
|
86
118
|
toolName: candidate.name,
|
|
87
|
-
args,
|
|
119
|
+
args: args.value,
|
|
88
120
|
}),
|
|
89
121
|
invocationId: createId("invocation"),
|
|
90
122
|
owner: tool.owner,
|
|
91
123
|
execute: tool.execute,
|
|
124
|
+
...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }),
|
|
92
125
|
...(interaction ? { interaction } : {}),
|
|
93
126
|
}),
|
|
94
127
|
};
|
|
95
128
|
}
|
|
96
129
|
function validatedArguments(tool, candidate) {
|
|
97
|
-
const validation = tool.
|
|
130
|
+
const validation = tool.inputSchema.validate(candidate.args);
|
|
98
131
|
if (!validation.ok)
|
|
99
|
-
return
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
message: validation.issues.map(renderIssue).join("; "),
|
|
135
|
+
details: Object.freeze({ phase: "input", issues: validation.issues }),
|
|
136
|
+
};
|
|
100
137
|
try {
|
|
101
138
|
assertJson(validation.value, `arguments for '${candidate.name}'`);
|
|
102
|
-
return copyJson(validation.value);
|
|
139
|
+
return { ok: true, value: copyJson(validation.value) };
|
|
103
140
|
}
|
|
104
141
|
catch (error) {
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
:
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: isHarnessError(error) ? error.message : String(error),
|
|
145
|
+
details: Object.freeze({
|
|
146
|
+
phase: "input",
|
|
147
|
+
issues: Object.freeze([
|
|
148
|
+
Object.freeze({
|
|
149
|
+
path: Object.freeze([]),
|
|
150
|
+
code: "invalid_json",
|
|
151
|
+
message: isHarnessError(error) ? error.message : String(error),
|
|
152
|
+
}),
|
|
153
|
+
]),
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
108
156
|
}
|
|
109
157
|
}
|
|
158
|
+
function renderIssue(issue) {
|
|
159
|
+
return `${issue.path.join(".") || "(root)"}: ${issue.message}`;
|
|
160
|
+
}
|
package/dist/turn/plan-runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HarnessError, isHarnessError } from "../errors.js";
|
|
2
2
|
import { createId } from "../utils/ids.js";
|
|
3
|
-
import { copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
3
|
+
import { assertJson, copyJson, copyJsonObject } from "../utils/immutable.js";
|
|
4
4
|
/** Owns one sealed plan's deterministic interaction and concurrent execution progress. */
|
|
5
5
|
export class ToolPlanRunner {
|
|
6
6
|
plan;
|
|
@@ -309,8 +309,18 @@ function resumeValue(event, pending) {
|
|
|
309
309
|
function resultFrom(entry, outcome) {
|
|
310
310
|
const base = { callId: entry.call.callId, toolName: entry.call.toolName };
|
|
311
311
|
switch (outcome.kind) {
|
|
312
|
-
case "completed":
|
|
313
|
-
|
|
312
|
+
case "completed": {
|
|
313
|
+
const output = validatedOutput(entry, outcome.output);
|
|
314
|
+
if (!output.ok)
|
|
315
|
+
return Object.freeze({
|
|
316
|
+
...base,
|
|
317
|
+
kind: "failed",
|
|
318
|
+
code: "tool.invalid-output",
|
|
319
|
+
message: output.message,
|
|
320
|
+
details: output.details,
|
|
321
|
+
});
|
|
322
|
+
return Object.freeze({ ...base, kind: "completed", output: output.value });
|
|
323
|
+
}
|
|
314
324
|
case "denied":
|
|
315
325
|
if (typeof outcome.reason !== "string")
|
|
316
326
|
throw new HarnessError("tool.invalid-tool-result", "Tool denial reason must be a string");
|
|
@@ -326,6 +336,31 @@ function resultFrom(entry, outcome) {
|
|
|
326
336
|
});
|
|
327
337
|
}
|
|
328
338
|
}
|
|
339
|
+
function validatedOutput(entry, value) {
|
|
340
|
+
const validation = entry.outputSchema?.validate(value) ?? { ok: true, value };
|
|
341
|
+
if (!validation.ok)
|
|
342
|
+
return validationFailure("output", validation.issues);
|
|
343
|
+
try {
|
|
344
|
+
assertJson(validation.value, `output for '${entry.call.toolName}'`);
|
|
345
|
+
return { ok: true, value: copyJson(validation.value) };
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
return validationFailure("output", [
|
|
349
|
+
Object.freeze({
|
|
350
|
+
path: Object.freeze([]),
|
|
351
|
+
code: "invalid_json",
|
|
352
|
+
message: error instanceof Error ? error.message : String(error),
|
|
353
|
+
}),
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function validationFailure(phase, issues) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
message: issues.map((item) => `${item.path.join(".") || "(root)"}: ${item.message}`).join("; "),
|
|
361
|
+
details: Object.freeze({ phase, issues: Object.freeze([...issues]) }),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
329
364
|
function completedEvent(entry, context, result) {
|
|
330
365
|
return {
|
|
331
366
|
type: "tool.completed",
|
package/dist/turn/runner.d.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import type { ActiveExecutionRecord, InputEvent, SessionEvent, SessionRecord, SessionSnapshot } from "../types/session.js";
|
|
2
|
-
import type { JsonObject, Tripwire } from "../types/shared.js";
|
|
2
|
+
import type { JsonObject, JsonValue, Tripwire } from "../types/shared.js";
|
|
3
3
|
import type { RequiredInteraction } from "../types/tool.js";
|
|
4
4
|
import type { LoopAgent } from "../build/agent.js";
|
|
5
5
|
import type { ObserveEmit } from "../utils/observe.js";
|
|
6
6
|
import { ToolPlanRunner } from "./plan-runner.js";
|
|
7
7
|
import type { CapabilityStateRegistry } from "../session/capability-state.js";
|
|
8
|
+
import type { TurnOutputContract } from "../session/output-contract.js";
|
|
8
9
|
export interface PendingTurn {
|
|
9
10
|
readonly plan: ToolPlanRunner;
|
|
10
11
|
readonly turnId: string;
|
|
11
12
|
readonly stepId: string;
|
|
12
13
|
readonly stepNumber: number;
|
|
14
|
+
readonly output?: TurnOutputContract;
|
|
13
15
|
}
|
|
14
16
|
export type TurnProgress = {
|
|
15
17
|
readonly kind: "final";
|
|
16
18
|
readonly state: SessionSnapshot;
|
|
17
19
|
readonly turnId: string;
|
|
18
20
|
readonly stepId: string;
|
|
19
|
-
readonly output:
|
|
21
|
+
readonly output: JsonValue;
|
|
20
22
|
} | {
|
|
21
23
|
readonly kind: "interaction-required";
|
|
22
24
|
readonly state: SessionSnapshot;
|
|
@@ -42,7 +44,7 @@ export interface TurnRunContext {
|
|
|
42
44
|
readonly assertCurrent: () => void;
|
|
43
45
|
readonly onPlanActive: (pending: PendingTurn | undefined) => void;
|
|
44
46
|
readonly commit: (state: SessionSnapshot, transition: SessionRecord["transition"], active?: ActiveExecutionRecord) => Promise<SessionSnapshot>;
|
|
45
|
-
readonly onConversation: (event: SessionEvent) => void;
|
|
47
|
+
readonly onConversation: (event: SessionEvent<JsonValue>) => void;
|
|
46
48
|
readonly claimInterrupts: (state: SessionSnapshot, turnId: string) => Promise<{
|
|
47
49
|
readonly state: SessionSnapshot;
|
|
48
50
|
readonly arrivals: readonly InputEvent[];
|
|
@@ -57,7 +59,7 @@ export declare class TurnRunner {
|
|
|
57
59
|
readonly userId?: string;
|
|
58
60
|
readonly context?: JsonObject;
|
|
59
61
|
}>);
|
|
60
|
-
start(state: SessionSnapshot, event: InputEvent, context: TurnRunContext): Promise<TurnProgress>;
|
|
62
|
+
start(state: SessionSnapshot, event: InputEvent, context: TurnRunContext, output?: TurnOutputContract): Promise<TurnProgress>;
|
|
61
63
|
continue(state: SessionSnapshot, context: TurnRunContext): Promise<TurnProgress>;
|
|
62
64
|
resume(state: SessionSnapshot, pending: PendingTurn, event: InputEvent, context: TurnRunContext): Promise<TurnProgress>;
|
|
63
65
|
private advance;
|