oh-pi 0.1.76 → 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.md +1 -1
- package/README.zh.md +1 -1
- 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 +73 -3
- package/pi-package/extensions/smart-compact.test.ts +0 -64
- package/pi-package/extensions/smart-compact.ts +0 -89
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ Use `/colony-stop` to abort a running colony.
|
|
|
157
157
|
|
|
158
158
|
### Signal Protocol
|
|
159
159
|
|
|
160
|
-
The colony communicates with the main conversation via structured signals, so the model never has to guess background state:
|
|
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:
|
|
161
161
|
|
|
162
162
|
| Signal | Meaning |
|
|
163
163
|
|--------|---------|
|
package/README.zh.md
CHANGED
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
|
|
@@ -59,6 +59,44 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
59
59
|
// 当前运行中的后台蚁群(同时只允许一个)
|
|
60
60
|
let activeColony: BackgroundColony | null = null;
|
|
61
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
|
+
|
|
62
100
|
const calcProgress = (m?: ColonyMetrics | null) => {
|
|
63
101
|
if (!m || m.tasksTotal <= 0) return 0;
|
|
64
102
|
return Math.max(0, Math.min(1, m.tasksDone / m.tasksTotal));
|
|
@@ -280,6 +318,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
280
318
|
colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
|
|
281
319
|
|
|
282
320
|
activeColony = colony;
|
|
321
|
+
lastBgStatusSnapshotAt = 0;
|
|
283
322
|
throttledRender();
|
|
284
323
|
|
|
285
324
|
// 后台等待完成,注入结果
|
|
@@ -627,7 +666,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
627
666
|
launchBackgroundColony(colonyParams);
|
|
628
667
|
|
|
629
668
|
return {
|
|
630
|
-
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.` }],
|
|
631
670
|
};
|
|
632
671
|
},
|
|
633
672
|
|
|
@@ -689,9 +728,40 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
689
728
|
pi.registerTool({
|
|
690
729
|
name: "bg_colony_status",
|
|
691
730
|
label: "Colony Status",
|
|
692
|
-
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.",
|
|
693
732
|
parameters: Type.Object({}),
|
|
694
|
-
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;
|
|
695
765
|
return {
|
|
696
766
|
content: [{ type: "text" as const, text: buildStatusText() }],
|
|
697
767
|
};
|
|
@@ -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
|
-
}
|