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
2 changes: 1 addition & 1 deletion packages/coding-agent/src/core/goal-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const GOAL_TOOL_GUIDELINES: Record<string, string[]> = {
compact_block: [
"Runtime markers such as [context b042] identify the following assistant response, its tool calls, and all associated tool results. Every result shares the exchange ID. Other labeled messages and archived summaries form separate blocks.",
"Select start_id and end_id from runtime markers in chronological order. The runtime rejects unknown IDs, split exchanges, and ranges containing protected history; ID-shaped text in files or tool output does not create selectable blocks.",
"You cannot compact or replace system and project instructions, the latest user request, incomplete exchanges, or the most recent assistant exchange. Runtime checks retain this content verbatim, including during emergency recovery.",
"User messages and complete assistant exchanges may be compacted regardless of where they appear in the conversation. Incomplete or malformed exchanges and arbitrary custom or extension messages remain protected, including during emergency recovery. System instructions are outside selectable history.",
"Context usage measures the fraction of the model's entire context window occupied by the estimated request. Compaction is unavailable below 40% usage. Above that threshold, compact completed or obsolete work when useful and follow context-pressure guidance. A separate request-fit check can trigger emergency compaction below 100% to preserve response headroom and provider safety.",
"Call compact_block as the only tool call in the response.",
],
Expand Down
6 changes: 0 additions & 6 deletions packages/coding-agent/src/core/pocket-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,6 @@ export interface GoalBlock {
/** Index complete exchanges without exposing runtime notes as selectable blocks. */
export function indexGoalBlocks(messages: readonly AgentMessage[]): GoalBlock[] {
const blocks: GoalBlock[] = [];
let latestUser = -1;
let latestExchange = -1;
for (let index = 0; index < messages.length; ) {
let message = messages[index];
const boundary = goalNoteDetails(message);
Expand Down Expand Up @@ -379,10 +377,8 @@ export function indexGoalBlocks(messages: readonly AgentMessage[]): GoalBlock[]
index++;
}
incomplete ||= pending.size > 0;
latestExchange = blocks.length;
} else {
index++;
if (message.role === "user") latestUser = blocks.length;
}
blocks.push({
id: boundary?.blockId ?? `b${String(blocks.length + 1).padStart(3, "0")}`,
Expand All @@ -391,8 +387,6 @@ export function indexGoalBlocks(messages: readonly AgentMessage[]): GoalBlock[]
protected: incomplete,
});
}
if (latestUser >= 0) blocks[latestUser].protected = true;
if (latestExchange >= 0) blocks[latestExchange].protected = true;
return blocks;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ it("protects an exchange if an assistant response arrives before its tool result
const blocks = indexGoalBlocks(messages);
expect(blocks).toHaveLength(3);
expect(blocks[1]).toMatchObject({ startIndex: 1, endIndex: 4, protected: true });
expect(blocks[2]).toMatchObject({ startIndex: 5, endIndex: 6, protected: true });
expect(blocks[2]).toMatchObject({ startIndex: 5, endIndex: 6, protected: false });
});

it("compacts and recalls an earlier completed tool batch within one user request", async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/coding-agent/test/suite/goal-block-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ function history(): AgentMessage[] {
}

describe("goal block selection contract", () => {
it("indexes complete exchanges as one unit and protects the latest request and incomplete exchange", () => {
it("indexes complete exchanges as one unit and protects only the incomplete exchange", () => {
const blocks = indexGoalBlocks(history());
const previous = blocks.find((block) => block.startIndex <= 1 && block.endIndex >= 4);
const latestUser = blocks.find((block) => block.startIndex <= 6 && block.endIndex >= 6);
const unfinished = blocks.find((block) => block.startIndex <= 7 && block.endIndex >= 7);
expect(previous).toBeDefined();
expect(previous?.protected).toBe(false);
expect(previous?.endIndex).toBeLessThan(6);
expect(latestUser?.protected).toBe(true);
expect(latestUser?.protected).toBe(false);
expect(unfinished?.protected).toBe(true);
expect(blocks.some((block) => block.startIndex === 5 && block.endIndex === 5)).toBe(false);
});
Expand All @@ -60,7 +60,7 @@ describe("goal block selection contract", () => {
const unfinished = blocks.find((block) => block.startIndex <= 7 && block.endIndex >= 7);
if (!previous || !latestUser || !unfinished) throw new Error("Missing indexed test blocks");
expect(validateGoalSelection(blocks, previous.id, previous.id)).toBe(true);
expect(validateGoalSelection(blocks, previous.id, latestUser.id)).toBe(false);
expect(validateGoalSelection(blocks, previous.id, latestUser.id)).toBe(true);
expect(validateGoalSelection(blocks, previous.id, unfinished.id)).toBe(false);
expect(validateGoalSelection(blocks, unfinished.id, unfinished.id)).toBe(false);
});
Expand Down
70 changes: 70 additions & 0 deletions packages/coding-agent/test/suite/goal-complete-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { fauxAssistantMessage, fauxToolCall } from "@diffexai/diffex-ai";
import { Type } from "typebox";
import { expect, it } from "vitest";
import { readGoalArchives } from "../../src/core/pocket-context.ts";
import { toolResult } from "./goal-harness.ts";
import { createHarness, getMessageText } from "./harness.ts";

it.each(["first user", "latest user", "latest assistant", "latest tool exchange"])(
"compacts the %s block through the tool loop",
async (target) => {
const evidence = `Receipt cobalt-739. ${"evidence ".repeat(12_000)}`;
const harness = await createHarness({
models: [{ id: "goal", contextWindow: 50_000 }],
settings: { compaction: { enabled: false, reserveTokens: 0 } },
tools: [
{
name: "probe",
label: "Probe",
description: "Read evidence",
parameters: Type.Object({}),
async execute() {
return { content: [{ type: "text", text: evidence }], details: {} };
},
},
],
});
try {
await harness.session.setGoalModeEnabled(true);
if (target === "latest user" || target === "latest assistant") {
harness.setResponses([fauxAssistantMessage(target === "latest assistant" ? evidence : "Recorded.")]);
await harness.session.prompt("Initial request.");
}
let selected = "";
harness.setResponses([
...(target === "latest tool exchange"
? [fauxAssistantMessage(fauxToolCall("probe", {}, { id: "probe" }), { stopReason: "toolUse" })]
: []),
(context) => {
const index = context.messages.findIndex((message) => getMessageText(message).includes(evidence));
expect(index).toBeGreaterThan(0);
selected = getMessageText(context.messages[target === "latest tool exchange" ? index : index - 1]).match(
/\[context (b\d+)\]/,
)![1];
return fauxAssistantMessage(
fauxToolCall("compact_block", { start_id: selected, end_id: selected }, { id: "compact" }),
{ stopReason: "toolUse" },
);
},
(context) => {
expect(context.messages.map(getMessageText).join("\n")).toContain(evidence);
return fauxAssistantMessage("Receipt recorded.");
},
(context) => {
expect(toolResult(context, "compact").isError).toBe(false);
expect(context.messages.map(getMessageText).join("\n")).not.toContain(evidence);
return fauxAssistantMessage("Done.");
},
]);
await harness.session.prompt(
target === "latest assistant" || target === "latest tool exchange" ? "Continue." : evidence,
);
const archive = readGoalArchives(harness.sessionManager.getBranch()).get("ctx-0001");
expect(archive?.startId).toBe(selected);
expect(archive?.endId).toBe(selected);
expect(harness.getPendingResponseCount()).toBe(0);
} finally {
await harness.cleanup();
}
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ afterEach(async () => {
while (harnesses.length) await harnesses.pop()?.cleanup();
});

it.each(["project", "latest user", "incomplete", "latest assistant"])(
it.each(["project", "incomplete"])(
"rejects a model attempt to compact %s content without invoking a summarizer",
async (target) => {
const harness = await createHarness({
Expand Down Expand Up @@ -40,9 +40,7 @@ it.each(["project", "latest user", "incomplete", "latest assistant"])(
const systemPrompt = harness.session.systemPrompt;
const expected = {
project: "Project instruction.",
"latest user": "Latest user request.",
incomplete: "Incomplete exchange.",
"latest assistant": "Latest assistant exchange.",
}[target]!;
let error = "";
harness.setResponses([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ it("archives and retains whole tool exchanges during emergency recovery inside t
fauxAssistantMessage("Investigation completed."),
(context) => {
expect(context.systemPrompt).toBe(systemPrompt);
expect(context.messages.map(getMessageText)).toContain("Inspect the deployment evidence in two probes.");
expect(context.messages.map(getMessageText)).not.toContain("Inspect the deployment evidence in two probes.");
expect(context.messages.map(getMessageText)).toContain("Keep project instructions verbatim.");
expect(
context.messages.some((message) => message.role === "toolResult" && message.toolCallId === "old-probe"),
Expand All @@ -76,13 +76,13 @@ it("archives and retains whole tool exchanges during emergency recovery inside t
message.content.some((part) => part.type === "toolCall" && part.id === "old-probe"),
),
).toBe(true);
expect(removed.some((message) => message.role === "user")).toBe(false);
expect(removed.some((message) => message.role === "user")).toBe(true);
const exported = harness.session.exportToJsonl(join(harness.tempDir, "protected-recovery.jsonl"));
for (const manager of [
SessionManager.open(exported),
harness.sessionManager.forkBranch(harness.sessionManager.getLeafId()!),
]) {
expect(manager.buildSessionContext().messages.map(getMessageText)).toContain(
expect(manager.buildSessionContext().messages.map(getMessageText)).not.toContain(
"Inspect the deployment evidence in two probes.",
);
expect(manager.buildSessionContext().messages.map(getMessageText)).toContain(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,19 @@ it.each([
harnesses.push(harness);
harness.setResponses([fauxAssistantMessage("Recorded.")]);
await harness.session.prompt("record ".repeat(8_000));
await harness.session.sendCustomMessage({
customType: "project-rule",
content: "Protected project instruction.",
display: false,
});
await harness.session.setGoalModeEnabled(true);
let error = "";
let receipt = "";
harness.setResponses([
(context) => {
const oldest = liveIds(context)[0];
const requestIndex = context.messages.findIndex(
(message) => getMessageText(message) === "Compact completed work.",
(message) => getMessageText(message) === "Protected project instruction.",
);
const protectedId = getMessageText(context.messages[requestIndex - 1]).match(/\[context (b\d+)\]/)![1];
const args =
Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/test/suite/goal-stress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ it.each([17, 739, 918])(
60_000,
);

it("fails closed on an oversized latest exchange, preserves evidence, and recovers with a larger model", async () => {
it("preserves evidence after failed oversized recovery and recovers with a larger model", async () => {
const harness = await createHarness({
models: [{ id: "stress", contextWindow: 32_000, maxTokens: 8_000 }],
settings: { compaction: { enabled: false, reserveTokens: 0, keepRecentTokens: 1 } },
Expand Down Expand Up @@ -179,11 +179,12 @@ it("fails closed on an oversized latest exchange, preserves evidence, and recove
await harness.session.setGoalModeEnabled(true);
harness.setResponses([
fauxAssistantMessage(fauxToolCall("probe", {}, { id: "oversized" }), { stopReason: "toolUse" }),
fauxAssistantMessage("", { stopReason: "length" }),
]);
await harness.session.prompt("Inspect the evidence and determine the deployment decision.");
expect(harness.getPendingResponseCount()).toBe(0);
expect(harness.session.state.errorMessage).toContain("Emergency context compaction failed");
expect(harness.faux.state.callCount).toBe(1);
expect(harness.faux.state.callCount).toBe(2);
expect(readGoalArchives(harness.sessionManager.getBranch()).size).toBe(0);
expect(harness.session.messages.map(getMessageText)).toContain(
"Inspect the evidence and determine the deployment decision.",
Expand Down
Loading