diff --git a/DESIGN-a2a-agents.md b/DESIGN-a2a-agents.md index ad03fcfe..d7396aa2 100644 --- a/DESIGN-a2a-agents.md +++ b/DESIGN-a2a-agents.md @@ -1,6 +1,6 @@ -# DESIGN — Agap Agent Platform v2: the agent algebra +# DESIGN — Agap Agent Platform v2.1: the agent algebra -Status: **v2, agreed with alvis 2026-07-21** (supersedes v1 draft `2499218c`) +Status: **v2.1, agreed with alvis 2026-07-21** (v2 `7be30c71` + hardening review) Owner: alvis · Written with Claude One design, two axioms, one verb. Everything alvis asked for — per-model queues, @@ -23,6 +23,9 @@ rule. This document is the reference; the Kanboard A2A tasks implement it. - **Backbone** — the concrete LLM an agent currently uses for reasoning. - **Context ref** — a pointer (Hindsight bank id, git ref, KB task id, file path) passed *instead of* pasted content. +- **fabric-keeper** — the janitor daemon owning time semantics (§6b): lease + sweeps, deadlines, cron catch-up, dead-letter, inbox digests. It never + assigns work. ## 1. Why @@ -75,6 +78,21 @@ Context travels **by reference, never by value** — the single most important efficiency rule for inter-agent communication (A2A context-passing practice). `sync` vs `async` is not a second mechanism: sync = submit + await. +**Transport rule.** Sync and async share the algebra but not the transport: +**sync goes direct** — an A2A `message/send` RPC straight to the target agent's +endpoint, journaled to KB afterwards; **async/durable goes through KB** and is +drained by polling workers. KB polling must never sit on a sync path — a sync +call may not inherit poll-interval latency. + +### Completion vs verification + +KB convention (native semantics, no new machinery): the **Done column = +unverified completion** — the worker/agent finished and self-reported. **Closing +the task = verified completion.** The producer never closes its own task; the +submitter, a human, or (later) a reviewer-agent closes after checking the +task's acceptance criteria. Lifecycle: … → done (unverified) → closed +(verified). For code, the PR review is the verification; closing follows merge. + ### Theorems — the old rules become consequences 1. **"Queues are per model, not per agent."** Every agent has an inbox, but @@ -132,6 +150,30 @@ fabric free of micro-churn by construction. └─────────────────────────────────────────────────────────────────────┘ ``` +### GPU residency — a local model's a(t) is not 1 + +Local "free" models contend for VRAM with interactive components (measured on +the 8 GB GTX 1070: bge-m3 + gemma3:4b + tei-reranker ≈ 6.2 GB; loading anything +bigger evicts the reranker and silently regresses recall latency). So a local +model's availability is **a(t) = f(VRAM headroom)**, and the model registry +carries a **residency policy**: a never-evict set (embedder, reranker — +interactive-critical), allowed co-residency groups, and a pre-load check every +worker must pass before pulling a model onto a GPU. With more GPUs this becomes +a placement problem — same policy, more slots. + +### Personas and Cards are code + +SOUL.md files and agent Cards live **in git** and are deployed to runtimes — +never edited live in volumes. "Who changed Adolf's soul" must be a `git log` +answer. (The current SOUL.md in the adolf-state volume is migration debt.) + +### Eval gate on backbone/routing changes + +Backbone swap being "one constraint edit" is quality-blind. Each agent keeps a +**golden set** (10–20 canonical exchanges); any backbone or routing change is +shadow-replayed against it and compared (Langfuse datasets/evals) before taking +effect. The algebra's flexibility must not become a silent-degradation machine. + ## 4. A2A: the protocol, adopted now The algebra maps 1:1 onto A2A v1.0 (Jan 2026), which is why we implement the @@ -149,6 +191,10 @@ Claude loop, workers) exposes/consumes A2A; Kanboard remains the durable state behind the endpoints. Scalability/extensibility later (remote nodes, third-party agents) then needs zero redesign. +**Auth is mandatory on every A2A surface.** The LAN is **not trusted** — the +xray/3x-ui VPN terminates other people's peers on it. No unauthenticated +JSON-RPC listener, ever: shared tokens minimum, mTLS preferred. + ## 5. Trust & sandboxing **Trust classes** (on every Card): @@ -183,6 +229,43 @@ Reviewer = human, or later a reviewer-agent (just another persona). (Adolf) outrank background ones (researcher, consolidation) — arbitration lives in the fabric (priorities + a small governor rule), not in LiteLLM. +**Fabric hygiene (runaway protection):** agents submit tasks that cause agents +to submit tasks — idempotency keys stop duplicates, not generative loops. So: +per-agent **task-creation quotas**; an **ancestry depth cap** on provenance +chains; cycle detection at submit; and a **dead-letter** state for poison tasks +after max-retries — never an infinite retry loop through paid quota. + +## 5b. Humans (plural) and memory partitioning + +There is more than one human already (alvis and elizaveta are both on Adolf's +Matrix allowlist) and there will be more. Every human is an agent with +trust=human, their own inbox, and — critically — **their own privacy domain**. + +**Memory partitioning (hard rules):** + +- **Per-human private banks**: `adolf-alvis`, `adolf-elizaveta`, … Everything + learned in conversation with human H goes to H's private bank by default. + **Content from one human's conversations must never surface to another + human.** This is a correctness property, not a preference. +- **One shared household bank** for facts that are explicitly household-wide + (addresses, devices, routines, shared plans). Trusted agents may write; + **promotion from a private bank happens only by that human's explicit action + or approval task** — never automatically. +- **Recall is interlocutor-scoped**: when Adolf talks to H it recalls from H's + private bank + the shared bank, nothing else. The recall/retain hooks select + the bank by interlocutor identity. +- Sandboxed agents (Torgash, researcher) read at most the shared bank; never + any private bank. This is the cross-human face of the memory matrix. +- The current single `adolf` bank is migration debt: split into + `adolf-alvis` + shared. + +**Human inbox design:** notifications are priority-routed — gate/urgent tasks +ping the human via Matrix (Adolf initiates them; cf. proactive-messaging work), +everything else lands in a daily digest from the fabric-keeper. Ignored gate +tasks park and re-remind; **they never default-approve**. Vacation mode: a +human's a(t)=0 parks their inbox like any other scarce queue — gated flows +wait; predefined degraded defaults apply where explicitly configured. + ## 6. Executor — thin KB-polling workers No Temporal/Hatchet: at homelab scale (dozens of tasks/day) a durable-execution @@ -205,6 +288,32 @@ queued. Two workers on one queue never double-run a task (claim is atomic). Idempotency keys on submission prevent duplicate proactive tasks. OpenClaw cron is the proactive *submitter* (Adolf's schedule); workers are the *drainers*. +### 6b. Who is "the scheduler"? — decomposed, plus one janitor + +There is deliberately **no central dispatcher**. Scheduling decomposes into +four concerns, each with its own owner: + +| Concern | Question | Owner | +|---|---|---| +| Triggering | when do tasks appear? | OpenClaw cron, agents' delayed self-submissions, humans | +| Dispatch | which task runs next? | each queue's worker (claim by priority under its a(t)) | +| Admission | may it run now? | LiteLLM budgets/rate + the budget governor | +| **Time semantics** | deadlines, missed crons, stuck tasks? | **the fabric-keeper** | + +The **fabric-keeper** is one tiny always-on daemon that never assigns work: it +sweeps expired leases back to queued, enforces deadlines (escalating to +inboxes), applies per-schedule **cron catch-up policy** (missed window → +run-once | skip, configured per schedule), moves poison tasks to dead-letter, +emits the human daily digest, and exports queue depths to Zabbix. Deadline and +catch-up semantics live here and nowhere else. + +### 6c. Kanboard is tier-0 now + +Promoting KB to the fabric's backbone promotes its ops class: **backups on par +with the vault**, Zabbix monitoring of the service and API, and a defined +**degraded mode** — if KB is down, Adolf still answers Matrix chat (no fabric +operations, no task memory), workers park, nothing crashes or data-loses. + ## 7. Observability — Langfuse (kept), wired for real Decision: keep **Langfuse** (already deployed; best-in-class self-hosted: @@ -251,3 +360,24 @@ completion is traced here — this is where sub-Task granularity lives. 9. Executor = thin KB-polling workers; no Hatchet/Temporal at this scale. 10. Sync routing = LiteLLM Auto Router v2; fabric owns async/parking. 11. Sandbox = per-task workspace lease + container + PR-only merges. + +Added in v2.1 (hardening review, same day): + +12. **Multi-human**: per-human private banks + shared household bank; + interlocutor-scoped recall; cross-human leakage forbidden (hard rule); + promotion to shared only by the owning human's action/approval. +13. A2A auth mandatory everywhere — the LAN is untrusted (VPN peers). +14. KB = tier-0 infrastructure (backup, monitoring, degraded mode). +15. Done column = unverified completion; closed task = verified; the producer + never closes its own task. +16. Scheduler = decomposed (cron/self-submission triggers; workers dispatch; + LiteLLM+governor admit); the **fabric-keeper** janitor owns time semantics; + cron catch-up policy is per-schedule config. +17. Personas/Cards live in git, deployed — never edited live. +18. Backbone/routing changes gated by golden-set shadow eval (Langfuse). +19. Fabric hygiene: creation quotas, ancestry depth cap, cycle detection, + dead-letter for poison tasks. +20. GPU residency policy: local a(t)=f(VRAM); never-evict set (embedder, + reranker); pre-load checks. +21. Transport: sync = direct A2A RPC (journaled); async = KB polling; polling + never on a sync path.