# תבנית לסוכן AI ראשון עם Claude Agent SDK

מקור: המדריך "בניית סוכן AI ראשון עם Claude Agent SDK" באתר אוטומציה (otomatzia.com/guides/first-ai-agent-claude-agent-sdk).

## 1. המשימה במשפט אחד

- קלט:
- פלט:
- מה נחשב "גמור" (בדיקה שאפשר להריץ):

## 2. בדיקת התאמה למשימה ראשונה

- [ ] המשימה מייגעת וחוזרת על עצמה
- [ ] אפשר לבטל כל תוצאה (הסוכן עובד על עותק)
- [ ] אפשר לבדוק את התוצאה מיד
- [ ] אין שליחה ללקוחות, תשלום, פרסום או מחיקה

## 3. תדריך (system_prompt)

```text
You <role>.

Goal: <one sentence>.

Done means:
- <measurable check 1>
- <measurable check 2>
- checklist.md has no unchecked items.

Rules:
- Work only inside <folder>. Never delete, move or rename files.
- If an input cannot be processed, skip it and list it under "Skipped".
- Keep checklist.md updated: one "- [ ]" line per part of the task.
- Do not end a turn with a plan for the next step. Take the step.
- Stop and ask only when you cannot continue without the user.
```

## 4. הרשאות ומעקות

| הגדרה | ערך להרצה הראשונה | נבדק |
|---|---|---|
| tools | Read, Glob, Grep, Write (רק הכלים המובנים שהסוכן רואה) | [ ] |
| allowed_tools | אותה רשימה, כדי שירוצו בלי לעצור לאישור | [ ] |
| disallowed_tools | Bash, WebFetch | [ ] |
| permission_mode | acceptEdits (על עותק בלבד) | [ ] |
| max_turns | 30 עד 40 לכל הודעת משתמש, כולל תזכורת | [ ] |
| max_budget_usd | סכום שהחלטתם מראש | [ ] |
| setting_sources | [] (ללא קובצי הגדרות משתמש, פרויקט ומחשב מקומיות) | [ ] |
| strict_mcp_config | True (רק שרתי MCP שהוגדרו כאן) | [ ] |
| hook מסוג PreToolUse | מאפשר כתיבה רק לשני קובצי הפלט של ההרצה | [ ] |

## 5. בדיקת סיום

- מספר המשכים אוטומטיים מרבי: 2 או 3
- מה קורה כשנשארו פריטים פתוחים: עצירה ובדיקה אנושית
- איפה נשמר הלוג של ההרצה:

## 6. יומן הרצות

| תאריך | subtype | total_cost_usd | num_turns | פריטים פתוחים | הערות |
|---|---|---|---|---|---|
|  |  |  |  |  |  |

ה-hook מגביל רק את כלי Write ו-Edit. הוא אינו sandbox של מערכת ההפעלה ואינו מגביל קריאה או כלי MCP מותאמים. מריצים את הדוגמה בתיקייה מבודדת עם עותקי נתונים וללא מידע רגיש. מספרים בדוח דורשים בדיקה מול CSV, גם כשהרשימה מסומנת כגמורה.

## 7. agent.py המלא

הקובץ שבנינו במדריך, בסדר הנכון ועם קריאת הרצה אחת. הגרסה המקורית נבדקה מול API ב־5 באוקטובר 2026 עם claude-agent-sdk 0.2.163. התיקונים כאן נבדקו מקומית עם SDK זה ותגובות מדומות, ללא מדידת API חדשה. מתקינים עם `pip install claude-agent-sdk==0.2.163`. כל הרצה יוצרת תיקיית agent-output-* חדשה לצד הסקריפט, בלי למחוק או לדרוס פלט קודם. צרו תיקייה `exports` עם קובצי CSV לדוגמה, הגדירו `ANTHROPIC_API_KEY` והריצו `python agent.py`.

```python
import asyncio
from pathlib import Path
from tempfile import mkdtemp
from typing import Any

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    HookContext,
    HookMatcher,
    ResultMessage,
    create_sdk_mcp_server,
    tool,
)

WORKSPACE = Path(__file__).resolve().parent
OUTPUT_DIR = Path(mkdtemp(prefix="agent-output-", dir=WORKSPACE))
REPORT = OUTPUT_DIR / "report.md"
CHECKLIST = OUTPUT_DIR / "checklist.md"

TASK = (
    f"Read every CSV file in ./exports and write {REPORT} with total sales "
    "per month and the 3 largest customers by revenue."
)

BRIEF = f"""You prepare a monthly sales report for a small business.

Goal: create {REPORT} from the CSV files in ./exports.

Done means:
- {REPORT} exists and has one row per month found in the data.
- Every monthly total equals the sum of the matching CSV rows.
- {CHECKLIST} has no unchecked items.

Rules:
- Read inputs only inside ./exports; you may also read {REPORT} and {CHECKLIST}. Never delete, move or rename files.
- If a file cannot be parsed, skip it and list it under "Skipped files".
- Keep {CHECKLIST} updated: one "- [ ]" line per part of the task.
- If you are blocked, add a line "BLOCKED: <reason>" to {CHECKLIST}.
- Do not end a turn with a plan for the next step. Take the step.
- Stop and ask only when you cannot continue without the user."""

# Stand-in for your CRM export; replace with a real lookup
CUSTOMERS = {"C-1001": "Cohen Ltd", "C-1002": "Levi Bakery"}


@tool(
    "lookup_customer",
    "Return the customer name for a customer ID from the CRM",
    {"customer_id": str},
)
async def lookup_customer(args: dict[str, Any]) -> dict[str, Any]:
    name = CUSTOMERS.get(args["customer_id"])
    if name is None:
        # is_error tells the model the call failed, so it can react
        return {
            "content": [{"type": "text", "text": f"Unknown ID {args['customer_id']}"}],
            "is_error": True,
        }
    return {"content": [{"type": "text", "text": name}]}


crm = create_sdk_mcp_server(name="crm", version="1.0.0", tools=[lookup_customer])

async def guard_output_paths(
    input_data: dict[str, Any], tool_use_id: str | None, context: HookContext
) -> dict[str, Any]:
    if input_data["tool_name"] not in ("Write", "Edit"):
        return {}
    raw = input_data["tool_input"].get("file_path") or ""
    path = Path(raw)
    if not path.is_absolute():
        path = WORKSPACE / path
    allowed = {REPORT, CHECKLIST}
    if not raw or path.is_symlink() or path.resolve() not in allowed:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "Write only this run's report.md or checklist.md",
            }
        }
    return {}

options = ClaudeAgentOptions(
    model="claude-opus-5-5",
    cwd=str(WORKSPACE),
    system_prompt=BRIEF,
    mcp_servers={"crm": crm},  # the custom tool from the previous section
    tools=["Read", "Glob", "Grep", "Write", "Edit"],  # the only built-ins the agent sees
    allowed_tools=["Read", "Glob", "Grep", "Write", "Edit", "mcp__crm__lookup_customer"],
    disallowed_tools=["Bash", "WebFetch"],  # removed from the agent's context
    permission_mode="acceptEdits",
    max_turns=40,  # tool-use round trips per user message, including each nudge
    max_budget_usd=3.0,  # cumulative estimated spend across this client run
    effort="medium",  # the default on Opus 5.5, set explicitly so it is visible
    setting_sources=[],  # skip user/project/local filesystem settings
    strict_mcp_config=True,  # only the explicitly configured MCP servers
    hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[guard_output_paths])]},
)

MAX_NUDGES = 2  # Anthropic suggests stopping after 2 or 3 automatic continuations


def open_items() -> list[str]:
    path = CHECKLIST
    if path.is_symlink() or not path.is_file():
        return ["This run has no regular checklist.md file"]
    lines = path.read_text(encoding="utf-8").splitlines()
    if not any(line.startswith(("- [ ] ", "- [x] ")) for line in lines):
        return ["checklist.md has no task lines"]
    items = [line[6:] for line in lines if line.startswith("- [ ] ")]
    if REPORT.is_symlink() or not REPORT.is_file() or not REPORT.read_text(encoding="utf-8").strip():
        items.append("This run has no nonempty regular report.md file")
    return items


def blockers() -> list[str]:
    path = CHECKLIST
    lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() and not path.is_symlink() else []
    return [line for line in lines if line.startswith("BLOCKED:")]


async def main():
    print(f"Output directory for this run: {OUTPUT_DIR}")
    print(f"Limits: max_turns={options.max_turns}, max_budget_usd={options.max_budget_usd}")
    async with ClaudeSDKClient(options=options) as client:
        await client.query(TASK)
        for attempt in range(MAX_NUDGES + 1):
            final = None
            async for message in client.receive_response():
                if isinstance(message, ResultMessage):
                    print(f"Turn ended: {message.subtype}, error: {message.is_error}, cost USD: {message.total_cost_usd}")
                    print(message.result or "")  # the agent's own report
                    final = message
            failed = (final is None or final.is_error or final.subtype != "success"
                      or getattr(final, "terminal_reason", None) not in (None, "completed")
                      or getattr(final, "stop_reason", None) == "refusal")
            if failed:
                print("Stopped for human review: missing, failed or interrupted result")
            remaining = open_items()
            if failed or blockers() or not remaining or attempt == MAX_NUDGES:
                break  # an error or a stated blocker goes to a human
            await client.query(
                "Your checklist still has open items: " + "; ".join(remaining)
                + ". Continue with them. If one is blocked, say what is blocking it."
            )
        print("Open items left:", open_items() or "none")
        print("Blockers:", blockers() or "none")
        print("Review the report totals against the CSV data before using it.")

asyncio.run(main())
```
