Skip to content

Commit 199883c

Browse files
committed
feat: logging for the goose-acp server + --server fix
1 parent 2d98f01 commit 199883c

9 files changed

Lines changed: 103 additions & 24 deletions

File tree

Cargo.lock

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

crates/goose-acp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ url = { workspace = true }
4141
# HTTP server dependencies
4242
axum = { workspace = true, features = ["ws"] }
4343
serde = { workspace = true, features = ["derive"] }
44-
tower-http = { workspace = true, features = ["cors"] }
44+
tower-http = { workspace = true, features = ["cors", "trace"] }
4545
async-stream = { workspace = true }
4646
http-body-util = "0.1.3"
4747
uuid = { workspace = true, features = ["v7"] }

crates/goose-acp/src/transport.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use axum::{
1616
};
1717
use serde_json::Value;
1818
use tower_http::cors::{Any, CorsLayer};
19+
use tower_http::trace::TraceLayer;
1920

2021
use crate::server_factory::AcpServer;
2122

@@ -145,5 +146,6 @@ pub fn create_router(server: Arc<AcpServer>) -> Router {
145146
get(handle_get).with_state((http_state.clone(), ws_state)),
146147
)
147148
.route("/acp", delete(http::handle_delete).with_state(http_state))
149+
.layer(TraceLayer::new_for_http())
148150
.layer(cors)
149151
}

crates/goose-acp/src/transport/http.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,21 @@ async fn run_router(
216216
router: Arc<MessageRouter>,
217217
) {
218218
while let Some(msg) = from_agent_rx.recv().await {
219+
if let Ok(parsed) = serde_json::from_str::<Value>(&msg) {
220+
let method = parsed
221+
.get("method")
222+
.and_then(|m| m.as_str())
223+
.unwrap_or("<response>");
224+
let id = parsed.get("id").map(|id| id.to_string());
225+
if is_jsonrpc_response_or_error(&parsed) {
226+
info!(
227+
id = id.as_deref().unwrap_or(""),
228+
"ACP response sent to client"
229+
);
230+
} else {
231+
debug!(method = method, "ACP server notification: {}", method);
232+
}
233+
}
219234
router.route(&msg).await;
220235
}
221236
debug!("Router task exiting — agent channel closed");
@@ -425,8 +440,18 @@ pub(crate) async fn handle_post(
425440
.into_response();
426441
}
427442

443+
let method = json_message
444+
.get("method")
445+
.and_then(|m| m.as_str())
446+
.unwrap_or("<response>");
447+
let rpc_id = json_message
448+
.get("id")
449+
.map(|id| id.to_string())
450+
.unwrap_or_default();
451+
428452
// Initialize — no Acp-Connection-Id required.
429453
if is_initialize_request(&json_message) {
454+
info!(method = method, id = %rpc_id, "ACP request: initialize (new connection)");
430455
return handle_initialize(state, &json_message).await;
431456
}
432457

@@ -444,9 +469,19 @@ pub(crate) async fn handle_post(
444469
}
445470

446471
if is_jsonrpc_request(&json_message) {
472+
info!(
473+
method = method,
474+
id = %rpc_id,
475+
connection_id = %conn_id,
476+
"ACP request: {}", method
477+
);
447478
handle_request(state, &conn_id, &json_message).await
448479
} else {
449-
// Notification or client response — fire and forget.
480+
info!(
481+
method = method,
482+
connection_id = %conn_id,
483+
"ACP notification/response: {}", method
484+
);
450485
handle_notification_or_response(state, &conn_id, &json_message).await
451486
}
452487
}

crates/goose-acp/src/transport/websocket.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use axum::{
44
response::{IntoResponse, Response},
55
};
66
use futures::{SinkExt, StreamExt};
7+
use serde_json::Value;
78
use std::{collections::HashMap, sync::Arc};
89
use tokio::sync::{mpsc, RwLock};
910
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
@@ -134,7 +135,10 @@ pub(crate) async fn handle_ws(socket: WebSocket, state: Arc<WsState>, connection
134135
match msg_result {
135136
Ok(Message::Text(text)) => {
136137
let text_str = text.to_string();
137-
debug!(connection_id = %connection_id, "Client → Agent: {} bytes", text_str.len());
138+
if let Ok(parsed) = serde_json::from_str::<Value>(&text_str) {
139+
let method = parsed.get("method").and_then(|m| m.as_str()).unwrap_or("<response>");
140+
info!(connection_id = %connection_id, method = method, "WS client → agent: {}", method);
141+
}
138142
if let Err(e) = to_agent.send(text_str).await {
139143
error!(connection_id = %connection_id, "Failed to send to agent: {}", e);
140144
break;
@@ -157,7 +161,15 @@ pub(crate) async fn handle_ws(socket: WebSocket, state: Arc<WsState>, connection
157161
}
158162

159163
Some(text) = from_agent_rx.recv() => {
160-
debug!(connection_id = %connection_id, "Agent → Client: {} bytes", text.len());
164+
if let Ok(parsed) = serde_json::from_str::<Value>(&text) {
165+
let method = parsed.get("method").and_then(|m| m.as_str());
166+
let id = parsed.get("id").map(|id| id.to_string());
167+
if let Some(m) = method {
168+
info!(connection_id = %connection_id, method = m, "WS agent → client: {}", m);
169+
} else if id.is_some() {
170+
info!(connection_id = %connection_id, id = id.as_deref().unwrap_or(""), "WS agent → client: response");
171+
}
172+
}
161173
if let Err(e) = ws_tx.send(Message::Text(text.into())).await {
162174
error!(connection_id = %connection_id, "Failed to send to client: {}", e);
163175
break;

crates/goose-cli/src/logging.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,20 @@ fn default_env_filter() -> EnvFilter {
2828
/// - No console output (all logs go to files only)
2929
/// - Optional Langfuse integration (DEBUG level)
3030
pub fn setup_logging(name: Option<&str>) -> Result<()> {
31-
setup_logging_internal(name, false)
31+
setup_logging_with_options(name, false, false)
3232
}
3333

34-
/// Internal function that allows bypassing the Once check for testing
35-
fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
34+
/// Sets up logging with console output on stderr.
35+
/// Used by the `serve` command so operators can see requests arriving.
36+
pub fn setup_logging_with_console(name: Option<&str>) -> Result<()> {
37+
setup_logging_with_options(name, false, true)
38+
}
39+
40+
fn setup_logging_with_options(name: Option<&str>, force: bool, console: bool) -> Result<()> {
41+
setup_logging_internal(name, force, console)
42+
}
43+
44+
fn setup_logging_internal(name: Option<&str>, force: bool, console: bool) -> Result<()> {
3645
let mut result = Ok(());
3746

3847
let mut setup = || {
@@ -63,10 +72,23 @@ fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
6372
EnvFilter::try_from_default_env().unwrap_or_else(|_| default_env_filter());
6473

6574
// Start building the subscriber
66-
let mut layers = vec![
67-
file_layer.with_filter(env_filter).boxed(),
68-
// Console logging disabled for CLI - all logs go to files only
69-
];
75+
let mut layers = vec![file_layer.with_filter(env_filter).boxed()];
76+
77+
if console {
78+
let console_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
79+
EnvFilter::new("")
80+
.add_directive("goose_acp=info".parse().unwrap())
81+
.add_directive("goose=info".parse().unwrap())
82+
.add_directive("tower_http=info".parse().unwrap())
83+
.add_directive(LevelFilter::WARN.into())
84+
});
85+
let console_layer = fmt::layer()
86+
.with_target(true)
87+
.with_level(true)
88+
.with_writer(std::io::stderr)
89+
.with_ansi(true);
90+
layers.push(console_layer.with_filter(console_filter).boxed());
91+
}
7092

7193
#[cfg(feature = "otel")]
7294
if !force {

crates/goose-cli/src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ use goose_cli::cli::cli;
33

44
#[tokio::main]
55
async fn main() -> Result<()> {
6-
if let Err(e) = goose_cli::logging::setup_logging(None) {
6+
let is_serve = std::env::args().any(|a| a == "serve");
7+
let logging_result = if is_serve {
8+
goose_cli::logging::setup_logging_with_console(Some("serve"))
9+
} else {
10+
goose_cli::logging::setup_logging(None)
11+
};
12+
if let Err(e) = logging_result {
713
eprintln!("Warning: Failed to initialize logging: {}", e);
814
}
915

crates/goose/src/agents/agent.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,6 @@ impl Agent {
12301230
.unwrap_or(DEFAULT_MAX_TURNS)
12311231
});
12321232
let mut compaction_attempts = 0;
1233-
let mut last_assistant_text = String::new();
12341233

12351234
loop {
12361235
if is_token_cancelled(&cancel_token) {
@@ -1337,10 +1336,6 @@ impl Agent {
13371336

13381337
let num_tool_requests = frontend_requests.len() + remaining_requests.len();
13391338
if num_tool_requests == 0 {
1340-
let text = filtered_response.as_concat_text();
1341-
if !text.is_empty() {
1342-
last_assistant_text = text;
1343-
}
13441339
messages_to_add.push(response);
13451340
continue;
13461341
}
@@ -1799,9 +1794,7 @@ impl Agent {
17991794
tokio::task::yield_now().await;
18001795
}
18011796

1802-
if !last_assistant_text.is_empty() {
1803-
tracing::info!(target: "goose::agents::agent", trace_output = last_assistant_text.as_str());
1804-
}
1797+
18051798
}.instrument(reply_stream_span));
18061799
Ok(inner)
18071800
}

ui/text/scripts/dev-start.mjs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22

3-
// Development entrypoint: ensures a goose binary is available, then launches
4-
// the TUI via tsx. Skips the cargo build if GOOSE_BINARY is already set.
3+
// Development entrypoint: ensures a goose binary is available, then launches the TUI
4+
// Skips the cargo build if GOOSE_BINARY is already set or if --server is provided
55

66
import { execFileSync } from "node:child_process";
77
import { existsSync } from "node:fs";
@@ -10,8 +10,16 @@ import { fileURLToPath } from "node:url";
1010

1111
const __dirname = dirname(fileURLToPath(import.meta.url));
1212
const repoRoot = join(__dirname, "..", "..", "..");
13-
14-
if (!process.env.GOOSE_BINARY) {
13+
const args = process.argv.slice(2);
14+
const hasServerFlag = args.some(
15+
(arg) =>
16+
arg === "--server" ||
17+
arg === "-s" ||
18+
arg.startsWith("--server=") ||
19+
arg.startsWith("-s="),
20+
);
21+
22+
if (!hasServerFlag && !process.env.GOOSE_BINARY) {
1523
const binName = process.platform === "win32" ? "goose.exe" : "goose";
1624
const binaryPath = join(repoRoot, "target", "debug", binName);
1725

0 commit comments

Comments
 (0)