@loom-node/amoeba 0.1.0
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/dist/__tests__/cluster.test.d.ts +1 -0
- package/dist/__tests__/cluster.test.js +54 -0
- package/dist/__tests__/helpers.d.ts +4 -0
- package/dist/__tests__/helpers.js +13 -0
- package/dist/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/__tests__/lifecycle.test.js +59 -0
- package/dist/__tests__/reward-bus.test.d.ts +1 -0
- package/dist/__tests__/reward-bus.test.js +40 -0
- package/dist/amoeba-loop.d.ts +41 -0
- package/dist/amoeba-loop.js +256 -0
- package/dist/cluster-provider.d.ts +9 -0
- package/dist/cluster-provider.js +39 -0
- package/dist/cluster.d.ts +18 -0
- package/dist/cluster.js +68 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +23 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +15 -0
- package/dist/lifecycle.d.ts +42 -0
- package/dist/lifecycle.js +171 -0
- package/dist/mitosis-provider.d.ts +10 -0
- package/dist/mitosis-provider.js +61 -0
- package/dist/mitosis-scratchpad.d.ts +8 -0
- package/dist/mitosis-scratchpad.js +23 -0
- package/dist/orchestrate-strategy.d.ts +21 -0
- package/dist/orchestrate-strategy.js +78 -0
- package/dist/plan-strategy.d.ts +9 -0
- package/dist/plan-strategy.js +39 -0
- package/dist/planner.d.ts +14 -0
- package/dist/planner.js +64 -0
- package/dist/reward-bus.d.ts +28 -0
- package/dist/reward-bus.js +68 -0
- package/dist/runtime.d.ts +64 -0
- package/dist/runtime.js +139 -0
- package/dist/skill-evolver.d.ts +7 -0
- package/dist/skill-evolver.js +89 -0
- package/dist/skill-node.d.ts +11 -0
- package/dist/skill-node.js +41 -0
- package/dist/skill-registry.d.ts +18 -0
- package/dist/skill-registry.js +71 -0
- package/package.json +27 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const DEFAULT_CONFIG = {
|
|
2
|
+
strategy: 'rule',
|
|
3
|
+
llmJudgeInterval: 10,
|
|
4
|
+
weights: { quality: 0.5, efficiency: 0.3, reliability: 0.2 },
|
|
5
|
+
ema: { alpha: 0.3, decayRate: 0.95 },
|
|
6
|
+
};
|
|
7
|
+
export class RewardBus {
|
|
8
|
+
config;
|
|
9
|
+
evalCount = 0;
|
|
10
|
+
bias = 0;
|
|
11
|
+
llmJudge;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
14
|
+
}
|
|
15
|
+
setLLMJudge(judge) {
|
|
16
|
+
this.llmJudge = judge;
|
|
17
|
+
}
|
|
18
|
+
async evaluate(node, task, result) {
|
|
19
|
+
const signal = await this.computeSignal(task, result);
|
|
20
|
+
const reward = this.computeReward(signal);
|
|
21
|
+
this.updateCapability(node, task.domain, reward);
|
|
22
|
+
node.rewardHistory.push({
|
|
23
|
+
taskId: task.taskId, reward, domain: task.domain,
|
|
24
|
+
tokenCost: result.tokenCost, timestamp: Date.now(),
|
|
25
|
+
});
|
|
26
|
+
return reward;
|
|
27
|
+
}
|
|
28
|
+
decayInactive(node) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [domain, score] of node.capabilities.scores) {
|
|
31
|
+
const last = [...node.rewardHistory].reverse().find(r => r.domain === domain);
|
|
32
|
+
const days = (now - (last?.timestamp ?? 0)) / 86_400_000;
|
|
33
|
+
if (days > 1) {
|
|
34
|
+
node.capabilities.scores.set(domain, score * (this.config.ema.decayRate ** days));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async computeSignal(task, result) {
|
|
39
|
+
const rule = this.ruleSignal(task, result);
|
|
40
|
+
if (this.config.strategy === 'hybrid' && this.llmJudge && ++this.evalCount % this.config.llmJudgeInterval === 0) {
|
|
41
|
+
const llm = await this.llmJudge(task, result);
|
|
42
|
+
this.bias = llm.quality - rule.quality;
|
|
43
|
+
return llm;
|
|
44
|
+
}
|
|
45
|
+
rule.quality = clamp(rule.quality + this.bias);
|
|
46
|
+
return rule;
|
|
47
|
+
}
|
|
48
|
+
ruleSignal(task, result) {
|
|
49
|
+
const budget = task.tokenBudget ?? 4096;
|
|
50
|
+
return {
|
|
51
|
+
quality: result.success ? 0.7 : 0.0,
|
|
52
|
+
efficiency: 1 - Math.min(result.tokenCost / budget, 1),
|
|
53
|
+
reliability: result.errorCount === 0 ? 1.0 : 0.0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
computeReward(s) {
|
|
57
|
+
const w = this.config.weights;
|
|
58
|
+
return s.quality * w.quality + s.efficiency * w.efficiency + s.reliability * w.reliability;
|
|
59
|
+
}
|
|
60
|
+
updateCapability(node, domain, reward) {
|
|
61
|
+
const α = this.config.ema.alpha;
|
|
62
|
+
const current = node.capabilities.scores.get(domain) ?? 0.5;
|
|
63
|
+
node.capabilities.scores.set(domain, α * reward + (1 - α) * current);
|
|
64
|
+
node.capabilities.totalTasks++;
|
|
65
|
+
node.capabilities.successRate = α * (reward > 0.5 ? 1 : 0) + (1 - α) * node.capabilities.successRate;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function clamp(v) { return Math.max(0, Math.min(1, v)); }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentNode, LLMProvider, Skill, MitosisContext, ContextProvider, ContextOrchestrator, MemoryManager } from '@loom-node/core';
|
|
2
|
+
import { Agent } from '@loom-node/core';
|
|
3
|
+
import { ClusterManager } from './cluster.js';
|
|
4
|
+
import type { AuctionConfig } from './cluster.js';
|
|
5
|
+
import { RewardBus } from './reward-bus.js';
|
|
6
|
+
import type { RewardConfig } from './reward-bus.js';
|
|
7
|
+
import { TaskPlanner } from './planner.js';
|
|
8
|
+
import type { PlannerConfig } from './planner.js';
|
|
9
|
+
import { LifecycleManager } from './lifecycle.js';
|
|
10
|
+
import type { LifecycleConfig } from './lifecycle.js';
|
|
11
|
+
import { ClusterProvider } from './cluster-provider.js';
|
|
12
|
+
import { PlanStrategy } from './plan-strategy.js';
|
|
13
|
+
import { OrchestrateStrategy } from './orchestrate-strategy.js';
|
|
14
|
+
import { AmoebaLoop } from './amoeba-loop.js';
|
|
15
|
+
import type { AmoebaLoopConfig } from './amoeba-loop.js';
|
|
16
|
+
import { SkillNodeRegistry } from './skill-registry.js';
|
|
17
|
+
export type AgentFactory = (domain: string, context?: MitosisContext) => Agent;
|
|
18
|
+
export interface AmoebaRuntimeConfig {
|
|
19
|
+
llm: LLMProvider;
|
|
20
|
+
agentFactory?: AgentFactory;
|
|
21
|
+
skills?: Skill[];
|
|
22
|
+
auction?: Partial<AuctionConfig>;
|
|
23
|
+
reward?: Partial<RewardConfig>;
|
|
24
|
+
planner?: Partial<Omit<PlannerConfig, 'llm'>>;
|
|
25
|
+
lifecycle?: Partial<LifecycleConfig>;
|
|
26
|
+
healthCheckIntervalMs?: number;
|
|
27
|
+
memory?: MemoryManager;
|
|
28
|
+
contextProviders?: ContextProvider[];
|
|
29
|
+
contextWindow?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class AmoebaRuntime {
|
|
32
|
+
readonly cluster: ClusterManager;
|
|
33
|
+
readonly rewardBus: RewardBus;
|
|
34
|
+
readonly planner: TaskPlanner;
|
|
35
|
+
readonly lifecycle: LifecycleManager;
|
|
36
|
+
readonly agentFactory?: AgentFactory;
|
|
37
|
+
readonly skillRegistry: SkillNodeRegistry;
|
|
38
|
+
private readonly llm;
|
|
39
|
+
private readonly _memory?;
|
|
40
|
+
private readonly sharedProviders;
|
|
41
|
+
private readonly contextWindow;
|
|
42
|
+
private healthTimer?;
|
|
43
|
+
constructor(config: AmoebaRuntimeConfig);
|
|
44
|
+
addNode(node: AgentNode): void;
|
|
45
|
+
createNode(agent: Agent, opts?: {
|
|
46
|
+
id?: string;
|
|
47
|
+
domain?: string;
|
|
48
|
+
tools?: string[];
|
|
49
|
+
}): AgentNode;
|
|
50
|
+
getMemory(): MemoryManager | undefined;
|
|
51
|
+
buildContextFor(agentId: string, opts?: {
|
|
52
|
+
mitosisCtx?: MitosisContext;
|
|
53
|
+
}): ContextOrchestrator | undefined;
|
|
54
|
+
/** Load a skill as an active node in the cluster */
|
|
55
|
+
loadSkill(skill: Skill): AgentNode;
|
|
56
|
+
/** Discover and lazy-load a matching skill when auction finds no good candidate */
|
|
57
|
+
discoverSkill(input: string, minScore?: number): Promise<AgentNode | undefined>;
|
|
58
|
+
providerFor(agentId: string): ClusterProvider;
|
|
59
|
+
planStrategy(): PlanStrategy;
|
|
60
|
+
orchestrateStrategy(): OrchestrateStrategy;
|
|
61
|
+
amoebaLoop(opts?: Partial<Pick<AmoebaLoopConfig, 'skillEvolver' | 'complexityLlmThreshold' | 'evolutionRewardThreshold' | 'evolutionWindow'>>): AmoebaLoop;
|
|
62
|
+
healthCheck(): void;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ContextOrchestratorImpl } from '@loom-node/core';
|
|
2
|
+
import { ClusterManager } from './cluster.js';
|
|
3
|
+
import { RewardBus } from './reward-bus.js';
|
|
4
|
+
import { TaskPlanner } from './planner.js';
|
|
5
|
+
import { LifecycleManager } from './lifecycle.js';
|
|
6
|
+
import { ClusterProvider } from './cluster-provider.js';
|
|
7
|
+
import { PlanStrategy } from './plan-strategy.js';
|
|
8
|
+
import { OrchestrateStrategy } from './orchestrate-strategy.js';
|
|
9
|
+
import { AmoebaLoop } from './amoeba-loop.js';
|
|
10
|
+
import { SkillNodeRegistry } from './skill-registry.js';
|
|
11
|
+
import { skillToNode } from './skill-node.js';
|
|
12
|
+
import { MitosisContextProvider } from './mitosis-provider.js';
|
|
13
|
+
export class AmoebaRuntime {
|
|
14
|
+
cluster;
|
|
15
|
+
rewardBus;
|
|
16
|
+
planner;
|
|
17
|
+
lifecycle;
|
|
18
|
+
agentFactory;
|
|
19
|
+
skillRegistry;
|
|
20
|
+
llm;
|
|
21
|
+
_memory;
|
|
22
|
+
sharedProviders;
|
|
23
|
+
contextWindow;
|
|
24
|
+
healthTimer;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.llm = config.llm;
|
|
27
|
+
this.cluster = new ClusterManager(config.auction);
|
|
28
|
+
this.rewardBus = new RewardBus(config.reward);
|
|
29
|
+
this.planner = new TaskPlanner({ llm: config.llm, ...config.planner });
|
|
30
|
+
this.lifecycle = new LifecycleManager(config.lifecycle);
|
|
31
|
+
this.agentFactory = config.agentFactory;
|
|
32
|
+
this.skillRegistry = new SkillNodeRegistry();
|
|
33
|
+
this._memory = config.memory;
|
|
34
|
+
this.sharedProviders = config.contextProviders ?? [];
|
|
35
|
+
this.contextWindow = config.contextWindow ?? 128_000;
|
|
36
|
+
if (config.skills)
|
|
37
|
+
this.skillRegistry.registerAll(config.skills);
|
|
38
|
+
if (config.healthCheckIntervalMs) {
|
|
39
|
+
this.healthTimer = setInterval(() => this.healthCheck(), config.healthCheckIntervalMs);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
addNode(node) {
|
|
43
|
+
this.cluster.addNode(node);
|
|
44
|
+
}
|
|
45
|
+
createNode(agent, opts) {
|
|
46
|
+
const node = {
|
|
47
|
+
id: opts?.id ?? `node-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
48
|
+
depth: 0,
|
|
49
|
+
capabilities: {
|
|
50
|
+
scores: new Map(opts?.domain ? [[opts.domain, 0.5]] : [['general', 0.5]]),
|
|
51
|
+
tools: opts?.tools ?? [],
|
|
52
|
+
totalTasks: 0,
|
|
53
|
+
successRate: 0,
|
|
54
|
+
},
|
|
55
|
+
rewardHistory: [],
|
|
56
|
+
status: 'idle',
|
|
57
|
+
load: 0,
|
|
58
|
+
agent,
|
|
59
|
+
lastActiveAt: Date.now(),
|
|
60
|
+
consecutiveLosses: 0,
|
|
61
|
+
};
|
|
62
|
+
this.cluster.addNode(node);
|
|
63
|
+
return node;
|
|
64
|
+
}
|
|
65
|
+
getMemory() {
|
|
66
|
+
return this._memory;
|
|
67
|
+
}
|
|
68
|
+
buildContextFor(agentId, opts) {
|
|
69
|
+
const providers = [
|
|
70
|
+
new ClusterProvider(this.cluster, agentId),
|
|
71
|
+
...this.sharedProviders,
|
|
72
|
+
];
|
|
73
|
+
if (opts?.mitosisCtx) {
|
|
74
|
+
const mc = opts.mitosisCtx;
|
|
75
|
+
providers.push(new MitosisContextProvider(mc.parentTaskSpec, mc.subtask, mc.scratchpad));
|
|
76
|
+
}
|
|
77
|
+
const orchestrator = new ContextOrchestratorImpl({
|
|
78
|
+
contextWindow: this.contextWindow,
|
|
79
|
+
outputReserveRatio: 0.25,
|
|
80
|
+
});
|
|
81
|
+
for (const p of providers)
|
|
82
|
+
orchestrator.register(p);
|
|
83
|
+
return orchestrator;
|
|
84
|
+
}
|
|
85
|
+
/** Load a skill as an active node in the cluster */
|
|
86
|
+
loadSkill(skill) {
|
|
87
|
+
const nodeId = `skill:${skill.name}`;
|
|
88
|
+
const context = this.buildContextFor(nodeId);
|
|
89
|
+
const node = skillToNode(skill, this.llm, { context, memory: this._memory });
|
|
90
|
+
this.cluster.addNode(node);
|
|
91
|
+
this.skillRegistry.markLoaded(skill.name);
|
|
92
|
+
return node;
|
|
93
|
+
}
|
|
94
|
+
/** Discover and lazy-load a matching skill when auction finds no good candidate */
|
|
95
|
+
async discoverSkill(input, minScore = 0.3) {
|
|
96
|
+
const match = await this.skillRegistry.findMatch(input, minScore);
|
|
97
|
+
if (!match)
|
|
98
|
+
return undefined;
|
|
99
|
+
return this.loadSkill(match.skill);
|
|
100
|
+
}
|
|
101
|
+
providerFor(agentId) {
|
|
102
|
+
return new ClusterProvider(this.cluster, agentId);
|
|
103
|
+
}
|
|
104
|
+
planStrategy() {
|
|
105
|
+
return new PlanStrategy(this.planner);
|
|
106
|
+
}
|
|
107
|
+
orchestrateStrategy() {
|
|
108
|
+
return new OrchestrateStrategy(this.cluster, this.rewardBus, this.lifecycle, this.planner, this.agentFactory, (input) => this.discoverSkill(input), this);
|
|
109
|
+
}
|
|
110
|
+
amoebaLoop(opts) {
|
|
111
|
+
return new AmoebaLoop({
|
|
112
|
+
cluster: this.cluster,
|
|
113
|
+
rewardBus: this.rewardBus,
|
|
114
|
+
lifecycle: this.lifecycle,
|
|
115
|
+
planner: this.planner,
|
|
116
|
+
skillRegistry: this.skillRegistry,
|
|
117
|
+
llm: this.llm,
|
|
118
|
+
agentFactory: this.agentFactory,
|
|
119
|
+
runtime: this,
|
|
120
|
+
...opts,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
healthCheck() {
|
|
124
|
+
for (const node of this.cluster.nodes.values()) {
|
|
125
|
+
const report = this.lifecycle.checkHealth(node);
|
|
126
|
+
if (report.recommendation === 'recycle') {
|
|
127
|
+
try {
|
|
128
|
+
this.lifecycle.apoptosis(node, this.cluster);
|
|
129
|
+
}
|
|
130
|
+
catch { /* rejected, skip */ }
|
|
131
|
+
}
|
|
132
|
+
this.rewardBus.decayInactive(node);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
dispose() {
|
|
136
|
+
if (this.healthTimer)
|
|
137
|
+
clearInterval(this.healthTimer);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Skill, LLMProvider } from '@loom-node/core';
|
|
2
|
+
import type { AmoebaRuntime } from './runtime.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a meta-skill that can generate/modify other skills via LLM.
|
|
5
|
+
* User opt-in: load this skill into the runtime to enable skill evolution.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createSkillEvolver(llm: LLMProvider, runtime: AmoebaRuntime): Skill;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const EVOLVE_PROMPT = `You are a skill architect. Given a task description and optional feedback about an existing skill's performance, generate or modify a skill definition.
|
|
2
|
+
|
|
3
|
+
Output ONLY valid JSON with this structure:
|
|
4
|
+
{
|
|
5
|
+
"name": "skill-name",
|
|
6
|
+
"description": "what this skill does",
|
|
7
|
+
"instructions": "system prompt for the skill agent",
|
|
8
|
+
"keywords": ["trigger", "keywords"]
|
|
9
|
+
}`;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a meta-skill that can generate/modify other skills via LLM.
|
|
12
|
+
* User opt-in: load this skill into the runtime to enable skill evolution.
|
|
13
|
+
*/
|
|
14
|
+
export function createSkillEvolver(llm, runtime) {
|
|
15
|
+
const evolveTool = {
|
|
16
|
+
name: 'evolve_skill',
|
|
17
|
+
description: 'Create or modify a skill based on task requirements and feedback',
|
|
18
|
+
parameters: {
|
|
19
|
+
parse: (input) => input,
|
|
20
|
+
toJsonSchema: () => ({
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
task: { type: 'string', description: 'Task description that needs a skill' },
|
|
24
|
+
existingSkill: { type: 'string', description: 'Name of existing skill to modify' },
|
|
25
|
+
feedback: { type: 'string', description: 'Performance feedback for modification' },
|
|
26
|
+
},
|
|
27
|
+
required: ['task'],
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
execute: async (input) => {
|
|
31
|
+
const messages = buildEvolveMessages(input, runtime);
|
|
32
|
+
const result = await llm.complete({ messages, temperature: 0.3, maxTokens: 512 });
|
|
33
|
+
const def = parseSkillDef(result.content);
|
|
34
|
+
if (!def)
|
|
35
|
+
return { success: false, error: 'Failed to parse skill definition' };
|
|
36
|
+
const skill = {
|
|
37
|
+
name: def.name,
|
|
38
|
+
description: def.description,
|
|
39
|
+
instructions: def.instructions,
|
|
40
|
+
activationLevel: 'on-demand',
|
|
41
|
+
trigger: { type: 'keyword', keywords: def.keywords },
|
|
42
|
+
priority: 0.5,
|
|
43
|
+
};
|
|
44
|
+
runtime.skillRegistry.register(skill);
|
|
45
|
+
const node = runtime.loadSkill(skill);
|
|
46
|
+
return { success: true, skillName: def.name, nodeId: node.id };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
name: 'skill-evolver',
|
|
51
|
+
description: 'Meta-skill that creates and modifies other skills based on task needs',
|
|
52
|
+
instructions: 'You can create new skills or modify existing ones. Use the evolve_skill tool when a task needs a specialized skill that does not exist.',
|
|
53
|
+
activationLevel: 'on-demand',
|
|
54
|
+
tools: [evolveTool],
|
|
55
|
+
trigger: { type: 'keyword', keywords: ['create skill', 'new skill', 'modify skill', 'evolve'] },
|
|
56
|
+
priority: 0.3,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildEvolveMessages(input, runtime) {
|
|
60
|
+
let content = EVOLVE_PROMPT + '\n\nTask: ' + input.task;
|
|
61
|
+
if (input.existingSkill) {
|
|
62
|
+
const existing = runtime.skillRegistry.get(input.existingSkill);
|
|
63
|
+
if (existing) {
|
|
64
|
+
content += `\n\nExisting skill "${existing.name}": ${existing.instructions}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (input.feedback)
|
|
68
|
+
content += '\n\nFeedback: ' + input.feedback;
|
|
69
|
+
return [{ role: 'user', content }];
|
|
70
|
+
}
|
|
71
|
+
function parseSkillDef(text) {
|
|
72
|
+
try {
|
|
73
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
74
|
+
if (!match)
|
|
75
|
+
return null;
|
|
76
|
+
const obj = JSON.parse(match[0]);
|
|
77
|
+
if (!obj.name || !obj.instructions)
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
name: obj.name,
|
|
81
|
+
description: obj.description ?? obj.name,
|
|
82
|
+
instructions: obj.instructions,
|
|
83
|
+
keywords: obj.keywords ?? [obj.name],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Skill, AgentNode, LLMProvider, ContextOrchestrator, MemoryManager } from '@loom-node/core';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Skill into an AgentNode.
|
|
4
|
+
* Trigger keywords/patterns seed initial capability scores.
|
|
5
|
+
*/
|
|
6
|
+
export declare function skillToNode(skill: Skill, provider: LLMProvider, opts?: {
|
|
7
|
+
parentId?: string;
|
|
8
|
+
depth?: number;
|
|
9
|
+
context?: ContextOrchestrator;
|
|
10
|
+
memory?: MemoryManager;
|
|
11
|
+
}): AgentNode;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Agent } from '@loom-node/core';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Skill into an AgentNode.
|
|
4
|
+
* Trigger keywords/patterns seed initial capability scores.
|
|
5
|
+
*/
|
|
6
|
+
export function skillToNode(skill, provider, opts) {
|
|
7
|
+
const agent = new Agent({
|
|
8
|
+
name: `skill:${skill.name}`,
|
|
9
|
+
provider,
|
|
10
|
+
systemPrompt: skill.instructions,
|
|
11
|
+
tools: skill.tools,
|
|
12
|
+
memory: opts?.memory,
|
|
13
|
+
context: opts?.context,
|
|
14
|
+
});
|
|
15
|
+
const scores = new Map();
|
|
16
|
+
if (skill.trigger?.type === 'keyword') {
|
|
17
|
+
for (const kw of skill.trigger.keywords)
|
|
18
|
+
scores.set(kw, 0.6);
|
|
19
|
+
}
|
|
20
|
+
if (skill.trigger?.type === 'semantic') {
|
|
21
|
+
scores.set(skill.trigger.description, 0.6);
|
|
22
|
+
}
|
|
23
|
+
scores.set(skill.name, 0.7);
|
|
24
|
+
return {
|
|
25
|
+
id: `skill:${skill.name}`,
|
|
26
|
+
parentId: opts?.parentId,
|
|
27
|
+
depth: opts?.depth ?? 0,
|
|
28
|
+
capabilities: {
|
|
29
|
+
scores,
|
|
30
|
+
tools: (skill.tools ?? []).map(t => t.name),
|
|
31
|
+
totalTasks: 0,
|
|
32
|
+
successRate: 0,
|
|
33
|
+
},
|
|
34
|
+
rewardHistory: [],
|
|
35
|
+
status: 'idle',
|
|
36
|
+
load: 0,
|
|
37
|
+
agent,
|
|
38
|
+
lastActiveAt: Date.now(),
|
|
39
|
+
consecutiveLosses: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Skill, SkillActivation } from '@loom-node/core';
|
|
2
|
+
/**
|
|
3
|
+
* Catalog of available-but-not-loaded skills.
|
|
4
|
+
* Matches tasks against skill triggers for lazy activation.
|
|
5
|
+
*/
|
|
6
|
+
export declare class SkillNodeRegistry {
|
|
7
|
+
private catalog;
|
|
8
|
+
private loaded;
|
|
9
|
+
register(skill: Skill): void;
|
|
10
|
+
registerAll(skills: Skill[]): void;
|
|
11
|
+
markLoaded(name: string): void;
|
|
12
|
+
isLoaded(name: string): boolean;
|
|
13
|
+
get(name: string): Skill | undefined;
|
|
14
|
+
/** Find best matching unloaded skill for a task description */
|
|
15
|
+
findMatch(input: string, minScore?: number): Promise<SkillActivation | undefined>;
|
|
16
|
+
get size(): number;
|
|
17
|
+
get unloadedCount(): number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog of available-but-not-loaded skills.
|
|
3
|
+
* Matches tasks against skill triggers for lazy activation.
|
|
4
|
+
*/
|
|
5
|
+
export class SkillNodeRegistry {
|
|
6
|
+
catalog = new Map();
|
|
7
|
+
loaded = new Set();
|
|
8
|
+
register(skill) {
|
|
9
|
+
this.catalog.set(skill.name, skill);
|
|
10
|
+
}
|
|
11
|
+
registerAll(skills) {
|
|
12
|
+
for (const s of skills)
|
|
13
|
+
this.register(s);
|
|
14
|
+
}
|
|
15
|
+
markLoaded(name) {
|
|
16
|
+
this.loaded.add(name);
|
|
17
|
+
}
|
|
18
|
+
isLoaded(name) {
|
|
19
|
+
return this.loaded.has(name);
|
|
20
|
+
}
|
|
21
|
+
get(name) {
|
|
22
|
+
return this.catalog.get(name);
|
|
23
|
+
}
|
|
24
|
+
/** Find best matching unloaded skill for a task description */
|
|
25
|
+
async findMatch(input, minScore = 0.3) {
|
|
26
|
+
const candidates = [];
|
|
27
|
+
for (const skill of this.catalog.values()) {
|
|
28
|
+
if (this.loaded.has(skill.name))
|
|
29
|
+
continue;
|
|
30
|
+
const act = await matchSkill(skill, input);
|
|
31
|
+
if (act && act.score >= minScore)
|
|
32
|
+
candidates.push(act);
|
|
33
|
+
}
|
|
34
|
+
if (candidates.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
37
|
+
return candidates[0];
|
|
38
|
+
}
|
|
39
|
+
get size() { return this.catalog.size; }
|
|
40
|
+
get unloadedCount() { return this.catalog.size - this.loaded.size; }
|
|
41
|
+
}
|
|
42
|
+
/** Inline trigger matching — no dependency on @loom-node/skills */
|
|
43
|
+
async function matchSkill(skill, input) {
|
|
44
|
+
if (!skill.trigger) {
|
|
45
|
+
return { skill, score: skill.priority ?? 0, reason: 'no-trigger' };
|
|
46
|
+
}
|
|
47
|
+
const t = skill.trigger;
|
|
48
|
+
const lower = input.toLowerCase();
|
|
49
|
+
if (t.type === 'keyword') {
|
|
50
|
+
const matched = t.keywords.filter(k => lower.includes(k.toLowerCase()));
|
|
51
|
+
if (matched.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (t.matchAll && matched.length < t.keywords.length)
|
|
54
|
+
return null;
|
|
55
|
+
const score = (skill.priority ?? 0.5) * (matched.length / t.keywords.length);
|
|
56
|
+
return { skill, score, reason: `keywords: ${matched.join(', ')}` };
|
|
57
|
+
}
|
|
58
|
+
if (t.type === 'pattern') {
|
|
59
|
+
if (!new RegExp(t.pattern, 'i').test(input))
|
|
60
|
+
return null;
|
|
61
|
+
return { skill, score: skill.priority ?? 0.8, reason: `pattern: ${t.pattern}` };
|
|
62
|
+
}
|
|
63
|
+
if (t.type === 'semantic') {
|
|
64
|
+
return { skill, score: t.threshold ?? 0.7, reason: `semantic: ${t.description}` };
|
|
65
|
+
}
|
|
66
|
+
if (t.type === 'custom') {
|
|
67
|
+
const ok = await t.match(input);
|
|
68
|
+
return ok ? { skill, score: skill.priority ?? 1, reason: 'custom' } : null;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loom-node/amoeba",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@loom-node/core": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b",
|
|
25
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
26
|
+
}
|
|
27
|
+
}
|