pointlock_store/projection/
inbox.rs1use pointlock_ir::{JsonSchemaDocument, RunLogPayload, RunPath, render_run_path};
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use super::ProjectionVersion;
18use crate::error::StoreError;
19use crate::store::Store;
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct HumanInboxEntry {
26 pub projection_version: ProjectionVersion,
28 pub run_id: String,
30 pub flow_id: String,
32 pub request_id: String,
34 pub purpose: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub mode: Option<String>,
39 pub prompt: String,
41 pub presents: Value,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub decisions: Option<Vec<String>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub output_schema: Option<JsonSchemaDocument>,
50 #[serde(skip_serializing_if = "Option::is_none")]
54 pub deadline_at_ms: Option<u64>,
55 pub requested_at_ms: u64,
57 pub requested_seq: u64,
59 pub run_path: String,
61 pub run_path_frames: RunPath,
63}
64
65pub fn run_inbox(store: &Store, run_id: &str) -> Result<Vec<HumanInboxEntry>, StoreError> {
67 let meta = store.run_meta(run_id)?;
68 let events = store.events(run_id)?;
69 let mut pending: Vec<HumanInboxEntry> = Vec::new();
70 for event in &events {
71 match &event.payload {
72 RunLogPayload::HumanRequested {
73 request_id,
74 purpose,
75 mode,
76 prompt,
77 presents,
78 decisions,
79 output_schema,
80 deadline_at_ms,
81 } => pending.push(HumanInboxEntry {
82 projection_version: ProjectionVersion,
83 run_id: run_id.to_owned(),
84 flow_id: meta.flow_id.to_string(),
85 request_id: request_id.clone(),
86 purpose: wire(purpose),
87 mode: mode.as_ref().map(wire),
88 prompt: prompt.clone(),
89 presents: presents.clone(),
90 decisions: decisions.clone(),
91 output_schema: output_schema.clone(),
92 deadline_at_ms: *deadline_at_ms,
93 requested_at_ms: event.at_ms,
94 requested_seq: event.seq,
95 run_path: render_run_path(&event.run_path),
96 run_path_frames: event.run_path.clone(),
97 }),
98 RunLogPayload::HumanResponded {
99 request_id,
100 purpose,
101 response,
102 ..
103 } => {
104 let non_final = *purpose == pointlock_ir::HumanPurpose::Supervision
105 && response.get("decision").and_then(Value::as_str) == Some("suspend");
106 if !non_final {
107 pending.retain(|entry| entry.request_id != *request_id);
108 }
109 }
110 RunLogPayload::StepExited { .. } => {
115 pending.retain(|entry| {
116 !crate::fold::exit_settles_pending(&event.run_path, &entry.run_path_frames)
117 });
118 }
119 _ => {}
120 }
121 }
122 Ok(pending)
123}
124
125pub fn human_inbox(store: &Store) -> Result<Vec<HumanInboxEntry>, StoreError> {
128 let mut entries = Vec::new();
129 for run in store.list_runs()? {
130 entries.extend(run_inbox(store, &run.run_id)?);
131 }
132 Ok(entries)
133}
134
135fn wire<T: Serialize>(value: &T) -> String {
137 serde_json::to_value(value)
138 .ok()
139 .and_then(|v| v.as_str().map(str::to_owned))
140 .unwrap_or_default()
141}