simbiot-harness/venv/lib/python3.12/site-packages/strands_harness/agent.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

552 lines
30 KiB
Python

"""The harness factory: a preconfigured ``strands.Agent`` in one call."""
from __future__ import annotations
import logging
import os
import tempfile
import uuid
from collections.abc import Mapping, Sequence
from typing import Any, Literal
from strands import Agent, BackgroundTasksConfig
from strands.experimental.context_manager import ContextManager
from strands.memory import MemoryManager
from strands.models import Model, ModelRouter
from strands.plugins import Plugin
from strands.session import SessionManager, SnapshotSessionManager
from strands.storage import LocalFileStorage
from strands.tools.mcp import MCPClient, MCPServerConfig
from strands.types.tools import AgentTool
from strands.vended_plugins.context_offloader import ContextOffloader, FileStorage
from strands.vended_plugins.skills import AgentSkills, SkillSources
from strands.vended_tools import make_shell
from strands_harness import defaults
from strands_harness.interventions import InterventionsOption, resolve_interventions
from strands_harness.memory import resolve_memory
from strands_harness.models import (
_supports_media,
resolve_model,
resolve_web_fetch_model,
supports_web_search,
)
from strands_harness.options import (
_builtin_tool_config,
_memory_config,
_normalize_builtin_tools,
_sanitize_session_id,
_session_config,
)
from strands_harness.plugins import EnvironmentContext, Todos
from strands_harness.prompt import build_system_prompt
from strands_harness.telemetry import setup_telemetry
from strands_harness.tools import (
edit,
exa_web_search,
make_programmatic_tool_caller,
make_read,
make_web_fetch,
write,
)
from strands_harness.tools.subagent import build_default_subagent
from strands_harness.types.agent import (
BuiltinPluginName,
BuiltinToolName,
BuiltinToolsConfig,
ContextManagerOption,
Effort,
MemoryConfig,
SessionConfig,
)
logger = logging.getLogger(__name__)
# Built-in plugins, each toggled by name via ``builtin_plugins``. Unlike the offloader and skills
# plugins (wired from their own options), these are opt-out feature plugins that only bundle a tool
# and a loop-level behavior; the map is the seam to grow the set (e.g. memories) later.
_BUILTIN_PLUGINS = {"todos": Todos, "environment": EnvironmentContext}
# The delegation tool always runs in the background: its calls are long-running subtasks whose
# intermediate work should stay out of the parent's turn.
_ALWAYS_BACKGROUND_TOOL_NAMES = frozenset({"subagent"})
_AUTO_MAX_RESULT_TOKENS = 1_500
_AUTO_PREVIEW_TOKENS = 750
# Sentinel for ``caching``: distinguishes "not passed" (the default, which warns on an unsupported
# provider) from an explicit value like ``caching=None`` (off) or ``caching=True`` (which raises on
# an unsupported provider string, and only warns when the model is a ``Model`` instance).
_UNSET: Any = object()
def _builtin_tools(parent_config: dict[str, Any]) -> dict[str, Any]:
"""The built-in tools by name. Each configurable one is built from its factory with the config it
was enabled with (``{}`` for ``True``, see ``_BUILTIN_TOOL_CONFIG_KEYS``); ``web_fetch`` also
derives its summarizer from the agent's model, ``subagent`` takes the whole parent config so it
can rebuild a child the way this agent was built. ``subagent``'s own delegation-depth budget
lives on ``agent.state``, tracked by the tool itself. ``web_search`` here is the Exa tool;
``create_harness`` selects it only when the setting is ``"exa"``."""
enabled = parent_config["builtin_tools"]
web_fetch_config = _builtin_tool_config(enabled, "web_fetch")
web_fetch_model = resolve_web_fetch_model(parent_config["model"], web_fetch_config.pop("model", None))
tools = (
make_shell(**_builtin_tool_config(enabled, "shell")),
make_read(**{"media": _supports_media(parent_config["model"]), **_builtin_tool_config(enabled, "read")}),
write,
edit,
make_web_fetch(model=web_fetch_model, **web_fetch_config),
exa_web_search,
make_programmatic_tool_caller(**_builtin_tool_config(enabled, "programmatic_tool_caller")),
build_default_subagent(create_harness, parent_config, **_builtin_tool_config(enabled, "subagent")),
)
return {t.tool_name: t for t in tools}
def _select_builtin_tools(enabled: Mapping[str, Any], tools: dict[str, Any]) -> list[Any]:
# A config mapping enables the tool even when empty (``{"web_fetch": {}}`` is "on with
# defaults"), so only ``False`` means off.
return [tools[name] for name, setting in enabled.items() if setting is not False]
def _web_search_mode(
setting: Any, explicit: bool, model: Model | ModelRouter | str | None
) -> Literal["native", "exa"] | None:
"""How ``web_search`` is served for ``model``: ``"exa"`` (the third-party tool, whenever opted
into with ``"exa"``), ``"native"`` (a model flag), or ``None`` (off)."""
if setting is False:
return None
if setting == "exa":
logger.warning(
"web_search is opted into Exa (exa.ai), a third-party service: every search query leaves your "
"environment and is subject to Exa's privacy policy (https://exa.ai/privacy-policy)."
)
return "exa"
if supports_web_search(model):
return "native"
target = (
"A pre-built Model instance"
if isinstance(model, (Model, ModelRouter))
else f"Model {model or defaults.DEFAULT_MODEL}"
)
message = (
f"{target} has no native web search. Pass builtin_tools={{'web_search': 'exa'}} to search "
"through Exa (a third party), or drop 'web_search'."
)
if explicit:
raise ValueError(message)
logger.warning(message)
return None
def _durable_offloader(offload_dir: str) -> Any:
return ContextOffloader(
storage=FileStorage(offload_dir),
max_result_tokens=_AUTO_MAX_RESULT_TOKENS,
preview_tokens=_AUTO_PREVIEW_TOKENS,
)
def _has_offloader(plugins: list[Any]) -> bool:
return any(isinstance(p, ContextOffloader) for p in plugins)
def _skills_plugin(skills: bool | SkillSources | AgentSkills | None) -> AgentSkills | None:
"""The skills plugin for ``skills``: the default dir when present (``True``), the sources named
(passed through untouched; the SDK reports a missing path), or the instance verbatim."""
if skills is None or skills is False:
return None
if isinstance(skills, AgentSkills):
return skills
if skills is True:
return AgentSkills(skills=[defaults.DEFAULT_SKILLS_DIR]) if os.path.isdir(defaults.DEFAULT_SKILLS_DIR) else None
if isinstance(skills, list) and not skills:
return None
return AgentSkills(skills=skills)
def _has_skills(plugins: list[Any]) -> bool:
return any(isinstance(p, AgentSkills) for p in plugins)
def _select_builtin_plugins(names: Sequence[str] | None, existing: list[Any]) -> list[Any]:
if names is None:
names = defaults.DEFAULT_BUILTIN_PLUGINS
selected = []
for name in names:
if name not in _BUILTIN_PLUGINS:
available = ", ".join(sorted(_BUILTIN_PLUGINS))
raise ValueError(f"Unknown built-in plugin {name!r}. Available: {available}.")
plugin_cls = _BUILTIN_PLUGINS[name]
if not any(isinstance(p, plugin_cls) for p in existing):
selected.append(plugin_cls())
return selected
def _check_name_collisions(*sources: tuple[str, bool, list[Any]]) -> None:
"""Raise if two tools would register under the same name, so a collision fails at construction
with the losing source named rather than one tool silently disappearing. Names differing only by
``-``/``_`` collide, matching the SDK tool registry. Each source is ``(label, is_builtin, tools)``;
the remedy only mentions dropping a built-in when a built-in is actually one of the two sources."""
seen: dict[str, tuple[str, bool]] = {}
for label, is_builtin, tools in sources:
for tool in tools:
# Only resolved tool objects have a fixed name here; the SDK also accepts strings, dicts,
# modules, and nested lists in ``tools`` and resolves them later, so skip those.
if not isinstance(tool, AgentTool):
continue
key = tool.tool_name.replace("-", "_")
if key in seen:
prior_label, prior_builtin = seen[key]
where = label if prior_label == label else f"{prior_label} and {label}"
if is_builtin or prior_builtin:
remedy = "Rename the tool you passed, or drop the built-in via builtin_tools / builtin_plugins."
else:
remedy = "Rename one so each tool has a unique name."
raise ValueError(f"Tool name {tool.tool_name!r} is registered more than once (from {where}). {remedy}")
seen[key] = (label, is_builtin)
def _drop_sandbox_tools(agent: Agent) -> None:
"""Drop the tools the sandbox vends (e.g. ``sandbox_bash``); the harness's built-ins already route through it."""
for tool in agent.sandbox.get_tools():
agent.tool_registry.registry.pop(tool.tool_name, None)
def _plugin_tools(plugins: list[Any]) -> list[Any]:
"""The tools each plugin vends (e.g. the todos plugin's ``todo_write``), via the SDK's ``.tools``."""
return [tool for plugin in plugins for tool in (getattr(plugin, "tools", None) or [])]
def _resolve_background_tasks(
configured: bool | BackgroundTasksConfig | None,
forced: Sequence[AgentTool],
) -> Literal[False] | BackgroundTasksConfig:
if configured is False:
return False
policy: BackgroundTasksConfig = {"agentic": ["*"]} if configured is None or configured is True else configured
forced_names = {tool.tool_name for tool in forced}
def omit_forced(selectors: Sequence[AgentTool | str]) -> list[AgentTool | str]:
return [
selector
for selector in selectors
if selector == "*" or (selector if isinstance(selector, str) else selector.tool_name) not in forced_names
]
always = [*omit_forced(policy.get("always", [])), *forced]
resolved: BackgroundTasksConfig = {**policy}
if "agentic" in policy:
resolved["agentic"] = omit_forced(policy["agentic"])
if always or "always" in policy:
resolved["always"] = always
if "never" in policy:
resolved["never"] = omit_forced(policy["never"])
return resolved
def create_harness(
*,
model: Model | ModelRouter | str | None = None,
effort: Effort = defaults.DEFAULT_EFFORT,
instructions: str | None = None,
tools: list[Any] | None = None,
plugins: list[Plugin] | None = None,
mcp_servers: str | dict[str, MCPServerConfig] | None = None,
builtin_tools: Sequence[BuiltinToolName] | BuiltinToolsConfig | None = None,
background_tasks: bool | BackgroundTasksConfig | None = None,
caching: str | bool | None = _UNSET,
context_manager: ContextManagerOption = defaults.DEFAULT_CONTEXT_MANAGER,
session: bool | SessionConfig | SessionManager | None = True,
skills: bool | SkillSources | AgentSkills | None = True,
memory: bool | MemoryConfig | MemoryManager | None = True,
builtin_plugins: list[BuiltinPluginName] | None = None,
interventions: InterventionsOption = None,
**agent_kwargs: Any,
) -> Agent:
"""Build a preconfigured Strands agent with the harness's defaults enabled.
Every default is overridable, and the return value is a plain ``strands.Agent`` that can
be modified further after construction. Any keyword accepted by ``Agent`` may be passed
through ``agent_kwargs``; an explicit SDK value there (``session_manager``, ``memory_manager``,
``system_prompt``, ...) always beats the harness sugar it corresponds to.
Args:
model: A ``Model`` or ``ModelRouter`` instance, a ``"provider/name"`` string (e.g.
``"anthropic/claude-fable-5"``), a bare Bedrock model id, or ``None`` for the
harness default (Bedrock Opus 5).
effort: Reasoning effort applied to the resolved model, mapped to each provider's request
fields. ``"auto"`` (the default) uses the provider's recommended level, ``"off"`` turns
reasoning off, and ``"minimal"``/``"low"``/``"medium"``/``"high"``/``"xhigh"``/``"max"``
set it explicitly; a level the provider does not offer raises. When ``model`` is a
``Model`` or ``ModelRouter`` instance a non-``"auto"`` effort is ignored with a warning
(configure reasoning on the instance).
instructions: A domain block appended after the harness contract. Ignored when a full
``system_prompt`` is passed via ``agent_kwargs``.
tools: Consumer tools, added alongside the built-in tools. To expose a specialist ``Agent``
as a tool, wrap it with ``Agent.as_tool()`` and pass it here. A tool name must be unique
across all sources (built-in tools, ``tools``, plugins); a collision raises at
construction. To replace a built-in, drop it first via ``builtin_tools`` so the name is
free. A connected ``MCPClient`` passed here is offered to delegates on the ``subagent``
tool's ``mcp_servers`` axis under its ``client_name`` (set ``application_name``), like the
servers loaded from ``mcp_servers``; an unnamed or same-named client is warned about and
left off that axis.
plugins: Consumer SDK plugins, added alongside the built-in plugins (consumer plugins run
first). Tools a plugin vends take part in the name-collision check, and the plugins reach
``subagent`` children too. A ``ContextOffloader`` or ``AgentSkills`` passed here replaces
the one the harness would add. Plugin *instances* are shared with children, so one that keeps
per-agent state must keep it in ``agent.state`` (the SDK convention), not on ``self``.
mcp_servers: MCP servers to connect, given as the standard ``mcpServers`` config: either a
path to a JSON file or the mapping itself (a flat ``{name: {...}}`` map, or that map under
an ``mcpServers`` key). Each server's tools are discovered and added to the tool list, and
the SDK manages connection and lifecycle. A server that fails to start yields no tools
rather than failing construction; set ``"continue_on_error": false`` on a server to make
its failure fatal. Each server's tools are prefixed with its name by default
(``<server>_<tool>``) so servers don't clash; set a server's ``"prefix"`` to override, or
``"prefix": ""`` to opt out.
builtin_tools: Which built-in tools to enable, as a list or a mapping. A **list pins** exactly
the names given (``[]`` disables them all). A **mapping edits** the harness's default set:
``False`` removes a tool, ``True`` adds one, and a config dict adds *and* configures it;
the ``"*"`` key (default ``True``) is the starting set, written ``False`` to start from
nothing: ``{"subagent": False}`` is the defaults minus one, ``{"*": False, "read": True}``
a pin. The default set is ``["shell", "read", "write", "edit", "web_fetch", "web_search",
"programmatic_tool_caller", "subagent"]``.
``web_fetch`` fetches a URL and answers a prompt about it via a small summarizer model,
keeping the raw page out of the main context; ``{"web_fetch": {"model": ...}}`` picks that
model (a ``Model``/``ModelRouter`` instance or ``"provider/name"`` string; the default is the
small fast model of the main agent's provider so credentials align), and ``{"web_fetch":
{"transport": "direct"}}`` fetches from the harness process instead of running ``curl`` in the
agent's sandbox (the default, ``"curl"``).
``web_search`` turns on the model provider's native web search (OpenAI, Anthropic, Google,
GPT-5/GPT-6 models on bedrock-mantle). Elsewhere (Bedrock Converse, other Mantle models,
``Model`` instances) it is off, and naming it there raises. ``{"web_search": "exa"}``
instead gives the model a ``web_search`` tool backed by Exa's hosted search on any model,
a third party that receives the queries (keyless; ``EXA_API_KEY`` lifts its rate limit).
``programmatic_tool_caller`` lets the model orchestrate its other tools by writing Python
that runs in a Monty sandbox (no filesystem, network, or process access; only the other
tools are reachable), returning only what the code prints.
``subagent`` delegates a self-contained subtask to a child agent built through this same
factory, so the child is a full harness member: it inherits the model, built-in tools,
built-in plugins, skills, interventions, and the consumer ``plugins`` and ``hooks`` passed
here (the gate and any policy plugins/hooks reach the delegate too), plus the consumer
``tools`` (narrowable — the delegate may be granted a subset). By default the model picks
the ``generalist`` role, may write an ad-hoc role prompt, and may narrow the tool set
(never widen it). Delegation depth is bounded, so a child eventually cannot delegate
further. For a fully configured delegation tool (custom roles, model tiers, fixed
prompts), build one with ``make_subagent`` and pass it via ``tools`` with ``subagent``
dropped from ``builtin_tools``.
background_tasks: SDK Background Tasks policy. The ``subagent`` tool always runs in the
background; by default, the model may choose background execution for any other
compatible tool. The invocation waits for completion and continues with the result.
Pass ``False`` to disable Background Tasks, or provide a policy to control other tools,
concurrency, completion, and timeouts.
caching: Enables prompt caching to save cost and latency where the provider supports it
(Bedrock and Anthropic direct set cache points and cached tools; OpenAI, Gemini, and
bedrock-mantle cache automatically server-side). Defaults to on. ``False``/``None`` turns
off what the harness configures, and has no effect where caching is automatic. Explicitly
enabling caching on a provider without it raises; on a pre-built ``Model`` instance it is
ignored with a warning (configure it on the instance).
context_manager: The SDK's ``Agent(context_manager=)`` option: ``"auto"`` (the default) or
``"agentic"`` for an SDK preset, a ``ContextManagerConfig`` dict or a ``ContextManager``
instance for a custom pipeline, or ``False``/``None`` to disable it. When enabled, large
tool results are also offloaded to disk (a preview and reference are kept in context) so
the agent can run longer before compacting; disabling turns this off too.
session: File-backed conversation persistence, on by default. ``True`` (or ``{}``) snapshots
the run under a fresh random id in ``./.agent/sessions`` via a ``SnapshotSessionManager``;
``{"id": ..., "dir": ...}`` picks the id and/or root directory; a ``SessionManager``
instance is used verbatim; ``False``/``None`` disables it (the conversation is in-memory
and offloaded artifacts go to a temporary directory that does not outlive the process).
This does not auto-resume across runs: with no ``id`` a new run starts a new session. To
continue a previous conversation, read the minted id off the returned agent
(``agent.session_id``) and pass it back as ``session={"id": ...}`` on the next run. An
explicit ``session_manager`` in ``agent_kwargs`` takes precedence.
skills: Agent Skills loaded via the SDK's ``AgentSkills`` plugin for progressive disclosure.
``True`` (the default) loads ``./.agent/skills`` when that directory exists and is
otherwise a no-op; the SDK's ``SkillSources`` (a skill dir, a parent dir of skills, a
``SKILL.md``, an ``https://`` URL, or a parsed ``Skill``, single or in a list) is passed
through untouched, so the SDK warns about a missing path; an ``AgentSkills`` instance is
used verbatim; ``False``/``None`` disables skills. Filesystem sources are read through the
agent's sandbox when there is one. An ``AgentSkills`` in ``plugins`` wins.
memory: File-backed long-term memory, on by default. When enabled, the agent distills durable
facts into markdown files, searches them before each turn, and folds the top matches into
context; persistence is independent of any session, so memory survives across sessions.
``True`` (or ``{}``) uses a file store under ``./.agent/memory``; ``{"dir": ...}`` moves it;
``{"stores": [...]}`` swaps in your own store(s) under the harness's memory policy (injection on,
``search_memory`` on, no ``add_memory`` tool), which a ``subagent`` delegate shares
read-only; a ``MemoryManager`` instance is used verbatim and not forwarded to delegates;
``False``/``None`` disables it. An explicit ``memory_manager`` in ``agent_kwargs`` takes
precedence. Extraction runs asynchronously every few turns on a small model, so a short run
may end before the first extraction fires; run
``if agent.memory_manager: await agent.memory_manager.flush()`` at your shutdown boundary to
persist what's pending.
builtin_plugins: Names of the built-in feature plugins to enable. Defaults to
``["todos", "environment"]``: ``todos`` adds a ``todo_write`` tool that tracks
multi-step work and re-surfaces the list before each step; ``environment`` injects the
platform, date, working directory, and the project's ``AGENTS.md`` (plus links to nearby
``AGENTS.md``/``README.md`` files) before each user turn. Pass ``[]`` to disable them.
interventions: Gate tool calls behind approval or a policy; defaults to ``None`` (off —
every call runs). Accepts a preset (``"ask"`` approves every call, ``"smart"`` lets the
SDK's LLM risk classifier flag risky ones), a natural-language policy used as that
classifier's prompt, a ``.cedar`` policy file (needs ``strands-agents[cedar]``), a
``HumanInTheLoop``/``CedarAuthorization`` instance for full control, or a list layering a
Cedar policy with one human gate. Sugar over the SDK's handlers; a ``subagent`` child
inherits the policy so a delegate cannot bypass it.
"""
setup_telemetry()
enabled_tools = _normalize_builtin_tools(builtin_tools)
session_option = _session_config(session)
memory_option = _memory_config(memory)
# Explicit when the caller named web_search themselves (a list containing it, or a mapping key).
web_search_explicit = (
"web_search" in builtin_tools if isinstance(builtin_tools, Mapping) else builtin_tools is not None
)
web_search = _web_search_mode(enabled_tools["web_search"], web_search_explicit, model)
if web_search is None:
# Off, or default-on but unavailable here: forward it off so a child with the same model stays quiet.
enabled_tools["web_search"] = False
elif not web_search_explicit:
# Forward the default as a default, so a child on a model without native search warns, not raises.
del enabled_tools["web_search"]
caching_explicit = caching is not _UNSET
caching_on = bool(defaults.DEFAULT_CACHING) if not caching_explicit else bool(caching)
resolved_model = resolve_model(
model,
defaults.DEFAULT_MODEL,
effort,
web_search=web_search == "native",
caching=caching_on,
caching_explicit=caching_explicit,
)
if "system_prompt" not in agent_kwargs:
agent_kwargs["system_prompt"] = build_system_prompt(instructions)
consumer_plugins = list(plugins or [])
# Hooks and the sandbox stay in agent_kwargs (the harness adds none) but are forwarded so subagent children
# run under them too.
consumer_hooks = list(agent_kwargs.get("hooks") or [])
consumer = list(tools or [])
# MCP clients are tool providers the SDK connects at load time; loaded ones join `tools` as consumer tools.
if mcp_servers:
consumer += MCPClient.load_servers(mcp_servers, continue_on_error=True, prefix_with_server_name=True)
# An explicit ``memory_manager`` (or a ``MemoryManager`` passed as ``memory``) wins over the harness's memory
# wiring and is not forwarded: a delegate shares harness-built stores read-only or gets none.
memory_manager = agent_kwargs.pop("memory_manager", None)
if memory_manager is None and isinstance(memory_option, MemoryManager):
memory_manager = memory_option
# ``{}`` is on (the harness's defaults) even though it is falsy; ``_memory_config`` maps off to ``None``.
child_memory: Literal[False] | MemoryConfig = (
False if memory_option is None or memory_manager is not None else memory_option
)
# What build_default_subagent rebuilds a child from: this call's keyword arguments, with ``builtin_tools``
# normalized and ``session`` forced off so a throwaway delegate never persists session state.
parent_config: dict[str, Any] = {
"model": model,
"effort": effort,
"caching": caching,
"context_manager": context_manager,
"builtin_tools": enabled_tools,
"background_tasks": background_tasks,
"builtin_plugins": builtin_plugins,
"skills": skills,
"memory": child_memory,
"session": False,
"interventions": interventions,
"plugins": consumer_plugins,
"hooks": consumer_hooks,
"tools": consumer,
"sandbox": agent_kwargs.get("sandbox"),
}
builtin = _select_builtin_tools({**enabled_tools, "web_search": web_search == "exa"}, _builtin_tools(parent_config))
context_enabled = context_manager is not None and context_manager is not False
# The SDK's Agent rejects a bare config dict; build the instance from it here.
if isinstance(context_manager, Mapping):
agent_kwargs["context_manager"] = ContextManager(**context_manager)
else:
agent_kwargs["context_manager"] = context_manager if context_enabled else False
session_manager = agent_kwargs.pop("session_manager", None)
session_dir: str | None = None
if session_manager is None:
if isinstance(session_option, SessionManager):
session_manager = session_option
elif session_option is not None:
session_id = session_option.get("id")
session_dir = session_option.get("dir") or defaults.DEFAULT_SESSION_DIR
resolved_id = _sanitize_session_id(session_id) if session_id else uuid.uuid4().hex[:8]
session_manager = SnapshotSessionManager(
resolved_id,
storage=LocalFileStorage(session_dir),
save_latest_on="message",
)
# Assemble plugins before the collision check so plugin-vended tools are checked too; consumer
# plugins stay first so the tail is the harness's own.
all_plugins: list[Any] = list(consumer_plugins)
if context_enabled and not _has_offloader(all_plugins):
offload_dir = (
os.path.join(session_dir, "offloaded")
if session_dir is not None
else tempfile.mkdtemp(prefix="strands-offload-")
)
all_plugins.append(_durable_offloader(offload_dir))
if not _has_skills(all_plugins):
skills_plugin = _skills_plugin(skills)
if skills_plugin is not None:
all_plugins.append(skills_plugin)
all_plugins.extend(_select_builtin_plugins(builtin_plugins, all_plugins))
harness_plugins = all_plugins[len(consumer_plugins) :]
# Memory is built from ``memory`` when on and nothing explicit was given. A ``MemoryManager``
# instance already carries its resolved tools (``search_memory`` by default); a bare config does
# not, so only an instance can be pre-flighted for name collisions here.
if memory_manager is None and child_memory is not False:
memory_manager = resolve_memory(
stores=child_memory.get("stores"),
model=model,
memory_dir=child_memory.get("dir") or defaults.DEFAULT_MEMORY_DIR,
web_fetch_model=_builtin_tool_config(enabled_tools, "web_fetch").get("model"),
)
memory_tools = memory_manager.tools if isinstance(memory_manager, MemoryManager) else []
_check_name_collisions(
("a built-in tool", True, builtin),
("tools", False, consumer),
("a built-in plugin", True, _plugin_tools(harness_plugins)),
("plugins", False, _plugin_tools(consumer_plugins)),
("memory", True, memory_tools),
)
# MCP tool names are only known once a client connects, so the pre-flight above skips them. They are
# namespaced by server (prefix_with_server_name) so cross-server names don't clash, but a server keyed
# to a built-in name (e.g. ``web`` exposing ``fetch`` -> ``web_fetch``) still can.
agent_tools = [*builtin, *consumer]
resolved_background_tasks = _resolve_background_tasks(
background_tasks,
[
candidate
for candidate in agent_tools
if isinstance(candidate, AgentTool) and candidate.tool_name in _ALWAYS_BACKGROUND_TOOL_NAMES
],
)
agent = Agent(
model=resolved_model,
tools=agent_tools,
plugins=all_plugins,
background_tasks=resolved_background_tasks,
session_manager=session_manager,
memory_manager=memory_manager,
interventions=resolve_interventions(interventions),
**agent_kwargs,
)
_drop_sandbox_tools(agent)
return agent