Clarify investigation source contracts and recover nested recall

This commit is contained in:
Nico committed 2026-10-20 22:42:48 +02:00
1 parent 84c4f7f96c
commit f2b13ae7cd
10 files changed
+302 -2

No files matched your search

@@ -127,6 +127,17 @@ def feedback(error, document):
and action.get('operation') == 'project.query' and 'arguments' not in action):
result.update(validation_path='/next/arguments', validation_fields=['arguments'],
requirement='next.arguments must contain one complete query object, including its query kind; do not flatten query fields into next or infer missing values')
elif (path.startswith('/next') and isinstance(action,dict) and action.get('kind') == 'tool'
and action.get('operation') == 'project.query' and isinstance(action.get('arguments'),dict)
and 'kind' not in action['arguments']):
result.update(validation_path='/next/arguments/kind',validation_fields=['kind'],
requirement='next.arguments.kind is required. Choose a literal query kind; source.search needs text,path_prefix,case_sensitive,offset,limit. Do not guess kind.')
elif (path.startswith('/next') and isinstance(action,dict) and action.get('operation') == 'project.query'
and isinstance(action.get('arguments'),dict) and action['arguments'].get('kind') == 'source.search'):
result['requirement'] = 'source.search requires kind,text,path_prefix,case_sensitive,offset,limit. Supply missing fields; offset >= 0, limit > 0 within configured maximum. Keep intent.'
elif path.startswith('/next') and isinstance(action,dict) and action.get('kind') == 'recall' and 'arguments' in action:
result.update(validation_path='/next',validation_fields=['evidence_refs','page'],
requirement='recall has evidence_refs and optional page directly in next, without arguments; preserve the requested evidence ID and page')
elif '/entity_refs' in path:
result['requirement'] = 'entity_refs must use exact N references from input.entities; raw domain IDs are not references; never invent or guess a mapping'
elif path == '/next/arguments' and isinstance(action,dict) and isinstance(action.get('arguments'),dict) and 'choices' in action['arguments']:
@@ -232,6 +232,9 @@ def project(request, context):
if v2(context):
from .investigation_contract import entity_catalog
view['entities'] = entity_catalog(context)
if context.get('research_guidance_version') == 1:
from .research_guidance import source_access
view['source_access'] = source_access(context)
evidence = context['evidence']
ordered = sorted(evidence)
previous = context.get('previous_result')
@@ -251,6 +254,9 @@ def project(request, context):
if key in old: body[key] = old[key]
result['messages'] = [{'role': 'system', 'content': instructions_v2() if v2(context) else INSTRUCTIONS},
{'role': 'user', 'content': compact(body)}]
if context.get('research_guidance_version') == 1:
from .research_guidance import INSTRUCTIONS as research_instructions
result['messages'][0]['content'] += research_instructions
if context.get('startup_explore'):
result['messages'][0]['content'] += '\nInitial round: assessment=null; choose a tool only. No consolidation or confidence update.'
def refresh():
@@ -249,6 +249,7 @@ class Investigator:
if state['policy'].get('prompt_window_version') == 2:
# Frozen per context: historical requests remain reconstructable.
context['assessment_scope_version'] = 1
context['research_guidance_version'] = 1
if state.get("prompt_protocol") == 2:
context["prompt_version"] = "investigation_project_v2"
context["case_sources"] = interpretation_context["sources"]
@@ -490,8 +491,12 @@ class Investigator:
except (InvestigationError,CaseBuildError,BudgetError,ToolError) as error:
if getattr(error,"code",None) in {"PROJECT_PROVIDER_UNAVAILABLE", "PROJECT_SOURCE_UNAVAILABLE", "PROJECT_DATABASE_UNAVAILABLE", "PROJECT_DATABASE_CREDENTIALS", "PROJECT_DATABASE_DRIVER", "PROJECT_BINDING_CHANGED"}:
return self._research_blocked(state,error.code)
detail = getattr(error,'feedback',None)
if getattr(error,'code',None) == 'TOOL_SOURCE_NOT_DECLARED' and context['policy'].get('prompt_window_version') == 2:
from .research_guidance import namespace_feedback
detail = namespace_feedback(decision.get('next')) or detail
return self._invalid(state,budget,getattr(error,"code","INVESTIGATION_OUTPUT"),outcome.result_ref,
feedback=getattr(error,'feedback',None))
feedback=detail)
active["decision"]=decision;active["llm_ref"]=outcome.result_ref;active["phase"]="executing"
if call['outcome'].get('normalization'):
state['pending_format_notice']=call['outcome']['normalization']['rules'][:16]
@@ -0,0 +1,31 @@
"""Small, versioned source namespaces and causal-method guidance; no bug rules."""
def source_access(context):
refs = context.get('case',{}).get('source_context',{}).get('refs',[])
paths = [v['path'] for v in refs if isinstance(v,dict) and isinstance(v.get('path'),str)]
shown = []
for path in paths:
if len(shown) == 3 or sum(map(len,shown)) + len(path) > 700:
break
shown.append(path)
binding = context['tool_schema'].get('x-project-bindings',{})
return {'repository_available':bool(binding.get('source')),
'repository_operation':'project.query',
'repository_search_fields':['kind','text','path_prefix','case_sensitive','offset','limit'],
'package_operations':['source.find_text','source.find_symbol','source.read_context'],
'package_source_refs':shown,'omitted_package_source_refs':len(paths)-len(shown)}
INSTRUCTIONS = '''
Source namespaces: configured Git uses operation project.query and a complete query object. source.search requires kind,text,path_prefix,case_sensitive,offset,limit. Package source.* operations accept only exact package_source_refs, never repository paths. Do not guess missing kind or translate namespaces silently. Prefer source.context for call/binding analysis when available, then targeted primitives.
Test the proposed mechanism, not just a preferred method name. Before declaring a hook missing, check callers, overrides, indirect delegation and registration. An absent wrapper method alone does not exclude another valid entry point. Existing implementation is neutral unless it establishes a causal defect; retrieve relevant contrary evidence. Distinguish not-yet-read from absent.'''
def namespace_feedback(action):
if (not isinstance(action,dict) or action.get('kind') != 'tool'
or action.get('operation') not in {'source.find_text','source.find_symbol','source.read_context'}):
return None
field = 'source_ref' if action['operation'] == 'source.read_context' else 'source_refs'
return {'validation_path':'/next/arguments/'+field,'validation_fields':[field],
'requirement':'package source.* requires declared package_source_refs, not repository paths. For Git use project.query and a complete source.search query; Do not guess.'}
@@ -103,6 +103,28 @@ def normalize(document, *, request=None):
and isinstance(filters[1],str) and filters[1] in {'eq','ne','lt','le','gt','ge','contains','in'}):
args['filters']=[dict(zip(('column','operator','value'),filters))]
rules.append('flat_filter:/next/arguments/filters')
# A known recall has a single unambiguous container error. Never change its
# evidence ID/page, select a reference, invent a page or move sibling fields.
if (request and enabled(request) and request.get('output_schema',{}).get('x-viano-investigation-window') == 2
and isinstance(action,dict) and set(action) == {'kind','arguments'} and action['kind'] == 'recall'
and isinstance(args,dict) and set(args) in ({'evidence_refs'},{'evidence_refs','page'})
and isinstance(args.get('evidence_refs'),list) and len(args['evidence_refs']) == 1
and isinstance(args['evidence_refs'][0],str) and re.fullmatch(r'E[0-9]{6,}',args['evidence_refs'][0])
and ('page' not in args or type(args['page']) is int and args['page'] >= 0)):
try:
body = json.loads(request['messages'][1]['content'])
visible = set(body['input'].get('evidence',{})) | set(body['input'].get('archive',{}).get('recent_ids',[]))
except (ValueError,KeyError,IndexError,TypeError):
visible = set()
if args['evidence_refs'][0] in visible:
from jsonschema import Draft202012Validator
candidate = copy.deepcopy(result)
candidate['next'] = {'kind':'recall',**copy.deepcopy(args)}
if Draft202012Validator(request['output_schema']).is_valid(candidate):
result = candidate
action = result['next']
args = {}
rules.append('unwrap_recall_arguments:/next')
# A recall is not a newly charged research action. Discard only a well-formed
# but non-applicable budget score, never causal findings, evidence references,
# action intent or missing required values. The original score stays audited.
+1 -1
View File
@@ -62,7 +62,7 @@ def split_document(document, prompt_version):
# Cache its one tool/response contract plus immutable, targeted case data.
current = copy.deepcopy(document)
stable_input = {k:current['input'].pop(k) for k in
('case','behavior','tool_profiles','entities') if k in current['input']}
('case','behavior','tool_profiles','entities','source_access') if k in current['input']}
descriptor = {'encoding':'bounded_investigation_v1','input':stable_input,
'response_contract':current.pop('response_contract')}
descriptor['id'] = hashlib.sha256(json_bytes(descriptor)).hexdigest()
@@ -904,6 +904,9 @@
},
"assessment_scope_version": {
"const": 1
},
"research_guidance_version": {
"const": 1
}
}
}