X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/f80b058ae0550a56b4e62f8e65c2a74349b970e4..f2e67d5579593e2d4f1cb2d9165b851c52a06715:/README.rst diff --git a/README.rst b/README.rst index c9aaaee..34d0d49 100644 --- a/README.rst +++ b/README.rst @@ -78,16 +78,38 @@ Start the daemon, and then the client in another terminal: The daemon owns the root agent in ``agent/`` in the data directory and listens on the Unix socket ``onechat.sock`` there. In the client, each line is a message to the agent, the line ``/stop`` interrupts the -running turn, and Ctrl-D ends the client. The client shows the replies, -the tool calls, and the first lines of each tool result as they happen. -It also shows each message that the user sends from another client, with -the client as a mark, such as ``[telegram] text``. The daemon is the -only process that uses the token file while it runs, so ``onechat -login`` and ``onechat probe`` refuse to start until it stops. +running turn, the line ``/stop NAME`` stops the subagent NAME, and +Ctrl-D ends the client. The client shows the replies, the tool calls, +and the first lines of each tool result as they happen. It also shows +each message that the user sends from another client, with the client as +a mark, such as ``[telegram] text``. When a subagent starts, the client +shows ``[NAME] started``, and it shows the whole report of the subagent, +which starts with ``[NAME]``, but not the tool calls of the subagent. +The daemon is the only process that uses the token file while it runs, +so ``onechat login`` and ``onechat probe`` refuse to start until it +stops. The agent runs commands with bash as the user in its working directory, with no sandbox. It has the rights of the user, also on the token file. +When the user asks for it, the agent starts subagents for long tasks. +Each subagent has a name, such as ``GreenOtter``, and runs in the +background while the agent answers other messages. It gets the view of +the agent and its task, and it runs its tools as the agent does, in +``agent/subagents/NAME/work``. A task in a repository gives the path of +the repository. The steps of a subagent go to its own log in +``agent/subagents/NAME``, not to the chat, and its final reply comes +back to the agent as one message that starts with ``[NAME]``. The agent +can send a subagent more messages: a running subagent reads them between +its tool calls, and a finished one goes on from its log, and its reply +comes back in the same way. When the user stops a subagent, the chat +gets a note, and the report of the subagent is logged, but it starts no +turn. Each model call of a subagent has the name in the ``agent`` field +of ``agent/usage.jsonl``. ``agent/work.json`` holds the names of the +running subagents. A subagent that runs when the daemon stops goes on at +the next start: the chat gets the note that the daemon restarted, and +the subagent gets the same note and goes on from its log. + ``agent/config.json`` holds the settings of the agent. The first start writes it with the defaults and a random ID. Do not change the ID: the prompt cache key comes from it. @@ -130,15 +152,18 @@ new messages, so it needs no open port. {"token": "123456:ABC-token-from-BotFather", "owner": 123456789} The bot reads only the private chat of the owner and ignores all other -updates. Each text message goes to the agent as a user message, and the -line ``/stop`` interrupts the running turn. The bot sends the replies in -parts of at most 4096 characters, with their Markdown as Telegram -formats: bold, italic, strikethrough, code, code blocks, links, and -headings as bold text. When Telegram refuses the formats of a part, the -bot sends the part again as plain text. Each error goes as a short -message in plain text. It shows the typing action while a turn runs. It -does not send the tool calls. It sends each message that the user types -in the terminal client, with the mark ``[terminal]``. +updates. Each text message goes to the agent as a user message, the line +``/stop`` interrupts the running turn, and the line ``/stop NAME`` stops +the subagent NAME. The bot sends the replies in parts of at most 4096 +characters, with their Markdown as Telegram formats: bold, italic, +strikethrough, code, code blocks, links, and headings as bold text. When +Telegram refuses the formats of a part, the bot sends the part again as +plain text. Each error goes as a short message in plain text. It shows +the typing action while a turn runs. It does not send the tool calls. +For a subagent it sends only ``[NAME] started`` and ``[NAME] finished``, +``[NAME] stopped``, or ``[NAME] failed``, because the reply of the agent +carries the content of the report. It sends each message that the user +types in the terminal client, with the mark ``[terminal]``. ``agent/telegram-offset.json`` holds the ID of the next update. The bot moves it past an update only after the message is durable in the log, so @@ -147,6 +172,107 @@ write of a message can make the message repeat once. A network failure or a rate limit makes the bot wait and try again, and does not stop the daemon. +********** + WhatsApp +********** + +The daemon connects to the WhatsApp account of the user as a linked +device. The terminal client and Telegram take three commands: + +``/whatsapp pair PHONE`` + Links the account of PHONE, the number with its country code, such as + ``+15550001111``. The daemon answers with the steps and then with a + pairing code in its own message: in WhatsApp on the phone, open + Linked devices, tap Link a device, then tap Link with phone number + instead, and enter the code. The code expires in about 2 minutes, and + the same command gives a new code. When the link succeeds, the daemon + answers with the linked number. + +``/whatsapp status`` + Shows the link: not linked, linked and connected, or linked and + disconnected, with the times of the connection. + +``/whatsapp unlink`` + Ends the link and deletes the session. When WhatsApp is disconnected, + the phone does not get the logout, so remove the device in Linked + devices on the phone. + +The answers go only to the client that asked, and the commands do not go +to the agent, so the agent never gets the phone number or the code. The +chat gets only the notes that WhatsApp is linked or unlinked. + +The session is ``whatsapp/session.db`` in the data directory, with mode +0600 in a directory with mode 0700. It gives full access to the WhatsApp +account, so the daemon refuses the file when its mode is not 0600. The +shell of the agent runs as the user, so it can read the session. If the +session leaks, remove the linked device in WhatsApp on the phone, and +the session stops working. Without the file, WhatsApp is not linked. + +The daemon reads the text messages of the private chats and the groups, +from other people and from the user. It does not read media, reactions, +edits, deletions, view-once and disappearing messages, or the history +from before the link. The messages of one chat form one batch until the +chat is quiet for 20 seconds, for 2 minutes or 50 messages at the most. +Each batch goes to the chat of the agent as one work message that starts +with ``[WhatsApp]``, with the name and the ID of the chat, and the time, +the sender, and the text of each message. The messages of the user show +as ``You``. ``whatsapp/state.json``, with mode 0600, keeps the batches +that are not in the chat yet, so they survive a restart, and the records +of the sends. A crash just after a batch goes to the chat can make the +batch repeat once. + +A batch of a private chat with a message of another person starts a +turn, and so does a batch of a group with a message that mentions the +user or replies to a message of the user. The other batches go to the +chat with no turn, and the agent sees them at its next turn. A batch +that starts a turn ends with a line that tells the agent to end its turn +with an empty reply when nothing in the batch needs the user. No client +shows an empty reply, so such a batch gives no message in Telegram. The +terminal client shows each batch, and Telegram shows none. + +The phone number of the user never goes to the chat: a mention of the +user shows as ``@you``, the number shows as ``OWN``, and the chat of the +user with itself is not read. But the IDs of the other chats go to the +chat and to the model, because the agent needs them to send. The ID of a +private chat holds the phone number of the contact, such as +``15550002222@s.whatsapp.net``, and the ID of a group can hold the +number of the person who made it. + +While the daemon is stopped or disconnected, the WhatsApp servers keep +the messages for the linked device and send them at the next connection. +They keep them only for a limited time, so a long stop can lose +messages. These messages form batches by their original times, as live +messages do. The daemon waits for the end of this catch-up, or one +minute at the most, and then gives all its batches to the agent in one +turn, not one turn for each chat. The batches of ``whatsapp/state.json`` +at the start wait for the first catch-up in the same way. + +The agent finds a chat with the tool ``whatsapp_find``: it gives the +contacts and the groups whose name holds a text, with no regard to case +and accents, with their IDs. The names of the contacts come from the +session, and the names of the groups from WhatsApp, kept in memory for +one hour. The names arrive from the phone some minutes after the link, +and until then the tool says that they are not ready. The agent sends a +text message with the tool ``whatsapp_send``, only when the user asks +for it. Each send has an ID that the agent gives: a repeated call with +the same ID gives the answer of the first call and sends nothing. The +state file keeps the last 1000 send records. When the daemon stops +during a send, the result is not known, and the tool tells the agent to +ask the user to look at the chat in WhatsApp. Without a link, both tools +tell the agent to ask the user to run ``/whatsapp pair PHONE``. Only the +agent has the WhatsApp tools, not its subagents. + +Each batch of a group goes to the chat, also with no turn, and the +memory tree summarizes it with model calls, which use the quota. + +When the session holds a linked device, the daemon connects at its +start, and after a network failure it connects again by itself. When a +disconnection lasts 10 minutes, all clients get one notice, and one more +when the connection comes back. When WhatsApp ends the connection for +good, for example because another program uses the same session, all +clients get a notice at once. When the user removes the device in the +phone, the daemon deletes the session and tells all clients one time. + ******* Probe ******* @@ -203,3 +329,6 @@ Format and check all files: bin/format bin/check nix flake check + +``bin/check`` and ``nix build`` run a short replay of the synthetic log. +``nix flake check`` also runs the full replay of 100,000 messages.