Skip to main content

harn_vm/personas/
types.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum PersonaLifecycleState {
10    Inactive,
11    Starting,
12    #[default]
13    Idle,
14    Running,
15    Paused,
16    Draining,
17    Failed,
18    Disabled,
19}
20
21impl PersonaLifecycleState {
22    pub fn as_str(self) -> &'static str {
23        match self {
24            Self::Inactive => "inactive",
25            Self::Starting => "starting",
26            Self::Idle => "idle",
27            Self::Running => "running",
28            Self::Paused => "paused",
29            Self::Draining => "draining",
30            Self::Failed => "failed",
31            Self::Disabled => "disabled",
32        }
33    }
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
37pub struct PersonaBudgetPolicy {
38    pub daily_usd: Option<f64>,
39    pub hourly_usd: Option<f64>,
40    pub run_usd: Option<f64>,
41    pub max_tokens: Option<u64>,
42}
43
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45pub struct PersonaRuntimeBinding {
46    pub name: String,
47    #[serde(default)]
48    pub template_ref: Option<String>,
49    pub entry_workflow: String,
50    #[serde(default)]
51    pub schedules: Vec<String>,
52    #[serde(default)]
53    pub triggers: Vec<String>,
54    #[serde(default)]
55    pub budget: PersonaBudgetPolicy,
56    /// Ordered stage declarations. Each stage names a `@step` and narrows the
57    /// runtime capability surface for the duration of that step (see
58    /// `crates/harn-vm/src/step_runtime.rs`). Empty means the persona keeps
59    /// the ambient `CapabilityPolicy`.
60    #[serde(default)]
61    pub stages: Vec<StageDecl>,
62}
63
64/// Per-stage narrowing of the runtime tool surface.
65///
66/// `name` matches a `@step` declaration on the persona. When that step's
67/// frame is entered the runtime pushes a `CapabilityPolicy` whose `tools`
68/// allowlist is `allowed_tools` (when set) and whose side-effect ceiling is
69/// `side_effect_level` (when set). Both narrow the ambient policy — the
70/// existing ceiling still applies, this just adds a tighter scope.
71#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72pub struct StageDecl {
73    pub name: String,
74    /// Tool allowlist for the stage. `None` (omitted) inherits the
75    /// persona-level allowlist; `Some(vec![])` denies every tool.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub allowed_tools: Option<Vec<String>>,
78    /// Optional side-effect ceiling override (`none` / `read_only` /
79    /// `workspace_write` / `process_exec` / `network`). Tightens, never
80    /// loosens, the ambient ceiling.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub side_effect_level: Option<String>,
83    /// Optional agent-loop iteration cap for the stage. Surfaced for
84    /// downstream consumers; `agent_loop` reads it via the persona binding.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub max_iterations: Option<u32>,
87    /// Exit transitions that name the next stage on success/failure plus an
88    /// optional explicit policy override.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub on_exit: Option<StageExit>,
91}
92
93#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
94pub struct StageExit {
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub on_complete: Option<String>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub on_failure: Option<String>,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub policy_override: Option<crate::orchestration::CapabilityPolicy>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104pub struct PersonaLease {
105    pub id: String,
106    pub holder: String,
107    pub work_key: String,
108    pub acquired_at_ms: i64,
109    pub expires_at_ms: i64,
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
113pub struct PersonaBudgetStatus {
114    pub daily_usd: Option<f64>,
115    pub hourly_usd: Option<f64>,
116    pub run_usd: Option<f64>,
117    pub max_tokens: Option<u64>,
118    pub spent_today_usd: f64,
119    pub spent_this_hour_usd: f64,
120    pub spent_last_run_usd: f64,
121    pub tokens_today: u64,
122    pub remaining_today_usd: Option<f64>,
123    pub remaining_hour_usd: Option<f64>,
124    pub exhausted: bool,
125    pub reason: Option<String>,
126    pub last_receipt_id: Option<String>,
127}
128
129#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
130pub struct PersonaStatus {
131    pub name: String,
132    #[serde(default)]
133    pub template_ref: Option<String>,
134    pub state: PersonaLifecycleState,
135    pub entry_workflow: String,
136    #[serde(default)]
137    pub role: String,
138    #[serde(default)]
139    pub current_assignment: Option<PersonaAssignmentStatus>,
140    pub last_run: Option<String>,
141    pub next_scheduled_run: Option<String>,
142    pub active_lease: Option<PersonaLease>,
143    pub budget: PersonaBudgetStatus,
144    pub last_error: Option<String>,
145    pub queued_events: usize,
146    #[serde(default)]
147    pub queued_work: Vec<PersonaQueuedWork>,
148    #[serde(default)]
149    pub handoff_inbox: Vec<PersonaHandoffInboxItem>,
150    #[serde(default)]
151    pub value_receipts: Vec<PersonaValueReceipt>,
152    pub disabled_events: usize,
153    pub paused_event_policy: String,
154}
155
156#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
157pub struct PersonaAssignmentStatus {
158    pub work_key: String,
159    pub lease_id: String,
160    pub holder: String,
161    pub acquired_at: String,
162    pub expires_at: String,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
166pub struct PersonaQueuedWork {
167    pub work_key: String,
168    pub provider: String,
169    pub kind: String,
170    pub queued_at: String,
171    pub reason: String,
172    pub source_event_id: Option<String>,
173    #[serde(default)]
174    pub metadata: BTreeMap<String, String>,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
178pub struct PersonaHandoffInboxItem {
179    pub work_key: String,
180    pub handoff_id: Option<String>,
181    pub handoff_kind: Option<String>,
182    pub source_persona: Option<String>,
183    pub task: Option<String>,
184    pub queued_at: String,
185    pub reason: String,
186}
187
188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
189pub struct PersonaValueReceipt {
190    pub kind: PersonaValueEventKind,
191    pub run_id: Option<Uuid>,
192    pub occurred_at: String,
193    pub paid_cost_usd: f64,
194    pub avoided_cost_usd: f64,
195    pub deterministic_steps: i64,
196    pub llm_steps: i64,
197    pub metadata: serde_json::Value,
198}
199
200#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub struct PersonaTriggerEnvelope {
202    pub provider: String,
203    pub kind: String,
204    pub subject_key: String,
205    pub source_event_id: Option<String>,
206    pub received_at_ms: i64,
207    #[serde(default)]
208    pub metadata: BTreeMap<String, String>,
209    #[serde(default)]
210    pub raw: serde_json::Value,
211}
212
213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
214pub struct PersonaRunReceipt {
215    pub status: String,
216    pub persona: String,
217    #[serde(default)]
218    pub run_id: Option<Uuid>,
219    pub work_key: String,
220    pub lease: Option<PersonaLease>,
221    pub queued: bool,
222    pub error: Option<String>,
223    pub budget_receipt_id: Option<String>,
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub result: Option<serde_json::Value>,
226}
227
228#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
229pub struct PersonaRunCost {
230    pub cost_usd: f64,
231    pub tokens: u64,
232    #[serde(default)]
233    pub avoided_cost_usd: f64,
234    #[serde(default)]
235    pub deterministic_steps: i64,
236    #[serde(default)]
237    pub llm_steps: i64,
238    #[serde(default)]
239    pub frontier_escalations: i64,
240    #[serde(default)]
241    pub metadata: serde_json::Value,
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum PersonaValueEventKind {
247    RunStarted,
248    RunCompleted,
249    AcceptedOutcome,
250    FrontierEscalation,
251    DeterministicExecution,
252    PromotionSavings,
253    ApprovalWait,
254}
255
256impl PersonaValueEventKind {
257    pub fn as_str(self) -> &'static str {
258        match self {
259            Self::RunStarted => "run_started",
260            Self::RunCompleted => "run_completed",
261            Self::AcceptedOutcome => "accepted_outcome",
262            Self::FrontierEscalation => "frontier_escalation",
263            Self::DeterministicExecution => "deterministic_execution",
264            Self::PromotionSavings => "promotion_savings",
265            Self::ApprovalWait => "approval_wait",
266        }
267    }
268}
269
270#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
271pub struct PersonaValueEvent {
272    pub persona_id: String,
273    pub template_ref: Option<String>,
274    pub run_id: Option<Uuid>,
275    pub kind: PersonaValueEventKind,
276    pub paid_cost_usd: f64,
277    pub avoided_cost_usd: f64,
278    pub deterministic_steps: i64,
279    pub llm_steps: i64,
280    pub metadata: serde_json::Value,
281    pub occurred_at: OffsetDateTime,
282}
283
284pub trait PersonaValueSink: Send + Sync {
285    fn handle_value_event(&self, event: &PersonaValueEvent);
286}
287
288/// Append-only multiplexed feed mirroring the cloud platform's
289/// `persona_supervision_events` projection. Sinks see the runtime-sourced
290/// `queue_position`, `repair_worker_status`, `receipt`, and
291/// `checkpoint` (restore-ack) variants; supervisor-sourced variants
292/// (`control`, `feedback`, `approval`, `handoff`, and checkpoint writes)
293/// originate on the hosted side and aren't routed here.
294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
295#[serde(tag = "update_kind", rename_all = "snake_case")]
296pub enum PersonaSupervisionEvent {
297    QueuePosition(PersonaQueuePositionUpdate),
298    RepairWorkerStatus(PersonaRepairWorkerStatusUpdate),
299    Receipt(PersonaReceiptUpdate),
300    Checkpoint(PersonaCheckpointUpdate),
301}
302
303impl PersonaSupervisionEvent {
304    pub fn update_kind(&self) -> &'static str {
305        match self {
306            Self::QueuePosition(_) => "queue_position",
307            Self::RepairWorkerStatus(_) => "repair_worker_status",
308            Self::Receipt(_) => "receipt",
309            Self::Checkpoint(_) => "checkpoint",
310        }
311    }
312
313    pub fn persona_id(&self) -> &str {
314        match self {
315            Self::QueuePosition(update) => &update.persona_id,
316            Self::RepairWorkerStatus(update) => &update.persona_id,
317            Self::Receipt(update) => &update.persona_id,
318            Self::Checkpoint(update) => &update.persona_id,
319        }
320    }
321
322    pub fn template_ref(&self) -> Option<&str> {
323        match self {
324            Self::QueuePosition(update) => update.template_ref.as_deref(),
325            Self::RepairWorkerStatus(update) => update.template_ref.as_deref(),
326            Self::Receipt(update) => update.template_ref.as_deref(),
327            Self::Checkpoint(update) => update.template_ref.as_deref(),
328        }
329    }
330
331    pub fn occurred_at_ms(&self) -> i64 {
332        match self {
333            Self::QueuePosition(update) => update.occurred_at_ms,
334            Self::RepairWorkerStatus(update) => update.occurred_at_ms,
335            Self::Receipt(update) => update.occurred_at_ms,
336            Self::Checkpoint(update) => update.occurred_at_ms,
337        }
338    }
339}
340
341#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
342pub struct PersonaQueuePositionUpdate {
343    pub persona_id: String,
344    #[serde(default)]
345    pub template_ref: Option<String>,
346    pub work_key: String,
347    pub queue_depth: i64,
348    /// 1-indexed position of `work_key`; `0` means the item left the queue.
349    pub position: i64,
350    pub queued_at_ms: i64,
351    pub occurred_at_ms: i64,
352}
353
354#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case")]
356pub enum PersonaRepairWorkerLifecycle {
357    Pending,
358    Running,
359    Verifying,
360    Pushing,
361    Succeeded,
362    Failed,
363    Cancelled,
364}
365
366impl PersonaRepairWorkerLifecycle {
367    pub fn as_str(self) -> &'static str {
368        match self {
369            Self::Pending => "pending",
370            Self::Running => "running",
371            Self::Verifying => "verifying",
372            Self::Pushing => "pushing",
373            Self::Succeeded => "succeeded",
374            Self::Failed => "failed",
375            Self::Cancelled => "cancelled",
376        }
377    }
378}
379
380#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
381pub struct PersonaRepairWorkerStatusUpdate {
382    pub persona_id: String,
383    #[serde(default)]
384    pub template_ref: Option<String>,
385    pub repair_worker_id: String,
386    pub lifecycle: PersonaRepairWorkerLifecycle,
387    #[serde(default)]
388    pub work_key: Option<String>,
389    #[serde(default)]
390    pub lease_id: Option<String>,
391    #[serde(default)]
392    pub scratchpad_url: Option<String>,
393    pub last_heartbeat_ms: i64,
394    pub occurred_at_ms: i64,
395}
396
397#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
398pub struct PersonaReceiptUpdate {
399    pub persona_id: String,
400    #[serde(default)]
401    pub template_ref: Option<String>,
402    pub receipt: PersonaRunReceipt,
403    pub occurred_at_ms: i64,
404}
405
406#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
407pub struct PersonaCheckpointUpdate {
408    pub persona_id: String,
409    #[serde(default)]
410    pub template_ref: Option<String>,
411    pub action: PersonaCheckpointAction,
412    pub checkpoint_id: String,
413    #[serde(default)]
414    pub work_key: Option<String>,
415    /// Coordinates the runtime actually resumed from when acking a restore.
416    #[serde(default)]
417    pub resumed_from: Option<PersonaCheckpointResume>,
418    pub occurred_at_ms: i64,
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(rename_all = "snake_case")]
423pub enum PersonaCheckpointAction {
424    RestoreAcked,
425}
426
427impl PersonaCheckpointAction {
428    pub fn as_str(self) -> &'static str {
429        match self {
430            Self::RestoreAcked => "restore_acked",
431        }
432    }
433}
434
435#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
436pub struct PersonaCheckpointResume {
437    pub run_id: Option<Uuid>,
438    pub lease_id: Option<String>,
439    pub last_run_ms: Option<i64>,
440    pub queued_work_keys: Vec<String>,
441    #[serde(default)]
442    pub note: Option<String>,
443}
444
445pub trait PersonaSupervisionSink: Send + Sync {
446    fn handle_supervision_event(&self, event: &PersonaSupervisionEvent);
447}