Clarify investigation source contracts and recover nested recall
This commit is contained in:
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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user