Skip to main content

salamander/agent/
mod.rs

1//! The agent-memory vocabulary — SalamanderDB's labeled beachhead (P1).
2//!
3//! Everything here is a *provided module over the generic engine*: a
4//! concrete payload type ([`EventBody`]), projections that fold it
5//! ([`SessionProjection`], [`KvProjection`]), and the session/fork
6//! operations that only make sense for an agent session. The core engine
7//! ([`crate::Salamander`], [`crate::Event`], [`crate::Projection`]) never
8//! references any of it — "SQLite for event-sourced state, built first for
9//! agent memory", not built *around* it.
10//!
11//! `session_view` and `fork` live here as an `impl Salamander<EventBody>`
12//! block: they read the `SessionStarted` / `ToolCall` vocabulary, so they
13//! are typed as operations on an agent database specifically, not on the
14//! generic engine.
15
16pub mod kv;
17pub mod session;
18
19use serde::{Deserialize, Serialize};
20
21use crate::projection::Projection;
22use crate::{BranchId, BranchInfo, BranchName, Metadata, Result, Salamander};
23
24pub use kv::KvProjection;
25pub use session::{
26    PendingToolCall, SessionProjection, SessionState, SessionStatus, TranscriptEntry,
27};
28
29/// A `Salamander` specialized to the agent vocabulary. Agent users open
30/// this instead of spelling out `Salamander<EventBody>` — "zero friction"
31/// (Phase 1.5 spec, WP-1 step 3).
32pub type AgentDb = Salamander<EventBody>;
33
34/// The speaker of a [`EventBody::ModelTurn`].
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum Role {
37    /// A system prompt or instruction.
38    System,
39    /// A message from the end user.
40    User,
41    /// A message from the model.
42    Assistant,
43    /// Output attributed to a tool.
44    Tool,
45}
46
47/// The built-in agent-memory payload: a key/value vocabulary plus the
48/// typed events of an agent session (turns, tool calls, decisions). This is
49/// one provided payload over the generic engine — you can define your own
50/// instead.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub enum EventBody {
53    /// Set `key` to `value` in the key/value projection.
54    Put {
55        /// The key to set.
56        key: String,
57        /// The value to store.
58        value: Vec<u8>,
59    },
60    /// Remove `key` from the key/value projection.
61    Delete {
62        /// The key to remove.
63        key: String,
64    },
65
66    /// Marks the start of an agent session.
67    SessionStarted {
68        /// Identifier of the agent.
69        agent_id: String,
70        /// Hash of the agent's configuration at session start.
71        config_hash: String,
72    },
73    /// One conversational turn.
74    ModelTurn {
75        /// Who produced the turn.
76        role: Role,
77        /// The turn's text content.
78        content: String,
79        /// The model that produced it.
80        model: String,
81    },
82    /// An invocation of a tool.
83    ToolCall {
84        /// Correlates this call with its [`EventBody::ToolResult`].
85        call_id: String,
86        /// Name of the tool invoked.
87        tool: String,
88        /// JSON-encoded arguments.
89        args_json: String,
90    },
91    /// The result of a [`EventBody::ToolCall`].
92    ToolResult {
93        /// The `call_id` of the originating call.
94        call_id: String,
95        /// Whether the call succeeded.
96        ok: bool,
97        /// The result content.
98        content: String,
99    },
100    /// A recorded decision — the natural fork point in a session.
101    Decision {
102        /// One-line summary of the decision.
103        summary: String,
104        /// Why it was made.
105        rationale: String,
106    },
107    /// Marks the end of a session.
108    SessionEnded {
109        /// Why the session ended.
110        reason: String,
111    },
112}
113
114/// Agent-vocabulary operations. These are inherent methods on the engine
115/// *specialized* to `EventBody`, so they are only in scope for an
116/// `AgentDb` — a `Salamander<MyOwnPayload>` never sees `fork` or
117/// `session_view`, which is exactly the P1 boundary made real in the type
118/// system.
119impl Salamander<EventBody> {
120    /// A `SessionProjection` for `namespace` on the default branch.
121    pub fn session_view(&self, namespace: &str) -> Result<SessionProjection> {
122        self.session_view_on_branch(BranchId::ZERO, namespace)
123    }
124
125    /// A [`SessionProjection`] for `namespace` on a specific branch.
126    pub fn session_view_on_branch(
127        &self,
128        branch: BranchId,
129        namespace: &str,
130    ) -> Result<SessionProjection> {
131        let mut projection = SessionProjection::new(namespace);
132        self.replay_branch(branch, namespace, 0..self.head(), |event| {
133            projection.apply(event);
134        })?;
135        Ok(projection)
136    }
137
138    /// Create an engine-owned branch at `n` while retaining `namespace` as
139    /// the session stream name on both histories.
140    pub fn fork(&mut self, namespace: &str, n: u64) -> Result<BranchInfo> {
141        let mut metadata = Metadata::new();
142        metadata.insert("session_stream".into(), namespace.as_bytes().to_vec());
143        self.fork_branch(
144            BranchId::ZERO,
145            n,
146            BranchName::new(format!("{namespace}-fork-{n}"))?,
147            metadata,
148        )
149    }
150}