1use super::*;
2
3pub trait SubagentBackend: Send + Sync {
9 fn subagent_config(&self) -> BoxFuture<'_, AnyResult<mj_core::config::SubagentConfig>> {
10 Box::pin(async { anyhow::bail!("sub-agent configuration unavailable") })
11 }
12
13 fn wait_revision(&self, _session_id: &str) -> AnyResult<Option<u64>> {
16 Ok(None)
17 }
18
19 fn validate_github_bundle(
22 &self,
23 _bundle_id: String,
24 ) -> BoxFuture<'_, Result<(), crate::controller::GithubBundleSelectionError>> {
25 Box::pin(async { Ok(()) })
26 }
27
28 fn github_token(
31 &self,
32 _owner: Option<String>,
33 _repositories: Vec<(String, String)>,
34 ) -> BoxFuture<'_, AnyResult<String>> {
35 Box::pin(async {
36 anyhow::bail!(
37 "GitHub App credentials are not configured; set [github.app] in config.toml"
38 )
39 })
40 }
41
42 fn transcript_history(
43 &self,
44 session_id: String,
45 before: Option<mj_core::storage::TranscriptCursor>,
46 ) -> BoxFuture<'_, AnyResult<mj_core::storage::TranscriptHistoryPage>> {
47 Box::pin(async move {
48 tokio::task::spawn_blocking(move || {
49 crate::database::load_transcript_history(&session_id, before.as_ref(), 128)?
50 .context("session history is unavailable")
51 })
52 .await?
53 })
54 }
55
56 fn native_agent_history(
57 &self,
58 owner: String,
59 child: String,
60 before: Option<(u64, String)>,
61 ) -> BoxFuture<'_, AnyResult<mj_core::native_agent::NativeAgentHistoryPage>> {
62 Box::pin(async move {
63 tokio::task::spawn_blocking(move || {
64 crate::database::native_agent_history(&owner, &child, before)
65 })
66 .await?
67 })
68 }
69 fn events(
70 &self,
71 filter: crate::database::ApiEventFilter,
72 after_seq: Option<u64>,
73 ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
74 events::load_events(filter, after_seq)
75 }
76
77 fn profile_config(
78 &self,
79 profile: String,
80 model: Option<String>,
81 refresh: bool,
82 ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
83 Box::pin(crate::controller::profile_config::discover(
84 profile, model, refresh,
85 ))
86 }
87 fn subagent_candidates(
92 &self,
93 _parent_profile: String,
94 ) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
95 Box::pin(async { anyhow::bail!("sub-agent profiles are unavailable") })
96 }
97 fn session_profile_candidates(&self) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
101 Box::pin(async { anyhow::bail!("session profile selection is unavailable") })
102 }
103 fn start_subagent(
104 &self,
105 _request: crate::controller::RegisterSubagentRequest,
106 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
107 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
108 }
109 fn list_subagents(
110 &self,
111 parent_session_id: String,
112 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
113 Box::pin(async move {
114 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
115 .await?
116 })
117 }
118 fn subagent_handed_back(&self, child_session_id: String) -> BoxFuture<'_, AnyResult<bool>> {
121 Box::pin(async move {
122 tokio::task::spawn_blocking(move || {
123 crate::controller::subagent_has_handed_back(&child_session_id)
124 })
125 .await?
126 })
127 }
128 fn read_context_file(
129 &self,
130 session_id: String,
131 path: PathBuf,
132 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
133 self.read_file(session_id, path)
134 }
135 fn list_workspaces(
138 &self,
139 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
140 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
141 }
142 fn create_workspace(
149 &self,
150 name: String,
151 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
152 Box::pin(async move {
153 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
154 .await?
155 })
156 }
157 fn set_config(
158 &self,
159 session_id: String,
160 key: String,
161 value: String,
162 ) -> BoxFuture<'_, AnyResult<()>> {
163 Box::pin(async move {
164 self.session_handle(session_id)
165 .await?
166 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
167 .set_config(key, value)
168 .await
169 })
170 }
171 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
172 Box::pin(async { Ok(()) })
173 }
174 fn session_handle(&self, session_id: String)
176 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
177
178 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
180
181 fn deliver_message(
184 &self,
185 _sender_session_id: Option<String>,
186 _target_session_id: String,
187 _text: String,
188 _request_id: String,
189 _created_at_ms: i64,
190 ) -> BoxFuture<'_, AnyResult<SessionMessageResponse>> {
191 Box::pin(async { anyhow::bail!("session messaging is unavailable") })
192 }
193
194 fn prompt_with_id(
196 &self,
197 session_id: String,
198 text: String,
199 command_id: Option<String>,
200 ) -> BoxFuture<'_, AnyResult<u64>> {
201 let Some(command_id) = command_id else {
202 return self.prompt(session_id, text);
203 };
204 Box::pin(async move {
205 let handle = self
206 .session_handle(session_id)
207 .await?
208 .ok_or_else(|| anyhow::anyhow!("session has no live worker"))?;
209 handle
210 .submit(
211 command_id,
212 mj_core::relay::RelayCommand::Prompt {
213 prompt: vec![agent_client_protocol::schema::v1::ContentBlock::Text(
214 agent_client_protocol::schema::v1::TextContent::new(text),
215 )],
216 },
217 )
218 .await
219 })
220 }
221
222 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
224
225 fn subagent_report(
228 &self,
229 _session_id: String,
230 ) -> BoxFuture<'_, AnyResult<Option<(bool, mj_core::subagent::SubagentReport)>>> {
231 Box::pin(async { Ok(None) })
232 }
233
234 fn turn_summary(
236 &self,
237 session_id: String,
238 turn: TurnSpan,
239 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
240
241 fn start_followup(
243 &self,
244 session_id: String,
245 followup: StartFollowup,
246 ) -> BoxFuture<'_, AnyResult<()>>;
247
248 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
250
251 fn transcript(
253 &self,
254 session_id: String,
255 after_seq: u64,
256 limit: usize,
257 roles: Vec<mj_core::transcript::TranscriptRole>,
258 finished_only: bool,
259 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
260
261 fn usage(
262 &self,
263 session_id: String,
264 after_seq: u64,
265 limit: usize,
266 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
267 Box::pin(async move {
268 tokio::task::spawn_blocking(move || {
269 crate::database::load_session_usage(&session_id, after_seq, limit)
270 })
271 .await?
272 })
273 }
274
275 fn usage_tree(
276 &self,
277 parent: String,
278 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsageTree>>> {
279 Box::pin(async move {
280 tokio::task::spawn_blocking(move || crate::database::load_usage_tree(&parent)).await?
281 })
282 }
283
284 fn diff(
286 &self,
287 session_id: String,
288 options: DiffOptions,
289 ) -> BoxFuture<'_, Result<String, ExportError>>;
290
291 fn read_file(
293 &self,
294 session_id: String,
295 path: PathBuf,
296 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
297
298 fn write_file(
299 &self,
300 _session_id: String,
301 _path: PathBuf,
302 _bytes: Vec<u8>,
303 _overwrite: bool,
304 ) -> BoxFuture<'_, Result<(), ExportError>> {
305 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
306 }
307
308 fn push_branch(
310 &self,
311 session_id: String,
312 branch: String,
313 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
314
315 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
317
318 fn wiki_sync_is_stale(&self) -> bool {
321 false
322 }
323
324 fn wiki_request_sync(&self) {}
327
328 fn wiki_search(
329 &self,
330 _query: String,
331 _limit: usize,
332 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
333 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
334 }
335
336 fn wiki_brief(
338 &self,
339 _wiki_id: String,
340 _max_chars: usize,
341 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
342 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
343 }
344
345 fn wiki_hits(
348 &self,
349 _wiki_id: String,
350 _query: String,
351 _context_messages: usize,
352 _per_message_chars: usize,
353 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
354 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
355 }
356
357 fn wiki_session(
360 &self,
361 _wiki_id: String,
362 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiSessionInfo>>> {
363 Box::pin(async { anyhow::bail!("SessionWiki lookups are unavailable") })
364 }
365
366 fn wiki_restore(
369 &self,
370 _request: mj_client::daemon::WikiRestoreRequest,
371 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
372 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
373 }
374}
375
376pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
377 state
378 .subagent
379 .as_ref()
380 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
381}
382
383