Skip to content

Dynamic Instructions

Static system prompts treat every user and request the same. Dynamic Instructions let the system prompt adapt per request using a callable that receives the RunContext.

Basic Usage

from datetime import date
from fastaiagent import Agent, LLMClient, RunContext

agent = Agent(
    name="support",
    system_prompt=lambda ctx: (
        f"You are a support agent for {ctx.state.company_name}. "
        f"The customer's name is {ctx.state.user_name}. "
        f"Their subscription: {ctx.state.plan}. "
        f"Today is {date.today()}."
    ),
    llm=LLMClient(provider="openai", model="gpt-4o"),
)

ctx = RunContext(state=CustomerState(
    company_name="Acme Corp",
    user_name="Alice",
    plan="enterprise",
))
result = agent.run("I can't access the admin panel", context=ctx)

The callable is invoked fresh on every call to run(), arun(), or astream(). The agent instance is reusable across requests — only the context changes.

Static Prompts Still Work

Dynamic Instructions is fully backward compatible. String prompts work exactly as before.

# This is unchanged
agent = Agent(name="bot", system_prompt="You are helpful.")
result = agent.run("Hello")

Handling Missing Context

If your agent might be called with or without context, handle None in the callable:

agent = Agent(
    name="flexible",
    system_prompt=lambda ctx: (
        f"You help {ctx.state.user_name} with their {ctx.state.plan} plan."
        if ctx else
        "You are a general-purpose assistant."
    ),
    llm=LLMClient(provider="openai", model="gpt-4o"),
)

# With context — personalized prompt
ctx = RunContext(state=UserState(user_name="Alice", plan="pro"))
result = agent.run("Help me", context=ctx)

# Without context — fallback prompt
result = agent.run("Help me")

Feature Flags and A/B Testing

Dynamic Instructions let you control agent behavior via feature flags without rebuilding the agent.

agent = Agent(
    name="assistant",
    system_prompt=lambda ctx: (
        "You are a concise assistant. Keep responses under 2 sentences."
        if ctx and ctx.state.feature_flags.get("concise_mode")
        else "You are a thorough assistant. Provide detailed explanations."
    ),
    llm=LLMClient(provider="openai", model="gpt-4o"),
)

Using Named Functions

Lambdas work for short prompts. For complex logic, use a named function:

def build_support_prompt(ctx: RunContext | None) -> str:
    if ctx is None:
        return "You are a support agent."

    user = ctx.state
    lines = [
        f"You are a support agent for {user.company}.",
        f"Customer: {user.name} ({user.email})",
        f"Plan: {user.plan_tier}",
    ]

    if user.plan_tier == "enterprise":
        lines.append("This is a high-priority customer. Escalate unresolved issues.")

    if user.open_tickets > 3:
        lines.append(f"Note: customer has {user.open_tickets} open tickets. Be empathetic.")

    return "\n".join(lines)

agent = Agent(
    name="support",
    system_prompt=build_support_prompt,
    llm=LLMClient(provider="openai", model="gpt-4o"),
)

Combining with Context Tools

Dynamic Instructions works naturally with Context & Dependency Injection. Both the prompt and tools receive the same RunContext:

from dataclasses import dataclass
from fastaiagent import Agent, LLMClient, RunContext, tool

@dataclass
class AppState:
    user_name: str
    plan_tier: str
    db: DatabaseClient

@tool(name="get_orders")
def get_orders(ctx: RunContext[AppState], status: str) -> str:
    """Get orders for the current user."""
    return ctx.state.db.query("orders", user=ctx.state.user_name, status=status)

agent = Agent(
    name="support",
    system_prompt=lambda ctx: f"You help {ctx.state.user_name} ({ctx.state.plan_tier} plan).",
    llm=LLMClient(provider="openai", model="gpt-4o"),
    tools=[get_orders],
)

ctx = RunContext(state=AppState(user_name="Alice", plan_tier="pro", db=get_db()))
result = agent.run("Show my open orders", context=ctx)

Streaming

Context flows through streaming execution identically:

ctx = RunContext(state=AppState(user_name="Alice", plan_tier="pro", db=get_db()))

async for event in agent.astream("Show my open orders", context=ctx):
    if isinstance(event, TextDelta):
        print(event.text, end="", flush=True)

Push Limitation

Callable prompts are SDK-runtime-only. They cannot be pushed to the platform because Python functions aren't serializable.

# This raises ValueError with a clear message
agent = Agent(name="bot", system_prompt=lambda ctx: "dynamic")
agent.to_dict()  # ValueError: callable system_prompt cannot be serialized

# To push, use a static string
agent_pushable = Agent(name="bot", system_prompt="You are helpful.")
fa.push(agent_pushable)  # Works

For dynamic prompts on the platform, use the Prompt Registry with {{variables}} template syntax and {{@fragments}} composition.

Per-Request Isolation

Each call to run() resolves the callable independently with its own RunContext. This is safe for concurrent requests:

from fastapi import FastAPI, Depends

app = FastAPI()

@app.post("/chat")
async def chat(message: str, user_id: str = Depends(get_current_user)):
    ctx = RunContext(state=AppState(
        user_name=get_user_name(user_id),
        plan_tier=get_plan(user_id),
        db=get_db_session(),
    ))
    result = await agent.arun(message, context=ctx)
    return {"response": result.output}

API Reference

Agent constructor — updated system_prompt parameter

Parameter Type Required Description
system_prompt str \| Callable[[RunContext \| None], str] No Static string or callable that receives RunContext and returns a string
prompt_slug str \| None No References a control-plane registry prompt by slug. When set, to_dict() emits the slug and sends system_prompt="" so a pushed agent links to the governed prompt (see Pushing agent definitions). Does not affect local runtime, where system_prompt still drives the prompt.

The callable signature is:

(context: RunContext[T] | None) -> str

The callable receives the RunContext passed to agent.run(). If agent.run() is called without context, the callable receives None. The callable must always return a str.


Next Steps