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
13 changes: 12 additions & 1 deletion crates/sprout-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ pub struct CliArgs {
value_parser = clap::value_parser!(u32).range(0..=100))]
pub context_message_limit: u32,

/// Maximum turns per session before proactive rotation. 0 = disabled
/// (rotate only on MaxTokens / MaxTurnRequests).
#[arg(long, env = "SPROUT_ACP_MAX_TURNS_PER_SESSION", default_value_t = 0,
value_parser = clap::value_parser!(u32))]
pub max_turns_per_session: u32,

/// Disable automatic presence (online/offline) status.
#[arg(long, env = "SPROUT_ACP_NO_PRESENCE")]
pub no_presence: bool,
Expand Down Expand Up @@ -234,6 +240,8 @@ pub struct Config {
pub no_mention_filter: bool,
pub config_path: PathBuf,
pub context_message_limit: u32,
/// Maximum turns per session before proactive rotation. 0 = disabled.
pub max_turns_per_session: u32,
pub presence_enabled: bool,
pub typing_enabled: bool,
/// Desired LLM model ID. Applied after every `session_new_full()`.
Expand Down Expand Up @@ -373,6 +381,7 @@ impl Config {
no_mention_filter: args.no_mention_filter,
config_path: args.config,
context_message_limit: args.context_message_limit,
max_turns_per_session: args.max_turns_per_session,
presence_enabled: !args.no_presence,
typing_enabled: !args.no_typing,
model: args.model,
Expand All @@ -382,7 +391,7 @@ impl Config {
/// Human-readable summary (no secrets).
pub fn summary(&self) -> String {
format!(
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} timeout={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} ignore_self={} context_limit={} presence={} typing={} model={}",
"relay={} pubkey={} agent_cmd={} {} mcp_cmd={} timeout={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} model={}",
self.relay_url,
self.keys.public_key().to_hex(),
self.agent_command,
Expand All @@ -395,6 +404,7 @@ impl Config {
self.dedup_mode,
self.ignore_self,
self.context_message_limit,
self.max_turns_per_session,
self.presence_enabled,
self.typing_enabled,
self.model.as_deref().unwrap_or("(agent default)"),
Expand Down Expand Up @@ -692,6 +702,7 @@ mod tests {
no_mention_filter: false,
config_path: PathBuf::from("./sprout-acp.toml"),
context_message_limit: 12,
max_turns_per_session: 0,
presence_enabled: true,
typing_enabled: true,
model: None,
Expand Down
21 changes: 9 additions & 12 deletions crates/sprout-acp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use config::{Config, DedupMode, ModelsArgs, SubscribeMode};
use filter::SubscriptionRule;
use futures_util::FutureExt;
use nostr::ToBech32;
use pool::{AgentPool, OwnedAgent, PromptContext, PromptOutcome, PromptResult, PromptSource};
use pool::{
AgentPool, OwnedAgent, PromptContext, PromptOutcome, PromptResult, PromptSource, SessionState,
};
use queue::{EventQueue, QueuedEvent};
use relay::HarnessRelay;
use sprout_core::kind::{
Expand Down Expand Up @@ -82,8 +84,7 @@ async fn main() -> Result<()> {
agents.push(OwnedAgent {
index: i,
acp,
sessions: HashMap::new(),
heartbeat_session: None,
state: SessionState::default(),
model_capabilities: None,
desired_model: config.model.clone(),
});
Expand Down Expand Up @@ -228,6 +229,7 @@ async fn main() -> Result<()> {
rest_client: relay.rest_client(),
channel_info: channel_info_map,
context_message_limit: config.context_message_limit,
max_turns_per_session: config.max_turns_per_session,
});

// ── Step 6: Heartbeat timer ───────────────────────────────────────────────
Expand Down Expand Up @@ -808,11 +810,8 @@ async fn handle_prompt_result(
// Strip sessions for channels the agent was removed from while this
// agent was checked out. This covers the gap where invalidate_channel_sessions
// only touches idle agents.
if !removed_channels.is_empty() {
result
.agent
.sessions
.retain(|ch, _| !removed_channels.contains(ch));
for ch in removed_channels {
result.agent.state.invalidate_channel(ch);
}

let outcome_label = match &result.outcome {
Expand Down Expand Up @@ -899,8 +898,7 @@ async fn recover_panicked_agent(
pool.agents_mut()[i] = Some(OwnedAgent {
index: i,
acp,
sessions: HashMap::new(),
heartbeat_session: None,
state: SessionState::default(),
model_capabilities: None,
desired_model: config.model.clone(),
});
Expand Down Expand Up @@ -1011,8 +1009,7 @@ async fn respawn_agent_into(old_agent: OwnedAgent, config: &Config) -> Result<Ow
Ok(OwnedAgent {
index,
acp,
sessions: HashMap::new(),
heartbeat_session: None,
state: SessionState::default(),
model_capabilities: None,
desired_model: config.model.clone(),
})
Expand Down
Loading
Loading