oh-pi 0.1.75 → 0.1.77
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 +25 -12
- package/README.md +35 -12
- package/README.zh.md +41 -12
- package/dist/index.js +1 -1
- package/dist/registry.js +0 -1
- package/package.json +1 -1
- package/pi-package/agents/colony-operator.md +3 -2
- package/pi-package/extensions/ant-colony/index.ts +273 -31
- package/pi-package/extensions/ant-colony/ui.test.ts +18 -1
- package/pi-package/extensions/ant-colony/ui.ts +32 -2
- package/pi-package/extensions/smart-compact.test.ts +0 -64
- package/pi-package/extensions/smart-compact.ts +0 -89
package/README.fr.md
CHANGED
|
@@ -22,20 +22,28 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent est puissant dès l'installation. Mais configurer manuellement les fournisseurs, thèmes, extensions, compétences et modèles de prompts est fastidieux. oh-pi offre une TUI moderne qui fait tout en moins d'une minute — et embarque un **essaim de fourmis** qui transforme pi en système multi-agents.
|
|
28
|
-
|
|
29
|
-
## Démarrage rapide
|
|
25
|
+
## Démarrage en 30 secondes
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # tout configurer
|
|
33
29
|
pi # commencer à coder
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
oh-pi détecte automatiquement votre environnement, vous guide dans une TUI moderne, puis écrit `~/.pi/agent/`.
|
|
33
|
+
|
|
34
|
+
Déjà configuré ? oh-pi détecte les fichiers existants et propose une **sauvegarde avant écrasement**.
|
|
35
|
+
|
|
36
|
+
## Valeur en 2 minutes
|
|
37
|
+
|
|
38
|
+
pi-coding-agent est puissant par défaut, mais la configuration manuelle (fournisseurs, thèmes, extensions, skills, prompts) prend du temps. oh-pi compresse cette phase en moins d'une minute — puis ajoute la colonie pour les tâches complexes.
|
|
39
|
+
|
|
40
|
+
- [`docs/DEMO-SCRIPT.md`](./docs/DEMO-SCRIPT.md) — démo rapide en 2 minutes
|
|
41
|
+
- [`ROADMAP.md`](./ROADMAP.md) — positionnement, jalons, métriques
|
|
42
|
+
- [`DECISIONS.md`](./DECISIONS.md) — décisions produit et compromis techniques
|
|
43
|
+
|
|
44
|
+
## Quand ne pas utiliser la colonie
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
Utilisez le flux pi classique (sans colonie) si la tâche est petite, très exploratoire, ou nécessite un pilotage humain continu.
|
|
39
47
|
|
|
40
48
|
## Ce que vous obtenez
|
|
41
49
|
|
|
@@ -139,14 +147,19 @@ Utilisez `/colony-stop` pour arrêter une colonie en cours.
|
|
|
139
147
|
|
|
140
148
|
### Protocole de signaux
|
|
141
149
|
|
|
142
|
-
La colonie communique
|
|
150
|
+
La colonie communique via des signaux structurés, pour éviter toute supposition côté modèle :
|
|
143
151
|
|
|
144
152
|
| Signal | Signification |
|
|
145
153
|
|--------|---------------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
154
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée en arrière-plan |
|
|
155
|
+
| `COLONY_SIGNAL:SCOUTING` | Vague d'éclaireuses en exploration/planification |
|
|
156
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | Boucle de récupération du plan en cours |
|
|
157
|
+
| `COLONY_SIGNAL:WORKING` | Exécution des tâches par les ouvrières |
|
|
158
|
+
| `COLONY_SIGNAL:REVIEWING` | Revue qualité par les soldats |
|
|
159
|
+
| `COLONY_SIGNAL:TASK_DONE` | Tâche terminée (jalon de progression) |
|
|
160
|
+
| `COLONY_SIGNAL:COMPLETE` | Mission terminée, rapport injecté |
|
|
161
|
+
| `COLONY_SIGNAL:FAILED` | Mission échouée avec diagnostic |
|
|
162
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | Budget maximal atteint |
|
|
150
163
|
|
|
151
164
|
### Contrôle des tours
|
|
152
165
|
|
package/README.md
CHANGED
|
@@ -22,20 +22,28 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent is powerful out of the box. But configuring providers, themes, extensions, skills, and prompts by hand is tedious. oh-pi gives you a modern TUI that does it all in under a minute — and ships an **ant colony swarm** that turns pi into a multi-agent system.
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
25
|
+
## 30-Second Start
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # configure everything
|
|
33
29
|
pi # start coding
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
oh-pi auto-detects your environment, guides setup in a modern TUI, and writes `~/.pi/agent/` for you.
|
|
33
|
+
|
|
34
|
+
Already configured? It detects existing files and offers **backup before overwriting**.
|
|
35
|
+
|
|
36
|
+
## 2-Minute Value
|
|
37
|
+
|
|
38
|
+
pi-coding-agent is powerful by default, but manual setup across providers, themes, extensions, skills, and prompts is slow. oh-pi compresses that setup into under a minute — then adds an **ant colony swarm** for multi-agent execution.
|
|
39
|
+
|
|
40
|
+
Want the fast walkthrough? See [`docs/DEMO-SCRIPT.md`](./docs/DEMO-SCRIPT.md).
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
## When Not to Use Ant Colony
|
|
43
|
+
|
|
44
|
+
Use plain pi workflows (without colony) when your task is tiny, highly exploratory, or needs constant human steering.
|
|
45
|
+
|
|
46
|
+
For positioning, scope, and milestones, see [`ROADMAP.md`](./ROADMAP.md). For rationale behind key trade-offs, see [`DECISIONS.md`](./DECISIONS.md).
|
|
39
47
|
|
|
40
48
|
## What You Get
|
|
41
49
|
|
|
@@ -96,6 +104,16 @@ oh-pi:
|
|
|
96
104
|
✅ Done — report auto-injected into conversation
|
|
97
105
|
```
|
|
98
106
|
|
|
107
|
+
### What's new in v0.1.75
|
|
108
|
+
|
|
109
|
+
- **Planning Recovery Loop**: if scouts return unstructured intel, colony enters `planning_recovery` instead of failing immediately.
|
|
110
|
+
- **Plan Validation Gate**: before workers start, tasks are validated (title/description/caste/priority).
|
|
111
|
+
- **Scout Quorum for complex goals**: multi-step goals default to at least 2 scouts for better planning reliability.
|
|
112
|
+
|
|
113
|
+
### Colony lifecycle (simple)
|
|
114
|
+
|
|
115
|
+
`SCOUTING → (if needed) PLANNING_RECOVERY → WORKING → REVIEWING → DONE`
|
|
116
|
+
|
|
99
117
|
### Architecture
|
|
100
118
|
|
|
101
119
|
Each ant is an in-process `AgentSession` (pi SDK), not a child process:
|
|
@@ -139,14 +157,19 @@ Use `/colony-stop` to abort a running colony.
|
|
|
139
157
|
|
|
140
158
|
### Signal Protocol
|
|
141
159
|
|
|
142
|
-
The colony communicates with the main conversation via structured signals,
|
|
160
|
+
The colony communicates with the main conversation via structured signals, so the model never has to guess background state. Updates are passively pushed (non-blocking), so polling is optional:
|
|
143
161
|
|
|
144
162
|
| Signal | Meaning |
|
|
145
163
|
|--------|---------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` | Colony started
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
164
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colony started in background |
|
|
165
|
+
| `COLONY_SIGNAL:SCOUTING` | Scout wave is exploring / planning |
|
|
166
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | Plan recovery loop is restructuring tasks |
|
|
167
|
+
| `COLONY_SIGNAL:WORKING` | Worker phase is executing tasks |
|
|
168
|
+
| `COLONY_SIGNAL:REVIEWING` | Soldier review phase is active |
|
|
169
|
+
| `COLONY_SIGNAL:TASK_DONE` | A task finished (progress checkpoint) |
|
|
170
|
+
| `COLONY_SIGNAL:COMPLETE` | Colony finished and report injected |
|
|
171
|
+
| `COLONY_SIGNAL:FAILED` | Colony failed with diagnostics |
|
|
172
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | Budget limit reached |
|
|
150
173
|
|
|
151
174
|
### Turn Control
|
|
152
175
|
|
package/README.zh.md
CHANGED
|
@@ -22,20 +22,34 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent 开箱即用很强大,但手动配置提供商、主题、扩展、技能和提示词模板很繁琐。oh-pi 提供现代化 TUI,一分钟内搞定一切 —— 还附带一个**蚁群系统**,把 pi 变成多智能体平台。
|
|
28
|
-
|
|
29
|
-
## 快速开始
|
|
25
|
+
## 30 秒上手
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # 配置一切
|
|
33
29
|
pi # 开始编码
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
就这样。oh-pi
|
|
32
|
+
就这样。oh-pi 会自动检测环境、引导配置,并写入 `~/.pi/agent/`。
|
|
33
|
+
|
|
34
|
+
已有配置?会先备份,再覆盖。
|
|
35
|
+
|
|
36
|
+
## 2 分钟看懂价值
|
|
37
|
+
|
|
38
|
+
oh-pi 把原本分散且手工的配置流程(提供商、主题、扩展、技能、提示词模板)整合成一次引导,通常 1 分钟内完成。
|
|
39
|
+
|
|
40
|
+
当任务涉及多文件或可并行流程时,可启用蚁群系统,把 pi 升级为可协作的多智能体执行流。
|
|
41
|
+
|
|
42
|
+
- [`docs/DEMO-SCRIPT.zh.md`](./docs/DEMO-SCRIPT.zh.md) — 2 分钟演示脚本(价值与节奏)
|
|
43
|
+
- [`ROADMAP.md`](./ROADMAP.md) — 定位、里程碑与衡量指标
|
|
44
|
+
- [`DECISIONS.md`](./DECISIONS.md) — 阶段性关键决策与取舍依据
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
## 何时不该用蚁群
|
|
47
|
+
|
|
48
|
+
以下场景建议**不要**启用蚁群,直接单代理更快:
|
|
49
|
+
|
|
50
|
+
- 只改 1 个文件、改动范围明确
|
|
51
|
+
- 快速问答、解释代码、一次性小修复
|
|
52
|
+
- 你需要严格串行控制每一步修改
|
|
39
53
|
|
|
40
54
|
## 你会得到
|
|
41
55
|
|
|
@@ -96,6 +110,16 @@ oh-pi:
|
|
|
96
110
|
✅ 完成 — 报告自动注入对话
|
|
97
111
|
```
|
|
98
112
|
|
|
113
|
+
### v0.1.75 更新亮点
|
|
114
|
+
|
|
115
|
+
- **Planning Recovery 回路**:当 scout 产出非结构化情报时,进入 `planning_recovery`,而不是直接失败。
|
|
116
|
+
- **执行前计划校验**:worker 启动前会校验任务字段完整性(title/description/caste/priority)。
|
|
117
|
+
- **复杂目标 Scout Quorum**:多步骤目标默认至少 2 个 scout,提升规划可靠性。
|
|
118
|
+
|
|
119
|
+
### 生命周期(简化)
|
|
120
|
+
|
|
121
|
+
`SCOUTING →(必要时)PLANNING_RECOVERY → WORKING → REVIEWING → DONE`
|
|
122
|
+
|
|
99
123
|
### 架构
|
|
100
124
|
|
|
101
125
|
每只蚂蚁是进程内的 `AgentSession`(pi SDK),而非子进程:
|
|
@@ -139,14 +163,19 @@ pi(主进程)
|
|
|
139
163
|
|
|
140
164
|
### 信号协议
|
|
141
165
|
|
|
142
|
-
|
|
166
|
+
蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态。更新采用被动推送(非阻塞),轮询仅在手动排障时需要:
|
|
143
167
|
|
|
144
168
|
| 信号 | 含义 |
|
|
145
169
|
|------|------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` |
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
170
|
+
| `COLONY_SIGNAL:LAUNCHED` | 蚁群已在后台启动 |
|
|
171
|
+
| `COLONY_SIGNAL:SCOUTING` | 侦察波次正在探索/规划 |
|
|
172
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | 计划恢复回路正在重组任务 |
|
|
173
|
+
| `COLONY_SIGNAL:WORKING` | 工蚁执行阶段进行中 |
|
|
174
|
+
| `COLONY_SIGNAL:REVIEWING` | 兵蚁审查阶段进行中 |
|
|
175
|
+
| `COLONY_SIGNAL:TASK_DONE` | 单个任务完成(进度检查点) |
|
|
176
|
+
| `COLONY_SIGNAL:COMPLETE` | 蚁群完成并注入报告 |
|
|
177
|
+
| `COLONY_SIGNAL:FAILED` | 蚁群失败并附带诊断信息 |
|
|
178
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | 达到预算上限 |
|
|
150
179
|
|
|
151
180
|
### 轮次控制
|
|
152
181
|
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ async function quickFlow(env) {
|
|
|
43
43
|
providers,
|
|
44
44
|
theme: "dark",
|
|
45
45
|
keybindings: "default",
|
|
46
|
-
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"
|
|
46
|
+
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
|
|
47
47
|
prompts: ["review", "fix", "explain", "commit", "test"],
|
|
48
48
|
agents: "general-developer",
|
|
49
49
|
thinking: "medium",
|
package/dist/registry.js
CHANGED
|
@@ -49,7 +49,6 @@ 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: "smart-compact", label: "🗜️ Smart Compact — Trim large tool outputs and old messages in-flight", default: true },
|
|
53
52
|
{ name: "bg-process", label: "⏳ Bg Process — Auto-background long-running commands (dev servers, etc.)", default: false },
|
|
54
53
|
];
|
|
55
54
|
/** 快捷键绑定方案(default / vim / emacs) */
|
package/package.json
CHANGED
|
@@ -17,8 +17,9 @@ You command an autonomous ant colony. Complex tasks are delegated to the swarm,
|
|
|
17
17
|
## Workflow
|
|
18
18
|
1. Assess task scope
|
|
19
19
|
2. If colony-worthy → use `ant_colony` tool with clear goal
|
|
20
|
-
3.
|
|
21
|
-
4.
|
|
20
|
+
3. After launch, use passive mode: wait for `COLONY_SIGNAL:*` updates; do not poll `bg_colony_status` unless user explicitly asks
|
|
21
|
+
4. If simple → do it directly
|
|
22
|
+
5. Review colony output, fix gaps manually if needed
|
|
22
23
|
|
|
23
24
|
## Code Standards
|
|
24
25
|
- Follow existing conventions
|
|
@@ -18,7 +18,7 @@ import { runColony, resumeColony, type QueenCallbacks } from "./queen.js";
|
|
|
18
18
|
import { Nest } from "./nest.js";
|
|
19
19
|
import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
|
|
20
20
|
|
|
21
|
-
import { formatDuration, formatCost, formatTokens, statusIcon, casteIcon, buildReport } from "./ui.js";
|
|
21
|
+
import { formatDuration, formatCost, formatTokens, statusIcon, statusLabel, progressBar, casteIcon, buildReport } from "./ui.js";
|
|
22
22
|
|
|
23
23
|
// ═══ Background colony state ═══
|
|
24
24
|
|
|
@@ -38,12 +38,19 @@ interface AntStreamState {
|
|
|
38
38
|
tokens: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface ColonyLogEntry {
|
|
42
|
+
timestamp: number;
|
|
43
|
+
level: "info" | "warning" | "error";
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
interface BackgroundColony {
|
|
42
48
|
goal: string;
|
|
43
49
|
abortController: AbortController;
|
|
44
50
|
state: ColonyState | null;
|
|
45
51
|
phase: string;
|
|
46
52
|
antStreams: Map<string, AntStreamState>;
|
|
53
|
+
logs: ColonyLogEntry[];
|
|
47
54
|
promise: Promise<ColonyState>;
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -52,6 +59,56 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
52
59
|
// 当前运行中的后台蚁群(同时只允许一个)
|
|
53
60
|
let activeColony: BackgroundColony | null = null;
|
|
54
61
|
|
|
62
|
+
// 防止主进程主动轮询导致阻塞:仅允许显式请求的手动快照,并加冷却
|
|
63
|
+
let lastBgStatusSnapshotAt = 0;
|
|
64
|
+
const STATUS_SNAPSHOT_COOLDOWN_MS = 15_000;
|
|
65
|
+
|
|
66
|
+
const extractMessageText = (message: any): string => {
|
|
67
|
+
const c = message?.content;
|
|
68
|
+
if (typeof c === "string") return c;
|
|
69
|
+
if (Array.isArray(c)) {
|
|
70
|
+
return c.map((p: any) => {
|
|
71
|
+
if (typeof p === "string") return p;
|
|
72
|
+
if (typeof p?.text === "string") return p.text;
|
|
73
|
+
if (typeof p?.content === "string") return p.content;
|
|
74
|
+
return "";
|
|
75
|
+
}).join("\n");
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const lastUserMessageText = (ctx: any): string => {
|
|
81
|
+
try {
|
|
82
|
+
const branch = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
83
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
84
|
+
const e = branch[i];
|
|
85
|
+
if (e?.type === "message" && e.message?.role === "user") {
|
|
86
|
+
return extractMessageText(e.message).trim();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const isExplicitStatusRequest = (ctx: any): boolean => {
|
|
96
|
+
const text = lastUserMessageText(ctx);
|
|
97
|
+
return /(?:\/colony-status|bg_colony_status)|(?:(?:蚁群|colony).{0,20}(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check))|(?:(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check).{0,20}(?:蚁群|colony))/i.test(text);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const calcProgress = (m?: ColonyMetrics | null) => {
|
|
101
|
+
if (!m || m.tasksTotal <= 0) return 0;
|
|
102
|
+
return Math.max(0, Math.min(1, m.tasksDone / m.tasksTotal));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const trim = (text: string, max: number) => text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
106
|
+
|
|
107
|
+
const pushLog = (colony: BackgroundColony, entry: Omit<ColonyLogEntry, "timestamp">) => {
|
|
108
|
+
colony.logs.push({ timestamp: Date.now(), ...entry });
|
|
109
|
+
if (colony.logs.length > 40) colony.logs.splice(0, colony.logs.length - 40);
|
|
110
|
+
};
|
|
111
|
+
|
|
55
112
|
// ─── Status 渲染 ───
|
|
56
113
|
|
|
57
114
|
let lastRender = 0;
|
|
@@ -78,10 +135,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
78
135
|
const { state } = activeColony;
|
|
79
136
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
80
137
|
const m = state?.metrics;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
138
|
+
const phase = state?.status || "scouting";
|
|
139
|
+
const progress = calcProgress(m);
|
|
140
|
+
const pct = `${Math.round(progress * 100)}%`;
|
|
141
|
+
const active = activeColony.antStreams.size;
|
|
142
|
+
|
|
143
|
+
const parts = [`🐜 ${statusIcon(phase)} ${statusLabel(phase)}`];
|
|
144
|
+
parts.push(m ? `${m.tasksDone}/${m.tasksTotal} (${pct})` : `0/0 (${pct})`);
|
|
145
|
+
parts.push(`⚡${active}`);
|
|
85
146
|
if (m) parts.push(formatCost(m.totalCost));
|
|
86
147
|
parts.push(elapsed);
|
|
87
148
|
|
|
@@ -162,9 +223,12 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
162
223
|
state: null,
|
|
163
224
|
phase: "initializing",
|
|
164
225
|
antStreams: new Map(),
|
|
226
|
+
logs: [],
|
|
165
227
|
promise: null as any, // set below
|
|
166
228
|
};
|
|
167
229
|
|
|
230
|
+
pushLog(colony, { level: "info", text: "INITIALIZING · Colony launched in background" });
|
|
231
|
+
|
|
168
232
|
let lastPhase = "";
|
|
169
233
|
|
|
170
234
|
const callbacks: QueenCallbacks = {
|
|
@@ -174,6 +238,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
174
238
|
if (signal.phase !== lastPhase) {
|
|
175
239
|
lastPhase = signal.phase;
|
|
176
240
|
const pct = Math.round(signal.progress * 100);
|
|
241
|
+
pushLog(colony, { level: "info", text: `${statusLabel(signal.phase)} ${pct}% · ${signal.message}` });
|
|
177
242
|
pi.sendMessage({
|
|
178
243
|
customType: "ant-colony-progress",
|
|
179
244
|
content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] 🐜 ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
|
|
@@ -184,6 +249,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
184
249
|
},
|
|
185
250
|
onPhase(phase, detail) {
|
|
186
251
|
colony.phase = detail;
|
|
252
|
+
pushLog(colony, { level: "info", text: `${statusLabel(phase)} · ${detail}` });
|
|
187
253
|
throttledRender();
|
|
188
254
|
},
|
|
189
255
|
onAntSpawn(ant, task) {
|
|
@@ -199,6 +265,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
199
265
|
const icon = ant.status === "done" ? "✓" : "✗";
|
|
200
266
|
const progress = m ? `${m.tasksDone}/${m.tasksTotal}` : "";
|
|
201
267
|
const cost = m ? formatCost(m.totalCost) : "";
|
|
268
|
+
pushLog(colony, {
|
|
269
|
+
level: ant.status === "done" ? "info" : "warning",
|
|
270
|
+
text: `${icon} ${task.title.slice(0, 80)} (${progress}${cost ? `, ${cost}` : ""})`,
|
|
271
|
+
});
|
|
202
272
|
pi.sendMessage({
|
|
203
273
|
customType: "ant-colony-progress",
|
|
204
274
|
content: `[COLONY_SIGNAL:TASK_DONE] 🐜 ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
|
|
@@ -221,6 +291,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
221
291
|
onComplete(state) {
|
|
222
292
|
colony.state = state;
|
|
223
293
|
colony.phase = state.status === "done" ? "Colony mission complete" : "Colony failed";
|
|
294
|
+
pushLog(colony, {
|
|
295
|
+
level: state.status === "done" ? "info" : "error",
|
|
296
|
+
text: `${statusLabel(state.status)} · ${state.metrics.tasksDone}/${state.metrics.tasksTotal} · ${formatCost(state.metrics.totalCost)}`,
|
|
297
|
+
});
|
|
224
298
|
colony.antStreams.clear();
|
|
225
299
|
throttledRender();
|
|
226
300
|
},
|
|
@@ -244,6 +318,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
244
318
|
colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
|
|
245
319
|
|
|
246
320
|
activeColony = colony;
|
|
321
|
+
lastBgStatusSnapshotAt = 0;
|
|
247
322
|
throttledRender();
|
|
248
323
|
|
|
249
324
|
// 后台等待完成,注入结果
|
|
@@ -251,6 +326,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
251
326
|
const ok = state.status === "done";
|
|
252
327
|
const report = buildReport(state);
|
|
253
328
|
const m = state.metrics;
|
|
329
|
+
pushLog(colony, {
|
|
330
|
+
level: ok ? "info" : "error",
|
|
331
|
+
text: `${ok ? "COMPLETE" : "FAILED"} · ${m.tasksDone}/${m.tasksTotal} · ${formatCost(m.totalCost)}`,
|
|
332
|
+
});
|
|
254
333
|
|
|
255
334
|
// 清理 UI
|
|
256
335
|
pi.events.emit("ant-colony:clear-ui");
|
|
@@ -268,6 +347,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
268
347
|
level: ok ? "success" : "error",
|
|
269
348
|
});
|
|
270
349
|
}).catch((e) => {
|
|
350
|
+
pushLog(colony, { level: "error", text: `CRASHED · ${String(e).slice(0, 120)}` });
|
|
271
351
|
pi.events.emit("ant-colony:clear-ui");
|
|
272
352
|
activeColony = null;
|
|
273
353
|
pi.events.emit("ant-colony:notify", { msg: `🐜 Colony crashed: ${e}`, level: "error" });
|
|
@@ -283,6 +363,29 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
283
363
|
|
|
284
364
|
|
|
285
365
|
|
|
366
|
+
// ═══ Custom message renderer for colony progress signals ═══
|
|
367
|
+
pi.registerMessageRenderer("ant-colony-progress", (message, theme) => {
|
|
368
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
369
|
+
const line = content.split("\n")[0] || content;
|
|
370
|
+
const phaseMatch = line.match(/\[COLONY_SIGNAL:([A-Z_]+)\]/);
|
|
371
|
+
const text = line.replace(/\[COLONY_SIGNAL:[A-Z_]+\]\s*/, "").trim();
|
|
372
|
+
|
|
373
|
+
const phase = phaseMatch?.[1]?.toLowerCase() || "working";
|
|
374
|
+
const icon = statusIcon(phase);
|
|
375
|
+
const label = statusLabel(phase);
|
|
376
|
+
|
|
377
|
+
const body = trim(text, 120);
|
|
378
|
+
const coloredBody = phase === "failed"
|
|
379
|
+
? theme.fg("error", body)
|
|
380
|
+
: phase === "budget_exceeded"
|
|
381
|
+
? theme.fg("warning", body)
|
|
382
|
+
: phase === "done" || phase === "complete"
|
|
383
|
+
? theme.fg("success", body)
|
|
384
|
+
: theme.fg("muted", body);
|
|
385
|
+
|
|
386
|
+
return new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))} ${coloredBody}`, 0, 0);
|
|
387
|
+
});
|
|
388
|
+
|
|
286
389
|
// ═══ Custom message renderer for colony reports ═══
|
|
287
390
|
pi.registerMessageRenderer("ant-colony-report", (message, theme) => {
|
|
288
391
|
const content = typeof message.content === "string" ? message.content : "";
|
|
@@ -304,7 +407,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
304
407
|
const taskLines = content.split("\n").filter(l => l.startsWith("- ✓") || l.startsWith("- ✗"));
|
|
305
408
|
for (const l of taskLines.slice(0, 8)) {
|
|
306
409
|
const icon = l.startsWith("- ✓") ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
307
|
-
container.addChild(new Text(` ${icon} ${theme.fg("
|
|
410
|
+
container.addChild(new Text(` ${icon} ${theme.fg("muted", l.slice(4).trim().slice(0, 70))}`, 0, 0));
|
|
308
411
|
}
|
|
309
412
|
if (taskLines.length > 8) {
|
|
310
413
|
container.addChild(new Text(theme.fg("muted", ` ⋯ +${taskLines.length - 8} more`), 0, 0));
|
|
@@ -331,6 +434,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
331
434
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
332
435
|
let cachedWidth: number | undefined;
|
|
333
436
|
let cachedLines: string[] | undefined;
|
|
437
|
+
let currentTab: "tasks" | "streams" | "log" = "tasks";
|
|
438
|
+
let taskFilter: "all" | "active" | "done" | "failed" = "all";
|
|
334
439
|
|
|
335
440
|
const buildLines = (width: number): string[] => {
|
|
336
441
|
const c = activeColony;
|
|
@@ -341,36 +446,121 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
341
446
|
|
|
342
447
|
// ── Header ──
|
|
343
448
|
const elapsed = c.state ? formatDuration(Date.now() - c.state.createdAt) : "0s";
|
|
344
|
-
const
|
|
449
|
+
const m = c.state?.metrics;
|
|
450
|
+
const phase = c.state?.status || "scouting";
|
|
451
|
+
const progress = calcProgress(m);
|
|
452
|
+
const pct = Math.round(progress * 100);
|
|
453
|
+
const cost = m ? formatCost(m.totalCost) : "$0";
|
|
454
|
+
const activeAnts = c.antStreams.size;
|
|
455
|
+
const barWidth = Math.max(10, Math.min(24, w - 28));
|
|
456
|
+
|
|
345
457
|
lines.push(theme.fg("accent", theme.bold(` 🐜 Colony Details`)) + theme.fg("muted", ` │ ${elapsed} │ ${cost}`));
|
|
346
|
-
lines.push(theme.fg("
|
|
458
|
+
lines.push(theme.fg("muted", ` Goal: ${trim(c.goal, w - 8)}`));
|
|
459
|
+
lines.push(` ${statusIcon(phase)} ${theme.bold(statusLabel(phase))} │ ${m ? `${m.tasksDone}/${m.tasksTotal}` : "0/0"} │ ${pct}% │ ⚡${activeAnts}`);
|
|
460
|
+
lines.push(theme.fg("muted", ` ${progressBar(progress, barWidth)} ${pct}%`));
|
|
461
|
+
if (c.phase && c.phase !== "initializing") {
|
|
462
|
+
lines.push(theme.fg("muted", ` Phase: ${trim(c.phase, w - 10)}`));
|
|
463
|
+
}
|
|
464
|
+
lines.push("");
|
|
465
|
+
|
|
466
|
+
// ── Tabs ──
|
|
467
|
+
const tabs: Array<{ key: "tasks" | "streams" | "log"; hotkey: string; label: string }> = [
|
|
468
|
+
{ key: "tasks", hotkey: "1", label: "Tasks" },
|
|
469
|
+
{ key: "streams", hotkey: "2", label: "Streams" },
|
|
470
|
+
{ key: "log", hotkey: "3", label: "Log" },
|
|
471
|
+
];
|
|
472
|
+
const tabLine = tabs.map((t) => {
|
|
473
|
+
const label = `[${t.hotkey}] ${t.label}`;
|
|
474
|
+
return currentTab === t.key ? theme.fg("accent", theme.bold(label)) : theme.fg("muted", label);
|
|
475
|
+
}).join(" ");
|
|
476
|
+
lines.push(` ${tabLine}`);
|
|
347
477
|
lines.push("");
|
|
348
478
|
|
|
349
|
-
// ── Tasks ──
|
|
350
479
|
const tasks = c.state?.tasks || [];
|
|
351
|
-
|
|
480
|
+
const streams = Array.from(c.antStreams.values());
|
|
481
|
+
|
|
482
|
+
// ── Tab: Tasks ──
|
|
483
|
+
if (currentTab === "tasks") {
|
|
484
|
+
const counts = {
|
|
485
|
+
done: tasks.filter(t => t.status === "done").length,
|
|
486
|
+
active: tasks.filter(t => t.status === "active").length,
|
|
487
|
+
failed: tasks.filter(t => t.status === "failed").length,
|
|
488
|
+
pending: tasks.filter(t => t.status === "pending" || t.status === "claimed" || t.status === "blocked").length,
|
|
489
|
+
};
|
|
352
490
|
lines.push(theme.fg("accent", " Tasks"));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
491
|
+
lines.push(theme.fg("muted", ` done:${counts.done} │ active:${counts.active} │ pending:${counts.pending} │ failed:${counts.failed}`));
|
|
492
|
+
lines.push(theme.fg("muted", " Filter: [0] all [a] active [d] done [f] failed"));
|
|
493
|
+
lines.push(theme.fg("muted", ` Current filter: ${taskFilter.toUpperCase()}`));
|
|
494
|
+
lines.push("");
|
|
495
|
+
|
|
496
|
+
const filtered = tasks.filter(t =>
|
|
497
|
+
taskFilter === "all" ? true :
|
|
498
|
+
taskFilter === "active" ? t.status === "active" :
|
|
499
|
+
taskFilter === "done" ? t.status === "done" :
|
|
500
|
+
t.status === "failed"
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (filtered.length === 0) {
|
|
504
|
+
lines.push(theme.fg("muted", " (no tasks match current filter)"));
|
|
505
|
+
} else {
|
|
506
|
+
for (const t of filtered.slice(0, 16)) {
|
|
507
|
+
const icon = t.status === "done" ? theme.fg("success", "✓")
|
|
508
|
+
: t.status === "failed" ? theme.fg("error", "✗")
|
|
509
|
+
: t.status === "active" ? theme.fg("warning", "●")
|
|
510
|
+
: theme.fg("dim", "○");
|
|
511
|
+
const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
|
|
512
|
+
lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", trim(t.title, w - 12))}${dur}`);
|
|
513
|
+
}
|
|
514
|
+
if (filtered.length > 16) lines.push(theme.fg("muted", ` ⋯ +${filtered.length - 16} more`));
|
|
360
515
|
}
|
|
361
|
-
if (tasks.length > 15) lines.push(theme.fg("muted", ` ⋯ +${tasks.length - 15} more`));
|
|
362
516
|
lines.push("");
|
|
363
517
|
}
|
|
364
518
|
|
|
365
|
-
// ──
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
lines.push(theme.fg("
|
|
519
|
+
// ── Tab: Streams ──
|
|
520
|
+
if (currentTab === "streams") {
|
|
521
|
+
lines.push(theme.fg("accent", ` Active Ant Streams (${streams.length})`));
|
|
522
|
+
lines.push(theme.fg("muted", " Shows latest line + token count for active ants"));
|
|
523
|
+
lines.push("");
|
|
524
|
+
if (streams.length === 0) {
|
|
525
|
+
lines.push(theme.fg("muted", " (no active streams right now)"));
|
|
526
|
+
} else {
|
|
527
|
+
for (const s of streams.slice(0, 10)) {
|
|
528
|
+
const excerpt = trim((s.lastLine || "...").replace(/\s+/g, " "), Math.max(20, w - 24));
|
|
529
|
+
lines.push(` ${casteIcon(s.caste)} ${theme.fg("muted", s.antId.slice(0, 12))} ${theme.fg("muted", `${formatTokens(s.tokens)}t`)} ${theme.fg("text", excerpt)}`);
|
|
530
|
+
}
|
|
531
|
+
if (streams.length > 10) lines.push(theme.fg("muted", ` ⋯ +${streams.length - 10} more streams`));
|
|
532
|
+
}
|
|
369
533
|
lines.push("");
|
|
370
534
|
}
|
|
371
535
|
|
|
372
|
-
|
|
373
|
-
|
|
536
|
+
// ── Tab: Log ──
|
|
537
|
+
if (currentTab === "log") {
|
|
538
|
+
const failedTasks = tasks.filter(t => t.status === "failed");
|
|
539
|
+
if (failedTasks.length > 0) {
|
|
540
|
+
lines.push(theme.fg("warning", ` Warnings (${failedTasks.length})`));
|
|
541
|
+
for (const t of failedTasks.slice(0, 4)) {
|
|
542
|
+
lines.push(` ${theme.fg("error", "✗")} ${theme.fg("text", trim(t.title, w - 8))}`);
|
|
543
|
+
}
|
|
544
|
+
if (failedTasks.length > 4) lines.push(theme.fg("muted", ` ⋯ +${failedTasks.length - 4} more failed tasks`));
|
|
545
|
+
lines.push("");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const recentLogs = c.logs.slice(-12);
|
|
549
|
+
lines.push(theme.fg("accent", " Recent Signals"));
|
|
550
|
+
if (recentLogs.length === 0) {
|
|
551
|
+
lines.push(theme.fg("muted", " (no signal logs yet)"));
|
|
552
|
+
} else {
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
for (const log of recentLogs) {
|
|
555
|
+
const age = formatDuration(Math.max(0, now - log.timestamp));
|
|
556
|
+
const levelIcon = log.level === "error" ? theme.fg("error", "✗") : log.level === "warning" ? theme.fg("warning", "!") : theme.fg("muted", "•");
|
|
557
|
+
lines.push(` ${levelIcon} ${theme.fg("muted", age)} ${theme.fg("text", trim(log.text, w - 12))}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
lines.push("");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
lines.push(theme.fg("muted", " [1/2/3] switch tabs │ [0/a/d/f] task filter │ esc close"));
|
|
374
564
|
return lines;
|
|
375
565
|
};
|
|
376
566
|
|
|
@@ -395,7 +585,21 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
395
585
|
if (matchesKey(data, "escape")) {
|
|
396
586
|
cleanup();
|
|
397
587
|
done(undefined);
|
|
588
|
+
return;
|
|
398
589
|
}
|
|
590
|
+
|
|
591
|
+
if (data === "1") currentTab = "tasks";
|
|
592
|
+
else if (data === "2") currentTab = "streams";
|
|
593
|
+
else if (data === "3") currentTab = "log";
|
|
594
|
+
else if (data === "0") taskFilter = "all";
|
|
595
|
+
else if (data.toLowerCase() === "a") taskFilter = "active";
|
|
596
|
+
else if (data.toLowerCase() === "d") taskFilter = "done";
|
|
597
|
+
else if (data.toLowerCase() === "f") taskFilter = "failed";
|
|
598
|
+
else return;
|
|
599
|
+
|
|
600
|
+
cachedWidth = undefined;
|
|
601
|
+
cachedLines = undefined;
|
|
602
|
+
tui.requestRender();
|
|
399
603
|
},
|
|
400
604
|
};
|
|
401
605
|
}, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
|
|
@@ -462,7 +666,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
462
666
|
launchBackgroundColony(colonyParams);
|
|
463
667
|
|
|
464
668
|
return {
|
|
465
|
-
content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony is
|
|
669
|
+
content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.` }],
|
|
466
670
|
};
|
|
467
671
|
},
|
|
468
672
|
|
|
@@ -471,7 +675,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
471
675
|
let text = theme.fg("toolTitle", theme.bold("🐜 ant_colony"));
|
|
472
676
|
if (args.maxAnts) text += theme.fg("muted", ` ×${args.maxAnts}`);
|
|
473
677
|
if (args.maxCost) text += theme.fg("warning", ` $${args.maxCost}`);
|
|
474
|
-
text += "\n" + theme.fg("
|
|
678
|
+
text += "\n" + theme.fg("muted", ` ${goal || "..."}`);
|
|
475
679
|
return new Text(text, 0, 0);
|
|
476
680
|
},
|
|
477
681
|
|
|
@@ -486,7 +690,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
486
690
|
0, 0,
|
|
487
691
|
));
|
|
488
692
|
if (activeColony) {
|
|
489
|
-
container.addChild(new Text(theme.fg("
|
|
693
|
+
container.addChild(new Text(theme.fg("muted", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
|
|
490
694
|
container.addChild(new Text(theme.fg("muted", ` Ctrl+Shift+A for details │ /colony-stop to cancel`), 0, 0));
|
|
491
695
|
}
|
|
492
696
|
return container;
|
|
@@ -502,12 +706,19 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
502
706
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
503
707
|
const m = state?.metrics;
|
|
504
708
|
const phase = state?.status || "scouting";
|
|
709
|
+
const progress = calcProgress(m);
|
|
710
|
+
const pct = Math.round(progress * 100);
|
|
711
|
+
const activeAnts = c.antStreams.size;
|
|
505
712
|
|
|
506
713
|
const lines: string[] = [
|
|
507
|
-
`🐜 ${statusIcon(phase)} ${c.goal
|
|
508
|
-
`${phase} │ ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
|
|
714
|
+
`🐜 ${statusIcon(phase)} ${trim(c.goal, 80)}`,
|
|
715
|
+
`${statusLabel(phase)} │ ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${pct}% │ ⚡${activeAnts} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
|
|
716
|
+
`${progressBar(progress, 18)} ${pct}%`,
|
|
509
717
|
];
|
|
510
718
|
|
|
719
|
+
if (c.phase && c.phase !== "initializing") lines.push(`Phase: ${trim(c.phase, 100)}`);
|
|
720
|
+
const lastLog = c.logs[c.logs.length - 1];
|
|
721
|
+
if (lastLog) lines.push(`Last: ${trim(lastLog.text, 100)}`);
|
|
511
722
|
if (m && m.tasksFailed > 0) lines.push(`⚠ ${m.tasksFailed} failed`);
|
|
512
723
|
|
|
513
724
|
return lines.join("\n");
|
|
@@ -517,9 +728,40 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
517
728
|
pi.registerTool({
|
|
518
729
|
name: "bg_colony_status",
|
|
519
730
|
label: "Colony Status",
|
|
520
|
-
description: "
|
|
731
|
+
description: "Optional manual snapshot for a running colony. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
|
|
521
732
|
parameters: Type.Object({}),
|
|
522
|
-
async execute() {
|
|
733
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
734
|
+
if (!activeColony) {
|
|
735
|
+
return {
|
|
736
|
+
content: [{ type: "text" as const, text: "No colony is currently running." }],
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const explicit = isExplicitStatusRequest(ctx);
|
|
741
|
+
if (!explicit) {
|
|
742
|
+
return {
|
|
743
|
+
content: [{
|
|
744
|
+
type: "text" as const,
|
|
745
|
+
text: "Passive mode is active. Colony progress is already pushed via [COLONY_SIGNAL:*] follow-up messages. Skipping bg_colony_status polling to avoid blocking the main process. Ask explicitly for a manual snapshot if needed.",
|
|
746
|
+
}],
|
|
747
|
+
isError: true,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
const delta = now - lastBgStatusSnapshotAt;
|
|
753
|
+
if (delta < STATUS_SNAPSHOT_COOLDOWN_MS) {
|
|
754
|
+
const waitSec = Math.ceil((STATUS_SNAPSHOT_COOLDOWN_MS - delta) / 1000);
|
|
755
|
+
return {
|
|
756
|
+
content: [{
|
|
757
|
+
type: "text" as const,
|
|
758
|
+
text: `Manual status snapshot is rate-limited. Please wait ${waitSec}s to avoid active polling loops.`,
|
|
759
|
+
}],
|
|
760
|
+
isError: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
lastBgStatusSnapshotAt = now;
|
|
523
765
|
return {
|
|
524
766
|
content: [{ type: "text" as const, text: buildStatusText() }],
|
|
525
767
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatDuration, formatCost, formatTokens, statusIcon, casteIcon, buildReport } from "./ui.js";
|
|
2
|
+
import { formatDuration, formatCost, formatTokens, statusIcon, statusLabel, progressBar, casteIcon, buildReport } from "./ui.js";
|
|
3
3
|
import type { ColonyState } from "./types.js";
|
|
4
4
|
|
|
5
5
|
describe("formatDuration", () => {
|
|
@@ -25,16 +25,33 @@ describe("formatTokens", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe("statusIcon", () => {
|
|
28
|
+
it("launched", () => expect(statusIcon("launched")).toBe("🚀"));
|
|
28
29
|
it("scouting", () => expect(statusIcon("scouting")).toBe("🔍"));
|
|
29
30
|
it("working", () => expect(statusIcon("working")).toBe("⚒️"));
|
|
30
31
|
it("planning_recovery", () => expect(statusIcon("planning_recovery")).toBe("♻️"));
|
|
31
32
|
it("reviewing", () => expect(statusIcon("reviewing")).toBe("🛡️"));
|
|
33
|
+
it("task_done", () => expect(statusIcon("task_done")).toBe("✅"));
|
|
32
34
|
it("done", () => expect(statusIcon("done")).toBe("✅"));
|
|
33
35
|
it("failed", () => expect(statusIcon("failed")).toBe("❌"));
|
|
34
36
|
it("budget_exceeded", () => expect(statusIcon("budget_exceeded")).toBe("💰"));
|
|
35
37
|
it("unknown", () => expect(statusIcon("xyz")).toBe("🐜"));
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
describe("statusLabel", () => {
|
|
41
|
+
it("launched", () => expect(statusLabel("launched")).toBe("LAUNCHED"));
|
|
42
|
+
it("scouting", () => expect(statusLabel("scouting")).toBe("SCOUTING"));
|
|
43
|
+
it("planning_recovery", () => expect(statusLabel("planning_recovery")).toBe("PLANNING_RECOVERY"));
|
|
44
|
+
it("task_done", () => expect(statusLabel("task_done")).toBe("TASK_DONE"));
|
|
45
|
+
it("budget_exceeded", () => expect(statusLabel("budget_exceeded")).toBe("BUDGET_EXCEEDED"));
|
|
46
|
+
it("unknown", () => expect(statusLabel("custom")).toBe("CUSTOM"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("progressBar", () => {
|
|
50
|
+
it("0%", () => expect(progressBar(0, 10)).toBe("[----------]"));
|
|
51
|
+
it("50%", () => expect(progressBar(0.5, 10)).toBe("[#####-----]"));
|
|
52
|
+
it("100%", () => expect(progressBar(1, 10)).toBe("[##########]"));
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
describe("casteIcon", () => {
|
|
39
56
|
it("scout", () => expect(casteIcon("scout")).toBe("🔍"));
|
|
40
57
|
it("soldier", () => expect(casteIcon("soldier")).toBe("🛡️"));
|
|
@@ -17,12 +17,42 @@ export function formatTokens(n: number): string {
|
|
|
17
17
|
|
|
18
18
|
export function statusIcon(status: string): string {
|
|
19
19
|
const icons: Record<string, string> = {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
launched: "🚀",
|
|
21
|
+
scouting: "🔍",
|
|
22
|
+
planning_recovery: "♻️",
|
|
23
|
+
working: "⚒️",
|
|
24
|
+
reviewing: "🛡️",
|
|
25
|
+
task_done: "✅",
|
|
26
|
+
done: "✅",
|
|
27
|
+
complete: "✅",
|
|
28
|
+
failed: "❌",
|
|
29
|
+
budget_exceeded: "💰",
|
|
22
30
|
};
|
|
23
31
|
return icons[status] || "🐜";
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
export function statusLabel(status: string): string {
|
|
35
|
+
const labels: Record<string, string> = {
|
|
36
|
+
launched: "LAUNCHED",
|
|
37
|
+
scouting: "SCOUTING",
|
|
38
|
+
planning_recovery: "PLANNING_RECOVERY",
|
|
39
|
+
working: "WORKING",
|
|
40
|
+
reviewing: "REVIEWING",
|
|
41
|
+
task_done: "TASK_DONE",
|
|
42
|
+
done: "DONE",
|
|
43
|
+
complete: "COMPLETE",
|
|
44
|
+
failed: "FAILED",
|
|
45
|
+
budget_exceeded: "BUDGET_EXCEEDED",
|
|
46
|
+
};
|
|
47
|
+
return labels[status] || status.toUpperCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function progressBar(progress: number, width = 14): string {
|
|
51
|
+
const p = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));
|
|
52
|
+
const filled = Math.round(width * p);
|
|
53
|
+
return `[${"#".repeat(filled)}${"-".repeat(Math.max(0, width - filled))}]`;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
export function casteIcon(caste: string): string {
|
|
27
57
|
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : caste === "drone" ? "⚙️" : "⚒️";
|
|
28
58
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { truncateText, compactContent } from "./smart-compact";
|
|
3
|
-
|
|
4
|
-
const longMultiline = (lines: number, lineLen = 100) =>
|
|
5
|
-
Array.from({ length: lines }, (_, i) => "x".repeat(lineLen) + i).join("\n");
|
|
6
|
-
|
|
7
|
-
describe("truncateText", () => {
|
|
8
|
-
it("short text returns unchanged", () => {
|
|
9
|
-
expect(truncateText("hello", 8000)).toBe("hello");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("few lines but long chars returns unchanged", () => {
|
|
13
|
-
const text = "x".repeat(9000) + "\n" + "y".repeat(9000);
|
|
14
|
-
expect(truncateText(text, 8000)).toBe(text);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("long text with many lines gets truncated", () => {
|
|
18
|
-
const text = longMultiline(200);
|
|
19
|
-
const result = truncateText(text, 8000);
|
|
20
|
-
expect(result).toContain("[...truncated");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("preserves head and tail", () => {
|
|
24
|
-
const text = longMultiline(200);
|
|
25
|
-
const result = truncateText(text, 8000);
|
|
26
|
-
expect(result.startsWith(text.slice(0, 1500))).toBe(true);
|
|
27
|
-
expect(result.endsWith(text.slice(-2500))).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("custom head/tail params work", () => {
|
|
31
|
-
const text = longMultiline(200);
|
|
32
|
-
const result = truncateText(text, 100, 50, 50);
|
|
33
|
-
expect(result).toContain("[...truncated");
|
|
34
|
-
expect(result.startsWith(text.slice(0, 50))).toBe(true);
|
|
35
|
-
expect(result.endsWith(text.slice(-50))).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("compactContent", () => {
|
|
40
|
-
it("short string returns unchanged", () => {
|
|
41
|
-
expect(compactContent("short")).toBe("short");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("long string gets truncated", () => {
|
|
45
|
-
const text = longMultiline(200);
|
|
46
|
-
expect(compactContent(text)).toContain("[...truncated");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("array text block gets truncated", () => {
|
|
50
|
-
const text = longMultiline(200);
|
|
51
|
-
const result = compactContent([{ type: "text", text }]);
|
|
52
|
-
expect(result[0].text).toContain("[...truncated");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("array non-text block returned unchanged", () => {
|
|
56
|
-
const block = { type: "image", url: "x" };
|
|
57
|
-
expect(compactContent([block])).toEqual([block]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("non-array non-string returned as-is", () => {
|
|
61
|
-
expect(compactContent(42)).toBe(42);
|
|
62
|
-
expect(compactContent({ a: 1 })).toEqual({ a: 1 });
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 智能压缩扩展 — 在发送给 LLM 前裁剪大块内容
|
|
3
|
-
*
|
|
4
|
-
* 策略:
|
|
5
|
-
* 1. 工具输出超过阈值 → 保留首尾,中间替换为 "[...truncated N lines]"
|
|
6
|
-
* 2. 用户粘贴的大块文本 → 同上
|
|
7
|
-
* 3. 越旧的消息裁剪越激进
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const MAX_TOOL_OUTPUT_CHARS = 8000;
|
|
11
|
-
const MAX_USER_BLOCK_CHARS = 12000;
|
|
12
|
-
const KEEP_HEAD = 1500;
|
|
13
|
-
const KEEP_TAIL = 2500;
|
|
14
|
-
|
|
15
|
-
export function truncateText(text: string, max: number, head = KEEP_HEAD, tail = KEEP_TAIL): string {
|
|
16
|
-
if (text.length <= max) return text;
|
|
17
|
-
const lines = text.split("\n");
|
|
18
|
-
if (lines.length <= 10) return text; // 短文本不裁
|
|
19
|
-
const headText = text.slice(0, head);
|
|
20
|
-
const tailText = text.slice(-tail);
|
|
21
|
-
const removedLines = text.slice(head, -tail).split("\n").length;
|
|
22
|
-
return `${headText}\n\n[...truncated ${removedLines} lines...]\n\n${tailText}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function compactContent(content: any): any {
|
|
26
|
-
if (typeof content === "string") {
|
|
27
|
-
return truncateText(content, MAX_TOOL_OUTPUT_CHARS);
|
|
28
|
-
}
|
|
29
|
-
if (!Array.isArray(content)) return content;
|
|
30
|
-
return content.map((block: any) => {
|
|
31
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
32
|
-
return { ...block, text: truncateText(block.text, MAX_TOOL_OUTPUT_CHARS) };
|
|
33
|
-
}
|
|
34
|
-
return block;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default function smartCompact(pi: any) {
|
|
39
|
-
pi.on("context", async (event: any) => {
|
|
40
|
-
const messages = event.messages;
|
|
41
|
-
if (!messages || messages.length < 4) return; // 太短不处理
|
|
42
|
-
|
|
43
|
-
// 只处理非最近 3 条消息(保留最近上下文完整)
|
|
44
|
-
const cutoff = messages.length - 3;
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < cutoff; i++) {
|
|
47
|
-
const msg = messages[i];
|
|
48
|
-
if (!msg) continue;
|
|
49
|
-
|
|
50
|
-
if (msg.role === "toolResult") {
|
|
51
|
-
msg.content = compactContent(msg.content);
|
|
52
|
-
} else if (msg.role === "user") {
|
|
53
|
-
// 用户消息用更宽松的阈值
|
|
54
|
-
if (typeof msg.content === "string" && msg.content.length > MAX_USER_BLOCK_CHARS) {
|
|
55
|
-
msg.content = truncateText(msg.content, MAX_USER_BLOCK_CHARS);
|
|
56
|
-
} else if (Array.isArray(msg.content)) {
|
|
57
|
-
msg.content = msg.content.map((block: any) => {
|
|
58
|
-
if (block.type === "text" && typeof block.text === "string" && block.text.length > MAX_USER_BLOCK_CHARS) {
|
|
59
|
-
return { ...block, text: truncateText(block.text, MAX_USER_BLOCK_CHARS) };
|
|
60
|
-
}
|
|
61
|
-
return block;
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
} else if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
65
|
-
// 裁剪 assistant 的大块工具调用参数
|
|
66
|
-
msg.content = msg.content.map((block: any) => {
|
|
67
|
-
if (block.type === "toolCall" && block.arguments) {
|
|
68
|
-
const args = JSON.stringify(block.arguments);
|
|
69
|
-
if (args.length > MAX_TOOL_OUTPUT_CHARS) {
|
|
70
|
-
try {
|
|
71
|
-
const parsed = typeof block.arguments === "string" ? JSON.parse(block.arguments) : block.arguments;
|
|
72
|
-
// 裁剪大字符串参数
|
|
73
|
-
for (const key of Object.keys(parsed)) {
|
|
74
|
-
if (typeof parsed[key] === "string" && parsed[key].length > MAX_TOOL_OUTPUT_CHARS) {
|
|
75
|
-
parsed[key] = truncateText(parsed[key], MAX_TOOL_OUTPUT_CHARS);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return { ...block, arguments: parsed };
|
|
79
|
-
} catch { return block; }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return block;
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { messages };
|
|
88
|
-
});
|
|
89
|
-
}
|