-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(engine): inject mode-change runtime message and include mode in turn metadata #2577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a8cfbbf
21721a4
dfb0b17
29c745c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -375,6 +375,8 @@ pub struct Engine { | |
| /// This keeps prompt refreshes cheap while still noticing append/update | ||
| /// writes from slop ledger tools during the same session. | ||
| slop_ledger_gate_cache: Option<(Option<SystemTime>, Option<String>)>, | ||
| /// Current operating mode. Updated on `ChangeMode` and `SendMessage`. | ||
| current_mode: AppMode, | ||
| } | ||
|
|
||
| // === Internal tool helpers === | ||
|
|
@@ -621,6 +623,7 @@ impl Engine { | |
| slop_ledger_gate_cache: None, | ||
| workshop_vars, | ||
| sandbox_backend, | ||
| current_mode: AppMode::Agent, | ||
| }; | ||
| engine.rehydrate_latest_canonical_state(); | ||
|
|
||
|
|
@@ -1046,11 +1049,23 @@ impl Engine { | |
| let _ = self.tx_event.send(Event::AgentList { agents }).await; | ||
| } | ||
| Op::ChangeMode { mode } => { | ||
| let previous_mode = self.current_mode; | ||
| self.current_mode = mode; | ||
| self.refresh_system_prompt(mode); | ||
| self.emit_session_updated().await; | ||
| // Notify the agent that the mode has changed so it can re-evaluate | ||
| // any operations that were blocked by the previous mode's policy. | ||
| if previous_mode != mode { | ||
| let msg = Self::mode_change_runtime_message(previous_mode, mode); | ||
| self.session.add_message(msg); | ||
|
Comment on lines
+1059
to
+1060
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user changes mode after an assistant response and then closes/reloads the session before sending another prompt, this appends a Useful? React with 👍 / 👎. |
||
| self.emit_session_updated().await; | ||
| } | ||
| let _ = self | ||
| .tx_event | ||
| .send(Event::status(format!("Mode changed to: {mode:?}"))) | ||
| .send(Event::status(format!( | ||
| "Mode changed to: {}", | ||
| mode.description() | ||
| ))) | ||
| .await; | ||
| } | ||
| Op::SetModel { model, mode } => { | ||
|
|
@@ -1190,6 +1205,47 @@ impl Engine { | |
| .await; | ||
| } | ||
|
|
||
| /// Build a runtime event message notifying the agent that the operating mode has changed. | ||
| /// This lets the agent re-evaluate any operations that were blocked by the previous mode. | ||
| fn mode_change_runtime_message(previous_mode: AppMode, new_mode: AppMode) -> Message { | ||
| let (policy_note, re_eval_hint) = match new_mode { | ||
| AppMode::Yolo => ( | ||
| "all operations run automatically without requiring user approval", | ||
| "Re-evaluate any previously blocked write, patch, or shell operations \ | ||
| — they are now auto-approved.", | ||
| ), | ||
| AppMode::Agent => ( | ||
| "read-only operations run silently; writes, patches, and shell \ | ||
| commands require user approval", | ||
| "Any operations you ran automatically under YOLO mode now require \ | ||
| explicit user approval before executing.", | ||
| ), | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| AppMode::Plan => ( | ||
| "all writes and patches are blocked; shell and code execution are unavailable", | ||
| "Any previously planned operations that require writes or shell access \ | ||
| must wait until the mode changes back to Agent or YOLO.", | ||
| ), | ||
| }; | ||
| Message { | ||
| role: "user".to_string(), | ||
| content: vec![ContentBlock::Text { | ||
| text: format!( | ||
| "<codewhale:runtime_event kind=\"mode_change\" visibility=\"internal\">\n\ | ||
| This is an internal runtime event, not user input. The operating mode has changed \ | ||
| from {previous} mode to {new} mode.\n\n\ | ||
| In {new} mode: {policy}\n\n\ | ||
| {re_eval}\n\ | ||
| </codewhale:runtime_event>", | ||
| previous = previous_mode.description(), | ||
| new = new_mode.description(), | ||
| policy = policy_note, | ||
| re_eval = re_eval_hint, | ||
| ), | ||
| cache_control: None, | ||
| }], | ||
| } | ||
| } | ||
|
|
||
| async fn add_session_message(&mut self, message: Message) { | ||
| self.session.add_message(message); | ||
| self.emit_session_updated().await; | ||
|
|
@@ -1198,11 +1254,13 @@ impl Engine { | |
| fn turn_metadata_block( | ||
| &self, | ||
| routed_model: &str, | ||
| mode: AppMode, | ||
| auto_model: bool, | ||
| reasoning_effort: Option<&str>, | ||
| reasoning_effort_auto: bool, | ||
| ) -> ContentBlock { | ||
| let today = chrono::Local::now().format("%Y-%m-%d").to_string(); | ||
| let mode_label = mode.description(); | ||
| let working_set_summary = self | ||
| .session | ||
| .working_set | ||
|
|
@@ -1212,6 +1270,7 @@ impl Engine { | |
|
|
||
| let mut lines = vec![ | ||
| format!("Current local date: {today}"), | ||
| format!("Current mode: {mode_label}"), | ||
| format!("Current model: {routed_model}"), | ||
| ]; | ||
| if auto_model { | ||
|
|
@@ -1234,6 +1293,7 @@ impl Engine { | |
| fn user_text_message_with_turn_metadata(&self, text: String) -> Message { | ||
| self.user_text_message_with_turn_metadata_for_route( | ||
| text, | ||
| self.current_mode, | ||
| &self.session.model, | ||
|
Comment on lines
1293
to
1297
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For mode changes made while a turn is still running, Useful? React with 👍 / 👎. |
||
| self.session.auto_model, | ||
| self.session.reasoning_effort.as_deref(), | ||
|
|
@@ -1244,6 +1304,7 @@ impl Engine { | |
| fn user_text_message_with_turn_metadata_for_route( | ||
| &self, | ||
| text: String, | ||
| mode: AppMode, | ||
| routed_model: &str, | ||
| auto_model: bool, | ||
| reasoning_effort: Option<&str>, | ||
|
|
@@ -1254,6 +1315,7 @@ impl Engine { | |
| content: vec![ | ||
| self.turn_metadata_block( | ||
| routed_model, | ||
| mode, | ||
| auto_model, | ||
| reasoning_effort, | ||
| reasoning_effort_auto, | ||
|
|
@@ -1289,6 +1351,9 @@ impl Engine { | |
| // Reset cancel token for fresh turn (in case previous was cancelled) | ||
| self.reset_cancel_token(); | ||
|
|
||
| // Track current mode so mid-turn messages include the right mode in turn metadata. | ||
| self.current_mode = mode; | ||
|
|
||
| // Drain stale steer messages from previous turns. | ||
| while self.rx_steer.try_recv().is_ok() {} | ||
|
|
||
|
|
@@ -1368,6 +1433,7 @@ impl Engine { | |
| // Add user message to session | ||
| let user_msg = self.user_text_message_with_turn_metadata_for_route( | ||
| content, | ||
| mode, | ||
| &model, | ||
| auto_model, | ||
| reasoning_effort.as_deref(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -314,7 +314,9 @@ impl Engine { | |
| // Three-zone prefix contract (#2264): freeze baseline on first | ||
| // turn, verify against it on subsequent turns. Operates alongside | ||
| // PrefixStabilityManager as an independent diagnostic layer. | ||
| // Phase 2: warn-only, auto-re-freeze on drift. | ||
| // Phase 3: emit a one-shot 'frozen' event on first turn. | ||
| // Drift is logged (tracing::debug!) but not re-emitted — | ||
| // PrefixStabilityManager already reports the change above. | ||
| let system_text = | ||
| crate::prefix_cache::system_prompt_text(self.session.system_prompt.as_ref()); | ||
| let current_tools: &[crate::models::Tool] = active_tools.as_deref().unwrap_or_default(); | ||
|
|
@@ -338,7 +340,19 @@ impl Engine { | |
| self.session.system_prompt.as_ref(), | ||
| current_tools.to_vec(), | ||
| ); | ||
| self.session.frozen_prefix = Some(pinned.freeze()); | ||
| let frozen = pinned.freeze(); | ||
| let _ = self | ||
| .tx_event | ||
| .send(Event::PrefixCacheChange { | ||
| description: format!("frozen: {}", frozen.short_id()), | ||
| system_prompt_changed: false, | ||
| tools_changed: false, | ||
| stability_pct: 100, | ||
|
Comment on lines
+346
to
+350
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the first request, the prefix-stability block just above this already sends a Useful? React with 👍 / 👎. |
||
| changed: false, | ||
| pinned_combined_hash: frozen.hash().to_string(), | ||
| }) | ||
| .await; | ||
| self.session.frozen_prefix = Some(frozen); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
current_modeis hardcoded toAppMode::Agentat construction time, regardless of the mode the host app was actually configured with. If the app starts in YOLO or Plan mode and anOp::ChangeModearrives before the firstOp::SendMessage(e.g. to sync initial UI state),previous_modewill wrongly beAgent, a spurious "Agent → X" runtime message will be injected into the session, and the conversation history will look like the user explicitly switched modes when they never did. Accepting the initial mode fromEngineConfig(or from the firstSendMessage) would prevent this.