4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
200 lines
7.0 KiB
Python
200 lines
7.0 KiB
Python
"""Headless programmatic entry point and in-memory port adapters.
|
|
|
|
This is the proof that the agent is decoupled from any terminal: a caller (an
|
|
HTTP handler, a script, a test) can run a full turn with only a message. All the
|
|
surface concerns are satisfied by the in-memory adapters below, but every
|
|
dependency is injectable so a real surface can override any of them.
|
|
|
|
Example::
|
|
|
|
from core.agent_harness.turns.headless_dispatch import (
|
|
HeadlessAgent,
|
|
InMemorySessionStore,
|
|
NullToolProvider,
|
|
StaticReasoningClientProvider,
|
|
)
|
|
|
|
class _Echo:
|
|
def invoke_stream(self, prompt):
|
|
yield "hello"
|
|
|
|
agent = HeadlessAgent(
|
|
tools=NullToolProvider(),
|
|
reasoning=StaticReasoningClientProvider(client=_Echo()),
|
|
)
|
|
result = agent.dispatch("hi there")
|
|
print(result.assistant_response_text) # -> "hello"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.agent_harness.accounting.turn_accounting import DefaultTurnAccounting
|
|
from core.agent_harness.ports import (
|
|
ConfirmFn,
|
|
ErrorReporter,
|
|
OutputSink,
|
|
PromptContextProvider,
|
|
ReasoningClientProvider,
|
|
RunRecordFactory,
|
|
SessionStore,
|
|
ToolProvider,
|
|
TurnAccounting,
|
|
)
|
|
from core.agent_harness.prompts.prompt_context import (
|
|
DefaultPromptContextProvider,
|
|
supports_default_prompt_context,
|
|
)
|
|
from core.agent_harness.turns.action_driver import run_action_agent_turn
|
|
from core.agent_harness.turns.evidence_driver import gather_tool_evidence
|
|
from core.agent_harness.turns.headless_adapters import (
|
|
BufferOutputSink,
|
|
EmptyPromptContextProvider,
|
|
InMemorySessionStore,
|
|
NoopErrorReporter,
|
|
NoopTurnAccounting,
|
|
NullToolProvider,
|
|
SimpleRunRecord,
|
|
SimpleRunRecordFactory,
|
|
StaticReasoningClientProvider,
|
|
)
|
|
from core.agent_harness.turns.orchestrator import run_turn, stream_answer
|
|
from core.agent_harness.turns.turn_plan import TurnPlan
|
|
from core.agent_harness.turns.turn_results import ShellTurnResult, ToolCallingTurnResult
|
|
from core.execution import ToolExecutionHooks
|
|
|
|
|
|
class HeadlessAgent:
|
|
"""Runs full agent turns headlessly from a fixed set of configured ports.
|
|
|
|
Construct once with the ports/dependencies, then call :meth:`dispatch` per
|
|
message. ``tools`` is required — a surface that genuinely wants a text-only
|
|
turn passes :class:`NullToolProvider` explicitly. Every other port defaults
|
|
to an in-memory headless adapter. ``reasoning`` defaults to "no client" (the
|
|
conversational assistant is skipped) so a turn runs with zero configuration;
|
|
inject a client to get an actual answer. ``gather_enabled`` turns on the live
|
|
evidence-gather pass (off by default, since it reaches out to integrations).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
tools: ToolProvider,
|
|
session: SessionStore | None = None,
|
|
output: OutputSink | None = None,
|
|
prompts: PromptContextProvider | None = None,
|
|
reasoning: ReasoningClientProvider | None = None,
|
|
run_factory: RunRecordFactory | None = None,
|
|
accounting: TurnAccounting | None = None,
|
|
error_reporter: ErrorReporter | None = None,
|
|
gather_enabled: bool = False,
|
|
confirm_fn: ConfirmFn | None = None,
|
|
is_tty: bool | None = None,
|
|
tool_hooks: ToolExecutionHooks | None = None,
|
|
) -> None:
|
|
self._tools = tools
|
|
self._store: SessionStore = session if session is not None else InMemorySessionStore()
|
|
self._output: OutputSink = output if output is not None else BufferOutputSink()
|
|
self._prompts: PromptContextProvider = (
|
|
prompts
|
|
if prompts is not None
|
|
else (
|
|
DefaultPromptContextProvider(self._store)
|
|
if supports_default_prompt_context(self._store)
|
|
else EmptyPromptContextProvider()
|
|
)
|
|
)
|
|
self._reasoning = reasoning if reasoning is not None else StaticReasoningClientProvider()
|
|
self._run_factory = run_factory if run_factory is not None else SimpleRunRecordFactory()
|
|
# None here defers to a per-message default in dispatch(): DefaultTurnAccounting
|
|
# needs the message, so it cannot be resolved once at construction.
|
|
self._accounting = accounting
|
|
self._error_reporter = error_reporter if error_reporter is not None else NoopErrorReporter()
|
|
self._gather_enabled = gather_enabled
|
|
self._confirm_fn = confirm_fn
|
|
self._is_tty = is_tty
|
|
self._tool_hooks = tool_hooks
|
|
|
|
def _accounting_for(self, message: str) -> TurnAccounting:
|
|
if self._accounting is not None:
|
|
return self._accounting
|
|
if hasattr(self._store, "storage"):
|
|
return DefaultTurnAccounting(self._store, message)
|
|
return NoopTurnAccounting()
|
|
|
|
def dispatch(self, message: str) -> ShellTurnResult:
|
|
"""Run one full turn for ``message`` and return the :class:`ShellTurnResult`."""
|
|
accounting = self._accounting_for(message)
|
|
|
|
def execute_actions(
|
|
text: str,
|
|
*,
|
|
confirm_fn: ConfirmFn | None = None,
|
|
is_tty: bool | None = None,
|
|
turn_plan: TurnPlan | None = None,
|
|
) -> ToolCallingTurnResult:
|
|
return run_action_agent_turn(
|
|
text,
|
|
self._store,
|
|
output=self._output,
|
|
tools=self._tools,
|
|
confirm_fn=confirm_fn,
|
|
is_tty=is_tty,
|
|
turn_plan=turn_plan,
|
|
error_reporter=self._error_reporter,
|
|
tool_hooks=self._tool_hooks,
|
|
)
|
|
|
|
def answer(text: str, **kwargs: object) -> object:
|
|
return stream_answer(
|
|
text,
|
|
self._store,
|
|
self._output,
|
|
prompts=self._prompts,
|
|
reasoning=self._reasoning,
|
|
run_factory=self._run_factory,
|
|
error_reporter=self._error_reporter,
|
|
**kwargs, # type: ignore[arg-type]
|
|
)
|
|
|
|
def gather(
|
|
text: str,
|
|
*,
|
|
is_tty: bool | None = None,
|
|
turn_plan: TurnPlan | None = None,
|
|
) -> str | None:
|
|
if not self._gather_enabled:
|
|
return None
|
|
resolved = turn_plan.resolved_integrations if turn_plan is not None else None
|
|
return gather_tool_evidence(
|
|
text,
|
|
self._store,
|
|
error_reporter=self._error_reporter,
|
|
is_tty=is_tty,
|
|
resolved_integrations=resolved,
|
|
)
|
|
|
|
return run_turn(
|
|
message,
|
|
self._store,
|
|
execute_actions=execute_actions,
|
|
answer=answer,
|
|
gather=gather,
|
|
accounting=accounting,
|
|
confirm_fn=self._confirm_fn,
|
|
is_tty=self._is_tty,
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"BufferOutputSink",
|
|
"EmptyPromptContextProvider",
|
|
"HeadlessAgent",
|
|
"InMemorySessionStore",
|
|
"NoopErrorReporter",
|
|
"NoopTurnAccounting",
|
|
"NullToolProvider",
|
|
"SimpleRunRecord",
|
|
"SimpleRunRecordFactory",
|
|
"StaticReasoningClientProvider",
|
|
]
|