Skip to content

Commit f0ced3a

Browse files
committed
feat(acp-nats-agent): add server-side ACP agent framework
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent c8e1839 commit f0ced3a

8 files changed

Lines changed: 883 additions & 1 deletion

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "acp-nats-agent"
3+
version = "0.0.1"
4+
edition = "2024"
5+
6+
[lints]
7+
workspace = true
8+
9+
[dependencies]
10+
acp-nats = { workspace = true }
11+
agent-client-protocol = { workspace = true, features = [
12+
"unstable_auth_methods",
13+
"unstable_boolean_config",
14+
"unstable_cancel_request",
15+
"unstable_message_id",
16+
"unstable_session_close",
17+
"unstable_session_fork",
18+
"unstable_session_model",
19+
"unstable_session_resume",
20+
"unstable_session_usage",
21+
] }
22+
async-nats = { workspace = true }
23+
async-trait = { workspace = true }
24+
bytes = { workspace = true }
25+
futures = { workspace = true }
26+
serde = { workspace = true }
27+
serde_json = { workspace = true }
28+
tokio = { workspace = true, features = ["rt", "macros", "sync"] }
29+
tracing = { workspace = true }
30+
trogon-nats = { workspace = true }
31+
trogon-std = { workspace = true }
32+
33+
[dev-dependencies]
34+
tokio = { workspace = true, features = ["test-util"] }
35+
trogon-nats = { workspace = true, features = ["test-support"] }
36+
trogon-std = { workspace = true, features = ["test-support"] }
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use acp_nats::AcpPrefix;
2+
3+
pub struct AgentServerConfig {
4+
acp_prefix: AcpPrefix,
5+
}
6+
7+
impl AgentServerConfig {
8+
pub fn new(acp_prefix: AcpPrefix) -> Self {
9+
Self { acp_prefix }
10+
}
11+
12+
pub fn acp_prefix(&self) -> &AcpPrefix {
13+
&self.acp_prefix
14+
}
15+
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use acp_nats::AcpSessionId;
2+
use acp_nats::ext_method_name::ExtMethodName;
3+
4+
const AGENT_MARKER: &str = ".agent.";
5+
const EXT_PREFIX: &str = "agent.ext.";
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub enum AgentMethod {
9+
Initialize,
10+
Authenticate,
11+
SessionNew,
12+
SessionList,
13+
SessionLoad,
14+
SessionPrompt,
15+
SessionCancel,
16+
SessionSetMode,
17+
SessionSetConfigOption,
18+
SessionSetModel,
19+
SessionFork,
20+
SessionResume,
21+
SessionClose,
22+
Ext(String),
23+
}
24+
25+
impl AgentMethod {
26+
pub fn is_session_scoped(&self) -> bool {
27+
!matches!(
28+
self,
29+
Self::Initialize
30+
| Self::Authenticate
31+
| Self::SessionNew
32+
| Self::SessionList
33+
| Self::Ext(_)
34+
)
35+
}
36+
37+
fn from_suffix(suffix: &str) -> Option<Self> {
38+
match suffix {
39+
"agent.initialize" => Some(Self::Initialize),
40+
"agent.authenticate" => Some(Self::Authenticate),
41+
"agent.session.new" => Some(Self::SessionNew),
42+
"agent.session.list" => Some(Self::SessionList),
43+
"agent.session.load" => Some(Self::SessionLoad),
44+
"agent.session.prompt" => Some(Self::SessionPrompt),
45+
"agent.session.cancel" => Some(Self::SessionCancel),
46+
"agent.session.set_mode" => Some(Self::SessionSetMode),
47+
"agent.session.set_config_option" => Some(Self::SessionSetConfigOption),
48+
"agent.session.set_model" => Some(Self::SessionSetModel),
49+
"agent.session.fork" => Some(Self::SessionFork),
50+
"agent.session.resume" => Some(Self::SessionResume),
51+
"agent.session.close" => Some(Self::SessionClose),
52+
other => {
53+
let ext_name = other.strip_prefix(EXT_PREFIX)?;
54+
ExtMethodName::new(ext_name).ok()?;
55+
Some(Self::Ext(ext_name.to_string()))
56+
}
57+
}
58+
}
59+
}
60+
61+
#[derive(Debug)]
62+
pub struct ParsedAgentSubject {
63+
pub session_id: Option<AcpSessionId>,
64+
pub method: AgentMethod,
65+
}
66+
67+
pub fn parse_agent_subject(subject: &str) -> Option<ParsedAgentSubject> {
68+
let agent_byte_pos = subject.rmatch_indices(AGENT_MARKER).next()?.0;
69+
70+
let suffix = &subject[agent_byte_pos + 1..];
71+
let method = AgentMethod::from_suffix(suffix)?;
72+
73+
let session_id = if method.is_session_scoped() {
74+
let before_agent = &subject[..agent_byte_pos];
75+
let session_dot = before_agent.rfind('.')?;
76+
let raw = &before_agent[session_dot + 1..];
77+
Some(AcpSessionId::new(raw).ok()?)
78+
} else {
79+
None
80+
};
81+
82+
Some(ParsedAgentSubject { session_id, method })
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
#[test]
90+
fn parse_initialize() {
91+
let parsed = parse_agent_subject("acp.agent.initialize").unwrap();
92+
assert!(parsed.session_id.is_none());
93+
assert_eq!(parsed.method, AgentMethod::Initialize);
94+
}
95+
96+
#[test]
97+
fn parse_authenticate() {
98+
let parsed = parse_agent_subject("acp.agent.authenticate").unwrap();
99+
assert!(parsed.session_id.is_none());
100+
assert_eq!(parsed.method, AgentMethod::Authenticate);
101+
}
102+
103+
#[test]
104+
fn parse_session_new() {
105+
let parsed = parse_agent_subject("acp.agent.session.new").unwrap();
106+
assert!(parsed.session_id.is_none());
107+
assert_eq!(parsed.method, AgentMethod::SessionNew);
108+
}
109+
110+
#[test]
111+
fn parse_session_list() {
112+
let parsed = parse_agent_subject("acp.agent.session.list").unwrap();
113+
assert!(parsed.session_id.is_none());
114+
assert_eq!(parsed.method, AgentMethod::SessionList);
115+
}
116+
117+
#[test]
118+
fn parse_session_load() {
119+
let parsed = parse_agent_subject("acp.s1.agent.session.load").unwrap();
120+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
121+
assert_eq!(parsed.method, AgentMethod::SessionLoad);
122+
}
123+
124+
#[test]
125+
fn parse_session_prompt() {
126+
let parsed = parse_agent_subject("acp.s1.agent.session.prompt").unwrap();
127+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
128+
assert_eq!(parsed.method, AgentMethod::SessionPrompt);
129+
}
130+
131+
#[test]
132+
fn parse_session_cancel() {
133+
let parsed = parse_agent_subject("acp.s1.agent.session.cancel").unwrap();
134+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
135+
assert_eq!(parsed.method, AgentMethod::SessionCancel);
136+
}
137+
138+
#[test]
139+
fn parse_session_set_mode() {
140+
let parsed = parse_agent_subject("acp.s1.agent.session.set_mode").unwrap();
141+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
142+
assert_eq!(parsed.method, AgentMethod::SessionSetMode);
143+
}
144+
145+
#[test]
146+
fn parse_session_set_config_option() {
147+
let parsed = parse_agent_subject("acp.s1.agent.session.set_config_option").unwrap();
148+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
149+
assert_eq!(parsed.method, AgentMethod::SessionSetConfigOption);
150+
}
151+
152+
#[test]
153+
fn parse_session_set_model() {
154+
let parsed = parse_agent_subject("acp.s1.agent.session.set_model").unwrap();
155+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
156+
assert_eq!(parsed.method, AgentMethod::SessionSetModel);
157+
}
158+
159+
#[test]
160+
fn parse_session_fork() {
161+
let parsed = parse_agent_subject("acp.s1.agent.session.fork").unwrap();
162+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
163+
assert_eq!(parsed.method, AgentMethod::SessionFork);
164+
}
165+
166+
#[test]
167+
fn parse_session_resume() {
168+
let parsed = parse_agent_subject("acp.s1.agent.session.resume").unwrap();
169+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
170+
assert_eq!(parsed.method, AgentMethod::SessionResume);
171+
}
172+
173+
#[test]
174+
fn parse_session_close() {
175+
let parsed = parse_agent_subject("acp.s1.agent.session.close").unwrap();
176+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
177+
assert_eq!(parsed.method, AgentMethod::SessionClose);
178+
}
179+
180+
#[test]
181+
fn parse_ext_method() {
182+
let parsed = parse_agent_subject("acp.agent.ext.my_tool").unwrap();
183+
assert!(parsed.session_id.is_none());
184+
assert_eq!(parsed.method, AgentMethod::Ext("my_tool".to_string()));
185+
}
186+
187+
#[test]
188+
fn parse_ext_dotted_namespace() {
189+
let parsed = parse_agent_subject("acp.agent.ext.vendor.operation").unwrap();
190+
assert!(parsed.session_id.is_none());
191+
assert_eq!(
192+
parsed.method,
193+
AgentMethod::Ext("vendor.operation".to_string())
194+
);
195+
}
196+
197+
#[test]
198+
fn parse_custom_prefix() {
199+
let parsed = parse_agent_subject("myapp.agent.initialize").unwrap();
200+
assert!(parsed.session_id.is_none());
201+
assert_eq!(parsed.method, AgentMethod::Initialize);
202+
}
203+
204+
#[test]
205+
fn parse_multi_part_prefix() {
206+
let parsed = parse_agent_subject("my.multi.prefix.s1.agent.session.load").unwrap();
207+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
208+
assert_eq!(parsed.method, AgentMethod::SessionLoad);
209+
}
210+
211+
#[test]
212+
fn parse_empty_returns_none() {
213+
assert!(parse_agent_subject("").is_none());
214+
}
215+
216+
#[test]
217+
fn parse_no_agent_marker_returns_none() {
218+
assert!(parse_agent_subject("acp.client.session.update").is_none());
219+
}
220+
221+
#[test]
222+
fn parse_unknown_method_returns_none() {
223+
assert!(parse_agent_subject("acp.agent.unknown.method").is_none());
224+
}
225+
226+
#[test]
227+
fn parse_invalid_session_id_returns_none() {
228+
assert!(parse_agent_subject("acp.sess*ion.agent.session.load").is_none());
229+
}
230+
231+
#[test]
232+
fn parse_ext_empty_name_returns_none() {
233+
assert!(parse_agent_subject("acp.agent.ext.").is_none());
234+
}
235+
236+
#[test]
237+
fn parse_ext_wildcard_returns_none() {
238+
assert!(parse_agent_subject("acp.agent.ext.*").is_none());
239+
}
240+
241+
#[test]
242+
fn parse_multi_dot_prefix_global_method_has_no_session() {
243+
let parsed = parse_agent_subject("my.multi.agent.initialize").unwrap();
244+
assert!(parsed.session_id.is_none());
245+
assert_eq!(parsed.method, AgentMethod::Initialize);
246+
}
247+
248+
#[test]
249+
fn parse_prefix_containing_agent_word() {
250+
let parsed = parse_agent_subject("org.agent.app.agent.initialize").unwrap();
251+
assert!(parsed.session_id.is_none());
252+
assert_eq!(parsed.method, AgentMethod::Initialize);
253+
}
254+
255+
#[test]
256+
fn parse_multi_dot_prefix_session_scoped() {
257+
let parsed = parse_agent_subject("my.multi.s1.agent.session.prompt").unwrap();
258+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
259+
assert_eq!(parsed.method, AgentMethod::SessionPrompt);
260+
}
261+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod config;
2+
mod dispatch;
3+
mod serve;
4+
5+
pub use config::AgentServerConfig;
6+
pub use serve::AgentServer;

0 commit comments

Comments
 (0)