feat: stabilize page AI ACP runtimes
实现并稳定页面 AI 的 ACP Hermes / ACP Reasonix 运行路径。 主要内容: - 分离 profile 与 acpRuntime,ACP Hermes 按所选 Hermes profile 启动并注入 provider key。 - 修复 Reasonix ACP wrapper 的 API key 读取、ToolRegistry 注册、LoopEvent role 映射和 reasoning/final 分流。 - 修复 ACP agent_thought_chunk 被 untagged enum 误解析为 message.delta 的问题,补充 thought 相关单测。 - 补充页面 AI 浏览器验证 skill 证据到 7-15 设计稿,并记录严格验收标准。 - 同步提交当前仓库中已存在的 rust-web / Hermes tools / SSE / bug 文档相关改动。 验证: - node --check scripts/reasonix-acp-wrapper.mjs - cargo test -p mnote-web acp -- --nocapture - 页面 AI ACP 浏览器验证:tmp/page-ai-acp-browser-UAYwyM/
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
/// frontend (HermesRunEvent), and manages the background prompt lifecycle.
|
||||
///
|
||||
/// Reference: `hermes-vscode-main/src/sessionManager.ts` handleUpdate()
|
||||
|
||||
use crate::acp_runtime::AcpRuntimeManager;
|
||||
use crate::acp_session_manager::{AcpSessionEvent, AcpSessionManager};
|
||||
use crate::acp_types::ContentBlock;
|
||||
@@ -59,9 +58,14 @@ impl AcpRunBridge {
|
||||
) -> Result<Self, AcpBridgeError> {
|
||||
// Get or activate the runtime
|
||||
let client = if runtime_mgr.is_active().await {
|
||||
runtime_mgr.active_client().await.ok_or(AcpBridgeError::NoActiveRuntime)?
|
||||
runtime_mgr
|
||||
.active_client()
|
||||
.await
|
||||
.ok_or(AcpBridgeError::NoActiveRuntime)?
|
||||
} else {
|
||||
runtime_mgr.switch_to(runtime_name).await
|
||||
runtime_mgr
|
||||
.switch_to(runtime_name)
|
||||
.await
|
||||
.map_err(|e| AcpBridgeError::SessionError(e.to_string()))?
|
||||
};
|
||||
|
||||
@@ -80,7 +84,8 @@ impl AcpRunBridge {
|
||||
});
|
||||
|
||||
// Create session
|
||||
let sid = mgr.create_session(None, None)
|
||||
let sid = mgr
|
||||
.create_session(None, None)
|
||||
.await
|
||||
.map_err(|e| AcpBridgeError::SessionError(e.to_string()))?;
|
||||
|
||||
@@ -134,10 +139,12 @@ impl AcpRunBridge {
|
||||
loop {
|
||||
match broadcast_rx.recv().await {
|
||||
Ok(event) => {
|
||||
let json = serde_json::to_string(&event.data).unwrap_or_else(|_| "{}".into());
|
||||
let bytes = axum::body::Bytes::from(
|
||||
format!("event: {}\ndata: {}\n\n", event.event, json)
|
||||
);
|
||||
let json =
|
||||
serde_json::to_string(&event.data).unwrap_or_else(|_| "{}".into());
|
||||
let bytes = axum::body::Bytes::from(format!(
|
||||
"event: {}\ndata: {}\n\n",
|
||||
event.event, json
|
||||
));
|
||||
if tx.send(Ok(bytes)).await.is_err() {
|
||||
break; // receiver dropped
|
||||
}
|
||||
@@ -175,33 +182,27 @@ impl AcpRunBridge {
|
||||
/// Reference: wolai-frontend bridge.ts HermesRunEvent type
|
||||
pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
|
||||
match event {
|
||||
AcpSessionEvent::TextDelta { text } => {
|
||||
Some(SseEvent {
|
||||
event: "message.delta".into(),
|
||||
data: json!({ "delta": text }),
|
||||
})
|
||||
}
|
||||
AcpSessionEvent::ThoughtDelta { text } => {
|
||||
Some(SseEvent {
|
||||
event: "thought.delta".into(),
|
||||
data: json!({ "delta": text }),
|
||||
})
|
||||
}
|
||||
AcpSessionEvent::TextDelta { text } => Some(SseEvent {
|
||||
event: "message.delta".into(),
|
||||
data: json!({ "delta": text }),
|
||||
}),
|
||||
AcpSessionEvent::ThoughtDelta { text } => Some(SseEvent {
|
||||
event: "thought.delta".into(),
|
||||
data: json!({ "delta": text }),
|
||||
}),
|
||||
AcpSessionEvent::ToolCall {
|
||||
tool_call_id,
|
||||
title,
|
||||
kind,
|
||||
..
|
||||
} => {
|
||||
Some(SseEvent {
|
||||
event: "tool.started".into(),
|
||||
data: json!({
|
||||
"tool": title,
|
||||
"toolCallId": tool_call_id,
|
||||
"kind": kind,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} => Some(SseEvent {
|
||||
event: "tool.started".into(),
|
||||
data: json!({
|
||||
"tool": title,
|
||||
"toolCallId": tool_call_id,
|
||||
"kind": kind,
|
||||
}),
|
||||
}),
|
||||
AcpSessionEvent::ToolCallUpdate {
|
||||
tool_call_id,
|
||||
status,
|
||||
@@ -215,24 +216,21 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
|
||||
}),
|
||||
})
|
||||
}
|
||||
AcpSessionEvent::UsageUpdate { used, size } => {
|
||||
Some(SseEvent {
|
||||
event: "usage.updated".into(),
|
||||
data: json!({ "used": used, "size": size }),
|
||||
})
|
||||
}
|
||||
AcpSessionEvent::UsageUpdate { used, size } => Some(SseEvent {
|
||||
event: "usage.updated".into(),
|
||||
data: json!({ "used": used, "size": size }),
|
||||
}),
|
||||
AcpSessionEvent::SessionInfoUpdate { .. } => {
|
||||
None // Not forwarded to frontend
|
||||
}
|
||||
AcpSessionEvent::PlanUpdate { .. } => {
|
||||
None // Not forwarded (Phase C)
|
||||
}
|
||||
AcpSessionEvent::Disconnected { reason } => {
|
||||
Some(SseEvent {
|
||||
event: "run.failed".into(),
|
||||
data: json!({ "error": reason }),
|
||||
})
|
||||
}
|
||||
AcpSessionEvent::Disconnected { reason } if reason == "session closed" => None,
|
||||
AcpSessionEvent::Disconnected { reason } => Some(SseEvent {
|
||||
event: "run.failed".into(),
|
||||
data: json!({ "error": reason }),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,4 +244,38 @@ pub fn runtime_name_for_profile(profile: &str) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn acp_normal_session_close_does_not_emit_failure() {
|
||||
let event = AcpSessionEvent::Disconnected {
|
||||
reason: "session closed".into(),
|
||||
};
|
||||
|
||||
assert!(acp_event_to_sse(event).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_unexpected_disconnect_emits_failure() {
|
||||
let event = AcpSessionEvent::Disconnected {
|
||||
reason: "transport lost".into(),
|
||||
};
|
||||
|
||||
let sse = acp_event_to_sse(event).expect("unexpected disconnect should be forwarded");
|
||||
assert_eq!(sse.event, "run.failed");
|
||||
assert_eq!(sse.data["error"], "transport lost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_thought_delta_does_not_emit_message_delta() {
|
||||
let event = AcpSessionEvent::ThoughtDelta {
|
||||
text: "internal reasoning".into(),
|
||||
};
|
||||
|
||||
let sse = acp_event_to_sse(event).expect("thought delta should be forwarded separately");
|
||||
assert_eq!(sse.event, "thought.delta");
|
||||
assert_eq!(sse.data["delta"], "internal reasoning");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user