salamander/agent/
session.rs1use std::collections::HashMap;
10
11use super::{EventBody, Role};
12use crate::event::Event;
13use crate::projection::{NamespaceScoped, Projection};
14
15#[derive(Debug, Clone)]
18pub enum TranscriptEntry {
19 ModelTurn {
21 role: Role,
23 content: String,
25 model: String,
27 },
28 ToolCall {
30 call_id: String,
32 tool: String,
34 args_json: String,
36 },
37 ToolResult {
39 call_id: String,
41 ok: bool,
43 content: String,
45 },
46 Decision {
48 summary: String,
50 rationale: String,
52 },
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum SessionStatus {
58 NotStarted,
60 Active,
62 Ended {
64 reason: String,
66 },
67}
68
69#[derive(Debug, Clone)]
72pub struct PendingToolCall {
73 pub tool: String,
75 pub args_json: String,
77}
78
79#[derive(Debug)]
82pub struct SessionState {
83 pub agent_id: Option<String>,
87 pub transcript: Vec<TranscriptEntry>,
89 pub pending: HashMap<String, PendingToolCall>,
91 pub status: SessionStatus,
93}
94
95impl Default for SessionState {
96 fn default() -> Self {
97 Self {
98 agent_id: None,
99 transcript: Vec::new(),
100 pending: HashMap::new(),
101 status: SessionStatus::NotStarted,
102 }
103 }
104}
105
106pub struct SessionProjection {
110 namespace: String,
111 state: SessionState,
112 cursor: u64,
113}
114
115impl SessionProjection {
116 pub fn new(namespace: impl Into<String>) -> Self {
118 Self {
119 namespace: namespace.into(),
120 state: SessionState::default(),
121 cursor: 0,
122 }
123 }
124}
125
126impl Projection for SessionProjection {
127 type Body = EventBody;
128 type State = SessionState;
129
130 fn apply(&mut self, event: &Event<EventBody>) {
131 if event.namespace != self.namespace {
132 self.cursor = event.offset + 1;
133 return;
134 }
135
136 match &event.body {
137 EventBody::SessionStarted { agent_id, .. } => {
138 self.state.status = SessionStatus::Active;
139 self.state.agent_id = Some(agent_id.clone());
140 }
141 EventBody::ModelTurn {
142 role,
143 content,
144 model,
145 } => {
146 self.state.transcript.push(TranscriptEntry::ModelTurn {
147 role: *role,
148 content: content.clone(),
149 model: model.clone(),
150 });
151 }
152 EventBody::ToolCall {
153 call_id,
154 tool,
155 args_json,
156 } => {
157 self.state.pending.insert(
158 call_id.clone(),
159 PendingToolCall {
160 tool: tool.clone(),
161 args_json: args_json.clone(),
162 },
163 );
164 self.state.transcript.push(TranscriptEntry::ToolCall {
165 call_id: call_id.clone(),
166 tool: tool.clone(),
167 args_json: args_json.clone(),
168 });
169 }
170 EventBody::ToolResult {
171 call_id,
172 ok,
173 content,
174 } => {
175 self.state.pending.remove(call_id);
176 self.state.transcript.push(TranscriptEntry::ToolResult {
177 call_id: call_id.clone(),
178 ok: *ok,
179 content: content.clone(),
180 });
181 }
182 EventBody::Decision { summary, rationale } => {
183 self.state.transcript.push(TranscriptEntry::Decision {
184 summary: summary.clone(),
185 rationale: rationale.clone(),
186 });
187 }
188 EventBody::SessionEnded { reason } => {
189 self.state.status = SessionStatus::Ended {
190 reason: reason.clone(),
191 };
192 }
193 EventBody::Put { .. } | EventBody::Delete { .. } => {}
194 }
195
196 self.cursor = event.offset + 1;
197 }
198
199 fn cursor(&self) -> u64 {
200 self.cursor
201 }
202
203 fn state(&self) -> &Self::State {
204 &self.state
205 }
206}
207
208impl NamespaceScoped for SessionProjection {
209 fn new_for(namespace: &str) -> Self {
210 SessionProjection::new(namespace)
211 }
212}