Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs
T
kdletters 54d0fb75ea 接入 Godot 编辑器插件与受控执行链路
新增 GDExtension 自动引导、GDScript 执行和实例隔离缓存
接入 AGC 插件开关、Runner、Agent 工具与权限审计
完善执行回执确认、不确定状态阻断及卸载恢复
补齐 Windows 分发资源、定向测试与实机验收文档
2026-09-20 16:35:04 +08:00

1422 lines
55 KiB
Rust

use super::{endpoint::*, project_owner::*, protocol::*, state::*};
use crate::{
register_game_creator_manifest_invalidation_event_sink,
validate_game_creator_manifest_invalidation_event_sink,
};
use serde::Deserialize;
use serde_json::json;
use sha2::{Digest as _, Sha256};
use std::fs;
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub(super) fn read_external_agent_runner_frame<R: Read>(
reader: &mut R,
) -> Result<Vec<u8>, ExternalAgentRunnerFrameError> {
let mut prefix = [0_u8; 4];
reader.read_exact(&mut prefix)?;
let length = u32::from_be_bytes(prefix);
if length as usize > EXTERNAL_AGENT_RUNNER_MAX_FRAME_BYTES {
return Err(ExternalAgentRunnerFrameError::Oversize(length));
}
let mut payload = vec![0_u8; length as usize];
reader.read_exact(&mut payload)?;
Ok(payload)
}
pub(super) fn write_external_agent_runner_frame<W: Write>(
writer: &mut W,
payload: &[u8],
) -> Result<(), ExternalAgentRunnerFrameError> {
if payload.len() > EXTERNAL_AGENT_RUNNER_MAX_FRAME_BYTES {
let reported = u32::try_from(payload.len()).unwrap_or(u32::MAX);
return Err(ExternalAgentRunnerFrameError::Oversize(reported));
}
let length = u32::try_from(payload.len())
.map_err(|_| ExternalAgentRunnerFrameError::Oversize(u32::MAX))?;
writer.write_all(&length.to_be_bytes())?;
writer.write_all(payload)?;
Ok(())
}
pub(super) fn valid_external_agent_runner_request_id(request_id: &str) -> bool {
!request_id.is_empty()
&& request_id.len() <= 128
&& request_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
}
pub(super) fn external_agent_runner_request_fingerprint(
request: &ExternalAgentRunnerRequest,
) -> String {
let params = serde_json::to_vec(&request.params).unwrap_or_default();
let mut digest = Sha256::new();
digest.update(request.protocol_version.to_be_bytes());
digest.update(request.method.as_bytes());
digest.update([0]);
digest.update(params);
hex_encode(&digest.finalize())
}
pub(super) fn external_agent_runner_request_root(
request: &ExternalAgentRunnerRequest,
) -> Result<PathBuf, String> {
let root = request
.params
.root
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "Runtime 请求缺少 root".to_string())?;
let root = PathBuf::from(root);
if !root.is_absolute() {
return Err("Runtime 请求 root 必须是绝对路径".to_string());
}
Ok(root)
}
pub(super) fn external_agent_runner_request_agent(
request: &ExternalAgentRunnerRequest,
) -> Result<String, String> {
let agent = request
.params
.agent
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "Runtime 请求缺少 agent".to_string())?;
if agent.len() > 256 {
return Err("Runtime 请求 agent 过长".to_string());
}
Ok(agent.to_string())
}
pub(super) fn apply_external_agent_runner_gui_owner_platform_session(
state: &ExternalAgentRunnerServerState,
params: &ExternalAgentRunnerRequestParams,
) -> Result<(), String> {
apply_external_agent_runner_gui_owner_attachment(state, params, None)
}
fn apply_external_agent_runner_gui_owner_attachment(
state: &ExternalAgentRunnerServerState,
params: &ExternalAgentRunnerRequestParams,
event_sink: Option<crate::GameCreatorManifestInvalidationEventSink>,
) -> Result<(), String> {
let requested_epoch = params
.gui_owner_epoch
.as_deref()
.filter(|value| uuid::Uuid::parse_str(value).is_ok())
.ok_or_else(|| "Agent Runner GUI owner 缺少有效 owner epoch".to_string())?;
let requested_revision = params
.gui_owner_session_revision
.ok_or_else(|| "Agent Runner GUI owner 缺少 session revision".to_string())?;
let config_dir = state
.gui_participant_lock_path
.parent()
.ok_or_else(|| "AGC 界面参与锁缺少 AppData 父目录".to_string())?;
let mut active_claim = lock_unpoisoned(&state.gui_owner_platform_session_claim);
let durable_claim = read_external_agent_runner_gui_owner_claim(config_dir)?;
if durable_claim.owner_epoch != requested_epoch
|| durable_claim.session_revision != requested_revision
{
return Err("Agent Runner GUI owner claim 已过期".to_string());
}
let requested_claim = (requested_epoch.to_string(), requested_revision);
// 同一 AppData 的多个窗口共享同一个 epoch:只有 epoch 变化(本窗口发布了新的
// 登录态权威)才允许强制替换会话。同一 epoch 内的重复 attach 只做单调校验,
// 因此后开窗口的“无登录态 attach”不会清空已有会话。
let epoch_changed = match active_claim.as_ref() {
Some(active) => active.0 != requested_epoch,
None => true,
};
let replace_claim = epoch_changed;
let result = match (
params.platform_user_id.as_deref(),
params.platform_access_token.as_deref(),
params.platform_api_base_url.as_deref(),
params.platform_auth_generation,
params.platform_auth_revision,
) {
(
Some(user_id),
Some(access_token),
Some(api_base_url),
Some(identity_generation),
Some(revision),
) => {
if replace_claim {
crate::replace_platform_session_for_gui_owner(
user_id,
access_token,
api_base_url,
identity_generation,
revision,
)
} else {
crate::install_platform_session_checked(
user_id,
access_token,
api_base_url,
identity_generation,
revision,
)
}
}
(None, None, None, Some(identity_generation), Some(revision)) => {
if replace_claim {
crate::clear_platform_session_for_gui_owner(identity_generation, revision);
Ok(())
} else {
crate::clear_platform_session_checked(identity_generation, revision)
}
}
(None, None, None, None, None) if epoch_changed => {
crate::clear_platform_session_for_gui_owner(0, 0);
Ok(())
}
(None, None, None, None, None) => Ok(()),
_ => Err("Agent Runner GUI owner 的平台登录态同步参数不完整".to_string()),
};
result?;
let committed_claim = match read_external_agent_runner_gui_owner_claim(config_dir) {
Ok(claim) => claim,
Err(error) => {
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0, 0);
return Err(format!(
"Agent Runner GUI owner claim 在 attach 提交期间无法核验,平台登录态已隔离:{error}"
));
}
};
if committed_claim.owner_epoch != requested_epoch
|| committed_claim.session_revision != requested_revision
{
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0, 0);
return Err("Agent Runner GUI owner claim 在 attach 提交期间已变化".to_string());
}
if let Some(event_sink) = event_sink {
register_game_creator_manifest_invalidation_event_sink(event_sink);
}
*active_claim = Some(requested_claim);
Ok(())
}
pub(super) fn validate_external_agent_runner_gui_owner_claim_current(
state: &ExternalAgentRunnerServerState,
) -> Result<(), String> {
let config_dir = state
.gui_participant_lock_path
.parent()
.ok_or_else(|| "AGC 界面参与锁缺少 AppData 父目录".to_string())?;
let mut active_claim = lock_unpoisoned(&state.gui_owner_platform_session_claim);
let durable_claim = read_external_agent_runner_gui_owner_claim(config_dir);
let matches = durable_claim.as_ref().is_ok_and(|claim| {
active_claim.as_ref() == Some(&(claim.owner_epoch.clone(), claim.session_revision))
});
if matches {
return Ok(());
}
*active_claim = None;
crate::clear_platform_session_for_gui_owner(0, 0);
match durable_claim {
Ok(_) => Err(
"authentication-required: Agent Runner GUI owner claim 已变化,平台登录态已隔离"
.to_string(),
),
Err(error) => Err(format!(
"authentication-required: Agent Runner GUI owner claim 无法核验,平台登录态已隔离:{error}"
)),
}
}
fn external_agent_runner_method_requires_current_gui_owner_claim(method: &str) -> bool {
method.starts_with("runtime.")
|| method.starts_with("unity.editor.")
|| method.starts_with("godot.editor.")
}
pub(super) fn external_agent_runner_request_session_id(
request: &ExternalAgentRunnerRequest,
) -> Result<Option<String>, String> {
let Some(session_id) = request
.params
.session_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(None);
};
if session_id.len() > 256 {
return Err("Runtime 请求 sessionId 过长".to_string());
}
Ok(Some(session_id.to_string()))
}
pub(super) fn external_agent_runner_request_run_id(
request: &ExternalAgentRunnerRequest,
) -> Result<String, String> {
let run_id = request
.params
.run_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "Runtime 请求缺少 runId".to_string())?;
if run_id.len() > 256 {
return Err("Runtime 请求 runId 过长".to_string());
}
Ok(run_id.to_string())
}
pub(super) fn external_agent_runner_request_action_id(
request: &ExternalAgentRunnerRequest,
) -> Result<String, String> {
let action_id = request
.params
.action_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "runtime.continue_action 请求缺少 actionId".to_string())?;
if action_id.len() > 256 {
return Err("runtime.continue_action actionId 过长".to_string());
}
Ok(action_id.to_string())
}
pub(super) fn external_agent_runner_request_steer_id(
request: &ExternalAgentRunnerRequest,
) -> Result<String, String> {
let steer_id = request
.params
.steer_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "runtime.steer 请求缺少 steerId".to_string())?;
if steer_id.len() > 256 {
return Err("runtime.steer steerId 过长".to_string());
}
Ok(steer_id.to_string())
}
pub(super) fn external_agent_runner_request_wake_target(
request: &ExternalAgentRunnerRequest,
) -> Result<Option<(String, String)>, String> {
match (&request.params.agent, &request.params.run_id) {
(None, None) => Ok(None),
(Some(_), Some(_)) => Ok(Some((
external_agent_runner_request_agent(request)?,
external_agent_runner_request_run_id(request)?,
))),
_ => Err("runtime.wake_pending 定向请求必须同时提供 agent 和 runId".to_string()),
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct ExternalAgentRunnerTargetRunProbe {
pub(super) agent_id: String,
pub(super) run_id: String,
pub(super) status: String,
pub(super) phase: String,
}
impl ExternalAgentRunnerTargetRunProbe {
pub(super) fn matches(&self, agent_id: &str, run_id: &str) -> bool {
self.agent_id == agent_id && self.run_id == run_id
}
pub(super) fn still_requires_wake(&self) -> bool {
self.status == "pending"
|| (self.status == "running"
&& matches!(
self.phase.as_str(),
"waiting-for-delegate-receipts" | "waiting-for-provider-retry"
))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ExternalAgentRunnerTargetWakeRetry {
NotObserved,
StillPending,
}
pub(super) fn external_agent_runner_target_run_probe(
runtime: &crate::AgentRuntimeResult,
agent_id: &str,
run_id: &str,
) -> Option<ExternalAgentRunnerTargetRunProbe> {
runtime
.recent_tasks
.iter()
.rev()
.find(|task| task.agent_id == agent_id && task.run_id == run_id)
.map(|task| ExternalAgentRunnerTargetRunProbe {
agent_id: task.agent_id.clone(),
run_id: task.run_id.clone(),
status: task.status.clone(),
phase: task.phase.clone(),
})
.or_else(|| {
let state = &runtime.state;
(state.agent_id == agent_id && state.run_id == run_id).then(|| {
ExternalAgentRunnerTargetRunProbe {
agent_id: state.agent_id.clone(),
run_id: state.run_id.clone(),
status: state.status.clone(),
phase: state.phase.clone(),
}
})
})
}
pub(super) fn classify_external_agent_runner_target_wake(
agent_id: &str,
run_id: &str,
scan_probes: &[ExternalAgentRunnerTargetRunProbe],
current_probe: Option<&ExternalAgentRunnerTargetRunProbe>,
) -> Result<(), ExternalAgentRunnerTargetWakeRetry> {
let scan_probe = scan_probes
.iter()
.find(|probe| probe.matches(agent_id, run_id));
if scan_probe.is_some_and(|probe| !probe.still_requires_wake())
|| current_probe
.is_some_and(|probe| probe.matches(agent_id, run_id) && !probe.still_requires_wake())
{
return Ok(());
}
if scan_probe.is_some() || current_probe.is_some_and(|probe| probe.matches(agent_id, run_id)) {
Err(ExternalAgentRunnerTargetWakeRetry::StillPending)
} else {
Err(ExternalAgentRunnerTargetWakeRetry::NotObserved)
}
}
pub(super) fn external_agent_runner_target_wake_retryable_response(
request_id: &str,
retry: ExternalAgentRunnerTargetWakeRetry,
) -> ExternalAgentRunnerResponse {
let message = match retry {
ExternalAgentRunnerTargetWakeRetry::NotObserved => {
"定向 wake 暂时未观察到目标 run,请复用同一 requestId 重试"
}
ExternalAgentRunnerTargetWakeRetry::StillPending => {
"目标 run 仍在等待推进,execution lane 可能正在占用,请复用同一 requestId 重试"
}
};
ExternalAgentRunnerResponse::failure(
request_id,
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE,
message,
)
}
pub(super) fn external_agent_runner_target_wake_error_is_retryable(error: &str) -> bool {
let normalized = error.to_ascii_lowercase();
error.contains("正在")
|| error.contains("暂时")
|| normalized.contains("would block")
|| normalized.contains("timed out")
|| normalized.contains("timeout")
|| normalized.contains("sharing violation")
}
pub(super) fn dispatch_external_agent_runner_wake_pending_request(
request: &ExternalAgentRunnerRequest,
root: &Path,
token: &str,
) -> ExternalAgentRunnerResponse {
let target = match external_agent_runner_request_wake_target(request) {
Ok(target) => target,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
};
let resumed = match crate::wake_pending_game_creator_agent_background_tasks_at(root) {
Ok(resumed) => resumed,
Err(error) => {
let error = redact_runner_secret(&error, token);
if target.is_some() && external_agent_runner_target_wake_error_is_retryable(&error) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE,
format!("定向 wake 暂时无法完成,请复用同一 requestId 重试:{error}"),
);
}
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-error",
error,
);
}
};
let Some((agent_id, run_id)) = target else {
return ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true }),
);
};
let scan_probes = resumed
.iter()
.filter_map(|runtime| external_agent_runner_target_run_probe(runtime, &agent_id, &run_id))
.collect::<Vec<_>>();
if classify_external_agent_runner_target_wake(&agent_id, &run_id, &scan_probes, None).is_ok() {
return ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true }),
);
}
let current = match crate::read_game_creator_agent_runtime_at(root, &agent_id) {
Ok(current) => current,
Err(error) => {
let error = redact_runner_secret(&error, token);
if external_agent_runner_target_wake_error_is_retryable(&error) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE,
format!("定向 wake 后暂时无法确认目标 run,请复用同一 requestId 重试:{error}"),
);
}
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-error",
error,
);
}
};
let current_probe = external_agent_runner_target_run_probe(&current, &agent_id, &run_id);
match classify_external_agent_runner_target_wake(
&agent_id,
&run_id,
&scan_probes,
current_probe.as_ref(),
) {
Ok(()) => {
ExternalAgentRunnerResponse::success(&request.request_id, json!({ "accepted": true }))
}
Err(retry) => {
external_agent_runner_target_wake_retryable_response(&request.request_id, retry)
}
}
}
pub(super) fn external_agent_runner_response_is_cacheable(
response: &ExternalAgentRunnerResponse,
) -> bool {
response.error.as_ref().is_none_or(|error| {
!matches!(
error.code.as_str(),
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE | "project-execution-owned"
)
})
}
pub(super) fn cache_external_agent_runner_response_if_cacheable(
cache: &mut ExternalAgentRunnerRequestCache,
request_id: &str,
fingerprint: &str,
response: &ExternalAgentRunnerResponse,
) {
if external_agent_runner_response_is_cacheable(response) {
cache.insert(
request_id.to_string(),
fingerprint.to_string(),
response.clone(),
);
}
}
pub(super) fn dispatch_external_agent_runner_runtime_request(
request: &ExternalAgentRunnerRequest,
state: &ExternalAgentRunnerServerState,
) -> ExternalAgentRunnerResponse {
dispatch_external_agent_runner_runtime_request_with_owner_claim(request, state, |root| {
state.claim_project_execution_owner(root)
})
}
pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
request: &ExternalAgentRunnerRequest,
state: &ExternalAgentRunnerServerState,
claim_owner: impl Fn(&Path) -> Result<ExternalAgentRunnerProjectExecutionOwnerClaim, String>,
) -> ExternalAgentRunnerResponse {
let fingerprint = external_agent_runner_request_fingerprint(request);
let use_request_cache = request.method != "runner.attach_gui_owner";
if use_request_cache {
let cache = lock_unpoisoned(&state.write_request_cache);
if let Some(cached) = cache.find(&request.request_id) {
if cached.fingerprint == fingerprint {
return cached.response.clone();
}
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"request-id-conflict",
"同一 requestId 不能用于不同请求",
);
}
}
let requires_project_execution_owner = matches!(
request.method.as_str(),
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
| "runtime.steer"
| "runtime.interrupt_for_steer_decision"
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact"
);
if requires_project_execution_owner && state.draining.load(Ordering::Acquire) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 正在排空并准备退出,拒绝新的写请求",
);
}
let token = state.endpoint_snapshot().token;
let claimed_project_root = if requires_project_execution_owner {
let root = match external_agent_runner_request_root(request) {
Ok(root) => root,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
};
match claim_owner(&root) {
Ok(claim) => Some(claim.root),
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"project-execution-owned",
error,
);
}
}
} else {
None
};
let mut cache = lock_unpoisoned(&state.write_request_cache);
if use_request_cache {
if let Some(cached) = cache.find(&request.request_id) {
if cached.fingerprint == fingerprint {
return cached.response.clone();
}
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"request-id-conflict",
"同一 requestId 不能用于不同请求",
);
}
}
let response = match request.method.as_str() {
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
| "runtime.steer"
| "runtime.interrupt_for_steer_decision"
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact" => {
let root = claimed_project_root
.expect("runtime write request must claim its project execution owner");
if request.method == "runtime.wake_pending" {
dispatch_external_agent_runner_wake_pending_request(request, &root, &token)
} else {
let result = match request.method.as_str() {
"runtime.resume" => crate::resume_game_creator_agent_background_tasks_at(&root)
.map(|_| json!({ "accepted": true }))
.map_err(|error| error.to_string()),
"runtime.compact" => (|| {
let agent = external_agent_runner_request_agent(request)?;
let session_id = external_agent_runner_request_session_id(request)?;
let result = tauri::async_runtime::block_on(
crate::compact_game_creator_agent_runtime_session_at(
&root,
&agent,
session_id.as_deref(),
),
)?;
serde_json::to_value(result)
.map_err(|error| format!("序列化上下文压缩结果失败:{error}"))
})(),
"runtime.continue_action" => (|| {
let agent = external_agent_runner_request_agent(request)?;
let run_id = external_agent_runner_request_run_id(request)?;
let action_id = external_agent_runner_request_action_id(request)?;
crate::resume_game_creator_agent_pending_action_for_agent_at(
&root, &agent, &run_id, &action_id,
)
.map(|_| json!({ "accepted": true }))
.map_err(|error| error.to_string())
})(),
"runtime.steer" => (|| {
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)?;
crate::validate_game_creator_agent_runtime_steer_notification_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": 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)?;
let runtime = crate::read_game_creator_agent_runtime_at(&root, &agent)?;
if runtime.state.run_id != run_id {
return Err("runtime.pause 与当前 Agent runId 不匹配".to_string());
}
let goal = crate::read_game_creator_agent_goal_at(
&root,
&agent,
&runtime.state.session_id,
)?
.ok_or_else(|| "runtime.pause 未找到当前 Session Goal".to_string())?;
if goal.run_id != run_id
|| goal.status != crate::AGENT_GOAL_STATUS_PAUSE_REQUESTED
{
return Err(
"runtime.pause 缺少精确的 durable pause request".to_string()
);
}
let provider_interrupted =
crate::interrupt_game_creator_agent_runtime_provider_request_at(
&root, &agent, &run_id,
)?;
let runtime =
crate::pause_game_creator_agent_runtime_for_goal_at(&root, &goal)?;
Ok(json!({
"accepted": true,
"providerInterrupted": provider_interrupted,
"status": runtime.state.status,
"phase": runtime.state.phase,
}))
})(),
"runtime.cancel" => (|| {
let agent = external_agent_runner_request_agent(request)?;
let run_id = external_agent_runner_request_run_id(request)?;
let runtime = crate::read_game_creator_agent_runtime_at(&root, &agent)?;
if runtime.state.run_id != run_id {
return Err("runtime.cancel 与当前 Agent runId 不匹配".to_string());
}
let goal = crate::read_game_creator_agent_goal_at(
&root,
&agent,
&runtime.state.session_id,
)?
.ok_or_else(|| "runtime.cancel 未找到当前 Session Goal".to_string())?;
if goal.run_id != run_id || goal.status != crate::AGENT_GOAL_STATUS_CLEARING
{
return Err(
"runtime.cancel 缺少精确的 durable Goal clear request".to_string()
);
}
crate::write_game_creator_agent_runtime_cancel_request(
&root,
&agent,
&run_id,
"开发者清理持久 Goal",
)?;
let provider_interrupted =
crate::interrupt_game_creator_agent_runtime_provider_request_at(
&root, &agent, &run_id,
)?;
let runtime = crate::cancel_game_creator_agent_runtime_task_at(
&root, &agent, &run_id,
)?;
Ok(json!({
"accepted": true,
"providerInterrupted": provider_interrupted,
"status": runtime.state.status,
"phase": runtime.state.phase,
}))
})(),
_ => unreachable!(),
};
match result {
Ok(result) => ExternalAgentRunnerResponse::success(&request.request_id, result),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-error",
redact_external_agent_runner_runtime_error(&root, &error, &token),
),
}
}
}
"runner.attach_gui_owner" => {
match external_agent_runner_lock_is_held(&state.gui_participant_lock_path) {
Ok(true) => {
let event_sink = request
.params
.event_sink_port
.zip(request.params.event_sink_token.as_deref())
.ok_or_else(|| {
"Agent Runner GUI owner 缺少 manifest 事件接收端".to_string()
})
.and_then(|(port, token)| {
validate_game_creator_manifest_invalidation_event_sink(port, token)
});
let event_sink = match event_sink {
Ok(event_sink) => event_sink,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"event-sink-invalid",
redact_runner_secret(&error, &token),
);
}
};
if let Err(error) = apply_external_agent_runner_gui_owner_attachment(
state,
&request.params,
Some(event_sink),
) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"platform-session-invalid",
error,
);
}
state.gui_owner_attached.store(true, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "attached": true, "eventSinkAttached": true }),
)
}
Ok(false) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"gui-owner-missing",
"Agent Runner 未检测到活跃的 AGC 界面进程",
),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"gui-owner-unreadable",
redact_runner_secret(&error, &token),
),
}
}
"platform.session.install" | "platform.session.clear" => {
ExternalAgentRunnerResponse::failure(
&request.request_id,
"platform-session-epoch-required",
"平台登录态只能通过当前 GUI owner epoch 的 runner.attach_gui_owner 同步",
)
}
"runner.shutdown" | "shutdown" => {
let provider_requests_interrupted =
request_external_agent_runner_forced_shutdown(state);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({
"accepted": true,
"willShutdown": true,
"providerRequestsInterrupted": provider_requests_interrupted,
}),
)
}
"runner.shutdown_for_client_exit" if cfg!(test) => {
if state.shutdown_requested.load(Ordering::Acquire) {
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "busy": false, "willShutdown": true }),
)
} else if state
.draining
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 已在排空",
)
} else if state.active_connections.load(Ordering::Acquire) > 1 {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": false, "busy": true, "willShutdown": false }),
)
} else {
match external_agent_runner_known_roots_are_idle(state) {
Ok(false) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": false, "busy": true, "willShutdown": false }),
)
}
Ok(true) => {
state.shutdown_requested.store(true, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "accepted": true, "busy": false, "willShutdown": true }),
)
}
Err(error) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-state-unreadable",
redact_runner_secret(&error, &token),
)
}
}
}
}
"runner.shutdown_if_idle" | "shutdown_if_idle" => {
if request.params.root.is_some() {
match external_agent_runner_request_root(request) {
Ok(root) => match canonicalize_external_agent_runner_project_root(&root) {
Ok(root) => state.remember_root(&root),
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
},
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
}
}
if state
.draining
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 已在排空",
)
} else if state.active_connections.load(Ordering::Acquire) > 1 {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "idle": false, "willShutdown": false }),
)
} else {
match external_agent_runner_known_roots_are_idle(state) {
Ok(false) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "idle": false, "willShutdown": false }),
)
}
Ok(true) => {
state.shutdown_requested.store(true, Ordering::Release);
ExternalAgentRunnerResponse::success(
&request.request_id,
json!({ "idle": true, "willShutdown": true }),
)
}
Err(error) => {
state.draining.store(false, Ordering::Release);
ExternalAgentRunnerResponse::failure(
&request.request_id,
"runtime-state-unreadable",
redact_runner_secret(&error, &token),
)
}
}
}
}
_ => ExternalAgentRunnerResponse::failure(
&request.request_id,
"method-not-found",
"Agent Runner 不支持该方法",
),
};
if use_request_cache {
cache_external_agent_runner_response_if_cacheable(
&mut cache,
&request.request_id,
&fingerprint,
&response,
);
}
response
}
pub(super) fn handle_external_agent_runner_request(
request: ExternalAgentRunnerRequest,
state: &ExternalAgentRunnerServerState,
) -> ExternalAgentRunnerResponse {
if !valid_external_agent_runner_request_id(&request.request_id) {
return ExternalAgentRunnerResponse::failure(
"",
"invalid-request-id",
"Agent Runner requestId 无效",
);
}
if request.protocol_version != EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"protocol-version-mismatch",
"Agent Runner 协议版本不兼容",
);
}
let expected_token = state.endpoint_snapshot().token;
if !constant_time_eq(request.token.as_bytes(), expected_token.as_bytes()) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"unauthorized",
"Agent Runner 请求未授权",
);
}
if request.method.len() > 128 {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-method",
"Agent Runner method 无效",
);
}
if state.gui_owner_attached.load(Ordering::Acquire)
&& external_agent_runner_method_requires_current_gui_owner_claim(&request.method)
{
if let Err(error) = validate_external_agent_runner_gui_owner_claim_current(state) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"platform-session-claim-stale",
redact_runner_secret(&error, &expected_token),
);
}
}
match request.method.as_str() {
// 编辑器使用自身的有界并发门闩;不能持有 Runtime 全局写请求缓存锁等待 编辑器。
"unity.editor.rpc" | "godot.editor.rpc" => {
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
.expect("matched editor RPC");
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct EditorCall {
method: String,
params: serde_json::Value,
#[serde(default)]
deadline_ms: Option<u64>,
}
let result = (|| {
if state.draining.load(Ordering::Acquire) {
return Err("Agent Runner 正在退出".to_string());
}
let call: EditorCall = serde_json::from_value(
request.params.editor_rpc.clone().ok_or("缺少 editorRpc")?,
)
.map_err(|_| "编辑器 RPC 参数无效".to_string())?;
if call
.deadline_ms
.is_some_and(|deadline| deadline <= unix_millis())
{
return Err("编辑器 RPC 派发期限已过,未发送执行".to_string());
}
crate::editor_adapters::managed_editor_rpc_owned(
editor,
&call.method,
call.params,
Some(&request.request_id),
)
})();
match result {
Ok(mut value) => {
if request
.params
.editor_rpc
.as_ref()
.and_then(|value| value["method"].as_str())
== Some("execute")
{
if value.get("ackRequired").is_none() {
value["ackRequired"] = json!(false);
}
}
ExternalAgentRunnerResponse::success(&request.request_id, value)
}
Err(error)
if request
.params
.editor_rpc
.as_ref()
.and_then(|value| value["method"].as_str())
== Some("execute") =>
{
let mut value = crate::editor_adapters::editor_not_dispatched(&error);
value["ackRequired"] = json!(false);
ExternalAgentRunnerResponse::success(&request.request_id, value)
}
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"editor-rpc-failed",
error,
),
}
}
"unity.editor.ack" | "godot.editor.ack" => {
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
.expect("matched editor ACK");
let result = request
.params
.editor_rpc
.as_ref()
.and_then(|value| value["requestId"].as_str())
.ok_or_else(|| "缺少 编辑器 回执身份".to_string())
.and_then(|id| crate::editor_adapters::acknowledge_editor_delivery(editor, id));
match result {
Ok(()) => ExternalAgentRunnerResponse::success(
&request.request_id,
json!({"acknowledged":true}),
),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"editor-ack-failed",
error,
),
}
}
"unity.editor.mark_uncertain" | "godot.editor.mark_uncertain" => {
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
.expect("matched editor uncertain RPC");
match crate::editor_adapters::mark_editor_execution_uncertain(editor) {
Ok(()) => ExternalAgentRunnerResponse::success(
&request.request_id,
json!({"status":"needs-reconciliation"}),
),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"editor-mark-failed",
error,
),
}
}
"runner.ping" => ExternalAgentRunnerResponse::success(
&request.request_id,
json!({
"status": "ok",
"pid": std::process::id(),
"bootId": state.endpoint_snapshot().boot_id,
}),
),
"runner.status" => match serde_json::to_value(state.public_status()) {
Ok(status) => ExternalAgentRunnerResponse::success(&request.request_id, status),
Err(_) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"status-serialization-failed",
"序列化 Agent Runner 状态失败",
),
},
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
| "runtime.steer"
| "runtime.interrupt_for_steer_decision"
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact"
| "runner.attach_gui_owner"
| "platform.session.install"
| "platform.session.clear"
| "runner.shutdown"
| "shutdown"
| "runner.shutdown_for_client_exit"
| "runner.shutdown_if_idle"
| "shutdown_if_idle" => dispatch_external_agent_runner_runtime_request(&request, state),
_ => ExternalAgentRunnerResponse::failure(
&request.request_id,
"method-not-found",
"Agent Runner 不支持该方法",
),
}
}
pub(super) fn request_external_agent_runner_forced_shutdown(
state: &ExternalAgentRunnerServerState,
) -> usize {
state.draining.store(true, Ordering::Release);
let roots = state.known_roots_snapshot();
let provider_requests_interrupted =
crate::interrupt_game_creator_agent_runtime_provider_requests_for_roots(&roots);
crate::shutdown_all_process_sessions();
state
.force_shutdown_requested
.store(true, Ordering::Release);
state.shutdown_requested.store(true, Ordering::Release);
provider_requests_interrupted
}
pub(super) fn external_agent_runner_runtime_state_is_idle(status: &str, phase: &str) -> bool {
if matches!(phase, "completed" | "cancelled" | "failed" | "paused") {
return true;
}
matches!(status, "idle" | "failed" | "cancelled" | "paused")
}
#[derive(Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerTaskQueueProbe {
#[serde(default)]
pub(super) pending: u64,
#[serde(default, alias = "waiting")]
pub(super) waiting_for_confirmation: u64,
#[serde(default)]
pub(super) waiting_for_user_input: u64,
#[serde(default)]
pub(super) running: u64,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ExternalAgentRunnerRuntimeStateProbe {
#[serde(default)]
pub(super) status: String,
#[serde(default)]
pub(super) phase: String,
#[serde(default)]
pub(super) task_queue: ExternalAgentRunnerTaskQueueProbe,
}
pub(super) fn external_agent_runner_directory_has_durable_files(
path: &Path,
) -> Result<bool, String> {
let entries = match fs::read_dir(path) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(format!(
"读取 Agent Runtime durable 目录失败:{}: {error}",
path.display()
));
}
};
for entry in entries {
let entry = entry.map_err(|error| {
format!(
"读取 Agent Runtime durable 目录项失败:{}: {error}",
path.display()
)
})?;
let file_type = entry.file_type().map_err(|error| {
format!(
"读取 Agent Runtime durable 项类型失败:{}: {error}",
entry.path().display()
)
})?;
if file_type.is_symlink() {
return Err(format!(
"Agent Runtime durable 目录不允许符号链接:{}",
entry.path().display()
));
}
if file_type.is_file()
|| (file_type.is_dir()
&& external_agent_runner_directory_has_durable_files(&entry.path())?)
{
return Ok(true);
}
}
Ok(false)
}
pub(super) fn external_agent_runner_root_is_idle(root: &Path) -> Result<bool, String> {
if crate::has_active_process_sessions_at(root)? {
return Ok(false);
}
for durable_dir in [
root.join(".agent/runtime/pending-actions"),
root.join(".agent/runtime/finalizations"),
root.join(".agent/runtime/provider-handoffs"),
root.join(".agent/runtime/provider-retries"),
root.join(".agent/runtime/tool-plan-handoffs"),
] {
if external_agent_runner_directory_has_durable_files(&durable_dir)? {
return Ok(false);
}
}
let agents_dir = root.join(".agent/runtime/agents");
let entries = match fs::read_dir(&agents_dir) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(true),
Err(error) => {
return Err(format!(
"读取 Agent Runtime 状态目录失败:{}: {error}",
agents_dir.display()
));
}
};
for entry in entries {
let entry = entry.map_err(|error| {
format!(
"读取 Agent Runtime 状态目录项失败:{}: {error}",
agents_dir.display()
)
})?;
let file_type = entry.file_type().map_err(|error| {
format!(
"读取 Agent Runtime 状态类型失败:{}: {error}",
entry.path().display()
)
})?;
if !file_type.is_file()
|| entry.path().extension().and_then(|value| value.to_str()) != Some("json")
{
continue;
}
let metadata = entry.metadata().map_err(|error| {
format!(
"读取 Agent Runtime 状态元数据失败:{}: {error}",
entry.path().display()
)
})?;
if metadata.len() > EXTERNAL_AGENT_RUNNER_MAX_FRAME_BYTES as u64 {
return Err(format!(
"Agent Runtime 状态文件超过读取上限:{}",
entry.path().display()
));
}
let content = fs::read(entry.path()).map_err(|error| {
format!(
"读取 Agent Runtime 状态失败:{}: {error}",
entry.path().display()
)
})?;
let runtime = serde_json::from_slice::<ExternalAgentRunnerRuntimeStateProbe>(&content)
.map_err(|_| format!("解析 Agent Runtime 状态失败:{}", entry.path().display()))?;
if runtime.task_queue.pending > 0
|| runtime.task_queue.waiting_for_confirmation > 0
|| runtime.task_queue.waiting_for_user_input > 0
|| runtime.task_queue.running > 0
{
return Ok(false);
}
if !external_agent_runner_runtime_state_is_idle(&runtime.status, &runtime.phase) {
return Ok(false);
}
}
Ok(true)
}
pub(super) fn external_agent_runner_known_roots_are_idle(
state: &ExternalAgentRunnerServerState,
) -> Result<bool, String> {
let roots = lock_unpoisoned(&state.known_roots)
.iter()
.cloned()
.collect::<Vec<_>>();
for root in roots {
if !external_agent_runner_root_is_idle(&root)? {
return Ok(false);
}
}
Ok(true)
}
pub(super) fn write_external_agent_runner_response(
stream: &mut TcpStream,
response: &ExternalAgentRunnerResponse,
) -> Result<(), String> {
let payload =
serde_json::to_vec(response).map_err(|_| "序列化 Agent Runner 响应失败".to_string())?;
write_external_agent_runner_frame(stream, &payload)
.map_err(|error| format!("写入 Agent Runner 响应失败:{error}"))?;
stream
.flush()
.map_err(|error| format!("刷新 Agent Runner 响应失败:{error}"))
}
pub(super) fn handle_external_agent_runner_connection(
mut stream: TcpStream,
state: Arc<ExternalAgentRunnerServerState>,
) -> Result<(), String> {
let _active = ExternalAgentRunnerActiveConnection { state: &state };
stream
.set_read_timeout(Some(EXTERNAL_AGENT_RUNNER_IO_TIMEOUT))
.and_then(|_| stream.set_write_timeout(Some(EXTERNAL_AGENT_RUNNER_IO_TIMEOUT)))
.map_err(|error| format!("配置 Agent Runner 连接超时失败:{error}"))?;
let payload = match read_external_agent_runner_frame(&mut stream) {
Ok(payload) => payload,
Err(ExternalAgentRunnerFrameError::Oversize(_)) => {
let response = ExternalAgentRunnerResponse::failure(
"",
"frame-too-large",
"Agent Runner 请求超过 1 MiB 上限",
);
return write_external_agent_runner_response(&mut stream, &response);
}
Err(ExternalAgentRunnerFrameError::Io(error))
if matches!(
error.kind(),
io::ErrorKind::UnexpectedEof
| io::ErrorKind::ConnectionReset
| io::ErrorKind::TimedOut
| io::ErrorKind::WouldBlock
) =>
{
return Ok(());
}
Err(error) => return Err(format!("读取 Agent Runner 请求失败:{error}")),
};
let request = match serde_json::from_slice::<ExternalAgentRunnerRequest>(&payload) {
Ok(request) => request,
Err(_) => {
let response = ExternalAgentRunnerResponse::failure(
"",
"invalid-json",
"Agent Runner 请求 JSON 无效",
);
return write_external_agent_runner_response(&mut stream, &response);
}
};
let response = handle_external_agent_runner_request(request, &state);
write_external_agent_runner_response(&mut stream, &response)
}