Multi-provider agent harness / personal+business assistant in Go (pluggable identities, skills, sub-agents; HTTP API; Telegram + Vue web clients)
A multi-provider agent harness in Go — a personal/company assistant engine
built from the standard library, no vendor SDK, one static binary. The agent
loop is provider-agnostic: Claude, OpenAI, DeepSeek, Gemini, Ollama, or any
OpenAI-compatible endpoint plug in behind one streaming interface, and the loop
above never knows which is in use.
The Provider/Agent shapes borrow from the Construct brain, rebuilt here as
a clean standalone library. The assistant model is shaped after personal-agent
frameworks like Hermes — an identity that remembers, has skills, runs on
a schedule, and improves itself.
| Pillar | What it is | Where |
|---|---|---|
| Soul | identities — persona + routing tier + delegation | profile/, harness profiles |
| Memory | per-identity second brain: a bounded digest is injected, remember saves (with tags), recall searches, resurface revisits aging notes |
memory/, harness memory |
| Skills | SKILL.md workflows (agentskills.io format), progressive disclosure | skill/, harness skills |
| Self-improvement | writes its own skills (learn_skill); self-critiques answers (-critique); reflects on past chats into memory + skills (harness reflect) |
skill/, agent/, session/ |
| Sessions | multi-turn conversations that persist across runs | session/, harness chat |
| Crons | scheduled, proactive runs on a clock | schedule/, harness schedule |
| Hands | a persistent per-identity workspace + read/write/edit/list file tools, writes behind a permission gate | tool/, profile/ |
| Background work | a persistent task queue — “do X, report when done”, results delivered to your chat | task/, harness task |
provider.Provider streams neutral Events overMessage/Block types. Each adapter translates to/from its vendorRequest.CapFlags (“caching”, “tools”, …)chat REPL, andprovider/ Provider interface + neutral types; mock/anthropic/openai(+compat) adapters; registry
tool/ Tool interface, Registry, mediated Env; read/write/edit/list files, web_fetch, web_search, bash
agent/ the provider-agnostic loop (stream → accumulate → dispatch tools → repeat) + compaction
router/ tier → per-provider model table; classify + escalate
profile/ identities (persona, tier, delegation) + per-identity data dirs
skill/ SKILL.md loader, load_skill + self-improving learn_skill
memory/ per-identity durable facts + remember tool
session/ persisted multi-turn conversations
schedule/ proactive scheduled tasks (every / daily / weekly)
connector/ integration layer: native, MCP, Google (calendar) connectors
server/ HTTP+SSE transport (POST /v1/chat, GET /v1/sessions, /healthz)
auth/ OAuth (Anthropic PKCE, Google) + token store
config/ config.json (search, Google client, default profile)
cmd/harness/ thin reference CLI built on the library
go install github.com/flakerimi/harness/cmd/harness@latest
Or build from a checkout: go build ./cmd/harness (Go 1.26+, no dependencies).
No keys needed — the mock provider exercises the full loop offline:
go run ./cmd/harness -provider mock "hello there"
Real providers (bring your own key, or harness login for Claude OAuth):
ANTHROPIC_API_KEY=sk-... go run ./cmd/harness -provider claude "summarize go.mod"
OPENAI_API_KEY=sk-... go run ./cmd/harness -provider openai "say hi"
go run ./cmd/harness -provider ollama -model llama3.1 "say hi" # local, no key
Turn the generic harness into a specific assistant (an ISP’s support desk, a
social-media agency, …) with a short interview — the model drafts a tailored
persona and writes the identity:
harness onboard
# Business / org name: Acme ISP
# What does it do: regional fiber internet
# Your role: support lead
# Tone: friendly, clear, no jargon
# → drafting persona… ✓ wrote profiles/acme-isp.md (set as default)
# Next: harness chat -profile acme-isp · harness connect google -profile acme-isp
It writes a profiles/<slug>.md identity and sets it as the default. Add domain
know-how afterwards with skills (skills/<name>/SKILL.md) and specialists
(agents/<name>.md). -no-ai uses a built-in template instead of the model.
harness onboard # guided setup for a business → a tailored identity
harness [flags] <prompt> # one-shot run (default)
harness chat # multi-turn conversation, persisted per identity
harness serve # HTTP+SSE server (POST /v1/chat)
harness channel telegram ... # reach the assistant from Telegram
harness schedule add ... # proactive scheduled tasks
harness task add ... # queue background work ("do X, report when done")
harness pulse -deliver ... # the heartbeat: periodic check-ins, silent when quiet
harness profiles # list identities
harness skills # list skills (incl. the identity's learned ones)
harness skills search <q> # find skills in the git registry
harness skills add <name> # install one from the registry
harness memory # what the identity remembers
harness memory search <q> # search that identity's memory
harness sessions # list stored conversations
harness reflect # review recent chats → learn (memory + skills)
harness login [-provider claude] # model-provider OAuth
harness connect google # integration OAuth (Calendar)
harness connectors # what's connected + the tools exposed
harness config # config + search settings
harness chat -provider claude # default rolling conversation
harness chat -session work-trip # a named side conversation
harness chat -new # start fresh
The conversation survives across invocations. Long chats are compacted: once the
history exceeds the -compact token budget, the oldest turns are summarized by a
fast-tier call into one synthetic exchange, keeping the prompt in-window while
the on-disk session keeps full fidelity.
harness schedule add -profile work -provider claude -spec "daily 08:00" "brief me on my day"
harness schedule add -spec "every 2h" "check my inbox and flag anything urgent"
harness schedule add -profile personal -provider claude -spec "daily 09:00" \
-deliver telegram:<chatID> "resurface a memory worth revisiting, as a warm one-liner"
harness schedule add -profile work -provider claude -spec "daily 07:00" \
-deliver telegram:<chatID> "run the chief-of-staff skill" # morning brief → your phone
harness schedule list
harness schedule run-due # fire what's due (wire to system cron/launchd)
harness schedule daemon # or keep a process checking every minute
Specs: every 30m · every 1d · daily 08:00 · weekly mon 09:00 — plus
one-shots that fire once and retire: once 09:00 · in 2h (“wake me in two
hours and check on X”). A task’s
output streams to stdout by default; -deliver telegram:<chatID> (with
$TELEGRAM_BOT_TOKEN set) also pushes it to your phone — the wire that turns a
scheduled run into a proactive message.
Where the schedule fires on a clock, the task queue fires immediately — in the
background. Queue from the CLI, or let the agent queue work for itself: the
background_task tool lets it say “I’ll work on this and get back to you” in a
chat, and the daemon’s worker runs the job as the same identity (its memory,
skills, workspace) and delivers the result back to the chat that asked.
harness task add -profile personal -deliver telegram:<chatID> \
"research X in depth and write the brief to workspace:briefs/x.md"
harness task list
harness task show <id> # status + stored result
harness task drain # execute queued jobs now (no daemon needed)
Jobs persist as JSON; a daemon restart re-queues anything caught mid-run.
Failures are delivered too, so the asker is never left waiting on silence.
The pulse is the identity’s heartbeat: on a schedule it looks around its own
house — finished/failed background work (task_status), the next hours of
calendar, one resurfaced memory — and decides whether anything deserves your
attention. Something worth saying → one short message to your channel.
Nothing → it sends nothing (an empty reply skips delivery; silence is the
default, not a failure).
harness pulse -deliver telegram:<chatID> # daily 08:30 check-in
harness pulse -spec "every 4h" -deliver telegram:… # more of a companion
harness pulse # show the current pulse
harness pulse -off # stop it
One stable schedule task per identity (re-running updates it in place); the
pulse skill defines the look-around procedure, and the identity can refine
it over time like any other skill.
harness daemon runs the HTTP API + scheduler + task worker + (optionally) the
Telegram bot together under one graceful shutdown — the thing you deploy and
leave running.
harness daemon -provider claude # API + scheduler
harness daemon -provider claude -telegram -allow 123 # + the Telegram bot
harness daemon -print-launchd # macOS launch-agent plist
On a VPS it sits behind HTTPS (CapRover/nginx), with the API token as the org
secret; launchctl/systemd (or the proxy’s process manager) keeps it alive.
A token-gated JSON/SSE API so web, desktop, and mobile clients all talk to one
brain — run it locally, or deploy it to a VPS (e.g. behind HTTPS on CapRover)
that a whole org connects to for shared identities, memory, and sessions.
harness serve -provider claude -addr :8080 # auto-generates + prints an API token
harness serve -open # NO auth (localhost dev only)
TOKEN=… # from startup, or -token / $HARNESS_API_TOKEN
curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/v1/profiles
curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/v1/models
curl -sN -H "Authorization: Bearer $TOKEN" -X POST localhost:8080/v1/chat \
-d '{"profile":"business","session":"web1","message":"hi","provider":"kimi"}'
| Endpoint | What |
|---|---|
POST /v1/chat |
stream a turn (SSE); body {profile, session, message, provider?, model?} |
GET /v1/profiles |
the identities |
GET /v1/models |
providers + their models |
GET /v1/sessions?profile= |
a profile’s conversations |
GET /v1/sessions/{id}?profile= |
one conversation’s history |
GET /healthz |
open liveness check |
Auth: every /v1 route needs the token (Bearer header, or ?token= for
EventSource). CORS is permitted (token-gated), so browser/desktop clients on
other origins work. TLS is terminated by the reverse proxy in front.
Publishing: the default identity’s workspace/pub/ dir is served at the
server root, unauthenticated — so the assistant can write_file a styled
pub/reports/foo.html and hand you https://<host>/reports/foo.html instead
of flooding the chat (the publish skill encodes the habit). Anything under
pub/ is on the open internet; the rest of the workspace stays private.
A decoupled Vue 3 SPA lives in web/ — connect with the API URL +
token, pick an identity and model, and chat with streaming replies. It’s a pure
client over the same /v1 API (local or a remote VPS); desktop/mobile can be
thin wrappers over it.
cd web && npm install && npm run dev # http://localhost:5173
harness channel telegram -token <BotFather-token> -profile personal -provider claude
# or: TELEGRAM_BOT_TOKEN=... harness channel telegram -provider claude
Each Telegram chat resumes its own per-identity session, so the conversation
has memory across messages.
A profile is who the assistant is for — and each identity is fully scoped: its
own connected accounts, memory, skills, sessions, and tools, isolated from the
others. So “you personally” and “you at work” are different identities with
different email, calendar, and tools.
harness profiles # personal, work, business, …
harness connect google -profile business # connect the work email/calendar to that identity
harness chat -profile business # talk to the business identity
harness -profile personal "what's on my calendar" # uses the personal account
Everything is per-identity, under <config>/harness/profiles/<name>/. On
remote surfaces (daemon, Telegram, schedules) the identity’s filesystem tools
are rooted in its workspace — a persistent home for files that survives
across sessions; a CLI run works in cwd (-root) with the workspace alongside
(tools address it with a workspace: path prefix).
The agent has real hands — write_file, edit_file, list_dir next to
read_file — mediated by a permission gate: on the CLI (your own
directory) every mutating call (write_file, edit_file, bash) asks on the
terminal first (-yes waives it; non-interactive runs deny). Remote surfaces
skip the prompt — they’re sandboxed inside the identity’s workspace, and path
traversal out of the sandbox is rejected. The gate follows delegated and
dispatched workers too, so it can’t be bypassed via a sub-agent.
| Scoped per identity | Where |
|---|---|
| Connected accounts (Google/email) | auth.json |
| Durable memory | memory/ |
| Learned skills | skills/ |
| Conversations | sessions/ |
| Workspace (persistent files: drafts, notes, projects) | workspace/ |
| Tools (MCP servers) | mcp.json (layered on the shared mcp.json) |
Add an identity by dropping a profiles/<name>.md (persona + tier + delegation).
Connecting Google needs a Desktop OAuth client once (google.client_id/secret
in config, from console.cloud.google.com).
The harness has four pluggable layers, all file-based and per-identity scopable:
| Layer | What | File |
|---|---|---|
| Identities | who the assistant is | profiles/<name>.md |
| Skills | procedures the agent follows itself | skills/<name>/SKILL.md |
| Specialists | sub-agents it dispatches tasks to | agents/<name>.md |
| Tools | raw capabilities | connectors / mcp.json |
| Plugins | executables that add tools + deliver kinds | plugins/<exe> |
Skills (agentskills.io / Anthropic open format) reach the model by progressive
disclosure: names + descriptions go in the system prompt; the model calls
load_skill to pull full instructions only when a task matches. An identity can
also learn_skill — saving a workflow it worked out, reusable by name next time.
Skills can also come from a shared registry — a git repo of skill folders.
Point skills.registry (in config, or $HARNESS_SKILLS_REGISTRY) at the repo,
then harness skills search <q> browses it and harness skills add <name>
installs a skill into the shared skills dir (-profile for one identity’s own).
The clone is cached and only refreshed with -refresh.
Specialists are separate agent runs with their own persona, model tier, and
tool subset. The orchestrator dispatches a focused task to one by name
(dispatch(agent, task)); it runs in isolation and returns its result — good for
focus, parallelism, and running grunt work at a cheaper tier. Define one:
---
name: researcher
description: Deep web research; returns a sourced brief.
tier: fast
tools: web_search, web_fetch
---
You are a research specialist. Find concrete, sourced facts and return a brief.
harness skills / harness agents / harness profiles list each layer. A skill
is a recipe; a specialist is a separate worker.
Plugins are the zero-framework extension seam: any executable dropped in
./plugins/ (or <config>/harness/plugins/, or a profile’s own plugins/)
that answers three verbs — writable in bash, Python, anything:
<exe> spec → JSON manifest: {"name", "tools": [...], "delivers": [...]}
<exe> run <tool> ← input JSON on stdin → output on stdout (exit ≠ 0 = error)
<exe> deliver <kind> <dest> ← text on stdin
Plugin tools join the agent namespaced (dice__roll) so they can’t shadow
built-ins; a manifest’s "writes": true puts the tool behind the permission
gate; and a delivers kind extends -deliver targets everywhere (schedules,
wake-ups, background tasks) — a plugin advertising sms makes
-deliver sms:+1555… work. The mediated env arrives as $HARNESS_ROOT /
$HARNESS_WORKSPACE. See plugins.example/dice for a
complete one-file example. Where MCP suits full servers with sessions and
state, a plugin is one file you drop in a directory.
The repo ships a developer profile: a senior-engineer identity with real
hands. Root it in any checkout and it reads the project’s own guidance first,
edits surgically, and verifies with the project’s checks before calling
anything done (the dev-loop skill encodes the loop). Every write asks on
your terminal unless you -yes it.
harness chat -profile developer -root . -bash -provider claude
you › the error messages in tool/fs.go are inconsistent — unify and run the tests
Because it’s an identity, it compounds: it remembers the codebase’s
conventions and gotchas between sessions, learns skills from procedures it
works out, and can reflect on past sessions to improve. Pointed at this very
repository, harness develops harness.
The engine plus all the pillars: identities, memory, skills (+ self-learning
and a git-backed registry), sessions, compaction, scheduling, a background
task queue, per-identity workspaces with gated file tools, an HTTP/SSE
server, and a Telegram channel. Provider adapters for mock, Anthropic (API key +
OAuth), and OpenAI-compatible (OpenAI, DeepSeek, Gemini, Kimi, Mistral, Qwen,
OpenRouter, Fireworks, Together, xAI, Z.ai, MiMo, Apple, Ollama, LM Studio).
Connectors: native tools, MCP, and Google Calendar + Gmail (read + draft + send).
The integration surface — what’s shipped, what’s planned, and which of the four
routes (native / MCP / exec plugin / deliver target) a new integration should
take — lives in INTEGRATIONS.md.
Small, dependency-free, offline-testable — see CONTRIBUTING.md
for the ground rules before opening a PR.