mj_controller/server/api/
subagent_backend.rs1use super::*;
2
3pub trait SubagentBackend: Send + Sync {
9 fn events(
10 &self,
11 filter: crate::database::ApiEventFilter,
12 after_seq: Option<u64>,
13 ) -> BoxFuture<'_, AnyResult<crate::database::ApiEventPage>> {
14 events::load_events(filter, after_seq)
15 }
16
17 fn profile_config(
18 &self,
19 profile: String,
20 model: Option<String>,
21 refresh: bool,
22 ) -> BoxFuture<'_, AnyResult<mj_core::worker_launch::ProfileConfig>> {
23 Box::pin(crate::controller::profile_config::discover(
24 profile, model, refresh,
25 ))
26 }
27 fn published_profile_config(
33 &self,
34 _profile: &str,
35 ) -> Option<mj_core::worker_launch::ProfileConfig> {
36 None
37 }
38 fn start_subagent(
39 &self,
40 _request: crate::controller::RegisterSubagentRequest,
41 ) -> BoxFuture<'_, AnyResult<mj_core::subagent::SubagentRecord>> {
42 Box::pin(async { anyhow::bail!("sub-agent creation is unavailable") })
43 }
44 fn list_subagents(
45 &self,
46 parent_session_id: String,
47 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::subagent::SubagentRecord>>> {
48 Box::pin(async move {
49 tokio::task::spawn_blocking(move || crate::database::list_subagents(&parent_session_id))
50 .await?
51 })
52 }
53 fn read_context_file(
54 &self,
55 session_id: String,
56 path: PathBuf,
57 ) -> BoxFuture<'_, std::result::Result<Vec<u8>, ExportError>> {
58 self.read_file(session_id, path)
59 }
60 fn list_workspaces(
63 &self,
64 ) -> BoxFuture<'_, AnyResult<Vec<mj_core::workspace::WorkspaceRecord>>> {
65 Box::pin(async { tokio::task::spawn_blocking(crate::database::list_workspaces).await? })
66 }
67 fn create_workspace(
74 &self,
75 name: String,
76 ) -> BoxFuture<'_, AnyResult<mj_core::workspace::WorkspaceRecord>> {
77 Box::pin(async move {
78 tokio::task::spawn_blocking(move || crate::database::create_or_get_workspace(&name))
79 .await?
80 })
81 }
82 fn set_config(
83 &self,
84 session_id: String,
85 key: String,
86 value: String,
87 ) -> BoxFuture<'_, AnyResult<()>> {
88 Box::pin(async move {
89 self.session_handle(session_id)
90 .await?
91 .ok_or_else(|| anyhow::anyhow!("session has no live actor"))?
92 .set_config(key, value)
93 .await
94 })
95 }
96 fn cancel_start(&self, _session_id: String) -> BoxFuture<'_, AnyResult<()>> {
97 Box::pin(async { Ok(()) })
98 }
99 fn session_handle(&self, session_id: String)
101 -> BoxFuture<'_, AnyResult<Option<SessionHandle>>>;
102
103 fn prompt(&self, session_id: String, text: String) -> BoxFuture<'_, AnyResult<u64>>;
105
106 fn turn_state(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<TurnState>>>;
108
109 fn turn_summary(
111 &self,
112 session_id: String,
113 turn: TurnSpan,
114 ) -> BoxFuture<'_, AnyResult<TurnSummary>>;
115
116 fn start_followup(
118 &self,
119 session_id: String,
120 followup: StartFollowup,
121 ) -> BoxFuture<'_, AnyResult<()>>;
122
123 fn start_status(&self, session_id: String) -> BoxFuture<'_, AnyResult<Option<StartStatus>>>;
125
126 fn transcript(
128 &self,
129 session_id: String,
130 after_seq: u64,
131 limit: usize,
132 role: Option<mj_core::transcript::TranscriptRole>,
133 ) -> BoxFuture<'_, AnyResult<Option<TranscriptPage>>>;
134
135 fn usage(
136 &self,
137 session_id: String,
138 after_seq: u64,
139 limit: usize,
140 ) -> BoxFuture<'_, AnyResult<Option<crate::database::UsagePage>>> {
141 Box::pin(async move {
142 tokio::task::spawn_blocking(move || {
143 crate::database::load_session_usage(&session_id, after_seq, limit)
144 })
145 .await?
146 })
147 }
148
149 fn diff(&self, session_id: String) -> BoxFuture<'_, Result<String, ExportError>>;
151
152 fn read_file(
154 &self,
155 session_id: String,
156 path: PathBuf,
157 ) -> BoxFuture<'_, Result<Vec<u8>, ExportError>>;
158
159 fn write_file(
160 &self,
161 _session_id: String,
162 _path: PathBuf,
163 _bytes: Vec<u8>,
164 _overwrite: bool,
165 ) -> BoxFuture<'_, Result<(), ExportError>> {
166 Box::pin(async { Err(ExportError::Refused("file injection is unavailable".into())) })
167 }
168
169 fn push_branch(
171 &self,
172 session_id: String,
173 branch: String,
174 ) -> BoxFuture<'_, Result<PushedBranch, ExportError>>;
175
176 fn bundle(&self, session_id: String) -> BoxFuture<'_, Result<BundleExport, ExportError>>;
178
179 fn wiki_sync_is_stale(&self) -> bool {
182 false
183 }
184
185 fn wiki_request_sync(&self) {}
188
189 fn wiki_search(
190 &self,
191 _query: String,
192 _limit: usize,
193 ) -> BoxFuture<'_, AnyResult<mj_client::daemon::WikiSearchPage>> {
194 Box::pin(async { anyhow::bail!("SessionWiki search is unavailable") })
195 }
196
197 fn wiki_brief(
199 &self,
200 _wiki_id: String,
201 _max_chars: usize,
202 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
203 Box::pin(async { anyhow::bail!("SessionWiki briefings are unavailable") })
204 }
205
206 fn wiki_hits(
209 &self,
210 _wiki_id: String,
211 _query: String,
212 _context_messages: usize,
213 _per_message_chars: usize,
214 ) -> BoxFuture<'_, AnyResult<Option<mj_client::daemon::WikiHitTranscript>>> {
215 Box::pin(async { anyhow::bail!("SessionWiki transcript hits are unavailable") })
216 }
217
218 fn wiki_restore(
221 &self,
222 _request: mj_client::daemon::WikiRestoreRequest,
223 ) -> BoxFuture<'_, AnyResult<Option<String>>> {
224 Box::pin(async { anyhow::bail!("SessionWiki restore is unavailable") })
225 }
226}
227
228pub(super) fn backend(state: &ServerState) -> Result<&Arc<dyn SubagentBackend>, ApiFailure> {
229 state
230 .subagent
231 .as_ref()
232 .ok_or_else(|| ApiFailure::unavailable("this server has no subagent backend installed"))
233}
234
235