monkeybot-cli 0.11.1__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/.gitignore +3 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/PKG-INFO +1 -1
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/pyproject.toml +1 -1
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/skills/monkeybot/SKILL.md +8 -4
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_session.py +101 -4
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tui.py +91 -19
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tui_widgets.py +152 -51
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/doctor.py +13 -1
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +1 -1
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_session.py +24 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_tui.py +20 -3
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_doctor.py +6 -2
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/uv.lock +3 -2
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/skills/monkeybot/references/config-sections.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/__init__.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_file_index.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_renderer.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_theme.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/__init__.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/chat.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/loop.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/mcp_login.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/new.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/refresh.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/talk.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/trace.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/validate.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/compat.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/config_resolve.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/exit_commands.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/extras_catalog.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/gateway_health.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/main.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/output.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/overrides.txt +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/process_tree.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/providers.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/client.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/session.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/runtime_python.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/env.example +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/AGENT.md +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/command_allowlist.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/monkeybot.overrides.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/permissions.yaml +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/session_controller.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_continue.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_e2e.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_errors.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_file_index.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_grounding.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_local_shell.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_renderer_parity.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_status_bar.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_theme.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_chat_tool_display.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_cli.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_config_resolve.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_extras_catalog.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_gateway_health.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_realtime_session_controller.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_run_cmd.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_runtime_python.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_scaffold.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_terminal_markdown.py +0 -0
- {monkeybot_cli-0.11.1 → monkeybot_cli-0.13.0}/tests/test_trace_cmd.py +0 -0
|
@@ -11,6 +11,9 @@ internal/
|
|
|
11
11
|
# Local scaffolded harness config (defaults ship in monkeybot-cli scaffold_defaults/)
|
|
12
12
|
/monkeybot_config/
|
|
13
13
|
|
|
14
|
+
# Local agent for manual CLI testing. Recreate with `monkeybot new --dest ./cli-agent`.
|
|
15
|
+
/cli-agent/
|
|
16
|
+
|
|
14
17
|
# Live eval run artifacts. Baselines under regression/t4/baselines/ stay versioned.
|
|
15
18
|
/regression/t4/runs/
|
|
16
19
|
# Live-eval agent project local state. Live secrets come from the repo-root .env; a stray key
|
|
@@ -31,11 +31,15 @@ Tell the user what you're installing and why. On macOS/Linux you can run these y
|
|
|
31
31
|
### 2. Install the global CLI
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
34
|
+
# browser-harness pins Pillow 12. Core 3.12 needs Pillow 13, and
|
|
35
|
+
# `uv tool install` ignores the package override unless this file is passed.
|
|
36
|
+
# Without it the resolver stays on core 3.10.0.
|
|
37
|
+
printf 'pillow>=13\n' > /tmp/monkeybot-overrides.txt
|
|
38
|
+
uv tool install --overrides /tmp/monkeybot-overrides.txt monkeybot-cli
|
|
35
39
|
monkeybot --help
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
Upgrade later with `uv tool
|
|
42
|
+
Upgrade later with `uv tool install --reinstall --overrides /tmp/monkeybot-overrides.txt monkeybot-cli`.
|
|
39
43
|
|
|
40
44
|
**Do not** tell day-1 users to clone `human-plus-machine/monkeybot` unless they are contributing to the harness itself (see Contributing below).
|
|
41
45
|
|
|
@@ -47,7 +51,7 @@ If the user arrived via skills.sh:
|
|
|
47
51
|
npx skills add human-plus-machine/monkeybot --skill monkeybot
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
…they have **this skill only**. Walk them through Tier 0: install `uv` → `uv tool install monkeybot-cli` → verify `monkeybot --help`. Then continue to Tier 1.
|
|
54
|
+
…they have **this skill only**. Walk them through Tier 0: install `uv` → `uv tool install --overrides /tmp/monkeybot-overrides.txt monkeybot-cli` (write `pillow>=13` into that file first) → verify `monkeybot --help`. Then continue to Tier 1.
|
|
51
55
|
|
|
52
56
|
## How config works (tell the user this first)
|
|
53
57
|
|
|
@@ -115,7 +119,7 @@ uv sync
|
|
|
115
119
|
| `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
120
|
| `ollama` | Legacy auto-route (key + blank URL = cloud; explicit URL wins) | `monkeybot[ollama]` |
|
|
117
121
|
|
|
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`).
|
|
122
|
+
**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 --reinstall --overrides /tmp/monkeybot-overrides.txt --with 'monkeybot[<extra>]' monkeybot-cli`, with `pillow>=13` in that file). A bare `uv tool install` resolves core 3.10.0.
|
|
119
123
|
|
|
120
124
|
`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`).
|
|
121
125
|
|
|
@@ -241,6 +241,8 @@ class ChatSessionController:
|
|
|
241
241
|
self._reconnecting = False
|
|
242
242
|
self._last_event_id: int | None = None
|
|
243
243
|
self._abandoned: deque[str] = deque(maxlen=64)
|
|
244
|
+
self._deferred: list[Any] = []
|
|
245
|
+
self._idle_task: asyncio.Task[None] | None = None
|
|
244
246
|
self._turn_abort = asyncio.Event()
|
|
245
247
|
self._active_request_id: str | None = None
|
|
246
248
|
self._hitl_future: asyncio.Future[HitlAnswer] | None = None
|
|
@@ -292,6 +294,7 @@ class ChatSessionController:
|
|
|
292
294
|
self._reconnecting = False
|
|
293
295
|
self._last_event_id = None
|
|
294
296
|
self._stream_task = asyncio.create_task(self._consume_stream())
|
|
297
|
+
self._start_idle_watch()
|
|
295
298
|
self._emit("session_ready", session_id=self.session_id)
|
|
296
299
|
self._emit("connection_state", state="connected")
|
|
297
300
|
await self._emit_transcript_backfill()
|
|
@@ -504,6 +507,14 @@ class ChatSessionController:
|
|
|
504
507
|
return
|
|
505
508
|
|
|
506
509
|
request_id = str(uuid.uuid4())
|
|
510
|
+
if self._active_request_id is not None:
|
|
511
|
+
self._emit("session_busy")
|
|
512
|
+
return
|
|
513
|
+
await self._stop_idle_watch()
|
|
514
|
+
if self._active_request_id is not None:
|
|
515
|
+
self._emit("session_busy")
|
|
516
|
+
self._start_idle_watch()
|
|
517
|
+
return
|
|
507
518
|
self._active_request_id = request_id
|
|
508
519
|
self._last_cancel_ok = None
|
|
509
520
|
self._turn_abort.clear()
|
|
@@ -513,15 +524,23 @@ class ChatSessionController:
|
|
|
513
524
|
json={"request_id": request_id, "message": message},
|
|
514
525
|
)
|
|
515
526
|
if reply.status_code == 409:
|
|
527
|
+
self._active_request_id = None
|
|
516
528
|
self._emit("session_busy")
|
|
529
|
+
self._start_idle_watch()
|
|
517
530
|
return
|
|
518
531
|
reply.raise_for_status()
|
|
519
532
|
except httpx.HTTPError as exc:
|
|
533
|
+
self._active_request_id = None
|
|
520
534
|
self._emit("error", message=format_http_error("Reply", exc))
|
|
535
|
+
self._start_idle_watch()
|
|
521
536
|
return
|
|
522
537
|
|
|
523
538
|
self._emit("turn_started", request_id=request_id)
|
|
524
|
-
|
|
539
|
+
try:
|
|
540
|
+
await self._run_turn(request_id)
|
|
541
|
+
finally:
|
|
542
|
+
if self._active_request_id is None and not self._closed:
|
|
543
|
+
self._start_idle_watch()
|
|
525
544
|
|
|
526
545
|
async def _dequeue_payload(self) -> str | None | Literal[False]:
|
|
527
546
|
"""Return payload, None for stream end, False on poll timeout."""
|
|
@@ -544,9 +563,13 @@ class ChatSessionController:
|
|
|
544
563
|
self._warn(f"skipping unparseable event: {exc}")
|
|
545
564
|
return None
|
|
546
565
|
|
|
547
|
-
async def _run_turn(self, request_id: str) -> None:
|
|
566
|
+
async def _run_turn(self, request_id: str, *, prelude: list[Any] | None = None) -> None:
|
|
548
567
|
assert self._client is not None and self.session_id is not None
|
|
549
568
|
state = _TurnState()
|
|
569
|
+
for evt in prelude or []:
|
|
570
|
+
if state.done or self._turn_abort.is_set():
|
|
571
|
+
break
|
|
572
|
+
await self._dispatch_turn_event(evt, request_id, state)
|
|
550
573
|
while not state.done and not self._turn_abort.is_set():
|
|
551
574
|
payload = await self._dequeue_payload()
|
|
552
575
|
if payload is False:
|
|
@@ -577,6 +600,71 @@ class ChatSessionController:
|
|
|
577
600
|
if not self._stream_alive and not self._reconnecting:
|
|
578
601
|
self._emit("stream_ended")
|
|
579
602
|
|
|
603
|
+
def _start_idle_watch(self) -> None:
|
|
604
|
+
"""Render turns the gateway starts while the prompt is idle."""
|
|
605
|
+
if self._closed or self._active_request_id is not None:
|
|
606
|
+
return
|
|
607
|
+
task = self._idle_task
|
|
608
|
+
if task is not None and not task.done():
|
|
609
|
+
return
|
|
610
|
+
self._idle_task = asyncio.create_task(self._watch_idle(), name="chat-idle-watch")
|
|
611
|
+
|
|
612
|
+
async def _stop_idle_watch(self) -> None:
|
|
613
|
+
task = self._idle_task
|
|
614
|
+
if task is None or task.done():
|
|
615
|
+
self._idle_task = None
|
|
616
|
+
return
|
|
617
|
+
if self._active_request_id is not None:
|
|
618
|
+
return
|
|
619
|
+
task.cancel()
|
|
620
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
621
|
+
await task
|
|
622
|
+
self._idle_task = None
|
|
623
|
+
|
|
624
|
+
async def _watch_idle(self) -> None:
|
|
625
|
+
"""Show a background-subagent wake (or any other turn) the user did not send."""
|
|
626
|
+
try:
|
|
627
|
+
while not self._closed and self._active_request_id is None:
|
|
628
|
+
await self._play_deferred()
|
|
629
|
+
if self._closed or self._active_request_id is not None:
|
|
630
|
+
return
|
|
631
|
+
payload = await self._dequeue_payload()
|
|
632
|
+
if payload is False:
|
|
633
|
+
continue
|
|
634
|
+
if payload is None:
|
|
635
|
+
return
|
|
636
|
+
if self._active_request_id is not None:
|
|
637
|
+
self._event_queue.put_nowait(payload)
|
|
638
|
+
return
|
|
639
|
+
evt = self._decode_event(payload)
|
|
640
|
+
if evt is None:
|
|
641
|
+
continue
|
|
642
|
+
rid = getattr(evt, "request_id", "") or ""
|
|
643
|
+
if not rid or rid in self._abandoned:
|
|
644
|
+
continue
|
|
645
|
+
self._deferred.append(evt)
|
|
646
|
+
except asyncio.CancelledError:
|
|
647
|
+
raise
|
|
648
|
+
|
|
649
|
+
async def _play_deferred(self) -> None:
|
|
650
|
+
while self._deferred and not self._closed and self._active_request_id is None:
|
|
651
|
+
first = self._deferred.pop(0)
|
|
652
|
+
rid = getattr(first, "request_id", "") or ""
|
|
653
|
+
if not rid or rid in self._abandoned:
|
|
654
|
+
continue
|
|
655
|
+
batch = [first]
|
|
656
|
+
rest: list[Any] = []
|
|
657
|
+
for evt in self._deferred:
|
|
658
|
+
if getattr(evt, "request_id", "") == rid:
|
|
659
|
+
batch.append(evt)
|
|
660
|
+
else:
|
|
661
|
+
rest.append(evt)
|
|
662
|
+
self._deferred = rest
|
|
663
|
+
self._active_request_id = rid
|
|
664
|
+
self._turn_abort.clear()
|
|
665
|
+
self._emit("turn_started", request_id=rid)
|
|
666
|
+
await self._run_turn(rid, prelude=batch)
|
|
667
|
+
|
|
580
668
|
async def _dispatch_turn_event(self, evt: Any, request_id: str, state: _TurnState) -> None:
|
|
581
669
|
evt_rid = getattr(evt, "request_id", "") or ""
|
|
582
670
|
|
|
@@ -589,8 +677,9 @@ class ChatSessionController:
|
|
|
589
677
|
self._emit("thinking", text="thinking…")
|
|
590
678
|
return
|
|
591
679
|
|
|
592
|
-
if evt_rid != request_id:
|
|
593
|
-
self.
|
|
680
|
+
if evt_rid and evt_rid != request_id:
|
|
681
|
+
if evt_rid not in self._abandoned:
|
|
682
|
+
self._deferred.append(evt)
|
|
594
683
|
return
|
|
595
684
|
|
|
596
685
|
if isinstance(evt, GroundingEvent):
|
|
@@ -885,6 +974,7 @@ class ChatSessionController:
|
|
|
885
974
|
if self._active_request_id is not None:
|
|
886
975
|
self._abandoned.append(self._active_request_id)
|
|
887
976
|
self._active_request_id = None
|
|
977
|
+
await self._stop_idle_watch()
|
|
888
978
|
|
|
889
979
|
if self._stream_task is not None:
|
|
890
980
|
self._stream_task.cancel()
|
|
@@ -913,6 +1003,7 @@ class ChatSessionController:
|
|
|
913
1003
|
await self._teardown_stream()
|
|
914
1004
|
await self._create_session()
|
|
915
1005
|
self._stream_task = asyncio.create_task(self._consume_stream())
|
|
1006
|
+
self._start_idle_watch()
|
|
916
1007
|
self._emit("session_ready", session_id=self.session_id)
|
|
917
1008
|
self._emit("connection_state", state="connected")
|
|
918
1009
|
|
|
@@ -926,6 +1017,7 @@ class ChatSessionController:
|
|
|
926
1017
|
await self._teardown_stream()
|
|
927
1018
|
await self._attach_or_create_session(sid)
|
|
928
1019
|
self._stream_task = asyncio.create_task(self._consume_stream())
|
|
1020
|
+
self._start_idle_watch()
|
|
929
1021
|
self._emit("session_ready", session_id=self.session_id)
|
|
930
1022
|
self._emit("connection_state", state="connected")
|
|
931
1023
|
await self._emit_transcript_backfill()
|
|
@@ -942,6 +1034,11 @@ class ChatSessionController:
|
|
|
942
1034
|
if self._closed:
|
|
943
1035
|
return
|
|
944
1036
|
self._closed = True
|
|
1037
|
+
if self._idle_task is not None:
|
|
1038
|
+
self._idle_task.cancel()
|
|
1039
|
+
with contextlib.suppress(asyncio.CancelledError, asyncio.TimeoutError, TimeoutError):
|
|
1040
|
+
await asyncio.wait_for(self._idle_task, timeout=0.5)
|
|
1041
|
+
self._idle_task = None
|
|
945
1042
|
if self._stream_task is not None:
|
|
946
1043
|
self._stream_task.cancel()
|
|
947
1044
|
with contextlib.suppress(asyncio.CancelledError, asyncio.TimeoutError, TimeoutError):
|
|
@@ -57,6 +57,7 @@ from monkeybot_cli.chat_tui_widgets import (
|
|
|
57
57
|
ToolCallBlock,
|
|
58
58
|
TranscriptPane,
|
|
59
59
|
UserTurn,
|
|
60
|
+
activity_label,
|
|
60
61
|
write_osc52_clipboard,
|
|
61
62
|
)
|
|
62
63
|
from monkeybot_cli.config_resolve import load_config_doc, resolve_config
|
|
@@ -360,6 +361,7 @@ class ChatApp(App[int]):
|
|
|
360
361
|
self._assistant: AssistantTurn | None = None
|
|
361
362
|
self._thinking: ThinkingLine | None = None
|
|
362
363
|
self._thinking_trace: ThinkingTrace | None = None
|
|
364
|
+
self._status_note = ""
|
|
363
365
|
self._hitl_card: HitlCard | None = None
|
|
364
366
|
self._hitl_status_flash: str | None = None
|
|
365
367
|
self._ring_text = "[dim]○ 0%[/]"
|
|
@@ -433,7 +435,10 @@ class ChatApp(App[int]):
|
|
|
433
435
|
if self._hitl_status_flash:
|
|
434
436
|
parts.append(self._hitl_status_flash)
|
|
435
437
|
if self._turn_active and not self._hitl_active:
|
|
436
|
-
|
|
438
|
+
if self._status_note:
|
|
439
|
+
parts.append(f"{self._status_note} · Esc interrupt")
|
|
440
|
+
else:
|
|
441
|
+
parts.append("working · Esc interrupt")
|
|
437
442
|
elif self._hitl_active:
|
|
438
443
|
if self._hitl_kind == "elicit":
|
|
439
444
|
parts.append("Enter submit · Ctrl-C cancel")
|
|
@@ -647,8 +652,24 @@ class ChatApp(App[int]):
|
|
|
647
652
|
transcript.mount(earlier, before=0)
|
|
648
653
|
earlier.absorb(digest)
|
|
649
654
|
|
|
655
|
+
def _mark_activity_lead(self, widget: object) -> None:
|
|
656
|
+
"""First row of a thought/tool/search stack gets the gap above it."""
|
|
657
|
+
if not isinstance(widget, (ThinkingTrace, ToolCallBlock, GroundingBlock)):
|
|
658
|
+
return
|
|
659
|
+
prev = None
|
|
660
|
+
for child in reversed(self._transcript().children):
|
|
661
|
+
if child is widget or getattr(child, "id", None) == "empty-hint":
|
|
662
|
+
continue
|
|
663
|
+
if isinstance(child, ThinkingTrace) and not child._raw.strip():
|
|
664
|
+
continue
|
|
665
|
+
prev = child
|
|
666
|
+
break
|
|
667
|
+
if not isinstance(prev, (ThinkingTrace, ToolCallBlock, GroundingBlock)):
|
|
668
|
+
widget.add_class("-lead")
|
|
669
|
+
|
|
650
670
|
def _mount(self, widget: object) -> None:
|
|
651
671
|
self._hide_empty_hint()
|
|
672
|
+
self._mark_activity_lead(widget)
|
|
652
673
|
if self._is_counted_turn(widget):
|
|
653
674
|
self._trim_old_turns()
|
|
654
675
|
self._turn_count += 1
|
|
@@ -673,10 +694,33 @@ class ChatApp(App[int]):
|
|
|
673
694
|
self._thinking.remove()
|
|
674
695
|
self._thinking = None
|
|
675
696
|
|
|
697
|
+
def _drop_empty_thinking(self) -> None:
|
|
698
|
+
trace = self._thinking_trace
|
|
699
|
+
if trace is None or not trace.is_attached or trace._raw.strip():
|
|
700
|
+
return
|
|
701
|
+
already = bool(getattr(trace, "_trimmed", False))
|
|
702
|
+
trace._trimmed = True # type: ignore[attr-defined]
|
|
703
|
+
trace.remove()
|
|
704
|
+
self._thinking_trace = None
|
|
705
|
+
if not already:
|
|
706
|
+
self._turn_count = max(0, self._turn_count - 1)
|
|
707
|
+
|
|
708
|
+
def _ensure_live_thinking(self) -> None:
|
|
709
|
+
trace = self._thinking_trace
|
|
710
|
+
if trace is not None and trace.is_attached and not trace.has_class("-done"):
|
|
711
|
+
return
|
|
712
|
+
self._clear_thinking()
|
|
713
|
+
block = ThinkingTrace()
|
|
714
|
+
self._thinking_trace = block
|
|
715
|
+
self._mount(block)
|
|
716
|
+
|
|
676
717
|
def _finish_thinking_trace(self, *, clear: bool = False) -> None:
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
718
|
+
trace = self._thinking_trace
|
|
719
|
+
if trace is not None and trace.is_attached:
|
|
720
|
+
if not trace._raw.strip():
|
|
721
|
+
self._drop_empty_thinking()
|
|
722
|
+
elif not trace.has_class("-done"):
|
|
723
|
+
trace.finish()
|
|
680
724
|
if clear:
|
|
681
725
|
self._thinking_trace = None
|
|
682
726
|
|
|
@@ -973,7 +1017,11 @@ class ChatApp(App[int]):
|
|
|
973
1017
|
self._follow_scroll()
|
|
974
1018
|
|
|
975
1019
|
def _ev_thinking(self, p: dict) -> None:
|
|
976
|
-
|
|
1020
|
+
text = str(p.get("text") or "thinking…")
|
|
1021
|
+
if text.strip().lower() in {"thinking…", "thinking...", "thinking"}:
|
|
1022
|
+
self._ensure_live_thinking()
|
|
1023
|
+
return
|
|
1024
|
+
self._show_thinking(text)
|
|
977
1025
|
|
|
978
1026
|
def _ev_thinking_clear(self, _p: dict) -> None:
|
|
979
1027
|
self._clear_thinking()
|
|
@@ -981,13 +1029,16 @@ class ChatApp(App[int]):
|
|
|
981
1029
|
def _ev_turn_started(self, _p: dict) -> None:
|
|
982
1030
|
self._turn_active = True
|
|
983
1031
|
self._assistant = None
|
|
1032
|
+
self._status_note = ""
|
|
984
1033
|
self._seal_thinking_trace()
|
|
985
1034
|
self._clear_open_tools()
|
|
986
|
-
self.
|
|
1035
|
+
self._clear_thinking()
|
|
1036
|
+
self._ensure_live_thinking()
|
|
987
1037
|
self._refresh_status()
|
|
988
1038
|
|
|
989
1039
|
def _ev_assistant_start(self, _p: dict) -> None:
|
|
990
1040
|
self._clear_thinking()
|
|
1041
|
+
self._drop_empty_thinking()
|
|
991
1042
|
|
|
992
1043
|
def _ev_assistant_delta(self, p: dict) -> None:
|
|
993
1044
|
delta = str(p.get("delta", ""))
|
|
@@ -1027,30 +1078,49 @@ class ChatApp(App[int]):
|
|
|
1027
1078
|
|
|
1028
1079
|
def _ev_summarizing(self, p: dict) -> None:
|
|
1029
1080
|
tokens = int(p.get("tokens") or 0)
|
|
1030
|
-
self.
|
|
1081
|
+
self._drop_empty_thinking()
|
|
1082
|
+
self._clear_thinking()
|
|
1083
|
+
self._status_note = f"summarizing context ({tokens:,} tokens)"
|
|
1084
|
+
self._refresh_status()
|
|
1031
1085
|
|
|
1032
1086
|
def _ev_summarized(self, p: dict) -> None:
|
|
1033
1087
|
turns = int(p.get("turns") or 0)
|
|
1034
1088
|
noun = "turn" if turns == 1 else "turns"
|
|
1035
|
-
self.
|
|
1089
|
+
self._status_note = f"summarized {turns} {noun}"
|
|
1090
|
+
self._refresh_status()
|
|
1036
1091
|
|
|
1037
1092
|
def _ev_grounding(self, p: dict) -> None:
|
|
1038
1093
|
self._clear_thinking()
|
|
1094
|
+
self._drop_empty_thinking()
|
|
1039
1095
|
queries = p.get("search_queries") or []
|
|
1040
|
-
|
|
1041
|
-
if
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
continue
|
|
1096
|
+
raw_sources = p.get("sources") or []
|
|
1097
|
+
sources = [source for source in raw_sources if isinstance(source, dict)]
|
|
1098
|
+
lines: list[str] = []
|
|
1099
|
+
shown = 0
|
|
1100
|
+
linked = 0
|
|
1101
|
+
for source in sources:
|
|
1047
1102
|
title = str(source.get("title") or "").strip()
|
|
1048
1103
|
uri = str(source.get("uri") or "").strip()
|
|
1049
1104
|
if not uri:
|
|
1050
1105
|
continue
|
|
1106
|
+
linked += 1
|
|
1107
|
+
if shown >= 5:
|
|
1108
|
+
continue
|
|
1109
|
+
shown += 1
|
|
1051
1110
|
label = title or uri
|
|
1052
1111
|
lines.append(f"- [{label}]({uri})")
|
|
1053
|
-
|
|
1112
|
+
query_text = ", ".join(str(query).strip() for query in queries if str(query).strip())
|
|
1113
|
+
if query_text and linked:
|
|
1114
|
+
noun = "source" if linked == 1 else "sources"
|
|
1115
|
+
hint = f"{query_text} · {linked} {noun}"
|
|
1116
|
+
elif query_text:
|
|
1117
|
+
hint = query_text
|
|
1118
|
+
elif linked:
|
|
1119
|
+
noun = "source" if linked == 1 else "sources"
|
|
1120
|
+
hint = f"{linked} {noun}"
|
|
1121
|
+
else:
|
|
1122
|
+
hint = ""
|
|
1123
|
+
self._mount(GroundingBlock("\n".join(lines), title=activity_label("Web", hint)))
|
|
1054
1124
|
|
|
1055
1125
|
def _ev_turn_error(self, p: dict) -> None:
|
|
1056
1126
|
self._close_assistant()
|
|
@@ -1157,9 +1227,10 @@ class ChatApp(App[int]):
|
|
|
1157
1227
|
def _ev_stream_ended(self, _p: dict) -> None:
|
|
1158
1228
|
self._exit_code = 1 if self._controller.stream_error else self._exit_code
|
|
1159
1229
|
|
|
1160
|
-
def _ev_thinking_trace(self,
|
|
1161
|
-
#
|
|
1162
|
-
|
|
1230
|
+
def _ev_thinking_trace(self, _p: dict) -> None:
|
|
1231
|
+
# thinking_block_* owns the collapsed row. This legacy event would
|
|
1232
|
+
# print the trace inline.
|
|
1233
|
+
return
|
|
1163
1234
|
|
|
1164
1235
|
def _ev_thinking_block_delta(self, p: dict) -> None:
|
|
1165
1236
|
text = str(p.get("text") or "")
|
|
@@ -1230,6 +1301,7 @@ class ChatApp(App[int]):
|
|
|
1230
1301
|
def _finish_turn_ui(self) -> None:
|
|
1231
1302
|
self._turn_active = False
|
|
1232
1303
|
self._assistant = None
|
|
1304
|
+
self._status_note = ""
|
|
1233
1305
|
self._clear_open_tools()
|
|
1234
1306
|
self._refresh_status()
|
|
1235
1307
|
self.query_one("#prompt", Composer).focus()
|