Skip to content

Commit 29e2e30

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

File tree

21 files changed

+1196
-2
lines changed

21 files changed

+1196
-2
lines changed

Cargo.lock

Lines changed: 17 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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
serde = { workspace = true }
17+
serde_json = { workspace = true }
18+
thiserror = { workspace = true }
19+
tokio = { workspace = true }
20+
tokio-stream = { workspace = true }
21+
futures = { workspace = true }
22+
tracing = { workspace = true }
23+
bytes = { workspace = true }
24+
25+
[dev-dependencies]
26+
wiremock = { workspace = true }
27+
tokio = { workspace = true }
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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::error::Result;
2+
use crate::types::requests::{
3+
AddExtensionRequest, RemoveExtensionRequest, RestartAgentRequest, ResumeAgentRequest,
4+
StartAgentRequest, StopAgentRequest, UpdateFromSessionRequest, UpdateProviderRequest,
5+
UpdateWorkingDirRequest,
6+
};
7+
use crate::types::responses::{RestartAgentResponse, ResumeAgentResponse};
8+
use crate::GooseClient;
9+
use goose::agents::ExtensionConfig;
10+
use goose::session::Session;
11+
12+
impl GooseClient {
13+
/// Start a new agent session and return the created `Session`.
14+
pub async fn start_agent(&self, request: StartAgentRequest) -> Result<Session> {
15+
self.http.post("/agent/start", &request).await
16+
}
17+
18+
/// Resume an existing session, optionally reloading the model and extensions.
19+
pub async fn resume_agent(
20+
&self,
21+
session_id: impl Into<String>,
22+
load_model_and_extensions: bool,
23+
) -> Result<ResumeAgentResponse> {
24+
self.http
25+
.post(
26+
"/agent/resume",
27+
&ResumeAgentRequest {
28+
session_id: session_id.into(),
29+
load_model_and_extensions,
30+
},
31+
)
32+
.await
33+
}
34+
35+
/// Stop and remove the agent for the given session.
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+
/// Restart the agent for the given session (reloads extensions and provider).
48+
pub async fn restart_agent(
49+
&self,
50+
session_id: impl Into<String>,
51+
) -> Result<RestartAgentResponse> {
52+
self.http
53+
.post(
54+
"/agent/restart",
55+
&RestartAgentRequest {
56+
session_id: session_id.into(),
57+
},
58+
)
59+
.await
60+
}
61+
62+
/// Change the working directory for an existing session. The agent is restarted.
63+
pub async fn update_working_dir(
64+
&self,
65+
session_id: impl Into<String>,
66+
working_dir: impl Into<String>,
67+
) -> Result<()> {
68+
self.http
69+
.post_empty(
70+
"/agent/update_working_dir",
71+
&UpdateWorkingDirRequest {
72+
session_id: session_id.into(),
73+
working_dir: working_dir.into(),
74+
},
75+
)
76+
.await
77+
}
78+
79+
/// Apply the session's recipe (system prompt, extensions) to the running agent.
80+
pub async fn update_from_session(&self, session_id: impl Into<String>) -> Result<()> {
81+
self.http
82+
.post_empty(
83+
"/agent/update_from_session",
84+
&UpdateFromSessionRequest {
85+
session_id: session_id.into(),
86+
},
87+
)
88+
.await
89+
}
90+
91+
/// Change the LLM provider and/or model for an existing session.
92+
pub async fn update_provider(&self, request: UpdateProviderRequest) -> Result<()> {
93+
self.http
94+
.post_empty("/agent/update_provider", &request)
95+
.await
96+
}
97+
98+
/// Add an MCP extension to a running session.
99+
pub async fn add_extension(
100+
&self,
101+
session_id: impl Into<String>,
102+
config: ExtensionConfig,
103+
) -> Result<()> {
104+
self.http
105+
.post_empty(
106+
"/agent/add_extension",
107+
&AddExtensionRequest {
108+
session_id: session_id.into(),
109+
config,
110+
},
111+
)
112+
.await
113+
}
114+
115+
/// Remove an MCP extension from a running session by name.
116+
pub async fn remove_extension(
117+
&self,
118+
session_id: impl Into<String>,
119+
name: impl Into<String>,
120+
) -> Result<()> {
121+
self.http
122+
.post_empty(
123+
"/agent/remove_extension",
124+
&RemoveExtensionRequest {
125+
session_id: session_id.into(),
126+
name: name.into(),
127+
},
128+
)
129+
.await
130+
}
131+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use crate::error::Result;
2+
use crate::streaming::SseStream;
3+
use crate::types::events::MessageEvent;
4+
use crate::types::requests::ChatRequest;
5+
use crate::GooseClient;
6+
use futures::Stream;
7+
use goose::conversation::message::Message;
8+
9+
impl GooseClient {
10+
/// Send a message and receive a streaming reply.
11+
///
12+
/// Returns a `Stream` of `MessageEvent` items. The stream ends with a
13+
/// `MessageEvent::Finish` event. `MessageEvent::Ping` heartbeats are included
14+
/// and may be filtered by the caller if not needed.
15+
pub async fn reply(
16+
&self,
17+
request: ChatRequest,
18+
) -> Result<impl Stream<Item = Result<MessageEvent>>> {
19+
let resp = self.http.post_streaming("/reply", &request).await?;
20+
Ok(SseStream::new(resp.bytes_stream()))
21+
}
22+
23+
/// Convenience wrapper: send a plain text message to an existing session.
24+
pub async fn send_message(
25+
&self,
26+
session_id: impl Into<String>,
27+
text: impl Into<String>,
28+
) -> Result<impl Stream<Item = Result<MessageEvent>>> {
29+
let request = ChatRequest::new(session_id, Message::user().with_text(text.into()));
30+
self.reply(request).await
31+
}
32+
}

crates/goose-client/src/api/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod action_required;
2+
pub mod agent;
3+
pub mod chat;
4+
pub mod sessions;
5+
pub mod status;
6+
pub mod tools;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use crate::error::Result;
2+
use crate::types::requests::{ForkRequest, ImportSessionRequest, UpdateSessionNameRequest};
3+
use crate::types::responses::{ForkResponse, SessionListResponse};
4+
use crate::GooseClient;
5+
use goose::session::Session;
6+
7+
impl GooseClient {
8+
/// List all sessions.
9+
pub async fn list_sessions(&self) -> Result<SessionListResponse> {
10+
self.http.get("/sessions").await
11+
}
12+
13+
/// Get a session by ID including its full conversation history.
14+
pub async fn get_session(&self, session_id: impl AsRef<str>) -> Result<Session> {
15+
self.http
16+
.get(&format!("/sessions/{}", session_id.as_ref()))
17+
.await
18+
}
19+
20+
/// Delete a session permanently.
21+
pub async fn delete_session(&self, session_id: impl AsRef<str>) -> Result<()> {
22+
self.http
23+
.delete(&format!("/sessions/{}", session_id.as_ref()))
24+
.await
25+
}
26+
27+
/// Rename a session.
28+
pub async fn rename_session(
29+
&self,
30+
session_id: impl AsRef<str>,
31+
name: impl Into<String>,
32+
) -> Result<()> {
33+
self.http
34+
.put_empty(
35+
&format!("/sessions/{}/name", session_id.as_ref()),
36+
&UpdateSessionNameRequest { name: name.into() },
37+
)
38+
.await
39+
}
40+
41+
/// Export a session as a JSON string.
42+
pub async fn export_session(&self, session_id: impl AsRef<str>) -> Result<String> {
43+
self.http
44+
.get(&format!("/sessions/{}/export", session_id.as_ref()))
45+
.await
46+
}
47+
48+
/// Import a previously exported session JSON and return the created `Session`.
49+
pub async fn import_session(&self, json: impl Into<String>) -> Result<Session> {
50+
self.http
51+
.post(
52+
"/sessions/import",
53+
&ImportSessionRequest { json: json.into() },
54+
)
55+
.await
56+
}
57+
58+
/// Fork a session. Set `truncate` to trim history at `timestamp`,
59+
/// `copy` to create a full copy instead.
60+
pub async fn fork_session(
61+
&self,
62+
session_id: impl AsRef<str>,
63+
truncate: bool,
64+
copy: bool,
65+
timestamp: Option<i64>,
66+
) -> Result<ForkResponse> {
67+
self.http
68+
.post(
69+
&format!("/sessions/{}/fork", session_id.as_ref()),
70+
&ForkRequest {
71+
timestamp,
72+
truncate,
73+
copy,
74+
},
75+
)
76+
.await
77+
}
78+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use crate::error::Result;
2+
use crate::GooseClient;
3+
4+
impl GooseClient {
5+
/// Check that goose-server is running. Returns `"ok"` on success.
6+
pub async fn status(&self) -> Result<String> {
7+
self.http.get("/status").await
8+
}
9+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::error::Result;
2+
use crate::types::requests::CallToolRequest;
3+
use crate::types::responses::CallToolResponse;
4+
use crate::GooseClient;
5+
use goose::agents::extension::ToolInfo;
6+
7+
impl GooseClient {
8+
/// List all tools available in a session, optionally filtered by extension name.
9+
pub async fn list_tools(
10+
&self,
11+
session_id: impl AsRef<str>,
12+
extension_name: Option<&str>,
13+
) -> Result<Vec<ToolInfo>> {
14+
let sid = session_id.as_ref();
15+
match extension_name {
16+
Some(ext) => {
17+
self.http
18+
.get_with_query(
19+
"/agent/tools",
20+
&[("session_id", sid), ("extension_name", ext)],
21+
)
22+
.await
23+
}
24+
None => {
25+
self.http
26+
.get_with_query("/agent/tools", &[("session_id", sid)])
27+
.await
28+
}
29+
}
30+
}
31+
32+
/// Directly invoke a tool by name within a session.
33+
pub async fn call_tool(
34+
&self,
35+
session_id: impl Into<String>,
36+
name: impl Into<String>,
37+
arguments: serde_json::Value,
38+
) -> Result<CallToolResponse> {
39+
self.http
40+
.post(
41+
"/agent/call_tool",
42+
&CallToolRequest {
43+
session_id: session_id.into(),
44+
name: name.into(),
45+
arguments,
46+
},
47+
)
48+
.await
49+
}
50+
}

0 commit comments

Comments
 (0)