diff --git a/CLAUDE.md b/CLAUDE.md index 000cb11..192d8c5 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ The shared loop lives in `Agent._loop()` (`app/core/agent.py`). Subclasses overr | `_on_no_choices` | raise | exponential backoff | raise | | `_should_stop` | — | `channel.has_stopped` | — | -Tool calls within a single LLM turn are dispatched in parallel via `asyncio.gather`. After each turn, the full message chain (assistant tool-call message + tool results + final response) is saved to `self.messages` with tool results truncated to `TOOL_RESULT_HISTORY_LIMIT` chars to keep context lean. `MessageHistory` (SQLite) stores only user + final assistant text for cross-session persistence. +Tool calls within a single LLM turn are dispatched in parallel via `asyncio.gather`. After each turn, the full message chain (assistant tool-call message + tool results + final response) is saved to `self.messages` at full length. `MessageHistory` (SQLite) stores only user + final assistant text for cross-session persistence. ### Tool System @@ -104,7 +104,7 @@ On startup, `load_system_context()` (`app/infra/startup.py`) loads `app/core/sys **Channel types** are defined in `ChannelType` enum (`app/channels/channel.py`): `CLI`, `TELEGRAM`, `DISCORD`, `WEB`. Each channel implements the `Channel` ABC and owns a `MessageQueue` instance. `bg_server.py` wires up enabled channels — each gets its own `MessageQueue`, `BackgroundAgent`, and set of coroutines (`run_polling`, `process_incoming`, `process_outgoing`) gathered into the event loop. -**WebChannel** (`app/channels/web_channel.py`) uses `python-fasthtml` + uvicorn to serve both a browser chat UI (`GET /`) and a JSON WebSocket endpoint (`WS /ws`) on the same port. Multiple concurrent browser tabs are supported — each connection gets a UUID tracked in `_connections`. A per-connection `asyncio.Lock` in `_send_locks` serializes writes to each WebSocket. The channel also exposes `GET /api/conversations`, `GET /api/messages` (scoped to web channel only), and `GET /api/status` REST endpoints. Only `/whoami` is handled inline (it needs the per-connection client ID); all other slash commands — including `/help`, `/status`, `/stop` — are forwarded to `BackgroundAgent`'s `CommandRegistry` via the message queue with `is_command=True` in metadata so `send_message()` emits `{"type":"system"}` responses, enabling the sidebar to refresh after conversation-mutating commands. +**WebChannel** (`app/channels/web_channel.py`) uses `python-fasthtml` + uvicorn to serve both a browser chat UI (`GET /`) and a JSON WebSocket endpoint (`WS /ws`) on the same port. Multiple concurrent browser tabs are supported — each connection gets a UUID tracked in `_connections`. A per-connection `asyncio.Lock` in `_send_locks` serializes writes to each WebSocket. The channel also exposes `GET /api/conversations`, `GET /api/messages` (scoped to web channel only), `GET /api/status`, and `POST /api/upload` REST endpoints. `POST /api/upload` accepts multipart `files` (one or more), writes each under `$ANOTHERBOT_HOME/uploads` with a UUID prefix, and returns the stored basenames; the browser (paperclip button next to the input) sends those basenames back in the WebSocket `message` frame's `files` field. `_resolve_upload_paths()` joins each basename to the upload dir (basename-only, so client paths can't traverse out) and the resolved absolute paths reach the agent via `metadata["files"]`, which `Agent._build_user_message()` encodes as image/file parts. Only `/whoami` is handled inline (it needs the per-connection client ID); all other slash commands — including `/help`, `/status`, `/stop` — are forwarded to `BackgroundAgent`'s `CommandRegistry` via the message queue with `is_command=True` in metadata so `send_message()` emits `{"type":"system"}` responses, enabling the sidebar to refresh after conversation-mutating commands. **Slash commands** are handled entirely by `BackgroundAgent`. Channel handlers (`command_handler` in Telegram, `on_message` in Discord) intercept only `/whoami` (resolved inline using the platform user object) and enqueue everything else as a plain `IncomingMessage`. `BackgroundAgent.process_incoming()` detects the leading `/` and dispatches via its own `CommandRegistry`. That registry owns the full command set: `/model`, `/status`, `/stop`, `/help`, `/list`, `/new`, `/load`, `/fork`, `/rename`, `/export`. diff --git a/app/__init__.py b/app/__init__.py old mode 100755 new mode 100644 diff --git a/app/channels/commands.py b/app/channels/commands.py index fb8d9c5..f469d89 100644 --- a/app/channels/commands.py +++ b/app/channels/commands.py @@ -69,6 +69,20 @@ async def model_cmd(args: str = "") -> str: return f"Model set to: {args.strip()}" +async def trace_cmd(args: str = "") -> str: + arg = args.strip().lower() + tracedir = runtime.get("tracedir") + if arg == "on": + runtime.set("trace", True) + return f"Tracing on. Writing to {tracedir}" + elif arg == "off": + runtime.set("trace", False) + return "Tracing off." + else: + state = runtime.get("trace", False) + return f"Tracing is {'on' if state else 'off'}. Dir: {tracedir}" + + def make_status_cmd(channel_str: str = "") -> CommandHandler: async def _status(args: str = "") -> str: uptime = datetime.now() - _STARTUP_TIME @@ -81,11 +95,15 @@ async def _status(args: str = "") -> str: else: conv_id = runtime.get("conversation_id", "—") conv_name = runtime.get("conversation_name", "—") + tracing = runtime.get("trace", False) + last_trace = runtime.get("last_trace") + trace_line = f"on ({last_trace})" if (tracing and last_trace) else ("on" if tracing else "off") return ( f"Bot status:\n" f" Model: {model}\n" f" Uptime: {hours}h {minutes}m {seconds}s\n" - f" Conversation: [{conv_id}] {conv_name}" + f" Conversation: [{conv_id}] {conv_name}\n" + f" Tracing: {trace_line}" ) return _status @@ -225,7 +243,6 @@ async def _mcp(args: str = "") -> str: desc = fn.get("description", "") lines.append(f" {bare}" + (f" — {desc}" if desc else "")) return "\n".join(lines) - return "\n".join(lines) else: specs = mcp_manager.get_tool_specs() if not specs: diff --git a/app/channels/static/web_channel.css b/app/channels/static/web_channel.css index 942dfbb..94b9069 100644 --- a/app/channels/static/web_channel.css +++ b/app/channels/static/web_channel.css @@ -318,3 +318,82 @@ body { #send-btn:hover { background: var(--accent-h); } #send-btn:active { transform: scale(.97); } #send-btn:disabled { background: var(--border); cursor: not-allowed; } + +/* Hide the file input without display:none — keeps it in the layout/event + tree so its `change` event fires reliably (Edge) when opened via the label. */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ---- attach button (a