@ian-pascoe/pi-advisor 0.6.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 +2 -2
- package/package.json +6 -6
- package/src/advisor-command.ts +26 -47
- package/src/advisor-evidence.ts +38 -270
- package/src/advisor-menu.ts +34 -253
- package/src/advisor-rendering.ts +185 -112
- package/src/advisor-settings.ts +26 -114
- package/src/index.ts +23 -11
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`@ian-pascoe/pi-advisor` reviews a Pi agent's completed work and surfaces concise, attributed findings for material problems and worthwhile low-risk cleanup.
|
|
4
4
|
|
|
5
|
-
Requires Node `>=22.19.0` and Pi `>=
|
|
5
|
+
Requires Node `>=22.19.0` and Pi `>=1.1.0`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -36,7 +36,7 @@ Argument autocomplete suggests command names, settings keys, and valid trailing
|
|
|
36
36
|
|
|
37
37
|
In the interactive TUI, `/advisor` alone opens a settings menu built from Pi's native settings list, like `/settings`. A Scope row chooses where edits are written (session, trusted project, or global), and each setting shows its effective value with any non-default source. `enabled`, `includeSubagents`, and `thinkingLevel` cycle the selected scope's own value in place (`inherit → on → off`, or through each thinking level); while that scope has none, the row shows what it inherits, such as `inherit (on · project)`, and a value overridden by a higher-precedence scope is marked. Model, tools, numbers, and the prompt open submenus, and the prompt uses Pi's own editor component. Every edit is validated and applied immediately; a paused Advisor shows its reason and a Resume row. Closing the menu records one status entry listing the changes it applied, or nothing if none were. Without the TUI, `/advisor` records the status entry instead.
|
|
38
38
|
|
|
39
|
-
`/advisor status` and the other subcommands each record a status entry in the transcript. Collapsed, it shows state, model, backlog, usage and cost, the number of Reviews with their running cost and the last Review's cost, unavailable tools, and the last error; expanded, it lists every setting with its source and each watched Child Agent. A mutating command's entry starts with the changes it applied. While enabled, the footer shows
|
|
39
|
+
`/advisor status` and the other subcommands each record a status entry in the transcript. Collapsed, it shows state, model, backlog, usage and cost, the number of Reviews with their running cost and the last Review's cost, unavailable tools, and the last error; expanded, it lists every setting with its source and each watched Child Agent. A mutating command's entry starts with the changes it applied. While enabled, the footer shows `● advisor on` when idle, `● advisor reviewing`/`consulting` with any `backlog N`, or `! advisor paused`, plus Child Agents that are reviewing or paused. Interventions and status entries use Pi's custom-message box with a bold `Advisor` label; an Intervention's severity is a coloured word (`nit`, `concern`, `blocker`), and Child Agent findings add the agent ID. `advisor_ask` renders as Pi's built-in tools do: a lowercase `advisor_ask` header with the question, a Markdown answer collapsed to ten lines with Pi's expand hint, and Pi's `Elapsed`/`Took` footer. Warnings and errors read `Advisor: ...`.
|
|
40
40
|
|
|
41
41
|
Interventions render with their severity and Advisor attribution. Long Nits collapse until expanded; Concerns and Blockers always show in full.
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-advisor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Background review and attributed corrective advice for Pi agent sessions.",
|
|
6
6
|
"keywords": [
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"provenance": true
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ian-pascoe/pi-utils": "^0.
|
|
35
|
+
"@ian-pascoe/pi-utils": "^0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-agent-core": ">=
|
|
39
|
-
"@earendil-works/pi-ai": ">=
|
|
40
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
-
"@earendil-works/pi-tui": ">=
|
|
38
|
+
"@earendil-works/pi-agent-core": ">=1.1.0",
|
|
39
|
+
"@earendil-works/pi-ai": ">=1.1.0",
|
|
40
|
+
"@earendil-works/pi-coding-agent": ">=1.1.0",
|
|
41
|
+
"@earendil-works/pi-tui": ">=1.1.0",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
package/src/advisor-command.ts
CHANGED
|
@@ -1,64 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
completeSettingsCommandArguments,
|
|
3
|
+
parseSettingsCommand,
|
|
4
|
+
type SettingsCommand,
|
|
5
|
+
} from "@ian-pascoe/pi-utils/settings-command";
|
|
1
6
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
2
7
|
import {
|
|
3
8
|
advisorOptionKey,
|
|
4
9
|
advisorOptionKeys,
|
|
5
10
|
parseAdvisorOptions,
|
|
6
11
|
type AdvisorOptions,
|
|
12
|
+
type AdvisorSettingScope,
|
|
7
13
|
} from "./advisor-settings.js";
|
|
8
14
|
|
|
9
15
|
/** Native command completion values replace the entire argument prefix. */
|
|
10
16
|
export function completeAdvisorCommandArguments(prefix: string): AutocompleteItem[] {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const command = parseAdvisorCommand(scopePrefix);
|
|
18
|
-
if ("scope" in command && command.scope === "session")
|
|
19
|
-
candidates.push(`${scopePrefix}--global`, `${scopePrefix}--project`);
|
|
20
|
-
} catch {
|
|
21
|
-
// Incomplete commands and JSON values cannot accept a scope yet.
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return candidates
|
|
25
|
-
.filter((value) => value.startsWith(prefix))
|
|
26
|
-
.map((value) => ({ value, label: value }));
|
|
17
|
+
return completeSettingsCommandArguments(prefix, {
|
|
18
|
+
words: ["on", "off", "status", "prompt", "inherit", "set"],
|
|
19
|
+
optionKeys: advisorOptionKeys,
|
|
20
|
+
parse: parseAdvisorCommand,
|
|
21
|
+
});
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
const usage =
|
|
30
25
|
"Usage: /advisor [on|off|status|prompt|inherit [key]|set <key> <JSON>] [--global|--project]; /advisor alone opens settings";
|
|
31
26
|
|
|
27
|
+
/** One parsed `/advisor` command. */
|
|
28
|
+
export type AdvisorCommand = SettingsCommand<
|
|
29
|
+
AdvisorOptions,
|
|
30
|
+
keyof AdvisorOptions,
|
|
31
|
+
{ action: "prompt"; scope: AdvisorSettingScope }
|
|
32
|
+
>;
|
|
33
|
+
|
|
32
34
|
/** Parse one configuration change; validated patches cannot invent option keys. */
|
|
33
|
-
export function parseAdvisorCommand(input: string) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const text = flag ? input.trim().slice(0, flag.index) : input.trim();
|
|
42
|
-
if (text === "" || text === "status") {
|
|
43
|
-
if (flag) throw new Error(usage);
|
|
44
|
-
return text === "" ? { action: "menu" as const } : { action: "status" as const };
|
|
45
|
-
}
|
|
46
|
-
if (text === "prompt") return { action: "prompt" as const, scope };
|
|
47
|
-
if (text === "on" || text === "off") {
|
|
48
|
-
return {
|
|
49
|
-
action: "set" as const,
|
|
50
|
-
scope,
|
|
51
|
-
key: "enabled" as const,
|
|
52
|
-
patch: { enabled: text === "on" },
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
const inherit = /^inherit(?:\s+(\S+))?$/.exec(text);
|
|
56
|
-
if (inherit) {
|
|
57
|
-
return { action: "inherit" as const, scope, key: advisorOptionKey(inherit[1] ?? "enabled") };
|
|
58
|
-
}
|
|
59
|
-
const set = /^set\s+(\S+)\s+([\s\S]+)$/.exec(text);
|
|
60
|
-
if (!set?.[1] || !set[2]) throw new Error(usage);
|
|
61
|
-
const key = advisorOptionKey(set[1]);
|
|
62
|
-
const patch: AdvisorOptions = parseAdvisorOptions({ [key]: JSON.parse(set[2]) }, scope);
|
|
63
|
-
return { action: "set" as const, scope, key, patch };
|
|
35
|
+
export function parseAdvisorCommand(input: string): AdvisorCommand {
|
|
36
|
+
return parseSettingsCommand(input, {
|
|
37
|
+
usage,
|
|
38
|
+
optionKey: advisorOptionKey,
|
|
39
|
+
parseOptions: parseAdvisorOptions,
|
|
40
|
+
toggle: (enabled) => ({ key: "enabled", patch: { enabled } }),
|
|
41
|
+
parseExtra: (text, scope) => (text === "prompt" ? { action: "prompt", scope } : undefined),
|
|
42
|
+
});
|
|
64
43
|
}
|
package/src/advisor-evidence.ts
CHANGED
|
@@ -1,72 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "@
|
|
11
|
-
|
|
1
|
+
import type { Context, Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
combineEvidence,
|
|
4
|
+
evidenceItemsCost,
|
|
5
|
+
fitEvidence,
|
|
6
|
+
projectEvidenceItem,
|
|
7
|
+
shortenEvidence,
|
|
8
|
+
type Evidence,
|
|
9
|
+
type EvidenceItem,
|
|
10
|
+
} from "@ian-pascoe/pi-utils/evidence";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
evidenceRefs,
|
|
14
|
+
evidenceTokens,
|
|
15
|
+
messageOrigins,
|
|
16
|
+
projectEvidence,
|
|
17
|
+
toolCallRef,
|
|
18
|
+
type Evidence,
|
|
19
|
+
type EvidenceBlock,
|
|
20
|
+
type EvidenceMessage,
|
|
21
|
+
} from "@ian-pascoe/pi-utils/evidence";
|
|
22
|
+
|
|
23
|
+
/** Marks text shortened to fit the Context Seed budget. */
|
|
24
|
+
const omissionMarker = (omitted: number) =>
|
|
25
|
+
`[… ${omitted} characters omitted from the Context Seed]`;
|
|
12
26
|
|
|
13
|
-
/**
|
|
14
|
-
* Review Evidence: the observed model's view of its context, without what Pi stores only for
|
|
15
|
-
* replay or display (signatures, `details`, provider/response metadata, native IDs).
|
|
16
|
-
*/
|
|
17
|
-
export type EvidenceBlock =
|
|
18
|
-
| { type: "text"; text: string }
|
|
19
|
-
| { type: "thinking"; thinking: string }
|
|
20
|
-
/** Reasoning the provider withheld; the observed model saw only an opaque payload. */
|
|
21
|
-
| { type: "thinking"; redacted: true }
|
|
22
|
-
| { type: "image"; attachment: number }
|
|
23
|
-
| { type: "toolCall"; ref: string; name: string; arguments: ToolCall["arguments"] };
|
|
24
|
-
export type EvidenceMessage =
|
|
25
|
-
| { role: "user"; content: string | EvidenceBlock[] }
|
|
26
|
-
| {
|
|
27
|
-
role: "assistant";
|
|
28
|
-
content: EvidenceBlock[];
|
|
29
|
-
/** Present only for responses that did not finish normally. */
|
|
30
|
-
stopReason?: "error" | "aborted" | "length";
|
|
31
|
-
errorMessage?: string;
|
|
32
|
-
}
|
|
33
|
-
| {
|
|
34
|
-
role: "toolResult";
|
|
35
|
-
ref: string;
|
|
36
|
-
toolName: string;
|
|
37
|
-
isError: boolean;
|
|
38
|
-
content: EvidenceBlock[];
|
|
39
|
-
};
|
|
40
|
-
export interface Evidence {
|
|
41
|
-
messages: EvidenceMessage[];
|
|
42
|
-
/** Native image attachments, referenced 1-based by `{ type: "image", attachment }` blocks. */
|
|
43
|
-
images: ImageContent[];
|
|
44
|
-
}
|
|
45
27
|
/** Observed Setup: the observed system prompt and tool summaries that open a Context Seed. */
|
|
46
28
|
export interface ObservedSetup {
|
|
47
29
|
systemPrompt: string | undefined;
|
|
48
30
|
tools: { name: string; summary: string }[];
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
/**
|
|
52
|
-
* Tool-Call Reference: compact, shared by an observed tool call and its result. Derived from
|
|
53
|
-
* the native call ID alone, so it is stable across projections and Advisor Sessions and can
|
|
54
|
-
* be recomputed from the observed transcript.
|
|
55
|
-
*/
|
|
56
|
-
export function toolCallRef(toolCallId: string): string {
|
|
57
|
-
// Coerce rather than trust the stored shape: a malformed ID must not pause the Advisor.
|
|
58
|
-
return createHash("sha256").update(String(toolCallId)).digest("base64url").slice(0, 8);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Every Tool-Call Reference in projected Review Evidence, from calls and their results. */
|
|
62
|
-
export function evidenceRefs(messages: readonly EvidenceMessage[]): string[] {
|
|
63
|
-
return messages.flatMap((message) => {
|
|
64
|
-
if (message.role === "toolResult") return [message.ref];
|
|
65
|
-
if (message.role !== "assistant") return [];
|
|
66
|
-
return message.content.flatMap((block) => (block.type === "toolCall" ? [block.ref] : []));
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
33
|
const summaryLimit = 160;
|
|
71
34
|
|
|
72
35
|
/** First sentence of a tool description's first line, bounded for the Observed Setup. */
|
|
@@ -90,45 +53,6 @@ export function projectObservedSetup(
|
|
|
90
53
|
};
|
|
91
54
|
}
|
|
92
55
|
|
|
93
|
-
/**
|
|
94
|
-
* Each observed message's role before Pi converted it for the model. Compaction and branch
|
|
95
|
-
* summaries, custom messages (including delivered Advisor findings), and `!` command results all
|
|
96
|
-
* reach the model as `user` messages; only `user` origins are requests from the user.
|
|
97
|
-
*/
|
|
98
|
-
export function messageOrigins(
|
|
99
|
-
messages: readonly Message[],
|
|
100
|
-
sources: AgentSession["messages"],
|
|
101
|
-
): string[] {
|
|
102
|
-
// Sources that reach the model as user messages, in order, grouped by timestamp.
|
|
103
|
-
const byTime = Map.groupBy(
|
|
104
|
-
sources.flatMap((source) => {
|
|
105
|
-
const [converted] = convertToLlm([source]);
|
|
106
|
-
return converted?.role === "user" ? [{ source, content: converted.content }] : [];
|
|
107
|
-
}),
|
|
108
|
-
({ source }) => source.timestamp,
|
|
109
|
-
);
|
|
110
|
-
const used = new Set<object>();
|
|
111
|
-
const origins = messages.map((message): string | undefined =>
|
|
112
|
-
message.role === "user" ? undefined : message.role,
|
|
113
|
-
);
|
|
114
|
-
const claim = (index: number, matches: (content: Message["content"]) => boolean) => {
|
|
115
|
-
const message = messages[index];
|
|
116
|
-
if (message?.role !== "user" || origins[index]) return;
|
|
117
|
-
const match = byTime
|
|
118
|
-
.get(message.timestamp)
|
|
119
|
-
?.find(({ source, content }) => !used.has(source) && matches(content));
|
|
120
|
-
if (!match) return;
|
|
121
|
-
used.add(match.source);
|
|
122
|
-
origins[index] = match.source.role;
|
|
123
|
-
};
|
|
124
|
-
// Exact conversions first; then, within a timestamp, the k-th unmatched message pairs with the
|
|
125
|
-
// k-th unused source (a message a context hook rewrote, such as with images removed).
|
|
126
|
-
for (const [index, message] of messages.entries())
|
|
127
|
-
claim(index, (content) => isDeepStrictEqual(content, message.content));
|
|
128
|
-
for (const index of messages.keys()) claim(index, () => true);
|
|
129
|
-
return origins.map((origin) => origin ?? "unknown");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
56
|
/** A Context Seed: the Observed Setup and the observed messages that fit its token budget. */
|
|
133
57
|
export interface ContextSeed extends Evidence {
|
|
134
58
|
observedSetup: ObservedSetup;
|
|
@@ -138,81 +62,6 @@ export interface ContextSeed extends Evidence {
|
|
|
138
62
|
shortened: number;
|
|
139
63
|
}
|
|
140
64
|
|
|
141
|
-
interface Projected {
|
|
142
|
-
message: EvidenceMessage;
|
|
143
|
-
images: ImageContent[];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Pi's compaction estimate for one image, plus a token for a longer attachment index.
|
|
147
|
-
const imageTokens =
|
|
148
|
-
estimateTokens({
|
|
149
|
-
role: "user",
|
|
150
|
-
content: [{ type: "image", data: "", mimeType: "image/png" }],
|
|
151
|
-
timestamp: 0,
|
|
152
|
-
}) + 1;
|
|
153
|
-
|
|
154
|
-
/** Pi's chars/4 estimate of projected evidence messages, plus Pi's per-image estimate. */
|
|
155
|
-
export function evidenceTokens({ messages, images }: Evidence): number {
|
|
156
|
-
return Math.ceil(JSON.stringify(messages).length / 4) + images.length * imageTokens;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Tokens a projected message adds to the seed JSON, by Pi's chars/4 heuristic. */
|
|
160
|
-
function cost(items: readonly Projected[]): number {
|
|
161
|
-
return items.reduce(
|
|
162
|
-
(total, { message, images }) =>
|
|
163
|
-
total + Math.ceil((JSON.stringify(message).length + 1) / 4) + images.length * imageTokens,
|
|
164
|
-
0,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Shorten every text longer than `limit` characters, marking how much was cut. */
|
|
169
|
-
function shorten(items: readonly Projected[], limit: number): Projected[] {
|
|
170
|
-
return items.map((item) => {
|
|
171
|
-
let changed = false;
|
|
172
|
-
const cut = (text: string) => {
|
|
173
|
-
const shortened = `${text.slice(0, limit)}\n[… ${text.length - limit} characters omitted from the Context Seed]`;
|
|
174
|
-
if (shortened.length >= text.length) return text;
|
|
175
|
-
changed = true;
|
|
176
|
-
return shortened;
|
|
177
|
-
};
|
|
178
|
-
const block = (part: EvidenceBlock): EvidenceBlock => {
|
|
179
|
-
if (part.type === "text") return { type: "text", text: cut(part.text) };
|
|
180
|
-
if (part.type === "thinking" && "thinking" in part)
|
|
181
|
-
return { type: "thinking", thinking: cut(part.thinking) };
|
|
182
|
-
if (part.type !== "toolCall") return part;
|
|
183
|
-
const json = JSON.stringify(part.arguments);
|
|
184
|
-
const shortened = cut(json);
|
|
185
|
-
// Oversized arguments become their marked JSON prefix, which no longer parses.
|
|
186
|
-
return shortened === json ? part : { ...part, arguments: { shortenedJson: shortened } };
|
|
187
|
-
};
|
|
188
|
-
const { message } = item;
|
|
189
|
-
const next: EvidenceMessage =
|
|
190
|
-
message.role === "user"
|
|
191
|
-
? {
|
|
192
|
-
role: "user",
|
|
193
|
-
content: Array.isArray(message.content)
|
|
194
|
-
? message.content.map(block)
|
|
195
|
-
: cut(message.content),
|
|
196
|
-
}
|
|
197
|
-
: { ...message, content: message.content.map(block) };
|
|
198
|
-
return changed ? { message: next, images: item.images } : item;
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** The unit unchanged if it fits, else with the longest per-string limit that fits. */
|
|
203
|
-
function fit(items: readonly Projected[], allowance: number): Projected[] | undefined {
|
|
204
|
-
if (cost(items) <= allowance) return [...items];
|
|
205
|
-
let low = 0;
|
|
206
|
-
let high = JSON.stringify(items.map(({ message }) => message)).length;
|
|
207
|
-
if (cost(shorten(items, low)) > allowance) return undefined;
|
|
208
|
-
while (low < high) {
|
|
209
|
-
const middle = Math.ceil((low + high) / 2);
|
|
210
|
-
if (cost(shorten(items, middle)) <= allowance) low = middle;
|
|
211
|
-
else high = middle - 1;
|
|
212
|
-
}
|
|
213
|
-
return shorten(items, low);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
65
|
/**
|
|
217
66
|
* Fit a Context Seed to a token budget, measured as Pi's chars/4 estimate of the seed JSON with
|
|
218
67
|
* Pi's per-image estimate. Always kept: the Observed Setup; the original request (the first
|
|
@@ -227,12 +76,9 @@ export function selectContextSeed(
|
|
|
227
76
|
): ContextSeed {
|
|
228
77
|
const { messages } = context;
|
|
229
78
|
const observedSetup = projectObservedSetup(context);
|
|
230
|
-
const projected = messages.map((message):
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
images,
|
|
234
|
-
} = projectEvidence([message]);
|
|
235
|
-
return only ? [{ message: only, images }] : [];
|
|
79
|
+
const projected = messages.map((message): EvidenceItem[] => {
|
|
80
|
+
const item = projectEvidenceItem(message);
|
|
81
|
+
return item ? [item] : [];
|
|
236
82
|
});
|
|
237
83
|
// Fall back to the converted role only when no converted user message has a known origin.
|
|
238
84
|
const origins = options.origins?.some(
|
|
@@ -265,17 +111,19 @@ export function selectContextSeed(
|
|
|
265
111
|
const recent = new Set([prompt, messages.length - 1].filter((index) => index >= 0).map(unitAt));
|
|
266
112
|
for (const unit of anchors) recent.delete(unit);
|
|
267
113
|
|
|
268
|
-
const chosen = new Map<number,
|
|
114
|
+
const chosen = new Map<number, EvidenceItem[]>();
|
|
269
115
|
let remaining =
|
|
270
116
|
options.budgetTokens - Math.ceil(JSON.stringify({ observedSetup, messages: [] }).length / 4);
|
|
271
117
|
const anchorItems = [...anchors].flatMap(content);
|
|
272
118
|
const recentItems = [...recent].flatMap(content);
|
|
273
119
|
// Shorten the newest turn first, then the original request too, so both always appear.
|
|
274
120
|
const required =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
121
|
+
fitEvidence(recentItems, remaining - evidenceItemsCost(anchorItems), omissionMarker)?.concat(
|
|
122
|
+
anchorItems,
|
|
123
|
+
) ??
|
|
124
|
+
fitEvidence([...recentItems, ...anchorItems], remaining, omissionMarker) ??
|
|
125
|
+
shortenEvidence([...recentItems, ...anchorItems], 0, omissionMarker);
|
|
126
|
+
remaining -= evidenceItemsCost(required);
|
|
279
127
|
const shortenedRequired = new Map(
|
|
280
128
|
[...recentItems, ...anchorItems].map((item, index) => [item, required[index]] as const),
|
|
281
129
|
);
|
|
@@ -286,10 +134,10 @@ export function selectContextSeed(
|
|
|
286
134
|
);
|
|
287
135
|
for (let unit = newest; unit >= 0; unit--) {
|
|
288
136
|
if (chosen.has(unit)) continue;
|
|
289
|
-
const items =
|
|
137
|
+
const items = fitEvidence(content(unit), remaining, omissionMarker);
|
|
290
138
|
if (!items) break;
|
|
291
139
|
chosen.set(unit, items);
|
|
292
|
-
remaining -=
|
|
140
|
+
remaining -= evidenceItemsCost(items);
|
|
293
141
|
if (items.some((item, index) => item !== content(unit)[index])) break;
|
|
294
142
|
}
|
|
295
143
|
|
|
@@ -302,88 +150,8 @@ export function selectContextSeed(
|
|
|
302
150
|
const original = ordered.flatMap(content);
|
|
303
151
|
return {
|
|
304
152
|
observedSetup,
|
|
305
|
-
...
|
|
153
|
+
...combineEvidence(items),
|
|
306
154
|
kept,
|
|
307
155
|
shortened: items.filter((item, index) => item.message !== original[index]?.message).length,
|
|
308
156
|
};
|
|
309
157
|
}
|
|
310
|
-
|
|
311
|
-
/** Join individually projected messages, renumbering their image attachments. */
|
|
312
|
-
function combine(items: readonly Projected[]): Evidence {
|
|
313
|
-
const images: ImageContent[] = [];
|
|
314
|
-
const messages = items.map(({ message, images: own }) => {
|
|
315
|
-
const offset = images.length;
|
|
316
|
-
images.push(...own);
|
|
317
|
-
if (!offset || !own.length || !Array.isArray(message.content)) return message;
|
|
318
|
-
const content = message.content.map((block) =>
|
|
319
|
-
block.type === "image" ? { ...block, attachment: block.attachment + offset } : block,
|
|
320
|
-
);
|
|
321
|
-
return { ...message, content };
|
|
322
|
-
});
|
|
323
|
-
return { messages, images };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** Project observed messages in order; attachment indexes restart at 1 for each projection. */
|
|
327
|
-
export function projectEvidence(messages: readonly Message[]): Evidence {
|
|
328
|
-
const images: ImageContent[] = [];
|
|
329
|
-
/** User and tool-result content; unknown future block types are omitted, not forwarded. */
|
|
330
|
-
const media = (blocks: readonly (TextContent | ImageContent)[]) =>
|
|
331
|
-
blocks.flatMap((block): EvidenceBlock[] => {
|
|
332
|
-
if (block.type === "text") return [{ type: "text", text: block.text }];
|
|
333
|
-
if (block.type === "image") {
|
|
334
|
-
images.push(block);
|
|
335
|
-
return [{ type: "image", attachment: images.length }];
|
|
336
|
-
}
|
|
337
|
-
return [];
|
|
338
|
-
});
|
|
339
|
-
const projected = messages.flatMap((message): EvidenceMessage[] => {
|
|
340
|
-
switch (message.role) {
|
|
341
|
-
case "user":
|
|
342
|
-
return [
|
|
343
|
-
{
|
|
344
|
-
role: "user",
|
|
345
|
-
content: Array.isArray(message.content) ? media(message.content) : message.content,
|
|
346
|
-
},
|
|
347
|
-
];
|
|
348
|
-
case "toolResult":
|
|
349
|
-
return [
|
|
350
|
-
{
|
|
351
|
-
role: "toolResult",
|
|
352
|
-
ref: toolCallRef(message.toolCallId),
|
|
353
|
-
toolName: message.toolName,
|
|
354
|
-
isError: message.isError,
|
|
355
|
-
content: media(message.content),
|
|
356
|
-
},
|
|
357
|
-
];
|
|
358
|
-
case "assistant": {
|
|
359
|
-
const content = message.content.flatMap((block): EvidenceBlock[] => {
|
|
360
|
-
if (block.type === "text") return [{ type: "text", text: block.text }];
|
|
361
|
-
if (block.type === "thinking") {
|
|
362
|
-
if (block.redacted) return [{ type: "thinking", redacted: true }];
|
|
363
|
-
return block.thinking.trim() ? [{ type: "thinking", thinking: block.thinking }] : [];
|
|
364
|
-
}
|
|
365
|
-
if (block.type === "toolCall")
|
|
366
|
-
return [
|
|
367
|
-
{
|
|
368
|
-
type: "toolCall",
|
|
369
|
-
ref: toolCallRef(block.id),
|
|
370
|
-
name: block.name,
|
|
371
|
-
arguments: block.arguments,
|
|
372
|
-
},
|
|
373
|
-
];
|
|
374
|
-
return [];
|
|
375
|
-
});
|
|
376
|
-
const stop = message.stopReason;
|
|
377
|
-
if (stop !== "error" && stop !== "aborted" && stop !== "length")
|
|
378
|
-
return [{ role: "assistant", content }];
|
|
379
|
-
const abnormal: EvidenceMessage = { role: "assistant", content, stopReason: stop };
|
|
380
|
-
if (message.errorMessage) abnormal.errorMessage = message.errorMessage;
|
|
381
|
-
return [abnormal];
|
|
382
|
-
}
|
|
383
|
-
default:
|
|
384
|
-
// System messages reach the Advisor through the Observed Setup; unknown roles are omitted.
|
|
385
|
-
return [];
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
return { messages: projected, images };
|
|
389
|
-
}
|