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}