libr-delegate
Delegate work to a running LibrAgent desktop/headless instance via the local HTTP Session API (`POST /api/sessions`, messages, status, children, resume, terminate, delete). Use from Cursor (or other e
SKILL.md.agents/skills/libr-delegate/SKILL.md
GitHubLibr Delegate (Cursor → LibrAgent Session API)
Spawn and steer LibrAgent sessions over HTTP from Cursor. LibrAgent must
already be running (desktop or headless). This skill does not ship into
LibrAgent bundled_skills.
vs in-app delegate
| Caller | Mechanism |
|---|---|
| Cursor / scripts / Harbor | This skill → HTTP /api/sessions |
| Inside a LibrAgent agent turn | In-app delegate → agent__* tools |
Base URL
PORT=$(cat ~/.libragent/http_port 2>/dev/null || echo 3030)
BASE="http://127.0.0.1:${PORT}"
Port file is written when LibrAgent’s HTTP server binds; fallback is 3030.
Workflow
- Confirm LibrAgent is up —
GET $BASE/api/health(or assistants list) - Resolve
assistantId—GET $BASE/api/assistants - Write a self-contained handoff (LibrAgent child does not see Cursor
chat, parent workspace instructions, or workspace-local skills unless you
pass an absolute
workspacePath) - Create — write JSON payload to a file, validate non-empty JSON, then
POST $BASE/api/sessions(includerequestor create idle). Seereferences/session-api.md(preferred file recipe + empty-body guard) - If idle —
POST $BASE/api/sessions/:id/messageswith{"content":"..."} - Poll —
GET $BASE/api/sessions/:iduntil terminal (preferscripts/poll-until-done.sh) - Read result —
GET $BASE/api/sessions/:id/messages - On paused after crash —
POST …/resume, then poll again - Optional lineage — set
parentSessionId; list withGET …/children - Extract transcript before delete — dump
GET …/messages?limit=500to a file (see below) - Cleanup one-shot sessions —
POST …/terminatethenDELETE …/sessions/:id(delete cascades descendants)
Curl recipes: references/session-api.md. Handoff / isolation:
references/handoff.md. Poll helper: scripts/poll-until-done.sh.
Extract transcript (API-side "trace") then delete
There is no HTTP /api/sessions/:id/trace and no ATIF/Markdown export
endpoint on the Session API. UI session export (Markdown / ATIF) is separate.
For Cursor/harness handoffs, the recoverable history is message JSON:
# After idle (or error) — save BEFORE delete
curl -sS "$BASE/api/sessions/$SID/messages?limit=500" \
> ".libragent/work/libr-delegate-${SID}-messages.json"
- Default
limitis 50 — use a high limit for full history - Treat latest assistant
textas the deliverable; keep the JSON dump when debugging tool calls / failures (roleuser|assistant|tool) - Optional deeper analysis of a dumped file: Cursor skill
trace-analyzerexpects.trace.jsonshape — only use it if the dump matches that format; otherwise inspect the messages JSON directly - Order matters: extract → terminate → DELETE. Delete removes DB history
One-shot / automation default: always extract (or confirm you only need the final assistant text) then terminate+delete so sessions do not accumulate.
Create payload essentials
{
"assistantId": "<from GET /api/assistants>",
"request": "<self-contained task + deliverable format>",
"workspacePath": "/absolute/path/optional",
"parentSessionId": "<optional exact parent session id>",
"executionMode": "normal | yolo | unsafe",
"name": "optional label"
}
- Blank/omit
request→ idle (no LLM turn until messages POST) - Unattended automation: prefer
yolo; useunsafeonly when hard-approval tools must also auto-run - Child inherits non-
normalparentexecutionModewhen omitted on create
Polling rules
Wire status is always lowercase (serde(rename_all = "lowercase")):
idle | busy | paused | error | queued | provisioning.
| Status (wire) | Action |
|---|---|
busy / queued / provisioning |
Wait with backoff; do not spam |
idle (after work) |
Read messages; treat latest assistant text as result |
paused |
POST …/resume once, then poll |
error |
Fail the handoff; do not claim success from partial prose |
Preferred poll (normalizes case; exits on terminal):
./.agents/skills/libr-delegate/scripts/poll-until-done.sh "$SID" "$BASE"
If writing a one-off loop, lowercase before match:
status=$(curl -sS "$BASE/api/sessions/$SID" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("status","").lower())')
case "$status" in idle|error|paused) break ;; esac
Follow-ups: another POST …/messages (queued if still busy).
Isolation (critical)
The LibrAgent session is not a clone of this Cursor chat:
- Default: LibrAgent session workspace, not the Cursor CWD unless
workspacePathis set to that absolute path - Put objective, scope, paths, and output format in
request/ message body - Require deliverables in the session’s final assistant text
- Do not assume Cursor files, notes, or tools are visible inside LibrAgent
Anti-patterns
- Calling nonexistent MCP tools (
spawnSession,session_api__*) from Cursor - Echo-only stubs instead of real
POST …/messages - Rapid fixed-interval polls with no backoff
- Matching PascalCase statuses (
Idle/Busy) — API returnsidle/busy; acase Idle)branch never fires and the poll loops until timeout even after the session finished (postmortem: editObject smoke, 2026-04) - Treating paused / empty history / error as success
- Deleting a session before saving
…/messageswhen a transcript is needed - Assuming
~/.libragent/traces/*.trace.jsonor/api/.../traceexists (they are not part of the Session API) - Leaving one-shot delegate sessions undeleted after the handoff completes
- Putting this skill under
src-tauri/bundled_skills(Cursor-only) - Referencing shell vars as bare Python names inside
<<'PY'(e.g.workspacePath: WORKSPACE) →NameError→ empty payload file → create 400EOF while parsing a value curl -d @filewithout checking the file is non-empty valid JSON first
Related repo docs
- HTTP API:
docs/api/http_api.md - In-app sub-agents (product):
docs/user/guides/sub-agents.md