Skip to content
GitHubLogin

Agent SDK Core Concepts

The core concepts of the Agent SDK.

One benefit to the 8080 Agent SDK is that many agentic use-cases can be implemented despite not having an explicit API for it. To do so, the 8080 Agent SDK gives you the building blocks to drive the agent, and lets you call the building blocks yourself with any custom logic in between.

By adding your own logic between the building blocks powering the agent, you can easily do the following despite the 8080 Agent SDK not providing an explicit API for it:

  • Implement additional guardrails
  • Human-in-the-loop approvals
  • Store the state in a datastore
  • Use any protocol with the SDK

To understand how the building blocks work, and how you can get in between the building blocks, you just need to understand the concept of the “state.” The Agent SDK centers around the idea of the “state.” The state is modified and transferred between by the three main building blocks to run an agent:

  • The user: takes user input and adds it to the state
  • The LLM: calls the LLM given an input state
  • The tools: updates the state if the LLM calls a tool

To help understand how the state works, here are some simple examples as to how the state gets passed between the components, and how the components modify the state.

The state is defined as a Python object. To start, we can define the agent with its instructions and tools.

from e80_sdk.agents.data import Agent, State
State(
agent=Agent(
name="Comedian Agent",
model="openrouter/qwen/qwen3-32b",
instructions="You are a comedian",
tools=[],
),
message_history=[]
)

The user modifies the state to add a user message:

State(
# ... same as above.
message_history=[
# The user building block takes input from the user and adds it as a message.
Message(role="user", message="Tell me a joke about the economy.")
]
)

The user passes this state to the LLM, which looks at the message history in the state, calls the LLM and modifies the state accordingly:

State(
# ... same as above.
message_history=[
Message(role="user", ...),
# The LLM building block adds a message to the message history.
Message(role="assistant", message="This is the message from the LLM"),
]
)

A more involved example shows how tool calling happens, and how agent handoffs are implemented as a state change.

To start, we define our agent as such:

from e80_sdk.agents.data import Agent, State, AgentTool
State(
agent=Agent(
name="Encyclopedia Agent",
model="openrouter/qwen/qwen3-32b",
instructions="You answer questions about the world by delegating to the expert agents available to you.",
tools=[
# Normally, you can build all this using `DelegateAgentToolHandler`
# instead of typing all this out.
AgentTool(
name="delegate_to_animal_agent",
description="Call this agent if the user is asking about animals.",
# These parameters will be filled in by the LLM
parameters=[
ToolParameter(name="instructions", arg_type="string", description="...")
],
# This key defines how the tool is handled by the Toolbox.
# It is not important for this example.
handler="...",
# The handler params can be anything, and are used by the tool
# handler to produce the state modification.
handler_params={
"agent": Agent(
name="Animal Agent",
model="openrouter/qwen/qwen3-32b",
instructions="You answer questions about animals."
tools=[]
).model_dump_json()
}
)
],
),
message_history=[]
)

As with the simple example, the user takes the user message and adds it to the state:

State(
# ... same as above.
message_history=[
# The user building block takes input from the user and adds it as a message.
Message(role="user", message="Tell me something about cheetahs.")
]
)

The user passes this state to the LLM, which calls the LLM with the user’s message. The LLM decides to call a tool and modifies the state as such:

State(
# ... same as above.
message_history=[
Message(role="user", message="..."),
Message(role="assistant", tool_calls=[
ToolCalled(
id="...",
name="delegate_to_animal_agent",
arguments="{\"instructions\": \"Give a cool fact about cheetahs\"}
)
])
]
)

Because the last message contains a tool call, you can route this message to the tool, which automatically runs the correct handler for you and returns the modified state:

State(
# ... same as above.
message_history=[
Message(role="user", message="..."),
Message(role="assistant", tool_calls=[
ToolCalled(
id="...",
name="delegate_to_animal_agent",
arguments="{\"instructions\": \"Give a cool fact about cheetahs\"}
)
]),
Message(role="tool", tool_call_id="...", content="Here's a cool fact about cheetahs...")
]
)

Which can then be passed back to the LLM component and continues on.

Note that the tool handlers can do whatever they want to the State. This includes:

  • Compacting the message history and/or pruning messages
  • Changing the Agent
  • Starting a completely new State

In practice, these APIs correspond to the three building blocks:

  • User: defined by you using whatever protocol (like a web framework, or even input(...))
  • LLM: e80_sdk.agents.llm.llm_async and sync and streaming variants.
  • Tool: e80_sdk.agents.toolbox.Toolbox and e80_sdk.agents.harness.merge_tool_results

An example implementation can look like such:

from e80_sdk.agents.llm import llm_async
from e80_sdk.agents.toolbox import Toolbox
from e80_sdk.agents.harness import merge_tool_results
from e80_sdk.agents.data import Agent, State, AgentTool
# The user component, get a user message and add it to the state
# You need to implement the mechanism to get the user's input.
working_state = State(
agent=Agent(...),
message_history=[
Message(
role="user",
message="...", # Get the user input and set the message here
)
]
)
# After receiving the user message, you can run any guardrails on the input
# here.
while True:
# The LLM component, calls the LLM and adds the result to the state
llm_result = await llm_async(
agent=working_state.agent, messages=working_state.message_history
)
working_state.message_history.append(llm_result.message)
# You can also run any guardrails on the LLM output here.
# Note that nothing is stopping you from defining and handling tool calls
# outside of the Toolbox. You can do that if you wish here.
if llm_result.message.tool_calls is not None:
# If you want to get any human-in-the-loop approvals,
# you can add the logic here.
# The tool component.
# Handles the tool calls for you.
new_state, new_usage = merge_tool_results(
working_state,
[
await toolbox.run_toolbox_async(working_state, tool_called=tc)
for tc in llm_result.message.tool_calls
],
)
working_state = new_state
else:
# Any filtering of the response can happen here.
print("Response:", llm_result.message)
break