@ian-pascoe/pi-advisor 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/README.md +37 -13
- package/package.json +6 -6
- package/skills/pi-advisor/SKILL.md +1 -1
- package/src/advisor-calibration.ts +62 -0
- package/src/advisor-command.ts +26 -47
- package/src/advisor-evidence.ts +45 -273
- package/src/advisor-menu.ts +52 -257
- package/src/advisor-observer.ts +118 -18
- package/src/advisor-rendering.ts +185 -112
- package/src/advisor-session.ts +32 -1
- package/src/advisor-settings.ts +53 -128
- package/src/index.ts +27 -13
package/src/advisor-rendering.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
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 {
|
|
@@ -29,12 +44,15 @@ import {
|
|
|
29
44
|
/** Theme operations used by Advisor transcript and footer renderers. */
|
|
30
45
|
export type AdvisorRenderTheme = Pick<Theme, "fg" | "bg" | "bold">;
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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>;
|
|
38
56
|
|
|
39
57
|
const nullableString = Type.Union([Type.String(), Type.Null()]);
|
|
40
58
|
/** The minimum every historical status entry recorded. */
|
|
@@ -74,37 +92,25 @@ const statusSchema = Type.Object({
|
|
|
74
92
|
});
|
|
75
93
|
/** Data recorded in a `pi-advisor-status` entry. */
|
|
76
94
|
export type AdvisorStatusEntry = Static<typeof statusSchema>;
|
|
77
|
-
const
|
|
78
|
-
disabled:
|
|
79
|
-
private:
|
|
80
|
-
armed:
|
|
81
|
-
reviewing:
|
|
82
|
-
consulting:
|
|
83
|
-
paused:
|
|
84
|
-
} 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>;
|
|
85
103
|
const promptPreviewWidth = 40;
|
|
86
104
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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);
|
|
90
112
|
|
|
91
|
-
/**
|
|
92
|
-
class Clipped implements Component {
|
|
93
|
-
constructor(
|
|
94
|
-
private readonly child: Component,
|
|
95
|
-
private readonly limit: number,
|
|
96
|
-
private readonly hint: string,
|
|
97
|
-
) {}
|
|
98
|
-
render(width: number): string[] {
|
|
99
|
-
const lines = this.child.render(width);
|
|
100
|
-
return lines.length <= this.limit ? lines : [...lines.slice(0, this.limit), this.hint];
|
|
101
|
-
}
|
|
102
|
-
invalidate(): void {
|
|
103
|
-
this.child.invalidate();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** 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. */
|
|
108
114
|
export function renderAdvisorIntervention(
|
|
109
115
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Journaled custom-message details are validated by the finding schema below.
|
|
110
116
|
details: unknown,
|
|
@@ -113,25 +119,21 @@ export function renderAdvisorIntervention(
|
|
|
113
119
|
agentId?: string,
|
|
114
120
|
): Component | undefined {
|
|
115
121
|
if (!Value.Check(advisorFindingSchema, details)) return undefined;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
? 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
|
+
})
|
|
130
135
|
: body,
|
|
131
|
-
);
|
|
132
|
-
const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
133
|
-
box.addChild(container);
|
|
134
|
-
return box;
|
|
136
|
+
]);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
const childFindingSchema = Type.Object({
|
|
@@ -145,52 +147,89 @@ const childStateSchema = Type.Object({
|
|
|
145
147
|
error: Type.Optional(Type.String()),
|
|
146
148
|
});
|
|
147
149
|
|
|
150
|
+
/** Pi's entry renderers get no `outputPad`, so entries use Pi's default of one column. */
|
|
151
|
+
const entryOutputPad = 1;
|
|
152
|
+
|
|
148
153
|
/** A Child Agent's Intervention or pause, attributed to that child. */
|
|
149
154
|
export function renderAdvisorChildEntry(
|
|
150
155
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Journaled entry data is validated below; anything else renders raw.
|
|
151
156
|
data: unknown,
|
|
152
157
|
expanded: boolean,
|
|
153
158
|
theme: AdvisorRenderTheme,
|
|
159
|
+
outputPad = entryOutputPad,
|
|
154
160
|
): Component {
|
|
155
161
|
if (Value.Check(childFindingSchema, data)) {
|
|
156
162
|
const finding = { severity: data.severity, message: data.message };
|
|
157
163
|
const rendered = renderAdvisorIntervention(
|
|
158
164
|
finding,
|
|
159
|
-
{ expanded, outputPad
|
|
165
|
+
{ expanded, outputPad },
|
|
160
166
|
theme,
|
|
161
167
|
data.agentId,
|
|
162
168
|
);
|
|
163
169
|
if (rendered) return rendered;
|
|
164
170
|
}
|
|
165
171
|
if (Value.Check(childStateSchema, data)) {
|
|
166
|
-
const
|
|
167
|
-
|
|
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
|
+
);
|
|
168
181
|
}
|
|
169
182
|
return new Text(`Advisor for Child Agent\n${JSON.stringify(data, null, 2)}`, 0, 0);
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
const collapsedAnswerLines = 8;
|
|
173
|
-
|
|
174
185
|
/** `advisor_ask` call: the question, first line only while collapsed. */
|
|
175
186
|
export function renderAdvisorAskCall(
|
|
176
187
|
args: { message: string },
|
|
177
188
|
expanded: boolean,
|
|
178
189
|
theme: AdvisorRenderTheme,
|
|
190
|
+
context: DurationContext,
|
|
179
191
|
): Component {
|
|
180
|
-
const
|
|
181
|
-
|
|
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;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
/** `advisor_ask` result: a Markdown answer, previewed while collapsed. */
|
|
202
|
+
/** `advisor_ask` result: a Markdown answer, previewed while collapsed, then Pi's duration footer. */
|
|
185
203
|
export function renderAdvisorAskResult(
|
|
186
204
|
answer: string,
|
|
187
205
|
options: { expanded: boolean; isPartial: boolean; isError: boolean },
|
|
188
206
|
theme: AdvisorRenderTheme,
|
|
207
|
+
context: DurationContext,
|
|
189
208
|
): Component {
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
194
233
|
}
|
|
195
234
|
|
|
196
235
|
function formatTokens(total: number): string {
|
|
@@ -243,7 +282,7 @@ export function formatAdvisorOption(options: AdvisorOptions, key: keyof AdvisorO
|
|
|
243
282
|
if (options.prompt === undefined) return "inherit";
|
|
244
283
|
const first = options.prompt.split("\n", 1)[0] ?? "";
|
|
245
284
|
const preview =
|
|
246
|
-
first.length > promptPreviewWidth ? `${first.slice(0, promptPreviewWidth -
|
|
285
|
+
first.length > promptPreviewWidth ? `${first.slice(0, promptPreviewWidth - 3)}...` : first;
|
|
247
286
|
return `${preview} (${options.prompt.length} chars)`;
|
|
248
287
|
}
|
|
249
288
|
case "allowedTools":
|
|
@@ -261,28 +300,39 @@ export function formatAdvisorOption(options: AdvisorOptions, key: keyof AdvisorO
|
|
|
261
300
|
}
|
|
262
301
|
}
|
|
263
302
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
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))}`;
|
|
267
310
|
}
|
|
268
311
|
|
|
269
312
|
function joinDefined(parts: ReadonlyArray<string | undefined>, separator: string): string {
|
|
270
313
|
return parts.filter((part) => part !== undefined).join(separator);
|
|
271
314
|
}
|
|
272
315
|
|
|
273
|
-
function stateLine(
|
|
316
|
+
function stateLine(
|
|
317
|
+
entry: AdvisorStatusEntry,
|
|
318
|
+
theme: AdvisorRenderTheme,
|
|
319
|
+
label: string | undefined,
|
|
320
|
+
paint: Paint,
|
|
321
|
+
): string {
|
|
274
322
|
const inherited = entry.settings && entry.settings.model === undefined;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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)}`;
|
|
286
336
|
}
|
|
287
337
|
|
|
288
338
|
/** Live state line plus any error, as shown at the top of the settings menu. */
|
|
@@ -291,17 +341,21 @@ export function advisorStatusHeadline(
|
|
|
291
341
|
theme: AdvisorRenderTheme,
|
|
292
342
|
): string[] {
|
|
293
343
|
const error = entry.error ?? entry.lastError;
|
|
294
|
-
return [
|
|
344
|
+
return [
|
|
345
|
+
stateLine(entry, theme, theme.bold("Advisor"), unpainted),
|
|
346
|
+
...(error ? [errorLine(theme, error)] : []),
|
|
347
|
+
];
|
|
295
348
|
}
|
|
296
349
|
|
|
350
|
+
/** The status entry's body below its `[advisor] ● on · model` label line. */
|
|
297
351
|
function summaryLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): string[] {
|
|
352
|
+
const paint = messagePaint(theme);
|
|
298
353
|
const lines: string[] = [];
|
|
299
354
|
for (const { scope, key, options } of entry.changes ?? []) {
|
|
300
355
|
lines.push(
|
|
301
|
-
`${theme
|
|
356
|
+
`${statusMark(theme, "done")} ${paint(`${key} → ${formatAdvisorOption(options, key)}`)} ${theme.fg("dim", `[${scope}]`)}`,
|
|
302
357
|
);
|
|
303
358
|
}
|
|
304
|
-
lines.push(stateLine(entry, theme));
|
|
305
359
|
const children = entry.children?.length ?? 0;
|
|
306
360
|
const activity = joinDefined(
|
|
307
361
|
[
|
|
@@ -316,13 +370,16 @@ function summaryLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): str
|
|
|
316
370
|
const counts = findingCounts(entry);
|
|
317
371
|
if (counts.length) lines.push(theme.fg("muted", counts.join(" · ")));
|
|
318
372
|
if (entry.unavailableTools?.length)
|
|
319
|
-
lines.push(
|
|
373
|
+
lines.push(
|
|
374
|
+
`${statusMark(theme, "warning")} ${theme.fg("warning", `unavailable tools: ${entry.unavailableTools.join(", ")}`)}`,
|
|
375
|
+
);
|
|
320
376
|
const error = entry.error ?? entry.lastError;
|
|
321
|
-
if (error) lines.push(theme
|
|
377
|
+
if (error) lines.push(errorLine(theme, error));
|
|
322
378
|
return lines;
|
|
323
379
|
}
|
|
324
380
|
|
|
325
381
|
function detailLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): string[] {
|
|
382
|
+
const paint = messagePaint(theme);
|
|
326
383
|
const lines: string[] = [];
|
|
327
384
|
if (entry.settings) {
|
|
328
385
|
const settings = entry.settings;
|
|
@@ -331,22 +388,23 @@ function detailLines(entry: AdvisorStatusEntry, theme: AdvisorRenderTheme): stri
|
|
|
331
388
|
for (const key of advisorOptionKeys) {
|
|
332
389
|
const source = entry.sources?.[key] ?? "default";
|
|
333
390
|
lines.push(
|
|
334
|
-
` ${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}]`)}`,
|
|
335
392
|
);
|
|
336
393
|
}
|
|
337
394
|
}
|
|
338
|
-
|
|
339
|
-
|
|
395
|
+
const children = entry.children ?? [];
|
|
396
|
+
if (children.length) lines.push("");
|
|
397
|
+
for (const [index, child] of children.entries())
|
|
340
398
|
lines.push(
|
|
341
|
-
joinDefined(
|
|
399
|
+
`${theme.fg("dim", treePrefix([], index === children.length - 1))}${joinDefined(
|
|
342
400
|
[
|
|
343
|
-
|
|
344
|
-
child.state ? badge(child.state, theme) : undefined,
|
|
345
|
-
child.backlog ? `backlog ${child.backlog}` : undefined,
|
|
346
|
-
child.reviewCost?.reviews ? formatReviewCost(child.reviewCost) : 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,
|
|
347
405
|
],
|
|
348
406
|
" ",
|
|
349
|
-
)
|
|
407
|
+
)}`,
|
|
350
408
|
);
|
|
351
409
|
return lines;
|
|
352
410
|
}
|
|
@@ -364,21 +422,34 @@ function salvageStatus(data: Static<typeof minimalStatusSchema>): AdvisorStatusE
|
|
|
364
422
|
: { state: data.state, error: data.error ?? null };
|
|
365
423
|
}
|
|
366
424
|
|
|
367
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* Status snapshot in Pi's custom-message look: a ten-line summary, with every setting and child
|
|
427
|
+
* when expanded.
|
|
428
|
+
*/
|
|
368
429
|
export function renderAdvisorStatus(
|
|
369
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.
|
|
370
431
|
data: unknown,
|
|
371
432
|
expanded: boolean,
|
|
372
433
|
theme: AdvisorRenderTheme,
|
|
434
|
+
outputPad = entryOutputPad,
|
|
373
435
|
): Component {
|
|
374
436
|
if (!Value.Check(minimalStatusSchema, data))
|
|
375
|
-
return
|
|
437
|
+
return customMessageBox(theme, { outputPad, source: "advisor" }, [
|
|
438
|
+
new Text(JSON.stringify(data, null, 2), 0, 0),
|
|
439
|
+
]);
|
|
376
440
|
const entry = salvageStatus(data);
|
|
377
|
-
const
|
|
441
|
+
const summary = summaryLines(entry, theme);
|
|
378
442
|
const details = detailLines(entry, theme);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
);
|
|
382
453
|
}
|
|
383
454
|
|
|
384
455
|
/** The live fields the footer summarizes for one watched agent. */
|
|
@@ -391,24 +462,26 @@ function count(amount: number, what: string): string {
|
|
|
391
462
|
return `${amount} ${amount === 1 ? "child" : "children"} ${what}`;
|
|
392
463
|
}
|
|
393
464
|
|
|
394
|
-
/**
|
|
465
|
+
/** Footer status entry; `undefined` clears it. Pause reasons stay in `/advisor status`. */
|
|
395
466
|
export function advisorFooterText(
|
|
396
467
|
root: AdvisorActivity | undefined,
|
|
397
468
|
children: readonly AdvisorActivity[],
|
|
398
469
|
theme: Pick<Theme, "fg">,
|
|
399
470
|
): string | undefined {
|
|
400
471
|
if (!root || root.state === "disabled" || root.state === "private") return undefined;
|
|
401
|
-
if (root.state === "paused")
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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}`);
|
|
407
481
|
// Consultations come only from the main agent, so a child segment shows Reviews and pauses.
|
|
408
482
|
const busy = children.filter((child) => child.state === "reviewing").length;
|
|
409
483
|
const paused = children.filter((child) => child.state === "paused").length;
|
|
410
484
|
if (busy) parts.push(count(busy, "reviewing"));
|
|
411
|
-
if (paused) parts.push(theme.fg("
|
|
412
|
-
|
|
413
|
-
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) });
|
|
414
487
|
}
|
package/src/advisor-session.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { Value } from "typebox/value";
|
|
6
6
|
import * as piAi from "@earendil-works/pi-ai";
|
|
7
|
+
import type { Api, CacheRetention, Model } from "@earendil-works/pi-ai";
|
|
7
8
|
import * as piSdk from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type {
|
|
9
10
|
SourceInfo,
|
|
@@ -70,6 +71,23 @@ export function providesContextManagement(tools: { has(name: string): boolean })
|
|
|
70
71
|
return contextManagementTools.every((name) => tools.has(name));
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Prompt-cache retention the Advisor asks for on its own requests. Pi's adapters map `long` to
|
|
76
|
+
* what each API accepts: OpenAI `prompt_cache_retention: "24h"` (`prompt_cache_options` on
|
|
77
|
+
* models with explicit cache modes), which costs nothing extra, and Anthropic's `ttl: "1h"`,
|
|
78
|
+
* whose writes cost 2× instead of 1.25×, so it needs the `anthropicLongCache` opt-in. Other
|
|
79
|
+
* providers keep their default: the Codex adapter sends no retention field and ChatGPT
|
|
80
|
+
* sign-in rejects the OpenAI one.
|
|
81
|
+
*/
|
|
82
|
+
export function advisorCacheRetention(
|
|
83
|
+
model: Pick<Model<Api>, "provider">,
|
|
84
|
+
config: Pick<AdvisorConfig, "anthropicLongCache">,
|
|
85
|
+
): CacheRetention | undefined {
|
|
86
|
+
if (model.provider === "openai") return "long";
|
|
87
|
+
if (model.provider === "anthropic" && config.anthropicLongCache) return "long";
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
/** Private role is native journal state and is installed before session_start. */
|
|
74
92
|
export function isAdvisorSession(manager: Pick<SessionManager, "getBranch">): boolean {
|
|
75
93
|
return manager
|
|
@@ -452,7 +470,7 @@ async function buildAdvisorSession(
|
|
|
452
470
|
services,
|
|
453
471
|
sessionManager,
|
|
454
472
|
model,
|
|
455
|
-
thinkingLevel: config.thinkingLevel
|
|
473
|
+
thinkingLevel: config.thinkingLevel,
|
|
456
474
|
tools: [...config.allowedTools, options.adviceTool.name],
|
|
457
475
|
customTools: [options.adviceTool],
|
|
458
476
|
};
|
|
@@ -470,6 +488,19 @@ async function buildAdvisorSession(
|
|
|
470
488
|
created.session.setActiveToolsByName(
|
|
471
489
|
created.session.getActiveToolNames().filter((name) => !scriptOnly.has(name)),
|
|
472
490
|
);
|
|
491
|
+
// Retention is a per-request stream option on the Advisor's own agent, never a process-wide
|
|
492
|
+
// setting, so the observed agent's requests are untouched. Pi's native compaction of this
|
|
493
|
+
// session reuses this stream function and sets `cacheRetention: "none"` on its summary
|
|
494
|
+
// call on purpose, so a retention the caller chose is never overridden.
|
|
495
|
+
const advisorStream = created.session.agent.streamFunction;
|
|
496
|
+
created.session.agent.streamFunction = (model, context, streamOptions) => {
|
|
497
|
+
const cacheRetention = streamOptions?.cacheRetention ?? advisorCacheRetention(model, config);
|
|
498
|
+
return advisorStream(
|
|
499
|
+
model,
|
|
500
|
+
context,
|
|
501
|
+
cacheRetention ? { ...streamOptions, cacheRetention } : streamOptions,
|
|
502
|
+
);
|
|
503
|
+
};
|
|
473
504
|
return { ...created, services, diagnostics: services.diagnostics };
|
|
474
505
|
};
|
|
475
506
|
const runtime = await piSdk.createAgentSessionRuntime(factory, {
|