@elizaos/core 1.5.1 → 1.5.2

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 (88) hide show
  1. package/dist/browser/index.browser.js +120 -120
  2. package/dist/browser/index.browser.js.map +5 -21
  3. package/dist/browser/index.d.ts +3 -1
  4. package/dist/index.d.ts +2 -3
  5. package/dist/index.js +1 -5
  6. package/dist/node/index.d.ts +3 -1
  7. package/package.json +10 -4
  8. package/src/__tests__/action-chaining-simple.test.ts +203 -0
  9. package/src/__tests__/actions.test.ts +218 -0
  10. package/src/__tests__/buffer.test.ts +337 -0
  11. package/src/__tests__/character-validation.test.ts +309 -0
  12. package/src/__tests__/database.test.ts +750 -0
  13. package/src/__tests__/entities.test.ts +727 -0
  14. package/src/__tests__/env.test.ts +23 -0
  15. package/src/__tests__/environment.test.ts +285 -0
  16. package/src/__tests__/logger-browser-node.test.ts +716 -0
  17. package/src/__tests__/logger.test.ts +403 -0
  18. package/src/__tests__/messages.test.ts +196 -0
  19. package/src/__tests__/mockCharacter.ts +544 -0
  20. package/src/__tests__/parsing.test.ts +58 -0
  21. package/src/__tests__/prompts.test.ts +159 -0
  22. package/src/__tests__/roles.test.ts +331 -0
  23. package/src/__tests__/runtime-embedding.test.ts +343 -0
  24. package/src/__tests__/runtime.test.ts +978 -0
  25. package/src/__tests__/search.test.ts +15 -0
  26. package/src/__tests__/services-by-type.test.ts +204 -0
  27. package/src/__tests__/services.test.ts +136 -0
  28. package/src/__tests__/settings.test.ts +810 -0
  29. package/src/__tests__/utils.test.ts +1105 -0
  30. package/src/__tests__/uuid.test.ts +94 -0
  31. package/src/actions.ts +122 -0
  32. package/src/database.ts +579 -0
  33. package/src/entities.ts +406 -0
  34. package/src/index.browser.ts +48 -0
  35. package/src/index.node.ts +39 -0
  36. package/src/index.ts +50 -0
  37. package/src/logger.ts +527 -0
  38. package/src/prompts.ts +243 -0
  39. package/src/roles.ts +85 -0
  40. package/src/runtime.ts +2514 -0
  41. package/src/schemas/character.ts +149 -0
  42. package/src/search.ts +1543 -0
  43. package/src/sentry/instrument.browser.ts +65 -0
  44. package/src/sentry/instrument.node.ts +57 -0
  45. package/src/sentry/instrument.ts +82 -0
  46. package/src/services.ts +105 -0
  47. package/src/settings.ts +409 -0
  48. package/src/test_resources/constants.ts +12 -0
  49. package/src/test_resources/testSetup.ts +21 -0
  50. package/src/test_resources/types.ts +22 -0
  51. package/src/types/agent.ts +112 -0
  52. package/src/types/browser.ts +145 -0
  53. package/src/types/components.ts +184 -0
  54. package/src/types/database.ts +348 -0
  55. package/src/types/email.ts +162 -0
  56. package/src/types/environment.ts +129 -0
  57. package/src/types/events.ts +249 -0
  58. package/src/types/index.ts +29 -0
  59. package/src/types/knowledge.ts +65 -0
  60. package/src/types/lp.ts +124 -0
  61. package/src/types/memory.ts +228 -0
  62. package/src/types/message.ts +233 -0
  63. package/src/types/messaging.ts +57 -0
  64. package/src/types/model.ts +359 -0
  65. package/src/types/pdf.ts +77 -0
  66. package/src/types/plugin.ts +78 -0
  67. package/src/types/post.ts +271 -0
  68. package/src/types/primitives.ts +97 -0
  69. package/src/types/runtime.ts +190 -0
  70. package/src/types/service.ts +198 -0
  71. package/src/types/settings.ts +30 -0
  72. package/src/types/state.ts +60 -0
  73. package/src/types/task.ts +72 -0
  74. package/src/types/tee.ts +107 -0
  75. package/src/types/testing.ts +30 -0
  76. package/src/types/token.ts +96 -0
  77. package/src/types/transcription.ts +133 -0
  78. package/src/types/video.ts +108 -0
  79. package/src/types/wallet.ts +56 -0
  80. package/src/types/web-search.ts +146 -0
  81. package/src/utils/__tests__/buffer.test.ts +80 -0
  82. package/src/utils/__tests__/environment.test.ts +58 -0
  83. package/src/utils/__tests__/stringToUuid.test.ts +88 -0
  84. package/src/utils/buffer.ts +312 -0
  85. package/src/utils/environment.ts +316 -0
  86. package/src/utils/server-health.ts +117 -0
  87. package/src/utils.ts +1076 -0
  88. package/dist/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,978 @@
1
+ import { beforeEach, afterEach, describe, expect, it } from 'bun:test';
2
+ import { mock, spyOn } from 'bun:test';
3
+ import { AgentRuntime } from '../runtime';
4
+ import { MemoryType, ModelType } from '../types';
5
+ import type {
6
+ Action,
7
+ Character,
8
+ IDatabaseAdapter,
9
+ Memory,
10
+ ModelTypeName,
11
+ Plugin,
12
+ Provider,
13
+ State,
14
+ UUID,
15
+ } from '../types';
16
+ import { v4 as uuidv4 } from 'uuid';
17
+ const stringToUuid = (id: string): UUID => id as UUID;
18
+
19
+ // --- Mocks ---
20
+
21
+ // Use hoisted for prompts mock
22
+ const mockSplitChunks = mock();
23
+ mock.module('../src/utils', () => ({
24
+ splitChunks: mockSplitChunks,
25
+ }));
26
+
27
+ // Use hoisted for ./index mock (safeReplacer)
28
+ const mockSafeReplacer = mock((_key, value) => value); // Simple replacer mock
29
+ // Don't mock the entire index module to avoid interfering with other tests
30
+
31
+ // Mock IDatabaseAdapter (inline style matching your example)
32
+ const mockDatabaseAdapter: IDatabaseAdapter = {
33
+ db: {},
34
+ init: mock().mockResolvedValue(undefined),
35
+ initialize: mock().mockResolvedValue(undefined),
36
+ runMigrations: mock().mockResolvedValue(undefined),
37
+ isReady: mock().mockResolvedValue(true),
38
+ close: mock().mockResolvedValue(undefined),
39
+ getConnection: mock().mockResolvedValue({}),
40
+ getEntityByIds: mock().mockResolvedValue([]),
41
+ getEntitiesByIds: mock().mockResolvedValue([]),
42
+ createEntities: mock().mockResolvedValue(true),
43
+ getMemories: mock().mockResolvedValue([]),
44
+ getMemoryById: mock().mockResolvedValue(null),
45
+ getMemoriesByRoomIds: mock().mockResolvedValue([]),
46
+ getMemoriesByIds: mock().mockResolvedValue([]),
47
+ getCachedEmbeddings: mock().mockResolvedValue([]),
48
+ log: mock().mockResolvedValue(undefined),
49
+ searchMemories: mock().mockResolvedValue([]),
50
+ createMemory: mock().mockResolvedValue(stringToUuid(uuidv4())),
51
+ deleteMemory: mock().mockResolvedValue(undefined),
52
+ deleteManyMemories: mock().mockResolvedValue(undefined),
53
+ deleteAllMemories: mock().mockResolvedValue(undefined),
54
+ countMemories: mock().mockResolvedValue(0),
55
+ getRoomsByIds: mock().mockResolvedValue([]),
56
+ createRooms: mock().mockResolvedValue([stringToUuid(uuidv4())]),
57
+ deleteRoom: mock().mockResolvedValue(undefined),
58
+ getRoomsForParticipant: mock().mockResolvedValue([]),
59
+ getRoomsForParticipants: mock().mockResolvedValue([]),
60
+ addParticipantsRoom: mock().mockResolvedValue(true),
61
+ removeParticipant: mock().mockResolvedValue(true),
62
+ getParticipantsForEntity: mock().mockResolvedValue([]),
63
+ getParticipantsForRoom: mock().mockResolvedValue([]),
64
+ getParticipantUserState: mock().mockResolvedValue(null),
65
+ setParticipantUserState: mock().mockResolvedValue(undefined),
66
+ createRelationship: mock().mockResolvedValue(true),
67
+ getRelationship: mock().mockResolvedValue(null),
68
+ getRelationships: mock().mockResolvedValue([]),
69
+ getAgent: mock().mockResolvedValue(null),
70
+ getAgents: mock().mockResolvedValue([]),
71
+ createAgent: mock().mockResolvedValue(true),
72
+ updateAgent: mock().mockResolvedValue(true),
73
+ deleteAgent: mock().mockResolvedValue(true),
74
+ ensureEmbeddingDimension: mock().mockResolvedValue(undefined),
75
+ getEntitiesForRoom: mock().mockResolvedValue([]),
76
+ updateEntity: mock().mockResolvedValue(undefined),
77
+ getComponent: mock().mockResolvedValue(null),
78
+ getComponents: mock().mockResolvedValue([]),
79
+ createComponent: mock().mockResolvedValue(true),
80
+ updateComponent: mock().mockResolvedValue(undefined),
81
+ deleteComponent: mock().mockResolvedValue(undefined),
82
+ createWorld: mock().mockResolvedValue(stringToUuid(uuidv4())),
83
+ getWorld: mock().mockResolvedValue(null),
84
+ getAllWorlds: mock().mockResolvedValue([]),
85
+ updateWorld: mock().mockResolvedValue(undefined),
86
+ updateRoom: mock().mockResolvedValue(undefined),
87
+ getRoomsByWorld: mock().mockResolvedValue([]),
88
+ updateRelationship: mock().mockResolvedValue(undefined),
89
+ getCache: mock().mockResolvedValue(undefined),
90
+ setCache: mock().mockResolvedValue(true),
91
+ deleteCache: mock().mockResolvedValue(true),
92
+ createTask: mock().mockResolvedValue(stringToUuid(uuidv4())),
93
+ getTasks: mock().mockResolvedValue([]),
94
+ getTask: mock().mockResolvedValue(null),
95
+ getTasksByName: mock().mockResolvedValue([]),
96
+ updateTask: mock().mockResolvedValue(undefined),
97
+ deleteTask: mock().mockResolvedValue(undefined),
98
+ updateMemory: mock().mockResolvedValue(true),
99
+ getLogs: mock().mockResolvedValue([]),
100
+ deleteLog: mock().mockResolvedValue(undefined),
101
+ removeWorld: mock().mockResolvedValue(undefined),
102
+ deleteRoomsByWorldId: function (_worldId: UUID): Promise<void> {
103
+ throw new Error('Function not implemented.');
104
+ },
105
+ getMemoriesByWorldId: function (_params: {
106
+ worldId: UUID;
107
+ count?: number;
108
+ tableName?: string;
109
+ }): Promise<Memory[]> {
110
+ throw new Error('Function not implemented.');
111
+ },
112
+ };
113
+
114
+ // Mock action creator (matches your example)
115
+ const createMockAction = (name: string): Action => ({
116
+ name,
117
+ description: `Test action ${name}`,
118
+ similes: [`like ${name}`],
119
+ examples: [],
120
+ handler: mock().mockResolvedValue(undefined),
121
+ validate: mock().mockImplementation(async () => true),
122
+ });
123
+
124
+ // Mock Memory creator
125
+ const createMockMemory = (
126
+ text: string,
127
+ id?: UUID,
128
+ entityId?: UUID,
129
+ roomId?: UUID,
130
+ agentId?: UUID
131
+ ): Memory => ({
132
+ id: id ?? stringToUuid(uuidv4()),
133
+ entityId: entityId ?? stringToUuid(uuidv4()),
134
+ agentId: agentId, // Pass agentId if needed
135
+ roomId: roomId ?? stringToUuid(uuidv4()),
136
+ content: { text }, // Assuming simple text content
137
+ createdAt: Date.now(),
138
+ metadata: { type: MemoryType.MESSAGE }, // Simple metadata
139
+ });
140
+
141
+ // Mock State creator
142
+ const createMockState = (text = '', values = {}, data = {}): State => ({
143
+ values,
144
+ data,
145
+ text,
146
+ });
147
+
148
+ // Mock Character
149
+ const mockCharacter: Character = {
150
+ id: stringToUuid(uuidv4()),
151
+ name: 'Test Character',
152
+ plugins: ['@elizaos/plugin-sql'],
153
+ username: 'test',
154
+ bio: ['Test bio'],
155
+ messageExamples: [], // Ensure required fields are present
156
+ postExamples: [],
157
+ topics: [],
158
+ adjectives: [],
159
+ style: {
160
+ all: [],
161
+ chat: [],
162
+ post: [],
163
+ },
164
+ // Add other fields if your runtime logic depends on them
165
+ };
166
+
167
+ // --- Test Suite ---
168
+
169
+ describe('AgentRuntime (Non-Instrumented Baseline)', () => {
170
+ let runtime: AgentRuntime;
171
+ let agentId: UUID;
172
+
173
+ beforeEach(() => {
174
+ mock.restore(); // Bun:test equivalent of clearAllMocks
175
+
176
+ // Reset all mock call counts manually but keep return values
177
+ Object.values(mockDatabaseAdapter).forEach((mockFn) => {
178
+ if (mockFn && typeof mockFn.mockClear === 'function') {
179
+ mockFn.mockClear();
180
+ }
181
+ });
182
+
183
+ agentId = mockCharacter.id!; // Use character's ID
184
+
185
+ // Instantiate runtime correctly, passing adapter in options object
186
+ runtime = new AgentRuntime({
187
+ character: mockCharacter,
188
+ agentId: agentId,
189
+ adapter: mockDatabaseAdapter, // Correct way to pass adapter
190
+ // No plugins passed here by default, tests can pass them if needed
191
+ });
192
+ });
193
+
194
+ it('should construct without errors', () => {
195
+ expect(runtime).toBeInstanceOf(AgentRuntime);
196
+ expect(runtime.agentId).toEqual(agentId);
197
+ expect(runtime.character).toEqual(mockCharacter);
198
+ expect(runtime.adapter).toBe(mockDatabaseAdapter);
199
+ });
200
+
201
+ it('should register database adapter via constructor', () => {
202
+ // This is implicitly tested by the constructor test above
203
+ expect(runtime.adapter).toBeDefined();
204
+ expect(runtime.adapter).toEqual(mockDatabaseAdapter);
205
+ });
206
+
207
+ describe('Plugin Registration', () => {
208
+ it('should register a simple plugin', async () => {
209
+ const mockPlugin: Plugin = { name: 'TestPlugin', description: 'A test plugin' };
210
+ await runtime.registerPlugin(mockPlugin);
211
+ console.log('runtime.plugins', runtime.plugins);
212
+ // Check if the plugin is added to the internal list
213
+ expect(runtime.plugins.some((p) => p.name === 'TestPlugin')).toBe(true);
214
+ });
215
+
216
+ it('should call plugin init function', async () => {
217
+ const initMock = mock().mockResolvedValue(undefined);
218
+ const mockPlugin: Plugin = {
219
+ name: 'InitPlugin',
220
+ description: 'Plugin with init',
221
+ init: initMock,
222
+ };
223
+ await runtime.registerPlugin(mockPlugin);
224
+ expect(initMock).toHaveBeenCalledTimes(1);
225
+ expect(initMock).toHaveBeenCalledWith(expect.anything(), runtime); // Check if called with config and runtime
226
+ });
227
+
228
+ it('should register plugin features (actions, providers, models) when initialized', async () => {
229
+ const actionHandler = mock();
230
+ const providerGet = mock().mockResolvedValue({ text: 'provider_text' });
231
+ const modelHandler = mock().mockResolvedValue('model_result');
232
+
233
+ const mockPlugin: Plugin = {
234
+ name: 'FeaturesPlugin',
235
+ description: 'Plugin with features',
236
+ actions: [
237
+ {
238
+ name: 'TestAction',
239
+ description: 'Test action',
240
+ handler: actionHandler,
241
+ validate: async () => true,
242
+ },
243
+ ],
244
+ providers: [{ name: 'TestProvider', get: providerGet }],
245
+ models: { [ModelType.TEXT_SMALL]: modelHandler },
246
+ };
247
+
248
+ // Re-create runtime passing plugin in constructor
249
+ runtime = new AgentRuntime({
250
+ character: mockCharacter,
251
+ agentId: agentId,
252
+ adapter: mockDatabaseAdapter,
253
+ plugins: [mockPlugin], // Pass plugin during construction
254
+ });
255
+
256
+ // Mock adapter calls needed for initialize
257
+ const ensureAgentExistsSpy = spyOn(
258
+ AgentRuntime.prototype,
259
+ 'ensureAgentExists'
260
+ ).mockResolvedValue({
261
+ ...mockCharacter,
262
+ id: agentId, // ensureAgentExists should return the agent
263
+ createdAt: Date.now(),
264
+ updatedAt: Date.now(),
265
+ enabled: true,
266
+ });
267
+
268
+ (mockDatabaseAdapter.getEntityByIds as any).mockResolvedValue([
269
+ {
270
+ id: agentId,
271
+ agentId: agentId,
272
+ names: [mockCharacter.name],
273
+ },
274
+ ]);
275
+ (mockDatabaseAdapter.getEntitiesByIds as any).mockResolvedValue([
276
+ {
277
+ id: agentId,
278
+ agentId: agentId,
279
+ names: [mockCharacter.name],
280
+ },
281
+ ]);
282
+ (mockDatabaseAdapter.getRoomsByIds as any).mockResolvedValue([]);
283
+ (mockDatabaseAdapter.getParticipantsForRoom as any).mockResolvedValue([]);
284
+
285
+ await runtime.initialize(); // Initialize to process registrations
286
+
287
+ expect(runtime.actions.some((a) => a.name === 'TestAction')).toBe(true);
288
+ expect(runtime.providers.some((p) => p.name === 'TestProvider')).toBe(true);
289
+ expect(runtime.models.has(ModelType.TEXT_SMALL)).toBe(true);
290
+ ensureAgentExistsSpy.mockRestore();
291
+ });
292
+ });
293
+
294
+ describe('Initialization', () => {
295
+ let ensureAgentExistsSpy: any;
296
+ beforeEach(() => {
297
+ // Mock adapter calls needed for a successful initialize
298
+ ensureAgentExistsSpy = spyOn(AgentRuntime.prototype, 'ensureAgentExists').mockResolvedValue({
299
+ ...mockCharacter,
300
+ id: agentId, // ensureAgentExists should return the agent
301
+ createdAt: Date.now(),
302
+ updatedAt: Date.now(),
303
+ enabled: true,
304
+ });
305
+ (mockDatabaseAdapter.getEntityByIds as any).mockResolvedValue([
306
+ {
307
+ id: agentId,
308
+ agentId: agentId,
309
+ names: [mockCharacter.name],
310
+ },
311
+ ]);
312
+ (mockDatabaseAdapter.getEntitiesByIds as any).mockResolvedValue([
313
+ {
314
+ id: agentId,
315
+ agentId: agentId,
316
+ names: [mockCharacter.name],
317
+ },
318
+ ]);
319
+ (mockDatabaseAdapter.getRoomsByIds as any).mockResolvedValue([]);
320
+ (mockDatabaseAdapter.getParticipantsForRoom as any).mockResolvedValue([]);
321
+ // mockDatabaseAdapter.getAgent is NOT called by initialize anymore after ensureAgentExists returns the agent
322
+ });
323
+
324
+ afterEach(() => {
325
+ ensureAgentExistsSpy.mockRestore();
326
+ });
327
+
328
+ it('should call adapter.init and core setup methods for an existing agent', async () => {
329
+ await runtime.initialize();
330
+
331
+ expect(mockDatabaseAdapter.init).toHaveBeenCalledTimes(1);
332
+ expect(runtime.ensureAgentExists).toHaveBeenCalledWith(mockCharacter);
333
+ // expect(mockDatabaseAdapter.getAgent).toHaveBeenCalledWith(agentId); // This is no longer called
334
+ expect(mockDatabaseAdapter.getEntitiesByIds).toHaveBeenCalledWith([agentId]);
335
+ expect(mockDatabaseAdapter.getRoomsByIds).toHaveBeenCalledWith([agentId]);
336
+ expect(mockDatabaseAdapter.createRooms).toHaveBeenCalled();
337
+ expect(mockDatabaseAdapter.addParticipantsRoom).toHaveBeenCalledWith([agentId], agentId);
338
+ });
339
+
340
+ it('should create a new agent if one does not exist', async () => {
341
+ // No need to override the spy, initialize should handle it.
342
+ await runtime.initialize();
343
+
344
+ expect(mockDatabaseAdapter.init).toHaveBeenCalledTimes(1);
345
+ expect(runtime.ensureAgentExists).toHaveBeenCalledWith(mockCharacter);
346
+ expect(mockDatabaseAdapter.getEntitiesByIds).toHaveBeenCalledWith([agentId]);
347
+ expect(mockDatabaseAdapter.getRoomsByIds).toHaveBeenCalledWith([agentId]);
348
+ expect(mockDatabaseAdapter.createRooms).toHaveBeenCalled();
349
+ expect(mockDatabaseAdapter.addParticipantsRoom).toHaveBeenCalledWith([agentId], agentId);
350
+ });
351
+
352
+ it('should throw if adapter is not available during initialize', async () => {
353
+ // Create runtime without passing adapter
354
+ const runtimeWithoutAdapter = new AgentRuntime({
355
+ character: mockCharacter,
356
+ agentId: agentId,
357
+ });
358
+ await expect(runtimeWithoutAdapter.initialize()).rejects.toThrow(
359
+ /Database adapter not initialized/
360
+ );
361
+ });
362
+
363
+ // Add more tests for initialize: existing entity, existing room, knowledge processing etc.
364
+ });
365
+
366
+ describe('State Composition', () => {
367
+ it('should call provider get methods', async () => {
368
+ const provider1Get = mock().mockResolvedValue({ text: 'p1_text', values: { p1_val: 1 } });
369
+ const provider2Get = mock().mockResolvedValue({ text: 'p2_text', values: { p2_val: 2 } });
370
+ const provider1: Provider = { name: 'P1', get: provider1Get };
371
+ const provider2: Provider = { name: 'P2', get: provider2Get };
372
+
373
+ runtime.registerProvider(provider1);
374
+ runtime.registerProvider(provider2);
375
+
376
+ const message = createMockMemory('test message', undefined, undefined, undefined, agentId);
377
+ const state = await runtime.composeState(message);
378
+
379
+ expect(provider1Get).toHaveBeenCalledTimes(1);
380
+ // The cached state passed will be the initial empty-ish one
381
+ expect(provider1Get).toHaveBeenCalledWith(runtime, message, {
382
+ values: {},
383
+ data: {},
384
+ text: '',
385
+ });
386
+ expect(provider2Get).toHaveBeenCalledTimes(1);
387
+ expect(provider2Get).toHaveBeenCalledWith(runtime, message, {
388
+ values: {},
389
+ data: {},
390
+ text: '',
391
+ });
392
+ expect(state.text).toContain('p1_text');
393
+ expect(state.text).toContain('p2_text');
394
+ expect(state.values).toHaveProperty('p1_val', 1);
395
+ expect(state.values).toHaveProperty('p2_val', 2);
396
+ // Check combined values includes provider outputs
397
+ expect(state.values).toHaveProperty('providers'); // Check if the combined text is stored
398
+ expect(state.data.providers.P1.values).toEqual({ p1_val: 1 }); // Check provider data cache
399
+ expect(state.data.providers.P2.values).toEqual({ p2_val: 2 });
400
+ });
401
+
402
+ it('should filter providers', async () => {
403
+ const provider1Get = mock().mockResolvedValue({ text: 'p1_text' });
404
+ const provider2Get = mock().mockResolvedValue({ text: 'p2_text' });
405
+ const provider1: Provider = { name: 'P1', get: provider1Get };
406
+ const provider2: Provider = { name: 'P2', get: provider2Get };
407
+
408
+ runtime.registerProvider(provider1);
409
+ runtime.registerProvider(provider2);
410
+
411
+ const message = createMockMemory('test message', undefined, undefined, undefined, agentId);
412
+ const state = await runtime.composeState(message, ['P1'], true); // Filter to only P1
413
+
414
+ expect(provider1Get).toHaveBeenCalledTimes(1);
415
+ expect(provider2Get).not.toHaveBeenCalled();
416
+ expect(state.text).toBe('p1_text');
417
+ });
418
+
419
+ // Add tests for includeList, caching behavior
420
+ });
421
+
422
+ describe('Model Usage', () => {
423
+ it('should call registered model handler', async () => {
424
+ const modelHandler = mock().mockResolvedValue('success');
425
+ const modelType = ModelType.TEXT_LARGE;
426
+
427
+ runtime.registerModel(modelType, modelHandler, 'test-provider');
428
+
429
+ const params = { prompt: 'test prompt', someOption: true };
430
+ const result = await runtime.useModel(modelType, params);
431
+
432
+ expect(modelHandler).toHaveBeenCalledTimes(1);
433
+ // Check that handler was called with runtime and merged params
434
+ expect(modelHandler).toHaveBeenCalledWith(
435
+ runtime,
436
+ expect.objectContaining({ ...params, runtime: runtime })
437
+ );
438
+ expect(result).toEqual('success');
439
+ // Check if log was called (part of useModel logic)
440
+ expect(mockDatabaseAdapter.log).toHaveBeenCalledWith(
441
+ expect.objectContaining({ type: `useModel:${modelType}` })
442
+ );
443
+ });
444
+
445
+ it('should throw if model type is not registered', async () => {
446
+ const modelType = 'UNREGISTERED_MODEL' as ModelTypeName;
447
+ const params = { prompt: 'test' };
448
+ await expect(runtime.useModel(modelType, params)).rejects.toThrow(/No handler found/);
449
+ });
450
+ });
451
+
452
+ describe('Action Processing', () => {
453
+ let mockActionHandler: any; // Use 'any' or specific function type
454
+ let testAction: Action;
455
+ let message: Memory;
456
+ let responseMemory: Memory;
457
+
458
+ beforeEach(() => {
459
+ mockActionHandler = mock().mockResolvedValue(undefined);
460
+ testAction = createMockAction('TestAction');
461
+ testAction.handler = mockActionHandler; // Assign mock handler
462
+
463
+ runtime.registerAction(testAction);
464
+
465
+ message = createMockMemory('user message', undefined, undefined, undefined, agentId);
466
+ responseMemory = createMockMemory(
467
+ 'agent response',
468
+ undefined,
469
+ undefined,
470
+ message.roomId,
471
+ agentId
472
+ ); // Same room
473
+ responseMemory.content.actions = ['TestAction']; // Specify action to run
474
+
475
+ // Mock composeState as it's called within processActions
476
+ spyOn(runtime, 'composeState').mockResolvedValue(createMockState('composed state text'));
477
+ });
478
+
479
+ it('should find and execute the correct action handler', async () => {
480
+ await runtime.processActions(message, [responseMemory]);
481
+
482
+ expect(runtime.composeState).toHaveBeenCalled(); // Verify state was composed
483
+ expect(mockActionHandler).toHaveBeenCalledTimes(1);
484
+ // Check arguments passed to the handler
485
+ expect(mockActionHandler).toHaveBeenCalledWith(
486
+ runtime,
487
+ message,
488
+ expect.objectContaining({ text: 'composed state text' }), // Check composed state
489
+ expect.objectContaining({
490
+ context: expect.objectContaining({
491
+ previousResults: expect.any(Array),
492
+ getPreviousResult: expect.any(Function),
493
+ }),
494
+ }), // options now contains context
495
+ undefined, // callback
496
+ [responseMemory] // responses array
497
+ );
498
+ expect(mockDatabaseAdapter.log).toHaveBeenCalledWith(
499
+ expect.objectContaining({
500
+ type: 'action',
501
+ body: expect.objectContaining({ action: 'TestAction' }),
502
+ })
503
+ );
504
+ });
505
+
506
+ // Add tests for action not found, simile matching, handler errors
507
+ it('should not execute if no action name matches', async () => {
508
+ responseMemory.content.actions = ['NonExistentAction'];
509
+ await runtime.processActions(message, [responseMemory]);
510
+ expect(mockActionHandler).not.toHaveBeenCalled();
511
+ expect(mockDatabaseAdapter.log).not.toHaveBeenCalledWith(
512
+ expect.objectContaining({ type: 'action' })
513
+ );
514
+ });
515
+
516
+ it('should prioritize exact action name matches over fuzzy matches', async () => {
517
+ // Create two actions where one name is a substring of another
518
+ const replyHandler = mock().mockResolvedValue(undefined);
519
+ const replyWithImageHandler = mock().mockResolvedValue(undefined);
520
+
521
+ const replyAction: Action = {
522
+ name: 'REPLY',
523
+ description: 'Simple reply action',
524
+ similes: [],
525
+ examples: [],
526
+ handler: replyHandler,
527
+ validate: mock().mockImplementation(async () => true),
528
+ };
529
+
530
+ const replyWithImageAction: Action = {
531
+ name: 'REPLY_WITH_IMAGE',
532
+ description: 'Reply with image action',
533
+ similes: [],
534
+ examples: [],
535
+ handler: replyWithImageHandler,
536
+ validate: mock().mockImplementation(async () => true),
537
+ };
538
+
539
+ // Register both actions
540
+ runtime.registerAction(replyAction);
541
+ runtime.registerAction(replyWithImageAction);
542
+
543
+ // Test 1: When asking for 'REPLY', it should match REPLY exactly, not REPLY_WITH_IMAGE
544
+ responseMemory.content.actions = ['REPLY'];
545
+ await runtime.processActions(message, [responseMemory]);
546
+
547
+ expect(replyHandler).toHaveBeenCalledTimes(1);
548
+ expect(replyWithImageHandler).not.toHaveBeenCalled();
549
+
550
+ // Reset mocks
551
+ replyHandler.mockClear();
552
+ replyWithImageHandler.mockClear();
553
+
554
+ // Test 2: When asking for 'REPLY_WITH_IMAGE', it should match REPLY_WITH_IMAGE exactly
555
+ responseMemory.content.actions = ['REPLY_WITH_IMAGE'];
556
+ await runtime.processActions(message, [responseMemory]);
557
+
558
+ expect(replyWithImageHandler).toHaveBeenCalledTimes(1);
559
+ expect(replyHandler).not.toHaveBeenCalled();
560
+ });
561
+ });
562
+
563
+ // --- Adapter Passthrough Tests ---
564
+ describe('Adapter Passthrough', () => {
565
+ it('createEntity should call adapter.createEntities', async () => {
566
+ const entityData = { id: stringToUuid(uuidv4()), agentId: agentId, names: ['Test Entity'] };
567
+ await runtime.createEntity(entityData);
568
+ expect(mockDatabaseAdapter.createEntities).toHaveBeenCalledTimes(1);
569
+ expect(mockDatabaseAdapter.createEntities).toHaveBeenCalledWith([entityData]);
570
+ });
571
+
572
+ it('getMemoryById should call adapter.getMemoryById', async () => {
573
+ const memoryId = stringToUuid(uuidv4());
574
+ await runtime.getMemoryById(memoryId);
575
+ expect(mockDatabaseAdapter.getMemoryById).toHaveBeenCalledTimes(1);
576
+ expect(mockDatabaseAdapter.getMemoryById).toHaveBeenCalledWith(memoryId);
577
+ });
578
+ // Add more tests for other adapter methods if full coverage is desired
579
+ });
580
+
581
+ // --- Event Emitter Tests ---
582
+ describe('Event Emitter (on/emit/off)', () => {
583
+ it('should register and emit events', () => {
584
+ const handler = mock();
585
+ const eventName = 'testEvent';
586
+ const eventData = { info: 'data' };
587
+
588
+ runtime.on(eventName, handler);
589
+ runtime.emit(eventName, eventData);
590
+
591
+ expect(handler).toHaveBeenCalledTimes(1);
592
+ expect(handler).toHaveBeenCalledWith(eventData);
593
+ });
594
+
595
+ it('should remove event handler with off', () => {
596
+ const handler = mock();
597
+ const eventName = 'testEvent';
598
+
599
+ runtime.on(eventName, handler);
600
+ runtime.off(eventName, handler);
601
+ runtime.emit(eventName, { info: 'data' });
602
+
603
+ expect(handler).not.toHaveBeenCalled();
604
+ });
605
+ });
606
+
607
+ // --- Tests from original suite ---
608
+ describe('Original Suite Tests', () => {
609
+ // Note: These might need slight adaptation if they relied on Jest specifics
610
+ // or different mock setups.
611
+
612
+ // Copied from your original suite:
613
+ describe('model provider management', () => {
614
+ it('should provide access to the configured model provider', () => {
615
+ // In this refactored structure, 'provider' likely refers to the runtime instance itself
616
+ // which acts as the primary interface.
617
+ const provider = runtime; // The runtime instance manages models
618
+ expect(provider).toBeDefined();
619
+ // You might add more specific checks here, e.g., ensuring getModel exists
620
+ expect(runtime.getModel).toBeInstanceOf(Function);
621
+ });
622
+ });
623
+
624
+ // Copied from your original suite:
625
+ describe('state management', () => {
626
+ it('should compose state with additional keys', async () => {
627
+ // Use the helper function for consistency
628
+ const message: Memory = createMockMemory(
629
+ 'test message',
630
+ stringToUuid('11111111-e89b-12d3-a456-426614174003'), // Use valid UUIDs
631
+ stringToUuid('22222222-e89b-12d3-a456-426614174004'),
632
+ stringToUuid('33333333-e89b-12d3-a456-426614174003'), // Room ID
633
+ agentId
634
+ );
635
+
636
+ // Mock provider needed by composeState
637
+ const providerGet = mock().mockResolvedValue({ text: 'provider text' });
638
+ runtime.registerProvider({ name: 'TestProvider', get: providerGet });
639
+
640
+ const state = await runtime.composeState(message);
641
+ expect(state).toHaveProperty('values');
642
+ expect(state).toHaveProperty('text');
643
+ expect(state).toHaveProperty('data');
644
+ // Add more specific state checks if needed
645
+ expect(state.text).toContain('provider text'); // Check provider text is included
646
+ });
647
+ });
648
+
649
+ // Copied from your original suite:
650
+ describe('action management', () => {
651
+ it('should register an action', () => {
652
+ const action = createMockAction('testAction');
653
+ runtime.registerAction(action);
654
+ expect(runtime.actions).toContain(action);
655
+ });
656
+
657
+ it('should allow registering multiple actions', () => {
658
+ const action1 = createMockAction('testAction1');
659
+ const action2 = createMockAction('testAction2');
660
+ runtime.registerAction(action1);
661
+ runtime.registerAction(action2);
662
+ expect(runtime.actions).toContain(action1);
663
+ expect(runtime.actions).toContain(action2);
664
+ });
665
+ });
666
+
667
+ describe('model settings from character configuration', () => {
668
+ it('should apply character model settings as defaults and allow overrides', async () => {
669
+ // Create character with model settings
670
+ const characterWithSettings: Character = {
671
+ ...mockCharacter,
672
+ settings: {
673
+ MODEL_MAX_TOKEN: 4096,
674
+ MODEL_TEMPERATURE: 0.5,
675
+ MODEL_FREQ_PENALTY: 0.8,
676
+ MODEL_PRESENCE_PENALTY: 0.9,
677
+ // Test invalid values that should be ignored
678
+ MODEL_INVALID: 'not-a-number',
679
+ },
680
+ };
681
+
682
+ // Create runtime with character settings
683
+ const runtimeWithSettings = new AgentRuntime({
684
+ character: characterWithSettings,
685
+ adapter: mockDatabaseAdapter,
686
+ });
687
+
688
+ // Mock a model handler to capture params
689
+ let capturedParams: any = null;
690
+ const mockHandler = mock().mockImplementation(async (_runtime: any, params: any) => {
691
+ capturedParams = params;
692
+ return 'test response';
693
+ });
694
+
695
+ // Register the mock model
696
+ runtimeWithSettings.registerModel(ModelType.TEXT_SMALL, mockHandler, 'test-provider');
697
+
698
+ // Test 1: Model settings are applied as defaults
699
+ await runtimeWithSettings.useModel(ModelType.TEXT_SMALL, {
700
+ prompt: 'test prompt',
701
+ });
702
+
703
+ expect(capturedParams.maxTokens).toBe(4096);
704
+ expect(capturedParams.temperature).toBe(0.5);
705
+ expect(capturedParams.frequencyPenalty).toBe(0.8);
706
+ expect(capturedParams.presencePenalty).toBe(0.9);
707
+ expect(capturedParams.prompt).toBe('test prompt');
708
+
709
+ // Test 2: Explicit parameters override character defaults
710
+ await runtimeWithSettings.useModel(ModelType.TEXT_SMALL, {
711
+ prompt: 'test prompt 2',
712
+ temperature: 0.2,
713
+ maxTokens: 2048,
714
+ });
715
+
716
+ expect(capturedParams.temperature).toBe(0.2);
717
+ expect(capturedParams.maxTokens).toBe(2048);
718
+ expect(capturedParams.frequencyPenalty).toBe(0.8); // Still from character
719
+ expect(capturedParams.presencePenalty).toBe(0.9); // Still from character
720
+
721
+ // Test 3: No settings configured - use only provided params
722
+ const characterNoSettings: Character = {
723
+ ...mockCharacter,
724
+ name: 'TestAgentNoSettings',
725
+ };
726
+ const runtimeNoSettings = new AgentRuntime({
727
+ character: characterNoSettings,
728
+ adapter: mockDatabaseAdapter,
729
+ });
730
+
731
+ // Use same mockHandler for the second test
732
+ mockHandler.mockClear();
733
+ runtimeNoSettings.registerModel(ModelType.TEXT_SMALL, mockHandler, 'test-provider');
734
+
735
+ await runtimeNoSettings.useModel(ModelType.TEXT_SMALL, {
736
+ prompt: 'test prompt 3',
737
+ temperature: 0.7,
738
+ });
739
+
740
+ expect(capturedParams.temperature).toBe(0.7);
741
+ expect(capturedParams.maxTokens).toBeUndefined();
742
+ expect(capturedParams.frequencyPenalty).toBeUndefined();
743
+ expect(capturedParams.presencePenalty).toBeUndefined();
744
+ });
745
+ });
746
+
747
+ describe('model settings from character configuration', () => {
748
+ it('should support per-model-type configuration with proper fallback chain', async () => {
749
+ // Create character with mixed settings: defaults, model-specific, and legacy
750
+ const characterWithMixedSettings: Character = {
751
+ ...mockCharacter,
752
+ settings: {
753
+ // Default settings (apply to all models)
754
+ DEFAULT_TEMPERATURE: 0.7,
755
+ DEFAULT_MAX_TOKENS: 2048,
756
+
757
+ // Model-specific settings (override defaults)
758
+ TEXT_SMALL_TEMPERATURE: 0.5,
759
+ TEXT_SMALL_MAX_TOKENS: 1024,
760
+ TEXT_LARGE_TEMPERATURE: 0.8,
761
+ TEXT_LARGE_FREQUENCY_PENALTY: 0.5,
762
+ OBJECT_SMALL_TEMPERATURE: 0.3,
763
+ OBJECT_LARGE_PRESENCE_PENALTY: 0.6,
764
+
765
+ // Legacy settings (should be lowest priority)
766
+ MODEL_TEMPERATURE: 0.9,
767
+ MODEL_MAX_TOKEN: 4096,
768
+ MODEL_FREQ_PENALTY: 0.7,
769
+ MODEL_PRESENCE_PENALTY: 0.8,
770
+ },
771
+ };
772
+
773
+ const runtimeWithMixedSettings = new AgentRuntime({
774
+ character: characterWithMixedSettings,
775
+ adapter: mockDatabaseAdapter,
776
+ });
777
+
778
+ // Mock handlers to capture params
779
+ let capturedTextSmall: any = null;
780
+ let capturedTextLarge: any = null;
781
+ let capturedObjectSmall: any = null;
782
+ let capturedObjectLarge: any = null;
783
+
784
+ const mockTextSmallHandler = mock().mockImplementation(
785
+ async (_runtime: any, params: any) => {
786
+ capturedTextSmall = params;
787
+ return 'text small response';
788
+ }
789
+ );
790
+ const mockTextLargeHandler = mock().mockImplementation(
791
+ async (_runtime: any, params: any) => {
792
+ capturedTextLarge = params;
793
+ return 'text large response';
794
+ }
795
+ );
796
+ const mockObjectSmallHandler = mock().mockImplementation(
797
+ async (_runtime: any, params: any) => {
798
+ capturedObjectSmall = params;
799
+ return { type: 'small' };
800
+ }
801
+ );
802
+ const mockObjectLargeHandler = mock().mockImplementation(
803
+ async (_runtime: any, params: any) => {
804
+ capturedObjectLarge = params;
805
+ return { type: 'large' };
806
+ }
807
+ );
808
+
809
+ // Register all models
810
+ runtimeWithMixedSettings.registerModel(
811
+ ModelType.TEXT_SMALL,
812
+ mockTextSmallHandler,
813
+ 'test-provider'
814
+ );
815
+ runtimeWithMixedSettings.registerModel(
816
+ ModelType.TEXT_LARGE,
817
+ mockTextLargeHandler,
818
+ 'test-provider'
819
+ );
820
+ runtimeWithMixedSettings.registerModel(
821
+ ModelType.OBJECT_SMALL,
822
+ mockObjectSmallHandler,
823
+ 'test-provider'
824
+ );
825
+ runtimeWithMixedSettings.registerModel(
826
+ ModelType.OBJECT_LARGE,
827
+ mockObjectLargeHandler,
828
+ 'test-provider'
829
+ );
830
+
831
+ // Test 1: TEXT_SMALL - should use model-specific settings, fall back to defaults/legacy
832
+ await runtimeWithMixedSettings.useModel(ModelType.TEXT_SMALL, {
833
+ prompt: 'test text small',
834
+ });
835
+
836
+ expect(capturedTextSmall.temperature).toBe(0.5); // Model-specific
837
+ expect(capturedTextSmall.maxTokens).toBe(1024); // Model-specific
838
+ expect(capturedTextSmall.frequencyPenalty).toBe(0.7); // Legacy fallback
839
+ expect(capturedTextSmall.presencePenalty).toBe(0.8); // Legacy fallback
840
+
841
+ // Test 2: TEXT_LARGE - mixed model-specific and defaults
842
+ await runtimeWithMixedSettings.useModel(ModelType.TEXT_LARGE, {
843
+ prompt: 'test text large',
844
+ });
845
+
846
+ expect(capturedTextLarge.temperature).toBe(0.8); // Model-specific
847
+ expect(capturedTextLarge.maxTokens).toBe(2048); // Default fallback
848
+ expect(capturedTextLarge.frequencyPenalty).toBe(0.5); // Model-specific
849
+ expect(capturedTextLarge.presencePenalty).toBe(0.8); // Legacy fallback
850
+
851
+ // Test 3: OBJECT_SMALL - some model-specific, rest from defaults/legacy
852
+ await runtimeWithMixedSettings.useModel(ModelType.OBJECT_SMALL, {
853
+ prompt: 'test object small',
854
+ });
855
+
856
+ expect(capturedObjectSmall.temperature).toBe(0.3); // Model-specific
857
+ expect(capturedObjectSmall.maxTokens).toBe(2048); // Default fallback
858
+ expect(capturedObjectSmall.frequencyPenalty).toBe(0.7); // Legacy fallback
859
+ expect(capturedObjectSmall.presencePenalty).toBe(0.8); // Legacy fallback
860
+
861
+ // Test 4: OBJECT_LARGE - minimal model-specific settings
862
+ await runtimeWithMixedSettings.useModel(ModelType.OBJECT_LARGE, {
863
+ prompt: 'test object large',
864
+ });
865
+
866
+ expect(capturedObjectLarge.temperature).toBe(0.7); // Default fallback
867
+ expect(capturedObjectLarge.maxTokens).toBe(2048); // Default fallback
868
+ expect(capturedObjectLarge.frequencyPenalty).toBe(0.7); // Legacy fallback
869
+ expect(capturedObjectLarge.presencePenalty).toBe(0.6); // Model-specific
870
+ });
871
+
872
+ it('should allow direct params to override all configuration levels', async () => {
873
+ const characterWithAllSettings: Character = {
874
+ ...mockCharacter,
875
+ settings: {
876
+ // All levels of configuration
877
+ DEFAULT_TEMPERATURE: 0.7,
878
+ TEXT_SMALL_TEMPERATURE: 0.5,
879
+ MODEL_TEMPERATURE: 0.9,
880
+ },
881
+ };
882
+
883
+ const runtime = new AgentRuntime({
884
+ character: characterWithAllSettings,
885
+ adapter: mockDatabaseAdapter,
886
+ });
887
+
888
+ let capturedParams: any = null;
889
+ const mockHandler = mock().mockImplementation(async (_runtime: any, params: any) => {
890
+ capturedParams = params;
891
+ return 'response';
892
+ });
893
+
894
+ runtime.registerModel(ModelType.TEXT_SMALL, mockHandler, 'test-provider');
895
+
896
+ // Direct params should override everything
897
+ await runtime.useModel(ModelType.TEXT_SMALL, {
898
+ prompt: 'test',
899
+ temperature: 0.1, // This should win
900
+ });
901
+
902
+ expect(capturedParams.temperature).toBe(0.1);
903
+ });
904
+
905
+ it('should handle models without specific configuration support', async () => {
906
+ const characterWithSettings: Character = {
907
+ ...mockCharacter,
908
+ settings: {
909
+ DEFAULT_TEMPERATURE: 0.7,
910
+ TEXT_SMALL_TEMPERATURE: 0.5,
911
+ // No specific settings for TEXT_REASONING_SMALL
912
+ },
913
+ };
914
+
915
+ const runtime = new AgentRuntime({
916
+ character: characterWithSettings,
917
+ adapter: mockDatabaseAdapter,
918
+ });
919
+
920
+ let capturedParams: any = null;
921
+ const mockHandler = mock().mockImplementation(async (_runtime: any, params: any) => {
922
+ capturedParams = params;
923
+ return 'response';
924
+ });
925
+
926
+ // Register a model type that doesn't have specific configuration support
927
+ runtime.registerModel(ModelType.TEXT_REASONING_SMALL, mockHandler, 'test-provider');
928
+
929
+ await runtime.useModel(ModelType.TEXT_REASONING_SMALL, {
930
+ prompt: 'test reasoning',
931
+ });
932
+
933
+ // Should fall back to default settings
934
+ expect(capturedParams.temperature).toBe(0.7);
935
+ expect(capturedParams.maxTokens).toBeUndefined(); // No default for this
936
+ });
937
+
938
+ it('should validate and ignore invalid numeric values at all configuration levels', async () => {
939
+ const characterWithInvalidSettings: Character = {
940
+ ...mockCharacter,
941
+ settings: {
942
+ // Mix of valid and invalid values at different levels
943
+ DEFAULT_TEMPERATURE: 'not-a-number',
944
+ DEFAULT_MAX_TOKENS: 2048,
945
+ TEXT_SMALL_TEMPERATURE: 0.5,
946
+ TEXT_SMALL_MAX_TOKENS: 'invalid',
947
+ TEXT_SMALL_FREQUENCY_PENALTY: null,
948
+ MODEL_TEMPERATURE: undefined,
949
+ MODEL_PRESENCE_PENALTY: 0.8,
950
+ },
951
+ };
952
+
953
+ const runtime = new AgentRuntime({
954
+ character: characterWithInvalidSettings,
955
+ adapter: mockDatabaseAdapter,
956
+ });
957
+
958
+ let capturedParams: any = null;
959
+ const mockHandler = mock().mockImplementation(async (_runtime: any, params: any) => {
960
+ capturedParams = params;
961
+ return 'response';
962
+ });
963
+
964
+ runtime.registerModel(ModelType.TEXT_SMALL, mockHandler, 'test-provider');
965
+
966
+ await runtime.useModel(ModelType.TEXT_SMALL, {
967
+ prompt: 'test invalid',
968
+ });
969
+
970
+ // Valid values should be used, invalid ones ignored
971
+ expect(capturedParams.temperature).toBe(0.5); // Valid model-specific
972
+ expect(capturedParams.maxTokens).toBe(2048); // Valid default (model-specific was invalid)
973
+ expect(capturedParams.frequencyPenalty).toBeUndefined(); // All invalid
974
+ expect(capturedParams.presencePenalty).toBe(0.8); // Valid legacy
975
+ });
976
+ });
977
+ });
978
+ }); // End of main describe block