diff options
| author | main <main@swarm.moe> | 2026-03-23 15:38:00 -0400 |
|---|---|---|
| committer | main <main@swarm.moe> | 2026-03-23 15:38:00 -0400 |
| commit | ff76c72f3c78694eebe4824318e85c4751343cf4 (patch) | |
| tree | cfb49d55874a9f90b11fed9756f00e1fc233faeb /crates/phone-opus/src/mcp | |
| parent | 36f3be895696e674fced66ef2b6d285149ee5562 (diff) | |
| download | phone_opus-ff76c72f3c78694eebe4824318e85c4751343cf4.zip | |
Support resuming Claude consult sessions
Diffstat (limited to 'crates/phone-opus/src/mcp')
| -rw-r--r-- | crates/phone-opus/src/mcp/catalog.rs | 6 | ||||
| -rw-r--r-- | crates/phone-opus/src/mcp/service.rs | 61 |
2 files changed, 61 insertions, 6 deletions
diff --git a/crates/phone-opus/src/mcp/catalog.rs b/crates/phone-opus/src/mcp/catalog.rs index a7e7cf6..4c71e83 100644 --- a/crates/phone-opus/src/mcp/catalog.rs +++ b/crates/phone-opus/src/mcp/catalog.rs @@ -41,7 +41,7 @@ impl ToolSpec { const TOOL_SPECS: &[ToolSpec] = &[ ToolSpec { name: "consult", - description: "Run a blocking consult against the system Claude Code install using a read-only built-in toolset and return the response.", + description: "Run a blocking consult against the system Claude Code install using a read-only built-in toolset, optionally resume a prior Claude session by session_id, and return the response.", dispatch: DispatchTarget::Worker, replay: ReplayContract::NeverReplay, }, @@ -94,6 +94,10 @@ fn tool_schema(name: &str) -> Value { "type": "integer", "minimum": 1, "description": "Optional maximum number of Claude agent turns before stopping." + }, + "session_id": { + "type": "string", + "description": "Optional Claude session handle returned by a previous consult call. When set, phone_opus resumes that conversation instead of starting a fresh one." } }, "required": ["prompt"] diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs index 2472887..c57d0db 100644 --- a/crates/phone-opus/src/mcp/service.rs +++ b/crates/phone-opus/src/mcp/service.rs @@ -7,6 +7,7 @@ use libmcp::{Generation, SurfaceKind}; use serde::Deserialize; use serde_json::{Value, json}; use thiserror::Error; +use uuid::Uuid; use crate::mcp::fault::{FaultRecord, FaultStage}; use crate::mcp::output::{ @@ -100,6 +101,7 @@ struct ConsultArgs { prompt: String, cwd: Option<String>, max_turns: Option<u64>, + session_id: Option<String>, } #[derive(Debug, Clone)] @@ -107,6 +109,7 @@ struct ConsultRequest { prompt: PromptText, cwd: WorkingDirectory, max_turns: Option<TurnLimit>, + session: Option<SessionHandle>, } impl ConsultRequest { @@ -115,8 +118,21 @@ impl ConsultRequest { prompt: PromptText::parse(args.prompt)?, cwd: WorkingDirectory::resolve(args.cwd)?, max_turns: args.max_turns.map(TurnLimit::parse).transpose()?, + session: args.session_id.map(SessionHandle::parse).transpose()?, }) } + + fn session_mode(&self) -> &'static str { + if self.session.is_some() { + "resumed" + } else { + "new" + } + } + + fn requested_session_id(&self) -> Option<String> { + self.session.as_ref().map(SessionHandle::display) + } } #[derive(Debug, Clone)] @@ -188,6 +204,21 @@ impl TurnLimit { } } +#[derive(Debug, Clone)] +struct SessionHandle(Uuid); + +impl SessionHandle { + fn parse(raw: String) -> Result<Self, ConsultRequestError> { + Uuid::parse_str(&raw) + .map(Self) + .map_err(|_| ConsultRequestError::InvalidSessionHandle(raw)) + } + + fn display(&self) -> String { + self.0.to_string() + } +} + #[derive(Debug, Error)] enum ConsultRequestError { #[error("prompt must not be empty")] @@ -200,6 +231,8 @@ enum ConsultRequestError { NotDirectory(String), #[error("max_turns must be greater than zero")] InvalidTurnLimit, + #[error("session_id must be a valid UUID, got `{0}`")] + InvalidSessionHandle(String), } #[derive(Debug, Error)] @@ -317,6 +350,9 @@ fn invoke_claude(request: &ConsultRequest) -> Result<ConsultResponse, ConsultInv .arg(CLAUDE_TOOLSET) .arg("--permission-mode") .arg("dontAsk"); + if let Some(session) = request.session.as_ref() { + let _ = command.arg("--resume").arg(session.display()); + } if let Some(max_turns) = request.max_turns { let _ = command.arg("--max-turns").arg(max_turns.get().to_string()); } @@ -401,6 +437,8 @@ fn consult_output( let concise = json!({ "response": response.result, "cwd": response.cwd.display(), + "session_mode": request.session_mode(), + "requested_session_id": request.requested_session_id(), "model": response.model_name(), "duration_ms": response.duration_ms, "num_turns": response.num_turns, @@ -414,6 +452,8 @@ fn consult_output( "cwd": response.cwd.display(), "prompt": request.prompt.as_str(), "max_turns": request.max_turns.map(TurnLimit::get), + "session_mode": request.session_mode(), + "requested_session_id": request.requested_session_id(), "duration_ms": response.duration_ms, "duration_api_ms": response.duration_api_ms, "num_turns": response.num_turns, @@ -429,8 +469,8 @@ fn consult_output( fallback_detailed_tool_output( &concise, &full, - concise_text(response), - Some(full_text(response)), + concise_text(request, response), + Some(full_text(request, response)), SurfaceKind::Read, generation, FaultStage::Worker, @@ -438,9 +478,10 @@ fn consult_output( ) } -fn concise_text(response: &ConsultResponse) -> String { +fn concise_text(request: &ConsultRequest, response: &ConsultResponse) -> String { let mut status = vec![ "consult ok".to_owned(), + format!("session={}", request.session_mode()), format!("turns={}", response.num_turns), format!("duration={}", render_duration_ms(response.duration_ms)), ]; @@ -456,6 +497,9 @@ fn concise_text(response: &ConsultResponse) -> String { let mut lines = vec![status.join(" ")]; lines.push(format!("cwd: {}", response.cwd.display())); + if let Some(session_id) = request.requested_session_id() { + lines.push(format!("requested_session: {session_id}")); + } if let Some(session_id) = response.session_id.as_deref() { lines.push(format!("session: {session_id}")); } @@ -470,12 +514,19 @@ fn concise_text(response: &ConsultResponse) -> String { lines.join("\n") } -fn full_text(response: &ConsultResponse) -> String { +fn full_text(request: &ConsultRequest, response: &ConsultResponse) -> String { let mut lines = vec![ - format!("consult ok turns={}", response.num_turns), + format!( + "consult ok session={} turns={}", + request.session_mode(), + response.num_turns + ), format!("cwd: {}", response.cwd.display()), format!("duration: {}", render_duration_ms(response.duration_ms)), ]; + if let Some(session_id) = request.requested_session_id() { + lines.push(format!("requested_session: {session_id}")); + } if let Some(duration_api_ms) = response.duration_api_ms { lines.push(format!( "api_duration: {}", |