@maxanstey-meridian/tandem 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +60 -0
- package/dist/index.d.ts +346 -0
- package/dist/index.js +1291 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const participantBrand = Symbol("participant");
|
|
4
|
+
const compileCapabilityBrand = Symbol("compileCapability");
|
|
5
|
+
const interactionHandlersBrand = Symbol("interactionHandlers");
|
|
6
|
+
const workspaceBrand = Symbol("workspace");
|
|
7
|
+
const toolGroupBrand = Symbol("toolGroup");
|
|
8
|
+
const commandSelectionBrand = Symbol("commandSelection");
|
|
9
|
+
export class TandemError extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "TandemError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class TandemRuntimeError extends TandemError {
|
|
16
|
+
operation;
|
|
17
|
+
constructor(operation, cause) {
|
|
18
|
+
super(`Tandem ${operation} failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
19
|
+
cause,
|
|
20
|
+
});
|
|
21
|
+
this.operation = operation;
|
|
22
|
+
this.name = "TandemRuntimeError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class TandemCancellationError extends TandemRuntimeError {
|
|
26
|
+
constructor(cause) {
|
|
27
|
+
super("run", cause);
|
|
28
|
+
this.name = "AbortError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class ContractValidationError extends TandemError {
|
|
32
|
+
boundary;
|
|
33
|
+
problems;
|
|
34
|
+
constructor(boundary, problems) {
|
|
35
|
+
super(`${boundary} validation failed: ${problems.map((p) => `${p.path}: ${p.message}`).join("; ")}`);
|
|
36
|
+
this.boundary = boundary;
|
|
37
|
+
this.name = "ContractValidationError";
|
|
38
|
+
this.problems = problems;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function callbackError(error) {
|
|
42
|
+
return error instanceof ContractValidationError
|
|
43
|
+
? {
|
|
44
|
+
name: error.name,
|
|
45
|
+
message: error.message,
|
|
46
|
+
boundary: error.boundary,
|
|
47
|
+
problems: error.problems,
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
name: error instanceof Error ? error.name : "Error",
|
|
51
|
+
message: error instanceof Error ? error.message : String(error),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
class CallbackRegistry {
|
|
55
|
+
#sync = new Map();
|
|
56
|
+
#async = new Map();
|
|
57
|
+
#next = 0;
|
|
58
|
+
#disposed = false;
|
|
59
|
+
registerSync(callback) {
|
|
60
|
+
const id = this.#allocate();
|
|
61
|
+
this.#sync.set(id, callback);
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
registerAsync(callback) {
|
|
65
|
+
const id = this.#allocate();
|
|
66
|
+
this.#async.set(id, callback);
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
invokeSync(id, state, input) {
|
|
70
|
+
try {
|
|
71
|
+
const callback = this.#sync.get(id);
|
|
72
|
+
if (!callback) {
|
|
73
|
+
throw new Error(`Unknown internal callback '${id}'.`);
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
succeeded: true,
|
|
77
|
+
value: callback(state, input),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
succeeded: false,
|
|
83
|
+
error: callbackError(error),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async invokeAsync(id, state, input, signal) {
|
|
88
|
+
try {
|
|
89
|
+
const callback = this.#async.get(id);
|
|
90
|
+
if (!callback) {
|
|
91
|
+
throw new Error(`Unknown internal async callback '${id}'.`);
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
succeeded: true,
|
|
95
|
+
value: await callback(state, input, signal),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
succeeded: false,
|
|
101
|
+
error: callbackError(error),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
dispose() {
|
|
106
|
+
this.#disposed = true;
|
|
107
|
+
this.#sync.clear();
|
|
108
|
+
this.#async.clear();
|
|
109
|
+
}
|
|
110
|
+
#allocate() {
|
|
111
|
+
if (this.#disposed) {
|
|
112
|
+
throw new Error("Callback registry has been disposed.");
|
|
113
|
+
}
|
|
114
|
+
return `c${this.#next++}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function callbackContractFailure(error) {
|
|
118
|
+
const marker = "TANDEM_CALLBACK_CONTRACT:";
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
const start = message.indexOf(marker);
|
|
121
|
+
if (start < 0) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(message.slice(start + marker.length));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function isCancellationError(error, signalAborted) {
|
|
132
|
+
if (!(error instanceof Error)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (error.message.includes("JavaScript callback failed:") && !signalAborted) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return (error.name === "AbortError" ||
|
|
139
|
+
/\b(?:operation was cancel(?:l)?ed|operation was aborted|this operation was aborted)\b/i.test(error.message));
|
|
140
|
+
}
|
|
141
|
+
function path(parts) {
|
|
142
|
+
return parts.length === 0
|
|
143
|
+
? "$"
|
|
144
|
+
: `$${parts.map((part) => (typeof part === "number" ? `[${part}]` : `.${String(part)}`)).join("")}`;
|
|
145
|
+
}
|
|
146
|
+
function parseValidated(schema, value, boundary) {
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = schema.safeParse(value);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof Error && /async/i.test(error.message)) {
|
|
153
|
+
throw new ContractValidationError(boundary, [
|
|
154
|
+
{
|
|
155
|
+
path: "$",
|
|
156
|
+
message: "Async Zod refinements are unsupported; Tandem contracts must validate synchronously.",
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
throw new ContractValidationError(boundary, result.error.issues.map((issue) => ({ path: path(issue.path), message: issue.message })));
|
|
164
|
+
}
|
|
165
|
+
return result.data;
|
|
166
|
+
}
|
|
167
|
+
function parse(schema, value, boundary) {
|
|
168
|
+
const result = parseValidated(schema, value, boundary);
|
|
169
|
+
if (!isDeepStrictEqual(result, value)) {
|
|
170
|
+
throw new ContractValidationError(boundary, [
|
|
171
|
+
{
|
|
172
|
+
path: "$",
|
|
173
|
+
message: "Zod contract changed the boundary value. Coercion, defaults, transforms, and stripping are unsupported.",
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function serializeBoundary(schema, value, boundary) {
|
|
180
|
+
const parsed = parse(schema, value, boundary);
|
|
181
|
+
const problem = jsonValueProblem(parsed, "$", new WeakSet());
|
|
182
|
+
if (problem) {
|
|
183
|
+
throw new ContractValidationError(boundary, [problem]);
|
|
184
|
+
}
|
|
185
|
+
let json;
|
|
186
|
+
try {
|
|
187
|
+
json = JSON.stringify(parsed);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
throw new ContractValidationError(boundary, [
|
|
191
|
+
{
|
|
192
|
+
path: "$",
|
|
193
|
+
message: `Value is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
if (json === undefined) {
|
|
198
|
+
throw new ContractValidationError(boundary, [
|
|
199
|
+
{ path: "$", message: "Top-level undefined is not JSON-serializable." },
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
const roundTripped = JSON.parse(json);
|
|
203
|
+
if (!isDeepStrictEqual(roundTripped, parsed)) {
|
|
204
|
+
throw new ContractValidationError(boundary, [
|
|
205
|
+
{ path: "$", message: "Value is not losslessly JSON-serializable." },
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
return json;
|
|
209
|
+
}
|
|
210
|
+
function jsonValueProblem(value, valuePath, seen) {
|
|
211
|
+
if (value === undefined) {
|
|
212
|
+
return { path: valuePath, message: "undefined is not JSON-serializable." };
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
215
|
+
return { path: valuePath, message: "Non-finite numbers are not JSON-serializable." };
|
|
216
|
+
}
|
|
217
|
+
if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
|
|
218
|
+
return { path: valuePath, message: `${typeof value} values are not JSON-serializable.` };
|
|
219
|
+
}
|
|
220
|
+
if (value === null || typeof value !== "object") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (seen.has(value)) {
|
|
224
|
+
return { path: valuePath, message: "Cyclic values are not JSON-serializable." };
|
|
225
|
+
}
|
|
226
|
+
seen.add(value);
|
|
227
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
228
|
+
return { path: valuePath, message: "Symbol properties are not JSON-serializable." };
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
for (let index = 0; index < value.length; index++) {
|
|
232
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
233
|
+
if (!descriptor) {
|
|
234
|
+
return {
|
|
235
|
+
path: `${valuePath}[${index}]`,
|
|
236
|
+
message: "Sparse arrays are not JSON-serializable.",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (!("value" in descriptor)) {
|
|
240
|
+
return {
|
|
241
|
+
path: `${valuePath}[${index}]`,
|
|
242
|
+
message: "Accessor properties are not supported at JSON boundaries.",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const problem = jsonValueProblem(descriptor.value, `${valuePath}[${index}]`, seen);
|
|
246
|
+
if (problem) {
|
|
247
|
+
return problem;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const additionalProperty = Object.getOwnPropertyNames(value).find((name) => {
|
|
251
|
+
if (name === "length") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const index = Number(name);
|
|
255
|
+
return (!Number.isInteger(index) || index < 0 || index >= value.length || String(index) !== name);
|
|
256
|
+
});
|
|
257
|
+
if (additionalProperty) {
|
|
258
|
+
return {
|
|
259
|
+
path: `${valuePath}.${additionalProperty}`,
|
|
260
|
+
message: "Additional array properties are not JSON-serializable.",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
seen.delete(value);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
267
|
+
return {
|
|
268
|
+
path: valuePath,
|
|
269
|
+
message: "Only plain objects are JSON-serializable boundary values.",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
for (const name of Object.getOwnPropertyNames(value)) {
|
|
273
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
274
|
+
if (!descriptor?.enumerable) {
|
|
275
|
+
return {
|
|
276
|
+
path: `${valuePath}.${name}`,
|
|
277
|
+
message: "Non-enumerable properties are not JSON-serializable.",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (!("value" in descriptor)) {
|
|
281
|
+
return {
|
|
282
|
+
path: `${valuePath}.${name}`,
|
|
283
|
+
message: "Accessor properties are not supported at JSON boundaries.",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const problem = jsonValueProblem(descriptor.value, `${valuePath}.${name}`, seen);
|
|
287
|
+
if (problem) {
|
|
288
|
+
return problem;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
seen.delete(value);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function parseJson(schema, json, boundary) {
|
|
295
|
+
let value;
|
|
296
|
+
try {
|
|
297
|
+
value = JSON.parse(json);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
throw new ContractValidationError(boundary, [{ path: "$", message: "Invalid JSON" }]);
|
|
301
|
+
}
|
|
302
|
+
return parse(schema, value, boundary);
|
|
303
|
+
}
|
|
304
|
+
function inputJsonSchema(schema, boundary) {
|
|
305
|
+
try {
|
|
306
|
+
z.toJSONSchema(schema, { io: "output" });
|
|
307
|
+
return JSON.stringify(z.toJSONSchema(schema, { io: "input" }));
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw new ContractValidationError(boundary, [
|
|
311
|
+
{ path: "$", message: error instanceof Error ? error.message : String(error) },
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
class NodeImplementation {
|
|
316
|
+
id;
|
|
317
|
+
persist;
|
|
318
|
+
[participantBrand];
|
|
319
|
+
constructor(id, persist) {
|
|
320
|
+
this.id = id;
|
|
321
|
+
this.persist = persist;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
class StageImplementation extends NodeImplementation {
|
|
325
|
+
execute;
|
|
326
|
+
kind = "stage";
|
|
327
|
+
constructor(id, persist, execute) {
|
|
328
|
+
super(id, persist);
|
|
329
|
+
this.execute = execute;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export function stage(definition) {
|
|
333
|
+
return new StageImplementation(definition.id, definition.persist, definition.execute);
|
|
334
|
+
}
|
|
335
|
+
class InteractionImplementation extends NodeImplementation {
|
|
336
|
+
requestSchema;
|
|
337
|
+
responseSchema;
|
|
338
|
+
request;
|
|
339
|
+
apply;
|
|
340
|
+
kind = "interaction";
|
|
341
|
+
requestType;
|
|
342
|
+
responseType;
|
|
343
|
+
constructor(id, persist, requestSchema, responseSchema, request, apply) {
|
|
344
|
+
super(id, persist);
|
|
345
|
+
this.requestSchema = requestSchema;
|
|
346
|
+
this.responseSchema = responseSchema;
|
|
347
|
+
this.request = request;
|
|
348
|
+
this.apply = apply;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
export function interaction(definition) {
|
|
352
|
+
return new InteractionImplementation(definition.id, definition.persist, definition.requestSchema, definition.responseSchema, definition.request, definition.apply);
|
|
353
|
+
}
|
|
354
|
+
class InteractionHandlersImplementation {
|
|
355
|
+
[interactionHandlersBrand] = true;
|
|
356
|
+
entries = [];
|
|
357
|
+
#interactions = new Set();
|
|
358
|
+
handle(interaction, handler) {
|
|
359
|
+
const opaque = interaction;
|
|
360
|
+
if (this.#interactions.has(opaque)) {
|
|
361
|
+
throw new TandemError(`Interaction '${interaction.id}' already has a handler.`);
|
|
362
|
+
}
|
|
363
|
+
this.#interactions.add(opaque);
|
|
364
|
+
this.entries.push({
|
|
365
|
+
interaction: opaque,
|
|
366
|
+
handle: handler,
|
|
367
|
+
});
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export function interactions() {
|
|
372
|
+
return new InteractionHandlersImplementation();
|
|
373
|
+
}
|
|
374
|
+
class CapabilityImplementation {
|
|
375
|
+
name;
|
|
376
|
+
instructions;
|
|
377
|
+
schema;
|
|
378
|
+
validateFor;
|
|
379
|
+
apply;
|
|
380
|
+
summarize;
|
|
381
|
+
requestJsonSchema;
|
|
382
|
+
constructor(name, instructions, schema, validateFor, apply, summarize) {
|
|
383
|
+
this.name = name;
|
|
384
|
+
this.instructions = instructions;
|
|
385
|
+
this.schema = schema;
|
|
386
|
+
this.validateFor = validateFor;
|
|
387
|
+
this.apply = apply;
|
|
388
|
+
this.summarize = summarize;
|
|
389
|
+
this.requestJsonSchema = inputJsonSchema(schema, `capability '${name}' schema`);
|
|
390
|
+
}
|
|
391
|
+
[compileCapabilityBrand]({ id, stateSchema, callbacks, }) {
|
|
392
|
+
const validate = callbacks.registerSync((_, input) => issues(this.schema, input));
|
|
393
|
+
const validateFor = this.validateFor
|
|
394
|
+
? callbacks.registerSync((state, input) => validationProblems(this.validateFor(parseJson(stateSchema, state, `${id} state`), parseJson(this.schema, input, `${id} capability '${this.name}' request`)), `${id} capability '${this.name}' contextual validation`))
|
|
395
|
+
: undefined;
|
|
396
|
+
const apply = callbacks.registerSync((state, input) => serializeBoundary(stateSchema, this.apply(parseJson(stateSchema, state, `${id} state`), parseJson(this.schema, input, `${id} capability '${this.name}' request`)), `${id} applied state`));
|
|
397
|
+
const summary = callbacks.registerSync((_, input) => this.summarize(parseJson(this.schema, input, `${id} capability '${this.name}' request`)));
|
|
398
|
+
return {
|
|
399
|
+
name: this.name,
|
|
400
|
+
instructions: this.instructions,
|
|
401
|
+
jsonSchema: this.requestJsonSchema,
|
|
402
|
+
validateCallback: validate,
|
|
403
|
+
validateForCallback: validateFor,
|
|
404
|
+
applyCallback: apply,
|
|
405
|
+
summaryCallback: summary,
|
|
406
|
+
valueType: `${id}.capability.${this.name}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
export function capability(definition) {
|
|
411
|
+
requireInstructions(definition.instructions, `Capability '${definition.name}' instructions`);
|
|
412
|
+
return new CapabilityImplementation(definition.name, definition.instructions, definition.schema, definition.validateFor, definition.apply, definition.summarize);
|
|
413
|
+
}
|
|
414
|
+
class AgentToolGroupImplementation {
|
|
415
|
+
predicate;
|
|
416
|
+
tools;
|
|
417
|
+
[toolGroupBrand];
|
|
418
|
+
constructor(predicate, tools) {
|
|
419
|
+
this.predicate = predicate;
|
|
420
|
+
this.tools = tools;
|
|
421
|
+
this[toolGroupBrand] = predicate ?? (() => true);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function createToolGroup(predicate, tools) {
|
|
425
|
+
if (tools.length === 0) {
|
|
426
|
+
throw new TandemError("An agent tool group cannot be empty.");
|
|
427
|
+
}
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
for (const tool of tools) {
|
|
430
|
+
if (typeof tool !== "string" && !(commandSelectionBrand in tool)) {
|
|
431
|
+
throw new TandemError("Agent tools must be built-in names or workspace.commands.");
|
|
432
|
+
}
|
|
433
|
+
if (seen.has(tool)) {
|
|
434
|
+
throw new TandemError("An agent tool group cannot select a tool twice.");
|
|
435
|
+
}
|
|
436
|
+
seen.add(tool);
|
|
437
|
+
}
|
|
438
|
+
return new AgentToolGroupImplementation(predicate, tools);
|
|
439
|
+
}
|
|
440
|
+
export const agentTools = {
|
|
441
|
+
always: (...tools) => createToolGroup(undefined, tools),
|
|
442
|
+
when: (predicate, ...tools) => createToolGroup(predicate, tools),
|
|
443
|
+
};
|
|
444
|
+
class AgentWorkspaceImplementation {
|
|
445
|
+
path;
|
|
446
|
+
commands;
|
|
447
|
+
commandSource;
|
|
448
|
+
constructor(path, commandSource) {
|
|
449
|
+
this.path = path;
|
|
450
|
+
this.commandSource =
|
|
451
|
+
typeof commandSource === "function" || commandSource === undefined
|
|
452
|
+
? commandSource
|
|
453
|
+
: commandSource.map(copyAgentCommand);
|
|
454
|
+
this.commands = { [commandSelectionBrand]: this };
|
|
455
|
+
}
|
|
456
|
+
withTools(groups, options) {
|
|
457
|
+
if (groups.length === 0) {
|
|
458
|
+
throw new TandemError("An agent workspace requires tool groups.");
|
|
459
|
+
}
|
|
460
|
+
const implementations = groups.map((group) => {
|
|
461
|
+
if (!(group instanceof AgentToolGroupImplementation)) {
|
|
462
|
+
throw new TandemError("Agent tool groups must be created by agentTools.");
|
|
463
|
+
}
|
|
464
|
+
return group;
|
|
465
|
+
});
|
|
466
|
+
if (options?.interceptTool !== undefined && typeof options.interceptTool !== "function") {
|
|
467
|
+
throw new TandemError("Workspace tool interceptor must be a function.");
|
|
468
|
+
}
|
|
469
|
+
return new AgentWorkspaceConfigurationImplementation(this, implementations, options?.interceptTool);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
class AgentWorkspaceConfigurationImplementation {
|
|
473
|
+
workspace;
|
|
474
|
+
groups;
|
|
475
|
+
interceptTool;
|
|
476
|
+
[workspaceBrand];
|
|
477
|
+
constructor(workspace, groups, interceptTool) {
|
|
478
|
+
this.workspace = workspace;
|
|
479
|
+
this.groups = groups;
|
|
480
|
+
this.interceptTool = interceptTool;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
export function agentWorkspace(definition) {
|
|
484
|
+
if (typeof definition.path !== "function") {
|
|
485
|
+
throw new TandemError("Workspace path is required.");
|
|
486
|
+
}
|
|
487
|
+
if (definition.commands !== undefined && typeof definition.commands !== "function") {
|
|
488
|
+
validateAgentCommands(definition.commands, "Workspace commands");
|
|
489
|
+
}
|
|
490
|
+
return new AgentWorkspaceImplementation(definition.path, definition.commands);
|
|
491
|
+
}
|
|
492
|
+
function copyAgentCommand(command) {
|
|
493
|
+
return command.arguments === undefined
|
|
494
|
+
? { ...command }
|
|
495
|
+
: {
|
|
496
|
+
...command,
|
|
497
|
+
arguments: command.arguments.map((argument) => argument.pattern !== undefined
|
|
498
|
+
? { ...argument }
|
|
499
|
+
: { ...argument, allowedValues: [...argument.allowedValues] }),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function validateAgentCommands(commands, context) {
|
|
503
|
+
if (!Array.isArray(commands)) {
|
|
504
|
+
throw new TandemError(`${context} must be an array.`);
|
|
505
|
+
}
|
|
506
|
+
for (const [commandIndex, command] of commands.entries()) {
|
|
507
|
+
const commandContext = `${context}[${commandIndex}]`;
|
|
508
|
+
if (typeof command !== "object" || command === null) {
|
|
509
|
+
throw new TandemError(`${commandContext} must be a command.`);
|
|
510
|
+
}
|
|
511
|
+
const candidate = command;
|
|
512
|
+
if (typeof candidate.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(candidate.name)) {
|
|
513
|
+
throw new TandemError(`${commandContext}.name must be a valid tool name.`);
|
|
514
|
+
}
|
|
515
|
+
if (typeof candidate.description !== "string" || candidate.description.trim().length === 0) {
|
|
516
|
+
throw new TandemError(`${commandContext}.description must be non-blank.`);
|
|
517
|
+
}
|
|
518
|
+
if (typeof candidate.command !== "string" || candidate.command.trim().length === 0) {
|
|
519
|
+
throw new TandemError(`${commandContext}.command must be non-blank.`);
|
|
520
|
+
}
|
|
521
|
+
if (candidate.arguments !== undefined && !Array.isArray(candidate.arguments)) {
|
|
522
|
+
throw new TandemError(`${commandContext}.arguments must be an array.`);
|
|
523
|
+
}
|
|
524
|
+
const argumentNames = new Set();
|
|
525
|
+
for (const [argumentIndex, argument] of (candidate.arguments ?? []).entries()) {
|
|
526
|
+
const argumentContext = `${commandContext}.arguments[${argumentIndex}]`;
|
|
527
|
+
if (typeof argument !== "object" || argument === null) {
|
|
528
|
+
throw new TandemError(`${argumentContext} must be an argument.`);
|
|
529
|
+
}
|
|
530
|
+
const candidateArgument = argument;
|
|
531
|
+
if (typeof candidateArgument.name !== "string" ||
|
|
532
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(candidateArgument.name)) {
|
|
533
|
+
throw new TandemError(`${argumentContext}.name must be a valid JSON property identifier.`);
|
|
534
|
+
}
|
|
535
|
+
if (argumentNames.has(candidateArgument.name)) {
|
|
536
|
+
throw new TandemError(`${argumentContext}.name duplicates '${candidateArgument.name}'.`);
|
|
537
|
+
}
|
|
538
|
+
argumentNames.add(candidateArgument.name);
|
|
539
|
+
if (typeof candidateArgument.description !== "string" ||
|
|
540
|
+
candidateArgument.description.trim().length === 0) {
|
|
541
|
+
throw new TandemError(`${argumentContext}.description must be non-blank.`);
|
|
542
|
+
}
|
|
543
|
+
if (typeof candidateArgument.flag !== "string" ||
|
|
544
|
+
candidateArgument.flag.trim().length === 0 ||
|
|
545
|
+
/\s/.test(candidateArgument.flag)) {
|
|
546
|
+
throw new TandemError(`${argumentContext}.flag must be a whitespace-free switch token.`);
|
|
547
|
+
}
|
|
548
|
+
if (candidateArgument.pattern !== undefined &&
|
|
549
|
+
typeof candidateArgument.pattern !== "string") {
|
|
550
|
+
throw new TandemError(`${argumentContext}.pattern must be a string.`);
|
|
551
|
+
}
|
|
552
|
+
if (candidateArgument.allowedValues !== undefined &&
|
|
553
|
+
!Array.isArray(candidateArgument.allowedValues)) {
|
|
554
|
+
throw new TandemError(`${argumentContext}.allowedValues must be an array.`);
|
|
555
|
+
}
|
|
556
|
+
const hasPattern = typeof candidateArgument.pattern === "string";
|
|
557
|
+
const hasAllowedValues = Array.isArray(candidateArgument.allowedValues);
|
|
558
|
+
if (hasPattern === hasAllowedValues) {
|
|
559
|
+
throw new TandemError(`${argumentContext} requires exactly one of pattern or allowedValues.`);
|
|
560
|
+
}
|
|
561
|
+
if (candidateArgument.maxLength !== undefined &&
|
|
562
|
+
(!Number.isSafeInteger(candidateArgument.maxLength) ||
|
|
563
|
+
candidateArgument.maxLength <= 0 ||
|
|
564
|
+
candidateArgument.maxLength > 2_147_483_647)) {
|
|
565
|
+
throw new TandemError(`${argumentContext}.maxLength must be a positive integer.`);
|
|
566
|
+
}
|
|
567
|
+
if (candidateArgument.allowedValues !== undefined) {
|
|
568
|
+
if (candidateArgument.allowedValues.length === 0) {
|
|
569
|
+
throw new TandemError(`${argumentContext}.allowedValues must not be empty.`);
|
|
570
|
+
}
|
|
571
|
+
const values = new Set();
|
|
572
|
+
for (const value of candidateArgument.allowedValues) {
|
|
573
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
574
|
+
throw new TandemError(`${argumentContext}.allowedValues must be non-blank strings.`);
|
|
575
|
+
}
|
|
576
|
+
if (values.has(value)) {
|
|
577
|
+
throw new TandemError(`${argumentContext}.allowedValues duplicates '${value}'.`);
|
|
578
|
+
}
|
|
579
|
+
values.add(value);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
export function skill(definition) {
|
|
586
|
+
if (typeof definition.directory !== "string" || definition.directory.trim().length === 0) {
|
|
587
|
+
throw new TandemError("Skill directory must be a non-blank string.");
|
|
588
|
+
}
|
|
589
|
+
return { directory: definition.directory };
|
|
590
|
+
}
|
|
591
|
+
class AgentImplementation extends NodeImplementation {
|
|
592
|
+
instructions;
|
|
593
|
+
client;
|
|
594
|
+
message;
|
|
595
|
+
output;
|
|
596
|
+
granted;
|
|
597
|
+
skills;
|
|
598
|
+
workspace;
|
|
599
|
+
temperature;
|
|
600
|
+
maxOutputTokens;
|
|
601
|
+
reasoning;
|
|
602
|
+
continueSession;
|
|
603
|
+
checkpoint;
|
|
604
|
+
timeoutMs;
|
|
605
|
+
kind = "agent";
|
|
606
|
+
constructor(id, persist, instructions, client, message, output, granted, skills, workspace, temperature, maxOutputTokens, reasoning, continueSession, checkpoint, timeoutMs) {
|
|
607
|
+
super(id, persist);
|
|
608
|
+
this.instructions = instructions;
|
|
609
|
+
this.client = client;
|
|
610
|
+
this.message = message;
|
|
611
|
+
this.output = output;
|
|
612
|
+
this.granted = granted;
|
|
613
|
+
this.skills = skills;
|
|
614
|
+
this.workspace = workspace;
|
|
615
|
+
this.temperature = temperature;
|
|
616
|
+
this.maxOutputTokens = maxOutputTokens;
|
|
617
|
+
this.reasoning = reasoning;
|
|
618
|
+
this.continueSession = continueSession;
|
|
619
|
+
this.checkpoint = checkpoint;
|
|
620
|
+
this.timeoutMs = timeoutMs;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
export function agent(definition) {
|
|
624
|
+
requireInstructions(definition.instructions, `Agent '${definition.id}' instructions`);
|
|
625
|
+
const reasoningEffort = definition.reasoning?.effort;
|
|
626
|
+
const reasoningMaxTokens = definition.reasoning?.maxTokens;
|
|
627
|
+
if (definition.reasoning &&
|
|
628
|
+
(reasoningEffort !== undefined) === (reasoningMaxTokens !== undefined)) {
|
|
629
|
+
throw new TandemError(`Agent '${definition.id}' reasoning must specify exactly one of effort or maxTokens.`);
|
|
630
|
+
}
|
|
631
|
+
if (reasoningEffort !== undefined &&
|
|
632
|
+
!["none", "low", "medium", "high"].includes(reasoningEffort)) {
|
|
633
|
+
throw new TandemError(`Agent '${definition.id}' has an invalid reasoning effort.`);
|
|
634
|
+
}
|
|
635
|
+
if (reasoningMaxTokens !== undefined) {
|
|
636
|
+
if (!Number.isSafeInteger(reasoningMaxTokens) ||
|
|
637
|
+
reasoningMaxTokens < 1024 ||
|
|
638
|
+
reasoningMaxTokens > 2_147_483_647) {
|
|
639
|
+
throw new TandemError(`Agent '${definition.id}' reasoning maxTokens must be a 32-bit integer of at least 1024.`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (definition.output) {
|
|
643
|
+
requireInstructions(definition.output.instructions, `Agent '${definition.id}' output instructions`);
|
|
644
|
+
}
|
|
645
|
+
const capabilities = definition.capabilities ?? [];
|
|
646
|
+
const names = new Set();
|
|
647
|
+
for (const item of capabilities) {
|
|
648
|
+
if (names.has(item.name)) {
|
|
649
|
+
throw new TandemError(`Agent '${definition.id}' has duplicate capability '${item.name}'.`);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
names.add(item.name);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const skills = definition.skills ?? [];
|
|
656
|
+
const skillDirectories = new Set();
|
|
657
|
+
for (const item of skills) {
|
|
658
|
+
if (typeof item?.directory !== "string" || item.directory.trim().length === 0) {
|
|
659
|
+
throw new TandemError(`Agent '${definition.id}' has a skill with an invalid directory.`);
|
|
660
|
+
}
|
|
661
|
+
if (!skillDirectories.add(item.directory)) {
|
|
662
|
+
throw new TandemError(`Agent '${definition.id}' has the skill directory '${item.directory}' more than once.`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (definition.temperature !== undefined &&
|
|
666
|
+
(!Number.isFinite(definition.temperature) ||
|
|
667
|
+
definition.temperature < 0 ||
|
|
668
|
+
definition.temperature > 2)) {
|
|
669
|
+
throw new TandemError(`Agent '${definition.id}' temperature must be between 0 and 2.`);
|
|
670
|
+
}
|
|
671
|
+
if (definition.maxOutputTokens !== undefined &&
|
|
672
|
+
(!Number.isSafeInteger(definition.maxOutputTokens) ||
|
|
673
|
+
definition.maxOutputTokens <= 0 ||
|
|
674
|
+
definition.maxOutputTokens > 2_147_483_647)) {
|
|
675
|
+
throw new TandemError(`Agent '${definition.id}' maxOutputTokens must be a positive 32-bit integer.`);
|
|
676
|
+
}
|
|
677
|
+
if (definition.checkpoint) {
|
|
678
|
+
const checkpoint = definition.checkpoint;
|
|
679
|
+
if (!Number.isSafeInteger(checkpoint.contextWindowTokens) ||
|
|
680
|
+
checkpoint.contextWindowTokens <= 0 ||
|
|
681
|
+
checkpoint.contextWindowTokens > 2_147_483_647) {
|
|
682
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint contextWindowTokens must be a positive 32-bit integer.`);
|
|
683
|
+
}
|
|
684
|
+
if (!Number.isSafeInteger(checkpoint.maxOutputTokens) ||
|
|
685
|
+
checkpoint.maxOutputTokens <= 0 ||
|
|
686
|
+
checkpoint.maxOutputTokens > 2_147_483_647 ||
|
|
687
|
+
checkpoint.maxOutputTokens >= checkpoint.contextWindowTokens) {
|
|
688
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint maxOutputTokens must be a positive 32-bit integer smaller than contextWindowTokens.`);
|
|
689
|
+
}
|
|
690
|
+
if (!Number.isSafeInteger(checkpoint.checkpointAtPercent) ||
|
|
691
|
+
checkpoint.checkpointAtPercent <= 0 ||
|
|
692
|
+
checkpoint.checkpointAtPercent >= 100) {
|
|
693
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint checkpointAtPercent must be between 1 and 99.`);
|
|
694
|
+
}
|
|
695
|
+
if (!capabilities.includes(checkpoint.capability)) {
|
|
696
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint capability must be attached to the agent.`);
|
|
697
|
+
}
|
|
698
|
+
requireInstructions(checkpoint.instructions, `Agent '${definition.id}' checkpoint instructions`);
|
|
699
|
+
if (checkpoint.session !== undefined &&
|
|
700
|
+
checkpoint.session !== "retain" &&
|
|
701
|
+
checkpoint.session !== "reset") {
|
|
702
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint session must be 'retain' or 'reset'.`);
|
|
703
|
+
}
|
|
704
|
+
if (checkpoint.disableCompaction !== undefined &&
|
|
705
|
+
typeof checkpoint.disableCompaction !== "boolean") {
|
|
706
|
+
throw new TandemError(`Agent '${definition.id}' checkpoint disableCompaction must be a boolean.`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return new AgentImplementation(definition.id, definition.persist, definition.instructions, definition.client, definition.message, definition.output, capabilities, skills, definition.workspace, definition.temperature, definition.maxOutputTokens, definition.reasoning, definition.continueSession ?? false, definition.checkpoint, definition.timeoutMs);
|
|
710
|
+
}
|
|
711
|
+
class ParallelImplementation extends NodeImplementation {
|
|
712
|
+
branches;
|
|
713
|
+
merge;
|
|
714
|
+
kind = "parallel";
|
|
715
|
+
constructor(id, persist, branches, merge) {
|
|
716
|
+
super(id, persist);
|
|
717
|
+
this.branches = branches;
|
|
718
|
+
this.merge = merge;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
export function parallel(definition) {
|
|
722
|
+
if (!definition) {
|
|
723
|
+
return (value) => createParallel(value);
|
|
724
|
+
}
|
|
725
|
+
return createParallel(definition);
|
|
726
|
+
}
|
|
727
|
+
function createParallel(definition) {
|
|
728
|
+
const entries = Object.entries(definition.branches);
|
|
729
|
+
if (entries.length < 2) {
|
|
730
|
+
throw new TandemError(`Parallel group '${definition.id}' requires at least two branches.`);
|
|
731
|
+
}
|
|
732
|
+
const participants = new Set(entries.map(([, participant]) => participant));
|
|
733
|
+
if (participants.size !== entries.length) {
|
|
734
|
+
throw new TandemError(`Parallel group '${definition.id}' must own a distinct participant per branch.`);
|
|
735
|
+
}
|
|
736
|
+
return new ParallelImplementation(definition.id, definition.persist, definition.branches, definition.merge);
|
|
737
|
+
}
|
|
738
|
+
class TerminalImplementation extends NodeImplementation {
|
|
739
|
+
failed;
|
|
740
|
+
summary;
|
|
741
|
+
kind = "terminal";
|
|
742
|
+
constructor(id, persist, failed, summary) {
|
|
743
|
+
super(id, persist);
|
|
744
|
+
this.failed = failed;
|
|
745
|
+
this.summary = summary;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
export function output(definition) {
|
|
749
|
+
return new TerminalImplementation(definition.id, definition.persist, definition.failed ?? false, definition.summary);
|
|
750
|
+
}
|
|
751
|
+
export function route(definition) {
|
|
752
|
+
return definition;
|
|
753
|
+
}
|
|
754
|
+
export function pipeline(definition) {
|
|
755
|
+
const start = definition.start;
|
|
756
|
+
const members = new Set(definition.nodes);
|
|
757
|
+
if (members.size !== definition.nodes.length) {
|
|
758
|
+
throw new Error("Pipeline nodes must contain each participant object exactly once.");
|
|
759
|
+
}
|
|
760
|
+
const ids = new Set(definition.nodes.map((node) => node.id));
|
|
761
|
+
if (ids.size !== definition.nodes.length) {
|
|
762
|
+
throw new Error("Pipeline node IDs must be unique.");
|
|
763
|
+
}
|
|
764
|
+
const ownedParticipants = new Set();
|
|
765
|
+
for (const node of definition.nodes) {
|
|
766
|
+
if (node.kind !== "parallel") {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const parallelNode = node;
|
|
770
|
+
for (const [branchId, participant] of Object.entries(parallelNode.branches)) {
|
|
771
|
+
if (branchId.trim().length === 0) {
|
|
772
|
+
throw new Error(`Parallel group '${node.id}' contains a blank branch ID.`);
|
|
773
|
+
}
|
|
774
|
+
if (members.has(participant)) {
|
|
775
|
+
throw new Error(`Parallel branch participant '${participant.id}' cannot also be a parent pipeline node.`);
|
|
776
|
+
}
|
|
777
|
+
if (!ownedParticipants.add(participant)) {
|
|
778
|
+
throw new Error(`Parallel branch participant '${participant.id}' is owned more than once.`);
|
|
779
|
+
}
|
|
780
|
+
if (ids.has(participant.id)) {
|
|
781
|
+
throw new Error(`Pipeline participant ID '${participant.id}' must be globally unique.`);
|
|
782
|
+
}
|
|
783
|
+
ids.add(participant.id);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!members.has(definition.start)) {
|
|
787
|
+
throw new Error(`Pipeline start '${definition.start.id}' must be the registered participant object.`);
|
|
788
|
+
}
|
|
789
|
+
if (start.kind === "terminal") {
|
|
790
|
+
throw new Error(`Pipeline start '${start.id}' cannot be a terminal.`);
|
|
791
|
+
}
|
|
792
|
+
for (const item of definition.routes) {
|
|
793
|
+
if (!members.has(item.from) || !members.has(item.to)) {
|
|
794
|
+
throw new Error(`Route '${item.label}' endpoints must be registered participant objects.`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const unconditionalRoutes = new Map();
|
|
798
|
+
for (const item of definition.routes) {
|
|
799
|
+
if (item.when) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const key = `${item.from.id}\u0000${item.outcome ?? "default"}`;
|
|
803
|
+
const existing = unconditionalRoutes.get(key);
|
|
804
|
+
if (existing) {
|
|
805
|
+
throw new Error(`Routes '${existing.label}' and '${item.label}' are both unconditional from '${item.from.id}'.`);
|
|
806
|
+
}
|
|
807
|
+
unconditionalRoutes.set(key, item);
|
|
808
|
+
}
|
|
809
|
+
if (new Set(definition.outputs).size !== definition.outputs.length) {
|
|
810
|
+
throw new Error("Pipeline outputs must contain each terminal exactly once.");
|
|
811
|
+
}
|
|
812
|
+
for (const item of definition.outputs) {
|
|
813
|
+
if (!members.has(item)) {
|
|
814
|
+
throw new Error(`Output '${item.id}' must be the registered participant object.`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const reachable = new Set([start]);
|
|
818
|
+
const pending = [start];
|
|
819
|
+
while (pending.length > 0) {
|
|
820
|
+
const source = pending.pop();
|
|
821
|
+
for (const item of definition.routes) {
|
|
822
|
+
if (item.from === source && !reachable.has(item.to)) {
|
|
823
|
+
reachable.add(item.to);
|
|
824
|
+
pending.push(item.to);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const outputs = new Set(definition.outputs);
|
|
829
|
+
for (const node of reachable) {
|
|
830
|
+
if (node.kind === "terminal" && !outputs.has(node)) {
|
|
831
|
+
throw new Error(`Reachable terminal '${node.id}' must be listed in outputs.`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const item of definition.outputs) {
|
|
835
|
+
if (!reachable.has(item)) {
|
|
836
|
+
throw new Error(`Output '${item.id}' must be reachable from start '${start.id}'.`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return { ...definition, persist: definition.persist ?? false };
|
|
840
|
+
}
|
|
841
|
+
const runObservationSchema = z.discriminatedUnion("kind", [
|
|
842
|
+
z
|
|
843
|
+
.object({ version: z.literal(1), kind: z.literal("stepStarted"), stepId: z.string().min(1) })
|
|
844
|
+
.strict(),
|
|
845
|
+
z
|
|
846
|
+
.object({ version: z.literal(1), kind: z.literal("stepCompleted"), stepId: z.string().min(1) })
|
|
847
|
+
.strict(),
|
|
848
|
+
z
|
|
849
|
+
.object({ version: z.literal(1), kind: z.literal("stepCancelled"), stepId: z.string().min(1) })
|
|
850
|
+
.strict(),
|
|
851
|
+
z
|
|
852
|
+
.object({
|
|
853
|
+
version: z.literal(1),
|
|
854
|
+
kind: z.literal("stepFaulted"),
|
|
855
|
+
stepId: z.string().min(1),
|
|
856
|
+
error: z.string(),
|
|
857
|
+
})
|
|
858
|
+
.strict(),
|
|
859
|
+
z
|
|
860
|
+
.object({
|
|
861
|
+
version: z.literal(1),
|
|
862
|
+
kind: z.literal("agentText"),
|
|
863
|
+
stepId: z.string().min(1),
|
|
864
|
+
text: z.string(),
|
|
865
|
+
})
|
|
866
|
+
.strict(),
|
|
867
|
+
z
|
|
868
|
+
.object({
|
|
869
|
+
version: z.literal(1),
|
|
870
|
+
kind: z.literal("agentModelSelected"),
|
|
871
|
+
stepId: z.string().min(1),
|
|
872
|
+
modelId: z.string().min(1),
|
|
873
|
+
})
|
|
874
|
+
.strict(),
|
|
875
|
+
z
|
|
876
|
+
.object({
|
|
877
|
+
version: z.literal(1),
|
|
878
|
+
kind: z.literal("agentReasoning"),
|
|
879
|
+
stepId: z.string().min(1),
|
|
880
|
+
text: z.string(),
|
|
881
|
+
})
|
|
882
|
+
.strict(),
|
|
883
|
+
z
|
|
884
|
+
.object({
|
|
885
|
+
version: z.literal(1),
|
|
886
|
+
kind: z.literal("agentUsage"),
|
|
887
|
+
stepId: z.string().min(1),
|
|
888
|
+
inputTokens: z.number().int().nonnegative(),
|
|
889
|
+
outputTokens: z.number().int().nonnegative(),
|
|
890
|
+
reasoningTokens: z.number().int().nonnegative(),
|
|
891
|
+
currentContextTokens: z.number().int().nonnegative(),
|
|
892
|
+
contextWindowTokens: z.number().int().nonnegative().nullable(),
|
|
893
|
+
})
|
|
894
|
+
.strict(),
|
|
895
|
+
z
|
|
896
|
+
.object({
|
|
897
|
+
version: z.literal(1),
|
|
898
|
+
kind: z.literal("structuredOutputRejected"),
|
|
899
|
+
stepId: z.string().min(1),
|
|
900
|
+
attempt: z.number().int().positive(),
|
|
901
|
+
problems: z.array(z.object({ field: z.string(), message: z.string().min(1) }).strict()),
|
|
902
|
+
rawResponse: z.string(),
|
|
903
|
+
})
|
|
904
|
+
.strict(),
|
|
905
|
+
]);
|
|
906
|
+
const acceptedKinds = [
|
|
907
|
+
"StructuredOutputAccepted",
|
|
908
|
+
"CapabilityAccepted",
|
|
909
|
+
"InteractionRequested",
|
|
910
|
+
"InteractionAnswered",
|
|
911
|
+
"StepCompleted",
|
|
912
|
+
];
|
|
913
|
+
const acceptedValueSchema = z
|
|
914
|
+
.object({
|
|
915
|
+
kind: z.enum(acceptedKinds),
|
|
916
|
+
stepId: z.string().min(1),
|
|
917
|
+
valueType: z.string().min(1).nullable(),
|
|
918
|
+
payload: z.unknown().nullable(),
|
|
919
|
+
})
|
|
920
|
+
.strict()
|
|
921
|
+
.refine((value) => value.valueType !== null || value.payload !== null, {
|
|
922
|
+
message: "valueType and payload cannot both be null",
|
|
923
|
+
});
|
|
924
|
+
const acceptedValuesSchema = z.array(acceptedValueSchema);
|
|
925
|
+
const runResultSchema = z
|
|
926
|
+
.object({
|
|
927
|
+
runId: z.uuid(),
|
|
928
|
+
succeeded: z.boolean(),
|
|
929
|
+
state: z.unknown(),
|
|
930
|
+
summary: z.string().nullable(),
|
|
931
|
+
})
|
|
932
|
+
.strict();
|
|
933
|
+
export async function inspectAccepted(options) {
|
|
934
|
+
try {
|
|
935
|
+
const { inspectAcceptedAsync } = await import("@maxanstey-meridian/tandem-runtime");
|
|
936
|
+
return parseJson(acceptedValuesSchema, await inspectAcceptedAsync(options.ledgerPath, options.runId), "accepted values").map((value) => ({
|
|
937
|
+
version: 1,
|
|
938
|
+
kind: value.kind,
|
|
939
|
+
stepId: value.stepId,
|
|
940
|
+
valueType: value.valueType,
|
|
941
|
+
payload: value.payload,
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (error instanceof TandemError) {
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
throw new TandemRuntimeError("inspect", error);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
export async function run(graph, initial, options = {}) {
|
|
952
|
+
const initialState = serializeBoundary(graph.state, initial, "initial state");
|
|
953
|
+
if ((graph.persist || graph.nodes.some(participantPersists)) && !options.ledgerPath) {
|
|
954
|
+
throw new TandemError("ledgerPath is required when persistence is enabled.");
|
|
955
|
+
}
|
|
956
|
+
const callbacks = new CallbackRegistry();
|
|
957
|
+
try {
|
|
958
|
+
const nodes = graph.nodes.map((node) => compileNode(node, graph.state, callbacks));
|
|
959
|
+
const routes = graph.routes.map((item) => {
|
|
960
|
+
const callback = item.when
|
|
961
|
+
? callbacks.registerSync((state) => String(item.when(parseJson(graph.state, state, `route '${item.label}' state`))))
|
|
962
|
+
: undefined;
|
|
963
|
+
return {
|
|
964
|
+
source: item.from.id,
|
|
965
|
+
target: item.to.id,
|
|
966
|
+
label: item.label,
|
|
967
|
+
outcome: item.outcome,
|
|
968
|
+
predicateCallback: callback,
|
|
969
|
+
};
|
|
970
|
+
});
|
|
971
|
+
const handlerEntries = interactionHandlerEntries(options.interactions);
|
|
972
|
+
const members = new Set(graph.nodes);
|
|
973
|
+
const interactionHandlers = handlerEntries.map((entry, index) => {
|
|
974
|
+
if (!members.has(entry.interaction)) {
|
|
975
|
+
throw new TandemError(`Interaction handler '${entry.interaction.id}' must target a participant in pipeline '${graph.name}'.`);
|
|
976
|
+
}
|
|
977
|
+
const implementation = entry.interaction;
|
|
978
|
+
const handleCallback = callbacks.registerAsync(async (_, input, signal) => {
|
|
979
|
+
const response = await entry.handle(parseJson(implementation.requestSchema, input, `${implementation.id} request input`), { signal });
|
|
980
|
+
signal.throwIfAborted();
|
|
981
|
+
return serializeBoundary(implementation.responseSchema, response, `${implementation.id} response`);
|
|
982
|
+
});
|
|
983
|
+
return { id: `h${index}`, target: entry.interaction.id, handleCallback };
|
|
984
|
+
});
|
|
985
|
+
const observationCallback = options.observe
|
|
986
|
+
? callbacks.registerAsync(async (_, input, signal) => {
|
|
987
|
+
const event = parseJson(runObservationSchema, input, "run observation");
|
|
988
|
+
const observationSignal = options.signal?.aborted === true
|
|
989
|
+
? AbortSignal.abort(options.signal.reason)
|
|
990
|
+
: event.kind === "stepCancelled" && !signal.aborted
|
|
991
|
+
? AbortSignal.abort()
|
|
992
|
+
: signal;
|
|
993
|
+
await options.observe(event, { signal: observationSignal });
|
|
994
|
+
return "";
|
|
995
|
+
})
|
|
996
|
+
: undefined;
|
|
997
|
+
const { runRegisteredGraphAsync } = await import("@maxanstey-meridian/tandem-runtime");
|
|
998
|
+
const resultJson = await runRegisteredGraphAsync(JSON.stringify({
|
|
999
|
+
contractVersion: 10,
|
|
1000
|
+
name: graph.name,
|
|
1001
|
+
start: graph.start.id,
|
|
1002
|
+
initialState,
|
|
1003
|
+
persist: graph.persist,
|
|
1004
|
+
ledgerPath: options.ledgerPath,
|
|
1005
|
+
presentation: options.presentation,
|
|
1006
|
+
terminal: options.terminal,
|
|
1007
|
+
observationCallback,
|
|
1008
|
+
nodes,
|
|
1009
|
+
routes,
|
|
1010
|
+
outputs: graph.outputs.map((item) => item.id),
|
|
1011
|
+
interactionHandlers,
|
|
1012
|
+
}), (id, state, input) => callbacks.invokeSync(id, state, input), (id, state, input, signal) => callbacks.invokeAsync(id, state, input, signal), options.signal);
|
|
1013
|
+
const result = parseJson(runResultSchema, resultJson, "run result");
|
|
1014
|
+
return { ...result, state: parse(graph.state, result.state, "final state") };
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
if (error instanceof TandemError) {
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
const callbackFailure = callbackContractFailure(error);
|
|
1021
|
+
if (callbackFailure) {
|
|
1022
|
+
throw new ContractValidationError(callbackFailure.boundary, callbackFailure.problems);
|
|
1023
|
+
}
|
|
1024
|
+
if (isCancellationError(error, options.signal?.aborted === true)) {
|
|
1025
|
+
throw new TandemCancellationError(error);
|
|
1026
|
+
}
|
|
1027
|
+
throw new TandemRuntimeError("run", error);
|
|
1028
|
+
}
|
|
1029
|
+
finally {
|
|
1030
|
+
callbacks.dispose();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function participantPersists(node) {
|
|
1034
|
+
const implementation = node;
|
|
1035
|
+
if (implementation.persist) {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
return implementation instanceof ParallelImplementation
|
|
1039
|
+
? Object.values(implementation.branches).some((branch) => branch.persist === true)
|
|
1040
|
+
: false;
|
|
1041
|
+
}
|
|
1042
|
+
function issues(schema, input) {
|
|
1043
|
+
let value;
|
|
1044
|
+
try {
|
|
1045
|
+
value = JSON.parse(input);
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
return JSON.stringify([{ path: "$", message: "Invalid JSON" }]);
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
parse(schema, value, "agent contract");
|
|
1052
|
+
return "";
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
if (error instanceof ContractValidationError) {
|
|
1056
|
+
return JSON.stringify(error.problems);
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const validationProblemsSchema = z.array(z.object({ path: z.string(), message: z.string() }).strict());
|
|
1062
|
+
function validationProblems(problems, boundary) {
|
|
1063
|
+
return JSON.stringify(parse(validationProblemsSchema, problems, boundary));
|
|
1064
|
+
}
|
|
1065
|
+
function requireInstructions(instructions, boundary) {
|
|
1066
|
+
if (typeof instructions !== "string" || instructions.trim().length === 0) {
|
|
1067
|
+
throw new ContractValidationError(boundary, [
|
|
1068
|
+
{ path: "$", message: "Instructions must be a non-blank string." },
|
|
1069
|
+
]);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function interactionHandlerEntries(handlers) {
|
|
1073
|
+
if (!handlers) {
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
if (!(handlers instanceof InteractionHandlersImplementation)) {
|
|
1077
|
+
throw new TandemError("interactions must be created by interactions().");
|
|
1078
|
+
}
|
|
1079
|
+
return handlers.entries;
|
|
1080
|
+
}
|
|
1081
|
+
function compileNode(node, stateSchema, callbacks) {
|
|
1082
|
+
const implementation = node;
|
|
1083
|
+
const base = { id: node.id, persist: implementation.persist };
|
|
1084
|
+
if (implementation instanceof StageImplementation) {
|
|
1085
|
+
const run = callbacks.registerAsync(async (state, _, signal) => serializeBoundary(stateSchema, await implementation.execute(parseJson(stateSchema, state, `${node.id} input`), {
|
|
1086
|
+
signal,
|
|
1087
|
+
}), `${node.id} output`));
|
|
1088
|
+
return { ...base, kind: "stage", runCallback: run };
|
|
1089
|
+
}
|
|
1090
|
+
if (implementation instanceof InteractionImplementation) {
|
|
1091
|
+
const request = callbacks.registerSync((state) => serializeBoundary(implementation.requestSchema, implementation.request(parseJson(stateSchema, state, `${node.id} state`)), `${node.id} request`));
|
|
1092
|
+
const apply = callbacks.registerSync((state, input) => serializeBoundary(stateSchema, implementation.apply(parseJson(stateSchema, state, `${node.id} state`), parseJson(implementation.responseSchema, input, `${node.id} response input`)), `${node.id} applied state`));
|
|
1093
|
+
return {
|
|
1094
|
+
...base,
|
|
1095
|
+
kind: "interaction",
|
|
1096
|
+
requestCallback: request,
|
|
1097
|
+
applyCallback: apply,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (implementation instanceof AgentImplementation) {
|
|
1101
|
+
const message = callbacks.registerSync((state) => implementation.message(parseJson(stateSchema, state, `${node.id} message state`)));
|
|
1102
|
+
const output = implementation.output
|
|
1103
|
+
? compileAgentOutput(node.id, implementation.output, stateSchema, callbacks)
|
|
1104
|
+
: undefined;
|
|
1105
|
+
const capabilities = implementation.granted.map((item) => item[compileCapabilityBrand]({ id: node.id, stateSchema, callbacks }));
|
|
1106
|
+
const workspace = implementation.workspace
|
|
1107
|
+
? compileWorkspace(node.id, implementation.workspace, stateSchema, callbacks)
|
|
1108
|
+
: undefined;
|
|
1109
|
+
return {
|
|
1110
|
+
...base,
|
|
1111
|
+
kind: "agent",
|
|
1112
|
+
instructions: implementation.instructions,
|
|
1113
|
+
client: { ...implementation.client, verifyModel: implementation.client.verifyModel ?? false },
|
|
1114
|
+
messageCallback: message,
|
|
1115
|
+
output,
|
|
1116
|
+
capabilities,
|
|
1117
|
+
skillDirectories: implementation.skills.map((item) => item.directory),
|
|
1118
|
+
temperature: implementation.temperature,
|
|
1119
|
+
maxOutputTokens: implementation.maxOutputTokens,
|
|
1120
|
+
reasoning: implementation.reasoning,
|
|
1121
|
+
continueSession: implementation.continueSession,
|
|
1122
|
+
checkpoint: implementation.checkpoint
|
|
1123
|
+
? {
|
|
1124
|
+
contextWindowTokens: implementation.checkpoint.contextWindowTokens,
|
|
1125
|
+
maxOutputTokens: implementation.checkpoint.maxOutputTokens,
|
|
1126
|
+
checkpointAtPercent: implementation.checkpoint.checkpointAtPercent,
|
|
1127
|
+
capabilityName: implementation.checkpoint.capability.name,
|
|
1128
|
+
instructions: implementation.checkpoint.instructions,
|
|
1129
|
+
messageCallback: callbacks.registerSync((state, input) => implementation.checkpoint.message(parseJson(stateSchema, state, `${node.id} checkpoint state`), Number(input))),
|
|
1130
|
+
resetSession: (implementation.checkpoint.session ?? "reset") === "reset",
|
|
1131
|
+
disableCompaction: implementation.checkpoint.disableCompaction ?? false,
|
|
1132
|
+
}
|
|
1133
|
+
: undefined,
|
|
1134
|
+
timeoutMilliseconds: implementation.timeoutMs,
|
|
1135
|
+
workspace,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
if (implementation instanceof ParallelImplementation) {
|
|
1139
|
+
const entries = Object.entries(implementation.branches);
|
|
1140
|
+
const branchIds = entries.map(([branchId]) => branchId);
|
|
1141
|
+
const mergeCallback = callbacks.registerSync((state, input) => {
|
|
1142
|
+
const baseline = parseJson(stateSchema, state, `${node.id} merge baseline`);
|
|
1143
|
+
let raw;
|
|
1144
|
+
try {
|
|
1145
|
+
raw = JSON.parse(input);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
throw new ContractValidationError(`${node.id} merge branches`, [
|
|
1149
|
+
{ path: "$", message: "Invalid JSON" },
|
|
1150
|
+
]);
|
|
1151
|
+
}
|
|
1152
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1153
|
+
throw new ContractValidationError(`${node.id} merge branches`, [
|
|
1154
|
+
{ path: "$", message: "Expected a branch-state object." },
|
|
1155
|
+
]);
|
|
1156
|
+
}
|
|
1157
|
+
const values = raw;
|
|
1158
|
+
if (!isDeepStrictEqual(Object.keys(values).sort(), [...branchIds].sort())) {
|
|
1159
|
+
throw new ContractValidationError(`${node.id} merge branches`, [
|
|
1160
|
+
{ path: "$", message: "Branch-state keys do not match the authored branches." },
|
|
1161
|
+
]);
|
|
1162
|
+
}
|
|
1163
|
+
const parsed = Object.fromEntries(branchIds.map((branchId) => [
|
|
1164
|
+
branchId,
|
|
1165
|
+
parse(stateSchema, values[branchId], `${node.id} branch '${branchId}' state`),
|
|
1166
|
+
]));
|
|
1167
|
+
return serializeBoundary(stateSchema, implementation.merge(baseline, parsed), `${node.id} merged state`);
|
|
1168
|
+
});
|
|
1169
|
+
return {
|
|
1170
|
+
...base,
|
|
1171
|
+
kind: "parallel",
|
|
1172
|
+
branches: entries.map(([id, participant]) => ({
|
|
1173
|
+
id,
|
|
1174
|
+
participant: compileNode(participant, stateSchema, callbacks),
|
|
1175
|
+
})),
|
|
1176
|
+
mergeCallback,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const terminal = implementation;
|
|
1180
|
+
const summary = callbacks.registerSync((state) => terminal.summary(parseJson(stateSchema, state, `${node.id} state`)));
|
|
1181
|
+
return { ...base, kind: terminal.failed ? "failure" : "completion", summaryCallback: summary };
|
|
1182
|
+
}
|
|
1183
|
+
function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
1184
|
+
if (!(configuration instanceof AgentWorkspaceConfigurationImplementation)) {
|
|
1185
|
+
throw new TandemError(`Agent '${id}' workspace must be created by agentWorkspace().withTools().`);
|
|
1186
|
+
}
|
|
1187
|
+
const workspace = configuration.workspace;
|
|
1188
|
+
const pathCallback = callbacks.registerSync((state) => {
|
|
1189
|
+
const value = workspace.path(parseJson(stateSchema, state, `${id} workspace path state`));
|
|
1190
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1191
|
+
throw new TandemError(`Agent '${id}' workspace path must be non-blank.`);
|
|
1192
|
+
}
|
|
1193
|
+
return value;
|
|
1194
|
+
});
|
|
1195
|
+
const commandsCallback = callbacks.registerSync((state) => {
|
|
1196
|
+
const typedState = parseJson(stateSchema, state, `${id} workspace commands state`);
|
|
1197
|
+
const value = typeof workspace.commandSource === "function"
|
|
1198
|
+
? workspace.commandSource(typedState)
|
|
1199
|
+
: (workspace.commandSource ?? []);
|
|
1200
|
+
validateAgentCommands(value, `Agent '${id}' workspace commands`);
|
|
1201
|
+
return serializeBoundary(z.array(z
|
|
1202
|
+
.object({
|
|
1203
|
+
name: z.string(),
|
|
1204
|
+
description: z.string(),
|
|
1205
|
+
command: z.string(),
|
|
1206
|
+
arguments: z
|
|
1207
|
+
.array(z
|
|
1208
|
+
.object({
|
|
1209
|
+
name: z.string(),
|
|
1210
|
+
description: z.string(),
|
|
1211
|
+
flag: z.string(),
|
|
1212
|
+
pattern: z.string().optional(),
|
|
1213
|
+
allowedValues: z.array(z.string()).optional(),
|
|
1214
|
+
maxLength: z.number().optional(),
|
|
1215
|
+
})
|
|
1216
|
+
.strict())
|
|
1217
|
+
.optional(),
|
|
1218
|
+
})
|
|
1219
|
+
.strict()), value, `${id} workspace commands`);
|
|
1220
|
+
});
|
|
1221
|
+
const selected = new Set();
|
|
1222
|
+
const toolGroups = configuration.groups.map((group, index) => {
|
|
1223
|
+
let includeCommands = false;
|
|
1224
|
+
const tools = [];
|
|
1225
|
+
for (const tool of group.tools) {
|
|
1226
|
+
if (typeof tool === "string") {
|
|
1227
|
+
if (selected.has(tool)) {
|
|
1228
|
+
throw new TandemError(`Agent '${id}' selects '${tool}' more than once.`);
|
|
1229
|
+
}
|
|
1230
|
+
selected.add(tool);
|
|
1231
|
+
tools.push(tool);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
if (tool[commandSelectionBrand] !== workspace) {
|
|
1235
|
+
throw new TandemError(`Agent '${id}' selects commands from another workspace.`);
|
|
1236
|
+
}
|
|
1237
|
+
if (workspace.commandSource === undefined) {
|
|
1238
|
+
throw new TandemError(`Agent '${id}' selects workspace commands without declaring a command catalogue.`);
|
|
1239
|
+
}
|
|
1240
|
+
if (selected.has(tool)) {
|
|
1241
|
+
throw new TandemError(`Agent '${id}' selects workspace commands twice.`);
|
|
1242
|
+
}
|
|
1243
|
+
selected.add(tool);
|
|
1244
|
+
includeCommands = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const whenCallback = group.predicate
|
|
1248
|
+
? callbacks.registerSync((state) => {
|
|
1249
|
+
const value = group.predicate(parseJson(stateSchema, state, `${id} tool group ${index} state`));
|
|
1250
|
+
if (typeof value !== "boolean") {
|
|
1251
|
+
throw new TandemError(`Agent '${id}' tool group ${index} predicate must return a boolean.`);
|
|
1252
|
+
}
|
|
1253
|
+
return String(value);
|
|
1254
|
+
})
|
|
1255
|
+
: undefined;
|
|
1256
|
+
return { tools, includeCommands, whenCallback };
|
|
1257
|
+
});
|
|
1258
|
+
const interceptCallback = configuration.interceptTool
|
|
1259
|
+
? callbacks.registerAsync(async (state, input, signal) => {
|
|
1260
|
+
const typedState = parseJson(stateSchema, state, `${id} tool interception state`);
|
|
1261
|
+
let invocation;
|
|
1262
|
+
try {
|
|
1263
|
+
invocation = JSON.parse(input);
|
|
1264
|
+
}
|
|
1265
|
+
catch {
|
|
1266
|
+
throw new TandemError(`Agent '${id}' received an invalid tool interception payload.`);
|
|
1267
|
+
}
|
|
1268
|
+
const result = await configuration.interceptTool(typedState, invocation, { signal });
|
|
1269
|
+
if (result !== null && typeof result !== "string") {
|
|
1270
|
+
throw new TandemError(`Agent '${id}' tool interceptor must return a string or null.`);
|
|
1271
|
+
}
|
|
1272
|
+
return JSON.stringify(result);
|
|
1273
|
+
})
|
|
1274
|
+
: undefined;
|
|
1275
|
+
return { pathCallback, commandsCallback, toolGroups, interceptCallback };
|
|
1276
|
+
}
|
|
1277
|
+
function compileAgentOutput(id, output, stateSchema, callbacks) {
|
|
1278
|
+
const validate = callbacks.registerSync((_, input) => issues(output.schema, input));
|
|
1279
|
+
const validateFor = output.validateFor
|
|
1280
|
+
? callbacks.registerSync((state, input) => validationProblems(output.validateFor(parseJson(stateSchema, state, `${id} state`), parseJson(output.schema, input, `${id} output`)), `${id} output contextual validation`))
|
|
1281
|
+
: undefined;
|
|
1282
|
+
const apply = callbacks.registerSync((state, input) => serializeBoundary(stateSchema, output.apply(parseJson(stateSchema, state, `${id} state`), parseJson(output.schema, input, `${id} output`)), `${id} applied state`));
|
|
1283
|
+
return {
|
|
1284
|
+
instructions: output.instructions,
|
|
1285
|
+
jsonSchema: inputJsonSchema(output.schema, `${id} output schema`),
|
|
1286
|
+
validateCallback: validate,
|
|
1287
|
+
validateForCallback: validateFor,
|
|
1288
|
+
applyCallback: apply,
|
|
1289
|
+
valueType: `${id}.output`,
|
|
1290
|
+
};
|
|
1291
|
+
}
|