1use a3s_effect::{
13 budget, coding_scheduler, compact, component, compose_coding_actor, system, tools,
14 CodingServices, ErasedComponent, HarnessConfig, HarnessGraph, HarnessView, MetaHarnessSpec,
15 ToolSpec,
16};
17
18pub use a3s_effect::HarnessPartId;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum HarnessComponentRef {
23 Stock(HarnessPartId),
24 Host(String),
26}
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct HarnessComposeOptions {
31 pub tool_budget: Option<u32>,
33 pub compact_after_chars: Option<usize>,
35 pub system: Vec<String>,
37 pub parts: Vec<HarnessPartId>,
39 pub components: Vec<String>,
42}
43
44impl HarnessComposeOptions {
45 pub fn to_spec(&self, tools: Vec<ToolSpec>, defaults: &HarnessConfig) -> MetaHarnessSpec {
46 MetaHarnessSpec {
47 name: "a3s-code",
48 budget: self.tool_budget.unwrap_or_else(|| defaults.budget()),
49 compact_after_chars: self
50 .compact_after_chars
51 .unwrap_or_else(|| defaults.compact_after_chars()),
52 step_limit: defaults.step_limit(),
53 model_attempts: defaults.model_attempts(),
54 system: if self.system.is_empty() {
55 defaults.system().to_vec()
56 } else {
57 self.system.clone()
58 },
59 tools,
60 tool_round_cap: defaults.tool_round_cap(),
61 parts: self.parts.clone(),
62 }
63 }
64
65 pub fn resolved_components(&self) -> anyhow::Result<Vec<HarnessComponentRef>> {
67 if !self.components.is_empty() {
68 return parse_harness_components(&self.components);
69 }
70 if !self.parts.is_empty() {
71 return Ok(self
72 .parts
73 .iter()
74 .copied()
75 .map(HarnessComponentRef::Stock)
76 .collect());
77 }
78 Ok(default_stock_components())
79 }
80
81 pub fn compose(
87 components: Vec<String>,
88 tool_budget: Option<u32>,
89 compact_after_chars: Option<usize>,
90 system: Vec<String>,
91 ) -> anyhow::Result<Self> {
92 let refs = parse_harness_components(&components)?;
93 let (stock_parts, host_ids) = partition_components(&refs);
94 let parts = if host_ids.is_empty() {
97 stock_parts
98 } else {
99 Vec::new()
100 };
101 Ok(Self {
102 tool_budget,
103 compact_after_chars,
104 system,
105 parts,
106 components,
107 })
108 }
109}
110
111fn default_stock_components() -> Vec<HarnessComponentRef> {
113 vec![
114 HarnessComponentRef::Stock(HarnessPartId::System),
115 HarnessComponentRef::Stock(HarnessPartId::Tools),
116 HarnessComponentRef::Stock(HarnessPartId::Budget),
117 HarnessComponentRef::Stock(HarnessPartId::Compact),
118 HarnessComponentRef::Stock(HarnessPartId::Infer),
119 ]
120}
121
122fn partition_components(refs: &[HarnessComponentRef]) -> (Vec<HarnessPartId>, Vec<String>) {
125 let mut stock = Vec::new();
126 let mut hosts = Vec::new();
127 for entry in refs {
128 match entry {
129 HarnessComponentRef::Stock(part) => stock.push(*part),
130 HarnessComponentRef::Host(id) => hosts.push(id.clone()),
131 }
132 }
133 (stock, hosts)
134}
135
136pub fn parse_harness_part(name: &str) -> anyhow::Result<HarnessPartId> {
138 match name.trim().to_ascii_lowercase().as_str() {
139 "system" => Ok(HarnessPartId::System),
140 "tools" => Ok(HarnessPartId::Tools),
141 "budget" => Ok(HarnessPartId::Budget),
142 "compact" => Ok(HarnessPartId::Compact),
143 "infer" => Ok(HarnessPartId::Infer),
144 other => anyhow::bail!(
145 "unknown harness part '{other}'; expected system|tools|budget|compact|infer"
146 ),
147 }
148}
149
150pub fn parse_harness_parts(parts: &[String]) -> anyhow::Result<Vec<HarnessPartId>> {
152 parts.iter().map(|part| parse_harness_part(part)).collect()
153}
154
155pub fn parse_harness_component(name: &str) -> anyhow::Result<HarnessComponentRef> {
157 let trimmed = name.trim();
158 if let Some(id) = trimmed
159 .strip_prefix("host:")
160 .or_else(|| trimmed.strip_prefix("HOST:"))
161 {
162 let id = id.trim();
163 if id.is_empty() {
164 anyhow::bail!("host harness mount requires a non-empty id after 'host:'");
165 }
166 if id.contains(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')) {
167 anyhow::bail!("invalid host harness id '{id}': use ascii alphanumeric, '_' or '-'");
168 }
169 return Ok(HarnessComponentRef::Host(id.to_ascii_lowercase()));
170 }
171 Ok(HarnessComponentRef::Stock(parse_harness_part(trimmed)?))
172}
173
174pub fn parse_harness_components(components: &[String]) -> anyhow::Result<Vec<HarnessComponentRef>> {
176 components
177 .iter()
178 .map(|entry| parse_harness_component(entry))
179 .collect()
180}
181
182pub fn host_component_id(id: impl AsRef<str>) -> String {
184 format!("host:{}", id.as_ref().trim().to_ascii_lowercase())
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct KernelPolicy {
190 pub permission_overlay: bool,
192 pub completion_gate: bool,
194}
195
196impl Default for KernelPolicy {
197 fn default() -> Self {
198 Self {
199 permission_overlay: true,
200 completion_gate: true,
201 }
202 }
203}
204
205impl KernelPolicy {
206 pub fn admit(self) -> Self {
208 Self {
209 permission_overlay: true,
210 completion_gate: true,
211 }
212 }
213}
214
215pub trait HostHarnessRegistry: Send + Sync {
217 fn mount(
219 &self,
220 id: &str,
221 config: &HarnessConfig,
222 ) -> anyhow::Result<ErasedComponent<CodingServices, HarnessView>>;
223}
224
225pub trait HostHarnessAssembler: Send + Sync {
227 fn assemble(&self, config: HarnessConfig) -> anyhow::Result<HarnessGraph>;
228}
229
230#[derive(Debug, Default, Clone, Copy)]
235pub struct BuiltinHostHarnessRegistry;
236
237pub const INTENT_STAMP_MARKER: &str = "a3s.meta_harness.intent_stamp.v1";
239
240fn intent_stamp_view() -> a3s_effect::CodingView {
242 a3s_effect::CodingView {
243 system: vec![INTENT_STAMP_MARKER.into()],
244 ..a3s_effect::CodingView::empty()
245 }
246}
247
248impl HostHarnessRegistry for BuiltinHostHarnessRegistry {
249 fn mount(
250 &self,
251 id: &str,
252 _config: &HarnessConfig,
253 ) -> anyhow::Result<ErasedComponent<CodingServices, HarnessView>> {
254 match id {
255 "intent_stamp" => Ok(component(
256 || (),
257 |state, _fact| state,
258 |_state| (intent_stamp_view(), Vec::new()),
259 )),
260 other => anyhow::bail!(
261 "unknown builtin host harness component '{other}'; known: intent_stamp"
262 ),
263 }
264 }
265}
266
267pub fn admit_default_graph(config: HarnessConfig) -> (HarnessGraph, KernelPolicy) {
269 (
270 HarnessGraph::coding(config),
271 KernelPolicy::default().admit(),
272 )
273}
274
275pub fn admit_spec_graph(spec: MetaHarnessSpec) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
277 let config = spec
278 .clone()
279 .into_config()
280 .map_err(|error| anyhow::anyhow!(error))?;
281 let graph = HarnessGraph::from_spec(spec, || coding_scheduler(config.clone()));
282 Ok((graph, KernelPolicy::default().admit()))
283}
284
285pub fn admit_component_tree(
287 name: &'static str,
288 components: Vec<ErasedComponent<CodingServices, HarnessView>>,
289) -> (HarnessGraph, KernelPolicy) {
290 (
291 HarnessGraph::from_actor(compose_coding_actor(name, components)),
292 KernelPolicy::default().admit(),
293 )
294}
295
296pub fn admit_from_compose(
298 compose: Option<&HarnessComposeOptions>,
299 config: HarnessConfig,
300) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
301 admit_from_compose_with_registry(compose, None, config)
302}
303
304pub fn admit_from_compose_with_registry(
306 compose: Option<&HarnessComposeOptions>,
307 registry: Option<&dyn HostHarnessRegistry>,
308 config: HarnessConfig,
309) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
310 let Some(options) = compose else {
311 return Ok(admit_default_graph(config));
312 };
313 let refs = options.resolved_components()?;
314 let needs_host = refs
315 .iter()
316 .any(|entry| matches!(entry, HarnessComponentRef::Host(_)));
317 if !needs_host {
318 let tools = config.tools().to_vec();
320 let mut spec = options.to_spec(tools, &config);
321 if !options.components.is_empty() {
322 let (stock_parts, _) = partition_components(&refs);
323 spec.parts = stock_parts;
324 }
325 return admit_spec_graph(spec);
326 }
327 let registry = registry.ok_or_else(|| {
328 anyhow::anyhow!(
329 "harness components include host:* mounts but no HostHarnessRegistry was installed"
330 )
331 })?;
332 admit_mixed_tree(options, refs, registry, config)
333}
334
335fn admit_mixed_tree(
336 options: &HarnessComposeOptions,
337 refs: Vec<HarnessComponentRef>,
338 registry: &dyn HostHarnessRegistry,
339 config: HarnessConfig,
340) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
341 let system_prompts = if options.system.is_empty() {
342 config.system().to_vec()
343 } else {
344 options.system.clone()
345 };
346 let tool_specs = config.tools().to_vec();
347 let budget_limit = options.tool_budget.unwrap_or_else(|| config.budget());
348 let compact_after = options
349 .compact_after_chars
350 .unwrap_or_else(|| config.compact_after_chars());
351
352 let mut components = Vec::with_capacity(refs.len());
353 for entry in refs {
354 match entry {
355 HarnessComponentRef::Stock(HarnessPartId::System) => {
356 components.push(system(system_prompts.clone()));
357 }
358 HarnessComponentRef::Stock(HarnessPartId::Tools) => {
359 components.push(tools(tool_specs.clone()));
360 }
361 HarnessComponentRef::Stock(HarnessPartId::Budget) => {
362 components.push(budget(budget_limit));
363 }
364 HarnessComponentRef::Stock(HarnessPartId::Compact) => {
365 components.push(compact(compact_after));
366 }
367 HarnessComponentRef::Stock(HarnessPartId::Infer) => {
368 components.push(coding_scheduler(config.clone()));
369 }
370 HarnessComponentRef::Host(id) => {
371 components.push(registry.mount(&id, &config)?);
372 }
373 }
374 }
375 Ok(admit_component_tree("a3s-code", components))
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn kernel_policy_cannot_disable_governance() {
384 let cleared = KernelPolicy {
385 permission_overlay: false,
386 completion_gate: false,
387 };
388 let admitted = cleared.admit();
389 assert!(admitted.permission_overlay);
390 assert!(admitted.completion_gate);
391 }
392
393 #[test]
394 fn default_admission_builds_stock_graph() {
395 let config = HarnessConfig::new(4, 1_000, 8, 1, vec!["s".into()], vec![]).expect("config");
396 let (graph, policy) = admit_default_graph(config);
397 assert_eq!(graph.actor().name, "a3s-code");
398 assert!(policy.permission_overlay);
399 assert!(policy.completion_gate);
400 }
401
402 #[test]
403 fn compose_options_override_budget() {
404 let defaults = HarnessConfig::new(8, 1_000, 8, 1, vec![], vec![]).expect("config");
405 let options = HarnessComposeOptions {
406 tool_budget: Some(3),
407 compact_after_chars: Some(50),
408 system: vec!["compose".into()],
409 parts: vec![HarnessPartId::System, HarnessPartId::Infer],
410 components: Vec::new(),
411 };
412 let spec = options.to_spec(Vec::new(), &defaults);
413 assert_eq!(spec.budget, 3);
414 assert_eq!(spec.compact_after_chars, 50);
415 assert_eq!(spec.system, vec!["compose".to_string()]);
416 assert_eq!(
417 spec.parts,
418 vec![HarnessPartId::System, HarnessPartId::Infer]
419 );
420 }
421
422 #[test]
423 fn compose_parses_stock_part_names() {
424 let options = HarnessComposeOptions::compose(
425 vec![
426 "system".into(),
427 "tools".into(),
428 "budget".into(),
429 "compact".into(),
430 "infer".into(),
431 ],
432 Some(2),
433 Some(10),
434 vec!["s".into()],
435 )
436 .expect("compose");
437 assert_eq!(
438 options.parts,
439 vec![
440 HarnessPartId::System,
441 HarnessPartId::Tools,
442 HarnessPartId::Budget,
443 HarnessPartId::Compact,
444 HarnessPartId::Infer,
445 ]
446 );
447 assert!(parse_harness_part("parallel_task").is_err());
448 }
449
450 #[test]
451 fn components_list_accepts_host_mount_and_reorder() {
452 let options = HarnessComposeOptions::compose(
453 vec![
454 "system".into(),
455 host_component_id("intent_stamp"),
456 "tools".into(),
457 "budget".into(),
458 "infer".into(),
459 ],
460 Some(2),
461 None,
462 vec!["base".into()],
463 )
464 .expect("compose");
465 assert!(
466 options.parts.is_empty(),
467 "host mounts force components path"
468 );
469 let refs = options.resolved_components().expect("refs");
470 assert_eq!(
471 refs,
472 vec![
473 HarnessComponentRef::Stock(HarnessPartId::System),
474 HarnessComponentRef::Host("intent_stamp".into()),
475 HarnessComponentRef::Stock(HarnessPartId::Tools),
476 HarnessComponentRef::Stock(HarnessPartId::Budget),
477 HarnessComponentRef::Stock(HarnessPartId::Infer),
478 ]
479 );
480 }
481
482 #[test]
483 fn host_mount_without_registry_fails_closed() {
484 let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
485 let options = HarnessComposeOptions::compose(
486 vec![
487 "system".into(),
488 host_component_id("intent_stamp"),
489 "infer".into(),
490 ],
491 None,
492 None,
493 vec![],
494 )
495 .expect("compose");
496 let err = match admit_from_compose_with_registry(Some(&options), None, config) {
497 Ok(_) => panic!("missing registry must fail"),
498 Err(error) => error,
499 };
500 assert!(err.to_string().contains("HostHarnessRegistry"));
501 }
502
503 #[test]
504 fn builtin_registry_admits_mixed_tree() {
505 let config = HarnessConfig::new(2, 100, 8, 1, vec!["sys".into()], vec![]).expect("config");
506 let options = HarnessComposeOptions::compose(
507 vec![
508 "system".into(),
509 "tools".into(),
510 host_component_id("intent_stamp"),
511 "budget".into(),
512 "infer".into(),
513 ],
514 Some(2),
515 None,
516 vec!["sys".into()],
517 )
518 .expect("compose");
519 let (graph, policy) = admit_from_compose_with_registry(
520 Some(&options),
521 Some(&BuiltinHostHarnessRegistry),
522 config,
523 )
524 .expect("admit");
525 assert_eq!(graph.actor().name, "a3s-code");
526 assert!(policy.permission_overlay && policy.completion_gate);
527 }
528
529 #[test]
530 fn unknown_host_id_fails_closed() {
531 let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
532 let options = HarnessComposeOptions::compose(
533 vec!["system".into(), host_component_id("nope"), "infer".into()],
534 None,
535 None,
536 vec![],
537 )
538 .expect("compose");
539 let err = match admit_from_compose_with_registry(
540 Some(&options),
541 Some(&BuiltinHostHarnessRegistry),
542 config,
543 ) {
544 Ok(_) => panic!("unknown host must fail"),
545 Err(error) => error,
546 };
547 assert!(err.to_string().contains("unknown builtin"));
548 }
549
550 #[test]
551 fn reject_empty_and_invalid_host_ids() {
552 assert!(parse_harness_component("host:").is_err());
553 assert!(parse_harness_component("host:bad.id").is_err());
554 assert!(parse_harness_component("parallel_task").is_err());
555 }
556
557 #[test]
558 fn admit_component_tree_preserves_kernel() {
559 let (_graph, policy) = admit_component_tree(
560 "custom",
561 vec![system(vec!["x".into()]), budget(1), compact(8)],
562 );
563 assert!(policy.permission_overlay);
564 assert!(policy.completion_gate);
565 }
566
567 #[test]
568 fn stock_only_components_list_uses_spec_path() {
569 let config = HarnessConfig::new(3, 50, 8, 1, vec![], vec![]).expect("config");
570 let options = HarnessComposeOptions::compose(
571 vec![
572 "system".into(),
573 "tools".into(),
574 "budget".into(),
575 "infer".into(),
576 ],
577 Some(3),
578 None,
579 vec![],
580 )
581 .expect("compose");
582 assert!(!options
584 .resolved_components()
585 .unwrap()
586 .iter()
587 .any(|entry| matches!(entry, HarnessComponentRef::Stock(HarnessPartId::Compact))));
588 let (graph, _) =
589 admit_from_compose_with_registry(Some(&options), None, config).expect("admit");
590 assert_eq!(graph.actor().name, "a3s-code");
591 }
592
593 #[test]
594 fn resolved_components_falls_back_to_parts_then_default_stock() {
595 let from_parts = HarnessComposeOptions {
596 parts: vec![HarnessPartId::System, HarnessPartId::Infer],
597 ..HarnessComposeOptions::default()
598 };
599 assert_eq!(
600 from_parts.resolved_components().expect("parts"),
601 vec![
602 HarnessComponentRef::Stock(HarnessPartId::System),
603 HarnessComponentRef::Stock(HarnessPartId::Infer),
604 ]
605 );
606
607 let defaults = HarnessComposeOptions::default()
608 .resolved_components()
609 .expect("default stock");
610 assert_eq!(defaults, default_stock_components());
611 }
612
613 #[test]
614 fn parse_harness_parts_accepts_stock_names_and_rejects_unknown() {
615 let parts = parse_harness_parts(&[
616 "System".into(),
617 "TOOLS".into(),
618 "budget".into(),
619 "compact".into(),
620 "infer".into(),
621 ])
622 .expect("stock names");
623 assert_eq!(
624 parts,
625 vec![
626 HarnessPartId::System,
627 HarnessPartId::Tools,
628 HarnessPartId::Budget,
629 HarnessPartId::Compact,
630 HarnessPartId::Infer,
631 ]
632 );
633 assert!(parse_harness_parts(&["nope".into()]).is_err());
634 }
635
636 #[test]
637 fn admit_from_compose_wrapper_defaults_and_admits_stock() {
638 let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
639 let (default_graph, policy) = admit_from_compose(None, config.clone()).expect("default");
640 assert_eq!(default_graph.actor().name, "a3s-code");
641 assert!(policy.permission_overlay && policy.completion_gate);
642
643 let options = HarnessComposeOptions {
644 parts: vec![
645 HarnessPartId::System,
646 HarnessPartId::Budget,
647 HarnessPartId::Infer,
648 ],
649 tool_budget: Some(2),
650 ..HarnessComposeOptions::default()
651 };
652 let (graph, _) = admit_from_compose(Some(&options), config).expect("stock");
653 assert_eq!(graph.actor().name, "a3s-code");
654 }
655
656 #[test]
657 fn mixed_tree_includes_compact_and_empty_system_fallback() {
658 let config =
659 HarnessConfig::new(2, 40, 8, 1, vec!["cfg-system".into()], vec![]).expect("config");
660 let options = HarnessComposeOptions::compose(
661 vec![
662 "system".into(),
663 "tools".into(),
664 "budget".into(),
665 "compact".into(),
666 host_component_id("intent_stamp"),
667 "infer".into(),
668 ],
669 Some(2),
670 Some(40),
671 vec![], )
673 .expect("compose");
674 let (graph, policy) = admit_from_compose_with_registry(
675 Some(&options),
676 Some(&BuiltinHostHarnessRegistry),
677 config,
678 )
679 .expect("admit mixed with compact");
680 assert_eq!(graph.actor().name, "a3s-code");
681 assert!(policy.permission_overlay && policy.completion_gate);
682 }
683
684 #[test]
685 fn partition_and_intent_stamp_view_are_behavior_oracles() {
686 let refs = vec![
687 HarnessComponentRef::Stock(HarnessPartId::System),
688 HarnessComponentRef::Host("intent_stamp".into()),
689 HarnessComponentRef::Stock(HarnessPartId::Infer),
690 ];
691 let (stock, hosts) = partition_components(&refs);
692 assert_eq!(stock, vec![HarnessPartId::System, HarnessPartId::Infer]);
693 assert_eq!(hosts, vec!["intent_stamp".to_string()]);
694
695 let view = intent_stamp_view();
696 assert_eq!(view.system, vec![INTENT_STAMP_MARKER.to_string()]);
697 }
698
699 #[test]
700 fn host_prefix_is_case_insensitive_and_normalizes_id() {
701 let upper = parse_harness_component("HOST:Intent_Stamp").expect("HOST:");
702 assert_eq!(upper, HarnessComponentRef::Host("intent_stamp".into()));
703 assert_eq!(
704 host_component_id(" Intent_Stamp "),
705 "host:intent_stamp".to_string()
706 );
707 }
708
709 #[test]
710 fn to_spec_keeps_default_system_when_compose_system_empty() {
711 let defaults =
712 HarnessConfig::new(4, 10, 8, 1, vec!["default-sys".into()], vec![]).expect("config");
713 let options = HarnessComposeOptions {
714 tool_budget: None,
715 compact_after_chars: None,
716 system: vec![],
717 parts: vec![HarnessPartId::System],
718 components: Vec::new(),
719 };
720 let spec = options.to_spec(Vec::new(), &defaults);
721 assert_eq!(spec.budget, 4);
722 assert_eq!(spec.compact_after_chars, 10);
723 assert_eq!(spec.system, vec!["default-sys".to_string()]);
724 }
725}