Skip to main content

artifactize_core/inspect/
session.rs

1//! Saved conversation lookup and continuation shared by interfaces.
2mod send;
3use crate::{
4    agent::session::{self, Conversation, SessionRef, Summary},
5    store::{self, Producer, RequestView},
6};
7pub use send::{Sent, send};
8use serde_json::{Value, json};
9use std::path::{Path, PathBuf};
10
11/// What names a saved conversation: its full reference, or a bare id, which is read as a
12/// request id and else as a session id (the two have the same form).
13#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
14#[serde(try_from = "String")]
15pub enum Selector {
16    Reference(SessionRef),
17    Id {
18        request: crate::types::RequestId,
19        session: crate::types::SessionId,
20    },
21}
22
23impl std::str::FromStr for Selector {
24    type Err = String;
25
26    fn from_str(text: &str) -> Result<Self, String> {
27        if let Some(reference) = SessionRef::parse(text) {
28            return Ok(Self::Reference(reference));
29        }
30        match (text.parse(), text.parse()) {
31            (Ok(request), Ok(session)) => Ok(Self::Id { request, session }),
32            _ => Err(format!(
33                "{text} is not a session reference, a request id or a session id."
34            )),
35        }
36    }
37}
38
39impl TryFrom<String> for Selector {
40    type Error = String;
41
42    fn try_from(text: String) -> Result<Self, String> {
43        text.parse()
44    }
45}
46
47/// The review whose conversation a reference names, in this state.
48pub struct Located {
49    /// The request whose Agent review ran as the session.
50    pub request: RequestView,
51    /// The request the reference named, when it reused that review's result.
52    pub reused_by: Option<crate::types::RequestId>,
53    pub id: crate::types::SessionId,
54    /// Set once the conversation was saved; absent when saving was off.
55    pub reference: Option<SessionRef>,
56    pub path: PathBuf,
57}
58
59impl Located {
60    /// The saved conversation, or why there is none.
61    pub fn load(&self) -> Result<Conversation, String> {
62        Conversation::load(&self.path)?.ok_or_else(|| {
63            let request = &self.request.request.id;
64            if self.reference.is_some() {
65                format!(
66                    "Session {} of request {request} was removed by the session GC (limits.json agentSessions).",
67                    self.id
68                )
69            } else {
70                format!(
71                    "Session {} of request {request} was not saved: saving was off (limits.json agentSessions.enabled), or the review never started.",
72                    self.id
73                )
74            }
75        })
76    }
77}
78
79/// Fail unless `reference` names this machine's producer and this state.
80fn here(state: &Path, reference: &SessionRef) -> Result<(), String> {
81    let producer = Producer::current().name;
82    let state_id = store::read_state_id(state)?;
83    if reference.producer == producer
84        && Some(reference.state.as_str()) == state_id.as_ref().map(crate::types::StateId::as_str)
85    {
86        return Ok(());
87    }
88    Err(format!(
89        "Session {} lives in {}'s state {}; this is {producer}'s state {}. Read or continue it there.",
90        reference.session_id,
91        reference.producer,
92        reference.state,
93        state_id.as_deref().unwrap_or("(not created yet)")
94    ))
95}
96
97/// Resolve a full reference, a request id or a session id to its review's request.
98pub async fn locate(state: &Path, selector: &Selector) -> Result<Located, String> {
99    let mut view = match selector {
100        Selector::Reference(reference) => {
101            here(state, reference)?;
102            let view = store::read_request(state, &reference.request_id)
103                .await
104                .map_err(|_| format!("Request {} is not in this state.", reference.request_id))?;
105            if view.request.session_id.as_ref() != Some(&reference.session_id) {
106                return Err(format!(
107                    "Request {} did not run session {}.",
108                    reference.request_id, reference.session_id
109                ));
110            }
111            view
112        }
113        Selector::Id { request, session } => {
114            match store::read_request(state, request.as_str()).await {
115                Ok(view) => view,
116                Err(_) => store::read_session_request(state, session.as_str())
117                    .await?
118                    .ok_or_else(|| {
119                        format!(
120                            "No Agent request or session {session} in this state ({}).",
121                            crate::platform::path_text(state)
122                        )
123                    })?,
124            }
125        }
126    };
127    let mut reused_by = None;
128    if view.request.session_id.is_none() {
129        // A reused result names the review that produced it, which may live elsewhere.
130        let Some(reference) = view.request.session.clone() else {
131            return Err(format!(
132                "Request {} ran no Agent review and names no saved conversation.",
133                view.request.id
134            ));
135        };
136        here(state, &reference)?;
137        reused_by = Some(view.request.id.clone());
138        view = store::read_request(state, &reference.request_id)
139            .await
140            .map_err(|_| format!("Request {} is not in this state.", reference.request_id))?;
141    }
142    let id = view
143        .request
144        .session_id
145        .clone()
146        .ok_or_else(|| format!("Request {} ran no Agent review.", view.request.id))?;
147    Ok(Located {
148        path: session::path(state, &id)?,
149        reference: view.request.session.clone(),
150        reused_by,
151        id,
152        request: view,
153    })
154}
155
156/// Project the same saved events or summary that `session show --json` prints.
157pub async fn show(state: &Path, selector: &Selector, summary: bool) -> Result<Value, String> {
158    let located = locate(state, selector).await?;
159    let conversation = located.load()?;
160    if summary {
161        let mut value = json!(Summary::new(&conversation));
162        value["reference"] = json!(located.reference.as_ref().map(ToString::to_string));
163        value["sessionId"] = json!(located.id);
164        value["requestId"] = json!(located.request.request.id);
165        Ok(value)
166    } else {
167        Ok(
168            json!({"reference": located.reference.as_ref().map(ToString::to_string),
169            "file": crate::platform::path_text(&conversation.path), "events": conversation.wire_events}),
170        )
171    }
172}