Dynamic Prompt Construction
The sectioned prompt from the last lesson is hardcoded. That's fine when there's one project, one sandbox, and one fixed toolset. The minute any of those move, the prompt has to move with them.
Different working directory. Different sandbox backend. A subagent that only gets read and grep. The hardcoded string can't carry any of that. A function can.
Outcome
A buildSystemPrompt(context) function in src/system.ts returns the system prompt string from a typed PromptContext. The agent's instructions are now derived from runtime state instead of pasted in.
Fast Track
- Create
src/system.tswith aPromptContextinterface and abuildSystemPrompt(ctx)function - Compose the prompt from sections, including optional sections like
gitBranchandprojectContext - Call
buildSystemPrompt(...)inindex.tsand pass the result toinstructions
Hands-on Exercise 3.2
Extract the prompt into a typed builder.
Requirements:
- Define
PromptContextwithworkingDirectory,sandboxType,toolNames, optionalgitBranch, optionalprojectContext - Write
buildSystemPrompt(ctx: PromptContext): stringthat returns the sectioned prompt - Make
gitBranchandprojectContextconditional, including their section only when set - In
index.ts, build the context object and callbuildSystemPrompt(ctx)for theinstructionsfield
Implementation hints:
- Push sections into an array and
join("\n")at the end. Plain string concat, no template engine - Conditional sections use a simple
if (ctx.foo) sections.push(...). Don't reach for fancier patterns - Keep
buildSystemPromptpure. Same context in, same prompt out, no side effects. That makes it unit-testable
The shape of context
The prompt depends on runtime state. Bottle that state into one object:
export interface PromptContext {
workingDirectory: string;
sandboxType: string;
toolNames: string[];
gitBranch?: string;
projectContext?: string;
}workingDirectory and sandboxType always apply. toolNames lets the prompt list the tools that are actually wired up (which matters when you give a subagent a subset). gitBranch and projectContext are optional because they're not always knowable.
The builder
export function buildSystemPrompt(ctx: PromptContext): string {
const sections: string[] = [];
sections.push(`You are a coding agent working in: ${ctx.workingDirectory}`);
sections.push(`Sandbox: ${ctx.sandboxType}`);
sections.push(`
# Agency
- USE your tools. Read files, search code, run commands, then answer.
- Do NOT explain what you WOULD do. Actually do it.
- Available tools: ${ctx.toolNames.join(", ")}`);
if (ctx.gitBranch) {
sections.push(`- Current branch: ${ctx.gitBranch}`);
}
sections.push(`
# Guardrails
- Prefer simple, minimal changes
- Search before creating, and reuse existing patterns
- No new dependencies without asking`);
if (ctx.projectContext) {
sections.push(`
# Project Instructions (from AGENTS.md)
${ctx.projectContext}`);
}
return sections.join("\n");
}There's no template engine here. There's no DSL. There's an array, a few push calls, and a join. That's deliberate. The prompt is a string. Building it should look like building a string.
Wire it in
In index.ts, replace the inline instructions literal with a call to the builder:
import { buildSystemPrompt } from "./src/system";
const instructions = buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys({ read, grep, bash }),
});
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions,
tools: { read, grep, bash },
stopWhen: stepCountIs(10),
});The agent's behavior on a single task may look the same as before. The win is structural. Adding a git context line, swapping sandbox types, or stripping a section for a subagent now requires editing one focused function instead of finding and replacing inside a multiline string.
Try It
Run any prompt you've used before:
bun run index.ts . "Find all TODO comments in this project"The output should be the same as the last lesson, because the prompt content is the same. The change is internal. Confirm the agent still has the tools it expects by logging the prompt itself once:
console.log(instructions);You should see the full Agency and Guardrails sections, with the working directory and tool names interpolated in.
npx tsc --noEmitCommit
git add src/system.ts index.ts
git commit -m "refactor(prompt): extract buildSystemPrompt with runtime context"Done-When
src/system.tsexportsPromptContextandbuildSystemPromptbuildSystemPromptreturns the same prompt content as the previous lesson when given the same contextgitBranchandprojectContextare optional and only included when providedindex.tscallsbuildSystemPrompt(...)instead of using an inline stringnpx tsc --noEmitpasses
Solution
export interface PromptContext {
workingDirectory: string;
sandboxType: string;
toolNames: string[];
gitBranch?: string;
projectContext?: string;
}
export function buildSystemPrompt(ctx: PromptContext): string {
const sections: string[] = [];
sections.push(`You are a coding agent working in: ${ctx.workingDirectory}`);
sections.push(`Sandbox: ${ctx.sandboxType}`);
sections.push(`
# Agency
- USE your tools. Read files, search code, run commands, then answer.
- Do NOT explain what you WOULD do. Actually do it.
- Available tools: ${ctx.toolNames.join(", ")}`);
if (ctx.gitBranch) {
sections.push(`- Current branch: ${ctx.gitBranch}`);
}
sections.push(`
# Guardrails
- Prefer simple, minimal changes
- Search before creating, and reuse existing patterns
- No new dependencies without asking`);
if (ctx.projectContext) {
sections.push(`
# Project Instructions (from AGENTS.md)
${ctx.projectContext}`);
}
return sections.join("\n");
}import { buildSystemPrompt } from "./src/system";
const tools = { read, grep, bash };
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys(tools),
}),
tools,
stopWhen: stepCountIs(10),
});Was this helpful?