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 +11 -13
- package/dist/index.cjs +78 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -9
- package/dist/index.d.ts +89 -9
- package/dist/index.js +78 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,14 +25,14 @@ npm install taias
|
|
|
25
25
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
28
|
-
**1. Define a flow** —
|
|
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",
|
|
35
|
-
flow.step("configure_app",
|
|
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",
|
|
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
|
|
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`
|
|
142
|
-
- Returns `null` if no step matches
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
35
|
-
|
|
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
|
|
152
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
114
153
|
for (const step of flow.steps) {
|
|
115
|
-
|
|
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
|
|
160
|
+
`Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`
|
|
118
161
|
);
|
|
119
162
|
}
|
|
120
|
-
|
|
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
|
|
176
|
+
const hasBroadSteps = broadSteps.length > 0;
|
|
124
177
|
const registryIndex = buildRegistryIndex(affordances);
|
|
125
178
|
return {
|
|
126
179
|
async resolve(ctx) {
|
|
127
|
-
|
|
128
|
-
if (!
|
|
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
|
-
|
|
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.`);
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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",
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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",
|
|
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
|
|
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(
|
|
6
|
-
|
|
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
|
|
123
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
85
124
|
for (const step of flow.steps) {
|
|
86
|
-
|
|
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
|
|
131
|
+
`Taias: Duplicate match condition '${label}' in flow '${flow.id}'. Each step must have a unique match condition.`
|
|
89
132
|
);
|
|
90
133
|
}
|
|
91
|
-
|
|
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
|
|
147
|
+
const hasBroadSteps = broadSteps.length > 0;
|
|
95
148
|
const registryIndex = buildRegistryIndex(affordances);
|
|
96
149
|
return {
|
|
97
150
|
async resolve(ctx) {
|
|
98
|
-
|
|
99
|
-
if (!
|
|
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
|
-
|
|
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":[]}
|