Skip to main content

phi_agent/bridge/
server.rs

1//! Protocol server — adapts [`AgentRuntime`] to the bridge protocol.
2//!
3//! Tool calls use a single-slot pattern: the serve loop pushes a receiver
4//! before each tool call, and ProxyTool pops it.  This handles sequential
5//! tool calls cleanly; parallel calls can be added later via a FIFO queue.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use agent_base::{
11    AgentResult, AgentRuntime, RunOutcome, RuntimeEvent, SessionId, Tool, ToolContext, ToolControlFlow, ToolMetadata,
12    ToolOutput,
13};
14use agent_works::AgentBuilder;
15use async_trait::async_trait;
16use serde_json::Value;
17use tokio::sync::{Mutex, mpsc};
18
19/// Bridge protocol server — wraps an [`AgentRuntime`] and exposes it over
20/// the NDJSON bridge protocol for external SDK consumption.
21///
22/// Manages sessions, tool registration via proxy tools, and event forwarding.
23/// Tool calls use a single-slot pattern: the serve loop pushes a receiver
24/// before each tool call, and ProxyTool pops it.
25#[derive(Clone)]
26pub struct ProtocolServer {
27    runtime: AgentRuntime,
28    /// Single-slot: the next tool call's response receiver.
29    /// serve loop pushes, ProxyTool pops.
30    slot: Arc<Mutex<Option<mpsc::UnboundedReceiver<AgentResult<ToolOutput>>>>>,
31    /// Map external_id → SessionId so that runs with the same
32    /// external_id reuse the same session.
33    sessions: Arc<Mutex<HashMap<String, SessionId>>>,
34}
35
36impl ProtocolServer {
37    /// Wrap an existing [`AgentRuntime`] in a protocol server.
38    pub fn new(runtime: AgentRuntime) -> Self {
39        Self { runtime, slot: Arc::new(Mutex::new(None)), sessions: Arc::new(Mutex::new(HashMap::new())) }
40    }
41
42    /// Build a protocol server from an [`AgentBuilder`].
43    pub fn from_builder(builder: AgentBuilder) -> Result<Self, agent_base::AgentError> {
44        let runtime = builder.build()?;
45        Ok(Self::new(runtime))
46    }
47
48    /// Register a tool implemented on the SDK side.
49    ///
50    /// The tool's `call` will block until the SDK sends a `tool_result`
51    /// message through the bridge.
52    pub async fn register_tool(&self, name: String, description: String, parameters: Value) {
53        let proxy = ProxyTool { name, description, parameters, slot: self.slot.clone() };
54        let tools_arc = self.runtime.tools_mut();
55        let mut tools = tools_arc.write().await;
56        tools.register(proxy);
57    }
58
59    /// Set up the response channel for the NEXT tool call.
60    /// Returns the sender — keep it; send the result when the SDK replies.
61    pub async fn prepare_tool_call(&self) -> mpsc::UnboundedSender<AgentResult<ToolOutput>> {
62        let (tx, rx) = mpsc::unbounded_channel();
63        *self.slot.lock().await = Some(rx);
64        tx
65    }
66
67    /// Create a new session, optionally with an external ID for reuse.
68    pub async fn create_session(&self, external_id: Option<String>) -> (SessionId, Option<String>) {
69        let sid = self.runtime.create_session().await;
70        // NOTE: We intentionally do NOT set sid.external_id because
71        // agent_base's run_turn() hangs when external_id is Some.
72        // Session reuse is handled by get_or_create_session() which
73        // maintains its own external_id → SessionId map.
74        let ext = external_id.clone();
75        (sid, ext)
76    }
77
78    /// Get or create a session by external_id.
79    ///
80    /// If ``external_id`` is ``Some`` and a session with that id already
81    /// exists, it is reused (preserving conversation history).  Otherwise
82    /// a new session is created and registered.
83    pub async fn get_or_create_session(&self, external_id: Option<String>) -> SessionId {
84        if let Some(ref ext) = external_id {
85            let mut sessions = self.sessions.lock().await;
86            if let Some(sid) = sessions.get(ext) {
87                return sid.clone();
88            }
89            // Create new and register
90            let (sid, _) = self.create_session(Some(ext.clone())).await;
91            sessions.insert(ext.clone(), sid.clone());
92            return sid;
93        }
94        // No external_id — always create new
95        self.create_session(None).await.0
96    }
97
98    /// Subscribe to runtime events broadcast by the agent.
99    pub fn subscribe_events(&self) -> tokio::sync::broadcast::Receiver<RuntimeEvent> {
100        self.runtime.subscribe_runtime_events()
101    }
102
103    /// Run a turn on the given session, forwarding events to the callback.
104    pub async fn run_turn<F>(&self, sid: &SessionId, input: &str, f: F) -> AgentResult<RunOutcome>
105    where
106        F: FnMut(RuntimeEvent) -> AgentResult<()> + Send,
107    {
108        self.runtime.run_turn(sid.clone(), input, f).await
109    }
110
111    /// Cancel the currently running turn.
112    pub fn cancel(&self) {
113        self.runtime.cancel();
114    }
115
116    /// List all registered tools with their metadata, sorted by name.
117    pub async fn list_tools(&self) -> Vec<ToolMetadata> {
118        let tools = self.runtime.tools_mut();
119        let registry = tools.read().await;
120        registry.metadatas()
121    }
122}
123
124// ── ProxyTool ─────────────────────────────────────────────────────────
125
126struct ProxyTool {
127    name: String,
128    description: String,
129    parameters: Value,
130    slot: Arc<Mutex<Option<mpsc::UnboundedReceiver<AgentResult<ToolOutput>>>>>,
131}
132
133#[async_trait]
134impl Tool for ProxyTool {
135    fn name(&self) -> &'static str {
136        Box::leak(self.name.clone().into_boxed_str())
137    }
138
139    fn definition(&self) -> Value {
140        serde_json::json!({
141            "type": "function",
142            "function": {
143                "name": self.name,
144                "description": self.description,
145                "parameters": self.parameters,
146            }
147        })
148    }
149
150    async fn call(&self, _args: &Value, _ctx: &ToolContext) -> AgentResult<ToolOutput> {
151        let mut rx = self
152            .slot
153            .lock()
154            .await
155            .take()
156            .ok_or_else(|| agent_base::AgentError::internal("no tool call slot prepared"))?;
157
158        match rx.recv().await {
159            Some(result) => result,
160            None => Ok(ToolOutput {
161                summary: "Tool call cancelled".to_string(),
162                raw: None,
163                control_flow: ToolControlFlow::Break,
164                truncation: None,
165            }),
166        }
167    }
168}