Skip to main content

rig_compose/
context.rs

1//! [`InvestigationContext`] — the runtime object that flows through every
2//! [`super::Skill`] in an agent step.
3//!
4//! Skills mutate the context by appending [`Evidence`] and adjusting
5//! confidence; they do not own it. The owning [`super::Agent`] threads a
6//! single context through its skill chain for one investigation.
7
8use std::time::SystemTime;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use uuid::Uuid;
13
14/// A named, lightweight signal lifted from a sketch, baseline check, or
15/// upstream skill. Skills key their `applies` predicate on signal names.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct Signal(pub String);
18
19impl Signal {
20    pub fn new(s: impl Into<String>) -> Self {
21        Self(s.into())
22    }
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28/// A single piece of evidence accumulated during an investigation.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Evidence {
31    pub source_skill: String,
32    pub label: String,
33    pub detail: Value,
34    pub recorded_at: SystemTime,
35}
36
37impl Evidence {
38    pub fn new(source_skill: impl Into<String>, label: impl Into<String>) -> Self {
39        Self {
40            source_skill: source_skill.into(),
41            label: label.into(),
42            detail: Value::Null,
43            recorded_at: SystemTime::now(),
44        }
45    }
46
47    pub fn with_detail(mut self, detail: Value) -> Self {
48        self.detail = detail;
49        self
50    }
51}
52
53/// Hint a skill may emit to drive subsequent skill selection. The agent
54/// loop is free to honour or ignore these — they are advisory.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub enum NextAction {
57    /// Suggest a follow-up skill by id.
58    RunSkill(String),
59    /// Suggest invoking a named tool with prepared args.
60    InvokeTool { tool: String, args: Value },
61    /// Stop the investigation; sufficient evidence has been gathered.
62    Conclude,
63    /// Drop the investigation; the entity is benign.
64    Discard,
65}
66
67/// Runtime state for one investigation. Cheap to construct; passed by
68/// `&mut` reference through the skill chain.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct InvestigationContext {
71    /// Stable identifier for the entity under investigation. May be a block
72    /// id stringified, an actor id from the grammar layer (Phase 2), or any
73    /// caller-defined key.
74    pub entity_id: String,
75
76    /// Optional originating block — present when the investigation was
77    /// triggered by an upstream pipeline. Stored as an opaque UUID so the
78    /// kernel does not depend on any specific block-id newtype.
79    pub block_id: Option<Uuid>,
80
81    /// Free-form partition tag (caller-defined).
82    pub partition: String,
83
84    /// Signals that triggered this investigation and any signals lifted by
85    /// earlier skills. Skills add to this set as evidence accumulates.
86    pub signals: Vec<Signal>,
87
88    /// Accumulated evidence in chronological order.
89    pub evidence: Vec<Evidence>,
90
91    /// Running confidence in `[0, 1]` that the entity exhibits malicious
92    /// behaviour. Skills emit deltas; the agent clamps after each step.
93    pub confidence: f32,
94
95    /// Hints from the most recently executed skill.
96    pub pending_actions: Vec<NextAction>,
97}
98
99impl InvestigationContext {
100    pub fn new(entity_id: impl Into<String>, partition: impl Into<String>) -> Self {
101        Self {
102            entity_id: entity_id.into(),
103            block_id: None,
104            partition: partition.into(),
105            signals: Vec::new(),
106            evidence: Vec::new(),
107            confidence: 0.0,
108            pending_actions: Vec::new(),
109        }
110    }
111
112    pub fn with_block<I: Into<Uuid>>(mut self, id: I) -> Self {
113        self.block_id = Some(id.into());
114        self
115    }
116
117    pub fn with_signal(mut self, s: impl Into<String>) -> Self {
118        self.signals.push(Signal::new(s));
119        self
120    }
121
122    pub fn has_signal(&self, name: &str) -> bool {
123        self.signals.iter().any(|s| s.as_str() == name)
124    }
125}