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.
- package/LICENSE +21 -0
- package/README.md +146 -66
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/airtable.js +20 -0
- package/dist/integrations/discord.js +18 -0
- package/dist/integrations/github.js +23 -0
- package/dist/integrations/gmail.js +19 -0
- package/dist/integrations/google-calendar.js +18 -0
- package/dist/integrations/index.js +61 -0
- package/dist/integrations/jira.js +21 -0
- package/dist/integrations/linear.js +19 -0
- package/dist/integrations/notion.js +19 -0
- package/dist/integrations/slack.js +18 -0
- package/dist/integrations/telegram.js +19 -0
- package/dist/providers/registry.js +7 -3
- package/docs/ARCHITECTURAL-IMPROVEMENTS-2025.md +1391 -0
- package/docs/ARCHITECTURAL-IMPROVEMENTS-REVISED-2025.md +1051 -0
- package/docs/CONFIGURATION.md +476 -0
- package/docs/COUNCIL_DECISION.json +308 -0
- package/docs/COUNCIL_SUMMARY.md +265 -0
- package/docs/COUNCIL_V2.2_DECISION.md +416 -0
- package/docs/IMPROVEMENT_ROADMAP.md +515 -0
- package/docs/LLM_COUNCIL_DECISION.md +508 -0
- package/docs/QUICK_START_VISIBILITY.md +782 -0
- package/docs/REDDIT_GAP_ANALYSIS.md +299 -0
- package/docs/RESEARCH_BACKED_IMPROVEMENTS.md +1180 -0
- package/docs/TMLPD_QNA.md +751 -0
- package/docs/TMLPD_V2.1_COMPLETE.md +763 -0
- package/docs/TMLPD_V2.2_RESEARCH_ROADMAP.md +754 -0
- package/docs/V2.2_IMPLEMENTATION_COMPLETE.md +446 -0
- package/docs/V2_IMPLEMENTATION_GUIDE.md +388 -0
- package/docs/VISIBILITY_ADOPTION_PLAN.md +1005 -0
- package/docs/launch-content/LAUNCH_EXECUTION_CHECKLIST.md +421 -0
- package/docs/launch-content/README.md +457 -0
- package/docs/launch-content/assets/cost_comparison_100_tasks.png +0 -0
- package/docs/launch-content/assets/cumulative_savings.png +0 -0
- package/docs/launch-content/assets/parallel_speedup.png +0 -0
- package/docs/launch-content/assets/provider_pricing_comparison.png +0 -0
- package/docs/launch-content/assets/task_breakdown_comparison.png +0 -0
- package/docs/launch-content/generate_charts.py +313 -0
- package/docs/launch-content/hn_show_post.md +139 -0
- package/docs/launch-content/partner_outreach_templates.md +745 -0
- package/docs/launch-content/reddit_posts.md +467 -0
- package/docs/launch-content/twitter_thread.txt +460 -0
- package/examples/QUICKSTART.md +1 -1
- package/openclaw-alexa-bridge/ALL_REMAINING_FIXES_PLAN.md +313 -0
- package/openclaw-alexa-bridge/REMAINING_FIXES_SUMMARY.md +277 -0
- package/openclaw-alexa-bridge/src/alexa_handler_no_tmlpd.js +1234 -0
- package/openclaw-alexa-bridge/test_fixes.js +77 -0
- package/package.json +120 -29
- package/package.json.tmp +0 -0
- package/qna/TMLPD_QNA.md +3 -3
- package/skill/SKILL.md +2 -2
- package/src/__tests__/integration/tmpld_integration.test.py +540 -0
- package/src/agents/skill_enhanced_agent.py +318 -0
- package/src/memory/__init__.py +15 -0
- package/src/memory/agentic_memory.py +353 -0
- package/src/memory/semantic_memory.py +444 -0
- package/src/memory/simple_memory.py +466 -0
- package/src/memory/working_memory.py +447 -0
- package/src/orchestration/__init__.py +52 -0
- package/src/orchestration/execution_engine.py +353 -0
- package/src/orchestration/halo_orchestrator.py +367 -0
- package/src/orchestration/mcts_workflow.py +498 -0
- package/src/orchestration/role_assigner.py +473 -0
- package/src/orchestration/task_planner.py +522 -0
- package/src/providers/__init__.py +67 -0
- package/src/providers/anthropic.py +304 -0
- package/src/providers/base.py +241 -0
- package/src/providers/cerebras.py +373 -0
- package/src/providers/registry.py +476 -0
- package/src/routing/__init__.py +30 -0
- package/src/routing/universal_router.py +621 -0
- package/src/skills/TMLPD-QUICKREF.md +210 -0
- package/src/skills/TMLPD-SETUP-SUMMARY.md +157 -0
- package/src/skills/TMLPD.md +540 -0
- package/src/skills/__tests__/skill_manager.test.ts +328 -0
- package/src/skills/skill_manager.py +385 -0
- package/src/skills/test-tmlpd.sh +108 -0
- package/src/skills/tmlpd-category.yaml +67 -0
- package/src/skills/tmlpd-monitoring.yaml +188 -0
- package/src/skills/tmlpd-phase.yaml +132 -0
- package/src/state/__init__.py +17 -0
- package/src/state/simple_checkpoint.py +508 -0
- package/src/tmlpd_agent.py +464 -0
- package/src/tmpld_v2.py +427 -0
- package/src/workflows/__init__.py +18 -0
- package/src/workflows/advanced_difficulty_classifier.py +377 -0
- package/src/workflows/chaining_executor.py +417 -0
- package/src/workflows/difficulty_integration.py +209 -0
- package/src/workflows/orchestrator.py +469 -0
- package/src/workflows/orchestrator_executor.py +456 -0
- package/src/workflows/parallelization_executor.py +382 -0
- package/src/workflows/router.py +311 -0
- package/test_integration_simple.py +86 -0
- package/test_mcts_workflow.py +150 -0
- package/test_templd_integration.py +262 -0
- package/test_universal_router.py +275 -0
- package/tmlpd-pi-extension/README.md +36 -0
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts +114 -0
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/cache/prefixCache.js +285 -0
- package/tmlpd-pi-extension/dist/cache/prefixCache.js.map +1 -0
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts +58 -0
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/cache/responseCache.js +153 -0
- package/tmlpd-pi-extension/dist/cache/responseCache.js.map +1 -0
- package/tmlpd-pi-extension/dist/cli.js +59 -0
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts +95 -0
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/cost/costTracker.js +240 -0
- package/tmlpd-pi-extension/dist/cost/costTracker.js.map +1 -0
- package/tmlpd-pi-extension/dist/index.d.ts +723 -0
- package/tmlpd-pi-extension/dist/index.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/index.js +239 -0
- package/tmlpd-pi-extension/dist/index.js.map +1 -0
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts +82 -0
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js +145 -0
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js.map +1 -0
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts +102 -0
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js +207 -0
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js.map +1 -0
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts +85 -0
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js +210 -0
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js.map +1 -0
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts +102 -0
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/providers/localProvider.js +338 -0
- package/tmlpd-pi-extension/dist/providers/localProvider.js.map +1 -0
- package/tmlpd-pi-extension/dist/providers/registry.d.ts +55 -0
- package/tmlpd-pi-extension/dist/providers/registry.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/providers/registry.js +138 -0
- package/tmlpd-pi-extension/dist/providers/registry.js.map +1 -0
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts +68 -0
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js +332 -0
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js.map +1 -0
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts +101 -0
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js +368 -0
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js.map +1 -0
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts +96 -0
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js +170 -0
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js.map +1 -0
- package/tmlpd-pi-extension/dist/utils/compression.d.ts +61 -0
- package/tmlpd-pi-extension/dist/utils/compression.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/utils/compression.js +281 -0
- package/tmlpd-pi-extension/dist/utils/compression.js.map +1 -0
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts +74 -0
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/utils/reliability.js +177 -0
- package/tmlpd-pi-extension/dist/utils/reliability.js.map +1 -0
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts +117 -0
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js +246 -0
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js.map +1 -0
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts +50 -0
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts.map +1 -0
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js +124 -0
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js.map +1 -0
- package/tmlpd-pi-extension/examples/QUICKSTART.md +183 -0
- package/tmlpd-pi-extension/package-lock.json +75 -0
- package/tmlpd-pi-extension/package.json +172 -0
- package/tmlpd-pi-extension/python/examples.py +53 -0
- package/tmlpd-pi-extension/python/integrations.py +330 -0
- package/tmlpd-pi-extension/python/setup.py +28 -0
- package/tmlpd-pi-extension/python/tmlpd.py +369 -0
- package/tmlpd-pi-extension/qna/REDDIT_GAP_ANALYSIS.md +299 -0
- package/tmlpd-pi-extension/qna/TMLPD_QNA.md +751 -0
- package/tmlpd-pi-extension/skill/SKILL.md +238 -0
- package/{src → tmlpd-pi-extension/src}/index.ts +1 -1
- package/tmlpd-pi-extension/tsconfig.json +18 -0
- package/demo/research-demo.js +0 -266
- package/notebooks/quickstart.ipynb +0 -157
- package/rust/tmlpd.h +0 -268
- package/src/cache/prefixCache.ts +0 -365
- package/src/routing/advancedRouter.ts +0 -406
- package/src/utils/speculativeDecoding.ts +0 -344
- /package/{src → tmlpd-pi-extension/src}/cache/responseCache.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/cost/costTracker.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/memory/episodicMemory.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/orchestration/haloOrchestrator.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/orchestration/mctsWorkflow.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/providers/localProvider.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/providers/registry.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/tools/tmlpdTools.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/utils/batchProcessor.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/utils/compression.ts +0 -0
- /package/{src → tmlpd-pi-extension/src}/utils/reliability.ts +0 -0
- /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
|