Repository navigation
Wire the agent's embedding service by construction instead of re-injecting it per engine swap #1328
Description
Overview
The agent's GraphToolExecutor receives the embedding service by having it manually re-passed every time the inference engine is swapped. One construction site passed None, which disabled semantic search and skill-based tool routing in production until it was fixed. A residual startup race remains. This issue makes the executor read the current embedding service by construction so the whole class of bug cannot recur.
Problem Statement
In packages/daemon/src/services/local_agent_service.rs:
- The embedding service is held in
LocalAgentServiceInner(asArc<RwLock<Option<Arc<NodeEmbeddingService>>>>). - It must be manually re-passed into a freshly-built
GraphToolExecutorinreplace_engine()each time the engine changes. build_noop_service()andreplace_engine()are two separate construction sites; one of them was passingembedding_service: None, which disabledsearch_semanticfor the agent AND disabled skill-basedselect_toolsrouting (skill matching runs semantic search over skill nodes via the same embedding service). The full tool list was always sent instead of the skill-scoped list.- Residual race: the embedding model loads in a background task. If the chat model is loaded before embeddings finish,
replace_engine()capturesNoneand will not pick up the embedding service until the next engine swap.
Proposed Solution
Have the executor read the embedding service through the shared handle by construction rather than capturing a snapshot at swap time. Options:
GraphToolExecutorholds the sharedArc<RwLock<Option<Arc<NodeEmbeddingService>>>>and reads it per call (so it always sees the current value, closing the race), or- Re-run the executor wiring when embeddings become ready (a re-swap hook from the background loader), as a narrower alternative.
Option 1 is the durable fix. Note GraphToolExecutor lives in the agent crate (packages/agent/src/local_agent/tools.rs), so the shared handle type must be expressible there without a daemon dependency.
Acceptance Criteria
- The executor can never be wired with a stale or
Noneembedding service when one is (or becomes) available - The startup race (chat model loaded before embeddings finish) is closed: a turn run after embeddings load uses semantic search and skill routing without an engine swap
-
search_semanticand skill-basedselect_toolswork in a daemon where the chat model was loaded first - No remaining construction site can pass
embedding_service: Nonewhen the service exists - Code passes `bun run quality:fix`
- Tests pass (`bun run test:all`)
Technical Specifications
Reference Files
- Service wiring: `packages/daemon/src/services/local_agent_service.rs` (`replace_engine`, `build_noop_service`, the background embedding loader)
- Executor struct: `packages/agent/src/local_agent/tools.rs` (`GraphToolExecutor`, `select_tools`, `exec_search_semantic`)
Context
This is the durable fix for the embedding_service: None class of bug. A point fix (passing the current service through in replace_engine) has already landed; this issue removes the need to remember to do that and closes the race.
Parallelization
Scoped to the daemon service wiring and the GraphToolExecutor struct. It does not change message types, the tool name registry, or prompt assembly, so it runs in parallel with the related issues. It shares the agent crate with the message-type issue but touches different files (tools.rs struct/methods vs agent_types.rs/inference.rs).
Activity
ai-chat agent quality: fix runaway/hang + tool-call template leaks, t…
malibio commentedon Jun 5, 2026
Frontend baseline: 3844 passed (134 files). Starting work in worktree issue-1328-wire-embedding-handle.
Wire agent embedding service by shared handle, closing startup race (c…
ai-chat agent quality: fix runaway/hang + tool-call template leaks, t…
Wire agent embedding service by shared handle, closing startup race (c…