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}
164
165pub(crate) fn build_governance_pipeline(
169 default_action: Option<PolicyAction>,
170 rules: Vec<PolicyRule>,
171 vetoed_tools: Vec<String>,
172 rate_limits: Vec<RateLimitSpec>,
173 constraints: Vec<ConstraintSpec>,
174) -> crate::governance::pipeline::GovernancePipeline {
175 use crate::governance::constraint::{ConstraintRule, ParamConstraint};
176 use crate::governance::permission::PermissionRule;
177 use crate::governance::rate_limit::RateLimit;
178 let default = default_action.unwrap_or(PolicyAction::Allow).into();
179 let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(default);
180 for rule in rules {
181 pipeline.permission.add_rule(PermissionRule {
182 tool_pattern: rule.tool_pattern.into(),
183 action: rule.action.into(),
184 });
185 }
186 for tool in vetoed_tools {
187 pipeline.veto.block_tool(tool);
188 }
189 for rl in rate_limits {
190 pipeline.rate_limiter.set_limit(
191 rl.tool,
192 RateLimit {
193 max_calls: rl.max_calls,
194 window_ms: rl.window_ms,
195 },
196 );
197 }
198 for c in constraints {
199 let (tool_name, param_path, rule) = match c {
200 ConstraintSpec::Required { tool, path } => (tool, path, ConstraintRule::Required),
201 ConstraintSpec::Enum { tool, path, values } => (tool, path, ConstraintRule::Enum(values)),
202 ConstraintSpec::Range { tool, path, min, max } => {
203 (tool, path, ConstraintRule::Range { min, max })
204 }
205 };
206 pipeline.constraints.add(ParamConstraint {
207 tool_name,
208 param_path,
209 rule,
210 });
211 }
212 pipeline
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(tag = "kind", rename_all = "snake_case")]
217pub enum KernelInputEvent {
218 SetTools {
219 tools: Vec<ToolSchema>,
220 },
221 SetAvailableSkills {
222 skills: Vec<SkillMetadata>,
223 },
224 SkillActivated {
228 name: String,
229 },
230 SetStableCoreTools {
233 tool_ids: Vec<String>,
234 },
235 SetMemoryEnabled {
236 enabled: bool,
237 },
238 SetKnowledgeEnabled {
239 enabled: bool,
240 },
241 SetPlanToolEnabled {
242 enabled: bool,
243 },
244 SetTokenizer {
245 name: String,
246 },
247 AddSystemMessage {
248 content: String,
249 tokens: u32,
250 },
251 AddKnowledgeMessage {
252 content: String,
253 tokens: u32,
254 },
255 AddHistoryMessage {
256 message: Message,
257 tokens: Option<u32>,
258 },
259 PreloadHistory {
260 messages: Vec<Message>,
261 },
262 MountCapability {
263 capability: CapabilityDescriptor,
264 },
265 UnmountCapability {
266 capability_kind: CapabilityKind,
267 id: String,
268 },
269 LoadMilestoneContract {
270 contract: MilestoneContract,
271 },
272 LoadGovernancePolicy {
276 #[serde(default)]
277 default_action: Option<PolicyAction>,
278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
279 rules: Vec<PolicyRule>,
280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 vetoed_tools: Vec<String>,
282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 rate_limits: Vec<RateLimitSpec>,
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 constraints: Vec<ConstraintSpec>,
288 },
289 SetAttentionPolicy {
292 #[serde(default = "default_signal_queue_size")]
293 max_queue_size: u32,
294 },
295 ForceCompact,
296 UpdateTask {
297 update: TaskUpdate,
298 },
299 StartRun {
300 task: RuntimeTask,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 run_spec: Option<AgentRunSpec>,
303 },
304 ConfigureRun {
309 config: RunConfig,
310 },
311 CapabilityCommand {
312 command: CapabilityCommand,
313 },
314 Resume {
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
319 approved_calls: Vec<String>,
320 #[serde(default, skip_serializing_if = "Vec::is_empty")]
321 denied_calls: Vec<String>,
322 },
323 SetSchedulerBudget {
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 max_wall_ms: Option<u64>,
329 },
330 SetResourceQuota {
336 quota: crate::governance::quota::ResourceQuota,
337 },
338 ProviderResult {
339 message: Message,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 observed_input_tokens: Option<u32>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 observed_output_tokens: Option<u32>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
348 now_ms: Option<u64>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
353 stop_reason: Option<String>,
354 },
355 ToolResults {
356 results: Vec<ToolResult>,
357 },
358 ProviderError {
365 message: String,
366 },
367 Signal {
368 signal: RuntimeSignal,
369 },
370 MilestoneResult {
371 result: MilestoneCheckResult,
372 },
373 SpawnSubAgent {
375 spec: AgentRunSpec,
376 parent_session_id: String,
377 },
378 LoadWorkflow {
383 spec: crate::orchestration::workflow::WorkflowSpec,
384 parent_session_id: String,
385 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 resumed_completed: Vec<String>,
388 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 resumed_submissions: Vec<Vec<crate::orchestration::workflow::WorkflowNode>>,
393 },
394 SubAgentCompleted {
396 result: SubAgentResult,
397 },
398 SubmitWorkflowNodes {
403 #[serde(default, skip_serializing_if = "Vec::is_empty")]
404 nodes: Vec<crate::orchestration::workflow::WorkflowNode>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
409 submitter_agent_id: Option<String>,
410 },
411 SubmitWorkflow {
417 spec: crate::orchestration::workflow::WorkflowSpec,
418 #[serde(default)]
420 parent_session_id: String,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
424 submitter_agent_id: Option<String>,
425 },
426 PageIn {
429 #[serde(default, skip_serializing_if = "Vec::is_empty")]
430 entries: Vec<crate::mm::PageInEntry>,
431 },
432 SetMemoryPolicy {
435 #[serde(default)]
436 memory_path: String,
437 #[serde(default = "default_stale_days")]
438 stale_warning_days: u32,
439 #[serde(default = "default_top_k")]
440 retrieval_top_k: usize,
441 #[serde(default = "default_validation_enabled")]
442 validation_enabled: bool,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 max_content_bytes: Option<u32>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 max_name_length: Option<usize>,
449 },
450 WriteMemory {
452 memory: crate::mm::memory::MemoryWriteRequest,
453 },
454 QueryMemory {
456 query: crate::mm::memory::MemoryQuery,
457 },
458 MemoryRetrievalResult {
460 retrieval: crate::mm::memory::MemoryRetrieval,
461 },
462 Timeout,
463}
464
465fn default_stale_days() -> u32 { 2 }
466fn default_top_k() -> usize { 5 }
467fn default_validation_enabled() -> bool { true }
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct KernelStep {
471 pub version: u32,
472 pub actions: Vec<KernelAction>,
473 pub observations: Vec<KernelObservation>,
474}
475
476impl KernelStep {
477 fn empty(observations: Vec<KernelObservation>) -> Self {
478 Self {
479 version: KERNEL_ABI_VERSION,
480 actions: Vec::new(),
481 observations,
482 }
483 }
484
485 fn single(action: LoopAction, observations: Vec<KernelObservation>) -> Self {
486 Self {
487 version: KERNEL_ABI_VERSION,
488 actions: vec![action.into()],
489 observations,
490 }
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495#[serde(tag = "kind", rename_all = "snake_case")]
496pub enum KernelAction {
497 CallProvider {
498 context: RenderedContext,
499 tools: Vec<ToolSchema>,
500 },
501 ExecuteTool {
502 calls: Vec<ToolCall>,
503 },
504 EvaluateMilestone {
505 phase_id: String,
506 criteria: Vec<String>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 verifier: Option<crate::types::milestone::MilestoneVerifier>,
509 #[serde(default, skip_serializing_if = "Vec::is_empty")]
510 required_evidence: Vec<String>,
511 },
512 Done {
513 result: LoopResult,
514 },
515}
516
517impl From<LoopAction> for KernelAction {
518 fn from(action: LoopAction) -> Self {
519 match action {
520 LoopAction::AwaitingResume => {
521 panic!("AwaitingResume must not be converted to KernelAction")
522 }
523 LoopAction::CallLLM { context, tools } => Self::CallProvider { context, tools },
524 LoopAction::ExecuteTools { calls } => Self::ExecuteTool { calls },
525 LoopAction::EvaluateMilestone {
526 phase_id,
527 criteria,
528 verifier,
529 required_evidence,
530 } => Self::EvaluateMilestone {
531 phase_id,
532 criteria,
533 verifier,
534 required_evidence,
535 },
536 LoopAction::Done { result } => Self::Done { result },
537 }
538 }
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
542#[serde(tag = "kind", rename_all = "snake_case")]
543pub enum KernelObservation {
544 Compressed {
545 action: KernelPressureAction,
546 rho_after: f64,
547 summary: Option<String>,
548 archived: Vec<Message>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
553 invalidates_prefix_at: Option<usize>,
554 },
555 Renewed {
556 sprint: u32,
557 },
558 Rollbacked {
559 turn: u32,
560 checkpoint_history_len: u32,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 reason: Option<RollbackReason>,
563 },
564 CapabilityChanged {
565 turn: u32,
566 #[serde(default, skip_serializing_if = "Vec::is_empty")]
567 added: Vec<String>,
568 #[serde(default, skip_serializing_if = "Vec::is_empty")]
569 removed: Vec<String>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 change_kind: Option<String>,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
573 capability_id: Option<String>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 version: Option<String>,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
577 mounted_by: Option<String>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
579 mount_reason: Option<String>,
580 },
581 MilestoneAdvanced {
582 turn: u32,
583 phase_id: String,
584 capabilities_unlocked: Vec<String>,
585 },
586 MilestoneBlocked {
587 turn: u32,
588 phase_id: String,
589 reason: String,
590 },
591 MilestoneEvidence {
593 turn: u32,
594 phase_id: String,
595 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 evidence: Vec<String>,
597 },
598 CheckpointTaken {
600 turn: u32,
601 history_len: u32,
602 },
603 AgentProcessChanged {
605 turn: u32,
606 agent_id: String,
607 parent_session_id: String,
608 role: String,
609 isolation: String,
610 context_inheritance: String,
611 state: String,
612 #[serde(default, skip_serializing_if = "Vec::is_empty")]
613 permitted_capability_ids: Vec<String>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 result_termination: Option<String>,
616 },
617 WorkflowBatchSpawned {
620 turn: u32,
621 nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
626 budget: Option<crate::orchestration::workflow::WorkflowBudget>,
627 },
628 WorkflowCompleted {
630 turn: u32,
631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 completed: Vec<String>,
633 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 failed: Vec<String>,
635 },
636 AgentPreempted {
642 turn: u32,
643 #[serde(default, skip_serializing_if = "Vec::is_empty")]
644 agent_ids: Vec<String>,
645 reason: String,
646 },
647 ToolGated {
650 turn: u32,
651 call_id: String,
652 tool: String,
653 reason: String,
654 },
655 SignalDisposed {
657 turn: u32,
658 signal_id: String,
659 disposition: String,
660 queue_depth: u32,
661 },
662 BudgetExceeded { turn: u32, budget: String },
664 Suspended {
666 turn: u32,
667 reason: String,
668 #[serde(default, skip_serializing_if = "Vec::is_empty")]
669 pending_calls: Vec<String>,
670 },
671 Resumed {
673 turn: u32,
674 #[serde(default, skip_serializing_if = "Vec::is_empty")]
675 approved: Vec<String>,
676 #[serde(default, skip_serializing_if = "Vec::is_empty")]
677 denied: Vec<String>,
678 },
679 PageOut {
681 turn: u32,
682 action: KernelPressureAction,
683 rho_after: f64,
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 summary: Option<String>,
686 #[serde(default, skip_serializing_if = "Vec::is_empty")]
687 archived: Vec<Message>,
688 tier_hint: String,
689 },
690 PageInRequested {
692 turn: u32,
693 call_id: String,
694 tool: String,
695 query: String,
696 top_k: u32,
697 },
698 MemoryWritten {
700 turn: u32,
701 memory_id: String,
702 memory_kind: String,
703 size_bytes: u32,
704 },
705 MemoryValidationFailed {
707 turn: u32,
708 memory_id: String,
709 error: String,
710 },
711 MemoryQueried {
713 turn: u32,
714 query_context: String,
715 requested_k: usize,
716 requires_async_response: bool,
717 },
718 LargeResultSpooled {
720 turn: u32,
721 call_id: String,
722 tool: String,
723 original_size: u32,
724 preview_size: u32,
725 spool_ref: Option<String>,
726 },
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
735#[serde(tag = "kind", rename_all = "snake_case")]
736pub enum TransactionObservation {
737 CheckpointTaken { turn: u32, history_len: u32 },
738 Rollbacked {
739 turn: u32,
740 checkpoint_history_len: u32,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
742 reason: Option<crate::runtime::session::RollbackReason>,
743 },
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
747#[serde(rename_all = "snake_case")]
748pub enum KernelPressureAction {
749 None,
750 SnipCompact,
751 MicroCompact,
752 ContextCollapse,
753 AutoCompact,
754}
755
756impl From<PressureAction> for KernelPressureAction {
757 fn from(action: PressureAction) -> Self {
758 match action {
759 PressureAction::None => Self::None,
760 PressureAction::SnipCompact => Self::SnipCompact,
761 PressureAction::MicroCompact => Self::MicroCompact,
762 PressureAction::ContextCollapse => Self::ContextCollapse,
763 PressureAction::AutoCompact => Self::AutoCompact,
764 }
765 }
766}
767
768pub struct KernelRuntime {
771 sm: LoopStateMachine,
772}
773
774impl KernelRuntime {
775 pub fn new(policy: LoopPolicy) -> Self {
776 Self {
777 sm: LoopStateMachine::new(policy),
778 }
779 }
780
781 pub fn state_machine(&self) -> &LoopStateMachine {
782 &self.sm
783 }
784
785 pub fn state_machine_mut(&mut self) -> &mut LoopStateMachine {
786 &mut self.sm
787 }
788
789 pub fn is_terminal(&self) -> bool {
790 self.sm.is_terminal()
791 }
792
793 pub fn local_subagents_spawned(&self) -> u32 {
796 self.sm.local_subagents_spawned()
797 }
798
799 pub fn step(&mut self, input: KernelInput) -> KernelStep {
800 let action = match input.event {
801 KernelInputEvent::SetTools { tools } => {
802 self.sm.tools = tools;
803 return KernelStep::empty(self.sm.take_observations());
804 }
805 KernelInputEvent::SetAvailableSkills { skills } => {
806 self.sm.ctx.set_available_skills(skills);
807 return KernelStep::empty(self.sm.take_observations());
808 }
809 KernelInputEvent::SkillActivated { name } => {
810 self.sm.ctx.activate_skill(name);
813 return KernelStep::empty(self.sm.take_observations());
814 }
815 KernelInputEvent::SetStableCoreTools { tool_ids } => {
816 self.sm.ctx.set_stable_core_tools(tool_ids.into_iter().map(Into::into));
817 return KernelStep::empty(self.sm.take_observations());
818 }
819 KernelInputEvent::SetMemoryEnabled { enabled } => {
820 self.sm.ctx.set_memory_enabled(enabled);
821 return KernelStep::empty(self.sm.take_observations());
822 }
823 KernelInputEvent::SetKnowledgeEnabled { enabled } => {
824 self.sm.ctx.set_knowledge_enabled(enabled);
825 return KernelStep::empty(self.sm.take_observations());
826 }
827 KernelInputEvent::SetPlanToolEnabled { enabled } => {
828 self.sm.ctx.set_plan_tool_enabled(enabled);
829 return KernelStep::empty(self.sm.take_observations());
830 }
831 KernelInputEvent::SetTokenizer { .. } => {
832 self.sm.ctx.engine = ContextTokenEngine::char_approx();
836 return KernelStep::empty(self.sm.take_observations());
837 }
838 KernelInputEvent::AddSystemMessage { content, tokens } => {
839 self.sm
840 .ctx
841 .partitions
842 .system
843 .push(Message::system(content), tokens.max(1));
844 return KernelStep::empty(self.sm.take_observations());
845 }
846 KernelInputEvent::AddKnowledgeMessage { content, tokens } => {
847 self.sm.ctx.partitions.knowledge.push(Message::system(content), tokens.max(1));
856 return KernelStep::empty(self.sm.take_observations());
857 }
858 KernelInputEvent::AddHistoryMessage { message, tokens } => {
859 let tokens = tokens.unwrap_or_else(|| self.sm.ctx.engine.count_message(&message));
860 self.sm.ctx.push_history(message, tokens.max(1));
861 return KernelStep::empty(self.sm.take_observations());
862 }
863 KernelInputEvent::PreloadHistory { messages } => {
864 self.sm.preload_history(messages);
865 return KernelStep::empty(self.sm.take_observations());
866 }
867 KernelInputEvent::MountCapability { capability } => {
868 self.sm.mount_capability(capability, None, None);
869 return KernelStep::empty(self.sm.take_observations());
870 }
871 KernelInputEvent::UnmountCapability {
872 capability_kind,
873 id,
874 } => {
875 self.sm.unmount_capability(capability_kind, &id);
876 return KernelStep::empty(self.sm.take_observations());
877 }
878 KernelInputEvent::LoadMilestoneContract { contract } => {
879 self.sm.load_milestone_contract(contract);
880 return KernelStep::empty(self.sm.take_observations());
881 }
882 KernelInputEvent::LoadGovernancePolicy {
883 default_action,
884 rules,
885 vetoed_tools,
886 rate_limits,
887 constraints,
888 } => {
889 self.sm.set_governance(build_governance_pipeline(
890 default_action,
891 rules,
892 vetoed_tools,
893 rate_limits,
894 constraints,
895 ));
896 return KernelStep::empty(self.sm.take_observations());
897 }
898 KernelInputEvent::ConfigureRun { config } => {
899 let RunConfig {
906 tools,
907 available_skills,
908 stable_core_tools,
909 memory_enabled,
910 knowledge_enabled,
911 plan_tool_enabled,
912 tokenizer,
913 governance,
914 attention_max_queue_size,
915 scheduler_max_wall_ms,
916 resource_quota,
917 group_tokens_base,
918 group_spawns_base,
919 } = config;
920 if let Some(tools) = tools {
921 self.sm.tools = tools;
922 }
923 if let Some(skills) = available_skills {
924 self.sm.ctx.set_available_skills(skills);
925 }
926 if let Some(ids) = stable_core_tools {
927 self.sm.ctx.set_stable_core_tools(ids.into_iter().map(Into::into));
928 }
929 if let Some(enabled) = memory_enabled {
930 self.sm.ctx.set_memory_enabled(enabled);
931 }
932 if let Some(enabled) = knowledge_enabled {
933 self.sm.ctx.set_knowledge_enabled(enabled);
934 }
935 if let Some(enabled) = plan_tool_enabled {
936 self.sm.ctx.set_plan_tool_enabled(enabled);
937 }
938 if tokenizer.is_some() {
939 self.sm.ctx.engine = ContextTokenEngine::char_approx();
940 }
941 if let Some(g) = governance {
942 self.sm.set_governance(build_governance_pipeline(
943 g.default_action,
944 g.rules,
945 g.vetoed_tools,
946 g.rate_limits,
947 g.constraints,
948 ));
949 }
950 if let Some(max_queue) = attention_max_queue_size {
951 self.sm.set_attention(max_queue as usize);
952 }
953 if let Some(ms) = scheduler_max_wall_ms {
954 self.sm.set_wall_budget(Some(ms));
955 }
956 if let Some(quota) = resource_quota {
957 self.sm.set_resource_quota(quota);
958 }
959 if let Some(base) = group_tokens_base {
960 self.sm.seed_group_budget(base);
961 }
962 if let Some(base) = group_spawns_base {
963 self.sm.seed_group_spawns(base);
964 }
965 return KernelStep::empty(self.sm.take_observations());
966 }
967 KernelInputEvent::SetAttentionPolicy { max_queue_size } => {
968 self.sm.set_attention(max_queue_size as usize);
969 return KernelStep::empty(self.sm.take_observations());
970 }
971 KernelInputEvent::PageIn { entries } => {
972 self.sm.apply_page_in(&entries);
973 return KernelStep::empty(self.sm.take_observations());
974 }
975 KernelInputEvent::ForceCompact => {
976 self.sm.force_compact();
977 return KernelStep::empty(self.sm.take_observations());
978 }
979 KernelInputEvent::UpdateTask { update } => {
980 self.sm.ctx.update_task(update);
981 return KernelStep::empty(self.sm.take_observations());
982 }
983 KernelInputEvent::StartRun { task, run_spec } => {
984 self.sm.run_spec = run_spec;
985 self.sm.start(task)
986 }
987 KernelInputEvent::CapabilityCommand { command } => {
988 self.sm.execute_capability_command(command);
989 return KernelStep::empty(self.sm.take_observations());
990 }
991 KernelInputEvent::Resume { approved_calls, denied_calls } => {
992 let action = self.sm.resume_from_suspend(approved_calls, denied_calls);
993 if matches!(action, LoopAction::AwaitingResume) {
994 return KernelStep::empty(self.sm.take_observations());
995 }
996 return KernelStep::single(action, self.sm.take_observations());
997 }
998 KernelInputEvent::SetSchedulerBudget { max_wall_ms } => {
999 self.sm.set_wall_budget(max_wall_ms);
1000 return KernelStep::empty(self.sm.take_observations());
1001 }
1002 KernelInputEvent::SetResourceQuota { quota } => {
1003 self.sm.set_resource_quota(quota);
1004 return KernelStep::empty(self.sm.take_observations());
1005 }
1006 KernelInputEvent::ProviderResult {
1007 message,
1008 observed_input_tokens,
1009 observed_output_tokens: _,
1010 now_ms,
1011 stop_reason,
1012 } => {
1013 if let Some(tokens) = observed_input_tokens {
1014 self.sm.ctx.set_observed_prompt_tokens(tokens);
1015 }
1016 if let Some(ms) = now_ms {
1019 self.sm.set_observed_time(ms);
1020 }
1021 self.sm.set_pending_stop_reason(stop_reason);
1023 self.sm.feed(LoopEvent::LLMResponse { message })
1024 }
1025 KernelInputEvent::ToolResults { results } => {
1026 self.sm.feed(LoopEvent::ToolResults { results })
1027 }
1028 KernelInputEvent::ProviderError { message } => {
1029 self.sm.recover_from_provider_error(&message)
1032 }
1033 KernelInputEvent::Signal { signal } => match self.sm.signal_event(signal) {
1034 Some(action) => action,
1035 None => return KernelStep::empty(self.sm.take_observations()),
1038 },
1039 KernelInputEvent::MilestoneResult { result } => {
1040 self.sm.feed(LoopEvent::MilestoneResult { result })
1041 }
1042 KernelInputEvent::SpawnSubAgent {
1043 spec,
1044 parent_session_id,
1045 } => {
1046 let action = self.sm.spawn_sub_agent(spec, &parent_session_id);
1047 if matches!(action, LoopAction::AwaitingResume) {
1048 return KernelStep::empty(self.sm.take_observations());
1049 }
1050 return KernelStep::single(action, self.sm.take_observations());
1051 }
1052 KernelInputEvent::LoadWorkflow {
1053 spec,
1054 parent_session_id,
1055 resumed_completed,
1056 resumed_submissions,
1057 } => {
1058 self.sm.ensure_started_for_workflow(&spec);
1062 let action = if resumed_completed.is_empty() && resumed_submissions.is_empty() {
1063 self.sm.load_workflow(spec, &parent_session_id)
1064 } else {
1065 self.sm.load_workflow_resumed(
1066 spec,
1067 &parent_session_id,
1068 &resumed_submissions,
1069 &resumed_completed,
1070 )
1071 };
1072 if matches!(action, LoopAction::AwaitingResume) {
1073 return KernelStep::empty(self.sm.take_observations());
1074 }
1075 return KernelStep::single(action, self.sm.take_observations());
1076 }
1077 KernelInputEvent::SubAgentCompleted { result } => {
1078 self.sm.feed(LoopEvent::SubAgentCompleted { result })
1079 }
1080 KernelInputEvent::SubmitWorkflowNodes {
1081 nodes,
1082 submitter_agent_id,
1083 } => {
1084 let action = self
1085 .sm
1086 .submit_workflow_nodes(nodes, submitter_agent_id.as_deref());
1087 if matches!(action, LoopAction::AwaitingResume) {
1088 return KernelStep::empty(self.sm.take_observations());
1089 }
1090 return KernelStep::single(action, self.sm.take_observations());
1091 }
1092 KernelInputEvent::SubmitWorkflow {
1093 spec,
1094 parent_session_id,
1095 submitter_agent_id,
1096 } => {
1097 let action = self.sm.submit_workflow(
1098 spec,
1099 &parent_session_id,
1100 submitter_agent_id.as_deref(),
1101 );
1102 if matches!(action, LoopAction::AwaitingResume) {
1103 return KernelStep::empty(self.sm.take_observations());
1104 }
1105 return KernelStep::single(action, self.sm.take_observations());
1106 }
1107 KernelInputEvent::SetMemoryPolicy {
1108 memory_path,
1109 stale_warning_days,
1110 retrieval_top_k,
1111 validation_enabled,
1112 max_content_bytes,
1113 max_name_length,
1114 } => {
1115 self.sm.set_memory_policy(crate::mm::memory::MemoryPolicy {
1119 memory_path,
1120 stale_warning_days,
1121 retrieval_top_k,
1122 validation_enabled,
1123 max_content_bytes,
1124 max_name_length,
1125 });
1126 return KernelStep::empty(self.sm.take_observations());
1127 }
1128 KernelInputEvent::WriteMemory { memory } => {
1129 use crate::mm::memory::validate_memory_write;
1132 let turn = self.sm.turn;
1133 let disposition = self
1137 .sm
1138 .gate_syscall(&crate::syscall::Syscall::WriteMemory(memory.clone()));
1139 if !disposition.is_allowed() {
1140 let error = match disposition {
1141 crate::syscall::Disposition::RateLimited { retry_after_ms } => {
1142 format!("memory write rate limited; retry after {retry_after_ms}ms")
1143 }
1144 crate::syscall::Disposition::Deny { reason, .. } => {
1145 format!("memory write denied: {reason}")
1146 }
1147 _ => "memory write not permitted".to_string(),
1148 };
1149 self.sm.observations.push(
1150 KernelObservation::MemoryValidationFailed {
1151 turn,
1152 memory_id: memory.metadata.name.clone(),
1153 error,
1154 },
1155 );
1156 return KernelStep::empty(self.sm.take_observations());
1157 }
1158 let validation_result = match self.sm.memory_policy() {
1162 Some(p) if !p.validation_enabled => Ok(()),
1163 Some(p) => p.validation().validate(&memory),
1164 None => validate_memory_write(&memory),
1165 };
1166 match validation_result {
1167 Ok(()) => {
1168 self.sm.observations.push(KernelObservation::MemoryWritten {
1170 turn,
1171 memory_id: memory.metadata.name.clone(),
1172 memory_kind: memory.metadata.kind.map(|k| k.label()).unwrap_or_else(|| {
1173 crate::mm::memory::MemoryKind::infer_from_metadata(&memory.metadata).label()
1174 }).to_string(),
1175 size_bytes: memory.content.len() as u32,
1176 });
1177 }
1178 Err(err) => {
1179 use crate::mm::memory::MemoryValidationError;
1181 let error_msg = match err {
1182 MemoryValidationError::MissingRequiredField { field } => format!("Missing required field: {}", field),
1183 MemoryValidationError::ContentTooLarge { size, limit } => format!("Content too large: {} bytes (limit: {})", size, limit),
1184 MemoryValidationError::ForbiddenPattern { pattern, reason } => format!("Forbidden pattern '{}': {}", pattern, reason),
1185 MemoryValidationError::InvalidKind { kind } => format!("Invalid kind: {}", kind),
1186 MemoryValidationError::NameTooLong { length, limit } => format!("Name too long: {} chars (limit: {})", length, limit),
1187 };
1188 self.sm.observations.push(KernelObservation::MemoryValidationFailed {
1189 turn,
1190 memory_id: memory.metadata.name.clone(),
1191 error: error_msg,
1192 });
1193 }
1194 }
1195 return KernelStep::empty(self.sm.take_observations());
1196 }
1197 KernelInputEvent::QueryMemory { query } => {
1198 let turn = self.sm.turn;
1201 let requested_k = match self.sm.memory_policy() {
1203 Some(p) => p.clamp_top_k(query.top_k),
1204 None => query.top_k,
1205 };
1206 self.sm.observations.push(KernelObservation::MemoryQueried {
1207 turn,
1208 query_context: query.current_context.clone(),
1209 requested_k,
1210 requires_async_response: true,
1211 });
1212 return KernelStep::empty(self.sm.take_observations());
1213 }
1214 KernelInputEvent::MemoryRetrievalResult { .. } => {
1215 return KernelStep::empty(self.sm.take_observations());
1217 }
1218 KernelInputEvent::Timeout => self.sm.feed(LoopEvent::Timeout),
1219 };
1220 if matches!(action, LoopAction::AwaitingResume) {
1221 return KernelStep::empty(self.sm.take_observations());
1222 }
1223 KernelStep::single(action, self.sm.take_observations())
1224 }
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229 use super::*;
1230
1231 #[test]
1232 fn start_run_returns_versioned_provider_action() {
1233 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1234 let step = runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1235 task: RuntimeTask::new("ship it"),
1236 run_spec: None,
1237 }));
1238
1239 assert_eq!(step.version, KERNEL_ABI_VERSION);
1240 assert!(matches!(
1241 step.actions.as_slice(),
1242 [KernelAction::CallProvider { .. }]
1243 ));
1244 }
1245
1246 #[test]
1247 fn provider_text_response_returns_done() {
1248 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1249 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1250 task: RuntimeTask::new("ship it"),
1251 run_spec: None,
1252 }));
1253 let step = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1254 message: Message::assistant("done"),
1255 observed_input_tokens: None,
1256 observed_output_tokens: None,
1257 stop_reason: None,
1258 now_ms: None,
1259 }));
1260
1261 assert!(matches!(
1262 step.actions.as_slice(),
1263 [KernelAction::Done { .. }]
1264 ));
1265 }
1266
1267 #[test]
1268 fn config_inputs_mutate_runtime_without_actions() {
1269 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1270 let step = runtime.step(KernelInput::new(KernelInputEvent::SetTools {
1271 tools: vec![ToolSchema {
1272 name: "echo".into(),
1273 description: "Echo input".to_string(),
1274 parameters: serde_json::json!({"type": "object"}),
1275 }],
1276 }));
1277
1278 assert!(step.actions.is_empty());
1279 assert_eq!(runtime.state_machine().tools.len(), 1);
1280 }
1281
1282 #[test]
1283 fn skill_activated_input_records_active_skill() {
1284 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1287 let mut debug = SkillMetadata::new("debug", "Debug helper");
1288 debug.allowed_tools = vec!["read".into(), "grep".into()];
1289 runtime.step(KernelInput::new(KernelInputEvent::SetAvailableSkills {
1290 skills: vec![debug],
1291 }));
1292
1293 let step = runtime.step(KernelInput::new(KernelInputEvent::SkillActivated {
1294 name: "debug".to_string(),
1295 }));
1296
1297 assert!(step.actions.is_empty(), "activation is config, not an action");
1298 assert!(runtime.state_machine().ctx.active_skills.contains("debug"));
1299 let filter = runtime.state_machine().ctx.active_skill_tool_filter().unwrap();
1300 assert_eq!(filter.len(), 2);
1301 }
1302
1303 #[test]
1304 fn update_task_input_mutates_task_state() {
1305 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1306 let step = runtime.step(KernelInput::new(KernelInputEvent::UpdateTask {
1307 update: TaskUpdate {
1308 progress: Some("tools executed".to_string()),
1309 ..Default::default()
1310 },
1311 }));
1312
1313 assert!(step.actions.is_empty());
1314 assert_eq!(
1315 runtime.state_machine().ctx.partitions.task_state.progress,
1316 "tools executed"
1317 );
1318 }
1319
1320 #[test]
1321 fn add_knowledge_message_enters_knowledge_partition() {
1322 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1323 let step = runtime.step(KernelInput::new(KernelInputEvent::AddKnowledgeMessage {
1324 content: "skill: debug".to_string(),
1325 tokens: 10,
1326 }));
1327
1328 assert!(step.actions.is_empty());
1329 assert_eq!(
1330 runtime.state_machine().ctx.partitions.knowledge.messages.len(),
1331 1
1332 );
1333 }
1334
1335 #[test]
1336 fn capability_mount_emits_observation() {
1337 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1338 let step = runtime.step(KernelInput::new(KernelInputEvent::MountCapability {
1339 capability: CapabilityDescriptor::marker(
1340 CapabilityKind::McpServer,
1341 "docs",
1342 "Documentation server",
1343 ),
1344 }));
1345
1346 assert!(step.actions.is_empty());
1347 assert!(matches!(
1348 step.observations.as_slice(),
1349 [KernelObservation::CapabilityChanged { .. }]
1350 ));
1351 }
1352
1353 #[test]
1354 fn spawn_sub_agent_input_registers_process() {
1355 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1356
1357 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1358 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1359 task: RuntimeTask::new("parent task"),
1360 run_spec: None,
1361 }));
1362 runtime.state_machine_mut().take_observations();
1363
1364 let spec = AgentRunSpec::new(
1365 AgentIdentity::sub_agent("worker", "worker-session"),
1366 AgentRole::Implement,
1367 "do work",
1368 );
1369 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1370 spec,
1371 parent_session_id: "parent-session".to_string(),
1372 }));
1373
1374 assert!(step.actions.is_empty());
1375 assert!(step.observations.iter().any(|o| matches!(
1376 o,
1377 KernelObservation::AgentProcessChanged {
1378 agent_id,
1379 parent_session_id,
1380 state,
1381 ..
1382 } if agent_id == "worker" && parent_session_id == "parent-session" && state == "running"
1383 )));
1384 assert_eq!(
1385 runtime
1386 .state_machine()
1387 .agent_process("worker")
1388 .expect("process")
1389 .parent_session_id
1390 .as_str(),
1391 "parent-session"
1392 );
1393 assert!(step.observations.iter().any(|o| matches!(
1394 o,
1395 KernelObservation::Suspended { reason, .. } if reason == "sub_agent_await"
1396 )));
1397 assert!(runtime.state_machine().is_suspended());
1398 assert!(matches!(
1399 runtime.state_machine().wait_reason(),
1400 Some(crate::scheduler::tcb::WaitReason::SubAgentJoin(_))
1401 ));
1402 }
1403
1404 #[test]
1405 fn set_resource_quota_input_denies_spawn_over_quota() {
1406 use crate::governance::quota::ResourceQuota;
1407 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1408
1409 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1410 let step = runtime.step(KernelInput::new(KernelInputEvent::SetResourceQuota {
1412 quota: ResourceQuota { max_spawn_depth: Some(0), ..ResourceQuota::default() },
1413 }));
1414 assert!(step.actions.is_empty(), "config input yields no actions");
1415
1416 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1417 task: RuntimeTask::new("parent task"),
1418 run_spec: None,
1419 }));
1420 runtime.state_machine_mut().take_observations();
1421
1422 let spec = AgentRunSpec::new(
1423 AgentIdentity::sub_agent("worker", "worker-session"),
1424 AgentRole::Implement,
1425 "do work",
1426 );
1427 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1428 spec,
1429 parent_session_id: "parent-session".to_string(),
1430 }));
1431
1432 assert!(matches!(
1435 step.actions.as_slice(),
1436 [KernelAction::CallProvider { .. }]
1437 ));
1438 assert!(!step.observations.iter().any(|o| matches!(
1439 o,
1440 KernelObservation::AgentProcessChanged { agent_id, .. } if agent_id == "worker"
1441 )));
1442 assert!(runtime.state_machine().agent_process("worker").is_none());
1443 assert!(!runtime.state_machine().is_suspended());
1444 }
1445
1446 #[test]
1447 fn group_budget_base_enforces_shared_token_cap() {
1448 use crate::types::message::{Content, Message, ToolCall, ToolResult};
1449
1450 fn run_one_turn(group_base: Option<u64>) -> KernelStep {
1454 let mut runtime = KernelRuntime::new(LoopPolicy {
1455 max_total_tokens: 100,
1456 ..LoopPolicy::default()
1457 });
1458 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
1459 config: RunConfig { group_tokens_base: group_base, ..RunConfig::default() },
1460 }));
1461 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1462 task: RuntimeTask::new("task"),
1463 run_spec: None,
1464 }));
1465 let mut msg = Message::assistant("");
1466 msg.token_count = Some(10); msg.tool_calls.push(ToolCall {
1468 id: "c1".into(),
1469 name: "echo".into(),
1470 arguments: serde_json::json!({}),
1471 });
1472 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1473 message: msg,
1474 observed_input_tokens: None,
1475 observed_output_tokens: None,
1476 stop_reason: None,
1477 now_ms: None,
1478 }));
1479 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1480 results: vec![ToolResult {
1481 call_id: "c1".into(),
1482 output: Content::Text("ok".into()),
1483 is_error: false,
1484 is_fatal: false,
1485 error_kind: None,
1486 token_count: None,
1487 }],
1488 }))
1489 }
1490
1491 let exceeded = |step: &KernelStep| {
1492 step.observations.iter().any(|o| {
1493 matches!(o, KernelObservation::BudgetExceeded { budget, .. } if budget == "token_budget")
1494 })
1495 };
1496
1497 assert!(
1499 exceeded(&run_one_turn(Some(95))),
1500 "group token budget must span the whole domain"
1501 );
1502 assert!(
1504 !exceeded(&run_one_turn(None)),
1505 "no group seed ⇒ per-vehicle budget, well under cap"
1506 );
1507 }
1508
1509 #[test]
1510 fn group_spawns_base_enforces_cumulative_spawn_cap() {
1511 use crate::governance::quota::ResourceQuota;
1512 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1513
1514 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1517 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
1518 config: RunConfig {
1519 resource_quota: Some(ResourceQuota {
1520 max_total_subagents: Some(2),
1521 ..ResourceQuota::default()
1522 }),
1523 group_spawns_base: Some(2),
1524 ..RunConfig::default()
1525 },
1526 }));
1527 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1528 task: RuntimeTask::new("task"),
1529 run_spec: None,
1530 }));
1531 runtime.state_machine_mut().take_observations();
1532
1533 let spec = AgentRunSpec::new(
1534 AgentIdentity::sub_agent("worker", "worker-session"),
1535 AgentRole::Implement,
1536 "do work",
1537 );
1538 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1539 spec,
1540 parent_session_id: "parent-session".to_string(),
1541 }));
1542
1543 assert!(matches!(
1545 step.actions.as_slice(),
1546 [KernelAction::CallProvider { .. }]
1547 ));
1548 assert!(runtime.state_machine().agent_process("worker").is_none());
1549 assert_eq!(runtime.local_subagents_spawned(), 0);
1550 }
1551
1552 #[test]
1553 fn default_runtime_leaves_spawn_unquota_ed() {
1554 use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
1555
1556 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1558 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1559 task: RuntimeTask::new("parent task"),
1560 run_spec: None,
1561 }));
1562 runtime.state_machine_mut().take_observations();
1563
1564 let spec = AgentRunSpec::new(
1565 AgentIdentity::sub_agent("worker", "worker-session"),
1566 AgentRole::Implement,
1567 "do work",
1568 );
1569 runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1570 spec,
1571 parent_session_id: "parent-session".to_string(),
1572 }));
1573 assert!(runtime.state_machine().agent_process("worker").is_some());
1574 assert!(runtime.state_machine().is_suspended());
1575 }
1576
1577 #[test]
1582 fn agent_process_changed_locks_multiword_wire_form() {
1583 use crate::types::agent::{AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec};
1584
1585 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1586 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1587 task: RuntimeTask::new("parent task"),
1588 run_spec: None,
1589 }));
1590 runtime.state_machine_mut().take_observations();
1591
1592 let spec = AgentRunSpec::new(
1594 AgentIdentity::sub_agent("worker", "worker-session"),
1595 AgentRole::Verify,
1596 "do work",
1597 )
1598 .with_isolation(AgentIsolation::ReadOnly);
1599 let step = runtime.step(KernelInput::new(KernelInputEvent::SpawnSubAgent {
1600 spec,
1601 parent_session_id: "parent-session".to_string(),
1602 }));
1603
1604 let obs = step
1605 .observations
1606 .iter()
1607 .find(|o| matches!(o, KernelObservation::AgentProcessChanged { .. }))
1608 .expect("agent_process_changed observation");
1609 let json = serde_json::to_value(obs).unwrap();
1610 assert_eq!(json["isolation"], "readonly", "isolation must stay debug-lowercase");
1611 assert_eq!(
1612 json["context_inheritance"], "systemonly",
1613 "context_inheritance must stay debug-lowercase"
1614 );
1615 assert_eq!(json["role"], "verify");
1616 assert_eq!(json["state"], "running");
1617 }
1618
1619 fn write_memory(runtime: &mut KernelRuntime, name: &str, content: &str) -> KernelStep {
1622 use crate::mm::memory::{MemoryMetadata, MemoryWriteRequest};
1623 runtime.step(KernelInput::new(KernelInputEvent::WriteMemory {
1624 memory: MemoryWriteRequest {
1625 metadata: MemoryMetadata {
1626 name: name.to_string(),
1627 description: "desc".to_string(),
1628 ..Default::default()
1629 },
1630 content: content.to_string(),
1631 },
1632 }))
1633 }
1634
1635 #[test]
1636 fn memory_policy_validation_disabled_admits_forbidden_write() {
1637 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1639 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1640 memory_path: String::new(),
1641 stale_warning_days: 2,
1642 retrieval_top_k: 5,
1643 validation_enabled: false,
1644 max_content_bytes: None,
1645 max_name_length: None,
1646 }));
1647 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1648 assert!(step
1649 .observations
1650 .iter()
1651 .any(|o| matches!(o, KernelObservation::MemoryWritten { .. })));
1652 assert!(!step
1653 .observations
1654 .iter()
1655 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1656 }
1657
1658 #[test]
1659 fn default_runtime_validates_forbidden_write() {
1660 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1662 let step = write_memory(&mut runtime, "note", "代码模式: foo");
1663 assert!(step
1664 .observations
1665 .iter()
1666 .any(|o| matches!(o, KernelObservation::MemoryValidationFailed { .. })));
1667 }
1668
1669 #[test]
1670 fn memory_policy_size_override_rejects_oversized_write() {
1671 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1672 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1673 memory_path: String::new(),
1674 stale_warning_days: 2,
1675 retrieval_top_k: 5,
1676 validation_enabled: true,
1677 max_content_bytes: Some(8),
1678 max_name_length: None,
1679 }));
1680 let step = write_memory(&mut runtime, "note", "this content is well over eight bytes");
1681 let failed = step.observations.iter().find_map(|o| match o {
1682 KernelObservation::MemoryValidationFailed { error, .. } => Some(error.clone()),
1683 _ => None,
1684 });
1685 assert!(failed.is_some_and(|e| e.contains("too large")));
1686 }
1687
1688 #[test]
1689 fn memory_policy_clamps_retrieval_top_k() {
1690 use crate::mm::memory::MemoryQuery;
1691 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1692 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryPolicy {
1693 memory_path: String::new(),
1694 stale_warning_days: 2,
1695 retrieval_top_k: 3,
1696 validation_enabled: true,
1697 max_content_bytes: None,
1698 max_name_length: None,
1699 }));
1700 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
1701 query: MemoryQuery { top_k: 50, ..Default::default() },
1702 }));
1703 let requested = step.observations.iter().find_map(|o| match o {
1704 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
1705 _ => None,
1706 });
1707 assert_eq!(requested, Some(3));
1708 }
1709
1710 #[test]
1711 fn default_runtime_uses_requested_top_k_verbatim() {
1712 use crate::mm::memory::MemoryQuery;
1713 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1714 let step = runtime.step(KernelInput::new(KernelInputEvent::QueryMemory {
1715 query: MemoryQuery { top_k: 50, ..Default::default() },
1716 }));
1717 let requested = step.observations.iter().find_map(|o| match o {
1718 KernelObservation::MemoryQueried { requested_k, .. } => Some(*requested_k),
1719 _ => None,
1720 });
1721 assert_eq!(requested, Some(50));
1722 }
1723
1724 #[test]
1725 fn provider_result_now_ms_drives_wall_time_budget() {
1726 let mut runtime = KernelRuntime::new(LoopPolicy {
1727 max_wall_ms: Some(10),
1728 ..LoopPolicy::default()
1729 });
1730 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1731 task: RuntimeTask::new("ship it"),
1732 run_spec: None,
1733 }));
1734 let mut msg = Message::assistant("");
1735 msg.tool_calls.push(ToolCall {
1736 id: "call-1".into(),
1737 name: "echo".into(),
1738 arguments: serde_json::json!({}),
1739 });
1740 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1741 message: msg,
1742 observed_input_tokens: None,
1743 observed_output_tokens: None,
1744 stop_reason: None,
1745 now_ms: Some(100),
1746 }));
1747 let step = runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
1748 results: vec![ToolResult {
1749 call_id: "call-1".into(),
1750 output: crate::types::message::Content::Text("ok".into()),
1751 is_error: false,
1752 is_fatal: false,
1753 error_kind: None,
1754 token_count: None,
1755 }],
1756 }));
1757
1758 assert!(matches!(
1759 step.actions.as_slice(),
1760 [KernelAction::CallProvider { tools, .. }] if tools.is_empty()
1761 ));
1762 }
1763
1764 fn assistant_calling(tool: &str) -> Message {
1767 let mut msg = Message::assistant("");
1768 msg.tool_calls.push(ToolCall {
1769 id: "call-1".into(),
1770 name: tool.into(),
1771 arguments: serde_json::json!({}),
1772 });
1773 msg
1774 }
1775
1776 fn run_with_tool_call(runtime: &mut KernelRuntime, tool: &str) -> KernelStep {
1778 run_with_tool_call_named(runtime, tool, "call-1")
1779 }
1780
1781 fn run_with_tool_call_named(
1782 runtime: &mut KernelRuntime,
1783 tool: &str,
1784 call_id: &str,
1785 ) -> KernelStep {
1786 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
1787 task: RuntimeTask::new("do the thing"),
1788 run_spec: None,
1789 }));
1790 runtime.state_machine_mut().take_observations();
1791 runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
1792 message: assistant_calling(tool),
1793 observed_input_tokens: None,
1794 observed_output_tokens: None,
1795 stop_reason: None,
1796 now_ms: None,
1797 }))
1798 }
1799
1800 #[test]
1801 fn governance_deny_blocks_tool_and_reprompts() {
1802 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1803 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1804 default_action: Some(PolicyAction::Allow),
1805 rules: vec![PolicyRule {
1806 tool_pattern: "danger.*".to_string(),
1807 action: PolicyAction::Deny,
1808 }],
1809 vetoed_tools: vec![],
1810 rate_limits: vec![],
1811 constraints: vec![],
1812 }));
1813
1814 let step = run_with_tool_call(&mut runtime, "danger.delete");
1815
1816 assert!(
1818 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1819 "denied tool should roll back and re-call provider, got {:?}",
1820 step.actions
1821 );
1822 assert!(
1823 step.observations
1824 .iter()
1825 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
1826 "expected a Rollbacked observation for the denied turn",
1827 );
1828 }
1829
1830 #[test]
1831 fn configure_run_bundle_applies_governance_equivalently_to_load_governance_policy() {
1832 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1835 runtime.step(KernelInput::new(KernelInputEvent::ConfigureRun {
1836 config: RunConfig {
1837 tools: Some(vec![]),
1838 governance: Some(GovernanceConfig {
1839 default_action: Some(PolicyAction::Allow),
1840 rules: vec![PolicyRule {
1841 tool_pattern: "danger.*".to_string(),
1842 action: PolicyAction::Deny,
1843 }],
1844 ..GovernanceConfig::default()
1845 }),
1846 attention_max_queue_size: Some(32),
1847 ..RunConfig::default()
1848 },
1849 }));
1850
1851 let step = run_with_tool_call(&mut runtime, "danger.delete");
1852
1853 assert!(
1854 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1855 "bundle-configured deny should roll back and re-call provider, got {:?}",
1856 step.actions
1857 );
1858 assert!(
1859 step.observations
1860 .iter()
1861 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
1862 "expected a Rollbacked observation for the bundle-denied turn",
1863 );
1864 }
1865
1866 #[test]
1867 fn configure_run_round_trips_over_the_abi() {
1868 let event = KernelInputEvent::ConfigureRun {
1871 config: RunConfig {
1872 resource_quota: Some(crate::governance::quota::ResourceQuota {
1873 max_concurrent_subagents: Some(2),
1874 ..Default::default()
1875 }),
1876 scheduler_max_wall_ms: Some(60_000),
1877 plan_tool_enabled: Some(true),
1878 ..RunConfig::default()
1879 },
1880 };
1881 let json = serde_json::to_string(&event).expect("serialize");
1882 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
1883 assert!(matches!(parsed, KernelInputEvent::ConfigureRun { .. }));
1884 }
1885
1886 #[test]
1887 fn governance_ask_user_suspends_until_resume() {
1888 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1889 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1890 default_action: Some(PolicyAction::Allow),
1891 rules: vec![PolicyRule {
1892 tool_pattern: "sensitive.*".to_string(),
1893 action: PolicyAction::AskUser,
1894 }],
1895 vetoed_tools: vec![],
1896 rate_limits: vec![],
1897 constraints: vec![],
1898 }));
1899
1900 let step = run_with_tool_call(&mut runtime, "sensitive.read");
1901
1902 assert!(
1903 step.actions.is_empty(),
1904 "AskUser should suspend without ExecuteTool, got {:?}",
1905 step.actions
1906 );
1907 assert!(
1908 step.observations.iter().any(|o| matches!(
1909 o,
1910 KernelObservation::ToolGated { tool, .. } if tool == "sensitive.read"
1911 )),
1912 "expected a ToolGated observation for the AskUser call",
1913 );
1914 assert!(
1915 step.observations.iter().any(|o| matches!(
1916 o,
1917 KernelObservation::Suspended { reason, .. } if reason == "ask_user"
1918 )),
1919 "expected a Suspended observation",
1920 );
1921
1922 let resumed = runtime.step(KernelInput::new(KernelInputEvent::Resume {
1923 approved_calls: vec!["call-1".to_string()],
1924 denied_calls: vec![],
1925 }));
1926 assert!(
1927 matches!(resumed.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
1928 "resume with approval should emit ExecuteTool, got {:?}",
1929 resumed.actions
1930 );
1931 assert!(
1932 resumed.observations.iter().any(|o| matches!(
1933 o,
1934 KernelObservation::Resumed { approved, denied, .. }
1935 if approved == &["call-1"] && denied.is_empty()
1936 )),
1937 );
1938 }
1939
1940 #[test]
1941 fn governance_ask_user_resume_all_denied_feeds_tool_results() {
1942 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1943 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
1944 default_action: Some(PolicyAction::Allow),
1945 rules: vec![PolicyRule {
1946 tool_pattern: "sensitive.*".to_string(),
1947 action: PolicyAction::AskUser,
1948 }],
1949 vetoed_tools: vec![],
1950 rate_limits: vec![],
1951 constraints: vec![],
1952 }));
1953 run_with_tool_call(&mut runtime, "sensitive.read");
1954 runtime.state_machine_mut().take_observations();
1955
1956 let step = runtime.step(KernelInput::new(KernelInputEvent::Resume {
1957 approved_calls: vec![],
1958 denied_calls: vec!["call-1".to_string()],
1959 }));
1960 assert!(
1961 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
1962 "all denied should re-prompt provider, got {:?}",
1963 step.actions
1964 );
1965 }
1966
1967 #[test]
1968 fn no_governance_policy_executes_all_tools() {
1969 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1970 let step = run_with_tool_call(&mut runtime, "danger.delete");
1971
1972 assert!(matches!(
1974 step.actions.as_slice(),
1975 [KernelAction::ExecuteTool { .. }]
1976 ));
1977 assert!(
1978 !step
1979 .observations
1980 .iter()
1981 .any(|o| matches!(o, KernelObservation::ToolGated { .. })),
1982 );
1983 }
1984
1985 fn tool_ok(call_id: &str) -> ToolResult {
1986 ToolResult {
1987 call_id: call_id.into(),
1988 output: crate::types::message::Content::Text("ok".to_string()),
1989 is_error: false,
1990 is_fatal: false,
1991 error_kind: None,
1992 token_count: None,
1993 }
1994 }
1995
1996 #[test]
1997 fn governance_rate_limit_blocks_second_call() {
1998 let mut runtime = KernelRuntime::new(LoopPolicy::default());
1999 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2000 default_action: Some(PolicyAction::Allow),
2001 rules: vec![],
2002 vetoed_tools: vec![],
2003 rate_limits: vec![RateLimitSpec {
2004 tool: "fetch".to_string(),
2005 max_calls: 1,
2006 window_ms: 60_000,
2007 }],
2008 constraints: vec![],
2009 }));
2010 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2011 task: RuntimeTask::new("fetch twice"),
2012 run_spec: None,
2013 }));
2014 runtime.state_machine_mut().take_observations();
2015
2016 let s1 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2018 message: assistant_calling("fetch"),
2019 observed_input_tokens: None,
2020 observed_output_tokens: None,
2021 stop_reason: None,
2022 now_ms: Some(1_000),
2023 }));
2024 assert!(
2025 matches!(s1.actions.as_slice(), [KernelAction::ExecuteTool { .. }]),
2026 "first call should execute, got {:?}",
2027 s1.actions
2028 );
2029
2030 runtime.step(KernelInput::new(KernelInputEvent::ToolResults {
2032 results: vec![tool_ok("call-1")],
2033 }));
2034 runtime.state_machine_mut().take_observations();
2035
2036 let s2 = runtime.step(KernelInput::new(KernelInputEvent::ProviderResult {
2038 message: assistant_calling("fetch"),
2039 observed_input_tokens: None,
2040 observed_output_tokens: None,
2041 stop_reason: None,
2042 now_ms: Some(1_001),
2043 }));
2044 assert!(
2045 matches!(s2.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2046 "rate-limited call should roll back and re-call provider, got {:?}",
2047 s2.actions
2048 );
2049 assert!(
2050 s2.observations
2051 .iter()
2052 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2053 "expected a Rollbacked observation for the rate-limited turn",
2054 );
2055 }
2056
2057 #[test]
2058 fn governance_constraint_required_param_denies() {
2059 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2060 runtime.step(KernelInput::new(KernelInputEvent::LoadGovernancePolicy {
2061 default_action: Some(PolicyAction::Allow),
2062 rules: vec![],
2063 vetoed_tools: vec![],
2064 rate_limits: vec![],
2065 constraints: vec![ConstraintSpec::Required {
2066 tool: "write".to_string(),
2067 path: "path".to_string(),
2068 }],
2069 }));
2070
2071 let step = run_with_tool_call(&mut runtime, "write");
2073 assert!(
2074 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2075 "missing required param should roll back, got {:?}",
2076 step.actions
2077 );
2078 assert!(
2079 step.observations
2080 .iter()
2081 .any(|o| matches!(o, KernelObservation::Rollbacked { .. })),
2082 "expected a Rollbacked observation for the constraint violation",
2083 );
2084 }
2085
2086 fn signal(urgency: crate::types::signal::Urgency, summary: &str) -> crate::types::signal::RuntimeSignal {
2089 use crate::types::signal::{RuntimeSignal, SignalSource, SignalType};
2090 RuntimeSignal::new(SignalSource::Gateway, SignalType::Alert, urgency, summary)
2091 }
2092
2093 fn started_runtime_with_attention(max_queue: u32) -> KernelRuntime {
2094 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2095 runtime.step(KernelInput::new(KernelInputEvent::SetAttentionPolicy {
2096 max_queue_size: max_queue,
2097 }));
2098 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2099 task: RuntimeTask::new("watch for signals"),
2100 run_spec: None,
2101 }));
2102 runtime.state_machine_mut().take_observations();
2103 runtime
2104 }
2105
2106 #[test]
2107 fn attention_policy_critical_signal_interrupts() {
2108 use crate::types::signal::Urgency;
2109 let mut runtime = started_runtime_with_attention(8);
2110 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2111 signal: signal(Urgency::Critical, "fire"),
2112 }));
2113 assert!(
2114 matches!(step.actions.as_slice(), [KernelAction::CallProvider { .. }]),
2115 "critical signal should drive a provider call, got {:?}",
2116 step.actions
2117 );
2118 assert!(step.observations.iter().any(|o| matches!(
2119 o,
2120 KernelObservation::SignalDisposed { disposition, .. } if disposition == "interrupt_now"
2121 )));
2122 }
2123
2124 #[test]
2125 fn attention_policy_normal_signal_queues_without_action() {
2126 use crate::types::signal::Urgency;
2127 let mut runtime = started_runtime_with_attention(8);
2128 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2129 signal: signal(Urgency::Normal, "job"),
2130 }));
2131 assert!(
2132 step.actions.is_empty(),
2133 "normal signal should queue without a provider call, got {:?}",
2134 step.actions
2135 );
2136 assert!(step.observations.iter().any(|o| matches!(
2137 o,
2138 KernelObservation::SignalDisposed { disposition, queue_depth, .. }
2139 if disposition == "queue" && *queue_depth == 1
2140 )));
2141 }
2142
2143 #[test]
2144 fn attention_policy_full_queue_drops() {
2145 use crate::types::signal::Urgency;
2146 let mut runtime = started_runtime_with_attention(1);
2147 runtime.step(KernelInput::new(KernelInputEvent::Signal {
2148 signal: signal(Urgency::Normal, "first"),
2149 }));
2150 let step = runtime.step(KernelInput::new(KernelInputEvent::Signal {
2151 signal: signal(Urgency::Normal, "second"),
2152 }));
2153 assert!(step.observations.iter().any(|o| matches!(
2154 o,
2155 KernelObservation::SignalDisposed { disposition, .. } if disposition == "dropped"
2156 )));
2157 }
2158
2159 #[test]
2160 #[test]
2161 fn page_in_populates_knowledge_partition() {
2162 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2163 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
2164 enabled: true,
2165 }));
2166 let before = runtime
2167 .state_machine()
2168 .ctx
2169 .partitions
2170 .knowledge
2171 .messages
2172 .len();
2173 runtime.step(KernelInput::new(KernelInputEvent::PageIn {
2174 entries: vec![crate::mm::PageInEntry {
2175 content: "[memory] prior fix".to_string(),
2176 tokens: Some(10),
2177 source: Some("memory".to_string()),
2178 }],
2179 }));
2180 let after = runtime
2181 .state_machine()
2182 .ctx
2183 .partitions
2184 .knowledge
2185 .messages
2186 .len();
2187 assert!(after > before, "page-in should add knowledge messages");
2188 }
2189
2190 #[test]
2191 fn memory_tool_emits_page_in_requested() {
2192 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2193 runtime.step(KernelInput::new(KernelInputEvent::SetMemoryEnabled {
2194 enabled: true,
2195 }));
2196 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2197 task: RuntimeTask::new("test"),
2198 run_spec: None,
2199 }));
2200 runtime.state_machine_mut().take_observations();
2201
2202 let step = run_with_tool_call(&mut runtime, "memory");
2203 assert!(step.observations.iter().any(|o| matches!(
2204 o,
2205 KernelObservation::PageInRequested { tool, .. } if tool == "memory"
2206 )));
2207 }
2208
2209 #[test]
2210 fn load_workflow_input_drives_dag_to_completion() {
2211 use crate::orchestration::workflow::fanout_synthesize;
2212 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2213
2214 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2215 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2216 task: RuntimeTask::new("parent task"),
2217 run_spec: None,
2218 }));
2219 runtime.state_machine_mut().take_observations();
2220
2221 let spec =
2223 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2224 let event = KernelInputEvent::LoadWorkflow {
2225 spec,
2226 parent_session_id: "sess".to_string(),
2227 resumed_completed: Vec::new(),
2228 resumed_submissions: Vec::new(),
2229 };
2230 let json = serde_json::to_string(&event).expect("serialize");
2231 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2232
2233 let step = runtime.step(KernelInput::new(parsed));
2234 let batch = step
2236 .observations
2237 .iter()
2238 .find_map(|o| match o {
2239 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2240 _ => None,
2241 })
2242 .expect("workflow_batch_spawned");
2243 assert_eq!(batch.len(), 2);
2244 let goals: Vec<&str> = batch.iter().map(|n| n.goal.as_str()).collect();
2245 assert!(goals.contains(&"w0") && goals.contains(&"w1"));
2246 assert_eq!(batch[0].agent_id, "wf-node0");
2247 assert_eq!(batch[0].isolation, "read_only"); let complete = |runtime: &mut KernelRuntime, id: &str| {
2250 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2251 result: SubAgentResult {
2252 agent_id: compact_str::CompactString::new(id),
2253 result: LoopResult {
2254 termination: TerminationReason::Completed,
2255 final_message: None,
2256 turns_used: 1,
2257 total_tokens_used: 1,
2258 loop_continue: None,
2259 classify_branch: None,
2260 tournament_winner: None,
2261 },
2262 },
2263 }))
2264 };
2265
2266 complete(&mut runtime, "wf-node0");
2267 let step = complete(&mut runtime, "wf-node1");
2269 assert!(step.observations.iter().any(|o| matches!(
2270 o,
2271 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2272 if nodes.len() == 1 && nodes[0].agent_id == "wf-node2"
2273 )));
2274
2275 let step = complete(&mut runtime, "wf-node2");
2277 assert!(step.observations.iter().any(|o| matches!(
2278 o,
2279 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 3
2280 )));
2281 }
2282
2283 #[test]
2284 fn load_workflow_self_bootstraps_with_no_prior_start_run() {
2285 use crate::orchestration::workflow::fanout_synthesize;
2289 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2290
2291 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2292 let spec =
2295 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2296 let step = runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2297 spec,
2298 parent_session_id: "sess".to_string(),
2299 resumed_completed: Vec::new(),
2300 resumed_submissions: Vec::new(),
2301 }));
2302 let batch = step
2303 .observations
2304 .iter()
2305 .find_map(|o| match o {
2306 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2307 _ => None,
2308 })
2309 .expect("workflow_batch_spawned even without a prior StartRun");
2310 assert_eq!(batch.len(), 2);
2311
2312 let complete = |runtime: &mut KernelRuntime, id: &str| {
2313 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2314 result: SubAgentResult {
2315 agent_id: compact_str::CompactString::new(id),
2316 result: LoopResult {
2317 termination: TerminationReason::Completed,
2318 final_message: None,
2319 turns_used: 1,
2320 total_tokens_used: 1,
2321 loop_continue: None,
2322 classify_branch: None,
2323 tournament_winner: None,
2324 },
2325 },
2326 }))
2327 };
2328 complete(&mut runtime, "wf-node0");
2329 complete(&mut runtime, "wf-node1");
2330 let step = complete(&mut runtime, "wf-node2");
2331 assert!(step.observations.iter().any(|o| matches!(
2332 o,
2333 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 3
2334 )));
2335 }
2336
2337 #[test]
2338 fn submit_workflow_nodes_input_appends_a_node_over_the_abi() {
2339 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
2342 use crate::types::agent::AgentRole;
2343 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2344
2345 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2346 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2347 task: RuntimeTask::new("parent task"),
2348 run_spec: None,
2349 }));
2350 runtime.state_machine_mut().take_observations();
2351
2352 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
2354 RuntimeTask::new("root"),
2355 AgentRole::Implement,
2356 )]);
2357 runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2358 spec,
2359 parent_session_id: "sess".to_string(),
2360 resumed_completed: Vec::new(),
2361 resumed_submissions: Vec::new(),
2362 }));
2363 runtime.state_machine_mut().take_observations();
2364
2365 let event = KernelInputEvent::SubmitWorkflowNodes {
2367 nodes: vec![WorkflowNode::new(RuntimeTask::new("more"), AgentRole::Implement)],
2368 submitter_agent_id: None,
2369 };
2370 let json = serde_json::to_string(&event).expect("serialize");
2371 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2372 let step = runtime.step(KernelInput::new(parsed));
2373 assert!(step.observations.iter().any(|o| matches!(
2375 o,
2376 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2377 if nodes.len() == 1 && nodes[0].agent_id == "wf-node1" && nodes[0].goal == "more"
2378 )));
2379
2380 let complete = |runtime: &mut KernelRuntime, id: &str| {
2381 runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2382 result: SubAgentResult {
2383 agent_id: compact_str::CompactString::new(id),
2384 result: LoopResult {
2385 termination: TerminationReason::Completed,
2386 final_message: None,
2387 turns_used: 1,
2388 total_tokens_used: 1,
2389 loop_continue: None,
2390 classify_branch: None,
2391 tournament_winner: None,
2392 },
2393 },
2394 }))
2395 };
2396 complete(&mut runtime, "wf-node0");
2397 let step = complete(&mut runtime, "wf-node1");
2399 assert!(step.observations.iter().any(|o| matches!(
2400 o,
2401 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 2
2402 )));
2403 }
2404
2405 #[test]
2406 fn submit_workflow_input_bootstraps_a_dag_over_the_abi() {
2407 use crate::orchestration::workflow::{WorkflowNode, WorkflowSpec};
2410 use crate::types::agent::AgentRole;
2411 use crate::types::result::{LoopResult, SubAgentResult, TerminationReason};
2412
2413 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2414 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2415 task: RuntimeTask::new("parent task"),
2416 run_spec: None,
2417 }));
2418 runtime.state_machine_mut().take_observations();
2419
2420 let spec = WorkflowSpec::new(vec![WorkflowNode::new(
2422 RuntimeTask::new("authored root"),
2423 AgentRole::Implement,
2424 )]);
2425 let event = KernelInputEvent::SubmitWorkflow {
2426 spec,
2427 parent_session_id: "sess".to_string(),
2428 submitter_agent_id: None,
2429 };
2430 let json = serde_json::to_string(&event).expect("serialize");
2431 let parsed: KernelInputEvent = serde_json::from_str(&json).expect("deserialize");
2432 let step = runtime.step(KernelInput::new(parsed));
2433 assert!(step.observations.iter().any(|o| matches!(
2435 o,
2436 KernelObservation::WorkflowBatchSpawned { nodes, .. }
2437 if nodes.len() == 1 && nodes[0].agent_id == "wf-node0" && nodes[0].goal == "authored root"
2438 )));
2439
2440 let step = runtime.step(KernelInput::new(KernelInputEvent::SubAgentCompleted {
2441 result: SubAgentResult {
2442 agent_id: compact_str::CompactString::new("wf-node0"),
2443 result: LoopResult {
2444 termination: TerminationReason::Completed,
2445 final_message: None,
2446 turns_used: 1,
2447 total_tokens_used: 1,
2448 loop_continue: None,
2449 classify_branch: None,
2450 tournament_winner: None,
2451 },
2452 },
2453 }));
2454 assert!(step.observations.iter().any(|o| matches!(
2455 o,
2456 KernelObservation::WorkflowCompleted { completed, .. } if completed.len() == 1
2457 )));
2458 }
2459
2460 #[test]
2461 fn load_workflow_resumes_from_completed_nodes() {
2462 use crate::orchestration::workflow::fanout_synthesize;
2463
2464 let mut runtime = KernelRuntime::new(LoopPolicy::default());
2465 runtime.step(KernelInput::new(KernelInputEvent::StartRun {
2466 task: RuntimeTask::new("parent task"),
2467 run_spec: None,
2468 }));
2469 runtime.state_machine_mut().take_observations();
2470
2471 let spec =
2473 fanout_synthesize(vec![RuntimeTask::new("w0"), RuntimeTask::new("w1")], RuntimeTask::new("synth"));
2474 let step = runtime.step(KernelInput::new(KernelInputEvent::LoadWorkflow {
2475 spec,
2476 parent_session_id: "sess".to_string(),
2477 resumed_completed: vec!["wf-node0".to_string()],
2478 resumed_submissions: Vec::new(),
2479 }));
2480
2481 let batch = step
2483 .observations
2484 .iter()
2485 .find_map(|o| match o {
2486 KernelObservation::WorkflowBatchSpawned { nodes, .. } => Some(nodes.clone()),
2487 _ => None,
2488 })
2489 .expect("workflow_batch_spawned");
2490 assert_eq!(batch.len(), 1);
2491 assert_eq!(batch[0].agent_id, "wf-node1");
2492 }
2493}