feat(explore): den offline generalproeven kan AAPNE en base (MAJOR-1 b, ORDRE 20260902T151931Z)
`--scripted-replies` tok EN konstant streng per rolle, saa ingen skriptet rolle
kunne emittere et `function_call`: maalt 0 verktoeykall / 0 approaches / 1 runde
paa 4/4 baser, mens hvert andre felt i artefaktet saa ut som en kjoering som
hadde virket. En operatoer som foelger dette repoets egen maalestige — «bevis saa
mye som mulig gratis foer det dyre trinnet» — kunne ikke bevise at navigatoeren
aapner noe.
En UTFORSKNINGSROLLE kan naa faa en LISTE av trinn, der et trinn er en tekst
eller ETT verktoeykall (`{"call": "<tool>", "args": {...}}`). Formen utvider den
ENE kanoniske `_inner_get_response`-kroppen (S2.5-konsolideringen) — ikke en
andre klient: rapportens vedlegg-A-`RecordingClient` var maaleinstrument, og en
kopi av kroppen ville drevet fra de tre conftest-dublettene som rir paa den.
Manuset ligger VED SIDEN AV selektoren, ikke inni: en selektor returnerer `str`
ved kontrakt, og et verktoeykall er ikke tekst. Naar manuset er tomt svarer
selektoren som foer, saa et utloept manus degraderer til konstantformen, ikke til
stillhet.
Listeformen NEKTES for debattens roller ved navn (proposeren svarer `generate`s
eget kall, ikke en agent-loekke som kan kalle et verktoey mellom turer), og hvert
malformet trinn nektes ved navn — validering, ALDRI reparasjon
(`write_concept_file`-regelen). Et trinn som slipper gjennom naar
`step["call"]` og krasjer midt i kjoeringen, som er nettopp MAJOR-2-klassen
filas foerste halvdel lukket.
MAALT: ni mutasjoner, alle roede mot HELE suiten, hver med sitt eget vitne +
groenn kontroll 2 failed (F15-diffen, KJENT) / 1087 passed / 5 skipped:
M1 detach recorderen fra explore() OG resume (1 roed) · M2 dropp `tool_calls`
fra `trace_payload` (3) · M3 sorter+dedupliser sinken (2) · M4 registrer navnet
uten basen (3) · M5 nekt listeformen igjen (2) · M6 aksepter lista men emitter
tekst (1) · M7 toler et malformet trinn (1) · M8 la en debattrolle ta
listeformen (1) · M9 dropp ukjent-noekkel-whitelisten (1). Golden
`demo-transcript.stdout` BYTE-UENDRET (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
M7 FALSIFISERTE TESTEN FOERST (repoets vakuoes-gate-klasse, TOLVTE gang):
nekt-testen brukte `{"invoke": "list_bundles"}` og sto GROENN med forms-sjekken
detached — UKJENT-NOEKKEL-grenen fanget den i stedet, saa den nekten som var
under test hadde intet vitne. De to grenene oeves naa av to armer med hver sin
mutasjon (M7 og M9).
Aerlighetsgrense, uttalt: at en LEVENDE modell kaller verktoeyene er fortsatt
ikke bevist (samme klasse som structured-output-grensen) — dette gjoer den
GRATIS halvdelen av stigen ekte, ikke den betalte.
Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
parent
4aa4f9c429
commit
2654219e8c
3 changed files with 267 additions and 14 deletions
|
|
@ -32,6 +32,7 @@ from agent_framework import (
|
|||
BaseChatClient,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Message,
|
||||
ResponseStream,
|
||||
UsageDetails,
|
||||
|
|
@ -399,6 +400,7 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
sink: list[str] | None = None,
|
||||
*,
|
||||
reply_selector: Callable[[str, str], str] | None = None,
|
||||
script: Sequence[str | Mapping[str, Any]] | None = None,
|
||||
role: str = "",
|
||||
default_reply: str = "ok",
|
||||
tokens_per_reply: int = 8,
|
||||
|
|
@ -415,6 +417,11 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
self._select = lambda _prompt, _role: reply
|
||||
else:
|
||||
self._select = lambda _prompt, _role: self._default
|
||||
# The step SCRIPT (MAJOR-1 b), consumed one entry per call and then exhausted — after
|
||||
# which the selector answers as before, so a script that runs out degrades to the constant
|
||||
# form rather than to silence. It lives BESIDE the selector, not inside it: a selector
|
||||
# returns ``str`` by contract, and a tool call is not text.
|
||||
self._script: list[str | Mapping[str, Any]] = list(script or ())
|
||||
self._tokens = tokens_per_reply
|
||||
self.call_count = 0
|
||||
|
||||
|
|
@ -430,22 +437,33 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
if self._sink is not None:
|
||||
self._sink.append(blob)
|
||||
self.call_count += 1
|
||||
reply = self._select(blob, self._role)
|
||||
usage = UsageDetails(total_token_count=self._tokens)
|
||||
step = self._script.pop(0) if self._script else None
|
||||
if step is None or isinstance(step, str):
|
||||
reply = self._select(blob, self._role) if step is None else step
|
||||
# The framework accepts a {"type": "text", ...} dict in an update (its types
|
||||
# under-specify it); a plain string is what the non-stream message has always carried.
|
||||
contents: list[Any] = [reply]
|
||||
update_contents: list[Any] = [{"type": "text", "text": reply}]
|
||||
else:
|
||||
call = Content(
|
||||
"function_call",
|
||||
call_id=f"scripted-{self.call_count}",
|
||||
name=str(step["call"]),
|
||||
arguments=dict(step.get("args") or {}),
|
||||
)
|
||||
contents = [call]
|
||||
update_contents = [call]
|
||||
if stream:
|
||||
|
||||
async def _agen() -> Any:
|
||||
# The framework accepts a {"type": "text", ...} dict here (its types under-specify it).
|
||||
yield ChatResponseUpdate(
|
||||
role="assistant",
|
||||
contents=[{"type": "text", "text": reply}], # type: ignore[list-item]
|
||||
)
|
||||
yield ChatResponseUpdate(role="assistant", contents=update_contents)
|
||||
|
||||
return self._build_response_stream(_agen())
|
||||
|
||||
async def _coro() -> ChatResponse:
|
||||
return ChatResponse(
|
||||
messages=[Message(role="assistant", contents=[reply])],
|
||||
messages=[Message(role="assistant", contents=contents)],
|
||||
response_id="synthetic",
|
||||
usage_details=usage,
|
||||
)
|
||||
|
|
@ -454,7 +472,7 @@ class ScriptedChatClient(OpenAIChatCompletionClient):
|
|||
|
||||
|
||||
def scripted_factory(
|
||||
replies: Mapping[str, str | Callable[[str, str], str]], sink: list[str]
|
||||
replies: Mapping[str, str | Sequence[Any] | Callable[[str, str], str]], sink: list[str]
|
||||
) -> Callable[[str], BaseChatClient]:
|
||||
"""A role-keyed client factory: ``factory("proposer")`` and ``factory("checker")`` each return a
|
||||
fresh ``ScriptedChatClient`` with that role's reply, all sharing ONE ``sink``. MAF stamps the
|
||||
|
|
@ -470,6 +488,11 @@ def scripted_factory(
|
|||
reply = replies[role]
|
||||
if callable(reply):
|
||||
return ScriptedChatClient(sink=sink, role=role, reply_selector=reply)
|
||||
if not isinstance(reply, str):
|
||||
# A step LIST (MAJOR-1 b): the only form that can make a scripted role emit a tool
|
||||
# call, and therefore the only form under which an offline rehearsal can prove the
|
||||
# navigator opens anything.
|
||||
return ScriptedChatClient(sink=sink, role=role, script=list(reply))
|
||||
return ScriptedChatClient(reply, sink, role=role)
|
||||
|
||||
return factory
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue