1use serde::{Deserialize, Serialize};
8
9use crate::{CognitiveVitals, ContextFreshness, ImpactDecision, TrajectoryOutcome, TrajectoryStep};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum ProcedureLifecycle {
15 #[default]
16 Draft,
17 Candidate,
18 Validated,
19 Promoted,
20 Deprecated,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ProcedureArtifactFormat {
27 MarkdownSkill,
28 OpenFangSkill,
29 AinlGraph,
30 Hand,
31 PromptOnly,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct ExperienceEvent {
37 pub event_id: String,
38 pub timestamp_ms: i64,
39 pub tool_or_adapter: String,
40 pub operation: String,
41 pub success: bool,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub input_preview: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub output_preview: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub error: Option<String>,
48 #[serde(default)]
49 pub duration_ms: u64,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub vitals: Option<CognitiveVitals>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub freshness_at_step: Option<ContextFreshness>,
54}
55
56impl From<&TrajectoryStep> for ExperienceEvent {
57 fn from(step: &TrajectoryStep) -> Self {
58 Self {
59 event_id: step.step_id.clone(),
60 timestamp_ms: step.timestamp_ms,
61 tool_or_adapter: step.adapter.clone(),
62 operation: step.operation.clone(),
63 success: step.success,
64 input_preview: step.inputs_preview.clone(),
65 output_preview: step.outputs_preview.clone(),
66 error: step.error.clone(),
67 duration_ms: step.duration_ms,
68 vitals: step.vitals.clone(),
69 freshness_at_step: step.freshness_at_step,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct ExperienceBundle {
77 pub schema_version: u32,
78 pub bundle_id: String,
79 pub agent_id: String,
80 pub intent: String,
81 pub outcome: TrajectoryOutcome,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub host_outcome: Option<String>,
84 pub observation_count: u32,
85 pub fitness: f32,
86 pub events: Vec<ExperienceEvent>,
87 #[serde(default)]
88 pub source_trajectory_ids: Vec<String>,
89 #[serde(default)]
90 pub source_failure_ids: Vec<String>,
91 pub freshness: ContextFreshness,
92 pub impact_decision: ImpactDecision,
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case", tag = "kind")]
98pub enum ProcedureStepKind {
99 ToolCall {
100 tool: String,
101 #[serde(default)]
102 args_schema: serde_json::Value,
103 },
104 AdapterCall {
105 adapter: String,
106 op: String,
107 },
108 Validate {
109 target: String,
110 },
111 Branch {
112 condition: String,
113 },
114 HumanReview {
115 reason: String,
116 },
117 Instruction {
118 text: String,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct ProcedureStep {
125 pub step_id: String,
126 pub title: String,
127 pub kind: ProcedureStepKind,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub rationale: Option<String>,
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
134pub struct ProcedureVerification {
135 #[serde(default)]
136 pub checks: Vec<String>,
137 #[serde(default)]
138 pub success_criteria: Vec<String>,
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143pub struct ProcedureArtifact {
144 pub schema_version: u32,
145 pub id: String,
146 pub title: String,
147 pub intent: String,
148 pub summary: String,
149 #[serde(default)]
150 pub required_tools: Vec<String>,
151 #[serde(default)]
152 pub required_adapters: Vec<String>,
153 #[serde(default)]
154 pub inputs: Vec<String>,
155 #[serde(default)]
156 pub outputs: Vec<String>,
157 #[serde(default)]
158 pub preconditions: Vec<String>,
159 #[serde(default)]
160 pub steps: Vec<ProcedureStep>,
161 #[serde(default)]
162 pub verification: ProcedureVerification,
163 #[serde(default)]
164 pub known_failures: Vec<String>,
165 #[serde(default)]
166 pub recovery: Vec<String>,
167 #[serde(default)]
168 pub source_trajectory_ids: Vec<String>,
169 #[serde(default)]
170 pub source_failure_ids: Vec<String>,
171 pub fitness: f32,
172 pub observation_count: u32,
173 pub lifecycle: ProcedureLifecycle,
174 #[serde(default)]
175 pub render_targets: Vec<ProcedureArtifactFormat>,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct ProcedurePatch {
181 pub schema_version: u32,
182 pub patch_id: String,
183 pub procedure_id: String,
184 pub rationale: String,
185 #[serde(default)]
186 pub add_steps: Vec<ProcedureStep>,
187 #[serde(default)]
188 pub add_known_failures: Vec<String>,
189 #[serde(default)]
190 pub add_recovery: Vec<String>,
191 #[serde(default)]
192 pub source_failure_ids: Vec<String>,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct ProcedureReuseOutcome {
198 pub procedure_id: String,
199 pub outcome: TrajectoryOutcome,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub failure_id: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub notes: Option<String>,
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208pub struct ProcedureExecutionPlan {
209 pub procedure_id: String,
210 pub schema_version: u32,
211 #[serde(default)]
212 pub steps: Vec<ProcedureExecutionStep>,
213 #[serde(default)]
214 pub verification: ProcedureVerification,
215}
216
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub struct ProcedureExecutionStep {
219 pub step_id: String,
220 pub title: String,
221 pub executor: String,
222 pub operation: String,
223 #[serde(default)]
224 pub args_schema: serde_json::Value,
225 #[serde(default)]
226 pub depends_on: Vec<String>,
227 #[serde(default)]
228 pub on_error: String,
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::{TrajectoryOutcome, LEARNER_SCHEMA_VERSION};
235
236 #[test]
237 fn procedure_artifact_roundtrips_json() {
238 let p = ProcedureArtifact {
239 schema_version: LEARNER_SCHEMA_VERSION,
240 id: "proc:demo".into(),
241 title: "Demo".into(),
242 intent: "Do a demo".into(),
243 summary: "Reusable demo flow".into(),
244 required_tools: vec!["file_read".into()],
245 required_adapters: vec![],
246 inputs: vec!["path".into()],
247 outputs: vec!["summary".into()],
248 preconditions: vec!["Workspace exists".into()],
249 steps: vec![ProcedureStep {
250 step_id: "step-1".into(),
251 title: "Read file".into(),
252 kind: ProcedureStepKind::ToolCall {
253 tool: "file_read".into(),
254 args_schema: serde_json::json!({"type":"object"}),
255 },
256 rationale: None,
257 }],
258 verification: ProcedureVerification {
259 checks: vec!["Confirm output is non-empty".into()],
260 success_criteria: vec!["Output summarizes file".into()],
261 },
262 known_failures: vec![],
263 recovery: vec![],
264 source_trajectory_ids: vec!["traj-1".into()],
265 source_failure_ids: vec![],
266 fitness: 0.9,
267 observation_count: 3,
268 lifecycle: ProcedureLifecycle::Candidate,
269 render_targets: vec![ProcedureArtifactFormat::MarkdownSkill],
270 };
271 let j = serde_json::to_value(&p).unwrap();
272 let back: ProcedureArtifact = serde_json::from_value(j).unwrap();
273 assert_eq!(back, p);
274 }
275
276 #[test]
277 fn experience_event_from_trajectory_step() {
278 let step = TrajectoryStep {
279 step_id: "s1".into(),
280 timestamp_ms: 10,
281 adapter: "tool".into(),
282 operation: "file_read".into(),
283 inputs_preview: Some("in".into()),
284 outputs_preview: Some("out".into()),
285 duration_ms: 5,
286 success: true,
287 error: None,
288 vitals: None,
289 freshness_at_step: None,
290 frame_vars: None,
291 tool_telemetry: None,
292 };
293 let event = ExperienceEvent::from(&step);
294 assert_eq!(event.operation, "file_read");
295 assert!(event.success);
296 assert_eq!(event.duration_ms, 5);
297 }
298
299 #[test]
300 fn reuse_outcome_serializes_failure() {
301 let outcome = ProcedureReuseOutcome {
302 procedure_id: "proc:demo".into(),
303 outcome: TrajectoryOutcome::Failure,
304 failure_id: Some("failure-1".into()),
305 notes: None,
306 };
307 let j = serde_json::to_string(&outcome).unwrap();
308 assert!(j.contains("failure"));
309 }
310}