Merge pull request #689 from rowboatlabs/runtime_optimizations

token optimizations
This commit is contained in:
Ramnique Singh 2026-07-07 21:11:41 +05:30 committed by GitHub
commit 24a87ea71a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1163 additions and 8 deletions

View file

@ -390,6 +390,40 @@ Rules:
It rejects the execution and does not append `turn_failed`.
- The reducer treats `context` as opaque data and never resolves references.
The app wires the resolver through a decorator (`context-elision.ts`) that
applies transmit-time elision to the materialized prefix: tool results from
prior turns above a size threshold are replaced with a short placeholder
telling the model to re-run the tool if it needs the output; inline image
parts from prior turns (video-mode webcam and screen-share frames) are
replaced with a text part recording how many frames of each kind were
dropped; and middle-pane note snapshots on prior-turn user messages keep
their kind and path but have their content replaced with a placeholder
pointing at the still-readable file. The durable log is untouched; only the
transmitted bytes change.
Elision is a pure function of each message, so resolved prefixes stay
byte-stable across calls (provider prefix caches keep working), and the
current turn's own messages never pass through the resolver, so in-flight
tool results and just-captured frames are always sent verbatim. Policy lives
in `config/context.json` (`elideHistoricToolResults`, default true;
`elideHistoricToolResultsThresholdChars`, default 2500;
`elideHistoricImages`, default true; `elideHistoricMiddlePaneContent`,
default true). The inspect CLI composes through the same decorated resolver.
Caveat: elision makes the composed payload a function of the durable log
PLUS the config in effect at compose time — the one deliberate exception to
"recomposition from durable state alone" (§8.3). Within a turn this is
harmless (policy is loaded once per execution, at resolve time), but
inspecting an old turn after a config change may show different prefix
bytes than were transmitted; the inspect CLI prints the active policy so
the divergence is visible. If exact-bytes replay ever becomes a hard
requirement, record the applied policy on the turn and compose from that.
Relatedly, Anthropic-family requests get cache_control breakpoints stamped
at the transport layer (`models/prompt-caching.ts`, applied inside the
model registry bridge just before streamText). These are provider metadata
only — message content is untouched, nothing is persisted, and inspect
does not render them; non-Anthropic requests pass through byte-identical.
### 6.7 Agent snapshot inheritance
Session turns whose resolved system prompt and tool set are byte-identical

View file

@ -31,7 +31,8 @@ import type { INotificationService } from "../application/notification/service.j
import { SystemClock, type IClock } from "../turns/clock.js";
import { FSTurnRepo } from "../turns/fs-repo.js";
import type { ITurnRepo } from "../turns/repo.js";
import { TurnRepoContextResolver, type IContextResolver } from "../turns/context-resolver.js";
import type { IContextResolver } from "../turns/context-resolver.js";
import { createContextResolver } from "../turns/context-elision.js";
import { EmitterTurnLifecycleBus, type ITurnLifecycleBus } from "../turns/bus.js";
import { RealUsageReporter } from "../turns/bridges/real-usage-reporter.js";
import type { IUsageReporter } from "../turns/usage-reporter.js";
@ -117,7 +118,9 @@ container.register({
turnsRootDir: asValue(path.join(WorkDir, "storage", "turns")),
sessionsRootDir: asValue(path.join(WorkDir, "storage", "sessions")),
turnRepo: asClass<ITurnRepo>(FSTurnRepo).singleton(),
contextResolver: asClass<IContextResolver>(TurnRepoContextResolver).singleton(),
contextResolver: asFunction<IContextResolver>(({ turnRepo }) =>
createContextResolver({ turnRepo }),
).singleton(),
lifecycleBus: asClass<ITurnLifecycleBus>(EmitterTurnLifecycleBus).singleton(),
usageReporter: asClass<IUsageReporter>(RealUsageReporter).singleton(),
agentResolver: asFunction<IAgentResolver>(() => new RealAgentResolver()).singleton(),

View file

@ -0,0 +1,100 @@
import { describe, expect, it } from "vitest";
import { applyPromptCaching, isAnthropicModel } from "./prompt-caching.js";
const BREAKPOINT = { anthropic: { cacheControl: { type: "ephemeral" } } };
function prompt(messageCount = 3) {
return {
system: "SYS",
messages: Array.from({ length: messageCount }, (_, i) => ({
role: i % 2 === 0 ? "user" : "assistant",
content: `m${i}`,
})),
};
}
describe("isAnthropicModel", () => {
it("matches the direct provider regardless of model id", () => {
expect(isAnthropicModel("anthropic", "whatever")).toBe(true);
});
it("matches aggregator ids by prefix or claude name", () => {
expect(isAnthropicModel("rowboat", "anthropic/claude-opus-4.8")).toBe(true);
expect(isAnthropicModel("openrouter", "anthropic/claude-3.5-sonnet")).toBe(true);
expect(isAnthropicModel("aigateway", "claude-sonnet-5")).toBe(true);
});
it("rejects everything else", () => {
expect(isAnthropicModel("openai", "gpt-test")).toBe(false);
expect(isAnthropicModel("rowboat", "google/gemini-3.5-flash")).toBe(false);
expect(isAnthropicModel("ollama", "llama3")).toBe(false);
expect(isAnthropicModel("openai-compatible", "qwen3")).toBe(false);
});
});
describe("applyPromptCaching", () => {
it("returns non-Anthropic prompts unchanged (same references)", () => {
const input = prompt();
const output = applyPromptCaching("openai", "gpt-test", input);
expect(output).toBe(input);
});
it("moves the system prompt into a breakpointed system message", () => {
const { system, messages } = applyPromptCaching("anthropic", "m", prompt());
expect(system).toBeUndefined();
expect(messages[0]).toEqual({
role: "system",
content: "SYS",
providerOptions: BREAKPOINT,
});
});
it("marks only the last message; middle messages stay untouched", () => {
const input = prompt(3);
const { messages } = applyPromptCaching("anthropic", "m", input);
// [system, m0, m1, m2]
expect(messages).toHaveLength(4);
expect(messages[1]).toBe(input.messages[0]);
expect(messages[2]).toBe(input.messages[1]);
expect(messages[3]).toEqual({
role: "user",
content: "m2",
providerOptions: BREAKPOINT,
});
});
it("does not mutate the input prompt", () => {
const input = prompt(2);
const snapshot = JSON.parse(JSON.stringify(input));
applyPromptCaching("anthropic", "m", input);
expect(input).toEqual(snapshot);
});
it("merges with pre-existing providerOptions on the last message", () => {
const input = {
system: "SYS",
messages: [
{
role: "user",
content: "hi",
providerOptions: { anthropic: { foo: 1 }, openai: { bar: 2 } },
},
],
};
const { messages } = applyPromptCaching("anthropic", "m", input);
expect(messages[1]).toEqual({
role: "user",
content: "hi",
providerOptions: {
openai: { bar: 2 },
anthropic: { foo: 1, cacheControl: { type: "ephemeral" } },
},
});
});
it("is deterministic (same input, same bytes)", () => {
const a = applyPromptCaching("anthropic", "m", prompt());
const b = applyPromptCaching("anthropic", "m", prompt());
expect(a).toEqual(b);
});
});

View file

@ -0,0 +1,92 @@
import type { JsonValue } from "@x/shared/dist/turns.js";
// Anthropic prompt caching is opt-in: without explicit cache_control
// breakpoints every call re-bills the full prefix at the base input rate
// (observed sessions: 0% cache hits on Claude vs ~82% implicit hits on
// Gemini, whose caching is automatic). Two breakpoints of the 4 allowed:
//
// 1. the system prompt — Anthropic's cache prefix is tools -> system ->
// messages, so this single breakpoint also covers the tool schemas;
// both are immutable for the life of a turn (agent snapshot),
// 2. the last message — the incremental-conversation pattern: each call
// writes a cache entry at the conversation tip, and the next call's
// lookup reuses the longest previously written prefix.
//
// Writes bill 1.25x on the newly cached segment and reads bill 0.1x, so a
// segment pays for itself on its first re-read; only tokens written and
// never read again (a session's final tail) cost extra, capped at 25%.
//
// This is a transport-layer decoration ONLY: nothing is persisted, so the
// durable log stays provider-agnostic and recomposition is unaffected.
// Non-Anthropic requests pass through byte-identical. Failure degrades
// soft: the AI SDK Anthropic provider validates placement and ignores
// (with a warning) misplaced or excess breakpoints, and the OpenRouter
// provider reads the same providerOptions.anthropic key.
const CACHE_CONTROL: Record<string, JsonValue> = {
cacheControl: { type: "ephemeral" },
};
// Anthropic models arrive three ways: the direct provider (flavor
// "anthropic", any model id), and aggregators (openrouter, aigateway, the
// rowboat gateway) that address them as "anthropic/<model>" or "claude-*".
export function isAnthropicModel(flavor: string, modelId: string): boolean {
if (flavor === "anthropic") {
return true;
}
const id = modelId.toLowerCase();
return id.startsWith("anthropic/") || id.includes("claude");
}
export interface PromptShape {
system?: string;
messages: JsonValue[];
}
export function applyPromptCaching(
flavor: string,
modelId: string,
prompt: { system: string; messages: JsonValue[] },
): PromptShape {
if (!isAnthropicModel(flavor, modelId)) {
return prompt;
}
// The system string moves into the message array so it can carry
// per-message providerOptions (the AI SDK's documented breakpoint
// mechanism); the provider reassembles it into the system field.
const systemMessage: JsonValue = {
role: "system",
content: prompt.system,
providerOptions: { anthropic: CACHE_CONTROL },
};
const messages = prompt.messages.map((message, index) => {
if (index !== prompt.messages.length - 1) {
return message;
}
const existing =
typeof message === "object" && message !== null && !Array.isArray(message)
? message
: {};
const existingOptions =
"providerOptions" in existing &&
typeof existing.providerOptions === "object" &&
existing.providerOptions !== null &&
!Array.isArray(existing.providerOptions)
? existing.providerOptions
: {};
return {
...existing,
providerOptions: {
...existingOptions,
anthropic: {
...(typeof existingOptions.anthropic === "object" &&
existingOptions.anthropic !== null &&
!Array.isArray(existingOptions.anthropic)
? existingOptions.anthropic
: {}),
...CACHE_CONTROL,
},
},
};
});
return { messages: [systemMessage, ...messages] };
}

View file

@ -188,6 +188,64 @@ describe("RealModelRegistry", () => {
});
});
it("applies cache breakpoints for Anthropic-family models", async () => {
const capture: InvokerOptions[] = [];
const fakeModel = { modelId: "claude-test" } as unknown as LanguageModel;
const registry = new RealModelRegistry({
resolveProvider: async () => ({ flavor: "anthropic" }),
createProviderImpl: (() => ({
languageModel: () => fakeModel,
})) as never,
invoke: (options) => {
capture.push(options);
return {
fullStream: (async function* () {
yield { type: "finish-step", finishReason: "stop", usage: {} };
})(),
};
},
});
const model = await registry.resolve({
provider: "anthropic",
model: "claude-test",
});
const drained: unknown[] = [];
for await (const event of model.stream(request())) {
drained.push(event);
}
expect(drained.length).toBeGreaterThan(0);
const [options] = capture;
// System prompt rides the message array with a breakpoint; no
// separate system string is sent.
expect(options.system).toBeUndefined();
expect(options.messages[0]).toMatchObject({
role: "system",
content: "SYS",
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
});
expect(options.messages[options.messages.length - 1]).toMatchObject({
role: "user",
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
});
});
it("leaves non-Anthropic requests byte-identical", async () => {
const capture: InvokerOptions[] = [];
const registry = makeRegistry(
[{ type: "finish-step", finishReason: "stop", usage: {} }],
capture,
);
const req = request();
const originalMessages = JSON.parse(JSON.stringify(req.messages));
await collect(registry, req);
const [options] = capture;
expect(options.system).toBe("SYS");
expect(options.messages).toEqual(originalMessages);
expect(JSON.stringify(options.messages)).not.toContain("cacheControl");
});
it("stops promptly when the signal aborts mid-stream", async () => {
const controller = new AbortController();
const registry = makeRegistry(

View file

@ -14,6 +14,7 @@ import type { JsonValue, ModelDescriptor, TurnUsage } from "@x/shared/dist/turns
import { convertFromMessages } from "../../agents/runtime.js";
import { resolveProviderConfig } from "../../models/defaults.js";
import { createProvider } from "../../models/models.js";
import { applyPromptCaching } from "../../models/prompt-caching.js";
import { applyLocalModelSettings } from "../../models/local.js";
import type {
IModelRegistry,
@ -26,7 +27,9 @@ import type {
// provider. The bridge always requests exactly one step.
export type StreamTextInvoker = (options: {
model: LanguageModel;
system: string;
// Absent when prompt caching moved the system prompt into messages
// (Anthropic-family models; see models/prompt-caching.ts).
system?: string;
messages: ModelMessage[];
tools: ToolSet;
abortSignal: AbortSignal;
@ -79,13 +82,16 @@ export class RealModelRegistry implements IModelRegistry {
// per-message, so composed requests are byte-stable.
encodeMessages: (messages) =>
convertFromMessages(messages) as unknown as JsonValue[],
stream: (request) => this.run(model, request),
stream: (request) =>
this.run(model, request, providerConfig.flavor, descriptor.model),
};
}
private async *run(
model: LanguageModel,
request: ModelStreamRequest,
flavor: string,
modelId: string,
): AsyncGenerator<LlmStreamEvent, void, void> {
const tools: ToolSet = {};
for (const descriptor of request.tools) {
@ -121,10 +127,17 @@ export class RealModelRegistry implements IModelRegistry {
let usage: z.infer<typeof TurnUsage> = {};
let providerMetadata: JsonValue | undefined;
// Anthropic-family models get cache_control breakpoints; everything
// else passes through byte-identical (transport-only, not persisted).
const prompt = applyPromptCaching(flavor, modelId, {
system: request.systemPrompt,
messages: request.messages,
});
const result = this.invoke({
model,
system: request.systemPrompt,
messages: request.messages as ModelMessage[],
...(prompt.system === undefined ? {} : { system: prompt.system }),
messages: prompt.messages as ModelMessage[],
tools,
abortSignal: request.signal,
...generationParams,

View file

@ -0,0 +1,590 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { z } from "zod";
import type { TurnContext, TurnEvent } from "@x/shared/dist/turns.js";
import {
DEFAULT_ELISION_POLICY,
ElidingContextResolver,
createContextResolver,
elideHistoricImages,
elideHistoricMiddlePaneContent,
elideHistoricToolResults,
loadElisionPolicy,
type ElisionPolicy,
} from "./context-elision.js";
import { TurnRepoContextResolver } from "./context-resolver.js";
import { InMemoryTurnRepo } from "./in-memory-turn-repo.js";
type TEvent = z.infer<typeof TurnEvent>;
function user(text: string) {
return { role: "user" as const, content: text };
}
function assistant(text: string) {
return { role: "assistant" as const, content: text };
}
function toolMsg(content: string, toolName = "file-readText") {
return {
role: "tool" as const,
content,
toolCallId: "tc1",
toolName,
};
}
// A completed turn containing one tool round trip whose result has the given
// output, followed by a final text response.
function toolTurnLog(
turnId: string,
context: z.infer<typeof TurnContext>,
toolOutput: string,
): TEvent[] {
const ts = "2026-07-02T10:00:00Z";
const echo = {
toolId: "tool.echo",
name: "echo",
description: "Echo",
inputSchema: {},
execution: "sync" as const,
requiresHuman: false,
};
const call = {
role: "assistant" as const,
content: [
{
type: "tool-call" as const,
toolCallId: "tc1",
toolName: "echo",
arguments: {},
},
],
};
return [
{
type: "turn_created",
schemaVersion: 1,
turnId,
ts,
sessionId: "sess-1",
agent: {
requested: { agentId: "copilot" },
resolved: {
agentId: "copilot",
systemPrompt: "SYS",
model: { provider: "fake", model: "m" },
tools: [echo],
},
},
context,
input: user("do it"),
config: {
autoPermission: false,
humanAvailable: true,
maxModelCalls: 20,
},
},
{
type: "model_call_requested",
turnId,
ts,
modelCallIndex: 0,
request: {
...(Array.isArray(context) ? {} : { contextRef: context }),
messages:
Array.isArray(context) && context.length > 0
? ["context", "input"]
: ["input"],
parameters: {},
},
},
{
type: "model_call_completed",
turnId,
ts,
modelCallIndex: 0,
message: call,
finishReason: "tool-calls",
usage: {},
},
{
type: "tool_invocation_requested",
turnId,
ts,
toolCallId: "tc1",
toolId: "tool.echo",
toolName: "echo",
execution: "sync",
input: {},
},
{
type: "tool_result",
turnId,
ts,
toolCallId: "tc1",
toolName: "echo",
source: "sync",
result: { output: toolOutput, isError: false },
},
{
type: "model_call_requested",
turnId,
ts,
modelCallIndex: 1,
request: {
messages: ["assistant:0", "toolResult:tc1"],
parameters: {},
},
},
{
type: "model_call_completed",
turnId,
ts,
modelCallIndex: 1,
message: assistant("done"),
finishReason: "stop",
usage: {},
},
{
type: "turn_completed",
turnId,
ts,
output: assistant("done"),
finishReason: "stop",
usage: {},
},
];
}
function frame(source: "camera" | "screen") {
return {
type: "image" as const,
data: "aGVsbG8=".repeat(50),
mediaType: "image/jpeg",
source,
capturedAt: "2026-07-02T10:00:00Z",
};
}
const T1 = "2026-07-02T10-00-00Z-0000001-000";
describe("elideHistoricToolResults", () => {
it("replaces tool results above the threshold with a placeholder", () => {
const big = "x".repeat(101);
const elided = elideHistoricToolResults([toolMsg(big)], 100);
expect(elided).toHaveLength(1);
expect(elided[0]).toMatchObject({
role: "tool",
toolCallId: "tc1",
toolName: "file-readText",
});
expect(elided[0].content).toContain("elided");
expect(elided[0].content).toContain("file-readText");
expect(elided[0].content).toContain("101");
});
it("keeps a head preview ahead of the placeholder", () => {
const content = `SKILL GUIDE${"y".repeat(5000)}`;
const [elided] = elideHistoricToolResults([toolMsg(content)], 2500);
// Starts with the first 400 chars verbatim, then the marker.
expect(String(elided.content).startsWith(content.slice(0, 400))).toBe(true);
expect(elided.content).toContain("Rest of tool result elided");
expect(String(elided.content).length).toBeLessThan(700);
// Preview never exceeds a tiny threshold.
const [tiny] = elideHistoricToolResults([toolMsg(content)], 50);
expect(String(tiny.content).startsWith(`${content.slice(0, 50)}\n[Rest`)).toBe(
true,
);
});
it("keeps tool results at or below the threshold verbatim", () => {
const exact = toolMsg("x".repeat(100));
expect(elideHistoricToolResults([exact], 100)).toEqual([exact]);
});
it("leaves non-tool messages untouched", () => {
const messages = [user("q".repeat(500)), assistant("a".repeat(500))];
expect(elideHistoricToolResults(messages, 100)).toEqual(messages);
});
it("is idempotent at realistic thresholds (placeholder is well under them)", () => {
const once = elideHistoricToolResults([toolMsg("x".repeat(5000))], 1000);
expect(once[0].content.length).toBeLessThan(1000);
expect(elideHistoricToolResults(once, 1000)).toEqual(once);
});
it("is deterministic for the same input", () => {
const messages = [toolMsg("x".repeat(5000)), user("q"), assistant("a")];
expect(elideHistoricToolResults(messages, 100)).toEqual(
elideHistoricToolResults(messages, 100),
);
});
});
describe("elideHistoricImages", () => {
it("replaces image parts with a labeled placeholder, keeping other parts", () => {
const message = {
role: "user" as const,
content: [
{ type: "text" as const, text: "how do I look?" },
frame("camera"),
frame("camera"),
frame("screen"),
],
};
const [elided] = elideHistoricImages([message]);
if (typeof elided.content === "string" || elided.role !== "user") {
throw new Error("expected user message with parts");
}
expect(elided.content.filter((p) => p.type === "image")).toHaveLength(0);
expect(elided.content[0]).toEqual({ type: "text", text: "how do I look?" });
const placeholder = elided.content[elided.content.length - 1];
if (placeholder.type !== "text") throw new Error("expected text placeholder");
expect(placeholder.text).toContain("2 webcam frames");
expect(placeholder.text).toContain("1 screen-share frame");
});
it("leaves string-content and image-free user messages untouched", () => {
const messages = [
user("plain"),
{
role: "user" as const,
content: [{ type: "text" as const, text: "no images" }],
},
assistant("a"),
];
expect(elideHistoricImages(messages)).toEqual(messages);
});
});
function noteMessage(content: string) {
return {
role: "user" as const,
content: "make this punchier",
userMessageContext: {
currentDateTime: "Thursday, July 2, 2026 at 10:00 AM GMT",
middlePane: {
kind: "note" as const,
path: "knowledge/Notes/Draft.md",
content,
},
},
};
}
describe("elideHistoricMiddlePaneContent", () => {
it("replaces large note snapshots, keeping kind and path", () => {
const [elided] = elideHistoricMiddlePaneContent([
noteMessage("n".repeat(600)),
]);
if (elided.role !== "user") throw new Error("expected user message");
const middlePane = elided.userMessageContext?.middlePane;
if (middlePane?.kind !== "note") throw new Error("expected note pane");
expect(middlePane.path).toBe("knowledge/Notes/Draft.md");
expect(middlePane.content).toContain("omitted from history");
expect(middlePane.content).toContain("600");
expect(elided.userMessageContext?.currentDateTime).toBeDefined();
expect(elided.content).toBe("make this punchier");
});
it("keeps small notes and non-note panes untouched", () => {
const small = noteMessage("short todo list");
const browser = {
role: "user" as const,
content: "what is this page?",
userMessageContext: {
middlePane: {
kind: "browser" as const,
url: "https://example.com",
title: "Example",
},
},
};
const plain = user("no context at all");
const messages = [small, browser, plain];
expect(elideHistoricMiddlePaneContent(messages)).toEqual(messages);
});
});
const POLICY_OFF = {
toolResults: false,
toolResultThresholdChars: 100,
images: false,
middlePaneContent: false,
};
describe("createContextResolver", () => {
// The factory is the app's one sanctioned constructor (DI container and
// inspect CLI). If this stops returning the eliding decorator, every
// construction site silently regresses to full historic replay.
it("wraps the repo resolver in the eliding decorator", () => {
const resolver = createContextResolver({
turnRepo: new InMemoryTurnRepo(),
});
expect(resolver).toBeInstanceOf(ElidingContextResolver);
});
it("applies the default policy through the decorated resolver", async () => {
const repo = new InMemoryTurnRepo();
repo.seed(toolTurnLog(T1, [], "x".repeat(5000)));
// Pinned to the shipped defaults (not the machine's context.json) so
// the test is hermetic while still exercising the full wiring.
const resolved = await createContextResolver({
turnRepo: repo,
loadPolicy: () => DEFAULT_ELISION_POLICY,
}).resolve({ previousTurnId: T1 });
const tool = resolved.find((m) => m.role === "tool");
expect(tool?.content).toContain("elided");
expect(tool?.content.length).toBeLessThan(1000);
});
});
describe("ElidingContextResolver", () => {
function resolver(repo: InMemoryTurnRepo, policy: ElisionPolicy) {
return new ElidingContextResolver({
inner: new TurnRepoContextResolver({ turnRepo: repo }),
loadPolicy: () => policy,
});
}
it("elides oversized tool results in the resolved prefix", async () => {
const repo = new InMemoryTurnRepo();
repo.seed(toolTurnLog(T1, [], "x".repeat(200)));
const resolved = await resolver(repo, {
...POLICY_OFF,
toolResults: true,
}).resolve({ previousTurnId: T1 });
const tool = resolved.find((m) => m.role === "tool");
expect(tool?.content).toContain("elided");
// The rest of the transcript is intact.
expect(resolved.map((m) => m.role)).toEqual([
"user",
"assistant",
"tool",
"assistant",
]);
});
it("returns the prefix unchanged when the policy is disabled", async () => {
const repo = new InMemoryTurnRepo();
const output = "x".repeat(200);
repo.seed(toolTurnLog(T1, [], output));
const resolved = await resolver(repo, POLICY_OFF).resolve({
previousTurnId: T1,
});
const tool = resolved.find((m) => m.role === "tool");
expect(tool?.content).toBe(output);
});
it("keeps small tool results verbatim when enabled", async () => {
const repo = new InMemoryTurnRepo();
repo.seed(toolTurnLog(T1, [], "small"));
const resolved = await resolver(repo, {
...POLICY_OFF,
toolResults: true,
}).resolve({ previousTurnId: T1 });
const tool = resolved.find((m) => m.role === "tool");
expect(tool?.content).toBe("small");
});
it("elides images in the resolved prefix when enabled", async () => {
const repo = new InMemoryTurnRepo();
const log = toolTurnLog(T1, [], "small").map((event) =>
event.type === "turn_created"
? {
...event,
input: {
role: "user" as const,
content: [
{ type: "text" as const, text: "watch me" },
frame("camera"),
],
},
}
: event,
);
repo.seed(log);
const resolved = await resolver(repo, {
...POLICY_OFF,
images: true,
}).resolve({ previousTurnId: T1 });
const input = resolved[0];
if (input.role !== "user" || typeof input.content === "string") {
throw new Error("expected user message with parts");
}
expect(input.content.some((p) => p.type === "image")).toBe(false);
expect(JSON.stringify(input.content)).toContain("1 webcam frame");
});
it("elides middle-pane note snapshots in the resolved prefix when enabled", async () => {
const repo = new InMemoryTurnRepo();
const log = toolTurnLog(T1, [], "small").map((event) =>
event.type === "turn_created"
? { ...event, input: noteMessage("n".repeat(600)) }
: event,
);
repo.seed(log);
const resolved = await resolver(repo, {
...POLICY_OFF,
middlePaneContent: true,
}).resolve({ previousTurnId: T1 });
const input = resolved[0];
if (input.role !== "user") throw new Error("expected user message");
const middlePane = input.userMessageContext?.middlePane;
if (middlePane?.kind !== "note") throw new Error("expected note pane");
expect(middlePane.content).toContain("omitted from history");
});
it("delegates resolveAgent to the inner resolver untouched", async () => {
const repo = new InMemoryTurnRepo();
const snapshot = {
agentId: "copilot",
systemPrompt: "SYS",
model: { provider: "fake", model: "m" },
tools: [],
};
const agent = await resolver(repo, POLICY_OFF).resolveAgent(snapshot);
expect(agent).toEqual(snapshot);
});
it("elides across every turn of a multi-turn reference chain", async () => {
const repo = new InMemoryTurnRepo();
const T2 = "2026-07-02T11-00-00Z-0000002-000";
repo.seed(toolTurnLog(T1, [], "a".repeat(200)));
repo.seed(toolTurnLog(T2, { previousTurnId: T1 }, "b".repeat(200)));
const resolved = await resolver(repo, {
...POLICY_OFF,
toolResults: true,
}).resolve({ previousTurnId: T2 });
const tools = resolved.filter((m) => m.role === "tool");
expect(tools).toHaveLength(2);
for (const tool of tools) {
expect(tool.content).toContain("elided");
}
});
it("reloads the policy on every resolve (hot config)", async () => {
const repo = new InMemoryTurnRepo();
const output = "x".repeat(200);
repo.seed(toolTurnLog(T1, [], output));
let loads = 0;
const eliding = new ElidingContextResolver({
inner: new TurnRepoContextResolver({ turnRepo: repo }),
loadPolicy: () => {
loads += 1;
return { ...POLICY_OFF, toolResults: loads > 1 };
},
});
const first = await eliding.resolve({ previousTurnId: T1 });
const second = await eliding.resolve({ previousTurnId: T1 });
expect(loads).toBe(2);
expect(first.find((m) => m.role === "tool")?.content).toBe(output);
expect(second.find((m) => m.role === "tool")?.content).toContain("elided");
});
});
describe("elision policy idempotency and determinism", () => {
// Placeholders must never themselves be elided on a second pass, and the
// transforms must be pure — both are what keeps replayed prefixes
// byte-stable for provider prefix caching.
it("elideHistoricImages is idempotent and deterministic", () => {
const messages = [
{
role: "user" as const,
content: [
{ type: "text" as const, text: "watch" },
frame("camera"),
frame("screen"),
],
},
];
const once = elideHistoricImages(messages);
expect(elideHistoricImages(once)).toEqual(once);
expect(elideHistoricImages(messages)).toEqual(once);
});
it("elideHistoricMiddlePaneContent is idempotent and deterministic", () => {
const messages = [noteMessage("n".repeat(600))];
const once = elideHistoricMiddlePaneContent(messages);
expect(elideHistoricMiddlePaneContent(once)).toEqual(once);
expect(elideHistoricMiddlePaneContent(messages)).toEqual(once);
});
it("keeps notes at the floor verbatim and elides just above it", () => {
const atFloor = noteMessage("n".repeat(500));
expect(elideHistoricMiddlePaneContent([atFloor])).toEqual([atFloor]);
const [above] = elideHistoricMiddlePaneContent([
noteMessage("n".repeat(501)),
]);
if (above.role !== "user") throw new Error("expected user message");
expect(above.userMessageContext?.middlePane?.kind === "note"
? above.userMessageContext.middlePane.content
: "").toContain("omitted from history");
});
});
describe("loadElisionPolicy", () => {
let dir: string;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), "elision-config-"));
});
afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
function write(content: string): string {
const p = path.join(dir, "context.json");
fs.writeFileSync(p, content);
return p;
}
it("returns defaults when the file is missing", () => {
expect(loadElisionPolicy(path.join(dir, "nope.json"))).toEqual(
DEFAULT_ELISION_POLICY,
);
});
it("applies a full config", () => {
const p = write(
JSON.stringify({
elideHistoricToolResults: false,
elideHistoricToolResultsThresholdChars: 42,
elideHistoricImages: false,
elideHistoricMiddlePaneContent: false,
}),
);
expect(loadElisionPolicy(p)).toEqual({
toolResults: false,
toolResultThresholdChars: 42,
images: false,
middlePaneContent: false,
});
});
it("merges a partial config with defaults", () => {
const p = write(JSON.stringify({ elideHistoricToolResultsThresholdChars: 42 }));
expect(loadElisionPolicy(p)).toEqual({
...DEFAULT_ELISION_POLICY,
toolResultThresholdChars: 42,
});
});
it("falls back to defaults on unparseable JSON", () => {
expect(loadElisionPolicy(write("{nope"))).toEqual(DEFAULT_ELISION_POLICY);
});
it("discards the whole file when any key is malformed (all-or-nothing)", () => {
const p = write(
JSON.stringify({
elideHistoricToolResults: false,
elideHistoricToolResultsThresholdChars: "not-a-number",
}),
);
expect(loadElisionPolicy(p)).toEqual(DEFAULT_ELISION_POLICY);
});
});

View file

@ -0,0 +1,246 @@
import fs from "fs";
import path from "path";
import { z } from "zod";
import type {
ConversationMessage,
ResolvedAgent,
ResolvedAgentSnapshot,
TurnContext,
} from "@x/shared/dist/turns.js";
import { WorkDir } from "../config/config.js";
import type { IContextResolver } from "./context-resolver.js";
import { TurnRepoContextResolver } from "./context-resolver.js";
import type { ITurnRepo } from "./repo.js";
// Transmit-time elision of historic context (the cross-turn prefix only —
// the current turn's own messages never pass through the resolver, so
// in-flight tool results and just-captured frames are always sent verbatim).
//
// Two policies, both on by default:
// - Tool results: large tool outputs from earlier turns (skill loads, file
// reads, HTTP fetches) dominate resent context; the model rarely needs
// them verbatim and can re-run the tool when it does.
// - Inline images: video-mode webcam and screen-share frames matter for
// the response they were captured for; afterwards the assistant's own
// text carries the takeaway, and fresh frames arrive with every new call
// message. Unlike tool results they cannot be re-fetched, so the
// placeholder only records what was there.
// - Middle-pane note content: every user message sent while a note is
// open carries a full snapshot of that note in userMessageContext. Only
// the newest snapshot matters (the system prompt already tells the model
// later middle-pane context overrides earlier), and the current file is
// always re-readable at the recorded path.
//
// Elision is a pure function of each message's content, so resolved prefixes
// stay byte-stable across calls and turns (provider prefix caches keep
// working), and the durable JSONL log is untouched — only the transmitted
// bytes change.
export interface ElisionPolicy {
toolResults: boolean;
toolResultThresholdChars: number;
images: boolean;
middlePaneContent: boolean;
}
// Threshold rationale: ~2,500 chars ≈ 600 tokens. Observed sessions show
// skill bodies at ~5k chars are the most common oversized result, so 10k
// would replay them forever; the head preview plus re-run hint makes the
// lower cut safe. Tunable via config/context.json.
export const DEFAULT_ELISION_POLICY: ElisionPolicy = {
toolResults: true,
toolResultThresholdChars: 2_500,
images: true,
middlePaneContent: true,
};
// Notes smaller than this stay verbatim in history: below the floor the
// placeholder saves nothing and a short note may carry useful context.
const MIDDLE_PANE_CONTENT_FLOOR_CHARS = 500;
// Head of an elided tool result kept verbatim ahead of the placeholder.
const TOOL_RESULT_PREVIEW_CHARS = 400;
const ContextConfig = z.object({
elideHistoricToolResults: z.boolean().optional(),
elideHistoricToolResultsThresholdChars: z.number().int().min(0).optional(),
elideHistoricImages: z.boolean().optional(),
elideHistoricMiddlePaneContent: z.boolean().optional(),
});
const CONTEXT_CONFIG_PATH = path.join(WorkDir, "config", "context.json");
// Read the elision policy from config/context.json, falling back to defaults
// for missing keys or an unreadable file (a single malformed key discards the
// whole file — all-or-nothing by design, so a typo can't half-apply). Read
// per resolve so a config edit applies to the next turn without a restart.
export function loadElisionPolicy(
configPath: string = CONTEXT_CONFIG_PATH,
): ElisionPolicy {
try {
if (!fs.existsSync(configPath)) {
return DEFAULT_ELISION_POLICY;
}
const raw = fs.readFileSync(configPath, "utf-8");
const parsed = ContextConfig.parse(JSON.parse(raw));
return {
toolResults:
parsed.elideHistoricToolResults ??
DEFAULT_ELISION_POLICY.toolResults,
toolResultThresholdChars:
parsed.elideHistoricToolResultsThresholdChars ??
DEFAULT_ELISION_POLICY.toolResultThresholdChars,
images:
parsed.elideHistoricImages ?? DEFAULT_ELISION_POLICY.images,
middlePaneContent:
parsed.elideHistoricMiddlePaneContent ??
DEFAULT_ELISION_POLICY.middlePaneContent,
};
} catch {
return DEFAULT_ELISION_POLICY;
}
}
export function elideHistoricToolResults(
messages: Array<z.infer<typeof ConversationMessage>>,
thresholdChars: number,
): Array<z.infer<typeof ConversationMessage>> {
return messages.map((message) => {
if (
message.role !== "tool" ||
message.content.length <= thresholdChars
) {
return message;
}
// Keep a head preview so the model knows what it is declining to
// re-fetch (a skill body reads very differently from a web page).
// Capped at the threshold so tiny thresholds still shrink content.
const preview = message.content.slice(
0,
Math.min(TOOL_RESULT_PREVIEW_CHARS, thresholdChars),
);
return {
...message,
content: `${preview}\n[Rest of tool result elided to save context: "${message.toolName}" returned ${message.content.length} characters in an earlier turn. Call the tool again if you need the full output now.]`,
};
});
}
export function elideHistoricImages(
messages: Array<z.infer<typeof ConversationMessage>>,
): Array<z.infer<typeof ConversationMessage>> {
return messages.map((message) => {
if (message.role !== "user" || typeof message.content === "string") {
return message;
}
const images = message.content.filter((part) => part.type === "image");
if (images.length === 0) {
return message;
}
const camera = images.filter((part) => part.source !== "screen").length;
const screen = images.length - camera;
const kinds = [
...(camera > 0 ? [`${camera} webcam frame${camera === 1 ? "" : "s"}`] : []),
...(screen > 0 ? [`${screen} screen-share frame${screen === 1 ? "" : "s"}`] : []),
].join(" and ");
return {
...message,
content: [
...message.content.filter((part) => part.type !== "image"),
{
type: "text" as const,
text: `[Omitted from history to save context: ${kinds} captured while this message was composed. The assistant saw them when responding to this message.]`,
},
],
};
});
}
export function elideHistoricMiddlePaneContent(
messages: Array<z.infer<typeof ConversationMessage>>,
): Array<z.infer<typeof ConversationMessage>> {
return messages.map((message) => {
if (message.role !== "user") {
return message;
}
const middlePane = message.userMessageContext?.middlePane;
if (
!middlePane ||
middlePane.kind !== "note" ||
middlePane.content.length <= MIDDLE_PANE_CONTENT_FLOOR_CHARS
) {
return message;
}
return {
...message,
userMessageContext: {
...message.userMessageContext,
middlePane: {
...middlePane,
content: `[Note content (${middlePane.content.length} characters) omitted from history to save context. This snapshot is stale; read the file at the path above if its content is needed now.]`,
},
},
};
});
}
// IContextResolver decorator: applies the elision policy to the materialized
// cross-turn prefix. Agent snapshot resolution is delegated untouched.
export class ElidingContextResolver implements IContextResolver {
private readonly inner: IContextResolver;
private readonly loadPolicy: () => ElisionPolicy;
constructor({
inner,
loadPolicy,
}: {
inner: IContextResolver;
loadPolicy?: () => ElisionPolicy;
}) {
this.inner = inner;
this.loadPolicy = loadPolicy ?? loadElisionPolicy;
}
async resolve(
context: z.infer<typeof TurnContext>,
): Promise<Array<z.infer<typeof ConversationMessage>>> {
let prefix = await this.inner.resolve(context);
const policy = this.loadPolicy();
if (policy.toolResults) {
prefix = elideHistoricToolResults(
prefix,
policy.toolResultThresholdChars,
);
}
if (policy.images) {
prefix = elideHistoricImages(prefix);
}
if (policy.middlePaneContent) {
prefix = elideHistoricMiddlePaneContent(prefix);
}
return prefix;
}
resolveAgent(
resolved: z.infer<typeof ResolvedAgentSnapshot>,
): Promise<z.infer<typeof ResolvedAgent>> {
return this.inner.resolveAgent(resolved);
}
}
// The one context resolver the app should construct (DI container and the
// inspect CLI both use this), so the debug view reproduces the same bytes
// the loop transmits.
export function createContextResolver({
turnRepo,
loadPolicy,
}: {
turnRepo: ITurnRepo;
// Injectable for tests; the app default reads config/context.json.
loadPolicy?: () => ElisionPolicy;
}): IContextResolver {
return new ElidingContextResolver({
inner: new TurnRepoContextResolver({ turnRepo }),
...(loadPolicy ? { loadPolicy } : {}),
});
}

View file

@ -28,6 +28,10 @@ export interface IContextResolver {
): Promise<z.infer<typeof ResolvedAgent>>;
}
// NOTE: app code must not construct this directly — use createContextResolver
// (context-elision.ts), which wraps it in the eliding decorator. A raw
// instance silently transmits full historic tool results, frames, and note
// snapshots. Direct construction is for tests of the raw resolution only.
export class TurnRepoContextResolver implements IContextResolver {
private readonly turnRepo: ITurnRepo;

View file

@ -8,6 +8,10 @@
// messages (user-message context, attachments, tool-result envelopes) are
// visible; the file itself stores only structural facts and references.
//
// Caveat: historic-context elision reads config/context.json at compose
// time, so recomposition is exact only while that config matches what was
// live when the call ran. The header prints the policy in effect NOW.
//
// Session mode prints the session overview (title, turns, statuses, sizes);
// pass --turns to cascade full turn inspection for every turn.
//
@ -27,7 +31,7 @@ import { convertFromMessages } from "../agents/runtime.js";
import { WorkDir } from "../config/config.js";
import { FSSessionRepo } from "../sessions/fs-repo.js";
import { composeModelRequest } from "./compose-model-request.js";
import { TurnRepoContextResolver } from "./context-resolver.js";
import { createContextResolver, loadElisionPolicy } from "./context-elision.js";
import { FSTurnRepo } from "./fs-repo.js";
const turnRepo = new FSTurnRepo({
@ -36,7 +40,7 @@ const turnRepo = new FSTurnRepo({
const sessionRepo = new FSSessionRepo({
sessionsRootDir: path.join(WorkDir, "storage", "sessions"),
});
const resolver = new TurnRepoContextResolver({ turnRepo });
const resolver = createContextResolver({ turnRepo });
const encode = (messages: Parameters<typeof convertFromMessages>[0]) =>
convertFromMessages(messages) as unknown as JsonValue[];
@ -81,6 +85,17 @@ async function inspectTurn(
console.log(
`agent ${agent.agentId} model ${agent.model.provider}/${agent.model.model} calls ${state.modelCalls.length}${inherited}`,
);
const policy = loadElisionPolicy();
console.log(
`context elision (per config/context.json NOW; transmitted bytes reflect the config live at call time): ` +
[
policy.toolResults
? `toolResults>${policy.toolResultThresholdChars}`
: "toolResults off",
policy.images ? "images" : "images off",
policy.middlePaneContent ? "notes" : "notes off",
].join(", "),
);
for (const call of state.modelCalls) {
if (onlyIndex !== undefined && call.index !== onlyIndex) continue;