פועל אחד, משטח צרOne verb, narrow surface
הכלי הראשון שכתבנו ל-builder נקרא manage_project. הוא קיבל שדה action עם הערכים create, update, delete, archive, fork, ו-rename, וכל אחד מהם דרש שדות אחרים. המודל נכשל בו ב-23% מהקריאות — בעיקר כי הוא היה מערבב שדות בין פעולות (fork_from בקריאת create).
פיצלנו לחמישה כלים: create_project, update_project, archive_project, rename_project, fork_project. כל אחד עם 2-4 שדות בלבד. אחוז הכישלונות ירד ל-2%. ה-system prompt לא השתנה. הסכמה היא זו שלימדה את המודל מה אפשר ומה אסור.
{
"name": "create_project",
"description": "Open a NEW project for the current user. Fails if a project with the same slug already exists. Use update_project to modify an existing project.",
"input_schema": {
"type": "object",
"properties": {
"slug": { "type": "string", "pattern": "^[a-z0-9-]{3,40}$" },
"display_name": { "type": "string", "maxLength": 80 },
"template": { "type": "string", "enum": ["blank", "landing", "shop"] }
},
"required": ["slug", "display_name"]
}
}הכלל: אם אתה מוסיף action או mode כפרמטר, עצור. כנראה הצנעת שלושה כלים מאחורי שם אחד.
Our first builder tool was called manage_project. It took an action field with values create, update, delete, archive, fork, and rename, each requiring different other fields. The model failed on 23% of calls — usually by mixing fields across actions (fork_from on a create call).
We split it into five tools: create_project, update_project, archive_project, rename_project, fork_project. Each has 2-4 fields. Failure rate dropped to 2%. The system prompt did not change. The schema taught the model what was legal.
{
"name": "create_project",
"description": "Open a NEW project for the current user. Fails if a project with the same slug already exists. Use update_project to modify an existing project.",
"input_schema": {
"type": "object",
"properties": {
"slug": { "type": "string", "pattern": "^[a-z0-9-]{3,40}$" },
"display_name": { "type": "string", "maxLength": 80 },
"template": { "type": "string", "enum": ["blank", "landing", "shop"] }
},
"required": ["slug", "display_name"]
}
}The rule: if you find yourself adding action or mode as a parameter, stop. You have probably hidden three tools behind one name.
סכמה מחמירה היא חלק מה-promptA strict schema is part of the prompt
סכמת הקלט אינה רק validation. היא הוראה. כשהמודל רואה "enum": ["blank", "landing", "shop"], הוא לא ממציא ecommerce. כשהוא רואה "pattern": "^[a-z0-9-]{3,40}$", הוא לא יוצר slug עם רווחים. כל אילוץ ש-JSON Schema תומך בו הוא הוראה ש-LLM לא צריך לקרוא בעברית.
השתמשו ב-enum כשיש קבוצה סגורה. השתמשו ב-pattern כשיש פורמט. השתמשו ב-minLength ו-maxLength כדי לסמן ציפיות (display_name של 80 תווים אומר "כותרת קצרה", לא תיאור). השתמשו ב-required בקפידה — כל שדה אופציונלי הוא הזמנה למודל לדלג עליו גם כשהוא קריטי.
{
"name": "set_colors",
"description": "Set the brand color palette. Both fields required; the model should not guess accent from primary.",
"input_schema": {
"type": "object",
"properties": {
"primary": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
"accent": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
},
"required": ["primary", "accent"],
"additionalProperties": false
}
}additionalProperties: false חובה. בלעדיו המודל יוסיף שדות מומצאים (secondary, background) שיעברו את ה-validator ואחר כך תיתקעו עם undefined.
The input schema is not just validation. It is instruction. When the model sees "enum": ["blank", "landing", "shop"], it does not invent ecommerce. When it sees "pattern": "^[a-z0-9-]{3,40}$", it does not produce a slug with spaces. Every constraint JSON Schema supports is an instruction the LLM does not have to read in prose.
Use enum for closed sets. Use pattern for formats. Use minLength and maxLength to signal expectation (a display_name capped at 80 says "short title", not description). Be deliberate with required — every optional field is an invitation to skip it even when it matters.
{
"name": "set_colors",
"description": "Set the brand color palette. Both fields required; the model should not guess accent from primary.",
"input_schema": {
"type": "object",
"properties": {
"primary": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
"accent": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
},
"required": ["primary", "accent"],
"additionalProperties": false
}
}additionalProperties: false is mandatory. Without it the model will add invented fields (secondary, background) that pass the validator and blow up later as undefined.
פלט מובנה, לא טקסט חופשיStructured output, not free text
כלי שמחזיר "OK, deployed to https://..." כסטרינג גורם למודל הבא בשרשרת לעשות regex על תוצאה של עצמו. זה שביר. כלי טוב מחזיר אובייקט עם שדות שמיים שאפשר לקרוא:
{
"ok": true,
"project_id": "prj_8a7c",
"url": "https://prj-8a7c.hiveagent.co",
"build_ms": 4180,
"warnings": [
{ "code": "missing_favicon", "hint": "call set_favicon" }
]
}שלושה רעיונות חשובים בפלט הזה. ראשית, ok כ-boolean ראשון — המודל לא צריך לפענח את הטון. שנית, warnings זה מערך של אובייקטים עם code מובנה ו-hint טקסטואלי. ה-code מאפשר ל-runtime לבדוק תנאים אוטומטית, ה-hint מנחה את המודל מה לעשות. שלישית, מטא-דאטה כמותית (build_ms) — אם המודל מקבל מספרים, הוא לפעמים יזכיר אותם בתשובה ויעזור לדבג.
הימנעו מ-data או result שמכיל אובייקט גנרי שאתם משאירים open-ended. כל שדה צריך שם משלו, ברמה העליונה. זה מקל גם על debugging — לוגים של tool calls נקראים כמו טבלה.
A tool that returns "OK, deployed to https://..." as a string forces the next model in the chain to regex its own output. That is brittle. A good tool returns an object with named fields the runtime can read:
{
"ok": true,
"project_id": "prj_8a7c",
"url": "https://prj-8a7c.hiveagent.co",
"build_ms": 4180,
"warnings": [
{ "code": "missing_favicon", "hint": "call set_favicon" }
]
}Three ideas in this shape. First, ok as the leading boolean — the model does not have to parse tone. Second, warnings is an array of objects with a structured code and a textual hint. The code lets the runtime check conditions automatically; the hint tells the model what to do. Third, quantitative metadata (build_ms) — when the model sees numbers, it sometimes echoes them in its summary, which helps debugging.
Avoid a generic data or result blob that you leave open-ended. Every field deserves its own name at the top level. It also makes logs readable: tool-call logs become a table.
אידמפוטנטיות מצילה loopsIdempotency saves loops
במהלך build אחד ראינו את ה-builder קורא ל-create_project שלוש פעמים — שניים מהם בגלל retry של ה-CLI אחרי תקלת רשת, אחת בגלל ש-Anthropic החזיר tool_use כפול אחרי שדה JSON-as-text. אם הכלי לא היה אידמפוטנטי, היו לנו שלושה פרויקטים רפאים.
איך עושים זאת. שיטה אחת — dedup לפי hash על שדות מהותיים, עם TTL. ב-Hive זה build dedup TTL 60s על (user_id, project_id_or_name_hash). השיטה השנייה — מפתח אידמפוטנטיות מפורש שהמודל מספק (idempotency_key בסכמה) והשרת זוכר את התוצאה למשך X דקות.
async function createProject(args, ctx) {
const key = sha256(`${ctx.user_id}:${args.slug}`);
const existing = await db.oneOrNone(
`SELECT * FROM idempotency_log
WHERE key=$1 AND created_at > now() - interval '60 seconds'`,
[key]);
if (existing) return existing.result;
const result = await actuallyCreate(args, ctx);
await db.none(`INSERT INTO idempotency_log (key, result) VALUES ($1, $2)`,
[key, result]);
return result;
}אידמפוטנטיות עוברת מטה ל-tool design: כתבו את הכלי כך שקריאה כפולה תחזיר את אותה תוצאה ולא תייצר resource חדש. זה הופך retry פוליסי לבטוח, וזה עוזר למודל להתאושש מטעויות שלו בלי שאתם תשלמו על כל אחת.
In a single build we saw the builder call create_project three times — two because the CLI retried after a network blip, one because Anthropic returned a duplicate tool_use block after a JSON-as-text fragment. Without idempotency we would have three ghost projects.
Two approaches. One — dedup by a hash over essential fields, with a TTL. In Hive this is a 60-second TTL over (user_id, project_id_or_name_hash). Two — an explicit idempotency key the model supplies (idempotency_key in the schema) and the server caches the result for X minutes.
async function createProject(args, ctx) {
const key = sha256(`${ctx.user_id}:${args.slug}`);
const existing = await db.oneOrNone(
`SELECT * FROM idempotency_log
WHERE key=$1 AND created_at > now() - interval '60 seconds'`,
[key]);
if (existing) return existing.result;
const result = await actuallyCreate(args, ctx);
await db.none(`INSERT INTO idempotency_log (key, result) VALUES ($1, $2)`,
[key, result]);
return result;
}Idempotency flows back into tool design: write the tool so that a duplicate call returns the same result without creating a new resource. That makes retry policy safe and lets the model recover from its own mistakes without you paying for every one.
שגיאות עם hint לתיקוןErrors that teach the next call
כשכלי נכשל, ה-error message הוא ה-prompt הבא של המודל. "500 Internal Server Error" לא מלמד כלום. הוא יחזור עם אותם args. שגיאה טובה אומרת מה לא בסדר וגם איך לתקן:
{
"ok": false,
"error": {
"code": "slug_taken",
"message": "slug 'my-shop' already belongs to project prj_4f1a",
"hint": "call update_project with project_id=prj_4f1a, or pick a different slug (try 'my-shop-2')"
}
}הקוד הוא לבני אדם וגם ל-runtime. ה-message מתאר את המצב. ה-hint נותן את האקשן הבא — ולעיתים גם הצעה ספציפית. ב-Hive 80% מהפעמים שכלי מחזיר שגיאה עם hint, המודל פותר בקריאה אחת נוספת. בלי hint אנחנו רואים 2-3 ניסיונות עיוורים.
הכלל: כל שגיאה שמטופלת על ידי המשך ה-loop של ה-LLM חייבת לכלול code, message, ו-hint. אם אין hint, אתם מבזבזים turn.
When a tool fails, the error message is the model's next prompt. "500 Internal Server Error" teaches nothing. The model retries with identical args. A good error names the problem and the fix:
{
"ok": false,
"error": {
"code": "slug_taken",
"message": "slug 'my-shop' already belongs to project prj_4f1a",
"hint": "call update_project with project_id=prj_4f1a, or pick a different slug (try 'my-shop-2')"
}
}The code is for humans and the runtime. The message describes the state. The hint names the next action — sometimes with a specific suggestion. In Hive, 80% of the time a tool returns an error with a hint, the model resolves it in one more call. Without a hint we see 2-3 blind retries.
hint. The model will try to fix your code instead of fixing the call. hint is always written from the model's point of view: "call X", "change Y".The rule: every error meant to be handled by the agent loop must carry code, message, and hint. No hint means you wasted a turn.
ה-description נכתב למודל, לא ליוזרThe description is for the model, not the user
description של כלי הוא לא תיעוד API. הוא חלק מה-prompt. כל מילה מבוזבזת שם עולה לכם בכל turn. וכל מילה חסרה גורמת לטעות.
שלושה דברים שצריכים להיות ב-description: (1) מתי להשתמש בכלי, (2) מתי לא להשתמש בו, (3) effects (האם הוא משנה state, או רק קורא). דוגמה לרע ולטוב:
{
"name": "deploy",
"description": "Deploys the project to the sandbox."
}{
"name": "deploy",
"description": "Push the current project files to the user's sandbox container and return the live URL. Mutating: increments build counter and overwrites previous deploy. Call only after at least 3 write_file calls and one set_colors call. Do NOT call to 'preview' — there is no dry-run mode."
}הגרסה השנייה ארוכה פי חמישה אבל מונעת שימוש שגוי שעלה לנו אינסוף turns. ספציפית: "Do NOT call to 'preview'" — סוכנים אהבו לקרוא ל-deploy פשוט כדי לראות איך זה ייראה. תיעוד שלילי שווה את המחיר שלו.
אל תכתבו ב-description "please" או "this tool will allow you to". המודל לא צריך נימוס, הוא צריך הוראות. כתבו בפועל: "Push", "Return", "Fail if".
A tool's description is not API documentation. It is part of the prompt. Every wasted word costs you every turn. Every missing word causes a misuse.
Three things belong in a description: (1) when to use the tool, (2) when not to, (3) effects (does it mutate state or only read). Bad and good:
{
"name": "deploy",
"description": "Deploys the project to the sandbox."
}{
"name": "deploy",
"description": "Push the current project files to the user's sandbox container and return the live URL. Mutating: increments build counter and overwrites previous deploy. Call only after at least 3 write_file calls and one set_colors call. Do NOT call to 'preview' — there is no dry-run mode."
}The second is five times longer but prevented misuse that cost us countless turns. Specifically: "Do NOT call to 'preview'" — agents loved to call deploy just to see how it would look. Negative documentation pays for itself.
Skip "please" and "this tool will allow you to". The model does not need politeness; it needs imperatives. Write in verbs: "Push", "Return", "Fail if".
צ'קליסט לפני שמכניסים כלי לפרודקשןPre-prod checklist for a tool
לפני שאנחנו מוסיפים כלי חדש ל-Hive, הוא חייב לעבור את הצ'קליסט הבא. כל אחד מהפריטים נכשל לפחות פעם אחת בעבר.
- פועל אחד. אין שדה
actionאוmode. - סכמה סגורה.
additionalProperties: false, כל ה-required מסומנים. - אילוצים מפורטים. כל שדה עם
enum,pattern, או טווחים כש-relevant. - פלט מובנה.
{ ok, ...named_fields }. איןdata: any. - שגיאות עם hint. כל error path מחזיר
code,message,hint. - אידמפוטנטיות. אם הכלי משנה state, יש מנגנון dedup לפי hash או
idempotency_key. - Description שלילי. כתוב לפחות מקרה אחד שבו לא להשתמש בכלי.
- תצפית. כל call מתועד ב-DB עם args, latency, ו-result; אנחנו יכולים לשאול "כמה פעמים הכלי הזה החזיר
code=Xהשבוע".
node tools/lint-tools.js src/tools/*.json
# checks all tools against the checklist aboveהצ'קליסט הזה הוא ה-DoD שלנו עבור tool design. כלי שלא עובר אותו לא נכנס ל-tool registry — לא משנה כמה הוא חכם.
Before a new tool ships in Hive, it must pass the checklist below. Every item failed at least once in our history.
- One verb. No
actionormodefield. - Closed schema.
additionalProperties: false, every required field marked. - Constraints declared. Each field with
enum,pattern, or numeric ranges where relevant. - Structured output.
{ ok, ...named_fields }. Nodata: any. - Errors with hints. Every error path returns
code,message,hint. - Idempotency. If the tool mutates state, there is dedup by hash or an explicit
idempotency_key. - Negative description. At least one explicit case of when not to call the tool.
- Observability. Every call logged to the DB with args, latency, result; we can ask "how many times did this tool return
code=Xthis week".
node tools/lint-tools.js src/tools/*.json
# checks all tools against the checklist aboveThe checklist is our DoD for tool design. A tool that fails it does not enter the registry — no matter how clever it is.