feat(coding-agent): added scoped snapcompact system-prompt imaging modes
- Added `snapcompact.systemPrompt` enum modes `none`, `agents-md`, and `all` with default `none`. - Added AGENTS.md-mode context extraction and mode-specific system-frame injection. - Updated `/context` and `/debug` request reporting to include snapcompact deltas, swap reasons, and estimates. - Normalized legacy boolean `snapcompact.systemPrompt` values to enum strings for compatibility.
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `snapcompact.systemPrompt` enum modes `none`, `agents-md`, and `all`, so users can disable system-prompt imaging, image only loaded AGENTS.md/context-file instruction sections, or image the full system prompt
|
||||
- Added `agents-md` snapcompact mode that rasterizes AGENTS.md-style context sections from the system prompt into frames and appends them to the first user message
|
||||
- `/context` (TUI panel and ACP report) now shows estimated snapcompact wire savings when `snapcompact.systemPrompt` or `snapcompact.toolResults` is enabled — per-feature text → frames token deltas, the reason a swap does not apply (savings margin, image budget, or text-only model), and the estimated size of the next request. The estimate and the live provider-request transform share one planner (`planInlineSwaps`) so displayed numbers cannot drift from wire behavior.
|
||||
- Added `/debug dump-request` and `/debug next-request` as aliases for `/debug dump-next-request` when arming a one-shot AI provider request dump
|
||||
- Added `/debug dump-next-request <path>` to dump the next AI provider HTTP request JSON to a chosen file.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `/context` system-prompt savings lines to show the scope as `AGENTS.md` or `all`, so savings estimates now indicate whether imaging applied only to context-file instructions or the entire prompt
|
||||
- Changed `/debug` handling in interactive mode so `/debug` with arguments now executes the requested debug subcommand instead of always opening the debug selector
|
||||
- Changed `/debug dump-next-request` path handling to expand `~` and resolve relative paths against the current working directory
|
||||
- Changed the task tool's TUI block: the header now shows the task dispatch glyph (`tool.task`) while agents are in flight instead of a spinner (async spawns return immediately, so a spinner misread the call as blocking), and per-agent rows use one static dot for every state — completed rows keep the same dot and settle from accent to the plain foreground color instead of switching to a different status glyph
|
||||
|
||||
@@ -1572,14 +1572,24 @@ export const SETTINGS_SCHEMA = {
|
||||
|
||||
// Experimental: snapcompact inline imaging (transient, per-request; never persisted)
|
||||
"snapcompact.systemPrompt": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
type: "enum",
|
||||
values: ["none", "agents-md", "all"] as const,
|
||||
default: "none",
|
||||
ui: {
|
||||
tab: "context",
|
||||
group: "Experimental",
|
||||
label: "Snapcompact System Prompt",
|
||||
description:
|
||||
"Experimental: render the system prompt as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses system-prompt prompt caching.",
|
||||
"Experimental: render selected system prompt text as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses prompt caching for imaged text.",
|
||||
options: [
|
||||
{ value: "none", label: "None", description: "Keep the system prompt as text." },
|
||||
{
|
||||
value: "agents-md",
|
||||
label: "AGENTS.md",
|
||||
description: "Only move loaded context-file instructions to images, when that saves tokens.",
|
||||
},
|
||||
{ value: "all", label: "All", description: "Move the full system prompt to images, when that saves tokens." },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -798,6 +798,15 @@ export class Settings {
|
||||
raw["compaction.strategy"] = "shake";
|
||||
}
|
||||
|
||||
// snapcompact.systemPrompt: boolean -> scoped enum.
|
||||
const snapcompactObj = raw.snapcompact as Record<string, unknown> | undefined;
|
||||
if (snapcompactObj && typeof snapcompactObj.systemPrompt === "boolean") {
|
||||
snapcompactObj.systemPrompt = snapcompactObj.systemPrompt ? "all" : "none";
|
||||
}
|
||||
if (typeof raw["snapcompact.systemPrompt"] === "boolean") {
|
||||
raw["snapcompact.systemPrompt"] = raw["snapcompact.systemPrompt"] ? "all" : "none";
|
||||
}
|
||||
|
||||
// statusLine: rename "plan_mode" segment to "mode"
|
||||
const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
|
||||
if (statusLineObj) {
|
||||
|
||||
@@ -181,7 +181,7 @@ export function computeContextBreakdown(
|
||||
if (options?.snapcompactSavings) {
|
||||
const renderSystemPrompt = session.settings.get("snapcompact.systemPrompt");
|
||||
const renderToolResults = session.settings.get("snapcompact.toolResults");
|
||||
if (renderSystemPrompt || renderToolResults) {
|
||||
if (renderSystemPrompt !== "none" || renderToolResults) {
|
||||
snapcompactSavings = estimateInlineSavings({
|
||||
options: { renderSystemPrompt, renderToolResults },
|
||||
model,
|
||||
@@ -332,7 +332,7 @@ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): str
|
||||
const sp = snap.systemPrompt;
|
||||
if (sp.applied) {
|
||||
lines.push(
|
||||
` System prompt: saves ${theme.bold(`~${formatNumber(sp.savedTokens)}`)} ` +
|
||||
` System prompt (${sp.scope === "agents-md" ? "AGENTS.md" : "all"}): saves ${theme.bold(`~${formatNumber(sp.savedTokens)}`)} ` +
|
||||
theme.fg(
|
||||
"dim",
|
||||
`(${formatNumber(sp.textTokens)} text → ${sp.frames} frame${sp.frames === 1 ? "" : "s"} ≈ ${formatNumber(sp.imageTokens)})`,
|
||||
@@ -345,7 +345,7 @@ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): str
|
||||
: sp.reason === "empty"
|
||||
? "nothing to image"
|
||||
: "frames would not save tokens";
|
||||
lines.push(` System prompt: ${theme.fg("dim", `stays text (${reason})`)}`);
|
||||
lines.push(` System prompt (${sp.scope === "agents-md" ? "AGENTS.md" : "all"}): ${theme.fg("dim", `stays text (${reason})`)}`);
|
||||
}
|
||||
}
|
||||
if (snap.toolResults) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
=== CONTEXT FILE INSTRUCTIONS — read the image(s) below as the loaded context files replaced in the system prompt ===
|
||||
@@ -0,0 +1 @@
|
||||
Loaded context-file instructions were moved to PNG image(s) attached below at the start of the first user message. Read every frame in order where this marker appears, then apply those instructions as if the original context-file text remained here.
|
||||
@@ -2162,10 +2162,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
// Per-request provider-context transforms. Obfuscate FIRST so secrets are
|
||||
// redacted from text before snapcompact rasterizes it into PNG frames.
|
||||
// Both operate on the transient outgoing Context only — never persisted.
|
||||
const snapcompactSystemPromptMode = settings.get("snapcompact.systemPrompt");
|
||||
const snapcompactInline =
|
||||
settings.get("snapcompact.systemPrompt") || settings.get("snapcompact.toolResults")
|
||||
snapcompactSystemPromptMode !== "none" || settings.get("snapcompact.toolResults")
|
||||
? new SnapcompactInlineTransformer({
|
||||
renderSystemPrompt: settings.get("snapcompact.systemPrompt"),
|
||||
renderSystemPrompt: snapcompactSystemPromptMode,
|
||||
renderToolResults: settings.get("snapcompact.toolResults"),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Snapcompact inline imaging: per-request transform that swaps the system
|
||||
* prompt and/or large historical tool results for dense PNG frames on
|
||||
* vision-capable models.
|
||||
*
|
||||
* prompt, loaded context-file instructions, and/or large historical tool
|
||||
* results for dense PNG frames on vision-capable models.
|
||||
* Runs inside the agent loop's `transformProviderContext` hook — after the
|
||||
* persisted history is converted to the outgoing `Context`, before the
|
||||
* provider stream call. It only ever builds NEW message objects/arrays; the
|
||||
@@ -14,15 +13,20 @@
|
||||
* `planInlineSwaps`, shared by the transform and the `/context` savings
|
||||
* estimate (`estimateInlineSavings`) so the two can never disagree.
|
||||
*/
|
||||
|
||||
import type { Context, ImageContent, Model, TextContent, ToolResultMessage, UserMessage } from "@oh-my-pi/pi-ai";
|
||||
import { countTokens } from "@oh-my-pi/pi-natives";
|
||||
import * as snapcompact from "@oh-my-pi/snapcompact";
|
||||
import contextFramesNote from "../prompts/system/snapcompact-context-frames-note.md" with { type: "text" };
|
||||
import contextStub from "../prompts/system/snapcompact-context-stub.md" with { type: "text" };
|
||||
import systemFramesNote from "../prompts/system/snapcompact-system-frames-note.md" with { type: "text" };
|
||||
import systemStub from "../prompts/system/snapcompact-system-stub.md" with { type: "text" };
|
||||
import toolResultNote from "../prompts/system/snapcompact-toolresult-note.md" with { type: "text" };
|
||||
|
||||
export type SnapcompactSystemPromptMode = "none" | "agents-md" | "all";
|
||||
|
||||
export interface SnapcompactInlineOptions {
|
||||
renderSystemPrompt: boolean;
|
||||
renderSystemPrompt: SnapcompactSystemPromptMode;
|
||||
renderToolResults: boolean;
|
||||
}
|
||||
|
||||
@@ -69,6 +73,57 @@ function passesSavingsGate(frames: number, shape: snapcompact.Shape, textTokens:
|
||||
return frames * shape.frameTokenEstimate <= textTokens * SAVINGS_MARGIN;
|
||||
}
|
||||
|
||||
interface SystemPromptImageTarget {
|
||||
scope: Exclude<SnapcompactSystemPromptMode, "none">;
|
||||
text: string;
|
||||
replacement: string[];
|
||||
userNote: string;
|
||||
}
|
||||
|
||||
const CONTEXT_SECTION_PATTERNS = [
|
||||
/<context>\n[\s\S]*?\n<\/context>/g,
|
||||
/## Context\n<instructions>\n[\s\S]*?\n<\/instructions>/g,
|
||||
] as const;
|
||||
|
||||
function replaceContextSections(block: string, extracted: string[]): string {
|
||||
let replaced = block;
|
||||
for (const pattern of CONTEXT_SECTION_PATTERNS) {
|
||||
replaced = replaced.replace(pattern, match => {
|
||||
extracted.push(match.trim());
|
||||
return contextStub.trim();
|
||||
});
|
||||
}
|
||||
return replaced;
|
||||
}
|
||||
|
||||
function selectSystemPromptImageTarget(
|
||||
systemPrompt: readonly string[] | undefined,
|
||||
mode: SnapcompactSystemPromptMode,
|
||||
): SystemPromptImageTarget | undefined {
|
||||
if (!systemPrompt?.length || mode === "none") return undefined;
|
||||
if (mode === "all") {
|
||||
const text = systemPrompt.join("\n\n");
|
||||
if (!text) return undefined;
|
||||
return {
|
||||
scope: "all",
|
||||
text,
|
||||
replacement: [systemStub],
|
||||
userNote: systemFramesNote,
|
||||
};
|
||||
}
|
||||
|
||||
const extracted: string[] = [];
|
||||
const replacement = systemPrompt.map(block => replaceContextSections(block, extracted));
|
||||
const text = extracted.join("\n\n");
|
||||
if (!text) return undefined;
|
||||
return {
|
||||
scope: "agents-md",
|
||||
text,
|
||||
replacement,
|
||||
userNote: contextFramesNote,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Swap planning (shared by the live transform and /context estimation)
|
||||
// ============================================================================
|
||||
@@ -97,9 +152,9 @@ export interface InlinePlanInput {
|
||||
budget: number;
|
||||
/** All tool results in context order, INCLUDING the most recent one. */
|
||||
toolResults: readonly InlineToolResultCandidate[];
|
||||
/** Joined system prompt; undefined when absent or system-prompt imaging is off. */
|
||||
/** Selected prompt text; undefined when system-prompt imaging is off or empty. */
|
||||
systemPrompt: InlineSystemPromptCandidate | undefined;
|
||||
/** Whether a user message exists to carry the system-prompt frames. */
|
||||
/** Whether a user message exists to carry the prompt frames. */
|
||||
hasUserMessage: boolean;
|
||||
}
|
||||
|
||||
@@ -135,7 +190,7 @@ export function planInlineSwaps(input: InlinePlanInput): InlineSwapPlan {
|
||||
|
||||
let systemPrompt: InlineSystemPromptCandidate | undefined;
|
||||
if (
|
||||
input.options.renderSystemPrompt &&
|
||||
input.options.renderSystemPrompt !== "none" &&
|
||||
input.systemPrompt &&
|
||||
budget > 0 &&
|
||||
input.systemPrompt.frames > 0 &&
|
||||
@@ -178,6 +233,7 @@ export interface SnapcompactSavingsEstimate {
|
||||
/** Estimated billed tokens for the frames (0 when there are none). */
|
||||
imageTokens: number;
|
||||
savedTokens: number;
|
||||
scope: Exclude<SnapcompactSystemPromptMode, "none">;
|
||||
};
|
||||
/** Present iff tool-result imaging is enabled. */
|
||||
toolResults?: {
|
||||
@@ -249,13 +305,16 @@ export function estimateInlineSavings(input: {
|
||||
}
|
||||
}
|
||||
|
||||
let systemPromptTarget: SystemPromptImageTarget | undefined;
|
||||
let systemPromptCandidate: InlineSystemPromptCandidate | undefined;
|
||||
if (options.renderSystemPrompt && input.systemPrompt.length > 0) {
|
||||
const joined = input.systemPrompt.join("\n\n");
|
||||
systemPromptCandidate = {
|
||||
textTokens: countTokens(joined),
|
||||
frames: snapcompact.frames(joined, { shape }),
|
||||
};
|
||||
if (options.renderSystemPrompt !== "none") {
|
||||
systemPromptTarget = selectSystemPromptImageTarget(input.systemPrompt, options.renderSystemPrompt);
|
||||
if (systemPromptTarget) {
|
||||
systemPromptCandidate = {
|
||||
textTokens: countTokens(systemPromptTarget.text),
|
||||
frames: snapcompact.frames(systemPromptTarget.text, { shape }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const plan = planInlineSwaps({
|
||||
@@ -269,7 +328,7 @@ export function estimateInlineSavings(input: {
|
||||
|
||||
let savedTokens = 0;
|
||||
let systemPromptEstimate: SnapcompactSavingsEstimate["systemPrompt"];
|
||||
if (options.renderSystemPrompt) {
|
||||
if (options.renderSystemPrompt !== "none") {
|
||||
const candidate = systemPromptCandidate ?? { textTokens: 0, frames: 0 };
|
||||
const applied = plan.systemPrompt !== undefined;
|
||||
const imageTokens = candidate.frames * shape.frameTokenEstimate;
|
||||
@@ -288,6 +347,7 @@ export function estimateInlineSavings(input: {
|
||||
frames: candidate.frames,
|
||||
imageTokens,
|
||||
savedTokens: saved,
|
||||
scope: systemPromptTarget?.scope ?? options.renderSystemPrompt,
|
||||
};
|
||||
savedTokens += saved;
|
||||
}
|
||||
@@ -383,14 +443,16 @@ export class SnapcompactInlineTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
let systemPromptTarget: SystemPromptImageTarget | undefined;
|
||||
let systemPromptCandidate: InlineSystemPromptCandidate | undefined;
|
||||
let joinedSystemPrompt = "";
|
||||
if (this.options.renderSystemPrompt && context.systemPrompt?.length) {
|
||||
joinedSystemPrompt = context.systemPrompt.join("\n\n");
|
||||
systemPromptCandidate = {
|
||||
textTokens: countTokens(joinedSystemPrompt),
|
||||
frames: snapcompact.frames(joinedSystemPrompt, { shape }),
|
||||
};
|
||||
if (this.options.renderSystemPrompt !== "none") {
|
||||
systemPromptTarget = selectSystemPromptImageTarget(context.systemPrompt, this.options.renderSystemPrompt);
|
||||
if (systemPromptTarget) {
|
||||
systemPromptCandidate = {
|
||||
textTokens: countTokens(systemPromptTarget.text),
|
||||
frames: snapcompact.frames(systemPromptTarget.text, { shape }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const userIndex = messages.findIndex(message => message.role === "user");
|
||||
@@ -420,13 +482,13 @@ export class SnapcompactInlineTransformer {
|
||||
}
|
||||
|
||||
let systemPrompt = context.systemPrompt;
|
||||
if (plan.systemPrompt && userIndex >= 0) {
|
||||
const hash = Bun.hash(joinedSystemPrompt);
|
||||
if (plan.systemPrompt && userIndex >= 0 && systemPromptTarget) {
|
||||
const hash = Bun.hash(systemPromptTarget.text);
|
||||
let cached = this.#systemCache;
|
||||
if (!cached || cached.hash !== hash) {
|
||||
cached = {
|
||||
hash,
|
||||
frames: snapcompact.renderMany(joinedSystemPrompt, { shape, maxFrames: MAX_SYSTEM_PROMPT_FRAMES }),
|
||||
frames: snapcompact.renderMany(systemPromptTarget.text, { shape, maxFrames: MAX_SYSTEM_PROMPT_FRAMES }),
|
||||
};
|
||||
this.#systemCache = cached;
|
||||
}
|
||||
@@ -436,9 +498,9 @@ export class SnapcompactInlineTransformer {
|
||||
typeof original.content === "string" ? [{ type: "text", text: original.content }] : original.content;
|
||||
messages[userIndex] = {
|
||||
...original,
|
||||
content: [{ type: "text", text: systemFramesNote }, ...frames, ...originalContent],
|
||||
content: [{ type: "text", text: systemPromptTarget.userNote }, ...frames, ...originalContent],
|
||||
};
|
||||
systemPrompt = [systemStub];
|
||||
systemPrompt = systemPromptTarget.replacement;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,14 +56,14 @@ describe("renderContextUsage snapcompact section", () => {
|
||||
const output = renderContextUsage(
|
||||
breakdownWith({
|
||||
visionCapable: true,
|
||||
systemPrompt: { applied: true, textTokens: 9768, frames: 2, imageTokens: 6600, savedTokens: 3168 },
|
||||
systemPrompt: { applied: true, scope: "all", textTokens: 9768, frames: 2, imageTokens: 6600, savedTokens: 3168 },
|
||||
toolResults: { total: 3, swapped: 0, textTokens: 0, frames: 0, imageTokens: 0, savedTokens: 0 },
|
||||
savedTokens: 3168,
|
||||
}),
|
||||
themeStub,
|
||||
);
|
||||
expect(output).toContain("Snapcompact (estimated wire savings)");
|
||||
expect(output).toContain("System prompt: saves ~3.2K (9.8K text → 2 frames ≈ 6.6K)");
|
||||
expect(output).toContain("System prompt (all): saves ~3.2K (9.8K text → 2 frames ≈ 6.6K)");
|
||||
expect(output).toContain("Tool results: none imaged (3 in history)");
|
||||
// 27929 logical − 3168 saved ≈ 25K on the wire.
|
||||
expect(output).toContain("Next request: ~25K tokens on the wire");
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Effort } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
type SettingPath,
|
||||
getDefault,
|
||||
onAppendOnlyModeChanged,
|
||||
onStatusLineSessionAccentChanged,
|
||||
@@ -121,6 +122,12 @@ describe("Settings", () => {
|
||||
expect(settings.get("enabledModels")).toEqual(["always-model", "other-model"]);
|
||||
expect(settings.get("disabledProviders")).toEqual(["always-provider", "other-provider"]);
|
||||
});
|
||||
|
||||
it("migrates legacy snapcompact system prompt booleans to scoped modes", () => {
|
||||
expect(Settings.isolated({ "snapcompact.systemPrompt": true }).get("snapcompact.systemPrompt")).toBe("all");
|
||||
const nestedLegacy = { snapcompact: { systemPrompt: false } } as Partial<Record<SettingPath, unknown>>;
|
||||
expect(Settings.isolated(nestedLegacy).get("snapcompact.systemPrompt")).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("statusLine.sessionAccent hooks", () => {
|
||||
|
||||
@@ -79,13 +79,13 @@ function imageCount(context: Context): number {
|
||||
|
||||
describe("SnapcompactInlineTransformer", () => {
|
||||
it("is a no-op for text-only models", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: true, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "all", renderToolResults: true });
|
||||
const context = makeContext();
|
||||
expect(transformer.transform(context, makeModel({ input: ["text"] }))).toBe(context);
|
||||
});
|
||||
|
||||
it("images large historical tool results, keeping small and most-recent ones as text", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: false, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "none", renderToolResults: true });
|
||||
const context = makeContext();
|
||||
const result = transformer.transform(context, makeModel());
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
});
|
||||
|
||||
it("never mutates the input context (persisted history shares these references)", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: true, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "all", renderToolResults: true });
|
||||
const context = makeContext();
|
||||
const originalMessages = context.messages;
|
||||
const originalSystemPrompt = context.systemPrompt;
|
||||
@@ -128,7 +128,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
});
|
||||
|
||||
it("leaves tool results that already carry images untouched", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: false, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "none", renderToolResults: true });
|
||||
const withImage: ToolResultMessage = {
|
||||
...toolResult("call_img", LARGE),
|
||||
content: [
|
||||
@@ -144,7 +144,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
});
|
||||
|
||||
it("replaces a large system prompt with a stub and rides frames on the first user message", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: true, renderToolResults: false });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "all", renderToolResults: false });
|
||||
const longPrompt = denseText(3000);
|
||||
const context: Context = {
|
||||
systemPrompt: [longPrompt],
|
||||
@@ -164,8 +164,33 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
expect(carrier.content[carrier.content.length - 1]).toEqual({ type: "text", text: "do the thing" });
|
||||
});
|
||||
|
||||
it("moves only loaded context-file instructions when AGENTS.md mode is selected", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "agents-md", renderToolResults: false });
|
||||
const longContext = denseText(3000);
|
||||
const context: Context = {
|
||||
systemPrompt: [
|
||||
`Core instructions.\n\n<context>\nYou MUST follow the context files below for all tasks:\n<file path="AGENTS.md">\n${longContext}\n</file>\n</context>\n\nToday is 2026-06-12.`,
|
||||
"Final system block.",
|
||||
],
|
||||
messages: [userMessage("do the thing")],
|
||||
};
|
||||
const result = transformer.transform(context, makeModel());
|
||||
|
||||
expect(result.systemPrompt).toHaveLength(2);
|
||||
expect(result.systemPrompt![0]).toContain("Core instructions.");
|
||||
expect(result.systemPrompt![0]).toContain("Today is 2026-06-12.");
|
||||
expect(result.systemPrompt![0]).toContain("Loaded context-file instructions were moved");
|
||||
expect(result.systemPrompt![0]).not.toContain(longContext);
|
||||
expect(result.systemPrompt![1]).toBe("Final system block.");
|
||||
|
||||
const carrier = result.messages[0] as { content: (TextContent | ImageContent)[] };
|
||||
expect((carrier.content[0] as TextContent).text).toContain("CONTEXT FILE INSTRUCTIONS");
|
||||
expect(carrier.content.some(block => block.type === "image")).toBe(true);
|
||||
expect(carrier.content[carrier.content.length - 1]).toEqual({ type: "text", text: "do the thing" });
|
||||
});
|
||||
|
||||
it("keeps a small system prompt as text and skips when no user message exists", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: true, renderToolResults: false });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "all", renderToolResults: false });
|
||||
const small: Context = { systemPrompt: ["Be terse."], messages: [userMessage("hi")] };
|
||||
expect(transformer.transform(small, makeModel())).toBe(small);
|
||||
|
||||
@@ -174,7 +199,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
});
|
||||
|
||||
it("never rasterizes tool results under the 3k-token floor, even when frames are cheaper", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: false, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "none", renderToolResults: true });
|
||||
// ~1.5k tokens: the google shape estimates 1 frame ≈ 1100 tokens, so the
|
||||
// savings gate alone would rasterize this — the floor must keep it text.
|
||||
const midsize = denseText(500);
|
||||
@@ -186,7 +211,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
});
|
||||
|
||||
it("respects the per-provider image budget for unknown providers", () => {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: false, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "none", renderToolResults: true });
|
||||
const context: Context = {
|
||||
messages: [
|
||||
userMessage("go"),
|
||||
@@ -207,7 +232,7 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
it("caches renders across turns: identical input does not re-rasterize", () => {
|
||||
const spy = spyOn(snapcompact, "renderMany");
|
||||
try {
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: true, renderToolResults: true });
|
||||
const transformer = new SnapcompactInlineTransformer({ renderSystemPrompt: "all", renderToolResults: true });
|
||||
const context = makeContext();
|
||||
const model = makeModel();
|
||||
|
||||
@@ -232,8 +257,8 @@ describe("SnapcompactInlineTransformer", () => {
|
||||
|
||||
describe("planInlineSwaps", () => {
|
||||
const shape = snapcompact.resolveShape("anthropic-messages");
|
||||
const toolOnly = { renderSystemPrompt: false, renderToolResults: true };
|
||||
const promptOnly = { renderSystemPrompt: true, renderToolResults: false };
|
||||
const toolOnly = { renderSystemPrompt: "none" as const, renderToolResults: true };
|
||||
const promptOnly = { renderSystemPrompt: "all" as const, renderToolResults: false };
|
||||
|
||||
it("never swaps the most recent tool result", () => {
|
||||
const plan = planInlineSwaps({
|
||||
@@ -288,7 +313,7 @@ describe("planInlineSwaps", () => {
|
||||
|
||||
it("gives the system prompt only the budget tool results left over", () => {
|
||||
const input = {
|
||||
options: { renderSystemPrompt: true, renderToolResults: true },
|
||||
options: { renderSystemPrompt: "all" as const, renderToolResults: true },
|
||||
shape,
|
||||
budget: 2,
|
||||
toolResults: [
|
||||
@@ -334,7 +359,7 @@ describe("planInlineSwaps", () => {
|
||||
describe("estimateInlineSavings", () => {
|
||||
it("reports vision-incapable models as inactive with zero savings", () => {
|
||||
const estimate = estimateInlineSavings({
|
||||
options: { renderSystemPrompt: true, renderToolResults: true },
|
||||
options: { renderSystemPrompt: "all", renderToolResults: true },
|
||||
model: makeModel({ input: ["text"] }),
|
||||
systemPrompt: [LARGE],
|
||||
messages: [],
|
||||
@@ -347,7 +372,7 @@ describe("estimateInlineSavings", () => {
|
||||
|
||||
it("assumes the next request carries a user message even with empty history", () => {
|
||||
const estimate = estimateInlineSavings({
|
||||
options: { renderSystemPrompt: true, renderToolResults: false },
|
||||
options: { renderSystemPrompt: "all", renderToolResults: false },
|
||||
model: makeModel(),
|
||||
systemPrompt: [LARGE],
|
||||
messages: [],
|
||||
@@ -366,7 +391,7 @@ describe("estimateInlineSavings", () => {
|
||||
|
||||
it("explains why a small system prompt stays text", () => {
|
||||
const estimate = estimateInlineSavings({
|
||||
options: { renderSystemPrompt: true, renderToolResults: false },
|
||||
options: { renderSystemPrompt: "all", renderToolResults: false },
|
||||
model: makeModel(),
|
||||
systemPrompt: ["Be terse."],
|
||||
messages: [],
|
||||
@@ -377,7 +402,7 @@ describe("estimateInlineSavings", () => {
|
||||
});
|
||||
|
||||
it("matches what the transform actually swaps on the same context", () => {
|
||||
const options = { renderSystemPrompt: true, renderToolResults: true };
|
||||
const options = { renderSystemPrompt: "all" as const, renderToolResults: true };
|
||||
const context = makeContext();
|
||||
const model = makeModel();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user