pip install exiting 0 is not evidence the venv is usable (D-24) - the
2026-08-09 core-api incident was exactly this shape: a venv that
"installed fine" but was missing sqlalchemy, surfacing as 11 collection
errors that read like broken imports rather than an environment
problem.
setup now ends with `pytest --collect-only`, scoped like `make test`
(excludes e2e/integration/contracts) and run with --no-cov. Collection
imports every test module without running the suite, so a missing or
mismatched dependency fails setup itself instead of showing up later
as a confusing test failure.
Workspace T-47.
Ships the Steward capability-extraction fix (a905363), which has been on
main since earlier today while production continued to route on prose:
the running v2.4.2 still matches capability domains as substrings across
the Steward's whole response, so "description" selects housekeeper and
"acknowledge" selects librarian and biographer.
Patch rather than minor: no new capability, and the JSON on the wire is
unchanged. What changes is which agents get invoked, and only in the
cases that were already wrong.
Also carries the routing benchmark, its fixtures, the shared GPU
residency guard and the findings document, none of which are
user-visible.
Co-Authored-By: Claude <noreply@anthropic.com>
The prompt tells the Steward to state its choice on a DELEGATE line and
to explain itself on REASON, COMPLEXITY and CONTEXT lines. Extraction
ignored that structure and substring-matched capability domains across
the entire response, so ordinary English in the explanation selected
agents: "description" contains the housekeeper domain "script",
"discover" contains "cover", "acknowledge" contains "knowledge" and
"know", "economy" contains the biographer domain "my".
Every one of those was a real delegation. A spurious librarian is a
multi-second web call on a query that asked for arithmetic.
It also made prose length a routing input, which would have quietly
corrupted the thinking benchmark this was found during: anything that
shortened the Steward's output reduces accidental substring hits and so
reads as improved routing.
Resolution is now layered, most explicit first — a DELEGATE line opening
with a capability name, then a capability named anywhere on that line,
then a domain on that line. With no DELEGATE line at all the response is
matched on capability names only, never domains, so the conversational
path still answers with no capabilities. Matching is whole-word
throughout.
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes the v2.4.1 crash-loop: fresh image builds resolved
opentelemetry-api 1.44.0, which removed the private _events module
that pydantic-ai 1.27 imports at startup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the container-name network-defaults change from [Unreleased] into
the 2.4.1 section and bump pyproject.toml. Patch release: the change
corrects service-host defaults (SEARXNG_HOST, LIBRARY_DESK_HOST,
CORE_API_HOST) for the docker-dataplane deployment, including the
wrong CORE_API_HOST port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The homelab is retiring *.schweitz.internal and will rebind host
ports to loopback; container-to-container traffic must use container
names on docker-dataplane.
- SEARXNG_HOST: http://localhost:8087 -> http://searxng:8080
(SearXNG's internal port is 8080; 8087 was the host-published port)
- LIBRARY_DESK_HOST: http://localhost:8089 -> http://library-desk:8089
- CORE_API_HOST: http://localhost:8090 -> http://core-api:8083
(8090 is the Scheduler's host port; Core-API serves 8083 internally,
confirmed by the housekeeper client and test suite hitting :8083)
- scripts/test_housekeeper.sh: reach Core-API via localhost:8083
instead of the LAN IP, which will refuse after loopback rebinding
Local development against host-published ports keeps working via .env
overrides (.env.example unchanged; localhost stays valid on the host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-coordination-removal sweep: the coordination wire protocol
(AgentRequest, AgentResponse, DelegationIntent, CoordinationResult,
DelegationReason, TaskComplexity, ToolCallRecord, AgentTimeoutError,
AgentUnavailableError, DelegationError) had zero importers left in
src/ - only its own test module. AgentError stays (raised by
run_librarian, mapped to user-safe failures by delegation.py).
Also drops the stale coordination.py line from the README tree.
Import-cycle sanity: python -c 'import src.main' passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
Phase A review minors:
- Coverage note: source_status (when present) is now used exclusively;
the source_counts-absence fallback only considers the optional legs
the request explicitly enabled (web/documents/volatile). library-desk
computes source_counts from the final top-N fused results only, so
absence of the always-on vector/graph legs is normal ranking behavior
- the old heuristic warned on virtually every healthy search
- update_wiki_page: the empty-list tags sentinel (leave unchanged) made
clearing all tags impossible; pass exactly ["__CLEAR__"] to send an
empty tag list, documented in the docstring for the local model
- Text-delegation parallel fallback: zip(..., strict=True) with an
explicit count-mismatch guard so results can never be silently
attributed to the wrong agent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
One delegation implementation remains (src/agents/delegation.py).
Removed, after verifying zero live importers post-Phase-A/B:
- src/agents/coordination.py: CoordinationEngine, duplicate
delegate_to_librarian, AGENT_EXECUTORS/AGENT_STREAM_EXECUTORS
(only importer was its own test module)
- run_librarian_stream: documented-broken path (Ollama streaming +
tool call bug, PydanticAI #1292/#2256), only called by the deleted
coordination engine
- stream_delegate_to_* wrappers + STREAMING_DELEGATION_WRAPPERS and
the never-parsed __DELEGATION_RESULT__ marker in delegation.py
- HouseholdRegistry.get_streaming_delegation_tools() (no callers)
- tests/agents/test_coordination.py and the wrapper/stream tests
Note: the STREAMING_DELEGATION_WRAPPERS import in
src/responses/streaming.py was already removed by Phase A (7ce1c1a);
nothing to delete there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
Session-scoped autouse guard in tests/conftest.py refuses to run any
test (pytest.exit, returncode 1) when the effective tenant resolves
to the production tenant jpmschweitzer - the same guard library-desk
applies on its side. _initialize_app now depends on the guard so the
refusal happens before any initialization.
Suite-level assertions pin that the live session runs under the
llm_tester namespaces: Qdrant memories_llm_tester collection and
Redis session:llm_tester:* keys. The biographer/memory unit tests
already run fully mocked (no shared-service writes); the e2e
isolation tests already used llm_tester - their constants now derive
from the shared TEST_TENANT/PRODUCTION_TENANT config constants so a
drift fails loudly instead of silently splitting.
Verified: ENVIRONMENT=production pytest run exits 1 with the TENANT
GUARD message and zero tests executed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Library-desk is removing its server-side default user, so a request
without an explicit tenant will 422 after its next deploy:
- New client-level _resolve_user() resolves the tenant (explicit arg
or request context) and raises ValueError on an empty/whitespace
value BEFORE any bytes hit the wire; all 15 tenant-scoped methods
use it
- extract_content / extract_content_batch now accept and send the
user (query param), matching the rest of the API surface
- search_web no longer falls back to a phantom "tatlock-librarian"
tenant; it sends the resolved user
- health_check stays user-less (public, not tenant-scoped)
Tests: parametrized sweep pins the wire contract (user present in
params or payload) for every tenant-scoped method, for both context
and explicit users; empty-tenant calls are asserted to fail without
any HTTP call; the recorded-fixture hybrid contract test now pins
user as an explicit query param.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Non-production environments (development/testing) now force the
effective tenant to the reserved test tenant "llm_tester" (or a
test_-prefixed override) regardless of DEFAULT_USER misconfiguration:
- Config.effective_default_user only honors DEFAULT_USER outside
production when it is llm_tester or test_-prefixed; anything else
is forced to llm_tester (tenant_forced flags the override)
- Config refuses startup (validation error) when a non-production
environment is explicitly configured with the production tenant
jpmschweitzer
- get_user() applies the same guard at request-context resolution,
so an explicit request for the production tenant in dev/test is
forced to llm_tester with a warning log
- initialize_application() emits one loud startup log line
(tenant_guard_active / tenant_guard_production) stating the
effective tenant
Unit tests cover the dev/test/prod x default/explicit-user matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- delegate_to_* now receives a trimmed conversation history (last ~6
turns, 500 chars/turn) as context on both live direct-delegation
paths (streaming and steward non-streaming), via new
build_delegation_context helper
- _stream_direct_delegation restructured as an async generator: the
butler 'start' think message streams BEFORE the expert runs and the
success/error message right after it finishes, instead of all
messages arriving after the research completed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ollama's OpenAI-compatible API mishandles anyOf[X, null] parameter
schemas. update_wiki_page (content/title/tags/description) and
smart_create_wiki_page (path) now use empty-string/empty-list
sentinels translated to None inside the tool, following the
biographer pattern from 9d7ce39.
Adds a snapshot test that walks every registered librarian tool's
emitted JSON schema and fails on any anyOf[..., null].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 2-attempt short-backoff retry for GETs and the read-only
POST /query/* and /rag/search endpoints only; wiki writes are never
retried (duplicate-page risk)
- honor the defined-but-ignored LIBRARY_DESK_TIMEOUT config instead of
hardcoded 60s/30s per-call values
- hold ONE shared httpx.AsyncClient per librarian run via
library_client_session (contextvar), instead of constructing a
client per tool call; nested sessions are no-ops and custom targets
still get their own client
- read tools raise ModelRetry on transient HTTP errors (transport
errors, 5xx, 429) so Agent(retries=2) engages; write tools keep
returning safe failure messages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- add LIBRARIAN_TIMEOUT config (default 180s) and enforce it with
asyncio.wait_for inside delegate_to_librarian, covering the live
paths (steward direct delegation and SSE streaming) that had no cap
- timeouts fail honestly: success=False with a curated butler sentence,
detail in logs
- set an explicit timeout on TatlockOllamaProvider's AsyncOpenAI client
from OLLAMA_TIMEOUT instead of the SDK default (~600s per LLM call)
- remove the contradictory unused 60s default from
AgentRequest.timeout_seconds; coordination falls back to the
configured budget
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- parse source_counts into HybridRAGResponse and additively parse the
shared-contract source_status/degraded fields when present (absence
tolerated, so deploy order between tatlock and library-desk never
matters)
- hybrid_search appends a one-line coverage note when a leg reported
'failed' (or degraded is set), falling back to inferring silent legs
from source_counts on older library-desk versions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- run_librarian / run_librarian_stream raise AgentError instead of
returning/yielding error text as normal output; detail stays in logs
- delegate_to_* wrappers now put a curated butler-toned sentence in
DelegationResult.output on failure and never expose str(e), so
streaming's error branch is reachable and honest
- _execute_single_delegation propagates success; direct delegation only
records delegate_to_* as called when the expert actually succeeded
- librarian tools return user-safe messages instead of
'Error searching: {e}' strings that leaked internal URLs into
synthesis; coordination stream errors are curated as well
- ruff cleanups (TYPE_CHECKING forward refs, B904, unused locals) in
the touched files to keep them lint-clean
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client parsed field names the live library-desk service never
returns, so every result rendered as "unknown (score: 0.00)":
- source_type/sources -> source + sources (icons key off sources values)
- rrf_score -> score
- context -> formatted_context
- related_dossiers are per-result; top level aggregates unique titles
- synonyms live inside the keywords dict as a {term: [synonyms]} map
Also stop sending zero limits (service 422s on limit < 1); disabled
legs now rely on the enable_* flags with limits clamped to >= 1.
Adds a recorded live response as a fixture plus contract tests that
pin the mapping (non-unknown sources, non-zero scores, icon coverage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gemma4:e2b has native function calling with dedicated tool tokens,
achieving 100% tool selection accuracy in benchmarks vs 67% for
mistral-nemo-large, with 5-8x faster response times (2-4s vs 15-20s)
and lower VRAM usage (8GB vs 9.2GB).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
exclude_none was too aggressive — it stripped finish_reason: null from
intermediate chunks (which OpenAI includes). exclude_unset correctly
omits only fields never passed to the constructor (like reasoning_content
on content-only chunks) while preserving explicitly-set finish_reason: null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OpenAI's API omits null fields in streaming chunks, but Tatlock was
including them (content: null, reasoning_content: null). This caused
parsing issues in Open WebUI's streaming handler.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
sse_starlette's EventSourceResponse added \r\n line endings that
Open WebUI couldn't parse. Switched to plain StreamingResponse with
manual SSE formatting matching OpenAI's exact format.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Steward's internal routing analysis (DELEGATE, COMPLEXITY, etc.)
was being exposed in <think> blocks. This is implementation detail,
not useful reasoning for the user.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PydanticAI handles tool_choice natively for Anthropic. The extra_body
hack caused an infinite tool call loop where Claude kept calling the
same tool because tool_choice was forced to "any".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All agents now prefer Claude API when ANTHROPIC_API_KEY is configured,
with automatic fallback to Ollama when offline or unconfigured. New
src/anthropic/ module provides model selection via get_model() factory.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Paperless document search to HybridRAG pipeline
- Add volatile cache (weather, forecast, news, stocks) to HybridRAG
- Add include_documents and include_volatile params to hybrid_search
- Add 📑 and ⚡ icons for document/volatile sources
- Update Librarian prompt with new data source awareness
- Fix Biographer routing: personal memory queries now route correctly
- Add location keywords to Steward pre-fetch logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Strengthened personality prompt to prevent unnecessary apologies after
successful Librarian delegations. Added explicit "do NOT apologize"
instructions to both system prompt and synthesis prompt.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rewrite system prompt with negative constraints and step-by-step process
- Set temperature to 0.1 for deterministic tool calling
- Sort room groups to top of device list (address positional bias)
- Add [ROOM GROUP] marker in list_devices output
- Update tool docstrings with explicit entity_id= parameter examples
- Add optimization findings doc (experiment log: 0% → 100% success)
- Add test script for room group detection regression testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update all client endpoints to use /housekeeping/ prefix
- Add critical rule requiring list_devices() before control actions
- Add housekeeping API spec documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert booleans to strings for Redis hset (Redis doesn't accept bool)
- Extract capability from delegate_to_X tool names for tracking
- Use loop_scope="module" for pytest-asyncio module-scoped fixtures
- Add note about using venv for tests in AGENTS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Messages in reasoning_content should be plain text, not wrapped
in <think> tags. Removed wrappers from:
- delegation.py household think messages
- orchestration.py status messages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use DeepSeek R1 format (reasoning_content field) instead of <think>
tags in content. Open WebUI now renders thinking as proper
collapsible blocks instead of broken escaped HTML.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
library-desk now returns keywords as dict with core_keywords field.
Client now handles both list and dict formats for backwards compat.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix `invalid message content type: <nil>` error from Ollama
- Create TatlockOllamaProvider that sanitizes messages (null → "")
- Update all agents to use sanitized provider
- Fix repeating think messages by adding ReasoningSummaryDone signal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes several issues with the web search migration to Librarian:
- Update Steward routing guidelines for web search/weather → Librarian
- Register search_web, read_url, read_urls_batch tools with Librarian agent
- Update Librarian system prompt with web search documentation
- Fix query enrichment not being passed to delegations (location context)
- Add URL reading keywords to RESEARCH action type detection
Weather queries now automatically include user's stored location from
the Biographer, enabling location-aware search results.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move web search functionality to The Librarian agent, integrating with
the library-desk /rag/search endpoint for enhanced search capabilities.
Changes:
- Add search_web, read_url, read_urls_batch tools to Librarian
- Add WebSearchResult, ContentExtractionResult models to client
- Add search_web, extract_content, extract_content_batch client methods
- Update Librarian capability with web/url/internet domains
- Remove search_web from tatlock_core tools and toolset
- Update Tatlock system prompt to delegate web search to Librarian
- Add comprehensive unit tests for new Librarian tools
- Clean up legacy src/agents/tools.py
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two-Phase Tatlock Execution:
- orchestrate_tool_calls() for Phase 1 coordination
- synthesize_from_results() for Phase 2 butler-toned synthesis
- Guarantees butler personality in all responses
Automatic Think Slugs:
- Deterministic butler-perspective messages during expert delegation
- ActionType enum: RETRIEVE, RESEARCH, CREATE, CONTROL, RECORD
- HOUSEHOLD_THINK_MESSAGES mapping for all experts
- Streaming delegation wrappers with automatic think messages
Steward Query Enrichment:
- Auto-fill user context (location, timezone) when not specified
- _build_enriched_query() with regex word boundary matching
- enriched_query field in StewardRecommendation schema
Documentation:
- ORCHESTRATION_SCENARIOS.md rewritten with Mermaid diagrams
- New Housekeeper and Biographer scenarios
- TESTING_IMPROVEMENTS.md for future LLM testing patterns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements The Housekeeper, a new expert agent for home automation
following the Librarian pattern. Communicates with core-api service
which wraps Home Assistant REST API.
New agent features:
- CoreAPIClient with 13 home automation methods
- 13 tools: list_areas, list_devices, get_device_state, turn_on,
turn_off, toggle, list_scenes, activate_scene, list_scripts,
run_script, list_automations, toggle_automation, get_history
- PydanticAI agent with butler-friendly system prompt
- HouseholdCapability registration for Steward coordination
- delegate_to_housekeeper() wrapper for orchestration
Also includes:
- Dev port changed from 8123 to 8777 (avoids Home Assistant conflict)
- Config: CORE_API_HOST, CORE_API_KEY, CORE_API_TIMEOUT
- 44 unit tests for client and capability
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
### Added
- Environment-aware configuration:
- Auto-selected logging (DEBUG for dev, WARNING for prod)
- Auto-selected default user (llm_tester for dev isolation)
- User context logging at request entry
- Direct delegation bypass:
- Pure memory/librarian requests skip Tatlock LLM
- Reduces latency for memory-only requests
- Text-based delegation fallback:
- Parse [DELEGATE:agent] patterns from LLM output
- Sequential and parallel execution support
- Comprehensive E2E test suite:
- 22 orchestration tests with QdrantVerifier
- assert_llm_behavior() for flexible pattern matching
- Tests for memory, delegation, isolation, scenarios
### Fixed
- Unit test mocks for streaming (async generator)
- Temporal context handling in tests
- LLM non-determinism with pytest.xfail()
- Streaming test timeouts increased
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change `str | None` to `str` with empty default for memory_type
- Remove `keywords` parameter from store_insight (auto-generated anyway)
- Ollama's OpenAI API doesn't handle union types with None properly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add delegate_to_biographer to household registry delegation map
- Was returning raw tools which caused Ollama "invalid message content type: nil"
- Add Qdrant host/port to .env.example
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>