@memberjunction/ai-agents 2.107.0 → 2.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -268,6 +268,302 @@ The callback is invoked:
268
268
 
269
269
  ## Advanced Features
270
270
 
271
+ ### Agent Data Preloading
272
+
273
+ Agents can declaratively preload reference data without requiring custom application code or action calls. Data sources are configured through the `AIAgentDataSource` entity and automatically loaded before agent execution.
274
+
275
+ #### Overview
276
+
277
+ Data preloading solves the common problem of agents needing access to reference data (like entity lists, configuration values, or initial state) that doesn't change during execution. Instead of:
278
+ - Writing custom application code to load data
279
+ - Having agents call actions to fetch data (which bloats conversation context)
280
+ - Manually passing the same data to every agent invocation
281
+
282
+ Agents can now specify data sources that are automatically loaded and injected into the appropriate destination (`data`, `context`, or `payload`).
283
+
284
+ #### Three Destination Types
285
+
286
+ **1. Data Destination** - For Nunjucks templates in prompts (visible to LLMs)
287
+ ```typescript
288
+ // Configuration
289
+ {
290
+ "Name": "ALL_ENTITIES",
291
+ "SourceType": "RunView",
292
+ "EntityName": "Entities",
293
+ "OrderBy": "Name ASC",
294
+ "DestinationType": "Data",
295
+ "DestinationPath": null // Uses "ALL_ENTITIES" at root level
296
+ }
297
+
298
+ // Result in agent prompt:
299
+ // params.data.ALL_ENTITIES = [{ Name: "Users", ... }, { Name: "Entities", ... }]
300
+
301
+ // Prompt can use Nunjucks:
302
+ // You have access to {{ALL_ENTITIES.length}} entities:
303
+ // {% for entity in ALL_ENTITIES %}
304
+ // - {{entity.Name}}: {{entity.Description}}
305
+ // {% endfor %}
306
+ ```
307
+
308
+ **2. Context Destination** - For actions only (NOT visible to LLMs)
309
+ ```typescript
310
+ // Configuration
311
+ {
312
+ "Name": "ORG_SETTINGS",
313
+ "SourceType": "RunView",
314
+ "EntityName": "Organization Settings",
315
+ "ExtraFilter": "OrgID='${context.organizationId}'",
316
+ "DestinationType": "Context",
317
+ "DestinationPath": "organization.settings"
318
+ }
319
+
320
+ // Result:
321
+ // params.context.organization.settings = { apiEndpoint: "...", features: [...] }
322
+
323
+ // Actions can access context, but prompts/LLMs cannot
324
+ // This keeps API keys and sensitive configuration away from LLMs
325
+ ```
326
+
327
+ **3. Payload Destination** - For agent state initialization
328
+ ```typescript
329
+ // Configuration
330
+ {
331
+ "Name": "CustomerOrders",
332
+ "SourceType": "RunQuery",
333
+ "QueryName": "Recent Orders by Customer",
334
+ "Parameters": JSON.stringify({ customerId: "{{context.customerId}}" }),
335
+ "DestinationType": "Payload",
336
+ "DestinationPath": "analysis.orders.recent"
337
+ }
338
+
339
+ // Result:
340
+ // params.payload.analysis.orders.recent = [{ OrderID: "123", ... }]
341
+
342
+ // Agent starts with rich initial state without caller manually loading it
343
+ ```
344
+
345
+ #### Data Source Types
346
+
347
+ **RunView Data Sources** - Query entities with filters
348
+ ```typescript
349
+ {
350
+ "Name": "ACTIVE_MODELS",
351
+ "SourceType": "RunView",
352
+ "EntityName": "AI Models",
353
+ "ExtraFilter": "IsActive=1 AND Vendor='OpenAI'",
354
+ "OrderBy": "Priority DESC",
355
+ "FieldsToRetrieve": JSON.stringify(["ID", "Name", "Vendor", "MaxInputTokens"]),
356
+ "ResultType": "simple", // or "entity_object"
357
+ "MaxRows": 100,
358
+ "DestinationType": "Data"
359
+ }
360
+ ```
361
+
362
+ **RunQuery Data Sources** - Execute stored queries
363
+ ```typescript
364
+ {
365
+ "Name": "MONTHLY_STATS",
366
+ "SourceType": "RunQuery",
367
+ "QueryName": "Monthly Analytics",
368
+ "CategoryPath": "/Reports/Analytics",
369
+ "Parameters": JSON.stringify({
370
+ month: "{{context.currentMonth}}",
371
+ year: "{{context.currentYear}}"
372
+ }),
373
+ "DestinationType": "Payload",
374
+ "DestinationPath": "stats.monthly"
375
+ }
376
+ ```
377
+
378
+ #### Path Support
379
+
380
+ The `DestinationPath` field supports nested paths using dot notation:
381
+
382
+ ```typescript
383
+ // Simple root-level
384
+ {
385
+ "Name": "ENTITIES",
386
+ "DestinationPath": null // Uses "ENTITIES" at root
387
+ }
388
+ // Result: data.ENTITIES
389
+
390
+ // Nested paths
391
+ {
392
+ "Name": "ModelList",
393
+ "DestinationPath": "config.ai.models"
394
+ }
395
+ // Result: data.config.ai.models
396
+
397
+ // Deep nesting
398
+ {
399
+ "Name": "CustomerData",
400
+ "DestinationPath": "analysis.customer.profile.orders"
401
+ }
402
+ // Result: payload.analysis.customer.profile.orders
403
+ ```
404
+
405
+ #### Caching Policies
406
+
407
+ Data sources support three caching strategies:
408
+
409
+ **1. None** - No caching (default)
410
+ ```typescript
411
+ {
412
+ "CachePolicy": "None"
413
+ // Data is loaded fresh every time
414
+ }
415
+ ```
416
+
417
+ **2. PerRun** - Cache for duration of a single agent run
418
+ ```typescript
419
+ {
420
+ "CachePolicy": "PerRun"
421
+ // Multiple data sources with same AgentID+Name share cached data within one run
422
+ // Cache is cleared when agent run completes
423
+ }
424
+ ```
425
+
426
+ **3. PerAgent** - Global cache with TTL
427
+ ```typescript
428
+ {
429
+ "CachePolicy": "PerAgent",
430
+ "CacheTimeoutSeconds": 3600 // 1 hour
431
+ // Cached across all runs for this agent until TTL expires
432
+ // Good for rarely-changing reference data like entity lists
433
+ }
434
+ ```
435
+
436
+ #### Execution Control
437
+
438
+ **Disable data preloading** for specific executions:
439
+ ```typescript
440
+ const result = await runner.RunAgent({
441
+ agent: myAgent,
442
+ conversationMessages: messages,
443
+ contextUser: user,
444
+ disableDataPreloading: true // Skip automatic data preloading
445
+ });
446
+ ```
447
+
448
+ **Caller precedence**: Caller-provided data always takes precedence over preloaded data:
449
+ ```typescript
450
+ const result = await runner.RunAgent({
451
+ agent: myAgent,
452
+ conversationMessages: messages,
453
+ contextUser: user,
454
+ data: {
455
+ CUSTOM_ENTITIES: myEntities // Overrides preloaded CUSTOM_ENTITIES
456
+ }
457
+ });
458
+ ```
459
+
460
+ #### Configuration Examples
461
+
462
+ **Database Research Agent** - Preload entity metadata
463
+ ```typescript
464
+ // Data source 1: All entities for reference
465
+ {
466
+ "AgentID": "database-research-agent-id",
467
+ "Name": "ALL_ENTITIES",
468
+ "SourceType": "RunView",
469
+ "EntityName": "Entities",
470
+ "OrderBy": "Name ASC",
471
+ "FieldsToRetrieve": JSON.stringify(["ID", "Name", "SchemaName", "Description", "BaseView"]),
472
+ "DestinationType": "Data",
473
+ "ExecutionOrder": 1,
474
+ "Status": "Active",
475
+ "CachePolicy": "PerAgent",
476
+ "CacheTimeoutSeconds": 3600
477
+ }
478
+
479
+ // Data source 2: Schema information
480
+ {
481
+ "AgentID": "database-research-agent-id",
482
+ "Name": "SCHEMA_INFO",
483
+ "SourceType": "RunView",
484
+ "EntityName": "Entity Fields",
485
+ "DestinationType": "Data",
486
+ "DestinationPath": "schema.fields",
487
+ "ExecutionOrder": 2,
488
+ "Status": "Active",
489
+ "CachePolicy": "PerAgent",
490
+ "CacheTimeoutSeconds": 3600
491
+ }
492
+ ```
493
+
494
+ **Customer Service Agent** - Preload customer context
495
+ ```typescript
496
+ // Preload customer data into payload
497
+ {
498
+ "AgentID": "customer-service-agent-id",
499
+ "Name": "CUSTOMER_PROFILE",
500
+ "SourceType": "RunView",
501
+ "EntityName": "Customers",
502
+ "ExtraFilter": "ID='{{context.customerId}}'",
503
+ "DestinationType": "Payload",
504
+ "DestinationPath": "customer.profile",
505
+ "Status": "Active",
506
+ "CachePolicy": "PerRun"
507
+ }
508
+
509
+ // Preload recent orders
510
+ {
511
+ "AgentID": "customer-service-agent-id",
512
+ "Name": "RECENT_ORDERS",
513
+ "SourceType": "RunQuery",
514
+ "QueryName": "Recent Orders by Customer",
515
+ "Parameters": JSON.stringify({ customerId: "{{context.customerId}}", days: 30 }),
516
+ "DestinationType": "Payload",
517
+ "DestinationPath": "customer.orders",
518
+ "Status": "Active",
519
+ "CachePolicy": "PerRun"
520
+ }
521
+
522
+ // Preload organization settings (for actions)
523
+ {
524
+ "AgentID": "customer-service-agent-id",
525
+ "Name": "ORG_CONFIG",
526
+ "SourceType": "RunView",
527
+ "EntityName": "Organization Settings",
528
+ "ExtraFilter": "OrgID='{{context.organizationId}}'",
529
+ "DestinationType": "Context",
530
+ "DestinationPath": "organization.config",
531
+ "Status": "Active",
532
+ "CachePolicy": "PerAgent",
533
+ "CacheTimeoutSeconds": 1800
534
+ }
535
+ ```
536
+
537
+ #### Benefits
538
+
539
+ - **Declarative**: Configure data preloading through metadata, not code
540
+ - **Reusable**: Same agent works across different environments
541
+ - **Efficient**: Caching reduces redundant database queries
542
+ - **Clean Separation**: Keeps data in appropriate destinations (data/context/payload)
543
+ - **Flexible**: Supports both RunView and RunQuery with full parameter control
544
+ - **Secure**: Context destination keeps sensitive data away from LLMs
545
+ - **Performance**: Multiple caching strategies for different use cases
546
+
547
+ #### Database Schema
548
+
549
+ The `AIAgentDataSource` table includes:
550
+ - **AgentID**: The agent using this data source
551
+ - **Name**: Variable name (used as fallback if DestinationPath is null)
552
+ - **SourceType**: RunView or RunQuery
553
+ - **EntityName**, **ExtraFilter**, **OrderBy**, **FieldsToRetrieve**, **ResultType**: RunView parameters
554
+ - **QueryName**, **CategoryPath**, **Parameters**: RunQuery parameters
555
+ - **MaxRows**: Limit results (applies to both source types)
556
+ - **DestinationType**: Data, Context, or Payload
557
+ - **DestinationPath**: Nested path using dot notation (optional)
558
+ - **ExecutionOrder**: Order to execute when multiple sources exist
559
+ - **Status**: Active or Disabled
560
+ - **CachePolicy**: None, PerRun, or PerAgent
561
+ - **CacheTimeoutSeconds**: TTL for PerAgent cache
562
+
563
+ **Unique Constraint**: `AgentID + Name + DestinationType + DestinationPath`
564
+ - Allows same Name across different destinations/paths
565
+ - Example: "ENTITIES" can exist in both Data and Payload destinations
566
+
271
567
  ### Payload Scoping for Sub-Agents
272
568
 
273
569
  The framework now supports narrowing the payload that sub-agents work with through the `PayloadScope` field:
@@ -498,10 +794,164 @@ const result = await agent.ExecutePrompt({
498
794
  ### Context Management
499
795
  Agents automatically manage conversation context:
500
796
  - Maintains message history across steps
797
+ - **Intelligent message expiration** - Automatically compacts or removes old action results
501
798
  - Compresses context when approaching token limits
502
799
  - Handles placeholder replacement in prompts
503
800
  - Preserves important context during compression
504
801
 
802
+ ### Message Expiration and Compaction
803
+
804
+ The framework provides sophisticated message lifecycle management to prevent context bloat from large action results:
805
+
806
+ **Per-Action Configuration** (in `AIAgentAction` table):
807
+ - `ResultExpirationTurns`: Number of turns before message expires (e.g., 2)
808
+ - `ResultExpirationMode`: 'None' | 'Remove' | 'Compact'
809
+ - `CompactMode`: 'First N Chars' | 'AI Summary'
810
+ - `CompactLength`: Character limit for 'First N Chars' mode
811
+ - `CompactPromptID`: Custom AI prompt for 'AI Summary' mode
812
+
813
+ **How It Works**:
814
+ ```typescript
815
+ // Configure a Google Search action to compact results after 2 turns
816
+ await agentAction.Save({
817
+ ResultExpirationTurns: 2,
818
+ ResultExpirationMode: 'Compact',
819
+ CompactMode: 'First N Chars',
820
+ CompactLength: 500
821
+ });
822
+
823
+ // Turn 1: Action returns 10,000 char search results
824
+ // Turn 2: Results still in conversation (turn 1, limit 2)
825
+ // Turn 3: Results still in conversation (turn 2, limit 2)
826
+ // Turn 4: Results compacted to 500 chars (turn 3 > limit 2)
827
+ // Original content preserved in metadata for expansion
828
+ ```
829
+
830
+ **Compaction Modes**:
831
+ 1. **First N Chars**: Fast truncation with annotation
832
+ ```
833
+ First 500 chars of result...
834
+
835
+ [Compacted: showing first 500 of 10000 characters. Agent can request expansion if needed.]
836
+ ```
837
+
838
+ 2. **AI Summary**: Intelligent LLM-based summarization
839
+ ```
840
+ [AI Summary of 10000 chars. Agent can request full expansion if needed.]
841
+
842
+ Search found 47 results for "MemberJunction". Top results include...
843
+ ```
844
+
845
+ **Message Expansion**:
846
+ Agents can restore compacted messages when needed:
847
+ ```typescript
848
+ // In agent's JSON response
849
+ {
850
+ "taskComplete": false,
851
+ "nextStep": {
852
+ "type": "Retry",
853
+ "messageIndex": 5, // Index of compacted message
854
+ "reason": "Need full search results to answer user's question about item #47"
855
+ }
856
+ }
857
+ ```
858
+
859
+ **Runtime Override**:
860
+ Test different expiration strategies without modifying database:
861
+ ```typescript
862
+ const result = await runner.RunAgent({
863
+ agent: myAgent,
864
+ conversationMessages: messages,
865
+ contextUser: user,
866
+ messageExpirationOverride: {
867
+ expirationTurns: 1,
868
+ expirationMode: 'Compact',
869
+ compactMode: 'First N Chars',
870
+ compactLength: 200,
871
+ preserveOriginalContent: true
872
+ }
873
+ });
874
+ ```
875
+
876
+ **Lifecycle Monitoring**:
877
+ Track message compaction for debugging and token savings analysis:
878
+ ```typescript
879
+ const result = await runner.RunAgent({
880
+ agent: myAgent,
881
+ conversationMessages: messages,
882
+ contextUser: user,
883
+ onMessageLifecycle: (event) => {
884
+ console.log(`[Turn ${event.turn}] ${event.type}: ${event.reason}`);
885
+ if (event.tokensSaved) {
886
+ console.log(` Tokens saved: ${event.tokensSaved}`);
887
+ }
888
+ }
889
+ });
890
+ // Output:
891
+ // [Turn 3] message-compacted: Compacted using First N Chars (saved 2375 tokens)
892
+ // [Turn 5] message-removed: Removed due to expiration
893
+ ```
894
+
895
+ **Prompt Lookup Hierarchy**:
896
+ For AI Summary mode, prompts are resolved in this order:
897
+ 1. Runtime override (`messageExpirationOverride.compactPromptId`)
898
+ 2. Agent action configuration (`AIAgentAction.CompactPromptID`)
899
+ 3. Action default (`Action.DefaultCompactPromptID`)
900
+ 4. System default ("Compact Agent Message" prompt)
901
+
902
+ **Benefits**:
903
+ - **Addresses Large Action Results**: Automatically handles the most common cause of context bloat
904
+ - **Configurable Per-Action**: Different expiration strategies for different action types
905
+ - **Non-Destructive**: Original content preserved in metadata for on-demand expansion
906
+ - **Token Savings**: Reduces context window usage by 70-95% for large results
907
+ - **Agent-Aware**: Agents can detect compacted messages and request full expansion when needed
908
+
909
+ ### Context Length Recovery
910
+
911
+ When a prompt execution fails due to context length overflow (even after model failover), BaseAgent provides **one-time automatic recovery** instead of immediately terminating. This gives the agent an opportunity to adapt its approach.
912
+
913
+ **How It Works**:
914
+ 1. **Prompt fails with ContextLengthExceeded** → Detected as fatal error
915
+ 2. **First occurrence**: Recovery is attempted automatically (once per run)
916
+ 3. **Last user message is trimmed** using smart strategies:
917
+ - JSON arrays: Keeps first 10 items with truncation notice
918
+ - CSV data: Keeps header + first 10 rows
919
+ - Plain text: Keeps first 1000 characters
920
+ 4. **Agent receives clear guidance** explaining what happened and recommended actions
921
+ 5. **Agent gets Retry step** to choose alternative approach (e.g., more specific filters, batch requests)
922
+ 6. **If recovery fails again**: Normal fatal error handling (agent terminates)
923
+
924
+ **Example Recovery Message**:
925
+ ```
926
+ ⚠️ CONTEXT OVERFLOW RECOVERY ⚠️
927
+
928
+ The previous step returned a result that exceeded the context window (147,532 characters truncated).
929
+
930
+ Here is a PARTIAL result from the previous action:
931
+ ---
932
+ [First 10 items from JSON array...]
933
+ ... (487 more items truncated due to context length)
934
+ ---
935
+
936
+ ❗ THE ABOVE IS INCOMPLETE - the full result was too large for the context window.
937
+
938
+ RECOMMENDED ACTIONS:
939
+ 1. Use a different action with more specific filters to get smaller result sets
940
+ 2. Request data in batches or pages instead of all at once
941
+ 3. Ask the user to clarify scope to narrow the query
942
+ 4. If you need the full data, acknowledge the limitation and ask the user how to proceed
943
+
944
+ Please choose an alternative approach to complete your task.
945
+ ```
946
+
947
+ **Benefits**:
948
+ - **Resilient**: Agents can adapt instead of failing immediately
949
+ - **Informative**: Clear explanation of what went wrong and how to recover
950
+ - **Safe**: ONE-TIME recovery prevents infinite loops
951
+ - **Smart**: Preserves data structure when possible (JSON, CSV)
952
+
953
+ This feature is particularly useful when agents call actions that can return very large datasets (e.g., "Get Entity List" without filters).
954
+
505
955
  ### Action Integration
506
956
  ```typescript
507
957
  // In agent type's DetermineNextStep
@@ -0,0 +1,25 @@
1
+ import { UserInfo } from '@memberjunction/core';
2
+ export interface PreloadedDataResult {
3
+ data: Record<string, unknown>;
4
+ context: Record<string, unknown>;
5
+ payload: Record<string, unknown>;
6
+ }
7
+ export declare class AgentDataPreloader {
8
+ private static _instance;
9
+ private _perAgentCache;
10
+ private _perRunCache;
11
+ private constructor();
12
+ static get Instance(): AgentDataPreloader;
13
+ PreloadAgentData(agentId: string, contextUser: UserInfo, runId?: string): Promise<PreloadedDataResult>;
14
+ clearRunCache(runId: string): void;
15
+ clearAgentCache(): void;
16
+ private loadDataSourcesForAgent;
17
+ private executeDataSource;
18
+ private executeRunView;
19
+ private executeRunQuery;
20
+ private getCachedData;
21
+ private cacheData;
22
+ private getPerAgentCacheKey;
23
+ }
24
+ export declare function LoadAgentDataPreloader(): AgentDataPreloader;
25
+ //# sourceMappingURL=AgentDataPreloader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentDataPreloader.d.ts","sourceRoot":"","sources":["../src/AgentDataPreloader.ts"],"names":[],"mappings":"AAcA,OAAO,EAAqE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAQnH,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AA4BD,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAmC;IAK3D,OAAO,CAAC,cAAc,CAAsC;IAK5D,OAAO,CAAC,YAAY,CAAgD;IAKpE,OAAO;IAOP,WAAkB,QAAQ,IAAI,kBAAkB,CAK/C;IA4BY,gBAAgB,CACzB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,QAAQ,EACrB,KAAK,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,mBAAmB,CAAC;IA8ExB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAalC,eAAe,IAAI,IAAI;YAchB,uBAAuB;YAuBvB,iBAAiB;YAsCjB,cAAc;YAwCd,eAAe;IAuC7B,OAAO,CAAC,aAAa;IA2CrB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,mBAAmB;CAG9B;AAKD,wBAAgB,sBAAsB,uBAGrC"}