Compare commits

...
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 013799eb41 test: treat mem0 missing-key as resolved in preset resolution test
The mem0 preset is a live backend that requires credentials at
construction. When mem0ai is installed in CI without MEM0_API_KEY,
Agent(memory="mem0") raises "Mem0 API Key not provided". The test's
intent (#4229) is to ensure no removed backend is advertised, not that
every backend constructs without credentials, so a missing-key error
now counts as resolved while unknown-preset errors still fail.

Co-authored-by: Mervin Praison <[email protected]>
2026-08-25 09:59:12 +00:00
MervinPraisonandClaude Opus 5 c1eba0c5bc fix: repair internal callers still passing verbose= to Agent()
Verbosity was consolidated into output=, but several shipped call sites were
left behind and now raise TypeError on construction:

  compaction/memory_flush.py  the pre-compaction agent could never be built
  tools/delegation_tools.py   every delegate_task call
  scheduler/yaml_loader.py    passed verbose unconditionally, so every
                              scheduled YAML agent raised whether or not the
                              key appeared in the YAML

Agent's rejection of unknown kwargs is correct and deliberate -- an allowlist
that raises rather than swallowing -- so it is left intact. What was missing
was guidance: verbose is accepted by six sibling classes, and Agent has no
verbosity parameter among its 41, so the answer is not discoverable from the
signature. The error now names output= as the destination.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-25 10:47:45 +01:00
6 changed files with 93 additions and 8 deletions

No files matched your search

@@ -801,9 +801,28 @@ class Agent(GoalLoopMixin, SteeringMixin, SandboxMixin, SkillReviewMixin, Unifie
_cloned_fallback_models = legacy_kwargs.pop("fallback_models", None)
_unknown = set(legacy_kwargs) - _legacy_defaults.keys()
if _unknown:
# Unknown kwargs are rejected rather than swallowed, so a typo can
# never silently disable the behaviour it was meant to configure.
# Some names are near-universal in this ecosystem, though -- and
# `verbose` is accepted by six sibling classes here -- so name the
# supported alternative instead of leaving the user to search a
# 41-parameter signature for something that is not in it.
_HINTS = {
"verbose": (
"verbosity moved into output=; use output='verbose' "
"(or output=OutputConfig(verbose=True))."
),
"stream": (
"streaming moved into output=; use "
"output=OutputConfig(stream=True)."
),
}
_hint = "".join(
f"\n {name}: {_HINTS[name]}" for name in sorted(_unknown) if name in _HINTS
)
raise TypeError(
f"Agent.__init__() got unexpected keyword argument(s): "
f"{', '.join(sorted(_unknown))}"
f"{', '.join(sorted(_unknown))}{_hint}"
)
allow_delegation = legacy_kwargs.get("allow_delegation", _legacy_defaults["allow_delegation"])
allow_code_execution = legacy_kwargs.get("allow_code_execution", _legacy_defaults["allow_code_execution"])
@@ -263,7 +263,7 @@ def create_memory_flush_agent(
) -> Any:
"""Create a restricted child agent sharing only the parent's memory store."""
from ..agent.agent import Agent
from ..config.feature_configs import ExecutionConfig
from ..config.feature_configs import ExecutionConfig, OutputConfig
from ..tools.memory import search_memory, store_memory
llm = config.llm or getattr(parent_agent, "llm", None)
@@ -286,8 +286,7 @@ def create_memory_flush_agent(
else getattr(parent_agent, "_memory_instance", None)
),
execution=ExecutionConfig(context_compaction=False, max_steps=3),
verbose=False,
stream=False,
output=OutputConfig(verbose=False, stream=False),
)
@@ -52,7 +52,6 @@ class DelegationTools:
role=role,
goal=f"Complete delegated {agent_type} tasks accurately.",
llm=llm,
verbose=False,
)
spawn = create_subagent_tool(agent_factory=_agent_factory)["function"]
@@ -0,0 +1,57 @@
"""An unknown Agent kwarg must say what to do instead, not just that it is wrong.
`Agent` deliberately rejects unknown keyword arguments rather than swallowing
them -- that part is correct and worth keeping. But several names are near
universal in this ecosystem (`verbose` is accepted by six sibling classes here,
and by crewai and langchain), so users reach for them constantly. A bare
"unexpected keyword argument" leaves them with no next step, and the answer is
not discoverable from the 41-parameter signature.
"""
import pytest
from praisonaiagents import Agent
def _err(**kwargs) -> str:
with pytest.raises(TypeError) as exc:
Agent(name="A", role="R", goal="G", **kwargs)
return str(exc.value)
def test_unknown_kwargs_are_still_rejected():
"""Positive control: the fail-fast behaviour must not be softened into a warning."""
assert "definitely_not_a_param" in _err(definitely_not_a_param=1)
def test_verbose_explains_where_verbosity_actually_lives():
"""Verbosity was consolidated into `output=`; the error must say so.
Naming the wrong destination is worse than naming none: it sends the user
to a setting that does not control this.
"""
message = _err(verbose=False)
assert "verbose" in message
assert "output" in message.lower(), (
"the error names the bad argument but not the supported alternative; "
f"got: {message}"
)
def test_stream_also_points_at_output():
"""`stream` moved the same way, and internal callers were passing both."""
assert "output" in _err(stream=False).lower()
def test_the_guidance_names_the_offending_argument_not_a_generic_hint():
"""Guidance must be specific, or it is noise on every unrelated typo."""
generic = _err(some_typo=1)
assert "some_typo" in generic
assert "output=" not in generic, (
"an unrelated typo received the migration guidance meant for `verbose`"
)
def test_multiple_unknown_kwargs_are_all_listed():
message = _err(verbose=True, another_bad_one=2)
assert "verbose" in message and "another_bad_one" in message
@@ -174,7 +174,7 @@ def create_agent_from_config(agent_config: Dict[str, Any]) -> Any:
goal=goal,
instructions=instructions,
tools=tools,
verbose=verbose
output="verbose" if verbose else "silent",
)
return agent
@@ -31,9 +31,20 @@ def test_generator_never_advertises_a_removed_backend():
def test_every_advertised_preset_resolves():
"""Each preset the generator teaches must construct without raising."""
"""Each preset the generator teaches must resolve to a live backend.
A preset "resolves" when the resolver routes it to a real backend. Some
backends (e.g. mem0's managed client) additionally require credentials at
construction time; a missing-credential error still proves the preset is
live, so it is treated as resolved. What must never happen is the resolver
itself rejecting the preset as unknown/removed.
"""
for preset in _memory_caps()["supports_presets"]:
Agent(instructions="t", memory=preset)
try:
Agent(instructions="t", memory=preset)
except ValueError as exc:
if "api key" not in str(exc).lower():
raise
def test_every_advertised_url_scheme_resolves():