You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Reconsider the nlp-engine/agent crate boundary for LLM connectivity code #1978
This surfaced while adding prompt/response dump instrumentation for debugging LLM conversations (see #1956, where a user asked to see the exact prompt/response sent to and received from the LLM at every point in the agent loop). The fix required two separate dump mechanisms in two different crates for what is conceptually one thing — "the exact conversation sent to/from an LLM" — which is a direct symptom of a crate-boundary question worth revisiting deliberately, not folding into an unrelated PR.
Problem Statement
packages/nlp-engine/src/chat/prompt_dump.rs, wired into ChatEngine::generate_blocking — the single chokepoint for the local/native GGUF path (Stage-1 routing, Stage-2 ReAct turns, resolve_query, the routing probe — everything that calls into llama.cpp).
packages/agent/src/local_agent/openai_compat_prompt_dump.rs, wired directly into OpenAiCompatInferenceEngine::generate in packages/agent/src/local_agent/openai_compat_inference.rs — because that implementation is a pure HTTP client (reqwest) that never touches nlp-engine at all.
Both implement the same ChatInferenceEngine trait and are functionally interchangeable inference backends, but live in different crates because one needs a network dependency and the other doesn't.
Current split, all inside packages/agent/src/local_agent/: the ReAct loop (agent_loop.rs), the native inference bridge (inference.rs, wrapping nlp-engine::ChatEngine), the OpenAI-compatible HTTP client (openai_compat_inference.rs, openai_compat_discovery.rs), and the model manager (model_manager.rs) all live in the agent crate. Only the actual llama.cpp wrapper (ChatEngine, generate_streaming) lives in nlp-engine.
Proposed Solution
Current architecture (../nodespace-docs/architecture/crate-boundaries.md): nlp-engine is documented as a leaf crate scoped to "LLM inference and embedding computation" only, explicitly "no internal deps," with its decision tree routing anything that "orchestrates AI agents" to the agent crate instead. Under this rule, OpenAiCompatInferenceEngine lives in agent specifically because it needs reqwest/futures, and giving a leaf crate a networking dependency conflicts with that document's stated design.
Alternative framing (raised during #1956's investigation): the split should be app/orchestration vs. NLP-domain, not local-compute vs. everything-else. Under that framing, "how a conversation reaches a model" — whether via local llama.cpp or a remote HTTP endpoint — is one cohesive NLP-domain concern ("LLM connectivity"), regardless of backend, and belongs together in nlp-engine (accepting a reqwest dependency there) rather than split by which backend happens to need a network call. The ReAct loop, tool execution, and skill routing (agent_loop.rs) would still belong in agent under either framing — this is specifically about the inference engine implementations, not the orchestration logic built on top of them.
Why this matters going forward: additional remote-model backends beyond OpenAI-compatible endpoints are expected. Under the current split, each new backend's HTTP client code keeps landing in agent for the same leaf-crate-purity reason, while the domain concept of "talking to an LLM" stays fragmented across two crates. Deciding this now, before more backends exist, avoids compounding the split.
Decide, as an explicit architecture decision, whether inference-engine implementations (both local and remote) should consolidate into nlp-engine, stay split as they are today, or use some other boundary.
Architecture Impact
This issue is explicitly about deciding whether to reverse or extend ../nodespace-docs/architecture/crate-boundaries.md's current rule (nlp-engine = LLM inference/embedding computation only, leaf crate, no deps; agent = orchestration, including any HTTP-based inference backend). That document was written specifically to prevent misplaced code ("agent-authored code was incorrectly placed in desktop-app... these rules prevent that class of mistake") — so any resolution here should either reaffirm the current rule with clearer rationale, or formally supersede it via a new ADR, not just move files without updating the doc.
If consolidation is chosen: nlp-engine gains a reqwest/HTTP dependency (currently absent — it is a "no internal deps" leaf crate); crate-boundaries.md's "Contains" / "Does not contain" sections for both crates need updating; the two now-separate prompt-dump mechanisms could merge into one.
Acceptance Criteria
Explicit decision made on whether inference-engine implementations (inference.rs, openai_compat_inference.rs, openai_compat_discovery.rs) consolidate into nlp-engine, stay in agent, or use a different boundary
Decision recorded as an ADR in ../nodespace-docs/decisions/ (or an edit to crate-boundaries.md directly, if the resolution is narrow enough not to warrant a full ADR)
crate-boundaries.md updated to reflect whichever boundary is decided, so it stays the accurate reference for future placement decisions
If consolidation is chosen, a follow-up issue is filed for the actual code move (out of scope here)
Technical Specifications
Reference Files
packages/nlp-engine/src/chat/mod.rs — ChatEngine::generate_streaming/generate_blocking, the local inference chokepoint
Status-confirmation only (2026-09-12) — this is an architecture question in Michael's lane (packages/agent), not something being resolved via triage here.
Checked whether anything since filing has made the question moot: it hasn't.
packages/agent/src/local_agent/ still contains inference.rs, openai_compat_inference.rs, openai_compat_discovery.rs, and openai_compat_prompt_dump.rs alongside agent_loop.rs — the split described in the issue is unchanged.
nlp-engine is still a leaf crate: packages/nlp-engine/Cargo.toml carries futures but no reqwest.
../nodespace-docs/architecture/crate-boundaries.md is unchanged and still documents the exact rule this issue is questioning.
Remove the Ollama-native inference path: drive Ollama through its OpenAI-compatible endpoint #1793 (closed) removed the Ollama-native inference path in favor of driving Ollama through the existing OpenAI-compatible client — this reduces near-term pressure to add more bespoke per-backend HTTP clients, but doesn't touch the crate-boundary decision itself. No ADR resolving this has been filed.
Still a live, unresolved architecture question — leaving open in Backlog. This needs Michael's actual input to resolve, not further triage.
Overview
This surfaced while adding prompt/response dump instrumentation for debugging LLM conversations (see #1956, where a user asked to see the exact prompt/response sent to and received from the LLM at every point in the agent loop). The fix required two separate dump mechanisms in two different crates for what is conceptually one thing — "the exact conversation sent to/from an LLM" — which is a direct symptom of a crate-boundary question worth revisiting deliberately, not folding into an unrelated PR.
Problem Statement
packages/nlp-engine/src/chat/prompt_dump.rs, wired intoChatEngine::generate_blocking— the single chokepoint for the local/native GGUF path (Stage-1 routing, Stage-2 ReAct turns,resolve_query, the routing probe — everything that calls into llama.cpp).packages/agent/src/local_agent/openai_compat_prompt_dump.rs, wired directly intoOpenAiCompatInferenceEngine::generateinpackages/agent/src/local_agent/openai_compat_inference.rs— because that implementation is a pure HTTP client (reqwest) that never touchesnlp-engineat all.Both implement the same
ChatInferenceEnginetrait and are functionally interchangeable inference backends, but live in different crates because one needs a network dependency and the other doesn't.Current split, all inside
packages/agent/src/local_agent/: the ReAct loop (agent_loop.rs), the native inference bridge (inference.rs, wrappingnlp-engine::ChatEngine), the OpenAI-compatible HTTP client (openai_compat_inference.rs,openai_compat_discovery.rs), and the model manager (model_manager.rs) all live in theagentcrate. Only the actual llama.cpp wrapper (ChatEngine,generate_streaming) lives innlp-engine.Proposed Solution
Current architecture (
../nodespace-docs/architecture/crate-boundaries.md):nlp-engineis documented as a leaf crate scoped to "LLM inference and embedding computation" only, explicitly "no internal deps," with its decision tree routing anything that "orchestrates AI agents" to theagentcrate instead. Under this rule,OpenAiCompatInferenceEnginelives inagentspecifically because it needsreqwest/futures, and giving a leaf crate a networking dependency conflicts with that document's stated design.Alternative framing (raised during #1956's investigation): the split should be app/orchestration vs. NLP-domain, not local-compute vs. everything-else. Under that framing, "how a conversation reaches a model" — whether via local llama.cpp or a remote HTTP endpoint — is one cohesive NLP-domain concern ("LLM connectivity"), regardless of backend, and belongs together in
nlp-engine(accepting areqwestdependency there) rather than split by which backend happens to need a network call. The ReAct loop, tool execution, and skill routing (agent_loop.rs) would still belong inagentunder either framing — this is specifically about the inference engine implementations, not the orchestration logic built on top of them.Why this matters going forward: additional remote-model backends beyond OpenAI-compatible endpoints are expected. Under the current split, each new backend's HTTP client code keeps landing in
agentfor the same leaf-crate-purity reason, while the domain concept of "talking to an LLM" stays fragmented across two crates. Deciding this now, before more backends exist, avoids compounding the split.Decide, as an explicit architecture decision, whether inference-engine implementations (both local and remote) should consolidate into
nlp-engine, stay split as they are today, or use some other boundary.Architecture Impact
This issue is explicitly about deciding whether to reverse or extend
../nodespace-docs/architecture/crate-boundaries.md's current rule (nlp-engine= LLM inference/embedding computation only, leaf crate, no deps;agent= orchestration, including any HTTP-based inference backend). That document was written specifically to prevent misplaced code ("agent-authored code was incorrectly placed in desktop-app... these rules prevent that class of mistake") — so any resolution here should either reaffirm the current rule with clearer rationale, or formally supersede it via a new ADR, not just move files without updating the doc.If consolidation is chosen:
nlp-enginegains areqwest/HTTP dependency (currently absent — it is a "no internal deps" leaf crate);crate-boundaries.md's "Contains" / "Does not contain" sections for both crates need updating; the two now-separate prompt-dump mechanisms could merge into one.Acceptance Criteria
inference.rs,openai_compat_inference.rs,openai_compat_discovery.rs) consolidate intonlp-engine, stay inagent, or use a different boundary../nodespace-docs/decisions/(or an edit tocrate-boundaries.mddirectly, if the resolution is narrow enough not to warrant a full ADR)crate-boundaries.mdupdated to reflect whichever boundary is decided, so it stays the accurate reference for future placement decisionsTechnical Specifications
Reference Files
packages/nlp-engine/src/chat/mod.rs—ChatEngine::generate_streaming/generate_blocking, the local inference chokepointpackages/nlp-engine/src/chat/prompt_dump.rs— the local-path dump mechanism added in Expose Gemma 4 12B as a selectable native model (once verified on adequate hardware) #1956packages/agent/src/local_agent/inference.rs—LlamaChatInferenceEngine, the native bridge implementingChatInferenceEnginepackages/agent/src/local_agent/openai_compat_inference.rs—OpenAiCompatInferenceEngine, the remote HTTP-based implementation of the same traitpackages/agent/src/local_agent/openai_compat_prompt_dump.rs— the remote-path dump mechanism added in Expose Gemma 4 12B as a selectable native model (once verified on adequate hardware) #1956packages/agent/src/local_agent/openai_compat_discovery.rs— endpoint model discovery for the remote path../nodespace-docs/architecture/crate-boundaries.md— the current documented rule this issue evaluatesNon-Goals
agent_loop.rs(ReAct loop, tool execution, skill routing) — out of scope regardless of which way this is decided.Related Issues