Skip to slide
Chapter 3 · Building the Prompt
17 / 142

CHAPTER 03 · Building the Prompt · 3 / 5

Code MVP: a prompt builder

This PromptBuilder assembles the three fields the way a real harness does. It is intentionally close to what the capstone uses.

"""
chapter 03: building the prompt.
Assemble layered instructions, tool schemas, and a structured history
into a single request dict. Stable content goes first (for caching, Ch 5).
"""
from dataclasses import dataclass, field

@dataclass
class PromptBuilder:
    base_instructions: str = "You are a careful coding agent."
    project_instructions: str = ""        # from AGENTS.md / CLAUDE.md (Ch 9)
    skills_summary: str = ""              # short skill descriptions (Ch 10)
    cwd: str = "."
    shell: str = "bash"
    tools: list = field(default_factory=list)   # JSON schemas (Ch 6)

    def build_instructions(self) -> str:
        # Layered, most-general first, most-specific last.
        layers = [self.base_instructions]
        if self.project_instructions:
            layers.append(f"<project_instructions>\n{self.project_instructions}\n</project_instructions>")
        if self.skills_summary:
            layers.append(f"<skills>\n{self.skills_summary}\n</skills>")
        return "\n\n".join(layers)

    def environment_item(self) -> dict:
        # A user-role item describing where the agent is running.
        return {"role": "user", "type": "message",
                "content": f"<environment_context><cwd>{self.cwd}</cwd>"
                           f"<shell>{self.shell}</shell></environment_context>"}

    def build_request(self, history: list) -> dict:
        """history is a list of structured items (see add_* helpers below)."""
        return {
            # STABLE prefix first: instructions and tools rarely change.
            "instructions": self.build_instructions(),
            "tools": self.tools,
            # VARIABLE content last: environment, then the live conversation.
            "input": [self.environment_item()] + history,
        }

# --- helpers that keep the history STRUCTURED, not a flat string ---
def user_msg(text):
    return {"role": "user", "type": "message", "content": text}

def assistant_msg(text):
    return {"role": "assistant", "type": "message", "content": text}

def tool_call(call_id, name, args):
    return {"role": "assistant", "type": "tool_call",
            "call_id": call_id, "name": name, "args": args}

def tool_result(call_id, output):
    # SAME call_id links this result to the call above, so the model
    # understands which output belongs to which command.
    return {"role": "tool", "type": "tool_result",
            "call_id": call_id, "content": output}

if __name__ == "__main__":
    pb = PromptBuilder(
        project_instructions="Run tests with `pytest -q`. Use 4-space indents.",
        tools=[{"type": "function", "name": "shell",
                "description": "Run a shell command",
                "parameters": {"type": "object",
                               "properties": {"command": {"type": "string"}},
                               "required": ["command"]}}],
        cwd="/repo", shell="zsh",
    )
    history = [
        user_msg("fix the failing test"),
        tool_call("c1", "shell", {"command": "pytest -q"}),
        tool_result("c1", "1 failed, 3 passed"),
    ]
    request = pb.build_request(history)
    import json
    print(json.dumps(request, indent=2))

The output is the exact structure a real API expects: an instructions string, a tools list, and an input list of typed items with call_id linkage. Swap in a real client and this request goes straight to the model.

← → arrow keys work too