Skip to main content

Reports

The report is the human-readable narrative produced from a finished simulation. It is generated by a smart-LLM tool-calling loop that can query the SimulationResult directly. Sources: simswarm/report.py, simswarm/report_tools.py, simswarm/prompts/report.j2. The reader-facing overview is in Reports.

Report generation runs off-pod, after the simulation finishes — it consumes a SimulationResult, not live engine state.

ReportGenerator.generate(result, goal)

class ReportGenerator:
def __init__(self, llm: LLMClient) -> None: ...
async def generate(self, result: SimulationResult, goal: str) -> Report: ...

It instantiates ReportTools(result), seeds a message list with the rendered system prompt (_render_system_prompt(goal) -> report.j2), and runs a tool loop bounded by _MAX_ROUNDS = 5. On each turn:

  • Call llm.chat(messages, tools=ReportTools.tool_schemas()).
  • If the response contains tool calls, append the assistant message, dispatch each call via tools.dispatch(name, args), and append each JSON result as a role: "tool" message.
  • If there are no tool calls, treat the response content as the finished markdown and break.

The returned Report has executive_brief, findings, and raw_markdown.

Query tools — ReportTools

ReportTools wraps a SimulationResult and exposes four query tools. Schemas come from ReportTools.tool_schemas(); dispatch routes by name and JSON-encodes the result (errors are caught and returned as {"error": ...}).

  • get_top_posts(limit=10)extract_posts(result.chat_log)[:limit]. This returns the first N posts in chat-log order; it is not engagement-ranked despite the name.
  • get_coalitions()_detect_mutual_follow_coalitions, which builds a follow graph from follow actions (target read from action_args["target"]) and emits a coalition for any agent whose followers it follows back, sized at least 2. Each coalition has name, description, agents, strength (min(100, size*20)), and a cycling color.

    Caveat: this depends on a target key being present in each follow action's args. The native social env's follow tool emits the target under agent_id (not target), so on current native runs the follow graph is empty and this tool returns []. The working, stance-based coalition detector is story_signals.name_coalitions — a separate construct; do not conflate the two.

  • get_agent_summary(agent_id)name, total_actions, total_posts, rounds_active, and up to 3 sample_posts (via post_text). Unknown agents return a zeroed summary with agent_id as the name.
  • get_trajectory(agent_id) — the per-round trajectory list from extract_agent_trajectories; [] for unknown agents.

Markdown parsing

The finished markdown is parsed into the Report dataclass by regex:

  • _extract_brief pulls the paragraph under ## Executive Summary.
  • _extract_findings pulls the ## Key Findings section and splits it on ### headers into {title, content} dicts.

The report.j2 template

The template asks for markdown with exact sections — ## Executive Summary, ## Verdict, ## Key Findings (exactly 4, each tagged with a slot from industry / regulator / intermediary / market / turning_point), ## Agent Coalitions, ## Market Analysis, ## Conclusion. It enforces phase/week language over round numbers and forbids inventing entities or events.

A note on two render paths

The template is written to receive pre-computed signals and forecast_days (the deterministic story signals) so the LLM is grounded and needs no tool calls. However, ReportGenerator._render_system_prompt(goal) renders report.j2 with only goal — the signals/forecast context is not threaded in on this engine-side path. The Jinja environment is configured with ChainableUndefined, so unprovided fields like signals.stakeholder_positions render empty rather than erroring, and the loop relies on the ReportTools query tools instead. Treat report.j2 as the prompt contract, but be aware the engine-side renderer passes only the goal; the fully-grounded signals path is wired up by the SaaS report layer (see simswarm/adapter.py:adapt_structured, which merges an LLM brief/verdict/findings with build_story_signals(...)).