Skip to content

Commit 3da14ff

Browse files
committed
append injections to text parts
1 parent fcfebbe commit 3da14ff

File tree

4 files changed

+169
-34
lines changed

4 files changed

+169
-34
lines changed

lib/messages/inject/inject.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { CompressionPriorityMap } from "../priority"
77
import { compressPermission, getLastUserMessage } from "../../shared-utils"
88
import { saveSessionState } from "../../state/persistence"
99
import {
10+
appendToTextPart,
1011
appendIdToTool,
1112
createSyntheticTextPart,
1213
findLastToolPart,
@@ -162,6 +163,10 @@ export const injectMessageIds = (
162163
: undefined
163164
const tag = formatMessageIdTag(messageRef, priority ? { priority } : undefined)
164165

166+
if (appendToTextPart(message, tag)) {
167+
continue
168+
}
169+
165170
if (message.info.role === "user") {
166171
message.parts.push(createSyntheticTextPart(message, tag))
167172
continue

lib/messages/inject/utils.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type MessagePriority,
99
listPriorityRefsBeforeIndex,
1010
} from "../priority"
11-
import { createSyntheticTextPart, isIgnoredUserMessage } from "../utils"
11+
import { appendToTextPart, createSyntheticTextPart, isIgnoredUserMessage } from "../utils"
1212
import { getLastUserMessage } from "../../shared-utils"
1313
import { getCurrentTokenUsage } from "../../strategies/utils"
1414

@@ -192,20 +192,20 @@ export function buildCompressedBlockGuidance(state: SessionState): string {
192192
].join("\n")
193193
}
194194

195-
function appendGuidanceToDcpTag(hintText: string, guidance: string): string {
195+
function appendGuidanceToDcpTag(nudgeText: string, guidance: string): string {
196196
if (!guidance.trim()) {
197-
return hintText
197+
return nudgeText
198198
}
199199

200200
const closeTag = "</dcp-system-reminder>"
201-
const closeTagIndex = hintText.lastIndexOf(closeTag)
201+
const closeTagIndex = nudgeText.lastIndexOf(closeTag)
202202

203203
if (closeTagIndex === -1) {
204-
return hintText
204+
return nudgeText
205205
}
206206

207-
const beforeClose = hintText.slice(0, closeTagIndex).trimEnd()
208-
const afterClose = hintText.slice(closeTagIndex)
207+
const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd()
208+
const afterClose = nudgeText.slice(closeTagIndex)
209209
return `${beforeClose}\n\n${guidance}\n${afterClose}`
210210
}
211211

@@ -225,21 +225,25 @@ function buildMessagePriorityGuidance(
225225
return renderMessagePriorityGuidance(priorityLabel, refs)
226226
}
227227

228-
function injectAnchoredNudge(message: WithParts, hintText: string): void {
229-
if (!hintText.trim()) {
228+
function injectAnchoredNudge(message: WithParts, nudgeText: string): void {
229+
if (!nudgeText.trim()) {
230+
return
231+
}
232+
233+
if (appendToTextPart(message, nudgeText)) {
230234
return
231235
}
232236

233237
if (message.info.role === "user") {
234-
message.parts.push(createSyntheticTextPart(message, hintText))
238+
message.parts.push(createSyntheticTextPart(message, nudgeText))
235239
return
236240
}
237241

238242
if (message.info.role !== "assistant") {
239243
return
240244
}
241245

242-
const syntheticPart = createSyntheticTextPart(message, hintText)
246+
const syntheticPart = createSyntheticTextPart(message, nudgeText)
243247
const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
244248
if (firstToolIndex === -1) {
245249
message.parts.push(syntheticPart)
@@ -291,23 +295,23 @@ function collectTurnNudgeAnchors(
291295
function applyRangeModeAnchoredNudge(
292296
anchorMessageIds: Set<string>,
293297
messages: WithParts[],
294-
basePrompt: string,
298+
baseNudgeText: string,
295299
compressedBlockGuidance: string,
296300
): void {
297-
const hintText = appendGuidanceToDcpTag(basePrompt, compressedBlockGuidance)
298-
if (!hintText.trim()) {
301+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance)
302+
if (!nudgeText.trim()) {
299303
return
300304
}
301305

302306
for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
303-
injectAnchoredNudge(message, hintText)
307+
injectAnchoredNudge(message, nudgeText)
304308
}
305309
}
306310

307311
function applyMessageModeAnchoredNudge(
308312
anchorMessageIds: Set<string>,
309313
messages: WithParts[],
310-
basePrompt: string,
314+
baseNudgeText: string,
311315
compressionPriorities?: CompressionPriorityMap,
312316
): void {
313317
for (const { message, index } of collectAnchoredMessages(anchorMessageIds, messages)) {
@@ -317,8 +321,8 @@ function applyMessageModeAnchoredNudge(
317321
index,
318322
MESSAGE_MODE_NUDGE_PRIORITY,
319323
)
320-
const hintText = appendGuidanceToDcpTag(basePrompt, priorityGuidance)
321-
injectAnchoredNudge(message, hintText)
324+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, priorityGuidance)
325+
injectAnchoredNudge(message, nudgeText)
322326
}
323327
}
324328

lib/messages/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,35 @@ export const createSyntheticTextPart = (
6767

6868
type MessagePart = WithParts["parts"][number]
6969
type ToolPart = Extract<MessagePart, { type: "tool" }>
70+
type TextPart = Extract<MessagePart, { type: "text" }>
71+
72+
const findLastTextPart = (message: WithParts): TextPart | null => {
73+
for (let i = message.parts.length - 1; i >= 0; i--) {
74+
const part = message.parts[i]
75+
if (part.type === "text") {
76+
return part
77+
}
78+
}
79+
80+
return null
81+
}
82+
83+
export const appendToTextPart = (message: WithParts, injection: string): boolean => {
84+
const textPart = findLastTextPart(message)
85+
if (!textPart || typeof textPart.text !== "string") {
86+
return false
87+
}
88+
89+
const normalizedInjection = injection.replace(/^\n+/, "")
90+
if (!normalizedInjection.trim()) {
91+
return false
92+
}
93+
94+
const baseText = textPart.text.replace(/\n*$/, "")
95+
textPart.text =
96+
baseText.length > 0 ? `${baseText}\n\n${normalizedInjection}` : normalizedInjection
97+
return true
98+
}
7099

71100
export const appendIdToTool = (part: ToolPart, tag: string): boolean => {
72101
if (part.type !== "tool") {

tests/message-priority.test.ts

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import assert from "node:assert/strict"
22
import test from "node:test"
3+
import type { PluginConfig } from "../lib/config"
4+
import { createTextCompleteHandler } from "../lib/hooks"
35
import { assignMessageRefs } from "../lib/message-ids"
4-
import { buildPriorityMap } from "../lib/messages/priority"
56
import { injectMessageIds } from "../lib/messages/inject/inject"
67
import { applyAnchoredNudges } from "../lib/messages/inject/utils"
8+
import { buildPriorityMap } from "../lib/messages/priority"
79
import { stripHallucinationsFromString } from "../lib/messages/utils"
810
import { createSessionState, type WithParts } from "../lib/state"
9-
import type { PluginConfig } from "../lib/config"
10-
import { createTextCompleteHandler } from "../lib/hooks"
1111

12-
function buildConfig(): PluginConfig {
12+
function buildConfig(mode: "message" | "range" = "message"): PluginConfig {
1313
return {
1414
enabled: true,
1515
debug: false,
@@ -33,7 +33,7 @@ function buildConfig(): PluginConfig {
3333
},
3434
protectedFilePatterns: [],
3535
compress: {
36-
mode: "message",
36+
mode,
3737
permission: "allow",
3838
showCompression: false,
3939
maxContextLimit: 150000,
@@ -68,6 +68,28 @@ function textPart(messageID: string, sessionID: string, id: string, text: string
6868
}
6969
}
7070

71+
function toolPart(
72+
messageID: string,
73+
sessionID: string,
74+
callID: string,
75+
toolName: string,
76+
output: string,
77+
) {
78+
return {
79+
id: `${callID}-part`,
80+
messageID,
81+
sessionID,
82+
type: "tool" as const,
83+
tool: toolName,
84+
callID,
85+
state: {
86+
status: "completed" as const,
87+
input: { description: "demo" },
88+
output,
89+
},
90+
}
91+
}
92+
7193
function buildMessage(
7294
id: string,
7395
role: "user" | "assistant",
@@ -106,11 +128,28 @@ function repeatedWord(word: string, count: number): string {
106128
return Array.from({ length: count }, () => word).join(" ")
107129
}
108130

109-
test("injectMessageIds adds priority attributes in message mode", () => {
131+
test("injectMessageIds appends priority tags to existing text parts in message mode", () => {
110132
const sessionID = "ses_message_priority_tags"
111133
const messages: WithParts[] = [
112134
buildMessage("msg-user-1", "user", sessionID, repeatedWord("investigate", 6000), 1),
113-
buildMessage("msg-assistant-1", "assistant", sessionID, "Short follow-up note.", 2),
135+
{
136+
info: {
137+
id: "msg-assistant-1",
138+
role: "assistant",
139+
sessionID,
140+
agent: "assistant",
141+
time: { created: 2 },
142+
} as WithParts["info"],
143+
parts: [
144+
textPart(
145+
"msg-assistant-1",
146+
sessionID,
147+
"msg-assistant-1-part",
148+
"Short follow-up note.",
149+
),
150+
toolPart("msg-assistant-1", sessionID, "call-task-1", "task", "task output body"),
151+
],
152+
},
114153
]
115154
const state = createSessionState()
116155
const config = buildConfig()
@@ -120,19 +159,28 @@ test("injectMessageIds adds priority attributes in message mode", () => {
120159

121160
injectMessageIds(state, config, messages, compressionPriorities)
122161

123-
const userTag = messages[0]?.parts[messages[0].parts.length - 1]
124-
const assistantTag = messages[1]?.parts[messages[1].parts.length - 1]
162+
assert.equal(messages[0]?.parts.length, 1)
163+
assert.equal(messages[1]?.parts.length, 2)
125164

126-
assert.equal(userTag?.type, "text")
127-
assert.equal(assistantTag?.type, "text")
128-
assert.match((userTag as any).text, /<dcp-message-id priority="high">m0001<\/dcp-message-id>/)
165+
const userText = messages[0]?.parts[0]
166+
const assistantText = messages[1]?.parts[0]
167+
const assistantTool = messages[1]?.parts[1]
168+
169+
assert.equal(userText?.type, "text")
170+
assert.equal(assistantText?.type, "text")
171+
assert.equal(assistantTool?.type, "tool")
129172
assert.match(
130-
(assistantTag as any).text,
131-
/<dcp-message-id priority="low">m0002<\/dcp-message-id>/,
173+
(userText as any).text,
174+
/\n\n<dcp-message-id priority="high">m0001<\/dcp-message-id>/,
132175
)
176+
assert.match(
177+
(assistantText as any).text,
178+
/\n\n<dcp-message-id priority="low">m0002<\/dcp-message-id>/,
179+
)
180+
assert.equal((assistantTool as any).state.output, "task output body")
133181
})
134182

135-
test("message-mode nudges list only earlier visible high-priority message IDs", () => {
183+
test("message-mode nudges append to existing text parts and list only earlier visible high-priority message IDs", () => {
136184
const sessionID = "ses_message_priority_nudges"
137185
const messages: WithParts[] = [
138186
buildMessage("msg-user-1", "user", sessionID, repeatedWord("alpha", 6000), 1),
@@ -168,15 +216,64 @@ test("message-mode nudges list only earlier visible high-priority message IDs",
168216
compressionPriorities,
169217
)
170218

171-
const injectedNudge = messages[2]?.parts[messages[2].parts.length - 1]
219+
assert.equal(messages[2]?.parts.length, 1)
220+
221+
const injectedNudge = messages[2]?.parts[0]
172222
assert.equal(injectedNudge?.type, "text")
223+
assert.match((injectedNudge as any).text, /\n\n<dcp-system-reminder>Base context nudge/)
173224
assert.match((injectedNudge as any).text, /Message priority context:/)
174225
assert.match((injectedNudge as any).text, /High-priority message IDs before this point: m0001/)
175226
assert.doesNotMatch((injectedNudge as any).text, /m0002/)
176227
assert.doesNotMatch((injectedNudge as any).text, /m0003/)
177228
assert.doesNotMatch((injectedNudge as any).text, /m0004/)
178229
})
179230

231+
test("range-mode nudges append to existing text parts before tool outputs", () => {
232+
const sessionID = "ses_range_nudge_injection"
233+
const messages: WithParts[] = [
234+
buildMessage("msg-user-1", "user", sessionID, repeatedWord("alpha", 6000), 1),
235+
{
236+
info: {
237+
id: "msg-assistant-1",
238+
role: "assistant",
239+
sessionID,
240+
agent: "assistant",
241+
time: { created: 2 },
242+
} as WithParts["info"],
243+
parts: [
244+
textPart("msg-assistant-1", sessionID, "msg-assistant-1-part", "Working summary."),
245+
toolPart("msg-assistant-1", sessionID, "call-task-2", "task", "task output body"),
246+
],
247+
},
248+
]
249+
const state = createSessionState()
250+
const config = buildConfig("range")
251+
252+
assignMessageRefs(state, messages)
253+
state.prune.messages.activeBlockIds.add(7)
254+
state.nudges.contextLimitAnchors.add("msg-assistant-1")
255+
256+
applyAnchoredNudges(state, config, messages, {
257+
system: "",
258+
compressRange: "",
259+
compressMessage: "",
260+
contextLimitNudge: "<dcp-system-reminder>Base context nudge</dcp-system-reminder>",
261+
turnNudge: "<dcp-system-reminder>Base turn nudge</dcp-system-reminder>",
262+
iterationNudge: "<dcp-system-reminder>Base iteration nudge</dcp-system-reminder>",
263+
})
264+
265+
assert.equal(messages[1]?.parts.length, 2)
266+
267+
const injectedNudge = messages[1]?.parts[0]
268+
const toolOutput = messages[1]?.parts[1]
269+
assert.equal(injectedNudge?.type, "text")
270+
assert.equal(toolOutput?.type, "tool")
271+
assert.match((injectedNudge as any).text, /\n\n<dcp-system-reminder>Base context nudge/)
272+
assert.match((injectedNudge as any).text, /Compressed block context:/)
273+
assert.match((injectedNudge as any).text, /Active compressed blocks in this session: 1 \(b7\)/)
274+
assert.equal((toolOutput as any).state.output, "task output body")
275+
})
276+
180277
test("hallucination stripping removes exact metadata tags and preserves lookalikes", async () => {
181278
const text =
182279
'alpha<dcp-message-id priority="high">m0007</dcp-message-id>' +

0 commit comments

Comments
 (0)