feat(coding-agent): exclude custom messages from context

This commit is contained in:
Armin Ronacher
2026-06-12 23:53:12 +02:00
Unverified
parent 17721d5e01
commit 0264730fbc
17 changed files with 392 additions and 27 deletions
+7 -1
View File
@@ -495,7 +495,13 @@ export class AgentHarness<
} else if (write.type === "custom") {
await this.session.appendCustomEntry(write.customType, write.data);
} else if (write.type === "custom_message") {
await this.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details);
await this.session.appendCustomMessageEntry(
write.customType,
write.content,
write.display,
write.details,
write.excludeFromContext,
);
} else if (write.type === "label") {
await this.session.appendLabel(write.targetId, write.label);
} else if (write.type === "session_info") {
@@ -103,7 +103,14 @@ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined
return entry.message;
case "custom_message":
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
return createCustomMessage(
entry.customType,
entry.content,
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
);
case "branch_summary":
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
@@ -143,6 +150,7 @@ export function prepareBranchEntries(entries: SessionTreeEntry[], tokenBudget: n
const entry = entries[i];
const message = getMessageFromEntry(entry);
if (!message) continue;
if (message.role === "custom" && message.excludeFromContext) continue;
extractFileOpsFromMessage(message, fileOps);
const tokens = estimateTokens(message);
@@ -68,6 +68,7 @@ function getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
);
}
if (entry.type === "branch_summary") {
@@ -83,7 +84,11 @@ function getMessageFromEntryForCompaction(entry: SessionTreeEntry): AgentMessage
if (entry.type === "compaction") {
return undefined;
}
return getMessageFromEntry(entry);
const message = getMessageFromEntry(entry);
if (message?.role === "custom" && message.excludeFromContext) {
return undefined;
}
return message;
}
/** Generated compaction data ready to be persisted as a compaction entry. */
@@ -240,7 +245,13 @@ export function estimateTokens(message: AgentMessage): number {
}
return Math.ceil(chars / 4);
}
case "custom":
case "custom": {
if (message.excludeFromContext) {
return 0;
}
chars = estimateTextAndImageContentChars(message.content);
return Math.ceil(chars / 4);
}
case "toolResult": {
chars = estimateTextAndImageContentChars(message.content);
return Math.ceil(chars / 4);
+6
View File
@@ -34,6 +34,7 @@ export interface CustomMessage<T = unknown> {
content: string | (TextContent | ImageContent)[];
display: boolean;
details?: T;
excludeFromContext?: boolean;
timestamp: number;
}
@@ -106,6 +107,7 @@ export function createCustomMessage(
display: boolean,
details: unknown | undefined,
timestamp: string,
excludeFromContext?: boolean,
): CustomMessage {
return {
role: "custom",
@@ -113,6 +115,7 @@ export function createCustomMessage(
content,
display,
details,
excludeFromContext,
timestamp: new Date(timestamp).getTime(),
};
}
@@ -131,6 +134,9 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
timestamp: m.timestamp,
};
case "custom": {
if (m.excludeFromContext) {
return undefined;
}
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return {
role: "user",
@@ -51,6 +51,7 @@ export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionCon
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
),
);
} else if (entry.type === "branch_summary" && entry.summary) {
@@ -206,6 +207,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
content: string | (TextContent | ImageContent)[],
display: boolean,
details?: T,
excludeFromContext?: boolean,
): Promise<string> {
return this.appendTypedEntry({
type: "custom_message",
@@ -216,6 +218,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
content,
display,
details,
excludeFromContext,
} satisfies CustomMessageEntry<T>);
}
+1
View File
@@ -388,6 +388,7 @@ export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
content: string | (TextContent | ImageContent)[];
details?: T;
display: boolean;
excludeFromContext?: boolean;
}
export interface LabelEntry extends SessionTreeEntryBase {
@@ -8,6 +8,7 @@ import {
type Usage,
} from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { prepareBranchEntries } from "../../src/harness/compaction/branch-summarization.ts";
import {
type CompactionPreparation,
calculateContextTokens,
@@ -289,9 +290,12 @@ describe("harness compaction", () => {
timestamp: Date.now(),
};
const excludedCustom: AgentMessage = { ...customString, content: "x".repeat(1000), excludeFromContext: true };
expect(estimateTokens({ role: "user", content: "plain user", timestamp: Date.now() })).toBeGreaterThan(0);
expect(estimateTokens(assistantWithThinkingAndTool)).toBeGreaterThan(0);
expect(estimateTokens(customString)).toBeGreaterThan(0);
expect(estimateTokens(excludedCustom)).toBe(0);
expect(estimateTokens(toolResultWithImage)).toBeGreaterThan(1000);
expect(estimateTokens(bashExecution)).toBeGreaterThan(0);
expect(estimateTokens(branchSummaryMessage)).toBeGreaterThan(0);
@@ -311,6 +315,11 @@ describe("harness compaction", () => {
usageTokens: 20,
lastUsageIndex: 0,
});
expect(estimateContextTokens([assistant, excludedCustom])).toMatchObject({
tokens: 20,
usageTokens: 20,
trailingTokens: 0,
});
});
it("builds session context with a compaction entry", () => {
@@ -380,6 +389,46 @@ describe("harness compaction", () => {
expect([...preparation!.fileOps.written]).toContain("written.ts");
});
it("skips excluded custom messages during compaction token estimates", () => {
const customMessage: CustomMessageEntry = {
type: "custom_message",
id: createId(),
parentId: null,
timestamp: new Date().toISOString(),
customType: "status",
content: "x".repeat(1000),
display: true,
excludeFromContext: true,
};
const user = createMessageEntry(createUserMessage("keep"), customMessage.id);
const preparation = getOrThrow(
prepareCompaction([customMessage, user], { enabled: true, reserveTokens: 0, keepRecentTokens: 1 }),
);
expect(preparation?.tokensBefore).toBe(1);
expect(preparation?.messagesToSummarize).toEqual([]);
});
it("skips excluded custom messages before branch summary token budgeting", () => {
const user = createMessageEntry(createUserMessage("keep"));
const customMessage: CustomMessageEntry = {
type: "custom_message",
id: createId(),
parentId: user.id,
timestamp: new Date().toISOString(),
customType: "status",
content: "x".repeat(1000),
display: true,
excludeFromContext: true,
};
const preparation = prepareBranchEntries([user, customMessage], 10);
expect(preparation.messages.map((message) => message.role)).toEqual(["user"]);
expect(preparation.totalTokens).toBe(1);
});
it("prepares custom and branch summary entries for summarization", () => {
const branchSummary: BranchSummaryEntry = {
type: "branch_summary",
+23 -12
View File
@@ -512,6 +512,7 @@ export class AgentSession {
event.message.content,
event.message.display,
event.message.details,
event.message.excludeFromContext,
);
} else if (
event.message.role === "user" ||
@@ -1112,6 +1113,7 @@ export class AgentSession {
content: msg.content,
display: msg.display,
details: msg.details,
excludeFromContext: msg.excludeFromContext,
timestamp: Date.now(),
});
}
@@ -1289,7 +1291,8 @@ export class AgentSession {
/**
* Send a custom message to the session. Creates a CustomMessageEntry.
*
* Handles three cases:
* Handles four cases:
* - Excluded from context: appends to state/session, no turn
* - Streaming: queues message, processed when loop pulls from queue
* - Not streaming + triggerTurn: appends to state/session, starts new turn
* - Not streaming + no trigger: appends to state/session, no turn
@@ -1299,7 +1302,7 @@ export class AgentSession {
* @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn"
*/
async sendCustomMessage<T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "excludeFromContext">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): Promise<void> {
const appMessage = {
@@ -1308,9 +1311,12 @@ export class AgentSession {
content: message.content,
display: message.display,
details: message.details,
excludeFromContext: message.excludeFromContext,
timestamp: Date.now(),
} satisfies CustomMessage<T>;
if (options?.deliverAs === "nextTurn") {
if (appMessage.excludeFromContext) {
this._recordCustomMessage(appMessage);
} else if (options?.deliverAs === "nextTurn") {
this._pendingNextTurnMessages.push(appMessage);
} else if (this.isStreaming) {
if (options?.deliverAs === "followUp") {
@@ -1321,18 +1327,23 @@ export class AgentSession {
} else if (options?.triggerTurn) {
await this._runAgentPrompt(appMessage);
} else {
this.agent.state.messages.push(appMessage);
this.sessionManager.appendCustomMessageEntry(
message.customType,
message.content,
message.display,
message.details,
);
this._emit({ type: "message_start", message: appMessage });
this._emit({ type: "message_end", message: appMessage });
this._recordCustomMessage(appMessage);
}
}
private _recordCustomMessage(message: CustomMessage): void {
this.agent.state.messages.push(message);
this.sessionManager.appendCustomMessageEntry(
message.customType,
message.content,
message.display,
message.details,
message.excludeFromContext,
);
this._emit({ type: "message_start", message });
this._emit({ type: "message_end", message });
}
/**
* Send a user message to the agent. Always triggers a turn.
* When the agent is streaming, use deliverAs to specify how to queue the message.
@@ -153,7 +153,14 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
return entry.message;
case "custom_message":
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
return createCustomMessage(
entry.customType,
entry.content,
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
);
case "branch_summary":
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
@@ -212,6 +219,7 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
const entry = entries[i];
const message = getMessageFromEntry(entry);
if (!message) continue;
if (message.role === "custom" && message.excludeFromContext) continue;
// Extract file ops from assistant messages (tool calls)
extractFileOpsFromMessage(message, fileOps);
@@ -81,7 +81,14 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
return entry.message;
}
if (entry.type === "custom_message") {
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
return createCustomMessage(
entry.customType,
entry.content,
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
);
}
if (entry.type === "branch_summary") {
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
@@ -96,7 +103,11 @@ function getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | u
if (entry.type === "compaction") {
return undefined;
}
return getMessageFromEntry(entry);
const message = getMessageFromEntry(entry);
if (message?.role === "custom" && message.excludeFromContext) {
return undefined;
}
return message;
}
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
@@ -270,7 +281,13 @@ export function estimateTokens(message: AgentMessage): number {
}
return Math.ceil(chars / 4);
}
case "custom":
case "custom": {
if (message.excludeFromContext) {
return 0;
}
chars = estimateTextAndImageContentChars(message.content);
return Math.ceil(chars / 4);
}
case "toolResult": {
chars = estimateTextAndImageContentChars(message.content);
return Math.ceil(chars / 4);
@@ -379,7 +379,7 @@ export interface ExtensionCommandContext extends ExtensionContext {
*/
export interface ReplacedSessionContext extends ExtensionCommandContext {
sendMessage<T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "excludeFromContext">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): Promise<void>;
@@ -1043,7 +1043,7 @@ export interface MessageEndEventResult {
}
export interface BeforeAgentStartEventResult {
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details" | "excludeFromContext">;
/** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
systemPrompt?: string;
}
@@ -1213,7 +1213,7 @@ export interface ExtensionAPI {
/** Send a custom message to the session. */
sendMessage<T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "excludeFromContext">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): void;
@@ -1442,7 +1442,7 @@ export interface ExtensionShortcut {
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
export type SendMessageHandler = <T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "excludeFromContext">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
) => void;
@@ -49,6 +49,8 @@ export interface CustomMessage<T = unknown> {
content: string | (TextContent | ImageContent)[];
display: boolean;
details?: T;
/** If true, this message is excluded from LLM context */
excludeFromContext?: boolean;
timestamp: number;
}
@@ -126,6 +128,7 @@ export function createCustomMessage(
display: boolean,
details: unknown | undefined,
timestamp: string,
excludeFromContext?: boolean,
): CustomMessage {
return {
role: "custom",
@@ -133,6 +136,7 @@ export function createCustomMessage(
content,
display,
details,
excludeFromContext,
timestamp: new Date(timestamp).getTime(),
};
}
@@ -160,6 +164,9 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
timestamp: m.timestamp,
};
case "custom": {
if (m.excludeFromContext) {
return undefined;
}
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return {
role: "user",
@@ -120,7 +120,7 @@ export interface SessionInfoEntry extends SessionEntryBase {
* Custom message entry for extensions to inject messages into LLM context.
* Use customType to identify your extension's entries.
*
* Unlike CustomEntry, this DOES participate in LLM context.
* Unlike CustomEntry, this DOES participate in LLM context unless excludeFromContext is true.
* The content is converted to a user message in buildSessionContext().
* Use details for extension-specific metadata (not sent to LLM).
*
@@ -134,6 +134,7 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
content: string | (TextContent | ImageContent)[];
details?: T;
display: boolean;
excludeFromContext?: boolean;
}
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
@@ -390,7 +391,14 @@ export function buildSessionContext(
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
createCustomMessage(
entry.customType,
entry.content,
entry.display,
entry.details,
entry.timestamp,
entry.excludeFromContext,
),
);
} else if (entry.type === "branch_summary" && entry.summary) {
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
@@ -1063,6 +1071,7 @@ export class SessionManager {
content: string | (TextContent | ImageContent)[],
display: boolean,
details?: T,
excludeFromContext?: boolean,
): string {
const entry: CustomMessageEntry<T> = {
type: "custom_message",
@@ -1070,6 +1079,7 @@ export class SessionManager {
content,
display,
details,
excludeFromContext,
id: generateId(this.byId),
parentId: this.leafId,
timestamp: new Date().toISOString(),
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { prepareBranchEntries } from "../src/core/compaction/branch-summarization.ts";
import type { CustomMessageEntry, SessionMessageEntry } from "../src/core/session-manager.ts";
function userEntry(id: string, parentId: string | null, text: string): SessionMessageEntry {
return {
type: "message",
id,
parentId,
timestamp: "2025-01-01T00:00:00Z",
message: {
role: "user",
content: [{ type: "text", text }],
timestamp: 1,
},
};
}
function customEntry(
id: string,
parentId: string | null,
content: string,
excludeFromContext: boolean,
): CustomMessageEntry {
return {
type: "custom_message",
id,
parentId,
timestamp: "2025-01-01T00:00:00Z",
customType: "status",
content,
display: true,
excludeFromContext,
};
}
describe("branch summarization", () => {
it("skips excluded custom messages before token budgeting", () => {
const user = userEntry("user", null, "keep");
const excluded = customEntry("custom", user.id, "x".repeat(1000), true);
const preparation = prepareBranchEntries([user, excluded], 10);
expect(preparation.messages.map((message) => message.role)).toEqual(["user"]);
expect(preparation.totalTokens).toBe(1);
});
});
@@ -15,9 +15,11 @@ import {
prepareCompaction,
shouldCompact,
} from "../src/core/compaction/index.ts";
import type { CustomMessage } from "../src/core/messages.ts";
import {
buildSessionContext,
type CompactionEntry,
type CustomMessageEntry,
type ModelChangeEntry,
migrateSessionEntries,
parseSessionEntries,
@@ -92,6 +94,22 @@ function createMessageEntry(message: AgentMessage): SessionMessageEntry {
return entry;
}
function createCustomMessageEntry(content: string, excludeFromContext?: boolean): CustomMessageEntry {
const id = `test-id-${entryCounter++}`;
const entry: CustomMessageEntry = {
type: "custom_message",
id,
parentId: lastId,
timestamp: new Date().toISOString(),
customType: "status",
content,
display: true,
excludeFromContext,
};
lastId = id;
return entry;
}
function createCompactionEntry(summary: string, firstKeptEntryId: string): CompactionEntry {
const id = `test-id-${entryCounter++}`;
const entry: CompactionEntry = {
@@ -184,6 +202,27 @@ describe("Token calculation", () => {
const usage = createMockUsage(0, 0, 0, 0);
expect(calculateContextTokens(usage)).toBe(0);
});
it("should ignore excluded custom messages in context token estimates", () => {
const excludedCustom: CustomMessage = {
role: "custom",
customType: "status",
content: "x".repeat(1000),
display: true,
excludeFromContext: true,
timestamp: Date.now(),
};
const visibleCustom: CustomMessage = { ...excludedCustom, excludeFromContext: false };
const assistant = createAssistantMessage("assistant", createMockUsage(10, 5));
expect(estimateContextTokens([excludedCustom])).toMatchObject({ tokens: 0, trailingTokens: 0 });
expect(estimateContextTokens([visibleCustom]).tokens).toBeGreaterThan(0);
expect(estimateContextTokens([assistant, excludedCustom])).toMatchObject({
tokens: 15,
usageTokens: 15,
trailingTokens: 0,
});
});
});
describe("getLastAssistantUsage", () => {
@@ -395,6 +434,22 @@ describe("buildSessionContext", () => {
});
});
describe("prepareCompaction with custom messages", () => {
it("should ignore excluded custom messages in token estimates and summarized messages", () => {
const excludedCustom = createCustomMessageEntry("x".repeat(1000), true);
const user = createMessageEntry(createUserMessage("keep"));
const preparation = prepareCompaction([excludedCustom, user], {
enabled: true,
reserveTokens: 0,
keepRecentTokens: 1,
});
expect(preparation).toBeDefined();
expect(preparation!.tokensBefore).toBe(1);
expect(preparation!.messagesToSummarize).toEqual([]);
});
});
describe("prepareCompaction with previous compaction", () => {
it("should preserve kept messages across repeated compactions when they still fit", () => {
const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)"));
@@ -4,7 +4,7 @@ import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { BashOperations } from "../../src/core/tools/bash.ts";
import { createHarness, type Harness } from "./harness.ts";
import { createHarness, getMessageText, type Harness } from "./harness.ts";
function getEntryTypes(harness: Harness): string[] {
return harness.sessionManager.getEntries().map((entry) => entry.type);
@@ -175,6 +175,40 @@ describe("AgentSession bash and persistence characterization", () => {
]);
});
it("excludes flagged custom messages from LLM context while preserving them", async () => {
const harness = await createHarness();
harnesses.push(harness);
let userTexts: string[] = [];
await harness.session.sendCustomMessage({
customType: "status",
content: "status panel",
display: true,
details: { a: 1 },
excludeFromContext: true,
});
harness.setResponses([
(context) => {
userTexts = context.messages
.filter((message) => message.role === "user")
.map((message) => getMessageText(message));
return fauxAssistantMessage("done");
},
]);
await harness.session.prompt("next prompt");
expect(userTexts).toEqual(["next prompt"]);
const entries = harness.sessionManager.getEntries();
expect(entries[0]?.type).toBe("custom_message");
if (entries[0]?.type !== "custom_message") return;
expect(entries[0].excludeFromContext).toBe(true);
expect(harness.sessionManager.buildSessionContext().messages[0]).toMatchObject({
role: "custom",
excludeFromContext: true,
});
});
it("does not emit message_end for bash execution messages", async () => {
const harness = await createHarness();
harnesses.push(harness);
@@ -258,6 +258,98 @@ describe("AgentSession queue characterization", () => {
expect(getAssistantTexts(harness)).toEqual(["", "original turn complete", "batched follow-up response"]);
});
it("records excluded custom messages with triggerTurn without starting a provider turn", async () => {
const harness = await createHarness();
harnesses.push(harness);
let providerCalled = false;
harness.setResponses([
() => {
providerCalled = true;
return fauxAssistantMessage("unexpected");
},
]);
await harness.session.sendCustomMessage(
{ customType: "status", content: "display only", display: true, details: {}, excludeFromContext: true },
{ triggerTurn: true },
);
expect(providerCalled).toBe(false);
expect(harness.session.messages).toHaveLength(1);
expect(harness.session.messages[0]).toMatchObject({
role: "custom",
customType: "status",
excludeFromContext: true,
});
expect(harness.getPendingResponseCount()).toBe(1);
});
it("records excluded custom messages with deliverAs steer while streaming", async () => {
const waiting = await createWaitingHarness();
const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting;
harnesses.push(harness);
let recordedBeforeRelease = false;
harness.setResponses([
fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }),
fauxAssistantMessage("done"),
]);
await waitForToolStart;
await harness.session.sendCustomMessage(
{ customType: "status", content: "steer display only", display: true, details: {}, excludeFromContext: true },
{ deliverAs: "steer" },
);
recordedBeforeRelease = harness.session.messages.some(
(message) => message.role === "custom" && message.customType === "status",
);
releaseToolExecution();
await promptPromise;
expect(recordedBeforeRelease).toBe(true);
expect(
harness.session.messages.filter((message) => message.role === "custom" && message.customType === "status"),
).toHaveLength(1);
});
it("records excluded custom messages with deliverAs followUp while streaming without starting another turn", async () => {
const waiting = await createWaitingHarness();
const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting;
harnesses.push(harness);
let providerCalledForFollowUp = false;
let recordedBeforeRelease = false;
harness.setResponses([
fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }),
fauxAssistantMessage("done"),
() => {
providerCalledForFollowUp = true;
return fauxAssistantMessage("unexpected follow-up");
},
]);
await waitForToolStart;
await harness.session.sendCustomMessage(
{
customType: "status",
content: "follow-up display only",
display: true,
details: {},
excludeFromContext: true,
},
{ deliverAs: "followUp" },
);
recordedBeforeRelease = harness.session.messages.some(
(message) => message.role === "custom" && message.customType === "status",
);
releaseToolExecution();
await promptPromise;
expect(recordedBeforeRelease).toBe(true);
expect(providerCalledForFollowUp).toBe(false);
expect(harness.getPendingResponseCount()).toBe(1);
});
it("queues custom messages with deliverAs steer while streaming", async () => {
const waiting = await createWaitingHarness();
const { harness, waitForToolStart, promptPromise, releaseToolExecution } = waiting;