Um resultado de agente pode ter a estrutura JSON correta e interpretar mal a fonte. Este guia dá a um programador Python o percurso completo de um ficheiro de notas permitido a um registo local: resultado tipado do produtor, verificações da fonte e do significado, aprovação humana ligada aos hashes exatos e um consumidor que escreve e relê um relatório Markdown agrupado. Use notas próprias com compromissos explícitos. Não foram usadas notas reais ou resultados de modelos na preparação.
A grande mudança
- O que mudou: O OpenAI Agents SDK aceita um
output_typedo Pydantic, pelo que uma aplicação pode pedir um resultado definido ao agente e verificá-lo antes de o usar. O exemplo acrescenta citações da fonte e um contrato com versão ao resultado. - Porque importa: Um programador que liga um agente a outro passo pode travar uma saída malformada, incompleta ou sem fundamento antes de se tornar uma lista de ações. Um revisor continua a ter de avaliar se as notas citadas sustentam cada tarefa, responsável e data e se o modelo omitiu algo.
- O que observar: O ponto de decisão está na fronteira do consumidor. Esta receita produz um registo local de triagem apenas após a aprovação da execução, fonte e candidato exatos. Uma importação posterior num sistema de tarefas exigiria autoridade própria, política de repetição e releitura no destino.
Prepare uma fonte autorizada
Precisa de Python 3.12, uma conta API OpenAI autorizada para as notas, um revisor que leia o original e um modelo cujo suporte atual a saída estruturada por esquema JSON tenha confirmado na conta. Verifique os termos de dados e preços atuais antes de executar o produtor. Não coloque credenciais nas notas ou no ficheiro de aprovação. A única chamada ao modelo abaixo é produce; o restante é processamento local.
Guarde as notas originais em UTF-8 em notes.txt. Devem conter compromissos de ação explícitos e reais do seu trabalho. Atribua um source_id estável, como o identificador do seu registo, e mantenha os bytes do ficheiro inalterados até à revisão e ao consumo. Crie candidate/ antes de produzir; o ficheiro candidato deve ser novo. Disponibilize um diretório de saída separado para o relatório local posterior.
Registe o hash dos bytes da fonte antes da execução:
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"Num ambiente Python 3.12 isolado, crie recipe-requirements.txt com estas linhas exatas e instale:
openai-agents==0.24.0
pydantic==2.14.1python -m pip install -r recipe-requirements.txt
mkdir candidateEstas versões foram instaladas em conjunto num ambiente local Python 3.12.7 em 20 de outubro de 2026. Confirme compatibilidade e disponibilidade do modelo quando executar. Guarde o script completo abaixo como action_register_recipe.py no mesmo diretório. Tem dois comandos: produce cria um candidato e recibo de execução; consume constrói o relatório apenas após validação e aprovação.
Execute o produtor tipado
O contrato Extraction exige a versão action-register/v1, o estado ready, partial ou blocked, o ID da fonte e SHA-256 dos bytes exatos, ações e um gap_note. Cada ação exige texto, citação exata e intervalo de linhas da fonte contado a partir de 1, responsável e data que admitem null, e complete ou needs_review como estado da prova. Responsável ou data null significam que as notas não os indicam. O modelo é instruído a não inferir nenhum. Todos os campos são obrigatórios e os adicionais são rejeitados.
Escolha explicitamente o modelo de saída estruturada. Indique o seu caminho permitido e identificador:
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODELLeia candidate/actions.json ao lado de candidate/actions.run.json. O segundo regista o UUID da execução, hashes da fonte e candidato, agente final e modelo pedido. Uma falha cria em alternativa um registo de diagnóstico .failure.json com o ID de execução como sufixo. Uma interrupção, None ou saída final em texto simples, ou um agente final inesperado, nunca contam como sucesso. Este comando não preserva o estado retomável do SDK, RunState; o recibo de falha não retoma a execução. Examine pedido, candidato e estado posterior antes de decidir se uma nova execução é adequada.
Reveja o significado e aprove o candidato exato
A aplicação verifica tipos estritos e campos obrigatórios, identidade da fonte, estado pronto, pelo menos uma ação completa, intervalos válidos, citação no trecho, tarefa ou responsável não vazios e datas canónicas YYYY-MM-DD. Isso prova a estrutura e a localização da citação. Não estabelece que a frase seja um compromisso real, que nome ou data tenham o significado afirmado ou que todos os compromissos tenham sido encontrados.
Leia as notas originais e cada ação proposta. Compare significado, responsável, data, citação e omissões. Resolva a ambiguidade através de uma nova execução revista; não aprove candidatos sem fundamento, parciais ou bloqueados. Após verificar que o ficheiro inalterado e o candidato correspondem a actions.run.json, escreva approval.json com exatamente estas sete chaves. Copie o ID e hashes do recibo verificado. Use a identificação real do revisor e uma descrição substancial do que foi efetivamente verificado:
{
"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 tem de ser o booleano JSON true, não uma cadeia de texto nem número. Os restantes valores são cadeias de texto; revisor e nota não podem estar vazios. Campos adicionais falham. A aprovação liga um candidato e revisão da fonte, não qualquer reescrita futura.
Consuma e releia o relatório
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-outputO consumidor agrupa ações aceites em Assigned with dates, Needs an owner e Needs a date. Cada entrada contém tarefa, estado de responsável e data, citação e linhas de origem. Escreve action-register-<payload-hash-prefix>.md e um ficheiro vizinho .receipt.json com hashes da execução, fonte, candidato e relatório. Abra ambos, compare as ações citadas com as notas e verifique report_sha256 contra os bytes do Markdown. Um recibo válido só diz que o ficheiro local foi criado e relido; não aprova escrever num sistema externo de tarefas.
O comando de consumo imprime os caminhos reais do relatório e recibo. Use-os para esta verificação de bytes:
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-RECEIPTCom um candidato disponível, verifique a rejeição com um exemplo estrutural claramente sintético. Copie o JSON para outro ficheiro, retire qualquer campo obrigatório da primeira ação e invoque consume com esse ficheiro malformado e um diretório de saída novo e vazio. Tem de falhar antes de criar o ficheiro. Nunca considere o exemplo uma extração factual nem o aprove. Por exemplo, a verificação local abaixo retira evidence_quote de uma cópia:
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-outputO segundo comando deve falhar e o diretório de saída continuar ausente. Não use o exemplo se o candidato real não tiver primeira ação; já é incompleto para este consumidor. Numa escrita incerta, examine relatório determinístico e recibo antes de repetir. Bytes existentes idênticos podem ser reutilizados; divergentes interrompem para conciliação. Uma fonte ou versão de esquema alterada exige nova revisão. Nunca repita uma chamada ao modelo apenas porque a resposta do consumidor local foi pouco clara.
Receita Python completa
Copie este script exatamente para 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()Limites e fontes
O contrato e consumidor local foram verificados offline com cinco testes e sete casos sintéticos de produtor simulado ou repetição. Abrangeram esquema estrito, rejeição antes de criar ficheiros, agrupamento, hashes e falhas. Não houve envio de esquema a fornecedor, chamada a modelo, extração de notas reais, aprovação humana factual, relatório com fonte real ou medição de fatura ou qualidade. A API tem encargos dependentes da conta; verifique compatibilidade, preços e permissão sobre dados antes de enviar a fonte.
Para contexto relacionado, o guia da BIG CHANGE para escolher uma tarefa de IA no próprio trabalho cobre a decisão anterior sobre o que testar. Este guia começa com uma extração selecionada e permitida e leva um resultado tipado a um consumidor local verificado.
Fontes e leituras adicionais
- OpenAI Agents SDK: Agents documenta o
output_typedo Pydantic. Não fornece a aprovação humana nem o consumidor local desta receita. - OpenAI Agents SDK: Results documenta
final_output,last_agent, interrupções eto_state(). Esta interface de linha de comandos não guarda estado retomável. - OpenAI Agents SDK: Quickstart cobre instalação e configuração da chave API. Mantenha a chave fora da fonte e dos recibos.
- OpenAI Structured Outputs explica regras de esquema estrito e modelos suportados. Confirme o suporte do modelo escolhido.
- Pydantic ConfigDict documenta validação estrita e proibição de campos adicionais.
- OpenAI Agents SDK 0.24.0 e Pydantic 2.14.1 identificam as versões fixadas instaladas localmente. A aceitação do fornecedor não foi testada.



