Skip to main content

gglib_core/ports/
llm_completion.rs

1//! Port definition for streaming LLM chat completions.
2//!
3//! This module defines the infrastructure contract that the agent loop uses to
4//! drive an LLM.  The port is intentionally narrow: it speaks **domain types**
5//! ([`AgentMessage`], [`ToolDefinition`], [`LlmStreamEvent`]) and hides all
6//! vendor wire-format details (`OpenAI` JSON schemas, SSE framing, HTTP headers,
7//! etc.) behind the trait boundary.
8//!
9//! # Adapter responsibility
10//!
11//! A concrete implementation (e.g. in `gglib-axum` or `gglib-proxy`) is
12//! responsible for:
13//!
14//! 1. Translating `&[AgentMessage]` into the vendor's `messages` array,
15//!    serialising `ToolCall::arguments` (`serde_json::Value`) into the JSON
16//!    string form that OpenAI-compatible APIs require.
17//! 2. Translating `&[ToolDefinition]` into the vendor's `tools` array.
18//! 3. Parsing the streaming SSE response into a sequence of [`LlmStreamEvent`]
19//!    values, accumulating incremental tool-call deltas where necessary.
20//! 4. When `response_format` is `Some`, injecting the appropriate
21//!    `response_format` / `grammar` field into the vendor request body.
22//!
23//! The agent loop never sees HTTP, never sees `reqwest`, and never contains a
24//! single OpenAI-specific field name.
25
26use std::pin::Pin;
27
28use anyhow::Result;
29use async_trait::async_trait;
30use futures_core::Stream;
31
32use crate::domain::agent::{AgentMessage, LlmStreamEvent, ToolDefinition};
33
34// =============================================================================
35// LlmCompletionPort
36// =============================================================================
37
38/// Port that the agent loop uses to drive a streaming LLM.
39///
40/// Implementations translate domain messages + tool definitions into
41/// vendor-specific HTTP requests and stream back [`LlmStreamEvent`] values.
42///
43/// # Contract
44///
45/// - The returned stream **must** end with exactly one [`LlmStreamEvent::Done`]
46///   item, even when the finish reason is abnormal (e.g. `"length"`).
47/// - Text and tool-call delta events may interleave freely before `Done`.
48/// - An `Err` item in the stream signals an unrecoverable infrastructure error;
49///   the agent loop will surface it as [`super::agent::AgentError::Internal`].
50#[async_trait]
51pub trait LlmCompletionPort: Send + Sync {
52    /// Begin a chat-completion request and return a live event stream.
53    ///
54    /// # Parameters
55    ///
56    /// - `messages` — conversation history in domain form.
57    /// - `tools` — tool schemas to advertise to the model.
58    ///
59    /// # Returns
60    ///
61    /// A pinned, heap-allocated, `Send`-able stream of [`LlmStreamEvent`].
62    /// The caller drives the stream by polling it; each item is either a
63    /// successfully parsed event or an infrastructure error.
64    async fn chat_stream(
65        &self,
66        messages: &[AgentMessage],
67        tools: &[ToolDefinition],
68    ) -> Result<Pin<Box<dyn Stream<Item = Result<LlmStreamEvent>> + Send>>>;
69}