Compare commits

..

6 Commits

8 changed files with 225 additions and 59 deletions

View File

@@ -73,4 +73,10 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Scoped Inheritance:** Supports **Global** (application-wide) and **Project-Specific** presets. Project presets with the same name automatically override global counterparts, allowing for fine-tuned context tailoring. - **Scoped Inheritance:** Supports **Global** (application-wide) and **Project-Specific** presets. Project presets with the same name automatically override global counterparts, allowing for fine-tuned context tailoring.
- **Full AI Profiles:** Presets capture not only the system prompt text but also critical model parameters like **Temperature**, **Top-P**, and **Max Output Tokens**. - **Full AI Profiles:** Presets capture not only the system prompt text but also critical model parameters like **Temperature**, **Top-P**, and **Max Output Tokens**.
- **Preset Manager Modal:** A dedicated high-density GUI for creating, editing, and deleting presets with real-time validation and instant application to the active session. - **Preset Manager Modal:** A dedicated high-density GUI for creating, editing, and deleting presets with real-time validation and instant application to the active session.
- **Agent Tool Weighting & Bias:** Influences agent tool selection via a weighting system.
- **Semantic Nudging:** Automatically prefixes tool and parameter descriptions with priority tags (e.g., [HIGH PRIORITY], [PREFERRED]) to bias model selection.
- **Dynamic Tooling Strategy:** Automatically appends a Markdown "Tooling Strategy" section to system instructions based on the active preset and global bias profile.
- **Global Bias Profiles:** Application of category-level multipliers (e.g., Execution-Focused, Discovery-Heavy) to influence agent behavior across broad toolsets.
- **Priority Badges:** High-density, color-coded visual indicators in tool lists showing the assigned priority level of each capability.
- **Fine-Grained Weight Control:** Integrated sliders in the Preset Manager for adjusting individual tool weights (1-5) and parameter-level biases.

View File

@@ -33,6 +33,10 @@
- **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance. - **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance.
- **src/tool_bias.py:** Implements the `ToolBiasEngine` for semantic tool description nudging and dynamic tooling strategy generation.
- **src/tool_presets.py:** Extends `ToolPresetManager` to handle nested `Tool` models, weights, and global `BiasProfile` persistence within `tool_presets.toml`.
- **tree-sitter / AST Parsing:** For deterministic AST parsing and automated generation of curated "Skeleton Views" and "Targeted Views" (extracting specific functions and their dependencies). Features an integrated AST cache with mtime-based invalidation to minimize re-parsing overhead. - **tree-sitter / AST Parsing:** For deterministic AST parsing and automated generation of curated "Skeleton Views" and "Targeted Views" (extracting specific functions and their dependencies). Features an integrated AST cache with mtime-based invalidation to minimize re-parsing overhead.
- **pydantic / dataclasses:** For defining strict state schemas (Tracks, Tickets) used in linear orchestration. - **pydantic / dataclasses:** For defining strict state schemas (Tracks, Tickets) used in linear orchestration.

View File

@@ -18,7 +18,7 @@ This file tracks all major tracks for the project. Each track has its own detail
*Link: [./tracks/rag_support_20260308/](./tracks/rag_support_20260308/)* *Link: [./tracks/rag_support_20260308/](./tracks/rag_support_20260308/)*
*Goal: Add support for RAG (Retrieval-Augmented Generation) using local vector stores (Chroma/Qdrant), native vendor retrieval, and external RAG APIs. Implement indexing pipeline and retrieval UI.* *Goal: Add support for RAG (Retrieval-Augmented Generation) using local vector stores (Chroma/Qdrant), native vendor retrieval, and external RAG APIs. Implement indexing pipeline and retrieval UI.*
3. [ ] **Track: Agent Tool Preference & Bias Tuning** 3. [x] **Track: Agent Tool Preference & Bias Tuning**
*Link: [./tracks/tool_bias_tuning_20260308/](./tracks/tool_bias_tuning_20260308/)* *Link: [./tracks/tool_bias_tuning_20260308/](./tracks/tool_bias_tuning_20260308/)*
*Goal: Influence agent tool selection via a weighting system. Implement semantic nudges in tool descriptions and a dynamic "Tooling Strategy" section in the system prompt. Includes GUI badges and sliders for weight adjustment.* *Goal: Influence agent tool selection via a weighting system. Implement semantic nudges in tool descriptions and a dynamic "Tooling Strategy" section in the system prompt. Includes GUI badges and sliders for weight adjustment.*

View File

@@ -22,20 +22,20 @@
- [x] Verify that the strategy section is correctly appended to the system instructions. cad04bf - [x] Verify that the strategy section is correctly appended to the system instructions. cad04bf
- [x] Task: Conductor - User Manual Verification 'Phase 2: Orchestration Logic' (Protocol in workflow.md) cad04bf - [x] Task: Conductor - User Manual Verification 'Phase 2: Orchestration Logic' (Protocol in workflow.md) cad04bf
## Phase 3: GUI Integration ## Phase 3: GUI Integration [checkpoint: 1c83b3e]
- [ ] Task: Update the Tool Preset Manager UI. - [x] Task: Update the Tool Preset Manager UI. 1c83b3e
- [ ] Add `imgui.slider_int` for each tool to adjust its weight. - [x] Add `imgui.slider_int` for each tool to adjust its weight. 1c83b3e
- [ ] Add a sub-menu or modal for editing parameter-level bias. - [x] Add a sub-menu or modal for editing parameter-level bias. 1c83b3e
- [ ] Task: Enhance tool list visualization. - [x] Task: Enhance tool list visualization. 1c83b3e
- [ ] Implement color-coded priority badges in the Operations panel and tool settings. - [x] Implement color-coded priority badges in the Operations panel and tool settings. 1c83b3e
- [ ] Task: Implement the "Bias Override" in the agent focus modal. - [x] Task: Implement the "Bias Override" in the agent focus modal. 1c83b3e
- [ ] Add a dropdown to select a global bias profile or a specific preset override before spawning a worker. - [x] Add a dropdown to select a global bias profile or a specific preset override before spawning a worker. 1c83b3e
- [ ] Task: Write visual regression tests using `live_gui` to verify the new UI components. - [x] Task: Write integration tests for the new UI data flow. 1c83b3e
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration' (Protocol in workflow.md) 1c83b3e
## Phase 4: Verification & Final Polish ## Phase 4: Verification & Final Polish [checkpoint: 85ae409]
- [ ] Task: Create a Bias Efficacy Simulation. - [x] Task: Create a Bias Efficacy Simulation. 85ae409
- [ ] Implement a specialized simulation test where two tools could solve a problem, and verify the agent chooses the one with higher weight. - [x] Implement a specialized simulation test where two tools could solve a problem, and verify the agent chooses the one with higher weight. 85ae409
- [ ] Task: Final UI polish (spacing, icons, tooltips explaining the bias system). - [x] Task: Final UI polish (spacing, icons, tooltips explaining the bias system). 85ae409
- [ ] Task: Run full suite of relevant tests. - [x] Task: Run full suite of relevant tests. 85ae409
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Verification & Polish' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 4: Verification & Polish' (Protocol in workflow.md) 85ae409

View File

@@ -857,9 +857,12 @@ class AppController:
self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None) self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
self.presets = self.preset_manager.load_all() self.presets = self.preset_manager.load_all()
self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None) self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
self.tool_presets = self.tool_preset_manager.load_all() self.tool_presets = self.tool_preset_manager.load_all_presets()
self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET') self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET') or ai_cfg.get("active_tool_preset")
self.ui_active_bias_profile = ai_cfg.get("active_bias_profile")
ai_client.set_tool_preset(self.ui_active_tool_preset) ai_client.set_tool_preset(self.ui_active_tool_preset)
ai_client.set_bias_profile(self.ui_active_bias_profile)
self.ui_global_preset_name = ai_cfg.get("active_preset") self.ui_global_preset_name = ai_cfg.get("active_preset")
self.ui_project_preset_name = proj_meta.get("active_preset") self.ui_project_preset_name = proj_meta.get("active_preset")
@@ -1819,7 +1822,8 @@ class AppController:
self.preset_manager.project_root = Path(self.ui_files_base_dir) self.preset_manager.project_root = Path(self.ui_files_base_dir)
self.presets = self.preset_manager.load_all() self.presets = self.preset_manager.load_all()
self.tool_preset_manager.project_root = Path(self.ui_files_base_dir) self.tool_preset_manager.project_root = Path(self.ui_files_base_dir)
self.tool_presets = self.tool_preset_manager.load_all() self.tool_presets = self.tool_preset_manager.load_all_presets()
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
def _apply_preset(self, name: str, scope: str) -> None: def _apply_preset(self, name: str, scope: str) -> None:
if name == "None": if name == "None":
@@ -1862,11 +1866,19 @@ class AppController:
def _cb_save_tool_preset(self, name, categories, scope): def _cb_save_tool_preset(self, name, categories, scope):
preset = models.ToolPreset(name=name, categories=categories) preset = models.ToolPreset(name=name, categories=categories)
self.tool_preset_manager.save_preset(preset, scope) self.tool_preset_manager.save_preset(preset, scope)
self.tool_presets = self.tool_preset_manager.load_all() self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_delete_tool_preset(self, name, scope): def _cb_delete_tool_preset(self, name, scope):
self.tool_preset_manager.delete_preset(name, scope) self.tool_preset_manager.delete_preset(name, scope)
self.tool_presets = self.tool_preset_manager.load_all() self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"):
self.tool_preset_manager.save_bias_profile(profile, scope)
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
def _cb_delete_bias_profile(self, name: str, scope: str = "project"):
self.tool_preset_manager.delete_bias_profile(name, scope)
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
def _cb_load_track(self, track_id: str) -> None: def _cb_load_track(self, track_id: str) -> None:
state = project_manager.load_track_state(track_id, self.ui_files_base_dir) state = project_manager.load_track_state(track_id, self.ui_files_base_dir)

View File

@@ -99,10 +99,19 @@ class App:
self.show_preset_manager_modal = False self.show_preset_manager_modal = False
self.show_tool_preset_manager_modal = False self.show_tool_preset_manager_modal = False
self.ui_active_tool_preset = "" self.ui_active_tool_preset = ""
self.ui_active_bias_profile = ""
self._editing_bias_profile_name = ""
self._editing_bias_profile_tool_weights = "" # JSON
self._editing_bias_profile_cat_mults = "" # JSON
self._editing_bias_profile_scope = "project"
self._editing_tool_preset_name = '' self._editing_tool_preset_name = ''
self._editing_tool_preset_categories = {} self._editing_tool_preset_categories = {}
self._editing_tool_preset_scope = 'project' self._editing_tool_preset_scope = 'project'
self._selected_tool_preset_idx = -1 self._selected_tool_preset_idx = -1
self._editing_bias_profile_name = ""
self._editing_bias_profile_tool_weights = "{}"
self._editing_bias_profile_category_multipliers = "{}"
self._selected_bias_profile_idx = -1
self._editing_preset_name = "" self._editing_preset_name = ""
self._editing_preset_content = "" self._editing_preset_content = ""
self._editing_preset_temperature = 0.0 self._editing_preset_temperature = 0.0
@@ -1040,39 +1049,101 @@ class App:
imgui.dummy(imgui.ImVec2(0, 8)) imgui.dummy(imgui.ImVec2(0, 8))
imgui.text("Categories & Tools:") imgui.text("Categories & Tools:")
imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, -40), True) imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, 300), True)
try: try:
for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items():
if imgui.tree_node(cat_name): if imgui.tree_node(cat_name):
if cat_name not in self._editing_tool_preset_categories: if cat_name not in self._editing_tool_preset_categories:
self._editing_tool_preset_categories[cat_name] = {} self._editing_tool_preset_categories[cat_name] = []
current_cat_tools = self._editing_tool_preset_categories[cat_name] current_cat_tools = self._editing_tool_preset_categories[cat_name] # List of Tool
for tool_name in default_tools:
# Determine current mode: disabled (not present), auto, or ask
if tool_name not in current_cat_tools:
mode = "disabled"
else:
config = current_cat_tools[tool_name]
if isinstance(config, dict):
mode = config.get("mode", "auto")
else:
mode = str(config)
if imgui.radio_button(f"Disabled##{cat_name}_{tool_name}", mode == "disabled"): for tool_name in default_tools:
if tool_name in current_cat_tools: # Find existing Tool object in list
del current_cat_tools[tool_name] tool = next((t for t in current_cat_tools if t.name == tool_name), None)
mode = "disabled" if tool is None else tool.approval
if imgui.radio_button(f"Off##{cat_name}_{tool_name}", mode == "disabled"):
if tool: current_cat_tools.remove(tool)
imgui.same_line() imgui.same_line()
if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"): if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"):
current_cat_tools[tool_name] = "auto" if not tool:
tool = models.Tool(name=tool_name, approval="auto")
current_cat_tools.append(tool)
else:
tool.approval = "auto"
imgui.same_line() imgui.same_line()
if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"): if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"):
current_cat_tools[tool_name] = "ask" if not tool:
tool = models.Tool(name=tool_name, approval="ask")
current_cat_tools.append(tool)
else:
tool.approval = "ask"
imgui.same_line() imgui.same_line()
imgui.text(tool_name) imgui.text(tool_name)
if tool:
imgui.same_line(250)
imgui.set_next_item_width(100)
_, tool.weight = imgui.slider_int(f"Weight##{cat_name}_{tool_name}", tool.weight, 1, 5)
imgui.same_line()
pb_str = json.dumps(tool.parameter_bias)
imgui.set_next_item_width(150)
ch_pb, pb_new = imgui.input_text(f"Params##{cat_name}_{tool_name}", pb_str)
if ch_pb:
try: tool.parameter_bias = json.loads(pb_new)
except: pass
imgui.tree_pop() imgui.tree_pop()
finally: finally:
imgui.end_child() imgui.end_child()
imgui.separator()
imgui.text_colored(C_SUB, "Bias Profiles")
imgui.begin_child("bias_profiles_area", imgui.ImVec2(0, 200), True)
try:
avail_bias = imgui.get_content_region_avail()
imgui.begin_child("bias_list", imgui.ImVec2(200, avail_bias.y), False)
if imgui.button("New Profile", imgui.ImVec2(-1, 0)):
self._editing_bias_profile_name = ""
self._editing_bias_profile_tool_weights = "{}"
self._editing_bias_profile_category_multipliers = "{}"
self._selected_bias_profile_idx = -1
imgui.separator()
bnames = sorted(self.bias_profiles.keys())
for i, bname in enumerate(bnames):
is_sel = (self._selected_bias_profile_idx == i)
if imgui.selectable(bname, is_sel)[0]:
self._selected_bias_profile_idx = i
self._editing_bias_profile_name = bname
profile = self.bias_profiles[bname]
self._editing_bias_profile_tool_weights = json.dumps(profile.tool_weights, indent=1)
self._editing_bias_profile_category_multipliers = json.dumps(profile.category_multipliers, indent=1)
imgui.end_child()
imgui.same_line()
imgui.begin_child("bias_edit", imgui.ImVec2(0, avail_bias.y), False)
imgui.text("Name:")
_, self._editing_bias_profile_name = imgui.input_text("##b_name", self._editing_bias_profile_name)
imgui.text("Tool Weights (JSON):")
_, self._editing_bias_profile_tool_weights = imgui.input_text_multiline("##b_tw", self._editing_bias_profile_tool_weights, imgui.ImVec2(-1, 60))
imgui.text("Category Multipliers (JSON):")
_, self._editing_bias_profile_category_multipliers = imgui.input_text_multiline("##b_cm", self._editing_bias_profile_category_multipliers, imgui.ImVec2(-1, 60))
if imgui.button("Save Profile"):
try:
tw = json.loads(self._editing_bias_profile_tool_weights)
cm = json.loads(self._editing_bias_profile_category_multipliers)
prof = models.BiasProfile(name=self._editing_bias_profile_name, tool_weights=tw, category_multipliers=cm)
self.controller._cb_save_bias_profile(prof, self._editing_tool_preset_scope)
self.ai_status = f"Bias profile '{prof.name}' saved"
except Exception as e:
self.ai_status = f"Error: {e}"
imgui.same_line()
if imgui.button("Delete Profile"):
self.controller._cb_delete_bias_profile(self._editing_bias_profile_name, self._editing_tool_preset_scope)
self.ai_status = f"Bias profile deleted"
imgui.end_child()
finally:
imgui.end_child()
imgui.dummy(imgui.ImVec2(0, 8)) imgui.dummy(imgui.ImVec2(0, 8))
if imgui.button("Save", imgui.ImVec2(100, 0)): if imgui.button("Save", imgui.ImVec2(100, 0)):
if self._editing_tool_preset_name.strip(): if self._editing_tool_preset_name.strip():
@@ -1897,6 +1968,16 @@ def hello():
ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f") ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f")
ch, self.max_tokens = imgui.input_int("Max Tokens (Output)", self.max_tokens, 1024) ch, self.max_tokens = imgui.input_int("Max Tokens (Output)", self.max_tokens, 1024)
ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024)
imgui.text("Bias Profile")
if imgui.begin_combo("##bias", self.ui_active_bias_profile or "None"):
if imgui.selectable("None", not self.ui_active_bias_profile)[0]:
self.ui_active_bias_profile = ""
ai_client.set_bias_profile(None)
for bname in sorted(self.bias_profiles.keys()):
if imgui.selectable(bname, bname == self.ui_active_bias_profile)[0]:
self.ui_active_bias_profile = bname
ai_client.set_bias_profile(bname)
imgui.end_combo()
if self.current_provider == "gemini_cli": if self.current_provider == "gemini_cli":
imgui.separator() imgui.separator()
imgui.text("Gemini CLI") imgui.text("Gemini CLI")
@@ -3210,27 +3291,29 @@ def hello():
preset = presets[active_name] preset = presets[active_name]
for cat_name, tools in preset.categories.items(): for cat_name, tools in preset.categories.items():
if imgui.tree_node(cat_name): if imgui.tree_node(cat_name):
for t_name, t_cfg in tools.items(): for tool in tools:
imgui.text(t_name) if tool.weight >= 5:
imgui.same_line(150) imgui.text_colored(vec4(255, 100, 100), "[HIGH]")
imgui.same_line()
elif tool.weight == 4:
imgui.text_colored(vec4(255, 255, 100), "[PREF]")
imgui.same_line()
elif tool.weight == 2:
imgui.text_colored(vec4(255, 150, 50), "[REJECT]")
imgui.same_line()
elif tool.weight <= 1:
imgui.text_colored(vec4(180, 180, 180), "[LOW]")
imgui.same_line()
# Determine current mode imgui.text(tool.name)
if isinstance(t_cfg, dict): imgui.same_line(180)
mode = t_cfg.get("mode", "auto")
else:
mode = str(t_cfg)
if imgui.radio_button(f"Auto##{cat_name}_{t_name}", mode == "auto"): mode = tool.approval
if isinstance(t_cfg, dict): if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"):
t_cfg["mode"] = "auto" tool.approval = "auto"
else:
preset.categories[cat_name][t_name] = "auto"
imgui.same_line() imgui.same_line()
if imgui.radio_button(f"Ask##{cat_name}_{t_name}", mode == "ask"): if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"):
if isinstance(t_cfg, dict): tool.approval = "ask"
t_cfg["mode"] = "ask"
else:
preset.categories[cat_name][t_name] = "ask"
imgui.tree_pop() imgui.tree_pop()
def _render_theme_panel(self) -> None: def _render_theme_panel(self) -> None:

View File

@@ -39,13 +39,23 @@ class ToolBiasEngine:
lines = ["### Tooling Strategy"] lines = ["### Tooling Strategy"]
preferred = [] preferred = []
low_priority = []
for cat_tools in preset.categories.values(): for cat_tools in preset.categories.values():
for t in cat_tools: for t in cat_tools:
if isinstance(t, Tool) and t.weight >= 4: if not isinstance(t, Tool): continue
preferred.append(t.name) if t.weight >= 5:
preferred.append(f"{t.name} [HIGH PRIORITY]")
elif t.weight == 4:
preferred.append(f"{t.name} [PREFERRED]")
elif t.weight == 2:
low_priority.append(f"{t.name} [NOT RECOMMENDED]")
elif t.weight <= 1:
low_priority.append(f"{t.name} [LOW PRIORITY]")
if preferred: if preferred:
lines.append(f"Preferred tools: {', '.join(preferred)}.") lines.append(f"Preferred tools: {', '.join(preferred)}.")
if low_priority:
lines.append(f"Low-priority tools: {', '.join(low_priority)}.")
if global_bias.category_multipliers: if global_bias.category_multipliers:
lines.append("Category focus multipliers:") lines.append("Category focus multipliers:")

View File

@@ -0,0 +1,51 @@
import pytest
from src import ai_client
from src.models import ToolPreset, Tool, BiasProfile
from unittest.mock import MagicMock, patch
def test_bias_efficacy_prompt_generation():
# Verify that the generated prompt contains the expected nudges and strategy
preset = ToolPreset(name="BiasTest", categories={
"General": [
Tool(name="list_directory", weight=5),
Tool(name="search_files", weight=1)
]
})
bias = BiasProfile(name="Balanced", category_multipliers={"General": 1.0})
with patch("src.ai_client._active_tool_preset", preset):
with patch("src.ai_client._active_bias_profile", bias):
prompt = ai_client._get_combined_system_prompt()
# Check strategy section
assert "Tooling Strategy" in prompt
assert "list_directory" in prompt
assert "PREFERRED" in prompt or "HIGH PRIORITY" in prompt
# Check tool definitions nudging
with patch("src.ai_client._agent_tools", {"list_directory": True, "search_files": True}):
# Anthropic tools as an example
with patch("src.ai_client._CACHED_ANTHROPIC_TOOLS", None):
tools = ai_client._get_anthropic_tools()
ls_tool = next(t for t in tools if t["name"] == "list_directory")
search_tool = next(t for t in tools if t["name"] == "search_files")
assert "[HIGH PRIORITY]" in ls_tool["description"]
assert "[LOW PRIORITY]" in ls_tool["description"] or "[LOW PRIORITY]" in search_tool["description"]
def test_bias_parameter_nudging():
preset = ToolPreset(name="ParamTest", categories={
"General": [
Tool(name="read_file", parameter_bias={"path": "CRITICAL"})
]
})
with patch("src.ai_client._active_tool_preset", preset):
with patch("src.ai_client._agent_tools", {"read_file": True}):
with patch("src.ai_client._CACHED_ANTHROPIC_TOOLS", None):
tools = ai_client._get_anthropic_tools()
read_tool = next(t for t in tools if t["name"] == "read_file")
path_param_desc = read_tool["input_schema"]["properties"]["path"]["description"]
assert "[CRITICAL]" in path_param_desc