mj_controller/server/api/
subagent_backend.rs1use super::*;
2
3pub trait SubagentBackend: Send + Sync {
9 fn wait_revision(&self, _session_id: &str) -> AnyResult<Option<u64>> {
12 Ok(None)
13 }
14
15 fn validate_github_bundle(
18 &self,
19 _bundle_id: String,
20 ) -> BoxFuture<'_, Result<(), crate::controller::GithubBundleSelectionError>> {
21 Box::pin(async { Ok(()) })
22 }
23
24 fn github_token(
27 &self,
28 _owner: Option<String>,
29 _repositories: Vec<(String, String)>,
30 ) -> BoxFuture<'_, AnyResult<String>> {
31 Box::pin(async {
32 anyhow::bail!(
33 "GitHub App credentials are not configured; set [github.app] in config.toml"
34 )
35 })
36 }
37
38 fn transcript_history(
39 &self,
40 session_id: String,
41 before: Option<mj_core::storage::TranscriptCursor>,
42 ) -> BoxFuture<'_, AnyResult<mj_core::storage::TranscriptHistoryPage>> {
43 Box::pin(async move {
44 tokio::task::spawn_blocking(move || {
45 crate::database::load_transcript_history(&session_id, before.as_ref(), 128)?
46 .context("session history is unavailable")
47 })
48 .await?
49 })
50 }
51
52 fn native_agent_history(
53 &self,
54 owner: String,
55 child: String,
56 before: Option<(u64, String)>,
57 ) -> BoxFuture<'_, AnyResult<mj_core::native_agent::NativeAgentHistoryPage>> {
58 Box::pin(async move {
59 tokio::task::spawn_blocking(move || {
60 crate::database::native_agent_history(&owner, &child, before)
61 })
62 .await?
63 })
64 }
65 fn events(
66 &self,
67 filter: crate::database::ApiEventFilter,
68 after_seq: Option<u64>,
69 ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
70 events::load_events(filter, after_seq)
71 }
72
73 fn profile_config(
74 &self,
75 profile: String,
76 model: Option<String>,
77 refresh: bool,
78 ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
79 Box::pin(crate::controller::profile_config::discover(
80 profile, model, refresh,
81 ))
82 }
83 fn subagent_candidates(
88 &self,
89 _parent_profile: String,
90 ) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
91 Box::pin(async { anyhow::bail!("sub-agent profiles are unavailable") })
92 }
93 fn session_profile_candidates(&self) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
97 Box::pin(async { anyhow::bail!("session profile selection is unavailable") })
98 }
99 fn start_subagent(
100 &self,
101 _request: crate::controller::RegisterSubagentRequest,
102 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
103 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
104 }
105 fn list_subagents(
106 &self,
107 parent_session_id: String,
108 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
109 Box::pin(async move {
110 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
111 .await?
112 })
113 }
114 fn subagent_handed_back(&self, child_session_id: String) -> BoxFuture<'_, AnyResult<bool>> {
117 Box::pin(async move {
118 tokio::task::spawn_blocking(move || {
119 crate::controller::subagent_has_handed_back(&child_session_id)
120 })
121 .await?
122 })
123 }
124 fn read_context_file(
125 &self,
126 session_id: String,
127 path: PathBuf,
128 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
129 self.read_file(session_id, path)
130 }
131 fn list_workspaces(
134 &self,
135 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
136 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
137 }
138 fn create_workspace(
145 &self,
146 name: String,
147 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
148 Box::pin(async move {
149 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
150 .await?
151 })
152 }
153 fn set_config(
154 &self,
155 session_id: String,
156 key: String,
157 value: String,
158 ) -> BoxFuture<'_, AnyResult<()>> {
159 Box::pin(async move {
160 self.session_handle(session_id)
161 .await?
162 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
163 .set_config(key, value)
164 .await
165 })
166 }
167 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
168 Box::pin(async { Ok(()) })
169 }
170 fn session_handle(&self, session_id: String)
172 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
173
174 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
176
177 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
179
180 fn subagent_report(
183 &self,
184 _session_id: String,
185 ) -> BoxFuture<'_, AnyResult<Option<(bool, mj_core::subagent::SubagentReport)>>> {
186 Box::pin(async { Ok(None) })
187 }
188
189 fn turn_summary(
191 &self,
192 session_id: String,
193 turn: TurnSpan,
194 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
195
196 fn start_followup(
198 &self,
199 session_id: String,
200 followup: StartFollowup,
201 ) -> BoxFuture<'_, AnyResult<()>>;
202
203 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
205
206 fn transcript(
208 &self,
209 session_id: String,
210 after_seq: u64,
211 limit: usize,
212 roles: Vec<mj_core::transcript::TranscriptRole>,
213 finished_only: bool,
214 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
215
216 fn usage(
217 &self,
218 session_id: String,
219 after_seq: u64,
220 limit: usize,
221 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
222 Box::pin(async move {
223 tokio::task::spawn_blocking(move || {
224 crate::database::load_session_usage(&session_id, after_seq, limit)
225 })
226 .await?
227 })
228 }
229
230 fn usage_tree(
231 &self,
232 parent: String,
233 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsageTree>>> {
234 Box::pin(async move {
235 tokio::task::spawn_blocking(move || crate::database::load_usage_tree(&parent)).await?
236 })
237 }
238
239 fn diff(
241 &self,
242 session_id: String,
243 options: DiffOptions,
244 ) -> BoxFuture<'_, Result<String, ExportError>>;
245
246 fn read_file(
248 &self,
249 session_id: String,
250 path: PathBuf,
251 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
252
253 fn write_file(
254 &self,
255 _session_id: String,
256 _path: PathBuf,
257 _bytes: Vec<u8>,
258 _overwrite: bool,
259 ) -> BoxFuture<'_, Result<(), ExportError>> {
260 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
261 }
262
263 fn push_branch(
265 &self,
266 session_id: String,
267 branch: String,
268 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
269
270 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
272
273 fn wiki_sync_is_stale(&self) -> bool {
276 false
277 }
278
279 fn wiki_request_sync(&self) {}
282
283 fn wiki_search(
284 &self,
285 _query: String,
286 _limit: usize,
287 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
288 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
289 }
290
291 fn wiki_brief(
293 &self,
294 _wiki_id: String,
295 _max_chars: usize,
296 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
297 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
298 }
299
300 fn wiki_hits(
303 &self,
304 _wiki_id: String,
305 _query: String,
306 _context_messages: usize,
307 _per_message_chars: usize,
308 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
309 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
310 }
311
312 fn wiki_session(
315 &self,
316 _wiki_id: String,
317 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiSessionInfo>>> {
318 Box::pin(async { anyhow::bail!("SessionWiki lookups are unavailable") })
319 }
320
321 fn wiki_restore(
324 &self,
325 _request: mj_client::daemon::WikiRestoreRequest,
326 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
327 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
328 }
329}
330
331pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
332 state
333 .subagent
334 .as_ref()
335 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
336}
337
338