taias 0.5.0 → 0.7.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
@@ -25,14 +25,14 @@ npm install taias
25
25
 
26
26
  ## Quick Start
27
27
 
28
- **1. Define a flow** — Map out your tool sequence:
28
+ **1. Define a flow** — Express your logic as structured data:
29
29
 
30
30
  ```ts
31
31
  import { defineFlow, createTaias } from "taias";
32
32
 
33
33
  const flow = defineFlow("onboard", (flow) => {
34
- flow.step("scan_repo", () => ({ nextTool: "configure_app" }));
35
- flow.step("configure_app", () => ({ nextTool: "deploy" }));
34
+ flow.step({ toolName: "scan_repo" }, { nextTool: "configure_app" });
35
+ flow.step({ toolName: "configure_app" }, { nextTool: "deploy" });
36
36
  });
37
37
  ```
38
38
 
@@ -80,13 +80,11 @@ return {
80
80
 
81
81
  ### `defineFlow(flowId, builder)`
82
82
 
83
- Creates a flow definition.
83
+ Creates a flow definition. Each step is a logic statement: a match condition paired with a decision.
84
84
 
85
85
  ```ts
86
86
  const myFlow = defineFlow("my_flow", (flow) => {
87
- flow.step("tool_name", (ctx) => ({
88
- nextTool: "next_tool_name",
89
- }));
87
+ flow.step({ toolName: "tool_name" }, { nextTool: "next_tool_name" });
90
88
  });
91
89
  ```
92
90
 
@@ -119,7 +117,7 @@ const taias = createTaias({
119
117
 
120
118
  ### `taias.resolve(ctx)`
121
119
 
122
- Resolves a tool call to get the suggested next step. Advice text to send the LLM is generated based on the `nextTool` specified in your step handler.
120
+ Resolves a tool call to get the decision and its manifestations. Evaluates the matching logic statement and produces advice text, the decision object, and UI selections.
123
121
 
124
122
  ```ts
125
123
  const affordances = await taias.resolve({
@@ -138,8 +136,8 @@ const affordances = await taias.resolve({
138
136
  - `ctx.result` - The output of the tool's execution (optional)
139
137
 
140
138
  **Returns:** `Affordances | null`
141
- - Returns an `Affordances` object with `advice` (and more) if a matching step is found
142
- - Returns `null` if no step matches or handler returns null
139
+ - Returns an `Affordances` object with `advice`, `decision`, and `selections` if a matching step is found
140
+ - Returns `null` if no step matches
143
141
 
144
142
  See the [full documentation](https://taias.xyz/docs) for complete API reference and types.
145
143
 
@@ -150,12 +148,12 @@ See the [full documentation](https://taias.xyz/docs) for complete API reference
150
148
 
151
149
  When `devMode: true`, Taias performs additional validation:
152
150
 
153
- 1. **Duplicate toolName detection** — Throws an error if a flow defines the same tool name twice:
151
+ 1. **Duplicate match condition detection** — Throws an error if a flow defines two steps with the same match condition:
154
152
  ```
155
- Taias: Duplicate step for tool 'scan_repo' in flow 'onboard_repo'. Only one handler per tool is supported.
153
+ Taias: Duplicate match condition 'scan_repo' in flow 'onboard_repo'. Each step must have a unique match condition.
156
154
  ```
157
155
 
158
- 2. **Empty nextTool warning** — Logs a warning if a handler returns empty nextTool:
156
+ 2. **Empty nextTool warning** — Logs a warning if a logic statement has an empty nextTool:
159
157
  ```
160
158
  Taias: nextTool for tool 'scan_repo' is empty.
161
159
  ```
package/dist/index.cjs CHANGED
@@ -31,8 +31,19 @@ module.exports = __toCommonJS(index_exports);
31
31
  function defineFlow(flowId, builder) {
32
32
  const steps = [];
33
33
  const flowBuilder = {
34
- step(toolName, handler) {
35
- steps.push({ toolName, handler });
34
+ step(match, input) {
35
+ const condition = typeof match === "string" ? { toolName: match } : match;
36
+ if (typeof input === "function") {
37
+ steps.push({ kind: "handler", match: condition, handler: input });
38
+ } else {
39
+ steps.push({
40
+ kind: "logic",
41
+ statement: {
42
+ match: condition,
43
+ decision: input
44
+ }
45
+ });
46
+ }
36
47
  }
37
48
  };
38
49
  builder(flowBuilder);
@@ -100,6 +111,34 @@ function selectUiAffordances(decision, index, opts = {}) {
100
111
  function generateAdvice(nextTool) {
101
112
  return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;
102
113
  }
114
+ function normalizeFieldCondition(field) {
115
+ return typeof field === "string" ? { is: field } : field;
116
+ }
117
+ function evaluateCondition(condition, value) {
118
+ if ("is" in condition) return value === condition.is;
119
+ if ("isNot" in condition) return value !== condition.isNot;
120
+ return false;
121
+ }
122
+ function evaluateMatch(match, ctx) {
123
+ const toolCondition = normalizeFieldCondition(match.toolName);
124
+ return evaluateCondition(toolCondition, ctx.toolName);
125
+ }
126
+ function isIndexable(field) {
127
+ if (typeof field === "string") return true;
128
+ return "is" in field;
129
+ }
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");
134
+ }
135
+ function getMatch(step) {
136
+ return step.kind === "logic" ? step.statement.match : step.match;
137
+ }
138
+ function serializeMatch(match) {
139
+ const normalized = normalizeFieldCondition(match.toolName);
140
+ return JSON.stringify({ toolName: normalized });
141
+ }
103
142
  function createTaias(options) {
104
143
  const {
105
144
  flow,
@@ -110,26 +149,55 @@ function createTaias(options) {
110
149
  } = options;
111
150
  const warn = onWarn ?? ((msg) => console.warn(msg));
112
151
  if (devMode) {
113
- const seenTools = /* @__PURE__ */ new Set();
152
+ const seenKeys = /* @__PURE__ */ new Set();
114
153
  for (const step of flow.steps) {
115
- if (seenTools.has(step.toolName)) {
154
+ const key = serializeMatch(getMatch(step));
155
+ 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}`;
116
159
  throw new Error(
117
- `Taias: Duplicate step for tool '${step.toolName}' in flow '${flow.id}'. Only one handler per tool is supported.`
160
+ `Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`
118
161
  );
119
162
  }
120
- seenTools.add(step.toolName);
163
+ seenKeys.add(key);
164
+ }
165
+ }
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);
121
174
  }
122
175
  }
123
- const stepMap = new Map(flow.steps.map((step) => [step.toolName, step.handler]));
176
+ const hasBroadSteps = broadSteps.length > 0;
124
177
  const registryIndex = buildRegistryIndex(affordances);
125
178
  return {
126
179
  async resolve(ctx) {
127
- const handler = stepMap.get(ctx.toolName);
128
- if (!handler) {
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;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ if (!step) {
129
192
  onMissingStep?.(ctx);
130
193
  return null;
131
194
  }
132
- const result = await handler(ctx);
195
+ let result;
196
+ if (step.kind === "logic") {
197
+ result = step.statement.decision;
198
+ } else {
199
+ result = await step.handler(ctx);
200
+ }
133
201
  if (!result) return null;
134
202
  if (devMode && result.nextTool === "") {
135
203
  warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/flow.ts","../src/uiAffordances/types.ts","../src/uiAffordances/indexing.ts","../src/uiAffordances/select.ts","../src/createTaias.ts","../src/uiAffordances/defineAffordances.ts","../src/uiAffordances/mergeAffordances.ts"],"sourcesContent":["// Main exports\nexport { defineFlow } from \"./flow\";\nexport { createTaias } from \"./createTaias\";\n\n// UI affordances exports\nexport { defineAffordances } from \"./uiAffordances/defineAffordances\";\nexport { mergeAffordances } from \"./uiAffordances/mergeAffordances\";\nexport type { AffordanceRegistrar } from \"./uiAffordances/defineAffordances\";\nexport type {\n DefaultSlots,\n CanonicalSlot,\n Binding,\n BindingInput,\n HandleRegistration,\n Selection,\n UiSelections,\n AffordanceRegistry,\n} from \"./uiAffordances/types\";\n\n// Core + flow type exports\nexport type {\n Decision,\n TaiasContext,\n StepDecision,\n Affordances,\n StepHandler,\n FlowStep,\n FlowDefinition,\n FlowBuilder,\n TaiasOptions,\n Taias,\n} from \"./types\";\n","import type { FlowBuilder, FlowDefinition, FlowStep, StepHandler } from \"./types\";\n\n/**\n * Define a flow with its steps.\n *\n * @param flowId - Unique identifier for the flow\n * @param builder - Callback that receives a FlowBuilder to define steps\n * @returns A FlowDefinition object\n *\n * @example\n * ```ts\n * const onboardRepoFlow = defineFlow(\"onboard_repo\", (flow) => {\n * flow.step(\"scan_repo\", (ctx) => ({\n * nextTool: \"configure_app\",\n * }));\n * });\n * ```\n */\nexport function defineFlow(\n flowId: string,\n builder: (flow: FlowBuilder) => void\n): FlowDefinition {\n const steps: FlowStep[] = [];\n\n const flowBuilder: FlowBuilder = {\n step(toolName: string, handler: StepHandler): void {\n steps.push({ toolName, handler });\n },\n };\n\n builder(flowBuilder);\n\n return {\n id: flowId,\n steps,\n };\n}\n\n","/**\n * Default slots for backwards compatibility.\n * Users can define custom slots by passing a type parameter.\n */\nexport type DefaultSlots = \"primaryCta\" | \"secondaryCta\" | \"widgetVariant\";\n\n/**\n * Alias for backwards compatibility in exports.\n * @deprecated Use DefaultSlots or define your own slot type\n */\nexport type CanonicalSlot = DefaultSlots;\n\nexport type Binding = {\n key: string;\n value: string;\n};\n\n/**\n * Input format for registering affordance bindings.\n * - { toolName } is shorthand for { key: \"nextTool\", value: toolName }\n * - { key, value } is the generalized form for custom bindings\n */\nexport type BindingInput = { toolName: string } | { key: string; value: string };\n\n/**\n * A registered UI affordance handle.\n * Generic over slot type S for custom slot support.\n */\nexport type HandleRegistration<S extends string = DefaultSlots> = {\n slot: S;\n handleId: string;\n bindsTo: Binding;\n};\n\nexport type Selection = {\n handleId: string;\n bindsTo: Binding;\n};\n\n/**\n * UI selections keyed by slot name.\n * Generic over slot type S for custom slot support.\n */\nexport type UiSelections<S extends string = DefaultSlots> = Partial<Record<S, Selection>>;\n\n/**\n * Collection of registered handles.\n * Generic over slot type S for custom slot support.\n */\nexport type AffordanceRegistry<S extends string = DefaultSlots> = {\n handles: HandleRegistration<S>[];\n};\n\nexport function normalizeBinding(input: BindingInput): Binding {\n if (\"toolName\" in input) return { key: \"nextTool\", value: input.toolName };\n return { key: input.key, value: input.value };\n}\n\n/**\n * Creates a binding key for indexing.\n * Accepts any string for slot to support custom slots.\n */\nexport function makeBindingKey(slot: string, binding: Binding): string {\n return `${slot}::${binding.key}::${binding.value}`;\n}\n","import type { AffordanceRegistry, DefaultSlots, HandleRegistration } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\n/**\n * Index structure for efficient affordance lookup.\n * Generic over slot type S for custom slot support.\n */\nexport type RegistryIndex<S extends string = DefaultSlots> = {\n byBindingKey: Map<string, HandleRegistration<S>>;\n slots: Set<S>;\n /** Inferred decision field for each slot, derived from handle bindings. */\n slotKeyMap: Map<S, string>;\n};\n\n/**\n * Build an index from a registry for efficient lookup during selection.\n * Tracks which slots have registered handles and infers the decision field\n * for each slot from its handle bindings.\n *\n * @throws Error if handles for the same slot bind to different keys\n */\nexport function buildRegistryIndex<S extends string = DefaultSlots>(\n registry?: AffordanceRegistry<S>\n): RegistryIndex<S> {\n const byBindingKey = new Map<string, HandleRegistration<S>>();\n const slots = new Set<S>();\n const slotKeyMap = new Map<S, string>();\n\n if (!registry) return { byBindingKey, slots, slotKeyMap };\n\n for (const h of registry.handles) {\n slots.add(h.slot);\n\n // Infer and validate slot key\n const existingKey = slotKeyMap.get(h.slot);\n if (existingKey && existingKey !== h.bindsTo.key) {\n throw new Error(\n `[Taias] Slot \"${h.slot}\" has handles bound to different keys: \"${existingKey}\" and \"${h.bindsTo.key}\". ` +\n `All handles for a slot must use the same decision field.`\n );\n }\n slotKeyMap.set(h.slot, h.bindsTo.key);\n\n byBindingKey.set(makeBindingKey(h.slot, h.bindsTo), h);\n }\n\n return { byBindingKey, slots, slotKeyMap };\n}\n","import type { Decision } from \"../types\";\nimport type { DefaultSlots, UiSelections } from \"./types\";\nimport type { RegistryIndex } from \"./indexing\";\nimport { makeBindingKey } from \"./types\";\n\nexport type SelectOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Select UI affordances based on flow decision.\n * Uses the inferred decision field for each slot from the registry index.\n */\nexport function selectUiAffordances<S extends string = DefaultSlots>(\n decision: Decision,\n index: RegistryIndex<S>,\n opts: SelectOptions = {}\n): UiSelections<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const selections: UiSelections<S> = {};\n\n for (const slot of index.slots) {\n // Use inferred key from handle bindings\n const field = index.slotKeyMap.get(slot);\n if (!field) continue;\n\n const value = decision[field];\n if (!value) continue;\n\n const k = makeBindingKey(slot, { key: field, value });\n const handle = index.byBindingKey.get(k);\n\n if (!handle) {\n if (devMode) warn(`[Taias] No affordance for slot \"${slot}\" when ${field}=\"${value}\"`);\n continue;\n }\n\n (selections as Record<string, unknown>)[slot] = {\n handleId: handle.handleId,\n bindsTo: handle.bindsTo,\n };\n }\n\n return selections;\n}\n","import type { Affordances, Taias, TaiasContext, TaiasOptions, Decision } from \"./types\";\nimport type { DefaultSlots } from \"./uiAffordances/types\";\nimport { buildRegistryIndex } from \"./uiAffordances/indexing\";\nimport { selectUiAffordances } from \"./uiAffordances/select\";\n\n/**\n * Generate advice text for a given next tool.\n */\nfunction generateAdvice(nextTool: string): string {\n return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;\n}\n\n/**\n * createTaias constructs a decision engine.\n *\n * Taias resolves tool context into a generalized Decision object,\n * and then manifests that decision into concrete affordances:\n *\n * - LLM guidance (advice)\n * - UI affordance selections\n *\n * Flow logic determines *what should happen next*.\n * UI affordances determine *how that decision appears in the interface*.\n *\n * This file is the boundary where:\n *\n * Inputs → Decision → Manifestations\n *\n * are unified into a single resolve() call.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const taias = createTaias({ flow, affordances });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta\", { key: \"nextTool\", value: \"createUser\" });\n * r.contentArea(\"content\", { key: \"contentArea\", value: \"form\" });\n * r.headerStyle(\"header\", { key: \"headerStyle\", value: \"progress\" });\n * });\n * const taias = createTaias<MySlots>({ flow, affordances });\n * ```\n */\nexport function createTaias<S extends string = DefaultSlots>(\n options: TaiasOptions<S>\n): Taias<S> {\n const {\n flow,\n affordances,\n devMode = false,\n onMissingStep,\n onWarn,\n } = options;\n\n const warn = onWarn ?? ((msg: string) => console.warn(msg));\n\n // Dev mode: Check for duplicate toolNames\n if (devMode) {\n const seenTools = new Set<string>();\n for (const step of flow.steps) {\n if (seenTools.has(step.toolName)) {\n throw new Error(\n `Taias: Duplicate step for tool '${step.toolName}' in flow '${flow.id}'. Only one handler per tool is supported.`\n );\n }\n seenTools.add(step.toolName);\n }\n }\n\n // Build a lookup map for efficient resolution\n const stepMap = new Map(flow.steps.map((step) => [step.toolName, step.handler]));\n\n // Build affordance index once (if provided)\n const registryIndex = buildRegistryIndex<S>(affordances);\n\n return {\n async resolve(ctx: TaiasContext): Promise<Affordances<S> | null> {\n const handler = stepMap.get(ctx.toolName);\n\n if (!handler) {\n onMissingStep?.(ctx);\n return null;\n }\n\n const result = await handler(ctx);\n if (!result) return null;\n\n if (devMode && result.nextTool === \"\") {\n warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);\n }\n\n // Build decision object from flow result (spread all fields)\n const decision: Decision = { ...result };\n\n // Compute UI selections (may be empty if no registry passed)\n const selections = selectUiAffordances<S>(decision, registryIndex, {\n devMode,\n onWarn: warn,\n });\n\n return {\n advice: generateAdvice(result.nextTool),\n decision,\n selections,\n };\n },\n };\n}\n","import type {\n AffordanceRegistry,\n BindingInput,\n DefaultSlots,\n HandleRegistration,\n} from \"./types\";\nimport { normalizeBinding } from \"./types\";\n\n/**\n * Mapped type that creates a registration method for each slot in S.\n * This enables fully typed custom slots via generics.\n */\nexport type AffordanceRegistrar<S extends string = DefaultSlots> = {\n [K in S]: (handleId: string, bindsTo: BindingInput) => void;\n};\n\n/**\n * Define UI affordances for a widget using a builder pattern.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const affordances = defineAffordances((r) => {\n * r.primaryCta(\"cta.recommend\", { toolName: \"get_recommendations\" });\n * r.widgetVariant(\"variant.discovery\", { toolName: \"get_recommendations\" });\n * });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta.create\", { toolName: \"createUser\" });\n * r.contentArea(\"content.form\", { key: \"contentArea\", value: \"email-form\" });\n * r.headerStyle(\"header.progress\", { key: \"headerStyle\", value: \"step-1\" });\n * });\n * ```\n */\nexport function defineAffordances<S extends string = DefaultSlots>(\n builder: (r: AffordanceRegistrar<S>) => void\n): AffordanceRegistry<S> {\n const handles: HandleRegistration<S>[] = [];\n\n // Proxy creates methods on-the-fly for any slot name.\n // TypeScript ensures only valid slot names (from S) are called.\n const registrar = new Proxy({} as AffordanceRegistrar<S>, {\n get(_, slot: string) {\n return (handleId: string, bindsTo: BindingInput) => {\n handles.push({\n slot: slot as S,\n handleId,\n bindsTo: normalizeBinding(bindsTo),\n });\n };\n },\n });\n\n builder(registrar);\n return { handles };\n}\n","import type { AffordanceRegistry, DefaultSlots } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\nexport type MergeAffordancesOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Merge multiple affordance registries into one.\n * Generic over slot type S for custom slot support.\n *\n * @example Default slots\n * ```ts\n * const merged = mergeAffordances([widgetA, widgetB]);\n * ```\n *\n * @example Custom slots\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\";\n * const merged = mergeAffordances<MySlots>([widgetA, widgetB]);\n * ```\n */\nexport function mergeAffordances<S extends string = DefaultSlots>(\n registries: AffordanceRegistry<S>[],\n opts: MergeAffordancesOptions = {}\n): AffordanceRegistry<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const merged: AffordanceRegistry<S> = { handles: registries.flatMap((r) => r.handles) };\n\n if (!devMode) return merged;\n\n // Check for duplicate handleIds\n const seenHandleIds = new Set<string>();\n for (const h of merged.handles) {\n if (seenHandleIds.has(h.handleId)) {\n throw new Error(`[Taias] Duplicate handleId \"${h.handleId}\"`);\n }\n seenHandleIds.add(h.handleId);\n }\n\n // Check for ambiguous bindings (same slot + key + value)\n const seenTriples = new Set<string>();\n for (const h of merged.handles) {\n const k = makeBindingKey(h.slot, h.bindsTo);\n if (seenTriples.has(k)) {\n throw new Error(\n `[Taias] Ambiguous affordance: slot \"${h.slot}\" has multiple handles bound to (${h.bindsTo.key}=\"${h.bindsTo.value}\")`\n );\n }\n seenTriples.add(k);\n }\n\n warn(`[Taias] Loaded ${merged.handles.length} UI affordance handles`);\n return merged;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/flow.ts","../src/uiAffordances/types.ts","../src/uiAffordances/indexing.ts","../src/uiAffordances/select.ts","../src/createTaias.ts","../src/uiAffordances/defineAffordances.ts","../src/uiAffordances/mergeAffordances.ts"],"sourcesContent":["// Main exports\nexport { defineFlow } from \"./flow\";\nexport { createTaias } from \"./createTaias\";\n\n// UI affordances exports\nexport { defineAffordances } from \"./uiAffordances/defineAffordances\";\nexport { mergeAffordances } from \"./uiAffordances/mergeAffordances\";\nexport type { AffordanceRegistrar } from \"./uiAffordances/defineAffordances\";\nexport type {\n DefaultSlots,\n CanonicalSlot,\n Binding,\n BindingInput,\n HandleRegistration,\n Selection,\n UiSelections,\n AffordanceRegistry,\n} from \"./uiAffordances/types\";\n\n// Core + flow type exports\nexport type {\n Condition,\n FieldCondition,\n Decision,\n TaiasContext,\n StepDecision,\n Affordances,\n StepHandler,\n StepInput,\n MatchCondition,\n LogicStatement,\n FlowStep,\n FlowDefinition,\n FlowBuilder,\n TaiasOptions,\n Taias,\n} from \"./types\";\n","import type { FlowBuilder, FlowDefinition, FlowStep, MatchCondition, StepInput } from \"./types\";\n\n/**\n * Define a flow with its steps.\n *\n * @param flowId - Unique identifier for the flow\n * @param builder - Callback that receives a FlowBuilder to define steps\n * @returns A FlowDefinition object\n *\n * @example Logic statement with explicit operator\n * ```ts\n * const onboardRepoFlow = defineFlow(\"onboard_repo\", (flow) => {\n * flow.step({ toolName: { is: \"scan_repo\" } }, { nextTool: \"configure_app\" });\n * });\n * ```\n *\n * @example isNot operator\n * ```ts\n * flow.step({ toolName: { isNot: \"abort_session\" } }, { nextTool: \"continue_flow\" });\n * ```\n *\n * @example Sugar forms (backwards compatible)\n * Bare strings are sugar for { toolName: { is: string } }:\n * ```ts\n * flow.step({ toolName: \"scan_repo\" }, { nextTool: \"configure_app\" }); // sugar for { is: \"scan_repo\" }\n * flow.step(\"scan_repo\", { nextTool: \"configure_app\" }); // string sugar for { toolName: { is: \"scan_repo\" } }\n * ```\n */\nexport function defineFlow(\n flowId: string,\n builder: (flow: FlowBuilder) => void\n): FlowDefinition {\n const steps: FlowStep[] = [];\n\n const flowBuilder: FlowBuilder = {\n step(match: string | MatchCondition, input: StepInput): void {\n // Normalize: string is sugar for { toolName: string }\n const condition: MatchCondition =\n typeof match === \"string\" ? { toolName: match } : match;\n\n if (typeof input === \"function\") {\n // Handler function -- backwards-compatible escape hatch.\n // The match condition is stored alongside the handler since\n // the function itself has no formal match conditions.\n steps.push({ kind: \"handler\", match: condition, handler: input });\n } else {\n // Static logic statement -- the core primitive.\n // The statement is the sole source of truth for its match\n // conditions and decision.\n steps.push({\n kind: \"logic\",\n statement: {\n match: condition,\n decision: input,\n },\n });\n }\n },\n };\n\n builder(flowBuilder);\n\n return {\n id: flowId,\n steps,\n };\n}\n\n","/**\n * Default slots for backwards compatibility.\n * Users can define custom slots by passing a type parameter.\n */\nexport type DefaultSlots = \"primaryCta\" | \"secondaryCta\" | \"widgetVariant\";\n\n/**\n * Alias for backwards compatibility in exports.\n * @deprecated Use DefaultSlots or define your own slot type\n */\nexport type CanonicalSlot = DefaultSlots;\n\nexport type Binding = {\n key: string;\n value: string;\n};\n\n/**\n * Input format for registering affordance bindings.\n * - { toolName } is shorthand for { key: \"nextTool\", value: toolName }\n * - { key, value } is the generalized form for custom bindings\n */\nexport type BindingInput = { toolName: string } | { key: string; value: string };\n\n/**\n * A registered UI affordance handle.\n * Generic over slot type S for custom slot support.\n */\nexport type HandleRegistration<S extends string = DefaultSlots> = {\n slot: S;\n handleId: string;\n bindsTo: Binding;\n};\n\nexport type Selection = {\n handleId: string;\n bindsTo: Binding;\n};\n\n/**\n * UI selections keyed by slot name.\n * Generic over slot type S for custom slot support.\n */\nexport type UiSelections<S extends string = DefaultSlots> = Partial<Record<S, Selection>>;\n\n/**\n * Collection of registered handles.\n * Generic over slot type S for custom slot support.\n */\nexport type AffordanceRegistry<S extends string = DefaultSlots> = {\n handles: HandleRegistration<S>[];\n};\n\nexport function normalizeBinding(input: BindingInput): Binding {\n if (\"toolName\" in input) return { key: \"nextTool\", value: input.toolName };\n return { key: input.key, value: input.value };\n}\n\n/**\n * Creates a binding key for indexing.\n * Accepts any string for slot to support custom slots.\n */\nexport function makeBindingKey(slot: string, binding: Binding): string {\n return `${slot}::${binding.key}::${binding.value}`;\n}\n","import type { AffordanceRegistry, DefaultSlots, HandleRegistration } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\n/**\n * Index structure for efficient affordance lookup.\n * Generic over slot type S for custom slot support.\n */\nexport type RegistryIndex<S extends string = DefaultSlots> = {\n byBindingKey: Map<string, HandleRegistration<S>>;\n slots: Set<S>;\n /** Inferred decision field for each slot, derived from handle bindings. */\n slotKeyMap: Map<S, string>;\n};\n\n/**\n * Build an index from a registry for efficient lookup during selection.\n * Tracks which slots have registered handles and infers the decision field\n * for each slot from its handle bindings.\n *\n * @throws Error if handles for the same slot bind to different keys\n */\nexport function buildRegistryIndex<S extends string = DefaultSlots>(\n registry?: AffordanceRegistry<S>\n): RegistryIndex<S> {\n const byBindingKey = new Map<string, HandleRegistration<S>>();\n const slots = new Set<S>();\n const slotKeyMap = new Map<S, string>();\n\n if (!registry) return { byBindingKey, slots, slotKeyMap };\n\n for (const h of registry.handles) {\n slots.add(h.slot);\n\n // Infer and validate slot key\n const existingKey = slotKeyMap.get(h.slot);\n if (existingKey && existingKey !== h.bindsTo.key) {\n throw new Error(\n `[Taias] Slot \"${h.slot}\" has handles bound to different keys: \"${existingKey}\" and \"${h.bindsTo.key}\". ` +\n `All handles for a slot must use the same decision field.`\n );\n }\n slotKeyMap.set(h.slot, h.bindsTo.key);\n\n byBindingKey.set(makeBindingKey(h.slot, h.bindsTo), h);\n }\n\n return { byBindingKey, slots, slotKeyMap };\n}\n","import type { Decision } from \"../types\";\nimport type { DefaultSlots, UiSelections } from \"./types\";\nimport type { RegistryIndex } from \"./indexing\";\nimport { makeBindingKey } from \"./types\";\n\nexport type SelectOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Select UI affordances based on flow decision.\n * Uses the inferred decision field for each slot from the registry index.\n */\nexport function selectUiAffordances<S extends string = DefaultSlots>(\n decision: Decision,\n index: RegistryIndex<S>,\n opts: SelectOptions = {}\n): UiSelections<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const selections: UiSelections<S> = {};\n\n for (const slot of index.slots) {\n // Use inferred key from handle bindings\n const field = index.slotKeyMap.get(slot);\n if (!field) continue;\n\n const value = decision[field];\n if (!value) continue;\n\n const k = makeBindingKey(slot, { key: field, value });\n const handle = index.byBindingKey.get(k);\n\n if (!handle) {\n if (devMode) warn(`[Taias] No affordance for slot \"${slot}\" when ${field}=\"${value}\"`);\n continue;\n }\n\n (selections as Record<string, unknown>)[slot] = {\n handleId: handle.handleId,\n bindsTo: handle.bindsTo,\n };\n }\n\n return selections;\n}\n","import type { Affordances, Condition, FieldCondition, FlowStep, MatchCondition, StepDecision, Taias, TaiasContext, TaiasOptions, Decision } from \"./types\";\nimport type { DefaultSlots } from \"./uiAffordances/types\";\nimport { buildRegistryIndex } from \"./uiAffordances/indexing\";\nimport { selectUiAffordances } from \"./uiAffordances/select\";\n\n/**\n * Generate advice text for a given next tool.\n */\nfunction generateAdvice(nextTool: string): string {\n return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;\n}\n\n// ---------------------------------------------------------------------------\n// Condition normalization and evaluation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize a FieldCondition to a canonical Condition object.\n * A bare string is sugar for { is: string }.\n */\nfunction normalizeFieldCondition(field: FieldCondition): Condition {\n return typeof field === \"string\" ? { is: field } : field;\n}\n\n/**\n * Evaluate a single Condition against a value.\n */\nfunction evaluateCondition(condition: Condition, value: string): boolean {\n if (\"is\" in condition) return value === condition.is;\n if (\"isNot\" in condition) return value !== condition.isNot;\n return false;\n}\n\n/**\n * Evaluate a full MatchCondition against a TaiasContext.\n * All conditions in the match must be satisfied.\n */\nfunction evaluateMatch(match: MatchCondition, ctx: TaiasContext): boolean {\n const toolCondition = normalizeFieldCondition(match.toolName);\n return evaluateCondition(toolCondition, ctx.toolName);\n}\n\n/**\n * Check whether a FieldCondition is indexable (i.e., uses the `is` operator).\n * Indexable conditions enable O(1) Map lookup at resolve time.\n */\nfunction isIndexable(field: FieldCondition): boolean {\n if (typeof field === \"string\") return true;\n return \"is\" in field;\n}\n\n/**\n * Extract the index key from an indexable FieldCondition.\n * Only call this when isIndexable() returns true.\n */\nfunction indexKey(field: FieldCondition): string {\n if (typeof field === \"string\") return field;\n if (\"is\" in field) return field.is;\n throw new Error(\"Cannot derive index key from non-indexable condition\");\n}\n\n// ---------------------------------------------------------------------------\n// Step access helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Get the match condition from a FlowStep.\n *\n * - Logic-based steps: match comes from statement.match (the statement is\n * the sole source of truth for its match conditions)\n * - Handler-based steps (backwards compatibility): match is stored directly on the step\n */\nfunction getMatch(step: FlowStep): MatchCondition {\n return step.kind === \"logic\" ? step.statement.match : step.match;\n}\n\n/**\n * Serialize a MatchCondition into a stable string for duplicate detection.\n * Normalizes sugar forms so that equivalent conditions produce the same key.\n */\nfunction serializeMatch(match: MatchCondition): string {\n const normalized = normalizeFieldCondition(match.toolName);\n return JSON.stringify({ toolName: normalized });\n}\n\n/**\n * createTaias constructs a decision engine.\n *\n * Taias resolves context into a generalized Decision object,\n * and then manifests that decision into concrete affordances:\n *\n * - LLM guidance (advice)\n * - UI affordance selections\n *\n * Flow logic is expressed as logic statements -- structured data that\n * Taias understands. (Handler functions remain as a backwards-compatible\n * escape hatch.)\n * \n * Flow logic determines *what should happen next*.\n * UI affordances determine *how that decision appears in the interface*.\n *\n * This file is the boundary where:\n *\n * Inputs → Decision → Manifestations\n *\n * are unified into a single resolve() call.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const taias = createTaias({ flow, affordances });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta\", { key: \"nextTool\", value: \"createUser\" });\n * r.contentArea(\"content\", { key: \"contentArea\", value: \"form\" });\n * r.headerStyle(\"header\", { key: \"headerStyle\", value: \"progress\" });\n * });\n * const taias = createTaias<MySlots>({ flow, affordances });\n * ```\n */\nexport function createTaias<S extends string = DefaultSlots>(\n options: TaiasOptions<S>\n): Taias<S> {\n const {\n flow,\n affordances,\n devMode = false,\n onMissingStep,\n onWarn,\n } = options;\n\n const warn = onWarn ?? ((msg: string) => console.warn(msg));\n\n // Dev mode: Check for duplicate match conditions.\n // Two steps with structurally identical normalized conditions are duplicates.\n if (devMode) {\n const seenKeys = new Set<string>();\n for (const step of flow.steps) {\n const key = serializeMatch(getMatch(step));\n if (seenKeys.has(key)) {\n const match = getMatch(step);\n const normalized = normalizeFieldCondition(match.toolName);\n const label = \"is\" in normalized ? normalized.is : `isNot:${normalized.isNot}`;\n throw new Error(\n `Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`\n );\n }\n seenKeys.add(key);\n }\n }\n\n // Build internal indexes for efficient resolution.\n //\n // Steps with indexable conditions (is / string sugar) go into an exact\n // Map for O(1) lookup. Steps with non-indexable conditions (isNot) go\n // into a separate list. When no broad steps exist, resolve uses the\n // fast path (Map only). When broad steps exist, resolve evaluates all\n // steps in definition order.\n //\n // This indexing is a performance optimization derived from the current\n // set of operators, not a permanent architectural choice. It will evolve\n // as operators and match condition fields expand.\n const exactIndex = new Map<string, FlowStep>();\n const broadSteps: FlowStep[] = [];\n\n for (const step of flow.steps) {\n const match = getMatch(step);\n if (isIndexable(match.toolName)) {\n exactIndex.set(indexKey(match.toolName), step);\n } else {\n broadSteps.push(step);\n }\n }\n\n const hasBroadSteps = broadSteps.length > 0;\n\n // Build affordance index once (if provided)\n const registryIndex = buildRegistryIndex<S>(affordances);\n\n return {\n async resolve(ctx: TaiasContext): Promise<Affordances<S> | null> {\n let step: FlowStep | undefined;\n\n if (!hasBroadSteps) {\n // Fast path: all steps use indexable conditions (is / string sugar).\n // O(1) Map lookup -- same performance as before operators were introduced.\n step = exactIndex.get(ctx.toolName);\n } else {\n // Full evaluation: some steps use non-indexable conditions (isNot).\n // Evaluate all steps in definition order; first match wins.\n for (const candidate of flow.steps) {\n if (evaluateMatch(getMatch(candidate), ctx)) {\n step = candidate;\n break;\n }\n }\n }\n\n if (!step) {\n onMissingStep?.(ctx);\n return null;\n }\n\n // Evaluate the step based on its kind:\n // - Logic statements: return the decision directly (no function call)\n // - Handler functions (backwards compatibility): call the handler and await the result\n let result: StepDecision | null;\n\n if (step.kind === \"logic\") {\n result = step.statement.decision;\n } else {\n result = await step.handler(ctx);\n }\n\n if (!result) return null;\n\n if (devMode && result.nextTool === \"\") {\n warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);\n }\n\n // Build decision object from flow result (spread all fields)\n const decision: Decision = { ...result };\n\n // Compute UI selections (may be empty if no registry passed)\n const selections = selectUiAffordances<S>(decision, registryIndex, {\n devMode,\n onWarn: warn,\n });\n\n return {\n advice: generateAdvice(result.nextTool),\n decision,\n selections,\n };\n },\n };\n}\n","import type {\n AffordanceRegistry,\n BindingInput,\n DefaultSlots,\n HandleRegistration,\n} from \"./types\";\nimport { normalizeBinding } from \"./types\";\n\n/**\n * Mapped type that creates a registration method for each slot in S.\n * This enables fully typed custom slots via generics.\n */\nexport type AffordanceRegistrar<S extends string = DefaultSlots> = {\n [K in S]: (handleId: string, bindsTo: BindingInput) => void;\n};\n\n/**\n * Define UI affordances for a widget using a builder pattern.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const affordances = defineAffordances((r) => {\n * r.primaryCta(\"cta.recommend\", { toolName: \"get_recommendations\" });\n * r.widgetVariant(\"variant.discovery\", { toolName: \"get_recommendations\" });\n * });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta.create\", { toolName: \"createUser\" });\n * r.contentArea(\"content.form\", { key: \"contentArea\", value: \"email-form\" });\n * r.headerStyle(\"header.progress\", { key: \"headerStyle\", value: \"step-1\" });\n * });\n * ```\n */\nexport function defineAffordances<S extends string = DefaultSlots>(\n builder: (r: AffordanceRegistrar<S>) => void\n): AffordanceRegistry<S> {\n const handles: HandleRegistration<S>[] = [];\n\n // Proxy creates methods on-the-fly for any slot name.\n // TypeScript ensures only valid slot names (from S) are called.\n const registrar = new Proxy({} as AffordanceRegistrar<S>, {\n get(_, slot: string) {\n return (handleId: string, bindsTo: BindingInput) => {\n handles.push({\n slot: slot as S,\n handleId,\n bindsTo: normalizeBinding(bindsTo),\n });\n };\n },\n });\n\n builder(registrar);\n return { handles };\n}\n","import type { AffordanceRegistry, DefaultSlots } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\nexport type MergeAffordancesOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Merge multiple affordance registries into one.\n * Generic over slot type S for custom slot support.\n *\n * @example Default slots\n * ```ts\n * const merged = mergeAffordances([widgetA, widgetB]);\n * ```\n *\n * @example Custom slots\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\";\n * const merged = mergeAffordances<MySlots>([widgetA, widgetB]);\n * ```\n */\nexport function mergeAffordances<S extends string = DefaultSlots>(\n registries: AffordanceRegistry<S>[],\n opts: MergeAffordancesOptions = {}\n): AffordanceRegistry<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const merged: AffordanceRegistry<S> = { handles: registries.flatMap((r) => r.handles) };\n\n if (!devMode) return merged;\n\n // Check for duplicate handleIds\n const seenHandleIds = new Set<string>();\n for (const h of merged.handles) {\n if (seenHandleIds.has(h.handleId)) {\n throw new Error(`[Taias] Duplicate handleId \"${h.handleId}\"`);\n }\n seenHandleIds.add(h.handleId);\n }\n\n // Check for ambiguous bindings (same slot + key + value)\n const seenTriples = new Set<string>();\n for (const h of merged.handles) {\n const k = makeBindingKey(h.slot, h.bindsTo);\n if (seenTriples.has(k)) {\n throw new Error(\n `[Taias] Ambiguous affordance: slot \"${h.slot}\" has multiple handles bound to (${h.bindsTo.key}=\"${h.bindsTo.value}\")`\n );\n }\n seenTriples.add(k);\n }\n\n warn(`[Taias] Loaded ${merged.handles.length} UI affordance handles`);\n return merged;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -93,10 +93,65 @@ type Affordances<S extends string = DefaultSlots> = {
93
93
  */
94
94
  type StepHandler = (ctx: TaiasContext) => StepDecision | null | Promise<StepDecision | null>;
95
95
  /**
96
- * A step within a flow, mapping a tool name to its handler.
96
+ * A condition operator applied to a single field value.
97
+ *
98
+ * - { is: "value" } -- exact equality (field === value)
99
+ * - { isNot: "value" } -- not equal (field !== value)
100
+ *
101
+ * The operator system is pure data (not wrapper functions), aligning with
102
+ * the logic-as-data philosophy. New operators (oneOf, contains, etc.) can
103
+ * be added as union members without changing the evaluation architecture.
104
+ */
105
+ type Condition = {
106
+ is: string;
107
+ } | {
108
+ isNot: string;
109
+ };
110
+ /**
111
+ * A field condition is either:
112
+ * - A bare string: sugar for { is: string }
113
+ * - An explicit Condition object
114
+ */
115
+ type FieldCondition = string | Condition;
116
+ /**
117
+ * Match condition for a logic statement.
118
+ *
119
+ * Each field accepts a FieldCondition -- either a bare value (sugar for
120
+ * { is: value }) or an explicit operator object ({ is: ... }, { isNot: ... }).
121
+ *
122
+ * Designed to expand with additional fields (params, result, state, etc.).
123
+ */
124
+ type MatchCondition = {
125
+ toolName: FieldCondition;
126
+ };
127
+ /**
128
+ * A declarative logic statement -- the core primitive of the decision engine.
129
+ *
130
+ * Formalizes the implicit "Given X, then Y" logic into structured data
131
+ * that Taias can understand, validate, and optimize.
132
+ *
133
+ * - match: the conditions under which this statement applies
134
+ * - decision: the decision to produce when matched
135
+ */
136
+ type LogicStatement = {
137
+ match: MatchCondition;
138
+ decision: StepDecision;
139
+ };
140
+ /**
141
+ * A step within a flow. Discriminated union:
142
+ *
143
+ * - "logic": A declarative logic statement. The statement is the sole source
144
+ * of truth for its match conditions and decision.
145
+ * - "handler": A handler function (backwards-compatible escape hatch).
146
+ * The match condition is stored alongside the handler since the function
147
+ * itself has no formal match conditions.
97
148
  */
98
149
  type FlowStep = {
99
- toolName: string;
150
+ kind: "logic";
151
+ statement: LogicStatement;
152
+ } | {
153
+ kind: "handler";
154
+ match: MatchCondition;
100
155
  handler: StepHandler;
101
156
  };
102
157
  /**
@@ -106,11 +161,22 @@ type FlowDefinition = {
106
161
  id: string;
107
162
  steps: Array<FlowStep>;
108
163
  };
164
+ /**
165
+ * The input accepted by flow.step() -- either a handler function
166
+ * or a static StepDecision object.
167
+ */
168
+ type StepInput = StepHandler | StepDecision;
109
169
  /**
110
170
  * Builder interface for defining flow steps.
171
+ *
172
+ * step() takes two arguments:
173
+ * - match: a MatchCondition object describing the conditions under which
174
+ * this step applies. A string is sugar for { toolName: string }.
175
+ * - input: a StepDecision object (creates a logic statement).
176
+ * A StepHandler function is also accepted for backwards compatibility.
111
177
  */
112
178
  interface FlowBuilder {
113
- step(toolName: string, handler: StepHandler): void;
179
+ step(match: string | MatchCondition, input: StepInput): void;
114
180
  }
115
181
  /**
116
182
  * Options for creating a Taias instance.
@@ -138,26 +204,40 @@ interface Taias<S extends string = DefaultSlots> {
138
204
  * @param builder - Callback that receives a FlowBuilder to define steps
139
205
  * @returns A FlowDefinition object
140
206
  *
141
- * @example
207
+ * @example Logic statement with explicit operator
142
208
  * ```ts
143
209
  * const onboardRepoFlow = defineFlow("onboard_repo", (flow) => {
144
- * flow.step("scan_repo", (ctx) => ({
145
- * nextTool: "configure_app",
146
- * }));
210
+ * flow.step({ toolName: { is: "scan_repo" } }, { nextTool: "configure_app" });
147
211
  * });
148
212
  * ```
213
+ *
214
+ * @example isNot operator
215
+ * ```ts
216
+ * flow.step({ toolName: { isNot: "abort_session" } }, { nextTool: "continue_flow" });
217
+ * ```
218
+ *
219
+ * @example Sugar forms (backwards compatible)
220
+ * Bare strings are sugar for { toolName: { is: string } }:
221
+ * ```ts
222
+ * flow.step({ toolName: "scan_repo" }, { nextTool: "configure_app" }); // sugar for { is: "scan_repo" }
223
+ * flow.step("scan_repo", { nextTool: "configure_app" }); // string sugar for { toolName: { is: "scan_repo" } }
224
+ * ```
149
225
  */
150
226
  declare function defineFlow(flowId: string, builder: (flow: FlowBuilder) => void): FlowDefinition;
151
227
 
152
228
  /**
153
229
  * createTaias constructs a decision engine.
154
230
  *
155
- * Taias resolves tool context into a generalized Decision object,
231
+ * Taias resolves context into a generalized Decision object,
156
232
  * and then manifests that decision into concrete affordances:
157
233
  *
158
234
  * - LLM guidance (advice)
159
235
  * - UI affordance selections
160
236
  *
237
+ * Flow logic is expressed as logic statements -- structured data that
238
+ * Taias understands. (Handler functions remain as a backwards-compatible
239
+ * escape hatch.)
240
+ *
161
241
  * Flow logic determines *what should happen next*.
162
242
  * UI affordances determine *how that decision appears in the interface*.
163
243
  *
@@ -236,4 +316,4 @@ type MergeAffordancesOptions = {
236
316
  */
237
317
  declare function mergeAffordances<S extends string = DefaultSlots>(registries: AffordanceRegistry<S>[], opts?: MergeAffordancesOptions): AffordanceRegistry<S>;
238
318
 
239
- export { type AffordanceRegistrar, type AffordanceRegistry, type Affordances, type Binding, type BindingInput, type CanonicalSlot, type Decision, type DefaultSlots, type FlowBuilder, type FlowDefinition, type FlowStep, type HandleRegistration, type Selection, type StepDecision, type StepHandler, type Taias, type TaiasContext, type TaiasOptions, type UiSelections, createTaias, defineAffordances, defineFlow, mergeAffordances };
319
+ export { type AffordanceRegistrar, type AffordanceRegistry, type Affordances, type Binding, type BindingInput, type CanonicalSlot, type Condition, type Decision, type DefaultSlots, type FieldCondition, type FlowBuilder, type FlowDefinition, type FlowStep, type HandleRegistration, type LogicStatement, type MatchCondition, type Selection, type StepDecision, type StepHandler, type StepInput, type Taias, type TaiasContext, type TaiasOptions, type UiSelections, createTaias, defineAffordances, defineFlow, mergeAffordances };
package/dist/index.d.ts CHANGED
@@ -93,10 +93,65 @@ type Affordances<S extends string = DefaultSlots> = {
93
93
  */
94
94
  type StepHandler = (ctx: TaiasContext) => StepDecision | null | Promise<StepDecision | null>;
95
95
  /**
96
- * A step within a flow, mapping a tool name to its handler.
96
+ * A condition operator applied to a single field value.
97
+ *
98
+ * - { is: "value" } -- exact equality (field === value)
99
+ * - { isNot: "value" } -- not equal (field !== value)
100
+ *
101
+ * The operator system is pure data (not wrapper functions), aligning with
102
+ * the logic-as-data philosophy. New operators (oneOf, contains, etc.) can
103
+ * be added as union members without changing the evaluation architecture.
104
+ */
105
+ type Condition = {
106
+ is: string;
107
+ } | {
108
+ isNot: string;
109
+ };
110
+ /**
111
+ * A field condition is either:
112
+ * - A bare string: sugar for { is: string }
113
+ * - An explicit Condition object
114
+ */
115
+ type FieldCondition = string | Condition;
116
+ /**
117
+ * Match condition for a logic statement.
118
+ *
119
+ * Each field accepts a FieldCondition -- either a bare value (sugar for
120
+ * { is: value }) or an explicit operator object ({ is: ... }, { isNot: ... }).
121
+ *
122
+ * Designed to expand with additional fields (params, result, state, etc.).
123
+ */
124
+ type MatchCondition = {
125
+ toolName: FieldCondition;
126
+ };
127
+ /**
128
+ * A declarative logic statement -- the core primitive of the decision engine.
129
+ *
130
+ * Formalizes the implicit "Given X, then Y" logic into structured data
131
+ * that Taias can understand, validate, and optimize.
132
+ *
133
+ * - match: the conditions under which this statement applies
134
+ * - decision: the decision to produce when matched
135
+ */
136
+ type LogicStatement = {
137
+ match: MatchCondition;
138
+ decision: StepDecision;
139
+ };
140
+ /**
141
+ * A step within a flow. Discriminated union:
142
+ *
143
+ * - "logic": A declarative logic statement. The statement is the sole source
144
+ * of truth for its match conditions and decision.
145
+ * - "handler": A handler function (backwards-compatible escape hatch).
146
+ * The match condition is stored alongside the handler since the function
147
+ * itself has no formal match conditions.
97
148
  */
98
149
  type FlowStep = {
99
- toolName: string;
150
+ kind: "logic";
151
+ statement: LogicStatement;
152
+ } | {
153
+ kind: "handler";
154
+ match: MatchCondition;
100
155
  handler: StepHandler;
101
156
  };
102
157
  /**
@@ -106,11 +161,22 @@ type FlowDefinition = {
106
161
  id: string;
107
162
  steps: Array<FlowStep>;
108
163
  };
164
+ /**
165
+ * The input accepted by flow.step() -- either a handler function
166
+ * or a static StepDecision object.
167
+ */
168
+ type StepInput = StepHandler | StepDecision;
109
169
  /**
110
170
  * Builder interface for defining flow steps.
171
+ *
172
+ * step() takes two arguments:
173
+ * - match: a MatchCondition object describing the conditions under which
174
+ * this step applies. A string is sugar for { toolName: string }.
175
+ * - input: a StepDecision object (creates a logic statement).
176
+ * A StepHandler function is also accepted for backwards compatibility.
111
177
  */
112
178
  interface FlowBuilder {
113
- step(toolName: string, handler: StepHandler): void;
179
+ step(match: string | MatchCondition, input: StepInput): void;
114
180
  }
115
181
  /**
116
182
  * Options for creating a Taias instance.
@@ -138,26 +204,40 @@ interface Taias<S extends string = DefaultSlots> {
138
204
  * @param builder - Callback that receives a FlowBuilder to define steps
139
205
  * @returns A FlowDefinition object
140
206
  *
141
- * @example
207
+ * @example Logic statement with explicit operator
142
208
  * ```ts
143
209
  * const onboardRepoFlow = defineFlow("onboard_repo", (flow) => {
144
- * flow.step("scan_repo", (ctx) => ({
145
- * nextTool: "configure_app",
146
- * }));
210
+ * flow.step({ toolName: { is: "scan_repo" } }, { nextTool: "configure_app" });
147
211
  * });
148
212
  * ```
213
+ *
214
+ * @example isNot operator
215
+ * ```ts
216
+ * flow.step({ toolName: { isNot: "abort_session" } }, { nextTool: "continue_flow" });
217
+ * ```
218
+ *
219
+ * @example Sugar forms (backwards compatible)
220
+ * Bare strings are sugar for { toolName: { is: string } }:
221
+ * ```ts
222
+ * flow.step({ toolName: "scan_repo" }, { nextTool: "configure_app" }); // sugar for { is: "scan_repo" }
223
+ * flow.step("scan_repo", { nextTool: "configure_app" }); // string sugar for { toolName: { is: "scan_repo" } }
224
+ * ```
149
225
  */
150
226
  declare function defineFlow(flowId: string, builder: (flow: FlowBuilder) => void): FlowDefinition;
151
227
 
152
228
  /**
153
229
  * createTaias constructs a decision engine.
154
230
  *
155
- * Taias resolves tool context into a generalized Decision object,
231
+ * Taias resolves context into a generalized Decision object,
156
232
  * and then manifests that decision into concrete affordances:
157
233
  *
158
234
  * - LLM guidance (advice)
159
235
  * - UI affordance selections
160
236
  *
237
+ * Flow logic is expressed as logic statements -- structured data that
238
+ * Taias understands. (Handler functions remain as a backwards-compatible
239
+ * escape hatch.)
240
+ *
161
241
  * Flow logic determines *what should happen next*.
162
242
  * UI affordances determine *how that decision appears in the interface*.
163
243
  *
@@ -236,4 +316,4 @@ type MergeAffordancesOptions = {
236
316
  */
237
317
  declare function mergeAffordances<S extends string = DefaultSlots>(registries: AffordanceRegistry<S>[], opts?: MergeAffordancesOptions): AffordanceRegistry<S>;
238
318
 
239
- export { type AffordanceRegistrar, type AffordanceRegistry, type Affordances, type Binding, type BindingInput, type CanonicalSlot, type Decision, type DefaultSlots, type FlowBuilder, type FlowDefinition, type FlowStep, type HandleRegistration, type Selection, type StepDecision, type StepHandler, type Taias, type TaiasContext, type TaiasOptions, type UiSelections, createTaias, defineAffordances, defineFlow, mergeAffordances };
319
+ export { type AffordanceRegistrar, type AffordanceRegistry, type Affordances, type Binding, type BindingInput, type CanonicalSlot, type Condition, type Decision, type DefaultSlots, type FieldCondition, type FlowBuilder, type FlowDefinition, type FlowStep, type HandleRegistration, type LogicStatement, type MatchCondition, type Selection, type StepDecision, type StepHandler, type StepInput, type Taias, type TaiasContext, type TaiasOptions, type UiSelections, createTaias, defineAffordances, defineFlow, mergeAffordances };
package/dist/index.js CHANGED
@@ -2,8 +2,19 @@
2
2
  function defineFlow(flowId, builder) {
3
3
  const steps = [];
4
4
  const flowBuilder = {
5
- step(toolName, handler) {
6
- steps.push({ toolName, handler });
5
+ step(match, input) {
6
+ const condition = typeof match === "string" ? { toolName: match } : match;
7
+ if (typeof input === "function") {
8
+ steps.push({ kind: "handler", match: condition, handler: input });
9
+ } else {
10
+ steps.push({
11
+ kind: "logic",
12
+ statement: {
13
+ match: condition,
14
+ decision: input
15
+ }
16
+ });
17
+ }
7
18
  }
8
19
  };
9
20
  builder(flowBuilder);
@@ -71,6 +82,34 @@ function selectUiAffordances(decision, index, opts = {}) {
71
82
  function generateAdvice(nextTool) {
72
83
  return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;
73
84
  }
85
+ function normalizeFieldCondition(field) {
86
+ return typeof field === "string" ? { is: field } : field;
87
+ }
88
+ function evaluateCondition(condition, value) {
89
+ if ("is" in condition) return value === condition.is;
90
+ if ("isNot" in condition) return value !== condition.isNot;
91
+ return false;
92
+ }
93
+ function evaluateMatch(match, ctx) {
94
+ const toolCondition = normalizeFieldCondition(match.toolName);
95
+ return evaluateCondition(toolCondition, ctx.toolName);
96
+ }
97
+ function isIndexable(field) {
98
+ if (typeof field === "string") return true;
99
+ return "is" in field;
100
+ }
101
+ function indexKey(field) {
102
+ if (typeof field === "string") return field;
103
+ if ("is" in field) return field.is;
104
+ throw new Error("Cannot derive index key from non-indexable condition");
105
+ }
106
+ function getMatch(step) {
107
+ return step.kind === "logic" ? step.statement.match : step.match;
108
+ }
109
+ function serializeMatch(match) {
110
+ const normalized = normalizeFieldCondition(match.toolName);
111
+ return JSON.stringify({ toolName: normalized });
112
+ }
74
113
  function createTaias(options) {
75
114
  const {
76
115
  flow,
@@ -81,26 +120,55 @@ function createTaias(options) {
81
120
  } = options;
82
121
  const warn = onWarn ?? ((msg) => console.warn(msg));
83
122
  if (devMode) {
84
- const seenTools = /* @__PURE__ */ new Set();
123
+ const seenKeys = /* @__PURE__ */ new Set();
85
124
  for (const step of flow.steps) {
86
- if (seenTools.has(step.toolName)) {
125
+ const key = serializeMatch(getMatch(step));
126
+ if (seenKeys.has(key)) {
127
+ const match = getMatch(step);
128
+ const normalized = normalizeFieldCondition(match.toolName);
129
+ const label = "is" in normalized ? normalized.is : `isNot:${normalized.isNot}`;
87
130
  throw new Error(
88
- `Taias: Duplicate step for tool '${step.toolName}' in flow '${flow.id}'. Only one handler per tool is supported.`
131
+ `Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`
89
132
  );
90
133
  }
91
- seenTools.add(step.toolName);
134
+ seenKeys.add(key);
135
+ }
136
+ }
137
+ const exactIndex = /* @__PURE__ */ new Map();
138
+ const broadSteps = [];
139
+ for (const step of flow.steps) {
140
+ const match = getMatch(step);
141
+ if (isIndexable(match.toolName)) {
142
+ exactIndex.set(indexKey(match.toolName), step);
143
+ } else {
144
+ broadSteps.push(step);
92
145
  }
93
146
  }
94
- const stepMap = new Map(flow.steps.map((step) => [step.toolName, step.handler]));
147
+ const hasBroadSteps = broadSteps.length > 0;
95
148
  const registryIndex = buildRegistryIndex(affordances);
96
149
  return {
97
150
  async resolve(ctx) {
98
- const handler = stepMap.get(ctx.toolName);
99
- if (!handler) {
151
+ let step;
152
+ if (!hasBroadSteps) {
153
+ step = exactIndex.get(ctx.toolName);
154
+ } else {
155
+ for (const candidate of flow.steps) {
156
+ if (evaluateMatch(getMatch(candidate), ctx)) {
157
+ step = candidate;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ if (!step) {
100
163
  onMissingStep?.(ctx);
101
164
  return null;
102
165
  }
103
- const result = await handler(ctx);
166
+ let result;
167
+ if (step.kind === "logic") {
168
+ result = step.statement.decision;
169
+ } else {
170
+ result = await step.handler(ctx);
171
+ }
104
172
  if (!result) return null;
105
173
  if (devMode && result.nextTool === "") {
106
174
  warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/flow.ts","../src/uiAffordances/types.ts","../src/uiAffordances/indexing.ts","../src/uiAffordances/select.ts","../src/createTaias.ts","../src/uiAffordances/defineAffordances.ts","../src/uiAffordances/mergeAffordances.ts"],"sourcesContent":["import type { FlowBuilder, FlowDefinition, FlowStep, StepHandler } from \"./types\";\n\n/**\n * Define a flow with its steps.\n *\n * @param flowId - Unique identifier for the flow\n * @param builder - Callback that receives a FlowBuilder to define steps\n * @returns A FlowDefinition object\n *\n * @example\n * ```ts\n * const onboardRepoFlow = defineFlow(\"onboard_repo\", (flow) => {\n * flow.step(\"scan_repo\", (ctx) => ({\n * nextTool: \"configure_app\",\n * }));\n * });\n * ```\n */\nexport function defineFlow(\n flowId: string,\n builder: (flow: FlowBuilder) => void\n): FlowDefinition {\n const steps: FlowStep[] = [];\n\n const flowBuilder: FlowBuilder = {\n step(toolName: string, handler: StepHandler): void {\n steps.push({ toolName, handler });\n },\n };\n\n builder(flowBuilder);\n\n return {\n id: flowId,\n steps,\n };\n}\n\n","/**\n * Default slots for backwards compatibility.\n * Users can define custom slots by passing a type parameter.\n */\nexport type DefaultSlots = \"primaryCta\" | \"secondaryCta\" | \"widgetVariant\";\n\n/**\n * Alias for backwards compatibility in exports.\n * @deprecated Use DefaultSlots or define your own slot type\n */\nexport type CanonicalSlot = DefaultSlots;\n\nexport type Binding = {\n key: string;\n value: string;\n};\n\n/**\n * Input format for registering affordance bindings.\n * - { toolName } is shorthand for { key: \"nextTool\", value: toolName }\n * - { key, value } is the generalized form for custom bindings\n */\nexport type BindingInput = { toolName: string } | { key: string; value: string };\n\n/**\n * A registered UI affordance handle.\n * Generic over slot type S for custom slot support.\n */\nexport type HandleRegistration<S extends string = DefaultSlots> = {\n slot: S;\n handleId: string;\n bindsTo: Binding;\n};\n\nexport type Selection = {\n handleId: string;\n bindsTo: Binding;\n};\n\n/**\n * UI selections keyed by slot name.\n * Generic over slot type S for custom slot support.\n */\nexport type UiSelections<S extends string = DefaultSlots> = Partial<Record<S, Selection>>;\n\n/**\n * Collection of registered handles.\n * Generic over slot type S for custom slot support.\n */\nexport type AffordanceRegistry<S extends string = DefaultSlots> = {\n handles: HandleRegistration<S>[];\n};\n\nexport function normalizeBinding(input: BindingInput): Binding {\n if (\"toolName\" in input) return { key: \"nextTool\", value: input.toolName };\n return { key: input.key, value: input.value };\n}\n\n/**\n * Creates a binding key for indexing.\n * Accepts any string for slot to support custom slots.\n */\nexport function makeBindingKey(slot: string, binding: Binding): string {\n return `${slot}::${binding.key}::${binding.value}`;\n}\n","import type { AffordanceRegistry, DefaultSlots, HandleRegistration } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\n/**\n * Index structure for efficient affordance lookup.\n * Generic over slot type S for custom slot support.\n */\nexport type RegistryIndex<S extends string = DefaultSlots> = {\n byBindingKey: Map<string, HandleRegistration<S>>;\n slots: Set<S>;\n /** Inferred decision field for each slot, derived from handle bindings. */\n slotKeyMap: Map<S, string>;\n};\n\n/**\n * Build an index from a registry for efficient lookup during selection.\n * Tracks which slots have registered handles and infers the decision field\n * for each slot from its handle bindings.\n *\n * @throws Error if handles for the same slot bind to different keys\n */\nexport function buildRegistryIndex<S extends string = DefaultSlots>(\n registry?: AffordanceRegistry<S>\n): RegistryIndex<S> {\n const byBindingKey = new Map<string, HandleRegistration<S>>();\n const slots = new Set<S>();\n const slotKeyMap = new Map<S, string>();\n\n if (!registry) return { byBindingKey, slots, slotKeyMap };\n\n for (const h of registry.handles) {\n slots.add(h.slot);\n\n // Infer and validate slot key\n const existingKey = slotKeyMap.get(h.slot);\n if (existingKey && existingKey !== h.bindsTo.key) {\n throw new Error(\n `[Taias] Slot \"${h.slot}\" has handles bound to different keys: \"${existingKey}\" and \"${h.bindsTo.key}\". ` +\n `All handles for a slot must use the same decision field.`\n );\n }\n slotKeyMap.set(h.slot, h.bindsTo.key);\n\n byBindingKey.set(makeBindingKey(h.slot, h.bindsTo), h);\n }\n\n return { byBindingKey, slots, slotKeyMap };\n}\n","import type { Decision } from \"../types\";\nimport type { DefaultSlots, UiSelections } from \"./types\";\nimport type { RegistryIndex } from \"./indexing\";\nimport { makeBindingKey } from \"./types\";\n\nexport type SelectOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Select UI affordances based on flow decision.\n * Uses the inferred decision field for each slot from the registry index.\n */\nexport function selectUiAffordances<S extends string = DefaultSlots>(\n decision: Decision,\n index: RegistryIndex<S>,\n opts: SelectOptions = {}\n): UiSelections<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const selections: UiSelections<S> = {};\n\n for (const slot of index.slots) {\n // Use inferred key from handle bindings\n const field = index.slotKeyMap.get(slot);\n if (!field) continue;\n\n const value = decision[field];\n if (!value) continue;\n\n const k = makeBindingKey(slot, { key: field, value });\n const handle = index.byBindingKey.get(k);\n\n if (!handle) {\n if (devMode) warn(`[Taias] No affordance for slot \"${slot}\" when ${field}=\"${value}\"`);\n continue;\n }\n\n (selections as Record<string, unknown>)[slot] = {\n handleId: handle.handleId,\n bindsTo: handle.bindsTo,\n };\n }\n\n return selections;\n}\n","import type { Affordances, Taias, TaiasContext, TaiasOptions, Decision } from \"./types\";\nimport type { DefaultSlots } from \"./uiAffordances/types\";\nimport { buildRegistryIndex } from \"./uiAffordances/indexing\";\nimport { selectUiAffordances } from \"./uiAffordances/select\";\n\n/**\n * Generate advice text for a given next tool.\n */\nfunction generateAdvice(nextTool: string): string {\n return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;\n}\n\n/**\n * createTaias constructs a decision engine.\n *\n * Taias resolves tool context into a generalized Decision object,\n * and then manifests that decision into concrete affordances:\n *\n * - LLM guidance (advice)\n * - UI affordance selections\n *\n * Flow logic determines *what should happen next*.\n * UI affordances determine *how that decision appears in the interface*.\n *\n * This file is the boundary where:\n *\n * Inputs → Decision → Manifestations\n *\n * are unified into a single resolve() call.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const taias = createTaias({ flow, affordances });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta\", { key: \"nextTool\", value: \"createUser\" });\n * r.contentArea(\"content\", { key: \"contentArea\", value: \"form\" });\n * r.headerStyle(\"header\", { key: \"headerStyle\", value: \"progress\" });\n * });\n * const taias = createTaias<MySlots>({ flow, affordances });\n * ```\n */\nexport function createTaias<S extends string = DefaultSlots>(\n options: TaiasOptions<S>\n): Taias<S> {\n const {\n flow,\n affordances,\n devMode = false,\n onMissingStep,\n onWarn,\n } = options;\n\n const warn = onWarn ?? ((msg: string) => console.warn(msg));\n\n // Dev mode: Check for duplicate toolNames\n if (devMode) {\n const seenTools = new Set<string>();\n for (const step of flow.steps) {\n if (seenTools.has(step.toolName)) {\n throw new Error(\n `Taias: Duplicate step for tool '${step.toolName}' in flow '${flow.id}'. Only one handler per tool is supported.`\n );\n }\n seenTools.add(step.toolName);\n }\n }\n\n // Build a lookup map for efficient resolution\n const stepMap = new Map(flow.steps.map((step) => [step.toolName, step.handler]));\n\n // Build affordance index once (if provided)\n const registryIndex = buildRegistryIndex<S>(affordances);\n\n return {\n async resolve(ctx: TaiasContext): Promise<Affordances<S> | null> {\n const handler = stepMap.get(ctx.toolName);\n\n if (!handler) {\n onMissingStep?.(ctx);\n return null;\n }\n\n const result = await handler(ctx);\n if (!result) return null;\n\n if (devMode && result.nextTool === \"\") {\n warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);\n }\n\n // Build decision object from flow result (spread all fields)\n const decision: Decision = { ...result };\n\n // Compute UI selections (may be empty if no registry passed)\n const selections = selectUiAffordances<S>(decision, registryIndex, {\n devMode,\n onWarn: warn,\n });\n\n return {\n advice: generateAdvice(result.nextTool),\n decision,\n selections,\n };\n },\n };\n}\n","import type {\n AffordanceRegistry,\n BindingInput,\n DefaultSlots,\n HandleRegistration,\n} from \"./types\";\nimport { normalizeBinding } from \"./types\";\n\n/**\n * Mapped type that creates a registration method for each slot in S.\n * This enables fully typed custom slots via generics.\n */\nexport type AffordanceRegistrar<S extends string = DefaultSlots> = {\n [K in S]: (handleId: string, bindsTo: BindingInput) => void;\n};\n\n/**\n * Define UI affordances for a widget using a builder pattern.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const affordances = defineAffordances((r) => {\n * r.primaryCta(\"cta.recommend\", { toolName: \"get_recommendations\" });\n * r.widgetVariant(\"variant.discovery\", { toolName: \"get_recommendations\" });\n * });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta.create\", { toolName: \"createUser\" });\n * r.contentArea(\"content.form\", { key: \"contentArea\", value: \"email-form\" });\n * r.headerStyle(\"header.progress\", { key: \"headerStyle\", value: \"step-1\" });\n * });\n * ```\n */\nexport function defineAffordances<S extends string = DefaultSlots>(\n builder: (r: AffordanceRegistrar<S>) => void\n): AffordanceRegistry<S> {\n const handles: HandleRegistration<S>[] = [];\n\n // Proxy creates methods on-the-fly for any slot name.\n // TypeScript ensures only valid slot names (from S) are called.\n const registrar = new Proxy({} as AffordanceRegistrar<S>, {\n get(_, slot: string) {\n return (handleId: string, bindsTo: BindingInput) => {\n handles.push({\n slot: slot as S,\n handleId,\n bindsTo: normalizeBinding(bindsTo),\n });\n };\n },\n });\n\n builder(registrar);\n return { handles };\n}\n","import type { AffordanceRegistry, DefaultSlots } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\nexport type MergeAffordancesOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Merge multiple affordance registries into one.\n * Generic over slot type S for custom slot support.\n *\n * @example Default slots\n * ```ts\n * const merged = mergeAffordances([widgetA, widgetB]);\n * ```\n *\n * @example Custom slots\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\";\n * const merged = mergeAffordances<MySlots>([widgetA, widgetB]);\n * ```\n */\nexport function mergeAffordances<S extends string = DefaultSlots>(\n registries: AffordanceRegistry<S>[],\n opts: MergeAffordancesOptions = {}\n): AffordanceRegistry<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const merged: AffordanceRegistry<S> = { handles: registries.flatMap((r) => r.handles) };\n\n if (!devMode) return merged;\n\n // Check for duplicate handleIds\n const seenHandleIds = new Set<string>();\n for (const h of merged.handles) {\n if (seenHandleIds.has(h.handleId)) {\n throw new Error(`[Taias] Duplicate handleId \"${h.handleId}\"`);\n }\n seenHandleIds.add(h.handleId);\n }\n\n // Check for ambiguous bindings (same slot + key + value)\n const seenTriples = new Set<string>();\n for (const h of merged.handles) {\n const k = makeBindingKey(h.slot, h.bindsTo);\n if (seenTriples.has(k)) {\n throw new Error(\n `[Taias] Ambiguous affordance: slot \"${h.slot}\" has multiple handles bound to (${h.bindsTo.key}=\"${h.bindsTo.value}\")`\n );\n }\n seenTriples.add(k);\n }\n\n warn(`[Taias] Loaded ${merged.handles.length} UI affordance handles`);\n return merged;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/flow.ts","../src/uiAffordances/types.ts","../src/uiAffordances/indexing.ts","../src/uiAffordances/select.ts","../src/createTaias.ts","../src/uiAffordances/defineAffordances.ts","../src/uiAffordances/mergeAffordances.ts"],"sourcesContent":["import type { FlowBuilder, FlowDefinition, FlowStep, MatchCondition, StepInput } from \"./types\";\n\n/**\n * Define a flow with its steps.\n *\n * @param flowId - Unique identifier for the flow\n * @param builder - Callback that receives a FlowBuilder to define steps\n * @returns A FlowDefinition object\n *\n * @example Logic statement with explicit operator\n * ```ts\n * const onboardRepoFlow = defineFlow(\"onboard_repo\", (flow) => {\n * flow.step({ toolName: { is: \"scan_repo\" } }, { nextTool: \"configure_app\" });\n * });\n * ```\n *\n * @example isNot operator\n * ```ts\n * flow.step({ toolName: { isNot: \"abort_session\" } }, { nextTool: \"continue_flow\" });\n * ```\n *\n * @example Sugar forms (backwards compatible)\n * Bare strings are sugar for { toolName: { is: string } }:\n * ```ts\n * flow.step({ toolName: \"scan_repo\" }, { nextTool: \"configure_app\" }); // sugar for { is: \"scan_repo\" }\n * flow.step(\"scan_repo\", { nextTool: \"configure_app\" }); // string sugar for { toolName: { is: \"scan_repo\" } }\n * ```\n */\nexport function defineFlow(\n flowId: string,\n builder: (flow: FlowBuilder) => void\n): FlowDefinition {\n const steps: FlowStep[] = [];\n\n const flowBuilder: FlowBuilder = {\n step(match: string | MatchCondition, input: StepInput): void {\n // Normalize: string is sugar for { toolName: string }\n const condition: MatchCondition =\n typeof match === \"string\" ? { toolName: match } : match;\n\n if (typeof input === \"function\") {\n // Handler function -- backwards-compatible escape hatch.\n // The match condition is stored alongside the handler since\n // the function itself has no formal match conditions.\n steps.push({ kind: \"handler\", match: condition, handler: input });\n } else {\n // Static logic statement -- the core primitive.\n // The statement is the sole source of truth for its match\n // conditions and decision.\n steps.push({\n kind: \"logic\",\n statement: {\n match: condition,\n decision: input,\n },\n });\n }\n },\n };\n\n builder(flowBuilder);\n\n return {\n id: flowId,\n steps,\n };\n}\n\n","/**\n * Default slots for backwards compatibility.\n * Users can define custom slots by passing a type parameter.\n */\nexport type DefaultSlots = \"primaryCta\" | \"secondaryCta\" | \"widgetVariant\";\n\n/**\n * Alias for backwards compatibility in exports.\n * @deprecated Use DefaultSlots or define your own slot type\n */\nexport type CanonicalSlot = DefaultSlots;\n\nexport type Binding = {\n key: string;\n value: string;\n};\n\n/**\n * Input format for registering affordance bindings.\n * - { toolName } is shorthand for { key: \"nextTool\", value: toolName }\n * - { key, value } is the generalized form for custom bindings\n */\nexport type BindingInput = { toolName: string } | { key: string; value: string };\n\n/**\n * A registered UI affordance handle.\n * Generic over slot type S for custom slot support.\n */\nexport type HandleRegistration<S extends string = DefaultSlots> = {\n slot: S;\n handleId: string;\n bindsTo: Binding;\n};\n\nexport type Selection = {\n handleId: string;\n bindsTo: Binding;\n};\n\n/**\n * UI selections keyed by slot name.\n * Generic over slot type S for custom slot support.\n */\nexport type UiSelections<S extends string = DefaultSlots> = Partial<Record<S, Selection>>;\n\n/**\n * Collection of registered handles.\n * Generic over slot type S for custom slot support.\n */\nexport type AffordanceRegistry<S extends string = DefaultSlots> = {\n handles: HandleRegistration<S>[];\n};\n\nexport function normalizeBinding(input: BindingInput): Binding {\n if (\"toolName\" in input) return { key: \"nextTool\", value: input.toolName };\n return { key: input.key, value: input.value };\n}\n\n/**\n * Creates a binding key for indexing.\n * Accepts any string for slot to support custom slots.\n */\nexport function makeBindingKey(slot: string, binding: Binding): string {\n return `${slot}::${binding.key}::${binding.value}`;\n}\n","import type { AffordanceRegistry, DefaultSlots, HandleRegistration } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\n/**\n * Index structure for efficient affordance lookup.\n * Generic over slot type S for custom slot support.\n */\nexport type RegistryIndex<S extends string = DefaultSlots> = {\n byBindingKey: Map<string, HandleRegistration<S>>;\n slots: Set<S>;\n /** Inferred decision field for each slot, derived from handle bindings. */\n slotKeyMap: Map<S, string>;\n};\n\n/**\n * Build an index from a registry for efficient lookup during selection.\n * Tracks which slots have registered handles and infers the decision field\n * for each slot from its handle bindings.\n *\n * @throws Error if handles for the same slot bind to different keys\n */\nexport function buildRegistryIndex<S extends string = DefaultSlots>(\n registry?: AffordanceRegistry<S>\n): RegistryIndex<S> {\n const byBindingKey = new Map<string, HandleRegistration<S>>();\n const slots = new Set<S>();\n const slotKeyMap = new Map<S, string>();\n\n if (!registry) return { byBindingKey, slots, slotKeyMap };\n\n for (const h of registry.handles) {\n slots.add(h.slot);\n\n // Infer and validate slot key\n const existingKey = slotKeyMap.get(h.slot);\n if (existingKey && existingKey !== h.bindsTo.key) {\n throw new Error(\n `[Taias] Slot \"${h.slot}\" has handles bound to different keys: \"${existingKey}\" and \"${h.bindsTo.key}\". ` +\n `All handles for a slot must use the same decision field.`\n );\n }\n slotKeyMap.set(h.slot, h.bindsTo.key);\n\n byBindingKey.set(makeBindingKey(h.slot, h.bindsTo), h);\n }\n\n return { byBindingKey, slots, slotKeyMap };\n}\n","import type { Decision } from \"../types\";\nimport type { DefaultSlots, UiSelections } from \"./types\";\nimport type { RegistryIndex } from \"./indexing\";\nimport { makeBindingKey } from \"./types\";\n\nexport type SelectOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Select UI affordances based on flow decision.\n * Uses the inferred decision field for each slot from the registry index.\n */\nexport function selectUiAffordances<S extends string = DefaultSlots>(\n decision: Decision,\n index: RegistryIndex<S>,\n opts: SelectOptions = {}\n): UiSelections<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const selections: UiSelections<S> = {};\n\n for (const slot of index.slots) {\n // Use inferred key from handle bindings\n const field = index.slotKeyMap.get(slot);\n if (!field) continue;\n\n const value = decision[field];\n if (!value) continue;\n\n const k = makeBindingKey(slot, { key: field, value });\n const handle = index.byBindingKey.get(k);\n\n if (!handle) {\n if (devMode) warn(`[Taias] No affordance for slot \"${slot}\" when ${field}=\"${value}\"`);\n continue;\n }\n\n (selections as Record<string, unknown>)[slot] = {\n handleId: handle.handleId,\n bindsTo: handle.bindsTo,\n };\n }\n\n return selections;\n}\n","import type { Affordances, Condition, FieldCondition, FlowStep, MatchCondition, StepDecision, Taias, TaiasContext, TaiasOptions, Decision } from \"./types\";\nimport type { DefaultSlots } from \"./uiAffordances/types\";\nimport { buildRegistryIndex } from \"./uiAffordances/indexing\";\nimport { selectUiAffordances } from \"./uiAffordances/select\";\n\n/**\n * Generate advice text for a given next tool.\n */\nfunction generateAdvice(nextTool: string): string {\n return `FOR THE BEST USER EXPERIENCE, TELL THE USER TO USE THE ${nextTool} TOOL NEXT!!!!!`;\n}\n\n// ---------------------------------------------------------------------------\n// Condition normalization and evaluation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize a FieldCondition to a canonical Condition object.\n * A bare string is sugar for { is: string }.\n */\nfunction normalizeFieldCondition(field: FieldCondition): Condition {\n return typeof field === \"string\" ? { is: field } : field;\n}\n\n/**\n * Evaluate a single Condition against a value.\n */\nfunction evaluateCondition(condition: Condition, value: string): boolean {\n if (\"is\" in condition) return value === condition.is;\n if (\"isNot\" in condition) return value !== condition.isNot;\n return false;\n}\n\n/**\n * Evaluate a full MatchCondition against a TaiasContext.\n * All conditions in the match must be satisfied.\n */\nfunction evaluateMatch(match: MatchCondition, ctx: TaiasContext): boolean {\n const toolCondition = normalizeFieldCondition(match.toolName);\n return evaluateCondition(toolCondition, ctx.toolName);\n}\n\n/**\n * Check whether a FieldCondition is indexable (i.e., uses the `is` operator).\n * Indexable conditions enable O(1) Map lookup at resolve time.\n */\nfunction isIndexable(field: FieldCondition): boolean {\n if (typeof field === \"string\") return true;\n return \"is\" in field;\n}\n\n/**\n * Extract the index key from an indexable FieldCondition.\n * Only call this when isIndexable() returns true.\n */\nfunction indexKey(field: FieldCondition): string {\n if (typeof field === \"string\") return field;\n if (\"is\" in field) return field.is;\n throw new Error(\"Cannot derive index key from non-indexable condition\");\n}\n\n// ---------------------------------------------------------------------------\n// Step access helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Get the match condition from a FlowStep.\n *\n * - Logic-based steps: match comes from statement.match (the statement is\n * the sole source of truth for its match conditions)\n * - Handler-based steps (backwards compatibility): match is stored directly on the step\n */\nfunction getMatch(step: FlowStep): MatchCondition {\n return step.kind === \"logic\" ? step.statement.match : step.match;\n}\n\n/**\n * Serialize a MatchCondition into a stable string for duplicate detection.\n * Normalizes sugar forms so that equivalent conditions produce the same key.\n */\nfunction serializeMatch(match: MatchCondition): string {\n const normalized = normalizeFieldCondition(match.toolName);\n return JSON.stringify({ toolName: normalized });\n}\n\n/**\n * createTaias constructs a decision engine.\n *\n * Taias resolves context into a generalized Decision object,\n * and then manifests that decision into concrete affordances:\n *\n * - LLM guidance (advice)\n * - UI affordance selections\n *\n * Flow logic is expressed as logic statements -- structured data that\n * Taias understands. (Handler functions remain as a backwards-compatible\n * escape hatch.)\n * \n * Flow logic determines *what should happen next*.\n * UI affordances determine *how that decision appears in the interface*.\n *\n * This file is the boundary where:\n *\n * Inputs → Decision → Manifestations\n *\n * are unified into a single resolve() call.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const taias = createTaias({ flow, affordances });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta\", { key: \"nextTool\", value: \"createUser\" });\n * r.contentArea(\"content\", { key: \"contentArea\", value: \"form\" });\n * r.headerStyle(\"header\", { key: \"headerStyle\", value: \"progress\" });\n * });\n * const taias = createTaias<MySlots>({ flow, affordances });\n * ```\n */\nexport function createTaias<S extends string = DefaultSlots>(\n options: TaiasOptions<S>\n): Taias<S> {\n const {\n flow,\n affordances,\n devMode = false,\n onMissingStep,\n onWarn,\n } = options;\n\n const warn = onWarn ?? ((msg: string) => console.warn(msg));\n\n // Dev mode: Check for duplicate match conditions.\n // Two steps with structurally identical normalized conditions are duplicates.\n if (devMode) {\n const seenKeys = new Set<string>();\n for (const step of flow.steps) {\n const key = serializeMatch(getMatch(step));\n if (seenKeys.has(key)) {\n const match = getMatch(step);\n const normalized = normalizeFieldCondition(match.toolName);\n const label = \"is\" in normalized ? normalized.is : `isNot:${normalized.isNot}`;\n throw new Error(\n `Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`\n );\n }\n seenKeys.add(key);\n }\n }\n\n // Build internal indexes for efficient resolution.\n //\n // Steps with indexable conditions (is / string sugar) go into an exact\n // Map for O(1) lookup. Steps with non-indexable conditions (isNot) go\n // into a separate list. When no broad steps exist, resolve uses the\n // fast path (Map only). When broad steps exist, resolve evaluates all\n // steps in definition order.\n //\n // This indexing is a performance optimization derived from the current\n // set of operators, not a permanent architectural choice. It will evolve\n // as operators and match condition fields expand.\n const exactIndex = new Map<string, FlowStep>();\n const broadSteps: FlowStep[] = [];\n\n for (const step of flow.steps) {\n const match = getMatch(step);\n if (isIndexable(match.toolName)) {\n exactIndex.set(indexKey(match.toolName), step);\n } else {\n broadSteps.push(step);\n }\n }\n\n const hasBroadSteps = broadSteps.length > 0;\n\n // Build affordance index once (if provided)\n const registryIndex = buildRegistryIndex<S>(affordances);\n\n return {\n async resolve(ctx: TaiasContext): Promise<Affordances<S> | null> {\n let step: FlowStep | undefined;\n\n if (!hasBroadSteps) {\n // Fast path: all steps use indexable conditions (is / string sugar).\n // O(1) Map lookup -- same performance as before operators were introduced.\n step = exactIndex.get(ctx.toolName);\n } else {\n // Full evaluation: some steps use non-indexable conditions (isNot).\n // Evaluate all steps in definition order; first match wins.\n for (const candidate of flow.steps) {\n if (evaluateMatch(getMatch(candidate), ctx)) {\n step = candidate;\n break;\n }\n }\n }\n\n if (!step) {\n onMissingStep?.(ctx);\n return null;\n }\n\n // Evaluate the step based on its kind:\n // - Logic statements: return the decision directly (no function call)\n // - Handler functions (backwards compatibility): call the handler and await the result\n let result: StepDecision | null;\n\n if (step.kind === \"logic\") {\n result = step.statement.decision;\n } else {\n result = await step.handler(ctx);\n }\n\n if (!result) return null;\n\n if (devMode && result.nextTool === \"\") {\n warn(`Taias: nextTool for tool '${ctx.toolName}' is empty.`);\n }\n\n // Build decision object from flow result (spread all fields)\n const decision: Decision = { ...result };\n\n // Compute UI selections (may be empty if no registry passed)\n const selections = selectUiAffordances<S>(decision, registryIndex, {\n devMode,\n onWarn: warn,\n });\n\n return {\n advice: generateAdvice(result.nextTool),\n decision,\n selections,\n };\n },\n };\n}\n","import type {\n AffordanceRegistry,\n BindingInput,\n DefaultSlots,\n HandleRegistration,\n} from \"./types\";\nimport { normalizeBinding } from \"./types\";\n\n/**\n * Mapped type that creates a registration method for each slot in S.\n * This enables fully typed custom slots via generics.\n */\nexport type AffordanceRegistrar<S extends string = DefaultSlots> = {\n [K in S]: (handleId: string, bindsTo: BindingInput) => void;\n};\n\n/**\n * Define UI affordances for a widget using a builder pattern.\n *\n * @example Default slots (backwards compatible)\n * ```ts\n * const affordances = defineAffordances((r) => {\n * r.primaryCta(\"cta.recommend\", { toolName: \"get_recommendations\" });\n * r.widgetVariant(\"variant.discovery\", { toolName: \"get_recommendations\" });\n * });\n * ```\n *\n * @example Custom slots (fully type-safe)\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\" | \"headerStyle\";\n * const affordances = defineAffordances<MySlots>((r) => {\n * r.primaryCta(\"cta.create\", { toolName: \"createUser\" });\n * r.contentArea(\"content.form\", { key: \"contentArea\", value: \"email-form\" });\n * r.headerStyle(\"header.progress\", { key: \"headerStyle\", value: \"step-1\" });\n * });\n * ```\n */\nexport function defineAffordances<S extends string = DefaultSlots>(\n builder: (r: AffordanceRegistrar<S>) => void\n): AffordanceRegistry<S> {\n const handles: HandleRegistration<S>[] = [];\n\n // Proxy creates methods on-the-fly for any slot name.\n // TypeScript ensures only valid slot names (from S) are called.\n const registrar = new Proxy({} as AffordanceRegistrar<S>, {\n get(_, slot: string) {\n return (handleId: string, bindsTo: BindingInput) => {\n handles.push({\n slot: slot as S,\n handleId,\n bindsTo: normalizeBinding(bindsTo),\n });\n };\n },\n });\n\n builder(registrar);\n return { handles };\n}\n","import type { AffordanceRegistry, DefaultSlots } from \"./types\";\nimport { makeBindingKey } from \"./types\";\n\nexport type MergeAffordancesOptions = {\n devMode?: boolean;\n onWarn?: (msg: string) => void;\n};\n\n/**\n * Merge multiple affordance registries into one.\n * Generic over slot type S for custom slot support.\n *\n * @example Default slots\n * ```ts\n * const merged = mergeAffordances([widgetA, widgetB]);\n * ```\n *\n * @example Custom slots\n * ```ts\n * type MySlots = \"primaryCta\" | \"contentArea\";\n * const merged = mergeAffordances<MySlots>([widgetA, widgetB]);\n * ```\n */\nexport function mergeAffordances<S extends string = DefaultSlots>(\n registries: AffordanceRegistry<S>[],\n opts: MergeAffordancesOptions = {}\n): AffordanceRegistry<S> {\n const devMode = !!opts.devMode;\n const warn = opts.onWarn ?? (() => {});\n\n const merged: AffordanceRegistry<S> = { handles: registries.flatMap((r) => r.handles) };\n\n if (!devMode) return merged;\n\n // Check for duplicate handleIds\n const seenHandleIds = new Set<string>();\n for (const h of merged.handles) {\n if (seenHandleIds.has(h.handleId)) {\n throw new Error(`[Taias] Duplicate handleId \"${h.handleId}\"`);\n }\n seenHandleIds.add(h.handleId);\n }\n\n // Check for ambiguous bindings (same slot + key + value)\n const seenTriples = new Set<string>();\n for (const h of merged.handles) {\n const k = makeBindingKey(h.slot, h.bindsTo);\n if (seenTriples.has(k)) {\n throw new Error(\n `[Taias] Ambiguous affordance: slot \"${h.slot}\" has multiple handles bound to (${h.bindsTo.key}=\"${h.bindsTo.value}\")`\n );\n }\n seenTriples.add(k);\n }\n\n warn(`[Taias] Loaded ${merged.handles.length} UI affordance handles`);\n return merged;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taias",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Taias - Give your MCP server an opinion, guide your users to achieve outcomes",
6
6
  "license": "Apache-2.0",