docs: ADR-0014 — unified Profile model + agent registry

Propose a shared substrate for per-user prefs, contexts, per-key
consents, and per-agent state so adding an agent stays a manifest
change. Updates CLAUDE.md, README, and architecture docs to reflect
the multi-agent pipeline (ADR-0013) and the registry direction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 10:19:07 +00:00
parent 41302d9f36
commit d454a0a8bf
7 changed files with 343 additions and 52 deletions

View File

@@ -25,12 +25,37 @@ Session auth
expires_at
revoked_at?
Profile profile
user_id (pk)
timezone
quiet_hours jsonb: [{start,end,days}]
contexts jsonb: [{name,predicate}] introduced in Phase 2
consents jsonb: {integration: {read,write,retain_days}}
User (extended) profile ADR-0014
+ tone 'direct' | 'gentle' | 'motivational'
+ tip_kinds_json jsonb: allowed tip kinds (stable globals)
UserPreference profile ADR-0014
user_id, scope, key (pk)
scope 'orchestrator' | 'agent:<id>'
value_json open-ended; agent validates against its pref_schema on read
source 'user' | 'inferred' (inferred never overwrites user)
updated_at
UserConsent profile ADR-0014
user_id, consent_key (pk)
consent_key 'data:todoist' | 'data:calendar' | 'agent:focus-area' | ...
granted_at
revoked_at? null = currently active
UserContext profile ADR-0014
user_id, name (pk) 'work' | 'home' | 'vacation' | user-named
active manual toggle in M2; auto-inference per agent in #112-#116
schedule_json? optional: when this context is active
created_at
AgentOutput recommender ADR-0013
id (pk)
user_id
agent_id e.g. 'overdue-task' (matches a manifest)
prompt_text snippet for the orchestrator prompt
signals_snapshot jsonb: inputs the agent consumed
computed_at, expires_at computed_at + manifest.ttl_sec
agent_version bump to invalidate cached outputs on logic changes
Credential integrations
user_id
@@ -53,10 +78,10 @@ Event events
TipInstance recommender
tip_id (ulid)
user_id
policy_name "random" | "bandit.linucb" | "remote:v3"
policy_name "v4-orchestrator" (ADR-0013) | legacy bandit names retained for history
policy_version
candidate_source "todoist" | "advice.library" | ...
context_snapshot jsonb: features seen at decision time
candidate_source "todoist" | "advice.library" | "agent-orchestrator" | ...
context_snapshot jsonb: features + agent snippets seen at decision time
tip jsonb: {kind,title,body,source,deep_link,meta}
created_at
shown_at? set when the client reports render

View File

@@ -48,6 +48,8 @@ User reactions (done / snooze / dismiss) are events too. They close the loop as
- **Feast** for feature store when we get there; homegrown adapter until then (Phase 1 seam).
- **MLflow** for model registry and experiment tracking; deployed at `o.alogins.net/mlflow`.
- **Auth.js** embedded behind an OIDC-shaped boundary (ADR-0004). Swap to a standalone OIDC provider when mobile ships.
- **Multi-agent recommendation** (ADR-0013) — pre-compute agents emit prompt snippets, an orchestrator LLM produces the tip. Replaced the ε-greedy bandit (ADR-0007/0012) for explainability, cold-start, and decoupling generation from selection.
- **Registry-driven agents + unified Profile** (ADR-0014) — agents are plugins with declared manifests; per-user prefs, contexts, and per-key consents live in shared tables; auto-inferred parameters share a common framework. Adding an agent is a manifest change.
- **k3s** as the first step beyond docker-compose — no "compose → full k8s" cliff.
## AI stack
@@ -59,30 +61,43 @@ All LLM inference routes through **LiteLLM** (`llm.alogins.net`) backed by **Oll
**OpenWebUI** (`ai.alogins.net`) is the human-facing interface for prompt iteration and model testing during development.
## Decision flow for a new tip (Phase 2 target)
## Decision flow for a new tip (M2, ADR-0013 + ADR-0014)
```
┌────────────────────────────────────────────────┐
│ Pre-compute (every 15 min, per registered agent) │
│ ml/agents/<id> → prompt snippet → agent_outputs │
│ TTL per manifest; agent_version invalidates │
└────────────────────────────────────────────────┘
client ─► gateway ─► recommender (TS)
├─► profile: GET /api/profile
│ (user, prefs, active context, consents)
├─► registry: GET /api/agents/registry
│ (manifests; eligibility filter inputs)
├─► outputs: pull freshest non-expired agent_outputs
│ for eligible agents (consents granted,
│ not silenced by active context, enabled)
ml/serving (Python)
├─► context: ml/features/context.py
(tasks + reactions + time patterns → prompt)
├─► assemble: v4-orchestrator prompt
= global prefs + active context + snippets
├─► generate: LiteLLM → Ollama
│ → N TipCandidates {content, kind, model, prompt_version}
├─► generate: LiteLLM → Ollama → one tip
─► score: bandit policy scores each candidate
├─► shadows: shadow policies log picks without serving
└─► persist: tip_scores {candidate, policy, features, latency}
◄─ best TipCandidate
─► persist: tip_scores {tip, contributing agents,
prompt_version, llm_model, latency}
◄─ tip
```
**Phase 1 (shipped M1):** candidates come from Todoist task list, no LLM. The bandit scores tasks directly.
**Evolution:**
- **Phase 1 (M1):** candidates from Todoist; ε-greedy bandit scored tasks directly (ADR-0007, ADR-0012). Superseded.
- **Phase 2 early (M2):** LLM-generated candidates ranked by bandit. Superseded mid-milestone.
- **Phase 2 current (M2):** multi-agent pipeline (ADR-0013), registry-driven and registry-extensible (ADR-0014). No bandit; the orchestrator LLM reasons over named agent snippets.
**Phase 2 (shipped M2):** LLM candidates are generated in parallel with Todoist fetch. Both pools are merged, scored by the bandit, and the winner served. `tip_scores` tracks `prompt_version`, `llm_model`, and `tip_kind` for every row.
Feedback: `POST /feedback → events.emit(reaction)` → online bandit update + `prompt_version` tracked for A/B analysis.
Feedback: `POST /feedback → events.emit(reaction)`. No online ML reward loop (ADR-0013 §Consequences); reactions are logged in `tip_feedback` for observability and potential future supervised learning.

View File

@@ -26,7 +26,7 @@ User taps "Delete account" in settings → hard confirm → `User.deleted_at` se
## Scope boundaries
Each integration declares the scopes it requests and the features it derives. The `Profile.consents` column is the source of truth; a scope removed from consent short-circuits derived-feature computation at the feature store.
Each integration and each agent declares the consent keys it requires (`data:todoist`, `agent:focus-area`, ...) in its manifest. The `user_consents` table is the source of truth (per-key rows, revocation is a `revoked_at` write — never a delete, so audits stay clean). A revoked consent short-circuits derived-feature computation at the feature store and removes the dependent agent from the orchestrator's eligible set on the next tip. See ADR-0014.
## Audit