acp runtime: write the session id by rename, and fail the first prompt when the system prompt is unreadable
This commit is contained in:
parent
4782f69110
commit
ca339b7ba3
1 changed files with 67 additions and 9 deletions
|
|
@ -112,6 +112,15 @@ pub enum AcpError {
|
||||||
NoHttpMcp,
|
NoHttpMcp,
|
||||||
#[error("reading the MCP config {path}: {detail}")]
|
#[error("reading the MCP config {path}: {detail}")]
|
||||||
McpConfig { path: PathBuf, detail: String },
|
McpConfig { path: PathBuf, detail: String },
|
||||||
|
#[error(
|
||||||
|
"the system prompt {path} is unreadable ({source}), so the first prompt of the session \
|
||||||
|
was not sent; the session stays pending and the next turn sends it"
|
||||||
|
)]
|
||||||
|
SystemPrompt {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
#[error(
|
#[error(
|
||||||
"ACP agent sent nothing for {silent_secs}s, so its turn was cancelled; a \
|
"ACP agent sent nothing for {silent_secs}s, so its turn was cancelled; a \
|
||||||
provider error the agent retries without reporting it, such as HTTP 429, looks like this"
|
provider error the agent retries without reporting it, such as HTTP 429, looks like this"
|
||||||
|
|
@ -558,7 +567,7 @@ impl<P: CompactionPolicy> AcpRuntime<P> {
|
||||||
let (servers, names) = mcp_servers(config)?;
|
let (servers, names) = mcp_servers(config)?;
|
||||||
let (session, created) = self.attach(live, &cwd, &servers).await?;
|
let (session, created) = self.attach(live, &cwd, &servers).await?;
|
||||||
choose_model_and_effort(live, &session, config, sink).await?;
|
choose_model_and_effort(live, &session, config, sink).await?;
|
||||||
let text = prompt_text(config, prompt, created);
|
let text = prompt_text(config, prompt, created)?;
|
||||||
let params = json!({ "sessionId": session, "prompt": [{ "type": "text", "text": text }] });
|
let params = json!({ "sessionId": session, "prompt": [{ "type": "text", "text": text }] });
|
||||||
discard_stale(live)?;
|
discard_stale(live)?;
|
||||||
let mut reply = live.conn.send("session/prompt", params).await?;
|
let mut reply = live.conn.send("session/prompt", params).await?;
|
||||||
|
|
@ -1007,16 +1016,22 @@ async fn choose(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The prompt as sent: the first one of a session carries the system prompt.
|
/// The prompt as sent: the first one of a session carries the system prompt.
|
||||||
fn prompt_text(config: &Config, prompt: &str, first: bool) -> String {
|
/// An unreadable prompt file fails the turn instead of sending the bare
|
||||||
|
/// prompt, because the session would then run without it for good.
|
||||||
|
fn prompt_text(
|
||||||
|
config: &Config,
|
||||||
|
prompt: &str,
|
||||||
|
first: bool,
|
||||||
|
) -> std::result::Result<String, AcpError> {
|
||||||
match (first, &config.system_prompt_file) {
|
match (first, &config.system_prompt_file) {
|
||||||
(true, Some(path)) => match std::fs::read_to_string(path) {
|
(true, Some(path)) => match std::fs::read_to_string(path) {
|
||||||
Ok(system) => format!("{system}\n\n{prompt}"),
|
Ok(system) => Ok(format!("{system}\n\n{prompt}")),
|
||||||
Err(e) => {
|
Err(source) => Err(AcpError::SystemPrompt {
|
||||||
tracing::warn!(path = %path.display(), error = %e, "system prompt unreadable");
|
path: path.clone(),
|
||||||
prompt.to_owned()
|
source,
|
||||||
}
|
}),
|
||||||
},
|
},
|
||||||
_ => prompt.to_owned(),
|
_ => Ok(prompt.to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1031,7 +1046,13 @@ fn write_id(file: &Path, id: &str) -> std::result::Result<(), AcpError> {
|
||||||
if let Some(dir) = file.parent() {
|
if let Some(dir) = file.parent() {
|
||||||
std::fs::create_dir_all(dir)?;
|
std::fs::create_dir_all(dir)?;
|
||||||
}
|
}
|
||||||
std::fs::write(file, id)?;
|
// Write beside the target and rename over it: a crash leaves the old id or
|
||||||
|
// the new one, never an empty file that `read_id` would treat as no session.
|
||||||
|
let mut tmp = file.as_os_str().to_owned();
|
||||||
|
tmp.push(".tmp");
|
||||||
|
let tmp = PathBuf::from(tmp);
|
||||||
|
std::fs::write(&tmp, id)?;
|
||||||
|
std::fs::rename(&tmp, file)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1370,6 +1391,43 @@ done
|
||||||
assert_eq!(recorded(dir.path()), "s1");
|
assert_eq!(recorded(dir.path()), "s1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_unreadable_system_prompt_fails_the_turn_and_the_next_one_sends_it() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let (runtime, config) = (runtime(dir.path(), "ok"), config(dir.path()));
|
||||||
|
std::fs::remove_file(dir.path().join("system.md")).unwrap();
|
||||||
|
|
||||||
|
let failed = runtime.run(&config, "one", &NoopSink).await;
|
||||||
|
assert!(
|
||||||
|
matches!(failed, Err(Error::Acp(AcpError::SystemPrompt { .. }))),
|
||||||
|
"{failed:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!dir.path().join("prompts").exists(),
|
||||||
|
"no prompt reached the agent"
|
||||||
|
);
|
||||||
|
assert!(!dir.path().join("session").exists());
|
||||||
|
|
||||||
|
std::fs::write(dir.path().join("system.md"), "SYSTEM PROMPT").unwrap();
|
||||||
|
let retried = runtime.run(&config, "two", &NoopSink).await.unwrap();
|
||||||
|
assert!(retried.created);
|
||||||
|
assert_eq!(carried_system_prompt(dir.path()), [true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_session_id_is_written_by_rename_and_leaves_no_temporary_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let file = dir.path().join("session");
|
||||||
|
super::write_id(&file, "s1").unwrap();
|
||||||
|
super::write_id(&file, "s2").unwrap();
|
||||||
|
assert_eq!(super::read_id(&file).as_deref(), Some("s2"));
|
||||||
|
let names: Vec<_> = std::fs::read_dir(dir.path())
|
||||||
|
.unwrap()
|
||||||
|
.map(|e| e.unwrap().file_name())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(names, ["session"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_turn_silent_for_the_idle_window_is_cancelled_and_fails() {
|
async fn a_turn_silent_for_the_idle_window_is_cancelled_and_fails() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue