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)]
507#[serde(deny_unknown_fields)]
508pub struct ToolCall {
509 pub call_id: CallId,
510 pub name: String,
511 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
512 pub arguments: BoundedJson,
513}
514
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516#[serde(deny_unknown_fields)]
517pub struct ApprovalRequest {
518 pub call_id: CallId,
519 pub tool_name: String,
520 #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
521 pub arguments: BoundedJson,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub reason: Option<String>,
524}
525
526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
532#[serde(deny_unknown_fields)]
533pub struct TaskLaunch {
534 pub task_id: TaskId,
535 pub attempt_id: AttemptId,
536 pub launch_token: LaunchToken,
537 pub node_id: NodeId,
538 pub spec: LogicalAgentSpec,
539}
540
541#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
542#[serde(deny_unknown_fields)]
543pub struct WorkflowBudget {
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub max_total_tokens: Option<WireU64>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub max_turns: Option<u32>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub max_concurrency: Option<u32>,
550}
551
552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
553#[serde(deny_unknown_fields)]
554pub struct TaskAttemptRef {
555 pub task_id: TaskId,
556 pub attempt_id: AttemptId,
557}
558
559#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct MemoryAccessBinding {
564 pub binding_id: MemoryBindingId,
565 pub capabilities: MemoryCapabilities,
566}
567
568#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
569#[serde(deny_unknown_fields)]
570pub struct MemoryCapabilities {
571 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
572 pub read: bool,
573 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
574 pub write: bool,
575}
576
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
584#[serde(deny_unknown_fields)]
585pub struct CanonicalMemoryWrite {
586 pub name: String,
587 pub kind: MemoryKind,
588 pub content: String,
589 #[serde(default, skip_serializing_if = "String::is_empty")]
590 pub description: String,
591 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 pub evidence_refs: Vec<String>,
593 pub accepted_at_ms: WireU64,
594 pub causation: SyscallCausation,
595}
596
597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598#[serde(deny_unknown_fields)]
599pub struct CanonicalMemoryQuery {
600 #[serde(default, skip_serializing_if = "String::is_empty")]
601 pub text: String,
602 #[serde(default, skip_serializing_if = "Vec::is_empty")]
603 pub kinds: Vec<MemoryKind>,
604 pub accepted_at_ms: WireU64,
605 pub causation: SyscallCausation,
606}
607
608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611#[serde(deny_unknown_fields)]
612pub struct PageOutPayload {
613 pub content: String,
614 pub digest: Digest,
615 pub original_size: WireU64,
616 #[serde(default, skip_serializing_if = "String::is_empty")]
617 pub preview: String,
618}
619
620#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
628#[serde(deny_unknown_fields)]
629pub struct MilestoneRequest {
630 pub contract_id: String,
631 pub phase_id: String,
632}
633
634#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644#[serde(tag = "status", rename_all = "snake_case")]
645pub enum EffectOutcome {
646 Succeeded(EffectSucceeded),
647 Failed(EffectFailed),
648}
649
650impl EffectOutcome {
651 pub fn success_tag(&self) -> Option<EffectSuccessTag> {
653 match self {
654 Self::Succeeded(success) => Some(success.result.tag()),
655 Self::Failed(_) => None,
656 }
657 }
658}
659
660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
663#[serde(deny_unknown_fields)]
664pub struct EffectSucceeded {
665 pub result: EffectSuccess,
666}
667
668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
670#[serde(deny_unknown_fields)]
671pub struct EffectFailed {
672 pub failure: HostEffectFailure,
673}
674
675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
679#[serde(deny_unknown_fields)]
680pub struct HostEffectFailure {
681 pub kind: HostEffectFailureKind,
682 #[serde(default, skip_serializing_if = "String::is_empty")]
683 pub message: String,
684 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub retryable: Option<bool>,
700}
701
702#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
721#[serde(rename_all = "snake_case")]
722pub enum HostEffectFailureKind {
723 TransportExhausted,
726 ProtocolError,
731 StorageUnavailable,
732 PermissionDenied,
733 ResourceExhausted,
734 Unknown,
735}
736
737impl HostEffectFailureKind {
738 pub const ALL: [Self; 6] = [
739 Self::TransportExhausted,
740 Self::ProtocolError,
741 Self::StorageUnavailable,
742 Self::PermissionDenied,
743 Self::ResourceExhausted,
744 Self::Unknown,
745 ];
746
747 pub fn as_str(self) -> &'static str {
748 match self {
749 Self::TransportExhausted => "transport_exhausted",
750 Self::ProtocolError => "protocol_error",
751 Self::StorageUnavailable => "storage_unavailable",
752 Self::PermissionDenied => "permission_denied",
753 Self::ResourceExhausted => "resource_exhausted",
754 Self::Unknown => "unknown",
755 }
756 }
757}
758
759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
761#[serde(tag = "kind", rename_all = "snake_case")]
762pub enum EffectSuccess {
763 Provider(ProviderSuccess),
764 Tools(ToolsSuccess),
765 Approval(ApprovalSuccess),
766 TasksSpawned(TasksSpawnedSuccess),
767 TasksPreempted(TasksPreemptedSuccess),
768 MemoryPersisted(MemoryPersistedSuccess),
769 MemoryQueried(MemoryQueriedSuccess),
770 PageOutArchived(PageOutArchivedSuccess),
771 PayloadLoaded(PayloadLoadedSuccess),
772 MilestoneEvaluated(MilestoneEvaluatedSuccess),
773 PromptMeasured(PromptMeasuredSuccess),
775}
776
777impl EffectSuccess {
778 pub fn tag(&self) -> EffectSuccessTag {
779 match self {
780 Self::Provider(_) => EffectSuccessTag::Provider,
781 Self::Tools(_) => EffectSuccessTag::Tools,
782 Self::Approval(_) => EffectSuccessTag::Approval,
783 Self::TasksSpawned(_) => EffectSuccessTag::TasksSpawned,
784 Self::TasksPreempted(_) => EffectSuccessTag::TasksPreempted,
785 Self::MemoryPersisted(_) => EffectSuccessTag::MemoryPersisted,
786 Self::MemoryQueried(_) => EffectSuccessTag::MemoryQueried,
787 Self::PageOutArchived(_) => EffectSuccessTag::PageOutArchived,
788 Self::PayloadLoaded(_) => EffectSuccessTag::PayloadLoaded,
789 Self::MilestoneEvaluated(_) => EffectSuccessTag::MilestoneEvaluated,
790 Self::PromptMeasured(_) => EffectSuccessTag::PromptMeasured,
791 }
792 }
793}
794
795#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
797#[serde(rename_all = "snake_case")]
798pub enum EffectSuccessTag {
799 Provider,
800 Tools,
801 Approval,
802 TasksSpawned,
803 TasksPreempted,
804 MemoryPersisted,
805 MemoryQueried,
806 PageOutArchived,
807 PayloadLoaded,
808 MilestoneEvaluated,
809 PromptMeasured,
811}
812
813impl EffectSuccessTag {
814 pub const ALL: [Self; 11] = [
815 Self::Provider,
816 Self::Tools,
817 Self::Approval,
818 Self::TasksSpawned,
819 Self::TasksPreempted,
820 Self::MemoryPersisted,
821 Self::MemoryQueried,
822 Self::PageOutArchived,
823 Self::PayloadLoaded,
824 Self::MilestoneEvaluated,
825 Self::PromptMeasured,
826 ];
827
828 pub fn as_str(self) -> &'static str {
829 match self {
830 Self::Provider => "provider",
831 Self::Tools => "tools",
832 Self::Approval => "approval",
833 Self::TasksSpawned => "tasks_spawned",
834 Self::TasksPreempted => "tasks_preempted",
835 Self::MemoryPersisted => "memory_persisted",
836 Self::MemoryQueried => "memory_queried",
837 Self::PageOutArchived => "page_out_archived",
838 Self::PayloadLoaded => "payload_loaded",
839 Self::MilestoneEvaluated => "milestone_evaluated",
840 Self::PromptMeasured => "prompt_measured",
841 }
842 }
843
844 pub fn resolves(self) -> EffectKindTag {
847 match self {
848 Self::Provider => EffectKindTag::CallProvider,
849 Self::Tools => EffectKindTag::ExecuteTools,
850 Self::Approval => EffectKindTag::RequestApproval,
851 Self::TasksSpawned => EffectKindTag::SpawnTasks,
852 Self::TasksPreempted => EffectKindTag::PreemptTasks,
853 Self::MemoryPersisted => EffectKindTag::PersistMemory,
854 Self::MemoryQueried => EffectKindTag::QueryMemory,
855 Self::PageOutArchived => EffectKindTag::ArchivePageOut,
856 Self::PayloadLoaded => EffectKindTag::LoadPayload,
857 Self::MilestoneEvaluated => EffectKindTag::EvaluateMilestone,
858 Self::PromptMeasured => EffectKindTag::MeasurePrompt,
859 }
860 }
861}
862
863impl fmt::Display for EffectSuccessTag {
864 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
865 f.write_str(self.as_str())
866 }
867}
868
869#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
874#[serde(deny_unknown_fields)]
875pub struct ProviderSuccess {
876 pub outcome: ProviderOutcome,
877}
878
879#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
884#[serde(tag = "kind", rename_all = "snake_case")]
885pub enum ProviderOutcome {
886 Completed(ProviderCompleted),
887 ContextOverflow(ProviderContextOverflow),
891}
892
893#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
894#[serde(deny_unknown_fields)]
895pub struct ProviderCompleted {
896 pub message: ProviderMessage,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub observed_input_tokens: Option<u32>,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
900 pub observed_output_tokens: Option<u32>,
901 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub stop_reason: Option<ProviderStopReason>,
903}
904
905#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
906#[serde(deny_unknown_fields)]
907pub struct ProviderContextOverflow {
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub observed_input_tokens: Option<u32>,
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
921#[serde(rename_all = "snake_case")]
922pub enum ProviderStopReason {
923 EndTurn,
924 ToolUse,
925 MaxTokens,
926 StopSequence,
927 ContentFilter,
928 Other,
929}
930
931impl ProviderStopReason {
932 pub const ALL: [Self; 6] = [
933 Self::EndTurn,
934 Self::ToolUse,
935 Self::MaxTokens,
936 Self::StopSequence,
937 Self::ContentFilter,
938 Self::Other,
939 ];
940
941 pub fn as_str(self) -> &'static str {
942 match self {
943 Self::EndTurn => "end_turn",
944 Self::ToolUse => "tool_use",
945 Self::MaxTokens => "max_tokens",
946 Self::StopSequence => "stop_sequence",
947 Self::ContentFilter => "content_filter",
948 Self::Other => "other",
949 }
950 }
951}
952
953impl fmt::Display for ProviderStopReason {
954 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
955 f.write_str(self.as_str())
956 }
957}
958
959#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
960#[serde(deny_unknown_fields)]
961pub struct ToolsSuccess {
962 pub results: Vec<ToolResultPayload>,
963}
964
965#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
966#[serde(deny_unknown_fields)]
967pub struct ApprovalSuccess {
968 #[serde(default, skip_serializing_if = "Vec::is_empty")]
969 pub approved_call_ids: Vec<CallId>,
970 #[serde(default, skip_serializing_if = "Vec::is_empty")]
971 pub denied_call_ids: Vec<CallId>,
972}
973
974#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
975#[serde(deny_unknown_fields)]
976pub struct TasksSpawnedSuccess {
977 pub attempts: Vec<TaskLaunchOutcome>,
978}
979
980#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
983#[serde(deny_unknown_fields)]
984pub struct TaskLaunchOutcome {
985 pub task_id: TaskId,
986 pub attempt_id: AttemptId,
987 pub outcome: TaskLaunchStatus,
988}
989
990#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
991#[serde(tag = "status", rename_all = "snake_case")]
992pub enum TaskLaunchStatus {
993 Started(TaskLaunchStarted),
994 Failed(TaskLaunchFailed),
995}
996
997#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
998#[serde(deny_unknown_fields)]
999pub struct TaskLaunchStarted {}
1000
1001#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1002#[serde(deny_unknown_fields)]
1003pub struct TaskLaunchFailed {
1004 pub failure: TaskLaunchFailure,
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1010#[serde(deny_unknown_fields)]
1011pub struct TaskLaunchFailure {
1012 pub kind: HostEffectFailureKind,
1013 #[serde(default, skip_serializing_if = "String::is_empty")]
1014 pub message: String,
1015}
1016
1017#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1018#[serde(deny_unknown_fields)]
1019pub struct TasksPreemptedSuccess {
1020 pub attempts: Vec<TaskPreemptOutcome>,
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1024#[serde(deny_unknown_fields)]
1025pub struct TaskPreemptOutcome {
1026 pub task_id: TaskId,
1027 pub attempt_id: AttemptId,
1028 pub outcome: TaskPreemptStatus,
1029}
1030
1031#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1032#[serde(tag = "status", rename_all = "snake_case")]
1033pub enum TaskPreemptStatus {
1034 Preempted(TaskPreempted),
1035 AlreadyFinished(TaskAlreadyFinished),
1038 Failed(TaskPreemptFailed),
1039}
1040
1041#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1042#[serde(deny_unknown_fields)]
1043pub struct TaskPreempted {}
1044
1045#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1046#[serde(deny_unknown_fields)]
1047pub struct TaskAlreadyFinished {}
1048
1049#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1050#[serde(deny_unknown_fields)]
1051pub struct TaskPreemptFailed {
1052 pub failure: TaskLaunchFailure,
1053}
1054
1055#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1056#[serde(deny_unknown_fields)]
1057pub struct MemoryPersistedSuccess {
1058 pub receipt: MemoryPersistReceipt,
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1062#[serde(deny_unknown_fields)]
1063pub struct MemoryPersistReceipt {
1064 pub binding_id: MemoryBindingId,
1065 pub record_ref: MemoryRecordRef,
1066 pub digest: Digest,
1067}
1068
1069#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1070#[serde(deny_unknown_fields)]
1071pub struct MemoryQueriedSuccess {
1072 pub recalls: Vec<MemoryRecall>,
1073}
1074
1075#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1076#[serde(deny_unknown_fields)]
1077pub struct MemoryRecall {
1078 pub record_ref: MemoryRecordRef,
1079 pub name: String,
1080 pub kind: MemoryKind,
1081 pub content: String,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub score: Option<FiniteF64>,
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089#[serde(deny_unknown_fields)]
1090pub struct PageOutArchivedSuccess {
1091 pub receipt: ArchiveReceipt,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1095#[serde(deny_unknown_fields)]
1096pub struct ArchiveReceipt {
1097 pub handle_id: HandleId,
1098 pub payload_ref: PayloadRef,
1099 pub digest: Digest,
1100 pub original_size: WireU64,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1104#[serde(deny_unknown_fields)]
1105pub struct PayloadLoadedSuccess {
1106 pub handle_id: HandleId,
1107 pub payload: InlinePayload,
1108}
1109
1110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct InlinePayload {
1115 pub content: String,
1116 pub digest: Digest,
1117 pub original_size: WireU64,
1118}
1119
1120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1121#[serde(deny_unknown_fields)]
1122pub struct MilestoneEvaluatedSuccess {
1123 pub result: MilestoneCheckResult,
1124}
1125
1126#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1130#[serde(deny_unknown_fields)]
1131pub struct MilestoneCheckResult {
1132 pub phase_id: String,
1133 pub passed: bool,
1134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1135 pub failed_criteria: Vec<String>,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub score: Option<FiniteF64>,
1138 #[serde(default, skip_serializing_if = "String::is_empty")]
1139 pub notes: String,
1140}
1141
1142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1145#[serde(deny_unknown_fields)]
1146pub struct PromptMeasuredSuccess {
1147 pub measurement: PromptMeasurement,
1148}
1149
1150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1162#[serde(tag = "kind", rename_all = "snake_case")]
1163pub enum ToolResultPayload {
1164 Inline(InlineToolResult),
1165 External(ExternalToolResult),
1166}
1167
1168impl ToolResultPayload {
1169 pub fn call_id(&self) -> &CallId {
1170 match self {
1171 Self::Inline(inline) => &inline.call_id,
1172 Self::External(external) => &external.call_id,
1173 }
1174 }
1175
1176 pub fn disposition(&self) -> ToolResultDisposition {
1181 match self {
1182 Self::Inline(inline) => inline.result.disposition,
1183 Self::External(external) => external.disposition,
1184 }
1185 }
1186
1187 pub fn is_error(&self) -> bool {
1188 match self {
1189 Self::Inline(inline) => inline.result.is_error,
1190 Self::External(external) => external.is_error,
1191 }
1192 }
1193}
1194
1195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1196#[serde(deny_unknown_fields)]
1197pub struct InlineToolResult {
1198 pub call_id: CallId,
1199 pub result: ToolResult,
1200}
1201
1202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1210#[serde(deny_unknown_fields)]
1211pub struct ExternalToolResult {
1212 pub call_id: CallId,
1213 pub payload_ref: PayloadRef,
1215 pub digest: Digest,
1216 pub original_size: WireU64,
1217 pub preview: String,
1219 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1220 pub is_error: bool,
1221 pub disposition: ToolResultDisposition,
1223}
1224
1225#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1236#[serde(deny_unknown_fields)]
1237pub struct ToolResult {
1238 pub output: String,
1239 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub durable_content: Option<DurableContent>,
1241 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1242 pub is_error: bool,
1243 pub disposition: ToolResultDisposition,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1247 pub tokens: Option<u32>,
1248}
1249
1250#[derive(
1262 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
1263)]
1264#[serde(rename_all = "snake_case")]
1265pub enum ToolResultDisposition {
1266 #[default]
1270 Recoverable,
1271 Fatal,
1275}
1276
1277impl ToolResultDisposition {
1278 pub const ALL: [Self; 2] = [Self::Recoverable, Self::Fatal];
1279
1280 pub fn as_str(self) -> &'static str {
1281 match self {
1282 Self::Recoverable => "recoverable",
1283 Self::Fatal => "fatal",
1284 }
1285 }
1286
1287 pub fn is_fatal(self) -> bool {
1288 matches!(self, Self::Fatal)
1289 }
1290}
1291
1292impl fmt::Display for ToolResultDisposition {
1293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1294 f.write_str(self.as_str())
1295 }
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1305#[serde(tag = "kind", rename_all = "snake_case")]
1306pub enum PayloadResidency {
1307 Resident(ResidentPayload),
1308 External(ExternalResidency),
1309 PagedOut(PagedOutResidency),
1310 Collapsed(CollapsedPayload),
1312}
1313
1314#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1315#[serde(deny_unknown_fields)]
1316pub struct ResidentPayload {}
1317
1318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1319#[serde(deny_unknown_fields)]
1320pub struct ExternalResidency {
1321 pub payload_ref: PayloadRef,
1322 pub digest: Digest,
1323 pub original_size: WireU64,
1324}
1325
1326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1327#[serde(deny_unknown_fields)]
1328pub struct PagedOutResidency {
1329 pub payload_ref: PayloadRef,
1330 pub digest: Digest,
1331}
1332
1333#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1334#[serde(deny_unknown_fields)]
1335pub struct CollapsedPayload {}
1336
1337#[cfg(test)]
1338mod tests {
1339 use std::collections::BTreeSet;
1340 use std::fs;
1341 use std::path::PathBuf;
1342
1343 use serde_json::{Value, json};
1344
1345 use crate::context::measurement::{
1346 MeasurementConfidence, MeasurementSource, PromptMeasurement,
1347 };
1348
1349 use super::super::*;
1350
1351 fn fixture_dir() -> PathBuf {
1356 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
1357 }
1358
1359 fn fixtures_with_prefix(prefix: &str) -> Vec<(String, Value)> {
1360 let dir = fixture_dir();
1361 let mut names: Vec<String> = fs::read_dir(&dir)
1362 .unwrap_or_else(|e| panic!("failed to read {}: {e}", dir.display()))
1363 .map(|entry| {
1364 entry
1365 .expect("dir entry")
1366 .file_name()
1367 .to_string_lossy()
1368 .to_string()
1369 })
1370 .filter(|name| name.ends_with(".json") && name.starts_with(prefix))
1371 .collect();
1372 names.sort();
1373 assert!(!names.is_empty(), "no {prefix}*.json fixtures");
1374 names
1375 .into_iter()
1376 .map(|name| {
1377 let raw = fs::read_to_string(dir.join(&name)).unwrap();
1378 let value: Value = serde_json::from_str(&raw).unwrap();
1379 (name, value)
1380 })
1381 .collect()
1382 }
1383
1384 fn keys(value: &Value, out: &mut BTreeSet<String>) {
1385 match value {
1386 Value::Object(map) => {
1387 for (key, child) in map {
1388 out.insert(key.clone());
1389 keys(child, out);
1390 }
1391 }
1392 Value::Array(items) => items.iter().for_each(|item| keys(item, out)),
1393 _ => {}
1394 }
1395 }
1396
1397 fn effect_json(outcome: Value) -> String {
1398 serde_json::to_string(&json!({
1399 "operation_id": "op-1",
1400 "input_id": "in-1",
1401 "observed_at_ms": "1700000000000",
1402 "input": { "kind": "resolve_effect", "effect_id": "op-1:step:1:effect:0", "outcome": outcome },
1403 }))
1404 .unwrap()
1405 }
1406
1407 fn decode_effect(outcome: Value) -> Result<WireEnvelope, WireRejection> {
1408 decode_envelope_json(&effect_json(outcome), &KernelBootstrapLimits::default())
1409 }
1410
1411 fn call_id(id: &str) -> CallId {
1412 CallId::new(id).unwrap()
1413 }
1414
1415 fn digest() -> Digest {
1416 Digest::new("sha256:3b1f4a7c9e2d05186a4c7f0b9d3e8c25714f6a0b8c5d2e9f1a3b6c8d0e2f4a61")
1417 .unwrap()
1418 }
1419
1420 fn payload_ref() -> PayloadRef {
1421 PayloadRef::new("payload:01J8Y2QK7C4N0V").unwrap()
1422 }
1423
1424 fn binding() -> MemoryAccessBinding {
1425 MemoryAccessBinding {
1426 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1427 capabilities: MemoryCapabilities {
1428 read: true,
1429 write: true,
1430 },
1431 }
1432 }
1433
1434 fn causation() -> SyscallCausation {
1435 SyscallCausation::ProviderTool(ProviderToolCausation {
1436 provider_effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1437 call_id: call_id("call-1"),
1438 task_id: TaskId::new("task-1").unwrap(),
1439 })
1440 }
1441
1442 fn effect_samples() -> Vec<EffectKind> {
1444 vec![
1445 EffectKind::CallProvider(CallProviderEffect {
1446 context: RenderedContext::default(),
1447 tools: vec![ToolSchema {
1448 name: "read_file".to_string(),
1449 description: "read a file".to_string(),
1450 parameters: BoundedJson::null(),
1451 }],
1452 }),
1453 EffectKind::ExecuteTools(ExecuteToolsEffect {
1454 calls: vec![ToolCall {
1455 call_id: call_id("call-1"),
1456 name: "read_file".to_string(),
1457 arguments: BoundedJson::null(),
1458 }],
1459 }),
1460 EffectKind::RequestApproval(RequestApprovalEffect {
1461 requests: vec![ApprovalRequest {
1462 call_id: call_id("call-1"),
1463 tool_name: "rm".to_string(),
1464 arguments: BoundedJson::null(),
1465 reason: Some("destructive".to_string()),
1466 }],
1467 }),
1468 EffectKind::SpawnTasks(SpawnTasksEffect {
1469 tasks: vec![TaskLaunch {
1470 task_id: TaskId::new("task-1").unwrap(),
1471 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1472 launch_token: LaunchToken::new("launch-1").unwrap(),
1473 node_id: NodeId::new("node-a").unwrap(),
1474 spec: LogicalAgentSpec::new("research"),
1475 }],
1476 budget: None,
1477 }),
1478 EffectKind::PreemptTasks(PreemptTasksEffect {
1479 attempts: vec![TaskAttemptRef {
1480 task_id: TaskId::new("task-1").unwrap(),
1481 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1482 }],
1483 reason: "budget exhausted".to_string(),
1484 }),
1485 EffectKind::PersistMemory(PersistMemoryEffect {
1486 binding: binding(),
1487 memory: CanonicalMemoryWrite {
1488 name: "release pipeline".to_string(),
1489 kind: MemoryKind::Project,
1490 content: "tag v* publishes".to_string(),
1491 description: String::new(),
1492 evidence_refs: Vec::new(),
1493 accepted_at_ms: WireU64::new(1_700_000_000_000),
1494 causation: causation(),
1495 },
1496 }),
1497 EffectKind::QueryMemory(QueryMemoryEffect {
1498 binding: binding(),
1499 query: CanonicalMemoryQuery {
1500 text: "release".to_string(),
1501 kinds: vec![MemoryKind::Project],
1502 accepted_at_ms: WireU64::new(1_700_000_000_000),
1503 causation: causation(),
1504 },
1505 requested_k: 5,
1506 }),
1507 EffectKind::ArchivePageOut(ArchivePageOutEffect {
1508 handle_id: HandleId::new("handle-9").unwrap(),
1509 payload: PageOutPayload {
1510 content: "the full tool output".to_string(),
1511 digest: digest(),
1512 original_size: WireU64::new(262_144),
1513 preview: "the full…".to_string(),
1514 },
1515 }),
1516 EffectKind::LoadPayload(LoadPayloadEffect {
1517 handle_id: HandleId::new("handle-9").unwrap(),
1518 payload_ref: payload_ref(),
1519 }),
1520 EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
1521 request: MilestoneRequest {
1522 contract_id: "brief-quality-primary".to_string(),
1523 phase_id: "phase-2".to_string(),
1524 },
1525 }),
1526 EffectKind::MeasurePrompt(MeasurePromptEffect {
1527 context: RenderedContext::default(),
1528 tools: Vec::new(),
1529 }),
1530 ]
1531 }
1532
1533 fn success_samples() -> Vec<EffectSuccess> {
1535 vec![
1536 EffectSuccess::Provider(ProviderSuccess {
1537 outcome: ProviderOutcome::Completed(ProviderCompleted {
1538 message: ProviderMessage {
1539 role: MessageRole::Assistant,
1540 content: "done".to_string(),
1541 tool_calls: Vec::new(),
1542 tool_call_id: None,
1543 tokens: None,
1544 },
1545 observed_input_tokens: Some(120),
1546 observed_output_tokens: Some(8),
1547 stop_reason: Some(ProviderStopReason::EndTurn),
1548 }),
1549 }),
1550 EffectSuccess::Tools(ToolsSuccess {
1551 results: vec![
1552 ToolResultPayload::Inline(InlineToolResult {
1553 call_id: call_id("call-1"),
1554 result: ToolResult {
1555 output: "ok".to_string(),
1556 durable_content: None,
1557 is_error: false,
1558 disposition: ToolResultDisposition::Recoverable,
1559 tokens: Some(2),
1560 },
1561 }),
1562 ToolResultPayload::External(ExternalToolResult {
1563 call_id: call_id("call-2"),
1564 payload_ref: payload_ref(),
1565 digest: digest(),
1566 original_size: WireU64::new(1_048_576),
1567 preview: "total 42".to_string(),
1568 is_error: false,
1569 disposition: ToolResultDisposition::Recoverable,
1570 }),
1571 ],
1572 }),
1573 EffectSuccess::Approval(ApprovalSuccess {
1574 approved_call_ids: vec![call_id("call-1")],
1575 denied_call_ids: vec![call_id("call-2")],
1576 }),
1577 EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
1578 attempts: vec![TaskLaunchOutcome {
1579 task_id: TaskId::new("task-1").unwrap(),
1580 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1581 outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
1582 }],
1583 }),
1584 EffectSuccess::TasksPreempted(TasksPreemptedSuccess {
1585 attempts: vec![TaskPreemptOutcome {
1586 task_id: TaskId::new("task-1").unwrap(),
1587 attempt_id: AttemptId::new("task-1:attempt:1").unwrap(),
1588 outcome: TaskPreemptStatus::Preempted(TaskPreempted {}),
1589 }],
1590 }),
1591 EffectSuccess::MemoryPersisted(MemoryPersistedSuccess {
1592 receipt: MemoryPersistReceipt {
1593 binding_id: MemoryBindingId::new("binding-a").unwrap(),
1594 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0W").unwrap(),
1595 digest: digest(),
1596 },
1597 }),
1598 EffectSuccess::MemoryQueried(MemoryQueriedSuccess {
1599 recalls: vec![MemoryRecall {
1600 record_ref: MemoryRecordRef::new("memory:01J8Y2QK7C4N0X").unwrap(),
1601 name: "release pipeline".to_string(),
1602 kind: MemoryKind::Project,
1603 content: "tag v* publishes".to_string(),
1604 score: Some(FiniteF64::new(0.82).unwrap()),
1605 }],
1606 }),
1607 EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
1608 receipt: ArchiveReceipt {
1609 handle_id: HandleId::new("handle-9").unwrap(),
1610 payload_ref: payload_ref(),
1611 digest: digest(),
1612 original_size: WireU64::new(262_144),
1613 },
1614 }),
1615 EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
1616 handle_id: HandleId::new("handle-9").unwrap(),
1617 payload: InlinePayload {
1618 content: "the full tool output".to_string(),
1619 digest: digest(),
1620 original_size: WireU64::new(262_144),
1621 },
1622 }),
1623 EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
1624 result: MilestoneCheckResult {
1625 phase_id: "phase-2".to_string(),
1626 passed: true,
1627 failed_criteria: Vec::new(),
1628 score: None,
1629 notes: String::new(),
1630 },
1631 }),
1632 EffectSuccess::PromptMeasured(PromptMeasuredSuccess {
1633 measurement: PromptMeasurement {
1634 input_tokens: 4200,
1635 source: MeasurementSource::Native {
1636 provider: "anthropic".to_string(),
1637 },
1638 confidence: MeasurementConfidence::Exact,
1639 },
1640 }),
1641 ]
1642 }
1643
1644 fn kernel_effect(effect: EffectKind) -> KernelEffect {
1645 KernelEffect {
1646 effect_id: EffectId::new("op-1:step:1:effect:0").unwrap(),
1647 causation_input_id: InputId::new("in-1").unwrap(),
1648 effect,
1649 }
1650 }
1651
1652 #[test]
1657 fn the_effect_union_is_exactly_the_eleven_host_executable_actions() {
1658 let tags: BTreeSet<&str> = EffectKindTag::ALL.iter().map(|tag| tag.as_str()).collect();
1659 assert_eq!(
1660 tags,
1661 BTreeSet::from([
1662 "call_provider",
1663 "execute_tools",
1664 "request_approval",
1665 "spawn_tasks",
1666 "preempt_tasks",
1667 "persist_memory",
1668 "query_memory",
1669 "archive_page_out",
1670 "load_payload",
1671 "evaluate_milestone",
1672 "measure_prompt",
1673 ])
1674 );
1675 assert_eq!(EffectKindTag::ALL.len(), 11);
1676
1677 let sampled: Vec<EffectKindTag> = effect_samples().iter().map(EffectKind::tag).collect();
1678 assert_eq!(
1679 sampled,
1680 EffectKindTag::ALL.to_vec(),
1681 "one sample per variant"
1682 );
1683 }
1684
1685 #[test]
1686 fn terminal_and_observation_shapes_are_not_effects() {
1687 for gone in [
1689 "done",
1690 "terminal",
1691 "compact",
1692 "sync_compact",
1693 "knowledge_sweep",
1694 "workflow_completed",
1695 "control_rejection",
1696 "signal_disposition",
1697 "budget_usage",
1698 "spool_large_result",
1699 ] {
1700 assert!(
1701 !EffectKindTag::ALL.iter().any(|tag| tag.as_str() == gone),
1702 "{gone} is a terminal or an observation, not an effect"
1703 );
1704 let raw = json!({ "kind": gone });
1705 assert!(serde_json::from_value::<EffectKind>(raw).is_err());
1706 }
1707 }
1708
1709 #[test]
1710 fn every_effect_carries_its_kernel_minted_id_and_causation() {
1711 for effect in effect_samples() {
1712 let value = serde_json::to_value(kernel_effect(effect)).unwrap();
1713 assert_eq!(value["effect_id"], json!("op-1:step:1:effect:0"));
1714 assert_eq!(value["causation_input_id"], json!("in-1"));
1715 }
1716 }
1717
1718 #[test]
1723 fn each_effect_kind_has_exactly_one_matching_success_kind() {
1724 let expected: Vec<EffectSuccessTag> = EffectKindTag::ALL
1725 .iter()
1726 .map(|tag| tag.expected_success())
1727 .collect();
1728 let distinct: BTreeSet<&str> = expected.iter().map(|tag| tag.as_str()).collect();
1729 assert_eq!(
1730 distinct.len(),
1731 EffectKindTag::ALL.len(),
1732 "the effect→success map must be a bijection"
1733 );
1734 assert_eq!(
1735 distinct,
1736 EffectSuccessTag::ALL
1737 .iter()
1738 .map(|tag| tag.as_str())
1739 .collect::<BTreeSet<&str>>()
1740 );
1741
1742 let sampled: Vec<EffectSuccessTag> =
1743 success_samples().iter().map(EffectSuccess::tag).collect();
1744 assert_eq!(
1745 sampled, expected,
1746 "samples must line up 1:1 with the effects"
1747 );
1748 }
1749
1750 #[test]
1751 fn a_resolution_of_the_wrong_kind_is_rejected_for_every_wrong_pair() {
1752 let effects = effect_samples();
1753 let successes = success_samples();
1754 for (i, effect) in effects.iter().enumerate() {
1755 let pending = kernel_effect(effect.clone());
1756 for (j, success) in successes.iter().enumerate() {
1757 let outcome = EffectOutcome::Succeeded(EffectSucceeded {
1758 result: success.clone(),
1759 });
1760 let verdict = pending.accept_outcome(&outcome);
1761 if i == j {
1762 assert!(
1763 verdict.is_ok(),
1764 "{:?} must accept its own success payload",
1765 effect.tag()
1766 );
1767 } else {
1768 let mismatch = verdict.expect_err("kind mismatch must be refused");
1769 assert_eq!(mismatch.effect_id, pending.effect_id);
1770 assert_eq!(mismatch.expected, effect.tag().expected_success());
1771 assert_eq!(mismatch.received, success.tag());
1772 }
1773 }
1774 }
1775 }
1776
1777 #[test]
1778 fn every_effect_accepts_the_same_host_failure_including_milestone() {
1779 let kinds = [
1780 HostEffectFailureKind::TransportExhausted,
1781 HostEffectFailureKind::ProtocolError,
1782 HostEffectFailureKind::StorageUnavailable,
1783 HostEffectFailureKind::PermissionDenied,
1784 HostEffectFailureKind::ResourceExhausted,
1785 HostEffectFailureKind::Unknown,
1786 ];
1787 assert_eq!(kinds.len(), 6, "§7.9 fixes six executor failure classes");
1788
1789 for effect in effect_samples() {
1790 let pending = kernel_effect(effect);
1791 for kind in kinds {
1792 let outcome = EffectOutcome::Failed(EffectFailed {
1793 failure: HostEffectFailure {
1794 kind,
1795 message: "boom".to_string(),
1796 retryable: None,
1797 },
1798 });
1799 assert!(
1800 pending.accept_outcome(&outcome).is_ok(),
1801 "{:?} must have the same failure path as every other effect",
1802 pending.effect.tag()
1803 );
1804 }
1805 }
1806 }
1807
1808 #[test]
1809 fn the_outcome_union_has_exactly_two_arms() {
1810 let arms: BTreeSet<String> = [
1811 EffectOutcome::Succeeded(EffectSucceeded {
1812 result: success_samples().remove(0),
1813 }),
1814 EffectOutcome::Failed(EffectFailed {
1815 failure: HostEffectFailure {
1816 kind: HostEffectFailureKind::Unknown,
1817 message: String::new(),
1818 retryable: None,
1819 },
1820 }),
1821 ]
1822 .iter()
1823 .map(|outcome| {
1824 serde_json::to_value(outcome).unwrap()["status"]
1825 .as_str()
1826 .unwrap()
1827 .to_string()
1828 })
1829 .collect();
1830 assert_eq!(
1831 arms,
1832 BTreeSet::from(["succeeded".to_string(), "failed".to_string()])
1833 );
1834
1835 for third in ["partial", "pending", "deferred", "succeeded_with_warnings"] {
1836 let raw = json!({ "status": third });
1837 assert!(
1838 serde_json::from_value::<EffectOutcome>(raw).is_err(),
1839 "{third} is not an outcome"
1840 );
1841 }
1842 }
1843
1844 #[test]
1845 fn the_kernel_never_retries_so_retryable_is_host_advice_only() {
1846 let with = json!({
1849 "status": "failed",
1850 "failure": { "kind": "transport_exhausted", "message": "429", "retryable": true },
1851 });
1852 let outcome: EffectOutcome = serde_json::from_value(with).unwrap();
1853 match outcome {
1854 EffectOutcome::Failed(failed) => {
1855 assert_eq!(failed.failure.retryable, Some(true));
1856 assert_eq!(
1857 failed.failure.kind,
1858 HostEffectFailureKind::TransportExhausted
1859 );
1860 }
1861 EffectOutcome::Succeeded(_) => panic!("failed outcome decoded as succeeded"),
1862 }
1863 }
1864
1865 #[test]
1875 fn every_provider_family_maps_onto_the_canonical_stop_reason_vocabulary() {
1876 let canonical: BTreeSet<&str> = ProviderStopReason::ALL
1877 .iter()
1878 .map(|reason| reason.as_str())
1879 .collect();
1880 assert_eq!(
1881 canonical,
1882 BTreeSet::from([
1883 "end_turn",
1884 "tool_use",
1885 "max_tokens",
1886 "stop_sequence",
1887 "content_filter",
1888 "other",
1889 ])
1890 );
1891 for reason in ProviderStopReason::ALL {
1892 let decoded: ProviderStopReason =
1893 serde_json::from_value(json!(reason.as_str())).unwrap();
1894 assert_eq!(decoded, reason);
1895 }
1896
1897 for vendor_word in [
1899 "stop", "length", "tool_calls", "function_call", "content_filter", "STOP", "MAX_TOKENS", "SAFETY", "FINISH_REASON_STOP", "eos", "eos_token", "sensitive", "insufficient_system_resource",
1912 ] {
1913 let decoded = serde_json::from_value::<ProviderStopReason>(json!(vendor_word));
1914 if vendor_word == "content_filter" {
1915 assert!(decoded.is_ok(), "content_filter *is* canonical");
1916 continue;
1917 }
1918 assert!(
1919 decoded.is_err(),
1920 "{vendor_word:?} is a vendor spelling; the host maps it, core never learns it"
1921 );
1922 }
1923
1924 let value = serde_json::to_value(ProviderStopReason::Other).unwrap();
1926 assert_eq!(value, json!("other"));
1927 assert!(
1928 serde_json::from_value::<ProviderStopReason>(
1929 json!({ "kind": "other", "raw": "insufficient_system_resource" })
1930 )
1931 .is_err(),
1932 "`other` is not a pass-through for vendor text"
1933 );
1934 }
1935
1936 #[test]
1943 fn no_vendor_failure_vocabulary_is_expressible_on_the_canonical_face() {
1944 assert_eq!(HostEffectFailureKind::ALL.len(), 6);
1945 let canonical: BTreeSet<&str> = HostEffectFailureKind::ALL
1946 .iter()
1947 .map(|kind| kind.as_str())
1948 .collect();
1949 assert_eq!(
1950 canonical,
1951 BTreeSet::from([
1952 "transport_exhausted",
1953 "protocol_error",
1954 "storage_unavailable",
1955 "permission_denied",
1956 "resource_exhausted",
1957 "unknown",
1958 ])
1959 );
1960
1961 for absent in [
1962 "rate_limited",
1963 "rate_limit_exceeded",
1964 "too_many_requests",
1965 "overloaded",
1966 "service_unavailable",
1967 "server_error",
1968 "timeout",
1969 "context_length_exceeded",
1970 "context_overflow",
1971 "cancelled",
1972 "canceled",
1973 "aborted",
1974 "user_interrupt",
1975 "interrupted",
1976 ] {
1977 assert!(
1978 serde_json::from_value::<HostEffectFailureKind>(json!(absent)).is_err(),
1979 "{absent:?} must not be an executor failure class"
1980 );
1981 assert!(
1982 !canonical.contains(absent),
1983 "{absent:?} leaked into the canonical vocabulary"
1984 );
1985 }
1986
1987 let spent: EffectOutcome = serde_json::from_value(json!({
1989 "status": "failed",
1990 "failure": {
1991 "kind": "transport_exhausted",
1992 "message": "5 attempts over 41s",
1993 "retryable": false,
1994 },
1995 }))
1996 .unwrap();
1997 let EffectOutcome::Failed(failed) = spent else {
1998 panic!("a spent ladder is a failure");
1999 };
2000 assert_eq!(
2001 failed.failure.kind,
2002 HostEffectFailureKind::TransportExhausted
2003 );
2004 }
2005
2006 #[test]
2011 fn retryable_is_uniformly_optional_advice_across_all_six_failure_kinds() {
2012 for kind in HostEffectFailureKind::ALL {
2013 for retryable in [None, Some(true), Some(false)] {
2014 let mut failure = json!({ "kind": kind.as_str(), "message": "boom" });
2015 if let Some(flag) = retryable {
2016 failure
2017 .as_object_mut()
2018 .unwrap()
2019 .insert("retryable".to_string(), json!(flag));
2020 }
2021 let outcome: EffectOutcome =
2022 serde_json::from_value(json!({ "status": "failed", "failure": failure }))
2023 .unwrap();
2024 let EffectOutcome::Failed(failed) = outcome else {
2025 panic!("failure decoded as success");
2026 };
2027 assert_eq!(failed.failure.kind, kind);
2028 assert_eq!(failed.failure.retryable, retryable);
2029 }
2030 }
2031 }
2032
2033 #[test]
2038 fn a_tool_result_must_state_whether_the_batch_can_continue() {
2039 assert_eq!(
2041 decode_effect(json!({
2042 "status": "succeeded",
2043 "result": { "kind": "tools", "results": [
2044 { "kind": "inline", "call_id": "c-1", "result": { "output": "ok" } }] },
2045 }))
2046 .unwrap_err()
2047 .kind,
2048 WireRejectionKind::MissingField
2049 );
2050
2051 assert_eq!(
2053 ToolResultDisposition::ALL
2054 .iter()
2055 .map(|d| d.as_str())
2056 .collect::<BTreeSet<&str>>(),
2057 BTreeSet::from(["recoverable", "fatal"])
2058 );
2059 for value in ["recoverable", "fatal"] {
2060 let decoded: ToolResultDisposition = serde_json::from_value(json!(value)).expect(value);
2061 assert_eq!(decoded.as_str(), value);
2062 assert_eq!(decoded.is_fatal(), value == "fatal");
2063 }
2064
2065 for gone in [
2069 "user_interrupt",
2070 "governance_denied",
2071 "provider_failure",
2072 "timeout",
2073 "cancelled",
2074 ] {
2075 assert!(
2076 serde_json::from_value::<ToolResultDisposition>(json!(gone)).is_err(),
2077 "{gone:?} is not a tool-result disposition"
2078 );
2079 }
2080 }
2081
2082 #[test]
2088 fn both_residency_arms_state_the_same_two_failure_facts() {
2089 let inline = ToolResultPayload::Inline(InlineToolResult {
2090 call_id: call_id("call-1"),
2091 result: ToolResult {
2092 output: "boom".to_string(),
2093 durable_content: None,
2094 is_error: true,
2095 disposition: ToolResultDisposition::Fatal,
2096 tokens: None,
2097 },
2098 });
2099 let external = ToolResultPayload::External(ExternalToolResult {
2100 call_id: call_id("call-2"),
2101 payload_ref: payload_ref(),
2102 digest: digest(),
2103 original_size: WireU64::new(1_048_576),
2104 preview: "Traceback (most recent call last):".to_string(),
2105 is_error: true,
2106 disposition: ToolResultDisposition::Fatal,
2107 });
2108 for payload in [&inline, &external] {
2109 assert!(payload.is_error(), "{payload:?}");
2110 assert_eq!(payload.disposition(), ToolResultDisposition::Fatal);
2111 assert!(payload.disposition().is_fatal());
2112 }
2113
2114 assert_eq!(
2117 decode_effect(json!({
2118 "status": "succeeded",
2119 "result": { "kind": "tools", "results": [
2120 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2121 "digest": "sha256:ab", "original_size": "1", "preview": "x" }] },
2122 }))
2123 .unwrap_err()
2124 .kind,
2125 WireRejectionKind::MissingField
2126 );
2127
2128 let ok: EffectOutcome = serde_json::from_value(json!({
2131 "status": "succeeded",
2132 "result": { "kind": "tools", "results": [
2133 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2134 "digest": "sha256:ab", "original_size": "1", "preview": "x",
2135 "disposition": "recoverable" }] },
2136 }))
2137 .unwrap();
2138 let EffectOutcome::Succeeded(success) = &ok else {
2139 panic!("not a success");
2140 };
2141 let EffectSuccess::Tools(tools) = &success.result else {
2142 panic!("not a tools success");
2143 };
2144 assert!(!tools.results[0].is_error());
2145 let value = serde_json::to_value(&tools.results[0]).unwrap();
2146 assert!(value.get("is_error").is_none(), "false stays off the wire");
2147 assert_eq!(value["disposition"], json!("recoverable"));
2148 }
2149
2150 #[test]
2152 fn a_milestone_request_is_exactly_the_contract_and_phase_pair() {
2153 let request = MilestoneRequest {
2154 contract_id: "brief-quality-primary".to_string(),
2155 phase_id: "collect".to_string(),
2156 };
2157 let value = serde_json::to_value(&request).unwrap();
2158 assert_eq!(
2159 value.as_object().unwrap().keys().collect::<Vec<_>>(),
2160 vec!["contract_id", "phase_id"],
2161 "a phase id is unique only inside its contract, so the pair is the whole key"
2162 );
2163
2164 for host_owned in ["criteria", "required_evidence", "verifier", "evidence"] {
2168 let mut with_extra = value.clone();
2169 with_extra
2170 .as_object_mut()
2171 .unwrap()
2172 .insert(host_owned.to_string(), json!([]));
2173 assert!(
2174 serde_json::from_value::<MilestoneRequest>(with_extra).is_err(),
2175 "{host_owned} must not decode"
2176 );
2177 }
2178 for missing in ["contract_id", "phase_id"] {
2179 let mut without = value.clone();
2180 without.as_object_mut().unwrap().remove(missing);
2181 assert!(
2182 serde_json::from_value::<MilestoneRequest>(without).is_err(),
2183 "{missing} is half the key and cannot be omitted"
2184 );
2185 }
2186 }
2187
2188 #[test]
2193 fn no_effect_outcome_payload_carries_a_host_wall_clock() {
2194 const BANNED: [&str; 8] = [
2195 "now_ms",
2196 "observed_at_ms",
2197 "timestamp",
2198 "timestamp_ms",
2199 "started_at_ms",
2200 "completed_at_ms",
2201 "wall_clock_ms",
2202 "received_at_ms",
2203 ];
2204
2205 let mut outcomes: Vec<EffectOutcome> = success_samples()
2206 .into_iter()
2207 .map(|result| EffectOutcome::Succeeded(EffectSucceeded { result }))
2208 .collect();
2209 outcomes.push(EffectOutcome::Failed(EffectFailed {
2210 failure: HostEffectFailure {
2211 kind: HostEffectFailureKind::TransportExhausted,
2212 message: "socket hang up".to_string(),
2213 retryable: Some(false),
2214 },
2215 }));
2216
2217 for outcome in outcomes {
2218 let value = serde_json::to_value(&outcome).unwrap();
2219 let mut all = BTreeSet::new();
2220 keys(&value, &mut all);
2221 for banned in BANNED {
2222 assert!(
2223 !all.contains(banned),
2224 "outcome payload must not carry {banned:?}: {value}"
2225 );
2226 }
2227 }
2228 }
2229
2230 #[test]
2235 fn an_external_tool_result_is_a_handle_with_a_digest_size_and_preview() {
2236 let external = ToolResultPayload::External(ExternalToolResult {
2237 call_id: call_id("call-2"),
2238 payload_ref: payload_ref(),
2239 digest: digest(),
2240 original_size: WireU64::new(1_048_576),
2241 preview: "total 42".to_string(),
2242 is_error: false,
2243 disposition: ToolResultDisposition::Recoverable,
2244 });
2245 let value = serde_json::to_value(&external).unwrap();
2246 assert_eq!(value["kind"], json!("external"));
2247 for required in ["payload_ref", "digest", "original_size", "preview"] {
2248 assert!(value.get(required).is_some(), "external needs {required}");
2249 }
2250 assert_eq!(
2251 value["original_size"],
2252 json!("1048576"),
2253 "sizes are decimal-string u64, not JS numbers"
2254 );
2255
2256 let mut all = BTreeSet::new();
2258 keys(&value, &mut all);
2259 for banned in ["path", "file_path", "spool_ref", "spool_dir", "archive_ref"] {
2260 assert!(!all.contains(banned), "payload ref must stay opaque");
2261 }
2262 }
2263
2264 #[test]
2265 fn payload_residency_distinguishes_generated_over_limit_from_pressure_archival() {
2266 let residencies = [
2267 PayloadResidency::Resident(ResidentPayload {}),
2268 PayloadResidency::External(ExternalResidency {
2269 payload_ref: payload_ref(),
2270 digest: digest(),
2271 original_size: WireU64::new(1_048_576),
2272 }),
2273 PayloadResidency::PagedOut(PagedOutResidency {
2274 payload_ref: payload_ref(),
2275 digest: digest(),
2276 }),
2277 PayloadResidency::Collapsed(CollapsedPayload {}),
2278 ];
2279 let tags: BTreeSet<String> = residencies
2280 .iter()
2281 .map(|residency| {
2282 serde_json::to_value(residency).unwrap()["kind"]
2283 .as_str()
2284 .unwrap()
2285 .to_string()
2286 })
2287 .collect();
2288 assert_eq!(
2289 tags,
2290 BTreeSet::from([
2291 "resident".to_string(),
2292 "external".to_string(),
2293 "paged_out".to_string(),
2294 "collapsed".to_string(),
2295 ]),
2296 "§7.10 keeps `external` (generated over limit) apart from `paged_out` (pressure)"
2297 );
2298
2299 let paged = serde_json::to_value(&residencies[2]).unwrap();
2301 assert!(paged.get("original_size").is_none());
2302 }
2303
2304 #[test]
2309 fn unknown_success_kinds_fields_and_variants_are_rejected() {
2310 assert_eq!(
2312 decode_effect(json!({ "status": "succeeded", "result": { "kind": "spooled" } }))
2313 .unwrap_err()
2314 .kind,
2315 WireRejectionKind::UnknownVariant
2316 );
2317 assert_eq!(
2319 decode_effect(json!({ "status": "failed", "failure": { "kind": "rate_limited" } }))
2320 .unwrap_err()
2321 .kind,
2322 WireRejectionKind::UnknownVariant
2323 );
2324 assert_eq!(
2326 decode_effect(json!({
2327 "status": "succeeded",
2328 "result": { "kind": "approval" },
2329 "now_ms": 1,
2330 }))
2331 .unwrap_err()
2332 .kind,
2333 WireRejectionKind::UnknownField
2334 );
2335 assert_eq!(
2337 decode_effect(json!({
2338 "status": "succeeded",
2339 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2340 "payload": { "content": "x", "digest": "sha256:ab",
2341 "original_size": "1", "path": "/tmp/x" } },
2342 }))
2343 .unwrap_err()
2344 .kind,
2345 WireRejectionKind::UnknownField
2346 );
2347 assert_eq!(
2349 decode_effect(json!({
2350 "status": "succeeded",
2351 "result": { "kind": "tools", "results": [
2352 { "kind": "external", "call_id": "c-1", "payload_ref": "p-1",
2353 "original_size": "1", "preview": "x" }] },
2354 }))
2355 .unwrap_err()
2356 .kind,
2357 WireRejectionKind::MissingField
2358 );
2359 assert_eq!(
2361 decode_effect(json!({
2362 "status": "succeeded",
2363 "result": { "kind": "payload_loaded", "handle_id": "h-1",
2364 "payload": { "content": "x", "digest": "sha256:ab",
2365 "original_size": 1 } },
2366 }))
2367 .unwrap_err()
2368 .kind,
2369 WireRejectionKind::InvalidScalar
2370 );
2371 }
2372
2373 #[test]
2374 fn every_effect_and_success_payload_denies_unknown_fields() {
2375 for effect in effect_samples() {
2376 let mut value = serde_json::to_value(&effect).unwrap();
2377 value
2378 .as_object_mut()
2379 .unwrap()
2380 .insert("host_hint".to_string(), json!("x"));
2381 assert!(
2382 serde_json::from_value::<EffectKind>(value).is_err(),
2383 "{:?} must deny unknown fields",
2384 effect.tag()
2385 );
2386 }
2387 for success in success_samples() {
2388 let mut value = serde_json::to_value(&success).unwrap();
2389 value
2390 .as_object_mut()
2391 .unwrap()
2392 .insert("host_hint".to_string(), json!("x"));
2393 assert!(
2394 serde_json::from_value::<EffectSuccess>(value).is_err(),
2395 "{:?} must deny unknown fields",
2396 success.tag()
2397 );
2398 }
2399 }
2400
2401 #[test]
2406 fn resolve_effect_goldens_cover_every_success_kind_and_the_failure_path() {
2407 let mut success_kinds: BTreeSet<String> = BTreeSet::new();
2408 let mut failure_kinds: BTreeSet<String> = BTreeSet::new();
2409
2410 for (name, fixture) in fixtures_with_prefix("input_resolve_") {
2411 let text = serde_json::to_string(&fixture).unwrap();
2412 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2413 .unwrap_or_else(|e| panic!("{name}: {e}"));
2414 assert_eq!(
2415 serde_json::to_value(&envelope).unwrap(),
2416 fixture,
2417 "{name}: round-trip changed the document"
2418 );
2419
2420 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2421 panic!("{name} is not a resolve_effect golden");
2422 };
2423 match &resolve.outcome {
2424 EffectOutcome::Succeeded(ok) => {
2425 success_kinds.insert(ok.result.tag().as_str().to_string());
2426 }
2427 EffectOutcome::Failed(failed) => {
2428 failure_kinds.insert(failed.failure.kind.as_str().to_string());
2429 }
2430 }
2431 }
2432
2433 assert_eq!(
2434 success_kinds,
2435 EffectSuccessTag::ALL
2436 .iter()
2437 .map(|tag| tag.as_str().to_string())
2438 .collect::<BTreeSet<String>>(),
2439 "every effect kind needs at least one resolve golden"
2440 );
2441 assert!(
2442 !failure_kinds.is_empty(),
2443 "the unified failure path needs a golden too"
2444 );
2445 }
2446
2447 #[test]
2448 fn the_milestone_effect_has_a_failure_channel_like_every_other_effect() {
2449 let golden = fixtures_with_prefix("input_resolve_effect_milestone_failed");
2451 assert_eq!(golden.len(), 1);
2452 let text = serde_json::to_string(&golden[0].1).unwrap();
2453 let envelope = decode_envelope_json(&text, &KernelBootstrapLimits::default()).unwrap();
2454 let KernelInput::ResolveEffect(resolve) = &envelope.input else {
2455 panic!("not a resolve_effect golden");
2456 };
2457 let EffectOutcome::Failed(failed) = &resolve.outcome else {
2458 panic!("milestone failure golden must be a Failed outcome");
2459 };
2460 assert_eq!(
2461 failed.failure.kind,
2462 HostEffectFailureKind::StorageUnavailable
2463 );
2464
2465 let milestone = kernel_effect(EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
2466 request: MilestoneRequest {
2467 contract_id: "brief-quality-primary".to_string(),
2468 phase_id: "phase-2".to_string(),
2469 },
2470 }));
2471 assert!(milestone.accept_outcome(&resolve.outcome).is_ok());
2472 }
2473
2474 #[test]
2475 fn effect_rejection_goldens_cover_the_payload_failure_modes() {
2476 let mut expected: BTreeSet<String> = BTreeSet::new();
2477 for (name, fixture) in fixtures_with_prefix("reject_effect_") {
2478 let kind = fixture["expect"]
2479 .as_str()
2480 .unwrap_or_else(|| panic!("{name}: missing `expect`"));
2481 let text = serde_json::to_string(&fixture["envelope"]).unwrap();
2482 let rejection = decode_envelope_json(&text, &KernelBootstrapLimits::default())
2483 .map(|ok| panic!("{name}: expected rejection, decoded {ok:?}"))
2484 .unwrap_err();
2485 assert_eq!(
2486 rejection.kind.as_str(),
2487 kind,
2488 "{name}: {}",
2489 rejection.message
2490 );
2491 expected.insert(kind.to_string());
2492 }
2493 for required in [
2494 "unknown_field",
2495 "unknown_variant",
2496 "missing_field",
2497 "invalid_scalar",
2498 ] {
2499 assert!(
2500 expected.contains(required),
2501 "effect rejection goldens must cover {required}"
2502 );
2503 }
2504 }
2505}