use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use futures_util::StreamExt;
use futures_util::future::LocalBoxFuture;
use pointlock_expr::Scope;
use pointlock_ir::{
ActionExecution, ActionOutcome, ActionResult, ActionStepIR, AssertStepIR, AssertionIR,
AssetRef, BoundAttempt, CallFrame, CallStepIR, EffectClassAction, ErrorClass, ErrorInfo,
EvidenceRef, ExecutionMode, FlowIR, ForeachStepIR, HandlerAction, HandlerBinding, HandlerHook,
HumanMode, HumanPending, HumanPurpose, HumanStepIR, IfStepIR, LetStepIR, Observation,
ObservationRecord, ObservationSource, ParamDecl, PathFrame, Phase, PredicateIR,
ProviderStateSummary, RetryPolicy, RunLogPayload, RunPath, StepBase, StepIR, StepId,
StepRecord, StepState, SupervisePolicy, UiSnapshotOmissionReason, Verdict, VerdictStatus,
VerifyChannel, render_run_path, to_canonical_json,
};
use pointlock_provider_kit::{
BoundActionCall, CancellationToken, ObserveRequest, ObserveWant, ProviderSession,
SessionOutcome, UiSnapshotOutcome, VERDICT_EVIDENCE_MAX_ENTRIES, VERDICT_SUMMARY_MAX_CHARS,
VerdictWrite,
};
use pointlock_store::Store;
use pointlock_vision::VisionVerifier;
use serde_json::{Map, Value};
use crate::error::{BlockedReason, RunnerError};
use crate::judge::{
FoldedVerdict, eval_expr_assertion, fold_flow_verdict, fold_step_verdict, project_output,
};
use crate::load::{LoadedFlow, MAX_CALL_DEPTH};
use crate::observe_eval::{
EvaluatedAssertion, ObserveMaterial, eval_observed_assertion, material_from_observation,
};
#[derive(Debug, Clone, PartialEq)]
pub enum RunOutcome {
Finished {
verdict: Option<Verdict>,
},
Suspended,
Blocked {
reason: BlockedReason,
},
AwaitingHuman {
pending: HumanPending,
},
}
const SUMMARY_CAPTURE_BUDGET_MS: u64 = 2_000;
pub(crate) async fn capture_provider_state_summary(
session: &dyn ProviderSession,
known_lineage: &[String],
device_id: &str,
platform: Option<&str>,
) -> ProviderStateSummary {
let budget = std::time::Duration::from_millis(SUMMARY_CAPTURE_BUDGET_MS);
let started = std::time::Instant::now();
let health = match tokio::time::timeout(budget, session.health()).await {
Ok(Ok(health)) => pointlock_ir::SessionHealthSnapshot {
ok: health.ok,
degraded: health.degraded,
},
Ok(Err(error)) => pointlock_ir::SessionHealthSnapshot {
ok: false,
degraded: Some(
serde_json::to_value(error.error_class)
.ok()
.and_then(|value| value.as_str().map(str::to_owned))
.unwrap_or_else(|| "unknown".to_owned()),
),
},
Err(_) => pointlock_ir::SessionHealthSnapshot {
ok: false,
degraded: Some("capture_timeout".to_owned()),
},
};
let remaining = budget.saturating_sub(started.elapsed());
let event_cursor = match tokio::time::timeout(remaining, session.current_cursor()).await {
Ok(Ok(cursor)) => Some(cursor),
_ => None,
};
let attestation = session.attestation();
let mut session_lineage = known_lineage.to_vec();
if let Some(cursor) = &event_cursor
&& session_lineage.last() != Some(&cursor.session_id)
{
session_lineage.push(cursor.session_id.clone());
}
ProviderStateSummary {
session_lineage,
event_cursor,
attestation: pointlock_ir::AttestationSnapshot {
lockfile_digest: attestation.lockfile_digest.clone(),
attested_at: attestation.attested_at.clone(),
},
health,
device_id: device_id.to_owned(),
platform: platform.map(str::to_owned),
}
}
fn escalate_verdict_material(evidence: &Option<AssetRef>) -> (Vec<AssetRef>, EvidenceManifest) {
match evidence {
Some(asset) => (vec![asset.clone()], human_manifest(asset)),
None => (Vec::new(), EvidenceManifest::default()),
}
}
fn human_manifest(asset: &AssetRef) -> EvidenceManifest {
EvidenceManifest {
localized: vec![pointlock_ir::EvidenceRef {
asset: asset.clone(),
sha256: asset.sha256.clone().unwrap_or_default(),
local_path: asset.uri.clone(),
}],
gaps: Vec::new(),
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct EvidenceManifest {
pub localized: Vec<pointlock_ir::EvidenceRef>,
pub gaps: Vec<pointlock_ir::EvidenceGap>,
}
pub(crate) fn chain_start(
chain_index: Option<u32>,
step: &ActionStepIR,
) -> Result<usize, RunnerError> {
match chain_index {
None => Ok(0),
Some(index) if index >= 1 && ((index - 1) as usize) < step.binding.attempts.len() => {
Ok((index - 1) as usize)
}
Some(index) => Err(RunnerError::M0Unsupported {
detail: format!(
"the pending intent's recorded chainIndex {index} does not exist in the \
resumed step's binding chain (len {}); refusing to guess a re-entry \
position (unreachable through the shipped resume rules — same-IR chains \
cannot shrink and effect-dirty repairs never adopt/replay)",
step.binding.attempts.len()
),
}),
}
}
pub(crate) fn quarantine_unpersistable(outcome: ActionOutcome) -> ActionOutcome {
let poisoned = match &outcome {
ActionOutcome::Succeeded { result } => result
.before
.iter()
.chain(result.after.iter())
.find(|observation| !observation.viewport.scale_factor.is_finite()),
_ => None,
};
match poisoned {
Some(observation) => ActionOutcome::Failed {
error: ErrorInfo {
code: "observation_viewport_invalid".to_owned(),
message: format!(
"provider contract violation: observation {} carries a non-finite \
viewport scaleFactor, which cannot be persisted",
observation.id
),
retryable: false,
details: None,
},
},
None => outcome,
}
}
pub(crate) fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0)
}
pub(crate) fn root_path(flow: &FlowIR) -> RunPath {
vec![PathFrame::Flow {
flow_id: flow.flow_id.clone(),
ir_hash: flow.ir_hash.clone(),
}]
}
pub(crate) fn attempt_of(path: &RunPath) -> Option<u64> {
path.iter().rev().find_map(|frame| match frame {
PathFrame::Attempt { n } => Some(*n),
_ => None,
})
}
pub(crate) fn instance_key(path: &[PathFrame]) -> String {
let trimmed = {
let mut end = path.len();
while end > 0
&& matches!(
path[end - 1],
PathFrame::Attempt { .. } | PathFrame::Phase { .. } | PathFrame::Assertion { .. }
)
{
end -= 1;
}
&path[..end]
};
let mut key = String::new();
for frame in trimmed {
match frame {
PathFrame::Flow { .. } => {}
PathFrame::Step { step_id } => {
let _ = write!(key, "/{step_id}");
}
PathFrame::Call { step_id, .. } => {
let _ = match step_id {
Some(step_id) => write!(key, "/{step_id}"),
None => write!(key, "/hook-call"),
};
}
PathFrame::Iteration { index, key: item } => {
let _ = match item {
Some(item) => write!(key, "[{index}:{item}]"),
None => write!(key, "[{index}]"),
};
}
PathFrame::Hook { hook, trigger } => {
let _ = write!(key, "/hook:{hook:?}:{trigger}");
}
PathFrame::Attempt { n } => {
let _ = write!(key, "#{n}");
}
PathFrame::Phase { .. } | PathFrame::Assertion { .. } => {}
}
}
key
}
pub(crate) fn child_frame(step: &StepIR) -> PathFrame {
match step {
StepIR::Call(call) => PathFrame::Call {
step_id: Some(call.base.step_id.clone()),
callee_flow_id: call.flow_ref.flow_id.clone(),
callee_ir_hash: call.flow_ref.ir_hash.clone(),
},
other => PathFrame::Step {
step_id: other.step_id().clone(),
},
}
}
pub(crate) enum FrontierWork {
Adopt {
call_id: String,
intent_path: RunPath,
outcome: Box<ActionOutcome>,
args: Value,
chain_index: Option<u32>,
},
Replay {
args: Value,
chain_index: Option<u32>,
},
ConfirmEffect {
message: String,
args: Value,
},
AbortRuled {
args: Value,
},
}
struct AdoptedSettle {
outcome: ActionOutcome,
settled_seq: u64,
attempt_n: u64,
}
enum Ctl {
Continue,
HaltFail,
Suspend(String),
Abort,
Blocked(BlockedReason),
AwaitHuman(HumanPending),
}
enum Consulted {
None,
Retry(RetryPolicy),
Continue,
Abort,
Escalated {
status: VerdictStatus,
summary: String,
evidence: Option<AssetRef>,
},
Repaired,
Pending(HumanPending),
RepairDone,
RepairFailed,
Propagate(Ctl),
}
fn hook_frame(hook: HandlerHook, trigger: u64) -> PathFrame {
PathFrame::Hook { hook, trigger }
}
fn hook_child_path(
step_path: &RunPath,
hook: HandlerHook,
trigger: u64,
human: &HumanStepIR,
) -> RunPath {
let mut path = step_path.clone();
path.push(hook_frame(hook, trigger));
path.push(PathFrame::Step {
step_id: human.base.step_id.clone(),
});
path
}
fn map_escalate_response(human: &HumanStepIR, response: &Value) -> Consulted {
let decision = response
.get("decision")
.and_then(Value::as_str)
.unwrap_or_default();
match human.mode {
HumanMode::RepairWorld => match decision {
"done" => Consulted::Repaired,
_ => Consulted::Abort,
},
HumanMode::Confirm => {
let first = human
.decisions
.as_ref()
.and_then(|labels| labels.first())
.map(String::as_str);
let status = if first == Some(decision) {
VerdictStatus::Pass
} else {
VerdictStatus::Fail
};
Consulted::Escalated {
status,
summary: format!("escalate confirm decision '{decision}' (position-mapped)"),
evidence: None,
}
}
_ => {
let status = match response.get("status").and_then(Value::as_str) {
Some("pass") => VerdictStatus::Pass,
Some("fail") => VerdictStatus::Fail,
_ => VerdictStatus::Unknown,
};
Consulted::Escalated {
status,
evidence: None,
summary: format!(
"escalate judge ruling: {}",
response
.get("status")
.and_then(Value::as_str)
.unwrap_or("unknown")
),
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct HumanRequestFact {
pub request_id: String,
pub run_path: RunPath,
pub purpose: HumanPurpose,
pub mode: Option<HumanMode>,
pub prompt: String,
pub presents: Value,
pub deadline_at_ms: Option<u64>,
pub final_response: Option<Value>,
pub final_actor: Option<String>,
}
enum ActPhase {
Succeeded {
result: Box<ActionResult>,
degraded: bool,
settled_seq: u64,
attempt_n: u64,
},
StepFail { class: ErrorClass, message: String },
StepUnknown {
message: String,
error_class: Option<ErrorClass>,
},
Unconfirmed {
message: String,
},
Aborted,
Suspend(String),
}
pub(crate) struct StepObs {
pub observations: Vec<ObservationRecord>,
pub before_id: Option<String>,
pub after_id: Option<String>,
}
pub(crate) struct Adopted {
pub record: StepRecord,
pub before_id: Option<String>,
pub after_id: Option<String>,
}
pub(crate) fn is_history(record: &StepRecord) -> bool {
!record.attempts.is_empty()
|| record.verdict.is_some()
|| record.output.is_some()
|| !record.resolved_inputs.is_null()
}
pub(crate) struct FrameState<'a> {
pub flow: &'a FlowIR,
pub base_path: RunPath,
pub params: Map<String, Value>,
pub vars: BTreeMap<String, Value>,
pub iters: Vec<(String, Value)>,
pub outputs: BTreeMap<String, Value>,
pub verdicts: BTreeMap<String, (VerdictStatus, bool)>,
pub fold: Vec<(VerdictStatus, bool)>,
pub observed: BTreeMap<String, StepObs>,
pub depth: usize,
}
impl<'a> FrameState<'a> {
pub fn new(
flow: &'a FlowIR,
base_path: RunPath,
params: Map<String, Value>,
depth: usize,
) -> Self {
FrameState {
flow,
base_path,
params,
vars: BTreeMap::new(),
iters: Vec::new(),
outputs: BTreeMap::new(),
verdicts: BTreeMap::new(),
fold: Vec::new(),
observed: BTreeMap::new(),
depth,
}
}
pub fn scope(&self, env: &[(String, Value)], self_binding: Option<(&str, &Value)>) -> Scope {
let mut scope = Scope::new();
for (name, value) in &self.params {
scope.set_param(name.clone(), value.clone());
}
for (name, value) in env {
scope.set_env(name.clone(), value.clone());
}
for (name, value) in &self.vars {
scope.set_var(name.clone(), value.clone());
}
for (name, value) in &self.iters {
scope.set_iter(name.clone(), value.clone());
}
for (step_id, output) in &self.outputs {
scope.set_step_output(step_id.clone(), output.clone());
}
for (step_id, (status, _degraded)) in &self.verdicts {
let status = serde_json::to_value(status).expect("VerdictStatus serializes");
scope.set_step_verdict(step_id.clone(), status);
}
if let Some((step_id, value)) = self_binding {
scope.set_step_output(step_id.to_owned(), value.clone());
}
scope
}
fn seed_verdict(&mut self, step_id: &StepId, status: VerdictStatus, degraded: bool) {
self.verdicts
.insert(step_id.as_str().to_owned(), (status, degraded));
self.fold.push((status, degraded));
}
fn reseed_last(&mut self, step_id: &StepId, status: VerdictStatus, degraded: bool) {
self.verdicts
.insert(step_id.as_str().to_owned(), (status, degraded));
self.fold.pop();
self.fold.push((status, degraded));
}
}
pub(crate) struct Execution<'a> {
pub flows: &'a LoadedFlow<'a>,
pub session: Box<dyn ProviderSession>,
pub store: &'a mut Store,
pub run_id: String,
pub stop: CancellationToken,
pub env: Vec<(String, Value)>,
pub attempt_base: BTreeMap<String, u64>,
pub open_spans: BTreeMap<String, Value>,
pub live_frames: BTreeMap<String, pointlock_ir::Hash>,
pub adoptable: BTreeMap<String, Adopted>,
pub frontier: Option<(String, FrontierWork)>,
pub resumed: bool,
pub authorized: BTreeSet<String>,
pub reentry_seen: bool,
pub vision: Option<Arc<dyn VisionVerifier>>,
pub session_lineage: Vec<String>,
pub pending_summaries: BTreeMap<String, ProviderStateSummary>,
pub supervise: Option<SupervisePolicy>,
pub human: BTreeMap<String, HumanRequestFact>,
pub settled: BTreeMap<String, crate::align::SettledFact>,
pub recorded_verdicts: BTreeMap<String, (VerdictStatus, bool)>,
pub hook_triggers: BTreeMap<String, u64>,
pub clock: Option<Arc<dyn Fn() -> u64 + Send + Sync>>,
}
impl<'a> Execution<'a> {
fn append(&mut self, path: &RunPath, payload: &RunLogPayload) -> Result<u64, RunnerError> {
let enriched;
let payload = match payload {
RunLogPayload::StepEntered { .. } => {
self.pending_summaries.remove(&instance_key(path));
payload
}
RunLogPayload::StepExited {
state,
output,
provider_state_summary: None,
localized,
localization_gaps,
} => match (state, self.pending_summaries.remove(&instance_key(path))) {
(StepState::Aborted, _) | (_, None) => payload,
(_, Some(summary)) => {
enriched = RunLogPayload::StepExited {
state: *state,
output: output.clone(),
provider_state_summary: Some(summary),
localized: localized.clone(),
localization_gaps: localization_gaps.clone(),
};
&enriched
}
},
_ => payload,
};
Ok(self
.store
.append_event(&self.run_id, now_ms(), path, payload)?)
}
async fn stash_open_span_summaries(&mut self) {
let keys: Vec<String> = self
.open_spans
.keys()
.filter(|key| {
matches!(
self.recorded_verdicts.get(*key),
Some((VerdictStatus::Fail | VerdictStatus::Unknown, _))
) && !self.pending_summaries.contains_key(*key)
})
.cloned()
.collect();
if keys.is_empty() {
return;
}
let summary = self.capture_summary().await;
for key in keys {
self.pending_summaries.insert(key, summary.clone());
}
}
async fn capture_summary(&self) -> ProviderStateSummary {
let env_str = |key: &str| {
self.env
.iter()
.find(|(name, _)| name == key)
.and_then(|(_, value)| value.as_str().map(str::to_owned))
};
capture_provider_state_summary(
self.session.as_ref(),
&self.session_lineage,
&env_str("deviceId").unwrap_or_default(),
env_str("platform").as_deref(),
)
.await
}
fn now(&self) -> u64 {
match &self.clock {
Some(clock) => clock(),
None => now_ms(),
}
}
pub async fn run(
mut self,
mut root: FrameState<'a>,
start: usize,
) -> Result<RunOutcome, RunnerError> {
let flows = self.flows;
let body: &'a [StepIR] = &flows.root.body;
let prefix = root.base_path.clone();
let ctl = self
.exec_body(&mut root, prefix.clone(), body, start)
.await?;
match ctl {
Ctl::Continue | Ctl::HaltFail => self.finish(false, &root).await,
Ctl::Abort => self.finish(true, &root).await,
Ctl::Suspend(reason) => {
let summary = self.capture_summary().await;
self.append(
&prefix,
&RunLogPayload::RunSuspended {
provider_state_summary: Some(summary),
reason: Some(reason),
},
)?;
self.end_session(SessionOutcome::Shutdown).await;
Ok(RunOutcome::Suspended)
}
Ctl::Blocked(reason) => {
let summary = self.capture_summary().await;
self.append(
&prefix,
&RunLogPayload::RunSuspended {
provider_state_summary: Some(summary),
reason: Some(reason.to_string()),
},
)?;
self.end_session(SessionOutcome::Shutdown).await;
Ok(RunOutcome::Blocked { reason })
}
Ctl::AwaitHuman(pending) => {
let summary = self.capture_summary().await;
self.append(
&prefix,
&RunLogPayload::RunSuspended {
provider_state_summary: Some(summary),
reason: Some(format!(
"awaiting human response (requestId {})",
pending.request_id
)),
},
)?;
self.end_session(SessionOutcome::Shutdown).await;
Ok(RunOutcome::AwaitingHuman { pending })
}
}
}
async fn finish(
mut self,
aborted: bool,
root: &FrameState<'a>,
) -> Result<RunOutcome, RunnerError> {
let prefix = root.base_path.clone();
let verdict = if aborted {
None
} else {
fold_flow_verdict(&root.fold, root.flow.verdict_policy).map(|folded| Verdict {
status: folded.status,
degraded: folded.degraded,
summary: folded.summary,
evidence: Vec::new(),
supersedes: None,
})
};
let remote_archival_error = match &verdict {
Some(verdict) => self.try_verdict_writeback(verdict).await,
None => None,
};
self.append(
&prefix,
&RunLogPayload::RunFinished {
verdict: verdict.clone(),
remote_archival_error,
},
)?;
let session_outcome = if aborted {
SessionOutcome::Cancelled
} else if verdict
.as_ref()
.is_some_and(|verdict| verdict.status == VerdictStatus::Fail)
{
SessionOutcome::Failed
} else {
SessionOutcome::Completed
};
self.end_session(session_outcome).await;
Ok(RunOutcome::Finished { verdict })
}
async fn exec_body(
&mut self,
frame: &mut FrameState<'a>,
prefix: RunPath,
body: &'a [StepIR],
start: usize,
) -> Result<Ctl, RunnerError> {
for (index, step) in body.iter().enumerate().skip(start) {
if self.stop.is_cancelled() {
return Ok(Ctl::Suspend("stop requested".to_owned()));
}
match self.exec_step(frame, &prefix, step).await? {
Ctl::Continue => {}
Ctl::HaltFail => {
self.stash_open_span_summaries().await;
self.record_pairs(&prefix, &body[index + 1..], StepState::Blocked)?;
return Ok(Ctl::HaltFail);
}
other => return Ok(other),
}
}
Ok(Ctl::Continue)
}
fn exec_step<'s>(
&'s mut self,
frame: &'s mut FrameState<'a>,
prefix: &'s RunPath,
step: &'a StepIR,
) -> LocalBoxFuture<'s, Result<Ctl, RunnerError>>
where
'a: 's,
{
Box::pin(async move {
let mut path = prefix.clone();
path.push(child_frame(step));
let key = instance_key(&path);
if self
.adoptable
.get(&key)
.is_some_and(|adopted| is_history(&adopted.record))
{
let adopted = self.adoptable.remove(&key).expect("checked present");
self.adopt_step(frame, step, adopted);
return Ok(Ctl::Continue);
}
match step {
StepIR::Action(s) => self.exec_action(frame, path, s).await,
StepIR::Call(s) => self.exec_call(frame, path, s).await,
StepIR::If(s) => self.exec_if(frame, path, s).await,
StepIR::Foreach(s) => self.exec_foreach(frame, path, s).await,
StepIR::Let(s) => self.exec_let(frame, path, s).await,
StepIR::Assert(s) => self.exec_assert(frame, path, s).await,
StepIR::Human(s) => self.exec_human(frame, path, s).await,
}
})
}
fn adopt_step(&mut self, frame: &mut FrameState<'a>, step: &'a StepIR, adopted: Adopted) {
let id = step.step_id().as_str().to_owned();
let record = adopted.record;
match step {
StepIR::Action(_) => {
if let Some(verdict) = &record.verdict {
frame.seed_verdict(step.step_id(), verdict.status, verdict.degraded);
}
if let Some(output) = record.output.clone() {
frame.outputs.insert(id.clone(), output);
}
frame.observed.insert(
id,
StepObs {
observations: record.observations,
before_id: adopted.before_id,
after_id: adopted.after_id,
},
);
}
StepIR::Assert(_) | StepIR::Call(_) | StepIR::Human(_) => {
if let Some(verdict) = &record.verdict {
frame.seed_verdict(step.step_id(), verdict.status, verdict.degraded);
}
if let Some(output) = record.output.clone() {
frame.outputs.insert(id, output);
}
}
StepIR::Let(_) => {
if let Value::Object(bindings) = record.resolved_inputs {
for (name, value) in bindings {
frame.vars.insert(name, value);
}
}
}
StepIR::If(s) => {
self.adopt_children(frame, &record.run_path, &s.then);
if let Some(otherwise) = &s.r#else {
self.adopt_children(frame, &record.run_path, otherwise);
}
}
StepIR::Foreach(s) => {
let rounds = record
.resolved_inputs
.get("items")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
for index in 0..rounds {
let mut prefix = record.run_path.clone();
prefix.push(PathFrame::Iteration {
index: index as u64,
key: None,
});
self.adopt_children(frame, &prefix, &s.body);
}
}
}
}
fn adopt_children(
&mut self,
frame: &mut FrameState<'a>,
prefix: &RunPath,
steps: &'a [StepIR],
) {
for step in steps {
let mut path = prefix.clone();
path.push(child_frame(step));
let key = instance_key(&path);
if let Some(adopted) = self.adoptable.remove(&key) {
self.adopt_step(frame, step, adopted);
}
}
}
fn record_pairs(
&mut self,
prefix: &RunPath,
steps: &'a [StepIR],
state: StepState,
) -> Result<(), RunnerError> {
for step in steps {
let mut path = prefix.clone();
path.push(child_frame(step));
match step {
StepIR::If(s) => {
self.record_pairs(&path, &s.then, state)?;
if let Some(otherwise) = &s.r#else {
self.record_pairs(&path, otherwise, state)?;
}
}
StepIR::Foreach(s) => self.record_pairs(&path, &s.body, state)?,
_ => {}
}
let key = instance_key(&path);
if self.adoptable.remove(&key).is_some() {
continue;
}
if self.open_spans.remove(&key).is_some() {
self.append(
&path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
continue;
}
self.append(
&path,
&RunLogPayload::StepEntered {
step_id: step.step_id().clone(),
effect_hash: step.base().effect_hash.clone(),
judge_hash: step.base().judge_hash.clone(),
resolved_inputs: Value::Null,
},
)?;
self.append(
&path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
}
Ok(())
}
fn open_span_inputs(&self, key: &str) -> Option<Value> {
self.open_spans.get(key).cloned()
}
fn enter_step(
&mut self,
path: &RunPath,
base: &StepBase,
resolved_inputs: Value,
) -> Result<(), RunnerError> {
let key = instance_key(path);
if self.open_spans.remove(&key).is_some() {
return Ok(());
}
self.append(
path,
&RunLogPayload::StepEntered {
step_id: base.step_id.clone(),
effect_hash: base.effect_hash.clone(),
judge_hash: base.judge_hash.clone(),
resolved_inputs,
},
)?;
Ok(())
}
fn resolve_args(
&self,
frame: &FrameState<'a>,
attempt: &BoundAttempt,
) -> Result<Value, String> {
let scope = frame.scope(&self.env, None);
let mut evaluated = serde_json::Map::new();
for (name, expr) in attempt.args.iter() {
match pointlock_expr::eval(expr, &scope) {
Ok(value) => {
evaluated.insert(name.as_str().to_owned(), value);
}
Err(error) => return Err(format!("argument evaluation failed: {error}")),
}
}
Ok(Value::Object(evaluated))
}
async fn probe_or_note(
&mut self,
frame: &mut FrameState<'a>,
path: &RunPath,
base: &'a StepBase,
) -> Result<Option<Ctl>, RunnerError> {
let reentry = self.resumed && !self.reentry_seen;
self.reentry_seen = true;
if let Some(probes) = &base.preflight {
return self.probe_preflight(frame, path, base, probes).await;
}
if reentry || self.authorized.contains(base.step_id.as_str()) {
let mut probe_path = path.clone();
probe_path.push(PathFrame::Phase {
phase: Phase::Preflight,
});
self.append(
&probe_path,
&RunLogPayload::PreflightProbed {
outcomes: Vec::new(),
},
)?;
}
Ok(None)
}
async fn probe_preflight(
&mut self,
frame: &mut FrameState<'a>,
path: &RunPath,
base: &'a StepBase,
probes: &'a [AssertionIR],
) -> Result<Option<Ctl>, RunnerError> {
let step_id = &base.step_id;
let mut probe_path = path.clone();
probe_path.push(PathFrame::Phase {
phase: Phase::Preflight,
});
let needs = VerifyNeeds::of(probes);
let mut active_retry: Option<(RetryPolicy, u32)> = None;
loop {
let material = self.fresh_material(&needs, &probe_path).await?;
let scope = frame.scope(&self.env, None);
let mut outcomes = Vec::with_capacity(probes.len());
for probe in probes {
let evaluated = match &probe.predicate {
PredicateIR::Expr { expr } => EvaluatedAssertion {
record: eval_expr_assertion(probe, expr, &scope),
degraded_verify: false,
},
_ => eval_observed_assertion(probe, &material, self.vision.as_deref()).await,
};
outcomes.push(evaluated.record);
}
self.append(
&probe_path,
&RunLogPayload::PreflightProbed {
outcomes: outcomes.clone(),
},
)?;
let Some(missed) = outcomes
.iter()
.find(|outcome| outcome.result != VerdictStatus::Pass)
else {
return Ok(None);
};
let what = match missed.result {
VerdictStatus::Fail => "did not hold",
_ => "could not be evaluated (treated as drift)",
};
let detail = format!("probe '{}' {what}: {}", missed.assert_id, missed.reason);
if let Some((policy, used)) = active_retry.take()
&& used < policy.max_attempts
{
self.backoff_policy(&policy, used).await;
active_retry = Some((policy, used + 1));
continue;
}
match self
.consult_hook(
frame,
path,
base.handlers.as_deref(),
HandlerHook::OnResumeDrift,
None,
)
.await?
{
Consulted::None | Consulted::RepairFailed => {
return Ok(Some(Ctl::Blocked(BlockedReason::Drifted {
step_id: step_id.as_str().to_owned(),
detail,
})));
}
Consulted::Continue => {
return Ok(None);
}
Consulted::Abort => return Ok(Some(Ctl::Abort)),
Consulted::Escalated { status, .. } => match status {
VerdictStatus::Pass => return Ok(None),
_ => {
return Ok(Some(Ctl::Blocked(BlockedReason::Drifted {
step_id: step_id.as_str().to_owned(),
detail: format!("{detail}; escalate ruling: not acceptable"),
})));
}
},
Consulted::Retry(policy) => {
self.backoff_policy(&policy, 0).await;
active_retry = Some((policy, 1));
}
Consulted::Repaired | Consulted::RepairDone => {}
Consulted::Pending(pending) => return Ok(Some(Ctl::AwaitHuman(pending))),
Consulted::Propagate(ctl) => return Ok(Some(ctl)),
}
}
}
async fn fresh_material(
&mut self,
needs: &VerifyNeeds,
anchor: &RunPath,
) -> Result<ObserveMaterial, RunnerError> {
let mut wants = Vec::new();
if needs.ui_tree {
wants.push(ObserveWant::UiSnapshot);
}
if needs.vision {
wants.push(ObserveWant::Screenshot);
}
if wants.is_empty() {
return Ok(ObserveMaterial::default());
}
let observation = match self.session.observe(ObserveRequest { wants }, None).await {
Ok(observation) => observation,
Err(error) => {
return Ok(ObserveMaterial::absent(&format!(
"fresh observation failed: {error}"
)));
}
};
let mut cited = Vec::new();
let mut material = ObserveMaterial::default();
let record = self
.localize_observation(&observation, &mut cited, Some((needs, &mut material)))
.await?;
self.append(
anchor,
&RunLogPayload::ObservationRecorded {
observation: record,
},
)?;
Ok(material)
}
async fn exec_action(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a ActionStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&step_path);
let work = match &self.frontier {
Some((frontier_key, _)) if *frontier_key == key => {
self.frontier.take().map(|(_, work)| work)
}
_ => None,
};
let resolved = match &work {
Some(FrontierWork::Adopt { args, .. })
| Some(FrontierWork::Replay { args, .. })
| Some(FrontierWork::ConfirmEffect { args, .. })
| Some(FrontierWork::AbortRuled { args }) => Ok(args.clone()),
None => {
let attempt = step
.binding
.attempts
.first()
.expect("sealed action steps carry at least one bound attempt");
self.resolve_args(frame, attempt)
}
};
let resolved_inputs = match resolved {
Ok(args) => args,
Err(message) => {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("act phase failed [bind_arguments_invalid]: {message}"),
)
.await;
}
};
let had_open_span = self.open_span_inputs(&key).is_some();
self.enter_step(&step_path, &step.base, resolved_inputs.clone())?;
let resume_ruling = if work.is_none() && had_open_span {
match (self.settled.get(&key), self.recorded_verdicts.get(&key)) {
(Some(_), Some(ruling)) => Some(*ruling),
_ => None,
}
} else {
None
};
if !matches!(work, Some(FrontierWork::Adopt { .. }))
&& resume_ruling.is_none()
&& let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await?
{
return Ok(ctl);
}
if work.is_none()
&& resume_ruling.is_none()
&& let Some(ctl) = self.gate_supervision(&step_path, step, &resolved_inputs)?
{
return Ok(ctl);
}
let mut acted = if resume_ruling.is_some() {
None
} else {
Some(match work {
Some(FrontierWork::Adopt {
call_id,
intent_path,
outcome,
args,
chain_index,
}) => {
let attempt_n = attempt_of(&intent_path).unwrap_or(1);
let outcome = quarantine_unpersistable(*outcome);
let settled_seq = self.append(
&intent_path,
&RunLogPayload::ActionSettled {
call_id,
outcome: outcome.clone(),
},
)?;
let adopted = AdoptedSettle {
outcome,
settled_seq,
attempt_n,
};
self.act_chain(
frame,
step,
&step_path,
Some(args),
Some(adopted),
chain_start(chain_index, step)?,
)
.await?
}
Some(FrontierWork::Replay { args, chain_index }) => {
self.act_chain(
frame,
step,
&step_path,
Some(args),
None,
chain_start(chain_index, step)?,
)
.await?
}
Some(FrontierWork::ConfirmEffect { message, .. }) => {
ActPhase::Unconfirmed { message }
}
Some(FrontierWork::AbortRuled { .. }) => {
ActPhase::Aborted
}
None => {
self.act_chain(
frame,
step,
&step_path,
Some(resolved_inputs.clone()),
None,
0,
)
.await?
}
})
};
let mut last_verdict_seq: Option<u64> = None;
let mut seeded = false;
let mut active_retry: Option<(RetryPolicy, u32)> = None;
let mut ruling = resume_ruling;
loop {
let (status, degraded, summary, cited, projected, error_class): (
Option<VerdictStatus>,
bool,
String,
Vec<AssetRef>,
Option<Value>,
Option<ErrorClass>,
);
let mut round_manifest = EvidenceManifest::default();
let mut ruled_from_ledger = false;
match (acted.take(), ruling.take()) {
(None, Some((recorded_status, recorded_degraded))) => {
ruled_from_ledger = true;
let recovered = match self.settled.get(&key).map(|fact| &fact.outcome) {
Some(ActionOutcome::Succeeded { result }) => {
let raw_scope =
frame.scope(&self.env, Some((step_id.as_str(), &result.output)));
project_output(step, &result.output, &raw_scope).ok()
}
_ => None,
};
status = Some(recorded_status);
degraded = recorded_degraded;
summary = "resumed at the recorded verdict (handler re-entry)".to_owned();
cited = Vec::new();
projected = recovered;
error_class = None;
if matches!(
recorded_status,
VerdictStatus::Fail | VerdictStatus::Unknown
) {
let captured = self.capture_summary().await;
self.pending_summaries.insert(key.clone(), captured);
}
}
(Some(phase), _) => match phase {
ActPhase::Succeeded {
result,
degraded: degraded_execution,
settled_seq,
attempt_n,
} => {
let (round_cited, material, observed, manifest) = self
.observing(step, &step_path, attempt_n, &result, settled_seq)
.await?;
round_manifest = manifest;
frame.observed.insert(step_id.as_str().to_owned(), observed);
let raw_scope =
frame.scope(&self.env, Some((step_id.as_str(), &result.output)));
let round_projected = match project_output(step, &result.output, &raw_scope)
{
Ok(value) => value,
Err(error) => {
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("output projection failed: {error}"),
)
.await;
}
};
let assert_scope =
frame.scope(&self.env, Some((step_id.as_str(), &round_projected)));
let mut outcomes = Vec::with_capacity(step.assertions.len());
let mut degraded_verify = false;
for assertion in &step.assertions {
let evaluated = match &assertion.predicate {
PredicateIR::Expr { expr } => EvaluatedAssertion {
record: eval_expr_assertion(assertion, expr, &assert_scope),
degraded_verify: false,
},
_ => {
eval_observed_assertion(
assertion,
&material,
self.vision.as_deref(),
)
.await
}
};
degraded_verify |= evaluated.degraded_verify;
outcomes.push(evaluated.record);
}
for outcome in &outcomes {
let mut path = step_path.clone();
path.push(PathFrame::Phase {
phase: Phase::Assert,
});
path.push(PathFrame::Assertion {
assert_id: outcome.assert_id.clone(),
});
self.append(
&path,
&RunLogPayload::AssertionEvaluated {
outcome: outcome.clone(),
},
)?;
}
if outcomes.is_empty() {
status = None;
degraded = false;
summary = String::new();
} else {
let folded = fold_step_verdict(
&outcomes,
degraded_execution,
degraded_verify,
frame.flow.verdict_policy,
);
status = Some(folded.status);
degraded = folded.degraded;
summary = folded.summary;
}
cited = round_cited;
projected = Some(round_projected);
error_class = None;
}
ActPhase::Unconfirmed { message } => {
let needs = VerifyNeeds::of(&step.assertions);
let material = self.fresh_material(&needs, &step_path).await?;
let assert_scope = frame.scope(&self.env, None);
let mut outcomes = Vec::with_capacity(step.assertions.len());
let mut degraded_verify = false;
for assertion in &step.assertions {
let evaluated = match &assertion.predicate {
PredicateIR::Expr { expr } => EvaluatedAssertion {
record: eval_expr_assertion(assertion, expr, &assert_scope),
degraded_verify: false,
},
_ => {
eval_observed_assertion(
assertion,
&material,
self.vision.as_deref(),
)
.await
}
};
degraded_verify |= evaluated.degraded_verify;
outcomes.push(evaluated.record);
}
for outcome in &outcomes {
let mut path = step_path.clone();
path.push(PathFrame::Phase {
phase: Phase::Assert,
});
path.push(PathFrame::Assertion {
assert_id: outcome.assert_id.clone(),
});
self.append(
&path,
&RunLogPayload::AssertionEvaluated {
outcome: outcome.clone(),
},
)?;
}
let folded = fold_step_verdict(
&outcomes,
false,
degraded_verify,
frame.flow.verdict_policy,
);
status = Some(folded.status);
degraded = folded.degraded;
summary = format!(
"{message}; assertions over a fresh observation: {}",
folded.summary
);
cited = Vec::new();
projected = None;
error_class = None;
}
ActPhase::StepFail { class, message } => {
let wire = serde_json::to_value(class).expect("ErrorClass serializes");
let wire = wire.as_str().expect("ErrorClass is a string literal");
status = Some(VerdictStatus::Fail);
degraded = false;
summary = format!("act phase failed [{wire}]: {message}");
cited = Vec::new();
projected = None;
error_class = Some(class);
}
ActPhase::StepUnknown {
message,
error_class: class,
} => {
status = Some(VerdictStatus::Unknown);
degraded = false;
summary = message;
cited = Vec::new();
projected = None;
error_class = class;
}
ActPhase::Aborted => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
ActPhase::Suspend(reason) => return Ok(Ctl::Suspend(reason)),
},
(None, None) => unreachable!("every round has an act result or a ruling"),
}
let round_status = status;
if let Some(current) = round_status {
if !ruled_from_ledger {
let folded = FoldedVerdict {
status: current,
degraded,
summary: summary.clone(),
};
let supersedes = last_verdict_seq.map(|seq| format!("seq:{seq}"));
let seq = self
.record_step_verdict(
&step_path,
&folded,
cited,
supersedes,
std::mem::take(&mut round_manifest),
)
.await?;
last_verdict_seq = Some(seq);
}
if seeded {
frame.reseed_last(&step_id, current, degraded);
} else {
frame.seed_verdict(&step_id, current, degraded);
seeded = true;
}
}
if round_status.is_none() || round_status == Some(VerdictStatus::Pass) {
let exit_manifest = if round_status.is_none() {
std::mem::take(&mut round_manifest)
} else {
EvidenceManifest::default()
};
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: projected.clone(),
localized: exit_manifest.localized,
localization_gaps: exit_manifest.gaps,
},
)?;
if let Some(projected) = projected {
frame.outputs.insert(step_id.as_str().to_owned(), projected);
}
return Ok(Ctl::Continue);
}
let current = round_status.expect("checked above");
if let Some((policy, used)) = active_retry.take()
&& used < policy.max_attempts
{
self.backoff_policy(&policy, used).await;
active_retry = Some((policy, used + 1));
acted = Some(
self.act_chain(
frame,
step,
&step_path,
Some(resolved_inputs.clone()),
None,
0,
)
.await?,
);
continue;
}
let hook = if error_class.is_some() {
HandlerHook::OnError
} else if current == VerdictStatus::Fail {
HandlerHook::OnFail
} else {
HandlerHook::OnUnknown
};
match self
.consult_hook(
frame,
&step_path,
step.base.handlers.as_deref(),
hook,
error_class,
)
.await?
{
Consulted::None | Consulted::RepairFailed => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: projected.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(projected) = projected {
frame.outputs.insert(step_id.as_str().to_owned(), projected);
}
return Ok(if current == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Continue => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: projected.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(projected) = projected {
frame.outputs.insert(step_id.as_str().to_owned(), projected);
}
return Ok(Ctl::Continue);
}
Consulted::Abort => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
Consulted::Escalated {
status: ruled,
summary: ruled_summary,
evidence: ruling_evidence,
} => {
let folded = FoldedVerdict {
status: ruled,
degraded: false,
summary: ruled_summary,
};
let supersedes = last_verdict_seq.map(|seq| format!("seq:{seq}"));
let (cited, manifest) = escalate_verdict_material(&ruling_evidence);
let ruled_seq = self
.record_step_verdict(&step_path, &folded, cited, supersedes, manifest)
.await?;
self.link_ruling_evidence(ruled_seq, &ruling_evidence)?;
if seeded {
frame.reseed_last(&step_id, ruled, false);
} else {
frame.seed_verdict(&step_id, ruled, false);
}
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: projected.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(projected) = projected {
frame.outputs.insert(step_id.as_str().to_owned(), projected);
}
return Ok(if ruled == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Retry(policy) => {
self.backoff_policy(&policy, 0).await;
active_retry = Some((policy, 1));
acted = Some(
self.act_chain(
frame,
step,
&step_path,
Some(resolved_inputs.clone()),
None,
0,
)
.await?,
);
}
Consulted::Repaired | Consulted::RepairDone => {
acted = Some(
self.act_chain(
frame,
step,
&step_path,
Some(resolved_inputs.clone()),
None,
0,
)
.await?,
);
}
Consulted::Pending(pending) => {
return Ok(Ctl::AwaitHuman(pending));
}
Consulted::Propagate(ctl) => return Ok(ctl),
}
}
}
async fn settle_error(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
step_id: &StepId,
status: VerdictStatus,
summary: String,
) -> Result<Ctl, RunnerError> {
let folded = FoldedVerdict {
status,
degraded: false,
summary,
};
self.record_step_verdict(
step_path,
&folded,
Vec::new(),
None,
EvidenceManifest::default(),
)
.await?;
frame.seed_verdict(step_id, status, false);
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if status == VerdictStatus::Fail {
Ok(Ctl::HaltFail)
} else {
Ok(Ctl::Continue)
}
}
async fn act_chain(
&mut self,
frame: &FrameState<'a>,
step: &'a ActionStepIR,
step_path: &RunPath,
args_override: Option<Value>,
mut adopted: Option<AdoptedSettle>,
start_position: usize,
) -> Result<ActPhase, RunnerError> {
let key = instance_key(step_path);
let chain_len = step.binding.attempts.len();
for (position, attempt) in step
.binding
.attempts
.iter()
.enumerate()
.skip(start_position)
{
let args = if position == start_position && args_override.is_some() {
args_override.clone().expect("checked is_some")
} else {
match self.resolve_args(frame, attempt) {
Ok(args) => args,
Err(message) => {
return Ok(ActPhase::StepFail {
class: ErrorClass::BindArgumentsInvalid,
message,
});
}
}
};
let mut tries: u32 = 0;
loop {
tries += 1;
let (outcome, settled_seq, attempt_n) = match adopted.take() {
Some(settle) => (settle.outcome, settle.settled_seq, settle.attempt_n),
None => {
let attempt_n = self.next_attempt_n(&key);
let call_id = uuid::Uuid::new_v4().to_string();
let mut attempt_path = step_path.clone();
attempt_path.push(PathFrame::Attempt { n: attempt_n });
self.store.write_action_intent(
&self.run_id,
now_ms(),
&attempt_path,
&call_id,
args.clone(),
Some(pointlock_store::IntentDispatch {
chain_index: (position + 1) as u32,
channel: attempt.channel,
action_name: attempt.action_name.clone(),
}),
)?;
let call = BoundActionCall {
call_id: call_id.clone(),
action_name: attempt.action_name.clone(),
arguments: args.clone(),
action_timeout_ms: step.base.timeout_ms,
request_timeout_ms: None,
};
let outcome = match self.session.execute(call, None).await {
Ok(outcome) => outcome,
Err(error) => {
return Ok(ActPhase::Suspend(format!(
"no terminal for callId {call_id}: {error}"
)));
}
};
let outcome = quarantine_unpersistable(outcome);
let settled_seq = self.append(
&attempt_path,
&RunLogPayload::ActionSettled {
call_id: call_id.clone(),
outcome: outcome.clone(),
},
)?;
(outcome, settled_seq, attempt_n)
}
};
match outcome {
ActionOutcome::Succeeded { result } => {
let degraded = !execution_accepted(attempt, &result.execution);
return Ok(ActPhase::Succeeded {
result,
degraded,
settled_seq,
attempt_n,
});
}
other => {
let class = classify(&other);
let message = terminal_message(&other);
if class == ErrorClass::ActionCancelled {
return Ok(ActPhase::Aborted);
}
if retry_allowed(step, class, tries) {
self.backoff(step, tries).await;
continue;
}
match class {
ErrorClass::ActionFailedFinal if position + 1 < chain_len => break,
ErrorClass::ActionTimedOut => {
if !step.assertions.is_empty() {
return Ok(ActPhase::Unconfirmed {
message: format!("action timed out ({message})"),
});
}
return Ok(ActPhase::StepUnknown {
message: format!(
"action timed out and the outcome could not be \
confirmed: {message}"
),
error_class: None,
});
}
ErrorClass::SessionDegraded => {
return Ok(ActPhase::StepUnknown {
message: format!("session degraded: {message}"),
error_class: Some(ErrorClass::SessionDegraded),
});
}
_ => {
return Ok(ActPhase::StepFail { class, message });
}
}
}
}
}
}
unreachable!("the act chain always returns from within its last attempt")
}
fn next_attempt_n(&mut self, key: &str) -> u64 {
let counter = self.attempt_base.entry(key.to_owned()).or_insert(0);
*counter += 1;
*counter
}
async fn backoff(&self, step: &ActionStepIR, tries: u32) {
let Some(policy) = &step.base.retry else {
return;
};
self.backoff_policy(policy, tries).await;
}
async fn backoff_policy(&self, policy: &RetryPolicy, tries: u32) {
let ms = backoff_ms(policy, tries);
if ms > 0 {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
}
}
fn gate_supervision(
&mut self,
step_path: &RunPath,
step: &'a ActionStepIR,
resolved_inputs: &Value,
) -> Result<Option<Ctl>, RunnerError> {
let key = instance_key(step_path);
if let Some(fact) = self
.human
.get(&key)
.filter(|fact| fact.purpose == HumanPurpose::Supervision)
{
let decision = fact
.final_response
.as_ref()
.and_then(|response| response.get("decision"))
.and_then(Value::as_str);
return match decision {
Some("proceed") => Ok(None),
Some("abort") => {
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
Ok(Some(Ctl::Abort))
}
_ => Ok(Some(Ctl::AwaitHuman(HumanPending {
run_path: fact.run_path.clone(),
request_id: fact.request_id.clone(),
purpose: HumanPurpose::Supervision,
mode: None,
prompt: fact.prompt.clone(),
deadline_at_ms: None,
}))),
};
}
let Some(policy) = self.supervise else {
return Ok(None);
};
let gated = match policy {
SupervisePolicy::All => true,
SupervisePolicy::Mutating => step.effect == EffectClassAction::Mutating,
};
if !gated {
return Ok(None);
}
let attempt = step
.binding
.attempts
.first()
.expect("sealed action steps carry at least one bound attempt");
let action_name = attempt.action_name.as_str().to_owned();
let rendered = render_run_path(step_path);
let request_id = uuid::Uuid::new_v4().to_string();
let prompt = format!(
"Supervision gate: approve dispatching action '{action_name}' at {rendered}? \
The resolved inputs are presented."
);
let presents = serde_json::json!([
{ "kind": "value", "label": "runPath", "value": rendered },
{ "kind": "value", "label": "actionName", "value": action_name },
{ "kind": "value", "label": "resolvedInputs", "value": resolved_inputs },
]);
self.append(
step_path,
&RunLogPayload::HumanRequested {
request_id: request_id.clone(),
purpose: HumanPurpose::Supervision,
mode: None,
prompt: prompt.clone(),
presents,
decisions: None,
output_schema: None,
deadline_at_ms: None,
},
)?;
Ok(Some(Ctl::AwaitHuman(HumanPending {
run_path: step_path.clone(),
request_id,
purpose: HumanPurpose::Supervision,
mode: None,
prompt,
deadline_at_ms: None,
})))
}
async fn exec_human(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a HumanStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&step_path);
if let Some(fact) = self
.human
.get(&key)
.filter(|fact| fact.purpose == HumanPurpose::Step)
{
let fact = fact.clone();
if let Some(response) = fact.final_response.clone() {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_human_response(frame, &step_path, step, &fact, response)
.await;
}
match fact.deadline_at_ms {
Some(deadline) if self.now() > deadline => {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_human_timeout(frame, &step_path, step, &fact)
.await;
}
_ => {
return Ok(Ctl::AwaitHuman(HumanPending {
run_path: fact.run_path.clone(),
request_id: fact.request_id.clone(),
purpose: HumanPurpose::Step,
mode: Some(step.mode),
prompt: fact.prompt.clone(),
deadline_at_ms: fact.deadline_at_ms,
}));
}
}
}
let snapshot = match self.open_span_inputs(&key) {
Some(archived) => archived,
None => {
let scope = frame.scope(&self.env, None);
let mut items = Vec::with_capacity(step.presents.len());
let mut error = None;
for (index, expr) in step.presents.iter().enumerate() {
match pointlock_expr::eval(expr, &scope) {
Ok(value) => items.push(value),
Err(eval_error) => {
error =
Some(format!("present #{index} evaluation failed: {eval_error}"));
break;
}
}
}
if let Some(message) = error {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("human presents failed [bind_arguments_invalid]: {message}"),
)
.await;
}
serde_json::json!({ "presents": items })
}
};
self.enter_step(&step_path, &step.base, snapshot.clone())?;
if let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await? {
return Ok(ctl);
}
let presents = snapshot
.get("presents")
.cloned()
.unwrap_or(Value::Array(Vec::new()));
let request_id = uuid::Uuid::new_v4().to_string();
let deadline_at_ms = self.now().saturating_add(step.timeout_ms);
self.append(
&step_path,
&RunLogPayload::HumanRequested {
request_id: request_id.clone(),
purpose: HumanPurpose::Step,
mode: Some(step.mode),
prompt: step.prompt.clone(),
presents,
decisions: step.decisions.clone(),
output_schema: step.output_schema.clone(),
deadline_at_ms: Some(deadline_at_ms),
},
)?;
Ok(Ctl::AwaitHuman(HumanPending {
run_path: step_path,
request_id,
purpose: HumanPurpose::Step,
mode: Some(step.mode),
prompt: step.prompt.clone(),
deadline_at_ms: Some(deadline_at_ms),
}))
}
async fn settle_human_response(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
step: &'a HumanStepIR,
fact: &HumanRequestFact,
response: Value,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let decision = response
.get("decision")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let (status, output, summary) = match step.mode {
HumanMode::Confirm => {
let labels = step
.decisions
.as_ref()
.expect("load validated confirm decisions");
let status = if labels.first().map(String::as_str) == Some(decision.as_str()) {
VerdictStatus::Pass
} else {
VerdictStatus::Fail
};
let position = if status == VerdictStatus::Pass {
"first"
} else {
"second"
};
(
status,
Some(response.clone()),
format!(
"human confirm decision '{decision}' is the {position} label \
(position-mapped verdict)"
),
)
}
HumanMode::Judge => {
let status = match response.get("status").and_then(Value::as_str) {
Some("pass") => VerdictStatus::Pass,
Some("fail") => VerdictStatus::Fail,
_ => VerdictStatus::Unknown,
};
let label = response
.get("status")
.and_then(Value::as_str)
.unwrap_or("unknown");
(
status,
Some(response.clone()),
format!("human judge ruling: {label}"),
)
}
HumanMode::ProvideInput => {
let input = response.get("input").cloned().unwrap_or(Value::Null);
(
VerdictStatus::Pass,
Some(input),
"human provided input validated against the outputSchema".to_owned(),
)
}
HumanMode::RepairWorld => {
let status = if decision == "done" {
VerdictStatus::Pass
} else {
VerdictStatus::Fail
};
let summary = if status == VerdictStatus::Pass {
"human declared the world repaired (testimony; follow-up machine \
re-check advised)"
.to_owned()
} else {
"human declared the world unrepairable (cannotRepair)".to_owned()
};
(status, Some(response.clone()), summary)
}
};
let asset = self.put_human_evidence(step_path, fact, Some(&response), "response")?;
let folded = FoldedVerdict {
status,
degraded: false,
summary,
};
let verdict_seq = self
.record_step_verdict(
step_path,
&folded,
vec![asset.clone()],
None,
human_manifest(&asset),
)
.await?;
if let Some(sha256) = &asset.sha256 {
self.store
.link_evidence(&self.run_id, verdict_seq, &asset.id, sha256)?;
}
frame.seed_verdict(&step_id, status, false);
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: output.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(output) = output {
frame.outputs.insert(step_id.as_str().to_owned(), output);
}
if status == VerdictStatus::Fail {
Ok(Ctl::HaltFail)
} else {
Ok(Ctl::Continue)
}
}
async fn settle_human_timeout(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
step: &'a HumanStepIR,
fact: &HumanRequestFact,
) -> Result<Ctl, RunnerError> {
let step_id = &step.base.step_id;
let deadline = fact.deadline_at_ms.unwrap_or_default();
let asset = self.put_human_evidence(step_path, fact, None, "timeout")?;
let folded = FoldedVerdict {
status: VerdictStatus::Unknown,
degraded: false,
summary: format!(
"human response deadline (deadlineAtMs {deadline}) passed without a \
response; onTimeout is fixed to unknown"
),
};
let verdict_seq = self
.record_step_verdict(
step_path,
&folded,
vec![asset.clone()],
None,
human_manifest(&asset),
)
.await?;
if let Some(sha256) = &asset.sha256 {
self.store
.link_evidence(&self.run_id, verdict_seq, &asset.id, sha256)?;
}
frame.seed_verdict(step_id, VerdictStatus::Unknown, false);
match self
.consult_hook(
frame,
step_path,
step.base.handlers.as_deref(),
HandlerHook::OnUnknown,
None,
)
.await?
{
Consulted::None | Consulted::RepairFailed | Consulted::Continue => {}
Consulted::Abort => {
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
Consulted::Escalated {
status: ruled,
summary: ruled_summary,
evidence: ruling_evidence,
} => {
let folded = FoldedVerdict {
status: ruled,
degraded: false,
summary: ruled_summary,
};
let (cited, manifest) = escalate_verdict_material(&ruling_evidence);
let ruled_seq = self
.record_step_verdict(
step_path,
&folded,
cited,
Some(format!("seq:{verdict_seq}")),
manifest,
)
.await?;
self.link_ruling_evidence(ruled_seq, &ruling_evidence)?;
frame.reseed_last(step_id, ruled, false);
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(if ruled == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Retry(_) | Consulted::Repaired | Consulted::RepairDone => {
return Err(RunnerError::M0Unsupported {
detail: format!(
"re-ask dispositions on the timed-out human step '{step_id}' need \
fresh-request machinery — not in the M2 subset \
(escalate/continue/abort are supported)"
),
});
}
Consulted::Pending(pending) => return Ok(Ctl::AwaitHuman(pending)),
Consulted::Propagate(ctl) => return Ok(ctl),
}
self.append(
step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
Ok(Ctl::Continue)
}
fn link_ruling_evidence(
&mut self,
verdict_seq: u64,
evidence: &Option<AssetRef>,
) -> Result<(), RunnerError> {
if let Some(asset) = evidence
&& let Some(sha256) = &asset.sha256
{
self.store
.link_evidence(&self.run_id, verdict_seq, &asset.id, sha256)?;
}
Ok(())
}
fn put_human_evidence(
&mut self,
step_path: &RunPath,
fact: &HumanRequestFact,
response: Option<&Value>,
settled_as: &str,
) -> Result<AssetRef, RunnerError> {
let document = serde_json::json!({
"pointlockEvidence": "humanResponse/1",
"requestId": fact.request_id,
"runId": self.run_id,
"runPath": render_run_path(step_path),
"purpose": fact.purpose,
"mode": fact.mode,
"prompt": fact.prompt,
"presented": fact.presents,
"response": response.cloned().unwrap_or(Value::Null),
"actor": match (settled_as, &fact.final_actor) {
("timeout", _) => Value::Null,
(_, Some(actor)) => Value::String(actor.clone()),
(_, None) => Value::Null,
},
"deadlineAtMs": fact.deadline_at_ms,
"settledAs": settled_as,
});
let bytes = to_canonical_json(&document).into_bytes();
let put = self.store.put_evidence(&bytes, "application/json")?;
Ok(AssetRef {
id: format!("humanResponse:{}", fact.request_id),
media_type: "application/json".to_owned(),
uri: put.local_path,
sha256: Some(put.sha256),
})
}
fn resolve_binding(
&self,
frame: &FrameState<'a>,
step_handlers: Option<&'a [HandlerBinding]>,
hook: HandlerHook,
error_class: Option<ErrorClass>,
) -> Option<&'a HandlerBinding> {
let matches = |binding: &&'a HandlerBinding| {
binding.hook == hook
&& (hook != HandlerHook::OnError
|| match (&binding.error_classes, error_class) {
(None, _) => true,
(Some(filter), Some(class)) => filter.contains(&class),
(Some(_), None) => false,
})
};
step_handlers
.and_then(|bindings| bindings.iter().find(matches))
.or_else(|| {
frame
.flow
.handlers
.as_deref()
.and_then(|bindings| bindings.iter().find(matches))
})
}
async fn consult_hook(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
step_handlers: Option<&'a [HandlerBinding]>,
hook: HandlerHook,
error_class: Option<ErrorClass>,
) -> Result<Consulted, RunnerError> {
let Some(binding) = self.resolve_binding(frame, step_handlers, hook, error_class) else {
return Ok(Consulted::None);
};
let counter_key = crate::align::hook_trigger_key(&instance_key(step_path), hook);
let current = self.hook_triggers.get(&counter_key).copied().unwrap_or(0);
if current >= 1
&& let HandlerAction::Escalate { human } = &binding.action
{
let human_path = hook_child_path(step_path, hook, current, human);
if self.human.contains_key(&instance_key(&human_path)) {
return self
.run_hook_human(frame, step_path, hook, current, human)
.await;
}
}
let trigger = current + 1;
if trigger > u64::from(binding.max_triggers) {
return Ok(Consulted::None);
}
self.hook_triggers.insert(counter_key, trigger);
let disposition = match &binding.action {
HandlerAction::Retry { .. } => "retry",
HandlerAction::Continue => "continue",
HandlerAction::Abort => "abort",
HandlerAction::Escalate { .. } => "escalate",
HandlerAction::Repair { .. } => "repair",
};
self.append(
step_path,
&RunLogPayload::HandlerTriggered {
hook,
trigger,
disposition: Some(disposition.to_owned()),
},
)?;
match &binding.action {
HandlerAction::Retry { policy } => Ok(Consulted::Retry(policy.clone())),
HandlerAction::Continue => Ok(Consulted::Continue),
HandlerAction::Abort => Ok(Consulted::Abort),
HandlerAction::Escalate { human } => {
self.run_hook_human(frame, step_path, hook, trigger, human)
.await
}
HandlerAction::Repair { flow_ref } => {
self.run_repair(frame, step_path, hook, trigger, flow_ref)
.await
}
}
}
async fn run_hook_human(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
hook: HandlerHook,
trigger: u64,
human: &'a HumanStepIR,
) -> Result<Consulted, RunnerError> {
let human_path = hook_child_path(step_path, hook, trigger, human);
let key = instance_key(&human_path);
if let Some(fact) = self.human.get(&key) {
let fact = fact.clone();
if let Some(response) = fact.final_response.clone() {
self.human.remove(&key);
let asset =
self.put_human_evidence(&human_path, &fact, Some(&response), "response")?;
return Ok(match map_escalate_response(human, &response) {
Consulted::Escalated {
status, summary, ..
} => Consulted::Escalated {
status,
summary,
evidence: Some(asset),
},
other => other,
});
}
match fact.deadline_at_ms {
Some(deadline) if self.now() > deadline => {
self.human.remove(&key);
let asset = self.put_human_evidence(&human_path, &fact, None, "timeout")?;
return Ok(Consulted::Escalated {
status: VerdictStatus::Unknown,
summary: format!(
"escalate human '{}' timed out (deadline watermark passed): unknown",
human.base.step_id
),
evidence: Some(asset),
});
}
_ => {
return Ok(Consulted::Pending(HumanPending {
run_path: fact.run_path.clone(),
request_id: fact.request_id.clone(),
purpose: HumanPurpose::Step,
mode: Some(human.mode),
prompt: fact.prompt.clone(),
deadline_at_ms: fact.deadline_at_ms,
}));
}
}
}
let scope = frame.scope(&self.env, None);
let mut items = Vec::with_capacity(human.presents.len());
for expr in &human.presents {
match pointlock_expr::eval(expr, &scope) {
Ok(value) => items.push(value),
Err(_) => items.push(Value::Null),
}
}
let request_id = uuid::Uuid::new_v4().to_string();
let deadline_at_ms = self.now().saturating_add(human.timeout_ms);
self.append(
&human_path,
&RunLogPayload::HumanRequested {
request_id: request_id.clone(),
purpose: HumanPurpose::Step,
mode: Some(human.mode),
prompt: human.prompt.clone(),
presents: Value::Array(items),
decisions: human.decisions.clone(),
output_schema: human.output_schema.clone(),
deadline_at_ms: Some(deadline_at_ms),
},
)?;
Ok(Consulted::Pending(HumanPending {
run_path: human_path,
request_id,
purpose: HumanPurpose::Step,
mode: Some(human.mode),
prompt: human.prompt.clone(),
deadline_at_ms: Some(deadline_at_ms),
}))
}
async fn run_repair(
&mut self,
frame: &mut FrameState<'a>,
step_path: &RunPath,
hook: HandlerHook,
trigger: u64,
flow_ref: &'a pointlock_ir::FlowRef,
) -> Result<Consulted, RunnerError> {
let callee = self.flows.callee(flow_ref);
let mut repair_path = step_path.clone();
repair_path.push(hook_frame(hook, trigger));
repair_path.push(PathFrame::Call {
step_id: None,
callee_flow_id: callee.flow_id.clone(),
callee_ir_hash: callee.ir_hash.clone(),
});
let params = match call_inputs_gate(callee, Map::new()) {
Ok(params) => params,
Err(_) => return Ok(Consulted::RepairFailed),
};
self.append(
&repair_path,
&RunLogPayload::CallFramePushed {
frame: CallFrame {
flow_id: callee.flow_id.clone(),
ir_hash: callee.ir_hash.clone(),
call_step_id: None,
inputs_snapshot: Value::Object(params.clone()),
vars: BTreeMap::new(),
iter_stack: Vec::new(),
next_index: 0,
},
rebase: false,
},
)?;
let mut repair_frame =
FrameState::new(callee, repair_path.clone(), params, frame.depth + 1);
let ctl = self
.exec_body(&mut repair_frame, repair_path.clone(), &callee.body, 0)
.await?;
match ctl {
Ctl::Continue | Ctl::HaltFail => {}
other => {
return Ok(Consulted::Propagate(other));
}
}
self.append(
&repair_path,
&RunLogPayload::CallFramePopped { outputs: None },
)?;
let verdict = fold_flow_verdict(&repair_frame.fold, callee.verdict_policy);
match (ctl, verdict) {
(Ctl::HaltFail, _) => Ok(Consulted::RepairFailed),
(_, Some(folded)) if folded.status != VerdictStatus::Pass => {
Ok(Consulted::RepairFailed)
}
_ => Ok(Consulted::RepairDone),
}
}
async fn exec_call(
&mut self,
frame: &mut FrameState<'a>,
call_path: RunPath,
step: &'a CallStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&call_path);
let callee: &'a FlowIR = self.flows.callee(&step.flow_ref);
if frame.depth + 1 > MAX_CALL_DEPTH {
return Err(RunnerError::CallDepthExceeded {
depth: frame.depth + 1,
max: MAX_CALL_DEPTH,
});
}
let archived = self.open_span_inputs(&key);
let gated = match archived {
Some(Value::Object(map)) => map,
Some(other) => {
self.enter_step(&call_path, &step.base, other)?;
return self
.settle_error(
frame,
&call_path,
&step_id,
VerdictStatus::Fail,
"archived call inputs snapshot is not an object".to_owned(),
)
.await;
}
None => {
let scope = frame.scope(&self.env, None);
let mut inputs = Map::new();
let mut eval_error = None;
for (name, expr) in step.inputs.iter() {
match pointlock_expr::eval(expr, &scope) {
Ok(value) => {
inputs.insert(name.as_str().to_owned(), value);
}
Err(error) => {
eval_error = Some(format!("input '{name}' evaluation failed: {error}"));
break;
}
}
}
if let Some(message) = eval_error {
self.enter_step(&call_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&call_path,
&step_id,
VerdictStatus::Fail,
format!("call inputs failed [bind_arguments_invalid]: {message}"),
)
.await;
}
match call_inputs_gate(callee, inputs.clone()) {
Ok(gated) => gated,
Err(message) => {
self.enter_step(&call_path, &step.base, Value::Object(inputs))?;
return self
.settle_error(
frame,
&call_path,
&step_id,
VerdictStatus::Fail,
format!("call inputs failed [bind_arguments_invalid]: {message}"),
)
.await;
}
}
}
};
self.enter_step(&call_path, &step.base, Value::Object(gated.clone()))?;
if let Some(ctl) = self.probe_or_note(frame, &call_path, &step.base).await? {
return Ok(ctl);
}
let open_pin = self.live_frames.remove(&key);
let rebase = match &open_pin {
None => false,
Some(pin) => *pin != callee.ir_hash,
};
if open_pin.is_none() || rebase {
self.append(
&call_path,
&RunLogPayload::CallFramePushed {
frame: CallFrame {
flow_id: callee.flow_id.clone(),
ir_hash: callee.ir_hash.clone(),
call_step_id: Some(step_id.clone()),
inputs_snapshot: Value::Object(gated.clone()),
vars: BTreeMap::new(),
iter_stack: Vec::new(),
next_index: 0,
},
rebase,
},
)?;
}
let mut callee_frame = FrameState::new(callee, call_path.clone(), gated, frame.depth + 1);
let ctl = self
.exec_body(&mut callee_frame, call_path.clone(), &callee.body, 0)
.await?;
match ctl {
Ctl::Continue | Ctl::HaltFail => {}
Ctl::Abort => {
self.append(
&call_path,
&RunLogPayload::CallFramePopped { outputs: None },
)?;
self.append(
&call_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
other => return Ok(other),
}
let outputs = if matches!(ctl, Ctl::Continue) {
match self.call_outputs_gate(callee, &callee_frame) {
Ok(outputs) => Some(outputs),
Err(message) => {
self.append(
&call_path,
&RunLogPayload::CallFramePopped { outputs: None },
)?;
return self
.settle_error(
frame,
&call_path,
&step_id,
VerdictStatus::Fail,
format!("callee outputs failed the outbound gate: {message}"),
)
.await;
}
}
} else {
None
};
self.append(
&call_path,
&RunLogPayload::CallFramePopped {
outputs: outputs.clone(),
},
)?;
let callee_verdict = fold_flow_verdict(&callee_frame.fold, callee.verdict_policy);
let mut status = None;
let mut verdict_seq = None;
if let Some(folded) = callee_verdict {
let folded = FoldedVerdict {
status: folded.status,
degraded: folded.degraded,
summary: format!(
"callee '{}' flow verdict: {}",
callee.flow_id, folded.summary
),
};
let seq = self
.record_step_verdict(
&call_path,
&folded,
Vec::new(),
None,
EvidenceManifest::default(),
)
.await?;
verdict_seq = Some(seq);
frame.seed_verdict(&step_id, folded.status, folded.degraded);
status = Some(folded.status);
}
if matches!(
status,
Some(VerdictStatus::Fail) | Some(VerdictStatus::Unknown)
) {
let hook = if status == Some(VerdictStatus::Fail) {
HandlerHook::OnFail
} else {
HandlerHook::OnUnknown
};
match self
.consult_hook(frame, &call_path, step.base.handlers.as_deref(), hook, None)
.await?
{
Consulted::None | Consulted::RepairFailed => {}
Consulted::Continue => {
self.append(
&call_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: outputs.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(outputs) = outputs {
frame.outputs.insert(step_id.as_str().to_owned(), outputs);
}
return Ok(Ctl::Continue);
}
Consulted::Abort => {
self.append(
&call_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
Consulted::Escalated {
status: ruled,
summary: ruled_summary,
evidence: ruling_evidence,
} => {
let folded = FoldedVerdict {
status: ruled,
degraded: false,
summary: ruled_summary,
};
let supersedes = verdict_seq.map(|seq| format!("seq:{seq}"));
let (cited, manifest) = escalate_verdict_material(&ruling_evidence);
let ruled_seq = self
.record_step_verdict(&call_path, &folded, cited, supersedes, manifest)
.await?;
self.link_ruling_evidence(ruled_seq, &ruling_evidence)?;
frame.reseed_last(&step_id, ruled, false);
self.append(
&call_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: outputs.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(outputs) = outputs {
frame.outputs.insert(step_id.as_str().to_owned(), outputs);
}
return Ok(if ruled == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Retry(_) | Consulted::Repaired | Consulted::RepairDone => {
return Err(RunnerError::M0Unsupported {
detail: format!(
"handler re-invocation dispositions on call step '{step_id}' need the 07 §1 \
attempt-framed full re-call — not in the M2 subset \
(escalate/continue/abort are supported)"
),
});
}
Consulted::Pending(pending) => {
return Ok(Ctl::AwaitHuman(pending));
}
Consulted::Propagate(inner) => return Ok(inner),
}
}
self.append(
&call_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: outputs.clone(),
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
if let Some(outputs) = outputs {
frame.outputs.insert(step_id.as_str().to_owned(), outputs);
}
if matches!(ctl, Ctl::HaltFail) || status == Some(VerdictStatus::Fail) {
Ok(Ctl::HaltFail)
} else {
Ok(Ctl::Continue)
}
}
fn call_outputs_gate(
&self,
callee: &FlowIR,
callee_frame: &FrameState<'a>,
) -> Result<Value, String> {
let scope = callee_frame.scope(&self.env, None);
let mut outputs = Map::new();
for decl in &callee.outputs {
let value = pointlock_expr::eval(&decl.from, &scope)
.map_err(|error| format!("output '{}' evaluation failed: {error}", decl.name))?;
jsonschema::validate(decl.schema.as_value(), &value)
.map_err(|error| format!("output '{}' failed its schema: {error}", decl.name))?;
outputs.insert(decl.name.as_str().to_owned(), value);
}
Ok(Value::Object(outputs))
}
async fn exec_if(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a IfStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&step_path);
let cond_value = match self.open_span_inputs(&key) {
Some(archived) => archived.get("cond").cloned().unwrap_or(Value::Null),
None => {
let scope = frame.scope(&self.env, None);
match pointlock_expr::eval(&step.cond, &scope) {
Ok(value) => value,
Err(error) => {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("if cond evaluation failed: {error}"),
)
.await;
}
}
}
};
let Some(cond) = cond_value.as_bool() else {
self.enter_step(
&step_path,
&step.base,
serde_json::json!({ "cond": cond_value }),
)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("if cond evaluated to a non-boolean value: {cond_value}"),
)
.await;
};
self.enter_step(&step_path, &step.base, serde_json::json!({ "cond": cond }))?;
if let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await? {
return Ok(ctl);
}
let empty: &'a [StepIR] = &[];
let (selected, unselected): (&'a [StepIR], &'a [StepIR]) = if cond {
(&step.then, step.r#else.as_deref().unwrap_or(empty))
} else {
(step.r#else.as_deref().unwrap_or(empty), &step.then)
};
self.stash_open_span_summaries().await;
self.record_pairs(&step_path, unselected, StepState::Skipped)?;
let ctl = self
.exec_body(frame, step_path.clone(), selected, 0)
.await?;
match ctl {
Ctl::Continue | Ctl::HaltFail => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
Ok(ctl)
}
Ctl::Abort => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
Ok(Ctl::Abort)
}
other => Ok(other),
}
}
async fn exec_foreach(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a ForeachStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&step_path);
let items_value = match self.open_span_inputs(&key) {
Some(archived) => archived.get("items").cloned().unwrap_or(Value::Null),
None => {
let scope = frame.scope(&self.env, None);
match pointlock_expr::eval(&step.items, &scope) {
Ok(value) => value,
Err(error) => {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("foreach items evaluation failed: {error}"),
)
.await;
}
}
}
};
let Some(items) = items_value.as_array().cloned() else {
self.enter_step(
&step_path,
&step.base,
serde_json::json!({ "items": items_value, "as": step.r#as.as_str() }),
)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("foreach items evaluated to a non-array value: {items_value}"),
)
.await;
};
self.enter_step(
&step_path,
&step.base,
serde_json::json!({ "items": items, "as": step.r#as.as_str() }),
)?;
if let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await? {
return Ok(ctl);
}
for (index, item) in items.iter().enumerate() {
frame
.iters
.push((step.r#as.as_str().to_owned(), item.clone()));
let mut iter_prefix = step_path.clone();
iter_prefix.push(PathFrame::Iteration {
index: index as u64,
key: None,
});
let ctl = self.exec_body(frame, iter_prefix, &step.body, 0).await?;
frame.iters.pop();
match ctl {
Ctl::Continue => {}
Ctl::HaltFail => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::HaltFail);
}
Ctl::Abort => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
other => return Ok(other),
}
}
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
Ok(Ctl::Continue)
}
async fn exec_let(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a LetStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
let key = instance_key(&step_path);
let evaluated = match self.open_span_inputs(&key) {
Some(Value::Object(map)) => map,
Some(_) | None => {
let scope = frame.scope(&self.env, None);
let mut evaluated = Map::new();
let mut error = None;
for (name, expr) in step.bindings.iter() {
if frame.vars.contains_key(name.as_str()) {
error = Some(format!(
"binding '{name}' rebinds an existing var (SSA single assignment)"
));
break;
}
match pointlock_expr::eval(expr, &scope) {
Ok(value) => {
evaluated.insert(name.as_str().to_owned(), value);
}
Err(eval_error) => {
error =
Some(format!("binding '{name}' evaluation failed: {eval_error}"));
break;
}
}
}
if let Some(message) = error {
self.enter_step(&step_path, &step.base, Value::Null)?;
return self
.settle_error(
frame,
&step_path,
&step_id,
VerdictStatus::Fail,
format!("let bindings failed: {message}"),
)
.await;
}
evaluated
}
};
self.enter_step(&step_path, &step.base, Value::Object(evaluated.clone()))?;
if let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await? {
return Ok(ctl);
}
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
for (name, value) in evaluated {
frame.vars.insert(name, value);
}
Ok(Ctl::Continue)
}
async fn exec_assert(
&mut self,
frame: &mut FrameState<'a>,
step_path: RunPath,
step: &'a AssertStepIR,
) -> Result<Ctl, RunnerError> {
let step_id = step.base.step_id.clone();
self.enter_step(&step_path, &step.base, Value::Null)?;
if let Some(ctl) = self.probe_or_note(frame, &step_path, &step.base).await? {
return Ok(ctl);
}
let needs = VerifyNeeds::of(&step.assertions);
let mut last_verdict_seq: Option<u64> = None;
let mut seeded = false;
let mut active_retry: Option<(RetryPolicy, u32)> = None;
loop {
let material = match &step.observe {
ObservationSource::Fresh(_) => {
let mut anchor = step_path.clone();
anchor.push(PathFrame::Phase {
phase: Phase::Observe,
});
self.fresh_material(&needs, &anchor).await?
}
ObservationSource::FromStep(from) => {
let which = from.which;
match frame.observed.get(from.from_step.as_str()) {
None => ObserveMaterial::absent(&format!(
"step '{}' has no archived observation in this frame",
from.from_step
)),
Some(observed) => {
let wanted = match which {
pointlock_ir::ObservationWhich::After => &observed.after_id,
pointlock_ir::ObservationWhich::Before => &observed.before_id,
};
match wanted {
None => ObserveMaterial::absent(&format!(
"step '{}' recorded no {:?} observation",
from.from_step, which
)),
Some(observation_id) => {
match observed
.observations
.iter()
.find(|record| record.observation_id == *observation_id)
{
None => ObserveMaterial::absent(
"the referenced observation was never localized",
),
Some(record) => {
material_from_observation(self.store, record)
}
}
}
}
}
}
}
};
let scope = frame.scope(&self.env, None);
let mut outcomes = Vec::with_capacity(step.assertions.len());
let mut degraded_verify = false;
for assertion in &step.assertions {
let evaluated = match &assertion.predicate {
PredicateIR::Expr { expr } => EvaluatedAssertion {
record: eval_expr_assertion(assertion, expr, &scope),
degraded_verify: false,
},
_ => {
eval_observed_assertion(assertion, &material, self.vision.as_deref()).await
}
};
degraded_verify |= evaluated.degraded_verify;
outcomes.push(evaluated.record);
}
for outcome in &outcomes {
let mut path = step_path.clone();
path.push(PathFrame::Phase {
phase: Phase::Assert,
});
path.push(PathFrame::Assertion {
assert_id: outcome.assert_id.clone(),
});
self.append(
&path,
&RunLogPayload::AssertionEvaluated {
outcome: outcome.clone(),
},
)?;
}
let folded =
fold_step_verdict(&outcomes, false, degraded_verify, frame.flow.verdict_policy);
let supersedes = last_verdict_seq.map(|seq| format!("seq:{seq}"));
let seq = self
.record_step_verdict(
&step_path,
&folded,
Vec::new(),
supersedes,
EvidenceManifest::default(),
)
.await?;
last_verdict_seq = Some(seq);
if seeded {
frame.reseed_last(&step_id, folded.status, folded.degraded);
} else {
frame.seed_verdict(&step_id, folded.status, folded.degraded);
seeded = true;
}
if folded.status == VerdictStatus::Pass {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Continue);
}
if let Some((policy, used)) = active_retry.take()
&& used < policy.max_attempts
{
self.backoff_policy(&policy, used).await;
active_retry = Some((policy, used + 1));
continue;
}
let hook = if folded.status == VerdictStatus::Fail {
HandlerHook::OnFail
} else {
HandlerHook::OnUnknown
};
match self
.consult_hook(frame, &step_path, step.base.handlers.as_deref(), hook, None)
.await?
{
Consulted::None | Consulted::RepairFailed => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(if folded.status == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Continue => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Continue);
}
Consulted::Abort => {
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Aborted,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(Ctl::Abort);
}
Consulted::Escalated {
status: ruled,
summary: ruled_summary,
evidence: ruling_evidence,
} => {
let folded = FoldedVerdict {
status: ruled,
degraded: false,
summary: ruled_summary,
};
let supersedes = last_verdict_seq.map(|seq| format!("seq:{seq}"));
let (cited, manifest) = escalate_verdict_material(&ruling_evidence);
let ruled_seq = self
.record_step_verdict(&step_path, &folded, cited, supersedes, manifest)
.await?;
self.link_ruling_evidence(ruled_seq, &ruling_evidence)?;
frame.reseed_last(&step_id, ruled, false);
self.append(
&step_path,
&RunLogPayload::StepExited {
provider_state_summary: None,
state: StepState::Judged,
output: None,
localized: Vec::new(),
localization_gaps: Vec::new(),
},
)?;
return Ok(if ruled == VerdictStatus::Fail {
Ctl::HaltFail
} else {
Ctl::Continue
});
}
Consulted::Retry(policy) => {
self.backoff_policy(&policy, 0).await;
active_retry = Some((policy, 1));
}
Consulted::Repaired | Consulted::RepairDone => {}
Consulted::Pending(pending) => {
return Ok(Ctl::AwaitHuman(pending));
}
Consulted::Propagate(ctl) => return Ok(ctl),
}
}
}
async fn observing(
&mut self,
step: &'a ActionStepIR,
step_path: &RunPath,
attempt_n: u64,
result: &ActionResult,
settled_seq: u64,
) -> Result<(Vec<AssetRef>, ObserveMaterial, StepObs, EvidenceManifest), RunnerError> {
let mut observe_path = step_path.clone();
observe_path.push(PathFrame::Attempt { n: attempt_n });
observe_path.push(PathFrame::Phase {
phase: Phase::Observe,
});
let needs = VerifyNeeds::of(&step.assertions);
let mut cited = Vec::new();
let mut material =
ObserveMaterial::absent("the action result carries no after observation");
let mut observed = StepObs {
observations: Vec::new(),
before_id: None,
after_id: None,
};
if let Some(observation) = &result.before {
let record = self
.localize_observation(observation, &mut cited, None)
.await?;
self.append(
&observe_path,
&RunLogPayload::ObservationRecorded {
observation: record.clone(),
},
)?;
observed.before_id = Some(record.observation_id.clone());
observed.observations.push(record);
}
if let Some(observation) = &result.after {
material = ObserveMaterial::default();
let record = self
.localize_observation(observation, &mut cited, Some((&needs, &mut material)))
.await?;
self.append(
&observe_path,
&RunLogPayload::ObservationRecorded {
observation: record.clone(),
},
)?;
observed.after_id = Some(record.observation_id.clone());
observed.observations.push(record);
}
let mut manifest = EvidenceManifest::default();
for asset in &result.evidence {
if manifest.localized.len() >= VERDICT_EVIDENCE_MAX_ENTRIES {
manifest.gaps.push(pointlock_ir::EvidenceGap {
asset: asset.clone(),
reason: format!(
"evidence cap exceeded ({VERDICT_EVIDENCE_MAX_ENTRIES} max per judgment)"
),
});
continue;
}
match self.try_localize(asset).await? {
Ok((evidence, _bytes)) => {
self.store.link_evidence(
&self.run_id,
settled_seq,
&asset.id,
&evidence.sha256,
)?;
cited.push(asset.clone());
manifest.localized.push(evidence);
}
Err(reason) => {
manifest.gaps.push(pointlock_ir::EvidenceGap {
asset: asset.clone(),
reason,
});
}
}
}
Ok((cited, material, observed, manifest))
}
async fn localize_observation(
&mut self,
observation: &Observation,
cited: &mut Vec<AssetRef>,
mut material: Option<(&VerifyNeeds, &mut ObserveMaterial)>,
) -> Result<ObservationRecord, RunnerError> {
let screenshot = match &observation.screenshot {
Some(asset) => match self.try_localize(asset).await? {
Ok((evidence, bytes)) => {
cited.push(asset.clone());
if let Some((needs, material)) = material.as_mut()
&& needs.vision
{
material.screenshot = Some((bytes, asset.media_type.clone()));
}
Some(evidence)
}
Err(gap) => {
if let Some((needs, material)) = material.as_mut()
&& needs.vision
{
material.screenshot_gap = Some(gap);
}
None
}
},
None => {
if let Some((needs, material)) = material.as_mut()
&& needs.vision
{
material.screenshot_gap = Some(match observation.screenshot_omission {
Some(reason) => {
format!("screenshot omitted by the provider ({reason:?})")
}
None => "the observation carries no screenshot".to_owned(),
});
}
None
}
};
let mut ui_snapshot_omission = observation.ui_snapshot_omission;
let ui_snapshot = match &observation.ui_snapshot {
Some(snapshot) => {
let wants_tree = matches!(&material, Some((needs, _)) if needs.ui_tree);
let localized = if wants_tree {
self.localize_ui_tree(
observation,
snapshot,
&mut material,
&mut ui_snapshot_omission,
)
.await?
} else {
self.try_localize(&snapshot.evidence)
.await?
.ok()
.map(|(evidence, _bytes)| evidence)
};
if localized.is_some() {
cited.push(snapshot.evidence.clone());
}
localized
}
None => {
if let Some((needs, material)) = material.as_mut()
&& needs.ui_tree
{
material.ui_tree_gap = Some(match observation.ui_snapshot_omission {
Some(reason) => {
format!("uiSnapshot omitted by the provider ({reason:?})")
}
None => "the observation carries no uiSnapshot".to_owned(),
});
}
None
}
};
let viewport = observation
.viewport
.scale_factor
.is_finite()
.then(|| observation.viewport.clone());
Ok(ObservationRecord {
observation_id: observation.id.clone(),
captured_at_ms: observation.captured_at_ms,
viewport,
screenshot,
screenshot_omission: observation.screenshot_omission,
ui_snapshot,
ui_snapshot_omission,
})
}
async fn localize_ui_tree(
&mut self,
observation: &Observation,
snapshot: &pointlock_ir::UiSnapshotRef,
material: &mut Option<(&VerifyNeeds, &mut ObserveMaterial)>,
ui_snapshot_omission: &mut Option<UiSnapshotOmissionReason>,
) -> Result<Option<EvidenceRef>, RunnerError> {
match self.session.ui_snapshot(&observation.id).await {
Ok(UiSnapshotOutcome::Available { snapshot: tree }) => {
let bytes =
serde_json::to_vec(&tree).expect("a serde_json::Value always serializes");
let put = self.store.put_evidence(&bytes, "application/json")?;
if let Some((_, material)) = material.as_mut() {
material.ui_tree = Some(bytes);
}
Ok(Some(EvidenceRef {
asset: snapshot.evidence.clone(),
sha256: put.sha256,
local_path: put.local_path,
}))
}
Ok(UiSnapshotOutcome::Unavailable { reason }) => {
*ui_snapshot_omission = Some(reason);
if let Some((_, material)) = material.as_mut() {
material.ui_tree_gap =
Some(format!("uiSnapshot dereference unavailable ({reason:?})"));
}
Ok(None)
}
Err(error) => {
if let Some((_, material)) = material.as_mut() {
material.ui_tree_gap = Some(format!("uiSnapshot dereference failed: {error}"));
}
Ok(None)
}
}
}
async fn try_localize(
&mut self,
asset: &AssetRef,
) -> Result<Result<(EvidenceRef, Vec<u8>), String>, RunnerError> {
let mut stream = match self.session.fetch_evidence(asset).await {
Ok(stream) => stream,
Err(error) => {
return Ok(Err(format!(
"evidence fetch failed for asset {}: {error}",
asset.id
)));
}
};
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(part) => bytes.extend(part),
Err(error) => {
return Ok(Err(format!(
"evidence stream failed for asset {}: {error}",
asset.id
)));
}
}
}
let put = self.store.put_evidence(&bytes, &asset.media_type)?;
if let Some(expected) = &asset.sha256
&& expected != &put.sha256
{
return Ok(Err(format!(
"evidence integrity failure for asset {}: sha256 {} != declared {expected}",
asset.id, put.sha256
)));
}
let evidence = EvidenceRef {
asset: asset.clone(),
sha256: put.sha256,
local_path: put.local_path,
};
Ok(Ok((evidence, bytes)))
}
async fn record_step_verdict(
&mut self,
path: &RunPath,
folded: &FoldedVerdict,
cited: Vec<AssetRef>,
supersedes: Option<String>,
manifest: EvidenceManifest,
) -> Result<u64, RunnerError> {
let verdict = Verdict {
status: folded.status,
degraded: folded.degraded,
summary: folded.summary.clone(),
evidence: cited
.into_iter()
.take(VERDICT_EVIDENCE_MAX_ENTRIES)
.collect(),
supersedes,
};
let key = instance_key(path);
match verdict.status {
VerdictStatus::Fail | VerdictStatus::Unknown => {
let summary = self.capture_summary().await;
self.pending_summaries.insert(key, summary);
}
VerdictStatus::Pass => {
self.pending_summaries.remove(&key);
}
}
let remote_archival_error = self.try_verdict_writeback(&verdict).await;
let seq = self.append(
path,
&RunLogPayload::VerdictRecorded {
verdict: verdict.clone(),
localized: manifest.localized,
localization_gaps: manifest.gaps,
remote_archival_error,
},
)?;
Ok(seq)
}
async fn try_verdict_writeback(&mut self, verdict: &Verdict) -> Option<String> {
self.session
.record_verdict(VerdictWrite {
status: verdict.status,
summary: cap_wire_summary(verdict),
evidence: verdict
.evidence
.iter()
.take(VERDICT_EVIDENCE_MAX_ENTRIES)
.cloned()
.collect(),
})
.await
.err()
.map(|error| format!("remote archival failed: {error}"))
}
async fn end_session(&mut self, outcome: SessionOutcome) {
let _ = self.session.end(outcome, None).await;
}
}
fn call_inputs_gate(
callee: &FlowIR,
inputs: Map<String, Value>,
) -> Result<Map<String, Value>, String> {
for key in inputs.keys() {
if !callee
.params
.iter()
.any(|decl: &ParamDecl| decl.name.as_str() == key)
{
return Err(format!(
"input '{key}' is not a declared param of callee '{}'",
callee.flow_id
));
}
}
let gated =
params_with_defaults(callee, Value::Object(inputs)).map_err(|error| error.to_string())?;
for decl in &callee.params {
if let Some(value) = gated.get(decl.name.as_str()) {
jsonschema::validate(decl.schema.as_value(), value)
.map_err(|error| format!("param '{}' failed its schema: {error}", decl.name))?;
}
}
Ok(gated)
}
pub(crate) fn params_with_defaults(
flow: &FlowIR,
params: Value,
) -> Result<Map<String, Value>, RunnerError> {
let mut map = match params {
Value::Object(map) => map,
Value::Null => Map::new(),
other => {
return Err(RunnerError::InvalidParams {
reason: format!("params must be a JSON object or null, got {other}"),
});
}
};
for decl in &flow.params {
if map.contains_key(decl.name.as_str()) {
continue;
}
if let Some(default) = &decl.default {
map.insert(decl.name.as_str().to_owned(), default.clone());
} else if decl.required {
return Err(RunnerError::InvalidParams {
reason: format!(
"required param '{}' is missing and has no default",
decl.name
),
});
}
}
Ok(map)
}
pub(crate) struct VerifyNeeds {
pub ui_tree: bool,
pub vision: bool,
}
impl VerifyNeeds {
pub fn of(assertions: &[AssertionIR]) -> Self {
let mut needs = VerifyNeeds {
ui_tree: false,
vision: false,
};
for assertion in assertions {
if matches!(assertion.predicate, PredicateIR::Expr { .. }) {
continue;
}
for channel in &assertion.verify_via {
match channel {
VerifyChannel::UiTree => needs.ui_tree = true,
VerifyChannel::Vision => needs.vision = true,
VerifyChannel::Dom => {}
}
}
}
needs
}
}
pub(crate) fn cap_wire_summary(verdict: &Verdict) -> String {
if verdict.summary.chars().count() <= VERDICT_SUMMARY_MAX_CHARS {
return verdict.summary.clone();
}
let pointer = format!(
" …[truncated; full local verdict {}]",
pointlock_ir::domain_hash(
"pointlock-runner/1/local-verdict",
&serde_json::to_value(verdict).expect("a Verdict always serializes"),
)
);
let keep = VERDICT_SUMMARY_MAX_CHARS.saturating_sub(pointer.chars().count());
let mut capped: String = verdict.summary.chars().take(keep).collect();
capped.push_str(&pointer);
capped
}
fn backoff_ms(policy: &pointlock_ir::RetryPolicy, tries: u32) -> u64 {
match &policy.backoff_ms {
pointlock_ir::BackoffMs::Fixed(number) => number.as_f64().unwrap_or(0.0) as u64,
pointlock_ir::BackoffMs::Schedule(schedule) => {
let initial = schedule.initial.as_f64().unwrap_or(0.0);
let factor = schedule.factor.as_f64().unwrap_or(1.0);
let max = schedule.max.as_f64().unwrap_or(f64::MAX);
let exponent = tries.saturating_sub(1);
(initial * factor.powi(exponent as i32)).min(max) as u64
}
}
}
fn retry_allowed(step: &ActionStepIR, class: ErrorClass, tries: u32) -> bool {
let Some(policy) = &step.base.retry else {
return false;
};
if tries >= policy.max_attempts {
return false;
}
if !policy.retry_on.contains(&class) {
return false;
}
match class {
ErrorClass::ActionFailedRetryable | ErrorClass::TargetStale => true,
ErrorClass::ActionTimedOut => step.idempotent,
_ => false,
}
}
pub(crate) fn classify(outcome: &ActionOutcome) -> ErrorClass {
match outcome {
ActionOutcome::Succeeded { .. } => {
unreachable!("classify is only called on non-succeeded terminals")
}
ActionOutcome::Failed { error } => code_spelled_class(&error.code).unwrap_or({
if error.retryable {
ErrorClass::ActionFailedRetryable
} else {
ErrorClass::ActionFailedFinal
}
}),
ActionOutcome::TimedOut { .. } => ErrorClass::ActionTimedOut,
ActionOutcome::Cancelled { .. } => ErrorClass::ActionCancelled,
}
}
fn code_spelled_class(code: &str) -> Option<ErrorClass> {
serde_json::from_value(Value::String(code.to_owned())).ok()
}
fn terminal_message(outcome: &ActionOutcome) -> String {
let error: &ErrorInfo = match outcome {
ActionOutcome::Failed { error }
| ActionOutcome::Cancelled { error }
| ActionOutcome::TimedOut { error } => error,
ActionOutcome::Succeeded { .. } => {
unreachable!("terminal_message is only called on non-succeeded terminals")
}
};
format!("{} ({})", error.message, error.code)
}
fn execution_accepted(attempt: &BoundAttempt, execution: &Option<ActionExecution>) -> bool {
match execution {
None => true,
Some(execution) => {
let mode = match execution {
ActionExecution::NativeSemantic { .. } => ExecutionMode::NativeSemantic,
ActionExecution::WebSemantic { .. } => ExecutionMode::WebSemantic,
ActionExecution::CoordinateFallback { .. } => ExecutionMode::CoordinateFallback,
};
attempt.accept_execution_modes.contains(&mode)
}
}
}
pub(crate) fn gated_mutating(step: &ActionStepIR) -> bool {
step.effect == EffectClassAction::Mutating && !step.idempotent
}
pub(crate) fn replay_permitted(step: &ActionStepIR) -> bool {
step.effect == EffectClassAction::Readonly || step.idempotent
}
#[cfg(test)]
mod chain_start_tests {
use super::*;
fn two_attempt_step() -> ActionStepIR {
serde_json::from_value(serde_json::json!({
"kind": "action",
"stepId": "s1",
"effectHash": format!("sha256:{}", "0".repeat(64)),
"judgeHash": format!("sha256:{}", "0".repeat(64)),
"checkpoint": true,
"effect": "mutating",
"idempotent": true,
"binding": { "attempts": [ {
"channel": "uiTree",
"actionName": "tapElement",
"args": {},
"acceptExecutionModes": ["nativeSemantic"],
"protection": "standard"
}, {
"channel": "uiTree",
"actionName": "setElementValue",
"args": {},
"acceptExecutionModes": ["nativeSemantic"],
"protection": "standard"
} ] },
"assertions": []
}))
.expect("fixture step")
}
#[test]
fn maps_recorded_positions_and_refuses_out_of_range() {
let step = two_attempt_step();
assert_eq!(chain_start(None, &step).expect("head"), 0);
assert_eq!(chain_start(Some(1), &step).expect("first"), 0);
assert_eq!(chain_start(Some(2), &step).expect("second"), 1);
assert!(chain_start(Some(3), &step).is_err());
assert!(chain_start(Some(0), &step).is_err());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_summary_truncation_appends_the_local_verdict_pointer() {
let verdict = Verdict {
status: VerdictStatus::Fail,
degraded: false,
summary: "x".repeat(VERDICT_SUMMARY_MAX_CHARS + 100),
evidence: Vec::new(),
supersedes: None,
};
let capped = cap_wire_summary(&verdict);
assert_eq!(capped.chars().count(), VERDICT_SUMMARY_MAX_CHARS);
assert!(
capped.ends_with(']') && capped.contains("full local verdict sha256:"),
"pointer missing: …{}",
&capped[capped.len().saturating_sub(90)..]
);
assert!(capped.starts_with("xxx"));
let short = Verdict {
summary: "all assertions passed".to_owned(),
..verdict
};
assert_eq!(cap_wire_summary(&short), "all assertions passed");
}
}