Skip to main content

mj_controller/server/api/
subagent_backend.rs

1use super::*;
2
3/// Everything the API needs from the daemon: live session actors, the durable
4/// projection, and the target-side git operations.
5///
6/// The daemon's implementation lives in `server_runtime::api`; route tests
7/// supply a fake.
8pub trait SubagentBackend: Send + Sync {
9    fn events(
10        &self,
11        filter: crate::database::ApiEventFilter,
12        after_seq: Option<u64>,
13    ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
14        events::load_events(filter, after_seq)
15    }
16
17    fn profile_config(
18        &self,
19        profile: String,
20        model: Option<String>,
21        refresh: bool,
22    ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
23        Box::pin(crate::controller::profile_config::discover(
24            profile, model, refresh,
25        ))
26    }
27    /// What the daemon's background-warmed profile catalogue already holds for
28    /// a profile. It never launches a harness and never waits, so a caller a
29    /// model is blocked on can check a selector without paying for discovery.
30    /// `None` means the catalogue cannot answer yet, not that the profile is
31    /// unusable.
32    fn published_profile_config(
33        &self,
34        _profile: &str,
35    ) -> Option<mj_core::worker_launch::ProfileConfig> {
36        None
37    }
38    fn start_subagent(
39        &self,
40        _request: crate::controller::RegisterSubagentRequest,
41    ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
42        Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
43    }
44    fn list_subagents(
45        &self,
46        parent_session_id: String,
47    ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
48        Box::pin(async move {
49            tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
50                .await?
51        })
52    }
53    fn read_context_file(
54        &self,
55        session_id: String,
56        path: PathBuf,
57    ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
58        self.read_file(session_id, path)
59    }
60    fn set_config(
61        &self,
62        session_id: String,
63        key: String,
64        value: String,
65    ) -> BoxFuture<'_, AnyResult<()>> {
66        Box::pin(async move {
67            self.session_handle(session_id)
68                .await?
69                .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
70                .set_config(key, value)
71                .await
72        })
73    }
74    fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
75        Box::pin(async { Ok(()) })
76    }
77    /// The live actor for a session, or `None` when none holds it.
78    fn session_handle(&self, session_id: String)
79    -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
80
81    /// Submit a prompt, returning its relay acceptance ordinal.
82    fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
83
84    /// Durable turn state for a session with no live actor.
85    fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
86
87    /// Summarize the turn that started at this transcript position.
88    fn turn_summary(
89        &self,
90        session_id: String,
91        turn_start_position: u64,
92    ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
93
94    /// Apply model, effort, and the first prompt once a new session is ready.
95    fn start_followup(
96        &self,
97        session_id: String,
98        followup: StartFollowup,
99    ) -> BoxFuture<'_, AnyResult<()>>;
100
101    /// How far a created session's follow-up has got.
102    fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
103
104    /// A page of transcript items after `after_seq`.
105    fn transcript(
106        &self,
107        session_id: String,
108        after_seq: u64,
109        limit: usize,
110        role: Option<mj_core::transcript::TranscriptRole>,
111    ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
112
113    fn usage(
114        &self,
115        session_id: String,
116        after_seq: u64,
117        limit: usize,
118    ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
119        Box::pin(async move {
120            tokio::task::spawn_blocking(move || {
121                crate::database::load_session_usage(&session_id, after_seq, limit)
122            })
123            .await?
124        })
125    }
126
127    /// A unified diff of the session's work.
128    fn diff(&self, session_id: String) -> BoxFuture<'_, Result<String, ExportError>>;
129
130    /// One file from the session's workspace.
131    fn read_file(
132        &self,
133        session_id: String,
134        path: PathBuf,
135    ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
136
137    fn write_file(
138        &self,
139        _session_id: String,
140        _path: PathBuf,
141        _bytes: Vec<u8>,
142        _overwrite: bool,
143    ) -> BoxFuture<'_, Result<(), ExportError>> {
144        Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
145    }
146
147    /// Push the session's branch to its repository's default remote.
148    fn push_branch(
149        &self,
150        session_id: String,
151        branch: String,
152    ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
153
154    /// A git bundle of the session's committed work.
155    fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
156
157    /// Whether the index was synced recently enough that a query need not ask
158    /// for one.
159    fn wiki_sync_is_stale(&self) -> bool {
160        false
161    }
162
163    /// Ask for a background sync. It is never waited for: a query answers from
164    /// what the index holds now.
165    fn wiki_request_sync(&self) {}
166
167    fn wiki_search(
168        &self,
169        _query: String,
170        _limit: usize,
171    ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
172        Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
173    }
174
175    /// `None` when the index holds no session with that id.
176    fn wiki_brief(
177        &self,
178        _wiki_id: String,
179        _max_chars: usize,
180    ) -> BoxFuture<'_, AnyResult<Option<String>>> {
181        Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
182    }
183
184    /// Start a session from an archived transcript, answering with its id, or
185    /// `None` when the index holds no session with that id.
186    fn wiki_restore(
187        &self,
188        _request: mj_client::daemon::WikiRestoreRequest,
189    ) -> BoxFuture<'_, AnyResult<Option<String>>> {
190        Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
191    }
192}
193
194pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
195    state
196        .subagent
197        .as_ref()
198        .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
199}
200
201// ---------------------------------------------------------------------------
202// Wait resolution
203// ---------------------------------------------------------------------------