fix(session): ignore internal synthetic turns

This commit is contained in:
YeonGyu-Kim
2026-05-15 23:16:05 +09:00
parent e8de8b79a8
commit c580b8f2ce
14 changed files with 464 additions and 80 deletions
+67 -1
View File
@@ -1,8 +1,13 @@
import { describe, expect, test } from "bun:test"
import {
OMO_INTERNAL_INITIATOR_MARKER,
createInternalAgentContinuationTextPart,
createInternalAgentTextPart,
hasInternalInitiatorMarker,
isRealUserMessage,
isRealUserTextPart,
isSyntheticOrInternalOnlyTextParts,
isSyntheticOrInternalUserMessage,
OMO_INTERNAL_INITIATOR_MARKER,
stripInternalInitiatorMarkers,
} from "./internal-initiator-marker"
@@ -145,4 +150,65 @@ describe("internal-initiator-marker", () => {
expect(result).toBe("")
})
})
describe("internal message guards", () => {
test("#given whitespace-normalized marker text #when checking marker presence #then detects it", () => {
// given
const text = "notice\n<!-- OMO_INTERNAL_INITIATOR -->"
// when
const result = hasInternalInitiatorMarker(text)
// then
expect(result).toBe(true)
})
test("#given synthetic and marker-only user parts #when classifying text parts #then treats them as internal-only", () => {
// given
const parts = [
{ type: "text", text: "hidden", synthetic: true },
{ type: "text", text: `reminder\n${OMO_INTERNAL_INITIATOR_MARKER}` },
]
// when
const result = isSyntheticOrInternalOnlyTextParts(parts)
// then
expect(result).toBe(true)
expect(parts.some(isRealUserTextPart)).toBe(false)
})
test("#given mixed real and internal user parts #when classifying #then keeps the message real", () => {
// given
const message = {
info: { role: "user" },
parts: [
{ type: "text", text: `reminder\n${OMO_INTERNAL_INITIATOR_MARKER}` },
{ type: "text", text: "actual user request" },
],
}
// when
const isInternal = isSyntheticOrInternalUserMessage(message)
// then
expect(isInternal).toBe(false)
expect(isRealUserMessage(message)).toBe(true)
})
test("#given user message with only a marker-tagged text part #when classifying #then rejects it as real user input", () => {
// given
const message = {
role: "user",
parts: [{ type: "text", text: `wake up\n${OMO_INTERNAL_INITIATOR_MARKER}` }],
}
// when
const result = isRealUserMessage(message)
// then
expect(result).toBe(false)
expect(isSyntheticOrInternalUserMessage(message)).toBe(true)
})
})
})
+59
View File
@@ -1,7 +1,66 @@
export const OMO_INTERNAL_INITIATOR_MARKER = "<!-- OMO_INTERNAL_INITIATOR -->"
const INTERNAL_INITIATOR_MARKER_DETECT_PATTERN = /<!--\s*OMO_INTERNAL_INITIATOR\s*-->/
const INTERNAL_INITIATOR_MARKER_PATTERN = /\n*<!--\s*OMO_INTERNAL_INITIATOR\s*-->\s*/g
export type InternalInitiatorTextPartLike = {
type?: string
text?: string
synthetic?: boolean
}
export type InternalInitiatorMessageLike = {
role?: string
info?: { role?: string }
parts?: readonly InternalInitiatorTextPartLike[]
}
export function hasInternalInitiatorMarker(text: string): boolean {
return INTERNAL_INITIATOR_MARKER_DETECT_PATTERN.test(text)
}
export function isTextPartLike(
part: InternalInitiatorTextPartLike
): part is InternalInitiatorTextPartLike & { type: "text"; text: string } {
return part.type === "text" && typeof part.text === "string"
}
export function isSyntheticOrInternalTextPart(
part: InternalInitiatorTextPartLike
): boolean {
return (
isTextPartLike(part) &&
(part.synthetic === true || hasInternalInitiatorMarker(part.text))
)
}
export function isRealUserTextPart(
part: InternalInitiatorTextPartLike
): part is InternalInitiatorTextPartLike & { type: "text"; text: string } {
return isTextPartLike(part) && !isSyntheticOrInternalTextPart(part)
}
export function isSyntheticOrInternalOnlyTextParts(
parts: readonly InternalInitiatorTextPartLike[] | undefined
): boolean {
const textParts = (parts ?? []).filter(isTextPartLike)
return textParts.length > 0 && textParts.every(isSyntheticOrInternalTextPart)
}
export function isSyntheticOrInternalUserMessage(
message: InternalInitiatorMessageLike
): boolean {
const role = message.info?.role ?? message.role
return role === "user" && isSyntheticOrInternalOnlyTextParts(message.parts)
}
export function isRealUserMessage(
message: InternalInitiatorMessageLike
): boolean {
const role = message.info?.role ?? message.role
return role === "user" && !isSyntheticOrInternalUserMessage(message)
}
export function stripInternalInitiatorMarkers(text: string): string {
return text.replace(INTERNAL_INITIATOR_MARKER_PATTERN, "").trimEnd()
}