删除AGC无调用方的 steer Tauri 命令与 LLM steer 判定链
- 删除 Tauri 命令 steer_game_creator_agent_runtime_task,并同步 main.rs invoke_handler 注册与 AgentRuntimeSteerResult 的 assistantReply / interruptDecision / decisionReason 字段 - 删除只服务该命令的 agent/interaction.rs 整层(runtime_steer_decision 工具、判定请求构建与响应解析、decide_game_creator_agent_runtime_steer_at)及其 agent.rs 模块挂载 - 删除 runtime.interrupt_for_steer_decision RPC 与 Runner 客户端/派发分支、持久 decision 记录 agent.runtime.steer_decision、AgentRuntimeProviderInterrupt::applied_steer_cursor 及 register 的 cursor 参数 - 删除只测该链条的 provider / runner / runtime_state 用例
This commit is contained in:
@@ -38,7 +38,6 @@ mod direct_turn_metrics;
|
||||
mod direct_turn_stream;
|
||||
mod direct_validation;
|
||||
mod generation;
|
||||
mod interaction;
|
||||
mod prompt;
|
||||
mod runtime_actions;
|
||||
mod runtime_adapter;
|
||||
@@ -76,7 +75,6 @@ pub(crate) use direct_turn_metrics::*;
|
||||
pub(crate) use direct_turn_stream::*;
|
||||
pub(crate) use direct_validation::DirectValidationConfig;
|
||||
pub(crate) use generation::*;
|
||||
pub(crate) use interaction::*;
|
||||
pub(crate) use prompt::*;
|
||||
pub(crate) use runtime_actions::*;
|
||||
pub(crate) use runtime_adapter::*;
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
const AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS: u32 = 1_200;
|
||||
pub(crate) const AGENT_RUNTIME_STEER_DECISION_TOOL: &str = "runtime_steer_decision";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct AgentRuntimeSteerDecisionArguments {
|
||||
reply: String,
|
||||
interrupt_current_provider: bool,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
fn agent_runtime_steer_decision_function_tool() -> platform_llm::LlmFunctionTool {
|
||||
platform_llm::LlmFunctionTool::new(
|
||||
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
||||
prompt_text!("interaction.steer_decision_description"),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"required": ["reply", "interruptCurrentProvider", "reason"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"reply": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2000
|
||||
},
|
||||
"interruptCurrentProvider": { "type": "boolean" },
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_strict(true)
|
||||
}
|
||||
|
||||
fn build_agent_runtime_steer_decision_request(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
instruction: &str,
|
||||
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
|
||||
let (llm, config_path, context) = build_game_creator_role_agent_context_for_session(
|
||||
root,
|
||||
&state.agent_id,
|
||||
Some(&state.session_id),
|
||||
)?;
|
||||
let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?;
|
||||
let runtime_summary = serde_json::json!({
|
||||
"status": state.status,
|
||||
"phase": state.phase,
|
||||
"currentAction": state.current_action,
|
||||
"waitingOn": state.waiting_on,
|
||||
"nextStep": state.next_step,
|
||||
"plan": state.plan,
|
||||
"appliedSteerCursor": state.applied_steer_cursor,
|
||||
});
|
||||
let system = format!(
|
||||
prompt_text!("interaction.steer_decision_system"),
|
||||
game_creator_project_supervisor_chat_system_prompt()
|
||||
);
|
||||
let user = format!(
|
||||
prompt_text!("interaction.steer_decision_user"),
|
||||
serde_json::to_string_pretty(&runtime_summary)
|
||||
.map_err(|error| format!("序列化 steer Runtime 摘要失败:{error}"))?,
|
||||
instruction.trim(),
|
||||
context = context,
|
||||
);
|
||||
let request = LlmRunRequest::single_turn(system, user)
|
||||
.with_api_kind(api_kind)
|
||||
.with_max_output_tokens(AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS)
|
||||
.with_function_tools(vec![agent_runtime_steer_decision_function_tool()])
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
let request = apply_game_creator_llm_reasoning_effort(request, &llm)?;
|
||||
Ok((llm, config_path, request))
|
||||
}
|
||||
|
||||
fn parse_agent_runtime_steer_decision_response(
|
||||
response: &platform_llm::LlmRunResponse,
|
||||
) -> Result<AgentRuntimeSteerDecision, String> {
|
||||
if response.tool_calls.len() != 1 {
|
||||
return Err("Supervisor steer decision 必须返回一个 runtime_steer_decision".to_string());
|
||||
}
|
||||
let call = &response.tool_calls[0];
|
||||
if call.name != AGENT_RUNTIME_STEER_DECISION_TOOL {
|
||||
return Err(format!(
|
||||
"Supervisor steer decision 返回未知工具:{}",
|
||||
call.name
|
||||
));
|
||||
}
|
||||
let arguments = serde_json::from_str::<AgentRuntimeSteerDecisionArguments>(&call.arguments)
|
||||
.map_err(|error| format!("runtime_steer_decision 参数无效:{error}"))?;
|
||||
let reply = arguments.reply.trim();
|
||||
let reason = arguments.reason.trim();
|
||||
if reply.is_empty() || reason.is_empty() {
|
||||
return Err("runtime_steer_decision reply/reason 不能为空".to_string());
|
||||
}
|
||||
Ok(AgentRuntimeSteerDecision {
|
||||
reply: reply.to_string(),
|
||||
interrupt_current_provider: arguments.interrupt_current_provider,
|
||||
reason: reason.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn decide_game_creator_agent_runtime_steer_at(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
steer_id: &str,
|
||||
sequence: u64,
|
||||
instruction: &str,
|
||||
) -> Result<AgentRuntimeSteerDecision, String> {
|
||||
if let Some(decision) = read_game_creator_agent_runtime_steer_decision_at(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
steer_id,
|
||||
)? {
|
||||
return Ok(decision);
|
||||
}
|
||||
if state.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| state.parent_agent_id.is_some()
|
||||
|| state.parent_run_id.is_some()
|
||||
{
|
||||
return Err("只有根 Project Supervisor 支持运行中非终态回复".to_string());
|
||||
}
|
||||
let (llm, config_path, request) =
|
||||
build_agent_runtime_steer_decision_request(root, state, instruction)?;
|
||||
let decision_snapshot = {
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.steer_decision.snapshot",
|
||||
)?;
|
||||
let mut snapshot = capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"steer-decision",
|
||||
&format!("steer-{sequence}"),
|
||||
state.applied_steer_cursor,
|
||||
)?;
|
||||
// The main Runtime Provider request must keep running while this
|
||||
// independent LLM turn decides whether it is stale. A distinct node
|
||||
// identity gives Codex app-server a separate process/thread gate.
|
||||
snapshot.agent_id = format!("{}-steer-decision", state.agent_id);
|
||||
snapshot.task_id = snapshot.agent_id.clone();
|
||||
snapshot
|
||||
};
|
||||
let agent_mode =
|
||||
normalize_game_creator_agent_mode(&load_game_creator_app_config()?.agent_mode)?;
|
||||
let response = match agent_mode.as_str() {
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER => {
|
||||
request_game_creator_agent_codex_app_server(&decision_snapshot, &llm, request).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_CLI => request_game_creator_agent_codex_cli(request).await,
|
||||
GAME_CREATOR_AGENT_MODE_PROVIDER => {
|
||||
build_game_creator_agent_runtime_llm_client(&llm, &config_path)?
|
||||
.run(request)
|
||||
.await
|
||||
}
|
||||
_ => unreachable!("agent mode is normalized"),
|
||||
}
|
||||
.map_err(|error| format!("Supervisor steer decision 调用 LLM 失败:{error}"))?;
|
||||
let decision = parse_agent_runtime_steer_decision_response(&response)?;
|
||||
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
root, state, steer_id, sequence, decision,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn response(
|
||||
text: &str,
|
||||
tool_calls: Vec<platform_llm::LlmToolCall>,
|
||||
) -> platform_llm::LlmRunResponse {
|
||||
platform_llm::LlmRunResponse {
|
||||
provider: LlmProvider::OpenAiCompatible,
|
||||
model: "interaction-test".to_string(),
|
||||
text: text.to_string(),
|
||||
reasoning: String::new(),
|
||||
finish_reason: Some("stop".to_string()),
|
||||
response_id: Some("interaction-response".to_string()),
|
||||
usage: None,
|
||||
tool_calls,
|
||||
responses_output: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_call(name: &str, arguments: &str) -> platform_llm::LlmToolCall {
|
||||
platform_llm::LlmToolCall {
|
||||
id: "call-interaction".to_string(),
|
||||
name: name.to_string(),
|
||||
arguments: arguments.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steer_decision_replies_without_interrupting_status_questions() {
|
||||
let decision = parse_agent_runtime_steer_decision_response(&response(
|
||||
"",
|
||||
vec![tool_call(
|
||||
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
||||
r#"{"reply":"我正在完成玩法实现,当前任务会继续。","interruptCurrentProvider":false,"reason":"这是状态询问,不会使当前方案过期。"}"#,
|
||||
)],
|
||||
))
|
||||
.expect("parse non-interrupting steer decision");
|
||||
assert_eq!(
|
||||
decision,
|
||||
AgentRuntimeSteerDecision {
|
||||
reply: "我正在完成玩法实现,当前任务会继续。".to_string(),
|
||||
interrupt_current_provider: false,
|
||||
reason: "这是状态询问,不会使当前方案过期。".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steer_decision_can_request_safe_interrupt_for_conflicting_change() {
|
||||
let decision = parse_agent_runtime_steer_decision_response(&response(
|
||||
"",
|
||||
vec![tool_call(
|
||||
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
||||
r#"{"reply":"明白,我会停止旧方向并改成回合制。","interruptCurrentProvider":true,"reason":"用户明确改向,旧 Provider 仍在生成过期方案。"}"#,
|
||||
)],
|
||||
))
|
||||
.expect("parse interrupting steer decision");
|
||||
assert!(decision.interrupt_current_provider);
|
||||
assert!(decision.reply.contains("改成回合制"));
|
||||
}
|
||||
}
|
||||
@@ -84,20 +84,15 @@ pub(crate) use run_configuration::{
|
||||
};
|
||||
pub(crate) use steering::{
|
||||
acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait,
|
||||
append_game_creator_agent_runtime_steer_decision_failure_reply_at,
|
||||
consume_game_creator_agent_runtime_steers,
|
||||
game_creator_agent_runtime_provider_request_count_for_roots,
|
||||
game_creator_agent_runtime_steer_ledger_path,
|
||||
interrupt_game_creator_agent_runtime_provider_for_decided_steer_at,
|
||||
interrupt_game_creator_agent_runtime_provider_request_at,
|
||||
interrupt_game_creator_agent_runtime_provider_requests_for_roots,
|
||||
persist_game_creator_agent_runtime_steer_decision_and_reply_at,
|
||||
read_game_creator_agent_runtime_steer_decision_at,
|
||||
register_game_creator_agent_runtime_provider_request,
|
||||
render_game_creator_agent_runtime_steers_for_prompt, steer_game_creator_agent_runtime_task_at,
|
||||
steer_game_creator_agent_runtime_task_for_profile_at,
|
||||
unregister_game_creator_agent_runtime_provider_request,
|
||||
validate_game_creator_agent_runtime_steer_notification_at, AgentRuntimeSteerDecision,
|
||||
validate_game_creator_agent_runtime_steer_notification_at,
|
||||
};
|
||||
pub(crate) use verification::{
|
||||
game_creator_agent_runtime_verification_gate_path,
|
||||
|
||||
@@ -489,7 +489,6 @@ pub(in crate::agent) struct AgentRuntimeSteerLedgerSnapshot {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AgentRuntimeProviderInterrupt {
|
||||
pub(crate) applied_steer_cursor: u64,
|
||||
pub(crate) interrupted: AtomicBool,
|
||||
pub(crate) notify: tokio::sync::Notify,
|
||||
}
|
||||
|
||||
@@ -1075,7 +1075,6 @@ where
|
||||
root,
|
||||
&snapshot.agent_id,
|
||||
&snapshot.run_id,
|
||||
snapshot.applied_steer_cursor,
|
||||
)
|
||||
.map_err(|error| redact_agent_runtime_error(root, &error, 500))?;
|
||||
before_control_recheck();
|
||||
|
||||
@@ -431,202 +431,6 @@ pub(in crate::agent) fn ensure_game_creator_agent_runtime_steer_audit(
|
||||
)
|
||||
}
|
||||
|
||||
const AGENT_RUNTIME_STEER_DECISION_RECORD_TYPE: &str = "agent.runtime.steer_decision";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct AgentRuntimeSteerDecision {
|
||||
pub(crate) reply: String,
|
||||
pub(crate) interrupt_current_provider: bool,
|
||||
pub(crate) reason: String,
|
||||
}
|
||||
|
||||
fn parse_game_creator_agent_runtime_steer_decision_record(
|
||||
record: &serde_json::Value,
|
||||
) -> Result<AgentRuntimeSteerDecision, String> {
|
||||
let reply = record
|
||||
.get("reply")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "Supervisor steer decision 缺少 reply".to_string())?;
|
||||
let reply_sha256 = format!("{:x}", Sha256::digest(reply.as_bytes()));
|
||||
if record
|
||||
.get("replySha256")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some(reply_sha256.as_str())
|
||||
{
|
||||
return Err("Supervisor steer decision reply 指纹冲突".to_string());
|
||||
}
|
||||
let interrupt_current_provider = record
|
||||
.get("interruptCurrentProvider")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.ok_or_else(|| "Supervisor steer decision 缺少 interruptCurrentProvider".to_string())?;
|
||||
let reason = record
|
||||
.get("reason")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
Ok(AgentRuntimeSteerDecision {
|
||||
reply: reply.to_string(),
|
||||
interrupt_current_provider,
|
||||
reason: reason.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_game_creator_agent_runtime_steer_decision_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
steer_id: &str,
|
||||
) -> Result<Option<AgentRuntimeSteerDecision>, String> {
|
||||
let (records, _) = read_agent_db_records_bounded(root, 16 * 1024 * 1024)?;
|
||||
let mut decision = None;
|
||||
for record in records.iter().filter(|record| {
|
||||
record.get("recordType").and_then(serde_json::Value::as_str)
|
||||
== Some(AGENT_RUNTIME_STEER_DECISION_RECORD_TYPE)
|
||||
&& record.get("agentId").and_then(serde_json::Value::as_str) == Some(agent_id)
|
||||
&& record.get("runId").and_then(serde_json::Value::as_str) == Some(run_id)
|
||||
&& record.get("steerId").and_then(serde_json::Value::as_str) == Some(steer_id)
|
||||
}) {
|
||||
let parsed = parse_game_creator_agent_runtime_steer_decision_record(record)?;
|
||||
if decision
|
||||
.as_ref()
|
||||
.is_some_and(|existing| existing != &parsed)
|
||||
{
|
||||
return Err("Supervisor steer decision 出现冲突终态".to_string());
|
||||
}
|
||||
decision = Some(parsed);
|
||||
}
|
||||
Ok(decision)
|
||||
}
|
||||
|
||||
pub(crate) fn persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
steer_id: &str,
|
||||
sequence: u64,
|
||||
decision: AgentRuntimeSteerDecision,
|
||||
) -> Result<AgentRuntimeSteerDecision, String> {
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.steer_decision.persist",
|
||||
)?;
|
||||
let snapshot =
|
||||
read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?;
|
||||
let entry = snapshot
|
||||
.entries
|
||||
.get(steer_id)
|
||||
.ok_or_else(|| "Supervisor steer decision 未命中持久 steer".to_string())?;
|
||||
if entry.identity.sequence != sequence
|
||||
|| entry.identity.session_id != state.session_id
|
||||
|| entry.identity.source != state.source
|
||||
{
|
||||
return Err("Supervisor steer decision 与持久 steer 身份冲突".to_string());
|
||||
}
|
||||
let reply = sanitize_agent_runtime_text(decision.reply.trim(), 2_000);
|
||||
if reply.is_empty() {
|
||||
return Err("Supervisor steer decision reply 不能为空".to_string());
|
||||
}
|
||||
let reason = redact_agent_runtime_project_paths(
|
||||
root,
|
||||
&sanitize_agent_runtime_text(decision.reason.trim(), 500),
|
||||
500,
|
||||
);
|
||||
let decision = AgentRuntimeSteerDecision {
|
||||
reply,
|
||||
interrupt_current_provider: decision.interrupt_current_provider,
|
||||
reason,
|
||||
};
|
||||
let persisted = if let Some(existing) = read_game_creator_agent_runtime_steer_decision_at(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
steer_id,
|
||||
)? {
|
||||
existing
|
||||
} else {
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": AGENT_RUNTIME_STEER_DECISION_RECORD_TYPE,
|
||||
"agentId": state.agent_id,
|
||||
"taskId": state.task_id,
|
||||
"sessionId": state.session_id,
|
||||
"runId": state.run_id,
|
||||
"source": state.source,
|
||||
"steerId": steer_id,
|
||||
"sequence": sequence,
|
||||
"messageId": entry.identity.message_id,
|
||||
"instructionSha256": entry.identity.instruction_sha256,
|
||||
"reply": decision.reply,
|
||||
"replySha256": format!("{:x}", Sha256::digest(decision.reply.as_bytes())),
|
||||
"interruptCurrentProvider": decision.interrupt_current_provider,
|
||||
"reason": decision.reason,
|
||||
"decidedAt": unix_timestamp(),
|
||||
}),
|
||||
)?;
|
||||
decision
|
||||
};
|
||||
let message_id = entry.identity.message_id.as_deref().unwrap_or_default();
|
||||
let correlation_id = message_id
|
||||
.strip_prefix(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX)
|
||||
.ok_or_else(|| "Supervisor steer decision 缺少消息关联身份".to_string())?;
|
||||
append_game_creator_agent_runtime_public_status_message_for_correlation_at(
|
||||
root,
|
||||
correlation_id,
|
||||
"steer-reply",
|
||||
&persisted.reply,
|
||||
)?;
|
||||
Ok(persisted)
|
||||
}
|
||||
|
||||
pub(crate) fn append_game_creator_agent_runtime_steer_decision_failure_reply_at(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
steer_id: &str,
|
||||
error: &str,
|
||||
) -> Result<String, String> {
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.steer_decision.failure",
|
||||
)?;
|
||||
let snapshot =
|
||||
read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?;
|
||||
let entry = snapshot
|
||||
.entries
|
||||
.get(steer_id)
|
||||
.ok_or_else(|| "Supervisor steer decision 失败记录未命中持久 steer".to_string())?;
|
||||
let correlation_id = entry
|
||||
.identity
|
||||
.message_id
|
||||
.as_deref()
|
||||
.and_then(|message_id| message_id.strip_prefix(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX))
|
||||
.ok_or_else(|| "Supervisor steer decision 失败记录缺少消息关联身份".to_string())?;
|
||||
let reply = "我会继续当前任务。这次没有完成中断判断,你的消息仍会在下一个安全边界进入规划。";
|
||||
append_game_creator_agent_runtime_public_status_message_for_correlation_at(
|
||||
root,
|
||||
correlation_id,
|
||||
"steer-decision-failed",
|
||||
reply,
|
||||
)?;
|
||||
let safe_error = redact_agent_runtime_error(root, error, 500);
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.steer_decision.failed",
|
||||
"agentId": state.agent_id,
|
||||
"taskId": state.task_id,
|
||||
"sessionId": state.session_id,
|
||||
"runId": state.run_id,
|
||||
"source": state.source,
|
||||
"steerId": steer_id,
|
||||
"sequence": entry.identity.sequence,
|
||||
"errorSha256": format!("{:x}", Sha256::digest(safe_error.as_bytes())),
|
||||
"errorChars": safe_error.chars().count(),
|
||||
"failedAt": unix_timestamp(),
|
||||
}),
|
||||
)?;
|
||||
Ok(reply.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_agent_runtime_accepts_steer(state: &AgentRuntimeState) -> bool {
|
||||
!state.agent_id.starts_with("child-")
|
||||
&& state.source != AGENT_RUNTIME_ISOLATED_CHILD_SOURCE
|
||||
@@ -1125,9 +929,6 @@ fn transition_goal_contract_root_steer_at(
|
||||
// Returning true prevents CLI/Tauri callers from forwarding a stale
|
||||
// runtime.steer notification to the old external-runner Run.
|
||||
provider_interrupted: true,
|
||||
assistant_reply: None,
|
||||
interrupt_decision: None,
|
||||
decision_reason: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1367,9 +1168,6 @@ pub(crate) fn steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
sequence: entry.identity.sequence,
|
||||
status: latest_status,
|
||||
provider_interrupted,
|
||||
assistant_reply: None,
|
||||
interrupt_decision: None,
|
||||
decision_reason: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1420,44 +1218,6 @@ pub(crate) fn interrupt_game_creator_agent_runtime_provider_request_at(
|
||||
Ok(first)
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt_game_creator_agent_runtime_provider_for_decided_steer_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
steer_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
let decision =
|
||||
read_game_creator_agent_runtime_steer_decision_at(root, agent_id, run_id, steer_id)?
|
||||
.ok_or_else(|| "Supervisor steer 尚无 LLM 中断判定".to_string())?;
|
||||
if !decision.interrupt_current_provider {
|
||||
return Ok(false);
|
||||
}
|
||||
let snapshot = read_game_creator_agent_runtime_steer_ledger(root, agent_id, run_id)?;
|
||||
let entry = snapshot
|
||||
.entries
|
||||
.get(steer_id)
|
||||
.ok_or_else(|| "LLM 中断判定未命中持久 steer".to_string())?;
|
||||
let key = agent_runtime_provider_interrupt_key(root, agent_id, run_id)?;
|
||||
let active = {
|
||||
let registry = game_creator_agent_provider_interrupts()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
registry.get(&key).cloned()
|
||||
};
|
||||
let Some(active) = active else {
|
||||
return Ok(false);
|
||||
};
|
||||
if active.applied_steer_cursor >= entry.identity.sequence {
|
||||
// The provider request was created after this steer was already
|
||||
// applied. Interrupting it would cancel the new plan, not the stale
|
||||
// request the LLM evaluated.
|
||||
return Ok(false);
|
||||
}
|
||||
let first = !active.interrupted.swap(true, Ordering::AcqRel);
|
||||
active.notify.notify_one();
|
||||
Ok(first)
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt_game_creator_agent_runtime_provider_requests_for_roots(
|
||||
roots: &[PathBuf],
|
||||
) -> usize {
|
||||
@@ -1523,13 +1283,9 @@ pub(crate) fn register_game_creator_agent_runtime_provider_request(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
applied_steer_cursor: u64,
|
||||
) -> Result<(String, Arc<AgentRuntimeProviderInterrupt>), String> {
|
||||
let key = agent_runtime_provider_interrupt_key(root, agent_id, run_id)?;
|
||||
let active = Arc::new(AgentRuntimeProviderInterrupt {
|
||||
applied_steer_cursor,
|
||||
..AgentRuntimeProviderInterrupt::default()
|
||||
});
|
||||
let active = Arc::new(AgentRuntimeProviderInterrupt::default());
|
||||
let mut registry = game_creator_agent_provider_interrupts()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -2183,14 +1939,12 @@ mod shutdown_tests {
|
||||
&known_root,
|
||||
"code-prototype",
|
||||
"known-run",
|
||||
0,
|
||||
)
|
||||
.expect("register known Provider request");
|
||||
let (other_key, other_request) = register_game_creator_agent_runtime_provider_request(
|
||||
&other_root,
|
||||
"code-prototype",
|
||||
"other-run",
|
||||
0,
|
||||
)
|
||||
.expect("register other Provider request");
|
||||
|
||||
|
||||
@@ -1525,139 +1525,6 @@ pub(crate) fn clear_game_creator_agent_goal(
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(dead_code)]
|
||||
// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。
|
||||
// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。
|
||||
pub(crate) async fn steer_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
run_id: String,
|
||||
steer_id: String,
|
||||
instruction: String,
|
||||
run_profile: Option<String>,
|
||||
source: Option<String>,
|
||||
) -> Result<AgentRuntimeSteerResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
enforce_project_permission_policy(root, "conversation.write")?;
|
||||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||||
if let Some(source) = source
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
if !agent_runtime_supervisor_source_is_trusted(source) {
|
||||
return Err("Project Supervisor steer source 不受信任".to_string());
|
||||
}
|
||||
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
)?
|
||||
.ok_or_else(|| "Agent Runtime steer 的 Run 不存在".to_string())?;
|
||||
if task.source != source {
|
||||
return Err("Agent Runtime steer source 与当前 Run 不一致".to_string());
|
||||
}
|
||||
}
|
||||
let mut result = steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
run_id.trim(),
|
||||
steer_id.trim(),
|
||||
instruction.trim(),
|
||||
run_profile.as_deref(),
|
||||
"tauri",
|
||||
)?;
|
||||
let external_runner =
|
||||
external_agent_runner_enabled() && !external_agent_runner_is_server_process();
|
||||
let wake_external_runner_without_interrupt = || -> Result<(), String> {
|
||||
let provider_interrupted =
|
||||
steer_external_agent_runner(root, agent_id.trim(), run_id.trim(), steer_id.trim())?;
|
||||
if provider_interrupted {
|
||||
return Err("Agent Runner 的 runtime.steer 非法中断了 Provider".to_string());
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let state = result.runtime.state.clone();
|
||||
let requires_supervisor_decision = state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& state.parent_agent_id.is_none()
|
||||
&& state.parent_run_id.is_none();
|
||||
if requires_supervisor_decision {
|
||||
match decide_game_creator_agent_runtime_steer_at(
|
||||
root,
|
||||
&state,
|
||||
steer_id.trim(),
|
||||
result.sequence,
|
||||
instruction.trim(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(decision) => {
|
||||
result.assistant_reply = Some(decision.reply.clone());
|
||||
result.interrupt_decision = Some(decision.interrupt_current_provider);
|
||||
result.decision_reason = Some(decision.reason.clone());
|
||||
if decision.interrupt_current_provider {
|
||||
result.provider_interrupted = if external_runner {
|
||||
interrupt_external_agent_runner_provider_for_steer_decision(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
steer_id.trim(),
|
||||
)?
|
||||
} else {
|
||||
interrupt_game_creator_agent_runtime_provider_for_decided_steer_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
steer_id.trim(),
|
||||
)?
|
||||
};
|
||||
if !external_runner {
|
||||
wake_pending_game_creator_agent_background_tasks_at(root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
} else if external_runner {
|
||||
wake_external_runner_without_interrupt()?;
|
||||
} else {
|
||||
wake_pending_game_creator_agent_background_tasks_at(root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
result.assistant_reply = Some(
|
||||
append_game_creator_agent_runtime_steer_decision_failure_reply_at(
|
||||
root,
|
||||
&state,
|
||||
steer_id.trim(),
|
||||
&error,
|
||||
)?,
|
||||
);
|
||||
result.decision_reason = Some("decision-failed".to_string());
|
||||
if external_runner {
|
||||
wake_external_runner_without_interrupt()?;
|
||||
} else {
|
||||
wake_pending_game_creator_agent_background_tasks_at(root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if external_runner {
|
||||
wake_external_runner_without_interrupt()?;
|
||||
} else {
|
||||
wake_pending_game_creator_agent_background_tasks_at(root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
result.runtime = read_game_creator_agent_runtime_for_session_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
Some(session_id.trim()),
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn cancel_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
|
||||
@@ -823,12 +823,6 @@ struct AgentRuntimeSteerResult {
|
||||
sequence: u64,
|
||||
status: String,
|
||||
provider_interrupted: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
assistant_reply: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
interrupt_decision: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
decision_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
@@ -2621,7 +2615,6 @@ fn main() {
|
||||
pause_game_creator_agent_goal,
|
||||
resume_game_creator_agent_goal,
|
||||
clear_game_creator_agent_goal,
|
||||
steer_game_creator_agent_runtime_task,
|
||||
cancel_game_creator_agent_runtime_task,
|
||||
retry_game_creator_agent_runtime_task,
|
||||
confirm_retry_game_creator_agent_runtime_task,
|
||||
|
||||
@@ -13,8 +13,7 @@ pub(crate) use client::{
|
||||
configure_external_agent_runner, configure_external_agent_runner_read_only,
|
||||
continue_external_agent_runner_action, ensure_external_agent_runner_started,
|
||||
ensure_external_agent_runner_started_for_gui, hold_external_agent_runner_gui_participant_lock,
|
||||
install_external_agent_runner_platform_session,
|
||||
interrupt_external_agent_runner_provider_for_steer_decision, notify_external_agent_runner,
|
||||
install_external_agent_runner_platform_session, notify_external_agent_runner,
|
||||
pause_external_agent_runner, read_external_agent_runner_status,
|
||||
require_external_agent_runner_configured_for_cli_runtime_write,
|
||||
require_external_agent_runner_for_cli_runtime_write, resume_external_agent_runner,
|
||||
|
||||
@@ -2071,29 +2071,6 @@ pub(crate) fn steer_external_agent_runner(
|
||||
parse_external_agent_runner_steer_result(&result)
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt_external_agent_runner_provider_for_steer_decision(
|
||||
root: &Path,
|
||||
agent: &str,
|
||||
run_id: &str,
|
||||
steer_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
if [agent, run_id, steer_id]
|
||||
.into_iter()
|
||||
.any(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("LLM steer 中断判定必须同时提供 agent/runId/steerId".to_string());
|
||||
}
|
||||
let result = send_external_agent_runner_runtime_request(
|
||||
root,
|
||||
"runtime.interrupt_for_steer_decision",
|
||||
Some(agent.trim()),
|
||||
Some(run_id.trim()),
|
||||
None,
|
||||
Some(steer_id.trim()),
|
||||
)?;
|
||||
parse_external_agent_runner_steer_result(&result)
|
||||
}
|
||||
|
||||
pub(crate) fn pause_external_agent_runner(
|
||||
root: &Path,
|
||||
agent: &str,
|
||||
|
||||
@@ -576,7 +576,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.interrupt_for_steer_decision"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact"
|
||||
@@ -634,7 +633,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.interrupt_for_steer_decision"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact" => {
|
||||
@@ -684,23 +682,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
|
||||
"providerInterrupted": false,
|
||||
}))
|
||||
})(),
|
||||
"runtime.interrupt_for_steer_decision" => (|| {
|
||||
let agent = external_agent_runner_request_agent(request)?;
|
||||
let run_id = external_agent_runner_request_run_id(request)?;
|
||||
let steer_id = external_agent_runner_request_steer_id(request)?;
|
||||
let provider_interrupted = crate::interrupt_game_creator_agent_runtime_provider_for_decided_steer_at(
|
||||
&root,
|
||||
&agent,
|
||||
&run_id,
|
||||
&steer_id,
|
||||
)?;
|
||||
crate::wake_pending_game_creator_agent_background_tasks_at(&root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(json!({
|
||||
"accepted": true,
|
||||
"providerInterrupted": provider_interrupted,
|
||||
}))
|
||||
})(),
|
||||
"runtime.pause" => (|| {
|
||||
let agent = external_agent_runner_request_agent(request)?;
|
||||
let run_id = external_agent_runner_request_run_id(request)?;
|
||||
@@ -1157,7 +1138,6 @@ pub(super) fn handle_external_agent_runner_request(
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.interrupt_for_steer_decision"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact"
|
||||
|
||||
@@ -2008,7 +2008,6 @@ fn runtime_steer_queues_without_interrupt_and_deduplicates_request_id() {
|
||||
&root,
|
||||
"code-prototype",
|
||||
"run-steer-rpc",
|
||||
0,
|
||||
)
|
||||
.expect("register active Provider before runtime.steer");
|
||||
let request = ExternalAgentRunnerRequest {
|
||||
@@ -2054,293 +2053,6 @@ fn runtime_steer_queues_without_interrupt_and_deduplicates_request_id() {
|
||||
crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_interrupt_for_steer_decision_rejects_missing_durable_decision() {
|
||||
let directory = unique_test_directory();
|
||||
let root = directory.0.join("project");
|
||||
crate::init_local_game_project_at(
|
||||
&root,
|
||||
"project-steer-missing-decision-rpc",
|
||||
"Runner steer 缺少判定测试",
|
||||
)
|
||||
.expect("initialize missing-decision project");
|
||||
let runtime = crate::start_game_creator_agent_runtime_task_for_session_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
None,
|
||||
"保持当前 Provider 请求运行",
|
||||
"run-steer-missing-decision-rpc",
|
||||
"agent-background-task",
|
||||
"等待 Supervisor 判定",
|
||||
vec!["完成当前任务".to_string()],
|
||||
)
|
||||
.expect("start missing-decision runtime");
|
||||
let steer = crate::steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
"steer-missing-decision-rpc-1",
|
||||
"先告诉我进度。",
|
||||
"runner-test",
|
||||
)
|
||||
.expect("persist steer without decision");
|
||||
assert_eq!(steer.sequence, 1);
|
||||
let (provider_key, active_provider) =
|
||||
crate::register_game_creator_agent_runtime_provider_request(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
0,
|
||||
)
|
||||
.expect("register active Provider before missing decision request");
|
||||
let token = "steer-missing-decision-token-steer-missing-decision-token";
|
||||
let state = ExternalAgentRunnerServerState::new(
|
||||
directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
test_endpoint(token, "steer-missing-decision-boot", 30304),
|
||||
);
|
||||
let response = dispatch_external_agent_runner_runtime_request(
|
||||
&ExternalAgentRunnerRequest {
|
||||
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id: "steer-missing-decision-request-1".to_string(),
|
||||
token: token.to_string(),
|
||||
method: "runtime.interrupt_for_steer_decision".to_string(),
|
||||
params: ExternalAgentRunnerRequestParams {
|
||||
root: Some(root.to_string_lossy().into_owned()),
|
||||
agent: Some(runtime.agent_id.clone()),
|
||||
run_id: Some(runtime.run_id.clone()),
|
||||
steer_id: Some(steer.steer_id),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
},
|
||||
},
|
||||
&state,
|
||||
);
|
||||
|
||||
assert!(!response.ok);
|
||||
assert_eq!(
|
||||
response.error.as_ref().map(|error| error.code.as_str()),
|
||||
Some("runtime-error")
|
||||
);
|
||||
assert!(response
|
||||
.error
|
||||
.as_ref()
|
||||
.is_some_and(|error| error.message.contains("尚无 LLM 中断判定")));
|
||||
assert!(
|
||||
!active_provider.interrupted.load(Ordering::Acquire),
|
||||
"a missing durable LLM decision must leave the active Provider running"
|
||||
);
|
||||
crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_interrupt_for_false_steer_decision_keeps_provider_running() {
|
||||
let directory = unique_test_directory();
|
||||
let root = directory.0.join("project");
|
||||
crate::init_local_game_project_at(
|
||||
&root,
|
||||
"project-steer-false-decision-rpc",
|
||||
"Runner steer 不中断判定测试",
|
||||
)
|
||||
.expect("initialize false-decision project");
|
||||
let runtime = crate::start_game_creator_agent_runtime_task_for_session_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
None,
|
||||
"保持当前 Provider 请求运行",
|
||||
"run-steer-false-decision-rpc",
|
||||
"agent-background-task",
|
||||
"处理状态询问",
|
||||
vec!["完成当前任务".to_string()],
|
||||
)
|
||||
.expect("start false-decision runtime");
|
||||
let steer = crate::steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
"steer-false-decision-rpc-1",
|
||||
"现在做到哪一步了?",
|
||||
"runner-test",
|
||||
)
|
||||
.expect("persist status-question steer");
|
||||
crate::persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
&root,
|
||||
&runtime,
|
||||
&steer.steer_id,
|
||||
steer.sequence,
|
||||
crate::AgentRuntimeSteerDecision {
|
||||
reply: "当前任务仍在继续,我会按现有方向完成。".to_string(),
|
||||
interrupt_current_provider: false,
|
||||
reason: "状态询问不会让当前方案过期。".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("persist non-interrupting LLM decision");
|
||||
let (provider_key, active_provider) =
|
||||
crate::register_game_creator_agent_runtime_provider_request(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
0,
|
||||
)
|
||||
.expect("register active Provider for false decision");
|
||||
let token = "steer-false-decision-token-steer-false-decision-token";
|
||||
let state = ExternalAgentRunnerServerState::new(
|
||||
directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
test_endpoint(token, "steer-false-decision-boot", 30305),
|
||||
);
|
||||
let response = dispatch_external_agent_runner_runtime_request(
|
||||
&ExternalAgentRunnerRequest {
|
||||
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id: "steer-false-decision-request-1".to_string(),
|
||||
token: token.to_string(),
|
||||
method: "runtime.interrupt_for_steer_decision".to_string(),
|
||||
params: ExternalAgentRunnerRequestParams {
|
||||
root: Some(root.to_string_lossy().into_owned()),
|
||||
agent: Some(runtime.agent_id.clone()),
|
||||
run_id: Some(runtime.run_id.clone()),
|
||||
steer_id: Some(steer.steer_id),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
},
|
||||
},
|
||||
&state,
|
||||
);
|
||||
|
||||
assert!(
|
||||
response.ok,
|
||||
"false decision RPC failed: {:?}",
|
||||
response.error
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|value| value["providerInterrupted"].as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert!(!active_provider.interrupted.load(Ordering::Acquire));
|
||||
crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_interrupt_for_true_steer_decision_only_interrupts_older_provider_cursor() {
|
||||
let directory = unique_test_directory();
|
||||
let root = directory.0.join("project");
|
||||
crate::init_local_game_project_at(
|
||||
&root,
|
||||
"project-steer-true-decision-rpc",
|
||||
"Runner steer 条件中断测试",
|
||||
)
|
||||
.expect("initialize true-decision project");
|
||||
let runtime = crate::start_game_creator_agent_runtime_task_for_session_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
None,
|
||||
"按当前方向实现玩法",
|
||||
"run-steer-true-decision-rpc",
|
||||
"agent-background-task",
|
||||
"生成当前方案",
|
||||
vec!["完成当前任务".to_string()],
|
||||
)
|
||||
.expect("start true-decision runtime");
|
||||
let steer = crate::steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
"steer-true-decision-rpc-1",
|
||||
"停止旧方向,改成全新的玩法。",
|
||||
"runner-test",
|
||||
)
|
||||
.expect("persist conflicting steer");
|
||||
crate::persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
&root,
|
||||
&runtime,
|
||||
&steer.steer_id,
|
||||
steer.sequence,
|
||||
crate::AgentRuntimeSteerDecision {
|
||||
reply: "这个改动会让旧方案过期,我会安全切换方向。".to_string(),
|
||||
interrupt_current_provider: true,
|
||||
reason: "当前 Provider 正在生成已经冲突的旧方案。".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("persist interrupting LLM decision");
|
||||
let token = "steer-true-decision-token-steer-true-decision-token";
|
||||
let state = ExternalAgentRunnerServerState::new(
|
||||
directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
test_endpoint(token, "steer-true-decision-boot", 30306),
|
||||
);
|
||||
let request = |request_id: &str| ExternalAgentRunnerRequest {
|
||||
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id: request_id.to_string(),
|
||||
token: token.to_string(),
|
||||
method: "runtime.interrupt_for_steer_decision".to_string(),
|
||||
params: ExternalAgentRunnerRequestParams {
|
||||
root: Some(root.to_string_lossy().into_owned()),
|
||||
agent: Some(runtime.agent_id.clone()),
|
||||
run_id: Some(runtime.run_id.clone()),
|
||||
steer_id: Some(steer.steer_id.clone()),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
},
|
||||
};
|
||||
|
||||
let (old_provider_key, old_provider) =
|
||||
crate::register_game_creator_agent_runtime_provider_request(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
steer.sequence.saturating_sub(1),
|
||||
)
|
||||
.expect("register Provider created before steer");
|
||||
let old_response = dispatch_external_agent_runner_runtime_request(
|
||||
&request("steer-true-decision-old-provider-request"),
|
||||
&state,
|
||||
);
|
||||
assert!(
|
||||
old_response.ok,
|
||||
"old Provider interrupt RPC failed: {:?}",
|
||||
old_response.error
|
||||
);
|
||||
assert_eq!(
|
||||
old_response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|value| value["providerInterrupted"].as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert!(old_provider.interrupted.load(Ordering::Acquire));
|
||||
crate::unregister_game_creator_agent_runtime_provider_request(&old_provider_key, &old_provider);
|
||||
|
||||
let (new_provider_key, new_provider) =
|
||||
crate::register_game_creator_agent_runtime_provider_request(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
steer.sequence,
|
||||
)
|
||||
.expect("register Provider created after steer was applied");
|
||||
let new_response = dispatch_external_agent_runner_runtime_request(
|
||||
&request("steer-true-decision-new-provider-request"),
|
||||
&state,
|
||||
);
|
||||
assert!(
|
||||
new_response.ok,
|
||||
"new Provider keep-running RPC failed: {:?}",
|
||||
new_response.error
|
||||
);
|
||||
assert_eq!(
|
||||
new_response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|value| value["providerInterrupted"].as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert!(
|
||||
!new_provider.interrupted.load(Ordering::Acquire),
|
||||
"a Provider created at the decided steer cursor must keep running"
|
||||
);
|
||||
crate::unregister_game_creator_agent_runtime_provider_request(&new_provider_key, &new_provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run() {
|
||||
let pause_directory = unique_test_directory();
|
||||
|
||||
@@ -7394,362 +7394,6 @@ fn prompt_context_tail_truncation_keeps_latest_blackboard_and_agent_message() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_runtime_steer_waits_for_llm_decision_before_interrupting_old_provider() {
|
||||
let root = unique_project_path();
|
||||
let state = start_agent_runtime_steer_fixture(&root, "steer-provider-run");
|
||||
let (provider_started_tx, provider_started_rx) = tokio::sync::oneshot::channel();
|
||||
let wait_root = root.clone();
|
||||
let wait_agent = state.agent_id.clone();
|
||||
let wait_session = state.session_id.clone();
|
||||
let wait_run = state.run_id.clone();
|
||||
let mut provider_wait = tokio::spawn(async move {
|
||||
await_game_creator_agent_runtime_provider_request(
|
||||
&wait_root,
|
||||
&wait_agent,
|
||||
&wait_session,
|
||||
&wait_run,
|
||||
"tool-plan",
|
||||
"test-interrupt",
|
||||
0,
|
||||
async move {
|
||||
let _ = provider_started_tx.send(());
|
||||
std::future::pending::<Result<(), String>>().await
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
provider_started_rx.await.expect("provider future started");
|
||||
let steer = steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"steer-provider-1",
|
||||
"停止等待旧方案,按新要求重新规划。",
|
||||
"test",
|
||||
)
|
||||
.expect("queue steer during provider wait");
|
||||
assert!(!steer.provider_interrupted);
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(100), &mut provider_wait)
|
||||
.await
|
||||
.is_err(),
|
||||
"queueing steer alone must leave the active Provider request running"
|
||||
);
|
||||
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
&root,
|
||||
&state,
|
||||
"steer-provider-1",
|
||||
steer.sequence,
|
||||
AgentRuntimeSteerDecision {
|
||||
reply: "这个要求会使旧方案过期,我会改向。".to_string(),
|
||||
interrupt_current_provider: true,
|
||||
reason: "旧 Provider 正在生成与新要求冲突的方案。".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("persist LLM steer decision");
|
||||
assert!(
|
||||
interrupt_game_creator_agent_runtime_provider_for_decided_steer_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
"steer-provider-1",
|
||||
)
|
||||
.expect("interrupt stale Provider after LLM decision")
|
||||
);
|
||||
let outcome = tokio::time::timeout(Duration::from_secs(2), &mut provider_wait)
|
||||
.await
|
||||
.expect("provider wait interrupted after decision")
|
||||
.expect("provider wait task joined")
|
||||
.expect("provider wait result");
|
||||
assert!(outcome.is_none());
|
||||
let lifecycle = read_agent_db_records_for_test(&root)
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == state.run_id
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(lifecycle.len(), 2);
|
||||
assert_eq!(lifecycle[0]["status"], "started");
|
||||
assert_eq!(lifecycle[1]["status"], "interrupted");
|
||||
assert_eq!(lifecycle[0]["requestId"], lifecycle[1]["requestId"]);
|
||||
assert!(lifecycle.iter().all(|record| {
|
||||
record["auditSchemaVersion"] == "game-creator-provider-request-lifecycle.v2"
|
||||
&& record["webSearchEnabled"] == false
|
||||
&& record["requestKind"] == "tool-plan"
|
||||
&& record["requestSlot"] == "test-interrupt"
|
||||
&& record.get("prompt").is_none()
|
||||
&& record.get("response").is_none()
|
||||
&& record.get("error").is_none()
|
||||
&& record.get("baseUrl").is_none()
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_supervisor_steer_decision_uses_llm_and_persists_non_terminal_reply() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "supervisor-steer-llm", "总控边做边聊测试")
|
||||
.expect("initialize Supervisor steer decision project");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"完成当前可玩版本",
|
||||
"supervisor-steer-llm-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"正在实现玩法",
|
||||
vec!["实现并验证玩法".to_string()],
|
||||
)
|
||||
.expect("start Supervisor runtime");
|
||||
let steer = steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"supervisor-steer-llm-1",
|
||||
"你现在在做什么?",
|
||||
"test",
|
||||
)
|
||||
.expect("queue Supervisor status question");
|
||||
let arguments = serde_json::json!({
|
||||
"reply": "我正在实现核心玩法,当前任务会继续。",
|
||||
"interruptCurrentProvider": false,
|
||||
"reason": "状态询问不会让当前方案过期。"
|
||||
})
|
||||
.to_string();
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let base_url = spawn_mock_llm_raw_responses_with_capture(
|
||||
vec![native_agent_tool_plan_chat_response(
|
||||
"steer-decision-call",
|
||||
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
||||
arguments,
|
||||
)],
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentMode": "provider",
|
||||
"agentLlm": {{
|
||||
"project-supervisor": {{
|
||||
"apiKey": "steer-decision-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "steer-decision-model",
|
||||
"apiKind": "openai_chat",
|
||||
"stream": false,
|
||||
"maxRetries": 0
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
let decision = decide_game_creator_agent_runtime_steer_at(
|
||||
&root,
|
||||
&state,
|
||||
&steer.steer_id,
|
||||
steer.sequence,
|
||||
"你现在在做什么?",
|
||||
)
|
||||
.await
|
||||
.expect("run Supervisor steer LLM decision");
|
||||
|
||||
assert_eq!(decision.reply, "我正在实现核心玩法,当前任务会继续。");
|
||||
assert!(!decision.interrupt_current_provider);
|
||||
let request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("capture Supervisor steer decision request");
|
||||
assert!(request.contains(AGENT_RUNTIME_STEER_DECISION_TOOL));
|
||||
assert!(request.contains("你现在在做什么?"));
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read public Supervisor steer reply");
|
||||
assert_eq!(
|
||||
project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.content == decision.reply)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_supervisor_steer_decision_failure_keeps_provider_and_persists_fallback_reply() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"supervisor-steer-decision-failure",
|
||||
"总控判定失败继续任务测试",
|
||||
)
|
||||
.expect("initialize Supervisor steer decision failure project");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"完成当前可玩版本",
|
||||
"supervisor-steer-decision-failure-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"正在实现玩法",
|
||||
vec!["实现并验证玩法".to_string()],
|
||||
)
|
||||
.expect("start Supervisor runtime");
|
||||
let (provider_started_tx, provider_started_rx) = tokio::sync::oneshot::channel();
|
||||
let wait_root = root.clone();
|
||||
let wait_agent = state.agent_id.clone();
|
||||
let wait_session = state.session_id.clone();
|
||||
let wait_run = state.run_id.clone();
|
||||
let mut provider_wait = tokio::spawn(async move {
|
||||
await_game_creator_agent_runtime_provider_request(
|
||||
&wait_root,
|
||||
&wait_agent,
|
||||
&wait_session,
|
||||
&wait_run,
|
||||
"tool-plan",
|
||||
"steer-decision-failure-active-provider",
|
||||
0,
|
||||
async move {
|
||||
let _ = provider_started_tx.send(());
|
||||
std::future::pending::<Result<(), String>>().await
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
provider_started_rx.await.expect("provider future started");
|
||||
let steer = steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"supervisor-steer-decision-failure-1",
|
||||
"现在做到哪里了?",
|
||||
"test",
|
||||
)
|
||||
.expect("queue Supervisor steer before failed decision");
|
||||
let base_url = spawn_mock_llm_raw_responses_with_capture(
|
||||
vec![native_agent_tool_plan_chat_response(
|
||||
"unexpected-steer-decision-call",
|
||||
"runtime_execute",
|
||||
"{}".to_string(),
|
||||
)],
|
||||
None,
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentMode": "provider",
|
||||
"agentLlm": {{
|
||||
"project-supervisor": {{
|
||||
"apiKey": "steer-decision-failure-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "steer-decision-failure-model",
|
||||
"apiKind": "openai_chat",
|
||||
"stream": false,
|
||||
"maxRetries": 0
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
let error = decide_game_creator_agent_runtime_steer_at(
|
||||
&root,
|
||||
&state,
|
||||
&steer.steer_id,
|
||||
steer.sequence,
|
||||
"现在做到哪里了?",
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid decision tool must fail closed");
|
||||
let fallback = append_game_creator_agent_runtime_steer_decision_failure_reply_at(
|
||||
&root,
|
||||
&state,
|
||||
&steer.steer_id,
|
||||
&error,
|
||||
)
|
||||
.expect("persist non-terminal decision failure reply");
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(100), &mut provider_wait)
|
||||
.await
|
||||
.is_err(),
|
||||
"failed steer decision must not interrupt the active Provider"
|
||||
);
|
||||
let runtime = read_game_creator_agent_runtime_at(&root, &state.agent_id)
|
||||
.expect("read continuing Supervisor runtime");
|
||||
assert_eq!(runtime.state.run_id, state.run_id);
|
||||
assert!(!matches!(
|
||||
runtime.state.status.as_str(),
|
||||
"completed" | "failed" | "cancelled"
|
||||
));
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read public fallback reply");
|
||||
assert_eq!(
|
||||
project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.content == fallback)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(read_game_creator_agent_runtime_steer_decision_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
&steer.steer_id,
|
||||
)
|
||||
.expect("read absent failed decision")
|
||||
.is_none());
|
||||
|
||||
provider_wait.abort();
|
||||
let _ = provider_wait.await;
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steer_decision_never_interrupts_provider_started_after_steer_was_applied() {
|
||||
let root = unique_project_path();
|
||||
let state = start_agent_runtime_steer_fixture(&root, "steer-new-provider-run");
|
||||
let steer = steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"steer-new-provider-1",
|
||||
"改成新的实现方向。",
|
||||
"test",
|
||||
)
|
||||
.expect("queue steer before new Provider");
|
||||
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
&root,
|
||||
&state,
|
||||
"steer-new-provider-1",
|
||||
steer.sequence,
|
||||
AgentRuntimeSteerDecision {
|
||||
reply: "我会按新方向继续。".to_string(),
|
||||
interrupt_current_provider: true,
|
||||
reason: "旧方向已经过期。".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("persist interrupt decision");
|
||||
let (key, active) = register_game_creator_agent_runtime_provider_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
steer.sequence,
|
||||
)
|
||||
.expect("register Provider created after steer application");
|
||||
|
||||
assert!(
|
||||
!interrupt_game_creator_agent_runtime_provider_for_decided_steer_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
"steer-new-provider-1",
|
||||
)
|
||||
.expect("keep new Provider running")
|
||||
);
|
||||
assert!(!active.interrupted.load(Ordering::Acquire));
|
||||
unregister_game_creator_agent_runtime_provider_request(&key, &active);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_runtime_durable_cancel_after_provider_registration_starts_no_request() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -5061,107 +5061,6 @@ fn agent_runtime_steer_is_idempotent_rejects_conflicts_and_enforces_capacity() {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_supervisor_steer_persists_one_correlated_public_acknowledgement() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-steer-public-ack", "总控追加消息确认测试")
|
||||
.expect("initialize Supervisor steer fixture");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"制作一个可玩的游戏",
|
||||
"supervisor-steer-public-ack-run",
|
||||
"project-supervisor-gui",
|
||||
"正在制作游戏",
|
||||
vec!["完成可玩版本".to_string()],
|
||||
)
|
||||
.expect("start root Supervisor runtime");
|
||||
|
||||
for _ in 0..2 {
|
||||
steer_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
"supervisor-steer-public-ack-1",
|
||||
"你在做什么?",
|
||||
"test",
|
||||
)
|
||||
.expect("queue idempotent Supervisor steer");
|
||||
}
|
||||
for _ in 0..2 {
|
||||
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
||||
&root,
|
||||
&state,
|
||||
"supervisor-steer-public-ack-1",
|
||||
1,
|
||||
AgentRuntimeSteerDecision {
|
||||
reply: "我正在完成玩法实现;这是状态询问,当前任务会继续。".to_string(),
|
||||
interrupt_current_provider: false,
|
||||
reason: "状态询问不需要中断当前 Provider。".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("persist idempotent Supervisor steer reply");
|
||||
}
|
||||
|
||||
let supervisor_conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(&state.agent_id),
|
||||
Some(&state.session_id),
|
||||
)
|
||||
.expect("read Supervisor steer conversation");
|
||||
let steer_message = supervisor_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.find(|message| message.role == "user" && message.content == "你在做什么?")
|
||||
.expect("find persisted steer user message");
|
||||
let correlation_id = steer_message
|
||||
.message_id
|
||||
.as_deref()
|
||||
.and_then(|message_id| message_id.strip_prefix("agent-steer-"))
|
||||
.expect("read steer correlation id");
|
||||
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read public steer acknowledgement");
|
||||
let acknowledgements = project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.content == "收到。我正在判断这条消息是否需要调整当前任务;现有任务会继续运行。"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(acknowledgements.len(), 1);
|
||||
assert!(acknowledgements[0]
|
||||
.message_id
|
||||
.as_deref()
|
||||
.is_some_and(|message_id| {
|
||||
message_id.starts_with(&format!(
|
||||
"{AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX}{correlation_id}-"
|
||||
))
|
||||
}));
|
||||
assert_eq!(
|
||||
project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.content == "我正在完成玩法实现;这是状态询问,当前任务会继续。"
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let decision = read_game_creator_agent_runtime_steer_decision_at(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
"supervisor-steer-public-ack-1",
|
||||
)
|
||||
.expect("read persisted Supervisor steer decision")
|
||||
.expect("Supervisor steer decision exists");
|
||||
assert!(!decision.interrupt_current_provider);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_runtime_steer_sequences_are_unique_under_concurrent_acceptance() {
|
||||
let root = unique_project_path();
|
||||
|
||||
Reference in New Issue
Block a user