taias 0.7.1 → 0.9.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/README.md CHANGED
@@ -31,8 +31,8 @@ npm install taias
31
31
  import { defineFlow, createTaias } from "taias";
32
32
 
33
33
  const flow = defineFlow("onboard", (flow) => {
34
- flow.step({ toolName: "scan_repo" }, { nextTool: "configure_app" });
35
- flow.step({ toolName: "configure_app" }, { nextTool: "deploy" });
34
+ flow.step({ toolName: { is: "scan_repo" } }, { nextTool: "configure_app" });
35
+ flow.step({ toolName: { is: "configure_app" } }, { nextTool: "deploy" });
36
36
  });
37
37
  ```
38
38
 
@@ -80,13 +80,20 @@ return {
80
80
 
81
81
  ### `defineFlow(flowId, builder)`
82
82
 
83
- Creates a flow definition. Each step is a logic statement: a match condition paired with a decision. Match condition fields support operators (`is`, `isNot`); bare strings are sugar for `{ is: "..." }`.
83
+ Creates a flow definition. Each step is a logic statement: a match condition paired with a decision. Match conditions use explicit operators (`{ is: ... }`, `{ isNot: ... }`) and can match on `toolName`, `params`, and `result`.
84
84
 
85
85
  ```ts
86
86
  const myFlow = defineFlow("my_flow", (flow) => {
87
- flow.step({ toolName: { is: "tool_a" } }, { nextTool: "tool_b" }); // explicit equality
88
- flow.step({ toolName: { isNot: "abort" } }, { nextTool: "continue" }); // negation
89
- flow.step({ toolName: "tool_a" }, { nextTool: "tool_b" }); // bare string = sugar for { is: "tool_a" }
87
+ flow.step({ toolName: { is: "tool_a" } }, { nextTool: "tool_b" });
88
+ flow.step({ toolName: { isNot: "abort" } }, { nextTool: "continue" });
89
+ flow.step(
90
+ { toolName: { is: "scan_repo" }, params: { language: { is: "python" } } },
91
+ { nextTool: "configure_python" },
92
+ );
93
+ flow.step(
94
+ { result: { hasConfig: { is: true } } },
95
+ { nextTool: "review_config" },
96
+ );
90
97
  });
91
98
  ```
92
99
 
@@ -113,6 +120,8 @@ const taias = createTaias({
113
120
  **Options:**
114
121
  - `flow` - A `FlowDefinition` created by `defineFlow`
115
122
  - `devMode` (optional) - Enable development mode checks
123
+ - `debug` (optional) - `true` or `{ format, logger }` -- enable built-in debug output
124
+ - `tracing` (optional) - `"summary"` (default) or `"detailed"` -- controls trace depth
116
125
  - `onMissingStep` (optional) - Callback invoked when no step matches
117
126
 
118
127
  **Returns:** `Taias` instance
@@ -143,6 +152,31 @@ const affordances = await taias.resolve({
143
152
 
144
153
  See the [full documentation](https://taias.xyz/docs) for complete API reference and types.
145
154
 
155
+ ## Observability
156
+
157
+ Enable debug output with a single option:
158
+
159
+ ```ts
160
+ const taias = createTaias({ flow, debug: true });
161
+ ```
162
+
163
+ Every `resolve()` call prints a formatted breakdown to the console -- what went in, how the decision was reached, and what was produced.
164
+
165
+ For compact single-line output:
166
+
167
+ ```ts
168
+ const taias = createTaias({ flow, debug: { format: "compact" } });
169
+ // [Taias] scan_repo → nextTool=configure_app (indexed, step 0, 1 evaluated)
170
+ ```
171
+
172
+ Enable detailed tracing for per-step evaluation breakdowns (verbose -- best used when debugging specific resolve calls):
173
+
174
+ ```ts
175
+ const taias = createTaias({ flow, debug: true, tracing: "detailed" });
176
+ ```
177
+
178
+ For programmatic access to resolve events, use the event emitter API (`taias.on` / `taias.off`). See the [full documentation](https://taias.xyz/docs) for details.
179
+
146
180
  ## Dev Mode
147
181
 
148
182
  <details>
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ createDebugSubscriber: () => createDebugSubscriber,
23
24
  createTaias: () => createTaias,
24
25
  defineAffordances: () => defineAffordances,
25
26
  defineFlow: () => defineFlow,
@@ -32,14 +33,13 @@ function defineFlow(flowId, builder) {
32
33
  const steps = [];
33
34
  const flowBuilder = {
34
35
  step(match, input) {
35
- const condition = typeof match === "string" ? { toolName: match } : match;
36
36
  if (typeof input === "function") {
37
- steps.push({ kind: "handler", match: condition, handler: input });
37
+ steps.push({ kind: "handler", match, handler: input });
38
38
  } else {
39
39
  steps.push({
40
40
  kind: "logic",
41
41
  statement: {
42
- match: condition,
42
+ match,
43
43
  decision: input
44
44
  }
45
45
  });
@@ -107,98 +107,396 @@ function selectUiAffordances(decision, index, opts = {}) {
107
107
  return selections;
108
108
  }
109
109
 
110
+ // src/debugSubscriber.ts
111
+ function formatContext(context) {
112
+ const parts = [];
113
+ if (context.toolName) parts.push(`toolName=${context.toolName}`);
114
+ if (context.params) parts.push(`params=${JSON.stringify(context.params)}`);
115
+ if (context.result) parts.push(`result=${JSON.stringify(context.result)}`);
116
+ return parts.length > 0 ? parts.join(", ") : "(empty)";
117
+ }
118
+ function formatContextLabel(context) {
119
+ if (context.toolName) return context.toolName;
120
+ if (context.params) return `params:${JSON.stringify(context.params)}`;
121
+ if (context.result) return `result:${JSON.stringify(context.result)}`;
122
+ return "(empty)";
123
+ }
124
+ function createDebugSubscriber(options) {
125
+ const format = options?.format ?? "default";
126
+ const log = options?.logger ?? console.log;
127
+ if (format === "compact") {
128
+ return (event) => {
129
+ const { trace, context } = event;
130
+ const label = formatContextLabel(context);
131
+ if (!trace.matched) {
132
+ log(`[Taias] ${label} \u2192 no match (${trace.candidatesEvaluated} evaluated)`);
133
+ return;
134
+ }
135
+ const decisionSummary = event.decision ? Object.entries(event.decision).map(([k, v]) => `${k}=${v}`).join(", ") : "null";
136
+ log(
137
+ `[Taias] ${label} \u2192 ${decisionSummary} (${trace.phase}, step ${trace.matchedStepIndex}, ${trace.candidatesEvaluated} evaluated)`
138
+ );
139
+ };
140
+ }
141
+ return (event) => {
142
+ const { trace, context } = event;
143
+ const lines = [];
144
+ lines.push("\u250C\u2500 Taias Resolve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
145
+ lines.push(`\u2502 Flow: ${event.flowId}`);
146
+ lines.push(`\u2502 Context: ${formatContext(context)}`);
147
+ lines.push("\u2502");
148
+ if (!trace.matched) {
149
+ lines.push("\u2502 Result: NO MATCH");
150
+ lines.push(`\u2502 Candidates evaluated: ${trace.candidatesEvaluated}`);
151
+ } else {
152
+ lines.push(`\u2502 Matched: step ${trace.matchedStepIndex} (${trace.matchedStepKind})`);
153
+ lines.push(`\u2502 Phase: ${trace.phase}`);
154
+ if (trace.resolutionPath.length > 0) {
155
+ lines.push(`\u2502 Resolution path: ${trace.resolutionPath.join(" \u2192 ")}`);
156
+ }
157
+ lines.push(`\u2502 Candidates evaluated: ${trace.candidatesEvaluated}`);
158
+ if (trace.matchedStepMatch) {
159
+ lines.push(`\u2502 Match condition: ${JSON.stringify(trace.matchedStepMatch)}`);
160
+ }
161
+ }
162
+ lines.push("\u2502");
163
+ if (event.decision) {
164
+ const fields = Object.entries(event.decision).map(([k, v]) => `${k}=${v}`).join(", ");
165
+ lines.push(`\u2502 Decision: ${fields}`);
166
+ } else {
167
+ lines.push("\u2502 Decision: null");
168
+ }
169
+ if (event.affordances) {
170
+ lines.push(`\u2502 Advice: ${event.affordances.advice.slice(0, 80)}${event.affordances.advice.length > 80 ? "..." : ""}`);
171
+ }
172
+ lines.push(`\u2502 Duration: ${event.durationMs.toFixed(2)}ms`);
173
+ if (trace.evaluations) {
174
+ lines.push("\u2502");
175
+ lines.push("\u2502 Detailed evaluations:");
176
+ for (const evaluation of trace.evaluations) {
177
+ const icon = evaluation.result === "matched" ? "\u2713" : "\u2717";
178
+ lines.push(`\u2502 ${icon} step ${evaluation.stepIndex}: ${evaluation.result}`);
179
+ if (evaluation.fieldResults) {
180
+ for (const [field, fr] of Object.entries(evaluation.fieldResults)) {
181
+ const fieldIcon = fr.passed ? "\u2713" : "\u2717";
182
+ lines.push(`\u2502 ${fieldIcon} ${field}: ${JSON.stringify(fr.condition)} vs ${JSON.stringify(fr.actual)}`);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
188
+ log(lines.join("\n"));
189
+ };
190
+ }
191
+
110
192
  // src/createTaias.ts
111
193
  function generateAdvice(nextTool) {
112
194
  return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;
113
195
  }
114
- function normalizeFieldCondition(field) {
115
- return typeof field === "string" ? { is: field } : field;
116
- }
117
196
  function evaluateCondition(condition, value) {
118
197
  if ("is" in condition) return value === condition.is;
119
198
  if ("isNot" in condition) return value !== condition.isNot;
120
199
  return false;
121
200
  }
122
201
  function evaluateMatch(match, ctx) {
123
- const toolCondition = normalizeFieldCondition(match.toolName);
124
- return evaluateCondition(toolCondition, ctx.toolName);
202
+ if (match.toolName && !evaluateCondition(match.toolName, ctx.toolName)) return false;
203
+ if (match.params) {
204
+ if (!ctx.params) return false;
205
+ for (const [key, condition] of Object.entries(match.params)) {
206
+ if (!evaluateCondition(condition, ctx.params[key])) return false;
207
+ }
208
+ }
209
+ if (match.result) {
210
+ if (!ctx.result) return false;
211
+ for (const [key, condition] of Object.entries(match.result)) {
212
+ if (!evaluateCondition(condition, ctx.result[key])) return false;
213
+ }
214
+ }
215
+ return true;
125
216
  }
126
- function isIndexable(field) {
127
- if (typeof field === "string") return true;
128
- return "is" in field;
217
+ function evaluateMatchDetailed(match, ctx) {
218
+ const fieldResults = {};
219
+ let allPassed = true;
220
+ if (match.toolName) {
221
+ const actual = ctx.toolName;
222
+ const passed = evaluateCondition(match.toolName, actual);
223
+ fieldResults["toolName"] = { condition: match.toolName, actual, passed };
224
+ if (!passed) allPassed = false;
225
+ }
226
+ if (match.params) {
227
+ if (!ctx.params) {
228
+ for (const [key, condition] of Object.entries(match.params)) {
229
+ fieldResults[`params.${key}`] = { condition, actual: void 0, passed: false };
230
+ }
231
+ allPassed = false;
232
+ } else {
233
+ for (const [key, condition] of Object.entries(match.params)) {
234
+ const actual = ctx.params[key];
235
+ const passed = evaluateCondition(condition, actual);
236
+ fieldResults[`params.${key}`] = { condition, actual, passed };
237
+ if (!passed) allPassed = false;
238
+ }
239
+ }
240
+ }
241
+ if (match.result) {
242
+ if (!ctx.result) {
243
+ for (const [key, condition] of Object.entries(match.result)) {
244
+ fieldResults[`result.${key}`] = { condition, actual: void 0, passed: false };
245
+ }
246
+ allPassed = false;
247
+ } else {
248
+ for (const [key, condition] of Object.entries(match.result)) {
249
+ const actual = ctx.result[key];
250
+ const passed = evaluateCondition(condition, actual);
251
+ fieldResults[`result.${key}`] = { condition, actual, passed };
252
+ if (!passed) allPassed = false;
253
+ }
254
+ }
255
+ }
256
+ return { passed: allPassed, fieldResults };
129
257
  }
130
- function indexKey(field) {
131
- if (typeof field === "string") return field;
132
- if ("is" in field) return field.is;
133
- throw new Error("Cannot derive index key from non-indexable condition");
258
+ function extractIsConditions(match) {
259
+ const conditions = [];
260
+ if (match.toolName && "is" in match.toolName) {
261
+ conditions.push({ path: "toolName", value: match.toolName.is });
262
+ }
263
+ if (match.params) {
264
+ for (const [key, cond] of Object.entries(match.params)) {
265
+ if ("is" in cond) conditions.push({ path: `params.${key}`, value: cond.is });
266
+ }
267
+ }
268
+ if (match.result) {
269
+ for (const [key, cond] of Object.entries(match.result)) {
270
+ if ("is" in cond) conditions.push({ path: `result.${key}`, value: cond.is });
271
+ }
272
+ }
273
+ return conditions;
274
+ }
275
+ function hasConditionOnField(match, path) {
276
+ if (path === "toolName") return !!match.toolName;
277
+ if (path.startsWith("params.")) return !!match.params?.[path.slice(7)];
278
+ if (path.startsWith("result.")) return !!match.result?.[path.slice(7)];
279
+ return false;
280
+ }
281
+ function getContextValue(ctx, path) {
282
+ if (path === "toolName") return ctx.toolName;
283
+ if (path.startsWith("params.")) return ctx.params?.[path.slice(7)];
284
+ if (path.startsWith("result.")) return ctx.result?.[path.slice(7)];
285
+ return void 0;
134
286
  }
135
287
  function getMatch(step) {
136
288
  return step.kind === "logic" ? step.statement.match : step.match;
137
289
  }
138
290
  function serializeMatch(match) {
139
- const normalized = normalizeFieldCondition(match.toolName);
140
- return JSON.stringify({ toolName: normalized });
291
+ return JSON.stringify(match);
141
292
  }
142
293
  function createTaias(options) {
143
294
  const {
144
295
  flow,
145
296
  affordances,
146
297
  devMode = false,
298
+ debug = false,
299
+ tracing = "summary",
147
300
  onMissingStep,
148
301
  onWarn
149
302
  } = options;
150
303
  const warn = onWarn ?? ((msg) => console.warn(msg));
304
+ const detailed = tracing === "detailed";
151
305
  if (devMode) {
152
306
  const seenKeys = /* @__PURE__ */ new Set();
153
307
  for (const step of flow.steps) {
154
308
  const key = serializeMatch(getMatch(step));
155
309
  if (seenKeys.has(key)) {
156
- const match = getMatch(step);
157
- const normalized = normalizeFieldCondition(match.toolName);
158
- const label = "is" in normalized ? normalized.is : `isNot:${normalized.isNot}`;
159
310
  throw new Error(
160
- `Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`
311
+ `Taias: Duplicate match condition in flow '${flow.id}'. Each step must have a unique match condition. Duplicate: ${key}`
161
312
  );
162
313
  }
163
314
  seenKeys.add(key);
164
315
  }
165
316
  }
166
- const exactIndex = /* @__PURE__ */ new Map();
167
- const broadSteps = [];
168
- for (const step of flow.steps) {
169
- const match = getMatch(step);
170
- if (isIndexable(match.toolName)) {
171
- exactIndex.set(indexKey(match.toolName), step);
172
- } else {
173
- broadSteps.push(step);
317
+ const fieldIndexes = /* @__PURE__ */ new Map();
318
+ const indexableStepIndices = [];
319
+ const broadStepIndices = [];
320
+ for (let i = 0; i < flow.steps.length; i++) {
321
+ const match = getMatch(flow.steps[i]);
322
+ const isConditions = extractIsConditions(match);
323
+ if (isConditions.length === 0) {
324
+ broadStepIndices.push(i);
325
+ continue;
326
+ }
327
+ indexableStepIndices.push(i);
328
+ for (const { path, value } of isConditions) {
329
+ let fieldIndex = fieldIndexes.get(path);
330
+ if (!fieldIndex) {
331
+ fieldIndex = { valueMap: /* @__PURE__ */ new Map(), unconstrained: [] };
332
+ fieldIndexes.set(path, fieldIndex);
333
+ }
334
+ let stepList = fieldIndex.valueMap.get(value);
335
+ if (!stepList) {
336
+ stepList = [];
337
+ fieldIndex.valueMap.set(value, stepList);
338
+ }
339
+ stepList.push(i);
340
+ }
341
+ }
342
+ for (const [fieldPath, fieldIndex] of fieldIndexes) {
343
+ for (const i of indexableStepIndices) {
344
+ if (!hasConditionOnField(getMatch(flow.steps[i]), fieldPath)) {
345
+ fieldIndex.unconstrained.push(i);
346
+ }
174
347
  }
175
348
  }
176
- const hasBroadSteps = broadSteps.length > 0;
349
+ const hasBroadSteps = broadStepIndices.length > 0;
177
350
  const registryIndex = buildRegistryIndex(affordances);
351
+ const listeners = /* @__PURE__ */ new Map();
352
+ function emit(event, data) {
353
+ const handlers = listeners.get(event);
354
+ if (handlers) {
355
+ for (const handler of handlers) handler(data);
356
+ }
357
+ }
358
+ function on(event, handler) {
359
+ let set = listeners.get(event);
360
+ if (!set) {
361
+ set = /* @__PURE__ */ new Set();
362
+ listeners.set(event, set);
363
+ }
364
+ set.add(handler);
365
+ }
366
+ function off(event, handler) {
367
+ listeners.get(event)?.delete(handler);
368
+ }
369
+ if (debug) {
370
+ const debugOpts = typeof debug === "object" ? debug : void 0;
371
+ on("resolve", createDebugSubscriber(debugOpts));
372
+ }
178
373
  return {
179
374
  async resolve(ctx) {
180
- let step;
181
- if (!hasBroadSteps) {
182
- step = exactIndex.get(ctx.toolName);
183
- } else {
184
- for (const candidate of flow.steps) {
185
- if (evaluateMatch(getMatch(candidate), ctx)) {
186
- step = candidate;
375
+ const startTime = performance.now();
376
+ const timestamp = Date.now();
377
+ let matchedStep;
378
+ let matchedStepIndex = null;
379
+ let matchPhase = null;
380
+ const resolutionPath = [];
381
+ let candidatesEvaluated = 0;
382
+ const evaluations = detailed ? [] : void 0;
383
+ function tryMatch(idx) {
384
+ candidatesEvaluated++;
385
+ const step = flow.steps[idx];
386
+ const match = getMatch(step);
387
+ if (detailed) {
388
+ const { passed, fieldResults } = evaluateMatchDetailed(match, ctx);
389
+ evaluations.push({
390
+ stepIndex: idx,
391
+ match,
392
+ result: passed ? "matched" : "no-match",
393
+ fieldResults
394
+ });
395
+ return passed;
396
+ }
397
+ return evaluateMatch(match, ctx);
398
+ }
399
+ const applicableFieldPaths = [];
400
+ for (const fieldPath of fieldIndexes.keys()) {
401
+ const ctxValue = getContextValue(ctx, fieldPath);
402
+ if (ctxValue !== void 0) {
403
+ applicableFieldPaths.push(fieldPath);
404
+ }
405
+ }
406
+ resolutionPath.push(...applicableFieldPaths);
407
+ if (applicableFieldPaths.length > 0) {
408
+ let candidates = null;
409
+ for (const fieldPath of applicableFieldPaths) {
410
+ const fieldIndex = fieldIndexes.get(fieldPath);
411
+ const ctxValue = getContextValue(ctx, fieldPath);
412
+ const fieldCandidates = /* @__PURE__ */ new Set();
413
+ const indexed = fieldIndex.valueMap.get(ctxValue);
414
+ if (indexed) {
415
+ for (const idx of indexed) fieldCandidates.add(idx);
416
+ }
417
+ for (const idx of fieldIndex.unconstrained) {
418
+ fieldCandidates.add(idx);
419
+ }
420
+ if (candidates === null) {
421
+ candidates = fieldCandidates;
422
+ } else {
423
+ for (const idx of candidates) {
424
+ if (!fieldCandidates.has(idx)) candidates.delete(idx);
425
+ }
426
+ }
427
+ }
428
+ if (candidates && candidates.size > 0) {
429
+ const sorted = [...candidates].sort((a, b) => a - b);
430
+ for (const idx of sorted) {
431
+ if (tryMatch(idx)) {
432
+ matchedStep = flow.steps[idx];
433
+ matchedStepIndex = idx;
434
+ matchPhase = "indexed";
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ } else if (indexableStepIndices.length > 0) {
440
+ for (const idx of indexableStepIndices) {
441
+ if (tryMatch(idx)) {
442
+ matchedStep = flow.steps[idx];
443
+ matchedStepIndex = idx;
444
+ matchPhase = "indexed";
445
+ break;
446
+ }
447
+ }
448
+ }
449
+ if (!matchedStep && hasBroadSteps) {
450
+ for (const idx of broadStepIndices) {
451
+ if (tryMatch(idx)) {
452
+ matchedStep = flow.steps[idx];
453
+ matchedStepIndex = idx;
454
+ matchPhase = "broad";
187
455
  break;
188
456
  }
189
457
  }
190
458
  }
191
- if (!step) {
459
+ const trace = {
460
+ matched: !!matchedStep,
461
+ matchedStepIndex,
462
+ matchedStepKind: matchedStep?.kind ?? null,
463
+ matchedStepMatch: matchedStep ? getMatch(matchedStep) : null,
464
+ phase: matchPhase,
465
+ resolutionPath,
466
+ candidatesEvaluated,
467
+ ...evaluations !== void 0 ? { evaluations } : {}
468
+ };
469
+ if (!matchedStep) {
192
470
  onMissingStep?.(ctx);
471
+ emit("resolve", {
472
+ flowId: flow.id,
473
+ timestamp,
474
+ durationMs: performance.now() - startTime,
475
+ context: ctx,
476
+ trace,
477
+ decision: null,
478
+ affordances: null
479
+ });
193
480
  return null;
194
481
  }
195
482
  let result;
196
- if (step.kind === "logic") {
197
- result = step.statement.decision;
483
+ if (matchedStep.kind === "logic") {
484
+ result = matchedStep.statement.decision;
198
485
  } else {
199
- result = await step.handler(ctx);
486
+ result = await matchedStep.handler(ctx);
487
+ }
488
+ if (!result) {
489
+ emit("resolve", {
490
+ flowId: flow.id,
491
+ timestamp,
492
+ durationMs: performance.now() - startTime,
493
+ context: ctx,
494
+ trace,
495
+ decision: null,
496
+ affordances: null
497
+ });
498
+ return null;
200
499
  }
201
- if (!result) return null;
202
500
  if (devMode && result.nextTool === "") {
203
501
  warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);
204
502
  }
@@ -207,12 +505,20 @@ function createTaias(options) {
207
505
  devMode,
208
506
  onWarn: warn
209
507
  });
210
- return {
211
- advice: generateAdvice(result.nextTool),
508
+ const advice = generateAdvice(result.nextTool);
509
+ emit("resolve", {
510
+ flowId: flow.id,
511
+ timestamp,
512
+ durationMs: performance.now() - startTime,
513
+ context: ctx,
514
+ trace,
212
515
  decision,
213
- selections
214
- };
215
- }
516
+ affordances: { advice, selections }
517
+ });
518
+ return { advice, decision, selections };
519
+ },
520
+ on,
521
+ off
216
522
  };
217
523
  }
218
524
 
@@ -263,6 +569,7 @@ function mergeAffordances(registries, opts = {}) {
263
569
  }
264
570
  // Annotate the CommonJS export names for ESM import in node:
265
571
  0 && (module.exports = {
572
+ createDebugSubscriber,
266
573
  createTaias,
267
574
  defineAffordances,
268
575
  defineFlow,