Skip to main content

starweaver_session/
management.rs

1//! Product-neutral agent-facing session-management contracts.
2//!
3//! These types deliberately contain no product services or transport details. A host constructs
4//! [`AgentSessionScope`] and implements the narrow handles in `starweaver-agent`.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use starweaver_core::{RunId, SessionId};
12use starweaver_stream::{DisplayMessage, ReplayCursor};
13
14use crate::{InputPart, RunStatus, SessionStatus};
15
16/// Backward-compatible namespace used by single-user local products.
17pub const LOCAL_SESSION_NAMESPACE: &str = "local";
18
19/// Composite identity of a durable session.
20#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
21pub struct ManagedSessionTarget {
22    /// Host-derived tenant/store namespace.
23    pub namespace_id: String,
24    /// Durable session id within the namespace.
25    pub session_id: SessionId,
26}
27
28impl ManagedSessionTarget {
29    /// Build a composite session target.
30    #[must_use]
31    pub fn new(namespace_id: impl Into<String>, session_id: SessionId) -> Self {
32        Self {
33            namespace_id: namespace_id.into(),
34            session_id,
35        }
36    }
37}
38
39/// Composite identity of a durable run. A run id is never globally unique.
40#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
41pub struct ManagedRunTarget {
42    /// Host-derived tenant/store namespace.
43    pub namespace_id: String,
44    /// Owning session id.
45    pub session_id: SessionId,
46    /// Session-scoped run id.
47    pub run_id: RunId,
48}
49
50impl ManagedRunTarget {
51    /// Build a composite run target.
52    #[must_use]
53    pub fn new(namespace_id: impl Into<String>, session_id: SessionId, run_id: RunId) -> Self {
54        Self {
55            namespace_id: namespace_id.into(),
56            session_id,
57            run_id,
58        }
59    }
60}
61
62/// Agent-facing session operation class.
63#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
64#[serde(rename_all = "snake_case")]
65pub enum AgentSessionOperation {
66    /// List/get session and run projections.
67    Read,
68    /// Use an independently injected search provider.
69    Search,
70    /// Create a session or start a run.
71    Create,
72    /// Update title/profile/archive state.
73    Update,
74    /// Steer or interrupt a live run.
75    Control,
76    /// Tombstone a session.
77    Delete,
78}
79
80/// Host-derived authority supplied separately from model arguments.
81#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub struct AgentSessionScope {
83    /// Authorized namespace.
84    pub namespace_id: String,
85    /// Principal/owner represented by this capability.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub owner_id: Option<String>,
88    /// Product that constructed the scope.
89    pub source_product: String,
90    /// Controlling session, used for self-target denial.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub source_session_id: Option<SessionId>,
93    /// Controlling run, used for self-target denial.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub source_run_id: Option<RunId>,
96    /// Effective operation intersection. Model input cannot alter this set.
97    #[serde(default)]
98    pub operations: BTreeSet<AgentSessionOperation>,
99    /// Optional allowlist; empty means the host's namespace/owner policy decides.
100    #[serde(default)]
101    pub allowed_session_ids: BTreeSet<SessionId>,
102    /// Whether the current session may be queried.
103    #[serde(default = "default_true")]
104    pub allow_self_query: bool,
105    /// Whether the current run/session may be controlled. Hosts should normally leave false.
106    #[serde(default)]
107    pub allow_self_control: bool,
108    /// Stable fingerprint of the intersected server/profile/caller/grant policy.
109    pub policy_fingerprint: String,
110    /// Absolute deadline for commands made through this capability.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub deadline: Option<DateTime<Utc>>,
113    /// Maximum page size after host policy intersection.
114    #[serde(default = "default_page_limit")]
115    pub max_page_size: u32,
116}
117
118const fn default_true() -> bool {
119    true
120}
121
122const fn default_page_limit() -> u32 {
123    50
124}
125
126impl AgentSessionScope {
127    /// Return whether an operation is in the effective grant.
128    #[must_use]
129    pub fn allows(&self, operation: AgentSessionOperation) -> bool {
130        self.operations.contains(&operation)
131    }
132
133    /// Return whether a session id is inside the host allowlist.
134    #[must_use]
135    pub fn allows_session(&self, session_id: &SessionId) -> bool {
136        self.allowed_session_ids.is_empty() || self.allowed_session_ids.contains(session_id)
137    }
138
139    /// Return whether a run target is the controlling run.
140    #[must_use]
141    pub fn is_self_run(&self, target: &ManagedRunTarget) -> bool {
142        self.namespace_id == target.namespace_id
143            && self.source_session_id.as_ref() == Some(&target.session_id)
144            && self.source_run_id.as_ref() == Some(&target.run_id)
145    }
146
147    /// Return whether a session target is the controlling session.
148    #[must_use]
149    pub fn is_self_session(&self, target: &ManagedSessionTarget) -> bool {
150        self.namespace_id == target.namespace_id
151            && self.source_session_id.as_ref() == Some(&target.session_id)
152    }
153}
154
155/// Explicit session deletion state/fence used by run and continuation admission.
156#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
157#[serde(tag = "state", rename_all = "snake_case")]
158pub enum SessionDeletionFence {
159    /// No deletion is in progress.
160    #[default]
161    Stable,
162    /// New runs, continuations, and child delegation are fenced.
163    Deleting {
164        /// Stable deletion intent id.
165        fence_id: String,
166        /// Revision from which deletion was acquired.
167        expected_revision: u64,
168        /// Principal requesting deletion.
169        requested_by: String,
170        /// Fence acquisition time.
171        started_at: DateTime<Utc>,
172    },
173    /// Session is tombstoned; evidence retention is an admin concern.
174    Deleted {
175        /// Stable deletion intent id.
176        fence_id: String,
177        /// Tombstone time.
178        deleted_at: DateTime<Utc>,
179    },
180}
181
182impl SessionDeletionFence {
183    /// Return whether new work and async continuations must be denied.
184    #[must_use]
185    pub const fn blocks_continuation(&self) -> bool {
186        !matches!(self, Self::Stable)
187    }
188}
189
190/// Deletion/continuation hook exposed to async supervisors without coupling both control planes.
191#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct SessionContinuationFence {
193    /// Composite session target.
194    pub target: ManagedSessionTarget,
195    /// Session revision at the authorization check.
196    pub revision: u64,
197    /// True when continuation/delegation is allowed.
198    pub continuation_allowed: bool,
199    /// Stable deletion fence id when blocked.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub fence_id: Option<String>,
202}
203
204/// Compact query for canonical session listing.
205#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
206pub struct AgentSessionListQuery {
207    /// Optional exact status.
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub status: Option<SessionStatus>,
210    /// Optional exact profile.
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub profile: Option<String>,
213    /// Optional exact workspace display value.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub workspace: Option<String>,
216    /// Maximum page size.
217    #[serde(default = "default_page_limit")]
218    pub limit: u32,
219    /// Opaque keyset token.
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub page_token: Option<String>,
222}
223
224/// Sections requested by a get-session operation.
225#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
226pub struct AgentSessionInclude {
227    /// Include bounded recent run summaries.
228    #[serde(default)]
229    pub recent_runs: bool,
230    /// Include compact trace counts.
231    #[serde(default)]
232    pub trace: bool,
233}
234
235/// Compact run list query.
236#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
237pub struct AgentRunListQuery {
238    /// Maximum page size.
239    #[serde(default = "default_page_limit")]
240    pub limit: u32,
241    /// Opaque session-local keyset token.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub page_token: Option<String>,
244}
245
246impl Default for AgentRunListQuery {
247    fn default() -> Self {
248        Self {
249            limit: default_page_limit(),
250            page_token: None,
251        }
252    }
253}
254
255/// Display-safe replay query.
256#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
257pub struct AgentReplayQuery {
258    /// Family-aware cursor.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub after: Option<ReplayCursor>,
261    /// Maximum number of display messages.
262    #[serde(default = "default_page_limit")]
263    pub limit: u32,
264}
265
266/// Prompt-safe compact session projection.
267#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
268pub struct AgentSessionView {
269    /// Composite target.
270    pub target: ManagedSessionTarget,
271    /// Optional bounded title.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub title: Option<String>,
274    /// Canonical status.
275    pub status: SessionStatus,
276    /// Optional profile.
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub profile: Option<String>,
279    /// Safe workspace display value, never a backend locator.
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub workspace: Option<String>,
282    /// Optimistic concurrency token.
283    pub revision: u64,
284    /// Head run id.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub head_run_id: Option<RunId>,
287    /// Current active run id when canonical storage has one.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub active_run_id: Option<RunId>,
290    /// Whether the current host can resume this session.
291    pub resumable: bool,
292    /// Whether the current host has a fenced local control handle.
293    pub controllable: bool,
294    /// Bounded recent run summaries.
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub recent_runs: Vec<AgentRunView>,
297    /// Creation time.
298    pub created_at: DateTime<Utc>,
299    /// Last update time.
300    pub updated_at: DateTime<Utc>,
301}
302
303/// Prompt-safe compact run projection.
304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
305pub struct AgentRunView {
306    /// Composite run target.
307    pub target: ManagedRunTarget,
308    /// Durable status.
309    pub status: RunStatus,
310    /// Session-local order.
311    pub sequence_no: usize,
312    /// Bounded text input preview.
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub input_preview: Option<String>,
315    /// Bounded output preview.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub output_preview: Option<String>,
318    /// Safe error category, never raw provider diagnostics.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub error_category: Option<String>,
321    /// Whether this process currently owns fenced control.
322    pub controllable: bool,
323    /// Creation time.
324    pub created_at: DateTime<Utc>,
325    /// Last update time.
326    pub updated_at: DateTime<Utc>,
327}
328
329/// Page of compact sessions.
330#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
331pub struct AgentSessionPage {
332    /// Results.
333    pub sessions: Vec<AgentSessionView>,
334    /// Opaque continuation token.
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub next_page_token: Option<String>,
337}
338
339/// Page of compact runs.
340#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
341pub struct AgentRunPage {
342    /// Results.
343    pub runs: Vec<AgentRunView>,
344    /// Opaque continuation token.
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub next_page_token: Option<String>,
347}
348
349/// Page of sanitized historical display evidence.
350#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
351pub struct AgentDisplayPage {
352    /// Display-safe messages. Hosts must exclude internal and diagnostic visibility.
353    pub messages: Vec<DisplayMessage>,
354    /// Cursor after the final returned message.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub next_cursor: Option<ReplayCursor>,
357    /// Explicit reminder that historical content is untrusted evidence.
358    #[serde(default = "untrusted_evidence_label")]
359    pub trust: String,
360}
361
362fn untrusted_evidence_label() -> String {
363    "untrusted_historical_evidence".to_string()
364}
365
366/// Allowlisted create-session command.
367#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
368pub struct CreateManagedSession {
369    /// Optional title.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub title: Option<String>,
372    /// Optional configured profile id.
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub profile: Option<String>,
375    /// Host-approved workspace reference/display value.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub workspace: Option<String>,
378    /// Approved typed metadata only.
379    #[serde(default)]
380    pub metadata: BTreeMap<String, Value>,
381    /// Stable idempotency key.
382    pub idempotency_key: String,
383}
384
385/// Typed session update patch.
386#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
387pub struct ManagedSessionPatch {
388    /// Set or clear the title. Outer `None` means unchanged.
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub title: Option<Option<String>>,
391    /// Set or clear the future-run profile. Outer `None` means unchanged.
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub profile: Option<Option<String>>,
394    /// Explicit archive transition.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub archived: Option<bool>,
397    /// Allowlisted metadata replacements/removals (`null` removes).
398    #[serde(default)]
399    pub metadata: BTreeMap<String, Value>,
400}
401
402/// Revision-checked session update.
403#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
404pub struct UpdateManagedSession {
405    /// Session id; namespace comes from scope.
406    pub session_id: SessionId,
407    /// Required optimistic revision.
408    pub expected_revision: u64,
409    /// Explicit patch.
410    pub patch: ManagedSessionPatch,
411    /// Stable idempotency key.
412    pub idempotency_key: String,
413}
414
415/// Revision-checked tombstone command. Evidence purge is intentionally absent.
416#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
417pub struct DeleteManagedSession {
418    /// Session id; namespace comes from scope.
419    pub session_id: SessionId,
420    /// Required optimistic revision.
421    pub expected_revision: u64,
422    /// Stable idempotency key.
423    pub idempotency_key: String,
424    /// Approval evidence supplied by the host approval layer.
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub approval_receipt_id: Option<String>,
427}
428
429/// Non-blocking run start command.
430#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
431pub struct StartManagedRun {
432    /// Owning session id.
433    pub session_id: SessionId,
434    /// Canonical model input.
435    pub input: Vec<InputPart>,
436    /// Optional configured profile override.
437    #[serde(default, skip_serializing_if = "Option::is_none")]
438    pub profile: Option<String>,
439    /// Approved environment attachment references.
440    #[serde(default)]
441    pub environment_refs: Vec<String>,
442    /// Stable idempotency key.
443    pub idempotency_key: String,
444}
445
446/// Structured steering command.
447#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
448pub struct SteerManagedRun {
449    /// Composite target.
450    pub target: ManagedRunTarget,
451    /// Stable steering id.
452    pub steering_id: String,
453    /// Bounded text baseline.
454    pub text: String,
455    /// Optional idempotency key.
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub idempotency_key: Option<String>,
458}
459
460/// Cooperative interruption command.
461#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
462pub struct InterruptManagedRun {
463    /// Composite target.
464    pub target: ManagedRunTarget,
465    /// Stable control operation id.
466    pub operation_id: String,
467    /// Safe bounded reason category.
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub reason_category: Option<String>,
470    /// Optional idempotency key.
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub idempotency_key: Option<String>,
473}
474
475/// Session mutation result.
476#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
477pub struct SessionMutationReceipt {
478    /// Stable receipt id.
479    pub receipt_id: String,
480    /// Compact canonical projection after mutation.
481    pub session: AgentSessionView,
482    /// True when an exact idempotent result was replayed.
483    pub idempotent_replay: bool,
484}
485
486/// Accepted non-blocking run result.
487#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
488pub struct RunStartReceipt {
489    /// Stable receipt id.
490    pub receipt_id: String,
491    /// Composite target and fencing generation.
492    pub target: ManagedRunTarget,
493    /// Accepted durable status.
494    pub status: RunStatus,
495    /// Admission fencing generation.
496    pub fencing_generation: u64,
497    /// True when an exact idempotent result was replayed.
498    pub idempotent_replay: bool,
499}
500
501/// Accepted steering/interruption result.
502#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
503pub struct RunControlReceipt {
504    /// Stable durable receipt id.
505    pub receipt_id: String,
506    /// Composite target.
507    pub target: ManagedRunTarget,
508    /// Operation id or steering id.
509    pub operation_id: String,
510    /// Fencing generation against which it was accepted.
511    pub fencing_generation: u64,
512    /// Accepted means queued/signalled, not completed/consumed.
513    pub accepted: bool,
514    /// True when an exact idempotent result was replayed.
515    pub idempotent_replay: bool,
516    /// Receipt creation time.
517    pub created_at: DateTime<Utc>,
518}
519
520/// Durable one-active-run lease owned by a host instance.
521#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
522pub struct RunAdmissionLease {
523    /// Composite target.
524    pub target: ManagedRunTarget,
525    /// Stable admission intent id.
526    pub admission_id: String,
527    /// Current host instance id.
528    pub host_instance_id: String,
529    /// Monotonic fencing generation for this session.
530    pub fencing_generation: u64,
531    /// Lease expiry.
532    pub lease_expires_at: DateTime<Utc>,
533    /// Last heartbeat.
534    pub heartbeat_at: DateTime<Utc>,
535    /// Normalized start-command fingerprint.
536    pub command_fingerprint: String,
537    /// Idempotency key bound to the fingerprint.
538    pub idempotency_key: String,
539}
540
541impl starweaver_core::VersionedRecord for RunAdmissionLease {
542    const SCHEMA: &'static str = "starweaver.session.run_admission_lease";
543}
544
545impl RunAdmissionLease {
546    /// Return whether this lease is expired at `now`.
547    #[must_use]
548    pub fn expired_at(&self, now: DateTime<Utc>) -> bool {
549        self.lease_expires_at <= now
550    }
551}
552
553/// Request for atomic one-active-run admission.
554#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
555pub struct AcquireRunAdmission {
556    /// Queued/starting run record to persist atomically with the lease.
557    pub run: crate::RunRecord,
558    /// Namespace derived by the product.
559    pub namespace_id: String,
560    /// Host instance claiming ownership.
561    pub host_instance_id: String,
562    /// Stable admission id.
563    pub admission_id: String,
564    /// Lease expiry.
565    pub lease_expires_at: DateTime<Utc>,
566    /// Stable idempotency key.
567    pub idempotency_key: String,
568    /// Normalized command fingerprint.
569    pub command_fingerprint: String,
570    /// Waiting run whose parked active slot this admission is authorized to replace.
571    ///
572    /// Ordinary admissions must leave this unset. A HITL continuation sets it to the source run
573    /// and must also restore from that same waiting run.
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub replaces_waiting_run_id: Option<RunId>,
576    /// Preflight HITL claim transitioned to `Started` atomically with waiting replacement.
577    ///
578    /// This must be present exactly when `replaces_waiting_run_id` is present. Ordinary
579    /// admissions leave both fields unset.
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    pub hitl_resume_claim_id: Option<String>,
582}
583
584/// Result of admission or an exact retry.
585#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
586pub struct RunAdmissionReceipt {
587    /// Persisted run.
588    pub run: crate::RunRecord,
589    /// Durable lease/fencing evidence.
590    pub lease: RunAdmissionLease,
591    /// True for same-key/same-command retry.
592    pub idempotent_replay: bool,
593}
594
595impl starweaver_core::VersionedRecord for RunAdmissionReceipt {
596    const SCHEMA: &'static str = "starweaver.session.run_admission_receipt";
597}
598
599/// Durable receipt stored independently from a process-local control handle.
600#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
601pub struct DurableControlReceipt {
602    /// Stable receipt id.
603    pub receipt_id: String,
604    /// Composite target.
605    pub target: ManagedRunTarget,
606    /// Steering/interruption operation id.
607    pub operation_id: String,
608    /// Operation category.
609    pub operation: String,
610    /// Idempotency key.
611    pub idempotency_key: String,
612    /// Safe normalized fingerprint.
613    pub command_fingerprint: String,
614    /// Matching owner generation.
615    pub fencing_generation: u64,
616    /// Effect state. Durable admission uses `pending`, `delivered`, `consumed`, or `reconciled`;
617    /// legacy compatibility paths may retain historical `reserved`, `accepted`, or `failed`.
618    pub state: String,
619    /// Creation time.
620    pub created_at: DateTime<Utc>,
621}
622
623impl starweaver_core::VersionedRecord for DurableControlReceipt {
624    const SCHEMA: &'static str = "starweaver.session.durable_control_receipt";
625}
626
627/// Required query error categories.
628#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
629#[serde(rename_all = "snake_case")]
630pub enum AgentSessionQueryErrorCode {
631    /// Query fields or limits are invalid.
632    InvalidQuery,
633    /// Authorized canonical target was not found (also used to hide unauthorized targets).
634    NotFound,
635    /// Optional capability is not installed.
636    Unsupported,
637    /// Canonical provider is temporarily unavailable.
638    Unavailable,
639    /// Effective host scope denies the operation.
640    PermissionDenied,
641    /// Cursor is malformed, stale, or belongs to another query/scope.
642    InvalidCursor,
643    /// Bounded internal query failure.
644    Failed,
645}
646
647/// Prompt-safe query error.
648#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
649pub struct AgentSessionQueryError {
650    /// Stable category.
651    pub code: AgentSessionQueryErrorCode,
652    /// Bounded safe message.
653    pub message: String,
654}
655
656impl std::fmt::Display for AgentSessionQueryError {
657    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        write!(formatter, "{:?}: {}", self.code, self.message)
659    }
660}
661
662impl std::error::Error for AgentSessionQueryError {}
663
664/// Required control error categories.
665#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
666#[serde(rename_all = "snake_case")]
667pub enum AgentSessionControlErrorCode {
668    /// Command fields or limits are invalid.
669    InvalidCommand,
670    /// Authorized target was not found (also used to hide unauthorized targets).
671    NotFound,
672    /// Effective host scope denies the operation.
673    PermissionDenied,
674    /// A host approval receipt is required.
675    ApprovalRequired,
676    /// Optimistic revision or lifecycle conflict.
677    Conflict,
678    /// Idempotency key was reused for a different normalized command.
679    IdempotencyConflict,
680    /// Session already owns an active run slot.
681    RunConflict,
682    /// Target has no current process-local control handle.
683    NotActive,
684    /// Target run is immutable and terminal.
685    Terminal,
686    /// Durable state is active but no matching fenced owner exists.
687    StaleActive,
688    /// Host quota rejects the operation.
689    QuotaExceeded,
690    /// Required coordinator or storage capability is unavailable.
691    Unavailable,
692    /// Bounded internal control failure.
693    Failed,
694}
695
696/// Prompt-safe control error.
697#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
698pub struct AgentSessionControlError {
699    /// Stable category.
700    pub code: AgentSessionControlErrorCode,
701    /// Bounded safe message.
702    pub message: String,
703    /// Current safe session revision for CAS conflicts.
704    #[serde(default, skip_serializing_if = "Option::is_none")]
705    pub current_revision: Option<u64>,
706}
707
708impl std::fmt::Display for AgentSessionControlError {
709    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710        write!(formatter, "{:?}: {}", self.code, self.message)
711    }
712}
713
714impl std::error::Error for AgentSessionControlError {}