@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,666 @@
1
+ /**
2
+ * Edge Side Inference (ESI) Processor
3
+ *
4
+ * Like Varnish ESI but for AI - brings inference to the edge at render time.
5
+ * Components can embed AI directly in templates for dynamic, personalized content.
6
+ */
7
+
8
+ import type {
9
+ ESIConfig,
10
+ ESIContent,
11
+ ESIDirective,
12
+ ESIModel,
13
+ ESIParams,
14
+ ESIProcessor,
15
+ ESIResult,
16
+ UserContext,
17
+ UserTier,
18
+ DEFAULT_ESI_CONFIG,
19
+ } from './types';
20
+
21
+ // ============================================================================
22
+ // Cache for ESI results
23
+ // ============================================================================
24
+
25
+ interface CacheEntry {
26
+ result: ESIResult;
27
+ expiresAt: number;
28
+ }
29
+
30
+ const esiCache = new Map<string, CacheEntry>();
31
+
32
+ function getCacheKey(directive: ESIDirective, context: UserContext): string {
33
+ // Include context signals in cache key if context-aware
34
+ const contextParts = directive.contextAware
35
+ ? [context.tier, context.emotionState?.primary, context.localHour].join(':')
36
+ : '';
37
+
38
+ return (
39
+ directive.cacheKey ||
40
+ `esi:${directive.params.model}:${directive.content.value}:${contextParts}`
41
+ );
42
+ }
43
+
44
+ function getCached(key: string): ESIResult | null {
45
+ const entry = esiCache.get(key);
46
+ if (!entry) return null;
47
+ if (Date.now() > entry.expiresAt) {
48
+ esiCache.delete(key);
49
+ return null;
50
+ }
51
+ return { ...entry.result, cached: true };
52
+ }
53
+
54
+ function setCache(key: string, result: ESIResult, ttl: number): void {
55
+ if (ttl <= 0) return;
56
+ esiCache.set(key, {
57
+ result,
58
+ expiresAt: Date.now() + ttl * 1000,
59
+ });
60
+ }
61
+
62
+ // ============================================================================
63
+ // Context Interpolation
64
+ // ============================================================================
65
+
66
+ function interpolatePrompt(
67
+ content: ESIContent,
68
+ context: UserContext,
69
+ signals: string[] = [],
70
+ ): string {
71
+ let prompt = content.value;
72
+
73
+ // Replace template variables
74
+ if (content.type === 'template' && content.variables) {
75
+ for (const [key, value] of Object.entries(content.variables)) {
76
+ prompt = prompt.replace(
77
+ new RegExp(`\\{\\{${key}\\}\\}`, 'g'),
78
+ String(value),
79
+ );
80
+ }
81
+ }
82
+
83
+ // Inject context if signals specified
84
+ if (signals.length > 0) {
85
+ const contextParts: string[] = [];
86
+
87
+ if (signals.includes('emotion') && context.emotionState) {
88
+ contextParts.push(
89
+ `User emotion: ${context.emotionState.primary} ` +
90
+ `(valence: ${context.emotionState.valence.toFixed(2)}, ` +
91
+ `arousal: ${context.emotionState.arousal.toFixed(2)})`,
92
+ );
93
+ }
94
+
95
+ if (
96
+ signals.includes('preferences') &&
97
+ Object.keys(context.preferences).length > 0
98
+ ) {
99
+ contextParts.push(
100
+ `User preferences: ${JSON.stringify(context.preferences)}`,
101
+ );
102
+ }
103
+
104
+ if (signals.includes('history') && context.recentPages.length > 0) {
105
+ contextParts.push(
106
+ `Recent pages: ${context.recentPages.slice(-5).join(', ')}`,
107
+ );
108
+ }
109
+
110
+ if (signals.includes('time')) {
111
+ contextParts.push(
112
+ `Local time: ${context.localHour}:00, Timezone: ${context.timezone}`,
113
+ );
114
+ }
115
+
116
+ if (signals.includes('device')) {
117
+ contextParts.push(
118
+ `Device: ${context.viewport.width}x${context.viewport.height}, ` +
119
+ `Connection: ${context.connection}`,
120
+ );
121
+ }
122
+
123
+ if (contextParts.length > 0) {
124
+ prompt = `[Context]\n${contextParts.join('\n')}\n\n[Task]\n${prompt}`;
125
+ }
126
+ }
127
+
128
+ return prompt;
129
+ }
130
+
131
+ // ============================================================================
132
+ // Tier Enforcement
133
+ // ============================================================================
134
+
135
+ function checkTierAccess(
136
+ directive: ESIDirective,
137
+ context: UserContext,
138
+ config: ESIConfig,
139
+ ): { allowed: boolean; reason?: string } {
140
+ const tierLimits = config.tierLimits?.[context.tier];
141
+ if (!tierLimits) {
142
+ return { allowed: true };
143
+ }
144
+
145
+ // Check if model is allowed
146
+ if (!tierLimits.allowedModels.includes(directive.params.model)) {
147
+ return {
148
+ allowed: false,
149
+ reason: `Model '${directive.params.model}' not available for ${context.tier} tier`,
150
+ };
151
+ }
152
+
153
+ // Check token limit
154
+ if (
155
+ directive.params.maxTokens &&
156
+ directive.params.maxTokens > tierLimits.maxTokens
157
+ ) {
158
+ return {
159
+ allowed: false,
160
+ reason: `Token limit ${directive.params.maxTokens} exceeds ${context.tier} tier max of ${tierLimits.maxTokens}`,
161
+ };
162
+ }
163
+
164
+ return { allowed: true };
165
+ }
166
+
167
+ // ============================================================================
168
+ // Edge Workers ESI Processor
169
+ // ============================================================================
170
+
171
+ export class EdgeWorkersESIProcessor implements ESIProcessor {
172
+ name = 'edge-workers';
173
+ private config: ESIConfig;
174
+ private warmupPromise?: Promise<void>;
175
+
176
+ constructor(config: Partial<ESIConfig> = {}) {
177
+ this.config = {
178
+ enabled: config.enabled ?? false,
179
+ endpoint: config.endpoint || process.env.ESI_ENDPOINT || '',
180
+ timeout: config.timeout ?? 5000,
181
+ defaultCacheTtl: config.defaultCacheTtl ?? 300,
182
+ maxConcurrent: config.maxConcurrent ?? 5,
183
+ warmupModels: config.warmupModels,
184
+ tierLimits: config.tierLimits,
185
+ };
186
+ }
187
+
188
+ async warmup(): Promise<void> {
189
+ if (this.warmupPromise) return this.warmupPromise;
190
+
191
+ this.warmupPromise = (async () => {
192
+ if (!this.config.warmupModels?.length) return;
193
+
194
+ // Ping the endpoint to warm up models
195
+ await Promise.all(
196
+ this.config.warmupModels.map(
197
+ (model) =>
198
+ fetch(`${this.config.endpoint}/api/warmup`, {
199
+ method: 'POST',
200
+ headers: { 'Content-Type': 'application/json' },
201
+ body: JSON.stringify({ model }),
202
+ }).catch(() => {}), // Ignore warmup failures
203
+ ),
204
+ );
205
+ })();
206
+
207
+ return this.warmupPromise;
208
+ }
209
+
210
+ isModelAvailable(model: ESIModel): boolean {
211
+ // All models available through edge-workers
212
+ return [
213
+ 'llm',
214
+ 'embed',
215
+ 'vision',
216
+ 'tts',
217
+ 'stt',
218
+ 'emotion',
219
+ 'classify',
220
+ 'custom',
221
+ ].includes(model);
222
+ }
223
+
224
+ async process(
225
+ directive: ESIDirective,
226
+ context: UserContext,
227
+ ): Promise<ESIResult> {
228
+ const startTime = Date.now();
229
+
230
+ // Check cache first
231
+ const cacheKey = getCacheKey(directive, context);
232
+ const cached = getCached(cacheKey);
233
+ if (cached) {
234
+ return cached;
235
+ }
236
+
237
+ // Check tier access
238
+ const access = checkTierAccess(directive, context, this.config);
239
+ if (!access.allowed) {
240
+ return {
241
+ id: directive.id,
242
+ success: false,
243
+ error: access.reason,
244
+ latencyMs: Date.now() - startTime,
245
+ cached: false,
246
+ model: directive.params.model,
247
+ };
248
+ }
249
+
250
+ // Interpolate prompt with context
251
+ const prompt = directive.contextAware
252
+ ? interpolatePrompt(directive.content, context, directive.signals)
253
+ : directive.content.value;
254
+
255
+ try {
256
+ const result = await this.callEdgeWorkers(directive, prompt);
257
+
258
+ // Cache successful results
259
+ const cacheTtl = directive.params.cacheTtl ?? this.config.defaultCacheTtl;
260
+ setCache(cacheKey, result, cacheTtl);
261
+
262
+ return {
263
+ ...result,
264
+ latencyMs: Date.now() - startTime,
265
+ };
266
+ } catch (error) {
267
+ // Return fallback if provided
268
+ if (directive.params.fallback) {
269
+ return {
270
+ id: directive.id,
271
+ success: true,
272
+ output: directive.params.fallback,
273
+ latencyMs: Date.now() - startTime,
274
+ cached: false,
275
+ model: directive.params.model,
276
+ };
277
+ }
278
+
279
+ return {
280
+ id: directive.id,
281
+ success: false,
282
+ error: error instanceof Error ? error.message : 'Unknown error',
283
+ latencyMs: Date.now() - startTime,
284
+ cached: false,
285
+ model: directive.params.model,
286
+ };
287
+ }
288
+ }
289
+
290
+ async processBatch(
291
+ directives: ESIDirective[],
292
+ context: UserContext,
293
+ ): Promise<ESIResult[]> {
294
+ // Process with controlled concurrency
295
+ const semaphore = new Semaphore(this.config.maxConcurrent);
296
+
297
+ return Promise.all(
298
+ directives.map(async (directive) => {
299
+ await semaphore.acquire();
300
+ try {
301
+ return await this.process(directive, context);
302
+ } finally {
303
+ semaphore.release();
304
+ }
305
+ }),
306
+ );
307
+ }
308
+
309
+ async stream(
310
+ directive: ESIDirective,
311
+ context: UserContext,
312
+ onChunk: (chunk: string) => void,
313
+ ): Promise<ESIResult> {
314
+ const startTime = Date.now();
315
+
316
+ // Check tier access
317
+ const access = checkTierAccess(directive, context, this.config);
318
+ if (!access.allowed) {
319
+ return {
320
+ id: directive.id,
321
+ success: false,
322
+ error: access.reason,
323
+ latencyMs: Date.now() - startTime,
324
+ cached: false,
325
+ model: directive.params.model,
326
+ };
327
+ }
328
+
329
+ const prompt = directive.contextAware
330
+ ? interpolatePrompt(directive.content, context, directive.signals)
331
+ : directive.content.value;
332
+
333
+ try {
334
+ const response = await fetch(`${this.config.endpoint}/api/llm/stream`, {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({
338
+ input: prompt,
339
+ model: directive.params.variant,
340
+ options: {
341
+ temperature: directive.params.temperature,
342
+ max_tokens: directive.params.maxTokens,
343
+ stop: directive.params.stop,
344
+ top_p: directive.params.topP,
345
+ system: directive.params.system,
346
+ },
347
+ }),
348
+ signal: AbortSignal.timeout(
349
+ directive.params.timeout ?? this.config.timeout,
350
+ ),
351
+ });
352
+
353
+ if (!response.ok) {
354
+ throw new Error(`Stream failed: ${response.status}`);
355
+ }
356
+
357
+ const reader = response.body?.getReader();
358
+ if (!reader) {
359
+ throw new Error('No response body');
360
+ }
361
+
362
+ const decoder = new TextDecoder();
363
+ let fullOutput = '';
364
+
365
+ while (true) {
366
+ const { done, value } = await reader.read();
367
+ if (done) break;
368
+
369
+ const chunk = decoder.decode(value);
370
+ fullOutput += chunk;
371
+ onChunk(chunk);
372
+ }
373
+
374
+ return {
375
+ id: directive.id,
376
+ success: true,
377
+ output: fullOutput,
378
+ latencyMs: Date.now() - startTime,
379
+ cached: false,
380
+ model: directive.params.variant || directive.params.model,
381
+ };
382
+ } catch (error) {
383
+ if (directive.params.fallback) {
384
+ onChunk(directive.params.fallback);
385
+ return {
386
+ id: directive.id,
387
+ success: true,
388
+ output: directive.params.fallback,
389
+ latencyMs: Date.now() - startTime,
390
+ cached: false,
391
+ model: directive.params.model,
392
+ };
393
+ }
394
+
395
+ return {
396
+ id: directive.id,
397
+ success: false,
398
+ error: error instanceof Error ? error.message : 'Stream failed',
399
+ latencyMs: Date.now() - startTime,
400
+ cached: false,
401
+ model: directive.params.model,
402
+ };
403
+ }
404
+ }
405
+
406
+ private async callEdgeWorkers(
407
+ directive: ESIDirective,
408
+ prompt: string,
409
+ ): Promise<ESIResult> {
410
+ const endpoint = this.getEndpointForModel(directive.params.model);
411
+
412
+ const response = await fetch(`${this.config.endpoint}${endpoint}`, {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify(this.buildRequestBody(directive, prompt)),
416
+ signal: AbortSignal.timeout(
417
+ directive.params.timeout ?? this.config.timeout,
418
+ ),
419
+ });
420
+
421
+ if (!response.ok) {
422
+ const error = await response.text();
423
+ throw new Error(`ESI inference failed: ${response.status} - ${error}`);
424
+ }
425
+
426
+ const data = (await response.json()) as Record<string, unknown>;
427
+ return this.parseResponse(directive, data);
428
+ }
429
+
430
+ private getEndpointForModel(model: ESIModel): string {
431
+ switch (model) {
432
+ case 'llm':
433
+ return '/api/llm/infer';
434
+ case 'embed':
435
+ return '/api/embed';
436
+ case 'vision':
437
+ return '/api/vision';
438
+ case 'tts':
439
+ return '/api/tts';
440
+ case 'stt':
441
+ return '/api/stt';
442
+ case 'emotion':
443
+ return '/api/emotion';
444
+ case 'classify':
445
+ return '/api/classify';
446
+ case 'custom':
447
+ return '/api/custom';
448
+ default:
449
+ return '/api/llm/infer';
450
+ }
451
+ }
452
+
453
+ private buildRequestBody(
454
+ directive: ESIDirective,
455
+ prompt: string,
456
+ ): Record<string, unknown> {
457
+ const { params, content } = directive;
458
+
459
+ const body: Record<string, unknown> = {
460
+ input: content.type === 'base64' ? content.value : prompt,
461
+ model: params.variant,
462
+ };
463
+
464
+ if (params.model === 'llm') {
465
+ body.options = {
466
+ temperature: params.temperature,
467
+ max_tokens: params.maxTokens,
468
+ stop: params.stop,
469
+ top_p: params.topP,
470
+ frequency_penalty: params.frequencyPenalty,
471
+ presence_penalty: params.presencePenalty,
472
+ system: params.system,
473
+ };
474
+ }
475
+
476
+ if (params.custom) {
477
+ body.custom = params.custom;
478
+ }
479
+
480
+ return body;
481
+ }
482
+
483
+ private parseResponse(
484
+ directive: ESIDirective,
485
+ data: Record<string, unknown>,
486
+ ): ESIResult {
487
+ const base: ESIResult = {
488
+ id: directive.id,
489
+ success: true,
490
+ latencyMs: 0,
491
+ cached: false,
492
+ model: String(data.model || directive.params.model),
493
+ };
494
+
495
+ switch (directive.params.model) {
496
+ case 'embed':
497
+ return { ...base, embedding: data.embedding as number[] };
498
+ case 'tts':
499
+ return { ...base, audio: data.audio as ArrayBuffer };
500
+ default:
501
+ return {
502
+ ...base,
503
+ output: String(data.output || data.text || data.result || ''),
504
+ tokens: data.tokens as ESIResult['tokens'],
505
+ };
506
+ }
507
+ }
508
+ }
509
+
510
+ // ============================================================================
511
+ // Simple Semaphore for concurrency control
512
+ // ============================================================================
513
+
514
+ class Semaphore {
515
+ private permits: number;
516
+ private queue: Array<() => void> = [];
517
+
518
+ constructor(permits: number) {
519
+ this.permits = permits;
520
+ }
521
+
522
+ async acquire(): Promise<void> {
523
+ if (this.permits > 0) {
524
+ this.permits--;
525
+ return;
526
+ }
527
+ return new Promise((resolve) => {
528
+ this.queue.push(resolve);
529
+ });
530
+ }
531
+
532
+ release(): void {
533
+ const next = this.queue.shift();
534
+ if (next) {
535
+ next();
536
+ } else {
537
+ this.permits++;
538
+ }
539
+ }
540
+ }
541
+
542
+ // ============================================================================
543
+ // ESI Helper Functions
544
+ // ============================================================================
545
+
546
+ /**
547
+ * Create an ESI directive for LLM inference
548
+ */
549
+ export function esiInfer(
550
+ prompt: string,
551
+ options: Partial<ESIParams> = {},
552
+ ): ESIDirective {
553
+ return {
554
+ id: `esi-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
555
+ params: {
556
+ model: 'llm',
557
+ ...options,
558
+ },
559
+ content: {
560
+ type: 'text',
561
+ value: prompt,
562
+ },
563
+ contextAware: options.system?.includes('{context}'),
564
+ };
565
+ }
566
+
567
+ /**
568
+ * Create an ESI directive for embeddings
569
+ */
570
+ export function esiEmbed(text: string): ESIDirective {
571
+ return {
572
+ id: `esi-embed-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
573
+ params: { model: 'embed' },
574
+ content: { type: 'text', value: text },
575
+ };
576
+ }
577
+
578
+ /**
579
+ * Create an ESI directive for emotion detection
580
+ */
581
+ export function esiEmotion(text: string, contextAware = true): ESIDirective {
582
+ return {
583
+ id: `esi-emotion-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
584
+ params: { model: 'emotion' },
585
+ content: { type: 'text', value: text },
586
+ contextAware,
587
+ signals: contextAware ? ['emotion', 'history'] : undefined,
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Create an ESI directive for vision (image analysis)
593
+ */
594
+ export function esiVision(
595
+ base64Image: string,
596
+ prompt: string,
597
+ options: Partial<ESIParams> = {},
598
+ ): ESIDirective {
599
+ return {
600
+ id: `esi-vision-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
601
+ params: {
602
+ model: 'vision',
603
+ system: prompt,
604
+ ...options,
605
+ },
606
+ content: { type: 'base64', value: base64Image },
607
+ };
608
+ }
609
+
610
+ /**
611
+ * Create a context-aware ESI directive
612
+ * Automatically injects user context into the prompt
613
+ */
614
+ export function esiWithContext(
615
+ prompt: string,
616
+ signals: ESIDirective['signals'] = ['emotion', 'preferences', 'time'],
617
+ options: Partial<ESIParams> = {},
618
+ ): ESIDirective {
619
+ return {
620
+ id: `esi-ctx-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
621
+ params: {
622
+ model: 'llm',
623
+ ...options,
624
+ },
625
+ content: { type: 'text', value: prompt },
626
+ contextAware: true,
627
+ signals,
628
+ };
629
+ }
630
+
631
+ // ============================================================================
632
+ // Cyrano Whisper Channel (Re-exports)
633
+ // ============================================================================
634
+
635
+ export {
636
+ esiContext,
637
+ esiCyrano,
638
+ esiHalo,
639
+ evaluateTrigger,
640
+ createExhaustEntry,
641
+ getToolSuggestions,
642
+ } from './esi-cyrano';
643
+
644
+ export type {
645
+ SessionContext,
646
+ EmotionContext,
647
+ BehaviorContext,
648
+ EnvironmentContext,
649
+ BiometricContext,
650
+ CyranoWhisperConfig,
651
+ CyranoIntent,
652
+ CyranoTone,
653
+ CyranoTrigger,
654
+ HaloInsightConfig,
655
+ HaloObservation,
656
+ HaloAction,
657
+ ChatExhaustType,
658
+ ChatExhaustEntry,
659
+ ESIWhisperResult,
660
+ } from './esi-cyrano';
661
+
662
+ // ============================================================================
663
+ // Exports
664
+ // ============================================================================
665
+
666
+ export { ESIConfig, ESIDirective, ESIResult, ESIProcessor, ESIModel };