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 transcript_history(
16 &self,
17 session_id: String,
18 before: Option<mj_core::storage::TranscriptCursor>,
19 ) -> BoxFuture<'_, AnyResult<mj_core::storage::TranscriptHistoryPage>> {
20 Box::pin(async move {
21 tokio::task::spawn_blocking(move || {
22 crate::database::load_transcript_history(&session_id, before.as_ref(), 128)?
23 .context("session history is unavailable")
24 })
25 .await?
26 })
27 }
28
29 fn native_agent_history(
30 &self,
31 owner: String,
32 child: String,
33 before: Option<(u64, String)>,
34 ) -> BoxFuture<'_, AnyResult<mj_core::native_agent::NativeAgentHistoryPage>> {
35 Box::pin(async move {
36 tokio::task::spawn_blocking(move || {
37 crate::database::native_agent_history(&owner, &child, before)
38 })
39 .await?
40 })
41 }
42 fn events(
43 &self,
44 filter: crate::database::ApiEventFilter,
45 after_seq: Option<u64>,
46 ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
47 events::load_events(filter, after_seq)
48 }
49
50 fn profile_config(
51 &self,
52 profile: String,
53 model: Option<String>,
54 refresh: bool,
55 ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
56 Box::pin(crate::controller::profile_config::discover(
57 profile, model, refresh,
58 ))
59 }
60 fn subagent_candidates(
65 &self,
66 _parent_profile: String,
67 ) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
68 Box::pin(async { anyhow::bail!("sub-agent profiles are unavailable") })
69 }
70 fn session_profile_candidates(&self) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
74 Box::pin(async { anyhow::bail!("session profile selection is unavailable") })
75 }
76 fn start_subagent(
77 &self,
78 _request: crate::controller::RegisterSubagentRequest,
79 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
80 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
81 }
82 fn list_subagents(
83 &self,
84 parent_session_id: String,
85 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
86 Box::pin(async move {
87 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
88 .await?
89 })
90 }
91 fn subagent_handed_back(&self, child_session_id: String) -> BoxFuture<'_, AnyResult<bool>> {
94 Box::pin(async move {
95 tokio::task::spawn_blocking(move || {
96 crate::controller::subagent_has_handed_back(&child_session_id)
97 })
98 .await?
99 })
100 }
101 fn read_context_file(
102 &self,
103 session_id: String,
104 path: PathBuf,
105 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
106 self.read_file(session_id, path)
107 }
108 fn list_workspaces(
111 &self,
112 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
113 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
114 }
115 fn create_workspace(
122 &self,
123 name: String,
124 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
125 Box::pin(async move {
126 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
127 .await?
128 })
129 }
130 fn set_config(
131 &self,
132 session_id: String,
133 key: String,
134 value: String,
135 ) -> BoxFuture<'_, AnyResult<()>> {
136 Box::pin(async move {
137 self.session_handle(session_id)
138 .await?
139 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
140 .set_config(key, value)
141 .await
142 })
143 }
144 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
145 Box::pin(async { Ok(()) })
146 }
147 fn session_handle(&self, session_id: String)
149 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
150
151 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
153
154 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
156
157 fn subagent_report(
160 &self,
161 _session_id: String,
162 ) -> BoxFuture<'_, AnyResult<Option<(bool, mj_core::subagent::SubagentReport)>>> {
163 Box::pin(async { Ok(None) })
164 }
165
166 fn turn_summary(
168 &self,
169 session_id: String,
170 turn: TurnSpan,
171 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
172
173 fn start_followup(
175 &self,
176 session_id: String,
177 followup: StartFollowup,
178 ) -> BoxFuture<'_, AnyResult<()>>;
179
180 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
182
183 fn transcript(
185 &self,
186 session_id: String,
187 after_seq: u64,
188 limit: usize,
189 roles: Vec<mj_core::transcript::TranscriptRole>,
190 finished_only: bool,
191 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
192
193 fn usage(
194 &self,
195 session_id: String,
196 after_seq: u64,
197 limit: usize,
198 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
199 Box::pin(async move {
200 tokio::task::spawn_blocking(move || {
201 crate::database::load_session_usage(&session_id, after_seq, limit)
202 })
203 .await?
204 })
205 }
206
207 fn usage_tree(
208 &self,
209 parent: String,
210 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsageTree>>> {
211 Box::pin(async move {
212 tokio::task::spawn_blocking(move || crate::database::load_usage_tree(&parent)).await?
213 })
214 }
215
216 fn diff(
218 &self,
219 session_id: String,
220 options: DiffOptions,
221 ) -> BoxFuture<'_, Result<String, ExportError>>;
222
223 fn read_file(
225 &self,
226 session_id: String,
227 path: PathBuf,
228 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
229
230 fn write_file(
231 &self,
232 _session_id: String,
233 _path: PathBuf,
234 _bytes: Vec<u8>,
235 _overwrite: bool,
236 ) -> BoxFuture<'_, Result<(), ExportError>> {
237 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
238 }
239
240 fn push_branch(
242 &self,
243 session_id: String,
244 branch: String,
245 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
246
247 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
249
250 fn wiki_sync_is_stale(&self) -> bool {
253 false
254 }
255
256 fn wiki_request_sync(&self) {}
259
260 fn wiki_search(
261 &self,
262 _query: String,
263 _limit: usize,
264 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
265 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
266 }
267
268 fn wiki_brief(
270 &self,
271 _wiki_id: String,
272 _max_chars: usize,
273 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
274 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
275 }
276
277 fn wiki_hits(
280 &self,
281 _wiki_id: String,
282 _query: String,
283 _context_messages: usize,
284 _per_message_chars: usize,
285 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
286 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
287 }
288
289 fn wiki_session(
292 &self,
293 _wiki_id: String,
294 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiSessionInfo>>> {
295 Box::pin(async { anyhow::bail!("SessionWiki lookups are unavailable") })
296 }
297
298 fn wiki_restore(
301 &self,
302 _request: mj_client::daemon::WikiRestoreRequest,
303 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
304 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
305 }
306}
307
308pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
309 state
310 .subagent
311 .as_ref()
312 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
313}
314
315