Symptom

The Server.LLM node was the only SourceMetaData node that didn’t behave like the rest of the swarm. It accumulated a multi-turn conversation in meta.chat and emitted a bespoke EventType.LLM instead of storing a result in snapshot and emitting SNAPSHOT_UPDATE. As a result no ordinary observer (DataPoint, SMTP, webhook) could consume its output, and ~550 lines of agentic-proposal model classes in krill-sdk were dead weight left over from an abandoned “LLM proposes nodes/actions/links” experiment.

Root cause

The node was built during an interactive-chat phase, before the observer-flow-collector-dispatch contract (“a node does its job, updates itself, and stops; observers collect its flow and re-run”) existed. The chat model and the dedicated SSE event were the wrong shape for a single-purpose source node.

Fix

Realigned the node to the single-purpose contract across two repos. In krill-sdk (published 0.0.40): LLMMetaData dropped chat and gained backend (Ollama / OpenAI-compatible), systemPrompt, responseFormat (natural language / JSON), and responseInstructions (defaulting to the new LLMResult JSON-schema contract); the dead agentic classes were deleted. In krill: ServerLLMProcessor now computes a prompt (system + user prompt + input node-type contracts + live input state), dispatches to the selected backend, stores the reply in meta.snapshot, and update()s itself so observers wake. RESET clears the snapshot and is terminal. LLMEventPayload and its serializer registration were removed; success surfaces via the standard SNAPSHOT_UPDATE path. The editor lost its chat panel and gained backend / response-format pickers and editable system-prompt + response-instructions fields.

Prevention

When adding a node type, conform it to the SourceMetaData contract from the start: store output in snapshot, fan out by updating yourself (never a bespoke event or a per-node history field). A node whose result can’t be read by a generic observer is a design smell. The regression guard is ServerLLMProcessorTest: it asserts the reply lands in the snapshot (text and JSON modes), that each backend hits the correct path, that RESET clears and a later EXECUTE overwrites (no history), and that failures set ERROR without fan-out — all against a MockEngine, no real ports or paths.