1use serde::{Deserialize, Serialize};
9
10use crate::context::pressure::PressureAction;
11use crate::context::renderer::RenderedContext;
12use crate::context::task_state::TaskUpdate;
13use crate::context::token_engine::ContextTokenEngine;
14use crate::runtime::session::RollbackReason;
15use crate::scheduler::policy::LoopPolicy;
16use crate::scheduler::state_machine::{LoopAction, LoopEvent, LoopStateMachine};
17use crate::types::agent::AgentRunSpec;
18use crate::types::capability::{CapabilityCommand, CapabilityDescriptor, CapabilityKind};
19use crate::types::message::{Message, ToolCall, ToolResult, ToolSchema};
20use crate::types::milestone::{MilestoneCheckResult, MilestoneContract};
21use crate::types::result::{LoopResult, SubAgentResult};
22use crate::types::signal::RuntimeSignal;
23use crate::types::skill::SkillMetadata;
24use crate::types::task::RuntimeTask;
25
26pub const KERNEL_ABI_VERSION: u32 = 1;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum PolicyAction {
34 Allow,
35 Deny,
36 AskUser,
37}
38
39impl From<PolicyAction> for crate::governance::permission::PermissionAction {
40 fn from(action: PolicyAction) -> Self {
41 match action {
42 PolicyAction::Allow => Self::Allow,
43 PolicyAction::Deny => Self::Deny,
44 PolicyAction::AskUser => Self::AskUser,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct PolicyRule {
52 pub tool_pattern: String,
53 pub action: PolicyAction,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RateLimitSpec {
60 pub tool: String,
61 pub max_calls: u32,
62 pub window_ms: u64,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(tag = "kind", rename_all = "snake_case")]
70pub enum ConstraintSpec {
71 Required { tool: String, path: String },
73 Enum {
75 tool: String,
76 path: String,
77 values: Vec<String>,
78 },
79 Range {
81 tool: String,
82 path: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 min: Option<f64>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 max: Option<f64>,
87 },
88}
89
90fn default_signal_queue_size() -> u32 {
91 64
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct KernelInput {
96 pub version: u32,
97 pub event: KernelInputEvent,
98}
99
100impl KernelInput {
101 pub fn new(event: KernelInputEvent) -> Self {
102 Self {
103 version: KERNEL_ABI_VERSION,
104 event,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "kind", rename_all = "snake_case")]
111pub enum KernelInputEvent {
112 SetTools {
113 tools: Vec<ToolSchema>,
114 },
115 SetAvailableSkills {
116 skills: Vec<SkillMetadata>,
117 },
118 SkillActivated {
122 name: String,
123 },
124 SetStableCoreTools {
127 tool_ids: Vec<String>,
128 },
129 SetMemoryEnabled {
130 enabled: bool,
131 },
132 SetKnowledgeEnabled {
133 enabled: bool,
134 },
135 SetPlanToolEnabled {
136 enabled: bool,
137 },
138 SetTokenizer {
139 name: String,
140 },
141 AddSystemMessage {
142 content: String,
143 tokens: u32,
144 },
145 AddKnowledgeMessage {
146 content: String,
147 tokens: u32,
148 },
149 AddHistoryMessage {
150 message: Message,
151 tokens: Option<u32>,
152 },
153 PreloadHistory {
154 messages: Vec<Message>,
155 },
156 MountCapability {
157 capability: CapabilityDescriptor,
158 },
159 UnmountCapability {
160 capability_kind: CapabilityKind,
161 id: String,
162 },
163 LoadMilestoneContract {
164 contract: MilestoneContract,
165 },
166 LoadGovernancePolicy {
170 #[serde(default)]
171 default_action: Option<PolicyAction>,
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
173 rules: Vec<PolicyRule>,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 vetoed_tools: Vec<String>,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
179 rate_limits: Vec<RateLimitSpec>,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 constraints: Vec<ConstraintSpec>,
182 },
183 SetAttentionPolicy {
186 #[serde(default = "default_signal_queue_size")]
187 max_queue_size: u32,
188 },
189 ForceCompact,
190 UpdateTask {
191 update: TaskUpdate,
192 },
193 StartRun {
194 task: RuntimeTask,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 run_spec: Option<AgentRunSpec>,
197 },
198 CapabilityCommand {
199 command: CapabilityCommand,
200 },
201 Resume {
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
206 approved_calls: Vec<String>,
207 #[serde(default, skip_serializing_if = "Vec::is_empty")]
208 denied_calls: Vec<String>,
209 },
210 SetSchedulerBudget {
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 max_wall_ms: Option<u64>,
216 },
217 SetResourceQuota {
223 quota: crate::governance::quota::ResourceQuota,
224 },
225 ProviderResult {
226 message: Message,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 observed_input_tokens: Option<u32>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 observed_output_tokens: Option<u32>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
235 now_ms: Option<u64>,
236 },
237 ToolResults {
238 results: Vec<ToolResult>,
239 },
240 Signal {
241 signal: RuntimeSignal,
242 },
243 MilestoneResult {
244 result: MilestoneCheckResult,
245 },
246 SpawnSubAgent {
248 spec: AgentRunSpec,
249 parent_session_id: String,
250 },
251 LoadWorkflow {
256 spec: crate::orchestration::workflow::WorkflowSpec,
257 parent_session_id: String,
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
260 resumed_completed: Vec<String>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 resumed_submissions: Vec<Vec<crate::orchestration::workflow::WorkflowNode>>,
266 },
267 SubAgentCompleted {
269 result: SubAgentResult,
270 },
271 SubmitWorkflowNodes {
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 nodes: Vec<crate::orchestration::workflow::WorkflowNode>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
282 submitter_agent_id: Option<String>,
283 },
284 SubmitWorkflow {
290 spec: crate::orchestration::workflow::WorkflowSpec,
291 #[serde(default)]
293 parent_session_id: String,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
297 submitter_agent_id: Option<String>,
298 },
299 PageIn {
302 #[serde(default, skip_serializing_if = "Vec::is_empty")]
303 entries: Vec<crate::mm::PageInEntry>,
304 },
305 SetMemoryPolicy {
308 #[serde(default)]
309 memory_path: String,
310 #[serde(default = "default_stale_days")]
311 stale_warning_days: u32,
312 #[serde(default = "default_top_k")]
313 retrieval_top_k: usize,
314 #[serde(default = "default_validation_enabled")]
315 validation_enabled: bool,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 max_content_bytes: Option<u32>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 max_name_length: Option<usize>,
322 },
323 WriteMemory {
325 memory: crate::mm::memory::MemoryWriteRequest,
326 },
327 QueryMemory {
329 query: crate::mm::memory::MemoryQuery,
330 },
331 MemoryRetrievalResult {
333 retrieval: crate::mm::memory::MemoryRetrieval,
334 },
335 Timeout,
336}
337
338fn default_stale_days() -> u32 { 2 }
339fn default_top_k() -> usize { 5 }
340fn default_validation_enabled() -> bool { true }
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct KernelStep {
344 pub version: u32,
345 pub actions: Vec<KernelAction>,
346 pub observations: Vec<KernelObservation>,
347}
348
349impl KernelStep {
350 fn empty(observations: Vec<KernelObservation>) -> Self {
351 Self {
352 version: KERNEL_ABI_VERSION,
353 actions: Vec::new(),
354 observations,
355 }
356 }
357
358 fn single(action: LoopAction, observations: Vec<KernelObservation>) -> Self {
359 Self {
360 version: KERNEL_ABI_VERSION,
361 actions: vec![action.into()],
362 observations,
363 }
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(tag = "kind", rename_all = "snake_case")]
369pub enum KernelAction {
370 CallProvider {
371 context: RenderedContext,
372 tools: Vec<ToolSchema>,
373 },
374 ExecuteTool {
375 calls: Vec<ToolCall>,
376 },
377 EvaluateMilestone {
378 phase_id: String,
379 criteria: Vec<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 verifier: Option<crate::types::milestone::MilestoneVerifier>,
382 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 required_evidence: Vec<String>,
384 },
385 Done {
386 result: LoopResult,
387 },
388}
389
390impl From<LoopAction> for KernelAction {
391 fn from(action: LoopAction) -> Self {
392 match action {
393 LoopAction::AwaitingResume => {
394 panic!("AwaitingResume must not be converted to KernelAction")
395 }
396 LoopAction::CallLLM { context, tools } => Self::CallProvider { context, tools },
397 LoopAction::ExecuteTools { calls } => Self::ExecuteTool { calls },
398 LoopAction::EvaluateMilestone {
399 phase_id,
400 criteria,
401 verifier,
402 required_evidence,
403 } => Self::EvaluateMilestone {
404 phase_id,
405 criteria,
406 verifier,
407 required_evidence,
408 },
409 LoopAction::Done { result } => Self::Done { result },
410 }
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(tag = "kind", rename_all = "snake_case")]
416pub enum KernelObservation {
417 Compressed {
418 action: KernelPressureAction,
419 rho_after: f64,
420 summary: Option<String>,
421 archived: Vec<Message>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
426 invalidates_prefix_at: Option<usize>,
427 },
428 Renewed {
429 sprint: u32,
430 },
431 Rollbacked {
432 turn: u32,
433 checkpoint_history_len: u32,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
435 reason: Option<RollbackReason>,
436 },
437 CapabilityChanged {
438 turn: u32,
439 #[serde(default, skip_serializing_if = "Vec::is_empty")]
440 added: Vec<String>,
441 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 removed: Vec<String>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 change_kind: Option<String>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 capability_id: Option<String>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 version: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 mounted_by: Option<String>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 mount_reason: Option<String>,
453 },
454 MilestoneAdvanced {
455 turn: u32,
456 phase_id: String,
457 capabilities_unlocked: Vec<String>,
458 },
459 MilestoneBlocked {
460 turn: u32,
461 phase_id: String,
462 reason: String,
463 },
464 MilestoneEvidence {
466 turn: u32,
467 phase_id: String,
468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
469 evidence: Vec<String>,
470 },
471 CheckpointTaken {
473 turn: u32,
474 history_len: u32,
475 },
476 AgentProcessChanged {
478 turn: u32,
479 agent_id: String,
480 parent_session_id: String,
481 role: String,
482 isolation: String,
483 context_inheritance: String,
484 state: String,
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
486 permitted_capability_ids: Vec<String>,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 result_termination: Option<String>,
489 },
490 WorkflowBatchSpawned {
493 turn: u32,
494 nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
499 budget: Option<crate::orchestration::workflow::WorkflowBudget>,
500 },
501 WorkflowCompleted {
503 turn: u32,
504 #[serde(default, skip_serializing_if = "Vec::is_empty")]
505 completed: Vec<String>,
506 #[serde(default, skip_serializing_if = "Vec::is_empty")]
507 failed: Vec<String>,
508 },
509 AgentPreempted {
515 turn: u32,
516 #[serde(default, skip_serializing_if = "Vec::is_empty")]
517 agent_ids: Vec<String>,
518 reason: String,
519 },
520 ToolGated {
523 turn: u32,
524 call_id: String,
525 tool: String,
526 reason: String,
527 },
528 SignalDisposed {
530 turn: u32,
531 signal_id: String,
532 disposition: String,
533 queue_depth: u32,
534 },
535 BudgetExceeded { turn: u32, budget: String },
537 Suspended {
539 turn: u32,
540 reason: String,
541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
542 pending_calls: Vec<String>,
543 },
544 Resumed {
546 turn: u32,
547 #[serde(default, skip_serializing_if = "Vec::is_empty")]
548 approved: Vec<String>,
549 #[serde(default, skip_serializing_if = "Vec::is_empty")]
550 denied: Vec<String>,
551 },
552 PageOut {
554 turn: u32,
555 action: KernelPressureAction,
556 rho_after: f64,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 summary: Option<String>,
559 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 archived: Vec<Message>,
561 tier_hint: String,
562 },
563 PageInRequested {
565 turn: u32,
566 call_id: String,
567 tool: String,
568 query: String,
569 top_k: u32,
570 },
571 MemoryWritten {
573 turn: u32,
574 memory_id: String,
575 memory_kind: String,
576 size_bytes: u32,
577 },
578 MemoryValidationFailed {
580 turn: u32,
581 memory_id: String,
582 error: String,
583 },
584 MemoryQueried {
586 turn: u32,
587 query_context: String,
588 requested_k: usize,
589 requires_async_response: bool,
590 },
591 LargeResultSpooled {
593 turn: u32,
594 call_id: String,
595 tool: String,
596 original_size: u32,
597 preview_size: u32,
598 spool_ref: Option<String>,
599 },
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
608#[serde(tag = "kind", rename_all = "snake_case")]
609pub enum TransactionObservation {
610 CheckpointTaken { turn: u32, history_len: u32 },
611 Rollbacked {
612 turn: u32,
613 checkpoint_history_len: u32,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 reason: Option<crate::runtime::session::RollbackReason>,
616 },
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
620#[serde(rename_all = "snake_case")]
621pub enum KernelPressureAction {
622 None,
623 SnipCompact,
624 MicroCompact,
625 ContextCollapse,
626 AutoCompact,
627}
628
629impl From<PressureAction> for KernelPressureAction {
630 fn from(action: PressureAction) -> Self {
631 match action {
632 PressureAction::None => Self::None,
633 PressureAction::SnipCompact => Self::SnipCompact,
634 PressureAction::MicroCompact => Self::MicroCompact,
635 PressureAction::ContextCollapse => Self::ContextCollapse,
636 PressureAction::AutoCompact => Self::AutoCompact,
637 }
638 }
639}
640
641pub struct KernelRuntime {
644 sm: LoopStateMachine,
645}
646
647impl KernelRuntime {
648 pub fn new(policy: LoopPolicy) -> Self {
649 Self {
650 sm: LoopStateMachine::new(policy),
651 }
652 }
653
654 pub fn state_machine(&self) -> &LoopStateMachine {
655 &self.sm
656 }
657
658 pub fn state_machine_mut(&mut self) -> &mut LoopStateMachine {
659 &mut self.sm
660 }
661
662 pub fn is_terminal(&self) -> bool {
663 self.sm.is_terminal()
664 }
665
666 pub fn step(&mut self, input: KernelInput) -> KernelStep {
667 let action = match input.event {
668 KernelInputEvent::SetTools { tools } => {
669 self.sm.tools = tools;
670 return KernelStep::empty(self.sm.take_observations());
671 }
672 KernelInputEvent::SetAvailableSkills { skills } => {
673 self.sm.ctx.set_available_skills(skills);
674 return KernelStep::empty(self.sm.take_observations());
675 }
676 KernelInputEvent::SkillActivated { name } => {
677 self.sm.ctx.activate_skill(name);
680 return KernelStep::empty(self.sm.take_observations());
681 }
682 KernelInputEvent::SetStableCoreTools { tool_ids } => {
683 self.sm.ctx.set_stable_core_tools(tool_ids.into_iter().map(Into::into));
684 return KernelStep::empty(self.sm.take_observations());
685 }
686 KernelInputEvent::SetMemoryEnabled { enabled } => {
687 self.sm.ctx.set_memory_enabled(enabled);
688 return KernelStep::empty(self.sm.take_observations());
689 }
690 KernelInputEvent::SetKnowledgeEnabled { enabled } => {
691 self.sm.ctx.set_knowledge_enabled(enabled);
692 return KernelStep::empty(self.sm.take_observations());
693 }
694 KernelInputEvent::SetPlanToolEnabled { enabled } => {
695 self.sm.ctx.set_plan_tool_enabled(enabled);
696 return KernelStep::empty(self.sm.take_observations());
697 }
698 KernelInputEvent::SetTokenizer { .. } => {
699 self.sm.ctx.engine = ContextTokenEngine::char_approx();
703 return KernelStep::empty(self.sm.take_observations());
704 }
705 KernelInputEvent::AddSystemMessage { content, tokens } => {
706 self.sm
707 .ctx
708 .partitions
709 .system
710 .push(Message::system(content), tokens.max(1));
711 return KernelStep::empty(self.sm.take_observations());
712 }
713 KernelInputEvent::AddKnowledgeMessage { content, tokens } => {
714 self.sm.ctx.partitions.knowledge.push(Message::system(content), tokens.max(1));
723 return KernelStep::empty(self.sm.take_observations());
724 }
725 KernelInputEvent::AddHistoryMessage { message, tokens } => {
726 let tokens = tokens.unwrap_or_else(|| self.sm.ctx.engine.count_message(&message));
727 self.sm.ctx.push_history(message, tokens.max(1));
728 return KernelStep::empty(self.sm.take_observations());
729 }
730 KernelInputEvent::PreloadHistory { messages } => {
731 self.sm.preload_history(messages);
732 return KernelStep::empty(self.sm.take_observations());
733 }
734 KernelInputEvent::MountCapability { capability } => {
735 self.sm.mount_capability(capability, None, None);
736 return KernelStep::empty(self.sm.take_observations());
737 }
738 KernelInputEvent::UnmountCapability {
739 capability_kind,
740 id,
741 } => {
742 self.sm.unmount_capability(capability_kind, &id);
743 return KernelStep::empty(self.sm.take_observations());
744 }
745 KernelInputEvent::LoadMilestoneContract { contract } => {
746 self.sm.load_milestone_contract(contract);
747 return KernelStep::empty(self.sm.take_observations());
748 }
749 KernelInputEvent::LoadGovernancePolicy {
750 default_action,
751 rules,
752 vetoed_tools,
753 rate_limits,
754 constraints,
755 } => {
756 use crate::governance::constraint::{ConstraintRule, ParamConstraint};
757 use crate::governance::permission::PermissionRule;
758 use crate::governance::rate_limit::RateLimit;
759 let default = default_action.unwrap_or(PolicyAction::Allow).into();
760 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(default);
761 for rule in rules {
762 pipeline.permission.add_rule(PermissionRule {
763 tool_pattern: rule.tool_pattern.into(),
764 action: rule.action.into(),
765 });
766 }
767 for tool in vetoed_tools {
768 pipeline.veto.block_tool(tool);
769 }
770 for rl in rate_limits {
771 pipeline.rate_limiter.set_limit(
772 rl.tool,
773 RateLimit {
774 max_calls: rl.max_calls,
775 window_ms: rl.window_ms,
776 },
777 );
778 }
779 for c in constraints {
780 let (tool_name, param_path, rule) = match c {
781 ConstraintSpec::Required { tool, path } => {
782 (tool, path, ConstraintRule::Required)
783 }
784 ConstraintSpec::Enum { tool, path, values } => {
785 (tool, path, ConstraintRule::Enum(values))
786 }
787 ConstraintSpec::Range {
788 tool,
789 path,
790 min,
791 max,
792 } => (tool, path, ConstraintRule::Range { min, max }),
793 };
794 pipeline.constraints.add(ParamConstraint {
795 tool_name,
796 param_path,
797 rule,
798 });
799 }
800 self.sm.set_governance(pipeline);
801 return KernelStep::empty(self.sm.take_observations());
802 }
803 KernelInputEvent::SetAttentionPolicy { max_queue_size } => {
804 self.sm.set_attention(max_queue_size as usize);
805 return KernelStep::empty(self.sm.take_observations());
806 }
807 KernelInputEvent::PageIn { entries } => {
808 self.sm.apply_page_in(&entries);
809 return KernelStep::empty(self.sm.take_observations());
810 }
811 KernelInputEvent::ForceCompact => {
812 self.sm.force_compact();
813 return KernelStep::empty(self.sm.take_observations());
814 }
815 KernelInputEvent::UpdateTask { update } => {
816 self.sm.ctx.update_task(update);
817 return KernelStep::empty(self.sm.take_observations());
818 }
819 KernelInputEvent::StartRun { task, run_spec } => {
820 self.sm.run_spec = run_spec;
821 self.sm.start(task)
822 }
823 KernelInputEvent::CapabilityCommand { command } => {
824 self.sm.execute_capability_command(command);
825 return KernelStep::empty(self.sm.take_observations());
826 }
827 KernelInputEvent::Resume { approved_calls, denied_calls } => {
828 let action = self.sm.resume_from_suspend(approved_calls, denied_calls);
829 if matches!(action, LoopAction::AwaitingResume) {
830 return KernelStep::empty(self.sm.take_observations());
831 }
832 return KernelStep::single(action, self.sm.take_observations());
833 }
834 KernelInputEvent::SetSchedulerBudget { max_wall_ms } => {
835 self.sm.set_wall_budget(max_wall_ms);
836 return KernelStep::empty(self.sm.take_observations());
837 }
838 KernelInputEvent::SetResourceQuota { quota } => {
839 self.sm.set_resource_quota(quota);
840 return KernelStep::empty(self.sm.take_observations());
841 }
842 KernelInputEvent::ProviderResult {
843 message,
844 observed_input_tokens,
845 observed_output_tokens: _,
846 now_ms,
847 } => {
848 if let Some(tokens) = observed_input_tokens {
849 self.sm.ctx.set_observed_prompt_tokens(tokens);
850 }
851 if let Some(ms) = now_ms {
854 self.sm.set_observed_time(ms);
855 }
856 self.sm.feed(LoopEvent::LLMResponse { message })
857 }
858 KernelInputEvent::ToolResults { results } => {
859 self.sm.feed(LoopEvent::ToolResults { results })
860 }
861 KernelInputEvent::Signal { signal } => match self.sm.signal_event(signal) {
862 Some(action) => action,
863 None => return KernelStep::empty(self.sm.take_observations()),
866 },
867 KernelInputEvent::MilestoneResult { result } => {
868 self.sm.feed(LoopEvent::MilestoneResult { result })
869 }
870 KernelInputEvent::SpawnSubAgent {
871 spec,
872 parent_session_id,
873 } => {
874 let action = self.sm.spawn_sub_agent(spec, &parent_session_id);
875 if matches!(action, LoopAction::AwaitingResume) {
876 return KernelStep::empty(self.sm.take_observations());
877 }
878 return KernelStep::single(action, self.sm.take_observations());
879 }
880 KernelInputEvent::LoadWorkflow {
881 spec,
882 parent_session_id,
883 resumed_completed,
884 resumed_submissions,
885 } => {
886 let action = if resumed_completed.is_empty() && resumed_submissions.is_empty() {
887 self.sm.load_workflow(spec, &parent_session_id)
888 } else {
889 self.sm.load_workflow_resumed(
890 spec,
891 &parent_session_id,
892 &resumed_submissions,
893 &resumed_completed,
894 )
895 };
896 if matches!(action, LoopAction::AwaitingResume) {
897 return KernelStep::empty(self.sm.take_observations());
898 }
899 return KernelStep::single(action, self.sm.take_observations());
900 }
901 KernelInputEvent::SubAgentCompleted { result } => {
902 self.sm.feed(LoopEvent::SubAgentCompleted { result })
903 }
904 KernelInputEvent::SubmitWorkflowNodes {
905 nodes,
906 submitter_agent_id,
907 } => {
908 let action = self
909 .sm
910 .submit_workflow_nodes(nodes, submitter_agent_id.as_deref());
911 if matches!(action, LoopAction::AwaitingResume) {
912 return KernelStep::empty(self.sm.take_observations());
913 }
914 return KernelStep::single(action, self.sm.take_observations());
915 }
916 KernelInputEvent::SubmitWorkflow {
917 spec,
918 parent_session_id,
919 submitter_agent_id,
920 } => {
921 let action = self.sm.submit_workflow(
922 spec,
923 &parent_session_id,
924 submitter_agent_id.as_deref(),
925 );
926 if matches!(action, LoopAction::AwaitingResume) {
927 return KernelStep::empty(self.sm.take_observations());
928 }
929 return KernelStep::single(action, self.sm.take_observations());
930 }
931 KernelInputEvent::SetMemoryPolicy {
932 memory_path,
933 stale_warning_days,
934 retrieval_top_k,
935 validation_enabled,
936 max_content_bytes,
937 max_name_length,
938 } => {
939 self.sm.set_memory_policy(crate::mm::memory::MemoryPolicy {
943 memory_path,
944 stale_warning_days,
945 retrieval_top_k,
946 validation_enabled,
947 max_content_bytes,
948 max_name_length,
949 });
950 return KernelStep::empty(self.sm.take_observations());
951 }
952 KernelInputEvent::WriteMemory { memory } => {
953 use crate::mm::memory::validate_memory_write;
956 let turn = self.sm.turn;
957 let disposition = self
961 .sm
962 .gate_syscall(&crate::syscall::Syscall::WriteMemory(memory.clone()));
963 if !disposition.is_allowed() {
964 let error = match disposition {
965 crate::syscall::Disposition::RateLimited { retry_after_ms } => {
966 format!("memory write rate limited; retry after {retry_after_ms}ms")
967 }
968 crate::syscall::Disposition::Deny { reason, .. } => {
969 format!("memory write denied: {reason}")
970 }
971 _ => "memory write not permitted".to_string(),
972 };
973 self.sm.observations.push(
974 KernelObservation::MemoryValidationFailed {
975 turn,
976 memory_id: memory.metadata.name.clone(),
977 error,
978 },
979 );
980 return KernelStep::empty(self.sm.take_observations());
981 }
982 let validation_result = match self.sm.memory_policy() {
986 Some(p) if !p.validation_enabled => Ok(()),
987 Some(p) => p.validation().validate(&memory),
988 None => validate_memory_write(&memory),
989 };
990 match validation_result {
991 Ok(()) => {
992 self.sm.observations.push(KernelObservation::MemoryWritten {
994 turn,
995 memory_id: memory.metadata.name.clone(),
996 memory_kind: memory.metadata.kind.map(|k| k.label()).unwrap_or_else(|| {
997 crate::mm::memory::MemoryKind::infer_from_metadata(&memory.metadata).label()
998 }).to_string(),
999 size_bytes: memory.content.len() as u32,
1000 });
1001 }
1002 Err(err) => {
1003 use crate::mm::memory::MemoryValidationError;
1005 let error_msg = match err {
1006 MemoryValidationError::MissingRequiredField { field } => format!("Missing required field: {}", field),
1007 MemoryValidationError::ContentTooLarge { size, limit } => format!("Content too large: {} bytes (limit: {})", size, limit),
1008 MemoryValidationError::ForbiddenPattern { pattern, reason } => format!("Forbidden pattern '{}': {}", pattern, reason),
1009 MemoryValidationError::InvalidKind { kind } => format!("Invalid kind: {}", kind),
1010 MemoryValidationError::NameTooLong { length, limit } => format!("Name too long: {} chars (limit: {})", length, limit),
1011 };
1012 self.sm.observations.push(KernelObservation::MemoryValidationFailed {
1013 turn,
1014 memory_id: memory.metadata.name.clone(),
1015 error: error_msg,
1016 });
1017 }
1018 }
1019 return KernelStep::empty(self.sm.take_observations());
1020 }
1021 KernelInputEvent::QueryMemory { query } => {
1022 let turn = self.sm.turn;
1025 let requested_k = match self.sm.memory_policy() {
1027 Some(p) => p.clamp_top_k(query.top_k),
1028 None => query.top_k,
1029 };
1030 self.sm.observations.push(KernelObservation::MemoryQueried {
1031 turn,
1032 query_context: query.current_context.clone(),
1033 requested_k,
1034 requires_async_response: true,
1035 });
1036 return KernelStep::empty(self.sm.take_observations());
1037 }
1038 KernelInputEvent::MemoryRetrievalResult { .. } => {
1039 return KernelStep::empty(self.sm.take_observations());
1041 }
1042 KernelInputEvent::Timeout => self.sm.feed(LoopEvent::Timeout),
1043 };
1044 if matches!(action, LoopAction::AwaitingResume) {
1045 return KernelStep::empty(self.sm.take_observations());
1046 }
1047 KernelStep::single(action, self.sm.take_observations())
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054
1055 #[test]
1056 fn start_run_returns_versioned_provider_action() {
1057 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1058 let step = runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1059 task: RuntimeTask::new("ship it"),
1060 run_spec: None,
1061 }));
1062
1063 assert_eq!(step.version, KERNEL_ABI_VERSION);
1064 assert!(matches!(
1065 step.actions.as_slice(),
1066 [KernelAction::CallProvider { .. }]
1067 ));
1068 }
1069
1070 #[test]
1071 fn provider_text_response_returns_done() {
1072 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1073 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1074 task: RuntimeTask::new("ship it"),
1075 run_spec: None,
1076 }));
1077 let step = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1078 message: Message::assistant("done"),
1079 observed_input_tokens: None,
1080 observed_output_tokens: None,
1081 now_ms: None,
1082 }));
1083
1084 assert!(matches!(
1085 step.actions.as_slice(),
1086 [KernelAction::Done { .. }]
1087 ));
1088 }
1089
1090 #[test]
1091 fn config_inputs_mutate_runtime_without_actions() {
1092 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1093 let step = runtime.step(KernelInput::new(KernelInputEvent::SetTools {
1094 tools: vec![ToolSchema {
1095 name: "echo".into(),
1096 description: "Echo input".to_string(),
1097 parameters: serde_json::json!({"type": "object"}),
1098 }],
1099 }));
1100
1101 assert!(step.actions.is_empty());
1102 assert_eq!(runtime.state_machine().tools.len(), 1);
1103 }
1104
1105 #[test]
1106 fn skill_activated_input_records_active_skill() {
1107 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1110 let mut debug = SkillMetadata::new("debug", "Debug helper");
1111 debug.allowed_tools = vec!["read".into(), "grep".into()];
1112 runtime.step(KernelInput::new(KernelInputEvent::SetAvailableSkills {
1113 skills: vec![debug],
1114 }));
1115
1116 let step = runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1117 name: "debug".to_string(),
1118 }));
1119
1120 assert!(step.actions.is_empty(), "activation is config, not an action");
1121 assert!(runtime.state_machine().ctx.active_skills.contains("debug"));
1122 let filter = runtime.state_machine().ctx.active_skill_tool_filter().unwrap();
1123 assert_eq!(filter.len(), 2);
1124 }
1125
1126 #[test]
1127 fn update_task_input_mutates_task_state() {
1128 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1129 let step = runtime.step(KernelInput::new(KernelInputEvent::UpdateTask {
1130 update: TaskUpdate {
1131 progress: Some("tools executed".to_string()),
1132 ..Default::default()
1133 },
1134 }));
1135
1136 assert!(step.actions.is_empty());
1137 assert_eq!(
1138 runtime.state_machine().ctx.partitions.task_state.progress,
1139 "tools executed"
1140 );
1141 }
1142
1143 #[test]
1144 fn add_knowledge_message_enters_knowledge_partition() {
1145 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1146 let step = runtime.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1147 content: "skill: debug".to_string(),
1148 tokens: 10,
1149 }));
1150
1151 assert!(step.actions.is_empty());
1152 assert_eq!(
1153 runtime.state_machine().ctx.partitions.knowledge.messages.len(),
1154 1
1155 );
1156 }
1157
1158 #[test]
1159 fn capability_mount_emits_observation() {
1160 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1161 let step = runtime.step(KernelInput::new(KernelInputEvent::MountCapability {
1162 capability: CapabilityDescriptor::marker(
1163 CapabilityKind::McpServer,
1164 "docs",
1165 "Documentation server",
1166 ),
1167 }));
1168
1169 assert!(step.actions.is_empty());
1170 assert!(matches!(
1171 step.observations.as_slice(),
1172 [KernelObservation::CapabilityChanged { .. }]
1173 ));
1174 }
1175
1176 #[test]
1177 fn spawn_sub_agent_input_registers_process() {
1178 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1179
1180 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1181 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1182 task: RuntimeTask::new("parent task"),
1183 run_spec: None,
1184 }));
1185 runtime.state_machine_mut().take_observations();
1186
1187 let spec = AgentRunSpec::new(
1188 AgentIdentity::sub_agent("worker", "worker-session"),
1189 AgentRole::Implement,
1190 "do work",
1191 );
1192 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1193 spec,
1194 parent_session_id: "parent-session".to_string(),
1195 }));
1196
1197 assert!(step.actions.is_empty());
1198 assert!(step.observations.iter().any(|o| matches!(
1199 o,
1200 KernelObservation::AgentProcessChanged {
1201 agent_id,
1202 parent_session_id,
1203 state,
1204 ..
1205 } if agent_id == "worker" && parent_session_id == "parent-session" && state == "running"
1206 )));
1207 assert_eq!(
1208 runtime
1209 .state_machine()
1210 .agent_process("worker")
1211 .expect("process")
1212 .parent_session_id
1213 .as_str(),
1214 "parent-session"
1215 );
1216 assert!(step.observations.iter().any(|o| matches!(
1217 o,
1218 KernelObservation::Suspended { reason, .. } if reason == "sub_agent_await"
1219 )));
1220 assert!(runtime.state_machine().is_suspended());
1221 assert!(matches!(
1222 runtime.state_machine().wait_reason(),
1223 Some(crate::scheduler::tcb::WaitReason::SubAgentJoin(_))
1224 ));
1225 }
1226
1227 #[test]
1228 fn set_resource_quota_input_denies_spawn_over_quota() {
1229 use crate::governance::quota::ResourceQuota;
1230 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1231
1232 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1233 let step = runtime.step(KernelInput::new(KernelInputEvent::SetResourceQuota {
1235 quota: ResourceQuota { max_spawn_depth: Some(0), ..ResourceQuota::default() },
1236 }));
1237 assert!(step.actions.is_empty(), "config input yields no actions");
1238
1239 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1240 task: RuntimeTask::new("parent task"),
1241 run_spec: None,
1242 }));
1243 runtime.state_machine_mut().take_observations();
1244
1245 let spec = AgentRunSpec::new(
1246 AgentIdentity::sub_agent("worker", "worker-session"),
1247 AgentRole::Implement,
1248 "do work",
1249 );
1250 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1251 spec,
1252 parent_session_id: "parent-session".to_string(),
1253 }));
1254
1255 assert!(matches!(
1258 step.actions.as_slice(),
1259 [KernelAction::CallProvider { .. }]
1260 ));
1261 assert!(!step.observations.iter().any(|o| matches!(
1262 o,
1263 KernelObservation::AgentProcessChanged { agent_id, .. } if agent_id == "worker"
1264 )));
1265 assert!(runtime.state_machine().agent_process("worker").is_none());
1266 assert!(!runtime.state_machine().is_suspended());
1267 }
1268
1269 #[test]
1270 fn default_runtime_leaves_spawn_unquota_ed() {
1271 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1272
1273 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1275 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1276 task: RuntimeTask::new("parent task"),
1277 run_spec: None,
1278 }));
1279 runtime.state_machine_mut().take_observations();
1280
1281 let spec = AgentRunSpec::new(
1282 AgentIdentity::sub_agent("worker", "worker-session"),
1283 AgentRole::Implement,
1284 "do work",
1285 );
1286 runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1287 spec,
1288 parent_session_id: "parent-session".to_string(),
1289 }));
1290 assert!(runtime.state_machine().agent_process("worker").is_some());
1291 assert!(runtime.state_machine().is_suspended());
1292 }
1293
1294 #[test]
1299 fn agent_process_changed_locks_multiword_wire_form() {
1300 use crate::types::agent::{AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec};
1301
1302 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1303 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1304 task: RuntimeTask::new("parent task"),
1305 run_spec: None,
1306 }));
1307 runtime.state_machine_mut().take_observations();
1308
1309 let spec = AgentRunSpec::new(
1311 AgentIdentity::sub_agent("worker", "worker-session"),
1312 AgentRole::Verify,
1313 "do work",
1314 )
1315 .with_isolation(AgentIsolation::ReadOnly);
1316 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1317 spec,
1318 parent_session_id: "parent-session".to_string(),
1319 }));
1320
1321 let obs = step
1322 .observations
1323 .iter()
1324 .find(|o| matches!(o, KernelObservation::AgentProcessChanged { .. }))
1325 .expect("agent_process_changed observation");
1326 let json = serde_json::to_value(obs).unwrap();
1327 assert_eq!(json["isolation"], "readonly", "isolation must stay debug-lowercase");
1328 assert_eq!(
1329 json["context_inheritance"], "systemonly",
1330 "context_inheritance must stay debug-lowercase"
1331 );
1332 assert_eq!(json["role"], "verify");
1333 assert_eq!(json["state"], "running");
1334 }
1335
1336 fn write_memory(runtime: &mut KernelRuntime, name: &str, content: &str) -> KernelStep {
1339 use crate::mm::memory::{MemoryMetadata, MemoryWriteRequest};
1340 runtime.step(KernelInput::new(KernelInputEvent::WriteMemory {
1341 memory: MemoryWriteRequest {
1342 metadata: MemoryMetadata {
1343 name: name.to_string(),
1344 description: "desc".to_string(),
1345 ..Default::default()
1346 },
1347 content: content.to_string(),
1348 },
1349 }))
1350 }
1351
1352 #[test]
1353 fn memory_policy_validation_disabled_admits_forbidden_write() {
1354 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1356 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1357 memory_path: String::new(),
1358 stale_warning_days: 2,
1359 retrieval_top_k: 5,
1360 validation_enabled: false,
1361 max_content_bytes: None,
1362 max_name_length: None,
1363 }));
1364 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1365 assert!(step
1366 .observations
1367 .iter()
1368 .any(|o| matches!(o, KernelObservation::MemoryWritten { .. })));
1369 assert!(!step
1370 .observations
1371 .iter()
1372 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1373 }
1374
1375 #[test]
1376 fn default_runtime_validates_forbidden_write() {
1377 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1379 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1380 assert!(step
1381 .observations
1382 .iter()
1383 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1384 }
1385
1386 #[test]
1387 fn memory_policy_size_override_rejects_oversized_write() {
1388 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1389 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1390 memory_path: String::new(),
1391 stale_warning_days: 2,
1392 retrieval_top_k: 5,
1393 validation_enabled: true,
1394 max_content_bytes: Some(8),
1395 max_name_length: None,
1396 }));
1397 let step = write_memory(&mut runtime, "note", "this content is well over eight bytes");
1398 let failed = step.observations.iter().find_map(|o| match o {
1399 KernelObservation::MemoryValidationFailed { error, .. } => Some(error.clone()),
1400 _ => None,
1401 });
1402 assert!(failed.is_some_and(|e| e.contains("too large")));
1403 }
1404
1405 #[test]
1406 fn memory_policy_clamps_retrieval_top_k() {
1407 use crate::mm::memory::MemoryQuery;
1408 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1409 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1410 memory_path: String::new(),
1411 stale_warning_days: 2,
1412 retrieval_top_k: 3,
1413 validation_enabled: true,
1414 max_content_bytes: None,
1415 max_name_length: None,
1416 }));
1417 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
1418 query: MemoryQuery { top_k: 50, ..Default::default() },
1419 }));
1420 let requested = step.observations.iter().find_map(|o| match o {
1421 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
1422 _ => None,
1423 });
1424 assert_eq!(requested, Some(3));
1425 }
1426
1427 #[test]
1428 fn default_runtime_uses_requested_top_k_verbatim() {
1429 use crate::mm::memory::MemoryQuery;
1430 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1431 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
1432 query: MemoryQuery { top_k: 50, ..Default::default() },
1433 }));
1434 let requested = step.observations.iter().find_map(|o| match o {
1435 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
1436 _ => None,
1437 });
1438 assert_eq!(requested, Some(50));
1439 }
1440
1441 #[test]
1442 fn provider_result_now_ms_drives_wall_time_budget() {
1443 let mut runtime = KernelRuntime::new(LoopPolicy {
1444 max_wall_ms: Some(10),
1445 ..LoopPolicy::default()
1446 });
1447 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1448 task: RuntimeTask::new("ship it"),
1449 run_spec: None,
1450 }));
1451 let mut msg = Message::assistant("");
1452 msg.tool_calls.push(ToolCall {
1453 id: "call-1".into(),
1454 name: "echo".into(),
1455 arguments: serde_json::json!({}),
1456 });
1457 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1458 message: msg,
1459 observed_input_tokens: None,
1460 observed_output_tokens: None,
1461 now_ms: Some(100),
1462 }));
1463 let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1464 results: vec![ToolResult {
1465 call_id: "call-1".into(),
1466 output: crate::types::message::Content::Text("ok".into()),
1467 is_error: false,
1468 is_fatal: false,
1469 error_kind: None,
1470 token_count: None,
1471 }],
1472 }));
1473
1474 assert!(matches!(
1475 step.actions.as_slice(),
1476 [KernelAction::CallProvider { tools, .. }] if tools.is_empty()
1477 ));
1478 }
1479
1480 fn assistant_calling(tool: &str) -> Message {
1483 let mut msg = Message::assistant("");
1484 msg.tool_calls.push(ToolCall {
1485 id: "call-1".into(),
1486 name: tool.into(),
1487 arguments: serde_json::json!({}),
1488 });
1489 msg
1490 }
1491
1492 fn run_with_tool_call(runtime: &mut KernelRuntime, tool: &str) -> KernelStep {
1494 run_with_tool_call_named(runtime, tool, "call-1")
1495 }
1496
1497 fn run_with_tool_call_named(
1498 runtime: &mut KernelRuntime,
1499 tool: &str,
1500 call_id: &str,
1501 ) -> KernelStep {
1502 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1503 task: RuntimeTask::new("do the thing"),
1504 run_spec: None,
1505 }));
1506 runtime.state_machine_mut().take_observations();
1507 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1508 message: assistant_calling(tool),
1509 observed_input_tokens: None,
1510 observed_output_tokens: None,
1511 now_ms: None,
1512 }))
1513 }
1514
1515 #[test]
1516 fn governance_deny_blocks_tool_and_reprompts() {
1517 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1518 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1519 default_action: Some(PolicyAction::Allow),
1520 rules: vec![PolicyRule {
1521 tool_pattern: "danger.*".to_string(),
1522 action: PolicyAction::Deny,
1523 }],
1524 vetoed_tools: vec![],
1525 rate_limits: vec![],
1526 constraints: vec![],
1527 }));
1528
1529 let step = run_with_tool_call(&mut runtime, "danger.delete");
1530
1531 assert!(
1533 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1534 "denied tool should roll back and re-call provider, got {:?}",
1535 step.actions
1536 );
1537 assert!(
1538 step.observations
1539 .iter()
1540 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
1541 "expected a Rollbacked observation for the denied turn",
1542 );
1543 }
1544
1545 #[test]
1546 fn governance_ask_user_suspends_until_resume() {
1547 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1548 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1549 default_action: Some(PolicyAction::Allow),
1550 rules: vec![PolicyRule {
1551 tool_pattern: "sensitive.*".to_string(),
1552 action: PolicyAction::AskUser,
1553 }],
1554 vetoed_tools: vec![],
1555 rate_limits: vec![],
1556 constraints: vec![],
1557 }));
1558
1559 let step = run_with_tool_call(&mut runtime, "sensitive.read");
1560
1561 assert!(
1562 step.actions.is_empty(),
1563 "AskUser should suspend without ExecuteTool, got {:?}",
1564 step.actions
1565 );
1566 assert!(
1567 step.observations.iter().any(|o| matches!(
1568 o,
1569 KernelObservation::ToolGated { tool, .. } if tool == "sensitive.read"
1570 )),
1571 "expected a ToolGated observation for the AskUser call",
1572 );
1573 assert!(
1574 step.observations.iter().any(|o| matches!(
1575 o,
1576 KernelObservation::Suspended { reason, .. } if reason == "ask_user"
1577 )),
1578 "expected a Suspended observation",
1579 );
1580
1581 let resumed = runtime.step(KernelInput::new(KernelInputEvent::Resume {
1582 approved_calls: vec!["call-1".to_string()],
1583 denied_calls: vec![],
1584 }));
1585 assert!(
1586 matches!(resumed.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
1587 "resume with approval should emit ExecuteTool, got {:?}",
1588 resumed.actions
1589 );
1590 assert!(
1591 resumed.observations.iter().any(|o| matches!(
1592 o,
1593 KernelObservation::Resumed { approved, denied, .. }
1594 if approved == &["call-1"] && denied.is_empty()
1595 )),
1596 );
1597 }
1598
1599 #[test]
1600 fn governance_ask_user_resume_all_denied_feeds_tool_results() {
1601 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1602 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1603 default_action: Some(PolicyAction::Allow),
1604 rules: vec![PolicyRule {
1605 tool_pattern: "sensitive.*".to_string(),
1606 action: PolicyAction::AskUser,
1607 }],
1608 vetoed_tools: vec![],
1609 rate_limits: vec![],
1610 constraints: vec![],
1611 }));
1612 run_with_tool_call(&mut runtime, "sensitive.read");
1613 runtime.state_machine_mut().take_observations();
1614
1615 let step = runtime.step(KernelInput::new(KernelInputEvent::Resume {
1616 approved_calls: vec![],
1617 denied_calls: vec!["call-1".to_string()],
1618 }));
1619 assert!(
1620 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1621 "all denied should re-prompt provider, got {:?}",
1622 step.actions
1623 );
1624 }
1625
1626 #[test]
1627 fn no_governance_policy_executes_all_tools() {
1628 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1629 let step = run_with_tool_call(&mut runtime, "danger.delete");
1630
1631 assert!(matches!(
1633 step.actions.as_slice(),
1634 [KernelAction::ExecuteTool { .. }]
1635 ));
1636 assert!(
1637 !step
1638 .observations
1639 .iter()
1640 .any(|o| matches!(o, KernelObservation::ToolGated { .. })),
1641 );
1642 }
1643
1644 fn tool_ok(call_id: &str) -> ToolResult {
1645 ToolResult {
1646 call_id: call_id.into(),
1647 output: crate::types::message::Content::Text("ok".to_string()),
1648 is_error: false,
1649 is_fatal: false,
1650 error_kind: None,
1651 token_count: None,
1652 }
1653 }
1654
1655 #[test]
1656 fn governance_rate_limit_blocks_second_call() {
1657 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1658 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1659 default_action: Some(PolicyAction::Allow),
1660 rules: vec![],
1661 vetoed_tools: vec![],
1662 rate_limits: vec![RateLimitSpec {
1663 tool: "fetch".to_string(),
1664 max_calls: 1,
1665 window_ms: 60_000,
1666 }],
1667 constraints: vec![],
1668 }));
1669 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1670 task: RuntimeTask::new("fetch twice"),
1671 run_spec: None,
1672 }));
1673 runtime.state_machine_mut().take_observations();
1674
1675 let s1 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1677 message: assistant_calling("fetch"),
1678 observed_input_tokens: None,
1679 observed_output_tokens: None,
1680 now_ms: Some(1_000),
1681 }));
1682 assert!(
1683 matches!(s1.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
1684 "first call should execute, got {:?}",
1685 s1.actions
1686 );
1687
1688 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1690 results: vec![tool_ok("call-1")],
1691 }));
1692 runtime.state_machine_mut().take_observations();
1693
1694 let s2 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1696 message: assistant_calling("fetch"),
1697 observed_input_tokens: None,
1698 observed_output_tokens: None,
1699 now_ms: Some(1_001),
1700 }));
1701 assert!(
1702 matches!(s2.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1703 "rate-limited call should roll back and re-call provider, got {:?}",
1704 s2.actions
1705 );
1706 assert!(
1707 s2.observations
1708 .iter()
1709 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
1710 "expected a Rollbacked observation for the rate-limited turn",
1711 );
1712 }
1713
1714 #[test]
1715 fn governance_constraint_required_param_denies() {
1716 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1717 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1718 default_action: Some(PolicyAction::Allow),
1719 rules: vec![],
1720 vetoed_tools: vec![],
1721 rate_limits: vec![],
1722 constraints: vec![ConstraintSpec::Required {
1723 tool: "write".to_string(),
1724 path: "path".to_string(),
1725 }],
1726 }));
1727
1728 let step = run_with_tool_call(&mut runtime, "write");
1730 assert!(
1731 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1732 "missing required param should roll back, got {:?}",
1733 step.actions
1734 );
1735 assert!(
1736 step.observations
1737 .iter()
1738 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
1739 "expected a Rollbacked observation for the constraint violation",
1740 );
1741 }
1742
1743 fn signal(urgency: crate::types::signal::Urgency, summary: &str) -> crate::types::signal::RuntimeSignal {
1746 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType};
1747 RuntimeSignal::new(SignalSource::Gateway, SignalType::Alert, urgency, summary)
1748 }
1749
1750 fn started_runtime_with_attention(max_queue: u32) -> KernelRuntime {
1751 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1752 runtime.step(KernelInput::new(KernelInputEvent::SetAttentionPolicy {
1753 max_queue_size: max_queue,
1754 }));
1755 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1756 task: RuntimeTask::new("watch for signals"),
1757 run_spec: None,
1758 }));
1759 runtime.state_machine_mut().take_observations();
1760 runtime
1761 }
1762
1763 #[test]
1764 fn attention_policy_critical_signal_interrupts() {
1765 use crate::types::signal::Urgency;
1766 let mut runtime = started_runtime_with_attention(8);
1767 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
1768 signal: signal(Urgency::Critical, "fire"),
1769 }));
1770 assert!(
1771 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1772 "critical signal should drive a provider call, got {:?}",
1773 step.actions
1774 );
1775 assert!(step.observations.iter().any(|o| matches!(
1776 o,
1777 KernelObservation::SignalDisposed { disposition, .. } if disposition == "interrupt_now"
1778 )));
1779 }
1780
1781 #[test]
1782 fn attention_policy_normal_signal_queues_without_action() {
1783 use crate::types::signal::Urgency;
1784 let mut runtime = started_runtime_with_attention(8);
1785 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
1786 signal: signal(Urgency::Normal, "job"),
1787 }));
1788 assert!(
1789 step.actions.is_empty(),
1790 "normal signal should queue without a provider call, got {:?}",
1791 step.actions
1792 );
1793 assert!(step.observations.iter().any(|o| matches!(
1794 o,
1795 KernelObservation::SignalDisposed { disposition, queue_depth, .. }
1796 if disposition == "queue" && *queue_depth == 1
1797 )));
1798 }
1799
1800 #[test]
1801 fn attention_policy_full_queue_drops() {
1802 use crate::types::signal::Urgency;
1803 let mut runtime = started_runtime_with_attention(1);
1804 runtime.step(KernelInput::new(KernelInputEvent::Signal {
1805 signal: signal(Urgency::Normal, "first"),
1806 }));
1807 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
1808 signal: signal(Urgency::Normal, "second"),
1809 }));
1810 assert!(step.observations.iter().any(|o| matches!(
1811 o,
1812 KernelObservation::SignalDisposed { disposition, .. } if disposition == "dropped"
1813 )));
1814 }
1815
1816 #[test]
1817 #[test]
1818 fn page_in_populates_knowledge_partition() {
1819 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1820 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
1821 enabled: true,
1822 }));
1823 let before = runtime
1824 .state_machine()
1825 .ctx
1826 .partitions
1827 .knowledge
1828 .messages
1829 .len();
1830 runtime.step(KernelInput::new(KernelInputEvent::PageIn {
1831 entries: vec![crate::mm::PageInEntry {
1832 content: "[memory] prior fix".to_string(),
1833 tokens: Some(10),
1834 source: Some("memory".to_string()),
1835 }],
1836 }));
1837 let after = runtime
1838 .state_machine()
1839 .ctx
1840 .partitions
1841 .knowledge
1842 .messages
1843 .len();
1844 assert!(after > before, "page-in should add knowledge messages");
1845 }
1846
1847 #[test]
1848 fn memory_tool_emits_page_in_requested() {
1849 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1850 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
1851 enabled: true,
1852 }));
1853 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1854 task: RuntimeTask::new("test"),
1855 run_spec: None,
1856 }));
1857 runtime.state_machine_mut().take_observations();
1858
1859 let step = run_with_tool_call(&mut runtime, "memory");
1860 assert!(step.observations.iter().any(|o| matches!(
1861 o,
1862 KernelObservation::PageInRequested { tool, .. } if tool == "memory"
1863 )));
1864 }
1865
1866 #[test]
1867 fn load_workflow_input_drives_dag_to_completion() {
1868 use crate::orchestration::workflow::fanout_synthesize;
1869 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
1870
1871 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1872 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1873 task: RuntimeTask::new("parent task"),
1874 run_spec: None,
1875 }));
1876 runtime.state_machine_mut().take_observations();
1877
1878 let spec =
1880 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
1881 let event = KernelInputEvent::LoadWorkflow {
1882 spec,
1883 parent_session_id: "sess".to_string(),
1884 resumed_completed: Vec::new(),
1885 resumed_submissions: Vec::new(),
1886 };
1887 let json = serde_json::to_string(&event).expect("serialize");
1888 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
1889
1890 let step = runtime.step(KernelInput::new(parsed));
1891 let batch = step
1893 .observations
1894 .iter()
1895 .find_map(|o| match o {
1896 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
1897 _ => None,
1898 })
1899 .expect("workflow_batch_spawned");
1900 assert_eq!(batch.len(), 2);
1901 let goals: Vec<&str> = batch.iter().map(|n| n.goal.as_str()).collect();
1902 assert!(goals.contains(&"w0") && goals.contains(&"w1"));
1903 assert_eq!(batch[0].agent_id, "wf-node0");
1904 assert_eq!(batch[0].isolation, "read_only"); let complete = |runtime: &mut KernelRuntime, id: &str| {
1907 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
1908 result: SubAgentResult {
1909 agent_id: compact_str::CompactString::new(id),
1910 result: LoopResult {
1911 termination: TerminationReason::Completed,
1912 final_message: None,
1913 turns_used: 1,
1914 total_tokens_used: 1,
1915 loop_continue: None,
1916 classify_branch: None,
1917 tournament_winner: None,
1918 },
1919 },
1920 }))
1921 };
1922
1923 complete(&mut runtime, "wf-node0");
1924 let step = complete(&mut runtime, "wf-node1");
1926 assert!(step.observations.iter().any(|o| matches!(
1927 o,
1928 KernelObservation::WorkflowBatchSpawned { nodes, .. }
1929 if nodes.len() == 1 && nodes[0].agent_id == "wf-node2"
1930 )));
1931
1932 let step = complete(&mut runtime, "wf-node2");
1934 assert!(step.observations.iter().any(|o| matches!(
1935 o,
1936 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 3
1937 )));
1938 }
1939
1940 #[test]
1941 fn submit_workflow_nodes_input_appends_a_node_over_the_abi() {
1942 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
1945 use crate::types::agent::AgentRole;
1946 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
1947
1948 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1949 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1950 task: RuntimeTask::new("parent task"),
1951 run_spec: None,
1952 }));
1953 runtime.state_machine_mut().take_observations();
1954
1955 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
1957 RuntimeTask::new("root"),
1958 AgentRole::Implement,
1959 )]);
1960 runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
1961 spec,
1962 parent_session_id: "sess".to_string(),
1963 resumed_completed: Vec::new(),
1964 resumed_submissions: Vec::new(),
1965 }));
1966 runtime.state_machine_mut().take_observations();
1967
1968 let event = KernelInputEvent::SubmitWorkflowNodes {
1970 nodes: vec![WorkflowNode::new(RuntimeTask::new("more"), AgentRole::Implement)],
1971 submitter_agent_id: None,
1972 };
1973 let json = serde_json::to_string(&event).expect("serialize");
1974 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
1975 let step = runtime.step(KernelInput::new(parsed));
1976 assert!(step.observations.iter().any(|o| matches!(
1978 o,
1979 KernelObservation::WorkflowBatchSpawned { nodes, .. }
1980 if nodes.len() == 1 && nodes[0].agent_id == "wf-node1" && nodes[0].goal == "more"
1981 )));
1982
1983 let complete = |runtime: &mut KernelRuntime, id: &str| {
1984 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
1985 result: SubAgentResult {
1986 agent_id: compact_str::CompactString::new(id),
1987 result: LoopResult {
1988 termination: TerminationReason::Completed,
1989 final_message: None,
1990 turns_used: 1,
1991 total_tokens_used: 1,
1992 loop_continue: None,
1993 classify_branch: None,
1994 tournament_winner: None,
1995 },
1996 },
1997 }))
1998 };
1999 complete(&mut runtime, "wf-node0");
2000 let step = complete(&mut runtime, "wf-node1");
2002 assert!(step.observations.iter().any(|o| matches!(
2003 o,
2004 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 2
2005 )));
2006 }
2007
2008 #[test]
2009 fn submit_workflow_input_bootstraps_a_dag_over_the_abi() {
2010 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
2013 use crate::types::agent::AgentRole;
2014 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2015
2016 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2017 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2018 task: RuntimeTask::new("parent task"),
2019 run_spec: None,
2020 }));
2021 runtime.state_machine_mut().take_observations();
2022
2023 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
2025 RuntimeTask::new("authored root"),
2026 AgentRole::Implement,
2027 )]);
2028 let event = KernelInputEvent::SubmitWorkflow {
2029 spec,
2030 parent_session_id: "sess".to_string(),
2031 submitter_agent_id: None,
2032 };
2033 let json = serde_json::to_string(&event).expect("serialize");
2034 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2035 let step = runtime.step(KernelInput::new(parsed));
2036 assert!(step.observations.iter().any(|o| matches!(
2038 o,
2039 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2040 if nodes.len() == 1 && nodes[0].agent_id == "wf-node0" && nodes[0].goal == "authored root"
2041 )));
2042
2043 let step = runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2044 result: SubAgentResult {
2045 agent_id: compact_str::CompactString::new("wf-node0"),
2046 result: LoopResult {
2047 termination: TerminationReason::Completed,
2048 final_message: None,
2049 turns_used: 1,
2050 total_tokens_used: 1,
2051 loop_continue: None,
2052 classify_branch: None,
2053 tournament_winner: None,
2054 },
2055 },
2056 }));
2057 assert!(step.observations.iter().any(|o| matches!(
2058 o,
2059 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 1
2060 )));
2061 }
2062
2063 #[test]
2064 fn load_workflow_resumes_from_completed_nodes() {
2065 use crate::orchestration::workflow::fanout_synthesize;
2066
2067 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2068 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2069 task: RuntimeTask::new("parent task"),
2070 run_spec: None,
2071 }));
2072 runtime.state_machine_mut().take_observations();
2073
2074 let spec =
2076 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2077 let step = runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2078 spec,
2079 parent_session_id: "sess".to_string(),
2080 resumed_completed: vec!["wf-node0".to_string()],
2081 resumed_submissions: Vec::new(),
2082 }));
2083
2084 let batch = step
2086 .observations
2087 .iter()
2088 .find_map(|o| match o {
2089 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2090 _ => None,
2091 })
2092 .expect("workflow_batch_spawned");
2093 assert_eq!(batch.len(), 1);
2094 assert_eq!(batch[0].agent_id, "wf-node1");
2095 }
2096}