use serde::{Deserialize, Serialize};
use super::driver::PlannedStep;
use super::effect::{
ArchivePageOutEffect, CallProviderEffect, EffectKind, EffectKindTag, EvaluateMilestoneEffect,
ExecuteToolsEffect, KernelEffect, LoadPayloadEffect, MeasurePromptEffect, PersistMemoryEffect,
PreemptTasksEffect, QueryMemoryEffect, RequestApprovalEffect, SpawnTasksEffect,
};
use super::scalar::EffectId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PublishedEffectRef {
pub effect_id: EffectId,
pub kind: EffectKindTag,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CanonicalHostAction {
CallProvider {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: CallProviderEffect,
},
ExecuteTools {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: ExecuteToolsEffect,
},
RequestApproval {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: RequestApprovalEffect,
},
SpawnTasks {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: SpawnTasksEffect,
},
PreemptTasks {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: PreemptTasksEffect,
},
PersistMemory {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: PersistMemoryEffect,
},
QueryMemory {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: QueryMemoryEffect,
},
ArchivePageOut {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: ArchivePageOutEffect,
},
LoadPayload {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: LoadPayloadEffect,
},
EvaluateMilestone {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: EvaluateMilestoneEffect,
},
MeasurePrompt {
effect_id: EffectId,
causation_input_id: super::scalar::InputId,
payload: MeasurePromptEffect,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "state", content = "action", rename_all = "snake_case")]
pub enum KernelProjection {
Idle,
Action(CanonicalHostAction),
Terminal(super::terminal::KernelTerminal),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectionError {
pub message: String,
}
pub fn project_effect(effect: &KernelEffect) -> CanonicalHostAction {
let effect_id = effect.effect_id.clone();
let causation_input_id = effect.causation_input_id.clone();
match &effect.effect {
EffectKind::CallProvider(payload) => CanonicalHostAction::CallProvider {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::ExecuteTools(payload) => CanonicalHostAction::ExecuteTools {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::RequestApproval(payload) => CanonicalHostAction::RequestApproval {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::SpawnTasks(payload) => CanonicalHostAction::SpawnTasks {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::PreemptTasks(payload) => CanonicalHostAction::PreemptTasks {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::PersistMemory(payload) => CanonicalHostAction::PersistMemory {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::QueryMemory(payload) => CanonicalHostAction::QueryMemory {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::ArchivePageOut(payload) => CanonicalHostAction::ArchivePageOut {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::LoadPayload(payload) => CanonicalHostAction::LoadPayload {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::EvaluateMilestone(payload) => CanonicalHostAction::EvaluateMilestone {
effect_id,
causation_input_id,
payload: payload.clone(),
},
EffectKind::MeasurePrompt(payload) => CanonicalHostAction::MeasurePrompt {
effect_id,
causation_input_id,
payload: payload.clone(),
},
}
}
pub fn project_action(step: &PlannedStep) -> Result<KernelProjection, ProjectionError> {
project_pending_action(step.disposition.terminal(), step.disposition.effects())
}
pub fn current_effect(step: &PlannedStep) -> Option<&KernelEffect> {
step.disposition.effects().first()
}
pub fn project_planned_step_json(raw: &str) -> Result<String, ProjectionError> {
let step: PlannedStep = serde_json::from_str(raw).map_err(|error| ProjectionError {
message: format!("invalid planned step: {error}"),
})?;
let projection = project_action(&step)?;
serde_json::to_string(&projection).map_err(|error| ProjectionError {
message: format!("projection serialization failed: {error}"),
})
}
pub fn published_effects_manifest_json(raw: &str) -> Result<String, ProjectionError> {
let step: PlannedStep = serde_json::from_str(raw).map_err(|error| ProjectionError {
message: format!("invalid planned step: {error}"),
})?;
serde_json::to_string(&published_effects_manifest(&step)).map_err(|error| ProjectionError {
message: format!("manifest serialization failed: {error}"),
})
}
pub fn project_pending_action<'a, I>(
terminal: Option<&super::terminal::KernelTerminal>,
effects: I,
) -> Result<KernelProjection, ProjectionError>
where
I: IntoIterator<Item = &'a KernelEffect>,
{
if let Some(terminal) = terminal {
return Ok(KernelProjection::Terminal(terminal.clone()));
}
match effects.into_iter().next() {
Some(effect) => Ok(KernelProjection::Action(project_effect(effect))),
None => Ok(KernelProjection::Idle),
}
}
pub fn published_effects_manifest(step: &PlannedStep) -> Vec<PublishedEffectRef> {
step.disposition.effects().iter().map(effect_ref).collect()
}
fn effect_ref(effect: &KernelEffect) -> PublishedEffectRef {
PublishedEffectRef {
effect_id: effect.effect_id.clone(),
kind: effect.tag(),
}
}
#[cfg(test)]
mod tests {
use super::{
CanonicalHostAction, KernelProjection, project_action, project_effect,
project_pending_action, published_effects_manifest,
};
use crate::runtime::kernel::wire::{EffectKindTag, PlannedStep};
#[test]
fn manifest_preserves_multi_effect_publication_order() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let step: PlannedStep =
serde_json::from_value(fixture["planned_step"].clone()).expect("planned step");
let manifest = published_effects_manifest(&step);
assert_eq!(manifest.len(), 2);
assert_eq!(
manifest[0].effect_id.as_str(),
"op-contract:step:9:effect:0"
);
assert_eq!(manifest[0].kind, EffectKindTag::QueryMemory);
assert_eq!(
manifest[1].effect_id.as_str(),
"op-contract:step:9:effect:1"
);
assert_eq!(manifest[1].kind, EffectKindTag::ExecuteTools);
}
#[test]
fn terminal_step_has_empty_manifest() {
let step = PlannedStep {
root_kind: None,
focus: None,
observations: Vec::new(),
disposition: crate::runtime::kernel::wire::StepDisposition::Terminal(
crate::runtime::kernel::wire::TerminalDisposition {
terminal: crate::runtime::kernel::wire::KernelTerminal::Cancelled(
crate::runtime::kernel::wire::CancelledTerminal {
reason: crate::runtime::kernel::wire::CancellationReason::HostShutdown,
usage: crate::runtime::kernel::wire::UsageReport {
input_tokens: crate::runtime::kernel::wire::WireU64::ZERO,
output_tokens: crate::runtime::kernel::wire::WireU64::ZERO,
turns: 0,
cached_input_tokens: None,
},
},
),
},
),
};
assert!(published_effects_manifest(&step).is_empty());
}
#[test]
fn projects_the_first_query_memory_effect_to_a_typed_action() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let step: PlannedStep =
serde_json::from_value(fixture["planned_step"].clone()).expect("planned step");
let effect = step.disposition.effects().first().expect("first effect");
let action = project_effect(effect);
match action {
CanonicalHostAction::QueryMemory {
effect_id, payload, ..
} => {
assert_eq!(effect_id.as_str(), "op-contract:step:9:effect:0");
assert_eq!(payload.query.text, "past briefs");
assert_eq!(payload.requested_k, 4);
}
other => panic!("expected query_memory action, got {other:?}"),
}
}
#[test]
fn canonical_action_serialization_keeps_wire_kind_and_identity() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let step: PlannedStep =
serde_json::from_value(fixture["planned_step"].clone()).expect("planned step");
let action = project_effect(step.disposition.effects().first().expect("effect"));
let value = serde_json::to_value(action).expect("action JSON");
assert_eq!(value["kind"], "query_memory");
assert_eq!(value["effect_id"], "op-contract:step:9:effect:0");
assert_eq!(value["causation_input_id"], "in-9");
assert_eq!(value["payload"]["requested_k"], 4);
assert_eq!(value["payload"]["query"]["text"], "past briefs");
}
#[test]
fn projection_serialization_has_explicit_state_tag() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let step: PlannedStep =
serde_json::from_value(fixture["planned_step"].clone()).expect("planned step");
let value = serde_json::to_value(project_action(&step).expect("projection"))
.expect("projection JSON");
assert_eq!(value["state"], "action");
assert_eq!(value["action"]["kind"], "query_memory");
assert_eq!(value["action"]["effect_id"], "op-contract:step:9:effect:0");
}
#[test]
fn planned_step_json_bridge_matches_projection() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let output = super::project_planned_step_json(
&serde_json::to_string(&fixture["planned_step"]).expect("planned step JSON"),
)
.expect("projection JSON");
let value: serde_json::Value = serde_json::from_str(&output).expect("projection");
assert_eq!(value["state"], "action");
assert_eq!(value["action"]["kind"], "query_memory");
}
#[test]
fn projection_selects_first_effect_and_distinguishes_idle() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../../tests/fixtures/abi/multi_effect_step.json"
))
.expect("fixture JSON");
let step: PlannedStep =
serde_json::from_value(fixture["planned_step"].clone()).expect("planned step");
let projection = project_action(&step).expect("projection");
assert!(matches!(
projection,
KernelProjection::Action(CanonicalHostAction::QueryMemory { .. })
));
let idle = PlannedStep {
root_kind: None,
focus: None,
observations: Vec::new(),
disposition: crate::runtime::kernel::wire::StepDisposition::Effects(Default::default()),
};
assert!(matches!(
project_action(&idle).expect("projection"),
KernelProjection::Idle
));
let ordered = step.disposition.effects().iter();
assert!(matches!(
project_pending_action(None, ordered).expect("projection"),
KernelProjection::Action(CanonicalHostAction::QueryMemory { .. })
));
}
}