oh-pi 0.1.48 → 0.1.50
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/README.fr.md +21 -12
- package/README.md +19 -10
- package/README.zh.md +19 -10
- package/dist/i18n.js +16 -34
- package/dist/tui/preset-select.js +13 -40
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/pi-package/extensions/ant-colony/concurrency.ts +4 -4
- package/pi-package/extensions/ant-colony/index.ts +139 -79
- package/pi-package/extensions/ant-colony/nest.ts +25 -15
- package/pi-package/extensions/ant-colony/queen.ts +32 -44
- package/pi-package/extensions/bg-process.ts +209 -0
package/README.fr.md
CHANGED
|
@@ -45,13 +45,14 @@ Vous avez déjà une config ? oh-pi la détecte et propose une **sauvegarde avan
|
|
|
45
45
|
├── settings.json Modèle, thème, niveau de réflexion
|
|
46
46
|
├── keybindings.json Raccourcis Vim/Emacs (optionnel)
|
|
47
47
|
├── AGENTS.md Directives IA par rôle
|
|
48
|
-
├── extensions/
|
|
48
|
+
├── extensions/ 8 extensions (7 par défaut + colonie)
|
|
49
49
|
│ ├── safe-guard Confirmation des commandes dangereuses + protection des chemins
|
|
50
50
|
│ ├── git-guard Points de contrôle stash auto + alerte dépôt sale
|
|
51
51
|
│ ├── auto-session Nommage de session depuis le premier message
|
|
52
52
|
│ ├── custom-footer Barre d'état améliorée (token/coût/temps/git/cwd)
|
|
53
53
|
│ ├── compact-header Informations de démarrage simplifiées
|
|
54
54
|
│ ├── auto-update Vérification des mises à jour au lancement
|
|
55
|
+
│ ├── bg-process ⏳ **Bg Process** — Mise en arrière-plan automatique des commandes longues (serveurs dev, etc.)
|
|
55
56
|
│ └── ant-colony/ 🐜 Essaim multi-agents autonome (optionnel)
|
|
56
57
|
├── prompts/ 10 modèles (/review /fix /commit /test ...)
|
|
57
58
|
├── skills/ 11 compétences (outils + design UI + workflows)
|
|
@@ -68,14 +69,11 @@ Vous avez déjà une config ? oh-pi la détecte et propose une **sauvegarde avan
|
|
|
68
69
|
|
|
69
70
|
### Préréglages
|
|
70
71
|
|
|
71
|
-
| |
|
|
72
|
-
|
|
73
|
-
| 🟢
|
|
74
|
-
| 🔵
|
|
75
|
-
| 🟣
|
|
76
|
-
| 🟠 Data & IA | Tokyo Night | medium | MLOps + pipelines |
|
|
77
|
-
| 🔴 Minimal | Default | off | Noyau uniquement |
|
|
78
|
-
| ⚫ Pleine puissance | oh-pi Dark | high | Tout + colonie de fourmis |
|
|
72
|
+
| | Inclut |
|
|
73
|
+
|---|--------|
|
|
74
|
+
| 🟢 **Complet** | Toutes extensions + colonie + bg-process |
|
|
75
|
+
| 🔵 **Propre** | Aucune extension |
|
|
76
|
+
| 🟣 **Colonie** | Colonie uniquement |
|
|
79
77
|
|
|
80
78
|
### Fournisseurs
|
|
81
79
|
|
|
@@ -133,12 +131,23 @@ Les vraies colonies de fourmis résolvent des problèmes complexes sans contrôl
|
|
|
133
131
|
|
|
134
132
|
En mode interactif, la colonie affiche la progression en direct :
|
|
135
133
|
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
134
|
+
- **Barre de statut** — footer compact avec métriques réelles : tâches terminées, fourmis actives, appels d'outils, tokens de sortie, coût, durée
|
|
135
|
+
- **Ctrl+Shift+A** — panneau de détails en overlay avec liste des tâches, flux des fourmis actives et journal de la colonie
|
|
138
136
|
- **Notification** — résumé à la fin
|
|
139
137
|
|
|
140
138
|
Utilisez `/colony-stop` pour arrêter une colonie en cours.
|
|
141
139
|
|
|
140
|
+
### Protocole de signaux
|
|
141
|
+
|
|
142
|
+
La colonie communique avec la conversation principale via des signaux structurés, empêchant le LLM de vérifier ou deviner l'état :
|
|
143
|
+
|
|
144
|
+
| Signal | Signification |
|
|
145
|
+
|--------|---------------|
|
|
146
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée — ne pas vérifier |
|
|
147
|
+
| `COLONY_SIGNAL:RUNNING` | Colonie active — injecté à chaque tour |
|
|
148
|
+
| `COLONY_SIGNAL:COMPLETE` | Colonie terminée — consulter le rapport |
|
|
149
|
+
| `COLONY_SIGNAL:FAILED` | Colonie crashée — signaler l'erreur |
|
|
150
|
+
|
|
142
151
|
### Contrôle des tours
|
|
143
152
|
|
|
144
153
|
Chaque fourmi a un budget strict de tours pour éviter les exécutions incontrôlées :
|
|
@@ -174,7 +183,7 @@ Le LLM décide quand déployer la colonie. Vous n'avez pas à y penser :
|
|
|
174
183
|
La colonie trouve automatiquement le parallélisme optimal pour votre machine :
|
|
175
184
|
|
|
176
185
|
```
|
|
177
|
-
Démarrage à froid →
|
|
186
|
+
Démarrage à froid → ceil(max/2) fourmis (démarrage rapide)
|
|
178
187
|
Exploration → +1 par vague, surveillance du débit
|
|
179
188
|
Débit ↓ → verrouiller l'optimal, stabiliser
|
|
180
189
|
CPU > 85% → réduire immédiatement
|
package/README.md
CHANGED
|
@@ -45,13 +45,14 @@ Already have a config? oh-pi detects it and offers **backup before overwriting**
|
|
|
45
45
|
├── settings.json Model, theme, thinking level
|
|
46
46
|
├── keybindings.json Vim/Emacs shortcuts (optional)
|
|
47
47
|
├── AGENTS.md Role-specific AI guidelines
|
|
48
|
-
├── extensions/
|
|
48
|
+
├── extensions/ 8 extensions (7 default + ant-colony)
|
|
49
49
|
│ ├── safe-guard Dangerous command confirmation + path protection
|
|
50
50
|
│ ├── git-guard Auto stash checkpoints + dirty repo warning
|
|
51
51
|
│ ├── auto-session Session naming from first message
|
|
52
52
|
│ ├── custom-footer Enhanced status bar (token/cost/time/git/cwd)
|
|
53
53
|
│ ├── compact-header Streamlined startup info
|
|
54
54
|
│ ├── auto-update Check for updates on launch
|
|
55
|
+
│ ├── bg-process ⏳ **Bg Process** — Auto-background long-running commands (dev servers, etc.)
|
|
55
56
|
│ └── ant-colony/ 🐜 Autonomous multi-agent swarm (optional)
|
|
56
57
|
├── prompts/ 10 templates (/review /fix /commit /test ...)
|
|
57
58
|
├── skills/ 11 skills (tools + UI design + workflows)
|
|
@@ -70,12 +71,9 @@ Already have a config? oh-pi detects it and offers **backup before overwriting**
|
|
|
70
71
|
|
|
71
72
|
| | Theme | Thinking | Includes |
|
|
72
73
|
|---|-------|----------|----------|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
| 🟠 Data & AI | Tokyo Night | medium | MLOps + pipelines |
|
|
77
|
-
| 🔴 Minimal | Default | off | Core only |
|
|
78
|
-
| ⚫ Full Power | oh-pi Dark | high | Everything + ant colony |
|
|
74
|
+
| ⚫ Full Power | oh-pi Dark | high | All extensions + bg-process + ant-colony |
|
|
75
|
+
| 🔴 Clean | Default | off | No extensions, just core |
|
|
76
|
+
| 🐜 Colony Only | oh-pi Dark | medium | Ant-colony with minimal setup |
|
|
79
77
|
|
|
80
78
|
### Providers
|
|
81
79
|
|
|
@@ -133,12 +131,23 @@ Real ant colonies solve complex problems without central control. Each ant follo
|
|
|
133
131
|
|
|
134
132
|
In interactive mode, the colony shows live progress:
|
|
135
133
|
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
134
|
+
- **Status bar** — compact footer with real metrics: tasks done, active ants, tool calls, output tokens, cost, elapsed time
|
|
135
|
+
- **Ctrl+Shift+A** — overlay detail panel with task list, active ant streams, and colony log
|
|
138
136
|
- **Notification** — completion summary when done
|
|
139
137
|
|
|
140
138
|
Use `/colony-stop` to abort a running colony.
|
|
141
139
|
|
|
140
|
+
### Signal Protocol
|
|
141
|
+
|
|
142
|
+
The colony communicates with the main conversation via structured signals, preventing the LLM from polling or guessing colony status:
|
|
143
|
+
|
|
144
|
+
| Signal | Meaning |
|
|
145
|
+
|--------|---------|
|
|
146
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colony started — don't poll |
|
|
147
|
+
| `COLONY_SIGNAL:RUNNING` | Colony active — injected each turn |
|
|
148
|
+
| `COLONY_SIGNAL:COMPLETE` | Colony finished — review report |
|
|
149
|
+
| `COLONY_SIGNAL:FAILED` | Colony crashed — report error |
|
|
150
|
+
|
|
142
151
|
### Turn Control
|
|
143
152
|
|
|
144
153
|
Each ant has a strict turn budget to prevent runaway execution:
|
|
@@ -174,7 +183,7 @@ The LLM decides when to deploy the colony. You don't have to think about it:
|
|
|
174
183
|
The colony automatically finds the optimal parallelism for your machine:
|
|
175
184
|
|
|
176
185
|
```
|
|
177
|
-
Cold start →
|
|
186
|
+
Cold start → ceil(max/2) ants (fast ramp-up)
|
|
178
187
|
Exploration → +1 each wave, monitoring throughput
|
|
179
188
|
Throughput ↓ → lock optimal, stabilize
|
|
180
189
|
CPU > 85% → reduce immediately
|
package/README.zh.md
CHANGED
|
@@ -45,13 +45,14 @@ pi # 开始编码
|
|
|
45
45
|
├── settings.json 模型、主题、思维级别
|
|
46
46
|
├── keybindings.json Vim/Emacs 快捷键(可选)
|
|
47
47
|
├── AGENTS.md 角色专属 AI 指南
|
|
48
|
-
├── extensions/
|
|
48
|
+
├── extensions/ 8 个扩展(7 个默认 + 蚁群)
|
|
49
49
|
│ ├── safe-guard 危险命令确认 + 路径保护
|
|
50
50
|
│ ├── git-guard 自动 stash 检查点 + 脏仓库警告
|
|
51
51
|
│ ├── auto-session 从首条消息自动命名会话
|
|
52
52
|
│ ├── custom-footer 增强状态栏(token/成本/时间/git/cwd)
|
|
53
53
|
│ ├── compact-header 精简启动信息
|
|
54
54
|
│ ├── auto-update 启动时检查更新
|
|
55
|
+
│ ├── bg-process ⏳ **后台进程** — 自动后台化长时间运行的命令(开发服务器等)
|
|
55
56
|
│ └── ant-colony/ 🐜 自主多智能体蚁群系统(可选)
|
|
56
57
|
├── prompts/ 10 个模板(/review /fix /commit /test ...)
|
|
57
58
|
├── skills/ 11 个技能(工具 + UI 设计 + 工作流)
|
|
@@ -70,12 +71,9 @@ pi # 开始编码
|
|
|
70
71
|
|
|
71
72
|
| | 主题 | 思维 | 包含 |
|
|
72
73
|
|---|------|------|------|
|
|
73
|
-
| 🟢
|
|
74
|
-
| 🔵
|
|
75
|
-
| 🟣
|
|
76
|
-
| 🟠 数据 & AI | Tokyo Night | medium | MLOps + 管道 |
|
|
77
|
-
| 🔴 极简 | Default | off | 仅核心 |
|
|
78
|
-
| ⚫ 全功率 | oh-pi Dark | high | 全部 + 蚁群 |
|
|
74
|
+
| 🟢 全功能 | oh-pi Dark | high | 全部扩展含蚁群 + 后台进程 |
|
|
75
|
+
| 🔵 干净 | Default | off | 无扩展仅核心 |
|
|
76
|
+
| 🟣 仅蚁群 | oh-pi Dark | medium | 蚁群 + 最小配置 |
|
|
79
77
|
|
|
80
78
|
### 提供商
|
|
81
79
|
|
|
@@ -133,12 +131,23 @@ pi(主进程)
|
|
|
133
131
|
|
|
134
132
|
交互模式下,蚁群显示实时进度:
|
|
135
133
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
134
|
+
- **状态栏** — 紧凑 footer 显示真实指标:完成任务数、活跃蚂蚁、工具调用次数、输出 token 数、成本、耗时
|
|
135
|
+
- **Ctrl+Shift+A** — 弹出详情面板,显示任务列表、活跃蚂蚁流、蚁群日志
|
|
138
136
|
- **通知** — 完成时显示汇总
|
|
139
137
|
|
|
140
138
|
使用 `/colony-stop` 中止运行中的蚁群。
|
|
141
139
|
|
|
140
|
+
### 信号协议
|
|
141
|
+
|
|
142
|
+
蚁群通过结构化信号与主对话通信,防止 LLM 轮询或猜测蚁群状态:
|
|
143
|
+
|
|
144
|
+
| 信号 | 含义 |
|
|
145
|
+
|------|------|
|
|
146
|
+
| `COLONY_SIGNAL:LAUNCHED` | 蚁群已启动 — 不要轮询 |
|
|
147
|
+
| `COLONY_SIGNAL:RUNNING` | 蚁群运行中 — 每轮注入 |
|
|
148
|
+
| `COLONY_SIGNAL:COMPLETE` | 蚁群完成 — 查看报告 |
|
|
149
|
+
| `COLONY_SIGNAL:FAILED` | 蚁群崩溃 — 报告错误 |
|
|
150
|
+
|
|
142
151
|
### 轮次控制
|
|
143
152
|
|
|
144
153
|
每只蚂蚁有严格的轮次预算,防止失控执行:
|
|
@@ -174,7 +183,7 @@ LLM 自行决定何时部署蚁群,你不需要操心:
|
|
|
174
183
|
蚁群自动找到你机器的最优并行度:
|
|
175
184
|
|
|
176
185
|
```
|
|
177
|
-
冷启动 →
|
|
186
|
+
冷启动 → ceil(max/2) 只蚂蚁(快速启动)
|
|
178
187
|
探索阶段 → 每波 +1,监控吞吐量
|
|
179
188
|
吞吐量下降 → 锁定最优值,稳定运行
|
|
180
189
|
CPU > 85% → 立即降低
|
package/dist/i18n.js
CHANGED
|
@@ -63,18 +63,12 @@ const messages = {
|
|
|
63
63
|
"provider.detectedAddHint": "Configure additional providers",
|
|
64
64
|
// preset
|
|
65
65
|
"preset.select": "Choose a preset:",
|
|
66
|
-
"preset.starter": "🟢 Starter",
|
|
67
|
-
"preset.starterHint": "New to AI coding? Start here",
|
|
68
|
-
"preset.pro": "🔵 Pro Developer",
|
|
69
|
-
"preset.proHint": "Full-stack dev with all the bells and whistles",
|
|
70
|
-
"preset.security": "🟣 Security Researcher",
|
|
71
|
-
"preset.securityHint": "Pentesting, auditing, vulnerability research",
|
|
72
|
-
"preset.dataai": "🟠 Data & AI Engineer",
|
|
73
|
-
"preset.dataaiHint": "MLOps, data pipelines, AI applications",
|
|
74
|
-
"preset.minimal": "🔴 Minimal",
|
|
75
|
-
"preset.minimalHint": "Just the core, nothing extra",
|
|
76
66
|
"preset.full": "⚫ Full Power",
|
|
77
|
-
"preset.fullHint": "
|
|
67
|
+
"preset.fullHint": "All features, extensions, and skills enabled",
|
|
68
|
+
"preset.clean": "✨ Clean",
|
|
69
|
+
"preset.cleanHint": "Minimal setup, just the essentials",
|
|
70
|
+
"preset.colony": "🐜 Colony",
|
|
71
|
+
"preset.colonyHint": "Ant swarm multi-agent system",
|
|
78
72
|
// theme
|
|
79
73
|
"theme.select": "Choose a theme:",
|
|
80
74
|
// keybindings
|
|
@@ -199,18 +193,12 @@ const messages = {
|
|
|
199
193
|
"provider.detectedAdd": "➕ 添加新 Provider",
|
|
200
194
|
"provider.detectedAddHint": "配置额外的 Provider",
|
|
201
195
|
"preset.select": "选择预设方案:",
|
|
202
|
-
"preset.starter": "🟢 入门",
|
|
203
|
-
"preset.starterHint": "AI 编程新手?从这里开始",
|
|
204
|
-
"preset.pro": "🔵 专业开发者",
|
|
205
|
-
"preset.proHint": "全栈开发,功能齐全",
|
|
206
|
-
"preset.security": "🟣 安全研究员",
|
|
207
|
-
"preset.securityHint": "渗透测试、审计、漏洞研究",
|
|
208
|
-
"preset.dataai": "🟠 数据与 AI 工程师",
|
|
209
|
-
"preset.dataaiHint": "MLOps、数据管道、AI 应用",
|
|
210
|
-
"preset.minimal": "🔴 极简",
|
|
211
|
-
"preset.minimalHint": "仅核心功能",
|
|
212
196
|
"preset.full": "⚫ 全功能",
|
|
213
|
-
"preset.fullHint": "
|
|
197
|
+
"preset.fullHint": "所有功能,包括蚁群系统",
|
|
198
|
+
"preset.clean": "🟢 简洁",
|
|
199
|
+
"preset.cleanHint": "核心功能,无蚁群",
|
|
200
|
+
"preset.colony": "🐜 蚁群",
|
|
201
|
+
"preset.colonyHint": "多 Agent 协同系统",
|
|
214
202
|
"theme.select": "选择主题:",
|
|
215
203
|
"kb.select": "快捷键方案:",
|
|
216
204
|
"kb.default": "⌨️ 默认",
|
|
@@ -328,18 +316,12 @@ const messages = {
|
|
|
328
316
|
"provider.detectedAdd": "➕ Ajouter de nouveaux fournisseurs",
|
|
329
317
|
"provider.detectedAddHint": "Configurer des fournisseurs supplémentaires",
|
|
330
318
|
"preset.select": "Choisir un préréglage :",
|
|
331
|
-
"preset.
|
|
332
|
-
"preset.
|
|
333
|
-
"preset.
|
|
334
|
-
"preset.
|
|
335
|
-
"preset.
|
|
336
|
-
"preset.
|
|
337
|
-
"preset.dataai": "🟠 Ingénieur Data & IA",
|
|
338
|
-
"preset.dataaiHint": "MLOps, pipelines de données, applications IA",
|
|
339
|
-
"preset.minimal": "🔴 Minimal",
|
|
340
|
-
"preset.minimalHint": "Juste l'essentiel",
|
|
341
|
-
"preset.full": "⚫ Pleine puissance",
|
|
342
|
-
"preset.fullHint": "Tout installé, colonie de fourmis incluse",
|
|
319
|
+
"preset.full": "⚫ Complet",
|
|
320
|
+
"preset.fullHint": "Toutes les extensions et compétences",
|
|
321
|
+
"preset.clean": "🟢 Propre",
|
|
322
|
+
"preset.cleanHint": "Configuration minimale, aucune extension",
|
|
323
|
+
"preset.colony": "🐜 Colonie",
|
|
324
|
+
"preset.colonyHint": "Mode essaim multi-agent de fourmis",
|
|
343
325
|
"theme.select": "Choisir un thème :",
|
|
344
326
|
"kb.select": "Schéma de raccourcis :",
|
|
345
327
|
"kb.default": "⌨️ Par défaut",
|
|
@@ -1,55 +1,28 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { t } from "../i18n.js";
|
|
3
3
|
const PRESETS = {
|
|
4
|
-
|
|
5
|
-
labelKey: "preset.
|
|
6
|
-
config: {
|
|
7
|
-
theme: "dark", keybindings: "default", thinking: "medium",
|
|
8
|
-
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
|
|
9
|
-
prompts: ["review", "fix", "explain", "commit"],
|
|
10
|
-
agents: "general-developer",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
pro: {
|
|
14
|
-
labelKey: "preset.pro", hintKey: "preset.proHint",
|
|
15
|
-
config: {
|
|
16
|
-
theme: "catppuccin-mocha", keybindings: "default", thinking: "high",
|
|
17
|
-
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
|
|
18
|
-
prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "document", "pr"],
|
|
19
|
-
agents: "fullstack-developer",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
security: {
|
|
23
|
-
labelKey: "preset.security", hintKey: "preset.securityHint",
|
|
24
|
-
config: {
|
|
25
|
-
theme: "cyberpunk", keybindings: "default", thinking: "high",
|
|
26
|
-
extensions: ["safe-guard", "custom-footer", "compact-header", "auto-update"],
|
|
27
|
-
prompts: ["review", "security", "fix", "explain"],
|
|
28
|
-
agents: "security-researcher",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
dataai: {
|
|
32
|
-
labelKey: "preset.dataai", hintKey: "preset.dataaiHint",
|
|
4
|
+
full: {
|
|
5
|
+
labelKey: "preset.full", hintKey: "preset.fullHint",
|
|
33
6
|
config: {
|
|
34
|
-
theme: "
|
|
35
|
-
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
|
|
36
|
-
prompts: ["review", "fix", "explain", "optimize", "document", "
|
|
37
|
-
agents: "
|
|
7
|
+
theme: "dark", keybindings: "default", thinking: "high",
|
|
8
|
+
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "ant-colony", "auto-update", "bg-process"],
|
|
9
|
+
prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "security", "document", "pr"],
|
|
10
|
+
agents: "colony-operator",
|
|
38
11
|
},
|
|
39
12
|
},
|
|
40
|
-
|
|
41
|
-
labelKey: "preset.
|
|
13
|
+
clean: {
|
|
14
|
+
labelKey: "preset.clean", hintKey: "preset.cleanHint",
|
|
42
15
|
config: {
|
|
43
16
|
theme: "dark", keybindings: "default", thinking: "off",
|
|
44
17
|
extensions: [], prompts: [], agents: "general-developer",
|
|
45
18
|
},
|
|
46
19
|
},
|
|
47
|
-
|
|
48
|
-
labelKey: "preset.
|
|
20
|
+
colony: {
|
|
21
|
+
labelKey: "preset.colony", hintKey: "preset.colonyHint",
|
|
49
22
|
config: {
|
|
50
|
-
theme: "dark", keybindings: "default", thinking: "
|
|
51
|
-
extensions: ["
|
|
52
|
-
prompts: ["review", "fix", "explain", "commit"
|
|
23
|
+
theme: "dark", keybindings: "default", thinking: "medium",
|
|
24
|
+
extensions: ["ant-colony", "auto-session-name", "compact-header"],
|
|
25
|
+
prompts: ["review", "fix", "explain", "commit"],
|
|
53
26
|
agents: "colony-operator",
|
|
54
27
|
},
|
|
55
28
|
},
|
package/dist/types.js
CHANGED
|
@@ -49,6 +49,7 @@ export const EXTENSIONS = [
|
|
|
49
49
|
{ name: "compact-header", label: "⚡ Compact Header — Dense startup info replacing verbose output", default: true },
|
|
50
50
|
{ name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
|
|
51
51
|
{ name: "auto-update", label: "🔄 Auto Update — Check for oh-pi updates on startup and notify", default: true },
|
|
52
|
+
{ name: "bg-process", label: "⏳ Bg Process — Auto-background long-running commands (dev servers, etc.)", default: false },
|
|
52
53
|
];
|
|
53
54
|
/** 快捷键绑定方案(default / vim / emacs) */
|
|
54
55
|
export const KEYBINDING_SCHEMES = {
|
package/package.json
CHANGED
|
@@ -14,10 +14,10 @@ const CPU_CORES = os.cpus().length;
|
|
|
14
14
|
|
|
15
15
|
export function defaultConcurrency(): ConcurrencyConfig {
|
|
16
16
|
return {
|
|
17
|
-
current:
|
|
17
|
+
current: 2,
|
|
18
18
|
min: 1,
|
|
19
19
|
max: Math.min(CPU_CORES, 8),
|
|
20
|
-
optimal:
|
|
20
|
+
optimal: 3,
|
|
21
21
|
history: [],
|
|
22
22
|
};
|
|
23
23
|
}
|
|
@@ -68,8 +68,8 @@ export function adapt(config: ConcurrencyConfig, pendingTasks: number): Concurre
|
|
|
68
68
|
const taskCap = Math.min(pendingTasks, config.max);
|
|
69
69
|
|
|
70
70
|
if (samples.length < 3) {
|
|
71
|
-
//
|
|
72
|
-
next.current = Math.min(2, taskCap);
|
|
71
|
+
// 冷启动:直接给一半 max,快速利用并发
|
|
72
|
+
next.current = Math.min(Math.ceil(config.max / 2), taskCap);
|
|
73
73
|
return next;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
|
-
import { Text, Container, Spacer } from "@mariozechner/pi-tui";
|
|
15
|
+
import { Text, Container, Spacer, matchesKey } from "@mariozechner/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { runColony, type QueenCallbacks } from "./queen.js";
|
|
18
18
|
import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
|
|
@@ -46,30 +46,6 @@ function casteIcon(caste: string): string {
|
|
|
46
46
|
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function progressBar(done: number, total: number, width: number, theme: any): string {
|
|
50
|
-
if (total === 0) return "";
|
|
51
|
-
const pct = Math.min(done / total, 1);
|
|
52
|
-
const filled = Math.round(pct * width);
|
|
53
|
-
const empty = width - filled;
|
|
54
|
-
return theme.fg("success", "█".repeat(filled)) + theme.fg("muted", "░".repeat(empty)) + " " + theme.fg("accent", `${done}/${total}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function phasePipeline(status: string, theme: any): string {
|
|
58
|
-
const phases = [
|
|
59
|
-
{ key: "scouting", icon: "🔍", label: "Scout" },
|
|
60
|
-
{ key: "working", icon: "⚒️", label: "Work" },
|
|
61
|
-
{ key: "reviewing", icon: "🛡️", label: "Review" },
|
|
62
|
-
{ key: "done", icon: "✅", label: "Done" },
|
|
63
|
-
];
|
|
64
|
-
const idx = phases.findIndex(p => p.key === status);
|
|
65
|
-
return phases.map((p, i) => {
|
|
66
|
-
const label = `${p.icon} ${p.label}`;
|
|
67
|
-
if (i < idx) return theme.fg("success", label);
|
|
68
|
-
if (i === idx) return theme.fg("accent", theme.bold(label));
|
|
69
|
-
return theme.fg("muted", label);
|
|
70
|
-
}).join(theme.fg("muted", " → "));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
// ═══ Background colony state ═══
|
|
74
50
|
|
|
75
51
|
interface AntStreamState {
|
|
@@ -94,64 +70,41 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
94
70
|
// 当前运行中的后台蚁群(同时只允许一个)
|
|
95
71
|
let activeColony: BackgroundColony | null = null;
|
|
96
72
|
|
|
97
|
-
// ───
|
|
73
|
+
// ─── Status 渲染 ───
|
|
98
74
|
|
|
99
75
|
let lastRender = 0;
|
|
100
76
|
const throttledRender = () => {
|
|
101
77
|
const now = Date.now();
|
|
102
|
-
if (now - lastRender <
|
|
78
|
+
if (now - lastRender < 500) return;
|
|
103
79
|
lastRender = now;
|
|
104
|
-
|
|
105
|
-
renderStatus();
|
|
80
|
+
pi.events.emit("ant-colony:render");
|
|
106
81
|
};
|
|
107
82
|
|
|
108
|
-
|
|
109
|
-
if (!activeColony) return;
|
|
110
|
-
const { state, phase, antStreams } = activeColony;
|
|
111
|
-
const streams = Array.from(antStreams.values());
|
|
112
|
-
const lines: string[] = [];
|
|
113
|
-
|
|
114
|
-
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
115
|
-
const cost = state ? formatCost(state.metrics.totalCost) : "$0";
|
|
116
|
-
lines.push(`🐜 Colony: ${phase} │ ${elapsed} │ ${cost}`);
|
|
117
|
-
|
|
118
|
-
if (state && state.metrics.tasksTotal > 0) {
|
|
119
|
-
const m = state.metrics;
|
|
120
|
-
const pct = Math.round((m.tasksDone / m.tasksTotal) * 100);
|
|
121
|
-
const filled = Math.round(pct / 5);
|
|
122
|
-
lines.push(` ${"█".repeat(filled)}${"░".repeat(20 - filled)} ${m.tasksDone}/${m.tasksTotal} (${pct}%)`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
for (const s of streams.slice(-4)) {
|
|
126
|
-
const icon = casteIcon(s.caste);
|
|
127
|
-
const line = s.lastLine.length > 60 ? s.lastLine.slice(0, 57) + "..." : s.lastLine;
|
|
128
|
-
lines.push(` ${icon} ${s.antId.slice(0, 15)} ▸ ${line || "..."}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
pi.events.emit("ant-colony:widget", lines);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const renderStatus = () => {
|
|
135
|
-
if (!activeColony) return;
|
|
136
|
-
const { state, antStreams } = activeColony;
|
|
137
|
-
if (!state) return;
|
|
138
|
-
const m = state.metrics;
|
|
139
|
-
const active = antStreams.size;
|
|
140
|
-
pi.events.emit("ant-colony:status",
|
|
141
|
-
`🐜 ${statusIcon(state.status)} ${m.tasksDone}/${m.tasksTotal} tasks │ ${active} active │ ${formatCost(m.totalCost)}`
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// 监听自己的事件来更新 UI(确保在有 ctx 的上下文中)
|
|
83
|
+
// 监听事件来更新 UI(确保在有 ctx 的上下文中)
|
|
146
84
|
pi.on("session_start", async (_event, ctx) => {
|
|
147
|
-
pi.events.on("ant-colony:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
85
|
+
pi.events.on("ant-colony:render", () => {
|
|
86
|
+
if (!activeColony) return;
|
|
87
|
+
const { state, antStreams } = activeColony;
|
|
88
|
+
const active = antStreams.size;
|
|
89
|
+
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
90
|
+
const m = state?.metrics;
|
|
91
|
+
const colonyStatus = state?.status || "scouting";
|
|
92
|
+
const ants = state?.ants || [];
|
|
93
|
+
const turns = ants.reduce((s, a) => s + a.usage.turns, 0);
|
|
94
|
+
const outTok = ants.reduce((s, a) => s + a.usage.output, 0);
|
|
95
|
+
|
|
96
|
+
const parts = [`🐜 ${statusIcon(colonyStatus)}`];
|
|
97
|
+
if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
|
|
98
|
+
parts.push(`${active}⚡`);
|
|
99
|
+
parts.push(`${turns}↻`);
|
|
100
|
+
parts.push(formatTokens(outTok) + "↑");
|
|
101
|
+
if (m) parts.push(formatCost(m.totalCost));
|
|
102
|
+
parts.push(elapsed);
|
|
103
|
+
|
|
104
|
+
ctx.ui.setStatus("ant-colony", parts.join(" │ "));
|
|
152
105
|
});
|
|
106
|
+
|
|
153
107
|
pi.events.on("ant-colony:clear-ui", () => {
|
|
154
|
-
ctx.ui.setWidget("ant-colony", undefined);
|
|
155
108
|
ctx.ui.setStatus("ant-colony", undefined);
|
|
156
109
|
});
|
|
157
110
|
pi.events.on("ant-colony:notify", (data: { msg: string; level: "info" | "success" | "warning" | "error" }) => {
|
|
@@ -361,7 +314,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
361
314
|
// 注入结果到对话
|
|
362
315
|
pi.sendMessage({
|
|
363
316
|
customType: "ant-colony-report",
|
|
364
|
-
content: report
|
|
317
|
+
content: `[COLONY_SIGNAL:COMPLETE]\n${report}`,
|
|
365
318
|
display: true,
|
|
366
319
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
367
320
|
|
|
@@ -373,6 +326,11 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
373
326
|
pi.events.emit("ant-colony:clear-ui");
|
|
374
327
|
activeColony = null;
|
|
375
328
|
pi.events.emit("ant-colony:notify", { msg: `🐜 Colony crashed: ${e}`, level: "error" });
|
|
329
|
+
pi.sendMessage({
|
|
330
|
+
customType: "ant-colony-report",
|
|
331
|
+
content: `[COLONY_SIGNAL:FAILED]\n## 🐜 Colony Crashed\n${e}`,
|
|
332
|
+
display: true,
|
|
333
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
376
334
|
});
|
|
377
335
|
}
|
|
378
336
|
|
|
@@ -390,7 +348,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
390
348
|
} catch { /* ignore */ }
|
|
391
349
|
|
|
392
350
|
const colonyStatus = activeColony
|
|
393
|
-
? `\n[
|
|
351
|
+
? `\n[COLONY_SIGNAL:RUNNING] A colony is currently running in the background for goal: "${activeColony.goal.slice(0, 100)}". Do NOT launch another colony. Do NOT check progress or assume failure. You will receive [COLONY_SIGNAL:COMPLETE] automatically when it finishes. If the user asks about colony status, tell them it's still running. Use /colony-stop to cancel.`
|
|
394
352
|
: "";
|
|
395
353
|
|
|
396
354
|
return {
|
|
@@ -405,8 +363,17 @@ You have the ant_colony tool. Use it automatically when:
|
|
|
405
363
|
Do NOT ask for confirmation. Call ant_colony directly with a clear goal.
|
|
406
364
|
For simple single-file tasks, work directly without the colony.
|
|
407
365
|
|
|
408
|
-
|
|
409
|
-
|
|
366
|
+
[COLONY SIGNAL PROTOCOL]
|
|
367
|
+
The colony communicates via signals. You MUST obey these:
|
|
368
|
+
- [COLONY_SIGNAL:LAUNCHED] — Colony started. Do NOT poll, check, or assume failure. Wait for completion signal.
|
|
369
|
+
- [COLONY_SIGNAL:RUNNING] — Colony is active. Do NOT launch another or check progress.
|
|
370
|
+
- [COLONY_SIGNAL:COMPLETE] — Colony finished. Review the report and summarize results to the user.
|
|
371
|
+
- [COLONY_SIGNAL:FAILED] — Colony crashed. Report the error to the user.
|
|
372
|
+
|
|
373
|
+
After launching a colony, your ONLY correct behavior is:
|
|
374
|
+
1. Tell the user the colony is running
|
|
375
|
+
2. Continue chatting about OTHER topics if the user asks
|
|
376
|
+
3. Wait for [COLONY_SIGNAL:COMPLETE] or [COLONY_SIGNAL:FAILED] — do NOT guess the outcome
|
|
410
377
|
${modelList ? `
|
|
411
378
|
[COLONY MODEL SELECTION]
|
|
412
379
|
Available models: ${modelList}
|
|
@@ -482,7 +449,7 @@ Strategy for choosing per-caste models:
|
|
|
482
449
|
launchBackgroundColony(colonyParams);
|
|
483
450
|
|
|
484
451
|
return {
|
|
485
|
-
content: [{ type: "text", text:
|
|
452
|
+
content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\n⚠️ IMPORTANT: The colony is now running autonomously. Do NOT check progress, do NOT ask about status, do NOT assume failure. You will receive a [COLONY_SIGNAL:COMPLETE] message automatically when it finishes. Continue chatting about other topics or wait silently.` }],
|
|
486
453
|
};
|
|
487
454
|
},
|
|
488
455
|
|
|
@@ -508,7 +475,7 @@ Strategy for choosing per-caste models:
|
|
|
508
475
|
));
|
|
509
476
|
if (activeColony) {
|
|
510
477
|
container.addChild(new Text(theme.fg("dim", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
|
|
511
|
-
container.addChild(new Text(theme.fg("muted", `
|
|
478
|
+
container.addChild(new Text(theme.fg("muted", ` Ctrl+Shift+A for details │ /colony-stop to cancel`), 0, 0));
|
|
512
479
|
}
|
|
513
480
|
return container;
|
|
514
481
|
},
|
|
@@ -550,6 +517,99 @@ Strategy for choosing per-caste models:
|
|
|
550
517
|
return container;
|
|
551
518
|
});
|
|
552
519
|
|
|
520
|
+
// ═══ Shortcut: Ctrl+Shift+A 展开蚁群详情 ═══
|
|
521
|
+
pi.registerShortcut("ctrl+shift+a", {
|
|
522
|
+
description: "Show ant colony details",
|
|
523
|
+
async handler(ctx) {
|
|
524
|
+
if (!activeColony) {
|
|
525
|
+
ctx.ui.notify("No colony is currently running.", "info");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
530
|
+
let cachedWidth: number | undefined;
|
|
531
|
+
let cachedLines: string[] | undefined;
|
|
532
|
+
|
|
533
|
+
const buildLines = (width: number): string[] => {
|
|
534
|
+
const c = activeColony;
|
|
535
|
+
if (!c) return [theme.fg("muted", " No colony running.")];
|
|
536
|
+
|
|
537
|
+
const lines: string[] = [];
|
|
538
|
+
const w = width - 2; // padding
|
|
539
|
+
|
|
540
|
+
// ── Header ──
|
|
541
|
+
const elapsed = c.state ? formatDuration(Date.now() - c.state.createdAt) : "0s";
|
|
542
|
+
const cost = c.state ? formatCost(c.state.metrics.totalCost) : "$0";
|
|
543
|
+
lines.push(theme.fg("accent", theme.bold(` 🐜 Colony Details`)) + theme.fg("muted", ` │ ${elapsed} │ ${cost}`));
|
|
544
|
+
lines.push(theme.fg("dim", ` Goal: ${c.goal.slice(0, w - 8)}`));
|
|
545
|
+
lines.push("");
|
|
546
|
+
|
|
547
|
+
// ── Tasks ──
|
|
548
|
+
const tasks = c.state?.tasks || [];
|
|
549
|
+
if (tasks.length > 0) {
|
|
550
|
+
lines.push(theme.fg("accent", " Tasks"));
|
|
551
|
+
for (const t of tasks.slice(0, 15)) {
|
|
552
|
+
const icon = t.status === "done" ? theme.fg("success", "✓")
|
|
553
|
+
: t.status === "failed" ? theme.fg("error", "✗")
|
|
554
|
+
: t.status === "active" ? theme.fg("warning", "●")
|
|
555
|
+
: theme.fg("dim", "○");
|
|
556
|
+
const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
|
|
557
|
+
lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", t.title.slice(0, w - 12))}${dur}`);
|
|
558
|
+
}
|
|
559
|
+
if (tasks.length > 15) lines.push(theme.fg("muted", ` ⋯ +${tasks.length - 15} more`));
|
|
560
|
+
lines.push("");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Active Ants ──
|
|
564
|
+
const streams = Array.from(c.antStreams.values());
|
|
565
|
+
if (streams.length > 0) {
|
|
566
|
+
lines.push(theme.fg("accent", " Active Ants"));
|
|
567
|
+
for (const s of streams) {
|
|
568
|
+
const line = s.lastLine.length > w - 20 ? s.lastLine.slice(0, w - 23) + "..." : s.lastLine;
|
|
569
|
+
lines.push(` ${casteIcon(s.caste)} ${theme.fg("accent", s.antId.slice(0, 14))} ${theme.fg("dim", `${s.tokens}tok`)} ${theme.fg("muted", "▸")} ${line}`);
|
|
570
|
+
}
|
|
571
|
+
lines.push("");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── Log (last 8) ──
|
|
575
|
+
if (c.log.length > 0) {
|
|
576
|
+
lines.push(theme.fg("accent", " Log"));
|
|
577
|
+
for (const l of c.log.slice(-8)) {
|
|
578
|
+
lines.push(theme.fg("dim", ` ${l.slice(0, w - 2)}`));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(theme.fg("muted", " esc close"));
|
|
584
|
+
return lines;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// 定时刷新
|
|
588
|
+
const timer = setInterval(() => {
|
|
589
|
+
cachedWidth = undefined;
|
|
590
|
+
cachedLines = undefined;
|
|
591
|
+
tui.requestRender();
|
|
592
|
+
}, 1000);
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
render(width: number): string[] {
|
|
596
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
597
|
+
cachedLines = buildLines(width);
|
|
598
|
+
cachedWidth = width;
|
|
599
|
+
return cachedLines;
|
|
600
|
+
},
|
|
601
|
+
invalidate() { cachedWidth = undefined; cachedLines = undefined; },
|
|
602
|
+
handleInput(data: string) {
|
|
603
|
+
if (matchesKey(data, "escape")) {
|
|
604
|
+
clearInterval(timer);
|
|
605
|
+
done(undefined);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
553
613
|
// ═══ Command: /colony-stop ═══
|
|
554
614
|
pi.registerCommand("colony-stop", {
|
|
555
615
|
description: "Stop the running background colony",
|
|
@@ -21,6 +21,7 @@ export class Nest {
|
|
|
21
21
|
private pheromoneCache: Pheromone[] = [];
|
|
22
22
|
private pheromoneOffset: number = 0;
|
|
23
23
|
private taskCache: Map<string, Task> = new Map();
|
|
24
|
+
private stateCache: ColonyState | null = null;
|
|
24
25
|
|
|
25
26
|
constructor(private cwd: string, private colonyId: string) {
|
|
26
27
|
this.dir = path.join(cwd, ".ant-colony", colonyId);
|
|
@@ -35,13 +36,16 @@ export class Nest {
|
|
|
35
36
|
|
|
36
37
|
init(state: ColonyState): void {
|
|
37
38
|
this.writeJson(this.stateFile, state);
|
|
39
|
+
this.stateCache = state;
|
|
38
40
|
this.taskCache.clear();
|
|
39
41
|
for (const t of state.tasks) this.writeTask(t);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
getState(): ColonyState {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
if (!this.stateCache) {
|
|
46
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
47
|
+
}
|
|
48
|
+
const base = { ...this.stateCache };
|
|
45
49
|
base.tasks = this.getAllTasks();
|
|
46
50
|
base.pheromones = this.getAllPheromones();
|
|
47
51
|
return base;
|
|
@@ -49,9 +53,11 @@ export class Nest {
|
|
|
49
53
|
|
|
50
54
|
updateState(patch: Partial<Pick<ColonyState, "status" | "concurrency" | "metrics" | "ants" | "finishedAt">>): void {
|
|
51
55
|
this.withStateLock(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if (!this.stateCache) {
|
|
57
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
58
|
+
}
|
|
59
|
+
Object.assign(this.stateCache, patch);
|
|
60
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
55
61
|
});
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -186,11 +192,13 @@ export class Nest {
|
|
|
186
192
|
|
|
187
193
|
updateAnt(ant: Ant): void {
|
|
188
194
|
this.withStateLock(() => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
if (!this.stateCache) {
|
|
196
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
197
|
+
}
|
|
198
|
+
const idx = this.stateCache.ants.findIndex(a => a.id === ant.id);
|
|
199
|
+
if (idx >= 0) this.stateCache.ants[idx] = ant;
|
|
200
|
+
else this.stateCache.ants.push(ant);
|
|
201
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -198,12 +206,14 @@ export class Nest {
|
|
|
198
206
|
|
|
199
207
|
recordSample(sample: ConcurrencySample): void {
|
|
200
208
|
this.withStateLock(() => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
if (!this.stateCache) {
|
|
210
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
211
|
+
}
|
|
212
|
+
this.stateCache.concurrency.history.push(sample);
|
|
213
|
+
if (this.stateCache.concurrency.history.length > 30) {
|
|
214
|
+
this.stateCache.concurrency.history = this.stateCache.concurrency.history.slice(-30);
|
|
205
215
|
}
|
|
206
|
-
this.writeJson(this.stateFile,
|
|
216
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
207
217
|
});
|
|
208
218
|
}
|
|
209
219
|
|
|
@@ -236,9 +236,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// 自适应并发(每
|
|
239
|
+
// 自适应并发(每 2000ms 采样一次)
|
|
240
240
|
const now = Date.now();
|
|
241
|
-
if (now - lastSampleTime >=
|
|
241
|
+
if (now - lastSampleTime >= 2000) {
|
|
242
242
|
lastSampleTime = now;
|
|
243
243
|
const completedRecently = state.tasks.filter(t =>
|
|
244
244
|
t.status === "done" && t.finishedAt && t.finishedAt > now - 120000
|
|
@@ -260,7 +260,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
260
260
|
|
|
261
261
|
if (slotsAvailable === 0) {
|
|
262
262
|
// 等待一下再检查
|
|
263
|
-
await new Promise(r => setTimeout(r,
|
|
263
|
+
await new Promise(r => setTimeout(r, 500));
|
|
264
264
|
continue;
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -336,51 +336,39 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
336
336
|
};
|
|
337
337
|
|
|
338
338
|
try {
|
|
339
|
-
// ═══ Phase 1:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
let workerTasks: Task[] = [];
|
|
339
|
+
// ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
|
|
340
|
+
callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
|
|
341
|
+
await runAntWave({ ...waveBase, caste: "scout" });
|
|
343
342
|
|
|
344
|
-
|
|
345
|
-
callbacks.onPhase("scouting", scoutAttempt === 0
|
|
346
|
-
? "Dispatching scout ants to explore codebase..."
|
|
347
|
-
: `Scout relay ${scoutAttempt}/${MAX_SCOUT_RETRIES} (building on previous discoveries)...`);
|
|
343
|
+
let workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
348
344
|
|
|
345
|
+
// 只在完全没有 worker 任务时才重试一次
|
|
346
|
+
if (workerTasks.length === 0) {
|
|
347
|
+
const pheromones = nest.getAllPheromones();
|
|
348
|
+
const hasDiscoveries = pheromones.some(p => p.type === "discovery");
|
|
349
|
+
const relayTask: Task = {
|
|
350
|
+
id: makeTaskId(),
|
|
351
|
+
parentId: null,
|
|
352
|
+
title: "Scout relay: generate worker tasks",
|
|
353
|
+
description: hasDiscoveries
|
|
354
|
+
? `Previous scout found information but didn't generate worker tasks. Generate concrete worker tasks based on discoveries.\n\nGoal:\n${opts.goal}`
|
|
355
|
+
: `Explore the codebase for this goal and generate worker tasks:\n\n${opts.goal}`,
|
|
356
|
+
caste: "scout",
|
|
357
|
+
status: "pending",
|
|
358
|
+
priority: 1,
|
|
359
|
+
files: [],
|
|
360
|
+
claimedBy: null,
|
|
361
|
+
result: null,
|
|
362
|
+
error: null,
|
|
363
|
+
spawnedTasks: [],
|
|
364
|
+
createdAt: Date.now(),
|
|
365
|
+
startedAt: null,
|
|
366
|
+
finishedAt: null,
|
|
367
|
+
};
|
|
368
|
+
nest.writeTask(relayTask);
|
|
369
|
+
callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
|
|
349
370
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
350
|
-
|
|
351
371
|
workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
352
|
-
if (workerTasks.length > 0) break;
|
|
353
|
-
|
|
354
|
-
scoutAttempt++;
|
|
355
|
-
if (scoutAttempt <= MAX_SCOUT_RETRIES) {
|
|
356
|
-
// 接力:检查是否有信息素(前一只 scout 的部分发现)
|
|
357
|
-
const pheromones = nest.getAllPheromones();
|
|
358
|
-
const hasDiscoveries = pheromones.some(p => p.type === "discovery");
|
|
359
|
-
|
|
360
|
-
// 创建接力 scout 任务(而非重置旧任务)
|
|
361
|
-
const relayDescription = hasDiscoveries
|
|
362
|
-
? `Continue exploring the codebase. Previous scouts made partial discoveries (see pheromone trail). Focus on areas NOT yet explored and generate worker tasks.\n\nOriginal goal:\n${opts.goal}`
|
|
363
|
-
: `Explore the codebase and identify all files, modules, and dependencies relevant to this goal:\n\n${opts.goal}\n\nBe thorough. The colony depends on your intelligence.`;
|
|
364
|
-
|
|
365
|
-
const relayTask: Task = {
|
|
366
|
-
id: makeTaskId(),
|
|
367
|
-
parentId: null,
|
|
368
|
-
title: hasDiscoveries ? "Scout relay: continue exploration" : "Scout: explore codebase for goal",
|
|
369
|
-
description: relayDescription,
|
|
370
|
-
caste: "scout",
|
|
371
|
-
status: "pending",
|
|
372
|
-
priority: 1,
|
|
373
|
-
files: [],
|
|
374
|
-
claimedBy: null,
|
|
375
|
-
result: null,
|
|
376
|
-
error: null,
|
|
377
|
-
spawnedTasks: [],
|
|
378
|
-
createdAt: Date.now(),
|
|
379
|
-
startedAt: null,
|
|
380
|
-
finishedAt: null,
|
|
381
|
-
};
|
|
382
|
-
nest.writeTask(relayTask);
|
|
383
|
-
}
|
|
384
372
|
}
|
|
385
373
|
|
|
386
374
|
if (workerTasks.length === 0) {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-pi Background Process Extension
|
|
3
|
+
*
|
|
4
|
+
* 任何 bash 命令超时未完成时,自动送到后台执行。
|
|
5
|
+
* 提供 bg_status 工具让 LLM 查看/停止后台进程。
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
10
|
+
import { spawn, execSync } from "node:child_process";
|
|
11
|
+
import { writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
/** 超时阈值(毫秒),超过此时间自动后台化 */
|
|
14
|
+
const BG_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
16
|
+
interface BgProcess {
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
logFile: string;
|
|
20
|
+
startedAt: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function (pi: ExtensionAPI) {
|
|
24
|
+
const bgProcesses = new Map<number, BgProcess>();
|
|
25
|
+
|
|
26
|
+
// 覆盖内置 bash 工具
|
|
27
|
+
pi.registerTool({
|
|
28
|
+
name: "bash",
|
|
29
|
+
label: "Bash",
|
|
30
|
+
description: `Execute a bash command. Output is truncated to 2000 lines or 50KB. If a command runs longer than ${BG_TIMEOUT_MS / 1000}s, it is automatically backgrounded and you get the PID + log file path. Use the bg_status tool to check on backgrounded processes.`,
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
33
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
|
|
34
|
+
}),
|
|
35
|
+
async execute(toolCallId, params, signal) {
|
|
36
|
+
const { command } = params;
|
|
37
|
+
const userTimeout = params.timeout ? params.timeout * 1000 : undefined;
|
|
38
|
+
const effectiveTimeout = userTimeout ?? BG_TIMEOUT_MS;
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
let settled = false;
|
|
44
|
+
|
|
45
|
+
const child = spawn("bash", ["-c", command], {
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
env: { ...process.env },
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); });
|
|
52
|
+
child.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); });
|
|
53
|
+
|
|
54
|
+
// 超时处理:分离进程,送到后台
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
|
|
59
|
+
// 分离子进程,让它继续运行
|
|
60
|
+
child.stdout?.removeAllListeners();
|
|
61
|
+
child.stderr?.removeAllListeners();
|
|
62
|
+
child.removeAllListeners();
|
|
63
|
+
child.unref();
|
|
64
|
+
|
|
65
|
+
const logFile = `/tmp/oh-pi-bg-${Date.now()}.log`;
|
|
66
|
+
const pid = child.pid!;
|
|
67
|
+
|
|
68
|
+
// 启动一个 tail 进程把后续输出写入日志
|
|
69
|
+
try {
|
|
70
|
+
const tailCmd = `(echo ${JSON.stringify(stdout + stderr)}; tail --pid=${pid} -f /proc/${pid}/fd/1 2>/dev/null) > ${logFile} 2>&1 &`;
|
|
71
|
+
spawn("bash", ["-c", tailCmd], { detached: true, stdio: "ignore" }).unref();
|
|
72
|
+
} catch {
|
|
73
|
+
// fallback: 至少把已有输出写入日志
|
|
74
|
+
writeFileSync(logFile, stdout + stderr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
bgProcesses.set(pid, { pid, command, logFile, startedAt: Date.now() });
|
|
78
|
+
|
|
79
|
+
const preview = (stdout + stderr).slice(0, 500);
|
|
80
|
+
const text = `Command still running after ${effectiveTimeout / 1000}s, moved to background.\nPID: ${pid}\nLog: ${logFile}\nView output: tail -f ${logFile}\nStop: kill ${pid}\n\nOutput so far:\n${preview}`;
|
|
81
|
+
|
|
82
|
+
resolve({
|
|
83
|
+
content: [{ type: "text", text }],
|
|
84
|
+
details: {},
|
|
85
|
+
});
|
|
86
|
+
}, effectiveTimeout);
|
|
87
|
+
|
|
88
|
+
// 正常结束
|
|
89
|
+
child.on("close", (code) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
|
|
94
|
+
const output = (stdout + stderr).trim();
|
|
95
|
+
const exitInfo = code !== 0 ? `\n[Exit code: ${code}]` : "";
|
|
96
|
+
|
|
97
|
+
resolve({
|
|
98
|
+
content: [{ type: "text", text: output + exitInfo }],
|
|
99
|
+
details: {},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on("error", (err) => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
settled = true;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
|
|
108
|
+
resolve({
|
|
109
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
110
|
+
details: {},
|
|
111
|
+
isError: true,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 处理 abort signal
|
|
116
|
+
if (signal) {
|
|
117
|
+
signal.addEventListener("abort", () => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
try { child.kill(); } catch {}
|
|
122
|
+
resolve({
|
|
123
|
+
content: [{ type: "text", text: "Command cancelled." }],
|
|
124
|
+
details: {},
|
|
125
|
+
});
|
|
126
|
+
}, { once: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// bg_status 工具:查看/管理后台进程
|
|
133
|
+
pi.registerTool({
|
|
134
|
+
name: "bg_status",
|
|
135
|
+
label: "Background Process Status",
|
|
136
|
+
description: "Check status, view output, or stop background processes that were auto-backgrounded.",
|
|
137
|
+
parameters: Type.Object({
|
|
138
|
+
action: StringEnum(["list", "log", "stop"] as const, { description: "list=show all, log=view output, stop=kill process" }),
|
|
139
|
+
pid: Type.Optional(Type.Number({ description: "PID of the process (required for log/stop)" })),
|
|
140
|
+
}),
|
|
141
|
+
async execute(toolCallId, params) {
|
|
142
|
+
const { action, pid } = params;
|
|
143
|
+
|
|
144
|
+
if (action === "list") {
|
|
145
|
+
if (bgProcesses.size === 0) {
|
|
146
|
+
return { content: [{ type: "text", text: "No background processes." }], details: {} };
|
|
147
|
+
}
|
|
148
|
+
const lines = [...bgProcesses.values()].map((p) => {
|
|
149
|
+
const alive = isAlive(p.pid);
|
|
150
|
+
const status = alive ? "🟢 running" : "⚪ stopped";
|
|
151
|
+
return `PID: ${p.pid} | ${status} | Log: ${p.logFile}\n Cmd: ${p.command}`;
|
|
152
|
+
});
|
|
153
|
+
return { content: [{ type: "text", text: lines.join("\n\n") }], details: {} };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!pid) {
|
|
157
|
+
return { content: [{ type: "text", text: "Error: pid is required for log/stop" }], details: {}, isError: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const proc = bgProcesses.get(pid);
|
|
161
|
+
|
|
162
|
+
if (action === "log") {
|
|
163
|
+
const logFile = proc?.logFile;
|
|
164
|
+
if (logFile && existsSync(logFile)) {
|
|
165
|
+
try {
|
|
166
|
+
const content = readFileSync(logFile, "utf-8");
|
|
167
|
+
const tail = content.slice(-5000);
|
|
168
|
+
const truncated = content.length > 5000 ? `[...truncated, showing last 5000 chars]\n${tail}` : tail;
|
|
169
|
+
return { content: [{ type: "text", text: truncated || "(empty)" }], details: {} };
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
return { content: [{ type: "text", text: `Error reading log: ${e.message}` }], details: {}, isError: true };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// fallback: 直接读 /proc
|
|
175
|
+
try {
|
|
176
|
+
const out = execSync(`tail -20 /proc/${pid}/fd/1 2>/dev/null || echo "(cannot read output)"`, { timeout: 3000 }).toString();
|
|
177
|
+
return { content: [{ type: "text", text: out }], details: {} };
|
|
178
|
+
} catch {
|
|
179
|
+
return { content: [{ type: "text", text: "No log available for this PID." }], details: {} };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (action === "stop") {
|
|
184
|
+
try {
|
|
185
|
+
process.kill(pid, "SIGTERM");
|
|
186
|
+
bgProcesses.delete(pid);
|
|
187
|
+
return { content: [{ type: "text", text: `Process ${pid} terminated.` }], details: {} };
|
|
188
|
+
} catch {
|
|
189
|
+
bgProcesses.delete(pid);
|
|
190
|
+
return { content: [{ type: "text", text: `Process ${pid} not found (already stopped?).` }], details: {} };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], details: {}, isError: true };
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 清理:退出时杀掉所有后台进程
|
|
199
|
+
pi.on("session_shutdown", async () => {
|
|
200
|
+
for (const [pid] of bgProcesses) {
|
|
201
|
+
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
202
|
+
}
|
|
203
|
+
bgProcesses.clear();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isAlive(pid: number): boolean {
|
|
208
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
209
|
+
}
|