mirror of
https://github.com/agno-agi/agno.git
synced 2026-10-11 22:09:58 +00:00
main
6184
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e1f77f52a |
feat: improve external agent APIs and framework cookbooks (#10926)
## Summary Make Claude and Codex agents easier to configure and test behind AgentOS. Ordinary settings no longer require untyped `_kwargs` dictionaries, and response printers return the final run and raise on failure so cookbook failures are visible. - Add typed native options and named tools, permissions, MCP and configuration fields. Named non-None settings take precedence; caller-owned configuration is copied. Legacy `_kwargs` aliases warn. - Group related constructor parameters and make built-in adapters keyword-only. Expose read-only `agent.sdk`; retain the Python `framework` compatibility alias and legacy session reads. API/session metadata emits only `sdk`. - Give sync/async response printers a shared result/error contract, tool-event deduplication and persistence warnings. Correct public streaming and async types, including optional final `RunOutput`. - Improve Claude/Codex cookbooks in their existing framework folders: streaming printers, native SDK comparisons, tool fixtures, structured output, sessions, media and AgentOS HTTP/SSE examples. Include a reproducible reliability kit and separately pinned historical results. - Integrate current main, including media, retries, metrics, compaction, structured output, and #10958/#10962 session-busy/replay changes. Preserve media on successful, failed and cancelled runs and both upstream/DX regression coverage. ## Type of change - [x] Bug fix - [x] New feature - [x] Breaking change - [x] Improvement - [ ] Model update - [x] Other: Cookbook and test coverage --- ## 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 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 ### Migration Use named constructor arguments and select the adapter class instead of passing `framework=`. Use `options`, `thread_options` and `turn_options` instead of their deprecated `_kwargs` names. Printers return the terminal `RunOutput` and raise on errors/cancellation by default; use `raise_on_error=False` to opt out. Unsupported separate media and native input objects fail explicitly. Import paths and transcript namespaces remain unchanged. Agno owns session selection; native lifecycle overrides that conflict with it are rejected. ### Validation — 11 October 2026 Integrated main `dbdca9ac6d9de6604383e6f4f04653a8244eb3c7` into DX head `c88e47d54de487f4d14b95c684a4c3e5889e8d0f`. From the normal checkout in the existing `.venvs/claude-dx-validation` environment: ```bash source .venvs/claude-dx-validation/bin/activate python -m pytest libs/agno/tests/unit/agents \ libs/agno/tests/unit/os/test_external_agent_background_stream.py \ libs/agno/tests/unit/os/test_schemas.py \ libs/agno/tests/unit/os/test_db_replay_fallback.py \ libs/agno/tests/unit/os/test_queue_worker.py \ libs/agno/tests/unit/run/test_queue_store.py \ libs/agno/tests/unit/os/test_ws_replay_floor.py -q -o addopts='' ./scripts/format.sh ./scripts/validate.sh ``` 620 tests pass, including public typing, media/schema combinations, retries, busy-session handling, replay and queue contracts. Full formatting and validation pass (1115 Agno / 21 agnoctl mypy files). No new live SDK calls were made for this merge refresh. Earlier live/provider and eight-hour paced-soak results are historical, with pinned revisions, first failures and limitations retained in the framework and reliability TEST_LOG files; they are not certification of this combined revision. The session-busy check is not an atomic distributed claim. Applications must serialize same-session submissions across replicas; the cookbooks state that limit. Native Claude resume and Codex bounded-history fallback remain distinct. Retries can repeat tool effects. Sandbox execution and final live release acceptance remain separate work. ### Final review follow-up — 11 October 2026 Final revision: `1cc091cc4c`. Found and fixed an interaction between native options and media: attachments now use the effective SDK workspace (including Claude native/legacy options and Codex thread/turn overrides) and absolute paths for relative working directories. Cleanup and recorded upload roots use the same workspace. Fifteen targeted cases failed before the fix; all 20 cases pass after. Expanded local validation: ```bash python -m pytest libs/agno/tests/unit/agents libs/agno/tests/unit/os \ libs/agno/tests/unit/run -q -o addopts='' python -m pytest libs/agno/tests/unit/os/test_public_json_bounds.py -q -o addopts='' ``` 4,484 unique tests pass across these runs; 23 cases skip. The broad run passed 4,467 cases; 17 HTTP cases initially hit the execution sandbox's loopback-bind restriction, then passed when the full 29-case HTTP file was rerun with loopback access. An initial optional Telegram import failure was resolved in the isolated validation environment. Required full format/validation passes. No new live provider calls or soak; final-commit CI is a separate merge gate. --------- Co-authored-by: kausmeows <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> |
||
|
|
dbdca9ac6d |
fix: harness soak follow-ups: tool errors, resume status, one turn per session (#10958)
## Summary Follow-ups from the external agent soak report, each reproduced live and fixed. The three items that fit in one PR are here; the related event-replay item is covered by #10957 (see below). ### Codex tool failures carry `tool_call_error` A failing MCP tool stored its error text with `tool_call_error=null`, and so did a shell command that exited non-zero. Codex marks both items `status=failed`; an MCP server that returns its error as a text result (`isError` on the MCP side) arrives with `error=null`, which is why the result text was right and the flag was not. The adapter now sets `tool_call_error` from the item status (`failed`, `declined`), the command exit code and the MCP error, on the streamed path, the non-streamed path and the per-item capture used for retries. Successful tools carry `False`. ### `/resume` on a run submitted with `stream=false` Resume on a still-running background run submitted with `stream=false` answered `"Run completed but no events stored."` with status `RUNNING`. A `stream=false` run never publishes to the event stream and stores no events, so there is nothing to attach to later. The database fallback now sends one `replay` frame that states the run's real status and, while it is pending or running, says to poll the run endpoint; a finished run says it stored no events. One helper serves the agent, team and workflow resume routes and the workflow websocket. The endpoint descriptions and both harness READMEs document the restriction: choose `stream=true` at submission when live or replayed events are needed. Making a non-streamed background run streamable afterwards would mean the worker publishing every job, which is a queue change and is not attempted here. ### One turn per session Codex accepted a second turn on a session whose first turn was still running and failed it on the app-server's thread lock (`already has an active writer`), leaving an error run in the session; Claude would have appended two turns to one transcript. The harness keeps one conversation per session on its side, so a session now takes one turn at a time: - A second turn is refused before anything runs with `SessionBusyError` (an `AgnoError` with status 409 and `error_type: session_busy`), naming the run in flight. No run row is written for the refused turn and the SDK is not called. - The check covers the turns this process is running and, when a database is configured, the session's pending and running run rows, so a turn accepted on another replica counts. A background submission is checked before its pending row is written, so the caller gets the 409 at submission. - Codex's own "already has an active writer" rejection maps to the same error, as the backstop for a turn that slips past the check on another replica; with `retries` set, that turn waits out the backoff and resumes the thread once it is free. The compaction guard reuses the same in-flight lookup. - AgentOS answers 409 on the non-streamed and background routes and a `RunError` event with `error_type: session_busy` on a stream. The streamer's generic error frame now carries the error's type and id. - A run row left pending or running by a crash keeps its session busy until that run is cancelled; the READMEs say so. A queued turn on a busy session is re-driven after the queue's retry delay when `max_attempts` allows it, which serializes it behind the turn in flight. ### Event replay after Redis retention expires The first part of this item, persisting replayable events for external agents at all, is fixed in #10957 (now on main and merged into this branch): streamed events are stored on the run row and AgentOS turns that on, so `/resume` can replay a finished run from the database once the stream has forgotten it. Verifying that against a Redis event stream with a 6-second TTL found one more defect, fixed here: the terminal event is persisted before the event stream stamps its index, so it replayed at its position (5) after events stamped 8 to 10, sending the client backwards. Now the run row is written again once the terminal event carries its stream index (`_arewrite_run_row`, a plain row upsert that does not re-add session metrics), and `stored_event_replay_dicts` never lets an unstamped stored event replay behind a stamped one. With that, the Redis check passes: keys expired, resume with `last_event_index`, ordered catch-up from the database ending with `RunCompleted` at its real index. ## 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) - [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 **Verified live** (claude-sonnet-4-6; openai-codex 0.161.0 with gpt-5.6-luna): - Tool errors: `cat` on a missing file and a stdio MCP server whose tool raises, streamed and non-streamed. Before: `tool_call_error=None` in all four cases. After: `True` in all four, result text unchanged. - Resume status: a Claude run submitted with `background=true, stream=false` against a real uvicorn server. Before: `"Run completed but no events stored."` with status `RUNNING`. After: `"Run is RUNNING and was submitted without streaming ... Poll the run endpoint for its result."`, and after completion `"Run COMPLETED with no stored events: ..."`. - Redis replay: a background streamed Claude run through AgentOS on a Redis event stream with a 6s TTL; after the keys expired, resume with the floor at `ToolCallStarted` replayed the later events and `RunCompleted` from the database in order with their real indices. - One turn per session: two concurrent turns on one session, the first running a 6-second shell command, on Codex and on Claude. Before (Codex): the second turn ended as an ERROR run with the app-server's active-writer message after 0.1s. After: the second turn raises `SessionBusyError` in under a millisecond, no row is written, the first turn completes, and a turn sent afterwards runs normally. **Unit tests:** failed MCP calls with the error on the item or only in the result text, on both paths (`test_codex.py`); the resume frame for a running and for a completed run without events (`test_db_replay_fallback.py`); a busy session refused before the adapter on both paths, a failed turn releasing the session, a pending row from another replica blocking until cancelled with no pending row written for the refused background submission, AgentOS 409 and the typed `RunError` event (`test_external_session_busy.py`); a stored terminal event carrying its stream index after a queued run, and an unstamped event after stamped ones keeping the order on replay (`test_external_store_events.py`, `test_db_replay_fallback.py`); the Codex active-writer rejection as `session_busy`, retryable, with the thread kept, and serialization by retry (`test_codex.py`). Agents suite plus replay and queue-worker tests: 408 passed after merging main; OS suite 3394 passed (the 42 errors are the telegram module's missing optional dependency in the local venv). **Soak items not in this PR:** tool history across retries, queue retries honoring permanent errors, retry limits docs and streaming retry semantics are on main via #10937. Claude authentication refresh across replicas is deferred. Release-candidate acceptance is next. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Yash Pratap Solanky <[email protected]> |
||
|
|
107f535248 |
feat: output_schema for external agents (Claude, Codex) (#10965)
## Summary `ClaudeAgent` had no `output_schema` at all, and `CodexAgent` took only a raw JSON Schema dict and returned the JSON as a string. Native agents take a Pydantic model or a dict, on the agent or per run, and return the parsed object. This brings the external agents to the same contract, with the shared part in `BaseExternalAgent`. Supersedes #8957 for Claude and Codex (see Additional Notes). ```python class Movie(BaseModel): title: str year: int agent = CodexAgent(name="Codex", output_schema=Movie) run = agent.run("Invent a movie set on Mars.") run.content # Movie(title=..., year=...) run.content_type # "Movie" agent.run("Suggest a title.", output_schema={"type": "object", ...}) # per run, content is a dict ``` **Base class** - `output_schema: Optional[Union[Type[BaseModel], Dict]]` on `BaseExternalAgent`, plus a per-run `output_schema` kwarg that wins over the agent's, as on `Agent`. - A completed run's content is parsed with the same helpers native agents use (`parse_response_model_str` / `parse_response_dict_str`), and `content_type` is set on `RunOutput` and on the `RunCompleted` event. An answer that cannot be parsed stays text and a warning is logged, as on `Agent`. - Adapters opt in with `_supports_output_schema`. LangGraph, DSPy and Antigravity now raise `ValueError` for `output_schema` instead of silently ignoring it. - Adapters can hand the base the answer to parse when it is not the run's text: `ExternalRunResult.structured_output` (non-streaming) or `ExternalRunStructuredOutputEvent` (streaming, consumed by the base and not forwarded, like the metrics event). Reset per retry attempt. - The AgentOS per-request `output_schema` form field already reached `arun` and was ignored; it now works for external agents. **Claude** - The schema is sent as `output_format` (`json_schema`). Claude Code delivers the object through a `StructuredOutput` tool call and then replies in prose, so the base parses `ResultMessage.structured_output`, not the reply. The streamed text and the stored assistant message are the prose; `RunCompleted` and the stored run carry the object. **Codex** - A Pydantic model is converted to a strict schema (`additionalProperties: false`, every field required) via `get_response_schema_for_provider(..., "openai")`; a dict is sent as given, as before. - When streaming, the text joins Codex's commentary and the final answer. The adapter tracks the final answer with the SDK's `final_response` rule (`final_answer` phase, else the last unphased message) and only that is parsed. - Removed an unreachable `raise RuntimeError` after the `CodexTurnError` raise in the stream path. **Breaking:** `CodexAgent` with a dict `output_schema` now returns a dict instead of a JSON string, matching `Agent`. Callers doing `json.loads(run.content)` need to drop it. The cookbook is updated. ## Type of change - [ ] Bug fix - [x] 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) - [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 - [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 **Relation to #8957:** that PR adds `output_schema` to the external base with native Claude support and a prompt-based fallback for LangGraph and DSPy. It predates the Codex adapter, takes Pydantic models only, and conflicts with `main` after the retries, continuation, metrics and stored-events changes to `_arun_stream`. This PR is a fresh implementation on the current base for the two harnesses with native structured output. The prompt-based fallback for LangGraph and DSPy is not included: those adapters reject `output_schema` here, and the fallback can follow on top of the same base hooks by setting `_supports_output_schema` and passing the schema into the prompt. Credit to @sannya-singal for the original. **Verified live** - Claude (claude-sonnet-4-6), `cookbook/frameworks/claude-agent-sdk/structured_output.py`: a nested Pydantic schema with Read and Glob tools, non-streaming and streaming, both returned a parsed `ProjectSummary`; a per-run dict schema returned a dict. - Claude through AgentOS: `POST /agents/claude/runs` with an `output_schema` form field returned `content_type` "Capital" and `{'city': 'Paris'}` (previously prose). - Codex (openai-codex 0.161.0, gpt-5.6-luna), `cookbook/frameworks/codex/codex_structured_output.py`: non-streaming and streaming returned a parsed `Movie` (Codex accepted the strict schema); a per-run dict returned a dict. `codex_basic.py` reran unchanged. **Unit tests:** Claude: the structured output is parsed instead of the prose reply on both paths, the `output_format` shape, per-run dict override, no `output_format` without a schema, an unparseable answer stays text. Codex: strict schema from a model, non-streaming final answer, streaming parses the final answer and not the commentary, dict schema returns a dict on both paths, failed turns keep the error text. Base: unsupported adapters reject `output_schema` at construction and per run. 11 of the new tests fail on `main`; the other two are regression guards. Agents suite passes (the two `test_sdk_conformance` cases need `claude_agent_sdk` installed and pass in the demo venv). **validate.sh:** ruff passes; mypy reports errors only in files this PR does not touch (google, mcp, genai), from the local `.venv`. `agno/agents` is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Kaustubh <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
df4a5861be |
feat: preserve Claude subagent tool lineage for tracing (#10959)
## Summary Claude SDK messages include `parent_tool_use_id`, but the adapter previously discarded it when creating Agno tool events. This prevented tracing integrations from distinguishing subagent tools from top-level tools. - Add optional `parent_tool_call_id` to `ToolExecution`, including serialization and deserialization. - Preserve Claude’s parent IDs in streaming tool events and non-streamed tool records. - Carry lineage from tool-start events into completion events when completion messages omit it. - Preserve the SDK’s tool-error flag in streaming completion events. - Add regression tests and a cookbook that verifies nested tool spans persisted in Agno’s tracing database. With the corresponding OpenInference update, traces can represent: ```text ClaudeAgent.run ├── Agent │ ├── Bash │ └── Read └── Bash ``` tracing update on openinference- https://github.com/Arize-ai/openinference/pull/4018 to test locally use `uv pip install -e ../openinference/python/instrumentation/openinference-instrumentation-agno/` Quick cookbook (not adding in the PR files since the openinference update needs to be released first, so the cookbook cant go on main yet.- ```python mport asyncio from pathlib import Path from tempfile import TemporaryDirectory from uuid import uuid4 from claude_agent_sdk import AgentDefinition from opentelemetry import trace from agno.agents.claude import ClaudeAgent from agno.db.sqlite import SqliteDb from agno.run.agent import RunOutput from agno.tracing import setup_tracing async def main() -> None: db = SqliteDb(db_file="tmp/claude-nested-traces.db") setup_tracing(db=db) run_id = str(uuid4()) with TemporaryDirectory(prefix="agno-trace-probe-") as directory: Path(directory, "probe.txt").write_text("nested_read_probe\n") agent = ClaudeAgent( name="Claude trace probe", model="claude-sonnet-4-6", db=db, cwd=directory, allowed_tools=["Agent", "Bash", "Read"], max_turns=8, max_budget_usd=1.0, options_kwargs={ "agents": { "trace-probe": AgentDefinition( description="Run the tracing verification tools.", prompt=( "Run Bash with `printf nested_shell_probe` once, then Read probe.txt " "in the working directory. Report both outputs. Do not modify files." ), tools=["Bash", "Read"], model="haiku", ) } }, ) output = None async for event in agent.arun( "Use the trace-probe subagent to perform its verification. After it finishes, " "run Bash yourself with `printf main_shell_probe` once. Then report the three outputs.", stream=True, yield_run_output=True, run_id=run_id, session_id=run_id, user_id="tracing-demo", ): if isinstance(event, RunOutput): output = event trace.get_tracer_provider().force_flush() assert output is not None and output.status.value.lower() == "completed", ( "Claude run did not complete" ) record = db.get_trace(run_id=run_id) assert record is not None, "No trace persisted; check the instrumenter installation" spans = db.get_spans(trace_id=record.trace_id) root = next(span for span in spans if span.parent_span_id is None) tool_spans = { span.attributes["tool_call.id"]: span for span in spans if "tool_call.id" in span.attributes } nested = [ span for span in tool_spans.values() if span.attributes.get("agno.tool.parent_call_id") ] assert nested, ( "No nested tools recorded; check both Agno and the instrumenter have lineage support" ) for span in nested: parent = tool_spans[span.attributes["agno.tool.parent_call_id"]] assert span.parent_span_id == parent.span_id assert any( span.name == "Bash" and span.parent_span_id == root.span_id for span in tool_spans.values() ) assert {"Bash", "Read"}.issubset({span.name for span in nested}) assert all(span.status_code == "OK" for span in spans) def show_tree(parent_id: str | None, depth: int = 0) -> None: for span in sorted(spans, key=lambda span: span.start_time): if span.parent_span_id == parent_id: print(" " * depth + span.name) show_tree(span.span_id, depth + 1) show_tree(None) print("Trace ID:", record.trace_id) print( "Verified nested tool spans and top-level sibling in tmp/claude-nested-traces.db" ) if __name__ == "__main__": asyncio.run(asyncio.wait_for(main(), timeout=120)) ``` |
||
|
|
24bc39f137 |
feat: store run events for external agents (#10957)
## Summary
External agents (`ClaudeAgent`, `CodexAgent` and every other
`BaseExternalAgent`) did not keep the events of a run. A finished run in
AgentOS showed its content and tools, but
`/agents/{id}/runs/{run_id}/resume` had nothing to replay once the live
event stream no longer held the run, and the run view had no event
timeline. Native agents keep events through `store_events` and
`events_to_skip`; this adds the same to the external base.
Supersedes #7841 (same idea, written against the base class before
retries, continuation and metrics were added, and conflicting with
`main` since).
Before:
<img width="1772" height="166" alt="image"
src="https://github.com/user-attachments/assets/a4ac1909-145d-4e60-9bf5-7f88375e89ec"
/>
After:
<img width="1984" height="538" alt="image"
src="https://github.com/user-attachments/assets/b85c2ad6-60f2-480a-b285-c1f88ef7b60d"
/>
**What changes**
- `BaseExternalAgent` takes `store_events` (default `False`) and
`events_to_skip` (default `[RunEvent.run_content]`), with the same names
and defaults as `Agent`. With `store_events=True`, a streamed run keeps
on `RunOutput.events` the start event, every event the adapter yielded
that is not skipped (tool calls, custom warning events), the `retry`
warning events emitted between attempts, and the terminal event
(`RunCompleted`, `RunError` or `RunCancelled`). Content deltas are
skipped by default because the run already holds the full content.
- The terminal event is built before the run is persisted, so the stored
run ends with it, as native agents do. Non-streamed runs store no
events, as for `Agent`.
- The started tool-call event is no longer rewritten when the tool
completes: the base keeps a copy of the started event's tool for its
merge, so a stored `ToolCallStarted` shows no result and
`ToolCallCompleted` does.
- AgentOS sets `store_events = True` on every external agent it serves,
next to the database injection, which is what it already does for native
agents.
- Stored events keep the `event_index` the event stream stamped on them
when the run went through the queue worker, so a client's
`last_event_index` applies on a replay from the database. Runs streamed
inline through `/runs` are unstamped and replay from position 0, the
same as native agents on that path.
**Cookbooks and docs**
- `cookbook/frameworks/claude-agent-sdk/claude_stored_events.py` and
`cookbook/frameworks/codex/codex_stored_events.py`: a direct streamed
run read back from SQLite with its stored events, then the agent served
through AgentOS in-process, a streamed run over HTTP, the run dropped
from the live event stream, and `/resume` replaying it from the
database.
- README sections on stored events for both harnesses; the
background-runs note that stored-event replay was a follow-up now points
at it.
## 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
- [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
**Relation to #7841:** that PR added the same two fields and collected
events in `_arun_stream`, but `_arun_stream` has since been rewritten
for retries, continuation and metrics, and the branch is 600+ commits
behind `main` with conflicts. This PR is a fresh implementation of the
same idea on the current base; credit to @anuragts for the original.
**Verified live** (claude-sonnet-4-6; openai-codex 0.161.0 with
gpt-5.6-luna): both cookbooks pass. Claude streamed 6 events and stored
4 (RunStarted, ToolCallStarted, ToolCallCompleted, RunCompleted); Codex
streamed 16 and stored 4. In both, AgentOS turned `store_events` on by
itself and `/resume` replayed the four stored events from the database
with status COMPLETED after the live buffer had forgotten the run.
**Unit tests** (`test_external_store_events.py`): off by default; stored
kinds and persistence round-trip without content deltas;
`events_to_skip` honoured; error runs end with `RunError`; a retried run
keeps both attempts' tool events with the `retry` warning between them;
a run cancelled during backoff ends with `RunCancelled`; non-streamed
runs store none; AgentOS sets the flag; `/resume` replays from the
database on the inline path; a queued run stores stamped indices and a
`last_event_index` floor applies on the database replay. Agents suite
plus the replay-fallback and queue-worker tests pass (396).
|
||
|
|
e39a56fb7e |
feat: compaction cookbooks for Claude and Codex, CodexAgent.compact (#10924)
## Summary Native context compaction for the external agent adapters, verified end to end and documented with runnable cookbooks. **Claude (`ClaudeAgent`)** needs no library change. `/compact` is an ordinary run input, and a `PreCompact` hook passed through `options_kwargs` sees every compaction. With transcript storage on, the compaction is mirrored into `agno_transcripts` like every other transcript line: a `compact_boundary` system entry followed by the `isCompactSummary` user entry. A later turn on any replica resumes from the database and continues from that summary. - `cookbook/frameworks/claude-agent-sdk/compaction.py`: seeds a fact, adds three turns including a long document, sends `/compact`, prints the two rows the compaction wrote, then resumes the session from a second agent instance with a different working directory. That instance answers from the summary. **Codex (`CodexAgent`)** has no slash command; compaction is an app-server request. This PR adds `CodexAgent.compact` / `acompact(session_id, user_id=None, timeout=120.0)`: - Resolves the Codex thread behind the Agno session (returns `False` when none exists yet, or when its rollout is gone: the stored id is then forgotten, as a run would). - Resumes the thread through the high-level `AsyncCodex.thread_resume` with the same kwargs a run uses, so the SDK applies its wire translation (`full-access` to `danger-full-access`, approval mode to approval policy, camelCase keys). Only `thread/compact/start` goes to the low-level app-server client behind it, which also owns the global notification queue. The first version resumed on the low-level client with the untranslated kwargs; the app-server rejected `sandbox="full-access"` and silently dropped instructions and approval mode (review finding from @ysolanky, replicated against 0.161.0). - Waits for the thread status to go active and then idle on the global notification queue. The `thread/compacted` notification is routed to a per-turn queue the caller never registered, so thread status is the reliable signal; the `thread/compacted` branch is kept as forward compatibility and commented as such. - Refuses with `RuntimeError` while a run on the session is pending or running, since compaction opens its own app-server on the thread and two processes would append to one rollout. Documented in the docstring and README. - Raises the builtin `TimeoutError` if the thread does not go idle within `timeout` (mapped from `asyncio.TimeoutError` for Python 3.10); the client is closed either way. - The sync wrapper uses `run_coroutine_sync`, now a shared helper in `agents/base.py` that `BaseExternalAgent.run` uses too. - `cookbook/frameworks/codex/codex_compaction.py`: same scenario, calling `acompact` and continuing from the summary. Both READMEs gain a compaction section and both TEST_LOGs record the live runs. ## 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 **Verification** - Both cookbooks run live: claude-sonnet-4-6 with SQLite transcript storage, and gpt-5.6-luna with openai-codex 0.161.0. In both, the seeded fact was recalled after compaction. - Claude: 35 transcript rows before `/compact`, 45 after; the hook fired with trigger `manual`. - Codex: `acompact` returned `False` before the first run and `True` after; the thread went idle again in about eight seconds and the rollout gained a `compacted` entry. - Unit tests: eight tests in `libs/agno/tests/unit/agents/test_codex.py` against fake high-level and low-level clients: no thread, resume and wait sequence, resume kwargs identical to a run's (`sandbox="full-access"`, instructions, approval mode, no start-only keys), missing rollout forgets the id, other resume failures propagate, in-flight run refused, the compacted notification, timeout closing the client. Agents suite passes. - Live: an agent with `sandbox="full-access"`, `instructions` and `approval_mode="deny_all"` compacted and recalled the fact; a nonexistent thread id returned False and was removed from the session. - The Claude cookbook reads transcripts through `db.get_transcript_entries` instead of raw SQL. **Notes** - A conversation of one turn returns "Not enough messages to compact"; the cookbooks add turns first. - The `/compact` run records an empty assistant message in the Agno run output. Harmless, and a candidate for a small follow-up. - Automatic compaction (triggered by context pressure) uses the same path on both sides; only the manual trigger is exercised here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> |
||
|
|
7479103782 |
fix: keep failed attempts' tools and announce retries to streaming consumers (#10937)
## Summary Follow-up to #10929 (run retries for external agents). This branch carries that PR's commits plus one more that fixes what the review and a live test turned up. The diff against `main` is the whole feature; the section below is what changed on top of #10929. **What #10929 adds, unchanged here:** `retries`, `delay_between_retries` and `exponential_backoff` on `BaseExternalAgent` with the same names and defaults as `Agent`. A failed attempt is retried with the same run id and resumes the SDK session or Codex thread the failed attempt started. Adapters classify errors so limits (`max_turns`, `max_budget_usd`, Codex budget and usage limits) and permanent failures (authentication, billing, bad requests, context window, policy) are not retried. Cancelled attempts are never retried, and cancelling during the backoff ends the run. ### Changes on top of #10929 **Tool calls from failed attempts are kept on both paths.** Live, a non-streamed retry's stored run had one Bash call while the same streamed retry had two, although the tool ran twice in both. The Claude adapter was already leaving a failed attempt's tools in `run_state["tools"]`, but nothing read them on retry, and Codex raised before converting a failed turn's items. Now: - the base collects a failed attempt's tools before retrying and merges them with the final attempt's, keeping call order and letting the final attempt's entry win on a repeated id; - when retries are exhausted, the error run keeps the tools from every attempt, and a run cancelled mid-attempt or during the retry backoff keeps the tools that had completed (review finding: both adapters persisted CANCELLED with zero tools on the non-streaming path); - the Codex non-streaming path no longer uses the SDK's `handle.run()`, which returns items only once the turn ends. It consumes the notification stream itself (`_acollect_turn`, same item and final-response rules as `run()`) and records each tool call in `run_state["tools"]` as it completes, so a turn that ends with a failed status or dies with a transport error mid-turn both leave the work that was done for the base to keep. **Streaming consumers are told about a retry.** A streaming client has already received the failed attempt's output when the retry starts, and the final `RunCompleted` only carries the retry's content. Before a new attempt starts, the stream now emits an `ExternalRunWarningEvent` with `type: "retry"` (attempt, attempts, delay, error, message), placed after the failed attempt's last event and before the first event of the new one. The same warning is stored in the run's metadata. Buffering each attempt until success was the alternative; it would defeat streaming. **Cookbooks show the retry instead of only setting the fields.** `claude_retries.py` and `codex_retries.py` inject a transient failure at the SDK boundary, clearly labelled as a simulation, and run four cases each: one failure then success (the second attempt resumes the same session or thread, printed), exhaustion after the configured retries with 1s then 2s backoff, a `max_turns` limit that is not retried (Claude) or the same case streamed (Codex), and the default of no retries. Each case announces the warnings and the ERROR traceback it is about to produce. **The job queue honors the agent's error classification.** The queue worker re-drove any run that ended in ERROR until `max_attempts`, including errors the agent had refused to retry (Codex `unauthorized`, Claude authentication, billing and `max_turns`), because external agents return an ERROR run rather than raising and the queue's `_is_permanent_failure` only sees exceptions. An error run now carries `metadata["retryable"]` (from `_is_retryable_error`) and `metadata["error_type"]`; the worker fails the ticket at once when `retryable` is False, on both the streaming and non-streaming job paths, and the streaming path publishes the terminal sentinel instead of leaving the stream open for a retry that will not come. The classification survives a worker restart: a worker that crashed after writing the ERROR row but before settling the ticket leaves the job to be reclaimed, and the reclaiming worker reads the row's classification and fails the ticket without re-executing (`_ahonor_non_retryable_row`, checked only on reclaims, so fresh claims pay no extra read). If that row read fails, the worker fails closed: it does not execute and leaves the claim unsettled so the lease goes stale and a later reclaim reads the row again (review finding: the first version treated a read failure as retryable and executed). **Attempt metadata.** A run that was retried stores `metadata["attempts"]` (the number of attempts made) next to the retry warnings, so clients and operators can tell a retried run from a clean one. **Docs.** READMEs explain the retry settings and the cookbook, plus what to know before turning retries on: the combined budget with the queue (`max_attempts × (retries + 1)` SDK invocations, unless the error is permanent) on top of the SDKs' own transport retries; what streaming clients should do with output received before the `retry` warning event (reset the buffered text, keep the tool events, read `attempt`, `attempts` and `delay` from the event); and that neither a resumed session nor a queue re-drive after a crash makes tools exactly-once, so side effects outside the workspace must be idempotent before enabling either. The Codex TEST_LOG entry that said `NOT RUN` is now a live PASS. ## Type of change - [x] 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 - [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 **Relation to #10929:** same feature, same three commits, plus `db97e3a814`. This PR supersedes it; the review comments there (streaming leak, lost non-streamed tools, `NOT RUN` status, cookbook not showing a retry) are addressed here. The positional-constructor note is left to #10926, which makes the adapter constructors keyword-only. **Live verification** (claude-sonnet-4-6, openai-codex 0.161.0 with gpt-5.6-luna), with a failure injected after the SDK session or thread had started: - Claude, non-streamed and streamed: two attempts, same SDK session id on both, completed; stored run keeps the Bash call from each attempt (two calls on both paths, previously one on the non-streamed path). - Claude, `max_turns=1` with `retries=2`: one attempt, `error_max_turns` logged as not retryable. - Codex, non-streamed and streamed: two attempts, same thread id on both, completed. - Both: three attempts with 1s then 2s backoff when failures outnumber retries, run ends in ERROR with the last error; one attempt with the default `retries=0`. **Unit tests:** `test_external_retries.py` adds carried tools on success and on exhaustion, the retry warning event's position and persistence, retryability recorded on error runs, and a permanent versus transient failure run through a real `QueueWorker` (streaming and non-streaming); `test_claude.py` adds a non-streamed retry keeping a tool call and queued runs for `max_turns`, authentication, billing and an overloaded error; `test_codex.py` adds a failed turn and a mid-turn exception each keeping the completed tool call, exhausted retries keeping tools from every attempt, and queued runs for `unauthorized` and an overloaded error; the non-streaming fakes now drive the notification stream. A small `_queue_harness.py` runs one job through the worker against the in-memory store. Also: a queued run whose agent retried once keeps the failed attempt's tool exactly once in tools and messages, a retried run records its attempt count, a worker-restart test where worker one writes the non-retryable ERROR row and crashes before settling, and worker two reclaims the ticket and fails it without running the agent again; the same with one injected row-read failure on the reclaim, where execution is deferred and the next reclaim fails the ticket; and a cancel during the retry backoff keeping the completed tool, streaming and non-streaming. ruff and mypy clean on `agno/agents` and `os/job_queue.py`. **Rebase note:** #10925 introduces the same `_acollect_turn` collector for metrics (it also keeps the first and last token usage report). The two versions are written to the same shape so whichever merges second only has to union the usage fields with the tool capture. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: ysolanky <[email protected]> |
||
|
|
1069000f7c |
feat: capture ext agent harness metrics (#10925)
## Summary Before: <img width="537" height="251" alt="image" src="https://github.com/user-attachments/assets/d72630df-d7a8-4b64-b49b-c7adee008f5f" /> After: <img width="523" height="532" alt="image" src="https://github.com/user-attachments/assets/dcb94cbe-6acc-47be-b215-f156099d90e9" /> External agents (`ClaudeAgent`, `CodexAgent`) now report run and session metrics the same way native agents do, so their runs and sessions show tokens and cost in AgentOS: on the run output, on the streamed `RunCompleted` event, on the session, in the sessions list token column, and in the daily `/metrics` aggregation. Before this, `RunOutput.metrics` was always empty for external agents and their sessions were invisible to the metrics page. **Base (`agents/base.py`)** - `ExternalRunResult.metrics` for non-streaming adapters, and an `ExternalRunMetricsEvent` that streaming adapters yield; the base consumes it and does not forward it to clients. - A per-run timer in the base sets `duration` and, when streaming, `time_to_first_token`, so every external agent measures these the same way. Timing is wall clock from the adapter's point of view (it includes process startup), matching how native agents measure it. - `_build_metrics(totals, models, additional_metrics)` assembles `RunMetrics` from a framework's turn totals and its per-model entries. Adapters only parse their SDK's usage object. Totals are taken from the framework's own figures rather than summed from the model entries, because Claude Code counts its helper model separately from the main conversation. - `_apersist_run_in_session` accumulates completed runs into `session_data["session_metrics"]` (same `SessionMetrics.accumulate_from_run` path as native agents), before the worker-owned write so background runs are covered. The persistence fallback re-reads the session, so a failed write cannot double count. **Claude (`agents/claude/agent.py`)** - Maps the SDK `ResultMessage`: `usage` for tokens, `total_cost_usd` for cost, `model_usage` for one `ModelMetrics` per model (Claude Code's Haiku helper model shows up as its own entry with its own cost), `num_turns` and `duration_api_ms` into `additional_metrics`. - Like the native Anthropic model, `input_tokens` excludes the cached prefix; cached tokens are in `cache_read_tokens` and `cache_write_tokens`. Cache reads count every API call inside the turn, so a turn with one tool call reports roughly two reads of the ~15k harness prefix. **Codex (`agents/codex/agent.py`)** - Codex reports usage after every model request as `thread/tokenUsage/updated`, with `last` (that request) and `total` (the thread so far). A turn with tool calls makes several requests, so the turn's usage is the growth of the thread total over the turn: the final `total` minus the `total` before the turn (the first report's `total` minus its own `last`). Reading only `last`, or the SDK's `TurnResult.usage` which is just the latest report, counts a single request (review finding from @ysolanky, replicated: a two-tool turn recorded 14972 of 44739). - The non-streaming path now consumes the notification stream itself (`_acollect_turn`, same rules as the SDK's `run()` for items and the final response) so it sees every usage report; the streaming path keeps the first and last report. A repeated report does not double count. Like the OpenAI API, `input_tokens` includes the cached prefix. Codex reports no cost, so cost stays unset. **Cookbooks** - `cookbook/frameworks/claude-agent-sdk/metrics.py` and `cookbook/frameworks/codex/codex_metrics.py`: run metrics for a plain and a streamed turn, then session totals. - `cookbook/frameworks/claude-agent-sdk/metrics_agentos.py` and `cookbook/frameworks/codex/codex_metrics_agentos.py`: serve the agent through AgentOS; `--verify` drives two runs over HTTP and prints the run metrics, the session totals, the sessions list column and the `/metrics` aggregation. - READMEs explain the conventions (cache token handling, why Claude cache reads look large) and TEST_LOGs record the live runs. Builds on the metrics mapping sketched in #9027 for Claude; that PR's `adapter_state` side-channel is replaced by the result field and event above, and nothing from it is rebased. ## 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 - [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 **Verified against the SDKs on the same runs.** The raw SDK result was captured inside the adapter's own runs and compared field by field with what Agno recorded. - Claude: input, output, total, cache read, cache write, cost, turn count, API duration and every per-model entry identical on a plain turn and on a streamed tool turn. Session cost equals the sum of the SDK cost figures to seven decimals. - Codex: input, cached input, output, reasoning and total identical to the SDK's usage reports on single-request turns, and on a two-tool turn (three requests) the recorded usage equals the thread total growth on both paths: streamed 44725 of 44725, non-streamed 43289 of 43289. Session total equals the SDK's own running thread total, so nothing is double counted. - The only intentional difference is `duration`: the SDKs report their own turn time, Agno reports wall clock including process startup. The SDK figure is still available for Claude as `additional_metrics["api_duration"]`. **Verified in AgentOS** through the test client and the UI: run output has `metrics`, the streamed `RunCompleted` has `metrics`, `GET /sessions/{id}` returns the totals with per-model details, `GET /sessions` shows the token column, and `GET /metrics` counts the runs and tokens for the day. **Unit tests**: eleven new tests across `test_claude.py` and `test_codex.py`, including three-request turns on both paths where `last` differs from the per-turn delta, a resumed thread with earlier usage, and a repeated usage report; the non-streaming fakes now drive the notification stream against the existing fake SDKs: run metrics from the result message or usage, streamed metrics on `RunCompleted` without a forwarded event, session accumulation across runs with per-model details, and duration reported for runs without usage. Agents and run suites pass; ruff and mypy clean. **Why the Codex numbers look large**: a one-line question costs ~15k input tokens because Codex's prefix is its base instructions (~3.5k), a developer message with skills, permissions and plugin instructions (~3.1k), environment context, and the schemas for built-in tools, configured MCP servers and plugins. The metrics report it as billed. Claude's `total_tokens` looks small by comparison because cache tokens are kept in their own fields, following the native Anthropic model. --------- Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
3be277189d |
feat: continue ClaudeAgent runs from a step (#10917)
## Summary Adds step replay to `ClaudeAgent` using the same API as native `Agent.continue_run`. Stacked on #10905 (`feat/harness-agents-phase2-astra`); it needs #10904's database-backed Claude SDK transcript store. - **`ClaudeAgent.continue_run` / `acontinue_run`**: accept `continue_from` (`"end"`, `"last_user"` or a message index), `fork` and `input`, and support `stream=True` and `background=True`. Continuing forks the stored SDK transcript at the chosen message (`fork_session_via_store(..., up_to_message_id=...)`) and resumes the fork, so the model only remembers what happened up to that point. - `fork=True` writes a sibling run with `forked_from_run_id` / `forked_from_message_index`; `fork=False` replaces the source run in place and keeps its fork lineage. - `continue_from="last_user"` without `input` replays the whole turn: it forks just before the prompt (or starts a fresh SDK session for the session's first turn) and re-sends the original input. - A mid-turn continue without `input` sends "Continue from where you left off." - Later turns in the session continue from the replayed branch, because the session's SDK pointer moves to the fork. - **Transcript positions on runs**: the adapter enables the SDK's `replay-user-messages` so the prompt arrives with its transcript uuid. It records the SDK session id and uuid for each top-level prompt, tool call, tool result and final reply in `Message.provider_data["claude_sdk"]`. Tool results are marked as checkpoints, so `GET /agents/{id}/runs/{run_id}/checkpoints` lists each step. - **AgentOS**: no router changes. `/continue` and `/checkpoints` already dispatch on `acontinue_run` / `aget_run_output`, so `ClaudeAgent` moves from 501 to supported. Unsupported options (`regenerate`, `replace_original`, `additional_instructions`, HITL `tools`) return 400. - **`BaseExternalAgent`**: generic plumbing only. `ExternalContinuation` carries the kept transcript into `_arun_non_stream` / `_arun_stream`, a per-run `run_state` dict reaches the adapter, and an `_annotate_run_output` hook (no-op by default) lets an adapter attach per-message data. Other adapters are unaffected: none forwards run kwargs to its framework. - **Resume fallback unchanged for normal runs**: the echoed prompt is not counted as a received message, so #10903's "stored session cannot be resumed, start fresh with history" fallback still works. Continuations never fall back; a failed fork resume errors instead of silently replaying the full history. ## 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 Base is `feat/harness-agents-phase2-astra` (#10905), not `main`. Merge after #10903, #10904 and #10905. Limits: - Only runs recorded with transcript storage (PostgresDb, AsyncPostgresDb, SqliteDb, AsyncSqliteDb) can be continued; older runs and other databases raise a clear error. - Files and other side effects after the checkpoint are not rewound. The SDK's file checkpointing does not carry over to forked sessions. - Steps inside subagents (`parent_tool_use_id`) are not checkpoints; they live in subagent transcripts. - `regenerate` / `replace_original` and Codex are follow-ups. Verification: - Confirmed against the real SDK (0.2.95) that streamed message uuids equal stored transcript uuids, and that forks cut at a tool result or before the prompt only remember up to that point. - Live end-to-end on SQLite: fork from a tool result, replay of a first and a later turn, in-place streamed continue, follow-up turn on the branch, and AgentOS `/checkpoints` + `/continue` (non-stream and SSE). `cookbook/frameworks/claude-agent-sdk/continue_from.py` passes with `claude-sonnet-4-6`. - New unit tests against the fake SDK: transcript positions and checkpoints (stream and non-stream), fork from a tool result, in-place continue lineage (sync API), turn replay with and without a parent entry, no fallback on a failed continuation, rejected options, and prompt echo not blocking the resume fallback. Each guard was mutation-checked: breaking lineage, allowing fallback, or counting the echo as received makes a test fail. - `validate.sh`: no ruff or mypy findings in touched files; the remaining mypy errors are pre-existing model/MCP SDK typing issues from local dependency versions. Postgres integration tests were not run locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Ashpreet <[email protected]> Co-authored-by: Ashpreet <[email protected]> Co-authored-by: kausmeows <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
c15c55ea7d |
chore: align dev and demo setup environments (#10920)
## Summary Local development and cookbook work need the same dependencies as demos. Both Unix setup scripts now install local editable `agnoctl[dev]` and `agno[demo]`, while keeping `.venv` for daily development and `.venvs/demo` for dedicated demos. - Remove `scripts/cookbook_setup.sh`; use `scripts/dev_setup.sh` for cookbook development. - Enable strict shell error handling, explicitly target the environment's Python, and make clipboard failures non-fatal. - Keep short activation commands relative to the repository root and update contributor and agent instructions. ## Type of change - [x] 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) - [ ] 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 - [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 ### Validation - `bash -n scripts/dev_setup.sh scripts/demo_setup.sh` and `git diff --check`: passed. - Isolated shell smoke checks for both retained setup scripts: passed for successful setup, active-environment rejection, missing `uv`, and installation failure. Successful runs also exercised paths with spaces, failing clipboard commands, and preservation of the other environment. - `uv pip install --dry-run --python .venv/bin/python -e 'libs/agnoctl[dev]' -e 'libs/agno[demo]'`: resolved 235 packages successfully. This was a dependency-resolution check, not a fresh install or a live demo run. - `./scripts/format.sh`: passed. Its unrelated formatting edits were reverted to keep this PR focused. - `./scripts/validate.sh`: failed. The latest run reports 32 mypy errors in 10 unchanged framework files in the current local environment, including SQL adapters, public limits, Google model compatibility, Anthropic, and AWS. The earlier run reported six errors. Other checks passed during the latest run after formatting. Draft pending the repository validation gate. No existing development or demo environment was rebuilt. Windows setup scripts are outside this change. Removing the cookbook script requires callers to switch to `./scripts/dev_setup.sh`; any existing `agnoenv` directory is left untouched. Dev setup now installs the larger demo bundle, including for CI jobs that call it. Related PR #8020 only quotes variables in an older version of `dev_setup.sh`. The current base already quotes those installation and removal paths; this PR addresses dependency parity, separate environment roles, script removal, and setup error handling. |
||
|
|
ff9b86d5be |
feat: add background lifecycle and cancellation to external agents (#10905)
## Summary External agents now support durable-queue run IDs, detached background execution, resumable indexed SSE and cancellation. Polling sees PENDING, RUNNING and the terminal run. Claude uses a per-run SDK client; Codex retains turn handles so the owning process can interrupt either SDK through direct calls or shared cancellation intent. Native agents and external agents share the background task/slot/SSE infrastructure. Queue preparation, error/shutdown persistence, retry fencing and scoped A2A polling/cancellation dispatch to external-agent helpers. A cancelled row is preserved if its durable ticket remains queued after a crash, including on adapters without atomic run primitives. ## Type of change - [x] Bug fix - [x] 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) - [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 Depends on #10904 (transcripts and tool persistence), which depends on #10903. Merge in that order before this PR. This PR targets main and currently includes both dependencies. Review the phase 2 changes with the comparison `feat/harness-agents-phase1-astra..feat/harness-agents-phase2-astra`. Refresh the branch onto main after the dependencies land so the final diff contains only lifecycle changes. Validation: 736 unit/regression tests, including the native background tests unchanged; 5 enabled integration cases with no skips. Real Claude and Codex turns were cancelled after content arrived and stored CANCELLED. SQLite and AsyncSQLite exercise status fallbacks, retries, cancellation, SSE replay and user scoping. Guard mutation results are retained locally. Required formatting and validation passed. Codex's cookbook explicitly sets `approval_mode="deny_all"`. Multi-replica cancellation and replay need shared cancellation/event-stream backends. Redis multi-replica deployment was not exercised. Database-stored events after the event-stream TTL, team background factoring, step replay/HITL and workspace persistence are follow-ups. SQLAlchemy 2.0.52 matches the demo environment and passes typing; the unchanged base has six mypy errors with the freshly resolved 2.1.4. ### Review fixes Worker-owned terminal persistence failures now propagate so durable tickets retry instead of completing over RUNNING rows. PostgreSQL writes SDK session metadata and the fenced run in one transaction; stale attempts change neither, and session-write failures roll back the run update. Missing prepared worker rows fail closed. Async lookups on sync in-memory SQLite retain the same connection thread as persistence. Includes the phase 1 warning and CI dependency fixes. Fresh validation: 751 unit/regression tests; 12 external-agent PostgreSQL integration cases; 13 existing native PostgreSQL fencing/status tests (temporary copies pointed at the disposable database port); 13 lifecycle/storage mutations killed. Required format and validation pass. One pre-existing native AsyncMock warning remains. The SDK mirror-failure reproduction passed; live provider cancellation/resume was not rerun for these fixes. --------- Co-authored-by: ysolanky <[email protected]> Co-authored-by: kausmeows <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> |
||
|
|
4dd9695c2d |
[feat] Add AnakinTools: scrape, crawl, map and search via Anakin.io (#10650)
## Summary
- Adds `AnakinTools`, a `Toolkit` wrapping Anakin's (anakin.io) URL
Scraper, Crawl, Map and Search API as four flag-gated methods
(`scrape_url`, `crawl_website`, `map_website`, `search_web`), following
the `FirecrawlTools`/`TavilyTools` pattern (`enable_*` flags + `all`
override).
- Scrape, crawl and map are asynchronous jobs on the Anakin API: the
tool submits the job (`POST /url-scraper`, `/crawl`, `/map`), then polls
`GET …/{id}` until the job is `completed` or `failed`, so the model
receives the page content or links rather than a job id. `poll_interval`
(default 2 s) and `max_wait_time` (default 120 s) bound the wait; a
failed or timed-out job is returned as an error string. Search is a
single synchronous `POST /search`.
- Uses `httpx` directly (already a core Agno dependency) instead of a
vendor SDK, matching other direct-HTTP toolkits, since the published
Anakin Python package is still alpha.
- Registers an `anakin` extra (empty, `httpx` is core) in
`pyproject.toml` and adds a usage example at
`cookbook/91_tools/anakin_tools.py`.
- Adds 27 unit tests in `libs/agno/tests/unit/tools/test_anakin.py`,
mocking both the job submission and the result polls (completed, failed,
timeout, missing job id, HTTP and transport errors).
closes #10651
## 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
This PR was built with Claude Code, directed and reviewed by me.
Endpoints and auth follow the published Anakin API reference:
`Authorization: Bearer <key>` is accepted by the hosted API, scrape uses
the asynchronous `POST /v1/url-scraper` + `GET /v1/url-scraper/{id}`
pair, and crawl and map use the equivalent `/v1/crawl` and `/v1/map`
pairs. All four tools were verified live against `api.anakin.io` during
review (scrape, crawl and map return completed content; search returns
results).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: Kaustubh <[email protected]>
Co-authored-by: Himanshu singh <[email protected]>
|
||
|
|
178888676e |
feat: persist external agent transcripts and tool results (#10904)
## Summary Claude sessions can now resume on another replica using transcripts persisted through Agno's database. A project-bound SDK session store supports PostgresDb, AsyncPostgresDb, SqliteDb and AsyncSqliteDb, preserves append order across processes and ignores repeated entry UUIDs. Unsupported databases retain local-file resume. Claude and Codex non-streaming runs retain tool calls and results in run history. The two-process cookbook proves transcript recovery with an empty config directory and no Agno run history. ## Type of change - [x] Bug fix - [x] 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) - [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 Depends on #10903 landing first; its session-id persistence is reused. This PR targets main and currently includes that dependency. Review the phase 1 changes with the comparison `8138922536..feat/harness-agents-phase1-astra`. Merge #10903 before this PR and refresh the branch after it lands. Validation: SDK store conformance with bound stores per project; explicit key-rewrite and retry tests; sync/async SQLite and PostgreSQL coverage; real two-process Claude resume. The final combined verification passed 736 unit/regression tests and all 5 enabled integration cases. Guard mutations failed named tests and were restored from copies. Development uses Python 3.12, claude-agent-sdk 0.2.148, openai-codex 0.162.0 and SQLAlchemy 2.0.52. SQLAlchemy 2.1.4 exposes six pre-existing mypy errors on the base. Both required scripts pass with 2.0.52, matching the demo environment. `project_key` defaults to the agent id; set a stable tenant-specific key when sharing a database. The adapter does not set CLAUDE_CONFIG_DIR. Empty config directories require environment-based authentication, documented in the cookbook. Workspace persistence and the optional SDK session-summary API are out of scope. ### Review fixes Exhausted transcript-mirror retries now surface as `transcript_persistence_failed` warnings in `RunOutput.metadata["warnings"]`, persisted history, and streaming `CustomEvent.warning`. Completed content is retained; mirror failures do not reexecute model work or tool side effects. The test extra installs `claude-agent-sdk>=0.2.148` so SDK conformance runs in CI. Fresh validation: 78 phase 1 agent tests, SDK parser/batcher fault injection, a killed mirror-handler mutation, formatting and type validation. The combined phase 2 regression run also passes. --------- Co-authored-by: ysolanky <[email protected]> Co-authored-by: Yash Pratap Solanky <[email protected]> Co-authored-by: kausmeows <[email protected]> |
||
|
|
30dd4f68c1 |
fix: include tool calls in Codex fallback history prompt (#10909)
## Summary Follow-up to #10901. When a Codex thread can't be resumed (`ephemeral=True`, rollout files removed, a new app-server that doesn't know the thread), `CodexAgent._build_prompt` prepends the persisted chat history to the prompt for the fresh thread. It only kept messages with `role in ("user", "assistant")` and non-empty `content`, which dropped: - assistant messages carrying `tool_calls` (their `content` is `""`) - `role: "tool"` result messages So the fresh thread lost every shell command, `apply_patch`, MCP and web search call from earlier turns. For Codex that's usually most of what happened, since the final assistant message is often just "Done." This also went against `BaseExternalAgent._get_history_from_session`, which includes tool messages so adapters have full context, and against the DSPy and LangGraph adapters, which both replay them. Changes in `libs/agno/agno/agents/codex/agent.py`: - Tool calls are replayed as `assistant called <name>(<arguments>)`. - Tool results are replayed as `tool result: ...`, each truncated to `_HISTORY_TOOL_RESULT_MAX_CHARS` (2000) with a `[truncated]` marker, so large shell output can't blow up the prompt. - Any assistant text that comes with tool calls is kept. - Resumed threads are unchanged: the prompt is still sent as-is. ## 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 - New unit test `test_history_prompt_includes_tool_calls_and_truncated_results` in `libs/agno/tests/unit/agents/test_codex.py`. It fails on `main` and passes with this change. All 17 Codex tests pass. - Ran `ruff format` and `ruff check` (pinned 0.15.20) on `libs/agno`: clean. `mypy` reports no errors in `agno/agents/codex`. The only errors are 4 that already exist in unrelated files (`regex`/`dns` stubs, `team/_run.py`). - No cookbook change: the behavior only shows up on the non-resumable fallback path, and the existing `codex_session.py` covers that flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BLQYkEigkXEFZrBKMiduYw --- _Generated by [Claude Code](https://claude.ai/code/session_01BLQYkEigkXEFZrBKMiduYw)_ ## Review follow-up (2026-10-09) - Merged `main`. `_build_prompt` moved from `CodexAgent` to `BaseExternalAgent` in #10903, so the tool-call replay now lives in `libs/agno/agno/agents/base.py` and applies to `ClaudeAgent`'s fallback too, which had the same gap. `codex/agent.py` is unchanged by this PR. - Truncated tool results keep their last 400 characters, so a shell exit status at the end of long output survives (addresses the Codex review comment). - The whole replay is bounded at 24000 characters, dropping the oldest entries first and noting `[earlier history omitted]`, so a long session cannot grow the fallback prompt without limit. - Verified live against Codex and Claude: after a tool call whose output held a fact the assistant text did not repeat, a forced fallback to a fresh thread or session recalled the fact with this change and answered NONE on `main`. - Unit tests: the original test now imports the limits from `agno.agents.base`; a second test covers the budget. 61 agent tests pass. Co-authored-by: Claude <[email protected]> Co-authored-by: kausmeows <[email protected]> |
||
|
|
b45c8410d5 |
fix: make partial-tool collision test independent of FastMCP's naming (#10914)
## Summary FastMCP 4.1.0 broke `test_exposed_id_colliding_with_fastmcp_derived_name_raises`, CI run: https://github.com/agno-agi/agno/actions/runs/37889156624/job/113685902995?pr=10903 that failed. FastMCP 4.1.0 changed the name a `functools.partial` tool is registered under: | fastmcp | `Tool.from_function(partial(base_tool, ...)).name` | |---------|----------------------------------------------------| | 4.0.11 | `"partial"` | | 4.1.0 | `"base_tool"` | The test hard-coded the agent id `"partial"` to force a collision. On 4.1.0 the tool registers as `base_tool`, so nothing collides and the test fails with `DID NOT RAISE ValueError`. ## 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.) |
||
|
|
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]> |