The five functions that dominate agno’s risk profile are all in the ‘fire’ quadrant: structurally extreme and actively changing right now. The top-ranked function, _acontinue_run_stream in libs/agno/agno/team/_run.py, carries an activity-weighted risk score of 21.44 with a cyclomatic complexity of 227 and was last modified one day ago — that is a live regression surface, not cleanup debt. Agno is an agent framework with 11,361 analyzed functions, of which 2,738 (roughly 24%) score in the critical band; I would start review and refactoring work in the async run layer immediately, because that is where complexity and development velocity are colliding in real time.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
_acontinue_run_stream | libs/agno/agno/team/_run.py | 21.4 | 227 | 8 | 68 |
_acontinue_run_stream | libs/agno/agno/agent/_run.py | 21.3 | 190 | 8 | 62 |
_acontinue_run | libs/agno/agno/agent/_run.py | 21.2 | 129 | 8 | 58 |
_acontinue_execute_stream | libs/agno/agno/workflow/workflow.py | 21.2 | 261 | 8 | 73 |
_aexecute_stream | libs/agno/agno/workflow/workflow.py | 21.2 | 220 | 8 | 81 |
Large Repo Analysis
agno is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.
Where the risk is concentrated
11,361 functions analyzed
Every function in agno falls into either the ‘fire’ or ‘watch’ quadrant — there is no dormant debt and no inactive low-complexity code in this snapshot. The fire quadrant holds 5,930 functions, and the five highest-risk ones are all async streaming handlers concentrated in three files: agent/_run.py, team/_run.py, and workflow/workflow.py. That is not coincidence — it reflects a deliberate architectural pattern where the framework’s most demanding logic (session management, tool resolution, model calls, retry loops, telemetry, post-hooks, and streaming event emission) is funneled into single async generator functions.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
All five top functions share the same five antipatterns: god function, complex branching, deeply nested control flow, exit-heavy structure, and excessive length. That uniformity is itself a signal — the same architectural decision was replicated across agent, team, and workflow layers, so the same refactoring strategy applies to all five.
_acontinue_run_stream — team/_run.py
This is the team layer’s async streaming continuation handler — it picks up a paused team run and drives it forward, emitting events as an AsyncIterator. From the source excerpt, it accepts a 20-parameter signature covering snapshot dispatch (fork, regenerate, replace_original, continue_from), session identity, response formatting, debug mode, and background tasks. Inside, it sets up a retry loop, restores session state, resolves the continue-index via _resolve_continue_from_team and _normalize_regenerate_params_team, and defers large chunks of logic through seven inline imports — hooks, MCP tool disconnection, streaming response handlers, telemetry, and tool management.
A cyclomatic complexity of 227 means there are 227 independent execution paths through this function. Each path is a potential bug surface and, in principle, a required test case. At ND 8, control flow reaches eight levels of nesting — deeply nested async generators in Python are notoriously hard to reason about because cancellation, exception propagation, and yield semantics interact in non-obvious ways. Fan-out of 68 means 68 distinct callees, and in Python’s duck-typed runtime that coupling is often wider than the number suggests — types are resolved at call time, so a change to any one of those 68 dependencies can alter behavior here without a type-checker catching it.
The external signals show a single commit, no bug-linked history, and a sole author over 90 days. The absence of historical defect signals is not a green light — it likely reflects how new this code is (total_commit_count: 1). A function this complex, this new, and touched yesterday is a live regression risk.
Recommendation: Extract the snapshot-dispatch block (fork/regenerate/continue_from resolution) into a standalone async function, and separate the session-setup phase from the streaming execution phase. Either extraction alone would cut the CC materially and make the retry boundary easier to test in isolation.
_acontinue_run_stream — agent/_run.py
This is the agent-layer counterpart — same function name, same quadrant, same patterns, last touched one day ago. The source excerpt shows a nearly identical outer structure: 20-parameter signature, inline imports of hooks, MCP disconnection, storage, telemetry, and tool handlers, followed by a retry loop that binds run_messages early to guard against cancellation races. The docstring enumerates eleven sequential steps — session read, dependency resolution, metadata update, run response preparation, tool determination, message preparation, tool-call handling, model response, structured format conversion, post-hooks, and cleanup — all consolidated into one function body.
CC 190 with ND 8 means the function is simultaneously wide (many branches) and deep (branches nested inside branches). The exit-heavy pattern compounds this: multiple return paths scattered through eleven logical steps mean a reader cannot assume any single exit point summarizes the function’s postconditions. Fan-out of 62 in Python means this function is the runtime hub for a large fraction of the agent subsystem — a change to ahandle_tool_call_updates_stream, agenerate_response_with_output_model_stream, or any of the other 60 callees can surface as a behavioral change here.
The structural parallel to the team-layer version is exact enough that whatever refactoring strategy is chosen for one should be designed to apply to both. The eleven documented steps in the docstring are already a decomposition blueprint — each step is a candidate for extraction.
Recommendation: Use the eleven-step docstring as a literal refactoring map. Steps 1–3 (session read, dependency resolution, metadata update) are stateless enough to extract immediately. Step 8 (model response) is the most complex and should become its own testable async function.
_acontinue_run — agent/_run.py
This is the non-streaming async continuation path for the agent layer — it returns a RunOutput directly rather than yielding an AsyncIterator. Its fourteen-step docstring mirrors _acontinue_run_stream closely but includes additional post-processing steps: structured format conversion, media storage, and session metrics. The source excerpt shows the same retry-loop skeleton with early run_messages binding, and the same inline import pattern for hooks, storage, telemetry, and tool handlers.
At CC 129, this is the lowest cyclomatic complexity in the top five — which still places it in extreme territory. The gap between the streaming (CC 190) and non-streaming (CC 129) versions gives a rough measure of how much additional branching the streaming path adds. ND 8 is shared across all five functions, suggesting the nesting depth is driven by the common retry-loop-within-try-block-within-attempt-loop structure visible in the excerpts, not by streaming-specific logic.
Fan-out of 58 means this non-streaming path is nearly as broadly coupled as the streaming one — many of the same callees appear in both, which means a refactoring that modifies a shared callee (say, aread_or_create_session or determine_tools_for_model) needs to be validated against both paths simultaneously.
Recommendation: Extract a shared _prepare_agent_session helper that covers steps 1–3 (session read, dependency resolution, metadata update) and is callable from both _acontinue_run and _acontinue_run_stream. This directly reduces fan-out in both functions and creates a single point of test coverage for session initialization.
_acontinue_execute_stream — workflow/workflow.py
This is the highest cyclomatic complexity in the dataset at 261. It continues a paused workflow from a specific step index, streaming events as it goes. The source excerpt shows it opens by initializing a ContinueExecutionState object and unpacking shared media collections (images, videos, audio, files) into local references — a pattern that hints at a large amount of state being threaded through the step loop. It then handles several kwargs-driven special cases before entering the main step iteration: edited output injection, last-output removal for retries, user input injection for the paused step, and human-in-the-loop routing back to a paused agent or team.
Fan-out of 73 is the second-highest in the top five, and the step iteration loop calls into individual step executors whose types are determined at runtime — in Python, that means the actual call graph is wider than 73 at execution time. The kwargs.get(...) pattern for injecting user_input, router_selection, rejection_feedback, retry_count, edited_output, and remove_last_output is a classic god-function signal: optional behaviors are being bolted onto a single function via a catch-all dict rather than through explicit parameters or strategy objects.
The exit-heavy pattern here likely manifests as early-termination flags (early_termination = False is visible in the excerpt), conditional yields, and exception-based flow control, all layered inside the step loop — a combination that makes it very hard to establish what state the function leaves behind when it exits via different paths.
Recommendation: The kwargs-driven special cases (edited output, retry removal, user input injection, HITL routing) should each become explicit parameters or, better, encapsulated in a ContinueExecutionOptions dataclass. That alone would convert several implicit branches into explicit, testable configuration, and would cut the CC noticeably.
_aexecute_stream — workflow/workflow.py
This is the fresh-start streaming execution path for the workflow layer — it initializes a new workflow run rather than continuing a paused one. Fan-out of 81 is the highest of the five functions, which in a Python codebase means this function is the broadest runtime coupling point in the entire top-five list. The source excerpt reveals why: it immediately branches on the type of self.steps (coroutine function, generator function, async generator function, or plain callable), handling each case with a separate async iteration pattern. That type-dispatch block alone contributes a meaningful fraction of the CC, and it is repeated in full for each branch rather than being abstracted into a dispatcher.
The excerpt also shows that cancellation handling is implemented inline — a RunCancelledException catch block immediately follows the steps dispatch, and it attempts session persistence in both async and sync database paths before re-raising. That is another embedded responsibility that could be extracted: cancellation cleanup is a cross-cutting concern that currently requires understanding the full function context to modify safely.
With CC 220 and ND 8, the function shares all five antipatterns with its siblings. The isasyncgenfunction / iscoroutinefunction / isgeneratorfunction dispatch from the inspect module — imported inline — is a good candidate for a dedicated _dispatch_steps helper that normalizes all four callable types into a single AsyncIterator[WorkflowRunOutputEvent] interface, which would collapse the branching at the top of this function significantly.
Recommendation: Extract the steps-type dispatcher into a _anormalize_steps_to_async_stream helper that accepts self.steps and execution_input and returns a uniform async iterator. Separating that dispatch from session management and event emission would reduce fan-out and CC in _aexecute_stream while making the callable-type handling independently testable.
What the pattern tells me about the architecture
All five functions share the same five antipatterns without exception. That unanimity points to an architectural decision rather than isolated technical debt: the framework concentrates session management, retry logic, tool resolution, model invocation, telemetry, post-hooks, and streaming event emission into single entry-point functions for each execution mode (fresh run, continued run, streaming, non-streaming) across each execution layer (agent, team, workflow). The result is functions that are individually correct but collectively form the most change-sensitive part of the codebase.
The context-only functions — generate_id in utils/string.py, select in workflow/types.py, and the logging helpers in utils/log.py — are all in the watch quadrant with low structural complexity. They are active but not a refactoring priority; I would revisit them only if their activity-weighted risk score rises as the codebase grows.
The broader distribution reinforces this: 5,930 functions in the fire quadrant and zero in debt means the codebase is young and actively developed across the board. The five functions above are not outliers in terms of activity — they are outliers in terms of structural complexity colliding with that activity.
Codebase Risk Distribution
All five top hotspots share the same structural patterns (complex_branching, deeply_nested, exit_heavy, god_function, long_function), which is typical of the highest-risk functions in any large codebase — they accumulate every structural signal on the way to the top. More useful context is how the risk is distributed across all 11,361 analyzed functions:
| Band | Functions |
|---|---|
| Critical | 2,738 |
| High | 3,192 |
| Moderate | 3,537 |
| Low | 1,894 |
Hotspot patterns belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
Reproduce This Analysis
git clone https://github.com/agno-agi/agno
cd agno
git checkout 17d82c8a4ac0f285229a069eaa718791b182fd63
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →