perspt_agent/toolloop/
envelope.rs1use anyhow::Result;
10use perspt_sdk::ledger::{tool_loop_body, ToolLoopBody};
11
12use super::contract::{LoopEvent, LoopEventEnvelopeV1};
13
14#[derive(Debug, Clone)]
16pub struct DecodedLoopEvent {
17 pub version: u16,
19 pub event: LoopEvent,
20}
21
22pub(crate) fn is_legacy_variant(event: &LoopEvent) -> bool {
25 match event {
26 LoopEvent::ConversationSeeded { .. }
27 | LoopEvent::ConversationDelta { .. }
28 | LoopEvent::TurnObserved { .. }
29 | LoopEvent::ToolCallObserved { .. }
30 | LoopEvent::ProposalObserved { .. }
31 | LoopEvent::ProposalChecked { .. }
32 | LoopEvent::EffectApplied { .. }
33 | LoopEvent::ToolBatchConflict { .. }
34 | LoopEvent::EffectDenied { .. }
35 | LoopEvent::CandidateMeasured { .. }
36 | LoopEvent::GateDecisionRecorded { .. }
37 | LoopEvent::DecisionBoundRefused { .. }
38 | LoopEvent::CandidateRestored { .. }
39 | LoopEvent::EffectBoundaryMeasured { .. }
40 | LoopEvent::ContextCheckpointCreated { .. }
41 | LoopEvent::DurableCandidateCheckpoint { .. }
42 | LoopEvent::RecoveryControlGranted { .. }
43 | LoopEvent::RouteFailover { .. }
44 | LoopEvent::RecoveryContained { .. } => true,
45 LoopEvent::SearchOpened { .. }
46 | LoopEvent::SearchUsageSnapshot { .. }
47 | LoopEvent::BranchForked { .. }
48 | LoopEvent::BranchStrategySelected { .. }
49 | LoopEvent::BranchObservation { .. }
50 | LoopEvent::BranchCandidateMeasured { .. }
51 | LoopEvent::PartialCheckpointed { .. }
52 | LoopEvent::FrontierEpochStarted { .. }
53 | LoopEvent::FrontierEntryServed { .. }
54 | LoopEvent::BranchIneligible { .. }
55 | LoopEvent::BranchNotSelected { .. }
56 | LoopEvent::BranchAbandoned { .. }
57 | LoopEvent::BranchSelected { .. }
58 | LoopEvent::BranchCommitted { .. }
59 | LoopEvent::NoGoodRecorded { .. }
60 | LoopEvent::SearchClosed { .. }
61 | LoopEvent::ContextWorkingSet { .. }
62 | LoopEvent::ContextPagesSelected { .. }
63 | LoopEvent::ContextMiss { .. }
64 | LoopEvent::ContextPageRecalled { .. }
65 | LoopEvent::ContextInfeasible { .. }
66 | LoopEvent::PromptProgramCompiled { .. }
67 | LoopEvent::PromptProgramInvoked { .. }
68 | LoopEvent::ContextCompacted { .. } => false,
69 }
70}
71
72pub fn decode_tool_loop(payload: &serde_json::Value) -> Result<DecodedLoopEvent> {
76 match tool_loop_body(payload).map_err(|e| anyhow::anyhow!("{e}"))? {
77 ToolLoopBody::Legacy(body) => {
78 let event: LoopEvent = serde_json::from_value(body.clone())?;
79 anyhow::ensure!(
80 is_legacy_variant(&event),
81 "a PSP-10 event appeared in an unversioned legacy row; refusing"
82 );
83 Ok(DecodedLoopEvent { version: 0, event })
84 }
85 ToolLoopBody::V1(_) => {
86 let envelope: LoopEventEnvelopeV1 = serde_json::from_value(payload.clone())?;
87 anyhow::ensure!(
88 envelope.schema_version == 1,
89 "envelope version {} is not 1",
90 envelope.schema_version
91 );
92 Ok(DecodedLoopEvent {
93 version: 1,
94 event: envelope.body,
95 })
96 }
97 }
98}