monkeybot-cli 0.5.1__tar.gz → 0.6.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.
Files changed (88) hide show
  1. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/PKG-INFO +3 -3
  2. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/pyproject.toml +3 -3
  3. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/skills/monkeybot/SKILL.md +3 -3
  4. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/skills/monkeybot/references/config-sections.md +8 -20
  5. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_session.py +3 -3
  6. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_tui.py +5 -5
  7. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/chat.py +2 -3
  8. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/doctor.py +153 -0
  9. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/validate.py +2 -13
  10. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -1
  11. monkeybot_cli-0.6.0/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +41 -0
  12. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +14 -8
  13. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_e2e.py +5 -1
  14. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_session.py +2 -2
  15. monkeybot_cli-0.6.0/tests/test_doctor.py +217 -0
  16. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_gateway_health.py +2 -2
  17. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/uv.lock +11 -3
  18. monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -16
  19. monkeybot_cli-0.5.1/tests/test_doctor.py +0 -85
  20. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/.gitignore +0 -0
  21. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/__init__.py +0 -0
  22. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_file_index.py +0 -0
  23. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
  24. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  25. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
  26. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_theme.py +0 -0
  27. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
  28. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
  29. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  30. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/loop.py +0 -0
  31. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/new.py +0 -0
  32. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/refresh.py +0 -0
  33. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
  34. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/commands/talk.py +0 -0
  35. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/compat.py +0 -0
  36. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/config_resolve.py +0 -0
  37. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/exit_commands.py +0 -0
  38. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/extras_catalog.py +0 -0
  39. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/gateway_health.py +0 -0
  40. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/main.py +0 -0
  41. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
  42. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/output.py +0 -0
  43. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/process_tree.py +0 -0
  44. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/providers.py +0 -0
  45. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  46. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  47. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/client.py +0 -0
  48. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  49. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  50. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/session.py +0 -0
  51. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
  52. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  53. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/runtime_python.py +0 -0
  54. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold.py +0 -0
  55. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  56. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
  57. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  58. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
  59. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
  60. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/env.example +0 -0
  61. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
  62. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
  63. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
  64. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
  65. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
  66. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  67. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  68. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/session_controller.py +0 -0
  69. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  70. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_continue.py +0 -0
  71. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_errors.py +0 -0
  72. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_file_index.py +0 -0
  73. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_grounding.py +0 -0
  74. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_local_shell.py +0 -0
  75. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_renderer_parity.py +0 -0
  76. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_status_bar.py +0 -0
  77. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_theme.py +0 -0
  78. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_tool_display.py +0 -0
  79. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_chat_tui.py +0 -0
  80. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_cli.py +0 -0
  81. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_config_resolve.py +0 -0
  82. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_extras_catalog.py +0 -0
  83. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_opensandbox_lifecycle.py +0 -0
  84. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_realtime_session_controller.py +0 -0
  85. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_run_cmd.py +0 -0
  86. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_runtime_python.py +0 -0
  87. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_scaffold.py +0 -0
  88. {monkeybot_cli-0.5.1 → monkeybot_cli-0.6.0}/tests/test_terminal_markdown.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.5.1
3
+ Version: 0.6.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
- Requires-Dist: monkeybot-browser-mcp<1,>=0.2.0
8
- Requires-Dist: monkeybot[cli]<4,>=3.0.0
7
+ Requires-Dist: monkeybot-browser-mcp<1,>=0.5.0
8
+ Requires-Dist: monkeybot[cli]<4,>=3.1.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,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeybot-cli"
7
- version = "0.5.1"
7
+ version = "0.6.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.0.0,<4",
14
- "monkeybot-browser-mcp>=0.2.0,<1",
13
+ "monkeybot[cli]>=3.1.0,<4",
14
+ "monkeybot-browser-mcp>=0.5.0,<1",
15
15
  "httpx>=0.27.0",
16
16
  "pyyaml>=6.0.2",
17
17
  "python-dotenv>=1.0.0",
@@ -56,7 +56,7 @@ npx skills add human-plus-machine/monkeybot --skill monkeybot
56
56
  - `monkeybot_config/monkeybot.yaml` — non-secret settings (model, paths, gateway, behavior).
57
57
  - `.env` — secrets and machine-local paths (API keys, GCP project, DB URL).
58
58
  - sidecars: `monkeybot_config/AGENT.md` (system prompt), `mcp.json` (MCP servers), `command_allowlist.yaml`.
59
- - **Precedence — important:** environment variables and `.env` win over `monkeybot.yaml`. If a user edits YAML but nothing changes, suspect a stale `.env` shadowing it (the YAML→env mapping lives in `runtime_env.py:ENV_MAP`).
59
+ - **Precedence — important:** environment variables and `.env` win over `monkeybot.yaml` except `model.*` (provider, name, sampling, context window, summarization model, max turns, cache retention), which is YAML-only. If a user edits YAML but nothing changes, suspect a stale `.env` shadowing it (the YAML→env mapping lives in `runtime_env.py:ENV_MAP`).
60
60
  - **Defaults are fine on day one.** Most first-time users only touch `model`, `.env` credentials, and `AGENT.md`.
61
61
 
62
62
  ## Running CLI commands
@@ -112,7 +112,7 @@ uv sync
112
112
  | `aws_bedrock` | `AWS_ACCESS_KEY_ID` / `AWS_PROFILE` + `AWS_REGION` | `monkeybot[bedrock]` |
113
113
  | `huggingface` | `HF_TOKEN` (or `HUGGINGFACE_API_KEY`) | `monkeybot[huggingface]` |
114
114
  | `ollama-cloud` | `OLLAMA_API_KEY` | `monkeybot[ollama]` |
115
- | `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`) | `monkeybot[ollama]` |
115
+ | `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`). Requests send `keep_alive` 24h so prefix KV cache survives idle. | `monkeybot[ollama]` |
116
116
  | `ollama` | Legacy auto-route (key + blank URL = cloud; explicit URL wins) | `monkeybot[ollama]` |
117
117
 
118
118
  **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway uses the CLI's interpreter when it already has MonkeyBot 3.x (and MemPalace if memory is on). If memory is enabled and that interpreter cannot import MemPalace, `run` / `chat` provision a cached CLI-managed venv under `~/.cache/monkeybot/runtimes/` holding `monkeybot[memory]` pinned to the running core (never rewrites a `pyproject.toml`). `doctor` reuses that cache when it is already present. Otherwise extras must be installed in the CLI env (`uv tool install --with 'monkeybot[<extra>]' monkeybot-cli`).
@@ -160,7 +160,7 @@ Decision → config map:
160
160
  | "Run untrusted code" | `sandbox.enabled` + `SANDBOX_API_KEY` |
161
161
  | "Use specialist agents" | `subagents.personas` + `monkeybot_config/agents/*.md` |
162
162
  | "Connect external tools" | `mcp.json` (`mcpServers` object), then `validate --check-mcp` |
163
- | "Control cost / context size" | `model.*`, `context_curation.*` |
163
+ | "Control cost / context size" | `model.*` |
164
164
  | "Restrict dangerous commands" | `tools.denied_patterns`, `command_allowlist.yaml` |
165
165
  | "Multiple environments" | top-level `includes:` fragments |
166
166
 
@@ -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. 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.
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.*`).
6
6
 
7
7
  ---
8
8
 
@@ -34,6 +34,8 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
34
34
 
35
35
  ## `model`
36
36
 
37
+ YAML-only for provider, name, sampling, context window, summarization model, max turns, and cache retention. Leftover `MODEL_*` / `MAX_TURNS` / `CONTEXT_SUMMARIZATION_MODEL` env vars are ignored.
38
+
37
39
  | Field | Default | When to change |
38
40
  |---|---|---|
39
41
  | `provider` | `gemini` | Switch LLM vendor (see provider table in SKILL.md) |
@@ -41,9 +43,11 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
41
43
  | `temperature` | `0.7` | Lower for deterministic output, higher for creative |
42
44
  | `max_tokens` | `60000` | Cap per-response length |
43
45
  | `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
44
- | `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets |
46
+ | `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets. **Not** Ollama `num_ctx` |
47
+ | `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
+ | `num_ctx` | (unset) | Optional pinned Ollama `num_ctx`. YAML only. Omit = server default. Do not copy `context_window` |
45
49
  | `max_turns` | `1000` | Hard cap on turns per run |
46
- | `summarization_model` | (main model) | Cheaper model for history summarization (env `CONTEXT_SUMMARIZATION_MODEL`) |
50
+ | `summarization_model` | (main model) | Cheaper model for history summarization (YAML-only)
47
51
 
48
52
  Validate check ids: `model.provider.supported`, `model.name.present`. Supported YAML providers: `gemini`/`vertex`, `openai`, `anthropic`, `vertex-claude`, `huggingface`, `ollama-cloud`, `ollama-local`, `ollama`, `aws_bedrock`, `fake`.
49
53
 
@@ -71,22 +75,6 @@ Required when `memory_storage_uri` is `gcs://…` or `provider: vertex-claude` (
71
75
  | `graceful_shutdown_timeout_sec` | `5` | Allow longer drain on shutdown |
72
76
  | `cors_allow_origins` | `http://localhost:5173` | **Custom web UI** — set its origin, or `"*"` for any |
73
77
 
74
- ## `context_curation`
75
-
76
- Trims memory injected into context. `enabled: true` by default.
77
-
78
- Recent window by default; LLM curator only when the index is token-heavy. On curator failure, falls back to the window.
79
-
80
- | Field | Default | Notes |
81
- |---|---|---|
82
- | `memory_window_lines` | `12` | Recent index lines injected; also caps curator-selected lines |
83
- | `memory_index_cap` | `200` | Organizer keeps this many INDEX.md entries; older rows move to `INDEX.archive.md` |
84
- | `memory_token_threshold` | `2000` | Call curator when estimated index tokens exceed this |
85
- | `curator_model` | `gemini-3-flash` | Separate small model; empty = main model |
86
- | `timeout_sec` | `10` | Curator call timeout |
87
-
88
- When the prompt shows fewer entries than exist, a structural confidence score triggers a `search_memory` nudge. Skill names are always shown in full in the prompt; use `list_skills` to get the skills root path.
89
-
90
78
  ## `memory`
91
79
 
92
80
  MemPalace capture, wake-up, and search teaching are on by default. Turn them off with `memory.enabled: false` or `MONKEYBOT_MEMORY_HOOK_ENABLED=0`. The vector stack is the optional `monkeybot[memory]` extra — omit it (and set `enabled: false`) when you do not want chromadb/onnxruntime.
@@ -176,4 +164,4 @@ Validate check: `config.includes.resolve`.
176
164
 
177
165
  ## `fake_provider`
178
166
 
179
- Test-only. `events_json` feeds `MODEL_PROVIDER=fake` scripted runs (env `MONKEYBOT_FAKE_PROVIDER_EVENTS`). Not for production.
167
+ Test-only. `events_json` feeds `model.provider: fake` scripted runs (env `MONKEYBOT_FAKE_PROVIDER_EVENTS`). Not for production.
@@ -229,7 +229,7 @@ class ChatSessionController:
229
229
  self.usage = UsageStore()
230
230
  self.session_id: str | None = None
231
231
  self.stream_error = False
232
- self.transcript_report_dir: str | None = None
232
+ self.transcript_dir: str | None = None
233
233
  self._client: httpx.AsyncClient | None = None
234
234
  self._event_queue: asyncio.Queue[str | None] = asyncio.Queue()
235
235
  self._stream_task: asyncio.Task[None] | None = None
@@ -924,9 +924,9 @@ class ChatSessionController:
924
924
  resp = await self._client.delete(f"{self.base}/sessions/{self.session_id}")
925
925
  if resp.status_code == 200:
926
926
  data = resp.json()
927
- report = data.get("transcript_report_dir") if isinstance(data, dict) else None
927
+ report = data.get("transcript_dir") if isinstance(data, dict) else None
928
928
  if isinstance(report, str) and report.strip():
929
- self.transcript_report_dir = report.strip()
929
+ self.transcript_dir = report.strip()
930
930
  except Exception:
931
931
  logger.warning(
932
932
  "session DELETE on close failed session_id=%s",
@@ -1680,8 +1680,8 @@ class ChatApp(App[int]):
1680
1680
  src = session_dir / "transcript.ndjson"
1681
1681
  if not src.is_file():
1682
1682
  self._mount_system(
1683
- "No trace file found — enable transcript capture on the gateway and restart "
1684
- "(external gateways need MONKEYBOT_TRANSCRIPT_ENABLED=1)",
1683
+ "No trace file found — set runtime.transcript_enabled: true in monkeybot.yaml "
1684
+ "and restart the gateway",
1685
1685
  error=True,
1686
1686
  )
1687
1687
  return
@@ -1829,10 +1829,10 @@ class ChatApp(App[int]):
1829
1829
  async def _close_session_and_exit(self) -> None:
1830
1830
  # Must not be named `_shutdown` — that shadows Textual.App._shutdown.
1831
1831
  await self._controller.close()
1832
- report_dir = getattr(self._controller, "transcript_report_dir", None)
1832
+ report_dir = getattr(self._controller, "transcript_dir", None)
1833
1833
  if isinstance(report_dir, str) and report_dir:
1834
- self._mount_system(f"Transcript report → {report_dir}")
1835
- print(f"Transcript report → {report_dir}", flush=True)
1834
+ self._mount_system(f"Transcript → {report_dir}")
1835
+ print(f"Transcript → {report_dir}", flush=True)
1836
1836
  self._exit_code = 1 if self._controller.stream_error else self._exit_code
1837
1837
  self.exit(self._exit_code)
1838
1838
 
@@ -626,9 +626,9 @@ async def _plain_chat_session(
626
626
  await controller.submit(user_line)
627
627
  finally:
628
628
  await controller.close()
629
- if controller.transcript_report_dir:
629
+ if controller.transcript_dir:
630
630
  print(
631
- f"{_DIM}Transcript report → {controller.transcript_report_dir}{_RESET}",
631
+ f"{_DIM}Transcript → {controller.transcript_dir}{_RESET}",
632
632
  flush=True,
633
633
  )
634
634
  await renderer.stop_io_worker()
@@ -678,7 +678,6 @@ def _spawn_gateway(config_path: Path | None, agent_root: Path, port: int) -> _Sp
678
678
  env["MONKEYBOT_CONFIG"] = str(config_path)
679
679
  env["PORT"] = str(port)
680
680
  env.setdefault("LOG_LEVEL", "error")
681
- env.setdefault("MONKEYBOT_TRANSCRIPT_ENABLED", "1")
682
681
  log_file = tempfile.NamedTemporaryFile(
683
682
  mode="w+",
684
683
  prefix="monkeybot-gateway-",
@@ -192,6 +192,146 @@ def _add_layout_checks(report: CommandReport, layout: AgentLayout) -> None:
192
192
  )
193
193
 
194
194
 
195
+ _OLLAMA_LOCAL_DOCS = "docs/ollama-local.md"
196
+ _OLLAMA_NUM_CTX_LARGE = 32_768
197
+ _OLLAMA_REASONING_TAGS = ("qwen3", "gemma4", "gemma-4", "deepseek-r1", "qwq", "magistral")
198
+
199
+
200
+ def _is_local_ollama_provider(provider: str) -> bool:
201
+ """True for ollama-local and legacy ``ollama`` (not ollama-cloud)."""
202
+ key = provider.strip().lower().replace("_", "-")
203
+ return key in {"ollama-local", "ollama"}
204
+
205
+
206
+ def _looks_like_reasoning_model(model_name: str) -> bool:
207
+ name = model_name.lower()
208
+ return any(tag in name for tag in _OLLAMA_REASONING_TAGS)
209
+
210
+
211
+ def _parse_int(raw: object) -> int | None:
212
+ if raw is None or raw == "":
213
+ return None
214
+ try:
215
+ return int(raw)
216
+ except (TypeError, ValueError):
217
+ return None
218
+
219
+
220
+ def _strict_positive_int(raw: object) -> tuple[int | None, bool]:
221
+ """Return ``(value, invalid)``. Unset is ``(None, False)``; garbage or ``<1`` is invalid."""
222
+ if raw is None or raw == "":
223
+ return None, False
224
+ if isinstance(raw, bool) or not isinstance(raw, int):
225
+ return None, True
226
+ if raw < 1:
227
+ return None, True
228
+ return raw, False
229
+
230
+
231
+ def _add_ollama_local_checks(
232
+ report: CommandReport,
233
+ *,
234
+ provider: str,
235
+ model_name: str,
236
+ thinking_budget: object,
237
+ num_ctx: object,
238
+ ) -> None:
239
+ """Warn on local-Ollama prefix-cache traps. Always emit the four check ids."""
240
+ local = _is_local_ollama_provider(provider)
241
+ mlx = "-mlx" in model_name.lower()
242
+ check(
243
+ report,
244
+ id="ollama.local.mlx_runner",
245
+ category="provider",
246
+ severity="warning",
247
+ passed=not mlx,
248
+ skip=not local,
249
+ message=(
250
+ f"model.name {model_name!r} looks like an MLX pack; prefix KV cache "
251
+ "often does not reuse between agent steps"
252
+ if mlx
253
+ else "model.name is not an MLX pack"
254
+ ),
255
+ field="model.name",
256
+ value=model_name or None,
257
+ remediation=(
258
+ f"Prefer a GGUF tag for tool-calling loops. See {_OLLAMA_LOCAL_DOCS}." if mlx else None
259
+ ),
260
+ docs=_OLLAMA_LOCAL_DOCS,
261
+ )
262
+ budget = _parse_int(thinking_budget)
263
+ if budget is None:
264
+ budget = -1
265
+ reasoning = _looks_like_reasoning_model(model_name)
266
+ thinking_on_default = budget == -1
267
+ check(
268
+ report,
269
+ id="ollama.local.thinking_default",
270
+ category="provider",
271
+ severity="warning",
272
+ passed=not thinking_on_default,
273
+ skip=not local or not reasoning,
274
+ message=(
275
+ "thinking_budget is -1 (server default); reasoning models generate "
276
+ "thinking tokens before the first visible reply"
277
+ if thinking_on_default
278
+ else f"thinking_budget is {budget}"
279
+ ),
280
+ field="model.thinking_budget",
281
+ value=budget,
282
+ remediation=(
283
+ f"Set thinking_budget: 0 to send reasoning_effort: none. See {_OLLAMA_LOCAL_DOCS}."
284
+ if thinking_on_default
285
+ else None
286
+ ),
287
+ docs=_OLLAMA_LOCAL_DOCS,
288
+ )
289
+ ctx, ctx_invalid = _strict_positive_int(num_ctx)
290
+ check(
291
+ report,
292
+ id="ollama.local.num_ctx_invalid",
293
+ category="provider",
294
+ severity="error",
295
+ passed=not ctx_invalid,
296
+ skip=not local or (ctx is None and not ctx_invalid),
297
+ message=(
298
+ f"num_ctx must be a positive integer, got {num_ctx!r}"
299
+ if ctx_invalid
300
+ else (f"num_ctx is {ctx}" if ctx is not None else "num_ctx is unset")
301
+ ),
302
+ field="model.num_ctx",
303
+ value=num_ctx,
304
+ remediation=(
305
+ f"Set a positive integer num_ctx (e.g. 8192), or omit it. See {_OLLAMA_LOCAL_DOCS}."
306
+ if ctx_invalid
307
+ else None
308
+ ),
309
+ docs=_OLLAMA_LOCAL_DOCS,
310
+ )
311
+ large = ctx is not None and ctx > _OLLAMA_NUM_CTX_LARGE
312
+ check(
313
+ report,
314
+ id="ollama.local.num_ctx_large",
315
+ category="provider",
316
+ severity="warning",
317
+ passed=not large,
318
+ skip=not local or ctx is None or ctx_invalid,
319
+ message=(
320
+ f"num_ctx {ctx} is large for a local runner; prefill will be slow"
321
+ if large
322
+ else (f"num_ctx is {ctx}" if ctx is not None else "num_ctx is unset")
323
+ ),
324
+ field="model.num_ctx",
325
+ value=ctx,
326
+ remediation=(
327
+ f"Pin a modest num_ctx (e.g. 8192). Do not copy context_window. See {_OLLAMA_LOCAL_DOCS}."
328
+ if large
329
+ else None
330
+ ),
331
+ docs=_OLLAMA_LOCAL_DOCS,
332
+ )
333
+
334
+
195
335
  def run_doctor(args: argparse.Namespace) -> int:
196
336
  cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
197
337
  config_path = resolve_config(args.config, cwd=cwd)
@@ -324,6 +464,19 @@ def run_doctor(args: argparse.Namespace) -> int:
324
464
  skip=True,
325
465
  )
326
466
 
467
+ thinking_raw = model.get("thinking_budget") if isinstance(model, dict) else None
468
+ if thinking_raw is None:
469
+ thinking_raw = "-1"
470
+ 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
+ _add_ollama_local_checks(
473
+ report,
474
+ provider=provider,
475
+ model_name=model_name,
476
+ thinking_budget=thinking_raw,
477
+ num_ctx=num_ctx_raw,
478
+ )
479
+
327
480
  runtime_cfg = doc.get("runtime") if isinstance(doc.get("runtime"), dict) else {}
328
481
  port = int(runtime_cfg.get("port", 8080)) if isinstance(runtime_cfg, dict) else 8080
329
482
  free = _port_free(port)
@@ -17,7 +17,7 @@ from monkeybot.core.config import (
17
17
  validate_monkeybot_yaml_doc,
18
18
  validate_provider_env,
19
19
  )
20
- from monkeybot.core.config.runtime_env import ENV_MAP
20
+ from monkeybot.core.config.runtime_env import _flatten_config
21
21
  from monkeybot.core.config.settings import ConfigError, normalize_model_provider
22
22
  from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
23
23
  from monkeybot.core.layout import resolve_agent_root
@@ -35,18 +35,7 @@ def _resolve_path(base: Path, rel: str) -> Path:
35
35
 
36
36
 
37
37
  def _flatten_to_env(doc: dict[str, Any]) -> dict[str, str]:
38
- out: dict[str, str] = {}
39
- for (section, key), env_name in ENV_MAP.items():
40
- sec = doc.get(section)
41
- if not isinstance(sec, dict) or key not in sec:
42
- continue
43
- raw = sec[key]
44
- if raw is None:
45
- continue
46
- if isinstance(raw, bool):
47
- out[env_name] = "true" if raw else "false"
48
- else:
49
- out[env_name] = str(raw)
38
+ out = _flatten_config(doc)
50
39
  gcp = doc.get("gcp") if isinstance(doc.get("gcp"), dict) else {}
51
40
  if isinstance(gcp, dict) and gcp.get("project_id"):
52
41
  out["GCP_PROJECT_ID"] = str(gcp["project_id"])
@@ -78,7 +78,6 @@ def _spawn_combined_gateway(
78
78
  env["MONKEYBOT_CONFIG"] = str(config_path)
79
79
  env["PORT"] = str(port)
80
80
  env.setdefault("LOG_LEVEL", "error")
81
- env.setdefault("MONKEYBOT_TRANSCRIPT_ENABLED", "1")
82
81
  log_file = tempfile.NamedTemporaryFile(
83
82
  mode="w+",
84
83
  prefix="monkeybot-gateway-",
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: browser
3
+ description: Control a real browser through the optional bundled browser MCP server.
4
+ ---
5
+
6
+ # Browser
7
+
8
+ When browser tools are needed, call `enable_mcp("browser")` first. Use the
9
+ name from the harness MCP catalog — do not read config files. Progressive
10
+ disclosure still requires `enable_mcp` before `browser__*` schemas are advertised.
11
+
12
+ Use indexed DOM tools before screenshots. Prefer `browser_act` with
13
+ `do: fill_form` for multi-field forms, `browser_click_text` when the visible
14
+ label is known, `browser_act` for a batch of steps, and `browser_extract` for
15
+ structured scraping. Browser screenshots (JPEG under
16
+ `browser/Screenshots/`) and agent-written playbooks are workspace data:
17
+ `browser/Screenshots/` and `browser/playbooks/`.
18
+ They are not trusted skills and may be discarded with an ephemeral workspace.
19
+
20
+ If `browser_list_playbooks` returns `flows`, call `browser_run_playbook` instead of
21
+ re-planning. On `failed_step`, continue by hand and append a corrected
22
+ `playbook` YAML fence. Secrets only via `{do: login, expected_origin}`.
23
+
24
+ `browser_input_by_index` fills in-page by default; pass `mode="keys"` for
25
+ comboboxes and fields that only listen to keydown. `browser_get_elements`
26
+ is viewport-first; indices remain valid until navigation. Actions return
27
+ an `observation` (diff by default) — read that instead of calling
28
+ `get_elements` again. `browser_goto` returns a full observation.
29
+ Use `browser_get_text` to read page copy.
30
+
31
+ When the user asked to sign in on the Spaces in-app browser, call
32
+ `browser_login` instead of typing a password. It returns `{ok, loggedIn, origin}`
33
+ only. Pass `expected_origin` so a login cannot land on the wrong site, and check
34
+ the returned `origin` before reporting success.
35
+
36
+ Tabs use aliases (`t1`, `t2`, …). Reads (`get_elements`, `page_info`, `js`,
37
+ `read_tabs`) never move focus; actions do. At most five agent-controlled tabs —
38
+ if you hit the cap, ask the user which to close, then `browser_close_tab`. Close
39
+ tabs you opened. `browser_login` still targets the tab the user has focused.
40
+
41
+ Finish browser sessions with `browser_stop`.
@@ -2,7 +2,9 @@
2
2
  #
3
3
  # cp monkeybot_config/monkeybot.example.yaml monkeybot_config/monkeybot.yaml
4
4
  #
5
- # Precedence: environment variables (and optional root `.env`) win over values here.
5
+ # Precedence: environment variables (and optional root `.env`) win over values here
6
+ # except ``model.*`` (provider, name, temperature, max_tokens, thinking_budget,
7
+ # context_window, summarization_model, max_turns, cache_retention) which are YAML-only.
6
8
  #
7
9
  # Secrets are not committed here. Create a `.env` in the repo root when you need keys or
8
10
  # machine-local paths, for example:
@@ -29,7 +31,7 @@ runtime:
29
31
  # Internal debugging only (not agent-visible): write every AgentEvent and raw
30
32
  # provider request/response for each session to
31
33
  # {workspace_root}/.monkeybot/transcripts/{UTC}_{session_id}/transcript.ndjson.
32
- # (env: MONKEYBOT_TRANSCRIPT_ENABLED)
34
+ # YAML-only (not overridable via environment).
33
35
  transcript_enabled: true
34
36
 
35
37
  # Required: choose the conversational harness for this deployment.
@@ -45,7 +47,7 @@ realtime:
45
47
  # Optional model override for the realtime session. Live-only models (e.g.
46
48
  # gemini-3.1-flash-live-preview) cannot be used for turn-based work, so the
47
49
  # realtime session can point to a different model than model.name. When omitted,
48
- # the realtime session falls back to model.name and MODEL_PROVIDER.
50
+ # the realtime session falls back to model.name and model.provider.
49
51
  # model:
50
52
  # name: gemini-3.1-flash-live-preview
51
53
  # provider: google_genai
@@ -98,10 +100,16 @@ model:
98
100
  # Summarisation trigger (tokens). Also drives soft-spill / read_file char budgets.
99
101
  context_window: 1000000
100
102
  max_turns: 1000
101
- # Prompt-cache session hints: none | short | long (env: MODEL_CACHE_RETENTION)
103
+ # Prompt-cache session hints: none | short | long (YAML-only)
102
104
  # cache_retention: short
103
- # Optional cheaper model for sync history summarization (env: CONTEXT_SUMMARIZATION_MODEL); omit = main model
105
+ # Optional cheaper model for sync history summarization; omit = main model
104
106
  # summarization_model: gemini-3-flash
107
+ # Ollama-local only (YAML). Default 24h so the KV prefix cache survives idle.
108
+ # Set "0" to omit. Distinct from the Ollama daemon env OLLAMA_KEEP_ALIVE.
109
+ # keep_alive: 24h
110
+ # Ollama-local only (YAML). Pin llama num_ctx; omit = server default.
111
+ # Do NOT copy context_window here — that budget is summarization/spill, often 1M.
112
+ # num_ctx: 8192
105
113
 
106
114
  # Non-secret GCP identifiers (prefer .env for secrets / ADC path).
107
115
  # gcp:
@@ -127,8 +135,6 @@ memory:
127
135
  backend: chroma
128
136
  embedding_model: embeddinggemma-300m
129
137
 
130
- # context_curation is unused after MemPalace wake-up replaced INDEX.md + curator.
131
-
132
138
  emission:
133
139
  # Terse emission-style guidance injected into the cached harness prefix.
134
140
  # Two named modes:
@@ -247,7 +253,7 @@ scheduler:
247
253
  # Env override: MONKEYBOT_SCHEDULER_ENABLED (1 | true | yes | on).
248
254
  enabled: false
249
255
 
250
- # Optional JSON for MODEL_PROVIDER=fake tests (env: MONKEYBOT_FAKE_PROVIDER_EVENTS)
256
+ # Optional JSON for model.provider: fake tests (env: MONKEYBOT_FAKE_PROVIDER_EVENTS)
251
257
  # fake_provider:
252
258
  # events_json: '[[]]'
253
259
 
@@ -25,6 +25,11 @@ def _free_port() -> int:
25
25
  def test_chat_repl_round_trip_with_fake_gateway(tmp_path: Path) -> None:
26
26
  port = _free_port()
27
27
  base = f"http://127.0.0.1:{port}"
28
+ (tmp_path / "monkeybot_config").mkdir()
29
+ (tmp_path / "monkeybot_config" / "monkeybot.yaml").write_text(
30
+ "model:\n provider: fake\n name: fake\n",
31
+ encoding="utf-8",
32
+ )
28
33
  agent = tmp_path / "AGENT.md"
29
34
  agent.write_text("# Test agent\nYou are a test assistant.\n", encoding="utf-8")
30
35
  policy = tmp_path / "command_allowlist.yaml"
@@ -40,7 +45,6 @@ def test_chat_repl_round_trip_with_fake_gateway(tmp_path: Path) -> None:
40
45
  "DB_URL": f"sqlite:///{tmp_path / 'mb.db'}",
41
46
  "MCP_CONFIG": str(tmp_path / "no_mcp.json"),
42
47
  "MEMORY_PATH": str(tmp_path / "memory"),
43
- "MODEL_PROVIDER": "fake",
44
48
  "PORT": str(port),
45
49
  "SKILLS_PATH": str(tmp_path / "skills"),
46
50
  "MONKEYBOT_CHAT_PLAIN": "1",
@@ -430,7 +430,7 @@ def test_close_deletes_session_and_captures_report_dir() -> None:
430
430
  200,
431
431
  json={
432
432
  "deleted": True,
433
- "transcript_report_dir": "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close",
433
+ "transcript_dir": "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close",
434
434
  },
435
435
  request=request,
436
436
  )
@@ -441,7 +441,7 @@ def test_close_deletes_session_and_captures_report_dir() -> None:
441
441
  await controller.close()
442
442
  client.delete.assert_awaited_once_with("http://localhost:8080/sessions/sess-close")
443
443
  assert (
444
- controller.transcript_report_dir
444
+ controller.transcript_dir
445
445
  == "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close"
446
446
  )
447
447
  client.aclose.assert_awaited_once()
@@ -0,0 +1,217 @@
1
+ """Tests for doctor remediation copy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ from monkeybot_cli.commands.doctor import (
10
+ _add_ollama_local_checks,
11
+ _agent_defines_project_extra,
12
+ _extra_remediation,
13
+ _is_local_ollama_provider,
14
+ _runtime_python_version,
15
+ )
16
+ from monkeybot_cli.output import CommandReport
17
+ from monkeybot_cli.runtime_python import RuntimePython
18
+
19
+
20
+ def test_extra_remediation_default_mvp_wording(tmp_path: Path) -> None:
21
+ (tmp_path / "pyproject.toml").write_text(
22
+ '[project]\nname = "bot"\ndependencies = ["monkeybot>=2.1.0,<3"]\n',
23
+ encoding="utf-8",
24
+ )
25
+ runtime = SimpleNamespace(source="venv")
26
+ text = _extra_remediation("openai", tmp_path, runtime)
27
+ assert "uv sync --extra" not in text
28
+ assert f"Add monkeybot[openai] to {tmp_path}/pyproject.toml dependencies" in text
29
+ assert f"cd {tmp_path} && uv sync" in text
30
+
31
+
32
+ def test_extra_remediation_keeps_extra_for_project_optionals(tmp_path: Path) -> None:
33
+ (tmp_path / "pyproject.toml").write_text(
34
+ "[project]\n"
35
+ 'name = "bot"\n'
36
+ "[project.optional-dependencies]\n"
37
+ 'openai = ["monkeybot[openai]"]\n',
38
+ encoding="utf-8",
39
+ )
40
+ runtime = SimpleNamespace(source="uv")
41
+ text = _extra_remediation("openai", tmp_path, runtime)
42
+ assert text == f"Install in the agent project: cd {tmp_path} && uv sync --extra openai"
43
+
44
+
45
+ def test_extra_remediation_config_only_cli_env(tmp_path: Path) -> None:
46
+ runtime = SimpleNamespace(source="cli")
47
+ text = _extra_remediation("bedrock", tmp_path, runtime)
48
+ assert "uv tool install --with 'monkeybot[bedrock]' monkeybot-cli" in text
49
+ assert "refresh the managed runtime" not in text
50
+
51
+
52
+ def test_extra_remediation_config_only_managed_runtime(tmp_path: Path) -> None:
53
+ runtime = SimpleNamespace(source="cli-managed")
54
+ text = _extra_remediation("openai", tmp_path, runtime)
55
+ assert "uv tool install --with 'monkeybot[openai]' monkeybot-cli" in text
56
+ assert "refresh the managed runtime" in text
57
+
58
+
59
+ def test_agent_defines_project_extra(tmp_path: Path) -> None:
60
+ assert not _agent_defines_project_extra(tmp_path, "openai")
61
+ (tmp_path / "pyproject.toml").write_text(
62
+ "[project.optional-dependencies]\npostgres = []\n",
63
+ encoding="utf-8",
64
+ )
65
+ assert _agent_defines_project_extra(tmp_path, "postgres")
66
+ assert not _agent_defines_project_extra(tmp_path, "openai")
67
+
68
+
69
+ def test_runtime_python_version_returns_zeros_when_uv_missing(tmp_path: Path, monkeypatch) -> None:
70
+ runtime = RuntimePython(["uv", "run", "python"], "uv", tmp_path)
71
+
72
+ def fake_run(*args, **kwargs):
73
+ del args, kwargs
74
+ raise FileNotFoundError("uv")
75
+
76
+ monkeypatch.setattr("monkeybot_cli.runtime_python.subprocess.run", fake_run)
77
+ assert _runtime_python_version(runtime) == (0, 0, 0)
78
+
79
+
80
+ def test_runtime_python_version_returns_zeros_on_timeout(tmp_path: Path, monkeypatch) -> None:
81
+ runtime = RuntimePython(["uv", "run", "python"], "uv", tmp_path)
82
+
83
+ def fake_run(*args, **kwargs):
84
+ del args, kwargs
85
+ raise subprocess.TimeoutExpired(cmd="uv", timeout=15)
86
+
87
+ monkeypatch.setattr("monkeybot_cli.runtime_python.subprocess.run", fake_run)
88
+ assert _runtime_python_version(runtime) == (0, 0, 0)
89
+
90
+
91
+ def test_is_local_ollama_provider() -> None:
92
+ assert _is_local_ollama_provider("ollama-local")
93
+ assert _is_local_ollama_provider("ollama_local")
94
+ assert _is_local_ollama_provider("ollama")
95
+ assert not _is_local_ollama_provider("ollama-cloud")
96
+ assert not _is_local_ollama_provider("gemini")
97
+
98
+
99
+ def _doctor_by_id(report: CommandReport) -> dict[str, object]:
100
+ return {c.id: c for c in report.checks}
101
+
102
+
103
+ def test_ollama_local_checks_skip_for_cloud() -> None:
104
+ report = CommandReport(command="doctor", ok=True, config_path=None)
105
+ _add_ollama_local_checks(
106
+ report,
107
+ provider="ollama-cloud",
108
+ model_name="qwen3:8b-mlx",
109
+ thinking_budget=-1,
110
+ num_ctx=100_000,
111
+ )
112
+ by_id = _doctor_by_id(report)
113
+ assert by_id["ollama.local.mlx_runner"].status == "skip"
114
+ assert by_id["ollama.local.thinking_default"].status == "skip"
115
+ assert by_id["ollama.local.num_ctx_invalid"].status == "skip"
116
+ assert by_id["ollama.local.num_ctx_large"].status == "skip"
117
+
118
+
119
+ def test_ollama_local_checks_warn_mlx_thinking_and_large_ctx() -> None:
120
+ report = CommandReport(command="doctor", ok=True, config_path=None)
121
+ _add_ollama_local_checks(
122
+ report,
123
+ provider="ollama-local",
124
+ model_name="qwen3:8b-mlx",
125
+ thinking_budget=-1,
126
+ num_ctx=65_536,
127
+ )
128
+ by_id = _doctor_by_id(report)
129
+ assert by_id["ollama.local.mlx_runner"].status == "fail"
130
+ assert by_id["ollama.local.mlx_runner"].severity == "warning"
131
+ assert "docs/ollama-local.md" in (by_id["ollama.local.mlx_runner"].remediation or "")
132
+ assert by_id["ollama.local.thinking_default"].status == "fail"
133
+ assert by_id["ollama.local.num_ctx_invalid"].status == "pass"
134
+ assert by_id["ollama.local.num_ctx_large"].status == "fail"
135
+
136
+
137
+ def test_ollama_local_checks_pass_when_tuned() -> None:
138
+ report = CommandReport(command="doctor", ok=True, config_path=None)
139
+ _add_ollama_local_checks(
140
+ report,
141
+ provider="ollama-local",
142
+ model_name="llama3.1:8b",
143
+ thinking_budget=0,
144
+ num_ctx=8192,
145
+ )
146
+ by_id = _doctor_by_id(report)
147
+ assert by_id["ollama.local.mlx_runner"].status == "pass"
148
+ assert by_id["ollama.local.thinking_default"].status == "skip"
149
+ assert by_id["ollama.local.num_ctx_invalid"].status == "pass"
150
+ assert by_id["ollama.local.num_ctx_large"].status == "pass"
151
+
152
+
153
+ def test_ollama_local_num_ctx_unset_is_skipped() -> None:
154
+ report = CommandReport(command="doctor", ok=True, config_path=None)
155
+ _add_ollama_local_checks(
156
+ report,
157
+ provider="ollama-local",
158
+ model_name="llama3.1:8b",
159
+ thinking_budget=0,
160
+ num_ctx=None,
161
+ )
162
+ by_id = _doctor_by_id(report)
163
+ assert by_id["ollama.local.num_ctx_invalid"].status == "skip"
164
+ assert by_id["ollama.local.num_ctx_large"].status == "skip"
165
+
166
+
167
+ def test_ollama_local_num_ctx_garbage_fails() -> None:
168
+ report = CommandReport(command="doctor", ok=True, config_path=None)
169
+ _add_ollama_local_checks(
170
+ report,
171
+ provider="ollama-local",
172
+ model_name="llama3.1:8b",
173
+ thinking_budget=0,
174
+ num_ctx="abc",
175
+ )
176
+ by_id = _doctor_by_id(report)
177
+ assert by_id["ollama.local.num_ctx_invalid"].status == "fail"
178
+ assert by_id["ollama.local.num_ctx_invalid"].severity == "error"
179
+ assert by_id["ollama.local.num_ctx_large"].status == "skip"
180
+
181
+
182
+ def test_ollama_local_num_ctx_zero_fails() -> None:
183
+ report = CommandReport(command="doctor", ok=True, config_path=None)
184
+ _add_ollama_local_checks(
185
+ report,
186
+ provider="ollama-local",
187
+ model_name="llama3.1:8b",
188
+ thinking_budget=0,
189
+ num_ctx=0,
190
+ )
191
+ by_id = _doctor_by_id(report)
192
+ assert by_id["ollama.local.num_ctx_invalid"].status == "fail"
193
+ assert by_id["ollama.local.num_ctx_large"].status == "skip"
194
+
195
+
196
+ def test_ollama_local_thinking_default_skips_non_reasoning() -> None:
197
+ report = CommandReport(command="doctor", ok=True, config_path=None)
198
+ _add_ollama_local_checks(
199
+ report,
200
+ provider="ollama-local",
201
+ model_name="llama3.1:8b",
202
+ thinking_budget=-1,
203
+ num_ctx=None,
204
+ )
205
+ assert _doctor_by_id(report)["ollama.local.thinking_default"].status == "skip"
206
+
207
+
208
+ def test_ollama_local_thinking_default_pass_when_off() -> None:
209
+ report = CommandReport(command="doctor", ok=True, config_path=None)
210
+ _add_ollama_local_checks(
211
+ report,
212
+ provider="ollama-local",
213
+ model_name="qwen3:8b",
214
+ thinking_budget=0,
215
+ num_ctx=8192,
216
+ )
217
+ assert _doctor_by_id(report)["ollama.local.thinking_default"].status == "pass"
@@ -48,7 +48,7 @@ def test_wait_for_health_rejects_dead_child_after_stale_200() -> None:
48
48
  assert wait_for_health("http://127.0.0.1:18080", proc, timeout_s=1.0) is False
49
49
 
50
50
 
51
- def test_spawn_gateway_enables_transcript_capture(tmp_path: Path, monkeypatch) -> None:
51
+ def test_spawn_gateway_does_not_set_transcript_env(tmp_path: Path, monkeypatch) -> None:
52
52
  captured: dict[str, object] = {}
53
53
 
54
54
  def fake_popen(*args: object, **kwargs: object) -> object:
@@ -65,7 +65,7 @@ def test_spawn_gateway_enables_transcript_capture(tmp_path: Path, monkeypatch) -
65
65
  try:
66
66
  env = captured["env"]
67
67
  assert isinstance(env, dict)
68
- assert env["MONKEYBOT_TRANSCRIPT_ENABLED"] == "1"
68
+ assert "MONKEYBOT_TRANSCRIPT_ENABLED" not in env
69
69
  finally:
70
70
  spawned.log_file.close()
71
71
  spawned.log_path.unlink()
@@ -617,7 +617,7 @@ wheels = [
617
617
 
618
618
  [[package]]
619
619
  name = "monkeybot"
620
- version = "3.0.0"
620
+ version = "3.1.0"
621
621
  source = { editable = "../" }
622
622
  dependencies = [
623
623
  { name = "aiosqlite" },
@@ -730,25 +730,33 @@ dev = [
730
730
 
731
731
  [[package]]
732
732
  name = "monkeybot-browser-mcp"
733
- version = "0.2.0"
733
+ version = "0.5.0"
734
734
  source = { editable = "../integrations/browser-mcp" }
735
735
  dependencies = [
736
736
  { name = "browser-harness" },
737
737
  { name = "mcp" },
738
+ { name = "pillow" },
739
+ { name = "pyyaml" },
738
740
  ]
739
741
 
740
742
  [package.metadata]
741
743
  requires-dist = [
744
+ { name = "bedrock-agentcore", marker = "extra == 'agentcore'" },
745
+ { name = "boto3", marker = "extra == 'agentcore'" },
742
746
  { name = "browser-harness", specifier = ">=0.1.3" },
743
747
  { name = "mcp", specifier = ">=1.0.0,<2" },
748
+ { name = "pillow", specifier = ">=10" },
749
+ { name = "playwright", marker = "extra == 'agentcore'" },
750
+ { name = "pyyaml", specifier = ">=6" },
744
751
  ]
752
+ provides-extras = ["agentcore"]
745
753
 
746
754
  [package.metadata.requires-dev]
747
755
  dev = [{ name = "pytest", specifier = ">=8.0.0" }]
748
756
 
749
757
  [[package]]
750
758
  name = "monkeybot-cli"
751
- version = "0.5.0"
759
+ version = "0.6.0"
752
760
  source = { editable = "." }
753
761
  dependencies = [
754
762
  { name = "httpx" },
@@ -1,16 +0,0 @@
1
- ---
2
- name: browser
3
- description: Control a real browser through the optional bundled browser MCP server.
4
- ---
5
-
6
- # Browser
7
-
8
- When browser tools are needed, call `enable_mcp("browser")` first. The browser
9
- MCP server ships **enabled** in new agents (`mcp.json`); progressive disclosure
10
- still requires `enable_mcp` before `browser__*` schemas are advertised.
11
-
12
- Use indexed DOM tools before screenshots. Browser screenshots and agent-written
13
- playbooks are workspace data: `browser/Screenshots/` and `browser/playbooks/`.
14
- They are not trusted skills and may be discarded with an ephemeral workspace.
15
-
16
- Finish browser sessions with `browser_stop`.
@@ -1,85 +0,0 @@
1
- """Tests for doctor remediation copy."""
2
-
3
- from __future__ import annotations
4
-
5
- import subprocess
6
- from pathlib import Path
7
- from types import SimpleNamespace
8
-
9
- from monkeybot_cli.commands.doctor import (
10
- _agent_defines_project_extra,
11
- _extra_remediation,
12
- _runtime_python_version,
13
- )
14
- from monkeybot_cli.runtime_python import RuntimePython
15
-
16
-
17
- def test_extra_remediation_default_mvp_wording(tmp_path: Path) -> None:
18
- (tmp_path / "pyproject.toml").write_text(
19
- '[project]\nname = "bot"\ndependencies = ["monkeybot>=2.1.0,<3"]\n',
20
- encoding="utf-8",
21
- )
22
- runtime = SimpleNamespace(source="venv")
23
- text = _extra_remediation("openai", tmp_path, runtime)
24
- assert "uv sync --extra" not in text
25
- assert f"Add monkeybot[openai] to {tmp_path}/pyproject.toml dependencies" in text
26
- assert f"cd {tmp_path} && uv sync" in text
27
-
28
-
29
- def test_extra_remediation_keeps_extra_for_project_optionals(tmp_path: Path) -> None:
30
- (tmp_path / "pyproject.toml").write_text(
31
- "[project]\n"
32
- 'name = "bot"\n'
33
- "[project.optional-dependencies]\n"
34
- 'openai = ["monkeybot[openai]"]\n',
35
- encoding="utf-8",
36
- )
37
- runtime = SimpleNamespace(source="uv")
38
- text = _extra_remediation("openai", tmp_path, runtime)
39
- assert text == f"Install in the agent project: cd {tmp_path} && uv sync --extra openai"
40
-
41
-
42
- def test_extra_remediation_config_only_cli_env(tmp_path: Path) -> None:
43
- runtime = SimpleNamespace(source="cli")
44
- text = _extra_remediation("bedrock", tmp_path, runtime)
45
- assert "uv tool install --with 'monkeybot[bedrock]' monkeybot-cli" in text
46
- assert "refresh the managed runtime" not in text
47
-
48
-
49
- def test_extra_remediation_config_only_managed_runtime(tmp_path: Path) -> None:
50
- runtime = SimpleNamespace(source="cli-managed")
51
- text = _extra_remediation("openai", tmp_path, runtime)
52
- assert "uv tool install --with 'monkeybot[openai]' monkeybot-cli" in text
53
- assert "refresh the managed runtime" in text
54
-
55
-
56
- def test_agent_defines_project_extra(tmp_path: Path) -> None:
57
- assert not _agent_defines_project_extra(tmp_path, "openai")
58
- (tmp_path / "pyproject.toml").write_text(
59
- "[project.optional-dependencies]\npostgres = []\n",
60
- encoding="utf-8",
61
- )
62
- assert _agent_defines_project_extra(tmp_path, "postgres")
63
- assert not _agent_defines_project_extra(tmp_path, "openai")
64
-
65
-
66
- def test_runtime_python_version_returns_zeros_when_uv_missing(tmp_path: Path, monkeypatch) -> None:
67
- runtime = RuntimePython(["uv", "run", "python"], "uv", tmp_path)
68
-
69
- def fake_run(*args, **kwargs):
70
- del args, kwargs
71
- raise FileNotFoundError("uv")
72
-
73
- monkeypatch.setattr("monkeybot_cli.runtime_python.subprocess.run", fake_run)
74
- assert _runtime_python_version(runtime) == (0, 0, 0)
75
-
76
-
77
- def test_runtime_python_version_returns_zeros_on_timeout(tmp_path: Path, monkeypatch) -> None:
78
- runtime = RuntimePython(["uv", "run", "python"], "uv", tmp_path)
79
-
80
- def fake_run(*args, **kwargs):
81
- del args, kwargs
82
- raise subprocess.TimeoutExpired(cmd="uv", timeout=15)
83
-
84
- monkeypatch.setattr("monkeybot_cli.runtime_python.subprocess.run", fake_run)
85
- assert _runtime_python_version(runtime) == (0, 0, 0)
File without changes