main
rs 177 lines 6.19 KB
Raw
1 //! An Agent Client Protocol (ACP) v1 server for Onde agents.
2 //!
3 //! `ed-acp` is the part of an ACP agent that is the same for every product: `initialize` and
4 //! version negotiation, terminal auth, `authenticate` and `logout`, durable sessions (`list`,
5 //! `load` with full replay, `resume`, `close`, `delete`), the model picker and auto-approve
6 //! config options, slash-command advertisement, prompt content (embedded and linked files,
7 //! images, audio), the turn loop against Onde Cloud (or any OpenAI API compatible endpoint)
8 //! with streaming, message ids and usage, tool approval, the workspace tools routed through the
9 //! client's `fs/*` and `terminal/*`, and MCP servers through [`ed_mcp`].
10 //!
11 //! A product supplies a [`Profile`]: its name, prompt, tools, slash commands and built-in MCP
12 //! servers. Everything in the Onde Agent Platform §5.5 conformance table is implemented here
13 //! once.
14 //!
15 //! ```ignore
16 //! let profile = Arc::new(MyProfile);
17 //! ed_acp::serve_stdio(profile, ServeOptions::from_env(&info)).await?;
18 //! ```
19 //!
20 //! To embed the agent in a host process instead (an app sandbox that can't launch a
21 //! subprocess, say), serve it over an in-memory channel and run an ACP client on the other end:
22 //!
23 //! ```ignore
24 //! let (agent_end, client_end) = agent_client_protocol::Channel::duplex();
25 //! tokio::spawn(ed_acp::serve(profile, opts, agent_end));
26 //! agent_client_protocol::Client
27 //! .builder()
28 //! .connect_with(client_end, |conn| async move {
29 //! conn.send_request(InitializeRequest::new(ProtocolVersion::V1)).block_task().await?;
30 //! Ok(())
31 //! })
32 //! .await?;
33 //! ```
34
35 mod agent;
36 pub mod auth;
37 pub mod cli;
38 pub mod content;
39 pub mod llm;
40 mod local;
41 mod server;
42 pub mod store;
43 pub mod tools;
44 mod workspace;
45
46 use std::path::{Path, PathBuf};
47 use std::sync::Arc;
48
49 use agent_client_protocol::schema::v1::{AvailableCommand, PromptCapabilities};
50 use async_trait::async_trait;
51 use ed_mcp::{Connection, McpTool, McpToolset, SharedServer};
52 use serde_json::Value;
53
54 pub use agent_client_protocol;
55 pub use auth::{AuthCtx, AuthProvider};
56 pub use content::AudioHints;
57 pub use ed_mcp;
58 pub use llm::{LlmConfig, LlmEnv, Provider};
59 pub use server::{ServeOptions, serve, serve_stdio};
60 pub use tools::{
61 DescribeCtx, ToolCtx, ToolOutcome, Toolset, absolutize, function_def, normalize, resolve_in,
62 truncate,
63 };
64 pub use workspace::WorkspaceTools;
65
66 /// Who the agent is.
67 #[derive(Debug, Clone, Copy)]
68 pub struct AgentInfo {
69 /// Binary and protocol name, e.g. `splitfire-agent`. Reported as `agentInfo.name` and in
70 /// `clientInfo` to MCP servers.
71 pub name: &'static str,
72 /// Name shown to people, e.g. `SplitFire`.
73 pub display_name: &'static str,
74 pub version: &'static str,
75 /// Where settings and credentials come from.
76 pub env: LlmEnv,
77 }
78
79 /// What the system prompt is built from, every turn.
80 pub struct PromptCtx<'a> {
81 /// `acp` or `tui`; see [`ServeOptions::surface`].
82 pub surface: &'a str,
83 pub cwd: &'a Path,
84 pub roots: &'a [PathBuf],
85 /// The session's MCP tools, so the prompt can mention optional capabilities only when
86 /// they are connected.
87 pub mcp: &'a McpToolset,
88 }
89
90 /// What slash commands depend on.
91 pub struct SessionCtx<'a> {
92 pub mcp: &'a McpToolset,
93 }
94
95 /// What a slash command answered without the model can report.
96 pub struct CommandCtx<'a> {
97 pub mcp: &'a McpToolset,
98 /// The session's model.
99 pub model: &'a str,
100 /// The models offered in the model picker.
101 pub models: &'a [String],
102 }
103
104 /// An MCP call awaiting approval.
105 pub struct McpCall<'a> {
106 pub tool: &'a McpTool,
107 pub args: &'a Value,
108 /// The server, for products that consult it (e.g. to report a download size).
109 pub connection: Option<&'a Connection>,
110 }
111
112 /// The product-specific part of an ACP agent.
113 #[async_trait]
114 pub trait Profile: Send + Sync + 'static {
115 fn info(&self) -> AgentInfo;
116
117 /// The system prompt for one turn.
118 fn system_prompt(&self, ctx: &PromptCtx<'_>) -> String;
119
120 /// The tools the model may call, besides MCP tools. Include [`WorkspaceTools`] to offer
121 /// file and shell access.
122 fn toolsets(&self) -> Vec<Arc<dyn Toolset>>;
123
124 /// Commands advertised with `available_commands_update` after a session is created or
125 /// reopened.
126 fn slash_commands(&self, _ctx: &SessionCtx<'_>) -> Vec<AvailableCommand> {
127 Vec::new()
128 }
129
130 /// When `text` starts with one of this profile's commands, the instruction the model gets
131 /// in its place.
132 fn expand_slash(&self, _text: &str, _ctx: &SessionCtx<'_>) -> Option<String> {
133 None
134 }
135
136 /// When `text` is one of this profile's commands that needs no model (`/models`, `/setup`),
137 /// the reply shown in the thread. It is not added to the conversation.
138 fn answer_slash(&self, _text: &str, _ctx: &CommandCtx<'_>) -> Option<String> {
139 None
140 }
141
142 /// Process-wide MCP servers offered to every session unless the client supplies one of
143 /// the same name.
144 fn builtin_mcp_servers(&self) -> Vec<&'static SharedServer> {
145 Vec::new()
146 }
147
148 /// A sign-in of the product's own, for products whose credentials are not an API key in
149 /// the environment. With one, `initialize` advertises its method, the turn loop reads
150 /// its [`LlmConfig`], and `authenticate` and `logout` delegate to it. `None` keeps the
151 /// terminal `--setup` flow.
152 fn auth(&self) -> Option<Arc<dyn AuthProvider>> {
153 None
154 }
155
156 /// Advertised in `initialize`. Audio is accepted only when this says so.
157 fn prompt_capabilities(&self) -> PromptCapabilities {
158 PromptCapabilities::new().embedded_context(true).image(true)
159 }
160
161 /// How audio links and attachments are described to the model.
162 fn audio_hints(&self) -> AudioHints {
163 AudioHints::default()
164 }
165
166 /// The permission prompt for a mutating MCP tool. `None` uses the default: the tool, its
167 /// server and its arguments.
168 async fn mcp_permission_preview(&self, _call: McpCall<'_>) -> Option<String> {
169 None
170 }
171
172 /// Files an MCP result produced, from its `structuredContent`, shown as tool-call
173 /// locations. Relative paths are dropped.
174 fn mcp_result_locations(&self, _tool: &McpTool, _structured: &Value) -> Vec<PathBuf> {
175 Vec::new()
176 }
177 }