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}