hive-runtime: record a new ACP session only once its first prompt is answered
The session id was written to the session file right after session/new, but the system prompt rides on the first prompt only. If that prompt failed, the next turn (or the next harness start) resumed the recorded session as not-new and the system prompt never reached it. The id is now written after the first session/prompt gets its reply. A failed first prompt leaves nothing recorded, so the next turn starts a new session and sends the system prompt again. Chosen over a separate "system prompt delivered" marker: one file, and "recorded" already means "usable". Tests drive AcpRuntime against a scripted sh agent that fails the first prompt: in-process and across a restart, the retry is a new session carrying the system prompt. Also: PermissionPolicy now sees a PermissionAsk (kind plus the MCP server the tool belongs to, matched by name against the servers handed to the session), so a caller can tell MCP tool calls from other `other` requests. Refs #4391
This commit is contained in:
parent
1b24edf4b4
commit
a2ab40cc69
6 changed files with 240 additions and 61 deletions
|
|
@ -33,10 +33,6 @@ impl StreamMapper {
|
|||
/// `servers` are the MCP server names handed to the agent, used to give
|
||||
/// their tools claude's `mcp__<server>__<tool>` names.
|
||||
pub(super) fn new(servers: Vec<String>) -> Self {
|
||||
let mut servers = servers;
|
||||
// Longest first, so a server whose name prefixes another's never
|
||||
// claims the other's tools.
|
||||
servers.sort_by_key(|s| std::cmp::Reverse(s.len()));
|
||||
Self {
|
||||
servers,
|
||||
text: String::new(),
|
||||
|
|
@ -226,18 +222,30 @@ pub(super) fn canonical_tool_name(name: &str, servers: &[String]) -> String {
|
|||
if name.starts_with("mcp__") {
|
||||
return name.to_owned();
|
||||
}
|
||||
for server in servers {
|
||||
if let Some(rest) = name
|
||||
.strip_prefix(server.as_str())
|
||||
.and_then(|r| r.strip_prefix('_'))
|
||||
{
|
||||
split_mcp_name(name, servers).map_or_else(
|
||||
|| name.to_owned(),
|
||||
|(server, tool)| format!("mcp__{server}__{tool}"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Which of `servers` a tool named `<server>_<tool>`, `<server>__<tool>` or
|
||||
/// `mcp__<server>__<tool>` belongs to, if any.
|
||||
pub(super) fn mcp_server_of<'a>(name: &str, servers: &'a [String]) -> Option<&'a str> {
|
||||
split_mcp_name(name, servers).map(|(server, _)| server)
|
||||
}
|
||||
|
||||
/// `(server, tool)` for an MCP tool name, matching the longest server name so
|
||||
/// one that prefixes another's never claims the other's tools.
|
||||
fn split_mcp_name<'a, 'n>(name: &'n str, servers: &'a [String]) -> Option<(&'a str, &'n str)> {
|
||||
let bare = name.strip_prefix("mcp__").unwrap_or(name);
|
||||
servers
|
||||
.iter()
|
||||
.filter_map(|server| {
|
||||
let rest = bare.strip_prefix(server.as_str())?.strip_prefix('_')?;
|
||||
let tool = rest.strip_prefix('_').unwrap_or(rest);
|
||||
if !tool.is_empty() {
|
||||
return format!("mcp__{server}__{tool}");
|
||||
}
|
||||
}
|
||||
}
|
||||
name.to_owned()
|
||||
(!tool.is_empty()).then_some((server.as_str(), tool))
|
||||
})
|
||||
.max_by_key(|(server, _)| server.len())
|
||||
}
|
||||
|
||||
fn chunk_text(update: &Value) -> &str {
|
||||
|
|
|
|||
Loading…
Reference in a new issue