morpheus-cli 0.1.5 → 0.1.6
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 +266 -213
- package/bin/morpheus.js +30 -0
- package/dist/channels/telegram.js +2 -2
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/index.js +4 -0
- package/dist/config/mcp-loader.js +42 -0
- package/dist/config/schemas.js +13 -0
- package/dist/http/__tests__/auth.test.js +53 -0
- package/dist/http/api.js +12 -0
- package/dist/http/middleware/auth.js +23 -0
- package/dist/http/server.js +2 -1
- package/dist/runtime/__tests__/manual_start_verify.js +1 -0
- package/dist/runtime/agent.js +79 -7
- package/dist/runtime/audio-agent.js +11 -1
- package/dist/runtime/display.js +12 -0
- package/dist/runtime/memory/sqlite.js +139 -9
- package/dist/runtime/providers/factory.js +27 -2
- package/dist/runtime/scaffold.js +5 -0
- package/dist/runtime/tools/__tests__/tools.test.js +127 -0
- package/dist/runtime/tools/analytics-tools.js +73 -0
- package/dist/runtime/tools/config-tools.js +70 -0
- package/dist/runtime/tools/diagnostic-tools.js +124 -0
- package/dist/runtime/tools/factory.js +55 -11
- package/dist/runtime/tools/index.js +4 -0
- package/dist/types/auth.js +4 -0
- package/dist/types/config.js +1 -0
- package/dist/types/mcp.js +11 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/usage.js +1 -0
- package/dist/ui/assets/index-4kQpg2wK.js +50 -0
- package/dist/ui/assets/index-CwL7mn36.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/dist/ui/assets/index-D1kvj0eG.css +0 -1
- package/dist/ui/assets/index-DTh8waF7.js +0 -50
package/dist/config/schemas.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
3
3
|
export const AudioConfigSchema = z.object({
|
|
4
|
+
provider: z.enum(['google']).default(DEFAULT_CONFIG.audio.provider),
|
|
4
5
|
enabled: z.boolean().default(DEFAULT_CONFIG.audio.enabled),
|
|
5
6
|
apiKey: z.string().optional(),
|
|
6
7
|
maxDurationSeconds: z.number().default(DEFAULT_CONFIG.audio.maxDurationSeconds),
|
|
@@ -16,6 +17,7 @@ export const ConfigSchema = z.object({
|
|
|
16
17
|
provider: z.enum(['openai', 'anthropic', 'ollama', 'gemini']).default(DEFAULT_CONFIG.llm.provider),
|
|
17
18
|
model: z.string().min(1).default(DEFAULT_CONFIG.llm.model),
|
|
18
19
|
temperature: z.number().min(0).max(1).default(DEFAULT_CONFIG.llm.temperature),
|
|
20
|
+
max_tokens: z.number().int().positive().optional(),
|
|
19
21
|
api_key: z.string().optional(),
|
|
20
22
|
}).default(DEFAULT_CONFIG.llm),
|
|
21
23
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
@@ -43,3 +45,14 @@ export const ConfigSchema = z.object({
|
|
|
43
45
|
retention: z.string().default(DEFAULT_CONFIG.logging.retention),
|
|
44
46
|
}).default(DEFAULT_CONFIG.logging),
|
|
45
47
|
});
|
|
48
|
+
export const MCPServerConfigSchema = z.object({
|
|
49
|
+
transport: z.enum(['stdio', 'http']),
|
|
50
|
+
command: z.string().min(1, 'Command is required'),
|
|
51
|
+
args: z.array(z.string()).optional().default([]),
|
|
52
|
+
env: z.record(z.string(), z.string()).optional().default({}),
|
|
53
|
+
_comment: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
export const MCPConfigFileSchema = z.record(z.string(), z.union([
|
|
56
|
+
MCPServerConfigSchema,
|
|
57
|
+
z.string(),
|
|
58
|
+
]));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import bodyParser from 'body-parser';
|
|
5
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
6
|
+
import { AUTH_HEADER } from '../../types/auth.js';
|
|
7
|
+
import { DisplayManager } from '../../runtime/display.js';
|
|
8
|
+
vi.mock('../../runtime/display.js');
|
|
9
|
+
describe('Auth Middleware', () => {
|
|
10
|
+
let app;
|
|
11
|
+
let mockDisplayManager;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
mockDisplayManager = {
|
|
15
|
+
log: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
DisplayManager.getInstance.mockReturnValue(mockDisplayManager);
|
|
18
|
+
app = express();
|
|
19
|
+
app.use(bodyParser.json());
|
|
20
|
+
app.use(authMiddleware);
|
|
21
|
+
app.get('/test', (req, res) => res.status(200).json({ success: true }));
|
|
22
|
+
// Reset env var
|
|
23
|
+
delete process.env.THE_ARCHITECT_PASS;
|
|
24
|
+
});
|
|
25
|
+
it('should allow access when THE_ARCHITECT_PASS is not set', async () => {
|
|
26
|
+
const res = await request(app).get('/test');
|
|
27
|
+
expect(res.status).toBe(200);
|
|
28
|
+
expect(res.body.success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('should block access when THE_ARCHITECT_PASS is set and header is missing', async () => {
|
|
31
|
+
process.env.THE_ARCHITECT_PASS = 'secret123';
|
|
32
|
+
const res = await request(app).get('/test');
|
|
33
|
+
expect(res.status).toBe(401);
|
|
34
|
+
expect(res.body.error).toBe('Unauthorized');
|
|
35
|
+
expect(mockDisplayManager.log).toHaveBeenCalledWith(expect.stringContaining('Unauthorized'), expect.objectContaining({ source: 'http', level: 'warning' }));
|
|
36
|
+
});
|
|
37
|
+
it('should block access when THE_ARCHITECT_PASS is set and header is incorrect', async () => {
|
|
38
|
+
process.env.THE_ARCHITECT_PASS = 'secret123';
|
|
39
|
+
const res = await request(app)
|
|
40
|
+
.get('/test')
|
|
41
|
+
.set(AUTH_HEADER, 'wrongpass');
|
|
42
|
+
expect(res.status).toBe(401);
|
|
43
|
+
expect(mockDisplayManager.log).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it('should allow access when THE_ARCHITECT_PASS is set and header matches', async () => {
|
|
46
|
+
process.env.THE_ARCHITECT_PASS = 'secret123';
|
|
47
|
+
const res = await request(app)
|
|
48
|
+
.get('/test')
|
|
49
|
+
.set(AUTH_HEADER, 'secret123');
|
|
50
|
+
expect(res.status).toBe(200);
|
|
51
|
+
expect(res.body.success).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/dist/http/api.js
CHANGED
|
@@ -4,6 +4,7 @@ import { PATHS } from '../config/paths.js';
|
|
|
4
4
|
import { DisplayManager } from '../runtime/display.js';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
|
|
7
8
|
async function readLastLines(filePath, n) {
|
|
8
9
|
try {
|
|
9
10
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -39,6 +40,17 @@ export function createApiRouter() {
|
|
|
39
40
|
router.get('/config', (req, res) => {
|
|
40
41
|
res.json(configManager.get());
|
|
41
42
|
});
|
|
43
|
+
router.get('/stats/usage', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
46
|
+
const stats = await history.getGlobalUsageStats();
|
|
47
|
+
history.close();
|
|
48
|
+
res.json(stats);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
res.status(500).json({ error: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
42
54
|
// Calculate diff between two objects
|
|
43
55
|
const getDiff = (obj1, obj2, prefix = '') => {
|
|
44
56
|
const changes = [];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AUTH_HEADER } from '../../types/auth.js';
|
|
2
|
+
import { DisplayManager } from '../../runtime/display.js';
|
|
3
|
+
/**
|
|
4
|
+
* Middleware to protect API routes with a password from THE_ARCHITECT_PASS env var.
|
|
5
|
+
* If the env var is not set, authentication is skipped (open mode).
|
|
6
|
+
*/
|
|
7
|
+
export const authMiddleware = (req, res, next) => {
|
|
8
|
+
const architectPass = process.env.THE_ARCHITECT_PASS;
|
|
9
|
+
// If password is not configured, allow all requests
|
|
10
|
+
if (!architectPass || architectPass.trim() === '') {
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
13
|
+
const providedPass = req.headers[AUTH_HEADER];
|
|
14
|
+
if (providedPass === architectPass) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
const display = DisplayManager.getInstance();
|
|
18
|
+
display.log(`Unauthorized access attempt to ${req.path}`, { source: 'http', level: 'warning' });
|
|
19
|
+
return res.status(401).json({
|
|
20
|
+
error: 'Unauthorized',
|
|
21
|
+
code: 'UNAUTHORIZED'
|
|
22
|
+
});
|
|
23
|
+
};
|
package/dist/http/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { ConfigManager } from '../config/manager.js';
|
|
7
7
|
import { DisplayManager } from '../runtime/display.js';
|
|
8
8
|
import { createApiRouter } from './api.js';
|
|
9
|
+
import { authMiddleware } from './middleware/auth.js';
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
11
12
|
export class HttpServer {
|
|
@@ -21,7 +22,7 @@ export class HttpServer {
|
|
|
21
22
|
this.app.use(bodyParser.json());
|
|
22
23
|
}
|
|
23
24
|
setupRoutes() {
|
|
24
|
-
this.app.use('/api', createApiRouter());
|
|
25
|
+
this.app.use('/api', authMiddleware, createApiRouter());
|
|
25
26
|
// Serve static frontend from compiled output
|
|
26
27
|
const uiPath = path.resolve(__dirname, '../ui');
|
|
27
28
|
this.app.use(express.static(uiPath));
|
package/dist/runtime/agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HumanMessage, SystemMessage
|
|
1
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
2
2
|
import { ProviderFactory } from "./providers/factory.js";
|
|
3
3
|
import { ToolsFactory } from "./tools/factory.js";
|
|
4
4
|
import { ConfigManager } from "../config/manager.js";
|
|
@@ -42,7 +42,7 @@ export class Agent {
|
|
|
42
42
|
throw new ProviderError(this.config.llm.provider || 'unknown', err, "Agent initialization failed");
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
async chat(message) {
|
|
45
|
+
async chat(message, extraUsage) {
|
|
46
46
|
if (!this.provider) {
|
|
47
47
|
throw new Error("Agent not initialized. Call initialize() first.");
|
|
48
48
|
}
|
|
@@ -52,7 +52,70 @@ export class Agent {
|
|
|
52
52
|
try {
|
|
53
53
|
this.display.log('Processing message...', { source: 'Agent' });
|
|
54
54
|
const userMessage = new HumanMessage(message);
|
|
55
|
-
|
|
55
|
+
// Attach extra usage (e.g. from Audio) to the user message to be persisted
|
|
56
|
+
if (extraUsage) {
|
|
57
|
+
userMessage.usage_metadata = extraUsage;
|
|
58
|
+
}
|
|
59
|
+
const systemMessage = new SystemMessage(`You are ${this.config.agent.name}, ${this.config.agent.personality},a local AI operator responsible for orchestrating tools, MCPs, and language models to solve the user’s request accurately and reliably.
|
|
60
|
+
|
|
61
|
+
Your primary responsibility is NOT to answer from memory when external tools are available.
|
|
62
|
+
|
|
63
|
+
You must follow these rules strictly:
|
|
64
|
+
|
|
65
|
+
1. Tool Evaluation First
|
|
66
|
+
Before generating a final answer, always evaluate whether any available tool or MCP is capable of providing a more accurate, up-to-date, or authoritative response.
|
|
67
|
+
|
|
68
|
+
If a tool can provide the answer, you MUST call the tool.
|
|
69
|
+
|
|
70
|
+
2. No Historical Assumptions for Dynamic Data
|
|
71
|
+
If the user asks something that:
|
|
72
|
+
- may change over time
|
|
73
|
+
- depends on system state
|
|
74
|
+
- depends on filesystem
|
|
75
|
+
- depends on external APIs
|
|
76
|
+
- was previously asked in the conversation
|
|
77
|
+
|
|
78
|
+
You MUST NOT reuse previous outputs as final truth.
|
|
79
|
+
|
|
80
|
+
Instead:
|
|
81
|
+
- Re-evaluate available tools
|
|
82
|
+
- Re-execute the relevant tool
|
|
83
|
+
- Provide a fresh result
|
|
84
|
+
|
|
85
|
+
Even if the user already asked the same question before, you must treat the request as requiring a new verification.
|
|
86
|
+
|
|
87
|
+
3. History Is Context, Not Source of Truth
|
|
88
|
+
Conversation history may help with context, but it must not replace real-time verification via tools when tools are available.
|
|
89
|
+
|
|
90
|
+
Never assume:
|
|
91
|
+
- System state
|
|
92
|
+
- File contents
|
|
93
|
+
- Database values
|
|
94
|
+
- API responses
|
|
95
|
+
based only on previous messages.
|
|
96
|
+
|
|
97
|
+
4. Tool Priority Over Language Guessing
|
|
98
|
+
If a tool can compute, fetch, inspect, or verify something, prefer tool usage over generating a speculative answer.
|
|
99
|
+
|
|
100
|
+
Never hallucinate values that could be retrieved through a tool.
|
|
101
|
+
|
|
102
|
+
5. Freshness Principle
|
|
103
|
+
Repeated user queries require fresh validation.
|
|
104
|
+
Do not respond with:
|
|
105
|
+
"As I said before..."
|
|
106
|
+
Instead, perform a new tool check if applicable.
|
|
107
|
+
|
|
108
|
+
6. Final Answer Policy
|
|
109
|
+
Only provide a direct natural language answer if:
|
|
110
|
+
- No tool is relevant
|
|
111
|
+
- Tools are unavailable
|
|
112
|
+
- The question is conceptual or explanatory
|
|
113
|
+
|
|
114
|
+
Otherwise, use tools first.
|
|
115
|
+
|
|
116
|
+
You are an operator, not a guesser.
|
|
117
|
+
Accuracy is more important than speed.
|
|
118
|
+
`);
|
|
56
119
|
// Load existing history from database
|
|
57
120
|
const previousMessages = await this.history.getMessages();
|
|
58
121
|
const messages = [
|
|
@@ -61,12 +124,21 @@ export class Agent {
|
|
|
61
124
|
userMessage
|
|
62
125
|
];
|
|
63
126
|
const response = await this.provider.invoke({ messages });
|
|
64
|
-
//
|
|
65
|
-
//
|
|
127
|
+
// Identify new messages generated during the interaction
|
|
128
|
+
// The `messages` array passed to invoke had length `messages.length`
|
|
129
|
+
// The `response.messages` contains the full state.
|
|
130
|
+
// New messages start after the inputs.
|
|
131
|
+
const startNewMessagesIndex = messages.length;
|
|
132
|
+
const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
|
|
133
|
+
// Persist User Message first
|
|
66
134
|
await this.history.addMessage(userMessage);
|
|
67
|
-
|
|
135
|
+
// Persist all new intermediate tool calls and responses
|
|
136
|
+
for (const msg of newGeneratedMessages) {
|
|
137
|
+
await this.history.addMessage(msg);
|
|
138
|
+
}
|
|
68
139
|
this.display.log('Response generated.', { source: 'Agent' });
|
|
69
|
-
|
|
140
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
141
|
+
return (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
70
142
|
}
|
|
71
143
|
catch (err) {
|
|
72
144
|
throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
|
|
@@ -32,7 +32,17 @@ export class AudioAgent {
|
|
|
32
32
|
if (!text) {
|
|
33
33
|
throw new Error('No transcription generated');
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
// Extract usage metadata
|
|
36
|
+
const usage = response.usageMetadata;
|
|
37
|
+
const usageMetadata = {
|
|
38
|
+
input_tokens: usage?.promptTokenCount ?? 0,
|
|
39
|
+
output_tokens: usage?.candidatesTokenCount ?? 0,
|
|
40
|
+
total_tokens: usage?.totalTokenCount ?? 0,
|
|
41
|
+
input_token_details: {
|
|
42
|
+
cache_read: usage?.cachedContentTokenCount ?? 0
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return { text, usage: usageMetadata };
|
|
36
46
|
}
|
|
37
47
|
catch (error) {
|
|
38
48
|
// Wrap error for clarity
|
package/dist/runtime/display.js
CHANGED
|
@@ -83,6 +83,18 @@ export class DisplayManager {
|
|
|
83
83
|
else if (options.source === 'AgentAudio') {
|
|
84
84
|
color = chalk.hex('#b902b9');
|
|
85
85
|
}
|
|
86
|
+
else if (options.source === 'ToolsFactory') {
|
|
87
|
+
color = chalk.hex('#806d00');
|
|
88
|
+
}
|
|
89
|
+
else if (options.source === 'MCPServer') {
|
|
90
|
+
color = chalk.hex('#be4b1d');
|
|
91
|
+
}
|
|
92
|
+
else if (options.source === 'ToolCall') {
|
|
93
|
+
color = chalk.hex('#e5ff00');
|
|
94
|
+
}
|
|
95
|
+
else if (options.source === 'Config') {
|
|
96
|
+
color = chalk.hex('#00c3ff');
|
|
97
|
+
}
|
|
86
98
|
prefix = color(`[${options.source}] `);
|
|
87
99
|
}
|
|
88
100
|
let formattedMessage = message;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
|
|
2
|
-
import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
|
|
2
|
+
import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import * as fs from "fs-extra";
|
|
5
5
|
import * as path from "path";
|
|
@@ -82,37 +82,115 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
82
82
|
session_id TEXT NOT NULL,
|
|
83
83
|
type TEXT NOT NULL,
|
|
84
84
|
content TEXT NOT NULL,
|
|
85
|
-
created_at INTEGER NOT NULL
|
|
85
|
+
created_at INTEGER NOT NULL,
|
|
86
|
+
input_tokens INTEGER,
|
|
87
|
+
output_tokens INTEGER,
|
|
88
|
+
total_tokens INTEGER,
|
|
89
|
+
cache_read_tokens INTEGER
|
|
86
90
|
);
|
|
87
91
|
|
|
88
92
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
89
93
|
ON messages(session_id);
|
|
90
94
|
`);
|
|
95
|
+
this.migrateTable();
|
|
91
96
|
}
|
|
92
97
|
catch (error) {
|
|
93
98
|
throw new Error(`Failed to create messages table: ${error}`);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Checks for missing columns and adds them if necessary.
|
|
103
|
+
*/
|
|
104
|
+
migrateTable() {
|
|
105
|
+
try {
|
|
106
|
+
const tableInfo = this.db.pragma('table_info(messages)');
|
|
107
|
+
const columns = new Set(tableInfo.map(c => c.name));
|
|
108
|
+
const newColumns = [
|
|
109
|
+
'input_tokens',
|
|
110
|
+
'output_tokens',
|
|
111
|
+
'total_tokens',
|
|
112
|
+
'cache_read_tokens'
|
|
113
|
+
];
|
|
114
|
+
for (const col of newColumns) {
|
|
115
|
+
if (!columns.has(col)) {
|
|
116
|
+
try {
|
|
117
|
+
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} INTEGER`);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
// Ignore error if column already exists (race condition or check failed)
|
|
121
|
+
console.warn(`[SQLite] Failed to add column ${col}: ${e}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.warn(`[SQLite] Migration check failed: ${error}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
96
130
|
/**
|
|
97
131
|
* Retrieves all messages for the current session from the database.
|
|
98
132
|
* @returns Promise resolving to an array of BaseMessage objects
|
|
99
133
|
*/
|
|
100
134
|
async getMessages() {
|
|
101
135
|
try {
|
|
102
|
-
//
|
|
103
|
-
const stmt = this.db.prepare("SELECT type, content FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
136
|
+
// Fetch new columns
|
|
137
|
+
const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
104
138
|
const rows = stmt.all(this.sessionId, this.limit);
|
|
105
139
|
return rows.map((row) => {
|
|
140
|
+
let msg;
|
|
141
|
+
// Reconstruct usage metadata if present
|
|
142
|
+
const usage_metadata = row.total_tokens != null ? {
|
|
143
|
+
input_tokens: row.input_tokens || 0,
|
|
144
|
+
output_tokens: row.output_tokens || 0,
|
|
145
|
+
total_tokens: row.total_tokens || 0,
|
|
146
|
+
input_token_details: row.cache_read_tokens ? { cache_read: row.cache_read_tokens } : undefined
|
|
147
|
+
} : undefined;
|
|
106
148
|
switch (row.type) {
|
|
107
149
|
case "human":
|
|
108
|
-
|
|
150
|
+
msg = new HumanMessage(row.content);
|
|
151
|
+
break;
|
|
109
152
|
case "ai":
|
|
110
|
-
|
|
153
|
+
try {
|
|
154
|
+
// Attempt to parse structured content (for tool calls)
|
|
155
|
+
const parsed = JSON.parse(row.content);
|
|
156
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.tool_calls)) {
|
|
157
|
+
msg = new AIMessage({
|
|
158
|
+
content: parsed.text || "",
|
|
159
|
+
tool_calls: parsed.tool_calls
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
msg = new AIMessage(row.content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Fallback for legacy text-only messages
|
|
168
|
+
msg = new AIMessage(row.content);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
111
171
|
case "system":
|
|
112
|
-
|
|
172
|
+
msg = new SystemMessage(row.content);
|
|
173
|
+
break;
|
|
174
|
+
case "tool":
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(row.content);
|
|
177
|
+
msg = new ToolMessage({
|
|
178
|
+
content: parsed.content,
|
|
179
|
+
tool_call_id: parsed.tool_call_id || 'unknown',
|
|
180
|
+
name: parsed.name
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
msg = new ToolMessage({ content: row.content, tool_call_id: 'unknown' });
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
113
187
|
default:
|
|
114
188
|
throw new Error(`Unknown message type: ${row.type}`);
|
|
115
189
|
}
|
|
190
|
+
if (usage_metadata) {
|
|
191
|
+
msg.usage_metadata = usage_metadata;
|
|
192
|
+
}
|
|
193
|
+
return msg;
|
|
116
194
|
});
|
|
117
195
|
}
|
|
118
196
|
catch (error) {
|
|
@@ -139,14 +217,50 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
139
217
|
else if (message instanceof SystemMessage) {
|
|
140
218
|
type = "system";
|
|
141
219
|
}
|
|
220
|
+
else if (message instanceof ToolMessage) {
|
|
221
|
+
type = "tool";
|
|
222
|
+
}
|
|
142
223
|
else {
|
|
143
224
|
throw new Error(`Unsupported message type: ${message.constructor.name}`);
|
|
144
225
|
}
|
|
145
226
|
const content = typeof message.content === "string"
|
|
146
227
|
? message.content
|
|
147
228
|
: JSON.stringify(message.content);
|
|
148
|
-
|
|
149
|
-
|
|
229
|
+
// Extract usage metadata
|
|
230
|
+
// 1. Try generic usage_metadata (LangChain standard)
|
|
231
|
+
// 2. Try extraUsage (passed via some adapters) - attached to additional_kwargs usually, but we might pass it differently
|
|
232
|
+
// The Spec says we might pass it to chat(), but addMessage receives a BaseMessage.
|
|
233
|
+
// So we should expect usage to be on the message object properties.
|
|
234
|
+
const anyMsg = message;
|
|
235
|
+
const usage = anyMsg.usage_metadata || anyMsg.response_metadata?.usage || anyMsg.response_metadata?.tokenUsage || anyMsg.usage;
|
|
236
|
+
const inputTokens = usage?.input_tokens ?? null;
|
|
237
|
+
const outputTokens = usage?.output_tokens ?? null;
|
|
238
|
+
const totalTokens = usage?.total_tokens ?? null;
|
|
239
|
+
const cacheReadTokens = usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null;
|
|
240
|
+
// Handle special content serialization for Tools
|
|
241
|
+
let finalContent = "";
|
|
242
|
+
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
243
|
+
// Serialize tool calls with content
|
|
244
|
+
finalContent = JSON.stringify({
|
|
245
|
+
text: message.content,
|
|
246
|
+
tool_calls: message.tool_calls
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else if (type === 'tool') {
|
|
250
|
+
const tm = message;
|
|
251
|
+
finalContent = JSON.stringify({
|
|
252
|
+
content: tm.content,
|
|
253
|
+
tool_call_id: tm.tool_call_id,
|
|
254
|
+
name: tm.name
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
finalContent = typeof message.content === "string"
|
|
259
|
+
? message.content
|
|
260
|
+
: JSON.stringify(message.content);
|
|
261
|
+
}
|
|
262
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
263
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens);
|
|
150
264
|
}
|
|
151
265
|
catch (error) {
|
|
152
266
|
// Check for specific SQLite errors
|
|
@@ -164,6 +278,22 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
164
278
|
throw new Error(`Failed to add message: ${error}`);
|
|
165
279
|
}
|
|
166
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Retrieves aggregated usage statistics for all messages in the database.
|
|
283
|
+
*/
|
|
284
|
+
async getGlobalUsageStats() {
|
|
285
|
+
try {
|
|
286
|
+
const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
|
|
287
|
+
const row = stmt.get();
|
|
288
|
+
return {
|
|
289
|
+
totalInputTokens: row.totalInput || 0,
|
|
290
|
+
totalOutputTokens: row.totalOutput || 0
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw new Error(`Failed to get usage stats: ${error}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
167
297
|
/**
|
|
168
298
|
* Clears all messages for the current session from the database.
|
|
169
299
|
*/
|
|
@@ -3,13 +3,30 @@ import { ChatAnthropic } from "@langchain/anthropic";
|
|
|
3
3
|
import { ChatOllama } from "@langchain/ollama";
|
|
4
4
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
5
5
|
import { ProviderError } from "../errors.js";
|
|
6
|
-
import { createAgent } from "langchain";
|
|
6
|
+
import { createAgent, createMiddleware } from "langchain";
|
|
7
7
|
// import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { DisplayManager } from "../display.js";
|
|
10
|
+
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool } from "../tools/index.js";
|
|
10
11
|
export class ProviderFactory {
|
|
11
12
|
static async create(config, tools = []) {
|
|
12
13
|
let display = DisplayManager.getInstance();
|
|
14
|
+
const toolMonitoringMiddleware = createMiddleware({
|
|
15
|
+
name: "ToolMonitoringMiddleware",
|
|
16
|
+
wrapToolCall: (request, handler) => {
|
|
17
|
+
display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ToolCall' });
|
|
18
|
+
display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ToolCall' });
|
|
19
|
+
try {
|
|
20
|
+
const result = handler(request);
|
|
21
|
+
display.log("Tool completed successfully", { level: "info", source: 'ToolCall' });
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
display.log(`Tool failed: ${e}`, { level: "error", source: 'ToolCall' });
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
13
30
|
let model;
|
|
14
31
|
const responseSchema = z.object({
|
|
15
32
|
content: z.string().describe("The main response content from the agent"),
|
|
@@ -49,10 +66,18 @@ export class ProviderFactory {
|
|
|
49
66
|
default:
|
|
50
67
|
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
51
68
|
}
|
|
52
|
-
const toolsForAgent =
|
|
69
|
+
const toolsForAgent = [
|
|
70
|
+
...tools,
|
|
71
|
+
ConfigQueryTool,
|
|
72
|
+
ConfigUpdateTool,
|
|
73
|
+
DiagnosticTool,
|
|
74
|
+
MessageCountTool,
|
|
75
|
+
TokenUsageTool
|
|
76
|
+
];
|
|
53
77
|
return createAgent({
|
|
54
78
|
model: model,
|
|
55
79
|
tools: toolsForAgent,
|
|
80
|
+
middleware: [toolMonitoringMiddleware]
|
|
56
81
|
});
|
|
57
82
|
}
|
|
58
83
|
catch (error) {
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { PATHS } from '../config/paths.js';
|
|
3
3
|
import { ConfigManager } from '../config/manager.js';
|
|
4
|
+
import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import ora from 'ora';
|
|
6
7
|
export async function scaffold() {
|
|
@@ -22,6 +23,10 @@ export async function scaffold() {
|
|
|
22
23
|
else {
|
|
23
24
|
await configManager.load(); // Load if exists (although load handles existence check too)
|
|
24
25
|
}
|
|
26
|
+
// Create mcps.json if not exists
|
|
27
|
+
if (!(await fs.pathExists(PATHS.mcps))) {
|
|
28
|
+
await fs.writeJson(PATHS.mcps, DEFAULT_MCP_TEMPLATE, { spaces: 2 });
|
|
29
|
+
}
|
|
25
30
|
spinner.succeed('Morpheus environment ready at ' + chalk.cyan(PATHS.root));
|
|
26
31
|
}
|
|
27
32
|
catch (error) {
|