use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ContextSourceKind {
Memory,
ToolResult,
Resource,
File,
Reasoning,
Instruction,
UserInput,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextItem {
pub source: ContextSourceKind,
pub source_id: String,
pub rank: usize,
pub score: f64,
pub text: String,
pub estimated_chars: usize,
pub provenance: Value,
pub metadata: Value,
}
impl ContextItem {
#[must_use]
pub fn new(
source: ContextSourceKind,
source_id: impl Into<String>,
text: impl Into<String>,
) -> Self {
let text = text.into();
Self {
source,
source_id: source_id.into(),
rank: 0,
score: 0.0,
estimated_chars: text.chars().count(),
text,
provenance: Value::Null,
metadata: Value::Null,
}
}
#[must_use]
pub fn with_rank(mut self, rank: usize) -> Self {
self.rank = rank;
self
}
#[must_use]
pub fn with_score(mut self, score: f64) -> Self {
self.score = score;
self
}
#[must_use]
pub fn with_estimated_chars(mut self, estimated_chars: usize) -> Self {
self.estimated_chars = estimated_chars;
self
}
#[must_use]
pub fn with_provenance(mut self, provenance: Value) -> Self {
self.provenance = provenance;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: Value) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContextOmissionReason {
MaxItems,
OverBudget,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OmittedContextItem {
pub item: ContextItem,
pub reason: ContextOmissionReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextPackConfig {
pub max_chars: usize,
pub max_items: usize,
pub reserve_chars: usize,
pub separator: String,
}
impl Default for ContextPackConfig {
fn default() -> Self {
Self {
max_chars: 4_000,
max_items: 16,
reserve_chars: 0,
separator: "\n".into(),
}
}
}
impl ContextPackConfig {
#[must_use]
pub fn new(max_chars: usize) -> Self {
Self {
max_chars,
..Self::default()
}
}
#[must_use]
pub fn with_max_items(mut self, max_items: usize) -> Self {
self.max_items = max_items;
self
}
#[must_use]
pub fn with_reserve_chars(mut self, reserve_chars: usize) -> Self {
self.reserve_chars = reserve_chars;
self
}
#[must_use]
pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
fn context_budget(&self) -> usize {
self.max_chars.saturating_sub(self.reserve_chars)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextPack {
pub config: ContextPackConfig,
pub selected: Vec<ContextItem>,
pub omitted: Vec<OmittedContextItem>,
pub total_estimated_chars: usize,
}
impl ContextPack {
#[must_use]
pub fn pack(mut items: Vec<ContextItem>, config: ContextPackConfig) -> Self {
items.sort_by_key(|item| item.rank);
let budget = config.context_budget();
let separator_chars = config.separator.chars().count();
let mut selected = Vec::new();
let mut omitted = Vec::new();
let mut total_estimated_chars = 0usize;
for item in items {
if selected.len() >= config.max_items {
omitted.push(OmittedContextItem {
item,
reason: ContextOmissionReason::MaxItems,
});
continue;
}
let item_chars = item.estimated_chars.max(item.text.chars().count());
let separator_cost = if selected.is_empty() {
0
} else {
separator_chars
};
let Some(next_total) = total_estimated_chars
.checked_add(separator_cost)
.and_then(|total| total.checked_add(item_chars))
else {
omitted.push(OmittedContextItem {
item,
reason: ContextOmissionReason::OverBudget,
});
continue;
};
if next_total > budget {
omitted.push(OmittedContextItem {
item,
reason: ContextOmissionReason::OverBudget,
});
continue;
}
total_estimated_chars = next_total;
selected.push(item);
}
Self {
config,
selected,
omitted,
total_estimated_chars,
}
}
#[must_use]
pub fn render_text(&self) -> String {
self.selected
.iter()
.map(|item| item.text.as_str())
.collect::<Vec<_>>()
.join(&self.config.separator)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Signal(pub String);
impl Signal {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Evidence {
pub source_skill: String,
pub label: String,
pub detail: Value,
pub recorded_at: SystemTime,
}
impl Evidence {
pub fn new(source_skill: impl Into<String>, label: impl Into<String>) -> Self {
Self {
source_skill: source_skill.into(),
label: label.into(),
detail: Value::Null,
recorded_at: SystemTime::now(),
}
}
pub fn with_detail(mut self, detail: Value) -> Self {
self.detail = detail;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NextAction {
RunSkill(String),
InvokeTool { tool: String, args: Value },
Conclude,
Discard,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvestigationContext {
pub entity_id: String,
pub block_id: Option<Uuid>,
pub partition: String,
pub signals: Vec<Signal>,
pub evidence: Vec<Evidence>,
pub confidence: f32,
pub pending_actions: Vec<NextAction>,
}
impl InvestigationContext {
pub fn new(entity_id: impl Into<String>, partition: impl Into<String>) -> Self {
Self {
entity_id: entity_id.into(),
block_id: None,
partition: partition.into(),
signals: Vec::new(),
evidence: Vec::new(),
confidence: 0.0,
pending_actions: Vec::new(),
}
}
pub fn with_block<I: Into<Uuid>>(mut self, id: I) -> Self {
self.block_id = Some(id.into());
self
}
pub fn with_signal(mut self, s: impl Into<String>) -> Self {
self.signals.push(Signal::new(s));
self
}
pub fn has_signal(&self, name: &str) -> bool {
self.signals.iter().any(|s| s.as_str() == name)
}
}