@ilya-lesikov/pi-pi 0.4.0 → 0.6.0
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/3p/pi-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +73 -31
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1191 -360
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +295 -148
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -35
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -29
- package/extensions/orchestrator/phases/review-task.ts +3 -3
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2631 -392
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- package/package.json +2 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { callExa } from "./exa.js";
|
|
3
|
+
|
|
4
|
+
function mockFetchText(text: string, init: { ok?: boolean; status?: number } = {}) {
|
|
5
|
+
return vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
6
|
+
ok: init.ok ?? true,
|
|
7
|
+
status: init.status ?? 200,
|
|
8
|
+
text: async () => text,
|
|
9
|
+
} as Response);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("callExa error handling", () => {
|
|
17
|
+
it("throws on an SSE error payload even when the message lacks the word 'error'", async () => {
|
|
18
|
+
mockFetchText(`data: ${JSON.stringify({ error: { message: "rate limit exceeded" } })}\n`);
|
|
19
|
+
await expect(callExa("web_search_exa", {})).rejects.toThrow("rate limit exceeded");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns text from a normal SSE success response", async () => {
|
|
23
|
+
mockFetchText(`data: ${JSON.stringify({ result: { content: [{ text: "hello world" }] } })}\n`);
|
|
24
|
+
await expect(callExa("web_search_exa", {})).resolves.toBe("hello world");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns text from a non-SSE JSON success response", async () => {
|
|
28
|
+
mockFetchText(JSON.stringify({ result: { content: [{ text: "plain json" }] } }));
|
|
29
|
+
await expect(callExa("web_search_exa", {})).resolves.toBe("plain json");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("throws on a non-SSE JSON error payload lacking the word 'error'", async () => {
|
|
33
|
+
mockFetchText(JSON.stringify({ error: { message: "quota reached" } }));
|
|
34
|
+
await expect(callExa("web_search_exa", {})).rejects.toThrow("quota reached");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("falls through and returns the raw body for a non-JSON OK response", async () => {
|
|
38
|
+
mockFetchText("not json at all", { ok: true, status: 200 });
|
|
39
|
+
await expect(callExa("web_search_exa", {})).resolves.toBe("not json at all");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("throws on a non-JSON body when the HTTP response is not ok", async () => {
|
|
43
|
+
mockFetchText("rate limited", { ok: false, status: 429 });
|
|
44
|
+
await expect(callExa("web_search_exa", {})).rejects.toThrow("Exa HTTP 429");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -3,7 +3,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
|
|
4
4
|
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
5
5
|
|
|
6
|
-
async function callExa(toolName: string, args: Record<string, unknown>): Promise<string> {
|
|
6
|
+
export async function callExa(toolName: string, args: Record<string, unknown>): Promise<string> {
|
|
7
7
|
const body = JSON.stringify({
|
|
8
8
|
jsonrpc: "2.0",
|
|
9
9
|
id: 1,
|
|
@@ -25,23 +25,29 @@ async function callExa(toolName: string, args: Record<string, unknown>): Promise
|
|
|
25
25
|
|
|
26
26
|
for (const line of raw.split("\n")) {
|
|
27
27
|
if (!line.startsWith("data:")) continue;
|
|
28
|
+
let json: any;
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (text) return text;
|
|
33
|
-
} catch (e: any) {
|
|
34
|
-
if (e.message?.includes("error")) throw e;
|
|
30
|
+
json = JSON.parse(line.slice(5).trim());
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
35
33
|
}
|
|
34
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
35
|
+
const text = json.result?.content?.[0]?.text;
|
|
36
|
+
if (text) return text;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
let json: any;
|
|
38
40
|
try {
|
|
39
|
-
|
|
40
|
-
if (json.error) throw new Error(json.error.message);
|
|
41
|
-
return json.result?.content?.[0]?.text ?? raw;
|
|
41
|
+
json = JSON.parse(raw);
|
|
42
42
|
} catch {
|
|
43
|
+
if (!res.ok) throw new Error(`Exa HTTP ${res.status}: ${raw.slice(0, 200)}`);
|
|
43
44
|
return raw;
|
|
44
45
|
}
|
|
46
|
+
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error));
|
|
47
|
+
const text = json.result?.content?.[0]?.text;
|
|
48
|
+
if (text != null) return text;
|
|
49
|
+
if (!res.ok) throw new Error(`Exa HTTP ${res.status}: ${raw.slice(0, 200)}`);
|
|
50
|
+
return raw;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
function ok(text: string) {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const tempDirs: string[] = [];
|
|
7
|
+
|
|
8
|
+
function makeTempDir(): string {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-pi-flant-infra-"));
|
|
10
|
+
tempDirs.push(dir);
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function loadFlantInfraModule(agentDir: string) {
|
|
15
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
return import("./flant-infra.js");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectModelSpecs(value: unknown): string[] {
|
|
21
|
+
if (!value || typeof value !== "object") return [];
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
const walk = (node: any) => {
|
|
24
|
+
if (!node || typeof node !== "object") return;
|
|
25
|
+
if (typeof node.model === "string") out.push(node.model);
|
|
26
|
+
for (const nested of Object.values(node)) {
|
|
27
|
+
walk(nested);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
walk(value);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
delete process.env.PI_CODING_AGENT_DIR;
|
|
36
|
+
for (const dir of tempDirs.splice(0)) {
|
|
37
|
+
rmSync(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("flant-infra", () => {
|
|
42
|
+
it("registerFlantProviders is idempotent across repeated calls", async () => {
|
|
43
|
+
const dir = makeTempDir();
|
|
44
|
+
const mod = await loadFlantInfraModule(dir);
|
|
45
|
+
|
|
46
|
+
const registered = new Map<string, unknown>();
|
|
47
|
+
const pi = {
|
|
48
|
+
registerProvider: vi.fn((name: string, config: unknown) => registered.set(name, config)),
|
|
49
|
+
unregisterProvider: vi.fn((name: string) => registered.delete(name)),
|
|
50
|
+
} as any;
|
|
51
|
+
|
|
52
|
+
const models = ["claude-opus-4-6", "gpt-5"];
|
|
53
|
+
mod.registerFlantProviders(pi, models, {});
|
|
54
|
+
mod.registerFlantProviders(pi, models, {});
|
|
55
|
+
|
|
56
|
+
expect(registered.size).toBe(2);
|
|
57
|
+
expect([...registered.keys()].sort()).toEqual(["pp-flant-anthropic", "pp-flant-openai"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("generateDisplayName formats model ids", async () => {
|
|
61
|
+
const dir = makeTempDir();
|
|
62
|
+
const mod = await loadFlantInfraModule(dir);
|
|
63
|
+
|
|
64
|
+
expect(mod.generateDisplayName("claude-opus-4-6")).toBe("Claude Opus 4.6");
|
|
65
|
+
expect(mod.generateDisplayName("gpt-5-4-mini")).toBe("GPT 5.4 Mini");
|
|
66
|
+
expect(mod.generateDisplayName("o3-mini")).toBe("O3 Mini");
|
|
67
|
+
expect(mod.generateDisplayName("qwen-3-coder")).toBe("Qwen 3 Coder");
|
|
68
|
+
expect(mod.generateDisplayName("api-ai-2")).toBe("API AI 2");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("generateFlantConfig creates config with mapped model specs", async () => {
|
|
72
|
+
const dir = makeTempDir();
|
|
73
|
+
const mod = await loadFlantInfraModule(dir);
|
|
74
|
+
|
|
75
|
+
const config = mod.generateFlantConfig([
|
|
76
|
+
"claude-opus-4-6",
|
|
77
|
+
"gpt-5-4",
|
|
78
|
+
"gpt-5-4-mini",
|
|
79
|
+
"gemini-3-1-pro",
|
|
80
|
+
"gemini-3-1-flash",
|
|
81
|
+
"deepseek-v3",
|
|
82
|
+
"grok-4",
|
|
83
|
+
]) as any;
|
|
84
|
+
|
|
85
|
+
expect(config.agents.orchestrators.implement.model).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
86
|
+
expect(config.agents.orchestrators.plan.model).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
87
|
+
expect(config.agents.orchestrators.debug.model).toBe("pp-flant-openai/gpt-5-4");
|
|
88
|
+
expect(config.agents.orchestrators.brainstorm.model).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
89
|
+
expect(config.agents.orchestrators.review.model).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
90
|
+
expect(config.agents.subagents.simple.explore.model).toBe("pp-flant-openai/gemini-3-1-flash");
|
|
91
|
+
expect(config.agents.subagents.simple.librarian.model).toBe("pp-flant-openai/gemini-3-1-flash");
|
|
92
|
+
expect(config.agents.subagents.presetGroups.planners.presets.regular.agents.opus.model).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
93
|
+
expect(config.agents.subagents.presetGroups.planners.presets.regular.agents.gpt.model).toBe("pp-flant-openai/gpt-5-4");
|
|
94
|
+
expect(config.agents.subagents.presetGroups.planners.presets.regular.agents.gemini.model).toBe("pp-flant-openai/gemini-3-1-pro");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("generateFlantConfig returns empty object for empty models", async () => {
|
|
98
|
+
const dir = makeTempDir();
|
|
99
|
+
const mod = await loadFlantInfraModule(dir);
|
|
100
|
+
|
|
101
|
+
expect(mod.generateFlantConfig([])).toEqual({});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("generateFlantConfig with anthropic-only models uses anthropic specs", async () => {
|
|
105
|
+
const dir = makeTempDir();
|
|
106
|
+
const mod = await loadFlantInfraModule(dir);
|
|
107
|
+
|
|
108
|
+
const config = mod.generateFlantConfig(["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-3-5"]);
|
|
109
|
+
const specs = collectModelSpecs(config);
|
|
110
|
+
|
|
111
|
+
expect(specs.length).toBeGreaterThan(0);
|
|
112
|
+
expect(specs.every((spec) => spec.startsWith("pp-flant-anthropic/"))).toBe(true);
|
|
113
|
+
expect(specs.some((spec) => spec.startsWith("pp-flant-openai/"))).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("generateFlantConfig picks latest models per family", async () => {
|
|
117
|
+
const dir = makeTempDir();
|
|
118
|
+
const mod = await loadFlantInfraModule(dir);
|
|
119
|
+
|
|
120
|
+
const config = mod.generateFlantConfig([
|
|
121
|
+
"claude-opus-4-5",
|
|
122
|
+
"claude-opus-4-7",
|
|
123
|
+
"gpt-5-3",
|
|
124
|
+
"gpt-5-4",
|
|
125
|
+
"gemini-3-0-pro",
|
|
126
|
+
"gemini-3-1-pro",
|
|
127
|
+
"gemini-3-1-flash",
|
|
128
|
+
"gemini-3-1-flash-lite",
|
|
129
|
+
]) as any;
|
|
130
|
+
|
|
131
|
+
expect(config.agents.orchestrators.implement.model).toBe("pp-flant-anthropic/claude-opus-4-7");
|
|
132
|
+
expect(config.agents.orchestrators.plan.model).toBe("pp-flant-anthropic/claude-opus-4-7");
|
|
133
|
+
expect(config.agents.orchestrators.debug.model).toBe("pp-flant-openai/gpt-5-4");
|
|
134
|
+
expect(config.agents.subagents.presetGroups.planners.presets.regular.agents.gemini.model).toBe("pp-flant-openai/gemini-3-1-pro");
|
|
135
|
+
expect(config.agents.subagents.simple.explore.model).toBe("pp-flant-openai/gemini-3-1-flash-lite");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("loadFlantSettings returns defaults when file is missing", async () => {
|
|
139
|
+
const dir = makeTempDir();
|
|
140
|
+
const mod = await loadFlantInfraModule(dir);
|
|
141
|
+
|
|
142
|
+
expect(mod.loadFlantSettings()).toEqual({
|
|
143
|
+
enabled: false,
|
|
144
|
+
autoUpdate: true,
|
|
145
|
+
cacheTTLDays: 7,
|
|
146
|
+
lastUpdated: null,
|
|
147
|
+
cachedFlantModels: null,
|
|
148
|
+
cachedOpenRouterData: null,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("loadFlantSettings parses valid settings file", async () => {
|
|
153
|
+
const dir = makeTempDir();
|
|
154
|
+
const mod = await loadFlantInfraModule(dir);
|
|
155
|
+
const settingsDir = join(dir, "extensions", "pp", "cache");
|
|
156
|
+
const settingsPath = join(settingsDir, "flant-models.json");
|
|
157
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
158
|
+
writeFileSync(
|
|
159
|
+
settingsPath,
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
enabled: true,
|
|
162
|
+
autoUpdate: false,
|
|
163
|
+
cacheTTLDays: "3",
|
|
164
|
+
lastUpdated: "2026-01-02T03:04:05.000Z",
|
|
165
|
+
cachedFlantModels: ["gpt-5-4", 123, "claude-opus-4-6"],
|
|
166
|
+
cachedOpenRouterData: {
|
|
167
|
+
"gpt-5-4": {
|
|
168
|
+
name: "GPT 5.4",
|
|
169
|
+
context_length: 123,
|
|
170
|
+
max_completion_tokens: 45,
|
|
171
|
+
pricing: { prompt: 1, completion: 2, cacheRead: 3, cacheWrite: 4 },
|
|
172
|
+
modality: "text",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
"utf-8",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(mod.loadFlantSettings()).toEqual({
|
|
180
|
+
enabled: true,
|
|
181
|
+
autoUpdate: false,
|
|
182
|
+
cacheTTLDays: 3,
|
|
183
|
+
lastUpdated: "2026-01-02T03:04:05.000Z",
|
|
184
|
+
cachedFlantModels: ["gpt-5-4", "claude-opus-4-6"],
|
|
185
|
+
cachedOpenRouterData: {
|
|
186
|
+
"gpt-5-4": {
|
|
187
|
+
name: "GPT 5.4",
|
|
188
|
+
context_length: 123,
|
|
189
|
+
max_completion_tokens: 45,
|
|
190
|
+
pricing: { prompt: 1, completion: 2, cacheRead: 3, cacheWrite: 4 },
|
|
191
|
+
modality: "text",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("saveFlantSettings and loadFlantSettings round-trip", async () => {
|
|
198
|
+
const dir = makeTempDir();
|
|
199
|
+
const mod = await loadFlantInfraModule(dir);
|
|
200
|
+
|
|
201
|
+
const settings = {
|
|
202
|
+
enabled: true,
|
|
203
|
+
autoUpdate: true,
|
|
204
|
+
cacheTTLDays: 14,
|
|
205
|
+
lastUpdated: "2026-02-01T00:00:00.000Z",
|
|
206
|
+
cachedFlantModels: ["claude-opus-4-6", "gpt-5-4"],
|
|
207
|
+
cachedOpenRouterData: {
|
|
208
|
+
"claude-opus-4-6": {
|
|
209
|
+
name: "Claude Opus 4.6",
|
|
210
|
+
context_length: 200000,
|
|
211
|
+
max_completion_tokens: 32000,
|
|
212
|
+
pricing: { prompt: 0.01, completion: 0.02, cacheRead: 0.003, cacheWrite: 0.004 },
|
|
213
|
+
modality: "text",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
mod.saveFlantSettings(settings);
|
|
219
|
+
|
|
220
|
+
const loaded = mod.loadFlantSettings();
|
|
221
|
+
expect(loaded).toEqual(settings);
|
|
222
|
+
expect(existsSync(join(dir, "extensions", "pp", "cache", "flant-models.json"))).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
4
5
|
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import type { PiPiConfig } from "./config.js";
|
|
7
|
+
import { updateRegistryFromAvailableModels } from "./model-registry.js";
|
|
8
|
+
import { compareModelVersion } from "./model-version.js";
|
|
9
|
+
import { getLogger } from "./log.js";
|
|
6
10
|
|
|
7
11
|
export interface OpenRouterModelData {
|
|
8
12
|
name: string;
|
|
@@ -117,7 +121,13 @@ export function loadFlantSettings(): FlantSettings {
|
|
|
117
121
|
|
|
118
122
|
export function saveFlantSettings(settings: FlantSettings): void {
|
|
119
123
|
ensureSettingsDir();
|
|
120
|
-
|
|
124
|
+
if (!existsSync(SETTINGS_PATH)) writeFileSync(SETTINGS_PATH, "{}\n", "utf-8");
|
|
125
|
+
const release = lockfile.lockSync(SETTINGS_PATH, { stale: 10000 });
|
|
126
|
+
try {
|
|
127
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
128
|
+
} finally {
|
|
129
|
+
release();
|
|
130
|
+
}
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
function toTitleCase(token: string): string {
|
|
@@ -284,9 +294,13 @@ export function registerFlantProviders(
|
|
|
284
294
|
models: string[],
|
|
285
295
|
metadata: Record<string, OpenRouterModelData>,
|
|
286
296
|
): void {
|
|
297
|
+
const log = getLogger();
|
|
287
298
|
const uniqueModels = [...new Set(models)];
|
|
288
299
|
const anthropicModels = uniqueModels.filter((m) => m.startsWith("claude-"));
|
|
289
300
|
const openaiModels = uniqueModels.filter((m) => !m.startsWith("claude-"));
|
|
301
|
+
log.debug({ s: "flant", total: uniqueModels.length, anthropic: anthropicModels.length, openai: openaiModels.length }, "registering flant providers");
|
|
302
|
+
|
|
303
|
+
unregisterFlantProviders(pi);
|
|
290
304
|
|
|
291
305
|
pi.registerProvider("pp-flant-anthropic", {
|
|
292
306
|
api: "anthropic-messages",
|
|
@@ -301,18 +315,11 @@ export function registerFlantProviders(
|
|
|
301
315
|
apiKey: "$FLANT_API_KEY",
|
|
302
316
|
models: openaiModels.map((m) => buildProviderModelConfig(m, metadata)),
|
|
303
317
|
});
|
|
304
|
-
}
|
|
305
318
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
for (let i = 0; i < len; i++) {
|
|
311
|
-
const ai = aParts[i] ?? 0;
|
|
312
|
-
const bi = bParts[i] ?? 0;
|
|
313
|
-
if (ai !== bi) return ai - bi;
|
|
314
|
-
}
|
|
315
|
-
return a.localeCompare(b);
|
|
319
|
+
updateRegistryFromAvailableModels([
|
|
320
|
+
...anthropicModels.map((id) => `pp-flant-anthropic/${id}`),
|
|
321
|
+
...openaiModels.map((id) => `pp-flant-openai/${id}`),
|
|
322
|
+
]);
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
function pickLatest(models: string[]): string | null {
|
|
@@ -337,6 +344,22 @@ function makeVariant(modelId: string | null, fallbackModelId: string): { enabled
|
|
|
337
344
|
return { enabled: true, model: modelSpec(modelId), thinking: "high" };
|
|
338
345
|
}
|
|
339
346
|
|
|
347
|
+
function makeVariantWithThinking(
|
|
348
|
+
modelId: string | null,
|
|
349
|
+
fallbackModelId: string,
|
|
350
|
+
thinking: string,
|
|
351
|
+
): { enabled: boolean; model: string; thinking: string } {
|
|
352
|
+
if (!modelId) return { enabled: false, model: modelSpec(fallbackModelId), thinking };
|
|
353
|
+
return { enabled: true, model: modelSpec(modelId), thinking };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildPresetGroup(
|
|
357
|
+
presets: Record<string, { enabled?: boolean; agents: Record<string, { enabled: boolean; model: string; thinking: string }> }>,
|
|
358
|
+
defaultPreset = "regular",
|
|
359
|
+
): { default: string; presets: typeof presets } {
|
|
360
|
+
return { default: defaultPreset, presets };
|
|
361
|
+
}
|
|
362
|
+
|
|
340
363
|
export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
341
364
|
const uniqueModels = [...new Set(models)];
|
|
342
365
|
if (uniqueModels.length === 0) return {};
|
|
@@ -358,36 +381,80 @@ export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
|
358
381
|
const fastModel = fastest ?? debugModel;
|
|
359
382
|
|
|
360
383
|
return {
|
|
361
|
-
mainModel: {
|
|
362
|
-
implement: { model: modelSpec(implementModel), thinking: "high" },
|
|
363
|
-
debug: { model: modelSpec(debugModel), thinking: "high" },
|
|
364
|
-
brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
|
|
365
|
-
review: { model: modelSpec(implementModel), thinking: "high" },
|
|
366
|
-
},
|
|
367
|
-
planners: {
|
|
368
|
-
opus: makeVariant(latestOpus, fallback),
|
|
369
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
370
|
-
gemini: makeVariant(latestGeminiPro, fallback),
|
|
371
|
-
},
|
|
372
|
-
planReviewers: {
|
|
373
|
-
opus: makeVariant(latestOpus, fallback),
|
|
374
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
375
|
-
gemini: makeVariant(latestGeminiPro, fallback),
|
|
376
|
-
},
|
|
377
|
-
codeReviewers: {
|
|
378
|
-
opus: makeVariant(latestOpus, fallback),
|
|
379
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
380
|
-
gemini: makeVariant(latestGeminiPro, fallback),
|
|
381
|
-
},
|
|
382
|
-
brainstormReviewers: {
|
|
383
|
-
opus: makeVariant(latestOpus, fallback),
|
|
384
|
-
gpt: makeVariant(latestGpt, fallback),
|
|
385
|
-
gemini: makeVariant(latestGeminiPro, fallback),
|
|
386
|
-
},
|
|
387
384
|
agents: {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
385
|
+
orchestrators: {
|
|
386
|
+
implement: { model: modelSpec(implementModel), thinking: "high" },
|
|
387
|
+
plan: { model: modelSpec(implementModel), thinking: "high" },
|
|
388
|
+
debug: { model: modelSpec(debugModel), thinking: "high" },
|
|
389
|
+
brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
|
|
390
|
+
review: { model: modelSpec(implementModel), thinking: "high" },
|
|
391
|
+
},
|
|
392
|
+
subagents: {
|
|
393
|
+
simple: {
|
|
394
|
+
explore: { model: modelSpec(fastModel), thinking: "low" },
|
|
395
|
+
librarian: { model: modelSpec(fastModel), thinking: "medium" },
|
|
396
|
+
task: { model: modelSpec(taskModel), thinking: "medium" },
|
|
397
|
+
},
|
|
398
|
+
presetGroups: {
|
|
399
|
+
planners: buildPresetGroup({
|
|
400
|
+
regular: {
|
|
401
|
+
agents: {
|
|
402
|
+
opus: makeVariant(latestOpus, fallback),
|
|
403
|
+
gpt: makeVariant(latestGpt, fallback),
|
|
404
|
+
gemini: makeVariant(latestGeminiPro, fallback),
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
planReviewers: buildPresetGroup({
|
|
409
|
+
regular: {
|
|
410
|
+
agents: {
|
|
411
|
+
opus: makeVariant(latestOpus, fallback),
|
|
412
|
+
gpt: makeVariant(latestGpt, fallback),
|
|
413
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
deep: {
|
|
417
|
+
agents: {
|
|
418
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
419
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
420
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
codeReviewers: buildPresetGroup({
|
|
425
|
+
regular: {
|
|
426
|
+
agents: {
|
|
427
|
+
opus: makeVariant(latestOpus, fallback),
|
|
428
|
+
gpt: makeVariant(latestGpt, fallback),
|
|
429
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
deep: {
|
|
433
|
+
agents: {
|
|
434
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
435
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
436
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
brainstormReviewers: buildPresetGroup({
|
|
441
|
+
regular: {
|
|
442
|
+
agents: {
|
|
443
|
+
opus: makeVariant(latestOpus, fallback),
|
|
444
|
+
gpt: makeVariant(latestGpt, fallback),
|
|
445
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
deep: {
|
|
449
|
+
agents: {
|
|
450
|
+
opus: makeVariantWithThinking(latestOpus, fallback, "xhigh"),
|
|
451
|
+
gpt: makeVariantWithThinking(latestGpt, fallback, "xhigh"),
|
|
452
|
+
gemini: makeVariantWithThinking(latestGeminiPro, fallback, "xhigh"),
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
},
|
|
391
458
|
},
|
|
392
459
|
};
|
|
393
460
|
}
|
|
@@ -469,7 +536,9 @@ export async function updateFlantInfra(
|
|
|
469
536
|
export function initFlantSync(pi: ExtensionAPI): void {
|
|
470
537
|
setPI(pi);
|
|
471
538
|
const settings = loadFlantSettings();
|
|
539
|
+
const log = getLogger();
|
|
472
540
|
if (!settings.enabled) {
|
|
541
|
+
log.debug({ s: "flant" }, "flant disabled");
|
|
473
542
|
generatedFlantConfig = null;
|
|
474
543
|
return;
|
|
475
544
|
}
|
|
@@ -11,15 +11,22 @@ import { validatePlan, validateArtifact } from "./validate-artifacts.js";
|
|
|
11
11
|
import { initFlantSync } from "./flant-infra.js";
|
|
12
12
|
|
|
13
13
|
const ORCHESTRATOR_KEY = Symbol.for("pi-pi:orchestrator-initialized");
|
|
14
|
+
const ORCHESTRATOR_CWD_KEY = Symbol.for("pi-pi:orchestrator-cwd");
|
|
15
|
+
// Shared with 3p/pi-subagents/src/agent-runner.ts: the value is a { depth: number }
|
|
16
|
+
// marking that this process runs as a subagent. The orchestrator only reads it for
|
|
17
|
+
// truthiness ("am I a subagent?"); agent-runner uses depth for nesting.
|
|
14
18
|
export const SUBAGENT_SESSION_KEY = Symbol.for("pi-pi:subagent-session");
|
|
15
19
|
|
|
16
20
|
export default function (pi: ExtensionAPI) {
|
|
17
21
|
if ((globalThis as any)[ORCHESTRATOR_KEY]) {
|
|
18
|
-
(globalThis as any)[SUBAGENT_SESSION_KEY]
|
|
22
|
+
if (!(globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
23
|
+
(globalThis as any)[SUBAGENT_SESSION_KEY] = { depth: 1 };
|
|
24
|
+
}
|
|
19
25
|
registerSubagentTools(pi);
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
28
|
(globalThis as any)[ORCHESTRATOR_KEY] = true;
|
|
29
|
+
(globalThis as any)[ORCHESTRATOR_CWD_KEY] = process.cwd();
|
|
23
30
|
|
|
24
31
|
initFlantSync(pi);
|
|
25
32
|
|
|
@@ -29,7 +36,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
function registerSubagentTools(pi: ExtensionAPI): void {
|
|
32
|
-
|
|
39
|
+
// Subagents run in-process; bind cbm/ast-search and plan validation to the
|
|
40
|
+
// orchestrator's project root (seeded to process.cwd() at init, then refreshed
|
|
41
|
+
// to ctx.cwd on session_start) rather than a raw process.cwd() captured here,
|
|
42
|
+
// which is the launch dir and wrong for worktree-isolated tasks.
|
|
43
|
+
const cwd = (globalThis as any)[ORCHESTRATOR_CWD_KEY] ?? process.cwd();
|
|
33
44
|
registerCbmTools(pi, cwd);
|
|
34
45
|
registerExaTools(pi);
|
|
35
46
|
registerAstSearchTool(pi, cwd);
|