mj_controller/server/api/
subagent_backend.rs1use super::*;
2
3pub trait SubagentBackend: Send + Sync {
9 fn runtime_receipt(
10 &self,
11 _session_id: String,
12 ) -> BoxFuture<'_, AnyResult<Option<mj_core::harness_runtime::RuntimeReceipt>>> {
13 Box::pin(async { Ok(None) })
14 }
15
16 fn transcript_history(
17 &self,
18 session_id: String,
19 before: Option<mj_core::storage::TranscriptCursor>,
20 ) -> BoxFuture<'_, AnyResult<mj_core::storage::TranscriptHistoryPage>> {
21 Box::pin(async move {
22 tokio::task::spawn_blocking(move || {
23 crate::database::load_transcript_history(&session_id, before.as_ref(), 128)?
24 .context("session history is unavailable")
25 })
26 .await?
27 })
28 }
29
30 fn native_agent_history(
31 &self,
32 owner: String,
33 child: String,
34 before: Option<(u64, String)>,
35 ) -> BoxFuture<'_, AnyResult<mj_core::native_agent::NativeAgentHistoryPage>> {
36 Box::pin(async move {
37 tokio::task::spawn_blocking(move || {
38 crate::database::native_agent_history(&owner, &child, before)
39 })
40 .await?
41 })
42 }
43 fn events(
44 &self,
45 filter: crate::database::ApiEventFilter,
46 after_seq: Option<u64>,
47 ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
48 events::load_events(filter, after_seq)
49 }
50
51 fn profile_config(
52 &self,
53 profile: String,
54 model: Option<String>,
55 refresh: bool,
56 ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
57 Box::pin(crate::controller::profile_config::discover(
58 profile, model, refresh,
59 ))
60 }
61 fn subagent_candidates(
66 &self,
67 _parent_profile: String,
68 ) -> BoxFuture<'_, AnyResult<SubagentCandidates>> {
69 Box::pin(async { anyhow::bail!("sub-agent profiles are unavailable") })
70 }
71 fn start_subagent(
72 &self,
73 _request: crate::controller::RegisterSubagentRequest,
74 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
75 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
76 }
77 fn list_subagents(
78 &self,
79 parent_session_id: String,
80 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
81 Box::pin(async move {
82 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
83 .await?
84 })
85 }
86 fn subagent_handed_back(&self, child_session_id: String) -> BoxFuture<'_, AnyResult<bool>> {
89 Box::pin(async move {
90 tokio::task::spawn_blocking(move || {
91 crate::controller::subagent_has_handed_back(&child_session_id)
92 })
93 .await?
94 })
95 }
96 fn read_context_file(
97 &self,
98 session_id: String,
99 path: PathBuf,
100 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
101 self.read_file(session_id, path)
102 }
103 fn list_workspaces(
106 &self,
107 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
108 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
109 }
110 fn create_workspace(
117 &self,
118 name: String,
119 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
120 Box::pin(async move {
121 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
122 .await?
123 })
124 }
125 fn set_config(
126 &self,
127 session_id: String,
128 key: String,
129 value: String,
130 ) -> BoxFuture<'_, AnyResult<()>> {
131 Box::pin(async move {
132 self.session_handle(session_id)
133 .await?
134 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
135 .set_config(key, value)
136 .await
137 })
138 }
139 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
140 Box::pin(async { Ok(()) })
141 }
142 fn session_handle(&self, session_id: String)
144 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
145
146 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
148
149 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
151
152 fn subagent_report(
155 &self,
156 _session_id: String,
157 ) -> BoxFuture<'_, AnyResult<Option<(bool, mj_core::subagent::SubagentReport)>>> {
158 Box::pin(async { Ok(None) })
159 }
160
161 fn turn_summary(
163 &self,
164 session_id: String,
165 turn: TurnSpan,
166 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
167
168 fn start_followup(
170 &self,
171 session_id: String,
172 followup: StartFollowup,
173 ) -> BoxFuture<'_, AnyResult<()>>;
174
175 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
177
178 fn transcript(
180 &self,
181 session_id: String,
182 after_seq: u64,
183 limit: usize,
184 role: Option<mj_core::transcript::TranscriptRole>,
185 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
186
187 fn usage(
188 &self,
189 session_id: String,
190 after_seq: u64,
191 limit: usize,
192 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
193 Box::pin(async move {
194 tokio::task::spawn_blocking(move || {
195 crate::database::load_session_usage(&session_id, after_seq, limit)
196 })
197 .await?
198 })
199 }
200
201 fn diff(
203 &self,
204 session_id: String,
205 options: DiffOptions,
206 ) -> BoxFuture<'_, Result<String, ExportError>>;
207
208 fn read_file(
210 &self,
211 session_id: String,
212 path: PathBuf,
213 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
214
215 fn write_file(
216 &self,
217 _session_id: String,
218 _path: PathBuf,
219 _bytes: Vec<u8>,
220 _overwrite: bool,
221 ) -> BoxFuture<'_, Result<(), ExportError>> {
222 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
223 }
224
225 fn push_branch(
227 &self,
228 session_id: String,
229 branch: String,
230 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
231
232 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
234
235 fn wiki_sync_is_stale(&self) -> bool {
238 false
239 }
240
241 fn wiki_request_sync(&self) {}
244
245 fn wiki_search(
246 &self,
247 _query: String,
248 _limit: usize,
249 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
250 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
251 }
252
253 fn wiki_brief(
255 &self,
256 _wiki_id: String,
257 _max_chars: usize,
258 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
259 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
260 }
261
262 fn wiki_hits(
265 &self,
266 _wiki_id: String,
267 _query: String,
268 _context_messages: usize,
269 _per_message_chars: usize,
270 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
271 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
272 }
273
274 fn wiki_session(
277 &self,
278 _wiki_id: String,
279 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiSessionInfo>>> {
280 Box::pin(async { anyhow::bail!("SessionWiki lookups are unavailable") })
281 }
282
283 fn wiki_restore(
286 &self,
287 _request: mj_client::daemon::WikiRestoreRequest,
288 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
289 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
290 }
291}
292
293pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
294 state
295 .subagent
296 .as_ref()
297 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
298}
299
300