@ian-pascoe/pi-advisor 0.5.0 → 0.7.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/README.md +52 -16
- package/package.json +7 -7
- package/skills/pi-advisor/SKILL.md +7 -3
- package/src/advisor-command.ts +26 -47
- package/src/advisor-contract.ts +70 -4
- package/src/advisor-evidence.ts +157 -0
- package/src/advisor-menu.ts +54 -253
- package/src/advisor-observer.ts +514 -117
- package/src/advisor-rendering.ts +231 -112
- package/src/advisor-session.ts +14 -3
- package/src/advisor-settings.ts +80 -115
- package/src/index.ts +31 -12
package/src/advisor-rendering.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getMarkdownTheme,
|
|
3
|
-
keyHint,
|
|
4
3
|
type MessageRenderOptions,
|
|
5
4
|
type Theme,
|
|
6
5
|
type ThemeColor,
|
|
7
6
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
7
|
+
import { Container, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
callDurationFooter,
|
|
10
|
+
COLLAPSED_LINES,
|
|
11
|
+
appendDurationFooter,
|
|
12
|
+
CollapsedPreview,
|
|
13
|
+
customMessageBox,
|
|
14
|
+
footerStatus,
|
|
15
|
+
joinInline,
|
|
16
|
+
previewBody,
|
|
17
|
+
statusMark,
|
|
18
|
+
summaryExpandHint,
|
|
19
|
+
toolHeader,
|
|
20
|
+
treePrefix,
|
|
21
|
+
type DurationContext,
|
|
22
|
+
type StatusKind,
|
|
23
|
+
} from "@ian-pascoe/pi-utils/ui";
|
|
9
24
|
import { Type, type Static } from "typebox";
|
|
10
25
|
import { Value } from "typebox/value";
|
|
11
26
|
import {
|
|
12
27
|
advisorFindingSchema,
|
|
28
|
+
advisorDroppedFindingsSchema,
|
|
29
|
+
advisorReviewCostSchema,
|
|
13
30
|
advisorSeveritySchema,
|
|
14
31
|
advisorStateSchema,
|
|
32
|
+
type AdvisorReviewCost,
|
|
15
33
|
type AdvisorSeverity,
|
|
16
34
|
type AdvisorState,
|
|
17
35
|
} from "./advisor-contract.js";
|
|
@@ -26,12 +44,15 @@ import {
|
|
|
26
44
|
/** Theme operations used by Advisor transcript and footer renderers. */
|
|
27
45
|
export type AdvisorRenderTheme = Pick<Theme, "fg" | "bg" | "bold">;
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
/** The registered name of the consultation tool. */
|
|
48
|
+
export const advisorAskToolName = "advisor_ask";
|
|
49
|
+
|
|
50
|
+
/** Severity Labels: a coloured word, never a Status Mark. */
|
|
51
|
+
const severityColor = {
|
|
52
|
+
nit: "dim",
|
|
53
|
+
concern: "warning",
|
|
54
|
+
blocker: "error",
|
|
55
|
+
} as const satisfies Record<AdvisorSeverity, ThemeColor>;
|
|
35
56
|
|
|
36
57
|
const nullableString = Type.Union([Type.String(), Type.Null()]);
|
|
37
58
|
/** The minimum every historical status entry recorded. */
|
|
@@ -49,6 +70,12 @@ const statusSchema = Type.Object({
|
|
|
49
70
|
effectiveThinkingLevel: Type.Optional(nullableString),
|
|
50
71
|
usage: Type.Optional(Type.Union([Type.Object({ total: Type.Number() }), Type.Null()])),
|
|
51
72
|
cost: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
73
|
+
reviewCost: Type.Optional(Type.Union([advisorReviewCostSchema, Type.Null()])),
|
|
74
|
+
deferredFindings: Type.Optional(Type.Number()),
|
|
75
|
+
droppedFindings: Type.Optional(
|
|
76
|
+
// Entries recorded before a reason existed omit its count.
|
|
77
|
+
Type.Partial(advisorDroppedFindingsSchema),
|
|
78
|
+
),
|
|
52
79
|
unavailableTools: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
53
80
|
children: Type.Optional(
|
|
54
81
|
Type.Array(
|
|
@@ -56,6 +83,7 @@ const statusSchema = Type.Object({
|
|
|
56
83
|
agentId: Type.String(),
|
|
57
84
|
state: Type.Optional(advisorStateSchema),
|
|
58
85
|
backlog: Type.Optional(Type.Number()),
|
|
86
|
+
reviewCost: Type.Optional(Type.Union([advisorReviewCostSchema, Type.Null()])),
|
|
59
87
|
}),
|
|
60
88
|
),
|
|
61
89
|
),
|
|
@@ -64,37 +92,25 @@ const statusSchema = Type.Object({
|
|
|
64
92
|
});
|
|
65
93
|
/** Data recorded in a `pi-advisor-status` entry. */
|
|
66
94
|
export type AdvisorStatusEntry = Static<typeof statusSchema>;
|
|
67
|
-
const
|
|
68
|
-
disabled:
|
|
69
|
-
private:
|
|
70
|
-
armed:
|
|
71
|
-
reviewing:
|
|
72
|
-
consulting:
|
|
73
|
-
paused:
|
|
74
|
-
} as const satisfies Record<AdvisorState,
|
|
95
|
+
const stateMarkKind = {
|
|
96
|
+
disabled: "idle",
|
|
97
|
+
private: "idle",
|
|
98
|
+
armed: "active",
|
|
99
|
+
reviewing: "active",
|
|
100
|
+
consulting: "active",
|
|
101
|
+
paused: "warning",
|
|
102
|
+
} as const satisfies Record<AdvisorState, StatusKind>;
|
|
75
103
|
const promptPreviewWidth = 40;
|
|
76
104
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
/** Styles a plain piece of text; messages use `customMessageText`, menus leave it as is. */
|
|
106
|
+
type Paint = (text: string) => string;
|
|
107
|
+
const unpainted: Paint = (text) => text;
|
|
108
|
+
const messagePaint =
|
|
109
|
+
(theme: Pick<Theme, "fg">): Paint =>
|
|
110
|
+
(text) =>
|
|
111
|
+
theme.fg("customMessageText", text);
|
|
80
112
|
|
|
81
|
-
/**
|
|
82
|
-
class Clipped implements Component {
|
|
83
|
-
constructor(
|
|
84
|
-
private readonly child: Component,
|
|
85
|
-
private readonly limit: number,
|
|
86
|
-
private readonly hint: string,
|
|
87
|
-
) {}
|
|
88
|
-
render(width: number): string[] {
|
|
89
|
-
const lines = this.child.render(width);
|
|
90
|
-
return lines.length <= this.limit ? lines : [...lines.slice(0, this.limit), this.hint];
|
|
91
|
-
}
|
|
92
|
-
invalidate(): void {
|
|
93
|
-
this.child.invalidate();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Severity-styled, attributed Intervention; invalid details fall back to Pi's renderer. */
|
|
113
|
+
/** Severity-labelled, attributed Intervention; invalid details fall back to Pi's renderer. */
|
|
98
114
|
export function renderAdvisorIntervention(
|
|
99
115
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Journaled custom-message details are validated by the finding schema below.
|
|
100
116
|
details: unknown,
|
|
@@ -103,25 +119,21 @@ export function renderAdvisorIntervention(
|
|
|
103
119
|
agentId?: string,
|
|
104
120
|
): Component | undefined {
|
|
105
121
|
if (!Value.Check(advisorFindingSchema, details)) return undefined;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
? new Clipped(body, collapsedNitLines, expandHint(theme))
|
|
122
|
+
const heading = joinInline(theme, [
|
|
123
|
+
theme.fg(severityColor[details.severity], details.severity),
|
|
124
|
+
agentId ? theme.fg("accent", agentId) : undefined,
|
|
125
|
+
]);
|
|
126
|
+
const body = new Markdown(details.message, 0, 0, getMarkdownTheme(), {
|
|
127
|
+
color: messagePaint(theme),
|
|
128
|
+
});
|
|
129
|
+
return customMessageBox(theme, { outputPad: options.outputPad, source: "advisor", heading }, [
|
|
130
|
+
details.severity === "nit"
|
|
131
|
+
? new CollapsedPreview(theme, body, {
|
|
132
|
+
limit: COLLAPSED_LINES.fallback,
|
|
133
|
+
expanded: options.expanded,
|
|
134
|
+
})
|
|
120
135
|
: body,
|
|
121
|
-
);
|
|
122
|
-
const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
123
|
-
box.addChild(container);
|
|
124
|
-
return box;
|
|
136
|
+
]);
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
const childFindingSchema = Type.Object({
|
|
@@ -135,52 +147,89 @@ const childStateSchema = Type.Object({
|
|
|
135
147
|
error: Type.Optional(Type.String()),
|
|
136
148
|
});
|
|
137
149
|
|
|
150
|
+
/** Pi's entry renderers get no `outputPad`, so entries use Pi's default of one column. */
|
|
151
|
+
const entryOutputPad = 1;
|
|
152
|
+
|
|
138
153
|
/** A Child Agent's Intervention or pause, attributed to that child. */
|
|
139
154
|
export function renderAdvisorChildEntry(
|
|
140
155
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Journaled entry data is validated below; anything else renders raw.
|
|
141
156
|
data: unknown,
|
|
142
157
|
expanded: boolean,
|
|
143
158
|
theme: AdvisorRenderTheme,
|
|
159
|
+
outputPad = entryOutputPad,
|
|
144
160
|
): Component {
|
|
145
161
|
if (Value.Check(childFindingSchema, data)) {
|
|
146
162
|
const finding = { severity: data.severity, message: data.message };
|
|
147
163
|
const rendered = renderAdvisorIntervention(
|
|
148
164
|
finding,
|
|
149
|
-
{ expanded, outputPad
|
|
165
|
+
{ expanded, outputPad },
|
|
150
166
|
theme,
|
|
151
167
|
data.agentId,
|
|
152
168
|
);
|
|
153
169
|
if (rendered) return rendered;
|
|
154
170
|
}
|
|
155
171
|
if (Value.Check(childStateSchema, data)) {
|
|
156
|
-
const
|
|
157
|
-
|
|
172
|
+
const heading = joinInline(theme, [
|
|
173
|
+
theme.fg("accent", data.agentId),
|
|
174
|
+
badge(data.state, theme, messagePaint(theme)),
|
|
175
|
+
]);
|
|
176
|
+
return customMessageBox(
|
|
177
|
+
theme,
|
|
178
|
+
{ outputPad, source: "advisor", heading },
|
|
179
|
+
data.error ? [new Text(theme.fg("error", data.error), 0, 0)] : [],
|
|
180
|
+
);
|
|
158
181
|
}
|
|
159
182
|
return new Text(`Advisor for Child Agent\n${JSON.stringify(data, null, 2)}`, 0, 0);
|
|
160
183
|
}
|
|
161
184
|
|
|
162
|
-
const collapsedAnswerLines = 8;
|
|
163
|
-
|
|
164
185
|
/** `advisor_ask` call: the question, first line only while collapsed. */
|
|
165
186
|
export function renderAdvisorAskCall(
|
|
166
187
|
args: { message: string },
|
|
167
188
|
expanded: boolean,
|
|
168
189
|
theme: AdvisorRenderTheme,
|
|
190
|
+
context: DurationContext,
|
|
169
191
|
): Component {
|
|
170
|
-
const
|
|
171
|
-
|
|
192
|
+
const [first = "", ...rest] = args.message.split("\n");
|
|
193
|
+
const hidden = rest.length > 0 && !expanded;
|
|
194
|
+
const heading = toolHeader(theme, advisorAskToolName, expanded ? undefined : first);
|
|
195
|
+
const container = new Container();
|
|
196
|
+
container.addChild(new Text(hidden ? `${heading}${summaryExpandHint(theme)}` : heading, 0, 0));
|
|
197
|
+
if (expanded) container.addChild(new Text(theme.fg("muted", `message: ${args.message}`), 0, 0));
|
|
198
|
+
container.addChild(callDurationFooter(theme, context));
|
|
199
|
+
return container;
|
|
172
200
|
}
|
|
173
201
|
|
|
174
|
-
/** `advisor_ask` result: a Markdown answer, previewed while collapsed. */
|
|
202
|
+
/** `advisor_ask` result: a Markdown answer, previewed while collapsed, then Pi's duration footer. */
|
|
175
203
|
export function renderAdvisorAskResult(
|
|
176
204
|
answer: string,
|
|
177
205
|
options: { expanded: boolean; isPartial: boolean; isError: boolean },
|
|
178
206
|
theme: AdvisorRenderTheme,
|
|
207
|
+
context: DurationContext,
|
|
179
208
|
): Component {
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
const container = new Container();
|
|
210
|
+
if (answer) {
|
|
211
|
+
container.addChild(new Spacer(1));
|
|
212
|
+
if (options.isError) {
|
|
213
|
+
const lines = previewBody(theme, answer.split("\n"), {
|
|
214
|
+
limit: COLLAPSED_LINES.fallback,
|
|
215
|
+
expanded: options.expanded,
|
|
216
|
+
color: "error",
|
|
217
|
+
});
|
|
218
|
+
container.addChild(new Text(lines.join("\n"), 0, 0));
|
|
219
|
+
} else {
|
|
220
|
+
const body = new Markdown(answer, 0, 0, getMarkdownTheme(), {
|
|
221
|
+
color: (text) => theme.fg("toolOutput", text),
|
|
222
|
+
});
|
|
223
|
+
container.addChild(
|
|
224
|
+
new CollapsedPreview(theme, body, {
|
|
225
|
+
limit: COLLAPSED_LINES.fallback,
|
|
226
|
+
expanded: options.expanded,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
appendDurationFooter(container, theme, context, { isPartial: options.isPartial });
|
|
232
|
+
return container;
|
|
184
233
|
}
|
|
185
234
|
|
|
186
235
|
function formatTokens(total: number): string {
|
|
@@ -189,9 +238,41 @@ function formatTokens(total: number): string {
|
|
|
189
238
|
return `${(total / 1_000_000).toFixed(1)}M`;
|
|
190
239
|
}
|
|
191
240
|
|
|
241
|
+
function formatMoney(cost: number): string {
|
|
242
|
+
return `$${cost.toFixed(cost < 0.01 ? 4 : 2)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
192
245
|
function formatCost(cost: number | null | undefined): string {
|
|
193
246
|
if (cost === null || cost === undefined) return "cost unknown";
|
|
194
|
-
return `cost
|
|
247
|
+
return `cost ${formatMoney(cost)}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Running Review total, then the last Review alone; unknown stays unknown. */
|
|
251
|
+
function formatReviewCost({ reviews, last, total }: AdvisorReviewCost) {
|
|
252
|
+
const amount = (cost: number | null) => (cost === null ? "cost unknown" : formatMoney(cost));
|
|
253
|
+
return `${reviews} ${reviews === 1 ? "Review" : "Reviews"} ${amount(total)} · last Review ${amount(last)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`;
|
|
257
|
+
|
|
258
|
+
/** Findings withheld for re-validation and findings dropped, when there are any. */
|
|
259
|
+
function findingCounts(entry: AdvisorStatusEntry): string[] {
|
|
260
|
+
const { deferredFindings: deferred, droppedFindings: dropped } = entry;
|
|
261
|
+
return [
|
|
262
|
+
deferred ? `${plural(deferred, "finding", "findings")} awaiting re-validation` : "",
|
|
263
|
+
dropped?.overNitCap
|
|
264
|
+
? `${plural(dropped.overNitCap, "Nit", "Nits")} over the request cap dropped`
|
|
265
|
+
: "",
|
|
266
|
+
dropped?.unsupported
|
|
267
|
+
? `${plural(dropped.unsupported, "finding", "findings")} without valid evidence dropped`
|
|
268
|
+
: "",
|
|
269
|
+
dropped?.superseded
|
|
270
|
+
? `${plural(dropped.superseded, "Nit", "Nits")} from a superseded re-validating Review dropped`
|
|
271
|
+
: "",
|
|
272
|
+
dropped?.invalidReviews
|
|
273
|
+
? `${plural(dropped.invalidReviews, "Review", "Reviews")} ended by invalid reports`
|
|
274
|
+
: "",
|
|
275
|
+
].filter(Boolean);
|
|
195
276
|
}
|
|
196
277
|
|
|
197
278
|
/** Human-readable option value; an absent value inherits. */
|
|
@@ -201,7 +282,7 @@ export function formatAdvisorOption(options: AdvisorOptions, key: keyof AdvisorO
|
|
|
201
282
|
if (options.prompt === undefined) return "inherit";
|
|
202
283
|
const first = options.prompt.split("\n", 1)[0] ?? "";
|
|
203
284
|
const preview =
|
|
204
|
-
first.length > promptPreviewWidth ? `${first.slice(0, promptPreviewWidth -
|
|
285
|
+
first.length > promptPreviewWidth ? `${first.slice(0, promptPreviewWidth - 3)}...` : first;
|
|
205
286
|
return `${preview} (${options.prompt.length} chars)`;
|
|
206
287
|
}
|
|
207
288
|
case "allowedTools":
|
|
@@ -219,28 +300,39 @@ export function formatAdvisorOption(options: AdvisorOptions, key: keyof AdvisorO
|
|
|
219
300
|
}
|
|
220
301
|
}
|
|
221
302
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return
|
|
303
|
+
/** The word shown for a state. Guardian says `on` when idle, so an armed Advisor does too; `armed` stays the stored state. */
|
|
304
|
+
function stateWord(state: AdvisorState): string {
|
|
305
|
+
return state === "armed" ? "on" : state;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function badge(state: AdvisorState, theme: AdvisorRenderTheme, paint: Paint): string {
|
|
309
|
+
return `${statusMark(theme, stateMarkKind[state])} ${paint(stateWord(state))}`;
|
|
225
310
|
}
|
|
226
311
|
|
|
227
312
|
function joinDefined(parts: ReadonlyArray<string | undefined>, separator: string): string {
|
|
228
313
|
return parts.filter((part) => part !== undefined).join(separator);
|
|
229
314
|
}
|
|
230
315
|
|
|
231
|
-
function stateLine(
|
|
316
|
+
function stateLine(
|
|
317
|
+
entry: AdvisorStatusEntry,
|
|
318
|
+
theme: AdvisorRenderTheme,
|
|
319
|
+
label: string | undefined,
|
|
320
|
+
paint: Paint,
|
|
321
|
+
): string {
|
|
232
322
|
const inherited = entry.settings && entry.settings.model === undefined;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
323
|
+
const state = badge(entry.state, theme, paint);
|
|
324
|
+
return joinInline(theme, [
|
|
325
|
+
label ? `${label} ${state}` : state,
|
|
326
|
+
entry.effectiveModel
|
|
327
|
+
? `${paint(entry.effectiveModel)}${inherited ? theme.fg("dim", " (inherited)") : ""}`
|
|
328
|
+
: undefined,
|
|
329
|
+
entry.effectiveThinkingLevel ? paint(entry.effectiveThinkingLevel) : undefined,
|
|
330
|
+
entry.backlog ? paint(`backlog ${entry.backlog}`) : undefined,
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function errorLine(theme: AdvisorRenderTheme, error: string): string {
|
|
335
|
+
return `${statusMark(theme, "failed")} ${theme.fg("error", error)}`;
|
|
244
336
|
}
|
|
245
337
|
|
|
246
338
|
/** Live state line plus any error, as shown at the top of the settings menu. */
|
|
@@ -249,35 +341,45 @@ export function advisorStatusHeadline(
|
|
|
249
341
|
theme: AdvisorRenderTheme,
|
|
250
342
|
): string[] {
|
|
251
343
|
const error = entry.error ?? entry.lastError;
|
|
252
|
-
return [
|
|
344
|
+
return [
|
|
345
|
+
stateLine(entry, theme, theme.bold("Advisor"), unpainted),
|
|
346
|
+
...(error ? [errorLine(theme, error)] : []),
|
|
347
|
+
];
|
|
253
348
|
}
|
|
254
349
|
|
|
350
|
+
/** The status entry's body below its `[advisor] ● on · model` label line. */
|
|
255
351
|
function summaryLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): string[] {
|
|
352
|
+
const paint = messagePaint(theme);
|
|
256
353
|
const lines: string[] = [];
|
|
257
354
|
for (const { scope, key, options } of entry.changes ?? []) {
|
|
258
355
|
lines.push(
|
|
259
|
-
`${theme
|
|
356
|
+
`${statusMark(theme, "done")} ${paint(`${key} → ${formatAdvisorOption(options, key)}`)} ${theme.fg("dim", `[${scope}]`)}`,
|
|
260
357
|
);
|
|
261
358
|
}
|
|
262
|
-
lines.push(stateLine(entry, theme));
|
|
263
359
|
const children = entry.children?.length ?? 0;
|
|
264
360
|
const activity = joinDefined(
|
|
265
361
|
[
|
|
266
362
|
entry.usage ? `tokens ${formatTokens(entry.usage.total)}` : undefined,
|
|
267
363
|
entry.usage ? formatCost(entry.cost) : undefined,
|
|
364
|
+
entry.reviewCost?.reviews ? formatReviewCost(entry.reviewCost) : undefined,
|
|
268
365
|
children ? `${children} ${children === 1 ? "child" : "children"}` : undefined,
|
|
269
366
|
],
|
|
270
367
|
" · ",
|
|
271
368
|
);
|
|
272
369
|
if (activity) lines.push(theme.fg("muted", activity));
|
|
370
|
+
const counts = findingCounts(entry);
|
|
371
|
+
if (counts.length) lines.push(theme.fg("muted", counts.join(" · ")));
|
|
273
372
|
if (entry.unavailableTools?.length)
|
|
274
|
-
lines.push(
|
|
373
|
+
lines.push(
|
|
374
|
+
`${statusMark(theme, "warning")} ${theme.fg("warning", `unavailable tools: ${entry.unavailableTools.join(", ")}`)}`,
|
|
375
|
+
);
|
|
275
376
|
const error = entry.error ?? entry.lastError;
|
|
276
|
-
if (error) lines.push(theme
|
|
377
|
+
if (error) lines.push(errorLine(theme, error));
|
|
277
378
|
return lines;
|
|
278
379
|
}
|
|
279
380
|
|
|
280
381
|
function detailLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): string[] {
|
|
382
|
+
const paint = messagePaint(theme);
|
|
281
383
|
const lines: string[] = [];
|
|
282
384
|
if (entry.settings) {
|
|
283
385
|
const settings = entry.settings;
|
|
@@ -286,21 +388,23 @@ function detailLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): stri
|
|
|
286
388
|
for (const key of advisorOptionKeys) {
|
|
287
389
|
const source = entry.sources?.[key] ?? "default";
|
|
288
390
|
lines.push(
|
|
289
|
-
` ${key.padEnd(width)} ${formatAdvisorOption(settings, key)} ${theme.fg(source === "default" ? "dim" : "accent", `[${source}]`)}`,
|
|
391
|
+
` ${paint(key.padEnd(width))} ${paint(formatAdvisorOption(settings, key))} ${theme.fg(source === "default" ? "dim" : "accent", `[${source}]`)}`,
|
|
290
392
|
);
|
|
291
393
|
}
|
|
292
394
|
}
|
|
293
|
-
|
|
294
|
-
|
|
395
|
+
const children = entry.children ?? [];
|
|
396
|
+
if (children.length) lines.push("");
|
|
397
|
+
for (const [index, child] of children.entries())
|
|
295
398
|
lines.push(
|
|
296
|
-
joinDefined(
|
|
399
|
+
`${theme.fg("dim", treePrefix([], index === children.length - 1))}${joinDefined(
|
|
297
400
|
[
|
|
298
|
-
|
|
299
|
-
child.state ? badge(child.state, theme) : undefined,
|
|
300
|
-
child.backlog ? `backlog ${child.backlog}` : undefined,
|
|
401
|
+
theme.fg("accent", child.agentId),
|
|
402
|
+
child.state ? badge(child.state, theme, paint) : undefined,
|
|
403
|
+
child.backlog ? paint(`backlog ${child.backlog}`) : undefined,
|
|
404
|
+
child.reviewCost?.reviews ? paint(formatReviewCost(child.reviewCost)) : undefined,
|
|
301
405
|
],
|
|
302
406
|
" ",
|
|
303
|
-
)
|
|
407
|
+
)}`,
|
|
304
408
|
);
|
|
305
409
|
return lines;
|
|
306
410
|
}
|
|
@@ -318,21 +422,34 @@ function salvageStatus(data: Static<typeof minimalStatusSchema>): AdvisorStatusE
|
|
|
318
422
|
: { state: data.state, error: data.error ?? null };
|
|
319
423
|
}
|
|
320
424
|
|
|
321
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* Status snapshot in Pi's custom-message look: a ten-line summary, with every setting and child
|
|
427
|
+
* when expanded.
|
|
428
|
+
*/
|
|
322
429
|
export function renderAdvisorStatus(
|
|
323
430
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Journaled entry data is validated below; older shapes keep their state and error, anything else renders raw.
|
|
324
431
|
data: unknown,
|
|
325
432
|
expanded: boolean,
|
|
326
433
|
theme: AdvisorRenderTheme,
|
|
434
|
+
outputPad = entryOutputPad,
|
|
327
435
|
): Component {
|
|
328
436
|
if (!Value.Check(minimalStatusSchema, data))
|
|
329
|
-
return
|
|
437
|
+
return customMessageBox(theme, { outputPad, source: "advisor" }, [
|
|
438
|
+
new Text(JSON.stringify(data, null, 2), 0, 0),
|
|
439
|
+
]);
|
|
330
440
|
const entry = salvageStatus(data);
|
|
331
|
-
const
|
|
441
|
+
const summary = summaryLines(entry, theme);
|
|
332
442
|
const details = detailLines(entry, theme);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
443
|
+
const heading = stateLine(entry, theme, undefined, messagePaint(theme));
|
|
444
|
+
const rows = [...summary, ...details];
|
|
445
|
+
const body = new Text(rows.join("\n"), 0, 0);
|
|
446
|
+
return customMessageBox(
|
|
447
|
+
theme,
|
|
448
|
+
{ outputPad, source: "advisor", heading },
|
|
449
|
+
rows.length > 0
|
|
450
|
+
? [new CollapsedPreview(theme, body, { limit: COLLAPSED_LINES.fallback, expanded })]
|
|
451
|
+
: [],
|
|
452
|
+
);
|
|
336
453
|
}
|
|
337
454
|
|
|
338
455
|
/** The live fields the footer summarizes for one watched agent. */
|
|
@@ -345,24 +462,26 @@ function count(amount: number, what: string): string {
|
|
|
345
462
|
return `${amount} ${amount === 1 ? "child" : "children"} ${what}`;
|
|
346
463
|
}
|
|
347
464
|
|
|
348
|
-
/**
|
|
465
|
+
/** Footer status entry; `undefined` clears it. Pause reasons stay in `/advisor status`. */
|
|
349
466
|
export function advisorFooterText(
|
|
350
467
|
root: AdvisorActivity | undefined,
|
|
351
468
|
children: readonly AdvisorActivity[],
|
|
352
469
|
theme: Pick<Theme, "fg">,
|
|
353
470
|
): string | undefined {
|
|
354
471
|
if (!root || root.state === "disabled" || root.state === "private") return undefined;
|
|
355
|
-
if (root.state === "paused")
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
472
|
+
if (root.state === "paused")
|
|
473
|
+
return footerStatus(theme, {
|
|
474
|
+
mark: "warning",
|
|
475
|
+
name: "advisor",
|
|
476
|
+
value: theme.fg("warning", "paused"),
|
|
477
|
+
});
|
|
478
|
+
const parts: string[] = [stateWord(root.state)];
|
|
479
|
+
if ((root.state === "reviewing" || root.state === "consulting") && root.backlog > 0)
|
|
480
|
+
parts.push(`backlog ${root.backlog}`);
|
|
361
481
|
// Consultations come only from the main agent, so a child segment shows Reviews and pauses.
|
|
362
482
|
const busy = children.filter((child) => child.state === "reviewing").length;
|
|
363
483
|
const paused = children.filter((child) => child.state === "paused").length;
|
|
364
484
|
if (busy) parts.push(count(busy, "reviewing"));
|
|
365
|
-
if (paused) parts.push(theme.fg("
|
|
366
|
-
|
|
367
|
-
return theme.fg("accent", `advisor: ${parts.join(" · ")}`);
|
|
485
|
+
if (paused) parts.push(theme.fg("warning", count(paused, "paused")));
|
|
486
|
+
return footerStatus(theme, { mark: "active", name: "advisor", value: joinInline(theme, parts) });
|
|
368
487
|
}
|
package/src/advisor-session.ts
CHANGED
|
@@ -63,6 +63,13 @@ const registrationNames = ({ tools, commands, flags }: Extension) => [
|
|
|
63
63
|
...[...flags.keys()].map((name) => `flag:${name}`),
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
+
/** Context Management's private tools; an extension or session providing all three loads it. */
|
|
67
|
+
export const contextManagementTools = ["context_notes", "context_history", "context_rollover"];
|
|
68
|
+
/** Whether these tool names include all of Context Management's; it then owns compaction. */
|
|
69
|
+
export function providesContextManagement(tools: { has(name: string): boolean }): boolean {
|
|
70
|
+
return contextManagementTools.every((name) => tools.has(name));
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
/** Private role is native journal state and is installed before session_start. */
|
|
67
74
|
export function isAdvisorSession(manager: Pick<SessionManager, "getBranch">): boolean {
|
|
68
75
|
return manager
|
|
@@ -256,12 +263,11 @@ async function buildAdvisorSession(
|
|
|
256
263
|
const global = JSON.stringify(observed.settingsManager.getGlobalSettings());
|
|
257
264
|
const project = JSON.stringify(observed.settingsManager.getProjectSettings());
|
|
258
265
|
const config = structuredClone(options.config);
|
|
259
|
-
const contextTools = ["context_notes", "context_history", "context_rollover"];
|
|
260
266
|
const hasContextManagement = source
|
|
261
267
|
.getExtensions()
|
|
262
|
-
.extensions.some((extension) =>
|
|
268
|
+
.extensions.some((extension) => providesContextManagement(extension.tools));
|
|
263
269
|
const missingContextGrants = hasContextManagement
|
|
264
|
-
?
|
|
270
|
+
? contextManagementTools.filter((name) => !config.allowedTools.includes(name))
|
|
265
271
|
: [];
|
|
266
272
|
if (missingContextGrants.length)
|
|
267
273
|
throw new Error(
|
|
@@ -353,6 +359,11 @@ async function buildAdvisorSession(
|
|
|
353
359
|
resourceLoaderOptions: {
|
|
354
360
|
noExtensions: true,
|
|
355
361
|
noContextFiles: true,
|
|
362
|
+
// Neither Reviews nor Consultations use skills; the catalogue would only bloat and
|
|
363
|
+
// destabilize the prompt. `noSkills` alone still admits skills that inherited extensions
|
|
364
|
+
// return from `resources_discover`, so also empty every skill load.
|
|
365
|
+
noSkills: true,
|
|
366
|
+
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
356
367
|
additionalExtensionPaths: extensionPaths,
|
|
357
368
|
extensionFactories: [
|
|
358
369
|
...builtins,
|