Files
Genarrative/server-rs/crates/agent-runtime-core/src/runtime.rs
T
AIGameCreator App d4075c3423
Project CI / Frontend tests (push) Failing after 3m43s
Project CI / Native shell tests (push) Failing after 3m36s
Project CI / Repository checks (push) Failing after 4m36s
Project CI / Backend tests (push) Successful in 8m36s
抽取通用多智能体运行时与可扩展Provider
新增纯 Rust agent-runtime-core,提供运行时契约、能力注册、Agent 目录、Profile、完成策略与零重放恢复
抽取中立 LLM Provider 协议与可扩展 Registry,并适配 OpenAI Responses、OpenAI Chat 和 Anthropic
将 AGC interaction、Provider 控制、Runner 生命周期、steering 与 tool-plan handoff 接入统一运行时边界
补充非游戏消费者、Provider 网络闭环、Runtime 恢复及 GUI Runner owner 测试
同步 Cargo/npm 门禁、Runtime 技术方案和项目共享记忆
2026-07-30 16:15:51 +08:00

1469 lines
43 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::BTreeSet;
use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::capability::CapabilityRegistry;
use crate::completion::{CompletionBlocker, CompletionPolicy};
use crate::contract::{validate_description, validate_identifier, validate_metadata};
pub const RUNTIME_SNAPSHOT_SCHEMA_VERSION: &str = "agent-runtime-core-snapshot.v1";
const MAX_DELEGATION_CHILDREN: usize = 64;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RunStatus {
Pending,
Running,
WaitingForAction,
WaitingForChildren,
Paused,
Completed,
Failed,
Cancelled,
NeedsReconciliation,
}
impl RunStatus {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
}
fn occupies_agent_lane(self) -> bool {
!matches!(self, Self::Pending) && !self.is_terminal()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ActionStatus {
Queued,
Executing,
Observed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ObservationStatus {
Completed,
Failed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum JoinMode {
All,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RunSpec {
run_id: String,
agent_id: String,
task: String,
metadata: Value,
}
impl RunSpec {
pub fn try_new(
run_id: impl Into<String>,
agent_id: impl Into<String>,
task: impl Into<String>,
) -> Result<Self, RuntimeError> {
let run_id = run_id.into();
let agent_id = agent_id.into();
let task = task.into();
validate_identifier(&run_id, "run id")?;
validate_identifier(&agent_id, "agent id")?;
validate_description(&task, "run task")?;
Ok(Self {
run_id,
agent_id,
task,
metadata: Value::Object(Map::new()),
})
}
pub fn with_metadata(mut self, metadata: Value) -> Result<Self, RuntimeError> {
validate_metadata(&metadata, "run metadata")?;
self.metadata = metadata;
Ok(self)
}
pub fn run_id(&self) -> &str {
&self.run_id
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn task(&self) -> &str {
&self.task
}
pub fn metadata(&self) -> &Value {
&self.metadata
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RuntimeAction {
id: String,
capability_id: String,
input: Value,
}
impl RuntimeAction {
pub fn try_new(
id: impl Into<String>,
capability_id: impl Into<String>,
input: Value,
) -> Result<Self, RuntimeError> {
let id = id.into();
let capability_id = capability_id.into();
validate_identifier(&id, "action id")?;
validate_identifier(&capability_id, "capability id")?;
if !input.is_object() {
return Err(RuntimeError::invalid("action input 必须是 JSON object"));
}
Ok(Self {
id,
capability_id,
input,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn capability_id(&self) -> &str {
&self.capability_id
}
pub fn input(&self) -> &Value {
&self.input
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ActionRecord {
action: RuntimeAction,
status: ActionStatus,
updated_at_ms: u64,
}
impl ActionRecord {
pub fn action(&self) -> &RuntimeAction {
&self.action
}
pub fn status(&self) -> ActionStatus {
self.status
}
pub fn updated_at_ms(&self) -> u64 {
self.updated_at_ms
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ToolOutput {
status: ObservationStatus,
summary: String,
output: Value,
}
impl ToolOutput {
pub fn completed(summary: impl Into<String>, output: Value) -> Result<Self, RuntimeError> {
Self::try_new(ObservationStatus::Completed, summary, output)
}
pub fn failed(summary: impl Into<String>, output: Value) -> Result<Self, RuntimeError> {
Self::try_new(ObservationStatus::Failed, summary, output)
}
fn try_new(
status: ObservationStatus,
summary: impl Into<String>,
output: Value,
) -> Result<Self, RuntimeError> {
let summary = summary.into();
validate_description(&summary, "tool output summary")?;
Ok(Self {
status,
summary,
output,
})
}
pub fn status(&self) -> ObservationStatus {
self.status
}
pub fn summary(&self) -> &str {
&self.summary
}
pub fn output(&self) -> &Value {
&self.output
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ToolExecution {
Observed(ToolOutput),
Unknown,
}
pub trait ToolHost {
fn execute(&mut self, run: &RunRecord, action: &RuntimeAction) -> ToolExecution;
}
pub trait RuntimeClock {
fn now_millis(&self) -> u64;
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RuntimeObservation {
action_id: String,
capability_id: String,
status: ObservationStatus,
summary: String,
output: Value,
observed_at_ms: u64,
}
impl RuntimeObservation {
pub fn action_id(&self) -> &str {
&self.action_id
}
pub fn capability_id(&self) -> &str {
&self.capability_id
}
pub fn status(&self) -> ObservationStatus {
self.status
}
pub fn summary(&self) -> &str {
&self.summary
}
pub fn output(&self) -> &Value {
&self.output
}
pub fn observed_at_ms(&self) -> u64 {
self.observed_at_ms
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RunRecord {
run_id: String,
agent_id: String,
task: String,
metadata: Value,
parent_run_id: Option<String>,
delegation_group_id: Option<String>,
status: RunStatus,
actions: Vec<ActionRecord>,
observations: Vec<RuntimeObservation>,
terminal_summary: Option<String>,
terminal_error: Option<String>,
created_at_ms: u64,
updated_at_ms: u64,
}
impl RunRecord {
pub fn run_id(&self) -> &str {
&self.run_id
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn task(&self) -> &str {
&self.task
}
pub fn metadata(&self) -> &Value {
&self.metadata
}
pub fn parent_run_id(&self) -> Option<&str> {
self.parent_run_id.as_deref()
}
pub fn delegation_group_id(&self) -> Option<&str> {
self.delegation_group_id.as_deref()
}
pub fn status(&self) -> RunStatus {
self.status
}
pub fn actions(&self) -> &[ActionRecord] {
&self.actions
}
pub fn observations(&self) -> &[RuntimeObservation] {
&self.observations
}
pub fn terminal_summary(&self) -> Option<&str> {
self.terminal_summary.as_deref()
}
pub fn terminal_error(&self) -> Option<&str> {
self.terminal_error.as_deref()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RunTerminalResult {
run_id: String,
status: RunStatus,
summary: Option<String>,
error: Option<String>,
}
impl RunTerminalResult {
pub fn run_id(&self) -> &str {
&self.run_id
}
pub fn status(&self) -> RunStatus {
self.status
}
pub fn summary(&self) -> Option<&str> {
self.summary.as_deref()
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct DelegationGroup {
id: String,
parent_run_id: String,
child_run_ids: Vec<String>,
join_mode: JoinMode,
resolved: bool,
results: Vec<RunTerminalResult>,
}
impl DelegationGroup {
pub fn id(&self) -> &str {
&self.id
}
pub fn parent_run_id(&self) -> &str {
&self.parent_run_id
}
pub fn child_run_ids(&self) -> &[String] {
&self.child_run_ids
}
pub fn join_mode(&self) -> JoinMode {
self.join_mode
}
pub fn is_resolved(&self) -> bool {
self.resolved
}
pub fn results(&self) -> &[RunTerminalResult] {
&self.results
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RuntimeSnapshot {
schema_version: String,
runtime_id: String,
revision: u64,
runs: Vec<RunRecord>,
delegations: Vec<DelegationGroup>,
}
impl RuntimeSnapshot {
pub fn schema_version(&self) -> &str {
&self.schema_version
}
pub fn runtime_id(&self) -> &str {
&self.runtime_id
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn runs(&self) -> &[RunRecord] {
&self.runs
}
pub fn run(&self, run_id: &str) -> Option<&RunRecord> {
self.runs.iter().find(|run| run.run_id == run_id)
}
pub fn delegations(&self) -> &[DelegationGroup] {
&self.delegations
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RuntimeEventKind {
RuntimeCreated,
RunAdded,
RunStarted,
ActionQueued,
ActionExecuting,
ActionObserved,
ActionNeedsReconciliation,
DelegationSpawned,
JoinResolved,
RunPaused,
RunResumed,
RunCompleted,
RunFailed,
RunCancelled,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RuntimeEvent {
runtime_id: String,
revision: u64,
occurred_at_ms: u64,
kind: RuntimeEventKind,
run_id: Option<String>,
detail: Value,
}
impl RuntimeEvent {
pub fn runtime_id(&self) -> &str {
&self.runtime_id
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn occurred_at_ms(&self) -> u64 {
self.occurred_at_ms
}
pub fn kind(&self) -> RuntimeEventKind {
self.kind
}
pub fn run_id(&self) -> Option<&str> {
self.run_id.as_deref()
}
pub fn detail(&self) -> &Value {
&self.detail
}
}
pub trait RuntimeStore {
fn load(&self, runtime_id: &str) -> Result<Option<RuntimeSnapshot>, String>;
fn commit(
&mut self,
runtime_id: &str,
expected_revision: Option<u64>,
snapshot: &RuntimeSnapshot,
events: &[RuntimeEvent],
) -> Result<(), String>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RuntimeErrorKind {
InvalidInput,
NotFound,
Conflict,
InvalidTransition,
Store,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimeError {
kind: RuntimeErrorKind,
detail: String,
}
impl RuntimeError {
fn invalid(detail: impl Into<String>) -> Self {
Self {
kind: RuntimeErrorKind::InvalidInput,
detail: detail.into(),
}
}
fn not_found(detail: impl Into<String>) -> Self {
Self {
kind: RuntimeErrorKind::NotFound,
detail: detail.into(),
}
}
fn conflict(detail: impl Into<String>) -> Self {
Self {
kind: RuntimeErrorKind::Conflict,
detail: detail.into(),
}
}
fn transition(detail: impl Into<String>) -> Self {
Self {
kind: RuntimeErrorKind::InvalidTransition,
detail: detail.into(),
}
}
fn store(detail: impl Into<String>) -> Self {
Self {
kind: RuntimeErrorKind::Store,
detail: detail.into(),
}
}
pub fn kind(&self) -> RuntimeErrorKind {
self.kind
}
pub fn detail(&self) -> &str {
&self.detail
}
}
impl fmt::Display for RuntimeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.detail)
}
}
impl std::error::Error for RuntimeError {}
impl From<crate::ContractError> for RuntimeError {
fn from(error: crate::ContractError) -> Self {
Self::invalid(error.to_string())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ActionExecutionOutcome {
Observed,
NeedsReconciliation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CompletionAttempt {
Completed,
Blocked(Vec<CompletionBlocker>),
}
struct PendingEvent {
kind: RuntimeEventKind,
run_id: Option<String>,
detail: Value,
}
pub struct RuntimeEngine<S, C> {
store: S,
clock: C,
}
impl<S, C> RuntimeEngine<S, C>
where
S: RuntimeStore,
C: RuntimeClock,
{
pub fn new(store: S, clock: C) -> Self {
Self { store, clock }
}
pub fn store(&self) -> &S {
&self.store
}
pub fn store_mut(&mut self) -> &mut S {
&mut self.store
}
pub fn into_parts(self) -> (S, C) {
(self.store, self.clock)
}
pub fn load(&self, runtime_id: &str) -> Result<Option<RuntimeSnapshot>, RuntimeError> {
validate_identifier(runtime_id, "runtime id")?;
let snapshot = self.store.load(runtime_id).map_err(RuntimeError::store)?;
if let Some(snapshot) = &snapshot {
validate_runtime_snapshot(snapshot, runtime_id)?;
}
Ok(snapshot)
}
pub fn create_runtime(
&mut self,
runtime_id: impl Into<String>,
) -> Result<RuntimeSnapshot, RuntimeError> {
let runtime_id = runtime_id.into();
validate_identifier(&runtime_id, "runtime id")?;
if self
.store
.load(&runtime_id)
.map_err(RuntimeError::store)?
.is_some()
{
return Err(RuntimeError::conflict(format!(
"runtime 已存在:{runtime_id}"
)));
}
let snapshot = RuntimeSnapshot {
schema_version: RUNTIME_SNAPSHOT_SCHEMA_VERSION.to_string(),
runtime_id: runtime_id.clone(),
revision: 1,
runs: Vec::new(),
delegations: Vec::new(),
};
let event = RuntimeEvent {
runtime_id: runtime_id.clone(),
revision: 1,
occurred_at_ms: self.clock.now_millis(),
kind: RuntimeEventKind::RuntimeCreated,
run_id: None,
detail: Value::Object(Map::new()),
};
self.store
.commit(&runtime_id, None, &snapshot, &[event])
.map_err(RuntimeError::store)?;
Ok(snapshot)
}
pub fn add_run(
&mut self,
runtime_id: &str,
spec: RunSpec,
) -> Result<RuntimeSnapshot, RuntimeError> {
let mut snapshot = self.load_required(runtime_id)?;
if snapshot.run(&spec.run_id).is_some() {
return Err(RuntimeError::conflict(format!(
"run 已存在:{}",
spec.run_id
)));
}
let now = self.clock.now_millis();
snapshot.runs.push(RunRecord {
run_id: spec.run_id.clone(),
agent_id: spec.agent_id,
task: spec.task,
metadata: spec.metadata,
parent_run_id: None,
delegation_group_id: None,
status: RunStatus::Pending,
actions: Vec::new(),
observations: Vec::new(),
terminal_summary: None,
terminal_error: None,
created_at_ms: now,
updated_at_ms: now,
});
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::RunAdded,
run_id: Some(spec.run_id),
detail: Value::Object(Map::new()),
}],
)
}
pub fn schedule_ready(&mut self, runtime_id: &str) -> Result<Vec<String>, RuntimeError> {
let mut snapshot = self.load_required(runtime_id)?;
let mut occupied = snapshot
.runs
.iter()
.filter(|run| run.status.occupies_agent_lane())
.map(|run| run.agent_id.clone())
.collect::<BTreeSet<_>>();
let now = self.clock.now_millis();
let mut started = Vec::new();
for run in &mut snapshot.runs {
if run.status == RunStatus::Pending && occupied.insert(run.agent_id.clone()) {
run.status = RunStatus::Running;
run.updated_at_ms = now;
started.push(run.run_id.clone());
}
}
if started.is_empty() {
return Ok(started);
}
let events = started
.iter()
.map(|run_id| PendingEvent {
kind: RuntimeEventKind::RunStarted,
run_id: Some(run_id.clone()),
detail: Value::Object(Map::new()),
})
.collect();
self.commit_transition(snapshot, events)?;
Ok(started)
}
pub fn enqueue_action<D>(
&mut self,
runtime_id: &str,
run_id: &str,
capabilities: &CapabilityRegistry<D>,
action: RuntimeAction,
) -> Result<RuntimeSnapshot, RuntimeError> {
if capabilities.get(action.capability_id()).is_none() {
return Err(RuntimeError::invalid(format!(
"action 引用了未注册 capability{}",
action.capability_id()
)));
}
let mut snapshot = self.load_required(runtime_id)?;
if snapshot
.runs
.iter()
.flat_map(|run| run.actions.iter())
.any(|record| record.action.id == action.id)
{
return Err(RuntimeError::conflict(format!(
"action id 已存在:{}",
action.id
)));
}
let now = self.clock.now_millis();
let run = run_mut(&mut snapshot, run_id)?;
require_status(run, &[RunStatus::Running], "enqueue action")?;
run.actions.push(ActionRecord {
action: action.clone(),
status: ActionStatus::Queued,
updated_at_ms: now,
});
run.status = RunStatus::WaitingForAction;
run.updated_at_ms = now;
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::ActionQueued,
run_id: Some(run_id.to_string()),
detail: serde_json::json!({"actionId": action.id, "capabilityId": action.capability_id}),
}],
)
}
pub fn execute_next_action<H: ToolHost>(
&mut self,
runtime_id: &str,
run_id: &str,
host: &mut H,
) -> Result<ActionExecutionOutcome, RuntimeError> {
let mut snapshot = self.load_required(runtime_id)?;
let now = self.clock.now_millis();
let (action, was_executing, already_reconciling) = {
let run = run_mut(&mut snapshot, run_id)?;
let current_status = run.status;
require_status(
run,
&[RunStatus::WaitingForAction, RunStatus::NeedsReconciliation],
"execute action",
)?;
let record = run
.actions
.last_mut()
.ok_or_else(|| RuntimeError::transition("run 缺少待执行 action"))?;
match record.status {
ActionStatus::Observed => {
return Err(RuntimeError::transition("最新 action 已 observed"));
}
ActionStatus::Executing => (
record.action.clone(),
true,
current_status == RunStatus::NeedsReconciliation,
),
ActionStatus::Queued => {
record.status = ActionStatus::Executing;
record.updated_at_ms = now;
(record.action.clone(), false, false)
}
}
};
if was_executing {
if already_reconciling {
return Ok(ActionExecutionOutcome::NeedsReconciliation);
}
let run = run_mut(&mut snapshot, run_id)?;
run.status = RunStatus::NeedsReconciliation;
run.updated_at_ms = now;
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::ActionNeedsReconciliation,
run_id: Some(run_id.to_string()),
detail: serde_json::json!({"actionId": action.id}),
}],
)?;
return Ok(ActionExecutionOutcome::NeedsReconciliation);
}
let executing = self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::ActionExecuting,
run_id: Some(run_id.to_string()),
detail: serde_json::json!({"actionId": action.id}),
}],
)?;
let execution = host.execute(
executing
.run(run_id)
.expect("committed executing run must exist"),
&action,
);
match execution {
ToolExecution::Observed(output) => {
self.persist_observation(runtime_id, run_id, &action, output)?;
Ok(ActionExecutionOutcome::Observed)
}
ToolExecution::Unknown => {
let mut snapshot = self.load_required(runtime_id)?;
let run = run_mut(&mut snapshot, run_id)?;
run.status = RunStatus::NeedsReconciliation;
run.updated_at_ms = self.clock.now_millis();
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::ActionNeedsReconciliation,
run_id: Some(run_id.to_string()),
detail: serde_json::json!({"actionId": action.id}),
}],
)?;
Ok(ActionExecutionOutcome::NeedsReconciliation)
}
}
}
pub fn reconcile_action(
&mut self,
runtime_id: &str,
run_id: &str,
output: ToolOutput,
) -> Result<RuntimeSnapshot, RuntimeError> {
let snapshot = self.load_required(runtime_id)?;
let run = snapshot
.run(run_id)
.ok_or_else(|| RuntimeError::not_found(format!("run 不存在:{run_id}")))?;
require_status(
run,
&[RunStatus::WaitingForAction, RunStatus::NeedsReconciliation],
"reconcile action",
)?;
let action = run
.actions
.last()
.filter(|record| record.status == ActionStatus::Executing)
.map(|record| record.action.clone())
.ok_or_else(|| RuntimeError::transition("run 缺少 executing action"))?;
self.persist_observation(runtime_id, run_id, &action, output)
}
pub fn spawn(
&mut self,
runtime_id: &str,
parent_run_id: &str,
group_id: impl Into<String>,
children: impl IntoIterator<Item = RunSpec>,
) -> Result<RuntimeSnapshot, RuntimeError> {
let group_id = group_id.into();
validate_identifier(&group_id, "delegation group id")?;
let children = children.into_iter().collect::<Vec<_>>();
if children.is_empty() || children.len() > MAX_DELEGATION_CHILDREN {
return Err(RuntimeError::invalid(format!(
"delegation children 必须为 1..={MAX_DELEGATION_CHILDREN}"
)));
}
let mut child_ids = BTreeSet::new();
for child in &children {
if !child_ids.insert(child.run_id.clone()) {
return Err(RuntimeError::conflict(format!(
"child run id 重复:{}",
child.run_id
)));
}
}
let mut snapshot = self.load_required(runtime_id)?;
if snapshot
.delegations
.iter()
.any(|group| group.id == group_id)
{
return Err(RuntimeError::conflict(format!(
"delegation group 已存在:{group_id}"
)));
}
if children
.iter()
.any(|child| snapshot.run(&child.run_id).is_some())
{
return Err(RuntimeError::conflict("child run id 已存在"));
}
let now = self.clock.now_millis();
let parent = run_mut(&mut snapshot, parent_run_id)?;
require_status(parent, &[RunStatus::Running], "spawn children")?;
parent.status = RunStatus::WaitingForChildren;
parent.updated_at_ms = now;
let child_run_ids = children
.iter()
.map(|child| child.run_id.clone())
.collect::<Vec<_>>();
for child in children {
snapshot.runs.push(RunRecord {
run_id: child.run_id,
agent_id: child.agent_id,
task: child.task,
metadata: child.metadata,
parent_run_id: Some(parent_run_id.to_string()),
delegation_group_id: Some(group_id.clone()),
status: RunStatus::Pending,
actions: Vec::new(),
observations: Vec::new(),
terminal_summary: None,
terminal_error: None,
created_at_ms: now,
updated_at_ms: now,
});
}
snapshot.delegations.push(DelegationGroup {
id: group_id.clone(),
parent_run_id: parent_run_id.to_string(),
child_run_ids: child_run_ids.clone(),
join_mode: JoinMode::All,
resolved: false,
results: Vec::new(),
});
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::DelegationSpawned,
run_id: Some(parent_run_id.to_string()),
detail: serde_json::json!({"groupId": group_id, "childRunIds": child_run_ids}),
}],
)
}
pub fn complete_if_ready<Context, Policy>(
&mut self,
runtime_id: &str,
run_id: &str,
policy: &Policy,
context: &Context,
summary: impl Into<String>,
) -> Result<CompletionAttempt, RuntimeError>
where
Policy: CompletionPolicy<Context>,
{
validate_identifier(policy.id(), "completion policy id")?;
let snapshot = self.load_required(runtime_id)?;
let run = snapshot
.run(run_id)
.ok_or_else(|| RuntimeError::not_found(format!("run 不存在:{run_id}")))?;
require_status(run, &[RunStatus::Running], "evaluate completion")?;
let decision = policy.evaluate(context);
if decision.is_ready() {
self.finish_run(
runtime_id,
run_id,
RunStatus::Completed,
summary.into(),
None,
)?;
Ok(CompletionAttempt::Completed)
} else {
Ok(CompletionAttempt::Blocked(decision.blockers().to_vec()))
}
}
pub fn fail_run(
&mut self,
runtime_id: &str,
run_id: &str,
summary: impl Into<String>,
error: impl Into<String>,
) -> Result<RuntimeSnapshot, RuntimeError> {
self.finish_run(
runtime_id,
run_id,
RunStatus::Failed,
summary.into(),
Some(error.into()),
)
}
pub fn cancel_run(
&mut self,
runtime_id: &str,
run_id: &str,
summary: impl Into<String>,
) -> Result<RuntimeSnapshot, RuntimeError> {
self.finish_run(
runtime_id,
run_id,
RunStatus::Cancelled,
summary.into(),
None,
)
}
pub fn pause_run(
&mut self,
runtime_id: &str,
run_id: &str,
) -> Result<RuntimeSnapshot, RuntimeError> {
self.change_status(
runtime_id,
run_id,
&[RunStatus::Running],
RunStatus::Paused,
RuntimeEventKind::RunPaused,
)
}
pub fn resume_run(
&mut self,
runtime_id: &str,
run_id: &str,
) -> Result<RuntimeSnapshot, RuntimeError> {
self.change_status(
runtime_id,
run_id,
&[RunStatus::Paused],
RunStatus::Running,
RuntimeEventKind::RunResumed,
)
}
fn load_required(&self, runtime_id: &str) -> Result<RuntimeSnapshot, RuntimeError> {
self.load(runtime_id)?
.ok_or_else(|| RuntimeError::not_found(format!("runtime 不存在:{runtime_id}")))
}
fn persist_observation(
&mut self,
runtime_id: &str,
run_id: &str,
action: &RuntimeAction,
output: ToolOutput,
) -> Result<RuntimeSnapshot, RuntimeError> {
let mut snapshot = self.load_required(runtime_id)?;
let now = self.clock.now_millis();
let run = run_mut(&mut snapshot, run_id)?;
require_status(
run,
&[RunStatus::WaitingForAction, RunStatus::NeedsReconciliation],
"persist observation",
)?;
let record = run
.actions
.last_mut()
.filter(|record| {
record.action.id == action.id && record.status == ActionStatus::Executing
})
.ok_or_else(|| RuntimeError::transition("executing action 与 observation 不匹配"))?;
record.status = ActionStatus::Observed;
record.updated_at_ms = now;
run.observations.push(RuntimeObservation {
action_id: action.id.clone(),
capability_id: action.capability_id.clone(),
status: output.status,
summary: output.summary.clone(),
output: output.output,
observed_at_ms: now,
});
run.status = RunStatus::Running;
run.updated_at_ms = now;
self.commit_transition(
snapshot,
vec![PendingEvent {
kind: RuntimeEventKind::ActionObserved,
run_id: Some(run_id.to_string()),
detail: serde_json::json!({
"actionId": action.id,
"status": output.status,
"summary": output.summary,
}),
}],
)
}
fn finish_run(
&mut self,
runtime_id: &str,
run_id: &str,
terminal_status: RunStatus,
summary: String,
error: Option<String>,
) -> Result<RuntimeSnapshot, RuntimeError> {
if !terminal_status.is_terminal() {
return Err(RuntimeError::invalid("finish_run 需要终态 status"));
}
validate_description(&summary, "terminal summary")?;
if let Some(error) = &error {
validate_description(error, "terminal error")?;
}
if terminal_status == RunStatus::Failed && error.is_none() {
return Err(RuntimeError::invalid("failed run 必须携带 error"));
}
let mut snapshot = self.load_required(runtime_id)?;
let now = self.clock.now_millis();
let run = run_mut(&mut snapshot, run_id)?;
if run.status.is_terminal() {
return Err(RuntimeError::transition(format!("run 已是终态:{run_id}")));
}
if terminal_status == RunStatus::Completed && run.status != RunStatus::Running {
return Err(RuntimeError::transition(
"只有 running run 可以进入 completed",
));
}
if matches!(
run.status,
RunStatus::WaitingForAction
| RunStatus::WaitingForChildren
| RunStatus::NeedsReconciliation
) {
return Err(RuntimeError::transition(
"未收束 action/delegation 时不能结束 run",
));
}
run.status = terminal_status;
run.terminal_summary = Some(summary);
run.terminal_error = error;
run.updated_at_ms = now;
let mut events = vec![PendingEvent {
kind: match terminal_status {
RunStatus::Completed => RuntimeEventKind::RunCompleted,
RunStatus::Failed => RuntimeEventKind::RunFailed,
RunStatus::Cancelled => RuntimeEventKind::RunCancelled,
_ => unreachable!(),
},
run_id: Some(run_id.to_string()),
detail: Value::Object(Map::new()),
}];
resolve_ready_joins(&mut snapshot, now, &mut events)?;
self.commit_transition(snapshot, events)
}
fn change_status(
&mut self,
runtime_id: &str,
run_id: &str,
allowed: &[RunStatus],
next: RunStatus,
kind: RuntimeEventKind,
) -> Result<RuntimeSnapshot, RuntimeError> {
let mut snapshot = self.load_required(runtime_id)?;
let run = run_mut(&mut snapshot, run_id)?;
require_status(run, allowed, "change status")?;
run.status = next;
run.updated_at_ms = self.clock.now_millis();
self.commit_transition(
snapshot,
vec![PendingEvent {
kind,
run_id: Some(run_id.to_string()),
detail: Value::Object(Map::new()),
}],
)
}
fn commit_transition(
&mut self,
mut snapshot: RuntimeSnapshot,
pending_events: Vec<PendingEvent>,
) -> Result<RuntimeSnapshot, RuntimeError> {
if pending_events.is_empty() {
return Err(RuntimeError::invalid(
"runtime transition 至少需要一个 event",
));
}
let expected_revision = snapshot.revision;
snapshot.revision = expected_revision
.checked_add(1)
.ok_or_else(|| RuntimeError::conflict("runtime revision 溢出"))?;
let now = self.clock.now_millis();
let events = pending_events
.into_iter()
.map(|event| RuntimeEvent {
runtime_id: snapshot.runtime_id.clone(),
revision: snapshot.revision,
occurred_at_ms: now,
kind: event.kind,
run_id: event.run_id,
detail: event.detail,
})
.collect::<Vec<_>>();
self.store
.commit(
&snapshot.runtime_id,
Some(expected_revision),
&snapshot,
&events,
)
.map_err(RuntimeError::store)?;
Ok(snapshot)
}
}
fn run_mut<'a>(
snapshot: &'a mut RuntimeSnapshot,
run_id: &str,
) -> Result<&'a mut RunRecord, RuntimeError> {
snapshot
.runs
.iter_mut()
.find(|run| run.run_id == run_id)
.ok_or_else(|| RuntimeError::not_found(format!("run 不存在:{run_id}")))
}
fn require_status(
run: &RunRecord,
allowed: &[RunStatus],
operation: &str,
) -> Result<(), RuntimeError> {
if allowed.contains(&run.status) {
Ok(())
} else {
Err(RuntimeError::transition(format!(
"{operation} 不允许 run {} 处于 {:?}",
run.run_id, run.status
)))
}
}
fn resolve_ready_joins(
snapshot: &mut RuntimeSnapshot,
now: u64,
events: &mut Vec<PendingEvent>,
) -> Result<(), RuntimeError> {
let ready = snapshot
.delegations
.iter()
.enumerate()
.filter(|(_, group)| {
!group.resolved
&& group.child_run_ids.iter().all(|child_id| {
snapshot
.run(child_id)
.is_some_and(|run| run.status.is_terminal())
})
})
.map(|(index, _)| index)
.collect::<Vec<_>>();
for index in ready {
let child_ids = snapshot.delegations[index].child_run_ids.clone();
let results = child_ids
.iter()
.map(|child_id| {
let child = snapshot.run(child_id).expect("ready child must exist");
RunTerminalResult {
run_id: child.run_id.clone(),
status: child.status,
summary: child.terminal_summary.clone(),
error: child.terminal_error.clone(),
}
})
.collect::<Vec<_>>();
let parent_run_id = snapshot.delegations[index].parent_run_id.clone();
let group_id = snapshot.delegations[index].id.clone();
snapshot.delegations[index].resolved = true;
snapshot.delegations[index].results = results;
let parent = run_mut(snapshot, &parent_run_id)?;
require_status(parent, &[RunStatus::WaitingForChildren], "resolve join")?;
parent.status = RunStatus::Running;
parent.updated_at_ms = now;
events.push(PendingEvent {
kind: RuntimeEventKind::JoinResolved,
run_id: Some(parent_run_id),
detail: serde_json::json!({"groupId": group_id, "childRunIds": child_ids}),
});
}
Ok(())
}
fn validate_runtime_snapshot(
snapshot: &RuntimeSnapshot,
expected_runtime_id: &str,
) -> Result<(), RuntimeError> {
if snapshot.schema_version != RUNTIME_SNAPSHOT_SCHEMA_VERSION {
return Err(RuntimeError::conflict(format!(
"runtime snapshot schemaVersion 不支持:{}",
snapshot.schema_version
)));
}
validate_identifier(&snapshot.runtime_id, "runtime id")?;
if snapshot.runtime_id != expected_runtime_id || snapshot.revision == 0 {
return Err(RuntimeError::conflict(
"runtime snapshot identity/revision 与读取请求不一致",
));
}
let mut run_ids = BTreeSet::new();
let mut action_ids = BTreeSet::new();
for run in &snapshot.runs {
validate_identifier(&run.run_id, "run id")?;
validate_identifier(&run.agent_id, "agent id")?;
validate_description(&run.task, "run task")?;
validate_metadata(&run.metadata, "run metadata")?;
if !run_ids.insert(run.run_id.as_str()) {
return Err(RuntimeError::conflict(format!(
"runtime snapshot run id 重复:{}",
run.run_id
)));
}
for action in &run.actions {
validate_identifier(&action.action.id, "action id")?;
validate_identifier(&action.action.capability_id, "capability id")?;
if !action.action.input.is_object() || !action_ids.insert(action.action.id.as_str()) {
return Err(RuntimeError::conflict(
"runtime snapshot action input 非 object 或 action id 重复",
));
}
}
let active_action = run
.actions
.last()
.filter(|action| action.status != ActionStatus::Observed);
match run.status {
RunStatus::WaitingForAction
if !active_action.is_some_and(|action| {
matches!(
action.status,
ActionStatus::Queued | ActionStatus::Executing
)
}) =>
{
return Err(RuntimeError::conflict(
"waiting-for-action run 缺少 queued/executing action",
));
}
RunStatus::NeedsReconciliation
if !active_action
.is_some_and(|action| action.status == ActionStatus::Executing) =>
{
return Err(RuntimeError::conflict(
"needs-reconciliation run 缺少 executing action",
));
}
status
if !matches!(
status,
RunStatus::WaitingForAction | RunStatus::NeedsReconciliation
) && active_action.is_some() =>
{
return Err(RuntimeError::conflict(
"非 action 等待态不能保留未观察 action",
));
}
_ => {}
}
}
let mut group_ids = BTreeSet::new();
for group in &snapshot.delegations {
validate_identifier(&group.id, "delegation group id")?;
let unique_child_ids =
group.child_run_ids.iter().collect::<BTreeSet<_>>().len() == group.child_run_ids.len();
if !group_ids.insert(group.id.as_str())
|| !run_ids.contains(group.parent_run_id.as_str())
|| group.child_run_ids.is_empty()
|| !unique_child_ids
|| group
.child_run_ids
.iter()
.any(|child_id| !run_ids.contains(child_id.as_str()))
{
return Err(RuntimeError::conflict(
"runtime snapshot delegation identity/reference 无效",
));
}
if group.resolved && group.results.len() != group.child_run_ids.len() {
return Err(RuntimeError::conflict(
"resolved delegation results 数量不匹配",
));
}
if group.resolved
&& group
.results
.iter()
.zip(&group.child_run_ids)
.any(|(result, child_id)| {
result.run_id != *child_id
|| !result.status.is_terminal()
|| snapshot.run(child_id).is_none_or(|child| {
child.status != result.status
|| child.terminal_summary != result.summary
|| child.terminal_error != result.error
})
})
{
return Err(RuntimeError::conflict(
"resolved delegation result identity/order 无效",
));
}
let parent = snapshot
.run(&group.parent_run_id)
.expect("validated parent reference");
if !group.resolved && parent.status != RunStatus::WaitingForChildren {
return Err(RuntimeError::conflict(
"unresolved delegation parent 必须等待 children",
));
}
for child_id in &group.child_run_ids {
let child = snapshot.run(child_id).expect("validated child reference");
if child.parent_run_id.as_deref() != Some(group.parent_run_id.as_str())
|| child.delegation_group_id.as_deref() != Some(group.id.as_str())
{
return Err(RuntimeError::conflict(
"delegation child parent/group binding 不匹配",
));
}
}
}
for run in &snapshot.runs {
if run.status == RunStatus::WaitingForChildren
&& snapshot
.delegations
.iter()
.filter(|group| group.parent_run_id == run.run_id && !group.resolved)
.count()
!= 1
{
return Err(RuntimeError::conflict(
"waiting-for-children run 必须绑定唯一 unresolved delegation",
));
}
}
Ok(())
}