Agents
This page documents the lower-level addressed
agentlane.harness.Agent.
For the higher-level local agent interface that exposes run(...) directly, see
Harness Default Agents.
This lower-level harness Agent is the type you bind to a runtime and address
by AgentId. It is where a long-lived agent definition meets one concrete
conversation. It keeps static configuration in
AgentDescriptor, persists the
conversation as RunState, and hands
execution to any bound shims and then to the
Runner.
That split is what makes an addressed agent feel stable across turns. The descriptor says what kind of agent this is. The run state says where one concrete interaction currently stands.
AgentDescriptor.name is always populated. Provide one when the application
has a stable human-readable name for logs, tracing, or tool correlation. If it
is omitted or blank, the framework generates a Faker-backed adjective-noun
fallback name, such as deterministic-butterfly, at descriptor construction so
the runner does not need defensive name handling later.
What The Lower-Level Agent Owns
- the descriptor that defines instructions, tools, schema, handoffs, and optional shims
- the lifecycle that queues later inputs while one run is active
- any bound shims declared by the descriptor
- the runner that executes each run
The runner does not own long-lived agent state. The lifecycle does not own the model loop. The agent is the place where those pieces meet.
Input And Recovery
The public input surface is intentionally small. This lower-level harness agent accepts:
- a
strfor a normal user turn - a list of supported run-history items for richer multi-item input
- a
RunStatewhen resuming an existing conversation
That is enough to support normal chat-like turns, richer prompt input, and recovery after a restart.
Supported run-history items include:
- canonical message dicts,
- prior
ModelResponsevalues, PromptSpecvalues, and- user-side content values such as strings, JSON-like values, or Pydantic models.
When you ask an agent for its current run state through
Agent.run_state, you get a snapshot
of the resumable state for that concrete agent instance.
List input can contain run-scoped context. Put an ephemeral message after the user input:
from agentlane.models import internal_message
view = internal_message(
{"role": "user", "content": "The user views report.csv."},
origin="ui_view",
ephemeral=True,
)
result = await agent.run(["Summarize this file.", view])
The model receives the message on each model call of that run. When the run
ends, the agent commits the state without it. The persisted baseline never
contains ephemeral messages. If you pass a RunState with ephemeral messages
to the agent constructor, the agent removes them from that baseline. A
RunState that you pass as run input starts a run, so that run keeps its
ephemeral messages.
Tools And Handoffs
An agent’s model-visible tool surface is built from three sources:
- direct descriptor tools resolved from
AgentDescriptor.tools - tools contributed by bound shims such as
HarnessToolsShim - predefined handoff targets and an optional generic handoff path
That design matters because handoffs are intentionally model-visible. From the model’s point of view, a handoff looks like a tool choice. From the framework’s point of view, it is a transfer of control to another agent.
The same distinction shows up in agent-as-tool usage. When an agent is exposed
through Agent.as_tool(...) or AgentDescriptor.as_tool(...), the model sees a
tool schema, but the framework still executes that tool call by routing work to
another agent run.
The base agent tool is the generic spawned-helper version of agent-as-tool.
It lets the model create a fresh helper for a delegated task without inheriting
the parent conversation or parent system prompt. The helper name is used for
logging and tracing; the task text is the instruction the helper receives.
That generic helper tool is
DefaultAgentTool, exported from
agentlane.harness. It defaults to the name agent and lets you configure the
helper’s description, instructions, model, model_args, output_schema,
and tools. It is normally wired through the tools shim rather than constructed
directly: base_harness_tools(...) (via HarnessToolsShim) contributes it as a
model-visible tool with prompt guidance.
The generic handoff path is configured with
DefaultHandoff, also exported from
agentlane.harness, and enabled through AgentDescriptor.default_handoff. It
defaults to the name handoff and exposes description, instructions,
model, model_args, schema, and tools for the spawned transfer agent.
from agentlane.harness import AgentDescriptor, DefaultHandoff
descriptor = AgentDescriptor(
name="Care Navigation",
model=model,
instructions="You are a patient care navigation agent.",
default_handoff=DefaultHandoff(
instructions="You are a focused follow-up agent.",
),
)
The configured handoff is surfaced at runtime through the
Agent.default_handoff property.
Default spawned helpers inherit two things through different paths. Direct
descriptor tools are resolved by ToolConfig: INHERIT_TOOLS keeps parent
tools, OVERRIDE_TOOLS replaces them, and RESTRICT_TOOLS.only(...) filters
them by name before adding child-local tools. Descriptor shims are inherited as
shim definitions and then bind for the child agent. If those inherited shims
include HarnessToolsShim(base_harness_tools(...)), the helper gets the same
configured first-party tools and prompt guidance through the normal
prepared-turn path. Those shim-contributed tools are not direct descriptor
tools, but they still become model-visible for the prepared turn.
Predefined handoffs are another category again. The model sees a handoff schema like a tool, but the runner intercepts the call and transfers control instead of executing a normal tool handler.
Concurrency Per Agent
The harness preserves the runtime rule that a single concrete AgentId is not
re-entered concurrently.
That means:
- one input may actively run
- later inputs for the same agent are queued
- queued inputs are drained in FIFO order
This is what makes resumable state practical. The framework does not need to guess how to merge multiple overlapping runs for the same agent instance.
Shims
If the descriptor includes shims, the lifecycle binds them once for the concrete agent instance before the first queued run starts.
Those bound sessions are then reused on later runs for the same addressed
agent. This allows shims to keep private in-memory state per agent instance
while still writing resumable state into the persisted ShimState stored on
RunState.shim_state when needed.
For the actual shim contract, see Harness Shims.
When To Read The Runner Docs
If you are trying to understand how tools are executed, how a handoff changes control flow, or how the next model request is built, move on to Harness Runner. The agent owns the pieces. The runner explains their behavior inside a run.