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.