Ein Agentenergebnis kann die richtige JSON-Struktur haben und die Quelle dennoch falsch verstehen. Dieser Leitfaden bietet Python-Entwicklern den vollständigen Weg von einer erlaubten Notizdatei zum lokalen Maßnahmenregister: typisiertes Erzeugerergebnis, Quellen- und Bedeutungsprüfung, menschliche Freigabe exakter Hashes sowie ein Verbraucher, der einen gruppierten Markdown-Bericht schreibt und zurückliest. Verwenden Sie eigene Notizen mit ausdrücklichen Zusagen. Für diesen Leitfaden wurden weder echte Notizen noch ein Modellergebnis verwendet.
Die große Veränderung
- Was sich geändert hat: OpenAI Agents SDK akzeptiert einen Pydantic-
output_type, sodass eine Anwendung ein definiertes Agentenergebnis anfordern und vor der Nutzung erneut prüfen kann. Das Beispiel ergänzt Quellenzitate und einen versionierten Vertrag für dieses Ergebnis. - Warum das wichtig ist: Wer einen Agenten mit einem weiteren Arbeitsschritt verbindet, kann fehlerhaft aufgebaute, unvollständige oder unbelegte Ausgaben stoppen, bevor daraus eine Aufgabenliste wird. Ein Mensch muss weiterhin beurteilen, ob die zitierten Notizen jede Aufgabe, Zuständigkeit und Frist stützen und ob etwas übersehen wurde.
- Worauf zu achten ist: Der Entscheidungspunkt liegt an der Verbrauchergrenze. Diese Anleitung erzeugt ein lokales Sortierregister erst nach Freigabe des exakten Laufs, der Quelle und des Kandidaten. Ein späterer Import in ein Aufgabensystem braucht eigene Befugnisse, Wiederholungsregeln und eine Rückleseprüfung am Ziel.
Eine genehmigte Quelle vorbereiten
Sie benötigen Python 3.12, ein für die Notizen zugelassenes OpenAI-API-Konto, einen Prüfer mit Zugriff auf die Originaldatei und ein Modell, dessen aktuelle Unterstützung strukturierter JSON-Schema-Ausgaben Sie im Konto bestätigt haben. Prüfen Sie Datenbedingungen und aktuelle Preise vor dem Erzeugerlauf. Zugangsdaten gehören weder in Notizen noch in die Freigabedatei. Der einzige Modellaufruf unten ist produce; der Rest verarbeitet lokal.
Speichern Sie die Originalnotizen als UTF-8 unter notes.txt. Sie müssen tatsächliche ausdrückliche Handlungszusagen aus Ihrer Arbeit enthalten. Vergeben Sie eine stabile source_id, etwa Ihre eigene Datensatzkennung, und verändern Sie die Dateibytes bis zur Prüfung und Verarbeitung nicht. Erstellen Sie candidate/ vor dem Erzeugen; die Kandidatendatei muss neu sein. Stellen Sie ein separates Ausgabeverzeichnis für den späteren lokalen Bericht bereit.
Erfassen Sie vor dem Lauf den Hash der Quelldateibytes:
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"Erstellen Sie in einer isolierten Python-3.12-Umgebung recipe-requirements.txt mit genau diesen Zeilen und installieren Sie die Pakete:
openai-agents==0.24.0
pydantic==2.14.1python -m pip install -r recipe-requirements.txt
mkdir candidateDiese Versionen ließen sich am 20. Oktober 2026 gemeinsam in einer lokalen Python-3.12.7-Umgebung installieren. Bestätigen Sie Kompatibilität und Modellverfügbarkeit bei Ihrer Ausführung. Speichern Sie das vollständige Skript unten als action_register_recipe.py im selben Verzeichnis. Es hat zwei Befehle: produce erzeugt Kandidat und Laufbeleg; consume erstellt den Bericht erst nach Validierung und Freigabe.
Den typisierten Erzeuger ausführen
Der Vertrag Extraction verlangt die Version action-register/v1 und den Status ready, partial oder blocked, Quellen-ID und SHA-256 der exakten Dateibytes, Maßnahmen und einen gap_note. Jede Maßnahme benötigt Text, ein exaktes Zitat und einen ab 1 gezählten Quellzeilenbereich, Zuständigkeit und Datum mit möglichem null-Wert sowie complete oder needs_review als Belegstatus. Ein null-Wert bei Zuständigkeit oder Datum bedeutet, dass die Notizen diese nicht nennen. Das Modell darf beides nicht herleiten. Alle Felder sind erforderlich, zusätzliche werden abgelehnt.
Wählen Sie das Modell mit strukturierter Ausgabe ausdrücklich. Geben Sie Ihren erlaubten Pfad und Ihre Kennung an:
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODELLesen Sie candidate/actions.json neben candidate/actions.run.json. Letztere Datei vermerkt UUID des Laufs, Quellen- und Kandidatenhashes, abschließenden Agenten und angefordertes Modell. Bei Fehler entsteht stattdessen ein Diagnosebeleg .failure.json mit angehängter Lauf-ID. Eine Unterbrechung, None oder eine reine Zeichenkettenausgabe oder ein unerwarteter abschließender Agent zählt niemals als Erfolg. Dieser Befehl speichert den wiederaufnehmbaren SDK-Zustand RunState nicht; sein Fehlerbeleg kann den Lauf nicht fortsetzen. Prüfen Sie Anfrage, Kandidat und nachgelagerten Zustand, bevor Sie einen neuen Lauf erwägen.
Bedeutung prüfen und exakten Kandidaten freigeben
Die Anwendung prüft strikte Typen und Pflichtfelder, Quellenidentität, Bereitstatus, mindestens eine vollständige Maßnahme, gültige Zeilenbereiche, das Zitat im genannten Bereich, nicht leere Aufgabe oder Zuständigkeit und kanonische Datumswerte YYYY-MM-DD. Das belegt Struktur und Zitatausschnitt. Es beweist weder eine wirkliche Zusage noch die behauptete Bedeutung von Name oder Datum oder die Vollständigkeit aller Zusagen.
Lesen Sie Originalnotizen und jede vorgeschlagene Maßnahme. Vergleichen Sie Bedeutung, Zuständigkeit, Datum, Zitat und Auslassungen. Klären Sie mehrdeutige Quellen oder Kandidaten durch einen neuen geprüften Lauf; unbelegte, teilweise oder blockierte Kandidaten bleiben ohne Freigabe. Nachdem unveränderte Datei und Kandidat mit actions.run.json abgeglichen sind, schreiben Sie approval.json mit genau diesen sieben Schlüsseln. Übernehmen Sie Lauf-ID und Hashes aus dem geprüften Beleg. Nutzen Sie die echte Prüferkennung und eine gehaltvolle Beschreibung der tatsächlich ausgeführten Prüfung:
{
"approved": true,
"run_id": "COPY-RUN-ID",
"source_id": "OWNED-ID",
"source_sha256": "COPY-SOURCE-SHA256",
"payload_sha256": "COPY-PAYLOAD-SHA256",
"reviewer": "YOUR-REVIEWER-ID",
"review_note": "Describe the source, task, owner, date, quotation and omission checks actually performed."
}approved muss der JSON-Boolesche Wert true sein, keine Zeichenkette oder Zahl. Alle anderen Werte müssen Zeichenketten sein; Prüfer und Notiz dürfen nicht leer sein. Zusätzliche Felder scheitern. Die Freigabe bindet einen Kandidaten und eine Quellrevision, keine spätere Neufassung.
Bericht verarbeiten und zurücklesen
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-outputDer Verbraucher gruppiert akzeptierte Maßnahmen unter Assigned with dates, Needs an owner und Needs a date. Jeder Eintrag enthält Aufgabe, Zuständigkeits- und Datumsstatus, Zitat und Quellzeilen. Er schreibt action-register-<payload-hash-prefix>.md sowie eine benachbarte .receipt.json mit Lauf-, Quellen-, Kandidaten- und Berichtshashes. Öffnen Sie beide Dateien, vergleichen Sie zitierte Maßnahmen mit den Notizen und prüfen Sie report_sha256 gegen die Bytes der Markdown-Datei. Ein gültiger Beleg besagt nur, dass das lokale Ergebnis erstellt und zurückgelesen wurde; er erlaubt keinen Schreibzugriff auf ein externes Aufgabensystem.
Der Verbraucherbefehl gibt die tatsächlichen Berichts- und Belegpfade aus. Verwenden Sie diese für die Byteprüfung:
python -c "from pathlib import Path; import hashlib,json,sys; report=Path(sys.argv[1]); receipt=json.loads(Path(sys.argv[2]).read_text(encoding='utf-8')); assert hashlib.sha256(report.read_bytes()).hexdigest()==receipt['report_sha256']; print('report hash matches receipt')" PATH-PRINTED-FOR-REPORT PATH-PRINTED-FOR-RECEIPTPrüfen Sie den Ablehnungspfad nach Erhalt eines Kandidaten mit einem klar synthetischen Strukturbeispiel. Kopieren Sie sein JSON in eine andere Datei, entfernen Sie ein beliebiges Pflichtfeld der ersten Maßnahme und rufen Sie consume mit dieser fehlerhaften Datei und einem neuen leeren Ausgabeverzeichnis auf. Er muss vor der Dateierstellung scheitern. Das Beispiel ist keine sachliche Extraktion und darf nicht freigegeben werden. Die folgende lokale Prüfung entfernt etwa evidence_quote aus einer Kopie:
python -c "import json; from pathlib import Path; p=json.loads(Path('candidate/actions.json').read_text(encoding='utf-8')); p['actions'][0].pop('evidence_quote'); Path('candidate/SYNTHETIC-malformed.json').write_text(json.dumps(p), encoding='utf-8')"
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/SYNTHETIC-malformed.json --approval approval.json --out synthetic-rejection-outputDer zweite Befehl muss scheitern und das Ausgabeverzeichnis fehlen. Nutzen Sie das Beispiel nicht, wenn der echte Kandidat keine erste Maßnahme enthält; dann ist er bereits unvollständig für diesen Verbraucher. Prüfen Sie bei ungewissem Schreiben den deterministischen Bericht und Beleg vor einer Wiederholung. Identische vorhandene Bytes können wiederverwendet werden; abweichende stoppen zur Klärung. Geänderte Quelle oder Schemaversion brauchen eine neue Prüfung. Wiederholen Sie niemals einen Modellaufruf nur wegen unklarer lokaler Verbraucherantwort.
Vollständige Python-Anleitung
Kopieren Sie dieses Skript unverändert nach action_register_recipe.py:
"""Private, bounded recipe. No model call occurs unless `produce` is explicitly run."""
import argparse
import hashlib
import json
import uuid
from datetime import date
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, ValidationError
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
class Action(StrictModel):
action_text: StrictStr
evidence_quote: StrictStr
line_start: StrictInt
line_end: StrictInt
owner: StrictStr | None
due_date: StrictStr | None
evidence_status: Literal["complete", "needs_review"]
class Extraction(StrictModel):
schema_version: Literal["action-register/v1"]
status: Literal["ready", "partial", "blocked"]
source_id: StrictStr
source_sha256: StrictStr
actions: list[Action]
gap_note: StrictStr | None
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def canonical(payload: Extraction) -> bytes:
return json.dumps(payload.model_dump(mode="json"), sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
def check(payload: Extraction, source_id: str, source: bytes) -> None:
if payload.source_id != source_id or payload.source_sha256 != digest(source):
raise ValueError("source identity or revision mismatch")
lines = source.decode("utf-8").splitlines()
if payload.status == "ready" and (not payload.actions or payload.gap_note is not None):
raise ValueError("ready requires actions and no gap")
if payload.status != "ready":
raise ValueError("partial or blocked result cannot reach consumer")
for index, item in enumerate(payload.actions, 1):
if item.evidence_status != "complete":
raise ValueError(f"action {index}: evidence needs review")
if not item.action_text.strip() or not item.evidence_quote.strip():
raise ValueError(f"action {index}: empty task or evidence")
if not 1 <= item.line_start <= item.line_end <= len(lines):
raise ValueError(f"action {index}: invalid source line range")
segment = "\n".join(lines[item.line_start - 1:item.line_end])
if item.evidence_quote not in segment:
raise ValueError(f"action {index}: quotation absent from source span")
if item.owner is not None and not item.owner.strip():
raise ValueError(f"action {index}: blank owner")
if item.due_date is not None:
try:
if date.fromisoformat(item.due_date).isoformat() != item.due_date:
raise ValueError()
except ValueError as exc:
raise ValueError(f"action {index}: due date must be YYYY-MM-DD") from exc
def report(payload: Extraction) -> str:
"""Application consumer: a triage register grouped by missing ownership/date."""
groups = [
("Assigned with dates", lambda x: x.owner is not None and x.due_date is not None),
("Needs an owner", lambda x: x.owner is None),
("Needs a date", lambda x: x.owner is not None and x.due_date is None),
]
rows = ["# Action register for " + payload.source_id, "", "Review artifact; source meaning was approved separately.", ""]
for heading, predicate in groups:
rows += ["## " + heading, ""]
matches = [(n, a) for n, a in enumerate(payload.actions, 1) if predicate(a)]
for number, action in matches:
rows += [f"{number}. {action.action_text}", f" Owner: {action.owner or 'Unspecified'}; due: {action.due_date or 'Unspecified'}", f" Source lines {action.line_start}-{action.line_end}: {action.evidence_quote}", ""]
if not matches:
rows += ["None.", ""]
return "\n".join(rows)
def load_payload(path: Path) -> Extraction:
return Extraction.model_validate_json(path.read_bytes(), strict=True)
def consume(source_id: str, source_path: Path, payload_path: Path, approval_path: Path, out_dir: Path) -> tuple[Path, Path]:
source = source_path.read_bytes()
payload = load_payload(payload_path)
check(payload, source_id, source)
run = json.loads(payload_path.with_suffix(".run.json").read_text(encoding="utf-8"))
payload_hash = digest(canonical(payload))
if run.get("source_id") != source_id or run.get("source_sha256") != digest(source) or run.get("payload_sha256") != payload_hash or run.get("last_agent") != "Owned-source action extractor" or not str(run.get("run_id", "")).strip():
raise ValueError("producer receipt does not match accepted candidate")
approval = json.loads(approval_path.read_text(encoding="utf-8"))
if (type(approval) is not dict
or set(approval) != {"approved", "run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"}
or approval["approved"] is not True
or any(type(approval[key]) is not str for key in ("run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"))
or not approval["reviewer"].strip() or not approval["review_note"].strip()
or approval["run_id"] != run["run_id"]
or approval["source_id"] != source_id
or approval["source_sha256"] != digest(source)
or approval["payload_sha256"] != payload_hash):
raise ValueError("human source/meaning approval must bind exact source and payload hashes")
body = report(payload).encode("utf-8")
stem = "action-register-" + payload_hash[:16]
out_dir.mkdir(parents=True, exist_ok=True)
target = out_dir / (stem + ".md")
receipt = out_dir / (stem + ".receipt.json")
record = {"run_id": run["run_id"], "source_id": source_id, "source_sha256": digest(source), "payload_sha256": payload_hash, "report_sha256": digest(body), "reviewer": approval["reviewer"], "operation": "local action-triage register"}
encoded = (json.dumps(record, indent=2) + "\n").encode("utf-8")
for path, data in [(target, body), (receipt, encoded)]:
if path.exists():
if path.read_bytes() != data:
raise ValueError(f"uncertain prior output differs: {path}; inspect before retry")
else:
with path.open("xb") as handle:
handle.write(data)
if path.read_bytes() != data:
raise IOError(f"readback failed: {path}")
return target, receipt
def produce(source_id: str, source_path: Path, candidate_path: Path, model: str) -> None:
from agents import Agent, Runner
source = source_path.read_bytes()
source_text = source.decode("utf-8")
if not source_text.strip() or candidate_path.exists() or not candidate_path.parent.is_dir():
raise ValueError("source must contain text; candidate must be new and its parent directory must already exist")
run_id = str(uuid.uuid4())
agent = Agent(
name="Owned-source action extractor",
model=model,
instructions=("Extract only explicit action commitments from the supplied numbered source. "
"Return action-register/v1. Copy an exact quote with matching 1-based line span. "
"Use null for an unstated owner or due date; never infer them. "
"Use partial and needs_review for ambiguous evidence; blocked for unusable input. "
"Do not use tools or hand off."),
output_type=Extraction,
)
numbered = "\n".join(f"{n}: {line}" for n, line in enumerate(source_text.splitlines(), 1))
try:
result = Runner.run_sync(agent, f"Source ID: {source_id}\nSHA-256: {digest(source)}\nNumbered source:\n{numbered}")
if result.interruptions or result.last_agent is not agent or not isinstance(result.final_output, Extraction):
raise ValueError("run interrupted, unfinished, or finished by unexpected agent; this CLI does not persist resumable RunState, so do not resume from its failure receipt")
output = result.final_output
if output.source_id != source_id or output.source_sha256 != digest(source):
raise ValueError("producer returned wrong source identity")
candidate = canonical(output)
with candidate_path.open("xb") as handle:
handle.write(candidate) # Candidate only; consumer still requires review.
receipt = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "payload_sha256": digest(candidate), "last_agent": result.last_agent.name, "model": model}
with candidate_path.with_suffix(".run.json").open("x", encoding="utf-8") as handle:
handle.write(json.dumps(receipt, indent=2) + "\n")
except Exception as exc:
failure = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "error_type": type(exc).__name__, "error": str(exc), "state": "unknown_or_blocked; inspect run and destination before retry"}
with candidate_path.with_name(candidate_path.stem + "." + run_id + ".failure.json").open("x", encoding="utf-8") as handle:
handle.write(json.dumps(failure, indent=2) + "\n")
raise
def main() -> None:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
for name in ("produce", "consume"):
p = sub.add_parser(name)
p.add_argument("--source-id", required=True)
p.add_argument("--source", type=Path, required=True)
if name == "produce":
p.add_argument("--candidate", type=Path, required=True)
p.add_argument("--model", required=True)
else:
p.add_argument("--candidate", type=Path, required=True)
p.add_argument("--approval", type=Path, required=True)
p.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
if args.command == "produce":
produce(args.source_id, args.source, args.candidate, args.model)
else:
print(*consume(args.source_id, args.source, args.candidate, args.approval, args.out), sep="\n")
if __name__ == "__main__":
main()Grenzen und Quellen
Vertrag und lokaler Verbraucher wurden offline mit fünf Tests sowie sieben synthetischen Fällen für simulierte Erzeuger oder Wiederholungen geprüft. Geprüft wurden strikte Schemaerzeugung, Ablehnung vor Dateierstellung, Gruppierung, Hashes und Fehlerbehandlung. Dabei wurde kein Schema an einen Anbieter gesendet, kein Modell aufgerufen, keine echte Notiz extrahiert, keine sachliche menschliche Freigabe eingeholt, kein Bericht aus echter Quelle erzeugt und weder Rechnung noch Qualität gemessen. API-Gebühren hängen vom Konto ab; prüfen Sie Modellkompatibilität, Preise und Datenerlaubnis vor dem Senden.
Als angrenzenden Kontext behandelt BIG CHANGEs Leitfaden zur Wahl einer KI-Aufgabe aus eigener Arbeit die frühere Entscheidung, welche Arbeit getestet werden soll. Dieser Leitfaden beginnt mit einer gewählten erlaubten Extraktion und führt ein typisiertes Ergebnis in einen geprüften lokalen Verbraucher.
Quellen und weiterführende Lektüre
- OpenAI Agents SDK: Agents dokumentiert Pydantics
output_type. Es liefert weder menschliche Freigabe noch lokalen Verbraucher dieser Anleitung. - OpenAI Agents SDK: Results dokumentiert
final_output,last_agent, Unterbrechungen undto_state(). Diese Befehlszeilenanwendung speichert keinen wiederaufnehmbaren Zustand. - OpenAI Agents SDK: Quickstart behandelt Installation und API-Schlüssel. Halten Sie den Schlüssel aus Quelle und Belegen heraus.
- OpenAI Structured Outputs erklärt strikte Schemaregeln und Modellunterstützung. Bestätigen Sie die Unterstützung Ihres Modells.
- Pydantic ConfigDict dokumentiert strikte Validierung und das Verbot zusätzlicher Felder.
- OpenAI Agents SDK 0.24.0 und Pydantic 2.14.1 bezeichnen die lokal installierten Versionsbindungen. Annahme durch den Anbieter wurde nicht getestet.



