@falai/agent 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +56 -1
  2. package/dist/cjs/core/Events.d.ts +2 -2
  3. package/dist/cjs/core/Events.d.ts.map +1 -1
  4. package/dist/cjs/core/Events.js +4 -4
  5. package/dist/cjs/core/Events.js.map +1 -1
  6. package/dist/cjs/core/Observation.d.ts.map +1 -1
  7. package/dist/cjs/core/Observation.js +3 -2
  8. package/dist/cjs/core/Observation.js.map +1 -1
  9. package/dist/cjs/core/Route.d.ts.map +1 -1
  10. package/dist/cjs/core/Route.js +3 -4
  11. package/dist/cjs/core/Route.js.map +1 -1
  12. package/dist/cjs/core/State.d.ts +1 -1
  13. package/dist/cjs/core/State.d.ts.map +1 -1
  14. package/dist/cjs/core/State.js +4 -3
  15. package/dist/cjs/core/State.js.map +1 -1
  16. package/dist/cjs/core/Tool.d.ts +1 -0
  17. package/dist/cjs/core/Tool.d.ts.map +1 -1
  18. package/dist/cjs/core/Tool.js +3 -2
  19. package/dist/cjs/core/Tool.js.map +1 -1
  20. package/dist/cjs/index.d.ts +1 -0
  21. package/dist/cjs/index.d.ts.map +1 -1
  22. package/dist/cjs/index.js +7 -1
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/types/agent.d.ts +2 -2
  25. package/dist/cjs/types/agent.d.ts.map +1 -1
  26. package/dist/cjs/types/observation.d.ts +2 -0
  27. package/dist/cjs/types/observation.d.ts.map +1 -1
  28. package/dist/cjs/types/route.d.ts +2 -0
  29. package/dist/cjs/types/route.d.ts.map +1 -1
  30. package/dist/cjs/utils/id.d.ts +25 -0
  31. package/dist/cjs/utils/id.d.ts.map +1 -0
  32. package/dist/cjs/utils/id.js +71 -0
  33. package/dist/cjs/utils/id.js.map +1 -0
  34. package/dist/core/Events.d.ts +2 -2
  35. package/dist/core/Events.d.ts.map +1 -1
  36. package/dist/core/Events.js +4 -4
  37. package/dist/core/Events.js.map +1 -1
  38. package/dist/core/Observation.d.ts.map +1 -1
  39. package/dist/core/Observation.js +3 -2
  40. package/dist/core/Observation.js.map +1 -1
  41. package/dist/core/Route.d.ts.map +1 -1
  42. package/dist/core/Route.js +3 -4
  43. package/dist/core/Route.js.map +1 -1
  44. package/dist/core/State.d.ts +1 -1
  45. package/dist/core/State.d.ts.map +1 -1
  46. package/dist/core/State.js +4 -3
  47. package/dist/core/State.js.map +1 -1
  48. package/dist/core/Tool.d.ts +1 -0
  49. package/dist/core/Tool.d.ts.map +1 -1
  50. package/dist/core/Tool.js +3 -2
  51. package/dist/core/Tool.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/types/agent.d.ts +2 -2
  57. package/dist/types/agent.d.ts.map +1 -1
  58. package/dist/types/observation.d.ts +2 -0
  59. package/dist/types/observation.d.ts.map +1 -1
  60. package/dist/types/route.d.ts +2 -0
  61. package/dist/types/route.d.ts.map +1 -1
  62. package/dist/utils/id.d.ts +25 -0
  63. package/dist/utils/id.d.ts.map +1 -0
  64. package/dist/utils/id.js +65 -0
  65. package/dist/utils/id.js.map +1 -0
  66. package/docs/API_REFERENCE.md +122 -6
  67. package/docs/CONSTRUCTOR_OPTIONS.md +43 -36
  68. package/docs/GETTING_STARTED.md +2 -0
  69. package/docs/PROVIDERS.md +3 -0
  70. package/examples/declarative-agent.ts +31 -7
  71. package/package.json +1 -1
  72. package/src/core/Events.ts +6 -4
  73. package/src/core/Observation.ts +3 -3
  74. package/src/core/Route.ts +3 -5
  75. package/src/core/State.ts +5 -4
  76. package/src/core/Tool.ts +4 -3
  77. package/src/index.ts +8 -0
  78. package/src/types/agent.ts +4 -2
  79. package/src/types/observation.ts +2 -0
  80. package/src/types/route.ts +2 -0
  81. package/src/utils/id.ts +74 -0
@@ -18,16 +18,16 @@ interface AgentOptions<TContext = unknown> {
18
18
  // Required
19
19
  name: string;
20
20
  ai: AiProvider;
21
-
21
+
22
22
  // Optional metadata
23
23
  description?: string;
24
24
  goal?: string;
25
25
  context?: TContext;
26
-
26
+
27
27
  // Configuration
28
28
  maxEngineIterations?: number;
29
29
  compositionMode?: CompositionMode;
30
-
30
+
31
31
  // Declarative initialization (NEW!)
32
32
  terms?: Term[];
33
33
  guidelines?: Guideline[];
@@ -44,43 +44,47 @@ const agent = new Agent({
44
44
  name: "SupportBot",
45
45
  description: "Helpful customer support",
46
46
  goal: "Resolve issues efficiently",
47
- ai: new GeminiProvider({ apiKey: "..." }),
47
+ ai: new GeminiProvider({ apiKey: "...", model: "..." }),
48
48
  context: { userId: "123" },
49
-
49
+
50
50
  terms: [
51
- { name: "SLA", description: "Service Level Agreement", synonyms: ["response time"] }
51
+ {
52
+ name: "SLA",
53
+ description: "Service Level Agreement",
54
+ synonyms: ["response time"],
55
+ },
52
56
  ],
53
-
57
+
54
58
  guidelines: [
55
59
  {
56
60
  condition: "User is frustrated",
57
61
  action: "Show empathy and offer escalation",
58
62
  tags: ["support"],
59
- enabled: true
60
- }
63
+ enabled: true,
64
+ },
61
65
  ],
62
-
66
+
63
67
  capabilities: [
64
- { title: "Ticket Management", description: "Create and track tickets" }
68
+ { title: "Ticket Management", description: "Create and track tickets" },
65
69
  ],
66
-
70
+
67
71
  routes: [
68
72
  {
69
73
  title: "Create Ticket",
70
74
  description: "Help user create a support ticket",
71
75
  conditions: ["User wants to report an issue"],
72
76
  guidelines: [
73
- { condition: "Issue is urgent", action: "Prioritize immediately" }
74
- ]
75
- }
77
+ { condition: "Issue is urgent", action: "Prioritize immediately" },
78
+ ],
79
+ },
76
80
  ],
77
-
81
+
78
82
  observations: [
79
83
  {
80
84
  description: "User mentions problem but unclear what kind",
81
- routeRefs: ["Create Ticket", "Check Ticket Status"] // By title!
82
- }
83
- ]
85
+ routeRefs: ["Create Ticket", "Check Ticket Status"], // By title!
86
+ },
87
+ ],
84
88
  });
85
89
  ```
86
90
 
@@ -92,7 +96,7 @@ const agent = new Agent({
92
96
  interface RouteOptions {
93
97
  // Required
94
98
  title: string;
95
-
99
+
96
100
  // Optional
97
101
  description?: string;
98
102
  conditions?: string[];
@@ -115,16 +119,16 @@ const agent = new Agent({
115
119
  {
116
120
  condition: "User skips a step",
117
121
  action: "Gently remind them it's important",
118
- tags: ["onboarding"]
122
+ tags: ["onboarding"],
119
123
  },
120
124
  {
121
125
  condition: "User seems confused",
122
126
  action: "Offer a quick tutorial video",
123
- tags: ["help"]
124
- }
125
- ]
126
- }
127
- ]
127
+ tags: ["help"],
128
+ },
129
+ ],
130
+ },
131
+ ],
128
132
  });
129
133
  ```
130
134
 
@@ -187,18 +191,21 @@ obs.disambiguate([route1, route2]);
187
191
  ## 🎨 Best Practices
188
192
 
189
193
  ### Use Declarative When:
194
+
190
195
  - ✅ Configuration is **static** and known upfront
191
196
  - ✅ Loading config from **JSON/YAML files**
192
197
  - ✅ Building **reusable agent templates**
193
198
  - ✅ You want **clean, readable initialization**
194
199
 
195
200
  ### Use Fluent When:
201
+
196
202
  - ✅ Logic is **dynamic** or **conditional**
197
203
  - ✅ Building routes with **complex state machines**
198
204
  - ✅ Adding features **based on runtime conditions**
199
205
  - ✅ You prefer **step-by-step construction**
200
206
 
201
207
  ### Mix Both!
208
+
202
209
  ```typescript
203
210
  // Start with static config
204
211
  const agent = new Agent({
@@ -212,7 +219,7 @@ const agent = new Agent({
212
219
  if (user.isPremium) {
213
220
  agent.createGuideline({
214
221
  condition: "User asks for priority support",
215
- action: "Escalate immediately to premium team"
222
+ action: "Escalate immediately to premium team",
216
223
  });
217
224
  }
218
225
  ```
@@ -221,15 +228,15 @@ if (user.isPremium) {
221
228
 
222
229
  ## 📊 Complete Comparison
223
230
 
224
- | Feature | Declarative (Constructor) | Fluent (Methods) |
225
- |---------|--------------------------|------------------|
226
- | **Terms** | `terms: Term[]` | `agent.createTerm(...)` |
227
- | **Guidelines** | `guidelines: Guideline[]` | `agent.createGuideline(...)` |
228
- | **Capabilities** | `capabilities: Capability[]` | `agent.createCapability(...)` |
229
- | **Routes** | `routes: RouteOptions[]` | `agent.createRoute(...)` |
230
- | **Route Guidelines** | `route.guidelines: Guideline[]` | `route.createGuideline(...)` |
231
- | **Observations** | `observations: ObservationOptions[]` | `agent.createObservation(...)` |
232
- | **Disambiguation** | `routeRefs: string[]` | `obs.disambiguate([...])` |
231
+ | Feature | Declarative (Constructor) | Fluent (Methods) |
232
+ | -------------------- | ------------------------------------ | ------------------------------ |
233
+ | **Terms** | `terms: Term[]` | `agent.createTerm(...)` |
234
+ | **Guidelines** | `guidelines: Guideline[]` | `agent.createGuideline(...)` |
235
+ | **Capabilities** | `capabilities: Capability[]` | `agent.createCapability(...)` |
236
+ | **Routes** | `routes: RouteOptions[]` | `agent.createRoute(...)` |
237
+ | **Route Guidelines** | `route.guidelines: Guideline[]` | `route.createGuideline(...)` |
238
+ | **Observations** | `observations: ObservationOptions[]` | `agent.createObservation(...)` |
239
+ | **Disambiguation** | `routeRefs: string[]` | `obs.disambiguate([...])` |
233
240
 
234
241
  ---
235
242
 
@@ -58,6 +58,7 @@ interface MyContext {
58
58
  // Create AI provider
59
59
  const ai = new GeminiProvider({
60
60
  apiKey: process.env.GEMINI_API_KEY!,
61
+ model: "models/gemini-2.5-pro",
61
62
  });
62
63
 
63
64
  // Create your agent
@@ -307,6 +308,7 @@ Increase timeout in provider config:
307
308
  ```typescript
308
309
  new GeminiProvider({
309
310
  apiKey: "...",
311
+ model: "models/gemini-2.5-flash",
310
312
  retryConfig: {
311
313
  timeout: 120000, // 2 minutes
312
314
  retries: 5,
package/docs/PROVIDERS.md CHANGED
@@ -281,6 +281,7 @@ const geminiAgent = new Agent({
281
281
  name: "Gemini Assistant",
282
282
  ai: new GeminiProvider({
283
283
  apiKey: process.env.GEMINI_API_KEY!,
284
+ model: "models/gemini-2.5-flash",
284
285
  }),
285
286
  });
286
287
 
@@ -315,10 +316,12 @@ config();
315
316
 
316
317
  const geminiProvider = new GeminiProvider({
317
318
  apiKey: process.env.GEMINI_API_KEY!,
319
+ model: "models/gemini-2.5-flash",
318
320
  });
319
321
 
320
322
  const openaiProvider = new OpenAIProvider({
321
323
  apiKey: process.env.OPENAI_API_KEY!,
324
+ model: "gpt-5",
322
325
  });
323
326
  ```
324
327
 
@@ -6,8 +6,9 @@
6
6
  * - Terms (glossary)
7
7
  * - Guidelines (behavior rules)
8
8
  * - Capabilities
9
- * - Routes with nested guidelines
10
- * - Observations with route references
9
+ * - Routes with nested guidelines and custom IDs
10
+ * - Observations with route references and custom IDs
11
+ * - Custom timestamps for events
11
12
  */
12
13
 
13
14
  import {
@@ -29,13 +30,16 @@ interface HealthcareContext {
29
30
  patientName: string;
30
31
  }
31
32
 
32
- // Define tools
33
+ // Define tools with custom IDs (optional - IDs are deterministic by default)
33
34
  const getInsuranceProviders = defineTool<HealthcareContext, [], string[]>(
34
35
  "get_insurance_providers",
35
36
  async () => {
36
37
  return { data: ["MegaCare Insurance", "HealthFirst", "WellnessPlus"] };
37
38
  },
38
- { description: "Retrieves list of accepted insurance providers" }
39
+ {
40
+ id: "healthcare_insurance_providers", // Custom ID for persistence
41
+ description: "Retrieves list of accepted insurance providers",
42
+ }
39
43
  );
40
44
 
41
45
  const getAvailableSlots = defineTool<
@@ -53,7 +57,10 @@ const getAvailableSlots = defineTool<
53
57
  ],
54
58
  };
55
59
  },
56
- { description: "Gets available appointment slots" }
60
+ {
61
+ id: "healthcare_available_slots", // Custom ID
62
+ description: "Gets available appointment slots",
63
+ }
57
64
  );
58
65
 
59
66
  const getLabResults = defineTool<
@@ -70,7 +77,10 @@ const getLabResults = defineTool<
70
77
  },
71
78
  };
72
79
  },
73
- { description: "Retrieves patient lab results" }
80
+ {
81
+ id: "healthcare_lab_results", // Custom ID
82
+ description: "Retrieves patient lab results",
83
+ }
74
84
  );
75
85
 
76
86
  // Declarative configuration
@@ -126,6 +136,7 @@ const capabilities: Capability[] = [
126
136
 
127
137
  const routes: RouteOptions[] = [
128
138
  {
139
+ id: "route_schedule_appointment", // Custom ID ensures consistency across restarts
129
140
  title: "Schedule Appointment",
130
141
  description: "Helps the patient schedule an appointment",
131
142
  conditions: ["The patient wants to schedule an appointment"],
@@ -139,6 +150,7 @@ const routes: RouteOptions[] = [
139
150
  ],
140
151
  },
141
152
  {
153
+ id: "route_check_lab_results", // Custom ID
142
154
  title: "Check Lab Results",
143
155
  description: "Retrieves and explains patient lab results",
144
156
  conditions: ["The patient wants to see their lab results"],
@@ -155,6 +167,7 @@ const routes: RouteOptions[] = [
155
167
 
156
168
  const observations: ObservationOptions[] = [
157
169
  {
170
+ id: "obs_visit_followup", // Custom ID for tracking
158
171
  description:
159
172
  "The patient asks to follow up on their visit, but it's not clear in which way",
160
173
  routeRefs: ["Schedule Appointment", "Check Lab Results"], // Reference by title
@@ -172,6 +185,7 @@ const agent = new Agent<HealthcareContext>({
172
185
  },
173
186
  ai: new GeminiProvider({
174
187
  apiKey: process.env.GEMINI_API_KEY || "demo-key",
188
+ model: "models/gemini-2.5-flash",
175
189
  }),
176
190
  // Declarative initialization
177
191
  terms,
@@ -196,19 +210,29 @@ agent
196
210
 
197
211
  // Example usage
198
212
  async function main() {
213
+ // Create events with custom timestamps (useful for historical data)
199
214
  const history = [
200
215
  createMessageEvent(
201
216
  EventSource.CUSTOMER,
202
217
  "Alice",
203
- "Hi, I need to follow up on my recent visit"
218
+ "Hi, I need to follow up on my recent visit",
219
+ "2025-10-13T14:30:00Z" // Optional custom timestamp
204
220
  ),
205
221
  ];
206
222
 
207
223
  const response = await agent.respond({ history });
208
224
  console.log("Agent:", response.message);
225
+ console.log("Route chosen:", response.route?.title);
226
+ console.log("Route ID:", response.route?.id); // Custom ID is preserved
209
227
 
210
228
  // The agent will use the observation to disambiguate
211
229
  // and ask which type of follow-up the patient needs
230
+
231
+ // Note: Custom IDs ensure consistency across server restarts
232
+ // This is crucial for:
233
+ // - Storing conversation state in databases
234
+ // - Tracking metrics and analytics
235
+ // - Referencing routes in external systems
212
236
  }
213
237
 
214
238
  // Uncomment to run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falai/agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Standalone, strongly-typed AI Agent framework with route DSL and AI provider strategy",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -70,7 +70,8 @@ export function adaptEvent(e: Event | EmittedEvent): string {
70
70
  export function createMessageEvent(
71
71
  source: EventSource,
72
72
  participantName: string,
73
- message: string
73
+ message: string,
74
+ timestamp?: string
74
75
  ): Event<MessageEventData> {
75
76
  return {
76
77
  kind: EventKind.MESSAGE,
@@ -79,7 +80,7 @@ export function createMessageEvent(
79
80
  participant: { display_name: participantName },
80
81
  message,
81
82
  },
82
- timestamp: new Date().toISOString(),
83
+ timestamp: timestamp || new Date().toISOString(),
83
84
  };
84
85
  }
85
86
 
@@ -88,7 +89,8 @@ export function createMessageEvent(
88
89
  */
89
90
  export function createToolEvent(
90
91
  source: EventSource,
91
- toolCalls: ToolCall[]
92
+ toolCalls: ToolCall[],
93
+ timestamp?: string
92
94
  ): Event<ToolEventData> {
93
95
  return {
94
96
  kind: EventKind.TOOL,
@@ -96,6 +98,6 @@ export function createToolEvent(
96
98
  data: {
97
99
  tool_calls: toolCalls,
98
100
  },
99
- timestamp: new Date().toISOString(),
101
+ timestamp: timestamp || new Date().toISOString(),
100
102
  };
101
103
  }
@@ -8,8 +8,7 @@ import type {
8
8
  } from "../types/observation";
9
9
  import type { RouteRef } from "../types/route";
10
10
  import type { Route } from "./Route";
11
-
12
- let observationIdCounter = 0;
11
+ import { generateObservationId } from "../utils/id";
13
12
 
14
13
  /**
15
14
  * An observation that can trigger disambiguation between routes
@@ -20,7 +19,8 @@ export class Observation implements IObservation {
20
19
  public routes: RouteRef[] = [];
21
20
 
22
21
  constructor(options: ObservationOptions) {
23
- this.id = `observation_${++observationIdCounter}`;
22
+ // Use provided ID or generate a deterministic one from the description
23
+ this.id = options.id || generateObservationId(options.description);
24
24
  this.description = options.description;
25
25
  }
26
26
 
package/src/core/Route.ts CHANGED
@@ -6,8 +6,7 @@ import type { RouteOptions, RouteRef } from "../types/route";
6
6
  import type { Guideline } from "../types/agent";
7
7
 
8
8
  import { State } from "./State";
9
-
10
- let routeIdCounter = 0;
9
+ import { generateRouteId } from "../utils/id";
11
10
 
12
11
  /**
13
12
  * Represents a conversational route/journey
@@ -21,9 +20,8 @@ export class Route<TContext = unknown> {
21
20
  private guidelines: Guideline[] = [];
22
21
 
23
22
  constructor(options: RouteOptions) {
24
- this.id = `route_${++routeIdCounter}_${options.title
25
- .toLowerCase()
26
- .replace(/\s+/g, "_")}`;
23
+ // Use provided ID or generate a deterministic one from the title
24
+ this.id = options.id || generateRouteId(options.title);
27
25
  this.title = options.title;
28
26
  this.description = options.description;
29
27
  this.conditions = options.conditions || [];
package/src/core/State.ts CHANGED
@@ -11,8 +11,7 @@ import type { Guideline } from "../types/agent";
11
11
 
12
12
  import { END_ROUTE } from "../constants";
13
13
  import { Transition } from "./Transition";
14
-
15
- let stateIdCounter = 0;
14
+ import { generateStateId } from "../utils/id";
16
15
 
17
16
  /**
18
17
  * Represents a state within a route
@@ -24,9 +23,11 @@ export class State<TContext = unknown> {
24
23
 
25
24
  constructor(
26
25
  public readonly routeId: string,
27
- public readonly description?: string
26
+ public readonly description?: string,
27
+ customId?: string
28
28
  ) {
29
- this.id = `state_${++stateIdCounter}`;
29
+ // Use provided ID or generate a deterministic one
30
+ this.id = customId || generateStateId(routeId, description);
30
31
  }
31
32
 
32
33
  /**
package/src/core/Tool.ts CHANGED
@@ -8,8 +8,7 @@ import type {
8
8
  ToolRef,
9
9
  ToolResult,
10
10
  } from "../types/tool";
11
-
12
- let toolIdCounter = 0;
11
+ import { generateToolId } from "../utils/id";
13
12
 
14
13
  /**
15
14
  * Define a new tool with type-safe context and arguments
@@ -33,11 +32,13 @@ export function defineTool<TContext, TArgs extends unknown[], TResult>(
33
32
  name: string,
34
33
  handler: ToolHandler<TContext, TArgs, TResult>,
35
34
  options?: {
35
+ id?: string;
36
36
  description?: string;
37
37
  parameters?: unknown;
38
38
  }
39
39
  ): ToolRef<TContext, TArgs, TResult> {
40
- const id = `tool_${++toolIdCounter}_${name}`;
40
+ // Use provided ID or generate a deterministic one from the name
41
+ const id = options?.id || generateToolId(name);
41
42
 
42
43
  return {
43
44
  id,
package/src/index.ts CHANGED
@@ -28,6 +28,14 @@ export type { OpenRouterProviderOptions } from "./providers/OpenRouterProvider";
28
28
  // Constants
29
29
  export { END_ROUTE } from "./constants";
30
30
 
31
+ // Utils
32
+ export {
33
+ generateRouteId,
34
+ generateStateId,
35
+ generateObservationId,
36
+ generateToolId,
37
+ } from "./utils/id";
38
+
31
39
  // Types
32
40
  export type {
33
41
  AgentOptions,
@@ -82,7 +82,8 @@ export interface Guideline {
82
82
  /** Tags for organizing and filtering guidelines */
83
83
  tags?: string[];
84
84
  /** Tools available when following this guideline */
85
- tools?: ToolRef<unknown, unknown[], unknown>[];
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ tools?: ToolRef<any, any[], any>[];
86
87
  /** Additional metadata */
87
88
  metadata?: Record<string, unknown>;
88
89
  }
@@ -98,7 +99,8 @@ export interface Capability {
98
99
  /** Description of what the capability does */
99
100
  description: string;
100
101
  /** Tools used by this capability */
101
- tools?: ToolRef<unknown, unknown[], unknown>[];
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ tools?: ToolRef<any, any[], any>[];
102
104
  }
103
105
 
104
106
  /**
@@ -20,6 +20,8 @@ export interface Observation {
20
20
  * Options for creating an observation
21
21
  */
22
22
  export interface ObservationOptions {
23
+ /** Custom ID for the observation (optional - will generate deterministic ID from description if not provided) */
24
+ id?: string;
23
25
  /** The observation description */
24
26
  description: string;
25
27
  /** Route IDs or titles to disambiguate between (can be set later with disambiguate()) */
@@ -31,6 +31,8 @@ import type { Guideline } from "./agent";
31
31
  * Options for creating a route
32
32
  */
33
33
  export interface RouteOptions {
34
+ /** Custom ID for the route (optional - will generate deterministic ID from title if not provided) */
35
+ id?: string;
34
36
  /** Title of the route */
35
37
  title: string;
36
38
  /** Description of what this route accomplishes */
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ID generation utilities
3
+ * Provides deterministic ID generation to ensure consistency across server restarts
4
+ */
5
+
6
+ /**
7
+ * Generate a deterministic ID from a string by creating a simple hash
8
+ * This ensures the same input always produces the same ID
9
+ */
10
+ function simpleHash(str: string): string {
11
+ let hash = 0;
12
+ for (let i = 0; i < str.length; i++) {
13
+ const char = str.charCodeAt(i);
14
+ hash = (hash << 5) - hash + char;
15
+ hash = hash & hash; // Convert to 32-bit integer
16
+ }
17
+ return Math.abs(hash).toString(36);
18
+ }
19
+
20
+ /**
21
+ * Sanitize a string for use in an ID
22
+ */
23
+ function sanitize(str: string): string {
24
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "_");
25
+ }
26
+
27
+ /**
28
+ * Generate a deterministic route ID
29
+ * Format: route_{sanitized_title}_{hash}
30
+ */
31
+ export function generateRouteId(title: string): string {
32
+ const sanitized = sanitize(title);
33
+ const hash = simpleHash(title);
34
+ return `route_${sanitized}_${hash}`;
35
+ }
36
+
37
+ /**
38
+ * Generate a deterministic state ID
39
+ * Format: state_{sanitized_description}_{hash} or state_{routeId}_{index}
40
+ */
41
+ export function generateStateId(
42
+ routeId: string,
43
+ description?: string,
44
+ index?: number
45
+ ): string {
46
+ if (description) {
47
+ const sanitized = sanitize(description);
48
+ const hash = simpleHash(`${routeId}_${description}`);
49
+ return `state_${sanitized}_${hash}`;
50
+ }
51
+ // Fallback for states without descriptions
52
+ const suffix = index !== undefined ? index : simpleHash(routeId);
53
+ return `state_${routeId}_${suffix}`;
54
+ }
55
+
56
+ /**
57
+ * Generate a deterministic observation ID
58
+ * Format: observation_{sanitized_description}_{hash}
59
+ */
60
+ export function generateObservationId(description: string): string {
61
+ const sanitized = sanitize(description.substring(0, 50)); // Limit length
62
+ const hash = simpleHash(description);
63
+ return `observation_${sanitized}_${hash}`;
64
+ }
65
+
66
+ /**
67
+ * Generate a deterministic tool ID
68
+ * Format: tool_{sanitized_name}_{hash}
69
+ */
70
+ export function generateToolId(name: string): string {
71
+ const sanitized = sanitize(name);
72
+ const hash = simpleHash(name);
73
+ return `tool_${sanitized}_${hash}`;
74
+ }