monkeybot-cli 0.3.0__tar.gz → 0.5.1__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.3.0 → monkeybot_cli-0.5.1}/.gitignore +10 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/PKG-INFO +3 -3
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/pyproject.toml +2 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/skills/monkeybot/SKILL.md +9 -6
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/skills/monkeybot/references/config-sections.md +38 -19
- monkeybot_cli-0.5.1/src/monkeybot_cli/chat_file_index.py +101 -0
- monkeybot_cli-0.5.1/src/monkeybot_cli/chat_local_shell.py +142 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_session.py +33 -6
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_status_bar.py +4 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_tool_display.py +34 -128
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_tui.py +447 -21
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_tui_widgets.py +128 -7
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/chat.py +70 -5
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/doctor.py +69 -23
- monkeybot_cli-0.5.1/src/monkeybot_cli/commands/refresh.py +36 -0
- monkeybot_cli-0.5.1/src/monkeybot_cli/commands/run_cmd.py +197 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/validate.py +15 -7
- monkeybot_cli-0.5.1/src/monkeybot_cli/compat.py +30 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/extras_catalog.py +4 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/main.py +5 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/opensandbox_lifecycle.py +25 -15
- monkeybot_cli-0.5.1/src/monkeybot_cli/process_tree.py +44 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/providers.py +35 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/gateway_manager.py +5 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/session.py +11 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/session_controller.py +1 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/talk_ui.py +7 -2
- monkeybot_cli-0.5.1/src/monkeybot_cli/runtime_python.py +746 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold.py +190 -17
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/Dockerfile +2 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +3 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +12 -2
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/env.example +8 -1
- monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +72 -0
- monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +214 -0
- monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +45 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/mcp.json +5 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +98 -37
- monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +46 -0
- monkeybot_cli-0.5.1/tests/test_chat_continue.py +198 -0
- monkeybot_cli-0.5.1/tests/test_chat_file_index.py +84 -0
- monkeybot_cli-0.5.1/tests/test_chat_local_shell.py +158 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_session.py +89 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_tui.py +1143 -3
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_cli.py +135 -5
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_doctor.py +37 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_extras_catalog.py +21 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_gateway_health.py +29 -1
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_realtime_session_controller.py +18 -0
- monkeybot_cli-0.5.1/tests/test_run_cmd.py +277 -0
- monkeybot_cli-0.5.1/tests/test_runtime_python.py +1076 -0
- monkeybot_cli-0.5.1/tests/test_scaffold.py +221 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/uv.lock +287 -61
- monkeybot_cli-0.3.0/src/monkeybot_cli/commands/run_cmd.py +0 -40
- monkeybot_cli-0.3.0/src/monkeybot_cli/compat.py +0 -7
- monkeybot_cli-0.3.0/src/monkeybot_cli/runtime_python.py +0 -91
- monkeybot_cli-0.3.0/tests/test_run_cmd.py +0 -80
- monkeybot_cli-0.3.0/tests/test_runtime_python.py +0 -109
- monkeybot_cli-0.3.0/tests/test_scaffold.py +0 -120
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/__init__.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_renderer.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/chat_theme.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/__init__.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/loop.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/new.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/commands/talk.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/config_resolve.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/exit_commands.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/gateway_health.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/output.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/__init__.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/audio_io.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/client.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/session_controller.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/src/monkeybot_cli/terminal_markdown.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_e2e.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_errors.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_grounding.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_renderer_parity.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_status_bar.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_theme.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_chat_tool_display.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_config_resolve.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.3.0 → monkeybot_cli-0.5.1}/tests/test_terminal_markdown.py +0 -0
|
@@ -30,6 +30,12 @@ build/
|
|
|
30
30
|
|
|
31
31
|
# Data (SQLite DB + memory files — never commit)
|
|
32
32
|
data/
|
|
33
|
+
/memory/
|
|
34
|
+
.monkeybot/
|
|
35
|
+
*.sqlite3
|
|
36
|
+
workspace/.monkeybot/
|
|
37
|
+
*.sqlite
|
|
38
|
+
*.sqlite.writer-pid
|
|
33
39
|
|
|
34
40
|
# Testing & coverage
|
|
35
41
|
.pytest_cache/
|
|
@@ -59,4 +65,7 @@ docker/data/
|
|
|
59
65
|
code/
|
|
60
66
|
|
|
61
67
|
# Local venv for evals dependency experiments
|
|
62
|
-
.evals-venv/
|
|
68
|
+
.evals-venv/
|
|
69
|
+
|
|
70
|
+
# monkeythink
|
|
71
|
+
.monkeythink/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: monkeybot-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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.2.0
|
|
8
|
-
Requires-Dist: monkeybot[cli]<3
|
|
8
|
+
Requires-Dist: monkeybot[cli]<4,>=3.0.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.5.1"
|
|
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]>=
|
|
13
|
+
"monkeybot[cli]>=3.0.0,<4",
|
|
14
14
|
"monkeybot-browser-mcp>=0.2.0,<1",
|
|
15
15
|
"httpx>=0.27.0",
|
|
16
16
|
"pyyaml>=6.0.2",
|
|
@@ -86,7 +86,7 @@ monkeybot new --dest /path/to/bot --provider openai --yes
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Creates `monkeybot_config/`, read-only `skills/`, writable `workspace/`,
|
|
89
|
-
`
|
|
89
|
+
`memory/`, `.env.example`, a Dockerfile, and an agent `pyproject.toml`.
|
|
90
90
|
Use `--force` only when overwriting is explicitly requested.
|
|
91
91
|
|
|
92
92
|
Then:
|
|
@@ -111,9 +111,11 @@ uv sync
|
|
|
111
111
|
| `vertex-claude` | `GCP_PROJECT_ID` / `GOOGLE_CLOUD_PROJECT` / `ANTHROPIC_VERTEX_PROJECT_ID` (ADC) | `monkeybot[vertex-claude]` |
|
|
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
|
-
| `ollama` |
|
|
114
|
+
| `ollama-cloud` | `OLLAMA_API_KEY` | `monkeybot[ollama]` |
|
|
115
|
+
| `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`) | `monkeybot[ollama]` |
|
|
116
|
+
| `ollama` | Legacy auto-route (key + blank URL = cloud; explicit URL wins) | `monkeybot[ollama]` |
|
|
115
117
|
|
|
116
|
-
**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
|
|
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`).
|
|
117
119
|
|
|
118
120
|
`doctor` is the source of truth for credentials and extras — when in doubt, run it and read the `remediation` field (add `monkeybot[<extra>]` to agent deps + `uv sync`).
|
|
119
121
|
|
|
@@ -156,13 +158,13 @@ Decision → config map:
|
|
|
156
158
|
| "I have a custom web UI" | `gateway.cors_allow_origins` |
|
|
157
159
|
| "Search the web" | `web_search.backend` + `.env` keys (Tavily/Firecrawl) |
|
|
158
160
|
| "Run untrusted code" | `sandbox.enabled` + `SANDBOX_API_KEY` |
|
|
159
|
-
| "Use specialist agents" | `subagents
|
|
161
|
+
| "Use specialist agents" | `subagents.personas` + `monkeybot_config/agents/*.md` |
|
|
160
162
|
| "Connect external tools" | `mcp.json` (`mcpServers` object), then `validate --check-mcp` |
|
|
161
163
|
| "Control cost / context size" | `model.*`, `context_curation.*` |
|
|
162
164
|
| "Restrict dangerous commands" | `tools.denied_patterns`, `command_allowlist.yaml` |
|
|
163
165
|
| "Multiple environments" | top-level `includes:` fragments |
|
|
164
166
|
|
|
165
|
-
**Subagents (`task` tool):** share the parent `AGENT.md
|
|
167
|
+
**Subagents (`task` tool):** without `subagent_type`, share the parent `AGENT.md`. With a persona, use that persona's `agent_md`. Relative paths resolve from the bot project root, not `workspace/`. Specialize via `task` / `context` or named personas. For parallel `task` fan-out, prefer Postgres: add `monkeybot[postgres]` to the **agent** `pyproject.toml` dependencies, run `uv sync`, then set `DB_URL=postgresql://...` in `.env`.
|
|
166
168
|
|
|
167
169
|
**Observability** is mostly env + add `monkeybot[observability]` to agent deps + `uv sync` + an OTel collector — not `monkeybot.yaml`. See `docs/observability-runbook.md`.
|
|
168
170
|
|
|
@@ -191,7 +193,8 @@ uv tool install --editable .
|
|
|
191
193
|
|
|
192
194
|
| Command | Purpose |
|
|
193
195
|
|---------|---------|
|
|
194
|
-
| `new` | Scaffold `monkeybot_config/`, `workspace/`, `
|
|
196
|
+
| `new` | Scaffold `monkeybot_config/`, `workspace/`, `memory/`, `skills/`, `pyproject.toml`, `.env.example` |
|
|
197
|
+
| `refresh` | Additive update of packaged YAML defaults on an existing agent (keeps AGENT.md, mcp.json, model) |
|
|
195
198
|
| `validate` | Config + paths + MCP shape (`--check-mcp` for network) |
|
|
196
199
|
| `doctor` | Python, provider extra, credentials, port |
|
|
197
200
|
| `run` | Start SSE gateway subprocess |
|
|
@@ -21,10 +21,11 @@ Validate/doctor: `doctor` → `runtime.port.free`.
|
|
|
21
21
|
| Field | Default | When to change |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `agent_md` | `./monkeybot_config/AGENT.md` | Alternate system-prompt location |
|
|
24
|
-
| `memory_storage_uri` | `local://./
|
|
24
|
+
| `memory_storage_uri` | `local://./memory` | `gcs://…` for shared/cloud memory (requires GCP project) |
|
|
25
25
|
| `skills_path` | `./skills` | Point at a different skills tree |
|
|
26
26
|
| `db_url` | `sqlite:///data/monkeybot.db` | **Postgres for parallel subagents** — SQLite hits `database is locked` under concurrency |
|
|
27
27
|
| `auto_schema` | `true` | Set `false` when migrations own the schema (managed Postgres with DML-only runtime user) |
|
|
28
|
+
| `agent_id` | resolved agent root path | **Set explicitly when this agent is relocatable** — moved to a new path, redeployed with a different mount point, or run as multiple replicas that must share one conversation history. Without it, conversation-history scoping (which agent owns which thread, on a shared `db_url`) keys off the filesystem path, so a path change strands existing history. Once set, keep the value stable across deploys. |
|
|
28
29
|
| `mcp_config` | `./monkeybot_config/mcp.json` | Relocate MCP definitions |
|
|
29
30
|
| `command_allowlist_config` | `./monkeybot_config/command_allowlist.yaml` | Relocate the shell allowlist |
|
|
30
31
|
| `workspace_root` | `./workspace` (if present) | Change the file-tool sandbox root |
|
|
@@ -40,11 +41,11 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
|
|
|
40
41
|
| `temperature` | `0.7` | Lower for deterministic output, higher for creative |
|
|
41
42
|
| `max_tokens` | `60000` | Cap per-response length |
|
|
42
43
|
| `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
|
|
43
|
-
| `context_window` | `1000000` | Summarization trigger
|
|
44
|
-
| `max_turns` | `
|
|
44
|
+
| `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets |
|
|
45
|
+
| `max_turns` | `1000` | Hard cap on turns per run |
|
|
45
46
|
| `summarization_model` | (main model) | Cheaper model for history summarization (env `CONTEXT_SUMMARIZATION_MODEL`) |
|
|
46
47
|
|
|
47
|
-
Validate check ids: `model.provider.supported`, `model.name.present`. Supported YAML providers: `gemini`/`vertex`, `openai`, `anthropic`, `vertex-claude`, `huggingface`, `ollama`, `aws_bedrock`, `fake`.
|
|
48
|
+
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`.
|
|
48
49
|
|
|
49
50
|
## `gcp` / `anthropic_vertex` (non-secret identifiers)
|
|
50
51
|
|
|
@@ -86,40 +87,48 @@ Recent window by default; LLM curator only when the index is token-heavy. On cur
|
|
|
86
87
|
|
|
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.
|
|
88
89
|
|
|
89
|
-
## `
|
|
90
|
+
## `memory`
|
|
90
91
|
|
|
91
|
-
`enabled:
|
|
92
|
+
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.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
| Field | Default | When to change |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `enabled` | `true` | Set `false` to skip capture, wake-up, and prompt teaching |
|
|
97
|
+
| `backend` | `chroma` | Alternate MemPalace vector backend |
|
|
98
|
+
| `embedding_model` | `embeddinggemma-300m` | Match the embedder the palace was built with |
|
|
99
|
+
|
|
100
|
+
## `subagents`
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
Global defaults for `task` calls, plus optional named personas:
|
|
96
103
|
|
|
97
104
|
| Field | Default | Notes |
|
|
98
105
|
|---|---|---|
|
|
99
106
|
| `timeout_sec` | `600` | Per-subagent timeout |
|
|
100
|
-
| `max_turns` | `
|
|
107
|
+
| `max_turns` | `1000` | Per-subagent turn cap |
|
|
101
108
|
| `vertex_google_search` | `false` | **Gemini only.** Enables native `google_search` grounding for subagent `task` runs. Config-file only. |
|
|
102
|
-
| `
|
|
103
|
-
|
|
104
|
-
`subagents[]` defines named personas the parent selects via `task(subagent_type=...)`:
|
|
109
|
+
| `personas` | (none) | Named types selected via `task(subagent_type=...)`; each persona sets its own `agent_md` |
|
|
105
110
|
|
|
106
111
|
```yaml
|
|
107
112
|
subagents:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
timeout_sec: 600
|
|
114
|
+
max_turns: 1000
|
|
115
|
+
vertex_google_search: false
|
|
116
|
+
personas:
|
|
117
|
+
- name: researcher
|
|
118
|
+
description: "Deep-dives a topic and returns a structured summary."
|
|
119
|
+
agent_md: ./monkeybot_config/agents/researcher.md
|
|
111
120
|
```
|
|
112
121
|
|
|
113
|
-
Relative paths resolve from the bot project root, not `workspace/`. For parallel fan-out, use Postgres (`db_url`).
|
|
122
|
+
Without a `subagent_type`, the task inherits the parent `AGENT.md` (`paths.agent_md`). Relative paths resolve from the bot project root, not `workspace/`. For parallel fan-out, use Postgres (`db_url`).
|
|
114
123
|
|
|
115
124
|
## `tools`
|
|
116
125
|
|
|
117
126
|
| Field | Default | When to change |
|
|
118
127
|
|---|---|---|
|
|
119
128
|
| `denied_patterns` | (none) | Block substrings in tool args, e.g. `"rm -rf"` (also env `MONKEYBOT_TOOL_DENIED_PATTERNS`) |
|
|
120
|
-
| `read_max_lines`
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
| `read_max_lines` | `5000` | Cap on `read_file` `limit` (**YAML only** — no env override). Default when `limit` is omitted is harness-fixed at **2000** (pass `limit` to request more). |
|
|
130
|
+
|
|
131
|
+
Spill and `read_file` char budgets are derived from `model.context_window` (retired keys `spill_min_chars` / `spill_read_max_lines` / `read_default_lines` warn and are ignored). Context pressure ratios and tool-result budget fractions are fixed in harness code (not YAML/env).
|
|
123
132
|
|
|
124
133
|
For shell-command safety, pair `denied_patterns` with `monkeybot_config/command_allowlist.yaml`.
|
|
125
134
|
|
|
@@ -144,6 +153,16 @@ Tavily/Firecrawl need `TAVILY_API_KEY` / `FIRECRAWL_API_KEY` in `.env`. Doctor c
|
|
|
144
153
|
|
|
145
154
|
Needs `SANDBOX_API_KEY` in `.env`.
|
|
146
155
|
|
|
156
|
+
## `scheduler`
|
|
157
|
+
|
|
158
|
+
Prompt-first scheduled loops (`start_loop`, `/scheduler/loops`). Requires durable storage (`paths.db_url`). Loops are registered at runtime — there is no static `jobs` list in YAML.
|
|
159
|
+
|
|
160
|
+
| Field | Default | When to change |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `enabled` | `false` | `true` runs the tick worker in-process on the gateway (local/dev). Production: leave `false` and run `python -m monkeybot.scheduler` as a separate process |
|
|
163
|
+
|
|
164
|
+
Env override: `MONKEYBOT_SCHEDULER_ENABLED` (`1` \| `true` \| `yes` \| `on`).
|
|
165
|
+
|
|
147
166
|
## `includes`
|
|
148
167
|
|
|
149
168
|
Top-level list of YAML fragments (paths relative to the config file's directory). Later files deep-merge over earlier ones — useful for per-environment overrides:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Pure helpers for the ``@`` file-mention palette in ``monkeybot chat``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_AT_TOKEN_RE = re.compile(r"@(\S*)$")
|
|
11
|
+
|
|
12
|
+
_SKIP_DIRS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
".git",
|
|
15
|
+
".venv",
|
|
16
|
+
"venv",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
".mypy_cache",
|
|
20
|
+
".pytest_cache",
|
|
21
|
+
".ruff_cache",
|
|
22
|
+
"data",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_at_token(line: str, col: int) -> tuple[int, str] | None:
|
|
28
|
+
"""Return ``(start_col, query)`` if the cursor sits in an ``@token``.
|
|
29
|
+
|
|
30
|
+
Only triggers when the ``@`` starts the line or follows whitespace, so
|
|
31
|
+
mid-word ``@`` (e.g. ``a@b.com``) never matches.
|
|
32
|
+
"""
|
|
33
|
+
prefix = line[:col]
|
|
34
|
+
match = _AT_TOKEN_RE.search(prefix)
|
|
35
|
+
if match is None:
|
|
36
|
+
return None
|
|
37
|
+
start = match.start()
|
|
38
|
+
if start > 0 and not prefix[start - 1].isspace():
|
|
39
|
+
return None
|
|
40
|
+
return start, match.group(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def list_workspace_files(root: Path, *, limit: int = 3000) -> list[str]:
|
|
44
|
+
"""List files under ``root``, preferring git (honors .gitignore)."""
|
|
45
|
+
try:
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
timeout=5.0,
|
|
51
|
+
check=False,
|
|
52
|
+
)
|
|
53
|
+
if result.returncode == 0:
|
|
54
|
+
files = [line for line in result.stdout.splitlines() if line.strip()]
|
|
55
|
+
if files:
|
|
56
|
+
return sorted(files)[:limit]
|
|
57
|
+
except (OSError, subprocess.SubprocessError):
|
|
58
|
+
pass
|
|
59
|
+
return _walk_files(root, limit=limit)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _walk_files(root: Path, *, limit: int) -> list[str]:
|
|
63
|
+
out: list[str] = []
|
|
64
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
65
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS and not d.startswith(".")]
|
|
66
|
+
rel_dir = os.path.relpath(dirpath, root)
|
|
67
|
+
for name in filenames:
|
|
68
|
+
rel = name if rel_dir == "." else f"{rel_dir}/{name}"
|
|
69
|
+
out.append(rel.replace(os.sep, "/"))
|
|
70
|
+
if len(out) >= limit:
|
|
71
|
+
return sorted(out)
|
|
72
|
+
return sorted(out)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def fuzzy_filter_files(files: list[str], query: str, *, limit: int = 8) -> list[str]:
|
|
76
|
+
"""Case-insensitive subsequence match, ranked basename-prefix > path-prefix > contiguity."""
|
|
77
|
+
if not query:
|
|
78
|
+
return files[:limit]
|
|
79
|
+
q = query.lower()
|
|
80
|
+
scored: list[tuple[int, int, str]] = []
|
|
81
|
+
for path in files:
|
|
82
|
+
low = path.lower()
|
|
83
|
+
basename = low.rsplit("/", 1)[-1]
|
|
84
|
+
if not _is_subsequence(q, low):
|
|
85
|
+
continue
|
|
86
|
+
if basename.startswith(q):
|
|
87
|
+
rank = 0
|
|
88
|
+
elif low.startswith(q):
|
|
89
|
+
rank = 1
|
|
90
|
+
elif q in low:
|
|
91
|
+
rank = 2
|
|
92
|
+
else:
|
|
93
|
+
rank = 3
|
|
94
|
+
scored.append((rank, len(path), path))
|
|
95
|
+
scored.sort()
|
|
96
|
+
return [path for _, _, path in scored[:limit]]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_subsequence(query: str, text: str) -> bool:
|
|
100
|
+
it = iter(text)
|
|
101
|
+
return all(ch in it for ch in query)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Run a local shell command from the ``!`` prefix in ``monkeybot chat``.
|
|
2
|
+
|
|
3
|
+
Output is local-only — never sent to the agent.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from monkeybot_cli.process_tree import IS_WINDOWS, kill_process_tree, popen_kwargs_for_platform
|
|
14
|
+
|
|
15
|
+
# Cap capture before TUI truncation so `!yes`-style floods cannot OOM the process.
|
|
16
|
+
_MAX_CAPTURE_CHARS = 100_000
|
|
17
|
+
_READ_CHUNK = 4_096
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _append_capped(chunks: list[str], total: int, data: str) -> tuple[int, bool]:
|
|
21
|
+
"""Append ``data`` up to the capture cap. Returns (new_total, hit_cap)."""
|
|
22
|
+
remain = _MAX_CAPTURE_CHARS - total
|
|
23
|
+
if remain <= 0:
|
|
24
|
+
return total, True
|
|
25
|
+
if len(data) <= remain:
|
|
26
|
+
chunks.append(data)
|
|
27
|
+
return total + len(data), False
|
|
28
|
+
chunks.append(data[:remain])
|
|
29
|
+
return total + remain, True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_local_shell(command: str, cwd: Path, *, timeout: float = 120.0) -> tuple[str, int | None]:
|
|
33
|
+
"""Run ``command`` in a shell, returning merged stdout+stderr and the exit code.
|
|
34
|
+
|
|
35
|
+
Runs as the root of its own process group/tree so a timeout can kill
|
|
36
|
+
everything it spawned — plain ``subprocess.run(..., timeout=...)`` only
|
|
37
|
+
kills the shell itself, leaving children it spawned (e.g. a backgrounded
|
|
38
|
+
``sleep``) running past the reported timeout.
|
|
39
|
+
|
|
40
|
+
Output is capped at ``_MAX_CAPTURE_CHARS`` while streaming (never fully
|
|
41
|
+
buffered first), so unbounded writers cannot exhaust memory before the
|
|
42
|
+
TUI truncates for display. Works the same on POSIX and Windows.
|
|
43
|
+
"""
|
|
44
|
+
proc = subprocess.Popen(
|
|
45
|
+
command,
|
|
46
|
+
shell=True,
|
|
47
|
+
cwd=cwd,
|
|
48
|
+
stdout=subprocess.PIPE,
|
|
49
|
+
stderr=subprocess.STDOUT,
|
|
50
|
+
text=True,
|
|
51
|
+
**popen_kwargs_for_platform(),
|
|
52
|
+
)
|
|
53
|
+
assert proc.stdout is not None
|
|
54
|
+
|
|
55
|
+
chunks: list[str] = []
|
|
56
|
+
state = {"total": 0, "capped": False}
|
|
57
|
+
stop_reader = threading.Event()
|
|
58
|
+
|
|
59
|
+
def _reader() -> None:
|
|
60
|
+
stdout = proc.stdout
|
|
61
|
+
if stdout is None:
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
while not stop_reader.is_set():
|
|
65
|
+
chunk = stdout.read(_READ_CHUNK)
|
|
66
|
+
if chunk == "":
|
|
67
|
+
break
|
|
68
|
+
state["total"], hit = _append_capped(chunks, state["total"], chunk)
|
|
69
|
+
if hit:
|
|
70
|
+
state["capped"] = True
|
|
71
|
+
kill_process_tree(proc.pid)
|
|
72
|
+
break
|
|
73
|
+
except (ValueError, OSError):
|
|
74
|
+
# stdout closed from the main thread to unblock a stuck read.
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
reader = threading.Thread(target=_reader, name="monkeybot-local-shell-reader", daemon=True)
|
|
78
|
+
reader.start()
|
|
79
|
+
|
|
80
|
+
timed_out = False
|
|
81
|
+
try:
|
|
82
|
+
try:
|
|
83
|
+
proc.wait(timeout=timeout)
|
|
84
|
+
except subprocess.TimeoutExpired:
|
|
85
|
+
timed_out = True
|
|
86
|
+
kill_process_tree(proc.pid)
|
|
87
|
+
|
|
88
|
+
# Shell may have exited while a descendant still holds the pipe —
|
|
89
|
+
# kill the tree and close stdout so the reader cannot block forever.
|
|
90
|
+
if reader.is_alive():
|
|
91
|
+
kill_process_tree(proc.pid)
|
|
92
|
+
stop_reader.set()
|
|
93
|
+
try:
|
|
94
|
+
proc.stdout.close()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
reader.join(timeout=2.0)
|
|
98
|
+
if reader.is_alive() and not timed_out:
|
|
99
|
+
# Still stuck after kill+close: treat as timeout so the TUI
|
|
100
|
+
# always gets a bounded result instead of hanging.
|
|
101
|
+
timed_out = True
|
|
102
|
+
else:
|
|
103
|
+
reader.join(timeout=0.1)
|
|
104
|
+
except Exception:
|
|
105
|
+
timed_out = True
|
|
106
|
+
kill_process_tree(proc.pid)
|
|
107
|
+
stop_reader.set()
|
|
108
|
+
try:
|
|
109
|
+
proc.stdout.close()
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
reader.join(timeout=1.0)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
if proc.poll() is None:
|
|
116
|
+
kill_process_tree(proc.pid)
|
|
117
|
+
try:
|
|
118
|
+
proc.wait(timeout=1)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
output = "".join(chunks)
|
|
125
|
+
if timed_out:
|
|
126
|
+
# ``None`` means timed out (distinct from a real exit status).
|
|
127
|
+
return output + f"\n(timed out after {timeout:.0f}s)", None
|
|
128
|
+
if state["capped"]:
|
|
129
|
+
output += f"\n… (output capped at {_MAX_CAPTURE_CHARS} chars)"
|
|
130
|
+
return output, proc.returncode
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def truncate_output(text: str, *, max_lines: int = 200, max_chars: int = 10_000) -> str:
|
|
134
|
+
"""Bound output for transcript display, noting how much was dropped."""
|
|
135
|
+
lines = text.splitlines()
|
|
136
|
+
if len(lines) > max_lines:
|
|
137
|
+
dropped = len(lines) - max_lines
|
|
138
|
+
text = "\n".join(lines[:max_lines]) + f"\n… (+{dropped} lines truncated)"
|
|
139
|
+
if len(text) > max_chars:
|
|
140
|
+
dropped_chars = len(text) - max_chars
|
|
141
|
+
text = text[:max_chars] + f"\n… (+{dropped_chars} chars truncated)"
|
|
142
|
+
return text
|
|
@@ -19,6 +19,7 @@ from monkeybot.core.runtime.events import (
|
|
|
19
19
|
AssistantDelta,
|
|
20
20
|
ContextSummarized,
|
|
21
21
|
ContextSummarizing,
|
|
22
|
+
ContextUsage,
|
|
22
23
|
Error,
|
|
23
24
|
FrontendToolRequestEvent,
|
|
24
25
|
GroundingEvent,
|
|
@@ -228,6 +229,7 @@ class ChatSessionController:
|
|
|
228
229
|
self.usage = UsageStore()
|
|
229
230
|
self.session_id: str | None = None
|
|
230
231
|
self.stream_error = False
|
|
232
|
+
self.transcript_report_dir: str | None = None
|
|
231
233
|
self._client: httpx.AsyncClient | None = None
|
|
232
234
|
self._event_queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
233
235
|
self._stream_task: asyncio.Task[None] | None = None
|
|
@@ -617,11 +619,21 @@ class ChatSessionController:
|
|
|
617
619
|
)
|
|
618
620
|
return
|
|
619
621
|
if isinstance(evt, ContextSummarizing):
|
|
620
|
-
self.
|
|
622
|
+
self._on_context_usage_hint(
|
|
623
|
+
estimated_tokens=evt.estimated_tokens,
|
|
624
|
+
context_window_tokens=evt.context_window_tokens,
|
|
625
|
+
)
|
|
626
|
+
self._emit("summarizing", tokens=evt.estimated_tokens)
|
|
627
|
+
return
|
|
628
|
+
if isinstance(evt, ContextUsage):
|
|
629
|
+
self._on_context_usage_hint(
|
|
630
|
+
estimated_tokens=evt.estimated_tokens,
|
|
631
|
+
context_window_tokens=evt.context_window_tokens,
|
|
632
|
+
)
|
|
621
633
|
return
|
|
622
634
|
if isinstance(evt, ContextSummarized):
|
|
635
|
+
# Mid-turn /usage is stale; post-compaction ContextUsage updates the ring.
|
|
623
636
|
self._emit("summarized", turns=evt.turns_summarized)
|
|
624
|
-
await self._fetch_usage()
|
|
625
637
|
return
|
|
626
638
|
if isinstance(evt, AssistantDelta):
|
|
627
639
|
self._on_assistant_delta(evt, state)
|
|
@@ -644,13 +656,14 @@ class ChatSessionController:
|
|
|
644
656
|
|
|
645
657
|
self._maybe_thinking_trace(evt, request_id)
|
|
646
658
|
|
|
647
|
-
def
|
|
659
|
+
def _on_context_usage_hint(
|
|
660
|
+
self, *, estimated_tokens: int, context_window_tokens: int
|
|
661
|
+
) -> None:
|
|
648
662
|
self.usage.update_context_hint(
|
|
649
|
-
estimated_prompt_tokens=
|
|
650
|
-
context_window_tokens=
|
|
663
|
+
estimated_prompt_tokens=estimated_tokens,
|
|
664
|
+
context_window_tokens=context_window_tokens,
|
|
651
665
|
)
|
|
652
666
|
self._emit("usage_updated", usage=self.usage.usage)
|
|
653
|
-
self._emit("summarizing", tokens=evt.estimated_tokens)
|
|
654
667
|
|
|
655
668
|
def _on_assistant_delta(self, evt: AssistantDelta, state: _TurnState) -> None:
|
|
656
669
|
if not state.assistant_started:
|
|
@@ -906,6 +919,20 @@ class ChatSessionController:
|
|
|
906
919
|
await asyncio.wait_for(self._stream_task, timeout=0.5)
|
|
907
920
|
for task in list(self._cancel_tasks):
|
|
908
921
|
task.cancel()
|
|
922
|
+
if self._client is not None and self.session_id is not None:
|
|
923
|
+
try:
|
|
924
|
+
resp = await self._client.delete(f"{self.base}/sessions/{self.session_id}")
|
|
925
|
+
if resp.status_code == 200:
|
|
926
|
+
data = resp.json()
|
|
927
|
+
report = data.get("transcript_report_dir") if isinstance(data, dict) else None
|
|
928
|
+
if isinstance(report, str) and report.strip():
|
|
929
|
+
self.transcript_report_dir = report.strip()
|
|
930
|
+
except Exception:
|
|
931
|
+
logger.warning(
|
|
932
|
+
"session DELETE on close failed session_id=%s",
|
|
933
|
+
self.session_id,
|
|
934
|
+
exc_info=True,
|
|
935
|
+
)
|
|
909
936
|
if self._client is not None:
|
|
910
937
|
await self._client.aclose()
|
|
911
938
|
self._client = None
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
+
from monkeybot.core.runtime.context_budget import SUMMARY_TRIGGER_RATIO
|
|
8
|
+
|
|
7
9
|
_DIM = "\x1b[2m"
|
|
8
10
|
_GREEN = "\x1b[32m"
|
|
9
11
|
_YELLOW = "\x1b[33m"
|
|
@@ -37,7 +39,7 @@ def parse_usage_response(data: dict[str, object]) -> SessionUsageView:
|
|
|
37
39
|
cap = _int("context_window_tokens", DEFAULT_CONTEXT_WINDOW)
|
|
38
40
|
thresh = _int("summarization_threshold_tokens", 0)
|
|
39
41
|
if thresh <= 0:
|
|
40
|
-
thresh = max(1, int(cap *
|
|
42
|
+
thresh = max(1, int(cap * SUMMARY_TRIGGER_RATIO))
|
|
41
43
|
return SessionUsageView(
|
|
42
44
|
input_tokens=_int("input_tokens"),
|
|
43
45
|
output_tokens=_int("output_tokens"),
|
|
@@ -85,7 +87,7 @@ def _ring_parts(
|
|
|
85
87
|
1,
|
|
86
88
|
summarization_threshold_tokens
|
|
87
89
|
if summarization_threshold_tokens > 0
|
|
88
|
-
else int(cap *
|
|
90
|
+
else int(cap * SUMMARY_TRIGGER_RATIO),
|
|
89
91
|
)
|
|
90
92
|
pct_used = min(100, max(0, round((ring_numerator / cap) * 100)))
|
|
91
93
|
return _ring_glyph(pct_used), pct_used, ring_numerator, thresh
|