- 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 первым)
552 lines
30 KiB
Python
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
|