#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(unreachable_pub)]
use std::time::Instant;
use behest_core::id::RunId;
use behest_core::message::Message;
use behest_core::run::RunState;
use behest_core::tool_types::ToolCall;
use serde_json::Value;
use tokio::sync::watch;
mod factory;
mod impls;
pub use factory::{
ContextAdapter, ContextFactory, ContextInput, ContextOutput, ContextResult, FunctionAdapter,
StaticAdapter,
};
pub use impls::{
AppContext, HookContextImpl, MemoryContextImpl, RunContextImpl, SessionContextImpl,
ToolContextImpl,
};
pub trait ReadonlyContext {
fn invocation_id(&self) -> &str;
fn session_id(&self) -> &str;
fn user_id(&self) -> &str;
fn app_name(&self) -> &str;
}
pub trait SessionContext: ReadonlyContext {
fn session_state(&self) -> &SessionState;
fn session_state_mut(&mut self) -> &mut SessionState;
}
#[derive(Debug, Clone, Default)]
pub struct SessionState {
entries: std::collections::HashMap<String, Value>,
}
impl SessionState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, value: Value) {
self.entries.insert(key.into(), value);
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.entries.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.entries.remove(key)
}
#[must_use]
pub fn all(&self) -> &std::collections::HashMap<String, Value> {
&self.entries
}
}
pub trait RunContext: SessionContext {
fn run_id(&self) -> &RunId;
fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken;
fn deadline(&self) -> Option<Instant>;
fn event_sink(&self) -> &EventSink;
fn budget(&self) -> &RunBudget;
}
#[derive(Clone)]
pub struct EventSink {
tx: watch::Sender<Option<Value>>,
}
impl std::fmt::Debug for EventSink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventSink")
.field("receiver_count", &self.tx.receiver_count())
.finish()
}
}
impl EventSink {
#[must_use]
pub fn new() -> Self {
let (tx, _) = watch::channel(None);
Self { tx }
}
pub fn emit(&self, event: Value) -> usize {
self.tx.send_if_modified(|current| {
*current = Some(event);
true
});
self.tx.receiver_count()
}
#[must_use]
pub fn subscribe(&self) -> watch::Receiver<Option<Value>> {
self.tx.subscribe()
}
}
impl Default for EventSink {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RunBudget {
max_tokens: Option<usize>,
used_tokens: usize,
}
impl RunBudget {
#[must_use]
pub fn new(max_tokens: Option<usize>) -> Self {
Self {
max_tokens,
used_tokens: 0,
}
}
pub fn consume(&mut self, tokens: usize) {
self.used_tokens += tokens;
}
#[must_use]
pub fn remaining(&self) -> Option<usize> {
self.max_tokens
.map(|max| max.saturating_sub(self.used_tokens))
}
#[must_use]
pub fn used(&self) -> usize {
self.used_tokens
}
}
pub trait ToolContext: RunContext {
fn tool_call(&self) -> &ToolCall;
fn emit_progress(&self, status: &str, data: Value) {
let progress = serde_json::json!({
"type": "tool_progress",
"call_id": self.tool_call().id,
"tool_name": self.tool_call().name,
"status": status,
"data": data,
});
self.event_sink().emit(progress);
}
fn search_memory(&self, _query: &str, _limit: usize) -> Vec<MemoryEntry> {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct MemoryEntry {
pub content: String,
pub score: f32,
pub source: Option<String>,
}
pub trait MemoryContext: SessionContext {
fn active_window(&self) -> &[Message];
fn demote(&self, messages: Vec<Message>) -> Result<(), String>;
fn compact(&self, messages: Vec<Message>) -> Result<String, String>;
}
pub trait HookContext: ReadonlyContext {
fn current_state(&self) -> &RunState;
fn snapshot(&self) -> RunSnapshot;
}
#[derive(Debug, Clone)]
pub struct RunSnapshot {
pub state: RunState,
pub run_id: RunId,
pub session_id: String,
pub iteration: usize,
pub tokens_used: usize,
}