# Skill: Write or Improve Agent Prompts

Use when you need to write a new agent prompt from scratch, improve an existing one,
or understand how the 3-layer prompt system works. Read this fully before touching any `.md` file.

## The 3-Layer Prompt Architecture

The framework composes prompts from three non-overlapping layers:

```
Layer 1 — arccrew/prompts/base.md
    Universal agent rules, shared by ALL agents.
    Contains: thinking process, tool usage rules, response format rules, error handling, hard limits.
    NEVER contains: tool names, domain logic, output schemas, business rules.

Layer 2 — my_project/prompts/{agent}.md
    Agent-specific instructions for ONE agent.
    Contains: role definition, list of available tools, exact JSON output schema.
    NEVER contains: rules already in base.md, {task} placeholder, respond() tool.

Layer 3 — assemble_prompt(variables={...})
    Runtime values injected into layer 2 at call time.
    Contains: workspace_path, current date, user-provided config, dynamic context.
```

`PromptManager.assemble_prompt()` automatically prepends `base.md` to every agent prompt.
This means every agent already has the thinking framework, tool usage rules, and hard limits
without you repeating them.

## What MUST Be in `agent.md`

### 1. Role Statement

One clear sentence describing the agent's purpose in the pipeline:

```markdown
# Researcher — Market Intelligence Analyst

You investigate topics using web search and return structured findings
for the downstream writing agent.
```

### 2. Every Tool Name and What It Does

The agent must know what tools it has. Be explicit:

```markdown
## Tools Available

- `web_search(query)` — Search the web. Use for finding current data, competitor info,
  market trends, or any fact you need to verify.
- `read_file(file_path)` — Read a file from the workspace. Use to access previously
  saved research or context files.
```

Name them exactly as they appear in code. The LLM uses these names to call them.

### 3. Step-by-Step Instructions

Numbered instructions prevent the agent from missing steps:

```markdown
## Instructions

1. Use `web_search` to find at least 3 relevant sources for the topic.
2. For each source, extract key findings — do not summarize from memory.
3. Identify gaps: what is unknown or uncertain from your research?
4. Compile findings into the required JSON format.
5. Output ONLY the JSON — no prose before or after.
```

### 4. Exact JSON Output Schema

Include the schema literally, not just a description:

```markdown
## Output Format

{
  "topic": "string — the topic as you understood it",
  "key_findings": ["finding 1", "finding 2", "finding 3"],
  "sources": ["url1", "url2"],
  "gaps": ["what is unknown"],
  "summary": "2-3 sentence executive summary"
}
Output ONLY the JSON. No markdown, no explanation, no prose.
```

"Output ONLY the JSON" on the last line is load-bearing. Without it, models wrap in prose.

## What Must NOT Be in `agent.md`

### The `{task}` Placeholder

The task is passed at runtime via `run_react(task="...")`. Do not add a `{task}` placeholder
in the prompt file — it will be treated as a missing variable and raise an error or be left blank.

Wrong:
```markdown
## Task
{task}
```

Correct: leave it out. The task string is injected as a user message by `run_react()`.

### The `respond()` Tool

The framework does not require agents to call `respond()`. `create_react_agent` stops
naturally when the LLM has no more tool calls. Mentioning `respond()` causes confusion
and leads to agents calling it incorrectly.

### Rules Already in `base.md`

Do not repeat:
- "Think step by step before acting"
- "If a tool fails, try once with a different approach"
- "Do not expose API keys"
- "Return JSON only"

These are in `base.md` and are already prepended. Repeating them wastes context tokens
and can create contradictions if `base.md` is updated.

## Variable Substitution

Use `{variable_name}` placeholders in `.md` files for values known only at runtime:

```markdown
## Workspace

All files must be saved to: `{workspace_path}`
Today's date is: {current_date}
```

Pass values via `assemble_prompt`:

```python
@property
def system_prompt(self) -> str:
    from arccrew.config import get_settings
    return self.get_prompt_manager().assemble_prompt(
        "my_agent",
        variables={
            "workspace_path": str(workspace),
            "current_date": datetime.now().strftime("%Y-%m-%d"),
        }
    )
```

If a `{variable}` in the `.md` file is not provided in `variables={}`, Python's
`.format_map()` will raise a `KeyError`. Always pass every variable your template uses.

## How `base.md` Works

Read `arccrew/prompts/base.md` before writing any agent prompt. It defines:

- **Agent Identity**: agents are pipeline stages with a single responsibility
- **Thinking Process**: 5-step reasoning framework (understand → plan → verify → minimal scope → explain)
- **Tool Usage**: targeted queries, stop when done, never invent
- **Response Format**: follow agent-specific instructions, JSON only when returning JSON
- **Error Handling**: make assumptions and document them, partial results over silence
- **Hard Limits**: no actions outside role, no invented data, no credential exposure

Your agent prompt must not contradict `base.md`. If you want to override a rule for a
specific agent, be very explicit in the agent prompt that this agent is an exception and why.

## Prompt Testing: Iterate Quickly with `--debug`

The fastest way to test a prompt change:

```bash
# Run with full trace — shows tool calls, results, and final LLM output
LOG_LEVEL=DEBUG arccrew serve --reload

# Or invoke directly
arccrew serve
```

With `--debug`, the log output shows:
- Each tool call with arguments (round-by-round)
- Tool result previews (first 200 chars)
- Final agent response text

If `extract_json()` returns `{}`, the agent's final response is not valid JSON.
Check the debug output for what the agent actually returned, then fix the prompt's
output schema section.

## Common Prompt Problems and Fixes

| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| `extract_json` returns `{}` | Agent not outputting JSON at all | Add "Output ONLY the JSON" to the end of Output Format section |
| Agent calls tools in wrong order | Instructions are not numbered | Number every step explicitly |
| Agent skips tools and answers from memory | Tool descriptions are vague | Add "Use for: ..." to each tool description |
| Agent outputs prose before JSON | Missing "no prose" instruction | Add "No markdown, no explanation, no prose" after schema |
| Agent loops (max_rounds hit) | Task too complex, or confusing instructions | Simplify instructions; break into two agents |
| Agent skips validation tool on some items | Tool not called "for EVERY item" explicitly | Add: "Call `tool_name` for EVERY item before adding it to your output" |
| Agent runs out of rounds mid-list | N items × M tools × 2 exceeds max_rounds | Add round budget hint: "You have ~X rounds. One `tool_name` call per item is enough." Set `AGENTNAME_MAX_ROUNDS` in `.env` |
| Wrong JSON fields | Schema in prompt differs from what the code expects | Match the schema exactly to `extract_json(result)` usage in `execute()` |

## Agent Prompt File Checklist

Before saving a new `agent.md`:

- [ ] Role statement is one clear sentence
- [ ] Every tool the agent will receive is listed with name and purpose
- [ ] If a tool must run on every item in a list, the prompt says "Call `tool_name` for EVERY item"
- [ ] If the agent processes N items, there is a round budget hint (N × M × 2 rounds estimated)
- [ ] Instructions are numbered and sequential
- [ ] JSON output schema is shown literally (not described)
- [ ] "Output ONLY the JSON" is the last line of the Output Format section
- [ ] No `{task}` placeholder
- [ ] No `respond()` tool mentioned
- [ ] No rules duplicated from `base.md`
- [ ] Any `{variable}` placeholders are passed in `assemble_prompt(variables={})`
