morpheus-cli 0.9.4 → 0.9.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 +63 -43
- package/dist/channels/discord.js +3 -6
- package/dist/channels/telegram.js +3 -6
- package/dist/cli/commands/restart.js +15 -0
- package/dist/cli/commands/start.js +16 -0
- package/dist/config/manager.js +61 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +11 -3
- package/dist/http/api.js +3 -0
- package/dist/http/routers/link.js +239 -0
- package/dist/http/routers/skills.js +1 -8
- package/dist/http/routers/smiths.js +14 -4
- package/dist/runtime/apoc.js +1 -1
- package/dist/runtime/audit/repository.js +1 -1
- package/dist/runtime/link-chunker.js +214 -0
- package/dist/runtime/link-repository.js +301 -0
- package/dist/runtime/link-search.js +298 -0
- package/dist/runtime/link-worker.js +284 -0
- package/dist/runtime/link.js +295 -0
- package/dist/runtime/memory/sati/service.js +1 -1
- package/dist/runtime/neo.js +1 -1
- package/dist/runtime/oracle.js +81 -44
- package/dist/runtime/scaffold.js +4 -17
- package/dist/runtime/skills/__tests__/loader.test.js +7 -10
- package/dist/runtime/skills/__tests__/registry.test.js +2 -18
- package/dist/runtime/skills/__tests__/tool.test.js +55 -224
- package/dist/runtime/skills/index.js +1 -2
- package/dist/runtime/skills/loader.js +0 -2
- package/dist/runtime/skills/registry.js +8 -20
- package/dist/runtime/skills/schema.js +0 -4
- package/dist/runtime/skills/tool.js +42 -209
- package/dist/runtime/smiths/delegator.js +1 -1
- package/dist/runtime/smiths/registry.js +1 -1
- package/dist/runtime/tasks/worker.js +12 -44
- package/dist/runtime/trinity.js +1 -1
- package/dist/types/config.js +14 -0
- package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
- package/dist/ui/assets/{Chat-5AeRYuRj.js → Chat-CK5sNcQ1.js} +8 -8
- package/dist/ui/assets/{Chronos-BrKldYVw.js → Chronos-m2h--GEe.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-DsbS3XkJ.js → ConfirmationModal-Dd5pUJme.js} +1 -1
- package/dist/ui/assets/{Dashboard-DvrTXLdo.js → Dashboard-ODwl7d-a.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-BfSjv04R.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
- package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
- package/dist/ui/assets/{Logs-B0ZYWs5x.js → Logs-Dc9Z2LBj.js} +1 -1
- package/dist/ui/assets/{MCPManager-BwHGTeNs.js → MCPManager-CMkb8vMn.js} +1 -1
- package/dist/ui/assets/{ModelPricing-CYhGRQr8.js → ModelPricing-DtHPPbEQ.js} +1 -1
- package/dist/ui/assets/{Notifications-BYMAtVMq.js → Notifications-BPvo-DWP.js} +1 -1
- package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
- package/dist/ui/assets/{SatiMemories-I1vsYtP2.js → SatiMemories-BUPu1Lxr.js} +1 -1
- package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
- package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
- package/dist/ui/assets/{Skills-lGU3I5DO.js → Skills-BUlvJgJ4.js} +1 -1
- package/dist/ui/assets/Smiths-CDtJdY0I.js +1 -0
- package/dist/ui/assets/{Tasks-Bz92GPWK.js → Tasks-DK_cOsNK.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-BUY-3j7Q.js → TrinityDatabases-X07by-19.js} +1 -1
- package/dist/ui/assets/{UsageStats-Dr5eSgJc.js → UsageStats-dYcgckLq.js} +1 -1
- package/dist/ui/assets/{WebhookManager-DIASAC-1.js → WebhookManager-DDw5eX2R.js} +1 -1
- package/dist/ui/assets/{audit-CcAEDbZh.js → audit-DZ5WLUEm.js} +1 -1
- package/dist/ui/assets/{chronos-2Z9E96_1.js → chronos-B_HI4mlq.js} +1 -1
- package/dist/ui/assets/{config-DdfK4DX6.js → config-B-YxlVrc.js} +1 -1
- package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
- package/dist/ui/assets/{index-Dpd1Mkgp.js → index-DfJwcKqG.js} +5 -5
- package/dist/ui/assets/{mcp-BWMt8aY7.js → mcp-k-_pwbqA.js} +1 -1
- package/dist/ui/assets/{skills-D7JjK7JH.js → skills-xMXangks.js} +1 -1
- package/dist/ui/assets/{stats-DoIhtLot.js → stats-C4QZIv5O.js} +1 -1
- package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/runtime/__tests__/keymaker.test.js +0 -148
- package/dist/runtime/keymaker.js +0 -157
- package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +0 -1
- package/dist/ui/assets/SessionAudit-BCecQWde.js +0 -9
- package/dist/ui/assets/Settings-Cu4D-7tb.js +0 -47
- package/dist/ui/assets/Smiths-DnEH3nID.js +0 -1
- package/dist/ui/assets/index-D4fzIKy1.css +0 -1
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
// Use vi.hoisted to define mocks before they're used in vi.mock calls
|
|
3
|
-
const { mockRegistry,
|
|
3
|
+
const { mockRegistry, mockAudit, mockContext } = vi.hoisted(() => ({
|
|
4
4
|
mockRegistry: {
|
|
5
5
|
get: vi.fn(),
|
|
6
6
|
getEnabled: vi.fn(() => []),
|
|
7
|
-
getContent: vi.fn(() => null),
|
|
8
7
|
},
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
mockDisplay: {
|
|
13
|
-
log: vi.fn(),
|
|
8
|
+
mockAudit: {
|
|
9
|
+
insert: vi.fn(),
|
|
14
10
|
},
|
|
15
11
|
mockContext: {
|
|
16
12
|
get: vi.fn(() => ({
|
|
@@ -19,13 +15,6 @@ const { mockRegistry, mockRepository, mockDisplay, mockContext, mockKeymaker } =
|
|
|
19
15
|
origin_message_id: '123',
|
|
20
16
|
origin_user_id: 'user-1',
|
|
21
17
|
})),
|
|
22
|
-
findDuplicateDelegation: vi.fn(() => null),
|
|
23
|
-
canEnqueueDelegation: vi.fn(() => true),
|
|
24
|
-
setDelegationAck: vi.fn(),
|
|
25
|
-
},
|
|
26
|
-
mockKeymaker: {
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
28
|
-
executeKeymakerTask: vi.fn((_skill, _obj, _ctx) => Promise.resolve('Keymaker result')),
|
|
29
18
|
},
|
|
30
19
|
}));
|
|
31
20
|
vi.mock('../registry.js', () => ({
|
|
@@ -33,234 +22,76 @@ vi.mock('../registry.js', () => ({
|
|
|
33
22
|
getInstance: () => mockRegistry,
|
|
34
23
|
},
|
|
35
24
|
}));
|
|
36
|
-
vi.mock('../../
|
|
37
|
-
|
|
38
|
-
getInstance: () =>
|
|
25
|
+
vi.mock('../../audit/repository.js', () => ({
|
|
26
|
+
AuditRepository: {
|
|
27
|
+
getInstance: () => mockAudit,
|
|
39
28
|
},
|
|
40
29
|
}));
|
|
41
30
|
vi.mock('../../tasks/context.js', () => ({
|
|
42
31
|
TaskRequestContext: mockContext,
|
|
43
32
|
}));
|
|
44
|
-
vi.mock('../../display.js', () => ({
|
|
45
|
-
DisplayManager: {
|
|
46
|
-
getInstance: () => mockDisplay,
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
vi.mock('../../keymaker.js', () => ({
|
|
50
|
-
executeKeymakerTask: (skillName, objective, context) => mockKeymaker.executeKeymakerTask(skillName, objective, context),
|
|
51
|
-
}));
|
|
52
33
|
// Now import the module under test
|
|
53
|
-
import {
|
|
54
|
-
describe('
|
|
34
|
+
import { createLoadSkillTool } from '../tool.js';
|
|
35
|
+
describe('load_skill tool', () => {
|
|
36
|
+
let loadSkillTool;
|
|
55
37
|
beforeEach(() => {
|
|
56
38
|
vi.clearAllMocks();
|
|
57
39
|
mockRegistry.get.mockReset();
|
|
58
40
|
mockRegistry.getEnabled.mockReset();
|
|
59
|
-
mockKeymaker.executeKeymakerTask.mockReset();
|
|
60
|
-
mockKeymaker.executeKeymakerTask.mockResolvedValue('Keymaker result');
|
|
61
41
|
mockRegistry.getEnabled.mockReturnValue([]);
|
|
42
|
+
loadSkillTool = createLoadSkillTool();
|
|
62
43
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]);
|
|
70
|
-
const description = getSkillExecuteDescription();
|
|
71
|
-
expect(description).toContain('code-review: Review code for issues');
|
|
72
|
-
expect(description).toContain('git-ops: Git operations helper');
|
|
73
|
-
expect(description).not.toContain('deploy');
|
|
74
|
-
});
|
|
75
|
-
it('should show no sync skills message when none enabled', () => {
|
|
76
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
77
|
-
const description = getSkillExecuteDescription();
|
|
78
|
-
expect(description).toContain('(no sync skills enabled)');
|
|
44
|
+
it('should return skill content for valid enabled skill', async () => {
|
|
45
|
+
mockRegistry.get.mockReturnValue({
|
|
46
|
+
name: 'test-skill',
|
|
47
|
+
description: 'Test',
|
|
48
|
+
enabled: true,
|
|
49
|
+
content: '# Instructions\n\nDo the thing.',
|
|
79
50
|
});
|
|
51
|
+
const result = await loadSkillTool.invoke({ skillName: 'test-skill' });
|
|
52
|
+
expect(result).toContain('Loaded skill: test-skill');
|
|
53
|
+
expect(result).toContain('# Instructions');
|
|
54
|
+
expect(result).toContain('Do the thing.');
|
|
80
55
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
execution_mode: 'sync',
|
|
88
|
-
content: 'Instructions here',
|
|
89
|
-
});
|
|
90
|
-
mockRegistry.getEnabled.mockReturnValue([{ name: 'test-skill', execution_mode: 'sync' }]);
|
|
91
|
-
const result = await SkillExecuteTool.invoke({
|
|
92
|
-
skillName: 'test-skill',
|
|
93
|
-
objective: 'do the thing',
|
|
94
|
-
});
|
|
95
|
-
expect(mockKeymaker.executeKeymakerTask).toHaveBeenCalledWith('test-skill', 'do the thing', expect.objectContaining({
|
|
96
|
-
origin_channel: 'telegram',
|
|
97
|
-
session_id: 'test-session',
|
|
98
|
-
}));
|
|
99
|
-
expect(result).toBe('Keymaker result');
|
|
100
|
-
});
|
|
101
|
-
it('should return error for non-existent skill', async () => {
|
|
102
|
-
mockRegistry.get.mockReturnValue(undefined);
|
|
103
|
-
mockRegistry.getEnabled.mockReturnValue([
|
|
104
|
-
{ name: 'other-skill', execution_mode: 'sync' },
|
|
105
|
-
]);
|
|
106
|
-
const result = await SkillExecuteTool.invoke({
|
|
107
|
-
skillName: 'non-existent',
|
|
108
|
-
objective: 'do something',
|
|
109
|
-
});
|
|
110
|
-
expect(result).toContain('Error');
|
|
111
|
-
expect(result).toContain('not found');
|
|
112
|
-
expect(result).toContain('other-skill');
|
|
113
|
-
});
|
|
114
|
-
it('should return error for async skill', async () => {
|
|
115
|
-
mockRegistry.get.mockReturnValue({
|
|
116
|
-
name: 'async-skill',
|
|
117
|
-
description: 'Async only',
|
|
118
|
-
enabled: true,
|
|
119
|
-
execution_mode: 'async',
|
|
120
|
-
});
|
|
121
|
-
const result = await SkillExecuteTool.invoke({
|
|
122
|
-
skillName: 'async-skill',
|
|
123
|
-
objective: 'do something',
|
|
124
|
-
});
|
|
125
|
-
expect(result).toContain('Error');
|
|
126
|
-
expect(result).toContain('async-only');
|
|
127
|
-
expect(result).toContain('skill_delegate');
|
|
56
|
+
it('should emit skill_loaded audit event', async () => {
|
|
57
|
+
mockRegistry.get.mockReturnValue({
|
|
58
|
+
name: 'test-skill',
|
|
59
|
+
description: 'Test',
|
|
60
|
+
enabled: true,
|
|
61
|
+
content: 'Instructions',
|
|
128
62
|
});
|
|
63
|
+
await loadSkillTool.invoke({ skillName: 'test-skill' });
|
|
64
|
+
expect(mockAudit.insert).toHaveBeenCalledWith(expect.objectContaining({
|
|
65
|
+
event_type: 'skill_loaded',
|
|
66
|
+
agent: 'oracle',
|
|
67
|
+
tool_name: 'test-skill',
|
|
68
|
+
status: 'success',
|
|
69
|
+
session_id: 'test-session',
|
|
70
|
+
}));
|
|
129
71
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
mockContext.findDuplicateDelegation.mockReturnValue(null);
|
|
139
|
-
mockContext.canEnqueueDelegation.mockReturnValue(true);
|
|
140
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
72
|
+
it('should return error for non-existent skill', async () => {
|
|
73
|
+
mockRegistry.get.mockReturnValue(undefined);
|
|
74
|
+
mockRegistry.getEnabled.mockReturnValue([
|
|
75
|
+
{ name: 'other-skill', description: 'Other' },
|
|
76
|
+
]);
|
|
77
|
+
const result = await loadSkillTool.invoke({ skillName: 'non-existent' });
|
|
78
|
+
expect(result).toContain('not found');
|
|
79
|
+
expect(result).toContain('other-skill');
|
|
141
80
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
{ name: 'code-review', description: 'Review code', execution_mode: 'sync' }, // should not appear
|
|
148
|
-
]);
|
|
149
|
-
const description = getSkillDelegateDescription();
|
|
150
|
-
expect(description).toContain('deploy-staging: Deploy to staging');
|
|
151
|
-
expect(description).toContain('batch-process: Process batch jobs');
|
|
152
|
-
expect(description).not.toContain('code-review');
|
|
153
|
-
});
|
|
154
|
-
it('should show no async skills message when none enabled', () => {
|
|
155
|
-
mockRegistry.getEnabled.mockReturnValue([]);
|
|
156
|
-
const description = getSkillDelegateDescription();
|
|
157
|
-
expect(description).toContain('(no async skills enabled)');
|
|
81
|
+
it('should return error for disabled skill', async () => {
|
|
82
|
+
mockRegistry.get.mockReturnValue({
|
|
83
|
+
name: 'disabled-skill',
|
|
84
|
+
description: 'Disabled',
|
|
85
|
+
enabled: false,
|
|
158
86
|
});
|
|
87
|
+
const result = await loadSkillTool.invoke({ skillName: 'disabled-skill' });
|
|
88
|
+
expect(result).toContain('disabled');
|
|
159
89
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
execution_mode: 'async',
|
|
167
|
-
});
|
|
168
|
-
mockRegistry.getEnabled.mockReturnValue([{ name: 'deploy-staging', execution_mode: 'async' }]);
|
|
169
|
-
mockRepository.createTask.mockReturnValue({
|
|
170
|
-
id: 'task-123',
|
|
171
|
-
agent: 'keymaker',
|
|
172
|
-
status: 'pending',
|
|
173
|
-
});
|
|
174
|
-
const result = await SkillDelegateTool.invoke({
|
|
175
|
-
skillName: 'deploy-staging',
|
|
176
|
-
objective: 'deploy to staging',
|
|
177
|
-
});
|
|
178
|
-
expect(mockRepository.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
179
|
-
agent: 'keymaker',
|
|
180
|
-
input: 'deploy to staging',
|
|
181
|
-
context: JSON.stringify({ skill: 'deploy-staging' }),
|
|
182
|
-
origin_channel: 'telegram',
|
|
183
|
-
session_id: 'test-session',
|
|
184
|
-
}));
|
|
185
|
-
expect(result).toContain('task-123');
|
|
186
|
-
expect(result).toContain('queued');
|
|
187
|
-
});
|
|
188
|
-
it('should return error for sync skill', async () => {
|
|
189
|
-
mockRegistry.get.mockReturnValue({
|
|
190
|
-
name: 'sync-skill',
|
|
191
|
-
description: 'Sync',
|
|
192
|
-
enabled: true,
|
|
193
|
-
execution_mode: 'sync',
|
|
194
|
-
});
|
|
195
|
-
const result = await SkillDelegateTool.invoke({
|
|
196
|
-
skillName: 'sync-skill',
|
|
197
|
-
objective: 'do something',
|
|
198
|
-
});
|
|
199
|
-
expect(result).toContain('Error');
|
|
200
|
-
expect(result).toContain('sync');
|
|
201
|
-
expect(result).toContain('skill_execute');
|
|
202
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
203
|
-
});
|
|
204
|
-
it('should return error for non-existent skill', async () => {
|
|
205
|
-
mockRegistry.get.mockReturnValue(undefined);
|
|
206
|
-
mockRegistry.getEnabled.mockReturnValue([
|
|
207
|
-
{ name: 'other-skill', execution_mode: 'async' },
|
|
208
|
-
]);
|
|
209
|
-
const result = await SkillDelegateTool.invoke({
|
|
210
|
-
skillName: 'non-existent',
|
|
211
|
-
objective: 'do something',
|
|
212
|
-
});
|
|
213
|
-
expect(result).toContain('Error');
|
|
214
|
-
expect(result).toContain('not found');
|
|
215
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
216
|
-
});
|
|
217
|
-
it('should return error for disabled skill', async () => {
|
|
218
|
-
mockRegistry.get.mockReturnValue({
|
|
219
|
-
name: 'disabled-skill',
|
|
220
|
-
description: 'Disabled',
|
|
221
|
-
enabled: false,
|
|
222
|
-
execution_mode: 'async',
|
|
223
|
-
});
|
|
224
|
-
const result = await SkillDelegateTool.invoke({
|
|
225
|
-
skillName: 'disabled-skill',
|
|
226
|
-
objective: 'do something',
|
|
227
|
-
});
|
|
228
|
-
expect(result).toContain('Error');
|
|
229
|
-
expect(result).toContain('disabled');
|
|
230
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
231
|
-
});
|
|
232
|
-
it('should deduplicate delegation requests', async () => {
|
|
233
|
-
mockRegistry.get.mockReturnValue({
|
|
234
|
-
name: 'dup-skill',
|
|
235
|
-
enabled: true,
|
|
236
|
-
execution_mode: 'async',
|
|
237
|
-
});
|
|
238
|
-
mockContext.findDuplicateDelegation.mockReturnValue({
|
|
239
|
-
task_id: 'existing-task',
|
|
240
|
-
agent: 'keymaker',
|
|
241
|
-
task: 'dup-skill:objective',
|
|
242
|
-
});
|
|
243
|
-
const result = await SkillDelegateTool.invoke({
|
|
244
|
-
skillName: 'dup-skill',
|
|
245
|
-
objective: 'objective',
|
|
246
|
-
});
|
|
247
|
-
expect(result).toContain('existing-task');
|
|
248
|
-
expect(result).toContain('already queued');
|
|
249
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
250
|
-
});
|
|
251
|
-
it('should block when delegation limit reached', async () => {
|
|
252
|
-
mockRegistry.get.mockReturnValue({
|
|
253
|
-
name: 'limit-skill',
|
|
254
|
-
enabled: true,
|
|
255
|
-
execution_mode: 'async',
|
|
256
|
-
});
|
|
257
|
-
mockContext.canEnqueueDelegation.mockReturnValue(false);
|
|
258
|
-
const result = await SkillDelegateTool.invoke({
|
|
259
|
-
skillName: 'limit-skill',
|
|
260
|
-
objective: 'objective',
|
|
261
|
-
});
|
|
262
|
-
expect(result).toContain('limit reached');
|
|
263
|
-
expect(mockRepository.createTask).not.toHaveBeenCalled();
|
|
264
|
-
});
|
|
90
|
+
it('should show no skills available when none enabled', async () => {
|
|
91
|
+
mockRegistry.get.mockReturnValue(undefined);
|
|
92
|
+
mockRegistry.getEnabled.mockReturnValue([]);
|
|
93
|
+
const result = await loadSkillTool.invoke({ skillName: 'anything' });
|
|
94
|
+
expect(result).toContain('not found');
|
|
95
|
+
expect(result).toContain('none');
|
|
265
96
|
});
|
|
266
97
|
});
|
|
@@ -4,5 +4,4 @@
|
|
|
4
4
|
export { SkillRegistry } from './registry.js';
|
|
5
5
|
export { SkillLoader } from './loader.js';
|
|
6
6
|
export { SkillMetadataSchema } from './schema.js';
|
|
7
|
-
export {
|
|
8
|
-
} from './tool.js';
|
|
7
|
+
export { createLoadSkillTool } from './tool.js';
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* ---
|
|
7
7
|
* name: my-skill
|
|
8
8
|
* description: What this skill does
|
|
9
|
-
* execution_mode: sync
|
|
10
9
|
* ---
|
|
11
10
|
*
|
|
12
11
|
* # Skill Instructions...
|
|
@@ -200,7 +199,6 @@ export class SkillLoader {
|
|
|
200
199
|
dirName,
|
|
201
200
|
content: content.trim(),
|
|
202
201
|
enabled: metadata.enabled ?? true,
|
|
203
|
-
execution_mode: metadata.execution_mode ?? 'sync',
|
|
204
202
|
};
|
|
205
203
|
return { skill };
|
|
206
204
|
}
|
|
@@ -107,26 +107,14 @@ export class SkillRegistry {
|
|
|
107
107
|
if (enabled.length === 0) {
|
|
108
108
|
return '';
|
|
109
109
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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');
|
|
110
|
+
const skillList = enabled
|
|
111
|
+
.map(s => `- **${s.name}**: ${s.description}`)
|
|
112
|
+
.join('\n');
|
|
113
|
+
return `## Available Skills
|
|
114
|
+
|
|
115
|
+
${skillList}
|
|
116
|
+
|
|
117
|
+
Use the \`load_skill\` tool when you need detailed instructions for handling a specific type of request.`;
|
|
130
118
|
}
|
|
131
119
|
/**
|
|
132
120
|
* Get skill names for tool description
|
|
@@ -15,10 +15,6 @@ export const SkillMetadataSchema = z.object({
|
|
|
15
15
|
.string()
|
|
16
16
|
.min(1, 'Description is required')
|
|
17
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
18
|
version: z
|
|
23
19
|
.string()
|
|
24
20
|
.regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., 1.0.0)')
|
|
@@ -1,229 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill
|
|
2
|
+
* Skill Tool — load_skill
|
|
3
|
+
*
|
|
4
|
+
* Oracle uses this tool to load skill content into context on-demand.
|
|
5
|
+
* Replaces the old skill_execute/skill_delegate + Keymaker pattern.
|
|
3
6
|
*/
|
|
4
7
|
import { tool } from "@langchain/core/tools";
|
|
5
8
|
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
9
|
import { SkillRegistry } from "./registry.js";
|
|
10
|
-
import { executeKeymakerTask } from "../keymaker.js";
|
|
11
10
|
import { AuditRepository } from "../audit/repository.js";
|
|
12
|
-
|
|
13
|
-
// skill_execute - Synchronous execution
|
|
14
|
-
// ============================================================================
|
|
15
|
-
/**
|
|
16
|
-
* Generates the skill_execute tool description dynamically with sync skills.
|
|
17
|
-
*/
|
|
18
|
-
export function getSkillExecuteDescription() {
|
|
19
|
-
const registry = SkillRegistry.getInstance();
|
|
20
|
-
const syncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'sync');
|
|
21
|
-
const skillList = syncSkills.length > 0
|
|
22
|
-
? syncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
23
|
-
: '(no sync skills enabled)';
|
|
24
|
-
return `Execute a skill synchronously using Keymaker. The result is returned immediately.
|
|
25
|
-
|
|
26
|
-
Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
|
|
27
|
-
|
|
28
|
-
Available sync skills:
|
|
29
|
-
${skillList}
|
|
30
|
-
|
|
31
|
-
Use this for skills that need immediate results in the conversation.`;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Tool that Oracle uses to execute skills synchronously via Keymaker.
|
|
35
|
-
* Result is returned directly to Oracle for inclusion in the response.
|
|
36
|
-
*/
|
|
37
|
-
export const SkillExecuteTool = tool(async ({ skillName, objective }) => {
|
|
38
|
-
const display = DisplayManager.getInstance();
|
|
39
|
-
const registry = SkillRegistry.getInstance();
|
|
40
|
-
// Validate skill exists and is enabled
|
|
41
|
-
const skill = registry.get(skillName);
|
|
42
|
-
if (!skill) {
|
|
43
|
-
const available = registry.getEnabled().map(s => s.name).join(', ');
|
|
44
|
-
return `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
|
|
45
|
-
}
|
|
46
|
-
if (!skill.enabled) {
|
|
47
|
-
return `Error: Skill "${skillName}" is disabled.`;
|
|
48
|
-
}
|
|
49
|
-
if (skill.execution_mode === 'async') {
|
|
50
|
-
return `Error: Skill "${skillName}" is async-only. Use skill_delegate instead.`;
|
|
51
|
-
}
|
|
52
|
-
display.log(`Executing skill "${skillName}" synchronously...`, {
|
|
53
|
-
source: "SkillExecuteTool",
|
|
54
|
-
level: "info",
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
const ctx = TaskRequestContext.get();
|
|
58
|
-
const sessionId = ctx?.session_id ?? "default";
|
|
59
|
-
const taskContext = {
|
|
60
|
-
origin_channel: ctx?.origin_channel ?? "api",
|
|
61
|
-
session_id: sessionId,
|
|
62
|
-
origin_message_id: ctx?.origin_message_id,
|
|
63
|
-
origin_user_id: ctx?.origin_user_id,
|
|
64
|
-
};
|
|
65
|
-
// Execute Keymaker directly (synchronous)
|
|
66
|
-
const result = await executeKeymakerTask(skillName, objective, taskContext);
|
|
67
|
-
display.log(`Skill "${skillName}" completed successfully.`, {
|
|
68
|
-
source: "SkillExecuteTool",
|
|
69
|
-
level: "info",
|
|
70
|
-
});
|
|
71
|
-
// Emit audit events for sync execution (async path is handled by TaskWorker)
|
|
72
|
-
const audit = AuditRepository.getInstance();
|
|
73
|
-
if (result.usage && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
|
|
74
|
-
audit.insert({
|
|
75
|
-
session_id: sessionId,
|
|
76
|
-
event_type: 'llm_call',
|
|
77
|
-
agent: 'keymaker',
|
|
78
|
-
provider: result.usage.provider,
|
|
79
|
-
model: result.usage.model,
|
|
80
|
-
input_tokens: result.usage.inputTokens,
|
|
81
|
-
output_tokens: result.usage.outputTokens,
|
|
82
|
-
duration_ms: result.usage.durationMs,
|
|
83
|
-
status: 'success',
|
|
84
|
-
metadata: { step_count: result.usage.stepCount },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
audit.insert({
|
|
88
|
-
session_id: sessionId,
|
|
89
|
-
event_type: 'skill_executed',
|
|
90
|
-
agent: 'keymaker',
|
|
91
|
-
tool_name: skillName,
|
|
92
|
-
duration_ms: result.usage?.durationMs,
|
|
93
|
-
status: 'success',
|
|
94
|
-
});
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
display.log(`Skill execution error: ${err.message}`, {
|
|
99
|
-
source: "SkillExecuteTool",
|
|
100
|
-
level: "error",
|
|
101
|
-
});
|
|
102
|
-
return `Skill execution failed: ${err.message}`;
|
|
103
|
-
}
|
|
104
|
-
}, {
|
|
105
|
-
name: "skill_execute",
|
|
106
|
-
description: getSkillExecuteDescription(),
|
|
107
|
-
schema: z.object({
|
|
108
|
-
skillName: z.string().describe("Exact name of the sync skill to use"),
|
|
109
|
-
objective: z.string().describe("Clear description of what to accomplish"),
|
|
110
|
-
}),
|
|
111
|
-
});
|
|
112
|
-
// ============================================================================
|
|
113
|
-
// skill_delegate - Asynchronous execution (background task)
|
|
114
|
-
// ============================================================================
|
|
115
|
-
/**
|
|
116
|
-
* Generates the skill_delegate tool description dynamically with async skills.
|
|
117
|
-
*/
|
|
118
|
-
export function getSkillDelegateDescription() {
|
|
119
|
-
const registry = SkillRegistry.getInstance();
|
|
120
|
-
const asyncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'async');
|
|
121
|
-
const skillList = asyncSkills.length > 0
|
|
122
|
-
? asyncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
123
|
-
: '(no async skills enabled)';
|
|
124
|
-
return `Delegate a task to Keymaker as a background job. You will be notified when complete.
|
|
125
|
-
|
|
126
|
-
Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
|
|
127
|
-
|
|
128
|
-
Available async skills:
|
|
129
|
-
${skillList}
|
|
130
|
-
|
|
131
|
-
Use this for long-running skills like builds, deployments, or batch processing.`;
|
|
132
|
-
}
|
|
11
|
+
import { TaskRequestContext } from "../tasks/context.js";
|
|
133
12
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
13
|
+
* Creates the load_skill tool for Oracle.
|
|
14
|
+
* Returns skill content as context — Oracle handles execution with its own tools.
|
|
136
15
|
*/
|
|
137
|
-
export
|
|
138
|
-
|
|
139
|
-
const display = DisplayManager.getInstance();
|
|
16
|
+
export function createLoadSkillTool() {
|
|
17
|
+
return tool(async ({ skillName }) => {
|
|
140
18
|
const registry = SkillRegistry.getInstance();
|
|
141
|
-
// Validate skill exists and is enabled
|
|
142
19
|
const skill = registry.get(skillName);
|
|
143
20
|
if (!skill) {
|
|
144
21
|
const available = registry.getEnabled().map(s => s.name).join(', ');
|
|
145
|
-
return `
|
|
22
|
+
return `Skill '${skillName}' not found. Available skills: ${available || 'none'}`;
|
|
146
23
|
}
|
|
147
24
|
if (!skill.enabled) {
|
|
148
|
-
return `
|
|
149
|
-
}
|
|
150
|
-
if (skill.execution_mode !== 'async') {
|
|
151
|
-
return `Error: Skill "${skillName}" is sync. Use skill_execute instead for immediate results.`;
|
|
25
|
+
return `Skill '${skillName}' is disabled.`;
|
|
152
26
|
}
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
display.log(`Keymaker delegation blocked by per-turn limit.`, {
|
|
164
|
-
source: "SkillDelegateTool",
|
|
165
|
-
level: "warning",
|
|
27
|
+
// Emit audit event
|
|
28
|
+
try {
|
|
29
|
+
const ctx = TaskRequestContext.get();
|
|
30
|
+
const sessionId = ctx?.session_id ?? 'default';
|
|
31
|
+
AuditRepository.getInstance().insert({
|
|
32
|
+
session_id: sessionId,
|
|
33
|
+
event_type: 'skill_loaded',
|
|
34
|
+
agent: 'oracle',
|
|
35
|
+
tool_name: skillName,
|
|
36
|
+
status: 'success',
|
|
166
37
|
});
|
|
167
|
-
return "Delegation limit reached for this user turn. Wait for current tasks to complete.";
|
|
168
38
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
origin_message_id: ctx?.origin_message_id ?? null,
|
|
180
|
-
origin_user_id: ctx?.origin_user_id ?? null,
|
|
181
|
-
max_attempts: 3,
|
|
182
|
-
});
|
|
183
|
-
TaskRequestContext.setDelegationAck({
|
|
184
|
-
task_id: created.id,
|
|
185
|
-
agent: "keymaker",
|
|
186
|
-
task: `${skillName}:${objective}`,
|
|
187
|
-
});
|
|
188
|
-
display.log(`Keymaker task created: ${created.id} (skill: ${skillName})`, {
|
|
189
|
-
source: "SkillDelegateTool",
|
|
190
|
-
level: "info",
|
|
191
|
-
meta: {
|
|
192
|
-
agent: created.agent,
|
|
193
|
-
skill: skillName,
|
|
194
|
-
origin_channel: created.origin_channel,
|
|
195
|
-
session_id: created.session_id,
|
|
196
|
-
input: created.input,
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
return `Task ${created.id} queued for Keymaker (skill: ${skillName}). You will be notified when complete.`;
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
const display = DisplayManager.getInstance();
|
|
203
|
-
display.log(`SkillDelegateTool error: ${err.message}`, {
|
|
204
|
-
source: "SkillDelegateTool",
|
|
205
|
-
level: "error",
|
|
206
|
-
});
|
|
207
|
-
return `Keymaker task enqueue failed: ${err.message}`;
|
|
208
|
-
}
|
|
209
|
-
}, {
|
|
210
|
-
name: "skill_delegate",
|
|
211
|
-
description: getSkillDelegateDescription(),
|
|
212
|
-
schema: z.object({
|
|
213
|
-
skillName: z.string().describe("Exact name of the async skill to use"),
|
|
214
|
-
objective: z.string().describe("Clear description of what Keymaker should accomplish"),
|
|
215
|
-
}),
|
|
216
|
-
});
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// Utility functions
|
|
219
|
-
// ============================================================================
|
|
39
|
+
catch { /* non-critical */ }
|
|
40
|
+
return `Loaded skill: ${skillName}\n\n${skill.content}`;
|
|
41
|
+
}, {
|
|
42
|
+
name: "load_skill",
|
|
43
|
+
description: buildLoadSkillDescription(),
|
|
44
|
+
schema: z.object({
|
|
45
|
+
skillName: z.string().describe("The name of the skill to load"),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
220
49
|
/**
|
|
221
|
-
*
|
|
222
|
-
* Should be called after skills are loaded/reloaded.
|
|
50
|
+
* Builds the load_skill tool description with available skills.
|
|
223
51
|
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
52
|
+
function buildLoadSkillDescription() {
|
|
53
|
+
const registry = SkillRegistry.getInstance();
|
|
54
|
+
const enabled = registry.getEnabled();
|
|
55
|
+
const skillList = enabled.length > 0
|
|
56
|
+
? enabled.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
57
|
+
: '(no skills available)';
|
|
58
|
+
return `Load a skill's instructions into your context. After loading, follow the instructions to handle the request using your existing tools or delegate to Agents.
|
|
59
|
+
|
|
60
|
+
Available skills:
|
|
61
|
+
${skillList}`;
|
|
227
62
|
}
|
|
228
|
-
// Backwards compatibility alias
|
|
229
|
-
export const updateSkillDelegateDescription = updateSkillToolDescriptions;
|