继续拆分客户端工作区与 Agent 运行时模块

拆出项目工作区视图、命令策略、摘要命令与开发面板模块。

拆出 Runtime action、driver 与 protocol 的职责模块并保留稳定 facade。

保留测试模块、兼容重导出与原有可见性边界。

补充结构拆分决策、踩坑记录与实施计划验收结果。
This commit is contained in:
AIGameCreator App
2026-07-22 16:03:34 +08:00
parent 0aaa191f8d
commit 759951ab12
64 changed files with 36160 additions and 34830 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
use super::*;
pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
root: &Path,
runtime: &mut AgentRuntimeState,
pending: &mut AgentRuntimePendingToolAction,
) -> Result<(), String> {
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Err("自主构建 Run 禁止进入 waiting-for-user-input".to_string());
}
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT.to_string();
pending.observation = None;
pending.updated_at = unix_timestamp();
write_game_creator_agent_runtime_pending_tool_action(root, pending)?;
let request = match prepare_game_creator_agent_user_input_request_at(root, pending)? {
AgentRuntimeUserInputRecovery::Waiting(request) => request,
AgentRuntimeUserInputRecovery::Answered { .. } => {
return Err("新建用户输入等待时 sidecar 已进入 answered,需由恢复路径继续".to_string());
}
AgentRuntimeUserInputRecovery::Cancelled => {
return Err("新建用户输入等待时 sidecar 已取消".to_string());
}
};
let waiting_observation = AgentRuntimeToolObservation {
tool: GAME_CREATOR_USER_INPUT_REQUEST_TOOL.to_string(),
status: "waiting-for-user-input".to_string(),
summary: format!(
"Agent 正在等待用户回答 {} 个澄清问题",
request.questions.len()
),
detail: None,
};
append_agent_runtime_tool_call_record(
root,
runtime,
&pending.task,
&pending.action,
&waiting_observation,
Some(&pending.action_id),
);
runtime.pending_tool_action = Some(pending.summary());
runtime.status = "waiting-for-user-input".to_string();
runtime.phase = "waiting-for-user-input".to_string();
runtime.current_action = "等待用户补充关键信息".to_string();
runtime.waiting_on = "用户回答 Agent 的结构化澄清问题".to_string();
runtime.next_step = "提交全部回答后在同一 run 继续当前计划".to_string();
runtime.error = None;
runtime.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task_projection_once(root, runtime, &pending.action_id)?;
refresh_game_creator_agent_runtime_task_queue(root, runtime)?;
write_game_creator_agent_runtime_state(root, runtime)?;
append_game_creator_agent_runtime_action_event(
root,
runtime,
"user_input.required",
"waiting-for-user-input",
"waiting-for-user-input",
"Agent 已暂停当前 run,等待用户补充关键信息。",
pending.input_summary.as_deref(),
&pending.action_id,
)?;
if let Err(error) = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.user_input.required",
"agentId": pending.agent_id,
"taskId": pending.task_id,
"sessionId": pending.session_id,
"runId": pending.run_id,
"actionId": pending.action_id,
"actionFingerprint": pending.action_fingerprint,
"requestId": request.request_id,
"questionCount": request.questions.len(),
"inputSummary": pending.input_summary,
}),
) {
let _ = append_game_creator_agent_runtime_event(
root,
runtime,
"user_input.audit_failed",
"waiting-for-user-input",
"waiting-for-user-input",
"用户输入请求已安全暂停,但公共审计记录写入失败。",
Some(&sanitize_agent_runtime_text(&error, 240)),
);
}
emit_game_creator_agent_runtime_update(root, &runtime.agent_id);
Ok(())
}
pub(crate) fn mark_game_creator_agent_runtime_auto_action_executing_if_current(
root: &Path,
pending: &mut AgentRuntimePendingToolAction,
) -> Result<bool, String> {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.tool_action.executing",
)?;
if game_creator_agent_runtime_cancel_requested_for(root, &pending.agent_id, &pending.run_id) {
return Ok(false);
}
if game_creator_agent_runtime_has_queued_steer_after_cursor(
root,
&pending.agent_id,
&pending.run_id,
pending.planned_steer_cursor,
)? {
return Ok(false);
}
if validate_agent_runtime_pending_current_goal_snapshot(root, pending).is_err() {
return Ok(false);
}
pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string();
pending.updated_at = unix_timestamp();
write_game_creator_agent_runtime_pending_tool_action(root, pending)?;
update_game_creator_agent_runtime_provider_batch_member(root, pending)?;
Ok(true)
}
pub(in crate::agent) fn append_game_creator_agent_runtime_auto_tool_action_executing_record(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<(), String> {
let public_input_summary = agent_runtime_public_action_input_summary(
root,
&pending.action.tool,
pending.input_summary.as_deref(),
);
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.tool_action.executing",
"agentId": pending.agent_id,
"taskId": pending.task_id,
"runId": pending.run_id,
"actionId": pending.action_id,
"actionFingerprint": pending.action_fingerprint,
"tool": pending.action.tool,
"executionMode": AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO,
"inputSummary": public_input_summary,
}),
)
}
pub(in crate::agent) fn append_game_creator_agent_runtime_auto_tool_action_observed_record(
root: &Path,
pending: &AgentRuntimePendingToolAction,
observation: &AgentRuntimeToolObservation,
) -> Result<(), String> {
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.tool_action.observed",
"agentId": pending.agent_id,
"taskId": pending.task_id,
"runId": pending.run_id,
"actionId": pending.action_id,
"actionFingerprint": pending.action_fingerprint,
"tool": pending.action.tool,
"executionMode": AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO,
"observationStatus": observation.status,
}),
)
}
pub(in crate::agent) fn append_game_creator_agent_runtime_parallel_action_executing_record(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<(), String> {
let record_type = "agent.runtime.tool_action.executing";
if agent_db_record_exists_for_action(
root,
record_type,
&pending.agent_id,
&pending.run_id,
&pending.action_id,
)? {
return Ok(());
}
append_game_creator_agent_runtime_auto_tool_action_executing_record(root, pending)
}
pub(in crate::agent) fn append_game_creator_agent_runtime_parallel_action_observed_record(
root: &Path,
pending: &AgentRuntimePendingToolAction,
observation: &AgentRuntimeToolObservation,
) -> Result<(), String> {
let record_type = "agent.runtime.tool_action.observed";
if agent_db_record_exists_for_action(
root,
record_type,
&pending.agent_id,
&pending.run_id,
&pending.action_id,
)? {
return Ok(());
}
append_game_creator_agent_runtime_auto_tool_action_observed_record(root, pending, observation)
}
@@ -0,0 +1,373 @@
use super::*;
pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
root: &Path,
agent_id: &str,
session_id: &str,
run_id: &str,
observations: &[AgentRuntimeToolObservation],
trigger: &str,
estimated_tokens_before: u64,
applied_steer_cursor: u64,
allow_idle_context_compaction: bool,
persist_transient_retry: bool,
request_kind: &str,
) -> Result<AgentRuntimeContextCompactionOutcome, String> {
if !matches!(
request_kind,
"context-compaction" | "final-reply-context-compaction"
) || (allow_idle_context_compaction && request_kind != "context-compaction")
{
return Err("上下文压缩 Provider requestKind 无效".to_string());
}
let (snapshot, source, llm, config_path, request) = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.context_compaction.build",
)?;
let source = build_game_creator_agent_runtime_context_compaction_source(
root,
agent_id,
session_id,
run_id,
observations,
trigger,
)?;
let request_slot = format!(
"source-{}",
source
.source_fingerprint
.chars()
.take(32)
.collect::<String>()
);
if !source.has_new_source {
provider_handoff::remove_consumed_context_at(
root,
agent_id,
run_id,
request_kind,
&request_slot,
)?;
let previous = source
.previous
.as_ref()
.map(|sidecar| Some(context_compaction_result(sidecar, true)))
.ok_or_else(|| "当前 Session 没有可压缩的旧上下文".to_string());
return previous.map(AgentRuntimeContextCompactionOutcome::Completed);
}
let template_agent_id = game_creator_runtime_template_agent_id_at(root, agent_id)?;
let app_config = load_game_creator_app_config()?;
let llm = resolve_game_creator_llm_config_for_agent(&app_config, &template_agent_id);
let config_path = format!("agentLlm.{template_agent_id}");
let request = build_game_creator_agent_runtime_context_compaction_request(&source, &llm)?;
let estimated_request_tokens = estimate_game_creator_llm_request_tokens(&request)?;
validate_game_creator_llm_request_context_budget(
&llm,
&request,
estimated_request_tokens,
"上下文压缩请求",
)?;
let snapshot = if allow_idle_context_compaction {
capture_idle_game_creator_agent_runtime_context_compaction_snapshot_at_locked(
root,
agent_id,
session_id,
run_id,
&request_slot,
applied_steer_cursor,
)?
} else {
capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
root,
agent_id,
session_id,
run_id,
request_kind,
&request_slot,
applied_steer_cursor,
)?
};
(snapshot, source, llm, config_path, request)
};
let handoff_identity =
game_creator_agent_runtime_provider_retry_identity(&snapshot, &llm, &request)?;
let response = if !persist_transient_retry {
AgentRuntimePersistedProviderRequestOutcome::Response(
request_game_creator_agent_runtime_llm_with_transient_retries(
root,
&snapshot,
&llm,
&config_path,
"上下文压缩",
&request,
)
.await?,
)
} else {
request_game_creator_agent_runtime_llm_with_persisted_transient_retry(
root,
&snapshot,
&llm,
&config_path,
"上下文压缩",
&request,
)
.await?
};
let response = match response {
AgentRuntimePersistedProviderRequestOutcome::Response(Some(response)) => response,
AgentRuntimePersistedProviderRequestOutcome::Response(None) => {
return Ok(AgentRuntimeContextCompactionOutcome::Completed(None));
}
AgentRuntimePersistedProviderRequestOutcome::Waiting(record) => {
return Ok(AgentRuntimeContextCompactionOutcome::Waiting(record));
}
AgentRuntimePersistedProviderRequestOutcome::HandoffPrepared => {
return Ok(AgentRuntimeContextCompactionOutcome::HandoffPrepared);
}
AgentRuntimePersistedProviderRequestOutcome::Superseded => {
return Ok(AgentRuntimeContextCompactionOutcome::Superseded);
}
};
let control_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.context_compaction.commit",
)?;
let current_source = build_game_creator_agent_runtime_context_compaction_source(
root,
agent_id,
session_id,
run_id,
observations,
trigger,
)?;
let source_stable = current_source.source_fingerprint == source.source_fingerprint
&& current_source.covered_agent_messages == source.covered_agent_messages
&& current_source.covered_project_messages == source.covered_project_messages
&& current_source.covered_observations == source.covered_observations
&& current_source.previous.as_ref().map(|value| value.revision)
== source.previous.as_ref().map(|value| value.revision);
if !source_stable {
let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot);
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
root,
&base_request_id,
)
.map(|value| value.0)
.unwrap_or(base_request_id);
let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context source drift"
));
}
let sidecar = finalize_game_creator_agent_runtime_context_compaction(
root,
&source,
&response,
estimated_tokens_before,
)?;
if let Err(error) = write_game_creator_agent_runtime_context_compaction(root, &sidecar) {
let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot);
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
root,
&base_request_id,
)
.map(|value| value.0)
.unwrap_or(base_request_id);
let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: {}",
redact_agent_runtime_error(root, &error, 320)
));
}
let persisted_sidecar = read_game_creator_agent_runtime_context_compaction(
root,
&sidecar.agent_id,
&sidecar.session_id,
)?
.ok_or_else(|| {
format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context compaction 写入后不存在"
)
})?;
if persisted_sidecar != sidecar {
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context compaction 写入后内容冲突"
));
}
if let Err(error) = provider_handoff::remove_matching_at(root, &handoff_identity) {
let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot);
let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked(
root,
&base_request_id,
)
.map(|value| value.0)
.unwrap_or(base_request_id);
let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: {}",
redact_agent_runtime_error(root, &error, 320)
));
}
drop(control_lock);
if let Ok(runtime) =
read_game_creator_agent_runtime_for_session_at(root, agent_id, Some(session_id))
.map(|result| result.state)
{
let summary = format!("Agent 已完成第 {} 次持久上下文压缩。", sidecar.revision);
let detail = format!(
"trigger={} · agentMessages={} · projectMessages={} · observations={} · estimatedBefore={} · estimatedAfter={}",
sidecar.trigger,
sidecar.covered_agent_messages,
sidecar.covered_project_messages,
sidecar.covered_observations,
sidecar.estimated_tokens_before,
sidecar.estimated_tokens_after,
);
let _ = append_game_creator_agent_runtime_event(
root,
&runtime,
"context.compacted",
runtime.status.as_str(),
runtime.phase.as_str(),
&summary,
Some(&detail),
);
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.context.compacted",
"agentId": runtime.agent_id,
"taskId": runtime.task_id,
"sessionId": runtime.session_id,
"runId": runtime.run_id,
"trigger": sidecar.trigger,
"revision": sidecar.revision,
"coveredAgentMessages": sidecar.covered_agent_messages,
"coveredProjectMessages": sidecar.covered_project_messages,
"coveredObservations": sidecar.covered_observations,
"estimatedTokensBefore": sidecar.estimated_tokens_before,
"estimatedTokensAfter": sidecar.estimated_tokens_after,
"promptTokens": sidecar.prompt_tokens,
"completionTokens": sidecar.completion_tokens,
"totalTokens": sidecar.total_tokens,
}),
);
}
Ok(AgentRuntimeContextCompactionOutcome::Completed(Some(
context_compaction_result(&sidecar, false),
)))
}
pub(crate) async fn compact_game_creator_agent_runtime_session_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
) -> Result<AgentRuntimeContextCompactionResult, String> {
validate_project_root(root)?;
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
let session_id = resolve_agent_conversation_session_id_at(root, &agent_id, session_id, false)?;
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock_with_wait(root, &agent_id)?
.ok_or_else(|| "Agent 正在运行,当前不能手动压缩上下文".to_string())?;
let runtime =
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))?;
if runtime.state.session_id != session_id
|| runtime.state.status != "idle"
|| !matches!(runtime.state.phase.as_str(), "idle" | "completed")
|| runtime.state.pending_tool_action.is_some()
|| runtime.task_queue.pending > 0
|| runtime.task_queue.running > 0
|| runtime.task_queue.waiting_for_confirmation > 0
|| runtime.task_queue.waiting_for_user_input > 0
|| game_creator_agent_runtime_has_pending_action_ledger(
root,
&agent_id,
&runtime.state.run_id,
)
|| game_creator_agent_runtime_provider_request_is_active_at(
root,
&agent_id,
&runtime.state.run_id,
)?
{
return Err(
"只有没有运行任务、Provider 请求或待确认动作的空闲 Session 才能手动压缩".to_string(),
);
}
let observations =
read_game_creator_agent_runtime_context_bundle_for_idle_compaction(root, &runtime.state)?
.map(|bundle| bundle.observations)
.unwrap_or_default();
let source = build_game_creator_agent_runtime_context_compaction_source(
root,
&agent_id,
&session_id,
&runtime.state.run_id,
&observations,
"manual",
)?;
let estimated_tokens_before = source.source_prompt_tokens.saturating_add(128);
let result = match compact_game_creator_agent_runtime_context_at(
root,
&agent_id,
&session_id,
&runtime.state.run_id,
&observations,
"manual",
estimated_tokens_before,
runtime.state.applied_steer_cursor,
true,
false,
"context-compaction",
)
.await?
{
AgentRuntimeContextCompactionOutcome::Completed(Some(result)) => result,
AgentRuntimeContextCompactionOutcome::Completed(None) => {
return Err("手动上下文压缩被新的控制指令中断".to_string());
}
AgentRuntimeContextCompactionOutcome::Waiting(_)
| AgentRuntimeContextCompactionOutcome::HandoffPrepared
| AgentRuntimeContextCompactionOutcome::Superseded => {
return Err("手动上下文压缩不应进入后台 Provider 重试等待".to_string());
}
};
let mut state =
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))?.state;
if state.run_id != runtime.state.run_id || state.status != "idle" {
return Err("手动压缩完成前 Runtime 身份或状态发生变化".to_string());
}
let app_config = load_game_creator_app_config()?;
let template_agent_id = game_creator_runtime_template_agent_id_at(root, &agent_id)?;
let llm = resolve_game_creator_llm_config_for_agent(&app_config, &template_agent_id);
state.context_usage.auto_compact_token_limit = llm.auto_compact_token_limit;
state.context_usage.estimated_input_tokens = result.estimated_tokens_after;
state.context_usage.last_prompt_tokens = result.prompt_tokens;
state.context_usage.last_completion_tokens = result.completion_tokens;
state.context_usage.last_total_tokens = result.total_tokens;
state.context_usage.compaction_revision = result.revision;
state.context_usage.compaction_count = result.revision;
state.context_usage.last_compaction_trigger = Some(result.trigger.clone());
state.context_usage.last_compacted_at = Some(result.compacted_at);
state.updated_at = unix_timestamp();
write_game_creator_agent_runtime_state(root, &state)?;
drop(runtime_lock);
emit_game_creator_agent_runtime_update(root, &agent_id);
Ok(result)
}
@@ -0,0 +1,451 @@
use super::*;
pub(crate) fn agent_runtime_tool_is_parallel_safe_read(tool: &str) -> bool {
matches!(
tool.trim(),
"memory.read"
| "conversation.read"
| "asset.list"
| "project.search"
| "project.diff"
| "git.inspect"
| "file.list"
| "file.read"
| "task.list"
)
}
pub(crate) fn agent_runtime_parallel_read_batch_len(
actions: &[AgentRuntimeToolAction],
start_index: usize,
) -> usize {
let count = actions
.iter()
.skip(start_index)
.take(AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT)
.take_while(|action| agent_runtime_tool_is_parallel_safe_read(&action.tool))
.count();
if count >= 2 {
count
} else {
0
}
}
pub(in crate::agent) fn agent_runtime_parallel_read_batch_is_auto_at(
root: &Path,
agent_id: &str,
run_id: &str,
run_profile: &str,
run_profile_binding_fingerprint: &str,
actions: &[AgentRuntimeToolAction],
) -> bool {
actions.iter().all(|action| {
let Some(command_id) = game_creator_agent_runtime_tool_command_id(action.tool.trim())
else {
return false;
};
agent_runtime_tool_is_parallel_safe_read(&action.tool)
&& game_creator_agent_runtime_tool_policy_rule_for_run(
root,
agent_id,
run_id,
Some(run_profile),
Some(run_profile_binding_fingerprint),
command_id,
)
.is_none()
})
}
pub(in crate::agent) fn game_creator_agent_runtime_tool_command_id(
tool: &str,
) -> Option<&'static str> {
match tool {
"memory.read" => Some("memory.read"),
"memory.write" => Some("memory.write"),
"conversation.read" => Some("conversation.read"),
"asset.list" => Some("asset.list"),
"project.index" => Some("project.index"),
"project.search" => Some("file.read"),
"project.verify" => Some("project.verify"),
"project.checkpoint" => Some("project.checkpoint"),
"project.restore" => Some("project.restore"),
"project.diff" => Some("project.diff"),
"git.inspect" => Some("project.git_inspect"),
"project.git_commit" => Some("project.git_commit"),
"project.patchset" => Some("project.patchset"),
"file.list" => Some("file.list"),
"file.read" => Some("file.read"),
"file.write" => Some("file.write"),
"file.patch" => Some("file.write"),
"file.delete" => Some("file.delete"),
"task.list" => Some("task.list"),
"task.create" => Some("task.create"),
"task.update" => Some("task.update"),
"command.exec" => Some("command.exec"),
"command.output_read" => Some("command.output_read"),
"command.start" => Some("command.start"),
"command.poll" => Some("command.poll"),
"command.stdin" => Some("command.stdin"),
"command.terminate" => Some("command.terminate"),
"command.run_limited" => Some("command.run_limited"),
"preview.start" => Some("preview.start"),
"preview.validate" => Some("preview.validate"),
"image.inspect" => Some("image.inspect"),
"canvas.asset_generate" => Some("canvas.asset_generate"),
"blackboard.write" => Some("memory.write"),
"agent.message" => Some("conversation.write"),
"agent.delegate" => Some("agent.delegate"),
"agent.spawn_isolated" => Some("agent.spawn_isolated"),
"agent.schedule_ready" => Some("agent.schedule_ready"),
"agent.action_history" => Some("agent.audit"),
"agent.run_status" => Some("agent.run_status"),
GAME_CREATOR_MCP_CALL_TOOL => Some(GAME_CREATOR_MCP_CALL_TOOL),
_ => None,
}
}
pub(crate) fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String {
let normalized = value
.trim()
.chars()
.map(|character| {
if character.is_ascii_alphanumeric()
|| character == '-'
|| character == '_'
|| character == '.'
{
character
} else {
'-'
}
})
.collect::<String>();
let normalized = normalized.trim_matches('-');
if normalized.is_empty() {
fallback.to_string()
} else {
truncate_agent_runtime_text(normalized, 160)
}
}
pub(in crate::agent) fn game_creator_agent_runtime_tool_confirmation_path(
root: &Path,
agent_id: &str,
run_id: &str,
command_id: &str,
) -> PathBuf {
game_creator_agent_runtime_confirmation_dir(root, agent_id, run_id).join(format!(
"{}.json",
agent_runtime_confirmation_path_component(command_id, "command")
))
}
pub(in crate::agent) fn game_creator_agent_runtime_confirmation_dir(
root: &Path,
agent_id: &str,
run_id: &str,
) -> PathBuf {
root.join(".agent/runtime/confirmations")
.join(agent_runtime_confirmation_path_component(agent_id, "agent"))
.join(agent_runtime_confirmation_path_component(run_id, "run"))
}
pub(crate) fn game_creator_agent_runtime_pending_tool_action_path(
root: &Path,
agent_id: &str,
run_id: &str,
) -> PathBuf {
root.join(game_creator_agent_runtime_pending_tool_action_relative_path(agent_id, run_id))
}
pub(in crate::agent) fn game_creator_agent_runtime_pending_tool_action_relative_path(
agent_id: &str,
run_id: &str,
) -> String {
format!(
".agent/runtime/pending-actions/{}/{}.json",
agent_runtime_confirmation_path_component(agent_id, "agent"),
agent_runtime_confirmation_path_component(run_id, "run")
)
}
pub(in crate::agent) fn game_creator_agent_runtime_parallel_read_batch_relative_path(
agent_id: &str,
run_id: &str,
) -> String {
format!(
".agent/runtime/parallel-read-batches/{}/{}.json",
agent_runtime_confirmation_path_component(agent_id, "agent"),
agent_runtime_confirmation_path_component(run_id, "run")
)
}
pub(crate) fn game_creator_agent_runtime_parallel_read_batch_path(
root: &Path,
agent_id: &str,
run_id: &str,
) -> PathBuf {
root.join(game_creator_agent_runtime_parallel_read_batch_relative_path(agent_id, run_id))
}
pub(in crate::agent) fn game_creator_agent_runtime_parallel_read_batch_exists(
root: &Path,
agent_id: &str,
run_id: &str,
) -> bool {
let path = game_creator_agent_runtime_parallel_read_batch_path(root, agent_id, run_id);
path.exists() || agent_runtime_json_sidecar_backup_path(&path).exists()
}
pub(in crate::agent) fn game_creator_agent_runtime_provider_action_batch_relative_path(
agent_id: &str,
run_id: &str,
) -> String {
format!(
".agent/runtime/provider-action-batches/{}/{}.json",
agent_runtime_confirmation_path_component(agent_id, "agent"),
agent_runtime_confirmation_path_component(run_id, "run")
)
}
pub(crate) fn game_creator_agent_runtime_provider_action_batch_path(
root: &Path,
agent_id: &str,
run_id: &str,
) -> PathBuf {
root.join(game_creator_agent_runtime_provider_action_batch_relative_path(agent_id, run_id))
}
pub(in crate::agent) fn game_creator_agent_runtime_provider_action_batch_exists(
root: &Path,
agent_id: &str,
run_id: &str,
) -> bool {
let path = game_creator_agent_runtime_provider_action_batch_path(root, agent_id, run_id);
path.exists() || agent_runtime_json_sidecar_backup_path(&path).exists()
}
pub(in crate::agent) fn game_creator_agent_runtime_has_pending_action_ledger(
root: &Path,
agent_id: &str,
run_id: &str,
) -> bool {
game_creator_agent_runtime_pending_tool_action_exists(root, agent_id, run_id)
|| game_creator_agent_runtime_parallel_read_batch_exists(root, agent_id, run_id)
|| game_creator_agent_runtime_provider_action_batch_exists(root, agent_id, run_id)
}
pub(in crate::agent) fn agent_runtime_parallel_read_batch_id(
project_id: &str,
agent_id: &str,
task_id: &str,
session_id: &str,
run_id: &str,
loop_iteration: u32,
actions: &[AgentRuntimePendingToolAction],
) -> Result<String, String> {
let action_ids = actions
.iter()
.map(|pending| pending.action_id.as_str())
.collect::<Vec<_>>();
let identity = serde_json::to_vec(&serde_json::json!({
"projectId": project_id,
"agentId": agent_id,
"taskId": task_id,
"sessionId": session_id,
"runId": run_id,
"loopIteration": loop_iteration,
"actionIds": action_ids,
}))
.map_err(|error| format!("序列化只读并行批次身份失败:{error}"))?;
let fingerprint = format!("{:x}", Sha256::digest(identity));
Ok(format!(
"parallel-read-{}",
fingerprint.chars().take(32).collect::<String>()
))
}
pub(in crate::agent) fn validate_game_creator_agent_runtime_parallel_read_batch(
root: &Path,
batch: &AgentRuntimeParallelReadBatch,
) -> Result<(), String> {
if batch.schema_version != AGENT_RUNTIME_PARALLEL_READ_BATCH_SCHEMA_VERSION {
return Err(format!(
"不支持的 Agent Runtime 只读并行批次版本:{}",
batch.schema_version
));
}
if batch.project_id != game_creator_agent_runtime_context_project_id(root)? {
return Err("Agent Runtime 只读并行批次项目身份不匹配".to_string());
}
if !matches!(
batch.status.as_str(),
AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING
| AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED
) {
return Err(format!(
"Agent Runtime 只读并行批次状态无效:{}",
batch.status
));
}
if !(2..=AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT).contains(&batch.actions.len()) {
return Err("Agent Runtime 只读并行批次动作数量必须在 2-3 之间".to_string());
}
if batch.status == AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING
&& !batch.timings.is_empty()
{
return Err("执行中的 Agent Runtime 只读并行批次不能提前保存计时结果".to_string());
}
if batch.status == AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED
&& batch.timings.len() != batch.actions.len()
{
return Err("已观察 Agent Runtime 只读并行批次缺少完整计时结果".to_string());
}
let mut previous_action_index = None;
let mut action_ids = std::collections::BTreeSet::new();
let first_pending = batch
.actions
.first()
.ok_or_else(|| "Agent Runtime 只读并行批次缺少首个动作".to_string())?;
for (index, pending) in batch.actions.iter().enumerate() {
validate_agent_runtime_pending_tool_action_record(root, pending)?;
if pending.agent_id != batch.agent_id
|| pending.task_id != batch.task_id
|| pending.session_id != batch.session_id
|| pending.run_id != batch.run_id
|| pending.source != batch.source
|| pending.loop_iteration != batch.loop_iteration
|| pending.planned_steer_cursor != batch.planned_steer_cursor
|| !pending.is_auto()
|| !agent_runtime_tool_is_parallel_safe_read(&pending.action.tool)
{
return Err("Agent Runtime 只读并行批次成员身份或工具分类不匹配".to_string());
}
if pending.task != first_pending.task
|| pending.goal_id != first_pending.goal_id
|| pending.goal_revision != first_pending.goal_revision
|| pending.goal_snapshot_fingerprint != first_pending.goal_snapshot_fingerprint
|| pending.thinking_summary != first_pending.thinking_summary
|| pending.plan != first_pending.plan
|| pending.fallback_response != first_pending.fallback_response
|| pending.observations != first_pending.observations
|| pending.project_revision_before != first_pending.project_revision_before
|| pending.verification_gate_before != first_pending.verification_gate_before
|| pending.planned_repository_context_fingerprint
!= first_pending.planned_repository_context_fingerprint
{
return Err("Agent Runtime 只读并行批次成员的 planning 快照不一致".to_string());
}
if !action_ids.insert(pending.action_id.clone()) {
return Err("Agent Runtime 只读并行批次包含重复 actionId".to_string());
}
if previous_action_index.is_some_and(|previous| pending.action_index != previous + 1) {
return Err("Agent Runtime 只读并行批次 action index 必须连续递增".to_string());
}
previous_action_index = Some(pending.action_index);
match batch.status.as_str() {
AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING
&& pending.observation.is_none() => {}
AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED
&& pending.observation.as_ref().is_some_and(|observation| {
observation.tool == pending.action.tool
&& !observation.is_waiting_for_confirmation()
&& !observation.requires_reconciliation()
}) => {}
_ => {
return Err(format!(
"Agent Runtime 只读并行批次成员状态与批次不一致:index={index}"
));
}
}
}
for (timing, pending) in batch.timings.iter().zip(batch.actions.iter()) {
if timing.action_id != pending.action_id
|| timing.started_at_nanos == 0
|| timing.finished_at_nanos < timing.started_at_nanos
{
return Err("Agent Runtime 只读并行批次计时身份或区间无效".to_string());
}
}
let expected_batch_id = agent_runtime_parallel_read_batch_id(
&batch.project_id,
&batch.agent_id,
&batch.task_id,
&batch.session_id,
&batch.run_id,
batch.loop_iteration,
&batch.actions,
)?;
if batch.batch_id != expected_batch_id {
return Err("Agent Runtime 只读并行批次身份指纹已变化".to_string());
}
Ok(())
}
pub(in crate::agent) fn write_game_creator_agent_runtime_parallel_read_batch(
root: &Path,
batch: &AgentRuntimeParallelReadBatch,
) -> Result<(), String> {
validate_game_creator_agent_runtime_parallel_read_batch(root, batch)?;
write_agent_runtime_json_sidecar_with_max_bytes(
root,
&game_creator_agent_runtime_parallel_read_batch_relative_path(
&batch.agent_id,
&batch.run_id,
),
"Agent Runtime 只读并行批次",
batch,
AGENT_RUNTIME_PARALLEL_READ_BATCH_SIDECAR_MAX_BYTES,
)
}
pub(in crate::agent) fn read_game_creator_agent_runtime_parallel_read_batch(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<AgentRuntimeParallelReadBatch, String> {
let relative_path =
game_creator_agent_runtime_parallel_read_batch_relative_path(agent_id, run_id);
let batch = read_agent_runtime_json_sidecar_with_max_bytes::<AgentRuntimeParallelReadBatch>(
root,
&relative_path,
"Agent Runtime 只读并行批次",
AGENT_RUNTIME_PARALLEL_READ_BATCH_SIDECAR_MAX_BYTES,
)?
.ok_or_else(|| "Agent Runtime 只读并行批次不存在".to_string())?;
if batch.agent_id != agent_id || batch.run_id != run_id {
return Err("Agent Runtime 只读并行批次 Agent 或 run 身份不匹配".to_string());
}
validate_game_creator_agent_runtime_parallel_read_batch(root, &batch)?;
Ok(batch)
}
pub(in crate::agent) fn remove_game_creator_agent_runtime_parallel_read_batch(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<(), String> {
let path = game_creator_agent_runtime_parallel_read_batch_path(root, agent_id, run_id);
let backup_path = agent_runtime_json_sidecar_backup_path(&path);
remove_agent_runtime_json_sidecar_backup(&backup_path, "Agent Runtime 只读并行批次")?;
match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
Err("Agent Runtime 只读并行批次必须是普通文件".to_string())
}
Ok(_) => fs::remove_file(&path).map_err(|error| {
format!(
"删除 Agent Runtime 只读并行批次失败:{}: {error}",
path.display()
)
}),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(format!(
"读取 Agent Runtime 只读并行批次元数据失败:{}: {error}",
path.display()
)),
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,234 @@
use super::*;
pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_at(
root: &Path,
agent_id: &str,
session_id: &str,
run_id: &str,
task: &str,
plan: &AgentRuntimeToolPlan,
observations: &[AgentRuntimeToolObservation],
applied_steer_cursor: u64,
request_slot: &str,
response_revision: u64,
) -> Result<RequestedAgentRuntimeFinalReplyOutcome, String> {
let mut built_request = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.provider_request.build.final_reply",
)?;
build_game_creator_agent_background_final_reply_request(
root,
agent_id,
session_id,
run_id,
task,
plan,
observations,
)?
};
let mut estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
let mut compaction = None;
if estimated_input_tokens > built_request.0.auto_compact_token_limit {
match compact_game_creator_agent_runtime_context_at(
root,
agent_id,
session_id,
run_id,
observations,
"auto",
estimated_input_tokens,
applied_steer_cursor,
false,
true,
"final-reply-context-compaction",
)
.await?
{
AgentRuntimeContextCompactionOutcome::Completed(Some(result)) => {
compaction = Some(result);
}
AgentRuntimeContextCompactionOutcome::Completed(None) => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Ready(None));
}
AgentRuntimeContextCompactionOutcome::Waiting(record) => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Waiting(record));
}
AgentRuntimeContextCompactionOutcome::HandoffPrepared => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::HandoffPrepared);
}
AgentRuntimeContextCompactionOutcome::Superseded => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Superseded);
}
}
built_request = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.provider_request.rebuild.final_reply",
)?;
build_game_creator_agent_background_final_reply_request(
root,
agent_id,
session_id,
run_id,
task,
plan,
observations,
)?
};
estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
if estimated_input_tokens > built_request.0.auto_compact_token_limit {
return Err(format!(
"上下文压缩后 final-reply 预计输入仍为 {estimated_input_tokens} tokens,超过 autoCompactTokenLimit={};请提高阈值或新建 Session",
built_request.0.auto_compact_token_limit
));
}
}
validate_game_creator_llm_request_context_budget(
&built_request.0,
&built_request.2,
estimated_input_tokens,
"final-reply 请求",
)?;
if compaction.is_none() {
compaction =
read_game_creator_agent_runtime_context_compaction(root, agent_id, session_id)?
.as_ref()
.map(|sidecar| context_compaction_result(sidecar, true));
}
let provider_snapshot = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.provider_request.capture.final_reply",
)?;
capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
root,
agent_id,
session_id,
run_id,
"final-reply",
request_slot,
applied_steer_cursor,
)?
};
let (llm, config_path, request) = built_request;
let auto_compact_token_limit = llm.auto_compact_token_limit;
let stream_snapshot = provider_snapshot.clone();
let suppress_private_process_output =
agent_runtime_observations_contain_private_process_output(observations);
let fallback_response = (!plan.response.trim().is_empty())
.then(|| strip_llm_thinking_blocks(&plan.response))
.map(|response| {
redact_agent_runtime_private_process_output_from_response(&response, observations)
})
.filter(|response| !response.trim().is_empty());
let stream_response = llm.stream;
let request_stream_snapshot = stream_snapshot.clone();
let response_result =
request_game_creator_agent_runtime_llm_with_persisted_transient_retry_using(
root,
&provider_snapshot,
&llm,
&config_path,
"后台 Agent 最终回复",
&request,
move |client, attempt_request| {
let stream_snapshot = request_stream_snapshot.clone();
async move {
if stream_response {
let mut publisher = AgentRuntimeResponseStreamPublisher::start(
root,
&stream_snapshot,
response_revision,
);
match client
.stream_run(attempt_request, |delta| {
if !suppress_private_process_output {
publisher.push(delta);
}
})
.await
{
Ok(response) => {
publisher.handoff();
Ok(response)
}
Err(error) => {
publisher.failed();
Err(error)
}
}
} else {
client.run(attempt_request).await
}
}
},
|response| {
let mut response = response.clone();
response.text = redact_agent_runtime_private_process_output_from_response(
&strip_llm_thinking_blocks(&response.text),
observations,
);
response
},
)
.await;
if response_result.is_err() {
if let Some(fallback_response) = fallback_response.as_deref() {
let _ = write_game_creator_agent_runtime_response_stream_ready_at(
root,
&stream_snapshot,
response_revision,
fallback_response,
Some("fallback"),
);
}
}
let response = match response_result? {
AgentRuntimePersistedProviderRequestOutcome::Response(Some(response)) => response,
AgentRuntimePersistedProviderRequestOutcome::Response(None) => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Ready(None));
}
AgentRuntimePersistedProviderRequestOutcome::Waiting(record) => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Waiting(record));
}
AgentRuntimePersistedProviderRequestOutcome::HandoffPrepared => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::HandoffPrepared);
}
AgentRuntimePersistedProviderRequestOutcome::Superseded => {
return Ok(RequestedAgentRuntimeFinalReplyOutcome::Superseded);
}
};
let reply = redact_agent_runtime_private_process_output_from_response(
&strip_llm_thinking_blocks(response.text.as_str()),
observations,
);
if reply.trim().is_empty() {
if let Some(fallback_response) = fallback_response.as_deref() {
let _ = write_game_creator_agent_runtime_response_stream_ready_at(
root,
&stream_snapshot,
response_revision,
fallback_response,
Some("fallback"),
);
}
return Err(format!("{config_path} 后台 Agent 最终回复为空"));
}
write_game_creator_agent_runtime_response_stream_ready_at(
root,
&stream_snapshot,
response_revision,
&reply,
response.finish_reason.as_deref(),
)?;
Ok(RequestedAgentRuntimeFinalReplyOutcome::Ready(Some(
RequestedAgentRuntimeFinalReply {
reply,
estimated_input_tokens,
auto_compact_token_limit,
usage: response.usage,
compaction,
},
)))
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,279 @@
use super::*;
#[derive(Default)]
pub(in crate::agent) struct AgentRuntimeThinkingStreamFilter {
pub(in crate::agent) pending: String,
pub(in crate::agent) inside_thinking: bool,
}
pub(in crate::agent) fn ascii_marker_suffix_len(value: &str, marker: &str) -> usize {
let max_len = value.len().min(marker.len().saturating_sub(1));
(1..=max_len)
.rev()
.find(|length| {
let start = value.len() - *length;
value.is_char_boundary(start) && value[start..].eq_ignore_ascii_case(&marker[..*length])
})
.unwrap_or(0)
}
impl AgentRuntimeThinkingStreamFilter {
fn push(&mut self, delta: &str) -> String {
const THINK_START: &str = "<think>";
const THINK_END: &str = "</think>";
self.pending.push_str(delta);
let mut visible = String::new();
loop {
if self.inside_thinking {
if let Some(end) = self.pending.to_ascii_lowercase().find(THINK_END) {
self.pending.drain(..end + THINK_END.len());
self.inside_thinking = false;
continue;
}
let keep = ascii_marker_suffix_len(&self.pending, THINK_END);
if keep == 0 {
self.pending.clear();
} else {
let start = self.pending.len() - keep;
self.pending = self.pending[start..].to_string();
}
break;
}
if let Some(start) = self.pending.to_ascii_lowercase().find(THINK_START) {
visible.push_str(&self.pending[..start]);
self.pending.drain(..start + THINK_START.len());
self.inside_thinking = true;
continue;
}
let keep = ascii_marker_suffix_len(&self.pending, THINK_START);
let split = self.pending.len() - keep;
visible.push_str(&self.pending[..split]);
self.pending = self.pending[split..].to_string();
break;
}
visible
}
}
pub(in crate::agent) fn normalize_agent_runtime_response_stream_finish_reason(
finish_reason: Option<&str>,
) -> Option<String> {
finish_reason.and_then(|reason| {
let reason = reason.trim();
(!reason.is_empty() && !reason.chars().any(char::is_control))
.then(|| truncate_agent_runtime_text(reason, 80))
})
}
pub(in crate::agent) fn build_game_creator_agent_runtime_response_stream(
snapshot: &AgentRuntimeProviderRequestSnapshot,
response_revision: u64,
status: &str,
sequence: u64,
accumulated_text: String,
finish_reason: Option<&str>,
) -> AgentRuntimeResponseStream {
let now = unix_timestamp();
AgentRuntimeResponseStream {
schema_version: AGENT_RUNTIME_RESPONSE_STREAM_SCHEMA_VERSION.to_string(),
agent_id: snapshot.agent_id.clone(),
task_id: snapshot.task_id.clone(),
session_id: snapshot.session_id.clone(),
run_id: snapshot.run_id.clone(),
request_kind: snapshot.request_kind.clone(),
request_slot: snapshot.request_slot.clone(),
applied_steer_cursor: snapshot.applied_steer_cursor,
response_revision,
sequence,
status: status.to_string(),
accumulated_text,
finish_reason: normalize_agent_runtime_response_stream_finish_reason(finish_reason),
started_at: now,
updated_at: now,
}
}
pub(in crate::agent) fn write_game_creator_agent_runtime_response_stream_ready_at(
root: &Path,
snapshot: &AgentRuntimeProviderRequestSnapshot,
response_revision: u64,
response: &str,
finish_reason: Option<&str>,
) -> Result<(), String> {
if response.trim().is_empty() {
return Err("Agent Runtime 回复流 ready 正文不能为空".to_string());
}
if response.chars().count() > AGENT_RUNTIME_RESPONSE_STREAM_MAX_CHARS {
return Err(format!(
"Agent Runtime 回复流 ready 正文超过 {} 字符上限",
AGENT_RUNTIME_RESPONSE_STREAM_MAX_CHARS
));
}
let previous = read_game_creator_agent_runtime_response_stream_at(
root,
&snapshot.agent_id,
&snapshot.run_id,
)?
.filter(|stream| {
stream.task_id == snapshot.task_id
&& stream.session_id == snapshot.session_id
&& stream.request_slot == snapshot.request_slot
&& stream.applied_steer_cursor == snapshot.applied_steer_cursor
&& stream.response_revision == response_revision
});
let mut stream = build_game_creator_agent_runtime_response_stream(
snapshot,
response_revision,
AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
previous
.as_ref()
.map(|stream| stream.sequence.saturating_add(1))
.unwrap_or(1),
response.to_string(),
finish_reason,
);
if let Some(previous) = previous {
stream.started_at = previous.started_at;
}
write_game_creator_agent_runtime_response_stream_at(root, &stream)
}
pub(in crate::agent) struct AgentRuntimeResponseStreamPublisher {
pub(in crate::agent) root: PathBuf,
pub(in crate::agent) stream: AgentRuntimeResponseStream,
pub(in crate::agent) filter: AgentRuntimeThinkingStreamFilter,
pub(in crate::agent) last_persisted_at: std::time::Instant,
pub(in crate::agent) dirty: bool,
pub(in crate::agent) terminal: bool,
}
impl AgentRuntimeResponseStreamPublisher {
pub(super) fn start(
root: &Path,
snapshot: &AgentRuntimeProviderRequestSnapshot,
response_revision: u64,
) -> Self {
let previous = read_game_creator_agent_runtime_response_stream_at(
root,
&snapshot.agent_id,
&snapshot.run_id,
)
.ok()
.flatten()
.filter(|stream| {
stream.task_id == snapshot.task_id
&& stream.session_id == snapshot.session_id
&& stream.request_slot == snapshot.request_slot
&& stream.applied_steer_cursor == snapshot.applied_steer_cursor
&& stream.response_revision == response_revision
});
let mut stream = build_game_creator_agent_runtime_response_stream(
snapshot,
response_revision,
AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
previous
.as_ref()
.map(|stream| stream.sequence.saturating_add(1))
.unwrap_or(0),
String::new(),
None,
);
if let Some(previous) = previous {
stream.started_at = previous.started_at;
}
let _ = write_game_creator_agent_runtime_response_stream_at(root, &stream);
let now = std::time::Instant::now();
Self {
root: root.to_path_buf(),
stream,
filter: AgentRuntimeThinkingStreamFilter::default(),
last_persisted_at: now
.checked_sub(AGENT_RUNTIME_RESPONSE_STREAM_PERSIST_INTERVAL)
.unwrap_or(now),
dirty: false,
terminal: false,
}
}
pub(super) fn push(&mut self, delta: &platform_llm::LlmStreamDelta) {
let visible_delta = self.filter.push(&delta.delta_text);
let remaining = AGENT_RUNTIME_RESPONSE_STREAM_MAX_CHARS
.saturating_sub(self.stream.accumulated_text.chars().count());
let visible_delta = visible_delta.chars().take(remaining).collect::<String>();
let finish_reason =
normalize_agent_runtime_response_stream_finish_reason(delta.finish_reason.as_deref());
if visible_delta.is_empty() && finish_reason == self.stream.finish_reason {
return;
}
self.stream.accumulated_text.push_str(&visible_delta);
self.stream.finish_reason = finish_reason;
self.stream.sequence = self.stream.sequence.saturating_add(1);
self.stream.updated_at = unix_timestamp();
self.dirty = true;
if self.last_persisted_at.elapsed() >= AGENT_RUNTIME_RESPONSE_STREAM_PERSIST_INTERVAL
|| self.stream.finish_reason.is_some()
{
self.persist();
}
}
pub(super) fn ready(&mut self, response: &str, finish_reason: Option<&str>) {
if response.trim().is_empty()
|| response.chars().count() > AGENT_RUNTIME_RESPONSE_STREAM_MAX_CHARS
{
self.finish_with_status(AGENT_RUNTIME_RESPONSE_STREAM_STATUS_FAILED);
return;
}
self.stream.accumulated_text = response.to_string();
self.stream.finish_reason =
normalize_agent_runtime_response_stream_finish_reason(finish_reason);
self.finish_with_status(AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY);
}
pub(super) fn failed(&mut self) {
self.finish_with_status(AGENT_RUNTIME_RESPONSE_STREAM_STATUS_FAILED);
}
pub(super) fn handoff(&mut self) {
self.persist();
self.terminal = true;
}
fn finish_with_status(&mut self, status: &str) {
self.stream.status = status.to_string();
self.stream.sequence = self.stream.sequence.saturating_add(1);
self.stream.updated_at = unix_timestamp();
self.dirty = true;
self.persist();
self.terminal = true;
}
pub(super) fn persist(&mut self) {
if !self.dirty {
return;
}
let _ = write_game_creator_agent_runtime_response_stream_at(&self.root, &self.stream);
self.last_persisted_at = std::time::Instant::now();
self.dirty = false;
}
}
impl Drop for AgentRuntimeResponseStreamPublisher {
fn drop(&mut self) {
if self.terminal {
return;
}
self.finish_with_status(AGENT_RUNTIME_RESPONSE_STREAM_STATUS_DISCARDED);
}
}
#[cfg(test)]
pub(crate) fn filter_agent_runtime_response_stream_for_test(chunks: &[&str]) -> String {
let mut filter = AgentRuntimeThinkingStreamFilter::default();
chunks
.iter()
.map(|chunk| filter.push(chunk))
.collect::<String>()
}
@@ -0,0 +1,179 @@
use super::*;
pub(in crate::agent) fn mark_supervisor_delivery_claims_observed_for_pending_action_at(
root: &Path,
pending: &AgentRuntimePendingToolAction,
observation: &AgentRuntimeToolObservation,
) -> Result<(), String> {
if pending.action.tool != "agent.run_status" || observation.status != "ok" {
return Ok(());
}
let observed_delegate_receipt_ids =
observed_delegate_receipt_ids_from_run_status(observation.detail.as_deref())?;
let observed_isolated_groups =
observed_isolated_join_group_ids_from_run_status(observation.detail.as_deref())?;
if pending.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
mark_static_delegate_claim_observed_for_receipts_at(
root,
&pending.agent_id,
&pending.run_id,
&pending.action_id,
&observed_delegate_receipt_ids,
)?;
}
mark_unobserved_isolated_join_claims_for_parent_at(
root,
&pending.agent_id,
&pending.run_id,
&observed_isolated_groups,
)?;
Ok(())
}
pub(in crate::agent) fn run_status_prefixed_payload<'a>(
detail: Option<&'a str>,
prefix: &str,
) -> Result<Option<&'a str>, String> {
let Some(mut remaining) = detail else {
return Ok(None);
};
let mut found = None;
loop {
let (block, next) = remaining
.split_once("\n\n")
.map_or((remaining, ""), |(block, next)| (block, next));
if let Some(payload) = block.strip_prefix(prefix) {
if found.replace(payload).is_some() {
return Err(format!(
"agent.run_status observation 含重复前置区块:{}",
prefix.trim_end()
));
}
}
if !matches!(
block.split_once(':').map(|(name, _)| name),
Some(
"readyIsolatedJoins"
| "readyDelegateReceipts"
| "claimedDelegateContracts"
| "claimedIsolatedJoins"
)
) {
return Ok(found);
}
if next.is_empty() {
return Ok(found);
}
remaining = next;
}
}
pub(in crate::agent) fn observed_delegate_receipt_ids_from_run_status(
detail: Option<&str>,
) -> Result<BTreeSet<String>, String> {
let Some(payload) = run_status_prefixed_payload(detail, "readyDelegateReceipts: ")? else {
return Ok(BTreeSet::new());
};
let payload = serde_json::from_str::<serde_json::Value>(payload)
.map_err(|error| format!("解析已观察专业 Agent ready receipts 失败:{error}"))?;
if payload.get("ready").and_then(serde_json::Value::as_bool) != Some(true) {
return Err("已观察专业 Agent ready receipts 结果未标记 ready=true".to_string());
}
let receipts = payload
.get("receipts")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| "已观察专业 Agent ready receipts 结果缺少 receipts".to_string())?;
let mut delegation_ids = BTreeSet::new();
for receipt in receipts {
let delegation_id = receipt
.get("delegationId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "已观察专业 Agent ready receipts 结果缺少 delegationId".to_string())?;
if !delegation_ids.insert(delegation_id.to_string()) {
return Err(format!(
"已观察专业 Agent ready receipts 结果含重复 delegationId{delegation_id}"
));
}
}
Ok(delegation_ids)
}
pub(in crate::agent) fn observed_isolated_join_group_ids_from_run_status(
detail: Option<&str>,
) -> Result<BTreeSet<String>, String> {
let Some(payload) = run_status_prefixed_payload(detail, "readyIsolatedJoins: ")? else {
return Ok(BTreeSet::new());
};
let payload = serde_json::from_str::<serde_json::Value>(payload)
.map_err(|error| format!("解析已观察动态隔离 Agent join 结果失败:{error}"))?;
if payload.get("ready").and_then(serde_json::Value::as_bool) != Some(true) {
return Err("已观察动态隔离 Agent join 结果未标记 ready=true".to_string());
}
let joins = payload
.get("joins")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| "已观察动态隔离 Agent join 结果缺少 joins".to_string())?;
let mut group_ids = BTreeSet::new();
for join in joins {
let group_id = join
.get("delegationGroupId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "已观察动态隔离 Agent join 结果缺少 delegationGroupId".to_string())?;
if !group_ids.insert(group_id.to_string()) {
return Err(format!(
"已观察动态隔离 Agent join 结果含重复 group{group_id}"
));
}
}
Ok(group_ids)
}
impl AgentRuntimeToolObservation {
pub(crate) fn summary(&self) -> String {
format!("{}{} · {}", self.tool, self.status, self.summary)
}
pub(in crate::agent) fn is_waiting_for_confirmation(&self) -> bool {
self.status == "waiting-for-confirmation"
}
pub(in crate::agent) fn requires_reconciliation(&self) -> bool {
self.status == AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
}
pub(in crate::agent) fn is_repository_context_drift(&self) -> bool {
self.status == "blocked"
&& self
.detail
.as_deref()
.is_some_and(|detail| detail.starts_with("repositoryContextDrift=true"))
}
}
pub(in crate::agent) fn agent_runtime_local_observation_detail(
observation: &AgentRuntimeToolObservation,
) -> Option<&str> {
if matches!(
observation.tool.as_str(),
"command.poll"
| "command.stdin"
| GAME_CREATOR_MCP_CALL_TOOL
| GAME_CREATOR_USER_INPUT_REQUEST_TOOL
) {
None
} else {
observation.detail.as_deref()
}
}
pub(in crate::agent) fn agent_runtime_public_observation_detail(
root: &Path,
observation: &AgentRuntimeToolObservation,
) -> Option<String> {
if observation.tool == "agent.action_history" && observation.status == "ok" {
let detail = observation.detail.as_deref()?;
validate_agent_runtime_pending_serialized_content(root, detail).ok()?;
return Some(detail.to_string());
}
agent_runtime_action_receipt_safe_detail(root, observation)
}
@@ -0,0 +1,41 @@
use super::*;
#[test]
fn mixed_ready_prefixes_parse_exact_static_and_isolated_ids() {
let detail = concat!(
"readyIsolatedJoins: {\"ready\":true,\"joins\":[{\"delegationGroupId\":\"group-a\"}]}\n\n",
"readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}\n\n",
"agentId: project-supervisor"
);
assert_eq!(
observed_isolated_join_group_ids_from_run_status(Some(detail))
.expect("parse mixed isolated prefix"),
BTreeSet::from(["group-a".to_string()])
);
assert_eq!(
observed_delegate_receipt_ids_from_run_status(Some(detail))
.expect("parse mixed static prefix"),
BTreeSet::from(["delivery-a".to_string()])
);
}
#[test]
fn ready_prefix_parser_rejects_false_and_duplicate_blocks() {
let not_ready =
"readyDelegateReceipts: {\"ready\":false,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}";
assert!(
observed_delegate_receipt_ids_from_run_status(Some(not_ready))
.expect_err("ready=false must fail closed")
.contains("ready=true")
);
let duplicate = concat!(
"readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}\n\n",
"readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}"
);
assert!(
observed_delegate_receipt_ids_from_run_status(Some(duplicate))
.expect_err("duplicate ready receipt blocks must fail closed")
.contains("重复前置区块")
);
}
@@ -0,0 +1,431 @@
use super::*;
pub(in crate::agent) fn agent_runtime_has_structured_plan(runtime: &AgentRuntimeState) -> bool {
runtime.plan_revision > 0
}
pub(in crate::agent) fn validate_agent_runtime_structured_plan_snapshot(
plan_revision: u64,
plan_explanation: &str,
plan: &[String],
plan_steps: &[AgentRuntimePlanStep],
active_plan_step_index: Option<u32>,
) -> Result<(), String> {
if plan_revision == 0 {
if !plan_explanation.trim().is_empty() {
return Err("Agent 旧计划不能携带结构化计划说明".to_string());
}
return Ok(());
}
if plan_explanation.trim().is_empty() {
return Err("Agent 结构化计划缺少 explanation".to_string());
}
if plan_steps.is_empty() || plan_steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT {
return Err(format!(
"Agent 结构化计划步骤数量必须在 1..={AGENT_RUNTIME_PLAN_STEP_LIMIT}"
));
}
if plan.len() != plan_steps.len() {
return Err("Agent 结构化计划文本与步骤数量不匹配".to_string());
}
let mut seen_steps = std::collections::BTreeSet::new();
let mut in_progress_index = None;
for (index, step) in plan_steps.iter().enumerate() {
if step.index != index as u32
|| step.title.trim().is_empty()
|| plan.get(index) != Some(&step.title)
{
return Err("Agent 结构化计划步骤索引或标题不匹配".to_string());
}
if !seen_steps.insert(step.title.as_str()) {
return Err("Agent 结构化计划包含重复步骤".to_string());
}
if !matches!(
step.status.as_str(),
AGENT_RUNTIME_PLAN_STATUS_PENDING
| AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS
| AGENT_RUNTIME_PLAN_STATUS_COMPLETED
| AGENT_RUNTIME_PLAN_STATUS_FAILED
) {
return Err("Agent 结构化计划持久状态无效".to_string());
}
if step.status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS {
if in_progress_index.replace(step.index).is_some() {
return Err("Agent 结构化计划同时存在多个 in_progress 步骤".to_string());
}
}
}
if active_plan_step_index != in_progress_index {
return Err("Agent 结构化计划 activePlanStepIndex 与 in_progress 步骤不匹配".to_string());
}
Ok(())
}
pub(crate) fn sanitize_agent_runtime_plan_update(
update: &AgentRuntimePlanUpdate,
) -> Result<AgentRuntimePlanUpdate, String> {
let explanation = sanitize_agent_runtime_text(update.explanation.trim(), 240);
if explanation.is_empty() {
return Err("Agent 结构化计划更新的 explanation 不能为空".to_string());
}
if update.steps.is_empty() {
return Err("Agent 结构化计划更新必须包含至少一个步骤".to_string());
}
if update.steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT {
return Err(format!(
"Agent 结构化计划更新最多包含 {AGENT_RUNTIME_PLAN_STEP_LIMIT} 个步骤"
));
}
let mut seen_steps = std::collections::BTreeSet::new();
let mut in_progress_count = 0usize;
let mut steps = Vec::with_capacity(update.steps.len());
for raw_step in &update.steps {
let step = sanitize_agent_runtime_text(raw_step.step.trim(), 180);
if step.is_empty() {
return Err("Agent 结构化计划更新不能包含空步骤".to_string());
}
if !seen_steps.insert(step.clone()) {
return Err("Agent 结构化计划更新包含重复步骤".to_string());
}
let status = raw_step.status.trim();
if !matches!(
status,
AGENT_RUNTIME_PLAN_STATUS_PENDING
| AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS
| AGENT_RUNTIME_PLAN_STATUS_COMPLETED
) {
return Err(
"Agent 结构化计划步骤状态无效,只允许 pending、in_progress、completed".to_string(),
);
}
if status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS {
in_progress_count += 1;
if in_progress_count > 1 {
return Err("Agent 结构化计划同时最多只能有一个 in_progress 步骤".to_string());
}
}
steps.push(AgentRuntimePlanUpdateStep {
step,
status: status.to_string(),
});
}
Ok(AgentRuntimePlanUpdate { explanation, steps })
}
pub(crate) fn apply_agent_runtime_plan_update(
runtime: &mut AgentRuntimeState,
update: &AgentRuntimePlanUpdate,
) -> Result<bool, String> {
let update = sanitize_agent_runtime_plan_update(update)?;
let mut terminal_steps = std::collections::BTreeMap::new();
if agent_runtime_has_structured_plan(runtime) {
for step in &runtime.plan_steps {
if matches!(
step.status.as_str(),
AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED
) {
terminal_steps.insert(step.title.clone(), step.clone());
}
}
}
for step in &update.steps {
let Some(existing) = terminal_steps.get(&step.step) else {
continue;
};
if existing.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED
&& step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED
{
return Err("Agent 结构化计划不能让已完成步骤回退".to_string());
}
if existing.status == AGENT_RUNTIME_PLAN_STATUS_FAILED {
return Err("Agent 结构化计划不能改写已失败步骤".to_string());
}
}
let incoming_titles = update
.steps
.iter()
.map(|step| step.step.as_str())
.collect::<std::collections::BTreeSet<_>>();
let mut merged = runtime
.plan_steps
.iter()
.filter(|step| {
matches!(
step.status.as_str(),
AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED
) && !incoming_titles.contains(step.title.as_str())
})
.map(|step| (step.title.clone(), step.status.clone()))
.collect::<Vec<_>>();
merged.extend(update.steps.iter().map(|step| {
let status = terminal_steps
.get(&step.step)
.map(|existing| existing.status.clone())
.unwrap_or_else(|| step.status.clone());
(step.step.clone(), status)
}));
if merged.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT {
return Err(format!(
"Agent 结构化计划保留终态步骤后超过 {AGENT_RUNTIME_PLAN_STEP_LIMIT} 步上限"
));
}
let unchanged = agent_runtime_has_structured_plan(runtime)
&& runtime.plan_explanation == update.explanation
&& runtime.plan_steps.len() == merged.len()
&& runtime
.plan_steps
.iter()
.zip(merged.iter())
.all(|(existing, (title, status))| {
existing.title == *title && existing.status == *status
});
if unchanged {
return Ok(false);
}
let now = unix_timestamp();
let previous_steps = runtime
.plan_steps
.iter()
.map(|step| (step.title.clone(), step.clone()))
.collect::<std::collections::BTreeMap<_, _>>();
runtime.plan_steps = merged
.into_iter()
.enumerate()
.map(|(index, (title, status))| {
let previous = previous_steps.get(&title);
let unchanged_status = previous.is_some_and(|step| step.status == status);
AgentRuntimePlanStep {
index: index as u32,
title,
status,
detail: previous.and_then(|step| step.detail.clone()),
updated_at: if unchanged_status {
previous.map(|step| step.updated_at).unwrap_or(now)
} else {
now
},
}
})
.collect();
runtime.plan = runtime
.plan_steps
.iter()
.map(|step| step.title.clone())
.collect();
runtime.active_plan_step_index = runtime
.plan_steps
.iter()
.find(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS)
.map(|step| step.index);
runtime.plan_explanation = update.explanation;
runtime.plan_revision = runtime.plan_revision.saturating_add(1).max(1);
Ok(true)
}
pub(in crate::agent) fn update_agent_runtime_plan_steps(
runtime: &mut AgentRuntimeState,
plan: Vec<String>,
) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
runtime.plan = plan
.into_iter()
.filter(|item| !item.trim().is_empty())
.take(AGENT_RUNTIME_PLAN_STEP_LIMIT)
.map(|item| sanitize_agent_runtime_text(&item, 180))
.collect();
runtime.plan_steps = runtime
.plan
.iter()
.enumerate()
.map(|(index, title)| AgentRuntimePlanStep {
index: index as u32,
title: title.clone(),
status: if index == 0 { "active" } else { "pending" }.to_string(),
detail: None,
updated_at: unix_timestamp(),
})
.collect();
runtime.active_plan_step_index = if runtime.plan_steps.is_empty() {
None
} else {
Some(0)
};
}
pub(crate) fn activate_agent_runtime_plan_step(
runtime: &mut AgentRuntimeState,
step_index: usize,
detail: &str,
) {
if agent_runtime_has_structured_plan(runtime) || runtime.plan_steps.is_empty() {
return;
}
let target_index = step_index.min(runtime.plan_steps.len().saturating_sub(1));
let now = unix_timestamp();
for step in runtime.plan_steps.iter_mut() {
if step.index as usize == target_index {
step.status = "active".to_string();
step.detail = Some(sanitize_agent_runtime_text(detail, 180));
step.updated_at = now;
} else if step.status == "active" {
step.status = "pending".to_string();
step.updated_at = now;
}
}
runtime.active_plan_step_index = Some(target_index as u32);
}
pub(crate) fn complete_agent_runtime_active_plan_step(
runtime: &mut AgentRuntimeState,
status: &str,
detail: &str,
) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
let Some(active_index) = runtime.active_plan_step_index else {
return;
};
let status = match status {
"failed" => "failed",
"waiting-for-confirmation" => "waiting-for-confirmation",
_ => "completed",
};
let now = unix_timestamp();
for step in runtime.plan_steps.iter_mut() {
if step.index == active_index {
step.status = status.to_string();
step.detail = Some(sanitize_agent_runtime_text(detail, 220));
step.updated_at = now;
break;
}
}
runtime.active_plan_step_index = None;
}
pub(crate) fn retry_agent_runtime_active_plan_step(runtime: &mut AgentRuntimeState, detail: &str) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
let Some(active_index) = runtime.active_plan_step_index else {
return;
};
let now = unix_timestamp();
for step in runtime.plan_steps.iter_mut() {
if step.index == active_index {
step.status = "pending".to_string();
step.detail = Some(sanitize_agent_runtime_text(detail, 220));
step.updated_at = now;
break;
}
}
runtime.active_plan_step_index = None;
}
pub(crate) fn activate_agent_runtime_response_plan_step(
runtime: &mut AgentRuntimeState,
detail: &str,
) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
let target_index = runtime
.plan_steps
.iter()
.find(|step| step.status == "pending" || step.status == "active")
.map(|step| step.index as usize);
if let Some(target_index) = target_index {
activate_agent_runtime_plan_step(runtime, target_index, detail);
return;
}
if runtime.plan_steps.len() >= AGENT_RUNTIME_PLAN_STEP_LIMIT {
return;
}
let index = runtime.plan_steps.len() as u32;
let title = "生成最终回复".to_string();
runtime.plan.push(title.clone());
runtime.plan_steps.push(AgentRuntimePlanStep {
index,
title,
status: "active".to_string(),
detail: Some(sanitize_agent_runtime_text(detail, 180)),
updated_at: unix_timestamp(),
});
runtime.active_plan_step_index = Some(index);
}
pub(in crate::agent) fn fail_agent_runtime_remaining_plan_steps(
runtime: &mut AgentRuntimeState,
detail: &str,
) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
if !agent_runtime_has_structured_plan(runtime) && runtime.active_plan_step_index.is_some() {
complete_agent_runtime_active_plan_step(runtime, "failed", detail);
return;
}
let now = unix_timestamp();
let detail = sanitize_agent_runtime_text(detail, 220);
let mut marked_failed = false;
for step in runtime.plan_steps.iter_mut() {
if matches!(
step.status.as_str(),
"pending" | "active" | "in_progress" | "waiting-for-confirmation"
) {
step.status = "failed".to_string();
step.detail = Some(detail.clone());
step.updated_at = now;
marked_failed = true;
}
}
if !marked_failed {
if let Some(step) = runtime
.plan_steps
.iter_mut()
.rev()
.find(|step| step.status != "failed")
{
step.status = "failed".to_string();
step.detail = Some(detail);
step.updated_at = now;
}
}
runtime.active_plan_step_index = None;
}
pub(crate) fn complete_agent_runtime_remaining_plan_steps(
runtime: &mut AgentRuntimeState,
detail: &str,
) {
if agent_runtime_has_structured_plan(runtime) {
return;
}
let now = unix_timestamp();
for step in runtime.plan_steps.iter_mut() {
if step.status != "failed" {
step.status = "completed".to_string();
if step
.detail
.as_deref()
.map_or(true, |value| value.trim().is_empty())
{
step.detail = Some(sanitize_agent_runtime_text(detail, 180));
}
step.updated_at = now;
}
}
runtime.active_plan_step_index = None;
}
@@ -0,0 +1,310 @@
use super::*;
pub(crate) fn parse_game_creator_agent_tool_plan_response(
content: &str,
) -> Result<AgentRuntimeToolPlan, String> {
parse_game_creator_agent_tool_plan_response_classified(content)
.map_err(|error| error.to_string())
}
pub(in crate::agent) fn parse_game_creator_agent_tool_plan_response_classified(
content: &str,
) -> Result<AgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
let stripped = strip_llm_thinking_blocks(content);
let payload = extract_json_payload(stripped.as_str()).ok_or_else(|| {
AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::ResponseShape,
"Agent 工具计划协议错误:未返回完整 JSON 对象",
)
})?;
parse_game_creator_agent_tool_plan_payload(payload, false)
}
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response(
response: &platform_llm::LlmRunResponse,
) -> Result<ParsedAgentRuntimeToolPlan, String> {
parse_game_creator_agent_tool_plan_llm_response_with_catalog(
response,
&GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
},
)
}
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_with_catalog(
response: &platform_llm::LlmRunResponse,
mcp_catalog: &GameCreatorMcpCatalog,
) -> Result<ParsedAgentRuntimeToolPlan, String> {
parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified(response, mcp_catalog)
.map_err(|error| error.to_string())
}
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified(
response: &platform_llm::LlmRunResponse,
mcp_catalog: &GameCreatorMcpCatalog,
) -> Result<ParsedAgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
if response.tool_calls.is_empty() {
return parse_game_creator_agent_tool_plan_response_classified(response.text.as_str()).map(
|plan| ParsedAgentRuntimeToolPlan {
plan,
protocol: "text_json",
call_id: None,
function_name: None,
call_ids: Vec::new(),
function_names: Vec::new(),
normalization_kinds: Vec::new(),
normalization_count: 0,
normalized_text_chars: 0,
normalized_text_sha256: None,
},
);
}
let mut text_normalization =
normalize_game_creator_agent_tool_plan_function_text(&response.text);
if !text_normalization.visible_text.is_empty() {
if !text_normalization.invalid_thinking_wrapper
&& response.tool_calls.iter().all(|call| {
call.name != AGENT_RUNTIME_RESPOND_FUNCTION_NAME
&& call.name != AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME
})
{
text_normalization.normalize_planner_commentary(&response.text);
} else {
return Err(AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::ResponseShape,
"Agent 原生工具协议错误:当前 function calls 响应不能同时携带普通文本正文(未闭合 thinking、最终回复或 legacy wrapper",
));
}
}
if response.tool_calls.len() == 1
&& response.tool_calls[0].name == AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME
{
let call = &response.tool_calls[0];
let plan = parse_game_creator_agent_tool_plan_payload(call.arguments.as_str(), true)
.map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
error.kind(),
format!("{error}function arguments 解析失败"),
)
})?;
return Ok(ParsedAgentRuntimeToolPlan {
plan,
protocol: "native_function",
call_id: Some(call.id.clone()),
function_name: Some(call.name.clone()),
call_ids: vec![call.id.clone()],
function_names: vec![call.name.clone()],
normalization_kinds: text_normalization.kinds,
normalization_count: text_normalization.count,
normalized_text_chars: text_normalization.source_text_chars,
normalized_text_sha256: text_normalization.source_text_sha256,
});
}
let native = parse_agent_runtime_native_tool_calls(&response.tool_calls, mcp_catalog)?;
let plan = normalize_game_creator_agent_tool_plan(native.plan)?;
Ok(ParsedAgentRuntimeToolPlan {
plan,
protocol: "native_runtime_tools",
call_id: native.call_ids.first().cloned(),
function_name: native.function_names.first().cloned(),
call_ids: native.call_ids,
function_names: native.function_names,
normalization_kinds: text_normalization.kinds,
normalization_count: text_normalization.count,
normalized_text_chars: text_normalization.source_text_chars,
normalized_text_sha256: text_normalization.source_text_sha256,
})
}
#[derive(Default)]
pub(in crate::agent) struct AgentRuntimeToolPlanTextNormalization {
pub(in crate::agent) visible_text: String,
pub(in crate::agent) kinds: Vec<&'static str>,
pub(in crate::agent) count: usize,
pub(in crate::agent) source_text_chars: usize,
pub(in crate::agent) source_text_sha256: Option<String>,
pub(in crate::agent) invalid_thinking_wrapper: bool,
}
impl AgentRuntimeToolPlanTextNormalization {
fn normalize_planner_commentary(&mut self, source: &str) {
if self.visible_text.is_empty() {
return;
}
self.visible_text.clear();
if !self.kinds.contains(&"planner-commentary") {
self.kinds.push("planner-commentary");
}
self.count = self.count.saturating_add(1);
if self.source_text_sha256.is_none() {
self.source_text_chars = source.chars().count();
self.source_text_sha256 = Some(format!("{:x}", Sha256::digest(source.as_bytes())));
}
}
}
pub(in crate::agent) fn normalize_game_creator_agent_tool_plan_function_text(
content: &str,
) -> AgentRuntimeToolPlanTextNormalization {
const THINK_START: &str = "<think>";
const THINK_END: &str = "</think>";
if content.trim().is_empty() {
return AgentRuntimeToolPlanTextNormalization::default();
}
let lower = content.to_ascii_lowercase();
let mut output = String::new();
let mut cursor = 0usize;
let mut scan = 0usize;
let mut depth = 0usize;
let mut count = 0usize;
let mut invalid_thinking_wrapper = false;
loop {
let next_start = lower[scan..].find(THINK_START).map(|index| scan + index);
let next_end = lower[scan..].find(THINK_END).map(|index| scan + index);
match (next_start, next_end) {
(Some(start), Some(end)) if start < end => {
if depth == 0 {
output.push_str(&content[cursor..start]);
}
depth = depth.saturating_add(1);
scan = start + THINK_START.len();
}
(Some(start), None) => {
if depth == 0 {
output.push_str(&content[cursor..start]);
}
depth = depth.saturating_add(1);
scan = start + THINK_START.len();
}
(_, Some(end)) => {
scan = end + THINK_END.len();
if depth == 0 {
invalid_thinking_wrapper = true;
continue;
}
depth -= 1;
if depth == 0 {
cursor = scan;
count = count.saturating_add(1);
}
}
(None, None) => break,
}
}
if depth != 0 || invalid_thinking_wrapper {
return AgentRuntimeToolPlanTextNormalization {
visible_text: content.trim().to_string(),
invalid_thinking_wrapper: true,
..AgentRuntimeToolPlanTextNormalization::default()
};
}
output.push_str(&content[cursor..]);
if count == 0 {
return AgentRuntimeToolPlanTextNormalization {
visible_text: content.trim().to_string(),
..AgentRuntimeToolPlanTextNormalization::default()
};
}
AgentRuntimeToolPlanTextNormalization {
visible_text: output.trim().to_string(),
kinds: vec!["complete-think-block"],
count,
source_text_chars: content.chars().count(),
source_text_sha256: Some(format!("{:x}", Sha256::digest(content.as_bytes()))),
invalid_thinking_wrapper: false,
}
}
pub(in crate::agent) fn game_creator_agent_tool_plan_response_preview(
response: &platform_llm::LlmRunResponse,
max_chars: usize,
) -> String {
if response.tool_calls.is_empty() {
return sanitize_agent_runtime_text(response.text.as_str(), max_chars);
}
let serialized = serde_json::to_string(&response.tool_calls)
.unwrap_or_else(|_| "无法序列化 function calls".to_string());
sanitize_agent_runtime_text(&serialized, max_chars)
}
pub(in crate::agent) fn parse_game_creator_agent_tool_plan_payload(
payload: &str,
require_plan_update_field: bool,
) -> Result<AgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
validate_agent_runtime_protocol_json(payload, "解析 Agent 工具计划失败")?;
let value = serde_json::from_str::<serde_json::Value>(payload).map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsJson,
format!("解析 Agent 工具计划失败:{error}"),
)
})?;
let plan = serde_json::from_str::<AgentRuntimeToolPlan>(payload).map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
format!("解析 Agent 工具计划 schema 失败:{error}"),
)
})?;
if require_plan_update_field {
if !value
.as_object()
.is_some_and(|object| object.contains_key("planUpdate"))
{
return Err(AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
"Agent 工具计划协议错误:native function arguments 必须显式包含 planUpdate",
));
}
}
normalize_game_creator_agent_tool_plan(plan)
}
pub(in crate::agent) fn normalize_game_creator_agent_tool_plan(
mut plan: AgentRuntimeToolPlan,
) -> Result<AgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
if plan.thinking_summary.trim().is_empty() {
return Err(AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
"Agent 工具计划协议错误:thinkingSummary 不能为空",
));
}
if plan
.actions
.iter()
.any(|action| action.tool.trim().is_empty())
{
return Err(AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
"Agent 工具计划协议错误:action.tool 不能为空",
));
}
plan.thinking_summary = truncate_agent_runtime_text(&plan.thinking_summary, 240);
plan.plan_update = plan
.plan_update
.as_ref()
.map(sanitize_agent_runtime_plan_update)
.transpose()
.map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
error,
)
})?;
plan.plan = plan
.plan
.into_iter()
.map(|item| truncate_agent_runtime_text(&item, 160))
.filter(|item| !item.trim().is_empty())
.take(AGENT_RUNTIME_PLAN_STEP_LIMIT)
.collect();
plan.response = truncate_agent_runtime_text(&plan.response, 1_200);
validate_game_creator_agent_user_input_tool_plan(&plan).map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
error,
)
})?;
Ok(plan)
}
@@ -0,0 +1,154 @@
use super::*;
pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
vec![
GAME_CREATOR_USER_INPUT_REQUEST_TOOL,
"memory.read",
"memory.write",
"conversation.read",
"asset.list",
"project.index",
"project.search",
"project.verify",
"project.checkpoint",
"project.restore",
"project.diff",
"git.inspect",
"project.git_commit",
"project.patchset",
"file.list",
"file.read",
"file.write",
"file.patch",
"file.delete",
"task.list",
"task.create",
"task.update",
"command.exec",
"command.output_read",
"command.start",
"command.poll",
"command.stdin",
"command.terminate",
"command.run_limited",
"preview.start",
"preview.validate",
"image.inspect",
"canvas.asset_generate",
"blackboard.write",
"agent.message",
"agent.delegate",
"agent.spawn_isolated",
"agent.schedule_ready",
"agent.action_history",
"agent.run_status",
GAME_CREATOR_MCP_CALL_TOOL,
]
}
pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
root: &Path,
agent_id: &str,
) -> Result<AgentRuntimeToolPolicySnapshot, String> {
let policy = agent_runtime_effective_tool_policy_at(root, agent_id)?;
let isolated = agent_id.trim().starts_with("child-");
let mut auto_tools = Vec::new();
let mut confirm_tools = Vec::new();
let mut denied_tools = Vec::new();
for tool in agent_runtime_executable_tools() {
if isolated && ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) {
denied_tools.push(tool.to_string());
continue;
}
if tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL {
auto_tools.push(tool.to_string());
continue;
}
let Some(command_id) = game_creator_agent_runtime_tool_command_id(tool) else {
continue;
};
if policy
.denied_commands
.iter()
.any(|command| command == command_id)
{
denied_tools.push(tool.to_string());
} else if policy
.confirm_commands
.iter()
.any(|command| command == command_id)
{
confirm_tools.push(tool.to_string());
} else {
auto_tools.push(tool.to_string());
}
}
Ok(AgentRuntimeToolPolicySnapshot {
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
allowed_tools: agent_runtime_executable_tools()
.into_iter()
.map(str::to_string)
.collect(),
auto_tools,
confirm_tools,
denied_tools,
updated_at: unix_timestamp(),
})
}
pub(crate) fn agent_runtime_tool_policy_snapshot_for_run_at(
root: &Path,
agent_id: &str,
run_id: &str,
stored_profile: Option<&str>,
stored_binding_fingerprint: Option<&str>,
) -> Result<AgentRuntimeToolPolicySnapshot, String> {
let mut snapshot = agent_runtime_tool_policy_snapshot_at(root, agent_id)?;
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
root,
agent_id,
run_id,
stored_profile,
stored_binding_fingerprint,
)?;
snapshot.run_profile = run_profile.clone();
snapshot.run_profile_binding_fingerprint = binding_fingerprint;
if run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Ok(snapshot);
}
snapshot
.auto_tools
.retain(|tool| tool != GAME_CREATOR_USER_INPUT_REQUEST_TOOL);
snapshot
.confirm_tools
.retain(|tool| tool != GAME_CREATOR_USER_INPUT_REQUEST_TOOL);
if !snapshot
.denied_tools
.iter()
.any(|tool| tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL)
{
snapshot
.denied_tools
.push(GAME_CREATOR_USER_INPUT_REQUEST_TOOL.to_string());
}
for tool in agent_runtime_executable_tools() {
let Some(command_id) = game_creator_agent_runtime_tool_command_id(tool) else {
continue;
};
if !AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS.contains(&command_id)
|| snapshot.denied_tools.iter().any(|denied| denied == tool)
{
continue;
}
snapshot.confirm_tools.retain(|candidate| candidate != tool);
if !snapshot
.auto_tools
.iter()
.any(|candidate| candidate == tool)
{
snapshot.auto_tools.push(tool.to_string());
}
}
Ok(snapshot)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More