エージェント結果は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はローカルに導入した固定バージョンです。プロバイダー側の受理は試していません。



