1use std::fmt;
24
25use serde::de::{self, Deserializer, Visitor};
26use serde::{Deserialize, Serialize, Serializer};
27
28use crate::context::measurement::PromptMeasurement;
29use crate::types::durable_content::DurableContent;
30
31use super::root::{LogicalAgentSpec, MessageRole};
32use super::scalar::{
33 AttemptId, BoundedJson, CallId, EffectId, FiniteF64, HandleId, InputId, MAX_ID_BYTES,
34 MemoryBindingId, NodeId, SCALAR_ERROR_MARKER, TaskId, WireScalarError, WireU64,
35};
36use super::syscall::{MemoryKind, SyscallCausation};
37
38#[doc(hidden)]
43pub(crate) fn validate_opaque(label: &'static str, value: &str) -> Result<(), WireScalarError> {
44 if value.is_empty() {
45 return Err(WireScalarError::new(format!("{label} must not be empty")));
46 }
47 if value.len() > MAX_ID_BYTES {
48 return Err(WireScalarError::new(format!(
49 "{label} is {} bytes; the bound is {MAX_ID_BYTES}",
50 value.len()
51 )));
52 }
53 if value.chars().any(char::is_control) {
54 return Err(WireScalarError::new(format!(
55 "{label} must not contain control characters"
56 )));
57 }
58 Ok(())
59}
60
61macro_rules! wire_opaque_ref {
64 ($(#[$doc:meta])* $name:ident, $label:literal) => {
65 $(#[$doc])*
66 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
67 pub struct $name(String);
68
69 impl $name {
70 pub fn new(value: impl Into<String>) -> Result<Self, WireScalarError> {
71 let value = value.into();
72 $crate::runtime::kernel::wire::effect::validate_opaque($label, &value)?;
73 Ok(Self(value))
74 }
75
76 pub fn as_str(&self) -> &str {
77 &self.0
78 }
79
80 pub fn into_string(self) -> String {
81 self.0
82 }
83 }
84
85 impl fmt::Display for $name {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.write_str(&self.0)
88 }
89 }
90
91 impl Serialize for $name {
92 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93 serializer.serialize_str(&self.0)
94 }
95 }
96
97 impl<'de> Deserialize<'de> for $name {
98 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99 struct RefVisitor;
100
101 impl Visitor<'_> for RefVisitor {
102 type Value = $name;
103
104 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.write_str(concat!("a non-empty ", $label, " string"))
106 }
107
108 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
109 $name::new(value).map_err(|err| {
110 E::custom(format!("{SCALAR_ERROR_MARKER}: {}", err.message))
111 })
112 }
113
114 fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
115 Err(E::custom(format!(
116 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got {value}",
117 $label
118 )))
119 }
120
121 fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
122 Err(E::custom(format!(
123 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got {value}",
124 $label
125 )))
126 }
127
128 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
129 Err(E::custom(format!(
130 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got null",
131 $label
132 )))
133 }
134 }
135
136 deserializer.deserialize_any(RefVisitor)
137 }
138 }
139 };
140}
141
142pub(crate) use wire_opaque_ref;
143
144wire_opaque_ref!(
145 PayloadRef,
152 "payload ref"
153);
154wire_opaque_ref!(
155 Digest,
158 "digest"
159);
160wire_opaque_ref!(
161 LaunchToken,
165 "launch token"
166);
167wire_opaque_ref!(
168 MemoryRecordRef,
170 "memory record ref"
171);
172
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(deny_unknown_fields)]
183pub struct KernelEffect {
184 pub effect_id: EffectId,
185 pub causation_input_id: InputId,
186 pub effect: EffectKind,
187}
188
189impl KernelEffect {
190 pub fn tag(&self) -> EffectKindTag {
191 self.effect.tag()
192 }
193
194 pub fn accept_outcome(&self, outcome: &EffectOutcome) -> Result<(), EffectResolutionMismatch> {
202 match outcome {
203 EffectOutcome::Failed(_) => Ok(()),
204 EffectOutcome::Succeeded(success) => {
205 let expected = self.effect.tag().expected_success();
206 let received = success.result.tag();
207 if expected == received {
208 Ok(())
209 } else {
210 Err(EffectResolutionMismatch {
211 effect_id: self.effect_id.clone(),
212 expected,
213 received,
214 })
215 }
216 }
217 }
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct EffectResolutionMismatch {
224 pub effect_id: EffectId,
225 pub expected: EffectSuccessTag,
226 pub received: EffectSuccessTag,
227}
228
229impl fmt::Display for EffectResolutionMismatch {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(
232 f,
233 "effect {} expects a {} resolution, got {}",
234 self.effect_id,
235 self.expected.as_str(),
236 self.received.as_str()
237 )
238 }
239}
240
241impl std::error::Error for EffectResolutionMismatch {}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248#[serde(tag = "kind", rename_all = "snake_case")]
249pub enum EffectKind {
250 CallProvider(CallProviderEffect),
251 ExecuteTools(ExecuteToolsEffect),
252 RequestApproval(RequestApprovalEffect),
253 SpawnTasks(SpawnTasksEffect),
254 PreemptTasks(PreemptTasksEffect),
255 PersistMemory(PersistMemoryEffect),
256 QueryMemory(QueryMemoryEffect),
257 ArchivePageOut(ArchivePageOutEffect),
258 LoadPayload(LoadPayloadEffect),
261 EvaluateMilestone(EvaluateMilestoneEffect),
262 MeasurePrompt(MeasurePromptEffect),
264}
265
266impl EffectKind {
267 pub fn tag(&self) -> EffectKindTag {
268 match self {
269 Self::CallProvider(_) => EffectKindTag::CallProvider,
270 Self::ExecuteTools(_) => EffectKindTag::ExecuteTools,
271 Self::RequestApproval(_) => EffectKindTag::RequestApproval,
272 Self::SpawnTasks(_) => EffectKindTag::SpawnTasks,
273 Self::PreemptTasks(_) => EffectKindTag::PreemptTasks,
274 Self::PersistMemory(_) => EffectKindTag::PersistMemory,
275 Self::QueryMemory(_) => EffectKindTag::QueryMemory,
276 Self::ArchivePageOut(_) => EffectKindTag::ArchivePageOut,
277 Self::LoadPayload(_) => EffectKindTag::LoadPayload,
278 Self::EvaluateMilestone(_) => EffectKindTag::EvaluateMilestone,
279 Self::MeasurePrompt(_) => EffectKindTag::MeasurePrompt,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
291#[serde(rename_all = "snake_case")]
292pub enum EffectKindTag {
293 CallProvider,
294 ExecuteTools,
295 RequestApproval,
296 SpawnTasks,
297 PreemptTasks,
298 PersistMemory,
299 QueryMemory,
300 ArchivePageOut,
301 LoadPayload,
302 EvaluateMilestone,
303 MeasurePrompt,
305}
306
307impl EffectKindTag {
308 pub const ALL: [Self; 11] = [
309 Self::CallProvider,
310 Self::ExecuteTools,
311 Self::RequestApproval,
312 Self::SpawnTasks,
313 Self::PreemptTasks,
314 Self::PersistMemory,
315 Self::QueryMemory,
316 Self::ArchivePageOut,
317 Self::LoadPayload,
318 Self::EvaluateMilestone,
319 Self::MeasurePrompt,
320 ];
321
322 pub fn as_str(self) -> &'static str {
323 match self {
324 Self::CallProvider => "call_provider",
325 Self::ExecuteTools => "execute_tools",
326 Self::RequestApproval => "request_approval",
327 Self::SpawnTasks => "spawn_tasks",
328 Self::PreemptTasks => "preempt_tasks",
329 Self::PersistMemory => "persist_memory",
330 Self::QueryMemory => "query_memory",
331 Self::ArchivePageOut => "archive_page_out",
332 Self::LoadPayload => "load_payload",
333 Self::EvaluateMilestone => "evaluate_milestone",
334 Self::MeasurePrompt => "measure_prompt",
335 }
336 }
337
338 pub fn expected_success(self) -> EffectSuccessTag {
340 match self {
341 Self::CallProvider => EffectSuccessTag::Provider,
342 Self::ExecuteTools => EffectSuccessTag::Tools,
343 Self::RequestApproval => EffectSuccessTag::Approval,
344 Self::SpawnTasks => EffectSuccessTag::TasksSpawned,
345 Self::PreemptTasks => EffectSuccessTag::TasksPreempted,
346 Self::PersistMemory => EffectSuccessTag::MemoryPersisted,
347 Self::QueryMemory => EffectSuccessTag::MemoryQueried,
348 Self::ArchivePageOut => EffectSuccessTag::PageOutArchived,
349 Self::LoadPayload => EffectSuccessTag::PayloadLoaded,
350 Self::EvaluateMilestone => EffectSuccessTag::MilestoneEvaluated,
351 Self::MeasurePrompt => EffectSuccessTag::PromptMeasured,
352 }
353 }
354}
355
356impl fmt::Display for EffectKindTag {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 f.write_str(self.as_str())
359 }
360}
361
362#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
367#[serde(deny_unknown_fields)]
368pub struct CallProviderEffect {
369 pub context: RenderedContext,
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub tools: Vec<ToolSchema>,
372}
373
374#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
388#[serde(deny_unknown_fields)]
389pub struct MeasurePromptEffect {
390 pub context: RenderedContext,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 pub tools: Vec<ToolSchema>,
393}
394
395#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
396#[serde(deny_unknown_fields)]
397pub struct ExecuteToolsEffect {
398 pub calls: Vec<ToolCall>,
399}
400
401#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
402#[serde(deny_unknown_fields)]
403pub struct RequestApprovalEffect {
404 pub requests: Vec<ApprovalRequest>,
405}
406
407#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
408#[serde(deny_unknown_fields)]
409pub struct SpawnTasksEffect {
410 pub tasks: Vec<TaskLaunch>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub budget: Option<WorkflowBudget>,
413}
414
415#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
416#[serde(deny_unknown_fields)]
417pub struct PreemptTasksEffect {
418 pub attempts: Vec<TaskAttemptRef>,
419 pub reason: String,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423#[serde(deny_unknown_fields)]
424pub struct PersistMemoryEffect {
425 pub binding: MemoryAccessBinding,
426 pub memory: CanonicalMemoryWrite,
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
430#[serde(deny_unknown_fields)]
431pub struct QueryMemoryEffect {
432 pub binding: MemoryAccessBinding,
433 pub query: CanonicalMemoryQuery,
434 pub requested_k: u32,
436}
437
438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
439#[serde(deny_unknown_fields)]
440pub struct ArchivePageOutEffect {
441 pub handle_id: HandleId,
442 pub payload: PageOutPayload,
443}
444
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446#[serde(deny_unknown_fields)]
447pub struct LoadPayloadEffect {
448 pub handle_id: HandleId,
449 pub payload_ref: PayloadRef,
450}
451
452#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
453#[serde(deny_unknown_fields)]
454pub struct EvaluateMilestoneEffect {
455 pub request: MilestoneRequest,
456}
457
458#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
464#[serde(deny_unknown_fields)]
465pub struct RenderedContext {
466 #[serde(default, skip_serializing_if = "String::is_empty")]
467 pub system_stable: String,
468 #[serde(default, skip_serializing_if = "String::is_empty")]
469 pub system_knowledge: String,
470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
471 pub turns: Vec<ProviderMessage>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub state_turn: Option<ProviderMessage>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub frozen_prefix_len: Option<u32>,
478}
479
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484#[serde(deny_unknown_fields)]
485pub struct ProviderMessage {
486 pub role: MessageRole,
487 pub content: String,
488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
489 pub tool_calls: Vec<ToolCall>,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub tool_call_id: Option<CallId>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub tokens: Option<u32>,
494}
495
496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct ToolSchema {
499 pub name: String,
500 #[serde(default, skip_serializing_if = "String::is_empty")]
501 pub description: String,
502 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
503 pub parameters: BoundedJson,
504}
505
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
510#[serde(deny_unknown_fields)]
511pub struct ToolCall {
512 pub call_id: CallId,
513 pub name: String,
514 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
515 pub arguments: BoundedJson,
516}
517
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
519#[serde(deny_unknown_fields)]
520pub struct ApprovalRequest {
521 pub call_id: CallId,
522 pub tool_name: String,
523 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
524 pub arguments: BoundedJson,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub reason: Option<String>,
527}
528
529#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535#[serde(deny_unknown_fields)]
536pub struct TaskLaunch {
537 pub task_id: TaskId,
538 pub attempt_id: AttemptId,
539 pub launch_token: LaunchToken,
540 pub node_id: NodeId,
541 pub spec: LogicalAgentSpec,
542}
543
544#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
545#[serde(deny_unknown_fields)]
546pub struct WorkflowBudget {
547 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub max_total_tokens: Option<WireU64>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub max_turns: Option<u32>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub max_concurrency: Option<u32>,
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
556#[serde(deny_unknown_fields)]
557pub struct TaskAttemptRef {
558 pub task_id: TaskId,
559 pub attempt_id: AttemptId,
560}
561
562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
565#[serde(deny_unknown_fields)]
566pub struct MemoryAccessBinding {
567 pub binding_id: MemoryBindingId,
568 pub capabilities: MemoryCapabilities,
569}
570
571#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
572#[serde(deny_unknown_fields)]
573pub struct MemoryCapabilities {
574 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
575 pub read: bool,
576 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
577 pub write: bool,
578}
579
580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587#[serde(deny_unknown_fields)]
588pub struct CanonicalMemoryWrite {
589 pub name: String,
590 pub kind: MemoryKind,
591 pub content: String,
592 #[serde(default, skip_serializing_if = "String::is_empty")]
593 pub description: String,
594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
595 pub evidence_refs: Vec<String>,
596 pub accepted_at_ms: WireU64,
597 pub causation: SyscallCausation,
598}
599
600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct CanonicalMemoryQuery {
603 #[serde(default, skip_serializing_if = "String::is_empty")]
604 pub text: String,
605 #[serde(default, skip_serializing_if = "Vec::is_empty")]
606 pub kinds: Vec<MemoryKind>,
607 pub accepted_at_ms: WireU64,
608 pub causation: SyscallCausation,
609}
610
611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
614#[serde(deny_unknown_fields)]
615pub struct PageOutPayload {
616 pub content: String,
617 pub digest: Digest,
618 pub original_size: WireU64,
619 #[serde(default, skip_serializing_if = "String::is_empty")]
620 pub preview: String,
621}
622
623#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
631#[serde(deny_unknown_fields)]
632pub struct MilestoneRequest {
633 pub contract_id: String,
634 pub phase_id: String,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
647#[serde(tag = "status", rename_all = "snake_case")]
648pub enum EffectOutcome {
649 Succeeded(EffectSucceeded),
650 Failed(EffectFailed),
651}
652
653impl EffectOutcome {
654 pub fn success_tag(&self) -> Option<EffectSuccessTag> {
656 match self {
657 Self::Succeeded(success) => Some(success.result.tag()),
658 Self::Failed(_) => None,
659 }
660 }
661}
662
663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
666#[serde(deny_unknown_fields)]
667pub struct EffectSucceeded {
668 pub result: EffectSuccess,
669}
670
671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
673#[serde(deny_unknown_fields)]
674pub struct EffectFailed {
675 pub failure: HostEffectFailure,
676}
677
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682#[serde(deny_unknown_fields)]
683pub struct HostEffectFailure {
684 pub kind: HostEffectFailureKind,
685 #[serde(default, skip_serializing_if = "String::is_empty")]
686 pub message: String,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub retryable: Option<bool>,
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
724#[serde(rename_all = "snake_case")]
725pub enum HostEffectFailureKind {
726 TransportExhausted,
729 ProtocolError,
734 StorageUnavailable,
735 PermissionDenied,
736 ResourceExhausted,
737 Unknown,
738}
739
740impl HostEffectFailureKind {
741 pub const ALL: [Self; 6] = [
742 Self::TransportExhausted,
743 Self::ProtocolError,
744 Self::StorageUnavailable,
745 Self::PermissionDenied,
746 Self::ResourceExhausted,
747 Self::Unknown,
748 ];
749
750 pub fn as_str(self) -> &'static str {
751 match self {
752 Self::TransportExhausted => "transport_exhausted",
753 Self::ProtocolError => "protocol_error",
754 Self::StorageUnavailable => "storage_unavailable",
755 Self::PermissionDenied => "permission_denied",
756 Self::ResourceExhausted => "resource_exhausted",
757 Self::Unknown => "unknown",
758 }
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
764#[serde(tag = "kind", rename_all = "snake_case")]
765pub enum EffectSuccess {
766 Provider(ProviderSuccess),
767 Tools(ToolsSuccess),
768 Approval(ApprovalSuccess),
769 TasksSpawned(TasksSpawnedSuccess),
770 TasksPreempted(TasksPreemptedSuccess),
771 MemoryPersisted(MemoryPersistedSuccess),
772 MemoryQueried(MemoryQueriedSuccess),
773 PageOutArchived(PageOutArchivedSuccess),
774 PayloadLoaded(PayloadLoadedSuccess),
775 MilestoneEvaluated(MilestoneEvaluatedSuccess),
776 PromptMeasured(PromptMeasuredSuccess),
778}
779
780impl EffectSuccess {
781 pub fn tag(&self) -> EffectSuccessTag {
782 match self {
783 Self::Provider(_) => EffectSuccessTag::Provider,
784 Self::Tools(_) => EffectSuccessTag::Tools,
785 Self::Approval(_) => EffectSuccessTag::Approval,
786 Self::TasksSpawned(_) => EffectSuccessTag::TasksSpawned,
787 Self::TasksPreempted(_) => EffectSuccessTag::TasksPreempted,
788 Self::MemoryPersisted(_) => EffectSuccessTag::MemoryPersisted,
789 Self::MemoryQueried(_) => EffectSuccessTag::MemoryQueried,
790 Self::PageOutArchived(_) => EffectSuccessTag::PageOutArchived,
791 Self::PayloadLoaded(_) => EffectSuccessTag::PayloadLoaded,
792 Self::MilestoneEvaluated(_) => EffectSuccessTag::MilestoneEvaluated,
793 Self::PromptMeasured(_) => EffectSuccessTag::PromptMeasured,
794 }
795 }
796}
797
798#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
800#[serde(rename_all = "snake_case")]
801pub enum EffectSuccessTag {
802 Provider,
803 Tools,
804 Approval,
805 TasksSpawned,
806 TasksPreempted,
807 MemoryPersisted,
808 MemoryQueried,
809 PageOutArchived,
810 PayloadLoaded,
811 MilestoneEvaluated,
812 PromptMeasured,
814}
815
816impl EffectSuccessTag {
817 pub const ALL: [Self; 11] = [
818 Self::Provider,
819 Self::Tools,
820 Self::Approval,
821 Self::TasksSpawned,
822 Self::TasksPreempted,
823 Self::MemoryPersisted,
824 Self::MemoryQueried,
825 Self::PageOutArchived,
826 Self::PayloadLoaded,
827 Self::MilestoneEvaluated,
828 Self::PromptMeasured,
829 ];
830
831 pub fn as_str(self) -> &'static str {
832 match self {
833 Self::Provider => "provider",
834 Self::Tools => "tools",
835 Self::Approval => "approval",
836 Self::TasksSpawned => "tasks_spawned",
837 Self::TasksPreempted => "tasks_preempted",
838 Self::MemoryPersisted => "memory_persisted",
839 Self::MemoryQueried => "memory_queried",
840 Self::PageOutArchived => "page_out_archived",
841 Self::PayloadLoaded => "payload_loaded",
842 Self::MilestoneEvaluated => "milestone_evaluated",
843 Self::PromptMeasured => "prompt_measured",
844 }
845 }
846
847 pub fn resolves(self) -> EffectKindTag {
850 match self {
851 Self::Provider => EffectKindTag::CallProvider,
852 Self::Tools => EffectKindTag::ExecuteTools,
853 Self::Approval => EffectKindTag::RequestApproval,
854 Self::TasksSpawned => EffectKindTag::SpawnTasks,
855 Self::TasksPreempted => EffectKindTag::PreemptTasks,
856 Self::MemoryPersisted => EffectKindTag::PersistMemory,
857 Self::MemoryQueried => EffectKindTag::QueryMemory,
858 Self::PageOutArchived => EffectKindTag::ArchivePageOut,
859 Self::PayloadLoaded => EffectKindTag::LoadPayload,
860 Self::MilestoneEvaluated => EffectKindTag::EvaluateMilestone,
861 Self::PromptMeasured => EffectKindTag::MeasurePrompt,
862 }
863 }
864}
865
866impl fmt::Display for EffectSuccessTag {
867 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868 f.write_str(self.as_str())
869 }
870}
871
872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
877#[serde(deny_unknown_fields)]
878pub struct ProviderSuccess {
879 pub outcome: ProviderOutcome,
880}
881
882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887#[serde(tag = "kind", rename_all = "snake_case")]
888pub enum ProviderOutcome {
889 Completed(ProviderCompleted),
890 ContextOverflow(ProviderContextOverflow),
894}
895
896#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
897#[serde(deny_unknown_fields)]
898pub struct ProviderCompleted {
899 pub message: ProviderMessage,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub observed_input_tokens: Option<u32>,
902 #[serde(default, skip_serializing_if = "Option::is_none")]
903 pub observed_output_tokens: Option<u32>,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub stop_reason: Option<ProviderStopReason>,
906}
907
908#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
909#[serde(deny_unknown_fields)]
910pub struct ProviderContextOverflow {
911 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub observed_input_tokens: Option<u32>,
913}
914
915#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
924#[serde(rename_all = "snake_case")]
925pub enum ProviderStopReason {
926 EndTurn,
927 ToolUse,
928 MaxTokens,
929 StopSequence,
930 ContentFilter,
931 Other,
932}
933
934impl ProviderStopReason {
935 pub const ALL: [Self; 6] = [
936 Self::EndTurn,
937 Self::ToolUse,
938 Self::MaxTokens,
939 Self::StopSequence,
940 Self::ContentFilter,
941 Self::Other,
942 ];
943
944 pub fn as_str(self) -> &'static str {
945 match self {
946 Self::EndTurn => "end_turn",
947 Self::ToolUse => "tool_use",
948 Self::MaxTokens => "max_tokens",
949 Self::StopSequence => "stop_sequence",
950 Self::ContentFilter => "content_filter",
951 Self::Other => "other",
952 }
953 }
954}
955
956impl fmt::Display for ProviderStopReason {
957 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
958 f.write_str(self.as_str())
959 }
960}
961
962#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
963#[serde(deny_unknown_fields)]
964pub struct ToolsSuccess {
965 pub results: Vec<ToolResultPayload>,
966}
967
968#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
969#[serde(deny_unknown_fields)]
970pub struct ApprovalSuccess {
971 #[serde(default, skip_serializing_if = "Vec::is_empty")]
972 pub approved_call_ids: Vec<CallId>,
973 #[serde(default, skip_serializing_if = "Vec::is_empty")]
974 pub denied_call_ids: Vec<CallId>,
975}
976
977#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
978#[serde(deny_unknown_fields)]
979pub struct TasksSpawnedSuccess {
980 pub attempts: Vec<TaskLaunchOutcome>,
981}
982
983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987pub struct TaskLaunchOutcome {
988 pub task_id: TaskId,
989 pub attempt_id: AttemptId,
990 pub outcome: TaskLaunchStatus,
991}
992
993#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
994#[serde(tag = "status", rename_all = "snake_case")]
995pub enum TaskLaunchStatus {
996 Started(TaskLaunchStarted),
997 Failed(TaskLaunchFailed),
998}
999
1000#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1001#[serde(deny_unknown_fields)]
1002pub struct TaskLaunchStarted {}
1003
1004#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1005#[serde(deny_unknown_fields)]
1006pub struct TaskLaunchFailed {
1007 pub failure: TaskLaunchFailure,
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1013#[serde(deny_unknown_fields)]
1014pub struct TaskLaunchFailure {
1015 pub kind: HostEffectFailureKind,
1016 #[serde(default, skip_serializing_if = "String::is_empty")]
1017 pub message: String,
1018}
1019
1020#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1021#[serde(deny_unknown_fields)]
1022pub struct TasksPreemptedSuccess {
1023 pub attempts: Vec<TaskPreemptOutcome>,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1027#[serde(deny_unknown_fields)]
1028pub struct TaskPreemptOutcome {
1029 pub task_id: TaskId,
1030 pub attempt_id: AttemptId,
1031 pub outcome: TaskPreemptStatus,
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1035#[serde(tag = "status", rename_all = "snake_case")]
1036pub enum TaskPreemptStatus {
1037 Preempted(TaskPreempted),
1038 AlreadyFinished(TaskAlreadyFinished),
1041 Failed(TaskPreemptFailed),
1042}
1043
1044#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1045#[serde(deny_unknown_fields)]
1046pub struct TaskPreempted {}
1047
1048#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1049#[serde(deny_unknown_fields)]
1050pub struct TaskAlreadyFinished {}
1051
1052#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1053#[serde(deny_unknown_fields)]
1054pub struct TaskPreemptFailed {
1055 pub failure: TaskLaunchFailure,
1056}
1057
1058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1059#[serde(deny_unknown_fields)]
1060pub struct MemoryPersistedSuccess {
1061 pub receipt: MemoryPersistReceipt,
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1065#[serde(deny_unknown_fields)]
1066pub struct MemoryPersistReceipt {
1067 pub binding_id: MemoryBindingId,
1068 pub record_ref: MemoryRecordRef,
1069 pub digest: Digest,
1070}
1071
1072#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1073#[serde(deny_unknown_fields)]
1074pub struct MemoryQueriedSuccess {
1075 pub recalls: Vec<MemoryRecall>,
1076}
1077
1078#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1079#[serde(deny_unknown_fields)]
1080pub struct MemoryRecall {
1081 pub record_ref: MemoryRecordRef,
1082 pub name: String,
1083 pub kind: MemoryKind,
1084 pub content: String,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub score: Option<FiniteF64>,
1089}
1090
1091#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1092#[serde(deny_unknown_fields)]
1093pub struct PageOutArchivedSuccess {
1094 pub receipt: ArchiveReceipt,
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1098#[serde(deny_unknown_fields)]
1099pub struct ArchiveReceipt {
1100 pub handle_id: HandleId,
1101 pub payload_ref: PayloadRef,
1102 pub digest: Digest,
1103 pub original_size: WireU64,
1104}
1105
1106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1107#[serde(deny_unknown_fields)]
1108pub struct PayloadLoadedSuccess {
1109 pub handle_id: HandleId,
1110 pub payload: InlinePayload,
1111}
1112
1113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1116#[serde(deny_unknown_fields)]
1117pub struct InlinePayload {
1118 pub content: String,
1119 pub digest: Digest,
1120 pub original_size: WireU64,
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1124#[serde(deny_unknown_fields)]
1125pub struct MilestoneEvaluatedSuccess {
1126 pub result: MilestoneCheckResult,
1127}
1128
1129#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1136#[serde(deny_unknown_fields)]
1137pub struct MilestoneCheckResult {
1138 pub phase_id: String,
1139 pub passed: bool,
1140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1141 pub failed_criteria: Vec<String>,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1143 pub score: Option<FiniteF64>,
1144 #[serde(default, skip_serializing_if = "String::is_empty")]
1145 pub notes: String,
1146}
1147
1148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1151#[serde(deny_unknown_fields)]
1152pub struct PromptMeasuredSuccess {
1153 pub measurement: PromptMeasurement,
1154}
1155
1156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1168#[serde(tag = "kind", rename_all = "snake_case")]
1169pub enum ToolResultPayload {
1170 Inline(InlineToolResult),
1171 External(ExternalToolResult),
1172}
1173
1174impl ToolResultPayload {
1175 pub fn call_id(&self) -> &CallId {
1176 match self {
1177 Self::Inline(inline) => &inline.call_id,
1178 Self::External(external) => &external.call_id,
1179 }
1180 }
1181
1182 pub fn disposition(&self) -> ToolResultDisposition {
1187 match self {
1188 Self::Inline(inline) => inline.result.disposition,
1189 Self::External(external) => external.disposition,
1190 }
1191 }
1192
1193 pub fn is_error(&self) -> bool {
1194 match self {
1195 Self::Inline(inline) => inline.result.is_error,
1196 Self::External(external) => external.is_error,
1197 }
1198 }
1199}
1200
1201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1202#[serde(deny_unknown_fields)]
1203pub struct InlineToolResult {
1204 pub call_id: CallId,
1205 pub result: ToolResult,
1206}
1207
1208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1216#[serde(deny_unknown_fields)]
1217pub struct ExternalToolResult {
1218 pub call_id: CallId,
1219 pub payload_ref: PayloadRef,
1221 pub digest: Digest,
1222 pub original_size: WireU64,
1223 pub preview: String,
1225 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1226 pub is_error: bool,
1227 pub disposition: ToolResultDisposition,
1229}
1230
1231#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1242#[serde(deny_unknown_fields)]
1243pub struct ToolResult {
1244 pub output: String,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub durable_content: Option<DurableContent>,
1247 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1248 pub is_error: bool,
1249 pub disposition: ToolResultDisposition,
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub tokens: Option<u32>,
1254}
1255
1256#[derive(
1268 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
1269)]
1270#[serde(rename_all = "snake_case")]
1271pub enum ToolResultDisposition {
1272 #[default]
1276 Recoverable,
1277 Fatal,
1281}
1282
1283impl ToolResultDisposition {
1284 pub const ALL: [Self; 2] = [Self::Recoverable, Self::Fatal];
1285
1286 pub fn as_str(self) -> &'static str {
1287 match self {
1288 Self::Recoverable => "recoverable",
1289 Self::Fatal => "fatal",
1290 }
1291 }
1292
1293 pub fn is_fatal(self) -> bool {
1294 matches!(self, Self::Fatal)
1295 }
1296}
1297
1298impl fmt::Display for ToolResultDisposition {
1299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1300 f.write_str(self.as_str())
1301 }
1302}
1303
1304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1311#[serde(tag = "kind", rename_all = "snake_case")]
1312pub enum PayloadResidency {
1313 Resident(ResidentPayload),
1314 External(ExternalResidency),
1315 PagedOut(PagedOutResidency),
1316 Collapsed(CollapsedPayload),
1318}
1319
1320#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1321#[serde(deny_unknown_fields)]
1322pub struct ResidentPayload {}
1323
1324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1325#[serde(deny_unknown_fields)]
1326pub struct ExternalResidency {
1327 pub payload_ref: PayloadRef,
1328 pub digest: Digest,
1329 pub original_size: WireU64,
1330}
1331
1332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1333#[serde(deny_unknown_fields)]
1334pub struct PagedOutResidency {
1335 pub payload_ref: PayloadRef,
1336 pub digest: Digest,
1337}
1338
1339#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1340#[serde(deny_unknown_fields)]
1341pub struct CollapsedPayload {}
1342
1343#[cfg(test)]
1344mod tests {
1345 use std::collections::BTreeSet;
1346 use std::fs;
1347 use std::path::PathBuf;
1348
1349 use serde_json::{Value, json};
1350
1351 use crate::context::measurement::{
1352 MeasurementConfidence, MeasurementSource, PromptMeasurement,
1353 };
1354
1355 use super::super::*;
1356
1357 fn fixture_dir() -> PathBuf {
1362 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
1363 }
1364
1365 fn fixtures_with_prefix(prefix: &str) -> Vec<(String, Value)> {
1366 let dir = fixture_dir();
1367 let mut names: Vec<String> = fs::read_dir(&dir)
1368 .unwrap_or_else(|e| panic!("failed to read {}: {e}", dir.display()))
1369 .map(|entry| {
1370 entry
1371 .expect("dir entry")
1372 .file_name()
1373 .to_string_lossy()
1374 .to_string()
1375 })
1376 .filter(|name| name.ends_with(".json") && name.starts_with(prefix))
1377 .collect();
1378 names.sort();
1379 assert!(!names.is_empty(), "no {prefix}*.json fixtures");
1380 names
1381 .into_iter()
1382 .map(|name| {
1383 let raw = fs::read_to_string(dir.join(&name)).unwrap();
1384 let value: Value = serde_json::from_str(&raw).unwrap();
1385 (name, value)
1386 })
1387 .collect()
1388 }
1389
1390 fn keys(value: &Value, out: &mut BTreeSet<String>) {
1391 match value {
1392 Value::Object(map) => {
1393 for (key, child) in map {
1394 out.insert(key.clone());
1395 keys(child, out);
1396 }
1397 }
1398 Value::Array(items) => items.iter().for_each(|item| keys(item, out)),
1399 _ => {}
1400 }
1401 }
1402
1403 fn effect_json(outcome: Value) -> String {
1404 serde_json::to_string(&json!({
1405 "operation_id": "op-1",
1406 "input_id": "in-1",
1407 "observed_at_ms": "1700000000000",
1408 "input": { "kind": "resolve_effect", "effect_id": "op-1:step:1:effect:0", "outcome": outcome },
1409 }))
1410 .unwrap()
1411 }
1412
1413 fn decode_effect(outcome: Value) -> Result<WireEnvelope, WireRejection> {
1414 decode_envelope_json(&effect_json(outcome), &KernelBootstrapLimits::default())
1415 }
1416
1417 fn call_id(id: &str) -> CallId {
1418 CallId::new(id).unwrap()
1419 }
1420
1421 fn digest() -> Digest {
1422 Digest::new("sha256:3b1f4a7c9e2d05186a4c7f0b9d3e8c25714f6a0b8c5d2e9f1a3b6c8d0e2f4a61")
1423 .unwrap()
1424 }
1425
1426 fn payload_ref() -> PayloadRef {
1427 PayloadRef::new("payload:01J8Y2QK7C4N0V").unwrap()
1428 }
1429
1430 fn binding() -> MemoryAccessBinding {
1431 MemoryAccessBinding {
1432 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1433 capabilities: MemoryCapabilities {
1434 read: true,
1435 write: true,
1436 },
1437 }
1438 }
1439
1440 fn causation() -> SyscallCausation {
1441 SyscallCausation::ProviderTool(ProviderToolCausation {
1442 provider_effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1443 call_id: call_id("call-1"),
1444 task_id: TaskId::new("task-1").unwrap(),
1445 })
1446 }
1447
1448 fn effect_samples() -> Vec<EffectKind> {
1450 vec![
1451 EffectKind::CallProvider(CallProviderEffect {
1452 context: RenderedContext::default(),
1453 tools: vec![ToolSchema {
1454 name: "read_file".to_string(),
1455 description: "read a file".to_string(),
1456 parameters: BoundedJson::null(),
1457 }],
1458 }),
1459 EffectKind::ExecuteTools(ExecuteToolsEffect {
1460 calls: vec![ToolCall {
1461 call_id: call_id("call-1"),
1462 name: "read_file".to_string(),
1463 arguments: BoundedJson::null(),
1464 }],
1465 }),
1466 EffectKind::RequestApproval(RequestApprovalEffect {
1467 requests: vec![ApprovalRequest {
1468 call_id: call_id("call-1"),
1469 tool_name: "rm".to_string(),
1470 arguments: BoundedJson::null(),
1471 reason: Some("destructive".to_string()),
1472 }],
1473 }),
1474 EffectKind::SpawnTasks(SpawnTasksEffect {
1475 tasks: vec![TaskLaunch {
1476 task_id: TaskId::new("task-1").unwrap(),
1477 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1478 launch_token: LaunchToken::new("launch-1").unwrap(),
1479 node_id: NodeId::new("node-a").unwrap(),
1480 spec: LogicalAgentSpec::new("research"),
1481 }],
1482 budget: None,
1483 }),
1484 EffectKind::PreemptTasks(PreemptTasksEffect {
1485 attempts: vec![TaskAttemptRef {
1486 task_id: TaskId::new("task-1").unwrap(),
1487 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1488 }],
1489 reason: "budget exhausted".to_string(),
1490 }),
1491 EffectKind::PersistMemory(PersistMemoryEffect {
1492 binding: binding(),
1493 memory: CanonicalMemoryWrite {
1494 name: "release pipeline".to_string(),
1495 kind: MemoryKind::Project,
1496 content: "tag v* publishes".to_string(),
1497 description: String::new(),
1498 evidence_refs: Vec::new(),
1499 accepted_at_ms: WireU64::new(1_700_000_000_000),
1500 causation: causation(),
1501 },
1502 }),
1503 EffectKind::QueryMemory(QueryMemoryEffect {
1504 binding: binding(),
1505 query: CanonicalMemoryQuery {
1506 text: "release".to_string(),
1507 kinds: vec![MemoryKind::Project],
1508 accepted_at_ms: WireU64::new(1_700_000_000_000),
1509 causation: causation(),
1510 },
1511 requested_k: 5,
1512 }),
1513 EffectKind::ArchivePageOut(ArchivePageOutEffect {
1514 handle_id: HandleId::new("handle-9").unwrap(),
1515 payload: PageOutPayload {
1516 content: "the full tool output".to_string(),
1517 digest: digest(),
1518 original_size: WireU64::new(262_144),
1519 preview: "the full…".to_string(),
1520 },
1521 }),
1522 EffectKind::LoadPayload(LoadPayloadEffect {
1523 handle_id: HandleId::new("handle-9").unwrap(),
1524 payload_ref: payload_ref(),
1525 }),
1526 EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
1527 request: MilestoneRequest {
1528 contract_id: "brief-quality-primary".to_string(),
1529 phase_id: "phase-2".to_string(),
1530 },
1531 }),
1532 EffectKind::MeasurePrompt(MeasurePromptEffect {
1533 context: RenderedContext::default(),
1534 tools: Vec::new(),
1535 }),
1536 ]
1537 }
1538
1539 fn success_samples() -> Vec<EffectSuccess> {
1541 vec![
1542 EffectSuccess::Provider(ProviderSuccess {
1543 outcome: ProviderOutcome::Completed(ProviderCompleted {
1544 message: ProviderMessage {
1545 role: MessageRole::Assistant,
1546 content: "done".to_string(),
1547 tool_calls: Vec::new(),
1548 tool_call_id: None,
1549 tokens: None,
1550 },
1551 observed_input_tokens: Some(120),
1552 observed_output_tokens: Some(8),
1553 stop_reason: Some(ProviderStopReason::EndTurn),
1554 }),
1555 }),
1556 EffectSuccess::Tools(ToolsSuccess {
1557 results: vec![
1558 ToolResultPayload::Inline(InlineToolResult {
1559 call_id: call_id("call-1"),
1560 result: ToolResult {
1561 output: "ok".to_string(),
1562 durable_content: None,
1563 is_error: false,
1564 disposition: ToolResultDisposition::Recoverable,
1565 tokens: Some(2),
1566 },
1567 }),
1568 ToolResultPayload::External(ExternalToolResult {
1569 call_id: call_id("call-2"),
1570 payload_ref: payload_ref(),
1571 digest: digest(),
1572 original_size: WireU64::new(1_048_576),
1573 preview: "total 42".to_string(),
1574 is_error: false,
1575 disposition: ToolResultDisposition::Recoverable,
1576 }),
1577 ],
1578 }),
1579 EffectSuccess::Approval(ApprovalSuccess {
1580 approved_call_ids: vec![call_id("call-1")],
1581 denied_call_ids: vec![call_id("call-2")],
1582 }),
1583 EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
1584 attempts: vec![TaskLaunchOutcome {
1585 task_id: TaskId::new("task-1").unwrap(),
1586 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1587 outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
1588 }],
1589 }),
1590 EffectSuccess::TasksPreempted(TasksPreemptedSuccess {
1591 attempts: vec![TaskPreemptOutcome {
1592 task_id: TaskId::new("task-1").unwrap(),
1593 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1594 outcome: TaskPreemptStatus::Preempted(TaskPreempted {}),
1595 }],
1596 }),
1597 EffectSuccess::MemoryPersisted(MemoryPersistedSuccess {
1598 receipt: MemoryPersistReceipt {
1599 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1600 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0W").unwrap(),
1601 digest: digest(),
1602 },
1603 }),
1604 EffectSuccess::MemoryQueried(MemoryQueriedSuccess {
1605 recalls: vec![MemoryRecall {
1606 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0X").unwrap(),
1607 name: "release pipeline".to_string(),
1608 kind: MemoryKind::Project,
1609 content: "tag v* publishes".to_string(),
1610 score: Some(FiniteF64::new(0.82).unwrap()),
1611 }],
1612 }),
1613 EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
1614 receipt: ArchiveReceipt {
1615 handle_id: HandleId::new("handle-9").unwrap(),
1616 payload_ref: payload_ref(),
1617 digest: digest(),
1618 original_size: WireU64::new(262_144),
1619 },
1620 }),
1621 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
1622 handle_id: HandleId::new("handle-9").unwrap(),
1623 payload: InlinePayload {
1624 content: "the full tool output".to_string(),
1625 digest: digest(),
1626 original_size: WireU64::new(262_144),
1627 },
1628 }),
1629 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
1630 result: MilestoneCheckResult {
1631 phase_id: "phase-2".to_string(),
1632 passed: true,
1633 failed_criteria: Vec::new(),
1634 score: None,
1635 notes: String::new(),
1636 },
1637 }),
1638 EffectSuccess::PromptMeasured(PromptMeasuredSuccess {
1639 measurement: PromptMeasurement {
1640 input_tokens: 4200,
1641 source: MeasurementSource::Native {
1642 provider: "anthropic".to_string(),
1643 },
1644 confidence: MeasurementConfidence::Exact,
1645 },
1646 }),
1647 ]
1648 }
1649
1650 fn kernel_effect(effect: EffectKind) -> KernelEffect {
1651 KernelEffect {
1652 effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1653 causation_input_id: InputId::new("in-1").unwrap(),
1654 effect,
1655 }
1656 }
1657
1658 #[test]
1663 fn the_effect_union_is_exactly_the_eleven_host_executable_actions() {
1664 let tags: BTreeSet<&str> = EffectKindTag::ALL.iter().map(|tag| tag.as_str()).collect();
1665 assert_eq!(
1666 tags,
1667 BTreeSet::from([
1668 "call_provider",
1669 "execute_tools",
1670 "request_approval",
1671 "spawn_tasks",
1672 "preempt_tasks",
1673 "persist_memory",
1674 "query_memory",
1675 "archive_page_out",
1676 "load_payload",
1677 "evaluate_milestone",
1678 "measure_prompt",
1679 ])
1680 );
1681 assert_eq!(EffectKindTag::ALL.len(), 11);
1682
1683 let sampled: Vec<EffectKindTag> = effect_samples().iter().map(EffectKind::tag).collect();
1684 assert_eq!(
1685 sampled,
1686 EffectKindTag::ALL.to_vec(),
1687 "one sample per variant"
1688 );
1689 }
1690
1691 #[test]
1692 fn terminal_and_observation_shapes_are_not_effects() {
1693 for gone in [
1695 "done",
1696 "terminal",
1697 "compact",
1698 "sync_compact",
1699 "knowledge_sweep",
1700 "workflow_completed",
1701 "control_rejection",
1702 "signal_disposition",
1703 "budget_usage",
1704 "spool_large_result",
1705 ] {
1706 assert!(
1707 !EffectKindTag::ALL.iter().any(|tag| tag.as_str() == gone),
1708 "{gone} is a terminal or an observation, not an effect"
1709 );
1710 let raw = json!({ "kind": gone });
1711 assert!(serde_json::from_value::<EffectKind>(raw).is_err());
1712 }
1713 }
1714
1715 #[test]
1716 fn every_effect_carries_its_kernel_minted_id_and_causation() {
1717 for effect in effect_samples() {
1718 let value = serde_json::to_value(kernel_effect(effect)).unwrap();
1719 assert_eq!(value["effect_id"], json!("op-1:step:1:effect:0"));
1720 assert_eq!(value["causation_input_id"], json!("in-1"));
1721 }
1722 }
1723
1724 #[test]
1729 fn each_effect_kind_has_exactly_one_matching_success_kind() {
1730 let expected: Vec<EffectSuccessTag> = EffectKindTag::ALL
1731 .iter()
1732 .map(|tag| tag.expected_success())
1733 .collect();
1734 let distinct: BTreeSet<&str> = expected.iter().map(|tag| tag.as_str()).collect();
1735 assert_eq!(
1736 distinct.len(),
1737 EffectKindTag::ALL.len(),
1738 "the effect→success map must be a bijection"
1739 );
1740 assert_eq!(
1741 distinct,
1742 EffectSuccessTag::ALL
1743 .iter()
1744 .map(|tag| tag.as_str())
1745 .collect::<BTreeSet<&str>>()
1746 );
1747
1748 let sampled: Vec<EffectSuccessTag> =
1749 success_samples().iter().map(EffectSuccess::tag).collect();
1750 assert_eq!(
1751 sampled, expected,
1752 "samples must line up 1:1 with the effects"
1753 );
1754 }
1755
1756 #[test]
1757 fn a_resolution_of_the_wrong_kind_is_rejected_for_every_wrong_pair() {
1758 let effects = effect_samples();
1759 let successes = success_samples();
1760 for (i, effect) in effects.iter().enumerate() {
1761 let pending = kernel_effect(effect.clone());
1762 for (j, success) in successes.iter().enumerate() {
1763 let outcome = EffectOutcome::Succeeded(EffectSucceeded {
1764 result: success.clone(),
1765 });
1766 let verdict = pending.accept_outcome(&outcome);
1767 if i == j {
1768 assert!(
1769 verdict.is_ok(),
1770 "{:?} must accept its own success payload",
1771 effect.tag()
1772 );
1773 } else {
1774 let mismatch = verdict.expect_err("kind mismatch must be refused");
1775 assert_eq!(mismatch.effect_id, pending.effect_id);
1776 assert_eq!(mismatch.expected, effect.tag().expected_success());
1777 assert_eq!(mismatch.received, success.tag());
1778 }
1779 }
1780 }
1781 }
1782
1783 #[test]
1784 fn every_effect_accepts_the_same_host_failure_including_milestone() {
1785 let kinds = [
1786 HostEffectFailureKind::TransportExhausted,
1787 HostEffectFailureKind::ProtocolError,
1788 HostEffectFailureKind::StorageUnavailable,
1789 HostEffectFailureKind::PermissionDenied,
1790 HostEffectFailureKind::ResourceExhausted,
1791 HostEffectFailureKind::Unknown,
1792 ];
1793 assert_eq!(kinds.len(), 6, "§7.9 fixes six executor failure classes");
1794
1795 for effect in effect_samples() {
1796 let pending = kernel_effect(effect);
1797 for kind in kinds {
1798 let outcome = EffectOutcome::Failed(EffectFailed {
1799 failure: HostEffectFailure {
1800 kind,
1801 message: "boom".to_string(),
1802 retryable: None,
1803 },
1804 });
1805 assert!(
1806 pending.accept_outcome(&outcome).is_ok(),
1807 "{:?} must have the same failure path as every other effect",
1808 pending.effect.tag()
1809 );
1810 }
1811 }
1812 }
1813
1814 #[test]
1815 fn the_outcome_union_has_exactly_two_arms() {
1816 let arms: BTreeSet<String> = [
1817 EffectOutcome::Succeeded(EffectSucceeded {
1818 result: success_samples().remove(0),
1819 }),
1820 EffectOutcome::Failed(EffectFailed {
1821 failure: HostEffectFailure {
1822 kind: HostEffectFailureKind::Unknown,
1823 message: String::new(),
1824 retryable: None,
1825 },
1826 }),
1827 ]
1828 .iter()
1829 .map(|outcome| {
1830 serde_json::to_value(outcome).unwrap()["status"]
1831 .as_str()
1832 .unwrap()
1833 .to_string()
1834 })
1835 .collect();
1836 assert_eq!(
1837 arms,
1838 BTreeSet::from(["succeeded".to_string(), "failed".to_string()])
1839 );
1840
1841 for third in ["partial", "pending", "deferred", "succeeded_with_warnings"] {
1842 let raw = json!({ "status": third });
1843 assert!(
1844 serde_json::from_value::<EffectOutcome>(raw).is_err(),
1845 "{third} is not an outcome"
1846 );
1847 }
1848 }
1849
1850 #[test]
1851 fn the_kernel_never_retries_so_retryable_is_host_advice_only() {
1852 let with = json!({
1855 "status": "failed",
1856 "failure": { "kind": "transport_exhausted", "message": "429", "retryable": true },
1857 });
1858 let outcome: EffectOutcome = serde_json::from_value(with).unwrap();
1859 match outcome {
1860 EffectOutcome::Failed(failed) => {
1861 assert_eq!(failed.failure.retryable, Some(true));
1862 assert_eq!(
1863 failed.failure.kind,
1864 HostEffectFailureKind::TransportExhausted
1865 );
1866 }
1867 EffectOutcome::Succeeded(_) => panic!("failed outcome decoded as succeeded"),
1868 }
1869 }
1870
1871 #[test]
1881 fn every_provider_family_maps_onto_the_canonical_stop_reason_vocabulary() {
1882 let canonical: BTreeSet<&str> = ProviderStopReason::ALL
1883 .iter()
1884 .map(|reason| reason.as_str())
1885 .collect();
1886 assert_eq!(
1887 canonical,
1888 BTreeSet::from([
1889 "end_turn",
1890 "tool_use",
1891 "max_tokens",
1892 "stop_sequence",
1893 "content_filter",
1894 "other",
1895 ])
1896 );
1897 for reason in ProviderStopReason::ALL {
1898 let decoded: ProviderStopReason =
1899 serde_json::from_value(json!(reason.as_str())).unwrap();
1900 assert_eq!(decoded, reason);
1901 }
1902
1903 for vendor_word in [
1905 "stop", "length", "tool_calls", "function_call", "content_filter", "STOP", "MAX_TOKENS", "SAFETY", "FINISH_REASON_STOP", "eos", "eos_token", "sensitive", "insufficient_system_resource",
1918 ] {
1919 let decoded = serde_json::from_value::<ProviderStopReason>(json!(vendor_word));
1920 if vendor_word == "content_filter" {
1921 assert!(decoded.is_ok(), "content_filter *is* canonical");
1922 continue;
1923 }
1924 assert!(
1925 decoded.is_err(),
1926 "{vendor_word:?} is a vendor spelling; the host maps it, core never learns it"
1927 );
1928 }
1929
1930 let value = serde_json::to_value(ProviderStopReason::Other).unwrap();
1932 assert_eq!(value, json!("other"));
1933 assert!(
1934 serde_json::from_value::<ProviderStopReason>(
1935 json!({ "kind": "other", "raw": "insufficient_system_resource" })
1936 )
1937 .is_err(),
1938 "`other` is not a pass-through for vendor text"
1939 );
1940 }
1941
1942 #[test]
1949 fn no_vendor_failure_vocabulary_is_expressible_on_the_canonical_face() {
1950 assert_eq!(HostEffectFailureKind::ALL.len(), 6);
1951 let canonical: BTreeSet<&str> = HostEffectFailureKind::ALL
1952 .iter()
1953 .map(|kind| kind.as_str())
1954 .collect();
1955 assert_eq!(
1956 canonical,
1957 BTreeSet::from([
1958 "transport_exhausted",
1959 "protocol_error",
1960 "storage_unavailable",
1961 "permission_denied",
1962 "resource_exhausted",
1963 "unknown",
1964 ])
1965 );
1966
1967 for absent in [
1968 "rate_limited",
1969 "rate_limit_exceeded",
1970 "too_many_requests",
1971 "overloaded",
1972 "service_unavailable",
1973 "server_error",
1974 "timeout",
1975 "context_length_exceeded",
1976 "context_overflow",
1977 "cancelled",
1978 "canceled",
1979 "aborted",
1980 "user_interrupt",
1981 "interrupted",
1982 ] {
1983 assert!(
1984 serde_json::from_value::<HostEffectFailureKind>(json!(absent)).is_err(),
1985 "{absent:?} must not be an executor failure class"
1986 );
1987 assert!(
1988 !canonical.contains(absent),
1989 "{absent:?} leaked into the canonical vocabulary"
1990 );
1991 }
1992
1993 let spent: EffectOutcome = serde_json::from_value(json!({
1995 "status": "failed",
1996 "failure": {
1997 "kind": "transport_exhausted",
1998 "message": "5 attempts over 41s",
1999 "retryable": false,
2000 },
2001 }))
2002 .unwrap();
2003 let EffectOutcome::Failed(failed) = spent else {
2004 panic!("a spent ladder is a failure");
2005 };
2006 assert_eq!(
2007 failed.failure.kind,
2008 HostEffectFailureKind::TransportExhausted
2009 );
2010 }
2011
2012 #[test]
2017 fn retryable_is_uniformly_optional_advice_across_all_six_failure_kinds() {
2018 for kind in HostEffectFailureKind::ALL {
2019 for retryable in [None, Some(true), Some(false)] {
2020 let mut failure = json!({ "kind": kind.as_str(), "message": "boom" });
2021 if let Some(flag) = retryable {
2022 failure
2023 .as_object_mut()
2024 .unwrap()
2025 .insert("retryable".to_string(), json!(flag));
2026 }
2027 let outcome: EffectOutcome =
2028 serde_json::from_value(json!({ "status": "failed", "failure": failure }))
2029 .unwrap();
2030 let EffectOutcome::Failed(failed) = outcome else {
2031 panic!("failure decoded as success");
2032 };
2033 assert_eq!(failed.failure.kind, kind);
2034 assert_eq!(failed.failure.retryable, retryable);
2035 }
2036 }
2037 }
2038
2039 #[test]
2044 fn a_tool_result_must_state_whether_the_batch_can_continue() {
2045 assert_eq!(
2047 decode_effect(json!({
2048 "status": "succeeded",
2049 "result": { "kind": "tools", "results": [
2050 { "kind": "inline", "call_id": "c-1", "result": { "output": "ok" } }] },
2051 }))
2052 .unwrap_err()
2053 .kind,
2054 WireRejectionKind::MissingField
2055 );
2056
2057 assert_eq!(
2059 ToolResultDisposition::ALL
2060 .iter()
2061 .map(|d| d.as_str())
2062 .collect::<BTreeSet<&str>>(),
2063 BTreeSet::from(["recoverable", "fatal"])
2064 );
2065 for value in ["recoverable", "fatal"] {
2066 let decoded: ToolResultDisposition = serde_json::from_value(json!(value)).expect(value);
2067 assert_eq!(decoded.as_str(), value);
2068 assert_eq!(decoded.is_fatal(), value == "fatal");
2069 }
2070
2071 for gone in [
2075 "user_interrupt",
2076 "governance_denied",
2077 "provider_failure",
2078 "timeout",
2079 "cancelled",
2080 ] {
2081 assert!(
2082 serde_json::from_value::<ToolResultDisposition>(json!(gone)).is_err(),
2083 "{gone:?} is not a tool-result disposition"
2084 );
2085 }
2086 }
2087
2088 #[test]
2094 fn both_residency_arms_state_the_same_two_failure_facts() {
2095 let inline = ToolResultPayload::Inline(InlineToolResult {
2096 call_id: call_id("call-1"),
2097 result: ToolResult {
2098 output: "boom".to_string(),
2099 durable_content: None,
2100 is_error: true,
2101 disposition: ToolResultDisposition::Fatal,
2102 tokens: None,
2103 },
2104 });
2105 let external = ToolResultPayload::External(ExternalToolResult {
2106 call_id: call_id("call-2"),
2107 payload_ref: payload_ref(),
2108 digest: digest(),
2109 original_size: WireU64::new(1_048_576),
2110 preview: "Traceback (most recent call last):".to_string(),
2111 is_error: true,
2112 disposition: ToolResultDisposition::Fatal,
2113 });
2114 for payload in [&inline, &external] {
2115 assert!(payload.is_error(), "{payload:?}");
2116 assert_eq!(payload.disposition(), ToolResultDisposition::Fatal);
2117 assert!(payload.disposition().is_fatal());
2118 }
2119
2120 assert_eq!(
2123 decode_effect(json!({
2124 "status": "succeeded",
2125 "result": { "kind": "tools", "results": [
2126 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2127 "digest": "sha256:ab", "original_size": "1", "preview": "x" }] },
2128 }))
2129 .unwrap_err()
2130 .kind,
2131 WireRejectionKind::MissingField
2132 );
2133
2134 let ok: EffectOutcome = serde_json::from_value(json!({
2137 "status": "succeeded",
2138 "result": { "kind": "tools", "results": [
2139 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2140 "digest": "sha256:ab", "original_size": "1", "preview": "x",
2141 "disposition": "recoverable" }] },
2142 }))
2143 .unwrap();
2144 let EffectOutcome::Succeeded(success) = &ok else {
2145 panic!("not a success");
2146 };
2147 let EffectSuccess::Tools(tools) = &success.result else {
2148 panic!("not a tools success");
2149 };
2150 assert!(!tools.results[0].is_error());
2151 let value = serde_json::to_value(&tools.results[0]).unwrap();
2152 assert!(value.get("is_error").is_none(), "false stays off the wire");
2153 assert_eq!(value["disposition"], json!("recoverable"));
2154 }
2155
2156 #[test]
2158 fn a_milestone_request_is_exactly_the_contract_and_phase_pair() {
2159 let request = MilestoneRequest {
2160 contract_id: "brief-quality-primary".to_string(),
2161 phase_id: "collect".to_string(),
2162 };
2163 let value = serde_json::to_value(&request).unwrap();
2164 assert_eq!(
2165 value.as_object().unwrap().keys().collect::<Vec<_>>(),
2166 vec!["contract_id", "phase_id"],
2167 "a phase id is unique only inside its contract, so the pair is the whole key"
2168 );
2169
2170 for host_owned in ["criteria", "required_evidence", "verifier", "evidence"] {
2174 let mut with_extra = value.clone();
2175 with_extra
2176 .as_object_mut()
2177 .unwrap()
2178 .insert(host_owned.to_string(), json!([]));
2179 assert!(
2180 serde_json::from_value::<MilestoneRequest>(with_extra).is_err(),
2181 "{host_owned} must not decode"
2182 );
2183 }
2184 for missing in ["contract_id", "phase_id"] {
2185 let mut without = value.clone();
2186 without.as_object_mut().unwrap().remove(missing);
2187 assert!(
2188 serde_json::from_value::<MilestoneRequest>(without).is_err(),
2189 "{missing} is half the key and cannot be omitted"
2190 );
2191 }
2192 }
2193
2194 #[test]
2199 fn no_effect_outcome_payload_carries_a_host_wall_clock() {
2200 const BANNED: [&str; 8] = [
2201 "now_ms",
2202 "observed_at_ms",
2203 "timestamp",
2204 "timestamp_ms",
2205 "started_at_ms",
2206 "completed_at_ms",
2207 "wall_clock_ms",
2208 "received_at_ms",
2209 ];
2210
2211 let mut outcomes: Vec<EffectOutcome> = success_samples()
2212 .into_iter()
2213 .map(|result| EffectOutcome::Succeeded(EffectSucceeded { result }))
2214 .collect();
2215 outcomes.push(EffectOutcome::Failed(EffectFailed {
2216 failure: HostEffectFailure {
2217 kind: HostEffectFailureKind::TransportExhausted,
2218 message: "socket hang up".to_string(),
2219 retryable: Some(false),
2220 },
2221 }));
2222
2223 for outcome in outcomes {
2224 let value = serde_json::to_value(&outcome).unwrap();
2225 let mut all = BTreeSet::new();
2226 keys(&value, &mut all);
2227 for banned in BANNED {
2228 assert!(
2229 !all.contains(banned),
2230 "outcome payload must not carry {banned:?}: {value}"
2231 );
2232 }
2233 }
2234 }
2235
2236 #[test]
2241 fn an_external_tool_result_is_a_handle_with_a_digest_size_and_preview() {
2242 let external = ToolResultPayload::External(ExternalToolResult {
2243 call_id: call_id("call-2"),
2244 payload_ref: payload_ref(),
2245 digest: digest(),
2246 original_size: WireU64::new(1_048_576),
2247 preview: "total 42".to_string(),
2248 is_error: false,
2249 disposition: ToolResultDisposition::Recoverable,
2250 });
2251 let value = serde_json::to_value(&external).unwrap();
2252 assert_eq!(value["kind"], json!("external"));
2253 for required in ["payload_ref", "digest", "original_size", "preview"] {
2254 assert!(value.get(required).is_some(), "external needs {required}");
2255 }
2256 assert_eq!(
2257 value["original_size"],
2258 json!("1048576"),
2259 "sizes are decimal-string u64, not JS numbers"
2260 );
2261
2262 let mut all = BTreeSet::new();
2264 keys(&value, &mut all);
2265 for banned in ["path", "file_path", "spool_ref", "spool_dir", "archive_ref"] {
2266 assert!(!all.contains(banned), "payload ref must stay opaque");
2267 }
2268 }
2269
2270 #[test]
2271 fn payload_residency_distinguishes_generated_over_limit_from_pressure_archival() {
2272 let residencies = [
2273 PayloadResidency::Resident(ResidentPayload {}),
2274 PayloadResidency::External(ExternalResidency {
2275 payload_ref: payload_ref(),
2276 digest: digest(),
2277 original_size: WireU64::new(1_048_576),
2278 }),
2279 PayloadResidency::PagedOut(PagedOutResidency {
2280 payload_ref: payload_ref(),
2281 digest: digest(),
2282 }),
2283 PayloadResidency::Collapsed(CollapsedPayload {}),
2284 ];
2285 let tags: BTreeSet<String> = residencies
2286 .iter()
2287 .map(|residency| {
2288 serde_json::to_value(residency).unwrap()["kind"]
2289 .as_str()
2290 .unwrap()
2291 .to_string()
2292 })
2293 .collect();
2294 assert_eq!(
2295 tags,
2296 BTreeSet::from([
2297 "resident".to_string(),
2298 "external".to_string(),
2299 "paged_out".to_string(),
2300 "collapsed".to_string(),
2301 ]),
2302 "§7.10 keeps `external` (generated over limit) apart from `paged_out` (pressure)"
2303 );
2304
2305 let paged = serde_json::to_value(&residencies[2]).unwrap();
2307 assert!(paged.get("original_size").is_none());
2308 }
2309
2310 #[test]
2315 fn unknown_success_kinds_fields_and_variants_are_rejected() {
2316 assert_eq!(
2318 decode_effect(json!({ "status": "succeeded", "result": { "kind": "spooled" } }))
2319 .unwrap_err()
2320 .kind,
2321 WireRejectionKind::UnknownVariant
2322 );
2323 assert_eq!(
2325 decode_effect(json!({ "status": "failed", "failure": { "kind": "rate_limited" } }))
2326 .unwrap_err()
2327 .kind,
2328 WireRejectionKind::UnknownVariant
2329 );
2330 assert_eq!(
2332 decode_effect(json!({
2333 "status": "succeeded",
2334 "result": { "kind": "approval" },
2335 "now_ms": 1,
2336 }))
2337 .unwrap_err()
2338 .kind,
2339 WireRejectionKind::UnknownField
2340 );
2341 assert_eq!(
2343 decode_effect(json!({
2344 "status": "succeeded",
2345 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2346 "payload": { "content": "x", "digest": "sha256:ab",
2347 "original_size": "1", "path": "/tmp/x" } },
2348 }))
2349 .unwrap_err()
2350 .kind,
2351 WireRejectionKind::UnknownField
2352 );
2353 assert_eq!(
2355 decode_effect(json!({
2356 "status": "succeeded",
2357 "result": { "kind": "tools", "results": [
2358 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2359 "original_size": "1", "preview": "x" }] },
2360 }))
2361 .unwrap_err()
2362 .kind,
2363 WireRejectionKind::MissingField
2364 );
2365 assert_eq!(
2367 decode_effect(json!({
2368 "status": "succeeded",
2369 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2370 "payload": { "content": "x", "digest": "sha256:ab",
2371 "original_size": 1 } },
2372 }))
2373 .unwrap_err()
2374 .kind,
2375 WireRejectionKind::InvalidScalar
2376 );
2377 }
2378
2379 #[test]
2380 fn every_effect_and_success_payload_denies_unknown_fields() {
2381 for effect in effect_samples() {
2382 let mut value = serde_json::to_value(&effect).unwrap();
2383 value
2384 .as_object_mut()
2385 .unwrap()
2386 .insert("host_hint".to_string(), json!("x"));
2387 assert!(
2388 serde_json::from_value::<EffectKind>(value).is_err(),
2389 "{:?} must deny unknown fields",
2390 effect.tag()
2391 );
2392 }
2393 for success in success_samples() {
2394 let mut value = serde_json::to_value(&success).unwrap();
2395 value
2396 .as_object_mut()
2397 .unwrap()
2398 .insert("host_hint".to_string(), json!("x"));
2399 assert!(
2400 serde_json::from_value::<EffectSuccess>(value).is_err(),
2401 "{:?} must deny unknown fields",
2402 success.tag()
2403 );
2404 }
2405 }
2406
2407 #[test]
2412 fn resolve_effect_goldens_cover_every_success_kind_and_the_failure_path() {
2413 let mut success_kinds: BTreeSet<String> = BTreeSet::new();
2414 let mut failure_kinds: BTreeSet<String> = BTreeSet::new();
2415
2416 for (name, fixture) in fixtures_with_prefix("input_resolve_") {
2417 let text = serde_json::to_string(&fixture).unwrap();
2418 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2419 .unwrap_or_else(|e| panic!("{name}: {e}"));
2420 assert_eq!(
2421 serde_json::to_value(&envelope).unwrap(),
2422 fixture,
2423 "{name}: round-trip changed the document"
2424 );
2425
2426 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2427 panic!("{name} is not a resolve_effect golden");
2428 };
2429 match &resolve.outcome {
2430 EffectOutcome::Succeeded(ok) => {
2431 success_kinds.insert(ok.result.tag().as_str().to_string());
2432 }
2433 EffectOutcome::Failed(failed) => {
2434 failure_kinds.insert(failed.failure.kind.as_str().to_string());
2435 }
2436 }
2437 }
2438
2439 assert_eq!(
2440 success_kinds,
2441 EffectSuccessTag::ALL
2442 .iter()
2443 .map(|tag| tag.as_str().to_string())
2444 .collect::<BTreeSet<String>>(),
2445 "every effect kind needs at least one resolve golden"
2446 );
2447 assert!(
2448 !failure_kinds.is_empty(),
2449 "the unified failure path needs a golden too"
2450 );
2451 }
2452
2453 #[test]
2454 fn the_milestone_effect_has_a_failure_channel_like_every_other_effect() {
2455 let golden = fixtures_with_prefix("input_resolve_effect_milestone_failed");
2457 assert_eq!(golden.len(), 1);
2458 let text = serde_json::to_string(&golden[0].1).unwrap();
2459 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default()).unwrap();
2460 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2461 panic!("not a resolve_effect golden");
2462 };
2463 let EffectOutcome::Failed(failed) = &resolve.outcome else {
2464 panic!("milestone failure golden must be a Failed outcome");
2465 };
2466 assert_eq!(
2467 failed.failure.kind,
2468 HostEffectFailureKind::StorageUnavailable
2469 );
2470
2471 let milestone = kernel_effect(EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
2472 request: MilestoneRequest {
2473 contract_id: "brief-quality-primary".to_string(),
2474 phase_id: "phase-2".to_string(),
2475 },
2476 }));
2477 assert!(milestone.accept_outcome(&resolve.outcome).is_ok());
2478 }
2479
2480 #[test]
2481 fn effect_rejection_goldens_cover_the_payload_failure_modes() {
2482 let mut expected: BTreeSet<String> = BTreeSet::new();
2483 for (name, fixture) in fixtures_with_prefix("reject_effect_") {
2484 let kind = fixture["expect"]
2485 .as_str()
2486 .unwrap_or_else(|| panic!("{name}: missing `expect`"));
2487 let text = serde_json::to_string(&fixture["envelope"]).unwrap();
2488 let rejection = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2489 .map(|ok| panic!("{name}: expected rejection, decoded {ok:?}"))
2490 .unwrap_err();
2491 assert_eq!(
2492 rejection.kind.as_str(),
2493 kind,
2494 "{name}: {}",
2495 rejection.message
2496 );
2497 expected.insert(kind.to_string());
2498 }
2499 for required in [
2500 "unknown_field",
2501 "unknown_variant",
2502 "missing_field",
2503 "invalid_scalar",
2504 ] {
2505 assert!(
2506 expected.contains(required),
2507 "effect rejection goldens must cover {required}"
2508 );
2509 }
2510 }
2511}