fix: workflow inference chain — surface llama-server errors, user-turn-last, bundle-relative prompts

- LlamaCppInferenceProvider: check HTTP status and surface the real error body instead of masking it as a ChatCompletionResponse deserialization failure
- PromptRenderer: render the live conversation layer (L1) last so the user turn is the final message (fixes Qwen 'No user query found' 500 when L3 recall is present)
- TomlWorkflowLoader: resolve stage prompts relative to the workflow file's own dir (bundle), CWD-independent; config-dir fallback for globals
- SessionOrchestrator: hard-fail a stage whose declared prompt can't resolve (was a silent user-less request)
- move healthcheck prompts next to the example workflow (examples/workflows/prompts/)
This commit is contained in:
2026-06-01 23:23:29 +04:00
parent 55a18b4b3a
commit 94f7ad0ee9
6 changed files with 50 additions and 23 deletions
@@ -15,6 +15,7 @@ import com.fasterxml.jackson.dataformat.toml.TomlMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
private data class WorkflowFile(
@@ -67,10 +68,17 @@ class TomlWorkflowLoader(
val raw = path.readText()
val file = runCatching { mapper.readValue<WorkflowFile>(raw) }
.getOrElse { throw WorkflowValidationException("Failed to parse workflow TOML: ${it.message}") }
return file.toWorkflowGraph()
return file.toWorkflowGraph(path.parent)
}
private fun WorkflowFile.toWorkflowGraph(): WorkflowGraph {
private fun resolvePromptPath(raw: String, workflowDir: Path?): String {
val p = Path.of(raw)
if (p.isAbsolute) return raw
val candidate = workflowDir?.resolve(raw)
return if (candidate != null && candidate.exists()) candidate.toString() else raw
}
private fun WorkflowFile.toWorkflowGraph(workflowDir: Path?): WorkflowGraph {
val startId = StageId(start)
val stageMap = stages.associate { s ->
StageId(s.id) to StageConfig(
@@ -84,8 +92,8 @@ class TomlWorkflowLoader(
tokenBudget = s.tokenBudget,
maxRetries = s.maxRetries,
metadata = buildMap {
s.prompt?.let { put("prompt", it) }
s.systemPrompt?.let { put("systemPrompt", it) }
s.prompt?.let { put("prompt", resolvePromptPath(it, workflowDir)) }
s.systemPrompt?.let { put("systemPrompt", resolvePromptPath(it, workflowDir)) }
},
)
}