@@ -69,6 +69,42 @@ async def resolve_taxonomies(
6969 return list (active_domains )
7070
7171
72+ class DefaultKeywordResolver (TaxonomyResolver ):
73+ """Declarative, configuration-driven keyword/phrase resolver.
74+
75+ Scans user prompt history for triggering phrases defined directly inside each
76+ taxonomy term's triggers list or alt_labels, resolving active domains natively.
77+ """
78+
79+ def __init__ (self , registry : Any ):
80+ self .registry = registry
81+
82+ async def resolve_taxonomies (self , context : ReadonlyContext , llm_request : LlmRequest ) -> list [str ]:
83+ active_domains : set [str ] = set ()
84+
85+ for term_id in self .registry .list_ids ():
86+ term = self .registry .get_term (term_id )
87+ if term :
88+ triggers = getattr (term , "triggers" , [])
89+ if not triggers and hasattr (term , "model_extra" ):
90+ triggers = (term .model_extra or {}).get ("triggers" , [])
91+
92+ # Fall back to alt_labels as secondary keyword triggers
93+ if not triggers and hasattr (term , "alt_labels" ):
94+ triggers = term .alt_labels
95+
96+ if triggers :
97+ for turn in llm_request .contents :
98+ for part in turn .parts :
99+ if part .text :
100+ text_upper = part .text .upper ()
101+ if any (str (phrase ).upper () in text_upper for phrase in triggers ):
102+ active_domains .add (term_id )
103+ break
104+
105+ return list (active_domains )
106+
107+
72108class SkillPolicy (ABC ):
73109 """Abstract policy engine determining skill execution permissions and instruction shaping.
74110
@@ -175,13 +211,38 @@ def _get_taxonomy_binds(skill: Skill) -> list[str]:
175211 return list (binds )
176212
177213
214+ def _interpolate_variables (text : str , active_taxonomies : list [str ], registry : Optional [Any ]) -> str :
215+ if not text or not registry :
216+ return text
217+
218+ import re
219+ pattern = r"\{taxonomy:([a-zA-Z0-9_-]+)\}"
220+
221+ def replace (match ):
222+ var_name = match .group (1 )
223+ for tax_id in active_taxonomies :
224+ term = registry .get_term (tax_id )
225+ if term :
226+ variables = getattr (term , "variables" , {})
227+ if not variables and hasattr (term , "model_extra" ):
228+ variables = (term .model_extra or {}).get ("variables" , {})
229+ if variables and var_name in variables :
230+ return str (variables [var_name ])
231+ return ""
232+
233+ return re .sub (pattern , replace , text )
234+
235+
178236class DefaultSkillPolicy (SkillPolicy ):
179237 """Default skill policy using taxonomy-bind set-intersection matching.
180238
181239 If a skill has no taxonomy binds defined, it is treated as unrestricted/allowed by default.
182240 If it has binds, at least one bind must intersect with the active taxonomy set.
183241 """
184242
243+ def __init__ (self , registry : Optional [Any ] = None ):
244+ self .registry = registry
245+
185246 def is_skill_allowed (
186247 self ,
187248 skill : Skill ,
@@ -201,17 +262,25 @@ def shape_instructions(
201262 context : ReadonlyContext ,
202263 original_instructions : str ,
203264 ) -> str :
204- # No-op pass-through for default behavior
205- return original_instructions
265+ active_taxonomies = context .state .get ("_active_taxonomies" ) or []
266+ return _interpolate_variables (original_instructions , active_taxonomies , self .registry )
267+
268+ def shape_description (
269+ self ,
270+ skill : Skill ,
271+ context : ReadonlyContext ,
272+ original_description : str ,
273+ ) -> str :
274+ active_taxonomies = context .state .get ("_active_taxonomies" ) or []
275+ return _interpolate_variables (original_description , active_taxonomies , self .registry )
206276
207277 def shape_system_instruction (
208278 self ,
209279 context : ReadonlyContext ,
210280 active_taxonomies : list [str ],
211281 original_instructions : str ,
212282 ) -> str :
213- # No-op pass-through for default behavior
214- return original_instructions
283+ return _interpolate_variables (original_instructions , active_taxonomies , self .registry )
215284
216285 def prioritize_skills (
217286 self ,
0 commit comments