Skip to content

Commit d4c86e9

Browse files
committed
feat: add goose-client crate for programmatic goose-server access
1 parent b43302a commit d4c86e9

File tree

24 files changed

+1523
-3
lines changed

24 files changed

+1523
-3
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/goose-client/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[package]
2+
name = "goose-client"
3+
version.workspace = true
4+
edition.workspace = true
5+
authors.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
description = "HTTP client for the goose agent server"
9+
10+
[lints]
11+
workspace = true
12+
13+
[dependencies]
14+
goose = { path = "../goose", default-features = false }
15+
reqwest = { workspace = true, features = ["rustls", "json", "stream", "query"] }
16+
rmcp = { workspace = true }
17+
serde = { workspace = true }
18+
serde_json = { workspace = true }
19+
thiserror = { workspace = true }
20+
tokio = { workspace = true }
21+
tokio-stream = { workspace = true }
22+
futures = { workspace = true }
23+
tracing = { workspace = true }
24+
bytes = { workspace = true }
25+
26+
[dev-dependencies]
27+
wiremock = { workspace = true }
28+
tokio = { workspace = true }
29+
anyhow = { workspace = true }
30+
dotenvy = { workspace = true }
31+
futures = { workspace = true }
32+
33+
[[example]]
34+
name = "smoke_test"
35+
36+
[[example]]
37+
name = "chat_stream"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! Streaming chat: verify SSE streaming against a running goose-server.
2+
//!
3+
//! Requires a configured LLM provider. Start goosed first:
4+
//! GOOSE_SERVER__SECRET_KEY=test cargo run -p goose-server --bin goosed -- agent
5+
//!
6+
//! Then run:
7+
//! cargo run -p goose-client --example chat_stream
8+
9+
use futures::StreamExt;
10+
use goose_client::{GooseClient, GooseClientConfig, MessageEvent, StartAgentRequest};
11+
12+
#[tokio::main]
13+
async fn main() -> anyhow::Result<()> {
14+
let _ = dotenvy::dotenv();
15+
16+
let url =
17+
std::env::var("GOOSE_SERVER_URL").unwrap_or_else(|_| "https://127.0.0.1:3000".to_string());
18+
let secret = std::env::var("GOOSE_SERVER_SECRET_KEY").unwrap_or_else(|_| "test".to_string());
19+
20+
let client = GooseClient::new(GooseClientConfig::new(&url, &secret))?;
21+
println!("Connecting to {url}");
22+
23+
let dir = std::env::temp_dir().join("goose-client-chat-test");
24+
std::fs::create_dir_all(&dir)?;
25+
let session = client
26+
.start_agent(StartAgentRequest::new(dir.to_string_lossy()))
27+
.await?;
28+
println!("Started session {}\n", session.id);
29+
30+
let mut stream = client
31+
.send_message(&session.id, "Say exactly: hello world")
32+
.await?;
33+
34+
let mut got_message = false;
35+
let mut got_finish = false;
36+
37+
while let Some(event) = stream.next().await {
38+
match event? {
39+
MessageEvent::Message { message, .. } => {
40+
got_message = true;
41+
println!("[message] {:?}", message);
42+
}
43+
MessageEvent::Finish { reason, .. } => {
44+
got_finish = true;
45+
println!("[finish] reason={reason}");
46+
}
47+
MessageEvent::Ping => {}
48+
other => println!("[event] {:?}", other),
49+
}
50+
}
51+
52+
assert!(got_message, "expected at least one Message event");
53+
assert!(got_finish, "expected a Finish event");
54+
55+
client.stop_agent(&session.id).await?;
56+
client.delete_session(&session.id).await?;
57+
println!("\nStreaming test passed.");
58+
Ok(())
59+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//! Smoke test: verify goose-client against a running goose-server.
2+
//!
3+
//! Start goosed first:
4+
//! GOOSE_SERVER__SECRET_KEY=test cargo run -p goose-server --bin goosed -- agent
5+
//!
6+
//! Then run:
7+
//! cargo run -p goose-client --example smoke_test
8+
9+
use goose_client::{GooseClient, GooseClientConfig, StartAgentRequest};
10+
11+
#[tokio::main]
12+
async fn main() -> anyhow::Result<()> {
13+
let _ = dotenvy::dotenv();
14+
15+
let url =
16+
std::env::var("GOOSE_SERVER_URL").unwrap_or_else(|_| "https://127.0.0.1:3000".to_string());
17+
let secret = std::env::var("GOOSE_SERVER_SECRET_KEY").unwrap_or_else(|_| "test".to_string());
18+
19+
let client = GooseClient::new(GooseClientConfig::new(&url, &secret))?;
20+
println!("Connecting to {url}");
21+
22+
let status = client.status().await?;
23+
assert_eq!(status, "ok");
24+
println!("[ok] status");
25+
26+
let dir = std::env::temp_dir().join("goose-client-smoke-test");
27+
std::fs::create_dir_all(&dir)?;
28+
let session = client
29+
.start_agent(StartAgentRequest::new(dir.to_string_lossy()))
30+
.await?;
31+
println!("[ok] start_agent -> session {}", session.id);
32+
33+
let list = client.list_sessions().await?;
34+
println!("[ok] list_sessions ({} total)", list.sessions.len());
35+
36+
let fetched = client.get_session(&session.id).await?;
37+
assert_eq!(fetched.id, session.id);
38+
println!("[ok] get_session");
39+
40+
client.rename_session(&session.id, "smoke-test").await?;
41+
println!("[ok] rename_session");
42+
43+
let insights = client.get_session_insights().await?;
44+
println!(
45+
"[ok] get_session_insights -> {} sessions, {} tokens",
46+
insights.total_sessions, insights.total_tokens
47+
);
48+
49+
let info = client.system_info().await?;
50+
println!("[ok] system_info -> {} {}", info.os, info.os_version);
51+
52+
client.stop_agent(&session.id).await?;
53+
println!("[ok] stop_agent");
54+
55+
client.delete_session(&session.id).await?;
56+
println!("[ok] delete_session");
57+
58+
println!("\nAll smoke tests passed.");
59+
Ok(())
60+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::error::Result;
2+
use crate::types::requests::ConfirmToolActionRequest;
3+
use crate::GooseClient;
4+
use goose::permission::permission_confirmation::Permission;
5+
6+
impl GooseClient {
7+
/// Confirm or deny a pending tool action request.
8+
///
9+
/// `id` is the tool request ID received in the SSE stream's `MessageEvent::Message`
10+
/// content as a `ToolRequest`. `action` is typically `Permission::AllowOnce`,
11+
/// `Permission::DenyOnce`, or `Permission::AlwaysAllow`.
12+
pub async fn confirm_tool_action(
13+
&self,
14+
id: impl Into<String>,
15+
action: Permission,
16+
session_id: impl Into<String>,
17+
) -> Result<()> {
18+
self.http
19+
.post_empty(
20+
"/action-required/tool-confirmation",
21+
&ConfirmToolActionRequest::new(id, action, session_id),
22+
)
23+
.await
24+
}
25+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
use crate::error::Result;
2+
use crate::types::requests::{
3+
AddExtensionRequest, ImportAppRequest, ReadResourceRequest, RemoveExtensionRequest,
4+
RestartAgentRequest, ResumeAgentRequest, SetContainerRequest, StartAgentRequest,
5+
StopAgentRequest, UpdateFromSessionRequest, UpdateProviderRequest, UpdateWorkingDirRequest,
6+
};
7+
use crate::types::responses::{
8+
ImportAppResponse, ListAppsResponse, ReadResourceResponse, RestartAgentResponse,
9+
ResumeAgentResponse,
10+
};
11+
use crate::GooseClient;
12+
use goose::agents::ExtensionConfig;
13+
use goose::session::Session;
14+
15+
impl GooseClient {
16+
pub async fn start_agent(&self, request: StartAgentRequest) -> Result<Session> {
17+
self.http.post("/agent/start", &request).await
18+
}
19+
20+
pub async fn resume_agent(
21+
&self,
22+
session_id: impl Into<String>,
23+
load_model_and_extensions: bool,
24+
) -> Result<ResumeAgentResponse> {
25+
self.http
26+
.post(
27+
"/agent/resume",
28+
&ResumeAgentRequest {
29+
session_id: session_id.into(),
30+
load_model_and_extensions,
31+
},
32+
)
33+
.await
34+
}
35+
36+
pub async fn stop_agent(&self, session_id: impl Into<String>) -> Result<()> {
37+
self.http
38+
.post_empty(
39+
"/agent/stop",
40+
&StopAgentRequest {
41+
session_id: session_id.into(),
42+
},
43+
)
44+
.await
45+
}
46+
47+
pub async fn restart_agent(
48+
&self,
49+
session_id: impl Into<String>,
50+
) -> Result<RestartAgentResponse> {
51+
self.http
52+
.post(
53+
"/agent/restart",
54+
&RestartAgentRequest {
55+
session_id: session_id.into(),
56+
},
57+
)
58+
.await
59+
}
60+
61+
pub async fn update_working_dir(
62+
&self,
63+
session_id: impl Into<String>,
64+
working_dir: impl Into<String>,
65+
) -> Result<()> {
66+
self.http
67+
.post_empty(
68+
"/agent/update_working_dir",
69+
&UpdateWorkingDirRequest {
70+
session_id: session_id.into(),
71+
working_dir: working_dir.into(),
72+
},
73+
)
74+
.await
75+
}
76+
77+
pub async fn update_from_session(&self, session_id: impl Into<String>) -> Result<()> {
78+
self.http
79+
.post_empty(
80+
"/agent/update_from_session",
81+
&UpdateFromSessionRequest {
82+
session_id: session_id.into(),
83+
},
84+
)
85+
.await
86+
}
87+
88+
pub async fn update_provider(&self, request: UpdateProviderRequest) -> Result<()> {
89+
self.http
90+
.post_empty("/agent/update_provider", &request)
91+
.await
92+
}
93+
94+
pub async fn add_extension(
95+
&self,
96+
session_id: impl Into<String>,
97+
config: ExtensionConfig,
98+
) -> Result<()> {
99+
self.http
100+
.post_empty(
101+
"/agent/add_extension",
102+
&AddExtensionRequest {
103+
session_id: session_id.into(),
104+
config,
105+
},
106+
)
107+
.await
108+
}
109+
110+
pub async fn remove_extension(
111+
&self,
112+
session_id: impl Into<String>,
113+
name: impl Into<String>,
114+
) -> Result<()> {
115+
self.http
116+
.post_empty(
117+
"/agent/remove_extension",
118+
&RemoveExtensionRequest {
119+
session_id: session_id.into(),
120+
name: name.into(),
121+
},
122+
)
123+
.await
124+
}
125+
126+
pub async fn read_resource(
127+
&self,
128+
request: ReadResourceRequest,
129+
) -> Result<ReadResourceResponse> {
130+
self.http.post("/agent/read_resource", &request).await
131+
}
132+
133+
pub async fn list_apps(&self, session_id: Option<&str>) -> Result<ListAppsResponse> {
134+
match session_id {
135+
Some(id) => {
136+
self.http
137+
.get_with_query("/agent/list_apps", &[("session_id", id)])
138+
.await
139+
}
140+
None => self.http.get("/agent/list_apps").await,
141+
}
142+
}
143+
144+
pub async fn export_app(&self, name: impl AsRef<str>) -> Result<String> {
145+
self.http
146+
.get_text(&format!("/agent/export_app/{}", name.as_ref()))
147+
.await
148+
}
149+
150+
pub async fn import_app(&self, html: impl Into<String>) -> Result<ImportAppResponse> {
151+
self.http
152+
.post("/agent/import_app", &ImportAppRequest { html: html.into() })
153+
.await
154+
}
155+
156+
pub async fn set_container(
157+
&self,
158+
session_id: impl Into<String>,
159+
container_id: Option<String>,
160+
) -> Result<()> {
161+
self.http
162+
.post_empty(
163+
"/agent/set_container",
164+
&SetContainerRequest {
165+
session_id: session_id.into(),
166+
container_id,
167+
},
168+
)
169+
.await
170+
}
171+
}

0 commit comments

Comments
 (0)