Skip to main content

mj_controller/server/api/
types.rs

1use super::*;
2
3/// Comparison and representation requested for a session's working-tree diff.
4#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct DiffOptions {
7    pub base: Option<String>,
8    #[serde(default)]
9    pub json: bool,
10}
11
12/// Observed provider-owned background work; absent when no live snapshot is available.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ApiBackgroundWork {
15    pub known: Option<bool>,
16    pub tasks: Vec<mj_core::relay::BackgroundCommand>,
17}
18
19impl From<&mj_core::relay::RelayOperationalState> for ApiBackgroundWork {
20    fn from(state: &mj_core::relay::RelayOperationalState) -> Self {
21        Self {
22            known: state.background_work_known,
23            tasks: state.background_commands.clone(),
24        }
25    }
26}
27
28/// One session as the API presents it. This is a narrower, more stable shape
29/// than the viewer's own session projection, which changes whenever the browser
30/// needs something new.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct ApiSession {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub assessment: Option<mj_core::assessment::Summary>,
35    #[serde(default)]
36    pub subagents: mj_core::subagent::SubagentPolicy,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub runtime: Option<mj_core::harness_runtime::RuntimeReceipt>,
39    /// Immutable starting selection, named as `StartSessionRequest` names
40    /// it; session readiness verifies preparation.
41    /// Commit the workspace started checked out at, when one was named.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub at: Option<String>,
44    /// Branch created at `at`, or the existing branch checked out without it.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub branch: Option<String>,
47    /// Diff base the session was started with; `at` unless another was named.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub base: Option<String>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub expected_runtime_identity: Option<String>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub background_work: Option<ApiBackgroundWork>,
54    pub id: String,
55    pub workspace_id: String,
56    pub title: String,
57    pub harness_kind: String,
58    pub profile_id: String,
59    pub target_id: String,
60    pub bundle_id: String,
61    pub state: String,
62    pub lifecycle: ViewerLifecycleCategory,
63    pub chat_phase: crate::server::ViewerChatPhase,
64    pub is_idle: bool,
65    /// What this session is doing, in more detail than `chat_phase`'s four
66    /// values allow: in particular it can say that the daemon cannot see the
67    /// worker and report what it last knew, rather than claiming idleness it
68    /// cannot prove.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub activity_state: Option<mj_core::activity::ActivityState>,
71    pub has_error: bool,
72    /// Why a launch failed, for a session in the error state. Absent
73    /// otherwise: raw runtime error text is deliberately not published for a
74    /// running session. Why a *turn* failed travels in `last_turn_diagnostic`,
75    /// which a single-session query fills.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub error: Option<String>,
78    pub created_at: String,
79    pub updated_at: String,
80    /// How the last finished prompt ended. Absent unless the caller asked for
81    /// one session by id or waited on it, because the dashboard projection the
82    /// list is built from does not carry turn identity.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub last_turn_outcome: Option<MaterializedTurnOutcome>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub last_turn_diagnostic: Option<mj_core::diagnostic::TurnDiagnostic>,
87    #[serde(default)]
88    pub config_options: Vec<crate::server::ViewerConfigOption>,
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub pending_elicitations: Vec<mj_core::elicitation::ElicitationRequest>,
91}
92
93impl From<&ViewerSession> for ApiSession {
94    fn from(session: &ViewerSession) -> Self {
95        Self {
96            assessment: None,
97            subagents: session.subagents.clone(),
98            background_work: None,
99            at: session.at.clone(),
100            branch: session.branch.clone(),
101            base: session.base.clone(),
102            expected_runtime_identity: session.expected_runtime_identity.clone(),
103            runtime: None,
104            id: session.id.clone(),
105            workspace_id: session.workspace_id.clone(),
106            title: session.title.clone(),
107            harness_kind: session.harness_kind.clone(),
108            profile_id: session.profile_id.clone(),
109            target_id: session.target_id.clone(),
110            bundle_id: session.bundle_id.clone(),
111            state: session.state.clone(),
112            lifecycle: session.lifecycle,
113            chat_phase: session.chat_phase,
114            is_idle: session.is_idle,
115            activity_state: session.activity_state.clone(),
116            has_error: session.has_error,
117            error: session.launch_error.clone(),
118            created_at: session.created_at.clone(),
119            updated_at: session.updated_at.clone(),
120            last_turn_outcome: None,
121            last_turn_diagnostic: None,
122            config_options: session.config_options.clone(),
123            pending_elicitations: session.pending_elicitations.clone(),
124        }
125    }
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct SessionListResponse {
130    pub sessions: Vec<ApiSession>,
131}
132
133/// The workspaces the daemon holds, newest opening first, exactly as the
134/// terminal's workspace tabs and the viewer's list see them.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct WorkspaceListResponse {
137    pub workspaces: Vec<mj_core::workspace::WorkspaceRecord>,
138}
139
140/// Name the workspace to work in. The name is the identity: it is trimmed, at
141/// most 64 characters, and unique case-insensitively, so naming one that
142/// already exists returns it rather than making a second.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct CreateWorkspaceRequest {
146    pub name: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct CreateWorkspaceResponse {
151    pub workspace: mj_core::workspace::WorkspaceRecord,
152}
153
154/// Create a session and, optionally, send its first prompt. Served in M2.
155///
156/// `profile_id` and `target_id` may be omitted, and each resolves
157/// independently: a caller may name a profile and take the saved default
158/// target. An omitted identifier comes from the pair the user last saved with
159/// the `mj go` workflow, which the first setup also becomes, so a caller that
160/// has never read `config.toml` can create a session by naming neither. The
161/// controller still receives two explicit identifiers, because a session whose
162/// profile was implicit would be a session nobody can explain later.
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct StartSessionRequest {
166    #[serde(default)]
167    pub create_managed_worktree: Option<bool>,
168    /// Full commit object ID to start the workspace at: an exact checkout of
169    /// the bundle's primary repository, detached unless `branch` is given.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub at: Option<String>,
172    /// With `at`, the new branch created there; otherwise the existing branch
173    /// to check out in a new isolated workspace.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub branch: Option<String>,
176    /// Diff base, when it is not `at`. Without `at`, also the starting
177    /// revision for a raw managed worktree.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub base: Option<String>,
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub expected_runtime_identity: Option<String>,
182    /// Omitted reuses the last accepted top-level session choice.
183    #[serde(default)]
184    pub subagents: Option<mj_core::subagent::SubagentPolicy>,
185    #[serde(default)]
186    pub workspace_id: Option<String>,
187    /// Omitted follows the saved default. See the type's own documentation.
188    #[serde(default)]
189    pub profile_id: Option<String>,
190    /// Omitted follows the saved default. See the type's own documentation.
191    #[serde(default)]
192    pub target_id: Option<String>,
193    #[serde(default)]
194    pub bundle_id: Option<String>,
195    #[serde(default)]
196    pub project_directory: Option<PathBuf>,
197    #[serde(default)]
198    pub title: Option<String>,
199    #[serde(default)]
200    pub model: Option<String>,
201    #[serde(default)]
202    pub effort: Option<String>,
203    #[serde(default)]
204    pub prompt: Option<String>,
205}
206
207/// Resume a stopped, lost, or failed session. Every field is optional: the
208/// session's own record supplies what the caller does not name, which is what
209/// makes `POST .../resume` with no body the scriptable "continue this session"
210/// call.
211#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(deny_unknown_fields)]
213pub struct ResumeSessionRequest {
214    /// Profile to resume on. Defaults to the one the session last ran.
215    #[serde(default)]
216    pub profile_id: Option<String>,
217    /// Target template to provision. Defaults to the session's own.
218    #[serde(default)]
219    pub target_id: Option<String>,
220    /// Workspace the resumed session belongs to. Defaults to its own.
221    #[serde(default)]
222    pub workspace_id: Option<String>,
223    /// Whether prompts queued when the session stopped are started or
224    /// discarded. Defaults to `start`, which is what the terminal's own resume
225    /// wizard defaults to.
226    #[serde(default)]
227    pub queue: Option<mj_core::state::ResumeQueueDisposition>,
228}
229
230/// What a resume was accepted as: the settings it will actually use, resolved
231/// from the request and the session's record.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct ResumeSessionResponse {
234    pub session_id: String,
235    pub workspace_id: String,
236    pub profile_id: String,
237    pub target_id: String,
238}
239
240/// What a suspend was accepted as. A suspend stops the session's active
241/// Mjolnir sub-agents without a checkpoint and suspends the session alone.
242#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
243pub struct SuspendSessionResponse {
244    #[serde(default)]
245    pub session_id: String,
246    /// Active sub-agents the suspend stops.
247    #[serde(default)]
248    pub stopped_subagents: usize,
249    /// How many of those have not handed back their report.
250    #[serde(default)]
251    pub subagents_not_handed_back: usize,
252    /// "N sub-agents have not handed back; suspending stops them", when any
253    /// have not.
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub warning: Option<String>,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct StartSessionResponse {
260    pub session_id: String,
261    /// The turn the follow-up prompt was accepted as, once it has been
262    /// submitted. Creation answers before that, so it is usually absent.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub turn_id: Option<u64>,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(deny_unknown_fields)]
269pub struct SubagentSourceRange {
270    pub file: PathBuf,
271    pub start: u64,
272    pub end: u64,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct SpawnSubagentRequest {
278    pub task_name: String,
279    pub instructions: String,
280    #[serde(default)]
281    pub profile_id: Option<String>,
282    #[serde(default)]
283    pub model: Option<String>,
284    #[serde(default)]
285    pub effort: Option<String>,
286    #[serde(default)]
287    pub working_directory: Option<PathBuf>,
288    #[serde(default)]
289    pub context: Option<String>,
290    #[serde(default)]
291    pub files: Vec<SubagentSourceRange>,
292}
293
294/// One profile a parent may start a sub-agent on, with what it offers and how
295/// much of its quota is left.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct SubagentCandidate {
298    pub profile_id: String,
299    pub harness: mj_core::config::HarnessKind,
300    pub choices: mj_core::worker_launch::ProfileConfig,
301    /// The lower of the profile's quota windows (the 5-hour and weekly ones
302    /// for Codex and Claude), 100 for a pay-per-use profile, and `None` when
303    /// no usable report exists.
304    pub remaining_percent: Option<u8>,
305}
306
307/// The profiles a parent may delegate to, split into those whose choices are
308/// known and those whose discovery failed, with the reason.
309#[derive(Debug, Clone, Default, PartialEq, Eq)]
310pub struct SubagentCandidates {
311    pub offered: Vec<SubagentCandidate>,
312    pub unavailable: Vec<(String, String)>,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub struct SubagentView {
317    pub parent_session_id: String,
318    pub task_name: String,
319    pub session: ApiSession,
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct SubagentListResponse {
324    pub subagents: Vec<SubagentView>,
325}
326
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct PromptRequest {
330    pub text: String,
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct PromptResponse {
335    /// The relay acceptance ordinal for this prompt, which is what `wait`
336    /// takes as `turn_id`.
337    pub turn_id: u64,
338}
339
340#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
341#[serde(deny_unknown_fields)]
342pub struct WaitRequest {
343    /// Return when the harness presents a structured input request.
344    #[serde(default)]
345    pub return_on_input: bool,
346    /// Wait for this specific prompt. Absent means "wait until the session is
347    /// idle with nothing queued", which is what a caller that lost its turn id
348    /// wants.
349    #[serde(default)]
350    pub turn_id: Option<u64>,
351    #[serde(default)]
352    pub timeout_secs: Option<u64>,
353}
354
355/// How a wait ended.
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358pub enum WaitOutcome {
359    /// A structured elicitation needs an answer; only returned by opt-in waits.
360    InputRequired,
361    /// The turn completed normally.
362    Finished,
363    /// The turn failed, was rejected, or the session reported an error.
364    Error,
365    /// The turn was cancelled or interrupted.
366    Cancelled,
367    /// The model was at capacity and no retry is armed.
368    QuotaLimit,
369    /// The wait's deadline passed with the turn still running.
370    Timeout,
371    /// The session stopped or is stopping, so no turn can finish on it.
372    Stopped,
373}
374
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct WaitCapacityRetry {
377    pub attempt: u32,
378    pub retry_at_ms: i64,
379}
380
381impl From<&CapacityRetry> for WaitCapacityRetry {
382    fn from(retry: &CapacityRetry) -> Self {
383        Self {
384            attempt: retry.attempt,
385            retry_at_ms: retry.retry_at_ms,
386        }
387    }
388}
389
390/// How the daemon's live view of a session's relay is doing.
391///
392/// This reports; it never decides an outcome. A caller that gets `timeout`
393/// needs to tell "the turn is still working" from "the daemon cannot see the
394/// worker at all", and those look identical without it.
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "snake_case")]
397pub enum RelayState {
398    /// The daemon is attached to the worker and following its events.
399    Connected,
400    /// Not attached, with no error recorded yet: attaching, or between tries.
401    Disconnected,
402    /// The worker could not be reached.
403    Unreachable,
404    /// The session's target is gone.
405    TargetMissing,
406    /// The event stream did not line up with what the daemon had projected.
407    ProjectionIntegrity,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct RelayHealth {
412    pub state: RelayState,
413    /// The view's own description of the problem, when it recorded one.
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub detail: Option<String>,
416}
417
418impl From<&mj_client::session::ManagedSessionView> for RelayHealth {
419    fn from(view: &mj_client::session::ManagedSessionView) -> Self {
420        use mj_client::session::ViewError;
421        // A recorded error outranks `connected`: it is the specific thing
422        // standing between the caller and a finished turn.
423        match &view.error {
424            Some(error) => Self {
425                state: match error {
426                    ViewError::Unreachable(_) => RelayState::Unreachable,
427                    ViewError::TargetMissing(_) => RelayState::TargetMissing,
428                    ViewError::ProjectionIntegrity(_) => RelayState::ProjectionIntegrity,
429                },
430                detail: Some(error.detail().to_owned()),
431            },
432            None if view.connected => Self {
433                state: RelayState::Connected,
434                detail: None,
435            },
436            None => Self {
437                state: RelayState::Disconnected,
438                detail: None,
439            },
440        }
441    }
442}
443
444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
445pub struct WaitResponse {
446    #[serde(default, skip_serializing_if = "Option::is_none")]
447    pub diagnostic: Option<mj_core::diagnostic::TurnDiagnostic>,
448
449    #[serde(default, skip_serializing_if = "Vec::is_empty")]
450    pub pending_elicitations: Vec<mj_core::elicitation::ElicitationRequest>,
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub usage: Option<mj_core::usage::TokenUsage>,
453    pub outcome: WaitOutcome,
454    /// The harness's own stop reason, when the turn reached one.
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub stop_reason: Option<String>,
457    /// Why the wait ended this way, when there is something to say.
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub message: Option<String>,
460    /// The agent's last message of the turn, flattened to text. For a
461    /// sub-agent child it is the report the child handed back, when it did.
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub final_message: Option<String>,
464    /// For a sub-agent child: `handback` when `final_message` is the report
465    /// the child handed back, `last_message` when it is the turn's last
466    /// message. Absent for every other session.
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub report_source: Option<String>,
469    /// The turn this answer is about: the latest turn that ended at or after
470    /// the requested one, since a session keeps only its latest turn's outcome.
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub turn_id: Option<u64>,
473    /// The turn the request named, so a caller can see when `turn_id` is a
474    /// later one.
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub requested_turn_id: Option<u64>,
477    /// One-based position of this turn in the conversation.
478    #[serde(default, skip_serializing_if = "Option::is_none")]
479    pub turn_number: Option<u64>,
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub elapsed_ms: Option<i64>,
482    /// How many tool calls the turn made. Absent when the wait ended without a
483    /// finished turn.
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub tool_calls: Option<u64>,
486    /// Legacy alias for a worker-owned server retry, retained for older clients.
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub capacity_retry: Option<WaitCapacityRetry>,
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub server_retry: Option<WaitCapacityRetry>,
491    #[serde(default)]
492    pub retry_assessment_pending: bool,
493    #[serde(default, skip_serializing_if = "Option::is_none")]
494    pub quota_recovery: Option<mj_core::continuation::QuotaRecovery>,
495    /// The health of the daemon's live view of this session. Absent when no
496    /// live actor holds the session, because there is then no view to report
497    /// on and inventing one would be worse than saying nothing.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub relay: Option<RelayHealth>,
500    pub session: ApiSession,
501}
502
503/// How many transcript items a page carries when the caller names no limit,
504/// and the most it may ask for. A caller that asks for more gets the ceiling
505/// rather than an error: paging is the point, and refusing a large limit would
506/// only make the caller retry with a smaller one.
507pub const DEFAULT_TRANSCRIPT_LIMIT: usize = 200;
508pub const MAX_TRANSCRIPT_LIMIT: usize = 1_000;
509
510#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
511pub struct TranscriptQuery {
512    #[serde(default)]
513    pub role: Option<mj_core::transcript::TranscriptRole>,
514    /// Resume from the highest sequence the caller has already seen.
515    #[serde(default)]
516    pub after_seq: Option<u64>,
517    #[serde(default)]
518    pub limit: Option<usize>,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
522pub struct TranscriptItemView {
523    pub stable_id: String,
524    pub position: u64,
525    /// What to pass as the next `after_seq`. It is the position for everything
526    /// but an agent message, which carries the ordinal of its latest content.
527    pub seq: u64,
528    pub role: String,
529    /// The item flattened to text, which is what a reading caller wants.
530    pub text: String,
531    pub created_at_ms: i64,
532    pub last_changed_at_ms: i64,
533    /// The stored body, for a caller that needs the structure behind the text.
534    pub body: mj_core::transcript::TranscriptBody,
535}
536
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538pub struct TranscriptResponse {
539    #[serde(default)]
540    pub next_after_seq: u64,
541    pub session_id: String,
542    /// The newest sequence in the whole transcript. A page whose last item
543    /// reaches this is up to date.
544    pub latest_seq: u64,
545    pub execution: MaterializedExecutionState,
546    pub items: Vec<TranscriptItemView>,
547}
548
549// ---------------------------------------------------------------------------
550// Backend
551// ---------------------------------------------------------------------------
552
553/// Where a session stands turn by turn, read from the durable projection when
554/// no live actor holds the session.
555#[derive(Debug, Clone, PartialEq, Eq)]
556pub struct TurnState {
557    pub execution: MaterializedExecutionState,
558    pub active_turn: Option<MaterializedTurn>,
559    pub last_turn_outcome: Option<MaterializedTurnOutcome>,
560}
561
562pub use crate::database::TurnSummary;
563
564/// Configuration and a first prompt to apply once a newly created session's
565/// harness is ready. Served in M2.
566#[derive(Debug, Clone, Default, PartialEq, Eq)]
567pub struct StartFollowup {
568    pub model: Option<String>,
569    pub effort: Option<String>,
570    pub prompt: Option<String>,
571    pub fast_mode: bool,
572}
573
574/// How far a created session's follow-up has got. Served in M2.
575#[derive(Debug, Clone, PartialEq, Eq)]
576pub enum StartStatus {
577    /// The session is still provisioning, or its harness is not ready.
578    Pending,
579    /// The follow-up prompt was submitted and accepted as this turn.
580    Submitted { turn_id: u64 },
581    /// The session could not be started, or the follow-up could not be applied.
582    Failed { message: String },
583}
584
585/// A page of transcript items, read from the durable projection.
586pub use crate::database::TranscriptPage;
587
588/// A branch the daemon pushed on the caller's behalf.
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
590pub struct PushedBranch {
591    pub branch: String,
592    pub remote: String,
593}
594
595/// Which file of the session's workspace to read.
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
597pub struct FileQuery {
598    /// Path relative to the session's workspace root.
599    pub path: String,
600}
601
602/// What form the caller wants the session's work in.
603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
604#[serde(rename_all = "snake_case")]
605pub enum ExportKind {
606    /// A unified diff, as `GET /diff` returns.
607    Patch,
608    /// A branch pushed to the repository's push remote.
609    Branch,
610    /// The git bundle of the session's committed work.
611    Bundle,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615pub struct ExportRequest {
616    pub kind: ExportKind,
617    /// The branch to push. Required when `kind` is `branch`.
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub branch: Option<String>,
620}
621
622/// A git bundle of the session's work.
623#[derive(Debug, Clone)]
624pub struct BundleExport {
625    pub repository: String,
626    pub bytes: Vec<u8>,
627}
628
629/// Why an export could not be produced. Served in M4.
630#[derive(Debug)]
631pub enum ExportError {
632    /// The session is in a state where this export is not possible. The caller
633    /// can act on it, so it answers 409.
634    Refused(String),
635    /// The export was attempted and failed.
636    Failed(anyhow::Error),
637}
638
639impl From<ExportError> for ApiFailure {
640    fn from(error: ExportError) -> Self {
641        match error {
642            ExportError::Refused(message) => Self::conflict(message),
643            ExportError::Failed(error) => Self::from(error),
644        }
645    }
646}
647
648// ---------------------------------------------------------------------------
649// SessionWiki
650// ---------------------------------------------------------------------------
651
652#[derive(Debug, Default, Deserialize)]
653#[serde(deny_unknown_fields)]
654pub struct WikiSearchQuery {
655    #[serde(default)]
656    pub q: Option<String>,
657    #[serde(default)]
658    pub limit: Option<usize>,
659}
660
661#[derive(Debug, Default, Deserialize)]
662#[serde(deny_unknown_fields)]
663pub struct WikiBriefQuery {
664    #[serde(default)]
665    pub max_chars: Option<usize>,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
669pub struct WikiBriefResponse {
670    pub markdown: String,
671}
672
673/// The query for the matching passages of one indexed session.
674#[derive(Debug, Default, Deserialize)]
675#[serde(deny_unknown_fields)]
676pub struct WikiHitsQuery {
677    pub q: String,
678    #[serde(default)]
679    pub context_messages: Option<usize>,
680    #[serde(default)]
681    pub per_message_chars: Option<usize>,
682}
683
684/// The fields of a start request a restore needs. The archived session decides
685/// the rest: its title, and the project it ran in when the caller names none.
686#[derive(Debug, Clone, Default, Serialize, Deserialize)]
687#[serde(deny_unknown_fields)]
688pub struct WikiRestoreBody {
689    #[serde(default)]
690    pub workspace_id: Option<String>,
691    pub profile_id: String,
692    pub target_id: String,
693    #[serde(default)]
694    pub project_directory: Option<PathBuf>,
695    #[serde(default)]
696    pub model: Option<String>,
697    #[serde(default)]
698    pub effort: Option<String>,
699}
700
701// ---------------------------------------------------------------------------
702// Launch options
703// ---------------------------------------------------------------------------
704
705/// What a caller may choose when it starts a session, and which pair to use
706/// when it chooses nothing.
707///
708/// Served by `GET /api/v1/options` so a caller that has never read
709/// `config.toml` can enumerate the profiles and targets this daemon knows,
710/// learn whether each host answered its last check, and read the remembered
711/// default. It is the public projection in `server/viewer_types.rs`, narrowed
712/// to what a launch decision needs: no harness home, no SSH host or key, no
713/// container environment, no AWS detail, and no controller-side path.
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
715#[serde(deny_unknown_fields)]
716pub struct LaunchOptions {
717    /// The projection revision these lists came from. Two reads carrying the
718    /// same revision describe the same configuration, so a caller can build a
719    /// form without mixing halves of two different worlds.
720    pub revision: u64,
721    pub profiles: Vec<LaunchProfile>,
722    pub targets: Vec<LaunchTarget>,
723    pub bundles: Vec<LaunchBundle>,
724    /// One entry per host that has published a capacity reading. The label is
725    /// how a person names the host; it is never a locator or an address.
726    #[serde(default, skip_serializing_if = "Vec::is_empty")]
727    pub hosts: Vec<LaunchHost>,
728    /// The pair the user last saved as their default, which the first setup
729    /// also becomes. Absent when nothing has ever been saved, and never an
730    /// error: a caller that cannot read a preference still has the lists.
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub default: Option<LaunchDefault>,
733}
734
735/// One account this daemon can run work under.
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(deny_unknown_fields)]
738pub struct LaunchProfile {
739    pub id: String,
740    /// The harness kind, such as `codex` or `claude`. Which account it is, and
741    /// where its credentials live, stay on the controller.
742    pub harness: String,
743}
744
745/// One runtime template a session can run on.
746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
747#[serde(deny_unknown_fields)]
748pub struct LaunchTarget {
749    pub id: String,
750    /// The runtime kind, such as `local-bare` or `ssh-podman`.
751    pub kind: String,
752    /// Whether this target needs an existing Git directory on its machine
753    /// instead of provisioning repositories into a workspace.
754    pub requires_project_directory: bool,
755    pub availability: LaunchAvailability,
756    /// What to tell a person when the host did not answer. This repository
757    /// composes the sentence: a probe's own message names hosts and commands
758    /// and stays on the controller.
759    #[serde(default, skip_serializing_if = "Option::is_none")]
760    pub unavailable_reason: Option<String>,
761    /// Whether the target's runtime (Docker, Podman) is not installed on the
762    /// daemon's host. That is permanent for the host, unlike a host that did
763    /// not answer its last check: a picker leaves such a target out, and a
764    /// request that names it is refused with `unavailable_reason`.
765    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
766    pub runtime_missing: bool,
767    /// Whether the target is a default candidate Mjolnir supplies, not one
768    /// the user wrote in `config.toml`. With `runtime_missing` it means the
769    /// user has no such runtime and never asked for the target, so a client
770    /// should not list it.
771    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
772    pub default_candidate: bool,
773    /// How a person names the host, when a reading covers this target.
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub host: Option<String>,
776}
777
778/// How much this daemon knows about a target's host.
779///
780/// A reading arrives from a background poll, so it can be absent, old, or
781/// failed. Only `Unavailable` is a statement that the target cannot be used;
782/// `Unknown` means nobody has checked yet, which is the ordinary state
783/// immediately after startup.
784#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
785#[serde(rename_all = "lowercase")]
786pub enum LaunchAvailability {
787    /// A reading covers this target and its last check succeeded.
788    Ready,
789    /// A reading covers this target but is marked stale.
790    Stale,
791    /// A reading covers this target and its last check failed.
792    Unavailable,
793    /// No reading covers this target yet.
794    Unknown,
795}
796
797/// One repository set a managed target can provision.
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(deny_unknown_fields)]
800pub struct LaunchBundle {
801    pub id: String,
802    pub primary_repository: String,
803    pub repositories: Vec<LaunchRepository>,
804}
805
806#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
807#[serde(deny_unknown_fields)]
808pub struct LaunchRepository {
809    pub id: String,
810    /// The GitHub source, when this repository has one. A repository sourced
811    /// from a local directory publishes nothing here, because that source is a
812    /// path on the controller.
813    #[serde(default, skip_serializing_if = "Option::is_none")]
814    pub github: Option<String>,
815    pub destination: String,
816}
817
818/// One host or fleet, as much as a caller needs to explain an unavailable
819/// target. The probe's own error text is deliberately absent.
820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
821#[serde(deny_unknown_fields)]
822pub struct LaunchHost {
823    pub id: String,
824    pub label: String,
825    /// The targets this reading covers.
826    pub targets: Vec<String>,
827    pub stale: bool,
828    pub refreshing: bool,
829    pub has_error: bool,
830}
831
832/// The pair a caller may leave unnamed when it starts a session.
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
834#[serde(deny_unknown_fields)]
835pub struct LaunchDefault {
836    pub profile_id: String,
837    pub target_id: String,
838}