@loom-node/amoeba 0.1.0 → 0.1.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/dist/amoeba-loop.d.ts +7 -5
- package/dist/amoeba-loop.js +65 -86
- package/dist/skill-registry.d.ts +5 -0
- package/dist/skill-registry.js +4 -0
- package/package.json +2 -2
package/dist/amoeba-loop.d.ts
CHANGED
|
@@ -25,11 +25,13 @@ export declare class AmoebaLoop implements LoopStrategy {
|
|
|
25
25
|
private calibrationMap;
|
|
26
26
|
constructor(config: AmoebaLoopConfig);
|
|
27
27
|
execute(ctx: LoopContext): AsyncGenerator<AgentEvent>;
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
private
|
|
28
|
+
private senseAndMatch;
|
|
29
|
+
/** Collect name+description from loaded nodes and unloaded catalog */
|
|
30
|
+
private collectSkillDescriptions;
|
|
31
|
+
/** Single LLM call: estimate complexity + select best skill */
|
|
32
|
+
private llmSenseAndMatch;
|
|
33
|
+
/** Resolve a skill name to a loaded node, lazy-loading if needed */
|
|
34
|
+
private resolveSkillNode;
|
|
33
35
|
private scaleAndExecute;
|
|
34
36
|
private buildEnrichedPrompt;
|
|
35
37
|
private evaluateAndAdapt;
|
package/dist/amoeba-loop.js
CHANGED
|
@@ -13,17 +13,15 @@ export class AmoebaLoop {
|
|
|
13
13
|
const userMsg = [...ctx.messages].reverse().find((m) => m.role === 'user');
|
|
14
14
|
const raw = userMsg?.content ?? '';
|
|
15
15
|
const input = typeof raw === 'string' ? raw : '';
|
|
16
|
-
// Phase 1: SENSE
|
|
17
|
-
const spec = await this.
|
|
18
|
-
yield { type: 'text-delta', content: `
|
|
19
|
-
// Phase 2: MATCH
|
|
20
|
-
const { winner, tier } = await this.match(spec, input);
|
|
16
|
+
// Phase 1+2: SENSE + MATCH (combined LLM semantic selection, à la Claude Code progressive disclosure)
|
|
17
|
+
const { spec, winner } = await this.senseAndMatch(input);
|
|
18
|
+
yield { type: 'text-delta', content: `complexity=${spec.task.estimatedComplexity.toFixed(2)} domains=${spec.domainHints.join(',')} ` };
|
|
21
19
|
if (!winner) {
|
|
22
20
|
yield { type: 'error', error: new AuctionNoWinnerError(spec.task.taskId), recoverable: false };
|
|
23
21
|
yield { type: 'done', content: '', usage: totalUsage, steps: 1, durationMs: Date.now() - startTime };
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
|
-
yield { type: 'text-delta', content: `
|
|
24
|
+
yield { type: 'text-delta', content: `winner=${winner.id} ` };
|
|
27
25
|
// Phase 3+4: SCALE + EXECUTE
|
|
28
26
|
const result = await this.scaleAndExecute(winner, spec, input);
|
|
29
27
|
totalUsage.totalTokens += result.tokenCost;
|
|
@@ -33,105 +31,86 @@ export class AmoebaLoop {
|
|
|
33
31
|
yield { type: 'text-delta', content: `\n[Adapt] reward=${adaptInfo.reward.toFixed(2)} evolved=${adaptInfo.evolved} recycled=${adaptInfo.recycled}\n` };
|
|
34
32
|
yield { type: 'done', content: result.content, usage: totalUsage, steps: 1, durationMs: Date.now() - startTime };
|
|
35
33
|
}
|
|
36
|
-
// ── Phase 1: SENSE ──
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
// ── Phase 1+2: SENSE + MATCH (Claude Code-style LLM semantic selection) ──
|
|
35
|
+
async senseAndMatch(input) {
|
|
36
|
+
// Collect all available skill descriptions (progressive disclosure L1: name + description)
|
|
37
|
+
const skillDescriptions = this.collectSkillDescriptions();
|
|
38
|
+
const { complexity, selectedSkill, domains } = await this.llmSenseAndMatch(input, skillDescriptions);
|
|
39
|
+
const calibrated = this.calibrate({ score: complexity, domains, method: 'llm' });
|
|
40
|
+
const spec = {
|
|
41
|
+
task: {
|
|
42
|
+
taskId: `amoeba-${Date.now()}`,
|
|
43
|
+
domain: selectedSkill ?? 'general',
|
|
44
|
+
description: input,
|
|
45
|
+
estimatedComplexity: calibrated.score,
|
|
46
|
+
priority: 'normal',
|
|
47
|
+
tokenBudget: calibrated.score < 0.4 ? 2048 : calibrated.score < 0.7 ? 4096 : 8192,
|
|
48
|
+
},
|
|
49
|
+
objective: input,
|
|
50
|
+
domainHints: calibrated.domains,
|
|
50
51
|
};
|
|
51
|
-
|
|
52
|
+
// Find or load the selected skill node
|
|
53
|
+
const winner = selectedSkill ? this.resolveSkillNode(selectedSkill) : this.config.cluster.findIdle();
|
|
54
|
+
return { spec, winner };
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
56
|
+
/** Collect name+description from loaded nodes and unloaded catalog */
|
|
57
|
+
collectSkillDescriptions() {
|
|
58
|
+
const fromNodes = [...this.config.cluster.nodes.values()]
|
|
59
|
+
.filter(n => n.id.startsWith('skill:'))
|
|
60
|
+
.map(n => {
|
|
61
|
+
const skillName = n.id.replace('skill:', '');
|
|
62
|
+
const skill = this.config.skillRegistry.get(skillName);
|
|
63
|
+
return { name: skillName, description: skill?.description ?? skillName };
|
|
64
|
+
});
|
|
65
|
+
const loadedNames = new Set(fromNodes.map(n => n.name));
|
|
66
|
+
const fromCatalog = this.config.skillRegistry.describeAll()
|
|
67
|
+
.filter(s => !loadedNames.has(s.name));
|
|
68
|
+
return [...fromNodes, ...fromCatalog];
|
|
66
69
|
}
|
|
67
|
-
|
|
70
|
+
/** Single LLM call: estimate complexity + select best skill */
|
|
71
|
+
async llmSenseAndMatch(input, skills) {
|
|
72
|
+
const skillList = skills.map(s => `- ${s.name}: ${s.description}`).join('\n');
|
|
68
73
|
try {
|
|
69
74
|
const result = await this.config.llm.complete({
|
|
70
75
|
messages: [{
|
|
71
76
|
role: 'user',
|
|
72
|
-
content: `
|
|
77
|
+
content: `You are a task router. Given available skills and a task, select the BEST skill and assess complexity.\n\nAvailable skills:\n${skillList}\n\nReply ONLY with JSON: {"skill":"<name>","complexity":0.0-1.0,"domains":["..."]}\n\nTask: ${input}`,
|
|
73
78
|
}],
|
|
74
79
|
temperature: 0,
|
|
75
80
|
maxTokens: 128,
|
|
76
81
|
});
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
const obj = JSON.parse(
|
|
82
|
+
const m = result.content.match(/\{[\s\S]*\}/);
|
|
83
|
+
if (m) {
|
|
84
|
+
const obj = JSON.parse(m[0]);
|
|
85
|
+
const name = typeof obj.skill === 'string' ? obj.skill.trim().toLowerCase() : null;
|
|
86
|
+
const matched = skills.find(s => s.name === name);
|
|
80
87
|
return {
|
|
81
|
-
|
|
88
|
+
complexity: Math.max(0, Math.min(1, obj.complexity ?? 0.5)),
|
|
89
|
+
selectedSkill: matched?.name ?? null,
|
|
82
90
|
domains: obj.domains ?? ['general'],
|
|
83
|
-
reasoning: obj.reasoning,
|
|
84
|
-
method: 'llm',
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
catch { /* fall through */ }
|
|
89
|
-
return
|
|
95
|
+
return { complexity: 0.5, selectedSkill: skills[0]?.name ?? null, domains: ['general'] };
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
// ── Phase 2: MATCH ──
|
|
108
|
-
async match(spec, input) {
|
|
109
|
-
// Tier 1: auction across loaded nodes
|
|
110
|
-
const auctionWinner = this.config.cluster.selectWinner(spec.task);
|
|
111
|
-
if (auctionWinner)
|
|
112
|
-
return { winner: auctionWinner, tier: 1 };
|
|
113
|
-
// Tier 2: scan unloaded skill catalog
|
|
114
|
-
const skillMatch = await this.config.skillRegistry.findMatch(input);
|
|
115
|
-
if (skillMatch) {
|
|
116
|
-
const node = this.config.runtime
|
|
117
|
-
? this.config.runtime.loadSkill(skillMatch.skill)
|
|
118
|
-
: (() => { const n = skillToNode(skillMatch.skill, this.config.llm); this.config.cluster.addNode(n); this.config.skillRegistry.markLoaded(skillMatch.skill.name); return n; })();
|
|
119
|
-
return { winner: node, tier: 2 };
|
|
120
|
-
}
|
|
121
|
-
// Tier 3: skill evolution via LLM (opt-in)
|
|
122
|
-
if (this.config.skillEvolver?.tools?.[0]) {
|
|
123
|
-
try {
|
|
124
|
-
const res = await this.config.skillEvolver.tools[0].execute({ task: input }, {});
|
|
125
|
-
const r = res;
|
|
126
|
-
if (r.success && r.nodeId) {
|
|
127
|
-
const node = this.config.cluster.nodes.get(r.nodeId);
|
|
128
|
-
if (node)
|
|
129
|
-
return { winner: node, tier: 3 };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch { /* tier 3 failed, fall through */ }
|
|
97
|
+
/** Resolve a skill name to a loaded node, lazy-loading if needed */
|
|
98
|
+
resolveSkillNode(skillName) {
|
|
99
|
+
const nodeId = `skill:${skillName}`;
|
|
100
|
+
const existing = this.config.cluster.nodes.get(nodeId);
|
|
101
|
+
if (existing && existing.status !== 'dying')
|
|
102
|
+
return existing;
|
|
103
|
+
// Lazy load from catalog
|
|
104
|
+
const skill = this.config.skillRegistry.get(skillName);
|
|
105
|
+
if (skill) {
|
|
106
|
+
if (this.config.runtime)
|
|
107
|
+
return this.config.runtime.loadSkill(skill);
|
|
108
|
+
const node = skillToNode(skill, this.config.llm);
|
|
109
|
+
this.config.cluster.addNode(node);
|
|
110
|
+
this.config.skillRegistry.markLoaded(skill.name);
|
|
111
|
+
return node;
|
|
133
112
|
}
|
|
134
|
-
return
|
|
113
|
+
return undefined;
|
|
135
114
|
}
|
|
136
115
|
// ── Phase 3+4: SCALE + EXECUTE ──
|
|
137
116
|
async scaleAndExecute(winner, spec, input) {
|
package/dist/skill-registry.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export declare class SkillNodeRegistry {
|
|
|
13
13
|
get(name: string): Skill | undefined;
|
|
14
14
|
/** Find best matching unloaded skill for a task description */
|
|
15
15
|
findMatch(input: string, minScore?: number): Promise<SkillActivation | undefined>;
|
|
16
|
+
/** Return name+description of ALL skills (for LLM semantic matching) */
|
|
17
|
+
describeAll(): {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}[];
|
|
16
21
|
get size(): number;
|
|
17
22
|
get unloadedCount(): number;
|
|
18
23
|
}
|
package/dist/skill-registry.js
CHANGED
|
@@ -36,6 +36,10 @@ export class SkillNodeRegistry {
|
|
|
36
36
|
candidates.sort((a, b) => b.score - a.score);
|
|
37
37
|
return candidates[0];
|
|
38
38
|
}
|
|
39
|
+
/** Return name+description of ALL skills (for LLM semantic matching) */
|
|
40
|
+
describeAll() {
|
|
41
|
+
return [...this.catalog.values()].map(s => ({ name: s.name, description: s.description }));
|
|
42
|
+
}
|
|
39
43
|
get size() { return this.catalog.size; }
|
|
40
44
|
get unloadedCount() { return this.catalog.size - this.loaded.size; }
|
|
41
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loom-node/amoeba",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@loom-node/core": "0.1.
|
|
21
|
+
"@loom-node/core": "0.1.1"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -b",
|