Skip to main content

contextgraph_host/
provider.rs

1//! The uniform provider handle.
2//!
3//! Every context source — an in-process built-in, a child process over
4//! stdio, or a remote HTTP endpoint — reaches the host through one trait,
5//! [`ContextProvider`]. Its shape mirrors the two Context Graph Protocol methods a host always
6//! needs: capability negotiation (cached from the handshake, SPEC.md §3) and
7//! `context/query` (SPEC.md §5). `stella-context` and any other Rust agent drive
8//! all three provider kinds through this single interface
9//! (`SPEC.md` §1 — "usable by any other Rust agent that wants Context Graph Protocol
10//! support").
11
12use async_trait::async_trait;
13use contextgraph_types::{
14    Capabilities, ContextQuery, ContextQueryResult, FrameKind, ProviderInfo, Verdict,
15    VerifyRequest, VerifyResponse,
16};
17
18use crate::error::HostError;
19
20/// A registered Context Graph Protocol provider, queryable behind one handle regardless of
21/// transport. `info()`/`capabilities()` return values cached at handshake
22/// time, so they are cheap synchronous getters even for out-of-process
23/// providers.
24#[async_trait]
25pub trait ContextProvider: Send + Sync {
26    /// The provider's host-facing id — its routing key and its consent key
27    /// (`SPEC.md` §4 and §10).
28    fn id(&self) -> &str;
29
30    /// Identity + declared data-flow direction, surfaced at consent time
31    /// (SPEC.md §3, ).
32    fn info(&self) -> &ProviderInfo;
33
34    /// Capabilities negotiated at the handshake (SPEC.md §3) — which frame kinds
35    /// and filters this provider serves, whether it upserts, does graph, is
36    /// an embedder, or supports subscriptions.
37    fn capabilities(&self) -> &Capabilities;
38
39    /// Answer a context query with budgeted, provenance-carrying frames
40    /// (SPEC.md §5). The host — not the provider — enforces the budget and consent;
41    /// a provider that over-runs its budget is caught by the host, not
42    /// trusted (`crate::host`).
43    async fn query(&self, query: &ContextQuery) -> Result<ContextQueryResult, HostError>;
44
45    /// Revalidate frames the host already holds, without any frame body
46    /// travelling (`docs/context-reuse.md` §4 `context/verify`).
47    ///
48    /// Defaults to answering [`Verdict::Unknown`](contextgraph_types::Verdict::Unknown)
49    /// for every requested identity, so an existing provider implements
50    /// nothing and is simply treated as unable to vouch for its frames — the
51    /// host then re-queries them. A provider that overrides this **MUST** also
52    /// advertise [`Capabilities::verify`](contextgraph_types::Capabilities::verify),
53    /// since the host only asks providers that declare support.
54    async fn verify(&self, request: &VerifyRequest) -> Result<VerifyResponse, HostError> {
55        Ok(VerifyResponse::uniform(request, Verdict::Unknown))
56    }
57
58    /// Shut the provider down cleanly (SPEC.md §3 lifecycle). In-process providers
59    /// default to a no-op; transport-backed providers send `shutdown` and
60    /// reap their child. Overridable.
61    async fn shutdown(&self) -> Result<(), HostError> {
62        Ok(())
63    }
64}
65
66/// The snake_case wire name of a [`FrameKind`], matching its `serde`
67/// representation and the strings a provider lists in
68/// [`Capabilities::query`]'s `kinds`.
69pub fn frame_kind_name(kind: FrameKind) -> &'static str {
70    match kind {
71        FrameKind::Snippet => "snippet",
72        FrameKind::Symbol => "symbol",
73        FrameKind::Fact => "fact",
74        FrameKind::Doc => "doc",
75        FrameKind::Memory => "memory",
76        FrameKind::Episode => "episode",
77        FrameKind::Graph => "graph",
78    }
79}
80
81/// Whether a provider is worth querying for a given request. A query with no
82/// `kinds` filter matches every provider (the host wants everyone's best
83/// frames, SPEC.md §5); otherwise a provider matches when it declares at least one
84/// of the requested frame kinds. Used by `query_all` to fan out only to
85/// relevant providers.
86pub fn capability_matches(caps: &Capabilities, query: &ContextQuery) -> bool {
87    if query.kinds.is_empty() {
88        return true;
89    }
90    query.kinds.iter().any(|requested| {
91        let name = frame_kind_name(*requested);
92        caps.query.kinds.iter().any(|served| served == name)
93    })
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use contextgraph_types::capability::QueryCapability;
100
101    fn caps_for(kinds: &[&str]) -> Capabilities {
102        Capabilities {
103            query: QueryCapability {
104                kinds: kinds.iter().map(|k| k.to_string()).collect(),
105            },
106            ..Capabilities::default()
107        }
108    }
109
110    fn query_for(kinds: Vec<FrameKind>) -> ContextQuery {
111        ContextQuery {
112            goal: "g".into(),
113            query_text: None,
114            embedding: None,
115            kinds,
116            anchors: vec![],
117            max_frames: 5,
118            max_tokens: 1000,
119            as_of: None,
120            representation_preferences: vec![],
121        }
122    }
123
124    #[test]
125    fn frame_kind_names_match_serde_snake_case() {
126        // The names a provider lists must be exactly the frames' serde names.
127        for (kind, name) in [
128            (FrameKind::Snippet, "snippet"),
129            (FrameKind::Symbol, "symbol"),
130            (FrameKind::Fact, "fact"),
131            (FrameKind::Doc, "doc"),
132            (FrameKind::Memory, "memory"),
133            (FrameKind::Episode, "episode"),
134            (FrameKind::Graph, "graph"),
135        ] {
136            assert_eq!(frame_kind_name(kind), name);
137            let serde_name = serde_json::to_value(kind).unwrap();
138            assert_eq!(serde_name, serde_json::Value::String(name.to_string()));
139        }
140    }
141
142    #[test]
143    fn an_empty_kind_filter_matches_every_provider() {
144        let caps = caps_for(&["doc"]);
145        assert!(capability_matches(&caps, &query_for(vec![])));
146    }
147
148    #[test]
149    fn a_kind_filter_matches_only_overlapping_providers() {
150        let doc_provider = caps_for(&["doc", "snippet"]);
151        assert!(capability_matches(
152            &doc_provider,
153            &query_for(vec![FrameKind::Doc])
154        ));
155        assert!(capability_matches(
156            &doc_provider,
157            &query_for(vec![FrameKind::Fact, FrameKind::Snippet])
158        ));
159        assert!(!capability_matches(
160            &doc_provider,
161            &query_for(vec![FrameKind::Memory, FrameKind::Episode])
162        ));
163    }
164}