Fix my tool model preset switching

This commit is contained in:
chengyongru
2026-06-18 00:03:13 +08:00
committed by Xubin Ren
parent bdf21c932b
commit ef6da0a94e
5 changed files with 84 additions and 9 deletions
+11 -3
View File
@@ -57,6 +57,9 @@ my(action="check", key="_last_usage.prompt_tokens")
my(action="check", key="model")
# → What model I'm currently running on
my(action="check", key="model_preset")
# → Which named model preset is active, or None/default
my(action="check", key="web_config.enable")
# → Whether web search is enabled
```
@@ -66,6 +69,7 @@ my(action="check", key="web_config.enable")
| Scenario | How |
|----------|-----|
| "What model are you using?" | `check("model")` |
| "Which model preset is active?" | `check("model_preset")` |
| "How many more tool calls can you make?" | `check("max_iterations")` minus `check("_current_iteration")` |
| "How many tokens has this conversation used?" | `check("_last_usage")` — cumulative across all turns |
| "Where is your working directory?" | `check("workspace")` |
@@ -82,8 +86,11 @@ Changes take effect immediately, no restart required.
my(action="set", key="max_iterations", value=80)
# → Bump iteration limit from 40 to 80
my(action="set", key="model_preset", value="fast")
# → Switch to a configured model preset
my(action="set", key="model", value="fast-model")
# → Switch to a faster model
# → Switch to a raw model and clear the active preset
my(action="set", key="context_window_tokens", value=131072)
# → Expand context window for long documents
@@ -107,6 +114,7 @@ These parameters have type and range validation — invalid values are rejected:
| `max_iterations` | int | 1100 | Max tool calls per conversation turn |
| `context_window_tokens` | int | 4,0961,000,000 | Context window size |
| `model` | str | non-empty | LLM model to use |
| `model_preset` | str | configured preset name | Named preset to use |
Other parameters (e.g. `workspace`, `provider_retry_mode`, `max_tool_result_chars`) can be set freely, as long as the value is JSON-safe.
@@ -124,8 +132,8 @@ Agent: This codebase is large, let me expand my context window to handle it.
### "Simple question, don't waste compute"
```text
Agent: This is a straightforward question, let me switch to a faster model.
→ my(action="set", key="model", value="fast-model")
Agent: This is a straightforward question, let me switch to the fast preset.
→ my(action="set", key="model_preset", value="fast")
```
### "Remember user preferences across turns"
+28 -4
View File
@@ -148,6 +148,7 @@ class MyTool(Tool, ContextAware):
"\n"
"When to use:\n"
"- User asks about your model, settings, or token usage → check that key.\n"
"- User asks to switch to a named model preset → set model_preset to that preset name.\n"
"- A tool fails or behaves unexpectedly → check the related config to diagnose.\n"
"- User asks you to remember a preference for this session → set to store it in your scratchpad.\n"
"- About to start a large task → check context_window_tokens and max_iterations first."
@@ -175,9 +176,9 @@ class MyTool(Tool, ContextAware):
"key": {
"type": "string",
"description": "Dot-path for check/set. Examples: 'max_iterations', 'workspace', 'provider_retry_mode'. "
"For check without key, shows all config values.",
"Use 'model_preset' to switch named model presets. For check without key, shows all config values.",
},
"value": {"description": "New value (for set). Type must match target (int for max_iterations/context_window_tokens, str for model)."},
"value": {"description": "New value (for set). Type must match target (int for max_iterations/context_window_tokens, str for model/model_preset)."},
},
"required": ["action"],
}
@@ -399,10 +400,28 @@ class MyTool(Tool, ContextAware):
setattr(parent, leaf, value)
self._audit("modify", f"{key} = {value!r}")
return f"Set {key} = {value!r}"
if key == "model_preset":
return self._modify_model_preset(value)
if key in self.RESTRICTED:
return self._modify_restricted(key, value)
return self._modify_free(key, value)
def _modify_model_preset(self, value: Any) -> str:
if not isinstance(value, str) or not value.strip():
return "Error: 'model_preset' must be a non-empty string"
name = value.strip()
result = self._modify_free("model_preset", name)
if result.startswith("Error:"):
return result if result.endswith((".", "!", "?")) else f"{result}."
model = getattr(self._runtime_state, "model", None)
context_window = getattr(self._runtime_state, "context_window_tokens", None)
details = [result]
if model is not None:
details.append(f"model is now {model!r}")
if context_window is not None:
details.append(f"context_window_tokens is now {context_window!r}")
return "; ".join(details)
def _modify_restricted(self, key: str, value: Any) -> str:
spec = self.RESTRICTED[key]
expected = spec["type"]
@@ -444,8 +463,9 @@ class MyTool(Tool, ContextAware):
try:
setattr(self._runtime_state, key, value)
except (ValueError, KeyError) as e:
self._audit("modify", f"REJECTED {key}: {e}")
return f"Error: {e}"
message = self._exception_message(e)
self._audit("modify", f"REJECTED {key}: {message}")
return f"Error: {message}"
self._audit("modify", f"{key}: {old!r} -> {value!r}")
return f"Set {key} = {value!r} (was {old!r})"
if callable(value):
@@ -463,6 +483,10 @@ class MyTool(Tool, ContextAware):
self._audit("modify", f"scratchpad.{key}: {old!r} -> {value!r}")
return f"Set scratchpad.{key} = {value!r}"
@staticmethod
def _exception_message(exc: Exception) -> str:
return str(exc.args[0] if isinstance(exc, KeyError) and exc.args else exc).strip('"')
@classmethod
def _validate_json_safe(cls, value: Any, depth: int = 0) -> str | None:
if depth > 10:
+3 -1
View File
@@ -36,7 +36,8 @@ always: true
| Situation | Command |
|-----------|---------|
| Large codebase analysis | `my(action="set", key="context_window_tokens", value=131072)` |
| Repetitive simple tasks | `my(action="set", key="model", value="<fast-model>")` |
| Switch to a named model preset | `my(action="set", key="model_preset", value="<preset-name>")` |
| Repetitive simple tasks without a preset | `my(action="set", key="model", value="<fast-model>")` |
| Long multi-step task | `my(action="set", key="max_iterations", value=80)` |
**Tradeoff:** Bias toward stability. Only set when defaults are genuinely insufficient.
@@ -58,6 +59,7 @@ always: true
## Constraints
- All modifications in-memory only — restart resets everything
- Prefer `model_preset` for configured model choices. Direct `model` changes clear the active preset and should only be used when no preset exists.
- Protected params have type/range validation: `max_iterations` (1100), `context_window_tokens` (40961M), `model` (non-empty str)
- If `tools.my.allow_set` is false, check only
+10 -1
View File
@@ -24,6 +24,8 @@ Concrete scenarios showing when and how to use the my tool effectively.
```
→ my(action="check", key="model")
→ 'anthropic/claude-sonnet-4-20250514'
→ my(action="check", key="model_preset")
→ 'deep'
```
## Adaptive Behavior
@@ -37,7 +39,14 @@ Concrete scenarios showing when and how to use the my tool effectively.
→ "I've expanded my context window to handle this large codebase."
```
### Switching to a faster model for repetitive tasks
### Switching to a configured model preset
```
→ my(action="set", key="model_preset", value="fast")
→ "Set model_preset = 'fast' (was 'deep'); model is now 'openai/gpt-4.1-mini'"
→ "Switched to the fast preset for these batch tasks."
```
### Switching to a raw model when no preset exists
```
→ my(action="set", key="model", value="anthropic/claude-haiku-4-5-20251001")
→ "Set model = 'anthropic/claude-haiku-4-5-20251001' (was 'anthropic/claude-sonnet-4-20250514')"
+32
View File
@@ -247,6 +247,38 @@ def test_self_tool_set_model_preset_via_modify(tmp_path) -> None:
assert loop.model == "openai/gpt-4.1"
def test_self_tool_set_model_preset_switches_back_to_default(tmp_path) -> None:
presets = {
"default": ModelPresetConfig(model="base-model", context_window_tokens=1000),
"fast": ModelPresetConfig(model="openai/gpt-4.1", context_window_tokens=32_768),
}
loop = _make_loop(tmp_path, presets=presets, active_preset="fast")
tool = MyTool(runtime_state=loop, modify_allowed=True)
result = tool._modify("model_preset", "default")
assert "Error" not in result
assert "model is now 'base-model'" in result
assert loop.model_preset == "default"
assert loop.model == "base-model"
assert loop.context_window_tokens == 1000
def test_self_tool_set_model_preset_unknown_lists_available(tmp_path) -> None:
presets = {
"default": ModelPresetConfig(model="base-model"),
"fast": ModelPresetConfig(model="openai/gpt-4.1"),
}
loop = _make_loop(tmp_path, presets=presets)
tool = MyTool(runtime_state=loop, modify_allowed=True)
result = tool._modify("model_preset", "missing")
assert result == "Error: model_preset 'missing' not found. Available: default, fast."
assert loop.model_preset is None
assert loop.model == "base-model"
def test_self_tool_set_model_clears_active_preset(tmp_path) -> None:
presets = {
"fast": ModelPresetConfig(model="openai/gpt-4.1"),