compaction: the leading system message is request chrome, never cut (fix the no-tools request)
Owner report (OpenRouter logs, 2026-10-05): the post-compaction request arrived with NO system prompt and NO tool definitions — everything before the cut replaced by the compaction message. Root cause: pi-agent-core's Agent seeds a SystemMessage carrying the system prompt AND the tool declarations at state.messages[0] (createInitialSystemMessage — 'messages declare the provider-visible loadout'). The record never persists it (ui-stream's settleWith filters the role), but convertToLlm DOES receive it — and the cut projection sliced from firstKeptMessageId, dropping [0] from the wire: no prompt, no tools. Fix (pi's own law — verified against pi-agent-core + pi's coding-agent): the projection now carries the leading system message through AHEAD of the compaction head, exactly as pi's prepareRequest re-applies context.systemPrompt + context.tools around its compaction projection. The cut slices the RECORD's history; the chrome is never compactable payload. Loop start skips [0] when chrome is present (no duplicate); record-path callers are unchanged. Pinned: chrome-first-through-cut, exactly-once without a cut, record-path head-first.
This commit is contained in:
@@ -491,8 +491,23 @@ export function toLlmMessages(messages: readonly AgentMessage[]): Message[] {
|
||||
};
|
||||
}
|
||||
}
|
||||
const out: Message[] = head ? [head] : [];
|
||||
for (let i = start; i < messages.length; i++) {
|
||||
// THE LEADING SYSTEM MESSAGE IS REQUEST CHROME, NEVER COMPACTABLE
|
||||
// PAYLOAD (the OpenRouter no-tools incident, owner 2026-10-05):
|
||||
// pi-agent-core's Agent seeds a SystemMessage carrying the system
|
||||
// prompt AND the tool declarations at state.messages[0]
|
||||
// (createInitialSystemMessage — "messages declare the provider-visible
|
||||
// loadout"). The record never persists it (ui-stream's settleWith
|
||||
// filters the role), so RECORD-path callers never see it — but the
|
||||
// Agent's convertToLlm DOES. The cut slices the RECORD's history;
|
||||
// when [0] is the chrome it must carry through AHEAD of the
|
||||
// compaction head, exactly as pi's own prepareRequest re-applies
|
||||
// context.systemPrompt + context.tools around its projection.
|
||||
const leading =
|
||||
(messages[0] as { role?: string } | undefined)?.role === "system" ? (messages[0] as Message) : null;
|
||||
const out: Message[] = [];
|
||||
if (leading) out.push(leading);
|
||||
if (head) out.push(head);
|
||||
for (let i = leading ? Math.max(start, 1) : start; i < messages.length; i++) {
|
||||
const m = messages[i]!;
|
||||
const role = (m as { role?: string }).role;
|
||||
if (role === "custom") {
|
||||
|
||||
@@ -143,7 +143,11 @@ export function effectiveThresholdTokens(settingTokens: number | undefined, cont
|
||||
// ─── the estimate over the ACTIVE projection ────────────────────────────────
|
||||
|
||||
/** The projected context estimate: what the next LLM call would carry —
|
||||
* toLlmMessages output (cut honored) + the system prompt. chars/4. */
|
||||
* toLlmMessages output (cut honored) + the system prompt. The record
|
||||
* path carries no leading system message (settleWith filters it), so
|
||||
* the chrome rides as the additive term instead: prompt text here,
|
||||
* tool declarations absorbed by the 16k reserve (the declarations are
|
||||
* a few k and stable per session). chars/4. */
|
||||
export function projectedContextTokens(messages: readonly LhMessage[], systemPromptTokens: number): number {
|
||||
return estimateTokens(toLlmMessages(messages as readonly AgentMessage[]) as unknown as readonly LhMessage[]) + systemPromptTokens;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user