0aaa191f8d
将 runtime_tools 拆为十五个职责模块并保留 Agent 可见性 将 Runner 拆为协议端点分发客户端与所有权模块 将进程会话拆为模型持久化生命周期 IO 恢复与测试模块 将项目摘要拆为十四个无环模块并保留一百一十二个导出 记录并行拆分边界与稳定树验收规则
630 lines
24 KiB
Rust
630 lines
24 KiB
Rust
use super::*;
|
||
|
||
pub(super) fn validate_process_session_identity(
|
||
identity: &ProcessSessionIdentity,
|
||
) -> Result<(), String> {
|
||
for (label, value, max_chars) in [
|
||
("projectId", identity.project_id.as_str(), 160),
|
||
("agentId", identity.agent_id.as_str(), 96),
|
||
("taskId", identity.task_id.as_str(), 96),
|
||
(
|
||
"conversationSessionId",
|
||
identity.conversation_session_id.as_str(),
|
||
160,
|
||
),
|
||
("runId", identity.run_id.as_str(), 160),
|
||
("startActionId", identity.start_action_id.as_str(), 160),
|
||
] {
|
||
let trimmed = value.trim();
|
||
if trimmed.is_empty()
|
||
|| trimmed.chars().count() > max_chars
|
||
|| trimmed.chars().any(|character| character.is_control())
|
||
{
|
||
return Err(format!("command.start {label} 无效"));
|
||
}
|
||
}
|
||
if identity.start_action_fingerprint.len() != 64
|
||
|| !identity
|
||
.start_action_fingerprint
|
||
.bytes()
|
||
.all(|byte| byte.is_ascii_hexdigit())
|
||
{
|
||
return Err("command.start action fingerprint 无效".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(super) fn validate_process_id(process_id: &str) -> Result<(), String> {
|
||
if process_id.len() != 37
|
||
|| !process_id.starts_with("proc-")
|
||
|| !process_id[5..].bytes().all(|byte| byte.is_ascii_hexdigit())
|
||
{
|
||
return Err("processId 格式无效".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(super) fn process_session_id(identity: &ProcessSessionIdentity) -> String {
|
||
let payload = serde_json::to_vec(&serde_json::json!({
|
||
"projectId": identity.project_id,
|
||
"agentId": identity.agent_id,
|
||
"taskId": identity.task_id,
|
||
"conversationSessionId": identity.conversation_session_id,
|
||
"runId": identity.run_id,
|
||
"startActionId": identity.start_action_id,
|
||
"startActionFingerprint": identity.start_action_fingerprint,
|
||
"ownerBootId": process_session_boot_id(),
|
||
}))
|
||
.unwrap_or_default();
|
||
let value = format!("{:x}", Sha256::digest(payload));
|
||
format!("proc-{}", &value[..32])
|
||
}
|
||
|
||
pub(super) fn process_session_record_relative_path(process_id: &str) -> String {
|
||
format!(".agent/runtime/process-sessions/{process_id}.json")
|
||
}
|
||
|
||
pub(super) fn process_session_transcript_relative_path(process_id: &str) -> String {
|
||
format!(".agent/runtime/process-sessions/{process_id}.output.json")
|
||
}
|
||
|
||
pub(super) fn process_session_cursor(process_id: &str, offset: usize) -> String {
|
||
format!("{PROCESS_SESSION_CURSOR_VERSION}:{process_id}:{offset}")
|
||
}
|
||
|
||
pub(super) fn parse_process_session_cursor(
|
||
process_id: &str,
|
||
cursor: Option<&str>,
|
||
output: &str,
|
||
) -> Result<usize, String> {
|
||
let Some(cursor) = cursor.filter(|value| !value.trim().is_empty()) else {
|
||
return Ok(0);
|
||
};
|
||
let mut parts = cursor.split(':');
|
||
let version = parts.next().unwrap_or_default();
|
||
let cursor_process_id = parts.next().unwrap_or_default();
|
||
let offset = parts
|
||
.next()
|
||
.ok_or_else(|| "command.poll cursor 无效".to_string())?
|
||
.parse::<usize>()
|
||
.map_err(|_| "command.poll cursor offset 无效".to_string())?;
|
||
if parts.next().is_some()
|
||
|| version != PROCESS_SESSION_CURSOR_VERSION
|
||
|| cursor_process_id != process_id
|
||
|| offset > output.len()
|
||
|| !output.is_char_boundary(offset)
|
||
{
|
||
return Err("command.poll cursor 与当前进程输出不匹配".to_string());
|
||
}
|
||
Ok(offset)
|
||
}
|
||
|
||
pub(super) fn write_process_session_record(
|
||
root: &Path,
|
||
record: &ProcessSessionRecord,
|
||
) -> Result<(), String> {
|
||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||
root,
|
||
&process_session_record_relative_path(&record.process_id),
|
||
"Agent Runtime process session",
|
||
record,
|
||
PROCESS_SESSION_RECORD_MAX_BYTES,
|
||
)
|
||
}
|
||
|
||
pub(super) fn read_process_session_record(
|
||
root: &Path,
|
||
process_id: &str,
|
||
) -> Result<Option<ProcessSessionRecord>, String> {
|
||
validate_process_id(process_id)?;
|
||
let mut record = read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionRecord>(
|
||
root,
|
||
&process_session_record_relative_path(process_id),
|
||
"Agent Runtime process session",
|
||
PROCESS_SESSION_RECORD_MAX_BYTES,
|
||
)?;
|
||
if let Some(record) = record.as_mut() {
|
||
let normalized = normalize_process_session_record(record);
|
||
validate_process_session_record(root, record, process_id)?;
|
||
if normalized {
|
||
write_process_session_record(root, record)?;
|
||
}
|
||
}
|
||
Ok(record)
|
||
}
|
||
|
||
pub(super) fn normalize_process_session_record(record: &mut ProcessSessionRecord) -> bool {
|
||
let legacy_schema = matches!(record.schema_version.as_str(), "1" | "2");
|
||
if !legacy_schema {
|
||
return false;
|
||
}
|
||
if record.schema_version == "1" {
|
||
record.sandbox_backend = "legacy-unknown".to_string();
|
||
record.sandbox_mode = "unknown".to_string();
|
||
record.network_access = "unknown".to_string();
|
||
record.sandbox_profile_version = "legacy-v1".to_string();
|
||
}
|
||
let legacy_active = matches!(
|
||
record.status.as_str(),
|
||
"prepared" | "launching" | "running" | "terminating"
|
||
);
|
||
record.schema_version = PROCESS_SESSION_SCHEMA_VERSION.to_string();
|
||
record.sandbox_establishment = "unknown".to_string();
|
||
record.target_exec = "unknown".to_string();
|
||
record.launch_failure_kind = Some(
|
||
if legacy_active {
|
||
"legacy-active-record"
|
||
} else {
|
||
"legacy-record"
|
||
}
|
||
.to_string(),
|
||
);
|
||
record.sandbox_ready_at = None;
|
||
record.exec_established_at = None;
|
||
if legacy_active {
|
||
record.status = "needs-reconciliation".to_string();
|
||
record.stdin_open = false;
|
||
record.needs_reconciliation = true;
|
||
record.terminal_at = Some(unix_timestamp());
|
||
record.updated_at = unix_timestamp();
|
||
}
|
||
legacy_active
|
||
}
|
||
|
||
pub(super) fn validate_process_session_record(
|
||
root: &Path,
|
||
record: &ProcessSessionRecord,
|
||
process_id: &str,
|
||
) -> Result<(), String> {
|
||
if record.schema_version != PROCESS_SESSION_SCHEMA_VERSION
|
||
|| record.process_id != process_id
|
||
|| record.project_id != game_creator_agent_runtime_context_project_id(root)?
|
||
|| record.sandbox_backend.is_empty()
|
||
|| record.sandbox_mode.is_empty()
|
||
|| record.network_access.is_empty()
|
||
|| record.sandbox_profile_version.is_empty()
|
||
|| !matches!(
|
||
record.sandbox_establishment.as_str(),
|
||
"not-established" | "established" | "unknown"
|
||
)
|
||
|| !matches!(
|
||
record.target_exec.as_str(),
|
||
"not-attempted" | "established" | "failed" | "unknown"
|
||
)
|
||
|| record.launch_failure_kind.as_deref().is_some_and(|value| {
|
||
!matches!(
|
||
value,
|
||
"pre-exec-failed"
|
||
| "durable-commit-failed"
|
||
| "target-exec-failed"
|
||
| "launch-unknown"
|
||
| "legacy-active-record"
|
||
| "legacy-record"
|
||
| "start-audit-failed"
|
||
)
|
||
})
|
||
{
|
||
return Err("Agent Runtime process session 身份不匹配".to_string());
|
||
}
|
||
validate_process_id(&record.process_id)?;
|
||
if !matches!(
|
||
record.status.as_str(),
|
||
"prepared"
|
||
| "launching"
|
||
| "running"
|
||
| "terminating"
|
||
| "exited"
|
||
| "terminated"
|
||
| "timed-out"
|
||
| "output-limit-exceeded"
|
||
| "needs-reconciliation"
|
||
| "failed"
|
||
) {
|
||
return Err("Agent Runtime process session 状态无效".to_string());
|
||
}
|
||
if matches!(
|
||
record.status.as_str(),
|
||
"prepared" | "launching" | "running" | "terminating"
|
||
) && record.terminal_at.is_some()
|
||
{
|
||
return Err("运行中的 process session 不应有 terminalAt".to_string());
|
||
}
|
||
if !matches!(
|
||
record.status.as_str(),
|
||
"prepared" | "launching" | "running" | "terminating"
|
||
) && record.terminal_at.is_none()
|
||
{
|
||
return Err("终态 process session 缺少 terminalAt".to_string());
|
||
}
|
||
let launch_state_valid = match record.launch_failure_kind.as_deref() {
|
||
None => match record.status.as_str() {
|
||
"prepared" => {
|
||
record.sandbox_establishment == "not-established"
|
||
&& record.target_exec == "not-attempted"
|
||
&& !record.needs_reconciliation
|
||
}
|
||
"launching" => {
|
||
matches!(
|
||
record.sandbox_establishment.as_str(),
|
||
"not-established" | "established"
|
||
) && record.target_exec == "not-attempted"
|
||
&& !record.needs_reconciliation
|
||
}
|
||
"running" | "terminating" => {
|
||
record.sandbox_establishment == "established"
|
||
&& record.target_exec == "established"
|
||
&& !record.needs_reconciliation
|
||
}
|
||
"needs-reconciliation" => {
|
||
record.sandbox_establishment == "established"
|
||
&& record.target_exec == "established"
|
||
&& record.needs_reconciliation
|
||
}
|
||
"exited" | "terminated" | "timed-out" | "output-limit-exceeded" | "failed" => {
|
||
record.sandbox_establishment == "established" && record.target_exec == "established"
|
||
}
|
||
_ => false,
|
||
},
|
||
Some("pre-exec-failed") => {
|
||
record.status == "failed"
|
||
&& record.sandbox_establishment == "not-established"
|
||
&& record.target_exec == "not-attempted"
|
||
&& !record.needs_reconciliation
|
||
}
|
||
Some("durable-commit-failed") => {
|
||
record.status == "failed"
|
||
&& matches!(
|
||
record.sandbox_establishment.as_str(),
|
||
"not-established" | "established"
|
||
)
|
||
&& record.target_exec == "not-attempted"
|
||
&& !record.needs_reconciliation
|
||
}
|
||
Some("target-exec-failed") => {
|
||
matches!(record.status.as_str(), "failed" | "needs-reconciliation")
|
||
&& record.sandbox_establishment == "established"
|
||
&& record.target_exec == "failed"
|
||
&& (record.status == "needs-reconciliation") == record.needs_reconciliation
|
||
}
|
||
Some("launch-unknown") => {
|
||
record.status == "needs-reconciliation"
|
||
&& record.needs_reconciliation
|
||
&& record.target_exec == "unknown"
|
||
&& matches!(
|
||
record.sandbox_establishment.as_str(),
|
||
"established" | "unknown"
|
||
)
|
||
}
|
||
Some("legacy-active-record") => {
|
||
record.status == "needs-reconciliation"
|
||
&& record.needs_reconciliation
|
||
&& record.sandbox_establishment == "unknown"
|
||
&& record.target_exec == "unknown"
|
||
}
|
||
Some("legacy-record") => {
|
||
!matches!(
|
||
record.status.as_str(),
|
||
"prepared" | "launching" | "running" | "terminating"
|
||
) && record.sandbox_establishment == "unknown"
|
||
&& record.target_exec == "unknown"
|
||
&& (record.status != "needs-reconciliation" || record.needs_reconciliation)
|
||
}
|
||
Some("start-audit-failed") => {
|
||
record.status == "needs-reconciliation"
|
||
&& record.needs_reconciliation
|
||
&& record.sandbox_establishment == "established"
|
||
&& record.target_exec == "established"
|
||
}
|
||
Some(_) => false,
|
||
};
|
||
if !launch_state_valid {
|
||
return Err("process session 可信 launch 状态组合无效".to_string());
|
||
}
|
||
if record.started_at > record.updated_at
|
||
|| record.terminal_at.is_some_and(|terminal_at| {
|
||
record.started_at > terminal_at || terminal_at > record.updated_at
|
||
})
|
||
{
|
||
return Err("process session 生命周期时间顺序无效".to_string());
|
||
}
|
||
match record.sandbox_establishment.as_str() {
|
||
"established" if record.sandbox_ready_at.is_none() => {
|
||
return Err("已建立的 process session sandbox 缺少 ready 时间".to_string());
|
||
}
|
||
"not-established" | "unknown" if record.sandbox_ready_at.is_some() => {
|
||
return Err("未建立或未知的 process session sandbox 不应有 ready 时间".to_string());
|
||
}
|
||
_ => {}
|
||
}
|
||
match record.target_exec.as_str() {
|
||
"established" if record.exec_established_at.is_none() => {
|
||
return Err("已建立的 process session target 缺少 exec 时间".to_string());
|
||
}
|
||
"not-attempted" | "failed" | "unknown" if record.exec_established_at.is_some() => {
|
||
return Err("未建立的 process session target 不应有 exec 时间".to_string());
|
||
}
|
||
_ => {}
|
||
}
|
||
if let Some(sandbox_ready_at) = record.sandbox_ready_at {
|
||
if record.started_at > sandbox_ready_at
|
||
|| sandbox_ready_at > record.updated_at
|
||
|| record
|
||
.terminal_at
|
||
.is_some_and(|terminal_at| sandbox_ready_at > terminal_at)
|
||
{
|
||
return Err("process session sandbox-ready 时间顺序无效".to_string());
|
||
}
|
||
}
|
||
if let Some(exec_established_at) = record.exec_established_at {
|
||
if record
|
||
.sandbox_ready_at
|
||
.is_none_or(|sandbox_ready_at| sandbox_ready_at > exec_established_at)
|
||
|| exec_established_at > record.updated_at
|
||
|| record
|
||
.terminal_at
|
||
.is_some_and(|terminal_at| exec_established_at > terminal_at)
|
||
{
|
||
return Err("process session exec-established 时间顺序无效".to_string());
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(super) fn reconcile_stale_active_process_session(record: &mut ProcessSessionRecord) {
|
||
let previous_status = record.status.clone();
|
||
record.status = "needs-reconciliation".to_string();
|
||
record.stdin_open = false;
|
||
record.needs_reconciliation = true;
|
||
if previous_status == "prepared" {
|
||
record.sandbox_establishment = "unknown".to_string();
|
||
record.sandbox_ready_at = None;
|
||
record.target_exec = "unknown".to_string();
|
||
record.exec_established_at = None;
|
||
record.launch_failure_kind = Some("launch-unknown".to_string());
|
||
} else if previous_status == "launching" {
|
||
if record.sandbox_establishment != "established" {
|
||
record.sandbox_establishment = "unknown".to_string();
|
||
record.sandbox_ready_at = None;
|
||
}
|
||
record.target_exec = "unknown".to_string();
|
||
record.exec_established_at = None;
|
||
record.launch_failure_kind = Some("launch-unknown".to_string());
|
||
}
|
||
record.terminal_at = Some(unix_timestamp());
|
||
record.updated_at = unix_timestamp();
|
||
}
|
||
|
||
pub(super) fn process_session_record_from_live(
|
||
live: &LiveProcessSession,
|
||
output: &ProcessOutputState,
|
||
) -> ProcessSessionRecord {
|
||
let output_sha256 = format!("{:x}", Sha256::digest(output.text.as_bytes()));
|
||
let terminal = output.status != "running";
|
||
ProcessSessionRecord {
|
||
schema_version: PROCESS_SESSION_SCHEMA_VERSION.to_string(),
|
||
project_id: live.identity.project_id.clone(),
|
||
agent_id: live.identity.agent_id.clone(),
|
||
task_id: live.identity.task_id.clone(),
|
||
conversation_session_id: live.identity.conversation_session_id.clone(),
|
||
run_id: live.identity.run_id.clone(),
|
||
start_action_id: live.identity.start_action_id.clone(),
|
||
start_action_fingerprint: live.identity.start_action_fingerprint.clone(),
|
||
process_id: live.process_id.clone(),
|
||
owner_boot_id: process_session_boot_id().to_string(),
|
||
command_id: live.command_id.clone(),
|
||
program: live.program.clone(),
|
||
cwd: live.cwd.clone(),
|
||
sandbox_backend: live.sandbox_backend.clone(),
|
||
sandbox_mode: live.sandbox_mode.clone(),
|
||
network_access: live.network_access.clone(),
|
||
sandbox_profile_version: live.sandbox_profile_version.clone(),
|
||
sandbox_establishment: live.sandbox_establishment.clone(),
|
||
target_exec: live.target_exec.clone(),
|
||
launch_failure_kind: output.launch_failure_kind.clone(),
|
||
sandbox_ready_at: live.sandbox_ready_at,
|
||
exec_established_at: live.exec_established_at,
|
||
status: output.status.clone(),
|
||
exit_code: output.exit_code,
|
||
signal: output.signal.clone(),
|
||
stdin_open: output.stdin_open,
|
||
output_bytes: output.text.len(),
|
||
output_sha256,
|
||
output_ref: Some(process_session_transcript_relative_path(&live.process_id)),
|
||
source_fingerprint_before: live.source_fingerprint_before.clone(),
|
||
source_fingerprint_after: output.source_fingerprint_after.clone(),
|
||
source_changed: output.source_changed,
|
||
needs_reconciliation: output.needs_reconciliation,
|
||
started_at: live.started_at,
|
||
terminal_at: terminal.then(unix_timestamp),
|
||
updated_at: unix_timestamp(),
|
||
}
|
||
}
|
||
|
||
pub(super) fn initial_process_session_record(
|
||
identity: &ProcessSessionIdentity,
|
||
process_id: &str,
|
||
command_id: &str,
|
||
spec: &ProjectCommandSpec,
|
||
launch: Option<&ProjectCommandLaunchSpec>,
|
||
source_fingerprint_before: &str,
|
||
status: &str,
|
||
) -> ProcessSessionRecord {
|
||
let now = unix_timestamp();
|
||
let sandbox_backend = launch
|
||
.map(|value| value.sandbox_backend.clone())
|
||
.unwrap_or_else(|| "test-unknown".to_string());
|
||
let sandbox_mode = launch
|
||
.map(|value| value.sandbox_mode.clone())
|
||
.unwrap_or_else(|| "unknown".to_string());
|
||
let network_access = launch
|
||
.map(|value| value.network_access.clone())
|
||
.unwrap_or_else(|| "unknown".to_string());
|
||
let sandbox_profile_version = launch
|
||
.map(|value| value.sandbox_profile_version.clone())
|
||
.unwrap_or_else(|| "test-v1".to_string());
|
||
ProcessSessionRecord {
|
||
schema_version: PROCESS_SESSION_SCHEMA_VERSION.to_string(),
|
||
project_id: identity.project_id.clone(),
|
||
agent_id: identity.agent_id.clone(),
|
||
task_id: identity.task_id.clone(),
|
||
conversation_session_id: identity.conversation_session_id.clone(),
|
||
run_id: identity.run_id.clone(),
|
||
start_action_id: identity.start_action_id.clone(),
|
||
start_action_fingerprint: identity.start_action_fingerprint.clone(),
|
||
process_id: process_id.to_string(),
|
||
owner_boot_id: process_session_boot_id().to_string(),
|
||
command_id: command_id.to_string(),
|
||
program: spec.program.clone(),
|
||
cwd: spec.cwd_relative.clone(),
|
||
sandbox_backend,
|
||
sandbox_mode,
|
||
network_access,
|
||
sandbox_profile_version,
|
||
sandbox_establishment: "not-established".to_string(),
|
||
target_exec: "not-attempted".to_string(),
|
||
launch_failure_kind: None,
|
||
sandbox_ready_at: None,
|
||
exec_established_at: None,
|
||
status: status.to_string(),
|
||
exit_code: None,
|
||
signal: None,
|
||
stdin_open: false,
|
||
output_bytes: 0,
|
||
output_sha256: format!("{:x}", Sha256::digest([])),
|
||
output_ref: None,
|
||
source_fingerprint_before: source_fingerprint_before.to_string(),
|
||
source_fingerprint_after: None,
|
||
source_changed: None,
|
||
needs_reconciliation: false,
|
||
started_at: now,
|
||
terminal_at: None,
|
||
updated_at: now,
|
||
}
|
||
}
|
||
|
||
#[cfg(not(target_os = "linux"))]
|
||
pub(super) fn process_session_launch_failed(
|
||
root: &Path,
|
||
record: &mut ProcessSessionRecord,
|
||
error: String,
|
||
) -> String {
|
||
record.status = "failed".to_string();
|
||
record.target_exec = "not-attempted".to_string();
|
||
record.launch_failure_kind = Some("pre-exec-failed".to_string());
|
||
record.stdin_open = false;
|
||
record.terminal_at = Some(unix_timestamp());
|
||
record.updated_at = unix_timestamp();
|
||
match write_process_session_record(root, record) {
|
||
Ok(()) => error,
|
||
Err(record_error) => format!("{error};process session 失败终态无法落盘:{record_error}"),
|
||
}
|
||
}
|
||
|
||
pub(super) fn validate_process_session_access(
|
||
record: &ProcessSessionRecord,
|
||
identity: &ProcessSessionIdentity,
|
||
) -> Result<(), String> {
|
||
if record.project_id != identity.project_id
|
||
|| record.agent_id != identity.agent_id
|
||
|| record.task_id != identity.task_id
|
||
|| record.conversation_session_id != identity.conversation_session_id
|
||
|| record.run_id != identity.run_id
|
||
{
|
||
return Err("process session 不属于当前 Agent run".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(crate) fn process_session_identity_for_run_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
task_id: &str,
|
||
conversation_session_id: &str,
|
||
run_id: &str,
|
||
process_id: &str,
|
||
) -> Result<ProcessSessionIdentity, String> {
|
||
let record = read_process_session_record(root, process_id)?
|
||
.ok_or_else(|| "process session 不存在".to_string())?;
|
||
if record.agent_id != agent_id
|
||
|| record.task_id != task_id
|
||
|| record.conversation_session_id != conversation_session_id
|
||
|| record.run_id != run_id
|
||
{
|
||
return Err("process session 不属于当前 Agent run".to_string());
|
||
}
|
||
Ok(ProcessSessionIdentity {
|
||
project_id: record.project_id,
|
||
agent_id: record.agent_id,
|
||
task_id: record.task_id,
|
||
conversation_session_id: record.conversation_session_id,
|
||
run_id: record.run_id,
|
||
start_action_id: record.start_action_id,
|
||
start_action_fingerprint: record.start_action_fingerprint,
|
||
})
|
||
}
|
||
|
||
pub(super) fn validate_process_session_transcript(
|
||
transcript: &ProcessSessionTranscript,
|
||
record: &ProcessSessionRecord,
|
||
) -> Result<(), String> {
|
||
let output_sha256 = format!("{:x}", Sha256::digest(transcript.output.as_bytes()));
|
||
if !matches!(transcript.schema_version.as_str(), "1" | "2")
|
||
|| transcript.project_id != record.project_id
|
||
|| transcript.agent_id != record.agent_id
|
||
|| transcript.task_id != record.task_id
|
||
|| transcript.conversation_session_id != record.conversation_session_id
|
||
|| transcript.run_id != record.run_id
|
||
|| transcript.start_action_id != record.start_action_id
|
||
|| transcript.start_action_fingerprint != record.start_action_fingerprint
|
||
|| transcript.process_id != record.process_id
|
||
|| transcript.output_bytes != transcript.output.len()
|
||
|| transcript.output_sha256 != output_sha256
|
||
|| record.output_bytes != transcript.output_bytes
|
||
|| record.output_sha256 != transcript.output_sha256
|
||
{
|
||
return Err("Agent Runtime process transcript 身份或摘要不匹配".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(super) fn find_existing_start_action_record(
|
||
root: &Path,
|
||
identity: &ProcessSessionIdentity,
|
||
) -> Result<Option<ProcessSessionRecord>, String> {
|
||
let directory = root.join(".agent/runtime/process-sessions");
|
||
let entries = match fs::read_dir(&directory) {
|
||
Ok(entries) => entries,
|
||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||
Err(error) => return Err(format!("读取 process session 目录失败:{error}")),
|
||
};
|
||
for entry in entries {
|
||
let entry = entry.map_err(|error| format!("读取 process session 目录项失败:{error}"))?;
|
||
let name = entry.file_name();
|
||
let Some(name) = name.to_str() else {
|
||
continue;
|
||
};
|
||
let Some(process_id) = name
|
||
.strip_suffix(".json")
|
||
.filter(|value| !value.ends_with(".output"))
|
||
else {
|
||
continue;
|
||
};
|
||
if validate_process_id(process_id).is_err() {
|
||
continue;
|
||
}
|
||
let Some(record) = read_process_session_record(root, process_id)? else {
|
||
continue;
|
||
};
|
||
if record.agent_id == identity.agent_id
|
||
&& record.run_id == identity.run_id
|
||
&& record.start_action_id == identity.start_action_id
|
||
{
|
||
if record.start_action_fingerprint != identity.start_action_fingerprint {
|
||
return Err("command.start action identity 冲突".to_string());
|
||
}
|
||
return Ok(Some(record));
|
||
}
|
||
}
|
||
Ok(None)
|
||
}
|