blob: 4f294c994b22b105c63b2c50b40852be9c6bdc5a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use futures_util::FutureExt;
use maka_agent_ports::model::Frame;
use maka_agent_ports::model::Socket;
use maka_agent_ports::model::Transport;
use serde_json::Value;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
time::Instant,
};
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
type Result<T> = std::result::Result<T, crate::Error>;
mod cache;
mod connect;
mod frame;
mod output;
mod shared;
use cache::{Baseline, Prepared};
use frame::{ReadError, receive};
pub use shared::Shared;
/// Ephemeral connection reuse, scoped to one sequential Turn. Clones are leases,
/// not history owners; dropping the final lease closes any idle socket.
#[derive(Clone, Default)]
pub struct ResponsesLane(Arc<Mutex<Lane>>);
#[derive(Default)]
struct Lane {
idle: Option<Idle>,
route: Option<u64>,
fallback: bool,
busy: bool,
}
struct Idle {
socket: Arc<dyn Socket>,
url: String,
headers: BTreeMap<String, String>,
network: u64,
baseline: Option<Baseline>,
}
impl ResponsesLane {
pub fn try_switch_fallback_transport(&self, transport: &Shared) -> bool {
let mut lane = self.0.lock().unwrap();
if lane.busy || lane.fallback {
return false;
}
let Some(route) = lane.route else {
return false;
};
lane.fallback = true;
lane.idle.take();
transport.defer(route, Instant::now());
true
}
pub fn response_id(&self) -> Option<String> {
let lane = self.0.lock().unwrap();
if lane.busy || lane.fallback {
return None;
}
lane.idle.as_ref()?.baseline.as_ref()?.response_id.clone()
}
}
pub struct Exchange {
lane: ResponsesLane,
active: AsyncMutex<Option<Idle>>,
cancellation: CancellationToken,
owns_lane: AtomicBool,
network: Arc<dyn Transport>,
transport: Arc<Shared>,
}
impl Exchange {
pub fn new(
lane: ResponsesLane,
cancellation: CancellationToken,
network: Arc<dyn Transport>,
transport: Arc<Shared>,
) -> Self {
Self {
lane,
active: AsyncMutex::new(None),
cancellation: cancellation.child_token(),
owns_lane: AtomicBool::new(false),
network,
transport,
}
}
pub fn cancel(&self) {
self.cancellation.cancel();
}
pub async fn start(
&self,
url: String,
headers: BTreeMap<String, String>,
body: Value,
) -> Result<Option<Value>> {
// Cancellation drops the in-flight connect/send future and its socket.
tokio::select! {
biased;
_ = self.cancellation.cancelled() => Err(error("Responses WebSocket cancelled")),
result = self.start_inner(url, headers, body) => result,
}
}
async fn start_inner(
&self,
url: String,
headers: BTreeMap<String, String>,
body: Value,
) -> Result<Option<Value>> {
let route = self
.transport
.route(&url, &headers, self.network.identity());
let mut active = self.active.lock().await;
if active.is_some() {
return Err(error("Responses WebSocket request already active"));
}
let mut idle = {
let mut lane = self.lane.0.lock().unwrap();
if lane.fallback || lane.busy {
// No caller may submit a shortened request without owning the
// matching baseline. Ordinary concurrent full requests use HTTP.
return Ok(Some(Prepared::new(body, None)?.full));
}
lane.busy = true;
lane.route = Some(route);
self.owns_lane.store(true, Ordering::Release);
lane.idle.take()
};
// Reconstruct before rejecting an old socket: store:false state cannot
// be recovered by carrying its response id to another connection.
let prepared = Prepared::new(body, idle.as_mut().and_then(|idle| idle.baseline.take()))?;
let mut idle = idle.filter(|idle| {
idle.url == url && idle.headers == headers && idle.network == self.network.identity()
});
// Consume an already observable idle close before deciding to reuse.
// After send starts, failure is never retried by this transport.
if let Some(connection) = idle.as_mut()
&& connection.socket.receive().now_or_never().is_some()
{
idle = None;
}
let reuse = idle.is_some();
if !reuse && self.transport.deferred(route, Instant::now()) {
let mut lane = self.lane.0.lock().unwrap();
lane.busy = false;
lane.fallback = true;
self.owns_lane.store(false, Ordering::Release);
return Ok(Some(prepared.full));
}
let mut connection = match idle {
Some(idle) => idle,
None => match connect::open(self.network.as_ref(), &url, &headers).await? {
Some(socket) => Idle {
socket,
url,
headers,
network: self.network.identity(),
baseline: None,
},
_ => {
if !self.cancellation.is_cancelled() {
self.transport.defer(route, Instant::now());
}
let mut lane = self.lane.0.lock().unwrap();
lane.fallback = true;
lane.busy = false;
self.owns_lane.store(false, Ordering::Release);
return Ok(Some(prepared.full));
}
},
};
let body = if reuse {
prepared.delta.as_ref().unwrap_or(&prepared.full)
} else {
&prepared.full
};
// Serialize a borrowed envelope without cloning the complete input.
let mut object: BTreeMap<_, _> = body
.as_object()
.unwrap()
.iter()
.filter(|(key, _)| !matches!(key.as_str(), "stream" | "background"))
.map(|(key, value)| (key.as_str(), value))
.collect();
let kind = Value::String("response.create".into());
object.insert("type", &kind);
let text =
serde_json::to_string(&object).map_err(|_| error("invalid Responses request"))?;
connection
.socket
.send(Frame::Text(text))
.await
.map_err(|cause| {
if !self.cancellation.is_cancelled() {
self.transport.defer(route, Instant::now());
}
match cause {
maka_agent_facts::model::error::ModelError::Provider(failure)
if failure.reason()
== maka_agent_facts::model::error::ProviderFailureReason::Network =>
{
// The engine owns recovery from canonical history. This
// transport never resends an uncertain response.create.
crate::Error::Interrupted(failure.message().into())
}
cause => {
crate::Error::Transport(format!("Responses WebSocket send failed: {cause}"))
}
}
})?;
connection.baseline = prepared.cache(&self.transport.cache);
*active = Some(connection);
Ok(None)
}
pub async fn next(&self) -> Result<Option<String>> {
let mut active = self.active.lock().await;
let Some(connection) = active.as_mut() else {
return Ok(None);
};
let result = tokio::select! {
biased;
_ = self.cancellation.cancelled() => Err(ReadError::Cancelled),
result = receive(connection.socket.as_ref()) => result,
};
match result {
Ok((text, terminal, reusable, mut event)) => {
connection.baseline = connection.baseline.take().and_then(|mut baseline| {
baseline.observe(&mut event, &self.transport.cache)?;
Some(baseline)
});
if terminal {
let mut idle = active.take();
if let Some(connection) = idle.as_mut() {
connection.baseline = connection.baseline.take().and_then(|baseline| {
baseline.complete(&mut event["response"], &self.transport.cache)
});
}
let mut lane = self.lane.0.lock().unwrap();
lane.busy = false;
self.owns_lane.store(false, Ordering::Release);
if reusable {
lane.idle = idle;
}
}
Ok(Some(text))
}
Err(error) => {
let reconnect = matches!(error, ReadError::Interrupted(_));
active.take();
let mut lane = self.lane.0.lock().unwrap();
if !reconnect
&& !matches!(error, ReadError::Cancelled)
&& !self.cancellation.is_cancelled()
&& let Some(route) = lane.route
{
self.transport.defer(route, Instant::now());
}
lane.busy = false;
// A retry gets a fresh socket and full request. Only the caller
// can select fallback after exhausting its stream retry budget.
lane.fallback = !reconnect;
self.owns_lane.store(false, Ordering::Release);
Err(error.into_error())
}
}
}
}
impl Drop for Exchange {
fn drop(&mut self) {
if self.owns_lane.load(Ordering::Acquire) {
let mut lane = self.lane.0.lock().unwrap();
if lane.busy {
lane.busy = false;
lane.fallback = true;
lane.idle.take();
}
}
}
}
fn error(message: &'static str) -> crate::Error {
crate::Error::Transport(message.into())
}