4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
168 lines
7.1 KiB
Python
168 lines
7.1 KiB
Python
"""The reusable tool-calling agent every surface runs (shell, gateway, investigation).
|
|
|
|
You create an ``Agent`` with its config (LLM, system prompt, tools, iteration
|
|
cap); ``run()`` gathers that config for one run and hands it to
|
|
``core.agent.react_loop.run_react_loop``, which runs the actual
|
|
think -> call-tools -> observe loop. ``Agent`` stays thin: it holds the config
|
|
and provides the callback methods (from the mixins) the loop calls back into —
|
|
it does not contain the loop itself.
|
|
|
|
The other agent shape — a direct answer with no tools — is not an ``Agent``;
|
|
see ``core/agent_harness/AGENTS.md``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import deque
|
|
from collections.abc import Sequence
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from core.agent.mixins import EventEmitterMixin, SteeringMixin, ToolFilterMixin
|
|
from core.agent.provider_hooks import ProviderHookDelegate
|
|
from core.agent.react_loop import run_react_loop
|
|
from core.agent.run_io import AgentRunInput, AgentRunResult
|
|
from core.events import RuntimeEventCallback, TupleEventCallback
|
|
from core.execution import ToolExecutionHooks
|
|
from core.llm.factory import LLMRole
|
|
from core.messages import ProviderMessage, RuntimeMessage, RuntimeMessageLike
|
|
from core.provider import ProviderHooks, ProviderRequest
|
|
from core.types import RuntimeTool
|
|
|
|
if TYPE_CHECKING:
|
|
from core.agent_harness.turns.turn_snapshot import AgentRuntimeRequest
|
|
|
|
|
|
class Agent[RuntimeToolT: RuntimeTool](EventEmitterMixin, ToolFilterMixin, SteeringMixin):
|
|
"""Stateful, configurable ReAct agent — the tool-calling agent shape.
|
|
|
|
Wires per-run context into ``run_react_loop`` and exposes hook methods so
|
|
subclasses can customise stopping logic and tool filtering without
|
|
re-implementing the loop. For the direct-answer shape (no tools), see
|
|
``core/agent_harness/AGENTS.md``.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
llm: Any | None = None,
|
|
system: str | None = None,
|
|
tools: Sequence[RuntimeToolT] | None = None,
|
|
resolved_integrations: dict[str, Any] | None = None,
|
|
max_iterations: int | None = None,
|
|
on_event: TupleEventCallback | None = None,
|
|
on_runtime_event: RuntimeEventCallback | None = None,
|
|
tool_hooks: ToolExecutionHooks | None = None,
|
|
tool_resources: dict[str, Any] | None = None,
|
|
provider_hooks: ProviderHooks | None = None,
|
|
) -> None:
|
|
self._llm = llm
|
|
self._system = system
|
|
self._tools: list[RuntimeToolT] | None = list(tools) if tools is not None else None
|
|
self._resolved = resolved_integrations
|
|
self._max_iterations = max_iterations
|
|
self._on_tuple_event = on_event
|
|
self._on_runtime_event = on_runtime_event
|
|
self._tool_hooks = tool_hooks or ToolExecutionHooks()
|
|
self._tool_resources = dict(tool_resources or {})
|
|
self._hooks = ProviderHookDelegate(provider_hooks or ProviderHooks())
|
|
self._steering_messages: deque[str] = deque()
|
|
self._follow_up_messages: deque[str] = deque()
|
|
self._react_iterations_used = 0
|
|
self._react_executed: list[tuple[Any, Any]] = []
|
|
self._react_hit_iteration_cap = False
|
|
|
|
def run(
|
|
self,
|
|
initial_messages: Sequence[RuntimeMessageLike] | None = None,
|
|
*,
|
|
runtime_request: AgentRuntimeRequest | None = None,
|
|
) -> AgentRunResult:
|
|
"""Assemble the resolved per-run input and hand it to ``run_react_loop``."""
|
|
self._react_iterations_used = 0
|
|
self._react_executed = []
|
|
self._react_hit_iteration_cap = False
|
|
run_input = self._build_run_input(initial_messages, runtime_request)
|
|
return run_react_loop(run_input, self)
|
|
|
|
def _note_react_run_progress(
|
|
self,
|
|
*,
|
|
iterations_used: int,
|
|
executed: list[tuple[Any, Any]],
|
|
hit_iteration_cap: bool,
|
|
) -> None:
|
|
"""Record partial loop progress for telemetry when ``run`` aborts early."""
|
|
self._react_iterations_used = iterations_used
|
|
self._react_executed = executed
|
|
self._react_hit_iteration_cap = hit_iteration_cap
|
|
|
|
def _build_run_input(
|
|
self,
|
|
initial_messages: Sequence[RuntimeMessageLike] | None,
|
|
runtime_request: AgentRuntimeRequest | None,
|
|
) -> AgentRunInput[RuntimeToolT]:
|
|
"""Assemble the run input from whichever source the caller supplied.
|
|
|
|
A ``runtime_request`` is validated and carries its own resolved context;
|
|
raw ``initial_messages`` fall back to the construction-time config, which
|
|
must include ``system`` and ``max_iterations``.
|
|
"""
|
|
if runtime_request is not None:
|
|
runtime_request.validate_runtime_request()
|
|
return AgentRunInput[RuntimeToolT].from_runtime_request(
|
|
runtime_request, llm=self._get_llm()
|
|
)
|
|
if initial_messages is not None:
|
|
if self._system is None:
|
|
raise ValueError("Agent.run: system= must be set at construction.")
|
|
if self._max_iterations is None:
|
|
raise ValueError("Agent.run: max_iterations= must be set at construction.")
|
|
return AgentRunInput[RuntimeToolT].from_messages(
|
|
initial_messages,
|
|
llm=self._get_llm(),
|
|
system=self._system,
|
|
tools=self._tools,
|
|
resolved=self._resolved,
|
|
tool_resources=self._tool_resources,
|
|
max_iterations=self._max_iterations,
|
|
)
|
|
raise ValueError("Agent.run requires initial_messages or runtime_request.")
|
|
|
|
def _get_llm(self) -> Any:
|
|
"""Return the run's LLM: the instance given at construction, or the process-wide singleton."""
|
|
if self._llm is None:
|
|
from core.llm import factory
|
|
|
|
self._llm = factory.get_llm(LLMRole.AGENT)
|
|
if self._llm is None:
|
|
raise RuntimeError("Agent.run: llm must be set before the loop")
|
|
return self._llm
|
|
|
|
def _should_accept_conclusion(
|
|
self,
|
|
*,
|
|
evidence_count: int, # noqa: ARG002
|
|
iteration: int, # noqa: ARG002
|
|
) -> tuple[bool, str | None]:
|
|
"""Hook: decide what to do when the LLM stops requesting tools.
|
|
|
|
Return ``(True, None)`` to accept the conclusion and end the loop.
|
|
Return ``(False, nudge_text)`` to inject a user message and continue.
|
|
"""
|
|
return True, None
|
|
|
|
# Thin forwarders to ``self._hooks`` (a ProviderHookDelegate). Kept as
|
|
# methods rather than an exposed attribute so LoopHost's contract is
|
|
# the four calls, not this concrete delegate type — see loop_host.py.
|
|
def _transform_messages(self, messages: list[RuntimeMessage]) -> list[RuntimeMessage]:
|
|
return self._hooks.transform_messages(messages)
|
|
|
|
def _convert_to_llm(self, llm: Any, messages: list[RuntimeMessage]) -> list[ProviderMessage]:
|
|
return self._hooks.convert_to_llm(llm, messages)
|
|
|
|
def _before_request(self, request: ProviderRequest) -> ProviderRequest:
|
|
return self._hooks.before_request(request)
|
|
|
|
def _after_response(self, request: ProviderRequest, response: Any) -> Any:
|
|
return self._hooks.after_response(request, response)
|