feat(harness): authority, turn contract, tools, clean_agent_preview

Chat/agent harness work: request-authority model, turn contract, tool
policy/parsing/execution, clean_agent_preview streaming/thinking, ask_user
persistence, session probes, and scheduler.

Source: vendor/photocraft (ad1cff44) harness files, 3-way merged onto
upstream/dev (a8c147b2). Harness core files were independently developed
in both lineages; resolution policy documented in pr_work/C_conflicts.md
(vendor kept where ahead, upstream kept where vendor is behind; full delta
preserved in pr_work/C_upstream_delta.patch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-13 13:41:53 +00:00
co-authored by Claude Opus 5.5
parent a8c147b238
commit 364229f2dc
60 changed files with 2778 additions and 696 deletions
+43 -7
View File
@@ -316,7 +316,12 @@ class RequestAuthority:
_BROWSER_READ_ACTIONS = frozenset({"open", "navigate", "snapshot", "text", "read", "find",
"screenshot", "scroll", "back", "forward", "wait", "status", "close", "tabs", "session_info"})
"screenshot", "scroll", "back", "forward", "wait", "status", "close", "tabs", "session_info",
"batch"})
# Interaction classes are admitted on a strict router (spec) surface whenever
# the server itself offered private_browser: the router decides the tool set,
# so requiring the verb in the text would break "browse X then click Y".
_BROWSER_INTERACTION_ACTIONS = frozenset({"click", "fill", "type", "press", "evaluate", "select"})
@dataclass(frozen=True)
@@ -363,7 +368,8 @@ def interpret_request(request_text, *, history=(), workspace=None, active_docume
def create_request_authority(request_text, *, owner=None, session_id=None, workspace=None,
history=(), policy=None, active_document=False,
image_attachment=False, capabilities=None, client_runtime_context=None):
image_attachment=False, capabilities=None, client_runtime_context=None,
spec_surface=False):
"""Deterministic server policy over semantic facts, never schema inventory."""
if not isinstance(request_text, str):
raise TypeError("Authority requires trusted request text")
@@ -372,10 +378,25 @@ def create_request_authority(request_text, *, owner=None, session_id=None, works
families = intent.capabilities
if capabilities is not None:
families |= frozenset(capabilities)
if spec_surface:
# Strict router (spec) surface: agent_loop builds the final tool set in
# src/spec_router.py AFTER this runs, so turn_contract.offered is still
# the compact inventory here. The router already limits schemas to its
# picks + sticky prior tools + ask_user/update_plan, and
# _resolve_tool_blocks drops calls to unoffered tools, so admit every
# family and let the spec surface be the boundary. Selected-tool
# narrowing is skipped for the same reason.
families |= frozenset(FAMILY_TOOLS)
tools = set().union(*(FAMILY_TOOLS.get(f, ()) for f in families))
selected = intent.selected_tools
selected = None if spec_surface else intent.selected_tools
if selected is not None:
tools = tools & set(selected) if families else set(selected)
if "web_search" in tools:
# Search selection narrows to the query tool, but reading a source
# it surfaces is the same bounded read operation. Without the
# sibling reader every discovered result is a dead end and the
# turn fails closed on the first web_fetch of a found URL.
tools.add("web_fetch")
if tools & {"web_search", "web_fetch"}:
tools.add("private_browser")
operation = intent.required_read
@@ -389,13 +410,28 @@ def create_request_authority(request_text, *, owner=None, session_id=None, works
actions = inputs = None
if name == "private_browser":
actions = _BROWSER_READ_ACTIONS
# Explicit interaction intent admits its operation class. A
# browser offered only as static-Web fallback gets no such grant.
if re.search(r"\b(?:browser|browse|private_browser)\b", request_text, re.I):
if spec_surface:
# The strict router itself offered a working browser: admit its
# interaction classes too, not only when the verb is in the text.
actions |= _BROWSER_INTERACTION_ACTIONS
elif re.search(r"\b(?:browser|browse|private_browser)\b", request_text, re.I):
# Explicit interaction intent admits its operation class. A
# browser offered only as static-Web fallback gets no such grant.
actions |= frozenset(action for action in ("click", "fill", "type", "press", "evaluate", "select")
if re.search(r"\b" + action + r"\b", request_text, re.I))
if operation is not None and name == canonical_tool(operation.tool):
inputs = frozenset({ExactOperation.normalize(name, _json(dict(operation.args))).input})
required_args = dict(operation.args)
if set(required_args) <= {"action"}:
# An action-only read ("list my tasks/memories") pins the action
# class, not the exact JSON: a model that adds benign arguments
# (limit/offset/filter) still performs the same admitted read.
# Reads that name an identity keep the exact-input seal below.
required_action = str(required_args.get("action") or "").strip().casefold() or None
if required_action:
actions = (frozenset({required_action}) if actions is None
else actions & frozenset({required_action}))
else:
inputs = frozenset({ExactOperation.normalize(name, _json(required_args)).input})
grants.append(OperationGrant(name, actions, inputs))
authority = RequestAuthority(uuid4().hex, _owner(owner), str(session_id or ""),
str(workspace or ""), tuple(grants))
+70 -11
View File
@@ -1,7 +1,8 @@
"""One presentation gate between agent execution and externally visible prose.
Tool, progress and interaction events stay live. Answer deltas are held until
the generator unwinds so a later replacement cannot conceal an earlier false
Tool, progress and interaction events stay live. Evidence-safe complete
sentences from ordinary answers may stream before the terminal check; execution
answers remain gated so a later replacement cannot conceal an earlier false
claim. This consumes no provider calls. Cancellation closes the inner generator
under the same journal/turn authority; it never emits a successful terminal event.
"""
@@ -123,7 +124,7 @@ def _supported_prose(text: str, ledger: EvidenceLedger, decision: CompletionDeci
or not ledger._supports_verifier_claim(identities, paths)):
why = 'no current passing executable verification supports the claim'
elif claim == 'mutation':
if not ledger._supports_artifact_claim(EvidenceKind.ARTIFACT_MUTATION, paths):
if not ledger._supports_mutation_claim(paths):
why = 'no matching artifact mutation supports the execution claim'
elif claim == 'execution':
# A generic assertion cannot be tied confidently to a receipt.
@@ -167,6 +168,16 @@ def _disclosure(answer: str, ledger: EvidenceLedger) -> str:
return ('\n\n' + summary) if answer.strip() else summary
def _complete_prose_prefix(text: str) -> str:
"""Return complete statement segments, keeping the current sentence pending."""
statements = list(_unquoted_statements(text))
if not statements:
return ''
if statements[-1][0].strip():
statements.pop()
return ''.join(statement for statement, _ in statements)
def _completion_answer(text: str, ledger: EvidenceLedger, decision: CompletionDecision) -> tuple[str, str]:
incomplete = decision.reason if not decision.can_complete and decision.status != CompletionStatus.AWAITING_USER else ''
execution_required = _execution_obligation(ledger.requirements)
@@ -232,6 +243,10 @@ def with_completion_gate(func):
journal.effects = (parent.effects if parent is not None and parent.effects is not None
else EffectLog(journal.run_id))
answer_events: list[dict] = []
streamed_answer_events: set[int] = set()
streamed_safe_text = ''
stream_conflict = False
first_visible_answer_at = None
metrics_events: list[dict] = []
answer = ''
has_final = False
@@ -297,18 +312,36 @@ def with_completion_gate(func):
first_answer_at = perf_counter()
# Boolean thinking=True marks a reasoning-only delta;
# a textual thinking companion must not hide an answer
# delta. Both shapes remain buffered until the gate.
# delta. Thinking remains buffered until terminal review.
if isinstance(data.get('thinking'), str):
answer_events.append({'delta': data['thinking'], 'thinking': True})
thinking_event = {'delta': data['thinking'], 'thinking': True}
answer_events.append(thinking_event)
data = {key: value for key, value in data.items() if key != 'thinking'}
if 'delta' not in data:
continue
if data.get('thinking') is not True and 'delta' in data:
if has_final:
stream_conflict |= bool(streamed_safe_text)
streamed_safe_text = ''
answer = ''
has_final = False
answer += str(data.get('delta') or '')
answer_events.append(data)
if data.get('thinking') is not True and 'delta' in data \
and not _execution_obligation(requirements):
prefix = _complete_prose_prefix(answer)
if prefix:
ledger_now = _ledger(journal, requirements)
decision_now = ledger_now.evaluate(exhausted=exhausted, awaiting_user=awaiting)
safe_prefix, _ = _supported_prose(prefix, ledger_now, decision_now)
if safe_prefix.startswith(streamed_safe_text):
delta = safe_prefix[len(streamed_safe_text):]
if delta:
first_visible_answer_at = first_visible_answer_at or perf_counter()
yield _event({'delta': delta})
streamed_safe_text = safe_prefix
elif streamed_safe_text:
stream_conflict = True
continue
yield chunk
if provider_error and not answer_events and not metrics_events:
@@ -361,7 +394,10 @@ def with_completion_gate(func):
disclosure_only = bool(disclosure) and not (presentation_replaced or reason or unsafe_draft
or filtered_answer != answer)
replaced_answer = not disclosure_only and bool(
presentation_replaced or reason or unsafe_draft or safe_answer != answer)
presentation_replaced or reason or unsafe_draft or safe_answer != answer or stream_conflict
or (provider_error and streamed_safe_text))
if streamed_safe_text and not answer.startswith(streamed_safe_text):
replaced_answer = True
if replaced_answer:
reasoning = [event for event in answer_events if event.get('thinking') is True]
_, unsafe_reasoning = completion_answer(
@@ -369,11 +405,31 @@ def with_completion_gate(func):
replace(presentation_decision, can_complete=True))
if not unsafe_reasoning:
for event in reasoning:
yield _event(event)
yield _event({'type': 'final_response', 'content': safe_answer})
if id(event) not in streamed_answer_events:
yield _event(event)
yield _event({
'type': 'final_response',
'content': safe_answer,
'render_owner': 'streamed',
'replacement_scope': 'turn',
})
else:
for event in answer_events:
yield _event(event)
if event.get('thinking') is True:
if id(event) not in streamed_answer_events:
streamed_answer_events.add(id(event))
first_visible_answer_at = first_visible_answer_at or perf_counter()
yield _event(event)
elif event.get('type') == 'final_response':
if streamed_safe_text:
event = {**event, 'replacement_scope': 'turn'}
first_visible_answer_at = first_visible_answer_at or perf_counter()
yield _event(event)
if not has_final:
remainder = answer[len(streamed_safe_text):] if answer.startswith(streamed_safe_text) else answer
if remainder:
first_visible_answer_at = first_visible_answer_at or perf_counter()
yield _event({'delta': remainder})
if disclosure_only:
yield _event({'delta': disclosure})
if provider_error:
@@ -386,8 +442,11 @@ def with_completion_gate(func):
if ledger.effects:
metadata['effect_assessments'] = [entry['assessment'].to_dict() for entry in ledger.effects]
metadata['completion_gate'] = {
'buffer_seconds': released_at - first_answer_at if first_answer_at is not None else 0,
'first_visible_answer_seconds': released_at - started,
'buffer_seconds': max(0, (first_visible_answer_at or released_at) - first_answer_at)
if first_answer_at is not None else 0,
'evaluation_seconds': released_at - first_answer_at if first_answer_at is not None else 0,
'first_visible_answer_seconds': first_visible_answer_at - started
if first_visible_answer_at is not None else 0,
'additional_provider_calls': 0,
'answer_replaced': replaced_answer,
}