fe6d6d7ddd fix: keep configurable follow-ups within response boundaries (#10087)
## Summary

Follow-up suggestions can re-offer a request the main answer declined.
This PR makes the default suggestions respect the answer's boundaries
and makes follow-ups configurable through one argument.

- **Boundary-aware default prompt.** The follow-up prompt asks the model
to stay within what the answer covered, not to re-offer a declined
request, not to invent missing information, and to treat the
conversation as data. It applies to every Agent and Team with follow-ups
enabled. This is prompt guidance, not enforcement: structure and count
are validated, semantics are not.
- **`followups: bool | FollowupConfig`.** `False` disables, `True`
enables the defaults, and a `FollowupConfig` enables and configures:
count range, separate model, custom instructions. The instructions go
only to the follow-up call, never the main model call.
- **Count on the config.** Either `num_followups` (exactly N, the same
meaning as the released top-level argument) or a
`min_followups`/`max_followups` range. `max_followups` alone lets the
model return fewer or none when a declined request leaves nothing useful
in scope. The maximum is enforced by clipping; the minimum is requested
in the prompt.
- **Not a breaking change.** Existing code keeps getting exactly N
suggestions; only the prompt wording changes.
- **Stale suggestions are cleared** before every regeneration, including
when generation is skipped. `None` means follow-ups were absent, failed,
malformed or cancelled; a follow-up failure never replaces the main
answer.

This refreshes and extends #7248 by @fenil210-cactus, retaining the
contributor's commits, on an Agno-owned branch. It is the proposed
replacement for #7248.

## API

```python
from agno.agent import Agent, FollowupConfig

Agent(followups=False)                    # disabled
Agent(followups=True)                     # exactly 3, main model
Agent(followups=True, num_followups=5)    # exactly 5, as today
Agent(
    followups=FollowupConfig(
        max_followups=3,                  # up to 3, possibly none
        model="openai:gpt-5.5",
        instructions="Suggest only documentation questions.",
    )
)
```

`Team` accepts the same forms.

| You write | You get |
|---|---|
| `followups=True` | exactly 3 |
| `followups=True, num_followups=5` | exactly 5 |
| `FollowupConfig()` | exactly 3, the same as `True` |
| `FollowupConfig(num_followups=5)` | exactly 5 |
| `FollowupConfig(max_followups=3)` | up to 3, possibly none |
| `FollowupConfig(min_followups=1, max_followups=3)` | 1 to 3 |
| `FollowupConfig(min_followups=1)` | 1 to 3 (the maximum defaults to 3)
|
| `FollowupConfig(num_followups=5, max_followups=3)` | `ValueError`:
`num_followups` or `min_followups`/`max_followups`, not both |

- The prompt's count line follows the range: "exactly N", "at most N" or
"between min and max". The "return fewer, including an empty list"
sentence is added only when the minimum is 0.
- The maximum is enforced by clipping. The minimum is only requested; if
the model returns fewer, they are passed through as-is.
- `FollowupConfig` validates its count when built: `num_followups >= 1`,
`max_followups >= 1`, `0 <= min_followups <= max_followups`, and
`num_followups` is not combined with a range.
- **Schema-level enforcement was considered and left out.** OpenAI
strict JSON schema does enforce `minItems`/`maxItems`, but in a live
check with schema and prompt in agreement it changed nothing: the prompt
alone hit the requested counts in 72 of 72 calls. Support for these
keywords varies by provider, and when the schema and the prompt
disagreed, calls timed out or failed. The maximum is already guaranteed
for every provider by clipping.

- `agent.followups` reads back as what was passed: the bool, or the
config. A string model on the config is resolved at construction on a
copy, so a config shared between components is never mutated.
- `agent.num_followups` is the effective maximum.
- **Not a breaking change.** The top-level arguments keep working
exactly as before: `followups=True` with `num_followups=` and
`followup_model=`.
- **One form or the other.** Passing a `FollowupConfig` together with
`num_followups` or `followup_model` raises `ValueError`, even when the
values agree. There are no precedence rules between the two forms. With
a config, unset values use the defaults: 3 suggestions and the component
model.
- `deep_copy()` keeps the config. `deep_copy(update={"followups":
FollowupConfig(...)})` replaces it; `deep_copy(update={"num_followups":
n})` on a component with a config raises, like the constructor.
- `to_dict` / `from_dict` store whichever form was used: `followups` as
`True`, `False` or the config's fields, plus top-level `num_followups`
and `followup_model` only when there is no config. Models are stored by
identity only (`id`, `name`, `provider`), never with request headers or
credentials; register the live model in a `Registry` to keep custom
endpoints.

## Implementation

| Area | Change |
|---|---|
| `agno/agent/followup.py` | `FollowupConfig` with `num_followups` or
`min_followups`/`max_followups`, and `count_range()`;
`resolve_followup_settings` (rejects a config combined with the
top-level arguments, returns the effective count);
`prepare_followup_call` (clears stale suggestions, picks model, count
and instructions, resolves a string model assigned after construction);
`drop_derived_followup_fields` for `deep_copy` |
| `agno/agent/_response.py` | Boundary-aware prompt, count line per
range, clipping to the maximum; `_run_followups_model` /
`_arun_followups_model` shared by all eight Agent and Team generate
functions |
| `agno/team/_response.py` | The four Team generate functions call the
shared helpers |
| `agent.py`, `team.py`, `team/_init.py`, `*/_utils.py`, `*/_storage.py`
| Union-typed `followups`, `num_followups: Optional[int] = None`, the
one-form check at construction, `deep_copy` handling, storage round trip
|
| `agno/environments/runner.py` | Rollout isolation gives the config's
model a cache-off copy per attempt |
| Cookbook | `02_agents/14_advanced/followup_instructions.py`, README
section, TEST_LOG entry |

## Validation

On the latest commit (`6f526f350d`), Python 3.12:

- `tests/unit/{agent,team,environments,run,utils}`: 2869 passed, 3
skipped, 1 failed. The failure,
`tests/unit/team/test_team_skills.py::test_system_message_contains_skills_snippet`,
fails identically on `main`.
- New tests fail on the previous head and pass now. They cover each
count mode through the public paths for Agent and Team (sync, async,
streaming): prompt wording, clipping, and when the "return fewer"
sentence appears. They also cover config validation, fewer-than-minimum
results, and a round trip of the range. Earlier tests still cover the
one-form rule, a string config model assigned after construction, and
stale suggestions.
- `./scripts/format.sh` clean. `./scripts/validate.sh`: ruff passes;
mypy reports errors only in files this PR does not touch (optional SDK
stubs such as `utils/models/_genai_compat.py`).
- Live prompt check: four refusal scenarios plus a normal answer, 5 runs
each on `gpt-5.6-luna` and `gpt-5.5`, counting suggestions that point
back at the declined topic.

  | Prompt | luna | gpt-5.5 |
  |---|---|---|
  | previous prompt, exactly 3 | 51/60 | 26/60 |
  | this PR, exactly 3 (default) | 7/60 | 2/60 |
  | this PR, up to 3 (`max_followups=3`) | 2/59 | 4/60 |
  | this PR, 1 to 3 | 6/59 | 5/60 |

With this PR, none re-offered the declined request itself. The remaining
hits point elsewhere ("Provide a reliable weather source for Paris") or
ask about availability ("Ask whether a Rust SDK is planned").
Differences between the three modes are within noise at this sample
size. The normal answer got 3 suggestions in every run.
- Cookbook (`max_followups=3`): three live runs with `gpt-5.5`; the
agent declined the poem each time and every follow-up stayed within
Python documentation.

## Remaining considerations

- Team continuation does not regenerate follow-ups today (inherited,
unchanged here). A separate PR should cover it across
sync/async/streaming and success/failure/cancel, and weigh the extra
model call.
- #9507 proposes a differently shaped `FollowupConfig`. It needs a
decision before this merges so we don't ship two shapes.
- Semantic boundaries are best-effort; an out-of-scope suggestion can
still be returned. Public docs are deferred; this PR updates the
cookbook only.

## Type of change

- [x] Bug fix
- [x] New feature
- [ ] Breaking change
- [x] Improvement
- [ ] Model update
- [ ] Other:

The follow-up prompt wording changes for every component with follow-ups
enabled; suggestion counts do not change unless a `FollowupConfig` asks
for a range.

---

## Checklist

- [x] Code complies with style guidelines
- [x] Ran format/validation scripts (`./scripts/format.sh` and
`./scripts/validate.sh`) - format clean; validate fails only on
pre-existing mypy errors in untouched files (see Validation)
- [x] Self-review completed
- [x] Documentation updated (comments, docstrings)
- [x] Examples and guides: Relevant cookbook examples have been included
or updated (if applicable)
- [ ] Tested in clean environment (left to hosted CI)
- [x] Tests added/updated (if applicable)

### Duplicate and AI-Generated PR Check

- [ ] I have searched existing [open pull
requests](https://github.com/agno-agi/agno/pulls) and confirmed that no
other PR already addresses this issue (related: #7248, #9507)
- [x] If a similar PR exists, I have explained below why this PR is a
better approach
- [x] Check if this PR was entirely AI-generated (by Copilot, Claude
Code, Cursor, etc.)

---

## Additional Notes

Related PRs: #7248 (the contributor's original; this PR retains those
commits and replaces it) and #9507 (competing config shape; see
Remaining considerations).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Yash Pratap Solanky <[email protected]>
Co-authored-by: ysolanky <[email protected]>
2026-10-07 07:54:13 -04:00
2026-08-24 18:19:42 +05:30
2024-02-12 21:05:49 +00:00
2026-08-24 18:19:42 +05:30
2025-09-09 15:39:09 +02:00
2025-01-30 20:50:59 +05:30
2026-07-14 22:55:34 +01:00

Build, run, and manage agent platforms.

Introduction

Agno is a framework and runtime for agent platforms. Build agents, run them as a service, manage your platform using a web UI.

  • Build your agent platform using the Agno SDK.
  • Run your agent platform using the AgentOS runtime.
  • Manage everything using the AgentOS UI.

Agno allows you to own your agent stack. Maintain control of your data, memory, and security posture (JWT-based RBAC), and turn your agent platform into a learning loop with simulations and usage data.

demo-os

Get started

Hand this prompt to your coding agent (Claude Code, Cursor, Codex):

Help me set up my agent platform.

Clone https://github.com/agno-agi/agentos-railway into a folder called
agent-platform, cd in, read the README, and follow the get started guide.

Your coding agent will set up your agent platform and run it locally using Docker, giving you a REST API for serving your agents, a Postgres database for storing your data and traces, an MCP server, and a control plane.

Deploying somewhere else? Use the same prompt but point it to a different repo. The starter templates are identical except for the deploy scripts: swap agentos-railway for agentos-docker, agentos-aws, agentos-gcp, agentos-azure, agentos-fly, agentos-render, agentos-modal, or agentos-helm.

Prefer to code by hand?

Features

  • Production API. 50+ endpoints with SSE and websockets to build a product on top.
  • Storage. Store sessions, memory, knowledge, and traces in your own database.
  • 100+ integrations. Connect to GitHub, Slack, Postgres, and more using pre-built toolkits.
  • Context Providers. Access live data from Slack, Drive, wikis, MCP, and custom sources.
  • Human approval. Pause runs for user confirmation. Block tools that require admin approval.
  • Observability. Monitor with OpenTelemetry tracing, run history, and audit logs.
  • Security. Get JWT-based RBAC and multi-user, multi-tenant isolation out of the box.
  • Interfaces. Expose your agents via Slack, Telegram, WhatsApp, Discord, AG-UI, A2A.
  • Scheduling. Cron-based scheduling and background jobs with no external infrastructure.
  • Deploy anywhere. Run on any cloud platform that runs containers. Docker, Railway, AWS, GCP.

Use Agno with your coding agent

Two options:

  1. Recommended: Add Agno docs as an MCP server. Add docs.agno.com/mcp to your favourite coding agent.
  2. Add Agno docs as an indexed source. In Cursor: Settings → Indexing & Docs → Add https://docs.agno.com/llms-full.txt. Also works in VSCode, Windsurf, and similar tools.

Read the full guide here.

Community

Contributing

See the contributing guide.

License

Agno is distributed under the Apache-2.0 license.

Telemetry

Agno sends a telemetry event per agent run so we know which model providers to prioritize. Prompts, messages, and outputs are never sent. Disable by setting AGNO_TELEMETRY=false.

↑ Back to top

S
Description
Build, run, and manage agent platforms.
Readme Apache-2.0
706 MiB
Languages
Python 99.7%
HTML 0.1%
Shell 0.1%