An LLM node’s prompts were fixed strings. The only dynamism available was the node’s inputs
list, whose members were serialized into the user turn as context — so “make the prompt be the
newest camera frame” or “be the payload the webhook just received” was not expressible, and
users approximated it by wiring nodes into inputs and hoping the model made the connection.
krill-sdk 0.0.59 added LLMMetaData.promptDataSource / systemPromptDataSource for exactly
this, and nothing in the server read them.
Two bugs surfaced while wiring them up, both from the same root: a camera prompt source resolves to an image and an empty string, and empty-string is not null.
(resolution as? Ok)?.prompt?.text ?: meta.systemPrompt.ifBlank { DEFAULT_SYSTEM_PROMPT } —
the elvis never fires for "", so a camera named as the system prompt source silently
shadowed the default persona and the model was sent with no persona at all.meta.inputs.isEmpty() rather than the inputs that actually
resolved. When every input had been deleted or lived on an unreachable peer, the guard
passed and the node POSTed a user turn consisting of one blank line — then persisted whatever
the model invented into its snapshot and fanned it out over SSE.server/.../krillapp/server/llm/PromptSourceResolver.kt (new) resolves a NodeIdentity? into
prompt material per node type: a Camera contributes its frame as a base64 image (read from
local disk, else fetched from the host that captured it), every other node contributes its
snapshot.value verbatim. A configured-but-unusable source is Failed(reason) — never a
silent fallback — and the reason names the actual cause (cold camera vs. over the size cap vs.
node missing) rather than collapsing them into one string.server/.../krillapp/server/llm/ServerLLMProcessor.kt resolves each prompt slot as
source → fixed text → empty, where an Ok resolution with blank text (i.e. an image-only
source) contributes no text and falls through. A camera wired as the prompt source alongside a
typed prompt therefore sends the user’s question with the frame. DEFAULT_SYSTEM_PROMPT
remains the system fallback; the invented "Summarize the current state of the input nodes."
user-prompt default is deleted.Failed on either slot records meta.error via nodeManager.failed() and skips the backend
call outright; both slots’ reasons are aggregated and slot-labelled.composeApp/.../krillapp/server/llm/EditLLM.kt picks and clears a source per prompt slot; the
text field is disabled (never wiped) while a source is set.?: guards null, not blank. Any fallback chain
whose upstream can legitimately produce "" needs ifBlank { null }, and the type should say
so. This bug pattern will recur wherever a resolver returns “success, but with nothing in it.”if (config.isEmpty()) sitting next
to an already-computed resolved collection is asking to be wrong.kotlin.test.assertIs<T> returns T. A test function whose expression body ends in it
infers a non-Unit return type, and JUnit Jupiter silently skips it — the suite reports green
while the test never ran. This bit us twice here. Give such functions an explicit : Unit, and
read skipped="0" out of the XML rather than trusting the console’s test count.