Skip to main content

a3s_code_core/
transcript.rs

1//! Product-transcript helpers for conversation messages.
2//!
3//! Model-wire history may carry composed Desktop context, planner chrome, and
4//! runtime steering. The product transcript must prefer the human-authored
5//! sentence. Keep this parser conservative: unwrap explicit bridge markers and
6//! strip known host appendices; never invent paraphrases.
7
8/// Extract the human-authored task from a possibly composed / planner-wrapped
9/// prompt. Returns empty when the text is pure runtime steering with no human
10/// sentence to recover.
11pub fn product_user_text(text: &str) -> String {
12    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
13    let mut working = normalized.as_str();
14
15    working = strip_prefix_fence(working, "```open-reply-review-findings");
16
17    let planner_markers = [
18        "\nPlanner-optimized request:",
19        "\n规划优化请求:",
20        "\n规划优化请求:",
21        "\n优化后的请求:",
22        "\n优化后的请求:",
23        // PrePlanning hooks dual-mark like the planner; keep only the Original /
24        // human prefix for product transcript.
25        "\nHook-modified planning task:",
26        "\nPlanning hook guidance:",
27    ];
28    for marker in planner_markers {
29        if let Some((before, _)) = working.split_once(marker) {
30            working = before;
31        }
32    }
33
34    if let Some((before, _)) = working.split_once("\n\nAutomatic subagent context:\n") {
35        working = before;
36    }
37
38    // PrePrompt hooks may append model-only context. Never surface it in the
39    // product user bubble — strip both well-formed blocks and a dangling open tag.
40    working = strip_user_prompt_hook_context(working);
41
42    // Prefer the last explicit human marker. Desktop follow-ups use
43    // "Latest user message:"; first turns use "User task:".
44    let user_markers = [
45        "\nLatest user message:\n",
46        "\nLatest user message:",
47        "\n最新用户消息:",
48        "\n最新用户消息:",
49        "\nUser task:\n",
50        "\nUser task:",
51        "\n用户任务:",
52        "\n用户任务:",
53    ];
54    let mut task = working;
55    for marker in user_markers {
56        if let Some((_, after)) = working.rsplit_once(marker) {
57            task = after;
58            break;
59        }
60    }
61    if std::ptr::eq(task, working) {
62        let leading_markers = [
63            "Latest user message:\n",
64            "Latest user message:",
65            "最新用户消息:",
66            "最新用户消息:",
67            "User task:\n",
68            "User task:",
69            "用户任务:",
70            "用户任务:",
71        ];
72        for marker in leading_markers {
73            if let Some(after) = working.strip_prefix(marker) {
74                task = after;
75                break;
76            }
77        }
78    }
79
80    let trimmed = task
81        .trim()
82        .trim_start_matches("Original user request:")
83        .trim_start_matches("Original user request:")
84        .trim();
85
86    if is_pure_runtime_steering(trimmed) || trimmed.is_empty() {
87        String::new()
88    } else {
89        trimmed.to_string()
90    }
91}
92
93/// True when `text` should be stored as a wire-only user turn.
94pub fn is_wire_only_user_text(text: &str) -> bool {
95    product_user_text(text).is_empty() && !text.trim().is_empty()
96}
97
98fn strip_prefix_fence<'a>(text: &'a str, fence: &str) -> &'a str {
99    let trimmed = text.trim_start();
100    if !trimmed.starts_with(fence) {
101        return text;
102    }
103    let after_open = &trimmed[fence.len()..];
104    let after_open = after_open.strip_prefix('\n').unwrap_or(after_open);
105    if let Some((_, rest)) = after_open.split_once("\n```") {
106        let rest = rest.trim_start_matches('\n');
107        if rest.is_empty() {
108            text
109        } else {
110            rest
111        }
112    } else {
113        text
114    }
115}
116
117fn strip_user_prompt_hook_context(text: &str) -> &str {
118    const OPEN: &str = "\n\n<user-prompt-hook-context>\n";
119    if let Some(start) = text.find(OPEN) {
120        // Hooks append this block at the end of the wire prompt. Keep the
121        // human prefix whether or not the close tag is present.
122        return text[..start].trim_end();
123    }
124    if let Some(start) = text.find("<user-prompt-hook-context>") {
125        return text[..start].trim_end();
126    }
127    text
128}
129
130fn is_pure_runtime_steering(text: &str) -> bool {
131    let text = text.trim_start();
132    if text.is_empty() {
133        return false;
134    }
135    (text.starts_with("Delegated plan step '") && text.contains(" failed:\n"))
136        || (text.starts_with("Tool '")
137            && (text.contains(" execution was REJECTED")
138                || text.contains(" confirmation failed:")
139                || text.contains(" cancelled by caller")))
140        || text.starts_with("Delegated parallel plan wave completed with failures:")
141        || (text.starts_with("Goal:") && text.contains("Execute the following plan step by step:"))
142        || text.starts_with("Execute step ")
143        || text.starts_with("Execute the following plan step by step:")
144        || text.starts_with("[Context Summary:")
145        || text.starts_with("Tool-use budget reached.")
146        || text.starts_with('{') && text.contains("\"type\":\"parallel_results\"")
147        || text.starts_with("Address each open review finding")
148}
149
150#[cfg(test)]
151mod tests {
152    use super::{is_wire_only_user_text, product_user_text};
153
154    #[test]
155    fn unwraps_desktop_user_task() {
156        let text = "A3S Desktop workbench context:\n- Active workbench: Office.\n\nUser task:\n规划如何开发一个游戏引擎?";
157        assert_eq!(product_user_text(text), "规划如何开发一个游戏引擎?");
158        assert!(!is_wire_only_user_text(text));
159    }
160
161    #[test]
162    fn unwraps_desktop_latest_user_message() {
163        let text = "A3S Desktop workbench context:\n- Active workbench: Office.\n\nLatest user message:\n再试试";
164        assert_eq!(product_user_text(text), "再试试");
165    }
166
167    #[test]
168    fn unwraps_planner_wrapper_with_latest_user_message() {
169        let text = "Original user request:\nA3S Desktop workbench context:\nLatest user message:\n再试试\n\nPlanner-optimized request:\n再试试\n当前请求仅表示“再试试”,需先澄清。";
170        assert_eq!(product_user_text(text), "再试试");
171    }
172
173    #[test]
174    fn strips_pre_prompt_hook_context_appendix() {
175        let text = "Ship the release\n\n<user-prompt-hook-context>\npolicy note\n</user-prompt-hook-context>";
176        assert_eq!(product_user_text(text), "Ship the release");
177    }
178
179    #[test]
180    fn strips_pre_planning_hook_appendix() {
181        let text = [
182            "Original user request:",
183            "A3S Desktop workbench context:",
184            "User task:",
185            "Ship the release",
186            "",
187            "Hook-modified planning task:",
188            "Rewrite the release checklist first",
189            "",
190            "Planning hook guidance:",
191            "Selected strategy: careful",
192        ]
193        .join("\n");
194        assert_eq!(product_user_text(&text), "Ship the release");
195    }
196
197    #[test]
198    fn keeps_human_skill_slash_spelling_from_desktop_compose() {
199        let text = "A3S Desktop workbench context:\n- Active workbench: Office.\n\nUser task:\n/a3s-box run nginx";
200        assert_eq!(product_user_text(text), "/a3s-box run nginx");
201    }
202
203    #[test]
204    fn unwraps_planner_optimized_wrapper() {
205        let text = "Original user request:\nA3S Desktop workbench context:\nUser task: 工作区有哪些文件?\nPlanner-optimized request:\nlist files";
206        assert_eq!(product_user_text(text), "工作区有哪些文件?");
207    }
208
209    #[test]
210    fn strips_auto_delegation_appendix() {
211        let text = "Ship the release\n\nAutomatic subagent context:\n{\"tasks\":[]}\n\nUse the subagent findings as evidence, but make the final decision yourself.";
212        assert_eq!(product_user_text(text), "Ship the release");
213    }
214
215    #[test]
216    fn blanks_plan_execute_goal_chrome() {
217        let text = "Goal: ship it\n\nExecute the following plan step by step:\n1. Draft\n2. Review";
218        assert_eq!(product_user_text(text), "");
219        assert!(is_wire_only_user_text(text));
220    }
221
222    #[test]
223    fn blanks_plan_execute_step_chrome() {
224        let text = "Execute step 2: Draft the architecture";
225        assert_eq!(product_user_text(text), "");
226        assert!(is_wire_only_user_text(text));
227    }
228
229    #[test]
230    fn blanks_compaction_summary_chrome() {
231        let text = "[Context Summary: prior turns compressed]\nKeep working.";
232        assert_eq!(product_user_text(text), "");
233        assert!(is_wire_only_user_text(text));
234    }
235
236    #[test]
237    fn unwraps_open_reply_review_findings_fence() {
238        let text =
239            "```open-reply-review-findings\nDATA\n```\nUser task:\nFix the failing assertion";
240        assert_eq!(product_user_text(text), "Fix the failing assertion");
241    }
242
243    #[test]
244    fn message_constructors_segregate_wire_and_transcript() {
245        use crate::llm::Message;
246
247        let product = Message::user("hello");
248        assert!(product.is_product_transcript());
249        assert_eq!(product.transcript_display_text(), "hello");
250
251        let wire = Message::user_wire("Execute step 1: Survey");
252        assert!(!wire.is_product_transcript());
253        assert_eq!(wire.text(), "Execute step 1: Survey");
254
255        let dual = Message::user_for_model_with_transcript(
256            "A3S Desktop workbench context:\n\nUser task:\n规划引擎",
257            "规划引擎",
258        );
259        assert!(dual.is_product_transcript());
260        assert_eq!(dual.transcript_display_text(), "规划引擎");
261        assert!(dual.text().contains("User task:"));
262
263        let address_reply = Message::assistant_wire("Finding f1: rebutted with tool exit 0");
264        assert!(!address_reply.is_product_transcript());
265        assert_eq!(
266            address_reply.text(),
267            "Finding f1: rebutted with tool exit 0"
268        );
269    }
270}