From c70b6779a31c1640ca05e66bb48892ec012b01ef Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 1 Aug 2026 01:03:23 +0400 Subject: [PATCH] fix(context): name emit_artifact in the schema instruction (#416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pinned schemaInstruction said only "Respond with a single JSON object matching this schema", contradicting every role prompt that says to emit via the emit_artifact tool. Two stated channels for one artifact, and the pinned one won. ResponseFormat.Json is built under exactly the condition that offers the tool (an llmEmitted slot — see emitArtifactTool), so the tool is always available wherever this instruction renders, and the kernel prefers it: tool-calling models are more reliable at it and it sidesteps llama.cpp's grammar+tools incompatibility. The instruction now names it, and keeps raw JSON as what to do when told to stop calling tools — which is what the tools-less final pass demands and what the executor already accepts either way (llmArtifactOverride ?: response.text). Co-Authored-By: Claude Opus 5 --- .../orchestration/SessionOrchestratorContext.kt | 14 ++++++++++++-- .../orchestration/SessionOrchestratorExecution.kt | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorContext.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorContext.kt index 4d106a0d..8e78ade6 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorContext.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorContext.kt @@ -52,11 +52,21 @@ internal fun SessionOrchestrator.evictArtifactContentCache(sessionId: SessionId) internal suspend fun SessionOrchestrator.buildSchemaEntries( responseFormat: ResponseFormat, stageId: StageId, + artifactName: String, ): List { if (responseFormat !is ResponseFormat.Json) return emptyList() val compactSchema = Json.encodeToString(JsonSchema.serializer(), responseFormat.schema) - val instruction = "Respond with a single JSON object matching this schema. " + - "Do not include markdown, code fences, or commentary outside the JSON. " + + // #416: names emit_artifact as the channel. ResponseFormat.Json is emitted under exactly the + // condition that offers the tool (an llmEmitted slot — see emitArtifactTool), so the tool is + // always available here, and the kernel prefers it: tool-calling models are more reliable at it + // and it sidesteps llama.cpp's grammar+tools incompatibility. The old text said only "respond + // with a single JSON object", contradicting every role prompt that says to call emit_artifact. + // Raw JSON stays valid as the second sentence describes, since the executor accepts either + // (llmArtifactOverride ?: response.text) and the tools-less final pass explicitly demands it. + val instruction = "Produce the '$artifactName' artifact by calling the $EMIT_ARTIFACT_TOOL tool " + + "with its fields filled in, matching this schema. If you are told to stop calling tools, " + + "output the same object as a single JSON object in your final message instead. Either way, " + + "no markdown, no code fences, and no commentary outside the JSON. " + "Schema: $compactSchema" return listOf( ContextEntry( diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorExecution.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorExecution.kt index b4403a0a..83b46a10 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorExecution.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestratorExecution.kt @@ -152,7 +152,11 @@ internal suspend fun SessionOrchestrator.executeStage( ?.let { ResponseFormat.Json(it.kind.deriveJsonSchema()) } ?: ResponseFormat.Text - val schemaEntries = buildSchemaEntries(responseFormat, stageId) + val schemaEntries = buildSchemaEntries( + responseFormat, + stageId, + llmEmittedSlots.firstOrNull()?.name?.value ?: "", + ) val intentEntries = buildIntentEntry(sessionId) val steeringEntries = buildSteeringNoteEntries(sessionId) val clarificationEntries = buildClarificationAnswerEntries(sessionId)