Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 163 additions & 34 deletions open-sse/services/compression/engines/rtk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,40 @@ import { normalizeCodeLanguage, stripCode } from "./codeStripper.ts";
import { maybePersistRtkRawOutput, type RtkRawOutputPointer } from "./rawOutput.ts";
import { isTextBlock } from "../../messageContent.ts";
import { adaptBodyForCompression } from "../../bodyAdapter.ts";
import { isAnthropicToolResultBlock } from "../../toolResultCompressor.ts";

type Message = {
role: string;
content?: string | Array<{ type?: string; text?: string; [key: string]: unknown }>;
[key: string]: unknown;
};

type ToolMeta = { toolName: string; command: string | null };

// Same terminal-tool pattern as grok-web.ts isTerminalTool(): RTK's command-aware filters
// only apply to bash/shell tool results. Non-shell tools (read, glob, grep, edit, write…)
// skip filter matching to avoid content-based false positives (e.g. a .ts file matching
// the build-typescript filter).
const SHELL_TOOL_NAME_RE = /\b(bash|shell|terminal|run_command|execute_command|exec|command)\b/;

/**
* Resolve the shell command + whether to skip RTK filters for a tool result, given the
* tool id (OpenAI `tool_call_id` / Anthropic `tool_use_id`) and the lookup built from the
* preceding assistant tool calls. A missing entry runs filters with text-based command
* detection; a non-shell tool name skips filters.
*/
function resolveToolMeta(
toolId: string | null,
lookup: Map<string, ToolMeta>
): { command: string | null; skipFilters: boolean } {
const meta = toolId ? lookup.get(toolId) : null;
if (!meta) return { command: null, skipFilters: false };
if (SHELL_TOOL_NAME_RE.test(meta.toolName.toLowerCase())) {
return { command: meta.command, skipFilters: false };
}
return { command: null, skipFilters: true };
}

const RTK_SCHEMA: EngineConfigField[] = [
{
key: "intensity",
Expand Down Expand Up @@ -188,6 +215,14 @@ function mergeRtkConfig(base?: Partial<RtkConfig>, override?: Record<string, unk
}

function shouldCompressMessage(message: Message, config: RtkConfig): boolean {
// Anthropic-shape tool results live in (typically role:"user") messages as `tool_result`
// content blocks — treat them like OpenAI tool messages (gated by applyToToolResults).
if (
config.applyToToolResults &&
Array.isArray(message.content) &&
message.content.some(isAnthropicToolResultBlock)
)
return true;
if (message.role === "tool")
return config.applyToToolResults || (config.applyToCodeBlocks && hasCodeFence(message.content));
if (message.role === "assistant")
Expand Down Expand Up @@ -388,6 +423,78 @@ export function processRtkText(
};
}

/**
* Compress the text inside Anthropic-shape `tool_result` content blocks (string or nested
* text-block content), resolving the shell command per block from its `tool_use_id`. The
* block type and `tool_use_id` are preserved exactly — only the inner text is rewritten.
*/
function processToolResultBlocks(
content: Message["content"],
config: RtkConfig,
toolCallLookup: Map<string, ToolMeta>
): {
content: Message["content"];
compressed: boolean;
techniquesUsed: string[];
rulesApplied: string[];
rawOutputPointers: RtkRawOutputPointer[];
} {
const techniquesUsed: string[] = [];
const rulesApplied: string[] = [];
const rawOutputPointers: RtkRawOutputPointer[] = [];

if (!Array.isArray(content)) {
return { content, compressed: false, techniquesUsed, rulesApplied, rawOutputPointers };
}

const collect = (processed: RtkProcessResult) => {
techniquesUsed.push(...processed.techniquesUsed);
rulesApplied.push(...processed.rulesApplied);
if (processed.rawOutputPointers) rawOutputPointers.push(...processed.rawOutputPointers);
};

let compressed = false;
const nextContent = content.map((part) => {
if (!isAnthropicToolResultBlock(part)) return part;
const toolUseId = typeof part.tool_use_id === "string" ? part.tool_use_id : null;
const { command, skipFilters } = resolveToolMeta(toolUseId, toolCallLookup);
const inner = part.content;

if (typeof inner === "string") {
if (!inner) return part;
const processed = processRtkText(inner, { config, command, skipFilters });
collect(processed);
if (!processed.compressed) return part;
compressed = true;
return { ...part, content: processed.text };
}

if (Array.isArray(inner)) {
let blockChanged = false;
const nextInner = inner.map((sub) => {
if (!isTextBlock(sub) || !sub.text) return sub;
const processed = processRtkText(sub.text, { config, command, skipFilters });
collect(processed);
if (!processed.compressed) return sub;
blockChanged = true;
compressed = true;
return { ...sub, text: processed.text };
});
return blockChanged ? { ...part, content: nextInner } : part;
}

return part;
});

return {
content: compressed ? nextContent : content,
compressed,
techniquesUsed,
rulesApplied,
rawOutputPointers,
};
}

function processRtkContent(
content: Message["content"],
config: RtkConfig,
Expand Down Expand Up @@ -497,57 +604,79 @@ export function applyRtkCompression(
// Build tool_call_id → tool metadata lookup from assistant messages.
// This lets us distinguish bash tool results (which RTK filters are designed for)
// from non-shell tool results (read, grep, glob, etc.) that should skip filters.
const toolCallLookup = new Map<string, { toolName: string; command: string | null }>();
const toolCallLookup = new Map<string, ToolMeta>();
for (const msg of messages) {
if (msg.role !== "assistant") continue;
// OpenAI-shape: assistant.tool_calls[].function.{name, arguments(JSON).command}
const toolCalls = msg.tool_calls;
if (!Array.isArray(toolCalls)) continue;
for (const tc of toolCalls as Array<Record<string, unknown>>) {
if (!tc || typeof tc !== "object") continue;
const id = typeof tc.id === "string" ? tc.id : null;
if (!id) continue;
const fn = tc.function as Record<string, unknown> | undefined;
if (!fn || typeof fn !== "object") continue;
const toolName = typeof fn.name === "string" ? fn.name : "";
let command: string | null = null;
if (typeof fn.arguments === "string") {
try {
const args = JSON.parse(fn.arguments);
if (Array.isArray(toolCalls)) {
for (const tc of toolCalls as Array<Record<string, unknown>>) {
if (!tc || typeof tc !== "object") continue;
const id = typeof tc.id === "string" ? tc.id : null;
if (!id) continue;
const fn = tc.function as Record<string, unknown> | undefined;
if (!fn || typeof fn !== "object") continue;
const toolName = typeof fn.name === "string" ? fn.name : "";
let command: string | null = null;
if (typeof fn.arguments === "string") {
try {
const args = JSON.parse(fn.arguments);
command =
typeof args.command === "string"
? args.command
: typeof args.cmd === "string"
? args.cmd
: null;
} catch {
// non-JSON arguments
}
}
toolCallLookup.set(id, { toolName, command });
}
}
// Anthropic-shape: assistant.content[] holds { type:"tool_use", id, name, input.command }.
if (Array.isArray(msg.content)) {
for (const part of msg.content as Array<Record<string, unknown>>) {
if (!part || typeof part !== "object" || part.type !== "tool_use") continue;
const id = typeof part.id === "string" ? part.id : null;
if (!id) continue;
const toolName = typeof part.name === "string" ? part.name : "";
const input = part.input as Record<string, unknown> | undefined;
let command: string | null = null;
if (input && typeof input === "object") {
command =
typeof args.command === "string"
? args.command
: typeof args.cmd === "string"
? args.cmd
typeof input.command === "string"
? input.command
: typeof input.cmd === "string"
? input.cmd
: null;
} catch {
// non-JSON arguments
}
toolCallLookup.set(id, { toolName, command });
}
toolCallLookup.set(id, { toolName, command });
}
}

const compressedMessages = messages.map((message) => {
if (!shouldCompressMessage(message, config)) return message;

// Resolve tool metadata from the preceding assistant tool_calls entry.
// Anthropic-shape tool results: `tool_result` content blocks inside a (typically
// role:"user") message. Compress each block's inner text, resolving the shell command
// per block from the matching assistant `tool_use` (mirrors the OpenAI tool path).
if (Array.isArray(message.content) && message.content.some(isAnthropicToolResultBlock)) {
const processed = processToolResultBlocks(message.content, config, toolCallLookup);
allTechniques.push(...processed.techniquesUsed);
allRules.push(...processed.rulesApplied);
rawOutputPointers.push(...processed.rawOutputPointers);
if (!processed.compressed) return message;
return { ...message, content: processed.content };
}

// OpenAI-shape tool message: resolve metadata from the preceding assistant tool_calls.
let command: string | null = null;
let skipFilters = false;
if (message.role === "tool") {
const callId = typeof message.tool_call_id === "string" ? message.tool_call_id : null;
const meta = callId ? toolCallLookup.get(callId) : null;
if (meta) {
const name = meta.toolName.toLowerCase();
// Match the same terminal tool pattern used in grok-web.ts isTerminalTool().
// Only apply RTK filters to bash/shell tool results.
// Non-shell tools (read, glob, grep, edit, write, etc.) skip filter matching
// to prevent content-based false positives (e.g. a TS file matching build-typescript).
if (/\b(bash|shell|terminal|run_command|execute_command|exec|command)\b/.test(name)) {
command = meta.command;
} else {
skipFilters = true;
}
}
({ command, skipFilters } = resolveToolMeta(callId, toolCallLookup));
}

const processed = processRtkContent(message.content, config, { command, skipFilters });
Expand Down
112 changes: 112 additions & 0 deletions tests/unit/compression/rtk-anthropic-tool-result.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* TDD regression (escalated cmqmf7d29): a user reported "compression mode = only RTK
* is not working — I ran `git status` via my agent and the RTK stats stayed the same".
*
* Root cause: RTK's `applyRtkCompression` only compressed OpenAI-shape tool results
* (`role:"tool"`) and assistant messages. Anthropic-shape tool results — which arrive as
* `tool_result` content blocks inside a (typically `role:"user"`) message — were skipped
* entirely, so coding agents speaking the Anthropic Messages format saw zero RTK savings.
* caveman/aggressive already handle this shape (B-AGG-ANTHROPIC-TR via
* `compressAnthropicToolResultBlock`); RTK was the only engine that didn't.
*
* These tests pin that RTK now compresses the text inside Anthropic `tool_result` blocks
* (preserving `tool_use_id` + block structure), resolving the shell command from the
* matching assistant `tool_use` block exactly like the OpenAI `tool_call_id` path.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";

import { applyRtkCompression } from "../../../open-sse/services/compression/index.ts";

// A verbose `git status` with the noise (blank lines + "(use ...)" hints) the git-status
// filter is designed to drop, so compression is unambiguous.
const GIT_STATUS_OUTPUT = [
"On branch main",
"Your branch is up to date with 'origin/main'.",
"",
"Changes not staged for commit:",
' (use "git add <file>..." to update what will be committed)',
' (use "git restore <file>..." to discard changes in working directory)',
"\tmodified: src/app.ts",
"\tmodified: src/index.ts",
"",
"Untracked files:",
' (use "git add <file>..." to include in what will be committed)',
"\tsrc/new.ts",
"",
'no changes added to commit (use "git add" and/or "git commit -a")',
].join("\n");

function anthropicBody(toolResultContent: unknown) {
return {
messages: [
{ role: "user", content: "run git status" },
{
role: "assistant",
content: [
{ type: "text", text: "Running it now." },
{
type: "tool_use",
id: "toolu_01abc",
name: "bash",
input: { command: "git status" },
},
],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_01abc",
content: toolResultContent,
},
],
},
],
};
}

describe("RTK — Anthropic-shape tool_result blocks", () => {
it("compresses a string-content tool_result inside a user message", () => {
const result = applyRtkCompression(anthropicBody(GIT_STATUS_OUTPUT));

assert.equal(result.compressed, true);
assert.equal(result.stats?.mode, "rtk");

const messages = result.body.messages as Array<{ role: string; content: unknown }>;
const block = (messages[2].content as Array<Record<string, unknown>>)[0];
// Block structure + linkage preserved exactly.
assert.equal(block.type, "tool_result");
assert.equal(block.tool_use_id, "toolu_01abc");
// Inner text was compressed: the "(use ...)" hint lines are dropped, the branch and
// changed-file lines survive.
const text = block.content as string;
assert.ok(text.length < GIT_STATUS_OUTPUT.length, "tool_result text should shrink");
assert.doesNotMatch(text, /\(use "git add/, "noise hint lines should be dropped");
assert.match(text, /On branch main/, "branch line preserved");
assert.match(text, /modified:\s+src\/app\.ts/, "changed-file lines preserved");
});

it("compresses an array-content tool_result (nested text blocks)", () => {
const result = applyRtkCompression(
anthropicBody([{ type: "text", text: GIT_STATUS_OUTPUT }])
);

assert.equal(result.compressed, true);
const messages = result.body.messages as Array<{ role: string; content: unknown }>;
const block = (messages[2].content as Array<Record<string, unknown>>)[0];
assert.equal(block.type, "tool_result");
assert.equal(block.tool_use_id, "toolu_01abc");
const inner = block.content as Array<{ type: string; text: string }>;
assert.equal(inner[0].type, "text");
assert.doesNotMatch(inner[0].text, /\(use "git add/);
assert.match(inner[0].text, /On branch main/);
});

it("leaves non-tool_result user content untouched", () => {
const body = { messages: [{ role: "user", content: "just a plain question" }] };
const result = applyRtkCompression(body);
assert.equal(result.compressed, false);
});
});
Loading