1use std::fmt;
24
25use serde::de::{self, Deserializer, Visitor};
26use serde::{Deserialize, Serialize, Serializer};
27
28use super::root::{LogicalAgentSpec, MessageRole};
29use super::scalar::{
30 AttemptId, BoundedJson, CallId, EffectId, FiniteF64, HandleId, InputId, MAX_ID_BYTES,
31 MemoryBindingId, NodeId, SCALAR_ERROR_MARKER, TaskId, WireScalarError, WireU64,
32};
33use super::syscall::{MemoryKind, SyscallCausation};
34
35#[doc(hidden)]
40pub(crate) fn validate_opaque(label: &'static str, value: &str) -> Result<(), WireScalarError> {
41 if value.is_empty() {
42 return Err(WireScalarError::new(format!("{label} must not be empty")));
43 }
44 if value.len() > MAX_ID_BYTES {
45 return Err(WireScalarError::new(format!(
46 "{label} is {} bytes; the bound is {MAX_ID_BYTES}",
47 value.len()
48 )));
49 }
50 if value.chars().any(char::is_control) {
51 return Err(WireScalarError::new(format!(
52 "{label} must not contain control characters"
53 )));
54 }
55 Ok(())
56}
57
58macro_rules! wire_opaque_ref {
61 ($(#[$doc:meta])* $name:ident, $label:literal) => {
62 $(#[$doc])*
63 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64 pub struct $name(String);
65
66 impl $name {
67 pub fn new(value: impl Into<String>) -> Result<Self, WireScalarError> {
68 let value = value.into();
69 $crate::runtime::kernel::wire::effect::validate_opaque($label, &value)?;
70 Ok(Self(value))
71 }
72
73 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76
77 pub fn into_string(self) -> String {
78 self.0
79 }
80 }
81
82 impl fmt::Display for $name {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.write_str(&self.0)
85 }
86 }
87
88 impl Serialize for $name {
89 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
90 serializer.serialize_str(&self.0)
91 }
92 }
93
94 impl<'de> Deserialize<'de> for $name {
95 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
96 struct RefVisitor;
97
98 impl Visitor<'_> for RefVisitor {
99 type Value = $name;
100
101 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(concat!("a non-empty ", $label, " string"))
103 }
104
105 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
106 $name::new(value).map_err(|err| {
107 E::custom(format!("{SCALAR_ERROR_MARKER}: {}", err.message))
108 })
109 }
110
111 fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
112 Err(E::custom(format!(
113 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got {value}",
114 $label
115 )))
116 }
117
118 fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
119 Err(E::custom(format!(
120 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got {value}",
121 $label
122 )))
123 }
124
125 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
126 Err(E::custom(format!(
127 "{SCALAR_ERROR_MARKER}: {} must be a branded string, got null",
128 $label
129 )))
130 }
131 }
132
133 deserializer.deserialize_any(RefVisitor)
134 }
135 }
136 };
137}
138
139pub(crate) use wire_opaque_ref;
140
141wire_opaque_ref!(
142 PayloadRef,
149 "payload ref"
150);
151wire_opaque_ref!(
152 Digest,
155 "digest"
156);
157wire_opaque_ref!(
158 LaunchToken,
162 "launch token"
163);
164wire_opaque_ref!(
165 MemoryRecordRef,
167 "memory record ref"
168);
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(deny_unknown_fields)]
180pub struct KernelEffect {
181 pub effect_id: EffectId,
182 pub causation_input_id: InputId,
183 pub effect: EffectKind,
184}
185
186impl KernelEffect {
187 pub fn tag(&self) -> EffectKindTag {
188 self.effect.tag()
189 }
190
191 pub fn accept_outcome(&self, outcome: &EffectOutcome) -> Result<(), EffectResolutionMismatch> {
199 match outcome {
200 EffectOutcome::Failed(_) => Ok(()),
201 EffectOutcome::Succeeded(success) => {
202 let expected = self.effect.tag().expected_success();
203 let received = success.result.tag();
204 if expected == received {
205 Ok(())
206 } else {
207 Err(EffectResolutionMismatch {
208 effect_id: self.effect_id.clone(),
209 expected,
210 received,
211 })
212 }
213 }
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct EffectResolutionMismatch {
221 pub effect_id: EffectId,
222 pub expected: EffectSuccessTag,
223 pub received: EffectSuccessTag,
224}
225
226impl fmt::Display for EffectResolutionMismatch {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 write!(
229 f,
230 "effect {} expects a {} resolution, got {}",
231 self.effect_id,
232 self.expected.as_str(),
233 self.received.as_str()
234 )
235 }
236}
237
238impl std::error::Error for EffectResolutionMismatch {}
239
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245#[serde(tag = "kind", rename_all = "snake_case")]
246pub enum EffectKind {
247 CallProvider(CallProviderEffect),
248 ExecuteTools(ExecuteToolsEffect),
249 RequestApproval(RequestApprovalEffect),
250 SpawnTasks(SpawnTasksEffect),
251 PreemptTasks(PreemptTasksEffect),
252 PersistMemory(PersistMemoryEffect),
253 QueryMemory(QueryMemoryEffect),
254 ArchivePageOut(ArchivePageOutEffect),
255 LoadPayload(LoadPayloadEffect),
258 EvaluateMilestone(EvaluateMilestoneEffect),
259}
260
261impl EffectKind {
262 pub fn tag(&self) -> EffectKindTag {
263 match self {
264 Self::CallProvider(_) => EffectKindTag::CallProvider,
265 Self::ExecuteTools(_) => EffectKindTag::ExecuteTools,
266 Self::RequestApproval(_) => EffectKindTag::RequestApproval,
267 Self::SpawnTasks(_) => EffectKindTag::SpawnTasks,
268 Self::PreemptTasks(_) => EffectKindTag::PreemptTasks,
269 Self::PersistMemory(_) => EffectKindTag::PersistMemory,
270 Self::QueryMemory(_) => EffectKindTag::QueryMemory,
271 Self::ArchivePageOut(_) => EffectKindTag::ArchivePageOut,
272 Self::LoadPayload(_) => EffectKindTag::LoadPayload,
273 Self::EvaluateMilestone(_) => EffectKindTag::EvaluateMilestone,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum EffectKindTag {
287 CallProvider,
288 ExecuteTools,
289 RequestApproval,
290 SpawnTasks,
291 PreemptTasks,
292 PersistMemory,
293 QueryMemory,
294 ArchivePageOut,
295 LoadPayload,
296 EvaluateMilestone,
297}
298
299impl EffectKindTag {
300 pub const ALL: [Self; 10] = [
301 Self::CallProvider,
302 Self::ExecuteTools,
303 Self::RequestApproval,
304 Self::SpawnTasks,
305 Self::PreemptTasks,
306 Self::PersistMemory,
307 Self::QueryMemory,
308 Self::ArchivePageOut,
309 Self::LoadPayload,
310 Self::EvaluateMilestone,
311 ];
312
313 pub fn as_str(self) -> &'static str {
314 match self {
315 Self::CallProvider => "call_provider",
316 Self::ExecuteTools => "execute_tools",
317 Self::RequestApproval => "request_approval",
318 Self::SpawnTasks => "spawn_tasks",
319 Self::PreemptTasks => "preempt_tasks",
320 Self::PersistMemory => "persist_memory",
321 Self::QueryMemory => "query_memory",
322 Self::ArchivePageOut => "archive_page_out",
323 Self::LoadPayload => "load_payload",
324 Self::EvaluateMilestone => "evaluate_milestone",
325 }
326 }
327
328 pub fn expected_success(self) -> EffectSuccessTag {
330 match self {
331 Self::CallProvider => EffectSuccessTag::Provider,
332 Self::ExecuteTools => EffectSuccessTag::Tools,
333 Self::RequestApproval => EffectSuccessTag::Approval,
334 Self::SpawnTasks => EffectSuccessTag::TasksSpawned,
335 Self::PreemptTasks => EffectSuccessTag::TasksPreempted,
336 Self::PersistMemory => EffectSuccessTag::MemoryPersisted,
337 Self::QueryMemory => EffectSuccessTag::MemoryQueried,
338 Self::ArchivePageOut => EffectSuccessTag::PageOutArchived,
339 Self::LoadPayload => EffectSuccessTag::PayloadLoaded,
340 Self::EvaluateMilestone => EffectSuccessTag::MilestoneEvaluated,
341 }
342 }
343}
344
345impl fmt::Display for EffectKindTag {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 f.write_str(self.as_str())
348 }
349}
350
351#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct CallProviderEffect {
358 pub context: RenderedContext,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
360 pub tools: Vec<ToolSchema>,
361}
362
363#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
364#[serde(deny_unknown_fields)]
365pub struct ExecuteToolsEffect {
366 pub calls: Vec<ToolCall>,
367}
368
369#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
370#[serde(deny_unknown_fields)]
371pub struct RequestApprovalEffect {
372 pub requests: Vec<ApprovalRequest>,
373}
374
375#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
376#[serde(deny_unknown_fields)]
377pub struct SpawnTasksEffect {
378 pub tasks: Vec<TaskLaunch>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub budget: Option<WorkflowBudget>,
381}
382
383#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
384#[serde(deny_unknown_fields)]
385pub struct PreemptTasksEffect {
386 pub attempts: Vec<TaskAttemptRef>,
387 pub reason: String,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct PersistMemoryEffect {
393 pub binding: MemoryAccessBinding,
394 pub memory: CanonicalMemoryWrite,
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398#[serde(deny_unknown_fields)]
399pub struct QueryMemoryEffect {
400 pub binding: MemoryAccessBinding,
401 pub query: CanonicalMemoryQuery,
402 pub requested_k: u32,
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407#[serde(deny_unknown_fields)]
408pub struct ArchivePageOutEffect {
409 pub handle_id: HandleId,
410 pub payload: PageOutPayload,
411}
412
413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[serde(deny_unknown_fields)]
415pub struct LoadPayloadEffect {
416 pub handle_id: HandleId,
417 pub payload_ref: PayloadRef,
418}
419
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
421#[serde(deny_unknown_fields)]
422pub struct EvaluateMilestoneEffect {
423 pub request: MilestoneRequest,
424}
425
426#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
432#[serde(deny_unknown_fields)]
433pub struct RenderedContext {
434 #[serde(default, skip_serializing_if = "String::is_empty")]
435 pub system_stable: String,
436 #[serde(default, skip_serializing_if = "String::is_empty")]
437 pub system_knowledge: String,
438 #[serde(default, skip_serializing_if = "Vec::is_empty")]
439 pub turns: Vec<ProviderMessage>,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub state_turn: Option<ProviderMessage>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub frozen_prefix_len: Option<u32>,
446}
447
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
452#[serde(deny_unknown_fields)]
453pub struct ProviderMessage {
454 pub role: MessageRole,
455 pub content: String,
456 #[serde(default, skip_serializing_if = "Vec::is_empty")]
457 pub tool_calls: Vec<ToolCall>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub tool_call_id: Option<CallId>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub tokens: Option<u32>,
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465#[serde(deny_unknown_fields)]
466pub struct ToolSchema {
467 pub name: String,
468 #[serde(default, skip_serializing_if = "String::is_empty")]
469 pub description: String,
470 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
471 pub parameters: BoundedJson,
472}
473
474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct ToolCall {
477 pub call_id: CallId,
478 pub name: String,
479 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
480 pub arguments: BoundedJson,
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484#[serde(deny_unknown_fields)]
485pub struct ApprovalRequest {
486 pub call_id: CallId,
487 pub tool_name: String,
488 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
489 pub arguments: BoundedJson,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub reason: Option<String>,
492}
493
494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct TaskLaunch {
502 pub task_id: TaskId,
503 pub attempt_id: AttemptId,
504 pub launch_token: LaunchToken,
505 pub node_id: NodeId,
506 pub spec: LogicalAgentSpec,
507}
508
509#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
510#[serde(deny_unknown_fields)]
511pub struct WorkflowBudget {
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub max_total_tokens: Option<WireU64>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub max_turns: Option<u32>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub max_concurrency: Option<u32>,
518}
519
520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521#[serde(deny_unknown_fields)]
522pub struct TaskAttemptRef {
523 pub task_id: TaskId,
524 pub attempt_id: AttemptId,
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
530#[serde(deny_unknown_fields)]
531pub struct MemoryAccessBinding {
532 pub binding_id: MemoryBindingId,
533 pub capabilities: MemoryCapabilities,
534}
535
536#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(deny_unknown_fields)]
538pub struct MemoryCapabilities {
539 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
540 pub read: bool,
541 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
542 pub write: bool,
543}
544
545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552#[serde(deny_unknown_fields)]
553pub struct CanonicalMemoryWrite {
554 pub name: String,
555 pub kind: MemoryKind,
556 pub content: String,
557 #[serde(default, skip_serializing_if = "String::is_empty")]
558 pub description: String,
559 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 pub evidence_refs: Vec<String>,
561 pub accepted_at_ms: WireU64,
562 pub causation: SyscallCausation,
563}
564
565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
566#[serde(deny_unknown_fields)]
567pub struct CanonicalMemoryQuery {
568 #[serde(default, skip_serializing_if = "String::is_empty")]
569 pub text: String,
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
571 pub kinds: Vec<MemoryKind>,
572 pub accepted_at_ms: WireU64,
573 pub causation: SyscallCausation,
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[serde(deny_unknown_fields)]
580pub struct PageOutPayload {
581 pub content: String,
582 pub digest: Digest,
583 pub original_size: WireU64,
584 #[serde(default, skip_serializing_if = "String::is_empty")]
585 pub preview: String,
586}
587
588#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
596#[serde(deny_unknown_fields)]
597pub struct MilestoneRequest {
598 pub contract_id: String,
599 pub phase_id: String,
600}
601
602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
612#[serde(tag = "status", rename_all = "snake_case")]
613pub enum EffectOutcome {
614 Succeeded(EffectSucceeded),
615 Failed(EffectFailed),
616}
617
618impl EffectOutcome {
619 pub fn success_tag(&self) -> Option<EffectSuccessTag> {
621 match self {
622 Self::Succeeded(success) => Some(success.result.tag()),
623 Self::Failed(_) => None,
624 }
625 }
626}
627
628#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
631#[serde(deny_unknown_fields)]
632pub struct EffectSucceeded {
633 pub result: EffectSuccess,
634}
635
636#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
638#[serde(deny_unknown_fields)]
639pub struct EffectFailed {
640 pub failure: HostEffectFailure,
641}
642
643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
647#[serde(deny_unknown_fields)]
648pub struct HostEffectFailure {
649 pub kind: HostEffectFailureKind,
650 #[serde(default, skip_serializing_if = "String::is_empty")]
651 pub message: String,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub retryable: Option<bool>,
668}
669
670#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
689#[serde(rename_all = "snake_case")]
690pub enum HostEffectFailureKind {
691 TransportExhausted,
694 ProtocolError,
699 StorageUnavailable,
700 PermissionDenied,
701 ResourceExhausted,
702 Unknown,
703}
704
705impl HostEffectFailureKind {
706 pub const ALL: [Self; 6] = [
707 Self::TransportExhausted,
708 Self::ProtocolError,
709 Self::StorageUnavailable,
710 Self::PermissionDenied,
711 Self::ResourceExhausted,
712 Self::Unknown,
713 ];
714
715 pub fn as_str(self) -> &'static str {
716 match self {
717 Self::TransportExhausted => "transport_exhausted",
718 Self::ProtocolError => "protocol_error",
719 Self::StorageUnavailable => "storage_unavailable",
720 Self::PermissionDenied => "permission_denied",
721 Self::ResourceExhausted => "resource_exhausted",
722 Self::Unknown => "unknown",
723 }
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
729#[serde(tag = "kind", rename_all = "snake_case")]
730pub enum EffectSuccess {
731 Provider(ProviderSuccess),
732 Tools(ToolsSuccess),
733 Approval(ApprovalSuccess),
734 TasksSpawned(TasksSpawnedSuccess),
735 TasksPreempted(TasksPreemptedSuccess),
736 MemoryPersisted(MemoryPersistedSuccess),
737 MemoryQueried(MemoryQueriedSuccess),
738 PageOutArchived(PageOutArchivedSuccess),
739 PayloadLoaded(PayloadLoadedSuccess),
740 MilestoneEvaluated(MilestoneEvaluatedSuccess),
741}
742
743impl EffectSuccess {
744 pub fn tag(&self) -> EffectSuccessTag {
745 match self {
746 Self::Provider(_) => EffectSuccessTag::Provider,
747 Self::Tools(_) => EffectSuccessTag::Tools,
748 Self::Approval(_) => EffectSuccessTag::Approval,
749 Self::TasksSpawned(_) => EffectSuccessTag::TasksSpawned,
750 Self::TasksPreempted(_) => EffectSuccessTag::TasksPreempted,
751 Self::MemoryPersisted(_) => EffectSuccessTag::MemoryPersisted,
752 Self::MemoryQueried(_) => EffectSuccessTag::MemoryQueried,
753 Self::PageOutArchived(_) => EffectSuccessTag::PageOutArchived,
754 Self::PayloadLoaded(_) => EffectSuccessTag::PayloadLoaded,
755 Self::MilestoneEvaluated(_) => EffectSuccessTag::MilestoneEvaluated,
756 }
757 }
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
762#[serde(rename_all = "snake_case")]
763pub enum EffectSuccessTag {
764 Provider,
765 Tools,
766 Approval,
767 TasksSpawned,
768 TasksPreempted,
769 MemoryPersisted,
770 MemoryQueried,
771 PageOutArchived,
772 PayloadLoaded,
773 MilestoneEvaluated,
774}
775
776impl EffectSuccessTag {
777 pub const ALL: [Self; 10] = [
778 Self::Provider,
779 Self::Tools,
780 Self::Approval,
781 Self::TasksSpawned,
782 Self::TasksPreempted,
783 Self::MemoryPersisted,
784 Self::MemoryQueried,
785 Self::PageOutArchived,
786 Self::PayloadLoaded,
787 Self::MilestoneEvaluated,
788 ];
789
790 pub fn as_str(self) -> &'static str {
791 match self {
792 Self::Provider => "provider",
793 Self::Tools => "tools",
794 Self::Approval => "approval",
795 Self::TasksSpawned => "tasks_spawned",
796 Self::TasksPreempted => "tasks_preempted",
797 Self::MemoryPersisted => "memory_persisted",
798 Self::MemoryQueried => "memory_queried",
799 Self::PageOutArchived => "page_out_archived",
800 Self::PayloadLoaded => "payload_loaded",
801 Self::MilestoneEvaluated => "milestone_evaluated",
802 }
803 }
804
805 pub fn resolves(self) -> EffectKindTag {
808 match self {
809 Self::Provider => EffectKindTag::CallProvider,
810 Self::Tools => EffectKindTag::ExecuteTools,
811 Self::Approval => EffectKindTag::RequestApproval,
812 Self::TasksSpawned => EffectKindTag::SpawnTasks,
813 Self::TasksPreempted => EffectKindTag::PreemptTasks,
814 Self::MemoryPersisted => EffectKindTag::PersistMemory,
815 Self::MemoryQueried => EffectKindTag::QueryMemory,
816 Self::PageOutArchived => EffectKindTag::ArchivePageOut,
817 Self::PayloadLoaded => EffectKindTag::LoadPayload,
818 Self::MilestoneEvaluated => EffectKindTag::EvaluateMilestone,
819 }
820 }
821}
822
823impl fmt::Display for EffectSuccessTag {
824 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
825 f.write_str(self.as_str())
826 }
827}
828
829#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
834#[serde(deny_unknown_fields)]
835pub struct ProviderSuccess {
836 pub outcome: ProviderOutcome,
837}
838
839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
844#[serde(tag = "kind", rename_all = "snake_case")]
845pub enum ProviderOutcome {
846 Completed(ProviderCompleted),
847 ContextOverflow(ProviderContextOverflow),
851}
852
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
854#[serde(deny_unknown_fields)]
855pub struct ProviderCompleted {
856 pub message: ProviderMessage,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub observed_input_tokens: Option<u32>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub observed_output_tokens: Option<u32>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub stop_reason: Option<ProviderStopReason>,
863}
864
865#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
866#[serde(deny_unknown_fields)]
867pub struct ProviderContextOverflow {
868 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub observed_input_tokens: Option<u32>,
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
881#[serde(rename_all = "snake_case")]
882pub enum ProviderStopReason {
883 EndTurn,
884 ToolUse,
885 MaxTokens,
886 StopSequence,
887 ContentFilter,
888 Other,
889}
890
891impl ProviderStopReason {
892 pub const ALL: [Self; 6] = [
893 Self::EndTurn,
894 Self::ToolUse,
895 Self::MaxTokens,
896 Self::StopSequence,
897 Self::ContentFilter,
898 Self::Other,
899 ];
900
901 pub fn as_str(self) -> &'static str {
902 match self {
903 Self::EndTurn => "end_turn",
904 Self::ToolUse => "tool_use",
905 Self::MaxTokens => "max_tokens",
906 Self::StopSequence => "stop_sequence",
907 Self::ContentFilter => "content_filter",
908 Self::Other => "other",
909 }
910 }
911}
912
913impl fmt::Display for ProviderStopReason {
914 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
915 f.write_str(self.as_str())
916 }
917}
918
919#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
920#[serde(deny_unknown_fields)]
921pub struct ToolsSuccess {
922 pub results: Vec<ToolResultPayload>,
923}
924
925#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
926#[serde(deny_unknown_fields)]
927pub struct ApprovalSuccess {
928 #[serde(default, skip_serializing_if = "Vec::is_empty")]
929 pub approved_call_ids: Vec<CallId>,
930 #[serde(default, skip_serializing_if = "Vec::is_empty")]
931 pub denied_call_ids: Vec<CallId>,
932}
933
934#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
935#[serde(deny_unknown_fields)]
936pub struct TasksSpawnedSuccess {
937 pub attempts: Vec<TaskLaunchOutcome>,
938}
939
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
943#[serde(deny_unknown_fields)]
944pub struct TaskLaunchOutcome {
945 pub task_id: TaskId,
946 pub attempt_id: AttemptId,
947 pub outcome: TaskLaunchStatus,
948}
949
950#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
951#[serde(tag = "status", rename_all = "snake_case")]
952pub enum TaskLaunchStatus {
953 Started(TaskLaunchStarted),
954 Failed(TaskLaunchFailed),
955}
956
957#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
958#[serde(deny_unknown_fields)]
959pub struct TaskLaunchStarted {}
960
961#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
962#[serde(deny_unknown_fields)]
963pub struct TaskLaunchFailed {
964 pub failure: TaskLaunchFailure,
965}
966
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
970#[serde(deny_unknown_fields)]
971pub struct TaskLaunchFailure {
972 pub kind: HostEffectFailureKind,
973 #[serde(default, skip_serializing_if = "String::is_empty")]
974 pub message: String,
975}
976
977#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
978#[serde(deny_unknown_fields)]
979pub struct TasksPreemptedSuccess {
980 pub attempts: Vec<TaskPreemptOutcome>,
981}
982
983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
984#[serde(deny_unknown_fields)]
985pub struct TaskPreemptOutcome {
986 pub task_id: TaskId,
987 pub attempt_id: AttemptId,
988 pub outcome: TaskPreemptStatus,
989}
990
991#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
992#[serde(tag = "status", rename_all = "snake_case")]
993pub enum TaskPreemptStatus {
994 Preempted(TaskPreempted),
995 AlreadyFinished(TaskAlreadyFinished),
998 Failed(TaskPreemptFailed),
999}
1000
1001#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1002#[serde(deny_unknown_fields)]
1003pub struct TaskPreempted {}
1004
1005#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(deny_unknown_fields)]
1007pub struct TaskAlreadyFinished {}
1008
1009#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1010#[serde(deny_unknown_fields)]
1011pub struct TaskPreemptFailed {
1012 pub failure: TaskLaunchFailure,
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1016#[serde(deny_unknown_fields)]
1017pub struct MemoryPersistedSuccess {
1018 pub receipt: MemoryPersistReceipt,
1019}
1020
1021#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1022#[serde(deny_unknown_fields)]
1023pub struct MemoryPersistReceipt {
1024 pub binding_id: MemoryBindingId,
1025 pub record_ref: MemoryRecordRef,
1026 pub digest: Digest,
1027}
1028
1029#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1030#[serde(deny_unknown_fields)]
1031pub struct MemoryQueriedSuccess {
1032 pub recalls: Vec<MemoryRecall>,
1033}
1034
1035#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1036#[serde(deny_unknown_fields)]
1037pub struct MemoryRecall {
1038 pub record_ref: MemoryRecordRef,
1039 pub name: String,
1040 pub kind: MemoryKind,
1041 pub content: String,
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub score: Option<FiniteF64>,
1046}
1047
1048#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1049#[serde(deny_unknown_fields)]
1050pub struct PageOutArchivedSuccess {
1051 pub receipt: ArchiveReceipt,
1052}
1053
1054#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1055#[serde(deny_unknown_fields)]
1056pub struct ArchiveReceipt {
1057 pub handle_id: HandleId,
1058 pub payload_ref: PayloadRef,
1059 pub digest: Digest,
1060 pub original_size: WireU64,
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1064#[serde(deny_unknown_fields)]
1065pub struct PayloadLoadedSuccess {
1066 pub handle_id: HandleId,
1067 pub payload: InlinePayload,
1068}
1069
1070#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1073#[serde(deny_unknown_fields)]
1074pub struct InlinePayload {
1075 pub content: String,
1076 pub digest: Digest,
1077 pub original_size: WireU64,
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1081#[serde(deny_unknown_fields)]
1082pub struct MilestoneEvaluatedSuccess {
1083 pub result: MilestoneCheckResult,
1084}
1085
1086#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1090#[serde(deny_unknown_fields)]
1091pub struct MilestoneCheckResult {
1092 pub phase_id: String,
1093 pub passed: bool,
1094 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1095 pub failed_criteria: Vec<String>,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub score: Option<FiniteF64>,
1098 #[serde(default, skip_serializing_if = "String::is_empty")]
1099 pub notes: String,
1100}
1101
1102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1114#[serde(tag = "kind", rename_all = "snake_case")]
1115pub enum ToolResultPayload {
1116 Inline(InlineToolResult),
1117 External(ExternalToolResult),
1118}
1119
1120impl ToolResultPayload {
1121 pub fn call_id(&self) -> &CallId {
1122 match self {
1123 Self::Inline(inline) => &inline.call_id,
1124 Self::External(external) => &external.call_id,
1125 }
1126 }
1127
1128 pub fn disposition(&self) -> ToolResultDisposition {
1133 match self {
1134 Self::Inline(inline) => inline.result.disposition,
1135 Self::External(external) => external.disposition,
1136 }
1137 }
1138
1139 pub fn is_error(&self) -> bool {
1140 match self {
1141 Self::Inline(inline) => inline.result.is_error,
1142 Self::External(external) => external.is_error,
1143 }
1144 }
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1148#[serde(deny_unknown_fields)]
1149pub struct InlineToolResult {
1150 pub call_id: CallId,
1151 pub result: ToolResult,
1152}
1153
1154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1162#[serde(deny_unknown_fields)]
1163pub struct ExternalToolResult {
1164 pub call_id: CallId,
1165 pub payload_ref: PayloadRef,
1167 pub digest: Digest,
1168 pub original_size: WireU64,
1169 pub preview: String,
1171 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1172 pub is_error: bool,
1173 pub disposition: ToolResultDisposition,
1175}
1176
1177#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1180#[serde(deny_unknown_fields)]
1181pub struct ToolResult {
1182 pub output: String,
1183 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1184 pub is_error: bool,
1185 pub disposition: ToolResultDisposition,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub tokens: Option<u32>,
1190}
1191
1192#[derive(
1204 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
1205)]
1206#[serde(rename_all = "snake_case")]
1207pub enum ToolResultDisposition {
1208 #[default]
1212 Recoverable,
1213 Fatal,
1217}
1218
1219impl ToolResultDisposition {
1220 pub const ALL: [Self; 2] = [Self::Recoverable, Self::Fatal];
1221
1222 pub fn as_str(self) -> &'static str {
1223 match self {
1224 Self::Recoverable => "recoverable",
1225 Self::Fatal => "fatal",
1226 }
1227 }
1228
1229 pub fn is_fatal(self) -> bool {
1230 matches!(self, Self::Fatal)
1231 }
1232}
1233
1234impl fmt::Display for ToolResultDisposition {
1235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1236 f.write_str(self.as_str())
1237 }
1238}
1239
1240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1247#[serde(tag = "kind", rename_all = "snake_case")]
1248pub enum PayloadResidency {
1249 Resident(ResidentPayload),
1250 External(ExternalResidency),
1251 PagedOut(PagedOutResidency),
1252 Collapsed(CollapsedPayload),
1254}
1255
1256#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1257#[serde(deny_unknown_fields)]
1258pub struct ResidentPayload {}
1259
1260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1261#[serde(deny_unknown_fields)]
1262pub struct ExternalResidency {
1263 pub payload_ref: PayloadRef,
1264 pub digest: Digest,
1265 pub original_size: WireU64,
1266}
1267
1268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1269#[serde(deny_unknown_fields)]
1270pub struct PagedOutResidency {
1271 pub payload_ref: PayloadRef,
1272 pub digest: Digest,
1273}
1274
1275#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1276#[serde(deny_unknown_fields)]
1277pub struct CollapsedPayload {}
1278
1279#[cfg(test)]
1280mod tests {
1281 use std::collections::BTreeSet;
1282 use std::fs;
1283 use std::path::PathBuf;
1284
1285 use serde_json::{Value, json};
1286
1287 use super::super::*;
1288
1289 fn fixture_dir() -> PathBuf {
1294 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
1295 }
1296
1297 fn fixtures_with_prefix(prefix: &str) -> Vec<(String, Value)> {
1298 let dir = fixture_dir();
1299 let mut names: Vec<String> = fs::read_dir(&dir)
1300 .unwrap_or_else(|e| panic!("failed to read {}: {e}", dir.display()))
1301 .map(|entry| {
1302 entry
1303 .expect("dir entry")
1304 .file_name()
1305 .to_string_lossy()
1306 .to_string()
1307 })
1308 .filter(|name| name.ends_with(".json") && name.starts_with(prefix))
1309 .collect();
1310 names.sort();
1311 assert!(!names.is_empty(), "no {prefix}*.json fixtures");
1312 names
1313 .into_iter()
1314 .map(|name| {
1315 let raw = fs::read_to_string(dir.join(&name)).unwrap();
1316 let value: Value = serde_json::from_str(&raw).unwrap();
1317 (name, value)
1318 })
1319 .collect()
1320 }
1321
1322 fn keys(value: &Value, out: &mut BTreeSet<String>) {
1323 match value {
1324 Value::Object(map) => {
1325 for (key, child) in map {
1326 out.insert(key.clone());
1327 keys(child, out);
1328 }
1329 }
1330 Value::Array(items) => items.iter().for_each(|item| keys(item, out)),
1331 _ => {}
1332 }
1333 }
1334
1335 fn effect_json(outcome: Value) -> String {
1336 serde_json::to_string(&json!({
1337 "abi_version": KERNEL_ABI_VERSION,
1338 "operation_id": "op-1",
1339 "input_id": "in-1",
1340 "observed_at_ms": "1700000000000",
1341 "input": { "kind": "resolve_effect", "effect_id": "op-1:step:1:effect:0", "outcome": outcome },
1342 }))
1343 .unwrap()
1344 }
1345
1346 fn decode_effect(outcome: Value) -> Result<WireEnvelope, WireRejection> {
1347 decode_envelope_json(&effect_json(outcome), &KernelBootstrapLimits::default())
1348 }
1349
1350 fn call_id(id: &str) -> CallId {
1351 CallId::new(id).unwrap()
1352 }
1353
1354 fn digest() -> Digest {
1355 Digest::new("sha256:3b1f4a7c9e2d05186a4c7f0b9d3e8c25714f6a0b8c5d2e9f1a3b6c8d0e2f4a61")
1356 .unwrap()
1357 }
1358
1359 fn payload_ref() -> PayloadRef {
1360 PayloadRef::new("payload:01J8Y2QK7C4N0V").unwrap()
1361 }
1362
1363 fn binding() -> MemoryAccessBinding {
1364 MemoryAccessBinding {
1365 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1366 capabilities: MemoryCapabilities {
1367 read: true,
1368 write: true,
1369 },
1370 }
1371 }
1372
1373 fn causation() -> SyscallCausation {
1374 SyscallCausation::ProviderTool(ProviderToolCausation {
1375 provider_effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1376 call_id: call_id("call-1"),
1377 task_id: TaskId::new("task-1").unwrap(),
1378 })
1379 }
1380
1381 fn effect_samples() -> Vec<EffectKind> {
1383 vec![
1384 EffectKind::CallProvider(CallProviderEffect {
1385 context: RenderedContext::default(),
1386 tools: vec![ToolSchema {
1387 name: "read_file".to_string(),
1388 description: "read a file".to_string(),
1389 parameters: BoundedJson::null(),
1390 }],
1391 }),
1392 EffectKind::ExecuteTools(ExecuteToolsEffect {
1393 calls: vec![ToolCall {
1394 call_id: call_id("call-1"),
1395 name: "read_file".to_string(),
1396 arguments: BoundedJson::null(),
1397 }],
1398 }),
1399 EffectKind::RequestApproval(RequestApprovalEffect {
1400 requests: vec![ApprovalRequest {
1401 call_id: call_id("call-1"),
1402 tool_name: "rm".to_string(),
1403 arguments: BoundedJson::null(),
1404 reason: Some("destructive".to_string()),
1405 }],
1406 }),
1407 EffectKind::SpawnTasks(SpawnTasksEffect {
1408 tasks: vec![TaskLaunch {
1409 task_id: TaskId::new("task-1").unwrap(),
1410 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1411 launch_token: LaunchToken::new("launch-1").unwrap(),
1412 node_id: NodeId::new("node-a").unwrap(),
1413 spec: LogicalAgentSpec::new("research"),
1414 }],
1415 budget: None,
1416 }),
1417 EffectKind::PreemptTasks(PreemptTasksEffect {
1418 attempts: vec![TaskAttemptRef {
1419 task_id: TaskId::new("task-1").unwrap(),
1420 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1421 }],
1422 reason: "budget exhausted".to_string(),
1423 }),
1424 EffectKind::PersistMemory(PersistMemoryEffect {
1425 binding: binding(),
1426 memory: CanonicalMemoryWrite {
1427 name: "release pipeline".to_string(),
1428 kind: MemoryKind::Project,
1429 content: "tag v* publishes".to_string(),
1430 description: String::new(),
1431 evidence_refs: Vec::new(),
1432 accepted_at_ms: WireU64::new(1_700_000_000_000),
1433 causation: causation(),
1434 },
1435 }),
1436 EffectKind::QueryMemory(QueryMemoryEffect {
1437 binding: binding(),
1438 query: CanonicalMemoryQuery {
1439 text: "release".to_string(),
1440 kinds: vec![MemoryKind::Project],
1441 accepted_at_ms: WireU64::new(1_700_000_000_000),
1442 causation: causation(),
1443 },
1444 requested_k: 5,
1445 }),
1446 EffectKind::ArchivePageOut(ArchivePageOutEffect {
1447 handle_id: HandleId::new("handle-9").unwrap(),
1448 payload: PageOutPayload {
1449 content: "the full tool output".to_string(),
1450 digest: digest(),
1451 original_size: WireU64::new(262_144),
1452 preview: "the full…".to_string(),
1453 },
1454 }),
1455 EffectKind::LoadPayload(LoadPayloadEffect {
1456 handle_id: HandleId::new("handle-9").unwrap(),
1457 payload_ref: payload_ref(),
1458 }),
1459 EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
1460 request: MilestoneRequest {
1461 contract_id: "brief-quality-v1".to_string(),
1462 phase_id: "phase-2".to_string(),
1463 },
1464 }),
1465 ]
1466 }
1467
1468 fn success_samples() -> Vec<EffectSuccess> {
1470 vec![
1471 EffectSuccess::Provider(ProviderSuccess {
1472 outcome: ProviderOutcome::Completed(ProviderCompleted {
1473 message: ProviderMessage {
1474 role: MessageRole::Assistant,
1475 content: "done".to_string(),
1476 tool_calls: Vec::new(),
1477 tool_call_id: None,
1478 tokens: None,
1479 },
1480 observed_input_tokens: Some(120),
1481 observed_output_tokens: Some(8),
1482 stop_reason: Some(ProviderStopReason::EndTurn),
1483 }),
1484 }),
1485 EffectSuccess::Tools(ToolsSuccess {
1486 results: vec![
1487 ToolResultPayload::Inline(InlineToolResult {
1488 call_id: call_id("call-1"),
1489 result: ToolResult {
1490 output: "ok".to_string(),
1491 is_error: false,
1492 disposition: ToolResultDisposition::Recoverable,
1493 tokens: Some(2),
1494 },
1495 }),
1496 ToolResultPayload::External(ExternalToolResult {
1497 call_id: call_id("call-2"),
1498 payload_ref: payload_ref(),
1499 digest: digest(),
1500 original_size: WireU64::new(1_048_576),
1501 preview: "total 42".to_string(),
1502 is_error: false,
1503 disposition: ToolResultDisposition::Recoverable,
1504 }),
1505 ],
1506 }),
1507 EffectSuccess::Approval(ApprovalSuccess {
1508 approved_call_ids: vec![call_id("call-1")],
1509 denied_call_ids: vec![call_id("call-2")],
1510 }),
1511 EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
1512 attempts: vec![TaskLaunchOutcome {
1513 task_id: TaskId::new("task-1").unwrap(),
1514 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1515 outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
1516 }],
1517 }),
1518 EffectSuccess::TasksPreempted(TasksPreemptedSuccess {
1519 attempts: vec![TaskPreemptOutcome {
1520 task_id: TaskId::new("task-1").unwrap(),
1521 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1522 outcome: TaskPreemptStatus::Preempted(TaskPreempted {}),
1523 }],
1524 }),
1525 EffectSuccess::MemoryPersisted(MemoryPersistedSuccess {
1526 receipt: MemoryPersistReceipt {
1527 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1528 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0W").unwrap(),
1529 digest: digest(),
1530 },
1531 }),
1532 EffectSuccess::MemoryQueried(MemoryQueriedSuccess {
1533 recalls: vec![MemoryRecall {
1534 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0X").unwrap(),
1535 name: "release pipeline".to_string(),
1536 kind: MemoryKind::Project,
1537 content: "tag v* publishes".to_string(),
1538 score: Some(FiniteF64::new(0.82).unwrap()),
1539 }],
1540 }),
1541 EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
1542 receipt: ArchiveReceipt {
1543 handle_id: HandleId::new("handle-9").unwrap(),
1544 payload_ref: payload_ref(),
1545 digest: digest(),
1546 original_size: WireU64::new(262_144),
1547 },
1548 }),
1549 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
1550 handle_id: HandleId::new("handle-9").unwrap(),
1551 payload: InlinePayload {
1552 content: "the full tool output".to_string(),
1553 digest: digest(),
1554 original_size: WireU64::new(262_144),
1555 },
1556 }),
1557 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
1558 result: MilestoneCheckResult {
1559 phase_id: "phase-2".to_string(),
1560 passed: true,
1561 failed_criteria: Vec::new(),
1562 score: None,
1563 notes: String::new(),
1564 },
1565 }),
1566 ]
1567 }
1568
1569 fn kernel_effect(effect: EffectKind) -> KernelEffect {
1570 KernelEffect {
1571 effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1572 causation_input_id: InputId::new("in-1").unwrap(),
1573 effect,
1574 }
1575 }
1576
1577 #[test]
1582 fn the_effect_union_is_exactly_the_ten_host_executable_actions() {
1583 let tags: BTreeSet<&str> = EffectKindTag::ALL.iter().map(|tag| tag.as_str()).collect();
1584 assert_eq!(
1585 tags,
1586 BTreeSet::from([
1587 "call_provider",
1588 "execute_tools",
1589 "request_approval",
1590 "spawn_tasks",
1591 "preempt_tasks",
1592 "persist_memory",
1593 "query_memory",
1594 "archive_page_out",
1595 "load_payload",
1596 "evaluate_milestone",
1597 ])
1598 );
1599 assert_eq!(EffectKindTag::ALL.len(), 10);
1600
1601 let sampled: Vec<EffectKindTag> = effect_samples().iter().map(EffectKind::tag).collect();
1602 assert_eq!(
1603 sampled,
1604 EffectKindTag::ALL.to_vec(),
1605 "one sample per variant"
1606 );
1607 }
1608
1609 #[test]
1610 fn terminal_and_observation_shapes_are_not_effects() {
1611 for gone in [
1613 "done",
1614 "terminal",
1615 "compact",
1616 "sync_compact",
1617 "knowledge_sweep",
1618 "workflow_completed",
1619 "control_rejection",
1620 "signal_disposition",
1621 "budget_usage",
1622 "spool_large_result",
1623 ] {
1624 assert!(
1625 !EffectKindTag::ALL.iter().any(|tag| tag.as_str() == gone),
1626 "{gone} is a terminal or an observation, not an effect"
1627 );
1628 let raw = json!({ "kind": gone });
1629 assert!(serde_json::from_value::<EffectKind>(raw).is_err());
1630 }
1631 }
1632
1633 #[test]
1634 fn every_effect_carries_its_kernel_minted_id_and_causation() {
1635 for effect in effect_samples() {
1636 let value = serde_json::to_value(kernel_effect(effect)).unwrap();
1637 assert_eq!(value["effect_id"], json!("op-1:step:1:effect:0"));
1638 assert_eq!(value["causation_input_id"], json!("in-1"));
1639 }
1640 }
1641
1642 #[test]
1647 fn each_effect_kind_has_exactly_one_matching_success_kind() {
1648 let expected: Vec<EffectSuccessTag> = EffectKindTag::ALL
1649 .iter()
1650 .map(|tag| tag.expected_success())
1651 .collect();
1652 let distinct: BTreeSet<&str> = expected.iter().map(|tag| tag.as_str()).collect();
1653 assert_eq!(
1654 distinct.len(),
1655 EffectKindTag::ALL.len(),
1656 "the effect→success map must be a bijection"
1657 );
1658 assert_eq!(
1659 distinct,
1660 EffectSuccessTag::ALL
1661 .iter()
1662 .map(|tag| tag.as_str())
1663 .collect::<BTreeSet<&str>>()
1664 );
1665
1666 let sampled: Vec<EffectSuccessTag> =
1667 success_samples().iter().map(EffectSuccess::tag).collect();
1668 assert_eq!(
1669 sampled, expected,
1670 "samples must line up 1:1 with the effects"
1671 );
1672 }
1673
1674 #[test]
1675 fn a_resolution_of_the_wrong_kind_is_rejected_for_every_wrong_pair() {
1676 let effects = effect_samples();
1677 let successes = success_samples();
1678 for (i, effect) in effects.iter().enumerate() {
1679 let pending = kernel_effect(effect.clone());
1680 for (j, success) in successes.iter().enumerate() {
1681 let outcome = EffectOutcome::Succeeded(EffectSucceeded {
1682 result: success.clone(),
1683 });
1684 let verdict = pending.accept_outcome(&outcome);
1685 if i == j {
1686 assert!(
1687 verdict.is_ok(),
1688 "{:?} must accept its own success payload",
1689 effect.tag()
1690 );
1691 } else {
1692 let mismatch = verdict.expect_err("kind mismatch must be refused");
1693 assert_eq!(mismatch.effect_id, pending.effect_id);
1694 assert_eq!(mismatch.expected, effect.tag().expected_success());
1695 assert_eq!(mismatch.received, success.tag());
1696 }
1697 }
1698 }
1699 }
1700
1701 #[test]
1702 fn every_effect_accepts_the_same_host_failure_including_milestone() {
1703 let kinds = [
1704 HostEffectFailureKind::TransportExhausted,
1705 HostEffectFailureKind::ProtocolError,
1706 HostEffectFailureKind::StorageUnavailable,
1707 HostEffectFailureKind::PermissionDenied,
1708 HostEffectFailureKind::ResourceExhausted,
1709 HostEffectFailureKind::Unknown,
1710 ];
1711 assert_eq!(kinds.len(), 6, "§7.9 fixes six executor failure classes");
1712
1713 for effect in effect_samples() {
1714 let pending = kernel_effect(effect);
1715 for kind in kinds {
1716 let outcome = EffectOutcome::Failed(EffectFailed {
1717 failure: HostEffectFailure {
1718 kind,
1719 message: "boom".to_string(),
1720 retryable: None,
1721 },
1722 });
1723 assert!(
1724 pending.accept_outcome(&outcome).is_ok(),
1725 "{:?} must have the same failure path as every other effect",
1726 pending.effect.tag()
1727 );
1728 }
1729 }
1730 }
1731
1732 #[test]
1733 fn the_outcome_union_has_exactly_two_arms() {
1734 let arms: BTreeSet<String> = [
1735 EffectOutcome::Succeeded(EffectSucceeded {
1736 result: success_samples().remove(0),
1737 }),
1738 EffectOutcome::Failed(EffectFailed {
1739 failure: HostEffectFailure {
1740 kind: HostEffectFailureKind::Unknown,
1741 message: String::new(),
1742 retryable: None,
1743 },
1744 }),
1745 ]
1746 .iter()
1747 .map(|outcome| {
1748 serde_json::to_value(outcome).unwrap()["status"]
1749 .as_str()
1750 .unwrap()
1751 .to_string()
1752 })
1753 .collect();
1754 assert_eq!(
1755 arms,
1756 BTreeSet::from(["succeeded".to_string(), "failed".to_string()])
1757 );
1758
1759 for third in ["partial", "pending", "deferred", "succeeded_with_warnings"] {
1760 let raw = json!({ "status": third });
1761 assert!(
1762 serde_json::from_value::<EffectOutcome>(raw).is_err(),
1763 "{third} is not an outcome"
1764 );
1765 }
1766 }
1767
1768 #[test]
1769 fn the_kernel_never_retries_so_retryable_is_host_advice_only() {
1770 let with = json!({
1773 "status": "failed",
1774 "failure": { "kind": "transport_exhausted", "message": "429", "retryable": true },
1775 });
1776 let outcome: EffectOutcome = serde_json::from_value(with).unwrap();
1777 match outcome {
1778 EffectOutcome::Failed(failed) => {
1779 assert_eq!(failed.failure.retryable, Some(true));
1780 assert_eq!(
1781 failed.failure.kind,
1782 HostEffectFailureKind::TransportExhausted
1783 );
1784 }
1785 EffectOutcome::Succeeded(_) => panic!("failed outcome decoded as succeeded"),
1786 }
1787 }
1788
1789 #[test]
1799 fn every_provider_family_maps_onto_the_canonical_stop_reason_vocabulary() {
1800 let canonical: BTreeSet<&str> = ProviderStopReason::ALL
1801 .iter()
1802 .map(|reason| reason.as_str())
1803 .collect();
1804 assert_eq!(
1805 canonical,
1806 BTreeSet::from([
1807 "end_turn",
1808 "tool_use",
1809 "max_tokens",
1810 "stop_sequence",
1811 "content_filter",
1812 "other",
1813 ])
1814 );
1815 for reason in ProviderStopReason::ALL {
1816 let decoded: ProviderStopReason =
1817 serde_json::from_value(json!(reason.as_str())).unwrap();
1818 assert_eq!(decoded, reason);
1819 }
1820
1821 for vendor_word in [
1823 "stop", "length", "tool_calls", "function_call", "content_filter", "STOP", "MAX_TOKENS", "SAFETY", "FINISH_REASON_STOP", "eos", "eos_token", "sensitive", "insufficient_system_resource",
1836 ] {
1837 let decoded = serde_json::from_value::<ProviderStopReason>(json!(vendor_word));
1838 if vendor_word == "content_filter" {
1839 assert!(decoded.is_ok(), "content_filter *is* canonical");
1840 continue;
1841 }
1842 assert!(
1843 decoded.is_err(),
1844 "{vendor_word:?} is a vendor spelling; the host maps it, core never learns it"
1845 );
1846 }
1847
1848 let value = serde_json::to_value(ProviderStopReason::Other).unwrap();
1850 assert_eq!(value, json!("other"));
1851 assert!(
1852 serde_json::from_value::<ProviderStopReason>(
1853 json!({ "kind": "other", "raw": "insufficient_system_resource" })
1854 )
1855 .is_err(),
1856 "`other` is not a pass-through for vendor text"
1857 );
1858 }
1859
1860 #[test]
1867 fn no_vendor_failure_vocabulary_is_expressible_on_the_canonical_face() {
1868 assert_eq!(HostEffectFailureKind::ALL.len(), 6);
1869 let canonical: BTreeSet<&str> = HostEffectFailureKind::ALL
1870 .iter()
1871 .map(|kind| kind.as_str())
1872 .collect();
1873 assert_eq!(
1874 canonical,
1875 BTreeSet::from([
1876 "transport_exhausted",
1877 "protocol_error",
1878 "storage_unavailable",
1879 "permission_denied",
1880 "resource_exhausted",
1881 "unknown",
1882 ])
1883 );
1884
1885 for absent in [
1886 "rate_limited",
1887 "rate_limit_exceeded",
1888 "too_many_requests",
1889 "overloaded",
1890 "service_unavailable",
1891 "server_error",
1892 "timeout",
1893 "context_length_exceeded",
1894 "context_overflow",
1895 "cancelled",
1896 "canceled",
1897 "aborted",
1898 "user_interrupt",
1899 "interrupted",
1900 ] {
1901 assert!(
1902 serde_json::from_value::<HostEffectFailureKind>(json!(absent)).is_err(),
1903 "{absent:?} must not be an executor failure class"
1904 );
1905 assert!(
1906 !canonical.contains(absent),
1907 "{absent:?} leaked into the canonical vocabulary"
1908 );
1909 }
1910
1911 let spent: EffectOutcome = serde_json::from_value(json!({
1913 "status": "failed",
1914 "failure": {
1915 "kind": "transport_exhausted",
1916 "message": "5 attempts over 41s",
1917 "retryable": false,
1918 },
1919 }))
1920 .unwrap();
1921 let EffectOutcome::Failed(failed) = spent else {
1922 panic!("a spent ladder is a failure");
1923 };
1924 assert_eq!(
1925 failed.failure.kind,
1926 HostEffectFailureKind::TransportExhausted
1927 );
1928 }
1929
1930 #[test]
1935 fn retryable_is_uniformly_optional_advice_across_all_six_failure_kinds() {
1936 for kind in HostEffectFailureKind::ALL {
1937 for retryable in [None, Some(true), Some(false)] {
1938 let mut failure = json!({ "kind": kind.as_str(), "message": "boom" });
1939 if let Some(flag) = retryable {
1940 failure
1941 .as_object_mut()
1942 .unwrap()
1943 .insert("retryable".to_string(), json!(flag));
1944 }
1945 let outcome: EffectOutcome =
1946 serde_json::from_value(json!({ "status": "failed", "failure": failure }))
1947 .unwrap();
1948 let EffectOutcome::Failed(failed) = outcome else {
1949 panic!("failure decoded as success");
1950 };
1951 assert_eq!(failed.failure.kind, kind);
1952 assert_eq!(failed.failure.retryable, retryable);
1953 }
1954 }
1955 }
1956
1957 #[test]
1962 fn a_tool_result_must_state_whether_the_batch_can_continue() {
1963 assert_eq!(
1965 decode_effect(json!({
1966 "status": "succeeded",
1967 "result": { "kind": "tools", "results": [
1968 { "kind": "inline", "call_id": "c-1", "result": { "output": "ok" } }] },
1969 }))
1970 .unwrap_err()
1971 .kind,
1972 WireRejectionKind::MissingField
1973 );
1974
1975 assert_eq!(
1977 ToolResultDisposition::ALL
1978 .iter()
1979 .map(|d| d.as_str())
1980 .collect::<BTreeSet<&str>>(),
1981 BTreeSet::from(["recoverable", "fatal"])
1982 );
1983 for value in ["recoverable", "fatal"] {
1984 let decoded: ToolResultDisposition = serde_json::from_value(json!(value)).expect(value);
1985 assert_eq!(decoded.as_str(), value);
1986 assert_eq!(decoded.is_fatal(), value == "fatal");
1987 }
1988
1989 for gone in [
1993 "user_interrupt",
1994 "governance_denied",
1995 "provider_failure",
1996 "timeout",
1997 "cancelled",
1998 ] {
1999 assert!(
2000 serde_json::from_value::<ToolResultDisposition>(json!(gone)).is_err(),
2001 "{gone:?} is not a tool-result disposition"
2002 );
2003 }
2004 }
2005
2006 #[test]
2012 fn both_residency_arms_state_the_same_two_failure_facts() {
2013 let inline = ToolResultPayload::Inline(InlineToolResult {
2014 call_id: call_id("call-1"),
2015 result: ToolResult {
2016 output: "boom".to_string(),
2017 is_error: true,
2018 disposition: ToolResultDisposition::Fatal,
2019 tokens: None,
2020 },
2021 });
2022 let external = ToolResultPayload::External(ExternalToolResult {
2023 call_id: call_id("call-2"),
2024 payload_ref: payload_ref(),
2025 digest: digest(),
2026 original_size: WireU64::new(1_048_576),
2027 preview: "Traceback (most recent call last):".to_string(),
2028 is_error: true,
2029 disposition: ToolResultDisposition::Fatal,
2030 });
2031 for payload in [&inline, &external] {
2032 assert!(payload.is_error(), "{payload:?}");
2033 assert_eq!(payload.disposition(), ToolResultDisposition::Fatal);
2034 assert!(payload.disposition().is_fatal());
2035 }
2036
2037 assert_eq!(
2040 decode_effect(json!({
2041 "status": "succeeded",
2042 "result": { "kind": "tools", "results": [
2043 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2044 "digest": "sha256:ab", "original_size": "1", "preview": "x" }] },
2045 }))
2046 .unwrap_err()
2047 .kind,
2048 WireRejectionKind::MissingField
2049 );
2050
2051 let ok: EffectOutcome = serde_json::from_value(json!({
2054 "status": "succeeded",
2055 "result": { "kind": "tools", "results": [
2056 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2057 "digest": "sha256:ab", "original_size": "1", "preview": "x",
2058 "disposition": "recoverable" }] },
2059 }))
2060 .unwrap();
2061 let EffectOutcome::Succeeded(success) = &ok else {
2062 panic!("not a success");
2063 };
2064 let EffectSuccess::Tools(tools) = &success.result else {
2065 panic!("not a tools success");
2066 };
2067 assert!(!tools.results[0].is_error());
2068 let value = serde_json::to_value(&tools.results[0]).unwrap();
2069 assert!(value.get("is_error").is_none(), "false stays off the wire");
2070 assert_eq!(value["disposition"], json!("recoverable"));
2071 }
2072
2073 #[test]
2075 fn a_milestone_request_is_exactly_the_contract_and_phase_pair() {
2076 let request = MilestoneRequest {
2077 contract_id: "brief-quality-v1".to_string(),
2078 phase_id: "collect".to_string(),
2079 };
2080 let value = serde_json::to_value(&request).unwrap();
2081 assert_eq!(
2082 value.as_object().unwrap().keys().collect::<Vec<_>>(),
2083 vec!["contract_id", "phase_id"],
2084 "a phase id is unique only inside its contract, so the pair is the whole key"
2085 );
2086
2087 for host_owned in ["criteria", "required_evidence", "verifier", "evidence"] {
2091 let mut with_extra = value.clone();
2092 with_extra
2093 .as_object_mut()
2094 .unwrap()
2095 .insert(host_owned.to_string(), json!([]));
2096 assert!(
2097 serde_json::from_value::<MilestoneRequest>(with_extra).is_err(),
2098 "{host_owned} must not decode"
2099 );
2100 }
2101 for missing in ["contract_id", "phase_id"] {
2102 let mut without = value.clone();
2103 without.as_object_mut().unwrap().remove(missing);
2104 assert!(
2105 serde_json::from_value::<MilestoneRequest>(without).is_err(),
2106 "{missing} is half the key and cannot be omitted"
2107 );
2108 }
2109 }
2110
2111 #[test]
2116 fn no_effect_outcome_payload_carries_a_host_wall_clock() {
2117 const BANNED: [&str; 8] = [
2118 "now_ms",
2119 "observed_at_ms",
2120 "timestamp",
2121 "timestamp_ms",
2122 "started_at_ms",
2123 "completed_at_ms",
2124 "wall_clock_ms",
2125 "received_at_ms",
2126 ];
2127
2128 let mut outcomes: Vec<EffectOutcome> = success_samples()
2129 .into_iter()
2130 .map(|result| EffectOutcome::Succeeded(EffectSucceeded { result }))
2131 .collect();
2132 outcomes.push(EffectOutcome::Failed(EffectFailed {
2133 failure: HostEffectFailure {
2134 kind: HostEffectFailureKind::TransportExhausted,
2135 message: "socket hang up".to_string(),
2136 retryable: Some(false),
2137 },
2138 }));
2139
2140 for outcome in outcomes {
2141 let value = serde_json::to_value(&outcome).unwrap();
2142 let mut all = BTreeSet::new();
2143 keys(&value, &mut all);
2144 for banned in BANNED {
2145 assert!(
2146 !all.contains(banned),
2147 "outcome payload must not carry {banned:?}: {value}"
2148 );
2149 }
2150 }
2151 }
2152
2153 #[test]
2158 fn an_external_tool_result_is_a_handle_with_a_digest_size_and_preview() {
2159 let external = ToolResultPayload::External(ExternalToolResult {
2160 call_id: call_id("call-2"),
2161 payload_ref: payload_ref(),
2162 digest: digest(),
2163 original_size: WireU64::new(1_048_576),
2164 preview: "total 42".to_string(),
2165 is_error: false,
2166 disposition: ToolResultDisposition::Recoverable,
2167 });
2168 let value = serde_json::to_value(&external).unwrap();
2169 assert_eq!(value["kind"], json!("external"));
2170 for required in ["payload_ref", "digest", "original_size", "preview"] {
2171 assert!(value.get(required).is_some(), "external needs {required}");
2172 }
2173 assert_eq!(
2174 value["original_size"],
2175 json!("1048576"),
2176 "sizes are decimal-string u64, not JS numbers"
2177 );
2178
2179 let mut all = BTreeSet::new();
2181 keys(&value, &mut all);
2182 for banned in ["path", "file_path", "spool_ref", "spool_dir", "archive_ref"] {
2183 assert!(!all.contains(banned), "payload ref must stay opaque");
2184 }
2185 }
2186
2187 #[test]
2188 fn payload_residency_distinguishes_generated_over_limit_from_pressure_archival() {
2189 let residencies = [
2190 PayloadResidency::Resident(ResidentPayload {}),
2191 PayloadResidency::External(ExternalResidency {
2192 payload_ref: payload_ref(),
2193 digest: digest(),
2194 original_size: WireU64::new(1_048_576),
2195 }),
2196 PayloadResidency::PagedOut(PagedOutResidency {
2197 payload_ref: payload_ref(),
2198 digest: digest(),
2199 }),
2200 PayloadResidency::Collapsed(CollapsedPayload {}),
2201 ];
2202 let tags: BTreeSet<String> = residencies
2203 .iter()
2204 .map(|residency| {
2205 serde_json::to_value(residency).unwrap()["kind"]
2206 .as_str()
2207 .unwrap()
2208 .to_string()
2209 })
2210 .collect();
2211 assert_eq!(
2212 tags,
2213 BTreeSet::from([
2214 "resident".to_string(),
2215 "external".to_string(),
2216 "paged_out".to_string(),
2217 "collapsed".to_string(),
2218 ]),
2219 "§7.10 keeps `external` (generated over limit) apart from `paged_out` (pressure)"
2220 );
2221
2222 let paged = serde_json::to_value(&residencies[2]).unwrap();
2224 assert!(paged.get("original_size").is_none());
2225 }
2226
2227 #[test]
2232 fn unknown_success_kinds_fields_and_variants_are_rejected() {
2233 assert_eq!(
2235 decode_effect(json!({ "status": "succeeded", "result": { "kind": "spooled" } }))
2236 .unwrap_err()
2237 .kind,
2238 WireRejectionKind::UnknownVariant
2239 );
2240 assert_eq!(
2242 decode_effect(json!({ "status": "failed", "failure": { "kind": "rate_limited" } }))
2243 .unwrap_err()
2244 .kind,
2245 WireRejectionKind::UnknownVariant
2246 );
2247 assert_eq!(
2249 decode_effect(json!({
2250 "status": "succeeded",
2251 "result": { "kind": "approval" },
2252 "now_ms": 1,
2253 }))
2254 .unwrap_err()
2255 .kind,
2256 WireRejectionKind::UnknownField
2257 );
2258 assert_eq!(
2260 decode_effect(json!({
2261 "status": "succeeded",
2262 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2263 "payload": { "content": "x", "digest": "sha256:ab",
2264 "original_size": "1", "path": "/tmp/x" } },
2265 }))
2266 .unwrap_err()
2267 .kind,
2268 WireRejectionKind::UnknownField
2269 );
2270 assert_eq!(
2272 decode_effect(json!({
2273 "status": "succeeded",
2274 "result": { "kind": "tools", "results": [
2275 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2276 "original_size": "1", "preview": "x" }] },
2277 }))
2278 .unwrap_err()
2279 .kind,
2280 WireRejectionKind::MissingField
2281 );
2282 assert_eq!(
2284 decode_effect(json!({
2285 "status": "succeeded",
2286 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2287 "payload": { "content": "x", "digest": "sha256:ab",
2288 "original_size": 1 } },
2289 }))
2290 .unwrap_err()
2291 .kind,
2292 WireRejectionKind::InvalidScalar
2293 );
2294 }
2295
2296 #[test]
2297 fn every_effect_and_success_payload_denies_unknown_fields() {
2298 for effect in effect_samples() {
2299 let mut value = serde_json::to_value(&effect).unwrap();
2300 value
2301 .as_object_mut()
2302 .unwrap()
2303 .insert("host_hint".to_string(), json!("x"));
2304 assert!(
2305 serde_json::from_value::<EffectKind>(value).is_err(),
2306 "{:?} must deny unknown fields",
2307 effect.tag()
2308 );
2309 }
2310 for success in success_samples() {
2311 let mut value = serde_json::to_value(&success).unwrap();
2312 value
2313 .as_object_mut()
2314 .unwrap()
2315 .insert("host_hint".to_string(), json!("x"));
2316 assert!(
2317 serde_json::from_value::<EffectSuccess>(value).is_err(),
2318 "{:?} must deny unknown fields",
2319 success.tag()
2320 );
2321 }
2322 }
2323
2324 #[test]
2329 fn resolve_effect_goldens_cover_every_success_kind_and_the_failure_path() {
2330 let mut success_kinds: BTreeSet<String> = BTreeSet::new();
2331 let mut failure_kinds: BTreeSet<String> = BTreeSet::new();
2332
2333 for (name, fixture) in fixtures_with_prefix("input_resolve_") {
2334 let text = serde_json::to_string(&fixture).unwrap();
2335 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2336 .unwrap_or_else(|e| panic!("{name}: {e}"));
2337 assert_eq!(
2338 serde_json::to_value(&envelope).unwrap(),
2339 fixture,
2340 "{name}: round-trip changed the document"
2341 );
2342
2343 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2344 panic!("{name} is not a resolve_effect golden");
2345 };
2346 match &resolve.outcome {
2347 EffectOutcome::Succeeded(ok) => {
2348 success_kinds.insert(ok.result.tag().as_str().to_string());
2349 }
2350 EffectOutcome::Failed(failed) => {
2351 failure_kinds.insert(failed.failure.kind.as_str().to_string());
2352 }
2353 }
2354 }
2355
2356 assert_eq!(
2357 success_kinds,
2358 EffectSuccessTag::ALL
2359 .iter()
2360 .map(|tag| tag.as_str().to_string())
2361 .collect::<BTreeSet<String>>(),
2362 "every effect kind needs at least one resolve golden"
2363 );
2364 assert!(
2365 !failure_kinds.is_empty(),
2366 "the unified failure path needs a golden too"
2367 );
2368 }
2369
2370 #[test]
2371 fn the_milestone_effect_has_a_failure_channel_like_every_other_effect() {
2372 let golden = fixtures_with_prefix("input_resolve_effect_milestone_failed");
2374 assert_eq!(golden.len(), 1);
2375 let text = serde_json::to_string(&golden[0].1).unwrap();
2376 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default()).unwrap();
2377 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2378 panic!("not a resolve_effect golden");
2379 };
2380 let EffectOutcome::Failed(failed) = &resolve.outcome else {
2381 panic!("milestone failure golden must be a Failed outcome");
2382 };
2383 assert_eq!(
2384 failed.failure.kind,
2385 HostEffectFailureKind::StorageUnavailable
2386 );
2387
2388 let milestone = kernel_effect(EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
2389 request: MilestoneRequest {
2390 contract_id: "brief-quality-v1".to_string(),
2391 phase_id: "phase-2".to_string(),
2392 },
2393 }));
2394 assert!(milestone.accept_outcome(&resolve.outcome).is_ok());
2395 }
2396
2397 #[test]
2398 fn effect_rejection_goldens_cover_the_payload_failure_modes() {
2399 let mut expected: BTreeSet<String> = BTreeSet::new();
2400 for (name, fixture) in fixtures_with_prefix("reject_effect_") {
2401 let kind = fixture["expect"]
2402 .as_str()
2403 .unwrap_or_else(|| panic!("{name}: missing `expect`"));
2404 let text = serde_json::to_string(&fixture["envelope"]).unwrap();
2405 let rejection = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2406 .map(|ok| panic!("{name}: expected rejection, decoded {ok:?}"))
2407 .unwrap_err();
2408 assert_eq!(
2409 rejection.kind.as_str(),
2410 kind,
2411 "{name}: {}",
2412 rejection.message
2413 );
2414 expected.insert(kind.to_string());
2415 }
2416 for required in [
2417 "unknown_field",
2418 "unknown_variant",
2419 "missing_field",
2420 "invalid_scalar",
2421 ] {
2422 assert!(
2423 expected.contains(required),
2424 "effect rejection goldens must cover {required}"
2425 );
2426 }
2427 }
2428}