AI-translated from English; not yet reviewed by a fluent editor.

# エージェント出力を確認済みのアクション台帳に変換する

> 自分のメモから型付きのOpenAI Agents SDK結果を作り、出典の根拠と意味を確認して、受け入れた結果をハッシュの読み戻し検証付きのローカルMarkdown台帳へ渡します。

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

![Conceptual charcoal illustration of one person holding a larger blank ruled source page and a smaller blank candidate page, with an orange reading strip across the source.](https://bigchange.ai/api/media/file/source-candidate-human-review-hero-v2.png)
AI-generated conceptual illustration by BIG CHANGE.

エージェント結果はJSONの形が正しくても出典を誤読することがあります。このガイドはPython開発者に、許可されたメモファイル一つからローカル台帳までの完全な経路を示します。型付きの生成側結果、出典と意味の確認、正確なハッシュに結び付いた人の承認、分類したMarkdown報告を書いて読み戻す消費側を使います。明確な行動の約束がある自分のメモを用意してください。準備に実際のメモやモデル結果は使っていません。

## 大きな変化

- **変わったこと：**OpenAI Agents SDKはPydanticの`output_type`を受け付けるため、アプリケーションは定義済みの結果を要求し、利用前に再確認できます。この例は結果に出典引用とバージョン付き契約を加えます。
- **重要な理由：**エージェントを次の作業へつなぐ開発者は、形式不正、不完全、根拠のない出力をタスク一覧になる前に止められます。ただし、引用したメモが各タスク、担当者、日付を実際に裏付けるか、漏れがないかは人が判断する必要があります。
- **注意点：**判断箇所は消費側との境界です。この手順は正確な実行、出典、候補が承認された後だけローカルの仕分け台帳を作ります。後でタスクシステムへ取り込むには、別の権限、再試行方針、送信先の読み戻しが必要です。

## 許可された出典を一つ準備する

Python 3.12、メモを扱う許可のあるOpenAI APIアカウント、元ファイルを読める確認者、現在のアカウントでJSON Schemaの構造化出力に対応すると確認したモデルが必要です。生成側を実行する前にデータ条件と最新料金を確認してください。認証情報をメモや承認ファイルに入れないでください。以下の唯一のモデル呼び出しは`produce`で、残りはローカル処理です。

元のメモをUTF-8で`notes.txt`に保存します。自分の仕事での実際の明示的な行動の約束を含めてください。自分の記録識別子など、安定した`source_id`を割り当て、確認と消費が終わるまでファイルの各バイトを変えないでください。生成前に`candidate/`を作ります。候補ファイルは新規でなければなりません。後のローカル報告用には別の出力ディレクトリを用意します。

実行前に出典バイトのハッシュを記録します：

```sh
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"
```

分離したPython 3.12環境で`recipe-requirements.txt`を作り、次の内容を正確に記入してインストールします：

```text
openai-agents==0.24.0
pydantic==2.14.1
```

```sh
python -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はメモに記載がないことを意味します。モデルはどちらも推測しないよう指示されます。全項目が必須で、余分な項目は拒否されます。

構造化出力のモデルを明示的に選びます。自分の許可されたパスと識別子を指定してください：

```sh
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とハッシュを写し、確認者の実際の識別子と実施した確認の具体的な説明を使ってください：

```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`はJSONの真偽値`true`でなければならず、文字列や数値ではいけません。他の値はすべて文字列で、確認者と説明は空欄不可です。追加項目は失敗します。承認は一つの候補と出典改訂を結び付けるもので、将来の書き換え全般には及びません。

## 消費して報告を読み戻す

```sh
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ファイルのバイトと照合します。有効な記録が示すのはローカル成果物を作って読み戻したことだけで、外部タスクシステムへの書き込みは承認しません。

消費コマンドは実際の報告と記録のパスを表示します。そのパスで次のバイト確認を行います：

```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
```

候補ができたら、明らかに合成と分かる構造サンプルで拒否経路を確認します。JSONを別ファイルへ写し、最初のアクションの必須項目を一つ除き、`consume`をその不正ファイルと新しい空の出力ディレクトリで呼びます。成果物を作る前に失敗しなければなりません。サンプルを事実の抽出と扱ったり承認したりしないでください。例えば次のローカル確認はコピーから`evidence_quote`を除きます：

```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
```

二つ目のコマンドが失敗し、出力ディレクトリが存在しないままであることを期待します。実際の候補に最初のアクションがなければサンプルは使わないでください。その候補は消費側にとって既に不完全です。書き込みが不確実なら、再試行前に決定的な報告と記録を調べます。同一の既存バイトは再利用できますが、異なる場合は照合のため停止します。出典やスキーマ版が変わったら再確認が必要です。ローカル消費側の応答が曖昧というだけでモデルを再呼び出ししないでください。

## 完全なPython手順

このスクリプトをそのまま`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()
```

## 限界と出典

スクリプトの契約とローカル消費側は、五つのオフラインテストと、生成側の模擬実行または再試行に関する七つの合成ケースで確認しました。厳密なスキーマ生成、成果物作成前の拒否、分類出力、ハッシュ、失敗処理を確認しています。プロバイダーへスキーマを送信せず、モデルも呼ばず、実際のメモも抽出せず、事実上の人の承認も得ず、実出典の報告も作らず、請求額や品質も測定していません。API料金はアカウントによります。送信前に現在のモデル互換性、料金、データ許可を確認してください。

関連する背景として、BIG CHANGEの[自分の業務からAIタスクを選ぶガイド](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide)は何を試すかという前段の判断を扱います。本ガイドは選定済みで許可された抽出タスクから始まり、一つの型付き結果を確認済みのローカル消費側へ渡します。

### 出典と参考資料

- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/)はPydanticの`output_type`を説明します。本手順の人による承認やローカル消費側は提供しません。
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/)は`final_output`、`last_agent`、中断、および`to_state()`を説明します。このCLIは再開可能状態を保存しません。
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/)はインストールとAPIキー設定を扱います。キーを出典や記録に入れないでください。
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs)は厳密なスキーマ規則とモデル対応を説明します。選択モデルの対応を確認してください。
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/)は厳密な検証と余分な項目の禁止を説明します。
- [OpenAI Agents SDK 0.24.0](https://pypi.org/project/openai-agents/0.24.0/)と[Pydantic 2.14.1](https://pypi.org/project/pydantic/2.14.1/)はローカルに導入した固定バージョンです。プロバイダー側の受理は試していません。

## Sources

- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/) — Pydantic output_typeの公式マニュアルです。記事の人による承認やローカル消費側を実装するものではありません。
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/) — final_output、last_agent、中断、to_stateの公式動作です。本手順はRunStateを永続化せず、再開も提供しません。
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/) — インストールとAPIキー設定の公式背景資料です。準備ではキーも実際のプロバイダー呼び出しも使っていません。
- [OpenAI Agents SDK：出力スキーマ参照](https://openai.github.io/openai-agents-python/ref/agent_output/) — 厳密な出力スキーマ生成の公式参照です。ローカル確認はプロバイダーの受理を証明しません。
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) — 必須項目とadditionalPropertiesの厳密な規則およびモデル互換性です。利用時に選択モデルを確認してください。
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/) — strictとextra-forbid設定を説明します。意味の正確さには引き続き人の出典確認が必要です。
- [openai-agents 0.24.0リリース](https://pypi.org/project/openai-agents/0.24.0/) — 分離したローカルPython 3.12.7オフライン確認で導入した正確なSDK固定版です。モデル呼び出しも請求もありません。
- [Pydantic 2.14.1リリース](https://pypi.org/project/pydantic/2.14.1/) — SDK 0.24.0と並べてローカルに導入した正確な検証ライブラリ版です。
- [BIG CHANGE：自分の業務を整理し、AIで試す最初のタスクを選ぶ](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide) — この連携手順に先立つタスク選定の関連ガイドです。本手順の正しさを証明するものではありません。
