@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.
Files changed (70) hide show
  1. package/dist/index.d.ts +86 -72
  2. package/dist/index.js +673 -922
  3. package/dist/runtime/darwin-arm64/Dapper.dll +0 -0
  4. package/dist/runtime/darwin-arm64/Spectre.Console.Json.dll +0 -0
  5. package/dist/runtime/darwin-arm64/Tandem.Advanced.dll +0 -0
  6. package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.cjs → Tandem.Bridge.cjs} +1 -1
  7. package/dist/runtime/{linux-x64/Tandem.NodeApiSpike.Bridge.d.ts → darwin-arm64/Tandem.Bridge.d.ts} +8 -2
  8. package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.deps.json → Tandem.Bridge.deps.json} +93 -144
  9. package/dist/runtime/darwin-arm64/Tandem.Bridge.dll +0 -0
  10. package/dist/runtime/{linux-x64/Tandem.NodeApiSpike.Bridge.mjs → darwin-arm64/Tandem.Bridge.mjs} +1 -1
  11. package/dist/runtime/darwin-arm64/Tandem.Ledger.dll +0 -0
  12. package/dist/runtime/darwin-arm64/Tandem.OpenAICompatible.dll +0 -0
  13. package/dist/runtime/darwin-arm64/Tandem.Terminal.dll +0 -0
  14. package/dist/runtime/darwin-arm64/Tandem.dll +0 -0
  15. package/dist/runtime/linux-x64/Dapper.dll +0 -0
  16. package/dist/runtime/linux-x64/Spectre.Console.Json.dll +0 -0
  17. package/dist/runtime/linux-x64/Tandem.Advanced.dll +0 -0
  18. package/dist/runtime/linux-x64/{Tandem.NodeApiSpike.Bridge.cjs → Tandem.Bridge.cjs} +1 -1
  19. package/dist/runtime/{darwin-arm64/Tandem.NodeApiSpike.Bridge.d.ts → linux-x64/Tandem.Bridge.d.ts} +8 -2
  20. package/dist/runtime/linux-x64/{Tandem.NodeApiSpike.Bridge.deps.json → Tandem.Bridge.deps.json} +93 -144
  21. package/dist/runtime/linux-x64/Tandem.Bridge.dll +0 -0
  22. package/dist/runtime/{darwin-arm64/Tandem.NodeApiSpike.Bridge.mjs → linux-x64/Tandem.Bridge.mjs} +1 -1
  23. package/dist/runtime/linux-x64/Tandem.Ledger.dll +0 -0
  24. package/dist/runtime/linux-x64/Tandem.OpenAICompatible.dll +0 -0
  25. package/dist/runtime/linux-x64/Tandem.Terminal.dll +0 -0
  26. package/dist/runtime/linux-x64/Tandem.dll +0 -0
  27. package/dist/runtime/loader.mjs +2 -2
  28. package/package.json +3 -1
  29. package/dist/runtime/darwin-arm64/Humanizer.dll +0 -0
  30. package/dist/runtime/darwin-arm64/Json.More.dll +0 -0
  31. package/dist/runtime/darwin-arm64/JsonPointer.Net.dll +0 -0
  32. package/dist/runtime/darwin-arm64/JsonSchema.Net.Generation.dll +0 -0
  33. package/dist/runtime/darwin-arm64/JsonSchema.Net.dll +0 -0
  34. package/dist/runtime/darwin-arm64/Tandem.NodeApiSpike.Bridge.dll +0 -0
  35. package/dist/runtime/darwin-arm64/cs/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  36. package/dist/runtime/darwin-arm64/cs/Microsoft.CodeAnalysis.resources.dll +0 -0
  37. package/dist/runtime/darwin-arm64/de/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  38. package/dist/runtime/darwin-arm64/de/Microsoft.CodeAnalysis.resources.dll +0 -0
  39. package/dist/runtime/darwin-arm64/es/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  40. package/dist/runtime/darwin-arm64/es/Microsoft.CodeAnalysis.resources.dll +0 -0
  41. package/dist/runtime/darwin-arm64/fr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  42. package/dist/runtime/darwin-arm64/fr/Microsoft.CodeAnalysis.resources.dll +0 -0
  43. package/dist/runtime/darwin-arm64/import.cjs +0 -3
  44. package/dist/runtime/darwin-arm64/it/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  45. package/dist/runtime/darwin-arm64/it/Microsoft.CodeAnalysis.resources.dll +0 -0
  46. package/dist/runtime/darwin-arm64/ja/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  47. package/dist/runtime/darwin-arm64/ja/Microsoft.CodeAnalysis.resources.dll +0 -0
  48. package/dist/runtime/darwin-arm64/ko/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  49. package/dist/runtime/darwin-arm64/ko/Microsoft.CodeAnalysis.resources.dll +0 -0
  50. package/dist/runtime/darwin-arm64/pl/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  51. package/dist/runtime/darwin-arm64/pl/Microsoft.CodeAnalysis.resources.dll +0 -0
  52. package/dist/runtime/darwin-arm64/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  53. package/dist/runtime/darwin-arm64/pt-BR/Microsoft.CodeAnalysis.resources.dll +0 -0
  54. package/dist/runtime/darwin-arm64/ru/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  55. package/dist/runtime/darwin-arm64/ru/Microsoft.CodeAnalysis.resources.dll +0 -0
  56. package/dist/runtime/darwin-arm64/tr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  57. package/dist/runtime/darwin-arm64/tr/Microsoft.CodeAnalysis.resources.dll +0 -0
  58. package/dist/runtime/darwin-arm64/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  59. package/dist/runtime/darwin-arm64/zh-Hans/Microsoft.CodeAnalysis.resources.dll +0 -0
  60. package/dist/runtime/darwin-arm64/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  61. package/dist/runtime/darwin-arm64/zh-Hant/Microsoft.CodeAnalysis.resources.dll +0 -0
  62. package/dist/runtime/linux-x64/Humanizer.dll +0 -0
  63. package/dist/runtime/linux-x64/Json.More.dll +0 -0
  64. package/dist/runtime/linux-x64/JsonPointer.Net.dll +0 -0
  65. package/dist/runtime/linux-x64/JsonSchema.Net.Generation.dll +0 -0
  66. package/dist/runtime/linux-x64/JsonSchema.Net.dll +0 -0
  67. package/dist/runtime/linux-x64/Tandem.NodeApiSpike.Bridge.dll +0 -0
  68. package/dist/runtime/linux-x64/import.cjs +0 -3
  69. /package/dist/runtime/darwin-arm64/{Tandem.NodeApiSpike.Bridge.runtimeconfig.json → Tandem.Bridge.runtimeconfig.json} +0 -0
  70. /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
- function callbackError(error) {
42
- return error instanceof ContractValidationError
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
- #sync = new Map();
56
- #async = new Map();
59
+ #callbacks = new Map();
57
60
  #next = 0;
58
61
  #disposed = false;
59
62
  registerSync(callback) {
60
- const id = this.#allocate();
61
- this.#sync.set(id, callback);
62
- return id;
63
+ return this.#register(callback);
63
64
  }
64
65
  registerAsync(callback) {
65
- const id = this.#allocate();
66
- this.#async.set(id, callback);
67
- return id;
66
+ return this.#register(callback);
68
67
  }
69
- invokeSync(id, state, input) {
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.#sync.get(id);
71
+ const callback = this.#callbacks.get(id);
72
72
  if (!callback) {
73
73
  throw new Error(`Unknown internal callback '${id}'.`);
74
74
  }
75
- return JSON.stringify({
76
- succeeded: true,
77
- value: callback(state, input),
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 JSON.stringify({
82
- succeeded: false,
83
- error: callbackError(error),
84
- });
85
- }
86
- }
87
- async invokeAsync(id, state, input, signal) {
88
- try {
89
- const callback = this.#async.get(id);
90
- if (!callback) {
91
- throw new Error(`Unknown internal async callback '${id}'.`);
92
- }
93
- return JSON.stringify({
94
- succeeded: true,
95
- value: await callback(state, input, signal),
96
- });
97
- }
98
- catch (error) {
99
- return JSON.stringify({
100
- succeeded: false,
101
- error: callbackError(error),
102
- });
81
+ return callbackFailed(error);
103
82
  }
104
83
  }
105
84
  dispose() {
106
85
  this.#disposed = true;
107
- this.#sync.clear();
108
- this.#async.clear();
86
+ this.#callbacks.clear();
109
87
  }
110
- #allocate() {
88
+ #register(callback) {
111
89
  if (this.#disposed) {
112
90
  throw new Error("Callback registry has been disposed.");
113
91
  }
114
- return `c${this.#next++}`;
115
- }
116
- }
117
- function callbackContractFailure(error) {
118
- const marker = "TANDEM_CALLBACK_CONTRACT:";
119
- const message = error instanceof Error ? error.message : String(error);
120
- const start = message.indexOf(marker);
121
- if (start < 0) {
122
- return null;
123
- }
124
- try {
125
- return JSON.parse(message.slice(start + marker.length));
126
- }
127
- catch {
128
- return null;
129
- }
130
- }
131
- function isCancellationError(error, signalAborted) {
132
- if (!(error instanceof Error)) {
133
- return false;
134
- }
135
- if (error.message.includes("JavaScript callback failed:") && !signalAborted) {
136
- return false;
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
- let json;
186
- try {
187
- json = JSON.stringify(parsed);
188
- }
189
- catch (error) {
190
- throw new ContractValidationError(boundary, [
191
- {
192
- path: "$",
193
- message: `Value is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`,
194
- },
195
- ]);
196
- }
197
- if (json === undefined) {
198
- throw new ContractValidationError(boundary, [
199
- { path: "$", message: "Top-level undefined is not JSON-serializable." },
200
- ]);
201
- }
202
- const roundTripped = JSON.parse(json);
203
- if (!isDeepStrictEqual(roundTripped, parsed)) {
204
- throw new ContractValidationError(boundary, [
205
- { path: "$", message: "Value is not losslessly JSON-serializable." },
206
- ]);
207
- }
208
- return json;
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
- class NodeImplementation {
316
- id;
317
- persist;
318
- [participantBrand];
319
- constructor(id, persist) {
320
- this.id = id;
321
- this.persist = persist;
322
- }
323
- }
324
- class StageImplementation extends NodeImplementation {
325
- execute;
326
- kind = "stage";
327
- constructor(id, persist, execute) {
328
- super(id, persist);
329
- this.execute = execute;
330
- }
331
- }
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
- return new StageImplementation(definition.id, definition.persist, definition.execute);
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
- class TaskAgentImplementation {
337
- id;
338
- input;
339
- result;
340
- [taskAgentBrand] = true;
341
- state;
342
- participant;
343
- constructor(id, input, result, definition) {
344
- this.id = id;
345
- this.input = input;
346
- this.result = result;
347
- this.state = z.object({ input, output: z.object({ value: result }).nullable() });
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: definition.output.validateFor
354
- ? (state, value) => definition.output.validateFor(state.input, value)
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
- class CollectionImplementation extends NodeImplementation {
368
- definition;
369
- kind = "collection";
370
- constructor(definition) {
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
- if (!Number.isInteger(definition.max) || definition.max < 1 || definition.max > 2147483647) {
377
- throw new TandemError("Collection max must be a positive 32-bit integer.");
378
- }
379
- if (new Set(definition.agents.map((agent) => agent.id)).size !== definition.agents.length) {
380
- throw new TandemError("Collection agent IDs must be unique.");
381
- }
382
- return new CollectionImplementation(definition);
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
- return new InteractionImplementation(definition.id, definition.persist, definition.requestSchema, definition.responseSchema, definition.request, definition.apply);
402
- }
403
- class InteractionHandlersImplementation {
404
- [interactionHandlersBrand] = true;
405
- entries = [];
406
- #interactions = new Set();
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
- return new InteractionHandlersImplementation();
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
- class CapabilityImplementation {
424
- name;
425
- instructions;
426
- schema;
427
- validateFor;
428
- apply;
429
- summarize;
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: this.name,
449
- instructions: this.instructions,
450
- jsonSchema: this.requestJsonSchema,
451
- validateCallback: validate,
452
- validateForCallback: validateFor,
453
- applyCallback: apply,
454
- summaryCallback: summary,
455
- valueType: `${id}.capability.${this.name}`,
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
- export function capability(definition) {
460
- requireInstructions(definition.instructions, `Capability '${definition.name}' instructions`);
461
- return new CapabilityImplementation(definition.name, definition.instructions, definition.schema, definition.validateFor, definition.apply, definition.summarize);
462
- }
463
- class AgentToolGroupImplementation {
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
- return new AgentToolGroupImplementation(predicate, tools);
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
- if (definition.commands !== undefined && typeof definition.commands !== "function") {
537
- validateAgentCommands(definition.commands, "Workspace commands");
538
- }
539
- return new AgentWorkspaceImplementation(definition.path, definition.commands);
540
- }
541
- function copyAgentCommand(command) {
542
- return command.arguments === undefined
543
- ? { ...command }
544
- : {
545
- ...command,
546
- arguments: [...command.arguments],
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
- if (argument.length > 200) {
584
- throw new TandemError(`${argumentContext} must be at most 200 characters.`);
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
- if (typeof definition.directory !== "string" || definition.directory.trim().length === 0) {
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
- const reasoningEffort = definition.reasoning?.effort;
633
- const reasoningMaxTokens = definition.reasoning?.maxTokens;
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
- const checkpoint = definition.checkpoint;
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
- if (definition.max !== undefined &&
754
- (!Number.isInteger(definition.max) || definition.max < 1 || definition.max > 2147483647)) {
755
- throw new TandemError(`Parallel group '${definition.id}' max must be a positive 32-bit integer.`);
756
- }
757
- const entries = Object.entries(definition.branches);
758
- if (entries.length < 2) {
759
- throw new TandemError(`Parallel group '${definition.id}' requires at least two branches.`);
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
- return new TerminalImplementation(definition.id, definition.persist, definition.failed ?? false, definition.summary);
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 Error("Pipeline nodes must contain each participant object exactly once.");
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 Error("Pipeline node IDs must be unique.");
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 parallelNode = node;
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 Error(`Parallel branch participant '${participant.id}' cannot also be a parent pipeline node.`);
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 Error(`Parallel branch participant '${participant.id}' is owned more than once.`);
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 Error(`Pipeline participant ID '${participant.id}' must be globally unique.`);
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 Error(`Pipeline start '${definition.start.id}' must be the registered participant object.`);
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 Error(`Pipeline start '${start.id}' cannot be a terminal.`);
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 Error(`Route '${item.label}' endpoints must be registered participant objects.`);
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 Error(`Routes '${existing.label}' and '${item.label}' are both unconditional from '${item.from.id}'.`);
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 Error("Pipeline outputs must contain each terminal exactly once.");
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 Error(`Output '${item.id}' must be the registered participant object.`);
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
- while (pending.length > 0) {
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 Error(`Reachable terminal '${node.id}' must be listed in outputs.`);
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 Error(`Output '${item.id}' must be reachable from start '${start.id}'.`);
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 implementation = node;
874
- const persist = implementation.persist;
875
- if (implementation instanceof CollectionImplementation) {
876
- return {
877
- id: node.id,
878
- kind: "collection",
879
- persist,
880
- max: implementation.definition.max,
881
- agents: implementation.definition.agents.map((agent) => node.id + "/" + agent.id),
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
- participant: inspectNode(participant),
924
- })),
925
- };
926
- }
927
- if (node.kind === "terminal") {
928
- const terminal = implementation;
929
- return { id: node.id, kind: terminal.failed ? "failure" : "completion", persist };
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 runObservationSchema = z.discriminatedUnion("kind", [
952
- z
953
- .object({
954
- version: z.literal(1),
955
- kind: z.literal("stepStarted"),
956
- stepId: z.string().min(1),
957
- visitId: z.string().nullable().optional(),
958
- })
959
- .strict(),
960
- z
961
- .object({
962
- version: z.literal(1),
963
- kind: z.literal("stepCompleted"),
964
- stepId: z.string().min(1),
965
- visitId: z.string().nullable().optional(),
966
- })
967
- .strict(),
968
- z
969
- .object({
970
- version: z.literal(1),
971
- kind: z.literal("stepCancelled"),
972
- stepId: z.string().min(1),
973
- visitId: z.string().nullable().optional(),
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(),
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
- 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(),
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
- version: z.literal(1),
1028
- kind: z.literal("structuredOutputRejected"),
1029
- stepId: z.string().min(1),
1030
- visitId: z.string().nullable().optional(),
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
- const acceptedKinds = [
1038
- "StructuredOutputAccepted",
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 import("./runtime/loader.mjs");
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((item) => {
1093
- const callback = item.when
1094
- ? callbacks.registerSync((state) => String(item.when(parseJson(graph.state, state, `route '${item.label}' state`))))
1095
- : undefined;
1096
- return {
1097
- source: item.from.id,
1098
- target: item.to.id,
1099
- label: item.label,
1100
- outcome: item.outcome,
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 implementation = entry.interaction;
867
+ const { id, requestSchema, responseSchema } = entry.interaction;
1111
868
  const handleCallback = callbacks.registerAsync(async (_, input, signal) => {
1112
- const response = await entry.handle(parseJson(implementation.requestSchema, input, `${implementation.id} request input`), { signal });
869
+ const response = await entry.handle(parseJson(requestSchema, input, `${id} request input`), { signal });
1113
870
  signal.throwIfAborted();
1114
- return serializeBoundary(implementation.responseSchema, response, `${implementation.id} response`);
871
+ return serializeBoundary(responseSchema, response, `${id} response`);
1115
872
  });
1116
873
  return { id: `h${index}`, target: entry.interaction.id, handleCallback };
1117
874
  });
1118
- const observationCallback = options.observe
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 options.observe(event, { signal: observationSignal });
884
+ await observe(event, { signal: observationSignal });
1127
885
  return "";
1128
886
  })
1129
887
  : undefined;
1130
- const { runRegisteredGraphAsync } = await import("./runtime/loader.mjs");
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) => callbacks.invokeSync(id, state, input), (id, state, input, signal) => callbacks.invokeAsync(id, state, input, signal), options.signal);
1147
- const result = parseJson(runResultSchema, resultJson, "run result");
1148
- return { ...result, state: parse(graph.state, result.state, "final state") };
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 implementation = node;
1169
- if (implementation.persist) {
1170
- return true;
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
- 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
- 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
- if (!(handlers instanceof InteractionHandlersImplementation)) {
983
+ const entries = registeredHandlers.get(handlers);
984
+ if (!entries) {
1214
985
  throw new TandemError("interactions must be created by interactions().");
1215
986
  }
1216
- return handlers.entries;
987
+ return entries;
1217
988
  }
1218
989
  function compileNode(node, stateSchema, callbacks) {
1219
- const implementation = node;
1220
- const base = { id: node.id, persist: implementation.persist };
1221
- if (implementation instanceof CollectionImplementation) {
1222
- const definition = implementation.definition;
1223
- const agents = definition.agents.map((value) => {
1224
- if (!(value instanceof TaskAgentImplementation))
1225
- throw new TandemError("Use taskAgent to declare collection agents.");
1226
- return compileNode(value.participant, value.state, callbacks);
1227
- });
1228
- const itemsCallback = callbacks.registerSync((state) => serializeBoundary(z.array(definition.item), definition.items(parseJson(stateSchema, state, node.id + " state")), node.id + " items"));
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
- try {
1258
- const result = await definition.execute(parseJson(definition.item, item, node.id + " item"), context);
1259
- if (active)
1260
- throw new TandemError("Collection item returned before its agent completed.");
1261
- signal.throwIfAborted();
1262
- return serializeBoundary(definition.result, result, node.id + " result");
1263
- }
1264
- finally {
1265
- closed = true;
1266
- }
1267
- });
1268
- const applyCallback = callbacks.registerSync((state, results) => serializeBoundary(stateSchema, definition.apply(parseJson(stateSchema, state, node.id + " state"), parseJson(z.array(definition.result), results, node.id + " results")), node.id + " output"));
1269
- return {
1270
- ...base,
1271
- kind: "collection",
1272
- agents,
1273
- max: definition.max,
1274
- itemsCallback,
1275
- runCallback,
1276
- applyCallback,
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
- if (implementation instanceof StageImplementation) {
1280
- const run = callbacks.registerAsync(async (state, _, signal) => serializeBoundary(stateSchema, await implementation.execute(parseJson(stateSchema, state, `${node.id} input`), {
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
- }), `${node.id} output`));
1283
- return { ...base, kind: "stage", runCallback: run };
1284
- }
1285
- if (implementation instanceof InteractionImplementation) {
1286
- const request = callbacks.registerSync((state) => serializeBoundary(implementation.requestSchema, implementation.request(parseJson(stateSchema, state, `${node.id} state`)), `${node.id} request`));
1287
- const apply = callbacks.registerSync((state, input) => serializeBoundary(stateSchema, implementation.apply(parseJson(stateSchema, state, `${node.id} state`), parseJson(implementation.responseSchema, input, `${node.id} response input`)), `${node.id} applied state`));
1288
- return {
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
- : undefined,
1329
- timeoutMilliseconds: implementation.timeoutMs,
1330
- workspace,
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
- if (implementation instanceof ParallelImplementation) {
1334
- const entries = Object.entries(implementation.branches);
1335
- const branchIds = entries.map(([branchId]) => branchId);
1336
- const mergeCallback = callbacks.registerSync((state, input) => {
1337
- const baseline = parseJson(stateSchema, state, `${node.id} merge baseline`);
1338
- let raw;
1339
- try {
1340
- raw = JSON.parse(input);
1341
- }
1342
- catch {
1343
- throw new ContractValidationError(`${node.id} merge branches`, [
1344
- { path: "$", message: "Invalid JSON" },
1345
- ]);
1346
- }
1347
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1348
- throw new ContractValidationError(`${node.id} merge branches`, [
1349
- { path: "$", message: "Expected a branch-state object." },
1350
- ]);
1351
- }
1352
- const values = raw;
1353
- if (!isDeepStrictEqual(Object.keys(values).sort(), [...branchIds].sort())) {
1354
- throw new ContractValidationError(`${node.id} merge branches`, [
1355
- { path: "$", message: "Branch-state keys do not match the authored branches." },
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
- const parsed = Object.fromEntries(branchIds.map((branchId) => [
1359
- branchId,
1360
- parse(stateSchema, values[branchId], `${node.id} branch '${branchId}' state`),
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, configuration, stateSchema, callbacks) {
1380
- if (!(configuration instanceof AgentWorkspaceConfigurationImplementation)) {
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 value = typeof workspace.commandSource === "function"
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
- validateAgentCommands(value, `Agent '${id}' workspace commands`);
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 whenCallback = group.predicate
1182
+ const { predicate } = group;
1183
+ const whenCallback = predicate
1433
1184
  ? callbacks.registerSync((state) => {
1434
- const value = group.predicate(parseJson(stateSchema, state, `${id} tool group ${index} state`));
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 interceptCallback = configuration.interceptTool
1194
+ const { interceptTool } = configuration;
1195
+ const interceptCallback = interceptTool
1444
1196
  ? callbacks.registerAsync(async (state, input, signal) => {
1445
- const typedState = parseJson(stateSchema, state, `${id} tool interception state`);
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
- const raw = output.raw === true;
1464
- const validate = raw
1465
- ? callbacks.registerSync((_, input) => issuesParsed(z.unknown(), output.parse(JSON.parse(input))))
1466
- : callbacks.registerSync((_, input) => issues(output.schema, input));
1467
- const validateFor = output.validateFor
1468
- ? callbacks.registerSync((state, input) => validationProblems(output.validateFor(parseJson(stateSchema, state, `${id} state`), raw ? JSON.parse(input) : parseJson(output.schema, input, `${id} output`)), `${id} output contextual validation`))
1469
- : undefined;
1470
- const apply = callbacks.registerSync((state, input) => serializeBoundary(stateSchema, output.apply(parseJson(stateSchema, state, `${id} state`), raw ? JSON.parse(input) : parseJson(output.schema, input, `${id} output`)), `${id} applied state`));
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
- ...(raw
1474
- ? {
1475
- raw: true,
1476
- rawParseCallback: callbacks.registerSync((_, input) => JSON.stringify(output.parse(input))),
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
  }