use litellm_host::event::{FailureOrigin, MachineEvent, RequestContext, Timing, WireRequest}; use litellm_host::route::Route; use pyo3::exceptions::PyRuntimeError; use pyo3::gc::{PyTraverseError, PyVisit}; use pyo3::prelude::*; use pyo3::types::PyDict; pub fn missing_state() -> PyErr { PyRuntimeError::new_err("missing native call state") } /// What an adapter step produced: either the value the driver asked for, or a Python /// awaitable the driver hands back to the caller's task before asking again. pub enum LifecycleStep { Await(Py), Arguments(Py), Wire(Box), Response(Py), Done, } /// What a lifecycle observes: the driver's start, the machine's own events, and one /// terminal event carrying the public value the caller receives. pub enum LifecycleEvent<'a> { Started { start_time: f64, }, Machine(&'a MachineEvent), Succeeded { timing: Timing, response: &'a Py, }, Failed { timing: Timing, origin: FailureOrigin, error: &'a PyErr, }, } /// One consumer of a call's lifecycle on the Python side. The driver calls the steps in /// order: `begin` before the machine starts, `before_send` and `emit` while it runs, /// `after_success` and one terminal `emit` after it completes. Whenever a step returns /// [`LifecycleStep::Await`], the driver awaits it in the caller's task and continues the /// same step through `resume`. /// /// A step that fails with an ordinary exception fails the call with that exception, /// except on a terminal event, where the adapter is expected to report and swallow its /// own errors. An exception that is not a `PyException`, such as a cancellation, ends /// the call without further dispatch. pub trait PythonLifecycle: Send + Sync { fn begin( &mut self, py: Python<'_>, arguments: Py, started_at: f64, ) -> PyResult; fn before_send( &mut self, py: Python<'_>, wire: Box, context: &RequestContext, ) -> PyResult; fn after_success( &mut self, py: Python<'_>, response: Py, timing: Timing, ) -> PyResult; fn emit(&mut self, py: Python<'_>, event: LifecycleEvent<'_>) -> PyResult; /// The call streams and its stream was handed to the caller. The caller is not /// inside an await here, so this step and `delivered` cannot suspend. fn opened(&mut self, py: Python<'_>) -> PyResult<()>; /// One chunk of an open stream is about to reach the caller. fn delivered(&mut self, py: Python<'_>, chunk: &Py) -> PyResult<()>; fn resume(&mut self, py: Python<'_>, result: PyResult>) -> PyResult; fn close(&mut self, py: Python<'_>); fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError>; } /// Why a route operation the host answered did not produce a result: the route's own code /// rejected it, which the route classifies like any other native failure, or Python code /// raised, which reaches the caller as it was raised. #[derive(Debug)] pub enum InvokeError { Native(E), Python(PyErr), } impl From for InvokeError { fn from(error: PyErr) -> Self { Self::Python(error) } } /// The Python side of one route: answers the route's own operations, builds the public /// response and classifies native failures into public exceptions. pub trait RouteHost: Send + Sync { type Route: Route; /// The public exception a native failure maps to, kept as a value until the driver /// raises it. type Failure: Into; /// `arguments` is the keyword view the lifecycle's `begin` produced, not the /// caller's own dict. A route host that projects from it inherits whatever that /// adapter rewrote. fn invoke( &mut self, py: Python<'_>, arguments: &Bound<'_, PyDict>, op: ::Op, ) -> Result<::OpResult, InvokeError<::Error>>; fn complete( &mut self, py: Python<'_>, response: ::Response, ) -> PyResult>; /// One streamed chunk as the caller receives it. fn chunk( &mut self, py: Python<'_>, chunk: ::Chunk, ) -> PyResult>; fn classify( &self, py: Python<'_>, error: ::Error, ) -> PyResult; fn host_error(error: &PyErr) -> ::Error; fn close(&mut self, py: Python<'_>); fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError>; }