X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/4b063ca25798c78cc03d1d07f67b02c94c95248d..7b8d8e8f4b57bd51fd6d0f0d926b821f06b0e1f0:/README.rst?ds=sidebyside diff --git a/README.rst b/README.rst index 4bda34e..bbb4475 100644 --- a/README.rst +++ b/README.rst @@ -3,31 +3,23 @@ ######### OneChat is one AI agent in a single chat that never ends. A daemon owns -the agent and its files, and a terminal client and a Telegram bot talk -to it. It is in early development. Linux is supported. +the agent and its memory, and you talk to it from a terminal client, +Telegram, and through your WhatsApp chats. It is in early development. +Linux is supported. -**************** - Data directory -**************** - -OneChat keeps its data and its token file in one directory. It uses the -first of these: - -#. ``ONECHAT_DIR``, when it is set. -#. ``$XDG_DATA_HOME/onechat``, when ``XDG_DATA_HOME`` is an absolute - path. A relative value is ignored. -#. ``~/.local/share/onechat``. With no home directory, OneChat exits - with an error. +******* + Setup +******* -OneChat creates the directory with mode 0700. +Data directory +============== -*************** - Configuration -*************** +OneChat keeps all its data in one directory, with mode 0700. It uses +``ONECHAT_DIR`` when it is set, else ``$XDG_DATA_HOME/onechat``, else +``~/.local/share/onechat``. The development shell sets ``ONECHAT_DIR`` +to ``.devenv/onechat`` in the checkout. -``config.json`` in the data directory holds the settings. OneChat does -not create it. A missing file or field takes the default, and an unknown -field is an error. +Optional settings go in ``config.json`` in the data directory: .. code:: json @@ -36,167 +28,221 @@ field is an error. "compactor": {"model": "gpt-6.1-sol", "effort": "high"} } -``endpoint`` - ``chatgpt`` (the default) sends model calls to the ChatGPT backend - with the subscription login. ``api`` sends them to the OpenAI API - with the key in ``OPENAI_API_KEY``, which must then be set. With - ``chatgpt``, OneChat ignores ``OPENAI_API_KEY``. - -``compactor`` - The model and the reasoning effort of the summaries. The defaults are - ``gpt-6.1-sol`` and ``high``. - -******* - Login -******* +``endpoint`` is ``chatgpt`` (the ChatGPT subscription, the default) or +``api`` (the OpenAI API, with ``OPENAI_API_KEY`` set). ``compactor`` is +the model and effort of the memory summaries. -The ``chatgpt`` endpoint needs a login with a ChatGPT account: +Login +===== .. code:: console onechat login -The command shows a link and a code. Open the link on any device, enter -the code, and approve the login. Device code login must be allowed in -the ChatGPT security settings. OneChat writes the tokens to -``auth.json`` in the data directory, with mode 0600, and refreshes them -before they expire. The file belongs to OneChat only. Do not share it -with the Codex CLI, because each program rotates the refresh token and -logs the other out. +Open the link on any device and enter the code. Device code login must +be allowed in the ChatGPT security settings. Do not share the login with +the Codex CLI: each program logs the other out. -******* - Agent -******* +Telegram +======== -Start the daemon, and then the client in another terminal: +#. Make a bot with BotFather and keep its token. +#. Find your numeric Telegram user ID. +#. Write both to ``telegram.json`` in the data directory, with mode + 0600: -.. code:: console +.. code:: json - onechat daemon - onechat chat + {"token": "123456:ABC-token-from-BotFather", "owner": 123456789} -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. +Start the daemon and write to the bot in a private chat. It answers only +you. -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. +WhatsApp +======== -``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. +Link your WhatsApp account as a linked device from the terminal client +or Telegram: -.. code:: json +``/whatsapp pair PHONE`` + Gives a pairing code. On the phone, open Linked devices, Link a + device, Link with phone number instead, and enter the code. - { - "name": "OneChat", - "id": "0123456789abcdef0123456789abcdef", - "model": "gpt-6.1-sol", - "effort": "medium", - "workdir": "/home/user" - } +``/whatsapp status`` + Shows the link and the routes of chats to child agents. -``workdir`` is an absolute path, and its default is the home directory. -``agent/instructions.md`` holds the user's instructions: who they are, -how their files are organized, and how they want work done. The prompt -ends with them, and a change applies at the next call. +``/whatsapp unlink`` + Ends the link. -********** - Telegram -********** +The session file gives full access to the account. If it leaks, remove +the device in Linked devices on the phone. -The daemon also connects the agent to a Telegram bot when -``telegram.json`` exists in the data directory. It polls the Bot API for -new messages, so it needs no open port. +User service +============ -#. Make a bot with BotFather and keep its token. +Run the daemon as a systemd user service. Keep the ``result-service`` +link, so that Nix keeps the package: -#. Find your numeric Telegram user ID. +.. code:: console -#. Write the token and the ID to ``telegram.json``, and give the file - mode 0600, because the token is a secret. The daemon refuses the file - with any other mode. + nix build --out-link result-service + systemctl --user link --force "$PWD/result-service/share/systemd/user/onechat.service" + systemctl --user edit onechat.service -#. Start the daemon, open a private chat with the bot, and write. +In the editor, set the data directory, and the tools that the agent may +use in its shell: -.. code:: json +.. code:: ini - {"token": "123456:ABC-token-from-BotFather", "owner": 123456789} + [Service] + Environment="ONECHAT_DIR=DATA_DIR" + Environment="PATH=TOOLS_PATH" -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 as -plain text, in parts of at most 4096 characters, and each error as a -short message. 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]``. - -``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 -a restart of the daemon does not lose a message. A crash just after the -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. +Then start it, and let it run while you are logged out: -******* - Probe -******* +.. code:: console -``onechat probe LOG COUNT [BUILDS]`` measures the compactor on a log. It -appends the first COUNT messages of LOG, one JSON object ``{"kind", -"text"}`` on each line, to a new chat in ``probe/`` in the data -directory, and builds its tree with real model calls. The calls use the -quota of the account. The run stops at the first failed call, after 660 -calls, or when the used percent of a quota window grows by more than 15 -points. At the end it reports the calls per message, the cached share of -the input tokens, the retries, the line sizes, and the quota at the -first and the last call. BUILDS, from 1 to 8, limits the model calls at -once in the run. + systemctl --user daemon-reload + systemctl --user enable --now onechat.service + loginctl enable-linger "$USER" -A test writes a generated log of readable made-up messages: +After a ``git pull``, ``bin/service-update`` builds and restarts it. +Never run two daemons on the same data. + +Backups +======= .. code:: console - ONECHAT_PROBE_LOG=probe.jsonl go test -run TestProbeLogHasTheSyntheticMix + onechat backup BACKUP_DIR/onechat.tar.gz + ONECHAT_DIR=EMPTY_DIR onechat restore BACKUP_DIR/onechat.tar.gz -************* - Development -************* +A backup runs while the daemon runs. Restore needs a stopped daemon and +an empty directory. **An archive holds your logins, the bot token, and +the WhatsApp link**, so keep it private. -Nix supplies the Go toolchain and all tools. Enter the development shell -with direnv: +For a daily backup with the last 14 kept, save this script, with +``BACKUP_DIR`` replaced by a private directory outside the data +directory: + +.. code:: sh + + #!/bin/sh + set -eu + umask 077 + cd BACKUP_DIR + onechat backup "onechat-$(date +%F).tar.gz" + ls -1 onechat-????-??-??.tar.gz | sort -r | tail -n +15 | xargs -r rm -- + +and run it from a ``Type=oneshot`` user service with the same +``ONECHAT_DIR`` and an ``OnCalendar=daily`` timer. + +********* + Talking +********* .. code:: console - direnv allow + onechat + +``onechat`` connects to the running daemon. Each line is a message, and +replies show with Markdown styles and tables. These lines are commands: + +``/stop`` + Interrupts the running turn. + +``/stop NAME`` + Stops the subagent NAME. + +``/image PATH [caption]`` + Sends a JPEG, PNG, or WebP image. Quote a path with spaces. + +``/whatsapp ...`` + The WhatsApp commands above. + +In Telegram, write as in the client, also with photos and image files. +``onechat help`` lists all commands. + +********************* + What the agent does +********************* + +Memory + The agent remembers the whole chat as summaries that it can open + again, so it never starts over. ``agent/instructions.md`` holds your + lasting preferences. The agent can propose a change to it and makes + the change only after you agree. -Or enter it directly: +Tools + The agent runs shell commands as you, in its working directory, with + no sandbox. + +Subagents + For long tasks it starts subagents in the background, such as + ``GreenOtter``, and gets their reports as messages. + +Child agents + A child is a permanent agent for one area of your life, such as + ``House``, with its own memory. The root creates it, talks to it, and + hears from it only what needs you. Children never send WhatsApp + messages; they draft replies for you to approve. + +WhatsApp + The agent reads your chats in batches and wakes for a private message + or a mention of you. It finds chats and sends messages only when you + ask. It proposes to route a chat that is only about one area to the + child of that area, and sets the route after you agree. + +Images + Send a photo in Telegram, WhatsApp, or with ``/image`` to the agent. + Its memory keeps what the image shows, and it can look at an older + image again. Ask it to show an image or send one to a WhatsApp chat. + +Schedules + Ask for reminders and timed tasks, such as "every weekday at 8, tell + me what my groups talked about". Each run uses the quota, so runs are + at least 5 minutes apart. + +Import + Old WhatsApp chats can join the memory in date order. Stop the + daemon, export the chat as a ZIP, and run the plan before the import. + The import rebuilds the target's memory with model calls: .. code:: console - nix develop + onechat import -n [--agent NAME] CHAT.zip + onechat import [--agent NAME] CHAT.zip -The shell sets ``ONECHAT_DIR`` to ``.devenv/onechat`` in the checkout, -unless it is already set. Development data goes there, and Git ignores -``.devenv``. +***** + Log +***** -Build and run: +The daemon writes short events with ``key=value`` attributes to the user +journal, at three levels: info, warning for a failure that is retried or +loses nothing, and error for a failure that loses data or stops work. .. code:: console - nix build - ./result/bin/onechat + journalctl --user -u onechat -f + journalctl --user -u onechat -p warning -Format and check all files: +************* + Development +************* + +Nix supplies the Go toolchain and all tools. Enter the development shell +with ``direnv allow`` or ``nix develop``. Then: .. code:: console + nix build + ./result/bin/onechat version bin/format bin/check nix flake check + +``onechat version`` prints ``yyyy-mm-dd-gitshort`` for a Nix build and +``dev`` for a direct Go build. ``bin/check`` runs the linters and a +short replay of a synthetic log, and ``nix flake check`` also runs the +full replay of 100,000 messages. ``onechat probe LOG COUNT`` measures +the memory summaries on a log with real model calls.