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