Python API
Each module's section below is written from its public docstrings by tools/doc_outputs.py, and a test keeps it current, so it reads the same here and on GitHub. For narrative guidance start with Your first frame or Reading a verdict.
midwife.pipeline:situate,plan,run,judge,overall,calls_needed.midwife.frame: frame parsing and hashing.midwife.errors: what Gadfly refuses, a class for each thing to fix, with its exit status.midwife.types:Observation,Record,Budget,Battery.midwife.verdict: categories, levels, evidence and obligations.midwife.interventions: trial planning, prompts and presentation renaming.midwife.batteries.*: the shipped loopback batteries, two critics,local-jsonandtendora.midwife.cli: thegadflycommand, with plan, run, report, check-frame and portfolio.midwife.portfolio: several frames' verdicts counted, never combined.
midwife.pipeline
The pipeline: situate → plan → run → judge (→ overall, over the inputs).
frame = situate(json.load(file))
trials = plan(frame)
result = run(frame, battery, Budget(calls=60))
findings = judge(frame, result.records)
verdict = overall(findings)overall names one category over all the inputs. run is the only stage that calls out. It never starts a call the budget cannot cover, stops between calls when the caller says so, and hands each record to on_record as soon as it exists, so the caller keeps what was spent even if the run stops early. Asked to, it skips the calls a settled trial no longer needs, stops an input once it reads unresolved whatever its other trials give, and makes several calls at once for a battery that takes them.
situate
Another name for parse in midwife.frame:
Check a frame and hash it. The frame keeps its own copy: changing the caller's dict afterwards cannot change what the hash names.
situate = midwife.frame.parseplan
Another name for plan in midwife.interventions:
The trials of one input, in a fixed order.
plan = midwife.interventions.planoverall
Another name for overall in midwife.verdict:
One category over the frame's inputs: outside scope when declared; otherwise coupled but misaligned if any input is; unresolved if any is, or if the relation carried on some inputs and cooled on others; carried or cooled only when every input is. Each input's evidence is kept; what every input leaves open is said once, the rest names its input.
overall = midwife.verdict.overallRunResult
What a run made: its records, the calls it planned, why it stopped early (None when it reached the end of its plan), and how many planned calls it skipped because their trial had settled or their input could read nothing but unresolved.
@dataclass(frozen=True)
class RunResultcalls_needed
The calls a whole run makes: every trial of every input, quorum times each. A budget of this many calls covers it.
calls_needed(frame)run
Every planned trial of every input, quorum times each, in a fixed order. only limits the run to some trial ids (a partial run); the verdict then reads what is there. kept finishes a run that stopped: the calls those records made are not made again (drop a record from it to make its call again), and the verdict reads the two runs together.
run(
frame,
battery,
budget,
*,
cancelled=lambda: False,
on_record=lambda record: None,
only=None,
kept=(),
settle=False,
stop_unresolved=False,
parallel=1,
)settle makes no more calls for a trial whose level can no longer change (settled); the verdict reads it as it would read every call. Under standing.stable the baseline and repeat always run whole, as their split is read across all their calls. stop_unresolved asks the DECISIVE trials first and asks an input nothing more once every answer its other trials could give reads unresolved (unneeded); the verdict reads the category a whole run would, and names the trials not asked. parallel makes up to that many calls at once, for a battery that says it takes them (a concurrent attribute that is true); records are handed on as each call ends, and a call already started is always kept: a first Ctrl-C starts no more calls, keeps those already out, then goes on as KeyboardInterrupt, and a second stops at once.
judge
One finding per input, read from the records alone: the Records of a run, or the dicts a caller kept of them. A record the plan and the frame's battery could not have made is refused.
judge(frame, records)midwife.frame
The frame you declare: the situated relation (L) and its assessment frame (Γ).
A frame is plain JSON. It is read whole, checked, and hashed in a canonical form before any trial runs; the hash travels with every record, so a verdict always names the frame it was read under. Changing any coordinate, the grain included, gives another hash: another frame.
Span
One piece of the body at the frame's grain.
@dataclass(frozen=True)
class SpanFrame
A checked frame. It keeps the canonical JSON its hash is taken of, and every coordinate read from it is a fresh copy: nothing a battery or the caller does with what it reads can change what the hash names.
@dataclass(frozen=True)
class FrameFrame has these properties:
raw:dict[str, Any]name:strinputs:tuple[dict[str, str], ...]instruction:strinterventions:dict[str, Any]allowed:dict[str, Any]battery:dict[str, Any]outside_scope:str | None
canonical
The one serialization a hash is taken of.
canonical(value)digest
The SHA-256 of a value's canonical serialization.
digest(value)schema
The frame's JSON Schema (draft 2020-12), for editors and authoring checks. It describes the shape; parse stays the authority on what only code can check (anchors in the body, trials that change something).
schema()load
Read a frame from a JSON file, then check and hash it as parse does.
load(path)parse
Check a frame and hash it. The frame keeps its own copy: changing the caller's dict afterwards cannot change what the hash names.
parse(raw)midwife.errors
What Gadfly refuses, as Python names it: a class for what the caller must fix (PEP 8), its exit status an attribute of the class (PEP 352), a message that says what went wrong, and notes that say what to do (PEP 678).
UsageError 2 the command or the call: flags, budget, standard input
FrameError 4 the frame
RecordsError 5 records or a report that are not this frame's run
BatteryError 6 the battery: its module, its settings, its serviceA file the command line cannot read or write is Python's own error (PEP 3151), exit 7. Each class is a ValueError, as every refusal was.
What a run did not do as asked, though it went on and kept its records, is a warning (PEP 230), raised with warnings.warn, which a caller may make an error with Python's own filters (-W error::midwife.errors.OpeningWarning):
OpeningWarning 1 calls came back with no answer (1 when every one did)
StoppedWarning 3 the run stopped before its plan's endGadflyError
A refusal: the message says what went wrong, each note what to do.
class GadflyError(ValueError)one_line
Whether a value prints on one line: no line break, nor any other control character a terminal could act on.
one_line(value)is_plain
Whether a value prints as one plain name: no line break, and no colon a label would end at.
is_plain(value)plain
A value as a line prints it: as it is when it is one plain name, else quoted as Python's repr, so it can neither end a label nor start a line.
plain(value)quoted
Values as a message names them: each quoted, as Python's repr does.
quoted(values)noted
Python's own error, with notes that say what to do: its constructor takes none.
noted(error, *notes)UsageError
The command or the call asks for what cannot be done: a flag, a budget, standard input.
class UsageError(GadflyError)FrameError
A frame that cannot be assessed as declared.
class FrameError(GadflyError)RecordsError
Records or a report that are not what this frame's run made.
class RecordsError(GadflyError)BatteryError
A battery that cannot be made, cannot serve this frame as set up, or cannot reach what it needs.
class BatteryError(GadflyError)GadflyWarning
What a run did not do as asked, though it went on and kept its records.
class GadflyWarning(UserWarning)OpeningWarning
Calls came back with no answer, as openings; exit 1 when every call did, as then nothing was read.
class OpeningWarning(GadflyWarning)StoppedWarning
The run stopped before its plan's end, so its records are partial.
class StoppedWarning(GadflyWarning)midwife.types
The types a pipeline passes from stage to stage, and the one a battery implements. Gadfly keeps no state: every value here is returned to the caller, who keeps it (a file, a database, a test fixture) as they see fit.
Observation
What a battery saw: one of the frame's labels, or a typed opening when there is no outcome to read (a refusal, an unreadable answer, a failure). served names the scorer that answered, as the service reports it (a model, its revision, the endpoint), so a record says who read.
@dataclass(frozen=True)
class ObservationRecord
One scorer call: which frame, input, trial and run, what was read and what it cost. A verdict is recomputed from records alone.
@dataclass(frozen=True)
class RecordRecord.as_dict
The record as a plain dict, as a caller keeps it (JSON Lines, say).
as_dict()Battery
A scorer: given the frame and the exact text it must read, one observation. It never sees which trial it is serving. A battery that can serve several calls at once says so with a class attribute concurrent = True; without it, run makes one call at a time.
@runtime_checkable
class Battery(Protocol)Battery.check
Refuse a frame this battery must not serve (raise ValueError).
check(frame)Battery.observe
Read the prompt once and return what it observed, spending at most max_output_tokens (the budget's cap per call) on the answer.
observe(frame, prompt, max_output_tokens)Budget
The most a run may spend: calls scorer calls, each answer capped at output_tokens_per_call, and, if tokens is set, that many output tokens in all. Because every call is capped, a call is never started that could pass the budget; a battery that reports more than the cap has broken that promise, and the run stops after its call.
@dataclass(frozen=True)
class Budgetmidwife.verdict
The verdict, read from the trial records by fixed rules and nothing else.
For each input, each trial's observations are reduced to one level when the frame's quorum agrees, or to none. The rules, in this order (the order of the reference implementation, After the Poem's finding with rules: 2, in scripts/load_bearing.py, https://github.com/mirzahusadzic/after-the-poem):
- unresolved: a planned trial has no agreed level; with
standing.stable, the baseline and repeat sessions do not all agree (a split level cannot show a one-level change); or repeat differs from baseline; - coupled but misaligned: presentation moves it, or placement does (the structure, not the wording, drove it);
- coupled but misaligned: ablation moves it to an observation the frame does not allow (for ordered labels, the wrong way);
- cooled: ablation leaves it as it was, unless a control trial (spans whose removal must move any scorer that reads) left it too: then unresolved, as the scorer cannot tell;
- unresolved: a sham moves it (the change is not specific to the relation);
- carried: otherwise.
Outside scope only when the frame declares the exclusion; never inferred.
With eachAnchor, each anchor is also cut alone. Those trials are evidence only and never change a category: after the ablation moved it, they say whether each anchor carries it, either suffices, or one adds nothing alone; where the ablation left it, they name any anchor that moved it alone.
What the trials do not settle is recorded as an open obligation beside the category:
- carried: no hidden fallback (a bypass trial), presentation invariance and specificity when their trials were not run, and materialization (whether the observation is the form the recipient receives, which the frame must say);
- cooled: discrimination, unless a control moved it;
- outside scope: the independent scope check, which only the receiving owner can make.
Traceability rests on the records: each names its frame, battery, body, prompt and what served it.
Finding
One input's verdict: its category, each trial's agreed level (None where the quorum did not agree), the evidence for the category, and the obligations it leaves open.
@dataclass
class FindingFinding.as_dict
The finding as a plain dict.
as_dict()Overall
The frame's one category over its test domain: every input's.
@dataclass
class OverallOverall.as_dict
The verdict as a plain dict.
as_dict()overall
One category over the frame's inputs: outside scope when declared; otherwise coupled but misaligned if any input is; unresolved if any is, or if the relation carried on some inputs and cooled on others; carried or cooled only when every input is. Each input's evidence is kept; what every input leaves open is said once, the rest names its input.
overall(findings)level
The observation at least agree runs gave, if exactly one did.
level(observations, agree)settled
Whether a trial's level can no longer change: whatever its remaining calls answer, level gives what it gives now. One label at agree that no other can still reach settles it; so does no label able to reach it.
settled(observations, quorum, agree, labels)judge
One finding per input, from the records of one run of one frame. Every planned trial is read; one without records has no level, and one with fewer than quorum has a level only once it is settled, so a stopped run never reads a level its missing calls could change.
judge(frame, records)unneeded
The trials input x need not be asked, given the answers so far: once every answer the rest could give reads unresolved, all those not asked to the end, but each anchor alone while a reading still gets as far as the ablation, where its evidence is read. Empty while an answer could read otherwise, or the answers are too many to read.
unneeded(frame, x, samples)unreachable
What no answers can make this frame read, found before a call: carried or cooled, each with the checks open in every reading that would name it but for them (empty when none comes that near), or the exclusion when the frame is declared outside scope. The rules themselves read every answer each trial could give; None when the readings would be more than OUTCOMES for an input.
unreachable(frame)check_records
Records are read only as the plan and the frame's battery made them: this frame, its battery, its inputs and trials, each trial's own body, runs within the quorum, each run once, and each a label of the frame or an opening. A record copied twice can never make a quorum. run reads the records it is given to keep by the same checks.
check_records(frame, records, bodies=None)allowed_for
What a trial may produce (ablation; bypass, its designated opening; an inversion or input inversion, what it must reach): the labels listed, or, for ordered labels, those below (or above) the baseline.
allowed_for(frame, base, trial="ablation")midwife.interventions
The admissible interventions Δ: each changes or removes the relation and keeps everything else as it was.
Every trial body is derived from the frame alone, so the plan is fixed, and recorded, before a single observation is made; the sham's draw is seeded by the frame's hash, never by the clock.
Trial
One intervention, applied: the body the scorer will read.
@dataclass(frozen=True)
class TrialTrial has these properties:
text:strbody_hash:str
place
The spans with one moved to follow another; every text unchanged.
place(spans, span_id, after)without
The spans, in their order, less those named.
without(spans, removed)rename
Rename each word consistently, whole words only, in one pass: a word is renamed once (a swap stays a swap), and a phrase wins over a word inside it. A capital is kept, and so is a word written in capitals. A word may begin or end in a sign, such as 20% or C++; it is never part of a longer word, nor of a longer run of its edge's sign (C++ is not in C+++).
rename(text, mapping)renamer
rename with its map built once: the table and the pattern are made here, and the function returned applies them to any text.
renamer(mapping)sham_draws
Spans to remove instead of the anchors: as many as the anchors, of the same kinds (or the kind the sham's kind names for an anchor), none cited, none bypassed, none a declared fallback, and no span in two draws. A fallback is declared to carry the relation, so cutting it is no sham. Each is the closest in length still free; spans equally close are drawn among by the frame's own seed. A draw that cannot be matched is left out; the verdict says so.
sham_draws(frame, count)trial_ids
The ids plan gives its trials, in its order, without building any trial's body: what a budget is counted from.
trial_ids(frame)plan
The trials of one input, in a fixed order.
plan(frame)prompt
What the scorer reads: the trial's body and one input, nothing else. Presentation renames the input's words too, so only the wording moves; an inversion changes the relation's sense in the body alone, and an input inversion what is asked alone.
prompt(frame, trial, x)midwife.batteries.critic
What every critic battery shares: one safe way to call a loopback server, and one way to read a label from a JSON answer (the frame's observation map).
loopback
The URL, if it names this machine over plain http on a valid port; else ValueError, which never repeats a user or password the URL holds.
loopback(url, what)opener
No proxy from the environment, no redirect: only the address named is called. Built per call, so no environment read at import can linger.
opener(*handlers)opened
The response to a request, opened through opener(), its status line and headers in within timeout seconds of the call: a server still sending the answer's head then is cut off, and the call raises TimeoutError. The body is read_reply's to bound, and a refusal's read_error's, both by the same deadline.
opened(request, timeout)Late
A reply still coming at its deadline, with the bytes that had arrived: kept beside the opening, never read as an answer.
class Late(TimeoutError)Late has these properties:
text:str. What had arrived, as text, for the record's raw.
wait_at_most
Let the next read on the response's socket wait at most seconds.
wait_at_most(response, seconds)read_reply
A reply as it comes, until it ends or passes the size cap; Late, with what had arrived, if it is still coming at the deadline. Each read waits only what is left of the deadline, so a stall never runs past it.
read_reply(response, deadline, clock)read_error
Up to 4,096 bytes of a refusal's body, read by the deadline: what had arrived by then, or nothing. Each read waits only what is left.
read_error(error, deadline, clock)check_output
Refuse a battery.output that is not {schema, label}, the label a JSON pointer into the answer.
check_output(battery)output_schema
The schema the critic answers to and the JSON pointer to its label.
output_schema(frame)pointed
The value at a JSON pointer (RFC 6901), or KeyError.
pointed(document, pointer)read_label
The label the answer gives at the frame's pointer, or an opening.
read_label(text, frame)label_at
The label at the frame's pointer in an answer already read as JSON.
label_at(answer, frame)midwife.batteries.unified
The unified battery: an LLM critic through the unified model router.
The unified model router serves the Responses API on loopback and maps a model slug to the provider that serves it, with the router's own key: this battery sees only the router. A slug whose provider trains on its inputs, by the router's own list, is sent only when the frame names it exactly under trainsOnInputs.
One battery covers them all; calling a provider's own endpoint is a different battery (a native one). An unprefixed slug is OpenAI's, which the router reaches only with the caller's Codex sign-in: this battery refuses it.
The critic answers in JSON to a schema, by default {"label": <one of the frame's labels>}. A critic of your own (its output schema, as your project already has it) is adapted by saying where in its answer the label is:
"battery": {"kind": "unified", "model": "anthropic/claude-sonnet-5-5",
"reasoning": "low",
"output": {"schema": {...}, "label": "/verdict"}}What this battery promises, whatever the frame says:
- it talks only to a router on this machine (loopback);
- it sends no tools: the router's web search runs on another provider, and a body would leave the model's own provider with it;
- it refuses a model whose provider trains on its inputs unless the frame names that model under
battery.trainsOnInputs, and sends every other call under the router's deny data policy (X-Codex-Router-Data-Policy: deny), so the router refuses a model whose provider trains and reaches only endpoints that keep no data; before the first call it checks that the router lists the policy atGET /health, and refuses to run if not; - it uses no proxy and follows no redirect, so the body and the token go to the loopback address named and nowhere else;
- each call has a deadline (
timeoutseconds for the whole answer), the budget's cap on output tokens, and a cap on what it reads back; - each record names what served it: the model the response names, and the provider the router reports (
X-Codex-Router-Report: provider, asked when the router lists it), or, for one pinned endpoint, that endpoint's tag; never a provider the router calls unknown; endpointspins the endpoints the router may use, in order and without fallbacks, through the router'sX-Codex-Router-Endpoints, so one endpoint reads a quorum; the router must list endpoint pins atGET /health;- the router's token comes from the environment or the router's own env file and is never logged, returned or recorded.
consented
Whether the frame lets this model go where inputs train: its slug, exactly, in the list under trainsOnInputs (a string is no list).
consented(battery)policy
The router's headers each call of this frame sends: the deny data policy, unless the frame names its model under trainsOnInputs.
policy(frame)pinned
The router's header that pins this frame's endpoints, in order and without fallbacks; none when the frame names none.
pinned(frame)capabilities
What the router lists under capabilities at its health address, which takes no token; empty when it lists nothing.
capabilities(url, timeout)payload
The request body. No tools key, ever; the data policy and the endpoints go in the router's headers (policy, pinned).
payload(frame, text, max_output_tokens)parse_stream
The answer's text, its usage, an opening if the call did not complete, and what served it, from the router's server-sent events: the model the response names, then the provider the router reports, or the tag of the one endpoint the call was pinned to, joined by " via ". A stream still coming at deadline (on clock) is cut there, and a connection that times out or fails mid-stream keeps what arrived.
parse_stream(lines, deadline=None, clock=time.monotonic)Unified
battery: {"kind": "unified", "model": "anthropic/claude-sonnet-5-5"}
class UnifiedUnified.check
Refuse, before any call, a frame this battery must not serve: another battery's, an unknown key, an OpenAI slug, tools, a model whose provider trains on its inputs without the frame's consent, or no router token.
check(frame)Unified.observe
One call through the router: the prompt under the frame's instruction, answered to its schema; the label at the frame's pointer, or an opening that says why there is none.
observe(frame, prompt, max_output_tokens)midwife.batteries.egemma
The eGemma battery: Gemma 4 on this machine, through eGemma's /judge.
eGemma reads the prompt under the frame's instruction, with no persona added, and a grammar holds its answer to the schema: by default {"label": <one of the frame's labels>}, or a critic's own schema with the label's place named, as for the unified battery. Nothing leaves this machine, and nothing is cached, so the quorum's runs are fresh samples; at temperature: 0 the battery is close to deterministic. With reasoning: true, Gemma thinks before every answer, and only the answer is held to the schema.
"battery": {"kind": "egemma", "temperature": 0.7}What this battery promises, whatever the frame says:
- it talks only to an eGemma on this machine (loopback), through no proxy and no redirect;
- each call has a deadline (
timeoutseconds for the whole reply), the budget's cap on output tokens, and a cap on what it reads back; with reasoning, the cap is the thought's and the answer's together: the answer keeps ANSWER_TOKENS of it and the thought gets the rest, up to MAX_THOUGHT_TOKENS, and a cap with no room left for a thought is an opening; - it calls one at a time and paces itself to the judge budget eGemma publishes at GET /rate-limits (read once, before the first call); when eGemma is busy anyway (429 or 503) it waits as long as eGemma asks, at most a minute, and tries at most three more times before the call becomes an opening;
- each record names the model and revision eGemma reports; a frame that pins them (
model,revision) gets an opening from any other; - the key comes from the file named by EGEMMA_API_KEY_FILE, else from EGEMMA_API_KEY or WORKBENCH_API_KEY, and is never logged or recorded.
payload
The /judge request: the instruction, the text, the schema, the answer's cap, and the frame's temperature if it names one. With reasoning, the call's cap is split: the answer keeps ANSWER_TOKENS and the thought the rest.
payload(frame, text, max_output_tokens)EGemma
battery: {"kind": "egemma"}
class EGemmaEGemma.read_budget
The judge budget eGemma publishes; without one, only the backoff.
read_budget()EGemma.pace
Wait until one more call fits the budget's window, then count it.
pace()EGemma.check
Refuse, before any call, a frame this battery must not serve: another battery's, an unknown key, a temperature outside 0 to 2, an empty pin, reasoning that is not true or false, or no key; then read the budget eGemma publishes.
check(frame)EGemma.observe
One /judge call, paced to eGemma's budget and retried while it is busy: the label at the frame's pointer, or an opening that says why there is none.
observe(frame, prompt, max_output_tokens)midwife.batteries.local_json
The local-json battery: any evaluator on this machine that answers JSON.
A local classifier, test oracle or retrieval checker that already reads an instruction and a text and answers to a schema needs no adapter of its own: name its address in the frame and this battery calls it with the same care the shipped critics take.
"battery": {"kind": "local-json", "url": "http://127.0.0.1:8765/judge",
"timeout": 30, "output": {"schema": {...}, "label": "/verdict"}}The evaluator receives {"instructions", "input", "schema", "max_tokens"} and answers {"answer", "text", "usage", "served"}: answer is the JSON the label is read from (at /label, or the frame's own pointer), served names what read it.
What this battery promises, whatever the frame says:
- it talks only to the address named, on this machine (loopback), over plain http, through no proxy and no redirect;
- it sends the instruction, the exact prompt, the schema and the answer's cap, and nothing else: no credentials, no tools;
- each call has a deadline (
timeoutseconds for the whole reply, 30 by default) and a cap on what it reads back; - a reply it cannot read is an opening, never a guessed label, and each record names what the evaluator says served it.
payload
The request: the frame's instruction, the exact prompt, the schema the answer is held to, and the answer's cap.
payload(frame, text, max_output_tokens)LocalJSON
battery: {"kind": "local-json", "url": "http://127.0.0.1:8765/judge"}
class LocalJSONLocalJSON.check
Refuse, before any call, a frame this battery must not serve: another battery's, an unknown key, an address off this machine, a timeout that is not a positive number of seconds, or an output that does not point at its label.
check(frame)LocalJSON.observe
One call to the evaluator: the label at the frame's pointer, or an opening that says why there is none.
observe(frame, prompt, max_output_tokens)midwife.batteries.tendora
The tendora battery: Tendora's engine, asked again against a trial's statements.
Tendora's engine server (in its demo package, npm run serve -- --project P --port N) answers POST /ask with statements against exactly the statements given, in their order, and saves nothing. This battery splits a trial's prompt by its documented form (the statements one per line, a line --- between blank lines, then the input), asks the frame's saved question and case again against those statements, and reads the label from the engine's answer.
"battery": {"kind": "tendora", "url": "http://127.0.0.1:8791",
"timeout": 60, "scope": "...", "offline": true}Each input reads Case: … on its first line when there is a case, then Question: …, exactly as the saved check asked it. The engine finds the saved check by the question, the case and the scope, so all three must be the check's own. When a trial has turned what is asked (an input inversion, or a presentation that renames the input's words), the battery finds the declared input that turned into it and names it as the basis, the saved check to ask it against; it never learns the trial or the input's id.
The label, from the engine's answer:
- supported, with one answer whose text is a label: that label; several answers or another text:
another answerwhen the frame lists it; - open, conditional, ambiguous and conflict: the label of the same name when the frame lists it (a proposal one confirmation away is still open);
- limited, needs-reading, a refusal, any status or answer the frame does not list, and a reply out of the engine's form (answers that are not objects with words and a true or false condition, or no
served) are openings, never labels.
Before any call, check refuses what the battery could not ask: no open among the labels, an input not in the form above, a statement with a line break in any trial (the engine is sent one a line), a rename that turns an input out of the form, and two inputs turned into one question.
What this battery promises, whatever the frame says:
- it talks only to the address named, on this machine (loopback), over plain http, through no proxy and no redirect, and sends no credentials;
- it sends the question, the case, the scope, the statements, the offline flag and, for a turned input, the saved check it came from (
basis), and nothing else; the trial's id is never among them; - each call has a deadline (
timeoutseconds for the whole reply, its head included, 60 by default) and a cap on what it reads back: a reply past the cap is an opening, and nothing of it is kept; - each record names the engine version, revision and packs that served it;
- each record keeps the answer, its status and each answer's text and condition, on its first line, then the reply as it came but for its echo of the statements: a long reply is cut at the record's cap, and the answer line holds as many answers as fit in half of it, with
moresaying how many were left out, so it is always whole JSON.
asked
The question and the case an input holds, or None when it is not in the form Case: … (optional, first) then Question: …, each with words in it, and no carriage return.
asked(text)request_body
What the engine is sent for one prompt, or None when the prompt is not in Gadfly's form: the question, the case and the scope, the statements in their order, and the offline flag.
request_body(frame, prompt)turned
Each text a declared input turns into, through the frame's input inversion or presentation, and the declared inputs that turn into it. A text some declared input already is is left out: a swap asks that input's own saved check. Worked out once for a frame.
turned(frame)malformed
Why a reply is not an answer in the engine's form, or None: a status in words, and answers, if any, a list of objects, each with words for its text and true or false for its condition, when it says one.
malformed(reply)label_of
The label the engine's answer gives, or the opening that says why it gives none; a reply out of the engine's form gives none.
label_of(reply, labels)answer_line
The answer alone: its status, and each answer's text and condition, as many as fit in half the record's cap, and how many more there were (more), so the line a record keeps first is always whole JSON.
answer_line(reply)served_by
What a record names as having served the reply: the engine's version, the first 16 characters of its revision, and its packs.
served_by(reply)Tendora
battery: {"kind": "tendora", "url": "http://127.0.0.1:8791"}
class TendoraTendora.check
Refuse, before any call, a frame this battery must not serve: another battery's, an unknown key, an address off this machine, a timeout that is not a positive number of seconds, a scope or offline flag of the wrong type, no open among the labels, a statement with a line break in any trial, an input not in the form Case: … then Question: …, an input inversion or presentation that turns an input out of that form, or two inputs turned into one question.
check(frame)Tendora.observe
One ask of the engine: the label its answer gives, or an opening that says why there is none.
observe(frame, prompt, max_output_tokens)midwife.cli
The command line: plan, run, report, check-frame, portfolio.
uv run gadfly plan examples/upgrade.json
uv run gadfly run examples/upgrade.json \
--battery midwife.batteries.egemma:EGemma \
--calls 40 --out records.jsonl
uv run gadfly report examples/upgrade.json records.jsonl
uv run gadfly check-frame my-frame.json
uv run gadfly portfolio a.json b.jsonIt uses only the public pipeline. run needs an explicit call budget, writes each record as one JSON line the moment it exists, never writes over a file and makes none when it is refused, and says when it stopped early; report keeps a finding's evidence and its open obligations apart. A battery is named module:Class and is imported from the current directory.
load_frame
The frame in a JSON file, or on standard input for -, checked and hashed.
load_frame(path)load_battery
An instance of the battery class module:Class.
load_battery(spec)read_text
A file's text, or standard input's for -; text that is not UTF-8 is a refusal.
read_text(path, refusal)load_json
text as JSON, or a refusal that names its file and where it broke; line is where text starts in the file, when it is one line.
load_json(path, text, refusal, line=None)load_records
The records kept in a JSON Lines file, or on standard input for -.
load_records(path)render_report
The person-readable report: the overall category, then each input's levels, evidence and open obligations, each on its own line.
render_report(frame, records)command_plan
gadfly plan: the trials, what each changes, and the call count, without a call.
command_plan(args)command_run
gadfly run: every planned call the budget covers, each record kept.
command_run(args)command_report
gadfly report: the verdict read from a kept JSON Lines file.
command_report(args)command_check_frame
gadfly check-frame: the frame's shape against the schema (when jsonschema is installed), then the parser's authoritative refusal.
command_check_frame(args)command_portfolio
gadfly portfolio: several reports counted side by side.
command_portfolio(args)parser
The command line's arguments.
parser()main
Run one command. A refusal prints its class and message, then its notes, and exits with its class's status; with --json, the same as one object on standard error. Anything else is a bug: its traceback, and 70.
main(argv=None)refused
Say a refusal as the last lines of a Python traceback, or as JSON, and give its exit status: a GadflyError's own, 7 for a file, 130 for Ctrl-C.
refused(error, as_json)described
A refusal or a warning as JSON: its type, the exit status it gives, its message and notes, and its troubleshooting section.
described(problem, status, message="", anchor="")Parser
The arguments; a mistake in them is a UsageError.
class Parser(argparse.ArgumentParser)Parser.parse_args
The arguments; one no command takes is that command's mistake.
parse_args(args=None, namespace=None)Parser.error
Refuse as a UsageError, in one clause, pointing at the help.
error(message)midwife.portfolio
Several frames' verdicts side by side, without a verdict of their own.
overall reads one frame over its inputs. Different frames have different relations, promises and recipients, and there is no fixed rule that combines them, so a portfolio counts and groups: frames by category, open obligations two or more frames share, and frames whose inputs disagree. It never names a category for the portfolio.
Entry
One frame's overall verdict, as gadfly report --json writes it.
@dataclass(frozen=True)
class Entrysummarize
Counts by category, the frames in each, the obligations frames share, and the frames whose inputs reached different categories.
summarize(entries)entry_from
An entry from a gadfly report --json document (or a bare overall).
entry_from(document)