Skip to content

Commit a0f3d1e

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 a0f3d1e

8 files changed

Lines changed: 932 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: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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(ExtMethodName),
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+
Some(Self::Ext(ExtMethodName::new(ext_name).ok()?))
55+
}
56+
}
57+
}
58+
}
59+
60+
#[derive(Debug)]
61+
pub struct ParsedAgentSubject {
62+
pub session_id: Option<AcpSessionId>,
63+
pub method: AgentMethod,
64+
}
65+
66+
pub fn parse_agent_subject(subject: &str) -> Option<ParsedAgentSubject> {
67+
for (agent_byte_pos, _) in subject.match_indices(AGENT_MARKER) {
68+
let suffix = &subject[agent_byte_pos + 1..];
69+
let method = match AgentMethod::from_suffix(suffix) {
70+
Some(m) => m,
71+
None => continue,
72+
};
73+
74+
let session_id = if method.is_session_scoped() {
75+
let before_agent = &subject[..agent_byte_pos];
76+
let session_dot = before_agent.rfind('.')?;
77+
let raw = &before_agent[session_dot + 1..];
78+
Some(AcpSessionId::new(raw).ok()?)
79+
} else {
80+
None
81+
};
82+
83+
return Some(ParsedAgentSubject { session_id, method });
84+
}
85+
86+
None
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
93+
#[test]
94+
fn parse_initialize() {
95+
let parsed = parse_agent_subject("acp.agent.initialize").unwrap();
96+
assert!(parsed.session_id.is_none());
97+
assert_eq!(parsed.method, AgentMethod::Initialize);
98+
}
99+
100+
#[test]
101+
fn parse_authenticate() {
102+
let parsed = parse_agent_subject("acp.agent.authenticate").unwrap();
103+
assert!(parsed.session_id.is_none());
104+
assert_eq!(parsed.method, AgentMethod::Authenticate);
105+
}
106+
107+
#[test]
108+
fn parse_session_new() {
109+
let parsed = parse_agent_subject("acp.agent.session.new").unwrap();
110+
assert!(parsed.session_id.is_none());
111+
assert_eq!(parsed.method, AgentMethod::SessionNew);
112+
}
113+
114+
#[test]
115+
fn parse_session_list() {
116+
let parsed = parse_agent_subject("acp.agent.session.list").unwrap();
117+
assert!(parsed.session_id.is_none());
118+
assert_eq!(parsed.method, AgentMethod::SessionList);
119+
}
120+
121+
#[test]
122+
fn parse_session_load() {
123+
let parsed = parse_agent_subject("acp.s1.agent.session.load").unwrap();
124+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
125+
assert_eq!(parsed.method, AgentMethod::SessionLoad);
126+
}
127+
128+
#[test]
129+
fn parse_session_prompt() {
130+
let parsed = parse_agent_subject("acp.s1.agent.session.prompt").unwrap();
131+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
132+
assert_eq!(parsed.method, AgentMethod::SessionPrompt);
133+
}
134+
135+
#[test]
136+
fn parse_session_cancel() {
137+
let parsed = parse_agent_subject("acp.s1.agent.session.cancel").unwrap();
138+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
139+
assert_eq!(parsed.method, AgentMethod::SessionCancel);
140+
}
141+
142+
#[test]
143+
fn parse_session_set_mode() {
144+
let parsed = parse_agent_subject("acp.s1.agent.session.set_mode").unwrap();
145+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
146+
assert_eq!(parsed.method, AgentMethod::SessionSetMode);
147+
}
148+
149+
#[test]
150+
fn parse_session_set_config_option() {
151+
let parsed = parse_agent_subject("acp.s1.agent.session.set_config_option").unwrap();
152+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
153+
assert_eq!(parsed.method, AgentMethod::SessionSetConfigOption);
154+
}
155+
156+
#[test]
157+
fn parse_session_set_model() {
158+
let parsed = parse_agent_subject("acp.s1.agent.session.set_model").unwrap();
159+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
160+
assert_eq!(parsed.method, AgentMethod::SessionSetModel);
161+
}
162+
163+
#[test]
164+
fn parse_session_fork() {
165+
let parsed = parse_agent_subject("acp.s1.agent.session.fork").unwrap();
166+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
167+
assert_eq!(parsed.method, AgentMethod::SessionFork);
168+
}
169+
170+
#[test]
171+
fn parse_session_resume() {
172+
let parsed = parse_agent_subject("acp.s1.agent.session.resume").unwrap();
173+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
174+
assert_eq!(parsed.method, AgentMethod::SessionResume);
175+
}
176+
177+
#[test]
178+
fn parse_session_close() {
179+
let parsed = parse_agent_subject("acp.s1.agent.session.close").unwrap();
180+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
181+
assert_eq!(parsed.method, AgentMethod::SessionClose);
182+
}
183+
184+
#[test]
185+
fn parse_ext_method() {
186+
let parsed = parse_agent_subject("acp.agent.ext.my_tool").unwrap();
187+
assert!(parsed.session_id.is_none());
188+
assert_eq!(parsed.method, AgentMethod::Ext(ExtMethodName::new("my_tool").unwrap()));
189+
}
190+
191+
#[test]
192+
fn parse_ext_dotted_namespace() {
193+
let parsed = parse_agent_subject("acp.agent.ext.vendor.operation").unwrap();
194+
assert!(parsed.session_id.is_none());
195+
assert_eq!(
196+
parsed.method,
197+
AgentMethod::Ext(ExtMethodName::new("vendor.operation").unwrap())
198+
);
199+
}
200+
201+
#[test]
202+
fn parse_custom_prefix() {
203+
let parsed = parse_agent_subject("myapp.agent.initialize").unwrap();
204+
assert!(parsed.session_id.is_none());
205+
assert_eq!(parsed.method, AgentMethod::Initialize);
206+
}
207+
208+
#[test]
209+
fn parse_multi_part_prefix() {
210+
let parsed = parse_agent_subject("my.multi.prefix.s1.agent.session.load").unwrap();
211+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
212+
assert_eq!(parsed.method, AgentMethod::SessionLoad);
213+
}
214+
215+
#[test]
216+
fn parse_empty_returns_none() {
217+
assert!(parse_agent_subject("").is_none());
218+
}
219+
220+
#[test]
221+
fn parse_no_agent_marker_returns_none() {
222+
assert!(parse_agent_subject("acp.client.session.update").is_none());
223+
}
224+
225+
#[test]
226+
fn parse_unknown_method_returns_none() {
227+
assert!(parse_agent_subject("acp.agent.unknown.method").is_none());
228+
}
229+
230+
#[test]
231+
fn parse_invalid_session_id_returns_none() {
232+
assert!(parse_agent_subject("acp.sess*ion.agent.session.load").is_none());
233+
}
234+
235+
#[test]
236+
fn parse_ext_empty_name_returns_none() {
237+
assert!(parse_agent_subject("acp.agent.ext.").is_none());
238+
}
239+
240+
#[test]
241+
fn parse_ext_wildcard_returns_none() {
242+
assert!(parse_agent_subject("acp.agent.ext.*").is_none());
243+
}
244+
245+
#[test]
246+
fn parse_multi_dot_prefix_global_method_has_no_session() {
247+
let parsed = parse_agent_subject("my.multi.agent.initialize").unwrap();
248+
assert!(parsed.session_id.is_none());
249+
assert_eq!(parsed.method, AgentMethod::Initialize);
250+
}
251+
252+
#[test]
253+
fn parse_prefix_containing_agent_word() {
254+
let parsed = parse_agent_subject("org.agent.app.agent.initialize").unwrap();
255+
assert!(parsed.session_id.is_none());
256+
assert_eq!(parsed.method, AgentMethod::Initialize);
257+
}
258+
259+
#[test]
260+
fn parse_ext_method_containing_agent_segment() {
261+
let parsed = parse_agent_subject("acp.agent.ext.agent.foo").unwrap();
262+
assert!(parsed.session_id.is_none());
263+
assert_eq!(
264+
parsed.method,
265+
AgentMethod::Ext(ExtMethodName::new("agent.foo").unwrap())
266+
);
267+
}
268+
269+
#[test]
270+
fn parse_multi_dot_prefix_session_scoped() {
271+
let parsed = parse_agent_subject("my.multi.s1.agent.session.prompt").unwrap();
272+
assert_eq!(parsed.session_id.unwrap().as_str(), "s1");
273+
assert_eq!(parsed.method, AgentMethod::SessionPrompt);
274+
}
275+
}
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)