DAO Proposals & Community
View active proposals, submit new ideas, and connect with the SWARMS community.
## What's wrong `Agent.run(task, n=k)` is supposed to give `k` samples of the same request. Instead, each sample is sent every earlier sample's task and answer. In `Agent._run` (`swarms/structs/agent.py:1094` on master), the request start is ```python request_start = 0 if messages is None else history_start ``` All the samples write into one `short_memory`. With no caller `messages`, every sample builds its request from row 0. So sample 2 gets sample 1's turns, sample 3 gets samples 1 and 2, and so on. The samples are not independent, and each request is bigger than the one before. The `stored_messages` handoff that `run()` sets up for `n > 1` only takes effect when the caller passes `messages`. Repro on master. The model is stubbed by patching `Agent.call_llm` and recording `kwargs["messages"]`: ```python agent = Agent(agent_name="Sampler", model_name="gpt-4o-mini", max_loops=1, print_on=False) agent.run("Pick a random number", n=3) ``` ``` request 1: [('user', 'Pick a random number')] request 2: [('user', 'Pick a random number'), ('assistant', 'answer 1'), ('user', 'Pick a random number')] request 3: [('user', 'Pick a random number'), ('assistant', 'answer 1'), ('user', 'Pick a random number'), ('assistant', 'answer 2'), ('user', 'Pick a random number')] ``` ## What this changes `Agent._run`, inside the existing `stored_messages` handoff: - The first sample stores the rows it sends ahead of its task, from `request_start` up to the task row, as `stored_messages[0]`. - Later samples send that stored list as the prefix, followed by only their own rows from `history_start`. Every sample's request is therefore the first sample's request. - `stored_messages` now holds one list of rows instead of a flat list of rows. That way an empty context, such as a fresh agent with `system_prompt=None`, still marks the later samples. - If the context compressor fires during the first sample, the stored context is replaced with the compacted history. Without that, later samples would resend the uncompressed rows that triggered compression. The path with caller `messages` behaves as before: the messages are stored once, and every sample starts at them. The two existing tests for that path pass unchanged. Deliberately left alone: - Each sample's return value is still `history_output_formatter` over the whole `short_memory`, the same as calling `run()` `n` times on one agent. With the default `output_type`, later samples' outputs therefore still include earlier samples' text. Only what the model is sent changes here. - If compression fires on loop 2 or later of the first sample (`max_loops > 1`), the refreshed context is a summary that also covers that sample's earlier answers. Later samples then see a condensed version of them. Compression on loop 1 keeps the samples fully independent. - `max_loops="auto"` ignores `n`, as before. No public parameter, default or return type changes, so there is no docs update. ## Verification The same repro on this branch: ``` request 1: [('user', 'Pick a random number')] request 2: [('user', 'Pick a random number')] request 3: [('user', 'Pick a random number')] ``` Compression during the first sample, with a stub compressor that compacts to `SUMMARY`. The second sample sends the summary plus its own task, not the old rows: ``` [('user', 'SUMMARY')] [('user', 'SUMMARY'), ('user', 'task')] ``` I added one test, `TestIntegerLoopRequest::test_each_sample_starts_at_the_history_before_the_run`, in `tests/structs/test_agent.py`. It runs one earlier task, then `run("task", n=2)`, and asserts that both samples send `[earlier task, earlier answer, task]`. - With master's `agent.py`: `1 failed, 9 passed` (`-k "TestIntegerLoopRequest or TestRunSamples"`). The failure is the new test. - On this branch: `10 passed`. - The full `tests/structs/test_agent.py` has 29 failures on master and the same 29 on this branch. They need a real model or key. The only difference between the two runs is the new test. `black --line-length 70 --check .` (24.2.0) and `ruff check` are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What `AgentsConfig` in `swarms/structs/auto_swarm_builder.py:197` has zero references in the package, the tests, the examples or the docs. It is a `BaseModel` with one field, `agents: List[AgentSpec]`, which is the same single field as the `Agents` model defined directly above it at line 190. One of the two is a leftover from a rename, and `Agents` is the one the builder uses. ## Change Delete the `AgentsConfig` class. Nothing imports it, so no deprecation is needed and no docs page changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `SwarmRouter.fetch_message_history_as_string` (`swarms/structs/swarm_router.py:630`) has no caller in the package, the tests or the examples. It reads `self.swarm.conversation` and returns everything except the first message, which `Conversation.return_all_except_first_string` already does on the swarm's own conversation. ## Change Delete the method. Remove its entry from `api/swarm-router.mdx` in the docs repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `Agent.check_available_tokens` (`swarms/structs/agent.py:2167`) has no caller in the package, the tests or the examples. The context budget it reports on is handled by `ContextCompressor`, which does its own accounting, and `agent.input_tokens` is the documented way to estimate the next request. ## Change Delete the method. Remove its entry from `api/agent.mdx` in the docs repo, and point readers at `input_tokens` on the token-usage page instead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `Agent.to_toml` (`swarms/structs/agent.py:2254`) has no caller in the package, the tests or the examples. It is one of the serialisation helpers that predate `WorkspaceManager` and the `save` / `load` pair, which are what the package actually uses. ## Change Delete the method. Remove its entry from `api/agent.mdx` in the docs repo. It is public, so a one-release deprecation warning is the conservative path; given it has no test and no example, deleting outright is defensible. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What Four definitions in `swarms/structs/dynamic_skills_loader.py` have no reference anywhere: not in the package, the tests, the examples, and for three of them not in the docs either. | Definition | Line | |---|---| | `DynamicSkillsLoader.get_skill_names` | 274 | | `DynamicSkillsLoader.load_full_skill_content` | 287 | | `DynamicSkillsLoader.get_similarity_scores` | 304 | | `create_dynamic_skills_loader` | 328 | The class itself stays: `swarms/agents/skills_manager.py` builds a `DynamicSkillsLoader` at line 136 and uses its other methods. ## Change Delete the four definitions. Remove the `get_similarity_scores` mention from `agents/agent-skills.mdx` in the docs repo; the other three are undocumented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `MixtureOfAgents` documents its `id` parameter twice (`swarms/structs/mixture_of_agents.py:46` and `:90`) as "Optional identifier accepted for API compatibility". The attribute `self.id` is referenced 1 time(s) in the module, and other structures use `id` for telemetry identity and workspace paths. ## Change If `id` is used, document it the way the other structures do. If it is not, remove it with a one-release deprecation warning. Either way the phrase "accepted for API compatibility" should go, since it tells the reader nothing about what the value does. Docs: the parameter entry on `api/mixture-of-agents.mdx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `SelfMoASeq` accepts `max_retries`, `retry_delay`, `retry_backoff_multiplier` and `retry_max_delay` (`swarms/structs/self_moa_seq.py:39` to `:42`), stores them (`:59` to `:62`), validates them in `setup` (`:140` onward), and contains no retry loop. Exceptions from `run`, sample generation and window aggregation propagate at once. ## Effect Configuration that reads as reliability and provides none; a user who sets `max_retries=5` gets no retries. ## Change One of: - Implement the loop around the model calls with the documented exponential backoff, capped at `retry_max_delay`. - Remove the four parameters and their validation, with a one-release deprecation warning. Docs: `api/self-moa-seq.mdx` already notes the values are not used; update it either way. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `GroupChat(idle_timeout=...)` is accepted at `swarms/structs/groupchat.py:203`, documented at `:224` as "Deprecated/unused — the chat now ends on a bidding lull rather than a wall-clock timeout", stored at `:245`, and never read. ## Change Remove the parameter and the attribute after one release of a deprecation warning. Update the parameter entry on `api/group-chat.mdx` and the `GroupChat` section of `CLAUDE.md`; the `CLAUDE.md` side is tracked in #2832. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `SequentialWorkflow(shared_memory_system=...)` is documented at `swarms/structs/sequential_workflow.py:71` and `:111` as a callable for managing shared memory between agents, stored at `:140`, and never called or read. It is not forwarded to the `AgentRearrange` the workflow builds. ## Change Remove the parameter, the attribute and the docstring entries, with a one-release deprecation warning since it is public. Shared history between agents is what the `channel` parameter from #2826 provides. Docs: the parameter entry on `api/sequential-workflow.mdx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `AgentRearrange(memory_system=...)` is documented at `swarms/structs/agent_rearrange.py:175` and `:244` as an optional memory system for persistence, stored at `:275`, and never read again. Nothing in the package, tests or examples uses `.memory_system`. ## Change Remove the parameter, the attribute and the two docstring entries. It is public, so accept it for one release with a deprecation warning, then drop it. Persistence is `autosave` plus the workspace, and shared history is the `channel` parameter from #2826. Docs: the parameter entry on `api/agent-rearrange.mdx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `swarms/structs/hierarchical_structured_communication_framework.py` is 965 lines with no caller in the package. Six unused classes were already removed from it in `846f0e040`; what remains is a framework no structure, router or agent imports. ## Related files to remove in the same change - `examples/multi_agent/hscf_examples/single_file_hierarchical_framework_example.py` and its folder - In the docs repo: `api/hierarchical-communication-framework.mdx`, its entries in `architectures/structures-catalog.mdx` and `api/structs-overview.mdx`, and a redirect in `docs.json` so old links still resolve - Any export in `swarms/structs/__init__.py` and the changelog entry under Unreleased ## Why delete rather than move Moving it to `examples/` keeps 965 lines alive that nothing exercises. If a hierarchical communication pattern is wanted later, `HierarchicalSwarm` is the maintained home for it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `swarms/structs/majority_voting.py:136` declares `additional_consensus_agent_kwargs: dict = {}`. The dict is created once at import time and shared by every `MajorityVoting` instance that does not pass its own. ## Effect Harmless today, because the dict is only splatted into the consensus agent's constructor at line 171. Any future `.update` or key assignment on it would leak between instances. ## Change Default to `None` and use `additional_consensus_agent_kwargs or {}`. ruff's B006 rule reports this one and nine others in the package; enabling B006 in the ruff config stops it coming back. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `Agent._run` already handles a failure: `_handle_run_error` (`swarms/structs/agent.py:1328`) logs the error with its traceback, saves state when `autosave` is on, and re-raises. Two wrappers then catch that same exception and call `_handle_run_error` again: - `Agent.arun` at `agent.py:1510` - `Agent.__call__` at `agent.py:1533` The autonomous loop does the same in `swarms/agents/autonomous_loop.py`. ## Effect Every failure through `arun` or `__call__` produces two error log lines and two state saves. ## Change Let `run` be the only place that handles. `arun` becomes `await asyncio.to_thread(self.run, ...)` with no try, and `__call__` becomes `return self.run(...)`. A test that fails a run through `__call__` and asserts one `_handle_run_error` call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `Conversation(output_metadata=...)` is stored at `swarms/structs/conversation.py:120` and never read. There is no other reference to it in the package, the tests or the examples. It is a documented flag with no behaviour. ## Change Remove the parameter, the attribute and its docstring entry. Nothing depends on it, so no deprecation is needed. Docs: the parameter entry on the Conversation page in the docs repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `HeavySwarm(agents=...)` is silently ignored. `swarms/structs/heavy_swarm.py:182` assigns `self.agents = self.create_agents()` unconditionally, and the docstring at line 126 says the parameter is "currently unused". Users who pass their own agents get the four generated ones and no warning. ## Change Either of these, not the current silence: - Honour the list: when `agents` is given, use it instead of `create_agents()`, mapping by role or by position. - Raise `ValueError` with a message that says the swarm builds its own agents, and remove the parameter from the signature and docs. Docs: `api/heavy-swarm.mdx` in the docs repo either way. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `MixtureOfAgents.aggregator_model_name` defaults to `"claude-sonnet-4-20250514"` (`swarms/structs/mixture_of_agents.py:87`). Every other default in the package is `"gpt-5.4"`, and the dated id is one provider deprecation away from a 404 for anyone who did not set it. ## Change Default to `DEFAULT_MODEL_NAME` from `swarms/agents/llm_manager.py`. Docs: the parameter entry on `api/mixture-of-agents.mdx` in the docs repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `CLAUDE.md` and the contributions skill describe three things the code does not have. - **PlannerWorkerSwarm.** The example reads `PlannerWorkerSwarm(planner_agent=planner, worker_agents=workers, ...)`. The class takes `agents` and `planner_model_name`; there is no `planner_agent` or `worker_agents` parameter, and the example raises `TypeError`. - **GroupChat idle_timeout.** Listed as a tuning knob ("Raise idle_timeout if agents need time to think"). `swarms/structs/groupchat.py:229` documents it as "Deprecated/unused"; it is stored and never read. - **Contributions skill.** Cites `tests/structs/test_aop.py` as a CI collection blocker. That file no longer exists. ## Change Regenerate the structure sections of `CLAUDE.md` from the constructor signatures, drop the `idle_timeout` advice, and update the CI table in `.claude/skills/swarms-contributions/SKILL.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `Agent.run` accepts `correct_answer` and does nothing with it. ruff reports it as an unused argument at `swarms/structs/agent.py:2448`. The docs say it is "only forwarded to fallback-model runs", and since #2811 the fallback path forwards nothing. ## Change Remove it from `run`, `_dispatch` and the docstring. Callers passing ground truth for evaluation should use the evaluation structures that read it. Docs: the `run()` parameter entry on `api/agent.mdx` in the docs repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What `ConcurrentWorkflow` takes both `auto_save` and `autosave`. Only `autosave` is read: - `swarms/structs/concurrent_workflow.py:151` stores `self.auto_save` - `:156` stores `self.autosave` - `:185` passes `enabled=self.autosave` to the workspace Setting `auto_save=False` does nothing. ## Change Remove `auto_save` from the signature, the docstring and the attribute. Every other structure spells it `autosave`, so this is the one outlier. If a deprecation is wanted, accept it for one release and warn. Docs: the parameter entry on `api/concurrent-workflow.mdx` in the docs repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What When `SWARMS_TELEMETRY_ON` is on but telemetry cannot work, the run must carry on with no error message and no other output. Telemetry is a side channel; a failure in it must never change what the user sees. ## Today Setup already does this: `swarms/telemetry/otel.py` catches a failed tracer or exporter setup and logs at debug level only. Two paths still surface errors: - `swarms/telemetry/bootup.py` logs `Telemetry error` and `Bootup error` at error level, and re-raises the bootup one. - The OpenTelemetry exporter's own loggers are silenced for one module name only (`opentelemetry.exporter.otlp.proto.http.trace_exporter` set to `CRITICAL`); an export that fails mid-run from a sibling logger still prints. ## Change - Catch and swallow in `bootup.py`; at most a debug line. - Silence every `opentelemetry.*` logger the exporter path uses, or wrap the exporter so an export failure is dropped. - A test that points the exporter at a closed port, runs an agent with a fake LLM, and asserts nothing reaches stdout or stderr and no exception escapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What A first `Agent()` in a process takes about 250 ms; a warm one takes under 1 ms. The cold cost is litellm loading its model map on the first `get_model_info`. After that, every agent still calls `get_model_info` twice: once in `_default_context_length` and once in `_default_max_tokens`. ## Change - Look the model up once per construction and derive both defaults from the one result. - Cache per model name with `lru_cache`, the way `_model_supports_function_calling` already does in `swarms/agents/llm_manager.py`. ## Impact Small per agent. Larger for `HeavySwarm`, `AutoSwarmBuilder` and any test that builds many agents, where the same model is looked up over and over. ## Where `swarms/structs/agent.py`: `_default_context_length` and `_default_max_tokens`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What Add `Conversation.truncate(index)` that drops every row from `index` onward, resets `_str_cache` once, and returns the removed rows. ## Why `Agent._run` drops a failed run's turns today by calling `delete(-1)` in a loop: ```python while len(self.short_memory.conversation_history) > history_start: self.short_memory.delete(-1) ``` A single method that cuts at an index says what is happening, invalidates the cache once instead of per row, and is what any resume logic wants, including the comms-channel seeding in #2826. ## Where `swarms/structs/conversation.py`, next to `delete`. About 8 lines plus a test in `tests/structs/test_conversation.py`. Replace the loop in `Agent._run` with the new call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Fable 5.1)
## What's wrong `BatchedGridWorkflow.run` hands each agent its task through `batched_grid_agent_execution`, but nothing clears the agents' `short_memory` between runs. If you call `run()` twice on the same workflow, which the existing `test_batched_grid_workflow_sequential_runs` does, each agent gets the previous run's task and answer before the new task. The agent's default `output_type` returns that history too, so the result the caller gets for Q2 includes the Q1 answer. Offline repro (`call_llm` stubbed, no API key): ```python wf = BatchedGridWorkflow(agents=[stub("Q-Analyst"), stub("Feedback-Reviewer")]) wf.run(["Analyze Q1 revenue", "Review Q1 feedback"]) second = wf.run(["Analyze Q2 revenue", "Review Q2 feedback"]) ``` On master: ``` messages sent on run 2 to Q-Analyst: ['Analyze Q1 revenue', 'Q-Analyst answer to: None', 'Analyze Q2 revenue'] run 2 result for Q-Analyst: 'Q-Analyst answer to: None\nAnalyze Q2 revenue\nQ-Analyst answer to: None' ``` This also costs more tokens with every run, and the model answers the Q2 task with the Q1 work still in its context. ## What this changes `run_` calls `agent.reset()` on every agent before its first loop. `MixtureOfAgents._run` already resets its agents the same way. The reset runs once per `run()`, not once per loop, so with `max_loops > 1` each loop still builds on the previous one, which is what the class docstring describes as iterative processing. ## Verification - New test `test_batched_grid_workflow_second_run_does_not_send_the_first` stubs `call_llm` on two real `Agent`s, runs the workflow twice, and checks that the second run's messages contain the Q2 task and nothing from Q1. It passes with the fix. - With `swarms/structs/batched_grid_workflow.py` reverted to master, it fails: ``` E AssertionError: ['Analyze Q1 revenue', 'Revenue answer', 'Analyze Q2 revenue'] E assert not True ``` - `black -l 70 --check` passes on both files. `ruff check` reports nothing on the changed lines; every finding it lists was already on master. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `swarms autoswarm` asks an LLM for a YAML swarm config, and `generate_swarm_config` loads it after `parse_yaml_from_swarm_markdown` passes it through `prepare_yaml_for_parsing`. That function runs three regex "repairs" on every config, including configs that are already valid YAML, and each repair alters valid input: - `(\S):(\S)` → `\1: \2` adds a space after every colon inside values, not just after keys. URLs, times and model ids are changed without any error (`https://arxiv.org` becomes `https: //arxiv.org`, `ollama/llama3:8b` becomes `ollama/llama3: 8b`). An unquoted value such as `Plan the 9:00 standup` turns into `9: 00`, which YAML rejects with `mapping values are not allowed here`. - In `(\b\w+\b):\s*-\s*`, `\s*` also matches newlines. A list indented with four spaces (`agents:\n - agent_name: ...`) is moved to two spaces while its other keys stay at six, and the result no longer parses. - In `\s+\n` → `\n`, `\s+` also matches newlines, so blank lines are removed from `|` block-scalar system prompts. In the parse cases the builder's `@retry` makes three LLM calls and then fails. In the other cases the generated `autoswarm_*.py` contains the altered prompt or model id. Offline repro: valid YAML loaded directly with `yaml.safe_load`, then through `parse_yaml_from_swarm_markdown` on master: ``` == url+time quoted raw : {... 'system_prompt': 'Cite sources from https://arxiv.org and note times like 09:30.', 'model_name': 'ollama/llama3:8b'} ... swarms : {... 'system_prompt': 'Cite sources from https: //arxiv.org and note times like 09: 30.', 'model_name': 'ollama/llama3: 8b'} ... == unquoted time raw : {'agents': [{'agent_name': 'Planner', 'system_prompt': 'Plan the 9:00 standup agenda'}]} swarms : ERROR ScannerError mapping values are not allowed here == 4-space indent raw : {'agents': [{'agent_name': 'A', 'system_prompt': 'You are A.'}, {'agent_name': 'B', 'system_prompt': 'You are B.'}]} swarms : ERROR ParserError while parsing a block mapping == block scalar raw : {'agents': [{'agent_name': 'A', 'system_prompt': 'Paragraph one.\n\nParagraph two.\n'}]} swarms : {'agents': [{'agent_name': 'A', 'system_prompt': 'Paragraph one.\nParagraph two.'}]} ``` ## What this changes `prepare_yaml_for_parsing` first tries `yaml.safe_load` on the raw text. If that works, the text is returned unchanged. The regex repairs now run only when the YAML does not load as written, and broken output such as `agents: - agent_name: A` is still repaired to `{'agents': [{'agent_name': 'A'}]}`. With this PR, all four configs above load the same as they do with a direct `yaml.safe_load`. One known limit: YAML that is missing the space after a key (`name:Foo`) still parses, but as a plain string, so it no longer goes through the space-after-colon repair. Current models rarely produce that, while colons inside values (URLs, times, model ids) are common. No public signature or return type changes. ## Verification - `tests/agents/test_autoswarm_writer.py::TestFullPipelineE2E::test_valid_yaml_is_parsed_as_written` puts four-space indentation, a URL, a block scalar with a blank line, an unquoted time and a colon-bearing model id through `parse_yaml_from_swarm_markdown`, and asserts the result equals `yaml.safe_load` of the same text. - With this PR: `50 passed` for the whole file. - With `auto_generate_swarm_config.py` checked out from `origin/master`, the new test fails: ``` E yaml.parser.ParserError: while parsing a block mapping E in "<unicode string>", line 2, column 5: E - agent_name: "Researcher" E expected <block end>, but found '<block mapping start>' ``` - `ruff check` on the changed files reports no findings in the added lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong #2541 fixed the schema for tools whose parameters are Pydantic models (`items: list[LineItem]`, `Optional[Model]`, a model nesting another), so the provider now gets a valid schema and sends back a `LineItem`-shaped object. Running the call is still broken. `BaseTool._execute_single_function_call` calls `func(**arguments)` with the decoded JSON as is, so a parameter typed `list[LineItem]` receives a list of dicts. The first time the tool touches a field, it fails. Every `Agent(tools=[...])` call goes through this method (`ToolManager.execute_tools` and the autonomous loop both call `execute_function_calls_from_api_response`). Offline repro on master, with no LLM call: ```python class LineItem(BaseModel): sku: str qty: int def place_order(items: list[LineItem], gift: Optional[LineItem] = None) -> str: """Place an order.""" return f"{sum(item.qty for item in items)} units, gift={gift and gift.sku}" call = {"id": "call_1", "type": "function", "function": {"name": "place_order", "arguments": json.dumps({"items": [{"sku": "A1", "qty": 2}, {"sku": "B2", "qty": 3}]})}} BaseTool(tools=[place_order]).execute_function_calls_from_api_response([call], return_as_string=False) ``` ``` master: ToolExecutionError ... Error executing function place_order: 'dict' object has no attribute 'qty' fixed: ['5 units, gift=None'] ``` ## What this changes - `BaseTool._execute_single_function_call` runs the tool through `load_basemodels_if_needed`. That helper already lives in `py_func_to_openai_func_str.py` and is exported from `swarms.tools`, but nothing called it. - Two gaps in that helper are fixed, because wiring it in as it was would have caused failures of its own: - Its loader only recognised a bare model, so `list[LineItem]` and `Optional[LineItem]` were still passed as dicts. It now detects a model anywhere in the annotation and builds the value with `TypeAdapter(t).validate_python`. That covers lists, `Optional`, dicts and `Annotated`. - It read `kwargs[k]` for every model parameter, so a call that left out an optional model parameter raised `KeyError`. Now it only converts parameters the call actually passed, and the default applies to the rest. - Parameters that do not involve a model are passed through unchanged. A tool whose parameters are all `str`/`int` behaves exactly as before. In particular, nothing new is coerced or validated for those parameters. ## Verification - New `test_tool_receives_models_for_model_parameters` in `tests/tools/test_base_tool.py` passes (all 23 tests in the file pass). - With the two source files reverted to master and the test kept, it fails: `ToolExecutionError: ... Error executing function ship: 'dict' object has no attribute 'qty'` - `tests/tools`: the same 11 `test_mcp_manager.py` failures/errors appear on master and on this branch. No new failures. - `ruff check` reports no new findings on the changed files, and `black -l 70 --check` leaves all three files unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `AgentRearrange` looks up each flow step by name with `find_agent_by_name`, which returns the first match. If two different agents share an `agent_name`, the flow can only ever reach the first one. It runs at every step that names it, and the second agent never runs. Nothing raises and nothing is logged. This is easy to hit through `SequentialWorkflow`, because `Agent.agent_name` defaults to `"swarm-worker-01"`. Agents built with a `system_prompt` and `model_name` but no name all end up in a flow like `swarm-worker-01 -> swarm-worker-01`. Offline repro, with `run` stubbed to record which agent was called: ```python wf = SequentialWorkflow(agents=[mk("researcher"), mk("writer")], autosave=False) wf.run("t") ``` ``` flow: swarm-worker-01 -> swarm-worker-01 calls: ['researcher', 'researcher'] ``` You pay for two calls and get the researcher twice. The writer never runs. ## What this changes `AgentRearrange.reliability_check` runs on every construction, including the ones `SequentialWorkflow` and `SwarmRouter` make. It now raises `ValueError` when two *different* members share a name: ``` ValueError: Two different agents are named 'swarm-worker-01'; the flow finds agents by name, so give each one a unique agent_name ``` The check compares identity. Repeating the same agent object, as in `agents=[writer, reviewer, writer]` with `flow="Writer -> Reviewer -> Writer"`, is still valid. A nested swarm is compared through its wrapper's `.swarm`, so listing the same swarm twice is also still valid. The check follows the same rule as #1921, which made `create_agent_map` reject duplicate names. `add_agent` and `add_agents` can still add a same-named agent after construction, because they don't run `reliability_check`. This PR covers the constructor path that `SequentialWorkflow` and `SwarmRouter` take; the add methods can follow if you want them checked too. Docs: the AgentRearrange and SequentialWorkflow pages in swarms-framework-docs should say that every agent in a flow needs a unique `agent_name`. That repo only accepts issues from collaborators, so I couldn't file one there. ## Verification - New test `test_two_agents_sharing_a_name_raise` in `tests/structs/test_agent_rearrange.py`. It builds two distinct mock agents both named `Writer` and expects `ValueError`. It then runs a flow that repeats one agent object (`Writer -> Reviewer -> Writer`) to confirm that case still works. - It passes with the fix. With `swarms/structs/agent_rearrange.py` reverted to master, it fails with `Failed: DID NOT RAISE ValueError`. - I ran `test_agent_rearrange.py`, `test_sequential_workflow.py` and `test_swarm_router.py` with and without the fix. The only difference in results is the new test. The 39 other failures are live-LLM tests that need `OPENAI_API_KEY`, and they fail the same way on master. - `black -l 70` is clean on the changed lines. `ruff` reports the same count before and after. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong When `max_planner_depth > 1`, `_run_planner` cancels every CRITICAL task and asks a sub-planner to break it into sub-tasks. The dependency graph is never updated to match, which breaks it in two ways: - **Dependents are dropped.** A task that depended on the critical task still points at the cancelled task's id. `TaskQueue.claim` only hands out a task once all its dependencies are COMPLETED, so that task is never claimed. Since #2542, `is_all_done` treats it as finished, so the run ends and the task is skipped without any error. #2538 named this case as one cause of the old hang; #2542 fixed the hang, but the task is still lost because nothing replans it when `max_loops=1`. - **Sub-tasks skip the parent's prerequisites.** The sub-tasks only carry dependencies on each other. They become claimable straight away, before the tasks the critical task was waiting on. Offline repro: stub planner, one stub worker, judge stubbed to `is_complete=True`. The plan is `Fetch` -> `Build` (CRITICAL) -> `Ship`, and the sub-plan for `Build` is `Compile` (HIGH) -> `Link`: ``` executed: ['Compile', 'Fetch', 'Link'] {'Fetch': 'completed', 'Build': 'cancelled', 'Ship': 'pending', 'Compile': 'completed', 'Link': 'completed'} ``` `Ship` never runs, and `Compile` runs before `Fetch`, which it was supposed to wait for. ## What this changes After the sub-planner returns, `_run_planner` updates the graph in the same place it cancels the parent: - each sub-task's `depends_on` is prefixed with the parent's `depends_on`; - every task that depended on the parent now depends on the sub-tasks instead. Sub-tasks that were themselves cancelled for deeper decomposition (`max_planner_depth >= 3`) are left out, so nothing ends up waiting on a cancelled id again. If the sub-plan comes back empty, the dependents wait on the parent's own prerequisites instead. Planning finishes before the `WorkerPool` starts, so the tasks are updated in place before any worker can claim them. Nothing changes when `max_planner_depth=1` (the default). With the fix, the same repro gives: ``` executed: ['Fetch', 'Compile', 'Link', 'Ship'] ``` ## Verification - New test `test_planner_worker_swarm_sub_plan_keeps_the_critical_task_dependencies` in `tests/structs/test_hierarchical_swarm.py`, which already holds the `PlannerWorkerSwarm`/`TaskQueue` tests. It passes. - With `swarms/structs/planner_worker_swarm.py` reverted to `master`, it fails: ``` AssertionError: assert ['Compile', 'Fetch', 'Link'] == ['Fetch', 'Co...Link', 'Ship'] ``` - The rest of the file: the same 5 tests fail with and without this change (they call a real model and need network or API keys). 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Summary `AutoSwarmBuilder` documented `max_agents` and `max_json_repair_attempts` as constructor parameters, and the rest of the class already validates and uses them, but `__init__` never accepted them (hard-coded `None` / `1`). Passing either raised `TypeError`. This wires both into the signature with the documented defaults and assigns them so the existing `reliability_check`, repair loop, and `_enforce_max_agents` paths become reachable from construction. Fixes #2726 ## Docs Companion docs update is ready on fork (upstream docs repo restricts PRs to collaborators): https://github.com/The-Swarm-Corporation/swarms-framework-docs/compare/main...ninglinLiu:swarms-framework-docs:docs/auto-swarm-builder-ctor-params-2726 ## Test plan - [x] Source smoke: constructor accepts `max_agents` / `max_json_repair_attempts` and stores them - [ ] CI: `tests/structs/test_auto_swarms_builder.py` cases that pass these kwargs (were TypeError on main) 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `LLMCouncil` sends each review to the members as anonymised `Response A/B/C`, and the evaluation prompt tells each member the set "includes potentially your own". In practice the member always knew which response was its own. Every stage reuses the same member `Agent`s, none of them was ever reset, and none of these calls passes `messages=`. So each request is built from the member's own `short_memory`. That causes two failures: - **The ranking is not anonymous.** When a member ranks, its request still holds its own first-stage answer as an `assistant` turn, word for word, so it can pick out its own letter. This happens on every run, not only when the council is reused. - **A reused council leaks earlier tasks.** On a second `run()`, or any `batched_run()`, each member answers task 2 with task 1, its answer, the full ranking prompt (every member's response) and its ranking still in the request. Offline repro: two members, `call_llm` stubbed to record the `messages` it receives, then `run("FIRST-TASK")` followed by `run("SECOND-TASK")`. These are the requests member `Alpha` sent on master: ``` --- Alpha request 1: 1 messages user | FIRST-TASK --- Alpha request 2: 3 messages user | FIRST-TASK assistant | Alpha answer #1 user | You are evaluating responses from your fellow LLM Council members to t --- Alpha request 3: 5 messages user | FIRST-TASK assistant | Alpha answer #1 user | You are evaluating responses from your fellow LLM Council members to t assistant | Alpha answer #2 user | SECOND-TASK --- Alpha request 4: 7 messages ... ``` ## What this changes `run()` now calls `member.reset()` on every member twice: before the members answer, and before they rank. `Agent.reset()` rebuilds `short_memory` the way `__init__` does, so the system prompt and any seeded messages stay. `MixtureOfAgents` already resets its workers per run in the same way (#2783). With the fix, each of the four requests above holds exactly one `user` message. The Chairman is left alone on purpose. #2207 and #2247 already change how it receives the conversation. #2184 changes which text the ranking prompt shows (final answers instead of transcripts); it does not clear the members' memory, so the two changes are independent. ## Verification - New test `test_llm_council_members_answer_each_call_from_a_clean_memory` in `tests/structs/test_llm_council.py`. It uses real `Agent`s with only `call_llm` stubbed, so no network or API key is needed. It runs two tasks and checks that every member's second answer request is just `SECOND-TASK`, and that each ranking request is a single `user` turn. - With `swarms/structs/llm_council.py` reverted to master, the test fails: `AssertionError: assert ['FIRST-TASK'...'SECOND-TASK'] == ['SECOND-TASK']` - With only the first reset in place, it fails on the ranking check: `AssertionError: assert ['user', 'assistant', 'user'] == ['user']` - With the fix, it passes. `black --check` is clean, and `ruff check` reports nothing new on the changed lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2814 ## What was wrong `Agent.add_tool`, `add_tools`, `remove_tool` and `remove_tools` only edited `agent.tools`. The three things that decide which tools the model can call are built from that list once, in `__init__`: the schemas in `agent.tools_list_dictionary`, the `BaseTool` executor in `agent.tool_struct` (pydantic copies the list, so later appends never reach it), and the `LiteLLM` client in `agent.llm`, which keeps its own copy of the schemas. So on master: - a tool added after construction is never offered to the model, and the executor raises `ToolNotFoundError` for it - a removed tool is still offered and still executable - `add_tool` on an agent built without `tools` raises `AttributeError: 'NoneType' object has no attribute 'append'` The docs currently warn about this ("not re-registered with the model") and give a four-line manual rebuild in `api/tool-manager.mdx`. ## What this changes - `ToolManager.reload_tools(removed=())` in `swarms/agents/tool_manager.py` drops the schemas of removed tools, re-runs the same branch `load_tools` uses (`setup_dynamic_tools` when a `tool_loader` exists, else `tool_handling`), rebuilds `tool_struct`, and rebuilds `agent.llm` through `llm_handling()` when it is a `LiteLLM`. A caller-supplied non-LiteLLM `llm` is left alone. - `Agent.add_tool` and `remove_tool` delegate to `add_tools` and `remove_tools`, which call `reload_tools`. `add_tools` builds a new list, which handles `tools=None` and no longer mutates the list the caller passed to the constructor. If the new tools cannot be converted, `agent.tools` is restored and the error is raised. - `tool_handling` is unchanged, so like the documented manual rebuild it appends the current tool list to `short_memory`. A function with no docstring now raises on `add_tool` the same `ValueError` the constructor already raises for it, since there is nothing to describe it to the model with. The old `test_callable_tools_register_and_unregister` added undocumented lambdas and only checked `len(agent.tools)`, which is how this went unnoticed; it now checks what the model is offered and what the executor runs, offline. ## Verification Repro from #2814, master (29b80ec6) vs this branch: ``` master this branch list_tools() ['get_weather'] ['get_weather', 'get_time'] llm tools ['get_weather'] ['get_weather', 'get_time'] tool_struct.tools ['get_weather'] ['get_weather', 'get_time'] after remove llm tools ['get_weather'] ['get_time'] execute get_time ToolExecutionError ["Function 'get_time' result:\nnoon in Paris"] add_tool, tools=None AttributeError ['get_time'] ``` With `dynamic_tools=True` the added tool lands in the deferred catalog (`deferred: ['get_time', 'get_weather']`) and a removed one leaves it. The rewritten test, with the source reverted to master and then restored: ``` $ pytest tests/structs/test_agent.py::TestAgentToolUsage::test_callable_tools_register_and_unregister master source: E AttributeError: 'NoneType' object has no attribute 'append' 1 failed this branch: 1 passed ``` With only the `None` crash patched on master, the same test still fails: the client is offered `None` tools. `tests/structs/test_agent.py` plus `tests/tools/test_dynamic_tool_loader.py` with no API key: 29 failed, 168 passed here, 30 failed, 167 passed on master. The 29 are the same provider-key failures on both; the one difference is this test. `black . --check` and `ruff check` are clean. ## Not done - With eager MCP (`mcp_url` set, `dynamic_tools` off), the client rebuild refetches the MCP tool list. If that server has gone down since construction, `add_tools` raises after the schemas and executor were already rebuilt, and only `agent.tools` is restored. Construction fails the same way against a down server; I kept the rollback to the conversion error rather than snapshotting every attribute. - The warnings in `api/agent.mdx`, `agents/agent-tools.mdx` and `api/tool-manager.mdx` in swarms-framework-docs describe the old behaviour. Tracked in The-Swarm-Corporation/swarms-framework-docs#322. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2815 ## What is wrong `OneOnOneDebate.run` records each speaker's raw `run()` return value as that speaker's turn, and passes the same value to the other speaker as its next message. An `Agent`'s default `output_type` is `"str-all-except-first"`, so that value is the speaker's whole transcript, not its reply. Each message carries the previous turn's transcript nested inside it, so context compounds every turn. Here are real `Agent`s with `call_llm` stubbed, `max_loops=3` and `output_type="list"`. The full script is in #2815. ``` {'role': 'Pro', 'content': 'Pro point 1\nIs remote work better?\nPro point 2'} {'role': 'Con', 'content': 'Con point 1\nPro point 1\nIs remote work better?\nPro point 2\nCon point 2'} {'role': 'Pro', 'content': 'Pro point 1\nIs remote work better?\nPro point 2\nCon point 1\nPro point 1\nIs remote work better?\nPro point 2\nCon point 2\nPro point 3'} ``` The turns should be `Pro point 2`, `Con point 2` and `Pro point 3`. ## Why `OneOnOneDebate.run` contains a second copy of the alternating debate loop. The first copy, `deep_discussion.one_on_one_debate`, was fixed for exactly this in #2113: it records and forwards `agent_answer(speaker)`. The same fix landed for `ExpertPanelDiscussion` in this file in #2634, and for `ReflexionAgent` in #2807. The copy in `OneOnOneDebate.run` never got it. ## What this changes `OneOnOneDebate.run` keeps its two-agent check and its intro priming, where each agent is told who it is debating. It then hands the turns to `deep_discussion.one_on_one_debate` with the same `max_loops`, `task`, `agents`, `img` and `output_type`, and the duplicated loop is deleted. I chose delegation over pasting `agent_answer` into the second copy so the loop lives in one place, and the next fix to it reaches both entry points. Behaviour other than what each turn records and forwards is unchanged: the agent call count is still `2 + max_loops`, the speaker order and the `ValueError` for anything other than two agents stay the same, and the default `output_type` is untouched. That default is a separate open issue, #2243. ## Verification - New test `test_one_on_one_debate_passes_on_only_the_latest_reply` in the existing `tests/structs/test_multi_agent_debate.py`. It reuses that file's `_HistoryAgent`, whose `run` returns the transcript the way a default `Agent` does. It asserts that each speaker receives only the other's latest reply and that the returned turns are the replies. - Unfixed `multi_agent_debates.py`: `AssertionError: assert 'Pro reply 1\nIs remote work better?\nPro reply 2' == 'Pro reply 2'` - With the fix: passes. - The rest of `tests/structs/test_multi_agent_debate.py` behaves as it does on master. The two `test_expert_panel_discussion_*` tests that need a real `OPENAI_API_KEY` fail on both. - With the fix, the repro above prints `Pro point 2`, `Con point 2` and `Pro point 3`. - `black . --check` and `ruff check .` pass repo-wide. ## Docs `api/orchestration-methods.mdx` currently documents the old behaviour as a caveat ("Each turn records, and passes on, whatever the speaker's `run()` returns ... Give both agents `output_type="final"`"). That caveat needs to go once this merges. Docs issue: The-Swarm-Corporation/swarms-framework-docs#321. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2813 ## What is wrong `AgentRearrange(team_awareness=True)` is meant to show every agent the whole pipeline. `SequentialWorkflow(team_awareness=True)` passes the flag straight through. On master, no agent ever receives the flow structure: the model input with the flag on is identical to the input with it off. ``` team_awareness=False Researcher system turns: ['Sequential awareness: Agent behind: Writer'] Writer system turns: ['Sequential awareness: Agent ahead: Researcher'] team_awareness=True Researcher system turns: ['Sequential awareness: Agent behind: Writer'] Writer system turns: ['Sequential awareness: Agent ahead: Researcher'] ``` The full repro script is in #2813. ## Why `_reset_conversation` stores the flow structure as a `system` row in the shared conversation. Since #2078, agents receive that conversation through `context_utils.messages_for`, which drops `system` rows on purpose: its docstring says roster-style bookkeeping belongs in a system prompt. But `AgentRearrange._messages_for` builds that system prompt from `collab_prompt` and the per-agent `extra_system` only. Nothing carries the flow structure to the model. The row is still recorded, so it shows up in the history `run()` returns, and the feature looks like it works. The existing test `test_team_awareness_is_a_system_turn_not_user_prose` asserted `"Sequential" in system_text`. The per-agent `Sequential awareness: ...` line satisfies that whether or not the flag is set, so the test passed while the feature was dead. ## What this changes `AgentRearrange._messages_for` adds `_get_sequential_flow_info()` to the system turn it already builds when `team_awareness` is set. The order is collaboration prompt, then flow structure, then per-agent position. Every execution path builds its messages through `_messages_for`: sequential steps, parallel steps, and both streaming paths. So this one change covers all of them. What deliberately stays the same: - The `system` row in the shared conversation is still written. `run()`'s returned history does not change, and the docs line "the shared conversation also starts with the full sequential flow structure" stays true. - `messages_for` is untouched. It still keeps structure rows out of the conversation body. - The per-agent `Sequential awareness` line is still sent whether or not the flag is set, as the docs describe. ## Verification - The existing `test_team_awareness_is_a_system_turn_not_user_prose` now runs `A -> B, C`, so it covers both a sequential step and a parallel step. It asserts that every agent gets `Sequential Flow Structure` in a system turn and never as user prose. - Unfixed `agent_rearrange.py`: `AssertionError: assert 'Sequential Flow Structure' in 'Sequential awareness: Agent behind: B, C'` - With the fix: `1 passed` - `tests/structs/test_agent_rearrange.py` and `tests/structs/test_sequential_workflow.py`: 22 failed, 91 passed on both master and this branch, with the same failing set. All 22 failures call a real model and fail with `No API key for provider 'openai'`. - `black . --check` and `ruff check .` pass repo-wide. ## Docs No docs change is needed. `architectures/agent-rearrange.mdx` already describes the behaviour this restores ("every agent sees the whole pipeline"). ## Not tested I have not run this against a live model. The tests check the messages each agent receives, not how a model uses them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Bug `OneOnOneDebate.run` records each speaker's raw `run()` return value as that speaker's turn, then hands the same value to the other speaker as its next message. An `Agent`'s default `output_type` is `"str-all-except-first"`, so that value is the speaker's whole transcript (the intro prompt, the task, and every earlier turn), not its reply. Each turn's message carries the previous turn's transcript inside it, so context compounds every turn. The returned debate history holds nested transcripts instead of arguments. ## Reproduction These are real `Agent`s with `call_llm` stubbed, so no API key is needed: ```python from swarms import Agent from swarms.structs.multi_agent_debates import OneOnOneDebate def agent_with_fake_llm(name): agent = Agent(agent_name=name, model_name="gpt-5.4", max_loops=1, print_on=False) counter = {"n": 0} def call_llm(task, *args, **kwargs): counter["n"] += 1 return f"{name} point {counter['n']}" agent.call_llm = call_llm return agent debate = OneOnOneDebate( max_loops=3, agents=[agent_with_fake_llm("Pro"), agent_with_fake_llm("Con")], output_type="list", ) for turn in debate.run("Is remote work better?"): print(turn) ``` Output on master `29b80ec6`: ``` {'role': 'Pro', 'content': 'Pro point 1\nIs remote work better?\nPro point 2'} {'role': 'Con', 'content': 'Con point 1\nPro point 1\nIs remote work better?\nPro point 2\nCon point 2'} {'role': 'Pro', 'content': 'Pro point 1\nIs remote work better?\nPro point 2\nCon point 1\nPro point 1\nIs remote work better?\nPro point 2\nCon point 2\nPro point 3'} ``` ## Expected vs actual Expected: the turns are `Pro point 2`, `Con point 2`, `Pro point 3`, and each speaker receives only the other's latest reply. Actual: each turn is the speaker's transcript, and it grows on every turn. By turn 3, Pro is sent a message that contains its own earlier turn twice. ## Root cause `OneOnOneDebate.run` in `swarms/structs/multi_agent_debates.py` contains its own copy of the alternating debate loop. `deep_discussion.one_on_one_debate` runs the same loop and was fixed for exactly this in #2113: it wraps each reply in `context_utils.agent_answer`. `ExpertPanelDiscussion`, in the same file, got the same fix in #2634. The copy in `OneOnOneDebate.run` never did. ## Proposed fix Keep `OneOnOneDebate.run`'s validation and its intro priming, then run the turns through `deep_discussion.one_on_one_debate` instead of the duplicated loop. That way the debate loop lives in one place and later fixes reach both entry points. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What happens `Agent.add_tool`, `add_tools`, `remove_tool` and `remove_tools` only edit the `agent.tools` list. Everything that decides which tools the model can call is built from that list once, in `__init__`, and never rebuilt: - `agent.tools_list_dictionary`, the schemas, filled by `ToolManager.load_tools` / `tool_handling` - `agent.tool_struct`, the `BaseTool` executor. Pydantic copies the list on construction, so it does not see later appends - `agent.llm`, the `LiteLLM` client, which holds its own `tools_list_dictionary` So a tool added after construction is never offered to the model, and if the model named it anyway the executor raises `ToolNotFoundError`. A removed tool stays in the request and stays executable. On an agent built without `tools`, `agent.tools` is `None` and `add_tool` raises `AttributeError`. ## Reproduction (master, 29b80ec6) ```python from swarms import Agent def get_weather(city: str) -> str: """Get the weather for a city. Args: city (str): The city name. """ return f"sunny in {city}" def get_time(city: str) -> str: """Get the time for a city. Args: city (str): The city name. """ return f"noon in {city}" a = Agent(agent_name="a", model_name="gpt-4o-mini", tools=[get_weather], print_on=False) a.add_tool(get_time) print("self.tools:", [t.__name__ for t in a.tools]) print("list_tools():", a.list_tools()) print("llm tools:", [s["function"]["name"] for s in a.llm.tools_list_dictionary]) print("tool_struct.tools:", [t.__name__ for t in a.tool_struct.tools]) a.remove_tool(get_weather) print("after remove llm tools:", [s["function"]["name"] for s in a.llm.tools_list_dictionary]) call = {"id": "c1", "type": "function", "function": {"name": "get_time", "arguments": '{"city": "Paris"}'}} a.tool_struct.execute_function_calls_from_api_response([call]) Agent(agent_name="b", model_name="gpt-4o-mini", print_on=False).add_tool(get_time) ``` Output: ``` self.tools: ['get_weather', 'get_time'] list_tools(): ['get_weather'] llm tools: ['get_weather'] tool_struct.tools: ['get_weather'] after remove llm tools: ['get_weather'] swarms.tools.base_tool.ToolExecutionError: Failed to execute function calls from API response: Failed to execute function get_time: Function get_time not found in tools ``` and the last line, run on its own: ``` AttributeError: 'NoneType' object has no attribute 'append' ``` ## Expected After `add_tool(get_time)` the model is offered `get_time` and the executor can run it. After `remove_tool(get_weather)` the model is no longer offered `get_weather`. `add_tool` works on an agent built without tools. ## Root cause `swarms/structs/agent.py`, `Agent.add_tool` / `add_tools` / `remove_tool` / `remove_tools`, call `self.tools.append`, `extend` and `remove` and nothing else. The derived state comes from `swarms/agents/tool_manager.py` (`ToolManager.setup_tools`, `load_tools`, `tool_handling`, `setup_dynamic_tools`) and `LLMManager.build`, all of which run only during `Agent.__init__`. The docs currently describe this as a limitation ("not re-registered with the model") with a manual rebuild workaround in `api/tool-manager.mdx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Bug `AgentRearrange(team_awareness=True)` is documented to show every agent the whole pipeline. `SequentialWorkflow(team_awareness=True)` passes the flag straight through to it. On master, no agent ever receives the flow structure. With `team_awareness=True` the model gets exactly the same input as with `team_awareness=False`. ## Reproduction Stub agents record the `messages` each one receives. No API key is needed. ```python from swarms import Agent, AgentRearrange def recording_agent(name, calls): agent = Agent(agent_name=name, model_name="gpt-5.4", max_loops=1) def run(task=None, messages=None, **kwargs): calls.append((name, list(messages or []))) agent.short_memory.add(role=name, content=f"{name}-answer") return f"{name}-answer" agent.run = run return agent for team_awareness in (False, True): calls = [] agents = [recording_agent(n, calls) for n in ("Researcher", "Writer")] AgentRearrange( agents=agents, flow="Researcher -> Writer", team_awareness=team_awareness, ).run("Explain rate hikes.") print(f"team_awareness={team_awareness}") for name, messages in calls: system = [m["content"] for m in messages if m["role"] == "system"] print(f" {name} system turns: {system}") ``` Output on master `29b80ec6`: ``` team_awareness=False Researcher system turns: ['Sequential awareness: Agent behind: Writer'] Writer system turns: ['Sequential awareness: Agent ahead: Researcher'] team_awareness=True Researcher system turns: ['Sequential awareness: Agent behind: Writer'] Writer system turns: ['Sequential awareness: Agent ahead: Researcher'] ``` ## Expected vs actual Expected: the docs (`architectures/agent-rearrange.mdx`) say *"With `team_awareness=True`, the shared conversation also starts with the full sequential flow structure, so every agent sees the whole pipeline."* So each agent should get `Sequential Flow Structure: Step 1: Researcher (leads to: Writer) ...`. Actual: each agent only gets its own `Sequential awareness` line. That line is sent whether or not the flag is set. ## Root cause `AgentRearrange._reset_conversation` writes the flow structure into the shared conversation as a `system` row. Since #2078, agents receive that conversation through `context_utils.messages_for`, which skips `system` rows on purpose: its docstring says structure bookkeeping "belongs in a system prompt, not in the conversation body". But `AgentRearrange._messages_for` builds the per-agent system turn from `collab_prompt` and `extra_system` only. Nothing carries the flow structure to the model. The row is still recorded, so it shows up in the history `run()` returns. That makes it look delivered. `test_team_awareness_is_a_system_turn_not_user_prose` in `tests/structs/test_agent_rearrange.py` did not catch this. It asserts `"Sequential" in system_text`, and the per-agent `Sequential awareness` line satisfies that on its own. ## Proposed fix When `team_awareness` is set, add `_get_sequential_flow_info()` to the system turn that `_messages_for` already builds. Every execution path goes through `_messages_for`: sequential steps, parallel steps, and both streaming paths. So that one change covers all of them. The shared-conversation row stays where it is, so the returned history does not change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Summary - Delete 23 orphaned files under `swarms/prompts/` (~1,677 lines) that are never imported from `swarms/`, `tests/`, or `examples/`, and are not re-exported from `swarms/prompts/__init__.py`. - Keeps reachable prompt modules untouched. Fixes #1835 ## Test plan - [x] Re-verified each file has zero external references on current `master` - [x] Confirmed none appear in `swarms/prompts/__init__.py` - [ ] CI passes
## Problem Fixes #2734 Previously, `swarms/__init__.py` star-imported every sub-package (`swarms.agents`, `swarms.prompts`, `swarms.schemas`, `swarms.structs`, `swarms.telemetry`, `swarms.tools`, `swarms.utils`), and `swarms/structs/__init__.py` eagerly imported all 52 structure modules. Consequently, importing just `from swarms import Agent` eagerly loaded 53 `swarms.structs.*` modules and 34 `swarms.prompts.*` modules, including complex structures and workflows (`AdvisorSwarm`, `GraphWorkflow`, `HeavySwarm`, `MixtureOfAgents`, `MajorityVoting`, `SequentialWorkflow`, `SpreadSheetSwarm`, etc.) that single-agent programs never use. ## Solution 1. **PEP 562 Lazy Loading in `swarms/structs/__init__.py`**: - Replaced eager imports with a static `_MODULE_MAP` mapping all 75 structure symbols to their respective submodules. - Implemented module-level `__getattr__` to dynamically import the specific structure submodule on first access and cache it in `globals()` for direct subsequent accesses. - Retained complete `__all__` and implemented `__dir__()` to preserve IDE autocomplete and `dir()` discoverability. 2. **PEP 562 Lazy Loading in `swarms/prompts/__init__.py` and `swarms/agents/__init__.py`**: - Converted eager prompt exports to lazy resolution on first access. - Converted eager agent exports to lazy resolution on first access, breaking the circular import between `agent_judge` and `Agent`. 3. **PEP 562 Lazy Loading in `swarms/__init__.py`**: - Replaced eager star imports with `_MODULE_MAP` covering all 168 public exports previously exposed by `swarms`. - Maintained full discoverability via explicit `__all__` and `__dir__()`. - `swarms.__version__` continues to be resolved lazily via `__getattr__`. 4. **Comprehensive Unit Tests in `tests/test___init__.py`**: - `test_lazy_agent_import_does_not_load_unneeded_structures`: In an isolated subprocess, verifies that `from swarms import Agent` does NOT load unneeded structures (`advisor_swarm`, `agent_rearrange`, `graph_workflow`, `groupchat`, `heavy_swarm`, `majority_voting`, `mixture_of_agents`, `sequential_workflow`, etc.) and keeps total struct modules <= 15. - `test_every_public_name_in_swarms_all_resolves`: Asserts every public name in `swarms.__all__` (168 symbols) resolves to an object. - `test_every_public_name_in_structs_all_resolves`: Asserts every public name in `swarms.structs.__all__` (75 symbols) resolves to an object. - `test_every_public_name_in_prompts_all_resolves`: Asserts every public name in `swarms.prompts.__all__` resolves to an object. - `test_dir_lists_all_exports_before_access`: Verifies `dir(swarms)` lists all public exports in a fresh process before any access. - `test_lazy_attribute_cached_after_first_access`: Verifies accessed attributes are cached in `vars(swarms)`. - `test_unknown_attribute_on_subpackages_raises_attribute_error`: Verifies `AttributeError` for unknown attributes on `swarms.structs` and `swarms.prompts`. - `test_subpackages_accessible_as_attributes`: Verifies subpackage modules (`structs`, `prompts`, `agents`, etc.) are accessible as attributes on `swarms`. - `test_direct_import_matches_structs_import`: Verifies `from swarms import Agent, GroupChat` yields the identical objects as `from swarms.structs import Agent, GroupChat`. ## Verification & Acceptance Benchmarks ### Import Profile for `from swarms import Agent` | Metric | Before (Master) | After (This PR) | Change | |---|---|---|---| | Total modules in `sys.modules` | 907 | 767 | **-140 modules (-15.4%)** | | `swarms.*` modules loaded | 150 | 69 | **-81 modules (-54.0%)** | | `swarms.structs.*` modules loaded | 53 | 11 | **-42 modules (-79.2%)** | | `swarms.prompts.*` modules loaded | 34 | 10 | **-24 modules (-70.6%)** | | Traced Memory (Peak) | ~33.8 MB | ~28.8 MB | **-5.0 MB (-14.8%)** | All tests pass: - `pytest tests/test___init__.py`: 10 passed - `pytest tests/structs/test_agent.py -k TestBasicAgent`: 9 passed - `black . --check --diff`: All done! 5 files left unchanged - `ruff check .`: All checks passed!
## What's wrong `ConcurrentWorkflow.run` starts each task on a fresh conversation (#2332), and `batch_run` says every task "runs against its own conversation". That only covers the workflow's record of the run. Each agent is still called as `agent.run(task=...)` with no `messages`, and in that case `Agent._run` sends the agent's whole `short_memory`. So an agent in a workflow that gets reused, through `batch_run`, repeated `run()` calls or the swarm `SwarmRouter` caches for `swarm_type="ConcurrentWorkflow"`, answers task 2 with task 1 and its own answer to it still in the request. The other flow structures don't have this problem. `AgentRearrange`, `GraphWorkflow`, `RoundRobinSwarm` and `GroupChat` pass `messages`, and `MixtureOfAgents.step` resets its workers (#2783). Offline repro: two real `Agent`s with `call_llm` stubbed to record the request, then `ConcurrentWorkflow(agents=[Alpha, Beta]).batch_run(["Summarise filing A", "Summarise filing B"])`. The messages each agent sends for the second task, on master: ``` Alpha 2nd request: ['Summarise filing A', '[Alpha answer]', 'Summarise filing B'] Beta 2nd request: ['Summarise filing A', '[Beta answer]', 'Summarise filing B'] ``` ## What this changes `run()` calls `agent.reset()` on every agent right after it builds the new conversation. Both the dashboard path and the plain path go through `run()`, and so do `batch_run` and `SwarmRouter`, so this one place covers all of them. `reset()` rebuilds `short_memory` the same way `__init__` does, so the system prompt, `rules`, any seeded `messages` and the MEMORY.md preload stay, and `usage` keeps its totals. The `_EchoAgent` test double now has a no-op `reset()` so the tests that already use it keep running. ## Verification - New `test_concurrent_workflow_agents_do_not_see_an_earlier_task` in `tests/structs/test_concurrent_workflow.py` runs offline: real `Agent`s with `call_llm` stubbed. It passes with the fix. - With `swarms/structs/concurrent_workflow.py` reverted to master, the new test fails: ``` AssertionError: ['summarise filing A', 'Alpha answer', 'summarise filing B'] assert 'summarise filing A' not in ['summarise filing A', 'Alpha answer', 'summarise filing B'] ``` - `test_concurrent_workflow.py`, `test_swarm_router.py` and `test_telemetry.py`: 268 passed, 18 failed. On master the counts are 267 passed and the same 18 failed. Those 18 tests make real model calls, and there are no API keys here. - Ruff and black report nothing new on the changed files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Fixes #2612. `MultiAgentRouter` previously built one shared `Conversation` in `__init__`. Every `route_task` call (including `run`, `__call__`, `batch_run`, and `concurrent_batch_run`) appended to that same instance, so: - a second sequential `run()` still contained the first task's history - concurrent threads raced on one conversation This creates a fresh `Conversation` inside `route_task` and passes it into the handoff helpers, so each call owns its history.
Part of #1843 ## What is wrong today #1843 lists three run methods that wrap their work in the same save-twice block: one `save_conversation()` on success and a second in `except Exception` before re-raising. #2011 moved the save logic itself into `WorkspaceManager`, but these three call sites still have both copies: - `SequentialWorkflow.run` (through its `_autosave_conversation` helper) - `ConcurrentWorkflow.run` - `HierarchicalSwarm.run` Saving twice is more than duplication. `KeyboardInterrupt` is a `BaseException`, not an `Exception`, so a Ctrl-C partway through a run skips both copies and `conversation_history.json` is never written. That is the run you most want the history for. It is the run you stopped because it was slow or going wrong, and the history is lost even though autosave is on. ## What this changes In each of the three `run` methods, the two saves become one `finally:` save. That runs on success, on an error and on an interrupt. #1843 proposed an `_autosave_guard()` context manager for these call sites. `try/finally` does the same job without adding a new helper. - `SequentialWorkflow._autosave_conversation` had no callers left, so it is removed. Its single line moves into the `finally` with the same `getattr(self.agent_rearrange, "conversation", None)` argument. - The existing `logger.error(...)` lines and re-raises in `SequentialWorkflow` and `HierarchicalSwarm` stay. `ConcurrentWorkflow`'s bare `except Exception: ... raise` is gone because `finally` covers it. Source: +7 / -18. One regression test: +28. ## Verification **Reproduced on `master` (`846f0e04`).** Each orchestrator was built with `autosave=True`. Its inner call (`ConcurrentWorkflow._run`, `AgentRearrange.run`, `HierarchicalSwarm.step`) was patched to return normally, raise `RuntimeError`, or raise `KeyboardInterrupt`. The check is whether `conversation_history.json` exists afterwards. Before, on `master`: ``` ConcurrentWorkflow no error raised=- conversation_history.json saved=True ConcurrentWorkflow RuntimeError raised=RuntimeError conversation_history.json saved=True ConcurrentWorkflow KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=False SequentialWorkflow no error raised=- conversation_history.json saved=True SequentialWorkflow RuntimeError raised=RuntimeError conversation_history.json saved=True SequentialWorkflow KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=False HierarchicalSwarm no error raised=- conversation_history.json saved=True HierarchicalSwarm RuntimeError raised=RuntimeError conversation_history.json saved=True HierarchicalSwarm KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=False ``` After, on this branch: ``` ConcurrentWorkflow no error raised=- conversation_history.json saved=True ConcurrentWorkflow RuntimeError raised=RuntimeError conversation_history.json saved=True ConcurrentWorkflow KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=True SequentialWorkflow no error raised=- conversation_history.json saved=True SequentialWorkflow RuntimeError raised=RuntimeError conversation_history.json saved=True SequentialWorkflow KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=True HierarchicalSwarm no error raised=- conversation_history.json saved=True HierarchicalSwarm RuntimeError raised=RuntimeError conversation_history.json saved=True HierarchicalSwarm KeyboardInterrupt raised=KeyboardInterrupt conversation_history.json saved=True ``` Success and error behaviour is unchanged. The interrupt case now saves. **Regression test.** `test_concurrent_workflow_autosave_saves_conversation_when_interrupted` is in `tests/structs/test_concurrent_workflow.py`. It uses the file's own `_EchoAgent` stub, so it makes no model call. It checks that the saved file contains the task. It fails with the source reverted to `master`: ``` E FileNotFoundError: [Errno 2] No such file or directory: '.../swarms/ConcurrentWorkflow/Interrupted-Workflow-20261021_223617/conversation_history.json' 1 failed in 0.28s ``` and passes with the fix: ``` 1 passed in 0.34s ``` **The three structures' test files, `master` vs this branch:** | file | master | this branch | |---|---|---| | `tests/structs/test_concurrent_workflow.py` | 15 passed, 1 failed (the new test) | 16 passed | | `tests/structs/test_sequential_workflow.py` | 24 passed, 6 failed | 24 passed, 6 failed (same 6) | | `tests/structs/test_hierarchical_swarm.py` | 31 passed, 5 failed | 31 passed, 5 failed (same 5) | `black . --check` (24.2.0, line length 70) and `ruff check` pass on the touched files. ## Limitations - The 11 sequential and hierarchical failures above call a live model. They fail with a 401 on both trees because I ran them without a real API key. I did not run them against a real model. - The committed test covers `ConcurrentWorkflow` only. The sequential and hierarchical cases are covered by the before/after reproduction above, not by new tests. - `finally` also runs on `SystemExit`, so that exit now saves too. That matches what autosave is for. On an error, the `logger.error` line now runs before the save instead of after it. - Open PRs #2470 (`ConcurrentWorkflow.arun`) and #2450 (`HierarchicalSwarm.arun`) include the end of `run()` as unchanged context lines. Whichever lands second needs a small rebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Part of #2711 **What is wrong.** #2711 asks for an `acompletion` path that covers streaming as well as non-streaming. At the model layer, the streaming half doesn't work today. `LiteLLM.arun` with `stream=True` passes the `acompletion` stream to `_track_streaming_usage`, which is a sync generator running `for chunk in stream`. An `acompletion` stream can only be read with `async for`, so the generator `arun` returns fails on its first chunk. This happens on both backends. Reproduced on master (`846f0e04`), no API key, using each backend's mock response: ```python llm = LiteLLM(model_name="gpt-4o-mini", stream=True) out = await llm.arun("hi", mock_response="hello there friend") list(out) ``` ``` SWARMS_LLM_BACKEND=litellm returned: generator ERROR: APIConnectionError litellm.APIConnectionError: APIConnectionError: OpenAIException - 'async_generator' object is not an iterator SWARMS_LLM_BACKEND=routehub returned: generator ERROR: TypeError 'AsyncChatStream' object is not iterable ``` So nothing in swarms can stream a model call on the event loop. Any async streaming path built on `LiteLLM.arun`, as #2711 proposes, has to fall back to a thread until this works. That is what the non-streaming `LLMManager.acall` in #2470 does today. **What this changes.** - When `self.stream` is set, `arun` returns `_atrack_streaming_usage(response)`, an async generator over the `acompletion` stream. Non-streaming `arun` is unchanged. - The per-chunk bookkeeping moved out of `_track_streaming_usage` into `_track_chunk`, and both generators use it. That bookkeeping is the finish reason, the response model, and consuming the trailing usage-only chunk without forwarding it. Sharing it means sync and async streams record usage the same way and can't drift apart. Sync `run` behaves the same as before. Same script after the change, consumed with `async for`: ``` SWARMS_LLM_BACKEND=litellm returned: async_generator chunks: 8 text: hello there friend usage: {'input_tokens': 8, 'output_tokens': 3, 'cached_tokens': 0, 'reasoning_tokens': 0, 'total_tokens': 11} finish: stop SWARMS_LLM_BACKEND=routehub returned: async_generator chunks: 5 text: hello there friend usage: {'input_tokens': 0, 'output_tokens': 4, 'cached_tokens': 0, 'reasoning_tokens': 0, 'total_tokens': 4} finish: stop ``` These match sync `run(stream=True)` on the same mock: 8 chunks / 11 tokens / `stop` on litellm, and 5 chunks / 4 tokens / `stop` on RouteHub. **Verification.** - I added one test, `test_arun_streams_the_async_completion`, to `tests/utils/test_litellm_usage.py` next to the sync stream-usage test. It stubs `acompletion` to return an async stream of two content chunks and a usage chunk, then checks that `arun` yields `["hel", "lo"]` and records 15 tokens. With `litellm_wrapper.py` reverted to master it fails with `TypeError: 'async for' requires an object with __aiter__ method, got generator`. With the fix it passes. - `pytest tests/utils/test_litellm_usage.py tests/utils/test_litellm_output_tokens.py tests/utils/test_litellm_temperature.py`: 37 passed. - `pytest tests/utils/`: the same 17 tests in `test_litellm_wrapper.py` fail before and after the change. They are live-provider tests that need API keys. - An automated validation run also exercised the change against a local fake OpenAI-compatible SSE server on both backends. It streams the content, records usage and the finish reason (`stop`, or `length` when the server sends that), and leaves sync `run` streaming unchanged. - `black --line-length 70 . --check` is clean. **Limitations.** - This covers only the model-layer streaming part of #2711. `Agent.arun` and `arun_stream` still run the agent loop on a thread. Moving them onto this path is the rest of the issue. - If the stream breaks partway through, the backend's own error propagates during iteration and isn't wrapped in `NetworkConnectionError`. The sync stream from `run` behaves the same way. - The `arun` docstring still says it returns the same output types as `run`. With `stream=True` it now returns the async counterpart of `run`'s generator. I didn't change the docstring. - I haven't tested this against a live provider with a real key. The verification above used each backend's mock response and a local SSE server. Docs: https://github.com/The-Swarm-Corporation/swarms-framework-docs/issues/319 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Summary Fixes #2734. `swarms/__init__.py` and `swarms/structs/__init__.py` used to star-import every structure (and prompts), so `from swarms import Agent` loaded 50+ unused modules. - Replace eager exports in `swarms.structs` and `swarms.prompts` with PEP 562 `__getattr__` maps. - Resolve those names from `swarms.__getattr__` so the public import surface stays the same. - Keep `__all__` / `__dir__` complete. ## Verification - `from swarms import Agent` no longer imports heavy structures (`heavy_swarm`, `groupchat`, `swarm_router`, …). - `tests/test___init__.py` — 11 passed (includes new acceptance tests).
## Summary Fixes #1947. ### Problem In `swarms/agents/auto_generate_swarm_config.py`, `swarm_architecture.max_loops` was guarded and validated with: ```python if swarm_arch.get("max_loops"): try: max_loops_val = int(swarm_arch["max_loops"]) except (ValueError, TypeError): raise ValueError( f"swarm_architecture.max_loops must be an integer, got {type(swarm_arch['max_loops']).__name__}: {swarm_arch['max_loops']!r}" ) router_lines.append(f" max_loops={max_loops_val},") ``` This created two gaps: 1. **Booleans pass validation and are coerced:** Since `bool` is a subclass of `int` in Python (`isinstance(True, int) == True`), `int(True) == 1` and `int(False) == 0` never raise. A YAML config containing `max_loops: true` silently produced `max_loops=1`. 2. **Falsy values bypass validation and are silently dropped:** The truthiness check `if swarm_arch.get("max_loops"):` evaluates to `False` for `0` and `False`. An explicit `max_loops: 0` was silently omitted from the generated router code without notice or validation, causing `SwarmRouter` to fall back to its default (`1`) rather than validating and rejecting `0`. ### Solution 1. **Check key presence instead of truthiness:** Switch the guard to `if "max_loops" in swarm_arch:` so explicit falsy values like `0` are retained and emitted into the generated router definition (`max_loops=0,`), which `SwarmRouter` validates against `max_loops != 0`. 2. **Reject `bool` explicitly:** Check `if isinstance(raw, bool):` and raise a descriptive `ValueError` naming the `bool` type and value. 3. **Reject `float` explicitly:** Reject float values (e.g., `3.5`) rather than silently truncating them with `int()`. 4. **Preserve integer coercion for valid ints and integer strings:** Support int values and numeric strings (e.g. `4` and `"5"`). ### Tests Added Added `TestSwarmArchitectureMaxLoopsValidation` in `tests/agents/test_autoswarm_writer.py`: - `test_max_loops_bool_true_raises`: verifies `max_loops: True` raises `ValueError`. - `test_max_loops_bool_false_raises`: verifies `max_loops: False` raises `ValueError`. - `test_max_loops_float_raises`: verifies `max_loops: 3.5` raises `ValueError`. - `test_max_loops_zero_emitted_in_router`: verifies `max_loops: 0` is emitted as `max_loops=0,` rather than silently dropped. - `test_max_loops_invalid_str_raises`: verifies invalid string `"invalid"` raises `ValueError`. - `test_max_loops_none_raises`: verifies `None` raises `ValueError`. - `test_max_loops_int_and_numeric_str`: verifies valid integers and integer strings are correctly rendered. - `test_max_loops_omitted_from_swarm_architecture`: verifies no `max_loops=` line is rendered when omitted. All 57 tests in `tests/agents/test_autoswarm_writer.py` pass.
## What's wrong `ModelRouter` takes `max_tokens` (default 4000, documented as "Maximum output tokens"), but `step()` never applies it. The routed `LiteLLM` call is built with whatever `max_tokens` the routing model put in its reply, so a value above the router's cap goes straight to the provider, and a reply that leaves `max_tokens` out sends `None`. On `master` (`6967b7da`), with `LiteLLM.run` stubbed so the routing reply carries `max_tokens` of 64000, then none, then 200, the routed calls are built with: ``` ModelRouter(max_tokens=500) -> routed max_tokens: [64000, None, 200] ``` The routing prompt gives the model no guidance on `max_tokens`, and `LiteLLM` doesn't clamp it to the model's limit. So 64000 would reach the provider as is, even though `gpt-4o-mini`, for example, is listed in litellm's model map with 16,384 output tokens. ## What this changes In `ModelRouter.step`, the router's `max_tokens` fills in a missing value and caps a larger one. A smaller value the routing model picks is kept. If a caller sets `max_tokens=None`, the routed value passes through unchanged, as before. ## Verification - One new test in `tests/structs/test_model_router.py` runs the three cases above against a stubbed `LiteLLM.run`. With `master`'s `model_router.py` it fails with `[64000, None, 200] == [500, 500, 200]`. With this change it passes, along with the file's other three tests. - black and ruff are clean on both files. ## Not in this PR `api_key` is also accepted and never used. Passing it to the routed call isn't a safe fix, because the routed provider can differ from the router's own, so I left it alone. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What's wrong `CouncilAsAJudge.run` runs the six dimension judges on a thread pool, and each worker (`_evaluate_dimension`) appended its evaluation to the shared `self.conversation` the moment it finished. The order of the council's transcript was therefore whatever order the model calls happened to return in. That has two effects: 1. **The output changes from run to run for the same input.** With `output_type="all"` (or any non-`final` type), the judge rows come back shuffled by latency. 2. **The aggregator is given a random judge's evaluation as its task.** `run` hands the aggregator `split_last_turn(messages_for(...))`, so the newest row becomes the `task` and the rest become `messages`. Which judge that is depended on who finished last. Repro, offline, with the LLM stubbed so the `accuracy` judge is the slowest and `instruction_adherence` the fastest: ``` roles on master: ['User', 'instruction_adherence_judge', 'conciseness_judge', 'coherence_judge', 'harmlessness_judge', 'helpfulness_judge', 'accuracy_judge', 'aggregator_agent'] expected: ['User', 'accuracy_judge', 'helpfulness_judge', 'harmlessness_judge', 'coherence_judge', 'conciseness_judge', 'instruction_adherence_judge', 'aggregator_agent'] ``` On master the aggregator's task in that run is the `accuracy` evaluation; flip the latencies and it becomes `instruction_adherence`. This is the same defect #2318 fixed in `ConcurrentWorkflow._run`, and #1862 fixed in `run_agents_concurrently`. Both now collect results in input order, not completion order. ## What this changes - `_evaluate_dimension` only runs the judge and returns `(dim, result)`. It no longer writes to the conversation from a worker thread. - `run` keeps the futures in `self.judge_agents` order and records each result from the calling thread, in `EVAL_DIMENSIONS` order. The failure path is unchanged: a failed dimension still raises `DimensionEvaluationError`. - The now-unused `tasks` list and the `as_completed` import are removed. ## Verification ``` $ .venv/bin/python -m pytest -q tests/structs/test_council_as_judge.py 2 passed ``` With the source change reverted and the new test kept: ``` $ .venv/bin/python -m pytest -q tests/structs/test_council_as_judge.py E AssertionError: assert ['User', 'ins...s_judge', ...] == ['User', 'acc...s_judge', ...] 1 failed, 1 passed ``` The new test, `test_judgements_are_recorded_in_dimension_order`, stubs `litellm_wrapper.completion` so that each judge sleeps for a time that is longest for the first dimension. It sets `max_workers` to the number of dimensions so the judges really run in parallel, and asserts the order of the conversation roles. It makes no network calls. ## Not in this PR - The aggregator is still given a judge's evaluation as its `task` rather than an explicit instruction. That is now deterministic (always the `instruction_adherence` row), but whether it should get its own instruction turn is a separate design question. - Choosing the aggregator's model is #2769. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `GraphWorkflow.compile()` derives `entry_points` and `end_points` only when they are empty. When it does, it writes the result back to `self.entry_points` and `self.end_points`. On every later compile the lists are no longer empty, so they are treated as user-set and never derived again. `_invalidate_compilation()` runs on every `add_node`/`add_edge`, but it never clears them. So if you grow a graph after its first `run()` or `compile()` (or after `from_spec`, or after `__init__` with `nodes=`, which both auto-compile), it runs with the old entry and end points. Repro on `master`: ```python wf = GraphWorkflow(max_loops=2) wf.add_node(first); wf.add_node(middle); wf.add_edge("First", "Middle") wf.run("task") # end_points -> ["Middle"] wf.add_node(last); wf.add_edge("Middle", "Last") wf.run("task") ``` - `wf.end_points` is still `["Middle"]`, not `["Last"]`. - In loop 2, the entry agent gets `"Middle (previous iteration): ..."`. The output of the real final node `Last` is never carried forward. - `wf.validate()` warns `Found 1 nodes that cannot reach any exit point: {'Last'}` about a graph that is valid. ## What this changes `auto_set_entry_points()` and `auto_set_end_points()` now record the list they produced. `compile()` derives the points again when the current list is still that same object, not only when it is empty. Points set through `set_entry_points`/`set_end_points` or the constructor are different list objects, so they are kept as before. No public signature changes and no new parameters. ## Verification ``` $ PYTHONPATH=$PWD .venv/bin/python -m pytest tests/structs/test_graph_workflow.py -q -k auto_points 1 passed ``` With `swarms/structs/graph_workflow.py` reverted to `origin/master` (test kept): ``` E AssertionError: assert ['Middle'] == ['Last'] 1 failed ``` Full module suite with the fix: ``` $ PYTHONPATH=$PWD .venv/bin/python -m pytest tests/structs/test_graph_workflow.py -q 74 passed, 11 skipped ``` `black --check -l 70` is clean on both files. ## Not in this PR - A topology spec saved with `to_spec()` stores the derived points as explicit lists, so they are still fixed after a reload. That is a serialization question, separate from this fix. - Checkpoint keying (#1539, #2286) and the endpoint-setter refactor (#2213) are separate. This change does not depend on them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong With `planning_enabled=True`, the director that writes the orders never sees the worker roster or the plan. `run_director` makes two calls in a row. Both read the shared conversation through `_context_for(self.director.agent_name)`, and that cursor only hands out messages the agent hasn't been given yet: 1. The planning pass runs first and uses up the director's cursor. That cursor covered the `System` roster and the `User` task. 2. The plan is then saved under `role=self.director.agent_name`. `new_context_for` skips messages from the agent's own name, because it assumes the agent already holds them in memory. But the plan came from a different `Agent` instance (`_get_planning_director()`), so the real director never had it. So the director that issues orders gets `History: (no new messages)` and only the task. It has to pick worker names without the roster, and the planning pass you paid for has no effect on the orders. Repro on master (a stub director that records its task, and a stub planning director): ```python director = Rec("Director", orders_json) planner = Rec("Director", "PLAN: Worker writes the brief") s = HierarchicalSwarm(director=director, agents=[Rec("Worker", "done")], planning_enabled=True, print_on=False) s._planning_director = planner s.run("Write a brief") ``` ``` --- planner saw: History: System: These are the agents in your team. ... | Worker | ... --- director saw: History: (no new messages) Task: Write a brief ``` ## What this changes The planning pass now has its own cursor and role, `"Planning Director"`. That means: - the director's cursor is left alone, so it still gets the roster and the task; - the plan is saved under a role the director doesn't treat as its own, so it shows up in the director's history; - across loops the planning director still skips its own earlier plans. It holds those in its own memory, and that is the same rule every other agent in this swarm follows. It's a two-line change in `HierarchicalSwarm.run_director`. Nothing else reads the planning role. ## Verification ``` $ pytest -q tests/structs/test_hierarchical_swarm.py::test_director_sees_roster_and_plan_when_planning_enabled 1 passed ``` The same test with the source fix reverted (`git stash push swarms/structs/hiearchical_swarm.py`): ``` > assert "| Worker |" in director_tasks[0] E AssertionError: assert '| Worker |' in 'History: (no new messages) \n\n Task: Write a brief' 1 failed ``` The whole module, with `tests/structs/test_hierarchical_swarm.py` and `tests/structs/test_hierarchical_order_parser.py` run together and no API key set: ``` branch: 5 failed, 54 passed master: 5 failed, 53 passed ``` The same 5 tests fail on both. They make live LLM calls (`test_hierarchical_swarm_execution`, `_multiple_loops`, `_collaboration_prompts`, `_real_world_scenario`, `_autosave_saves_conversation_after_run`). ## Not in this PR - `run_director` saves the director's own output under the fixed role `"Director"`, but it reads its cursor by `self.director.agent_name`. A custom director with a different `agent_name` therefore gets its own output sent back to it. That's a separate bug and needs its own change. - The planning director is still built with `agent_name=self.director_name`. Only the conversation role and the cursor key change here. Its name on the `Agent` itself stays as it was. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `BaseTool.execute_function_calls_from_api_response` runs every OpenAI tool call twice when the calls sit under a response-level `tool_calls` key. That is the shape of an assistant message, for example `response.choices[0].message` passed as a model or as its `model_dump()`. `_extract_function_calls_from_response` runs the OpenAI, Anthropic and generic extractors one after another and concatenates what they return. The OpenAI extractor parses each `{"type": "function", "function": {...}}` entry correctly. The Anthropic extractor then walks the same `tool_calls` list, and its `elif "function" in tool_call` fallback matches the same entry a second time. It takes `function.arguments` as-is, so the arguments are still a JSON string. Repro on master: ```python from swarms.tools.base_tool import BaseTool ran = [] def record(name: str) -> str: """Record a call.""" ran.append(name) return name message = {"role": "assistant", "tool_calls": [ {"id": "call_1", "type": "function", "function": {"name": "record", "arguments": '{"name": "1"}'}}, ]} BaseTool(tools=[record]).execute_function_calls_from_api_response(message) print(ran) ``` - Expected: `['1']` - Actual: `['1', '{"name": "1"}']` A one-argument tool runs a second time with the raw JSON string, so its side effects happen twice and a bogus extra result comes back. With a tool that takes two or more arguments, the duplicate fails with `add() missing 1 required positional argument: 'b'`. That failure raises `ToolExecutionError` for the whole batch, and the correct result is thrown away along with it. ## What this changes The Anthropic `function.name` fallback in `_extract_anthropic_function_calls` now skips entries whose `type` is `"function"`. The OpenAI extractor already parses those entries. Entries without that type still go through the fallback, so Anthropic-converted calls carrying `function.input` behave as before. ## Verification ``` $ .venv/bin/python -m pytest tests/tools/test_base_tool.py -q 23 passed, 1 warning in 0.23s ``` With the source fix reverted and the new test kept: ``` $ git stash push swarms/tools/base_tool.py $ .venv/bin/python -m pytest tests/tools/test_base_tool.py -q E assert ['1', '2', '{..."name": "2"}'] == ['1', '2'] E Left contains 2 more items, first extra item: '{"name": "1"}' FAILED tests/tools/test_base_tool.py::test_a_message_dict_runs_each_openai_tool_call_once 1 failed, 22 passed, 1 warning in 0.25s ``` `tests/tools/test_parse_tools.py` has 3 failures, and it fails the same way on master with or without this change. ## Not in this PR - The 3 failures in `tests/tools/test_parse_tools.py`, which already fail on master. - An entry typed `"function"` that carries `function.input` and no `function.arguments` still parses with empty arguments in the OpenAI extractor. No provider response in the tests uses that shape. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What's wrong `Conversation._index_after_first_message` decides where the "all except first" outputs start. It skips one leading `System` row, then the first input. That holds for a plain agent, whose history is `[System, User, ...]`. With `persistent_memory=True`, a resumed agent loads its MEMORY.md as a second `System` row (`_preload_memory_md`), so the history is `[System, System, User, agent, ...]`. The slice then starts on the user's task. `Agent`'s default `output_type` is `str-all-except-first`, so every resumed persistent agent returns the task glued to the front of its answer. `dict-all-except-first` has the same problem. Repro with no model call: ```python from swarms.structs.conversation import Conversation from swarms.utils.history_output_formatter import history_output_formatter open("MEMORY.md", "w").write( "# Agent Memory\n\n### User — earlier\n\nMy project is Helios.\n\n---\n\n" ) c = Conversation(system_prompt="You are helpful.", memory_md_path="MEMORY.md") c.add("User", "What is my project called?") c.add("Analyst", "Helios.") print(repr(history_output_formatter(c, "str-all-except-first"))) ``` On master: `'What is my project called?\nHelios.'`. Expected: `'Helios.'`. ## What this changes `_index_after_first_message` now skips every leading `System` row instead of at most one. Histories with one System row or none (`Agent`, `ConcurrentWorkflow`) get the same index as before. Only the preamble case changes. ## Verification ``` $ .venv/bin/python -m pytest -q tests/structs/test_conversation.py tests/utils/test_history_output_formatter.py 100 passed ``` The new test, `test_all_except_first_skips_the_preamble_and_the_task`, fails on master with the source fix reverted: ``` > assert conv.return_all_except_first_string() == "Helios." E AssertionError: assert 'What is my p...led?\nHelios.' == 'Helios.' 1 failed ``` ## Not in this PR - An agent built with `rules=` has the history `[System, User(rules), User(task), ...]`, so the task still shows up in its output. Rules and the task are both user rows, and telling them apart needs a different signal than role. That's a separate change. - `return_dict_final` returning a tuple is already handled in #2617. - No docs change. The API is unchanged; only the wrong output is fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2424 ## What is wrong With MCP configured, the agent's LiteLLM is built with `mcp_call=True` (`swarms/agents/llm_manager.py`), and in that mode `output_for_tools` rebuilds every tool call as `{"function": {"name": ..., "arguments": ...}}`, dropping the call's `id` and `type`. Two silent failures follow: 1. `BaseTool.execute_function_calls_from_api_response` does not recognise a call without `id`/`type` and returns `[]` — an `Agent(tools=[...], mcp_url=...)` can call its MCP tools but none of its own local tools, even when the local tool is the only call in the turn. 2. Tool results are stored under the call's `id`. With the id gone, the transcript answers the call with `(no result recorded for <tool>)` instead of the result, so with `dynamic_tools=True` the model keeps searching for a tool that is already loaded. ## What this changes Both `mcp_call` shapes in `output_for_tools` — the single-call dict and the multiple-call list — now carry the call's `id` and `type` through the rebuild. Nothing else moves; the standard (non-MCP) branch already returns the full tool calls. ## Verification `TestMcpToolCallIds` in `tests/utils/test_litellm_wrapper.py` builds a `LiteLLM(mcp_call=True)`, feeds it a response with one tool call, and asserts the rebuilt call keeps `id` and `type` **and** that `BaseTool.execute_function_calls_from_api_response` now actually runs the tool (the issue's offline repro: `[]` on master, the result on this branch). The test fails on master and passes here; the file's other 17 failures are live-model tests, identical on master.
Part of #2714. ## What is wrong `PlannerWorkerSwarm` lives in `swarms/structs/planner_worker_swarm.py` but is missing from `swarms/structs/__init__.py`, so the documented import raises: ```pycon >>> from swarms import PlannerWorkerSwarm ImportError: cannot import name 'PlannerWorkerSwarm' from 'swarms' ``` CLAUDE.md both shows `from swarms import PlannerWorkerSwarm` in its example and states the rule "always import from `swarms`" — the package currently fails its own rule for this class. ## What this changes Adds `PlannerWorkerSwarm` to the `swarms.structs` imports and `__all__` (star-exported by `swarms`). Two lines; the submodule import keeps working. The remaining bullets of #2714 are covered elsewhere: the four example fixes are in #2464 and #2430, and I left the CI doc-block runner out on purpose — every CLAUDE.md example it would run is broken on master until those PRs merge, so the workflow would sit permanently red. It should land alongside or after them. ## Verification `from swarms import PlannerWorkerSwarm` resolves to `swarms.structs.planner_worker_swarm.PlannerWorkerSwarm`. No test added: a mechanical export with no logic.
Closes #2710 ## What is wrong `run_stream` and `arun_stream` yield bare strings, so `[thinking]` text, `[tool call]` text and content are indistinguishable except by parsing those markers — even though `LLMManager.stream_parts` already labels every piece as `reasoning`, `content` or `tool_call` internally. The value `run()` returns is also discarded, so a caller who wants the formatted output (`output_type`) has to run the task a second time. ## What this changes - `StreamEvent(kind, text)` dataclass, exported from `swarms` (`swarms.structs.__all__`). - `run_stream(task, events=True)` and `arun_stream(..., events=True)` yield `StreamEvent`s whose `kind` is `reasoning`, `content`, `tool_call` — the labels `stream_parts` already produces — and end with exactly one `final` event whose `text` is `run()`'s return value. - Event text is the raw fragment: `stream_parts` gained `display=True` and skips the `[thinking]`/`[tool call]` headers, section gaps and separators when it is False, so joining `kind == "content"` events reproduces the reply text. Text consumers are untouched — `_collect_stream` and `_consume_detailed` only pass `display=False` to callbacks that opted in via a `wants_kinds` attribute, which only `events=True` sets. - `events=False` (the default) keeps today's byte-identical string stream, including the markers; no rendered output changes, so no screenshots. Side calls (the tool summary) already stream inline into the string stream today; with `events=True` they arrive as `content` events too. Read the reply from the `final` event, per the docstring. ## Acceptance against #2710 - A stubbed streaming tool turn (`reasoning_content` + `content` + `tool_calls` chunks) yields all three kinds and exactly one `final`: `TestRunStreamEvents` in `tests/structs/test_agent.py`. - The `final` payload equals `run()`'s return for the same stubbed responses — the test runs a twin agent with the same stub and asserts equality, and asserts the content join equals the reply. - The test fails on master (`events` kwarg does not exist) and passes on the branch. `tests/structs/test_agent.py` and `tests/agents/test_llm_manager.py` show the same 29 + 1 live-model failures as master, nothing new. ## Docs The user-facing docs change is tracked in The-Swarm-Corporation/swarms-framework-docs#318 (a docs PR from a fork is blocked by the repo's fork-collab setting, so it is an issue with the drafted section instead).
Part of #2637 (the `{current_loop}` placeholder half is already fixed in #2530, which rewrites `generate_reasoning_prompt`; this PR takes the tool panel half only). ## What is wrong `ToolManager` prints each function-call argument through `_clip(value, 200)` and each result through `_clip(result, 500)` (`swarms/agents/tool_manager.py`, `visualize_function_call` and `visualize_handoff_call`). When a value is longer than the limit the panel shows it ending in a bare `...`, with no sign that anything was removed. The full value is what gets passed to the tool and stored in memory — only the display is cut — but the panel reads as if the agent sent a truncated argument, which is what #2637 reports. ## What this changes `_clip` keeps the same limits and appends the cut size: a 250-character argument now renders as `<first 200 chars>... (+50 more chars)`. Nothing else moves — the same three call sites (200, 500 and 150 chars) keep their limits, and values within the limit are untouched. | before | after | |---|---| |  |  | ## Verification - `_clip("a" * 250, 200)` ends with `... (+50 more chars)`; short values return unchanged. - No test added: `_clip` is a small display helper and its behavior is shown above; the panel shots come from running `visualize_function_call` on this branch vs master. - `black`/`ruff` introduce no new findings on the touched file.
## What is wrong The issue measures ~170 of the 247 failures in the latest `Run Tests and Python package` master run (`8ec7948ce`) as tests that call a real LLM with no key: ``` AgentLLMError: Agent '...' got no response from 'gpt-5.4' after 3 attempt(s): litellm.InternalServerError: ... Missing credentials. ... set the `OPENAI_API_KEY` ... ``` `Test Main Features` fails the same way. These network tests sit in the unit suite and bury the real failures. ## What this changes `tests/conftest.py` reclassifies a failure or setup error as a skip only when none of `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY` or `OPENROUTER_API_KEY` is set and the exception chain carries a provider-credential signature (`Missing credentials`, `AuthenticationError`, incorrect/invalid API key). Nothing is skipped up front, so tests that can run without a key still run, and non-credential failures stay red. A run that has a key, such as `Test Main Features` with its repo secrets, never skips: an expired secret or a broken key hand-off still fails the job. Full local run of the suite without `OPENAI_API_KEY`, before and after this file: | | failed | passed | skipped | |---|---|---|---| | before | 249 | 2135 | 23 | | after | 77 | 2135 | 195 | 172 credential failures became skips; the pass count is unchanged; the 77 remaining failures are the real ones (e.g. `AutoSwarmBuilder` rejecting its documented arguments, #2726). How the live tests should run at all (provider keys as repo secrets with a nightly or manual workflow, or making `Test Main Features` `workflow_dispatch`-only) is a maintainer call, so this PR does not touch the workflows. Closes #2780 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What is wrong `GroupChat` has no public hook to inspect, rewrite or block a reply before it is posted. The only way to add a guardrail today is to subclass, override the internal `_select_speaker`, and filter the winning bid — which ties user code to a private method's signature and return shape. ## What this changes Two optional constructor arguments, both called with `(sender_name, reply)`: - `before_post` — runs after a speaker wins the floor and before `_post`. Return the reply, edited or not, to post it; return `None` to drop it, and the floor passes to the next-best bid that clears the threshold (or the turn counts as a lull, same as today's no-winner turn). - `after_post` — runs after the reply is posted, for logging and moderation. Both default to `None`, so existing behaviour is unchanged. Tests cover edit-in-place, drop-and-pass-to-next-best, and `after_post` observing each posted reply. Closes #2415
## Summary Remove litellm from swarms completely and make RouteHub the only model-call backend, installed by default. ## Current state (master) - `swarms/utils/llm_backend.py` picks the library at import time. RouteHub is used when it's installed, otherwise litellm, and `SWARMS_LLM_BACKEND` can force either one. - litellm is a **required** dependency, pinned to `1.76.1` in `pyproject.toml:69` and `requirements.txt:12`. RouteHub is **optional**, installed only by the `swarms[fast]` extra (`pyproject.toml:77,83`). - Nothing in `swarms/` imports litellm directly. Every call goes through `llm_backend`, so the removal is mostly dependency changes, naming and cleanup, not a rewrite of call sites. - The package uses these backend names, which RouteHub must provide: `completion`, `acompletion`, `embedding`, `model_list`, `get_model_info`, `get_max_tokens`, `encode`, `supports_vision`, `supports_function_calling`, `supports_parallel_function_calling`, `supports_reasoning`, and the provider exception classes that are read through the module's `__getattr__`. - `litellm` is mentioned in 30 files under `swarms/`, 25 under `tests/` and 60 under `examples/`. It also appears in `README.md`, `SKILL.md`, `CLAUDE.md` and `.env.example`. ## Proposed changes - [ ] **Dependencies:** remove `litellm` from `pyproject.toml` and `requirements.txt`. Make `routehub` (with its `fast` extra) a required dependency, and drop it from the `fast` extra. - [ ] **`swarms/utils/llm_backend.py`:** always use RouteHub. Remove the litellm branch, the `BACKENDS` tuple and its install hints, and the `SWARMS_LLM_BACKEND` switch. Decide whether a leftover `SWARMS_LLM_BACKEND=litellm` warns or raises. - [ ] **`swarms/utils/litellm_wrapper.py`:** rename the module and its `LiteLLM` class to a backend-neutral name, and keep a deprecated alias for one release. Remove litellm-specific workarounds that no longer apply. - [ ] **Package code:** remove the remaining litellm references in `swarms/`, including the CLI (`cli/main.py`, `cli/models.py`, `cli/tips.py`, `cli/utils.py`), prompts and comments. - [ ] **Tests:** update the 25 test files that patch or name litellm. Many tests monkeypatch `litellm_wrapper.completion`, so they should move to the new module path. - [ ] **Examples and docs:** update the 60 example files and the README, `SKILL.md`, `CLAUDE.md` and `.env.example` text that says "Any LiteLLM model string". Document the model-string format RouteHub accepts. - [ ] **swarms-framework-docs:** remove `SWARMS_LLM_BACKEND`, update the installation and model-name pages, and add a changelog entry marking this as a breaking change. ## Open litellm issues this would close or make obsolete #2391 (bump litellm), #2126 (litellm Pydantic serializer warnings), #2410 (LiteLLM request issues with Claude models), #2359, #2424, #1754 (litellm import time). Each one should be re-checked against RouteHub before closing. ## Risks to check first - **Model coverage:** does every model string that works today (OpenAI, Anthropic, Groq, Gemini, OpenRouter, Azure and others) work through RouteHub with the same name? - **API parity:** every name in the list above must exist in RouteHub with the same signature and return shape. That includes the streaming chunk format, tool-call fields and the usage object. - **Platform support:** RouteHub must install everywhere swarms supports, across Python 3.10–3.12, macOS, Linux and Windows. It's the only backend once litellm is gone. ## Acceptance criteria - `pip install swarms` installs RouteHub and not litellm. - `git grep -i litellm` returns nothing in `swarms/`, `tests/`, `examples/` or the top-level docs, except a changelog or migration note. - The scoped tests for the touched modules pass with no new failures against the master baseline. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Problem About 170 of the 247 failures in Run Tests and Python package (latest master run, `8ec7948ce`) are tests that call a real LLM with no key: ``` AgentLLMError: Agent '...' got no response from 'gpt-5.4' after 3 attempt(s): litellm.InternalServerError: ... OpenAIException - Missing credentials. ... set the `OPENAI_API_KEY` ... ``` Test Main Features fails the same way: every test in `tests/test_main_features.py` calls a live provider. The workflow reads `secrets.OPENAI_API_KEY`, but the key is missing at run time, even on master pushes. These network tests sit in the unit suite and bury the real failures. ## Fix 1. Skip LLM-calling tests when no API key is set. Either: - a `conftest.py` that skips them when `OPENAI_API_KEY` is unset, or - a marker like the existing `remote` one, deselected in CI. 2. Decide how the live tests run at all. Either: - add the provider keys as repo secrets and run them in a separate nightly or manual workflow, or - make Test Main Features `workflow_dispatch`-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## Summary Fixes #2717. ### Problem In `swarms/utils/loguru_logger.py`, the file sinks configured in `initialize_logger` (the daily rotating file sink `swarms_YYYY-MM-DD.log` and the `_module_log_router` sink) as well as the console sink used `diagnose=True` (or loguru's default `diagnose=True`). On any logged exception, loguru then inspects and formats the values of local variables in each traceback frame. In Swarms, these frames frequently hold request parameters, authentication headers, and API keys (`sk-...`), resulting in plaintext secrets written to `{WORKSPACE_DIR}/logs` files that persist for 10 days. ### Solution 1. **Disable `diagnose` on File Sinks:** Set `diagnose=False` on the rotating daily log file handler (`swarms_{time:YYYY-MM-DD}.log`) and on `_module_log_router`. 2. **Preserve `backtrace=True`:** Keep `backtrace=True` so complete call stacks, file paths, line numbers, and error types remain visible for debugging without dumping local frame variables. 3. **Condition Console Sink on `SWARMS_VERBOSE_GLOBAL`:** Console logging now uses `diagnose=verbose_global`, enabling variable introspection only when `SWARMS_VERBOSE_GLOBAL` is explicitly set to a truthy value (`"true"`, `"1"`, `"yes"`, `"on"`, `"t"`). ### Tests Added Added comprehensive unit tests in `tests/utils/test_loguru_logger.py`: - `test_file_sinks_have_diagnose_disabled`: Verifies file sinks always have `diagnose=False` and `backtrace=True`, even when verbose mode is active. - `test_console_diagnose_respects_verbose_global`: Tests truthy (`true`, `1`, `yes`, `on`, `t`) and falsy (`false`, `0`, `no`, `off`, `""`, `None`) values for `SWARMS_VERBOSE_GLOBAL`. - `test_exception_in_frame_does_not_leak_secret_to_log_files`: End-to-end verification that an exception raised in a frame holding `api_key="sk-test-super-secret-12345"` does not write the secret into either `swarms_*.log` or per-module `*.log`, while preserving full traceback and exception details. All 29 tests in `tests/utils/test_loguru_logger.py` pass.
## Problem `AutoSwarmBuilder` documents two constructor arguments that were not accepted by `__init__` (`swarms/structs/auto_swarm_builder.py:273-276`): - `max_agents` (int, optional): upper bound on how many agents the boss may generate. - `max_json_repair_attempts` (int): repair retries for malformed boss output, default 1. Previously, `__init__` hard-coded `self.max_agents = None` and `self.max_json_repair_attempts = 1`, causing: ``` TypeError: AutoSwarmBuilder.__init__() got an unexpected keyword argument 'max_json_repair_attempts' ``` when passing either setting to the constructor. Fixes #2726 ## What this changes - **`swarms/structs/auto_swarm_builder.py`**: - Added `max_agents: Optional[int] = None` and `max_json_repair_attempts: int = 1` to `AutoSwarmBuilder.__init__`. - Assigned `self.max_agents = max_agents` and `self.max_json_repair_attempts = max_json_repair_attempts` before `self.reliability_check()`. - **`tests/structs/test_auto_swarms_builder.py`**: - Added `test_constructor_accepts_max_agents_and_max_json_repair_attempts` validating both custom values and defaults. - Resolved `TypeError` across all 13 unit tests passing these parameters. ## Verification & Edge Case Analysis - **Custom instantiation:** `AutoSwarmBuilder(max_agents=3, max_json_repair_attempts=2)` sets attributes correctly. - **Default values:** `AutoSwarmBuilder()` retains `max_agents=None` (unbounded) and `max_json_repair_attempts=1`. - **Reliability check edge cases:** - `max_agents=0` and `max_agents=-1` raise `ValueError("max_agents must be greater than 0")`. - `max_json_repair_attempts=-1` raises `ValueError("max_json_repair_attempts must be greater than or equal to 0")`. - `max_json_repair_attempts=0` allows 0 repair attempts without raising. - **Agent count capping:** `_enforce_max_agents` correctly triggers `ValueError` when boss output exceeds `max_agents`. - **Unit test results:** All 32 non-network tests in `tests/structs/test_auto_swarms_builder.py` pass (`32 passed in 11.95s`).
`Agent.run_concurrent_tasks` and `Agent.talk_to_multiple_agents` size their thread pool with `max_workers=os.cpu_count()`. Every task they submit is a model call that spends almost all of its time waiting on the network, so the core count is the wrong limit: on a 4-core machine, 20 tasks run four at a time and take five rounds instead of one. ### Evidence 20 items against a stub whose `run` / `talk_to` sleeps 0.3 s, on master (`cdfa0883`) and on this branch. The 4-core rows patch `os.cpu_count` to return 4, as in the issue. | call | `os.cpu_count()` | master | this branch | |---|---|---|---| | `run_concurrent_tasks`, 20 tasks | 10 | 0.61 s | 0.31 s | | `talk_to_multiple_agents`, 20 agents | 10 | 0.62 s | 0.30 s | | `run_concurrent_tasks`, 20 tasks | 4 | 1.53 s | 0.31 s | | `talk_to_multiple_agents`, 20 agents | 4 | 1.52 s | 0.31 s | | either, empty list | 10 | `[]` | `[]` | <details><summary>Script</summary> ```python import os, time from types import SimpleNamespace from swarms.structs.agent import Agent def stub(task=None, *a, **k): time.sleep(0.3) return task fake = SimpleNamespace(run=stub, talk_to=lambda agent, task, *a, **k: stub(task)) tasks = [f"t{i}" for i in range(20)] os.cpu_count = lambda: 4 t = time.perf_counter(); Agent.run_concurrent_tasks(fake, tasks); print(time.perf_counter() - t) t = time.perf_counter(); Agent.talk_to_multiple_agents(fake, list(range(20)), "hi"); print(time.perf_counter() - t) ``` </details> ### Change Both pools are now sized at `max(1, min(len(items), MAX_CONCURRENT_AGENTS))`, the rule `ConcurrentWorkflow._resolve_max_workers` already applies. A pool never receives more work than it has items, and the existing cap of 32 from #1766 keeps a large batch from opening dozens of simultaneous provider connections. The floor of 1 keeps an empty list returning `[]`, because `ThreadPoolExecutor` rejects `max_workers=0`. `MAX_CONCURRENT_AGENTS` was defined in `concurrent_workflow.py`, which imports `Agent`, so `agent.py` could not import it from there without a cycle. It moves to `execution_utils.py`, which imports only `swarms.telemetry.otel`, and `concurrent_workflow.py` imports it back, so the existing `from swarms.structs.concurrent_workflow import MAX_CONCURRENT_AGENTS` in `groupchat.py` keeps working. The one-line comment over the old definition (the cap guards rate limits, not CPU) is dropped with it; that reasoning is the paragraph above. The issue also offered a configurable parameter. I reused the shared constant instead, so neither method's signature changes and no docs page needs updating. ### Verification - The script above, before and after. - `tests/structs/test_concurrent_workflow.py` 15 passed, `test_groupchat.py` 42 passed, `test_execution_utils.py` 43 passed. - `tests/structs/test_agent.py -k "concurrent or talk_to"`: 6 passed, 1 failed. The failure is `test_agent_concurrent_execution`, which calls a live model and fails identically on master without `OPENAI_API_KEY`. - `black . --check` clean. No test added: the change is the pool size in two helper methods, and the timing above is the check. A wall-clock assertion would be flaky under CI load. Closes #2713 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2709 ## What's wrong A started run can't be stopped. `run_stream` runs `run()` on a daemon thread and only joins it once the token queue drains. When the consumer stops iterating (`break` followed by close, an exception, or a server whose client disconnected), the generator closes at `yield` and the thread keeps going. It calls the model and the tools until the task finishes, paying for output nobody reads. `arun_stream` has the same problem, and there is no `Agent.cancel()` for a plain `run()`. On master, with a stubbed `call_llm` and `max_loops=3`, closing the stream after the first token still produces three model calls (`[1, 2, 3]`). ## What this changes All in `swarms/structs/agent.py`, as the issue proposes: - Each run gets a `threading.Event`. `_run` reads it once at the start and checks it before every model call (the outer loop and the retry loop) and before running the tool calls a reply asked for. When it is set, the loop exits right after the model call and the run returns its history the normal way. It does not raise `AgentLLMError`, so a cancelled run never moves on to the fallback models, and interactive input and `loop_interval` are skipped. A skipped tool call keeps the existing "(no result recorded ...)" placeholder, so the conversation stays well formed. - `Agent.cancel()` sets the event for the run in progress. - `run_stream` and `arun_stream` create the run's event before starting the thread, and set it in a `try/finally` around the consumer loop, so closing either generator cancels the run. - `run()` swaps in a fresh event when the current one is already set. A cancelled or finished stream therefore doesn't stop the agent's next run, and a `cancel()` made while nothing is running is ignored. The event lives on a private attribute, which `SafeStateManager` already skips, so save and load are unaffected. ## Verification - New test in `tests/structs/test_agent.py`: it closes `run_stream` after the first token and asserts the run made one model call. It fails on master's `agent.py` (`assert [1, 2, 3] == [1]`) and passes here. - `tests/structs/test_agent.py` and `tests/structs/test_conversation.py` offline: 29 failed and 191 passed on the branch. The same 29 fail on master because they need a real model. - One-off script, not committed: closing `arun_stream` after the first token gave one model call. With `retry_attempts=3` and a fallback model set, a `cancel()` during a model call that then raised gave one call, no retries and no fallback, and `run()` returned normally. The agent's next `run()` then ran all 3 loops. - black and ruff are clean, and the diff adds no comments. ## Not in this PR - `max_loops="auto"` runs go through `_run_autonomous_loop`, which this doesn't touch. My open #2279 adds cancellation there. - Cancelling between two tool calls of the same reply would mean threading the event into `ToolManager`. Here the check sits before the reply's tools start. ## Docs `api/agent.mdx` in swarms-framework-docs could gain this under the streaming section: > Call `agent.cancel()` from another thread to stop a running `run()` before its next model or tool call; the run returns the history it has so far. Closing a `run_stream` or `arun_stream` generator (for example when an HTTP client disconnects) cancels the run the same way, so it stops calling the model. `max_loops="auto"` runs are not covered yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What is wrong `CouncilAsAJudge` picks two models in two different ways: - The six dimension judges use `judge_agent_model_name or model_name`. - The aggregator uses `aggregation_model_name`, which defaults to the fixed string `"gpt-5.4"`. So `CouncilAsAJudge(model_name="claude-sonnet-4-6")` runs the judges on Claude and then sends the final report to OpenAI. `SwarmRouter(swarm_type="CouncilAsAJudge", council_judge_model_name=...)` has the same split, because the router passes that value as `model_name` and has no way to set the aggregator. Someone with only an Anthropic key gets an OpenAI credentials error on the last call, after all six judges have already run. ## Evidence The new test records the `model` of every completion request for `CouncilAsAJudge(model_name="claude-sonnet-4-6").run(...)`: - **master:** 7 requests, the six judges on `claude-sonnet-4-6` and the aggregator on `gpt-5.4` (`Extra items in the left set: 'gpt-5.4'`). - **this PR:** all 7 on `claude-sonnet-4-6`. ## What this changes - `aggregation_model_name` defaults to `None`, and the aggregator uses `aggregation_model_name or model_name`, the same fallback the judges already use. - An explicit `aggregation_model_name` still wins. - The default behaviour is unchanged, since `model_name` itself defaults to `"gpt-5.4"`. - The `SwarmRouter` docstring for `council_judge_model_name` now says it covers the aggregator too. The test reuses the existing file's fake `completion`, with the reply builder pulled into a `_reply()` helper so both tests share it. `tests/structs/test_council_as_judge.py` passes on its own (2 passed). In `test_swarm_router.py`, `test_run_with_council_as_judge` and `test_run_with_llm_council` fail with and without this change, because they need a live OpenAI key. ## Docs `swarms-framework-docs` needs these edits. I can't open a PR there. - `api/council-as-judge.mdx`, `aggregation_model_name`: change `default="gpt-5.4"` to `default="None"`, and the text to: "Model name for the aggregator agent that writes the final report. Defaults to `model_name`, like the judges." - `api/swarm-router.mdx`, `council_judge_model_name`: replace the text with: "Passed to `CouncilAsAJudge` as `model_name`, so it sets the model for every dimension judge and for the aggregator." - `architectures/swarm-router.mdx`, `council_judge_model_name`: the same sentence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Part of #2735 ## What was wrong `Agent` printed a Rich panel for every response by default (`print_on: bool = True`, `agent.py:337`). Library and server code that embeds an agent had to remember `print_on=False`, or its stdout filled with agent output. On master, a plain `Agent(...).run(task)` with a stub LLM writes 243 bytes of panel to stdout. ## What this changes `print_on` now defaults to `None` and is resolved once in `__init__`: | caller passes | resolved `print_on` | |---|---| | nothing | `False` | | `interactive=True` | `True` | | `streaming_on=True` | `True` | | `print_on=True` or `print_on=False` | that value, always | `interactive=True` is the case the issue names, because a human is watching. `streaming_on=True` goes in the same group. It is documented as "stream tokens to stdout", and `LLMManager._consume_panel` drains the stream silently when `print_on` is False. Without this, a `streaming_on=True` agent would stream nowhere. An explicit `print_on` always wins. That is why the default is a `None` sentinel and not `print_on or interactive`: `interactive=True, print_on=False` still means off, as it does on master. Three CLI paths relied on the panels to show a result, because they drop `run()`'s return value or only print a summary. Each one now shows the result itself: - `swarms chat` (`auto_chat_agent`) passes `print_on=True`. It is a human-facing loop that never prints what `run()` returns. - `swarms autoswarm` (`run_autoswarm`) prints the swarm's result. - `swarms run-agents` (`handle_run_agents`) prints a non-str, non-dict result, such as the list a concurrent swarm returns. Before, it printed only "Agents completed their tasks successfully!". Both CLI prints use `markup=False`, so LLM text containing `[x]`, `List[str]` or `[/INST]` prints as written. Without it, Rich would parse that text as markup and either drop it or raise `MarkupError`. `swarms agent`, `heavy-swarm`, `llm-council`, and `run-agents` with str or dict results already print the result themselves, so they are unchanged. ## Before / after Same script on both commits: a stub LLM and a default `Agent(agent_name="Analyst", llm=StubLLM(), max_loops=1)`, then `print(agent.run(...))`. | before (origin/master) | after (this branch) | |---|---| |  |  | ## Verification Repro with a stub LLM, stdout captured: ``` master default print_on=True out='raw answer' stdout_bytes=243 branch default print_on=False out='raw answer' stdout_bytes=0 branch print_on=True print_on=True out='raw answer' stdout_bytes=243 branch interactive=True print_on=True branch interactive=True, print_on=False print_on=False ``` `streaming_on=True` with a streaming stub LLM shows the same output on master and on this branch. Tokens reach stdout with no `print_on` set, and nothing reaches stdout with `print_on=False`. `tests/structs/test_agent.py::TestPrintOnDefault::test_console_output_is_opt_in`: ``` with master's agent.py, auto_chat_agent.py and cli/main.py: 1 failed (panel captured on stdout) with this branch: 1 passed ``` The whole of `tests/structs/test_agent.py` fails the same 29 tests on master and on this branch. Those are pre-existing failures, and this change adds none. `tests/structs/test_hierarchical_swarm.py` and `tests/utils/test_formatter.py` give the same results as on master. `black --check .` is clean. ## Not done - The issue's third acceptance item is still open: examples under `examples/` that depend on the panels should set `print_on=True`. About 38 example files call `.run()` without printing the result. That sweep is left for a follow-up so this diff stays reviewable, which is why this PR says `Part of #2735` rather than closing it. - The `print_on` row in the repo-root `SKILL.md` still says `bool`, default `True`. It is not edited here, to keep this PR to four files. - Docs: The-Swarm-Corporation/swarms-framework-docs#317 tracks `api/agent.mdx` and `agents/agent-configuration.mdx`, which both show `print_on` with `default="True"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Part of #2736 This adds option 2 from the issue, top-k bidders. Options 1 (`bid_model_name`) and 3 (selector mode) are not in this PR. ## What is wrong today `GroupChat._collect_bids` asks every agent for a `respond(score, message)` bid on every turn, each on its own model with the full conversation. A chat with n agents and m messages costs n × m model calls. The issue measured 32 calls for 4 agents and 8 messages, against 8 for `RoundRobinSwarm`. ## What this changes `GroupChat(max_bidders=k)` asks only k agents per turn: - Agents are ranked by their most recent bid score. Agents that spoke within `recency_window` lose `recency_penalty`, the same adjustment `_select_speaker` applies. Agents that have not bid yet rank first, and ties keep roster order. - If none of the k clears `threshold`, the rest of the agents bid before the turn counts as a lull. So the chat still ends only when every agent has passed, and `max_bidders` never cuts a chat short. - Every agent asked in either round records its score for the next turn's ranking. - `max_bidders=None` (the default) behaves exactly as before: every agent bids every turn, in roster order, in one round. Why rank by bid score: two simpler orderings failed review on this branch. Ranking by "least recently spoke" let an agent that always passes, such as a moderator, hold a slot every turn, so every turn fell back to asking everyone. Rotating the slots let two passing agents trade the slot, with the same result. The bid score is what the chat already picks speakers on. An agent that passed moves to the back, and it gets asked again when the top k all pass. ## Verification Scripted agents (no model), 8 messages after the user task: | roster | master | `max_bidders=2` | `max_bidders=1` | |---|---|---|---| | 4 agents bidding 0.9 / 0.8 / 0.7 / 0.6 | 32 calls | 16 | 8 | | an always-passing moderator plus 3 agents | 32 calls | 16 | 11 | ``` master default | messages: 8 | bid calls: 32 | speakers: ABABABAB TypeError: GroupChat.__init__() got an unexpected keyword argument 'max_bidders' this branch, 4 bidding agents default | messages: 8 | bid calls: 32 | speakers: ABABABAB {'max_bidders': 2} | messages: 8 | bid calls: 16 | speakers: ACABABAB {'max_bidders': 1} | messages: 8 | bid calls: 8 | speakers: ABCDABAB this branch, always-passing moderator plus A, B, C default | messages: 8 | bid calls: 32 | moderator calls: 8 | speakers: ABABABAB {'max_bidders': 2} | messages: 8 | bid calls: 16 | moderator calls: 1 | speakers: ABABABAB {'max_bidders': 1} | messages: 8 | bid calls: 11 | moderator calls: 1 | speakers: ABABABAB ``` With the moderator roster, the same agents speak in the same order as on master, at a third of the calls. Tests: two added to the existing `tests/structs/test_groupchat.py`. `test_max_bidders_skips_an_agent_that_passed` checks that a silent agent is asked once and then skipped. `test_max_bidders_asks_the_rest_before_a_lull` checks that the fallback round lets a speaker outside the top k still take the floor. ``` master (source reverted, tests kept): 2 failed, 42 deselected this branch: 44 passed ``` `test_max_bidders_skips_an_agent_that_passed` also fails on this branch's first commit (the last-spoke ranking), so it pins the fix in the second commit. `black . --check` (line length 70) and `ruff check` pass. The no-mistakes gate (review, test, document, lint) passed. ## Limitations - A bid score goes stale until that agent is asked again. An agent that passed once sits out until the top k all pass, which can last a while in a lively chat. - A fallback turn runs two rounds of bids one after the other, so it takes about twice as long as one turn on master. - The counts above come from scripted agents. I did not record `agent.usage` on a live model, so the token reduction from the issue's acceptance list is not shown here. - The new parameter is not in the `__init__` docstring. The docs update is tracked in The-Swarm-Corporation/swarms-framework-docs#316. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2737 ## What is wrong An `Agent` keeps one conversation across `run()` calls, so reusing an agent for independent tasks sends every earlier task and answer with each new request. There is no per-call way out: `agent.run(task, fresh=True)` is accepted today, but `fresh` lands in `**kwargs` and changes nothing. Reproduced on master (b091c1c0) with an `llm=` stub that records the conversation size at each request: ``` default run : [2, 4, 6] fresh run : [8, 10] stream + arun : [12, 14, 16] default after : [18] stored turns: 19 ``` ## What this changes `Agent.run` takes a keyword-only `fresh: bool = False`. With `fresh=True` it sets the agent's conversation aside, calls `reset()` to build the conversation a newly constructed agent starts with (system prompt, seeded `messages`, the MEMORY.md preload when `persistent_memory` is on, `tool_schema` rows), runs the task on that, and puts the stored conversation back in a `finally`, so it comes back even when the run raises. - `usage` is untouched, because `reset()` keeps it. - `fresh` is declared after `*args`, so no positional argument of `run` moves. - `run_stream`, `arun_stream` and `arun` already forward `**kwargs` to `run`, so `fresh=True` works through all of them without touching them. - It calls `reset()` instead of repeating its body, so a fresh run can't drift from what `reset()` and `__init__` build. The issue's agent-level `history="per_run"` setting is not added. The per-call flag covers the need, and a setting can wrap it later if one is wanted. ## Verification Same script on this branch: ``` default run : [2, 4, 6] fresh run : [2, 2] stream + arun : [2, 2, 2] default after : [8] stored turns: 9 ``` Two fresh runs send the same request, and the next default run continues from the stored conversation as if the fresh runs never happened. `test_fresh_run_sends_only_its_own_task_and_keeps_the_history` in `tests/structs/test_agent.py` records the messages each `call_llm` receives across `run`, `run_stream(fresh=True)`, `arun(fresh=True)` and `run`. With `agent.py` reverted to master it fails with `assert [1, 3, 5, 7, 9] == [1, 1, 1, 1, 3]`; it passes here. The rest of `test_agent.py` fails the same 29 tests as master (live-provider tests that need an API key) and passes the other 106. The pre-PR validation pipeline also ran the real `Agent` through LiteLLM against a local OpenAI-compatible server: `run`, `run_stream` and `arun` with `fresh=True` each sent a 2-message request holding only their own task, default runs matched master's requests byte for byte, and a fresh run that raised still restored the stored history. ## Limitations - `fresh=True` swaps the agent's conversation for the length of the call, so it is not safe on one agent from several threads at once. Two overlapping fresh calls (`asyncio.gather` over `arun`, or `run_concurrent_tasks(..., fresh=True)`) can leave the agent on a fresh conversation instead of the stored one, and breaking out of a `run_stream` loop early leaves the background run to swap back later. Concurrent isolated runs are what `Agent.clone` in #2753 is for. - With `persistent_memory=True`, every fresh call rebuilds the conversation the way `reset()` does, which appends the seeded `messages` and any tool-schema rows to MEMORY.md again. Constructing a new `Agent` already does this (one seed row per construction, checked); `fresh=True` makes it happen once per call. - The MEMORY.md preload is a `System` row, which the request builder skips, so a fresh run does not send it to the model. A newly built agent behaves the same way. Docs: The-Swarm-Corporation/swarms-framework-docs#315 tracks the `fresh` parameter on the Agent page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2712 ## What's wrong `ContextCompressor.should_compress` runs on every loop of an autonomous run. To skip tokenizing when the history is clearly under budget, it called `_history_bytes`, which re-formatted every message as `"[timestamp] role: content\n\n"` and UTF-8 encoded it. That is O(n) per loop in history length, so O(n²) across a long run, just to read a number that only changes by one message at a time. ## What this changes - `Conversation` keeps the total in `_history_byte_total`. `add_in_memory`, the path every `add` takes, adds the new message's size. Every other place in `Conversation` that changes `conversation_history` (`delete`, `update`, `clear`, `compact`, `batch_add`, the `load_conversation` paths, `truncate_memory_with_tokenizer`) resets it next to the existing `_str_cache = None`, so the next read recomputes once. These are the same sites that already invalidate the history string cache. - `history_bytes()` returns the total. `_message_bytes` is the per-message size formula moved over from `ContextCompressor._history_bytes`, unchanged. - `should_compress` calls `history_bytes()` when `short_memory` is a `Conversation`, and otherwise falls through to the exact token count as before. `_history_bytes` is deleted. On the hot path (the agent adds a message, then checks), no history is walked. A delete, update or compact costs one recompute on the next check. ## Verification - New `tests/structs/test_conversation.py::test_history_bytes_is_a_running_total`: after a read, one `add` measures exactly one message, and the total equals a fresh recompute after `delete`, `update`, `compact` and a following `add`. It fails on `master`. It also fails if the `update` reset is removed. - `tests/agents/test_context_compressor.py`: the `TestShouldCompressSkipsTokenizing` stub is now `MagicMock(spec=Conversation)` with `history_bytes` set, and the byte-bound test reads `history_bytes()`. These tests fail with `master`'s source and pass here. - `tests/agents`, `tests/structs/test_conversation.py` and `tests/structs/test_agent.py` offline: 32 failed and 590 passed on the branch, and the same 32 fail on `master`. All of them need a model API key or are the existing `AgentError` import test. black, ruff and the added-comment grep are clean. ## Not in this PR Code outside `Conversation` that mutates `conversation_history` directly after a `history_bytes()` read would leave the total stale. Nothing in `swarms/` does that to an agent's `short_memory` today. `autonomous_loop` extends a fresh `Conversation` before any read, and `social_algorithms` clears a list that never reaches the compressor. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What is wrong `get_parameter_json_schema` copies each parameter's Python default straight into the tool schema: ```python schema["default"] = dv ``` When the default is not a JSON value, such as an `Enum` member or a `Path`, the schema holds the raw object. Every request that carries the agent's tools then fails while it is serialized, before it reaches the provider. ```python class Units(Enum): CELSIUS = "celsius" FAHRENHEIT = "fahrenheit" def get_weather(city: str, units: Units = Units.CELSIUS) -> str: """Get the weather for a city.""" agent = Agent(model_name="openai/gpt-5.4-mini", tools=[get_weather], max_loops=1) agent.run("Weather in Paris?") ``` On master, every call fails: ``` litellm.InternalServerError: OpenAIException - Object of type Units is not JSON serializable ``` The schema itself already says the right thing: `type2schema` turns `Units` into `{"enum": ["celsius", "fahrenheit"], "type": "string"}`. Only the default is left as `<Units.CELSIUS: 'celsius'>`, so an agent whose tools take an `Enum` or `Path` default can never reach the model. ## What this changes The default is written with `pydantic_core.to_jsonable_python`, the same conversion pydantic uses for JSON output. It is already installed with pydantic. An `Enum` becomes its value (`"celsius"`), a `Path` its string, and `datetime` its ISO string. `fallback=str` covers any other object, so building a schema never raises where it did not before. JSON defaults (`None`, numbers, strings, lists) are unchanged. ## Verification - `test_func_to_dict_sends_json_defaults` in `tests/tools/test_base_tool.py`, next to the other `func_to_dict` schema tests. It checks that the `Enum` and `Path` defaults come out as `"celsius"` and `"forecasts"`, and that the whole schema survives `json.dumps`. On master, with the source reverted, it fails with `AssertionError: assert <Units.CELSIUS: 'celsius'> == 'celsius'`. With the fix it passes, and all 23 tests in the file pass. - I also re-ran the agent above against a dead local endpoint. On master it fails with `not JSON serializable`. With the fix the request is serialized and fails only with the expected connection error. No docs change is needed: the docs say any typed Python function can be a tool, which is the behaviour this fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
## What is wrong `ForestSwarm.run` wraps the whole call in `try/except Exception`. The handler logs the error only when `verbose=True`, and `verbose` defaults to `False`. It then falls through and returns `None`, so any failure in the selected agent reaches the caller as a silent `None`: a model outage, a bad API key, or `AgentLLMError` after retries are exhausted. ```python forest = ForestSwarm(trees=[Tree("Experts", [tax_agent])]) forest.run("Can you sort my corporate filings?") # tax_agent.run raises RuntimeError # master: returns None, nothing logged ``` `Agent.run` raises on failure, and #2751 just made `Agent.bulk_run` raise instead of logging and returning `None`. `ForestSwarm.run` is the remaining wrapper that turns an agent's error into `None`. ## What this changes - **`swarms/structs/tree_swarm.py`:** drop the `try/except`, so the selected agent's exception propagates unchanged. Nothing else changes: when no tree matches, `run` still returns `"No relevant agent found to handle this task."` and logs it under `verbose`, and the routing and `log_tree_execution` are untouched. - **`tests/structs/test_forest_swarm.py`:** add one test. The agent's `run` raises `RuntimeError`, and the test checks that `forest.run` raises it. Embeddings are stubbed offline as in the existing routing test, so no model is called. ## Verification - With `tree_swarm.py` checked out from `origin/master`, the new test fails: `pytest.raises(RuntimeError)` sees no exception, because `run` returned `None`. With the fix it passes. - `tests/structs/test_forest_swarm.py`: 14 passed. - `black` 24.2.0 reports no changes. The diff adds no comments. ## Docs `api/forest-swarm.mdx` already describes `run` as returning "the result of the task execution" and never says it returns `None` on error, so it needs no change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Closes #2708 ## What is wrong `copy.deepcopy(agent)` always raises `TypeError: cannot pickle '_thread.lock' object`, so there is no way to copy an `Agent` with its own state. The two callers that need one per task both end up sharing the original: - `Agent.run_concurrent_tasks` runs `self.run` on one agent from several threads, so each request carries the other tasks' turns. - `AgentRearrange._clone_for_task` deep-copies each agent, always hits the exception and falls back to the original instance. Reproduced on master (32ee60e7): ``` deepcopy: TypeError: cannot pickle '_thread.lock' object AgentRearrange clone shares agent: True user turns per request: [['A'], ['A', 'B'], ['A', 'B', 'C'], ['A', 'B', 'C', 'D']] ``` ## What this changes - **`Agent.clone()`** rebuilds the agent from the constructor arguments, recorded on the first line of `__init__`. Tools, handoffs and MCP config carry over. The clone gets a new LLM client and a fresh `short_memory`. It replays the original arguments rather than reading attributes back, because `__init__` appends the autonomous, reasoning, react and handoff prompts to `system_prompt` and would append them twice otherwise. - **What the clone keeps from the original:** the `id` (so files land in the same workspace directory), the `saved_state_path` (so autosave keeps rewriting one file) and the usage counter (so `agent.usage` still includes every task, as it did when the original was shared). - **What the clone never does:** it never republishes to the marketplace. - **`run_concurrent_tasks`** builds every clone before any task starts, then runs each task on its own clone. Building them first means a `persistent_memory` clone never preloads MEMORY.md lines that a running task has already written. - **`AgentRearrange._clone_for_task`** clones `Agent` members with `clone()`. Other members keep the deep-copy-or-share path; an existing test relies on it for stateful duck-typed members. - **`tools_list_dictionary`** is now copied instead of aliasing the caller's list. `load_tools` extends that list in place, so a clone appended `handoff_task` a second time and changed the caller's list. Without the copy, an agent with handoffs ended with `['handoff_task', 'lookup', 'handoff_task']` on both the original and the clone. ## Verification Same script on this branch: ``` deepcopy: TypeError: cannot pickle '_thread.lock' object AgentRearrange clone shares agent: False user turns per request: [['A'], ['B'], ['C'], ['D']] has clone: True ``` A clone of an agent with `max_loops=2`, a tool, a handoff and an `mcp_url` (`MCPManager.get_tools` stubbed): ``` memory distinct True llm distinct True system_prompt equal True schemas ['handoff_task', 'lookup'] ['handoff_task', 'lookup'] caller list 0 ``` Usage after three concurrent tasks that each report 10 input and 5 output tokens: `{'input_tokens': 30, 'output_tokens': 15, 'total_tokens': 45, ...}` on the original agent. Tests in `tests/structs/test_agent.py`: - `test_clone_has_own_memory_and_client` and `test_run_concurrent_tasks_isolates_each_task` fail on master's source (`AttributeError: 'Agent' object has no attribute 'clone'`, and `assert [['A'], ['A',...B', 'C', 'D']] == [['A'], ['B'], ['C'], ['D']]`). Both pass here. - The `TestConcurrentExecutionPool` helper now builds a real agent with `_patched_agent`, because a bare `Agent.__new__` instance has no constructor arguments to clone. Across `test_agent.py`, `test_agent_rearrange.py`, `test_sequential_workflow.py`, `tests/agents`, `test_phased_fan_out_workflow.py` and `test_image_batch_processor.py`, the failing set is the same as master's. Those failures are the live-provider tests, which need an API key. ## Limitations - A clone is built from constructor arguments, so changes made after construction are not carried over. That covers `update_max_loops`, assigning an attribute directly, and `load()`. - A clone runs `__init__` again, including the MCP `list_tools` call and any `marketplace_prompt_id` fetch. That is one call per task. - A passed-in `llm` object is shared, not rebuilt, since it cannot be rebuilt generically. - Clones of a `persistent_memory` agent append to the same MEMORY.md under separate per-Conversation locks. Each append is a single write in append mode. - Because the clone keeps the id, it reloads `./conversations/conversation_<name>.json` if the original's conversation was exported there. - `AgentRearrange.batch_run`, `AgentRearrange.concurrent_run` and `SequentialWorkflow.run_concurrent` still build each clone inside their worker loops. - `MajorityVoting._clone_for_task` is left alone because #2469 is open on that code. Docs: The-Swarm-Corporation/swarms-framework-docs#313 tracks the `Agent.clone()` page and the `run_concurrent_tasks` behaviour change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) (Claude Opus 5.5)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to 2.14.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pydantic/pydantic/releases">pydantic's releases</a>.</em></p> <blockquote> <h2>v2.14.1 2026-10-11</h2> <h3>What's Changed</h3> <h4>Performance</h4> <ul> <li>Avoid Python tuple allocation during model fields validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13955">#13955</a></li> <li>Avoid unnecessary extra dict allocation in model validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13956">#13956</a></li> </ul> <h4>Fixes</h4> <ul> <li>Fix <code>arguments-v3</code> schema with variadic keywords by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13954">#13954</a></li> <li>Fix <code>TypeError</code> on <code>ForwardRef</code> on Python 3.10.0 by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13964">#13964</a></li> <li>Support <code>closed</code> and <code>extra_items</code> via inheritance in <code>TypedDict</code> by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13967">#13967</a></li> <li>Enforce the <code>max_digits</code> whole-digits limit in the Python <code>Decimal</code> validator by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13972">#13972</a></li> <li>Reject out-of-range int inputs for <code>timedelta</code> by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13973">#13973</a></li> <li>Support named tuples in <code>Sequence</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13978">#13978</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/pydantic/pydantic/compare/v2.14.0...v2.14.1">https://github.com/pydantic/pydantic/compare/v2.14.0...v2.14.1</a></p> <h2>v2.14.0 2026-10-08</h2> <h3>What's Changed</h3> <p>The highlights of the v2.14 release are available in the <a href="https://pydantic.dev/articles/pydantic-v2-14-release">blog post</a>. Several minor changes (considered non-breaking changes according to our <a href="https://pydantic.dev/docs/validation/2.14/get-started/version-policy/#pydantic-v2">versioning policy</a>) are also included in this release. Make sure to look into them before upgrading.</p> <p>This release drops support for Python 3.9 and adds support for Python 3.15.</p> <h4>New Features</h4> <ul> <li>Add support for <code>multiple_of</code> constraint in <code>fraction</code> and <code>timedelta</code> core schemas by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13873">#13873</a></li> <li>Add <code>__namespace__</code> parameter to <code>create_model()</code> by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13895">#13895</a></li> </ul> <h4>Changes</h4> <ul> <li>Add an <code>ordered-dict</code> core schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13796">#13796</a></li> <li>Add a <code>counter</code> core schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13824">#13824</a></li> <li>Do not mutate <code>model_config</code> attribute by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13825">#13825</a></li> <li>Reject newlines in <code>NameEmail</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13897">#13897</a></li> </ul> <h4>Fixes</h4> <ul> <li>Call enum constructors in both validation modes by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13792">#13792</a></li> <li>Fix documentation examples not raising and remove unused <code>string_sub_type</code> unused error code by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13821">#13821</a></li> <li>Catch <code>OverflowError</code> during <code>ByteSize</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13850">#13850</a></li> <li>Require <code>multiple_of</code> constraints to be positive by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13862">#13862</a></li> <li>Always pop field name and model type stacks during schema generation by <a href="https://github.com/saquibjawedbit"><code>@​saquibjawedbit</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13859">#13859</a></li> <li>Handle <code>TypeError</code> gracefully during docstring extraction by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13874">#13874</a></li> <li>Fix config propagation of stdlib dataclasses and <code>TypedDict</code>s in JSON Schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13891">#13891</a></li> <li>Don't apply <code>ser_json_timedelta</code> to all datetime types in serialization inference by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13892">#13892</a></li> <li>Encode JSON Schema defaults with a consistent configuration by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13893">#13893</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/pydantic/pydantic/blob/main/HISTORY.md">pydantic's changelog</a>.</em></p> <blockquote> <!-- raw HTML omitted --> <!-- raw HTML omitted --> <!-- raw HTML omitted --> <h2>v2.14.0 (2026-10-08)</h2> <p><a href="https://github.com/pydantic/pydantic/releases/tag/v2.14.0">GitHub release</a></p> <h3>What's Changed</h3> <p>The highlights of the v2.14 release are available in the <a href="https://pydantic.dev/articles/pydantic-v2-14-release">blog post</a>. Several minor changes (considered non-breaking changes according to our <a href="https://pydantic.dev/docs/validation/2.14/get-started/version-policy/#pydantic-v2">versioning policy</a>) are also included in this release. Make sure to look into them before upgrading.</p> <p>This release drops support for Python 3.9 and adds support for Python 3.15.</p> <h4>New Features</h4> <ul> <li>Add support for <code>multiple_of</code> constraint in <code>fraction</code> and <code>timedelta</code> core schemas by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13873">#13873</a></li> <li>Add <code>__namespace__</code> parameter to <code>create_model()</code> by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13895">#13895</a></li> </ul> <h4>Changes</h4> <ul> <li>Add an <code>ordered-dict</code> core schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13796">#13796</a></li> <li>Add a <code>counter</code> core schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13824">#13824</a></li> <li>Do not mutate <code>model_config</code> attribute by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13825">#13825</a></li> <li>Reject newlines in <code>NameEmail</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13897">#13897</a></li> </ul> <h4>Fixes</h4> <ul> <li>Call enum constructors in both validation modes by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13792">#13792</a></li> <li>Fix documentation examples not raising and remove unused <code>string_sub_type</code> unused error code by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13821">#13821</a></li> <li>Catch <code>OverflowError</code> during <code>ByteSize</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13850">#13850</a></li> <li>Require <code>multiple_of</code> constraints to be positive by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13862">#13862</a></li> <li>Always pop field name and model type stacks during schema generation by <a href="https://github.com/saquibjawedbit"><code>@​saquibjawedbit</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13859">#13859</a></li> <li>Handle <code>TypeError</code> gracefully during docstring extraction by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13874">#13874</a></li> <li>Fix config propagation of stdlib dataclasses and <code>TypedDict</code>s in JSON Schema by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13891">#13891</a></li> <li>Don't apply <code>ser_json_timedelta</code> to all datetime types in serialization inference by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13892">#13892</a></li> <li>Encode JSON Schema defaults with a consistent configuration by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13893">#13893</a></li> <li>Propagate <code>init</code> and <code>kw_only=False</code> from <code>Field()</code> to stdlib dataclass fields by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13898">#13898</a></li> <li>Respect propagated extra config in the JSON Schema of stdlib dataclasses by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13899">#13899</a></li> <li>Handle OS errors during <code>ZoneInfo</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13912">#13912</a></li> <li>Handle exceptions during <code>Decimal</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13940">#13940</a></li> <li>Rebuild incomplete types during serialization inference by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13922">#13922</a></li> <li>Reject large exponents in <code>Fraction</code> validation by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13944">#13944</a></li> <li>Avoid validating URL prefix multiple times in multi host URLs by <a href="https://github.com/Viicos"><code>@​Viicos</code></a> in <a href="https://redirect.github.com/pydantic/pydantic/pull/13945">#13945</a></li> </ul> <h3>New Contributors</h3> <ul> <li><a href="https://github.com/saquibjawedbit"><code>@​saquibjawedbit</code></a> made their first contribution in <a href="https://redirect.github.com/pydantic/pydantic/pull/13859">#13859</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pydantic/pydantic/commit/b2212c787fb5c73386dc770681ec49cd09da8e73"><code>b2212c7</code></a> Prepare release v2.14.1</li> <li><a href="https://github.com/pydantic/pydantic/commit/968fb2b6eed196a3f17ca9a3aa4697b518614531"><code>968fb2b</code></a> Fix docs conversion table</li> <li><a href="https://github.com/pydantic/pydantic/commit/6adc8b3f6146cabf290bdefd51c87486dd659091"><code>6adc8b3</code></a> Support named tuples in <code>Sequence</code> validation</li> <li><a href="https://github.com/pydantic/pydantic/commit/7637fbc31c91f86870c3b34d220d5b7fd62c54db"><code>7637fbc</code></a> Reject out-of-range int inputs for <code>timedelta</code></li> <li><a href="https://github.com/pydantic/pydantic/commit/dfdbfcb5fa6cf0ae291cb8aebc5bf2153670fe9c"><code>dfdbfcb</code></a> Enforce the <code>max_digits</code> whole-digits limit in the Python <code>Decimal</code> validator</li> <li><a href="https://github.com/pydantic/pydantic/commit/e2456d3161d0969b917fc769b2e06b7e076cb886"><code>e2456d3</code></a> Support <code>closed</code> and <code>extra_items</code> via inheritance in <code>TypedDict</code></li> <li><a href="https://github.com/pydantic/pydantic/commit/317655dfe022ebfc818c8e8e744b671fe53d8c3f"><code>317655d</code></a> Harden CI security</li> <li><a href="https://github.com/pydantic/pydantic/commit/3ba1dd48939542a04748a9bcfd3248496fa0bcc5"><code>3ba1dd4</code></a> Fix <code>TypeError</code> on <code>ForwardRef</code> on Python 3.10.0</li> <li><a href="https://github.com/pydantic/pydantic/commit/43681abc488119808fcdae2f60cadd5520ceed26"><code>43681ab</code></a> Bump <code>samuelcolvin/check-python-version</code> action to v5.1</li> <li><a href="https://github.com/pydantic/pydantic/commit/d6bc53a8559d93814009eb2043db133a90f7f921"><code>d6bc53a</code></a> Avoid unnecessary extra dict allocation in model validation</li> <li>Additional commits viewable in <a href="https://github.com/pydantic/pydantic/compare/v2.12.5...v2.14.1">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details>