1use std::error::Error;
10use std::fmt;
11
12use crate::engine::{normalize_prompt, stable_id, KNOWLEDGE_SCHEMA_VERSION};
13use crate::link_store::{DoubletLink, LinkRecord};
14use crate::links_format::{format_lino_record, push_lino_node};
15use crate::seed::{self, Slot};
16
17mod structured;
18
19use structured::{
20 handler_signature, handler_stub_source, parse_structured_skill, structured_canonical,
21 StructuredSkillSpec,
22};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CompiledSkillPackage {
27 pub id: String,
29 pub legacy_behavior_rule_id: String,
31 pub source_description: String,
33 pub trigger: String,
35 pub normalized_trigger: String,
37 pub response: String,
39 pub rule_id: String,
41 pub handler_id: String,
43 pub skill_name: String,
45 pub inputs: Vec<CompiledSkillInput>,
47 pub preconditions: Vec<CompiledSkillPrecondition>,
49 pub steps: Vec<CompiledSkillStep>,
51 pub effects: Vec<CompiledSkillEffect>,
53 pub expected_tests: Vec<CompiledSkillExpectedTest>,
55 pub required_permissions: Vec<CompiledSkillPermission>,
57 pub handler_stubs: Vec<CompiledSkillHandlerStub>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct CompiledSkillInput {
64 pub id: String,
65 pub name: String,
66 pub value_type: String,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct CompiledSkillPrecondition {
72 pub id: String,
73 pub description: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct CompiledSkillStep {
79 pub id: String,
80 pub order: usize,
81 pub description: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CompiledSkillEffect {
87 pub id: String,
88 pub description: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct CompiledSkillExpectedTest {
94 pub id: String,
95 pub input: String,
96 pub normalized_input: String,
97 pub expected_output: String,
98 pub trigger_id: String,
99 pub handler_id: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct CompiledSkillPermission {
105 pub id: String,
106 pub capability: String,
107 pub description: String,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct CompiledSkillHandlerStub {
113 pub id: String,
114 pub target: String,
115 pub signature: String,
116 pub source: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct CompiledSkillReplay {
122 pub package_id: String,
124 pub rule_id: String,
126 pub handler_id: String,
128 pub answer: String,
130 pub cache_hit: String,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum SkillCompileError {
137 UnsupportedShape,
139 UnsupportedInstruction { reason: String },
141 PermissionRequired { capability: String },
143}
144
145impl fmt::Display for SkillCompileError {
146 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147 match self {
148 Self::UnsupportedShape => {
149 write!(formatter, "unsupported natural-language skill shape")
150 }
151 Self::UnsupportedInstruction { reason } => {
152 write!(
153 formatter,
154 "unsupported natural-language instruction: {reason}"
155 )
156 }
157 Self::PermissionRequired { capability } => {
158 write!(formatter, "skill requires explicit permission {capability}")
159 }
160 }
161 }
162}
163
164impl Error for SkillCompileError {}
165
166pub fn compile_natural_language_skill(
172 description: &str,
173) -> Result<CompiledSkillPackage, SkillCompileError> {
174 if let Some(spec) = parse_structured_skill(description)? {
175 return CompiledSkillPackage::from_structured(description, spec);
176 }
177 let Some((trigger, response)) = extract_trigger_response(description) else {
178 return Err(SkillCompileError::UnsupportedShape);
179 };
180 Ok(CompiledSkillPackage::new(description, &trigger, &response))
181}
182
183pub(crate) fn natural_language_skill_compiler_record() -> String {
185 format_lino_record(
186 "natural_language_skill_compiler",
187 &[
188 ("type", String::from("compiled_handler")),
189 ("rule_shape", String::from("natural_language_skill")),
190 ("package_type", String::from("CompiledSkillPackage")),
191 ("module", String::from("src/skill_compiler.rs")),
192 ("exports", String::from("compile_natural_language_skill")),
193 ("trigger_record", String::from("CompiledSkillTriggerRule")),
194 ("handler_record", String::from("CompiledSkillHandler")),
195 ("cache_event", String::from("cache_hit")),
196 ("source", String::from("ARCHITECTURE.md section 9 #5")),
197 ],
198 )
199}
200
201impl CompiledSkillPackage {
202 #[must_use]
204 pub fn new(source_description: &str, trigger: &str, response: &str) -> Self {
205 let normalized_trigger = normalize_prompt(trigger);
206 let canonical = format!("{normalized_trigger}\n{response}");
207 let id = stable_id("compiled_skill", &canonical);
208 let rule_id = stable_id(
209 "compiled_skill_rule",
210 &format!("{id}:rule:{normalized_trigger}"),
211 );
212 let handler_id = stable_id(
213 "compiled_skill_handler",
214 &format!("{id}:handler:{response}"),
215 );
216 let legacy_behavior_rule_id =
217 stable_id("behavior_rule_runtime", &format!("{trigger}\n{response}"));
218 Self {
219 id,
220 legacy_behavior_rule_id,
221 source_description: source_description.to_owned(),
222 trigger: trigger.to_owned(),
223 normalized_trigger,
224 response: response.to_owned(),
225 rule_id,
226 handler_id,
227 skill_name: String::from("trigger_response_skill"),
228 inputs: Vec::new(),
229 preconditions: Vec::new(),
230 steps: Vec::new(),
231 effects: Vec::new(),
232 expected_tests: Vec::new(),
233 required_permissions: Vec::new(),
234 handler_stubs: Vec::new(),
235 }
236 }
237
238 fn from_structured(
239 source_description: &str,
240 spec: StructuredSkillSpec,
241 ) -> Result<Self, SkillCompileError> {
242 let Some(primary) = spec.primary_replay() else {
243 return Err(SkillCompileError::UnsupportedShape);
244 };
245 let normalized_trigger = normalize_prompt(&primary.trigger);
246 let canonical = structured_canonical(source_description, &spec, &primary);
247 let id = stable_id("compiled_skill", &canonical);
248 let rule_id = stable_id(
249 "compiled_skill_rule",
250 &format!("{id}:rule:{normalized_trigger}"),
251 );
252 let handler_id = stable_id(
253 "compiled_skill_handler",
254 &format!("{id}:handler:{}", primary.response),
255 );
256 let legacy_behavior_rule_id = stable_id(
257 "behavior_rule_runtime",
258 &format!("{}\n{}", primary.trigger, primary.response),
259 );
260 let inputs = spec
261 .inputs
262 .into_iter()
263 .map(|input| {
264 let record_id = stable_id("compiled_skill_input", &format!("{id}:{}", input.name));
265 CompiledSkillInput {
266 id: record_id,
267 name: input.name,
268 value_type: input.value_type,
269 }
270 })
271 .collect::<Vec<_>>();
272 let preconditions = spec
273 .preconditions
274 .into_iter()
275 .map(|description| CompiledSkillPrecondition {
276 id: stable_id(
277 "compiled_skill_precondition",
278 &format!("{id}:{description}"),
279 ),
280 description,
281 })
282 .collect();
283 let steps = spec
284 .steps
285 .into_iter()
286 .enumerate()
287 .map(|(index, description)| CompiledSkillStep {
288 id: stable_id(
289 "compiled_skill_step",
290 &format!("{id}:{}:{description}", index + 1),
291 ),
292 order: index + 1,
293 description,
294 })
295 .collect::<Vec<_>>();
296 let effects = spec
297 .effects
298 .into_iter()
299 .map(|description| CompiledSkillEffect {
300 id: stable_id("compiled_skill_effect", &format!("{id}:{description}")),
301 description,
302 })
303 .collect();
304 let expected_tests = spec
305 .expected_tests
306 .into_iter()
307 .map(|test| {
308 let normalized_input = normalize_prompt(&test.input);
309 let test_id = stable_id(
310 "compiled_skill_test",
311 &format!("{id}:{normalized_input}:{}", test.expected_output),
312 );
313 CompiledSkillExpectedTest {
314 id: test_id,
315 trigger_id: stable_id(
316 "compiled_skill_rule",
317 &format!("{id}:test_rule:{normalized_input}"),
318 ),
319 handler_id: stable_id(
320 "compiled_skill_handler",
321 &format!(
322 "{id}:test_handler:{normalized_input}:{}",
323 test.expected_output
324 ),
325 ),
326 input: test.input,
327 normalized_input,
328 expected_output: test.expected_output,
329 }
330 })
331 .collect::<Vec<_>>();
332 let required_permissions = spec
333 .permissions
334 .into_iter()
335 .map(|permission| CompiledSkillPermission {
336 id: stable_id(
337 "compiled_skill_permission",
338 &format!("{id}:{}:{}", permission.capability, permission.description),
339 ),
340 capability: permission.capability,
341 description: permission.description,
342 })
343 .collect::<Vec<_>>();
344 let skill_name = spec.name.clone();
345 let handler_stubs = spec
346 .targets
347 .into_iter()
348 .map(|target| {
349 let signature = handler_signature(&skill_name, &inputs);
350 let source = handler_stub_source(&target, &skill_name, &inputs, &steps, &primary);
351 CompiledSkillHandlerStub {
352 id: stable_id("compiled_skill_handler_stub", &format!("{id}:{target}")),
353 target,
354 signature,
355 source,
356 }
357 })
358 .collect();
359 Ok(Self {
360 id,
361 legacy_behavior_rule_id,
362 source_description: source_description.to_owned(),
363 trigger: primary.trigger,
364 normalized_trigger,
365 response: primary.response,
366 rule_id,
367 handler_id,
368 skill_name,
369 inputs,
370 preconditions,
371 steps,
372 effects,
373 expected_tests,
374 required_permissions,
375 handler_stubs,
376 })
377 }
378
379 #[must_use]
381 pub fn replay(&self, prompt: &str) -> Option<CompiledSkillReplay> {
382 let normalized = normalize_prompt(prompt);
383 if let Some(test) = self
384 .expected_tests
385 .iter()
386 .find(|test| test.normalized_input == normalized)
387 {
388 return Some(CompiledSkillReplay {
389 package_id: self.id.clone(),
390 rule_id: test.trigger_id.clone(),
391 handler_id: test.handler_id.clone(),
392 answer: test.expected_output.clone(),
393 cache_hit: self.id.clone(),
394 });
395 }
396 if normalized != self.normalized_trigger {
397 return None;
398 }
399 Some(CompiledSkillReplay {
400 package_id: self.id.clone(),
401 rule_id: self.rule_id.clone(),
402 handler_id: self.handler_id.clone(),
403 answer: self.response.clone(),
404 cache_hit: self.id.clone(),
405 })
406 }
407
408 #[must_use]
410 pub fn links_notation(&self) -> String {
411 let mut out = String::new();
412 push_lino_node(&mut out, 0, &self.id, None);
413 push_lino_node(&mut out, 2, "type", Some("compiled_skill_package"));
414 push_lino_node(
415 &mut out,
416 2,
417 "schema_version",
418 Some(KNOWLEDGE_SCHEMA_VERSION),
419 );
420 push_lino_node(&mut out, 2, "package_kind", Some("associative_package"));
421 push_lino_node(&mut out, 2, "source", Some("natural_language_skill"));
422 push_lino_node(
423 &mut out,
424 2,
425 "source_description",
426 Some(&self.source_description),
427 );
428 push_lino_node(&mut out, 2, "skill_name", Some(&self.skill_name));
429 push_lino_node(&mut out, 2, "trigger_rule", Some(&self.rule_id));
430 push_lino_node(&mut out, 2, "trigger", Some(&self.trigger));
431 push_lino_node(
432 &mut out,
433 2,
434 "normalized_trigger",
435 Some(&self.normalized_trigger),
436 );
437 push_lino_node(&mut out, 2, "compiled_handler", Some(&self.handler_id));
438 push_lino_node(&mut out, 2, "handler_kind", Some("deterministic_response"));
439 push_lino_node(&mut out, 2, "response", Some(&self.response));
440 push_lino_node(&mut out, 2, "replay_mode", Some("exact_normalized_prompt"));
441 push_lino_node(
442 &mut out,
443 2,
444 "legacy_behavior_rule_id",
445 Some(&self.legacy_behavior_rule_id),
446 );
447 for input in &self.inputs {
448 push_lino_node(&mut out, 2, "input", Some(&input.name));
449 push_lino_node(&mut out, 4, "id", Some(&input.id));
450 push_lino_node(&mut out, 4, "type", Some(&input.value_type));
451 }
452 for precondition in &self.preconditions {
453 push_lino_node(&mut out, 2, "precondition", Some(&precondition.id));
454 push_lino_node(&mut out, 4, "description", Some(&precondition.description));
455 }
456 for step in &self.steps {
457 let order = step.order.to_string();
458 push_lino_node(&mut out, 2, "step", Some(&step.id));
459 push_lino_node(&mut out, 4, "order", Some(&order));
460 push_lino_node(&mut out, 4, "description", Some(&step.description));
461 }
462 for effect in &self.effects {
463 push_lino_node(&mut out, 2, "effect", Some(&effect.id));
464 push_lino_node(&mut out, 4, "description", Some(&effect.description));
465 }
466 for test in &self.expected_tests {
467 push_lino_node(&mut out, 2, "expected_test", Some(&test.id));
468 push_lino_node(&mut out, 4, "input", Some(&test.input));
469 push_lino_node(
470 &mut out,
471 4,
472 "normalized_input",
473 Some(&test.normalized_input),
474 );
475 push_lino_node(&mut out, 4, "expected_output", Some(&test.expected_output));
476 push_lino_node(&mut out, 4, "trigger_rule", Some(&test.trigger_id));
477 push_lino_node(&mut out, 4, "compiled_handler", Some(&test.handler_id));
478 }
479 for permission in &self.required_permissions {
480 push_lino_node(&mut out, 2, "permission", Some(&permission.id));
481 push_lino_node(&mut out, 4, "capability", Some(&permission.capability));
482 push_lino_node(&mut out, 4, "description", Some(&permission.description));
483 }
484 for stub in &self.handler_stubs {
485 push_lino_node(&mut out, 2, "handler_stub", Some(&stub.id));
486 push_lino_node(&mut out, 4, "target", Some(&stub.target));
487 push_lino_node(&mut out, 4, "signature", Some(&stub.signature));
488 push_lino_node(&mut out, 4, "source_code", Some(&stub.source));
489 }
490 out.trim_end().to_owned()
491 }
492
493 #[must_use]
495 pub fn link_records(&self) -> Vec<LinkRecord> {
496 let mut records = vec![
497 link_record(
498 &self.id,
499 "CompiledSkillPackage",
500 "associative_package",
501 &stable_id("natural_language_skill", &self.source_description),
502 &[
503 ("source_description", self.source_description.as_str()),
504 ("skill_name", self.skill_name.as_str()),
505 ("trigger_rule", self.rule_id.as_str()),
506 ("compiled_handler", self.handler_id.as_str()),
507 ("replay_mode", "exact_normalized_prompt"),
508 ],
509 ),
510 link_record(
511 &self.rule_id,
512 "CompiledSkillTriggerRule",
513 "substitution_rule",
514 &self.id,
515 &[
516 ("trigger", self.trigger.as_str()),
517 ("normalized_trigger", self.normalized_trigger.as_str()),
518 ("handler", self.handler_id.as_str()),
519 ],
520 ),
521 link_record(
522 &self.handler_id,
523 "CompiledSkillHandler",
524 "deterministic_response_handler",
525 &self.id,
526 &[
527 ("handler_kind", "deterministic_response"),
528 ("response", self.response.as_str()),
529 ],
530 ),
531 ];
532 for input in &self.inputs {
533 records.push(link_record(
534 &input.id,
535 "CompiledSkillInput",
536 "typed_input",
537 &self.id,
538 &[
539 ("name", input.name.as_str()),
540 ("type", input.value_type.as_str()),
541 ],
542 ));
543 }
544 for precondition in &self.preconditions {
545 records.push(link_record(
546 &precondition.id,
547 "CompiledSkillPrecondition",
548 "precondition",
549 &self.id,
550 &[("description", precondition.description.as_str())],
551 ));
552 }
553 for step in &self.steps {
554 let order = step.order.to_string();
555 records.push(link_record(
556 &step.id,
557 "CompiledSkillStep",
558 "procedure_step",
559 &self.id,
560 &[
561 ("order", order.as_str()),
562 ("description", step.description.as_str()),
563 ],
564 ));
565 }
566 for effect in &self.effects {
567 records.push(link_record(
568 &effect.id,
569 "CompiledSkillEffect",
570 "declared_effect",
571 &self.id,
572 &[("description", effect.description.as_str())],
573 ));
574 }
575 for test in &self.expected_tests {
576 records.push(link_record(
577 &test.id,
578 "CompiledSkillExpectedTest",
579 "generated_test",
580 &self.id,
581 &[
582 ("input", test.input.as_str()),
583 ("expected_output", test.expected_output.as_str()),
584 ("trigger_rule", test.trigger_id.as_str()),
585 ("compiled_handler", test.handler_id.as_str()),
586 ],
587 ));
588 }
589 for permission in &self.required_permissions {
590 records.push(link_record(
591 &permission.id,
592 "CompiledSkillPermission",
593 "permission_grant",
594 &self.id,
595 &[
596 ("capability", permission.capability.as_str()),
597 ("description", permission.description.as_str()),
598 ],
599 ));
600 }
601 for stub in &self.handler_stubs {
602 records.push(link_record(
603 &stub.id,
604 "CompiledSkillHandlerStub",
605 "generated_handler_stub",
606 &self.id,
607 &[
608 ("target", stub.target.as_str()),
609 ("signature", stub.signature.as_str()),
610 ("source_code", stub.source.as_str()),
611 ],
612 ));
613 }
614 records
615 }
616}
617
618fn extract_trigger_response(description: &str) -> Option<(String, String)> {
619 if !looks_like_skill_description(description) {
620 return None;
621 }
622 let spans = code_spans(description);
623 if spans.len() < 2 {
624 return None;
625 }
626 let trigger = spans[0].trim();
627 let response = spans[1].trim();
628 if trigger.is_empty() || response.is_empty() {
629 return None;
630 }
631 Some((trigger.to_owned(), response.to_owned()))
632}
633
634fn looks_like_skill_description(description: &str) -> bool {
650 let lower = description.to_lowercase();
651 if explicit_teaching_form(&lower) {
652 return true;
653 }
654 seed::lexicon()
655 .role_word_forms(seed::ROLE_SKILL_WHEN_THEN_PAIR)
656 .into_iter()
657 .filter(|form| form.slot() == Slot::Circumfix)
658 .any(|form| {
659 let head = form.before_slot();
660 let link = form.after_slot();
661 let Some(head_pos) = lower.find(head) else {
662 return false;
663 };
664 let Some(link_pos) = lower[head_pos + head.len()..].find(link) else {
665 return false;
666 };
667 let absolute_link_pos = head_pos + head.len() + link_pos;
668 let before_link = &description[head_pos..absolute_link_pos];
669 let after_link = &description[absolute_link_pos + link.len()..];
670 before_link.contains('`') && after_link.contains('`')
671 })
672}
673
674fn explicit_teaching_form(lower: &str) -> bool {
689 let lexicon = seed::lexicon();
690 (lexicon.mentions_role_raw(seed::ROLE_SKILL_TEACHING_TRIGGER_LEAD, lower)
691 && lexicon.mentions_role_raw(seed::ROLE_SKILL_TEACHING_RESPONSE_VERB, lower))
692 || lexicon.mentions_role_raw(seed::ROLE_BEHAVIOR_RULE_EDIT_DIRECTIVE, lower)
693}
694
695fn code_spans(text: &str) -> Vec<String> {
696 text.split('`')
697 .enumerate()
698 .filter_map(|(index, part)| {
699 let trimmed = part.trim();
700 if index % 2 == 1 && !trimmed.is_empty() {
701 Some(trimmed.to_owned())
702 } else {
703 None
704 }
705 })
706 .collect()
707}
708
709fn link_record(
710 record_id: &str,
711 record_type: &str,
712 subtype: &str,
713 source_id: &str,
714 fields: &[(&str, &str)],
715) -> LinkRecord {
716 let mut links = Vec::new();
717 push_doublet(&mut links, record_id, "Type");
718 push_doublet(&mut links, "Type", record_type);
719 push_doublet(&mut links, record_type, "SubType");
720 push_doublet(&mut links, "SubType", subtype);
721 push_doublet(&mut links, subtype, "Value");
722 push_doublet(&mut links, record_id, source_id);
723 push_field(
724 &mut links,
725 record_id,
726 "schema_version",
727 KNOWLEDGE_SCHEMA_VERSION,
728 );
729 for (key, value) in fields {
730 push_field(&mut links, record_id, key, value);
731 }
732 LinkRecord {
733 stable_id: record_id.to_owned(),
734 schema_version: String::from(KNOWLEDGE_SCHEMA_VERSION),
735 record_type: record_type.to_owned(),
736 source_id: source_id.to_owned(),
737 links,
738 }
739}
740
741fn push_field(links: &mut Vec<DoubletLink>, record_id: &str, key: &str, value: &str) {
742 if value.is_empty() {
743 return;
744 }
745 let field = format!("field:{key}");
746 let field_value = format!("value:{value}");
747 push_doublet(links, record_id, &field);
748 push_doublet(links, &field, &field_value);
749}
750
751fn push_doublet(links: &mut Vec<DoubletLink>, from: &str, to: &str) {
752 links.push(DoubletLink {
753 index: stable_id("doublet", &format!("{from}->{to}")),
754 from: from.to_owned(),
755 to: to.to_owned(),
756 });
757}