1use serde_json::Value;
2
3pub async fn trace_runtime_flow(args: &Value) -> Result<String, String> {
4 let topic = args
5 .get("topic")
6 .and_then(|v| v.as_str())
7 .unwrap_or("user_turn");
8
9 match topic {
10 "user_turn" => Ok(trace_user_turn(args)),
11 "session_reset" => Ok(trace_session_reset(args)),
12 "reasoning_split" => Ok(trace_reasoning_split()),
13 "runtime_subsystems" => Ok(trace_runtime_subsystems()),
14 "startup" => Ok(trace_startup()),
15 "voice" => Ok(trace_voice()),
16 other => Err(format!(
17 "Unknown topic '{}'. Use one of: user_turn, session_reset, reasoning_split, runtime_subsystems, startup, voice.",
18 other
19 )),
20 }
21}
22
23fn trace_user_turn(args: &Value) -> String {
24 let input = args
25 .get("input")
26 .and_then(|v| v.as_str())
27 .unwrap_or("who are you?");
28
29 format!(
30 "Verified runtime trace for a normal text turn with input {:?}\n\n\
31Visible chat output path\n\
321. Keyboard input is collected inside `run_app` in `src/ui/tui.rs`. When Enter is pressed on a non-slash command, the TUI drains `app.input`, pushes `You`, marks `app.agent_running = true`, and sends the text through `app.user_input_tx`.\n\
33 File refs: `src/ui/tui.rs` -> `run_app`, `App::push_message`\n\
342. `app.user_input_tx` is the `user_input_tx` side of `tokio::sync::mpsc::channel::<UserTurn>(32)` assembled inside `build_runtime_bundle` in `src/runtime.rs`. The receiver side is `user_input_rx`.\n\
35 File refs: `src/runtime.rs` -> `build_runtime_bundle`; `src/main.rs` -> `main`\n\
363. `main` spawns `run_agent_loop(AgentLoopRuntime {{ user_input_rx, agent_tx, ... }}, AgentLoopConfig {{ ... }})`. That loop waits on `user_input_rx.recv()` and forwards each turn into `ConversationManager::run_turn(&input, agent_tx.clone(), yolo)`.\n\
37 File refs: `src/runtime.rs` -> `run_agent_loop`, `AgentLoopRuntime`, `AgentLoopConfig`; `src/agent/conversation.rs` -> `ConversationManager::run_turn`\n\
384. `ConversationManager::run_turn` handles slash-command short circuits first. For a normal prompt like {:?}, it builds the system prompt, updates `self.history`, queries Vein context, and calls `InferenceEngine::call_with_tools(&prompt_msgs, &self.tools, ...)`.\n\
39 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`; `src/agent/inference.rs` -> `InferenceEngine::call_with_tools`\n\
405. If the model returns final text and no tool calls, `run_turn` strips think blocks with `strip_think_blocks`, records `ChatMessage::assistant_text(&cleaned)`, then streams the visible reply out as `InferenceEvent::Token(chunk)` values followed by `InferenceEvent::Done`.\n\
41 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`; `src/agent/inference.rs` -> `strip_think_blocks`, `InferenceEvent`\n\
426. `run_app` receives those events on `agent_rx`, handles `InferenceEvent::Token` / `InferenceEvent::MutedToken`, ensures the current speaker is `Hematite`, and appends text with `app.update_last_message(token)`. `InferenceEvent::Done` clears busy state and finalizes reasoning state.\n\
43 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Token`, `InferenceEvent::MutedToken`, `InferenceEvent::Done`\n\n\
44Reasoning and specular path\n\
451. Model reasoning emitted inside `<think>` blocks is split out in `ConversationManager::run_turn` with `extract_think_block` and sent to the TUI as `InferenceEvent::Thought(thought)`.\n\
46 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`; `src/agent/inference.rs` -> `extract_think_block`, `InferenceEvent::Thought`\n\
472. `run_app` handles `InferenceEvent::Thought` separately from visible chat text by setting `app.thinking = true` and appending the payload into `app.current_thought`.\n\
48 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Thought`\n\
493. File-watcher diagnostics travel on a different channel. `build_runtime_bundle` creates `specular_tx` / `specular_rx`, `spawn_watcher(specular_tx)` starts the watcher, and `run_app` handles `SpecularEvent::FileChanged` and `SpecularEvent::SyntaxError` in a separate branch from `agent_rx`.\n\
50 File refs: `src/runtime.rs` -> `build_runtime_bundle`; `src/agent/specular.rs` -> `spawn_watcher`, `SpecularEvent`; `src/ui/tui.rs` -> `run_app`\n\n\
51Voice path\n\
521. `build_runtime_bundle` constructs `VoiceManager::new(agent_tx.clone())`, so the voice subsystem can emit `InferenceEvent::VoiceStatus` messages back into the same `agent_tx` channel used for model events.\n\
53 File refs: `src/runtime.rs` -> `build_runtime_bundle`; `src/ui/voice.rs` -> `VoiceManager::new`\n\
542. In `run_app`, only `InferenceEvent::Token` triggers speech. `InferenceEvent::MutedToken` is displayed but not spoken. When speech is allowed, the TUI calls `app.voice_manager.speak(token.clone())`.\n\
55 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Token`, `InferenceEvent::MutedToken`\n\
563. `VoiceManager::speak` pushes text into its internal sync channel. Background threads in `VoiceManager::new` assemble sentence chunks, synthesize them through `TTSKoko::tts_raw_audio_streaming`, and append PCM chunks into the active `rodio::Sink`.\n\
57 File refs: `src/ui/voice.rs` -> `VoiceManager::speak`, `VoiceManager::new`\n\
584. When the turn finishes, `run_app` handles `InferenceEvent::Done` and calls `app.voice_manager.flush()` if voice is enabled.\n\
59 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Done`\n\n\
60Possible weak points\n\
61- `ConversationManager::run_turn` in `src/agent/conversation.rs` is a very large control hub. It owns command handling, prompt assembly, tool orchestration, verification, and session persistence in one place.\n\
62- `run_agent_loop` in `src/runtime.rs` is the single consumer of `user_input_rx`. If inference or tool work stalls inside one turn, the next user turn waits behind it.\n\
63- `spawn_watcher` in `src/agent/specular.rs` runs `cargo check` after `.rs` file modifications and `run_app` can auto-inject a repair prompt from `SpecularEvent::SyntaxError` when the user is idle. That can be noisy or surprising during rapid edits."
64 ,
65 input,
66 input
67 )
68}
69
70fn trace_session_reset(args: &Value) -> String {
71 let command = args
72 .get("command")
73 .and_then(|v| v.as_str())
74 .unwrap_or("all");
75
76 let header = match command {
77 "/clear" => "Verified reset trace for /clear",
78 "/new" => "Verified reset trace for /new",
79 "/forget" => "Verified reset trace for /forget",
80 _ => "Verified reset trace for /clear, /new, and /forget",
81 };
82
83 let mut out = format!("{header}\n\n");
84
85 if command == "all" || command == "/clear" {
86 out.push_str(
87 "/clear\n\
881. The slash command is handled entirely inside `run_app` in `src/ui/tui.rs`.\n\
892. It clears TUI-local state: `messages`, `messages_raw`, `last_reasoning`, `current_thought`, `specular_logs`, `active_context`, and resets `current_objective` to `Idle`.\n\
903. It does not send anything through `user_input_tx`, so `ConversationManager::run_turn` is not involved.\n\
914. It pushes the visible system message `Dialogue buffer cleared.` and returns to the event loop.\n\
92 File refs: `src/ui/tui.rs` -> `run_app`, `/clear` branch\n\n"
93 );
94 }
95
96 if command == "all" || command == "/new" {
97 out.push_str(
98 "/new\n\
991. `run_app` clears the visible TUI state, clears pending attachments, pushes `You: /new`, marks `app.agent_running = true`, and sends `UserTurn::text(\"/new\")` through `app.user_input_tx`.\n\
100 File refs: `src/ui/tui.rs` -> `run_app`, `/new` branch\n\
1012. `run_agent_loop` receives `/new` from `user_input_rx` and forwards it into `ConversationManager::run_turn(&input, agent_tx.clone(), yolo)`.\n\
102 File refs: `src/runtime.rs` -> `run_agent_loop`, `user_input_rx`\n\
1033. `ConversationManager::run_turn` matches `user_input.trim() == \"/new\"`, then clears `history`, `reasoning_history`, `session_memory`, `running_summary`, `correction_hints`, and `pinned_files`, resets task files, removes `session.json`, rewrites the empty session file, and streams the fresh-context confirmation before `InferenceEvent::Done`.\n\
104 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`, `reset_task_files`, `session_path`\n\n"
105 );
106 }
107
108 if command == "all" || command == "/forget" {
109 out.push_str(
110 "/forget\n\
1111. `run_app` clears the same visible TUI state as `/new`, clears pending attachments, pushes `You: /forget`, marks `app.agent_running = true`, and sends `UserTurn::text(\"/forget\")` through `app.user_input_tx`.\n\
112 File refs: `src/ui/tui.rs` -> `run_app`, `/forget` branch\n\
1132. `run_agent_loop` forwards that string into `ConversationManager::run_turn`.\n\
114 File refs: `src/runtime.rs` -> `run_agent_loop`\n\
1153. `ConversationManager::run_turn` matches `user_input.trim() == \"/forget\"`, clears `history`, `reasoning_history`, `session_memory`, `running_summary`, `correction_hints`, and `pinned_files`, resets task files, purges saved memory artifacts, resets the Vein index, removes and rewrites `session.json`, then streams the hard-forget confirmation before `InferenceEvent::Done`.\n\
116 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`, `reset_task_files`, `session_path`\n\n"
117 );
118 }
119
120 out.push_str(
121 "Possible weak points\n\
122- Reset behavior is split across `src/ui/tui.rs` and `src/agent/conversation.rs`, so future changes can drift if both sides are not updated together.\n\
123- `/clear` is UI-only while `/new` and `/forget` cross the channel boundary into the agent loop. That difference is real and easy to misstate if it is not documented."
124 );
125
126 out
127}
128
129fn trace_reasoning_split() -> String {
130 "Verified reasoning/specular split\n\n\
1311. Model reasoning is represented by `InferenceEvent::Thought` in `src/agent/inference.rs`.\n\
1322. `ConversationManager::run_turn` sends `InferenceEvent::Thought` when it extracts a `<think>` block or emits internal status updates during tool execution and verification.\n\
133 File refs: `src/agent/conversation.rs` -> `ConversationManager::run_turn`; `src/agent/inference.rs` -> `InferenceEvent`, `extract_think_block`\n\
1343. `run_app` handles `InferenceEvent::Thought` by appending to `app.current_thought`; it does not append that payload into the main dialogue transcript.\n\
135 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Thought`\n\
1364. Visible assistant text is carried on `InferenceEvent::Token` and `InferenceEvent::MutedToken`, then appended into the `Hematite` chat message via `app.update_last_message(token)`.\n\
137 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Token`, `InferenceEvent::MutedToken`\n\
1385. Watcher-driven specular events are separate from model reasoning. `SpecularEvent::FileChanged` and `SpecularEvent::SyntaxError` come from `spawn_watcher` in `src/agent/specular.rs` over `specular_rx`, not over `agent_rx`.\n\
139 File refs: `src/runtime.rs` -> `build_runtime_bundle`; `src/agent/specular.rs` -> `spawn_watcher`, `SpecularEvent`; `src/ui/tui.rs` -> `run_app`\n\n\
140Possible weak points\n\
141- The SPECULAR panel currently mixes watcher events and model reasoning in one UI area, even though they arrive from different event sources.\n\
142- `current_thought` and `last_reasoning` are TUI-managed buffers, so UI reset bugs can make the panel look stale even when the model state is clean."
143 .to_string()
144}
145
146fn trace_runtime_subsystems() -> String {
147 "Verified runtime subsystems\n\n\
148- UI and input surface: `src/ui/tui.rs` -> `run_app`, `App`\n\
149- Agent turn loop and tool orchestration: `src/agent/conversation.rs` -> `ConversationManager`, `ConversationManager::run_turn`\n\
150- Model transport and event schema: `src/agent/inference.rs` -> `InferenceEngine`, `InferenceEvent`\n\
151- Runtime bundle assembly and channels: `src/runtime.rs` -> `build_runtime_bundle`, `run_agent_loop`, `user_input_tx`, `user_input_rx`, `agent_tx`, `agent_rx`, `specular_rx`\n\
152- Voice subsystem: `src/ui/voice.rs` -> `VoiceManager`\n\
153- File watcher / specular subsystem: `src/agent/specular.rs` -> `spawn_watcher`, `SpecularEvent`\n\
154- Memory and project indexing: `src/agent/conversation.rs` -> `initialize_vein`, `build_vein_context`; `src/memory/vein.rs`\n\
155- External MCP tool bridge: `src/agent/mcp.rs`, `src/agent/mcp_manager.rs`\n\
156- LSP bridge: `src/agent/lsp`, `src/tools/lsp_tools.rs`\n\n\
157Primary communication paths\n\
158- TUI to agent turn loop: `user_input_tx` -> `user_input_rx`\n\
159- Agent turn loop to TUI: `agent_tx` -> `agent_rx` carrying `InferenceEvent`\n\
160- File watcher to TUI: `specular_rx` carrying `SpecularEvent`\n\
161- Swarm worker progress to TUI: `swarm_tx` -> `swarm_rx` carrying `SwarmMessage`\n\n\
162Possible weak points\n\
163- Runtime flow is distributed across `src/runtime.rs`, `src/main.rs`, `src/ui/tui.rs`, and `src/agent/conversation.rs`, so architectural questions are easy for the model to blur without a grounded helper.\n\
164- `ConversationManager::run_turn` is still the highest-complexity subsystem and the main maintenance hotspot."
165 .to_string()
166}
167
168fn trace_startup() -> String {
169 "Verified startup flow\n\n\
1701. `main` parses CLI args, then calls `build_runtime_bundle(...)` to assemble the runtime. That function builds `InferenceEngine`, starts GPU and git monitors, runs the LM Studio health check, detects the loaded model and context length, creates channels, starts the watcher, and constructs the voice/swarm services.\n\
171 File refs: `src/main.rs` -> `main`; `src/runtime.rs` -> `build_runtime_bundle`; `src/agent/inference.rs` -> `InferenceEngine::new`, `health_check`, `get_loaded_model`, `detect_context_length`\n\
1722. `main` spawns `run_agent_loop(...)` for steady-state turn handling and `spawn_runtime_profile_sync(...)` for background LM Studio profile refresh.\n\
173 File refs: `src/main.rs` -> `main`; `src/runtime.rs` -> `run_agent_loop`, `spawn_runtime_profile_sync`\n\
1743. `main` enters alternate-screen TUI mode and awaits `run_app(...)` with the already-assembled receivers, senders, and runtime services.\n\
175 File refs: `src/main.rs` -> `main`; `src/ui/tui.rs` -> `run_app`\n\
1764. Inside `run_agent_loop`, Hematite constructs `ConversationManager`, emits `InferenceEvent::RuntimeProfile`, initializes MCP and Vein, emits startup `InferenceEvent::Thought` / `InferenceEvent::Done`, then sends the boot greeting as `InferenceEvent::MutedToken`.\n\
177 File refs: `src/runtime.rs` -> `run_agent_loop`; `src/agent/conversation.rs` -> `ConversationManager::new`, `initialize_mcp`, `initialize_vein`\n\n\
178Possible weak points\n\
179- Startup depends on LM Studio being available before the TUI fully launches.\n\
180- `run_agent_loop` still mixes boot diagnostics and steady-state turn handling in one task, even though runtime assembly is cleaner now."
181 .to_string()
182}
183
184fn trace_voice() -> String {
185 "Verified voice synthesis flow\n\n\
186Ctrl+T toggle\n\
1871. `run_app` in `src/ui/tui.rs` handles `KeyCode::Char('t') if key.modifiers.contains(event::KeyModifiers::CONTROL)`. \
188 It calls `app.voice_manager.toggle()` and pushes a System message showing the new state.\n\
189 File refs: `src/ui/tui.rs` -> `run_app`, `KeyCode::Char('t')`, `KeyModifiers::CONTROL`\n\
1902. `VoiceManager::toggle()` flips the internal `enabled` AtomicBool and returns the new state.\n\
191 File refs: `src/ui/voice.rs` -> `VoiceManager::toggle`\n\n\
192Speech pipeline\n\
1931. `build_runtime_bundle` constructs `VoiceManager::new(agent_tx.clone())` so the voice subsystem \
194 can emit `InferenceEvent::VoiceStatus` back on the agent channel.\n\
195 File refs: `src/runtime.rs` -> `build_runtime_bundle`; `src/ui/voice.rs` -> `VoiceManager::new`\n\
1962. Inside `run_app`, only `InferenceEvent::Token` triggers speech (not `MutedToken`). \
197 When enabled and not muted, the TUI calls `app.voice_manager.speak(token.clone())`.\n\
198 File refs: `src/ui/tui.rs` -> `run_app`, `InferenceEvent::Token`\n\
1993. `VoiceManager::speak` pushes text into an internal sync channel. Background threads assemble \
200 sentence chunks, synthesize via `TTSKoko::tts_raw_audio_streaming`, and append PCM into a `rodio::Sink`.\n\
201 File refs: `src/ui/voice.rs` -> `VoiceManager::speak`, `TTSKoko::tts_raw_audio_streaming`\n\
2024. When the turn ends, `run_app` calls `app.voice_manager.flush()` to drain any remaining audio.\n\
203 File refs: `src/ui/tui.rs` -> `InferenceEvent::Done`; `src/ui/voice.rs` -> `VoiceManager::flush`\n\n\
204Possible weak points\n\
205- The Ctrl+T handler uses crossterm's `KeyCode::Char('t')` + `KeyModifiers::CONTROL` — not a string like 'Ctrl+T'. \
206 Searching for 'Ctrl.*T' or 'toggle_voice' will find nothing; search for `KeyCode::Char.*'t'` instead.\n\
207- Voice state is an AtomicBool inside `VoiceManager`; there is no `voice_enabled` field on `App`."
208 .to_string()
209}