adaptive-memory-multi-model-router 1.2.2 → 1.3.1

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -66
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/integrations/airtable.js +20 -0
  6. package/dist/integrations/discord.js +18 -0
  7. package/dist/integrations/github.js +23 -0
  8. package/dist/integrations/gmail.js +19 -0
  9. package/dist/integrations/google-calendar.js +18 -0
  10. package/dist/integrations/index.js +61 -0
  11. package/dist/integrations/jira.js +21 -0
  12. package/dist/integrations/linear.js +19 -0
  13. package/dist/integrations/notion.js +19 -0
  14. package/dist/integrations/slack.js +18 -0
  15. package/dist/integrations/telegram.js +19 -0
  16. package/dist/providers/registry.js +7 -3
  17. package/docs/ARCHITECTURAL-IMPROVEMENTS-2025.md +1391 -0
  18. package/docs/ARCHITECTURAL-IMPROVEMENTS-REVISED-2025.md +1051 -0
  19. package/docs/CONFIGURATION.md +476 -0
  20. package/docs/COUNCIL_DECISION.json +308 -0
  21. package/docs/COUNCIL_SUMMARY.md +265 -0
  22. package/docs/COUNCIL_V2.2_DECISION.md +416 -0
  23. package/docs/IMPROVEMENT_ROADMAP.md +515 -0
  24. package/docs/LLM_COUNCIL_DECISION.md +508 -0
  25. package/docs/QUICK_START_VISIBILITY.md +782 -0
  26. package/docs/REDDIT_GAP_ANALYSIS.md +299 -0
  27. package/docs/RESEARCH_BACKED_IMPROVEMENTS.md +1180 -0
  28. package/docs/TMLPD_QNA.md +751 -0
  29. package/docs/TMLPD_V2.1_COMPLETE.md +763 -0
  30. package/docs/TMLPD_V2.2_RESEARCH_ROADMAP.md +754 -0
  31. package/docs/V2.2_IMPLEMENTATION_COMPLETE.md +446 -0
  32. package/docs/V2_IMPLEMENTATION_GUIDE.md +388 -0
  33. package/docs/VISIBILITY_ADOPTION_PLAN.md +1005 -0
  34. package/docs/launch-content/LAUNCH_EXECUTION_CHECKLIST.md +421 -0
  35. package/docs/launch-content/README.md +457 -0
  36. package/docs/launch-content/assets/cost_comparison_100_tasks.png +0 -0
  37. package/docs/launch-content/assets/cumulative_savings.png +0 -0
  38. package/docs/launch-content/assets/parallel_speedup.png +0 -0
  39. package/docs/launch-content/assets/provider_pricing_comparison.png +0 -0
  40. package/docs/launch-content/assets/task_breakdown_comparison.png +0 -0
  41. package/docs/launch-content/generate_charts.py +313 -0
  42. package/docs/launch-content/hn_show_post.md +139 -0
  43. package/docs/launch-content/partner_outreach_templates.md +745 -0
  44. package/docs/launch-content/reddit_posts.md +467 -0
  45. package/docs/launch-content/twitter_thread.txt +460 -0
  46. package/examples/QUICKSTART.md +1 -1
  47. package/openclaw-alexa-bridge/ALL_REMAINING_FIXES_PLAN.md +313 -0
  48. package/openclaw-alexa-bridge/REMAINING_FIXES_SUMMARY.md +277 -0
  49. package/openclaw-alexa-bridge/src/alexa_handler_no_tmlpd.js +1234 -0
  50. package/openclaw-alexa-bridge/test_fixes.js +77 -0
  51. package/package.json +120 -29
  52. package/package.json.tmp +0 -0
  53. package/qna/TMLPD_QNA.md +3 -3
  54. package/skill/SKILL.md +2 -2
  55. package/src/__tests__/integration/tmpld_integration.test.py +540 -0
  56. package/src/agents/skill_enhanced_agent.py +318 -0
  57. package/src/memory/__init__.py +15 -0
  58. package/src/memory/agentic_memory.py +353 -0
  59. package/src/memory/semantic_memory.py +444 -0
  60. package/src/memory/simple_memory.py +466 -0
  61. package/src/memory/working_memory.py +447 -0
  62. package/src/orchestration/__init__.py +52 -0
  63. package/src/orchestration/execution_engine.py +353 -0
  64. package/src/orchestration/halo_orchestrator.py +367 -0
  65. package/src/orchestration/mcts_workflow.py +498 -0
  66. package/src/orchestration/role_assigner.py +473 -0
  67. package/src/orchestration/task_planner.py +522 -0
  68. package/src/providers/__init__.py +67 -0
  69. package/src/providers/anthropic.py +304 -0
  70. package/src/providers/base.py +241 -0
  71. package/src/providers/cerebras.py +373 -0
  72. package/src/providers/registry.py +476 -0
  73. package/src/routing/__init__.py +30 -0
  74. package/src/routing/universal_router.py +621 -0
  75. package/src/skills/TMLPD-QUICKREF.md +210 -0
  76. package/src/skills/TMLPD-SETUP-SUMMARY.md +157 -0
  77. package/src/skills/TMLPD.md +540 -0
  78. package/src/skills/__tests__/skill_manager.test.ts +328 -0
  79. package/src/skills/skill_manager.py +385 -0
  80. package/src/skills/test-tmlpd.sh +108 -0
  81. package/src/skills/tmlpd-category.yaml +67 -0
  82. package/src/skills/tmlpd-monitoring.yaml +188 -0
  83. package/src/skills/tmlpd-phase.yaml +132 -0
  84. package/src/state/__init__.py +17 -0
  85. package/src/state/simple_checkpoint.py +508 -0
  86. package/src/tmlpd_agent.py +464 -0
  87. package/src/tmpld_v2.py +427 -0
  88. package/src/workflows/__init__.py +18 -0
  89. package/src/workflows/advanced_difficulty_classifier.py +377 -0
  90. package/src/workflows/chaining_executor.py +417 -0
  91. package/src/workflows/difficulty_integration.py +209 -0
  92. package/src/workflows/orchestrator.py +469 -0
  93. package/src/workflows/orchestrator_executor.py +456 -0
  94. package/src/workflows/parallelization_executor.py +382 -0
  95. package/src/workflows/router.py +311 -0
  96. package/test_integration_simple.py +86 -0
  97. package/test_mcts_workflow.py +150 -0
  98. package/test_templd_integration.py +262 -0
  99. package/test_universal_router.py +275 -0
  100. package/tmlpd-pi-extension/README.md +36 -0
  101. package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts +114 -0
  102. package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts.map +1 -0
  103. package/tmlpd-pi-extension/dist/cache/prefixCache.js +285 -0
  104. package/tmlpd-pi-extension/dist/cache/prefixCache.js.map +1 -0
  105. package/tmlpd-pi-extension/dist/cache/responseCache.d.ts +58 -0
  106. package/tmlpd-pi-extension/dist/cache/responseCache.d.ts.map +1 -0
  107. package/tmlpd-pi-extension/dist/cache/responseCache.js +153 -0
  108. package/tmlpd-pi-extension/dist/cache/responseCache.js.map +1 -0
  109. package/tmlpd-pi-extension/dist/cli.js +59 -0
  110. package/tmlpd-pi-extension/dist/cost/costTracker.d.ts +95 -0
  111. package/tmlpd-pi-extension/dist/cost/costTracker.d.ts.map +1 -0
  112. package/tmlpd-pi-extension/dist/cost/costTracker.js +240 -0
  113. package/tmlpd-pi-extension/dist/cost/costTracker.js.map +1 -0
  114. package/tmlpd-pi-extension/dist/index.d.ts +723 -0
  115. package/tmlpd-pi-extension/dist/index.d.ts.map +1 -0
  116. package/tmlpd-pi-extension/dist/index.js +239 -0
  117. package/tmlpd-pi-extension/dist/index.js.map +1 -0
  118. package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts +82 -0
  119. package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts.map +1 -0
  120. package/tmlpd-pi-extension/dist/memory/episodicMemory.js +145 -0
  121. package/tmlpd-pi-extension/dist/memory/episodicMemory.js.map +1 -0
  122. package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts +102 -0
  123. package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts.map +1 -0
  124. package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js +207 -0
  125. package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js.map +1 -0
  126. package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts +85 -0
  127. package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts.map +1 -0
  128. package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js +210 -0
  129. package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js.map +1 -0
  130. package/tmlpd-pi-extension/dist/providers/localProvider.d.ts +102 -0
  131. package/tmlpd-pi-extension/dist/providers/localProvider.d.ts.map +1 -0
  132. package/tmlpd-pi-extension/dist/providers/localProvider.js +338 -0
  133. package/tmlpd-pi-extension/dist/providers/localProvider.js.map +1 -0
  134. package/tmlpd-pi-extension/dist/providers/registry.d.ts +55 -0
  135. package/tmlpd-pi-extension/dist/providers/registry.d.ts.map +1 -0
  136. package/tmlpd-pi-extension/dist/providers/registry.js +138 -0
  137. package/tmlpd-pi-extension/dist/providers/registry.js.map +1 -0
  138. package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts +68 -0
  139. package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts.map +1 -0
  140. package/tmlpd-pi-extension/dist/routing/advancedRouter.js +332 -0
  141. package/tmlpd-pi-extension/dist/routing/advancedRouter.js.map +1 -0
  142. package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts +101 -0
  143. package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts.map +1 -0
  144. package/tmlpd-pi-extension/dist/tools/tmlpdTools.js +368 -0
  145. package/tmlpd-pi-extension/dist/tools/tmlpdTools.js.map +1 -0
  146. package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts +96 -0
  147. package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts.map +1 -0
  148. package/tmlpd-pi-extension/dist/utils/batchProcessor.js +170 -0
  149. package/tmlpd-pi-extension/dist/utils/batchProcessor.js.map +1 -0
  150. package/tmlpd-pi-extension/dist/utils/compression.d.ts +61 -0
  151. package/tmlpd-pi-extension/dist/utils/compression.d.ts.map +1 -0
  152. package/tmlpd-pi-extension/dist/utils/compression.js +281 -0
  153. package/tmlpd-pi-extension/dist/utils/compression.js.map +1 -0
  154. package/tmlpd-pi-extension/dist/utils/reliability.d.ts +74 -0
  155. package/tmlpd-pi-extension/dist/utils/reliability.d.ts.map +1 -0
  156. package/tmlpd-pi-extension/dist/utils/reliability.js +177 -0
  157. package/tmlpd-pi-extension/dist/utils/reliability.js.map +1 -0
  158. package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts +117 -0
  159. package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts.map +1 -0
  160. package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js +246 -0
  161. package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js.map +1 -0
  162. package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts +50 -0
  163. package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts.map +1 -0
  164. package/tmlpd-pi-extension/dist/utils/tokenUtils.js +124 -0
  165. package/tmlpd-pi-extension/dist/utils/tokenUtils.js.map +1 -0
  166. package/tmlpd-pi-extension/examples/QUICKSTART.md +183 -0
  167. package/tmlpd-pi-extension/package-lock.json +75 -0
  168. package/tmlpd-pi-extension/package.json +172 -0
  169. package/tmlpd-pi-extension/python/examples.py +53 -0
  170. package/tmlpd-pi-extension/python/integrations.py +330 -0
  171. package/tmlpd-pi-extension/python/setup.py +28 -0
  172. package/tmlpd-pi-extension/python/tmlpd.py +369 -0
  173. package/tmlpd-pi-extension/qna/REDDIT_GAP_ANALYSIS.md +299 -0
  174. package/tmlpd-pi-extension/qna/TMLPD_QNA.md +751 -0
  175. package/tmlpd-pi-extension/skill/SKILL.md +238 -0
  176. package/{src → tmlpd-pi-extension/src}/index.ts +1 -1
  177. package/tmlpd-pi-extension/tsconfig.json +18 -0
  178. package/demo/research-demo.js +0 -266
  179. package/notebooks/quickstart.ipynb +0 -157
  180. package/rust/tmlpd.h +0 -268
  181. package/src/cache/prefixCache.ts +0 -365
  182. package/src/routing/advancedRouter.ts +0 -406
  183. package/src/utils/speculativeDecoding.ts +0 -344
  184. /package/{src → tmlpd-pi-extension/src}/cache/responseCache.ts +0 -0
  185. /package/{src → tmlpd-pi-extension/src}/cost/costTracker.ts +0 -0
  186. /package/{src → tmlpd-pi-extension/src}/memory/episodicMemory.ts +0 -0
  187. /package/{src → tmlpd-pi-extension/src}/orchestration/haloOrchestrator.ts +0 -0
  188. /package/{src → tmlpd-pi-extension/src}/orchestration/mctsWorkflow.ts +0 -0
  189. /package/{src → tmlpd-pi-extension/src}/providers/localProvider.ts +0 -0
  190. /package/{src → tmlpd-pi-extension/src}/providers/registry.ts +0 -0
  191. /package/{src → tmlpd-pi-extension/src}/tools/tmlpdTools.ts +0 -0
  192. /package/{src → tmlpd-pi-extension/src}/utils/batchProcessor.ts +0 -0
  193. /package/{src → tmlpd-pi-extension/src}/utils/compression.ts +0 -0
  194. /package/{src → tmlpd-pi-extension/src}/utils/reliability.ts +0 -0
  195. /package/{src → tmlpd-pi-extension/src}/utils/tokenUtils.ts +0 -0
@@ -0,0 +1,328 @@
1
+ """
2
+ Tests for SkillManager and TMLEnhancedAgent
3
+ """
4
+
5
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
6
+ import { SkillManager, Skill } from '../skill_manager';
7
+ import { TMLEnhancedAgent } from '../../agents/skill_enhanced_agent';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+
11
+ // Mock fs operations
12
+ jest.mock('fs');
13
+
14
+ describe('SkillManager', () => {
15
+ let skillManager: SkillManager;
16
+ const mockSkillsDir = '/mock/skills';
17
+
18
+ beforeEach(() => {
19
+ jest.clearAllMocks();
20
+ skillManager = new SkillManager(mockSkillsDir);
21
+ });
22
+
23
+ describe('initialization', () => {
24
+ it('should create skill manager instance', () => {
25
+ expect(skillManager).toBeInstanceOf(SkillManager);
26
+ expect(skillManager.skills_dir).toBeDefined();
27
+ });
28
+
29
+ it('should have empty skills initially if directory does not exist', () => {
30
+ expect(skillManager.list_skills()).toEqual([]);
31
+ });
32
+ });
33
+
34
+ describe('load_skills_metadata', () => {
35
+ it('should load skill metadata from SKILL.md files', () => {
36
+ // Mock directory listing and file reading
37
+ const mockSkillMD = `---
38
+ name: "Test Skill"
39
+ description: "A test skill for testing"
40
+ ---
41
+ # Test Skill Content`;
42
+
43
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
44
+ jest.spyOn(Path.prototype, 'isDirectory').mockReturnValue(true);
45
+ jest.spyOn(Path.prototype, 'iterdir').mockReturnValue([
46
+ { name: 'SKILL.md', isFile: () => true }
47
+ ] as any);
48
+ jest.spyOn(fs, 'readFileSync').mockReturnValue(mockSkillMD);
49
+
50
+ skillManager.reload_skills();
51
+
52
+ expect(skillManager.list_skills()).toContain('Test Skill');
53
+ });
54
+
55
+ it('should skip directories without SKILL.md', () => {
56
+ jest.spyOn(Path.prototype, 'existsSync').mockReturnValue(false);
57
+
58
+ skillManager.reload_skills();
59
+
60
+ expect(skillManager.list_skills()).toEqual([]);
61
+ });
62
+ });
63
+
64
+ describe('get_relevant_skills', () => {
65
+ beforeEach(() => {
66
+ // Add mock skills
67
+ skillManager.skills['React Development'] = new Skill(
68
+ 'React Development',
69
+ 'Best practices for React components',
70
+ Path('/mock/react'),
71
+ {}
72
+ );
73
+
74
+ skillManager.skills['Node.js API'] = new Skill(
75
+ 'Node.js API',
76
+ 'Building backend APIs with Node.js and Express',
77
+ Path('/mock/nodejs'),
78
+ {}
79
+ );
80
+
81
+ skillManager.skills['Python Django'] = new Skill(
82
+ 'Python Django',
83
+ 'Django web framework for Python',
84
+ Path('/mock/python'),
85
+ {}
86
+ );
87
+ });
88
+
89
+ it('should find skills with keyword matching', () => {
90
+ const relevant = skillManager.get_relevant_skills(
91
+ 'Build a React component for user login',
92
+ 2
93
+ );
94
+
95
+ expect(relevant).toContain('React Development');
96
+ });
97
+
98
+ it('should return skills ordered by relevance', () => {
99
+ const relevant = skillManager.get_relevant_skills(
100
+ 'Create a React component with Node.js backend',
101
+ 3
102
+ );
103
+
104
+ // React should come first (exact match)
105
+ expect(relevant[0]).toBe('React Development');
106
+ expect(relevant).toContain('Node.js API');
107
+ });
108
+
109
+ it('should respect threshold parameter', () => {
110
+ const relevant = skillManager.get_relevant_skills(
111
+ 'Build a Go microservice',
112
+ 2,
113
+ 0.5 // Higher threshold
114
+ );
115
+
116
+ // Should return fewer or no skills due to high threshold
117
+ expect(relevant.length).toBeLessThanOrEqual(2);
118
+ });
119
+ });
120
+
121
+ describe('load_skill', () => {
122
+ it('should load full skill content on first call', () => {
123
+ const mockSkill = skillManager.skills['React Development'];
124
+ mockSkill.content = null;
125
+
126
+ const mockContent = '# React Development\n\nBest practices...';
127
+
128
+ jest.spyOn(fs, 'readFileSync').mockReturnValue(
129
+ `---\nname: "React Development"\ndescription: "Best practices"\n---\n${mockContent}`
130
+ );
131
+
132
+ const loaded = skillManager.load_skill('React Development');
133
+
134
+ expect(loaded.content).toBe(mockContent);
135
+ expect(loaded.loaded_at).toBeDefined();
136
+ });
137
+
138
+ it('should return cached content on subsequent calls', () => {
139
+ const mockSkill = skillManager.skills['React Development'];
140
+ mockSkill.content = 'Cached content';
141
+
142
+ const loaded1 = skillManager.load_skill('React Development');
143
+ const loaded2 = skillManager.load_skill('React Development');
144
+
145
+ expect(loaded1).toBe(loaded2);
146
+ expect(loaded1.content).toBe('Cached content');
147
+ });
148
+
149
+ it('should throw error for non-existent skill', () => {
150
+ expect(() => {
151
+ skillManager.load_skill('Non-existent Skill');
152
+ }).toThrow("Skill 'Non-existent Skill' not found");
153
+ });
154
+ });
155
+
156
+ describe('validate_skill', () => {
157
+ it('should return validation results for existing skill', () => {
158
+ const mockSkill = skillManager.skills['React Development'];
159
+
160
+ jest.spyOn(skillManager, 'list_additional_files').mockReturnValue([]);
161
+
162
+ const validation = skillManager.validate_skill('React Development');
163
+
164
+ expect(validation).toHaveProperty('exists', true);
165
+ expect(validation).toHaveProperty('has_name', true);
166
+ expect(validation).toHaveProperty('has_description', true);
167
+ });
168
+
169
+ it('should return all false for non-existent skill', () => {
170
+ const validation = skillManager.validate_skill('Non-existent');
171
+
172
+ expect(validation.exists).toBe(false);
173
+ expect(validation.has_skill_md).toBe(false);
174
+ });
175
+ });
176
+ });
177
+
178
+ describe('TMLEnhancedAgent', () => {
179
+ let agent: TMLEnhancedAgent;
180
+
181
+ beforeEach(() => {
182
+ agent = new TMLEnhancedAgent(
183
+ 'frontend-agent',
184
+ 'anthropic',
185
+ 'claude-sonnet-4',
186
+ 'mock-skills',
187
+ ['React Frontend Development', 'TypeScript Best Practices']
188
+ );
189
+ });
190
+
191
+ describe('initialization', () => {
192
+ it('should create agent with configuration', () => {
193
+ expect(agent.agent_id).toBe('frontend-agent');
194
+ expect(agent.provider).toBe('anthropic');
195
+ expect(agent.model).toBe('claude-sonnet-4');
196
+ });
197
+
198
+ it('should initialize with assigned skills', () => {
199
+ expect(agent.assigned_skills).toContain('React Frontend Development');
200
+ expect(agent.assigned_skills).toContain('TypeScript Best Practices');
201
+ });
202
+ });
203
+
204
+ describe('execute_task', () => {
205
+ it('should execute task with relevant skills', async () => {
206
+ const task = {
207
+ description: 'Build a React login form component',
208
+ context: 'Must include email and password fields',
209
+ requirements: 'Use TypeScript and Material-UI'
210
+ };
211
+
212
+ // Mock skill loading
213
+ jest.spyOn(agent, '_get_relevant_skills').mockReturnValue([]);
214
+
215
+ // Mock LLM call
216
+ jest.spyOn(agent, '_execute_llm_call').mockReturnValue({
217
+ success: true,
218
+ output: 'React component code...',
219
+ tokens_used: 150,
220
+ cost: 0.015,
221
+ execution_time: 3.2
222
+ });
223
+
224
+ const result = agent.execute_task(task);
225
+
226
+ expect(result.success).toBe(true);
227
+ expect(result.output).toBeDefined();
228
+ });
229
+
230
+ it('should remember successful patterns', () => {
231
+ const task = { description: 'Test task' };
232
+
233
+ jest.spyOn(agent, '_get_relevant_skills').mockReturnValue([]);
234
+ jest.spyOn(agent, '_execute_llm_call').mockReturnValue({
235
+ success: true,
236
+ output: 'Success'
237
+ });
238
+
239
+ jest.spyOn(agent, '_remember_success_pattern').mockImplementation(() => {});
240
+
241
+ agent.execute_task(task);
242
+
243
+ expect(agent._remember_success_pattern).toHaveBeenCalled();
244
+ });
245
+ });
246
+
247
+ describe('skill management', () => {
248
+ it('should add skill to agent', () => {
249
+ agent.add_skill('Jest Testing');
250
+
251
+ expect(agent.assigned_skills).toContain('Jest Testing');
252
+ });
253
+
254
+ it('should remove skill from agent', () => {
255
+ agent.remove_skill('TypeScript Best Practices');
256
+
257
+ expect(agent.assigned_skills).not.toContain('TypeScript Best Practices');
258
+ });
259
+
260
+ it('should list all available skills', () => {
261
+ jest.spyOn(agent.skill_manager, 'list_skills').mockReturnValue([
262
+ 'Skill 1',
263
+ 'Skill 2',
264
+ 'Skill 3'
265
+ ]);
266
+
267
+ const skills = agent.list_available_skills();
268
+
269
+ expect(skills).toHaveLength(3);
270
+ });
271
+ });
272
+
273
+ describe('serialization', () => {
274
+ it('should convert to dictionary', () => {
275
+ const dict = agent.to_dict();
276
+
277
+ expect(dict).toHaveProperty('agent_id', 'frontend-agent');
278
+ expect(dict).toHaveProperty('provider', 'anthropic');
279
+ expect(dict).toHaveProperty('model', 'claude-sonnet-4');
280
+ expect(dict).toHaveProperty('assigned_skills');
281
+ expect(dict).toHaveProperty('available_skills');
282
+ });
283
+ });
284
+ });
285
+
286
+ describe('TMLEnhancedAgentFactory', () => {
287
+ describe('create_from_config', () => {
288
+ it('should create agent from config', () => {
289
+ const config = {
290
+ id: 'test-agent',
291
+ provider: 'openai',
292
+ model: 'gpt-4-turbo',
293
+ skills_dir: 'test-skills',
294
+ skills: ['Test Skill']
295
+ };
296
+
297
+ const agent = TMLEnhancedAgentFactory.create_from_config(config);
298
+
299
+ expect(agent).toBeInstanceOf(TMLEnhancedAgent);
300
+ expect(agent.agent_id).toBe('test-agent');
301
+ });
302
+ });
303
+
304
+ describe('create_multiple_from_config', () => {
305
+ it('should create multiple agents from config list', () => {
306
+ const configs = [
307
+ {
308
+ id: 'agent-1',
309
+ provider: 'anthropic',
310
+ model: 'claude-sonnet-4',
311
+ skills: ['Skill A']
312
+ },
313
+ {
314
+ id: 'agent-2',
315
+ provider: 'openai',
316
+ model: 'gpt-4-turbo',
317
+ skills: ['Skill B']
318
+ }
319
+ ];
320
+
321
+ const agents = TMLEnhancedAgentFactory.create_multiple_from_config(configs);
322
+
323
+ expect(agents).toHaveLength(2);
324
+ expect(agents[0].agent_id).toBe('agent-1');
325
+ expect(agents[1].agent_id).toBe('agent-2');
326
+ });
327
+ });
328
+ });
@@ -0,0 +1,385 @@
1
+ """
2
+ SkillManager - Agent Skills Management System
3
+
4
+ Implements Anthropic's Agent Skills specification with progressive disclosure.
5
+
6
+ Based on:
7
+ https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
8
+
9
+ Progressive Disclosure Levels:
10
+ Level 1: Metadata (name, description) - loaded at startup
11
+ Level 2: SKILL.md content - loaded when relevant
12
+ Level 3: Additional files - loaded on-demand
13
+ """
14
+
15
+ from pathlib import Path
16
+ from typing import Dict, List, Optional
17
+ from dataclasses import dataclass, field
18
+ import yaml
19
+ import json
20
+ import re
21
+ from datetime import datetime
22
+
23
+
24
+ @dataclass
25
+ class Skill:
26
+ """Represents an Agent Skill"""
27
+ name: str
28
+ description: str
29
+ directory: Path
30
+ metadata: Dict[str, str] = field(default_factory=dict)
31
+ content: Optional[str] = None
32
+ loaded_at: Optional[datetime] = None
33
+
34
+ def to_dict(self) -> Dict:
35
+ """Convert skill to dictionary"""
36
+ return {
37
+ "name": self.name,
38
+ "description": self.description,
39
+ "directory": str(self.directory),
40
+ "metadata": self.metadata,
41
+ "loaded": self.content is not None,
42
+ "loaded_at": self.loaded_at.isoformat() if self.loaded_at else None
43
+ }
44
+
45
+
46
+ class SkillManager:
47
+ """
48
+ Manages Agent Skills following Anthropic's specification.
49
+ Implements progressive disclosure for efficient context loading.
50
+ """
51
+
52
+ def __init__(self, skills_dir: str = "tmlpd-skills"):
53
+ """
54
+ Initialize SkillManager
55
+
56
+ Args:
57
+ skills_dir: Directory containing skill folders
58
+ """
59
+ self.skills_dir = Path(skills_dir)
60
+ self.skills: Dict[str, Skill] = {}
61
+ self._load_skills_metadata()
62
+
63
+ def _load_skills_metadata(self):
64
+ """
65
+ Load Level 1: Metadata only (name, description)
66
+ This is loaded into system prompt at startup.
67
+ """
68
+
69
+ if not self.skills_dir.exists():
70
+ return
71
+
72
+ for skill_dir in self.skills_dir.iterdir():
73
+ if not skill_dir.is_dir():
74
+ continue
75
+
76
+ skill_md = skill_dir / "SKILL.md"
77
+
78
+ if not skill_md.exists():
79
+ continue
80
+
81
+ # Parse YAML frontmatter
82
+ try:
83
+ with open(skill_md, "r", encoding="utf-8") as f:
84
+ content = f.read()
85
+
86
+ # Extract YAML frontmatter
87
+ if content.startswith("---"):
88
+ parts = content.split("---", 2)
89
+ if len(parts) >= 3:
90
+ frontmatter = parts[1]
91
+ metadata = yaml.safe_load(frontmatter)
92
+ else:
93
+ continue
94
+ else:
95
+ continue
96
+
97
+ self.skills[metadata["name"]] = Skill(
98
+ name=metadata["name"],
99
+ description=metadata["description"],
100
+ directory=skill_dir,
101
+ metadata=metadata
102
+ )
103
+ except Exception as e:
104
+ print(f"Warning: Failed to load skill from {skill_dir}: {e}")
105
+ continue
106
+
107
+ def list_skills(self) -> List[str]:
108
+ """
109
+ List all available skills (Level 1 metadata)
110
+
111
+ Returns:
112
+ List of skill names
113
+ """
114
+ return list(self.skills.keys())
115
+
116
+ def get_skill_info(self, skill_name: str) -> Optional[Dict]:
117
+ """
118
+ Get skill metadata without loading full content
119
+
120
+ Args:
121
+ skill_name: Name of the skill
122
+
123
+ Returns:
124
+ Dictionary with skill info or None if not found
125
+ """
126
+ if skill_name not in self.skills:
127
+ return None
128
+
129
+ return self.skills[skill_name].to_dict()
130
+
131
+ def get_relevant_skills(
132
+ self,
133
+ task: str,
134
+ top_k: int = 3,
135
+ threshold: float = 0.1
136
+ ) -> List[str]:
137
+ """
138
+ Find relevant skills based on task description.
139
+ Uses keyword matching with scoring.
140
+
141
+ Args:
142
+ task: Task description
143
+ top_k: Maximum number of skills to return
144
+ threshold: Minimum relevance threshold (0-1)
145
+
146
+ Returns:
147
+ List of relevant skill names
148
+ """
149
+ task_lower = task.lower()
150
+ task_words = set(re.findall(r'\w+', task_lower))
151
+
152
+ if not task_words:
153
+ return []
154
+
155
+ relevant = []
156
+
157
+ for skill_name, skill in self.skills.items():
158
+ # Check if task keywords match skill description
159
+ skill_desc_lower = skill.description.lower()
160
+ skill_words = set(re.findall(r'\w+', skill_desc_lower))
161
+
162
+ # Calculate Jaccard similarity
163
+ intersection = task_words & skill_words
164
+ union = task_words | skill_words
165
+
166
+ if union:
167
+ similarity = len(intersection) / len(union)
168
+ else:
169
+ similarity = 0.0
170
+
171
+ # Also check for keyword containment
172
+ contains_score = sum(1 for word in task_words if word in skill_desc_lower)
173
+ combined_score = similarity + (contains_score * 0.1)
174
+
175
+ if combined_score >= threshold:
176
+ relevant.append((skill_name, combined_score))
177
+
178
+ # Sort by relevance and return top_k
179
+ relevant.sort(key=lambda x: x[1], reverse=True)
180
+ return [skill_name for skill_name, _ in relevant[:top_k]]
181
+
182
+ def load_skill(self, skill_name: str) -> Skill:
183
+ """
184
+ Load Level 2: Full SKILL.md content
185
+ Called only when skill is relevant to current task.
186
+
187
+ Args:
188
+ skill_name: Name of the skill to load
189
+
190
+ Returns:
191
+ Skill with loaded content
192
+
193
+ Raises:
194
+ ValueError: If skill not found
195
+ """
196
+ if skill_name not in self.skills:
197
+ raise ValueError(f"Skill '{skill_name}' not found")
198
+
199
+ skill = self.skills[skill_name]
200
+
201
+ # Return cached if already loaded
202
+ if skill.content is not None:
203
+ return skill
204
+
205
+ skill_md = skill.directory / "SKILL.md"
206
+
207
+ try:
208
+ with open(skill_md, "r", encoding="utf-8") as f:
209
+ content = f.read()
210
+
211
+ # Skip YAML frontmatter, get content
212
+ if "---" in content:
213
+ _, _, content = content.split("---", 2)
214
+
215
+ skill.content = content.strip()
216
+ skill.loaded_at = datetime.now()
217
+
218
+ return skill
219
+ except Exception as e:
220
+ raise IOError(f"Failed to load skill '{skill_name}': {e}")
221
+
222
+ def load_additional_file(self, skill_name: str, filename: str) -> str:
223
+ """
224
+ Load Level 3: Additional files from skill
225
+ Progressive disclosure - load only when needed.
226
+
227
+ Args:
228
+ skill_name: Name of the skill
229
+ filename: Name of the additional file
230
+
231
+ Returns:
232
+ File content
233
+
234
+ Raises:
235
+ ValueError: If skill not found
236
+ FileNotFoundError: If file not found in skill directory
237
+ """
238
+ if skill_name not in self.skills:
239
+ raise ValueError(f"Skill '{skill_name}' not found")
240
+
241
+ skill = self.skills[skill_name]
242
+ additional_file = skill.directory / filename
243
+
244
+ if not additional_file.exists():
245
+ raise FileNotFoundError(
246
+ f"File '{filename}' not found in skill '{skill_name}'"
247
+ )
248
+
249
+ try:
250
+ with open(additional_file, "r", encoding="utf-8") as f:
251
+ return f.read()
252
+ except Exception as e:
253
+ raise IOError(
254
+ f"Failed to load file '{filename}' from skill '{skill_name}': {e}"
255
+ )
256
+
257
+ def list_additional_files(self, skill_name: str) -> List[str]:
258
+ """
259
+ List all additional files available in a skill
260
+
261
+ Args:
262
+ skill_name: Name of the skill
263
+
264
+ Returns:
265
+ List of filenames (excluding SKILL.md)
266
+ """
267
+ if skill_name not in self.skills:
268
+ return []
269
+
270
+ skill = self.skills[skill_name]
271
+
272
+ files = []
273
+ for file_path in skill.directory.iterdir():
274
+ if file_path.is_file() and file_path.name != "SKILL.md":
275
+ # Skip subdirectories and hidden files
276
+ if not file_path.name.startswith("."):
277
+ files.append(file_path.name)
278
+
279
+ return sorted(files)
280
+
281
+ def get_skills_summary(self) -> Dict[str, Dict]:
282
+ """
283
+ Get summary of all skills with their metadata
284
+
285
+ Returns:
286
+ Dictionary mapping skill names to their info
287
+ """
288
+ return {
289
+ name: skill.to_dict()
290
+ for name, skill in self.skills.items()
291
+ }
292
+
293
+ def reload_skills(self):
294
+ """
295
+ Reload all skills from disk
296
+ Useful for dynamic skill updates
297
+ """
298
+ self.skills.clear()
299
+ self._load_skills_metadata()
300
+
301
+ def validate_skill(self, skill_name: str) -> Dict[str, bool]:
302
+ """
303
+ Validate a skill's structure
304
+
305
+ Args:
306
+ skill_name: Name of the skill to validate
307
+
308
+ Returns:
309
+ Dictionary with validation results
310
+ """
311
+ if skill_name not in self.skills:
312
+ return {
313
+ "exists": False,
314
+ "has_skill_md": False,
315
+ "valid_yaml": False,
316
+ "has_name": False,
317
+ "has_description": False
318
+ }
319
+
320
+ skill = self.skills[skill_name]
321
+ skill_md = skill.directory / "SKILL.md"
322
+
323
+ validation = {
324
+ "exists": True,
325
+ "has_skill_md": skill_md.exists(),
326
+ "valid_yaml": "name" in skill.metadata and "description" in skill.metadata,
327
+ "has_name": "name" in skill.metadata,
328
+ "has_description": "description" in skill.metadata,
329
+ "has_additional_files": len(self.list_additional_files(skill_name)) > 0
330
+ }
331
+
332
+ return validation
333
+
334
+ def create_skill_template(self, skill_name: str, output_dir: Optional[str] = None):
335
+ """
336
+ Create a template for a new skill
337
+
338
+ Args:
339
+ skill_name: Name for the new skill
340
+ output_dir: Directory to create skill in (default: skills_dir/<skill_name>)
341
+ """
342
+ if output_dir is None:
343
+ output_dir = self.skills_dir / skill_name.lower().replace(" ", "_")
344
+ else:
345
+ output_dir = Path(output_dir)
346
+
347
+ output_dir.mkdir(parents=True, exist_ok=True)
348
+
349
+ skill_md = output_dir / "SKILL.md"
350
+
351
+ template = f"""---
352
+ name: "{skill_name}"
353
+ description: "A brief description of what this skill does"
354
+ ---
355
+
356
+ # {skill_name}
357
+
358
+ ## Overview
359
+
360
+ Provide a brief overview of this skill and when it should be used.
361
+
362
+ ## Core Principles
363
+
364
+ List the key principles that this skill follows.
365
+
366
+ ## Common Patterns
367
+
368
+ Describe common patterns or approaches used in this skill.
369
+
370
+ ## When to Use This Skill
371
+
372
+ Trigger this skill when:
373
+ - Condition 1
374
+ - Condition 2
375
+ - Condition 3
376
+
377
+ ## Examples
378
+
379
+ Provide examples of when to use this skill.
380
+ """
381
+
382
+ with open(skill_md, "w", encoding="utf-8") as f:
383
+ f.write(template)
384
+
385
+ return skill_md