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//!
21//! The agent loop never sees HTTP, never sees `reqwest`, and never contains a
22//! single OpenAI-specific field name.
23
24use std::pin::Pin;
25
26use anyhow::Result;
27use async_trait::async_trait;
28use futures_core::Stream;
29
30use crate::domain::agent::{AgentMessage, LlmStreamEvent, ToolDefinition};
31
32/// Port that the agent loop uses to drive a streaming LLM.
33///
34/// Implementations translate domain messages + tool definitions into
35/// vendor-specific HTTP requests and stream back [`LlmStreamEvent`] values.
36///
37/// # Contract
38///
39/// - The returned stream **must** end with exactly one [`LlmStreamEvent::Done`]
40/// item, even when the finish reason is abnormal (e.g. `"length"`).
41/// - Text and tool-call delta events may interleave freely before `Done`.
42/// - An `Err` item in the stream signals an unrecoverable infrastructure error;
43/// the agent loop will surface it as [`super::agent::AgentError::Internal`].
44#[async_trait]
45pub trait LlmCompletionPort: Send + Sync {
46 /// Begin a chat-completion request and return a live event stream.
47 ///
48 /// # Parameters
49 ///
50 /// - `messages` — conversation history in domain form.
51 /// - `tools` — tool schemas to advertise to the model.
52 ///
53 /// # Returns
54 ///
55 /// A pinned, heap-allocated, `Send`-able stream of [`LlmStreamEvent`].
56 /// The caller drives the stream by polling it; each item is either a
57 /// successfully parsed event or an infrastructure error.
58 async fn chat_stream(
59 &self,
60 messages: &[AgentMessage],
61 tools: &[ToolDefinition],
62 ) -> Result<Pin<Box<dyn Stream<Item = Result<LlmStreamEvent>> + Send>>>;
63}