use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::bounds;
use crate::env::SecretResolver;
use crate::error::AdapterError;
use crate::model::RpcCallSpec;
use crate::model::{
AgentEvent, Capabilities, CommandSpec, RpcLaneSpec, SessionRequest, TurnOutcome,
};
use crate::observe::SharedObserver;
use crate::rpc::{CallFailure, JsonRpcPeer, RpcDelegate, RpcErrorObject};
use crate::supervision::SupervisedChild;
use crate::transport::stdio;
#[async_trait::async_trait]
pub trait RpcSessionDelegate: Send + Sync {
async fn on_event(&self, _event: AgentEvent) {}
async fn on_request(&self, method: &str, _params: Value) -> Result<Value, RpcErrorObject> {
Err(RpcErrorObject::method_not_found(method))
}
}
pub struct RpcLane {
pub command: CommandSpec,
pub spec: RpcLaneSpec,
pub secrets: Arc<dyn SecretResolver>,
pub observer: SharedObserver,
}
pub struct RpcSession {
peer: Arc<JsonRpcPeer>,
child: Mutex<SupervisedChild>,
spec: RpcLaneSpec,
session_id: Option<String>,
cwd: String,
turn_lock: Mutex<()>,
}
impl RpcLane {
pub async fn open(
&self,
request: SessionRequest,
delegate: Arc<dyn RpcSessionDelegate>,
) -> Result<RpcSession, AdapterError> {
let env = crate::env::resolve(&self.command.env, &request.env, &*self.secrets).await?;
let mut command = SupervisedChild::command(&self.command, &env, request.cwd.as_deref());
let mut child = SupervisedChild::spawn(&mut command, stdio::PROTOCOL_IO)?;
let (stdin, stdout, stderr) = stdio::protocol_halves(&mut child)?;
if let Some(stderr) = stderr {
tokio::spawn(bounds::drain_stderr(stderr, self.observer.clone()));
}
let text_method = self.spec.text_event_method.clone();
let observer = self.observer.clone();
struct Inner {
delegate: Arc<dyn RpcSessionDelegate>,
text_method: Option<String>,
}
#[async_trait::async_trait]
impl RpcDelegate for Inner {
async fn on_request(
&self,
method: &str,
params: Value,
) -> Result<Value, RpcErrorObject> {
self.delegate.on_request(method, params).await
}
async fn on_notification(&self, method: &str, params: Value) {
let event = if Some(method) == self.text_method.as_deref() {
let text = params
.get("text")
.or_else(|| params.get("content"))
.and_then(Value::as_str)
.map(str::to_owned)
.or_else(|| params.as_str().map(str::to_owned));
text.map(|text| AgentEvent::MessageChunk { text })
} else {
None
};
self.delegate
.on_event(event.unwrap_or(AgentEvent::Raw {
raw: serde_json::json!({"method": method, "params": params}),
}))
.await;
}
}
let peer = JsonRpcPeer::spawn(
stdout,
stdin,
Arc::new(Inner {
delegate,
text_method,
}),
observer,
crate::framing::DEFAULT_MAX_MESSAGE_BYTES,
);
let cwd = request
.cwd
.clone()
.or_else(|| self.command.cwd.clone())
.map(|path| path.display().to_string())
.unwrap_or_else(|| std::env::temp_dir().display().to_string());
let mut session_id = None;
if let Some(open) = &self.spec.session_open {
let result = call(&peer, open, &vars(None, &cwd, "", None, None)).await?;
session_id = result
.get("sessionId")
.or_else(|| result.get("session_id"))
.or_else(|| result.get("id"))
.and_then(Value::as_str)
.map(str::to_owned);
}
Ok(RpcSession {
peer,
child: Mutex::new(child),
spec: self.spec.clone(),
session_id,
cwd,
turn_lock: Mutex::new(()),
})
}
}
impl RpcSession {
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub fn capabilities(&self) -> Capabilities {
self.spec.capabilities()
}
pub async fn prompt(
&self,
text: &str,
model: Option<&str>,
effort: Option<&str>,
) -> Result<TurnOutcome, AdapterError> {
let _guard = self.turn_lock.lock().await;
let vars = vars(self.session_id.as_deref(), &self.cwd, text, model, effort);
let result = call(&self.peer, &self.spec.prompt, &vars).await?;
let stop_reason = ["stopReason", "stop_reason", "status"]
.iter()
.find_map(|key| result.get(*key).and_then(Value::as_str))
.unwrap_or("end_turn")
.to_owned();
Ok(TurnOutcome {
stop_reason,
text: extract_text(&result, self.spec.result_text_path.as_deref())
.unwrap_or_else(|| result.to_string()),
truncated: false,
exit_code: None,
})
}
pub fn cancel(&self) -> Result<(), AdapterError> {
let Some(cancel) = &self.spec.cancel else {
return Err(AdapterError::permanent(
"agent_cancel_unsupported",
"session/cancel",
));
};
let vars = vars(self.session_id.as_deref(), &self.cwd, "", None, None);
let params = substitute(&cancel.params, &vars);
self.peer.notify(&cancel.method, params).map_err(|error| {
AdapterError::uncertain("agent_cancel_failed", "session/cancel")
.with_detail(format!("{error:?}"))
})
}
pub async fn call_passthrough(
&self,
method: &str,
params: Value,
) -> Result<Value, AdapterError> {
if !self.spec.passthrough.iter().any(|entry| entry == method) {
return Err(AdapterError::permanent(
"agent_method_not_admitted",
"rpc/call",
));
}
self.peer.call(method, params).await.map_err(call_failure)
}
pub async fn close(self) -> Result<(), AdapterError> {
if let Some(close) = &self.spec.session_close {
let vars = vars(self.session_id.as_deref(), &self.cwd, "", None, None);
let params = substitute(&close.params, &vars);
let _ = self.peer.call(&close.method, params).await;
}
self.peer.close().await;
let mut child = self.child.lock().await;
let _ = child.terminate().await;
Ok(())
}
}
type Vars = BTreeMap<String, String>;
fn vars(
session_id: Option<&str>,
cwd: &str,
prompt: &str,
model: Option<&str>,
effort: Option<&str>,
) -> Vars {
let mut vars = BTreeMap::new();
vars.insert("cwd".to_owned(), cwd.to_owned());
vars.insert("prompt".to_owned(), prompt.to_owned());
vars.insert("model".to_owned(), model.unwrap_or_default().to_owned());
vars.insert("effort".to_owned(), effort.unwrap_or_default().to_owned());
if let Some(session) = session_id {
vars.insert("sessionId".to_owned(), session.to_owned());
}
vars
}
fn substitute(template: &Value, vars: &Vars) -> Value {
match template {
Value::String(text) => {
let mut result = text.clone();
for (key, value) in vars {
result = result.replace(&format!("{{{key}}}"), value);
}
Value::String(result)
}
Value::Array(items) => {
Value::Array(items.iter().map(|item| substitute(item, vars)).collect())
}
Value::Object(map) => Value::Object(
map.iter()
.map(|(key, value)| (key.clone(), substitute(value, vars)))
.collect(),
),
other => other.clone(),
}
}
async fn call(peer: &JsonRpcPeer, spec: &RpcCallSpec, vars: &Vars) -> Result<Value, AdapterError> {
if spec.method.is_empty() {
return Err(AdapterError::permanent("agent_method_unset", "rpc/call"));
}
peer.call(&spec.method, substitute(&spec.params, vars))
.await
.map_err(call_failure)
}
fn call_failure(failure: CallFailure) -> AdapterError {
match failure {
CallFailure::Remote(error) => AdapterError::permanent("agent_remote_error", "rpc/call")
.with_detail(format!("{} ({})", error.message, error.code)),
CallFailure::Transport(reason) => {
AdapterError::uncertain("agent_transport_failed", "rpc/call").with_detail(reason)
}
}
}
fn extract_text(result: &Value, path: Option<&str>) -> Option<String> {
let mut cursor = result;
for segment in path?.split('.') {
cursor = cursor.get(segment)?;
}
cursor.as_str().map(str::to_owned)
}