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::ResolvedHostFunctions;
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>, host_function_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 host_function_reference = host_function_reference.trim();
571 if !host_function_reference.is_empty() {
572 parts.push(host_function_reference.to_string());
573 }
574 if parts.is_empty() {
575 None
576 } else {
577 Some(parts.join("\n\n"))
578 }
579}
580
581fn build_host_function_reference(host_functions: &[ResolvedHostFunctions]) -> String {
582 if host_functions.is_empty() {
583 return String::new();
584 }
585
586 let mut lines = vec![
587 String::from("## Available Host Functions"),
588 String::new(),
589 String::from("Run `agentos list-host-functions` to see all available host functions."),
590 String::new(),
591 ];
592
593 for collection in host_functions {
594 lines.push(format!("### {}", collection.name));
595 lines.push(String::new());
596 for host_function in &collection.functions {
597 let signature = build_host_function_flag_signature(&host_function.input_schema);
598 let suffix = if signature.is_empty() {
599 String::new()
600 } else {
601 format!(" {signature}")
602 };
603 lines.push(if host_function.description.is_empty() {
604 format!(
605 "- `agentos-{} {}{}`",
606 collection.name, host_function.name, suffix
607 )
608 } else {
609 format!(
610 "- `agentos-{} {}{}` - {}",
611 collection.name, host_function.name, suffix, host_function.description
612 )
613 });
614 }
615 lines.push(String::new());
616 lines.push(format!(
617 "Run `agentos-{} <function> --help` for details.",
618 collection.name
619 ));
620 lines.push(String::new());
621 }
622
623 lines.join("\n")
624}
625
626fn build_host_function_flag_signature(schema: &Value) -> String {
627 describe_host_function_flags(schema)
628 .into_iter()
629 .map(|flag| {
630 if flag.required {
631 format!("{} <{}>", flag.name, flag.value_type)
632 } else {
633 format!("[{} <{}>]", flag.name, flag.value_type)
634 }
635 })
636 .collect::<Vec<_>>()
637 .join(" ")
638}
639
640struct HostFunctionFlagDescription {
641 name: String,
642 value_type: String,
643 required: bool,
644}
645
646fn describe_host_function_flags(schema: &Value) -> Vec<HostFunctionFlagDescription> {
647 let properties = schema
648 .get("properties")
649 .and_then(Value::as_object)
650 .cloned()
651 .unwrap_or_default();
652 let required = schema
653 .get("required")
654 .and_then(Value::as_array)
655 .map(|items| {
656 items
657 .iter()
658 .filter_map(Value::as_str)
659 .map(str::to_owned)
660 .collect::<BTreeSet<_>>()
661 })
662 .unwrap_or_default();
663
664 properties
665 .into_iter()
666 .map(|(field_name, field_schema)| HostFunctionFlagDescription {
667 name: format!("--{}", camel_to_kebab(&field_name)),
668 value_type: describe_host_function_flag_type(&field_schema),
669 required: required.contains(&field_name),
670 })
671 .collect()
672}
673
674fn describe_host_function_flag_type(schema: &Value) -> String {
675 match json_schema_type(schema) {
676 Some("array") => {
677 let item_type = schema
678 .get("items")
679 .and_then(json_schema_type)
680 .unwrap_or("string");
681 format!("{item_type}[]")
682 }
683 Some("string") => schema
684 .get("enum")
685 .and_then(Value::as_array)
686 .map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
687 .filter(|values| !values.is_empty())
688 .map(|values| values.join("|"))
689 .unwrap_or_else(|| String::from("string")),
690 Some(other) => other.to_string(),
691 None => String::from("string"),
692 }
693}
694
695fn json_schema_type(schema: &Value) -> Option<&str> {
696 schema.get("type").and_then(Value::as_str)
697}
698
699fn camel_to_kebab(value: &str) -> String {
700 let mut output = String::new();
701 for (index, ch) in value.chars().enumerate() {
702 if ch.is_ascii_uppercase() && index > 0 {
703 output.push('-');
704 }
705 output.push(ch.to_ascii_lowercase());
706 }
707 output
708}
709
710impl AgentOs {
715 fn session_ownership(&self) -> wire::OwnershipScope {
717 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
718 connection_id: self.connection_id().to_string(),
719 session_id: self.wire_session_id().to_string(),
720 vm_id: self.vm_id().to_string(),
721 })
722 }
723
724 async fn send_acp_request(
725 &self,
726 request: AcpRequest,
727 ) -> std::result::Result<AcpResponse, ClientError> {
728 let payload = serde_bare::to_vec(&request).map_err(|error| {
729 ClientError::Sidecar(format!("failed to encode ACP request: {error}"))
730 })?;
731 let response = self
732 .transport()
733 .request_wire(
734 self.session_ownership(),
735 wire::RequestPayload::ExtEnvelope(wire::ExtEnvelope {
736 namespace: ACP_EXTENSION_NAMESPACE.to_string(),
737 payload,
738 }),
739 )
740 .await?;
741 let envelope = match response {
742 wire::ResponsePayload::ExtEnvelope(envelope) => envelope,
743 wire::ResponsePayload::RejectedResponse(rejected) => {
744 return Err(ClientError::Kernel {
745 code: rejected.code,
746 message: rejected.message,
747 });
748 }
749 other => {
750 return Err(ClientError::Sidecar(format!(
751 "unexpected ACP Ext response: {other:?}"
752 )));
753 }
754 };
755 if envelope.namespace != ACP_EXTENSION_NAMESPACE {
756 return Err(ClientError::Sidecar(format!(
757 "unexpected ACP Ext namespace: {}",
758 envelope.namespace
759 )));
760 }
761 let response: AcpResponse = serde_bare::from_slice(&envelope.payload).map_err(|error| {
762 ClientError::Sidecar(format!("failed to decode ACP response: {error}"))
763 })?;
764 match response {
765 AcpResponse::AcpErrorResponse(error) => Err(ClientError::AcpOperation {
766 code: error.code,
767 message: error.message,
768 }),
769 response => Ok(response),
770 }
771 }
772
773 pub async fn list_agents(&self) -> Result<Vec<AgentRegistryEntry>> {
778 let response = self
779 .send_acp_request(AcpRequest::AcpListAgentsRequest(AcpListAgentsRequest {
780 reserved: false,
781 }))
782 .await?;
783 let AcpResponse::AcpListAgentsResponse(listed) = response else {
784 return Err(unexpected_acp_response("AcpListAgentsRequest", response).into());
785 };
786 Ok(listed
787 .agents
788 .into_iter()
789 .map(|agent| AgentRegistryEntry {
790 id: agent.id,
791 installed: agent.installed,
792 })
793 .collect())
794 }
795
796 pub async fn open_session(&self, input: OpenSessionInput) -> Result<()> {
801 let caller_instructions = [
802 self.config().additional_instructions.as_deref(),
803 input.additional_instructions.as_deref(),
804 ]
805 .into_iter()
806 .flatten()
807 .map(str::trim)
808 .filter(|value| !value.is_empty())
809 .collect::<Vec<_>>()
810 .join("\n\n");
811 let host_function_reference = build_host_function_reference(self.host_functions());
812 let additional_instructions = combine_instructions(
813 (!caller_instructions.is_empty()).then_some(caller_instructions.as_str()),
814 &host_function_reference,
815 );
816 let response = self
817 .send_acp_request(AcpRequest::AcpOpenSessionRequest(AcpOpenSessionRequest {
818 session_id: input.session_id,
819 agent: input.agent,
820 cwd: input.cwd,
821 additional_directories: encode_optional_json(input.additional_directories)?,
822 env: encode_optional_json(input.env)?,
823 mcp_servers: encode_optional_json(input.mcp_servers)?,
824 permission_policy: input.permission_policy.map(permission_policy_wire),
825 skip_os_instructions: input.skip_os_instructions,
826 additional_instructions,
827 }))
828 .await?;
829 match response {
830 AcpResponse::AcpOpenSessionResponse(_) => Ok(()),
831 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
832 other => Err(unexpected_acp_response("AcpOpenSessionRequest", other).into()),
833 }
834 }
835
836 pub async fn get_session(&self, session_id: Option<&str>) -> Result<SessionInfo> {
838 let response = self
839 .send_acp_request(AcpRequest::AcpGetDurableSessionRequest(
840 AcpGetDurableSessionRequest {
841 session_id: session_id.map(ToOwned::to_owned),
842 },
843 ))
844 .await?;
845 match response {
846 AcpResponse::AcpGetDurableSessionResponse(response) => {
847 decode_session_info(response.session)
848 }
849 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
850 other => Err(unexpected_acp_response("AcpGetDurableSessionRequest", other).into()),
851 }
852 }
853
854 pub async fn list_sessions(&self, input: ListSessionsInput) -> Result<SessionPage> {
857 let response = self
858 .send_acp_request(AcpRequest::AcpListDurableSessionsRequest(
859 AcpListDurableSessionsRequest {
860 cursor: input.cursor,
861 limit: input.limit,
862 },
863 ))
864 .await?;
865 match response {
866 AcpResponse::AcpListDurableSessionsResponse(response) => Ok(SessionPage {
867 sessions: response
868 .sessions
869 .into_iter()
870 .map(decode_session_info)
871 .collect::<Result<Vec<_>>>()?,
872 next_cursor: response.next_cursor,
873 }),
874 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
875 other => Err(unexpected_acp_response("AcpListDurableSessionsRequest", other).into()),
876 }
877 }
878
879 pub async fn delete_session(&self, session_id: Option<&str>) -> Result<()> {
881 let response = self
882 .send_acp_request(AcpRequest::AcpDeleteSessionRequest(
883 AcpDeleteSessionRequest {
884 session_id: session_id.map(ToOwned::to_owned),
885 },
886 ))
887 .await?;
888 match response {
889 AcpResponse::AcpDeleteSessionResponse(_) => Ok(()),
890 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
891 other => Err(unexpected_acp_response("AcpDeleteSessionRequest", other).into()),
892 }
893 }
894
895 pub async fn unload_session(&self, session_id: Option<&str>) -> Result<()> {
897 let response = self
898 .send_acp_request(AcpRequest::AcpUnloadSessionRequest(
899 AcpUnloadSessionRequest {
900 session_id: session_id.map(ToOwned::to_owned),
901 },
902 ))
903 .await?;
904 match response {
905 AcpResponse::AcpUnloadSessionResponse(_) => Ok(()),
906 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
907 other => Err(unexpected_acp_response("AcpUnloadSessionRequest", other).into()),
908 }
909 }
910
911 pub async fn prompt(&self, input: PromptInput) -> Result<PromptResult> {
914 let response = self
915 .send_acp_request(AcpRequest::AcpPromptRequest(AcpPromptRequest {
916 session_id: input.session_id,
917 idempotency_key: input.idempotency_key,
918 content: encode_json(&input.content)?,
919 }))
920 .await?;
921 match response {
922 AcpResponse::AcpPromptResponse(response) => Ok(PromptResult {
923 session_id: response.session_id,
924 message: decode_optional_json(response.message)?,
925 stop_reason: decode_json(&serde_json::to_string(&response.stop_reason)?)?,
926 }),
927 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
928 other => Err(unexpected_acp_response("AcpPromptRequest", other).into()),
929 }
930 }
931
932 pub async fn cancel_prompt(&self, session_id: Option<&str>) -> Result<CancelPromptStatus> {
933 let response = self
934 .send_acp_request(AcpRequest::AcpCancelPromptRequest(AcpCancelPromptRequest {
935 session_id: session_id.map(ToOwned::to_owned),
936 }))
937 .await?;
938 match response {
939 AcpResponse::AcpCancelPromptResponse(response) => match response.status.as_str() {
940 "cancelled" => Ok(CancelPromptStatus::Cancelled),
941 "no_active_prompt" => Ok(CancelPromptStatus::NoActivePrompt),
942 status => Err(ClientError::Sidecar(format!(
943 "invalid cancelPrompt status: {status}"
944 ))
945 .into()),
946 },
947 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
948 other => Err(unexpected_acp_response("AcpCancelPromptRequest", other).into()),
949 }
950 }
951
952 pub async fn respond_permission(
953 &self,
954 session_id: &str,
955 request_id: &str,
956 option_id: &str,
957 ) -> Result<PermissionResponseStatus> {
958 let response = self
959 .send_acp_request(AcpRequest::AcpRespondPermissionRequest(
960 AcpRespondPermissionRequest {
961 session_id: session_id.to_owned(),
962 request_id: request_id.to_owned(),
963 option_id: option_id.to_owned(),
964 },
965 ))
966 .await?;
967 match response {
968 AcpResponse::AcpRespondPermissionResponse(response) => match response.status.as_str() {
969 "accepted" => Ok(PermissionResponseStatus::Accepted),
970 "not_pending" => Ok(PermissionResponseStatus::NotPending(
971 permission_terminal_reason(
972 response.reason.as_deref().unwrap_or("request_not_found"),
973 )?,
974 )),
975 status => Err(ClientError::Sidecar(format!(
976 "invalid respondPermission status: {status}"
977 ))
978 .into()),
979 },
980 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
981 other => Err(unexpected_acp_response("AcpRespondPermissionRequest", other).into()),
982 }
983 }
984
985 pub async fn read_history(&self, input: ReadHistoryInput) -> Result<HistoryPage> {
988 let response = self
989 .send_acp_request(AcpRequest::AcpReadHistoryRequest(AcpReadHistoryRequest {
990 session_id: input.session_id,
991 before: input.before,
992 after: input.after,
993 limit: input.limit,
994 }))
995 .await?;
996 match response {
997 AcpResponse::AcpHistoryPageResponse(response) => Ok(HistoryPage {
998 events: response
999 .events
1000 .into_iter()
1001 .map(decode_history_entry)
1002 .collect::<Result<Vec<_>>>()?,
1003 has_more_before: response.has_more_before,
1004 has_more_after: response.has_more_after,
1005 }),
1006 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1007 other => Err(unexpected_acp_response("AcpReadHistoryRequest", other).into()),
1008 }
1009 }
1010
1011 pub async fn get_session_config(&self, session_id: Option<&str>) -> Result<SessionConfig> {
1012 let response = self
1013 .send_acp_request(AcpRequest::AcpGetSessionConfigRequest(
1014 AcpGetSessionConfigRequest {
1015 session_id: session_id.map(ToOwned::to_owned),
1016 },
1017 ))
1018 .await?;
1019 decode_session_config_response(response, "AcpGetSessionConfigRequest")
1020 }
1021
1022 pub async fn set_session_config_option(
1023 &self,
1024 session_id: Option<&str>,
1025 config_id: &str,
1026 value: SessionConfigValue,
1027 ) -> Result<SessionConfig> {
1028 let response = self
1029 .send_acp_request(AcpRequest::AcpSetSessionConfigOptionRequest(
1030 AcpSetSessionConfigOptionRequest {
1031 session_id: session_id.map(ToOwned::to_owned),
1032 config_id: config_id.to_owned(),
1033 value: encode_json(&value)?,
1034 },
1035 ))
1036 .await?;
1037 decode_session_config_response(response, "AcpSetSessionConfigOptionRequest")
1038 }
1039
1040 pub async fn get_session_capabilities(
1041 &self,
1042 session_id: Option<&str>,
1043 ) -> Result<Option<SessionCapabilities>> {
1044 let response = self
1045 .send_acp_request(AcpRequest::AcpGetSessionCapabilitiesRequest(
1046 AcpGetSessionCapabilitiesRequest {
1047 session_id: session_id.map(ToOwned::to_owned),
1048 },
1049 ))
1050 .await?;
1051 match response {
1052 AcpResponse::AcpSessionCapabilitiesResponse(response) => response
1053 .capabilities
1054 .map(|raw| normalize_session_capabilities(decode_json(&raw)?))
1055 .transpose(),
1056 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1057 other => Err(unexpected_acp_response("AcpGetSessionCapabilitiesRequest", other).into()),
1058 }
1059 }
1060
1061 pub async fn get_session_agent_info(
1062 &self,
1063 session_id: Option<&str>,
1064 ) -> Result<Option<agent_client_protocol_schema::v1::Implementation>> {
1065 let response = self
1066 .send_acp_request(AcpRequest::AcpGetSessionAgentInfoRequest(
1067 AcpGetSessionAgentInfoRequest {
1068 session_id: session_id.map(ToOwned::to_owned),
1069 },
1070 ))
1071 .await?;
1072 match response {
1073 AcpResponse::AcpSessionAgentInfoResponse(response) => {
1074 decode_optional_json(response.agent_info)
1075 }
1076 AcpResponse::AcpErrorResponse(error) => Err(acp_operation_error(error).into()),
1077 other => Err(unexpected_acp_response("AcpGetSessionAgentInfoRequest", other).into()),
1078 }
1079 }
1080
1081 pub fn on_session_event(&self, session_id: Option<&str>) -> DurableSessionEventSubscription {
1086 let session_id = session_id.unwrap_or("main").to_owned();
1087 let rx = self.inner().durable_session_event_tx.subscribe();
1088 let stream = futures::stream::unfold((rx, session_id), |(mut rx, session_id)| async move {
1089 loop {
1090 match rx.recv().await {
1091 Ok(entry) if session_stream_id(&entry) == session_id => {
1092 return Some((Ok(entry), (rx, session_id)));
1093 }
1094 Ok(_) => continue,
1095 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1096 return Some((
1097 Err(SessionSubscriptionError::Lagged { skipped }),
1098 (rx, session_id),
1099 ));
1100 }
1101 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1102 }
1103 }
1104 });
1105 (Box::pin(stream), Subscription::noop())
1106 }
1107
1108 pub fn on_agent_exit(&self, session_id: Option<&str>) -> AgentExitSubscription {
1112 let session_id = session_id.map(ToOwned::to_owned);
1113 let rx = self.inner().durable_agent_exit_tx.subscribe();
1114 let stream = futures::stream::unfold((rx, session_id), |(mut rx, session_id)| async move {
1115 loop {
1116 match rx.recv().await {
1117 Ok(event)
1118 if session_id
1119 .as_ref()
1120 .is_none_or(|expected| expected == &event.session_id) =>
1121 {
1122 return Some((event, (rx, session_id)));
1123 }
1124 Ok(_) => continue,
1125 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
1126 tracing::warn!(skipped, "lagged durable ACP agent-exit subscription");
1127 }
1128 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
1129 }
1130 }
1131 });
1132 (Box::pin(stream), Subscription::noop())
1133 }
1134}
1135
1136fn permission_terminal_reason(reason: &str) -> Result<PermissionTerminalReason> {
1137 Ok(match reason {
1138 "already_resolved" => PermissionTerminalReason::AlreadyResolved,
1139 "prompt_cancelled" => PermissionTerminalReason::PromptCancelled,
1140 "adapter_exited" => PermissionTerminalReason::AdapterExited,
1141 "session_deleted" => PermissionTerminalReason::SessionDeleted,
1142 "vm_shutdown" => PermissionTerminalReason::VmShutdown,
1143 "request_not_found" => PermissionTerminalReason::RequestNotFound,
1144 other => anyhow::bail!("invalid permission terminal reason: {other}"),
1145 })
1146}