Skip to content

@ahpd/bot ​

npmCIlicense MITnode >=22Agent Host Protocol 0.9.0

A bot plugin for the @ahpd/server daemon. It serves the bot: scheme, where a bot is a record with a workspace of its own, talked to in a chat of its own.

Part of ahpd. The source is in packages/bot.

Install ​

bash
ahpd plugin install @ahpd/bot
ahpd restart

Update, remove and one-run flags such as --plugin are in Plugins.

Options ​

In the configuration file, with every option set:

json
{ "plugins": [{ "name": "@ahpd/bot", "options": { "root": "/srv/bots", "lastChats": 20 } }] }
OptionDefaultWhat it does
root~/.botsThe folder a bot's workspace is under, as <root>/<slug>
lastChats10How many chats a template lists for {{chats}}

A bot is a record of the host's record store, so the daemon keeps each one as a file beside its own configuration: <configDir>/bots/<slug>.md, the fields as YAML frontmatter and instructions as the Markdown body, with the tombstones in <configDir>/bots-gone/. A file there that cannot be read is reported and skipped, and every other bot is kept. The chats are in <configDir>/bot-chats/: one row per bot and person, and beside it the log of the chat that row names.

The record store is a port, so a plugin that takes it over with registerRecords(store, 'replace') - a sqlite or a postgresql store - keeps the bots there instead of in files, and nothing in this plugin changes. docs/plugins/BOTS.md shows one bot file and what a hand edit may change.

No option holds a credential, so the plugin takes nothing from the vault.

In your own host ​

The package's apply is the plugin entry. Hand it a plugin host from @ahpd/sdk, then fold what it registered into the host's options:

ts
import { createHost, foldHostOptions, listen, pluginHost } from '@ahpd/sdk';
import { claude } from '@ahpd/agent-claude';
import { apply, name } from '@ahpd/bot';

const path = process.cwd();
const context = { path, paths: [path], version: '0.10.0', hostName: 'my-host', configDir: '/var/lib/my-host', log: console.log, say: console.log };
const { host: plugin, contribution, seal } = pluginHost(name, context);
await apply(plugin, { root: '/srv/bots' });
seal();

const { options } = foldHostOptions({ path, agents: [claude({ paths: [path] })] }, [contribution]);
const host = createHost(options);
await listen({ port: 9187 }, (peer) => host.accept(peer));

The bot: scheme ​

A bot is addressed by its slug, which is the URI path and the record's own id:

CommandWhat it does
resourceWrite bot://<slug> with createOnlyMakes a bot. The body is the record, and name is the only field it must carry.
resourceWrite bot://<slug> with ifMatchEdits one. ifMatch is the etag the last resourceResolve answered, so a write against a bot somebody else has changed is refused -32011.
resourceRead bot://<slug>The record, as JSON, with chat where the reader may talk to it.
resourceResolve bot://<slug>Its metadata, with etag.
resourceList bot://The bots the reader may see, by slug.
resourceDelete bot://<slug>Deletes one, and leaves a tombstone.
createResourceWatch bot:// or bot://<slug>Tells the reader when a bot it may read is made, edited or deleted.

A slug is lowercase letters, digits and dashes, one to forty of them, starting with a letter. Anything else is refused -32602: the slug is a folder name and a URI path at once.

A body may carry name, labels, description, body, color, instructions, template, harness, model, preset, session, workspace, computer and owner. id, createdAt and updatedAt are read but never obeyed - what they name is the host's, and a write that names another id is refused rather than followed. owner may be named by a make, as the maker or a team or project the maker belongs to; a write naming anybody else is refused -32009, and an edit may not move it. A read answers one key more, chat, the reader's own conversation with the bot: it is answered and never obeyed, so a client that writes a record it read back is writing back a bot.

A bot's session is the name of the back session a message to it wakes. It is a slug, or a slug holding {turn} where the turn id of each message takes that word's place - so a bot that names {turn} is a session per message rather than one a person comes back to. A body that names none has work, a body that names something that is no name is refused -32602, and a file an older daemon wrote without the line is read as the bot that named none.

A bot is drawn in one of fifteen bodies and in one of eleven colours or a hex colour, #rrggbb. A make that names neither gets a body at random and the first colour.

Who may see what ​

WhoWhat they may do
The ownerRead, edit, delete
A member of the owning team or projectRead, edit, delete
bot:getRead any bot
bot:listList every bot
bot:putMake a bot in the scheme, edit their own
bot:chatBe answered a chat by any bot they may read
*:*, or a root connectionAny of it, on any bot

A listing shows what a read would allow: a list that showed a bot the reader cannot open would be a list of names they cannot use. A read that opens a chat also needs the reader's own session:create, because the chat is theirs rather than the bot's.

The catalogue ​

A subscribe to x-ahpd-bots:// answers { bots }, the bots the reader may read, then x-ahpd-bots/added, x-ahpd-bots/changed and x-ahpd-bots/removed as they change - BOTS.md.

The workspace ​

A bot works in a folder of its own, <root>/<slug> unless the body named one, and a named one has to be under root. No two bots share a folder, and an edit never moves one: two bots in one tree would be two of them working over each other. The folder is made when the bot is made, so a bot has one whether or not anybody has talked to it yet. A bot in a computer is not made a folder here: the machine makes it, as the working directory of whatever runs there.

The chat ​

A person talks to a bot in a chat: one session of the bot-chat provider per bot and person. The provider runs no model, and a turn it begins stays open until the plugin that started the chat ends it - which is what the bot's back session answers into. Each turn is one JSON line under <configDir>/bot-chats/, so a chat is still readable after the daemon restarts. Nothing about a chat is kept in memory past the process that made it.

A chat is opened by a read. resourceRead bot://<slug> answers the record with chat, the URI of the reader's own conversation with that bot, and the first read by a reader who may chat starts it and keeps the row. Reading the same bot again answers the same chat, and two people reading one bot are answered two different chats. A reader who may not chat is answered the record alone, with no chat and no refusal.

A chat works in the bot's workspace and is titled with the bot's name. Starting one asks the reader's own session:create, by the host's own road, and a refusal there is the read's refusal.

A bot may name a template, the text a chat with it would open with, and a bot that names none gets the default one the plugin ships. A make or an edit that names a template that does not exist is refused -32602. Nothing fills a template yet: a chat runs no model, so there is no first turn for one to open.

The tombstone ​

A deleted slug is not made again. A client held the address, and a different bot answering to it would be a lie about what that client has. A make on a deleted slug is refused -32010.

Documentation ​

BOTS.mdThe bot: scheme, the record and the chat
PLUGINS.mdWriting and loading a plugin
GET-STARTED.mdRunning the daemon

License ​

MIT © Softov

What a bot is, its scheme and its session are BOTS.md.