Preserve bootstrap schema error paths for targeted correction

This commit is contained in:
Nico committed 2026-10-18 22:37:56 +02:00
1 parent af2ee6cb91
commit d957974498
7 files changed
+114 -5

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Schemafehler bei Bootstrap v21
Der Gateway validiert weiterhin gegen das vollständige lokale Ausgabeschema.
Bootstrap v21 war jedoch nicht für die detaillierte, bereinigte
Schemafehlerdiagnostik freigeschaltet. Deshalb enthielten fehlgeschlagene Aufrufe
teilweise nur `validation_keyword: type`, ohne die betroffene Stelle.
Jetzt werden auch für diesen Prompt `validation_path`, bekannte
`validation_fields` und das konkrete Validierungskeyword erfasst. Bei verschachtelten
Aktionsalternativen wird die zum vorhandenen Diskriminator passende Schemaalternative
verwendet. Unbekannte Feldnamen werden maskiert; Antwortwerte und rohe
Fehlermeldungen werden nicht in die Diagnose übernommen. Die bekannten Werkzeugfelder
`profile` und `entity_refs` sind für sichere Feldpfade freigegeben.
Der Controller speichert diese Diagnose append-only mit dem fehlgeschlagenen Aufruf.
Für Bootstrap-Aufrufe bekommen Typ- und Enumfehler einen gezielten Korrekturhinweis
am gemeldeten Pfad. Das Schema wird nicht gelockert und die Antwort nicht automatisch
umgeschrieben. Ein technischer Fehlversuch löst keinen Rechercheauftrag aus.
Die vorhandene Begrenzung ungültiger Antworten bleibt wirksam.
Lokale Tests prüfen Typfehler, fehlende Pflichtfelder, zusätzliche Felder,
Datenschutz und den Weg vom Gateway über das Paket bis zur Wiederaufnahme und
erfolgreichen Korrektur mit einem simulierten Anbieter. Es werden keine
kostenpflichtigen Anfragen ausgeführt. Historisch nicht gespeicherte Fehlerpfade
lassen sich damit nicht nachträglich rekonstruieren.
+2 -2
View File
@@ -114,5 +114,5 @@ primären Kandidaten und keine historische SQL-Sammlung; das Suchbudget wird dab
ausdrücklich als begrenzt gemeldet. Dies ist ein lokaler Werkzeugtest, keine neue
LLM-Bewertung des Bugs und kein Nachweis realer Token-/Kostenersparnis.
Die zuvor bekannte fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 ist
ein separater Punkt und wird durch diese Source-Überarbeitung nicht behoben.
Die fehlende Gateway-Schemafehlerdiagnostik für Bootstrap v21 wurde anschließend
separat korrigiert; siehe `bootstrap-schema-diagnostics.md`.
@@ -618,6 +618,12 @@ class Investigator:
'oneOf':'choose_one_valid_next_action',
'anyOf':'match_an_allowed_shape',
}.get(info.get('validation_keyword'),'match_schema_at_validation_path')
if context.get('bootstrap_json_framing'):
context['previous_rejection_details']['requirement']={
'type':'match_expected_type_at_validation_path',
'enum':'use_allowed_value_at_validation_path',
'const':'use_allowed_value_at_validation_path',
}.get(info.get('validation_keyword'),context['previous_rejection_details']['requirement'])
elif code == 'LLM_RESPONSE' and context.get('prompt_version') in {'investigation_project_v16','investigation_project_v17','investigation_project_v18'}:
with PackageReader(self._latest(state['case_id'],state['package_id']),self.store.limits) as reader:
call=self._read(reader,llm_ref,'llm-call')
+1 -1
View File
@@ -11,7 +11,7 @@ VALIDATION_FIELDS = frozenset({'retire','finding_id','intent','hypothesis_id','a
'prerequisites','requested_observations','environment','countercheck_refs','path',
'start_line','end_line','limit','offset','table','columns','filters','column','operator',
'value','text','case_sensitive','path_prefix','source_ref','source_refs','note','causal_review',
'next_step','goal','supports_if','weakens_if','investigate_if'})
'next_step','goal','supports_if','weakens_if','investigate_if','profile','entity_refs'})
def schema_feedback(error, _depth=0, *, include_keyword=False):
+1 -1
View File
@@ -155,7 +155,7 @@ class LLMGateway:
LOGGER.warning("LLM endpoint unavailable provider=%s code=%s", provider.id, code)
except ValidationError as invalid:
info={'validation_keyword':invalid.validator}
if request['prompt_version'] in {'investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18','investigation_project_v19','investigation_project_v20'}:
if request['prompt_version'] in {'investigation_bootstrap_v21','investigation_project_v14','investigation_project_v15','investigation_project_v16','investigation_project_v17','investigation_project_v18','investigation_project_v19','investigation_project_v20'}:
from .diagnostics import schema_feedback
info.update(schema_feedback(invalid,include_keyword=True))
error = LLMError("LLM_OUTPUT_SCHEMA",diagnostics=info)
@@ -0,0 +1,77 @@
import asyncio
import copy
import json
import pytest
from test_case_builder import case_setup
from test_case_interpreter import gateway
from test_investigation_tools import tool_case
from test_parameter_and_bootstrap_guard import bootstrap
from test_lean_investigation_decision import reply
from viano_bug_analysis.llm import LLMError, ProviderResponse
@pytest.mark.parametrize('fault,path,keyword,fields', [
('type','/update/working_brief/findings','type',[]),
('required','/update/working_brief','required',['retired']),
('extra','/update/working_brief','additionalProperties',['confidence']),
])
def test_bootstrap_gateway_reports_safe_nested_schema_location(fault,path,keyword,fields):
document=reply();document['update']['next_step']=None
brief=document['update']['working_brief']
if fault=='type':brief['findings']='PRIVATE_RESPONSE_VALUE'
elif fault=='required':brief.pop('retired')
else:brief['confidence']='PRIVATE_RESPONSE_VALUE'
class Adapter:
async def generate(self,*args,**kwargs):
return ProviderResponse(document,'fixture',100,50)
with pytest.raises(LLMError) as caught:
asyncio.run(gateway(Adapter()).complete_round(bootstrap()))
assert caught.value.code=='LLM_OUTPUT_SCHEMA'
details=caught.value.diagnostics
assert details['validation_path']==path
assert details['validation_keyword']==keyword
assert details['validation_fields']==fields
assert 'PRIVATE_RESPONSE_VALUE' not in json.dumps(details)
assert caught.value.metadata['input_tokens']==100
def test_bootstrap_schema_feedback_survives_package_resume_and_repairs_once(tool_case,tmp_path):
from test_investigation_hybrid import HybridAdapter
from test_working_brief import service_for
from test_investigator import CASE,state
from viano_bug_analysis.analysis import Investigator,InvestigationConfig
from viano_bug_analysis.budget import BudgetManager
from viano_bug_analysis.lifecycle import CheckpointStore,Workspace
from viano_bug_analysis.package import PackageReader,PackageValidator
from viano_bug_analysis.tools import InvestigationToolRouter
class Adapter(HybridAdapter):
def __init__(self):super().__init__();self.sent=[]
async def generate(self,config,request,**kwargs):
self.sent.append(copy.deepcopy(request))
assert request['prompt_version']=='investigation_bootstrap_v21'
response=await super().generate(config,dict(request,prompt_version='investigation_project_v18'),**kwargs)
if len(self.sent)==1:
response.document['update']['working_brief']['findings']='PRIVATE_RESPONSE_VALUE'
return response
adapter=Adapter();router=InvestigationToolRouter(project_research=service_for(tool_case))
loop=Investigator(tool_case[0],gateway(adapter),InvestigationConfig(
lean_loop=True,lean_protocol=2,lean_checkpoint_steps=0,bootstrap_json_framing=True),router=router)
first=asyncio.run(loop.step(*CASE))
assert first.status=='REJECTED' and first.code=='LLM_OUTPUT_SCHEMA'
assert not BudgetManager(tool_case[0]).snapshot(*CASE).state['accounts']['H1']['actions']
with PackageReader(first.package) as reader:
context=json.loads(reader.read_bytes(state(first)['active']['context_ref']))
call=json.loads(reader.read_bytes(state(first)['hypotheses']['H1']['notes'][-1]['llm_ref']))
feedback=context['previous_rejection_details']
assert feedback=={'validation_path':'/update/working_brief/findings','validation_fields':[],
'requirement':'match_expected_type_at_validation_path'}
assert call['outcome']['diagnostics']['validation_path']==feedback['validation_path']
assert 'PRIVATE_RESPONSE_VALUE' not in json.dumps(call)
portable=CheckpointStore(Workspace(tmp_path/'portable'));portable.intake(first.package)
result=asyncio.run(Investigator(portable,gateway(adapter),router=router).step(*CASE))
assert result.status=='PROGRESSED' and len(adapter.sent)==2
assert 'match_expected_type_at_validation_path' in json.dumps(adapter.sent[1])
assert '/update/working_brief/findings' in json.dumps(adapter.sent[1])
assert PackageValidator().validate(result.package).valid
+2 -1
View File
@@ -34,8 +34,9 @@ def test_feedback_descends_through_nested_filters_and_root_definitions():
'kind':'database.rows','table':'creature','columns':['guid'],
'filters':[{'column':'id','operator':'eq'}],'offset':0,'limit':20}}
with pytest.raises(InvestigationError) as caught:validate_tool_shape(c,r)
from viano_bug_analysis.analysis.tool_guidance import FILTER_REQUIREMENT
assert caught.value.feedback=={'validation_path':'/next/arguments/filters/0',
'validation_fields':['value'],'requirement':'provide_missing_required_fields'}
'validation_fields':['value'],'requirement':FILTER_REQUIREMENT}
def test_feedback_reports_type_error_without_recommending_missing_fields():