mirror of
https://github.com/agno-agi/agno.git
synced 2026-10-12 02:10:54 +00:00
78fa25982ef3732f6cb7f374c0e3c8ec55520363
6169
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
78fa25982e |
fix: persist ClaudeAgent SDK session id across restarts (#10903)
## Summary `ClaudeAgent` kept the Agno `session_id` -> Claude SDK session id mapping only in memory (`self._sdk_session_ids`). After a process restart, or on a different AgentOS worker, the same Agno session silently started a fresh Claude conversation. The AgentOS UI still showed the full history from the DB, so it looked as if the session had continued. - **Persist the mapping:** the SDK session id is now stored in `session.session_data["claude_sdk_session_id"]`, which the base class already upserts after each run. The in-memory map stays as the fallback when no `db` is configured. This matches what `CodexAgent` and `AntigravityAgent` already do. - **Fall back when a stored id can't be resumed:** the SDK keeps its transcript on local disk under the agent's `cwd`, so a stored id may point at a transcript that isn't there (another host, changed `cwd`, deleted transcript). In that case the CLI exits 1 before yielding any message, and without a fallback the session would fail on every run from then on. If a resume fails before the first message, the adapter forgets the id and starts a fresh SDK session, with the Agno history added to the start of the prompt. Errors after messages have started streaming are still raised, so output is never duplicated. - **Share the prompt builder:** `_build_prompt` moved from `CodexAgent` to `BaseExternalAgent` so both adapters use it. Codex behavior is unchanged. - **Simpler adapter hooks:** both hooks now go through a single `_aquery` helper, which removes the duplicated session-id capture code in the streaming and non-streaming paths. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **Tests:** new file `libs/agno/tests/unit/agents/test_claude.py`, using a fake `claude_agent_sdk`. It covers: - persistence and resume across a fresh agent instance (streaming and non-streaming) - the stale-id fallback, including the history added to the prompt - the in-memory mapping when no db is configured - no retry when the run wasn't resuming The 3 persistence and fallback tests fail on `main`; the other 3 are regression guards. All 58 tests in `tests/unit/agents/` pass. **End-to-end check with the real SDK (0.2.95):** each turn ran in a separate Python process against a shared SQLite db. 1. Turn 1 stored the SDK session id. 2. Turn 2, in the same `cwd`, resumed that session and recalled a fact from turn 1. 3. Turn 3, in a different `cwd`, logged the resume warning, fell back to a fresh SDK session, still recalled the fact from the added history, and stored the new id. **validate.sh:** ruff passes. mypy reports 0 errors in `agno/agents`. The 36 mypy errors it does report are in modules that wrap third-party SDKs (`tools/mcp`, `utils/models/_genai_compat`, `models/google`, `models/anthropic`). They come from SDK-version drift in my local venv and are not touched by this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: kausmeows <[email protected]> |
||
|
|
5f1fd0ce28 |
chore: Release v3.1.2 (#10892)
# Changelog ## New Features: - **Conversation Compaction**: `Agent(compaction=True)` keeps long sessions inside the context window by folding older turns into a summary. Stored messages are never rewritten: a `CompactionRecord` in the new `agno_compactions` table drives a derived model payload, so removing the record restores the full conversation. Tune with `Compaction(model=..., compact_at_tokens=..., uncompacted_runs=...)`, fold on demand with `agent.compact()` / `acompact()`, and inspect `run.compaction`. With `searchable=True` the agent gets a `search_compacted_history` tool, a regex grep over the archived transcript. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/03_context_management/compaction/compaction.py). (#9873) - **Codex External Agent**: Added `CodexAgent`, an adapter for OpenAI Codex built on the `openai-codex` SDK, alongside the Claude Agent SDK, LangGraph, DSPy and Antigravity adapters. Codex runs its own agent loop; the adapter translates its notifications into Agno run and tool-call events, maps each Agno session to one Codex thread (persisted when a `db` is set), and exposes `sandbox`, `approval_mode`, `reasoning_effort`, `output_schema` and MCP config. Works standalone and through AgentOS. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/frameworks/codex/codex_basic.py). (#10901) - **HyDE Query Transform**: Added `QueryTransformer` as a new retrieval hook and `HyDE` as its first implementation: `Knowledge(vector_db=..., query_transformer=HyDE())` searches with a hypothetical answer instead of the raw question, while rerankers still score against the original query. `HyDE` uses its own `model`, then the calling agent or team's model, then the framework default. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/07_knowledge/02_building_blocks/12_hyde_query_transformer.py). (#10527) - **Browser Origin Policy**: Added `AgentOS(cors=CORSConfig(origins=..., origin_regex=..., merge_base_app=...))` and opt-in `PublicSurface(enforce_browser_origins=True)`. One origin policy is now shared by CORS preflights, public run and cancel admission, auth error headers, workflow WebSockets and MCP aliases, and CORS is the outermost middleware so error responses carry consistent headers. `cors_allowed_origins` keeps its existing behaviour. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/05_agent_os/27_public_pages/browser_origins.py). (#10869) - **Configurable Follow-ups**: `followups: bool | FollowupConfig` on Agent and Team. `FollowupConfig` sets a count range (`num_followups` or `min_followups` / `max_followups`), a separate model and custom instructions that go only to the follow-up call. The default prompt now stays within the answer's boundaries and no longer re-offers a request the answer declined. Existing `num_followups` callers keep getting exactly N suggestions. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/14_advanced/followup_instructions.py). (#10087) - **Heabsy**: Added `Heabsy` as an OpenAI-compatible model provider, also resolvable as `"heabsy:<model>"`. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/90_models/heabsy/basic.py). (#10828) - **FlexAI**: Added `FlexAI` as an OpenAI-compatible model provider for open-weight models, also resolvable as `"flexai:<model>"`. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/90_models/flexai/basic.py). (#10871) - **SixtyDBTools**: New toolkit for the 60db hosted speech API: discover workspace voices and generate speech as WAV audio artifacts. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/91_tools/sixtydb_tools.py). (#10786) ## Improvements: - **Page Sync Logs**: Page synchronization now logs lock waits, discovery, progress every 25 pages and final counts, so long syncs show progress without a custom callback. Added an AgentOS cookbook that syncs documentation into PostgreSQL-backed knowledge pages and exposes them through the agent's filesystem. See [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/05_agent_os/27_filesystem/knowledge_fs.py). (#10868) - **OpenAIResponses & OpenRouterResponses**: Recognise GPT-6 (OpenAI) and GPT-5/6 (OpenRouter) as reasoning models, so reasoning-state chaining and encrypted reasoning replay apply and tool-call follow-ups no longer fail with missing reasoning-item errors. (#10830) - **PerplexitySearch**: Requests now send an `X-Pplx-Integration: agno` header so Perplexity can attribute traffic from the integration. (#10827) ## Bug Fixes: - **Approvals**: Each `@approval` tool call in a run now gets its own approval record, for agents and teams. Previously a second pause in the same run created no record and the next continue ran the new call against the approval resolved for the first one. (#10812) - **Model Response Cache**: Pydantic `output_schema` classes are keyed by their JSON schema instead of their class name, so two schemas with the same name but different fields no longer share a cached response. (#10621) - **Usage Metrics**: `MessageMetrics.cache_write_tokens` is now populated from OpenAI Chat Completions and Responses usage (#10313) and from LiteLLM's `prompt_tokens_details` (#10476). Previously it was always zero. - **JsonDb**: Tables are serialized to a staging file and published with `os.replace`, so a failed write leaves the last successfully saved table readable instead of a truncated file. (#10794) - **MongoDb & MongoVectorDb**: `session_name`, `search_content` and keyword-search filters are regex-escaped, so a value like `(draft` no longer returns HTTP 500 from `GET /sessions` and `GET /memories`, and `v1.2` no longer matches `v1x2`. (#10709) - **Tool Schemas**: `Annotated[T, ...]` hints are unwrapped to `T` before JSON schema generation instead of falling through to an empty object schema. (#10858) - **SQLTools**: Connection URLs are built with `URL.create`, so a password like `p@ssword` is no longer split into password `p` and host `ssword@...`. (#10800) - **FileTools**: `replace_file_chunk` rejects negative, reversed and out-of-bounds line ranges with an error and leaves the file untouched; a valid replacement preserves the trailing newline. (#10798) - **GoogleCalendarTools**: `find_available_slots` compares the full slot end against closing time on its start date, so a 45 minute slot is no longer offered at 16:30 for a 17:00 close and slots cannot cross midnight. (#10878) - **BaiduSearchTools, Searxng & SpiderTools**: An explicit `max_results=0` / `fixed_max_results=0` now means no results instead of falling back to the default. (#10877) - **Readers**: `TextReader`, `MarkdownReader` (#10779) and `JSONReader` (#10634) accept streams whose `name` is an integer or `None`, such as descriptor-backed temporary files, instead of returning nothing or raising. - **Cookbooks**: Fixed the `DynamoDb` import path in the DynamoDB storage README. (#10788) --------- Co-authored-by: Yash Pratap Solanky <[email protected]> Co-authored-by: ysolanky <[email protected]>v3.1.2 |
||
|
|
83dd1329bc |
feat: auto compaction with a searchable archive (#9873)
## Summary
Adds **conversation compaction** — keeping a long session inside the
context window by folding old turns into a summary, while the originals
stay recoverable.
### The problem
A long session has no good options in Agno today:
| Existing mechanism | Scope | Why it doesn't solve this |
|---|---|---|
| `num_history_runs` / `num_history_messages` | History window | Drops
old turns entirely — lossy, no summary |
| `CompressionManager` | Individual **tool results**, inside the model
loop | Per-run only. Never touches conversation turns |
| `ResultStore` (offload) | Oversized **tool results** → AgentFS |
Tool-call-scoped by construction. No message equivalent |
| `SessionSummaryManager` | Session summary → system prompt |
*Additive*: you get the summary **plus** all raw history. Fires every
run, no threshold. Shrinks nothing |
### The approach: derived views over an append-only transcript
Compaction never rewrites stored messages. A **`CompactionRecord`** —
summary, boundary anchor, elision watermark — is written to
`agno_compactions`, and the model payload is **derived fresh per call**
from the canonical messages plus that record.
```
CANONICAL RECORD (one row) DERIVED (per call, discarded)
runs[].messages + first_kept_message_id → [system] [summary] [kept tail]
(never mutated) elision_watermark_id
summary · archived_messages
```
Three properties fall out: nothing is destroyed (drop the record and the
full conversation returns), concurrent readers are safe, and an anchor
that no longer resolves **fails open** to the full list rather than
cutting in the wrong place.
By default (`searchable=True`) the agent also gets a **grep over the
archived transcript**, so a summary that dropped a detail is still
recoverable mid-conversation.
---
## DX
**One flag**
```python
agent = Agent(
model=OpenAIResponses(id="gpt-5.6-luna"),
db=SqliteDb(db_file="tmp/compaction.db"),
add_history_to_context=True,
compaction=True,
)
```
**Tuned — every field a plain scalar**
```python
from agno.compaction import Compaction
compaction=Compaction(
model=OpenAIResponses(id="gpt-5-mini"), # a cheap model does the summarizing
compact_at_tokens=100_000,
uncompacted_runs=5,
)
```
**Folding on demand**
```python
result = agent.compact(session_id="s1") # and acompact()
if result.compacted:
print(result.record.tokens_before, "->", result.record.tokens_after)
else:
print(result.status.value, result.message)
```
Only the size trigger is bypassed — a caller asking to compact has
supplied the judgement `compact_at_tokens` exists to make. Every other
guard still applies, and **declining is reported, not raised**: a
summary costs a few hundred tokens whatever it replaces, so folding a
smaller span would leave the context *bigger*. `CompactionResult`
carries a status and a message written for display.
**Searchable archive**
```python
compaction=Compaction(uncompacted_runs=5, searchable=True)
```
The agent gains one tool, `search_compacted_history(pattern,
context_lines)` — a **regex grep** returning numbered lines with
context, `grep -n -C` style, scoped to this session by construction.
Observed in a live run, unprompted:
```
search_compacted_history(pattern=Pinned build hash|Rotation window|b7f2ae91c4|47 days, context_lines=2)
```
**Inspecting what happened**
```python
run = agent.run("...")
print(run.compaction)
# CompactionRecord(messages_compacted=6, tokens_before=17786, tokens_after=5537, archived=True)
```
**Streaming events**
```python
async for chunk in agent.arun(q, stream=True, stream_events=True):
if chunk.event == RunEvent.compaction_completed.value:
print(chunk.messages_compacted, chunk.tokens_before, chunk.tokens_after)
```
### Full surface
| kwarg | default | meaning |
|---|---|---|
| `model` | agent's model | model used to summarize |
| `compact_at_tokens` | `150_000` | fold when the context exceeds this;
`None` disables the automatic trigger |
| `uncompacted_runs` | `5` | recent runs kept verbatim |
| `uncompacted_tokens` | `None` | recent history kept verbatim, bounded
by size instead of turns; mutually exclusive with `uncompacted_runs` |
| `archive` | `True` | store the folded transcript alongside the record
|
| `searchable` | `True` | give the agent grep over the archive; off when
`archive=False` leaves nothing to search |
| `elide_tool_results` | `True` | render old tool results as a
placeholder |
| `min_fold_ratio` | `2.0` | skip a fold not meaningfully larger than
the tail it keeps |
| `summary_budget_tokens` | `2_000` | soft length target given to the
summarizer |
| `on_context_overflow` | `False` | also fold reactively when the
provider rejects a request as too long; bare `compaction=True` turns
this on (and leaves `compact_at_tokens` unset) |
| `instructions` | `None` | custom summarization prompt |
`bool | object` matches every existing Agno manager, so nothing new is
learned.
**The tail is measured in runs, not messages.** A run is one turn, so a
run-based tail never
cuts through the middle of one — which is what the pair-safe boundary
walk wants anyway. A message
count was also invisible to the planner-window guard below, which
reasons in runs, so it could
strand a boundary anchor with no warning.
**Size is the only automatic trigger.** Run and message counts were
considered and dropped: twenty short exchanges and twenty research turns
differ by orders of magnitude, so counting them fires on conversations
far too small to fold and stays quiet on ones that overflow.
`agent.compact()` covers the "fold at a seam I choose" case that a count
was standing in for.
---
## Design notes
**Where it plugs in.** `get_run_messages()` / `aget_run_messages()`,
right after the history block — one seam shared by all four run paths,
so none of them needed changing.
**Anchors are message ids, not indexes.** History is rebuilt from stored
runs every run, so a positional boundary means something different each
time and the cut silently crawls forward. An id resolves to the same
message forever, or not at all.
**Three tiers, increasing cost.** Elision replaces old tool results with
a placeholder (zero inference, reclaims the bulk of a tool-heavy
transcript); folding summarizes old turns (one LLM call); archiving
stores the originals (one write, buys recoverability). Independent
knobs, not one mode.
**Cut safety.** `choose_boundary` never splits a tool batch — in either
direction — and never anchors on a message that won't survive to the
next run (`temporary`, `add_to_agent_memory=False`). Returns `None`
rather than cutting unsafely.
**Storage.** `agno_compactions`, one row per fold, written once and
never updated. Records are immutable facts about single runs, so
concurrent writers can't collide and a resumed run can resolve the fold
it actually saw. Optional-table pattern following `agno_tool_results`:
base raises `NotImplementedError`, SQLite and PostgreSQL implement,
other backends degrade to summary-only with a warning.
### Bounding the kept tail by size
`uncompacted_runs` says how many turns survive a fold. It does not say
how large they are — and
compaction only folds what sits in FRONT of the tail, so a verbose turn
landing inside the tail
is one it can never bring back down. Five huge runs produce an unbounded
tail that no amount of
folding reaches.
`uncompacted_tokens` bounds the tail's size instead. On five detailed
turns of ~4,700 tokens each:
```
uncompacted_runs=2 fold 14,178 tail 9,452 ratio 1.50 -> declined
uncompacted_tokens=2000 fold 18,929 tail 4,701 ratio 4.03 -> folds
```
Both kept "the recent conversation". The run count kept two enormous
turns, leaving too little
in front of them to pay for a summary — so nothing happened and all
23,630 tokens stayed.
The cut is still pair-safe: it snaps to a turn boundary rather than
severing a tool call from
its result, so the tail can come out somewhat larger than the budget. It
is a target, not a hard
cap — and once the tail holds a single turn, no smaller budget can
shrink it further.
The two are **mutually exclusive and raise** if both are set. They
describe the same tail in
different units, and silently honouring one of two values the user
deliberately set is the kind
of surprise that costs an afternoon.
### The history window compaction reads
`num_history_runs` defaults to `3`; `uncompacted_runs` defaults to `5`.
Left alone, those two independent defaults collide and
**`compaction=True` could never fold** — the tail is larger than the
whole window, so nothing is ever in front of it.
Worse, a fold's anchor is a message id but `num_history_runs` is a
sliding count. A few runs after a fold the anchor falls outside the
window, stops resolving, and the view fails open — so the model loses
the folded turns *and* the summary that replaced them, silently.
So the planner reads its **own bounded window** rather than the replay
window. Bounded, not unlimited: capped so a 10,000-run session costs no
more than a short one. An explicit `num_history_runs` is respected
as-is, except where it sits at or below `uncompacted_runs` — a
configuration that cannot express a working compaction — where it is
raised to the minimum that keeps the anchor findable, **with a
warning**. Silently ignoring a number the user set is worse than the
misconfiguration it works around.
```
compaction=True (both defaulted) planner window widened
num_history_runs=50 50, respected exactly
num_history_runs=3, uncompacted_runs=5 6, and warns
```
`num_history_runs` continues to govern what the model replays. Only the
planner's read is widened.
---
## How a fold is decided
Two gates, and both have to pass.
**Gate 1 — the trigger: "is there enough here?"** `compact_at_tokens`,
measured against the view about to be sent — system text, tool schemas
and the history that survives the active record. Passing it only means
*look closer*.
The obvious cheaper signal is wrong here. `RunMetrics.input_tokens` is
billing telemetry: it sums *every* model call in a run, so a tool loop
that re-sends a 20k context four times reports 80k, and a 25k threshold
fires on a request that never exceeded 21k. It also absorbs background
and compaction-model calls, so folding inflates the number that decides
whether to fold again. Per-call sizes are not recoverable either —
`ModelMetrics` entries merge by provider+id before compaction sees them.
So the trigger estimates locally instead, via
`agno.utils.tokens.count_tokens` rather than `Model.count_tokens` (a
network call on some providers). ~150ms on a 400-message transcript,
once per run, against a summarizer call costing seconds. It is also the
same unit the ratio guard below already uses, so both gates now measure
the same quantity.
**Gate 2 — the ratio: "is it worth it?"** This is the one that decides:
```
ratio = tokens being folded ÷ tokens in the kept tail
```
`min_fold_ratio` defaults to `2.0` — the fold has to be at least twice
the tail.
A summary is never free: it costs a few hundred tokens whatever it
replaces. Folding 20,000 tokens to save 19,500 is obviously worth it;
folding 600 to save 100 is not, and it discards the prompt-cache prefix
to do it. A ratio catches that at any scale, where a fixed size
threshold would not.
From a live session (`compact_at_tokens=4_000`, `uncompacted_runs=1`, an
agent instructed to answer at length):
```
Q1 fold 9,691 tail 8,265 ratio 1.17 skipped - barely more than is kept
Q2 FOLD 24,965 → 8,620 tokens
Q3 fold 7,009 tail 12,075 ratio 0.58 skipped - little new since the fold
Q4 fold 19,084 tail 10,962 ratio 1.74 skipped - just under the bar
manual compact() 59,045 → 14,173 tokens
```
Q3 is the clearest case: right after a fold, the summary plus the tail
*is* most of the context, so there is little in front of the tail left
to reclaim. Q4 shows the bar doing its job on a near miss — and note
that Q3's total (19,084) becomes Q4's foldable span exactly, once one
more turn pushes a new tail in front of it.
**Tuning.** Continuing the conversation is usually the real answer: the
fold grows with every
turn while the tail stays roughly fixed, so the ratio climbs on its own.
Shrinking the tail helps
only while it still holds more than one turn — the cut is pair-safe, so
no setting can cut inside
a turn, and a decline names that setting only when changing it could
actually move the boundary.
`min_fold_ratio=1.5` is the other lever.
A declined fold says why, with the numbers:
```
This fold would replace 1234 tokens against a 1234-token tail (ratio 1.00, needs 2.0),
so the context would not shrink. Continue the conversation, or lower uncompacted_runs=2
or min_fold_ratio to fold sooner.
```
**Known limit.** The ratio measures the *input*, and cannot know how
large the summary will be until it exists. On a span that is mostly raw
data — a table, a log dump — the prompt requires identifiers be
reproduced verbatim, so the summary can come back nearly the size of
what it replaced. Observed one fold saving only 20 tokens for this
reason. Catching that needs a post-hoc check; see Additional Notes.
### Mental model
```
← fold grows forever ──────┤ window is fixed at N turns ├→
(everything ever said) (but its SIZE varies wildly,
because turns vary in length)
ratio = fold ÷ window want ≥ 2.0
```
### Provider interaction worth flagging
`OpenAIResponses` chains via `previous_response_id`, and the server then
replays the **whole** pre-fold conversation — so the savings would be
imaginary and the agent would never consult the archive. The view strips
that key from assistant copies. Only that key: reasoning items survive,
because a `function_call` without its paired reasoning item is a hard
400.
**Only `OpenAIResponses` has been audited for this.** Other providers
with server-side conversation state may need the same treatment.
### Composing with `offload_tool_results`
Supported. Offload envelopes are never elided, and when one is folded
away its `result_id` is carried forward in the summary so `read_result`
still resolves the payload. Envelopes are also excluded from the
fold-ratio calculation, since they're pinned cost that folding can't
reclaim.
---
## AgentOS route
```
POST /agents/{agent_id}/sessions/{session_id}/compact
```
Returns **200 with a status** whether or not it folded, because
declining is a normal outcome rather than a failure — raising would make
"folding would not help here" indistinguishable from an error and force
every caller to catch it.
```json
{
"status": "compacted",
"message": "Compacted 18 messages (15864 -> 3981 tokens).",
"compacted": true,
"record": { "messages_compacted": 18, "tokens_before": 15864, "tokens_after": 3981 }
}
```
| status | meaning |
|---|---|
| `compacted` | the fold happened; `record` carries the counts |
| `not_worth_it` | the span is too small to pay for the summary
replacing it |
| `nothing_to_fold` | the kept tail covers the whole conversation |
| `already_compacted` | a previous fold already covers everything
foldable |
| `no_history` | the session has no stored history yet |
| `not_enabled` | compaction is not configured on this agent |
| `summary_failed` | the summarizer returned nothing |
A missing agent is a 404 — that is a real error. Consumed by the AgentOS
chat UI as a "Compact now" action (separate repo, not in this diff).
---
## Type of change
- [x] New feature
- [ ] Bug fix
- [ ] Breaking change
- [ ] Improvement
- [ ] Model update
- [ ] Other:
---
## Checklist
- [x] Code complies with style guidelines
- [x] Ran format/validation scripts (`./scripts/format.sh` and
`./scripts/validate.sh`)
- [x] Self-review completed
- [x] Documentation updated (comments, docstrings)
- [x] Examples and guides: cookbook examples included
- [x] Tested in clean environment
- [x] Tests added/updated
### Duplicate and AI-Generated PR Check
- [x] I have searched existing open pull requests
- [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.)
---
## Relationship to #9834 and #9291
**#9834** (context compaction for agents and teams) is the more complete
implementation, and this PR **adopts its core architecture**: derived
views (`_view.py`), pair-safe durable cut selection (`_cut.py`),
local-only token estimation, and its structured summarization prompt.
Credit where due — those are better than what this PR had.
What this PR keeps that #9834 does not have:
- **A searchable archive.** #9834's summary *is* the history; a detail
it dropped is gone. Here the folded transcript is stored and greppable,
so the summary works as an index over ground truth.
- **Records in a table** rather than on the session, giving
`continue_run` correctness and multi-writer safety.
- **Manual folding** — `agent.compact()` plus an AgentOS route, with
declines reported as statuses rather than raised.
What #9834 still has that this does not: background folds, an in-run
loop seam, and Teams.
**#9291** is superseded by both. It rewrites stored history in place,
has no handling for provider-side conversation state (so its savings are
partly illusory on `OpenAIResponses`), and its own PR body notes the DX
is under review across three companion PRs.
---
## Testing
**83 unit tests** in `libs/agno/tests/unit/compaction/` plus **4 route
tests** in `libs/agno/tests/unit/os/routers/`: trigger matrix, boundary
safety (property test over every cut point), grep behaviour, archive
round-trip and cross-session isolation, provider-state detachment, both
fold guards, `continue_run` resolution, the planner-window fix and its
warning, the manual-compaction result contract, the trigger measuring a
tool loop's context rather than its billing total, and the size-bounded
tail with its mutual exclusion.
Cookbooks, run end-to-end against a live model:
- `cookbook/02_agents/03_context_management/compaction/` — basic,
thresholds, searchable archive, streaming events, async, manual, with
tools, Anthropic, Anthropic + thinking
- `cookbook/05_agent_os/21_compaction/` — an AgentOS serving both an
automatic and a manual-only agent, plus a REST script driving a fold
over HTTP
---
## Additional Notes
Deliberate follow-ups, not gaps:
- **Team parity.** `Team` mirrors every compression flag but has none of
this wired yet — the largest remaining gap.
- **Background folds and the in-run seam.** The summarizer call
currently blocks the turn, and compaction only fires at run start, so a
single long tool-calling run can still overflow. These are coupled: a
fold started in the background needs a loop-top to be adopted at.
- **`compress_tool_results` deprecation.** The existing surface still
runs in parallel. Nothing here changes or breaks it — it also still
covers in-run tool results that compaction cannot reach.
- **A post-hoc size check.** The ratio bounds the input but cannot
guarantee the outcome; verifying a fold actually shrank the context
needs a check after the summary exists.
---------
Co-authored-by: Anurag <[email protected]>
Co-authored-by: Harsh <[email protected]>
Co-authored-by: Harsh Sinha <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Yash Pratap Solanky <[email protected]>
|
||
|
|
98e3751e52 |
feat: add support for codex harness external agent (#10901)
## Summary Adds `CodexAgent`, an external-agent adapter for OpenAI Codex, alongside the existing Claude Agent SDK, LangGraph, DSPy and Antigravity adapters. Codex agents can now be served through AgentOS or used standalone via `.run()` / `.print_response()`. The adapter wraps the official [Codex Python SDK](https://github.com/openai/codex/tree/main/sdk/python) (`pip install openai-codex`), which bundles the Codex CLI and runs it locally as an app-server subprocess. Codex runs the full agent loop itself (shell commands, file edits, web search, MCP tools); the adapter translates its notifications into Agno run events and maps each Agno session to one Codex thread. **Adapter** (`libs/agno/agno/agents/codex/`) - Subclasses `BaseExternalAgent`, implementing `_arun_adapter` and `_arun_adapter_stream`. - Options map onto the SDK: `model`, `instructions` (developer instructions), `base_instructions`, `sandbox` (`read-only` / `workspace-write` / `full-access`), `approval_mode` (`auto_review` / `deny_all`), `reasoning_effort`, `cwd`, `ephemeral`, `output_schema`, `config` (Codex config overrides, e.g. `mcp_servers`), `codex_bin`, `env`, plus `thread_kwargs` and `turn_kwargs` escape hatches. - Streaming translation: `item/agentMessage/delta` -> `RunContentEvent`; `commandExecution`, `fileChange`, `mcpToolCall`, `webSearch` and `dynamicToolCall` items -> `ToolCallStartedEvent` / `ToolCallCompletedEvent` (tool names `shell`, `apply_patch`, `mcp__<server>__<tool>`, `web_search`); reasoning deltas -> `reasoning_content`; a failed `turn/completed` or non-retryable `error` -> `RunErrorEvent`. - Sessions: the Codex thread id is stored in the Agno session's `session_data["codex_thread_id"]` when a `db` is configured (survives restarts and new app-server processes), with an in-memory fallback otherwise. If a thread cannot be resumed (`ephemeral=True`, rollout files removed) the adapter starts a fresh thread and prepends the persisted chat history to the prompt. - One `AsyncCodex` client per run keeps the client bound to the current event loop, so the sync `run()` path works unchanged. - `login_api_key` / `alogin_api_key` helpers for environments where the `codex` CLI is not on PATH. **Cookbooks** (`cookbook/frameworks/codex/`, plus `00_quickstart/codex_agent.py`) - `codex_basic.py`, `codex_tools.py`, `codex_session.py`, `codex_mcp_tools.py`, `codex_structured_output.py`, `codex_agentos.py`, `codex_session_agentos.py`, with `README.md` and `TEST_LOG.md`. - Quickstart README updated to list Codex. **Packaging** - `openai-codex` added to the `demo` extra; `openai_codex.*` added to the mypy ignore list. **Docs** are in a separate agno-docs-v2 change: new `agent-os/multi-framework/codex.mdx`, overview table/card and nav entry. ## Type of change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [x] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **Testing** - 16 new unit tests in `libs/agno/tests/unit/agents/test_codex.py` run against a fake `openai_codex` module (option mapping, notification translation, session/thread persistence, resume fallback, ephemeral handling). No live calls in CI. - All cookbooks were run live against openai-codex 0.161.0 (bundled Codex CLI 0.161.0) with `gpt-5.6-luna`; results are recorded in `cookbook/frameworks/codex/TEST_LOG.md`. Verified: thread resume across turns and across separate app-server processes, MCP tool calls rendering in the Tool Calls panel, structured output parsing as JSON, SSE streaming and non-streaming runs through AgentOS, and session recall across requests. - `ruff` clean; `mypy` reports no errors in the new code (the remaining errors on `main` are pre-existing in unrelated files). **Things to know** - The Codex Python SDK requires Python 3.10+. The adapter imports it lazily, so Agno on 3.9 is unaffected. - Authentication: `codex login` (ChatGPT) or `CODEX_API_KEY` / `OPENAI_API_KEY`. There is no interactive approval prompt in this context, so `approval_mode="deny_all"` is the safe choice for unattended servers. - `ephemeral=True` threads cannot be resumed by Codex, so multi-turn context falls back to history injection in that mode. - Same scope as the other external adapters: registration, streaming, sessions and tool visibility. Background runs, durable queue and workflow step replay are not wired up for external agents. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b53271584d |
feat: add HyDE query transform (#10527)
## Summary This PR adds HyDE retrieval strategy. A new hook, which changes what is searched for, so it needs somewhere earlier to sit: Knowledge(vector_db=..., query_transformer=HyDE(), reranker=CohereReranker()) `QueryTransform` mirrors `Reranker` - one method, sync and async, the async default running the sync one off the event loop. The transform changes the query the store receives; the reranker still scores against the question the caller asked, so relevance is judged on what the user wanted rather than on a stand in for it. `HyDE()` uses its own `model` if set, then the model of the agent or team that triggered the search, then the same OpenAI default the rest of the framework falls back to. `get_model_kwarg` offers it only to a callee that declares `model`. Unlike the owner kwarg, where a dropped `user_id` leaks data across users and `**kwargs` is worth the risk, this is an enhancement: a legacy `retrieve(query, **kwargs)` forwarding to a narrower search would raise on an argument it never asked for, and losing the transform is the lesser cost. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] 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 - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.) --------- |
||
|
|
718ae26fb9 |
[fix] Keep Google Calendar slots within working hours (#10878)
## Summary Fixes #10880. `find_available_slots` can offer 16:30–17:15 for a 45-minute appointment when working hours end at 17:00. Comparing only the end hour also admits appointments that cross midnight. Compare the complete slot end with the closing time on its start date, preserving appointments that end exactly at closing time and custom working hours. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [ ] 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 Validation in a fresh Python 3.12 environment: - Before the fix, six new regression cases failed and the 30-minute control passed. - After the fix, all 70 tests in `libs/agno/tests/unit/tools/test_google_calendar.py` passed. - Changed files pass Ruff lint, Ruff formatting, and `git diff --check`. - `scripts/format.sh` passed. - `scripts/validate.sh` passed: all Ruff checks, mypy for 1,091 Agno and 21 agnoctl source files, and the cookbook pattern check. The tests cover minute overflow, exact closing time, a full working day, oversized and next-day appointments, and custom closing hours. No Google credentials or network calls are used. AI tool: Codex. Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
1a07cbcb4f |
feat: unify browser origin policy across AgentOS surfaces (#10869)
## Summary Supersedes #10093, rebased onto current main. The original branch still carried the pre-squash #10090 commit and conflicted with v3.1. This PR re-applies only the browser-origin-policy commit (authorship preserved) on top of main. What the change does: - Adds `AgentOS(cors=CORSConfig(origins=..., origin_regex=..., merge_base_app=...))` and opt-in `PublicSurface(enforce_browser_origins=True)` - One exact/full-match `OriginPolicy` shared by CORS preflights, public run/cancel admission, auth error headers, workflow WebSockets and opted-in MCP aliases - CORS is now always the outermost middleware, so 401/403/413/429 responses carry consistent headers - Base-app CORS origins and regex patterns are merged by default; `CORSConfig(merge_base_app=False)` replaces them Changes vs #10093: - The two new loose kwargs (`cors_allowed_origin_regex`, `cors_merge_base_app_origins`) are grouped into `CORSConfig`, matching `MCPConfig` / `QueueConfig`. They were never released, so nothing is removed from the public API - Not a breaking change: `cors_allowed_origins` keeps its existing behavior (`[]` or `None` still mean settings defaults). `CORSConfig(origins=[])` is the opt-in way to allow no exact origins. Passing both `cors` and `cors_allowed_origins` raises `ValueError` - Origin patterns are combined with scoped inline flags, so a `(?i)` or `(?x)` regex (from `CORSConfig` or a `base_app` `CORSMiddleware`) no longer crashes `get_app()`. `origin_regex` is validated when `CORSConfig` is constructed - `get_app()` no longer rewrites `AgentOS.cors_allowed_origins` with merged base-app origins Conflict resolution: - `AuthMiddleware._is_origin_allowed` keeps main's fail-closed behavior (no allow-list reflects nothing). The shared policy is checked first; the original PR's `None -> allow all` branch is dropped since it would have reverted that fix - Kept main's `test_reflects_nothing_when_empty_origins_list`; the PR's rename is redundant now - Merged the new `cors_*` params into the v3.1 `AgentOS` docstring - README, TEST_LOG and `test_mcp_routing.py`: kept both sides ## Type of change - [x] Bug fix - [x] New feature - [ ] Breaking change - [x] Improvement --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [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 - Replaces #10093 to avoid force-pushing over the original branch. - `ruff format --check` and `ruff check` pass on touched files. mypy errors locally are only in unrelated MCP/knowledge modules (local `mcp` package version). - PR tests: 202 passed (`test_origin_policy`, `test_jwt_middleware_helpers`, `test_mcp_routing`, `test_public_authorization`). The new legacy-`[]`, no-mutation and inline-flag tests fail against the previous commit. Full `tests/unit/os`: the 53 failures are the same set that fails on main in the same env (`croniter` missing, `mcp` version). - Behavior notes for existing apps: CORS is now always the outermost middleware (it only adds CORS headers to auth errors for already-allowed origins), and a `base_app` `CORSMiddleware(allow_origin_regex=...)` is now honored instead of silently dropped. --------- Co-authored-by: Ashpreet <[email protected]> |
||
|
|
6f01068cc0 |
[feat] Add Heabsy as an OpenAI-compatible model provider (#10828)
## Summary Adds [Heabsy](https://heabsy.com/platform), an OpenAI-compatible inference API for open models, as a model provider. It follows the "Adding a new Model Provider" section of CONTRIBUTING.md and the layout of #10326 (Y-API) and #9788 (Synthorai): - `libs/agno/agno/models/heabsy/heabsy.py`: `Heabsy(OpenAILike)` with `id="qwen38"`, `name="Heabsy"`, `provider="Heabsy"`, `base_url="https://api.heabsy.com/v1"` and a `HEABSY_API_KEY` fallback in `_get_client_params()` that raises `ModelAuthenticationError` when no key is set. - `libs/agno/agno/models/utils.py`: one row in `_PROVIDERS`, `"heabsy": ("agno.models.heabsy", "Heabsy", "Heabsy", "heabsy")`, between `groq` and `huggingface`. - `libs/agno/tests/unit/models/test_heabsy.py`: defaults, env-var key, missing-key error, client params, `"heabsy:qwen38"` string resolution and the `to_dict()` / `get_model_from_dict()` round trip. - `cookbook/90_models/heabsy/`: `README.md` (key setup, model notes, links), `basic.py`, `tool_use.py`, `structured_output.py`, `TEST_LOG.md`, with both the class and the string syntax. The API serves `/v1/models` and `/v1/chat/completions` with streaming. There is no `/v1/embeddings` endpoint, so no embedder is added. Keys are issued in the Heabsy console (https://platform.heabsy.com); accounts are opened on request at https://heabsy.com/contacts. ## Type of change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [x] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Checked in a fresh venv (Python 3.13, `pip install -e libs/agnoctl -e "libs/agno[dev,models]"`, ruff 0.15.20, mypy 2.1.0): | Check | Result | | --- | --- | | `pytest libs/agno/tests/unit/models/test_heabsy.py` | 7 passed | | `pytest libs/agno/tests/unit/models libs/agno/tests/unit/utils/test_model_string.py` | 1520 passed, 3 skipped (1511 passed before; the new ones are `test_heabsy.py` and the two `heabsy` cases in `test_provider_resolution.py`; `test_every_model_subclass_is_registered` passes) | | `./scripts/format.sh` | no changes to the files in this PR (it re-sorts imports in 6 unrelated files on `main`, left out) | | `./scripts/validate.sh` | ruff clean for agno, agnoctl and cookbook; mypy reports the same 6 errors as on `main`, none in `models/heabsy` | | `python3 cookbook/scripts/check_cookbook_pattern.py --base-dir cookbook/90_models/heabsy --recursive` | 3 files, 0 violations | The cookbook examples were imported but not run against the live API from this environment, so `TEST_LOG.md` marks them SKIP. If you want to run the cookbook examples yourself, I can send a test API key for review; just say where to send it. I work on Heabsy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Kaustubh <[email protected]> |
||
|
|
e1f4771529 |
fix: honor zero max_results in BaiduSearch, Searxng and Spider toolkits (#10877)
## Summary `BaiduSearchTools`, `Searxng` and `SpiderTools` resolve their result limits with falsy `or` fallbacks, so a configured value of **0** is silently discarded: - `BaiduSearchTools(fixed_max_results=0)` falls back to the per-call `max_results` argument (`agno/tools/baidusearch.py`). - `Searxng(fixed_max_results=0)` does the same when truncating the backend payload (`agno/tools/searxng.py`). - `SpiderTools(max_results=0)` falls back to the hardcoded default `5` (`agno/tools/spider.py` — the explicit call argument path was already fixed in #6401; only the constructor path was left). Reproduction (offline, no keys needed): ```python from agno.tools.spider import SpiderTools tools = SpiderTools(max_results=0) # tools.search_web("q") sends {"num": 5} to the client; expected {"num": 0} ``` The fix resolves the toolkit value with an explicit `is None` check, so `0` means "return no results" while `None` keeps the existing fallbacks. This matches the behavior already being added for `WebSearchTools`/`DuckDuckGoTools` in #10806 and extends it to the remaining toolkits that share the same pattern. Regression tests (all offline, mocked clients) cover: zero and non-zero `fixed_max_results`, explicit zero call arguments, the untouched defaults (`5`), and the BaiduSearch output format. The new `tests/unit/tools/test_baidusearch.py` follows the `pytest.importorskip` pattern used by `test_spider.py`. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`ruff check` and `ruff format --check` with the repo-pinned ruff 0.15.20) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue (#10806 covers WebSearchTools/DuckDuckGoTools only; #6401 fixed Spider's explicit-argument path only) - [ ] 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 Local validation: red-green confirmed — the three zero-value regression tests fail on the unpatched code and pass with the fix; the full `tests/unit/tools` suites for the three toolkits (33 tests) pass, plus `ruff check` and `ruff format --check` clean. Co-authored-by: Weijian He <[email protected]> Co-authored-by: Kaustubh <[email protected]> |
||
|
|
3713888fcc |
[fix] Unwrap Annotated types in tool JSON schema generation (#10858)
## Summary Fixes #10857 `get_json_schema_for_arg` had no branch for `typing.Annotated`, so Annotated hints (including dataclass fields that keep Annotated metadata) fell through to `{"type":"object","properties":{},"additionalProperties":false}`. This PR unwraps `Annotated[T, ...]` to `T` before schema generation. ## Type of change - [x] Bug fix ## Checklist - [x] I have read and understood the contribution guidelines - [x] I have added tests that fail without this fix - [x] PR title follows `[fix] ...` format ## Test plan - [x] `pytest libs/agno/tests/unit/utils/test_json_schema.py` — 22 passed - [x] Manual repro: `@tool` with dataclass `Annotated[int, Field(ge=0)]` fields now emit `{"type":"integer"}` Co-authored-by: michaelkillgta <[email protected]> Co-authored-by: Kaustubh <[email protected]> |
||
|
|
b2f642a468 |
chore: auto-close external PRs that skip triage or the PR template (#10866)
## Summary Tightens the contribution policy for external PRs, modeled on [MLflow's `auto-close-pr` workflow](https://github.com/mlflow/mlflow/blob/master/.github/workflows/auto-close-pr.js). External PRs now need a linked issue that a maintainer has triaged with a `ready` label, and must keep the PR template. This cuts down on unsolicited and low-effort AI-generated PRs: work only starts once a maintainer has agreed the issue is worth doing. Everything lives in the existing `PR Triage` workflow (`.github/workflows/pr-triage.yml`). **New auto-close rules (external contributors only)** | Rule | Label | Closes when | |---|---|---| | No linked issue | `missing-issue-link` | The PR doesn't close any issue. Previously label-only; now closes regardless of PR size | | Issue not triaged | `issue-not-ready` | Any linked issue lacks the `ready` label | | PR template not followed | `template-not-followed` | More than half of the template's headings are missing from the body. With the current 5 headings, 3 or more missing closes the PR | | Duplicate / assigned (existing) | `possible-duplicate`, `issue-assigned-to-other` | Unchanged, except duplicates now only count *earlier* open PRs | The close comment lists every finding, names the reasons, and tells the contributor to address them and reopen. It also asks them not to delete or force-push the branch, because a force-pushed branch can't be reopened. **Grandfathering** A `CUTOFF_DATE` constant, set to 7 October 2026 (`2026-10-07T00:00:00Z`), keeps the new rules away from in-flight work: - The `ready` check only applies to issues created on or after the cutoff. - The missing-issue and template closes only apply to PRs created on or after the cutoff. Older PRs still get the label but stay open. **Reopen flow** - The workflow now runs on `reopened` as well as `opened`. When a maintainer later labels the issue `ready`, the contributor reopens the PR and it passes. No extra comment is posted when a reopened PR passes. - If a maintainer reopens a PR, all checks are skipped. A sender with `admin`, `maintain` or `write` permission can always override an auto-close. **Exemptions** All checks are skipped for: - bots, e.g. Dependabot, which would otherwise be closed for having no issue - authors with `OWNER`, `MEMBER` or `COLLABORATOR` association - authors with write access, which covers org members with private membership **Other changes in `pr-triage.yml`** - Linked issues come from GraphQL `closingIssuesReferences` instead of regex. It covers `Fixes/Closes/Resolves #N`, full issue URLs and sidebar-linked issues. A bare `#123` or `see #5` no longer counts as a link. - The assignee check reuses the same GraphQL result, which removes one REST call per issue. - Template headings are read from the base branch's `.github/pull_request_template.md`, so the check stays in sync when the template changes. - If the linked-issue lookup or the template fetch fails, that check is skipped, so a transient API error never closes a PR. **Docs** - `.github/pull_request_template.md`: the issue line now says external contributors must use `Fixes #<issue_number>` and that the issue needs the `ready` label. - `CONTRIBUTING.md`: "Link to Issue" became "Link to a Ready Issue", which explains the triage flow and the reopen steps. Added a "Use the PR Template" rule. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [x] Other: CI / contribution policy --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **Before merging** 1. Create the label: `gh label create ready -R agno-agi/agno -c 0E8A16 -d "Triaged by a maintainer; open for PRs"`. The other labels (`issue-not-ready`, `template-not-followed`) are created automatically the first time they're applied. 2. Make sure maintainers know that outside issues now need a `ready` label before contributors can open PRs for them. If triage falls behind, good contributions will stall. **Testing** `pull_request_target` runs the workflow from the base branch, so this PR can't test itself. I extracted the script and ran it against mocked GitHub API calls in 18 scenarios, and all of them behaved as expected: - External PR with no issue: closed. A pre-cutoff PR reopened with no issue: label only. - New issue without `ready`: closed. With `ready`: passes. Pre-cutoff issue without the label: passes. - `ready` issue assigned to someone else, or claimed by an earlier open PR: closed. Claimed by a *later* PR: passes. - Author reopens while the issue is still not ready: closed again. After the issue is marked `ready`: passes with no comment. - Maintainer reopens: all checks skipped. - Template stripped, or 3 of 5 headings missing: closed. 2 of 5 missing: passes. Pre-cutoff PR: label only. - Org member or bot with no issue and no template: passes. An end-to-end run on a fork before or after merge is still worthwhile. The format and validate scripts don't apply because no Python files changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Harsh <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> |
||
|
|
ea21593946 |
[fix] Preserve cache-write tokens in the LiteLLM usage parser (#10476)
## Summary `LiteLLM._get_metrics` never read the prompt cache write from LiteLLM's usage, so `MessageMetrics.cache_write_tokens` was always 0 for Claude (and any other cache-writing provider) behind LiteLLM. LiteLLM reports it as `prompt_tokens_details.cache_write_tokens`, its canonical name, mirrored to `cache_creation_tokens`. The parser now reads the first and falls back to the second, on both the dict and the object path. Absent or null fields keep producing 0. Same defect as #10314, whose fix #10313 covers the two OpenAI parsers and not this file. The net/gross `input_tokens` question from #9538 is left alone here. Fixes #10475. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [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 #10313 is the sibling fix for `openai/chat.py` and `openai/responses.py`; this PR only touches `litellm/chat.py`, so the two do not overlap. Tests cover both usage shapes plus the real `litellm.types.utils.Usage` type; `mypy` reports the same six pre-existing errors on the file before and after. Written with Claude Code, reviewed and run locally by me. Co-authored-by: Kaustubh <[email protected]> |
||
|
|
326d7e29a6 |
[fix] Preserve OpenAI cache-write usage metrics (#10313)
## Summary OpenAI reports `cache_write_tokens`, but both usage parsers leave `MessageMetrics.cache_write_tokens` at zero. Preserve the counter in Chat Completions and Responses, defaulting to zero when absent or null for older SDKs and compatible providers. Two production lines, plus eight parameterized regression cases in the existing metrics tests. No dependency or API changes. Fixes #10314. ## Type of change - [x] Bug fix ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Tests added/updated - [x] Tested in clean environment - Documentation and cookbook changes are not applicable to this internal metrics correction. ### Duplicate and AI-Generated PR Check - [x] Searched existing open pull requests; no duplicate found - [x] This PR was entirely AI-generated with Codex ## Validation - Before the fix: both nonzero-write cases fail (`0 != 3000`). - After the fix: all 108 OpenAI and Azure Responses unit tests pass with OpenAI SDK 3.16.1. - All 13 metrics tests also pass with the minimum supported OpenAI SDK 1.106.0. - Both repository format/validation scripts pass. - Full unit-suite collection is blocked by missing optional provider, tool, and database dependencies in the development environment (124 collection errors); no full-suite pass is claimed. Co-authored-by: Kaustubh <[email protected]> |
||
|
|
932c89fa9c |
[feat] Add FlexAI as an OpenAI-compatible model provider (#10871)
Closes #10872 ## Summary Adds **FlexAI** (`https://api.flex.ai/v1`) as an OpenAI-compatible model provider, following the "Adding a new Model Provider" section of `CONTRIBUTING.md` and the same layout as the Y-API provider (#10326): - `libs/agno/agno/models/flexai/flexai.py`: `FlexAI(OpenAILike)`, with `id` / `name` / `provider` / `api_key` / `base_url` attributes and a `FLEXAI_API_KEY` fallback in `_get_client_params()`. The default id is `DeepSeek-V4-Flash-0731` (1M context, supports tool calls). - `libs/agno/agno/models/utils.py`: one row in the `_PROVIDERS` table (`"flexai": ("agno.models.flexai", "FlexAI", "FlexAI", "flexai")`), placed alphabetically after `fireworks`. - `libs/agno/tests/unit/models/flexai/test_flexai.py`: unit tests for defaults, env-var auth, missing-key error, client params, `"flexai:<model>"` string resolution, and the `to_dict()` / `get_model_from_dict()` round trip. - `cookbook/90_models/flexai/`: `README.md`, `basic.py`, `tool_use.py`, `TEST_LOG.md`, showing both the class and the string syntax. FlexAI serves open-weight models (DeepSeek, Qwen, GLM, Gemma, gpt-oss, Llama, MiniMax) behind an OpenAI-compatible API (`/chat/completions`, `/completions`, `/embeddings`, `/models`). Docs: https://docs.flex.ai/inference-api/quickstart. The cookbook README lists the models with verified tool-calling and image support, and points to `GET /v1/models` for the live catalogue rather than hardcoding it. (If applicable, issue number: none; this adds a provider rather than fixing reported behaviour.) ## Type of change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [x] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **AI disclosure:** this PR was drafted with Claude Code and reviewed before submission. **Verification** (fresh `.venv` from `./scripts/dev_setup.sh`; ruff 0.15.20, mypy 2.1.0): | Check | Result | | --- | --- | | `pytest tests/unit/models/flexai tests/unit/models/test_provider_resolution.py tests/unit/utils/test_model_string.py -q` | 151 passed, 45 skipped (includes `test_every_model_subclass_is_registered`) | | `ruff format --check` / `ruff check` on the touched paths | clean | | `python cookbook/scripts/check_cookbook_pattern.py --base-dir cookbook/90_models/flexai --recursive` | 2 files, 0 violations | | `./scripts/validate.sh` | ruff check clean for agno, agnoctl and cookbook. mypy reports 6 errors in files this PR does not touch (`os/public/_limits.py`, `db/sql/authz.py`, `db/{sqlite,postgres,mysql}/async_*.py`); none in `models/flexai` or `models/utils.py` | | `./scripts/format.sh` | no changes to the files in this PR (it also reformats 8 pre-existing files on `main`; those changes are not included here) | **Live test** against `https://api.flex.ai/v1` with a real `FLEXAI_API_KEY`: ``` 1) Agent.run with tool: DeepSeek-V4-Flash-0731 FlexAI | tool calls: ['Paris'] | The temperature in Paris is currently **21 degrees Celsius**. 2) string syntax: FlexAI https://api.flex.ai/v1 | ok 3) async stream: ok ``` (1) is `Agent(model=FlexAI(), tools=[get_temperature]).run(...)`, where the model called the local tool and used its result. (2) is `Agent(model="flexai:DeepSeek-V4-Flash-0731")`. (3) is `arun(..., stream=True)`. A `response_format` `json_schema` request with `strict: true` also returned valid JSON, so the inherited `supports_native_structured_outputs` default is left as is. **API notes** (also in the cookbook README): the chat endpoint rejects `n` greater than 1 and `verbosity`, and neither is sent by default. Reasoning models return `reasoning_content`, which `OpenAIChat` already parses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KhTQnAVFEyMD4SDoAdpJ6h --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Kaustubh <[email protected]> |
||
|
|
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]> |
||
|
|
e7f7bd3415 |
[fix] Include response schema in model cache keys (#10621)
## Summary `_get_model_cache_key` currently serializes a Pydantic response-format class using its class name. Two different `Output` classes with different fields therefore generate the same key for identical messages and model settings. With `cache_response=True`, the second call can reuse a response generated for the wrong output schema. Serialize Pydantic response-format classes using `model_json_schema()` instead. This includes nested field definitions in the cache key while leaving other class serialization unchanged. This follows up on #7155, which made class inputs serializable but used the class name as the cache identity. ## Type of change - [x] Bug fix ## Checklist - [x] Code complies with style guidelines - [x] Self-review completed - [x] Documentation updated (regression test docstrings; no user-facing docs change) - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [x] This PR was generated with AI assistance and reviewed with focused tests. ## Additional Notes The new same-name schema tests cover streaming and non-streaming keys; equivalent schemas keep the same key. A mocked HTTP regression verifies that sync and async requests do not replay another output schema's cached response. `pytest -q libs/agno/tests/unit/models/test_cache_key_serialization.py`: 10 passed. `ruff check`, `ruff format --check`, and `git diff --check` pass. The broader model suite was not run locally because optional provider packages are missing; no provider call is needed for these focused tests. --------- Co-authored-by: Sannya Singal <[email protected]> Co-authored-by: sannya-singal <[email protected]> |
||
|
|
f1911c12c0 |
cookbook: add knowledge filesystem example and sync progress logs (#10868)
## Summary Add an AgentOS cookbook that syncs Agno documentation into PostgreSQL-backed knowledge pages, indexes them with PgVector and OpenAI embeddings, and exposes the `product-docs` namespace through the agent's filesystem. The agent can search the synced documentation, and the server starts after synchronization succeeds. Add built-in page synchronization logs for lock waiting, discovery, progress every 25 processed pages, and final counts. Long syncs now show progress without a custom CLI or progress callback in the cookbook. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [x] Other: Cookbook example --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [ ] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 The cookbook uses PostgreSQL at `localhost:5532` and requires `OPENAI_API_KEY` to sync documentation. A partial sync prints its report and stops before starting the server. Embedding retry behavior is unchanged. No tests or cookbook execution were run for this PR. Formatting ran on an isolated copy; its incidental import-order change was reverted. Validation was stopped at the user's request, and no further checks were run. |
||
|
|
97d3f588d2 |
fix: match Mongo text filters as literal text (#10709)
## Summary `MongoDb.get_sessions(session_name=...)` and `MongoDb.get_user_memories(search_content=...)` put the raw user string into `$regex`. `AsyncMongoDb` does the same, and `MongoVectorDb.keyword_search` does it with the search query. A value with an unbalanced `(` is an invalid pattern, so `GET /sessions` and `GET /memories` return HTTP 500. `.` matches any character, so a search for `v1.2` also returns `v1x2`. This PR passes the value through `re.escape()` at the 5 sites. The `$options: "i"` flag stays, so the match stays case-insensitive. (If applicable, issue number: #10708) I ran the AgentOS routes through `TestClient` on a `MongoDb` backed by mongomock. The rows are `Release v1.2 (draft)` and `Release v1x2 final` for sessions, and `likes v1.2 (draft) builds` and `likes v1x2 builds` for memories. | request | `main` | this branch | |---|---|---| | `GET /sessions?type=agent&session_name=v1.2` | 200, both rows | 200, `Release v1.2 (draft)` only | | `GET /memories?search_content=v1.2` | 200, both rows | 200, `likes v1.2 (draft) builds` only | | `GET /sessions?type=agent&session_name=(draft` | 500 | 200, `Release v1.2 (draft)` | | `GET /memories?search_content=(draft` | 500 | 200, `likes v1.2 (draft) builds` | ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 3 new tests call `get_sessions`, `get_user_memories` and `keyword_search` with `v1.2(draft` and assert the literal query `{"$regex": "v1\\.2\\(draft", "$options": "i"}`. One test is for `MongoDb`, one for `AsyncMongoDb` and one for `MongoVectorDb`. With the fix reverted, all 3 fail. With the fix, `test_mongo.py`, `test_async_mongo.py` and `vectordb/test_mongodb.py` give 53 passed and 1 skipped. `./scripts/format.sh` reformats the whole repo, so I ran `ruff format --check` and `ruff check` on the 6 changed files. Both pass. mypy reports the same 6 errors on `main` and on this branch, all in untouched files. Versions: ruff 0.15.20, mypy 2.1.0, pymongo 4.18.2, mongomock 4.3.0, Python 3.12.14. Open PR #10373 also edits `mongo.py` and `async_mongo.py`, in `upsert_sessions` only. The two diffs do not overlap. Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
a44cd8f123 |
[feat] Add 60db workspace voice tools (#10786)
## Summary 60db provides a hosted speech API with workspace-scoped voices. This adds `SixtyDBTools` for discovering those voices and generating speech as Agno WAV audio artifacts. The toolkit uses the existing HTTP dependency, supports a configured voice with per-call overrides, and reads one JSON response with `audio_base64` at the top level or directly under `backendResponse`. It decodes the audio once and validates and strips a WAV header when present. The cookbook discovers a workspace voice when none is configured, saves audio under `tmp/`, and prints the result. Failures log concise diagnostics without credentials or response bodies. Closes #10787 ## Type of change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [x] Examples and guides: Relevant cookbook examples have been included or updated - [x] Tested in clean environment - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [ ] 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 Validation: - 157 tests pass: 62 provider/cookbook checks plus existing toolkit and shared audio regressions. Provider checks cover both JSON locations, PCM/WAV output, malformed responses, unsupported response shapes, timeouts, response limits and redirects. - All 62 provider/cookbook tests pass against the rebuilt wheel installed in a separate environment. - `./scripts/format.sh` and `./scripts/validate.sh` pass, including repository lint, typing and cookbook pattern checks. Unrelated baseline import-format changes were excluded from this PR; scoped formatting checks pass. - Wheel and source distribution builds pass. Run `pytest libs/agno/tests/unit/tools/test_sixtydb.py` for provider checks using local HTTP fixtures without service credentials. The current JSON decoder and cookbook setup/output paths were verified locally. Live synthesis and the cookbook's LLM-driven conversation have not been rerun for this revision. --------- Co-authored-by: uditgoenka <[email protected]> |
||
|
|
f529ba0ff4 |
fix: recognize GPT reasoning models in OpenAI and OpenRouter Responses (#10830)
## Summary GPT-6 models were missing from OpenAI reasoning-model detection, and GPT-5/6 models were missing from OpenRouter detection. This could bypass reasoning-state handling and cause tool-call follow-ups to fail with missing reasoning-item errors. - Recognize GPT-6 in `OpenAIResponses` and GPT-5/6 in `OpenRouterResponses`, enabling existing chaining and encrypted reasoning replay. - Extend regression coverage for OpenAI sync/async streaming and non-streaming paths, plus OpenRouter stateless tool follow-ups. - Update the OpenRouter tool-use cookbook to demonstrate GPT reasoning. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [x] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [ ] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [ ] 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 Tests, cookbook execution, formatting, and validation remain pending, deferred per user instruction. ## Summary Describe key changes, mention related issues or motivation for the changes. (If applicable, issue number: #\_\_\_\_) ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] 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 - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.) Co-authored-by: Kaustubh <[email protected]> |
||
|
|
2ddddfb527 |
[fix] Preserve saved JsonDb tables when writes fail (#10794)
## Summary Fixes #10793. JsonDb overwrites a live table before JSON serialization and the filesystem write finish. A failed save can therefore destroy previously persisted sessions rather than leaving the last successful table readable. I reproduced this through public `JsonDb.upsert_session` with valid session data on macOS/Python 3.12 using an isolated subprocess and `RLIMIT_FSIZE`. The second save raises native `EFBIG`: the previous 347-byte valid table becomes a 411-byte partial JSON file. With this change the same error leaves all 347 prior bytes intact, and `get_session` still returns the previously saved session. Regular tables are serialized into a unique sibling file and published with `os.replace` only after the file closes successfully. Failed serialization/publication removes only the staging file this writer created. Existing write permissions, mode bits, file/directory symlinks and direct writes to nonregular paths are preserved. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 This patch and its regression tests were prepared with AI assistance. The actual filesystem failure was reproduced before the change, the resulting patch was reviewed, and the verification results below come from executed tests. ### Verification - Before the fix, three focused failure-preservation tests fail; seven success/compatibility controls pass. - After the fix, 132 tests pass and 35 are skipped across the changed writer tests and adjacent JSON session migration/run/eval tests. Tests cover native file-size failure, partial serialization failure, rename failure, private mode preservation, write denial, write-only files, table/directory symlinks, an owned FIFO, UTF-8 and staging filename collision ownership. - Root `./scripts/format.sh` and `./scripts/validate.sh` pass using the declared-compatible SQLAlchemy 2.0.48 development environment. The format script also touches seven unrelated pre-existing style discrepancies; those changes are excluded from this PR. - Fresh hosted [unchanged Validation](https://github.com/rudycelekli/agno/actions/runs/37157702686) passes all ten jobs: all five full unit-test groups with the configured PostgreSQL service, four style groups, and supported-runtime page guards. The QA pull-request merge tree exactly equals reviewed head `09a6851fa034c50ced55a3d1b796ed4be85cfd5e`. PR Lint also passes. No workflow, test selection or dependency constraints were changed. - After the capability-aware Windows symlink fixture helper, all 12 focused writer/UTF-8 tests pass and root format/validation pass again. The symlink controls skip on Windows only if native creation actually fails for missing privilege. - This local development environment omits optional all-test SDKs. An attempted adjacent Mongo-containing suite stops collection because `pymongo` is absent; that local run is not described as passing. Full test verification comes from the fresh hosted matrix above. This change protects a single regular table's prior content when publishing that table fails. It does not merge concurrent writers, make multi-table operations transactional, or guarantee power-loss durability. Atomic replacement requires the table directory to allow creation/rename. Existing mode bits are preserved; no claim is made about extended ACLs or arbitrary hard-link aliases. No LLM/API calls or real database services were needed for the focused reproduction. --------- Signed-off-by: Rudy Celekli <[email protected]> Co-authored-by: Sannya Singal <[email protected]> Co-authored-by: sannya-singal <[email protected]> |
||
|
|
9f80ff55d9 |
[fix] Preserve reserved characters in SQLTools credentials (#10800)
## Summary Constructing `SQLTools` with `password="p@ssword"` currently passes password `p` and host `ssword@localhost` to SQLAlchemy. Use `URL.create` to preserve the original connection fields and add eight offline regression cases for reserved characters, percent sequences and an omitted database. This was reproduced on current main; no existing issue is being claimed. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other ## Checklist - [x] Code complies with style guidelines - [ ] Ran format/validation scripts successfully (Windows root validation limitation explained below) - [x] Self-review completed - [x] Documentation updated (regression-test docstring) - [x] Examples and guides reviewed: no public API change or new example needed - [x] Tested in a dedicated Python 3.11 virtual environment - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] Searched existing open PRs; no matching credential-construction fix found - [x] Related PR differences explained below - [x] Code and tests were generated with Codex ## Additional Notes Compatibility note: `user=` and `password=` connection fields are now passed verbatim to SQLAlchemy. Pass raw credentials, not percent-encoded values. If you previously passed an encoded password such as `p%40ss` as a workaround for the old parser behavior, pass the original password `p@ss` instead. A literal `%40` in a raw password remains literal. The `db_url=` path is unchanged and still accepts a properly encoded full connection URL. The 12 SQL tests pass with SQLAlchemy 2.0.54 and previously passed with 2.1.3. With the missing local dependencies installed and SQLAlchemy 2.0.54, the Agno package validator passes Ruff and Mypy for all 1087 source files. Changed-file Mypy, agnoctl lint/Mypy and the cookbook lint/pattern checks also pass. Additional offline verification covers 32 cases across raw connection fields and encoded full `db_url` values, including reserved characters, literal percent sequences, Unicode credentials, and an omitted database. The generated PyMySQL connection arguments preserve the expected credentials; no live database connection was attempted. The Windows root formatter ran successfully; its unrelated formatting changes were excluded from this patch. The root `validate.bat` cannot complete because its final `ELSE` block has a commented-out closing parenthesis. Its package validators and the checks specified by the Linux root validator were run directly instead. The full integration/CI matrix and a live MySQL connection have not been exercised. Native SQLAlchemy 2.1 full-package type diagnostics were also reproduced on unchanged upstream; related compatibility work exists in #10656. PR #10417 changes engine precedence in the same constructor; #5008 expands SQL tooling and retains interpolated connection URLs. This fix preserves raw credentials without changing engine precedence or query behavior and may need rebasing if those PRs alter the same hunk. Co-authored-by: wToTw123 <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
ab0120677e |
[feat] Add Perplexity integration attribution (#10827)
## Summary - Perplexity would like to identify requests made through Agno's Perplexity integration. - Add `X-Pplx-Integration: agno` to the headers of both `PerplexitySearch.search` and `PerplexitySearch.asearch`, alongside the existing `X-Source: agno`. - Leave every other provider and request behavior unchanged. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Verification: `ruff format --check`, `ruff check .` and `mypy . --config-file pyproject.toml` pass in `libs/agno`, and `pytest libs/agno/tests/unit/tools/test_perplexity.py` passes (17 tests). The sync and async success tests now assert both headers. Co-authored-by: Kaustubh <[email protected]> |
||
|
|
1a91338857 |
[fix] Reject invalid file replacement line ranges (#10798)
`FileTools.replace_file_chunk` previously accepted negative, reversed, and out-of-bounds inclusive ranges, allowing invalid tool calls to alter an existing file. Validate the range before saving and return a useful error while preserving the original bytes. Also address the review's trailing-separator example: `a\nb\nc\n` contains three real lines, although splitting produces a fourth empty element. Exclude that virtual element from the valid line count while retaining it during reconstruction, so a valid replacement preserves the final terminator. Empty files, real blank lines, Unicode, and custom separators are covered. Changes are limited to `libs/agno/agno/tools/file/file.py` and its existing unit-test module. The review follow-up is an atomic append commit, `ea15e2aefae2a955074327ce3c32e0e1485cfa18`, preserving the original PR commit. It does not change the existing CRLF translation behavior. Local validation of this exact head on 2026-10-05: - Full official unit-test collection on Debian bookworm arm64 / Python 3.12.15: **21,512 passed, 97 skipped, 4,415 warnings**, 911.86s pytest / 923.569s wall, exit 0. Command: `python -m pytest ./libs/agno/tests/unit --ignore=libs/agno/tests/unit/models/litellm --ignore=libs/agno/tests/unit/tools/test_crawl4ai.py --ignore=libs/agno/tests/unit/tools/test_scrapegraph.py -q`. These are the repository's existing CI exclusions; no new exclusions or timeouts were added. - Complete file-tool module: `PYTHONPATH=libs/agno python -m pytest libs/agno/tests/unit/tools/test_filetools.py -q`: **50 passed**, 0.20s pytest / 0.438s wall, exit 0, using the existing project dev environment, Python 3.10.6. - The same 18 new boundary cases against the previous PR head: **7 failed, 11 passed**, 0.09s pytest / 0.345s wall, exit 1. Against the unmodified target base: **8 failed, 10 passed**, 0.12s pytest / 0.848s wall, exit 1. Both execute the real SDK against local files. - Repository `scripts/validate.sh` and the library validation command: exit 0, 3.257s and 25.637s respectively. Ruff, Mypy, and cookbook checks passed. - Repository formatting ran successfully on an isolated copy; the changed files stayed byte-identical. Seven existing formatting differences match the unmodified baseline and are excluded from this fix. - Compilation, import/source checks, and `git diff --check`: exit 0. Source hashes were verified before and after the full unit run. The first fresh-container attempt failed collection because `libGL.so.1` was missing. The same step failed on the unmodified base; installing the image's system dependencies in the isolated test container resolved it before the successful full run. Host services and the shared Python environment were not changed. All 958 currently open PR file lists were checked, with fresh data for changed heads; related file-tool patches and exact-head production methods were inspected. No other related change covers this terminal-line-count fix. AI assistance: implementation and tests were developed with Codex using `gpt-6.1-sol` agents and reviewed against the complete diff and real local execution. These results cover local SDK/unit behavior; live model and production-service behavior were not tested. The author remains responsible for the contribution. --------- Co-authored-by: sannya-singal <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
f3aa7be6af |
fix: create a separate approval record for each approval-required tool call in a run (#10812)
## Summary Each approval-required (`@approval`) tool call in a run now gets its own approval record. This applies to agents and teams. ### The issue When a run paused a second time for a new `@approval` tool call, no approval record was created for that call. The next continue then ran it using the approval resolved for the first call. Example with `pay_invoice` decorated with `@approval` + `@tool(requires_confirmation=True)`: 1. The run pauses on `pay_invoice(INV-1)`, and one pending record is created. 2. The admin approves it, and the run is continued by `run_id` with no requirements (the documented database-resolution flow). 3. INV-1 is paid. The model calls `pay_invoice(INV-2)`, and the run pauses again. 4. No pending record exists for INV-2, so there is nothing for the admin to approve. 5. Continuing the run pays INV-2 anyway. The run has one approval record, and it names INV-1 only. There were two causes, both in `agno/run/approval.py`: - **No record for the second call.** `create_approval_from_pause` skipped creating a record if any required tool in `run_response.tools` already had an `approval_id`. That list grows across every pause in a run, so the earlier call (already approved and run) made the check return the old id. - **The old record was reused.** The new call had no `approval_id`, so `_group_tools_by_approval` used the run-level fallback. That fallback returned the run's latest record, which was the approved INV-1 record, and the new call was confirmed with it. ### The fix - **`create_approval_from_pause` / `acreate_approval_from_pause`:** a record is skipped only when every required tool already has an `approval_id`. Tools without one get a new record. `_build_approval_dict` builds the record from the current pause only: `tool_name`, `tool_args` and `context.tool_names` come from the new approval calls, and `requirements` lists those calls plus any plain HITL calls paused in the same turn. The second record names INV-2, not INV-1. A repeated pause hook on the same pause still creates no duplicate. - **`_group_tools_by_approval` / `_agroup_tools_by_approval`:** a record found by a run-level or member-run lookup is not paired with a tool call it doesn't name in its `requirements`. Such a call is left without a record, so the continue is blocked. Records that don't list their tool calls behave as before. - **`check_and_apply_approval_resolution` / `acheck_and_apply_approval_resolution`:** only `approval_type="required"` tools are gated. Audit tools never get a required record, so they no longer block the continue. `metadata["approval"]` is now the record of the call this continue runs, not the oldest record in the run. - **Agent and team continue with requirements:** a requirement no longer replaces a tool call that already has a result. After a pause resolved from the approvals table, the requirement can hold an out-of-date copy of the call (confirmed, no result), and resending `run.requirements` ran the call a second time. ### Agent and team coverage | Scenario | Before | After | |---|---|---| | Agent with an `@approval` tool | Second call ran under the first call's approval | Separate record and admin decision per call | | Team with an `@approval` tool on the team leader | Same bug (the team pause hook uses the same `create_approval_from_pause`) | Separate record and admin decision per call | | Team with an `@approval` tool on a member agent | Each pause got its own record, but the second also named the first call and showed its arguments. If the second record was deleted, the first call's approval ran the second call | Second record names only the second call; a deleted record blocks the continue | The sync, async and streaming paths all go through the shared functions above. Related: #10600 handles a different route to the approval gate (continues that send tool or approval fields from the client). This PR covers the continue with no requirements, where the call picked up the old record inside `approval.py`. ## Reproduction <details> <summary>Reproduction code (AgentOS app)</summary> ```python """ Approval Reuse Across Pauses (AgentOS repro) ============================================ A second @approval tool call in the same run must get its own approval record. Before the fix, the second call had no record, and continuing the run executed it under the approval an admin resolved for the first call. Components served: - invoice-agent: agent on OpenAIResponses (needs OPENAI_API_KEY) - invoice-team: team with the tool on the team leader, OpenAIResponses - invoice-agent-scripted: agent, deterministic model (no API key needed) - invoice-team-scripted: team-level tool, deterministic model The real-model components run with parallel tool calls off, so every pay_invoice call is its own pause. The scripted model pays every invoice named in the user message (e.g. INV-1, INV-2), one tool call per model turn. Run the server: python <file>.py Run the client: python <file>.py --demo [--scripted] """ import argparse import json import os import re from typing import Any, AsyncIterator, Iterator, List import httpx from agno.agent import Agent from agno.approval import approval from agno.db.sqlite import SqliteDb from agno.metrics import MessageMetrics from agno.models.base import Model from agno.models.openai import OpenAIResponses from agno.models.response import ModelResponse, ModelResponseEvent from agno.os import AgentOS from agno.team import Team from agno.tools import tool BASE_URL = "http://localhost:7777" DB_FILE = "tmp/approval_repro.db" PAYMENTS: List[str] = [] # --------------------------------------------------------------------------- # Scripted Model: one pay_invoice call per turn for each invoice in the request # --------------------------------------------------------------------------- class InvoiceScriptedModel(Model): """Stateless, so it is safe to share across runs and sessions. Reads the latest user message for invoice numbers and counts the tool results that followed it: with n results, it pays invoice n + 1, or answers once every invoice is paid. """ def __init__(self): super().__init__(id="invoice-scripted", name="invoice-scripted", provider="test") def _next(self, messages: List[Any]) -> ModelResponse: last_user = max((i for i, m in enumerate(messages) if m.role == "user"), default=-1) user_text = str(messages[last_user].content) if last_user >= 0 else "" invoices = re.findall(r"\b[A-Z]+-\d+\b", user_text) or ["INV-1", "INV-2"] answered = sum(1 for m in messages[last_user + 1 :] if m.role == "tool") if answered < len(invoices): r = ModelResponse(role="assistant") args = {"invoice": invoices[answered], "payee": "ACME", "amount": "100.00"} r.tool_calls = [ { "id": f"call_{answered + 1}", "type": "function", "function": {"name": "pay_invoice", "arguments": json.dumps(args)}, } ] else: r = ModelResponse(content=f"Paid {', '.join(invoices)}.", role="assistant") r.event = ModelResponseEvent.assistant_response.value r.response_usage = MessageMetrics(input_tokens=10, output_tokens=5, total_tokens=15) return r def invoke(self, *a, **k): return self._next(k["messages"]) async def ainvoke(self, *a, **k): return self._next(k["messages"]) def invoke_stream(self, *a, **k) -> Iterator[ModelResponse]: yield self._next(k["messages"]) async def ainvoke_stream(self, *a, **k) -> AsyncIterator[ModelResponse]: yield self._next(k["messages"]) def _parse_provider_response(self, response: Any, **k) -> ModelResponse: return response if isinstance(response, ModelResponse) else ModelResponse() def _parse_provider_response_delta(self, response: Any) -> ModelResponse: return response if isinstance(response, ModelResponse) else ModelResponse() # --------------------------------------------------------------------------- # Approval-gated tool # --------------------------------------------------------------------------- @approval @tool(requires_confirmation=True) def pay_invoice(invoice: str, payee: str, amount: str) -> str: """Pay a vendor invoice. This moves money and cannot be undone. Args: invoice (str): Invoice number payee (str): Payee account amount (str): Amount to pay """ PAYMENTS.append(invoice) print(f"[ledger] PAID {invoice} to {payee} ({amount}). All payments: {PAYMENTS}") return f"PAID {invoice}" # --------------------------------------------------------------------------- # Create AgentOS # --------------------------------------------------------------------------- os.makedirs("tmp", exist_ok=True) db = SqliteDb(id="approval-repro-db", db_file=DB_FILE, approvals_table="approvals") LLM_INSTRUCTIONS = [ "You pay vendor invoices with the pay_invoice tool.", "Pay EVERY invoice the user lists. Call pay_invoice for one invoice, and as soon as its result comes back, " "call pay_invoice for the next unpaid invoice.", "Do not reply to the user or ask for confirmation until every listed invoice has been paid.", "Use payee ACME and amount 100.00 unless the user says otherwise.", ] agents = [ Agent( id="invoice-agent", name="Invoice Agent", model=OpenAIResponses(id="gpt-5.6-luna", parallel_tool_calls=False), tools=[pay_invoice], instructions=LLM_INSTRUCTIONS, db=db, ), Agent( id="invoice-agent-scripted", name="Invoice Agent (scripted)", model=InvoiceScriptedModel(), tools=[pay_invoice], db=db, ), ] teams = [ Team( id="invoice-team", name="Invoice Team", model=OpenAIResponses(id="gpt-5.6-luna", parallel_tool_calls=False), members=[ Agent( id="helper", name="Helper", role="Answers general questions", model=OpenAIResponses(id="gpt-5.6-luna"), ) ], tools=[pay_invoice], instructions=LLM_INSTRUCTIONS + ["Call pay_invoice yourself; do not delegate payments."], db=db, ), Team( id="invoice-team-scripted", name="Invoice Team (scripted)", model=InvoiceScriptedModel(), members=[Agent(id="helper-scripted", name="Helper", model=InvoiceScriptedModel())], tools=[pay_invoice], db=db, ), ] agent_os = AgentOS(id="approval-reuse-os", db=db, agents=agents, teams=teams) app = agent_os.get_app() # --------------------------------------------------------------------------- # Demo client: the documented "database resolution" flow over HTTP # --------------------------------------------------------------------------- def describe_approvals(client: httpx.Client, run_id: str) -> List[str]: out = [] resp = client.get("/approvals", params={"run_id": run_id, "approval_type": "required"}) for a in resp.json().get("data", []): calls = [ ( (r.get("tool_execution") or {}).get("tool_call_id"), ((r.get("tool_execution") or {}).get("tool_args") or {}).get("invoice"), ) for r in a.get("requirements") or [] ] out.append(f"{a['id'][:8]} {a['status']} (by {a.get('resolved_by')}) for {calls}") return out def describe_tools(run: dict) -> list: return [ (t.get("tool_call_id"), (t.get("tool_args") or {}).get("invoice"), t.get("confirmed")) for t in run.get("tools") or [] if t.get("tool_name") == "pay_invoice" ] def run_demo(kind: str, component_id: str) -> None: print(f"\n=== {kind}/{component_id} ===") with httpx.Client(base_url=BASE_URL, timeout=120.0) as client: resp = client.post( f"/{kind}/{component_id}/runs", data={"message": "Pay invoices INV-1 and INV-2 to ACME.", "stream": "false"}, ) resp.raise_for_status() run = resp.json() run_id, session_id = run["run_id"], run["session_id"] print(f"run: {run['status']} tools={describe_tools(run)}") for step in range(1, 5): print(f" approvals: {describe_approvals(client, run_id)}") if run.get("status") != "PAUSED": break pending = client.get( "/approvals", params={"run_id": run_id, "status": "pending", "approval_type": "required"} ).json()["data"] for a in pending: client.post(f"/approvals/{a['id']}/resolve", json={"status": "approved", "resolved_by": "admin"}) print(f" resolved {len(pending)} pending approval(s)") resp = client.post( f"/{kind}/{component_id}/runs/{run_id}/continue", data={"tools": "", "session_id": session_id, "stream": "false"}, ) if resp.status_code >= 400: print(f"continue {step}: HTTP {resp.status_code} {resp.text[:160]}") break run = resp.json() print(f"continue {step}: {run['status']} tools={describe_tools(run)}") records = client.get("/approvals", params={"run_id": run_id, "approval_type": "required"}).json() executed = [t for t in describe_tools(run) if t[2]] print( f"pay_invoice calls executed: {len(executed)}; required approval records: {records['meta']['total_count']}" ) # --------------------------------------------------------------------------- # Run AgentOS # --------------------------------------------------------------------------- if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--demo", action="store_true", help="Run the HTTP client against a server on port 7777.") parser.add_argument("--scripted", action="store_true", help="With --demo, drive the scripted components.") args = parser.parse_args() if args.demo: suffix = "-scripted" if args.scripted else "" run_demo("agents", f"invoice-agent{suffix}") run_demo("teams", f"invoice-team{suffix}") else: agent_os.serve(app=app, port=7777) ``` </details> ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes **Tests added** - `tests/unit/run/test_approval.py`: record creation on a later pause (new record for the new call only, with that call's requirements and args), no duplicate record from a repeated pause hook, and the fallback refusing a record that names a different call, including when the tool's own record is missing. Sync and async. - `tests/unit/run/test_approval_multi_pause.py`: two calls one after the other in one run produce two records and two admin decisions, and the second call doesn't run while its record is pending. Covers an agent and a team-leader tool, sync and async, with a scripted model. - `tests/integration/os/test_approval_multi_pause.py`: the AgentOS HTTP flow (`GET /approvals`, `POST /approvals/{id}/resolve`, `POST /{agents|teams}/{id}/runs/{run_id}/continue` with empty tools) for the agent and the team. It checks that the second call gets its own pending record, isn't run while that record is pending, and doesn't run if rejected. Uses a scripted model, so no API key is needed. - `tests/integration/agent/human_in_the_loop/test_approval_multi_pause.py` and `tests/integration/teams/human_in_the_loop/test_team_approval_multi_pause.py`: the same checks with a real model (`OpenAIResponses`, `parallel_tool_calls=False`). Skipped without `OPENAI_API_KEY`. All new tests fail on `main` and pass with this change. The existing run, agent, team and os unit suites pass. `validate.sh` reports mypy errors only in files this PR doesn't touch, and the same errors appear on `main`. **Behaviour change:** a continue with no requirements now blocks if a required tool call is paired with a record that names other tool calls. Before, that call ran under the other record. **Not in this PR** (same behaviour on `main`, to be fixed separately): one approval can still pay a call twice when a run is forked and the original is continued too, or when two continues for the same run arrive at once. A workflow continue with no payload doesn't read the approval record (see #9494). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Harsh <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
c44b082010 |
[fix] Read text and Markdown streams with non-string names (#10779)
## Summary `TextReader` and `MarkdownReader` call `.split()` on an input stream's `name`. For a stream opened from a file descriptor, `name` is an integer; a stream can also expose `name=None`. Both readers catch the resulting exception and return `[]`, dropping otherwise valid content. Check that the stream name is a string before extracting its prefix, in both `read()` and `async_read()`. Other names use each reader's existing fallback (`text_file` or `file`). Explicit document names and string-name behavior are preserved. The regression tests use a real descriptor-backed stream, plus binary and text in-memory streams. They check content, names, rewinding partially read inputs, and leaving caller-owned streams open. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed (Codex reviewed the diff and executed the regression tests) - [x] Documentation updated (comments explain descriptor-backed stream names) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (not applicable: existing reader interface) - [x] Tested in clean environment (isolated virtual environment) - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Implementation, regression tests, diff review, and validation were performed by Codex. Validation on Windows with Python 3.11.9, Ruff 0.15.20, mypy 2.1.0, and SQLAlchemy 2.0.54: ```sh python -X utf8 -m pytest libs/agno/tests/unit/reader/test_text_stream_names.py libs/agno/tests/unit/reader/test_text_reader.py libs/agno/tests/unit/reader/test_markdown_reader.py -q ./scripts/format.sh ./scripts/validate.sh git diff --check ``` - New regression suite before the fix: **20 failed, 12 passed**. - All three related test files after the fix: **81 passed**. - Repository formatting and final validation passed, including Ruff, mypy (1,087 Agno files and 21 agnoctl files), and the cookbook pattern check. Unrelated formatter changes were removed from the diff before final validation. - SQLAlchemy 2.1.3 initially produced six type errors outside the changed files. The same errors were reproduced on an unmodified checkout of this base; validation passed with 2.0.54, which is permitted by the current dependency specification. The separate 2.1 compatibility work is tracked in #10655 / #10656. - UTF-8 mode is enabled because existing tests write Unicode fixtures using the platform's default encoding. The new descriptor fixture writes explicit UTF-8 bytes. - These tests do not call models or require external services. Related PRs #10634 and #10489 fix JSONReader and CSVReader respectively. This patch only changes TextReader and MarkdownReader. #9976 concerns async chunking and changes a different method. Co-authored-by: zjp <你的邮箱> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
3feab68adf |
[cookbook] Fix DynamoDb import path in the DynamoDB cookbook README (#10788)
## Summary Fixes #10801 `cookbook/06_storage/dynamodb/README.md` imports `DynamoDb` from `agno.db.dynamodb`, a module that does not exist. The package ships `agno/db/dynamo/`, and `DynamoDb` is exported from the package root (`libs/agno/agno/db/dynamo/__init__.py`, `__all__ = ["DynamoDb"]`). Copying the snippet as written raises `ModuleNotFoundError`. The cookbook's own runnable example in this directory (`dynamo_for_agent.py`) already imports it correctly as `from agno.db import DynamoDb`, so this just aligns the README with the code sitting next to it. How it was found: I extracted every `from agno...` import in all 754 markdown files and checked each module path against the source tree. Of the 62 distinct `agno.*` modules referenced, this is the only one that does not exist. (If applicable, issue number: **#10801** — tracked by issue [#10801](https://github.com/agno-agi/agno/issues/10801).) ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) — not run: this is a one-line markdown change and touches no Python - [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 - [ ] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [x] This PR was produced with AI assistance, disclosed per CONTRIBUTING §5 --- ## Additional Notes Verification performed: - `ls libs/agno/agno/db/` shows `dynamo/`, not `dynamodb/`. - `libs/agno/agno/db/dynamo/__init__.py` does `from agno.db.dynamo.dynamo import DynamoDb` with `__all__ = ["DynamoDb"]`. - `grep -rn "agno.db.dynamodb"` across the repo returns exactly one line: the one changed here. - The sibling example `cookbook/06_storage/dynamodb/dynamo_for_agent.py` uses `from agno.db import DynamoDb`. Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
a184c9011b |
[fix] Handle non-string stream names in JSONReader (#10634)
## Summary Fixes #10632. `JSONReader.read()` and `async_read()` raise `AttributeError` for valid temporary file streams whose `name` is an integer or `None`. Use the existing `json_file` fallback for non-string stream names in the shared reading path. Explicit names and existing string-filename behavior are preserved. Add sync/async regression coverage for both non-string cases, plus explicit-name and multi-dot filename controls. The tests also verify parsed content, rewinding from EOF, and caller ownership of the stream. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) AI wrote the code, and I reviewed it line by line. --- ## Additional Notes Validation on macOS / Python 3.12.13: - Before the fix: the JSONReader module had **4 failed, 42 passed**, with all failures reproducing the reported `AttributeError`. - After the fix: `python -m pytest libs/agno/tests/unit/reader/test_json_reader.py -q` — **46 passed**. - Also reproduced the fix with real `TemporaryFile` and `SpooledTemporaryFile` inputs in both reading modes. - `./scripts/format.sh`, `./scripts/validate.sh`, `uv pip check`, and `git diff --check` passed. - Validation used SQLAlchemy 2.0.44 in the local development environment. Installing SQLAlchemy 2.1.1 produced four unrelated mypy errors in unchanged database/public-limit files; using 2.0.44 resolved them without changing source, dependency declarations, or check configuration. - Broader test execution was attempted but could not complete in the development environment: the reader directory had 12 collection errors, and `./scripts/test.sh` had 123 collection errors because optional provider/reader/vector-database SDKs were unavailable. No full-suite pass is claimed. No public API or usage pattern changes; documentation and cookbook updates are not applicable to this compatibility fix. The focused regression tests do not need model credentials or external services. Co-authored-by: bmh201708 <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
3ca74c272f |
chore: Release v3.1.1 (#10736)
# Changelog ## New Features - Live progress & cancellation for page sync: `Knowledge.stream_sync_pages()` / `astream_sync_pages()` yield typed `PageSyncProgress` snapshots plus one terminal `SyncReport`, and `sync_pages` / `async_sync_pages` accept an `on_progress` observer. A workflow function step can now yield `StepProgress(content=..., data=...)` before its `StepOutput`, emitted as a native `StepProgressEvent` under the existing workflow run/step ID — AgentOS streams it over the existing workflow REST/SSE route, and `AgentOSClient.run_workflow_stream()` parses it. Cancelling a sync now actually reaches the work instead of draining to the end. (#10098) - Page sync reports name failed pages: `SyncReport.failed_paths` lists the site paths of pages that failed to publish/delete (capped at the first 20, like `errors`; `failed` keeps the full count; defaults to `()` so existing callers/stored reports are unaffected). Logs now name the page and its cause chain, e.g. `Page sync failed for /guides/setup.md (SyncFailed: sync_failed <- ConnectError: [Errno 104] Connection reset by peer)`, plus a new `Page delete failed for …` warning on the prune path. (#10729) ## Bug Fixes - Retry transient page-fetch failures with backoff: on large corpora (e.g. [docs.agno.com](http://docs.agno.com/), 3,913 pages) 1–2 random pages failed per run with `Connection reset by peer`. Each attempt previously consumed the entire 30s fetch deadline (leaving no time to retry) and retries were too short — both are fixed with a bounded per-attempt timeout and backoff. (#10730) - Complete page sync for Mintlify-style sites: large Mintlify sites (e.g. [docs.langchain.com](http://docs.langchain.com/)) synced as `partial` even when every page indexed — and a `partial` result skips pruning, so removed pages stayed searchable indefinitely. Discovery now skips links to non-page files (`.json`, `.yaml`, `.xml`, `.csv`, `.pdf`, images/audio/video, archives, fonts — while keeping `.js`/`.css` as valid page names like `/guides/node.js`), and handles nested indexes, MDX code blocks and redirect aliases. Off-host page links still block pruning (by design). (#10726)v3.1.1 |
||
|
|
18413f7f60 |
fix: complete page sync for Mintlify-style sites (non-page links, nested indexes, MDX code, redirect aliases) (#10726)
## Summary Large Mintlify sites sync as `partial` today even though every documentation page indexes. A `partial` result skips pruning, so pages removed from the site stay searchable indefinitely. docs.langchain.com reproduces it. Four causes, all fixed here: 1. **Links to non-page files fail discovery.** LangChain's `llms.txt` has an "OpenAPI Specs" section: - `https://api.host.langchain.com/openapi.json` is off-host, which marks discovery incomplete. - Two same-site `.json` specs are treated as pages, fetched as `…openapi.json.md`, and 404 as page failures. Discovery now skips links to files that cannot be pages (specs, data, media, archives: `.json`, `.yaml`, `.xml`, `.csv`, `.pdf`, images, audio and video, archives, fonts) wherever they point. `.js` and `.css` are excluded, because pages can be named like `/guides/node.js`. **Off-host page links still block pruning:** `test_collisions_and_foreign_destinations_cannot_prune` is unchanged. 2. **Depth-first traversal exceeds `max_depth` on indexes the root lists directly.** Mintlify roots list every `/_llms/` index directly, and each index also links its children. Depth-first traversal follows the first index's chain down to depth 4+ and flags the run, even though the same indexes are reachable at depth 1. Discovery is now breadth-first, so each index is reached at its shallowest depth. `max_depth` and `max_indexes` are unchanged. 3. **Mintlify inlines component code into page Markdown.** Pages carry top-level MDX statements such as `export const PatternEmbed = ({...}) => {...}`. On LangChain's Deep Agents overview that's 48 KB of a 77 KB page. The code was chunked and embedded as content. The `fumadocs` and `mintlify` profiles now drop top-level `import`/`export` statements. `strip_esm=False` keeps them, and the `markdown` profile stays verbatim. The matching is conservative: - A statement must start a block (first line or after a blank line) and match ESM syntax, so a wrapped prose line beginning "export the file…" is kept. - Statements inside code fences are kept. - Multi-line statements end when brackets balance outside strings and comments. Quotes reset at line ends, so an apostrophe in embedded JSX text can't swallow the page. - A statement that never closes is kept rather than risk dropping the rest of the page. 4. **Listed pages that redirect elsewhere fail as pages.** A live sync of docs.langchain.com ended `partial` with 38 failed pages, and every one was a listed page that redirects somewhere else: - 24 to other hosts (reference.langchain.com, GitHub, academy.langchain.com, status and marketing pages): `SyncFailed`. - 4 to a `#section` of another page: `invalid_source_url`. - 10 to a same-site URL without `.md`, which serves HTML. That HTML was fetched and embedded as page text until the provider rejected it at its 8,192-token input limit. A listed `.md` page that redirects off-origin, to a section or to a non-Markdown URL now raises `PageMoved`. The page is **skipped, not failed**: it isn't retried and is never stored, and it doesn't make the run `partial`. If an older sync stored it, it's pruned. `SyncReport.skipped` and `skipped_paths` (up to 20) report skipped pages, and each is logged with its target. A move to another `.md` URL on the same origin is still followed. Index (`llms.txt`) redirects behave as before, and foreign index redirects still fail. A `.md` page answered with `text/html` now raises `PageNotMarkdown`, a failed page, instead of being embedded. The same sync surfaced a normalizer gap, also fixed: component tags with `>` or `<` inside quoted attributes (`<Tab title="From Fleet > Integrations">`, `<ResponseField type="Record<string, string>">`) were left in page text. They now render, and `ParamField` is named by its `path`/`query`/`body`/`header` attribute. ## Results on live corpora Discovery and profile output were measured without a database or model calls. The full sync ran on a docs-agent template stack with this branch mounted: | | Before | After | |---|---|---| | docs.langchain.com discovery | 1,668 entries incl. 2 `.json` specs, `complete=False` | 1,666 pages, `complete=True` | | docs.langchain.com `mintlify` output | inlined component code indexed | 1.7 MB removed across 55 of 1,666 pages; all 29 headings and every prose line kept on the Deep Agents page; the cut ends at the component's closing `};` | | docs.langchain.com full sync (Postgres + pgvector, `mintlify`) | `partial` in 1,147 s: 1,628 updated, **38 failed**, pruning skipped | `completed` in 243 s: 0 failed, 38 skipped, 1,628 pages stored, no alias stored | | docs.agno.com discovery | 3,913 pages, complete | unchanged | | docs.agno.com `fumadocs` output | — | byte-identical with and without `strip_esm` on all 3,913 pages, so existing indexes need no `index_version` bump | ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **Tests** - New unit tests: - Non-page links (same-site, off-host, mixed case, a `node.js` page) are skipped without marking discovery incomplete. - A five-deep `/_llms/` chain listed directly by the root completes. - ESM stripping on a Mintlify-shaped page, for both profiles. - The keep cases: disabled, unclosed, verbatim profile. - Redirects: aliases to another host, to a `#section` or to a non-`.md` URL raise `PageMoved` on the first request, with no retry. A move to another `.md` URL is followed, `llms.txt` redirects still are, and a `.md` page served as HTML raises `PageNotMarkdown`. - MDX: quoted attribute values containing `<` and `>`, and `ParamField` location names. - New integration test: an alias is skipped, not failed. The run stays `completed`, and the alias's copy stored by an earlier sync is pruned. - `tests/unit/knowledge` and `tests/unit/reader`: same results as `main`, plus the 4 new passing tests. Both trees share 35 failures from optional dependencies (docling, PDF image) missing in the local venv. - `tests/unit/knowledge/test_page_source_reliability.py` and `tests/unit/reader/test_mdx_normalize.py`: 53 passed. - `tests/integration/knowledge/test_page_storage.py` against PostgreSQL 18 + pgvector: 160 passed. The CLI test needs this worktree on `PYTHONPATH`, because it runs a subprocess. **Validation** - `./scripts/format.sh`: also reformats five unrelated files on current `main`. I reverted those to keep this diff scoped. - `./scripts/validate.sh`: ruff passes. mypy reports the same 54 errors as `main`, none in the changed files. **Cookbook** - `27_public_pages/README.md` documents breadth-first nested indexes, skipped non-page links, `strip_esm` and skipped redirect aliases (`SyncReport.skipped`). - TEST_LOG has entries for the discovery change and for the live LangChain sync before and after the redirect fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
150a4e4123 |
feat: add AgentOS lifecycle support for Knowledge page synchronization (#10098)
## Summary Function-based indexing workflows had no native way to report progress or to be stopped: applications created synthetic agent runs and a task/queue/heartbeat bridge only to show "N of M pages", and cancelling a workflow drained a function step to its end. This PR adds the primitives, proves them with one consumer that already exists in the repository, and makes cancellation reach the work. - `Knowledge.stream_sync_pages()` / `astream_sync_pages()` yield typed `PageSyncProgress` snapshots and one terminal `SyncReport`. `sync_pages` / `async_sync_pages` accept an `on_progress` observer. - A workflow function step can yield `StepProgress(content=..., data=...)` before its `StepOutput`. Streaming emits a native `StepProgressEvent` under the existing workflow run ID, step ID and retry attempt. No agent run or executor history is invented; non-streaming execution ignores progress. - AgentOS streams the event over its existing workflow REST/SSE route, and the existing `AgentOSClient.run_workflow_stream()` parses it. The `27_public_pages` example uses this end to end, with the sync trigger still bearer-protected. - Cancelling a workflow now stops a function step at its next yield: `Step` checks the run between the items a function yields, closes the function's generator so its cleanup runs, and raises the established `RunCancelledException`. For the example that closes the Knowledge stream, which cancels the page worker's budget: the worker stops before its next page, nothing is pruned and the writer lock is released. Related #8710 handles AG-UI presentation of workflow structure; it adds neither a function progress event nor Knowledge sync progress. ## How progress and cancellation travel ``` Knowledge.astream_sync_pages() libs/agno/agno/knowledge/knowledge.py │ PageSyncProgress (waiting → discovered → publishing → pruning), then SyncReport ▼ sync_source() function step cookbook/05_agent_os/27_public_pages/public_pages.py │ StepProgress(content="Processed 2 of 3 pages …", data=<all counts>), then StepOutput ▼ Step._afunction_events() libs/agno/agno/workflow/step.py │ between items: araise_if_cancelled(run_id); on cancel: aclose() the function, raise ▼ Workflow streaming loop libs/agno/agno/workflow/workflow.py (unchanged) │ StepProgressEvent (run_id, step_id, step_name, step_index, attempt) │ on cancel: WorkflowCancelled, then WorkflowCompleted as the stream-end sentinel ▼ AgentOS POST /workflows/sync-docs/runs existing REST/SSE route, bearer-protected trigger AgentOS POST …/runs/{run_id}/cancel existing cancel route ▼ AgentOSClient.run_workflow_stream() existing client; the new registry entry lets it parse the event AgentOSClient.cancel_workflow_run() ▼ page_sync_progress.py prints each line, then the report; non-zero exit on failure ``` Closing the function's generator is what carries the cancel down: `sync_source`'s `finally` closes the Knowledge stream, whose `finally` closes the bounded worker stream, whose `finally` sets the worker budget's cancel flag. The coordinator already checks that budget before every page and at its lock wait. ## Decisions a reviewer should know about | Decision | Reason | |---|---| | Consumer is the existing `AgentOSClient`, not MCP and not a second server | main already has a protected workflow trigger and a typed SSE client. One server, port 7777. | | The sync trigger stays bearer-only (`PAGE_DEMO_SYNC_TOKEN`) | Anonymous users can chat, search and read; they cannot start a sync (401, tested against PostgreSQL). | | Cancellation is fixed in `Step`, not in the workflow loop and not in the cookbook | `Step` owns the function's iterator. Agent and team executors are cancelled by the existing member-run cascade and raise from inside their own loops; a function has no run of its own, so the step checks the workflow run for it. The workflow's drain of agent/team executors is untouched. | | Cancellation is cooperative | It is observed at the function's next yield. A progress snapshot reports a page that has just been published, so that page and any page already in progress complete their own transactions before the worker stops; a function that never yields cannot be stopped. Measured live: against three pages, one and then two pages completed after the cancel request, the third never started. The README says so. | | `BoundedWorkers.stream`/`astream` stop applying the deadline once the worker has finished | A consumer that resumed after the deadline got `TimeoutError` instead of the buffered snapshots and the result, contradicting the documented contract. The deadline still applies while the worker runs. | | `astream_sync_pages` uses `try/finally` + `aclose()` | `contextlib.aclosing` does not exist on Python 3.9, which the library supports. A compatibility test guards it; the docstring says so. | | The example's workflow sets `events_to_skip=[WorkflowRunEvent.step_progress]` | AgentOS sets `store_events=True` on every workflow it serves and saved events are not capped; one progress event per page made a 3,913-item run save 2.05 MB of events (measured with a synthetic step of the same event shape). Progress is still streamed live. Follows `run_control/event_storage.py`. | | No `--check` mode in `page_sync_progress.py` | An offline check would bypass the consumer being demonstrated; the same path is covered by a real-route test. | Not in this PR: MCP delivery of step progress, Control Plane or AG-UI rendering of `StepProgress`, a framework-level default skip for Workflow, any authentication change. ## Changes | Area | Files | |---|---| | Knowledge progress | `knowledge/knowledge.py`, `knowledge/page/__init__.py`, `knowledge/page/_coordinator.py`, `knowledge/page/types.py`, `utils/bounded.py` | | Workflow progress event and cancellation | `workflow/types.py`, `workflow/step.py`, `run/workflow.py` | | Example | `27_public_pages/public_pages.py`, `page_sync_progress.py`, `README.md`, `TEST_LOG.md` | | Tests | `unit/workflow/test_function_progress.py`, `unit/workflow/test_workflow_event_stream.py`, `unit/os/test_client.py`, `unit/knowledge/test_page_contract.py`, `unit/knowledge/test_sync_progress_workers.py`, `unit/test_py39_compat.py`, `integration/os/test_workflow_runs.py`, `integration/os/test_public_surface.py`, `integration/knowledge/test_page_storage.py`, `integration/workflows/test_workflow_cancellation.py` | Merged with main at `3c83750cd`; the six conflicts (README, TEST_LOG, `knowledge.py`, `page/__init__.py`, `page/_coordinator.py`, `test_page_storage.py`) keep both sides. ## Test plan Python 3.12, PostgreSQL 18 with pgvector 0.8, all with `PYTHONPATH=libs/agno`. Every test that was written for a fix was seen failing first; stateful tests run twice. - [x] Cancellation, framework: a function step yielding ten pages, cancelled after the first, does no further work, its `finally` runs before the stream ends, exactly one `WorkflowCancelled` then `WorkflowCompleted`, run stored as cancelled; sync and async, streaming and non-streaming, twice each (`test_workflow_cancellation.py`) - [x] Cancellation, AgentOS: a cancel through `POST /workflows/sync-docs/runs/{run_id}/cancel` while the step is between pages stops it the same way, twice (`test_workflow_runs.py`) - [x] Cancellation, page sync: cancelling the example's `sync-docs` run cancels the page worker's budget, the worker ends and its capacity returns, twice (`test_page_contract.py`) - [x] `BoundedWorkers`: a finished worker's buffered snapshots and result survive a consumer that resumes after the deadline; an unfinished worker still hits the deadline; sync and async (`test_sync_progress_workers.py`) - [x] Focused set (client, page contract, function progress, event stream, sync workers, 3.9 compatibility): 112 passed, 2 skipped by design, twice - [x] `integration/os/test_workflow_runs.py`: 20 passed, twice - [x] `integration/os/test_public_surface.py` (PostgreSQL): 7 passed, twice. Anonymous 401; the trusted token receives progress before the report - [x] `unit/workflow`: 795 passed, 9 skipped. `unit/knowledge`: 1110 passed, 13 skipped. `unit/os`: 3331 passed, 26 skipped - [x] `integration/workflows`: no test fails that does not also fail on main without `OPENAI_API_KEY` - [x] `integration/knowledge/test_page_storage.py` (PostgreSQL): 159 passed, 0 skipped; progress tests twice; no temporary database left behind - [x] Python 3.9.6: every changed file parses; `bounded.py` runs the late-consumer scenario there - [x] `ruff` clean on every changed file; `./scripts/validate.sh`: 53 mypy diagnostics, identical to main at `3c83750cd` (zero new) - [x] Live on the final tree (AgentOS on 127.0.0.1:7777, PostgreSQL 18/pgvector on 5532, `https://llmstxt.org/llms.txt`, OpenAI embeddings): anonymous and wrong-token sync 401; normal sync `unchanged`; `--reindex` `completed`, 3 updated, saved runs hold 4 events and no `StepProgress`; two cancels through `AgentOSClient.cancel_workflow_run` after "Discovered 3 pages": no progress after the cancel, `WorkflowCancelled` then `WorkflowCompleted`, run stored `CANCELLED`, no advisory lock left, revision 6→7 then 7→9 (one, then two pages completed after the request; the third never started), a normal sync right after each cancel took the lock and reported `unchanged`; clean shutdown, port closed, no leftover process, lock or database ## Type of change - [x] New feature - [x] Bug fix - [x] Improvement --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [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 Control Plane rendering remains an adoption gate: a consumer must render `StepProgress` before an application can remove its old display adapter. SSE transport keepalives remain separate from progress snapshots. --------- Co-authored-by: Himanshu Singh <[email protected]> Co-authored-by: kausmeows <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
1e72e54b8c |
fix: retry transient page fetch failures with backoff (#10730)
## Summary A Render deployment syncing docs.agno.com (3,913 pages) failed 1–2 random pages on every run, reported only as `Page sync failed (SyncFailed)`. Fetching each page in isolation never failed, from Render or locally. Wrapping `PageSource.fetch` during a real sync revealed the cause, on a different page each run: ``` SyncFailed: sync_failed <- ConnectError: [Errno 104] Connection reset by peer ``` Two problems combined: 1. **A stalled connection used up the whole fetch.** Each attempt's HTTP timeout was the entire remaining 30 s fetch deadline. Timing the failures on Render showed the failing attempt took about 30 s: a TLS handshake that hung, then ended in `Connection reset by peer`. No time was left for a retry. 2. **The retries were too short.** The fetcher retried transient failures only twice, waiting 0.25 s and then 0.5 s. This PR makes `PageSource.fetch`, used for both pages and nested indexes, more patient with transient failures: - **Each attempt gets its own timeout:** 5 s to connect, including the TLS handshake, and 10 s overall, always within the fetch deadline. A stalled handshake is abandoned after 5 s and retried on a fresh connection. Healthy page fetches take 0.05–0.2 s. - **Up to 5 attempts** with jittered exponential backoff: about 0.5, 1, 2 and 4 s, each multiplied by a random 0.5–1.0, so concurrent fetch workers don't retry in lockstep. - **`Retry-After` is honored** on 429/5xx (delta-seconds or HTTP date), capped at 10 s. - **Dropped connections are retried too:** `RemoteProtocolError` and `WriteError`, via `httpx.NetworkError`, alongside connect/read errors, timeouts, 429 and 5xx. - **The existing 30 s per-fetch deadline still bounds everything.** A retry that would wait past it fails immediately, with the transport error as `__cause__`. - **Unchanged:** other 4xx responses, foreign redirects, oversized bodies and cancellation still fail on the first request. DNS pinning and SNI behavior are unchanged. - **Configurable:** the policy is set by class attributes on `PageSource` (`fetch_attempts`, `retry_base_seconds`, `retry_max_seconds`, `retry_after_max_seconds`, `connect_timeout_seconds`, `attempt_timeout_seconds`), like the existing `max_pages`/`max_depth`. ## Live results on Render Each run was a real sync of docs.agno.com from the deployment's shell, with this PR's fetch logic swapped in at runtime. | Policy | Result | |---|---| | agno 3.1.0 (3 attempts, attempt timeout = the whole 30 s deadline) | 1–2 failed pages per run (`SyncFailed <- ConnectError: Connection reset by peer`) | | Backoff only (5 attempts, attempt timeout = the whole deadline) | Still 1 failed page: the attempt took about 30 s, so no retry could run | | This PR (backoff + 5 s connect / 10 s attempt timeouts) | The stalled handshake timed out at 5.0 s (`ConnectTimeout: The handshake operation timed out`), recovered on attempt 2; **0 failed pages** | A local reproduction with injected reset bursts (1.5 s every 20 s) gave 129 failed pages under the old policy and 0 under this one. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 **Tests** (`tests/unit/knowledge/test_page_source_reliability.py`) - A burst of four `Connection reset by peer` errors recovers on the fifth attempt, with waits of exactly `[0.5, 1, 2, 4]` (jitter pinned). - A 502, a `RemoteProtocolError` and a `WriteError` are each retried once. - `Retry-After: 3` is honored, and `Retry-After: 99` is capped at 10. - A `Retry-After` past the fetch deadline fails with no wait and keeps the `HTTPStatusError` cause. - `Retry-After` parsing covers delta-seconds, a future and a past HTTP date, and invalid values. - Each attempt's client gets `connect=5`/`read=10` timeouts, and a `ConnectTimeout` (stalled handshake) is retried. Attempt timeouts never exceed the remaining fetch deadline. - The existing pinned-DNS retry test now expects `fetch_attempts` requests when exhausted. Existing no-retry cases (foreign redirect, oversize, permanent 404, cancellation) are unchanged and pass. **Results** - After merging `main` (which now includes agno-agi/agno#10729): `test_page_storage.py` against PostgreSQL 18 + pgvector, together with the fetch reliability, page contract and per-page content tests: 255 passed. The `tests/unit/knowledge` failures are the same 33 optional-dependency failures as `main`, none page-related. - `./scripts/validate.sh`: ruff passes; mypy reports the same 54 pre-existing errors as `main`, none in `_source.py`. `ruff format --check` is clean on the changed files. `format.sh` was not applied because it reformats unrelated files on current `main`. **Live check and follow-ups** - A possible follow-up: each fetch opens a fresh TLS connection to the pinned IP. Reusing connections within a sync would also reduce the connection churn that likely triggers the resets. - Related: agno-agi/agno#10729 (merged) logs the cause chain for failed pages, which is how this was diagnosed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
516f599723 |
feat: name failed pages and their causes in page sync reports (#10729)
## Summary When a page fails during `Knowledge.sync_pages` / `async_sync_pages`, nothing says which page it was. `SyncReport` carries only counts and error codes, and the log line is `Page sync failed (RuntimeError)`. On a deployed docs corpus, one of 3,913 pages failed, and the only way to find it would have been querying the catalog tables directly. - **`SyncReport.failed_paths`:** the site paths (e.g. `"/guides/setup.md"`) of pages that failed to publish or delete in the run. It's capped at the first 20, like `errors`; `failed` stays the full count. It defaults to `()`, so existing callers and stored reports are unaffected. - **Logs name the page and the cause:** `Page sync failed for /guides/setup.md (SyncFailed: sync_failed <- ConnectError: [Errno 104] Connection reset by peer)`, plus a new `Page delete failed for …` warning on the prune path. Previously the fetcher's underlying error was dropped and the log said only `SyncFailed`. The cause chain is one line, bounded to 300 characters, logs only. How this was found: on a Render deployment, every full sync of docs.agno.com failed 1–2 random pages. Fetching each page in isolation, from Render or locally, never failed. Only wrapping the fetcher during a real sync revealed `ConnectError: [Errno 104] Connection reset by peer` on a different page each run. With this PR, the first failed run's log would have said that. This lets applications tell users which pages to check, and set their own tolerance. One example: treat a run where a handful of thousands of pages failed as a success that needs a retry. Agno's own `status="partial"` semantics, and pruning only after a clean run, are unchanged. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [x] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [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 - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 - Tests: `tests/integration/knowledge/test_page_storage.py` now asserts `failed_paths` for three cases: a publication failure after the vector write (plus its logged path and cause), a failed first sync and its clean retry (which reports none), and a failed prune deletion. A unit test formats the exact `SyncFailed <- ConnectError` chain from production. Against PostgreSQL 18 + pgvector, together with `test_page_contract.py` and `test_per_page_content_rows.py`: 234 passed. - `ruff format --check` is clean on the changed files; `format.sh` was not applied because it reformats unrelated files on current `main`. `./scripts/validate.sh`: ruff passes, and mypy has the same 54 pre-existing errors as `main`, none in these files. - Cookbook: `27_public_pages/README.md` describes reading a partial report. TEST_LOG has an entry. - Independent of agno-agi/agno#10726 (discovery and MDX fixes); the two touch different files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
ab1d6007f0 |
feat: Release v3.1.0 (#10702)
## New Features - **User Management / Authorization (RBAC):** a new `agno.os.authz` package adds role-based access control to AgentOS — a role store, scope policy, audit log, user directory and an admin router, with pluggable authorization engines (a native engine plus a fine-grained `fga` engine). Scopes and auth middleware were extended to enforce it. - **AgentOS Filesystem:** a new `agno.fs` filesystem (`DbFileSystem`) with dedicated `/filesystem` routes (list/read/manage files) backed by an `agno_fs` table. - **MCP config / auth updates:** refreshed MCP server config and built-in MCP auth handling. - **AIMLAPITools:** new toolkit for image, video, speech and transcription via the AI/ML API. ## ⚠️ Breaking Changes - Filesystem table re-key (`agno_fs`): v3.1 keys the filesystem table by `(namespace, user_id, path)`, where `user_id` is the user partition (`""` for the shared/no-user partition). A table created by an earlier release is refused with `SchemaOutdatedError` until upgraded — the re-key never runs automatically. Upgrade once, with the application stopped, using the script for your database (#10530): This affects you only if you use `DbFileSystem`. The table is managed by `DbFileSystem` (its own schema, buildable from a bare `db_url`), so it is deliberately not part of `MigrationManager`. ```python python libs/agno/migrations/migrate_filesystem_postgres.py # PostgreSQL python libs/agno/migrations/migrate_filesystem_sqlite.py # SQLite ``` ## Bug Fixes - **HITL:** continuing a paused run with `background` + `stream` no longer duplicates the paused run in its own history. - **OpenAIResponses:** recover requests when the chained (previous) response is gone. - **DB:** an async delete that *failed* is no longer reported as a run that *wasn't there* — the two cases are distinguished. - **Structured output:** don't crash when a JSON fragment provides a scalar before a list. - **Python tool results:** distinguish `None`valued results from missing variables. - **Chunking:** reject an overlap that is not smaller than the chunk size. - **CSV readers:** handle CR (`\r`) record endings. - **CsvTools:** honor explicit zero row limits. - **Shell / workspace command tools:** honor an explicit zero `tail`. - **GeminiTools:** store raw video bytes in the `generate_video` artifact. - **GithubTools:** build branch links from the repository web URL (#10509); `get_file_content` preserves valid non-ASCII UTF-8 text - **LocalFileSystemTools:** read and write local files as UTF-8. - **PubMed:** preserve metadata for short abstracts. - **Claude citations:** preserve nullable document titles. Co-authored-by: Sannya Singal <[email protected]>v3.1.0 |
||
|
|
657c9f70e9 |
feat: make MCPConfig default and lifecycle tools opt-in (#10285)
## Summary Target release: **the next Agno 3.0.x patch release** (exact patch version TBD). This changes configuration defaults and requires migration for integrations that rely on automatically included tools. `MCPConfig(tools=[...])` publishes exactly the application's listed tools. Both `default_tools` and `lifecycle_tools` default to `False`. `AgentOS(mcp=True)` and `MCPConfig(default_tools=True)` retain the complete eight-tool built-in surface, including continuation and cancellation. | Configuration | Tools published | | --- | --- | | `MCPConfig(tools=[...])` | Listed tools only | | `MCPConfig(tools=[...], lifecycle_tools=True)` | Listed tools plus `continue_run` and `cancel_run` for exposed components | | `MCPConfig(tools=[...], default_tools=True)` | Listed tools plus all eight built-ins | | `mcp=True` | All eight built-ins | `lifecycle_tools` is additive: `lifecycle_tools=False` does not remove tools enabled by `default_tools=True`. Existing tag filters, publication boundaries, scopes, and run ownership checks remain in effect. Ordinary session follow-ups do not need lifecycle tools. - Preserve deprecated aliases and provide actionable errors for configurations with no tools. - Migrate examples/tests that intentionally enable default or lifecycle tools; update public MCP validation guidance and migration documentation. - Show the MCP endpoint below the OS address in the startup panel when MCP is enabled, respecting the configured public URL/path and serving address. - Clarify stateless behavior: modern MCP requests are already sessionless; `stateless=True` also disables transport sessions for legacy clients. Agno conversation/run state remains independent. - Add full-app HTTP coverage for both protocol eras, authentication, registration, method-header validation, FastMCP settings inheritance, streamed progress, and disconnect cancellation. Run this coverage in PR CI. Migration examples: ```python # Publish exactly this tool; neither False flag is required. mcp = MCPConfig(tools=[product_agent.as_tool(name="ask_product_agent")]) # Custom component integrations using approval continuation/cancellation opt in. mcp = MCPConfig(tools=[approval_agent.as_tool(name="ask_approval_agent")], lifecycle_tools=True) # Metadata/transport-only configurations explicitly enable the complete default set. mcp = MCPConfig(default_tools=True, name="Support", stateless=True) ``` Bare `MCPConfig()` raises: pass `tools=[...]` or set `default_tools=True`. `include_tags` does not implicitly enable default tools. If a custom exposed component pauses without continuation enabled, its result explains how to resume through REST or enable lifecycle tools. ## Type of change - [ ] Bug fix - [ ] New feature - [x] Breaking change - [x] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [x] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Validation of the final lifecycle defaults: - Minimum supported stack (`mcp==2.1.0`, `fastmcp==4.0.0`): **812 passed, 6 skipped**, across MCP server/auth/exposure/routing/client and public-authorization suites. - Newer stack (`mcp==2.1.1`, `fastmcp==4.0.3`): **362 passed**, covering MCP server/exposure/result/public-configuration and HTTP protocol regressions. - Exact tool lists verified across agents, teams, and workflows for both flags, explicit overrides, and tag filtering. Lifecycle scope, ownership, cancellation, continuation, and collision tests explicitly enable the capability they exercise. - Real Agent execution with SQLite and an offline model verifies that custom-tool session follow-ups load prior messages without any built-in tools. - **Nine cookbook configuration/discovery checks passed** in an isolated demo environment; cookbook test logs updated. - Required `./scripts/format.sh`, `./scripts/validate.sh`, and `git diff --check` passed. Unrelated formatter changes were excluded. Earlier startup-banner validation covered eight configurations: disabled MCP, default endpoint, custom path, public URL, environment host/port, IPv6, TLS/root-path, and dedicated root host. The six skipped tests require PostgreSQL. Cookbook checks build apps and verify registration; they do not call live models, ingest remote documentation, or validate hosted clients. The disconnect test uses a deterministic agent stream and real ASGI transport/middleware. This PR leaves the stateless default and outbound `MCPTools.protocol_mode` unchanged; native MCP Tasks and interactive approvals remain separate work. --------- Co-authored-by: Sannya Singal <[email protected]> Co-authored-by: sannya-singal <[email protected]> |
||
|
+10 |
6a7b205414 |
feat: v3.1 (#10530)
- User Management - File system page - Optum Requests - MCP config update --------- Co-authored-by: Sannya Singal <[email protected]> Co-authored-by: Samuel Jupe <[email protected]> Co-authored-by: sannya-singal <[email protected]> Co-authored-by: Harsh <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> Co-authored-by: ysolanky <[email protected]> Co-authored-by: Mustafa Esoofally <[email protected]> Co-authored-by: Harshita Jain <[email protected]> Co-authored-by: Ashpreet <[email protected]> Co-authored-by: Harsh Sinha <[email protected]> Co-authored-by: Monalisha Mishra <[email protected]> Co-authored-by: Monalisha Mishra <[email protected]> Co-authored-by: Harsh Sinha <[email protected]> Co-authored-by: Ayush Jha <[email protected]> Co-authored-by: Ayush Jha <[email protected]> Co-authored-by: zocern <[email protected]> Co-authored-by: 许兆程 <[email protected]> Co-authored-by: Samuel Jupe <[email protected]> |
||
|
|
54468b8371 |
[fix] Don't crash when a JSON fragment provides a scalar before a list (#10654)
## Summary
`_parse_individual_json()` merges streamed JSON fragments field by
field. For a
list field it only created its accumulator when the key was **absent**,
so an
earlier fragment that supplied a scalar for the same field was extended
instead
of replaced:
```python
AttributeError: 'str' object has no attribute 'extend'
```
An implementation-level exception escaped instead of the helper's
documented
`None`-on-invalid result. Models commonly emit a draft scalar before the
real
list (`{"items": "draft"}` followed by `{"items": ["final"]}`), so this
is easy
to hit with `output_schema` on a list field.
## Root cause
```python
if isinstance(field_value, list):
if field_name not in merged_data: # key IS present (bound to a scalar)
merged_data[field_name] = []
merged_data[field_name].extend(field_value) # -> str/int/None/dict.extend
```
The same failure occurs with an earlier `str`, `int`, `None`, `bool` or
`dict`.
## Fix
Re/initialize the accumulator whenever its current value is not a list:
```diff
- if field_name not in merged_data:
+ if not isinstance(merged_data.get(field_name), list):
merged_data[field_name] = []
merged_data[field_name].extend(field_value)
```
This discards the incompatible earlier value and keeps the later list,
which
matches the last-value-wins treatment the function already applies to
scalar
fragments. The reverse order (list then scalar) is unchanged: the scalar
still
overwrites, validation fails, and the helper still returns `None`.
## Tests
Added to `libs/agno/tests/unit/utils/test_string.py`:
- `test_parse_individual_json_merges_lists` — existing list+list
behaviour preserved
- `test_parse_individual_json_discards_scalar_before_list` — `str` /
`int` / `null` / `bool` / `dict` before a list (the reported crash)
- `test_parse_individual_json_list_before_scalar_still_fails_softly`
- `test_parse_individual_json_scalar_only_still_fails_softly`
Verification:
- With the fix reverted (`git stash`),
`test_parse_individual_json_discards_scalar_before_list` fails with the
reported `AttributeError`; with it, all pass — the tests are
discriminating, not tautological.
- `pytest libs/agno/tests/unit/utils/test_string.py` → **48 passed**.
- `pytest libs/agno/tests/unit/utils` → the same 6 collection errors
before and after the change (missing optional deps: `openai`,
`google-genai`, `slack_sdk`, `fastapi`), i.e. no regression.
- Coverage of `agno/utils/string.py`: the new branch is fully covered;
no previously covered line becomes uncovered.
- `ruff format --check` clean; `ruff check` reports 21 diagnostics both
before and after (all pre-existing) — I used `list[str]` rather than
`List[str]` so as not to add a new one.
## Note on overlap
`avinash-git-24` commented on the issue shortly before this PR was
opened with
the same diagnosis and fix, and asked whether they could take it. No PR
existed
at the time of opening and the issue is unassigned. I have replied on
the issue
as well: if they would rather submit it themselves, I am happy to close
this one
rather than leave two duplicate patches in the queue.
## AI disclosure
This contribution was produced with AI assistance (ZCode / GLM, run
through the
WorkBuddy agent harness). The author reviewed the diagnosis, wrote and
ran the
reproduction, chose the fix over alternatives, and verified the tests
fail
before the change and pass after it.
Fixes #10649
---------
Co-authored-by: inchang-ing <[email protected]>
Co-authored-by: Sannya Singal <[email protected]>
|
||
|
|
13d3c9469c |
[fix] db: an async delete that failed is not a run that was not there (#10682)
Closes #10685 ## Problem `AsyncPostgresDb.delete_run` and `AsyncMySQLDb.delete_run` return `False` when the delete fails. `False` already means something else here - the docstring says *"True if the run was deleted, False otherwise"*, and it comes from `result.rowcount > 0`. So a dropped connection, a deadlock or a timeout reaches the caller as "there was no such run", and the run is still in the table. `delete_runs` in both files has the same handler without even the `return`, so it logs and falls out of the function. This is not an async convention. Across `agno/db`, 13 of 17 backends re-raise from these handlers, and that includes `AsyncSqliteDb` and `AsyncMongoDb` - two async backends that get it right. The two files here disagree with **their own sync twins**: | backend | sync `delete_run` | async `delete_run` | |---|---|---| | sqlite | raise | raise | | mongo | raise | raise | | postgres | raise | **return False** | | mysql | raise | **return False** | `postgres.py:1234` and `mysql.py:841` both end `log_error(...)` with `raise e`. The async copies kept the log line and dropped the raise. ## Changes Four lines. `raise e` in `delete_run` and `delete_runs` in `async_postgres.py` and `async_mysql.py`, matching the sync twin in each case exactly, including the `raise e` form the rest of the file uses. Nothing else is touched. `dynamo` and `surrealdb` swallow here too, but they have no sync twin to contradict them, so whether that is deliberate is a question for whoever wrote them rather than something to change in this PR. ## Testing Six unit tests, no database needed. The existing `test_async_postgres.py` fixture pattern is reused, and `test_async_mysql.py` is new because there was no unit test file for that backend. Against unmodified `main` four fail and two pass. The two that pass are the point: they pin that `delete_run` still returns `False` when there genuinely is no such run, so the change cannot quietly turn a missing run into an exception. ``` 11 passed # the two files 642 -> 642, 382 -> 382 # ruff findings before and after, unchanged 4 files already formatted # ruff format --check ``` The nine collection errors in `tests/unit/db/` are missing optional backend drivers. They are identical on a clean tree, so they are not from this change. ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) On that last box, since guideline 5 asks for it: I have left it unticked because "entirely AI-generated" is not accurate, and I would rather say what this is than tick or skip the box. The patch was written with AI assistance (Claude Code). The author reviewed it, ran everything quoted above, and takes responsibility for it, and the tests fail on `main` and pass here. --------- Co-authored-by: Kaustubh <[email protected]> |
||
|
|
eecade71ff |
fix: HITL continue with background and stream duplicates the paused run in its own history (#10597)
## Summary Fixes #10086. Supersedes #10088 by @rodrigocoliveira, who found the root cause and wrote the continue wiring tests adapted here. Thanks! **This is a major fix. Human-in-the-loop (HITL) does not work in AgentOS with its default settings.** The AgentOS UI sends `background=true` and `stream=true` on every run and continue: background mode is on by default for AgentOS above 2.6.0 when the agent or team has a database. With that setting, continuing a paused run fails for any agent or team that has `add_history_to_context=True` and uses a model that replays the full conversation, such as `OpenAIChat`: ``` An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. ``` Every HITL type is affected (confirmation, user input, external execution), for both agents and teams. Retrying doesn't help, because each background continue produces the same duplicate. ### Root cause A background continue saves the run as `PENDING`, then `RUNNING`, before it rebuilds the run's messages, so the run shows as in progress to anyone polling it. History then excludes runs only by status (`PAUSED`, `CANCELLED`, `ERROR`, `REGENERATED`), and the continued run no longer reads `PAUSED`, so it comes back as history. The model receives the paused turn twice: once from history, with no tool result, and once as the current turn. Chat Completions rejects the unanswered copy. ### Fix - **Continue history:** `continue_history_session()` in `session/_utils.py` returns a copy of the session without the continued run, matched by run id whatever the stored status. It runs before `num_history_runs` and `num_history_messages` are applied, so earlier turns in the session are kept. The original session object, including a cached one, is not changed. - **Wiring:** the Agent and Team continue message builders receive `run_response` at all 12 continue call sites (3 on Agent, 9 on Team). ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`): ran `ruff format`, `ruff check` and `mypy` on the changed files, which were clean apart from 4 existing mypy errors in files this PR doesn't touch. - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [x] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes ### Relation to #10088 #10088 fixes the same bug by excluding the continued run by id, and its root-cause write-up is correct. This PR keeps that approach and differs in two ways: 1. It keeps `exclude_run_ids` off the public `get_messages()` signature. 2. The change is much smaller. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Harsh <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Harsh Sinha <[email protected]> |
||
|
|
4db4180ad9 |
[fix] Handle CR record endings in CSV readers (#10657)
## Summary Fix CSV ingestion silently returning no documents for CR-delimited records in `CSVReader` and `FieldLabeledCSVReader`. Both readers already open synchronous path inputs with `newline=""`, but their in-memory wrappers use the default `StringIO` newline mode. With CR-only record separators, `csv.reader` raises `new-line character seen in unquoted field`; the readers catch it and return `[]`. This affects binary/text uploads in both modes and path inputs in async mode. Use `newline=""` for the six `StringIO` wrappers, matching the existing path handling. This recognizes LF, CRLF, and CR record boundaries while preserving quoted multiline cells for the existing cell normalization. Minimal reproduction (no network, model, or database needed): ```python import asyncio from io import BytesIO from agno.knowledge.reader.csv_reader import CSVReader payload = b"name,city\rAlice,Paris\rBob,Rome\r" reader = CSVReader(chunk=False) print(reader.read(BytesIO(payload))) print(asyncio.run(reader.async_read(BytesIO(payload)))) ``` Before: both calls log a CSV error and return `[]`. After: each returns a document containing all three rows. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Generated by Codex with the account holder's authorization. Codex performed the diff review, reproduction, and checks described here. No claim of independent human code review is made. Regression coverage includes both readers, sync/async calls, path/bytes/text inputs, LF/CRLF/CR separators, quoted multiline cells, small and paginated inputs, row order, names, and caller-owned stream lifetime. On upstream `476f61b000ef15a77ab3a6851b2c6cefbc7571c9`, the new test matrix reports **20 failed, 52 passed**. After the fix, it reports **72 passed**; combined with both existing CSV reader suites, **171 passed**. Validation on macOS ARM64, Python 3.12.14, pytest 9.1.1, Ruff 0.15.20, mypy 2.1.0, SQLAlchemy 2.0.54: ```text pytest libs/agno/tests/unit/reader/test_csv_newlines.py libs/agno/tests/unit/reader/test_csv_reader.py libs/agno/tests/unit/reader/test_csv_field_label_reader.py -q # 171 passed ./scripts/format.sh # passed; no unrelated formatting changes ./scripts/validate.sh # passed: Agno (1062 source files), agnoctl (21), cookbook lint and pattern checks git diff --check # passed ``` The initial development dependency resolution installed SQLAlchemy 2.1.1, under which full validation reported four type errors in unchanged database/public-limit modules. Validation passes with SQLAlchemy 2.0.54, within the project's declared dependency range. This PR does not alter dependency constraints or those modules. The entire repository test suite and live integrations were not run. No new public API or cookbook is needed; the regression test documents the expected newline behavior. Co-authored-by: lakers-abing <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
c0642f495c |
[fix] Distinguish None-valued Python tool results from missing variables (#10637)
## Summary Fixes #10636. PythonTools uses `namespace.get(variable_to_return)` to distinguish a result from a missing name. A defined variable with value `None` is consequently reported as missing after successful execution. Check namespace membership before retrieving the value in `run_python_code`, `save_to_file_and_run` and `run_python_file_return_variable`. Preserve the existing error for absent names and the string conversion of returned values. ## Type of change - [x] Bug fix ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) — limitations below - [x] Self-review completed by Codex; no human review is claimed - [ ] Documentation updated (existing return-value contract is unchanged) - [ ] Examples and guides updated (not applicable) - [x] Tested in clean environment - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] Searched existing open pull requests and found no matching fix - [ ] Similar PR explanation (not applicable) - [x] This PR was entirely AI-generated ## Verification Windows, Python 3.12.14, editable `agno[dev]` and `agnoctl[dev]` in an isolated virtual environment. - `test_python_tools.py` with new tests against unchanged main: **3 failed, 40 passed**. All three failures are the defined-None case, one for each execution helper. - With the fix: **43 passed**. New coverage includes `None`, `False`, `0`, empty strings/lists/dicts and genuinely missing names across all three helpers. Tests execute real local Python code and temporary files without a model service. - `scripts/format.sh`: passed; no unrelated changes. - `scripts/validate.sh`: Ruff checks and agnoctl mypy pass. Agno mypy has the same four upstream errors previously reproduced on unchanged main: `_limits.py:132`, `db/sqlite/async_sqlite.py:2469`, `db/postgres/async_postgres.py:2179`, `db/mysql/async_mysql.py:2096`. None of these files is changed. - The cookbook check's `python3` invocation is unavailable in the Windows shell. The equivalent command using `.venv/Scripts/python.exe` passes (13 files, 0 violations). - `git diff --check`: passed. ```sh .venv/Scripts/python.exe -m pytest libs/agno/tests/unit/tools/test_python_tools.py -q ``` Full-suite and live-provider end-to-end tests were not run; CI status is separate from these local checks. ## Additional Notes Based directly on upstream main, independent of the other submitted fixes. Implementation and tests were generated, run and self-reviewed by Codex under the account owner's authorization. Signed-off-by: icearia0219 <[email protected]> Co-authored-by: Kaustubh <[email protected]> |
||
|
|
476f61b000 |
[fix] Preserve PubMed metadata for short abstracts (#10640)
## Summary Fixes #10639. `PubmedTools.search_pubmed()` currently omits `Title` and `Published` from its default concise output whenever an abstract has 200 characters or fewer. This also affects articles without an abstract. Restrict the conditional expression to summary truncation so the metadata is always included, while preserving the existing 200-character cutoff and expanded output. Add regression coverage for short abstracts, exactly 200 characters, 201 characters, absent abstracts, and expanded results. The tests mock HTTP responses and exercise the real XML parsing and JSON output paths. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) AI wrote the code, and I reviewed it line by line. --- ## Additional Notes Validation on macOS / Python 3.12.13: - Before the fix, `python -m pytest libs/agno/tests/unit/tools/test_pubmed.py -q`: **3 failed, 9 passed**. The three failures reproduced missing title/year for short, exactly 200-character, and absent abstracts. - After the fix, the same command: **12 passed**. - Repeated the focused module in a newly created virtual environment with editable local `agno`/`agnoctl` packages and the required pytest plugins: **12 passed**. - `./scripts/format.sh`: passed, with no additional file changes. - `./scripts/validate.sh`: passed, including Ruff, mypy, and cookbook pattern checks. - `git diff --check`: passed. - `./scripts/test.sh` was attempted in the development environment but stopped during collection with **124 errors and 30 skips** because optional SDKs/dependencies were unavailable (including Google API clients, NumPy, Beautiful Soup, Anthropic, and vector-database adapters). The full suite did not run to completion; no full-suite pass is claimed. No new public API or usage pattern is introduced, so documentation and cookbook changes are not applicable. Existing result-limit and XML-parser PRs address separate behavior; this change is confined to concise result formatting and its tests. Co-authored-by: lily <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
c2d8fd0070 |
[fix] Preserve nullable document titles in Claude citations (#10147)
## Summary Claude can return a document citation with `document_title: null`. Agno serializes the text block with `model_dump(exclude_none=True)`, which removes that key. On the next turn, replaying the saved assistant message fails with HTTP 400: `page_location.document_title: Field required`. The shared content-block coercion helper now restores a missing `document_title` as `None` for page, character and content-block document citations. This covers response capture and replay, including histories already saved without the field and list-shaped assistant content. Existing titles are kept, and normalization copies the affected dictionaries so formatting a request does not change stored history. The production change is confined to one helper. Signed thinking blocks, block order, web citations and omission of unrelated null fields keep their existing behavior. ## Where this appeared We hit this in production with Agno 3.0.9 while running MindRoom: two follow-up replies were rejected because a saved page citation lacked `document_title`. The regression reproduces the field loss with the real Anthropic SDK and Agno's response parsers, then checks the stored representation and the next request. No live provider request was needed. Anthropic's request types declare `document_title` as `Required[Optional[str]]` for [page](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/citation_page_location_param.py), [character](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/citation_char_location_param.py) and [content-block](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/citation_content_block_location_param.py) citations: null is valid, omission is not. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](../../pulls) and confirmed that no other PR already addresses this issue - [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: #8686 removes output-only citations from server-tool result blocks. This addresses a different request-schema failure: restoring a required nullable field inside text-block document citations. The changes are complementary. Added 19 regression cases in `libs/agno/tests/unit/models/anthropic/test_citation_replay.py`. They cover streaming and non-streaming capture, JSON storage and replay, all three document-location types, existing titles, old histories in both supported representations, nonmutation, repeated formatting and web citations. On unmodified Agno, 12 cases fail and seven preservation checks pass; with the fix, all 19 pass. Local validation on Python 3.12 with Anthropic SDK 1.5.0: - Anthropic model and Claude utility tests: 267 passed, seven skipped. - Repository format and validation scripts pass with the standard development dependencies, including Ruff and mypy. - Independent review reran all 19 regression cases and found no high-confidence issues. The provider-enabled test environment additionally reports three mypy errors in unchanged Anthropic/AWS adapter code; the clean development environment passes full validation. No unrelated adapter changes are included. Drafted, tested and self-reviewed with Codex, then independently reviewed by a separate coding agent. Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
8c3d8ec52b |
chore: move Docling knowledge tests into their own CI job (#10499)
## Summary `test-knowledge-1` in Main Validation keeps hitting its 30-minute `timeout-minutes` and being cancelled, even after #10498 dropped the IMDB CSV. `test_docling_knowledge.py` is the largest single file in the job, it converts documents with local layout and OCR models, so it's slow on its own even when the API is fast. CI run: https://github.com/agno-agi/agno/actions/runs/35858299707/attempts/1?pr=10444 New docling CI job run: https://github.com/agno-agi/agno/actions/runs/35871483384/job/107216425586?pr=10499 ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] 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 - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.) --------- Co-authored-by: Kaustubh <[email protected]> |
||
|
|
087691ecb0 |
[fix] Honor explicit zero tail in shell and workspace command tools (#10593)
## Summary Fixes #10592. `ShellTools.run_shell_command`, `agno.utils.shell.run_shell_command`, and `Workspace.run_command` / `arun_command` slice command output with `[-tail:]`. For an explicit `tail=0` that slice becomes `[-0:]` == the whole list, so asking for zero lines returns **all** output. Guard the slice with `tail > 0` so `0` returns an empty string, mirroring the explicit-zero semantics #10492 established for `CsvTools.read_csv_file(row_limit=0)` (and proposed for these same sites in closed PR #8987). Positive tails and all error paths keep their current behavior. Sites changed: - `libs/agno/agno/tools/shell.py` (`run_shell_command`) - `libs/agno/agno/utils/shell.py` (`run_shell_command`) - `libs/agno/agno/tools/workspace.py` (`run_command` stdout + error paths, `arun_command` stdout + error paths) ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (ruff 0.15.20 check + format on the touched files) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [ ] 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 Six new regression/control cases across `test_shell_tools.py` and `test_workspace.py` (`tail=0` -> empty, positive tail sanity for both tools): **4 failed on the original code** (both zero-tail tool paths, the util path, and the async path) and all pass with the fix; the full `test_shell_tools.py` (6 passed) and `test_workspace.py` suites show an identical failure set before and after on this machine (3 pre-existing POSIX-only failures: `pwd`/`printf`-based tests). --------- Co-authored-by: Kaustubh <[email protected]> |
||
|
|
9f697b2db8 |
fix: store raw video bytes in GeminiTools.generate_video artifact (#10554)
## Summary
`GeminiTools.generate_video` built the returned `Video` artifact with
base64 **text** instead of the raw video bytes
(`libs/agno/agno/tools/models/gemini.py:186`):
```python
content=base64.b64encode(generated_video.video_bytes).decode("utf-8"),
```
`agno.media.Video.content` is declared `Optional[bytes]` ("Raw video
bytes"), so Pydantic coerced that string into the UTF-8 bytes of the
base64 text. Every consumer of `Video.get_content_bytes()` — media
offload uploads, writing the artifact to disk — then received base64
text instead of the video.
This PR passes the raw bytes instead, matching the `generate_image`
branch in the same toolkit and the other video toolkits (`opencv.py`,
`fal.py`, `minimax.py`, `replicate.py`, `wavespeed.py`, `lumalab.py`),
and drops the now-unused `base64` import.
Issue number: #10550
## Type of change
- [x] Bug fix
---
## Checklist
- [x] Code complies with style guidelines
- [x] Ran format/validation scripts (`./scripts/format.sh` and
`./scripts/validate.sh`)
- [x] Self-review completed
- [x] Documentation updated (comments, docstrings)
- [ ] Examples and guides: Relevant cookbook examples have been included
or updated (if applicable)
- [x] Tests added/updated (if applicable)
### Duplicate and AI-Generated PR Check
- [x] I have searched existing [open pull
requests](https://github.com/agno-agi/agno/pulls) and confirmed that no
other PR already addresses this issue
- [ ] 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
### Testing
Commands and results (run from `libs/agno` with the repo `.venv`):
**Regression test on unpatched `main`** (assertion updated to expect raw
bytes — fails before the fix):
```
$ pytest tests/unit/tools/models/test_gemini.py::test_generate_video_success -q
> assert video_artifact.content == b"fake_video_bytes"
E AssertionError: assert b'ZmFrZV92aWRlb19ieXRlcw==' == b'fake_video_bytes'
FAILED tests/unit/tools/models/test_gemini.py::test_generate_video_success
1 failed in 0.33s
```
**After the fix:**
```
$ pytest tests/unit/tools/models/test_gemini.py -q
11 passed in 0.40s
```
**Format / validation:**
```
$ ./scripts/format.sh
✓ ruff format — 2481 files left unchanged (agno); all targets pass
$ ./scripts/validate.sh
✓ ruff check — All checks passed! (agno, agnoctl, cookbook)
✗ mypy (agno) — 30 errors in 10 files
```
The mypy errors are pre-existing on unpatched `main`: stashing this
change and re-running produces the identical 30 errors in the same 10
files (`utils/models/_genai_compat.py`, `models/google/gemini.py`,
`knowledge/page/*`, `db/*`, ...). None are in the files changed here.
**Broader sweep** (no regressions): `pytest tests/unit/tools/ -q
--continue-on-collection-errors` reports an identical failure/error set
before and after this change (66 failed / 269 errors both times; the
failures are in unrelated modules and stem from optional dependencies
not installed in the local environment, e.g. `googleapiclient`, `ddgs`,
`mcp`).
---------
Co-authored-by: Sannya Singal <[email protected]>
Co-authored-by: sannya-singal <[email protected]>
|
||
|
|
0a9043c704 |
fix: reject an overlap that is not smaller than the chunk size (#10321)
Fixes #10347 ## Summary `DocumentChunking` did not validate its `overlap` argument, while `FixedSizeChunking` and `RecursiveChunking` raise for `overlap >= chunk_size`. The invalid configuration was accepted and silently produced chunks longer than the configured `chunk_size`: ```python from agno.knowledge.chunking.document import DocumentChunking from agno.knowledge.document.base import Document text = ("Para one. " * 20) + "\n\n" + ("Para two. " * 20) chunks = DocumentChunking(chunk_size=100, overlap=150).chunk(Document(name="d", content=text)) print([len(c.content) for c in chunks]) # before: [206, 206, 206, 161] chunk_size=100 # after: ValueError: Invalid parameters: overlap (150) must be less than chunk size (100). ``` The same arguments on `FixedSizeChunking` and `RecursiveChunking` raise `ValueError`, so the three strategies disagreed on what a valid overlap is. This is not the oversized-content case tracked in #10187 / #10188: here the arguments are invalid rather than the text. (If applicable, issue number: none — no issue was filed for this.) ## Type of change - [x] Bug fix - [ ] New feature - [x] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [x] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing [open pull requests](https://github.com/agno-agi/agno/pulls) and confirmed that no other PR already addresses this issue - [ ] 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 Disclosure: this PR was prepared with AI assistance; I directed the work and reviewed the final diff, which only adds the validation the sibling strategies already perform. Verification: - New tests in `libs/agno/tests/unit/knowledge/chunking/test_overlap_validation.py` parametrise all three strategies: `overlap` equal to and greater than `chunk_size` must raise, and the largest valid overlap (`chunk_size - 1`) must still chunk. Removing the check fails the two `DocumentChunking` cases (`2 failed, 7 passed`), so they pin the bug. - `pytest libs/agno/tests/unit/knowledge/chunking/test_overlap_validation.py test_fixed_size_chunking.py test_recursive_chunking.py` -> `15 passed`. Some modules of that directory need optional dependencies (`unstructured`, `chonkie`) and were not collected locally. - `./scripts/format.sh` and `./scripts/validate.sh` pass for `agno` ruff check, `agnoctl` (ruff + mypy) and `cookbook`. The `agno` mypy stage ends with 7 pre-existing `import-not-found` errors in files this PR does not touch (`knowledge/page/*`, `os/interfaces/whatsapp/router.py`, `utils/cryptography.py`, `utils/encryption.py`), because the local environment does not install every optional dependency. Co-authored-by: sclfcz <[email protected]> Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
025a01f768 |
[fix] Build GithubTools branch links from repository web URL (#10509)
## Summary `GithubTools.create_branch()` currently derives its returned branch link by replacing parts of the Git reference API URL. That works for `api.github.com`, but an Enterprise API URL such as `https://github.example.com/api/v3/repos/test-org/test-repo/git/refs/heads/feature/agent` becomes `https://github.example.com/api/v3/repos/test-org/test-repo/tree/feature/agent`, which is not the repository's branch page. Build the link from `repo.html_url`, the repository's web URL, instead. The existing GitHub.com test still passes, and a new test covers an Enterprise API URL and a branch name containing `/`. Fixes #10510. ## Type of change - [x] Bug fix --- ## Checklist - [x] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`): equivalent targeted Ruff checks and full-library mypy passed; the wrappers were not run - [x] Self-review completed - [ ] Documentation updated: no documentation change is needed for this return-value fix - [ ] Examples and guides: no new example is needed - [ ] Tested in clean environment: tests ran in an existing isolated virtual environment - [x] Tests added/updated ### Duplicate and AI-Generated PR Check - [x] I searched existing open pull requests and found no PR addressing this Enterprise branch URL issue - [ ] If a similar PR exists: none found - [x] This PR was prepared with an AI coding assistant. The complete diff and regression tests were reviewed and run locally. --- ## Validation - Before the fix, the new Enterprise regression failed: it returned a link containing `/api/v3/repos/`. - After the fix, `pytest .../test_github.py -q`: **32 passed**. The two changed files were fetched from current `main` (commit `8063a7e`); tests used a local source snapshot for the remaining package files. - Ruff check and Ruff format check with the repository's `pyproject.toml`: passed. - Full `libs/agno` mypy check on that local source snapshot with the patched `github.py`: **no issues in 1,061 source files**. No live GitHub Enterprise instance was needed; the test uses a mocked PyGithub repository and reference URL. --------- Co-authored-by: Sannya Singal <[email protected]> Co-authored-by: sannya-singal <[email protected]> |
||
|
|
76cde021e1 |
[fix] Read and write local files as UTF-8 in LocalFileSystemTools (#10538)
## Description `LocalFileSystemTools.read_file`/`write_file` used `Path.read_text()`/`write_text()` without an explicit encoding, so they default to the **locale** encoding. On a host whose preferred encoding is not UTF-8 (cp1252, GBK, or ASCII under `LC_ALL=C`), the tool mangles or rejects valid non-ASCII text: - `write_file` with non-ASCII content raises `UnicodeEncodeError` → swallowed by the tool's `except` → the agent gets `Error: Failed to write file: 'gbk' codec can't encode ...` and the file is never written. - `read_file` on a UTF-8 file under a non-UTF-8 locale either raises (`ascii` codec) or returns mojibake. Same class as the recently merged UTF-8 fixes for the YAML/Antigravity/Airflow readers and `GithubTools.get_file_content`. Fixes: none (found by auditing the codebase for text file IO without an explicit encoding). ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/agno-agi/agno/blob/main/CONTRIBUTING.md) and my code follows the project's style - [x] I have run `make lint` / formatting on the changed files (`ruff check` + `ruff format --check` clean) - [x] I have added tests — `test_read_write_round_trip_non_ascii_under_non_utf8_locale` in `libs/agno/tests/unit/tools/test_local_file_system.py` drives the round-trip in a subprocess with a forced non-UTF-8 locale: fails on the previous head, passes with the change; full file: 28 passed - [ ] I have updated the documentation (N/A — no public API change; both methods still accept and return `str`) - [x] My changes generate no new warnings ## AI Disclosure This change was prepared with AI assistance and reviewed line by line. Verification (red/green, lint, 28 passed) was run on this machine with the local checkout. Co-authored-by: Sannya Singal <[email protected]> |
||
|
|
660c0ee8c5 |
fix: make test_encoded_sse_cannot_bypass_public_error_inspection test more robust (#10533)
## Summary `test_encoded_sse_cannot_bypass_public_error_inspection` fails intermittently in CI https://github.com/agno-agi/agno/actions/runs/35972742169/job/107546006628, the follow-up request after the first run gets `503` instead of `200`, caused due to a race in the test setup. Adds a `post_when_slot_free` helper. It retries a request (up to ~5s) only while the response is `503` with error code `run_capacity`. ## Type of change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] 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 - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [ ] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.) |