1use std::collections::{BTreeMap, BTreeSet};
13use std::pin::Pin;
14use std::sync::atomic::Ordering;
15
16use anyhow::Result;
17use futures::Stream;
18use serde::{Deserialize, Serialize};
19use serde_json::{json, Value};
20
21use agentos_protocol::generated::v1::{
22 AcpCloseSessionRequest, AcpCreateSessionRequest, AcpGetSessionStateRequest,
23 AcpListAgentsRequest, AcpRequest, AcpResponse, AcpResumeSessionRequest, AcpRuntimeKind,
24 AcpSessionCreatedResponse, AcpSessionRequest, AcpSessionStateResponse,
25};
26use agentos_protocol::ACP_EXTENSION_NAMESPACE;
27use agentos_sidecar_client::wire;
28
29use crate::agent_os::{AgentOs, SessionEntry};
30use crate::config::ToolKit;
31use crate::error::ClientError;
32use crate::json_rpc::{JsonRpcError, JsonRpcId, JsonRpcNotification, JsonRpcResponse};
33use crate::stream::Subscription;
34use crate::{CLOSED_SESSION_ID_RETENTION_LIMIT, PERMISSION_TIMEOUT_MS};
35
36const LEGACY_PERMISSION_METHOD: &str = "request/permission";
38
39pub(crate) const ACP_PERMISSION_METHOD: &str = "session/request_permission";
42
43const SESSION_PENDING_REQUEST_LIMIT: usize = 1024;
45
46pub(crate) struct PermissionRouteRequest {
47 pub(crate) session_id: String,
48 pub(crate) permission_id: String,
49 pub(crate) params: Value,
50}
51
52pub(crate) struct PermissionRouteResult {
53 pub(crate) reply: Option<String>,
54}
55
56struct SessionCreatedResponse {
57 session_id: String,
58 modes: Option<Value>,
59 config_options: Vec<Value>,
60 agent_capabilities: Option<Value>,
61 agent_info: Option<Value>,
62}
63
64pub(crate) struct SessionStateResponse {
65 modes: Option<Value>,
66 config_options: Vec<Value>,
67 agent_capabilities: Option<Value>,
68 agent_info: Option<Value>,
69}
70
71const PROMPT_TEXT_CAPTURE_LIMIT_BYTES: usize = 16 * 1024 * 1024;
73
74const PROMPT_DELIVERED_CHUNK_LIMIT: usize = 262_144;
76
77pub type SessionEventStream = Pin<Box<dyn Stream<Item = JsonRpcNotification> + Send>>;
78pub type SessionEventSubscription = (SessionEventStream, Subscription);
79pub type PermissionRequestStream = Pin<Box<dyn Stream<Item = PermissionRequest> + Send>>;
80pub type PermissionRequestSubscription = (PermissionRequestStream, Subscription);
81pub type AgentExitStream = Pin<Box<dyn Stream<Item = AgentExitEvent> + Send>>;
82pub type AgentExitSubscription = (AgentExitStream, Subscription);
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct AgentExitEvent {
96 #[serde(rename = "sessionId")]
97 pub session_id: String,
98 #[serde(rename = "agentType")]
99 pub agent_type: String,
100 #[serde(rename = "processId")]
101 pub process_id: String,
102 #[serde(rename = "exitCode")]
104 pub exit_code: Option<i32>,
105 pub restart: String,
106 #[serde(rename = "restartCount")]
107 pub restart_count: u32,
108 #[serde(rename = "maxRestarts")]
109 pub max_restarts: u32,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct SessionInfo {
119 #[serde(rename = "sessionId")]
120 pub session_id: String,
121 #[serde(rename = "agentType")]
122 pub agent_type: String,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct AgentRegistryEntry {
132 pub id: String,
133 pub installed: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(tag = "type", rename_all = "lowercase")]
139pub enum McpServerConfig {
140 Local {
141 command: String,
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 args: Vec<String>,
144 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
145 env: BTreeMap<String, String>,
146 },
147 Remote {
148 url: String,
149 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
150 headers: BTreeMap<String, String>,
151 },
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Default)]
156pub struct CreateSessionOptions {
157 pub cwd: Option<String>,
159 pub env: BTreeMap<String, String>,
160 pub mcp_servers: Vec<McpServerConfig>,
162 pub skip_os_instructions: bool,
164 pub additional_instructions: Option<String>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct SessionId {
170 #[serde(rename = "sessionId")]
171 pub session_id: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct ResumeSessionResult {
180 #[serde(rename = "sessionId")]
181 pub session_id: String,
182 pub mode: String,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Default)]
189pub struct ResumeSessionOptions {
190 pub transcript_path: Option<String>,
193 pub cwd: Option<String>,
195 pub env: BTreeMap<String, String>,
196}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct PromptResult {
201 pub response: JsonRpcResponse,
202 pub text: String,
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct SessionMode {
208 pub id: String,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub name: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub label: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub description: Option<String>,
215 #[serde(flatten)]
217 pub extra: BTreeMap<String, Value>,
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
226pub struct SessionModeState {
227 #[serde(default, rename = "currentModeId")]
228 pub current_mode_id: String,
229 #[serde(default, rename = "availableModes")]
230 pub available_modes: Vec<SessionMode>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ConfigAllowedValue {
236 pub id: String,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub label: Option<String>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct SessionConfigOption {
248 #[serde(default)]
249 pub id: String,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub category: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub label: Option<String>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub description: Option<String>,
256 #[serde(
257 default,
258 rename = "currentValue",
259 skip_serializing_if = "Option::is_none"
260 )]
261 pub current_value: Option<String>,
262 #[serde(
263 default,
264 rename = "allowedValues",
265 skip_serializing_if = "Option::is_none"
266 )]
267 pub allowed_values: Option<Vec<ConfigAllowedValue>>,
268 #[serde(default, rename = "readOnly", skip_serializing_if = "Option::is_none")]
269 pub read_only: Option<bool>,
270}
271
272#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
274pub struct PromptCapabilities {
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub audio: Option<bool>,
277 #[serde(
278 default,
279 rename = "embeddedContext",
280 skip_serializing_if = "Option::is_none"
281 )]
282 pub embedded_context: Option<bool>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub image: Option<bool>,
285 #[serde(flatten)]
286 pub extra: BTreeMap<String, Value>,
287}
288
289#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
291pub struct AgentCapabilities {
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub permissions: Option<bool>,
294 #[serde(default, rename = "plan_mode", skip_serializing_if = "Option::is_none")]
295 pub plan_mode: Option<bool>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub questions: Option<bool>,
298 #[serde(
299 default,
300 rename = "tool_calls",
301 skip_serializing_if = "Option::is_none"
302 )]
303 pub tool_calls: Option<bool>,
304 #[serde(
305 default,
306 rename = "text_messages",
307 skip_serializing_if = "Option::is_none"
308 )]
309 pub text_messages: Option<bool>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub images: Option<bool>,
312 #[serde(
313 default,
314 rename = "file_attachments",
315 skip_serializing_if = "Option::is_none"
316 )]
317 pub file_attachments: Option<bool>,
318 #[serde(
319 default,
320 rename = "session_lifecycle",
321 skip_serializing_if = "Option::is_none"
322 )]
323 pub session_lifecycle: Option<bool>,
324 #[serde(
325 default,
326 rename = "error_events",
327 skip_serializing_if = "Option::is_none"
328 )]
329 pub error_events: Option<bool>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub reasoning: Option<bool>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub status: Option<bool>,
334 #[serde(
335 default,
336 rename = "streaming_deltas",
337 skip_serializing_if = "Option::is_none"
338 )]
339 pub streaming_deltas: Option<bool>,
340 #[serde(default, rename = "mcp_tools", skip_serializing_if = "Option::is_none")]
341 pub mcp_tools: Option<bool>,
342 #[serde(
343 default,
344 rename = "promptCapabilities",
345 skip_serializing_if = "Option::is_none"
346 )]
347 pub prompt_capabilities: Option<PromptCapabilities>,
348 #[serde(flatten)]
349 pub extra: BTreeMap<String, Value>,
350}
351
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354pub struct AgentInfo {
355 pub name: String,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub title: Option<String>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub version: Option<String>,
360 #[serde(flatten)]
361 pub extra: BTreeMap<String, Value>,
362}
363
364#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
366pub struct SessionInitData {
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub modes: Option<SessionModeState>,
369 #[serde(
370 default,
371 rename = "configOptions",
372 skip_serializing_if = "Option::is_none"
373 )]
374 pub config_options: Option<Vec<SessionConfigOption>>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub capabilities: Option<AgentCapabilities>,
377 #[serde(default, rename = "agentInfo", skip_serializing_if = "Option::is_none")]
378 pub agent_info: Option<AgentInfo>,
379}
380
381#[derive(Clone)]
388pub struct PermissionResponder {
389 inner:
390 std::sync::Arc<parking_lot::Mutex<Option<tokio::sync::oneshot::Sender<PermissionReply>>>>,
391}
392
393impl PermissionResponder {
394 pub fn new() -> (Self, tokio::sync::oneshot::Receiver<PermissionReply>) {
396 let (tx, rx) = tokio::sync::oneshot::channel();
397 (
398 Self {
399 inner: std::sync::Arc::new(parking_lot::Mutex::new(Some(tx))),
400 },
401 rx,
402 )
403 }
404
405 pub fn respond(&self, reply: PermissionReply) {
407 if let Some(tx) = self.inner.lock().take() {
408 let _ = tx.send(reply);
409 }
410 }
411}
412
413#[derive(Clone)]
420pub struct PermissionRequest {
421 pub permission_id: String,
422 pub description: Option<String>,
423 pub params: Value,
424 pub responder: PermissionResponder,
425}
426
427impl std::fmt::Debug for PermissionRequest {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("PermissionRequest")
430 .field("permission_id", &self.permission_id)
431 .field("description", &self.description)
432 .field("params", &self.params)
433 .finish_non_exhaustive()
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
439#[serde(rename_all = "lowercase")]
440pub enum PermissionReply {
441 Once,
442 Always,
443 Reject,
444}
445
446fn permission_reply_wire(reply: PermissionReply) -> &'static str {
449 match reply {
450 PermissionReply::Once => "once",
451 PermissionReply::Always => "always",
452 PermissionReply::Reject => "reject",
453 }
454}
455
456fn agent_capabilities_is_empty(caps: &AgentCapabilities) -> bool {
464 caps.permissions.is_none()
465 && caps.plan_mode.is_none()
466 && caps.questions.is_none()
467 && caps.tool_calls.is_none()
468 && caps.text_messages.is_none()
469 && caps.images.is_none()
470 && caps.file_attachments.is_none()
471 && caps.session_lifecycle.is_none()
472 && caps.error_events.is_none()
473 && caps.reasoning.is_none()
474 && caps.status.is_none()
475 && caps.streaming_deltas.is_none()
476 && caps.mcp_tools.is_none()
477 && caps.prompt_capabilities.is_none()
478 && caps.extra.is_empty()
479}
480
481fn should_dispatch_to_session_event_handlers(notification: &JsonRpcNotification) -> bool {
484 notification.method == "session/update"
485}
486
487pub(crate) fn record_live_session_event(entry: &SessionEntry, notification: JsonRpcNotification) {
488 apply_session_update(entry, ¬ification);
489 if should_dispatch_to_session_event_handlers(¬ification) {
490 let _ = entry.event_tx.send(notification);
491 }
492}
493
494fn apply_session_update(entry: &SessionEntry, notification: &JsonRpcNotification) {
495 if notification.method != "session/update" {
496 return;
497 }
498 let Some(params) = notification.params.as_ref().and_then(Value::as_object) else {
499 return;
500 };
501 let update = params
502 .get("update")
503 .and_then(Value::as_object)
504 .unwrap_or(params);
505 match update.get("sessionUpdate").and_then(Value::as_str) {
506 Some("current_mode_update") => {
507 let Some(mode_id) = update.get("currentModeId").and_then(Value::as_str) else {
508 return;
509 };
510 let mut modes = entry.modes.lock();
511 if let Some(modes) = modes.as_mut() {
512 modes.current_mode_id = mode_id.to_string();
513 }
514 }
515 Some("config_option_update") | Some("config_options_update") => {
516 let Some(options) = update.get("configOptions").and_then(Value::as_array) else {
517 return;
518 };
519 let parsed = options
520 .iter()
521 .filter_map(|value| serde_json::from_value(value.clone()).ok())
522 .collect();
523 *entry.config_options.lock() = parsed;
524 apply_synthetic_config_overrides(entry);
525 }
526 Some("agent_message_chunk") | None | Some(_) => {}
527 }
528}
529
530fn accumulate_agent_message_chunk(
531 notification: &JsonRpcNotification,
532 delivered_chunks: &mut usize,
533 agent_text: &mut String,
534) -> std::result::Result<(), ClientError> {
535 let params = notification.params.clone().unwrap_or(Value::Null);
536 let update = params.get("update").cloned().unwrap_or(Value::Null);
537 if update.get("sessionUpdate").and_then(Value::as_str) != Some("agent_message_chunk") {
538 return Ok(());
539 }
540 if let Some(chunk) = update
541 .get("content")
542 .and_then(|content| content.get("text"))
543 .and_then(Value::as_str)
544 {
545 if *delivered_chunks >= PROMPT_DELIVERED_CHUNK_LIMIT {
546 return Err(prompt_chunk_limit_error());
547 }
548 let next_len = agent_text
549 .len()
550 .checked_add(chunk.len())
551 .ok_or_else(|| prompt_text_limit_error(usize::MAX))?;
552 if next_len > PROMPT_TEXT_CAPTURE_LIMIT_BYTES {
553 return Err(prompt_text_limit_error(next_len));
554 }
555 agent_text.push_str(chunk);
556 *delivered_chunks += 1;
557 }
558 Ok(())
559}
560
561fn pending_session_request_count(entry: &SessionEntry) -> usize {
562 let mut count = 0;
563 entry.pending_prompt_resolvers.scan(|_, _| {
564 count += 1;
565 });
566 count
567}
568
569fn prompt_text_limit_error(size: usize) -> ClientError {
570 ClientError::Sidecar(format!(
571 "prompt text capture is {size} bytes, limit is {PROMPT_TEXT_CAPTURE_LIMIT_BYTES}"
572 ))
573}
574
575fn prompt_chunk_limit_error() -> ClientError {
576 ClientError::Sidecar(format!(
577 "prompt chunk tracking limit exceeded: at most {PROMPT_DELIVERED_CHUNK_LIMIT} chunks can be captured per prompt"
578 ))
579}
580
581struct PendingSessionRequestGuard<'a> {
582 os: &'a AgentOs,
583 session_id: &'a str,
584 resolver_id: i64,
585 active: bool,
586}
587
588impl<'a> PendingSessionRequestGuard<'a> {
589 fn new(os: &'a AgentOs, session_id: &'a str, resolver_id: i64) -> Self {
590 Self {
591 os,
592 session_id,
593 resolver_id,
594 active: true,
595 }
596 }
597
598 fn cleanup(&mut self) {
599 if self.active {
600 self.os
601 .cleanup_pending_resolver(self.session_id, self.resolver_id);
602 self.active = false;
603 }
604 }
605}
606
607impl Drop for PendingSessionRequestGuard<'_> {
608 fn drop(&mut self) {
609 self.cleanup();
610 }
611}
612
613fn apply_synthetic_config_overrides(entry: &SessionEntry) {
616 let overrides = entry.config_overrides.lock().clone();
617 if overrides.is_empty() {
618 return;
619 }
620 let mut options = entry.config_options.lock();
621 for option in options.iter_mut() {
622 let override_value = overrides
625 .get(&option.id)
626 .filter(|_| !option.id.starts_with(PENDING_METHOD_PREFIX))
627 .cloned()
628 .or_else(|| {
629 option
630 .category
631 .as_ref()
632 .and_then(|category| overrides.get(category).cloned())
633 });
634 if let Some(value) = override_value {
635 option.current_value = Some(value);
636 }
637 }
638}
639
640const PENDING_METHOD_PREFIX: &str = "__pending_method::";
643
644fn sync_session_state(entry: &SessionEntry, state: &SessionStateResponse) {
647 *entry.modes.lock() = state
648 .modes
649 .as_ref()
650 .filter(|value| value.is_object())
651 .and_then(|value| serde_json::from_value(value.clone()).ok());
652
653 *entry.config_options.lock() = state
654 .config_options
655 .iter()
656 .filter_map(|value| serde_json::from_value(value.clone()).ok())
657 .collect();
658
659 apply_synthetic_config_overrides(entry);
660
661 *entry.capabilities.lock() = state
662 .agent_capabilities
663 .as_ref()
664 .filter(|value| value.is_object())
665 .and_then(|value| serde_json::from_value(value.clone()).ok());
666
667 *entry.agent_info.lock() = state
668 .agent_info
669 .as_ref()
670 .filter(|value| value.is_object())
671 .and_then(|value| serde_json::from_value(value.clone()).ok());
672}
673
674fn unsupported_config_response(agent_type: &str, category: &str) -> JsonRpcResponse {
677 let message = if agent_type == "opencode" && category == "model" {
678 "OpenCode reports available models, but model switching must be configured before createSession() because ACP session/set_config_option is not implemented.".to_string()
679 } else {
680 format!("The {category} config option is read-only for {agent_type} sessions.")
681 };
682 JsonRpcResponse {
683 jsonrpc: "2.0".to_string(),
684 id: Some(JsonRpcId::Null),
685 result: None,
686 error: Some(JsonRpcError {
687 code: -32601,
688 message,
689 data: None,
690 }),
691 }
692}
693
694fn session_closed_response(session_id: &str) -> JsonRpcResponse {
696 JsonRpcResponse {
697 jsonrpc: "2.0".to_string(),
698 id: Some(JsonRpcId::Null),
699 result: None,
700 error: Some(JsonRpcError {
701 code: -32000,
702 message: format!("Session closed: {session_id}"),
703 data: None,
704 }),
705 }
706}
707
708fn session_created_from_acp(
709 response: AcpSessionCreatedResponse,
710) -> std::result::Result<SessionCreatedResponse, ClientError> {
711 Ok(SessionCreatedResponse {
712 session_id: response.session_id,
713 modes: parse_optional_json(response.modes, "modes")?,
714 config_options: parse_json_vec(response.config_options, "configOptions")?,
715 agent_capabilities: parse_optional_json(response.agent_capabilities, "agentCapabilities")?,
716 agent_info: parse_optional_json(response.agent_info, "agentInfo")?,
717 })
718}
719
720fn session_state_from_acp(
721 response: AcpSessionStateResponse,
722) -> std::result::Result<SessionStateResponse, ClientError> {
723 Ok(SessionStateResponse {
724 modes: parse_optional_json(response.modes, "modes")?,
725 config_options: parse_json_vec(response.config_options, "configOptions")?,
726 agent_capabilities: parse_optional_json(response.agent_capabilities, "agentCapabilities")?,
727 agent_info: parse_optional_json(response.agent_info, "agentInfo")?,
728 })
729}
730
731fn parse_optional_json(
732 value: Option<String>,
733 label: &str,
734) -> std::result::Result<Option<Value>, ClientError> {
735 value
736 .map(|value| {
737 serde_json::from_str(&value).map_err(|error| {
738 ClientError::Sidecar(format!("malformed ACP {label} JSON: {error}"))
739 })
740 })
741 .transpose()
742}
743
744fn parse_json_vec(
745 values: Vec<String>,
746 label: &str,
747) -> std::result::Result<Vec<Value>, ClientError> {
748 values
749 .into_iter()
750 .map(|value| {
751 serde_json::from_str(&value).map_err(|error| {
752 ClientError::Sidecar(format!("malformed ACP {label} JSON: {error}"))
753 })
754 })
755 .collect()
756}
757
758fn unexpected_acp_response(operation: &str, response: AcpResponse) -> ClientError {
759 ClientError::Sidecar(format!("unexpected response to {operation}: {response:?}"))
760}
761
762fn combine_instructions(additional: Option<&str>, tool_reference: &str) -> Option<String> {
763 let mut parts = Vec::new();
764 if let Some(additional) = additional.map(str::trim).filter(|value| !value.is_empty()) {
765 parts.push(additional.to_string());
766 }
767 let tool_reference = tool_reference.trim();
768 if !tool_reference.is_empty() {
769 parts.push(tool_reference.to_string());
770 }
771 if parts.is_empty() {
772 None
773 } else {
774 Some(parts.join("\n\n"))
775 }
776}
777
778fn build_host_tool_reference(tool_kits: &[ToolKit]) -> String {
779 if tool_kits.is_empty() {
780 return String::new();
781 }
782
783 let mut lines = vec![
784 String::from("## Available Host Tools"),
785 String::new(),
786 String::from("Run `agentos list-tools` to see all available tools."),
787 String::new(),
788 ];
789
790 for kit in tool_kits {
791 lines.push(format!("### {}", kit.name));
792 lines.push(String::new());
793 lines.push(kit.description.clone());
794 lines.push(String::new());
795 for tool in &kit.tools {
796 let signature = build_tool_flag_signature(&tool.input_schema);
797 let suffix = if signature.is_empty() {
798 String::new()
799 } else {
800 format!(" {signature}")
801 };
802 lines.push(format!(
803 "- `agentos-{} {}{}` — {}",
804 kit.name, tool.name, suffix, tool.description
805 ));
806 }
807 lines.push(String::new());
808 lines.push(format!(
809 "Run `agentos-{} <tool> --help` for details.",
810 kit.name
811 ));
812 lines.push(String::new());
813 }
814
815 lines.join("\n")
816}
817
818fn build_tool_flag_signature(schema: &Value) -> String {
819 describe_tool_flags(schema)
820 .into_iter()
821 .map(|flag| {
822 if flag.required {
823 format!("{} <{}>", flag.name, flag.value_type)
824 } else {
825 format!("[{} <{}>]", flag.name, flag.value_type)
826 }
827 })
828 .collect::<Vec<_>>()
829 .join(" ")
830}
831
832struct ToolFlagDescription {
833 name: String,
834 value_type: String,
835 required: bool,
836}
837
838fn describe_tool_flags(schema: &Value) -> Vec<ToolFlagDescription> {
839 let properties = schema
840 .get("properties")
841 .and_then(Value::as_object)
842 .cloned()
843 .unwrap_or_default();
844 let required = schema
845 .get("required")
846 .and_then(Value::as_array)
847 .map(|items| {
848 items
849 .iter()
850 .filter_map(Value::as_str)
851 .map(str::to_owned)
852 .collect::<BTreeSet<_>>()
853 })
854 .unwrap_or_default();
855
856 properties
857 .into_iter()
858 .map(|(field_name, field_schema)| ToolFlagDescription {
859 name: format!("--{}", camel_to_kebab(&field_name)),
860 value_type: describe_tool_flag_type(&field_schema),
861 required: required.contains(&field_name),
862 })
863 .collect()
864}
865
866fn describe_tool_flag_type(schema: &Value) -> String {
867 match json_schema_type(schema) {
868 Some("array") => {
869 let item_type = schema
870 .get("items")
871 .and_then(json_schema_type)
872 .unwrap_or("string");
873 format!("{item_type}[]")
874 }
875 Some("string") => schema
876 .get("enum")
877 .and_then(Value::as_array)
878 .map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
879 .filter(|values| !values.is_empty())
880 .map(|values| values.join("|"))
881 .unwrap_or_else(|| String::from("string")),
882 Some(other) => other.to_string(),
883 None => String::from("string"),
884 }
885}
886
887fn json_schema_type(schema: &Value) -> Option<&str> {
888 schema.get("type").and_then(Value::as_str)
889}
890
891fn camel_to_kebab(value: &str) -> String {
892 let mut output = String::new();
893 for (index, ch) in value.chars().enumerate() {
894 if ch.is_ascii_uppercase() && index > 0 {
895 output.push('-');
896 }
897 output.push(ch.to_ascii_lowercase());
898 }
899 output
900}
901
902impl AgentOs {
907 fn session_ownership(&self) -> wire::OwnershipScope {
909 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
910 connection_id: self.connection_id().to_string(),
911 session_id: self.wire_session_id().to_string(),
912 vm_id: self.vm_id().to_string(),
913 })
914 }
915
916 fn require_session<R>(
918 &self,
919 session_id: &str,
920 f: impl FnOnce(&SessionEntry) -> R,
921 ) -> std::result::Result<R, ClientError> {
922 self.inner()
923 .sessions
924 .read(session_id, |_, entry| f(entry))
925 .ok_or_else(|| ClientError::SessionNotFound(session_id.to_string()))
926 }
927
928 async fn hydrate_session_state(
931 &self,
932 session_id: &str,
933 ) -> std::result::Result<(), ClientError> {
934 self.require_session(session_id, |_| ())?;
935 let response = self
936 .send_acp_request(AcpRequest::AcpGetSessionStateRequest(
937 AcpGetSessionStateRequest {
938 session_id: session_id.to_string(),
939 },
940 ))
941 .await?;
942 let AcpResponse::AcpSessionStateResponse(state) = response else {
943 return Err(unexpected_acp_response(
944 "AcpGetSessionStateRequest",
945 response,
946 ));
947 };
948 let state = session_state_from_acp(state)?;
949
950 self.require_session(session_id, |entry| sync_session_state(entry, &state))?;
951 Ok(())
952 }
953
954 pub(crate) async fn send_session_request(
958 &self,
959 session_id: &str,
960 method: &str,
961 params: Option<Value>,
962 ) -> std::result::Result<JsonRpcResponse, ClientError> {
963 let request_params = params;
964
965 let resolver_id = self.inner().request_counter.fetch_add(1, Ordering::SeqCst);
970 let (resolve_tx, resolve_rx) = tokio::sync::oneshot::channel::<JsonRpcResponse>();
971 self.require_session(session_id, |entry| {
972 let _guard = entry.pending_session_request_lock.lock();
973 if pending_session_request_count(entry) >= SESSION_PENDING_REQUEST_LIMIT {
974 return Err(ClientError::Sidecar(format!(
975 "session pending request limit exceeded: at most {SESSION_PENDING_REQUEST_LIMIT} requests can be in flight per session"
976 )));
977 }
978 let _ = entry
979 .pending_prompt_resolvers
980 .insert(resolver_id, resolve_tx);
981 entry
983 .config_overrides
984 .lock()
985 .entry(format!("{PENDING_METHOD_PREFIX}{resolver_id}"))
986 .or_insert_with(|| method.to_string());
987 Ok(())
988 })??;
989 let mut pending_request_guard =
990 PendingSessionRequestGuard::new(self, session_id, resolver_id);
991
992 let rpc = self.send_acp_request(AcpRequest::AcpSessionRequest(AcpSessionRequest {
993 session_id: session_id.to_string(),
994 method: method.to_string(),
995 params: request_params
996 .clone()
997 .map(|params| serde_json::to_string(¶ms))
998 .transpose()
999 .map_err(|error| {
1000 ClientError::Sidecar(format!("failed to encode session params: {error}"))
1001 })?,
1002 }));
1003 tokio::pin!(rpc);
1004
1005 let response = tokio::select! {
1006 biased;
1007 resolved = resolve_rx => {
1008 pending_request_guard.cleanup();
1012 match resolved {
1013 Ok(response) => return Ok(response),
1014 Err(_) => return Ok(session_closed_response(session_id)),
1015 }
1016 }
1017 result = &mut rpc => {
1018 pending_request_guard.cleanup();
1019 result?
1020 }
1021 };
1022
1023 let response = match response {
1024 AcpResponse::AcpSessionRpcResponse(rpc) => {
1025 serde_json::from_str::<JsonRpcResponse>(&rpc.response).map_err(|err| {
1026 ClientError::Sidecar(format!("malformed session rpc response: {err}"))
1027 })?
1028 }
1029 other => return Err(unexpected_acp_response("AcpSessionRequest", other)),
1030 };
1031
1032 let _ = self.hydrate_session_state(session_id).await;
1034
1035 if response.error.is_none() {
1036 self.apply_post_send_cache_updates(session_id, method, request_params.as_ref())?;
1037 }
1038
1039 Ok(response)
1040 }
1041
1042 fn cleanup_pending_resolver(&self, session_id: &str, resolver_id: i64) {
1044 let _ = self.require_session(session_id, |entry| {
1045 let _ = entry.pending_prompt_resolvers.remove(&resolver_id);
1046 entry
1047 .config_overrides
1048 .lock()
1049 .remove(&format!("{PENDING_METHOD_PREFIX}{resolver_id}"));
1050 });
1051 }
1052
1053 fn apply_post_send_cache_updates(
1055 &self,
1056 session_id: &str,
1057 method: &str,
1058 params: Option<&Value>,
1059 ) -> std::result::Result<(), ClientError> {
1060 self.require_session(session_id, |entry| {
1061 if method == "session/set_mode" {
1062 if let Some(mode_id) = params.and_then(|p| p.get("modeId")).and_then(Value::as_str)
1063 {
1064 let mut modes = entry.modes.lock();
1065 if let Some(modes) = modes.as_mut() {
1066 modes.current_mode_id = mode_id.to_string();
1067 }
1068 }
1069 }
1070 if method == "session/set_config_option" {
1071 let config_id = params
1072 .and_then(|p| p.get("configId"))
1073 .and_then(Value::as_str);
1074 let value = params.and_then(|p| p.get("value")).and_then(Value::as_str);
1075 if let (Some(config_id), Some(value)) = (config_id, value) {
1076 let mut options = entry.config_options.lock();
1077 for option in options.iter_mut() {
1078 if option.id == config_id {
1079 option.current_value = Some(value.to_string());
1080 }
1081 }
1082 }
1083 }
1084 })
1085 }
1086
1087 async fn set_session_config_by_category(
1090 &self,
1091 session_id: &str,
1092 category: &str,
1093 value: &str,
1094 ) -> std::result::Result<JsonRpcResponse, ClientError> {
1095 let (read_only, config_id, agent_type) = self.require_session(session_id, |entry| {
1096 let options = entry.config_options.lock();
1097 let option = options
1098 .iter()
1099 .find(|option| option.category.as_deref() == Some(category));
1100 (
1101 option.and_then(|option| option.read_only).unwrap_or(false),
1102 option.map(|option| option.id.clone()),
1103 entry.agent_type.clone(),
1104 )
1105 })?;
1106
1107 if read_only {
1108 return Ok(unsupported_config_response(&agent_type, category));
1109 }
1110
1111 let config_id = config_id.unwrap_or_else(|| category.to_string());
1112 let response = self
1113 .send_session_request(
1114 session_id,
1115 "session/set_config_option",
1116 Some(json!({ "configId": config_id, "value": value })),
1117 )
1118 .await?;
1119
1120 Ok(response)
1121 }
1122
1123 pub fn list_sessions(&self) -> Vec<SessionInfo> {
1125 let mut sessions = Vec::new();
1126 self.inner().sessions.scan(|session_id, entry| {
1127 sessions.push(SessionInfo {
1128 session_id: session_id.clone(),
1129 agent_type: entry.agent_type.clone(),
1130 });
1131 });
1132 sessions
1133 }
1134
1135 pub async fn list_agents(&self) -> Result<Vec<AgentRegistryEntry>> {
1140 let response = self
1141 .send_acp_request(AcpRequest::AcpListAgentsRequest(AcpListAgentsRequest {
1142 reserved: false,
1143 }))
1144 .await?;
1145 let AcpResponse::AcpListAgentsResponse(listed) = response else {
1146 return Err(unexpected_acp_response("AcpListAgentsRequest", response).into());
1147 };
1148 Ok(listed
1149 .agents
1150 .into_iter()
1151 .map(|agent| AgentRegistryEntry {
1152 id: agent.id,
1153 installed: agent.installed,
1154 })
1155 .collect())
1156 }
1157
1158 pub async fn create_session(
1164 &self,
1165 agent_type: &str,
1166 options: CreateSessionOptions,
1167 ) -> Result<SessionId> {
1168 let env: BTreeMap<String, String> = options.env.clone();
1172
1173 let cwd = options
1174 .cwd
1175 .clone()
1176 .unwrap_or_else(|| "/workspace".to_string());
1177 let mcp_servers: Vec<Value> = options
1178 .mcp_servers
1179 .iter()
1180 .filter_map(|server| serde_json::to_value(server).ok())
1181 .collect();
1182 let client_capabilities = json!({
1183 "fs": { "readTextFile": true, "writeTextFile": true },
1184 "terminal": true,
1185 });
1186 let tool_reference = build_host_tool_reference(&self.config().tool_kits);
1187 let additional_instructions =
1188 combine_instructions(options.additional_instructions.as_deref(), &tool_reference);
1189
1190 let response = self
1191 .send_acp_request(AcpRequest::AcpCreateSessionRequest(
1192 AcpCreateSessionRequest {
1193 agent_type: agent_type.to_string(),
1194 runtime: AcpRuntimeKind::JavaScript,
1195 args: Vec::new(),
1196 env: env.into_iter().collect(),
1197 cwd,
1198 mcp_servers: serde_json::to_string(&mcp_servers).map_err(|error| {
1199 ClientError::Sidecar(format!("failed to encode MCP servers: {error}"))
1200 })?,
1201 protocol_version: crate::ACP_PROTOCOL_VERSION as i32,
1202 client_capabilities: serde_json::to_string(&client_capabilities).map_err(
1203 |error| {
1204 ClientError::Sidecar(format!(
1205 "failed to encode client capabilities: {error}"
1206 ))
1207 },
1208 )?,
1209 additional_instructions,
1210 skip_os_instructions: options.skip_os_instructions,
1211 },
1212 ))
1213 .await?;
1214 let AcpResponse::AcpSessionCreatedResponse(created) = response else {
1215 return Err(unexpected_acp_response("AcpCreateSessionRequest", response).into());
1216 };
1217 let created = session_created_from_acp(created)?;
1218
1219 let state = SessionStateResponse {
1222 modes: created.modes,
1223 config_options: created.config_options,
1224 agent_capabilities: created.agent_capabilities,
1225 agent_info: created.agent_info,
1226 };
1227 self.register_session(&created.session_id, agent_type, &state)
1228 .await?;
1229
1230 Ok(SessionId {
1231 session_id: created.session_id,
1232 })
1233 }
1234
1235 pub(crate) async fn register_session(
1239 &self,
1240 session_id: &str,
1241 agent_type: &str,
1242 state: &SessionStateResponse,
1243 ) -> std::result::Result<(), ClientError> {
1244 {
1245 let mut closed = self.inner().closed_session_ids.lock();
1246 closed.retain(|id| id != session_id);
1247 }
1248
1249 let (event_tx, _) = tokio::sync::broadcast::channel(1024);
1250 let (permission_tx, _) = tokio::sync::broadcast::channel(64);
1251 let (agent_exit_tx, _) = tokio::sync::broadcast::channel(16);
1252 let entry = SessionEntry {
1253 agent_type: agent_type.to_string(),
1254 modes: parking_lot::Mutex::new(None),
1255 config_options: parking_lot::Mutex::new(Vec::new()),
1256 capabilities: parking_lot::Mutex::new(None),
1257 agent_info: parking_lot::Mutex::new(None),
1258 config_overrides: parking_lot::Mutex::new(BTreeMap::new()),
1259 event_tx,
1260 permission_tx,
1261 agent_exit_tx,
1262 pending_permission_replies: scc::HashMap::new(),
1263 pending_session_request_lock: parking_lot::Mutex::new(()),
1264 pending_prompt_resolvers: scc::HashMap::new(),
1265 };
1266 sync_session_state(&entry, state);
1267 let _ = self.inner().sessions.insert(session_id.to_string(), entry);
1268
1269 match self.hydrate_session_state(session_id).await {
1270 Ok(()) => Ok(()),
1271 Err(error) => {
1272 let _ = self.inner().sessions.remove(session_id);
1273 Err(error)
1274 }
1275 }
1276 }
1277
1278 pub async fn resume_session(
1292 &self,
1293 session_id: &str,
1294 agent_type: &str,
1295 options: ResumeSessionOptions,
1296 ) -> Result<ResumeSessionResult> {
1297 let env: BTreeMap<String, String> = options.env.clone();
1301
1302 let cwd = options
1303 .cwd
1304 .clone()
1305 .unwrap_or_else(|| "/workspace".to_string());
1306
1307 let response = self
1308 .send_acp_request(AcpRequest::AcpResumeSessionRequest(
1309 AcpResumeSessionRequest {
1310 session_id: session_id.to_string(),
1311 agent_type: agent_type.to_string(),
1312 transcript_path: options.transcript_path.clone(),
1313 cwd,
1314 env: env.into_iter().collect(),
1315 },
1316 ))
1317 .await?;
1318 let AcpResponse::AcpSessionResumedResponse(resumed) = response else {
1319 return Err(unexpected_acp_response("AcpResumeSessionRequest", response).into());
1320 };
1321
1322 let empty_state = SessionStateResponse {
1324 modes: None,
1325 config_options: Vec::new(),
1326 agent_capabilities: None,
1327 agent_info: None,
1328 };
1329 self.register_session(&resumed.session_id, agent_type, &empty_state)
1330 .await?;
1331
1332 Ok(ResumeSessionResult {
1333 session_id: resumed.session_id,
1334 mode: resumed.mode,
1335 })
1336 }
1337
1338 pub async fn destroy_session(&self, session_id: &str) -> Result<()> {
1340 self.require_session(session_id, |_| ())?;
1341 let _ = self.cancel_session(session_id).await;
1342 self.close_session_internal(session_id).await?;
1343 Ok(())
1344 }
1345
1346 pub async fn prompt(&self, session_id: &str, text: &str) -> Result<PromptResult> {
1350 let mut rx = self.require_session(session_id, |entry| entry.event_tx.subscribe())?;
1351
1352 let mut agent_text = String::new();
1353 let mut delivered_chunks = 0;
1354 let mut prompt_text_error: Option<ClientError> = None;
1355
1356 let request = self.send_session_request(
1357 session_id,
1358 "session/prompt",
1359 Some(json!({ "prompt": [{ "type": "text", "text": text }] })),
1360 );
1361 tokio::pin!(request);
1362
1363 let response = loop {
1366 tokio::select! {
1367 biased;
1368 result = &mut request => break result,
1369 event = rx.recv() => {
1370 match event {
1371 Ok(event) => accumulate_agent_message_chunk(
1372 &event,
1373 &mut delivered_chunks,
1374 &mut agent_text,
1375 )
1376 .unwrap_or_else(|error| {
1377 prompt_text_error.get_or_insert(error);
1378 }),
1379 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
1380 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
1381 break (&mut request).await;
1383 }
1384 }
1385 }
1386 }
1387 };
1388
1389 loop {
1391 match rx.try_recv() {
1392 Ok(event) => {
1393 accumulate_agent_message_chunk(&event, &mut delivered_chunks, &mut agent_text)
1394 .unwrap_or_else(|error| {
1395 prompt_text_error.get_or_insert(error);
1396 })
1397 }
1398 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
1399 Err(tokio::sync::broadcast::error::TryRecvError::Empty)
1400 | Err(tokio::sync::broadcast::error::TryRecvError::Closed) => break,
1401 }
1402 }
1403 drop(rx);
1404
1405 let response = response?;
1406 if let Some(error) = prompt_text_error {
1407 return Err(error.into());
1408 }
1409
1410 Ok(PromptResult {
1411 response,
1412 text: agent_text,
1413 })
1414 }
1415
1416 pub async fn cancel_session(&self, session_id: &str) -> Result<JsonRpcResponse> {
1420 self.require_session(session_id, |_| ())?;
1421 let cancelled_pending_prompt = self.cancel_pending_prompt_requests(session_id)?;
1422 if cancelled_pending_prompt {
1423 let this = self.clone();
1426 let session_id_owned = session_id.to_string();
1427 tokio::spawn(async move {
1428 let _ = this
1429 .send_session_request(&session_id_owned, "session/cancel", None)
1430 .await;
1431 });
1432 return Ok(JsonRpcResponse {
1433 jsonrpc: "2.0".to_string(),
1434 id: Some(JsonRpcId::Null),
1435 result: Some(json!({
1436 "cancelled": true,
1437 "requested": true,
1438 "via": "prompt-fallback",
1439 })),
1440 error: None,
1441 });
1442 }
1443 Ok(self
1444 .send_session_request(session_id, "session/cancel", None)
1445 .await?)
1446 }
1447
1448 fn cancel_pending_prompt_requests(
1451 &self,
1452 session_id: &str,
1453 ) -> std::result::Result<bool, ClientError> {
1454 self.require_session(session_id, |entry| {
1455 let mut prompt_resolver_ids = Vec::new();
1456 {
1457 let overrides = entry.config_overrides.lock();
1458 for (key, method) in overrides.iter() {
1459 if let Some(id) = key.strip_prefix(PENDING_METHOD_PREFIX) {
1460 if method == "session/prompt" {
1461 if let Ok(id) = id.parse::<i64>() {
1462 prompt_resolver_ids.push(id);
1463 }
1464 }
1465 }
1466 }
1467 }
1468 let mut cancelled = false;
1469 for id in prompt_resolver_ids {
1470 if let Some((_, resolver)) = entry.pending_prompt_resolvers.remove(&id) {
1471 let _ = resolver.send(JsonRpcResponse {
1474 jsonrpc: "2.0".to_string(),
1475 id: Some(JsonRpcId::Null),
1476 result: Some(json!({ "stopReason": "cancelled" })),
1477 error: None,
1478 });
1479 cancelled = true;
1480 }
1481 entry
1482 .config_overrides
1483 .lock()
1484 .remove(&format!("{PENDING_METHOD_PREFIX}{id}"));
1485 }
1486 cancelled
1487 })
1488 }
1489
1490 fn abort_pending_session_requests(&self, session_id: &str) {
1493 let _ = self.require_session(session_id, |entry| {
1494 let mut ids = Vec::new();
1495 entry.pending_prompt_resolvers.scan(|id, _| ids.push(*id));
1496 for id in ids {
1497 if let Some((_, resolver)) = entry.pending_prompt_resolvers.remove(&id) {
1498 let _ = resolver.send(session_closed_response(session_id));
1501 }
1502 entry
1503 .config_overrides
1504 .lock()
1505 .remove(&format!("{PENDING_METHOD_PREFIX}{id}"));
1506 }
1507 });
1508 }
1509
1510 fn reject_pending_permission_replies(&self, session_id: &str) {
1514 let _ = self.require_session(session_id, |entry| {
1515 let mut ids = Vec::new();
1516 entry
1517 .pending_permission_replies
1518 .scan(|id, _| ids.push(id.clone()));
1519 for id in ids {
1520 let _ = entry.pending_permission_replies.remove(&id);
1521 }
1522 });
1523 }
1524
1525 pub fn close_session(&self, session_id: &str) -> std::result::Result<(), ClientError> {
1530 let known = self.inner().sessions.contains(session_id)
1531 || self.inner().closing_session_ids.contains(session_id)
1532 || self
1533 .inner()
1534 .closed_session_ids
1535 .lock()
1536 .iter()
1537 .any(|id| id == session_id);
1538 if !known {
1539 return Err(ClientError::SessionNotFound(session_id.to_string()));
1540 }
1541
1542 let _ = self
1546 .inner()
1547 .closing_session_ids
1548 .insert(session_id.to_string());
1549
1550 let this = self.clone();
1551 let session_id_owned = session_id.to_string();
1552 tokio::spawn(async move {
1553 let _ = this.close_session_internal(&session_id_owned).await;
1554 let _ = this.inner().closing_session_ids.remove(&session_id_owned);
1555 });
1556 Ok(())
1557 }
1558
1559 pub(crate) async fn close_session_internal(
1563 &self,
1564 session_id: &str,
1565 ) -> std::result::Result<(), ClientError> {
1566 if self
1567 .inner()
1568 .closed_session_ids
1569 .lock()
1570 .iter()
1571 .any(|id| id == session_id)
1572 {
1573 return Ok(());
1574 }
1575
1576 self.abort_pending_session_requests(session_id);
1577 self.reject_pending_permission_replies(session_id);
1578
1579 if !self.inner().sessions.contains(session_id) {
1581 return Err(ClientError::SessionNotFound(session_id.to_string()));
1582 }
1583 let _ = self.inner().sessions.remove(session_id);
1584 {
1585 let mut closed = self.inner().closed_session_ids.lock();
1586 closed.push_back(session_id.to_string());
1587 while closed.len() > CLOSED_SESSION_ID_RETENTION_LIMIT {
1588 closed.pop_front();
1589 }
1590 }
1591
1592 let response = self
1603 .send_acp_request(AcpRequest::AcpCloseSessionRequest(AcpCloseSessionRequest {
1604 session_id: session_id.to_string(),
1605 }))
1606 .await?;
1607 match response {
1608 AcpResponse::AcpSessionClosedResponse(_) => Ok(()),
1609 other => Err(unexpected_acp_response("AcpCloseSessionRequest", other)),
1610 }
1611 }
1612
1613 async fn send_acp_request(
1614 &self,
1615 request: AcpRequest,
1616 ) -> std::result::Result<AcpResponse, ClientError> {
1617 let payload = serde_bare::to_vec(&request).map_err(|error| {
1618 ClientError::Sidecar(format!("failed to encode ACP request: {error}"))
1619 })?;
1620 let response = self
1621 .transport()
1622 .request_wire(
1623 self.session_ownership(),
1624 wire::RequestPayload::ExtEnvelope(wire::ExtEnvelope {
1625 namespace: ACP_EXTENSION_NAMESPACE.to_string(),
1626 payload,
1627 }),
1628 )
1629 .await?;
1630 let envelope = match response {
1631 wire::ResponsePayload::ExtEnvelope(envelope) => envelope,
1632 wire::ResponsePayload::RejectedResponse(rejected) => {
1633 return Err(ClientError::Kernel {
1634 code: rejected.code,
1635 message: rejected.message,
1636 });
1637 }
1638 other => {
1639 return Err(ClientError::Sidecar(format!(
1640 "unexpected ACP Ext response: {other:?}"
1641 )));
1642 }
1643 };
1644 if envelope.namespace != ACP_EXTENSION_NAMESPACE {
1645 return Err(ClientError::Sidecar(format!(
1646 "unexpected ACP Ext namespace: {}",
1647 envelope.namespace
1648 )));
1649 }
1650 let response: AcpResponse = serde_bare::from_slice(&envelope.payload).map_err(|error| {
1651 ClientError::Sidecar(format!("failed to decode ACP response: {error}"))
1652 })?;
1653 match response {
1654 AcpResponse::AcpErrorResponse(error) => Err(ClientError::Kernel {
1655 code: error.code,
1656 message: error.message,
1657 }),
1658 response => Ok(response),
1659 }
1660 }
1661
1662 pub async fn respond_permission(
1666 &self,
1667 session_id: &str,
1668 permission_id: &str,
1669 reply: PermissionReply,
1670 ) -> Result<JsonRpcResponse> {
1671 let pending = self.require_session(session_id, |entry| {
1672 entry
1673 .pending_permission_replies
1674 .remove(permission_id)
1675 .map(|(_, responder)| responder)
1676 })?;
1677
1678 if let Some(responder) = pending {
1679 let _ = responder.send(reply);
1680 return Ok(JsonRpcResponse {
1681 jsonrpc: "2.0".to_string(),
1682 id: Some(JsonRpcId::Null),
1683 result: Some(json!({
1684 "permissionId": permission_id,
1685 "reply": reply,
1686 "via": "sidecar-request",
1687 })),
1688 error: None,
1689 });
1690 }
1691
1692 Ok(self
1693 .send_session_request(
1694 session_id,
1695 LEGACY_PERMISSION_METHOD,
1696 Some(json!({ "permissionId": permission_id, "reply": reply })),
1697 )
1698 .await?)
1699 }
1700
1701 pub async fn set_session_mode(
1703 &self,
1704 session_id: &str,
1705 mode_id: &str,
1706 ) -> Result<JsonRpcResponse> {
1707 Ok(self
1708 .send_session_request(
1709 session_id,
1710 "session/set_mode",
1711 Some(json!({ "modeId": mode_id })),
1712 )
1713 .await?)
1714 }
1715
1716 pub fn get_session_modes(&self, session_id: &str) -> Option<SessionModeState> {
1718 self.require_session(session_id, |entry| entry.modes.lock().clone())
1719 .ok()
1720 .flatten()
1721 }
1722
1723 pub async fn set_session_model(
1726 &self,
1727 session_id: &str,
1728 model: &str,
1729 ) -> Result<JsonRpcResponse> {
1730 Ok(self
1731 .set_session_config_by_category(session_id, "model", model)
1732 .await?)
1733 }
1734
1735 pub async fn set_session_thought_level(
1737 &self,
1738 session_id: &str,
1739 level: &str,
1740 ) -> Result<JsonRpcResponse> {
1741 Ok(self
1742 .set_session_config_by_category(session_id, "thought_level", level)
1743 .await?)
1744 }
1745
1746 pub fn get_session_config_options(&self, session_id: &str) -> Vec<SessionConfigOption> {
1748 self.require_session(session_id, |entry| entry.config_options.lock().clone())
1749 .unwrap_or_default()
1750 }
1751
1752 pub fn get_session_capabilities(&self, session_id: &str) -> Option<AgentCapabilities> {
1755 self.require_session(session_id, |entry| entry.capabilities.lock().clone())
1756 .ok()
1757 .flatten()
1758 .filter(|caps| !agent_capabilities_is_empty(caps))
1759 }
1760
1761 pub fn get_session_agent_info(&self, session_id: &str) -> Option<AgentInfo> {
1763 self.require_session(session_id, |entry| entry.agent_info.lock().clone())
1764 .ok()
1765 .flatten()
1766 }
1767
1768 pub async fn raw_session_send(
1771 &self,
1772 session_id: &str,
1773 method: &str,
1774 params: Option<Value>,
1775 ) -> Result<JsonRpcResponse> {
1776 Ok(self
1777 .send_session_request(session_id, method, params)
1778 .await?)
1779 }
1780
1781 pub async fn raw_send(
1783 &self,
1784 session_id: &str,
1785 method: &str,
1786 params: Option<Value>,
1787 ) -> Result<JsonRpcResponse> {
1788 self.raw_session_send(session_id, method, params).await
1789 }
1790
1791 pub fn on_session_event(
1794 &self,
1795 session_id: &str,
1796 ) -> std::result::Result<SessionEventSubscription, ClientError> {
1797 let rx = self.require_session(session_id, |entry| entry.event_tx.subscribe())?;
1798 let stream = futures::stream::unfold(rx, move |mut rx| async move {
1799 loop {
1800 match rx.recv().await {
1801 Ok(notification) => return Some((notification, rx)),
1802 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
1803 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1804 }
1805 }
1806 });
1807 Ok((Box::pin(stream), Subscription::noop()))
1808 }
1809
1810 pub fn on_permission_request(
1817 &self,
1818 session_id: &str,
1819 ) -> std::result::Result<PermissionRequestSubscription, ClientError> {
1820 let rx = self.require_session(session_id, |entry| entry.permission_tx.subscribe())?;
1821
1822 let stream = futures::stream::unfold(rx, move |mut rx| async move {
1826 loop {
1827 match rx.recv().await {
1828 Ok(request) => return Some((request, rx)),
1829 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
1830 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1831 }
1832 }
1833 });
1834
1835 Ok((Box::pin(stream), Subscription::noop()))
1836 }
1837
1838 pub fn on_agent_exit(
1843 &self,
1844 session_id: &str,
1845 ) -> std::result::Result<AgentExitSubscription, ClientError> {
1846 let rx = self.require_session(session_id, |entry| entry.agent_exit_tx.subscribe())?;
1847 let stream = futures::stream::unfold(rx, move |mut rx| async move {
1848 loop {
1849 match rx.recv().await {
1850 Ok(event) => return Some((event, rx)),
1851 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
1852 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1853 }
1854 }
1855 });
1856 Ok((Box::pin(stream), Subscription::noop()))
1857 }
1858
1859 pub(crate) async fn deliver_sidecar_permission_request(
1868 &self,
1869 request: PermissionRouteRequest,
1870 ) -> PermissionRouteResult {
1871 let PermissionRouteRequest {
1872 session_id,
1873 permission_id,
1874 params,
1875 } = request;
1876
1877 let (slot_tx, slot_rx) = tokio::sync::oneshot::channel::<PermissionReply>();
1878 let (responder, responder_rx) = PermissionResponder::new();
1879 let description = params
1880 .get("description")
1881 .and_then(Value::as_str)
1882 .map(str::to_string);
1883 let delivered = PermissionRequest {
1884 permission_id: permission_id.clone(),
1885 description,
1886 params,
1887 responder,
1888 };
1889
1890 let registered = self.require_session(&session_id, |entry| {
1893 if entry.permission_tx.receiver_count() == 0 {
1894 return false;
1895 }
1896 let _ = entry
1897 .pending_permission_replies
1898 .insert(permission_id.clone(), slot_tx);
1899 let _ = entry.permission_tx.send(delivered);
1900 true
1901 });
1902 match registered {
1903 Ok(true) => {}
1904 Ok(false) => {
1905 return PermissionRouteResult {
1906 reply: Some(permission_reply_wire(PermissionReply::Reject).to_string()),
1907 };
1908 }
1909 Err(_) => {
1910 return PermissionRouteResult { reply: None };
1911 }
1912 }
1913
1914 let this = self.clone();
1916 let bridge_session_id = session_id.clone();
1917 let bridge_permission_id = permission_id.clone();
1918 tokio::spawn(async move {
1919 if let Ok(reply) = responder_rx.await {
1920 let _ = this
1921 .respond_permission(&bridge_session_id, &bridge_permission_id, reply)
1922 .await;
1923 }
1924 });
1925
1926 let timeout = tokio::time::sleep(std::time::Duration::from_millis(PERMISSION_TIMEOUT_MS));
1927 tokio::pin!(timeout);
1928 tokio::select! {
1929 reply = slot_rx => match reply {
1930 Ok(reply) => PermissionRouteResult {
1931 reply: Some(permission_reply_wire(reply).to_string()),
1932 },
1933 Err(_) => PermissionRouteResult {
1935 reply: Some(permission_reply_wire(PermissionReply::Reject).to_string()),
1936 },
1937 },
1938 _ = &mut timeout => {
1939 let _ = self.require_session(&session_id, |entry| {
1940 let _ = entry.pending_permission_replies.remove(&permission_id);
1941 });
1942 PermissionRouteResult {
1943 reply: None,
1944 }
1945 }
1946 }
1947 }
1948}
1949
1950#[cfg(test)]
1953mod prompt_accumulation_tests {
1954 use super::*;
1955
1956 fn notification(update: Value) -> JsonRpcNotification {
1957 JsonRpcNotification {
1958 jsonrpc: "2.0".to_string(),
1959 method: "session/update".to_string(),
1960 params: Some(json!({ "update": update })),
1961 }
1962 }
1963
1964 #[test]
1965 fn non_chunk_events_do_not_affect_prompt_text() {
1966 let chunk = notification(json!({
1967 "sessionUpdate": "agent_message_chunk",
1968 "content": { "text": "hello" },
1969 }));
1970 let non_chunk = notification(json!({
1971 "sessionUpdate": "current_mode_update",
1972 "currentModeId": "default",
1973 }));
1974
1975 let mut delivered_chunks = 0;
1976 let mut text = String::new();
1977 accumulate_agent_message_chunk(&non_chunk, &mut delivered_chunks, &mut text)
1978 .expect("non-chunk");
1979 accumulate_agent_message_chunk(&chunk, &mut delivered_chunks, &mut text).expect("chunk");
1980
1981 assert_eq!(text, "hello");
1982 }
1983
1984 #[test]
1985 fn prompt_text_capture_limit_rejects_overflowing_chunk() {
1986 let chunk = notification(json!({
1987 "sessionUpdate": "agent_message_chunk",
1988 "content": { "text": "abcd" },
1989 }));
1990 let mut delivered_chunks = 0;
1991 let mut text = "x".repeat(PROMPT_TEXT_CAPTURE_LIMIT_BYTES - 3);
1992 let error = accumulate_agent_message_chunk(&chunk, &mut delivered_chunks, &mut text)
1993 .expect_err("chunk should exceed prompt text cap");
1994 assert!(
1995 error.to_string().contains("prompt text capture is"),
1996 "unexpected error: {error}"
1997 );
1998 assert_eq!(text.len(), PROMPT_TEXT_CAPTURE_LIMIT_BYTES - 3);
1999 }
2000
2001 #[test]
2002 fn prompt_chunk_limit_rejects_more_tracked_chunks() {
2003 let chunk = notification(json!({
2004 "sessionUpdate": "agent_message_chunk",
2005 "content": { "text": "x" },
2006 }));
2007 let mut delivered_chunks = PROMPT_DELIVERED_CHUNK_LIMIT;
2008 let mut text = String::new();
2009 let error = accumulate_agent_message_chunk(&chunk, &mut delivered_chunks, &mut text)
2010 .expect_err("chunk should exceed chunk tracking cap");
2011 assert!(
2012 error
2013 .to_string()
2014 .contains("prompt chunk tracking limit exceeded"),
2015 "unexpected error: {error}"
2016 );
2017 assert!(text.is_empty());
2018 }
2019
2020 #[test]
2021 fn pending_session_request_count_tracks_registered_resolvers() {
2022 let (event_tx, _) = tokio::sync::broadcast::channel(1);
2023 let (permission_tx, _) = tokio::sync::broadcast::channel(1);
2024 let (agent_exit_tx, _) = tokio::sync::broadcast::channel(1);
2025 let entry = SessionEntry {
2026 agent_type: "pi".to_string(),
2027 modes: parking_lot::Mutex::new(None),
2028 config_options: parking_lot::Mutex::new(Vec::new()),
2029 capabilities: parking_lot::Mutex::new(None),
2030 agent_info: parking_lot::Mutex::new(None),
2031 config_overrides: parking_lot::Mutex::new(BTreeMap::new()),
2032 event_tx,
2033 permission_tx,
2034 agent_exit_tx,
2035 pending_permission_replies: scc::HashMap::new(),
2036 pending_session_request_lock: parking_lot::Mutex::new(()),
2037 pending_prompt_resolvers: scc::HashMap::new(),
2038 };
2039 let (first_tx, _first_rx) = tokio::sync::oneshot::channel();
2040 let (second_tx, _second_rx) = tokio::sync::oneshot::channel();
2041 let _ = entry.pending_prompt_resolvers.insert(1, first_tx);
2042 let _ = entry.pending_prompt_resolvers.insert(2, second_tx);
2043
2044 assert_eq!(pending_session_request_count(&entry), 2);
2045 }
2046}