morpheus-cli 0.7.1 → 0.7.3
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 +125 -0
- package/dist/channels/discord.js +109 -0
- package/dist/channels/telegram.js +94 -0
- package/dist/cli/commands/start.js +12 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +4 -0
- package/dist/http/api.js +3 -0
- package/dist/http/routers/skills.js +127 -0
- package/dist/runtime/__tests__/keymaker.test.js +145 -0
- package/dist/runtime/chronos/parser.js +38 -20
- package/dist/runtime/keymaker.js +162 -0
- package/dist/runtime/oracle.js +7 -2
- package/dist/runtime/scaffold.js +75 -0
- package/dist/runtime/skills/__tests__/loader.test.js +187 -0
- package/dist/runtime/skills/__tests__/registry.test.js +201 -0
- package/dist/runtime/skills/__tests__/tool.test.js +266 -0
- package/dist/runtime/skills/index.js +8 -0
- package/dist/runtime/skills/loader.js +213 -0
- package/dist/runtime/skills/registry.js +141 -0
- package/dist/runtime/skills/schema.js +30 -0
- package/dist/runtime/skills/tool.js +204 -0
- package/dist/runtime/skills/types.js +7 -0
- package/dist/runtime/tasks/worker.js +22 -0
- package/dist/ui/assets/index-CiT3ltw7.css +1 -0
- package/dist/ui/assets/{index-7e8TCoiy.js → index-DfDByABF.js} +21 -21
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-B9ngtbja.css +0 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillLoader - Discovers and loads skills from filesystem
|
|
3
|
+
*
|
|
4
|
+
* Skills are SKILL.md files with YAML frontmatter containing metadata.
|
|
5
|
+
* Format:
|
|
6
|
+
* ---
|
|
7
|
+
* name: my-skill
|
|
8
|
+
* description: What this skill does
|
|
9
|
+
* execution_mode: sync
|
|
10
|
+
* ---
|
|
11
|
+
*
|
|
12
|
+
* # Skill Instructions...
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { SkillMetadataSchema } from './schema.js';
|
|
17
|
+
import { DisplayManager } from '../display.js';
|
|
18
|
+
const SKILL_MD = 'SKILL.md';
|
|
19
|
+
const MAX_SKILL_MD_SIZE = 50 * 1024; // 50KB
|
|
20
|
+
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
21
|
+
/**
|
|
22
|
+
* Simple YAML frontmatter parser
|
|
23
|
+
* Handles basic key: value pairs and arrays
|
|
24
|
+
*/
|
|
25
|
+
function parseFrontmatter(yaml) {
|
|
26
|
+
const result = {};
|
|
27
|
+
const lines = yaml.split('\n');
|
|
28
|
+
let currentKey = null;
|
|
29
|
+
let currentArray = null;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
33
|
+
continue;
|
|
34
|
+
// Check for array item (indented with -)
|
|
35
|
+
if (trimmed.startsWith('- ') && currentKey && currentArray !== null) {
|
|
36
|
+
currentArray.push(trimmed.slice(2).trim().replace(/^["']|["']$/g, ''));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Check for key: value
|
|
40
|
+
const colonIndex = line.indexOf(':');
|
|
41
|
+
if (colonIndex > 0) {
|
|
42
|
+
// Save previous array if exists
|
|
43
|
+
if (currentKey && currentArray !== null && currentArray.length > 0) {
|
|
44
|
+
result[currentKey] = currentArray;
|
|
45
|
+
}
|
|
46
|
+
currentArray = null;
|
|
47
|
+
const key = line.slice(0, colonIndex).trim();
|
|
48
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
49
|
+
currentKey = key;
|
|
50
|
+
if (value === '') {
|
|
51
|
+
// Could be start of array
|
|
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
|
+
// Remove quotes if present
|
|
68
|
+
result[key] = value.replace(/^["']|["']$/g, '');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Save last array if exists
|
|
73
|
+
if (currentKey && currentArray !== null && currentArray.length > 0) {
|
|
74
|
+
result[currentKey] = currentArray;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
export class SkillLoader {
|
|
79
|
+
skillsDir;
|
|
80
|
+
display = DisplayManager.getInstance();
|
|
81
|
+
constructor(skillsDir) {
|
|
82
|
+
this.skillsDir = skillsDir;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Scan skills directory and load all valid skills
|
|
86
|
+
*/
|
|
87
|
+
async scan() {
|
|
88
|
+
const skills = [];
|
|
89
|
+
const errors = [];
|
|
90
|
+
// Check if skills directory exists
|
|
91
|
+
if (!fs.existsSync(this.skillsDir)) {
|
|
92
|
+
this.display.log(`Skills directory does not exist: ${this.skillsDir}`, {
|
|
93
|
+
source: 'SkillLoader',
|
|
94
|
+
level: 'debug',
|
|
95
|
+
});
|
|
96
|
+
return { skills, errors };
|
|
97
|
+
}
|
|
98
|
+
// Read directory contents
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(this.skillsDir, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.display.log(`Failed to read skills directory: ${err}`, {
|
|
105
|
+
source: 'SkillLoader',
|
|
106
|
+
level: 'error',
|
|
107
|
+
});
|
|
108
|
+
return { skills, errors };
|
|
109
|
+
}
|
|
110
|
+
// Process each subdirectory
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.isDirectory())
|
|
113
|
+
continue;
|
|
114
|
+
const dirPath = path.join(this.skillsDir, entry.name);
|
|
115
|
+
const result = this.loadSkillFromDir(dirPath, entry.name);
|
|
116
|
+
if (result.skill) {
|
|
117
|
+
skills.push(result.skill);
|
|
118
|
+
}
|
|
119
|
+
else if (result.error) {
|
|
120
|
+
errors.push(result.error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { skills, errors };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load a single skill from a directory
|
|
127
|
+
*/
|
|
128
|
+
loadSkillFromDir(dirPath, dirName) {
|
|
129
|
+
const mdPath = path.join(dirPath, SKILL_MD);
|
|
130
|
+
// Check SKILL.md exists
|
|
131
|
+
if (!fs.existsSync(mdPath)) {
|
|
132
|
+
return {
|
|
133
|
+
error: {
|
|
134
|
+
directory: dirName,
|
|
135
|
+
message: `Missing ${SKILL_MD}`,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Read SKILL.md content
|
|
140
|
+
let rawContent;
|
|
141
|
+
try {
|
|
142
|
+
const stats = fs.statSync(mdPath);
|
|
143
|
+
if (stats.size > MAX_SKILL_MD_SIZE) {
|
|
144
|
+
this.display.log(`SKILL.md for "${dirName}" exceeds ${MAX_SKILL_MD_SIZE / 1024}KB`, { source: 'SkillLoader', level: 'warning' });
|
|
145
|
+
}
|
|
146
|
+
rawContent = fs.readFileSync(mdPath, 'utf-8').slice(0, MAX_SKILL_MD_SIZE);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
error: {
|
|
151
|
+
directory: dirName,
|
|
152
|
+
message: `Failed to read ${SKILL_MD}: ${err instanceof Error ? err.message : String(err)}`,
|
|
153
|
+
error: err instanceof Error ? err : undefined,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Parse frontmatter
|
|
158
|
+
const match = rawContent.match(FRONTMATTER_REGEX);
|
|
159
|
+
if (!match) {
|
|
160
|
+
return {
|
|
161
|
+
error: {
|
|
162
|
+
directory: dirName,
|
|
163
|
+
message: `Invalid format: ${SKILL_MD} must start with YAML frontmatter (--- ... ---)`,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const [, frontmatterYaml, content] = match;
|
|
168
|
+
// Parse YAML frontmatter
|
|
169
|
+
let rawMeta;
|
|
170
|
+
try {
|
|
171
|
+
rawMeta = parseFrontmatter(frontmatterYaml);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
error: {
|
|
176
|
+
directory: dirName,
|
|
177
|
+
message: `Invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
178
|
+
error: err instanceof Error ? err : undefined,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Validate against schema
|
|
183
|
+
const parseResult = SkillMetadataSchema.safeParse(rawMeta);
|
|
184
|
+
if (!parseResult.success) {
|
|
185
|
+
const issues = parseResult.error.issues
|
|
186
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
187
|
+
.join('; ');
|
|
188
|
+
return {
|
|
189
|
+
error: {
|
|
190
|
+
directory: dirName,
|
|
191
|
+
message: `Schema validation failed: ${issues}`,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const metadata = parseResult.data;
|
|
196
|
+
// Build Skill object
|
|
197
|
+
const skill = {
|
|
198
|
+
...metadata,
|
|
199
|
+
path: mdPath,
|
|
200
|
+
dirName,
|
|
201
|
+
content: content.trim(),
|
|
202
|
+
enabled: metadata.enabled ?? true,
|
|
203
|
+
execution_mode: metadata.execution_mode ?? 'sync',
|
|
204
|
+
};
|
|
205
|
+
return { skill };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Read SKILL.md content for a skill (returns just the body, no frontmatter)
|
|
209
|
+
*/
|
|
210
|
+
readContent(skill) {
|
|
211
|
+
return skill.content || null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillRegistry - Singleton registry for loaded skills
|
|
3
|
+
*/
|
|
4
|
+
import { PATHS } from '../../config/paths.js';
|
|
5
|
+
import { DisplayManager } from '../display.js';
|
|
6
|
+
import { SkillLoader } from './loader.js';
|
|
7
|
+
export class SkillRegistry {
|
|
8
|
+
static instance = null;
|
|
9
|
+
skills = new Map();
|
|
10
|
+
loader;
|
|
11
|
+
display = DisplayManager.getInstance();
|
|
12
|
+
constructor() {
|
|
13
|
+
this.loader = new SkillLoader(PATHS.skills);
|
|
14
|
+
}
|
|
15
|
+
static getInstance() {
|
|
16
|
+
if (!SkillRegistry.instance) {
|
|
17
|
+
SkillRegistry.instance = new SkillRegistry();
|
|
18
|
+
}
|
|
19
|
+
return SkillRegistry.instance;
|
|
20
|
+
}
|
|
21
|
+
static resetInstance() {
|
|
22
|
+
SkillRegistry.instance = null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load skills from filesystem
|
|
26
|
+
*/
|
|
27
|
+
async load() {
|
|
28
|
+
const result = await this.loader.scan();
|
|
29
|
+
this.skills.clear();
|
|
30
|
+
for (const skill of result.skills) {
|
|
31
|
+
if (this.skills.has(skill.name)) {
|
|
32
|
+
this.display.log(`Duplicate skill name "${skill.name}", overwriting`, {
|
|
33
|
+
source: 'SkillRegistry',
|
|
34
|
+
level: 'warning',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
this.skills.set(skill.name, skill);
|
|
38
|
+
}
|
|
39
|
+
// Log errors
|
|
40
|
+
for (const error of result.errors) {
|
|
41
|
+
this.display.log(`Failed to load skill from "${error.directory}": ${error.message}`, {
|
|
42
|
+
source: 'SkillRegistry',
|
|
43
|
+
level: 'warning',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
this.display.log(`Loaded ${this.skills.size} skills`, { source: 'SkillRegistry' });
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reload skills from filesystem
|
|
51
|
+
*/
|
|
52
|
+
async reload() {
|
|
53
|
+
return this.load();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get all loaded skills
|
|
57
|
+
*/
|
|
58
|
+
getAll() {
|
|
59
|
+
return Array.from(this.skills.values());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get only enabled skills
|
|
63
|
+
*/
|
|
64
|
+
getEnabled() {
|
|
65
|
+
return this.getAll().filter((s) => s.enabled);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a skill by name
|
|
69
|
+
*/
|
|
70
|
+
get(name) {
|
|
71
|
+
return this.skills.get(name);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Enable a skill (runtime only, doesn't persist to YAML)
|
|
75
|
+
*/
|
|
76
|
+
enable(name) {
|
|
77
|
+
const skill = this.skills.get(name);
|
|
78
|
+
if (!skill)
|
|
79
|
+
return false;
|
|
80
|
+
skill.enabled = true;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Disable a skill (runtime only, doesn't persist to YAML)
|
|
85
|
+
*/
|
|
86
|
+
disable(name) {
|
|
87
|
+
const skill = this.skills.get(name);
|
|
88
|
+
if (!skill)
|
|
89
|
+
return false;
|
|
90
|
+
skill.enabled = false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read SKILL.md content for a skill
|
|
95
|
+
*/
|
|
96
|
+
getContent(name) {
|
|
97
|
+
const skill = this.skills.get(name);
|
|
98
|
+
if (!skill)
|
|
99
|
+
return null;
|
|
100
|
+
return skill.content || null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate system prompt section listing available skills
|
|
104
|
+
*/
|
|
105
|
+
getSystemPromptSection() {
|
|
106
|
+
const enabled = this.getEnabled();
|
|
107
|
+
if (enabled.length === 0) {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
const syncSkills = enabled.filter((s) => s.execution_mode === 'sync');
|
|
111
|
+
const asyncSkills = enabled.filter((s) => s.execution_mode === 'async');
|
|
112
|
+
const lines = ['## Available Skills', ''];
|
|
113
|
+
if (syncSkills.length > 0) {
|
|
114
|
+
lines.push('### Sync Skills (immediate result via skill_execute)');
|
|
115
|
+
for (const s of syncSkills) {
|
|
116
|
+
lines.push(`- **${s.name}**: ${s.description}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
if (asyncSkills.length > 0) {
|
|
121
|
+
lines.push('### Async Skills (background task via skill_delegate)');
|
|
122
|
+
for (const s of asyncSkills) {
|
|
123
|
+
lines.push(`- **${s.name}**: ${s.description}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
lines.push('Use `skill_execute(skillName, objective)` for sync skills — result returned immediately.');
|
|
128
|
+
lines.push('Use `skill_delegate(skillName, objective)` for async skills — runs in background, notifies when done.');
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get skill names for tool description
|
|
133
|
+
*/
|
|
134
|
+
getSkillNamesForTool() {
|
|
135
|
+
const enabled = this.getEnabled();
|
|
136
|
+
if (enabled.length === 0) {
|
|
137
|
+
return 'No skills available.';
|
|
138
|
+
}
|
|
139
|
+
return `Available skills: ${enabled.map((s) => s.name).join(', ')}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Zod Schema for SKILL.md frontmatter validation
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
/**
|
|
6
|
+
* Schema for SKILL.md frontmatter metadata
|
|
7
|
+
*/
|
|
8
|
+
export const SkillMetadataSchema = z.object({
|
|
9
|
+
name: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1, 'Skill name is required')
|
|
12
|
+
.max(64, 'Skill name must be at most 64 characters')
|
|
13
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, 'Skill name must be lowercase alphanumeric with hyphens, cannot start/end with hyphen'),
|
|
14
|
+
description: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1, 'Description is required')
|
|
17
|
+
.max(500, 'Description must be at most 500 characters'),
|
|
18
|
+
execution_mode: z
|
|
19
|
+
.enum(['sync', 'async'])
|
|
20
|
+
.default('sync')
|
|
21
|
+
.describe('Execution mode: sync returns result inline, async creates background task'),
|
|
22
|
+
version: z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., 1.0.0)')
|
|
25
|
+
.optional(),
|
|
26
|
+
author: z.string().max(100).optional(),
|
|
27
|
+
enabled: z.boolean().optional().default(true),
|
|
28
|
+
tags: z.array(z.string().max(32)).max(10).optional(),
|
|
29
|
+
examples: z.array(z.string().max(200)).max(5).optional(),
|
|
30
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Tools - skill_execute (sync) and skill_delegate (async)
|
|
3
|
+
*/
|
|
4
|
+
import { tool } from "@langchain/core/tools";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { TaskRepository } from "../tasks/repository.js";
|
|
7
|
+
import { TaskRequestContext } from "../tasks/context.js";
|
|
8
|
+
import { DisplayManager } from "../display.js";
|
|
9
|
+
import { SkillRegistry } from "./registry.js";
|
|
10
|
+
import { executeKeymakerTask } from "../keymaker.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// skill_execute - Synchronous execution
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Generates the skill_execute tool description dynamically with sync skills.
|
|
16
|
+
*/
|
|
17
|
+
export function getSkillExecuteDescription() {
|
|
18
|
+
const registry = SkillRegistry.getInstance();
|
|
19
|
+
const syncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'sync');
|
|
20
|
+
const skillList = syncSkills.length > 0
|
|
21
|
+
? syncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
22
|
+
: '(no sync skills enabled)';
|
|
23
|
+
return `Execute a skill synchronously using Keymaker. The result is returned immediately.
|
|
24
|
+
|
|
25
|
+
Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
|
|
26
|
+
|
|
27
|
+
Available sync skills:
|
|
28
|
+
${skillList}
|
|
29
|
+
|
|
30
|
+
Use this for skills that need immediate results in the conversation.`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tool that Oracle uses to execute skills synchronously via Keymaker.
|
|
34
|
+
* Result is returned directly to Oracle for inclusion in the response.
|
|
35
|
+
*/
|
|
36
|
+
export const SkillExecuteTool = tool(async ({ skillName, objective }) => {
|
|
37
|
+
const display = DisplayManager.getInstance();
|
|
38
|
+
const registry = SkillRegistry.getInstance();
|
|
39
|
+
// Validate skill exists and is enabled
|
|
40
|
+
const skill = registry.get(skillName);
|
|
41
|
+
if (!skill) {
|
|
42
|
+
const available = registry.getEnabled().map(s => s.name).join(', ');
|
|
43
|
+
return `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
|
|
44
|
+
}
|
|
45
|
+
if (!skill.enabled) {
|
|
46
|
+
return `Error: Skill "${skillName}" is disabled.`;
|
|
47
|
+
}
|
|
48
|
+
if (skill.execution_mode === 'async') {
|
|
49
|
+
return `Error: Skill "${skillName}" is async-only. Use skill_delegate instead.`;
|
|
50
|
+
}
|
|
51
|
+
display.log(`Executing skill "${skillName}" synchronously...`, {
|
|
52
|
+
source: "SkillExecuteTool",
|
|
53
|
+
level: "info",
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const ctx = TaskRequestContext.get();
|
|
57
|
+
const sessionId = ctx?.session_id ?? "default";
|
|
58
|
+
const taskContext = {
|
|
59
|
+
origin_channel: ctx?.origin_channel ?? "api",
|
|
60
|
+
session_id: sessionId,
|
|
61
|
+
origin_message_id: ctx?.origin_message_id,
|
|
62
|
+
origin_user_id: ctx?.origin_user_id,
|
|
63
|
+
};
|
|
64
|
+
// Execute Keymaker directly (synchronous)
|
|
65
|
+
const result = await executeKeymakerTask(skillName, objective, taskContext);
|
|
66
|
+
display.log(`Skill "${skillName}" completed successfully.`, {
|
|
67
|
+
source: "SkillExecuteTool",
|
|
68
|
+
level: "info",
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
display.log(`Skill execution error: ${err.message}`, {
|
|
74
|
+
source: "SkillExecuteTool",
|
|
75
|
+
level: "error",
|
|
76
|
+
});
|
|
77
|
+
return `Skill execution failed: ${err.message}`;
|
|
78
|
+
}
|
|
79
|
+
}, {
|
|
80
|
+
name: "skill_execute",
|
|
81
|
+
description: getSkillExecuteDescription(),
|
|
82
|
+
schema: z.object({
|
|
83
|
+
skillName: z.string().describe("Exact name of the sync skill to use"),
|
|
84
|
+
objective: z.string().describe("Clear description of what to accomplish"),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// skill_delegate - Asynchronous execution (background task)
|
|
89
|
+
// ============================================================================
|
|
90
|
+
/**
|
|
91
|
+
* Generates the skill_delegate tool description dynamically with async skills.
|
|
92
|
+
*/
|
|
93
|
+
export function getSkillDelegateDescription() {
|
|
94
|
+
const registry = SkillRegistry.getInstance();
|
|
95
|
+
const asyncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'async');
|
|
96
|
+
const skillList = asyncSkills.length > 0
|
|
97
|
+
? asyncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
98
|
+
: '(no async skills enabled)';
|
|
99
|
+
return `Delegate a task to Keymaker as a background job. You will be notified when complete.
|
|
100
|
+
|
|
101
|
+
Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
|
|
102
|
+
|
|
103
|
+
Available async skills:
|
|
104
|
+
${skillList}
|
|
105
|
+
|
|
106
|
+
Use this for long-running skills like builds, deployments, or batch processing.`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Tool that Oracle uses to delegate tasks to Keymaker via async task queue.
|
|
110
|
+
* Keymaker will execute the skill instructions in background.
|
|
111
|
+
*/
|
|
112
|
+
export const SkillDelegateTool = tool(async ({ skillName, objective }) => {
|
|
113
|
+
try {
|
|
114
|
+
const display = DisplayManager.getInstance();
|
|
115
|
+
const registry = SkillRegistry.getInstance();
|
|
116
|
+
// Validate skill exists and is enabled
|
|
117
|
+
const skill = registry.get(skillName);
|
|
118
|
+
if (!skill) {
|
|
119
|
+
const available = registry.getEnabled().map(s => s.name).join(', ');
|
|
120
|
+
return `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
|
|
121
|
+
}
|
|
122
|
+
if (!skill.enabled) {
|
|
123
|
+
return `Error: Skill "${skillName}" is disabled.`;
|
|
124
|
+
}
|
|
125
|
+
if (skill.execution_mode !== 'async') {
|
|
126
|
+
return `Error: Skill "${skillName}" is sync. Use skill_execute instead for immediate results.`;
|
|
127
|
+
}
|
|
128
|
+
// Check for duplicate delegation
|
|
129
|
+
const existingAck = TaskRequestContext.findDuplicateDelegation("keymaker", `${skillName}:${objective}`);
|
|
130
|
+
if (existingAck) {
|
|
131
|
+
display.log(`Keymaker delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
|
|
132
|
+
source: "SkillDelegateTool",
|
|
133
|
+
level: "info",
|
|
134
|
+
});
|
|
135
|
+
return `Task ${existingAck.task_id} already queued for Keymaker (${skillName}) execution.`;
|
|
136
|
+
}
|
|
137
|
+
if (!TaskRequestContext.canEnqueueDelegation()) {
|
|
138
|
+
display.log(`Keymaker delegation blocked by per-turn limit.`, {
|
|
139
|
+
source: "SkillDelegateTool",
|
|
140
|
+
level: "warning",
|
|
141
|
+
});
|
|
142
|
+
return "Delegation limit reached for this user turn. Wait for current tasks to complete.";
|
|
143
|
+
}
|
|
144
|
+
const ctx = TaskRequestContext.get();
|
|
145
|
+
const repository = TaskRepository.getInstance();
|
|
146
|
+
// Store skill name in context as JSON
|
|
147
|
+
const taskContext = JSON.stringify({ skill: skillName });
|
|
148
|
+
const created = repository.createTask({
|
|
149
|
+
agent: "keymaker",
|
|
150
|
+
input: objective,
|
|
151
|
+
context: taskContext,
|
|
152
|
+
origin_channel: ctx?.origin_channel ?? "api",
|
|
153
|
+
session_id: ctx?.session_id ?? "default",
|
|
154
|
+
origin_message_id: ctx?.origin_message_id ?? null,
|
|
155
|
+
origin_user_id: ctx?.origin_user_id ?? null,
|
|
156
|
+
max_attempts: 3,
|
|
157
|
+
});
|
|
158
|
+
TaskRequestContext.setDelegationAck({
|
|
159
|
+
task_id: created.id,
|
|
160
|
+
agent: "keymaker",
|
|
161
|
+
task: `${skillName}:${objective}`,
|
|
162
|
+
});
|
|
163
|
+
display.log(`Keymaker task created: ${created.id} (skill: ${skillName})`, {
|
|
164
|
+
source: "SkillDelegateTool",
|
|
165
|
+
level: "info",
|
|
166
|
+
meta: {
|
|
167
|
+
agent: created.agent,
|
|
168
|
+
skill: skillName,
|
|
169
|
+
origin_channel: created.origin_channel,
|
|
170
|
+
session_id: created.session_id,
|
|
171
|
+
input: created.input,
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return `Task ${created.id} queued for Keymaker (skill: ${skillName}). You will be notified when complete.`;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const display = DisplayManager.getInstance();
|
|
178
|
+
display.log(`SkillDelegateTool error: ${err.message}`, {
|
|
179
|
+
source: "SkillDelegateTool",
|
|
180
|
+
level: "error",
|
|
181
|
+
});
|
|
182
|
+
return `Keymaker task enqueue failed: ${err.message}`;
|
|
183
|
+
}
|
|
184
|
+
}, {
|
|
185
|
+
name: "skill_delegate",
|
|
186
|
+
description: getSkillDelegateDescription(),
|
|
187
|
+
schema: z.object({
|
|
188
|
+
skillName: z.string().describe("Exact name of the async skill to use"),
|
|
189
|
+
objective: z.string().describe("Clear description of what Keymaker should accomplish"),
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Utility functions
|
|
194
|
+
// ============================================================================
|
|
195
|
+
/**
|
|
196
|
+
* Updates both skill tool descriptions with current skill list.
|
|
197
|
+
* Should be called after skills are loaded/reloaded.
|
|
198
|
+
*/
|
|
199
|
+
export function updateSkillToolDescriptions() {
|
|
200
|
+
SkillExecuteTool.description = getSkillExecuteDescription();
|
|
201
|
+
SkillDelegateTool.description = getSkillDelegateDescription();
|
|
202
|
+
}
|
|
203
|
+
// Backwards compatibility alias
|
|
204
|
+
export const updateSkillDelegateDescription = updateSkillToolDescriptions;
|
|
@@ -3,6 +3,7 @@ import { DisplayManager } from '../display.js';
|
|
|
3
3
|
import { Apoc } from '../apoc.js';
|
|
4
4
|
import { Neo } from '../neo.js';
|
|
5
5
|
import { Trinity } from '../trinity.js';
|
|
6
|
+
import { executeKeymakerTask } from '../keymaker.js';
|
|
6
7
|
import { TaskRepository } from './repository.js';
|
|
7
8
|
export class TaskWorker {
|
|
8
9
|
workerId;
|
|
@@ -71,6 +72,27 @@ export class TaskWorker {
|
|
|
71
72
|
output = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
|
|
72
73
|
break;
|
|
73
74
|
}
|
|
75
|
+
case 'keymaker': {
|
|
76
|
+
// Parse skill name from context JSON
|
|
77
|
+
let skillName = 'unknown';
|
|
78
|
+
if (task.context) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(task.context);
|
|
81
|
+
skillName = parsed.skill || 'unknown';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// context is not JSON, use as skill name directly for backwards compat
|
|
85
|
+
skillName = task.context;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
output = await executeKeymakerTask(skillName, task.input, {
|
|
89
|
+
origin_channel: task.origin_channel,
|
|
90
|
+
session_id: task.session_id,
|
|
91
|
+
origin_message_id: task.origin_message_id ?? undefined,
|
|
92
|
+
origin_user_id: task.origin_user_id ?? undefined,
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
74
96
|
default: {
|
|
75
97
|
throw new Error(`Unknown task agent: ${task.agent}`);
|
|
76
98
|
}
|