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 start_subagent(
71 &self,
72 _request: crate::controller::RegisterSubagentRequest,
73 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
74 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
75 }
76 fn list_subagents(
77 &self,
78 parent_session_id: String,
79 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
80 Box::pin(async move {
81 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
82 .await?
83 })
84 }
85 fn subagent_handed_back(&self, child_session_id: String) -> BoxFuture<'_, AnyResult<bool>> {
88 Box::pin(async move {
89 tokio::task::spawn_blocking(move || {
90 crate::controller::subagent_has_handed_back(&child_session_id)
91 })
92 .await?
93 })
94 }
95 fn read_context_file(
96 &self,
97 session_id: String,
98 path: PathBuf,
99 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
100 self.read_file(session_id, path)
101 }
102 fn list_workspaces(
105 &self,
106 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
107 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
108 }
109 fn create_workspace(
116 &self,
117 name: String,
118 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
119 Box::pin(async move {
120 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
121 .await?
122 })
123 }
124 fn set_config(
125 &self,
126 session_id: String,
127 key: String,
128 value: String,
129 ) -> BoxFuture<'_, AnyResult<()>> {
130 Box::pin(async move {
131 self.session_handle(session_id)
132 .await?
133 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
134 .set_config(key, value)
135 .await
136 })
137 }
138 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
139 Box::pin(async { Ok(()) })
140 }
141 fn session_handle(&self, session_id: String)
143 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
144
145 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
147
148 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
150
151 fn subagent_report(
154 &self,
155 _session_id: String,
156 ) -> BoxFuture<'_, AnyResult<Option<(bool, mj_core::subagent::SubagentReport)>>> {
157 Box::pin(async { Ok(None) })
158 }
159
160 fn turn_summary(
162 &self,
163 session_id: String,
164 turn: TurnSpan,
165 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
166
167 fn start_followup(
169 &self,
170 session_id: String,
171 followup: StartFollowup,
172 ) -> BoxFuture<'_, AnyResult<()>>;
173
174 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
176
177 fn transcript(
179 &self,
180 session_id: String,
181 after_seq: u64,
182 limit: usize,
183 role: Option<mj_core::transcript::TranscriptRole>,
184 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
185
186 fn usage(
187 &self,
188 session_id: String,
189 after_seq: u64,
190 limit: usize,
191 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
192 Box::pin(async move {
193 tokio::task::spawn_blocking(move || {
194 crate::database::load_session_usage(&session_id, after_seq, limit)
195 })
196 .await?
197 })
198 }
199
200 fn usage_tree(
201 &self,
202 parent: String,
203 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsageTree>>> {
204 Box::pin(async move {
205 tokio::task::spawn_blocking(move || crate::database::load_usage_tree(&parent)).await?
206 })
207 }
208
209 fn diff(
211 &self,
212 session_id: String,
213 options: DiffOptions,
214 ) -> BoxFuture<'_, Result<String, ExportError>>;
215
216 fn read_file(
218 &self,
219 session_id: String,
220 path: PathBuf,
221 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
222
223 fn write_file(
224 &self,
225 _session_id: String,
226 _path: PathBuf,
227 _bytes: Vec<u8>,
228 _overwrite: bool,
229 ) -> BoxFuture<'_, Result<(), ExportError>> {
230 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
231 }
232
233 fn push_branch(
235 &self,
236 session_id: String,
237 branch: String,
238 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
239
240 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
242
243 fn wiki_sync_is_stale(&self) -> bool {
246 false
247 }
248
249 fn wiki_request_sync(&self) {}
252
253 fn wiki_search(
254 &self,
255 _query: String,
256 _limit: usize,
257 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
258 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
259 }
260
261 fn wiki_brief(
263 &self,
264 _wiki_id: String,
265 _max_chars: usize,
266 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
267 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
268 }
269
270 fn wiki_hits(
273 &self,
274 _wiki_id: String,
275 _query: String,
276 _context_messages: usize,
277 _per_message_chars: usize,
278 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
279 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
280 }
281
282 fn wiki_session(
285 &self,
286 _wiki_id: String,
287 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiSessionInfo>>> {
288 Box::pin(async { anyhow::bail!("SessionWiki lookups are unavailable") })
289 }
290
291 fn wiki_restore(
294 &self,
295 _request: mj_client::daemon::WikiRestoreRequest,
296 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
297 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
298 }
299}
300
301pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
302 state
303 .subagent
304 .as_ref()
305 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
306}
307
308