1use std::collections::{BTreeMap, BTreeSet};
12use std::pin::Pin;
13
14use anyhow::Result;
15use futures::Stream;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19pub use agent_client_protocol_schema::v1::McpServer as McpServerConfig;
20
21pub use agent_client_protocol_schema::v1::{
22 AvailableCommandsUpdate, ConfigOptionUpdate, ContentBlock, ContentChunk, CurrentModeUpdate,
23 Meta, PermissionOption, Plan, RequestPermissionOutcome, RequestPermissionRequest,
24 RequestPermissionResponse, SessionConfigOption, SessionInfoUpdate, SessionUpdate, StopReason,
25 ToolCall, ToolCallUpdate, UsageUpdate,
26};
27
28use agentos_protocol::generated::v1::{
29 AcpCancelPromptRequest, AcpDeleteSessionRequest, AcpDurableEvent, AcpDurableHistoryEntry,
30 AcpDurableSessionInfo, AcpGetDurableSessionRequest, AcpGetSessionAgentInfoRequest,
31 AcpGetSessionCapabilitiesRequest, AcpGetSessionConfigRequest, AcpListAgentsRequest,
32 AcpListDurableSessionsRequest, AcpOpenSessionRequest, AcpPromptRequest, AcpReadHistoryRequest,
33 AcpRequest, AcpRespondPermissionRequest, AcpResponse, AcpSetSessionConfigOptionRequest,
34 AcpUnloadSessionRequest,
35};
36use agentos_protocol::ACP_EXTENSION_NAMESPACE;
37use agentos_sidecar_client::wire;
38
39use crate::agent_os::AgentOs;
40use crate::config::Bindings;
41use crate::error::ClientError;
42use crate::stream::Subscription;
43pub type DurableSessionEventStream = Pin<
44 Box<
45 dyn Stream<Item = std::result::Result<SessionStreamEntry, SessionSubscriptionError>> + Send,
46 >,
47>;
48pub type DurableSessionEventSubscription = (DurableSessionEventStream, Subscription);
49pub type AgentExitStream = Pin<Box<dyn Stream<Item = AgentExitEvent> + Send>>;
50pub type AgentExitSubscription = (AgentExitStream, Subscription);
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum AgentRestartOutcome {
62 NotAttempted,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct AgentExitEvent {
67 #[serde(rename = "sessionId")]
68 pub session_id: String,
69 #[serde(rename = "agentType")]
70 pub agent_type: String,
71 #[serde(rename = "processId")]
72 pub process_id: String,
73 pub pid: Option<u32>,
75 #[serde(rename = "exitCode")]
77 pub exit_code: Option<i32>,
78 pub restart: AgentRestartOutcome,
79 #[serde(rename = "restartCount")]
80 pub restart_count: u32,
81 #[serde(rename = "maxRestarts")]
82 pub max_restarts: u32,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct AgentRegistryEntry {
96 pub id: String,
97 pub installed: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct OpenSessionInput {
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub session_id: Option<String>,
109 pub agent: String,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub cwd: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub additional_directories: Option<Vec<String>>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub env: Option<BTreeMap<String, String>>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub mcp_servers: Option<Vec<McpServerConfig>>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub permission_policy: Option<PermissionPolicy>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub skip_os_instructions: Option<bool>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub additional_instructions: Option<String>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130pub enum PermissionPolicy {
131 RejectAll,
132 Ask,
133 AllowAll,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[serde(tag = "status", rename_all = "snake_case")]
138pub enum SessionState {
139 Idle,
140 Running {
141 #[serde(rename = "startedAt")]
142 started_at: String,
143 },
144 Waiting {
145 #[serde(rename = "waitingSince")]
146 waiting_since: String,
147 requests: Vec<PendingPermissionRequest>,
148 },
149 Failed {
150 error: Value,
151 },
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct SessionInfo {
158 pub session_id: String,
159 pub agent: String,
160 pub cwd: String,
161 pub additional_directories: Vec<String>,
162 pub state: SessionState,
163 pub latest_sequence: u64,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub title: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub metadata: Option<BTreeMap<String, Value>>,
168 pub created_at: String,
169 pub updated_at: String,
170}
171
172#[derive(Debug, Clone, Default, PartialEq, Eq)]
173pub struct ListSessionsInput {
174 pub cursor: Option<String>,
175 pub limit: Option<u32>,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct SessionPage {
181 pub sessions: Vec<SessionInfo>,
182 pub next_cursor: Option<String>,
183}
184
185#[derive(Debug, Clone, PartialEq)]
186pub struct PromptInput {
187 pub session_id: Option<String>,
188 pub idempotency_key: Option<String>,
189 pub content: Vec<ContentBlock>,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct AgentMessage {
194 pub id: String,
195 pub role: String,
196 pub content: Vec<ContentBlock>,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct PromptResult {
202 pub session_id: String,
203 pub message: Option<AgentMessage>,
204 pub stop_reason: StopReason,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum CancelPromptStatus {
210 Cancelled,
211 NoActivePrompt,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum PermissionResponseStatus {
216 Accepted,
217 NotPending(PermissionTerminalReason),
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum PermissionTerminalReason {
223 AlreadyResolved,
224 PromptCancelled,
225 AdapterExited,
226 SessionDeleted,
227 VmShutdown,
228 RequestNotFound,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "snake_case")]
233pub enum PermissionEventStatus {
234 Accepted,
235 NotPending,
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[serde(tag = "type", rename_all = "snake_case")]
240pub enum DurableSessionEvent {
241 UserMessageChunk(ContentChunk),
242 AgentMessageChunk(ContentChunk),
243 AgentThoughtChunk(ContentChunk),
244 ToolCall(ToolCall),
245 ToolCallUpdate(ToolCallUpdate),
246 Plan(Plan),
247 AvailableCommandsUpdate(AvailableCommandsUpdate),
248 CurrentModeUpdate(CurrentModeUpdate),
249 ConfigOptionUpdate(ConfigOptionUpdate),
250 SessionInfoUpdate(SessionInfoUpdate),
251 UsageUpdate(UsageUpdate),
252 PermissionRequest {
253 #[serde(rename = "requestId")]
254 request_id: String,
255 options: Vec<PermissionOption>,
256 #[serde(rename = "toolCall")]
257 tool_call: ToolCallUpdate,
258 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
259 meta: Option<Meta>,
260 },
261 PermissionResponse {
262 #[serde(rename = "requestId")]
263 request_id: String,
264 outcome: RequestPermissionOutcome,
265 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
266 meta: Option<Meta>,
267 status: PermissionEventStatus,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 reason: Option<PermissionTerminalReason>,
270 },
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[serde(tag = "type", rename_all = "snake_case")]
275pub enum EphemeralSessionEvent {
276 AgentMessageChunk(ContentChunk),
277 AgentThoughtChunk(ContentChunk),
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct PendingPermissionRequest {
283 pub request_id: String,
284 pub options: Vec<PermissionOption>,
285 pub tool_call: ToolCallUpdate,
286 #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
287 pub meta: Option<Meta>,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
294pub enum SessionSubscriptionError {
295 #[error("session subscription lagged by {skipped} entries; resume durable history with read_history")]
296 Lagged { skipped: u64 },
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct DurableSessionEventEntry {
302 pub durability: DurableEventKind,
303 pub session_id: String,
304 pub sequence: u64,
305 pub timestamp: String,
306 #[serde(flatten)]
307 pub event: DurableSessionEvent,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum DurableEventKind {
313 Durable,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(rename_all = "snake_case")]
318pub enum EphemeralEventKind {
319 Ephemeral,
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct EphemeralSessionEventEntry {
325 pub durability: EphemeralEventKind,
326 pub session_id: String,
327 pub after_sequence: u64,
328 #[serde(flatten)]
329 pub event: EphemeralSessionEvent,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333#[serde(untagged)]
334pub enum SessionStreamEntry {
335 Durable(DurableSessionEventEntry),
336 Ephemeral(EphemeralSessionEventEntry),
337}
338
339#[derive(Debug, Clone, Default, PartialEq, Eq)]
340pub struct ReadHistoryInput {
341 pub session_id: Option<String>,
342 pub before: Option<u64>,
343 pub after: Option<u64>,
344 pub limit: Option<u32>,
345}
346
347#[derive(Debug, Clone, PartialEq)]
348pub struct HistoryPage {
349 pub events: Vec<DurableSessionEventEntry>,
350 pub has_more_before: bool,
351 pub has_more_after: bool,
352}
353
354#[derive(Debug, Clone, PartialEq)]
355pub struct SessionConfig {
356 pub revision: u64,
357 pub options: Vec<SessionConfigOption>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(untagged)]
364pub enum SessionConfigValue {
365 String(String),
366 Boolean(bool),
367}
368
369impl From<String> for SessionConfigValue {
370 fn from(value: String) -> Self {
371 Self::String(value)
372 }
373}
374
375impl From<&str> for SessionConfigValue {
376 fn from(value: &str) -> Self {
377 Self::String(value.to_owned())
378 }
379}
380
381impl From<bool> for SessionConfigValue {
382 fn from(value: bool) -> Self {
383 Self::Boolean(value)
384 }
385}
386
387#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct SessionCapabilities {
390 pub protocol_version: u64,
391 pub load_session: bool,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub prompt: Option<Value>,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub mcp: Option<Value>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub session: Option<Value>,
398 #[serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)]
399 pub extensions: BTreeMap<String, Value>,
400}
401
402fn encode_json<T: Serialize>(value: &T) -> Result<String> {
403 serde_json::to_string(value).map_err(Into::into)
404}
405
406fn encode_optional_json<T: Serialize>(value: Option<T>) -> Result<Option<String>> {
407 value.as_ref().map(encode_json).transpose()
408}
409
410fn decode_json<T: for<'de> Deserialize<'de>>(value: &str) -> Result<T> {
411 serde_json::from_str(value).map_err(Into::into)
412}
413
414fn decode_optional_json<T: for<'de> Deserialize<'de>>(value: Option<String>) -> Result<Option<T>> {
415 value.as_deref().map(decode_json).transpose()
416}
417
418fn permission_policy_wire(policy: PermissionPolicy) -> String {
419 match policy {
420 PermissionPolicy::RejectAll => String::from("reject_all"),
421 PermissionPolicy::Ask => String::from("ask"),
422 PermissionPolicy::AllowAll => String::from("allow_all"),
423 }
424}
425
426fn session_stream_id(entry: &SessionStreamEntry) -> &str {
427 match entry {
428 SessionStreamEntry::Durable(entry) => &entry.session_id,
429 SessionStreamEntry::Ephemeral(entry) => &entry.session_id,
430 }
431}
432
433fn decode_session_info(value: AcpDurableSessionInfo) -> Result<SessionInfo> {
434 Ok(SessionInfo {
435 session_id: value.session_id,
436 agent: value.agent,
437 cwd: value.cwd,
438 additional_directories: decode_json(&value.additional_directories)?,
439 state: decode_json(&value.state)?,
440 latest_sequence: value.latest_sequence,
441 title: value.title,
442 metadata: decode_optional_json(value.metadata)?,
443 created_at: value.created_at,
444 updated_at: value.updated_at,
445 })
446}
447
448fn decode_history_entry(value: AcpDurableHistoryEntry) -> Result<DurableSessionEventEntry> {
449 Ok(DurableSessionEventEntry {
450 durability: DurableEventKind::Durable,
451 session_id: value.session_id,
452 sequence: value.sequence,
453 timestamp: value.timestamp,
454 event: decode_durable_event(value.event)?,
455 })
456}
457
458pub(crate) fn decode_durable_event(value: AcpDurableEvent) -> Result<DurableSessionEvent> {
459 match value {
460 AcpDurableEvent::AcpDurableSessionUpdate(event) => {
461 let update: SessionUpdate = decode_json(&event.update)?;
462 let mut value = serde_json::to_value(update)?;
463 let object = value
464 .as_object_mut()
465 .ok_or_else(|| anyhow::anyhow!("ACP session update must serialize as an object"))?;
466 let update_type = object
467 .remove("sessionUpdate")
468 .ok_or_else(|| anyhow::anyhow!("ACP session update is missing sessionUpdate"))?;
469 object.insert(String::from("type"), update_type);
470 serde_json::from_value(value).map_err(Into::into)
471 }
472 AcpDurableEvent::AcpDurablePermissionRequest(event) => {
473 let request: RequestPermissionRequest = decode_json(&event.request)?;
474 Ok(DurableSessionEvent::PermissionRequest {
475 request_id: event.request_id,
476 options: request.options,
477 tool_call: request.tool_call,
478 meta: request.meta,
479 })
480 }
481 AcpDurableEvent::AcpDurablePermissionResponse(event) => {
482 let response: RequestPermissionResponse = decode_json(&event.response)?;
483 let status = match event.status.as_str() {
484 "accepted" => PermissionEventStatus::Accepted,
485 "not_pending" => PermissionEventStatus::NotPending,
486 status => anyhow::bail!("invalid permission response event status: {status}"),
487 };
488 let reason = event
489 .reason
490 .as_deref()
491 .map(permission_terminal_reason)
492 .transpose()?;
493 Ok(DurableSessionEvent::PermissionResponse {
494 request_id: event.request_id,
495 outcome: response.outcome,
496 meta: response.meta,
497 status,
498 reason,
499 })
500 }
501 }
502}
503
504fn decode_session_config_response(response: AcpResponse, operation: &str) -> Result<SessionConfig> {
505 match response {
506 AcpResponse::AcpSessionConfigResponse(response) => Ok(SessionConfig {
507 revision: response.revision,
508 options: decode_json(&response.options)?,
509 }),
510 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
511 other => Err(unexpected_acp_response(operation, other).into()),
512 }
513}
514
515fn normalize_session_capabilities(value: Value) -> Result<SessionCapabilities> {
516 let object = value.as_object().ok_or_else(|| {
517 ClientError::Sidecar(String::from("malformed ACP agentCapabilities JSON"))
518 })?;
519 let prompt = object
520 .get("promptCapabilities")
521 .filter(|value| value.is_object())
522 .cloned();
523 let mcp = object
524 .get("mcpCapabilities")
525 .filter(|value| value.is_object())
526 .cloned();
527 let session = object
528 .get("sessionCapabilities")
529 .filter(|value| value.is_object())
530 .cloned();
531 let extensions = object
532 .iter()
533 .filter(|(key, _)| {
534 !matches!(
535 key.as_str(),
536 "loadSession" | "promptCapabilities" | "mcpCapabilities" | "sessionCapabilities"
537 )
538 })
539 .map(|(key, value)| (key.clone(), value.clone()))
540 .collect();
541 Ok(SessionCapabilities {
542 protocol_version: crate::ACP_PROTOCOL_VERSION,
543 load_session: object
544 .get("loadSession")
545 .and_then(Value::as_bool)
546 .unwrap_or(false),
547 prompt,
548 mcp,
549 session,
550 extensions,
551 })
552}
553
554fn acp_operation_error(error: agentos_protocol::generated::v1::AcpErrorResponse) -> ClientError {
555 ClientError::AcpOperation {
556 code: error.code,
557 message: error.message,
558 }
559}
560
561fn unexpected_acp_response(operation: &str, response: AcpResponse) -> ClientError {
562 ClientError::Sidecar(format!("unexpected response to {operation}: {response:?}"))
563}
564
565fn combine_instructions(additional: Option<&str>, binding_reference: &str) -> Option<String> {
566 let mut parts = Vec::new();
567 if let Some(additional) = additional.map(str::trim).filter(|value| !value.is_empty()) {
568 parts.push(additional.to_string());
569 }
570 let binding_reference = binding_reference.trim();
571 if !binding_reference.is_empty() {
572 parts.push(binding_reference.to_string());
573 }
574 if parts.is_empty() {
575 None
576 } else {
577 Some(parts.join("\n\n"))
578 }
579}
580
581fn build_binding_reference(bindings: &[Bindings]) -> String {
582 if bindings.is_empty() {
583 return String::new();
584 }
585
586 let mut lines = vec![
587 String::from("## Available Host Bindings"),
588 String::new(),
589 String::from("Run `agentos list-bindings` to see all available bindings."),
590 String::new(),
591 ];
592
593 for collection in bindings {
594 lines.push(format!("### {}", collection.name));
595 lines.push(String::new());
596 lines.push(collection.description.clone());
597 lines.push(String::new());
598 for binding in &collection.bindings {
599 let signature = build_binding_flag_signature(&binding.input_schema);
600 let suffix = if signature.is_empty() {
601 String::new()
602 } else {
603 format!(" {signature}")
604 };
605 lines.push(format!(
606 "- `agentos-{} {}{}` — {}",
607 collection.name, binding.name, suffix, binding.description
608 ));
609 }
610 lines.push(String::new());
611 lines.push(format!(
612 "Run `agentos-{} <binding> --help` for details.",
613 collection.name
614 ));
615 lines.push(String::new());
616 }
617
618 lines.join("\n")
619}
620
621fn build_binding_flag_signature(schema: &Value) -> String {
622 describe_binding_flags(schema)
623 .into_iter()
624 .map(|flag| {
625 if flag.required {
626 format!("{} <{}>", flag.name, flag.value_type)
627 } else {
628 format!("[{} <{}>]", flag.name, flag.value_type)
629 }
630 })
631 .collect::<Vec<_>>()
632 .join(" ")
633}
634
635struct BindingFlagDescription {
636 name: String,
637 value_type: String,
638 required: bool,
639}
640
641fn describe_binding_flags(schema: &Value) -> Vec<BindingFlagDescription> {
642 let properties = schema
643 .get("properties")
644 .and_then(Value::as_object)
645 .cloned()
646 .unwrap_or_default();
647 let required = schema
648 .get("required")
649 .and_then(Value::as_array)
650 .map(|items| {
651 items
652 .iter()
653 .filter_map(Value::as_str)
654 .map(str::to_owned)
655 .collect::<BTreeSet<_>>()
656 })
657 .unwrap_or_default();
658
659 properties
660 .into_iter()
661 .map(|(field_name, field_schema)| BindingFlagDescription {
662 name: format!("--{}", camel_to_kebab(&field_name)),
663 value_type: describe_binding_flag_type(&field_schema),
664 required: required.contains(&field_name),
665 })
666 .collect()
667}
668
669fn describe_binding_flag_type(schema: &Value) -> String {
670 match json_schema_type(schema) {
671 Some("array") => {
672 let item_type = schema
673 .get("items")
674 .and_then(json_schema_type)
675 .unwrap_or("string");
676 format!("{item_type}[]")
677 }
678 Some("string") => schema
679 .get("enum")
680 .and_then(Value::as_array)
681 .map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
682 .filter(|values| !values.is_empty())
683 .map(|values| values.join("|"))
684 .unwrap_or_else(|| String::from("string")),
685 Some(other) => other.to_string(),
686 None => String::from("string"),
687 }
688}
689
690fn json_schema_type(schema: &Value) -> Option<&str> {
691 schema.get("type").and_then(Value::as_str)
692}
693
694fn camel_to_kebab(value: &str) -> String {
695 let mut output = String::new();
696 for (index, ch) in value.chars().enumerate() {
697 if ch.is_ascii_uppercase() && index > 0 {
698 output.push('-');
699 }
700 output.push(ch.to_ascii_lowercase());
701 }
702 output
703}
704
705impl AgentOs {
710 fn session_ownership(&self) -> wire::OwnershipScope {
712 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
713 connection_id: self.connection_id().to_string(),
714 session_id: self.wire_session_id().to_string(),
715 vm_id: self.vm_id().to_string(),
716 })
717 }
718
719 async fn send_acp_request(
720 &self,
721 request: AcpRequest,
722 ) -> std::result::Result<AcpResponse, ClientError> {
723 let payload = serde_bare::to_vec(&request).map_err(|error| {
724 ClientError::Sidecar(format!("failed to encode ACP request: {error}"))
725 })?;
726 let response = self
727 .transport()
728 .request_wire(
729 self.session_ownership(),
730 wire::RequestPayload::ExtEnvelope(wire::ExtEnvelope {
731 namespace: ACP_EXTENSION_NAMESPACE.to_string(),
732 payload,
733 }),
734 )
735 .await?;
736 let envelope = match response {
737 wire::ResponsePayload::ExtEnvelope(envelope) => envelope,
738 wire::ResponsePayload::RejectedResponse(rejected) => {
739 return Err(ClientError::Kernel {
740 code: rejected.code,
741 message: rejected.message,
742 });
743 }
744 other => {
745 return Err(ClientError::Sidecar(format!(
746 "unexpected ACP Ext response: {other:?}"
747 )));
748 }
749 };
750 if envelope.namespace != ACP_EXTENSION_NAMESPACE {
751 return Err(ClientError::Sidecar(format!(
752 "unexpected ACP Ext namespace: {}",
753 envelope.namespace
754 )));
755 }
756 let response: AcpResponse = serde_bare::from_slice(&envelope.payload).map_err(|error| {
757 ClientError::Sidecar(format!("failed to decode ACP response: {error}"))
758 })?;
759 match response {
760 AcpResponse::AcpErrorResponse(error) => Err(ClientError::AcpOperation {
761 code: error.code,
762 message: error.message,
763 }),
764 response => Ok(response),
765 }
766 }
767
768 pub async fn list_agents(&self) -> Result<Vec<AgentRegistryEntry>> {
773 let response = self
774 .send_acp_request(AcpRequest::AcpListAgentsRequest(AcpListAgentsRequest {
775 reserved: false,
776 }))
777 .await?;
778 let AcpResponse::AcpListAgentsResponse(listed) = response else {
779 return Err(unexpected_acp_response("AcpListAgentsRequest", response).into());
780 };
781 Ok(listed
782 .agents
783 .into_iter()
784 .map(|agent| AgentRegistryEntry {
785 id: agent.id,
786 installed: agent.installed,
787 })
788 .collect())
789 }
790
791 pub async fn open_session(&self, input: OpenSessionInput) -> Result<()> {
796 let caller_instructions = [
797 self.config().additional_instructions.as_deref(),
798 input.additional_instructions.as_deref(),
799 ]
800 .into_iter()
801 .flatten()
802 .map(str::trim)
803 .filter(|value| !value.is_empty())
804 .collect::<Vec<_>>()
805 .join("\n\n");
806 let binding_reference = build_binding_reference(&self.config().bindings);
807 let additional_instructions = combine_instructions(
808 (!caller_instructions.is_empty()).then_some(caller_instructions.as_str()),
809 &binding_reference,
810 );
811 let response = self
812 .send_acp_request(AcpRequest::AcpOpenSessionRequest(AcpOpenSessionRequest {
813 session_id: input.session_id,
814 agent: input.agent,
815 cwd: input.cwd,
816 additional_directories: encode_optional_json(input.additional_directories)?,
817 env: encode_optional_json(input.env)?,
818 mcp_servers: encode_optional_json(input.mcp_servers)?,
819 permission_policy: input.permission_policy.map(permission_policy_wire),
820 skip_os_instructions: input.skip_os_instructions,
821 additional_instructions,
822 }))
823 .await?;
824 match response {
825 AcpResponse::AcpOpenSessionResponse(_) => Ok(()),
826 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
827 other => Err(unexpected_acp_response("AcpOpenSessionRequest", other).into()),
828 }
829 }
830
831 pub async fn get_session(&self, session_id: Option<&str>) -> Result<SessionInfo> {
833 let response = self
834 .send_acp_request(AcpRequest::AcpGetDurableSessionRequest(
835 AcpGetDurableSessionRequest {
836 session_id: session_id.map(ToOwned::to_owned),
837 },
838 ))
839 .await?;
840 match response {
841 AcpResponse::AcpGetDurableSessionResponse(response) => {
842 decode_session_info(response.session)
843 }
844 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
845 other => Err(unexpected_acp_response("AcpGetDurableSessionRequest", other).into()),
846 }
847 }
848
849 pub async fn list_sessions(&self, input: ListSessionsInput) -> Result<SessionPage> {
852 let response = self
853 .send_acp_request(AcpRequest::AcpListDurableSessionsRequest(
854 AcpListDurableSessionsRequest {
855 cursor: input.cursor,
856 limit: input.limit,
857 },
858 ))
859 .await?;
860 match response {
861 AcpResponse::AcpListDurableSessionsResponse(response) => Ok(SessionPage {
862 sessions: response
863 .sessions
864 .into_iter()
865 .map(decode_session_info)
866 .collect::<Result<Vec<_>>>()?,
867 next_cursor: response.next_cursor,
868 }),
869 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
870 other => Err(unexpected_acp_response("AcpListDurableSessionsRequest", other).into()),
871 }
872 }
873
874 pub async fn delete_session(&self, session_id: Option<&str>) -> Result<()> {
876 let response = self
877 .send_acp_request(AcpRequest::AcpDeleteSessionRequest(
878 AcpDeleteSessionRequest {
879 session_id: session_id.map(ToOwned::to_owned),
880 },
881 ))
882 .await?;
883 match response {
884 AcpResponse::AcpDeleteSessionResponse(_) => Ok(()),
885 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
886 other => Err(unexpected_acp_response("AcpDeleteSessionRequest", other).into()),
887 }
888 }
889
890 pub async fn unload_session(&self, session_id: Option<&str>) -> Result<()> {
892 let response = self
893 .send_acp_request(AcpRequest::AcpUnloadSessionRequest(
894 AcpUnloadSessionRequest {
895 session_id: session_id.map(ToOwned::to_owned),
896 },
897 ))
898 .await?;
899 match response {
900 AcpResponse::AcpUnloadSessionResponse(_) => Ok(()),
901 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
902 other => Err(unexpected_acp_response("AcpUnloadSessionRequest", other).into()),
903 }
904 }
905
906 pub async fn prompt(&self, input: PromptInput) -> Result<PromptResult> {
909 let response = self
910 .send_acp_request(AcpRequest::AcpPromptRequest(AcpPromptRequest {
911 session_id: input.session_id,
912 idempotency_key: input.idempotency_key,
913 content: encode_json(&input.content)?,
914 }))
915 .await?;
916 match response {
917 AcpResponse::AcpPromptResponse(response) => Ok(PromptResult {
918 session_id: response.session_id,
919 message: decode_optional_json(response.message)?,
920 stop_reason: decode_json(&serde_json::to_string(&response.stop_reason)?)?,
921 }),
922 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
923 other => Err(unexpected_acp_response("AcpPromptRequest", other).into()),
924 }
925 }
926
927 pub async fn cancel_prompt(&self, session_id: Option<&str>) -> Result<CancelPromptStatus> {
928 let response = self
929 .send_acp_request(AcpRequest::AcpCancelPromptRequest(AcpCancelPromptRequest {
930 session_id: session_id.map(ToOwned::to_owned),
931 }))
932 .await?;
933 match response {
934 AcpResponse::AcpCancelPromptResponse(response) => match response.status.as_str() {
935 "cancelled" => Ok(CancelPromptStatus::Cancelled),
936 "no_active_prompt" => Ok(CancelPromptStatus::NoActivePrompt),
937 status => Err(ClientError::Sidecar(format!(
938 "invalid cancelPrompt status: {status}"
939 ))
940 .into()),
941 },
942 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
943 other => Err(unexpected_acp_response("AcpCancelPromptRequest", other).into()),
944 }
945 }
946
947 pub async fn respond_permission(
948 &self,
949 session_id: &str,
950 request_id: &str,
951 option_id: &str,
952 ) -> Result<PermissionResponseStatus> {
953 let response = self
954 .send_acp_request(AcpRequest::AcpRespondPermissionRequest(
955 AcpRespondPermissionRequest {
956 session_id: session_id.to_owned(),
957 request_id: request_id.to_owned(),
958 option_id: option_id.to_owned(),
959 },
960 ))
961 .await?;
962 match response {
963 AcpResponse::AcpRespondPermissionResponse(response) => match response.status.as_str() {
964 "accepted" => Ok(PermissionResponseStatus::Accepted),
965 "not_pending" => Ok(PermissionResponseStatus::NotPending(
966 permission_terminal_reason(
967 response.reason.as_deref().unwrap_or("request_not_found"),
968 )?,
969 )),
970 status => Err(ClientError::Sidecar(format!(
971 "invalid respondPermission status: {status}"
972 ))
973 .into()),
974 },
975 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
976 other => Err(unexpected_acp_response("AcpRespondPermissionRequest", other).into()),
977 }
978 }
979
980 pub async fn read_history(&self, input: ReadHistoryInput) -> Result<HistoryPage> {
983 let response = self
984 .send_acp_request(AcpRequest::AcpReadHistoryRequest(AcpReadHistoryRequest {
985 session_id: input.session_id,
986 before: input.before,
987 after: input.after,
988 limit: input.limit,
989 }))
990 .await?;
991 match response {
992 AcpResponse::AcpHistoryPageResponse(response) => Ok(HistoryPage {
993 events: response
994 .events
995 .into_iter()
996 .map(decode_history_entry)
997 .collect::<Result<Vec<_>>>()?,
998 has_more_before: response.has_more_before,
999 has_more_after: response.has_more_after,
1000 }),
1001 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1002 other => Err(unexpected_acp_response("AcpReadHistoryRequest", other).into()),
1003 }
1004 }
1005
1006 pub async fn get_session_config(&self, session_id: Option<&str>) -> Result<SessionConfig> {
1007 let response = self
1008 .send_acp_request(AcpRequest::AcpGetSessionConfigRequest(
1009 AcpGetSessionConfigRequest {
1010 session_id: session_id.map(ToOwned::to_owned),
1011 },
1012 ))
1013 .await?;
1014 decode_session_config_response(response, "AcpGetSessionConfigRequest")
1015 }
1016
1017 pub async fn set_session_config_option(
1018 &self,
1019 session_id: Option<&str>,
1020 config_id: &str,
1021 value: SessionConfigValue,
1022 ) -> Result<SessionConfig> {
1023 let response = self
1024 .send_acp_request(AcpRequest::AcpSetSessionConfigOptionRequest(
1025 AcpSetSessionConfigOptionRequest {
1026 session_id: session_id.map(ToOwned::to_owned),
1027 config_id: config_id.to_owned(),
1028 value: encode_json(&value)?,
1029 },
1030 ))
1031 .await?;
1032 decode_session_config_response(response, "AcpSetSessionConfigOptionRequest")
1033 }
1034
1035 pub async fn get_session_capabilities(
1036 &self,
1037 session_id: Option<&str>,
1038 ) -> Result<Option<SessionCapabilities>> {
1039 let response = self
1040 .send_acp_request(AcpRequest::AcpGetSessionCapabilitiesRequest(
1041 AcpGetSessionCapabilitiesRequest {
1042 session_id: session_id.map(ToOwned::to_owned),
1043 },
1044 ))
1045 .await?;
1046 match response {
1047 AcpResponse::AcpSessionCapabilitiesResponse(response) => response
1048 .capabilities
1049 .map(|raw| normalize_session_capabilities(decode_json(&raw)?))
1050 .transpose(),
1051 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1052 other => Err(unexpected_acp_response("AcpGetSessionCapabilitiesRequest", other).into()),
1053 }
1054 }
1055
1056 pub async fn get_session_agent_info(
1057 &self,
1058 session_id: Option<&str>,
1059 ) -> Result<Option<agent_client_protocol_schema::v1::Implementation>> {
1060 let response = self
1061 .send_acp_request(AcpRequest::AcpGetSessionAgentInfoRequest(
1062 AcpGetSessionAgentInfoRequest {
1063 session_id: session_id.map(ToOwned::to_owned),
1064 },
1065 ))
1066 .await?;
1067 match response {
1068 AcpResponse::AcpSessionAgentInfoResponse(response) => {
1069 decode_optional_json(response.agent_info)
1070 }
1071 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1072 other => Err(unexpected_acp_response("AcpGetSessionAgentInfoRequest", other).into()),
1073 }
1074 }
1075
1076 pub fn on_session_event(&self, session_id: Option<&str>) -> DurableSessionEventSubscription {
1081 let session_id = session_id.unwrap_or("main").to_owned();
1082 let rx = self.inner().durable_session_event_tx.subscribe();
1083 let stream = futures::stream::unfold((rx, session_id), |(mut rx, session_id)| async move {
1084 loop {
1085 match rx.recv().await {
1086 Ok(entry) if session_stream_id(&entry) == session_id => {
1087 return Some((Ok(entry), (rx, session_id)));
1088 }
1089 Ok(_) => continue,
1090 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1091 return Some((
1092 Err(SessionSubscriptionError::Lagged { skipped }),
1093 (rx, session_id),
1094 ));
1095 }
1096 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1097 }
1098 }
1099 });
1100 (Box::pin(stream), Subscription::noop())
1101 }
1102
1103 pub fn on_agent_exit(&self, session_id: Option<&str>) -> AgentExitSubscription {
1107 let session_id = session_id.map(ToOwned::to_owned);
1108 let rx = self.inner().durable_agent_exit_tx.subscribe();
1109 let stream = futures::stream::unfold((rx, session_id), |(mut rx, session_id)| async move {
1110 loop {
1111 match rx.recv().await {
1112 Ok(event)
1113 if session_id
1114 .as_ref()
1115 .is_none_or(|expected| expected == &event.session_id) =>
1116 {
1117 return Some((event, (rx, session_id)));
1118 }
1119 Ok(_) => continue,
1120 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1121 tracing::warn!(skipped, "lagged durable ACP agent-exit subscription");
1122 }
1123 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1124 }
1125 }
1126 });
1127 (Box::pin(stream), Subscription::noop())
1128 }
1129}
1130
1131fn permission_terminal_reason(reason: &str) -> Result<PermissionTerminalReason> {
1132 Ok(match reason {
1133 "already_resolved" => PermissionTerminalReason::AlreadyResolved,
1134 "prompt_cancelled" => PermissionTerminalReason::PromptCancelled,
1135 "adapter_exited" => PermissionTerminalReason::AdapterExited,
1136 "session_deleted" => PermissionTerminalReason::SessionDeleted,
1137 "vm_shutdown" => PermissionTerminalReason::VmShutdown,
1138 "request_not_found" => PermissionTerminalReason::RequestNotFound,
1139 other => anyhow::bail!("invalid permission terminal reason: {other}"),
1140 })
1141}