fix: verify channel agent continuity and tool loops

This commit is contained in:
冰朔 2026-08-21 08:45:00 +08:00
commit 4bf32bc09b
5 changed files with 251 additions and 31 deletions

View file

@ -31,6 +31,7 @@ const MAX_MESSAGE_BYTES: usize = 64 * 1024;
const MAX_CONTEXT_MESSAGES: usize = 12;
const MAX_CONTEXT_CHARS: usize = 24_000;
const MAX_TOOL_ROUNDS: usize = 6;
const MAX_TOOL_RESULT_CHARS: usize = 12_000;
const TOKEN_PLAN_BASE_URL: &str =
"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
const LEGACY_CONVERSATION_ID: &str = "HLP-AGENT-CONV-LEGACY-0001";
@ -583,8 +584,10 @@ pub async fn send_message(
crate::persona_binding::begin_orientation(&app, &context.channel_number)?;
}
let channel_receipt = compile_human_channel_receipt(&app, &context, &content)?;
if binding_snapshot.binding.is_none() && !explicit_persona_wake {
if let Some(answer) = fast_channel_system_reply(&content, &context) {
if !explicit_persona_wake && (binding_snapshot.binding.is_none() || channel_system_addressed) {
if let Some(answer) =
fast_channel_system_reply(&content, &context, binding_snapshot.binding.is_some())
{
let history = conversation_at(&database, &conversation_id)?.messages;
let assistant_version = append_message(
&database,
@ -607,8 +610,11 @@ pub async fn send_message(
&CognitiveThoughtSummary {
trigger: "人类直接进入或确认零点原核频道在线状态".into(),
emergence: "本地确定性路由识别频道进入→编译频道回执→频道系统即时回应".into(),
lock: "频道进入不等于人格唤醒,广播保持关闭".into(),
why: "该轮只需确认频道本体和边界,无需加载人格脑或调用模型".into(),
lock:
"频道进入不等于人格唤醒;即使已有绑定,本轮也由频道本体回应,广播保持关闭"
.into(),
why: "该轮明确询问频道本体在线状态,无需让已绑定人格接管,也无需调用模型"
.into(),
},
)?;
emit_progress(
@ -857,6 +863,8 @@ fn explicit_channel_system_address(content: &str) -> bool {
[
"频道系统",
"频道本体",
"零点原核频道",
"零点原核本体频道",
"和频道说",
"回到频道",
"退出人格",
@ -901,7 +909,11 @@ fn select_reasoning_model(
}
}
fn fast_channel_system_reply(content: &str, context: &AgentChannelContext) -> Option<String> {
fn fast_channel_system_reply(
content: &str,
context: &AgentChannelContext,
persona_already_bound: bool,
) -> Option<String> {
let compact = content
.chars()
.filter(|character| !character.is_whitespace())
@ -924,10 +936,17 @@ fn fast_channel_system_reply(content: &str, context: &AgentChannelContext) -> Op
if !presence && !greeting {
return None;
}
Some(format!(
"在,我是{}本体。你现在是在直接和这个频道说话,还没有唤醒任何单一人格。\n\n你可以继续说要处理的事情;我会先判断它该由频道本地逻辑、私有技能脑或本机工具完成,确实需要语义推理时再进入模型层。若你明确点名某个人格,我才会另行启动它自己的定向与绑定。",
context.channel_name
))
Some(if persona_already_bound {
format!(
"在,我是{}本体。你这一轮明确叫的是频道,所以现在由频道本体直接回答;已有的人格绑定不会被删除,但也不会接管这一轮。\n\n你可以继续在这里讨论频道本身、设计频道或交给我判断下一步调度。只有你明确转向某个人格时,我才把回应目标切回该人格。",
context.channel_name
)
} else {
format!(
"在,我是{}本体。你现在是在直接和这个频道说话,还没有唤醒任何单一人格。\n\n你可以继续说要处理的事情;我会先判断它该由频道本地逻辑、私有技能脑或本机工具完成,确实需要语义推理时再进入模型层。若你明确点名某个人格,我才会另行启动它自己的定向与绑定。",
context.channel_name
)
})
}
fn compile_human_channel_receipt(
@ -1096,8 +1115,8 @@ fn tool_definitions() -> Vec<ToolDefinition> {
number: "HLP-AGENT-TOOL-KNOWLEDGE-READ-0001",
name: "hololake_knowledge_read",
display_name: "按编号读取知识页",
description: "只接受知识索引返回的精确 documentNumber映射并读取对应原始知识页;不接受路径猜测,不做第二次模糊联想。",
parameters: json!({"type":"object","additionalProperties":false,"required":["documentNumber"],"properties":{"documentNumber":{"type":"string"}}}),
description: "只接受知识索引返回的精确 documentNumber分页读取对应原始知识页;不接受路径猜测,不做第二次模糊联想。返回 totalCharacters、complete 和 nextOffset未完成时继续按 nextOffset 读取。",
parameters: json!({"type":"object","additionalProperties":false,"required":["documentNumber"],"properties":{"documentNumber":{"type":"string"},"offset":{"type":"integer","minimum":0},"maxCharacters":{"type":"integer","minimum":1000,"maximum":12000}}}),
},
ToolDefinition {
number: "HLP-AGENT-TOOL-KNOWLEDGE-LIST-0001",
@ -1214,6 +1233,30 @@ fn bounded_history(history: &[AgentMessage]) -> Vec<&AgentMessage> {
selected
}
fn character_page(value: &str, offset: usize, maximum: usize) -> (String, usize, bool) {
let total = value.chars().count();
let bounded_offset = offset.min(total);
let body = value
.chars()
.skip(bounded_offset)
.take(maximum)
.collect::<String>();
let consumed = bounded_offset.saturating_add(body.chars().count());
(body, consumed, consumed >= total)
}
fn model_tool_calls(message: &Value) -> Vec<Value> {
message
.get("tool_calls")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
}
fn is_channel_turn_commit(call: &Value) -> bool {
call.pointer("/function/name").and_then(Value::as_str) == Some("hololake_commit_channel_turn")
}
async fn run_agent_loop(
app: &AppHandle,
turn_id: &str,
@ -1232,7 +1275,7 @@ async fn run_agent_loop(
),
String,
> {
let enable_thinking = !model.contains("flash");
let enable_initial_thinking = !model.contains("flash");
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(150))
.build()
@ -1281,7 +1324,7 @@ async fn run_agent_loop(
"tools": wire_tools,
"tool_choice": "auto",
"stream": false,
"enable_thinking": enable_thinking
"enable_thinking": enable_initial_thinking && round == 0
}))
.send()
.await
@ -1302,11 +1345,7 @@ async fn run_agent_loop(
.pointer("/choices/0/message")
.cloned()
.ok_or_else(|| "HOLOLAKE_MODEL_RESPONSE_MESSAGE_MISSING".to_string())?;
let calls = message
.get("tool_calls")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let calls = model_tool_calls(&message);
if calls.is_empty() {
if round == MAX_TOOL_ROUNDS {
return Err("HOLOLAKE_AGENT_COGNITIVE_COMMIT_REQUIRED".into());
@ -1318,10 +1357,7 @@ async fn run_agent_loop(
}));
continue;
}
if let Some(commit_call) = calls.iter().find(|call| {
call.pointer("/function/name").and_then(Value::as_str)
== Some("hololake_commit_channel_turn")
}) {
if let Some(commit_call) = calls.iter().find(|call| is_channel_turn_commit(call)) {
if calls.len() != 1 {
return Err("HOLOLAKE_AGENT_COGNITIVE_COMMIT_MIXED_WITH_TOOLS".into());
}
@ -1437,6 +1473,12 @@ async fn execute_tool(
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "HOLOLAKE_AGENT_KNOWLEDGE_NUMBER_REQUIRED".to_string())?;
let offset = args.get("offset").and_then(Value::as_u64).unwrap_or(0) as usize;
let max_characters =
args.get("maxCharacters")
.and_then(Value::as_u64)
.unwrap_or(8_000)
.clamp(1_000, MAX_TOOL_RESULT_CHARS as u64) as usize;
emit_progress(
&app,
turn_id,
@ -1451,18 +1493,44 @@ async fn execute_tool(
},
)
.await?;
let total_characters = document.body.chars().count();
let (body, consumed, complete) = character_page(&document.body, offset, max_characters);
let receipt = AgentToolReceipt {
tool_number: "HLP-AGENT-TOOL-KNOWLEDGE-READ-0001".into(),
tool_name: "按编号读取知识页".into(),
target_path: document.page_header.path.clone(),
target_path: format!("{}?offset={offset}", document.page_header.path),
content_sha256: document.content_sha256.clone(),
summary: document.title.clone(),
};
Ok((
knowledge_evidence(
"KNOWLEDGE_DOCUMENT",
serde_json::to_value(document).map_err(|error| error.to_string())?,
summary: format!(
"{};读取字符 {}..{} / {};完成 {}",
document.title,
offset.min(total_characters),
consumed,
total_characters,
complete
),
};
let mut value = serde_json::to_value(document).map_err(|error| error.to_string())?;
let object = value
.as_object_mut()
.ok_or_else(|| "HOLOLAKE_AGENT_TOOL_RESULT_INVALID".to_string())?;
object.insert("body".into(), Value::String(body));
object.insert("offset".into(), json!(offset.min(total_characters)));
object.insert(
"returnedCharacters".into(),
json!(consumed.saturating_sub(offset.min(total_characters))),
);
object.insert("totalCharacters".into(), json!(total_characters));
object.insert("complete".into(), json!(complete));
object.insert(
"nextOffset".into(),
if complete {
Value::Null
} else {
json!(consumed)
},
);
Ok((
knowledge_evidence("KNOWLEDGE_DOCUMENT_PAGE", value),
receipt,
))
}
@ -2368,6 +2436,22 @@ mod tests {
assert!(!explicit_persona_wake_request("查看铸渊的历史署名"));
assert!(explicit_persona_wake_request("唤醒铸渊,我要和你说话"));
assert!(explicit_persona_wake_request("铸渊,你在吗?"));
assert!(explicit_channel_system_address(
"我是冰朔,零点原核频道。你在吗。"
));
let context = AgentChannelContext {
channel_number: CHANNEL_NUMBER.into(),
channel_name: "零点原核本体频道".into(),
human_number: HUMAN_NUMBER.into(),
human_name: HUMAN_NAME.into(),
domain: "第五域".into(),
channel_path: "第五域 / 零点原核本体频道".into(),
};
let reply =
fast_channel_system_reply("我是冰朔,零点原核频道。你在吗。", &context, true).unwrap();
assert!(reply.contains("现在由频道本体直接回答"));
assert!(reply.contains("已有的人格绑定不会被删除"));
assert!(!reply.contains("还没有唤醒任何单一人格"));
}
#[test]
@ -2475,6 +2559,46 @@ mod tests {
assert_eq!(context.last().unwrap().state_version, 20);
}
#[test]
fn large_tool_evidence_is_paged_without_silent_loss() {
let source = "".repeat(25_001);
let (first, next, complete) = character_page(&source, 0, MAX_TOOL_RESULT_CHARS);
assert_eq!(first.chars().count(), MAX_TOOL_RESULT_CHARS);
assert_eq!(next, MAX_TOOL_RESULT_CHARS);
assert!(!complete);
let (second, next, complete) = character_page(&source, next, MAX_TOOL_RESULT_CHARS);
assert_eq!(second.chars().count(), MAX_TOOL_RESULT_CHARS);
assert_eq!(next, MAX_TOOL_RESULT_CHARS * 2);
assert!(!complete);
let (last, next, complete) = character_page(&source, next, MAX_TOOL_RESULT_CHARS);
assert_eq!(last.chars().count(), 1_001);
assert_eq!(next, 25_001);
assert!(complete);
}
#[test]
fn provider_protocol_accepts_multiple_tool_rounds_before_single_commit() {
let rounds = [
json!({"tool_calls":[{"id":"one","function":{"name":"hololake_knowledge_search","arguments":"{\"query\":\"频道系统\"}"}}]}),
json!({"tool_calls":[{"id":"two","function":{"name":"hololake_knowledge_read","arguments":"{\"documentNumber\":\"HLP-KB-DOC-001\"}"}}]}),
json!({"tool_calls":[{"id":"commit","function":{"name":"hololake_commit_channel_turn","arguments":"{}"}}]}),
];
let first = model_tool_calls(&rounds[0]);
let second = model_tool_calls(&rounds[1]);
let final_calls = model_tool_calls(&rounds[2]);
assert_eq!(
first[0].pointer("/function/name").and_then(Value::as_str),
Some("hololake_knowledge_search")
);
assert_eq!(
second[0].pointer("/function/name").and_then(Value::as_str),
Some("hololake_knowledge_read")
);
assert_eq!(final_calls.len(), 1);
assert!(is_channel_turn_commit(&final_calls[0]));
assert!(rounds.len() <= MAX_TOOL_ROUNDS);
}
#[test]
fn fuzzy_language_routes_thought_then_reads_exact_numbered_branch() {
let directory = tempfile::tempdir().unwrap();