Composition
A single agent is just a function. Real systems are built by wiring agents together — calling sub-agents, invoking tools, running work in parallel, and exposing the whole thing back to the LLM for function-calling. Because everything is Starlark, composition is just function composition.
Calling another agent
Reference another agent by its filename (without the .star extension). The runtime resolves "summarizer" to agents/summarizer.star:
# agents/research_pipeline.star
config(model = "claude-sonnet")
def agent(question):
raw = tool("web_search", query = question)
summary = agent("summarizer", document = repr(raw))
verified = agent("fact_checker", claims = summary)
return {"answer": verified, "sources": raw}Sub-agents are isolated: they get their own call log segment, their own config(), and their output flows back as a plain value.
Invoking tools
Tools are .star files in the tools/ directory. Each file exports one function — its name becomes the tool name, its docstring becomes the description, and its parameters are auto-generated into a JSON schema for LLM function-calling.
tools/search.star
def web_search(query, max_results = 5):
"""Search the web and return results."""
response = http("GET", "https://api.search.example/search", params = {
"q": query,
"limit": max_results,
})
return response["results"]Call it from an agent:
results = tool("web_search", query = "rust programming", max_results = 5)Letting the LLM pick tools
Pass a list of tool names to prompt() and the LLM can call them autonomously during its response. max_turns caps the tool-use loop:
answer = prompt(
"Research this question and verify your findings: " + question,
tools = ["web_search", "summarizer", "fact_checker"],
max_turns = 5,
)Sub-agents can be exposed as tools too — this is how you build recursive multi-agent systems without a special framework.
Parallel fan-out
parallel(fns) runs a list of functions concurrently and returns their results in order. Use it for independent searches, multi-audience summaries, or any embarrassingly parallel step:
# Search multiple queries in parallel
results = parallel([
lambda q = q: tool("web_search", query = q)
for q in queries
])
# Generate summaries for different audiences concurrently
summaries = parallel([
lambda a = a: prompt("Summarize for " + a + " audience:\n" + doc)
for a in ["technical", "executive", "general"]
])Use lambda q = q: (default-arg capture) inside comprehensions — exactly like
Python — to avoid late-binding closure bugs.
Jinja templates for complex prompts
For prompts with conditionals, loops, or reusable fragments, render a Jinja template with template() and pass the result into prompt():
prompts/research.jinja
You are a {{ role }} research assistant.
Answer the following question: {{ question }}
{% if sources %}
Use these sources:
{% for source in sources %}
- [{{ source.title }}]({{ source.url }}): {{ source.snippet }}
{% endfor %}
{% endif %}
{% if format == "detailed" %}
Provide a comprehensive analysis with citations.
{% else %}
Provide a concise answer in 2-3 sentences.
{% endif %}answer = prompt(template("prompts/research.jinja",
role = "senior",
question = question,
sources = sources,
format = "detailed",
))A composed example
A research pipeline that plans queries, fans out searches, optionally fetches sources, renders a Jinja prompt, and fact-checks the result:
config(model = "claude-sonnet", max_turns = 15)
def agent(question, depth = "standard"):
queries = prompt(
"Generate 3 search queries for: " + question +
"\nReturn as a JSON array of strings.",
format = "json",
)
all_results = parallel([
lambda q = q: tool("web_search", query = q)
for q in queries
])
sources = []
if depth == "deep":
urls = [r["url"] for batch in all_results for r in batch[:2]]
sources = parallel([lambda u = u: tool("fetch_url", url = u) for u in urls])
answer = prompt(template("prompts/research.jinja",
question = question,
results = all_results,
sources = sources,
), model = "claude-opus", temperature = 0.3)
final = prompt("Fact-check this answer. Return the corrected version.\n\n" + answer)
return {"answer": final, "sources": all_results, "queries_used": queries}Every step is ordinary Starlark. Every side effect is a logged, replayable host function call.