oh-pi 0.1.55 → 0.1.56
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/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 import graph — 静态分析 ts/js 文件的依赖关系
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
export interface ImportGraph {
|
|
8
|
+
/** file → files it imports */
|
|
9
|
+
imports: Map<string, Set<string>>;
|
|
10
|
+
/** file → files that import it */
|
|
11
|
+
importedBy: Map<string, Set<string>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 从 import/require 语句中提取相对路径 */
|
|
15
|
+
const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
|
|
16
|
+
const REQUIRE_RE = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
17
|
+
|
|
18
|
+
function resolveImport(from: string, specifier: string, cwd: string): string | null {
|
|
19
|
+
const dir = path.dirname(path.resolve(cwd, from));
|
|
20
|
+
const base = path.resolve(dir, specifier);
|
|
21
|
+
const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
|
|
22
|
+
for (const ext of exts) {
|
|
23
|
+
const full = base + ext;
|
|
24
|
+
if (fs.existsSync(full)) return path.relative(cwd, full);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 构建项目的 import graph
|
|
31
|
+
* @param files - 相对于 cwd 的文件路径列表
|
|
32
|
+
* @param cwd - 项目根目录
|
|
33
|
+
*/
|
|
34
|
+
export function buildImportGraph(files: string[], cwd: string): ImportGraph {
|
|
35
|
+
const imports = new Map<string, Set<string>>();
|
|
36
|
+
const importedBy = new Map<string, Set<string>>();
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const abs = path.resolve(cwd, file);
|
|
40
|
+
if (!fs.existsSync(abs)) continue;
|
|
41
|
+
let content: string;
|
|
42
|
+
try { content = fs.readFileSync(abs, "utf-8"); } catch { continue; }
|
|
43
|
+
|
|
44
|
+
const deps = new Set<string>();
|
|
45
|
+
for (const re of [IMPORT_RE, REQUIRE_RE]) {
|
|
46
|
+
re.lastIndex = 0;
|
|
47
|
+
for (const m of content.matchAll(re)) {
|
|
48
|
+
const resolved = resolveImport(file, m[1], cwd);
|
|
49
|
+
if (resolved) deps.add(resolved);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
imports.set(file, deps);
|
|
54
|
+
for (const dep of deps) {
|
|
55
|
+
if (!importedBy.has(dep)) importedBy.set(dep, new Set());
|
|
56
|
+
importedBy.get(dep)!.add(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { imports, importedBy };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 计算文件的依赖深度(被多少文件直接或间接依赖)
|
|
65
|
+
* 深度越高 = 越底层 = 应优先处理
|
|
66
|
+
*/
|
|
67
|
+
export function dependencyDepth(file: string, graph: ImportGraph): number {
|
|
68
|
+
const visited = new Set<string>();
|
|
69
|
+
const queue = [file];
|
|
70
|
+
while (queue.length > 0) {
|
|
71
|
+
const f = queue.pop()!;
|
|
72
|
+
if (visited.has(f)) continue;
|
|
73
|
+
visited.add(f);
|
|
74
|
+
const dependents = graph.importedBy.get(f);
|
|
75
|
+
if (dependents) for (const d of dependents) queue.push(d);
|
|
76
|
+
}
|
|
77
|
+
return visited.size - 1; // 不算自己
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 检查 taskA 的文件是否依赖 taskB 的文件
|
|
82
|
+
* 即 taskA 的某个文件 import 了 taskB 的某个文件
|
|
83
|
+
*/
|
|
84
|
+
export function taskDependsOn(taskAFiles: string[], taskBFiles: string[], graph: ImportGraph): boolean {
|
|
85
|
+
for (const a of taskAFiles) {
|
|
86
|
+
const deps = graph.imports.get(a);
|
|
87
|
+
if (deps) {
|
|
88
|
+
for (const b of taskBFiles) {
|
|
89
|
+
if (deps.has(b)) return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
@@ -22,6 +22,7 @@ import { DEFAULT_ANT_CONFIGS } from "./types.js";
|
|
|
22
22
|
import { Nest } from "./nest.js";
|
|
23
23
|
import { spawnAnt, runDrone, makeTaskId } from "./spawner.js";
|
|
24
24
|
import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
|
|
25
|
+
import { buildImportGraph, taskDependsOn, type ImportGraph } from "./deps.js";
|
|
25
26
|
import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
26
27
|
|
|
27
28
|
export interface QueenCallbacks {
|
|
@@ -149,6 +150,7 @@ interface WaveOptions {
|
|
|
149
150
|
callbacks: QueenCallbacks;
|
|
150
151
|
authStorage?: AuthStorage;
|
|
151
152
|
modelRegistry?: ModelRegistry;
|
|
153
|
+
importGraph?: ImportGraph;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/**
|
|
@@ -188,11 +190,13 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
188
190
|
|
|
189
191
|
// 蚂蚁产生的子任务加入巢穴
|
|
190
192
|
for (const sub of result.newTasks) {
|
|
191
|
-
//
|
|
193
|
+
// 检查文件锁冲突和依赖冲突
|
|
192
194
|
const allTasks = nest.getAllTasks();
|
|
193
195
|
const conflicting = allTasks.find(t =>
|
|
194
|
-
t.status === "active" &&
|
|
195
|
-
|
|
196
|
+
t.status === "active" && (
|
|
197
|
+
t.files.some(f => sub.files.includes(f)) ||
|
|
198
|
+
(opts.importGraph && taskDependsOn(sub.files, t.files, opts.importGraph))
|
|
199
|
+
)
|
|
196
200
|
);
|
|
197
201
|
const child = childTaskFromParsed(task.id, sub);
|
|
198
202
|
if (conflicting) {
|
|
@@ -230,11 +234,15 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
230
234
|
await new Promise(r => setTimeout(r, backoffMs));
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
// 解除 blocked
|
|
237
|
+
// 解除 blocked 任务(如果锁定文件和依赖文件都已释放)
|
|
234
238
|
const activeTasks = state.tasks.filter(t => t.status === "active");
|
|
235
239
|
const activeFiles = new Set(activeTasks.flatMap(t => t.files));
|
|
236
240
|
for (const t of state.tasks.filter(t => t.status === "blocked" && t.caste === caste)) {
|
|
237
|
-
|
|
241
|
+
const fileConflict = t.files.some(f => activeFiles.has(f));
|
|
242
|
+
const depConflict = opts.importGraph && activeTasks.some(at =>
|
|
243
|
+
taskDependsOn(t.files, at.files, opts.importGraph!)
|
|
244
|
+
);
|
|
245
|
+
if (!fileConflict && !depConflict) {
|
|
238
246
|
nest.updateTaskStatus(t.id, "pending");
|
|
239
247
|
}
|
|
240
248
|
}
|
|
@@ -324,7 +332,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
324
332
|
|
|
325
333
|
nest.init(initialState);
|
|
326
334
|
const { signal, callbacks } = opts;
|
|
327
|
-
const waveBase: Omit<WaveOptions, "caste"> = {
|
|
335
|
+
const waveBase: Omit<WaveOptions, "caste"> & { importGraph?: ImportGraph } = {
|
|
328
336
|
nest, cwd: opts.cwd, signal, callbacks,
|
|
329
337
|
currentModel: opts.currentModel,
|
|
330
338
|
modelOverrides: opts.modelOverrides,
|
|
@@ -387,6 +395,16 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
387
395
|
// ═══ Phase 2: 工作 ═══
|
|
388
396
|
nest.updateState({ status: "working" });
|
|
389
397
|
|
|
398
|
+
// 构建 import graph 用于依赖感知调度
|
|
399
|
+
let importGraph: ImportGraph | undefined;
|
|
400
|
+
try {
|
|
401
|
+
const allFiles = nest.getAllTasks().flatMap(t => t.files).filter(f => /\.[tj]sx?$/.test(f));
|
|
402
|
+
if (allFiles.length > 0) {
|
|
403
|
+
importGraph = buildImportGraph([...new Set(allFiles)], opts.cwd);
|
|
404
|
+
waveBase.importGraph = importGraph;
|
|
405
|
+
}
|
|
406
|
+
} catch { /* graph build failed, proceed without */ }
|
|
407
|
+
|
|
390
408
|
// 先执行 drone 任务(零 LLM 成本)
|
|
391
409
|
const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
392
410
|
if (droneTasks.length > 0) {
|
|
@@ -168,7 +168,7 @@ function parseSubTasks(output: string): ParsedSubTask[] {
|
|
|
168
168
|
tasks.push({
|
|
169
169
|
title: m[1]?.trim() || "Untitled",
|
|
170
170
|
description: m[2]?.trim() || m[1]?.trim() || "",
|
|
171
|
-
files: (m[3]?.trim() || "").split(",").map(f => f.trim()).filter(Boolean),
|
|
171
|
+
files: (m[3]?.trim() || "").split(",").map((f: string) => f.trim()).filter(Boolean),
|
|
172
172
|
caste: (m[4]?.trim() as AntCaste) || "worker",
|
|
173
173
|
priority: (parseInt(m[5] || "3") as 1 | 2 | 3 | 4 | 5) || 3,
|
|
174
174
|
context,
|
|
@@ -361,7 +361,7 @@ export async function spawnAnt(
|
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
// 订阅实时事件
|
|
364
|
-
session.subscribe((event) => {
|
|
364
|
+
session.subscribe((event: any) => {
|
|
365
365
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
366
366
|
const delta = event.assistantMessageEvent.delta;
|
|
367
367
|
accumulatedText += delta;
|