@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/cluster.test.d.ts +1 -0
  3. package/dist/__tests__/cluster.test.js +54 -0
  4. package/dist/__tests__/helpers.d.ts +4 -0
  5. package/dist/__tests__/helpers.js +13 -0
  6. package/dist/__tests__/lifecycle.test.d.ts +1 -0
  7. package/dist/__tests__/lifecycle.test.js +59 -0
  8. package/dist/__tests__/reward-bus.test.d.ts +1 -0
  9. package/dist/__tests__/reward-bus.test.js +40 -0
  10. package/dist/amoeba-loop.d.ts +41 -0
  11. package/dist/amoeba-loop.js +256 -0
  12. package/dist/cluster-provider.d.ts +9 -0
  13. package/dist/cluster-provider.js +39 -0
  14. package/dist/cluster.d.ts +18 -0
  15. package/dist/cluster.js +68 -0
  16. package/dist/errors.d.ts +12 -0
  17. package/dist/errors.js +23 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +15 -0
  20. package/dist/lifecycle.d.ts +42 -0
  21. package/dist/lifecycle.js +171 -0
  22. package/dist/mitosis-provider.d.ts +10 -0
  23. package/dist/mitosis-provider.js +61 -0
  24. package/dist/mitosis-scratchpad.d.ts +8 -0
  25. package/dist/mitosis-scratchpad.js +23 -0
  26. package/dist/orchestrate-strategy.d.ts +21 -0
  27. package/dist/orchestrate-strategy.js +78 -0
  28. package/dist/plan-strategy.d.ts +9 -0
  29. package/dist/plan-strategy.js +39 -0
  30. package/dist/planner.d.ts +14 -0
  31. package/dist/planner.js +64 -0
  32. package/dist/reward-bus.d.ts +28 -0
  33. package/dist/reward-bus.js +68 -0
  34. package/dist/runtime.d.ts +64 -0
  35. package/dist/runtime.js +139 -0
  36. package/dist/skill-evolver.d.ts +7 -0
  37. package/dist/skill-evolver.js +89 -0
  38. package/dist/skill-node.d.ts +11 -0
  39. package/dist/skill-node.js +41 -0
  40. package/dist/skill-registry.d.ts +18 -0
  41. package/dist/skill-registry.js +71 -0
  42. package/package.json +27 -0
package/dist/errors.js ADDED
@@ -0,0 +1,23 @@
1
+ import { LoomError } from '@loom-node/core';
2
+ export class AuctionNoWinnerError extends LoomError {
3
+ constructor(taskId) {
4
+ super('AUCTION_NO_WINNER', `No agent won auction for task ${taskId}`);
5
+ this.name = 'AuctionNoWinnerError';
6
+ }
7
+ }
8
+ export class MitosisError extends LoomError {
9
+ parentId;
10
+ constructor(parentId, message, cause) {
11
+ super('MITOSIS_FAILED', message, cause);
12
+ this.name = 'MitosisError';
13
+ this.parentId = parentId;
14
+ }
15
+ }
16
+ export class ApoptosisRejectedError extends LoomError {
17
+ nodeId;
18
+ constructor(nodeId, reason) {
19
+ super('APOPTOSIS_REJECTED', `Cannot recycle node ${nodeId}: ${reason}`);
20
+ this.name = 'ApoptosisRejectedError';
21
+ this.nodeId = nodeId;
22
+ }
23
+ }
@@ -0,0 +1,22 @@
1
+ export { RewardBus } from './reward-bus.js';
2
+ export type { RewardConfig } from './reward-bus.js';
3
+ export { TaskPlanner } from './planner.js';
4
+ export type { PlannerConfig } from './planner.js';
5
+ export { ClusterManager } from './cluster.js';
6
+ export type { AuctionConfig } from './cluster.js';
7
+ export { LifecycleManager } from './lifecycle.js';
8
+ export type { MitosisConfig, ApoptosisConfig, LifecycleConfig, HealthReport, HealthStatus, AgentFactory } from './lifecycle.js';
9
+ export { ClusterProvider } from './cluster-provider.js';
10
+ export { PlanStrategy } from './plan-strategy.js';
11
+ export { OrchestrateStrategy } from './orchestrate-strategy.js';
12
+ export { AmoebaRuntime } from './runtime.js';
13
+ export type { AmoebaRuntimeConfig } from './runtime.js';
14
+ export { AuctionNoWinnerError, MitosisError, ApoptosisRejectedError } from './errors.js';
15
+ export { skillToNode } from './skill-node.js';
16
+ export { SkillNodeRegistry } from './skill-registry.js';
17
+ export { createSkillEvolver } from './skill-evolver.js';
18
+ export type { SkillDiscoverer } from './orchestrate-strategy.js';
19
+ export { AmoebaLoop } from './amoeba-loop.js';
20
+ export type { AmoebaLoopConfig } from './amoeba-loop.js';
21
+ export { MitosisScratchpad } from './mitosis-scratchpad.js';
22
+ export { MitosisContextProvider } from './mitosis-provider.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { RewardBus } from './reward-bus.js';
2
+ export { TaskPlanner } from './planner.js';
3
+ export { ClusterManager } from './cluster.js';
4
+ export { LifecycleManager } from './lifecycle.js';
5
+ export { ClusterProvider } from './cluster-provider.js';
6
+ export { PlanStrategy } from './plan-strategy.js';
7
+ export { OrchestrateStrategy } from './orchestrate-strategy.js';
8
+ export { AmoebaRuntime } from './runtime.js';
9
+ export { AuctionNoWinnerError, MitosisError, ApoptosisRejectedError } from './errors.js';
10
+ export { skillToNode } from './skill-node.js';
11
+ export { SkillNodeRegistry } from './skill-registry.js';
12
+ export { createSkillEvolver } from './skill-evolver.js';
13
+ export { AmoebaLoop } from './amoeba-loop.js';
14
+ export { MitosisScratchpad } from './mitosis-scratchpad.js';
15
+ export { MitosisContextProvider } from './mitosis-provider.js';
@@ -0,0 +1,42 @@
1
+ import type { AgentNode, TaskAd, TaskResult, TaskSpec, MitosisContext } from '@loom-node/core';
2
+ import type { Agent } from '@loom-node/core';
3
+ import type { ClusterManager } from './cluster.js';
4
+ import type { TaskPlanner } from './planner.js';
5
+ import type { RewardBus } from './reward-bus.js';
6
+ import type { AmoebaRuntime } from './runtime.js';
7
+ export type AgentFactory = (domain: string, context?: MitosisContext) => Agent;
8
+ export interface MitosisConfig {
9
+ complexityThreshold: number;
10
+ maxChildren: number;
11
+ maxDepth: number;
12
+ }
13
+ export interface ApoptosisConfig {
14
+ minNodes: number;
15
+ maxConsecutiveLosses: number;
16
+ minRewardThreshold: number;
17
+ maxIdleMs: number;
18
+ rewardWindow: number;
19
+ }
20
+ export interface LifecycleConfig {
21
+ mitosis: MitosisConfig;
22
+ apoptosis: ApoptosisConfig;
23
+ }
24
+ export type HealthStatus = 'healthy' | 'warning' | 'dying';
25
+ export interface HealthReport {
26
+ nodeId: string;
27
+ status: HealthStatus;
28
+ recentAvgReward: number;
29
+ idleMs: number;
30
+ recommendation: 'keep' | 'merge' | 'recycle';
31
+ }
32
+ export declare class LifecycleManager {
33
+ private readonly config;
34
+ constructor(config?: Partial<LifecycleConfig>);
35
+ shouldMitosis(node: AgentNode, task: TaskAd): boolean;
36
+ mitosis(parent: AgentNode, task: TaskAd, planner: TaskPlanner, cluster: ClusterManager, rewardBus: RewardBus, agentFactory?: AgentFactory, taskSpec?: TaskSpec, runtime?: AmoebaRuntime): Promise<TaskResult>;
37
+ private buildChildPrompt;
38
+ checkHealth(node: AgentNode): HealthReport;
39
+ findMergeTarget(dying: AgentNode, cluster: ClusterManager): AgentNode | undefined;
40
+ mergeCapabilities(dying: AgentNode, target: AgentNode): void;
41
+ apoptosis(dying: AgentNode, cluster: ClusterManager): boolean;
42
+ }
@@ -0,0 +1,171 @@
1
+ import { ApoptosisRejectedError } from './errors.js';
2
+ import { MitosisScratchpad } from './mitosis-scratchpad.js';
3
+ const DEFAULTS = {
4
+ mitosis: { complexityThreshold: 0.6, maxChildren: 4, maxDepth: 2 },
5
+ apoptosis: { minNodes: 2, maxConsecutiveLosses: 20, minRewardThreshold: 0.3, maxIdleMs: 300_000, rewardWindow: 10 },
6
+ };
7
+ export class LifecycleManager {
8
+ config;
9
+ constructor(config) {
10
+ this.config = {
11
+ mitosis: { ...DEFAULTS.mitosis, ...config?.mitosis },
12
+ apoptosis: { ...DEFAULTS.apoptosis, ...config?.apoptosis },
13
+ };
14
+ }
15
+ shouldMitosis(node, task) {
16
+ if (node.depth >= this.config.mitosis.maxDepth)
17
+ return false;
18
+ return task.estimatedComplexity > this.config.mitosis.complexityThreshold;
19
+ }
20
+ async mitosis(parent, task, planner, cluster, rewardBus, agentFactory, taskSpec, runtime) {
21
+ parent.status = 'splitting';
22
+ const subtasks = await planner.decompose(task);
23
+ const limited = subtasks.slice(0, this.config.mitosis.maxChildren);
24
+ const scratchpad = new MitosisScratchpad();
25
+ const spec = taskSpec ?? { task, objective: task.description, domainHints: [task.domain] };
26
+ const executor = async (sub) => {
27
+ sub.resolvedInputs = scratchpad.readDependencies(sub.dependencies);
28
+ const subTask = {
29
+ taskId: sub.id, domain: sub.domain, description: sub.description,
30
+ estimatedComplexity: task.estimatedComplexity / limited.length,
31
+ priority: task.priority,
32
+ };
33
+ const winner = cluster.selectWinner(subTask);
34
+ let node;
35
+ if (winner) {
36
+ node = winner;
37
+ }
38
+ else if (agentFactory) {
39
+ const childId = `child-${parent.id}-${sub.id}`;
40
+ const mitosisCtx = {
41
+ parentTaskSpec: spec, subtask: sub, scratchpad, parentTools: parent.capabilities.tools,
42
+ context: runtime?.buildContextFor(childId, { mitosisCtx: undefined }),
43
+ memory: runtime?.getMemory(),
44
+ };
45
+ // Re-assign context after mitosisCtx is created (self-reference)
46
+ mitosisCtx.context = runtime?.buildContextFor(childId, { mitosisCtx });
47
+ const child = agentFactory(sub.domain, mitosisCtx);
48
+ node = {
49
+ id: childId,
50
+ depth: parent.depth + 1,
51
+ capabilities: {
52
+ scores: new Map([[sub.domain, 0.5]]),
53
+ tools: [...parent.capabilities.tools], totalTasks: 0, successRate: 0,
54
+ },
55
+ rewardHistory: [], status: 'idle', load: 0,
56
+ agent: child, lastActiveAt: Date.now(), consecutiveLosses: 0,
57
+ };
58
+ cluster.addNode(node);
59
+ }
60
+ else {
61
+ node = parent;
62
+ }
63
+ node.status = 'busy';
64
+ const start = Date.now();
65
+ const prompt = this.buildChildPrompt(sub, spec);
66
+ const result = await node.agent.run(prompt);
67
+ const taskResult = {
68
+ taskId: sub.id, agentId: node.id, content: result.content,
69
+ success: true, tokenCost: result.usage.totalTokens,
70
+ errorCount: 0, durationMs: Date.now() - start,
71
+ };
72
+ scratchpad.write(sub.id, taskResult);
73
+ await rewardBus.evaluate(node, subTask, taskResult);
74
+ node.status = 'idle';
75
+ return taskResult;
76
+ };
77
+ const results = await planner.executeDAG(limited, executor);
78
+ const content = await planner.aggregate(task, results);
79
+ parent.status = 'idle';
80
+ return {
81
+ taskId: task.taskId, agentId: parent.id, content, success: results.every(r => r.success),
82
+ tokenCost: results.reduce((s, r) => s + r.tokenCost, 0),
83
+ errorCount: results.reduce((s, r) => s + r.errorCount, 0),
84
+ durationMs: results.reduce((s, r) => s + r.durationMs, 0),
85
+ metadata: {
86
+ keyFindings: results.flatMap(r => r.metadata?.keyFindings ?? []).slice(0, 5),
87
+ toolsUsed: [...new Set(results.flatMap(r => r.metadata?.toolsUsed ?? []))],
88
+ summary: content.slice(0, 500),
89
+ confidence: results.reduce((s, r) => s + (r.metadata?.confidence ?? 0.5), 0) / results.length,
90
+ },
91
+ };
92
+ }
93
+ buildChildPrompt(sub, spec) {
94
+ let prompt = sub.description;
95
+ if (spec.outputFormat)
96
+ prompt += `\nOutput format: ${spec.outputFormat}`;
97
+ if (spec.boundaries)
98
+ prompt += `\nConstraints: ${spec.boundaries}`;
99
+ if (sub.resolvedInputs && Object.keys(sub.resolvedInputs).length > 0) {
100
+ prompt += '\nContext from prior tasks:\n' +
101
+ Object.entries(sub.resolvedInputs).map(([id, s]) => `[${id}]: ${s}`).join('\n');
102
+ }
103
+ return prompt;
104
+ }
105
+ checkHealth(node) {
106
+ const cfg = this.config.apoptosis;
107
+ const now = Date.now();
108
+ const idleMs = now - node.lastActiveAt;
109
+ const recent = node.rewardHistory.slice(-cfg.rewardWindow);
110
+ const recentAvgReward = recent.length > 0
111
+ ? recent.reduce((s, r) => s + r.reward, 0) / recent.length
112
+ : 0.5;
113
+ if (node.consecutiveLosses >= cfg.maxConsecutiveLosses
114
+ || recentAvgReward < cfg.minRewardThreshold
115
+ || idleMs > cfg.maxIdleMs) {
116
+ return { nodeId: node.id, status: 'dying', recentAvgReward, idleMs, recommendation: 'recycle' };
117
+ }
118
+ if (node.consecutiveLosses >= cfg.maxConsecutiveLosses / 2
119
+ || recentAvgReward < cfg.minRewardThreshold * 1.5) {
120
+ return { nodeId: node.id, status: 'warning', recentAvgReward, idleMs, recommendation: 'merge' };
121
+ }
122
+ return { nodeId: node.id, status: 'healthy', recentAvgReward, idleMs, recommendation: 'keep' };
123
+ }
124
+ findMergeTarget(dying, cluster) {
125
+ let best;
126
+ let bestScore = -1;
127
+ for (const node of cluster.nodes.values()) {
128
+ if (node.id === dying.id)
129
+ continue;
130
+ let complementarity = 0;
131
+ for (const [domain, score] of dying.capabilities.scores) {
132
+ const ts = node.capabilities.scores.get(domain) ?? 0;
133
+ if (score > ts)
134
+ complementarity += score - ts;
135
+ }
136
+ const s = complementarity * (1 - node.load * 0.5);
137
+ if (s > bestScore) {
138
+ bestScore = s;
139
+ best = node;
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+ mergeCapabilities(dying, target) {
145
+ const dw = dying.capabilities.totalTasks;
146
+ const tw = target.capabilities.totalTasks;
147
+ const total = dw + tw || 1;
148
+ for (const [domain, ds] of dying.capabilities.scores) {
149
+ const ts = target.capabilities.scores.get(domain) ?? 0;
150
+ target.capabilities.scores.set(domain, (ds * dw + ts * tw) / total);
151
+ }
152
+ for (const tool of dying.capabilities.tools) {
153
+ if (!target.capabilities.tools.includes(tool))
154
+ target.capabilities.tools.push(tool);
155
+ }
156
+ }
157
+ apoptosis(dying, cluster) {
158
+ if (cluster.nodes.size <= this.config.apoptosis.minNodes) {
159
+ throw new ApoptosisRejectedError(dying.id, 'cluster at minimum node count');
160
+ }
161
+ if (dying.status === 'busy') {
162
+ throw new ApoptosisRejectedError(dying.id, 'node is busy');
163
+ }
164
+ dying.status = 'dying';
165
+ const target = this.findMergeTarget(dying, cluster);
166
+ if (target)
167
+ this.mergeCapabilities(dying, target);
168
+ cluster.removeNode(dying.id);
169
+ return true;
170
+ }
171
+ }
@@ -0,0 +1,10 @@
1
+ import type { ContextProvider, ContextFragment, TaskSpec, SubTask, IScratchpad } from '@loom-node/core';
2
+ export declare class MitosisContextProvider implements ContextProvider {
3
+ private parentSpec;
4
+ private subtask;
5
+ private scratchpad;
6
+ readonly source: "mitosis";
7
+ constructor(parentSpec: Pick<TaskSpec, 'objective' | 'outputFormat' | 'boundaries' | 'toolGuidance'>, subtask: SubTask, scratchpad: IScratchpad);
8
+ provide(_query: string, budget: number): Promise<ContextFragment[]>;
9
+ private buildGuidance;
10
+ }
@@ -0,0 +1,61 @@
1
+ function estimateTokens(text) {
2
+ return Math.ceil(text.length / 4);
3
+ }
4
+ export class MitosisContextProvider {
5
+ parentSpec;
6
+ subtask;
7
+ scratchpad;
8
+ source = 'mitosis';
9
+ constructor(parentSpec, subtask, scratchpad) {
10
+ this.parentSpec = parentSpec;
11
+ this.subtask = subtask;
12
+ this.scratchpad = scratchpad;
13
+ }
14
+ async provide(_query, budget) {
15
+ const fragments = [];
16
+ let used = 0;
17
+ // Fragment 1: Parent guidance (high relevance)
18
+ const guidance = this.buildGuidance();
19
+ if (guidance) {
20
+ const tokens = estimateTokens(guidance);
21
+ if (used + tokens <= budget) {
22
+ fragments.push({ source: 'mitosis', content: guidance, tokens, relevance: 0.95, metadata: { type: 'parent-guidance' } });
23
+ used += tokens;
24
+ }
25
+ }
26
+ // Fragment 2: Dependency results (just-in-time from scratchpad)
27
+ if (this.subtask.dependencies.length > 0) {
28
+ const deps = this.scratchpad.readDependencies(this.subtask.dependencies);
29
+ const entries = Object.entries(deps);
30
+ if (entries.length > 0) {
31
+ const depText = entries.map(([id, summary]) => `[${id}]: ${summary}`).join('\n');
32
+ const tokens = estimateTokens(depText);
33
+ if (used + tokens <= budget) {
34
+ fragments.push({ source: 'mitosis', content: depText, tokens, relevance: 0.9, metadata: { type: 'dependency-results' } });
35
+ used += tokens;
36
+ }
37
+ }
38
+ }
39
+ // Fragment 3: Sibling awareness (lower relevance, progressive disclosure)
40
+ const all = this.scratchpad.readAll();
41
+ const siblingIds = Object.keys(all).filter(id => id !== this.subtask.id && !this.subtask.dependencies.includes(id));
42
+ if (siblingIds.length > 0) {
43
+ const sibText = `Completed siblings: ${siblingIds.join(', ')}`;
44
+ const tokens = estimateTokens(sibText);
45
+ if (used + tokens <= budget) {
46
+ fragments.push({ source: 'mitosis', content: sibText, tokens, relevance: 0.4, metadata: { type: 'sibling-awareness' } });
47
+ }
48
+ }
49
+ return fragments;
50
+ }
51
+ buildGuidance() {
52
+ const parts = [];
53
+ if (this.parentSpec.outputFormat)
54
+ parts.push(`Output format: ${this.parentSpec.outputFormat}`);
55
+ if (this.parentSpec.boundaries)
56
+ parts.push(`Constraints: ${this.parentSpec.boundaries}`);
57
+ if (this.parentSpec.toolGuidance)
58
+ parts.push(`Tool guidance: ${this.parentSpec.toolGuidance}`);
59
+ return parts.length > 0 ? parts.join('\n') : undefined;
60
+ }
61
+ }
@@ -0,0 +1,8 @@
1
+ import type { TaskResult, IScratchpad } from '@loom-node/core';
2
+ export declare class MitosisScratchpad implements IScratchpad {
3
+ private entries;
4
+ write(taskId: string, result: TaskResult): void;
5
+ readDependencies(depIds: string[]): Record<string, string>;
6
+ readAll(): Record<string, string>;
7
+ private condense;
8
+ }
@@ -0,0 +1,23 @@
1
+ export class MitosisScratchpad {
2
+ entries = new Map();
3
+ write(taskId, result) {
4
+ this.entries.set(taskId, result.metadata?.summary ?? this.condense(result.content));
5
+ }
6
+ readDependencies(depIds) {
7
+ const out = {};
8
+ for (const id of depIds) {
9
+ const entry = this.entries.get(id);
10
+ if (entry)
11
+ out[id] = entry;
12
+ }
13
+ return out;
14
+ }
15
+ readAll() {
16
+ return Object.fromEntries(this.entries);
17
+ }
18
+ condense(content) {
19
+ if (content.length <= 800)
20
+ return content;
21
+ return content.slice(0, 780) + '... [truncated]';
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import type { AgentEvent } from '@loom-node/core';
2
+ import type { LoopStrategy, LoopContext } from '@loom-node/core';
3
+ import type { AgentNode } from '@loom-node/core';
4
+ import type { ClusterManager } from './cluster.js';
5
+ import type { RewardBus } from './reward-bus.js';
6
+ import type { AgentFactory, LifecycleManager } from './lifecycle.js';
7
+ import type { TaskPlanner } from './planner.js';
8
+ import type { AmoebaRuntime } from './runtime.js';
9
+ export type SkillDiscoverer = (input: string) => Promise<AgentNode | undefined>;
10
+ export declare class OrchestrateStrategy implements LoopStrategy {
11
+ private cluster;
12
+ private rewardBus;
13
+ private lifecycle;
14
+ private planner;
15
+ private agentFactory?;
16
+ private discoverSkill?;
17
+ private runtime?;
18
+ readonly name = "orchestrate";
19
+ constructor(cluster: ClusterManager, rewardBus: RewardBus, lifecycle: LifecycleManager, planner: TaskPlanner, agentFactory?: AgentFactory | undefined, discoverSkill?: SkillDiscoverer | undefined, runtime?: AmoebaRuntime | undefined);
20
+ execute(ctx: LoopContext): AsyncGenerator<AgentEvent>;
21
+ }
@@ -0,0 +1,78 @@
1
+ import { AuctionNoWinnerError, MitosisError } from './errors.js';
2
+ export class OrchestrateStrategy {
3
+ cluster;
4
+ rewardBus;
5
+ lifecycle;
6
+ planner;
7
+ agentFactory;
8
+ discoverSkill;
9
+ runtime;
10
+ name = 'orchestrate';
11
+ constructor(cluster, rewardBus, lifecycle, planner, agentFactory, discoverSkill, runtime) {
12
+ this.cluster = cluster;
13
+ this.rewardBus = rewardBus;
14
+ this.lifecycle = lifecycle;
15
+ this.planner = planner;
16
+ this.agentFactory = agentFactory;
17
+ this.discoverSkill = discoverSkill;
18
+ this.runtime = runtime;
19
+ }
20
+ async *execute(ctx) {
21
+ const startTime = Date.now();
22
+ const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
23
+ const userMsg = [...ctx.messages].reverse().find((m) => m.role === 'user');
24
+ const raw = userMsg?.content ?? '';
25
+ const input = typeof raw === 'string' ? raw : '';
26
+ const task = {
27
+ taskId: `orch-${Date.now()}`, domain: 'general', description: input,
28
+ estimatedComplexity: 0.5, priority: 'normal',
29
+ };
30
+ yield { type: 'step-start', step: 0, totalSteps: 2 };
31
+ // Auction: select best agent, or discover a skill-node
32
+ let winner = this.cluster.selectWinner(task);
33
+ if (!winner && this.discoverSkill) {
34
+ const discovered = await this.discoverSkill(input);
35
+ if (discovered) {
36
+ winner = discovered;
37
+ yield { type: 'text-delta', content: `[Orchestrate] Skill discovered: ${discovered.id}\n` };
38
+ }
39
+ }
40
+ if (!winner) {
41
+ yield { type: 'error', error: new AuctionNoWinnerError(task.taskId), recoverable: false };
42
+ yield { type: 'done', content: '', usage: totalUsage, steps: 1, durationMs: Date.now() - startTime };
43
+ return;
44
+ }
45
+ yield { type: 'text-delta', content: `[Orchestrate] Winner: ${winner.id}\n` };
46
+ // Check mitosis
47
+ if (this.lifecycle.shouldMitosis(winner, task)) {
48
+ yield { type: 'text-delta', content: '[Orchestrate] Mitosis triggered\n' };
49
+ try {
50
+ const result = await this.lifecycle.mitosis(winner, task, this.planner, this.cluster, this.rewardBus, this.agentFactory, { task, objective: input, domainHints: [task.domain] }, this.runtime);
51
+ totalUsage.totalTokens += result.tokenCost;
52
+ yield { type: 'text-delta', content: result.content };
53
+ yield { type: 'step-end', step: 0, reason: 'complete' };
54
+ yield { type: 'done', content: result.content, usage: totalUsage, steps: 1, durationMs: Date.now() - startTime };
55
+ }
56
+ catch (err) {
57
+ const me = new MitosisError(winner.id, 'Mitosis failed', err instanceof Error ? err : undefined);
58
+ yield { type: 'error', error: me, recoverable: true };
59
+ yield { type: 'done', content: '', usage: totalUsage, steps: 1, durationMs: Date.now() - startTime };
60
+ }
61
+ return;
62
+ }
63
+ // Direct execution
64
+ winner.status = 'busy';
65
+ const start = Date.now();
66
+ const done = await winner.agent.run(input);
67
+ const result = {
68
+ taskId: task.taskId, agentId: winner.id, content: done.content,
69
+ success: true, tokenCost: done.usage.totalTokens, errorCount: 0, durationMs: Date.now() - start,
70
+ };
71
+ await this.rewardBus.evaluate(winner, task, result);
72
+ winner.status = 'idle';
73
+ winner.lastActiveAt = Date.now();
74
+ yield { type: 'step-end', step: 0, reason: 'complete' };
75
+ yield { type: 'text-delta', content: done.content };
76
+ yield { type: 'done', content: done.content, usage: done.usage, steps: 1, durationMs: Date.now() - startTime };
77
+ }
78
+ }
@@ -0,0 +1,9 @@
1
+ import type { AgentEvent } from '@loom-node/core';
2
+ import type { LoopStrategy, LoopContext } from '@loom-node/core';
3
+ import type { TaskPlanner } from './planner.js';
4
+ export declare class PlanStrategy implements LoopStrategy {
5
+ private planner;
6
+ readonly name = "plan";
7
+ constructor(planner: TaskPlanner);
8
+ execute(ctx: LoopContext): AsyncGenerator<AgentEvent>;
9
+ }
@@ -0,0 +1,39 @@
1
+ export class PlanStrategy {
2
+ planner;
3
+ name = 'plan';
4
+ constructor(planner) {
5
+ this.planner = planner;
6
+ }
7
+ async *execute(ctx) {
8
+ const startTime = Date.now();
9
+ const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
10
+ const userMsg = [...ctx.messages].reverse().find((m) => m.role === 'user');
11
+ const raw = userMsg?.content ?? '';
12
+ const input = typeof raw === 'string' ? raw : '';
13
+ yield { type: 'step-start', step: 0, totalSteps: 3 };
14
+ // Step 1: Decompose
15
+ const task = { taskId: `plan-${Date.now()}`, domain: 'general', description: input, estimatedComplexity: 0.7, priority: 'normal' };
16
+ const subtasks = await this.planner.decompose(task);
17
+ yield { type: 'text-delta', content: `[Plan] Decomposed into ${subtasks.length} subtasks\n` };
18
+ yield { type: 'step-end', step: 0, reason: 'complete' };
19
+ // Step 2: Execute DAG
20
+ yield { type: 'step-start', step: 1, totalSteps: 3 };
21
+ const results = await this.planner.executeDAG(subtasks, async (sub) => {
22
+ const res = await ctx.provider.complete({ messages: [...ctx.messages, { role: 'user', content: sub.description }] });
23
+ accUsage(totalUsage, res.usage);
24
+ return { taskId: sub.id, agentId: 'self', content: res.content, success: true, tokenCost: res.usage.totalTokens, errorCount: 0, durationMs: 0 };
25
+ });
26
+ yield { type: 'step-end', step: 1, reason: 'complete' };
27
+ // Step 3: Aggregate
28
+ yield { type: 'step-start', step: 2, totalSteps: 3 };
29
+ const content = await this.planner.aggregate(task, results);
30
+ yield { type: 'text-delta', content };
31
+ yield { type: 'step-end', step: 2, reason: 'complete' };
32
+ yield { type: 'done', content, usage: totalUsage, steps: 3, durationMs: Date.now() - startTime };
33
+ }
34
+ }
35
+ function accUsage(t, d) {
36
+ t.promptTokens += d.promptTokens;
37
+ t.completionTokens += d.completionTokens;
38
+ t.totalTokens += d.totalTokens;
39
+ }
@@ -0,0 +1,14 @@
1
+ import type { LLMProvider, SubTask, TaskAd, TaskResult } from '@loom-node/core';
2
+ export interface PlannerConfig {
3
+ llm: LLMProvider;
4
+ maxSubTasks: number;
5
+ aggregation: 'llm' | 'concat';
6
+ }
7
+ export declare class TaskPlanner {
8
+ private readonly config;
9
+ constructor(config: Pick<PlannerConfig, 'llm'> & Partial<PlannerConfig>);
10
+ decompose(task: TaskAd): Promise<SubTask[]>;
11
+ executeDAG(subtasks: SubTask[], executor: (sub: SubTask) => Promise<TaskResult>): Promise<TaskResult[]>;
12
+ aggregate(task: TaskAd, results: TaskResult[]): Promise<string>;
13
+ private parseSubTasks;
14
+ }
@@ -0,0 +1,64 @@
1
+ const DEFAULT_CONFIG = {
2
+ maxSubTasks: 6,
3
+ aggregation: 'llm',
4
+ };
5
+ export class TaskPlanner {
6
+ config;
7
+ constructor(config) {
8
+ this.config = { ...DEFAULT_CONFIG, ...config };
9
+ }
10
+ async decompose(task) {
11
+ const result = await this.config.llm.complete({
12
+ messages: [
13
+ { role: 'system', content: DECOMPOSE_PROMPT },
14
+ { role: 'user', content: `Task: ${task.description}\nDomain: ${task.domain}\nComplexity: ${task.estimatedComplexity}` },
15
+ ],
16
+ temperature: 0.2,
17
+ });
18
+ return this.parseSubTasks(result.content, task.taskId);
19
+ }
20
+ async executeDAG(subtasks, executor) {
21
+ const done = new Map();
22
+ const pending = new Set(subtasks.map(s => s.id));
23
+ while (pending.size > 0) {
24
+ const ready = subtasks.filter(s => pending.has(s.id) && s.dependencies.every(d => done.has(d)));
25
+ if (ready.length === 0)
26
+ throw new Error('Cycle detected in subtask DAG');
27
+ const results = await Promise.all(ready.map(s => executor(s)));
28
+ for (let i = 0; i < ready.length; i++) {
29
+ done.set(ready[i].id, results[i]);
30
+ pending.delete(ready[i].id);
31
+ }
32
+ }
33
+ return subtasks.map(s => done.get(s.id));
34
+ }
35
+ async aggregate(task, results) {
36
+ if (this.config.aggregation === 'concat') {
37
+ return results.map(r => r.content).join('\n\n');
38
+ }
39
+ const summary = results.map((r, i) => `[SubTask ${i + 1}] ${r.success ? '✓' : '✗'}\n${r.content}`).join('\n---\n');
40
+ const res = await this.config.llm.complete({
41
+ messages: [
42
+ { role: 'system', content: 'Synthesize the sub-task results into a coherent final answer.' },
43
+ { role: 'user', content: `Original task: ${task.description}\n\nResults:\n${summary}` },
44
+ ],
45
+ });
46
+ return res.content;
47
+ }
48
+ parseSubTasks(raw, parentTaskId) {
49
+ const match = raw.match(/\[[\s\S]*\]/);
50
+ if (!match)
51
+ return [];
52
+ const arr = JSON.parse(match[0]);
53
+ return arr.slice(0, this.config.maxSubTasks).map((item, i) => ({
54
+ id: item.id ?? `${parentTaskId}-sub-${i}`,
55
+ parentTaskId,
56
+ description: item.description,
57
+ domain: item.domain ?? 'general',
58
+ dependencies: item.dependencies ?? [],
59
+ }));
60
+ }
61
+ }
62
+ const DECOMPOSE_PROMPT = `You are a task planner. Decompose the given task into independent sub-tasks.
63
+ Output a JSON array where each element has: id, description, domain, dependencies (array of other sub-task ids).
64
+ Minimize dependencies to maximize parallelism. Output ONLY the JSON array.`;
@@ -0,0 +1,28 @@
1
+ import type { AgentNode, TaskAd, TaskResult, RewardSignal } from '@loom-node/core';
2
+ export interface RewardConfig {
3
+ strategy: 'rule' | 'hybrid';
4
+ llmJudgeInterval: number;
5
+ weights: {
6
+ quality: number;
7
+ efficiency: number;
8
+ reliability: number;
9
+ };
10
+ ema: {
11
+ alpha: number;
12
+ decayRate: number;
13
+ };
14
+ }
15
+ export declare class RewardBus {
16
+ private readonly config;
17
+ private evalCount;
18
+ private bias;
19
+ private llmJudge?;
20
+ constructor(config?: Partial<RewardConfig>);
21
+ setLLMJudge(judge: (task: TaskAd, result: TaskResult) => Promise<RewardSignal>): void;
22
+ evaluate(node: AgentNode, task: TaskAd, result: TaskResult): Promise<number>;
23
+ decayInactive(node: AgentNode): void;
24
+ private computeSignal;
25
+ private ruleSignal;
26
+ private computeReward;
27
+ private updateCapability;
28
+ }