Skip to main content

solo_core/
llm.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `LlmClient` trait + supporting types. See ADR-0002.
4//!
5//! The Steward struct (`solo-steward`) consumes `Arc<dyn LlmClient>` rather
6//! than implementing a Steward trait — the LLM is the swap point, not the
7//! consolidation logic.
8
9use crate::error::Result;
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum Role {
16    System,
17    User,
18    Assistant,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Message {
23    pub role: Role,
24    pub content: String,
25}
26
27impl Message {
28    pub fn system(content: impl Into<String>) -> Self {
29        Self {
30            role: Role::System,
31            content: content.into(),
32        }
33    }
34
35    pub fn user(content: impl Into<String>) -> Self {
36        Self {
37            role: Role::User,
38            content: content.into(),
39        }
40    }
41
42    pub fn assistant(content: impl Into<String>) -> Self {
43        Self {
44            role: Role::Assistant,
45            content: content.into(),
46        }
47    }
48}
49
50#[async_trait]
51pub trait LlmClient: Send + Sync {
52    /// Backend identifier — "qwen3-coder-30b-local", "claude-sonnet-4-6",
53    /// "gpt-5o", etc. Used in dev-log entries and consolidation provenance
54    /// (`Provenance::by`).
55    fn name(&self) -> &str;
56
57    /// Run a single completion turn. Implementations handle their own
58    /// retries, rate limits, and context-window management.
59    async fn complete(&self, messages: &[Message]) -> Result<Message>;
60
61    /// True iff this client talks to a real LLM backend (HTTP, FFI,
62    /// local model), false for deterministic test stubs whose
63    /// `complete()` returns canned data.
64    ///
65    /// Callers gate LLM-dependent work on this so the system stays
66    /// usable in stub-only configurations: e.g. the writer's
67    /// contradiction sweep early-returns when the steward's client is
68    /// a stub, since a canned response can't faithfully arbitrate two
69    /// triples it has never seen.
70    ///
71    /// Default `true` — production backends inherit the right answer
72    /// without per-impl plumbing. The stub overrides to `false`.
73    fn is_real_llm(&self) -> bool {
74        true
75    }
76}