139 lines
4.1 KiB
Rust
139 lines
4.1 KiB
Rust
use crate::session::{TurnBlock, TurnSnapshot};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ReasoningVisibility {
|
|
Hidden,
|
|
Collapsed,
|
|
Expanded,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ToolVisibility {
|
|
Hidden,
|
|
Compact,
|
|
Detailed,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct PresentationPolicy {
|
|
pub live: bool,
|
|
pub reasoning: ReasoningVisibility,
|
|
pub tools: ToolVisibility,
|
|
}
|
|
|
|
impl PresentationPolicy {
|
|
pub const fn interactive() -> Self {
|
|
Self {
|
|
live: true,
|
|
reasoning: ReasoningVisibility::Collapsed,
|
|
tools: ToolVisibility::Detailed,
|
|
}
|
|
}
|
|
|
|
pub const fn external(live: bool) -> Self {
|
|
Self {
|
|
live,
|
|
reasoning: ReasoningVisibility::Hidden,
|
|
tools: ToolVisibility::Compact,
|
|
}
|
|
}
|
|
|
|
pub const fn unattended() -> Self {
|
|
Self {
|
|
live: false,
|
|
reasoning: ReasoningVisibility::Hidden,
|
|
tools: ToolVisibility::Hidden,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Produce the immutable view that is allowed to leave the Gateway core.
|
|
///
|
|
/// Presentation filtering deliberately clones the snapshot. Conversation
|
|
/// history and the authoritative TurnController state remain untouched.
|
|
pub fn project_snapshot(snapshot: &TurnSnapshot, policy: PresentationPolicy) -> TurnSnapshot {
|
|
let mut projected = snapshot.clone();
|
|
projected.blocks.retain_mut(|block| match block {
|
|
TurnBlock::Reasoning { .. } => policy.reasoning != ReasoningVisibility::Hidden,
|
|
TurnBlock::Assistant { .. } => true,
|
|
TurnBlock::Tool {
|
|
arguments, preview, ..
|
|
} => match policy.tools {
|
|
ToolVisibility::Hidden => false,
|
|
ToolVisibility::Compact => {
|
|
*arguments = serde_json::Value::Null;
|
|
*preview = None;
|
|
true
|
|
}
|
|
ToolVisibility::Detailed => true,
|
|
},
|
|
});
|
|
projected
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::session::{BlockId, ToolStatus, TurnId, TurnPhase, TurnState, TurnStatus};
|
|
|
|
fn snapshot() -> TurnSnapshot {
|
|
TurnState {
|
|
id: TurnId("turn".into()),
|
|
session_id: "session".into(),
|
|
message_id: "message".into(),
|
|
revision: 3,
|
|
status: TurnStatus::Running,
|
|
phase: TurnPhase::Acting,
|
|
blocks: vec![
|
|
TurnBlock::Reasoning {
|
|
id: BlockId("reasoning".into()),
|
|
iteration: 0,
|
|
text: "private chain".into(),
|
|
},
|
|
TurnBlock::Assistant {
|
|
id: BlockId("text".into()),
|
|
iteration: 0,
|
|
text: "visible".into(),
|
|
},
|
|
TurnBlock::Tool {
|
|
id: "tool".into(),
|
|
iteration: 0,
|
|
name: "bash".into(),
|
|
arguments: serde_json::json!({"token": "secret"}),
|
|
status: ToolStatus::Completed,
|
|
preview: Some("sensitive output".into()),
|
|
},
|
|
],
|
|
usage: None,
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn external_projection_removes_reasoning_and_tool_details_without_mutating_source() {
|
|
let source = snapshot();
|
|
let projected = project_snapshot(&source, PresentationPolicy::external(true));
|
|
|
|
assert_eq!(projected.blocks.len(), 2);
|
|
assert!(matches!(projected.blocks[0], TurnBlock::Assistant { .. }));
|
|
assert!(matches!(
|
|
&projected.blocks[1],
|
|
TurnBlock::Tool {
|
|
arguments: serde_json::Value::Null,
|
|
preview: None,
|
|
..
|
|
}
|
|
));
|
|
assert_eq!(source.blocks.len(), 3);
|
|
assert!(matches!(source.blocks[0], TurnBlock::Reasoning { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn unattended_projection_keeps_only_assistant_blocks() {
|
|
let projected = project_snapshot(&snapshot(), PresentationPolicy::unattended());
|
|
|
|
assert_eq!(projected.blocks.len(), 1);
|
|
assert!(matches!(projected.blocks[0], TurnBlock::Assistant { .. }));
|
|
}
|
|
}
|