1use crate::engine::{stable_id, ThinkingStep};
12use crate::link_store::{LinkStore, LinkStoreError};
13use crate::memory::MemoryEvent;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Event {
18 pub id: String,
19 pub kind: &'static str,
20 pub payload: String,
21}
22
23#[derive(Debug, Default, Clone)]
25pub struct EventLog {
26 events: Vec<Event>,
27}
28
29impl EventLog {
30 #[must_use]
31 pub const fn new() -> Self {
32 Self { events: Vec::new() }
33 }
34
35 pub fn append(&mut self, kind: &'static str, payload: impl Into<String>) -> String {
40 let payload = payload.into();
41 let seed = format!("{kind}:{}:{payload}", self.events.len());
42 let id = stable_id(kind, &seed);
43 self.events.push(Event {
44 id: id.clone(),
45 kind,
46 payload,
47 });
48 id
49 }
50
51 #[must_use]
52 pub fn events(&self) -> &[Event] {
53 &self.events
54 }
55
56 #[must_use]
58 pub fn first_of(&self, kind: &str) -> Option<&Event> {
59 self.events.iter().find(|event| event.kind == kind)
60 }
61
62 #[must_use]
64 pub fn last_of(&self, kind: &str) -> Option<&Event> {
65 self.events.iter().rev().find(|event| event.kind == kind)
66 }
67
68 #[must_use]
71 pub fn evidence_links(&self) -> Vec<String> {
72 self.events
73 .iter()
74 .map(|event| format!("{}:{}", event.kind, event.id))
75 .collect()
76 }
77
78 #[must_use]
81 pub fn steps_block(&self) -> String {
82 use std::fmt::Write as _;
83 let mut buffer = String::from("steps:");
84 for (index, event) in self.events.iter().enumerate() {
85 let _ = write!(
86 buffer,
87 "\n step_{index} {} {}",
88 event.kind,
89 sanitize_payload(&event.payload)
90 );
91 }
92 buffer
93 }
94
95 #[must_use]
108 pub fn thinking_steps(&self) -> Vec<ThinkingStep> {
109 self.thinking_steps_for_answer("")
110 }
111
112 #[must_use]
116 pub fn thinking_steps_for_answer(&self, final_answer: &str) -> Vec<ThinkingStep> {
117 let answer = collapse_thinking_whitespace(final_answer);
118
119 let mut plan: Vec<PlannedThinkingStep> = Vec::new();
122 let mut calculation = CalculationCluster::default();
123 for event in &self.events {
124 if event.kind == "calculation" || event.kind.starts_with("calculation:") {
125 calculation.absorb(event.kind, &event.payload);
126 continue;
127 }
128 if calculation.has_data() {
129 calculation.drain_into(&mut plan);
130 }
131 if let Some(step) = curate_thinking_event(event.kind, &event.payload, &answer) {
132 plan.push(step);
133 }
134 }
135 if calculation.has_data() {
136 calculation.drain_into(&mut plan);
137 }
138
139 let mut steps: Vec<ThinkingStep> = Vec::new();
142 let mut order: u32 = 0;
143 let mut previous_key: Option<String> = None;
144 let mut current_parent: Option<String> = None;
145 for planned in plan {
146 let key = format!("{}\u{1f}{}", planned.step, planned.detail);
147 let is_child = planned.role == StepRole::Child;
148 if !is_child && previous_key.as_deref() == Some(key.as_str()) {
149 continue;
150 }
151 previous_key = Some(key);
152 let mut step = ThinkingStep::new(
153 order,
154 planned.step,
155 planned.detail,
156 planned.level,
157 planned.source,
158 );
159 match planned.role {
160 StepRole::Parent => current_parent = Some(step.id.clone()),
161 StepRole::Child => {
162 if let Some(parent) = current_parent.clone() {
163 step = step.with_parent(parent);
164 }
165 }
166 StepRole::Normal => current_parent = None,
167 }
168 steps.push(step);
169 order += 1;
170 }
171 steps
172 }
173
174 pub fn append_to_link_store<S: LinkStore>(
180 &self,
181 store: &mut S,
182 ) -> Result<usize, LinkStoreError> {
183 for event in &self.events {
184 store.append_memory_event(MemoryEvent {
185 id: event.id.clone(),
186 kind: Some(event.kind.to_owned()),
187 content: Some(event.payload.clone()),
188 evidence: vec![format!("{}:{}", event.kind, event.id)],
189 ..MemoryEvent::default()
190 })?;
191 }
192 Ok(self.events.len())
193 }
194}
195
196#[derive(Clone, Copy, PartialEq, Eq)]
198enum StepRole {
199 Normal,
201 Parent,
203 Child,
205}
206
207struct PlannedThinkingStep {
209 step: &'static str,
210 detail: String,
211 level: &'static str,
212 source: &'static str,
213 role: StepRole,
214}
215
216impl PlannedThinkingStep {
217 fn normal(
218 step: &'static str,
219 detail: impl Into<String>,
220 level: &'static str,
221 source: &'static str,
222 ) -> Self {
223 Self {
224 step,
225 detail: detail.into(),
226 level,
227 source,
228 role: StepRole::Normal,
229 }
230 }
231
232 fn parent(
233 step: &'static str,
234 detail: impl Into<String>,
235 level: &'static str,
236 source: &'static str,
237 ) -> Self {
238 Self {
239 role: StepRole::Parent,
240 ..Self::normal(step, detail, level, source)
241 }
242 }
243
244 fn child(
245 step: &'static str,
246 detail: impl Into<String>,
247 level: &'static str,
248 source: &'static str,
249 ) -> Self {
250 Self {
251 role: StepRole::Child,
252 ..Self::normal(step, detail, level, source)
253 }
254 }
255}
256
257#[derive(Default)]
261struct CalculationCluster {
262 request: Option<String>,
263 engine: Option<String>,
264 expression: Option<String>,
265 steps: Option<String>,
266 result: Option<String>,
267}
268
269impl CalculationCluster {
270 fn absorb(&mut self, kind: &str, payload: &str) {
271 let value = payload.trim().to_owned();
272 match kind {
273 "calculation" => self.result = Some(value),
274 "calculation:request" => self.request = Some(value),
275 "calculation:engine" => self.engine = Some(value),
276 "calculation:lino" => self.expression = Some(value),
277 "calculation:steps" => self.steps = Some(value),
278 _ => {}
281 }
282 }
283
284 const fn has_data(&self) -> bool {
285 self.result.is_some()
286 || self.request.is_some()
287 || self.engine.is_some()
288 || self.expression.is_some()
289 || self.steps.is_some()
290 }
291
292 fn drain_into(&mut self, plan: &mut Vec<PlannedThinkingStep>) {
293 let parent_detail = self
294 .result
295 .clone()
296 .or_else(|| self.request.clone())
297 .unwrap_or_default();
298 plan.push(PlannedThinkingStep::parent(
299 "compute",
300 parent_detail,
301 "high",
302 "calculation",
303 ));
304 if let Some(engine) = self.engine.take() {
305 plan.push(PlannedThinkingStep::child(
306 "compute_engine",
307 engine,
308 "detailed",
309 "calculation:engine",
310 ));
311 }
312 if let Some(expression) = self.expression.take() {
313 plan.push(PlannedThinkingStep::child(
314 "compute_expression",
315 expression,
316 "detailed",
317 "calculation:lino",
318 ));
319 }
320 if let Some(steps) = self.steps.take() {
321 plan.push(PlannedThinkingStep::child(
322 "compute_steps",
323 steps,
324 "detailed",
325 "calculation:steps",
326 ));
327 }
328 *self = Self::default();
329 }
330}
331
332fn collapse_thinking_whitespace(value: &str) -> String {
335 value.split_whitespace().collect::<Vec<_>>().join(" ")
336}
337
338fn curate_thinking_event(
348 kind: &'static str,
349 payload: &str,
350 final_answer: &str,
351) -> Option<PlannedThinkingStep> {
352 let detail = payload.trim();
353 let keep = |step: &'static str, detail: &str, level: &'static str| {
354 Some(PlannedThinkingStep::normal(
355 step,
356 detail.to_owned(),
357 level,
358 kind,
359 ))
360 };
361 match kind {
362 "impulse" => keep("impulse", detail, "high"),
364 "language" | "language_from" => keep("detect_language", detail, "high"),
365 "language_to" => keep("resolve_response_language", detail, "high"),
366 "intent_formalization:route" => keep("formalize", detail, "high"),
367 "intent" | "legacy_intent" => keep("dispatch_handler", detail, "high"),
368 "program_plan" | "program_parameters" => keep("program_plan", detail, "high"),
369 "response" => {
370 let value = if final_answer.is_empty() {
371 detail
372 } else {
373 final_answer
374 };
375 keep("deformalize", value, "high")
376 }
377
378 "concept_lookup:request" | "procedural_how_to:request" => {
380 keep("scan_memory", detail, "detailed")
381 }
382 "concept_lookup:hit" | "fact_query:relation" | "fact_query:subject" | "fact_lookup:hit" => {
383 keep("lookup_fact", detail, "detailed")
384 }
385 "web_search:request" | "http_fetch:request" | "url_navigate:request" => {
386 keep("http_chat", detail, "detailed")
387 }
388 "tool_call" | "tool_result" => keep("invoke_tool", detail, "detailed"),
389 "coreference" | "program_coreference" => keep("coreference_binding", "", "detailed"),
390 "modifier_detection" | "program_modifiers" => keep("modifier_detection", "", "detailed"),
391 "rule_construction" => keep("rule_construction", "", "detailed"),
392 "validation" => keep("rule_verification", "", "detailed"),
396 _ if kind.starts_with("tool_") => keep("invoke_tool", detail, "detailed"),
397 _ if kind.starts_with("agent_mode") || kind == "action_log" => {
398 keep("agent_plan", detail, "detailed")
399 }
400
401 _ => None,
403 }
404}
405
406#[must_use]
411pub fn build_evidence_links(prompt: &str, log: &EventLog, response_link: &str) -> Vec<String> {
412 let mut links: Vec<String> = Vec::new();
413 links.push(format!("prompt:{}", stable_id("prompt", prompt)));
414 for event in log.events() {
415 let evidence = match event.kind {
416 "trace:execution_failure" => format!("trace:execution_failure:{}", event.id),
417 "language" => format!("language:{}", event.payload),
418 "language_from" => format!("language_from:{}", event.payload),
419 "language_to" => format!("language_to:{}", event.payload),
420 "definition_merge:language" => format!("definition_merge:language:{}", event.payload),
421 "meaning" => format!("meaning:{}", event.payload),
422 "translation_gap" => format!("translation_gap:{}", event.payload),
423 "wikidata" => format!("wikidata:{}", event.payload),
424 "formalization" => format!("formalization:{}", event.id),
425 "formalization:subject_q" => {
426 format!("formalization:subject_q:{}", event.payload)
427 }
428 "formalization:predicate_p" => {
429 format!("formalization:predicate_p:{}", event.payload)
430 }
431 "formalization:object_q" => {
432 format!("formalization:object_q:{}", event.payload)
433 }
434 "formalization:item_q" => format!("formalization:item_q:{}", event.payload),
435 "formalization:property_p" => {
436 format!("formalization:property_p:{}", event.payload)
437 }
438 "formalization:fallback" => {
439 format!("formalization:fallback:{}", event.payload)
440 }
441 "formalization:raw" => format!("formalization:raw:{}", event.payload),
442 "formalization_unresolved" => {
443 format!("formalization_unresolved:{}", event.payload)
444 }
445 "statement_weight" => format!("statement_weight:{}", event.payload),
450 "requirement_contradiction" => {
454 format!("requirement_contradiction:{}", event.id)
455 }
456 "intent_formalization" => format!("intent_formalization:{}", event.id),
457 "intent_formalization_cache" => {
458 format!(
459 "intent_formalization_cache:{}",
460 event.payload.replace(' ', ":")
461 )
462 }
463 "intent_formalization:kind" => {
464 format!("intent_formalization:kind:{}", event.payload)
465 }
466 "intent_formalization:route" => {
467 format!("intent_formalization:route:{}", event.payload)
468 }
469 "intent_formalization:relevant" => {
470 format!("intent_formalization:relevant:{}", event.payload)
471 }
472 "fact_query:request" => format!("fact_query:request:{}", event.id),
476 "fact_query:relation" => format!("fact_query:relation:{}", event.payload),
477 "fact_query:subject" => format!("fact_query:subject:{}", event.payload),
478 "fact_query:cache:hit" => format!("fact_query:cache:hit:{}", event.payload),
479 "fact_query:cache:miss" => String::from("fact_query:cache:miss"),
480 "fact_query:cache:bypass" => String::from("fact_query:cache:bypass"),
481 "fact_query:force_fresh" => String::from("fact_query:force_fresh"),
482 "fact_query:subject_qid" => format!("fact_query:subject_qid:{}", event.payload),
483 "fact_query:value_qid" => format!("fact_query:value_qid:{}", event.payload),
484 "web_search:request" => format!("web_search:request:{}", event.payload),
490 "web_search:query_kind" => format!("web_search:query_kind:{}", event.payload),
491 "web_search:provider" => format!("web_search:provider:{}", event.payload),
492 "web_search:language" => format!("web_search:language:{}", event.payload),
493 "web_search:combined" => format!("web_search:combined:{}", event.payload),
494 "web_search:rank" => format!("web_search:rank:{}", event.payload),
495 "web_search:fused" => format!("web_search:fused:{}", event.payload),
496 "web_search:disabled" => format!("web_search:disabled:{}", event.payload),
497 "document_originality_check:request" => {
498 format!("document_originality_check:request:{}", event.payload)
499 }
500 "document_originality_check:attachment" => {
501 format!("document_originality_check:attachment:{}", event.payload)
502 }
503 "document_originality_check:text_sample" => {
504 format!("document_originality_check:text_sample:{}", event.payload)
505 }
506 "relative_meta_logic:assumed_prior" => {
511 format!("relative_meta_logic:assumed_prior:{}", event.payload)
512 }
513 "relative_meta_logic:trusted_source_tier" => {
514 format!("relative_meta_logic:trusted_source_tier:{}", event.payload)
515 }
516 "relative_meta_logic:ignored_source_tier" => {
517 format!("relative_meta_logic:ignored_source_tier:{}", event.payload)
518 }
519 "statement_verification:statement_count" => {
522 format!("statement_verification:statement_count:{}", event.payload)
523 }
524 "statement_verification:statement" => {
525 format!("statement_verification:statement:{}", event.payload)
526 }
527 "statement_verification:query" => {
528 format!("statement_verification:query:{}", event.payload)
529 }
530 "statement_verification:assessment" => {
531 format!("statement_verification:assessment:{}", event.payload)
532 }
533 "market_price_claim:claim_count" => {
534 format!("market_price_claim:claim_count:{}", event.payload)
535 }
536 "market_price_claim:claim" => {
537 format!("market_price_claim:claim:{}", event.payload)
538 }
539 "market_price_claim:asset" => {
540 format!("market_price_claim:asset:{}", event.payload)
541 }
542 "market_price_claim:period" => {
543 format!("market_price_claim:period:{}", event.payload)
544 }
545 "market_price_claim:claimed_price" => {
546 format!("market_price_claim:claimed_price:{}", event.payload)
547 }
548 "market_price_claim:source" => {
549 format!("market_price_claim:source:{}", event.payload)
550 }
551 "market_price_claim:range" => {
552 format!("market_price_claim:range:{}", event.payload)
553 }
554 "market_price_claim:assessment" => {
555 format!("market_price_claim:assessment:{}", event.payload)
556 }
557 "read_local_file:request" => format!("read_local_file:request:{}", event.payload),
558 "http_fetch:request" => format!("http_fetch:request:{}", event.payload),
559 "docs_method:request" => format!("docs_method:request:{}", event.id),
560 "docs_method:project" => format!("docs_method:project:{}", event.payload),
561 "docs_method:method" => format!("docs_method:method:{}", event.payload),
562 "docs_method:source_kind" => {
563 format!("docs_method:source_kind:{}", event.payload)
564 }
565 "docs_method:source" | "source" => format!("source:{}", event.payload),
566 "project:promoted" => format!("project:promoted:{}", event.payload),
567 "project_lookup:promotion" => {
568 format!("project_lookup:promotion:{}", event.payload)
569 }
570 "project_lookup:repository:github" => {
571 format!("project_lookup:repository:github:{}", event.payload)
572 }
573 "project_lookup:repository:gitlab" => {
574 format!("project_lookup:repository:gitlab:{}", event.payload)
575 }
576 "project_lookup:repository:bitbucket" => {
577 format!("project_lookup:repository:bitbucket:{}", event.payload)
578 }
579 "url_navigate:request" => format!("url_navigate:request:{}", event.payload),
580 "url_preview:iframe" => format!("url_preview:iframe:{}", event.payload),
581 "tool_call" => format!("tool_call:{}", event.payload),
582 "tool_parameter" => format!("tool_parameter:{}", event.payload.replace(' ', ":")),
583 "tool_result" => format!("tool_result:{}", event.payload.replace(' ', ":")),
584 "tool_permission" => {
585 format!("tool_permission:{}", event.payload.replace(' ', ":"))
586 }
587 "text_operation" => format!("text_operation:{}", event.payload),
588 "text_rule" => format!("text_rule:{}", event.payload),
589 "text_rule_chain" => format!("text_rule_chain:{}", event.payload),
590 "text_result" => format!("text_result:{}", event.id),
591 "text_substitution_rules" => format!("text_substitution_rules:{}", event.id),
592 "text_substitution_trace" => format!("text_substitution_trace:{}", event.id),
593 "text_substitution_graph" => format!("text_substitution_graph:{}", event.id),
594 "procedural_how_to:request" => {
595 format!("procedural_how_to:request:{}", event.payload)
596 }
597 "procedural_how_to:action" => {
598 format!("procedural_how_to:action:{}", event.payload)
599 }
600 "procedural_how_to:object" => {
601 format!("procedural_how_to:object:{}", event.payload)
602 }
603 "procedural_how_to:stage" => {
604 format!("procedural_how_to:stage:{}", event.payload)
605 }
606 "procedural_how_to:wikihow_candidate" => {
607 format!("procedural_how_to:wikihow_candidate:{}", event.payload)
608 }
609 "procedural_how_to:source_gate" => {
610 format!("procedural_how_to:source_gate:{}", event.payload)
611 }
612 "spelling_correction" => {
613 format!("spelling_correction:{}", event.payload.replace(' ', ""))
614 }
615 "concept_lookup:request" => format!("concept_lookup:request:{}", event.payload),
616 "concept_lookup:context" => format!("concept_lookup:context:{}", event.payload),
617 "concept_lookup:response-language" => {
618 format!("concept_lookup:response-language:{}", event.payload)
619 }
620 "concept_lookup:hit" => format!("concept_lookup:hit:{}", event.payload),
621 "concept_lookup:miss" => format!("concept_lookup:miss:{}", event.payload),
622 "concept_lookup:context-match" => {
623 format!("concept_lookup:context-match:{}", event.payload)
624 }
625 "concept_lookup:context-mismatch" => {
626 format!("concept_lookup:context-mismatch:{}", event.payload)
627 }
628 "followup:subject" => format!("followup:subject:{}", event.payload),
629 "mechanism_query:request" => {
630 format!("mechanism_query:request:{}", event.payload)
631 }
632 "mechanism_query:stage" => format!("mechanism_query:stage:{}", event.payload),
633 "mechanism_query:source_gate" => {
634 format!("mechanism_query:source_gate:{}", event.payload)
635 }
636 "search:local" => format!("search:local:{}", event.id),
637 "search:external" if event.payload == "skipped:offline" => {
638 String::from("policy:offline")
639 }
640 "search:external" => format!("search:external:{}", event.id),
641 "source:http" => format!("source:http:{}", event.payload.replace(' ', ":")),
642 "source_refresh" => format!("source_refresh:{}", event.payload),
643 "skill_compile:package" => format!("skill_compile:package:{}", event.payload),
644 "compiled_skill:package" => format!("compiled_skill:package:{}", event.id),
645 "compiled_skill:replay" => format!("compiled_skill:replay:{}", event.payload),
646 "conflict:source_disagreement" => {
647 format!("conflict:source_disagreement:{}", event.id)
648 }
649 "cache_hit" => format!("cache_hit:{}", event.payload),
650 "network_fetch" => format!("network_fetch:{}", event.id),
651 "calculation:engine" => format!("calculation:engine:{}", event.payload),
652 "calculation:lino" => format!("calculation:lino:{}", event.payload),
653 "intent" => format!("intent:{}", event.payload),
654 "program_parameter:language" => {
655 format!("program_parameter:language:{}", event.payload)
656 }
657 "program_parameter:task" => format!("program_parameter:task:{}", event.payload),
658 "program_parameters" => {
659 format!(
660 "program_parameters:{}",
661 event.payload.replace([' ', ','], ":")
662 )
663 }
664 "legacy_intent" => format!("legacy_intent:{}", event.payload),
665 "response" => event.payload.clone(),
666 "agent_mode:opted_in" => format!("agent_mode:opted_in:{}", event.id),
667 "agent_mode:active" => format!("agent_mode:active:{}", event.id),
668 "policy:chat_bounded_autonomy" => String::from("policy:chat_bounded_autonomy"),
669 "policy:add_only_history" => String::from("policy:add_only_history"),
670 "policy:destructive_action_requires_confirmation" => {
671 String::from("policy:destructive_action_requires_confirmation")
672 }
673 "policy:agent_time_budget" => format!("policy:agent_time_budget:{}", event.id),
674 "policy:cache_flush_requires_confirmation" => {
675 String::from("policy:cache_flush_requires_confirmation")
676 }
677 "policy:offline" => String::from("policy:offline"),
678 "policy:inappropriate_content" => String::from("policy:inappropriate_content"),
679 "policy:agent_mode_required_for_tools" => {
680 format!("policy:agent_mode_required_for_tools:{}", event.payload)
681 }
682 "policy:package_permission_required" => {
683 format!("policy:package_permission_required:{}", event.payload)
684 }
685 "policy:temperature_selection" => {
686 format!("policy:temperature_selection:{}", event.id)
687 }
688 "policy:guessed_under_ambiguity" => String::from("policy:guessed_under_ambiguity"),
689 "policy:clarify_under_ambiguity" => String::from("policy:clarify_under_ambiguity"),
690 "probability:evidence" => format!("probability:evidence:{}", event.id),
691 "probability:model" => format!("probability:model:{}", event.payload),
692 "probability:ranking" => format!("probability:ranking:{}", event.id),
693 "error" => format!("error:{}", event.id),
694 "filter:user" => format!("filter:user:{}", event.payload),
695 "diagnostic_mode" => format!("diagnostic_mode:{}", event.payload),
696 "execution_status" => format!("execution_status:{}", event.id),
697 "execution_environment" => format!("execution_environment:{}", event.id),
698 _ => format!("{}:{}", event.kind, event.id),
699 };
700 links.push(evidence);
701 }
702 if !links.iter().any(|link| link == response_link) {
703 links.push(response_link.to_owned());
704 }
705 links
706}
707
708fn sanitize_payload(value: &str) -> String {
709 value
710 .replace('\r', "\\r")
711 .replace('\n', "\\n")
712 .replace('\t', "\\t")
713}