@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
package/3p/pi-ask-user/index.ts
CHANGED
|
@@ -111,15 +111,35 @@ type AskResponse =
|
|
|
111
111
|
text: string;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
// Reason a question was cancelled. Only "user" (a deliberate top-level ESC)
|
|
115
|
+
// should abort the LLM turn; "timeout" and "signal" are programmatic and must not.
|
|
116
|
+
type CancelReason = "user" | "timeout" | "signal";
|
|
117
|
+
|
|
118
|
+
// Sentinel returned through the UI/askUser boundary to carry a cancel reason.
|
|
119
|
+
// Distinct from a plain AskResponse so callers can disambiguate cancel vs answer.
|
|
120
|
+
interface AskCancel {
|
|
121
|
+
__cancel: true;
|
|
122
|
+
reason: CancelReason;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function makeCancel(reason: CancelReason): AskCancel {
|
|
126
|
+
return { __cancel: true, reason };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isCancel(value: unknown): value is AskCancel {
|
|
130
|
+
return typeof value === "object" && value !== null && (value as AskCancel).__cancel === true;
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
interface AskToolDetails {
|
|
115
134
|
question: string;
|
|
116
135
|
context?: string;
|
|
117
136
|
options: QuestionOption[];
|
|
118
137
|
response: AskResponse | null;
|
|
119
138
|
cancelled: boolean;
|
|
139
|
+
cancelReason?: CancelReason;
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
type AskUIResult = AskResponse;
|
|
142
|
+
type AskUIResult = AskResponse | AskCancel;
|
|
123
143
|
|
|
124
144
|
function normalizeOptions(options: AskOptionInput[]): QuestionOption[] {
|
|
125
145
|
return options
|
|
@@ -983,7 +1003,6 @@ class WrappedSingleSelectList implements Component {
|
|
|
983
1003
|
*/
|
|
984
1004
|
class AskComponent extends Container {
|
|
985
1005
|
private question: string;
|
|
986
|
-
private context?: string;
|
|
987
1006
|
private options: QuestionOption[];
|
|
988
1007
|
private allowMultiple: boolean;
|
|
989
1008
|
private allowFreeform: boolean;
|
|
@@ -1003,7 +1022,6 @@ class AskComponent extends Container {
|
|
|
1003
1022
|
// Static layout components
|
|
1004
1023
|
private titleText: Text;
|
|
1005
1024
|
private questionText: Text;
|
|
1006
|
-
private contextComponent?: Component;
|
|
1007
1025
|
private modeContainer: Container;
|
|
1008
1026
|
private helpText: Text;
|
|
1009
1027
|
|
|
@@ -1026,7 +1044,7 @@ class AskComponent extends Container {
|
|
|
1026
1044
|
|
|
1027
1045
|
constructor(
|
|
1028
1046
|
question: string,
|
|
1029
|
-
|
|
1047
|
+
_context: string | undefined,
|
|
1030
1048
|
options: QuestionOption[],
|
|
1031
1049
|
allowMultiple: boolean,
|
|
1032
1050
|
allowFreeform: boolean,
|
|
@@ -1041,7 +1059,6 @@ class AskComponent extends Container {
|
|
|
1041
1059
|
super();
|
|
1042
1060
|
|
|
1043
1061
|
this.question = question;
|
|
1044
|
-
this.context = context;
|
|
1045
1062
|
this.options = options;
|
|
1046
1063
|
this.allowMultiple = allowMultiple;
|
|
1047
1064
|
this.allowFreeform = allowFreeform;
|
|
@@ -1067,17 +1084,9 @@ class AskComponent extends Container {
|
|
|
1067
1084
|
|
|
1068
1085
|
this.questionText = new Text("", 1, 0);
|
|
1069
1086
|
this.addChild(this.questionText);
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const mdTheme = safeMarkdownTheme();
|
|
1074
|
-
if (mdTheme) {
|
|
1075
|
-
this.contextComponent = new Markdown("", 1, 0, mdTheme);
|
|
1076
|
-
} else {
|
|
1077
|
-
this.contextComponent = new Text("", 1, 0);
|
|
1078
|
-
}
|
|
1079
|
-
this.addChild(this.contextComponent);
|
|
1080
|
-
}
|
|
1087
|
+
// The interactive dialogue intentionally omits a separate context block:
|
|
1088
|
+
// the model output rendered above carries the detail. Only a short, dimmed
|
|
1089
|
+
// question line is shown here to keep the prompt scannable (see LOCKED #2).
|
|
1081
1090
|
|
|
1082
1091
|
this.addChild(new Spacer(1));
|
|
1083
1092
|
|
|
@@ -1140,29 +1149,19 @@ class AskComponent extends Container {
|
|
|
1140
1149
|
private countStaticLines(width: number): number {
|
|
1141
1150
|
const titleLines = 1;
|
|
1142
1151
|
const questionLines = this.countWrappedLines(this.question, width);
|
|
1143
|
-
const contextLines = this.context ? 1 + this.countWrappedLines(this.context, width) : 0;
|
|
1144
1152
|
const helpLines = 1;
|
|
1145
1153
|
const borderLines = 2;
|
|
1146
|
-
const spacerLines =
|
|
1147
|
-
return borderLines + spacerLines + titleLines + questionLines +
|
|
1154
|
+
const spacerLines = 5;
|
|
1155
|
+
return borderLines + spacerLines + titleLines + questionLines + helpLines;
|
|
1148
1156
|
}
|
|
1149
1157
|
|
|
1150
1158
|
private updateStaticText(): void {
|
|
1151
1159
|
const theme = this.theme;
|
|
1152
1160
|
const title = this.mode === "comment" ? "Optional comment" : "Question";
|
|
1153
1161
|
this.titleText.setText(theme.fg("accent", theme.bold(title)));
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
(this.contextComponent as Markdown).setText(
|
|
1158
|
-
`**Context:**\n${this.context}`,
|
|
1159
|
-
);
|
|
1160
|
-
} else {
|
|
1161
|
-
(this.contextComponent as Text).setText(
|
|
1162
|
-
`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`,
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1162
|
+
// Dimmed, non-bold so it stays visually subordinate to the "Question" title
|
|
1163
|
+
// (accent+bold) above and the richer model output rendered before the dialogue.
|
|
1164
|
+
this.questionText.setText(theme.fg("dim", this.question));
|
|
1166
1165
|
}
|
|
1167
1166
|
|
|
1168
1167
|
private updateHelpText(): void {
|
|
@@ -1171,7 +1170,7 @@ class AskComponent extends Container {
|
|
|
1171
1170
|
? literalHint(theme, this.shortcuts.overlayToggle.spec, "hide")
|
|
1172
1171
|
: null;
|
|
1173
1172
|
const commentHint = this.allowComment && !this.shortcuts.commentToggle.disabled
|
|
1174
|
-
? literalHint(theme, this.shortcuts.commentToggle.spec, "
|
|
1173
|
+
? literalHint(theme, this.shortcuts.commentToggle.spec, "add text")
|
|
1175
1174
|
: null;
|
|
1176
1175
|
if (this.mode === "freeform" || this.mode === "comment") {
|
|
1177
1176
|
const alternateCancelKeys = this.keybindings
|
|
@@ -1236,7 +1235,7 @@ class AskComponent extends Container {
|
|
|
1236
1235
|
this.shortcuts.commentToggle,
|
|
1237
1236
|
);
|
|
1238
1237
|
list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
|
|
1239
|
-
list.onCancel = () => this.onDone(
|
|
1238
|
+
list.onCancel = () => this.onDone(makeCancel("user"));
|
|
1240
1239
|
list.onEnterFreeform = () => this.showFreeformMode();
|
|
1241
1240
|
|
|
1242
1241
|
this.singleSelectList = list;
|
|
@@ -1254,7 +1253,7 @@ class AskComponent extends Container {
|
|
|
1254
1253
|
this.keybindings,
|
|
1255
1254
|
this.shortcuts.commentToggle,
|
|
1256
1255
|
);
|
|
1257
|
-
list.onCancel = () => this.onDone(
|
|
1256
|
+
list.onCancel = () => this.onDone(makeCancel("user"));
|
|
1258
1257
|
list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
|
|
1259
1258
|
list.onEnterFreeform = () => this.showFreeformMode();
|
|
1260
1259
|
|
|
@@ -1389,7 +1388,7 @@ class AskComponent extends Container {
|
|
|
1389
1388
|
}
|
|
1390
1389
|
|
|
1391
1390
|
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
1392
|
-
this.onDone(
|
|
1391
|
+
this.onDone(makeCancel("user"));
|
|
1393
1392
|
return;
|
|
1394
1393
|
}
|
|
1395
1394
|
|
|
@@ -1490,14 +1489,14 @@ export async function askUser(
|
|
|
1490
1489
|
overlayToggleKey?: string | null;
|
|
1491
1490
|
commentToggleKey?: string | null;
|
|
1492
1491
|
},
|
|
1493
|
-
): Promise<AskResponse | null> {
|
|
1492
|
+
): Promise<AskResponse | AskCancel | null> {
|
|
1494
1493
|
const {
|
|
1495
1494
|
question,
|
|
1496
1495
|
context,
|
|
1497
1496
|
options: rawOptions = [],
|
|
1498
1497
|
allowMultiple = false,
|
|
1499
1498
|
allowFreeform = true,
|
|
1500
|
-
allowComment =
|
|
1499
|
+
allowComment = true,
|
|
1501
1500
|
timeout,
|
|
1502
1501
|
signal,
|
|
1503
1502
|
overlay,
|
|
@@ -1544,11 +1543,11 @@ export async function askUser(
|
|
|
1544
1543
|
try {
|
|
1545
1544
|
const customFactory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskUIResult | null) => void) => {
|
|
1546
1545
|
if (signal) {
|
|
1547
|
-
const onAbort = () => done(
|
|
1546
|
+
const onAbort = () => done(makeCancel("signal"));
|
|
1548
1547
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1549
1548
|
}
|
|
1550
1549
|
if (timeout && timeout > 0) {
|
|
1551
|
-
setTimeout(() => done(
|
|
1550
|
+
setTimeout(() => done(makeCancel("timeout")), timeout);
|
|
1552
1551
|
}
|
|
1553
1552
|
return new AskComponent(
|
|
1554
1553
|
question,
|
|
@@ -1604,11 +1603,12 @@ export default function(pi: ExtensionAPI) {
|
|
|
1604
1603
|
name: "ask_user",
|
|
1605
1604
|
label: "Ask User",
|
|
1606
1605
|
description:
|
|
1607
|
-
"Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call.
|
|
1606
|
+
"Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Keep the `question` SHORT — one scannable line; put any reasoning, findings, or detail in your message BEFORE the call, not in the question itself.",
|
|
1608
1607
|
promptSnippet:
|
|
1609
|
-
"Ask the user one focused question with optional multiple-choice answers to gather information interactively",
|
|
1608
|
+
"Ask the user one short, focused question with optional multiple-choice answers to gather information interactively",
|
|
1610
1609
|
promptGuidelines: [
|
|
1611
|
-
"
|
|
1610
|
+
"Keep the `question` field SHORT — a single scannable line (ideally under ~100 chars). The dialogue de-emphasizes it; the user reads your detail from the message rendered above the dialogue.",
|
|
1611
|
+
"Put context, reasoning, and findings in your assistant message BEFORE calling ask_user — do NOT pack them into the `question` field.",
|
|
1612
1612
|
"Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
|
|
1613
1613
|
"Ask exactly one focused question per ask_user call.",
|
|
1614
1614
|
"Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
|
|
@@ -1618,7 +1618,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1618
1618
|
// (potentially with side effects) before the user sees the prompt.
|
|
1619
1619
|
executionMode: "sequential",
|
|
1620
1620
|
parameters: Type.Object({
|
|
1621
|
-
question: Type.String({ description: "The question to ask the user" }),
|
|
1621
|
+
question: Type.String({ description: "The question to ask the user. Keep it SHORT — one scannable line. Put detail/reasoning in your message above the dialogue, not here." }),
|
|
1622
1622
|
context: Type.Optional(
|
|
1623
1623
|
Type.String({
|
|
1624
1624
|
description: "Relevant context to show before the question (summary of findings)",
|
|
@@ -1645,7 +1645,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1645
1645
|
Type.Boolean({ description: "Add a freeform text option. Default: true" }),
|
|
1646
1646
|
),
|
|
1647
1647
|
allowComment: Type.Optional(
|
|
1648
|
-
Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default:
|
|
1648
|
+
Type.Boolean({ description: "Collect an optional comment to append after selecting one or more options. Default: true" }),
|
|
1649
1649
|
),
|
|
1650
1650
|
displayMode: Type.Optional(
|
|
1651
1651
|
StringEnum(["overlay", "inline"] as const, {
|
|
@@ -1683,7 +1683,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1683
1683
|
options: rawOptions = [],
|
|
1684
1684
|
allowMultiple = false,
|
|
1685
1685
|
allowFreeform = true,
|
|
1686
|
-
allowComment =
|
|
1686
|
+
allowComment = true,
|
|
1687
1687
|
displayMode,
|
|
1688
1688
|
overlayToggleKey,
|
|
1689
1689
|
commentToggleKey,
|
|
@@ -1712,7 +1712,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1712
1712
|
});
|
|
1713
1713
|
}
|
|
1714
1714
|
|
|
1715
|
-
let result: AskResponse | null;
|
|
1715
|
+
let result: AskResponse | AskCancel | null;
|
|
1716
1716
|
try {
|
|
1717
1717
|
result = await askUser(ctx, {
|
|
1718
1718
|
question,
|
|
@@ -1737,11 +1737,27 @@ export default function(pi: ExtensionAPI) {
|
|
|
1737
1737
|
};
|
|
1738
1738
|
}
|
|
1739
1739
|
|
|
1740
|
-
if (result === null) {
|
|
1741
|
-
|
|
1740
|
+
if (result === null || isCancel(result)) {
|
|
1741
|
+
// Only a deliberate top-level user ESC carries reason "user"; timeout and
|
|
1742
|
+
// programmatic signal aborts must NOT abort the LLM turn. A bare null
|
|
1743
|
+
// (e.g. empty submit) is treated as non-user.
|
|
1744
|
+
const cancelReason: CancelReason | undefined = isCancel(result) ? result.reason : undefined;
|
|
1745
|
+
pi.events.emit("ask:cancelled", {
|
|
1746
|
+
question,
|
|
1747
|
+
context: normalizedContext,
|
|
1748
|
+
options,
|
|
1749
|
+
reason: cancelReason,
|
|
1750
|
+
});
|
|
1742
1751
|
return {
|
|
1743
1752
|
content: [{ type: "text" as const, text: "User cancelled the question" }],
|
|
1744
|
-
details: {
|
|
1753
|
+
details: {
|
|
1754
|
+
question,
|
|
1755
|
+
context: normalizedContext,
|
|
1756
|
+
options,
|
|
1757
|
+
response: null,
|
|
1758
|
+
cancelled: true,
|
|
1759
|
+
cancelReason,
|
|
1760
|
+
} as AskToolDetails,
|
|
1745
1761
|
};
|
|
1746
1762
|
}
|
|
1747
1763
|
|
|
@@ -47,6 +47,8 @@ interface SpawnOptions {
|
|
|
47
47
|
onTurnEnd?: (turnCount: number) => void;
|
|
48
48
|
validateCompletion?: () => string | undefined;
|
|
49
49
|
maxValidationRetries?: number;
|
|
50
|
+
/** toolCallId of the parent tool call that spawned this subagent (for trace correlation). */
|
|
51
|
+
toolCallId?: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export class AgentManager {
|
|
@@ -120,6 +122,7 @@ export class AgentManager {
|
|
|
120
122
|
private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
|
|
121
123
|
record.status = "running";
|
|
122
124
|
record.startedAt = Date.now();
|
|
125
|
+
if (options.toolCallId && !record.toolCallId) record.toolCallId = options.toolCallId;
|
|
123
126
|
if (options.isBackground) this.runningBackground++;
|
|
124
127
|
this.onStart?.(record);
|
|
125
128
|
|
|
@@ -156,6 +159,10 @@ export class AgentManager {
|
|
|
156
159
|
onTextDelta: options.onTextDelta,
|
|
157
160
|
validateCompletion: options.validateCompletion,
|
|
158
161
|
maxValidationRetries: options.maxValidationRetries,
|
|
162
|
+
subagentId: id,
|
|
163
|
+
subagentType: type,
|
|
164
|
+
subagentDescription: options.description,
|
|
165
|
+
parentToolCallId: options.toolCallId ?? record.toolCallId,
|
|
159
166
|
onSessionCreated: (session) => {
|
|
160
167
|
record.session = session;
|
|
161
168
|
// Flush any steers that arrived before the session was ready
|
|
@@ -283,6 +290,7 @@ export class AgentManager {
|
|
|
283
290
|
if (activity.type === "end") record.toolUses++;
|
|
284
291
|
},
|
|
285
292
|
signal,
|
|
293
|
+
subagentId: id,
|
|
286
294
|
});
|
|
287
295
|
record.status = "completed";
|
|
288
296
|
record.result = responseText;
|
|
@@ -24,6 +24,72 @@ import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
|
24
24
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
25
25
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
26
26
|
|
|
27
|
+
const TRACER_KEY = Symbol.for("pi-pi:tracer");
|
|
28
|
+
const SUBAGENT_SESSION_KEY = Symbol.for("pi-pi:subagent-session");
|
|
29
|
+
|
|
30
|
+
interface PiPiTracer {
|
|
31
|
+
openSubagent(meta: {
|
|
32
|
+
subagentId: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
parentToolCallId?: string;
|
|
36
|
+
parentSubagentId?: string;
|
|
37
|
+
depth: number;
|
|
38
|
+
systemPrompt?: string;
|
|
39
|
+
effectivePrompt?: string;
|
|
40
|
+
}): void;
|
|
41
|
+
traceSubagent(subagentId: string, kind: string, payload: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getTracer(): PiPiTracer | undefined {
|
|
45
|
+
return (globalThis as any)[TRACER_KEY];
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
function traceSubagentEvent(subagentId: string | undefined, event: AgentSessionEvent, turnIndex: number): void {
|
|
54
|
+
if (!subagentId) return;
|
|
55
|
+
const tracer = getTracer();
|
|
56
|
+
if (!tracer) return;
|
|
57
|
+
try {
|
|
58
|
+
switch (event.type) {
|
|
59
|
+
case "agent_start":
|
|
60
|
+
tracer.traceSubagent(subagentId, "agent_start", {});
|
|
61
|
+
break;
|
|
62
|
+
case "agent_end":
|
|
63
|
+
tracer.traceSubagent(subagentId, "agent_end", { messages: (event as any).messages, willRetry: (event as any).willRetry });
|
|
64
|
+
break;
|
|
65
|
+
case "turn_start":
|
|
66
|
+
tracer.traceSubagent(subagentId, "turn_start", { turnIndex });
|
|
67
|
+
break;
|
|
68
|
+
case "turn_end":
|
|
69
|
+
tracer.traceSubagent(subagentId, "turn_end", { turnIndex, message: (event as any).message, toolResults: (event as any).toolResults });
|
|
70
|
+
break;
|
|
71
|
+
case "message_start":
|
|
72
|
+
tracer.traceSubagent(subagentId, "message_start", { turnIndex, message: (event as any).message });
|
|
73
|
+
break;
|
|
74
|
+
case "message_update":
|
|
75
|
+
tracer.traceSubagent(subagentId, "message_update", { turnIndex, assistantMessageEvent: (event as any).assistantMessageEvent });
|
|
76
|
+
break;
|
|
77
|
+
case "message_end":
|
|
78
|
+
tracer.traceSubagent(subagentId, "message_end", { turnIndex, message: (event as any).message });
|
|
79
|
+
break;
|
|
80
|
+
case "tool_execution_start":
|
|
81
|
+
tracer.traceSubagent(subagentId, "tool_execution_start", { turnIndex, toolCallId: (event as any).toolCallId, toolName: (event as any).toolName, args: (event as any).args });
|
|
82
|
+
break;
|
|
83
|
+
case "tool_execution_update":
|
|
84
|
+
tracer.traceSubagent(subagentId, "tool_execution_update", { turnIndex, toolCallId: (event as any).toolCallId, toolName: (event as any).toolName, args: (event as any).args, partialResult: (event as any).partialResult });
|
|
85
|
+
break;
|
|
86
|
+
case "tool_execution_end":
|
|
87
|
+
tracer.traceSubagent(subagentId, "tool_execution_end", { turnIndex, toolCallId: (event as any).toolCallId, toolName: (event as any).toolName, result: (event as any).result, isError: (event as any).isError });
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
28
94
|
let defaultMaxTurns: number | undefined;
|
|
29
95
|
|
|
@@ -109,6 +175,11 @@ export interface RunOptions {
|
|
|
109
175
|
*/
|
|
110
176
|
validateCompletion?: () => string | undefined;
|
|
111
177
|
maxValidationRetries?: number;
|
|
178
|
+
/** Correlation IDs for session tracing. */
|
|
179
|
+
subagentId?: string;
|
|
180
|
+
subagentType?: string;
|
|
181
|
+
subagentDescription?: string;
|
|
182
|
+
parentToolCallId?: string;
|
|
112
183
|
}
|
|
113
184
|
|
|
114
185
|
export interface RunResult {
|
|
@@ -171,6 +242,8 @@ export async function runAgent(
|
|
|
171
242
|
|
|
172
243
|
// Resolve working directory: worktree override > parent cwd
|
|
173
244
|
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.
|
|
174
247
|
const subagentSessionKey = Symbol.for("pi-pi:subagent-session");
|
|
175
248
|
const previousSubagentSession = (globalThis as any)[subagentSessionKey];
|
|
176
249
|
const previousDepth = typeof previousSubagentSession === "object" && previousSubagentSession !== null
|
|
@@ -178,7 +251,10 @@ export async function runAgent(
|
|
|
178
251
|
: previousSubagentSession
|
|
179
252
|
? 1
|
|
180
253
|
: 0;
|
|
181
|
-
const
|
|
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 };
|
|
182
258
|
(globalThis as any)[subagentSessionKey] = subagentSessionState;
|
|
183
259
|
|
|
184
260
|
try {
|
|
@@ -332,16 +408,7 @@ export async function runAgent(
|
|
|
332
408
|
|
|
333
409
|
let currentMessageText = "";
|
|
334
410
|
const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
if (event.type === "agent_start") {
|
|
338
|
-
}
|
|
339
|
-
if (event.type === "turn_start") {
|
|
340
|
-
}
|
|
341
|
-
if (event.type === "message_start") {
|
|
342
|
-
}
|
|
343
|
-
if (event.type === "tool_execution_start") {
|
|
344
|
-
}
|
|
411
|
+
traceSubagentEvent(options.subagentId, event, turnCount);
|
|
345
412
|
if (event.type === "turn_end") {
|
|
346
413
|
turnCount++;
|
|
347
414
|
const msg = (event as any).message;
|
|
@@ -385,6 +452,21 @@ export async function runAgent(
|
|
|
385
452
|
}
|
|
386
453
|
}
|
|
387
454
|
|
|
455
|
+
if (options.subagentId) {
|
|
456
|
+
try {
|
|
457
|
+
getTracer()?.openSubagent({
|
|
458
|
+
subagentId: options.subagentId,
|
|
459
|
+
type: options.subagentType ?? type,
|
|
460
|
+
description: options.subagentDescription,
|
|
461
|
+
parentToolCallId: options.parentToolCallId,
|
|
462
|
+
parentSubagentId,
|
|
463
|
+
depth: subagentDepth(),
|
|
464
|
+
systemPrompt,
|
|
465
|
+
effectivePrompt,
|
|
466
|
+
});
|
|
467
|
+
} catch {}
|
|
468
|
+
}
|
|
469
|
+
|
|
388
470
|
try {
|
|
389
471
|
await session.prompt(effectivePrompt);
|
|
390
472
|
|
|
@@ -436,23 +518,34 @@ export async function runAgent(
|
|
|
436
518
|
export async function resumeAgent(
|
|
437
519
|
session: AgentSession,
|
|
438
520
|
prompt: string,
|
|
439
|
-
options: { onToolActivity?: (activity: ToolActivity) => void; signal?: AbortSignal } = {},
|
|
521
|
+
options: { onToolActivity?: (activity: ToolActivity) => void; signal?: AbortSignal; subagentId?: string } = {},
|
|
440
522
|
): Promise<string> {
|
|
441
523
|
const collector = collectResponseText(session);
|
|
442
524
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
443
525
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
526
|
+
// Seed from prior assistant turns so resumed turn indexes don't collide with
|
|
527
|
+
// the turns already written to this subagent's trace file.
|
|
528
|
+
let resumeTurnCount = session.messages.filter((m) => m.role === "assistant").length;
|
|
529
|
+
const unsubTrace = session.subscribe((event: AgentSessionEvent) => {
|
|
530
|
+
traceSubagentEvent(options.subagentId, event, resumeTurnCount);
|
|
531
|
+
if (event.type === "turn_end") resumeTurnCount++;
|
|
532
|
+
if (options.onToolActivity) {
|
|
533
|
+
if (event.type === "tool_execution_start") options.onToolActivity({ type: "start", toolName: event.toolName });
|
|
534
|
+
if (event.type === "tool_execution_end") options.onToolActivity({ type: "end", toolName: event.toolName });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (options.subagentId) {
|
|
539
|
+
try {
|
|
540
|
+
getTracer()?.traceSubagent(options.subagentId, "resume_start", { prompt });
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
450
543
|
|
|
451
544
|
try {
|
|
452
545
|
await session.prompt(prompt);
|
|
453
546
|
} finally {
|
|
454
547
|
collector.unsubscribe();
|
|
455
|
-
|
|
548
|
+
unsubTrace();
|
|
456
549
|
cleanupAbort();
|
|
457
550
|
}
|
|
458
551
|
|
|
@@ -397,6 +397,7 @@ Use get_subagent_result for full output.`,
|
|
|
397
397
|
durationMs,
|
|
398
398
|
modelId,
|
|
399
399
|
tokens,
|
|
400
|
+
toolCallId: record.toolCallId,
|
|
400
401
|
};
|
|
401
402
|
}
|
|
402
403
|
|
|
@@ -960,6 +961,7 @@ Guidelines:
|
|
|
960
961
|
thinkingLevel: thinking,
|
|
961
962
|
isBackground: true,
|
|
962
963
|
isolation,
|
|
964
|
+
toolCallId,
|
|
963
965
|
...bgCallbacks,
|
|
964
966
|
});
|
|
965
967
|
|
|
@@ -1069,6 +1071,7 @@ Guidelines:
|
|
|
1069
1071
|
inheritContext,
|
|
1070
1072
|
thinkingLevel: thinking,
|
|
1071
1073
|
isolation,
|
|
1074
|
+
toolCallId,
|
|
1072
1075
|
...fgCallbacks,
|
|
1073
1076
|
});
|
|
1074
1077
|
|
|
@@ -1,38 +1,51 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { VariantConfig } from "../config.js";
|
|
2
|
+
import { loadAllContextFiles } from "../context.js";
|
|
3
|
+
import { resolveModel, getModelInfo } from "../model-registry.js";
|
|
4
|
+
import type { RepoInfo } from "../repo-utils.js";
|
|
5
|
+
import { buildRepoContext } from "./repo-context.js";
|
|
6
|
+
import { TOOLS_BLOCK, ALL_CBM_TOOLS, EXA_TOOLS, PRINCIPLES_BLOCK } from "./tool-routing.js";
|
|
3
7
|
|
|
4
8
|
export function createBrainstormReviewerAgent(
|
|
5
9
|
variant: string,
|
|
6
|
-
|
|
10
|
+
variants: Record<string, VariantConfig>,
|
|
7
11
|
taskArtifacts: { userRequest: string; research: string; artifacts?: { name: string; content: string }[] },
|
|
8
12
|
outputPath: string,
|
|
13
|
+
contextDirs: string[],
|
|
14
|
+
phase?: string,
|
|
15
|
+
repos: RepoInfo[] = [],
|
|
9
16
|
) {
|
|
10
|
-
const variantConfig =
|
|
17
|
+
const variantConfig = variants[variant];
|
|
11
18
|
if (!variantConfig) {
|
|
12
19
|
throw new Error(`Unknown brainstorm-reviewer variant: ${variant}`);
|
|
13
20
|
}
|
|
21
|
+
const contextFiles = loadAllContextFiles(contextDirs, "brainstormReviewer", "system", phase, getModelInfo(variantConfig.model));
|
|
22
|
+
const contextBlock = contextFiles.map((f) => f.content).join("\n\n");
|
|
23
|
+
const repoContext = buildRepoContext(repos);
|
|
14
24
|
|
|
15
25
|
return {
|
|
16
26
|
frontmatter: {
|
|
17
27
|
description: `Brainstorm reviewer (${variant} variant, pi-pi)`,
|
|
18
28
|
tools: `read, grep, find, bash, write, lsp, ast_search, ${ALL_CBM_TOOLS}, ${EXA_TOOLS}`,
|
|
19
|
-
model: variantConfig.model,
|
|
29
|
+
model: resolveModel(variantConfig.model),
|
|
20
30
|
thinking: variantConfig.thinking,
|
|
21
|
-
max_turns:
|
|
31
|
+
max_turns: 120,
|
|
22
32
|
prompt_mode: "replace",
|
|
23
33
|
},
|
|
24
34
|
prompt: [
|
|
25
|
-
|
|
35
|
+
// --- static prefix (cacheable) ---
|
|
36
|
+
"<constraints>",
|
|
37
|
+
"You are a research reviewer. You verify the thoroughness and accuracy of brainstorm research artifacts. You are a GAP-FINDER, not a perfectionist — approve by default, reject only for critical gaps (maximum 3).",
|
|
38
|
+
"These rules override your default helpfulness. Strict compliance is required.",
|
|
39
|
+
"You are READ-ONLY: you MUST NOT implement or modify any file except the single review .md file named below. You MUST write it before finishing.",
|
|
40
|
+
"Your review MUST begin with the verdict on the VERY FIRST LINE: `VERDICT: APPROVE` or `VERDICT: NEEDS_WORK`.",
|
|
41
|
+
"</constraints>",
|
|
26
42
|
"",
|
|
27
|
-
|
|
28
|
-
"Approve by default. Reject only for critical gaps — maximum 3.",
|
|
43
|
+
PRINCIPLES_BLOCK,
|
|
29
44
|
"",
|
|
30
|
-
|
|
31
|
-
"",
|
|
32
|
-
COMMUNICATION,
|
|
33
|
-
"",
|
|
34
|
-
TOOL_ROUTING,
|
|
45
|
+
TOOLS_BLOCK,
|
|
35
46
|
"",
|
|
47
|
+
...(contextBlock ? ["<project_context>", contextBlock, "</project_context>", ""] : []),
|
|
48
|
+
"<task>",
|
|
36
49
|
"# Your job:",
|
|
37
50
|
"1. Read USER_REQUEST.md and RESEARCH.md provided below",
|
|
38
51
|
"2. INDEPENDENTLY investigate the codebase to verify claims and find gaps",
|
|
@@ -45,23 +58,22 @@ export function createBrainstormReviewerAgent(
|
|
|
45
58
|
"- Separation: does USER_REQUEST.md contain only user-stated info (no agent findings leaked in)?",
|
|
46
59
|
"- Structure: do both files follow the required format?",
|
|
47
60
|
"",
|
|
48
|
-
"# Format your review
|
|
61
|
+
"# Format your review with the verdict on the VERY FIRST LINE, then findings:",
|
|
62
|
+
"VERDICT: APPROVE | NEEDS_WORK",
|
|
49
63
|
"- GAPS: (missing information that would block planning)",
|
|
50
64
|
"- INACCURACIES: (claims that don't match the code)",
|
|
51
65
|
"- SUGGESTIONS: (improvements, not required)",
|
|
52
|
-
"- VERDICT: APPROVE or NEEDS_WORK (with reason)",
|
|
53
66
|
"",
|
|
54
67
|
"subagent_type is REQUIRED when spawning subagents — calls without it are rejected:",
|
|
55
68
|
'- Agent(subagent_type="Explore", ...) — codebase research. Prefer this for most lookups. Fast and cheap.',
|
|
56
69
|
'- Agent(subagent_type="Librarian", ...) — external docs, library APIs, web research.',
|
|
57
70
|
"Spawn multiple Explore agents in parallel for broad searches.",
|
|
71
|
+
"</task>",
|
|
58
72
|
"",
|
|
59
73
|
"# MANDATORY: Write your review to this exact file using the write tool:",
|
|
60
74
|
` ${outputPath}`,
|
|
61
75
|
"",
|
|
62
|
-
"
|
|
63
|
-
"You MUST NOT write to any other file. Only write .md files inside .pp/state/.",
|
|
64
|
-
"Do NOT implement, fix, or modify any source code — you are a reviewer, not an implementer.",
|
|
76
|
+
"You MUST write only to the review file above. Do NOT write to any other file.",
|
|
65
77
|
"",
|
|
66
78
|
"=== USER REQUEST ===",
|
|
67
79
|
taskArtifacts.userRequest,
|
|
@@ -74,6 +86,7 @@ export function createBrainstormReviewerAgent(
|
|
|
74
86
|
...taskArtifacts.artifacts.flatMap((a) => [`=== ${a.name} ===`, a.content, ""]),
|
|
75
87
|
]
|
|
76
88
|
: []),
|
|
89
|
+
...(repoContext ? [repoContext] : []),
|
|
77
90
|
"",
|
|
78
91
|
"The artifacts above are already in your context. Do NOT re-read them from disk.",
|
|
79
92
|
].join("\n"),
|