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}