use crate::engine::{stable_id, ThinkingStep};
use crate::link_store::{LinkStore, LinkStoreError};
use crate::memory::MemoryEvent;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub id: String,
pub kind: &'static str,
pub payload: String,
}
#[derive(Debug, Default, Clone)]
pub struct EventLog {
events: Vec<Event>,
}
impl EventLog {
#[must_use]
pub const fn new() -> Self {
Self { events: Vec::new() }
}
pub fn append(&mut self, kind: &'static str, payload: impl Into<String>) -> String {
let payload = payload.into();
let seed = format!("{kind}:{}:{payload}", self.events.len());
let id = stable_id(kind, &seed);
self.events.push(Event {
id: id.clone(),
kind,
payload,
});
id
}
#[must_use]
pub fn events(&self) -> &[Event] {
&self.events
}
#[must_use]
pub fn first_of(&self, kind: &str) -> Option<&Event> {
self.events.iter().find(|event| event.kind == kind)
}
#[must_use]
pub fn last_of(&self, kind: &str) -> Option<&Event> {
self.events.iter().rev().find(|event| event.kind == kind)
}
#[must_use]
pub fn evidence_links(&self) -> Vec<String> {
self.events
.iter()
.map(|event| format!("{}:{}", event.kind, event.id))
.collect()
}
#[must_use]
pub fn steps_block(&self) -> String {
use std::fmt::Write as _;
let mut buffer = String::from("steps:");
for (index, event) in self.events.iter().enumerate() {
let _ = write!(
buffer,
"\n step_{index} {} {}",
event.kind,
sanitize_payload(&event.payload)
);
}
buffer
}
#[must_use]
pub fn thinking_steps(&self) -> Vec<ThinkingStep> {
self.thinking_steps_for_answer("")
}
#[must_use]
pub fn thinking_steps_for_answer(&self, final_answer: &str) -> Vec<ThinkingStep> {
let answer = collapse_thinking_whitespace(final_answer);
let mut plan: Vec<PlannedThinkingStep> = Vec::new();
let mut calculation = CalculationCluster::default();
for event in &self.events {
if event.kind == "calculation" || event.kind.starts_with("calculation:") {
calculation.absorb(event.kind, &event.payload);
continue;
}
if calculation.has_data() {
calculation.drain_into(&mut plan);
}
if let Some(step) = curate_thinking_event(event.kind, &event.payload, &answer) {
plan.push(step);
}
}
if calculation.has_data() {
calculation.drain_into(&mut plan);
}
let mut steps: Vec<ThinkingStep> = Vec::new();
let mut order: u32 = 0;
let mut previous_key: Option<String> = None;
let mut current_parent: Option<String> = None;
for planned in plan {
let key = format!("{}\u{1f}{}", planned.step, planned.detail);
let is_child = planned.role == StepRole::Child;
if !is_child && previous_key.as_deref() == Some(key.as_str()) {
continue;
}
previous_key = Some(key);
let mut step = ThinkingStep::new(
order,
planned.step,
planned.detail,
planned.level,
planned.source,
);
match planned.role {
StepRole::Parent => current_parent = Some(step.id.clone()),
StepRole::Child => {
if let Some(parent) = current_parent.clone() {
step = step.with_parent(parent);
}
}
StepRole::Normal => current_parent = None,
}
steps.push(step);
order += 1;
}
steps
}
pub fn append_to_link_store<S: LinkStore>(
&self,
store: &mut S,
) -> Result<usize, LinkStoreError> {
for event in &self.events {
store.append_memory_event(MemoryEvent {
id: event.id.clone(),
kind: Some(event.kind.to_owned()),
content: Some(event.payload.clone()),
evidence: vec![format!("{}:{}", event.kind, event.id)],
..MemoryEvent::default()
})?;
}
Ok(self.events.len())
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StepRole {
Normal,
Parent,
Child,
}
struct PlannedThinkingStep {
step: &'static str,
detail: String,
level: &'static str,
source: &'static str,
role: StepRole,
}
impl PlannedThinkingStep {
fn normal(
step: &'static str,
detail: impl Into<String>,
level: &'static str,
source: &'static str,
) -> Self {
Self {
step,
detail: detail.into(),
level,
source,
role: StepRole::Normal,
}
}
fn parent(
step: &'static str,
detail: impl Into<String>,
level: &'static str,
source: &'static str,
) -> Self {
Self {
role: StepRole::Parent,
..Self::normal(step, detail, level, source)
}
}
fn child(
step: &'static str,
detail: impl Into<String>,
level: &'static str,
source: &'static str,
) -> Self {
Self {
role: StepRole::Child,
..Self::normal(step, detail, level, source)
}
}
}
#[derive(Default)]
struct CalculationCluster {
request: Option<String>,
engine: Option<String>,
expression: Option<String>,
steps: Option<String>,
result: Option<String>,
}
impl CalculationCluster {
fn absorb(&mut self, kind: &str, payload: &str) {
let value = payload.trim().to_owned();
match kind {
"calculation" => self.result = Some(value),
"calculation:request" => self.request = Some(value),
"calculation:engine" => self.engine = Some(value),
"calculation:lino" => self.expression = Some(value),
"calculation:steps" => self.steps = Some(value),
_ => {}
}
}
const fn has_data(&self) -> bool {
self.result.is_some()
|| self.request.is_some()
|| self.engine.is_some()
|| self.expression.is_some()
|| self.steps.is_some()
}
fn drain_into(&mut self, plan: &mut Vec<PlannedThinkingStep>) {
let parent_detail = self
.result
.clone()
.or_else(|| self.request.clone())
.unwrap_or_default();
plan.push(PlannedThinkingStep::parent(
"compute",
parent_detail,
"high",
"calculation",
));
if let Some(engine) = self.engine.take() {
plan.push(PlannedThinkingStep::child(
"compute_engine",
engine,
"detailed",
"calculation:engine",
));
}
if let Some(expression) = self.expression.take() {
plan.push(PlannedThinkingStep::child(
"compute_expression",
expression,
"detailed",
"calculation:lino",
));
}
if let Some(steps) = self.steps.take() {
plan.push(PlannedThinkingStep::child(
"compute_steps",
steps,
"detailed",
"calculation:steps",
));
}
*self = Self::default();
}
}
fn collapse_thinking_whitespace(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn curate_thinking_event(
kind: &'static str,
payload: &str,
final_answer: &str,
) -> Option<PlannedThinkingStep> {
let detail = payload.trim();
let keep = |step: &'static str, detail: &str, level: &'static str| {
Some(PlannedThinkingStep::normal(
step,
detail.to_owned(),
level,
kind,
))
};
match kind {
"impulse" => keep("impulse", detail, "high"),
"language" | "language_from" => keep("detect_language", detail, "high"),
"language_to" => keep("resolve_response_language", detail, "high"),
"intent_formalization:route" => keep("formalize", detail, "high"),
"intent" | "specialized_handler" | "legacy_intent" => {
keep("dispatch_handler", detail, "high")
}
"program_plan" | "program_parameters" => keep("program_plan", detail, "high"),
"response" => {
let value = if final_answer.is_empty() {
detail
} else {
final_answer
};
keep("deformalize", value, "high")
}
"concept_lookup:request" | "procedural_how_to:request" => {
keep("scan_memory", detail, "detailed")
}
"concept_lookup:hit" | "fact_query:relation" | "fact_query:subject" | "fact_lookup:hit" => {
keep("lookup_fact", detail, "detailed")
}
"web_search:request" | "http_fetch:request" | "url_navigate:request" => {
keep("http_chat", detail, "detailed")
}
"tool_call" | "tool_result" => keep("invoke_tool", detail, "detailed"),
"coreference" | "program_coreference" => keep("coreference_binding", "", "detailed"),
"modifier_detection" | "program_modifiers" => keep("modifier_detection", "", "detailed"),
"rule_construction" => keep("rule_construction", "", "detailed"),
"validation" => keep("rule_verification", "", "detailed"),
_ if kind.starts_with("tool_") => keep("invoke_tool", detail, "detailed"),
_ if kind.starts_with("agent_mode") || kind == "action_log" => {
keep("agent_plan", detail, "detailed")
}
_ => None,
}
}
#[must_use]
pub fn build_evidence_links(prompt: &str, log: &EventLog, response_link: &str) -> Vec<String> {
let mut links: Vec<String> = Vec::new();
links.push(format!("prompt:{}", stable_id("prompt", prompt)));
for event in log.events() {
let evidence = match event.kind {
"trace:execution_failure" => format!("trace:execution_failure:{}", event.id),
"language" => format!("language:{}", event.payload),
"language_from" => format!("language_from:{}", event.payload),
"language_to" => format!("language_to:{}", event.payload),
"definition_merge:language" => format!("definition_merge:language:{}", event.payload),
"meaning" => format!("meaning:{}", event.payload),
"translation_gap" => format!("translation_gap:{}", event.payload),
"wikidata" => format!("wikidata:{}", event.payload),
"formalization" => format!("formalization:{}", event.id),
"formalization:subject_q" => {
format!("formalization:subject_q:{}", event.payload)
}
"formalization:predicate_p" => {
format!("formalization:predicate_p:{}", event.payload)
}
"formalization:object_q" => {
format!("formalization:object_q:{}", event.payload)
}
"formalization:item_q" => format!("formalization:item_q:{}", event.payload),
"formalization:property_p" => {
format!("formalization:property_p:{}", event.payload)
}
"formalization:fallback" => {
format!("formalization:fallback:{}", event.payload)
}
"formalization:raw" => format!("formalization:raw:{}", event.payload),
"formalization_unresolved" => {
format!("formalization_unresolved:{}", event.payload)
}
"intent_formalization" => format!("intent_formalization:{}", event.id),
"intent_formalization_cache" => {
format!(
"intent_formalization_cache:{}",
event.payload.replace(' ', ":")
)
}
"intent_formalization:kind" => {
format!("intent_formalization:kind:{}", event.payload)
}
"intent_formalization:route" => {
format!("intent_formalization:route:{}", event.payload)
}
"intent_formalization:relevant" => {
format!("intent_formalization:relevant:{}", event.payload)
}
"fact_query:request" => format!("fact_query:request:{}", event.id),
"fact_query:relation" => format!("fact_query:relation:{}", event.payload),
"fact_query:subject" => format!("fact_query:subject:{}", event.payload),
"fact_query:cache:hit" => format!("fact_query:cache:hit:{}", event.payload),
"fact_query:cache:miss" => String::from("fact_query:cache:miss"),
"fact_query:cache:bypass" => String::from("fact_query:cache:bypass"),
"fact_query:force_fresh" => String::from("fact_query:force_fresh"),
"fact_query:subject_qid" => format!("fact_query:subject_qid:{}", event.payload),
"fact_query:value_qid" => format!("fact_query:value_qid:{}", event.payload),
"web_search:request" => format!("web_search:request:{}", event.payload),
"web_search:query_kind" => format!("web_search:query_kind:{}", event.payload),
"web_search:provider" => format!("web_search:provider:{}", event.payload),
"web_search:language" => format!("web_search:language:{}", event.payload),
"web_search:combined" => format!("web_search:combined:{}", event.payload),
"web_search:rank" => format!("web_search:rank:{}", event.payload),
"web_search:fused" => format!("web_search:fused:{}", event.payload),
"web_search:disabled" => format!("web_search:disabled:{}", event.payload),
"http_fetch:request" => format!("http_fetch:request:{}", event.payload),
"docs_method:request" => format!("docs_method:request:{}", event.id),
"docs_method:project" => format!("docs_method:project:{}", event.payload),
"docs_method:method" => format!("docs_method:method:{}", event.payload),
"docs_method:source_kind" => {
format!("docs_method:source_kind:{}", event.payload)
}
"docs_method:source" => format!("source:{}", event.payload),
"project:promoted" => format!("project:promoted:{}", event.payload),
"project_lookup:promotion" => {
format!("project_lookup:promotion:{}", event.payload)
}
"project_lookup:repository:github" => {
format!("project_lookup:repository:github:{}", event.payload)
}
"project_lookup:repository:gitlab" => {
format!("project_lookup:repository:gitlab:{}", event.payload)
}
"project_lookup:repository:bitbucket" => {
format!("project_lookup:repository:bitbucket:{}", event.payload)
}
"url_navigate:request" => format!("url_navigate:request:{}", event.payload),
"url_preview:iframe" => format!("url_preview:iframe:{}", event.payload),
"tool_call" => format!("tool_call:{}", event.payload),
"tool_parameter" => format!("tool_parameter:{}", event.payload.replace(' ', ":")),
"tool_result" => format!("tool_result:{}", event.payload.replace(' ', ":")),
"tool_permission" => {
format!("tool_permission:{}", event.payload.replace(' ', ":"))
}
"text_operation" => format!("text_operation:{}", event.payload),
"text_rule" => format!("text_rule:{}", event.payload),
"text_rule_chain" => format!("text_rule_chain:{}", event.payload),
"text_result" => format!("text_result:{}", event.id),
"text_substitution_rules" => format!("text_substitution_rules:{}", event.id),
"text_substitution_trace" => format!("text_substitution_trace:{}", event.id),
"text_substitution_graph" => format!("text_substitution_graph:{}", event.id),
"procedural_how_to:request" => {
format!("procedural_how_to:request:{}", event.payload)
}
"procedural_how_to:action" => {
format!("procedural_how_to:action:{}", event.payload)
}
"procedural_how_to:object" => {
format!("procedural_how_to:object:{}", event.payload)
}
"procedural_how_to:stage" => {
format!("procedural_how_to:stage:{}", event.payload)
}
"procedural_how_to:wikihow_candidate" => {
format!("procedural_how_to:wikihow_candidate:{}", event.payload)
}
"procedural_how_to:source_gate" => {
format!("procedural_how_to:source_gate:{}", event.payload)
}
"spelling_correction" => {
format!("spelling_correction:{}", event.payload.replace(' ', ""))
}
"concept_lookup:request" => format!("concept_lookup:request:{}", event.payload),
"concept_lookup:context" => format!("concept_lookup:context:{}", event.payload),
"concept_lookup:hit" => format!("concept_lookup:hit:{}", event.payload),
"concept_lookup:miss" => format!("concept_lookup:miss:{}", event.payload),
"concept_lookup:context-match" => {
format!("concept_lookup:context-match:{}", event.payload)
}
"concept_lookup:context-mismatch" => {
format!("concept_lookup:context-mismatch:{}", event.payload)
}
"followup:subject" => format!("followup:subject:{}", event.payload),
"mechanism_query:request" => {
format!("mechanism_query:request:{}", event.payload)
}
"mechanism_query:stage" => format!("mechanism_query:stage:{}", event.payload),
"mechanism_query:source_gate" => {
format!("mechanism_query:source_gate:{}", event.payload)
}
"search:local" => format!("search:local:{}", event.id),
"search:external" if event.payload == "skipped:offline" => {
String::from("policy:offline")
}
"search:external" => format!("search:external:{}", event.id),
"source:http" => format!("source:http:{}", event.payload.replace(' ', ":")),
"source_refresh" => format!("source_refresh:{}", event.payload),
"skill_compile:package" => format!("skill_compile:package:{}", event.payload),
"compiled_skill:package" => format!("compiled_skill:package:{}", event.id),
"compiled_skill:replay" => format!("compiled_skill:replay:{}", event.payload),
"conflict:source_disagreement" => {
format!("conflict:source_disagreement:{}", event.id)
}
"cache_hit" => format!("cache_hit:{}", event.payload),
"network_fetch" => format!("network_fetch:{}", event.id),
"calculation:engine" => format!("calculation:engine:{}", event.payload),
"calculation:lino" => format!("calculation:lino:{}", event.payload),
"intent" => format!("intent:{}", event.payload),
"program_parameter:language" => {
format!("program_parameter:language:{}", event.payload)
}
"program_parameter:task" => format!("program_parameter:task:{}", event.payload),
"program_parameters" => {
format!(
"program_parameters:{}",
event.payload.replace([' ', ','], ":")
)
}
"legacy_intent" => format!("legacy_intent:{}", event.payload),
"response" => event.payload.clone(),
"agent_mode:opted_in" => format!("agent_mode:opted_in:{}", event.id),
"agent_mode:active" => format!("agent_mode:active:{}", event.id),
"policy:chat_bounded_autonomy" => String::from("policy:chat_bounded_autonomy"),
"policy:add_only_history" => String::from("policy:add_only_history"),
"policy:destructive_action_requires_confirmation" => {
String::from("policy:destructive_action_requires_confirmation")
}
"policy:agent_time_budget" => format!("policy:agent_time_budget:{}", event.id),
"policy:cache_flush_requires_confirmation" => {
String::from("policy:cache_flush_requires_confirmation")
}
"policy:offline" => String::from("policy:offline"),
"policy:inappropriate_content" => String::from("policy:inappropriate_content"),
"policy:agent_mode_required_for_tools" => {
format!("policy:agent_mode_required_for_tools:{}", event.payload)
}
"policy:package_permission_required" => {
format!("policy:package_permission_required:{}", event.payload)
}
"policy:temperature_selection" => {
format!("policy:temperature_selection:{}", event.id)
}
"policy:guessed_under_ambiguity" => String::from("policy:guessed_under_ambiguity"),
"policy:clarify_under_ambiguity" => String::from("policy:clarify_under_ambiguity"),
"probability:evidence" => format!("probability:evidence:{}", event.id),
"probability:model" => format!("probability:model:{}", event.payload),
"probability:ranking" => format!("probability:ranking:{}", event.id),
"error" => format!("error:{}", event.id),
"filter:user" => format!("filter:user:{}", event.payload),
"diagnostic_mode" => format!("diagnostic_mode:{}", event.payload),
"execution_status" => format!("execution_status:{}", event.id),
"execution_environment" => format!("execution_environment:{}", event.id),
_ => format!("{}:{}", event.kind, event.id),
};
links.push(evidence);
}
if !links.iter().any(|link| link == response_link) {
links.push(response_link.to_owned());
}
links
}
fn sanitize_payload(value: &str) -> String {
value
.replace('\r', "\\r")
.replace('\n', "\\n")
.replace('\t', "\\t")
}