@maxanstey-meridian/tandem 0.1.6 → 0.2.0
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/dist/index.d.ts +86 -72
- package/dist/index.js +656 -894
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -38,8 +38,10 @@ export class ContractValidationError extends TandemError {
|
|
|
38
38
|
this.problems = problems;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const callbackSucceeded = (value) => JSON.stringify({ succeeded: true, value });
|
|
42
|
+
const callbackFailed = (error) => JSON.stringify({
|
|
43
|
+
succeeded: false,
|
|
44
|
+
error: error instanceof ContractValidationError
|
|
43
45
|
? {
|
|
44
46
|
name: error.name,
|
|
45
47
|
message: error.message,
|
|
@@ -49,94 +51,80 @@ function callbackError(error) {
|
|
|
49
51
|
: {
|
|
50
52
|
name: error instanceof Error ? error.name : "Error",
|
|
51
53
|
message: error instanceof Error ? error.message : String(error),
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
/** Synchronous callbacks cannot observe cancellation; they receive a signal that never aborts. */
|
|
57
|
+
const neverAborted = new AbortController().signal;
|
|
54
58
|
class CallbackRegistry {
|
|
55
|
-
#
|
|
56
|
-
#async = new Map();
|
|
59
|
+
#callbacks = new Map();
|
|
57
60
|
#next = 0;
|
|
58
61
|
#disposed = false;
|
|
59
62
|
registerSync(callback) {
|
|
60
|
-
|
|
61
|
-
this.#sync.set(id, callback);
|
|
62
|
-
return id;
|
|
63
|
+
return this.#register(callback);
|
|
63
64
|
}
|
|
64
65
|
registerAsync(callback) {
|
|
65
|
-
|
|
66
|
-
this.#async.set(id, callback);
|
|
67
|
-
return id;
|
|
66
|
+
return this.#register(callback);
|
|
68
67
|
}
|
|
69
|
-
|
|
68
|
+
/** Never throws: failures travel back to the bridge as an encoded `CallbackResult`. */
|
|
69
|
+
invoke(id, state, input, signal) {
|
|
70
70
|
try {
|
|
71
|
-
const callback = this.#
|
|
71
|
+
const callback = this.#callbacks.get(id);
|
|
72
72
|
if (!callback) {
|
|
73
73
|
throw new Error(`Unknown internal callback '${id}'.`);
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
});
|
|
75
|
+
const value = callback(state, input, signal);
|
|
76
|
+
return typeof value === "string"
|
|
77
|
+
? callbackSucceeded(value)
|
|
78
|
+
: value.then(callbackSucceeded, callbackFailed);
|
|
97
79
|
}
|
|
98
80
|
catch (error) {
|
|
99
|
-
return
|
|
100
|
-
succeeded: false,
|
|
101
|
-
error: callbackError(error),
|
|
102
|
-
});
|
|
81
|
+
return callbackFailed(error);
|
|
103
82
|
}
|
|
104
83
|
}
|
|
105
84
|
dispose() {
|
|
106
85
|
this.#disposed = true;
|
|
107
|
-
this.#
|
|
108
|
-
this.#async.clear();
|
|
86
|
+
this.#callbacks.clear();
|
|
109
87
|
}
|
|
110
|
-
#
|
|
88
|
+
#register(callback) {
|
|
111
89
|
if (this.#disposed) {
|
|
112
90
|
throw new Error("Callback registry has been disposed.");
|
|
113
91
|
}
|
|
114
|
-
|
|
92
|
+
const id = `c${this.#next++}`;
|
|
93
|
+
this.#callbacks.set(id, callback);
|
|
94
|
+
return id;
|
|
115
95
|
}
|
|
116
96
|
}
|
|
97
|
+
// The bridge surfaces JS callback errors and .NET cancellations only as exception message text,
|
|
98
|
+
// so these markers are how a failure's origin is recovered (until the bridge returns structured
|
|
99
|
+
// errors).
|
|
100
|
+
const CALLBACK_CONTRACT_MARKER = "TANDEM_CALLBACK_CONTRACT:";
|
|
101
|
+
const CALLBACK_FAILURE_MARKER = "JavaScript callback failed:";
|
|
102
|
+
const BRIDGE_CANCELLATION_MESSAGE = /\boperation was (?:cancell?ed|aborted)\b/i;
|
|
103
|
+
const callbackFailureSchema = z.object({
|
|
104
|
+
boundary: z.string(),
|
|
105
|
+
problems: z.array(z.object({ path: z.string(), message: z.string() })),
|
|
106
|
+
});
|
|
117
107
|
function callbackContractFailure(error) {
|
|
118
|
-
const marker = "TANDEM_CALLBACK_CONTRACT:";
|
|
119
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
-
const start = message.indexOf(
|
|
109
|
+
const start = message.indexOf(CALLBACK_CONTRACT_MARKER);
|
|
121
110
|
if (start < 0) {
|
|
122
111
|
return null;
|
|
123
112
|
}
|
|
124
113
|
try {
|
|
125
|
-
|
|
114
|
+
const failure = callbackFailureSchema.safeParse(JSON.parse(message.slice(start + CALLBACK_CONTRACT_MARKER.length)));
|
|
115
|
+
return failure.success ? failure.data : null;
|
|
126
116
|
}
|
|
127
117
|
catch {
|
|
128
118
|
return null;
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
|
-
function
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
if (error.message.includes("JavaScript callback failed:") && !signalAborted) {
|
|
136
|
-
return false;
|
|
121
|
+
function isCancellation(error, signal) {
|
|
122
|
+
if (signal?.aborted) {
|
|
123
|
+
return true;
|
|
137
124
|
}
|
|
138
|
-
return (error
|
|
139
|
-
|
|
125
|
+
return (error instanceof Error &&
|
|
126
|
+
!error.message.includes(CALLBACK_FAILURE_MARKER) &&
|
|
127
|
+
(error.name === "AbortError" || BRIDGE_CANCELLATION_MESSAGE.test(error.message)));
|
|
140
128
|
}
|
|
141
129
|
function path(parts) {
|
|
142
130
|
return parts.length === 0
|
|
@@ -176,43 +164,24 @@ function parse(schema, value, boundary) {
|
|
|
176
164
|
}
|
|
177
165
|
return result;
|
|
178
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The walker is the whole losslessness check: a stringify/parse round-trip compares only enumerable
|
|
169
|
+
* data, so it misses accessors and non-enumerable properties, and it cannot name the failing path.
|
|
170
|
+
*/
|
|
179
171
|
function serializeBoundary(schema, value, boundary) {
|
|
180
172
|
const parsed = parse(schema, value, boundary);
|
|
181
173
|
const problem = jsonValueProblem(parsed, "$", new WeakSet());
|
|
182
174
|
if (problem) {
|
|
183
175
|
throw new ContractValidationError(boundary, [problem]);
|
|
184
176
|
}
|
|
185
|
-
|
|
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;
|
|
177
|
+
return JSON.stringify(parsed);
|
|
209
178
|
}
|
|
210
179
|
function jsonValueProblem(value, valuePath, seen) {
|
|
211
180
|
if (value === undefined) {
|
|
212
181
|
return { path: valuePath, message: "undefined is not JSON-serializable." };
|
|
213
182
|
}
|
|
214
|
-
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
215
|
-
return { path: valuePath, message: "Non-finite numbers are not JSON-serializable." };
|
|
183
|
+
if (typeof value === "number" && (!Number.isFinite(value) || Object.is(value, -0))) {
|
|
184
|
+
return { path: valuePath, message: "Non-finite numbers and -0 are not JSON-serializable." };
|
|
216
185
|
}
|
|
217
186
|
if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
|
|
218
187
|
return { path: valuePath, message: `${typeof value} values are not JSON-serializable.` };
|
|
@@ -312,164 +281,133 @@ function inputJsonSchema(schema, boundary) {
|
|
|
312
281
|
]);
|
|
313
282
|
}
|
|
314
283
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
}
|
|
284
|
+
const positiveInt32 = z.int32({ error: "must be a positive 32-bit integer" }).min(1);
|
|
285
|
+
const nonBlankString = z
|
|
286
|
+
.string({ error: "must be a non-blank string" })
|
|
287
|
+
.refine((value) => value.trim().length > 0, { error: "must be a non-blank string" });
|
|
288
|
+
const hasUnique = (items, key) => new Set(items.map(key)).size === items.length;
|
|
289
|
+
/** The participant brand only pins `TState` for the type checker; `kind` identifies records at runtime. */
|
|
290
|
+
const keepState = (state) => state;
|
|
332
291
|
export function stage(definition) {
|
|
333
|
-
|
|
292
|
+
const record = {
|
|
293
|
+
...definition,
|
|
294
|
+
[participantBrand]: keepState,
|
|
295
|
+
kind: "stage",
|
|
296
|
+
};
|
|
297
|
+
return Object.freeze(record);
|
|
334
298
|
}
|
|
335
299
|
const taskAgentBrand = Symbol("taskAgent");
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
this.participant = agent({
|
|
300
|
+
export function taskAgent(definition) {
|
|
301
|
+
const { validateFor } = definition.output;
|
|
302
|
+
const record = {
|
|
303
|
+
[taskAgentBrand]: true,
|
|
304
|
+
id: definition.id,
|
|
305
|
+
input: definition.input,
|
|
306
|
+
result: definition.result,
|
|
307
|
+
state: z.object({
|
|
308
|
+
input: definition.input,
|
|
309
|
+
output: z.object({ value: definition.result }).nullable(),
|
|
310
|
+
}),
|
|
311
|
+
participant: agent({
|
|
349
312
|
...definition,
|
|
350
313
|
message: (state) => definition.message(state.input),
|
|
351
314
|
output: {
|
|
352
315
|
...definition.output,
|
|
353
|
-
validateFor:
|
|
354
|
-
|
|
355
|
-
: undefined,
|
|
356
|
-
apply: (state, value) => ({
|
|
357
|
-
...state,
|
|
358
|
-
output: { value },
|
|
359
|
-
}),
|
|
316
|
+
validateFor: validateFor ? (state, value) => validateFor(state.input, value) : undefined,
|
|
317
|
+
apply: (state, value) => ({ ...state, output: { value } }),
|
|
360
318
|
},
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function taskAgent(definition) {
|
|
365
|
-
return new TaskAgentImplementation(definition.id, definition.input, definition.result, definition);
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
return Object.freeze(record);
|
|
366
322
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
super(definition.id, definition.persist);
|
|
372
|
-
this.definition = definition;
|
|
323
|
+
/** Task agents are only created by `taskAgent()`, whose record is the returned object. */
|
|
324
|
+
function taskAgentRecord(reference) {
|
|
325
|
+
if (!("participant" in reference)) {
|
|
326
|
+
throw new TandemError("Use taskAgent to declare collection agents.");
|
|
373
327
|
}
|
|
328
|
+
return reference;
|
|
374
329
|
}
|
|
330
|
+
const collectionOptionsSchema = z.object({
|
|
331
|
+
max: positiveInt32,
|
|
332
|
+
agents: z
|
|
333
|
+
.array(z.object({ id: z.string() }))
|
|
334
|
+
.refine((agents) => hasUnique(agents, (agent) => agent.id), { error: "must have unique IDs" }),
|
|
335
|
+
});
|
|
375
336
|
export function collection(definition) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
class InteractionImplementation extends NodeImplementation {
|
|
385
|
-
requestSchema;
|
|
386
|
-
responseSchema;
|
|
387
|
-
request;
|
|
388
|
-
apply;
|
|
389
|
-
kind = "interaction";
|
|
390
|
-
requestType;
|
|
391
|
-
responseType;
|
|
392
|
-
constructor(id, persist, requestSchema, responseSchema, request, apply) {
|
|
393
|
-
super(id, persist);
|
|
394
|
-
this.requestSchema = requestSchema;
|
|
395
|
-
this.responseSchema = responseSchema;
|
|
396
|
-
this.request = request;
|
|
397
|
-
this.apply = apply;
|
|
398
|
-
}
|
|
337
|
+
parseDefinition(collectionOptionsSchema, definition, `Collection '${definition.id}'`);
|
|
338
|
+
const record = {
|
|
339
|
+
...definition,
|
|
340
|
+
[participantBrand]: keepState,
|
|
341
|
+
kind: "collection",
|
|
342
|
+
};
|
|
343
|
+
return Object.freeze(record);
|
|
399
344
|
}
|
|
400
345
|
export function interaction(definition) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
handle(interaction, handler) {
|
|
408
|
-
const opaque = interaction;
|
|
409
|
-
if (this.#interactions.has(opaque)) {
|
|
410
|
-
throw new TandemError(`Interaction '${interaction.id}' already has a handler.`);
|
|
411
|
-
}
|
|
412
|
-
this.#interactions.add(opaque);
|
|
413
|
-
this.entries.push({
|
|
414
|
-
interaction: opaque,
|
|
415
|
-
handle: handler,
|
|
416
|
-
});
|
|
417
|
-
return this;
|
|
418
|
-
}
|
|
346
|
+
const record = {
|
|
347
|
+
...definition,
|
|
348
|
+
[participantBrand]: keepState,
|
|
349
|
+
kind: "interaction",
|
|
350
|
+
};
|
|
351
|
+
return Object.freeze(record);
|
|
419
352
|
}
|
|
353
|
+
const registeredHandlers = new WeakMap();
|
|
420
354
|
export function interactions() {
|
|
421
|
-
|
|
355
|
+
const entries = [];
|
|
356
|
+
const handlers = {
|
|
357
|
+
[interactionHandlersBrand]: true,
|
|
358
|
+
handle(interaction, handler) {
|
|
359
|
+
if (entries.some((entry) => entry.interaction === interaction)) {
|
|
360
|
+
throw new TandemError(`Interaction '${interaction.id}' already has a handler.`);
|
|
361
|
+
}
|
|
362
|
+
entries.push({
|
|
363
|
+
// An interaction is the record `interaction()` built; its types are erased for storage.
|
|
364
|
+
interaction: interaction,
|
|
365
|
+
handle: handler,
|
|
366
|
+
});
|
|
367
|
+
return handlers;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
registeredHandlers.set(handlers, entries);
|
|
371
|
+
return Object.freeze(handlers);
|
|
422
372
|
}
|
|
423
|
-
|
|
424
|
-
name;
|
|
425
|
-
instructions;
|
|
426
|
-
schema;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
requestJsonSchema;
|
|
431
|
-
constructor(name, instructions, schema, validateFor, apply, summarize) {
|
|
432
|
-
this.name = name;
|
|
433
|
-
this.instructions = instructions;
|
|
434
|
-
this.schema = schema;
|
|
435
|
-
this.validateFor = validateFor;
|
|
436
|
-
this.apply = apply;
|
|
437
|
-
this.summarize = summarize;
|
|
438
|
-
this.requestJsonSchema = inputJsonSchema(schema, `capability '${name}' schema`);
|
|
439
|
-
}
|
|
440
|
-
[compileCapabilityBrand]({ id, stateSchema, callbacks, }) {
|
|
441
|
-
const validate = callbacks.registerSync((_, input) => issues(this.schema, input));
|
|
442
|
-
const validateFor = this.validateFor
|
|
443
|
-
? 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`))
|
|
444
|
-
: undefined;
|
|
445
|
-
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`));
|
|
446
|
-
const summary = callbacks.registerSync((_, input) => this.summarize(parseJson(this.schema, input, `${id} capability '${this.name}' request`)));
|
|
373
|
+
export function capability(definition) {
|
|
374
|
+
const { name, instructions, schema, validateFor, apply, summarize } = definition;
|
|
375
|
+
requireInstructions(instructions, `Capability '${name}' instructions`);
|
|
376
|
+
const requestJsonSchema = inputJsonSchema(schema, `capability '${name}' schema`);
|
|
377
|
+
const compile = ({ id, stateSchema, callbacks }) => {
|
|
378
|
+
const parseState = (state) => parseJson(stateSchema, state, `${id} state`);
|
|
379
|
+
const parseRequest = (input) => parseJson(schema, input, `${id} capability '${name}' request`);
|
|
447
380
|
return {
|
|
448
|
-
name
|
|
449
|
-
instructions
|
|
450
|
-
jsonSchema:
|
|
451
|
-
validateCallback:
|
|
452
|
-
validateForCallback: validateFor
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
381
|
+
name,
|
|
382
|
+
instructions,
|
|
383
|
+
jsonSchema: requestJsonSchema,
|
|
384
|
+
validateCallback: callbacks.registerSync((_, input) => issues(schema, input)),
|
|
385
|
+
validateForCallback: validateFor
|
|
386
|
+
? callbacks.registerSync((state, input) => validationProblems(validateFor(parseState(state), parseRequest(input)), `${id} capability '${name}' contextual validation`))
|
|
387
|
+
: undefined,
|
|
388
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, apply(parseState(state), parseRequest(input)), `${id} applied state`)),
|
|
389
|
+
summaryCallback: callbacks.registerSync((_, input) => summarize(parseRequest(input))),
|
|
390
|
+
valueType: `${id}.capability.${name}`,
|
|
456
391
|
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
predicate;
|
|
465
|
-
tools;
|
|
466
|
-
[toolGroupBrand];
|
|
467
|
-
constructor(predicate, tools) {
|
|
468
|
-
this.predicate = predicate;
|
|
469
|
-
this.tools = tools;
|
|
470
|
-
this[toolGroupBrand] = predicate ?? (() => true);
|
|
471
|
-
}
|
|
392
|
+
};
|
|
393
|
+
const record = {
|
|
394
|
+
name,
|
|
395
|
+
requestJsonSchema,
|
|
396
|
+
[compileCapabilityBrand]: compile,
|
|
397
|
+
};
|
|
398
|
+
return Object.freeze(record);
|
|
472
399
|
}
|
|
400
|
+
const agentToolInvocationSchema = z.strictObject({
|
|
401
|
+
name: z.string(),
|
|
402
|
+
effect: z.enum([
|
|
403
|
+
"read",
|
|
404
|
+
"workspaceMutation",
|
|
405
|
+
"processExecution",
|
|
406
|
+
"lifecycleTransition",
|
|
407
|
+
"unclassified",
|
|
408
|
+
]),
|
|
409
|
+
arguments: z.unknown(),
|
|
410
|
+
});
|
|
473
411
|
function createToolGroup(predicate, tools) {
|
|
474
412
|
if (tools.length === 0) {
|
|
475
413
|
throw new TandemError("An agent tool group cannot be empty.");
|
|
@@ -484,264 +422,154 @@ function createToolGroup(predicate, tools) {
|
|
|
484
422
|
}
|
|
485
423
|
seen.add(tool);
|
|
486
424
|
}
|
|
487
|
-
|
|
425
|
+
const record = {
|
|
426
|
+
[toolGroupBrand]: predicate ?? (() => true),
|
|
427
|
+
predicate,
|
|
428
|
+
tools,
|
|
429
|
+
};
|
|
430
|
+
return Object.freeze(record);
|
|
488
431
|
}
|
|
489
432
|
export const agentTools = {
|
|
490
433
|
always: (...tools) => createToolGroup(undefined, tools),
|
|
491
434
|
when: (predicate, ...tools) => createToolGroup(predicate, tools),
|
|
492
435
|
};
|
|
493
|
-
class AgentWorkspaceImplementation {
|
|
494
|
-
path;
|
|
495
|
-
commands;
|
|
496
|
-
commandSource;
|
|
497
|
-
constructor(path, commandSource) {
|
|
498
|
-
this.path = path;
|
|
499
|
-
this.commandSource =
|
|
500
|
-
typeof commandSource === "function" || commandSource === undefined
|
|
501
|
-
? commandSource
|
|
502
|
-
: commandSource.map(copyAgentCommand);
|
|
503
|
-
this.commands = { [commandSelectionBrand]: this };
|
|
504
|
-
}
|
|
505
|
-
withTools(groups, options) {
|
|
506
|
-
if (groups.length === 0) {
|
|
507
|
-
throw new TandemError("An agent workspace requires tool groups.");
|
|
508
|
-
}
|
|
509
|
-
const implementations = groups.map((group) => {
|
|
510
|
-
if (!(group instanceof AgentToolGroupImplementation)) {
|
|
511
|
-
throw new TandemError("Agent tool groups must be created by agentTools.");
|
|
512
|
-
}
|
|
513
|
-
return group;
|
|
514
|
-
});
|
|
515
|
-
if (options?.interceptTool !== undefined && typeof options.interceptTool !== "function") {
|
|
516
|
-
throw new TandemError("Workspace tool interceptor must be a function.");
|
|
517
|
-
}
|
|
518
|
-
return new AgentWorkspaceConfigurationImplementation(this, implementations, options?.interceptTool);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
class AgentWorkspaceConfigurationImplementation {
|
|
522
|
-
workspace;
|
|
523
|
-
groups;
|
|
524
|
-
interceptTool;
|
|
525
|
-
[workspaceBrand];
|
|
526
|
-
constructor(workspace, groups, interceptTool) {
|
|
527
|
-
this.workspace = workspace;
|
|
528
|
-
this.groups = groups;
|
|
529
|
-
this.interceptTool = interceptTool;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
436
|
export function agentWorkspace(definition) {
|
|
533
437
|
if (typeof definition.path !== "function") {
|
|
534
438
|
throw new TandemError("Workspace path is required.");
|
|
535
439
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
: {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
function validateAgentCommands(commands, context) {
|
|
550
|
-
if (!Array.isArray(commands)) {
|
|
551
|
-
throw new TandemError(`${context} must be an array.`);
|
|
552
|
-
}
|
|
553
|
-
for (const [commandIndex, command] of commands.entries()) {
|
|
554
|
-
const commandContext = `${context}[${commandIndex}]`;
|
|
555
|
-
if (typeof command !== "object" || command === null) {
|
|
556
|
-
throw new TandemError(`${commandContext} must be a command.`);
|
|
557
|
-
}
|
|
558
|
-
const candidate = command;
|
|
559
|
-
if (typeof candidate.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(candidate.name)) {
|
|
560
|
-
throw new TandemError(`${commandContext}.name must be a valid tool name.`);
|
|
561
|
-
}
|
|
562
|
-
if (typeof candidate.description !== "string" || candidate.description.trim().length === 0) {
|
|
563
|
-
throw new TandemError(`${commandContext}.description must be non-blank.`);
|
|
564
|
-
}
|
|
565
|
-
if (typeof candidate.command !== "string" || candidate.command.trim().length === 0) {
|
|
566
|
-
throw new TandemError(`${commandContext}.command must be non-blank.`);
|
|
567
|
-
}
|
|
568
|
-
if (candidate.arguments !== undefined && !Array.isArray(candidate.arguments)) {
|
|
569
|
-
throw new TandemError(`${commandContext}.arguments must be an array of strings.`);
|
|
570
|
-
}
|
|
571
|
-
const argumentList = candidate.arguments ?? [];
|
|
572
|
-
if (argumentList.length > 16) {
|
|
573
|
-
throw new TandemError(`${commandContext}.arguments accepts at most 16 arguments.`);
|
|
574
|
-
}
|
|
575
|
-
for (const [argumentIndex, argument] of argumentList.entries()) {
|
|
576
|
-
const argumentContext = `${commandContext}.arguments[${argumentIndex}]`;
|
|
577
|
-
if (typeof argument !== "string") {
|
|
578
|
-
throw new TandemError(`${argumentContext} must be a string.`);
|
|
579
|
-
}
|
|
580
|
-
if (argument.trim().length === 0) {
|
|
581
|
-
throw new TandemError(`${argumentContext} must not be blank.`);
|
|
440
|
+
const workspace = Object.freeze({
|
|
441
|
+
path: definition.path,
|
|
442
|
+
// Parsing copies a static catalogue, so later mutation by the caller cannot change it.
|
|
443
|
+
commandSource: typeof definition.commands === "function" || definition.commands === undefined
|
|
444
|
+
? definition.commands
|
|
445
|
+
: parseDefinition(agentCommandsSchema, definition.commands, "Workspace commands"),
|
|
446
|
+
});
|
|
447
|
+
return Object.freeze({
|
|
448
|
+
commands: Object.freeze({ [commandSelectionBrand]: workspace }),
|
|
449
|
+
withTools(groups, options) {
|
|
450
|
+
if (groups.length === 0) {
|
|
451
|
+
throw new TandemError("An agent workspace requires tool groups.");
|
|
582
452
|
}
|
|
583
|
-
|
|
584
|
-
|
|
453
|
+
const records = groups.map((group) => {
|
|
454
|
+
if (!("tools" in group)) {
|
|
455
|
+
throw new TandemError("Agent tool groups must be created by agentTools.");
|
|
456
|
+
}
|
|
457
|
+
// A tool group record is the object `agentTools` returned.
|
|
458
|
+
return group;
|
|
459
|
+
});
|
|
460
|
+
if (options?.interceptTool !== undefined && typeof options.interceptTool !== "function") {
|
|
461
|
+
throw new TandemError("Workspace tool interceptor must be a function.");
|
|
585
462
|
}
|
|
586
|
-
|
|
587
|
-
|
|
463
|
+
const configuration = {
|
|
464
|
+
[workspaceBrand]: keepState,
|
|
465
|
+
workspace,
|
|
466
|
+
groups: records,
|
|
467
|
+
interceptTool: options?.interceptTool,
|
|
468
|
+
};
|
|
469
|
+
return Object.freeze(configuration);
|
|
470
|
+
},
|
|
471
|
+
});
|
|
588
472
|
}
|
|
473
|
+
const agentCommandsSchema = z.array(z.strictObject({
|
|
474
|
+
name: z
|
|
475
|
+
.string({ error: "must be a valid tool name" })
|
|
476
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, { error: "must be a valid tool name" }),
|
|
477
|
+
description: nonBlankString,
|
|
478
|
+
command: nonBlankString,
|
|
479
|
+
arguments: z
|
|
480
|
+
.array(z
|
|
481
|
+
.string({ error: "must be a string" })
|
|
482
|
+
.refine((value) => value.trim().length > 0, { error: "must not be blank" })
|
|
483
|
+
.max(200, { error: "must be at most 200 characters" }), { error: "must be an array of strings" })
|
|
484
|
+
.max(16, { error: "accepts at most 16 arguments" })
|
|
485
|
+
.optional(),
|
|
486
|
+
}), { error: "must be an array" });
|
|
487
|
+
const skillSchema = z.object({ directory: nonBlankString });
|
|
589
488
|
export function skill(definition) {
|
|
590
|
-
|
|
591
|
-
throw new TandemError("Skill directory must be a non-blank string.");
|
|
592
|
-
}
|
|
593
|
-
return { directory: definition.directory };
|
|
594
|
-
}
|
|
595
|
-
class AgentImplementation extends NodeImplementation {
|
|
596
|
-
instructions;
|
|
597
|
-
client;
|
|
598
|
-
message;
|
|
599
|
-
output;
|
|
600
|
-
granted;
|
|
601
|
-
skills;
|
|
602
|
-
workspace;
|
|
603
|
-
temperature;
|
|
604
|
-
maxOutputTokens;
|
|
605
|
-
reasoning;
|
|
606
|
-
continueSession;
|
|
607
|
-
checkpoint;
|
|
608
|
-
timeoutMs;
|
|
609
|
-
kind = "agent";
|
|
610
|
-
constructor(id, persist, instructions, client, message, output, granted, skills, workspace, temperature, maxOutputTokens, reasoning, continueSession, checkpoint, timeoutMs) {
|
|
611
|
-
super(id, persist);
|
|
612
|
-
this.instructions = instructions;
|
|
613
|
-
this.client = client;
|
|
614
|
-
this.message = message;
|
|
615
|
-
this.output = output;
|
|
616
|
-
this.granted = granted;
|
|
617
|
-
this.skills = skills;
|
|
618
|
-
this.workspace = workspace;
|
|
619
|
-
this.temperature = temperature;
|
|
620
|
-
this.maxOutputTokens = maxOutputTokens;
|
|
621
|
-
this.reasoning = reasoning;
|
|
622
|
-
this.continueSession = continueSession;
|
|
623
|
-
this.checkpoint = checkpoint;
|
|
624
|
-
this.timeoutMs = timeoutMs;
|
|
625
|
-
}
|
|
489
|
+
return parseDefinition(skillSchema, definition, "Skill");
|
|
626
490
|
}
|
|
491
|
+
const capabilityReference = z.custom();
|
|
492
|
+
const agentOutputSchema = z.discriminatedUnion("raw", [
|
|
493
|
+
z.object({
|
|
494
|
+
raw: z.literal(true),
|
|
495
|
+
schema: z.undefined({ error: "is forbidden for raw output" }).optional(),
|
|
496
|
+
parse: z.custom((value) => typeof value === "function", {
|
|
497
|
+
error: "must be a function for raw output",
|
|
498
|
+
}),
|
|
499
|
+
}),
|
|
500
|
+
z.object({
|
|
501
|
+
raw: z.undefined().optional(),
|
|
502
|
+
parse: z.undefined({ error: "requires raw output mode" }).optional(),
|
|
503
|
+
}),
|
|
504
|
+
], { error: "must be true" });
|
|
505
|
+
const agentOptionsSchema = z
|
|
506
|
+
.object({
|
|
507
|
+
output: agentOutputSchema.optional(),
|
|
508
|
+
reasoning: z
|
|
509
|
+
.object({
|
|
510
|
+
effort: z
|
|
511
|
+
.enum(["none", "low", "medium", "high"], {
|
|
512
|
+
error: "must be 'none', 'low', 'medium' or 'high'",
|
|
513
|
+
})
|
|
514
|
+
.optional(),
|
|
515
|
+
maxTokens: z
|
|
516
|
+
.int32({ error: "must be a 32-bit integer of at least 1024" })
|
|
517
|
+
.min(1024)
|
|
518
|
+
.optional(),
|
|
519
|
+
})
|
|
520
|
+
.refine((value) => (value.effort === undefined) !== (value.maxTokens === undefined), {
|
|
521
|
+
error: "must specify exactly one of effort or maxTokens",
|
|
522
|
+
})
|
|
523
|
+
.optional(),
|
|
524
|
+
capabilities: z
|
|
525
|
+
.array(capabilityReference)
|
|
526
|
+
.refine((items) => hasUnique(items, (item) => item.name), {
|
|
527
|
+
error: "must not contain a duplicate capability",
|
|
528
|
+
})
|
|
529
|
+
.optional(),
|
|
530
|
+
skills: z
|
|
531
|
+
.array(skillSchema)
|
|
532
|
+
.refine((items) => hasUnique(items, (item) => item.directory), {
|
|
533
|
+
error: "must not repeat a skill directory",
|
|
534
|
+
})
|
|
535
|
+
.optional(),
|
|
536
|
+
temperature: z.number({ error: "must be between 0 and 2" }).min(0).max(2).optional(),
|
|
537
|
+
maxOutputTokens: positiveInt32.optional(),
|
|
538
|
+
checkpoint: z
|
|
539
|
+
.object({
|
|
540
|
+
contextWindowTokens: positiveInt32,
|
|
541
|
+
maxOutputTokens: positiveInt32,
|
|
542
|
+
checkpointAtPercent: z.int({ error: "must be between 1 and 99" }).min(1).max(99),
|
|
543
|
+
capability: capabilityReference,
|
|
544
|
+
session: z.enum(["retain", "reset"], { error: "must be 'retain' or 'reset'" }).optional(),
|
|
545
|
+
disableCompaction: z.boolean({ error: "must be a boolean" }).optional(),
|
|
546
|
+
})
|
|
547
|
+
.refine((value) => value.maxOutputTokens < value.contextWindowTokens, {
|
|
548
|
+
path: ["maxOutputTokens"],
|
|
549
|
+
error: "must be smaller than contextWindowTokens",
|
|
550
|
+
})
|
|
551
|
+
.optional(),
|
|
552
|
+
})
|
|
553
|
+
.refine((value) => value.checkpoint === undefined ||
|
|
554
|
+
(value.capabilities ?? []).includes(value.checkpoint.capability), { path: ["checkpoint", "capability"], error: "must be attached to the agent" });
|
|
627
555
|
export function agent(definition) {
|
|
628
556
|
const rawOutput = definition.output && "raw" in definition.output && definition.output.raw === true;
|
|
629
557
|
if (!rawOutput || typeof definition.instructions !== "string") {
|
|
630
558
|
requireInstructions(definition.instructions, `Agent '${definition.id}' instructions`);
|
|
631
559
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (definition.reasoning &&
|
|
635
|
-
(reasoningEffort !== undefined) === (reasoningMaxTokens !== undefined)) {
|
|
636
|
-
throw new TandemError(`Agent '${definition.id}' reasoning must specify exactly one of effort or maxTokens.`);
|
|
637
|
-
}
|
|
638
|
-
if (reasoningEffort !== undefined &&
|
|
639
|
-
!["none", "low", "medium", "high"].includes(reasoningEffort)) {
|
|
640
|
-
throw new TandemError(`Agent '${definition.id}' has an invalid reasoning effort.`);
|
|
641
|
-
}
|
|
642
|
-
if (reasoningMaxTokens !== undefined) {
|
|
643
|
-
if (!Number.isSafeInteger(reasoningMaxTokens) ||
|
|
644
|
-
reasoningMaxTokens < 1024 ||
|
|
645
|
-
reasoningMaxTokens > 2_147_483_647) {
|
|
646
|
-
throw new TandemError(`Agent '${definition.id}' reasoning maxTokens must be a 32-bit integer of at least 1024.`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (definition.output) {
|
|
650
|
-
if (!rawOutput || typeof definition.output.instructions !== "string") {
|
|
651
|
-
requireInstructions(definition.output.instructions, `Agent '${definition.id}' output instructions`);
|
|
652
|
-
}
|
|
653
|
-
if ("raw" in definition.output) {
|
|
654
|
-
if (definition.output.raw !== true) {
|
|
655
|
-
throw new TandemError(`Agent '${definition.id}' output raw must be true.`);
|
|
656
|
-
}
|
|
657
|
-
if ("schema" in definition.output) {
|
|
658
|
-
throw new TandemError(`Agent '${definition.id}' output schema is forbidden for raw output.`);
|
|
659
|
-
}
|
|
660
|
-
if (typeof definition.output.parse !== "function") {
|
|
661
|
-
throw new TandemError(`Agent '${definition.id}' raw output requires a parse function.`);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
else if ("parse" in definition.output) {
|
|
665
|
-
throw new TandemError(`Agent '${definition.id}' output parse requires raw output mode.`);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
const capabilities = definition.capabilities ?? [];
|
|
669
|
-
const names = new Set();
|
|
670
|
-
for (const item of capabilities) {
|
|
671
|
-
if (names.has(item.name)) {
|
|
672
|
-
throw new TandemError(`Agent '${definition.id}' has duplicate capability '${item.name}'.`);
|
|
673
|
-
}
|
|
674
|
-
else {
|
|
675
|
-
names.add(item.name);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
const skills = definition.skills ?? [];
|
|
679
|
-
const skillDirectories = new Set();
|
|
680
|
-
for (const item of skills) {
|
|
681
|
-
if (typeof item?.directory !== "string" || item.directory.trim().length === 0) {
|
|
682
|
-
throw new TandemError(`Agent '${definition.id}' has a skill with an invalid directory.`);
|
|
683
|
-
}
|
|
684
|
-
if (!skillDirectories.add(item.directory)) {
|
|
685
|
-
throw new TandemError(`Agent '${definition.id}' has the skill directory '${item.directory}' more than once.`);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (definition.temperature !== undefined &&
|
|
689
|
-
(!Number.isFinite(definition.temperature) ||
|
|
690
|
-
definition.temperature < 0 ||
|
|
691
|
-
definition.temperature > 2)) {
|
|
692
|
-
throw new TandemError(`Agent '${definition.id}' temperature must be between 0 and 2.`);
|
|
693
|
-
}
|
|
694
|
-
if (definition.maxOutputTokens !== undefined &&
|
|
695
|
-
(!Number.isSafeInteger(definition.maxOutputTokens) ||
|
|
696
|
-
definition.maxOutputTokens <= 0 ||
|
|
697
|
-
definition.maxOutputTokens > 2_147_483_647)) {
|
|
698
|
-
throw new TandemError(`Agent '${definition.id}' maxOutputTokens must be a positive 32-bit integer.`);
|
|
560
|
+
if (definition.output && (!rawOutput || typeof definition.output.instructions !== "string")) {
|
|
561
|
+
requireInstructions(definition.output.instructions, `Agent '${definition.id}' output instructions`);
|
|
699
562
|
}
|
|
700
563
|
if (definition.checkpoint) {
|
|
701
|
-
|
|
702
|
-
if (!Number.isSafeInteger(checkpoint.contextWindowTokens) ||
|
|
703
|
-
checkpoint.contextWindowTokens <= 0 ||
|
|
704
|
-
checkpoint.contextWindowTokens > 2_147_483_647) {
|
|
705
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint contextWindowTokens must be a positive 32-bit integer.`);
|
|
706
|
-
}
|
|
707
|
-
if (!Number.isSafeInteger(checkpoint.maxOutputTokens) ||
|
|
708
|
-
checkpoint.maxOutputTokens <= 0 ||
|
|
709
|
-
checkpoint.maxOutputTokens > 2_147_483_647 ||
|
|
710
|
-
checkpoint.maxOutputTokens >= checkpoint.contextWindowTokens) {
|
|
711
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint maxOutputTokens must be a positive 32-bit integer smaller than contextWindowTokens.`);
|
|
712
|
-
}
|
|
713
|
-
if (!Number.isSafeInteger(checkpoint.checkpointAtPercent) ||
|
|
714
|
-
checkpoint.checkpointAtPercent <= 0 ||
|
|
715
|
-
checkpoint.checkpointAtPercent >= 100) {
|
|
716
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint checkpointAtPercent must be between 1 and 99.`);
|
|
717
|
-
}
|
|
718
|
-
if (!capabilities.includes(checkpoint.capability)) {
|
|
719
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint capability must be attached to the agent.`);
|
|
720
|
-
}
|
|
721
|
-
requireInstructions(checkpoint.instructions, `Agent '${definition.id}' checkpoint instructions`);
|
|
722
|
-
if (checkpoint.session !== undefined &&
|
|
723
|
-
checkpoint.session !== "retain" &&
|
|
724
|
-
checkpoint.session !== "reset") {
|
|
725
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint session must be 'retain' or 'reset'.`);
|
|
726
|
-
}
|
|
727
|
-
if (checkpoint.disableCompaction !== undefined &&
|
|
728
|
-
typeof checkpoint.disableCompaction !== "boolean") {
|
|
729
|
-
throw new TandemError(`Agent '${definition.id}' checkpoint disableCompaction must be a boolean.`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
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);
|
|
733
|
-
}
|
|
734
|
-
class ParallelImplementation extends NodeImplementation {
|
|
735
|
-
branches;
|
|
736
|
-
max;
|
|
737
|
-
merge;
|
|
738
|
-
kind = "parallel";
|
|
739
|
-
constructor(id, persist, branches, max, merge) {
|
|
740
|
-
super(id, persist);
|
|
741
|
-
this.branches = branches;
|
|
742
|
-
this.max = max;
|
|
743
|
-
this.merge = merge;
|
|
564
|
+
requireInstructions(definition.checkpoint.instructions, `Agent '${definition.id}' checkpoint instructions`);
|
|
744
565
|
}
|
|
566
|
+
parseDefinition(agentOptionsSchema, definition, `Agent '${definition.id}'`);
|
|
567
|
+
const record = {
|
|
568
|
+
...definition,
|
|
569
|
+
[participantBrand]: keepState,
|
|
570
|
+
kind: "agent",
|
|
571
|
+
};
|
|
572
|
+
return Object.freeze(record);
|
|
745
573
|
}
|
|
746
574
|
export function parallel(definition) {
|
|
747
575
|
if (!definition) {
|
|
@@ -749,33 +577,43 @@ export function parallel(definition) {
|
|
|
749
577
|
}
|
|
750
578
|
return createParallel(definition);
|
|
751
579
|
}
|
|
580
|
+
const parallelOptionsSchema = z.object({
|
|
581
|
+
max: positiveInt32.optional(),
|
|
582
|
+
branches: z
|
|
583
|
+
.record(z.string(), z.custom())
|
|
584
|
+
.refine((branches) => Object.keys(branches).every((id) => id.trim().length > 0), {
|
|
585
|
+
error: "must not have a blank branch ID",
|
|
586
|
+
})
|
|
587
|
+
.refine((branches) => Object.keys(branches).length >= 2, {
|
|
588
|
+
error: "requires at least two branches",
|
|
589
|
+
})
|
|
590
|
+
.refine((branches) => hasUnique(Object.values(branches), (participant) => participant), {
|
|
591
|
+
error: "must own a distinct participant per branch",
|
|
592
|
+
}),
|
|
593
|
+
});
|
|
752
594
|
function createParallel(definition) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
const participants = new Set(entries.map(([, participant]) => participant));
|
|
762
|
-
if (participants.size !== entries.length) {
|
|
763
|
-
throw new TandemError(`Parallel group '${definition.id}' must own a distinct participant per branch.`);
|
|
764
|
-
}
|
|
765
|
-
return new ParallelImplementation(definition.id, definition.persist, definition.branches, definition.max, definition.merge);
|
|
766
|
-
}
|
|
767
|
-
class TerminalImplementation extends NodeImplementation {
|
|
768
|
-
failed;
|
|
769
|
-
summary;
|
|
770
|
-
kind = "terminal";
|
|
771
|
-
constructor(id, persist, failed, summary) {
|
|
772
|
-
super(id, persist);
|
|
773
|
-
this.failed = failed;
|
|
774
|
-
this.summary = summary;
|
|
775
|
-
}
|
|
595
|
+
parseDefinition(parallelOptionsSchema, definition, `Parallel group '${definition.id}'`);
|
|
596
|
+
const record = {
|
|
597
|
+
...definition,
|
|
598
|
+
[participantBrand]: keepState,
|
|
599
|
+
kind: "parallel",
|
|
600
|
+
};
|
|
601
|
+
return Object.freeze(record);
|
|
776
602
|
}
|
|
777
603
|
export function output(definition) {
|
|
778
|
-
|
|
604
|
+
const record = {
|
|
605
|
+
...definition,
|
|
606
|
+
[participantBrand]: keepState,
|
|
607
|
+
kind: "terminal",
|
|
608
|
+
};
|
|
609
|
+
return Object.freeze(record);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Every node is the frozen record its factory built; the exported node interfaces are opaque views
|
|
613
|
+
* of those records, with the request/output/item types erased here.
|
|
614
|
+
*/
|
|
615
|
+
function nodeRecord(node) {
|
|
616
|
+
return node;
|
|
779
617
|
}
|
|
780
618
|
export function route(definition) {
|
|
781
619
|
return definition;
|
|
@@ -784,43 +622,39 @@ export function pipeline(definition) {
|
|
|
784
622
|
const start = definition.start;
|
|
785
623
|
const members = new Set(definition.nodes);
|
|
786
624
|
if (members.size !== definition.nodes.length) {
|
|
787
|
-
throw new
|
|
625
|
+
throw new TandemError("Pipeline nodes must contain each participant object exactly once.");
|
|
788
626
|
}
|
|
789
627
|
const ids = new Set(definition.nodes.map((node) => node.id));
|
|
790
628
|
if (ids.size !== definition.nodes.length) {
|
|
791
|
-
throw new
|
|
629
|
+
throw new TandemError("Pipeline node IDs must be unique.");
|
|
792
630
|
}
|
|
793
631
|
const ownedParticipants = new Set();
|
|
794
|
-
for (const node of definition.nodes) {
|
|
632
|
+
for (const node of definition.nodes.map(nodeRecord)) {
|
|
795
633
|
if (node.kind !== "parallel") {
|
|
796
634
|
continue;
|
|
797
635
|
}
|
|
798
|
-
const
|
|
799
|
-
for (const [branchId, participant] of Object.entries(parallelNode.branches)) {
|
|
800
|
-
if (branchId.trim().length === 0) {
|
|
801
|
-
throw new Error(`Parallel group '${node.id}' contains a blank branch ID.`);
|
|
802
|
-
}
|
|
636
|
+
for (const participant of Object.values(node.branches)) {
|
|
803
637
|
if (members.has(participant)) {
|
|
804
|
-
throw new
|
|
638
|
+
throw new TandemError(`Parallel branch participant '${participant.id}' cannot also be a parent pipeline node.`);
|
|
805
639
|
}
|
|
806
640
|
if (!ownedParticipants.add(participant)) {
|
|
807
|
-
throw new
|
|
641
|
+
throw new TandemError(`Parallel branch participant '${participant.id}' is owned more than once.`);
|
|
808
642
|
}
|
|
809
643
|
if (ids.has(participant.id)) {
|
|
810
|
-
throw new
|
|
644
|
+
throw new TandemError(`Pipeline participant ID '${participant.id}' must be globally unique.`);
|
|
811
645
|
}
|
|
812
646
|
ids.add(participant.id);
|
|
813
647
|
}
|
|
814
648
|
}
|
|
815
649
|
if (!members.has(definition.start)) {
|
|
816
|
-
throw new
|
|
650
|
+
throw new TandemError(`Pipeline start '${definition.start.id}' must be the registered participant object.`);
|
|
817
651
|
}
|
|
818
652
|
if (start.kind === "terminal") {
|
|
819
|
-
throw new
|
|
653
|
+
throw new TandemError(`Pipeline start '${start.id}' cannot be a terminal.`);
|
|
820
654
|
}
|
|
821
655
|
for (const item of definition.routes) {
|
|
822
656
|
if (!members.has(item.from) || !members.has(item.to)) {
|
|
823
|
-
throw new
|
|
657
|
+
throw new TandemError(`Route '${item.label}' endpoints must be registered participant objects.`);
|
|
824
658
|
}
|
|
825
659
|
}
|
|
826
660
|
const unconditionalRoutes = new Map();
|
|
@@ -831,22 +665,21 @@ export function pipeline(definition) {
|
|
|
831
665
|
const key = `${item.from.id}\u0000${item.outcome ?? "default"}`;
|
|
832
666
|
const existing = unconditionalRoutes.get(key);
|
|
833
667
|
if (existing) {
|
|
834
|
-
throw new
|
|
668
|
+
throw new TandemError(`Routes '${existing.label}' and '${item.label}' are both unconditional from '${item.from.id}'.`);
|
|
835
669
|
}
|
|
836
670
|
unconditionalRoutes.set(key, item);
|
|
837
671
|
}
|
|
838
672
|
if (new Set(definition.outputs).size !== definition.outputs.length) {
|
|
839
|
-
throw new
|
|
673
|
+
throw new TandemError("Pipeline outputs must contain each terminal exactly once.");
|
|
840
674
|
}
|
|
841
675
|
for (const item of definition.outputs) {
|
|
842
676
|
if (!members.has(item)) {
|
|
843
|
-
throw new
|
|
677
|
+
throw new TandemError(`Output '${item.id}' must be the registered participant object.`);
|
|
844
678
|
}
|
|
845
679
|
}
|
|
846
680
|
const reachable = new Set([start]);
|
|
847
681
|
const pending = [start];
|
|
848
|
-
|
|
849
|
-
const source = pending.pop();
|
|
682
|
+
for (let source = pending.pop(); source !== undefined; source = pending.pop()) {
|
|
850
683
|
for (const item of definition.routes) {
|
|
851
684
|
if (item.from === source && !reachable.has(item.to)) {
|
|
852
685
|
reachable.add(item.to);
|
|
@@ -857,12 +690,12 @@ export function pipeline(definition) {
|
|
|
857
690
|
const outputs = new Set(definition.outputs);
|
|
858
691
|
for (const node of reachable) {
|
|
859
692
|
if (node.kind === "terminal" && !outputs.has(node)) {
|
|
860
|
-
throw new
|
|
693
|
+
throw new TandemError(`Reachable terminal '${node.id}' must be listed in outputs.`);
|
|
861
694
|
}
|
|
862
695
|
}
|
|
863
696
|
for (const item of definition.outputs) {
|
|
864
697
|
if (!reachable.has(item)) {
|
|
865
|
-
throw new
|
|
698
|
+
throw new TandemError(`Output '${item.id}' must be reachable from start '${start.id}'.`);
|
|
866
699
|
}
|
|
867
700
|
}
|
|
868
701
|
return { ...definition, persist: definition.persist ?? false };
|
|
@@ -870,65 +703,61 @@ export function pipeline(definition) {
|
|
|
870
703
|
/** Projects an instantiated pipeline without compiling, registering, or invoking callbacks. */
|
|
871
704
|
export function inspectPipeline(graph) {
|
|
872
705
|
const inspectNode = (node) => {
|
|
873
|
-
const
|
|
874
|
-
const persist =
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
id:
|
|
878
|
-
|
|
879
|
-
persist
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
if (node.kind === "interaction") {
|
|
885
|
-
const interaction = implementation;
|
|
886
|
-
return {
|
|
887
|
-
id: node.id,
|
|
888
|
-
kind: "interaction",
|
|
889
|
-
persist,
|
|
890
|
-
interaction: {
|
|
891
|
-
requestSchema: z.toJSONSchema(interaction.requestSchema, { io: "input" }),
|
|
892
|
-
responseSchema: z.toJSONSchema(interaction.responseSchema, { io: "input" }),
|
|
893
|
-
},
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
if (node.kind === "agent") {
|
|
897
|
-
const agent = implementation;
|
|
898
|
-
return {
|
|
899
|
-
id: node.id,
|
|
900
|
-
kind: "agent",
|
|
901
|
-
persist,
|
|
902
|
-
agent: {
|
|
903
|
-
capabilities: agent.granted.map((item) => ({
|
|
904
|
-
name: item.name,
|
|
905
|
-
requestSchema: JSON.parse(item.requestJsonSchema),
|
|
906
|
-
})),
|
|
907
|
-
...(!agent.output || "raw" in agent.output
|
|
908
|
-
? {}
|
|
909
|
-
: { outputSchema: z.toJSONSchema(agent.output.schema, { io: "input" }) }),
|
|
910
|
-
workspace: agent.workspace !== undefined,
|
|
911
|
-
},
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
if (node.kind === "parallel") {
|
|
915
|
-
const parallel = implementation;
|
|
916
|
-
return {
|
|
917
|
-
id: node.id,
|
|
918
|
-
kind: "parallel",
|
|
919
|
-
persist,
|
|
920
|
-
...(parallel.max !== undefined ? { max: parallel.max } : {}),
|
|
921
|
-
branches: Object.entries(parallel.branches).map(([id, participant]) => ({
|
|
706
|
+
const record = nodeRecord(node);
|
|
707
|
+
const { id, persist } = record;
|
|
708
|
+
switch (record.kind) {
|
|
709
|
+
case "stage":
|
|
710
|
+
return { id, kind: "stage", persist };
|
|
711
|
+
case "terminal":
|
|
712
|
+
return { id, kind: record.failed ? "failure" : "completion", persist };
|
|
713
|
+
case "collection":
|
|
714
|
+
return {
|
|
922
715
|
id,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
716
|
+
kind: "collection",
|
|
717
|
+
persist,
|
|
718
|
+
max: record.max,
|
|
719
|
+
agents: record.agents.map((agent) => `${id}/${agent.id}`),
|
|
720
|
+
};
|
|
721
|
+
case "interaction":
|
|
722
|
+
return {
|
|
723
|
+
id,
|
|
724
|
+
kind: "interaction",
|
|
725
|
+
persist,
|
|
726
|
+
interaction: {
|
|
727
|
+
requestSchema: z.toJSONSchema(record.requestSchema, { io: "input" }),
|
|
728
|
+
responseSchema: z.toJSONSchema(record.responseSchema, { io: "input" }),
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
case "agent":
|
|
732
|
+
return {
|
|
733
|
+
id,
|
|
734
|
+
kind: "agent",
|
|
735
|
+
persist,
|
|
736
|
+
agent: {
|
|
737
|
+
capabilities: (record.capabilities ?? []).map((item) => ({
|
|
738
|
+
name: item.name,
|
|
739
|
+
requestSchema: JSON.parse(item.requestJsonSchema),
|
|
740
|
+
})),
|
|
741
|
+
...(!record.output || "raw" in record.output
|
|
742
|
+
? {}
|
|
743
|
+
: { outputSchema: z.toJSONSchema(record.output.schema, { io: "input" }) }),
|
|
744
|
+
workspace: record.workspace !== undefined,
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
case "parallel":
|
|
748
|
+
return {
|
|
749
|
+
id,
|
|
750
|
+
kind: "parallel",
|
|
751
|
+
persist,
|
|
752
|
+
...(record.max !== undefined ? { max: record.max } : {}),
|
|
753
|
+
branches: Object.entries(record.branches).map(([branchId, participant]) => ({
|
|
754
|
+
id: branchId,
|
|
755
|
+
participant: inspectNode(participant),
|
|
756
|
+
})),
|
|
757
|
+
};
|
|
758
|
+
default:
|
|
759
|
+
return record;
|
|
930
760
|
}
|
|
931
|
-
return { id: node.id, kind: "stage", persist };
|
|
932
761
|
};
|
|
933
762
|
return {
|
|
934
763
|
name: graph.name,
|
|
@@ -948,112 +777,59 @@ export function inspectPipeline(graph) {
|
|
|
948
777
|
outputs: graph.outputs.map((item) => item.id),
|
|
949
778
|
};
|
|
950
779
|
}
|
|
951
|
-
const
|
|
952
|
-
z
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
.strict(),
|
|
976
|
-
z
|
|
977
|
-
.object({
|
|
978
|
-
version: z.literal(1),
|
|
979
|
-
kind: z.literal("stepFaulted"),
|
|
980
|
-
stepId: z.string().min(1),
|
|
981
|
-
visitId: z.string().nullable().optional(),
|
|
982
|
-
error: z.string(),
|
|
983
|
-
})
|
|
984
|
-
.strict(),
|
|
985
|
-
z
|
|
986
|
-
.object({
|
|
987
|
-
version: z.literal(1),
|
|
988
|
-
kind: z.literal("agentText"),
|
|
989
|
-
stepId: z.string().min(1),
|
|
990
|
-
visitId: z.string().nullable().optional(),
|
|
991
|
-
text: z.string(),
|
|
992
|
-
})
|
|
993
|
-
.strict(),
|
|
994
|
-
z
|
|
995
|
-
.object({
|
|
996
|
-
version: z.literal(1),
|
|
997
|
-
kind: z.literal("agentModelSelected"),
|
|
998
|
-
stepId: z.string().min(1),
|
|
999
|
-
visitId: z.string().nullable().optional(),
|
|
1000
|
-
modelId: z.string().min(1),
|
|
1001
|
-
})
|
|
1002
|
-
.strict(),
|
|
1003
|
-
z
|
|
1004
|
-
.object({
|
|
1005
|
-
version: z.literal(1),
|
|
1006
|
-
kind: z.literal("agentReasoning"),
|
|
1007
|
-
stepId: z.string().min(1),
|
|
1008
|
-
visitId: z.string().nullable().optional(),
|
|
1009
|
-
text: z.string(),
|
|
1010
|
-
})
|
|
1011
|
-
.strict(),
|
|
1012
|
-
z
|
|
1013
|
-
.object({
|
|
1014
|
-
version: z.literal(1),
|
|
1015
|
-
kind: z.literal("agentUsage"),
|
|
1016
|
-
stepId: z.string().min(1),
|
|
1017
|
-
visitId: z.string().nullable().optional(),
|
|
1018
|
-
inputTokens: z.number().int().nonnegative(),
|
|
1019
|
-
outputTokens: z.number().int().nonnegative(),
|
|
1020
|
-
reasoningTokens: z.number().int().nonnegative(),
|
|
1021
|
-
currentContextTokens: z.number().int().nonnegative(),
|
|
1022
|
-
contextWindowTokens: z.number().int().nonnegative().nullable(),
|
|
1023
|
-
})
|
|
1024
|
-
.strict(),
|
|
1025
|
-
z
|
|
1026
|
-
.object({
|
|
1027
|
-
version: z.literal(1),
|
|
1028
|
-
kind: z.literal("structuredOutputRejected"),
|
|
1029
|
-
stepId: z.string().min(1),
|
|
1030
|
-
visitId: z.string().nullable().optional(),
|
|
780
|
+
const observationBase = z.object({
|
|
781
|
+
version: z.literal(1),
|
|
782
|
+
stepId: z.string().min(1),
|
|
783
|
+
visitId: z.string().nullable().optional(),
|
|
784
|
+
});
|
|
785
|
+
const observation = (kind, shape) => observationBase.extend({ kind: z.literal(kind), ...shape }).strict();
|
|
786
|
+
const tokenCount = z.number().int().nonnegative();
|
|
787
|
+
const runObservationSchema = z
|
|
788
|
+
.discriminatedUnion("kind", [
|
|
789
|
+
observation("stepStarted", {}),
|
|
790
|
+
observation("stepCompleted", {}),
|
|
791
|
+
observation("stepCancelled", {}),
|
|
792
|
+
observation("stepFaulted", { error: z.string() }),
|
|
793
|
+
observation("agentText", { text: z.string() }),
|
|
794
|
+
observation("agentReasoning", { text: z.string() }),
|
|
795
|
+
observation("agentModelSelected", { modelId: z.string().min(1) }),
|
|
796
|
+
observation("agentUsage", {
|
|
797
|
+
inputTokens: tokenCount,
|
|
798
|
+
outputTokens: tokenCount,
|
|
799
|
+
reasoningTokens: tokenCount,
|
|
800
|
+
currentContextTokens: tokenCount,
|
|
801
|
+
contextWindowTokens: tokenCount.nullable(),
|
|
802
|
+
}),
|
|
803
|
+
observation("structuredOutputRejected", {
|
|
1031
804
|
attempt: z.number().int().positive(),
|
|
1032
|
-
problems: z
|
|
805
|
+
problems: z
|
|
806
|
+
.array(z
|
|
807
|
+
.object({ field: z.string(), message: z.string().min(1) })
|
|
808
|
+
.strict()
|
|
809
|
+
.readonly())
|
|
810
|
+
.readonly(),
|
|
1033
811
|
rawResponse: z.string(),
|
|
1034
|
-
})
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
"StructuredOutputAccepted",
|
|
1039
|
-
"CapabilityAccepted",
|
|
1040
|
-
"InteractionRequested",
|
|
1041
|
-
"InteractionAnswered",
|
|
1042
|
-
"StepCompleted",
|
|
1043
|
-
];
|
|
1044
|
-
const acceptedValueSchema = z
|
|
812
|
+
}),
|
|
813
|
+
])
|
|
814
|
+
.readonly();
|
|
815
|
+
const acceptedValueFields = z
|
|
1045
816
|
.object({
|
|
1046
|
-
kind: z.enum(
|
|
1047
|
-
|
|
817
|
+
kind: z.enum([
|
|
818
|
+
"StructuredOutputAccepted",
|
|
819
|
+
"CapabilityAccepted",
|
|
820
|
+
"InteractionRequested",
|
|
821
|
+
"InteractionAnswered",
|
|
822
|
+
"StepCompleted",
|
|
823
|
+
]),
|
|
1048
824
|
stepId: z.string().min(1),
|
|
825
|
+
visitId: z.string().nullable().optional(),
|
|
1049
826
|
valueType: z.string().min(1).nullable(),
|
|
1050
|
-
payload: z.unknown()
|
|
827
|
+
payload: z.unknown(),
|
|
1051
828
|
})
|
|
1052
|
-
.strict()
|
|
1053
|
-
|
|
829
|
+
.strict();
|
|
830
|
+
const acceptedValuesSchema = z.array(acceptedValueFields.refine((value) => value.valueType !== null || value.payload !== null, {
|
|
1054
831
|
message: "valueType and payload cannot both be null",
|
|
1055
|
-
});
|
|
1056
|
-
const acceptedValuesSchema = z.array(acceptedValueSchema);
|
|
832
|
+
}));
|
|
1057
833
|
const runResultSchema = z
|
|
1058
834
|
.object({
|
|
1059
835
|
runId: z.uuid(),
|
|
@@ -1062,9 +838,11 @@ const runResultSchema = z
|
|
|
1062
838
|
summary: z.string().nullable(),
|
|
1063
839
|
})
|
|
1064
840
|
.strict();
|
|
841
|
+
/** Lazy (and cached by the module loader) so importing the authoring API never starts .NET. */
|
|
842
|
+
const loadRuntime = () => import("./runtime/loader.mjs");
|
|
1065
843
|
export async function inspectAccepted(options) {
|
|
1066
844
|
try {
|
|
1067
|
-
const { inspectAcceptedAsync } = await
|
|
845
|
+
const { inspectAcceptedAsync } = await loadRuntime();
|
|
1068
846
|
return parseJson(acceptedValuesSchema, await inspectAcceptedAsync(options.ledgerPath, options.runId), "accepted values").map((value) => ({
|
|
1069
847
|
version: 1,
|
|
1070
848
|
kind: value.kind,
|
|
@@ -1089,33 +867,31 @@ export async function run(graph, initial, options = {}) {
|
|
|
1089
867
|
const callbacks = new CallbackRegistry();
|
|
1090
868
|
try {
|
|
1091
869
|
const nodes = graph.nodes.map((node) => compileNode(node, graph.state, callbacks));
|
|
1092
|
-
const routes = graph.routes.map((
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
predicateCallback: callback,
|
|
1102
|
-
};
|
|
1103
|
-
});
|
|
870
|
+
const routes = graph.routes.map(({ from, to, label, outcome, when }) => ({
|
|
871
|
+
source: from.id,
|
|
872
|
+
target: to.id,
|
|
873
|
+
label,
|
|
874
|
+
outcome,
|
|
875
|
+
predicateCallback: when
|
|
876
|
+
? callbacks.registerSync((state) => String(when(parseJson(graph.state, state, `route '${label}' state`))))
|
|
877
|
+
: undefined,
|
|
878
|
+
}));
|
|
1104
879
|
const handlerEntries = interactionHandlerEntries(options.interactions);
|
|
1105
880
|
const members = new Set(graph.nodes);
|
|
1106
881
|
const interactionHandlers = handlerEntries.map((entry, index) => {
|
|
1107
882
|
if (!members.has(entry.interaction)) {
|
|
1108
883
|
throw new TandemError(`Interaction handler '${entry.interaction.id}' must target a participant in pipeline '${graph.name}'.`);
|
|
1109
884
|
}
|
|
1110
|
-
const
|
|
885
|
+
const { id, requestSchema, responseSchema } = entry.interaction;
|
|
1111
886
|
const handleCallback = callbacks.registerAsync(async (_, input, signal) => {
|
|
1112
|
-
const response = await entry.handle(parseJson(
|
|
887
|
+
const response = await entry.handle(parseJson(requestSchema, input, `${id} request input`), { signal });
|
|
1113
888
|
signal.throwIfAborted();
|
|
1114
|
-
return serializeBoundary(
|
|
889
|
+
return serializeBoundary(responseSchema, response, `${id} response`);
|
|
1115
890
|
});
|
|
1116
891
|
return { id: `h${index}`, target: entry.interaction.id, handleCallback };
|
|
1117
892
|
});
|
|
1118
|
-
const
|
|
893
|
+
const { observe } = options;
|
|
894
|
+
const observationCallback = observe
|
|
1119
895
|
? callbacks.registerAsync(async (_, input, signal) => {
|
|
1120
896
|
const event = parseJson(runObservationSchema, input, "run observation");
|
|
1121
897
|
const observationSignal = options.signal?.aborted === true
|
|
@@ -1123,11 +899,11 @@ export async function run(graph, initial, options = {}) {
|
|
|
1123
899
|
: event.kind === "stepCancelled" && !signal.aborted
|
|
1124
900
|
? AbortSignal.abort()
|
|
1125
901
|
: signal;
|
|
1126
|
-
await
|
|
902
|
+
await observe(event, { signal: observationSignal });
|
|
1127
903
|
return "";
|
|
1128
904
|
})
|
|
1129
905
|
: undefined;
|
|
1130
|
-
const { runRegisteredGraphAsync } = await
|
|
906
|
+
const { runRegisteredGraphAsync } = await loadRuntime();
|
|
1131
907
|
const resultJson = await runRegisteredGraphAsync(JSON.stringify({
|
|
1132
908
|
contractVersion: 10,
|
|
1133
909
|
name: graph.name,
|
|
@@ -1143,7 +919,12 @@ export async function run(graph, initial, options = {}) {
|
|
|
1143
919
|
routes,
|
|
1144
920
|
outputs: graph.outputs.map((item) => item.id),
|
|
1145
921
|
interactionHandlers,
|
|
1146
|
-
}), (id, state, input) =>
|
|
922
|
+
}), (id, state, input) => {
|
|
923
|
+
const result = callbacks.invoke(id, state, input, neverAborted);
|
|
924
|
+
return typeof result === "string"
|
|
925
|
+
? result
|
|
926
|
+
: callbackFailed(new Error(`Internal callback '${id}' is asynchronous.`));
|
|
927
|
+
}, async (id, state, input, signal) => callbacks.invoke(id, state, input, signal), options.signal);
|
|
1147
928
|
const result = parseJson(runResultSchema, resultJson, "run result");
|
|
1148
929
|
return { ...result, state: parse(graph.state, result.state, "final state") };
|
|
1149
930
|
}
|
|
@@ -1155,7 +936,7 @@ export async function run(graph, initial, options = {}) {
|
|
|
1155
936
|
if (callbackFailure) {
|
|
1156
937
|
throw new ContractValidationError(callbackFailure.boundary, callbackFailure.problems);
|
|
1157
938
|
}
|
|
1158
|
-
if (
|
|
939
|
+
if (isCancellation(error, options.signal)) {
|
|
1159
940
|
throw new TandemCancellationError(error);
|
|
1160
941
|
}
|
|
1161
942
|
throw new TandemRuntimeError("run", error);
|
|
@@ -1165,27 +946,13 @@ export async function run(graph, initial, options = {}) {
|
|
|
1165
946
|
}
|
|
1166
947
|
}
|
|
1167
948
|
function participantPersists(node) {
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}
|
|
1172
|
-
return implementation instanceof ParallelImplementation
|
|
1173
|
-
? Object.values(implementation.branches).some((branch) => branch.persist === true)
|
|
1174
|
-
: false;
|
|
949
|
+
const record = nodeRecord(node);
|
|
950
|
+
return (record.persist === true ||
|
|
951
|
+
(record.kind === "parallel" && Object.values(record.branches).some(participantPersists)));
|
|
1175
952
|
}
|
|
1176
953
|
function issues(schema, input) {
|
|
1177
|
-
let value;
|
|
1178
|
-
try {
|
|
1179
|
-
value = JSON.parse(input);
|
|
1180
|
-
}
|
|
1181
|
-
catch {
|
|
1182
|
-
return JSON.stringify([{ path: "$", message: "Invalid JSON" }]);
|
|
1183
|
-
}
|
|
1184
|
-
return issuesParsed(schema, value);
|
|
1185
|
-
}
|
|
1186
|
-
function issuesParsed(schema, value) {
|
|
1187
954
|
try {
|
|
1188
|
-
|
|
955
|
+
parseJson(schema, input, "agent contract");
|
|
1189
956
|
return "";
|
|
1190
957
|
}
|
|
1191
958
|
catch (error) {
|
|
@@ -1206,180 +973,182 @@ function requireInstructions(instructions, boundary) {
|
|
|
1206
973
|
]);
|
|
1207
974
|
}
|
|
1208
975
|
}
|
|
976
|
+
/** Authoring errors read as "<subject> <field path> <problem>", e.g. "Agent 'a' checkpoint session must be ...". */
|
|
977
|
+
function parseDefinition(schema, value, subject) {
|
|
978
|
+
const result = schema.safeParse(value);
|
|
979
|
+
if (result.success) {
|
|
980
|
+
return result.data;
|
|
981
|
+
}
|
|
982
|
+
const problems = result.error.issues.map((issue) => {
|
|
983
|
+
const field = issue.path
|
|
984
|
+
.map((part) => (typeof part === "number" ? `[${part}]` : ` ${String(part)}`))
|
|
985
|
+
.join("");
|
|
986
|
+
return `${field} ${issue.message}`;
|
|
987
|
+
});
|
|
988
|
+
throw new TandemError(`${subject}${problems.join(";")}.`);
|
|
989
|
+
}
|
|
1209
990
|
function interactionHandlerEntries(handlers) {
|
|
1210
991
|
if (!handlers) {
|
|
1211
992
|
return [];
|
|
1212
993
|
}
|
|
1213
|
-
|
|
994
|
+
const entries = registeredHandlers.get(handlers);
|
|
995
|
+
if (!entries) {
|
|
1214
996
|
throw new TandemError("interactions must be created by interactions().");
|
|
1215
997
|
}
|
|
1216
|
-
return
|
|
998
|
+
return entries;
|
|
1217
999
|
}
|
|
1218
1000
|
function compileNode(node, stateSchema, callbacks) {
|
|
1219
|
-
const
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const runCallback = callbacks.registerAsync(async (item, scopeId, signal) => {
|
|
1230
|
-
let active = false;
|
|
1231
|
-
let closed = false;
|
|
1232
|
-
const context = {
|
|
1233
|
-
signal,
|
|
1234
|
-
async run(value, input) {
|
|
1235
|
-
if (closed)
|
|
1236
|
-
throw new TandemError("Collection scope has ended.");
|
|
1237
|
-
if (active)
|
|
1238
|
-
throw new TandemError("Collection item agents must be awaited serially.");
|
|
1239
|
-
if (!definition.agents.includes(value) || !(value instanceof TaskAgentImplementation)) {
|
|
1240
|
-
throw new TandemError("Agent is not declared in this collection.");
|
|
1241
|
-
}
|
|
1242
|
-
signal.throwIfAborted();
|
|
1243
|
-
active = true;
|
|
1244
|
-
try {
|
|
1245
|
-
const { runCollectionAgentAsync } = await import("./runtime/loader.mjs");
|
|
1246
|
-
const response = await runCollectionAgentAsync(scopeId, value.id, serializeBoundary(value.state, { input, output: null }, value.id + " input"));
|
|
1247
|
-
const state = parseJson(value.state, response, value.id + " output");
|
|
1248
|
-
if (state.output === null)
|
|
1249
|
-
throw new TandemError("Agent returned no output.");
|
|
1250
|
-
return parse(value.result, state.output.value, value.id + " result");
|
|
1251
|
-
}
|
|
1252
|
-
finally {
|
|
1253
|
-
active = false;
|
|
1254
|
-
}
|
|
1255
|
-
},
|
|
1001
|
+
const record = nodeRecord(node);
|
|
1002
|
+
const { id } = record;
|
|
1003
|
+
const base = { id, persist: record.persist };
|
|
1004
|
+
const parseState = (state, boundary) => parseJson(stateSchema, state, `${id} ${boundary}`);
|
|
1005
|
+
switch (record.kind) {
|
|
1006
|
+
case "stage":
|
|
1007
|
+
return {
|
|
1008
|
+
...base,
|
|
1009
|
+
kind: "stage",
|
|
1010
|
+
runCallback: callbacks.registerAsync(async (state, _, signal) => serializeBoundary(stateSchema, await record.execute(parseState(state, "input"), { signal }), `${id} output`)),
|
|
1256
1011
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1012
|
+
case "terminal":
|
|
1013
|
+
return {
|
|
1014
|
+
...base,
|
|
1015
|
+
kind: record.failed ? "failure" : "completion",
|
|
1016
|
+
summaryCallback: callbacks.registerSync((state) => record.summary(parseState(state, "state"))),
|
|
1017
|
+
};
|
|
1018
|
+
case "interaction":
|
|
1019
|
+
return {
|
|
1020
|
+
...base,
|
|
1021
|
+
kind: "interaction",
|
|
1022
|
+
requestCallback: callbacks.registerSync((state) => serializeBoundary(record.requestSchema, record.request(parseState(state, "state")), `${id} request`)),
|
|
1023
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, record.apply(parseState(state, "state"), parseJson(record.responseSchema, input, `${id} response input`)), `${id} applied state`)),
|
|
1024
|
+
};
|
|
1025
|
+
case "collection":
|
|
1026
|
+
return compileCollection(record, stateSchema, callbacks);
|
|
1027
|
+
case "agent":
|
|
1028
|
+
return compileAgent(record, stateSchema, callbacks);
|
|
1029
|
+
case "parallel": {
|
|
1030
|
+
const branches = Object.entries(record.branches);
|
|
1031
|
+
const branchStates = z.strictObject(Object.fromEntries(branches.map(([branchId]) => [branchId, stateSchema])));
|
|
1032
|
+
return {
|
|
1033
|
+
...base,
|
|
1034
|
+
kind: "parallel",
|
|
1035
|
+
...(record.max !== undefined ? { max: record.max } : {}),
|
|
1036
|
+
branches: branches.map(([branchId, participant]) => ({
|
|
1037
|
+
id: branchId,
|
|
1038
|
+
participant: compileNode(participant, stateSchema, callbacks),
|
|
1039
|
+
})),
|
|
1040
|
+
mergeCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, record.merge(parseState(state, "merge baseline"), parseJson(branchStates, input, `${id} merge branches`)), `${id} merged state`)),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
default:
|
|
1044
|
+
return record;
|
|
1278
1045
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1046
|
+
}
|
|
1047
|
+
function compileCollection(collection, stateSchema, callbacks) {
|
|
1048
|
+
const { id } = collection;
|
|
1049
|
+
const agents = collection.agents.map((reference) => {
|
|
1050
|
+
const task = taskAgentRecord(reference);
|
|
1051
|
+
return compileNode(task.participant, task.state, callbacks);
|
|
1052
|
+
});
|
|
1053
|
+
const itemsCallback = callbacks.registerSync((state) => serializeBoundary(z.array(collection.item), collection.items(parseJson(stateSchema, state, `${id} state`)), `${id} items`));
|
|
1054
|
+
const runCallback = callbacks.registerAsync(async (item, scopeId, signal) => {
|
|
1055
|
+
let active = false;
|
|
1056
|
+
let closed = false;
|
|
1057
|
+
const context = {
|
|
1281
1058
|
signal,
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
...base,
|
|
1290
|
-
kind: "interaction",
|
|
1291
|
-
requestCallback: request,
|
|
1292
|
-
applyCallback: apply,
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
if (implementation instanceof AgentImplementation) {
|
|
1296
|
-
const message = callbacks.registerSync((state) => implementation.message(parseJson(stateSchema, state, `${node.id} message state`)));
|
|
1297
|
-
const output = implementation.output
|
|
1298
|
-
? compileAgentOutput(node.id, implementation.output, stateSchema, callbacks)
|
|
1299
|
-
: undefined;
|
|
1300
|
-
const capabilities = implementation.granted.map((item) => item[compileCapabilityBrand]({ id: node.id, stateSchema, callbacks }));
|
|
1301
|
-
const workspace = implementation.workspace
|
|
1302
|
-
? compileWorkspace(node.id, implementation.workspace, stateSchema, callbacks)
|
|
1303
|
-
: undefined;
|
|
1304
|
-
return {
|
|
1305
|
-
...base,
|
|
1306
|
-
kind: "agent",
|
|
1307
|
-
instructions: implementation.instructions,
|
|
1308
|
-
client: { ...implementation.client, verifyModel: implementation.client.verifyModel ?? false },
|
|
1309
|
-
messageCallback: message,
|
|
1310
|
-
output,
|
|
1311
|
-
capabilities,
|
|
1312
|
-
skillDirectories: implementation.skills.map((item) => item.directory),
|
|
1313
|
-
temperature: implementation.temperature,
|
|
1314
|
-
maxOutputTokens: implementation.maxOutputTokens,
|
|
1315
|
-
reasoning: implementation.reasoning,
|
|
1316
|
-
continueSession: implementation.continueSession,
|
|
1317
|
-
checkpoint: implementation.checkpoint
|
|
1318
|
-
? {
|
|
1319
|
-
contextWindowTokens: implementation.checkpoint.contextWindowTokens,
|
|
1320
|
-
maxOutputTokens: implementation.checkpoint.maxOutputTokens,
|
|
1321
|
-
checkpointAtPercent: implementation.checkpoint.checkpointAtPercent,
|
|
1322
|
-
capabilityName: implementation.checkpoint.capability.name,
|
|
1323
|
-
instructions: implementation.checkpoint.instructions,
|
|
1324
|
-
messageCallback: callbacks.registerSync((state, input) => implementation.checkpoint.message(parseJson(stateSchema, state, `${node.id} checkpoint state`), Number(input))),
|
|
1325
|
-
resetSession: (implementation.checkpoint.session ?? "reset") === "reset",
|
|
1326
|
-
disableCompaction: implementation.checkpoint.disableCompaction ?? false,
|
|
1059
|
+
async run(value, input) {
|
|
1060
|
+
if (closed)
|
|
1061
|
+
throw new TandemError("Collection scope has ended.");
|
|
1062
|
+
if (active)
|
|
1063
|
+
throw new TandemError("Collection item agents must be awaited serially.");
|
|
1064
|
+
if (!collection.agents.includes(value)) {
|
|
1065
|
+
throw new TandemError("Agent is not declared in this collection.");
|
|
1327
1066
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1067
|
+
const task = taskAgentRecord(value);
|
|
1068
|
+
signal.throwIfAborted();
|
|
1069
|
+
active = true;
|
|
1070
|
+
try {
|
|
1071
|
+
const { runCollectionAgentAsync } = await loadRuntime();
|
|
1072
|
+
const response = await runCollectionAgentAsync(scopeId, task.id, serializeBoundary(task.state, { input, output: null }, `${task.id} input`));
|
|
1073
|
+
const state = parseJson(task.state, response, `${task.id} output`);
|
|
1074
|
+
if (state.output === null)
|
|
1075
|
+
throw new TandemError("Agent returned no output.");
|
|
1076
|
+
return parse(task.result, state.output.value, `${task.id} result`);
|
|
1077
|
+
}
|
|
1078
|
+
finally {
|
|
1079
|
+
active = false;
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1331
1082
|
};
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await collection.execute(parseJson(collection.item, item, `${id} item`), context);
|
|
1085
|
+
if (active)
|
|
1086
|
+
throw new TandemError("Collection item returned before its agent completed.");
|
|
1087
|
+
signal.throwIfAborted();
|
|
1088
|
+
return serializeBoundary(collection.result, result, `${id} result`);
|
|
1089
|
+
}
|
|
1090
|
+
finally {
|
|
1091
|
+
closed = true;
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
const applyCallback = callbacks.registerSync((state, results) => serializeBoundary(stateSchema, collection.apply(parseJson(stateSchema, state, `${id} state`), parseJson(z.array(collection.result), results, `${id} results`)), `${id} output`));
|
|
1095
|
+
return {
|
|
1096
|
+
id,
|
|
1097
|
+
persist: collection.persist,
|
|
1098
|
+
kind: "collection",
|
|
1099
|
+
agents,
|
|
1100
|
+
max: collection.max,
|
|
1101
|
+
itemsCallback,
|
|
1102
|
+
runCallback,
|
|
1103
|
+
applyCallback,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function compileAgent(agent, stateSchema, callbacks) {
|
|
1107
|
+
const { id, checkpoint } = agent;
|
|
1108
|
+
const message = callbacks.registerSync((state) => agent.message(parseJson(stateSchema, state, `${id} message state`)));
|
|
1109
|
+
const output = agent.output
|
|
1110
|
+
? compileAgentOutput(id, agent.output, stateSchema, callbacks)
|
|
1111
|
+
: undefined;
|
|
1112
|
+
const capabilities = (agent.capabilities ?? []).map((item) => item[compileCapabilityBrand]({ id, stateSchema, callbacks }));
|
|
1113
|
+
const workspace = agent.workspace
|
|
1114
|
+
? compileWorkspace(id, agent.workspace, stateSchema, callbacks)
|
|
1115
|
+
: undefined;
|
|
1116
|
+
return {
|
|
1117
|
+
id,
|
|
1118
|
+
persist: agent.persist,
|
|
1119
|
+
kind: "agent",
|
|
1120
|
+
instructions: agent.instructions,
|
|
1121
|
+
client: { ...agent.client, verifyModel: agent.client.verifyModel ?? false },
|
|
1122
|
+
messageCallback: message,
|
|
1123
|
+
output,
|
|
1124
|
+
capabilities,
|
|
1125
|
+
skillDirectories: (agent.skills ?? []).map((item) => item.directory),
|
|
1126
|
+
temperature: agent.temperature,
|
|
1127
|
+
maxOutputTokens: agent.maxOutputTokens,
|
|
1128
|
+
reasoning: agent.reasoning,
|
|
1129
|
+
continueSession: agent.continueSession ?? false,
|
|
1130
|
+
checkpoint: checkpoint
|
|
1131
|
+
? {
|
|
1132
|
+
contextWindowTokens: checkpoint.contextWindowTokens,
|
|
1133
|
+
maxOutputTokens: checkpoint.maxOutputTokens,
|
|
1134
|
+
checkpointAtPercent: checkpoint.checkpointAtPercent,
|
|
1135
|
+
capabilityName: checkpoint.capability.name,
|
|
1136
|
+
instructions: checkpoint.instructions,
|
|
1137
|
+
messageCallback: callbacks.registerSync((state, input) => checkpoint.message(parseJson(stateSchema, state, `${id} checkpoint state`), Number(input))),
|
|
1138
|
+
resetSession: (checkpoint.session ?? "reset") === "reset",
|
|
1139
|
+
disableCompaction: checkpoint.disableCompaction ?? false,
|
|
1357
1140
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
return serializeBoundary(stateSchema, implementation.merge(baseline, parsed), `${node.id} merged state`);
|
|
1363
|
-
});
|
|
1364
|
-
return {
|
|
1365
|
-
...base,
|
|
1366
|
-
kind: "parallel",
|
|
1367
|
-
...(implementation.max !== undefined ? { max: implementation.max } : {}),
|
|
1368
|
-
branches: entries.map(([id, participant]) => ({
|
|
1369
|
-
id,
|
|
1370
|
-
participant: compileNode(participant, stateSchema, callbacks),
|
|
1371
|
-
})),
|
|
1372
|
-
mergeCallback,
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
const terminal = implementation;
|
|
1376
|
-
const summary = callbacks.registerSync((state) => terminal.summary(parseJson(stateSchema, state, `${node.id} state`)));
|
|
1377
|
-
return { ...base, kind: terminal.failed ? "failure" : "completion", summaryCallback: summary };
|
|
1141
|
+
: undefined,
|
|
1142
|
+
timeoutMilliseconds: agent.timeoutMs,
|
|
1143
|
+
workspace,
|
|
1144
|
+
};
|
|
1378
1145
|
}
|
|
1379
|
-
function compileWorkspace(id,
|
|
1380
|
-
if (!(
|
|
1146
|
+
function compileWorkspace(id, authored, stateSchema, callbacks) {
|
|
1147
|
+
if (!("groups" in authored)) {
|
|
1381
1148
|
throw new TandemError(`Agent '${id}' workspace must be created by agentWorkspace().withTools().`);
|
|
1382
1149
|
}
|
|
1150
|
+
// A workspace configuration is the record `withTools()` returned.
|
|
1151
|
+
const configuration = authored;
|
|
1383
1152
|
const workspace = configuration.workspace;
|
|
1384
1153
|
const pathCallback = callbacks.registerSync((state) => {
|
|
1385
1154
|
const value = workspace.path(parseJson(stateSchema, state, `${id} workspace path state`));
|
|
@@ -1390,18 +1159,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1390
1159
|
});
|
|
1391
1160
|
const commandsCallback = callbacks.registerSync((state) => {
|
|
1392
1161
|
const typedState = parseJson(stateSchema, state, `${id} workspace commands state`);
|
|
1393
|
-
const
|
|
1394
|
-
? workspace.commandSource(typedState)
|
|
1162
|
+
const commands = typeof workspace.commandSource === "function"
|
|
1163
|
+
? parseDefinition(agentCommandsSchema, workspace.commandSource(typedState), `Agent '${id}' workspace commands`)
|
|
1395
1164
|
: (workspace.commandSource ?? []);
|
|
1396
|
-
|
|
1397
|
-
return serializeBoundary(z.array(z
|
|
1398
|
-
.object({
|
|
1399
|
-
name: z.string(),
|
|
1400
|
-
description: z.string(),
|
|
1401
|
-
command: z.string(),
|
|
1402
|
-
arguments: z.array(z.string()).optional(),
|
|
1403
|
-
})
|
|
1404
|
-
.strict()), value, `${id} workspace commands`);
|
|
1165
|
+
return JSON.stringify(commands);
|
|
1405
1166
|
});
|
|
1406
1167
|
const selected = new Set();
|
|
1407
1168
|
const toolGroups = configuration.groups.map((group, index) => {
|
|
@@ -1429,9 +1190,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1429
1190
|
includeCommands = true;
|
|
1430
1191
|
}
|
|
1431
1192
|
}
|
|
1432
|
-
const
|
|
1193
|
+
const { predicate } = group;
|
|
1194
|
+
const whenCallback = predicate
|
|
1433
1195
|
? callbacks.registerSync((state) => {
|
|
1434
|
-
const value =
|
|
1196
|
+
const value = predicate(parseJson(stateSchema, state, `${id} tool group ${index} state`));
|
|
1435
1197
|
if (typeof value !== "boolean") {
|
|
1436
1198
|
throw new TandemError(`Agent '${id}' tool group ${index} predicate must return a boolean.`);
|
|
1437
1199
|
}
|
|
@@ -1440,17 +1202,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1440
1202
|
: undefined;
|
|
1441
1203
|
return { tools, includeCommands, whenCallback };
|
|
1442
1204
|
});
|
|
1443
|
-
const
|
|
1205
|
+
const { interceptTool } = configuration;
|
|
1206
|
+
const interceptCallback = interceptTool
|
|
1444
1207
|
? callbacks.registerAsync(async (state, input, signal) => {
|
|
1445
|
-
const
|
|
1446
|
-
let invocation;
|
|
1447
|
-
try {
|
|
1448
|
-
invocation = JSON.parse(input);
|
|
1449
|
-
}
|
|
1450
|
-
catch {
|
|
1451
|
-
throw new TandemError(`Agent '${id}' received an invalid tool interception payload.`);
|
|
1452
|
-
}
|
|
1453
|
-
const result = await configuration.interceptTool(typedState, invocation, { signal });
|
|
1208
|
+
const result = await interceptTool(parseJson(stateSchema, state, `${id} tool interception state`), parseJson(agentToolInvocationSchema, input, `${id} tool interception payload`), { signal });
|
|
1454
1209
|
if (result !== null && typeof result !== "string") {
|
|
1455
1210
|
throw new TandemError(`Agent '${id}' tool interceptor must return a string or null.`);
|
|
1456
1211
|
}
|
|
@@ -1460,27 +1215,34 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1460
1215
|
return { pathCallback, commandsCallback, toolGroups, interceptCallback };
|
|
1461
1216
|
}
|
|
1462
1217
|
function compileAgentOutput(id, output, stateSchema, callbacks) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1218
|
+
let decode;
|
|
1219
|
+
let format;
|
|
1220
|
+
if ("raw" in output) {
|
|
1221
|
+
const { parse } = output;
|
|
1222
|
+
// The bridge hands back exactly the JSON that rawParseCallback produced from `parse`.
|
|
1223
|
+
decode = (input) => JSON.parse(input);
|
|
1224
|
+
format = {
|
|
1225
|
+
raw: true,
|
|
1226
|
+
rawParseCallback: callbacks.registerSync((_, input) => JSON.stringify(parse(input))),
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
const { schema } = output;
|
|
1231
|
+
decode = (input) => parseJson(schema, input, `${id} output`);
|
|
1232
|
+
format = {
|
|
1233
|
+
jsonSchema: inputJsonSchema(schema, `${id} output schema`),
|
|
1234
|
+
validateCallback: callbacks.registerSync((_, input) => issues(schema, input)),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const { validateFor, apply } = output;
|
|
1238
|
+
const parseState = (state) => parseJson(stateSchema, state, `${id} state`);
|
|
1471
1239
|
return {
|
|
1472
1240
|
instructions: output.instructions,
|
|
1473
|
-
...
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
: {
|
|
1479
|
-
jsonSchema: inputJsonSchema(output.schema, `${id} output schema`),
|
|
1480
|
-
validateCallback: validate,
|
|
1481
|
-
}),
|
|
1482
|
-
validateForCallback: validateFor,
|
|
1483
|
-
applyCallback: apply,
|
|
1241
|
+
...format,
|
|
1242
|
+
validateForCallback: validateFor
|
|
1243
|
+
? callbacks.registerSync((state, input) => validationProblems(validateFor(parseState(state), decode(input)), `${id} output contextual validation`))
|
|
1244
|
+
: undefined,
|
|
1245
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, apply(parseState(state), decode(input)), `${id} applied state`)),
|
|
1484
1246
|
valueType: `${id}.output`,
|
|
1485
1247
|
};
|
|
1486
1248
|
}
|