monkeybot-cli 0.8.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/PKG-INFO +2 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/pyproject.toml +2 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/skills/monkeybot/references/config-sections.md +3 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_session.py +3 -9
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tool_display.py +2 -4
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tui.py +23 -31
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/chat.py +2 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/doctor.py +64 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/loop.py +6 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/new.py +1 -3
- monkeybot_cli-0.10.0/src/monkeybot_cli/commands/trace.py +174 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/validate.py +41 -8
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/compat.py +2 -6
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/extras_catalog.py +1 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/gateway_health.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/main.py +2 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/opensandbox_lifecycle.py +4 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/output.py +3 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/providers.py +2 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/push_to_talk.py +1 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/session_controller.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +3 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/env.example +3 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +19 -29
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_continue.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_errors.py +12 -4
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_local_shell.py +3 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_renderer_parity.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_session.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_tui.py +27 -21
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_cli.py +9 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_config_resolve.py +3 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_doctor.py +52 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_realtime_session_controller.py +1 -3
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_run_cmd.py +6 -2
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_runtime_python.py +3 -1
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_terminal_markdown.py +1 -5
- monkeybot_cli-0.10.0/tests/test_trace_cmd.py +85 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/uv.lock +12 -91
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/.gitignore +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/skills/monkeybot/SKILL.md +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/__init__.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_file_index.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_renderer.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_theme.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/__init__.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/refresh.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/talk.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/config_resolve.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/exit_commands.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/process_tree.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/client.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/session.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/runtime_python.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/session_controller.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_e2e.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_file_index.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_grounding.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_status_bar.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_theme.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_chat_tool_display.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_extras_catalog.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_gateway_health.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.8.0 → monkeybot_cli-0.10.0}/tests/test_scaffold.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: monkeybot-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: CLI to create, configure, validate, and chat with monkeybot agents.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.27.0
|
|
7
7
|
Requires-Dist: monkeybot-browser-mcp<1,>=0.5.0
|
|
8
|
-
Requires-Dist: monkeybot[cli]<4,>=3.
|
|
8
|
+
Requires-Dist: monkeybot[cli]<4,>=3.9.0
|
|
9
9
|
Requires-Dist: python-dotenv>=1.0.0
|
|
10
10
|
Requires-Dist: pyyaml>=6.0.2
|
|
11
11
|
Requires-Dist: textual>=8.2.8
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "monkeybot-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "CLI to create, configure, validate, and chat with monkeybot agents."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
11
|
# `cli` pulls realtime + typer so `monkeybot talk` works out of the box (text mode).
|
|
12
12
|
# Audio mic/PTT needs the agent (or this env) to also install monkeybot[cli-realtime].
|
|
13
|
-
"monkeybot[cli]>=3.
|
|
13
|
+
"monkeybot[cli]>=3.9.0,<4",
|
|
14
14
|
"monkeybot-browser-mcp>=0.5.0,<1",
|
|
15
15
|
"httpx>=0.27.0",
|
|
16
16
|
"pyyaml>=6.0.2",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Deep reference for every `monkeybot.yaml` section. Load this only when a user needs to customize beyond Tier 1. The canonical, fully-commented template lives at `cli/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml` in the monkeybot repo (copied to `monkeybot_config/monkeybot.example.yaml` when you scaffold); this file adds the **"when would I change this?"** context the comments don't.
|
|
4
4
|
|
|
5
|
-
**Precedence:** env vars and `.env` win over YAML for `ENV_MAP` keys. `model.*` (provider, name, temperature, max_tokens, thinking_budget, context_window, summarization_model, max_turns, cache_retention) is YAML-only — leftover `MODEL_*` env vars are ignored. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var (except `model.*`).
|
|
5
|
+
**Precedence:** env vars and `.env` win over YAML for `ENV_MAP` keys. `model.*` (provider, name, temperature, max_tokens, thinking_budget, reasoning_effort, context_window, summarization_model, max_turns, cache_retention) is YAML-only — leftover `MODEL_*` env vars are ignored. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var (except `model.*`).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -42,7 +42,8 @@ YAML-only for provider, name, sampling, context window, summarization model, max
|
|
|
42
42
|
| `name` | `gemini-3-flash` | Pick a specific model id |
|
|
43
43
|
| `temperature` | `0.7` | Lower for deterministic output, higher for creative |
|
|
44
44
|
| `max_tokens` | `60000` | Cap per-response length |
|
|
45
|
-
| `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
|
|
45
|
+
| `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`). Ignored when `reasoning_effort` is set on providers that take an effort level |
|
|
46
|
+
| `reasoning_effort` | (unset) | Effort level passed through to the provider (`low`, `medium`, `high`, `xhigh`, `max`, or whatever that provider accepts). Omit for the provider default. YAML-only. Gemini only sends SDK levels (`minimal`, `low`, `medium`, `high`) and otherwise uses `thinking_budget`. A level the API rejects is omitted on later turns. Bedrock sends it for Claude ids only; Converse models ignore it. Curator and summarizer calls skip the configured effort; when one is set, OpenAI and Ollama send `none` instead |
|
|
46
47
|
| `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets. **Not** Ollama `num_ctx` |
|
|
47
48
|
| `keep_alive` | `24h` (ollama-local only) | How long local Ollama keeps the model (and KV prefix cache) loaded. YAML only. Set `"0"` to omit. See `docs/ollama-local.md` |
|
|
48
49
|
| `num_ctx` | (unset) | Optional pinned Ollama `num_ctx`. YAML only. Omit = server default. Do not copy `context_window` |
|
|
@@ -101,9 +101,7 @@ def extract_elicitation_fields(payload: dict[str, Any]) -> tuple[str | None, dic
|
|
|
101
101
|
else:
|
|
102
102
|
message = message.strip()
|
|
103
103
|
schema_raw = (
|
|
104
|
-
payload.get("requestedSchema")
|
|
105
|
-
or payload.get("requested_schema")
|
|
106
|
-
or payload.get("schema")
|
|
104
|
+
payload.get("requestedSchema") or payload.get("requested_schema") or payload.get("schema")
|
|
107
105
|
)
|
|
108
106
|
schema = dict(schema_raw) if isinstance(schema_raw, dict) else None
|
|
109
107
|
return message, schema
|
|
@@ -662,9 +660,7 @@ class ChatSessionController:
|
|
|
662
660
|
|
|
663
661
|
self._maybe_thinking_trace(evt, request_id)
|
|
664
662
|
|
|
665
|
-
def _on_context_usage_hint(
|
|
666
|
-
self, *, estimated_tokens: int, context_window_tokens: int
|
|
667
|
-
) -> None:
|
|
663
|
+
def _on_context_usage_hint(self, *, estimated_tokens: int, context_window_tokens: int) -> None:
|
|
668
664
|
self.usage.update_context_hint(
|
|
669
665
|
estimated_prompt_tokens=estimated_tokens,
|
|
670
666
|
context_window_tokens=context_window_tokens,
|
|
@@ -750,9 +746,7 @@ class ChatSessionController:
|
|
|
750
746
|
if answer.cancelled:
|
|
751
747
|
approved = False
|
|
752
748
|
reason = (
|
|
753
|
-
"cancelled by user"
|
|
754
|
-
if answer.cancelled
|
|
755
|
-
else (None if approved else "denied by user")
|
|
749
|
+
"cancelled by user" if answer.cancelled else (None if approved else "denied by user")
|
|
756
750
|
)
|
|
757
751
|
ok = await self._post_hitl(
|
|
758
752
|
f"{self.base}/sessions/{self.session_id}/tool-confirmations/{evt.tool_call_id}",
|
|
@@ -37,11 +37,9 @@ _SHELL_TAIL_LINES = 40
|
|
|
37
37
|
|
|
38
38
|
_SHELL_TOOLS = frozenset({"run_command", "execute", "shell", "bash"})
|
|
39
39
|
_READ_TOOLS = frozenset({"read_file", "read"})
|
|
40
|
-
_EDIT_TOOLS = frozenset(
|
|
41
|
-
{"write_file", "write", "edit_file", "apply_patch", "str_replace"}
|
|
42
|
-
)
|
|
40
|
+
_EDIT_TOOLS = frozenset({"write_file", "write", "edit_file", "apply_patch", "str_replace"})
|
|
43
41
|
_PATH_TOOLS = _READ_TOOLS | _EDIT_TOOLS
|
|
44
|
-
_SEARCH_TOOLS = frozenset({"grep", "
|
|
42
|
+
_SEARCH_TOOLS = frozenset({"grep", "web_search"})
|
|
45
43
|
_HINT_KEYS = frozenset(
|
|
46
44
|
{
|
|
47
45
|
"argv",
|
|
@@ -93,6 +93,7 @@ def cycle_approval_mode(mode: str) -> str:
|
|
|
93
93
|
idx = 0
|
|
94
94
|
return _APPROVAL_MODES[(idx + 1) % len(_APPROVAL_MODES)]
|
|
95
95
|
|
|
96
|
+
|
|
96
97
|
_SLASH_SPECS: tuple[tuple[str, str], ...] = (
|
|
97
98
|
("/help", "Show commands and key hints"),
|
|
98
99
|
("/new", "Start a fresh session"),
|
|
@@ -105,7 +106,7 @@ _SLASH_SPECS: tuple[tuple[str, str], ...] = (
|
|
|
105
106
|
("/timestamps", "Toggle turn timestamps"),
|
|
106
107
|
("/copy", "Copy last assistant reply"),
|
|
107
108
|
("/export", "Export transcript to a markdown file"),
|
|
108
|
-
("/export-trace", "Export
|
|
109
|
+
("/export-trace", "Export the debug trace directory (ndjson + subagents)"),
|
|
109
110
|
("/bye", "Exit chat"),
|
|
110
111
|
)
|
|
111
112
|
|
|
@@ -327,11 +328,7 @@ class ChatApp(App[int]):
|
|
|
327
328
|
self.theme = resolve_theme_name(theme_choice)
|
|
328
329
|
self.base = base
|
|
329
330
|
self.agent_root = agent_root
|
|
330
|
-
self.config_path = (
|
|
331
|
-
config_path.expanduser().resolve()
|
|
332
|
-
if config_path is not None
|
|
333
|
-
else None
|
|
334
|
-
)
|
|
331
|
+
self.config_path = config_path.expanduser().resolve() if config_path is not None else None
|
|
335
332
|
self.provider = provider
|
|
336
333
|
self.model = model
|
|
337
334
|
self.spawned_gateway = spawned_gateway
|
|
@@ -443,9 +440,7 @@ class ChatApp(App[int]):
|
|
|
443
440
|
else:
|
|
444
441
|
parts.append("y approve · n deny · Ctrl-C cancel")
|
|
445
442
|
elif self.show_hints:
|
|
446
|
-
parts.append(
|
|
447
|
-
"Enter send · Ctrl+J newline · ↑ history · / commands · Ctrl+U usage"
|
|
448
|
-
)
|
|
443
|
+
parts.append("Enter send · Ctrl+J newline · ↑ history · / commands · Ctrl+U usage")
|
|
449
444
|
else:
|
|
450
445
|
parts.append("F1 hints")
|
|
451
446
|
return " · ".join(parts)
|
|
@@ -495,8 +490,7 @@ class ChatApp(App[int]):
|
|
|
495
490
|
self._session_cost_text = f"${usage.cost_usd:.4f}"
|
|
496
491
|
if self.show_usage:
|
|
497
492
|
self._usage_text = (
|
|
498
|
-
f"in={usage.input_tokens} out={usage.output_tokens} "
|
|
499
|
-
f"${usage.cost_usd:.4f}"
|
|
493
|
+
f"in={usage.input_tokens} out={usage.output_tokens} ${usage.cost_usd:.4f}"
|
|
500
494
|
)
|
|
501
495
|
|
|
502
496
|
def _hide_empty_hint(self) -> None:
|
|
@@ -556,6 +550,7 @@ class ChatApp(App[int]):
|
|
|
556
550
|
if transcript.is_anchored and not transcript._anchor_released:
|
|
557
551
|
transcript.release_anchor()
|
|
558
552
|
self._refresh_jump_button()
|
|
553
|
+
|
|
559
554
|
def _refresh_jump_button(self) -> None:
|
|
560
555
|
with contextlib.suppress(NoMatches):
|
|
561
556
|
btn = self.query_one("#jump-bottom", Button)
|
|
@@ -627,7 +622,12 @@ class ChatApp(App[int]):
|
|
|
627
622
|
if isinstance(child, EarlierTurns):
|
|
628
623
|
earlier = child
|
|
629
624
|
continue
|
|
630
|
-
if
|
|
625
|
+
if (
|
|
626
|
+
child is self._assistant
|
|
627
|
+
or child is self._thinking
|
|
628
|
+
or child is self._thinking_trace
|
|
629
|
+
or child is self._hitl_card
|
|
630
|
+
):
|
|
631
631
|
continue
|
|
632
632
|
if isinstance(child, ToolCallBlock) and (
|
|
633
633
|
child in self._open_tools.values() or child in self._anon_tools
|
|
@@ -683,6 +683,7 @@ class ChatApp(App[int]):
|
|
|
683
683
|
def _seal_thinking_trace(self) -> None:
|
|
684
684
|
"""Finish the current thinking block and start fresh on the next delta."""
|
|
685
685
|
self._finish_thinking_trace(clear=True)
|
|
686
|
+
|
|
686
687
|
def _mount_user(self, body: str) -> None:
|
|
687
688
|
self._mount(UserTurn(body, show_timestamp=self.show_timestamps))
|
|
688
689
|
|
|
@@ -768,9 +769,7 @@ class ChatApp(App[int]):
|
|
|
768
769
|
palette.display = False
|
|
769
770
|
palette.clear_options()
|
|
770
771
|
return
|
|
771
|
-
palette.set_options(
|
|
772
|
-
[Option(f"{cmd} {desc}", id=cmd) for cmd, desc in matches]
|
|
773
|
-
)
|
|
772
|
+
palette.set_options([Option(f"{cmd} {desc}", id=cmd) for cmd, desc in matches])
|
|
774
773
|
palette.highlighted = 0
|
|
775
774
|
palette.display = True
|
|
776
775
|
|
|
@@ -945,9 +944,7 @@ class ChatApp(App[int]):
|
|
|
945
944
|
if state == "reconnecting":
|
|
946
945
|
attempt = p.get("attempt")
|
|
947
946
|
self._conn_text = (
|
|
948
|
-
f"[yellow]reconnecting…[/] ({attempt})"
|
|
949
|
-
if attempt
|
|
950
|
-
else "[yellow]reconnecting…[/]"
|
|
947
|
+
f"[yellow]reconnecting…[/] ({attempt})" if attempt else "[yellow]reconnecting…[/]"
|
|
951
948
|
)
|
|
952
949
|
elif state == "connected":
|
|
953
950
|
self._conn_text = ""
|
|
@@ -1508,7 +1505,9 @@ class ChatApp(App[int]):
|
|
|
1508
1505
|
self.model = name
|
|
1509
1506
|
self._refresh_topbar()
|
|
1510
1507
|
await self._reset_transcript_ui()
|
|
1511
|
-
self._mount_system(
|
|
1508
|
+
self._mount_system(
|
|
1509
|
+
f"Model set to {provider}/{name} — new session started (context cleared)"
|
|
1510
|
+
)
|
|
1512
1511
|
self._refresh_status()
|
|
1513
1512
|
self.query_one("#prompt", Composer).focus()
|
|
1514
1513
|
|
|
@@ -1566,6 +1565,7 @@ class ChatApp(App[int]):
|
|
|
1566
1565
|
except Exception as exc: # noqa: BLE001 - surface any YAML/parse error to the user
|
|
1567
1566
|
self._mount_system(f"Could not read config: {exc}", error=True)
|
|
1568
1567
|
return
|
|
1568
|
+
|
|
1569
1569
|
def _section(key: str) -> dict[str, object]:
|
|
1570
1570
|
value = doc.get(key)
|
|
1571
1571
|
return value if isinstance(value, dict) else {}
|
|
@@ -1686,10 +1686,10 @@ class ChatApp(App[int]):
|
|
|
1686
1686
|
)
|
|
1687
1687
|
return
|
|
1688
1688
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1689
|
-
dest = self.agent_root / "data" / f"trace_export_{ts}
|
|
1689
|
+
dest = self.agent_root / "data" / f"trace_export_{ts}"
|
|
1690
1690
|
try:
|
|
1691
1691
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1692
|
-
shutil.
|
|
1692
|
+
shutil.copytree(session_dir, dest)
|
|
1693
1693
|
except OSError as exc:
|
|
1694
1694
|
self._mount_system(f"Trace export failed: {exc}", error=True)
|
|
1695
1695
|
return
|
|
@@ -1755,7 +1755,6 @@ class ChatApp(App[int]):
|
|
|
1755
1755
|
def action_toggle_usage(self) -> None:
|
|
1756
1756
|
self._cmd_usage()
|
|
1757
1757
|
|
|
1758
|
-
|
|
1759
1758
|
def action_toggle_ptt(self) -> None:
|
|
1760
1759
|
"""Space toggles in-TUI PTT when composer is empty (realtime / SSH)."""
|
|
1761
1760
|
with contextlib.suppress(NoMatches):
|
|
@@ -1786,10 +1785,7 @@ class ChatApp(App[int]):
|
|
|
1786
1785
|
self._controller.abort_turn()
|
|
1787
1786
|
return
|
|
1788
1787
|
now = time.monotonic()
|
|
1789
|
-
if
|
|
1790
|
-
self._last_esc is not None
|
|
1791
|
-
and now - self._last_esc <= _ESC_RECALL_WINDOW
|
|
1792
|
-
):
|
|
1788
|
+
if self._last_esc is not None and now - self._last_esc <= _ESC_RECALL_WINDOW:
|
|
1793
1789
|
self._last_esc = None
|
|
1794
1790
|
composer.recall_last()
|
|
1795
1791
|
return
|
|
@@ -1815,11 +1811,7 @@ class ChatApp(App[int]):
|
|
|
1815
1811
|
self._goodbye_and_exit()
|
|
1816
1812
|
|
|
1817
1813
|
def _goodbye_and_exit(self) -> None:
|
|
1818
|
-
msg =
|
|
1819
|
-
"Goodbye — shutting down gateway…"
|
|
1820
|
-
if self.spawned_gateway
|
|
1821
|
-
else "Goodbye."
|
|
1822
|
-
)
|
|
1814
|
+
msg = "Goodbye — shutting down gateway…" if self.spawned_gateway else "Goodbye."
|
|
1823
1815
|
self._mount_system(msg)
|
|
1824
1816
|
if self._session_id:
|
|
1825
1817
|
self._mount_system("To continue this conversation, run: monkeybot chat --continue")
|
|
@@ -64,6 +64,7 @@ _UNDERLINE = "\x1b[4m"
|
|
|
64
64
|
_SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
65
65
|
_GROUNDING_SOURCES_MAX = 5
|
|
66
66
|
|
|
67
|
+
|
|
67
68
|
def _hyperlink(url: str, label: str) -> str:
|
|
68
69
|
"""OSC 8 terminal hyperlink; falls back to a plain URL on non-TTY streams."""
|
|
69
70
|
if not url:
|
|
@@ -278,9 +279,7 @@ def _resolve_base_url(args: argparse.Namespace, config_path: Path | None) -> str
|
|
|
278
279
|
return f"http://127.0.0.1:{port}"
|
|
279
280
|
|
|
280
281
|
|
|
281
|
-
def _model_banner_fields(
|
|
282
|
-
args: argparse.Namespace, config_path: Path | None
|
|
283
|
-
) -> tuple[str, str]:
|
|
282
|
+
def _model_banner_fields(args: argparse.Namespace, config_path: Path | None) -> tuple[str, str]:
|
|
284
283
|
provider = (args.model_provider or "").strip()
|
|
285
284
|
model = (args.model_name or "").strip()
|
|
286
285
|
if config_path is not None and (not provider or not model):
|
|
@@ -10,9 +10,12 @@ import tomllib
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
|
+
from monkeybot.core.config.settings import normalize_model_provider
|
|
13
14
|
from monkeybot.core.layout import AgentLayout, bootstrap_agent_layout
|
|
14
15
|
from monkeybot.core.memory.config import memory_enabled_from_config
|
|
15
16
|
from monkeybot.core.tools.sandbox_executor import SandboxConfig
|
|
17
|
+
from monkeybot.providers._bedrock_converse import uses_anthropic_bedrock
|
|
18
|
+
from monkeybot.providers.sampling import normalize_reasoning_effort
|
|
16
19
|
|
|
17
20
|
from monkeybot_cli.compat import COMPATIBLE_CORE_RANGE
|
|
18
21
|
from monkeybot_cli.config_resolve import (
|
|
@@ -228,6 +231,58 @@ def _strict_positive_int(raw: object) -> tuple[int | None, bool]:
|
|
|
228
231
|
return raw, False
|
|
229
232
|
|
|
230
233
|
|
|
234
|
+
def _add_reasoning_effort_check(
|
|
235
|
+
report: CommandReport,
|
|
236
|
+
*,
|
|
237
|
+
reasoning_effort: object,
|
|
238
|
+
provider: str = "",
|
|
239
|
+
model_name: str = "",
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Note a YAML effort value.
|
|
242
|
+
|
|
243
|
+
Severity stays ``warning``: the doctor schema has no ``info`` level, and a
|
|
244
|
+
passing check does not increment the warning summary. A non-Claude Bedrock
|
|
245
|
+
model fails this check because the Converse path does not send the field.
|
|
246
|
+
"""
|
|
247
|
+
text = normalize_reasoning_effort(reasoning_effort) or ""
|
|
248
|
+
converse = (
|
|
249
|
+
normalize_model_provider(provider) in {"aws_bedrock", "bedrock"}
|
|
250
|
+
and bool(model_name)
|
|
251
|
+
and not uses_anthropic_bedrock(model_name)
|
|
252
|
+
)
|
|
253
|
+
if text == "none":
|
|
254
|
+
message = "reasoning_effort turns thinking off where the provider supports it."
|
|
255
|
+
passed = True
|
|
256
|
+
elif text and converse:
|
|
257
|
+
message = (
|
|
258
|
+
f"reasoning_effort {text!r} is not sent for this Bedrock model. "
|
|
259
|
+
"Only Claude ids receive it; Converse models ignore it."
|
|
260
|
+
)
|
|
261
|
+
passed = False
|
|
262
|
+
elif text:
|
|
263
|
+
message = (
|
|
264
|
+
f"reasoning_effort {text!r} is sent to the provider. "
|
|
265
|
+
"It wins over thinking_budget where the provider accepts an effort level. "
|
|
266
|
+
"Gemini only sends levels its SDK knows; other values use thinking_budget. "
|
|
267
|
+
"A level the API rejects is omitted on later turns."
|
|
268
|
+
)
|
|
269
|
+
passed = True
|
|
270
|
+
else:
|
|
271
|
+
message = "reasoning_effort unset"
|
|
272
|
+
passed = True
|
|
273
|
+
check(
|
|
274
|
+
report,
|
|
275
|
+
id="model.reasoning_effort",
|
|
276
|
+
category="provider",
|
|
277
|
+
severity="warning",
|
|
278
|
+
passed=passed,
|
|
279
|
+
skip=not text,
|
|
280
|
+
message=message,
|
|
281
|
+
field="model.reasoning_effort",
|
|
282
|
+
value=text or None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
231
286
|
def _add_ollama_local_checks(
|
|
232
287
|
report: CommandReport,
|
|
233
288
|
*,
|
|
@@ -464,11 +519,19 @@ def run_doctor(args: argparse.Namespace) -> int:
|
|
|
464
519
|
skip=True,
|
|
465
520
|
)
|
|
466
521
|
|
|
522
|
+
effort_raw = model.get("reasoning_effort") if isinstance(model, dict) else None
|
|
523
|
+
model_name = str(model.get("name", "")).strip() if isinstance(model, dict) else ""
|
|
524
|
+
_add_reasoning_effort_check(
|
|
525
|
+
report,
|
|
526
|
+
reasoning_effort=effort_raw,
|
|
527
|
+
provider=provider,
|
|
528
|
+
model_name=model_name,
|
|
529
|
+
)
|
|
530
|
+
|
|
467
531
|
thinking_raw = model.get("thinking_budget") if isinstance(model, dict) else None
|
|
468
532
|
if thinking_raw is None:
|
|
469
533
|
thinking_raw = "-1"
|
|
470
534
|
num_ctx_raw = model.get("num_ctx") if isinstance(model, dict) else None
|
|
471
|
-
model_name = str(model.get("name", "")).strip() if isinstance(model, dict) else ""
|
|
472
535
|
_add_ollama_local_checks(
|
|
473
536
|
report,
|
|
474
537
|
provider=provider,
|
|
@@ -9,7 +9,12 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
-
from monkeybot_cli.config_resolve import
|
|
12
|
+
from monkeybot_cli.config_resolve import (
|
|
13
|
+
load_agent_dotenv,
|
|
14
|
+
load_config_doc,
|
|
15
|
+
resolve_agent_root,
|
|
16
|
+
resolve_config,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
DEFAULT_GATEWAY_PORT = 8000
|
|
15
20
|
|
|
@@ -144,9 +144,7 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
144
144
|
default_key=_DEFAULT_PROVIDER,
|
|
145
145
|
)
|
|
146
146
|
if model is None:
|
|
147
|
-
model = (
|
|
148
|
-
_prompt_line(f"Model name [{_DEFAULT_MODEL}]: ").strip() or _DEFAULT_MODEL
|
|
149
|
-
)
|
|
147
|
+
model = _prompt_line(f"Model name [{_DEFAULT_MODEL}]: ").strip() or _DEFAULT_MODEL
|
|
150
148
|
# Only prompt for optional deps when --with was not already given.
|
|
151
149
|
if not extras:
|
|
152
150
|
more_providers = additional_provider_extra_choices(provider)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""monkeybot trace — list and digest session transcripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from monkeybot.core.layout import resolve_workspace_root
|
|
11
|
+
from monkeybot.core.persistence.transcript import (
|
|
12
|
+
TRANSCRIPT_FILENAME,
|
|
13
|
+
resolve_session_artifact_dir,
|
|
14
|
+
)
|
|
15
|
+
from monkeybot.core.persistence.transcript_reader import (
|
|
16
|
+
build_digest,
|
|
17
|
+
digest_to_json,
|
|
18
|
+
iter_session_dirs,
|
|
19
|
+
records_by_seq,
|
|
20
|
+
render_digest_markdown,
|
|
21
|
+
scan_session_signals,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from monkeybot_cli.config_resolve import resolve_agent_root, resolve_config
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _workspace(args: argparse.Namespace) -> Path:
|
|
28
|
+
cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
|
|
29
|
+
config_path = resolve_config(args.config, cwd=cwd)
|
|
30
|
+
agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
|
|
31
|
+
return resolve_workspace_root(agent_root=agent_root, config_path=config_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _transcripts_root(workspace: Path) -> Path:
|
|
35
|
+
return workspace / ".monkeybot" / "transcripts"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve_target(raw: str | None, workspace: Path) -> Path | None:
|
|
39
|
+
"""Session directory from an id, a directory, or a transcript.ndjson path."""
|
|
40
|
+
if raw:
|
|
41
|
+
candidate = Path(raw).expanduser()
|
|
42
|
+
if candidate.is_file() and candidate.name == TRANSCRIPT_FILENAME:
|
|
43
|
+
return candidate.parent
|
|
44
|
+
if candidate.is_dir() and (candidate / TRANSCRIPT_FILENAME).is_file():
|
|
45
|
+
return candidate
|
|
46
|
+
session_dir = resolve_session_artifact_dir(workspace, raw)
|
|
47
|
+
if (session_dir / TRANSCRIPT_FILENAME).is_file():
|
|
48
|
+
return session_dir
|
|
49
|
+
return None
|
|
50
|
+
sessions = iter_session_dirs(_transcripts_root(workspace))
|
|
51
|
+
return sessions[0] if sessions else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _print_list(workspace: Path, *, as_json: bool) -> int:
|
|
55
|
+
sessions = iter_session_dirs(_transcripts_root(workspace))
|
|
56
|
+
rows = []
|
|
57
|
+
for session_dir in sessions:
|
|
58
|
+
signals = scan_session_signals(session_dir)
|
|
59
|
+
rows.append(
|
|
60
|
+
{
|
|
61
|
+
"dir": session_dir.name,
|
|
62
|
+
"path": str(session_dir),
|
|
63
|
+
"score": signals.score(),
|
|
64
|
+
"errors": sum(signals.tool_errors.values()),
|
|
65
|
+
"interventions": sum(signals.interventions.values()),
|
|
66
|
+
"steers": signals.steers,
|
|
67
|
+
"verdicts": signals.verdicts,
|
|
68
|
+
"empty": signals.empty_assistant,
|
|
69
|
+
"summaries": signals.summaries,
|
|
70
|
+
"slow": len(signals.slow_tools),
|
|
71
|
+
"repeated": len(signals.repeated_calls),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
if as_json:
|
|
75
|
+
print(json.dumps(rows, indent=2))
|
|
76
|
+
return 0
|
|
77
|
+
if not rows:
|
|
78
|
+
print(f"No transcripts under {_transcripts_root(workspace)}")
|
|
79
|
+
return 0
|
|
80
|
+
for row in rows:
|
|
81
|
+
print(
|
|
82
|
+
f"{row['dir']} score={row['score']} errors={row['errors']} "
|
|
83
|
+
f"interventions={row['interventions']} steers={row['steers']} "
|
|
84
|
+
f"verdicts={row['verdicts']} empty={row['empty']} "
|
|
85
|
+
f"summaries={row['summaries']} slow={row['slow']} repeated={row['repeated']}"
|
|
86
|
+
)
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_failure(exc: OSError) -> int:
|
|
91
|
+
print(f"Transcript read failed: {exc}", file=sys.stderr)
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_trace_list(args: argparse.Namespace) -> int:
|
|
96
|
+
try:
|
|
97
|
+
return _print_list(_workspace(args), as_json=bool(args.json))
|
|
98
|
+
except OSError as exc:
|
|
99
|
+
return _read_failure(exc)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_trace_digest(args: argparse.Namespace) -> int:
|
|
103
|
+
workspace = _workspace(args)
|
|
104
|
+
try:
|
|
105
|
+
session_dir = _resolve_target(args.target, workspace)
|
|
106
|
+
if session_dir is None:
|
|
107
|
+
target = args.target or "(latest)"
|
|
108
|
+
print(f"No transcript found for {target}", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
seqs = args.seq or []
|
|
111
|
+
if seqs:
|
|
112
|
+
found = records_by_seq(session_dir, seqs)
|
|
113
|
+
missing = False
|
|
114
|
+
for seq in seqs:
|
|
115
|
+
record = found.get(seq)
|
|
116
|
+
if record is None:
|
|
117
|
+
print(f"No record with seq {seq} in {session_dir}", file=sys.stderr)
|
|
118
|
+
missing = True
|
|
119
|
+
continue
|
|
120
|
+
print(json.dumps(record, ensure_ascii=False, indent=2, default=str))
|
|
121
|
+
return 1 if missing else 0
|
|
122
|
+
digest = build_digest(session_dir, full=bool(args.full))
|
|
123
|
+
except OSError as exc:
|
|
124
|
+
return _read_failure(exc)
|
|
125
|
+
if args.json:
|
|
126
|
+
print(json.dumps(digest_to_json(digest), ensure_ascii=False, indent=2, default=str))
|
|
127
|
+
return 0
|
|
128
|
+
print(render_digest_markdown(digest, max_chars=args.max_chars))
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
133
|
+
trace = subparsers.add_parser(
|
|
134
|
+
"trace",
|
|
135
|
+
help="List and digest session transcripts for debugging and retros",
|
|
136
|
+
)
|
|
137
|
+
sub = trace.add_subparsers(dest="trace_command", required=True)
|
|
138
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
139
|
+
common.add_argument("--cwd", help="Agent project root")
|
|
140
|
+
common.add_argument("--config", help="Path to monkeybot.yaml")
|
|
141
|
+
|
|
142
|
+
listing = sub.add_parser(
|
|
143
|
+
"list",
|
|
144
|
+
parents=[common],
|
|
145
|
+
help="List captured sessions, newest first",
|
|
146
|
+
)
|
|
147
|
+
listing.add_argument("--json", action="store_true", help="JSON output")
|
|
148
|
+
listing.set_defaults(func=run_trace_list)
|
|
149
|
+
|
|
150
|
+
digest = sub.add_parser(
|
|
151
|
+
"digest",
|
|
152
|
+
parents=[common],
|
|
153
|
+
help="Condensed timeline of one session",
|
|
154
|
+
)
|
|
155
|
+
digest.add_argument(
|
|
156
|
+
"target",
|
|
157
|
+
nargs="?",
|
|
158
|
+
help="Session id, session directory, or transcript.ndjson path (default: newest)",
|
|
159
|
+
)
|
|
160
|
+
digest.add_argument("--json", action="store_true", help="JSON output")
|
|
161
|
+
digest.add_argument("--full", action="store_true", help="Do not clip excerpts")
|
|
162
|
+
digest.add_argument(
|
|
163
|
+
"--max-chars",
|
|
164
|
+
type=int,
|
|
165
|
+
default=0,
|
|
166
|
+
help="Cap markdown length (0 = no cap)",
|
|
167
|
+
)
|
|
168
|
+
digest.add_argument(
|
|
169
|
+
"--seq",
|
|
170
|
+
type=int,
|
|
171
|
+
action="append",
|
|
172
|
+
help="Print one resolved record instead of the digest (repeatable)",
|
|
173
|
+
)
|
|
174
|
+
digest.set_defaults(func=run_trace_digest)
|
|
@@ -120,10 +120,19 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
120
120
|
category="config",
|
|
121
121
|
severity="warning",
|
|
122
122
|
passed=not missing,
|
|
123
|
-
message=f"Missing includes: {', '.join(missing)}"
|
|
123
|
+
message=f"Missing includes: {', '.join(missing)}"
|
|
124
|
+
if missing
|
|
125
|
+
else "All includes resolved",
|
|
124
126
|
)
|
|
125
127
|
else:
|
|
126
|
-
check(
|
|
128
|
+
check(
|
|
129
|
+
report,
|
|
130
|
+
id="config.includes.resolve",
|
|
131
|
+
category="config",
|
|
132
|
+
severity="warning",
|
|
133
|
+
passed=True,
|
|
134
|
+
skip=True,
|
|
135
|
+
)
|
|
127
136
|
|
|
128
137
|
model = merged.get("model") if isinstance(merged.get("model"), dict) else {}
|
|
129
138
|
provider = str(model.get("provider", "")).strip().lower() if isinstance(model, dict) else ""
|
|
@@ -134,7 +143,9 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
134
143
|
category="config",
|
|
135
144
|
severity="error",
|
|
136
145
|
passed=not provider or provider in SUPPORTED_YAML_MODEL_PROVIDERS,
|
|
137
|
-
message=f"model.provider '{provider}' is not supported"
|
|
146
|
+
message=f"model.provider '{provider}' is not supported"
|
|
147
|
+
if provider and provider not in SUPPORTED_YAML_MODEL_PROVIDERS
|
|
148
|
+
else "",
|
|
138
149
|
field="model.provider",
|
|
139
150
|
value=provider,
|
|
140
151
|
expected=sorted(SUPPORTED_YAML_MODEL_PROVIDERS),
|
|
@@ -172,12 +183,23 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
172
183
|
)
|
|
173
184
|
|
|
174
185
|
flat = _flatten_to_env(merged)
|
|
175
|
-
flat.update(
|
|
186
|
+
flat.update(
|
|
187
|
+
{
|
|
188
|
+
k: v
|
|
189
|
+
for k, v in os.environ.items()
|
|
190
|
+
if k.startswith(("GCP_", "GOOGLE_", "ANTHROPIC_VERTEX"))
|
|
191
|
+
}
|
|
192
|
+
)
|
|
176
193
|
norm_provider = normalize_model_provider(provider or "gemini")
|
|
177
194
|
needs_gcp = norm_provider == "vertex_anthropic"
|
|
178
195
|
gcp_present = any(
|
|
179
196
|
os.environ.get(k, "").strip() or flat.get(k, "").strip()
|
|
180
|
-
for k in (
|
|
197
|
+
for k in (
|
|
198
|
+
"GCP_PROJECT_ID",
|
|
199
|
+
"VERTEX_AI_PROJECT_ID",
|
|
200
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
201
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
202
|
+
)
|
|
181
203
|
)
|
|
182
204
|
if needs_gcp:
|
|
183
205
|
check(
|
|
@@ -190,11 +212,20 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
190
212
|
remediation="Set gcp.project_id in monkeybot.yaml or GCP_PROJECT_ID in .env",
|
|
191
213
|
)
|
|
192
214
|
else:
|
|
193
|
-
check(
|
|
215
|
+
check(
|
|
216
|
+
report,
|
|
217
|
+
id="gcp.project.required",
|
|
218
|
+
category="config",
|
|
219
|
+
severity="error",
|
|
220
|
+
passed=True,
|
|
221
|
+
skip=True,
|
|
222
|
+
)
|
|
194
223
|
|
|
195
224
|
try:
|
|
196
225
|
validate_monkeybot_yaml_doc(merged, env=dict(os.environ))
|
|
197
|
-
validate_provider_env(
|
|
226
|
+
validate_provider_env(
|
|
227
|
+
{**flat, "MODEL_PROVIDER": provider or flat.get("MODEL_PROVIDER", "gemini")}
|
|
228
|
+
)
|
|
198
229
|
except ConfigError as exc:
|
|
199
230
|
check(
|
|
200
231
|
report,
|
|
@@ -329,7 +360,9 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
329
360
|
category="mcp",
|
|
330
361
|
severity="warning",
|
|
331
362
|
passed=not missing_refs,
|
|
332
|
-
message=f"Unresolved MCP env refs: {', '.join(missing_refs)}"
|
|
363
|
+
message=f"Unresolved MCP env refs: {', '.join(missing_refs)}"
|
|
364
|
+
if missing_refs
|
|
365
|
+
else "All MCP env refs set",
|
|
333
366
|
)
|
|
334
367
|
if args.check_mcp:
|
|
335
368
|
for name, srv in servers.items():
|