Skip to main content

jamjet_state/
event.rs

1use chrono::{DateTime, Utc};
2use jamjet_core::node::NodeId;
3use jamjet_core::workflow::ExecutionId;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Monotonically increasing sequence number within a workflow execution.
8pub type EventSequence = i64;
9
10/// A durable, immutable record of a state transition. Events are appended
11/// to the event log and never modified or deleted (except by compaction).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Event {
14    pub id: Uuid,
15    pub execution_id: ExecutionId,
16    pub sequence: EventSequence,
17    pub kind: EventKind,
18    pub created_at: DateTime<Utc>,
19}
20
21impl Event {
22    pub fn new(execution_id: ExecutionId, sequence: EventSequence, kind: EventKind) -> Self {
23        Self {
24            id: Uuid::new_v4(),
25            execution_id,
26            sequence,
27            kind,
28            created_at: Utc::now(),
29        }
30    }
31}
32
33/// Provenance metadata attached to node completions for research traceability.
34#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
35pub struct ProvenanceMetadata {
36    /// Model identifier used for this node (e.g., "claude-haiku-4-5-20251001")
37    pub model_id: Option<String>,
38    /// Model version or checkpoint (e.g., "20251001")
39    pub model_version: Option<String>,
40    /// Confidence score (0.0-1.0) if available
41    pub confidence: Option<f64>,
42    /// Whether the output was verified by another model/check
43    #[serde(default)]
44    pub verified: bool,
45    /// Source identifier (e.g., "mcp:brave-search", "a2a:research-agent")
46    pub source: Option<String>,
47    /// Trust domain this output belongs to (e.g., "internal", "external:partner-org").
48    pub trust_domain: Option<String>,
49    /// References to supporting evidence (URIs, document IDs, event sequences).
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub evidence_refs: Vec<String>,
52}
53
54/// All possible event kinds in the JamJet event log.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum EventKind {
58    // ── Workflow lifecycle ───────────────────────────────────────────────
59    WorkflowStarted {
60        workflow_id: String,
61        workflow_version: String,
62        initial_input: serde_json::Value,
63    },
64    WorkflowCompleted {
65        final_state: serde_json::Value,
66    },
67    WorkflowFailed {
68        error: String,
69    },
70    WorkflowCancelled {
71        reason: Option<String>,
72    },
73
74    // ── Node lifecycle ───────────────────────────────────────────────────
75    NodeScheduled {
76        node_id: NodeId,
77        queue_type: String,
78    },
79    NodeStarted {
80        node_id: NodeId,
81        worker_id: String,
82        attempt: u32,
83    },
84    NodeCompleted {
85        node_id: NodeId,
86        output: serde_json::Value,
87        /// JSON merge patch to apply to workflow state.
88        state_patch: serde_json::Value,
89        duration_ms: u64,
90        // ── GenAI telemetry (populated for model nodes) ──────────────────────
91        /// AI provider system (e.g. "anthropic", "openai"). None for non-model nodes.
92        #[serde(skip_serializing_if = "Option::is_none")]
93        gen_ai_system: Option<String>,
94        /// Model name used.
95        #[serde(skip_serializing_if = "Option::is_none")]
96        gen_ai_model: Option<String>,
97        /// Input tokens consumed.
98        #[serde(skip_serializing_if = "Option::is_none")]
99        input_tokens: Option<u64>,
100        /// Output tokens generated.
101        #[serde(skip_serializing_if = "Option::is_none")]
102        output_tokens: Option<u64>,
103        /// Finish reason (e.g. "stop", "length", "tool_calls").
104        #[serde(skip_serializing_if = "Option::is_none")]
105        finish_reason: Option<String>,
106        /// Estimated USD cost for this node.
107        #[serde(skip_serializing_if = "Option::is_none")]
108        cost_usd: Option<f64>,
109        /// Provenance metadata for research traceability.
110        #[serde(skip_serializing_if = "Option::is_none")]
111        provenance: Option<Box<ProvenanceMetadata>>,
112    },
113    NodeFailed {
114        node_id: NodeId,
115        error: String,
116        attempt: u32,
117        retryable: bool,
118    },
119    NodeSkipped {
120        node_id: NodeId,
121        reason: String,
122    },
123    NodeCancelled {
124        node_id: NodeId,
125    },
126
127    // ── Retry ────────────────────────────────────────────────────────────
128    RetryScheduled {
129        node_id: NodeId,
130        attempt: u32,
131        delay_ms: u64,
132    },
133
134    // ── Human approval / interrupt ────────────────────────────────────────
135    InterruptRaised {
136        node_id: NodeId,
137        reason: String,
138        state_for_review: serde_json::Value,
139    },
140    ApprovalReceived {
141        node_id: NodeId,
142        user_id: String,
143        decision: ApprovalDecision,
144        comment: Option<String>,
145        state_patch: Option<serde_json::Value>,
146    },
147
148    // ── Timers ────────────────────────────────────────────────────────────
149    TimerCreated {
150        node_id: NodeId,
151        fire_at: DateTime<Utc>,
152        correlation_key: Option<String>,
153    },
154    TimerFired {
155        node_id: NodeId,
156        correlation_key: Option<String>,
157    },
158
159    // ── External events ───────────────────────────────────────────────────
160    ExternalEventReceived {
161        correlation_key: String,
162        payload: serde_json::Value,
163    },
164
165    // ── Child workflows ───────────────────────────────────────────────────
166    ChildWorkflowStarted {
167        node_id: NodeId,
168        child_execution_id: String,
169        child_workflow_id: String,
170    },
171    ChildWorkflowCompleted {
172        node_id: NodeId,
173        child_execution_id: String,
174        result: serde_json::Value,
175    },
176    ChildWorkflowFailed {
177        node_id: NodeId,
178        child_execution_id: String,
179        error: String,
180    },
181
182    // ── Budget / autonomy ─────────────────────────────────────────────────
183    BudgetExceeded {
184        node_id: NodeId,
185        kind: String,
186        limit: u64,
187        current: u64,
188    },
189    TokenBudgetExceeded {
190        node_id: NodeId,
191        /// "input_tokens" | "output_tokens" | "total_tokens"
192        kind: String,
193        limit: u64,
194        current: u64,
195    },
196    CostBudgetExceeded {
197        node_id: NodeId,
198        limit_usd: f64,
199        current_usd: f64,
200    },
201    AutonomyLimitReached {
202        node_id: NodeId,
203        agent_ref: String,
204        /// "max_iterations" | "cost_budget" | "token_budget" | "max_tool_calls"
205        limit_type: String,
206        limit_value: serde_json::Value,
207        actual_value: serde_json::Value,
208    },
209    CircuitBreakerTripped {
210        node_id: NodeId,
211        agent_ref: String,
212        consecutive_errors: u32,
213        threshold: u32,
214    },
215    EscalationRequired {
216        node_id: NodeId,
217        agent_ref: String,
218        /// "circuit_breaker" | "autonomy_limit" | "budget_exceeded"
219        reason: String,
220        /// "supervisor_agent:<id>" | "human_approval"
221        escalation_target: String,
222    },
223
224    // ── Policy ────────────────────────────────────────────────────────────
225    PolicyViolation {
226        node_id: NodeId,
227        /// Which rule triggered (e.g. "block_tool:payments.*")
228        rule: String,
229        /// "blocked" | "require_approval"
230        decision: String,
231        /// "global" | "tenant" | "workflow" | "node"
232        policy_scope: String,
233    },
234    ToolApprovalRequired {
235        node_id: NodeId,
236        tool_name: String,
237        approver: String,
238        context: serde_json::Value,
239    },
240
241    // ── Reasoning strategy lifecycle (§14.5) ─────────────────────────────
242    /// Emitted when a reasoning strategy begins execution.
243    StrategyStarted {
244        strategy: String,
245        config: serde_json::Value,
246    },
247    /// Emitted by plan-and-execute when the plan is generated.
248    PlanGenerated {
249        steps: Vec<String>,
250    },
251    /// Emitted at the start of each reasoning loop iteration.
252    IterationStarted {
253        iteration: u32,
254    },
255    /// Emitted each time a tool is invoked within a strategy loop.
256    ToolCalled {
257        node_id: NodeId,
258        tool: String,
259    },
260    /// Emitted by critic/verifier nodes with a quality score.
261    CriticVerdict {
262        node_id: NodeId,
263        score: f64,
264        passed: bool,
265        feedback: Option<String>,
266    },
267    /// Emitted at the end of each iteration with cost/token delta.
268    IterationCompleted {
269        iteration: u32,
270        cost_delta_usd: Option<f64>,
271        input_tokens: u64,
272        output_tokens: u64,
273    },
274    /// Emitted when a strategy limit (max_iterations, max_cost_usd, timeout) is hit.
275    /// Workflow transitions to `LimitExceeded` after this event.
276    StrategyLimitHit {
277        limit_type: String,
278        limit_value: serde_json::Value,
279        actual_value: serde_json::Value,
280    },
281    /// Emitted when strategy execution completes successfully.
282    StrategyCompleted {
283        iterations: u32,
284        total_cost_usd: Option<f64>,
285    },
286
287    // ── Coordinator events ────────────────────────────────────────────────
288    CoordinatorDiscovery {
289        node_id: NodeId,
290        query_skills: Vec<String>,
291        query_trust_domain: Option<String>,
292        candidates: Vec<serde_json::Value>,
293        filtered_out: Vec<serde_json::Value>,
294    },
295    CoordinatorScoring {
296        node_id: NodeId,
297        rankings: Vec<serde_json::Value>,
298        spread: f64,
299        weights: serde_json::Value,
300    },
301    CoordinatorDecision {
302        node_id: NodeId,
303        selected: Option<String>,
304        method: String,
305        reasoning: Option<String>,
306        confidence: f64,
307        rejected: Vec<serde_json::Value>,
308        tiebreaker_tokens: Option<serde_json::Value>,
309        tiebreaker_cost: Option<f64>,
310    },
311
312    // ── Agent-as-Tool events ──────────────────────────────────────────────
313    AgentToolInvoked {
314        node_id: NodeId,
315        agent_uri: String,
316        mode: String,
317        protocol: String,
318        input_hash: String,
319    },
320    AgentToolProgress {
321        node_id: NodeId,
322        chunk_index: u32,
323        partial_output_summary: String,
324    },
325    AgentToolTurn {
326        node_id: NodeId,
327        turn_number: u32,
328        direction: String,
329        content_summary: String,
330        tokens: u32,
331        cost: f64,
332    },
333    AgentToolCompleted {
334        node_id: NodeId,
335        output: serde_json::Value,
336        provenance: Option<serde_json::Value>,
337        total_cost: f64,
338        latency_ms: u64,
339        total_turns: Option<u32>,
340    },
341    AgentToolTerminated {
342        node_id: NodeId,
343        reason: String,
344        chunks_received: u32,
345        partial_output: Option<serde_json::Value>,
346        cost: f64,
347    },
348    AgentToolFailed {
349        node_id: NodeId,
350        failure_type: String,
351        message: String,
352        retryable: bool,
353    },
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358pub enum ApprovalDecision {
359    Approved,
360    Rejected,
361}
362
363impl EventKind {
364    /// Returns the node_id associated with this event, if any.
365    pub fn node_id(&self) -> Option<&str> {
366        match self {
367            Self::NodeScheduled { node_id, .. }
368            | Self::NodeStarted { node_id, .. }
369            | Self::NodeCompleted { node_id, .. }
370            | Self::NodeFailed { node_id, .. }
371            | Self::NodeSkipped { node_id, .. }
372            | Self::NodeCancelled { node_id }
373            | Self::RetryScheduled { node_id, .. }
374            | Self::InterruptRaised { node_id, .. }
375            | Self::ApprovalReceived { node_id, .. }
376            | Self::TimerCreated { node_id, .. }
377            | Self::TimerFired { node_id, .. }
378            | Self::BudgetExceeded { node_id, .. }
379            | Self::TokenBudgetExceeded { node_id, .. }
380            | Self::CostBudgetExceeded { node_id, .. }
381            | Self::AutonomyLimitReached { node_id, .. }
382            | Self::CircuitBreakerTripped { node_id, .. }
383            | Self::EscalationRequired { node_id, .. }
384            | Self::PolicyViolation { node_id, .. }
385            | Self::ToolApprovalRequired { node_id, .. }
386            | Self::ChildWorkflowStarted { node_id, .. }
387            | Self::ChildWorkflowCompleted { node_id, .. }
388            | Self::ChildWorkflowFailed { node_id, .. }
389            | Self::CoordinatorDiscovery { node_id, .. }
390            | Self::CoordinatorScoring { node_id, .. }
391            | Self::CoordinatorDecision { node_id, .. }
392            | Self::AgentToolInvoked { node_id, .. }
393            | Self::AgentToolProgress { node_id, .. }
394            | Self::AgentToolTurn { node_id, .. }
395            | Self::AgentToolCompleted { node_id, .. }
396            | Self::AgentToolTerminated { node_id, .. }
397            | Self::AgentToolFailed { node_id, .. } => Some(node_id.as_str()),
398            _ => None,
399        }
400    }
401}