1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//! Session-local operating-mode selection for `/mode auto`.
//!
//! The core loop receives this narrow collaborator only when the human has
//! selected `auto`, and the model may use it to request one of the bounded
//! working styles for a future turn. The collaborator cannot change the
//! current turn's disposition or caveats, and the schema deliberately excludes
//! `full-auto`.
//!
//! The line above used to read "the core loop knows nothing about the TUI's
//! concrete mode enum", and the schema below carried its own hand-written copy
//! of the mode list as a result. The enum moved down in #2009 PR4b, so the
//! schema is now generated from `OperatingMode::model_selectable()` — one
//! vocabulary, and a mode added to the enum cannot be silently missing here.
/// Model-facing seam behind `select_operating_mode`.
///
/// Implementations own the session state and validate the requested name.
/// Selection applies to a later turn; callers must never reinterpret the
/// result as authority for the tool round already in flight.
pub trait OperatingModeControl: Send + Sync {
/// Select a bounded working style for a future turn.
fn select_operating_mode(&self, mode: &str) -> Result<String, String>;
}
/// Tool definition advertised only when an [`OperatingModeControl`] is
/// injected (the interactive session is configured with `/mode auto`).
pub fn select_operating_mode_tool_definition() -> serde_json::Value {
// #2051: the sentence about what a selection does to the turn in flight
// is disposition vocabulary, owned by `disposition_voice`. It used to say
// "never changes the authority or disposition of the current turn" — the
// mechanism by name, plus a reminder that the model has no move.
let scope = super::DispositionVoices::default().next_turn_scope;
serde_json::json!({
"type": "function",
"function": {
"name": "select_operating_mode",
"description": format!(
"In /mode auto, select the working style for the next action-shaped turn. \
{scope} Choose chat, dev, admin, plan, or diagnose; full-auto is human-only."
),
"parameters": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": crate::operating_mode::OperatingMode::model_selectable(),
"description": "The bounded working style to use next."
}
},
"required": ["mode"],
"additionalProperties": false
}
}
})
}
#[cfg(test)]
mod tests {
use super::select_operating_mode_tool_definition;
#[test]
fn schema_excludes_human_only_modes() {
let definition = select_operating_mode_tool_definition();
let modes = definition["function"]["parameters"]["properties"]["mode"]["enum"]
.as_array()
.unwrap();
assert!(modes.iter().any(|mode| mode == "dev"));
assert!(!modes.iter().any(|mode| mode == "auto"));
assert!(!modes.iter().any(|mode| mode == "full-auto"));
// ...and it is the enum's own list, not a copy that happens to agree
// today. A style added to `OperatingMode` now appears here or fails.
let want = crate::operating_mode::OperatingMode::model_selectable();
let got: Vec<&str> = modes.iter().filter_map(|m| m.as_str()).collect();
assert_eq!(got, want, "the schema drifted from the mode vocabulary");
assert!(
!want.is_empty(),
"an empty list would make every assertion above vacuous"
);
}
}