@maxanstey-meridian/tandem 0.1.6 → 0.3.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 +673 -922
- package/dist/runtime/darwin-arm64/Dapper.dll +0 -0
- package/dist/runtime/darwin-arm64/Spectre.Console.Json.dll +0 -0
- package/dist/runtime/darwin-arm64/Tandem.Advanced.dll +0 -0
- package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.cjs → Tandem.Bridge.cjs} +1 -1
- package/dist/runtime/{linux-x64/Tandem.NodeApiSpike.Bridge.d.ts → darwin-arm64/Tandem.Bridge.d.ts} +8 -2
- package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.deps.json → Tandem.Bridge.deps.json} +93 -144
- package/dist/runtime/darwin-arm64/Tandem.Bridge.dll +0 -0
- package/dist/runtime/{linux-x64/Tandem.NodeApiSpike.Bridge.mjs → darwin-arm64/Tandem.Bridge.mjs} +1 -1
- package/dist/runtime/darwin-arm64/Tandem.Ledger.dll +0 -0
- package/dist/runtime/darwin-arm64/Tandem.OpenAICompatible.dll +0 -0
- package/dist/runtime/darwin-arm64/Tandem.Terminal.dll +0 -0
- package/dist/runtime/darwin-arm64/Tandem.dll +0 -0
- package/dist/runtime/linux-x64/Dapper.dll +0 -0
- package/dist/runtime/linux-x64/Spectre.Console.Json.dll +0 -0
- package/dist/runtime/linux-x64/Tandem.Advanced.dll +0 -0
- package/dist/runtime/linux-x64/{Tandem.NodeApiSpike.Bridge.cjs → Tandem.Bridge.cjs} +1 -1
- package/dist/runtime/{darwin-arm64/Tandem.NodeApiSpike.Bridge.d.ts → linux-x64/Tandem.Bridge.d.ts} +8 -2
- package/dist/runtime/linux-x64/{Tandem.NodeApiSpike.Bridge.deps.json → Tandem.Bridge.deps.json} +93 -144
- package/dist/runtime/linux-x64/Tandem.Bridge.dll +0 -0
- package/dist/runtime/{darwin-arm64/Tandem.NodeApiSpike.Bridge.mjs → linux-x64/Tandem.Bridge.mjs} +1 -1
- package/dist/runtime/linux-x64/Tandem.Ledger.dll +0 -0
- package/dist/runtime/linux-x64/Tandem.OpenAICompatible.dll +0 -0
- package/dist/runtime/linux-x64/Tandem.Terminal.dll +0 -0
- package/dist/runtime/linux-x64/Tandem.dll +0 -0
- package/dist/runtime/loader.mjs +2 -2
- package/package.json +3 -1
- package/dist/runtime/darwin-arm64/Humanizer.dll +0 -0
- package/dist/runtime/darwin-arm64/Json.More.dll +0 -0
- package/dist/runtime/darwin-arm64/JsonPointer.Net.dll +0 -0
- package/dist/runtime/darwin-arm64/JsonSchema.Net.Generation.dll +0 -0
- package/dist/runtime/darwin-arm64/JsonSchema.Net.dll +0 -0
- package/dist/runtime/darwin-arm64/Tandem.NodeApiSpike.Bridge.dll +0 -0
- package/dist/runtime/darwin-arm64/cs/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/cs/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/de/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/de/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/es/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/es/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/fr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/fr/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/import.cjs +0 -3
- package/dist/runtime/darwin-arm64/it/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/it/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ja/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ja/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ko/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ko/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/pl/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/pl/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/pt-BR/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ru/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/ru/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/tr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/tr/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/zh-Hans/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/runtime/darwin-arm64/zh-Hant/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/runtime/linux-x64/Humanizer.dll +0 -0
- package/dist/runtime/linux-x64/Json.More.dll +0 -0
- package/dist/runtime/linux-x64/JsonPointer.Net.dll +0 -0
- package/dist/runtime/linux-x64/JsonSchema.Net.Generation.dll +0 -0
- package/dist/runtime/linux-x64/JsonSchema.Net.dll +0 -0
- package/dist/runtime/linux-x64/Tandem.NodeApiSpike.Bridge.dll +0 -0
- package/dist/runtime/linux-x64/import.cjs +0 -3
- /package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.runtimeconfig.json → Tandem.Bridge.runtimeconfig.json} +0 -0
- /package/dist/runtime/linux-x64/{Tandem.NodeApiSpike.Bridge.runtimeconfig.json → Tandem.Bridge.runtimeconfig.json} +0 -0
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,48 @@ 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
|
-
|
|
75
|
+
const value = callback(state, input, signal);
|
|
76
|
+
return typeof value === "string"
|
|
77
|
+
? callbackSucceeded(value)
|
|
78
|
+
: value.then(callbackSucceeded, callbackFailed);
|
|
79
79
|
}
|
|
80
80
|
catch (error) {
|
|
81
|
-
return
|
|
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
|
-
});
|
|
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
|
-
|
|
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;
|
|
92
|
+
const id = `c${this.#next++}`;
|
|
93
|
+
this.#callbacks.set(id, callback);
|
|
94
|
+
return id;
|
|
137
95
|
}
|
|
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
96
|
}
|
|
141
97
|
function path(parts) {
|
|
142
98
|
return parts.length === 0
|
|
@@ -176,43 +132,24 @@ function parse(schema, value, boundary) {
|
|
|
176
132
|
}
|
|
177
133
|
return result;
|
|
178
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The walker is the whole losslessness check: a stringify/parse round-trip compares only enumerable
|
|
137
|
+
* data, so it misses accessors and non-enumerable properties, and it cannot name the failing path.
|
|
138
|
+
*/
|
|
179
139
|
function serializeBoundary(schema, value, boundary) {
|
|
180
140
|
const parsed = parse(schema, value, boundary);
|
|
181
141
|
const problem = jsonValueProblem(parsed, "$", new WeakSet());
|
|
182
142
|
if (problem) {
|
|
183
143
|
throw new ContractValidationError(boundary, [problem]);
|
|
184
144
|
}
|
|
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;
|
|
145
|
+
return JSON.stringify(parsed);
|
|
209
146
|
}
|
|
210
147
|
function jsonValueProblem(value, valuePath, seen) {
|
|
211
148
|
if (value === undefined) {
|
|
212
149
|
return { path: valuePath, message: "undefined is not JSON-serializable." };
|
|
213
150
|
}
|
|
214
|
-
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
215
|
-
return { path: valuePath, message: "Non-finite numbers are not JSON-serializable." };
|
|
151
|
+
if (typeof value === "number" && (!Number.isFinite(value) || Object.is(value, -0))) {
|
|
152
|
+
return { path: valuePath, message: "Non-finite numbers and -0 are not JSON-serializable." };
|
|
216
153
|
}
|
|
217
154
|
if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
|
|
218
155
|
return { path: valuePath, message: `${typeof value} values are not JSON-serializable.` };
|
|
@@ -312,164 +249,133 @@ function inputJsonSchema(schema, boundary) {
|
|
|
312
249
|
]);
|
|
313
250
|
}
|
|
314
251
|
}
|
|
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
|
-
}
|
|
252
|
+
const positiveInt32 = z.int32({ error: "must be a positive 32-bit integer" }).min(1);
|
|
253
|
+
const nonBlankString = z
|
|
254
|
+
.string({ error: "must be a non-blank string" })
|
|
255
|
+
.refine((value) => value.trim().length > 0, { error: "must be a non-blank string" });
|
|
256
|
+
const hasUnique = (items, key) => new Set(items.map(key)).size === items.length;
|
|
257
|
+
/** The participant brand only pins `TState` for the type checker; `kind` identifies records at runtime. */
|
|
258
|
+
const keepState = (state) => state;
|
|
332
259
|
export function stage(definition) {
|
|
333
|
-
|
|
260
|
+
const record = {
|
|
261
|
+
...definition,
|
|
262
|
+
[participantBrand]: keepState,
|
|
263
|
+
kind: "stage",
|
|
264
|
+
};
|
|
265
|
+
return Object.freeze(record);
|
|
334
266
|
}
|
|
335
267
|
const taskAgentBrand = Symbol("taskAgent");
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
this.participant = agent({
|
|
268
|
+
export function taskAgent(definition) {
|
|
269
|
+
const { validateFor } = definition.output;
|
|
270
|
+
const record = {
|
|
271
|
+
[taskAgentBrand]: true,
|
|
272
|
+
id: definition.id,
|
|
273
|
+
input: definition.input,
|
|
274
|
+
result: definition.result,
|
|
275
|
+
state: z.object({
|
|
276
|
+
input: definition.input,
|
|
277
|
+
output: z.object({ value: definition.result }).nullable(),
|
|
278
|
+
}),
|
|
279
|
+
participant: agent({
|
|
349
280
|
...definition,
|
|
350
281
|
message: (state) => definition.message(state.input),
|
|
351
282
|
output: {
|
|
352
283
|
...definition.output,
|
|
353
|
-
validateFor:
|
|
354
|
-
|
|
355
|
-
: undefined,
|
|
356
|
-
apply: (state, value) => ({
|
|
357
|
-
...state,
|
|
358
|
-
output: { value },
|
|
359
|
-
}),
|
|
284
|
+
validateFor: validateFor ? (state, value) => validateFor(state.input, value) : undefined,
|
|
285
|
+
apply: (state, value) => ({ ...state, output: { value } }),
|
|
360
286
|
},
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function taskAgent(definition) {
|
|
365
|
-
return new TaskAgentImplementation(definition.id, definition.input, definition.result, definition);
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
return Object.freeze(record);
|
|
366
290
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
super(definition.id, definition.persist);
|
|
372
|
-
this.definition = definition;
|
|
291
|
+
/** Task agents are only created by `taskAgent()`, whose record is the returned object. */
|
|
292
|
+
function taskAgentRecord(reference) {
|
|
293
|
+
if (!("participant" in reference)) {
|
|
294
|
+
throw new TandemError("Use taskAgent to declare collection agents.");
|
|
373
295
|
}
|
|
296
|
+
return reference;
|
|
374
297
|
}
|
|
298
|
+
const collectionOptionsSchema = z.object({
|
|
299
|
+
max: positiveInt32,
|
|
300
|
+
agents: z
|
|
301
|
+
.array(z.object({ id: z.string() }))
|
|
302
|
+
.refine((agents) => hasUnique(agents, (agent) => agent.id), { error: "must have unique IDs" }),
|
|
303
|
+
});
|
|
375
304
|
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
|
-
}
|
|
305
|
+
parseDefinition(collectionOptionsSchema, definition, `Collection '${definition.id}'`);
|
|
306
|
+
const record = {
|
|
307
|
+
...definition,
|
|
308
|
+
[participantBrand]: keepState,
|
|
309
|
+
kind: "collection",
|
|
310
|
+
};
|
|
311
|
+
return Object.freeze(record);
|
|
399
312
|
}
|
|
400
313
|
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
|
-
}
|
|
314
|
+
const record = {
|
|
315
|
+
...definition,
|
|
316
|
+
[participantBrand]: keepState,
|
|
317
|
+
kind: "interaction",
|
|
318
|
+
};
|
|
319
|
+
return Object.freeze(record);
|
|
419
320
|
}
|
|
321
|
+
const registeredHandlers = new WeakMap();
|
|
420
322
|
export function interactions() {
|
|
421
|
-
|
|
323
|
+
const entries = [];
|
|
324
|
+
const handlers = {
|
|
325
|
+
[interactionHandlersBrand]: true,
|
|
326
|
+
handle(interaction, handler) {
|
|
327
|
+
if (entries.some((entry) => entry.interaction === interaction)) {
|
|
328
|
+
throw new TandemError(`Interaction '${interaction.id}' already has a handler.`);
|
|
329
|
+
}
|
|
330
|
+
entries.push({
|
|
331
|
+
// An interaction is the record `interaction()` built; its types are erased for storage.
|
|
332
|
+
interaction: interaction,
|
|
333
|
+
handle: handler,
|
|
334
|
+
});
|
|
335
|
+
return handlers;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
registeredHandlers.set(handlers, entries);
|
|
339
|
+
return Object.freeze(handlers);
|
|
422
340
|
}
|
|
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`)));
|
|
341
|
+
export function capability(definition) {
|
|
342
|
+
const { name, instructions, schema, validateFor, apply, summarize } = definition;
|
|
343
|
+
requireInstructions(instructions, `Capability '${name}' instructions`);
|
|
344
|
+
const requestJsonSchema = inputJsonSchema(schema, `capability '${name}' schema`);
|
|
345
|
+
const compile = ({ id, stateSchema, callbacks }) => {
|
|
346
|
+
const parseState = (state) => parseJson(stateSchema, state, `${id} state`);
|
|
347
|
+
const parseRequest = (input) => parseJson(schema, input, `${id} capability '${name}' request`);
|
|
447
348
|
return {
|
|
448
|
-
name
|
|
449
|
-
instructions
|
|
450
|
-
jsonSchema:
|
|
451
|
-
validateCallback:
|
|
452
|
-
validateForCallback: validateFor
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
349
|
+
name,
|
|
350
|
+
instructions,
|
|
351
|
+
jsonSchema: requestJsonSchema,
|
|
352
|
+
validateCallback: callbacks.registerSync((_, input) => issues(schema, input)),
|
|
353
|
+
validateForCallback: validateFor
|
|
354
|
+
? callbacks.registerSync((state, input) => validationProblems(validateFor(parseState(state), parseRequest(input)), `${id} capability '${name}' contextual validation`))
|
|
355
|
+
: undefined,
|
|
356
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, apply(parseState(state), parseRequest(input)), `${id} applied state`)),
|
|
357
|
+
summaryCallback: callbacks.registerSync((_, input) => summarize(parseRequest(input))),
|
|
358
|
+
valueType: `${id}.capability.${name}`,
|
|
456
359
|
};
|
|
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
|
-
}
|
|
360
|
+
};
|
|
361
|
+
const record = {
|
|
362
|
+
name,
|
|
363
|
+
requestJsonSchema,
|
|
364
|
+
[compileCapabilityBrand]: compile,
|
|
365
|
+
};
|
|
366
|
+
return Object.freeze(record);
|
|
472
367
|
}
|
|
368
|
+
const agentToolInvocationSchema = z.strictObject({
|
|
369
|
+
name: z.string(),
|
|
370
|
+
effect: z.enum([
|
|
371
|
+
"read",
|
|
372
|
+
"workspaceMutation",
|
|
373
|
+
"processExecution",
|
|
374
|
+
"lifecycleTransition",
|
|
375
|
+
"unclassified",
|
|
376
|
+
]),
|
|
377
|
+
arguments: z.unknown(),
|
|
378
|
+
});
|
|
473
379
|
function createToolGroup(predicate, tools) {
|
|
474
380
|
if (tools.length === 0) {
|
|
475
381
|
throw new TandemError("An agent tool group cannot be empty.");
|
|
@@ -484,264 +390,154 @@ function createToolGroup(predicate, tools) {
|
|
|
484
390
|
}
|
|
485
391
|
seen.add(tool);
|
|
486
392
|
}
|
|
487
|
-
|
|
393
|
+
const record = {
|
|
394
|
+
[toolGroupBrand]: predicate ?? (() => true),
|
|
395
|
+
predicate,
|
|
396
|
+
tools,
|
|
397
|
+
};
|
|
398
|
+
return Object.freeze(record);
|
|
488
399
|
}
|
|
489
400
|
export const agentTools = {
|
|
490
401
|
always: (...tools) => createToolGroup(undefined, tools),
|
|
491
402
|
when: (predicate, ...tools) => createToolGroup(predicate, tools),
|
|
492
403
|
};
|
|
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
404
|
export function agentWorkspace(definition) {
|
|
533
405
|
if (typeof definition.path !== "function") {
|
|
534
406
|
throw new TandemError("Workspace path is required.");
|
|
535
407
|
}
|
|
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.`);
|
|
408
|
+
const workspace = Object.freeze({
|
|
409
|
+
path: definition.path,
|
|
410
|
+
// Parsing copies a static catalogue, so later mutation by the caller cannot change it.
|
|
411
|
+
commandSource: typeof definition.commands === "function" || definition.commands === undefined
|
|
412
|
+
? definition.commands
|
|
413
|
+
: parseDefinition(agentCommandsSchema, definition.commands, "Workspace commands"),
|
|
414
|
+
});
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
commands: Object.freeze({ [commandSelectionBrand]: workspace }),
|
|
417
|
+
withTools(groups, options) {
|
|
418
|
+
if (groups.length === 0) {
|
|
419
|
+
throw new TandemError("An agent workspace requires tool groups.");
|
|
582
420
|
}
|
|
583
|
-
|
|
584
|
-
|
|
421
|
+
const records = groups.map((group) => {
|
|
422
|
+
if (!("tools" in group)) {
|
|
423
|
+
throw new TandemError("Agent tool groups must be created by agentTools.");
|
|
424
|
+
}
|
|
425
|
+
// A tool group record is the object `agentTools` returned.
|
|
426
|
+
return group;
|
|
427
|
+
});
|
|
428
|
+
if (options?.interceptTool !== undefined && typeof options.interceptTool !== "function") {
|
|
429
|
+
throw new TandemError("Workspace tool interceptor must be a function.");
|
|
585
430
|
}
|
|
586
|
-
|
|
587
|
-
|
|
431
|
+
const configuration = {
|
|
432
|
+
[workspaceBrand]: keepState,
|
|
433
|
+
workspace,
|
|
434
|
+
groups: records,
|
|
435
|
+
interceptTool: options?.interceptTool,
|
|
436
|
+
};
|
|
437
|
+
return Object.freeze(configuration);
|
|
438
|
+
},
|
|
439
|
+
});
|
|
588
440
|
}
|
|
441
|
+
const agentCommandsSchema = z.array(z.strictObject({
|
|
442
|
+
name: z
|
|
443
|
+
.string({ error: "must be a valid tool name" })
|
|
444
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, { error: "must be a valid tool name" }),
|
|
445
|
+
description: nonBlankString,
|
|
446
|
+
command: nonBlankString,
|
|
447
|
+
arguments: z
|
|
448
|
+
.array(z
|
|
449
|
+
.string({ error: "must be a string" })
|
|
450
|
+
.refine((value) => value.trim().length > 0, { error: "must not be blank" })
|
|
451
|
+
.max(200, { error: "must be at most 200 characters" }), { error: "must be an array of strings" })
|
|
452
|
+
.max(16, { error: "accepts at most 16 arguments" })
|
|
453
|
+
.optional(),
|
|
454
|
+
}), { error: "must be an array" });
|
|
455
|
+
const skillSchema = z.object({ directory: nonBlankString });
|
|
589
456
|
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
|
-
}
|
|
457
|
+
return parseDefinition(skillSchema, definition, "Skill");
|
|
626
458
|
}
|
|
459
|
+
const capabilityReference = z.custom();
|
|
460
|
+
const agentOutputSchema = z.discriminatedUnion("raw", [
|
|
461
|
+
z.object({
|
|
462
|
+
raw: z.literal(true),
|
|
463
|
+
schema: z.undefined({ error: "is forbidden for raw output" }).optional(),
|
|
464
|
+
parse: z.custom((value) => typeof value === "function", {
|
|
465
|
+
error: "must be a function for raw output",
|
|
466
|
+
}),
|
|
467
|
+
}),
|
|
468
|
+
z.object({
|
|
469
|
+
raw: z.undefined().optional(),
|
|
470
|
+
parse: z.undefined({ error: "requires raw output mode" }).optional(),
|
|
471
|
+
}),
|
|
472
|
+
], { error: "must be true" });
|
|
473
|
+
const agentOptionsSchema = z
|
|
474
|
+
.object({
|
|
475
|
+
output: agentOutputSchema.optional(),
|
|
476
|
+
reasoning: z
|
|
477
|
+
.object({
|
|
478
|
+
effort: z
|
|
479
|
+
.enum(["none", "low", "medium", "high"], {
|
|
480
|
+
error: "must be 'none', 'low', 'medium' or 'high'",
|
|
481
|
+
})
|
|
482
|
+
.optional(),
|
|
483
|
+
maxTokens: z
|
|
484
|
+
.int32({ error: "must be a 32-bit integer of at least 1024" })
|
|
485
|
+
.min(1024)
|
|
486
|
+
.optional(),
|
|
487
|
+
})
|
|
488
|
+
.refine((value) => (value.effort === undefined) !== (value.maxTokens === undefined), {
|
|
489
|
+
error: "must specify exactly one of effort or maxTokens",
|
|
490
|
+
})
|
|
491
|
+
.optional(),
|
|
492
|
+
capabilities: z
|
|
493
|
+
.array(capabilityReference)
|
|
494
|
+
.refine((items) => hasUnique(items, (item) => item.name), {
|
|
495
|
+
error: "must not contain a duplicate capability",
|
|
496
|
+
})
|
|
497
|
+
.optional(),
|
|
498
|
+
skills: z
|
|
499
|
+
.array(skillSchema)
|
|
500
|
+
.refine((items) => hasUnique(items, (item) => item.directory), {
|
|
501
|
+
error: "must not repeat a skill directory",
|
|
502
|
+
})
|
|
503
|
+
.optional(),
|
|
504
|
+
temperature: z.number({ error: "must be between 0 and 2" }).min(0).max(2).optional(),
|
|
505
|
+
maxOutputTokens: positiveInt32.optional(),
|
|
506
|
+
checkpoint: z
|
|
507
|
+
.object({
|
|
508
|
+
contextWindowTokens: positiveInt32,
|
|
509
|
+
maxOutputTokens: positiveInt32,
|
|
510
|
+
checkpointAtPercent: z.int({ error: "must be between 1 and 99" }).min(1).max(99),
|
|
511
|
+
capability: capabilityReference,
|
|
512
|
+
session: z.enum(["retain", "reset"], { error: "must be 'retain' or 'reset'" }).optional(),
|
|
513
|
+
disableCompaction: z.boolean({ error: "must be a boolean" }).optional(),
|
|
514
|
+
})
|
|
515
|
+
.refine((value) => value.maxOutputTokens < value.contextWindowTokens, {
|
|
516
|
+
path: ["maxOutputTokens"],
|
|
517
|
+
error: "must be smaller than contextWindowTokens",
|
|
518
|
+
})
|
|
519
|
+
.optional(),
|
|
520
|
+
})
|
|
521
|
+
.refine((value) => value.checkpoint === undefined ||
|
|
522
|
+
(value.capabilities ?? []).includes(value.checkpoint.capability), { path: ["checkpoint", "capability"], error: "must be attached to the agent" });
|
|
627
523
|
export function agent(definition) {
|
|
628
524
|
const rawOutput = definition.output && "raw" in definition.output && definition.output.raw === true;
|
|
629
525
|
if (!rawOutput || typeof definition.instructions !== "string") {
|
|
630
526
|
requireInstructions(definition.instructions, `Agent '${definition.id}' instructions`);
|
|
631
527
|
}
|
|
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.`);
|
|
528
|
+
if (definition.output && (!rawOutput || typeof definition.output.instructions !== "string")) {
|
|
529
|
+
requireInstructions(definition.output.instructions, `Agent '${definition.id}' output instructions`);
|
|
699
530
|
}
|
|
700
531
|
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;
|
|
532
|
+
requireInstructions(definition.checkpoint.instructions, `Agent '${definition.id}' checkpoint instructions`);
|
|
744
533
|
}
|
|
534
|
+
parseDefinition(agentOptionsSchema, definition, `Agent '${definition.id}'`);
|
|
535
|
+
const record = {
|
|
536
|
+
...definition,
|
|
537
|
+
[participantBrand]: keepState,
|
|
538
|
+
kind: "agent",
|
|
539
|
+
};
|
|
540
|
+
return Object.freeze(record);
|
|
745
541
|
}
|
|
746
542
|
export function parallel(definition) {
|
|
747
543
|
if (!definition) {
|
|
@@ -749,33 +545,43 @@ export function parallel(definition) {
|
|
|
749
545
|
}
|
|
750
546
|
return createParallel(definition);
|
|
751
547
|
}
|
|
548
|
+
const parallelOptionsSchema = z.object({
|
|
549
|
+
max: positiveInt32.optional(),
|
|
550
|
+
branches: z
|
|
551
|
+
.record(z.string(), z.custom())
|
|
552
|
+
.refine((branches) => Object.keys(branches).every((id) => id.trim().length > 0), {
|
|
553
|
+
error: "must not have a blank branch ID",
|
|
554
|
+
})
|
|
555
|
+
.refine((branches) => Object.keys(branches).length >= 2, {
|
|
556
|
+
error: "requires at least two branches",
|
|
557
|
+
})
|
|
558
|
+
.refine((branches) => hasUnique(Object.values(branches), (participant) => participant), {
|
|
559
|
+
error: "must own a distinct participant per branch",
|
|
560
|
+
}),
|
|
561
|
+
});
|
|
752
562
|
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
|
-
}
|
|
563
|
+
parseDefinition(parallelOptionsSchema, definition, `Parallel group '${definition.id}'`);
|
|
564
|
+
const record = {
|
|
565
|
+
...definition,
|
|
566
|
+
[participantBrand]: keepState,
|
|
567
|
+
kind: "parallel",
|
|
568
|
+
};
|
|
569
|
+
return Object.freeze(record);
|
|
776
570
|
}
|
|
777
571
|
export function output(definition) {
|
|
778
|
-
|
|
572
|
+
const record = {
|
|
573
|
+
...definition,
|
|
574
|
+
[participantBrand]: keepState,
|
|
575
|
+
kind: "terminal",
|
|
576
|
+
};
|
|
577
|
+
return Object.freeze(record);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Every node is the frozen record its factory built; the exported node interfaces are opaque views
|
|
581
|
+
* of those records, with the request/output/item types erased here.
|
|
582
|
+
*/
|
|
583
|
+
function nodeRecord(node) {
|
|
584
|
+
return node;
|
|
779
585
|
}
|
|
780
586
|
export function route(definition) {
|
|
781
587
|
return definition;
|
|
@@ -784,43 +590,39 @@ export function pipeline(definition) {
|
|
|
784
590
|
const start = definition.start;
|
|
785
591
|
const members = new Set(definition.nodes);
|
|
786
592
|
if (members.size !== definition.nodes.length) {
|
|
787
|
-
throw new
|
|
593
|
+
throw new TandemError("Pipeline nodes must contain each participant object exactly once.");
|
|
788
594
|
}
|
|
789
595
|
const ids = new Set(definition.nodes.map((node) => node.id));
|
|
790
596
|
if (ids.size !== definition.nodes.length) {
|
|
791
|
-
throw new
|
|
597
|
+
throw new TandemError("Pipeline node IDs must be unique.");
|
|
792
598
|
}
|
|
793
599
|
const ownedParticipants = new Set();
|
|
794
|
-
for (const node of definition.nodes) {
|
|
600
|
+
for (const node of definition.nodes.map(nodeRecord)) {
|
|
795
601
|
if (node.kind !== "parallel") {
|
|
796
602
|
continue;
|
|
797
603
|
}
|
|
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
|
-
}
|
|
604
|
+
for (const participant of Object.values(node.branches)) {
|
|
803
605
|
if (members.has(participant)) {
|
|
804
|
-
throw new
|
|
606
|
+
throw new TandemError(`Parallel branch participant '${participant.id}' cannot also be a parent pipeline node.`);
|
|
805
607
|
}
|
|
806
608
|
if (!ownedParticipants.add(participant)) {
|
|
807
|
-
throw new
|
|
609
|
+
throw new TandemError(`Parallel branch participant '${participant.id}' is owned more than once.`);
|
|
808
610
|
}
|
|
809
611
|
if (ids.has(participant.id)) {
|
|
810
|
-
throw new
|
|
612
|
+
throw new TandemError(`Pipeline participant ID '${participant.id}' must be globally unique.`);
|
|
811
613
|
}
|
|
812
614
|
ids.add(participant.id);
|
|
813
615
|
}
|
|
814
616
|
}
|
|
815
617
|
if (!members.has(definition.start)) {
|
|
816
|
-
throw new
|
|
618
|
+
throw new TandemError(`Pipeline start '${definition.start.id}' must be the registered participant object.`);
|
|
817
619
|
}
|
|
818
620
|
if (start.kind === "terminal") {
|
|
819
|
-
throw new
|
|
621
|
+
throw new TandemError(`Pipeline start '${start.id}' cannot be a terminal.`);
|
|
820
622
|
}
|
|
821
623
|
for (const item of definition.routes) {
|
|
822
624
|
if (!members.has(item.from) || !members.has(item.to)) {
|
|
823
|
-
throw new
|
|
625
|
+
throw new TandemError(`Route '${item.label}' endpoints must be registered participant objects.`);
|
|
824
626
|
}
|
|
825
627
|
}
|
|
826
628
|
const unconditionalRoutes = new Map();
|
|
@@ -831,22 +633,21 @@ export function pipeline(definition) {
|
|
|
831
633
|
const key = `${item.from.id}\u0000${item.outcome ?? "default"}`;
|
|
832
634
|
const existing = unconditionalRoutes.get(key);
|
|
833
635
|
if (existing) {
|
|
834
|
-
throw new
|
|
636
|
+
throw new TandemError(`Routes '${existing.label}' and '${item.label}' are both unconditional from '${item.from.id}'.`);
|
|
835
637
|
}
|
|
836
638
|
unconditionalRoutes.set(key, item);
|
|
837
639
|
}
|
|
838
640
|
if (new Set(definition.outputs).size !== definition.outputs.length) {
|
|
839
|
-
throw new
|
|
641
|
+
throw new TandemError("Pipeline outputs must contain each terminal exactly once.");
|
|
840
642
|
}
|
|
841
643
|
for (const item of definition.outputs) {
|
|
842
644
|
if (!members.has(item)) {
|
|
843
|
-
throw new
|
|
645
|
+
throw new TandemError(`Output '${item.id}' must be the registered participant object.`);
|
|
844
646
|
}
|
|
845
647
|
}
|
|
846
648
|
const reachable = new Set([start]);
|
|
847
649
|
const pending = [start];
|
|
848
|
-
|
|
849
|
-
const source = pending.pop();
|
|
650
|
+
for (let source = pending.pop(); source !== undefined; source = pending.pop()) {
|
|
850
651
|
for (const item of definition.routes) {
|
|
851
652
|
if (item.from === source && !reachable.has(item.to)) {
|
|
852
653
|
reachable.add(item.to);
|
|
@@ -857,12 +658,12 @@ export function pipeline(definition) {
|
|
|
857
658
|
const outputs = new Set(definition.outputs);
|
|
858
659
|
for (const node of reachable) {
|
|
859
660
|
if (node.kind === "terminal" && !outputs.has(node)) {
|
|
860
|
-
throw new
|
|
661
|
+
throw new TandemError(`Reachable terminal '${node.id}' must be listed in outputs.`);
|
|
861
662
|
}
|
|
862
663
|
}
|
|
863
664
|
for (const item of definition.outputs) {
|
|
864
665
|
if (!reachable.has(item)) {
|
|
865
|
-
throw new
|
|
666
|
+
throw new TandemError(`Output '${item.id}' must be reachable from start '${start.id}'.`);
|
|
866
667
|
}
|
|
867
668
|
}
|
|
868
669
|
return { ...definition, persist: definition.persist ?? false };
|
|
@@ -870,65 +671,61 @@ export function pipeline(definition) {
|
|
|
870
671
|
/** Projects an instantiated pipeline without compiling, registering, or invoking callbacks. */
|
|
871
672
|
export function inspectPipeline(graph) {
|
|
872
673
|
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]) => ({
|
|
674
|
+
const record = nodeRecord(node);
|
|
675
|
+
const { id, persist } = record;
|
|
676
|
+
switch (record.kind) {
|
|
677
|
+
case "stage":
|
|
678
|
+
return { id, kind: "stage", persist };
|
|
679
|
+
case "terminal":
|
|
680
|
+
return { id, kind: record.failed ? "failure" : "completion", persist };
|
|
681
|
+
case "collection":
|
|
682
|
+
return {
|
|
922
683
|
id,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
684
|
+
kind: "collection",
|
|
685
|
+
persist,
|
|
686
|
+
max: record.max,
|
|
687
|
+
agents: record.agents.map((agent) => `${id}/${agent.id}`),
|
|
688
|
+
};
|
|
689
|
+
case "interaction":
|
|
690
|
+
return {
|
|
691
|
+
id,
|
|
692
|
+
kind: "interaction",
|
|
693
|
+
persist,
|
|
694
|
+
interaction: {
|
|
695
|
+
requestSchema: z.toJSONSchema(record.requestSchema, { io: "input" }),
|
|
696
|
+
responseSchema: z.toJSONSchema(record.responseSchema, { io: "input" }),
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
case "agent":
|
|
700
|
+
return {
|
|
701
|
+
id,
|
|
702
|
+
kind: "agent",
|
|
703
|
+
persist,
|
|
704
|
+
agent: {
|
|
705
|
+
capabilities: (record.capabilities ?? []).map((item) => ({
|
|
706
|
+
name: item.name,
|
|
707
|
+
requestSchema: JSON.parse(item.requestJsonSchema),
|
|
708
|
+
})),
|
|
709
|
+
...(!record.output || "raw" in record.output
|
|
710
|
+
? {}
|
|
711
|
+
: { outputSchema: z.toJSONSchema(record.output.schema, { io: "input" }) }),
|
|
712
|
+
workspace: record.workspace !== undefined,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
case "parallel":
|
|
716
|
+
return {
|
|
717
|
+
id,
|
|
718
|
+
kind: "parallel",
|
|
719
|
+
persist,
|
|
720
|
+
...(record.max !== undefined ? { max: record.max } : {}),
|
|
721
|
+
branches: Object.entries(record.branches).map(([branchId, participant]) => ({
|
|
722
|
+
id: branchId,
|
|
723
|
+
participant: inspectNode(participant),
|
|
724
|
+
})),
|
|
725
|
+
};
|
|
726
|
+
default:
|
|
727
|
+
return record;
|
|
930
728
|
}
|
|
931
|
-
return { id: node.id, kind: "stage", persist };
|
|
932
729
|
};
|
|
933
730
|
return {
|
|
934
731
|
name: graph.name,
|
|
@@ -948,123 +745,86 @@ export function inspectPipeline(graph) {
|
|
|
948
745
|
outputs: graph.outputs.map((item) => item.id),
|
|
949
746
|
};
|
|
950
747
|
}
|
|
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
|
-
.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
})
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
stepId: z.string().min(1),
|
|
1008
|
-
visitId: z.string().nullable().optional(),
|
|
1009
|
-
text: z.string(),
|
|
1010
|
-
})
|
|
1011
|
-
.strict(),
|
|
748
|
+
const observationBase = z.object({
|
|
749
|
+
version: z.literal(1),
|
|
750
|
+
stepId: z.string().min(1),
|
|
751
|
+
visitId: z.string().nullable().optional(),
|
|
752
|
+
});
|
|
753
|
+
const observation = (kind, shape) => observationBase.extend({ kind: z.literal(kind), ...shape }).strict();
|
|
754
|
+
const tokenCount = z.number().int().nonnegative();
|
|
755
|
+
const runObservationSchema = z
|
|
756
|
+
.discriminatedUnion("kind", [
|
|
757
|
+
observation("stepStarted", {}),
|
|
758
|
+
observation("stepCompleted", {}),
|
|
759
|
+
observation("stepCancelled", {}),
|
|
760
|
+
observation("stepFaulted", { error: z.string() }),
|
|
761
|
+
observation("agentText", { text: z.string() }),
|
|
762
|
+
observation("agentReasoning", { text: z.string() }),
|
|
763
|
+
observation("agentModelSelected", { modelId: z.string().min(1) }),
|
|
764
|
+
observation("agentUsage", {
|
|
765
|
+
inputTokens: tokenCount,
|
|
766
|
+
outputTokens: tokenCount,
|
|
767
|
+
reasoningTokens: tokenCount,
|
|
768
|
+
currentContextTokens: tokenCount,
|
|
769
|
+
contextWindowTokens: tokenCount.nullable(),
|
|
770
|
+
}),
|
|
771
|
+
observation("structuredOutputRejected", {
|
|
772
|
+
attempt: z.number().int().positive(),
|
|
773
|
+
problems: z
|
|
774
|
+
.array(z
|
|
775
|
+
.object({ field: z.string(), message: z.string().min(1) })
|
|
776
|
+
.strict()
|
|
777
|
+
.readonly())
|
|
778
|
+
.readonly(),
|
|
779
|
+
rawResponse: z.string(),
|
|
780
|
+
}),
|
|
781
|
+
])
|
|
782
|
+
.readonly();
|
|
783
|
+
const acceptedValueFields = z
|
|
784
|
+
.object({
|
|
785
|
+
kind: z.enum([
|
|
786
|
+
"StructuredOutputAccepted",
|
|
787
|
+
"CapabilityAccepted",
|
|
788
|
+
"InteractionRequested",
|
|
789
|
+
"InteractionAnswered",
|
|
790
|
+
"StepCompleted",
|
|
791
|
+
]),
|
|
792
|
+
stepId: z.string().min(1),
|
|
793
|
+
visitId: z.string().nullable().optional(),
|
|
794
|
+
valueType: z.string().min(1).nullable(),
|
|
795
|
+
payload: z.unknown(),
|
|
796
|
+
})
|
|
797
|
+
.strict();
|
|
798
|
+
const acceptedValuesSchema = z.array(acceptedValueFields.refine((value) => value.valueType !== null || value.payload !== null, {
|
|
799
|
+
message: "valueType and payload cannot both be null",
|
|
800
|
+
}));
|
|
801
|
+
const validationProblemsSchema = z.array(z.object({ path: z.string(), message: z.string() }).strict());
|
|
802
|
+
/** How the bridge reports the end of a run; only a bridge defect rejects instead. */
|
|
803
|
+
const runEnvelopeSchema = z.discriminatedUnion("status", [
|
|
1012
804
|
z
|
|
1013
805
|
.object({
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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(),
|
|
806
|
+
status: z.enum(["succeeded", "failed"]),
|
|
807
|
+
runId: z.uuid(),
|
|
808
|
+
state: z.unknown(),
|
|
809
|
+
summary: z.string().nullable(),
|
|
1023
810
|
})
|
|
1024
811
|
.strict(),
|
|
812
|
+
z.object({ status: z.literal("cancelled"), runId: z.uuid() }).strict(),
|
|
1025
813
|
z
|
|
1026
814
|
.object({
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
attempt: z.number().int().positive(),
|
|
1032
|
-
problems: z.array(z.object({ field: z.string(), message: z.string().min(1) }).strict()),
|
|
1033
|
-
rawResponse: z.string(),
|
|
815
|
+
status: z.literal("contract"),
|
|
816
|
+
runId: z.uuid(),
|
|
817
|
+
boundary: z.string(),
|
|
818
|
+
problems: validationProblemsSchema,
|
|
1034
819
|
})
|
|
1035
820
|
.strict(),
|
|
821
|
+
z.object({ status: z.literal("faulted"), runId: z.uuid(), message: z.string() }).strict(),
|
|
1036
822
|
]);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
"CapabilityAccepted",
|
|
1040
|
-
"InteractionRequested",
|
|
1041
|
-
"InteractionAnswered",
|
|
1042
|
-
"StepCompleted",
|
|
1043
|
-
];
|
|
1044
|
-
const acceptedValueSchema = z
|
|
1045
|
-
.object({
|
|
1046
|
-
kind: z.enum(acceptedKinds),
|
|
1047
|
-
visitId: z.string().nullable().optional(),
|
|
1048
|
-
stepId: z.string().min(1),
|
|
1049
|
-
valueType: z.string().min(1).nullable(),
|
|
1050
|
-
payload: z.unknown().nullable(),
|
|
1051
|
-
})
|
|
1052
|
-
.strict()
|
|
1053
|
-
.refine((value) => value.valueType !== null || value.payload !== null, {
|
|
1054
|
-
message: "valueType and payload cannot both be null",
|
|
1055
|
-
});
|
|
1056
|
-
const acceptedValuesSchema = z.array(acceptedValueSchema);
|
|
1057
|
-
const runResultSchema = z
|
|
1058
|
-
.object({
|
|
1059
|
-
runId: z.uuid(),
|
|
1060
|
-
succeeded: z.boolean(),
|
|
1061
|
-
state: z.unknown(),
|
|
1062
|
-
summary: z.string().nullable(),
|
|
1063
|
-
})
|
|
1064
|
-
.strict();
|
|
823
|
+
/** Lazy (and cached by the module loader) so importing the authoring API never starts .NET. */
|
|
824
|
+
const loadRuntime = () => import("./runtime/loader.mjs");
|
|
1065
825
|
export async function inspectAccepted(options) {
|
|
1066
826
|
try {
|
|
1067
|
-
const { inspectAcceptedAsync } = await
|
|
827
|
+
const { inspectAcceptedAsync } = await loadRuntime();
|
|
1068
828
|
return parseJson(acceptedValuesSchema, await inspectAcceptedAsync(options.ledgerPath, options.runId), "accepted values").map((value) => ({
|
|
1069
829
|
version: 1,
|
|
1070
830
|
kind: value.kind,
|
|
@@ -1089,33 +849,31 @@ export async function run(graph, initial, options = {}) {
|
|
|
1089
849
|
const callbacks = new CallbackRegistry();
|
|
1090
850
|
try {
|
|
1091
851
|
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
|
-
});
|
|
852
|
+
const routes = graph.routes.map(({ from, to, label, outcome, when }) => ({
|
|
853
|
+
source: from.id,
|
|
854
|
+
target: to.id,
|
|
855
|
+
label,
|
|
856
|
+
outcome,
|
|
857
|
+
predicateCallback: when
|
|
858
|
+
? callbacks.registerSync((state) => String(when(parseJson(graph.state, state, `route '${label}' state`))))
|
|
859
|
+
: undefined,
|
|
860
|
+
}));
|
|
1104
861
|
const handlerEntries = interactionHandlerEntries(options.interactions);
|
|
1105
862
|
const members = new Set(graph.nodes);
|
|
1106
863
|
const interactionHandlers = handlerEntries.map((entry, index) => {
|
|
1107
864
|
if (!members.has(entry.interaction)) {
|
|
1108
865
|
throw new TandemError(`Interaction handler '${entry.interaction.id}' must target a participant in pipeline '${graph.name}'.`);
|
|
1109
866
|
}
|
|
1110
|
-
const
|
|
867
|
+
const { id, requestSchema, responseSchema } = entry.interaction;
|
|
1111
868
|
const handleCallback = callbacks.registerAsync(async (_, input, signal) => {
|
|
1112
|
-
const response = await entry.handle(parseJson(
|
|
869
|
+
const response = await entry.handle(parseJson(requestSchema, input, `${id} request input`), { signal });
|
|
1113
870
|
signal.throwIfAborted();
|
|
1114
|
-
return serializeBoundary(
|
|
871
|
+
return serializeBoundary(responseSchema, response, `${id} response`);
|
|
1115
872
|
});
|
|
1116
873
|
return { id: `h${index}`, target: entry.interaction.id, handleCallback };
|
|
1117
874
|
});
|
|
1118
|
-
const
|
|
875
|
+
const { observe } = options;
|
|
876
|
+
const observationCallback = observe
|
|
1119
877
|
? callbacks.registerAsync(async (_, input, signal) => {
|
|
1120
878
|
const event = parseJson(runObservationSchema, input, "run observation");
|
|
1121
879
|
const observationSignal = options.signal?.aborted === true
|
|
@@ -1123,11 +881,11 @@ export async function run(graph, initial, options = {}) {
|
|
|
1123
881
|
: event.kind === "stepCancelled" && !signal.aborted
|
|
1124
882
|
? AbortSignal.abort()
|
|
1125
883
|
: signal;
|
|
1126
|
-
await
|
|
884
|
+
await observe(event, { signal: observationSignal });
|
|
1127
885
|
return "";
|
|
1128
886
|
})
|
|
1129
887
|
: undefined;
|
|
1130
|
-
const { runRegisteredGraphAsync } = await
|
|
888
|
+
const { runRegisteredGraphAsync } = await loadRuntime();
|
|
1131
889
|
const resultJson = await runRegisteredGraphAsync(JSON.stringify({
|
|
1132
890
|
contractVersion: 10,
|
|
1133
891
|
name: graph.name,
|
|
@@ -1143,21 +901,34 @@ export async function run(graph, initial, options = {}) {
|
|
|
1143
901
|
routes,
|
|
1144
902
|
outputs: graph.outputs.map((item) => item.id),
|
|
1145
903
|
interactionHandlers,
|
|
1146
|
-
}), (id, state, input) =>
|
|
1147
|
-
|
|
1148
|
-
|
|
904
|
+
}), (id, state, input) => {
|
|
905
|
+
const result = callbacks.invoke(id, state, input, neverAborted);
|
|
906
|
+
return typeof result === "string"
|
|
907
|
+
? result
|
|
908
|
+
: callbackFailed(new Error(`Internal callback '${id}' is asynchronous.`));
|
|
909
|
+
}, async (id, state, input, signal) => callbacks.invoke(id, state, input, signal), options.signal);
|
|
910
|
+
const envelope = parseJson(runEnvelopeSchema, resultJson, "run result");
|
|
911
|
+
switch (envelope.status) {
|
|
912
|
+
case "succeeded":
|
|
913
|
+
case "failed":
|
|
914
|
+
return {
|
|
915
|
+
runId: envelope.runId,
|
|
916
|
+
succeeded: envelope.status === "succeeded",
|
|
917
|
+
state: parse(graph.state, envelope.state, "final state"),
|
|
918
|
+
summary: envelope.summary,
|
|
919
|
+
};
|
|
920
|
+
case "cancelled":
|
|
921
|
+
throw new TandemCancellationError(new Error("The run was cancelled."));
|
|
922
|
+
case "contract":
|
|
923
|
+
throw new ContractValidationError(envelope.boundary, envelope.problems);
|
|
924
|
+
case "faulted":
|
|
925
|
+
throw new TandemRuntimeError("run", new Error(envelope.message));
|
|
926
|
+
}
|
|
1149
927
|
}
|
|
1150
928
|
catch (error) {
|
|
1151
929
|
if (error instanceof TandemError) {
|
|
1152
930
|
throw error;
|
|
1153
931
|
}
|
|
1154
|
-
const callbackFailure = callbackContractFailure(error);
|
|
1155
|
-
if (callbackFailure) {
|
|
1156
|
-
throw new ContractValidationError(callbackFailure.boundary, callbackFailure.problems);
|
|
1157
|
-
}
|
|
1158
|
-
if (isCancellationError(error, options.signal?.aborted === true)) {
|
|
1159
|
-
throw new TandemCancellationError(error);
|
|
1160
|
-
}
|
|
1161
932
|
throw new TandemRuntimeError("run", error);
|
|
1162
933
|
}
|
|
1163
934
|
finally {
|
|
@@ -1165,27 +936,13 @@ export async function run(graph, initial, options = {}) {
|
|
|
1165
936
|
}
|
|
1166
937
|
}
|
|
1167
938
|
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;
|
|
939
|
+
const record = nodeRecord(node);
|
|
940
|
+
return (record.persist === true ||
|
|
941
|
+
(record.kind === "parallel" && Object.values(record.branches).some(participantPersists)));
|
|
1175
942
|
}
|
|
1176
943
|
function issues(schema, input) {
|
|
1177
|
-
let value;
|
|
1178
944
|
try {
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
catch {
|
|
1182
|
-
return JSON.stringify([{ path: "$", message: "Invalid JSON" }]);
|
|
1183
|
-
}
|
|
1184
|
-
return issuesParsed(schema, value);
|
|
1185
|
-
}
|
|
1186
|
-
function issuesParsed(schema, value) {
|
|
1187
|
-
try {
|
|
1188
|
-
parse(schema, value, "agent contract");
|
|
945
|
+
parseJson(schema, input, "agent contract");
|
|
1189
946
|
return "";
|
|
1190
947
|
}
|
|
1191
948
|
catch (error) {
|
|
@@ -1195,7 +952,6 @@ function issuesParsed(schema, value) {
|
|
|
1195
952
|
throw error;
|
|
1196
953
|
}
|
|
1197
954
|
}
|
|
1198
|
-
const validationProblemsSchema = z.array(z.object({ path: z.string(), message: z.string() }).strict());
|
|
1199
955
|
function validationProblems(problems, boundary) {
|
|
1200
956
|
return JSON.stringify(parse(validationProblemsSchema, problems, boundary));
|
|
1201
957
|
}
|
|
@@ -1206,180 +962,182 @@ function requireInstructions(instructions, boundary) {
|
|
|
1206
962
|
]);
|
|
1207
963
|
}
|
|
1208
964
|
}
|
|
965
|
+
/** Authoring errors read as "<subject> <field path> <problem>", e.g. "Agent 'a' checkpoint session must be ...". */
|
|
966
|
+
function parseDefinition(schema, value, subject) {
|
|
967
|
+
const result = schema.safeParse(value);
|
|
968
|
+
if (result.success) {
|
|
969
|
+
return result.data;
|
|
970
|
+
}
|
|
971
|
+
const problems = result.error.issues.map((issue) => {
|
|
972
|
+
const field = issue.path
|
|
973
|
+
.map((part) => (typeof part === "number" ? `[${part}]` : ` ${String(part)}`))
|
|
974
|
+
.join("");
|
|
975
|
+
return `${field} ${issue.message}`;
|
|
976
|
+
});
|
|
977
|
+
throw new TandemError(`${subject}${problems.join(";")}.`);
|
|
978
|
+
}
|
|
1209
979
|
function interactionHandlerEntries(handlers) {
|
|
1210
980
|
if (!handlers) {
|
|
1211
981
|
return [];
|
|
1212
982
|
}
|
|
1213
|
-
|
|
983
|
+
const entries = registeredHandlers.get(handlers);
|
|
984
|
+
if (!entries) {
|
|
1214
985
|
throw new TandemError("interactions must be created by interactions().");
|
|
1215
986
|
}
|
|
1216
|
-
return
|
|
987
|
+
return entries;
|
|
1217
988
|
}
|
|
1218
989
|
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
|
-
},
|
|
990
|
+
const record = nodeRecord(node);
|
|
991
|
+
const { id } = record;
|
|
992
|
+
const base = { id, persist: record.persist };
|
|
993
|
+
const parseState = (state, boundary) => parseJson(stateSchema, state, `${id} ${boundary}`);
|
|
994
|
+
switch (record.kind) {
|
|
995
|
+
case "stage":
|
|
996
|
+
return {
|
|
997
|
+
...base,
|
|
998
|
+
kind: "stage",
|
|
999
|
+
runCallback: callbacks.registerAsync(async (state, _, signal) => serializeBoundary(stateSchema, await record.execute(parseState(state, "input"), { signal }), `${id} output`)),
|
|
1256
1000
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1001
|
+
case "terminal":
|
|
1002
|
+
return {
|
|
1003
|
+
...base,
|
|
1004
|
+
kind: record.failed ? "failure" : "completion",
|
|
1005
|
+
summaryCallback: callbacks.registerSync((state) => record.summary(parseState(state, "state"))),
|
|
1006
|
+
};
|
|
1007
|
+
case "interaction":
|
|
1008
|
+
return {
|
|
1009
|
+
...base,
|
|
1010
|
+
kind: "interaction",
|
|
1011
|
+
requestCallback: callbacks.registerSync((state) => serializeBoundary(record.requestSchema, record.request(parseState(state, "state")), `${id} request`)),
|
|
1012
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, record.apply(parseState(state, "state"), parseJson(record.responseSchema, input, `${id} response input`)), `${id} applied state`)),
|
|
1013
|
+
};
|
|
1014
|
+
case "collection":
|
|
1015
|
+
return compileCollection(record, stateSchema, callbacks);
|
|
1016
|
+
case "agent":
|
|
1017
|
+
return compileAgent(record, stateSchema, callbacks);
|
|
1018
|
+
case "parallel": {
|
|
1019
|
+
const branches = Object.entries(record.branches);
|
|
1020
|
+
const branchStates = z.strictObject(Object.fromEntries(branches.map(([branchId]) => [branchId, stateSchema])));
|
|
1021
|
+
return {
|
|
1022
|
+
...base,
|
|
1023
|
+
kind: "parallel",
|
|
1024
|
+
...(record.max !== undefined ? { max: record.max } : {}),
|
|
1025
|
+
branches: branches.map(([branchId, participant]) => ({
|
|
1026
|
+
id: branchId,
|
|
1027
|
+
participant: compileNode(participant, stateSchema, callbacks),
|
|
1028
|
+
})),
|
|
1029
|
+
mergeCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, record.merge(parseState(state, "merge baseline"), parseJson(branchStates, input, `${id} merge branches`)), `${id} merged state`)),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
default:
|
|
1033
|
+
return record;
|
|
1278
1034
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1035
|
+
}
|
|
1036
|
+
function compileCollection(collection, stateSchema, callbacks) {
|
|
1037
|
+
const { id } = collection;
|
|
1038
|
+
const agents = collection.agents.map((reference) => {
|
|
1039
|
+
const task = taskAgentRecord(reference);
|
|
1040
|
+
return compileNode(task.participant, task.state, callbacks);
|
|
1041
|
+
});
|
|
1042
|
+
const itemsCallback = callbacks.registerSync((state) => serializeBoundary(z.array(collection.item), collection.items(parseJson(stateSchema, state, `${id} state`)), `${id} items`));
|
|
1043
|
+
const runCallback = callbacks.registerAsync(async (item, scopeId, signal) => {
|
|
1044
|
+
let active = false;
|
|
1045
|
+
let closed = false;
|
|
1046
|
+
const context = {
|
|
1281
1047
|
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,
|
|
1048
|
+
async run(value, input) {
|
|
1049
|
+
if (closed)
|
|
1050
|
+
throw new TandemError("Collection scope has ended.");
|
|
1051
|
+
if (active)
|
|
1052
|
+
throw new TandemError("Collection item agents must be awaited serially.");
|
|
1053
|
+
if (!collection.agents.includes(value)) {
|
|
1054
|
+
throw new TandemError("Agent is not declared in this collection.");
|
|
1327
1055
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1056
|
+
const task = taskAgentRecord(value);
|
|
1057
|
+
signal.throwIfAborted();
|
|
1058
|
+
active = true;
|
|
1059
|
+
try {
|
|
1060
|
+
const { runCollectionAgentAsync } = await loadRuntime();
|
|
1061
|
+
const response = await runCollectionAgentAsync(scopeId, task.id, serializeBoundary(task.state, { input, output: null }, `${task.id} input`));
|
|
1062
|
+
const state = parseJson(task.state, response, `${task.id} output`);
|
|
1063
|
+
if (state.output === null)
|
|
1064
|
+
throw new TandemError("Agent returned no output.");
|
|
1065
|
+
return parse(task.result, state.output.value, `${task.id} result`);
|
|
1066
|
+
}
|
|
1067
|
+
finally {
|
|
1068
|
+
active = false;
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1331
1071
|
};
|
|
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
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
const result = await collection.execute(parseJson(collection.item, item, `${id} item`), context);
|
|
1074
|
+
if (active)
|
|
1075
|
+
throw new TandemError("Collection item returned before its agent completed.");
|
|
1076
|
+
signal.throwIfAborted();
|
|
1077
|
+
return serializeBoundary(collection.result, result, `${id} result`);
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
closed = true;
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
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`));
|
|
1084
|
+
return {
|
|
1085
|
+
id,
|
|
1086
|
+
persist: collection.persist,
|
|
1087
|
+
kind: "collection",
|
|
1088
|
+
agents,
|
|
1089
|
+
max: collection.max,
|
|
1090
|
+
itemsCallback,
|
|
1091
|
+
runCallback,
|
|
1092
|
+
applyCallback,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function compileAgent(agent, stateSchema, callbacks) {
|
|
1096
|
+
const { id, checkpoint } = agent;
|
|
1097
|
+
const message = callbacks.registerSync((state) => agent.message(parseJson(stateSchema, state, `${id} message state`)));
|
|
1098
|
+
const output = agent.output
|
|
1099
|
+
? compileAgentOutput(id, agent.output, stateSchema, callbacks)
|
|
1100
|
+
: undefined;
|
|
1101
|
+
const capabilities = (agent.capabilities ?? []).map((item) => item[compileCapabilityBrand]({ id, stateSchema, callbacks }));
|
|
1102
|
+
const workspace = agent.workspace
|
|
1103
|
+
? compileWorkspace(id, agent.workspace, stateSchema, callbacks)
|
|
1104
|
+
: undefined;
|
|
1105
|
+
return {
|
|
1106
|
+
id,
|
|
1107
|
+
persist: agent.persist,
|
|
1108
|
+
kind: "agent",
|
|
1109
|
+
instructions: agent.instructions,
|
|
1110
|
+
client: { ...agent.client, verifyModel: agent.client.verifyModel ?? false },
|
|
1111
|
+
messageCallback: message,
|
|
1112
|
+
output,
|
|
1113
|
+
capabilities,
|
|
1114
|
+
skillDirectories: (agent.skills ?? []).map((item) => item.directory),
|
|
1115
|
+
temperature: agent.temperature,
|
|
1116
|
+
maxOutputTokens: agent.maxOutputTokens,
|
|
1117
|
+
reasoning: agent.reasoning,
|
|
1118
|
+
continueSession: agent.continueSession ?? false,
|
|
1119
|
+
checkpoint: checkpoint
|
|
1120
|
+
? {
|
|
1121
|
+
contextWindowTokens: checkpoint.contextWindowTokens,
|
|
1122
|
+
maxOutputTokens: checkpoint.maxOutputTokens,
|
|
1123
|
+
checkpointAtPercent: checkpoint.checkpointAtPercent,
|
|
1124
|
+
capabilityName: checkpoint.capability.name,
|
|
1125
|
+
instructions: checkpoint.instructions,
|
|
1126
|
+
messageCallback: callbacks.registerSync((state, input) => checkpoint.message(parseJson(stateSchema, state, `${id} checkpoint state`), Number(input))),
|
|
1127
|
+
resetSession: (checkpoint.session ?? "reset") === "reset",
|
|
1128
|
+
disableCompaction: checkpoint.disableCompaction ?? false,
|
|
1357
1129
|
}
|
|
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 };
|
|
1130
|
+
: undefined,
|
|
1131
|
+
timeoutMilliseconds: agent.timeoutMs,
|
|
1132
|
+
workspace,
|
|
1133
|
+
};
|
|
1378
1134
|
}
|
|
1379
|
-
function compileWorkspace(id,
|
|
1380
|
-
if (!(
|
|
1135
|
+
function compileWorkspace(id, authored, stateSchema, callbacks) {
|
|
1136
|
+
if (!("groups" in authored)) {
|
|
1381
1137
|
throw new TandemError(`Agent '${id}' workspace must be created by agentWorkspace().withTools().`);
|
|
1382
1138
|
}
|
|
1139
|
+
// A workspace configuration is the record `withTools()` returned.
|
|
1140
|
+
const configuration = authored;
|
|
1383
1141
|
const workspace = configuration.workspace;
|
|
1384
1142
|
const pathCallback = callbacks.registerSync((state) => {
|
|
1385
1143
|
const value = workspace.path(parseJson(stateSchema, state, `${id} workspace path state`));
|
|
@@ -1390,18 +1148,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1390
1148
|
});
|
|
1391
1149
|
const commandsCallback = callbacks.registerSync((state) => {
|
|
1392
1150
|
const typedState = parseJson(stateSchema, state, `${id} workspace commands state`);
|
|
1393
|
-
const
|
|
1394
|
-
? workspace.commandSource(typedState)
|
|
1151
|
+
const commands = typeof workspace.commandSource === "function"
|
|
1152
|
+
? parseDefinition(agentCommandsSchema, workspace.commandSource(typedState), `Agent '${id}' workspace commands`)
|
|
1395
1153
|
: (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`);
|
|
1154
|
+
return JSON.stringify(commands);
|
|
1405
1155
|
});
|
|
1406
1156
|
const selected = new Set();
|
|
1407
1157
|
const toolGroups = configuration.groups.map((group, index) => {
|
|
@@ -1429,9 +1179,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1429
1179
|
includeCommands = true;
|
|
1430
1180
|
}
|
|
1431
1181
|
}
|
|
1432
|
-
const
|
|
1182
|
+
const { predicate } = group;
|
|
1183
|
+
const whenCallback = predicate
|
|
1433
1184
|
? callbacks.registerSync((state) => {
|
|
1434
|
-
const value =
|
|
1185
|
+
const value = predicate(parseJson(stateSchema, state, `${id} tool group ${index} state`));
|
|
1435
1186
|
if (typeof value !== "boolean") {
|
|
1436
1187
|
throw new TandemError(`Agent '${id}' tool group ${index} predicate must return a boolean.`);
|
|
1437
1188
|
}
|
|
@@ -1440,17 +1191,10 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1440
1191
|
: undefined;
|
|
1441
1192
|
return { tools, includeCommands, whenCallback };
|
|
1442
1193
|
});
|
|
1443
|
-
const
|
|
1194
|
+
const { interceptTool } = configuration;
|
|
1195
|
+
const interceptCallback = interceptTool
|
|
1444
1196
|
? 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 });
|
|
1197
|
+
const result = await interceptTool(parseJson(stateSchema, state, `${id} tool interception state`), parseJson(agentToolInvocationSchema, input, `${id} tool interception payload`), { signal });
|
|
1454
1198
|
if (result !== null && typeof result !== "string") {
|
|
1455
1199
|
throw new TandemError(`Agent '${id}' tool interceptor must return a string or null.`);
|
|
1456
1200
|
}
|
|
@@ -1460,27 +1204,34 @@ function compileWorkspace(id, configuration, stateSchema, callbacks) {
|
|
|
1460
1204
|
return { pathCallback, commandsCallback, toolGroups, interceptCallback };
|
|
1461
1205
|
}
|
|
1462
1206
|
function compileAgentOutput(id, output, stateSchema, callbacks) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1207
|
+
let decode;
|
|
1208
|
+
let format;
|
|
1209
|
+
if ("raw" in output) {
|
|
1210
|
+
const { parse } = output;
|
|
1211
|
+
// The bridge hands back exactly the JSON that rawParseCallback produced from `parse`.
|
|
1212
|
+
decode = (input) => JSON.parse(input);
|
|
1213
|
+
format = {
|
|
1214
|
+
raw: true,
|
|
1215
|
+
rawParseCallback: callbacks.registerSync((_, input) => JSON.stringify(parse(input))),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
const { schema } = output;
|
|
1220
|
+
decode = (input) => parseJson(schema, input, `${id} output`);
|
|
1221
|
+
format = {
|
|
1222
|
+
jsonSchema: inputJsonSchema(schema, `${id} output schema`),
|
|
1223
|
+
validateCallback: callbacks.registerSync((_, input) => issues(schema, input)),
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const { validateFor, apply } = output;
|
|
1227
|
+
const parseState = (state) => parseJson(stateSchema, state, `${id} state`);
|
|
1471
1228
|
return {
|
|
1472
1229
|
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,
|
|
1230
|
+
...format,
|
|
1231
|
+
validateForCallback: validateFor
|
|
1232
|
+
? callbacks.registerSync((state, input) => validationProblems(validateFor(parseState(state), decode(input)), `${id} output contextual validation`))
|
|
1233
|
+
: undefined,
|
|
1234
|
+
applyCallback: callbacks.registerSync((state, input) => serializeBoundary(stateSchema, apply(parseState(state), decode(input)), `${id} applied state`)),
|
|
1484
1235
|
valueType: `${id}.output`,
|
|
1485
1236
|
};
|
|
1486
1237
|
}
|