chore(conductor): Mark track 'Saved Tool Presets' as complete

This commit is contained in:
2026-03-10 01:23:57 -04:00
parent 5f208684db
commit dcc13efaf7
24 changed files with 899 additions and 121 deletions

View File

@@ -98,6 +98,8 @@ tool_log_callback: Optional[Callable[[str, str], None]] = None
_local_storage = threading.local()
_tool_approval_modes: dict[str, str] = {}
def get_current_tier() -> Optional[str]:
"""Returns the current tier from thread-local storage."""
return getattr(_local_storage, "current_tier", None)
@@ -468,6 +470,35 @@ def set_agent_tools(tools: dict[str, bool]) -> None:
_agent_tools = tools
_CACHED_ANTHROPIC_TOOLS = None
def set_tool_preset(preset_name: Optional[str]) -> None:
"""Loads a tool preset and applies it via set_agent_tools."""
global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _tool_approval_modes
_tool_approval_modes = {}
if not preset_name or preset_name == "None":
# Enable all tools if no preset
_agent_tools = {name: True for name in mcp_client.TOOL_NAMES}
_agent_tools[TOOL_NAME] = True
else:
try:
from src.tool_presets import ToolPresetManager
manager = ToolPresetManager()
presets = manager.load_all()
if preset_name in presets:
preset = presets[preset_name]
new_tools = {name: False for name in mcp_client.TOOL_NAMES}
new_tools[TOOL_NAME] = False
for cat in preset.categories.values():
for tool_entry in cat:
if isinstance(tool_entry, dict) and "name" in tool_entry:
name = tool_entry["name"]
new_tools[name] = True
_tool_approval_modes[name] = tool_entry.get("mode", "ask")
_agent_tools = new_tools
except Exception as e:
sys.stderr.write(f"[ERROR] Failed to set tool preset '{preset_name}': {e}\n")
sys.stderr.flush()
_CACHED_ANTHROPIC_TOOLS = None
def _build_anthropic_tools() -> list[dict[str, Any]]:
mcp_tools: list[dict[str, Any]] = []
for spec in mcp_client.MCP_TOOL_SPECS:
@@ -621,22 +652,29 @@ async def _execute_single_tool_call_async(
tool_executed = False
events.emit("tool_execution", payload={"status": "started", "tool": name, "args": args, "round": r_idx})
# Check for auto approval mode
approval_mode = _tool_approval_modes.get(name, "ask")
# Check for run_powershell
if name == TOOL_NAME and pre_tool_callback:
if name == TOOL_NAME:
scr = cast(str, args.get("script", ""))
_append_comms("OUT", "tool_call", {"name": TOOL_NAME, "id": call_id, "script": scr})
# pre_tool_callback is synchronous and might block for HITL
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
if res is None:
out = "USER REJECTED: tool execution cancelled"
else:
out = res
tool_executed = True
if approval_mode == "auto":
out = await asyncio.to_thread(_run_script, scr, base_dir, qa_callback, patch_callback)
tool_executed = True
elif pre_tool_callback:
# pre_tool_callback is synchronous and might block for HITL
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
if res is None:
out = "USER REJECTED: tool execution cancelled"
else:
out = res
tool_executed = True
if not tool_executed:
if name and name in mcp_client.TOOL_NAMES:
_append_comms("OUT", "tool_call", {"name": name, "id": call_id, "args": args})
if name in mcp_client.MUTATING_TOOLS and pre_tool_callback:
if name in mcp_client.MUTATING_TOOLS and approval_mode != "auto" and pre_tool_callback:
desc = f"# MCP MUTATING TOOL: {name}\n" + "\n".join(f"# {k}: {repr(v)}" for k, v in args.items())
_res = await asyncio.to_thread(pre_tool_callback, desc, base_dir, qa_callback)
out = "USER REJECTED: tool execution cancelled" if _res is None else await mcp_client.async_dispatch(name, args)
@@ -2161,21 +2199,13 @@ def _add_bleed_derived(d: dict[str, Any], sys_tok: int = 0, tool_tok: int = 0) -
d["tool_tokens"] = tool_tok
d["history_tokens"] = max(0, cur - sys_tok - tool_tok)
return d
def _is_mutating_tool(name: str) -> bool:
"""Returns True if the tool name is considered a mutating tool."""
return name in mcp_client.MUTATING_TOOLS or name == TOOL_NAME
def _confirm_and_run(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Optional[str]:
"""
Wrapper for the confirm_and_run_callback.
This is what the providers call to trigger HITL approval.
"""
if confirm_and_run_callback:
return confirm_and_run_callback(script, base_dir, qa_callback, patch_callback)
# Fallback to direct execution if no callback registered (headless default)
from src import shell_runner
return shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
# Check for tool preset in environment variable (headless mode)
if os.environ.get("SLOP_TOOL_PRESET"):
try:
set_tool_preset(os.environ["SLOP_TOOL_PRESET"])
except Exception as _e:
sys.stderr.write(f"[DEBUG] Failed to auto-set tool preset from env: {_e}\n")
sys.stderr.flush()
def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
if _provider == "anthropic":

View File

@@ -33,6 +33,7 @@ from src import aggregate
from src import orchestrator_pm
from src import conductor_tech_lead
from src import multi_agent_conductor
from src import tool_presets
from src import theme_2 as theme
def hide_tk_root() -> Tk:
@@ -181,10 +182,10 @@ class AppController:
"last_latency": 0.0
}
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite"},
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None},
"Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None},
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
}
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
self._pending_gui_tasks: List[Dict[str, Any]] = []
@@ -225,6 +226,7 @@ class AppController:
self.ui_word_wrap: bool = True
self.ui_summary_only: bool = False
self.ui_auto_add_history: bool = False
self.ui_active_tool_preset: str | None = None
self.ui_global_system_prompt: str = ""
self.ui_agent_tools: Dict[str, bool] = {}
self.available_models: List[str] = []
@@ -305,6 +307,9 @@ class AppController:
self._editing_preset_top_p: float = 0.0
self._editing_preset_max_output_tokens: int = 4096
self._editing_preset_scope: str = "project"
self._editing_tool_preset_name: str = ""
self._editing_tool_preset_categories: Dict[str, Dict[str, Any]] = {}
self._editing_tool_preset_scope: str = "project"
self.diagnostic_log: List[Dict[str, Any]] = []
self._settable_fields: Dict[str, str] = {
'ai_input': 'ui_ai_input',
@@ -334,6 +339,7 @@ class AppController:
'project_system_prompt': 'ui_project_system_prompt',
'global_preset_name': 'ui_global_preset_name',
'project_preset_name': 'ui_project_preset_name',
'ui_active_tool_preset': 'ui_active_tool_preset',
'temperature': 'temperature',
'max_tokens': 'max_tokens',
'show_preset_manager_modal': 'show_preset_manager_modal',
@@ -343,6 +349,9 @@ class AppController:
'_editing_preset_top_p': '_editing_preset_top_p',
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
'_editing_preset_scope': '_editing_preset_scope',
'_editing_tool_preset_name': '_editing_tool_preset_name',
'_editing_tool_preset_categories': '_editing_tool_preset_categories',
'_editing_tool_preset_scope': '_editing_tool_preset_scope',
'show_windows': 'show_windows',
'ui_separate_task_dag': 'ui_separate_task_dag',
'ui_separate_usage_analytics': 'ui_separate_usage_analytics',
@@ -378,6 +387,7 @@ class AppController:
'project_system_prompt': 'ui_project_system_prompt',
'global_preset_name': 'ui_global_preset_name',
'project_preset_name': 'ui_project_preset_name',
'ui_active_tool_preset': 'ui_active_tool_preset',
'temperature': 'temperature',
'max_tokens': 'max_tokens',
'show_preset_manager_modal': 'show_preset_manager_modal',
@@ -471,6 +481,8 @@ class AppController:
'_apply_preset': self._apply_preset,
'_cb_save_preset': self._cb_save_preset,
'_cb_delete_preset': self._cb_delete_preset,
'_cb_save_tool_preset': self._cb_save_tool_preset,
'_cb_delete_tool_preset': self._cb_delete_tool_preset,
'_switch_project': self._switch_project,
'_refresh_from_project': self._refresh_from_project
}
@@ -844,6 +856,10 @@ class AppController:
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.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.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET')
ai_client.set_tool_preset(self.ui_active_tool_preset)
self.ui_global_preset_name = ai_cfg.get("active_preset")
self.ui_project_preset_name = proj_meta.get("active_preset")
@@ -1769,6 +1785,12 @@ class AppController:
# Restore MMA state
mma_sec = proj.get("mma", {})
self.ui_epic_input = mma_sec.get("epic", "")
tier_models = mma_sec.get("tier_models", {})
for tier, data in tier_models.items():
if tier in self.mma_tier_usage:
self.mma_tier_usage[tier]["model"] = data.get("model", self.mma_tier_usage[tier]["model"])
self.mma_tier_usage[tier]["provider"] = data.get("provider", self.mma_tier_usage[tier]["provider"])
self.mma_tier_usage[tier]["tool_preset"] = data.get("tool_preset", self.mma_tier_usage[tier].get("tool_preset"))
at_data = mma_sec.get("active_track")
if at_data:
try:
@@ -1796,6 +1818,8 @@ class AppController:
self.preset_manager.project_root = Path(self.ui_files_base_dir)
self.presets = self.preset_manager.load_all()
self.tool_preset_manager.project_root = Path(self.ui_files_base_dir)
self.tool_presets = self.tool_preset_manager.load_all()
def _apply_preset(self, name: str, scope: str) -> None:
if name == "None":
@@ -1835,6 +1859,15 @@ class AppController:
self.preset_manager.delete_preset(name, scope)
self.presets = self.preset_manager.load_all()
def _cb_save_tool_preset(self, name, categories, scope):
preset = models.ToolPreset(name=name, categories=categories)
self.tool_preset_manager.save_preset(preset, scope)
self.tool_presets = self.tool_preset_manager.load_all()
def _cb_delete_tool_preset(self, name, scope):
self.tool_preset_manager.delete_preset(name, scope)
self.tool_presets = self.tool_preset_manager.load_all()
def _cb_load_track(self, track_id: str) -> None:
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
if state:
@@ -2178,7 +2211,7 @@ class AppController:
# Save MMA State
mma_sec = proj.setdefault("mma", {})
mma_sec["epic"] = self.ui_epic_input
mma_sec["tier_models"] = {t: {"model": d["model"], "provider": d.get("provider", "gemini")} for t, d in self.mma_tier_usage.items()}
mma_sec["tier_models"] = {t: {"model": d["model"], "provider": d.get("provider", "gemini"), "tool_preset": d.get("tool_preset")} for t, d in self.mma_tier_usage.items()}
if self.active_track:
mma_sec["active_track"] = asdict(self.active_track)
else:

View File

@@ -6,6 +6,7 @@ import math
import json
import sys
import os
import copy
from pathlib import Path
from tkinter import filedialog, Tk
from typing import Optional, Any
@@ -96,6 +97,12 @@ class App:
self.show_windows.setdefault("Diagnostics", False)
self.controller.start_services(self)
self.show_preset_manager_modal = False
self.show_tool_preset_manager_modal = False
self.ui_active_tool_preset = ""
self._editing_tool_preset_name = ''
self._editing_tool_preset_categories = {}
self._editing_tool_preset_scope = 'project'
self._selected_tool_preset_idx = -1
self._editing_preset_name = ""
self._editing_preset_content = ""
self._editing_preset_temperature = 0.0
@@ -354,6 +361,7 @@ class App:
self._render_patch_modal()
self._render_save_preset_modal()
self._render_preset_manager_modal()
self._render_tool_preset_manager_modal()
# Auto-save (every 60s)
now = time.time()
if now - self._last_autosave >= self._autosave_interval:
@@ -422,6 +430,7 @@ class App:
self._render_provider_panel()
if imgui.collapsing_header("System Prompts"):
self._render_system_prompts_panel()
self._render_agent_tools_panel()
self._render_cache_panel()
imgui.end()
@@ -976,6 +985,118 @@ class App:
finally:
imgui.end_popup()
def _render_tool_preset_manager_modal(self) -> None:
if not self.show_tool_preset_manager_modal: return
imgui.open_popup("Tool Preset Manager")
opened, self.show_tool_preset_manager_modal = imgui.begin_popup_modal("Tool Preset Manager", self.show_tool_preset_manager_modal)
if opened:
try:
avail = imgui.get_content_region_avail()
# Left Column: Listbox
imgui.begin_child("tool_preset_list_area", imgui.ImVec2(250, avail.y), True)
try:
if imgui.button("New Tool Preset", imgui.ImVec2(-1, 0)):
self._editing_tool_preset_name = ""
self._editing_tool_preset_categories = {}
self._editing_tool_preset_scope = "project"
self._selected_tool_preset_idx = -1
if imgui.is_item_hovered():
imgui.set_tooltip("Create a new tool preset configuration.")
imgui.separator()
preset_names = sorted(self.controller.tool_presets.keys())
for i, name in enumerate(preset_names):
is_selected = (self._selected_tool_preset_idx == i)
if imgui.selectable(name, is_selected)[0]:
self._selected_tool_preset_idx = i
self._editing_tool_preset_name = name
preset = self.controller.tool_presets[name]
self._editing_tool_preset_categories = copy.deepcopy(preset.categories)
finally:
imgui.end_child()
imgui.same_line()
# Right Column: Edit Area
imgui.begin_child("tool_preset_edit_area", imgui.ImVec2(0, avail.y), False)
try:
p_name = self._editing_tool_preset_name or "(New Tool Preset)"
imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}")
imgui.separator()
imgui.dummy(imgui.ImVec2(0, 8))
imgui.text("Name:")
_, self._editing_tool_preset_name = imgui.input_text("##edit_tp_name", self._editing_tool_preset_name)
imgui.dummy(imgui.ImVec2(0, 8))
imgui.text("Scope:")
if imgui.radio_button("Global", self._editing_tool_preset_scope == "global"):
self._editing_tool_preset_scope = "global"
imgui.same_line()
if imgui.radio_button("Project", self._editing_tool_preset_scope == "project"):
self._editing_tool_preset_scope = "project"
imgui.dummy(imgui.ImVec2(0, 8))
imgui.text("Categories & Tools:")
imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, -40), True)
try:
for cat_name, tools in self._editing_tool_preset_categories.items():
if imgui.tree_node(cat_name):
for tool_name, config in tools.items():
# config can be a string ("auto", "ask") or a dict {"mode": "auto"}
if isinstance(config, dict):
mode = config.get("mode", "auto")
else:
mode = str(config)
if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"):
if isinstance(config, dict): config["mode"] = "auto"
else: tools[tool_name] = "auto"
imgui.same_line()
if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"):
if isinstance(config, dict): config["mode"] = "ask"
else: tools[tool_name] = "ask"
imgui.same_line()
imgui.text(tool_name)
imgui.tree_pop()
finally:
imgui.end_child()
imgui.dummy(imgui.ImVec2(0, 8))
if imgui.button("Save", imgui.ImVec2(100, 0)):
if self._editing_tool_preset_name.strip():
self.controller._cb_save_tool_preset(
self._editing_tool_preset_name.strip(),
self._editing_tool_preset_categories,
self._editing_tool_preset_scope
)
self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' saved"
if imgui.is_item_hovered():
imgui.set_tooltip("Save the current tool preset configuration.")
imgui.same_line()
if imgui.button("Delete", imgui.ImVec2(100, 0)):
if self._editing_tool_preset_name.strip():
self.controller._cb_delete_tool_preset(
self._editing_tool_preset_name.strip(),
self._editing_tool_preset_scope
)
self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' deleted"
self._editing_tool_preset_name = ""
self._editing_tool_preset_categories = {}
self._selected_tool_preset_idx = -1
if imgui.is_item_hovered():
imgui.set_tooltip("Delete this tool preset permanently.")
imgui.same_line()
if imgui.button("Close", imgui.ImVec2(100, 0)):
self.show_tool_preset_manager_modal = False
imgui.close_current_popup()
finally:
imgui.end_child()
finally:
imgui.end_popup()
def _render_projects_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem)
@@ -1058,12 +1179,6 @@ class App:
ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only)
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
if imgui.collapsing_header("Agent Tools"):
for t_name in models.AGENT_TOOL_NAMES:
val = self.ui_agent_tools.get(t_name, True)
ch, val = imgui.checkbox(f"Enable {t_name}", val)
if ch:
self.ui_agent_tools[t_name] = val
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
def _render_track_proposal_modal(self) -> None:
@@ -2716,7 +2831,7 @@ def hello():
imgui.push_id(f"tier_cfg_{tier}")
# Provider selection
imgui.push_item_width(100)
imgui.push_item_width(80)
if imgui.begin_combo("##prov", current_provider):
for p in PROVIDERS:
if imgui.selectable(p, p == current_provider)[0]:
@@ -2731,7 +2846,7 @@ def hello():
imgui.same_line()
# Model selection
imgui.push_item_width(-1)
imgui.push_item_width(150)
models_list = self.controller.all_available_models.get(current_provider, [])
if imgui.begin_combo("##model", current_model):
for model in models_list:
@@ -2739,6 +2854,19 @@ def hello():
self.mma_tier_usage[tier]["model"] = model
imgui.end_combo()
imgui.pop_item_width()
imgui.same_line()
# Tool Preset selection
imgui.push_item_width(-1)
current_preset = self.mma_tier_usage[tier].get("tool_preset") or "None"
preset_names = ["None"] + sorted(self.controller.tool_presets.keys())
if imgui.begin_combo("##preset", current_preset):
for preset_name in preset_names:
if imgui.selectable(preset_name, current_preset == preset_name)[0]:
self.mma_tier_usage[tier]["tool_preset"] = None if preset_name == "None" else preset_name
imgui.end_combo()
imgui.pop_item_width()
imgui.pop_id()
imgui.separator()
@@ -3042,6 +3170,58 @@ def hello():
self.show_preset_manager_modal = True
imgui.set_item_tooltip("Open preset management modal")
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
def _render_agent_tools_panel(self) -> None:
imgui.text_colored(C_LBL, 'Active Tool Preset')
presets = self.controller.tool_presets
preset_names = [""] + sorted(list(presets.keys()))
# Gracefully handle None or missing preset
active = getattr(self, "ui_active_tool_preset", "")
if active is None: active = ""
try:
idx = preset_names.index(active)
except ValueError:
idx = 0
ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names)
if ch:
self.ui_active_tool_preset = preset_names[new_idx]
imgui.same_line()
if imgui.button("Manage Presets##tools"):
self.show_tool_preset_manager_modal = True
if imgui.is_item_hovered():
imgui.set_tooltip("Configure tool availability and default modes.")
imgui.dummy(imgui.ImVec2(0, 8))
active_name = self.ui_active_tool_preset
if active_name and active_name in presets:
preset = presets[active_name]
for cat_name, tools in preset.categories.items():
if imgui.tree_node(cat_name):
for t_name, t_cfg in tools.items():
imgui.text(t_name)
imgui.same_line(150)
# Determine current mode
if isinstance(t_cfg, dict):
mode = t_cfg.get("mode", "auto")
else:
mode = str(t_cfg)
if imgui.radio_button(f"Auto##{cat_name}_{t_name}", mode == "auto"):
if isinstance(t_cfg, dict):
t_cfg["mode"] = "auto"
else:
preset.categories[cat_name][t_name] = "auto"
imgui.same_line()
if imgui.radio_button(f"Ask##{cat_name}_{t_name}", mode == "ask"):
if isinstance(t_cfg, dict):
t_cfg["mode"] = "ask"
else:
preset.categories[cat_name][t_name] = "ask"
imgui.tree_pop()
def _render_theme_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])

View File

@@ -208,6 +208,7 @@ class Track:
class WorkerContext:
ticket_id: str
model_name: str
tool_preset: Optional[str] = None
messages: List[Dict[str, Any]] = field(default_factory=list)
@@ -345,3 +346,20 @@ class Preset:
top_p=data.get("top_p"),
max_output_tokens=data.get("max_output_tokens"),
)
@dataclass
class ToolPreset:
name: str
categories: Dict[str, Dict[str, Any]]
def to_dict(self) -> Dict[str, Any]:
return {
"categories": self.categories,
}
@classmethod
def from_dict(cls, name: str, data: Dict[str, Any]) -> "ToolPreset":
return cls(
name=name,
categories=data.get("categories", {}),
)

View File

@@ -127,10 +127,10 @@ class ConductorEngine:
self.track = track
self.event_queue = event_queue
self.tier_usage = {
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview", "tool_preset": None},
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview", "tool_preset": None},
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None},
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None},
}
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
@@ -294,7 +294,8 @@ class ConductorEngine:
context = WorkerContext(
ticket_id=ticket.id,
model_name=model_name,
messages=[]
messages=[],
tool_preset=self.tier_usage["Tier 3"]["tool_preset"]
)
context_files = ticket.context_requirements if ticket.context_requirements else None
@@ -407,6 +408,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
# Enforce Context Amnesia: each ticket starts with a clean slate.
ai_client.reset_session()
ai_client.set_provider(ai_client.get_provider(), context.model_name)
ai_client.set_tool_preset(context.tool_preset)
# Check for abort BEFORE any major work
if engine and hasattr(engine, "_abort_events"):

View File

@@ -57,6 +57,13 @@ def get_global_presets_path() -> Path:
def get_project_presets_path(project_root: Path) -> Path:
return project_root / "project_presets.toml"
def get_global_tool_presets_path() -> Path:
root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml"))
def get_project_tool_presets_path(project_root: Path) -> Path:
return project_root / "project_tool_presets.toml"
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
if env_var in os.environ:
return Path(os.environ[env_var])

91
src/tool_presets.py Normal file
View File

@@ -0,0 +1,91 @@
import tomllib
import tomli_w
from pathlib import Path
from typing import Dict, List, Optional, Union
from src import paths
from src.models import ToolPreset
class ToolPresetManager:
def __init__(self, project_root: Optional[Union[str, Path]] = None):
self.project_root = Path(project_root) if project_root else None
def _load_from_path(self, path: Path) -> Dict[str, ToolPreset]:
if not path.exists():
return {}
try:
with open(path, "rb") as f:
data = tomllib.load(f)
presets = {}
for name, config in data.items():
if isinstance(config, dict):
presets[name] = ToolPreset.from_dict(name, config)
return presets
except Exception:
return {}
def load_all(self) -> Dict[str, ToolPreset]:
"""
Merges global and project presets.
Project presets override global ones if they have the same name.
"""
presets = self._load_from_path(paths.get_global_tool_presets_path())
if self.project_root:
project_presets = self._load_from_path(paths.get_project_tool_presets_path(self.project_root))
presets.update(project_presets)
return presets
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
"""
Saves a preset to either 'global' or 'project' scope.
Scope must be 'global' or 'project'.
"""
if scope == "global":
path = paths.get_global_tool_presets_path()
elif scope == "project":
if not self.project_root:
raise ValueError("Project root not set for project scope saving.")
path = paths.get_project_tool_presets_path(self.project_root)
else:
raise ValueError(f"Invalid scope: {scope}")
data = {}
if path.exists():
try:
with open(path, "rb") as f:
data = tomllib.load(f)
except Exception:
data = {}
data[preset.name] = preset.to_dict()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f:
tomli_w.dump(data, f)
def delete_preset(self, name: str, scope: str = "project") -> None:
"""
Deletes a preset from the specified scope.
Scope must be 'global' or 'project'.
"""
if scope == "global":
path = paths.get_global_tool_presets_path()
elif scope == "project":
if not self.project_root:
raise ValueError("Project root not set for project scope deletion.")
path = paths.get_project_tool_presets_path(self.project_root)
else:
raise ValueError(f"Invalid scope: {scope}")
if not path.exists():
return
try:
with open(path, "rb") as f:
data = tomllib.load(f)
except Exception:
return
if name in data:
del data[name]
with open(path, "wb") as f:
tomli_w.dump(data, f)