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, Default, Serialize, Deserialize)]
112pub struct GovernanceConfig {
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub default_action: Option<PolicyAction>,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub rules: Vec<PolicyRule>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub vetoed_tools: Vec<String>,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub rate_limits: Vec<RateLimitSpec>,
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub constraints: Vec<ConstraintSpec>,
123}
124
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129pub struct RunConfig {
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub tools: Option<Vec<ToolSchema>>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub available_skills: Option<Vec<SkillMetadata>>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub stable_core_tools: Option<Vec<String>>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub memory_enabled: Option<bool>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub knowledge_enabled: Option<bool>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub plan_tool_enabled: Option<bool>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub tokenizer: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub governance: Option<GovernanceConfig>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub attention_max_queue_size: Option<u32>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub scheduler_max_wall_ms: Option<u64>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub resource_quota: Option<crate::governance::quota::ResourceQuota>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub group_tokens_base: Option<u64>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub group_spawns_base: Option<u32>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub repeat_fuse: Option<crate::governance::repeat_fuse::RepeatFuseConfig>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub criteria_gate: Option<bool>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub knowledge_budget_ratio: Option<f64>,
174}
175
176pub(crate) fn build_governance_pipeline(
180 default_action: Option<PolicyAction>,
181 rules: Vec<PolicyRule>,
182 vetoed_tools: Vec<String>,
183 rate_limits: Vec<RateLimitSpec>,
184 constraints: Vec<ConstraintSpec>,
185) -> crate::governance::pipeline::GovernancePipeline {
186 use crate::governance::constraint::{ConstraintRule, ParamConstraint};
187 use crate::governance::permission::PermissionRule;
188 use crate::governance::rate_limit::RateLimit;
189 let default = default_action.unwrap_or(PolicyAction::Allow).into();
190 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(default);
191 for rule in rules {
192 pipeline.permission.add_rule(PermissionRule {
193 tool_pattern: rule.tool_pattern.into(),
194 action: rule.action.into(),
195 });
196 }
197 for tool in vetoed_tools {
198 pipeline.veto.block_tool(tool);
199 }
200 for rl in rate_limits {
201 pipeline.rate_limiter.set_limit(
202 rl.tool,
203 RateLimit {
204 max_calls: rl.max_calls,
205 window_ms: rl.window_ms,
206 },
207 );
208 }
209 for c in constraints {
210 let (tool_name, param_path, rule) = match c {
211 ConstraintSpec::Required { tool, path } => (tool, path, ConstraintRule::Required),
212 ConstraintSpec::Enum { tool, path, values } => (tool, path, ConstraintRule::Enum(values)),
213 ConstraintSpec::Range { tool, path, min, max } => {
214 (tool, path, ConstraintRule::Range { min, max })
215 }
216 };
217 pipeline.constraints.add(ParamConstraint {
218 tool_name,
219 param_path,
220 rule,
221 });
222 }
223 pipeline
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(tag = "kind", rename_all = "snake_case")]
228pub enum KernelInputEvent {
229 SetTools {
230 tools: Vec<ToolSchema>,
231 },
232 SetAvailableSkills {
233 skills: Vec<SkillMetadata>,
234 },
235 SkillActivated {
239 name: String,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
244 lease_turns: Option<u32>,
245 },
246 SkillDeactivated {
251 name: String,
252 },
253 SetStableCoreTools {
256 tool_ids: Vec<String>,
257 },
258 SetMemoryEnabled {
259 enabled: bool,
260 },
261 SetKnowledgeEnabled {
262 enabled: bool,
263 },
264 SetPlanToolEnabled {
265 enabled: bool,
266 },
267 SetTokenizer {
268 name: String,
269 },
270 AddSystemMessage {
271 content: String,
272 tokens: u32,
273 },
274 AddKnowledgeMessage {
275 content: String,
276 tokens: u32,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
280 key: Option<String>,
281 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
283 pinned: bool,
284 },
285 RemoveKnowledge {
288 key: String,
289 },
290 AddHistoryMessage {
291 message: Message,
292 tokens: Option<u32>,
293 },
294 PreloadHistory {
295 messages: Vec<Message>,
296 },
297 MountCapability {
298 capability: CapabilityDescriptor,
299 },
300 UnmountCapability {
301 capability_kind: CapabilityKind,
302 id: String,
303 },
304 LoadMilestoneContract {
305 contract: MilestoneContract,
306 },
307 LoadGovernancePolicy {
311 #[serde(default)]
312 default_action: Option<PolicyAction>,
313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
314 rules: Vec<PolicyRule>,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 vetoed_tools: Vec<String>,
317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
320 rate_limits: Vec<RateLimitSpec>,
321 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 constraints: Vec<ConstraintSpec>,
323 },
324 SetAttentionPolicy {
327 #[serde(default = "default_signal_queue_size")]
328 max_queue_size: u32,
329 },
330 ForceCompact,
331 UpdateTask {
332 update: TaskUpdate,
333 },
334 StartRun {
335 task: RuntimeTask,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 run_spec: Option<AgentRunSpec>,
338 },
339 ConfigureRun {
344 config: RunConfig,
345 },
346 CapabilityCommand {
347 command: CapabilityCommand,
348 },
349 Resume {
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 approved_calls: Vec<String>,
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
356 denied_calls: Vec<String>,
357 },
358 SetKnowledgeBudget {
363 ratio: f64,
364 },
365 SetCriteriaGate {
366 enabled: bool,
367 },
368 SetRepeatFuse {
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 enabled: Option<bool>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 deny_after: Option<u32>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 terminate_after: Option<u32>,
377 },
378 SetSchedulerBudget {
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 max_wall_ms: Option<u64>,
384 },
385 SetResourceQuota {
391 quota: crate::governance::quota::ResourceQuota,
392 },
393 ProviderResult {
394 message: Message,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 observed_input_tokens: Option<u32>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 observed_output_tokens: Option<u32>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
403 now_ms: Option<u64>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
408 stop_reason: Option<String>,
409 },
410 ToolResults {
411 results: Vec<ToolResult>,
412 },
413 ProviderError {
420 message: String,
421 },
422 Signal {
423 signal: RuntimeSignal,
424 },
425 MilestoneResult {
426 result: MilestoneCheckResult,
427 },
428 SpawnSubAgent {
430 spec: AgentRunSpec,
431 parent_session_id: String,
432 },
433 LoadWorkflow {
438 spec: crate::orchestration::workflow::WorkflowSpec,
439 parent_session_id: String,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 resumed_completed: Vec<String>,
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
447 resumed_submissions: Vec<Vec<crate::orchestration::workflow::WorkflowNode>>,
448 },
449 SubAgentCompleted {
451 result: SubAgentResult,
452 },
453 SubmitWorkflowNodes {
458 #[serde(default, skip_serializing_if = "Vec::is_empty")]
459 nodes: Vec<crate::orchestration::workflow::WorkflowNode>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
464 submitter_agent_id: Option<String>,
465 },
466 SubmitWorkflow {
472 spec: crate::orchestration::workflow::WorkflowSpec,
473 #[serde(default)]
475 parent_session_id: String,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
479 submitter_agent_id: Option<String>,
480 },
481 PageIn {
484 #[serde(default, skip_serializing_if = "Vec::is_empty")]
485 entries: Vec<crate::mm::PageInEntry>,
486 },
487 SetMemoryPolicy {
490 #[serde(default)]
491 memory_path: String,
492 #[serde(default = "default_stale_days")]
493 stale_warning_days: u32,
494 #[serde(default = "default_top_k")]
495 retrieval_top_k: usize,
496 #[serde(default = "default_validation_enabled")]
497 validation_enabled: bool,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 max_content_bytes: Option<u32>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
503 max_name_length: Option<usize>,
504 },
505 WriteMemory {
507 memory: crate::mm::memory::MemoryWriteRequest,
508 },
509 QueryMemory {
511 query: crate::mm::memory::MemoryQuery,
512 },
513 MemoryRetrievalResult {
515 retrieval: crate::mm::memory::MemoryRetrieval,
516 },
517 Timeout,
518}
519
520fn default_stale_days() -> u32 { 2 }
521fn default_top_k() -> usize { 5 }
522fn default_validation_enabled() -> bool { true }
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct KernelStep {
526 pub version: u32,
527 pub actions: Vec<KernelAction>,
528 pub observations: Vec<KernelObservation>,
529}
530
531impl KernelStep {
532 fn empty(observations: Vec<KernelObservation>) -> Self {
533 Self {
534 version: KERNEL_ABI_VERSION,
535 actions: Vec::new(),
536 observations,
537 }
538 }
539
540 fn single(action: LoopAction, observations: Vec<KernelObservation>) -> Self {
541 Self {
542 version: KERNEL_ABI_VERSION,
543 actions: vec![action.into()],
544 observations,
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(tag = "kind", rename_all = "snake_case")]
551pub enum KernelAction {
552 CallProvider {
553 context: RenderedContext,
554 tools: Vec<ToolSchema>,
555 },
556 ExecuteTool {
557 calls: Vec<ToolCall>,
558 },
559 EvaluateMilestone {
560 phase_id: String,
561 criteria: Vec<String>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 verifier: Option<crate::types::milestone::MilestoneVerifier>,
564 #[serde(default, skip_serializing_if = "Vec::is_empty")]
565 required_evidence: Vec<String>,
566 },
567 Done {
568 result: LoopResult,
569 },
570}
571
572impl From<LoopAction> for KernelAction {
573 fn from(action: LoopAction) -> Self {
574 match action {
575 LoopAction::AwaitingResume => {
576 panic!("AwaitingResume must not be converted to KernelAction")
577 }
578 LoopAction::CallLLM { context, tools } => Self::CallProvider { context, tools },
579 LoopAction::ExecuteTools { calls } => Self::ExecuteTool { calls },
580 LoopAction::EvaluateMilestone {
581 phase_id,
582 criteria,
583 verifier,
584 required_evidence,
585 } => Self::EvaluateMilestone {
586 phase_id,
587 criteria,
588 verifier,
589 required_evidence,
590 },
591 LoopAction::Done { result } => Self::Done { result },
592 }
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
597#[serde(tag = "kind", rename_all = "snake_case")]
598pub enum KernelObservation {
599 Compressed {
600 action: KernelPressureAction,
601 rho_after: f64,
602 summary: Option<String>,
603 archived: Vec<Message>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
608 invalidates_prefix_at: Option<usize>,
609 },
610 Renewed {
611 sprint: u32,
612 },
613 KnowledgeSwept {
617 turn: u32,
618 #[serde(default, skip_serializing_if = "Vec::is_empty")]
619 removed_keys: Vec<String>,
620 tokens_freed: u32,
621 },
622 KnowledgeBudgetExceeded {
626 turn: u32,
627 used: u32,
628 budget: u32,
629 },
630 Rollbacked {
631 turn: u32,
632 checkpoint_history_len: u32,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
634 reason: Option<RollbackReason>,
635 },
636 CapabilityChanged {
637 turn: u32,
638 #[serde(default, skip_serializing_if = "Vec::is_empty")]
639 added: Vec<String>,
640 #[serde(default, skip_serializing_if = "Vec::is_empty")]
641 removed: Vec<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 change_kind: Option<String>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 capability_id: Option<String>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 version: Option<String>,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 mounted_by: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
651 mount_reason: Option<String>,
652 },
653 MilestoneAdvanced {
654 turn: u32,
655 phase_id: String,
656 capabilities_unlocked: Vec<String>,
657 },
658 MilestoneBlocked {
659 turn: u32,
660 phase_id: String,
661 reason: String,
662 },
663 MilestoneEvidence {
665 turn: u32,
666 phase_id: String,
667 #[serde(default, skip_serializing_if = "Vec::is_empty")]
668 evidence: Vec<String>,
669 },
670 CheckpointTaken {
672 turn: u32,
673 history_len: u32,
674 },
675 RepeatFuseTripped {
679 turn: u32,
680 signature: String,
681 count: u32,
682 action: String,
683 },
684 CriteriaGateFired {
687 turn: u32,
688 criteria: Vec<String>,
689 },
690 AgentProcessChanged {
692 turn: u32,
693 agent_id: String,
694 parent_session_id: String,
695 role: String,
696 isolation: String,
697 context_inheritance: String,
698 state: String,
699 #[serde(default, skip_serializing_if = "Vec::is_empty")]
700 permitted_capability_ids: Vec<String>,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 result_termination: Option<String>,
703 },
704 WorkflowBatchSpawned {
707 turn: u32,
708 nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
709 #[serde(default, skip_serializing_if = "Option::is_none")]
713 budget: Option<crate::orchestration::workflow::WorkflowBudget>,
714 },
715 WorkflowCompleted {
717 turn: u32,
718 #[serde(default, skip_serializing_if = "Vec::is_empty")]
719 completed: Vec<String>,
720 #[serde(default, skip_serializing_if = "Vec::is_empty")]
721 failed: Vec<String>,
722 },
723 AgentPreempted {
729 turn: u32,
730 #[serde(default, skip_serializing_if = "Vec::is_empty")]
731 agent_ids: Vec<String>,
732 reason: String,
733 },
734 ToolGated {
737 turn: u32,
738 call_id: String,
739 tool: String,
740 reason: String,
741 },
742 SignalDisposed {
744 turn: u32,
745 signal_id: String,
746 disposition: String,
747 queue_depth: u32,
748 },
749 BudgetExceeded { turn: u32, budget: String },
751 Suspended {
753 turn: u32,
754 reason: String,
755 #[serde(default, skip_serializing_if = "Vec::is_empty")]
756 pending_calls: Vec<String>,
757 },
758 Resumed {
760 turn: u32,
761 #[serde(default, skip_serializing_if = "Vec::is_empty")]
762 approved: Vec<String>,
763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
764 denied: Vec<String>,
765 },
766 PageOut {
768 turn: u32,
769 action: KernelPressureAction,
770 rho_after: f64,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
772 summary: Option<String>,
773 #[serde(default, skip_serializing_if = "Vec::is_empty")]
774 archived: Vec<Message>,
775 tier_hint: String,
776 },
777 PageInRequested {
779 turn: u32,
780 call_id: String,
781 tool: String,
782 query: String,
783 top_k: u32,
784 },
785 MemoryWritten {
787 turn: u32,
788 memory_id: String,
789 memory_kind: String,
790 size_bytes: u32,
791 },
792 MemoryValidationFailed {
794 turn: u32,
795 memory_id: String,
796 error: String,
797 },
798 MemoryQueried {
800 turn: u32,
801 query_context: String,
802 requested_k: usize,
803 requires_async_response: bool,
804 },
805 LargeResultSpooled {
807 turn: u32,
808 call_id: String,
809 tool: String,
810 original_size: u32,
811 preview_size: u32,
812 spool_ref: Option<String>,
813 },
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize)]
822#[serde(tag = "kind", rename_all = "snake_case")]
823pub enum TransactionObservation {
824 CheckpointTaken { turn: u32, history_len: u32 },
825 Rollbacked {
826 turn: u32,
827 checkpoint_history_len: u32,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
829 reason: Option<crate::runtime::session::RollbackReason>,
830 },
831}
832
833#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
834#[serde(rename_all = "snake_case")]
835pub enum KernelPressureAction {
836 None,
837 SnipCompact,
838 MicroCompact,
839 ContextCollapse,
840 AutoCompact,
841}
842
843impl From<PressureAction> for KernelPressureAction {
844 fn from(action: PressureAction) -> Self {
845 match action {
846 PressureAction::None => Self::None,
847 PressureAction::SnipCompact => Self::SnipCompact,
848 PressureAction::MicroCompact => Self::MicroCompact,
849 PressureAction::ContextCollapse => Self::ContextCollapse,
850 PressureAction::AutoCompact => Self::AutoCompact,
851 }
852 }
853}
854
855pub struct KernelRuntime {
858 sm: LoopStateMachine,
859}
860
861impl KernelRuntime {
862 pub fn new(policy: LoopPolicy) -> Self {
863 Self {
864 sm: LoopStateMachine::new(policy),
865 }
866 }
867
868 pub fn state_machine(&self) -> &LoopStateMachine {
869 &self.sm
870 }
871
872 pub fn state_machine_mut(&mut self) -> &mut LoopStateMachine {
873 &mut self.sm
874 }
875
876 pub fn is_terminal(&self) -> bool {
877 self.sm.is_terminal()
878 }
879
880 pub fn local_subagents_spawned(&self) -> u32 {
883 self.sm.local_subagents_spawned()
884 }
885
886 pub fn step(&mut self, input: KernelInput) -> KernelStep {
887 let action = match input.event {
888 KernelInputEvent::SetTools { tools } => {
889 self.sm.tools = tools;
890 return KernelStep::empty(self.sm.take_observations());
891 }
892 KernelInputEvent::SetAvailableSkills { skills } => {
893 self.sm.ctx.set_available_skills(skills);
894 return KernelStep::empty(self.sm.take_observations());
895 }
896 KernelInputEvent::SkillActivated { name, lease_turns } => {
897 let expires_at_turn = lease_turns.map(|n| self.sm.turn.saturating_add(n));
901 self.sm.ctx.activate_skill_leased(name, expires_at_turn);
902 return KernelStep::empty(self.sm.take_observations());
903 }
904 KernelInputEvent::SkillDeactivated { name } => {
905 self.sm.ctx.deactivate_skill(&name);
906 return KernelStep::empty(self.sm.take_observations());
907 }
908 KernelInputEvent::SetStableCoreTools { tool_ids } => {
909 self.sm.ctx.set_stable_core_tools(tool_ids.into_iter().map(Into::into));
910 return KernelStep::empty(self.sm.take_observations());
911 }
912 KernelInputEvent::SetMemoryEnabled { enabled } => {
913 self.sm.ctx.set_memory_enabled(enabled);
914 return KernelStep::empty(self.sm.take_observations());
915 }
916 KernelInputEvent::SetKnowledgeEnabled { enabled } => {
917 self.sm.ctx.set_knowledge_enabled(enabled);
918 return KernelStep::empty(self.sm.take_observations());
919 }
920 KernelInputEvent::SetPlanToolEnabled { enabled } => {
921 self.sm.ctx.set_plan_tool_enabled(enabled);
922 return KernelStep::empty(self.sm.take_observations());
923 }
924 KernelInputEvent::SetTokenizer { .. } => {
925 self.sm.ctx.engine = ContextTokenEngine::char_approx();
929 return KernelStep::empty(self.sm.take_observations());
930 }
931 KernelInputEvent::AddSystemMessage { content, tokens } => {
932 self.sm
933 .ctx
934 .partitions
935 .system
936 .push(Message::system(content), tokens.max(1));
937 return KernelStep::empty(self.sm.take_observations());
938 }
939 KernelInputEvent::AddKnowledgeMessage { content, tokens, key, pinned } => {
940 self.sm.ctx.push_knowledge_entry(
953 key.map(compact_str::CompactString::from),
954 Message::system(content),
955 tokens.max(1),
956 pinned,
957 );
958 return KernelStep::empty(self.sm.take_observations());
959 }
960 KernelInputEvent::RemoveKnowledge { key } => {
961 self.sm.ctx.remove_knowledge(&key);
962 return KernelStep::empty(self.sm.take_observations());
963 }
964 KernelInputEvent::AddHistoryMessage { message, tokens } => {
965 let tokens = tokens.unwrap_or_else(|| self.sm.ctx.engine.count_message(&message));
966 self.sm.ctx.push_history(message, tokens.max(1));
967 return KernelStep::empty(self.sm.take_observations());
968 }
969 KernelInputEvent::PreloadHistory { messages } => {
970 self.sm.preload_history(messages);
971 return KernelStep::empty(self.sm.take_observations());
972 }
973 KernelInputEvent::MountCapability { capability } => {
974 self.sm.mount_capability(capability, None, None);
975 return KernelStep::empty(self.sm.take_observations());
976 }
977 KernelInputEvent::UnmountCapability {
978 capability_kind,
979 id,
980 } => {
981 self.sm.unmount_capability(capability_kind, &id);
982 return KernelStep::empty(self.sm.take_observations());
983 }
984 KernelInputEvent::LoadMilestoneContract { contract } => {
985 self.sm.load_milestone_contract(contract);
986 return KernelStep::empty(self.sm.take_observations());
987 }
988 KernelInputEvent::LoadGovernancePolicy {
989 default_action,
990 rules,
991 vetoed_tools,
992 rate_limits,
993 constraints,
994 } => {
995 self.sm.set_governance(build_governance_pipeline(
996 default_action,
997 rules,
998 vetoed_tools,
999 rate_limits,
1000 constraints,
1001 ));
1002 return KernelStep::empty(self.sm.take_observations());
1003 }
1004 KernelInputEvent::ConfigureRun { config } => {
1005 let RunConfig {
1012 tools,
1013 available_skills,
1014 stable_core_tools,
1015 memory_enabled,
1016 knowledge_enabled,
1017 plan_tool_enabled,
1018 tokenizer,
1019 governance,
1020 attention_max_queue_size,
1021 scheduler_max_wall_ms,
1022 resource_quota,
1023 group_tokens_base,
1024 group_spawns_base,
1025 repeat_fuse,
1026 criteria_gate,
1027 knowledge_budget_ratio,
1028 } = config;
1029 if let Some(tools) = tools {
1030 self.sm.tools = tools;
1031 }
1032 if let Some(skills) = available_skills {
1033 self.sm.ctx.set_available_skills(skills);
1034 }
1035 if let Some(ids) = stable_core_tools {
1036 self.sm.ctx.set_stable_core_tools(ids.into_iter().map(Into::into));
1037 }
1038 if let Some(enabled) = memory_enabled {
1039 self.sm.ctx.set_memory_enabled(enabled);
1040 }
1041 if let Some(enabled) = knowledge_enabled {
1042 self.sm.ctx.set_knowledge_enabled(enabled);
1043 }
1044 if let Some(enabled) = plan_tool_enabled {
1045 self.sm.ctx.set_plan_tool_enabled(enabled);
1046 }
1047 if tokenizer.is_some() {
1048 self.sm.ctx.engine = ContextTokenEngine::char_approx();
1049 }
1050 if let Some(g) = governance {
1051 self.sm.set_governance(build_governance_pipeline(
1052 g.default_action,
1053 g.rules,
1054 g.vetoed_tools,
1055 g.rate_limits,
1056 g.constraints,
1057 ));
1058 }
1059 if let Some(max_queue) = attention_max_queue_size {
1060 self.sm.set_attention(max_queue as usize);
1061 }
1062 if let Some(ms) = scheduler_max_wall_ms {
1063 self.sm.set_wall_budget(Some(ms));
1064 }
1065 if let Some(quota) = resource_quota {
1066 self.sm.set_resource_quota(quota);
1067 }
1068 if let Some(base) = group_tokens_base {
1069 self.sm.seed_group_budget(base);
1070 }
1071 if let Some(base) = group_spawns_base {
1072 self.sm.seed_group_spawns(base);
1073 }
1074 if let Some(fuse) = repeat_fuse {
1075 self.sm.set_repeat_fuse(fuse);
1076 }
1077 if let Some(enabled) = criteria_gate {
1078 self.sm.set_criteria_gate(enabled);
1079 }
1080 if let Some(ratio) = knowledge_budget_ratio {
1081 self.sm.ctx.config.knowledge_budget_ratio = ratio;
1082 }
1083 return KernelStep::empty(self.sm.take_observations());
1084 }
1085 KernelInputEvent::SetAttentionPolicy { max_queue_size } => {
1086 self.sm.set_attention(max_queue_size as usize);
1087 return KernelStep::empty(self.sm.take_observations());
1088 }
1089 KernelInputEvent::PageIn { entries } => {
1090 self.sm.apply_page_in(&entries);
1091 return KernelStep::empty(self.sm.take_observations());
1092 }
1093 KernelInputEvent::ForceCompact => {
1094 self.sm.force_compact();
1095 return KernelStep::empty(self.sm.take_observations());
1096 }
1097 KernelInputEvent::UpdateTask { update } => {
1098 self.sm.ctx.update_task(update);
1099 return KernelStep::empty(self.sm.take_observations());
1100 }
1101 KernelInputEvent::StartRun { task, run_spec } => {
1102 self.sm.run_spec = run_spec;
1103 self.sm.start(task)
1104 }
1105 KernelInputEvent::CapabilityCommand { command } => {
1106 self.sm.execute_capability_command(command);
1107 return KernelStep::empty(self.sm.take_observations());
1108 }
1109 KernelInputEvent::Resume { approved_calls, denied_calls } => {
1110 let action = self.sm.resume_from_suspend(approved_calls, denied_calls);
1111 if matches!(action, LoopAction::AwaitingResume) {
1112 return KernelStep::empty(self.sm.take_observations());
1113 }
1114 return KernelStep::single(action, self.sm.take_observations());
1115 }
1116 KernelInputEvent::SetSchedulerBudget { max_wall_ms } => {
1117 self.sm.set_wall_budget(max_wall_ms);
1118 return KernelStep::empty(self.sm.take_observations());
1119 }
1120 KernelInputEvent::SetResourceQuota { quota } => {
1121 self.sm.set_resource_quota(quota);
1122 return KernelStep::empty(self.sm.take_observations());
1123 }
1124 KernelInputEvent::SetCriteriaGate { enabled } => {
1125 self.sm.set_criteria_gate(enabled);
1126 return KernelStep::empty(self.sm.take_observations());
1127 }
1128 KernelInputEvent::SetKnowledgeBudget { ratio } => {
1129 self.sm.ctx.config.knowledge_budget_ratio = ratio;
1130 return KernelStep::empty(self.sm.take_observations());
1131 }
1132 KernelInputEvent::SetRepeatFuse { enabled, deny_after, terminate_after } => {
1133 let mut cfg = self.sm.repeat_fuse_config();
1134 if let Some(e) = enabled { cfg.enabled = e; }
1135 if let Some(d) = deny_after { cfg.deny_after = d; }
1136 if let Some(t) = terminate_after { cfg.terminate_after = t; }
1137 self.sm.set_repeat_fuse(cfg);
1138 return KernelStep::empty(self.sm.take_observations());
1139 }
1140 KernelInputEvent::ProviderResult {
1141 message,
1142 observed_input_tokens,
1143 observed_output_tokens: _,
1144 now_ms,
1145 stop_reason,
1146 } => {
1147 if let Some(tokens) = observed_input_tokens {
1148 self.sm.ctx.set_observed_prompt_tokens(tokens);
1149 }
1150 if let Some(ms) = now_ms {
1153 self.sm.set_observed_time(ms);
1154 }
1155 self.sm.set_pending_stop_reason(stop_reason);
1157 self.sm.feed(LoopEvent::LLMResponse { message })
1158 }
1159 KernelInputEvent::ToolResults { results } => {
1160 self.sm.feed(LoopEvent::ToolResults { results })
1161 }
1162 KernelInputEvent::ProviderError { message } => {
1163 self.sm.recover_from_provider_error(&message)
1166 }
1167 KernelInputEvent::Signal { signal } => match self.sm.signal_event(signal) {
1168 Some(action) => action,
1169 None => return KernelStep::empty(self.sm.take_observations()),
1172 },
1173 KernelInputEvent::MilestoneResult { result } => {
1174 self.sm.feed(LoopEvent::MilestoneResult { result })
1175 }
1176 KernelInputEvent::SpawnSubAgent {
1177 spec,
1178 parent_session_id,
1179 } => {
1180 let action = self.sm.spawn_sub_agent(spec, &parent_session_id);
1181 if matches!(action, LoopAction::AwaitingResume) {
1182 return KernelStep::empty(self.sm.take_observations());
1183 }
1184 return KernelStep::single(action, self.sm.take_observations());
1185 }
1186 KernelInputEvent::LoadWorkflow {
1187 spec,
1188 parent_session_id,
1189 resumed_completed,
1190 resumed_submissions,
1191 } => {
1192 self.sm.ensure_started_for_workflow(&spec);
1196 let action = if resumed_completed.is_empty() && resumed_submissions.is_empty() {
1197 self.sm.load_workflow(spec, &parent_session_id)
1198 } else {
1199 self.sm.load_workflow_resumed(
1200 spec,
1201 &parent_session_id,
1202 &resumed_submissions,
1203 &resumed_completed,
1204 )
1205 };
1206 if matches!(action, LoopAction::AwaitingResume) {
1207 return KernelStep::empty(self.sm.take_observations());
1208 }
1209 return KernelStep::single(action, self.sm.take_observations());
1210 }
1211 KernelInputEvent::SubAgentCompleted { result } => {
1212 self.sm.feed(LoopEvent::SubAgentCompleted { result })
1213 }
1214 KernelInputEvent::SubmitWorkflowNodes {
1215 nodes,
1216 submitter_agent_id,
1217 } => {
1218 let action = self
1219 .sm
1220 .submit_workflow_nodes(nodes, submitter_agent_id.as_deref());
1221 if matches!(action, LoopAction::AwaitingResume) {
1222 return KernelStep::empty(self.sm.take_observations());
1223 }
1224 return KernelStep::single(action, self.sm.take_observations());
1225 }
1226 KernelInputEvent::SubmitWorkflow {
1227 spec,
1228 parent_session_id,
1229 submitter_agent_id,
1230 } => {
1231 let action = self.sm.submit_workflow(
1232 spec,
1233 &parent_session_id,
1234 submitter_agent_id.as_deref(),
1235 );
1236 if matches!(action, LoopAction::AwaitingResume) {
1237 return KernelStep::empty(self.sm.take_observations());
1238 }
1239 return KernelStep::single(action, self.sm.take_observations());
1240 }
1241 KernelInputEvent::SetMemoryPolicy {
1242 memory_path,
1243 stale_warning_days,
1244 retrieval_top_k,
1245 validation_enabled,
1246 max_content_bytes,
1247 max_name_length,
1248 } => {
1249 self.sm.set_memory_policy(crate::mm::memory::MemoryPolicy {
1253 memory_path,
1254 stale_warning_days,
1255 retrieval_top_k,
1256 validation_enabled,
1257 max_content_bytes,
1258 max_name_length,
1259 });
1260 return KernelStep::empty(self.sm.take_observations());
1261 }
1262 KernelInputEvent::WriteMemory { memory } => {
1263 use crate::mm::memory::validate_memory_write;
1266 let turn = self.sm.turn;
1267 let disposition = self
1271 .sm
1272 .gate_syscall(&crate::syscall::Syscall::WriteMemory(memory.clone()));
1273 if !disposition.is_allowed() {
1274 let error = match disposition {
1275 crate::syscall::Disposition::RateLimited { retry_after_ms } => {
1276 format!("memory write rate limited; retry after {retry_after_ms}ms")
1277 }
1278 crate::syscall::Disposition::Deny { reason, .. } => {
1279 format!("memory write denied: {reason}")
1280 }
1281 _ => "memory write not permitted".to_string(),
1282 };
1283 self.sm.observations.push(
1284 KernelObservation::MemoryValidationFailed {
1285 turn,
1286 memory_id: memory.metadata.name.clone(),
1287 error,
1288 },
1289 );
1290 return KernelStep::empty(self.sm.take_observations());
1291 }
1292 let validation_result = match self.sm.memory_policy() {
1296 Some(p) if !p.validation_enabled => Ok(()),
1297 Some(p) => p.validation().validate(&memory),
1298 None => validate_memory_write(&memory),
1299 };
1300 match validation_result {
1301 Ok(()) => {
1302 self.sm.observations.push(KernelObservation::MemoryWritten {
1304 turn,
1305 memory_id: memory.metadata.name.clone(),
1306 memory_kind: memory.metadata.kind.map(|k| k.label()).unwrap_or_else(|| {
1307 crate::mm::memory::MemoryKind::infer_from_metadata(&memory.metadata).label()
1308 }).to_string(),
1309 size_bytes: memory.content.len() as u32,
1310 });
1311 }
1312 Err(err) => {
1313 use crate::mm::memory::MemoryValidationError;
1315 let error_msg = match err {
1316 MemoryValidationError::MissingRequiredField { field } => format!("Missing required field: {}", field),
1317 MemoryValidationError::ContentTooLarge { size, limit } => format!("Content too large: {} bytes (limit: {})", size, limit),
1318 MemoryValidationError::ForbiddenPattern { pattern, reason } => format!("Forbidden pattern '{}': {}", pattern, reason),
1319 MemoryValidationError::InvalidKind { kind } => format!("Invalid kind: {}", kind),
1320 MemoryValidationError::NameTooLong { length, limit } => format!("Name too long: {} chars (limit: {})", length, limit),
1321 };
1322 self.sm.observations.push(KernelObservation::MemoryValidationFailed {
1323 turn,
1324 memory_id: memory.metadata.name.clone(),
1325 error: error_msg,
1326 });
1327 }
1328 }
1329 return KernelStep::empty(self.sm.take_observations());
1330 }
1331 KernelInputEvent::QueryMemory { query } => {
1332 let turn = self.sm.turn;
1335 let requested_k = match self.sm.memory_policy() {
1337 Some(p) => p.clamp_top_k(query.top_k),
1338 None => query.top_k,
1339 };
1340 self.sm.observations.push(KernelObservation::MemoryQueried {
1341 turn,
1342 query_context: query.current_context.clone(),
1343 requested_k,
1344 requires_async_response: true,
1345 });
1346 return KernelStep::empty(self.sm.take_observations());
1347 }
1348 KernelInputEvent::MemoryRetrievalResult { .. } => {
1349 return KernelStep::empty(self.sm.take_observations());
1351 }
1352 KernelInputEvent::Timeout => self.sm.feed(LoopEvent::Timeout),
1353 };
1354 if matches!(action, LoopAction::AwaitingResume) {
1355 return KernelStep::empty(self.sm.take_observations());
1356 }
1357 KernelStep::single(action, self.sm.take_observations())
1358 }
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363 use super::*;
1364
1365 #[test]
1366 fn start_run_returns_versioned_provider_action() {
1367 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1368 let step = runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1369 task: RuntimeTask::new("ship it"),
1370 run_spec: None,
1371 }));
1372
1373 assert_eq!(step.version, KERNEL_ABI_VERSION);
1374 assert!(matches!(
1375 step.actions.as_slice(),
1376 [KernelAction::CallProvider { .. }]
1377 ));
1378 }
1379
1380 #[test]
1381 fn provider_text_response_returns_done() {
1382 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1383 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1384 task: RuntimeTask::new("ship it"),
1385 run_spec: None,
1386 }));
1387 let step = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1388 message: Message::assistant("done"),
1389 observed_input_tokens: None,
1390 observed_output_tokens: None,
1391 stop_reason: None,
1392 now_ms: None,
1393 }));
1394
1395 assert!(matches!(
1396 step.actions.as_slice(),
1397 [KernelAction::Done { .. }]
1398 ));
1399 }
1400
1401 #[test]
1402 fn config_inputs_mutate_runtime_without_actions() {
1403 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1404 let step = runtime.step(KernelInput::new(KernelInputEvent::SetTools {
1405 tools: vec![ToolSchema {
1406 name: "echo".into(),
1407 description: "Echo input".to_string(),
1408 parameters: serde_json::json!({"type": "object"}),
1409 }],
1410 }));
1411
1412 assert!(step.actions.is_empty());
1413 assert_eq!(runtime.state_machine().tools.len(), 1);
1414 }
1415
1416 #[test]
1417 fn skill_activated_input_records_active_skill() {
1418 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1421 let mut debug = SkillMetadata::new("debug", "Debug helper");
1422 debug.allowed_tools = vec!["read".into(), "grep".into()];
1423 runtime.step(KernelInput::new(KernelInputEvent::SetAvailableSkills {
1424 skills: vec![debug],
1425 }));
1426
1427 let step = runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1428 name: "debug".to_string(),
1429 lease_turns: None,
1430 }));
1431
1432 assert!(step.actions.is_empty(), "activation is config, not an action");
1433 assert!(runtime.state_machine().ctx.active_skills.contains_key("debug"));
1434 let filter = runtime.state_machine().ctx.active_skill_tool_filter().unwrap();
1435 assert_eq!(filter.len(), 2);
1436 }
1437
1438 #[test]
1439 fn skill_deactivated_rewidens_toolset_and_unpins_knowledge() {
1440 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1443 let mut debug = SkillMetadata::new("debug", "Debug helper");
1444 debug.allowed_tools = vec!["read".into(), "grep".into()];
1445 runtime.step(KernelInput::new(KernelInputEvent::SetAvailableSkills {
1446 skills: vec![debug],
1447 }));
1448 runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1449 name: "debug".to_string(),
1450 lease_turns: None,
1451 }));
1452 runtime.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1453 content: "debug skill content".to_string(),
1454 tokens: 5,
1455 key: Some("skill:debug".to_string()),
1456 pinned: false,
1457 }));
1458 assert!(runtime.state_machine().ctx.active_skill_tool_filter().is_some());
1459
1460 runtime.step(KernelInput::new(KernelInputEvent::SkillDeactivated {
1461 name: "debug".to_string(),
1462 }));
1463 let sm = runtime.state_machine();
1464 assert!(!sm.ctx.active_skills.contains_key("debug"));
1465 assert!(sm.ctx.active_skill_tool_filter().is_none(), "toolset re-widens");
1466 assert!(
1467 sm.ctx.partitions.knowledge.entries[0].evict_at_boundary,
1468 "knowledge pin marked for the boundary sweep"
1469 );
1470 }
1471
1472 #[test]
1473 fn skill_lease_expires_after_turns_and_reactivation_renarrows() {
1474 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1478 let mut debug = SkillMetadata::new("debug", "Debug helper");
1479 debug.allowed_tools = vec!["read".into(), "grep".into()];
1480 runtime.step(KernelInput::new(KernelInputEvent::SetAvailableSkills {
1481 skills: vec![debug],
1482 }));
1483 runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1484 name: "debug".to_string(),
1485 lease_turns: Some(1),
1486 }));
1487 assert!(runtime.state_machine().ctx.active_skill_tool_filter().is_some());
1488
1489 run_with_tool_call(&mut runtime, "read");
1492 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1493 results: vec![ToolResult {
1494 call_id: "call-1".into(),
1495 output: crate::types::message::Content::Text("ok".into()),
1496 is_error: false,
1497 is_fatal: false,
1498 error_kind: None,
1499 token_count: None,
1500 }],
1501 }));
1502 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1503 message: Message::assistant("done"),
1504 observed_input_tokens: None,
1505 observed_output_tokens: None,
1506 stop_reason: None,
1507 now_ms: None,
1508 }));
1509 assert!(
1510 !runtime.state_machine().ctx.active_skills.contains_key("debug"),
1511 "lease expired after the turn advanced"
1512 );
1513 assert!(runtime.state_machine().ctx.active_skill_tool_filter().is_none());
1514
1515 runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1517 name: "debug".to_string(),
1518 lease_turns: None,
1519 }));
1520 assert!(runtime.state_machine().ctx.active_skill_tool_filter().is_some());
1521 }
1522
1523 #[test]
1524 fn update_task_input_mutates_task_state() {
1525 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1526 let step = runtime.step(KernelInput::new(KernelInputEvent::UpdateTask {
1527 update: TaskUpdate {
1528 progress: Some("tools executed".to_string()),
1529 ..Default::default()
1530 },
1531 }));
1532
1533 assert!(step.actions.is_empty());
1534 assert_eq!(
1535 runtime.state_machine().ctx.partitions.task_state.progress,
1536 "tools executed"
1537 );
1538 }
1539
1540 #[test]
1541 fn add_knowledge_message_enters_knowledge_partition() {
1542 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1543 let step = runtime.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1544 content: "skill: debug".to_string(),
1545 tokens: 10,
1546 key: None,
1547 pinned: false,
1548 }));
1549
1550 assert!(step.actions.is_empty());
1551 assert_eq!(
1552 runtime.state_machine().ctx.partitions.knowledge.len(),
1553 1
1554 );
1555 }
1556
1557 #[test]
1558 fn knowledge_budget_exceeded_observed_in_live_loop() {
1559 let mut runtime = KernelRuntime::new(LoopPolicy {
1563 max_tokens: 100,
1564 ..LoopPolicy::default()
1565 });
1566 runtime.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1567 content: "reference".to_string(),
1568 tokens: 40,
1569 key: None,
1570 pinned: false,
1571 }));
1572 run_with_tool_call(&mut runtime, "search");
1573 let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1575 results: vec![ToolResult {
1576 call_id: "call-1".into(),
1577 output: crate::types::message::Content::Text("ok".into()),
1578 is_error: false,
1579 is_fatal: false,
1580 error_kind: None,
1581 token_count: None,
1582 }],
1583 }));
1584 assert!(step.observations.iter().any(|o| matches!(
1585 o,
1586 KernelObservation::KnowledgeBudgetExceeded { used: 40, budget: 25, .. }
1587 )));
1588
1589 let mut runtime2 = KernelRuntime::new(LoopPolicy {
1591 max_tokens: 100,
1592 ..LoopPolicy::default()
1593 });
1594 runtime2.step(KernelInput::new(KernelInputEvent::SetKnowledgeBudget { ratio: 0.9 }));
1595 runtime2.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1596 content: "reference".to_string(),
1597 tokens: 40,
1598 key: None,
1599 pinned: false,
1600 }));
1601 run_with_tool_call(&mut runtime2, "search");
1602 let step2 = runtime2.step(KernelInput::new(KernelInputEvent::ToolResults {
1603 results: vec![ToolResult {
1604 call_id: "call-1".into(),
1605 output: crate::types::message::Content::Text("ok".into()),
1606 is_error: false,
1607 is_fatal: false,
1608 error_kind: None,
1609 token_count: None,
1610 }],
1611 }));
1612 assert!(!step2.observations.iter().any(|o| matches!(
1613 o,
1614 KernelObservation::KnowledgeBudgetExceeded { .. }
1615 )));
1616 }
1617
1618 #[test]
1619 fn keyed_add_knowledge_dedupes_and_remove_marks() {
1620 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1624 for content in ["v1", "v2"] {
1625 let ev: KernelInputEvent = serde_json::from_value(serde_json::json!({
1626 "kind": "add_knowledge_message",
1627 "content": content,
1628 "tokens": 5,
1629 "key": "skill:debug",
1630 }))
1631 .unwrap();
1632 runtime.step(KernelInput::new(ev));
1633 }
1634 let knowledge = &runtime.state_machine().ctx.partitions.knowledge;
1635 assert_eq!(knowledge.len(), 1);
1636 assert_eq!(
1637 knowledge.messages().next().and_then(|m| m.content.as_text()),
1638 Some("v1"),
1639 "upsert deferred to boundary — original bytes still rendered"
1640 );
1641
1642 let ev: KernelInputEvent = serde_json::from_value(serde_json::json!({
1643 "kind": "remove_knowledge",
1644 "key": "skill:debug",
1645 }))
1646 .unwrap();
1647 runtime.step(KernelInput::new(ev));
1648 assert!(runtime.state_machine().ctx.partitions.knowledge.entries[0].evict_at_boundary);
1649 }
1650
1651 #[test]
1652 fn capability_mount_emits_observation() {
1653 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1654 let step = runtime.step(KernelInput::new(KernelInputEvent::MountCapability {
1655 capability: CapabilityDescriptor::marker(
1656 CapabilityKind::McpServer,
1657 "docs",
1658 "Documentation server",
1659 ),
1660 }));
1661
1662 assert!(step.actions.is_empty());
1663 assert!(matches!(
1664 step.observations.as_slice(),
1665 [KernelObservation::CapabilityChanged { .. }]
1666 ));
1667 }
1668
1669 #[test]
1670 fn spawn_sub_agent_input_registers_process() {
1671 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1672
1673 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1674 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1675 task: RuntimeTask::new("parent task"),
1676 run_spec: None,
1677 }));
1678 runtime.state_machine_mut().take_observations();
1679
1680 let spec = AgentRunSpec::new(
1681 AgentIdentity::sub_agent("worker", "worker-session"),
1682 AgentRole::Implement,
1683 "do work",
1684 );
1685 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1686 spec,
1687 parent_session_id: "parent-session".to_string(),
1688 }));
1689
1690 assert!(step.actions.is_empty());
1691 assert!(step.observations.iter().any(|o| matches!(
1692 o,
1693 KernelObservation::AgentProcessChanged {
1694 agent_id,
1695 parent_session_id,
1696 state,
1697 ..
1698 } if agent_id == "worker" && parent_session_id == "parent-session" && state == "running"
1699 )));
1700 assert_eq!(
1701 runtime
1702 .state_machine()
1703 .agent_process("worker")
1704 .expect("process")
1705 .parent_session_id
1706 .as_str(),
1707 "parent-session"
1708 );
1709 assert!(step.observations.iter().any(|o| matches!(
1710 o,
1711 KernelObservation::Suspended { reason, .. } if reason == "sub_agent_await"
1712 )));
1713 assert!(runtime.state_machine().is_suspended());
1714 assert!(matches!(
1715 runtime.state_machine().wait_reason(),
1716 Some(crate::scheduler::tcb::WaitReason::SubAgentJoin(_))
1717 ));
1718 }
1719
1720 #[test]
1721 fn set_resource_quota_input_denies_spawn_over_quota() {
1722 use crate::governance::quota::ResourceQuota;
1723 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1724
1725 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1726 let step = runtime.step(KernelInput::new(KernelInputEvent::SetResourceQuota {
1728 quota: ResourceQuota { max_spawn_depth: Some(0), ..ResourceQuota::default() },
1729 }));
1730 assert!(step.actions.is_empty(), "config input yields no actions");
1731
1732 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1733 task: RuntimeTask::new("parent task"),
1734 run_spec: None,
1735 }));
1736 runtime.state_machine_mut().take_observations();
1737
1738 let spec = AgentRunSpec::new(
1739 AgentIdentity::sub_agent("worker", "worker-session"),
1740 AgentRole::Implement,
1741 "do work",
1742 );
1743 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1744 spec,
1745 parent_session_id: "parent-session".to_string(),
1746 }));
1747
1748 assert!(matches!(
1751 step.actions.as_slice(),
1752 [KernelAction::CallProvider { .. }]
1753 ));
1754 assert!(!step.observations.iter().any(|o| matches!(
1755 o,
1756 KernelObservation::AgentProcessChanged { agent_id, .. } if agent_id == "worker"
1757 )));
1758 assert!(runtime.state_machine().agent_process("worker").is_none());
1759 assert!(!runtime.state_machine().is_suspended());
1760 }
1761
1762 #[test]
1763 fn group_budget_base_enforces_shared_token_cap() {
1764 use crate::types::message::{Content, Message, ToolCall, ToolResult};
1765
1766 fn run_one_turn(group_base: Option<u64>) -> KernelStep {
1770 let mut runtime = KernelRuntime::new(LoopPolicy {
1771 max_total_tokens: 100,
1772 ..LoopPolicy::default()
1773 });
1774 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
1775 config: RunConfig { group_tokens_base: group_base, ..RunConfig::default() },
1776 }));
1777 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1778 task: RuntimeTask::new("task"),
1779 run_spec: None,
1780 }));
1781 let mut msg = Message::assistant("");
1782 msg.token_count = Some(10); msg.tool_calls.push(ToolCall {
1784 id: "c1".into(),
1785 name: "echo".into(),
1786 arguments: serde_json::json!({}),
1787 });
1788 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1789 message: msg,
1790 observed_input_tokens: None,
1791 observed_output_tokens: None,
1792 stop_reason: None,
1793 now_ms: None,
1794 }));
1795 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1796 results: vec![ToolResult {
1797 call_id: "c1".into(),
1798 output: Content::Text("ok".into()),
1799 is_error: false,
1800 is_fatal: false,
1801 error_kind: None,
1802 token_count: None,
1803 }],
1804 }))
1805 }
1806
1807 let exceeded = |step: &KernelStep| {
1808 step.observations.iter().any(|o| {
1809 matches!(o, KernelObservation::BudgetExceeded { budget, .. } if budget == "token_budget")
1810 })
1811 };
1812
1813 assert!(
1815 exceeded(&run_one_turn(Some(95))),
1816 "group token budget must span the whole domain"
1817 );
1818 assert!(
1820 !exceeded(&run_one_turn(None)),
1821 "no group seed ⇒ per-vehicle budget, well under cap"
1822 );
1823 }
1824
1825 #[test]
1826 fn group_spawns_base_enforces_cumulative_spawn_cap() {
1827 use crate::governance::quota::ResourceQuota;
1828 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1829
1830 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1833 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
1834 config: RunConfig {
1835 resource_quota: Some(ResourceQuota {
1836 max_total_subagents: Some(2),
1837 ..ResourceQuota::default()
1838 }),
1839 group_spawns_base: Some(2),
1840 ..RunConfig::default()
1841 },
1842 }));
1843 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1844 task: RuntimeTask::new("task"),
1845 run_spec: None,
1846 }));
1847 runtime.state_machine_mut().take_observations();
1848
1849 let spec = AgentRunSpec::new(
1850 AgentIdentity::sub_agent("worker", "worker-session"),
1851 AgentRole::Implement,
1852 "do work",
1853 );
1854 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1855 spec,
1856 parent_session_id: "parent-session".to_string(),
1857 }));
1858
1859 assert!(matches!(
1861 step.actions.as_slice(),
1862 [KernelAction::CallProvider { .. }]
1863 ));
1864 assert!(runtime.state_machine().agent_process("worker").is_none());
1865 assert_eq!(runtime.local_subagents_spawned(), 0);
1866 }
1867
1868 #[test]
1869 fn default_runtime_leaves_spawn_unquota_ed() {
1870 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1871
1872 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1874 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1875 task: RuntimeTask::new("parent task"),
1876 run_spec: None,
1877 }));
1878 runtime.state_machine_mut().take_observations();
1879
1880 let spec = AgentRunSpec::new(
1881 AgentIdentity::sub_agent("worker", "worker-session"),
1882 AgentRole::Implement,
1883 "do work",
1884 );
1885 runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1886 spec,
1887 parent_session_id: "parent-session".to_string(),
1888 }));
1889 assert!(runtime.state_machine().agent_process("worker").is_some());
1890 assert!(runtime.state_machine().is_suspended());
1891 }
1892
1893 #[test]
1898 fn agent_process_changed_locks_multiword_wire_form() {
1899 use crate::types::agent::{AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec};
1900
1901 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1902 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1903 task: RuntimeTask::new("parent task"),
1904 run_spec: None,
1905 }));
1906 runtime.state_machine_mut().take_observations();
1907
1908 let spec = AgentRunSpec::new(
1910 AgentIdentity::sub_agent("worker", "worker-session"),
1911 AgentRole::Verify,
1912 "do work",
1913 )
1914 .with_isolation(AgentIsolation::ReadOnly);
1915 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1916 spec,
1917 parent_session_id: "parent-session".to_string(),
1918 }));
1919
1920 let obs = step
1921 .observations
1922 .iter()
1923 .find(|o| matches!(o, KernelObservation::AgentProcessChanged { .. }))
1924 .expect("agent_process_changed observation");
1925 let json = serde_json::to_value(obs).unwrap();
1926 assert_eq!(json["isolation"], "readonly", "isolation must stay debug-lowercase");
1927 assert_eq!(
1928 json["context_inheritance"], "systemonly",
1929 "context_inheritance must stay debug-lowercase"
1930 );
1931 assert_eq!(json["role"], "verify");
1932 assert_eq!(json["state"], "running");
1933 }
1934
1935 fn write_memory(runtime: &mut KernelRuntime, name: &str, content: &str) -> KernelStep {
1938 use crate::mm::memory::{MemoryMetadata, MemoryWriteRequest};
1939 runtime.step(KernelInput::new(KernelInputEvent::WriteMemory {
1940 memory: MemoryWriteRequest {
1941 metadata: MemoryMetadata {
1942 name: name.to_string(),
1943 description: "desc".to_string(),
1944 ..Default::default()
1945 },
1946 content: content.to_string(),
1947 },
1948 }))
1949 }
1950
1951 #[test]
1952 fn memory_policy_validation_disabled_admits_forbidden_write() {
1953 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1955 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1956 memory_path: String::new(),
1957 stale_warning_days: 2,
1958 retrieval_top_k: 5,
1959 validation_enabled: false,
1960 max_content_bytes: None,
1961 max_name_length: None,
1962 }));
1963 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1964 assert!(step
1965 .observations
1966 .iter()
1967 .any(|o| matches!(o, KernelObservation::MemoryWritten { .. })));
1968 assert!(!step
1969 .observations
1970 .iter()
1971 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1972 }
1973
1974 #[test]
1975 fn default_runtime_validates_forbidden_write() {
1976 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1978 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1979 assert!(step
1980 .observations
1981 .iter()
1982 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1983 }
1984
1985 #[test]
1986 fn memory_policy_size_override_rejects_oversized_write() {
1987 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1988 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1989 memory_path: String::new(),
1990 stale_warning_days: 2,
1991 retrieval_top_k: 5,
1992 validation_enabled: true,
1993 max_content_bytes: Some(8),
1994 max_name_length: None,
1995 }));
1996 let step = write_memory(&mut runtime, "note", "this content is well over eight bytes");
1997 let failed = step.observations.iter().find_map(|o| match o {
1998 KernelObservation::MemoryValidationFailed { error, .. } => Some(error.clone()),
1999 _ => None,
2000 });
2001 assert!(failed.is_some_and(|e| e.contains("too large")));
2002 }
2003
2004 #[test]
2005 fn memory_policy_clamps_retrieval_top_k() {
2006 use crate::mm::memory::MemoryQuery;
2007 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2008 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
2009 memory_path: String::new(),
2010 stale_warning_days: 2,
2011 retrieval_top_k: 3,
2012 validation_enabled: true,
2013 max_content_bytes: None,
2014 max_name_length: None,
2015 }));
2016 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
2017 query: MemoryQuery { top_k: 50, ..Default::default() },
2018 }));
2019 let requested = step.observations.iter().find_map(|o| match o {
2020 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
2021 _ => None,
2022 });
2023 assert_eq!(requested, Some(3));
2024 }
2025
2026 #[test]
2027 fn default_runtime_uses_requested_top_k_verbatim() {
2028 use crate::mm::memory::MemoryQuery;
2029 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2030 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
2031 query: MemoryQuery { top_k: 50, ..Default::default() },
2032 }));
2033 let requested = step.observations.iter().find_map(|o| match o {
2034 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
2035 _ => None,
2036 });
2037 assert_eq!(requested, Some(50));
2038 }
2039
2040 #[test]
2041 fn provider_result_now_ms_drives_wall_time_budget() {
2042 let mut runtime = KernelRuntime::new(LoopPolicy {
2043 max_wall_ms: Some(10),
2044 ..LoopPolicy::default()
2045 });
2046 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2047 task: RuntimeTask::new("ship it"),
2048 run_spec: None,
2049 }));
2050 let mut msg = Message::assistant("");
2051 msg.tool_calls.push(ToolCall {
2052 id: "call-1".into(),
2053 name: "echo".into(),
2054 arguments: serde_json::json!({}),
2055 });
2056 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2057 message: msg,
2058 observed_input_tokens: None,
2059 observed_output_tokens: None,
2060 stop_reason: None,
2061 now_ms: Some(100),
2062 }));
2063 let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
2064 results: vec![ToolResult {
2065 call_id: "call-1".into(),
2066 output: crate::types::message::Content::Text("ok".into()),
2067 is_error: false,
2068 is_fatal: false,
2069 error_kind: None,
2070 token_count: None,
2071 }],
2072 }));
2073
2074 assert!(matches!(
2075 step.actions.as_slice(),
2076 [KernelAction::CallProvider { tools, .. }] if tools.is_empty()
2077 ));
2078 }
2079
2080 fn assistant_calling(tool: &str) -> Message {
2083 let mut msg = Message::assistant("");
2084 msg.tool_calls.push(ToolCall {
2085 id: "call-1".into(),
2086 name: tool.into(),
2087 arguments: serde_json::json!({}),
2088 });
2089 msg
2090 }
2091
2092 fn run_with_tool_call(runtime: &mut KernelRuntime, tool: &str) -> KernelStep {
2094 run_with_tool_call_named(runtime, tool, "call-1")
2095 }
2096
2097 fn run_with_tool_call_named(
2098 runtime: &mut KernelRuntime,
2099 tool: &str,
2100 call_id: &str,
2101 ) -> KernelStep {
2102 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2103 task: RuntimeTask::new("do the thing"),
2104 run_spec: None,
2105 }));
2106 runtime.state_machine_mut().take_observations();
2107 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2108 message: assistant_calling(tool),
2109 observed_input_tokens: None,
2110 observed_output_tokens: None,
2111 stop_reason: None,
2112 now_ms: None,
2113 }))
2114 }
2115
2116 #[test]
2117 fn governance_deny_blocks_tool_and_reprompts() {
2118 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2119 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2120 default_action: Some(PolicyAction::Allow),
2121 rules: vec![PolicyRule {
2122 tool_pattern: "danger.*".to_string(),
2123 action: PolicyAction::Deny,
2124 }],
2125 vetoed_tools: vec![],
2126 rate_limits: vec![],
2127 constraints: vec![],
2128 }));
2129
2130 let step = run_with_tool_call(&mut runtime, "danger.delete");
2131
2132 assert!(
2134 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2135 "denied tool should roll back and re-call provider, got {:?}",
2136 step.actions
2137 );
2138 assert!(
2139 step.observations
2140 .iter()
2141 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2142 "expected a Rollbacked observation for the denied turn",
2143 );
2144 }
2145
2146 #[test]
2147 fn configure_run_bundle_applies_governance_equivalently_to_load_governance_policy() {
2148 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2151 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
2152 config: RunConfig {
2153 tools: Some(vec![]),
2154 governance: Some(GovernanceConfig {
2155 default_action: Some(PolicyAction::Allow),
2156 rules: vec![PolicyRule {
2157 tool_pattern: "danger.*".to_string(),
2158 action: PolicyAction::Deny,
2159 }],
2160 ..GovernanceConfig::default()
2161 }),
2162 attention_max_queue_size: Some(32),
2163 ..RunConfig::default()
2164 },
2165 }));
2166
2167 let step = run_with_tool_call(&mut runtime, "danger.delete");
2168
2169 assert!(
2170 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2171 "bundle-configured deny should roll back and re-call provider, got {:?}",
2172 step.actions
2173 );
2174 assert!(
2175 step.observations
2176 .iter()
2177 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2178 "expected a Rollbacked observation for the bundle-denied turn",
2179 );
2180 }
2181
2182 #[test]
2183 fn configure_run_round_trips_over_the_abi() {
2184 let event = KernelInputEvent::ConfigureRun {
2187 config: RunConfig {
2188 resource_quota: Some(crate::governance::quota::ResourceQuota {
2189 max_concurrent_subagents: Some(2),
2190 ..Default::default()
2191 }),
2192 scheduler_max_wall_ms: Some(60_000),
2193 plan_tool_enabled: Some(true),
2194 ..RunConfig::default()
2195 },
2196 };
2197 let json = serde_json::to_string(&event).expect("serialize");
2198 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2199 assert!(matches!(parsed, KernelInputEvent::ConfigureRun { .. }));
2200 }
2201
2202 #[test]
2203 fn governance_ask_user_suspends_until_resume() {
2204 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2205 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2206 default_action: Some(PolicyAction::Allow),
2207 rules: vec![PolicyRule {
2208 tool_pattern: "sensitive.*".to_string(),
2209 action: PolicyAction::AskUser,
2210 }],
2211 vetoed_tools: vec![],
2212 rate_limits: vec![],
2213 constraints: vec![],
2214 }));
2215
2216 let step = run_with_tool_call(&mut runtime, "sensitive.read");
2217
2218 assert!(
2219 step.actions.is_empty(),
2220 "AskUser should suspend without ExecuteTool, got {:?}",
2221 step.actions
2222 );
2223 assert!(
2224 step.observations.iter().any(|o| matches!(
2225 o,
2226 KernelObservation::ToolGated { tool, .. } if tool == "sensitive.read"
2227 )),
2228 "expected a ToolGated observation for the AskUser call",
2229 );
2230 assert!(
2231 step.observations.iter().any(|o| matches!(
2232 o,
2233 KernelObservation::Suspended { reason, .. } if reason == "ask_user"
2234 )),
2235 "expected a Suspended observation",
2236 );
2237
2238 let resumed = runtime.step(KernelInput::new(KernelInputEvent::Resume {
2239 approved_calls: vec!["call-1".to_string()],
2240 denied_calls: vec![],
2241 }));
2242 assert!(
2243 matches!(resumed.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
2244 "resume with approval should emit ExecuteTool, got {:?}",
2245 resumed.actions
2246 );
2247 assert!(
2248 resumed.observations.iter().any(|o| matches!(
2249 o,
2250 KernelObservation::Resumed { approved, denied, .. }
2251 if approved == &["call-1"] && denied.is_empty()
2252 )),
2253 );
2254 }
2255
2256 #[test]
2257 fn governance_ask_user_resume_all_denied_feeds_tool_results() {
2258 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2259 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2260 default_action: Some(PolicyAction::Allow),
2261 rules: vec![PolicyRule {
2262 tool_pattern: "sensitive.*".to_string(),
2263 action: PolicyAction::AskUser,
2264 }],
2265 vetoed_tools: vec![],
2266 rate_limits: vec![],
2267 constraints: vec![],
2268 }));
2269 run_with_tool_call(&mut runtime, "sensitive.read");
2270 runtime.state_machine_mut().take_observations();
2271
2272 let step = runtime.step(KernelInput::new(KernelInputEvent::Resume {
2273 approved_calls: vec![],
2274 denied_calls: vec!["call-1".to_string()],
2275 }));
2276 assert!(
2277 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2278 "all denied should re-prompt provider, got {:?}",
2279 step.actions
2280 );
2281 }
2282
2283 #[test]
2284 fn no_governance_policy_executes_all_tools() {
2285 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2286 let step = run_with_tool_call(&mut runtime, "danger.delete");
2287
2288 assert!(matches!(
2290 step.actions.as_slice(),
2291 [KernelAction::ExecuteTool { .. }]
2292 ));
2293 assert!(
2294 !step
2295 .observations
2296 .iter()
2297 .any(|o| matches!(o, KernelObservation::ToolGated { .. })),
2298 );
2299 }
2300
2301 fn tool_ok(call_id: &str) -> ToolResult {
2302 ToolResult {
2303 call_id: call_id.into(),
2304 output: crate::types::message::Content::Text("ok".to_string()),
2305 is_error: false,
2306 is_fatal: false,
2307 error_kind: None,
2308 token_count: None,
2309 }
2310 }
2311
2312 #[test]
2313 fn governance_rate_limit_blocks_second_call() {
2314 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2315 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2316 default_action: Some(PolicyAction::Allow),
2317 rules: vec![],
2318 vetoed_tools: vec![],
2319 rate_limits: vec![RateLimitSpec {
2320 tool: "fetch".to_string(),
2321 max_calls: 1,
2322 window_ms: 60_000,
2323 }],
2324 constraints: vec![],
2325 }));
2326 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2327 task: RuntimeTask::new("fetch twice"),
2328 run_spec: None,
2329 }));
2330 runtime.state_machine_mut().take_observations();
2331
2332 let s1 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2334 message: assistant_calling("fetch"),
2335 observed_input_tokens: None,
2336 observed_output_tokens: None,
2337 stop_reason: None,
2338 now_ms: Some(1_000),
2339 }));
2340 assert!(
2341 matches!(s1.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
2342 "first call should execute, got {:?}",
2343 s1.actions
2344 );
2345
2346 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
2348 results: vec![tool_ok("call-1")],
2349 }));
2350 runtime.state_machine_mut().take_observations();
2351
2352 let s2 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2354 message: assistant_calling("fetch"),
2355 observed_input_tokens: None,
2356 observed_output_tokens: None,
2357 stop_reason: None,
2358 now_ms: Some(1_001),
2359 }));
2360 assert!(
2361 matches!(s2.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2362 "rate-limited call should roll back and re-call provider, got {:?}",
2363 s2.actions
2364 );
2365 assert!(
2366 s2.observations
2367 .iter()
2368 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2369 "expected a Rollbacked observation for the rate-limited turn",
2370 );
2371 }
2372
2373 #[test]
2374 fn governance_constraint_required_param_denies() {
2375 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2376 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2377 default_action: Some(PolicyAction::Allow),
2378 rules: vec![],
2379 vetoed_tools: vec![],
2380 rate_limits: vec![],
2381 constraints: vec![ConstraintSpec::Required {
2382 tool: "write".to_string(),
2383 path: "path".to_string(),
2384 }],
2385 }));
2386
2387 let step = run_with_tool_call(&mut runtime, "write");
2389 assert!(
2390 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2391 "missing required param should roll back, got {:?}",
2392 step.actions
2393 );
2394 assert!(
2395 step.observations
2396 .iter()
2397 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2398 "expected a Rollbacked observation for the constraint violation",
2399 );
2400 }
2401
2402 fn signal(urgency: crate::types::signal::Urgency, summary: &str) -> crate::types::signal::RuntimeSignal {
2405 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType};
2406 RuntimeSignal::new(SignalSource::Gateway, SignalType::Alert, urgency, summary)
2407 }
2408
2409 fn started_runtime_with_attention(max_queue: u32) -> KernelRuntime {
2410 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2411 runtime.step(KernelInput::new(KernelInputEvent::SetAttentionPolicy {
2412 max_queue_size: max_queue,
2413 }));
2414 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2415 task: RuntimeTask::new("watch for signals"),
2416 run_spec: None,
2417 }));
2418 runtime.state_machine_mut().take_observations();
2419 runtime
2420 }
2421
2422 #[test]
2423 fn attention_policy_critical_signal_interrupts() {
2424 use crate::types::signal::Urgency;
2425 let mut runtime = started_runtime_with_attention(8);
2426 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2427 signal: signal(Urgency::Critical, "fire"),
2428 }));
2429 assert!(
2430 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2431 "critical signal should drive a provider call, got {:?}",
2432 step.actions
2433 );
2434 assert!(step.observations.iter().any(|o| matches!(
2435 o,
2436 KernelObservation::SignalDisposed { disposition, .. } if disposition == "interrupt_now"
2437 )));
2438 }
2439
2440 #[test]
2441 fn attention_policy_normal_signal_queues_without_action() {
2442 use crate::types::signal::Urgency;
2443 let mut runtime = started_runtime_with_attention(8);
2444 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2445 signal: signal(Urgency::Normal, "job"),
2446 }));
2447 assert!(
2448 step.actions.is_empty(),
2449 "normal signal should queue without a provider call, got {:?}",
2450 step.actions
2451 );
2452 assert!(step.observations.iter().any(|o| matches!(
2453 o,
2454 KernelObservation::SignalDisposed { disposition, queue_depth, .. }
2455 if disposition == "queue" && *queue_depth == 1
2456 )));
2457 }
2458
2459 #[test]
2460 fn attention_policy_full_queue_drops() {
2461 use crate::types::signal::Urgency;
2462 let mut runtime = started_runtime_with_attention(1);
2463 runtime.step(KernelInput::new(KernelInputEvent::Signal {
2464 signal: signal(Urgency::Normal, "first"),
2465 }));
2466 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2467 signal: signal(Urgency::Normal, "second"),
2468 }));
2469 assert!(step.observations.iter().any(|o| matches!(
2470 o,
2471 KernelObservation::SignalDisposed { disposition, .. } if disposition == "dropped"
2472 )));
2473 }
2474
2475 #[test]
2476 #[test]
2477 fn page_in_populates_knowledge_partition() {
2478 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2479 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
2480 enabled: true,
2481 }));
2482 let before = runtime.state_machine().ctx.partitions.knowledge.len();
2483 runtime.step(KernelInput::new(KernelInputEvent::PageIn {
2484 entries: vec![crate::mm::PageInEntry {
2485 content: "[memory] prior fix".to_string(),
2486 tokens: Some(10),
2487 source: Some("memory".to_string()),
2488 key: None,
2489 pinned: false,
2490 }],
2491 }));
2492 let after = runtime.state_machine().ctx.partitions.knowledge.len();
2493 assert!(after > before, "page-in should add knowledge messages");
2494 }
2495
2496 #[test]
2497 fn memory_tool_does_not_emit_page_in_requested() {
2498 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2502 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
2503 enabled: true,
2504 }));
2505 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2506 task: RuntimeTask::new("test"),
2507 run_spec: None,
2508 }));
2509 runtime.state_machine_mut().take_observations();
2510
2511 let step = run_with_tool_call(&mut runtime, "memory");
2512 assert!(!step.observations.iter().any(|o| matches!(o, KernelObservation::PageInRequested { .. })));
2513 }
2514
2515 #[test]
2516 fn load_workflow_input_drives_dag_to_completion() {
2517 use crate::orchestration::workflow::fanout_synthesize;
2518 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2519
2520 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2521 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2522 task: RuntimeTask::new("parent task"),
2523 run_spec: None,
2524 }));
2525 runtime.state_machine_mut().take_observations();
2526
2527 let spec =
2529 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2530 let event = KernelInputEvent::LoadWorkflow {
2531 spec,
2532 parent_session_id: "sess".to_string(),
2533 resumed_completed: Vec::new(),
2534 resumed_submissions: Vec::new(),
2535 };
2536 let json = serde_json::to_string(&event).expect("serialize");
2537 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2538
2539 let step = runtime.step(KernelInput::new(parsed));
2540 let batch = step
2542 .observations
2543 .iter()
2544 .find_map(|o| match o {
2545 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2546 _ => None,
2547 })
2548 .expect("workflow_batch_spawned");
2549 assert_eq!(batch.len(), 2);
2550 let goals: Vec<&str> = batch.iter().map(|n| n.goal.as_str()).collect();
2551 assert!(goals.contains(&"w0") && goals.contains(&"w1"));
2552 assert_eq!(batch[0].agent_id, "wf-node0");
2553 assert_eq!(batch[0].isolation, "read_only"); let complete = |runtime: &mut KernelRuntime, id: &str| {
2556 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2557 result: SubAgentResult {
2558 agent_id: compact_str::CompactString::new(id),
2559 result: LoopResult {
2560 termination: TerminationReason::Completed,
2561 final_message: None,
2562 turns_used: 1,
2563 total_tokens_used: 1,
2564 loop_continue: None,
2565 classify_branch: None,
2566 tournament_winner: None,
2567 },
2568 },
2569 }))
2570 };
2571
2572 complete(&mut runtime, "wf-node0");
2573 let step = complete(&mut runtime, "wf-node1");
2575 assert!(step.observations.iter().any(|o| matches!(
2576 o,
2577 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2578 if nodes.len() == 1 && nodes[0].agent_id == "wf-node2"
2579 )));
2580
2581 let step = complete(&mut runtime, "wf-node2");
2583 assert!(step.observations.iter().any(|o| matches!(
2584 o,
2585 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 3
2586 )));
2587 }
2588
2589 #[test]
2590 fn load_workflow_self_bootstraps_with_no_prior_start_run() {
2591 use crate::orchestration::workflow::fanout_synthesize;
2595 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2596
2597 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2598 let spec =
2601 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2602 let step = runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2603 spec,
2604 parent_session_id: "sess".to_string(),
2605 resumed_completed: Vec::new(),
2606 resumed_submissions: Vec::new(),
2607 }));
2608 let batch = step
2609 .observations
2610 .iter()
2611 .find_map(|o| match o {
2612 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2613 _ => None,
2614 })
2615 .expect("workflow_batch_spawned even without a prior StartRun");
2616 assert_eq!(batch.len(), 2);
2617
2618 let complete = |runtime: &mut KernelRuntime, id: &str| {
2619 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2620 result: SubAgentResult {
2621 agent_id: compact_str::CompactString::new(id),
2622 result: LoopResult {
2623 termination: TerminationReason::Completed,
2624 final_message: None,
2625 turns_used: 1,
2626 total_tokens_used: 1,
2627 loop_continue: None,
2628 classify_branch: None,
2629 tournament_winner: None,
2630 },
2631 },
2632 }))
2633 };
2634 complete(&mut runtime, "wf-node0");
2635 complete(&mut runtime, "wf-node1");
2636 let step = complete(&mut runtime, "wf-node2");
2637 assert!(step.observations.iter().any(|o| matches!(
2638 o,
2639 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 3
2640 )));
2641 }
2642
2643 #[test]
2644 fn submit_workflow_nodes_input_appends_a_node_over_the_abi() {
2645 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
2648 use crate::types::agent::AgentRole;
2649 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2650
2651 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2652 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2653 task: RuntimeTask::new("parent task"),
2654 run_spec: None,
2655 }));
2656 runtime.state_machine_mut().take_observations();
2657
2658 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
2660 RuntimeTask::new("root"),
2661 AgentRole::Implement,
2662 )]);
2663 runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2664 spec,
2665 parent_session_id: "sess".to_string(),
2666 resumed_completed: Vec::new(),
2667 resumed_submissions: Vec::new(),
2668 }));
2669 runtime.state_machine_mut().take_observations();
2670
2671 let event = KernelInputEvent::SubmitWorkflowNodes {
2673 nodes: vec![WorkflowNode::new(RuntimeTask::new("more"), AgentRole::Implement)],
2674 submitter_agent_id: None,
2675 };
2676 let json = serde_json::to_string(&event).expect("serialize");
2677 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2678 let step = runtime.step(KernelInput::new(parsed));
2679 assert!(step.observations.iter().any(|o| matches!(
2681 o,
2682 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2683 if nodes.len() == 1 && nodes[0].agent_id == "wf-node1" && nodes[0].goal == "more"
2684 )));
2685
2686 let complete = |runtime: &mut KernelRuntime, id: &str| {
2687 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2688 result: SubAgentResult {
2689 agent_id: compact_str::CompactString::new(id),
2690 result: LoopResult {
2691 termination: TerminationReason::Completed,
2692 final_message: None,
2693 turns_used: 1,
2694 total_tokens_used: 1,
2695 loop_continue: None,
2696 classify_branch: None,
2697 tournament_winner: None,
2698 },
2699 },
2700 }))
2701 };
2702 complete(&mut runtime, "wf-node0");
2703 let step = complete(&mut runtime, "wf-node1");
2705 assert!(step.observations.iter().any(|o| matches!(
2706 o,
2707 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 2
2708 )));
2709 }
2710
2711 #[test]
2712 fn submit_workflow_input_bootstraps_a_dag_over_the_abi() {
2713 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
2716 use crate::types::agent::AgentRole;
2717 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2718
2719 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2720 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2721 task: RuntimeTask::new("parent task"),
2722 run_spec: None,
2723 }));
2724 runtime.state_machine_mut().take_observations();
2725
2726 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
2728 RuntimeTask::new("authored root"),
2729 AgentRole::Implement,
2730 )]);
2731 let event = KernelInputEvent::SubmitWorkflow {
2732 spec,
2733 parent_session_id: "sess".to_string(),
2734 submitter_agent_id: None,
2735 };
2736 let json = serde_json::to_string(&event).expect("serialize");
2737 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2738 let step = runtime.step(KernelInput::new(parsed));
2739 assert!(step.observations.iter().any(|o| matches!(
2741 o,
2742 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2743 if nodes.len() == 1 && nodes[0].agent_id == "wf-node0" && nodes[0].goal == "authored root"
2744 )));
2745
2746 let step = runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2747 result: SubAgentResult {
2748 agent_id: compact_str::CompactString::new("wf-node0"),
2749 result: LoopResult {
2750 termination: TerminationReason::Completed,
2751 final_message: None,
2752 turns_used: 1,
2753 total_tokens_used: 1,
2754 loop_continue: None,
2755 classify_branch: None,
2756 tournament_winner: None,
2757 },
2758 },
2759 }));
2760 assert!(step.observations.iter().any(|o| matches!(
2761 o,
2762 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 1
2763 )));
2764 }
2765
2766 #[test]
2767 fn load_workflow_resumes_from_completed_nodes() {
2768 use crate::orchestration::workflow::fanout_synthesize;
2769
2770 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2771 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2772 task: RuntimeTask::new("parent task"),
2773 run_spec: None,
2774 }));
2775 runtime.state_machine_mut().take_observations();
2776
2777 let spec =
2779 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2780 let step = runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2781 spec,
2782 parent_session_id: "sess".to_string(),
2783 resumed_completed: vec!["wf-node0".to_string()],
2784 resumed_submissions: Vec::new(),
2785 }));
2786
2787 let batch = step
2789 .observations
2790 .iter()
2791 .find_map(|o| match o {
2792 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2793 _ => None,
2794 })
2795 .expect("workflow_batch_spawned");
2796 assert_eq!(batch.len(), 1);
2797 assert_eq!(batch[0].agent_id, "wf-node1");
2798 }
2799}