Skip to content

Commit ccccf83

Browse files
authored
feat: add /btw, /fast, /insights, /steer, /queue commands and auxiliary model config (#926)
1 parent 33204d9 commit ccccf83

45 files changed

Lines changed: 2873 additions & 64 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agents/src/runner/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ pub use {
2222
tool_result::{ExtractedImage, sanitize_tool_result, tool_result_to_content},
2323
};
2424

25+
/// Shared inbox for mid-flight steering text (populated by `/steer` command).
26+
///
27+
/// The agent loop drains this between iterations and injects the text as a
28+
/// system notice so the LLM sees the guidance on its next call.
29+
pub type SteerInbox = std::sync::Arc<tokio::sync::Mutex<Vec<String>>>;
30+
2531
// Re-export helpers at the module level so that sibling submodules
2632
// (`non_streaming`, `streaming`) can continue to import via `super::item_name`.
2733
pub(crate) use helpers::{

crates/agents/src/runner/streaming.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub async fn run_agent_loop_streaming(
6161
tool_context: Option<serde_json::Value>,
6262
hook_registry: Option<Arc<HookRegistry>>,
6363
sender_name: Option<String>,
64+
steer_inbox: Option<super::SteerInbox>,
6465
) -> Result<AgentRunResult, AgentRunError> {
6566
let native_tools = provider.supports_tools();
6667
let config = moltis_config::discover_and_load();
@@ -866,5 +867,19 @@ pub async fn run_agent_loop_streaming(
866867
&mut strip_tools_next_iter,
867868
on_event,
868869
);
870+
871+
// Drain any pending /steer text and inject as a system note.
872+
// Uses system role to avoid consecutive-user-message violations
873+
// with strict providers that enforce role alternation.
874+
if let Some(ref inbox) = steer_inbox {
875+
let mut guard = inbox.lock().await;
876+
if !guard.is_empty() {
877+
let combined = guard.drain(..).collect::<Vec<_>>().join("\n");
878+
debug!(steer_text = %combined, "injecting /steer guidance");
879+
messages.push(ChatMessage::system(format!(
880+
"[Steering note from the user — adjust your approach accordingly]: {combined}"
881+
)));
882+
}
883+
}
869884
}
870885
}

crates/agents/src/runner/tests/basic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ async fn test_streaming_runner_preserves_cache_usage() {
198198
None,
199199
None,
200200
None,
201+
None,
201202
)
202203
.await
203204
.unwrap();

crates/channels/src/commands.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ pub fn all_commands() -> &'static [CommandDef] {
3434
name: "attach",
3535
description: "Attach an existing session here",
3636
},
37+
CommandDef {
38+
name: "fork",
39+
description: "Fork this session into a new branch",
40+
},
3741
CommandDef {
3842
name: "clear",
3943
description: "Clear session history",
@@ -91,6 +95,31 @@ pub fn all_commands() -> &'static [CommandDef] {
9195
name: "update",
9296
description: "Update moltis to latest or specified version",
9397
},
98+
CommandDef {
99+
name: "rollback",
100+
description: "List or restore file checkpoints",
101+
},
102+
// Quick actions
103+
CommandDef {
104+
name: "btw",
105+
description: "Quick side question (no tools, not persisted)",
106+
},
107+
CommandDef {
108+
name: "fast",
109+
description: "Toggle fast/priority mode",
110+
},
111+
CommandDef {
112+
name: "insights",
113+
description: "Show session analytics and usage stats",
114+
},
115+
CommandDef {
116+
name: "steer",
117+
description: "Inject guidance into the current agent run",
118+
},
119+
CommandDef {
120+
name: "queue",
121+
description: "Queue a message for the next agent turn",
122+
},
94123
// Meta
95124
CommandDef {
96125
name: "help",
@@ -189,6 +218,7 @@ mod tests {
189218
let names: Vec<&str> = all_commands().iter().map(|c| c.name).collect();
190219
for expected in [
191220
"new",
221+
"fork",
192222
"clear",
193223
"compact",
194224
"context",
@@ -205,6 +235,12 @@ mod tests {
205235
"stop",
206236
"peek",
207237
"update",
238+
"rollback",
239+
"btw",
240+
"fast",
241+
"insights",
242+
"steer",
243+
"queue",
208244
"help",
209245
] {
210246
assert!(

crates/chat/src/compaction_run/structured.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ fn warn_if_unused_auxiliary_model_config(config: &CompactionConfig) {
159159
tracing::warn!(
160160
summary_model = ?config.summary_model,
161161
max_summary_tokens = config.max_summary_tokens,
162-
"chat.compact: chat.compaction.summary_model / max_summary_tokens are reserved \
163-
for the auxiliary-model subsystem (beads issue moltis-8me) and have no effect \
164-
on the structured strategy yet — the session's primary provider will be used"
162+
"chat.compact: chat.compaction.summary_model / max_summary_tokens are not wired \
163+
into the structured strategy yet — the session's primary provider will be used. \
164+
Use `[auxiliary] compaction = \"model-id\"` for auxiliary model routing once wired"
165165
);
166166
}
167167

crates/chat/src/run_with_tools.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,23 @@ pub(crate) async fn run_with_tools(
847847
runtime_context,
848848
);
849849

850+
// Create a shared steer inbox that the gateway can push steering text into.
851+
// A background task polls the ChatRuntime and forwards any `/steer` text.
852+
let steer_inbox: moltis_agents::runner::SteerInbox = Arc::new(Mutex::new(Vec::new()));
853+
let steer_inbox_writer = steer_inbox.clone();
854+
let steer_state = state.clone();
855+
let steer_session_key = session_key.to_string();
856+
let steer_task = tokio::spawn(async move {
857+
// Drain any stale steering text left over from a previous run.
858+
let _ = steer_state.take_steer_text(&steer_session_key).await;
859+
loop {
860+
tokio::time::sleep(Duration::from_millis(500)).await;
861+
if let Some(texts) = steer_state.take_steer_text(&steer_session_key).await {
862+
steer_inbox_writer.lock().await.extend(texts);
863+
}
864+
}
865+
});
866+
850867
let provider_ref = provider.clone();
851868
let first_result = run_agent_loop_streaming(
852869
provider,
@@ -858,6 +875,7 @@ pub(crate) async fn run_with_tools(
858875
Some(tool_context.clone()),
859876
hook_registry.clone(),
860877
sender_name.clone(),
878+
Some(steer_inbox.clone()),
861879
)
862880
.await;
863881

@@ -956,6 +974,7 @@ pub(crate) async fn run_with_tools(
956974
Some(tool_context),
957975
hook_registry,
958976
sender_name,
977+
Some(steer_inbox.clone()),
959978
)
960979
.await
961980
},
@@ -981,6 +1000,7 @@ pub(crate) async fn run_with_tools(
9811000
},
9821001
other => other,
9831002
};
1003+
steer_task.abort();
9841004

9851005
// Ensure all runner events (including deltas) are broadcast in order before
9861006
// emitting terminal final/error frames.

crates/chat/src/runtime.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,16 @@ pub trait ChatRuntime: Send + Sync {
158158

159159
/// List currently connected remote nodes.
160160
async fn connected_nodes(&self) -> Vec<ConnectedNodeSummary>;
161+
162+
// ── Mid-flight steering ──────────────────────────────────────────────
163+
164+
/// Take (drain) all pending `/steer` texts for a session.
165+
async fn take_steer_text(&self, _session_key: &str) -> Option<Vec<String>> {
166+
None
167+
}
168+
169+
/// Check whether fast/priority mode is enabled for a session.
170+
async fn is_fast_mode(&self, _session_key: &str) -> bool {
171+
false
172+
}
161173
}

crates/config/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ pub struct MoltisConfig {
255255
pub caldav: CalDavConfig,
256256
pub home_assistant: HomeAssistantConfig,
257257
pub webhooks: WebhooksConfig,
258+
/// Auxiliary model assignments for side tasks (compaction, titles, vision).
259+
pub auxiliary: AuxiliaryModelsConfig,
258260
/// Code-index configuration for codebase search tools.
259261
pub code_index: CodeIndexTomlConfig,
260262
/// Per-model overrides that apply across all providers.

crates/config/src/schema/chat.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,26 @@ pub enum ToolRegistryMode {
297297
/// Only `tool_search` is sent; the model discovers and activates tools on demand.
298298
Lazy,
299299
}
300+
301+
/// Auxiliary model assignments for side tasks.
302+
///
303+
/// Route compression, title generation, and vision to cheaper/faster models
304+
/// while keeping the main session on a more capable model. Falls back to the
305+
/// session's primary provider when a field is `None`.
306+
///
307+
/// ```toml
308+
/// [auxiliary]
309+
/// compaction = "openrouter/google/gemini-2.5-flash"
310+
/// title_generation = "openrouter/google/gemini-2.5-flash"
311+
/// ```
312+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
313+
#[serde(default)]
314+
pub struct AuxiliaryModelsConfig {
315+
/// Model for context compaction/summarization.
316+
/// Overrides `chat.compaction.summary_model` when set.
317+
pub compaction: Option<String>,
318+
/// Model for session title generation.
319+
pub title_generation: Option<String>,
320+
/// Model for vision/image analysis tasks.
321+
pub vision: Option<String>,
322+
}

0 commit comments

Comments
 (0)