Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/compaction_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ impl crate::Agent {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/curate_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl crate::Agent {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/explore_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ impl crate::Agent {
max_tokens_explicit: self.config.max_tokens_explicit,
temperature: self.config.temperature,
thinking_budget: self.config.thinking_budget,
reasoning_effort: self.config.reasoning_effort,
compat: self.config.compat,
context_window: self.config.context_window,
project_context: self.config.project_context.clone(),
Expand Down
24 changes: 24 additions & 0 deletions crates/hi-agent/src/agent/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,30 @@ impl crate::Agent {
self.config.verify = stages;
}

/// The reasoning effort applied to main-turn requests (`None` = off, i.e. no
/// `reasoning_effort` sent and the endpoint's own default is used).
pub fn reasoning_effort(&self) -> Option<hi_ai::ReasoningEffort> {
self.config.reasoning_effort
}

/// Set (or clear, with `None`) the reasoning effort for subsequent turns.
/// Applies to main-turn requests on OpenAI-compatible endpoints that accept
/// `reasoning_effort`; the Anthropic adapter and non-supporting endpoints
/// ignore it. Safe to call between turns (like the other `/`-command setters).
pub fn set_reasoning_effort(&mut self, effort: Option<hi_ai::ReasoningEffort>) {
self.config.reasoning_effort = effort;
}

/// The sampling temperature applied to requests (`None` = provider default).
pub fn temperature(&self) -> Option<f32> {
self.config.temperature
}

/// Set (or clear, with `None`) the sampling temperature for subsequent turns.
pub fn set_temperature(&mut self, temperature: Option<f32>) {
self.config.temperature = temperature;
}

pub(crate) fn persist(&mut self) -> Result<()> {
if let Some(session) = self.session.as_mut() {
// Clamp the cursor: transcript-shrinking ops (`strip_trailing_nudges`,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/memory_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl crate::Agent {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/plan_goal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl crate::Agent {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-agent/src/agent/skeptic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ impl crate::Agent {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
2 changes: 2 additions & 0 deletions crates/hi-agent/src/agent/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ impl crate::Agent {
top_p,
frequency_penalty,
thinking_budget: self.config.thinking_budget,
reasoning_effort: self.config.reasoning_effort,
profile: RequestProfile {
compat: self.config.compat,
tool_mode,
Expand Down Expand Up @@ -3218,6 +3219,7 @@ If the task is already complete, stop and give your final recap."
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: self.config.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down
152 changes: 152 additions & 0 deletions crates/hi-agent/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Slash-command parsing, shared by every frontend.

use hi_ai::ReasoningEffort;

/// A recognized in-session command. Frontends decide how to act on each.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Command {
Expand All @@ -8,6 +10,10 @@ pub enum Command {
Clear,
/// Set the model for subsequent turns (empty = report current).
Model(String),
/// Show or set per-session request config live: `reasoning <level|off>`
/// (OpenAI-compatible `reasoning_effort`) and `temp <value|off>` (sampling
/// temperature). Empty arg reports the current values.
Config(String),
/// Run exactly one turn through the conservative MoA virtual route.
Moa(String),
/// Use a provider/profile for subsequent turns (empty = report current).
Expand Down Expand Up @@ -100,6 +106,7 @@ pub fn parse(line: &str) -> Option<Command> {
"help" | "h" | "?" => Command::Help,
"clear" | "new" => Command::Clear,
"model" | "m" => Command::Model(arg),
"config" | "cfg" | "set" => Command::Config(arg),
"moa" => Command::Moa(arg),
"provider" | "prov" => Command::Provider(arg),
"usage" | "cost" => Command::Removed("usage — removed; use /status".into()),
Expand Down Expand Up @@ -503,6 +510,73 @@ pub fn parse_goal_team(arg: &str) -> Option<GoalTeamArg> {
})
}

/// The parsed form of a `/config` argument.
#[derive(Clone, Debug, PartialEq)]
pub enum ConfigArg {
/// `/config` — report the current reasoning effort and temperature.
Show,
/// `/config reasoning <level|off>` — set the reasoning effort (`None` = off,
/// i.e. send no `reasoning_effort` and take the endpoint default).
Reasoning(Option<ReasoningEffort>),
/// `/config temp <value|off>` — set the sampling temperature (`None` clears
/// it, leaving the provider default).
Temperature(Option<f32>),
/// Unrecognized option or bad value; carries a usage/error hint.
Invalid(String),
}

/// Parse a `/config` argument into a [`ConfigArg`]. Shared by every frontend so
/// the plain REPL and the TUI accept exactly the same syntax.
pub fn parse_config_arg(arg: &str) -> ConfigArg {
let a = arg.trim();
if a.is_empty() {
return ConfigArg::Show;
}
let (key, val) = match a.split_once(char::is_whitespace) {
Some((k, v)) => (k, v.trim()),
None => (a, ""),
};
let off = |v: &str| matches!(v.to_ascii_lowercase().as_str(), "off" | "none" | "clear");
match key.to_ascii_lowercase().as_str() {
"reasoning" | "reasoning-effort" | "reason" | "effort" | "r" => {
if val.is_empty() {
ConfigArg::Invalid(
"usage: /config reasoning <minimal|low|medium|high|xhigh|off>".into(),
)
} else if off(val) || val.eq_ignore_ascii_case("disable") {
ConfigArg::Reasoning(None)
} else {
match ReasoningEffort::from_arg(val) {
Some(e) => ConfigArg::Reasoning(Some(e)),
None => ConfigArg::Invalid(format!(
"unknown reasoning level '{val}' — use minimal, low, medium, high, xhigh, or off"
)),
}
}
}
"temp" | "temperature" | "t" => {
if val.is_empty() {
ConfigArg::Invalid("usage: /config temp <0.0-2.0|off>".into())
} else if off(val) || val.eq_ignore_ascii_case("default") {
ConfigArg::Temperature(None)
} else {
match val.parse::<f32>() {
Ok(t) if (0.0..=2.0).contains(&t) => ConfigArg::Temperature(Some(t)),
Ok(_) => ConfigArg::Invalid(format!(
"temperature '{val}' out of range — use 0.0 to 2.0, or off"
)),
Err(_) => ConfigArg::Invalid(format!(
"bad temperature '{val}' — use a number from 0.0 to 2.0, or off"
)),
}
}
}
other => ConfigArg::Invalid(format!(
"unknown /config option '{other}' — try: reasoning <level>, temp <value>"
)),
}
}

fn read_only_macro_prompt(kind: &str, topic: &str) -> String {
let topic = topic.trim();
let topic = if topic.is_empty() {
Expand Down Expand Up @@ -580,6 +654,18 @@ pub const COMMANDS: &[CommandSpec] = &[
help: "show or set the model (no id opens the selector)",
arg_values: &[],
},
CommandSpec {
name: "config",
args: "[reasoning <level>|temp <value>]",
help: "show or set reasoning effort and sampling temperature for this session",
arg_values: &[
(
"reasoning",
"set reasoning_effort: minimal|low|medium|high|xhigh|off",
),
("temp", "set sampling temperature: 0.0-2.0, or off"),
],
},
CommandSpec {
name: "moa",
args: "<prompt>",
Expand Down Expand Up @@ -1339,6 +1425,72 @@ mod tests {
assert_eq!(parse_goal_limit("limitless"), None);
}

#[test]
fn config_arg_parsing() {
use super::{ConfigArg, parse_config_arg};
use hi_ai::ReasoningEffort;
// Empty → show.
assert_eq!(parse_config_arg(""), ConfigArg::Show);
assert_eq!(parse_config_arg(" "), ConfigArg::Show);
// Reasoning levels + aliases.
assert_eq!(
parse_config_arg("reasoning high"),
ConfigArg::Reasoning(Some(ReasoningEffort::High))
);
assert_eq!(
parse_config_arg("effort MEDIUM"),
ConfigArg::Reasoning(Some(ReasoningEffort::Medium))
);
assert_eq!(
parse_config_arg("r xhigh"),
ConfigArg::Reasoning(Some(ReasoningEffort::Xhigh))
);
// Off spellings clear it.
assert_eq!(parse_config_arg("reasoning off"), ConfigArg::Reasoning(None));
assert_eq!(
parse_config_arg("reasoning none"),
ConfigArg::Reasoning(None)
);
// Bad level / missing value.
assert!(matches!(
parse_config_arg("reasoning turbo"),
ConfigArg::Invalid(_)
));
assert!(matches!(
parse_config_arg("reasoning"),
ConfigArg::Invalid(_)
));
// Temperature: in range, off, out of range, non-numeric.
assert_eq!(
parse_config_arg("temp 0.7"),
ConfigArg::Temperature(Some(0.7))
);
assert_eq!(parse_config_arg("temperature 0"), ConfigArg::Temperature(Some(0.0)));
assert_eq!(parse_config_arg("temp off"), ConfigArg::Temperature(None));
assert_eq!(
parse_config_arg("temp default"),
ConfigArg::Temperature(None)
);
assert!(matches!(parse_config_arg("temp 5"), ConfigArg::Invalid(_)));
assert!(matches!(parse_config_arg("temp hot"), ConfigArg::Invalid(_)));
// Unknown option.
assert!(matches!(parse_config_arg("bogus x"), ConfigArg::Invalid(_)));
// Command parse wiring + aliases.
assert_eq!(parse("/config"), Some(Command::Config(String::new())));
assert_eq!(
parse("/config reasoning high"),
Some(Command::Config("reasoning high".into()))
);
assert_eq!(
parse("/cfg temp 0.5"),
Some(Command::Config("temp 0.5".into()))
);
assert_eq!(
parse("/set reasoning off"),
Some(Command::Config("reasoning off".into()))
);
}

#[test]
fn goal_team_subcommand_parsing() {
assert_eq!(parse_goal_team("team on"), Some(GoalTeamArg::On));
Expand Down
9 changes: 8 additions & 1 deletion crates/hi-agent/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Per-session agent configuration and the layered-verification stage type.

use hi_ai::{CompatMode, ToolMode};
use hi_ai::{CompatMode, ReasoningEffort, ToolMode};

use crate::compaction::{CompactionKind, DEFAULT_KEEP_RECENT};
use crate::{
Expand Down Expand Up @@ -52,6 +52,12 @@ pub struct AgentConfig {
pub max_tokens_explicit: bool,
pub temperature: Option<f32>,
pub thinking_budget: Option<u32>,
/// Abstract reasoning level (`reasoning_effort`) applied to every main-turn
/// request on OpenAI-compatible endpoints that support it; `None` leaves the
/// endpoint default. See [`hi_ai::ReasoningEffort`]. Housekeeping calls
/// (compaction/memory/recap) deliberately leave this off. Set via
/// `--reasoning-effort`, a profile, or `/config reasoning <level>`.
pub reasoning_effort: Option<ReasoningEffort>,
pub tool_mode: ToolMode,
pub compat: CompatMode,
/// Model context window, when known — used to show how full it is.
Expand Down Expand Up @@ -191,6 +197,7 @@ impl Default for AgentConfig {
max_tokens_explicit: false,
temperature: None,
thinking_budget: None,
reasoning_effort: None,
tool_mode: ToolMode::Auto,
compat: CompatMode::Auto,
context_window: None,
Expand Down
1 change: 1 addition & 0 deletions crates/hi-ai/src/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ mod tests {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile: Default::default(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/hi-ai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub use provider::{
};
pub use types::{
ChatRequest, CompatMode, Completion, Content, Message, RateLimitBucket, RateLimitState,
RequestProfile, Role, StreamEvent, ToolCall, ToolMode, ToolSpec, Usage,
ReasoningEffort, RequestProfile, Role, StreamEvent, ToolCall, ToolMode, ToolSpec, Usage,
estimate_completion_output_tokens, estimate_content_tokens, estimate_messages_tokens,
estimate_text_tokens,
};
2 changes: 2 additions & 0 deletions crates/hi-ai/src/moa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ fn reference_request(
top_p: request.top_p,
frequency_penalty: request.frequency_penalty,
thinking_budget: None,
reasoning_effort: None,
profile: RequestProfile {
compat: request.profile.compat,
tool_mode: ToolMode::ChatOnly,
Expand Down Expand Up @@ -538,6 +539,7 @@ mod tests {
top_p: None,
frequency_penalty: None,
thinking_budget: Some(1024),
reasoning_effort: None,
profile: RequestProfile::default(),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/hi-ai/src/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ mod tests {
top_p: None,
frequency_penalty: None,
thinking_budget: None,
reasoning_effort: None,
profile,
}
}
Expand Down
Loading
Loading