morpheus-cli 0.9.5 → 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.
Files changed (74) hide show
  1. package/README.md +63 -43
  2. package/dist/channels/discord.js +3 -6
  3. package/dist/channels/telegram.js +3 -6
  4. package/dist/cli/commands/restart.js +15 -0
  5. package/dist/cli/commands/start.js +16 -0
  6. package/dist/config/manager.js +61 -0
  7. package/dist/config/paths.js +1 -0
  8. package/dist/config/schemas.js +11 -3
  9. package/dist/http/api.js +3 -0
  10. package/dist/http/routers/link.js +239 -0
  11. package/dist/http/routers/skills.js +1 -8
  12. package/dist/runtime/apoc.js +1 -1
  13. package/dist/runtime/audit/repository.js +1 -1
  14. package/dist/runtime/link-chunker.js +214 -0
  15. package/dist/runtime/link-repository.js +301 -0
  16. package/dist/runtime/link-search.js +298 -0
  17. package/dist/runtime/link-worker.js +284 -0
  18. package/dist/runtime/link.js +295 -0
  19. package/dist/runtime/memory/sati/service.js +1 -1
  20. package/dist/runtime/neo.js +1 -1
  21. package/dist/runtime/oracle.js +81 -44
  22. package/dist/runtime/scaffold.js +4 -17
  23. package/dist/runtime/skills/__tests__/loader.test.js +7 -10
  24. package/dist/runtime/skills/__tests__/registry.test.js +2 -18
  25. package/dist/runtime/skills/__tests__/tool.test.js +55 -224
  26. package/dist/runtime/skills/index.js +1 -2
  27. package/dist/runtime/skills/loader.js +0 -2
  28. package/dist/runtime/skills/registry.js +8 -20
  29. package/dist/runtime/skills/schema.js +0 -4
  30. package/dist/runtime/skills/tool.js +42 -209
  31. package/dist/runtime/smiths/delegator.js +1 -1
  32. package/dist/runtime/smiths/registry.js +1 -1
  33. package/dist/runtime/tasks/worker.js +12 -44
  34. package/dist/runtime/trinity.js +1 -1
  35. package/dist/types/config.js +14 -0
  36. package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
  37. package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
  38. package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
  39. package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
  40. package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
  41. package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
  42. package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
  43. package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
  44. package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
  45. package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
  46. package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
  47. package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
  48. package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
  49. package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
  50. package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
  51. package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
  52. package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
  53. package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
  54. package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
  55. package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
  56. package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
  57. package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
  58. package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
  59. package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
  60. package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
  61. package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
  62. package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
  63. package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
  64. package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
  65. package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
  66. package/dist/ui/index.html +3 -3
  67. package/dist/ui/sw.js +1 -1
  68. package/package.json +3 -1
  69. package/dist/runtime/__tests__/keymaker.test.js +0 -148
  70. package/dist/runtime/keymaker.js +0 -157
  71. package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
  72. package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
  73. package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
  74. 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, mockRepository, mockDisplay, mockContext, mockKeymaker } = vi.hoisted(() => ({
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
- mockRepository: {
10
- createTask: vi.fn(),
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('../../tasks/repository.js', () => ({
37
- TaskRepository: {
38
- getInstance: () => mockRepository,
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 { SkillExecuteTool, SkillDelegateTool, getSkillExecuteDescription, getSkillDelegateDescription } from '../tool.js';
54
- describe('SkillExecuteTool (sync)', () => {
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
- describe('getSkillExecuteDescription()', () => {
64
- it('should include enabled sync skills in description', () => {
65
- mockRegistry.getEnabled.mockReturnValue([
66
- { name: 'code-review', description: 'Review code for issues', execution_mode: 'sync' },
67
- { name: 'git-ops', description: 'Git operations helper', execution_mode: 'sync' },
68
- { name: 'deploy', description: 'Deploy to prod', execution_mode: 'async' }, // should not appear
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
- describe('invoke()', () => {
82
- it('should execute sync skill via Keymaker', async () => {
83
- mockRegistry.get.mockReturnValue({
84
- name: 'test-skill',
85
- description: 'Test',
86
- enabled: true,
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
- describe('SkillDelegateTool (async)', () => {
132
- beforeEach(() => {
133
- vi.clearAllMocks();
134
- mockRegistry.get.mockReset();
135
- mockRegistry.getEnabled.mockReset();
136
- mockRepository.createTask.mockReset();
137
- mockDisplay.log.mockReset();
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
- describe('getSkillDelegateDescription()', () => {
143
- it('should include enabled async skills in description', () => {
144
- mockRegistry.getEnabled.mockReturnValue([
145
- { name: 'deploy-staging', description: 'Deploy to staging', execution_mode: 'async' },
146
- { name: 'batch-process', description: 'Process batch jobs', execution_mode: 'async' },
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
- describe('invoke()', () => {
161
- it('should create task for valid async skill', async () => {
162
- mockRegistry.get.mockReturnValue({
163
- name: 'deploy-staging',
164
- description: 'Deploy',
165
- enabled: true,
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 { SkillExecuteTool, SkillDelegateTool, getSkillExecuteDescription, getSkillDelegateDescription, updateSkillToolDescriptions, updateSkillDelegateDescription, // backwards compat
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 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');
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 Tools - skill_execute (sync) and skill_delegate (async)
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
- * Tool that Oracle uses to delegate tasks to Keymaker via async task queue.
135
- * Keymaker will execute the skill instructions in background.
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 const SkillDelegateTool = tool(async ({ skillName, objective }) => {
138
- try {
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 `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
22
+ return `Skill '${skillName}' not found. Available skills: ${available || 'none'}`;
146
23
  }
147
24
  if (!skill.enabled) {
148
- return `Error: Skill "${skillName}" is disabled.`;
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
- // Check for duplicate delegation
154
- const existingAck = TaskRequestContext.findDuplicateDelegation("keymaker", `${skillName}:${objective}`);
155
- if (existingAck) {
156
- display.log(`Keymaker delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
157
- source: "SkillDelegateTool",
158
- level: "info",
159
- });
160
- return `Task ${existingAck.task_id} already queued for Keymaker (${skillName}) execution.`;
161
- }
162
- if (!TaskRequestContext.canEnqueueDelegation()) {
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
- const ctx = TaskRequestContext.get();
170
- const repository = TaskRepository.getInstance();
171
- // Store skill name in context as JSON
172
- const taskContext = JSON.stringify({ skill: skillName });
173
- const created = repository.createTask({
174
- agent: "keymaker",
175
- input: objective,
176
- context: taskContext,
177
- origin_channel: ctx?.origin_channel ?? "api",
178
- session_id: ctx?.session_id ?? "default",
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
- * Updates both skill tool descriptions with current skill list.
222
- * Should be called after skills are loaded/reloaded.
50
+ * Builds the load_skill tool description with available skills.
223
51
  */
224
- export function updateSkillToolDescriptions() {
225
- SkillExecuteTool.description = getSkillExecuteDescription();
226
- SkillDelegateTool.description = getSkillDelegateDescription();
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;