3.8 KiB
The Veskslop agent
The agent is the local LLM harness. It lets a remote llama.cpp instance act through the daemon.
It is separate from the core and from the daemon, and it is optional.
Two consumers
- The local harness (veskslop agent). A small Go process on the same host as the daemon. It
talks to a remote OpenAI-compatible
llama.cppendpoint and to the daemon over MCP. It owns free-form messages. It is the only consumer that runs an LLM loop. - Remote agents. External systems connect to the daemon's MCP endpoint and use it as a sensor
and notifier. Example: a task runner wants to reboot a switch, calls
get_voice_statefor the owner, sees the owner is in a call, and reschedules. Remote agents do not run an LLM here and do not receive every message. They query state and send notifications.
Routing: commands versus free-form
The daemon keeps the fixed commands (whitelist, speak) and the poll parser. A trusted
free-form message becomes a task for the local harness. Commands never go to the LLM; this avoids
double replies and keeps privileged actions out of model reach.
Local harness design
| Piece | Choice |
|---|---|
| Language | Go |
| Agent framework | Eino (github.com/cloudwego/eino), ReAct agent |
| Model | eino-ext/components/model/openai against a remote llama.cpp /v1 endpoint |
| Tools | eino-ext/components/tool/mcp/officialmcp over the official MCP Go SDK, Streamable HTTP |
| Tool surface | The daemon's MCP tools. Nothing else. |
| Entry | agent/ |
| Kill switch | VESKSLOP_AGENT_ENABLED=1 is required. Default is off. |
| Auth | VESKSLOP_AGENT_TOKEN bearer token on the task endpoint. |
| Budget | VESKSLOP_AGENT_MAX_STEPS (default 16). Per-task wall clock: 5 minutes. |
The harness receives tasks on POST /task. The daemon will push them (not implemented yet; the
scaffold accepts manual pushes). The task body:
{
"id": "trigger-id",
"guildId": "…",
"channelId": "…",
"author": "…",
"content": "the trigger text",
"sensitive": false,
"context": [{ "author": "…", "content": "…", "timestamp": "…" }]
}
The model replies through the daemon's own send tools, so pacing, the rate limit, and the action log apply as for any other sender. The harness never calls Discord directly.
Safety rules
- Tool results and message context are untrusted. The system prompt says so explicitly.
- The harness is stateless per task. Context comes from the daemon each time.
- The model cannot run fixed commands. It only has MCP tools.
- Planned, not implemented: a tool allowlist, per-conversation serialization with
cancel-and-replace, delivery IDs for dedupe, and
RATE_LIMITEDbackoff beyond one retry.
Remote agent path
The daemon already exposes its tools over MCP (Streamable HTTP). Remote clients can:
- read state (
get_voice_state,get_voice_overview,get_user_status,list_dms, …) - notify (
send_dm,send_message,speak)
Planned work: a small notification tool set for external systems (notify_owner), a documented
LAN or Tailscale bind, and optional shared-token auth on the MCP listener. The listener currently
binds localhost.
Status
Scaffold implemented in agent/ and smoke-tested on 2026-09-14: it loaded 28 tools from the
daemon, ran one ReAct tool round against a fake OpenAI-compatible endpoint, and produced the final
message. Daemon push, tests, and the remote notification tools are pending.
Run the scaffold
cd agent
VESKSLOP_AGENT_ENABLED=1 \
VESKSLOP_AGENT_LLAMA_URL=http://llama-host:8080/v1 \
VESKSLOP_AGENT_MODEL=your-model \
VESKSLOP_AGENT_MCP_URL=http://127.0.0.1:8788/mcp \
go run . --task "Who is in the call right now?"
Build a binary with go build -o veskstop-agent .. The Go toolchain is not part of the root
pnpm workspace; run go build and go vet in agent/ directly.