skill
pibot
Personal agent companions built on the pi SDK — chat over Telegram, per-agent plugins, memory, calendar, scheduling heartbeat, self-evolution
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/glebis/pibot itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 25, 2026pibot
Notable changes: CHANGELOG.md.
Personal agent companions built on the pi SDK. Chat with agents over Telegram (or a local CLI); every agent has its own plugin system, memory, calendar awareness, a scheduling heartbeat, and goal-driven skill self-evolution.
Design in one paragraph
Every agent is a private runtime directory under ~/.local/share/pibot/agents/ by default (PIBOT_AGENTS_DIR overrides it). Only the generic scaffold in agents/_template/ belongs in source control:
~/.local/share/pibot/agents/<name>/
agent.json # manifest: model, heartbeat rhythm, evolution config
AGENTS.md # persona (loaded as context by pi)
memory/ # long-term memory, owned by the agent
skills/ # evolved + self-saved skills (SKILL.md per skill)
extensions/ # agent-private pi extensions (plugins)
sessions/ # persistent per-chat conversation state
state/events.jsonl # event log — feeds heartbeat + evolution
The host process (src/) runs a scheduler (JSON-backed timer wheel with snooze semantics), a heartbeat engine (economical ephemeral cheap-model ticks that decide whether anything is worth saying), and pluggable transports (Telegram, CLI). One capability registry binds each host plugin's factory, tool allowlist, dependency check, and prompt description so agents are never told about unavailable tools. Each agent additionally loads its own private extensions and skills.
Per-agent capabilities
agent.json may set capabilities to an explicit list. Omitting it uses the conservative default: scheduler, memory, calendar-read, skills, agent-comms, and questions (the last two only when their host hooks exist). External-data or mutation capabilities are opt-in:
{
"capabilities": ["scheduler", "memory", "calendar-read", "skills", "agent-comms", "questions", "gmail-read", "knowledge"]
}
Available ids are avatar, speech, scheduler, memory, calendar-read, calendar-write, gmail-read, linear, skills, knowledge, agent-comms, questions, link-triage, attend, telegram-responder, delegate, herdr, developer, and remote-workshop. Avatar, Calendar, and Linear mutations require owner confirmation. Dependencies are loaded and advertised only when available. Ordinary agents receive no generic filesystem tools by default; tools is an explicit SDK/custom-tool allowlist and capabilities controls host plugins.
Jev link triage (shadow mode)
The link-triage capability is an opt-in, scraper-only decision aid. After the scraper has saved one material under its own materials/ directory, it can call link_triage_shadow with that material's absolute path. The tool reads at most 64 KiB locally and sends only a bounded source title, description or excerpt, public source host, tool name, and configured focus text to Jev through Vercel AI Gateway. It does not send the full URL, query parameters, full page, session history, or material path. It returns separate relevance, suggested destination, and suggested processing-depth choices. Results are advisory: no fetching, saving, discarding, routing, or messaging follows automatically.
Enable it with both an explicit "link-triage" entry in the scraper agent's capabilities list and these server-side variables:
PIBOT_LINK_TRIAGE_SHADOW=1
PIBOT_LINK_TRIAGE_FOCUS=<one short, owner-approved focus statement>
AI_GATEWAY_API_KEY=<Vercel AI Gateway key>
The feature defaults off. Focus text is capped at 500 characters. Use only public source descriptions that you are comfortable sending to the Gateway and TypeSafe AI; do not put private vault notes, credentials, or signed URLs into the focus statement. Missing focus, insufficient description, blocked sources, invalid local paths, malformed model replies, timeouts, and weak choice probabilities yield review_needed. A probability threshold of 0.75 is provisional and needs evaluation on owner-labeled links before it guides any action. The event log records only outcome categories and timings, not source content or URLs.
PiBot currently has no typed scraper-completion event: scraping is agent-directed and its saved materials vary in format. The tool call is therefore an explicit post-save step, not an automatic hook. A future deterministic completion event would let the same evaluator run automatically. The reusable jev-evaluator supports bounded Boolean, Choice, and Score questions for later experiments in routing, pre-action review, post-action verification, heartbeat novelty, skill evaluation, and context selection. Two of those hooks now ship: the advisory skill-evolution shadow observer (see Self-evolution) and the optional /goal judge (see Goals), both default-off, both behind a per-agent grant plus a daemon switch, and neither able to change a decision while in shadow mode.
The herdr capability (requires the herdr CLI and a running herdr instance) lets an agent run subagents in new tabs of the owner's herdr UI: herdr_dispatch spawns claude/codex/pi/opencode in a fresh tab with a self-contained brief, waits for done, and returns the transcript (--detach returns immediately; herdr_read and herdr_wait observe the pane afterwards). The target workspace resolves from the invoking herdr pane, $PIBOT_HERDR_WORKSPACE, or an explicit workspace argument; briefs are passed as temp files and never carry secrets.
providers is a per-agent model-provider allowlist. Without it, only models named directly in model/cascade are used; global and authenticated-provider fallback discovery is disabled.
Telegram bot avatars
Avatar access is opt-in per agent. Add avatar to that agent's manifest; bots bound to other agents do not gain the capability:
{
"capabilities": ["scheduler", "memory", "avatar"]
}
Generation uses the deterministic, offline local provider by default. Optional external image providers are configured separately and remain unavailable until explicitly configured; provider discovery does not call them. Generated JPG artifacts remain private in the owning agent's runtime data.
avatar_generate only creates a candidate. A later avatar_apply targets the Telegram bot handling that chat and presents an explicit Confirm / Cancel choice before changing its profile photo. PiBot never rotates avatars on a timer or applies a generated image automatically.
Run
npm install
npm run cli # terminal chat + dashboard, no tokens needed
cp .env.example .env # add TELEGRAM_BOT_TOKEN for the real thing
npm start # telegram bot
The web dashboard runs at http://localhost:7860 (disable: PIBOT_WEB=0, port: PIBOT_WEB_PORT). Always open it via localhost, not 127.0.0.1 — Touch ID / passkeys are bound to the localhost domain, and browsers reject the raw IP with "This is an invalid domain." (browser navigations from 127.0.0.1 are redirected automatically). It is always locked: set PIBOT_WEB_TOKEN for the first login, then enrol a passkey. First-passkey enrolment requires that token-authenticated session.
Voice notes & media
Beyond text, the bot accepts Telegram voice, audio, video notes, and audio documents (locally validated and transcribed → routed like typed text), plus photos/documents (agent gets the local file path + caption). Unsupported media is declined instead of dropped. Video-note audio is extracted locally; the video itself is never passed into the agent context.
Transcription is local-first and controlled by STT_PROVIDERS (default whisperkit,local_whisper):
whisperkit-cli— Apple Silicon native (default local-first on macOS; modelSTT_WHISPERKIT_MODEL, defaultwhisper-tiny, cached in~/Library/whisperkit)- local fallback: the
whisperCLI on PATH (model overrideSTT_LOCAL_MODEL) - optional external fallback:
groqwithGROQ_API_KEY(model overrideSTT_GROQ_MODEL) is used only when the resolved agent manifest explicitly lists it underspeech.sttProvidersand setsspeech.allowExternalStt: true STT_LANGUAGE— optional ISO-639-1 language hint (omit for auto-detect)
Downloaded speech media uses private runtime storage, opaque filenames, local media probing, bounded duration/size/timeouts, and is removed after the transcription attempt. Photos and ordinary documents follow the separate bounded retention work tracked for runtime media.
Outgoing speech is local-only and opt-in per agent. Add speech to that agent's capabilities. speech_generate uses macOS say plus local ffmpeg to create either an OGG/Opus Telegram voice message or M4A music-player audio; it never sends. speech_send sends the selected artifact only to the same invoking Telegram chat through the normal per-chat queue, rate-limit retry, and duplicate guard. A successful send deletes the artifact; unsent artifacts expire after 24 hours.
Speech is never generated or sent automatically, and it has no heartbeat, scheduler, replay, or automatic reply-conversion hook. No external TTS provider or credential is included in this foundation.
Minimal voice communication (/minimal)
A per-chat mode for replies meant to be listened to. Off by default; turn it on per chat with /minimal on, back to the agent's manifest default with /minimal default, and /minimal reports both the state and where it came from.
Two mechanisms, because they do different jobs. A prompt-side directive asks the model for speakable output (≤4 short sentences, no URLs/code/markdown/hashes/model names, a file by name rather than by path). A deterministic filter then guarantees what leaves: URLs dropped, markdown links keep their label, code fences keep their words, headings/bullets/quotes/emoji/hashes stripped, provider specs dropped, and paths shortened to the file name. Paths survive in full only when the request explicitly asked for one (full/exact path, which file, where is, путь) — remembered for that turn and cleared after it. Line breaks collapse, because speech has none.
- The cap applies to spoken output (4 sentences / 400 characters, cut at a sentence boundary); text replies lose the technical detail but keep their length.
- Operational notices are exempt (
⚠️ 🪫 🔑 ⛔️): a mode that hid "your message was queued" would recreate exactly the silence the rest of this codebase works to avoid. - It never empties a reply: if the cap or the filter would leave nothing, the stripped text is sent uncapped.
- Voice styling happens before synthesis, so the audio itself is minimal rather than just its caption.
- Per-agent default:
speech.minimal: trueinagent.json. Per-chat/minimalalways wins.
The dashboard provides: agent CRUD, manifest editor (model/heartbeat/evolution/quiet hours), persona + memory editors, schedule table with cancel, snooze/wake, staged-skill review (promote/reject), run-evolution, event tail.
Chat commands: /help /agents /agent <name> /newagent <name> <persona> /handoff <agent> [note] /schedules /snooze 2h /wake /status /skills /minimal on|off|default /goal … /evolve [goal] /evolve status|promote|reject
Handoff between agents
Agents can delegate among themselves (agent_message / agent_ask / agent_list), and a conversation can move between agents with full context: /handoff <agent> [note] or an agent calling its handoff tool packages the thread into a task brief (task, context, artifacts, done, next step — distilled by a cheap ephemeral model, raw transcript fallback) and delivers it to the target's session in the same chat, which is then rebound so the user's next message reaches the target. In a dedicated sub-bot chat the chat stays pinned to its agent — there the brief lands in the target's inter-agent pair session instead. External delegation is opt-in per agent: delegate (claude/codex/gemini CLIs) and herdr (subagents in herdr tabs).
Scheduling & the one-button flow
Ask naturally: "remind me to stretch in 20 minutes", "note to take: check the lab results tomorrow 9am", "daily standup note at 08:00", "every 2h drink water", "friday 18:00 submit the report".
- The agent calls
schedule_create(instant — no confirmation ritual), you get a card with inline buttons: ✅ OK · ⏰ +10m · 🕒 +1h · 🗑 Cancel. wake: "important"items fire even when everything is snoozed.delivery: "agent"wakes the agent to compose the message itself;directsends a quick formatted ping.- Promises: "I'll send Anna the invoice by Friday" →
promise_maketracks it with an automatic pre-check,/promiseslists open ones. /snooze 2hpauses the whole rhythm (heartbeats skip a beat, normal items defer, important pierce).
Heartbeat (economical aliveness)
Give each agent a checklist: edit ~/.local/share/pibot/agents/<name>/HEARTBEAT.md — a tiny, user-editable list the agent follows on every tick (OpenClaw's most-loved pattern). Agents can also send images: include MEDIA: <url> in a reply.
Every agent's manifest sets a rhythm (interval, cheap model, quietHours). Each tick spawns an ephemeral cheap-model session (never touching the main session's cache) with a compact digest: persona + memory + pending items + recent events. It decides via one tool call: speak (short proactive message), escalate (hand to the full agent brain), or nothing (a few hundred tokens).
Adaptive wakeups (self-paced rhythm, from Ouroboros's set_next_wakeup): the heartbeat decision can also include wakeup ("10m"…"12h") — the agent compresses the next gap when something is brewing and stretches it when everything is quiet, instead of firing on a fixed drumbeat. Hard bounds come from the global clamp (5m–12h), narrowed per-agent via manifest heartbeat.minInterval / heartbeat.maxInterval (editable in the dashboard). Skipped ticks (quiet hours, snooze, backoff) always fall back to the base rhythm.
Maintenance rotation (one item per wakeup): each tick's digest ends with a maintenance panel — freshness of memory/MEMORY.md, AGENTS.md, memory notes, event consolidation, and the last maintenance entry (Ouroboros CONSCIOUSNESS.md pattern). The agent services at most one stale item per tick and rotates, recording what it did via the maintain field: a one-line durable note appended to memory/maintenance.jsonl (also logged as a maintenance event, visible to you in the dashboard event tail). Everything fresh → silence + longer wakeup; maintenance never breaks a tick. One verb runs host-side machinery: maintain: "consolidate events" triggers the consolidation pipeline for real.
Event consolidation (Skill Forge blueprint)
The event log is ephemeral (bounded, redacted JSONL). The consolidation pipeline distills it into durable memory per agent (adapted from Ouroboros's consolidator.py + memory.py):
- Block-based memory —
memory/consolidated/blocks.jsonholds summary/gap/era blocks;CONSOLIDATED.mdandjournal.jsonlare regenerated views + run records. - Advance-only cursor — meta records the last-consumed
events.jsonloffset; a model failure never advances it (nothing is ever lost to a bad run). - Gap blocks — event-log rotation (head trimming) is reconciled by timestamp and recorded as an explicit gap block: lost/skipped events are never silently dropped.
- Cheap-model distillation — unconsolidated events are summarized into dense blocks; durable lessons are promoted into
memory/notes/(memory-plugin-compatible) with an index insideCONSOLIDATED.md. - Eras — older blocks compress into era blocks once the count grows (deterministic fallback keeps every record if the model is down).
- Triggers — evolution cycles distill first (unless
consolidation.enabled: false) and feed the distilled view into skill proposals; heartbeats can service it via the maintenance verb; the opt-in scheduler job (consolidation.enabled: true,interval,model) runs it on its own rhythm;/consolidate [status]runs it manually.
Self-evolution (Hermes-style, pibot-native)
Adapted from NousResearch/hermes-agent-self-evolution (GEPA loop: propose → evaluate → guardrails → apply) and the pi ecosystem's existing packages (pi-agent-skill-evolution, pk-pi-hermes-evolve, @artale/pi-evolve, pi-skill-evolution) — none of which fit pibot's per-agent architecture, so the loop is built in:
collect (events + persona + skills) → propose (cheap model)
→ guardrails (name, size ≤15KB, structure, patch-applies)
→ stage in skills/.staging/
→ eval probes (ephemeral sessions with the candidate skill loaded, LLM judge 1-5)
→ avg ≥ 4: promote + git commit + announce | else: stays staged for review
- Automatic:
evolution.enabled: truein agent.json runs a cycle on a rhythm (default 6h, max 4/day), announces promotions in chat. - Improvement backlog (per agent,
memory/improvement-backlog.md): durable, deduplicated candidates ranked priority → recurrence → recency (ported from Ouroboros's improvement backlog — a repeated irritation is counted, never dropped). Heartbeats append candidates viamaintain: "backlog: <summary>"; the digest shows the top 5 to every heartbeat and evolution cycle; a goal-less/evolve(scheduled tick, dashboard) targets the top-ranked open item as its goal, and landing the skill closes what it addressed (closesBacklogproposal field + staging sidecar, close-on-promote). Advisory only — implementation always goes through the gated cycle. - Manual:
/evolve [goal]runs a cycle now;npm run evolve -- assistant "get better at morning briefings". - Staging workflow:
/evolve status→/evolve promote <name>//evolve reject <name>. Probes below 4 avg never auto-apply. - Agents evolve themselves mid-chat too:
skill_save/skill_patch/skill_list(Hermesskill_managestyle) after non-trivial workflows.
Goals (/goal)
A bounded autonomy loop for work that takes more than one turn: the agent keeps going until a judge sees the goal done, and stops itself when it should. Set with /goal <objective> or /goal draft <objective> (a model expands the objective into a completion contract); /goal, /goal sub <criterion>, /goal max <n>, /goal pause, /goal resume, /goal done, /goal clear manage it. State is per chat and survives restarts.
/goal draft <objective>
→ contract: outcome / verification / constraints / boundaries / stop_when
→ each turn: the [goal] block rides the prompt (objective, contract, criteria, turn N/M)
→ after the turn: a judge returns done | continue | wait
done → announce completion and stop
continue → re-prompt for the next step (bounded)
wait → park until something external changes
- Bounds are the design, not a footnote: default 8 turns (max 50 via
/goal max), auto-pause after three unreadable judge replies, no second auto-turn in the same instant, a deferral while the agent is snoozed, and — per the owner's decision — a newer message from the owner steers rather than pauses: their turn is the next step and the loop resumes from it. - A goal's creation is its first turn boundary, so the message that set the goal cannot veto the loop it started.
- A judged continuation that throws parks loudly — status paused, an event with the error, and an owner-visible
/goal resumeprompt — instead of stalling silently. - The loop only judges chat-relevant turns: owner turns and its own
[goal]continuations, never scheduler, heartbeat, or handoff traffic.
Judging: local, Jev, or shadow
The judge is one seam and three options, chosen per agent and gated by a daemon switch — default is local, with no external call:
PIBOT_GOAL_JUDGE | Behaviour |
|---|---|
off (default) | local ephemeral judge only |
shadow | local is authoritative; Jev runs alongside and the two verdicts are logged as metadata |
jev | Jev judges (typed choice + two booleans); local is the fallback |
To opt an agent in, its manifest carries goal: { judge: "jev", dataScope: "redacted_approved", providers: ["typesafe-ai"] } — a per-agent grant and the daemon switch, so a config change alone cannot start sending goal text off the machine. The goal objective and the agent's reply travel bounded and redacted, framed as untrusted data.
Jev answers three typed questions: the verdict (done/continue/wait/unclear), whether the contract's stated verification is visible in the reply, and whether progress is externally blocked. Two rules make it stricter than a freeform judge, and both come from observed failures: a done whose verification is not visible is downgraded to continue, and a done below 0.6 confidence is refused as a verdict rather than finishing a goal on a coin flip. Every failure — timeout, invalid response, oversize state, provider denial — is typed, logged with its cause, and falls back to the local judge; an unavailable evaluator can never look like a verdict.
draftContract always stays local: Jev is a judge, not a generator.
Architecture
src/
config.ts env + .env
core/
types.ts manifests, schedules, transports
util.ts natural-language time parsing ("in 20m", "daily at 08:00", "friday 18:00")
scheduler.ts job store + timer wheel + snooze + promises + card flow
agent-manager.ts agent discovery, scaffolding, per-chat pi sessions
heartbeat.ts ephemeral cheap-model ticks (speak/escalate/note)
bot.ts command routing, cards, fire delivery, time envelope
evolution.ts goal-driven skill evolution (staging + probes + gates)
consolidation.ts event log → durable memory (blocks, gaps, eras, lessons)
events.ts per-agent event log (feeds heartbeat + evolution)
disk-guard.ts ENOSPC tripwire → auto disk-cleanup (safe preset)
plugins/ shared agent plugins
scheduler-plugin.ts schedule_create/list/cancel, snooze, promise_make/keep
memory-plugin.ts memory_save / memory_recall
calendar-plugin.ts calendar_today (icalBuddy + gws)
skill-manage-plugin.ts agents save their own skills
transports/
telegram.ts grammY, long polling, inline keyboards
cli.ts terminal chat with numbered cards
Developer agent (off by default)
Set PIBOT_DEV_AGENT=1 to scaffold the built-in pibot-dev agent: it develops, tests, and stages the bot's own source.
- React-only (no heartbeat, no evolution) — it never self-initiates; you talk to it (
/agent pibot-dev, or hand a task off viaagent_message). - Its session runs with
workspace: "repo"— the pibot repo root is its cwd, and it getsbashplus three tools:dev_status— branch / git status / diffstat / recent commitsdev_test— runstsc --noEmit+ the vitest suite (optionally scoped)dev_stage— lands the change as a git checkpoint commit, but only after re-running the full gate (typecheck + tests). Refuses on red or when forbidden paths changed (.env,data/, sessions, node_modules).
- You review source changes in git history (
git log,git revert); runtime staging logs stay private under the configured agent directory.
echo 'PIBOT_DEV_AGENT=1' >> .env # enable at next restart
git revert <commit> # rollback any staged change
Disk guard (ENOSPC auto-cleanup)
When the daemon hits ENOSPC: no space left on device — via process-level
uncaughtException/unhandledRejection, errors swallowed by inner catches (funnelled
through errorMessage), or a low-water watcher (free space < 2 GiB, checked at boot and
every 10 min) — it automatically reclaims space with the owner's disk-cleanup skill:
python3 ~/.claude/skills/disk-cleanup/scripts/clean.py --preset safe --go --no-quit --empty-trash --json
Only risk:safe targets run (regenerable caches — the script's own preflight and risk
gating apply; trash CLI, never rm). --no-quit means running apps are never
auto-quit — targets that need their app closed are skipped. --empty-trash is
owner-authorized: it permanently empties the whole Trash, without it trashed bytes would
free nothing. Medium/never targets are never automatic; if space is still low afterwards
the owner gets a Telegram push with the exact manual escalation command.
A durable cooldown (data/disk-guard.json, 30 min) prevents cleanup loops and cannot be
bypassed by restarting. PIBOT_DISK_CLEANUP_DIR moves the skill dir; PIBOT_DISK_GUARD=0
disables the guard entirely.
Testing
npm test # full Vitest suite
npm run test:coverage
npm run evolve -- assistant "goal text" # CLI evolution cycle
.git/hooks/pre-commit runs typecheck + tests before every commit (TDD gate).
Telegram live-test harness (end-to-end, autonomous)
Unit mocks can't catch transport-level bugs (a message-outbox self-deadlock shipped exactly that way behind 419 passing tests). The live harness drives a bot through the real Telegram round-trip — send a command as a user, await the reply, assert reactions and the daemon log:
npx tsx scripts/telegram-live-test.mts --attach --chat @thebot # post-deploy smoke vs a running bot (DM: use the bot's @username)
npx tsx scripts/telegram-live-test.mts # full: isolated daemon + dedicated test bot
- spawn mode (default) boots an isolated daemon (
PIBOT_DATA_DIR/PIBOT_AGENTS_DIRoverride, dedicated test-bot token, its own dashboard port) with a pre-seeded test agent (heartbeat@1m + 8 distillable events) and runs the full scenario set: boot health (lock, pollers, dashboard auth challenge),/statusround-trip, unknown-command reply, seeded consolidation round (model distills the 8 events → blocks.json + journal + rendered CONSOLIDATED.md), consolidation idempotence (second call is a no-op), live heartbeat tick (1m interval, silent-rule asserted), the reaction-settle wedge canary (👀→👍 on the sent command — catches outbox deadlocks), a no-error log soak, and lock cleanup on shutdown. --attachmode runs read-only smoke against any already-running bot chat (no side-effecting commands) — this is the post-deploy verification step before trusting a restart. It sends through your user account, so target the bot's @username: a numeric id is resolved as a peer of yours, and your own numeric (allowlist) id lands the smoke commands in Saved Messages where the bot never sees them.- Probe cleanup: every message the harness sends — and the bot's direct reply to it — is deleted via telethon ~10s after its scenario completes, so live-test traffic never litters real chats (
--keep-probesdisables). - Missing vars (
TELEGRAM_LIVE_TEST_TOKEN,TELEGRAM_LIVE_TEST_CHAT_ID) are requested via an AppleScript dialog — hidden input for the token; plain input with the production allowlist id pre-filled for the chat id — and persisted todata/telegram-test.env(0600, gitignored): set once, prompted never again.--no-promptdisables the dialog. One-time prerequisites: a test bot via BotFather (never reuse the production token),/start-ed once from your account. The DM chat target is derived from the token itself, so--chatis unnecessary in spawn mode.
Roadmap
Shipped changes are recorded in CHANGELOG.md; the items below are still open.
- Skill Forge pattern-mining beyond the event log: mine session transcripts and staged-skill outcomes (the event-log distillation foundation ships in
consolidation.ts) - Multi-strategy mutation archive (compress/quality/radical, from
@artale/pi-evolve) - Rich image-generation previews and provider-specific adapters beyond bot avatars
- Stronger process-level sandboxing for agent-private extensions
License
MIT — see LICENSE.