monkeybot-cli 0.9.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.
Files changed (89) hide show
  1. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/PKG-INFO +2 -2
  2. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/pyproject.toml +2 -2
  3. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/skills/monkeybot/references/config-sections.md +3 -2
  4. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_session.py +3 -9
  5. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tool_display.py +1 -3
  6. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tui.py +20 -28
  7. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/chat.py +2 -3
  8. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/doctor.py +64 -1
  9. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/loop.py +6 -1
  10. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/new.py +1 -3
  11. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/validate.py +41 -8
  12. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/compat.py +2 -6
  13. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/extras_catalog.py +1 -0
  14. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/gateway_health.py +1 -3
  15. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/opensandbox_lifecycle.py +4 -3
  16. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/output.py +3 -1
  17. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/providers.py +2 -0
  18. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/push_to_talk.py +1 -2
  19. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/session_controller.py +1 -3
  20. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/env.example +3 -0
  21. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +11 -2
  22. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_continue.py +1 -3
  23. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_errors.py +12 -4
  24. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_local_shell.py +3 -1
  25. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_renderer_parity.py +1 -3
  26. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_session.py +1 -3
  27. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_tui.py +15 -18
  28. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_cli.py +9 -3
  29. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_config_resolve.py +3 -1
  30. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_doctor.py +52 -0
  31. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_realtime_session_controller.py +1 -3
  32. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_run_cmd.py +6 -2
  33. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_runtime_python.py +3 -1
  34. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_terminal_markdown.py +1 -5
  35. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/uv.lock +6 -3
  36. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/.gitignore +0 -0
  37. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/skills/monkeybot/SKILL.md +0 -0
  38. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/__init__.py +0 -0
  39. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_file_index.py +0 -0
  40. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
  41. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  42. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
  43. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_theme.py +0 -0
  44. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
  45. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  46. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/refresh.py +0 -0
  47. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
  48. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/talk.py +0 -0
  49. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/commands/trace.py +0 -0
  50. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/config_resolve.py +0 -0
  51. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/exit_commands.py +0 -0
  52. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/main.py +0 -0
  53. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/process_tree.py +0 -0
  54. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  55. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  56. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/client.py +0 -0
  57. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  58. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/session.py +0 -0
  59. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
  60. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  61. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/runtime_python.py +0 -0
  62. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold.py +0 -0
  63. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  64. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
  65. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  66. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -0
  67. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
  68. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
  69. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
  70. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
  71. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
  72. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
  73. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
  74. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
  75. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  76. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  77. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/session_controller.py +0 -0
  78. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  79. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_e2e.py +0 -0
  80. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_file_index.py +0 -0
  81. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_grounding.py +0 -0
  82. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_status_bar.py +0 -0
  83. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_theme.py +0 -0
  84. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_chat_tool_display.py +0 -0
  85. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_extras_catalog.py +0 -0
  86. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_gateway_health.py +0 -0
  87. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_opensandbox_lifecycle.py +0 -0
  88. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_scaffold.py +0 -0
  89. {monkeybot_cli-0.9.0 → monkeybot_cli-0.10.0}/tests/test_trace_cmd.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.9.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.0
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.9.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.8.0,<4",
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,9 +37,7 @@ _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
42
  _SEARCH_TOOLS = frozenset({"grep", "web_search"})
45
43
  _HINT_KEYS = frozenset(
@@ -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"),
@@ -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 child is self._assistant or child is self._thinking or child is self._thinking_trace or child is self._hitl_card:
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(f"Model set to {provider}/{name} — new session started (context cleared)")
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 {}
@@ -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 load_agent_dotenv, load_config_doc, resolve_agent_root, resolve_config
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)
@@ -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)}" if missing else "All includes resolved",
123
+ message=f"Missing includes: {', '.join(missing)}"
124
+ if missing
125
+ else "All includes resolved",
124
126
  )
125
127
  else:
126
- check(report, id="config.includes.resolve", category="config", severity="warning", passed=True, skip=True)
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" if provider and provider not in SUPPORTED_YAML_MODEL_PROVIDERS else "",
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({k: v for k, v in os.environ.items() if k.startswith(("GCP_", "GOOGLE_", "ANTHROPIC_VERTEX"))})
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 ("GCP_PROJECT_ID", "VERTEX_AI_PROJECT_ID", "ANTHROPIC_VERTEX_PROJECT_ID", "GOOGLE_CLOUD_PROJECT")
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(report, id="gcp.project.required", category="config", severity="error", passed=True, skip=True)
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({**flat, "MODEL_PROVIDER": provider or flat.get("MODEL_PROVIDER", "gemini")})
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)}" if missing_refs else "All MCP env refs set",
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():
@@ -14,14 +14,10 @@ def _bounds_from_range(range_: str) -> tuple[str, str]:
14
14
  """Parse inclusive lower / exclusive upper versions from ``COMPATIBLE_CORE_RANGE``."""
15
15
  match = re.fullmatch(r">\s*=\s*([^,]+)\s*,\s*<\s*(.+)", range_.strip())
16
16
  if match is None:
17
- raise ValueError(
18
- f"COMPATIBLE_CORE_RANGE must be '>=LOWER,<UPPER', got {range_!r}"
19
- )
17
+ raise ValueError(f"COMPATIBLE_CORE_RANGE must be '>=LOWER,<UPPER', got {range_!r}")
20
18
  lower, upper = match.group(1).strip(), match.group(2).strip()
21
19
  if not lower or not upper:
22
- raise ValueError(
23
- f"COMPATIBLE_CORE_RANGE must be '>=LOWER,<UPPER', got {range_!r}"
24
- )
20
+ raise ValueError(f"COMPATIBLE_CORE_RANGE must be '>=LOWER,<UPPER', got {range_!r}")
25
21
  return lower, upper
26
22
 
27
23
 
@@ -28,6 +28,7 @@ PROVIDER_CHOICES: tuple[ExtraChoice, ...] = (
28
28
  ExtraChoice("ollama-local", "Ollama (local)"),
29
29
  ExtraChoice("nvidia", "NVIDIA NIM"),
30
30
  ExtraChoice("openrouter", "OpenRouter"),
31
+ ExtraChoice("fireworks", "Fireworks AI"),
31
32
  )
32
33
 
33
34
  # Non-provider agent features (root ``[project.optional-dependencies]`` names).
@@ -20,9 +20,7 @@ def port_free(port: int, host: str = "127.0.0.1") -> bool:
20
20
  return False
21
21
 
22
22
 
23
- def wait_for_health(
24
- base: str, proc: subprocess.Popen[str] | None, timeout_s: float = 30.0
25
- ) -> bool:
23
+ def wait_for_health(base: str, proc: subprocess.Popen[str] | None, timeout_s: float = 30.0) -> bool:
26
24
  """Poll ``GET {base}/health`` until 200 from *our* child, process dies, or timeout.
27
25
 
28
26
  On success we re-check ``proc.poll()`` so a pre-existing listener on the same
@@ -261,7 +261,9 @@ def ensure_opensandbox_for_agent(
261
261
  )
262
262
  return False
263
263
 
264
- container = os.environ.get("SANDBOX_CONTAINER", _DEFAULT_CONTAINER).strip() or _DEFAULT_CONTAINER
264
+ container = (
265
+ os.environ.get("SANDBOX_CONTAINER", _DEFAULT_CONTAINER).strip() or _DEFAULT_CONTAINER
266
+ )
265
267
  image = _server_image()
266
268
  if health_wait_secs is None:
267
269
  health_wait_secs = float(
@@ -312,8 +314,7 @@ def ensure_opensandbox_for_agent(
312
314
 
313
315
  if _container_exists(container) and not _container_running(container):
314
316
  print(
315
- "monkeybot: OpenSandbox container exited before becoming healthy "
316
- f"(image={image}).",
317
+ f"monkeybot: OpenSandbox container exited before becoming healthy (image={image}).",
317
318
  flush=True,
318
319
  )
319
320
  else:
@@ -73,7 +73,9 @@ class CommandReport:
73
73
  if c.remediation and c.status == "fail":
74
74
  print(f" → {c.remediation}")
75
75
  s = self.summary
76
- print(f" summary: {s['passed']} passed, {s['warnings']} warnings, {s['failed']} failed, {s['skipped']} skipped")
76
+ print(
77
+ f" summary: {s['passed']} passed, {s['warnings']} warnings, {s['failed']} failed, {s['skipped']} skipped"
78
+ )
77
79
 
78
80
  def emit(self, *, as_json: bool) -> int:
79
81
  self.compute_ok()
@@ -50,6 +50,7 @@ PROVIDER_SPECS: dict[str, ProviderSpec] = {
50
50
  ),
51
51
  "nvidia": ProviderSpec(("nvidia",), "nvidia", ("NVIDIA_API_KEY",)),
52
52
  "openrouter": ProviderSpec(("openrouter",), "openrouter", ("OPENROUTER_API_KEY",)),
53
+ "fireworks": ProviderSpec(("fireworks",), "fireworks", ("FIREWORKS_API_KEY",)),
53
54
  "ollama": ProviderSpec(
54
55
  # credential_env_vars is empty: credentials_optional=True short-circuits
55
56
  # credentials_present() before these are ever read, and OLLAMA_BASE_URL
@@ -96,6 +97,7 @@ def extra_module(extra: str) -> str:
96
97
  "ollama": "openai",
97
98
  "nvidia": "openai",
98
99
  "openrouter": "openai",
100
+ "fireworks": "openai",
99
101
  "postgres": "asyncpg",
100
102
  "firestore": "google.cloud.firestore",
101
103
  "gcs": "google.cloud.storage",
@@ -57,8 +57,7 @@ class PushToTalkGate:
57
57
  keys = aliases.get(key_name)
58
58
  if keys is None:
59
59
  raise PushToTalkError(
60
- f"Unsupported push-to-talk key '{key_name}'. "
61
- "Use one of: cmd, alt, ctrl, space"
60
+ f"Unsupported push-to-talk key '{key_name}'. Use one of: cmd, alt, ctrl, space"
62
61
  )
63
62
  return keys
64
63
 
@@ -171,9 +171,7 @@ class RealtimeSessionController:
171
171
  self.push_to_talk = None
172
172
  try:
173
173
  self._ws = await websockets.connect(url)
174
- await self._ws.send(
175
- json.dumps({"kind": "connect", "session_id": self.session_id})
176
- )
174
+ await self._ws.send(json.dumps({"kind": "connect", "session_id": self.session_id}))
177
175
  except websockets.exceptions.InvalidStatus as exc:
178
176
  status = getattr(exc, "status_code", getattr(exc, "status", "unknown"))
179
177
  raise RuntimeError(f"Failed to connect to {url}: HTTP {status}") from exc
@@ -28,6 +28,9 @@ DB_URL=sqlite:///data/monkeybot.db
28
28
  # Hugging Face
29
29
  # HF_TOKEN=
30
30
 
31
+ # Fireworks AI
32
+ # FIREWORKS_API_KEY=
33
+
31
34
  # Ollama Cloud (model.provider: ollama-cloud) — key from ollama.com/settings/keys
32
35
  # OLLAMA_API_KEY=
33
36
 
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # Precedence: environment variables (and optional root `.env`) win over values here
6
6
  # except ``model.*`` (provider, name, temperature, max_tokens, thinking_budget,
7
- # context_window, summarization_model, max_turns, cache_retention) which are YAML-only.
7
+ # reasoning_effort, context_window, summarization_model, max_turns, cache_retention)
8
+ # which are YAML-only.
8
9
  #
9
10
  # Secrets are not committed here. Create a `.env` in the repo root when you need keys or
10
11
  # machine-local paths, for example:
@@ -14,6 +15,7 @@
14
15
  # ANTHROPIC_VERTEX_PROJECT_ID, ANTHROPIC_VERTEX_REGION,
15
16
  # OLLAMA_BASE_URL (default http://localhost:11434, no API key needed),
16
17
  # NVIDIA_API_KEY (free key from https://build.nvidia.com),
18
+ # FIREWORKS_API_KEY (https://fireworks.ai/account/api-keys),
17
19
  # TAVILY_API_KEY, FIRECRAWL_API_KEY, SANDBOX_API_KEY,
18
20
  # MONKEYBOT_TOOL_DENIED_PATTERNS, MONKEYBOT_CORS_ALLOW_ORIGINS, MONKEYBOT_CONFIG, …
19
21
  #
@@ -89,7 +91,7 @@ paths:
89
91
  workspace_root: ./workspace
90
92
 
91
93
  model:
92
- # gemini | openai | anthropic | vertex-claude | huggingface | ollama-cloud | ollama-local | ollama | nvidia | openrouter | aws_bedrock | fake
94
+ # gemini | openai | anthropic | vertex-claude | huggingface | ollama-cloud | ollama-local | ollama | nvidia | openrouter | fireworks | aws_bedrock | fake
93
95
  provider: gemini
94
96
  name: gemini-3.1-flash-live-preview
95
97
  temperature: 0.7
@@ -97,6 +99,13 @@ model:
97
99
  # Gemini: -1 = model default, 0 = off, N = token budget
98
100
  # Ollama (Gemma 4, Qwen3, …): -1 = server default, 0 = off (reasoning_effort: none)
99
101
  thinking_budget: -1
102
+ # Effort level for models that take one (low, medium, high, xhigh, max, …).
103
+ # Omit to use the provider default. Wins over thinking_budget when set.
104
+ # off / none / disabled turn thinking off (a bare YAML `off` is read as false; also off).
105
+ # Gemini only sends levels its SDK knows (minimal, low, medium, high); anything
106
+ # else falls back to thinking_budget. A value the API rejects is omitted on
107
+ # later turns. Bedrock sends it for Claude ids only.
108
+ # reasoning_effort: high
100
109
  # Summarisation trigger (tokens). Also drives soft-spill / read_file char budgets.
101
110
  context_window: 1000000
102
111
  max_turns: 1000
@@ -133,9 +133,7 @@ def test_run_chat_continue_resolves_and_sets_session(
133
133
  "monkeybot_cli.commands.chat._resolve_continue_session_id",
134
134
  lambda base: "resolved-session-id",
135
135
  )
136
- args = build_parser().parse_args(
137
- ["chat", "--url", "http://127.0.0.1:65001", "--continue"]
138
- )
136
+ args = build_parser().parse_args(["chat", "--url", "http://127.0.0.1:65001", "--continue"])
139
137
  run_chat(args)
140
138
  assert args.session == "resolved-session-id"
141
139
  assert "Continuing session resolved" in capsys.readouterr().out