에이전트 결과는 JSON 모양이 맞아도 출처를 잘못 읽을 수 있습니다. 이 가이드는 Python 개발자에게 허가된 메모 파일 하나에서 로컬 조치 목록까지의 완전한 경로를 제공합니다. 형식이 지정된 생성 결과, 출처와 의미 확인, 정확한 해시에 대한 사람 승인, 분류된 Markdown 보고서를 쓰고 다시 읽는 소비자를 사용합니다. 명시적 행동 약속이 담긴 자신의 메모를 사용하세요. 준비 과정에 실제 메모나 모델 결과는 쓰지 않았습니다.
핵심 변화
- 달라진 점: OpenAI Agents SDK는 Pydantic
output_type를 받아 애플리케이션이 정해진 결과를 요청하고 사용 전에 재검사할 수 있습니다. 이 예시는 결과에 출처 인용과 버전이 있는 계약을 더합니다. - 중요한 이유: 에이전트를 다음 업무 단계와 연결하는 개발자는 잘못된 형식, 불완전하거나 근거 없는 출력을 조치 목록이 되기 전에 막을 수 있습니다. 인용 메모가 각 작업, 담당자와 날짜를 실제로 뒷받침하는지, 모델이 빠뜨린 것은 없는지 사람 검토는 여전히 필요합니다.
- 주의할 점: 결정 지점은 소비자 경계입니다. 이 절차는 정확한 실행, 출처와 후보를 승인한 뒤에만 로컬 분류 목록을 만듭니다. 나중에 작업 시스템에 가져오려면 별도 권한, 재시도 정책과 목적지 재확인이 필요합니다.
허가된 출처 하나 준비하기
Python 3.12, 메모 처리 권한이 있는 OpenAI API 계정, 원본을 읽을 수 있는 검토자, 계정에서 현재 JSON-schema 구조화 출력을 지원한다고 확인한 모델이 필요합니다. 생성자를 실행하기 전에 데이터 조건과 현재 요금을 확인하세요. 자격 증명을 메모나 승인 파일에 넣지 마세요. 아래 유일한 모델 호출은 produce이며 나머지는 로컬 처리입니다.
원본 메모를 UTF-8로 notes.txt에 저장하세요. 자신의 업무에 관한 실제 명시적 행동 약속이 있어야 합니다. 자신의 기록 식별자 같은 안정적인 source_id를 지정하고 검토 및 소비가 끝날 때까지 파일의 모든 바이트를 그대로 유지하세요. 생성 전에 candidate/를 만드세요. 후보 파일은 새 파일이어야 합니다. 나중의 로컬 보고서에는 별도 출력 디렉터리를 준비하세요.
실행 전 출처 바이트 해시를 기록하세요:
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"격리된 Python 3.12 환경에서 recipe-requirements.txt를 만들고 다음 줄을 정확히 넣어 설치하세요:
openai-agents==0.24.0
pydantic==2.14.1python -m pip install -r recipe-requirements.txt
mkdir candidate이 버전들은 2026년 10월 20일 로컬 Python 3.12.7 환경에 함께 설치됐습니다. 실행 시 호환성과 모델 가용성을 확인하세요. 아래 완전한 스크립트를 action_register_recipe.py로 같은 디렉터리에 저장하세요. 두 명령이 있으며 produce는 후보와 실행 영수증을 만들고 consume는 검증 및 승인 후에만 보고서를 만듭니다.
형식이 지정된 생성자 실행하기
계약 Extraction는 버전 action-register/v1, 상태 ready, partial 또는 blocked, 출처 ID와 정확한 파일 바이트의 SHA-256, 조치 및 gap_note를 요구합니다. 각 조치에는 텍스트, 정확한 인용과 1부터 세는 출처 줄 범위, null을 허용하는 담당자와 날짜, 그리고 complete 또는 needs_review의 근거 상태가 필요합니다. 담당자나 날짜가 null이면 메모에 없다는 뜻입니다. 모델은 이를 추정하지 않도록 지시받습니다. 모든 필드는 필수이며 추가 필드는 거부됩니다.
구조화 출력 모델을 명시적으로 선택하고 허가된 경로와 식별자를 제공하세요:
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODEL다음 candidate/actions.json와 옆의 candidate/actions.run.json를 읽으세요. 뒤의 파일에는 UUID 실행 ID, 출처와 후보 해시, 마지막 에이전트와 요청 모델이 기록됩니다. 실패 시 대신 실행 ID가 붙은 .failure.json 진단 기록을 만듭니다. 중단, None 또는 단순 문자열 최종 출력, 예상하지 않은 마지막 에이전트는 성공으로 간주되지 않습니다. 이 명령은 SDK의 재개 가능한 RunState를 저장하지 않습니다. 실패 영수증으로 실행을 재개할 수 없습니다. 새 실행이 적절한지 결정하기 전에 요청, 후보 및 후속 상태를 살피세요.
의미를 검토하고 정확한 후보 승인하기
애플리케이션은 엄격한 타입과 필수 필드, 출처 식별, 준비 상태, 완전한 조치 한 개 이상, 유효 줄 범위, 해당 범위 내 인용, 빈칸이 아닌 작업 또는 담당자, 표준 YYYY-MM-DD 날짜를 검사합니다. 이는 구조와 인용 위치를 입증할 뿐입니다. 문장이 실제 약속인지, 이름이나 날짜가 후보의 주장과 같은 의미인지, 모든 약속이 발견됐는지는 확인하지 못합니다.
원본 메모와 모든 제안 조치를 읽고 작업 의미, 담당자, 날짜, 인용과 누락을 비교하세요. 출처나 후보의 모호함은 새로 검토한 실행으로 해결하고, 근거 없거나 부분적 또는 차단된 후보는 승인하지 마세요. 변경 없는 파일과 후보가 actions.run.json와 맞는지 확인한 뒤 approval.json를 정확히 다음 일곱 키로 작성하세요. 확인한 영수증에서 실행 ID와 해시를 복사하세요. 검토자의 실제 식별자와 실제 확인 사항에 대한 충분한 설명을 쓰세요:
{
"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는 JSON 불리언 true여야 하며 문자열이나 숫자여서는 안 됩니다. 다른 값은 모두 문자열이며 검토자와 메모가 비면 안 됩니다. 추가 필드는 실패합니다. 승인은 후보 하나와 출처 수정본에 묶이며 이후 재작성에는 적용되지 않습니다.
소비하고 보고서 다시 읽기
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-output소비자는 수락한 조치를 Assigned with dates, Needs an owner 및 Needs a date로 묶습니다. 각 항목에는 작업, 담당자·날짜 상태, 인용과 출처 줄이 있습니다. action-register-<payload-hash-prefix>.md와 그 옆에 실행, 출처, 후보 및 보고서 해시가 있는 .receipt.json를 씁니다. 두 파일을 열어 보고서가 인용한 조치를 메모와 비교하고 report_sha256를 Markdown 파일 바이트와 대조하세요. 유효 영수증은 로컬 산출물을 만들고 다시 읽었다는 뜻일 뿐, 외부 작업 시스템에 쓰기를 승인하지 않습니다.
소비 명령은 보고서와 영수증의 실제 경로를 출력합니다. 이 경로로 바이트를 확인하세요:
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후보를 얻은 뒤 명확히 합성임을 밝힌 구조 예제로 거부 경로를 확인하세요. JSON을 별도 파일에 복사하고 첫 조치의 필수 필드 하나를 제거한 뒤 consume를 잘못된 파일과 새 빈 출력 디렉터리로 호출하세요. 산출물 생성 전에 실패해야 합니다. 예제를 사실 추출로 취급하거나 승인하지 마세요. 예를 들어 다음 로컬 검사는 사본에서 evidence_quote를 제거합니다:
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두 번째 명령은 실패하고 출력 디렉터리는 없어야 합니다. 실제 후보에 첫 조치가 없다면 예제를 사용하지 마세요. 이미 소비자에게 불완전한 후보입니다. 쓰기 결과가 불확실하면 재시도 전에 결정적인 보고서와 영수증을 살피세요. 동일한 기존 바이트는 재사용할 수 있지만 다르면 조정을 위해 멈춥니다. 출처나 스키마 버전이 바뀌면 새 검토가 필요합니다. 로컬 소비자 응답이 불분명하다는 이유만으로 모델 호출을 반복하지 마세요.
완전한 Python 절차
이 스크립트를 그대로 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()한계 및 출처
스크립트 계약과 로컬 소비자는 다섯 오프라인 테스트 및 일곱 합성 모의 생성자·재시도 사례로 검사했습니다. 엄격한 스키마 생성, 산출물 생성 전 거부, 분류 출력, 해시와 실패 처리를 확인했습니다. 제공자에게 스키마를 제출하거나 모델을 호출하거나 실제 메모를 추출하거나 사실적인 사람 승인을 받거나 실제 출처 보고서를 만들거나 요금 및 품질을 측정하지 않았습니다. API 요금은 계정에 따라 다릅니다. 출처를 보내기 전에 현재 호환성, 가격과 데이터 허가를 확인하세요.
관련 맥락으로 BIG CHANGE의 자신의 업무에서 AI 작업을 선택하는 가이드는 어떤 업무를 시험할지에 관한 앞선 결정을 다룹니다. 이 가이드는 선택된 허가된 추출 작업에서 출발해 형식이 지정된 결과 하나를 확인된 로컬 소비자에 전달합니다.
출처 및 추가 자료
- OpenAI Agents SDK: Agents는 Pydantic
output_type를 설명합니다. 이 절차의 사람 승인이나 로컬 소비자를 제공하지 않습니다. - OpenAI Agents SDK: Results는
final_output,last_agent, 중단 및to_state()를 설명합니다. 이 CLI는 재개 가능한 상태를 저장하지 않습니다. - OpenAI Agents SDK: Quickstart는 설치와 API 키 설정을 다룹니다. 키는 출처 및 영수증에 넣지 마세요.
- OpenAI Structured Outputs는 엄격한 스키마 규칙과 모델 지원을 설명합니다. 선택 모델의 지원을 확인하세요.
- Pydantic ConfigDict는 엄격한 검증과 추가 필드 금지를 설명합니다.
- OpenAI Agents SDK 0.24.0 및 Pydantic 2.14.1은 로컬 설치된 고정 버전입니다. 제공자 측 수용은 시험하지 않았습니다.



