Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/agent/examples/read_only_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ fn print_event(label: &str, event: &AgentEvent) {
AgentEvent::TurnCompleted { status, .. } => {
eprintln!("probe: {label} TurnCompleted status={status:?}")
}
AgentEvent::Warning(message) => eprintln!("probe: {label} Warning {message:?}"),
AgentEvent::Warning { message } => eprintln!("probe: {label} Warning {message:?}"),
AgentEvent::Error { fatal, message } => {
eprintln!("probe: {label} Error fatal={fatal} {message:?}")
}
Expand Down
2 changes: 1 addition & 1 deletion crates/agent/examples/steer_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ fn main() {
assistant.push('\n');
}
}
AgentEvent::Warning(message) => {
AgentEvent::Warning { message } => {
eprintln!("steer_probe: WARNING: {message}");
}
AgentEvent::Error { message, .. } => {
Expand Down
119 changes: 62 additions & 57 deletions crates/agent/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,10 +755,10 @@ async fn handshake(
agent.id
);
let _ = events
.send(AgentEvent::Warning(format!(
.send(AgentEvent::Warning { message: format!(
"{} does not support HTTP MCP servers; tcode MCP tools are unavailable in this session",
agent.name
)))
) })
.await;
}

Expand All @@ -770,46 +770,47 @@ async fn handshake(
.map(str::to_string)
.filter(|_| caps.load_session);

let (session_id, mut modes, config_options) = match resumed {
Some(session_id) => {
// `session/load` replays the whole conversation as `session/update`
// notifications. Our JSONL log is the authoritative history and the
// UI has already folded it, so the replay is swallowed (see
// `State::replaying`); we only want the session live again.
state.lock().unwrap().replaying = true;
let session_id = acp::SessionId::new(session_id);
let loaded = connection
.send_request(
acp::LoadSessionRequest::new(session_id.clone(), opts.cwd.clone())
.mcp_servers(mcp_servers.clone()),
)
.block_task()
.await;
state.lock().unwrap().replaying = false;
match loaded {
Ok(loaded) => (session_id, loaded.modes, loaded.config_options),
Err(err) => {
log::warn!(
"acp[{}]: session/load failed ({}); starting a fresh session",
agent.id,
describe(&err)
);
let _ = events
.send(AgentEvent::Warning(format!(
let (session_id, mut modes, config_options) =
match resumed {
Some(session_id) => {
// `session/load` replays the whole conversation as `session/update`
// notifications. Our JSONL log is the authoritative history and the
// UI has already folded it, so the replay is swallowed (see
// `State::replaying`); we only want the session live again.
state.lock().unwrap().replaying = true;
let session_id = acp::SessionId::new(session_id);
let loaded = connection
.send_request(
acp::LoadSessionRequest::new(session_id.clone(), opts.cwd.clone())
.mcp_servers(mcp_servers.clone()),
)
.block_task()
.await;
state.lock().unwrap().replaying = false;
match loaded {
Ok(loaded) => (session_id, loaded.modes, loaded.config_options),
Err(err) => {
log::warn!(
"acp[{}]: session/load failed ({}); starting a fresh session",
agent.id,
describe(&err)
);
let _ = events
.send(AgentEvent::Warning { message: format!(
"{} could not resume the previous conversation; starting a new one",
agent.name
)))
) })
.await;
let new = new_session(connection, agent, opts, &mcp_servers, &init).await?;
(new.session_id, new.modes, new.config_options)
let new = new_session(connection, agent, opts, &mcp_servers, &init).await?;
(new.session_id, new.modes, new.config_options)
}
}
}
}
None => {
let new = new_session(connection, agent, opts, &mcp_servers, &init).await?;
(new.session_id, new.modes, new.config_options)
}
};
None => {
let new = new_session(connection, agent, opts, &mcp_servers, &init).await?;
(new.session_id, new.modes, new.config_options)
}
};

if opts.approval_mode == ApprovalMode::ReadOnly
&& let Some(plan_mode) = modes.as_ref().and_then(acp_plan_mode)
Expand All @@ -832,11 +833,13 @@ async fn handshake(
// current mode, which is the same least-privilege fallback used
// for Supervised sessions.
let _ = events
.send(AgentEvent::Warning(format!(
"{} could not enter its read-only plan mode: {}",
agent.name,
describe(&err)
)))
.send(AgentEvent::Warning {
message: format!(
"{} could not enter its read-only plan mode: {}",
agent.name,
describe(&err)
),
})
.await;
}
}
Expand Down Expand Up @@ -1054,11 +1057,11 @@ async fn handle_command(
SessionCommand::Steer { .. } => {
log::warn!("acp: steering is not part of the protocol; ignoring");
let _ = events
.send(AgentEvent::Warning(
"This agent cannot be steered (ACP has no steering method); \
.send(AgentEvent::Warning {
message: "This agent cannot be steered (ACP has no steering method); \
send the message after the turn finishes."
.into(),
))
})
.await;
}
SessionCommand::SendTurn {
Expand All @@ -1071,10 +1074,11 @@ async fn handle_command(
// ACP has no steering: one `session/prompt` per turn. The app
// queues turns, so this should not happen.
let _ = events
.send(AgentEvent::Warning(
.send(AgentEvent::Warning {
message:
"a turn is already running; ACP agents cannot take a second prompt mid-turn"
.into(),
))
})
.await;
return;
}
Expand Down Expand Up @@ -1153,10 +1157,11 @@ async fn handle_command(
Some(outcome) => outcome,
None => {
let _ = events
.send(AgentEvent::Warning(
.send(AgentEvent::Warning {
message:
"the agent offered no matching permission option; cancelling instead"
.into(),
))
})
.await;
acp::RequestPermissionOutcome::Cancelled
}
Expand Down Expand Up @@ -1188,10 +1193,9 @@ async fn handle_command(
}
Err(err) => {
let _ = events
.send(AgentEvent::Warning(format!(
"could not apply `{id}`: {}",
describe(&err)
)))
.send(AgentEvent::Warning {
message: format!("could not apply `{id}`: {}", describe(&err)),
})
.await;
}
}
Expand All @@ -1202,10 +1206,11 @@ async fn handle_command(
}
SessionCommand::SetApprovalMode(_) => {
let _ = events
.send(AgentEvent::Warning(
"ACP agents own their permission policy; use the agent's own mode selector"
.into(),
))
.send(AgentEvent::Warning {
message:
"ACP agents own their permission policy; use the agent's own mode selector"
.into(),
})
.await;
}
SessionCommand::SetInteractionMode(_) => {
Expand Down
6 changes: 3 additions & 3 deletions crates/agent/src/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,9 @@ async fn handle_command(
let msg = mapper.set_permission_mode_request(mode);
if write_line(stdin, &msg).await.is_err() {
let _ = event_tx
.send(AgentEvent::Warning(format!(
"claude: failed to switch permission mode to {mode:?}"
)))
.send(AgentEvent::Warning {
message: format!("claude: failed to switch permission mode to {mode:?}"),
})
.await;
} else {
mapper.applied_permission_mode = flag.to_string();
Expand Down
55 changes: 30 additions & 25 deletions crates/agent/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,9 +1115,9 @@ impl Actor {
}
SessionCommand::Interrupt => {
let Some(turn_id) = self.active_turn.clone() else {
self.emit(AgentEvent::Warning(
"cannot interrupt: no active Codex turn".into(),
))
self.emit(AgentEvent::Warning {
message: "cannot interrupt: no active Codex turn".into(),
})
.await;
return Ok(());
};
Expand All @@ -1133,9 +1133,9 @@ impl Actor {
decision,
} => {
let Some(json_rpc_id) = self.approvals.remove(&request_id) else {
self.emit(AgentEvent::Warning(format!(
"unknown Codex approval request id: {request_id}"
)))
self.emit(AgentEvent::Warning {
message: format!("unknown Codex approval request id: {request_id}"),
})
.await;
return Ok(());
};
Expand Down Expand Up @@ -1171,9 +1171,9 @@ impl Actor {
answers,
} => {
let Some(json_rpc_id) = self.user_inputs.remove(&request_id) else {
self.emit(AgentEvent::Warning(format!(
"unknown Codex user-input request id: {request_id}"
)))
self.emit(AgentEvent::Warning {
message: format!("unknown Codex user-input request id: {request_id}"),
})
.await;
return Ok(());
};
Expand Down Expand Up @@ -1201,9 +1201,11 @@ impl Actor {
// request. Signal the UI to fall back to a resume-restart (the
// fresh thread/resume carries the new mode), mirroring the
// model-switch path.
self.emit(AgentEvent::Warning(format!(
"codex: applying approval mode {mode:?} requires a session restart"
)))
self.emit(AgentEvent::Warning {
message: format!(
"codex: applying approval mode {mode:?} requires a session restart"
),
})
.await;
Ok(())
}
Expand All @@ -1227,9 +1229,9 @@ impl Actor {
// which is exactly the race we want to lose loudly rather than
// silently start a second turn.
let Some(turn_id) = self.active_turn.clone() else {
self.emit(AgentEvent::Warning(
"cannot steer: no active Codex turn".into(),
))
self.emit(AgentEvent::Warning {
message: "cannot steer: no active Codex turn".into(),
})
.await;
return Ok(());
};
Expand Down Expand Up @@ -1381,9 +1383,9 @@ impl Actor {
&mut self.stdin,
&json!({ "id": id, "error": { "code": -32601, "message": format!("unsupported server request: {method}") } }),
);
self.emit(AgentEvent::Warning(format!(
"unsupported Codex server request: {method}"
)))
self.emit(AgentEvent::Warning {
message: format!("unsupported Codex server request: {method}"),
})
.await;
return;
}
Expand Down Expand Up @@ -1454,9 +1456,9 @@ impl Actor {
.await;
}
Err(err) => {
self.emit(AgentEvent::Warning(format!(
"Codex supplied an invalid turn diff: {err}"
)))
self.emit(AgentEvent::Warning {
message: format!("Codex supplied an invalid turn diff: {err}"),
})
.await;
}
}
Expand Down Expand Up @@ -1603,13 +1605,16 @@ impl Actor {
}
"warning" | "configWarning" | "deprecationNotice" => {
if let Some(message) = params.get("message").and_then(Value::as_str) {
self.emit(AgentEvent::Warning(message.into())).await;
self.emit(AgentEvent::Warning {
message: message.into(),
})
.await;
}
}
"thread/closed" => {
self.emit(AgentEvent::Warning(
"Codex thread was closed by the server".into(),
))
self.emit(AgentEvent::Warning {
message: "Codex thread was closed by the server".into(),
})
.await;
}
_ => log::trace!("ignored Codex notification {method}: {params}"),
Expand Down
19 changes: 18 additions & 1 deletion crates/agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,9 @@ pub enum AgentEvent {
item_id: String,
markdown: String,
},
Warning(String),
Warning {
message: String,
},
/// A runtime-synthesized fatal provider startup failure persisted for replay.
#[rustfmt::skip]
ProviderStartFailed { error: String },
Expand Down Expand Up @@ -1482,6 +1484,21 @@ mod turn_diff_tests {
assert!(file_changes_from_unified_diff("--- a/file\n+++ b/file\n").is_err());
}

#[test]
fn warning_event_round_trips() {
let event = AgentEvent::Warning {
message: "boom".into(),
};
let json = serde_json::to_value(&event).unwrap();
assert_eq!(json["type"], "warning");
assert_eq!(json["message"], "boom");
let decoded: AgentEvent = serde_json::from_value(json).unwrap();
assert!(matches!(
decoded,
AgentEvent::Warning { message } if message == "boom"
));
}

#[test]
fn turn_change_event_round_trips() {
let event = AgentEvent::TurnChangesUpdated {
Expand Down
Loading