@loom-node/amoeba 0.1.0 → 0.1.2

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.
@@ -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 sense;
29
- private heuristicComplexity;
30
- private llmComplexity;
31
- private detectDomains;
32
- private match;
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;
@@ -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.sense(input);
18
- yield { type: 'text-delta', content: `[Sense] complexity=${spec.task.estimatedComplexity.toFixed(2)} domains=[${spec.domainHints}]\n` };
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: `[Match] winner=${winner.id} tier=${tier}\n` };
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 sense(input) {
38
- const threshold = this.config.complexityLlmThreshold ?? 200;
39
- const estimate = input.length < threshold
40
- ? this.heuristicComplexity(input)
41
- : await this.llmComplexity(input);
42
- const calibrated = this.calibrate(estimate);
43
- const task = {
44
- taskId: `amoeba-${Date.now()}`,
45
- domain: calibrated.domains[0] ?? 'general',
46
- description: input,
47
- estimatedComplexity: calibrated.score,
48
- priority: 'normal',
49
- tokenBudget: calibrated.score < 0.4 ? 2048 : calibrated.score < 0.7 ? 4096 : 8192,
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
- return { task, objective: input, domainHints: calibrated.domains };
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
- heuristicComplexity(input) {
54
- const words = input.split(/\s+/).length;
55
- const sentences = (input.match(/[.!?。!?]/g) ?? []).length;
56
- const hasList = /\d+[.)]|[-*•]/.test(input);
57
- const domains = this.detectDomains(input);
58
- let score = Math.min(words / 200, 0.5);
59
- if (sentences > 2)
60
- score += 0.15;
61
- if (hasList)
62
- score += 0.1;
63
- if (domains.length > 2)
64
- score += 0.15;
65
- return { score: Math.min(score, 1), domains, method: 'heuristic' };
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
- async llmComplexity(input) {
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: `Assess this task. Reply ONLY with JSON: {"score":0.0-1.0,"domains":["..."],"reasoning":"..."}\n\nTask: ${input}`,
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 match = result.content.match(/\{[\s\S]*\}/);
78
- if (match) {
79
- const obj = JSON.parse(match[0]);
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
- score: Math.max(0, Math.min(1, obj.score ?? 0.5)),
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 this.heuristicComplexity(input);
95
+ return { complexity: 0.5, selectedSkill: skills[0]?.name ?? null, domains: ['general'] };
90
96
  }
91
- detectDomains(input) {
92
- const lower = input.toLowerCase();
93
- const domainKeywords = {
94
- code: ['code', 'function', 'bug', 'api', 'implement', 'refactor'],
95
- data: ['data', 'database', 'query', 'sql', 'schema'],
96
- writing: ['write', 'draft', 'essay', 'article', 'document'],
97
- math: ['calculate', 'equation', 'formula', 'math', 'statistics'],
98
- research: ['research', 'analyze', 'compare', 'evaluate', 'study'],
99
- };
100
- const found = [];
101
- for (const [domain, kws] of Object.entries(domainKeywords)) {
102
- if (kws.some(k => lower.includes(k)))
103
- found.push(domain);
104
- }
105
- return found.length > 0 ? found : ['general'];
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 { winner: undefined, tier: 1 };
113
+ return undefined;
135
114
  }
136
115
  // ── Phase 3+4: SCALE + EXECUTE ──
137
116
  async scaleAndExecute(winner, spec, input) {
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { AmoebaRuntimeConfig } from './runtime.js';
14
14
  export { AuctionNoWinnerError, MitosisError, ApoptosisRejectedError } from './errors.js';
15
15
  export { skillToNode } from './skill-node.js';
16
16
  export { SkillNodeRegistry } from './skill-registry.js';
17
+ export { SkillCatalogProvider } from './skill-catalog-provider.js';
17
18
  export { createSkillEvolver } from './skill-evolver.js';
18
19
  export type { SkillDiscoverer } from './orchestrate-strategy.js';
19
20
  export { AmoebaLoop } from './amoeba-loop.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { AmoebaRuntime } from './runtime.js';
9
9
  export { AuctionNoWinnerError, MitosisError, ApoptosisRejectedError } from './errors.js';
10
10
  export { skillToNode } from './skill-node.js';
11
11
  export { SkillNodeRegistry } from './skill-registry.js';
12
+ export { SkillCatalogProvider } from './skill-catalog-provider.js';
12
13
  export { createSkillEvolver } from './skill-evolver.js';
13
14
  export { AmoebaLoop } from './amoeba-loop.js';
14
15
  export { MitosisScratchpad } from './mitosis-scratchpad.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SkillCatalogProvider — LLM semantic routing for progressive skill loading.
3
+ *
4
+ * Implements Progressive Disclosure as a ContextProvider:
5
+ * Level 1 — describeAll() summaries fed to LLM
6
+ * Level 2 — LLM selects best skill → loaded into SkillRegistry
7
+ * Level 3 — SkillProvider handles execution context
8
+ */
9
+ import type { ContextProvider, ContextFragment, LLMProvider, SkillRegistry } from '@loom-node/core';
10
+ import type { SkillNodeRegistry } from './skill-registry.js';
11
+ export declare class SkillCatalogProvider implements ContextProvider {
12
+ private catalog;
13
+ private registry;
14
+ private llm;
15
+ readonly source: "skill";
16
+ constructor(catalog: SkillNodeRegistry, registry: SkillRegistry, llm: LLMProvider);
17
+ provide(query: string, budget: number): Promise<ContextFragment[]>;
18
+ private selectSkill;
19
+ }
@@ -0,0 +1,66 @@
1
+ export class SkillCatalogProvider {
2
+ catalog;
3
+ registry;
4
+ llm;
5
+ source = 'skill';
6
+ constructor(catalog, registry, llm) {
7
+ this.catalog = catalog;
8
+ this.registry = registry;
9
+ this.llm = llm;
10
+ }
11
+ async provide(query, budget) {
12
+ const descs = this.catalog.describeAll()
13
+ .filter(d => !this.catalog.isLoaded(d.name));
14
+ if (descs.length === 0)
15
+ return [];
16
+ const name = await this.selectSkill(query, descs);
17
+ if (!name)
18
+ return [];
19
+ const skill = this.catalog.get(name);
20
+ if (!skill)
21
+ return [];
22
+ // Level 2: activate — load into SkillRegistry
23
+ this.registry.register(skill);
24
+ this.catalog.markLoaded(name);
25
+ if (!skill.instructions)
26
+ return [];
27
+ const tokens = Math.ceil(skill.instructions.length / 4);
28
+ if (tokens > budget)
29
+ return [];
30
+ return [{
31
+ source: 'skill',
32
+ content: skill.instructions,
33
+ tokens,
34
+ relevance: 0.9,
35
+ metadata: { skillName: name, reason: 'llm_semantic_route' },
36
+ }];
37
+ }
38
+ async selectSkill(query, skills) {
39
+ const listing = skills.map(s => `- ${s.name}: ${s.description}`).join('\n');
40
+ try {
41
+ const result = await this.llm.complete({
42
+ messages: [{
43
+ role: 'user',
44
+ content: 'You are a skill router. Given a task and available skills, ' +
45
+ 'pick the BEST matching skill or reply null.\n\n' +
46
+ `Skills:\n${listing}\n\n` +
47
+ 'Reply ONLY JSON: {"skill":"<name>"}\n' +
48
+ 'If none match, reply: null\n\n' +
49
+ `Task: ${query}`,
50
+ }],
51
+ temperature: 0,
52
+ maxTokens: 64,
53
+ });
54
+ const match = result.content.match(/\{[\s\S]*?\}/);
55
+ if (!match)
56
+ return null;
57
+ const obj = JSON.parse(match[0]);
58
+ const picked = obj.skill ?? '';
59
+ const valid = new Set(skills.map(s => s.name));
60
+ return valid.has(picked) ? picked : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ }
@@ -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
  }
@@ -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.0",
3
+ "version": "0.1.2",
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.0"
21
+ "@loom-node/core": "0.1.2"
22
22
  },
23
23
  "scripts": {
24
24
  "build": "tsc -b",