AI-translated from English; not yet reviewed by a fluent editor.
# Convierte la salida de un agente en un registro de acciones revisado
> Crea un resultado tipado con OpenAI Agents SDK desde tus notas, revisa sus pruebas y significado y pasa el resultado aceptado a un registro Markdown local con comprobación de hashes.
By BIG CHANGE Editorial
Published: 2026-10-20T12:52:38.610Z
Updated: 2026-10-20T12:52:38.610Z
Canonical: https://bigchange.ai/blog/agents-sdk-reviewed-action-register-guide

AI-generated conceptual illustration by BIG CHANGE.
Un resultado de agente puede tener la estructura JSON correcta y aun así interpretar mal una fuente. Esta guía ofrece al desarrollador Python un recorrido completo desde un archivo de notas permitido a un registro local: resultado tipado del productor, comprobaciones de fuente y significado, aprobación humana ligada a hashes exactos y un consumidor que escribe y relee un informe Markdown agrupado. Usa notas propias con compromisos explícitos. No se utilizaron notas reales ni resultados de modelos para preparar la guía.
## El gran cambio
- **Qué cambió:** OpenAI Agents SDK acepta un `output_type` de Pydantic, de modo que una aplicación puede pedir al agente un resultado definido y comprobarlo antes de usarlo. El ejemplo añade citas de la fuente y un contrato versionado alrededor del resultado.
- **Por qué importa:** Un desarrollador que conecta un agente a otro paso puede detener salidas mal formadas, incompletas o sin respaldo antes de convertirlas en acciones. Un revisor aún debe decidir si las notas citadas respaldan cada tarea, responsable y fecha, y si el modelo omitió algo.
- **Qué vigilar:** El punto de decisión está en el límite del consumidor. Esta receta produce un registro local de clasificación solo tras aprobar la ejecución, fuente y candidato exactos. Importarlo después a un sistema de tareas requeriría autoridad propia, política de reintentos y lectura de comprobación en destino.
## Prepara una fuente autorizada
Necesitas Python 3.12, una cuenta API OpenAI autorizada para las notas, un revisor que pueda leer el original y un modelo cuya compatibilidad actual con salida estructurada mediante esquema JSON hayas confirmado en tu cuenta. Comprueba las condiciones de datos y los precios actuales antes de ejecutar el productor. No pongas credenciales en las notas ni en la aprobación. La única llamada al modelo es `produce`; el resto es procesamiento local.
Guarda tus notas originales en UTF-8 en `notes.txt`. Deben contener compromisos de acción explícitos y reales de tu trabajo. Asigna un `source_id` estable, como un identificador de registro propio, y conserva el archivo sin cambiar un solo byte hasta revisarlo y consumirlo. Crea `candidate/` antes de producir; el archivo candidato debe ser nuevo. Prepara un directorio de salida independiente para el informe local posterior.
Registra el hash de los bytes de la fuente antes de ejecutar:
```sh
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"
```
En un entorno Python 3.12 aislado, crea `recipe-requirements.txt` con estas líneas exactas e instálalo:
```text
openai-agents==0.24.0
pydantic==2.14.1
```
```sh
python -m pip install -r recipe-requirements.txt
mkdir candidate
```
Estas versiones se instalaron juntas en un entorno local Python 3.12.7 el 20 de octubre de 2026. Confirma su compatibilidad y la disponibilidad del modelo al utilizar la guía. Guarda el script completo siguiente como `action_register_recipe.py` en el mismo directorio. Tiene dos comandos: `produce` crea un candidato y recibo de ejecución; `consume` construye el informe solo después de validar y aprobar.
## Ejecuta el productor tipado
El contrato `Extraction` exige la versión `action-register/v1`, el estado `ready`, `partial` o `blocked`, el ID de fuente y SHA-256 de los bytes exactos, las acciones y un `gap_note`. Cada acción exige texto, cita exacta y rango de líneas de fuente numerado desde 1, responsable y fecha que pueden ser nulos, y `complete` o `needs_review` como estado de evidencia. Un responsable o fecha nulos significan que las notas no los indican. Se ordena al modelo no inferirlos. Todos los campos son obligatorios y se rechazan los adicionales.
Elige explícitamente el modelo de salida estructurada. Aporta tu ruta e identificador permitidos:
```sh
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODEL
```
Lee `candidate/actions.json` junto a `candidate/actions.run.json`. Este último registra el UUID de ejecución, hashes de fuente y candidato, agente que finalizó y modelo solicitado. Un fallo crea en cambio un registro de diagnóstico `.failure.json` con el ID de ejecución como sufijo. Una interrupción, `None` o una salida final de texto simple, o un agente final inesperado, nunca cuentan como éxito. Este comando no conserva el estado reanudable del SDK, `RunState`; su recibo de fallo no puede reanudar la ejecución. Inspecciona la petición, el candidato y cualquier estado posterior antes de decidir si corresponde ejecutar de nuevo.
## Revisa el significado y aprueba el candidato exacto
La aplicación comprueba tipos estrictos y campos obligatorios, identidad de fuente, estado listo, al menos una acción completa, rangos válidos, presencia de la cita en el rango, tarea o responsable no vacíos y fechas canónicas `YYYY-MM-DD`. Eso demuestra estructura y ubicación de la cita. No establece que una frase sea un compromiso real, que un nombre o fecha signifique lo que afirma el candidato ni que se hayan encontrado todos los compromisos.
Lee las notas originales y cada acción propuesta. Compara significado, responsable, fecha, cita y omisiones. Resuelve la ambigüedad de fuente o candidato mediante una nueva ejecución revisada; deja sin aprobar candidatos sin respaldo, parciales o bloqueados. Tras verificar que el archivo sin cambios y el candidato coinciden con `actions.run.json`, escribe `approval.json` con exactamente estas siete claves. Copia el ID y los hashes del recibo comprobado. Usa el identificador real del revisor y una explicación sustancial de lo que se comprobó:
```json
{
"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` debe ser el booleano JSON `true`, no texto ni número. Los demás valores deben ser cadenas; revisor y nota no pueden estar vacíos. Los campos adicionales fallan. La aprobación vincula un candidato y revisión de fuente, no cualquier reescritura futura.
## Consume y relee el informe
```sh
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-output
```
El consumidor agrupa las acciones aceptadas en **Assigned with dates**, **Needs an owner** y **Needs a date**. Cada entrada contiene tarea, estado de responsable y fecha, cita y líneas de fuente. Escribe `action-register-<payload-hash-prefix>.md` y un archivo vecino `.receipt.json` con hashes de ejecución, fuente, candidato e informe. Abre ambos, compara las acciones citadas con las notas y verifica `report_sha256` frente a los bytes del Markdown. Un recibo válido solo indica que se creó y releyó el archivo local; no aprueba escribir en un sistema de tareas externo.
El comando de consumo imprime las rutas reales del informe y recibo. Úsalas para esta comprobación de bytes:
```sh
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-RECEIPT
```
Comprueba el rechazo con un ejemplo estructural claramente sintético después de obtener un candidato. Copia su JSON a otro archivo, elimina cualquier campo obligatorio de su primera acción e invoca `consume` con ese archivo mal formado y un directorio de salida nuevo y vacío. Debe fallar antes de crear el archivo. Nunca trates este ejemplo como extracción factual ni lo apruebes. Por ejemplo, la siguiente comprobación local elimina `evidence_quote` de una copia:
```sh
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-output
```
El segundo comando debe fallar y el directorio de salida seguir ausente. No uses el ejemplo si el candidato real no tiene primera acción; ya está incompleto para este consumidor. Ante una escritura incierta, inspecciona el informe determinista y el recibo antes de reintentar. Pueden reutilizarse bytes existentes idénticos; los diferentes detienen la operación para conciliación. Una fuente o versión de esquema modificada exige una nueva revisión. Nunca repitas una llamada al modelo solo porque la respuesta del consumidor local no fue clara.
## Receta Python completa
Copia exactamente este script en `action_register_recipe.py`:
```python
"""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()
```
## Límites y fuentes
El contrato y consumidor local se comprobaron sin conexión con cinco pruebas y siete casos sintéticos de productor simulado o reintento. Cubrieron generación de esquema estricto, rechazo antes de crear archivos, agrupación, hashes y fallos. No se envió un esquema a un proveedor, ni se llamó a un modelo, ni se extrajeron notas reales, ni se obtuvo aprobación humana factual, ni se creó un informe con fuente real, ni se midieron factura o calidad. Los cargos de API dependen de tu cuenta; verifica compatibilidad, precios y permiso sobre los datos antes de enviar la fuente.
Para contexto relacionado, la [guía de BIG CHANGE para elegir una tarea de IA entre tu propio trabajo](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide) aborda la decisión previa sobre qué trabajo probar. Esta guía empieza con una extracción seleccionada y permitida y lleva un resultado tipado a un consumidor local comprobado.
### Fuentes y lecturas adicionales
- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/) documenta el `output_type` de Pydantic. No proporciona la aprobación humana ni el consumidor local de esta receta.
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/) documenta `final_output`, `last_agent`, interrupciones y `to_state()`. Esta interfaz de línea de comandos no guarda estado reanudable.
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/) cubre instalación y clave API. Mantén la clave fuera de fuente y recibos.
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) explica las reglas del esquema estricto y modelos compatibles. Confirma la compatibilidad de tu modelo.
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/) documenta la validación estricta y la prohibición de campos adicionales.
- [OpenAI Agents SDK 0.24.0](https://pypi.org/project/openai-agents/0.24.0/) y [Pydantic 2.14.1](https://pypi.org/project/pydantic/2.14.1/) identifican las versiones fijadas que se instalaron localmente. No se probó la aceptación del proveedor.
## Sources
- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/) — Manual oficial de output_type de Pydantic. No implementa la aprobación humana ni el consumidor local del artículo.
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/) — Comportamiento oficial de final_output, last_agent, interrupciones y to_state. La receta no conserva RunState ni ofrece reanudación.
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/) — Contexto oficial de instalación y clave API; no se usó una clave ni una llamada real al proveedor en la preparación.
- [OpenAI Agents SDK: referencia del esquema de salida](https://openai.github.io/openai-agents-python/ref/agent_output/) — Referencia oficial de generación estricta de esquemas; inspeccionar localmente no demuestra aceptación del proveedor.
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) — Reglas estrictas de campos obligatorios y additionalProperties, más compatibilidad; el lector debe verificar su modelo al usarlo.
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/) — Documenta la configuración strict y extra-forbid; la precisión semántica sigue exigiendo revisión humana de la fuente.
- [Versión openai-agents 0.24.0](https://pypi.org/project/openai-agents/0.24.0/) — Versión exacta del SDK instalada en la comprobación sin conexión y aislada con Python 3.12.7; sin llamada al modelo ni factura.
- [Versión Pydantic 2.14.1](https://pypi.org/project/pydantic/2.14.1/) — Versión exacta de validación instalada localmente junto al SDK 0.24.0.
- [BIG CHANGE: Haz un mapa de tu trabajo y elige una primera tarea para probar con IA](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide) — Guía relacionada para elegir una tarea antes de esta integración; no demuestra la corrección de esta receta.
El boletín de BIG CHANGE
La visión global, a tu ritmo.
Historias recientes sobre IA y robótica, cambios que vale la pena observar e ideas prácticas para usar. Elige un informe diario, un resumen semanal o una perspectiva mensual.
Próximo envío programado (UTC): . Recibirás tu primera edición en el próximo envío programado después de confirmar.
Tu privacidad, tu elección.
El almacenamiento necesario ayuda a proteger el sitio y a recordar tus preferencias. Google Analytics opcional permanece desactivado hasta que lo autorices. Puedes leer todas las historias usando solo el almacenamiento necesario. Detalles de privacidad