| 1 | // Presentation only: never rewrite persisted or provider-visible messages. |
| 2 | // Keep this tag vocabulary aligned with agent.TransientUserBlockTags. |
| 3 | export const transientUserTags = [ |
| 4 | "response-language", "reasoning-language", "memory-update", "background-jobs", |
| 5 | "active-goal", "autoresearch-runtime", "hook-context", "capability-route", |
| 6 | "interrupted-turn-recovery", "execution-policy", |
| 7 | ] as const; |
| 8 | const leadingBlock = new RegExp(`^\\s*<(${transientUserTags.join("|")})(?:\\s+[^>]*)?>[\\s\\S]*?</\\1>\\s*`); |
| 9 | const steerPrefix = "[Mid-turn steer queued by the user. Do not treat this as a new task; use it only as additional guidance for the current task after completing the current step.]"; |
| 10 | const deliveryMarker = `<delivery-runtime> |
| 11 | This session is in delivery-first mode. Use todo_write when a task benefits from |
| 12 | an explicit task list, update it from your own assessment, and finish when the |
| 13 | user's request is handled. Structured file tools require a current host-observed |
| 14 | file version; reading any useful window establishes that observation. |
| 15 | </delivery-runtime>`; |
| 16 | const hostRecoveryPrefixes = [ |
| 17 | "A tool failed. Use read-only diagnosis as needed", |
| 18 | "The tool timed out or hit a transient execution limit.", |
| 19 | ]; |
| 20 | |
| 21 | function legacyText(content: string): string { |
| 22 | let text = content; |
| 23 | for (let depth = 0; depth < 24; depth++) { |
| 24 | const next = text.replace(/<memory-compiler-execution>\s*([\s\S]*?)\s*<\/memory-compiler-execution>/g, (_block, json: string) => { |
| 25 | try { |
| 26 | const contract = JSON.parse(json); |
| 27 | const source = contract?.planner_ir?.source_event || contract?.source_event; |
| 28 | return typeof source === "string" ? source : ""; |
| 29 | } catch { return ""; } |
| 30 | }).replace(leadingBlock, ""); |
| 31 | if (next === text) break; |
| 32 | text = next; |
| 33 | } |
| 34 | if (text.trimStart().startsWith("# Reasonix executor handoff")) { |
| 35 | const start = text.indexOf("Original task:\n"); |
| 36 | if (start >= 0) text = text.slice(start + "Original task:\n".length).split("\n\nPlanner output:")[0]; |
| 37 | while (leadingBlock.test(text)) text = text.replace(leadingBlock, ""); |
| 38 | } |
| 39 | text = text.trimEnd(); |
| 40 | if (text.endsWith(deliveryMarker)) text = text.slice(0, -deliveryMarker.length); |
| 41 | text = text.replace(/\n*<execution-policy(?:\s+[^>]*)?>[\s\S]*?<\/execution-policy>\s*$/, ""); |
| 42 | if (text.trimEnd().endsWith("</memory-recall>")) { |
| 43 | const index = text.lastIndexOf("<memory-recall>"); |
| 44 | if (index >= 0) text = text.slice(0, index); |
| 45 | } |
| 46 | return text.trim(); |
| 47 | } |
| 48 | |
| 49 | export function canonicalUserDisplay(raw: Record<string, unknown>, fallback: string): { role: string; content: string } { |
| 50 | // An explicit origin wins over text heuristics. A user's quoted XML stays |
| 51 | // visible, while host snapshots stay hidden even when they have RawContent. |
| 52 | if (raw.origin === "host") return { role: "hidden", content: "" }; |
| 53 | const stored = typeof raw.content === "string" ? raw.content : fallback; |
| 54 | const authored = typeof raw.raw_content === "string" && raw.raw_content.trim() ? raw.raw_content : undefined; |
| 55 | const steer = legacyText(stored); |
| 56 | const text = authored ?? steer; |
| 57 | if (raw.origin !== "user" && !authored && /^<session-context version="1">\s*This host-generated snapshot supersedes every earlier session-context snapshot\./.test(text)) { |
| 58 | return { role: "hidden", content: "" }; |
| 59 | } |
| 60 | if (steer.startsWith(steerPrefix)) { |
| 61 | const content = authored ?? steer.slice(steerPrefix.length).trim(); |
| 62 | if (raw.origin !== "user" && !authored && hostRecoveryPrefixes.some(prefix => content.startsWith(prefix))) return { role: "hidden", content: "" }; |
| 63 | return { role: "notice", content: `↪ ${content}` }; |
| 64 | } |
| 65 | if (raw.origin !== "user" && !authored && hostRecoveryPrefixes.some(prefix => text.startsWith(prefix))) return { role: "hidden", content: "" }; |
| 66 | return { role: "user", content: text }; |
| 67 | } |
| 68 |