morpheus-cli 0.7.3 → 0.7.5
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/dist/config/manager.js +4 -0
- package/dist/config/schemas.js +8 -2
- package/dist/http/routers/skills.js +164 -0
- package/dist/runtime/oracle.js +8 -2
- package/dist/runtime/providers/factory.js +14 -0
- package/dist/runtime/tasks/context.js +16 -0
- package/dist/runtime/tools/apoc-tool.js +34 -1
- package/dist/runtime/tools/morpheus-tools.js +4 -0
- package/dist/runtime/tools/neo-tool.js +38 -0
- package/dist/runtime/tools/trinity-tool.js +33 -0
- package/dist/types/config.js +5 -1
- package/dist/ui/assets/index-DlwA5wEh.js +117 -0
- package/dist/ui/assets/index-Dz_qYlIb.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +4 -1
- package/dist/ui/assets/index-CiT3ltw7.css +0 -1
- package/dist/ui/assets/index-DfDByABF.js +0 -111
package/dist/config/manager.js
CHANGED
|
@@ -169,6 +169,7 @@ export class ConfigManager {
|
|
|
169
169
|
working_dir: resolveString('MORPHEUS_APOC_WORKING_DIR', config.apoc.working_dir, process.cwd()),
|
|
170
170
|
timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000,
|
|
171
171
|
personality: resolveString('MORPHEUS_APOC_PERSONALITY', config.apoc.personality, 'pragmatic_dev'),
|
|
172
|
+
execution_mode: resolveString('MORPHEUS_APOC_EXECUTION_MODE', config.apoc.execution_mode, 'async'),
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
// Apply precedence to Neo config
|
|
@@ -209,6 +210,7 @@ export class ConfigManager {
|
|
|
209
210
|
base_url: neoBaseUrl || undefined,
|
|
210
211
|
context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
|
|
211
212
|
personality: resolveString('MORPHEUS_NEO_PERSONALITY', config.neo?.personality, 'analytical_engineer'),
|
|
213
|
+
execution_mode: resolveString('MORPHEUS_NEO_EXECUTION_MODE', config.neo?.execution_mode, 'async'),
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
// Apply precedence to Trinity config
|
|
@@ -233,6 +235,7 @@ export class ConfigManager {
|
|
|
233
235
|
base_url: config.trinity?.base_url || config.llm.base_url,
|
|
234
236
|
context_window: resolveOptionalNumeric('MORPHEUS_TRINITY_CONTEXT_WINDOW', config.trinity?.context_window, trinityContextWindowFallback),
|
|
235
237
|
personality: resolveString('MORPHEUS_TRINITY_PERSONALITY', config.trinity?.personality, 'data_specialist'),
|
|
238
|
+
execution_mode: resolveString('MORPHEUS_TRINITY_EXECUTION_MODE', config.trinity?.execution_mode, 'async'),
|
|
236
239
|
};
|
|
237
240
|
}
|
|
238
241
|
// Apply precedence to audio config
|
|
@@ -297,6 +300,7 @@ export class ConfigManager {
|
|
|
297
300
|
logging: loggingConfig,
|
|
298
301
|
memory: memoryConfig,
|
|
299
302
|
chronos: chronosConfig,
|
|
303
|
+
verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
|
|
300
304
|
};
|
|
301
305
|
}
|
|
302
306
|
get() {
|
package/dist/config/schemas.js
CHANGED
|
@@ -25,9 +25,14 @@ export const SatiConfigSchema = LLMConfigSchema.extend({
|
|
|
25
25
|
export const ApocConfigSchema = LLMConfigSchema.extend({
|
|
26
26
|
working_dir: z.string().optional(),
|
|
27
27
|
timeout_ms: z.number().int().positive().optional(),
|
|
28
|
+
execution_mode: z.enum(['sync', 'async']).default('async'),
|
|
29
|
+
});
|
|
30
|
+
export const NeoConfigSchema = LLMConfigSchema.extend({
|
|
31
|
+
execution_mode: z.enum(['sync', 'async']).default('async'),
|
|
32
|
+
});
|
|
33
|
+
export const TrinityConfigSchema = LLMConfigSchema.extend({
|
|
34
|
+
execution_mode: z.enum(['sync', 'async']).default('async'),
|
|
28
35
|
});
|
|
29
|
-
export const NeoConfigSchema = LLMConfigSchema;
|
|
30
|
-
export const TrinityConfigSchema = LLMConfigSchema;
|
|
31
36
|
export const KeymakerConfigSchema = LLMConfigSchema.extend({
|
|
32
37
|
skills_dir: z.string().optional(),
|
|
33
38
|
});
|
|
@@ -62,6 +67,7 @@ export const ConfigSchema = z.object({
|
|
|
62
67
|
}).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
|
|
63
68
|
}).optional(),
|
|
64
69
|
chronos: ChronosConfigSchema.optional(),
|
|
70
|
+
verbose_mode: z.boolean().default(true),
|
|
65
71
|
channels: z.object({
|
|
66
72
|
telegram: z.object({
|
|
67
73
|
enabled: z.boolean().default(false),
|
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import extract from 'extract-zip';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
2
7
|
import { SkillRegistry, updateSkillDelegateDescription } from '../../runtime/skills/index.js';
|
|
3
8
|
import { DisplayManager } from '../../runtime/display.js';
|
|
9
|
+
import { PATHS } from '../../config/paths.js';
|
|
10
|
+
import { SkillMetadataSchema } from '../../runtime/skills/schema.js';
|
|
11
|
+
// Multer config for ZIP uploads
|
|
12
|
+
const upload = multer({
|
|
13
|
+
storage: multer.memoryStorage(),
|
|
14
|
+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
|
15
|
+
fileFilter: (_req, file, cb) => {
|
|
16
|
+
if (file.mimetype === 'application/zip' || file.mimetype === 'application/x-zip-compressed' || file.originalname.endsWith('.zip')) {
|
|
17
|
+
cb(null, true);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
cb(new Error('Only ZIP files are allowed'));
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
// YAML frontmatter regex
|
|
25
|
+
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
26
|
+
/**
|
|
27
|
+
* Simple YAML frontmatter parser
|
|
28
|
+
*/
|
|
29
|
+
function parseFrontmatter(yaml) {
|
|
30
|
+
const result = {};
|
|
31
|
+
const lines = yaml.split('\n');
|
|
32
|
+
let currentKey = null;
|
|
33
|
+
let currentArray = null;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
37
|
+
continue;
|
|
38
|
+
if (trimmed.startsWith('- ') && currentKey && currentArray !== null) {
|
|
39
|
+
currentArray.push(trimmed.slice(2).trim().replace(/^["']|["']$/g, ''));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const colonIndex = line.indexOf(':');
|
|
43
|
+
if (colonIndex > 0) {
|
|
44
|
+
if (currentKey && currentArray !== null && currentArray.length > 0) {
|
|
45
|
+
result[currentKey] = currentArray;
|
|
46
|
+
}
|
|
47
|
+
currentArray = null;
|
|
48
|
+
const key = line.slice(0, colonIndex).trim();
|
|
49
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
50
|
+
currentKey = key;
|
|
51
|
+
if (value === '') {
|
|
52
|
+
currentArray = [];
|
|
53
|
+
}
|
|
54
|
+
else if (value === 'true') {
|
|
55
|
+
result[key] = true;
|
|
56
|
+
}
|
|
57
|
+
else if (value === 'false') {
|
|
58
|
+
result[key] = false;
|
|
59
|
+
}
|
|
60
|
+
else if (/^\d+$/.test(value)) {
|
|
61
|
+
result[key] = parseInt(value, 10);
|
|
62
|
+
}
|
|
63
|
+
else if (/^\d+\.\d+$/.test(value)) {
|
|
64
|
+
result[key] = parseFloat(value);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
result[key] = value.replace(/^["']|["']$/g, '');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (currentKey && currentArray !== null && currentArray.length > 0) {
|
|
72
|
+
result[currentKey] = currentArray;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
4
76
|
/**
|
|
5
77
|
* Create the skills API router
|
|
6
78
|
*
|
|
@@ -8,6 +80,7 @@ import { DisplayManager } from '../../runtime/display.js';
|
|
|
8
80
|
* - GET /api/skills - List all skills
|
|
9
81
|
* - GET /api/skills/:name - Get single skill with content
|
|
10
82
|
* - POST /api/skills/reload - Reload skills from filesystem
|
|
83
|
+
* - POST /api/skills/upload - Upload a skill ZIP
|
|
11
84
|
* - POST /api/skills/:name/enable - Enable a skill
|
|
12
85
|
* - POST /api/skills/:name/disable - Disable a skill
|
|
13
86
|
*/
|
|
@@ -65,6 +138,97 @@ export function createSkillsRouter() {
|
|
|
65
138
|
res.status(500).json({ error: err.message });
|
|
66
139
|
}
|
|
67
140
|
});
|
|
141
|
+
// POST /api/skills/upload - Upload a skill ZIP
|
|
142
|
+
// Must be before /:name routes
|
|
143
|
+
router.post('/upload', upload.single('file'), async (req, res) => {
|
|
144
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'morpheus-skill-'));
|
|
145
|
+
try {
|
|
146
|
+
if (!req.file) {
|
|
147
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
148
|
+
}
|
|
149
|
+
const zipPath = path.join(tempDir, 'skill.zip');
|
|
150
|
+
const extractDir = path.join(tempDir, 'extracted');
|
|
151
|
+
// Write buffer to temp file
|
|
152
|
+
await fs.writeFile(zipPath, req.file.buffer);
|
|
153
|
+
// Extract ZIP
|
|
154
|
+
await extract(zipPath, { dir: extractDir });
|
|
155
|
+
// Find root entries
|
|
156
|
+
const entries = await fs.readdir(extractDir, { withFileTypes: true });
|
|
157
|
+
const folders = entries.filter(e => e.isDirectory());
|
|
158
|
+
const files = entries.filter(e => e.isFile());
|
|
159
|
+
// Validate: exactly one folder at root, no loose files
|
|
160
|
+
if (folders.length !== 1 || files.length > 0) {
|
|
161
|
+
return res.status(400).json({
|
|
162
|
+
error: 'Invalid ZIP structure',
|
|
163
|
+
details: 'ZIP must contain exactly one folder at root level (no loose files)',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const skillFolderName = folders[0].name;
|
|
167
|
+
const skillFolderPath = path.join(extractDir, skillFolderName);
|
|
168
|
+
// Check for SKILL.md
|
|
169
|
+
const skillMdPath = path.join(skillFolderPath, 'SKILL.md');
|
|
170
|
+
if (!await fs.pathExists(skillMdPath)) {
|
|
171
|
+
return res.status(400).json({
|
|
172
|
+
error: 'Missing SKILL.md',
|
|
173
|
+
details: `Folder "${skillFolderName}" must contain a SKILL.md file`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Read and validate SKILL.md frontmatter
|
|
177
|
+
const skillMdContent = await fs.readFile(skillMdPath, 'utf-8');
|
|
178
|
+
const match = skillMdContent.match(FRONTMATTER_REGEX);
|
|
179
|
+
if (!match) {
|
|
180
|
+
return res.status(400).json({
|
|
181
|
+
error: 'Invalid SKILL.md format',
|
|
182
|
+
details: 'SKILL.md must have YAML frontmatter between --- delimiters',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const [, frontmatterYaml] = match;
|
|
186
|
+
const rawMetadata = parseFrontmatter(frontmatterYaml);
|
|
187
|
+
// Validate with Zod schema
|
|
188
|
+
const parseResult = SkillMetadataSchema.safeParse(rawMetadata);
|
|
189
|
+
if (!parseResult.success) {
|
|
190
|
+
const issues = parseResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`);
|
|
191
|
+
return res.status(400).json({
|
|
192
|
+
error: 'Invalid skill metadata',
|
|
193
|
+
details: issues.join('; '),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const metadata = parseResult.data;
|
|
197
|
+
// Check if skill already exists
|
|
198
|
+
const targetPath = path.join(PATHS.skills, metadata.name);
|
|
199
|
+
if (await fs.pathExists(targetPath)) {
|
|
200
|
+
return res.status(409).json({
|
|
201
|
+
error: 'Skill already exists',
|
|
202
|
+
details: `A skill named "${metadata.name}" already exists. Delete it first or choose a different name.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Copy to skills directory
|
|
206
|
+
await fs.copy(skillFolderPath, targetPath);
|
|
207
|
+
// Reload skills
|
|
208
|
+
const registry = SkillRegistry.getInstance();
|
|
209
|
+
await registry.reload();
|
|
210
|
+
updateSkillDelegateDescription();
|
|
211
|
+
display.log(`Skill "${metadata.name}" uploaded successfully`, { source: 'SkillsAPI' });
|
|
212
|
+
res.json({
|
|
213
|
+
success: true,
|
|
214
|
+
skill: {
|
|
215
|
+
name: metadata.name,
|
|
216
|
+
description: metadata.description,
|
|
217
|
+
version: metadata.version,
|
|
218
|
+
author: metadata.author,
|
|
219
|
+
path: targetPath,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
display.log(`Skill upload error: ${err.message}`, { source: 'SkillsAPI', level: 'error' });
|
|
225
|
+
res.status(500).json({ error: err.message });
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
// Cleanup temp directory
|
|
229
|
+
await fs.remove(tempDir).catch(() => { });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
68
232
|
// GET /api/skills/:name - Get single skill with content
|
|
69
233
|
router.get('/:name', (req, res) => {
|
|
70
234
|
try {
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -341,9 +341,11 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
341
341
|
origin_user_id: taskContext?.origin_user_id,
|
|
342
342
|
};
|
|
343
343
|
let contextDelegationAcks = [];
|
|
344
|
+
let syncDelegationCount = 0;
|
|
344
345
|
const response = await TaskRequestContext.run(invokeContext, async () => {
|
|
345
346
|
const agentResponse = await this.provider.invoke({ messages });
|
|
346
347
|
contextDelegationAcks = TaskRequestContext.getDelegationAcks();
|
|
348
|
+
syncDelegationCount = TaskRequestContext.getSyncDelegationCount();
|
|
347
349
|
return agentResponse;
|
|
348
350
|
});
|
|
349
351
|
// Identify new messages generated during the interaction
|
|
@@ -364,13 +366,17 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
364
366
|
const toolDelegationAcks = this.extractDelegationAcksFromMessages(newGeneratedMessages);
|
|
365
367
|
const hadDelegationToolCall = this.hasDelegationToolCall(newGeneratedMessages);
|
|
366
368
|
const hadChronosToolCall = this.hasChronosToolCall(newGeneratedMessages);
|
|
369
|
+
// When all delegation tool calls ran synchronously, there are no task IDs to validate.
|
|
370
|
+
// Treat as a normal (non-delegation) response so the inline result flows through.
|
|
371
|
+
const allDelegationsSyncInline = hadDelegationToolCall && syncDelegationCount > 0
|
|
372
|
+
&& contextDelegationAcks.length === 0;
|
|
367
373
|
const mergedDelegationAcks = [
|
|
368
374
|
...contextDelegationAcks.map((ack) => ({ task_id: ack.task_id, agent: ack.agent })),
|
|
369
375
|
...toolDelegationAcks,
|
|
370
376
|
];
|
|
371
377
|
const validDelegationAcks = this.validateDelegationAcks(mergedDelegationAcks, message);
|
|
372
378
|
if (mergedDelegationAcks.length > 0) {
|
|
373
|
-
this.display.log(`Delegation trace: context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, valid=${validDelegationAcks.length}`, { source: "Oracle", level: "info" });
|
|
379
|
+
this.display.log(`Delegation trace: context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, valid=${validDelegationAcks.length}, sync_inline=${syncDelegationCount}`, { source: "Oracle", level: "info" });
|
|
374
380
|
}
|
|
375
381
|
const delegatedThisTurn = validDelegationAcks.length > 0;
|
|
376
382
|
let blockedSyntheticDelegationAck = false;
|
|
@@ -392,7 +398,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
392
398
|
// returned to the caller (Telegram / UI) immediately after this point.
|
|
393
399
|
this.taskRepository.markAckSent(validDelegationAcks.map(a => a.task_id));
|
|
394
400
|
}
|
|
395
|
-
else if (mergedDelegationAcks.length > 0 || hadDelegationToolCall) {
|
|
401
|
+
else if (!allDelegationsSyncInline && (mergedDelegationAcks.length > 0 || hadDelegationToolCall)) {
|
|
396
402
|
this.display.log(`Delegation attempted but no valid task id was confirmed (context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, had_tool_call=${hadDelegationToolCall}).`, { source: "Oracle", level: "error" });
|
|
397
403
|
// Delegation was attempted but no valid task id could be confirmed in DB.
|
|
398
404
|
responseContent = this.buildDelegationFailureResponse();
|
|
@@ -6,6 +6,11 @@ import { ProviderError } from "../errors.js";
|
|
|
6
6
|
import { createAgent, createMiddleware } from "langchain";
|
|
7
7
|
import { DisplayManager } from "../display.js";
|
|
8
8
|
import { getUsableApiKey } from "../trinity-crypto.js";
|
|
9
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
10
|
+
import { TaskRequestContext } from "../tasks/context.js";
|
|
11
|
+
import { ChannelRegistry } from "../../channels/registry.js";
|
|
12
|
+
/** Channels that should NOT receive verbose tool notifications */
|
|
13
|
+
const SILENT_CHANNELS = new Set(['api', 'ui']);
|
|
9
14
|
export class ProviderFactory {
|
|
10
15
|
static buildMonitoringMiddleware() {
|
|
11
16
|
const display = DisplayManager.getInstance();
|
|
@@ -14,6 +19,15 @@ export class ProviderFactory {
|
|
|
14
19
|
wrapToolCall: (request, handler) => {
|
|
15
20
|
display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ConstructLoad' });
|
|
16
21
|
display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ConstructLoad' });
|
|
22
|
+
// Verbose mode: notify originating channel about which tool is running
|
|
23
|
+
const verboseEnabled = ConfigManager.getInstance().get().verbose_mode !== false;
|
|
24
|
+
if (verboseEnabled) {
|
|
25
|
+
const ctx = TaskRequestContext.get();
|
|
26
|
+
if (ctx?.origin_channel && ctx.origin_user_id && !SILENT_CHANNELS.has(ctx.origin_channel)) {
|
|
27
|
+
ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🔧 executing: ${request.toolCall.name}`)
|
|
28
|
+
.catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
try {
|
|
18
32
|
const result = handler(request);
|
|
19
33
|
display.log(`Tool completed successfully. Result: ${JSON.stringify(result)}`, { level: "info", source: 'ConstructLoad' });
|
|
@@ -27,6 +27,22 @@ export class TaskRequestContext {
|
|
|
27
27
|
static canEnqueueDelegation() {
|
|
28
28
|
return this.getDelegationAcks().length < this.MAX_DELEGATIONS_PER_TURN;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Record that a delegation tool executed synchronously (inline).
|
|
32
|
+
* Oracle uses this to know that the tool call was NOT an async enqueue.
|
|
33
|
+
*/
|
|
34
|
+
static incrementSyncDelegation() {
|
|
35
|
+
const current = storage.getStore();
|
|
36
|
+
if (!current)
|
|
37
|
+
return;
|
|
38
|
+
current.sync_delegation_count = (current.sync_delegation_count ?? 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the number of delegation tools that executed synchronously this turn.
|
|
42
|
+
*/
|
|
43
|
+
static getSyncDelegationCount() {
|
|
44
|
+
return storage.getStore()?.sync_delegation_count ?? 0;
|
|
45
|
+
}
|
|
30
46
|
static findDuplicateDelegation(agent, task) {
|
|
31
47
|
const acks = this.getDelegationAcks();
|
|
32
48
|
if (acks.length === 0)
|
|
@@ -4,6 +4,16 @@ import { TaskRepository } from "../tasks/repository.js";
|
|
|
4
4
|
import { TaskRequestContext } from "../tasks/context.js";
|
|
5
5
|
import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
|
|
6
6
|
import { DisplayManager } from "../display.js";
|
|
7
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
8
|
+
import { Apoc } from "../apoc.js";
|
|
9
|
+
import { ChannelRegistry } from "../../channels/registry.js";
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when Apoc is configured to execute synchronously (inline).
|
|
12
|
+
*/
|
|
13
|
+
function isApocSync() {
|
|
14
|
+
const config = ConfigManager.getInstance().get();
|
|
15
|
+
return config.apoc?.execution_mode === 'sync';
|
|
16
|
+
}
|
|
7
17
|
/**
|
|
8
18
|
* Tool that Oracle uses to delegate devtools tasks to Apoc.
|
|
9
19
|
* Oracle should call this whenever the user requests operations like:
|
|
@@ -25,6 +35,29 @@ export const ApocDelegateTool = tool(async ({ task, context }) => {
|
|
|
25
35
|
});
|
|
26
36
|
return compositeDelegationError();
|
|
27
37
|
}
|
|
38
|
+
// ── Sync mode: execute inline and return result directly ──
|
|
39
|
+
if (isApocSync()) {
|
|
40
|
+
display.log(`Apoc executing synchronously: ${task.slice(0, 80)}...`, {
|
|
41
|
+
source: "ApocDelegateTool",
|
|
42
|
+
level: "info",
|
|
43
|
+
});
|
|
44
|
+
const ctx = TaskRequestContext.get();
|
|
45
|
+
const sessionId = ctx?.session_id ?? "default";
|
|
46
|
+
// Notify originating channel that the agent is working
|
|
47
|
+
if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
|
|
48
|
+
ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, '🧑🔬 Apoc is executing your request...')
|
|
49
|
+
.catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
const apoc = Apoc.getInstance();
|
|
52
|
+
const result = await apoc.execute(task, context, sessionId);
|
|
53
|
+
TaskRequestContext.incrementSyncDelegation();
|
|
54
|
+
display.log(`Apoc sync execution completed.`, {
|
|
55
|
+
source: "ApocDelegateTool",
|
|
56
|
+
level: "info",
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
// ── Async mode (default): create background task ──
|
|
28
61
|
const existingAck = TaskRequestContext.findDuplicateDelegation("apoc", task);
|
|
29
62
|
if (existingAck) {
|
|
30
63
|
display.log(`Apoc delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
|
|
@@ -72,7 +105,7 @@ export const ApocDelegateTool = tool(async ({ task, context }) => {
|
|
|
72
105
|
}
|
|
73
106
|
}, {
|
|
74
107
|
name: "apoc_delegate",
|
|
75
|
-
description: `Delegate a devtools task to Apoc, the specialized development subagent
|
|
108
|
+
description: `Delegate a devtools task to Apoc, the specialized development subagent.
|
|
76
109
|
|
|
77
110
|
This tool enqueues a background task and returns an acknowledgement with task id.
|
|
78
111
|
Do not expect final execution output in the same response.
|
|
@@ -29,20 +29,24 @@ const CONFIG_TO_ENV_MAP = {
|
|
|
29
29
|
'neo.model': ['MORPHEUS_NEO_MODEL'],
|
|
30
30
|
'neo.temperature': ['MORPHEUS_NEO_TEMPERATURE'],
|
|
31
31
|
'neo.api_key': ['MORPHEUS_NEO_API_KEY'],
|
|
32
|
+
'neo.execution_mode': ['MORPHEUS_NEO_EXECUTION_MODE'],
|
|
32
33
|
'apoc.provider': ['MORPHEUS_APOC_PROVIDER'],
|
|
33
34
|
'apoc.model': ['MORPHEUS_APOC_MODEL'],
|
|
34
35
|
'apoc.temperature': ['MORPHEUS_APOC_TEMPERATURE'],
|
|
35
36
|
'apoc.api_key': ['MORPHEUS_APOC_API_KEY'],
|
|
36
37
|
'apoc.working_dir': ['MORPHEUS_APOC_WORKING_DIR'],
|
|
37
38
|
'apoc.timeout_ms': ['MORPHEUS_APOC_TIMEOUT_MS'],
|
|
39
|
+
'apoc.execution_mode': ['MORPHEUS_APOC_EXECUTION_MODE'],
|
|
38
40
|
'trinity.provider': ['MORPHEUS_TRINITY_PROVIDER'],
|
|
39
41
|
'trinity.model': ['MORPHEUS_TRINITY_MODEL'],
|
|
40
42
|
'trinity.temperature': ['MORPHEUS_TRINITY_TEMPERATURE'],
|
|
41
43
|
'trinity.api_key': ['MORPHEUS_TRINITY_API_KEY'],
|
|
44
|
+
'trinity.execution_mode': ['MORPHEUS_TRINITY_EXECUTION_MODE'],
|
|
42
45
|
'audio.provider': ['MORPHEUS_AUDIO_PROVIDER'],
|
|
43
46
|
'audio.model': ['MORPHEUS_AUDIO_MODEL'],
|
|
44
47
|
'audio.apiKey': ['MORPHEUS_AUDIO_API_KEY'],
|
|
45
48
|
'audio.maxDurationSeconds': ['MORPHEUS_AUDIO_MAX_DURATION'],
|
|
49
|
+
'verbose_mode': ['MORPHEUS_VERBOSE_MODE'],
|
|
46
50
|
};
|
|
47
51
|
/**
|
|
48
52
|
* Checks if a config field is overridden by an environment variable.
|
|
@@ -4,6 +4,9 @@ import { TaskRepository } from "../tasks/repository.js";
|
|
|
4
4
|
import { TaskRequestContext } from "../tasks/context.js";
|
|
5
5
|
import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
|
|
6
6
|
import { DisplayManager } from "../display.js";
|
|
7
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
8
|
+
import { Neo } from "../neo.js";
|
|
9
|
+
import { ChannelRegistry } from "../../channels/registry.js";
|
|
7
10
|
const NEO_BUILTIN_CAPABILITIES = `
|
|
8
11
|
Neo built-in capabilities (always available — no MCP required):
|
|
9
12
|
• Config: morpheus_config_query, morpheus_config_update — read/write Morpheus configuration (LLM, channels, UI, etc.)
|
|
@@ -39,6 +42,13 @@ function buildCatalogSection(mcpTools) {
|
|
|
39
42
|
}
|
|
40
43
|
return `\n\nRuntime MCP tools:\n${lines.join("\n")}`;
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns true when Neo is configured to execute synchronously (inline).
|
|
47
|
+
*/
|
|
48
|
+
function isNeoSync() {
|
|
49
|
+
const config = ConfigManager.getInstance().get();
|
|
50
|
+
return config.neo?.execution_mode === 'sync';
|
|
51
|
+
}
|
|
42
52
|
export function updateNeoDelegateToolDescription(tools) {
|
|
43
53
|
const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(tools)}`;
|
|
44
54
|
NeoDelegateTool.description = full;
|
|
@@ -53,6 +63,34 @@ export const NeoDelegateTool = tool(async ({ task, context }) => {
|
|
|
53
63
|
});
|
|
54
64
|
return compositeDelegationError();
|
|
55
65
|
}
|
|
66
|
+
// ── Sync mode: execute inline and return result directly ──
|
|
67
|
+
if (isNeoSync()) {
|
|
68
|
+
display.log(`Neo executing synchronously: ${task.slice(0, 80)}...`, {
|
|
69
|
+
source: "NeoDelegateTool",
|
|
70
|
+
level: "info",
|
|
71
|
+
});
|
|
72
|
+
const ctx = TaskRequestContext.get();
|
|
73
|
+
const sessionId = ctx?.session_id ?? "default";
|
|
74
|
+
// Notify originating channel that the agent is working
|
|
75
|
+
if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
|
|
76
|
+
ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, '🥷 Neo is executing your request...')
|
|
77
|
+
.catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
const neo = Neo.getInstance();
|
|
80
|
+
const result = await neo.execute(task, context, sessionId, {
|
|
81
|
+
origin_channel: ctx?.origin_channel ?? "api",
|
|
82
|
+
session_id: sessionId,
|
|
83
|
+
origin_message_id: ctx?.origin_message_id,
|
|
84
|
+
origin_user_id: ctx?.origin_user_id,
|
|
85
|
+
});
|
|
86
|
+
TaskRequestContext.incrementSyncDelegation();
|
|
87
|
+
display.log(`Neo sync execution completed.`, {
|
|
88
|
+
source: "NeoDelegateTool",
|
|
89
|
+
level: "info",
|
|
90
|
+
});
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
// ── Async mode (default): create background task ──
|
|
56
94
|
const existingAck = TaskRequestContext.findDuplicateDelegation("neo", task);
|
|
57
95
|
if (existingAck) {
|
|
58
96
|
display.log(`Neo delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
|
|
@@ -4,6 +4,9 @@ import { TaskRepository } from "../tasks/repository.js";
|
|
|
4
4
|
import { TaskRequestContext } from "../tasks/context.js";
|
|
5
5
|
import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
|
|
6
6
|
import { DisplayManager } from "../display.js";
|
|
7
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
8
|
+
import { Trinity } from "../trinity.js";
|
|
9
|
+
import { ChannelRegistry } from "../../channels/registry.js";
|
|
7
10
|
const TRINITY_BASE_DESCRIPTION = `Delegate a database task to Trinity, the specialized database subagent, asynchronously.
|
|
8
11
|
|
|
9
12
|
This tool enqueues a background task and returns an acknowledgement with task id.
|
|
@@ -30,6 +33,13 @@ export function updateTrinityDelegateToolDescription(databases) {
|
|
|
30
33
|
const full = `${TRINITY_BASE_DESCRIPTION}${buildDatabaseCatalog(databases)}`;
|
|
31
34
|
TrinityDelegateTool.description = full;
|
|
32
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns true when Trinity is configured to execute synchronously (inline).
|
|
38
|
+
*/
|
|
39
|
+
function isTrinitySync() {
|
|
40
|
+
const config = ConfigManager.getInstance().get();
|
|
41
|
+
return config.trinity?.execution_mode === 'sync';
|
|
42
|
+
}
|
|
33
43
|
export const TrinityDelegateTool = tool(async ({ task, context }) => {
|
|
34
44
|
try {
|
|
35
45
|
const display = DisplayManager.getInstance();
|
|
@@ -40,6 +50,29 @@ export const TrinityDelegateTool = tool(async ({ task, context }) => {
|
|
|
40
50
|
});
|
|
41
51
|
return compositeDelegationError();
|
|
42
52
|
}
|
|
53
|
+
// ── Sync mode: execute inline and return result directly ──
|
|
54
|
+
if (isTrinitySync()) {
|
|
55
|
+
display.log(`Trinity executing synchronously: ${task.slice(0, 80)}...`, {
|
|
56
|
+
source: 'TrinityDelegateTool',
|
|
57
|
+
level: 'info',
|
|
58
|
+
});
|
|
59
|
+
const ctx = TaskRequestContext.get();
|
|
60
|
+
const sessionId = ctx?.session_id ?? 'default';
|
|
61
|
+
// Notify originating channel that the agent is working
|
|
62
|
+
if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
|
|
63
|
+
ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, '👩💻 Trinity is executing your request...')
|
|
64
|
+
.catch(() => { });
|
|
65
|
+
}
|
|
66
|
+
const trinity = Trinity.getInstance();
|
|
67
|
+
const result = await trinity.execute(task, context, sessionId);
|
|
68
|
+
TaskRequestContext.incrementSyncDelegation();
|
|
69
|
+
display.log(`Trinity sync execution completed.`, {
|
|
70
|
+
source: 'TrinityDelegateTool',
|
|
71
|
+
level: 'info',
|
|
72
|
+
});
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
// ── Async mode (default): create background task ──
|
|
43
76
|
const existingAck = TaskRequestContext.findDuplicateDelegation('trinit', task);
|
|
44
77
|
if (existingAck) {
|
|
45
78
|
display.log(`Trinity delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
|
package/dist/types/config.js
CHANGED
|
@@ -51,17 +51,21 @@ export const DEFAULT_CONFIG = {
|
|
|
51
51
|
temperature: 0.2,
|
|
52
52
|
timeout_ms: 30000,
|
|
53
53
|
personality: 'pragmatic_dev',
|
|
54
|
+
execution_mode: 'async',
|
|
54
55
|
},
|
|
55
56
|
neo: {
|
|
56
57
|
provider: 'openai',
|
|
57
58
|
model: 'gpt-4',
|
|
58
59
|
temperature: 0.2,
|
|
59
60
|
personality: 'analytical_engineer',
|
|
61
|
+
execution_mode: 'async',
|
|
60
62
|
},
|
|
61
63
|
trinity: {
|
|
62
64
|
provider: 'openai',
|
|
63
65
|
model: 'gpt-4',
|
|
64
66
|
temperature: 0.2,
|
|
65
67
|
personality: 'data_specialist',
|
|
66
|
-
|
|
68
|
+
execution_mode: 'async',
|
|
69
|
+
},
|
|
70
|
+
verbose_mode: true,
|
|
67
71
|
};
|