From 7554c96d6f1cf5b846cca40c56425649ff42e6b4 Mon Sep 17 00:00:00 2001 From: Marianne <203729272+thrialectics@users.noreply.github.com> Date: Thu, 14 May 2026 12:18:43 -0400 Subject: [PATCH] Kass/claudemd staleness (#680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: refresh CLAUDE.md to match current architecture Bring the project CLAUDE.md in line with the codebase as of main: - API prefix /v1 -> /v3; add Conclusions + Webhooks to route listing; note that Collections/Documents are now partially exposed via the Conclusions API - New Runtime Architecture section describing the API server / deriver worker split and the in-process Reconciler scheduler - Rewrite Agent Architecture: * Deriver - "minimal" single-LLM-call architecture (no agentic tool loop); current entry point and prompts * Dialectic - actual DIALECTIC_TOOLS list, 5 reasoning tiers, DIALECTIC_TOOLS_MINIMAL for the minimal level * Dreamer - orchestrator + DeductionSpecialist + InductionSpecialist, surprisal-based prioritization, reasoning trees * Summarizer documented as a distinct agent - Refresh project structure tree: add cache/, llm/ (+backends/), reconciler/, telemetry/, vector_store/; correct schemas/ (now a directory); drop nonexistent dialectic/agent/, deriver/agent/, dreamer/agent.py + dreamer.py - Architectural decisions: fill in the missing #2 (Peer Paradigm); add hybrid search (FTS + vector), pluggable external vector stores, composite-FK multi-tenancy, dialectic reasoning tiers * docs(CLAUDE.md): rename "observations" -> "conclusions" in agent prose Per Plastic's positioning, "conclusions" is the documentation-facing term for what the Deriver produces; "observations" remains the internal code-symbol vocabulary (`create_observations`, `get_observation_context`, etc.). Swap conceptual prose, preserve all backticked code references. - New terminology callout at the top of Agent Architecture so the mapping is explicit for coding agents reading this file - Deriver/Dreamer prose: observations -> conclusions for the abstract noun; specialist tool lists keep their `get_recent_observations`, `create_observations_deductive`, etc. unchanged - Reasoning-trees bullet rephrased to "each conclusion links to its premises and downstream conclusions" (the original "observations link to premises/conclusions" becomes recursive after the swap; rephrasing makes intent clearer) - Tree comment for surprisal.py updated to prose-style "conclusion prioritization" Companion to a3fa16ef on kass/readme-refresh, which made the same swap in the README's Conclusions definition. * docs(CLAUDE.md): align "Dialectic API" with README's "Chat Endpoint" rename Two spots framed the public surface as "Dialectic API" — preserve the code-agent name (Dialectic) while matching the documentation-facing "Chat Endpoint" we standardized on in the README. * chore: nits in CLAUDE.md --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> --- CLAUDE.md | 242 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 135 insertions(+), 107 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7ace3529..6fa9ce2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Honcho is an infrastructure layer for building AI agents with memory and social - Imbuing agents with a sense of identity - Personalizing user experiences through understanding user psychology -- Providing a Dialectic API that injects personal context just-in-time +- Providing a Chat Endpoint (the Dialectic agent) that injects personal context just-in-time - Supporting development of LLM-powered applications that adapt to end users - Enabling multi-peer sessions where multiple participants (users or agents) can interact @@ -32,23 +32,25 @@ Honcho uses a peer-based model where both users and agents are represented as "p - **Peer** (formerly User): Any participant in the system (human or AI) - **Session**: A conversation context that can involve multiple peers - **Message**: Data units that can represent communication between peers OR arbitrary data ingested by a peer to enhance its global representation -- **Collections & Documents**: Internal vector storage for peer representations (not exposed via API) +- **Collections & Documents**: Internal vector storage for peer representations. Collections are keyed by `(observer, observed)` peer pairs. Collections/Documents are not directly exposed via API, but the observations stored within them are exposed as **Conclusions** (see `/v3/.../conclusions` endpoints). ## Architecture Overview ### API Structure -All API routes follow the pattern: `/v1/{resource}/{id}/{action}` +All API routes follow the pattern: `/v3/{resource}/{id}/{action}`. Most "list/search" endpoints are `POST` so they can accept rich filter bodies. - **Workspaces**: Create, list, update, search - **Peers**: Create, list, update, chat (dialectic), messages, representation - **Sessions**: Create, list, update, delete, clone, manage peers, get context -- **Messages**: Create (batch up to 100), list, get, update +- **Messages**: Create (batch up to 100), upload (file), list, get, update +- **Conclusions**: Create, list, query (semantic search), delete — the API-facing name for observations stored in `(observer, observed)` collections - **Keys**: Create scoped JWTs +- **Webhooks**: Register endpoint, list, delete, test ### Key Features -#### Dialectic API (`/peers/{peer_id}/chat`) +#### Chat Endpoint (Dialectic agent) (`/peers/{peer_id}/chat`) - Provides bespoke responses informed by the representation - Integrates long-term facts from vector storage @@ -115,127 +117,147 @@ cd sdks/typescript && bun run tsc --noEmit - Docstrings: Use Google style docstrings - **Never hold a DB session during external calls** (LLM, embedding, HTTP). If a function needs both a DB session and an external call result, compute the external result first and pass it as a parameter. This avoids tying up DB connections during slow network I/O. Use `tracked_db` for short-lived, DB-only operations; pass a shared session when multiple DB-only calls can reuse one connection. +### Runtime Architecture + +Honcho runs as two cooperating processes that share a Postgres database and Redis cache: + +- **API server** (`uv run fastapi dev src/main.py`) — handles HTTP, enqueues background work, returns immediately. Hosts the **Dialectic** agent inline (synchronous tool loop during chat requests). +- **Deriver worker** (`uv run python -m src.deriver`) — long-running queue consumer (uvloop). Runs the **Deriver**, **Summarizer**, and **Dreamer** off the queue. Can run multiple instances (`DERIVER_WORKERS`). Also hosts an in-process **Reconciler scheduler** (`src/reconciler/`) that periodically embeds messages with `sync_state='pending'` in `MessageEmbedding` and cleans up stale queue items — embedding generation is decoupled from message creation by design. + ### Agent Architecture -Honcho uses three specialized LLM agents that work together to form memories and answer queries: +Honcho uses several specialized LLM agents. They share tool definitions and the LLM client abstraction in `src/utils/agent_tools.py` + `src/llm/`. -#### 1. Deriver Agent (`src/deriver/agent/`) +> **Terminology:** what users see as **conclusions** (the public API surface and the term we use in documentation) is called **observations** in code symbols — `create_observations`, `delete_observations`, `get_observation_context`, etc. Doc prose below uses "conclusions"; references to actual code symbols stay as "observations." -**Role**: Memory formation through content ingestion +#### 1. Deriver (`src/deriver/`) -The Deriver processes incoming messages and extracts observations about peers. +**Role**: Memory formation through content ingestion. -- **Trigger**: Messages created via API are enqueued for background processing -- **Tools**: `create_observations`, `update_peer_card`, `get_recent_history`, `search_memory`, `get_observation_context`, `search_messages` -- **Output**: Explicit observations (direct facts) and deductive observations (inferences) -- **Entry point**: `src/deriver/agent/worker.py` → `Agent.run_loop()` +The Deriver processes batches of incoming messages and extracts conclusions about peers. The current architecture is "minimal deriver" — a **single LLM call** per batch using structured output, not an agentic tool loop. This trades flexibility for cost and predictability. -#### 2. Dialectic Agent (`src/dialectic/agent/`) +- **Trigger**: Messages enqueued by `src/deriver/enqueue.py` on message create; consumed by `src/deriver/queue_manager.py` → `consumer.process_item()` → `deriver.process_representation_tasks_batch()`. +- **Output**: Explicit conclusions (direct facts) and deductive conclusions (inferences) saved to `(observer, observed)` collections. +- **Entry point**: `src/deriver/__main__.py` → `queue_manager.main()`. +- **Prompts**: `src/deriver/prompts.py` (`minimal_deriver_prompt`). -**Role**: Analysis and recall for answering queries +#### 2. Dialectic (`src/dialectic/`) -The Dialectic answers questions about peers by strategically gathering context from memory. +**Role**: Analysis and recall for answering queries. -- **Trigger**: API call to `/peers/{peer_id}/chat` with `agentic=true` -- **Tools**: `search_memory`, `get_recent_history`, `get_observation_context`, `search_messages`, `get_recent_observations`, `get_most_derived_observations`, `get_session_summary`, `get_peer_card`, `create_observations` (deductive only) -- **Output**: Natural language response grounded in gathered context -- **Entry point**: `src/dialectic/chat.py` → `agentic_chat()` → `DialecticAgent.answer()` +The Dialectic answers questions about peers by strategically gathering context from memory. It is the only tool-using agent on the synchronous request path — it loops over `DIALECTIC_TOOLS` until it has enough context to answer. (The Dreamer specialists also use tools, but run off the queue.) -#### 3. Dreamer Agent (`src/dreamer/agent.py`) +- **Trigger**: API call to `POST /v3/.../peers/{peer_id}/chat`. +- **Tools** (see `DIALECTIC_TOOLS` in `src/utils/agent_tools.py`): `search_memory`, `search_messages`, `get_observation_context`, `grep_messages`, `get_messages_by_date_range`, `search_messages_temporal`, `get_reasoning_chain`. At the `minimal` reasoning level, a reduced set (`DIALECTIC_TOOLS_MINIMAL`) is used: just `search_memory` + `search_messages`. +- **Reasoning levels**: 5 tiers — `minimal`, `low`, `medium`, `high`, `max` — each with its own model config (see `DialecticLevelSettings` in `src/config.py`). +- **Output**: Natural language response grounded in gathered context. Supports SSE streaming. +- **Entry point**: `src/dialectic/chat.py` → `agentic_chat()` / `agentic_chat_stream()` → `DialecticAgent` (in `src/dialectic/core.py`). -**Role**: Consolidation and self-improvement of memory +#### 3. Dreamer (`src/dreamer/`) -The Dreamer explores and consolidates observations to improve memory quality. +**Role**: Consolidation and self-improvement of memory. -- **Trigger**: Scheduled or explicit dream task via queue -- **Tools**: `get_recent_observations`, `get_most_derived_observations`, `search_memory`, `create_observations`, `delete_observations`, `update_peer_card` -- **Strategy**: Random walk exploration - start from recent/high-value observations, search for related content, consolidate redundancies -- **Output**: Consolidated observations, deleted redundancies -- **Entry point**: `src/dreamer/agent.py` → `DreamerAgent.consolidate()` +The Dreamer is an orchestrated multi-specialist system that runs during scheduled "dreams" to consolidate conclusions and build reasoning trees. + +- **Trigger**: Scheduled via `DreamScheduler` (`src/dreamer/dream_scheduler.py`) or explicit dream task on the queue. +- **Strategy**: Surprisal-based prioritization (`src/dreamer/surprisal.py`) selects which conclusions to expand. The orchestrator (`orchestrator.run_dream`) runs two specialist phases: + 1. **DeductionSpecialist** (`specialists.py`) — produces deductive conclusions from explicit conclusions. Tools: `get_recent_observations`, `search_memory`, `search_messages`, `create_observations_deductive`, `delete_observations`, `update_peer_card`. + 2. **InductionSpecialist** — produces inductive conclusions from explicit + deductive conclusions. Tools: same discovery set + `create_observations_inductive`, `update_peer_card`. +- **Reasoning trees** (`src/dreamer/trees/`, migration `f1a2b3c4d5e6_add_reasoning_tree_columns`): each conclusion links to its premises and downstream conclusions, enabling `get_reasoning_chain` traversal at recall time. +- **Output**: Deductive/inductive conclusions, consolidated redundancies, updated peer cards. +- **Entry point**: `src/dreamer/orchestrator.py` → `process_dream()` (the package-level export from `src/dreamer/__init__.py`), which wraps `run_dream()`. + +#### 4. Summarizer (`src/utils/summarizer.py`) + +**Role**: Two-tier session summarization (direct LLM call — no agentic tools). + +- **Trigger**: Runs as part of the queue pipeline alongside representation tasks. +- **Tiers**: short summary every `SUMMARY_MESSAGES_PER_SHORT_SUMMARY` messages (default 20); long summary every `SUMMARY_MESSAGES_PER_LONG_SUMMARY` (default 60). Token caps configurable via `SUMMARY_MAX_TOKENS_SHORT` / `SUMMARY_MAX_TOKENS_LONG`. #### Shared Agent Infrastructure -All agents share common infrastructure in `src/utils/agent_tools.py`: - -- **Tool definitions**: Unified tool schemas used by all agents -- **Tool executor**: `create_tool_executor()` factory creates context-aware executors -- **LLM client**: `honcho_llm_call()` handles tool calling loops with configurable iterations +- **Tool definitions** (`src/utils/agent_tools.py`): unified `TOOLS` dict; per-agent lists (`DIALECTIC_TOOLS`, `DIALECTIC_TOOLS_MINIMAL`, `DREAMER_TOOLS`, `DEDUCTION_SPECIALIST_TOOLS`, `INDUCTION_SPECIALIST_TOOLS`). +- **LLM subsystem** (`src/llm/`): provider-agnostic `honcho_llm_call()`. Backends in `src/llm/backends/` (`anthropic.py`, `gemini.py`, `openai.py`). Includes prompt caching (`caching.py`), structured output (`structured_output.py`), tool loop (`tool_loop.py`), history adapters for cross-provider message formats, and a model registry. +- **Per-agent model config**: each agent has its own `MODEL_CONFIG` in `src/config.py` with fallback chains (see `ConfiguredModelSettings`, `FallbackModelSettings`). ### Project Structure ``` src/ -├── main.py # FastAPI app setup with middleware and exception handlers -├── models.py # SQLAlchemy ORM models with proper type annotations -├── schemas.py # Pydantic validation schemas for API -├── config.py # Configuration management -├── db.py # Database connection and session management -├── dependencies.py # Dependency injection (DB sessions) -├── exceptions.py # Custom exception types +├── main.py # FastAPI app: middleware, routers, lifespan, exception handlers +├── models.py # SQLAlchemy ORM models (Workspace/Peer/Session/Message/ +│ # MessageEmbedding/Collection/Document/QueueItem/...) +├── config.py # Pydantic-settings configuration (very large; see README) +├── db.py # Engine + session/context management (request_context var) +├── dependencies.py # FastAPI DI (tracked_db, etc.) +├── exceptions.py # Custom exception types (HonchoException + subclasses) ├── security.py # JWT authentication -├── embedding_client.py # Embedding service client -├── crud/ # Database operations -│ ├── __init__.py -│ ├── collection.py # Collection CRUD operations -│ ├── deriver.py # Deriver-related CRUD operations -│ ├── document.py # Document CRUD operations -│ ├── message.py # Message CRUD operations -│ ├── peer.py # Peer CRUD operations -│ ├── peer_card.py # Peer Card CRUD operations -│ ├── representation.py # RepresentationManager and representation operations -│ ├── session.py # Session CRUD operations -│ ├── webhook.py # Webhook CRUD operations -│ └── workspace.py # Workspace CRUD operations -├── dialectic/ # Dialectic API implementation -│ ├── __init__.py -│ ├── chat.py # Chat functionality (standard + agentic) -│ ├── prompts.py # Prompt templates -│ └── agent/ # Agentic dialectic implementation -│ ├── __init__.py -│ ├── core.py # DialecticAgent class -│ └── prompts.py # Agent system prompts -├── routers/ # API endpoints -│ ├── workspaces.py -│ ├── peers.py -│ ├── sessions.py -│ ├── messages.py -│ ├── keys.py -│ └── webhooks.py # Webhook endpoints -├── deriver/ # Background processing system -│ ├── __init__.py -│ ├── __main__.py # Deriver entry point -│ ├── consumer.py # Message consumer -│ ├── enqueue.py # Queue operations -│ ├── queue_manager.py # Queue management -│ └── agent/ # Agentic deriver implementation -│ ├── __init__.py -│ ├── core.py # Agent class -│ ├── worker.py # Task processing -│ └── prompts.py # Agent system prompts -├── dreamer/ # Memory consolidation system -│ ├── __init__.py -│ ├── agent.py # DreamerAgent class + process_agent_dream -│ └── dreamer.py # Legacy dreamer (scheduled) -├── utils/ # Utilities -│ ├── __init__.py -│ ├── agent_tools.py # Shared agent tools and executor -│ ├── clients.py # LLM client abstraction -│ ├── files.py # File handling utilities -│ ├── filter.py # Query filtering utilities -│ ├── formatting.py # Message formatting utilities -│ ├── logging.py # Logging and metrics (Rich console output) -│ ├── search.py # Search functionality -│ ├── shared_models.py # Shared data models -│ ├── summarizer.py # Session summarization -│ └── types.py # Type definitions -└── webhooks/ # Webhook system - ├── events.py # Webhook event definitions - ├── webhook_delivery.py # Webhook delivery logic - └── README.md # Webhook documentation +├── embedding_client.py # Embedding provider client +├── schemas/ # Pydantic schemas +│ ├── api.py # Public API request/response schemas +│ ├── configuration.py # Per-resource configuration schemas +│ └── internal.py # Internal-only schemas (queue payloads, etc.) +├── crud/ # Per-resource DB operations +│ ├── collection.py, deriver.py, document.py, message.py +│ ├── peer.py, peer_card.py, representation.py (RepresentationManager) +│ ├── session.py, webhook.py, workspace.py +├── routers/ # FastAPI route handlers (all under /v3) +│ ├── workspaces.py, peers.py (dialectic /chat lives here), sessions.py +│ ├── messages.py, conclusions.py, keys.py, webhooks.py +├── dialectic/ # Dialectic agent — runs inline per chat request +│ ├── chat.py # agentic_chat() / agentic_chat_stream() +│ ├── core.py # DialecticAgent (the tool-loop driver) +│ └── prompts.py +├── deriver/ # Background queue consumer (separate process) +│ ├── __main__.py # `python -m src.deriver` entry point +│ ├── queue_manager.py # QueueManager + main() loop +│ ├── consumer.py # process_item dispatcher (representation / deletion / reconciler) +│ ├── deriver.py # "minimal deriver" — single-LLM-call batch processor +│ ├── enqueue.py # API → queue producer +│ └── prompts.py +├── dreamer/ # Memory consolidation (runs off the queue) +│ ├── orchestrator.py # run_dream() / process_dream() +│ ├── specialists.py # DeductionSpecialist + InductionSpecialist +│ ├── dream_scheduler.py +│ ├── surprisal.py # Surprisal-based conclusion prioritization +│ └── trees/ # Reasoning-tree primitives +├── reconciler/ # In-process scheduler hosted by the deriver worker +│ ├── scheduler.py # ReconcilerScheduler (started from queue_manager.py) +│ ├── sync_vectors.py # Embeds MessageEmbedding rows with sync_state='pending' +│ └── queue_cleanup.py # Removes stale queue items +├── llm/ # Provider-agnostic LLM client subsystem +│ ├── api.py, backend.py, executor.py, runtime.py, registry.py +│ ├── caching.py, structured_output.py, tool_loop.py, conversation.py +│ ├── history_adapters.py, request_builder.py, credentials.py, types.py +│ └── backends/ # anthropic.py, gemini.py, openai.py +├── cache/ # Redis cache abstraction (cashews-backed) +│ └── client.py +├── vector_store/ # Optional external vector stores (pgvector is default, +│ │ # implemented via MessageEmbedding/Document in models+crud) +│ ├── lancedb.py +│ └── turbopuffer.py +├── telemetry/ # Observability +│ ├── emitter.py # CloudEvents emitter +│ ├── logging.py # Logging helpers + route-template extraction +│ ├── metrics_collector.py, reasoning_traces.py, sentry.py +│ ├── events/ # Event type definitions +│ └── prometheus/ # Prometheus metric definitions +├── utils/ # Cross-cutting utilities +│ ├── agent_tools.py # Tool definitions + per-agent tool lists +│ ├── summarizer.py # Two-tier session summarizer +│ ├── representation.py # Representation formatting (distinct from crud/representation.py) +│ ├── search.py, filter.py, formatting.py +│ ├── tokens.py # tiktoken-based counting +│ ├── work_unit.py, queue_payload.py +│ ├── config_helpers.py, json_parser.py, files.py +│ └── types.py +└── webhooks/ # Webhook delivery + ├── events.py + └── webhook_delivery.py ``` -- Tests in pytest with fixtures in tests/conftest.py -- Use environment variables via python-dotenv (.env) +- Tests in pytest with fixtures in tests/conftest.py; subdirs mirror src/ (`tests/deriver/`, `tests/dialectic/`, etc.) plus `tests/bench/` (perf benchmarks), `tests/integration/`, `tests/live_llm/` (gated by `--live-llm`), and `tests/unified/` (the unified runner). +- Use environment variables via python-dotenv (.env). Config precedence: env > .env > config.toml > defaults. ### Database Design @@ -248,12 +270,18 @@ src/ ### Key Architectural Decisions -1. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings -3. **Background Processing**: Async queue system for expensive operations -4. **Provider Abstraction**: Model client supports multiple LLM providers -5. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level -6. **Batch Operations**: Support for bulk message creation (up to 100 messages) -7. **Session History**: Two-tier summarization (short every 20 messages, long every 60) +1. **Peer Paradigm**: humans and AI agents are unified as "Peers"; many-to-many with Sessions. Internal vector storage (Collections/Documents) is keyed by `(observer, observed)` peer pairs — the same mechanism powers self-representation (`observer == observed`) and cross-peer modeling. +2. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings. +3. **API server / worker split**: API enqueues, deriver worker process consumes. Never block HTTP on LLM work. The Reconciler runs as an in-process scheduler inside the deriver, handling async embedding sync and queue cleanup. +4. **"Minimal" deriver**: memory formation is a single structured-output LLM call per batch, not an agentic tool loop. Predictable cost, lower latency. The Dialectic is the one true tool-using agent. +5. **Provider-agnostic LLM layer** (`src/llm/`): all model calls go through `honcho_llm_call()`. Backends (`anthropic`, `gemini`, `openai`) sit behind a registry; per-agent `MODEL_CONFIG` with fallback chains is resolved at call time. +6. **Dialectic reasoning tiers**: 5 levels (`minimal` → `max`); each level has its own model config and tool set (`minimal` uses a reduced toolset). +7. **Hybrid search**: Postgres FTS (GIN index on `to_tsvector('english', content)`) + vector similarity (HNSW on `MessageEmbedding.embedding`). `MessageEmbedding` is a separate table from `Message` with its own `sync_state` so embedding is decoupled from message creation. +8. **Pluggable external vector stores**: defaults to pgvector inline; can swap to turbopuffer or lancedb (`VECTOR_STORE_*` config; `src/vector_store/`). +9. **Composite-FK multi-tenancy**: `workspace_name` participates in nearly every composite FK. Cross-workspace data leakage is structurally impossible at the schema level. +10. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level. +11. **Batch Operations**: Bulk message creation up to 100 messages per request. +12. **Session History**: Two-tier summarization — short every `SUMMARY_MESSAGES_PER_SHORT_SUMMARY` (default 20), long every `SUMMARY_MESSAGES_PER_LONG_SUMMARY` (default 60). ### Error Handling