Skip to main content

defect_cli/
session_open.rs

1//! Opens a session running directly on the local machine — a neutral helper shared by the
2//! interactive REPL (`repl`) and the single-shot (`oneshot`) paths.
3//!
4//! Both frontend paths execute files and commands directly on the local machine: fs and
5//! shell both use the local backend, and the frontend is marked [`Frontend::Cli`]. They
6//! depend on this module at the same level, not on each other — this avoids making
7//! oneshot depend on the repl module (which would tie oneshot to the `repl` feature).
8
9use std::path::Path;
10use std::sync::Arc;
11
12use agent_client_protocol_schema::SessionId;
13use defect_agent::session::{AgentCore, Frontend, Session, new_session_id};
14use defect_tools::{LocalFsBackend, LocalShellBackend};
15
16/// Knobs for opening a local CLI session — everything tied to the local backends or the
17/// open/resume choice. Grouped into one struct so the frontend run functions
18/// (`repl::run` / `oneshot::run`) forward a single value instead of growing a new
19/// positional argument every time a local-backend setting becomes configurable.
20#[derive(Debug, Clone)]
21pub struct LocalSessionOpts {
22    /// `Some(id)` resumes that session; `None` creates a fresh one.
23    pub resume: Option<SessionId>,
24    /// Captured-output cap (bytes) for the local shell backend, from
25    /// `[tools.bash].output_max_bytes`.
26    pub shell_output_max_bytes: usize,
27}
28
29/// Opens a session running directly on the local machine (both fs and shell use local
30/// backends, frontend is [`Frontend::Cli`]).
31///
32/// # Errors
33///
34/// Returns an error if `load_session` / `create_session` fails (session does not exist,
35/// duplicate id, cwd unavailable, etc.).
36pub async fn open_local_session(
37    agent: &Arc<dyn AgentCore>,
38    cwd: &Path,
39    opts: LocalSessionOpts,
40) -> anyhow::Result<Arc<dyn Session>> {
41    let fs = Arc::new(LocalFsBackend::new(cwd.to_path_buf()));
42    let shell = Arc::new(LocalShellBackend::with_max_output_bytes(
43        opts.shell_output_max_bytes,
44    ));
45    match opts.resume {
46        Some(id) => agent
47            .load_session(id, fs, shell, Frontend::Cli)
48            .await
49            .map_err(|e| anyhow::anyhow!("load_session failed: {e}")),
50        None => {
51            let session_id = SessionId::new(new_session_id());
52            agent
53                .create_session(
54                    session_id,
55                    cwd.to_path_buf(),
56                    // Per-session MCP increment left empty: the configured `[mcp]` servers
57                    // already live in the McpToolFactory's `default_servers`. This second
58                    // channel exists for an ACP client to attach extra servers per
59                    // `session/new`; the CLI is single-session with process-level config
60                    // and has no per-session dimension to fill here.
61                    Vec::new(),
62                    fs,
63                    shell,
64                    Frontend::Cli,
65                )
66                .await
67                .map_err(|e| anyhow::anyhow!("create_session failed: {e}"))
68        }
69    }
70}