fix(tools): a filled optional argument that asks for nothing is not a refusal

Models fill every key of a tool schema. Five handlers refused the empty or
mode-irrelevant form of an optional argument with a text that restated the
rule, the retry kept the same key set, and several of those refusals were
recorded as successful calls, so neither the task nor its reflection could
locate the fault.

- escalate: a wait bound on a quiz that does not wait, or 0 on one that does,
  takes the omitted path and the receipt says so; a bound past the task ceiling
  is lowered to it; the ignored bound is never persisted with the quiz.
- knowledge_read: an end past the note is lowered to it, 0..0 reads the whole
  note, one bound alone is allowed; the returned range is the range delivered.
- delegate_wait: the empty pair (0, "") asks for no checkpoint and the wake
  says so; a half-filled pair is refused naming the missing half.
- schedule_followup: a zone beside an absolute run_at is ignored, disclosed
  and not stored.
- get_task_result source ranges: an invalid range still returns no text, now
  with the complete length and the range received, published as an argument
  error instead of ok.

The remaining refusals at these sites are published typed and name the value
received (tools/arg_feedback). The typed-refusal source lint now counts
interpolated marker returns per file, growth-only.
This commit is contained in:
Ouroboros 2026-09-18 21:53:51 +03:00
parent 8d8710e248
commit 520a6f2e32
18 changed files with 314 additions and 39 deletions

View file

@ -72,11 +72,18 @@ def test_native_selector_retains_source_task_and_canonical_drive(tmp_path, monke
@pytest.mark.parametrize('start,end', [(None, 1), (0, None), (True, 2), (0, False), (-1, 2), (2, 2), (0, 1000000)])
def test_completion_reader_preserves_the_existing_strict_range_contract(tmp_path, monkeypatch, start, end):
reg, _ref, _path, _canonical, _execution, _row = source_reader(tmp_path, monkeypatch, 'split_readonly')
result = json.loads(reg.execute('get_task_result', {
typed = reg.execute_result('get_task_result', {
'task_id': 'source-task', 'include_completion_source': True,
'source_start_char': start, 'source_end_char': end,
}))['completion_source']
})
# The text was not returned, so the call is recorded as an argument fault, never `ok`.
assert (typed.status, typed.code) == ('error', 'TOOL_ARG_ERROR')
result = json.loads(typed.text)['completion_source']
assert result['reason'] == 'source_range_invalid' and 'text' not in result
# The refusal carries what a legal range needs: the complete length and what was asked.
# ("range_required" stays the no-range answer; "unavailable" would be false for a readable source.)
assert result['complete_chars'] > 0 and result['requested_range'] == [start, end]
assert 'range_required' not in result and 'status' not in result
@pytest.mark.parametrize('fault', ['digest', 'bytes', 'missing', 'traversal'])