feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening
Threads reasoning_content across inference calls and journal renders, filters markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks, and tightens filesystem tool output/dir-listing behavior surfaced by prior live QA (see project_readloop_campaign memory).
This commit is contained in:
+42
-1
@@ -142,9 +142,15 @@ class LlamaCppInferenceProvider(
|
||||
|
||||
val baseMessages = PromptRenderer.render(request.contextPack)
|
||||
val messages = baseMessages.map { msg ->
|
||||
val toolCalls = if (msg.role == "assistant") {
|
||||
parseAssistantToolCalls(msg.content)
|
||||
} else emptyList()
|
||||
LlamaCppChatMessage(
|
||||
role = msg.role,
|
||||
content = msg.content,
|
||||
content = if (toolCalls.isNotEmpty()) null else msg.content,
|
||||
reasoningContent = msg.reasoning,
|
||||
toolCalls = toolCalls,
|
||||
toolCallId = msg.toolCallId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,5 +236,40 @@ class LlamaCppInferenceProvider(
|
||||
ProviderHealth.Unavailable("Health check failed: ${e.message}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an assistant message's content back into [ToolCallRequest] objects.
|
||||
*
|
||||
* The context builder's FORMAT_COMPRESS stage converts raw ToolCallRequest JSON into a compact
|
||||
* dotted-line format (id = ..., function.name = ...). If compression is disabled, the content
|
||||
* is still valid JSON. This handles both forms so the Gemma4 Jinja template sees the native
|
||||
* `tool_calls` array — which it requires to render `reasoning_content` as a `<|channel>thought`
|
||||
* block (without `tool_calls` the reasoning field is silently dropped and the model re-enters
|
||||
* thought mode from scratch, causing the degenerate `<|channel>thought`~16384-token loop).
|
||||
*/
|
||||
private fun parseAssistantToolCalls(content: String): List<ToolCallRequest> {
|
||||
// Try JSON parse first (FORMAT_COMPRESS disabled — raw ToolCallRequest JSON).
|
||||
if (content.startsWith("{\"id\"")) {
|
||||
runCatching {
|
||||
return listOf(json.decodeFromString<ToolCallRequest>(content))
|
||||
}
|
||||
}
|
||||
// Try dotted-line format (FORMAT_COMPRESS enabled).
|
||||
if (!content.contains("function.name")) return emptyList()
|
||||
val lines = content.split('\n')
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var arguments: String? = null
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.startsWith("id = ") -> id = line.removePrefix("id = ").trim()
|
||||
line.startsWith("function.name = ") -> name = line.removePrefix("function.name = ").trim()
|
||||
line.startsWith("function.arguments = ") -> arguments = line.removePrefix("function.arguments = ").trim()
|
||||
}
|
||||
}
|
||||
return if (name != null && arguments != null) {
|
||||
listOf(ToolCallRequest(id = id, function = ToolCallFunction(name = name, arguments = arguments)))
|
||||
} else emptyList()
|
||||
}
|
||||
|
||||
override fun capabilities(): Set<CapabilityScore> = descriptor.capabilities
|
||||
}
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ class OpenAiCompatInferenceProvider(
|
||||
// tool_calls turn (we don't carry tool_call_ids through the context pack). Fold any
|
||||
// non-standard role into a user turn so the model still sees the content.
|
||||
if (msg.role in STANDARD_ROLES) {
|
||||
OpenAiChatMessage(role = msg.role, content = msg.content)
|
||||
OpenAiChatMessage(role = msg.role, content = msg.content, reasoningContent = msg.reasoning)
|
||||
} else {
|
||||
OpenAiChatMessage(role = "user", content = "[${msg.role}] ${msg.content}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user