@ifi/oh-pi-ant-colony 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ifiok Jr.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # 🐜 Ant Colony — Multi-Agent Swarm Extension
2
+
3
+ > A self-organizing multi-agent system modeled after real ant colony ecology. Adaptive concurrency,
4
+ > pheromone communication, zero centralized scheduling.
5
+
6
+ ## Architecture
7
+
8
+ ```
9
+ Queen Main pi process, receives goals, orchestrates lifecycle
10
+
11
+ ├─ 🔍 Scout Lightweight haiku, explores paths, marks food sources
12
+ ├─ ⚒️ Worker Sonnet, executes tasks, may spawn sub-tasks
13
+ └─ 🛡️ Soldier Sonnet, reviews quality, may request rework
14
+
15
+ Pheromone .ant-colony/ file system, indirect ant-to-ant communication
16
+ Nest Shared state, atomic file operations, cross-process safe
17
+ ```
18
+
19
+ ## Lifecycle
20
+
21
+ ```
22
+ Goal → Scouting → Task Pool → Workers Execute in Parallel → Soldiers Review → Fix (if needed) → Done
23
+ │ │
24
+ │ Pheromone decay (10min) │ Sub-tasks auto-spawned
25
+ └───────────────────────────┘
26
+ ```
27
+
28
+ ## Adaptive Concurrency
29
+
30
+ Models real ant colony dynamic recruitment:
31
+
32
+ - **Cold start**: 1–2 ants, gradual exploration
33
+ - **Exploration phase**: +1 each wave, monitoring throughput inflection point
34
+ - **Steady state**: fine-tune around optimal value
35
+ - **Overload protection**: CPU > 85% or memory < 500MB → auto-reduce
36
+ - **Elastic scaling**: more tasks → recruit; fewer tasks → shrink
37
+
38
+ ## Usage
39
+
40
+ ### Auto-Trigger
41
+
42
+ The LLM automatically invokes the `ant_colony` tool when task complexity warrants it.
43
+
44
+ ### Commands
45
+
46
+ ```
47
+ /colony-stop Cancel a running colony
48
+ Ctrl+Shift+A Open colony details panel
49
+ ```
50
+
51
+ ### Examples
52
+
53
+ ```
54
+ /colony Migrate the entire project from CommonJS to ESM, updating all imports/exports and tsconfig
55
+
56
+ /colony Add unit tests for all modules under src/, targeting 80% coverage
57
+
58
+ /colony Refactor auth system from session-based to JWT, maintaining API compatibility
59
+ ```
60
+
61
+ ## Pheromone System
62
+
63
+ Ants communicate indirectly through pheromones (stigmergy), not direct messages:
64
+
65
+ | Type | Released By | Meaning |
66
+ | ---------- | ----------- | --------------------------------------- |
67
+ | discovery | Scout | Discovered code structure, dependencies |
68
+ | progress | Worker | Completed changes, file modifications |
69
+ | warning | Soldier | Quality issues, conflict risks |
70
+ | completion | Worker | Task completion marker |
71
+ | dependency | Any | File dependency relationships |
72
+
73
+ Pheromones decay exponentially (10-minute half-life), preventing stale info from misleading
74
+ subsequent ants.
75
+
76
+ ## File Locking
77
+
78
+ Each task declares the files it operates on. The queen guarantees:
79
+
80
+ - Only one ant modifies a given file at any time
81
+ - Conflicting tasks are automatically marked `blocked` and resume when locks release
82
+
83
+ ## Nest Structure
84
+
85
+ ```
86
+ .ant-colony/{colony-id}/
87
+ ├── state.json Colony state
88
+ ├── pheromone.jsonl Append-only pheromone log
89
+ └── tasks/ One file per task (atomic updates)
90
+ ├── t-xxx.json
91
+ └── t-yyy.json
92
+ ```
93
+
94
+ ## Installation
95
+
96
+ ```bash
97
+ # Option 1: Symlink to pi extensions directory
98
+ mkdir -p ~/.pi/agent/extensions/ant-colony
99
+ ln -sf "$(pwd)/pi-package/extensions/ant-colony/index.ts" ~/.pi/agent/extensions/ant-colony/index.ts
100
+ # ... (symlink all .ts files)
101
+
102
+ # Option 2: Install via oh-pi
103
+ npx oh-pi # Select "Full Power" preset
104
+ ```
105
+
106
+ ## Module Reference
107
+
108
+ | File | Lines | Responsibility |
109
+ | ---------------- | ----- | -------------------------------------------------------------------------- |
110
+ | `types.ts` | ~150 | Type system: ants, tasks, pheromones, colony state |
111
+ | `nest.ts` | ~500 | Nest: file-system shared state, atomic R/W, pheromone decay |
112
+ | `concurrency.ts` | ~120 | Adaptive concurrency: system sampling, exploration/steady-state adjustment |
113
+ | `spawner.ts` | ~370 | Ant spawning: session management, prompt construction, output parsing |
114
+ | `queen.ts` | ~1000 | Queen scheduling: lifecycle, task waves, multi-round iteration |
115
+ | `index.ts` | ~900 | Extension entry: tool/shortcut registration, TUI rendering |
116
+ | `deps.ts` | ~140 | Lightweight import graph for dependency-aware scheduling |
117
+ | `parser.ts` | ~180 | Sub-task and pheromone extraction from ant output |
118
+ | `prompts.ts` | ~90 | Per-caste system prompts and prompt builder |
119
+ | `ui.ts` | ~140 | Formatting helpers for status bar, overlay, and reports |
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Adaptive concurrency control — modeled after ant colony dynamic recruitment.
3
+ *
4
+ * Real ant colonies: more food → more recruitment pheromones → more foragers leave the nest.
5
+ * Mapping: more tasks + idle system → increase concurrency; overloaded/few tasks → decrease.
6
+ *
7
+ * Explores boundaries: gradually increases concurrency at startup, monitoring throughput inflection points.
8
+ */
9
+
10
+ import * as os from "node:os";
11
+ import type { ConcurrencyConfig, ConcurrencySample } from "./types.js";
12
+
13
+ const CPU_CORES = os.cpus().length;
14
+
15
+ export function defaultConcurrency(): ConcurrencyConfig {
16
+ return {
17
+ current: 2,
18
+ min: 1,
19
+ max: Math.min(CPU_CORES, 8),
20
+ optimal: 3,
21
+ history: [],
22
+ };
23
+ }
24
+
25
+ /** Sample current system load (CPU, memory, throughput). */
26
+ export function sampleSystem(activeTasks: number, completedRecently: number, windowMinutes: number): ConcurrencySample {
27
+ const cpus = os.cpus();
28
+ const cpuLoad =
29
+ cpus.reduce((sum, c) => {
30
+ const total = Object.values(c.times).reduce((a, b) => a + b, 0);
31
+ return sum + 1 - c.times.idle / total;
32
+ }, 0) / cpus.length;
33
+
34
+ const mem = os.freemem();
35
+ const throughput = windowMinutes > 0 ? completedRecently / windowMinutes : 0;
36
+
37
+ return {
38
+ timestamp: Date.now(),
39
+ concurrency: activeTasks,
40
+ cpuLoad,
41
+ memFree: mem,
42
+ throughput,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Adaptive adjustment algorithm.
48
+ *
49
+ * Phase 1 — Exploration (samples < 10): increment by 1 each wave, finding throughput inflection.
50
+ * Phase 2 — Steady state: fine-tune around the optimal value.
51
+ *
52
+ * Constraints:
53
+ * - CPU load > 85% → reduce
54
+ * - Free memory < 500MB → reduce
55
+ * - Throughput declining → revert to previous optimal
56
+ * - No pending tasks → drop to min
57
+ */
58
+ export function adapt(config: ConcurrencyConfig, pendingTasks: number): ConcurrencyConfig {
59
+ const next = { ...config };
60
+ const samples = config.history;
61
+
62
+ // No pending tasks — drop to minimum
63
+ if (pendingTasks === 0) {
64
+ next.current = config.min;
65
+ return next;
66
+ }
67
+
68
+ // Cap at pending task count
69
+ const taskCap = Math.min(pendingTasks, config.max);
70
+
71
+ if (samples.length < 2) {
72
+ // Cold start: use half of max for fast ramp-up
73
+ next.current = Math.min(Math.ceil(config.max / 2), taskCap);
74
+ return next;
75
+ }
76
+
77
+ const latest = samples[samples.length - 1];
78
+ const prev = samples[samples.length - 2];
79
+
80
+ // CPU sliding window: average of last 3 samples
81
+ const recentCpuSamples = samples.slice(-3);
82
+ const avgCpu = recentCpuSamples.reduce((s, x) => s + x.cpuLoad, 0) / recentCpuSamples.length;
83
+
84
+ // 429 cooldown: no concurrency increases within 30s of rate limit
85
+ const inRateLimitCooldown = config.lastRateLimitAt != null && Date.now() - config.lastRateLimitAt < 30000;
86
+
87
+ // Hard constraint: reduce immediately on overload (hysteresis: >85% reduce, 60-85% hold)
88
+ if (avgCpu > 0.85 || latest.memFree < 500 * 1024 * 1024) {
89
+ next.current = Math.max(config.min, config.current - 1);
90
+ return next;
91
+ }
92
+
93
+ // Hysteresis band: CPU between 60-85% holds steady
94
+ const canIncrease = avgCpu < 0.6 && !inRateLimitCooldown;
95
+
96
+ // Exploration phase: insufficient samples, ramp up gradually
97
+ if (samples.length < 10) {
98
+ if (latest.throughput >= prev.throughput && canIncrease) {
99
+ next.current = Math.min(config.current + 1, taskCap);
100
+ } else if (latest.throughput < prev.throughput) {
101
+ // Throughput declining — inflection point found
102
+ next.optimal = prev.concurrency;
103
+ next.current = prev.concurrency;
104
+ }
105
+ return next;
106
+ }
107
+
108
+ // Steady state: fine-tune around optimal
109
+ const recentThroughput = samples.slice(-5).reduce((s, x) => s + x.throughput, 0) / 5;
110
+ const olderThroughput = samples.slice(-10, -5).reduce((s, x) => s + x.throughput, 0) / 5;
111
+
112
+ if (recentThroughput > olderThroughput * 1.1 && canIncrease) {
113
+ next.current = Math.min(config.current + 1, taskCap);
114
+ if (recentThroughput > olderThroughput * 1.2) {
115
+ next.optimal = next.current;
116
+ }
117
+ } else if (recentThroughput < olderThroughput * 0.8) {
118
+ next.current = Math.max(config.min, config.optimal);
119
+ }
120
+
121
+ // 429 recovery: restore to optimal when CPU is underutilized (e.g. after backoff)
122
+ if (avgCpu < 0.5 && next.current < config.optimal && !inRateLimitCooldown) {
123
+ next.current = config.optimal;
124
+ }
125
+
126
+ return next;
127
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Lightweight import graph builder — static analysis of ts/js file dependencies.
3
+ *
4
+ * Parses `import`/`export`/`require` statements to build a bidirectional
5
+ * dependency graph. Used by the queen scheduler to detect file-level
6
+ * dependencies between tasks, preventing workers from editing files that
7
+ * depend on each other simultaneously.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+
12
+ /**
13
+ * Bidirectional import graph mapping files to their imports and reverse-imports.
14
+ *
15
+ * - `imports`: file → set of files it directly imports
16
+ * - `importedBy`: file → set of files that import it (reverse edges)
17
+ */
18
+ export interface ImportGraph {
19
+ /** Forward edges: file → files it imports. */
20
+ imports: Map<string, Set<string>>;
21
+ /** Reverse edges: file → files that import it. */
22
+ importedBy: Map<string, Set<string>>;
23
+ }
24
+
25
+ /** Matches ESM `import ... from './path'` and `export ... from './path'` statements. */
26
+ const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
27
+
28
+ /** Matches CommonJS `require('./path')` calls. */
29
+ const REQUIRE_RE = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
30
+
31
+ /**
32
+ * Resolve a relative import specifier to an actual file path.
33
+ * Tries common extensions (.ts, .tsx, .js, .jsx) and index files.
34
+ *
35
+ * @param from - The file that contains the import statement (relative to cwd)
36
+ * @param specifier - The relative import specifier (e.g. `./foo`)
37
+ * @param cwd - Project root directory
38
+ * @returns Resolved path relative to cwd, or null if not found
39
+ */
40
+ function resolveImport(from: string, specifier: string, cwd: string): string | null {
41
+ const dir = path.dirname(path.resolve(cwd, from));
42
+ const base = path.resolve(dir, specifier);
43
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
44
+ for (const ext of exts) {
45
+ const full = base + ext;
46
+ if (fs.existsSync(full)) {
47
+ return path.relative(cwd, full);
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Build a bidirectional import graph from a list of source files.
55
+ * Scans each file for import/require statements and resolves them to
56
+ * relative file paths within the project.
57
+ *
58
+ * @param files - List of file paths relative to `cwd`
59
+ * @param cwd - Project root directory
60
+ * @returns The constructed import graph
61
+ */
62
+ export function buildImportGraph(files: string[], cwd: string): ImportGraph {
63
+ const imports = new Map<string, Set<string>>();
64
+ const importedBy = new Map<string, Set<string>>();
65
+
66
+ for (const file of files) {
67
+ const abs = path.resolve(cwd, file);
68
+ if (!fs.existsSync(abs)) {
69
+ continue;
70
+ }
71
+ let content: string;
72
+ try {
73
+ content = fs.readFileSync(abs, "utf-8");
74
+ } catch {
75
+ continue;
76
+ }
77
+
78
+ const deps = new Set<string>();
79
+ for (const re of [IMPORT_RE, REQUIRE_RE]) {
80
+ re.lastIndex = 0;
81
+ for (const m of content.matchAll(re)) {
82
+ const resolved = resolveImport(file, m[1], cwd);
83
+ if (resolved) {
84
+ deps.add(resolved);
85
+ }
86
+ }
87
+ }
88
+
89
+ imports.set(file, deps);
90
+ for (const dep of deps) {
91
+ if (!importedBy.has(dep)) {
92
+ importedBy.set(dep, new Set());
93
+ }
94
+ const dependents = importedBy.get(dep);
95
+ if (dependents) {
96
+ dependents.add(file);
97
+ }
98
+ }
99
+ }
100
+
101
+ return { imports, importedBy };
102
+ }
103
+
104
+ /**
105
+ * Calculate the dependency depth of a file — how many files directly or
106
+ * indirectly depend on it. Higher depth = more foundational = should be
107
+ * processed first to avoid cascading breakage.
108
+ *
109
+ * Uses BFS traversal through the `importedBy` reverse edges.
110
+ *
111
+ * @param file - File path relative to cwd
112
+ * @param graph - The import graph to traverse
113
+ * @returns Number of transitive dependents (excluding the file itself)
114
+ */
115
+ export function dependencyDepth(file: string, graph: ImportGraph): number {
116
+ const visited = new Set<string>();
117
+ const queue = [file];
118
+ while (queue.length > 0) {
119
+ const current = queue.pop();
120
+ if (!current || visited.has(current)) {
121
+ continue;
122
+ }
123
+ visited.add(current);
124
+ const dependents = graph.importedBy.get(current);
125
+ if (dependents) {
126
+ for (const d of dependents) {
127
+ queue.push(d);
128
+ }
129
+ }
130
+ }
131
+ return visited.size - 1;
132
+ }
133
+
134
+ /**
135
+ * Check whether any file in taskA's scope imports any file in taskB's scope.
136
+ * Used by the queen to detect inter-task dependencies and block conflicting
137
+ * concurrent execution.
138
+ *
139
+ * @param taskAFiles - File paths assigned to task A
140
+ * @param taskBFiles - File paths assigned to task B
141
+ * @param graph - The import graph
142
+ * @returns `true` if task A depends on task B's files
143
+ */
144
+ export function taskDependsOn(taskAFiles: string[], taskBFiles: string[], graph: ImportGraph): boolean {
145
+ for (const a of taskAFiles) {
146
+ const deps = graph.imports.get(a);
147
+ if (deps) {
148
+ for (const b of taskBFiles) {
149
+ if (deps.has(b)) {
150
+ return true;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ return false;
156
+ }