|
|
@@ -13,7 +13,7 @@ metadata:
|
|
|
|
|
|
## When to Use
|
|
|
|
|
|
-Using or developing/debugging the Chatto gateway plugin: its plugin tools (`chatto_send_dm`, `chatto_send_room`, `chatto_list_rooms`) or adding new tools to the plugin. Repo-internal rules (editing, linting, vendoring) live in the plugin root's `AGENTS.md` — see Development workflow.
|
|
|
+Using or developing/debugging the Chatto gateway plugin: its plugin tools (`chatto_send_dm`, `chatto_send_room`, `chatto_list_rooms`, `chatto_get_message`, `chatto_get_history`) or adding new tools to the plugin. Repo-internal rules (editing, linting, vendoring) live in the plugin root's `AGENTS.md` — see Development workflow.
|
|
|
|
|
|
## The plugin tools and when to use them
|
|
|
|
|
|
@@ -36,15 +36,15 @@ Resolves the room by ID or case-insensitively by name, then posts. A name shared
|
|
|
|
|
|
### chatto_get_message(room_id|room_name, event_id|event_ids, follow_reply_chain?) — read messages by ID
|
|
|
|
|
|
-Resolves the room (same `_resolve_room` as the send tools) and fetches the message(s) via `client.batch_get_messages` / `get_message`. Returns every field: body, author_user_id + author_login (hydrated via `batch_get_room_members`, best-effort — an unreadable directory costs the names, not the messages), created/updated/deleted timestamps, `in_reply_to`, `thread_root_event_id`, attachments, reactions, pinned, link preview, thread summary. Use it whenever a `reply_to=` / `in_reply_to` reference or any event ID appears in a message or session metadata.
|
|
|
+Resolves the room (same `_resolve_room` as the send tools) and fetches the message(s) — `client.get_message` for a single `event_id`, `client.batch_get_messages` for `event_ids`. Returns every field: body, author_user_id + author_login (hydrated via `batch_get_room_members`, best-effort — an unreadable directory costs the names, not the messages), created/updated/deleted timestamps, `in_reply_to`, `thread_root_event_id`, attachments (with a `video` block carrying duration, thumbnail and transcoded variants when the server sent one), reactions, pinned, link preview, thread summary. Use it whenever a `reply_to=` / `in_reply_to` reference or any event ID appears in a message or session metadata.
|
|
|
|
|
|
-- `follow_reply_chain` (0-10): walks the `in_reply_to` chain backwards (one RPC per hop) and returns the ancestors oldest-first in `reply_chain`.
|
|
|
-- `event_ids[]`: batch fetch; unknown IDs come back in `missing_event_ids` instead of failing the whole call. A single unknown `event_id` refuses cleanly.
|
|
|
+- `follow_reply_chain` (0-10): walks the `in_reply_to` chain backwards (`client.get_message` per hop) and returns the ancestors oldest-first in `reply_chain`. A single `event_id` only — a chain belongs to one message, so the tool refuses the combination rather than picking one. A hop the server cannot resolve ends the chain.
|
|
|
+- `event_ids[]`: batch fetch, answered in the order asked for; unknown IDs come back in `missing_event_ids` instead of failing the whole call. A single unknown `event_id` refuses cleanly.
|
|
|
- **Not gated on the mention lists**: reading posts nothing and answers nothing, so any room the server lets the bot see is readable — the server's visibility rules are the only gate.
|
|
|
|
|
|
### chatto_get_history(room_id|room_name, thread_root_event_id?, limit?) — read recent context
|
|
|
|
|
|
-Reads the newest messages of a room (`client.get_room_events`) or of one thread (`client.get_thread_events`), returned oldest-first (sorted by the timestamps each message carries, not the server's ordering). `limit` 1-100 (default 20), `has_older` in the result says whether more history exists. Author logins come from the timeline page's own `users_by_id` directory first; actors missing there are hydrated via `batch_get_room_members`. Same room resolution and the same thread-root rule as `chatto_send_room` (a reply inside a thread is refused as a root). Not gated on the mention lists either — the server's own visibility rules decide what the bot may read.
|
|
|
+Reads the newest messages of a room (`client.get_room_events`) or of one thread (`client.get_thread_events`), returned oldest-first (`_oldest_first`, the same ordering the thread seeding uses — the server's own ordering is not a contract). `limit` 1-100 (default 20), `has_older` in the result says whether more history exists. Author logins come from the timeline page's own `users_by_id` directory first; actors missing there are hydrated via `batch_get_room_members`. Same room resolution and the same thread-root rule as `chatto_send_room` — both go through `_resolve_thread_root`, so the two tools cannot disagree about what a root is; a thread page that omits its root gets it prepended (the root is already in hand from that check). Not gated on the mention lists either — the server's own visibility rules decide what the bot may read.
|
|
|
|
|
|
### chatto_send_dm(user_login|user_id, message) — DM a user
|
|
|
|
|
|
@@ -63,6 +63,7 @@ Looks up the user by login or ID and posts into the DM room.
|
|
|
- **Async handlers need `is_async=True`** — the default is sync; an `async def` handler registered without it returns an unawaited coroutine and the dispatcher fails with `tool_result_contract: unsupported result type: coroutine` (`tools/registry.py: dispatch()`).
|
|
|
- **Handlers must return JSON strings** (`json.dumps({...})`), never plain dicts — `_normalize_handler_result` accepts only str or the multimodal envelope. Annotate them `-> str` too: the annotation is what makes a regression to `dict` a type error instead of a runtime break.
|
|
|
- **Early exits raise `_ToolRefusal`, they do not return.** The `async with _tool_client(cfg)` context manager and the `_refuse(...)` helper keep one result shape per failure; the `_handle_*` wrapper turns a refusal back into its JSON and catches `ChattoError`/`ChattoAuthError` around it. Never `except Exception: pass` — the AGENTS.md rule about naming each recovery applies here too.
|
|
|
+- **Numeric arguments go through `_int_arg`.** Every tool argument comes from a model, so a numeric one arrives as `"20"`, `"twenty"`, `0` or `3.5`. `int()` raises on the word, and `0 or default` silently widens the answer past what was asked. `_int_arg(args, key, default, low=…, high=…)` refuses anything it cannot read instead.
|
|
|
- **Config access: never read `os.environ` in handlers.** `.env` credentials like `CHATTO_TOKEN` are profile-scoped and NOT visible in cron fires' os.environ, and `hermes_validate_config` never runs in a fresh cron session. Resolve config through `_chatto_config_for_tools()` in adapter.py: (1) `_resolved_chatto_configs[_profile_scope_key()]` — the snapshot `hermes_validate_config` installed for *this* profile, only after it accepted the config; the map is keyed by `hermes_home_key()` because Hermes multiplexes profiles in one process, so a single module-level slot would hand the tools whichever profile validated last; (2) a fresh `load_gateway_config()` read for the `chatto-platform` platform entry, so `config.yaml`'s own `extra` block is honoured, re-filled from the installed secret scope (`agent.secret_scope.current_secret_scope()`) or, when none is installed, from `build_profile_secret_scope(get_hermes_home())`, which re-reads the profile's own `.env` and works in gateway, cron and CLI alike; (3) clean error.
|
|
|
- **Never put a credential or a server URL in a tool schema.** The configured bot token must not be reachable from an argument: a `base_url` parameter would send it as `Authorization: Bearer …` to whatever host the caller names, and any message the bot reads is something the model could be talked into repeating. Take the client from `async with _tool_client(cfg) as client:` — it is the only sanctioned way to build one.
|
|
|
- **A half-delivered send says so.** The split, the thread routing and the stop-at-first-failure live in `_deliver_chunks`; `send()` turns its result into a `SendResult` and `_post_chunks` into a refusal carrying `partial: true` plus the `message_ids` that went out, so a retry repeats only what is missing. **Add a new send path by calling `_deliver_chunks`, not by writing a new loop** — two copies of the routing rules drift, and `send()` has to keep marking its own output seen (`on_posted`) so the realtime echo never comes back as input.
|