diff --git a/specs/002-multi-user-support/plan.md b/specs/002-multi-user-support/plan.md new file mode 100644 index 0000000..f22fa8e --- /dev/null +++ b/specs/002-multi-user-support/plan.md @@ -0,0 +1,107 @@ +# Plan: Multi-user support (002) + +**Feature**: 002-multi-user-support +**Spec**: [spec.md](./spec.md) +**Date**: 2025-05-21 (design); recorded 2026-08-19 + +Honest status: **designed**, not a shipped tenant model. The tree has an identity +seam. It does not identify speakers. + +## 1. Architecture + +Identity is a string. Memory isolation is that string used as a Supermemory +`container_tag`. Session isolation is `thread_id` (LangGraph checkpointer is +spec 004 — not required to call this design done). + +``` +household user (voice or text) + │ + ▼ + identify → user_id v1: explicit only + │ (--user, /user , "this is ") + ▼ + get_agent(user_id) ──► create_memory_tools(user_id) + │ profile / add / search + │ container_tag = user_id + ▼ + AgentState.user_id + thread_id + │ + ├── long-term: Supermemory container_tag + └── short-term: LangGraph thread (004, not in tree) +``` + +| Piece | Owner | +|-------|--------| +| `DEFAULT_USER_ID`, `get_agent(user_id)`, MemoryChat `/user` | **this repo** (`thelab_langchain`) | +| Supermemory `container_tag` scoping | `create_memory_tools` / `MemoryChat` | +| Speaker diarization / voice embeddings | **out of scope for v1** | +| Checkpointer per `thread_id` | spec 004 | + +Live voice I/O (sibling package) must pass the bound `user_id` into `get_agent()`. +It must not invent a second identity model. + +## 2. Tech choices (locked for v1) + +| Concern | Choice | Why | +|---------|--------|-----| +| Identity | Opaque `user_id` string | No household roster in code or SDD | +| Long-term isolation | Supermemory `container_tag = user_id` | Already the memory API’s tenant key | +| Session isolation | `thread_id`, later `{user_id}::{thread_id}` | Prevents short-term mix when 004 lands | +| Who is talking (v1) | Explicit identification first | `/user`, `--user`, spoken declaration | +| Who is talking (not v1) | Speaker diarization | Out of scope; do not block v1 on it | +| Default session | `DEFAULT_USER_ID` | Single-user path stays one flag | +| Shared facts | Optional household `container_tag` | Only if explicitly stored as shared | +| Brain factory | Existing `get_agent(user_id)` | Do not fork the graph per person | + +## 3. Phases + +### Phase 0 — Identity seam (this repo; in the tree) + +- `DEFAULT_USER_ID` from settings. +- `get_agent(user_id)` / `build_agent_graph(user_id)` bind tools to that id. +- `MemoryChat(user_id)` uses `container_tag=self.user_id`. +- CLI `--user` and `/user ` rebuild the chat for a different container. +- `AgentState` already has `user_id` and `thread_id` fields. + +This is a **container switch**, not speaker ID. + +### Phase 1 — Explicit identification (not shipped) + +- Bind a session to a `user_id` at start, or parse an explicit declaration. +- If unbound / unknown, ask which `user_id` to use; do not guess. +- Voice I/O passes the bound id into `get_agent(user_id)` on every turn. +- Known ids come from config, not from a coded household list. + +### Phase 2 — Isolation completeness (design; not a tenant product) + +- Namespace threads `{user_id}::{thread_id}` once spec 004 has a checkpointer. +- Tests: container A must not recall container B. +- Optional shared household `container_tag` with an explicit write path. +- Still no diarization. + +Phase 2 is finishing **this** isolation design. It is not multi-tenant SaaS. + +## 4. Risks + +| Risk | Mitigation | +|------|------------| +| Calling this “multi-tenant” because `/user` exists | Spec and tasks state: seam only, not speaker ID | +| Cross-container recall via a shared client | Always pass `container_tag=user_id`; never a global search | +| Default `user_id` silently used for the wrong person | Unknown speaker → ask; do not fall back without saying so | +| Inventing a household roster in docs or config | Opaque `user_id` / `container_tag` only | +| Checkpointer mixing threads across users | Spec 004 namespacing; this plan does not fake persistence | +| Diarization scope creep | Keep v1 explicit-only; diarization stays a non-goal | + +## 5. Success metrics + +- Two `user_id` values, two containers: facts stored under A never appear in B’s profile/search. +- `/user ` (or `--user`) changes the container for subsequent turns. +- Unbound session asks for a `user_id` instead of guessing. +- Adding a user is a new id in config, not a code change. +- No speaker-ID model required for the above to be true. + +## 6. What this plan is not + +It is not a shipped tenant model. It is not speaker identification. It is not +spec 004 (checkpointers). It does not define a household of named people. It +does not replace spec 001. diff --git a/specs/002-multi-user-support/spec.md b/specs/002-multi-user-support/spec.md index 6af56d1..043ffd1 100644 --- a/specs/002-multi-user-support/spec.md +++ b/specs/002-multi-user-support/spec.md @@ -1,98 +1,122 @@ # Feature Spec: Multi-User Support for the Voice Agent -**Feature ID**: 002-multi-user-support -**Status**: Draft / Future -**Related to**: [001-voice-dgx-spark-agent](../001-voice-dgx-spark-agent/spec.md) +**Feature ID**: 002-multi-user-support +**Status**: Designed; not a shipped tenant model +**Related to**: [001-voice-dgx-spark-agent](../001-voice-dgx-spark-agent/spec.md) **Created**: 2025-05-21 +**Recorded here**: 2026-08-19 +**Owner**: Derek Clair + +## Current state (honest) + +This spec is the **design** for per-user isolation. It is not a product multi-tenant system. + +Code in this repo today is an identity **seam**, not speaker ID: + +- `DEFAULT_USER_ID` in settings +- `user_id` on `get_agent()` / `build_agent_graph()` and `AgentState` +- Supermemory calls scoped with `container_tag=user_id` +- MemoryChat CLI `--user` and `/user ` (rebuilds the chat for that container) + +There is no speaker diarization, no voice fingerprint, and no automatic “who is talking” path. A `/user` switch is an explicit container change. ## Overview -The voice agent should gracefully support multiple users within the same household (initially: Derek, wife, two daughters, son, and occasional "other" guests or family members). +The voice agent should support multiple **household users** on the same deployment. -Each person should have their own persistent identity and long-term memory context. When someone speaks to the agent, it should correctly identify who they are (or be told) and recall the right history, preferences, ongoing projects, and relationships. +Each user is an opaque `user_id`. Long-term memory is isolated by a Supermemory `container_tag` (the same string as `user_id`). Short-term conversation state is isolated by `thread_id`. When someone speaks, the session must already be bound to a `user_id`, or the speaker must **declare** it. -This is a **cross-cutting concern** that affects user identification, Supermemory container isolation, session/thread management, the LangGraph state, and the overall voice experience. +This is a **cross-cutting concern**: identification, Supermemory container isolation, session/thread management, LangGraph state, and the voice loop. ## Goals -- Natural multi-user experience in a family setting. -- Strong long-term memory isolation per person (via Supermemory `container_tag`). -- Reasonable accuracy in knowing "who is talking" without constant re-identification. -- Future-proof for adding more family members or occasional guests. -- Maintain privacy boundaries between users. +- Natural multi-user experience for household users. +- Strong long-term memory isolation per `user_id` (via Supermemory `container_tag`). +- Low-friction identification: explicit declaration first; do not require a login ritual every turn once the session is bound. +- Adding another `user_id` is configuration, not a rewrite. +- Privacy boundaries between users: no cross-container recall. -## Non-Goals (for initial version) +## Non-Goals (for v1) -- Full biometric voice fingerprinting / speaker diarization (nice to have later). -- Remote multi-user access from outside the home. -- Complex household roles/permissions system. -- Guest accounts with temporary memory. +- Speaker diarization / biometric voice fingerprinting (out of scope for v1; possible later). +- Remote multi-user access from outside the deployment. +- Roles, permissions, or an admin/RBAC model. +- Guest accounts with temporary memory (open question, not v1). +- Inferring household relationships from names or stories. ## User Stories -1. **As Derek**, I want the agent to remember my ongoing projects, preferences, and conversations even when other family members have spoken to it recently. -2. **As my wife**, I want the agent to remember things that are important to me (kids' schedules, our shared tasks, etc.) without mixing them up with Derek's work stuff. -3. **As a kid**, I want the agent to know who I am when I talk to it and remember things like my homework, favorite games, or ongoing stories. -4. **As a parent**, I want to be able to say "Hey Lab, this is Sarah talking" or have the system figure it out reasonably well. -5. **As the household**, we want the agent to understand family relationships ("my sister", "Dad", "the kids") when context is relevant. +1. **As a household user**, I want memories, preferences, and ongoing work scoped to my `user_id` even if another household user spoke to the agent recently. +2. **As a household user**, I want my Supermemory `container_tag` isolated so another user’s facts are not injected into my turns. +3. **As a household user**, I want to identify myself explicitly (or start a session already bound to my `user_id`) so the agent uses the right container. +4. **As the operator**, I want adding or switching a `user_id` to be a low-effort config / command, not a new deployment. +5. **As a household user**, if the agent does not know which `user_id` is speaking, I want it to ask rather than guess. ## Functional Requirements ### FR-1: User Identity & Routing -- The system must be able to associate a voice interaction with a specific user identity. -- Supported identification methods (in rough priority order): - 1. Explicit declaration ("Hey Lab, it's Derek") - 2. Wake-word + name patterns - 3. Heuristic / voice characteristics (future) - 4. Device or room context (if multiple microphones are added later) + +- Every voice or text interaction must be associated with a specific `user_id`. +- Identification methods for v1, in priority order: + 1. Explicit declaration (e.g. “this is ``”) or session start with `--user` / `/user ` + 2. Wake-word + declared-name patterns (same explicit idea; not voice biometrics) + 3. Heuristic / voice characteristics — **out of scope for v1** + 4. Device or room context — later, if multiple capture devices exist +- Speaker diarization is **out of scope for v1**. ### FR-2: Memory Isolation (Supermemory) -- Every user must have their own `container_tag` in Supermemory. -- All `profile()`, `add()`, and `search` calls must be correctly scoped to the identified user. -- Cross-user leakage must be prevented (the agent should not accidentally recall one person's private facts to another). + +- Every `user_id` has its own `container_tag`. +- All `profile()`, `add()`, and `search` calls must be scoped to the identified `user_id`. +- Cross-user leakage must be prevented (no accidental recall of one container’s facts into another). ### FR-3: Session & Thread Management -- Each user should have their own conversation threads (`thread_id`). -- Short-term memory (LangGraph checkpointer) must be isolated per user. -- It should be possible to have parallel conversations with different family members. -### FR-4: Relationship & Household Context -- The agent should be able to reason about family relationships when given the right context ("Tell my wife...", "What does Dad usually say about this?"). -- There may be a lightweight "household" or "family" memory layer in addition to individual profiles. +- Each user has their own conversation threads (`thread_id`). +- Short-term memory (LangGraph checkpointer, when spec 004 lands) must be isolated per user. +- Parallel conversations for different household users must not share thread state. + +### FR-4: Shared household container (optional) + +- Individual profiles stay per `user_id` / `container_tag`. +- There may be a lightweight **shared** household `container_tag` for facts that are explicitly stored as shared — not a substitute for per-user isolation. +- The agent must not invent a household roster or infer private relationships. + +### FR-5: Unknown speakers -### FR-5: Graceful Handling of Unknown Speakers -- If the agent cannot confidently identify the speaker, it should ask for clarification in a friendly way ("Sorry, I didn't catch who I'm speaking with — is this Derek, Sarah, or one of the kids?"). +- If the agent cannot bind a turn to a `user_id`, it asks for clarification (e.g. “Which `user_id` should I use for this session?”). +- It must not guess a container. ## Non-Functional Requirements -- **Privacy**: One family member's private memories or conversations must never leak to another. -- **Low Friction**: Identification should feel natural, not like logging into a system every time. -- **Scalability**: Design should support adding more users without major rewrites. -- **Auditability** (future): It should be possible to see which user a memory belongs to. +- **Privacy**: One user’s private memories or conversations must never leak to another `user_id`. +- **Low friction**: Identification should feel like a one-time bind for the session, not a login on every turn. +- **Scalability**: Design supports additional `user_id` values without major rewrites. +- **Auditability** (future): It should be possible to see which `user_id` a memory belongs to. ## Open Questions -- How do we initially bootstrap user identities? (Manual config file? First-time "register yourself" flow?) -- Should there be a concept of a "primary user" (Derek) who has elevated capabilities? -- Do we want speaker diarization / voice embedding models running locally on the DGX for passive identification? -- How do we handle "other" / guests? Temporary containers? A generic "guest" profile? -- Should the agent proactively learn voices over time ("You sound like Maya today")? +- How do we bootstrap known `user_id` values? (Config file vs first-time “register this id” flow.) +- Is there a default `user_id` (`DEFAULT_USER_ID`) for single-user sessions, or must every session declare one? +- Do we want speaker diarization / voice embeddings later (local, on-box)? Not v1. +- How do we handle unknown / guest speakers? A generic `guest` container vs refuse until identified. +- Optional shared household `container_tag`: what is allowed to be written there, and who can read it? ## Relationship to Feature 001 -This feature is a natural evolution of the single-user voice agent defined in 001. +This feature extends the single-user voice agent in 001. -The core architecture decisions made in 001 (Supermemory `container_tag` per user, `thread_id` per session, LangGraph state) were intentionally designed to be multi-user friendly. This spec captures the additional work needed to make the experience truly multi-user in a family context. +001 already assumed `container_tag` per user and `thread_id` per session. This spec is the extra work to make that a real multi-user experience: explicit identification, isolation guarantees, and unknown-speaker handling. It does not replace 001 and does not implement spec 004 (checkpointers). -## Success Criteria (for when we eventually implement) +## Success Criteria (when implemented) -- Derek, his wife, and both kids can have natural, separate ongoing conversations with the agent over weeks/months with correct memory recall. -- The agent rarely confuses one person's context with another's. -- Adding a new family member is a low-effort configuration task. -- The experience feels personal and "knows" each person without feeling creepy or overly technical. +- Distinct household users can keep separate ongoing conversations with correct memory recall. +- The agent does not mix one `user_id`’s context into another’s. +- Adding a new `user_id` is a low-effort configuration task. +- Identification is explicit first; no biometric path is required for v1. --- -**Status**: This spec is captured for future planning. It is **not** in scope for the current implementation wave. +**Status**: Design captured for planning. **Not** a shipped tenant model. The identity seam (`DEFAULT_USER_ID`, `get_agent(user_id)`, MemoryChat `/user`) is in the tree; speaker ID and product isolation are not. -Next time we pick up multi-user work, we should create a `plan.md` and `tasks.md` under this directory following the established SDD process. \ No newline at end of file +See [plan.md](./plan.md) and [tasks.md](./tasks.md) for the SDD record of what exists vs what remains. diff --git a/specs/002-multi-user-support/tasks.md b/specs/002-multi-user-support/tasks.md new file mode 100644 index 0000000..0b18e20 --- /dev/null +++ b/specs/002-multi-user-support/tasks.md @@ -0,0 +1,45 @@ +# Tasks: Multi-user support (002) + +**Feature**: 002-multi-user-support +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +Checkboxes record what is in this tree today versus what remains design-only. +This file was filled in when the SDD record was completed, not when the +identity seam was first written. This is **not** a shipped tenant model. + +## Phase 0 — Identity seam (this repo) + +- [x] `DEFAULT_USER_ID` in settings (`thelab_langchain.config`) +- [x] `user_id` on `get_agent()` / `build_agent_graph()` +- [x] `AgentState.user_id` and `AgentState.thread_id` fields +- [x] `create_memory_tools(user_id)` scopes `profile` / `add` / `search` with `container_tag=user_id` +- [x] `MemoryChat(user_id)` uses the same `container_tag` +- [x] CLI `--user` and `/user ` (rebuilds MemoryChat for that container) +- [x] Voice orchestrator accepts `user_id` and passes it into `get_agent()` +- [ ] LangGraph checkpointer per thread (spec 004 — not required to call 002’s seam done) +- [ ] Speaker identification (not in tree; `/user` is not speaker ID) + +## Phase 1 — Explicit identification (not shipped) + +- [ ] Bind a session to a `user_id` at start (flag, config, or spoken declaration) +- [ ] Parse explicit “this is ``” (or equivalent) and switch the bound id +- [ ] If the turn cannot be bound, ask which `user_id` to use; do not guess +- [ ] Known ids from config only — no coded household roster +- [ ] Voice I/O (sibling package) must pass the bound `user_id` into `get_agent()` every turn +- [ ] Document the bind/switch commands next to `/user` without treating them as speaker ID + +## Phase 2 — Isolation completeness (design) + +- [ ] Thread namespacing `{user_id}::{thread_id}` when spec 004 has a checkpointer +- [ ] Test: memories stored under `user_a` are not returned for `user_b` +- [ ] Test: `/user` (or equivalent) actually changes `container_tag` for the next turn +- [ ] Optional shared household `container_tag` with an explicit write path +- [ ] Guest / unknown policy (refuse vs generic `guest` container) — decide, then implement +- [ ] Speaker diarization / voice embeddings — out of scope for v1 + +## Traceability + +Phase 0 lives in this package (`thelab_langchain.config`, `agent.graph.get_agent`, +`agent.tools.memory.create_memory_tools`, `chat.MemoryChat`, `cli` `/user`). +Phases 1–2 are not done. This tasks file is only the checklist view of the +design plus the identity seam that already exists. diff --git a/specs/003-deployment-infrastructure/plan.md b/specs/003-deployment-infrastructure/plan.md new file mode 100644 index 0000000..c5d6884 --- /dev/null +++ b/specs/003-deployment-infrastructure/plan.md @@ -0,0 +1,113 @@ +# Plan: Deployment infrastructure & Dockerization (003) + +**Feature**: 003-deployment-infrastructure +**Spec**: [spec.md](./spec.md) +**Date**: 2026-08-19 (SDD record; spec drafted 2025-05-21) + +## 1. Architecture + +Two deployment stories. Only one of them is the live spoken path. + +``` +Live desk voice (NOT this compose) + Lenovo Go (ALSA / HID) + │ + ▼ + conversational-voice-agent ← STT / TTS / button / LED + │ get_agent() + ▼ + this package (thelab_langchain) + +Experimental compose in *this* tree + agent ──gRPC──► riva + ──HTTP──► nemotron NIM +``` + +| Piece | Owner | +|-------|--------| +| Live STT, TTS, USB I/O | [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) ([spec 008](../008-local-tts-lenovo-go-spike/plan.md)) | +| LangGraph brain, provider factory | **this repo** | +| Experimental `agent` + `riva` + `nemotron` Compose | `docker-compose.yml` here — **not** production voice | +| Riva wrappers in `src/thelab_langchain/voice/` | Spike / Phase 2; streaming still `NotImplementedError` | + +`docker compose up` of this file is an experiment toward spec 001's Riva/NIM stack. It is not how the desk currently talks. + +## 2. What is already in the tree + +Honest inventory — the May 2025 spec "current state" is stale. + +| Artifact | What it actually is | +|----------|---------------------| +| `Dockerfile` | Multi-stage slim Python image, non-root `appuser`, PortAudio for `sounddevice`. Exists. | +| `docker-compose.yml` | Three services (`agent`, `riva`, `nemotron`) with Compose **profiles**, `restart: unless-stopped`, named model volumes. Experimental. | +| YAML `healthcheck:` blocks | Present on all three services. Agent probe is `import thelab_langchain` (import-ok, not readiness). Riva/NIM HTTP probes are unproven on this hardware. **Still a gap.** | +| YAML `deploy.resources` GPU reservations | Present. Swarm-style `deploy.devices` is not a verified `docker compose up` GPU story. No CPU/memory limits. No light-vs-full GPU profiles. **Still a gap.** | +| `.env.example` | Documents Python/app keys and provider flags. Not compose-time validation. | +| Makefile `docker-build` / `docker-push` | Local helpers. Tag and registry come from the operator's environment. This SDD does not name a registry host. | + +There is no `docker-compose.override.yml`, no model-downloader service, no secrets driver, and no CI image build (see [005](../005-testing-and-cicd/plan.md)). + +## 3. Tech choices (locked for this spec) + +| Concern | Choice | Why | +|---------|--------|-----| +| Live voice I/O | Sibling `conversational-voice-agent` | Spec 008 already runs on the desk; do not pretend Compose is that path | +| Experimental GPU stack | Compose on a single DGX Spark | Spec out of scope: Kubernetes / multi-node | +| Agent image | Existing multi-stage `Dockerfile` | Same image should run on a laptop CI runner *or* Spark; no Mac-only layers | +| GPU in Compose | Compose-native device requests (`gpus` / device_requests), not Swarm-only `deploy` | `docker compose up` is the intended command | +| Secrets | Host `.env` (gitignored); never bake keys into compose YAML or this SDD | No secrets in compose docs | +| Private registry | Operator sets `REGISTRY` + `TAG`; push/pull is a documented workflow, not a hostname in git | Do not commit a registry URL | +| LLM switch | Existing `LLM_PROVIDER` / `openai_compatible` | One-line switch; Compose must not fork the factory | +| Observability | Deferred (spec Phase C) | Structured logs later; no metrics stack in this wave | + +Do not put API keys, NGC tokens, or example secret values in compose comments or this directory. + +## 4. Phases + +### Phase A — Reliability & DX (high value) + +Close the gaps that make the *experimental* stack start and fail loudly: + +- Healthchecks that mean "ready for traffic", not "Python import succeeded". Agent should wait on real Riva/NIM readiness when those profiles are used. +- Compose-native GPU reservations that `docker compose up` honors; CPU/memory limits so one service cannot starve the box. +- `.env.example` fields the compose stack actually reads, with required vs optional called out (names only — no values). +- Compose profiles that match the comments (`full`, `agent`, `riva-only`, `nemotron-only`). Today every service has a profile, so a bare `docker compose up` starts nothing. +- A documented **hack-on-the-brain** path: venv + mocked / host LLM, no Riva container required. Live spoken testing stays in the sibling repo. + +Dockerfile multi-stage + non-root is already done; do not redo it unless a probe or user change requires it. + +### Phase B — Volumes, registry, restarts + +- First-run model download / cache story for Riva and NIM volumes (script or one-shot service). Version the cache layout; do not copy weights into the agent image. +- Private registry workflow: build → tag (`git-sha` and optional semver) → push → pull on Spark. Registry hostname stays in the operator's environment, not in SDD. +- Restart backoff that survives model-load and GPU OOM without a tight crash loop. +- Basic structured logging (no Prometheus/Grafana yet). + +### Phase C — Later + +- Secrets management (Docker secrets / a vault) instead of plain env. +- Observability stack (when the team is ready). +- Automated image builds in CI — owned with spec 005; this spec only requires the image to *be* buildable. + +## 5. Risks + +| Risk | Mitigation | +|------|------------| +| Treating `docker-compose.yml` as the live voice stack | This plan + README: live path is the sibling I/O repo; Compose is experimental | +| Riva `2.15.0` image vs current Spark GPU | Compose already notes GB10 may not run that tag; do not block desk voice on it | +| Large NIM as compose default vs spec 007 budget | Do not bless 120B-class agent loops on one Spark; keep NIM as an experiment | +| Swarm `deploy.resources` ignored by Compose | Move GPU requests to a Compose-native key and verify with `nvidia-smi` in-container | +| Import-only agent healthcheck | Replace with a real ready check; `depends_on: service_healthy` is useless until then | +| Secrets or a registry hostname landing in git | `.env` gitignored; SDD and compose comments stay hostname-free and key-free | + +## 6. Success metrics + +- A new person can follow a runbook and bring up the **experimental** full profile on a Spark in under two hours when models are cached (spec success criterion). This is not "desk voice works." +- Agent image rebuilds independently of Riva/NIM images. +- `LLM_PROVIDER` remains a one-line switch (already true in code). +- Individual experimental services can restart without taking the others down permanently. +- Live spoken sessions still go through [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent). + +## 7. What this plan is not + +It is not a rewrite of spec 001. It is not the Lenovo Go spike (008). It is not Kubernetes, canary deploys, or cost work across multiple Sparks (spec out of scope). It does not document a private registry hostname or any credentials. diff --git a/specs/003-deployment-infrastructure/tasks.md b/specs/003-deployment-infrastructure/tasks.md new file mode 100644 index 0000000..4f3762e --- /dev/null +++ b/specs/003-deployment-infrastructure/tasks.md @@ -0,0 +1,65 @@ +# Tasks: Deployment infrastructure & Dockerization (003) + +**Feature**: 003-deployment-infrastructure +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +Checkboxes are the honest tree as of this SDD record, not the May 2025 spec +"current state". Compose here is **experimental**. Live voice I/O is +[`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent). + +Do not commit secrets, serials, or a registry hostname in any of this work. + +## Already in tree (do not redo as if missing) + +- [x] Multi-stage `Dockerfile` (builder wheel → slim runtime, non-root user) +- [x] Root `docker-compose.yml` with `agent`, `riva`, `nemotron` services +- [x] Compose profiles keys (`full`, `agent`, `riva` / `riva-only`, `nemotron` / `nemotron-only`) +- [x] `restart: unless-stopped` on those services +- [x] Named volumes declared for Riva / NIM caches +- [x] `.env.example` for the Python app (provider + key *names*) +- [x] Makefile `docker-build` / `docker-push` taking `REGISTRY` + `TAG` from the environment + +## Phase A — Reliability & DX + +YAML stubs exist for health and GPU; they are **not** done. + +- [ ] Real healthchecks: agent ready for traffic (not `import thelab_langchain`) +- [ ] Real healthchecks: Riva and NIM probes verified on the images we actually run +- [ ] Agent `depends_on` / startup order that waits on those probes when using `full` +- [ ] Compose-native GPU device requests that `docker compose up` honors (not Swarm-only `deploy.resources`) +- [ ] CPU and memory limits per service +- [ ] Light vs full GPU allocation profiles (documented, not just a comment) +- [ ] Bare `docker compose up` vs `--profile` behavior matches the file comments (today a bare up starts nothing) +- [ ] `.env.example` lists compose-relevant variables; required vs optional; **no secret values** +- [ ] Document hack-on-the-brain: `make install` / `make chat` with host or mocked LLM, no Riva +- [ ] Document that spoken I/O is the sibling repo, not this compose file + +## Phase B — Volumes, registry, restarts + +- [ ] First-run model download / cache helper (script or one-shot service) +- [ ] Document volume layout and how caches are shared; do not copy weights into the agent image +- [ ] Registry workflow in a runbook: build → tag `git-sha` (and optional semver) → push → pull on Spark +- [ ] Keep registry hostname out of git and out of this SDD (operator env only) +- [ ] Restart policy / backoff that survives model load and GPU OOM +- [ ] Basic structured logging configuration (no metrics stack) + +## Phase C — Later + +- [ ] Secrets mechanism other than plain env (Docker secrets or a vault) +- [ ] Observability stack (deferred with the spec) +- [ ] Automated image build in CI (tracked in [005](../005-testing-and-cicd/tasks.md); this spec only needs the image to stay buildable) + +## Out of scope (leave unchecked on purpose) + +- [ ] Kubernetes / Spark-specific cluster orchestration +- [ ] Canary / blue-green +- [ ] Multi-node cost / placement +- [ ] Replacing the live 008 voice path with Riva Compose + +## Traceability + +Implementation of the live spoken path is +[`derekclair/conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent). +This tasks file is the checklist for *this* repo's experimental Compose/Docker +gaps. Dockerfile + compose skeleton are already here; health, Compose GPU, and +registry hygiene are not. diff --git a/specs/004-persistence-checkpointers/plan.md b/specs/004-persistence-checkpointers/plan.md new file mode 100644 index 0000000..9444fbc --- /dev/null +++ b/specs/004-persistence-checkpointers/plan.md @@ -0,0 +1,103 @@ +# Plan: Persistence & checkpointers (004) + +**Feature**: 004-persistence-checkpointers +**Spec**: [spec.md](./spec.md) +**Date**: 2025-05-21 (spec); recorded 2026-08-19 + +## 1. Architecture (as shipped) + +Short-term conversation state is **not** a LangGraph checkpoint. `get_agent()` +compiles with no `checkpointer` argument. Turns that survive a process only +do so because the **caller** keeps a message list. + +``` +Live voice I/O (sibling) this package + session Human/AI list get_agent(user_id) + │ │ + └── graph.invoke({messages}) ──► graph.compile() # no checkpointer + │ + memory_injection → call_llm → execute_tools + │ + ▼ + Supermemory (long-term only) +``` + +| Piece | Owner today | +|-------|-------------| +| Per-turn history | Caller. Live path is [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent): accumulate `HumanMessage` / `AIMessage` for the session. | +| LangGraph checkpoint | **None.** `get_agent()` is `return graph.compile()`. | +| `thread_id` on state | Data field / log tag. Not `config["configurable"]["thread_id"]`. | +| In-tree `VoiceOrchestrator` | Invokes with **this turn only** (`[HumanMessage(text)]`). Does not accumulate. | +| Long-term facts | Supermemory tools (`create_memory_tools`). Out of scope for 004. | + +`MemorySaver` is **not** implicit. Omitting `checkpointer` means no thread +memory inside the graph. Each `invoke` sees only the `messages` the caller +passed. + +## 2. Tech choices + +### Locked now (honest) + +| Concern | Choice | Why | +|---------|--------|-----| +| Checkpointer | Not wired | Spec 008 does not require it; nothing here survives a restart | +| Session history | Caller-side list | Sibling already does this; do not double-store | +| Durable short-term | None | Process death / reboot drops in-flight turns | +| Long-term | Unchanged Supermemory | Spec 006 | + +### If this spec is picked up later (not started) + +| Concern | Intended choice | Why | +|---------|-----------------|-----| +| Dev | `MemorySaver` | In-process only | +| Default durable | SQLite (`langgraph-checkpoint-sqlite`) + volume | Single host, low ops | +| Upgrade | Postgres via env | Same factory | +| Factory | `get_checkpointer()` + `CHECKPOINTER_BACKEND` | `memory` / `sqlite` / `postgres` | +| Isolation | `{user_id}::{thread_id}` as LangGraph `thread_id` | Spec 002; do not confuse with `AgentState.thread_id` | + +## 3. Phases + +### Phase 0 — Document current in-memory behavior (this SDD) + +- Record that `compile()` has no checkpointer. +- Record caller-side accumulation on the live voice path. +- Record that the in-tree orchestrator is single-turn per invoke. + +### Phase 1 — Factory + wire (not started) + +- `get_checkpointer()` in the agent package. +- Pass it into `graph.compile(checkpointer=...)`. +- Invoke with `configurable.thread_id` (namespaced). +- Decide whether the sibling still accumulates, or the graph becomes the source of history (do not do both blindly). + +### Phase 2 — Durable backend (not started) + +- SQLite file on a persistent volume. +- Postgres as a config change, not a second graph. +- Optional last-N checkpoint cleanup. + +Phase 1–2 are **not** in this tree. Do not treat this plan as a claim they shipped. + +## 4. Risks + +| Risk | Mitigation | +|------|------------| +| Double history (checkpointer + caller list) | Pick one owner of short-term turns before wiring | +| `AgentState.thread_id` vs LangGraph config `thread_id` | Namespacing lives in `configurable`; state field is not a checkpoint key | +| Spec text that called MemorySaver “implicit default” | This plan supersedes that: it is opt-in | +| Backend swap later | Factory + one env var; no graph fork | + +## 5. Success metrics (only after Phase 1–2) + +- Restarting the agent process does not drop an active thread. +- Two `user_id` values cannot read each other’s short-term state. +- SQLite → Postgres is env + volume, not a rewrite. + +None of these hold today. + +## 6. What this plan is not + +It is not an implementation of `get_checkpointer()`. It is not a SQLite volume +in compose. It is not a requirement to call spec 008 done. It is not +persistence of audio / LED / VAD state. It is not a second long-term memory +store (that is 006, and 006 is also not building adapters). diff --git a/specs/004-persistence-checkpointers/tasks.md b/specs/004-persistence-checkpointers/tasks.md new file mode 100644 index 0000000..2fccdb4 --- /dev/null +++ b/specs/004-persistence-checkpointers/tasks.md @@ -0,0 +1,47 @@ +# Tasks: Persistence & checkpointers (004) + +**Feature**: 004-persistence-checkpointers +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +Checkboxes record what is actually in the tree. A LangGraph checkpointer is +**not** wired. Phase 0 is this SDD record. Leave Phase 1–2 unchecked until +code ships. + +## Phase 0 — Document current in-memory behavior + +- [x] Record that `get_agent()` is `graph.compile()` with no `checkpointer` +- [x] Record that live voice I/O accumulates `HumanMessage` / `AIMessage` caller-side +- [x] Record that in-tree `VoiceOrchestrator` invokes with the current turn only +- [x] Record that `AgentState.thread_id` is not LangGraph `configurable.thread_id` +- [x] Record that `MemorySaver` is opt-in, not an implicit default + +## Phase 1 — Factory + wire (not started) + +- [ ] Add `get_checkpointer()` in the agent package +- [ ] `CHECKPOINTER_BACKEND` env (`memory` / `sqlite` / `postgres`) +- [ ] Pass the checkpointer into `graph.compile(...)` +- [ ] Namespace LangGraph `thread_id` as `{user_id}::{thread_id}` +- [ ] Invoke with `config={"configurable": {"thread_id": ...}}` +- [ ] Choose one owner of short-term history (graph vs caller); do not double-store +- [ ] Tests that two thread ids do not share checkpoint state (in-memory backend) + +## Phase 2 — Durable backend (not started) + +- [ ] SQLite checkpointer + persistent volume +- [ ] Postgres path as the same factory, different env +- [ ] Document backend swap (env + volume; no graph fork) +- [ ] Optional last-N checkpoint cleanup per thread + +## Out of scope (do not check as 004 done) + +- [ ] LangGraph checkpointer required for spec 008 +- [ ] Persist voice / audio / LED state +- [ ] Second long-term memory backend (spec 006) +- [ ] Multi-tenant product isolation (spec 002 is design-only) + +## Traceability + +`src/thelab_langchain/agent/graph.py` — `get_agent()` compiles with no +checkpointer. Live session lists live in +[`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent), +not in this graph. diff --git a/specs/005-testing-and-cicd/plan.md b/specs/005-testing-and-cicd/plan.md new file mode 100644 index 0000000..61a611c --- /dev/null +++ b/specs/005-testing-and-cicd/plan.md @@ -0,0 +1,127 @@ +# Plan: Testing strategy, CI/CD, and coverage (005) + +**Feature**: 005-testing-and-cicd +**Spec**: [spec.md](./spec.md) +**Date**: 2026-08-19 (SDD record; spec drafted 2025-05-21) + +## 1. Architecture + +Tests live in this repo. CI is GitHub Actions on a CPU runner. Live spoken I/O +and GPU voice loops are **not** in this workflow. + +``` +PR / push + │ + ▼ +.github/workflows/ci.yml ubuntu-latest, no GPU + ├── ruff check . + └── pytest -q tests/ only, mocked services +``` + +| Layer | Where | What it covers | +|-------|--------|----------------| +| Unit (shipped) | `tests/` | Graph routing, memory injection, config keys, LLM factory, prompt block | +| CI (shipped) | `.github/workflows/ci.yml` | Ruff + pytest, CPU-only install (`pip install -e . --no-deps` + lightweight deps) | +| Coverage gates | **not shipped** | spec target 60% overall / 80%+ on `agent/` and `voice/` | +| Docker image CI | **not shipped** | build (and later push) of the agent image | +| GPU / hardware e2e | **not shipped** | out of scope for every PR (spec) | + +The May 2025 spec "current state" (`No tests/ directory`, `No GitHub Actions`) is +**wrong today**. Do not plan as if those are missing. + +## 2. What is already in the tree + +| Artifact | Notes | +|----------|--------| +| `tests/test_agent_graph.py` | `_should_continue` routing; `_memory_injection` with mocked tools; no extra summarization LLM | +| `tests/test_chat.py` | `MemoryContext.to_prompt_block` | +| `tests/test_config.py` | `Settings.validate_keys` per provider | +| `tests/test_llm.py` | `get_chat_model` routing with fake provider modules | +| `pyproject.toml` | `pytest` + `pytest-asyncio`; `[tool.pytest.ini_options]` `testpaths = ["tests"]`, `asyncio_mode = auto` | +| `ruff` / `mypy` | Dev deps. `make lint` runs both. **CI runs ruff only**, not mypy. | +| CI install | Skips `sounddevice` / `nvidia-riva-client` so a plain runner can import the brain | + +There is **no** `make test` target (Makefile has `lint`, not pytest). There is +**no** `pytest-cov` / coverage config. There is **no** image-build job. There is +**no** GPU job. + +## 3. Tech choices (locked for this spec) + +| Concern | Choice | Why | +|---------|--------|-----| +| Runner | GitHub Actions `ubuntu-latest` | Matches the existing workflow | +| Unit suite | pytest, mocked Supermemory / LLM | Fast, no keys, no GPU, no PortAudio | +| Lint on PR | ruff (already) | Keep the current job; add mypy later, do not drop ruff | +| Coverage | `pytest-cov` / coverage.py when we add gates | Spec names these; do not invent a hosted-coverage vendor requirement | +| Image CI | Separate job or workflow, not on the CPU unit job | Unit job must stay lightweight | +| GPU e2e | Gated / manual / self-hosted later | Spec: not on every PR | +| Live voice | Sibling [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) | Do not put ALSA / Parakeet / Piper in this repo's CI | + +No API keys in workflow files. No registry hostname in workflow YAML committed +to this repo; if a later push job needs a registry, it reads from Actions +secrets / env that are not documented as literals here. + +## 4. Phases + +### Phase 0 — Unit + CPU CI (done) + +Keep: + +- `tests/` as the example for new tests (spec success: contributors copy these). +- CPU-only CI install so audio/GPU deps do not break the runner. +- Ruff + pytest on push and pull_request. + +Do not delete or "bootstrap" a tests directory that already exists. + +### Phase 1 — Coverage, DX, types in CI + +- Add coverage measurement (`pytest-cov` or coverage.py) and a **gate** that + matches the spec's intent: fail on significant drops / below the initial + floor (60% overall; 80%+ on `agent/` and `voice/` once those packages are + measured honestly — `voice/` is mostly untested Riva spike code). +- `make test` (and optionally `make test-cov`) so local DX matches CI. +- Add mypy to CI if we want the spec's "lint + type check" line; local + `make lint` already runs it. +- Cache pip in Actions. + +### Phase 2 — Docker image CI + +- Build the agent image from the existing `Dockerfile` on tags and/or main. +- Tag with `git-sha` (and semver when we cut tags). +- Push is optional and operator-configured. Do not bake a registry URL into + the workflow file in git. +- Multi-platform only if we prove we need it (Spark is aarch64; CI is amd64). + +Compose stack smoke (`docker compose` healthy) is integration, not this unit +job. It stays gated. + +### Phase 3 — Heavier tests (gated) + +- Graph integration with real-ish tools (still no live Supermemory account in CI). +- Voice loop with mocked audio / Riva — this repo's `voice/` module, not 008. +- GPU e2e on a Spark or a GPU runner: **not** every PR. +- Hardware-in-the-loop with the Lenovo Go: out of scope for this package's CI. + +## 5. Risks + +| Risk | Mitigation | +|------|------------| +| Planning as if `tests/` or CI do not exist | This plan; mark those tasks `[x]` | +| Coverage gate that punishes the untested `voice/` spike | Measure `agent/` first; do not fail the repo for Riva `NotImplementedError` paths until we test them | +| Pulling audio/GPU wheels on `ubuntu-latest` | Keep the CPU-only `--no-deps` install in CI | +| Image push leaking a registry hostname or credentials | Secrets only; SDD stays hostname-free | +| Treating Compose e2e as desk voice | Compose is experimental (003); live I/O is the sibling repo | + +## 6. Success metrics + +- `pytest` locally (ideally `make test`) runs the unit suite in well under 30s. +- Every PR gets ruff + unit results (already true). +- Coverage gate exists before we claim "CI enforces coverage." +- Image builds in CI before we claim "we can cut a Spark image from git." +- New tests follow `tests/test_*.py` patterns (mocked services, no keys). + +## 7. What this plan is not + +It is not a claim that the May 2025 spec current-state bullets are still true. +It is not GPU voice CI. It is not mutation testing or a performance bench in +Actions (spec out of scope). It is not the 008 hardware loop. diff --git a/specs/005-testing-and-cicd/spec.md b/specs/005-testing-and-cicd/spec.md index da780d4..f1c7f98 100644 --- a/specs/005-testing-and-cicd/spec.md +++ b/specs/005-testing-and-cicd/spec.md @@ -1,23 +1,23 @@ # Feature Spec: Testing Strategy, CI/CD, and Coverage **Feature ID**: 005-testing-and-cicd -**Status**: Draft +**Status**: Partial (unit tests + CPU CI exist; coverage gates and image CI do not) **Related**: 001-voice-dgx-spark-agent, 003-deployment-infrastructure -**Date**: 2025-05-21 +**Date**: 2025-05-21 +**Updated**: 2026-08-20 ## Overview -The repository currently has almost no automated tests, no CI pipeline, and no coverage measurement. As the system grows (especially the agent brain, voice layer, and multi-service Docker stack), this becomes a major risk. +The agent graph, config, and LLM factory need automated tests so changes do not rely on desk smoke only. This spec is the testing and delivery target. Some of it has shipped; some has not. -This spec defines the target testing and delivery infrastructure. +## Current State (2026-08-20) -## Current State - -- No `tests/` directory with meaningful coverage. -- No `pytest` configuration beyond a stub in `pyproject.toml`. -- No GitHub Actions or other CI workflow. -- No coverage reporting (codecov, etc.). -- Manual testing is the primary validation method. +- `tests/` exists: `test_agent_graph`, `test_chat`, `test_config`, `test_llm` (CPU, mocked externals). +- `pytest` + `pytest-asyncio` and `testpaths` are in `pyproject.toml`. +- GitHub Actions `.github/workflows/ci.yml` runs ruff + pytest with a CPU-only `--no-deps` install. +- No coverage measurement or coverage gate. +- No Docker image build in CI. +- No hardware-in-the-loop tests (and they stay out of every-PR CI). ## Goals diff --git a/specs/005-testing-and-cicd/tasks.md b/specs/005-testing-and-cicd/tasks.md new file mode 100644 index 0000000..12792ce --- /dev/null +++ b/specs/005-testing-and-cicd/tasks.md @@ -0,0 +1,59 @@ +# Tasks: Testing strategy, CI/CD, and coverage (005) + +**Feature**: 005-testing-and-cicd +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +The May 2025 spec said there was no `tests/` directory and no GitHub Actions. +That is outdated. Phase 0 is **done**. Coverage gates, GPU e2e, and Docker +image CI are **not**. + +## Phase 0 — Unit tests + CPU CI (done) + +- [x] `tests/` directory with meaningful unit tests +- [x] `tests/test_agent_graph.py` — graph routing + memory injection (mocked tools) +- [x] `tests/test_chat.py` — `MemoryContext.to_prompt_block` +- [x] `tests/test_config.py` — `Settings.validate_keys` +- [x] `tests/test_llm.py` — `get_chat_model` provider routing +- [x] pytest + pytest-asyncio in `[project.optional-dependencies] dev` +- [x] `[tool.pytest.ini_options]` (`testpaths = ["tests"]`, `asyncio_mode = auto`) +- [x] `.github/workflows/ci.yml` on push and pull_request +- [x] CI: ruff check +- [x] CI: pytest -q on ubuntu-latest +- [x] CI: CPU-only install (`pip install -e . --no-deps` + lightweight brain deps; no Riva / PortAudio) + +## Phase 1 — Coverage, DX, types + +- [ ] `pytest-cov` or coverage.py wired so `pytest` can emit a report +- [ ] Coverage **gate** (spec floor: 60% overall; 80%+ on `agent/` and `voice/` once measured honestly) +- [ ] Fail PRs on the gate (or on a significant drop) — not "coverage is printed but ignored" +- [ ] `make test` (Makefile currently has `lint`, not pytest) +- [ ] mypy in CI (local `make lint` already runs ruff + mypy; Actions does not) +- [ ] pip cache on the CI job + +## Phase 2 — Docker image CI + +- [ ] CI job that builds the agent image from the root `Dockerfile` +- [ ] Tag with `git-sha` (semver when tags exist) +- [ ] Optional push to a private registry via operator secrets — **no registry hostname in git** +- [ ] Multi-platform build only if we need amd64 CI → aarch64 Spark; do not assume it + +## Phase 3 — Gated / heavier tests + +- [ ] Integration: graph execution with lightly mocked tools beyond the current unit file +- [ ] Voice loop with mocked audio / Riva (this repo's `voice/` package) +- [ ] Compose smoke: stack starts and reports healthy (depends on [003](../003-deployment-infrastructure/tasks.md) probes actually meaning ready) +- [ ] GPU e2e on a Spark or GPU runner — **not** on every PR + +## Out of scope (leave unchecked on purpose) + +- [ ] Hardware-in-the-loop on the Lenovo Go in this repo's CI +- [ ] Mutation testing +- [ ] Performance benchmarking in CI +- [ ] Hosted coverage SaaS (optional; not required to close the gate) + +## Traceability + +Unit tests and the CPU workflow are in this repo today +(`.github/workflows/ci.yml`, `tests/`). Live spoken e2e belongs to +[`derekclair/conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) +(spec 008), not this checklist. diff --git a/specs/006-alternative-memory-systems/plan.md b/specs/006-alternative-memory-systems/plan.md new file mode 100644 index 0000000..bb9b691 --- /dev/null +++ b/specs/006-alternative-memory-systems/plan.md @@ -0,0 +1,93 @@ +# Plan: Alternative long-term memory systems (006) + +**Feature**: 006-alternative-memory-systems +**Spec**: [spec.md](./spec.md) +**Date**: 2025-05-21 (spec); recorded 2026-08-19 + +## 1. Architecture (escape hatch, not a second store) + +Long-term memory already has a narrow seam. The graph and voice layer do not +import a second backend. Keep it that way until a **real** need appears. + +``` +get_agent(user_id) + │ + ├── memory_injection ──► create_memory_tools(user_id) + ├── call_llm (tools bound) │ + └── execute_tools ▼ + get_user_profile + recall_memories(query, limit) + store_memory(content, metadata) + │ + ▼ + Supermemory +``` + +| Piece | Owner | +|-------|--------| +| Tool names + signatures | `src/thelab_langchain/agent/tools/memory.py` | +| Per-user scope | `user_id` → Supermemory `container_tag` | +| Graph / voice | Call the three tools only | +| Short-term turns | Spec 004 (caller-side list; no checkpointer). Not this spec. | + +## 2. Tech choices (locked until a trigger fires) + +| Concern | Choice | Why | +|---------|--------|-----| +| Long-term store | Supermemory | Already delivering profile + recall | +| Seam | The three tools above | Cheap swap later; no ABC yet | +| `MemoryBackend` protocol | **Do not add** | Protocol-for-one-impl is noise | +| Second adapter (Mem0, Zep, local vectors, …) | **Do not build** | No air-gap / cost / quality trigger yet | +| Default if we ever swap | Keep Supermemory as default | Household path stays the known UX | + +Candidates in the spec (Zep, Mem0, LangGraph store, custom vector+graph, +SQLite+embeddings) stay a table of options. They are not a backlog to +implement in order. + +## 3. Phases + +### Phase 0 — Keep the interface narrow (now) + +- Leave `create_memory_tools(user_id)` as the only factory. +- Do not introduce a protocol, registry, or dual-write. +- New graph nodes must not import a memory SDK except through those tools. + +### Phase 1 — Adapter, only after a real need (not started) + +Triggers that would justify Phase 1 (any one is enough; none are true today): + +- Fully air-gapped deploy (no outbound memory calls). +- Cost of the current store is material at this scale. +- Measured recall/profile gap another system actually fixes. +- Need graph-shaped queries the current store cannot do. + +Then, and only then: + +1. Pick **one** second backend for that need (not a portfolio). +2. Extract a small protocol that matches the three methods we already use. +3. Make `create_memory_tools` pluggable; default remains Supermemory. + +Do not start Phase 1 “so it will be ready.” + +## 4. Risks + +| Risk | Mitigation | +|------|------------| +| Premature ABC | No protocol until a second impl is chosen | +| Dual-write / split brain | One store per deploy; no silent fan-out | +| Backend types leaking into graph nodes | Tools stay the only import surface | +| Confusing 004 checkpointers with 006 stores | Short-term ≠ long-term; do not merge them | + +## 5. Success metrics + +- Still one factory and three tool names. +- `graph.py` does not grow a second memory client. +- A second backend appears only after a trigger above is written down. + +There is no success metric for “we have N adapters.” + +## 6. What this plan is not + +It is not a Mem0 or Zep port. It is not a local vector store. It is not +leaving Supermemory. It is not a LangGraph checkpointer (004). It is not +permission to add `MemoryBackend` “for cleanliness.” diff --git a/specs/006-alternative-memory-systems/tasks.md b/specs/006-alternative-memory-systems/tasks.md new file mode 100644 index 0000000..dd7a472 --- /dev/null +++ b/specs/006-alternative-memory-systems/tasks.md @@ -0,0 +1,47 @@ +# Tasks: Alternative long-term memory systems (006) + +**Feature**: 006-alternative-memory-systems +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +Checkboxes record what is actually in the tree. This spec is an escape hatch. +Do **not** build adapters or a second backend until a real need is written +down. + +## Phase 0 — Keep the interface narrow + +- [x] Long-term access only via `get_user_profile`, `recall_memories`, `store_memory` +- [x] Keep `create_memory_tools(user_id)` as the only factory +- [x] Scope tools with `user_id` (`container_tag`); graph does not pick a backend +- [x] Document this as an escape hatch, not a multi-backend project +- [x] Do not add a `MemoryBackend` protocol / ABC for a single implementation +- [x] Do not add a second memory client in `graph.py` or the voice layer + +Standing rule: new code talks to long-term memory only through those three +tools. + +## Phase 1 — One adapter, only after a real need (not started) + +Do not schedule these. They unlock when a trigger in the plan is real. + +- [ ] Write the trigger (air-gap, cost, measured recall gap, or graph queries) +- [ ] Choose **one** second store for that trigger +- [ ] Define a protocol that matches the three methods we already use +- [ ] Implement one adapter; keep Supermemory the default +- [ ] Make `create_memory_tools` pluggable without changing graph node shape +- [ ] Tests: graph still compiles when the default backend is the only one configured + +## Out of scope (no second backend until a real need) + +- [ ] Zep adapter +- [ ] Mem0 adapter +- [ ] LangGraph long-term memory store as a parallel backend +- [ ] Custom vector + graph store +- [ ] SQLite + embeddings as a second production path +- [ ] Dual-write to two stores +- [ ] Swap motivated only by “we might want it later” + +## Traceability + +`src/thelab_langchain/agent/tools/memory.py` is the seam. +`src/thelab_langchain/agent/graph.py` calls `create_memory_tools` for injection +and tool-calling. Short-term turns are spec 004 (still no checkpointer). diff --git a/specs/007-dgx-hardware-optimization/plan.md b/specs/007-dgx-hardware-optimization/plan.md index 4b2e6f5..0498c21 100644 --- a/specs/007-dgx-hardware-optimization/plan.md +++ b/specs/007-dgx-hardware-optimization/plan.md @@ -2,49 +2,85 @@ **Feature**: 007-dgx-hardware-optimization **Related Spec**: [spec.md](./spec.md) -**Date**: 2025-05-22 +**Date**: 2025-05-22 (living slot policy notes added 2026-08-19) **Implementation Branch**: `feat/007-dgx-hardware-optimization-impl` ## 1. Goal -Execute the strategy defined in the spec with rigorous measurement: +Execute the strategy defined in the spec with rigorous measurement, **without** treating the 001 compose stack as live production. -- Capture an accurate **baseline** on the current production configuration (120B + full Riva on single DGX Spark). -- Run controlled experiments for the highest-leverage changes (lightweight English audio stack, 49B model swap). +- Keep the **living inference slot policy** (spec.md) aligned with the desk: one local generative LLM, CPU STT/TTS, hosted Grok for quality-critical fleet roles. +- Capture an accurate **live-path baseline** (spec 008 I/O + `get_agent()` + hosted Grok and/or one ~30B-class `openai_compatible` / Ollama worker). This is what actually runs. +- Treat **120B NIM + full Riva** (`docker-compose` in this repo) as an **optional experimental** capture — not “current production.” Do not invent GB figures; those runs stay unmeasured until a report exists. +- Run controlled experiments for high-leverage changes (lightweight English audio on the compose path if revived; ~30B vs 49B as the *single* occupied slot). **No 120B+ agent loops on one Spark** as a daily driver. - Quantify headroom, voice turn latency, concurrency limits, and memory behavior. -- Make a data-driven decision on the **sweet-spot configuration**. +- Make a data-driven decision on the **sweet-spot configuration**, or keep the living policy as practice-without-numbers, labeled unmeasured. - Document everything so future changes (including 2× node work) have a clear before/after reference. -Success = we have reproducible numbers and a locked "recommended daily driver" profile for the family voice agent. +Success = the slot policy matches reality, and we have reproducible numbers (or an explicit “unmeasured” label) for a locked daily-driver profile for the household voice agent. + +## 1.1 Inference slot policy (plan notes) + +Matches [spec.md — Inference slot policy (living)](./spec.md#inference-slot-policy-living). This is how we schedule work on the GB10; the phases below are how we *measure*. + +| Rule | Practice | +|------|----------| +| GB10 budget | ~128 GB unified, ~273 GB/s, **one serious local LLM at a time** | +| Live voice I/O | `conversational-voice-agent` (spec 008): Parakeet TDT 0.6B **CPU** STT + Piper **CPU** TTS | +| Brain | this repo `get_agent()` | +| Default LLM | hosted Grok | +| Local option | `openai_compatible` / Ollama ~30B-class, hosted fallback | +| This repo compose (`agent` + `riva` + `nemotron` 120b) | experimental; **not** the live spoken path | +| Quality-critical fleet | orchestrator, architect, reviewer, design → hosted Grok (do not fight the slot) | +| Local workers | coder, researcher → ~30B-class with hosted fallback | +| Forbidden on one Spark | 120B+ agent loops; a second large local LLM next to the occupied slot | +| Speech vs GPU | Prefer CPU STT/TTS so the unified/GPU slot stays with at most one generative LLM | + +Harness work must label every report **live** vs **experimental-compose**. Phase 0 originally assumed compose 120B + Riva was the as-is stack; that assumption is **retired**. ## 2. High-Level Phases ### Phase 0 – Baseline Capture (Must Do First) -Establish the "as-is" numbers on the exact current stack before touching anything. +Establish numbers **before** changing the *experimental* compose stack — and, separately, sample the **live** path that already runs. + +**0a. Live path (priority; this is as-is):** +- Spec 008 I/O (Parakeet CPU + Piper CPU) + `get_agent()` + hosted Grok, then the same with one ~30B-class local worker occupying the slot. +- Instrument or manually sample unified memory / CPU / (if a local LLM is up) GPU. +- Produce a live-path report. Do not invent GB figures if the sampler is not in place — leave TBD. + +**0b. Experimental compose (optional; not production):** +- Clean DGX Spark with this repo’s `docker-compose.yml` + nemotron-3-super-120b-a12b + full Riva. +- Same metrics. Label the report experimental. NIM + Riva GB numbers remain **unmeasured** until this run exists. -- Run on clean DGX Spark with current `docker-compose.yml` + nemotron-3-super-120b-a12b + full Riva. -- Instrument or manually measure the core metrics from the spec. -- Produce a `baseline-report.md` (or JSON + human summary) committed in the repo. +Do not present 0b as “what we run today.” -### Phase 1 – Audio Stack Reduction (Highest Leverage Quick Win) -Replace full Riva with a minimal English-only path (Parakeet CTC + high-quality English TTS NIM or equivalent). +### Phase 1 – Audio Stack Reduction (Compose experiment; live path already on CPU) + +**Already practice (not a 007 deliverable):** live desk voice is Parakeet TDT 0.6B CPU + Piper CPU (spec 008). That was the high-leverage win for the spoken path. Do not plan this phase as if Riva is the live ASR. + +**If compose/Riva is revived:** replace full Riva with a minimal English-only path (Parakeet CTC + high-quality English TTS NIM or equivalent). - Create a lightweight audio service profile (new container or slimmed Riva config). - Update `docker-compose` with profiles or separate override files. - Re-run the benchmark harness. -- Compare delta vs baseline (memory saved, latency change, perceived voice quality). +- Compare delta vs the *experimental* compose baseline (memory saved, latency change, perceived voice quality). **Unmeasured** until that run exists. +- Prefer keeping speech off the GPU/unified generative slot. + +**Decision gate**: Live default stays 008 CPU speech. Compose light-audio becomes the experimental default only if measured quality and headroom justify it. + +### Phase 2 – Model A/B Testing (single occupied slot) + +Living policy: **one** local generative LLM. Quality-critical fleet stays on hosted Grok. Local workers are ~30B-class with hosted fallback. Do **not** stand up 49B *alongside* 120B on one Spark. -**Decision gate**: If quality is acceptable and headroom improves significantly → adopt as new default. +A/B the **single** slot: -### Phase 2 – Model A/B Testing (49B vs Current 120B) -Stand up `llama-3.3-nemotron-super-49b-v1.5` alongside the 120B. +- Live option already: `openai_compatible` / Ollama ~30B-class vs hosted Grok (agent already routes via `LLM_PROVIDER` / `LLM_BASE_URL`). +- Optional experiment: `llama-3.3-nemotron-super-49b-v1.5` as the one loaded model (compose profile or override) — not a second concurrent NIM. +- 120B remains an optional labeled experiment, **not** a daily “deep mode” on one Spark (that *is* occupying the only slot with a forbidden-size loop). -- Add a second LLM service in compose (different port or profile). -- Make the agent configurable (env var or CLI flag) to point at different NIM endpoints. -- Run identical benchmark scenarios on both models (with the winning audio stack from Phase 1). -- Measure: latency (especially TTFT + full turn), memory headroom, subjective quality on memory-recall + household prompts, tool-calling reliability. +Measure: latency (TTFT + full turn), memory headroom (**unmeasured** until sampled), subjective quality on memory-recall + household prompts, tool-calling reliability. -**Decision gate**: Choose primary model (likely 49B for daily use, 120B as optional "deep" mode). +**Decision gate**: Confirm ~30B as the local-worker class, or promote 49B as the single-slot experiment winner. Do not lock 120B as optional always-on deep mode on one node. ### Phase 3 – Context & Memory Efficiency Tuning With the chosen model + audio, optimize how we use the remaining headroom. @@ -55,10 +91,11 @@ With the chosen model + audio, optimize how we use the remaining headroom. - Validate multi-user (2–4 concurrent simulated family members) stability. ### Phase 4 – Sweet-Spot Lock + Operationalization -- Update default `docker-compose.yml`, `.env` examples, and Makefile targets for the chosen configuration. +- Do **not** make 120B + Riva the default compose “production” profile. Defaults must match the living slot policy (CPU speech lives in the I/O repo; this package is `get_agent()`; local LLM is optional ~30B-class). +- Update default `docker-compose.yml`, `.env` examples, and Makefile targets only for configurations we actually intend to run, and mark experimental profiles as such. - Add documented "benchmark" and "profile" make targets. -- Update architecture docs and the 001 spec references. -- Create a "current sweet spot" section in the 007 directory with the final numbers and rationale. +- Update architecture docs and the 001 spec references so they do not re-introduce the old production framing. +- Create a "current sweet spot" section in the 007 directory with the final numbers and rationale — or an explicit unmeasured label. ### Phase 5 – 2× DGX Spark Preparation (Future, After Phase 4) - Design multi-node compose / orchestration approach (tensor-parallel for 340B or service separation). @@ -103,7 +140,7 @@ These will be thin wrappers that set the right compose profiles + env and invoke ## 4. Docker & Deployment Changes -- Keep the existing `docker-compose.yml` as the "current baseline" reference. +- Keep the existing `docker-compose.yml` as the **experimental compose** reference (agent + riva + nemotron 120b). It is **not** the live spoken path and **not** the production baseline. - Introduce compose profiles or override files: - `docker-compose.light-audio.yml` - `docker-compose.49b.yml` @@ -121,7 +158,7 @@ After each major phase we will: 3. Update the decision matrix in the spec (or a living `decision-log.md`). 4. Hold a quick "gate" discussion (even async via PR comment or the issue tracker) before proceeding to the next phase. -No optimization change lands in the default compose without passing through this measured gate. +No optimization change lands in the default compose without passing through this measured gate. Do not land a 120B+ daily loop on one Spark even if a report looks flattering — that violates the living slot policy. ## 6. Risks & Mitigations @@ -149,4 +186,4 @@ This keeps the loop tight and the excitement high. --- -**Ready to cut.** Once the spec PR is reviewed/merged, we will land the first pieces of the harness and capture the all-important baseline numbers on the actual DGX Spark hardware. \ No newline at end of file +**Ready to measure the live slot.** Harness work should start from the desk path (008 + `get_agent()`), not from a fictional 120B + Riva production stack. Experimental compose numbers stay optional and labeled unmeasured until captured. \ No newline at end of file diff --git a/specs/007-dgx-hardware-optimization/spec.md b/specs/007-dgx-hardware-optimization/spec.md index c140f9c..2b95880 100644 --- a/specs/007-dgx-hardware-optimization/spec.md +++ b/specs/007-dgx-hardware-optimization/spec.md @@ -1,8 +1,8 @@ # Spec: DGX Spark Hardware Optimization & Sweet-Spot Strategy **Feature ID**: 007-dgx-hardware-optimization -**Status**: Draft / Strategy & Benchmarking Spec -**Related to**: 001-voice-dgx-spark-agent, 002-multi-user-support, 003-deployment-infrastructure +**Status**: Draft / Strategy & Benchmarking Spec (living slot policy added 2026-08-19) +**Related to**: 001-voice-dgx-spark-agent, 002-multi-user-support, 003-deployment-infrastructure, 008-local-tts-lenovo-go-spike **Created**: 2025-05-22 **Branch**: `feat/007-dgx-hardware-optimization` @@ -10,7 +10,13 @@ We have reached the point where we need a deliberate, measurable optimization strategy for the voice-first LangGraph + Supermemory agent running on NVIDIA DGX Spark hardware (single node today, with an eye toward 2× DGX Spark). -Current production configuration (as of Feature 001): +**Honest live path (what actually runs on the desk today — not the 001 compose hypothesis):** +- Desk voice I/O: [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) (spec 008) — Parakeet TDT 0.6B CPU STT + Piper CPU TTS +- Brain: this repo’s `get_agent()`; default LLM is hosted Grok (xAI); local option is `openai_compatible` / Ollama ~30B-class with hosted fallback +- `docker-compose` in this repo (`agent` + `riva` + `nemotron` 120b) is **experimental**, not the live spoken path +- See [Inference slot policy (living)](#inference-slot-policy-living) for the one-local-LLM budget and fleet-role split + +**Original 001 compose hypothesis (experimental / unmeasured on this Spark — do not read as “what we run today”):** - LLM: `nvcr.io/nim/nvidia/nemotron-3-super-120b-a12b:latest` (120B hybrid MoE/Mamba, ~12B active params, 1M native context) - Voice: Full NVIDIA Riva (NeMo ASR + TTS) via gRPC sidecar - Agent: LangGraph StateGraph with proactive Supermemory injection + reactive memory tools @@ -37,35 +43,99 @@ Key characteristics that drive every optimization decision: - Networking: 10 GbE + dual 100/200 GbE ConnectX-7 (RDMA capable) - Power/thermals: ~140 W SoC, ~240 W PSU, designed for quiet/home-lab operation -**Implication**: Every added service (Riva, larger model, longer context, multiple concurrent family conversations) directly reduces headroom for the "rest of the app" (LangGraph execution, Supermemory client calls, VAD, playback, future tools). +**Implication**: Every added service (Riva, larger model, longer context, multiple concurrent household conversations) directly reduces headroom for the "rest of the app" (LangGraph execution, Supermemory client calls, VAD, playback, future tools). **One serious local generative LLM at a time** on this chip; see the living slot policy below. + +## Inference slot policy (living) + +This section is the operating policy for the single GB10. It **corrects** earlier 007/001 wording that treated 120B NIM + full Riva as the live spoken path. That stack remains a valid *experiment* if we ever want numbers for it; it is not what we run today. + +### Hardware budget (qualitative — no invented GB figures) + +- GB10: ~128 GB unified LPDDR5X, ~273 GB/s, coherent CPU + GPU. There is no separate VRAM. +- **One serious local LLM at a time.** Do not run 120B+ agent loops on one Spark. +- Prefer keeping the GPU / unified slot for **at most one** local generative LLM. +- STT/TTS on CPU is a **deliberate** budget choice: Parakeet TDT 0.6B (CPU) and Piper (CPU) leave the slot free for a ~30B-class worker, or empty while hosted Grok does the turn. + +Measured NIM + Riva footprints on this Spark are **still unmeasured**. Ranges elsewhere in this spec (40–70 GB, 10–25 GB, etc.) stay **engineering estimates / TBD**, not inventory. + +### Honest live path + +| Layer | What actually runs | +|-------|-------------------| +| Voice I/O | spec 008 / `conversational-voice-agent`: Parakeet TDT 0.6B CPU STT + Piper CPU TTS | +| Brain | this repo `get_agent()` | +| Default LLM | hosted Grok (xAI) | +| Local LLM option | `openai_compatible` / Ollama, ~30B-class, hosted fallback if the slot is busy or the local endpoint is down | +| This repo `docker-compose` (`agent` + `riva` + `nemotron` 120b) | experimental; **not** the live spoken path | + +### Fleet roles vs the slot + +Quality-critical roles **do not** occupy the local slot — they use hosted Grok so they never fight a worker for unified memory: + +- orchestrator, architect, reviewer, design → hosted Grok + +Local workers **may** occupy the single slot, with hosted fallback: + +- coder, researcher → ~30B-class local (`openai_compatible` / Ollama) + +When the slot is occupied, other work uses hosted models. Do not co-schedule a second large local LLM. Workstation fleet ops live in Hermes (`~/.hermes/docs/agentic-workflow.md`); this spec only owns the **memory-budget** rule. + +### Policy rules -## Current Baseline (What We Are Running Today) +1. At most one local generative LLM loaded on the Spark. +2. No 120B+ (or 340B) agent loops on a single Spark. +3. Keep STT/TTS on CPU unless a measured experiment shows GPU speech still leaves the generative slot intact. +4. Treat compose 120B + full Riva as an optional harness target, not production. +5. Do not publish GB “we use X GB today” numbers until a 007 report lands them. -- **LLM**: nemotron-3-super-120b-a12b (120B total / ~12–12.7B active per token via hybrid MoE + Mamba). Excellent agentic/tool-calling and long-context reasoning — ideal for our Supermemory injection + multi-turn household conversations. +### What this spec still measures + +The rest of 007 (baseline harness, light-audio vs full Riva, 49B vs 120B A/B, 2× Spark) remains useful **experiment design**. Those runs are gated on real hardware numbers. They are **not** a claim that the experimental stack is the daily driver. + +## Current baseline (live vs experimental) + +### Live desk path (practice today) + +- **Voice**: Parakeet TDT 0.6B via NeMo on CPU + Piper CPU TTS (spec 008). +- **Brain**: `get_agent()` in this package. +- **LLM**: hosted Grok by default; optional local ~30B-class via `openai_compatible` / Ollama. +- **Slot**: CPU speech; GPU/unified reserved for at most one ~30B-class worker (or idle). + +We do **not** yet have a committed 007 harness report for this live path’s unified-memory samples either. Latency and quality notes belong in 008 / the I/O repo until a 007 report exists. + +### Experimental compose stack (001 hypothesis — unmeasured) + +If we stand up this repo’s compose on the Spark, the intended services are: + +- **LLM**: nemotron-3-super-120b-a12b (120B total / ~12–12.7B active per token via hybrid MoE + Mamba). - **Context**: Native 1M tokens (practical NIM limits often 128K–256K depending on profile and KV precision). - **Voice**: Full Riva stack (Parakeet-class ASR + high-quality TTS, multilingual capable). -- **Expected characteristics** (to be validated on hardware): - - Model load + idle memory: Significant fraction of 128 GB (exact TBD via NIM profile). +- **Expected characteristics** (**unmeasured** on this Spark; exact TBD via NIM profile + `nvidia-smi` / container stats): + - Model load + idle memory: Significant fraction of 128 GB (exact TBD). - Real-time voice turn latency (end-of-speech → first audio out): Target sub-second natural feel. - - Concurrent family users: Currently designed for single primary user; multi-user will increase memory pressure. + - Concurrent household users: Originally designed for a single primary user; multi-user will increase memory pressure. -We do **not** yet have hard numbers on this exact DGX Spark + Docker + Riva + 120B combination. This spec exists to create those numbers systematically. +We do **not** have hard numbers on this exact DGX Spark + Docker + Riva + 120B combination. This spec still exists to create those numbers **if** we run that experiment. Do not treat the 120B + Riva row as the current daily driver. ## Model Comparison | Model | Params (Total / Active) | Context | Architecture | Expected Footprint (Single Spark) | Strengths for Our Use Case | Weaknesses / Risks | Voice Latency Impact | |-------|--------------------------|---------|--------------|-----------------------------------|----------------------------|--------------------|----------------------| -| **nemotron-3-super-120b-a12b** (current) | 120B / ~12B active | 1M native (NIM ~128–256K practical) | Hybrid Mamba + Transformer MoE | High (but runnable per community reports; tight with Riva + long ctx) | Best agentic reasoning, tool use, long-horizon memory recall, retains large Supermemory context without constant re-fetch | Highest memory/latency of the three practical options; risk of swapping under load | Higher TTFT + decode latency vs lighter models | +| **nemotron-3-super-120b-a12b** (experimental compose; **not** the live daily driver) | 120B / ~12B active | 1M native (NIM ~128–256K practical) | Hybrid Mamba + Transformer MoE | High (community reports; tight with Riva + long ctx). **Unmeasured** on this Spark. **Forbidden as a daily agent loop** on one Spark under the living slot policy. | Best agentic reasoning, tool use, long-horizon memory recall, retains large Supermemory context without constant re-fetch | Occupies the only local slot; fights voice/fleet workers; risk of swapping under load | Higher TTFT + decode latency vs lighter models | | **llama-3.3-nemotron-super-49b-v1.5** (strong candidate) | 49B dense | 128K | NAS-optimized dense Transformer | Medium (comfortable headroom on single Spark) | Excellent accuracy/efficiency; fast tokens/s; proven on H100-class; lower latency, more room for Riva or concurrent users | Smaller context than 120B (may require more aggressive summarization) | Best-in-class for its size; fastest turn times of the three | | **nemotron-4-340b-instruct** ("big boi") | 340B dense | 4K native (extendable) | Dense Transformer | Impractical on single node even heavily quantized; feasible on 2× via tensor-parallel | Maximum raw intelligence and instruction following; potential "reasoning brain" for hardest queries | Enormous memory (hundreds of GB raw); high latency even sharded; overkill for most voice turns | Significantly higher latency; best used selectively or for offline tasks | -**Recommendation for primary inference path**: Start with the 49B v1.5 as the default "daily driver" for voice responsiveness while keeping the 120B as an optional "deep thinker" that can be swapped in for complex multi-step planning or heavy memory synthesis. +**Recommendation for primary inference path (updated by the living slot policy)**: Live daily driver is **hosted Grok** for quality-critical work and **at most one ~30B-class local worker** (coder / researcher) when we want on-box generation. The 49B v1.5 remains a strong *experiment* if we measure a single-slot local voice/brain. The 120B is **not** an optional always-on “deep thinker” on one Spark — swapping it in *is* occupying the only local slot, and the policy forbids 120B+ agent loops on a single node. 340B stays multi-node-only. ## Audio Stack: Can We Axe Riva? -Current: Full Riva (enterprise-grade, multi-language, multiple models for ASR + TTS). +**Live path already did, on CPU.** Spec 008 / `conversational-voice-agent` uses Parakeet TDT 0.6B (NeMo, CPU) + Piper CPU TTS. That is the desk spoken loop. Full Riva in this repo’s compose is an experimental sidecar, not production audio. + +The remainder of this section is still useful as an experiment design **if** we ever bring Riva (or a GPU speech NIM) onto the Spark next to a local LLM. -For an **English-only household** (Derek + family), the multilingual enterprise features are mostly wasted. +**Experimental compose “current”**: Full Riva (enterprise-grade, multi-language, multiple models for ASR + TTS). + +For an **English-only household**, the multilingual enterprise features are mostly wasted. **Lighter English-only alternatives**: - Pin to **Parakeet 1.1B CTC English** (or smaller 0.6B variants) via dedicated lightweight ASR NIM or direct NeMo inference — typically 2–8 GB for real-time streaming. @@ -73,7 +143,7 @@ For an **English-only household** (Derek + family), the multilingual enterprise - Total audio stack: **4–12 GB** instead of 10–25+ GB for full Riva. **Benefits of dropping full Riva**: -- Reclaim 8–15+ GB of unified memory → directly usable for larger KV cache (longer effective context), higher quality model, or concurrent family sessions. +- Reclaim 8–15+ GB of unified memory (**estimate, unmeasured**) → directly usable for larger KV cache (longer effective context), a higher-quality single local model, or leaving the slot free. - Simpler deployment (fewer sidecars, smaller attack surface, faster startup). - Lower CPU/GPU contention during voice turns. @@ -82,11 +152,11 @@ For an **English-only household** (Derek + family), the multilingual enterprise - Must validate English quality and latency of the lighter path (Parakeet CTC is already very strong for English). - Potential future desire for "voice cloning" or multiple family voices — still doable with lighter dedicated voices. -**Conclusion**: Yes — for the English-only family voice agent we can (and probably should) replace full Riva with a minimal English Parakeet + English TTS profile (or emerging smaller NVIDIA speech NIMs). This is one of the highest-leverage single changes available today. +**Conclusion**: For the English-only household voice agent we already run a minimal CPU Parakeet + Piper path (spec 008). If the compose experiment is revived, prefer that same “light English audio” posture over full Riva so the unified slot stays with at most one generative LLM. GPU speech NIMs are an experiment, not a default, until measured. ## Headroom Analysis (Single DGX Spark, 128 GB Unified) -Rough engineering estimates (to be replaced by measured data): +Rough engineering estimates (**unmeasured** on this Spark — to be replaced by harness reports; do not treat as live inventory): **Always-present baseline**: - OS + Docker + non-root agent container + Python + sounddevice + VAD + Supermemory client + LangGraph overhead + checkpointers: **8–15 GB** @@ -101,11 +171,11 @@ Rough engineering estimates (to be replaced by measured data): - English-only Parakeet + TTS: **4–12 GB** **Headroom for "the rest of the app"** (reactive tool calls, memory injection, future vision/tools, burst concurrency): -- Current 120B + full Riva: **Very tight** (often <10–15 GB free under load). Risk of OOM, swapping, or forced context truncation during long family conversations. -- 49B + lighter audio: **Healthy headroom** (20–40+ GB free) → room for 2–4 concurrent family members, longer context windows, or future capabilities. -- 120B + lighter audio: **Recoverable** — may be the pragmatic sweet spot for intelligence + responsiveness. +- Experimental 120B + full Riva: **Very tight** (estimate, often <10–15 GB free under load — **unmeasured**). Risk of OOM, swapping, or forced context truncation during long household conversations. **Not** the live daily driver. +- 49B + lighter audio: **Healthy headroom** (20–40+ GB free, **unmeasured**) → more room for longer context or future capabilities if that single slot is occupied by 49B rather than 120B. +- 120B + lighter audio: **Recoverable** vs full Riva (**unmeasured**) — still a 120B+ loop on one Spark, so **not** a living-policy daily driver even if headroom improves. -**Key insight**: The biggest single lever for headroom today is **replacing full Riva with an English-only lightweight audio path**. The second biggest is **model choice** (49B vs 120B MoE). +**Key insight (living policy)**: The biggest lever already in practice is **not occupying the GPU/unified slot with speech** (CPU Parakeet + Piper) and **not loading a second local LLM**. The next lever, if we revive compose experiments, is still **not running full Riva next to a large NIM**, then **model class** (~30B worker vs 49B experiment vs 120B — the last is forbidden as a daily loop on one Spark). ## Multi-Node (2× DGX Spark) Projections @@ -144,11 +214,19 @@ We will establish a repeatable benchmark harness before making major changes. - **Thermals / power / noise**: Important for a living-room/home device. ### Baseline Capture (First Experiment on Current Stack) -1. Single DGX Spark, current 120B + full Riva Docker Compose. -2. Clean boot, measure idle memory. -3. Run scripted voice sessions (single user, then 2–3 overlapping). -4. Capture all metrics above + full `nvidia-smi` / container memory + system logs. -5. Document exact NIM profiles, quantization settings, Riva config, and context management strategy used. + +Two different “baselines” — do not collapse them: + +**A. Live path (practice; still needs a 007 report):** spec 008 I/O + `get_agent()` + hosted Grok and/or one ~30B-class `openai_compatible` worker. CPU STT/TTS. This is what the desk actually runs. + +**B. Experimental compose (001 hypothesis; unmeasured):** single DGX Spark, this repo’s `docker-compose.yml` with 120B NIM + full Riva. Optional harness target only. + +If we run **B**: +1. Clean boot, measure idle memory. +2. Run scripted voice sessions (single user, then 2–3 overlapping). +3. Capture all metrics above + full `nvidia-smi` / container memory + system logs. +4. Document exact NIM profiles, quantization settings, Riva config, and context management strategy used. +5. Label the report experimental — not “production baseline.” ### Subsequent Experiments (Compare Against Baseline) - 120B + lighter English audio only @@ -162,35 +240,41 @@ Every change must be accompanied by before/after numbers against the baseline. " Primary levers (ranked by expected impact on single-node headroom + latency): -1. **Audio stack reduction** (full Riva → English Parakeet + TTS): Highest immediate win. -2. **Model swap** (120B MoE → 49B dense): Large win on latency and headroom; acceptable quality trade for most turns. -3. **Context strategy** (aggressive summarization + proactive injection vs. raw long context): Reduces KV pressure and improves recall quality. -4. **Quantization / NIM profile tuning**: FP8, NVFP4, lower KV precision where quality allows. -5. **Concurrency limits & backpressure**: Limit parallel family sessions or queue intelligently. -6. **Process placement** (future): Move audio to a dedicated lightweight container or even separate node. -7. **2× node scaling**: When single-node sweet spot is exhausted. +1. **Do not fight the slot** (living policy, already practice): one local generative LLM; STT/TTS on CPU; quality-critical fleet on hosted Grok. +2. **Audio stack reduction** (full Riva → English Parakeet + TTS): Highest immediate win *if* compose/Riva is revived; live path already uses CPU Parakeet + Piper. +3. **Model class** (~30B local worker vs 49B experiment vs 120B): 120B is not a daily loop on one Spark. 49B remains an A/B candidate for a *single* occupied slot. +4. **Context strategy** (aggressive summarization + proactive injection vs. raw long context): Reduces KV pressure and improves recall quality. +5. **Quantization / NIM profile tuning**: FP8, NVFP4, lower KV precision where quality allows. +6. **Concurrency limits & backpressure**: Limit parallel household sessions or queue intelligently. +7. **Process placement** (future): Move audio to a dedicated lightweight container or even separate node. +8. **2× node scaling**: When single-node sweet spot is exhausted — including when we want a second local LLM. ### Decision Matrix (Example) | Configuration | Expected Headroom | Expected Voice Latency | Intelligence Level | Multi-User Comfort | Recommendation | |---------------|-------------------|------------------------|--------------------|--------------------|----------------| -| 120B + Full Riva | Low | Medium-High | Highest | Poor | Baseline only; optimize away | -| 120B + Light Audio | Medium | Medium | Highest | Good | Strong candidate if quality holds | -| 49B + Light Audio | High | Lowest | Very High | Excellent | Default daily driver target | -| 340B (2× sharded) | N/A (multi-node) | High | Maximum | Excellent | On-demand specialist brain | +| Hosted Grok + CPU Parakeet/Piper (live) | Slot free or idle (**unmeasured** GB) | Dominated by network LLM + CPU speech | Highest for quality-critical roles | N/A (hosted) | **Live default** for orchestrator / architect / reviewer / design and for voice when no local worker is loaded | +| ~30B local worker + CPU Parakeet/Piper (live option) | Occupies the one local slot (**unmeasured** GB) | Local TTFT + CPU speech | Good for coder / researcher | One local LLM only | **Live local option** with hosted fallback; do not co-schedule a second LLM | +| 120B + Full Riva (experimental compose) | Low (**unmeasured**) | Medium-High | Highest | Poor | Experiment only; **not** live production; forbidden as a daily agent loop on one Spark | +| 120B + Light Audio | Medium (**unmeasured**) | Medium | Highest | Good | Experiment only; still a 120B+ loop on one Spark — policy says no | +| 49B + Light Audio | High (**unmeasured**) | Lowest | Very High | Excellent | Strong *single-slot* experiment; not claimed as measured sweet spot | +| 340B (2× sharded) | N/A (multi-node) | High | Maximum | Excellent | On-demand specialist brain; never a single-Spark daily loop | ## Recommended Path to Sweet Spot (Single Node First) -1. **Immediate (this branch / next sprint)**: Capture rigorous baseline on current 120B + Riva. -2. **High-leverage experiment**: Replace Riva with minimal English audio stack; re-benchmark. -3. **Model A/B**: Stand up 49B v1.5 side-by-side; measure latency + memory + subjective quality on household-style prompts + memory recall tasks. -4. **Context tuning**: Implement or improve summarization + injection strategy; measure impact on effective memory quality vs. KV usage. -5. **Decision gate**: Choose primary model + audio stack for the family deployment based on data. -6. **2× node phase**: Once single-node sweet spot is locked and we need more (concurrency, 340B, or future capabilities), move to multi-node architecture. +1. **Immediate (living policy — already practice, document it)**: Treat the live path as 008 CPU speech + `get_agent()` + hosted Grok / one ~30B local worker. Do not call 120B + Riva “production.” +2. **Measure the live slot** (still open): idle vs one ~30B worker vs CPU STT/TTS; no invented GB figures until a report exists. +3. **Optional compose experiment**: If we want numbers, capture a labeled *experimental* baseline on 120B + Riva — not a production baseline. +4. **High-leverage experiment (compose only)**: Replace Riva with minimal English audio; re-benchmark. Live path already did the CPU version. +5. **Model A/B**: ~30B worker vs 49B v1.5 as the *single* occupied slot; 120B is not a daily-driver candidate on one Spark. +6. **Context tuning**: summarization + injection; measure recall quality vs. KV usage on the chosen single local model. +7. **Decision gate**: confirm the living slot policy with measured numbers (or keep it as practice-without-numbers, labeled unmeasured). +8. **2× node phase**: Once the single-node slot is understood and we need more (concurrency, 340B, or a second local LLM), move to multi-node architecture. ## Success Criteria -- We have a documented, reproducible benchmark baseline for the current stack. +- The living slot policy is written down and matches the desk (CPU speech, one local LLM, hosted Grok for quality-critical roles). +- We have a documented, reproducible benchmark baseline for the **live** stack, and (optionally) a clearly labeled experimental 120B + Riva report. Neither is claimed without a report. - We have measured data (not guesses) for at least two alternative configurations (light audio, 49B model). - We can articulate "the sweet spot" with numbers: model choice, audio stack, max comfortable context, max concurrent users, and expected voice turn latency. - The chosen configuration leaves **measurable, comfortable headroom** (>15–20 GB) for the agent, memory system, and future features under typical household load. @@ -198,7 +282,8 @@ Primary levers (ranked by expected impact on single-node headroom + latency): ## Open Questions & Risks -- Exact real-world memory footprint of the 120B NIM on DGX Spark unified memory (with our Docker setup) — highest priority unknown. +- Exact real-world memory footprint of the 120B NIM on DGX Spark unified memory (with our Docker setup) — **unmeasured**; optional experiment, not a live-path unknown. +- Exact real-world footprint of one ~30B-class Ollama / `openai_compatible` worker plus CPU Parakeet + Piper — **unmeasured** (practice exists; 007 report does not). - Quality delta between full Riva voices and lighter English TTS options for family members (subjective but important). - Whether 128K context on the 49B is "enough" given our Supermemory + summarization strategy, or whether we will miss the 1M capability. - Practical limits of 2-node RDMA tensor-parallel for the 340B in a home setting (latency, stability, complexity). @@ -207,14 +292,14 @@ Primary levers (ranked by expected impact on single-node headroom + latency): ## Next Steps -1. Review and approve this spec (user + team). -2. Create `plan.md` and `tasks.md` under `specs/007-dgx-hardware-optimization/` following the established SDD process. -3. Implement the benchmark harness + first baseline capture. -4. Run the high-leverage experiments (audio reduction, model comparison). -5. Iterate to the sweet spot with data in hand. +1. Keep the living slot policy in sync with the desk (this section is the source of truth for “what occupies the Spark”). +2. `plan.md` / `tasks.md` in this folder already exist; extend them when adding harness work — do not re-open 120B + Riva as the implied production path. +3. Implement the benchmark harness and capture a **live-path** report (CPU speech + hosted Grok and/or one ~30B worker). NIM + Riva GB numbers stay `[ ]` until measured. +4. Optional: labeled experimental compose runs (audio reduction, 49B vs 30B). No 120B+ daily loops. +5. Iterate to a measured sweet spot; until then, practice follows the living policy and estimates stay labeled unmeasured. --- -**We are getting there.** This spec gives us the map, the measuring stick, and the decision framework so we can move from "it works" to "it is optimal under our real constraints" with confidence and excitement. +**We are getting there.** This spec gives us the map, the measuring stick, and the decision framework so we can move from "it works" to "it is optimal under our real constraints" with confidence and excitement. The living slot policy is how we use the one local generative slot **today**; the rest of the document is how we measure experiments without pretending they are production. -**Status**: Ready for review. Once approved, we will commit the spec and proceed to planning the concrete benchmarking and optimization work. \ No newline at end of file +**Status**: Living policy in effect. Benchmark numbers for NIM + Riva (and for the live 30B worker) remain unmeasured until a 007 report lands. \ No newline at end of file diff --git a/specs/007-dgx-hardware-optimization/tasks.md b/specs/007-dgx-hardware-optimization/tasks.md index a20bf90..b4a947c 100644 --- a/specs/007-dgx-hardware-optimization/tasks.md +++ b/specs/007-dgx-hardware-optimization/tasks.md @@ -3,12 +3,42 @@ **Feature**: 007-dgx-hardware-optimization **Related Spec**: [spec.md](./spec.md) **Related Plan**: [plan.md](./plan.md) -**Status**: Ready for implementation +**Status**: Ready for implementation (living slot policy recorded 2026-08-19) **Branch**: `feat/007-dgx-hardware-optimization-impl` -This document breaks the work into small, dependency-ordered, checkable tasks. The first priority is always **capturing a trustworthy baseline** before any changes. +This document breaks the work into small, dependency-ordered, checkable tasks. The first priority is always **capturing a trustworthy baseline** before any changes — and labeling **live** vs **experimental-compose**. Do not treat 120B NIM + full Riva as the production spoken path. -Mark tasks complete only after the work is committed and (where applicable) the corresponding benchmark report is added. +Mark tasks complete only after the work is committed and (where applicable) the corresponding benchmark report is added. Policy-practice items below may be `[x]` without a 007 report when they already match the desk; **memory-number** items stay `[ ]` until measured. + +--- + +## Inference slot policy (living) — practice vs unmeasured + +Canonical text: [spec.md — Inference slot policy (living)](./spec.md#inference-slot-policy-living). + +### T-SLOT.1 – Honest live path (already practice) + +- [x] Desk voice I/O is spec 008 / `conversational-voice-agent`: Parakeet TDT 0.6B CPU STT + Piper CPU TTS (not this repo’s Riva orchestrator). +- [x] Brain is this repo `get_agent()`; default LLM is hosted Grok; local option is `openai_compatible` / Ollama ~30B-class with hosted fallback. +- [x] This repo’s `docker-compose` (`agent` + `riva` + `nemotron` 120b) is documented as experimental, not the live spoken path. +- [x] Policy recorded: GB10 ~128 GB unified / ~273 GB/s; **one serious local LLM at a time**; no 120B+ agent loops on one Spark. +- [x] STT/TTS on CPU is an explicit budget choice so the GPU/unified slot stays with at most one generative LLM. + +### T-SLOT.2 – Fleet vs slot (already practice) + +- [x] Quality-critical roles (orchestrator, architect, reviewer, design) use hosted Grok and do not occupy the local slot. +- [x] Local workers (coder, researcher) use ~30B-class with hosted fallback. +- [x] Do not co-schedule a second large local LLM next to an occupied slot. + +### T-SLOT.3 – Unmeasured NIM + Riva (and live-slot) memory numbers + +Do **not** invent GB figures. Leave TBD until a harness report exists. + +- [ ] Measured idle + load unified-memory footprint of the 120B NIM on this Spark (experimental compose). +- [ ] Measured full Riva ASR+TTS unified-memory footprint on this Spark (experimental compose). +- [ ] Measured peak for 120B NIM + full Riva + agent during a voice turn (experimental compose). +- [ ] Measured idle vs occupied-slot samples for one ~30B-class `openai_compatible` / Ollama worker plus CPU Parakeet + Piper (live path). +- [ ] Written 007 report that labels live vs experimental-compose and does not call 120B + Riva “production.” --- @@ -37,23 +67,32 @@ Mark tasks complete only after the work is committed and (where applicable) the - System RAM / swap. - [ ] Integrate sampling into the benchmark runner for the duration of a run. -### T0.5 – Baseline Run on DGX (Current Stack) -- [ ] On clean DGX Spark, pull the exact current images (120B + full Riva). -- [ ] Run the benchmark harness against the existing `docker-compose.yml`. +### T0.5a – Live-path baseline (as-is desk; priority) +- [ ] On the Spark, sample the spec 008 I/O loop + `get_agent()` with hosted Grok (slot idle). +- [ ] Repeat with one ~30B-class local worker occupying the slot (hosted fallback still configured). +- [ ] Execute at least one short-turns session and one longer household conversation on the live voice path. +- [ ] Capture whatever metrics the harness can take (do not invent GB figures). +- [ ] Commit as `benchmarks/reports/YYYY-MM-DD-baseline-live-cpu-speech/` (with `summary.md` + `raw/`), labeled **live**. + +### T0.5b – Experimental compose run (120B + Riva; not production) +- [ ] On clean DGX Spark, pull the compose images (120B NIM + full Riva). Optional experiment only. +- [ ] Run the benchmark harness against this repo’s `docker-compose.yml`. - [ ] Execute at least one "short turns" session and one "long household conversation" session. - [ ] Capture full report (metrics, logs, nvidia-smi samples, container stats). -- [ ] Commit the report as `benchmarks/reports/2025-05-XX-baseline-120b-riva/` (with `summary.md` + `raw/`). +- [ ] Commit as `benchmarks/reports/YYYY-MM-DD-experimental-120b-riva/` (with `summary.md` + `raw/`), labeled **experimental-compose**, never “production baseline.” ### T0.6 – Baseline Documentation -- [ ] Write `specs/007-dgx-hardware-optimization/results/phase-0-baseline.md` summarizing the measured numbers against the expectations in the spec. -- [ ] Update the decision matrix in the spec (or a living `decision-log.md`) with actual data. +- [ ] Write `specs/007-dgx-hardware-optimization/results/phase-0-baseline.md` summarizing measured numbers against the spec. Separate live vs experimental. Leave NIM + Riva GB as **unmeasured** if T0.5b has not run. +- [ ] Update the decision matrix in the spec (or a living `decision-log.md`) with actual data, or keep estimates labeled unmeasured. --- -## Phase 1 – Audio Stack Reduction +## Phase 1 – Audio Stack Reduction (compose experiment) + +Live path already uses CPU Parakeet + Piper (T-SLOT.1). Phase 1 is only if we revive Riva/NIM speech in compose. ### T1.1 – Lightweight English Audio Service Definition -- [ ] Research and select the exact lighter image(s): Parakeet English CTC NIM (or equivalent small ASR) + English TTS. +- [ ] Research and select the exact lighter image(s): Parakeet English CTC NIM (or equivalent small ASR) + English TTS. Prefer not occupying the generative GPU slot. - [ ] Create `docker-compose.light-audio.yml` (or profile) that replaces the full Riva service with the slimmed version. - [ ] Document exact tags, ports, and healthcheck expectations in the compose file and a small `audio-profiles.md`. @@ -67,18 +106,20 @@ Mark tasks complete only after the work is committed and (where applicable) the - [ ] Produce report `benchmarks/reports/...-light-audio/`. ### T1.4 – Phase 1 Gate & Decision -- [ ] Compare memory headroom, voice turn latency (p50/p95), and subjective quality. +- [ ] Compare memory headroom, voice turn latency (p50/p95), and subjective quality (**unmeasured** until T1.3). - [ ] Write `results/phase-1-audio-reduction.md`. -- [ ] Decision recorded: adopt light audio as new default (or keep full Riva). +- [ ] Decision recorded: live default remains 008 CPU speech; compose light-audio vs full Riva is experimental only. --- -## Phase 2 – Model Comparison (49B Candidate) +## Phase 2 – Model Comparison (single occupied slot) + +Do not load 49B *and* 120B at once. Living policy: one local generative LLM. -### T2.1 – Second LLM Service in Compose -- [ ] Add support for `llama-3.3-nemotron-super-49b-v1.5` (new service definition or override). -- [ ] Make the agent LLM endpoint configurable (`LLM_BASE_URL`, `LLM_MODEL` or similar) so we can point at different NIMs without code changes. -- [ ] Create `docker-compose.49b.yml` profile. +### T2.1 – Alternate LLM as the one slot occupant +- [x] Agent LLM endpoint already configurable (`LLM_PROVIDER`, `LLM_BASE_URL`, `LLM_MODEL`) for hosted Grok vs `openai_compatible` / Ollama. +- [ ] Add support for `llama-3.3-nemotron-super-49b-v1.5` as an **alternate** single-slot occupant (new service definition or override — not a concurrent second NIM). +- [ ] Create `docker-compose.49b.yml` profile, marked experimental. ### T2.2 – 49B Benchmark Runs - [ ] With the winning audio stack from Phase 1, run identical scenarios on the 49B model. @@ -86,8 +127,8 @@ Mark tasks complete only after the work is committed and (where applicable) the - [ ] Produce report and `results/phase-2-49b-comparison.md`. ### T2.3 – Phase 2 Gate -- [ ] Update decision matrix with real latency + headroom numbers. -- [ ] Lock primary model recommendation (49B daily driver + 120B optional deep mode is the current hypothesis). +- [ ] Update decision matrix with real latency + headroom numbers, or keep **unmeasured**. +- [ ] Lock local-worker class (~30B with hosted fallback is the living-policy hypothesis). Do **not** lock 120B as optional always-on deep mode on one Spark. --- @@ -108,9 +149,10 @@ Mark tasks complete only after the work is committed and (where applicable) the ## Phase 4 – Sweet-Spot Operationalization ### T4.1 – Default Configuration Update -- [ ] Update the main `docker-compose.yml` (or make the winning profiles the easy defaults via env). +- [ ] Do not default compose to 120B + Riva as “production.” Experimental profiles stay named experimental. +- [ ] Update the main `docker-compose.yml` (or make the winning profiles the easy defaults via env) only for stacks we intend to run. - [ ] Add high-level `make` targets: `make benchmark`, `make profile-sweet-spot`, etc. -- [ ] Update `docs/development.md` and any DGX runbooks with the new recommended command sequence. +- [ ] Update `docs/development.md` and any DGX runbooks with the new recommended command sequence (live path = 008 I/O + `get_agent()`). ### T4.2 – Final Results Package - [ ] Write `results/final-sweet-spot.md` with the locked configuration, all key metrics, and rationale. @@ -139,6 +181,6 @@ Mark tasks complete only after the work is committed and (where applicable) the --- -**First actionable tasks**: T0.1 – T0.3 (get the harness skeleton + instrumentation in place) so that the very first DGX run (T0.5) produces trustworthy, comparable numbers. +**First actionable tasks**: T-SLOT is recorded. Harness skeleton remains T0.1 – T0.3 so the first DGX run (T0.5a live path, optional T0.5b experimental compose) produces trustworthy, comparable numbers. NIM + Riva GB items in T-SLOT.3 stay `[ ]` until measured. -Let's go get those baseline numbers! \ No newline at end of file +Let's go get those baseline numbers — and keep calling the live path the live path. \ No newline at end of file diff --git a/specs/009-architect-coder-handoff/plan.md b/specs/009-architect-coder-handoff/plan.md new file mode 100644 index 0000000..663d221 --- /dev/null +++ b/specs/009-architect-coder-handoff/plan.md @@ -0,0 +1,115 @@ +# Plan: Architect ↔ coder inter-agent handoff (009) + +**Feature**: 009-architect-coder-handoff +**Spec**: [spec.md](./spec.md) +**Date**: 2026-08-19 + +## 1. What this plan is + +A map of **who writes what, in which language, and in which order**. It is not a plan to add modules to `thelab-langchain`. Success is a practiced loop, not a merged feature flag. + +Living practice sits in Hermes profiles `dgx-architect` and `dgx-coder`. This repo only records the contract. + +## 2. Handoff flow + +``` +architect writes packet + │ + ▼ + human accept ──reject──► architect revises packet + │ accept + ▼ + coder implements accepted spec only + │ + ▼ + review against acceptance criteria + │ + ├── pass → close work; residual risks stay documented + └── fail or new blocker → architect (redesign) or coder (fix), + only inside the accepted spec +``` + +### Step 1 — Spec (architect) + +The architect writes the design. Human prose for goals, non-goals, and trade-offs. STE for any procedure the coder must run. The architect does not open an implementation branch as the architect. + +### Step 2 — Human accept + +A person reads the five artifacts. Accept means: the coder may implement **this** packet. Reject means: the architect revises. Chat agreement without the packet is not accept. + +### Step 3 — Implement (coder) + +The coder follows the kanban body and the spec. The coder does not add architecture. If the packet is wrong, the coder files a blocker and stops. The coder does not “fix the spec in the PR.” + +### Step 4 — Review + +A human (and optionally a reviewer agent) ticks acceptance criteria. Residual risks are not automatic fail. Unstated work is fail (scope creep) or a new spec, not a silent extra commit. + +## 3. Where text is STE vs human prose + +| Text | Language | Why | +|------|----------|-----| +| This SDD folder (`spec.md` / `plan.md` / `tasks.md`) | Human prose | Humans read rationale. STE is the wrong register for “why.” | +| Spec overview, goals, non-goals, relationships | Human prose | Design argument. | +| Spec procedures the coder must execute | STE | Agent-consumed. Skill `asd-ste100`. | +| Acceptance criteria | STE (one check per line) | Reviewer ticks; no synonyms. | +| Kanban body | STE | Primary coder input. Numbered lists for 3+ steps. | +| Blockers | STE facts | “X is missing.” Not “we should maybe wait.” | +| Residual risks | Human prose is allowed; names stay one-meaning | Risk needs context; do not hide it in hedges. | +| README / marketing / user docs | Human prose | STE is **not** marketing copy. | +| Review pass/fail lines | STE | “Criterion FR-2 fails. The factory is missing.” | +| Review narrative | Human prose | What was tried, what was out of scope. | +| Chat between humans | Human prose | Chat is not the packet. | + +The skill **`asd-ste100`** (agents skill `~/.agents/skills/asd-ste100`, Grok skill `asd-ste100`) is the procedure reference. Do not paste the skill body or the ASD dictionary into the packet. + +## 4. Packet shape (architect output) + +The architect produces, in one place the coder can fetch: + +1. **Spec** — design. Link or body. Seams named (`get_agent()`, editable install, env examples) without host home paths as required layout. +2. **Acceptance criteria** — tick list. No invented latency numbers. +3. **Kanban body** — STE steps. Points at (1) and (2). +4. **Blockers** — empty list is allowed if explicitly written as “none.” +5. **Residual risks** — empty list is allowed if explicitly written as “none.” + +“None” written is complete. A missing section is not. + +## 5. Coder constraints + +- Implement **accepted** specs only. +- Do not implement from an architect draft, a voice transcript, or a chat summary. +- Do not copy Hermes profile files into the worktree. +- Stop at the spec’s edge. Out-of-tree work (as in spec 008) stays out of tree; this package keeps the brain seam only. + +## 6. What this repo does and does not run + +| Mechanism | Status | +|-----------|--------| +| Hermes profiles `dgx-architect` / `dgx-coder` | Practiced on the workstation (outside this repo) | +| SDD record in `specs/009-architect-coder-handoff/` | This folder | +| CI lint of STE | **Not done** | +| CI that requires an accepted packet | **Not done** | +| Runtime enforcement in `thelab-langchain` | Out of scope | + +Do not add a STE linter, pre-commit hook, or GitHub Action under this spec. That would be a later spec, likely after 005, and it is not claimed here. + +## 7. Relationship to 001 and 008 + +- Work **toward** spec 001 still uses this handoff. 009 does not pick Riva vs Parakeet vs Piper. +- Spec 008 already ran out of tree. New I/O-repo work should arrive as an accepted packet, not as a paste of a Hermes wiki. + +## 8. Risks + +| Risk | Mitigation | +|------|------------| +| Chat replaces the packet | Human accept looks for the five artifacts. No packet → no implement. | +| Architect implements “a small fix” | Role rule: architect never implements. Small fixes still need a coder (or a human who is not wearing the architect role). | +| Coder redesigns in the PR | Review fails on unstated work. New design → architect + new accept. | +| STE used for README voice | Plan table: marketing and rationale stay prose. | +| Skill or dictionary copied into git | Spec forbids it. Reference `asd-ste100` by name. | +| This spec treated as a product feature | Status line: living practice only. | + +## 9. What this plan is not + +It is not a rewrite of spec 001. It is not the fleet operating manual. It is not a promise that CI will catch a missing handoff. diff --git a/specs/009-architect-coder-handoff/spec.md b/specs/009-architect-coder-handoff/spec.md new file mode 100644 index 0000000..ce4a4d9 --- /dev/null +++ b/specs/009-architect-coder-handoff/spec.md @@ -0,0 +1,173 @@ +# Feature Spec: Architect ↔ coder inter-agent handoff (STE) + +**Feature ID**: 009-architect-coder-handoff +**Status**: Living practice (documented here; **not** a product feature of `thelab-langchain`) +**Created**: 2026-08-19 +**Owner**: Derek +**Related**: [001-voice-dgx-spark-agent](../001-voice-dgx-spark-agent/spec.md), [008-local-tts-lenovo-go-spike](../008-local-tts-lenovo-go-spike/spec.md) + +## Record-keeping note + +This spec records a **workstation practice**: two Hermes profiles (`dgx-architect` and `dgx-coder`) hand work to each other through written artifacts. The practice is already in use. This folder is the SDD trail in the brain repo so later specs (and humans) can see the contract. + +It is **not** a library, CLI flag, LangGraph node, or CI gate in this package. Spec 008’s spike ran out of tree; this protocol is how design and implementation stay split when that kind of work happens again. Spec 001 remains the long-term voice-agent goal. This spec does not change 001’s stack. + +Do **not** treat Hermes profile files, skill bodies, or an ASD dictionary as part of this repo. Those live outside git. This document **distills** the design. + +## Overview + +An **architect** agent designs. A **coder** agent implements. A **human** accepts the design before code is written. The packet that crosses the gap is a small, named set of artifacts. Procedure text that an agent must follow is written in Simplified Technical English (ASD-STE100 **principles** — see rules of thumb below). Rationale, trade-offs, and user-facing prose stay in ordinary English. + +The full controlled-language skill is **`asd-ste100`**. It lives outside this repo (`~/.agents/skills/asd-ste100`, and the Grok skill of the same name). Reference the skill by name. Do not paste the skill body or the ASD dictionary into specs, PRs, or kanban text. + +## Goals + +- Keep design and implementation in different roles so the coder does not invent architecture. +- Make the handoff packet complete enough that a coder can start without a chat transcript. +- Put agent-consumed procedures in STE so instructions have one meaning. +- Leave a human accept gate between “designed” and “implement this.” +- Document residual risk instead of hiding it in chat. + +## Non-goals + +- A product feature, API, or runtime mode inside `thelab-langchain`. +- CI that lints STE, blocks merges, or assigns Hermes profiles (see [tasks.md](./tasks.md) — **not done**). +- The full workstation fleet manual (orchestrator, researcher, reviewer, kanban-vs-chat). That stays in Hermes docs, not this spec. +- Copying profile `SOUL.md`, Hermes env files, or the ASD-STE100 dictionary into git. +- Marketing copy, README voice, or human design rationale written as STE. +- Telephony, hosted voice, or unpublished-company product framing. + +## Roles + +| Role | Does | Does not | +|------|------|----------| +| **Architect** (`dgx-architect`) | Designs. Writes the spec, acceptance criteria, kanban body, blockers, and residual risks. Names seams and out-of-scope work. | Implement. Open an implementation PR. “Just quickly” patch production code. Expand into researcher or reviewer work. | +| **Coder** (`dgx-coder`) | Implements **accepted** specs only. Follows the kanban body and acceptance criteria. Reports new blockers. Stops at the spec’s edge. | Redesign. Implement from chat only. Widen scope. Silently drop acceptance checks. | +| **Human** | Accepts or rejects the design packet. Resolves product calls the architect flagged. Reviews the result against acceptance criteria (alone or with a reviewer). | Skip the accept gate “because the architect was confident.” | + +The architect never implements. The coder never treats an unaccepted draft as a build order. + +## Domain terms (define once) + +Use these words with one meaning in handoff artifacts: + +| Term | Meaning | +|------|---------| +| **Spec** | The design document the architect writes. It states what to build, what not to build, and which seams stay stable. | +| **Acceptance criteria** | Binary checks. The implementation passes or it fails. No “should feel faster.” | +| **Kanban body** | The work-item text the coder **consumes**. Procedure, not a status emoji. | +| **Blocker** | A condition that prevents start or completion. Named, owned, and either cleared or carried. | +| **Residual risk** | A known remaining risk after the human accepts the design. Not a surprise at review. | +| **Handoff** | The packet: spec + acceptance criteria + kanban body + blockers + residual risks, in the accepted state. | +| **Human accept** | The gate. A person marks the packet accepted. Only then may the coder implement. | +| **STE** | Simplified Technical English using ASD-STE100 principles (rules of thumb in this spec). The `asd-ste100` skill is the procedure reference. | + +Do not reuse these words for other meanings in the same packet (for example, do not call a brainstorm a “spec”). + +## Handoff artifacts + +Every architect → coder handoff includes all five. If one is missing, the packet is not ready for human accept. + +### 1. Spec + +- States goals, non-goals, seams, and the smallest change that meets the goal. +- Human prose is allowed for *why*. +- If the spec contains a procedure the coder must execute, that procedure is STE. + +### 2. Acceptance criteria + +- Written as checks a reviewer can tick. +- Each criterion is one testable outcome. +- No latent numbers, no invented latency targets, no “as before unless it is better.” + +### 3. Kanban body + +- The instruction the coder follows. +- STE. Numbered steps when there are three or more. +- Points at the spec and the acceptance criteria. Does not replace them. +- Does not embed secrets, tokens, host layout paths, or board/issue identifiers as required reading. + +### 4. Blockers + +- What must be true before implementation starts, or what will stop it mid-flight. +- Each blocker is a fact (missing seam, unaccepted dependency, out-of-tree repo not ready), not a mood. + +### 5. Residual risks + +- What remains wrong or fragile if the coder meets every acceptance criterion. +- The reviewer reads this list. The coder does not “fix” residual risk unless the accepted spec says so. + +## STE rules of thumb + +These are **principles** for agent-consumed procedures. They are not a substitute for the `asd-ste100` skill and not a copy of the ASD dictionary. + +1. **One meaning per word.** Pick a term from the table above (or define a new domain term **once**) and keep it. +2. **Active voice.** “The coder writes the factory.” Not “the factory should be written.” +3. **Simple tense.** Give instructions in the present or imperative. Do not stack conditionals. +4. **One instruction per sentence.** +5. **Short sentences.** Split a long sentence. +6. **Numbered lists for 3+ steps.** Do not hide a sequence in a paragraph. +7. **Noun clusters ≤ 3 words.** Prefer “checkpointer factory” to “optional session persistence checkpointer factory helper.” +8. **Define domain terms once.** Then use the defined word. + +STE is for **agent-consumed procedures** (kanban bodies, implementation steps, acceptance checks). It is **not** for marketing copy, README tone, or the “why we chose this” sections of a spec. + +## Functional requirements + +### FR-1 Role split + +- Architect output is design artifacts only. +- Coder input is an **accepted** handoff packet. +- Unaccepted drafts are not implementation tasks. + +### FR-2 Packet completeness + +- Human accept is refused if any of the five artifacts is missing or is a placeholder. +- The kanban body must not be the only copy of the spec. + +### FR-3 Language split + +- Procedures the coder or another agent must follow: STE, skill `asd-ste100`. +- Human rationale, status notes, and this SDD folder: ordinary prose. + +### FR-4 Stop conditions + +- The coder stops when acceptance criteria are met or a new blocker appears. +- Scope not in the spec is out of scope, including “obvious” refactors. + +### FR-5 Secrets and layout + +- Handoff text must not require API keys, tokens, hardware serials, board/chat identifiers, or absolute home-directory paths as the layout of record. +- Point to gitignored env examples and documented seams (`get_agent()`, provider factory) instead. + +## Non-functional requirements + +- The protocol is practiced in Hermes profiles `dgx-architect` and `dgx-coder` on the workstation. +- This repository **does not** enforce the protocol in CI. +- Specs in `specs/` remain human-readable SDD. They may *describe* STE; they need not be written entirely in STE. +- No latency or throughput numbers unless a later spec measures them. + +## User stories + +1. As architect, I hand a complete packet to a human so the coder never has to reconstruct the design from chat. +2. As human, I accept or reject before anyone writes production code. +3. As coder, I implement only what the accepted spec and kanban body say, in STE steps I can follow without guessing synonyms. +4. As reviewer, I tick acceptance criteria and read residual risks instead of rediscovering them. + +## Acceptance criteria (for this SDD record) + +- [x] This folder contains `spec.md`, `plan.md`, and `tasks.md` that name the two roles, the five artifacts, the STE rules of thumb, and the `asd-ste100` skill **by name only**. +- [x] Status is “living practice,” not a `thelab-langchain` feature. +- [ ] CI in this repo lint-checks STE or blocks coder PRs that lack an accepted packet — **not done** (out of scope until a later spec). +- [x] No Hermes profile file, skill body, or ASD dictionary is copied into this repo. + +## Relationship to other specs + +- **001** — long-term voice agent. Handoffs for work *on* 001 follow this protocol. This spec does not revise 001’s ASR/TTS/NIM choices. +- **008** — Lenovo Go spike, **executed out of tree**. The spike is the example of implementation living outside this package while the brain seam stays here. Future out-of-tree work should still cross this handoff, not a chat paste. +- **004 / 005** — checkpointers and CI remain their own specs. 009 does not implement them and does not claim CI enforcement. + +## Open questions + +- Whether a later spec should add a lightweight “packet complete?” checklist in PR templates (still not CI). +- Whether reviewer-agent output must also be STE. Default until decided: **acceptance write-up in human prose; fail/pass lines in STE.** diff --git a/specs/009-architect-coder-handoff/tasks.md b/specs/009-architect-coder-handoff/tasks.md new file mode 100644 index 0000000..8bd9993 --- /dev/null +++ b/specs/009-architect-coder-handoff/tasks.md @@ -0,0 +1,44 @@ +# Tasks: Architect ↔ coder inter-agent handoff (009) + +**Feature**: 009-architect-coder-handoff +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +Checkboxes are honest. The protocol is **practiced in Hermes**. This repo **does not** enforce it in CI. This folder is the SDD record, not a `thelab-langchain` feature. + +## Phase 0 — Record the practice (this repo) + +- [x] Write `spec.md` with roles, five handoff artifacts, STE rules of thumb, and skill name `asd-ste100` (no skill body, no ASD dictionary) +- [x] Write `plan.md` with flow spec → human accept → implement → review, and STE vs human prose +- [x] Write `tasks.md` (this file) +- [x] State status as living practice, not a product feature +- [x] Point at related specs 001 and 008 without revising their stack choices + +## Phase 1 — Workstation practice (Hermes, outside this repo) + +- [x] Architect profile `dgx-architect` designs and does not implement (living practice) +- [x] Coder profile `dgx-coder` implements accepted specs only (living practice) +- [x] Handoff packet in use: spec, acceptance criteria, kanban body, blockers, residual risks +- [x] STE reserved for agent-consumed procedures via skill `asd-ste100` (agents skill and Grok skill; not vendored here) + +Do not copy profile files, Hermes env files, or skill bodies into this tree to “complete” a checkbox. + +## Phase 2 — Enforcement in this repo (**not done**) + +- [ ] CI job that lints STE in kanban/spec procedures +- [ ] CI job that blocks implementation PRs without an accepted packet +- [ ] Pre-commit or ruff-like hook for noun-cluster / sentence rules +- [ ] Runtime or library support in `thelab-langchain` for architect/coder roles + +Phase 2 is **out of scope** for 009. Leave the boxes empty. Do not implement them under this spec. + +## Phase 3 — Optional later SDD hygiene (not required to call 009 done) + +- [ ] PR template checklist that names the five artifacts (docs only; still not CI) +- [ ] Decision on reviewer-agent output language (plan default: fail/pass in STE, narrative in prose) + +## Traceability + +- Practice: Hermes profiles `dgx-architect` and `dgx-coder` on the workstation. +- Skill: `asd-ste100` outside this repo. +- Spike that already ran out of tree: [008](../008-local-tts-lenovo-go-spike/tasks.md). +- This tasks file is only the checklist view. It does not claim CI or package enforcement. diff --git a/specs/010-worker-completion-protocol/plan.md b/specs/010-worker-completion-protocol/plan.md new file mode 100644 index 0000000..98e431e --- /dev/null +++ b/specs/010-worker-completion-protocol/plan.md @@ -0,0 +1,105 @@ +# Plan: Durable-board worker completion protocol (010) + +**Feature**: 010-worker-completion-protocol +**Spec**: [spec.md](./spec.md) +**Date**: 2026-08-19 + +## 1. Protocol vs runtime + +The spec is the durable contract. **Hermes Kanban is the current runtime** — an implementation detail, not the protocol. + +``` +Human + └─ orchestrator (front door + dispatcher) + └─ durable board + ├─ architect / researcher / designer (durable directory) + ├─ coder (git worktree → PR) + └─ reviewer (same tree as the change) +``` + +This Python package does not own that board, spawn workers, or persist cards. Do not add a board module here to “implement 010.” + +## 2. Mapping table (Hermes today) + +Abstract spec terms map onto the workstation fleet as follows. Profile *models* and how to start a gateway are out of scope; they live in the local Hermes operating manual and will change. + +| Spec term | Hermes Kanban today | +|-----------|---------------------| +| Board | Durable Kanban (SQLite-backed). Cards outlive a chat turn. | +| **complete** | Worker terminal `kanban_complete` (or equivalent complete action). | +| **block** | Worker terminal `kanban_block`. Human later unblocks. | +| Protocol violation | Clean worker exit with neither action. Circuit breaker auto-blocks the card. Not success. | +| **scratch** | `scratch` workspace — deleted on complete. | +| **durable directory** | `dir:` workspace rooted at the repo or docs tree. | +| **git worktree** | `worktree` / `worktree:` workspace. | +| **orchestrator** | Default profile; owns dispatch (`kanban.orchestrator_profile`). | +| **architect** | Architect specialist profile (specs only). | +| **researcher** | Researcher specialist profile. | +| **coder** | Coder specialist profile. | +| **designer** | Designer profile (visual only). | +| **reviewer** | Reviewer profile (never implements). | +| Unknown assignee | Card stays **ready**; dispatcher does not spawn. | +| Done (code) | Human merge on the host Git forge. Worker complete ≠ merge. | + +If the board product changes, keep the spec terms and rewrite this table. Do not fork a second protocol. + +## 3. How a card is supposed to finish (runtime) + +1. Dispatcher assigns a **roster** profile and a **workspace kind** that matches the deliverable. +2. Worker does the work in that workspace. +3. Worker calls **complete** (artifacts in the summary) or **block** (human question / gate). +4. Dependent cards stay unstarted until parents are **done** in board terms. For code, “done” in the *product* sense is still human merge (spec FR-5); parent-complete is only the board edge that unblocks the next specialist. + +Standard shape, not a command sheet: + +``` +architect (durable directory, spec) + → human accepts + → coder (worktree, PR + tests) → reviewer (block or approve) + → human merges +``` + +Research or design lanes may feed architect; they still complete or block, and they still do not land product code. + +## 4. What Hermes is responsible for (not this repo) + +- Persisting cards and comments. +- Spawning the assigned profile into the chosen workspace. +- Treating missing complete/block as a violation (circuit breaker). +- Leaving unknown assignees in ready. +- Deleting scratch on complete. +- Not running a second dispatcher beside the orchestrator’s. + +Operator recovery (reclaim, reassign, unblock) is Hermes operations. This plan does not catalog those commands. + +## 5. What this repo is responsible for + +- Keep this SDD folder as the protocol source of truth in git. +- When fleet workers touch **this** tree, they obey the spec: durable directory for `specs/` and `docs/`, worktree for `src/` / tests, complete/block, no secrets on the card, human merge for code. +- Do **not** encode the protocol in pytest or GitHub Actions. Spec 005 CI is for this package’s Python, not for Hermes worker exits. + +## 6. Risks + +| Risk | Mitigation | +|------|------------| +| Spec lives only in a local how-to and drifts | This folder; revise when the runtime mapping changes | +| Worker “succeeds” by exiting | Runtime circuit breaker; treat as violation; fix the worker, then reclaim | +| Spec/code written on scratch | Ban scratch for durable deliverables (spec FR workspace table) | +| Invented assignee | Closed roster; idle-in-ready is the failure mode | +| Coder complete treated as ship | Spec FR-5: human merge | +| Secrets in card comments | Spec FR-3; redact and re-complete/block if it happens | +| Building a board in `thelab_langchain` | Explicit non-goal | + +## 7. Success (qualitative) + +No invented metrics. The protocol is working when: + +- Completed cards name artifacts a human can open. +- Blocked cards name the human action required. +- Silent exits are treated as violations, not green cards. +- Specs still exist after architect complete (durable directory). +- Merged PRs, not completed coder cards, are what landed in `main`. + +## 8. What this plan is not + +It is not a Hermes CLI cheat sheet. It is not Slack (or any messenger) delivery. It is not a model-routing or GPU-budget plan (see 007). It is not a request to vendor `~/.hermes/docs/agentic-workflow.md` into this tree — that file stays where Hermes expects it. diff --git a/specs/010-worker-completion-protocol/spec.md b/specs/010-worker-completion-protocol/spec.md new file mode 100644 index 0000000..9b22d50 --- /dev/null +++ b/specs/010-worker-completion-protocol/spec.md @@ -0,0 +1,162 @@ +# Feature Spec: Durable-board worker completion protocol + +**Feature ID**: 010-worker-completion-protocol +**Status**: Living practice +**Created**: 2026-08-19 +**Owner**: Derek Clair +**Related**: [009-architect-coder-handoff](../009-architect-coder-handoff/spec.md), [008-local-tts-lenovo-go-spike](../008-local-tts-lenovo-go-spike/spec.md) + +## Record-keeping note + +This is the workstation **fleet protocol**: how a durable-board worker is allowed to finish. It is already practiced on the desk. This folder is the SDD record in the brain repo so the rule is not only a local Hermes how-to. + +It does **not** add a board to `thelab_langchain`. The current runtime is Hermes Kanban (see [plan.md](./plan.md)). The protocol outlives that runtime. + +## Overview + +Specialist workers (architect, researcher, coder, designer, reviewer) take durable cards from a board. Each run has exactly one legal finish: + +1. **complete** — the card’s acceptance criteria are met, and the summary names concrete artifacts a human can open; or +2. **block** — the worker cannot proceed without a human (missing decision, failed gate, unsafe change). + +A clean process exit with neither is a **protocol violation**. The card is not done. Downstream work must not treat silence as success. + +## Goals + +- Make terminal state unambiguous: complete or block, never “the process returned 0.” +- Make completed work inspectable: paths, PR URLs, test counts — not vibes. +- Keep secrets off the board (summaries, comments, metadata, artifact fields). +- Put durable work on durable workspaces; delete-on-complete scratch is only for throwaway probes. +- Dispatch only the real roster. Invented role names must not look like they are queued to run. +- Keep **human merge** as the definition of done for code. + +## Non-goals + +- A Kanban (or any board) implementation inside this Python package. +- CI in this repo that asserts complete/block (the fleet is not a thelab unit test). +- Vendoring the Hermes operating manual, CLI recipes, or chat/notification plumbing. +- Short in-conversation subagents (`delegate_task` and the like). Those die with the parent turn and are not this protocol. +- Changing spec 008’s hardware spike or this package’s `get_agent()` contract. + +## User stories + +1. As a worker, I finish by completing or blocking so the board never confuses a quiet exit with success. +2. As a human, I open a completed card and find artifacts I can verify (a spec path, a PR, a test count). +3. As a human, I never find keys, tokens, or env dumps in board fields. +4. As an architect, my spec still exists after the card completes because it was not on scratch. +5. As a dispatcher, I only assign names on the roster; a typo sits in ready instead of spawning a ghost worker. +6. As a coder, “I opened a PR” is not done — a human merges. + +## Roster (closed) + +Board dispatch uses **only** these roles: + +| Role | Owns | Must not | +|------|------|----------| +| **orchestrator** | Decompose work, assign the roster, keep the board moving | Invent assignees; flood the board without a task graph | +| **architect** | Specs, architecture, plans | Product implementation | +| **researcher** | Sources, findings, comparisons | Product implementation | +| **coder** | Implementation from an accepted spec; branch, tests, PR | Merge; treat unreviewed work as done | +| **designer** | Visual / UI deliverables | Backend ownership | +| **reviewer** | Review only; approve or block with comments | Implement the fix | + +Unknown role names are **not** dispatched. They remain in **ready** forever. That idle state is a routing bug, not a running worker. + +Other chat profiles may exist on the workstation. They are not board assignees unless they are added to this table in a spec revision. + +## Workspace kinds + +| Kind | Use for | After **complete** | +|------|---------|-------------------| +| **scratch** | Throwaway probes only | **Deleted**. Never for durable specs, docs, or product code. | +| **durable directory** | Specs, plans, docs packages | Survives. This is where SDD lives. | +| **git worktree** | Code changes | Survives as a worktree / branch. Not a substitute for a PR + human merge. | + +A card whose deliverable must be read later **must not** use scratch. Completing a spec card on scratch is a failed card even if the worker called complete. + +## Functional requirements + +### FR-1 Terminal action (non-negotiable) + +- Every worker run **MUST** end with **complete** or **block**. +- Clean exit without either is a **protocol violation**. +- A violation MUST NOT be recorded as success. The runtime SHOULD trip a circuit breaker / auto-block so the card cannot look healthy. +- Reclaim and retry are operator actions after a violation; they do not rewrite history into “completed.” + +### FR-2 Complete payload + +On **complete**, the board-visible summary (and any artifact metadata) MUST include concrete, checkable items as they apply: + +- Filesystem paths for specs/docs (durable directory). +- PR URL (and branch name if useful) for code. +- Test counts actually observed (e.g. `N passed` / `N failed`) — do not invent numbers. + +Optional but useful: what was *not* done, if the accepted spec scoped it out. + +### FR-3 Secrets stay off the board + +Board fields (title, body, comments, complete summary, metadata, artifact lists) MUST NOT contain: + +- API keys, tokens, OAuth material, `.env` contents +- Serials, phone numbers, personal IPs +- Chat/channel/DM identifiers +- Issue-tracker deep links that are private coordination, when a repo path or PR URL suffices + +Secrets belong in the worker’s private environment, never in the card. + +### FR-4 Block is a first-class finish + +**block** is a legal, expected terminal action. The worker MUST say: + +- what is blocked, +- what a human must decide or provide, +- what was already tried, if that is needed to unblock. + +A reviewer who will not approve **blocks**. An architect who lacks a decision **blocks**. Stalling in-process hoping the parent notices is not a finish. + +### FR-5 Definition of done (code) + +For product code: + +1. Architect spec accepted by a human. +2. Coder implements on a worktree / branch and opens a PR; coder **completes** with that PR and test evidence. +3. Reviewer **completes** (approve) or **blocks** (comments). Reviewer complete is not merge. +4. **A human merges.** That merge is the definition of done. + +A completed coder card with an unmerged PR is *ready for review / merge*, not done. + +For specs and docs on a durable directory, **complete** means the files are on disk at the named paths. Human review of the spec is still the gate before implementation (see FR-6). + +### FR-6 Spec-first for non-trivial code + +Non-trivial implementation is not assigned to coder until a human has accepted the spec. Reviewer does not write the product patch. + +### FR-7 Handoff language + +*What* to write in the card (tone, how to name artifacts, how to ask a human) is spec **009**. This spec is the *terminal action* and the workspace/roster rules. A well-worded silent exit still violates FR-1. + +## Non-functional requirements + +- Protocol is role- and runtime-agnostic: complete/block, workspace kinds, closed roster, human merge. +- Honest SDD: this package does not run the board; do not write tasks as if it will. +- No metrics theater: do not invent pass rates, latency, or fleet health numbers in this spec. + +## Acceptance criteria + +- [x] A worker that exits without complete or block is a protocol violation, not a successful card. +- [x] Complete summaries name artifacts (paths, PR URLs, and/or real test counts) and contain no secrets. +- [x] Scratch is never used for specs, docs, or product code that must survive the card. +- [x] Durable directory is the workspace for SDD; git worktree is the workspace for code. +- [x] Dispatch uses only orchestrator, architect, researcher, coder, designer, reviewer. Unknown names sit in ready. +- [x] Architect / researcher / reviewer / designer do not implement product code on their cards. +- [x] Code is done when a human merges, not when a worker completes. + +## Relationship to other specs + +- **009** — handoff *language* (how a worker talks on the card). This spec is the *protocol* (how a worker is allowed to stop). +- **008** — Lenovo Go voice I/O spike, executed in another repo. Same honesty rule: record where work actually lives; do not pretend this package owns it. +- **001 / 007** — product/hardware goals. This spec does not change them. + +## What this spec is not + +It is not a Hermes CLI manual. It is not a request to build `thelab_langchain.kanban`. It is not permission to treat chat-profile names as board roles. diff --git a/specs/010-worker-completion-protocol/tasks.md b/specs/010-worker-completion-protocol/tasks.md new file mode 100644 index 0000000..8d8d500 --- /dev/null +++ b/specs/010-worker-completion-protocol/tasks.md @@ -0,0 +1,47 @@ +# Tasks: Durable-board worker completion protocol (010) + +**Feature**: 010-worker-completion-protocol +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) + +This protocol is **practiced on the Hermes Kanban runtime**. This Python package +does not implement a board. There is **no CI** in thelab for complete/block. +Checkboxes below are a record of that split, not a backlog to build Kanban here. + +## Phase 0 — Protocol (living practice, not this package) + +Practiced on the workstation fleet; not code in `src/thelab_langchain/`. + +- [x] Workers end with **complete** or **block** +- [x] Clean exit without either is a protocol violation (not success) +- [x] Complete carries concrete artifacts (paths, PR URLs, observed test counts) +- [x] No secrets in board fields +- [x] Workspace: scratch deleted on complete; never for durable specs/code +- [x] Workspace: durable directory for specs/docs +- [x] Workspace: git worktree for product code +- [x] Closed roster only (orchestrator, architect, researcher, coder, designer, reviewer) +- [x] Unknown role names sit in ready; not dispatched +- [x] Human merge is definition of done for code +- [x] Architect / researcher / designer / reviewer do not implement product code on their cards + +## Phase 1 — SDD record (this repo) + +- [x] `specs/010-worker-completion-protocol/spec.md` +- [x] `specs/010-worker-completion-protocol/plan.md` (Hermes Kanban as current runtime) +- [x] `specs/010-worker-completion-protocol/tasks.md` (this file) + +## Explicitly not tasks in thelab + +Do not open work in this package for: + +- A board, dispatcher, or worker runner under `thelab_langchain` +- Pytest or GitHub Actions that assert Hermes complete/block +- Copying the local Hermes operating manual into git +- Chat/notification integration as part of 010 + +If the board runtime is replaced, update [plan.md](./plan.md) mapping — do not add a board here to “finish” 010. + +## Traceability + +Runtime and recovery procedure: local Hermes docs (not vendored). +Handoff wording: spec 009. +This folder is only the protocol SDD. diff --git a/specs/011-voice-reply-contract/plan.md b/specs/011-voice-reply-contract/plan.md new file mode 100644 index 0000000..7c58ec4 --- /dev/null +++ b/specs/011-voice-reply-contract/plan.md @@ -0,0 +1,149 @@ +# Plan: Voice-facing reply contract (011) + +**Feature**: 011-voice-reply-contract +**Spec**: [spec.md](./spec.md) +**Date**: 2026-08-19 +**Status**: Specified / not fully enforced in code + +## 1. Where the text comes from + +``` +utterance ─► I/O repo (Parakeet) + │ + ▼ + get_agent() this repo + memory_injection → LLM (+ optional memory tools) + │ + ▼ + AIMessage.content ← contract applies here + │ + ▼ + I/O repo: sentence-chunk → Piper → aplay + (button stop_event cancels playback) +``` + +The I/O process does not re-implement the agent (spec 008). It also does not +rewrite the reply for speakability. Chunking splits on `.` `!` `?` so first +audio can start before the full reply is synthesized. That is playback +scheduling, not a markdown/code filter. + +This plan is only about making `AIMessage.content` safe to speak. Latency +tables stay in the sibling repo README; this plan does not copy them. + +## 2. Three ways to enforce (none locked) + +### A. Prompt / system message on the graph (possible now) + +Add a short voice-facing `SystemMessage` (or prepend to the memory-injection +message) so `get_agent()` asks for short speakable prose, no tables/code unless +asked, and escalation of multi-step work. + +Facts that make this cheap: + +- Live speakerphone already uses `get_agent()`. `thelab-chat` uses `MemoryChat`, + not the graph. A graph-only instruction would not change the CLI path. +- No extra LLM call. Same turn, different instruction. +- 001 T4.2 already named this; it was never done. + +Limits: + +- Models ignore style instructions under tool-use or “be thorough” pressure. +- No unit-testable guarantee. A table can still come out. +- Must keep the text short so it does not fight memory context for attention. + +### B. Thin deterministic formatter (not built) + +A pure function on the reply string after the graph returns, before the I/O +process speaks it. Examples of mechanical rules (illustrative, not a shipped +list): + +- Drop fenced code blocks or replace with “I have a code block; say if you want + it read.” +- Drop markdown tables or summarize as “that is a table of N rows.” +- Strip heading hashes and collapse bullet markers into commas. +- Cap length (e.g. first N sentences) unless the user asked for more. + +Where it could live: + +- **This repo** — I/O keeps calling `get_agent()` / `invoke` and speaking + whatever comes back. Better seam: one brain, one content policy. +- **I/O repo** — this package stays format-agnostic. Worse: every consumer + reimplements the contract. + +Limits: + +- Easy to over-strip when the user *did* ask for a snippet. +- Heuristics are English-and-markdown-shaped; they will miss clever formatting. +- Still not a second model. Must not add an LLM rewrite pass (spec 007 / the + existing “no extra round-trip per voice turn” rule in `graph.py`). + +Status: **not built**. No module, no tests, no hook in `get_agent()`. + +### C. Leave it to the voice-profile SOUL in Hermes + +A Hermes voice profile already carries persona and tone for some sessions. +That file stays in Hermes. **Do not copy `SOUL.md` into this tree.** + +Limits: + +- The Lenovo Go loop (spec 008) invokes `get_agent()` with session + `HumanMessage` / `AIMessage` history plus this package’s memory injection. + It does not load a Hermes SOUL. Relying on SOUL alone does **not** cover the + live speakerphone path. +- Same “models can ignore it” limit as option A, plus an extra repo to keep + in sync. +- Useful as *additional* flavor if a Hermes-hosted session injects it; not a + substitute for A or B on the 008 path. + +## 3. Suggested sequence (if we implement) + +Not a commitment; a default order if someone picks this up: + +1. **Prompt-level on `get_agent()`** (option A). Smallest change, possible now, + matches 001 T4.2. Keep the instruction to a handful of lines. +2. **Listen to real sessions.** If markdown/code still hits Piper, add option B + in *this* repo as a post-`invoke` helper the I/O process can call — or fold + it into `get_agent()` so the I/O import surface stays one function. +3. **Do not vendor SOUL.** If Hermes sessions need the same rules, point them + at this spec rather than duplicating a second policy file here. + +Mixes are allowed (A + B). C is optional flavor, not the desk-loop control. + +## 4. Escalation (spoken turn vs board) + +The spoken turn is the wrong place to implement a multi-step coding or research +job. Implementation of *how* work reaches the orchestrator / board is Hermes +fleet operations, not this package. + +Until that wiring exists, option A can still say: if the ask is a multi-file +change or a research spike, reply with a short ack and do not dump the work +product. That is a content rule we can state now even if the handoff is +manual. + +Do not add tools to `get_agent()` whose only job is “file a ticket” unless the +fleet spec asks for it. Scope creep. + +## 5. What we will not do in this plan + +- Copy or paraphrase Hermes `SOUL.md`. +- Move Piper/ALSA/button interrupt into this repo. +- Claim a formatter exists. +- Put measured TTS timings in this tree. +- Add an LLM-as-judge or rewrite node on the voice path. + +## 6. Risks + +| Risk | Mitigation | +|------|------------| +| Prompt ignored; Piper reads a table | Option B later; do not mark A as “enforced” | +| Formatter strips a requested snippet | Opt-in exception when the user asked this turn; keep rules dumb | +| SOUL assumed to cover 008 | Document that the Go loop does not load SOUL | +| Extra LLM rewrite “to be safe” | Forbidden: extra round-trip per turn | +| I/O and brain both grow formatters | Prefer one helper in this package | + +## 7. Success + +- Developers reading this folder know the contract and that it is not a filter. +- If A ships: graph tests or a fixture show a voice system message exists. +- If B ships: unit tests on the formatter, no hardware required. +- I/O still owns stop-on-button and chunked playback. diff --git a/specs/011-voice-reply-contract/spec.md b/specs/011-voice-reply-contract/spec.md new file mode 100644 index 0000000..f956670 --- /dev/null +++ b/specs/011-voice-reply-contract/spec.md @@ -0,0 +1,190 @@ +# Feature Spec: Voice-facing reply contract + +**Feature ID**: 011-voice-reply-contract +**Status**: Specified / not fully enforced in code +**Created**: 2026-08-19 +**Owner**: Derek Clair +**Related**: [008-local-tts-lenovo-go-spike](../008-local-tts-lenovo-go-spike/spec.md), +[`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) +**Parent**: [001-voice-dgx-spark-agent](../001-voice-dgx-spark-agent/spec.md) (T4.2 +voice-aware behaviors) + +## Honest current state + +This is a **contract we want**, not a shipped filter. + +`get_agent()` in this repo produces the text that Piper speaks. The live I/O +loop ([008](../008-local-tts-lenovo-go-spike/spec.md), executed in +[`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent)) +takes the last `AIMessage.content` and synthesizes it. Sentence chunking in that +repo is for time-to-first-audio, not for speakability. + +This package does **not** post-process replies for speakability. There is no +graph node, wrapper, or test that strips markdown, caps length, or blocks +unspeakable formatting. `MemoryChat` (CLI text) has a generic “be concise but +warm” system prompt; the graph used by voice does not. Memory injection is a +`SystemMessage` of profile + recall, not a spoken-UX policy. + +Button interrupt of TTS is an I/O concern (spec 008 / the sibling repo). This +spec is about **what** the model is allowed to emit, not how playback is +cancelled. + +## Overview + +Spoken UX constraints belong in the brain repo even though ALSA, Piper, and +the Teams button live next door. + +When the consumer is a speakerphone, a good reply is short, speakable prose. +Markdown tables, fenced code, heading hashes, and other unspeakable formatting +are a failure of the contract unless the person at the desk asked for them in +that voice session. + +Multi-step coding and research do not belong in the spoken turn. The desk loop +should acknowledge and escalate to the workstation orchestrator / board (Hermes +fleet operating manual, not vendored here). It should not narrate a long plan +or dump a patch through Piper. + +## Goals + +- Define what a voice-facing `get_agent()` reply may contain. +- Keep spoken answers short and listenable by default. +- Forbid unspeakable formatting unless the user asked for it in the voice session. +- Send multi-step coding / research work to the orchestrator / board instead of + doing it in the spoken turn. +- Stay honest: specify the contract without claiming a filter exists in code. + +## Non-goals + +- ALSA, Piper, Parakeet, VAD, LED, USB hotplug, or button interrupt of playback + (spec 008 / I/O repo). +- Inventing or copying TTS latency numbers. Measured stage times, if any, live + in the sibling repo README; they are not this contract. +- Vendoring Hermes voice-profile SOUL or copying `SOUL.md` into this tree. +- Specifying the orchestrator / board / Kanban workflow (stays in Hermes). +- Changing `MemoryChat` / `thelab-chat` text UX, except to note it is a + different consumer. +- Streaming barge-in, Riva, or rewriting spec 001’s long-term voice stack. +- A product “customer service” tone guide. + +## User stories + +1. As the person at the desk, I ask a short question over the speakerphone and + hear a short spoken answer, not a markdown document. +2. As that person, I ask for a multi-file change or a research spike and hear + that it is handed to the board, not a spoken walkthrough of the work. +3. As that person, I can still say “read me that snippet” or “say the table” + and get what I asked for in that turn. +4. As a developer, I know this repo owns the reply *content* contract, and the + I/O repo owns *playback* (chunking, stop-on-button). +5. As a developer, I can tell “prompt guidance we could add now” from “a + deterministic speakability filter we have not built.” + +## Functional requirements + +### FR-1 Default spoken shape + +- Default voice replies are short, speakable prose (a few sentences, one + thought-group). Not an essay, not a blog post, not a README. +- Prefer words Piper can say. Avoid layout that only makes sense on a screen. +- Warm and direct is fine. Padding, recap-the-question, and “as an AI” throat-clearing + are not. + +### FR-2 Unspeakable formatting (opt-in, not default) + +Unless the user **asked for it in this voice session**, do not emit: + +- Markdown tables +- Fenced or indented code dumps +- Heading-hash outlines (`##`, `###`) +- Long bullet forests, numbered runbooks, or checkbox lists meant for a ticket +- Raw JSON / YAML / diff dumps +- Bare URLs or path dumps read aloud as punctuation soup + +If the user did ask (e.g. “read the function”, “say the rows”), the model may +emit that content. The I/O layer still sentence-chunks for playback; that is +not a license to dump an unbounded file. + +### FR-3 Escalate instead of doing the work in the spoken turn + +- Multi-step coding, multi-file edits, and open-ended research are **out of + band** for a voice turn. +- The spoken reply should confirm the ask and say it is going to the + orchestrator / board. It should not start implementing, paste a patch, or + narrate a long investigation. +- What “going to the board” means operationally lives in Hermes, not here. + This spec only forbids doing that work *as the spoken answer*. + +### FR-4 This spec vs I/O + +| Concern | Owner | +|---------|--------| +| What text the model may emit | **this repo** (`get_agent()` output) | +| Sentence-chunked Piper / TTFA | I/O repo | +| Button interrupt of TTS | I/O repo (spec 008) | +| Half-duplex ALSA, STT, LED | I/O repo (spec 008) | +| Prompt / optional formatter that enforces FR-1–FR-3 | this repo (not built; see plan) | +| Voice-profile SOUL | Hermes (not copied here) | + +### FR-5 Honesty in code + +- Until a formatter or voice system message exists, consumers must assume + `AIMessage.content` is unconstrained LLM text. +- Docs and tasks must not mark a speakability filter as done. +- Text CLI (`MemoryChat`) is out of this contract’s enforcement path; do not + pretend a CLI “be concise” line covers the speakerphone. + +## Non-functional requirements + +- No secrets, serials, or household identifiers in this spec or in example + utterances used for the contract. +- Do not bake measured TTS timings into this package. Point at the sibling + README if someone needs hardware numbers. +- Same `get_agent(user_id)` seam as spec 008. Do not fork the graph for “voice + vs text” unless the plan explicitly chooses a voice-only wrapper. +- Optional enforcement (prompt or formatter) must not add an extra LLM + round-trip per turn. Memory injection already skipped summarization for that + reason. + +## Acceptance criteria + +- [ ] Spec reviewed: voice replies are defined as short speakable prose with + unspeakable formatting opt-in, not default. +- [ ] Escalation of multi-step coding/research is written as a requirement, not + a suggestion. +- [ ] Boundary with spec 008 is explicit (content here, interrupt/playback there). +- [ ] Code in this repo still has **no** speakability post-process (honest until + a later task ships one). +- [ ] Prompt-level guidance is identified as possible now; a deterministic + filter is identified as not built (see [tasks.md](./tasks.md)). + +## Seams this package must keep stable + +| Seam | Contract | +|------|----------| +| `get_agent(user_id)` | Compiled graph. Voice I/O invokes this; reply text is last AI content. | +| `graph.invoke({"messages": ...})` | Unchanged call shape from spec 008. | +| Reply string | Today: raw model text. Wanted: FR-1–FR-3. Not filtered. | +| Hermes SOUL | Optional persona for some sessions. Not an API of this package. | + +## Relationship to other specs + +- **001** — long-term desktop voice. T4.2 (“voice-aware behaviors / shorter + responses”) is the historical checkbox; this folder is the actual contract. +- **008** — Lenovo Go I/O spike. Consumes the text this spec governs. Does not + define speakability. +- **004** — checkpointers. Orthogonal; session memory is not reply shape. +- **007** — Spark budget. A speakability filter, if built, stays CPU-cheap + (no second model call). +- Workstation fleet (orchestrator / architect / researcher / coder / reviewer) + is not specified here. + +## Open questions + +- Enforce via prompt on the graph, a thin deterministic formatter, Hermes SOUL, + or some mix? Options are in [plan.md](./plan.md); none is locked. +- How does the spoken turn *signal* escalation (a sentence of ack vs a tool vs + a convention the orchestrator already watches)? Out of scope until fleet + wiring is specified in Hermes. +- Should `get_agent()` always be voice-shaped, given the CLI uses `MemoryChat`? + Probably yes if the only live graph consumer is the speakerphone — confirm + before adding a system message. diff --git a/specs/011-voice-reply-contract/tasks.md b/specs/011-voice-reply-contract/tasks.md new file mode 100644 index 0000000..7a63b27 --- /dev/null +++ b/specs/011-voice-reply-contract/tasks.md @@ -0,0 +1,73 @@ +# Tasks: Voice-facing reply contract (011) + +**Feature**: 011-voice-reply-contract +**Spec**: [spec.md](./spec.md) · **Plan**: [plan.md](./plan.md) +**Status**: Specified / not fully enforced in code + +Checkboxes are honest. Spec-only work can be marked done; enforcement is not. + +## Phase 0 — Specify the contract (this folder) + +- [x] Write `spec.md` (spoken shape, unspeakable formatting, escalate vs + implement, I/O vs brain boundary) +- [x] Write `plan.md` (prompt vs thin formatter vs Hermes SOUL; SOUL not copied) +- [x] Write `tasks.md` (this file) +- [x] Record that this package does **not** post-process replies today + +## Phase 1 — Prompt-level guidance (possible now) + +Not done. Possible now because `get_agent()` is the live voice consumer and +`MemoryChat` is a separate CLI path. No new service, no extra LLM call. + +- [ ] Add a short voice-facing system instruction on the graph (`get_agent()` / + memory injection or a dedicated preamble) +- [ ] Cover: short speakable prose; no tables / code dumps / heading hashes + unless the user asked this turn; escalate multi-step coding/research + rather than doing it in the spoken answer +- [ ] Keep the instruction small so it does not drown memory context +- [ ] Unit test or fixture: compiled graph (or injection helper) includes the + voice instruction — does **not** prove the model obeys it +- [ ] Do not treat this checkbox as “enforced speakability” + +## Phase 2 — Deterministic speakability filter (not built) + +Not built. Do not check these off until a pure helper exists and is wired. + +- [ ] Pure formatter: strip or replace unspeakable markdown (tables, fences, + heading hashes) without a second model call +- [ ] Length / sentence cap with an exception when the user asked for a dump +- [ ] Unit tests on strings only (no ALSA, no Piper, no keys) +- [ ] Wire through this package so I/O can keep a single brain import +- [ ] Decide with a review whether formatter lives inside `get_agent()` or as a + sibling helper the I/O process calls +- [ ] Explicit non-goal until then: claiming Piper is “safe” because of chunking + +## Phase 3 — Escalation seam (Hermes, not this repo) + +Out of band. Listed so it is not silently implemented as a spoken dump. + +- [ ] Spoken ack of “handed to the board” once fleet wiring exists (Hermes + operating manual, not vendored) +- [ ] No `get_agent()` ticket-filing tool unless that fleet spec asks for it +- [ ] Do not copy voice-profile `SOUL.md` into this tree + +## Out of scope (stay unchecked here) + +- [ ] Button interrupt of TTS — I/O repo / spec 008 +- [ ] Sentence-chunked Piper / TTFA — I/O repo +- [ ] Voice barge-in — out of scope for 008 and for this contract +- [ ] Copying sibling-repo latency tables into this package + +## Traceability + +| Want | Code today | +|------|------------| +| Speakable default | Unconstrained `AIMessage.content` | +| Prompt-level guidance | Possible now; **not** in `graph.py` | +| Deterministic filter | **Not built** | +| Button stop of playback | Sibling I/O repo | +| Hermes SOUL | Hermes only | + +Live consume path: [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) +calls `thelab_langchain.agent.graph.get_agent`. Historical 001 checkbox: T4.2 +in [001/tasks.md](../001-voice-dgx-spark-agent/tasks.md). diff --git a/specs/README.md b/specs/README.md index 5a3e00b..052d333 100644 --- a/specs/README.md +++ b/specs/README.md @@ -1,19 +1,23 @@ # Specs -Design and planning for this package. Status in each file is honest: several -are drafts or future work, not a claim that every spec is implemented. +Each numbered folder has **spec.md**, **plan.md**, and **tasks.md**. Status in +the spec is honest: living practice, executed out of tree, designed-not-built, +or partial. | ID | Title | What it is | |----|-------|------------| -| [001](001-voice-dgx-spark-agent/spec.md) | Voice-enabled agent on DGX Spark | Original desktop-voice goal; live I/O now lives in [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) | -| [002](002-multi-user-support/spec.md) | Multi-user support | Per-user `container_tag` / `thread_id` — designed, not a product multi-tenant system | -| [003](003-deployment-infrastructure/spec.md) | Deployment / Docker | Compose + NIM path; gaps called out in the spec | -| [004](004-persistence-checkpointers/spec.md) | Persistence & checkpointers | Planned LangGraph checkpointer; conversation state is still in-memory | -| [005](005-testing-and-cicd/spec.md) | Testing & CI | Direction; a CPU-only pytest + ruff workflow is in `.github/workflows/ci.yml` | -| [006](006-alternative-memory-systems/spec.md) | Alternative memory backends | Future consideration; Supermemory is the current store | -| [007](007-dgx-hardware-optimization/spec.md) | DGX Spark hardware budget | ~30B-class local models; no 120B+ agent loops on one Spark | -| [008](008-local-tts-lenovo-go-spike/spec.md) | Local-tts Lenovo Go spike | Button → VAD → Parakeet → `get_agent()` → Piper. **Executed in** [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent) | +| [001](001-voice-dgx-spark-agent/spec.md) | Voice-enabled agent on DGX Spark | Broader desktop-voice goal (Riva/NIM compose). Not the live path. | +| [002](002-multi-user-support/spec.md) | Multi-user support | Per-user `container_tag` / `thread_id`. Designed; not speaker ID. | +| [003](003-deployment-infrastructure/spec.md) | Deployment / Docker | Experimental compose; healthchecks and GPU limits still open. | +| [004](004-persistence-checkpointers/spec.md) | Persistence & checkpointers | Not wired. Caller (or in-memory process) holds turns. | +| [005](005-testing-and-cicd/spec.md) | Testing & CI | Unit tests + CPU GitHub Actions exist; no coverage gate or image CI. | +| [006](006-alternative-memory-systems/spec.md) | Alternative memory backends | Escape hatch. Supermemory stays default; do not build adapters yet. | +| [007](007-dgx-hardware-optimization/spec.md) | Spark hardware + inference slot | One local LLM; CPU STT/TTS; Grok for quality-critical roles. | +| [008](008-local-tts-lenovo-go-spike/spec.md) | Local-tts Lenovo Go spike | **Executed in** [`conversational-voice-agent`](https://github.com/derekclair/conversational-voice-agent). | +| [009](009-architect-coder-handoff/spec.md) | Architect ↔ coder handoff (STE) | Living practice. Not a library feature; not CI-enforced. | +| [010](010-worker-completion-protocol/spec.md) | Worker complete-or-block | Living practice. This package does not implement a board. | +| [011](011-voice-reply-contract/spec.md) | Voice-facing reply contract | Wanted speakability rules. No filter in code yet. | -Workstation **fleet operations** (orchestrator / architect / researcher / -coder / reviewer, Kanban vs chat) are not specified here. That operating -manual stays in Hermes at `~/.hermes/docs/agentic-workflow.md`. +Hermes **operating manual** (CLI, gateway, profile files) stays at +`~/.hermes/docs/agentic-workflow.md`. Specs 009–010 record the *protocol*, +not that file.