54d0fb75ea
新增 GDExtension 自动引导、GDScript 执行和实例隔离缓存 接入 AGC 插件开关、Runner、Agent 工具与权限审计 完善执行回执确认、不确定状态阻断及卸载恢复 补齐 Windows 分发资源、定向测试与实机验收文档
1422 lines
55 KiB
Rust
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(¤t, &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)
|
|
}
|