@ilya-lesikov/pi-pi 0.6.0 → 0.8.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 +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import type { Model } from "@earendil-works/pi-ai";
|
|
6
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import {
|
|
@@ -27,6 +28,53 @@ const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
|
27
28
|
const TRACER_KEY = Symbol.for("pi-pi:tracer");
|
|
28
29
|
const SUBAGENT_SESSION_KEY = Symbol.for("pi-pi:subagent-session");
|
|
29
30
|
|
|
31
|
+
// Concurrency-safe subagent lineage. The previous approach derived parent/depth
|
|
32
|
+
// from the mutable process-global SUBAGENT_SESSION_KEY, which is racy for
|
|
33
|
+
// CONCURRENT siblings: MAIN spawns opus/gpt/gemini in parallel, each runAgent's
|
|
34
|
+
// synchronous prologue read whatever the *previous sibling* had just written to
|
|
35
|
+
// the global — so gpt recorded opus as its parent, gemini recorded gpt, forming a
|
|
36
|
+
// bogus opus→gpt→gemini chain with depth 3-6 instead of three depth-1 children of
|
|
37
|
+
// MAIN. AsyncLocalStorage instead propagates DOWN the async call chain (a nested
|
|
38
|
+
// spawn inherits its true parent's store) but NOT ACROSS sibling promises (each
|
|
39
|
+
// sibling started from MAIN sees no store → parent=undefined, depth=1).
|
|
40
|
+
interface SubagentLineage {
|
|
41
|
+
depth: number;
|
|
42
|
+
subagentId?: string;
|
|
43
|
+
}
|
|
44
|
+
const subagentLineage = new AsyncLocalStorage<SubagentLineage>();
|
|
45
|
+
|
|
46
|
+
// The process-global SUBAGENT_SESSION_KEY marker is read elsewhere purely for
|
|
47
|
+
// truthiness ("is any subagent running in this process?", e.g. orchestrator nudge
|
|
48
|
+
// gates). Manage it with a ref-count so concurrent in-process siblings set/clear
|
|
49
|
+
// it correctly regardless of completion order (a plain save/restore would leak a
|
|
50
|
+
// stale marker whenever the last sibling to finish wasn't the last to start).
|
|
51
|
+
let activeSubagentRuns = 0;
|
|
52
|
+
let markerBeforeFirstRun: unknown;
|
|
53
|
+
function enterSubagentMarker(): void {
|
|
54
|
+
if (activeSubagentRuns === 0) {
|
|
55
|
+
// Snapshot whatever was there before our first concurrent run (undefined in
|
|
56
|
+
// the main process; the orchestrator's persistent bootstrap { depth: 1 } in a
|
|
57
|
+
// subagent process) so we can restore it exactly when the last run exits.
|
|
58
|
+
markerBeforeFirstRun = (globalThis as any)[SUBAGENT_SESSION_KEY];
|
|
59
|
+
}
|
|
60
|
+
activeSubagentRuns++;
|
|
61
|
+
const marker = (globalThis as any)[SUBAGENT_SESSION_KEY];
|
|
62
|
+
if (typeof marker !== "object" || marker === null) {
|
|
63
|
+
(globalThis as any)[SUBAGENT_SESSION_KEY] = { depth: 1 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function exitSubagentMarker(): void {
|
|
67
|
+
activeSubagentRuns = Math.max(0, activeSubagentRuns - 1);
|
|
68
|
+
if (activeSubagentRuns === 0) {
|
|
69
|
+
if (markerBeforeFirstRun === undefined) {
|
|
70
|
+
delete (globalThis as any)[SUBAGENT_SESSION_KEY];
|
|
71
|
+
} else {
|
|
72
|
+
(globalThis as any)[SUBAGENT_SESSION_KEY] = markerBeforeFirstRun;
|
|
73
|
+
}
|
|
74
|
+
markerBeforeFirstRun = undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
interface PiPiTracer {
|
|
31
79
|
openSubagent(meta: {
|
|
32
80
|
subagentId: string;
|
|
@@ -45,11 +93,6 @@ function getTracer(): PiPiTracer | undefined {
|
|
|
45
93
|
return (globalThis as any)[TRACER_KEY];
|
|
46
94
|
}
|
|
47
95
|
|
|
48
|
-
function subagentDepth(): number {
|
|
49
|
-
const marker = (globalThis as any)[SUBAGENT_SESSION_KEY];
|
|
50
|
-
return typeof marker?.depth === "number" ? marker.depth : 1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
96
|
function traceSubagentEvent(subagentId: string | undefined, event: AgentSessionEvent, turnIndex: number): void {
|
|
54
97
|
if (!subagentId) return;
|
|
55
98
|
const tracer = getTracer();
|
|
@@ -242,21 +285,22 @@ export async function runAgent(
|
|
|
242
285
|
|
|
243
286
|
// Resolve working directory: worktree override > parent cwd
|
|
244
287
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
245
|
-
// Shared marker with extensions/orchestrator/index.ts (SUBAGENT_SESSION_KEY).
|
|
246
|
-
// Canonical shape is { depth: number }; a legacy boolean true is tolerated as depth 1.
|
|
247
|
-
const subagentSessionKey = Symbol.for("pi-pi:subagent-session");
|
|
248
|
-
const previousSubagentSession = (globalThis as any)[subagentSessionKey];
|
|
249
|
-
const previousDepth = typeof previousSubagentSession === "object" && previousSubagentSession !== null
|
|
250
|
-
? ((previousSubagentSession as { depth?: number }).depth ?? 0)
|
|
251
|
-
: previousSubagentSession
|
|
252
|
-
? 1
|
|
253
|
-
: 0;
|
|
254
|
-
const parentSubagentId = typeof previousSubagentSession === "object" && previousSubagentSession !== null
|
|
255
|
-
? (previousSubagentSession as { subagentId?: string }).subagentId
|
|
256
|
-
: undefined;
|
|
257
|
-
const subagentSessionState = { depth: previousDepth + 1, subagentId: options.subagentId };
|
|
258
|
-
(globalThis as any)[subagentSessionKey] = subagentSessionState;
|
|
259
288
|
|
|
289
|
+
// Derive lineage from the async-context store (see subagentLineage above).
|
|
290
|
+
// Reading it BEFORE entering the child's own als.run means concurrent siblings
|
|
291
|
+
// each see their real parent (MAIN → undefined store), not a leaked sibling.
|
|
292
|
+
const parentLineage = subagentLineage.getStore();
|
|
293
|
+
const parentSubagentId = parentLineage?.subagentId;
|
|
294
|
+
const traceDepth = (parentLineage?.depth ?? 0) + 1;
|
|
295
|
+
const childLineage: SubagentLineage = { depth: traceDepth, subagentId: options.subagentId };
|
|
296
|
+
|
|
297
|
+
// Mark the process as "running a subagent" for truthiness readers (see
|
|
298
|
+
// enter/exitSubagentMarker). Lineage (parent/depth) comes from als above, so
|
|
299
|
+
// this marker no longer participates in nesting math and its old sibling-race
|
|
300
|
+
// can no longer corrupt the org chart.
|
|
301
|
+
enterSubagentMarker();
|
|
302
|
+
|
|
303
|
+
return subagentLineage.run(childLineage, async () => {
|
|
260
304
|
try {
|
|
261
305
|
const env = await detectEnv(options.pi, effectiveCwd);
|
|
262
306
|
|
|
@@ -460,7 +504,7 @@ export async function runAgent(
|
|
|
460
504
|
description: options.subagentDescription,
|
|
461
505
|
parentToolCallId: options.parentToolCallId,
|
|
462
506
|
parentSubagentId,
|
|
463
|
-
depth:
|
|
507
|
+
depth: traceDepth,
|
|
464
508
|
systemPrompt,
|
|
465
509
|
effectivePrompt,
|
|
466
510
|
});
|
|
@@ -496,20 +540,9 @@ export async function runAgent(
|
|
|
496
540
|
}
|
|
497
541
|
return { responseText, session, aborted, steered: softLimitReached };
|
|
498
542
|
} finally {
|
|
499
|
-
|
|
500
|
-
? ((((globalThis as any)[subagentSessionKey]) as { depth?: number }).depth ?? subagentSessionState.depth)
|
|
501
|
-
: subagentSessionState.depth;
|
|
502
|
-
const nextDepth = Math.max(0, currentDepth - 1);
|
|
503
|
-
if (nextDepth === 0) {
|
|
504
|
-
if (previousSubagentSession === undefined) {
|
|
505
|
-
delete (globalThis as any)[subagentSessionKey];
|
|
506
|
-
} else {
|
|
507
|
-
(globalThis as any)[subagentSessionKey] = previousSubagentSession;
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
(globalThis as any)[subagentSessionKey] = { depth: nextDepth };
|
|
511
|
-
}
|
|
543
|
+
exitSubagentMarker();
|
|
512
544
|
}
|
|
545
|
+
});
|
|
513
546
|
}
|
|
514
547
|
|
|
515
548
|
/**
|
|
@@ -402,13 +402,7 @@ Use get_subagent_result for full output.`,
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
// Background completion: route through group join or send individual nudge
|
|
405
|
-
const firstProgressSeen = new Set<string>();
|
|
406
|
-
const firstTurnSeen = new Set<string>();
|
|
407
|
-
|
|
408
405
|
const manager = new AgentManager((record) => {
|
|
409
|
-
firstProgressSeen.delete(record.id);
|
|
410
|
-
firstTurnSeen.delete(record.id);
|
|
411
|
-
|
|
412
406
|
let eventData: ReturnType<typeof buildEventData> | undefined;
|
|
413
407
|
try {
|
|
414
408
|
eventData = buildEventData(record);
|
|
@@ -907,36 +901,9 @@ Guidelines:
|
|
|
907
901
|
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
908
902
|
|
|
909
903
|
let id: string;
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
pi.events.emit("subagents:first_tool", {
|
|
914
|
-
id,
|
|
915
|
-
type: subagentType,
|
|
916
|
-
description: params.description,
|
|
917
|
-
toolName,
|
|
918
|
-
});
|
|
919
|
-
};
|
|
920
|
-
const emitFirstTurn = (turnCount: number) => {
|
|
921
|
-
if (firstTurnSeen.has(id)) return;
|
|
922
|
-
firstTurnSeen.add(id);
|
|
923
|
-
pi.events.emit("subagents:first_turn", {
|
|
924
|
-
id,
|
|
925
|
-
type: subagentType,
|
|
926
|
-
description: params.description,
|
|
927
|
-
turnCount,
|
|
928
|
-
});
|
|
929
|
-
};
|
|
930
|
-
const originalBgToolActivity = bgCallbacks.onToolActivity;
|
|
931
|
-
bgCallbacks.onToolActivity = (activity) => {
|
|
932
|
-
originalBgToolActivity(activity);
|
|
933
|
-
if (activity.type === "start") emitFirstTool(activity.toolName);
|
|
934
|
-
};
|
|
935
|
-
const originalBgTurnEnd = bgCallbacks.onTurnEnd;
|
|
936
|
-
bgCallbacks.onTurnEnd = (turnCount) => {
|
|
937
|
-
originalBgTurnEnd(turnCount);
|
|
938
|
-
emitFirstTurn(turnCount);
|
|
939
|
-
};
|
|
904
|
+
// first_tool/first_turn are now emitted centrally by AgentManager.startAgent
|
|
905
|
+
// (the single choke point for ALL spawn paths, including RPC-spawned panels),
|
|
906
|
+
// so no per-branch wiring is needed here.
|
|
940
907
|
|
|
941
908
|
// Wrap onSessionCreated to wire output file streaming.
|
|
942
909
|
// The callback lazily reads record.outputFile (set right after spawn)
|
|
@@ -944,8 +911,6 @@ Guidelines:
|
|
|
944
911
|
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
945
912
|
bgCallbacks.onSessionCreated = (session: any) => {
|
|
946
913
|
origBgOnSession(session);
|
|
947
|
-
firstProgressSeen.delete(id);
|
|
948
|
-
firstTurnSeen.delete(id);
|
|
949
914
|
const rec = manager.getRecord(id);
|
|
950
915
|
if (rec?.outputFile) {
|
|
951
916
|
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
|
|
@@ -81,6 +81,10 @@ export interface AgentRecord {
|
|
|
81
81
|
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
82
82
|
/** The tool_use_id from the original Agent tool call. */
|
|
83
83
|
toolCallId?: string;
|
|
84
|
+
/** Whether the subagents:first_tool event has been emitted for this run. */
|
|
85
|
+
firstToolEmitted?: boolean;
|
|
86
|
+
/** Whether the subagents:first_turn event has been emitted for this run. */
|
|
87
|
+
firstTurnEmitted?: boolean;
|
|
84
88
|
/** Path to the streaming output transcript file. */
|
|
85
89
|
outputFile?: string;
|
|
86
90
|
/** Cleanup function for the output file stream subscription. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PiPiConfig } from "../config.js";
|
|
2
|
+
import { resolveModel } from "../model-registry.js";
|
|
3
|
+
import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
|
|
4
|
+
|
|
5
|
+
export function createAdvisorAgent(config: PiPiConfig) {
|
|
6
|
+
return {
|
|
7
|
+
frontmatter: {
|
|
8
|
+
description: "Deep-reasoning advisor for design decisions and 'why is this broken' analysis (pi-pi)",
|
|
9
|
+
tools: `read, bash, grep, find, ls, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
|
|
10
|
+
model: resolveModel(config.agents.subagents.simple.advisor.model),
|
|
11
|
+
thinking: config.agents.subagents.simple.advisor.thinking,
|
|
12
|
+
max_turns: 120,
|
|
13
|
+
prompt_mode: "replace",
|
|
14
|
+
},
|
|
15
|
+
prompt: [
|
|
16
|
+
"<constraints>",
|
|
17
|
+
"You are a deep-reasoning ADVISOR. You investigate one hard question — a design decision, an architecture tradeoff, a \"why is this broken\", or a correctness/soundness judgment — and return a reasoned recommendation backed by evidence.",
|
|
18
|
+
"These rules override your default helpfulness. Strict compliance is required.",
|
|
19
|
+
"You are READ-ONLY: you MUST NOT modify any file. Diagnose and advise; do NOT change code.",
|
|
20
|
+
"</constraints>",
|
|
21
|
+
"",
|
|
22
|
+
PRINCIPLES_BLOCK,
|
|
23
|
+
"",
|
|
24
|
+
TOOLS_BLOCK,
|
|
25
|
+
"",
|
|
26
|
+
"<task>",
|
|
27
|
+
"- Verify every claim with tool calls — read the actual code. Never reason from memory about this codebase.",
|
|
28
|
+
"- Generate multiple competing hypotheses or approaches before converging. Surface and question hidden assumptions.",
|
|
29
|
+
"- Scope recommendations by effort: name the quick fix vs the thorough one.",
|
|
30
|
+
"- Structure your answer: Diagnosis (what is actually true, with file:line evidence) → Options & tradeoffs → Recommendation.",
|
|
31
|
+
"- Be honest about uncertainty. If evidence is thin, say so and state what would resolve it.",
|
|
32
|
+
"</task>",
|
|
33
|
+
].join("\n"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VariantConfig } from "../config.js";
|
|
2
|
-
import { loadAllContextFiles } from "../context.js";
|
|
2
|
+
import { loadAllContextFiles, formatManifestBlock } from "../context.js";
|
|
3
3
|
import { resolveModel, getModelInfo } from "../model-registry.js";
|
|
4
4
|
import type { RepoInfo } from "../repo-utils.js";
|
|
5
5
|
import { buildRepoContext } from "./repo-context.js";
|
|
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
|
|
|
8
8
|
export function createBrainstormReviewerAgent(
|
|
9
9
|
variant: string,
|
|
10
10
|
variants: Record<string, VariantConfig>,
|
|
11
|
-
taskArtifacts: { userRequest: string; research: string; artifacts?: { name: string; content: string }[] },
|
|
11
|
+
taskArtifacts: { userRequest: string; research: string; artifacts?: { name: string; content: string }[]; manifest?: { title: string; path: string }[] },
|
|
12
12
|
outputPath: string,
|
|
13
13
|
contextDirs: string[],
|
|
14
14
|
phase?: string,
|
|
@@ -64,10 +64,10 @@ export function createBrainstormReviewerAgent(
|
|
|
64
64
|
"- INACCURACIES: (claims that don't match the code)",
|
|
65
65
|
"- SUGGESTIONS: (improvements, not required)",
|
|
66
66
|
"",
|
|
67
|
-
"
|
|
68
|
-
'- Agent(subagent_type="
|
|
69
|
-
'- Agent(subagent_type="
|
|
70
|
-
"Spawn multiple
|
|
67
|
+
"You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
|
|
68
|
+
'- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
|
|
69
|
+
'- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
|
|
70
|
+
"Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
|
|
71
71
|
"</task>",
|
|
72
72
|
"",
|
|
73
73
|
"# MANDATORY: Write your review to this exact file using the write tool:",
|
|
@@ -88,7 +88,7 @@ export function createBrainstormReviewerAgent(
|
|
|
88
88
|
: []),
|
|
89
89
|
...(repoContext ? [repoContext] : []),
|
|
90
90
|
"",
|
|
91
|
-
|
|
91
|
+
formatManifestBlock(taskArtifacts.manifest ?? []),
|
|
92
92
|
].join("\n"),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VariantConfig } from "../config.js";
|
|
2
|
-
import { loadAllContextFiles } from "../context.js";
|
|
2
|
+
import { loadAllContextFiles, formatManifestBlock } from "../context.js";
|
|
3
3
|
import { resolveModel, getModelInfo } from "../model-registry.js";
|
|
4
4
|
import type { RepoInfo } from "../repo-utils.js";
|
|
5
5
|
import { buildRepoContext } from "./repo-context.js";
|
|
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
|
|
|
8
8
|
export function createCodeReviewerAgent(
|
|
9
9
|
variant: string,
|
|
10
10
|
variants: Record<string, VariantConfig>,
|
|
11
|
-
taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string },
|
|
11
|
+
taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string; manifest?: { title: string; path: string }[] },
|
|
12
12
|
outputPath: string,
|
|
13
13
|
contextDirs: string[],
|
|
14
14
|
phase?: string,
|
|
@@ -81,10 +81,10 @@ export function createCodeReviewerAgent(
|
|
|
81
81
|
"- MINOR: (nice to have)",
|
|
82
82
|
"- OPEN QUESTIONS: (low-confidence concerns, speculative follow-ups)",
|
|
83
83
|
"",
|
|
84
|
-
"
|
|
85
|
-
'- Agent(subagent_type="
|
|
86
|
-
'- Agent(subagent_type="
|
|
87
|
-
"Spawn multiple
|
|
84
|
+
"You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
|
|
85
|
+
'- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
|
|
86
|
+
'- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
|
|
87
|
+
"Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
|
|
88
88
|
"</task>",
|
|
89
89
|
"",
|
|
90
90
|
// --- dynamic suffix ---
|
|
@@ -101,7 +101,7 @@ export function createCodeReviewerAgent(
|
|
|
101
101
|
taskArtifacts.synthesizedPlan,
|
|
102
102
|
...(repoContext ? [repoContext] : []),
|
|
103
103
|
"",
|
|
104
|
-
|
|
104
|
+
formatManifestBlock(taskArtifacts.manifest ?? []),
|
|
105
105
|
].join("\n"),
|
|
106
106
|
};
|
|
107
107
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { completionLine, constraintsBlock } from "./constraints.js";
|
|
3
|
+
|
|
4
|
+
describe("completionLine", () => {
|
|
5
|
+
it("guided plan and implement instruct calling pp_phase_complete on completion", () => {
|
|
6
|
+
for (const phase of ["plan", "implement"] as const) {
|
|
7
|
+
const line = completionLine(phase, "guided");
|
|
8
|
+
expect(line).toContain("call pp_phase_complete");
|
|
9
|
+
expect(line).not.toContain("Do NOT advance on your own");
|
|
10
|
+
expect(line).not.toMatch(/do not.*wait for.*input/i);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("guided review and debug stay hands-off (no unprompted self-complete)", () => {
|
|
15
|
+
for (const phase of ["review", "debug"] as const) {
|
|
16
|
+
const line = completionLine(phase, "guided");
|
|
17
|
+
expect(line).toContain("Do NOT advance on your own or call pp_phase_complete unprompted");
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("guided brainstorm never self-completes", () => {
|
|
22
|
+
const line = completionLine("brainstorm", "guided");
|
|
23
|
+
expect(line).toContain("Do NOT call pp_phase_complete yourself");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("autonomous phases always self-complete regardless of phase", () => {
|
|
27
|
+
for (const phase of ["plan", "implement", "review", "brainstorm"] as const) {
|
|
28
|
+
expect(completionLine(phase, "autonomous")).toContain("call pp_phase_complete");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("constraintsBlock", () => {
|
|
34
|
+
it("guided implement block tells the agent to self-complete", () => {
|
|
35
|
+
const block = constraintsBlock("implement", "guided");
|
|
36
|
+
expect(block).toContain("call pp_phase_complete");
|
|
37
|
+
expect(block).not.toContain("Do NOT advance on your own");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("guided plan block tells the agent to self-complete", () => {
|
|
41
|
+
const block = constraintsBlock("plan", "guided");
|
|
42
|
+
expect(block).toContain("call pp_phase_complete");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -29,6 +29,9 @@ export function completionLine(phase: Phase, mode: TaskMode): string {
|
|
|
29
29
|
if (phase === "brainstorm") {
|
|
30
30
|
return "This is a conversation. Do NOT call pp_phase_complete yourself — keep going until the user ends it or advances via the /pp menu.";
|
|
31
31
|
}
|
|
32
|
+
if (phase === "plan" || phase === "implement") {
|
|
33
|
+
return "When you judge this phase complete, call pp_phase_complete — the extension opens the advance gate for the user to review and confirm. Do NOT instead stop and ask the user to run /pp manually.";
|
|
34
|
+
}
|
|
32
35
|
return "When the work is complete, stop and let the user review and advance it via the /pp menu. Do NOT advance on your own or call pp_phase_complete unprompted.";
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PiPiConfig } from "../config.js";
|
|
2
|
+
import { resolveModel } from "../model-registry.js";
|
|
3
|
+
import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
|
|
4
|
+
|
|
5
|
+
export function createDeepDebuggerAgent(config: PiPiConfig) {
|
|
6
|
+
return {
|
|
7
|
+
frontmatter: {
|
|
8
|
+
description: "Deep root-cause analysis for HARD, persistent failures — not every error (pi-pi)",
|
|
9
|
+
tools: `read, write, edit, bash, grep, find, ls, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
|
|
10
|
+
model: resolveModel(config.agents.subagents.simple["deep-debugger"].model),
|
|
11
|
+
thinking: config.agents.subagents.simple["deep-debugger"].thinking,
|
|
12
|
+
max_turns: 120,
|
|
13
|
+
prompt_mode: "replace",
|
|
14
|
+
},
|
|
15
|
+
prompt: [
|
|
16
|
+
"<constraints>",
|
|
17
|
+
"You are a DEEP DEBUGGER. You do root-cause analysis on hard, persistent failures — failing tests, build/compile errors, regressions, flaky behavior — that quick attempts have NOT resolved. Do NOT engage for trivial or first-attempt errors.",
|
|
18
|
+
"These rules override your default helpfulness. Strict compliance is required.",
|
|
19
|
+
"You have write/edit access for DIAGNOSIS ONLY: creating repro scripts, adding temporary logging, or running experiments. You MUST NOT write the actual fix in the source code — find the root cause and recommend the fix; do NOT apply it. Remove any temporary diagnostic artifacts you create.",
|
|
20
|
+
"</constraints>",
|
|
21
|
+
"",
|
|
22
|
+
PRINCIPLES_BLOCK,
|
|
23
|
+
"",
|
|
24
|
+
TOOLS_BLOCK,
|
|
25
|
+
"",
|
|
26
|
+
"<task>",
|
|
27
|
+
"- Reproduce/inspect first: run the failing command, read the actual error and stack trace, check recent changes (git diff, cbm_changes).",
|
|
28
|
+
"- Form competing hypotheses. For each, gather evidence FOR and AGAINST with tool calls. Do not commit to the first plausible cause.",
|
|
29
|
+
"- Trace the failure to its true root, not the surface symptom. Use lsp findReferences / cbm_trace to follow the chain.",
|
|
30
|
+
"- Report: Symptom → Hypotheses considered (with evidence) → Root cause (with file:line proof) → Minimal recommended fix.",
|
|
31
|
+
"- If you cannot prove the root cause, say so: report the narrowed-down suspects and the single most useful next probe.",
|
|
32
|
+
"</task>",
|
|
33
|
+
].join("\n"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VariantConfig } from "../config.js";
|
|
2
|
-
import { loadAllContextFiles } from "../context.js";
|
|
2
|
+
import { loadAllContextFiles, formatManifestBlock } from "../context.js";
|
|
3
3
|
import { resolveModel, getModelInfo } from "../model-registry.js";
|
|
4
4
|
import type { RepoInfo } from "../repo-utils.js";
|
|
5
5
|
import { buildRepoContext } from "./repo-context.js";
|
|
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
|
|
|
8
8
|
export function createPlanReviewerAgent(
|
|
9
9
|
variant: string,
|
|
10
10
|
variants: Record<string, VariantConfig>,
|
|
11
|
-
taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string },
|
|
11
|
+
taskArtifacts: { userRequest: string; research: string; synthesizedPlan: string; manifest?: { title: string; path: string }[] },
|
|
12
12
|
outputPath: string,
|
|
13
13
|
contextDirs: string[],
|
|
14
14
|
phase?: string,
|
|
@@ -63,10 +63,10 @@ export function createPlanReviewerAgent(
|
|
|
63
63
|
"- BLOCKERS: (critical issues that must be fixed)",
|
|
64
64
|
"- SUGGESTIONS: (improvements, not required)",
|
|
65
65
|
"",
|
|
66
|
-
"
|
|
67
|
-
'- Agent(subagent_type="
|
|
68
|
-
'- Agent(subagent_type="
|
|
69
|
-
"Spawn multiple
|
|
66
|
+
"You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
|
|
67
|
+
'- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
|
|
68
|
+
'- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
|
|
69
|
+
"Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
|
|
70
70
|
"</task>",
|
|
71
71
|
"",
|
|
72
72
|
// --- dynamic suffix ---
|
|
@@ -87,7 +87,7 @@ export function createPlanReviewerAgent(
|
|
|
87
87
|
taskArtifacts.synthesizedPlan,
|
|
88
88
|
...(repoContext ? [repoContext] : []),
|
|
89
89
|
"",
|
|
90
|
-
|
|
90
|
+
formatManifestBlock(taskArtifacts.manifest ?? []),
|
|
91
91
|
].join("\n"),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VariantConfig } from "../config.js";
|
|
2
|
-
import { loadAllContextFiles } from "../context.js";
|
|
2
|
+
import { loadAllContextFiles, formatManifestBlock } from "../context.js";
|
|
3
3
|
import { resolveModel, getModelInfo } from "../model-registry.js";
|
|
4
4
|
import type { RepoInfo } from "../repo-utils.js";
|
|
5
5
|
import { buildRepoContext } from "./repo-context.js";
|
|
@@ -8,7 +8,7 @@ import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-
|
|
|
8
8
|
export function createPlannerAgent(
|
|
9
9
|
variant: string,
|
|
10
10
|
variants: Record<string, VariantConfig>,
|
|
11
|
-
taskArtifacts: { userRequest: string; research: string },
|
|
11
|
+
taskArtifacts: { userRequest: string; research: string; manifest?: { title: string; path: string }[] },
|
|
12
12
|
outputPath: string,
|
|
13
13
|
contextDirs: string[],
|
|
14
14
|
phase?: string,
|
|
@@ -51,14 +51,15 @@ export function createPlannerAgent(
|
|
|
51
51
|
"- ## Scope: 2-4 lines — what changes, what doesn't, critical constraints",
|
|
52
52
|
"- ## Checklist: each item is - [ ] <outcome> — Done when: <observable condition>",
|
|
53
53
|
" Each item = one independently verifiable outcome. No code snippets or file-by-file instructions.",
|
|
54
|
+
"- ## Pattern constraints: include whenever the task adds a type, function, parser, annotation, config key, enum, or user-facing value. For each, name the CLOSEST EXISTING analog (found by behavior, not filename) and the conventions the implementer MUST mirror: data shape (prefer one existing shape over parallel/duplicated state), spelling/casing of user-facing values (match existing — never invent a new casing), and parser/validation/error-handling shape. Acceptance criteria, not suggestions. Omit only if the task adds none of the above.",
|
|
54
55
|
"- ## Blockers: unresolved issues blocking implementation (omit if none)",
|
|
55
56
|
"- No other top-level sections allowed",
|
|
56
|
-
"- Describe outcomes, not code-level mechanics",
|
|
57
|
+
"- Describe outcomes, not code-level mechanics, EXCEPT in ## Pattern constraints where the concrete analog and conventions are required",
|
|
57
58
|
"",
|
|
58
|
-
"
|
|
59
|
-
'- Agent(subagent_type="
|
|
60
|
-
'- Agent(subagent_type="
|
|
61
|
-
"Spawn multiple
|
|
59
|
+
"You may spawn ONLY explore/librarian subagents (subagent_type is REQUIRED — calls without it are rejected):",
|
|
60
|
+
'- Agent(subagent_type="explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
|
|
61
|
+
'- Agent(subagent_type="librarian", ...) — external docs, library APIs, web research.',
|
|
62
|
+
"Spawn multiple explore agents in parallel for broad searches. Do NOT spawn task, advisor, deep-debugger, or reviewer.",
|
|
62
63
|
"</task>",
|
|
63
64
|
"",
|
|
64
65
|
// --- dynamic suffix ---
|
|
@@ -74,7 +75,7 @@ export function createPlannerAgent(
|
|
|
74
75
|
taskArtifacts.research,
|
|
75
76
|
...(repoContext ? [repoContext] : []),
|
|
76
77
|
"",
|
|
77
|
-
|
|
78
|
+
formatManifestBlock(taskArtifacts.manifest ?? []),
|
|
78
79
|
].join("\n"),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getDefaultConfig, resolvePreset } from "../config.js";
|
|
3
|
+
import { DELEGATION_BLOCK } from "./tool-routing.js";
|
|
4
|
+
import { createAdvisorAgent } from "./advisor.js";
|
|
5
|
+
import { createDeepDebuggerAgent } from "./deep-debugger.js";
|
|
6
|
+
import { createReviewerAgent } from "./reviewer.js";
|
|
7
|
+
import { createTaskAgent } from "./task.js";
|
|
8
|
+
import { createPlannerAgent } from "./planner.js";
|
|
9
|
+
import { createPlanReviewerAgent } from "./plan-reviewer.js";
|
|
10
|
+
import { createCodeReviewerAgent } from "./code-reviewer.js";
|
|
11
|
+
import { createBrainstormReviewerAgent } from "./brainstorm-reviewer.js";
|
|
12
|
+
|
|
13
|
+
const config = getDefaultConfig();
|
|
14
|
+
|
|
15
|
+
describe("DELEGATION_BLOCK", () => {
|
|
16
|
+
it("covers all six free-form agents with lowercase registry names", () => {
|
|
17
|
+
for (const name of ["explore", "librarian", "task", "advisor", "deep-debugger", "reviewer"]) {
|
|
18
|
+
expect(DELEGATION_BLOCK).toContain(name);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("states the reviewer and deep-debugger gating explicitly", () => {
|
|
23
|
+
expect(DELEGATION_BLOCK).toContain("ONLY when the user explicitly asks");
|
|
24
|
+
expect(DELEGATION_BLOCK).toMatch(/deep-debugger diagnoses/i);
|
|
25
|
+
expect(DELEGATION_BLOCK).toContain("must NOT write the actual fix");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("new free-form agent factories", () => {
|
|
30
|
+
it("advisor is read-only (no write/edit) and reasons in Diagnosis/Options/Recommendation", () => {
|
|
31
|
+
const a = createAdvisorAgent(config);
|
|
32
|
+
expect(a.frontmatter.tools).not.toContain("write");
|
|
33
|
+
expect(a.frontmatter.tools).not.toContain("edit");
|
|
34
|
+
expect(a.prompt).toContain("READ-ONLY");
|
|
35
|
+
expect(a.prompt).toContain("Diagnosis");
|
|
36
|
+
expect(a.prompt).toContain("Recommendation");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("deep-debugger has write/edit but restricts writes to diagnosis only", () => {
|
|
40
|
+
const d = createDeepDebuggerAgent(config);
|
|
41
|
+
expect(d.frontmatter.tools).toContain("write");
|
|
42
|
+
expect(d.frontmatter.tools).toContain("edit");
|
|
43
|
+
expect(d.prompt).toContain("DIAGNOSIS ONLY");
|
|
44
|
+
expect(d.prompt).toContain("MUST NOT write the actual fix");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reviewer is read-only, retains bash for git diff, and is verdict-first", () => {
|
|
48
|
+
const r = createReviewerAgent(config);
|
|
49
|
+
expect(r.frontmatter.tools).toContain("bash");
|
|
50
|
+
expect(r.frontmatter.tools).not.toContain("write");
|
|
51
|
+
expect(r.frontmatter.tools).not.toContain("edit");
|
|
52
|
+
expect(r.prompt).toContain("git diff");
|
|
53
|
+
expect(r.prompt).toContain("VERY FIRST LINE");
|
|
54
|
+
expect(r.frontmatter.description).toContain("only when the user asks");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("task factory no longer bakes artifacts and stays explore/librarian-only", () => {
|
|
59
|
+
it("takes only config (no baked artifact arg) and does not inline USER REQUEST / SYNTHESIZED PLAN", () => {
|
|
60
|
+
expect(createTaskAgent.length).toBe(1);
|
|
61
|
+
const t = createTaskAgent(config);
|
|
62
|
+
expect(t.prompt).not.toContain("=== USER REQUEST ===");
|
|
63
|
+
expect(t.prompt).not.toContain("=== SYNTHESIZED PLAN ===");
|
|
64
|
+
expect(t.prompt).not.toContain("Do NOT re-read them from disk");
|
|
65
|
+
expect(t.prompt).toContain("ONLY explore/librarian");
|
|
66
|
+
expect(t.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("phased factory prompts: manifest guidance replaces the do-not-re-read trailer", () => {
|
|
71
|
+
const planners = resolvePreset(config, "planners");
|
|
72
|
+
const planReviewers = resolvePreset(config, "planReviewers");
|
|
73
|
+
const codeReviewers = resolvePreset(config, "codeReviewers");
|
|
74
|
+
const brainstormReviewers = resolvePreset(config, "brainstormReviewers");
|
|
75
|
+
const manifest = [{ title: "Design Doc", path: "/t/artifacts/design.md" }];
|
|
76
|
+
|
|
77
|
+
it("planner lists manifest paths and restricts spawns to explore/librarian", () => {
|
|
78
|
+
const p = createPlannerAgent("opus", planners, { userRequest: "u", research: "r", manifest }, "/out.md", []);
|
|
79
|
+
expect(p.prompt).toContain("/t/artifacts/design.md");
|
|
80
|
+
expect(p.prompt).toContain("read them from disk with the read tool");
|
|
81
|
+
expect(p.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("plan-reviewer lists manifest paths and restricts spawns", () => {
|
|
85
|
+
const p = createPlanReviewerAgent(
|
|
86
|
+
"opus",
|
|
87
|
+
planReviewers,
|
|
88
|
+
{ userRequest: "u", research: "r", synthesizedPlan: "p", manifest },
|
|
89
|
+
"/out.md",
|
|
90
|
+
[],
|
|
91
|
+
);
|
|
92
|
+
expect(p.prompt).toContain("/t/artifacts/design.md");
|
|
93
|
+
expect(p.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("code-reviewer lists manifest paths and restricts spawns", () => {
|
|
97
|
+
const c = createCodeReviewerAgent(
|
|
98
|
+
"opus",
|
|
99
|
+
codeReviewers,
|
|
100
|
+
{ userRequest: "u", research: "r", synthesizedPlan: "p", manifest },
|
|
101
|
+
"/out.md",
|
|
102
|
+
[],
|
|
103
|
+
);
|
|
104
|
+
expect(c.prompt).toContain("/t/artifacts/design.md");
|
|
105
|
+
expect(c.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("brainstorm-reviewer restricts spawns and lists manifest paths when provided", () => {
|
|
109
|
+
const b = createBrainstormReviewerAgent(
|
|
110
|
+
"opus",
|
|
111
|
+
brainstormReviewers,
|
|
112
|
+
{ userRequest: "u", research: "r", manifest },
|
|
113
|
+
"/out.md",
|
|
114
|
+
[],
|
|
115
|
+
);
|
|
116
|
+
expect(b.prompt).toContain("Do NOT spawn task, advisor, deep-debugger, or reviewer");
|
|
117
|
+
expect(b.prompt).toContain("/t/artifacts/design.md");
|
|
118
|
+
expect(b.prompt).toContain("read them from disk with the read tool");
|
|
119
|
+
});
|
|
120
|
+
});
|