Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@elizaos/plugin-knowledge",
"description": "Plugin for Knowledge",
"version": "1.2.1",
"version": "1.2.2",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export const processKnowledgeAction: Action = {
await callback(response);
}
} catch (error) {
logger.error('Error in PROCESS_KNOWLEDGE action:', error);
logger.error({ error }, 'Error in PROCESS_KNOWLEDGE action');

const errorResponse: Content = {
text: `I encountered an error while processing the knowledge: ${error instanceof Error ? error.message : 'Unknown error'}`,
Expand Down Expand Up @@ -327,7 +327,7 @@ export const searchKnowledgeAction: Action = {
await callback(response);
}
} catch (error) {
logger.error('Error in SEARCH_KNOWLEDGE action:', error);
logger.error({ error }, 'Error in SEARCH_KNOWLEDGE action');

const errorResponse: Content = {
text: `I encountered an error while searching the knowledge base: ${error instanceof Error ? error.message : 'Unknown error'}`,
Expand Down
7 changes: 4 additions & 3 deletions src/docs-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { isBinaryContentType } from './utils.ts';
*/
export function getKnowledgePath(runtimePath?: string): string {
// Priority: runtime setting > environment variable > default
const knowledgePath = runtimePath || process.env.KNOWLEDGE_PATH || path.join(process.cwd(), 'docs');
const knowledgePath =
runtimePath || process.env.KNOWLEDGE_PATH || path.join(process.cwd(), 'docs');
const resolvedPath = path.resolve(knowledgePath);

if (!fs.existsSync(resolvedPath)) {
Expand Down Expand Up @@ -107,7 +108,7 @@ export async function loadDocsFromPath(
logger.info(`✅ "${fileName}": ${result.fragmentCount} fragments created`);
successful++;
} catch (error) {
logger.error(`Failed to process file ${filePath}:`, error);
logger.error({ error }, `Failed to process file ${filePath}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Structured error logging adopted here

Good switch to structured error logging. Confirm that the logger recognizes the "error" key for proper stack capture (some loggers expect an Error or { err }).

Run this quick check to spot non-structured usages still present and ensure consistency:


🏁 Script executed:

#!/bin/bash
# Count structured vs. likely unstructured logger.error calls
echo "Structured (object with error):"
rg -n "logger\.error\(\s*\{\s*error\s*:" -g "src/**" || true
rg -n "logger\.error\(\s*\{\s*error\s*\}" -g "src/**" || true

echo -e "\nPossibly unstructured (message, errorVar):"
rg -n "logger\.error\([^)]*`[^`]*`[^)]*,\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\)" -g "src/**" || true
rg -n "logger\.error\([^)]*'[^']*'[^)]*,\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\)" -g "src/**" || true

Length of output: 2720


🏁 Script executed:

#!/bin/bash
# List all logger.error calls to establish baseline
echo "All logger.error calls:"
rg -n "logger\.error" -g "src/**" || true

# Check for unstructured usage: logger.error('message', errorVar) or logger.error("message", errorVar)
echo -e "\nPotential unstructured logger.error calls (single/double quotes):"
rg -n "logger\.error\(\s*['\"].*['\"],\s*[a-zA-Z_]" -g "src/**" || true

# Check for unstructured usage with template literals: logger.error(`message`, errorVar)
echo -e "\nPotential unstructured logger.error calls (backticks):"
rg -n "logger\.error\(\s*\`.*\`,\s*[a-zA-Z_]" -g "src/**" || true

Length of output: 4824


Ensure consistent structured error logging

We still have a few logger.error calls using the old (message, errorVar) signature, which won’t capture stacks under our new structured‐logging setup. Please convert these to pass the error object under an error key:

• src/routes.ts:324

- logger.error(`[Document Processor] ❌ Error processing URL ${fileUrl}:`, urlError);
+ logger.error({ error: urlError }, `[Document Processor] ❌ Error processing URL ${fileUrl}:`);

• src/utils.ts:172

- logger.error(`[PdfService] Error converting PDF ${docName}:`, error.message);
+ logger.error({ error }, `[PdfService] Error converting PDF ${docName}:`);

After updating these, run a global check (e.g. rg "logger\\.error" -g "src/**" | grep -v "{ error") to catch any remaining unstructured calls. Also verify your logger implementation picks up the error key for stack traces.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/docs-loader.ts at line 111, the logger.error call uses the old signature
that does not pass the error object under an error key, which prevents capturing
stack traces properly. Update this call to use structured logging by passing the
error object inside an object with the key error, like logger.error({ error },
`Failed to process file ${filePath}`). After fixing this, review the other
specified files src/routes.ts line 324 and src/utils.ts line 172 for similar
issues and update them accordingly. Finally, run a global search to find any
remaining unstructured logger.error calls and ensure your logger implementation
correctly handles the error key for stack traces.

failed++;
}
}
Expand Down Expand Up @@ -143,7 +144,7 @@ function getAllFiles(dirPath: string, files: string[] = []): string[] {
}
}
} catch (error) {
logger.error(`Error reading directory ${dirPath}:`, error);
logger.error({ error }, `Error reading directory ${dirPath}`);
}

return files;
Expand Down
6 changes: 3 additions & 3 deletions src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function generateTextEmbedding(

throw new Error(`Unsupported embedding provider: ${config.EMBEDDING_PROVIDER}`);
} catch (error) {
logger.error(`[Document Processor] ${config.EMBEDDING_PROVIDER} embedding error:`, error);
logger.error({ error }, `[Document Processor] ${config.EMBEDDING_PROVIDER} embedding error`);
throw error;
}
}
Expand Down Expand Up @@ -81,7 +81,7 @@ export async function generateTextEmbeddingsBatch(
index: globalIndex,
};
} catch (error) {
logger.error(`[Document Processor] Embedding error for item ${globalIndex}:`, error);
logger.error({ error }, `[Document Processor] Embedding error for item ${globalIndex}`);
return {
embedding: null,
success: false,
Expand Down Expand Up @@ -245,7 +245,7 @@ export async function generateText(
throw new Error(`Unsupported text provider: ${provider}`);
}
} catch (error) {
logger.error(`[Document Processor] ${provider} ${modelName} error:`, error);
logger.error({ error }, `[Document Processor] ${provider} ${modelName} error`);
throw error;
}
}
Expand Down
22 changes: 11 additions & 11 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const cleanupFile = (filePath: string) => {
try {
fs.unlinkSync(filePath);
} catch (error) {
logger.error(`Error cleaning up file ${filePath}:`, error);
logger.error({ error }, `Error cleaning up file ${filePath}`);
}
}
};
Expand Down Expand Up @@ -334,7 +334,7 @@ async function uploadKnowledgeHandler(req: any, res: any, runtime: IAgentRuntime
sendSuccess(res, results);
}
} catch (error: any) {
logger.error('[Document Processor] ❌ Error processing knowledge:', error);
logger.error({ error }, '[Document Processor] ❌ Error processing knowledge');
if (hasUploadedFiles) {
cleanupFiles(req.files as MulterFile[]);
}
Expand Down Expand Up @@ -411,7 +411,7 @@ async function getKnowledgeDocumentsHandler(req: any, res: any, runtime: IAgentR
totalRequested: fileUrls ? fileUrls.length : 0,
});
} catch (error: any) {
logger.error('[Document Processor] ❌ Error retrieving documents:', error);
logger.error({ error }, '[Document Processor] ❌ Error retrieving documents');
sendError(res, 500, 'RETRIEVAL_ERROR', 'Failed to retrieve documents', error.message);
}
}
Expand Down Expand Up @@ -446,7 +446,7 @@ async function deleteKnowledgeDocumentHandler(req: any, res: any, runtime: IAgen
logger.info(`[Document Processor] ✅ Successfully deleted document: ${typedKnowledgeId}`);
sendSuccess(res, null, 204);
} catch (error: any) {
logger.error(`[Document Processor] ❌ Error deleting document ${knowledgeId}:`, error);
logger.error({ error }, `[Document Processor] ❌ Error deleting document ${knowledgeId}`);
sendError(res, 500, 'DELETE_ERROR', 'Failed to delete document', error.message);
}
}
Expand Down Expand Up @@ -502,7 +502,7 @@ async function getKnowledgeByIdHandler(req: any, res: any, runtime: IAgentRuntim

sendSuccess(res, { document: cleanDocument });
} catch (error: any) {
logger.error(`[Document Processor] ❌ Error retrieving document ${knowledgeId}:`, error);
logger.error({ error }, `[Document Processor] ❌ Error retrieving document ${knowledgeId}`);
sendError(res, 500, 'RETRIEVAL_ERROR', 'Failed to retrieve document', error.message);
}
}
Expand Down Expand Up @@ -560,7 +560,7 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)
}
}
} catch (manifestError) {
logger.error('[Document Processor] ❌ Error reading manifest:', manifestError);
logger.error({ error: manifestError }, '[Document Processor] ❌ Error reading manifest');
// Continue with default filenames if manifest can't be read
}
}
Expand Down Expand Up @@ -600,7 +600,7 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)
res.end(html);
}
} catch (error: any) {
logger.error('[Document Processor] ❌ Error serving frontend:', error);
logger.error({ error }, '[Document Processor] ❌ Error serving frontend');
sendError(res, 500, 'FRONTEND_ERROR', 'Failed to load knowledge panel', error.message);
}
}
Expand Down Expand Up @@ -647,7 +647,7 @@ async function frontendAssetHandler(req: any, res: any, runtime: IAgentRuntime)
sendError(res, 404, 'NOT_FOUND', `Asset not found: ${req.url}`);
}
} catch (error: any) {
logger.error(`[Document Processor] ❌ Error serving asset ${req.url}:`, error);
logger.error({ error }, `[Document Processor] ❌ Error serving asset ${req.url}`);
sendError(res, 500, 'ASSET_ERROR', `Failed to load asset ${req.url}`, error.message);
}
}
Expand Down Expand Up @@ -722,7 +722,7 @@ async function getKnowledgeChunksHandler(req: any, res: any, runtime: IAgentRunt
},
});
} catch (error: any) {
logger.error('[Document Processor] ❌ Error retrieving chunks:', error);
logger.error({ error }, '[Document Processor] ❌ Error retrieving chunks');
sendError(res, 500, 'RETRIEVAL_ERROR', 'Failed to retrieve knowledge chunks', error.message);
}
}
Expand Down Expand Up @@ -838,7 +838,7 @@ async function searchKnowledgeHandler(req: any, res: any, runtime: IAgentRuntime
count: enhancedResults.length,
});
} catch (error: any) {
logger.error('[Document Processor] ❌ Error searching knowledge:', error);
logger.error({ error }, '[Document Processor] ❌ Error searching knowledge');
sendError(res, 500, 'SEARCH_ERROR', 'Failed to search knowledge', error.message);
}
}
Expand All @@ -854,7 +854,7 @@ async function uploadKnowledgeWithMulter(req: any, res: any, runtime: IAgentRunt
// Apply multer middleware manually
uploadArray(req, res, (err: any) => {
if (err) {
logger.error('[Document Processor] ❌ File upload error:', err);
logger.error({ error: err }, '[Document Processor] ❌ File upload error');
return sendError(res, 400, 'UPLOAD_ERROR', err.message);
}
// If multer succeeded, call the actual handler
Expand Down
49 changes: 23 additions & 26 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ export class KnowledgeService extends Service {
try {
// Use a small delay to ensure runtime is fully ready if needed, though constructor implies it should be.
await new Promise((resolve) => setTimeout(resolve, 1000));

// Get the agent-specific knowledge path from runtime settings
const knowledgePath = this.runtime.getSetting('KNOWLEDGE_PATH');

const result: LoadResult = await loadDocsFromPath(
this as any,
this as any,
this.runtime.agentId,
undefined, // worldId
knowledgePath
);
Comment on lines 67 to 72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Drop the any cast; use type-only import in docs-loader to keep strong typing

Passing this as any masks real type issues and weakens safety. The likely cause is a type/runtime cycle with docs-loader.ts. Prefer a type-only import in docs-loader and pass this directly.

Proposed change here:

-      const result: LoadResult = await loadDocsFromPath(
-        this as any,
+      const result: LoadResult = await loadDocsFromPath(
+        this,
         this.runtime.agentId,
         undefined, // worldId
         knowledgePath
       );

And in src/docs-loader.ts (outside this diff), ensure the KnowledgeService type is imported type-only to avoid runtime cycles:

// at top of docs-loader.ts
import type { KnowledgeService } from './service.ts';

Alternatively, decouple the function signature to accept a narrow interface and avoid referencing the class type:

// docs-loader.ts
export type KnowledgeAdder = (opts: AddKnowledgeOptions) => Promise<{ fragmentCount: number }>;

export async function loadDocsFromPath(
  service: { addKnowledge: KnowledgeAdder },
  agentId: UUID,
  worldId?: UUID,
  knowledgePath?: string
) { /* ... */ }

Please confirm docs-loader.ts uses a type-only import or a narrowed interface so we can safely remove the any cast.


I’ll check how loadDocsFromPath is defined and whether docs-loader.ts already uses a type-only import.


🏁 Script executed:

#!/bin/bash
set -e

# Show the declaration of loadDocsFromPath in docs-loader.ts
echo "=== loadDocsFromPath Declaration ==="
rg --context 3 --max-count 1 "export async function loadDocsFromPath" -n src/docs-loader.ts

# Show the top of docs-loader.ts to inspect imports
echo
echo "=== Top of docs-loader.ts ==="
head -n 20 src/docs-loader.ts

Length of output: 1322


Use a type-only import in docs-loader and remove the any cast

To preserve strong typing and avoid runtime cycles, update both files:

• src/service.ts (around the loadDocsFromPath call)
• src/docs-loader.ts (at the top import)

Proposed diffs:

--- a/src/service.ts
+++ b/src/service.ts
@@ -67,7 +67,7 @@
       const result: LoadResult = await loadDocsFromPath(
-        this as any,
+        this,
         this.runtime.agentId,
         undefined, // worldId
         knowledgePath
       );
--- a/src/docs-loader.ts
+++ b/src/docs-loader.ts
@@ -3,7 +3,7 @@
 import { logger, UUID } from '@elizaos/core';
 import * as fs from 'fs';
 import * as path from 'path';
-import { KnowledgeService } from './service.ts';
+import type { KnowledgeService } from './service.ts';
 import { AddKnowledgeOptions } from './types.ts';
 import { isBinaryContentType } from './utils.ts';

These changes let you drop the any cast safely while keeping KnowledgeService purely a compile-time type.

🤖 Prompt for AI Agents
In src/service.ts around lines 67 to 72, the call to loadDocsFromPath casts
'this' to 'any', which weakens type safety and may cause runtime issues. To fix
this, update the import of KnowledgeService in src/docs-loader.ts to be a
type-only import, ensuring it is used only for type checking and does not create
runtime dependencies. Then, remove the 'any' cast in src/service.ts and pass
'this' directly with the correct type, preserving strong typing and avoiding
runtime cycles.


if (result.successful > 0) {
logger.info(
`KnowledgeService: Loaded ${result.successful} documents from docs folder on startup for agent ${this.runtime.agentId}`
Expand All @@ -82,8 +82,8 @@ export class KnowledgeService extends Service {
}
} catch (error) {
logger.error(
`KnowledgeService: Error loading documents on startup for agent ${this.runtime.agentId}:`,
error
{ error },
`KnowledgeService: Error loading documents on startup for agent ${this.runtime.agentId}`
);
}
}
Expand Down Expand Up @@ -161,14 +161,14 @@ export class KnowledgeService extends Service {
}
}

logger.success('Model configuration validated successfully.');
logger.success(`Knowledge Plugin initialized for agent: ${runtime.character.name}`);
logger.info('Model configuration validated successfully.');
logger.info(`Knowledge Plugin initialized for agent: ${runtime.character.name}`);

logger.info(
'Knowledge Plugin initialized. Frontend panel should be discoverable via its public route.'
);
} catch (error) {
logger.error('Failed to initialize Knowledge plugin:', error);
logger.error({ error }, 'Failed to initialize Knowledge plugin');
throw error;
}

Expand All @@ -178,7 +178,7 @@ export class KnowledgeService extends Service {
if (service.config.LOAD_DOCS_ON_STARTUP) {
logger.info('LOAD_DOCS_ON_STARTUP is enabled. Loading documents from docs folder...');
service.loadInitialDocuments().catch((error) => {
logger.error('Error during initial document loading in KnowledgeService:', error);
logger.error({ error }, 'Error during initial document loading in KnowledgeService');
});
} else {
logger.info('LOAD_DOCS_ON_STARTUP is disabled. Skipping automatic document loading.');
Expand All @@ -195,8 +195,8 @@ export class KnowledgeService extends Service {
// Run in background, don't await here to prevent blocking startup
await service.processCharacterKnowledge(stringKnowledge).catch((err) => {
logger.error(
`KnowledgeService: Error processing character knowledge during startup: ${err.message}`,
err
{ error: err },
'KnowledgeService: Error processing character knowledge during startup'
);
});
} else {
Expand Down Expand Up @@ -330,7 +330,8 @@ export class KnowledgeService extends Service {
fileBuffer = Buffer.from(content, 'base64');
} catch (e: any) {
logger.error(
`KnowledgeService: Failed to convert base64 to buffer for ${originalFilename}: ${e.message}`
{ error: e },
`KnowledgeService: Failed to convert base64 to buffer for ${originalFilename}`
);
throw new Error(`Invalid base64 content for PDF file ${originalFilename}`);
}
Expand All @@ -342,7 +343,8 @@ export class KnowledgeService extends Service {
fileBuffer = Buffer.from(content, 'base64');
} catch (e: any) {
logger.error(
`KnowledgeService: Failed to convert base64 to buffer for ${originalFilename}: ${e.message}`
{ error: e },
`KnowledgeService: Failed to convert base64 to buffer for ${originalFilename}`
);
throw new Error(`Invalid base64 content for binary file ${originalFilename}`);
}
Expand Down Expand Up @@ -373,9 +375,7 @@ export class KnowledgeService extends Service {
extractedText = decodedText;
documentContentToStore = decodedText;
} catch (e) {
logger.error(
`Failed to decode base64 for ${originalFilename}: ${e instanceof Error ? e.message : String(e)}`
);
logger.error({ error: e as any }, `Failed to decode base64 for ${originalFilename}`);
// If it looked like base64 but failed to decode properly, this is an error
throw new Error(
`File ${originalFilename} appears to be corrupted or incorrectly encoded`
Expand Down Expand Up @@ -452,8 +452,8 @@ export class KnowledgeService extends Service {
};
} catch (error: any) {
logger.error(
`KnowledgeService: Error processing document ${originalFilename}: ${error.message}`,
error.stack
{ error, stack: error.stack },
`KnowledgeService: Error processing document ${originalFilename}`
);
throw error;
}
Expand All @@ -462,7 +462,7 @@ export class KnowledgeService extends Service {
// --- Knowledge methods moved from AgentRuntime ---

private async handleProcessingError(error: any, context: string) {
logger.error(`KnowledgeService: Error ${context}:`, error?.message || error || 'Unknown error');
logger.error({ error }, `KnowledgeService: Error ${context}`);
throw error;
}

Expand Down Expand Up @@ -797,8 +797,8 @@ export class KnowledgeService extends Service {
fragmentsProcessed++;
} catch (error) {
logger.error(
`KnowledgeService: Error processing fragment ${fragment.id} for document ${item.id}:`,
error
{ error },
`KnowledgeService: Error processing fragment ${fragment.id} for document ${item.id}`
);
}
}
Expand All @@ -816,10 +816,7 @@ export class KnowledgeService extends Service {
// Store the fragment in the knowledge table
await this.runtime.createMemory(fragment, 'knowledge');
} catch (error) {
logger.error(
`KnowledgeService: Error processing fragment ${fragment.id}:`,
error instanceof Error ? error.message : String(error)
);
logger.error({ error }, `KnowledgeService: Error processing fragment ${fragment.id}`);
throw error;
}
}
Expand Down
18 changes: 9 additions & 9 deletions src/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ export class KnowledgeTestSuite implements TestSuite {
}

// Verify service is registered
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);
const retrievedService = runtime.getService(KnowledgeService.serviceType);

Comment on lines +558 to 560
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Array-based service registry requires mock runtime updates

You’re storing [service] under the key, but the mock defines services: Map<string, Service> and getService returns a single Service. The lifecycle test that compares retrievedService !== service will fail because getService returns the array.

Update the mock to hold arrays and make getService return the first service:

-  const services: Map<string, Service> = new Map();
+  const services: Map<string, Service[]> = new Map();
-    getService<T extends Service>(name: string): T | null {
-      return (services.get(name) as T) || null;
-    },
+    getService<T extends Service>(name: string): T | null {
+      const entry = services.get(name);
+      return (entry?.[0] as T) || null;
+    },
-    getAllServices() {
-      return services;
-    },
+    getAllServices() {
+      return services; // Map<string, Service[]>
+    },
-    async registerService(ServiceClass: typeof Service) {
-      const service = await ServiceClass.start(this);
-      services.set(ServiceClass.serviceType, service);
-    },
+    async registerService(ServiceClass: typeof Service) {
+      const service = await ServiceClass.start(this);
+      const key = ServiceClass.serviceType;
+      const list = services.get(key) ?? [];
+      list.push(service);
+      services.set(key, list);
+    },

After applying the above, the equality check in this test should succeed and the other tests that do runtime.services.set(..., [service]) will be consistent with the mock.


Let’s locate the mock runtime implementation to confirm its current shape before making refactors.


🏁 Script executed:

#!/bin/bash
# Find where the services Map is declared
rg -n "const services" -A3

# Find getService definition
rg -n "getService<.*>" -A5

# Find getAllServices
rg -n "getAllServices" -A3

# Find registerService implementation
rg -n "registerService" -A5

Length of output: 3500


Refactor mock runtime to support array-based service registry

The mock in src/tests.ts still uses Map<string, Service> and single‐item APIs, but tests now store arrays ([service]) and expect getService to unwrap the first element. Update these four spots in src/tests.ts:

• Change the services declaration to hold an array
• Make getService return the first entry of that array
• Update getAllServices’ return type to Map<string, Service[]>
• Adjust registerService to append to the array instead of replacing

--- a/src/tests.ts
+++ b/src/tests.ts
@@ 92c92
-  const services: Map<string, Service> = new Map();
+  const services: Map<string, Service[]> = new Map();
@@ 331,333c331,335
-    getService<T extends Service>(name: string): T | null {
-      return (services.get(name) as T) || null;
-    },
+    getService<T extends Service>(name: string): T | null {
+      const entry = services.get(name);
+      return (entry?.[0] as T) || null;
+    },
@@ 335,337c337,338
-    getAllServices() {
-      return services;
-    },
+    getAllServices(): Map<string, Service[]> {
+      return services;
+    },
@@ 339,342c340,345
-    async registerService(ServiceClass: typeof Service) {
-      const service = await ServiceClass.start(this);
-      services.set(ServiceClass.serviceType, service);
-    },
+    async registerService(ServiceClass: typeof Service) {
+      const service = await ServiceClass.start(this);
+      const key = ServiceClass.serviceType;
+      const list = services.get(key) ?? [];
+      list.push(service);
+      services.set(key, list);
+    },

After applying these changes, your lifecycle test’s equality check (retrievedService === service) will pass, and any code/tests that do runtime.services.set(..., [service]) remain consistent.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/tests.ts around lines 558 to 560, refactor the mock runtime's service
registry to use arrays instead of single service instances. Change the services
declaration to Map<string, Service[]> to hold arrays, modify getService to
return the first element of the array for a given key, update getAllServices to
reflect the new Map<string, Service[]> type, and adjust registerService to
append new services to the existing array rather than replacing it. This will
ensure consistency with tests storing arrays and fix the equality check in
lifecycle tests.

if (retrievedService !== service) {
Expand Down Expand Up @@ -635,7 +635,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should add knowledge successfully',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);

const testDocument = {
clientDocumentId: uuidv4() as UUID,
Expand Down Expand Up @@ -675,7 +675,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should handle duplicate document uploads',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);

const testDocument = {
clientDocumentId: uuidv4() as UUID,
Expand Down Expand Up @@ -711,7 +711,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should retrieve knowledge based on query',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);

// Add some test knowledge
const testDocument = {
Expand Down Expand Up @@ -762,7 +762,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should format knowledge in provider output',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set('knowledge' as any, service);
runtime.services.set('knowledge' as any, [service]);

// Add test knowledge
const testDocument = {
Expand Down Expand Up @@ -885,7 +885,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should handle and log errors appropriately',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);

// Clear previous mock calls
mockLogger.clearCalls();
Expand Down Expand Up @@ -952,8 +952,8 @@ export class KnowledgeTestSuite implements TestSuite {

// Start service
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set('knowledge' as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);
runtime.services.set('knowledge' as any, [service]);

// Register provider
runtime.registerProvider(knowledgeProvider);
Expand Down Expand Up @@ -1029,7 +1029,7 @@ export class KnowledgeTestSuite implements TestSuite {
name: 'Should handle large documents with chunking',
fn: async (runtime: IAgentRuntime) => {
const service = await KnowledgeService.start(runtime);
runtime.services.set(KnowledgeService.serviceType as any, service);
runtime.services.set(KnowledgeService.serviceType as any, [service]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Service Retrieval Returns Array Instead of Object

Services are incorrectly stored as arrays in the runtime.services map. The runtime.getService method expects to retrieve a single service object, but the change causes it to return an array [service] instead. This breaks equality checks (e.g., retrievedService !== service) and subsequent calls to service methods that expect a service object.

Fix in Cursor Fix in Web


// Create a large document
const largeContent = Array(100)
Expand Down