use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "snake_case")]
pub enum ReplCommand {
Help,
Load {
path: String,
},
Compile {
name: String,
},
Run {
graph: String,
input: String,
},
Set {
key: String,
value: String,
},
Get {
key: String,
},
Show {
what: String,
},
Call {
capability: String,
args: serde_json::Value,
},
Quit,
}
impl ReplCommand {
pub fn name(&self) -> &'static str {
match self {
ReplCommand::Help => "help",
ReplCommand::Load { .. } => "load",
ReplCommand::Compile { .. } => "compile",
ReplCommand::Run { .. } => "run",
ReplCommand::Set { .. } => "set",
ReplCommand::Get { .. } => "get",
ReplCommand::Show { .. } => "show",
ReplCommand::Call { .. } => "call",
ReplCommand::Quit => "quit",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum ReplOutcome {
Message(String),
Value(serde_json::Value),
Planned {
action: String,
detail: serde_json::Value,
},
Quit,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilityPolicy {
allowed: HashSet<String>,
}
impl CapabilityPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn allow(&mut self, name: impl Into<String>) -> &mut Self {
self.allowed.insert(name.into());
self
}
pub fn is_allowed(&self, name: &str) -> bool {
self.allowed.contains(name)
}
pub fn from_list<I, S>(names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut policy = Self::new();
for name in names {
policy.allow(name.into());
}
policy
}
pub fn len(&self) -> usize {
self.allowed.len()
}
pub fn is_empty(&self) -> bool {
self.allowed.is_empty()
}
}
pub struct ReplSession {
variables: HashMap<String, serde_json::Value>,
policy: CapabilityPolicy,
pub history: Vec<ReplCommand>,
}
impl ReplSession {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
policy: CapabilityPolicy::new(),
history: Vec::new(),
}
}
pub fn with_policy(mut self, policy: CapabilityPolicy) -> Self {
self.policy = policy;
self
}
pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
self.variables.insert(key.into(), value);
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.variables.get(key)
}
pub fn vars(&self) -> &HashMap<String, serde_json::Value> {
&self.variables
}
pub fn execute(&mut self, cmd: ReplCommand) -> crate::error::Result<ReplOutcome> {
self.history.push(cmd.clone());
match cmd {
ReplCommand::Help => {
let text = concat!(
"Commands:\n",
" help — show this help\n",
" load <path> — load a .rag blueprint\n",
" compile <name> — compile a loaded blueprint\n",
" run <graph> <input> — run a compiled graph\n",
" set <key> <value> — set a session variable\n",
" get <key> — retrieve a session variable\n",
" show <vars|graphs|status> — show session info\n",
" call <capability> <json> — invoke a registered capability\n",
" quit — exit the session",
);
Ok(ReplOutcome::Message(text.to_string()))
}
ReplCommand::Quit => Ok(ReplOutcome::Quit),
ReplCommand::Set { key, value } => {
self.variables.insert(key, serde_json::Value::String(value));
Ok(ReplOutcome::Message("ok".to_string()))
}
ReplCommand::Get { key } => {
let val = self
.variables
.get(&key)
.cloned()
.unwrap_or(serde_json::Value::Null);
Ok(ReplOutcome::Value(val))
}
ReplCommand::Show { what } => match what.as_str() {
"vars" => {
let map = serde_json::to_value(&self.variables)?;
Ok(ReplOutcome::Value(map))
}
"graphs" => Ok(ReplOutcome::Message(
"(graph registry not yet wired in skeleton)".to_string(),
)),
"status" => {
let status = serde_json::json!({
"variables": self.variables.len(),
"history": self.history.len(),
"policy_allowed": self.policy.len(),
});
Ok(ReplOutcome::Value(status))
}
other => Ok(ReplOutcome::Message(format!(
"unknown show subject `{other}`; recognised subjects: vars, graphs, status"
))),
},
ReplCommand::Load { path } => {
self.check_capability("load")?;
Ok(ReplOutcome::Planned {
action: "load".to_string(),
detail: serde_json::json!({ "path": path }),
})
}
ReplCommand::Compile { name } => {
self.check_capability("compile")?;
Ok(ReplOutcome::Planned {
action: "compile".to_string(),
detail: serde_json::json!({ "name": name }),
})
}
ReplCommand::Run { graph, input } => {
self.check_capability("run")?;
Ok(ReplOutcome::Planned {
action: "graph_run".to_string(),
detail: serde_json::json!({ "graph": graph, "input": input }),
})
}
ReplCommand::Call { capability, args } => {
self.check_capability(&capability)?;
Ok(ReplOutcome::Planned {
action: "capability_call".to_string(),
detail: serde_json::json!({ "capability": capability, "args": args }),
})
}
}
}
fn check_capability(&self, name: &str) -> crate::error::Result<()> {
if self.policy.is_allowed(name) {
Ok(())
} else {
Err(crate::error::TinyAgentsError::Capability(format!(
"capability `{name}` is not in the session allowlist"
)))
}
}
}
impl Default for ReplSession {
fn default() -> Self {
Self::new()
}
}