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