Skip to content

Commit 616440a

Browse files
committed
fix(agents): strip transcript marker from model context
1 parent fff4263 commit 616440a

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

src/agents/embedded-agent-runner/tool-result-context-guard.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
formatContextLimitTruncationNotice,
99
installContextEngineLoopHook,
1010
installToolResultContextGuard,
11+
markTranscriptPromptText,
1112
PREEMPTIVE_CONTEXT_OVERFLOW_MESSAGE,
1213
} from "./tool-result-context-guard.js";
1314

@@ -669,6 +670,34 @@ describe("installContextEngineLoopHook", () => {
669670
});
670671
});
671672

673+
it("projects marked model prompts for ingest without leaking the marker to assembly", async () => {
674+
const agent = makeGuardableAgent();
675+
const engine = makeMockEngine();
676+
installHook(agent, engine, 0);
677+
678+
const modelPrompt = makeUser("model-only hook context\n\nvisible prompt");
679+
markTranscriptPromptText(modelPrompt, "visible prompt");
680+
const messages = [modelPrompt, makeToolResult("call_1", "result")];
681+
const transformed = await callTransform(agent, messages);
682+
683+
const afterTurnMessage = (recordMockArg(engine.afterTurn).messages as AgentMessage[])[0];
684+
const assembleMessage = (recordMockArg(engine.assemble).messages as AgentMessage[])[0];
685+
const transformedMessage = (transformed as AgentMessage[])[0];
686+
687+
expect(afterTurnMessage).toMatchObject({ role: "user", content: "visible prompt" });
688+
expect(JSON.stringify(afterTurnMessage)).not.toContain("__openclawTranscriptPromptText");
689+
expect(assembleMessage).toMatchObject({
690+
role: "user",
691+
content: "model-only hook context\n\nvisible prompt",
692+
});
693+
expect(JSON.stringify(assembleMessage)).not.toContain("__openclawTranscriptPromptText");
694+
expect(transformedMessage).toMatchObject({
695+
role: "user",
696+
content: "model-only hook context\n\nvisible prompt",
697+
});
698+
expect(JSON.stringify(transformedMessage)).not.toContain("__openclawTranscriptPromptText");
699+
});
700+
672701
it("calls afterTurn and assemble when new messages are appended after the first call", async () => {
673702
const agent = makeGuardableAgent();
674703
const engine = makeMockEngine();

src/agents/embedded-agent-runner/tool-result-context-guard.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ function restoreTranscriptPromptText(
102102
return restoredMessage;
103103
}
104104

105+
function stripTranscriptPromptMarker(message: AgentMessage): AgentMessage {
106+
if (getTranscriptPromptText(message) === undefined) {
107+
return message;
108+
}
109+
const { [TRANSCRIPT_PROMPT_TEXT_KEY]: _transcriptPromptText, ...messageRest } =
110+
message as unknown as Record<string, unknown>;
111+
return messageRest as unknown as AgentMessage;
112+
}
113+
105114
function projectTranscriptPromptMessages(
106115
messages: AgentMessage[],
107116
cache: WeakMap<AgentMessage, AgentMessage>,
@@ -115,6 +124,16 @@ function projectTranscriptPromptMessages(
115124
return changed ? projected : messages;
116125
}
117126

127+
function stripTranscriptPromptMarkers(messages: AgentMessage[]): AgentMessage[] {
128+
let changed = false;
129+
const stripped = messages.map((message) => {
130+
const next = stripTranscriptPromptMarker(message);
131+
changed ||= next !== message;
132+
return next;
133+
});
134+
return changed ? stripped : messages;
135+
}
136+
118137
function truncateTextToBudget(text: string, maxChars: number): string {
119138
if (text.length <= maxChars) {
120139
return text;
@@ -329,6 +348,7 @@ export function installContextEngineLoopHook(params: {
329348
sourceMessages,
330349
transcriptProjectionCache,
331350
);
351+
const providerMessages = stripTranscriptPromptMarkers(sourceMessages);
332352
const checkedPrefixLength =
333353
lastSeenLength == null ? 0 : Math.min(lastSeenLength, transcriptMessages.length);
334354
const sourceHistoryChanged =
@@ -359,7 +379,7 @@ export function installContextEngineLoopHook(params: {
359379
if (!hasNewMessages) {
360380
lastSeenLength = prePromptMessageCount;
361381
lastSourceMessages = transcriptMessages;
362-
return lastAssembledView ?? sourceMessages;
382+
return lastAssembledView ?? providerMessages;
363383
}
364384
try {
365385
if (typeof contextEngine.afterTurn === "function") {
@@ -401,11 +421,15 @@ export function installContextEngineLoopHook(params: {
401421
const assembled = await contextEngine.assemble({
402422
sessionId,
403423
sessionKey,
404-
messages: sourceMessages,
424+
messages: providerMessages,
405425
tokenBudget,
406426
model: modelId,
407427
});
408-
if (assembled && Array.isArray(assembled.messages) && assembled.messages !== sourceMessages) {
428+
if (
429+
assembled &&
430+
Array.isArray(assembled.messages) &&
431+
assembled.messages !== providerMessages
432+
) {
409433
lastAssembledView = assembled.messages;
410434
return assembled.messages;
411435
}
@@ -418,7 +442,7 @@ export function installContextEngineLoopHook(params: {
418442
lastSourceMessages = transcriptMessages;
419443
}
420444

421-
return sourceMessages;
445+
return providerMessages;
422446
}) as GuardableTransformContext;
423447

424448
return () => {

0 commit comments

Comments
 (0)