Skip to content

feat(chat): ChatProvider abstraction over REM synthesizer — #178 chat-side foundation#192

Merged
Dewinator merged 1 commit into
mainfrom
agent/chat-provider-abstraction
May 3, 2026
Merged

feat(chat): ChatProvider abstraction over REM synthesizer — #178 chat-side foundation#192
Dewinator merged 1 commit into
mainfrom
agent/chat-provider-abstraction

Conversation

@Dewinator

Copy link
Copy Markdown
Owner

Summary

What changes

  • mcp-server/src/services/chat.ts (new): ChatProvider interface, OllamaChatProvider extracted verbatim from synthesize-cluster.mjs's callQwen + unloadQwen, extractJSON() pure helper (Qwen3 <think> blocks, ```json fences, prose-wrapped JSON), createChatProvider() env factory honouring MYCELIUM_LLM_PROVIDER. The llama-cpp branch throws a clear stub error pointing at the follow-up PR — no silent fallthrough.
  • mcp-server/src/__tests__/chat.test.ts (new): 19 unit tests
    • extractJSON: think blocks, multiple think blocks, fenced JSON (with/without json), prose-wrapped, empty input, no-JSON-found
    • OllamaChatProvider: shape sent to /api/chat (model/messages/keep_alive/think/options), missing-fields fallback to empty strings, HTTP error path with status + body, per-request keepAlive override, unload() posts keep_alive=0 on /api/generate, unload() swallows network errors (best-effort)
    • createChatProvider: default → ollama, case-insensitive, llama-cpp stub throws, llamacpp alias, unknown provider with helpful list
  • scripts/synthesize-cluster.mjs (refactor): uses createChatProvider() + extractJSON from the compiled module instead of inline fetch. unloadModel() is the new name; unloadQwen kept as a back-compat re-export.

Why this slice now

Spike-2 doc §"Suggested follow-up issues" #2: "add LlamaCppChatProvider and route REM digest through it when MYCELIUM_LLM_PROVIDER=llama-cpp". The pre-step is to give synthesize-cluster.mjs a swap point — that's this PR. Without it the LlamaCpp impl would have to either fork the script or re-introduce the same hard-coded HTTP shape.

Out of scope

Test plan

  • npm run build — clean
  • npm test — 958 pass (was 939; +19 new), 0 fail
  • node --check scripts/synthesize-cluster.mjs — OK
  • Manual: node scripts/synthesize-cluster.mjs --dry against a live Ollama (env paths preserved: OLLAMA_URL, REM_MODEL, REM_KEEP_ALIVE, REM_NUM_CTX)

Pillar check

Pillar 1 (decentralised AI) — strengthened indirectly: sets up the seam that lets the native-app build drop the Ollama daemon. No pillar weakened.

🤖 Generated with Claude Code

…-side foundation

Mirrors the EmbeddingProvider pattern from PR #187 on the chat half of
issue #178 (node-llama-cpp bridge for the native-app track #176).

The REM synthesizer in scripts/synthesize-cluster.mjs talked Ollama HTTP
directly: /api/chat for the call, /api/generate for unload, plus inline
extractJSON to strip Qwen3 <think> blocks. Hard-codes the provider, no
seam for the in-process llama.cpp path.

This change extracts the seam:

* mcp-server/src/services/chat.ts — ChatProvider interface, OllamaChatProvider
  with the existing fetch+think:true behaviour, extractJSON pure helper,
  and createChatProvider() env factory honouring MYCELIUM_LLM_PROVIDER
  (matches createEmbeddingProvider's switch). llama-cpp branch throws a
  clear stub error pointing at the follow-up PR.
* mcp-server/src/__tests__/chat.test.ts — 19 unit tests covering
  extractJSON edge cases (think blocks, code fences, prose-wrapped JSON,
  empty input), OllamaChatProvider shape (model/messages/keep_alive/
  options sent correctly, HTTP error path, per-request keepAlive override,
  unload best-effort), and factory env routing.
* scripts/synthesize-cluster.mjs — uses the abstraction instead of inline
  fetch + extractJSON. unloadQwen() kept as a back-compat alias of the new
  unloadModel().

No package.json changes, no new dependencies — keeps the merge surface
clean against PR #187 (which adds node-llama-cpp). The LlamaCppChatProvider
itself is the next slice and lands once #187 is on main.

Tests: 958 pass (was 939; +19 new), 0 fail.

Pillar 1 (decentralised AI) — strengthened indirectly by setting up the
seam that lets the native-app build drop the second daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dewinator added a commit that referenced this pull request May 3, 2026
…n W4.1

W4.1 of docs/wave-4-anti-echo.md says "the first PR of this wave creates
the directory" — this is that scaffold. Lands two files only:

  - mcp-server/src/__tests__/fixtures/anti-echo/README.md
    Developer-facing spec for the corpus shape, mirrors the governance
    rules from the anchor doc but written for the file-format reader.
  - mcp-server/src/__tests__/fixtures/anti-echo/corpus-types.ts
    `AntiEchoCorpusFixture` + `AntiEchoCohortFixture` discriminated union
    over the v1.1 Lesson envelope (services/wire-types.ts). Types only,
    no loader, no harness — those land alongside the first concrete
    fixture per category in subsequent PRs.

Why scaffold-first instead of one big "land all 8 fixtures" PR: the eight
attack categories from wave-4-anti-echo.md §"Corpus categories" each have
their own subtleties (cohort vs single-envelope, signing-key handling,
which §10 mechanism asserts). Decomposing into one fixture per follow-up
PR keeps each diff reviewable and lets the harness shape evolve from the
first concrete fixture rather than from speculation.

Why this can land while the 9-PR native-app queue is open: the new
directory lives entirely under `__tests__/fixtures/`, so it has zero file
overlap with the native-app stack (#185 / #187 / #188 / #189 / #190 /
#191 / #192 / #193 / #194). 939/939 node --test tests still green;
`tsc --noEmit` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dewinator Dewinator merged commit a611c1b into main May 3, 2026
1 check passed
Dewinator added a commit that referenced this pull request May 3, 2026
…rain)

Reed merged 10 PRs today: all 3 W4.1 anti-echo (#197/#198/#201), both W2
federation (#199/#200), 5 native-app (#190/#191/#192/#193/#194). Only the
linear 4-PR #178-stack remains open (#185 independent + #187#188#189
strictly stacked). Three-cohort split collapsed to one cohort — old order-
independence proofs (143rd/148th tick) now obsolete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant