fix(kernel,inference): reliable artifact emission for local models
Two fixes that together let the freestyle analyst/architect stages actually produce valid artifacts on a local model (verified end-to-end: analyst passed first try, architect produced a validated plan): 1. Tools-less final emission (SessionOrchestrator): producing the artifact while tools are in the request is unreliable — llama.cpp's Gemma template switches to a channel format and leaks <|channel> markers into the text, and models keep tool-calling until the round cap without ever emitting. After the tool loop, fire ONE tools-less inference to get clean JSON (also re-enables the JSON grammar, since grammar+tools is rejected). Gated on the artifact not already being produced via emit_artifact. 2. GBNF grammar handles arrays/objects (GbnfGrammarConverter): array-typed properties fell through to a 'string' rule, so the grammar forced a string where the schema demanded an array — an unwinnable grammar-vs-validator disagreement. Add generic value/object/array rules and map types correctly. Note: also needs schema 'items' on array props (runtime schemas updated; mirror to repo schemas/*.json).
This commit is contained in:
+20
-3
@@ -19,7 +19,17 @@ internal object GbnfGrammarConverter {
|
||||
val escapeSeq = """"\\" (["\\/bfnrt] | $unicodeEscape)"""
|
||||
val stringRule = """string ::= "\"" ($charClass | $escapeSeq)* $closingQuote"""
|
||||
sb.appendLine(stringRule)
|
||||
sb.appendLine("""number ::= "-"? [0-9]+""")
|
||||
sb.appendLine("""number ::= "-"? [0-9]+ ("." [0-9]+)?""")
|
||||
sb.appendLine("""boolean ::= "true" | "false"""")
|
||||
sb.appendLine("""null ::= "null"""")
|
||||
// Generic JSON value/object/array rules so non-scalar properties are constrained to the right
|
||||
// JSON shape. Without these, an `array` property fell through to `string`, so the grammar forced
|
||||
// the model to emit a string where the schema (and post-hoc validation) demand an array — an
|
||||
// unwinnable disagreement. Element/field *types* are left to schema validation; the grammar only
|
||||
// enforces the JSON shape (brackets/braces).
|
||||
sb.appendLine("""value ::= string | number | boolean | null | object | array""")
|
||||
sb.appendLine("""object ::= "{" ws (string ws ":" ws value (ws "," ws string ws ":" ws value)*)? ws "}"""")
|
||||
sb.appendLine("""array ::= "[" ws (value (ws "," ws value)*)? ws "]"""")
|
||||
|
||||
// Build members rule:
|
||||
// - Required keys are always present, joined by commas.
|
||||
@@ -58,7 +68,14 @@ internal object GbnfGrammarConverter {
|
||||
}
|
||||
|
||||
private fun valueRuleFor(schema: JsonSchema, key: String): String {
|
||||
val prop = schema.properties[key] ?: return "string"
|
||||
return if (prop.type == "integer") "number" else "string"
|
||||
val prop = schema.properties[key] ?: return "value"
|
||||
return when (prop.type) {
|
||||
"integer", "number" -> "number"
|
||||
"boolean" -> "boolean"
|
||||
"array" -> "array"
|
||||
"object" -> "object"
|
||||
"string" -> "string"
|
||||
else -> "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -40,10 +40,31 @@ class GbnfGrammarConverterTest {
|
||||
required = emptyList(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `array property maps to the array rule, not string`() {
|
||||
// Regression: an array-typed property used to fall through to `string`, so the grammar forced
|
||||
// a string where the schema demands an array (analysis.requirements). It must map to `array`.
|
||||
val schema = JsonSchema(
|
||||
type = "object",
|
||||
properties = mapOf(
|
||||
"summary" to JsonSchemaProperty(type = "string"),
|
||||
"requirements" to JsonSchemaProperty(type = "array", items = JsonSchemaProperty(type = "string")),
|
||||
),
|
||||
required = listOf("summary", "requirements"),
|
||||
)
|
||||
val grammar = GbnfGrammarConverter.convert(schema)
|
||||
assertTrue(grammar.contains("array ::="), "grammar must declare an array rule")
|
||||
val reqLine = grammar.lineSequence().first { it.contains("\\\"requirements\\\"") }
|
||||
assertTrue(reqLine.trimEnd().endsWith("array"), "requirements must use the array rule, was: $reqLine")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `string rule does not contain quoted character class`() {
|
||||
val grammar = GbnfGrammarConverter.convert(allRequiredSchema)
|
||||
assertFalse(grammar.contains("\"["), "string rule must not contain quoted character class (\"[)")
|
||||
// Guard the original bug: the `string` rule's char class must stay unquoted. The `array` rule
|
||||
// legitimately contains a quoted "[" literal, so scope the assertion to the string rule line.
|
||||
val stringRuleLine = grammar.lineSequence().first { it.trimStart().startsWith("string ::=") }
|
||||
assertFalse(stringRuleLine.contains("\"["), "string rule must not contain quoted character class (\"[)")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user