Server
Servers in Protolink act as the coordination layer between business logic (Agents or Registries) and the underlying Transport mechanism. They are responsible for wiring endpoints, managing lifecycle, and ensuring that the core logic remains transport-agnostic.
AgentServer always binds ProtoLink's native endpoints. When an HTTP agent is created with a2a=True, it additionally binds the A2A 1.0 adapter to the same execution logic. Agent authors still implement handle_task(Task) once.
The endpoint declaration layer that binds Agent and Registry behavior to whatever transport backend is running the process.
protolink.server/tasks//agents/EndpointSpecstart() / stop()Concept
A Server does not implement networking itself. Instead, it:
- Defines Endpoints: Declares the API surface (paths, methods, handlers).
- Binds Handlers: Connects these endpoints to the implementation (Agent or Registry).
- Manages Lifecycle: Starts and stops the underlying transport.
This separation allows an Agent to run over HTTP, WebSocket, or in-memory transports without changing a single line of agent code.
AgentServer
The AgentServer exposes an Agent over a Transport.
Responsibilities
- Exposing the Task submission endpoint.
- Exposing the Task streaming endpoint when the transport supports streaming.
- Serving the Agent's identity card (
/.well-known/agent.json). - On HTTP with
a2a=True, serving the A2A 1.0 Agent Card and JSON-RPC adapter. - Providing a status page.
- Exposing the Chat Gateway when the agent has an LLM.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/tasks/ | POST | Task Submission. Accepts a Task object, processes it via the Agent, and returns the result. |
/tasks/cancel | POST | Task Cancellation. Parses a TaskCancellationRequest and asks the Agent to cancel active work. |
/tasks/stream | POST | Task Streaming. Accepts a Task object and streams status, LLM, tool, artifact, and final events. Only registered when the transport has supports_streaming=True. |
/llm/history/compact | POST | History Compaction. Runs the Agent's explicit conversation-compaction control plane. |
/state/describe | POST | State Description. Reports enabled persistent state stores. |
/state/reset | POST | State Reset. Clears selected persistent state stores after policy authorization. |
/state/compact | POST | State Compaction. Compacts selected persistent state stores after policy authorization. |
/.well-known/agent.json | GET | Agent Discovery. Returns the AgentCard describing this agent. |
/.well-known/agent-card.json | GET | A2A 1.0 Agent Card. Added only by Agent(..., transport="http", a2a=True); returns the standard wire card. |
/ | POST | A2A 1.0 JSON-RPC. Added only with a2a=True; handles SendMessage, GetTask, ListTasks, and CancelTask. |
/status | GET | Status Page. Returns a human-readable HTML status dashboard. |
/healthz | GET | Health. Returns the underlying transport's health snapshot. |
/readyz | GET | Readiness. Currently calls the same transport health method as /healthz. |
/chat | GET | Chat Page. Returns a self-contained HTML/CSS/JS chat interface. Always registered; shows a fallback message if no LLM is configured. |
/chat | POST | Chat Message. Accepts {"message": "...", "session_id": "..."} and returns the agent's response. Only registered when the agent has an LLM. |
The two A2A 1.0 routes require a transport whose transport_type is "http"
and a2a=True. With the default False, HTTP retains the native endpoint
contract and does not expose /.well-known/agent-card.json or POST /. Other
transports are not presented as A2A 1.0 wire bindings. See
A2A Core and 1.0 Compatibility for the implemented scope and TCK
evidence.
Usage
The Agent class automatically creates an AgentServer internally when a transport is provided. You rarely need to instantiate AgentServer or wire the A2A adapter directly.
# AgentServer is created internally; A2A routes are explicit and additive.
agent = Agent(card=card, transport="http", a2a=True)
agent.start()
RegistryServer
The RegistryServer exposes a Registry over a Transport.
Responsibilities
- Handling agent registration and deregistration.
- Serving the discovery endpoint for finding agents.
- Providing a status page.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/agents/ | POST | Register. Registers an agent with the registry. Body: AgentCard. |
/agents/ | DELETE | Unregister. Removes an agent. Body: {"agent_url": "..."}. |
/agents/heartbeat | POST | Heartbeat. Refreshes liveness for one agent URL. |
/agents/ | GET | Discover. Returns agents matching direct query filters such as ?name=worker&role=worker. |
/status | GET | Status Page. Returns a human-readable HTML status dashboard. |
/healthz | GET | Health. Returns the underlying transport's health snapshot. |
/readyz | GET | Readiness. Currently calls the same transport health method as /healthz. |
Usage
from protolink.discovery.registry import Registry
from protolink.transport import HTTPTransport
from protolink.server.registry import RegistryServer
# Create logic and transport
transport = HTTPTransport(url="http://localhost:8000")
registry = Registry(transport=transport)
# Create Server (wiring)
server = RegistryServer(registry, transport)
# Start
await server.start()
Architecture
The server architecture relies on the EndpointSpec model to define routes in a generic way.
EndpointSpec
The EndpointSpec class (defined in protolink.server.endpoint_handler) is the contract between a Server and a Transport.
@dataclass(frozen=True)
class EndpointSpec:
name: str # Internal unique name for the endpoint
path: str # URL path (e.g. "/tasks/")
method: HttpMethod # HTTP Method (GET, POST, etc.)
handler: Callable # Async function to process the request
# Configuration
content_type: Literal["json", "html"] = "json"
streaming: bool = False
mode: Literal["request_response", "stream"] = "request_response"
# Request Parsing
request_parser: Callable[[Any], Any] | None = None
request_source: RequestSourceType = "none"
How it Works
- Transport-Agnostic Definition: The Server creates a list of
EndpointSpecobjects describing what it needs to expose. - Transport Implementation: The Transport iterates over these specs and registers them with its underlying web framework (e.g., Starlette or FastAPI).
- Request Handling:
- The Transport receives a raw HTTP request.
- It extracts data based on
request_source(e.g., reads the body JSON). - It passes this data to the
request_parser(if defined) to convert it into a domain object. - It calls the
handlerwith the domain object. - For request/response endpoints, it serializes the result back to the wire format.
- For streaming endpoints, it iterates the handler and serializes each event until a final event closes the stream.
This design ensures that your Agent logic deals only with Task and Message objects, never raw HTTP requests, while the Transport handles the nitty-gritty of networking protocols.
Server API reference
The two server classes are the public package surface. EndpointSpec and
EndpointRequest live in protolink.server.endpoint_handler; they are included
here because custom transports and protocol adapters need the same declarative
route contract even though those types are not re-exported from
protolink.server.
AgentInterface server protocol
class AgentInterface(Protocol)Describe the structural surface that AgentServer calls. An object does not
need to inherit from this protocol; it only needs to provide compatible
attributes and methods.
Required surface
cardAgentCardPublic identity and capabilities.
handle_task / run_taskasync (Task) -> TaskBusiness handler and its cancellation-aware execution wrapper.
handle_task_streaming / run_task_streaming(Task) -> AsyncIterator[Any]Business event stream and its cancellation-aware wrapper.
cancel_taskasync (TaskCancellationRequest) -> TaskActive-task cancellation control.
compact_historyasync (HistoryCompactionRequest) -> HistoryCompactionResultExplicit LLM history compaction.
describe_state / reset_state / compact_stateasync (StateOperationRequest) -> StateOperationResultPersistent-state control-plane handlers.
get_agent_card(*, as_json: bool = True) -> AgentCard | dict[str, Any]Native discovery document.
get_status(output_format: "html" | "json" = "html") -> strRender Agent status as HTML or, for
"json", the current Pythonstr(card.to_dict())representation. That branch is not guaranteed to be valid JSON.get_chat() -> strRender the HTML chat page.
handle_chat_messageasync (dict[str, Any]) -> dict[str, str]Chat message endpoint handler.
This internal server protocol is not re-exported from
protolink.server. The public
protolink.AgentInterface name refers instead to the Agent Card
interface dataclass containing a URL, transport, and protocol version.
AgentServer
class AgentServer(
transport: Transport,
agent: AgentInterface,
*,
a2a: bool = False,
)Bind an Agent-compatible object to a configured transport. Construction records
the collaborators and validates A2A compatibility; endpoint registration and
network startup are deferred until start().
Parameters
transportTransportrequiredConcrete transport that receives endpoint specifications and owns the listening lifecycle. Passing
Noneis rejected immediately.agentAgentInterfacerequiredStructurally compatible object implementing task execution, streaming, cancellation, history/state controls, card/status/chat rendering, and chat message handling. Runtime inheritance from the protocol is not required.
a2abooldefault: FalseBuild an A2A 1.0 JSON-RPC adapter and its two additional endpoints. When enabled,
transport.transport_typemust equal"http".
Raises
ValueErrorRaised when no transport is supplied, or when
a2a=Trueis paired with a non-HTTP transport type.adapter construction errorA2A adapter initialization errors propagate when A2A support is enabled.
Constructing the server does not call setup_routes(). Route
declarations are built during the first successful start().
AgentServer.start
await start() -> NoneBuild the complete endpoint table, pass it to the transport, and await the transport's server startup.
Returns
NoneNoneThe server records itself as running only after
transport.start()completes successfully.
Side effects
native routesEndpointSpec[]Registers task submission/cancellation, history and state controls, card, status, health/readiness, and chat-page endpoints.
task streamconditional routeAdds
POST /tasks/streamonly when the transport advertisessupports_streaming=True.chat messageconditional routeAdds
POST /chatwhen the Agent has anllm, or its card capabilities advertisehas_llm=True.A2A routesconditional routesAdds the standard Agent Card and root JSON-RPC routes when the server owns an A2A adapter.
Raises
route or transport errorExceptions from endpoint construction,
setup_routes(), ortransport.start()propagate. The running flag remainsFalseif startup does not complete.
Calling start() again while the server is running is a no-op.
A stop-then-start cycle builds and submits the routes again; whether duplicate
route registration is accepted depends on the transport implementation.
AgentServer.stop
await stop() -> NoneStop the underlying transport and close the optional A2A adapter.
Returns
NoneNoneReturns immediately when the server is not marked as running.
Shutdown order
1transportAwait
transport.stop().2A2A adapterAwait adapter closure when A2A support was enabled.
3server stateMark the server idle after both preceding operations complete.
Raises
shutdown errorTransport and A2A closure exceptions propagate. If either operation fails, the running flag is not cleared by the current implementation.
RegistryInterface server protocol
class RegistryInterface(Protocol)Describe the structural registry behavior consumed by RegistryServer.
Required surface
handle_registerasync (AgentCard) -> dict[str, str]Store or replace one agent card.
handle_unregisterasync (agent_url: str) -> dict[str, str]Remove one URL.
handle_heartbeatasync (agent_url: str) -> dict[str, str]Refresh liveness metadata.
handle_discoverasync (filter_by: dict[str, Any] | None = None) -> list[dict[str, Any]] | list[AgentCard]Return cards matching optional filters.
handle_status_html() -> strRender the registry status page.
The protocol is a server-module implementation detail and is not re-exported.
Ordinary applications pass a Registry instance rather than
implementing it directly.
RegistryServer
class RegistryServer(
registry: RegistryInterface,
transport: Transport,
)Bind a Registry-compatible object to its transport endpoint table. As with
AgentServer, the registry object supplies business behavior and the transport
supplies route binding and networking.
Parameters
registryRegistryInterfacerequiredStructurally compatible object implementing register, unregister, heartbeat, discovery, and HTML status handlers.
transportTransportrequiredConcrete route-binding and server-lifecycle implementation.
Raises
ValueErrorRaised when
transportisNone. The registry argument is stored without runtime validation.
RegistryServer.register_parser
await register_parser(
request: Any,
) -> AgentCardConvert an inbound registration body into ProtoLink's runtime AgentCard.
Parameters
requestAnyrequiredValue forwarded directly to
AgentCard.from_dict(). Normal transport usage supplies a decoded mapping.
Returns
cardAgentCardNormalized agent identity and capability model passed to the registry's registration handler.
Raises
deserialization errorMapping-shape, required-field, and nested model errors from
AgentCard.from_dict()propagate.
RegistryServer.unregister_parser
await unregister_parser(
request: Any,
) -> strRead the agent_url value from an unregistration request body.
Parameters
requestAnyrequiredMapping-like object expected to provide
.get("agent_url").
Returns
agent_urlstrURL forwarded to
handle_unregister(). Despite the annotation, a missing key currently producesNone.
Raises
AttributeErrorRaised when the request has no compatible
.get()method.
RegistryServer.heartbeat_parser
await heartbeat_parser(
request: Any,
) -> strRead the agent_url value from a heartbeat body.
Parameters
requestAnyrequiredMapping-like object expected to provide
.get("agent_url").
Returns
agent_urlstrURL forwarded to
handle_heartbeat(). A missing key currently yieldsNonedespite the declared return type.
Raises
AttributeErrorRaised when the request has no compatible
.get()method.
RegistryServer.discover_parser
await discover_parser(
request: Any,
) -> dict[str, Any] | NoneNormalize discovery query data into the filter mapping expected by the registry handler.
Parameters
requestAnyrequiredDecoded query mapping. A wrapper key named
filter_byis unwrapped when present.
Returns
filter_bydict[str, Any] | NoneNonefor a non-dictionary request; the nestedfilter_byvalue when that key exists; otherwise the request dictionary itself.
A nested filter_by value is returned without checking that it is
a dictionary. Normal RegistryClient discovery sends filter keys
directly, such as ?name=worker; a raw
?filter_by={...} query value is only a string and is
not JSON-decoded by this parser.
RegistryServer.start
await start() -> NoneRegister all registry, status, health, and readiness routes, then await transport startup.
Returns
NoneNoneThe running flag is set only after the transport starts successfully. Repeated calls while running return immediately.
Raises
route or transport errorExceptions from
setup_routes()ortransport.start()propagate.
RegistryServer.stop
await stop() -> NoneAwait transport shutdown and mark the registry server idle.
Returns
NoneNoneReturns immediately when the server is not running.
Raises
transport errorShutdown exceptions propagate, and the running flag remains set when transport shutdown fails.
Endpoint declaration reference
EndpointSpec
class EndpointSpec(
name: str,
path: str,
method: HttpMethod,
handler: Callable[..., Any],
content_type: Literal["json", "html"] = "json",
streaming: bool = False,
mode: Literal["request_response", "stream"] = "request_response",
request_parser: Callable[[Any], Any] | None = None,
request_source: RequestSourceType = "none",
)Describe one transport-neutral route. Server classes create these immutable declarations; each transport interprets their path, extraction, parsing, invocation, serialization, and streaming fields for its own backend.
Parameters
namestrrequiredLogical route identifier. EndpointSpec itself does not enforce uniqueness.
pathstrrequiredProtocol-facing route path, conventionally beginning with
/. No syntax validation is performed by the dataclass.method"GET" | "POST" | "DELETE" | "PUT" | "PATCH"requiredHTTP-style operation used by applicable transports.
handlerCallable[..., Any]requiredSynchronous function, coroutine function, or streaming callable invoked after extraction and optional parsing.
content_type"json" | "html"default: "json"Response rendering mode interpreted by the HTTP/ASGI backends. WebSocket, gRPC, and Runtime transports serialize returned string values through their normal protocol envelope instead.
streamingbooldefault: FalseSignals that the handler returns an asynchronous stream rather than one response value.
mode"request_response" | "stream"default: "request_response"Explicit execution mode consumed by transports. It is separate from
streaming; the dataclass does not require the two fields to agree.request_parserCallable[[Any], Any] | Nonedefault: NoneOptional transformer applied to the extracted request value before handler invocation. Transports may support synchronous and asynchronous parser results.
request_sourceRequestSourceTypedefault: "none"Select
none,body,query_params,form,headers,path_params, orrequest. The last form supplies anEndpointRequestcontaining several request facets.
Attribute reassignment is blocked by the frozen dataclass, but contained callables and other referenced values remain mutable. Construction does not validate paths, method strings at runtime, handler callability, unique names, or streaming-field consistency.
The shared type includes form, but current built-in transports do
not extract form data. HTTP backends support body, query, headers, path
parameters, and the combined request view; WebSocket, gRPC, and Runtime
currently bind body and query-parameter sources.
EndpointRequest
class EndpointRequest(
body: Any = None,
query_params: Mapping[str, str] = field(default_factory=dict),
path_params: Mapping[str, str] = field(default_factory=dict),
headers: Mapping[str, str] = field(default_factory=dict),
method: str = "",
url: str = "",
principal_id: str | None = None,
)Provide a small, framework-independent view of an inbound HTTP-style request. It is used by endpoints whose adapters need more than one extracted source, such as A2A JSON-RPC requests that also need authenticated-principal information.
Parameters
bodyAnydefault: NoneDecoded or raw body value supplied by the transport.
query_paramsMapping[str, str]default: {}Query parameter view. A new dictionary is used when omitted.
path_paramsMapping[str, str]default: {}Route parameter view. A new dictionary is used when omitted.
headersMapping[str, str]default: {}Request header view. Header normalization depends on the transport.
methodstrdefault: ""Incoming protocol method.
urlstrdefault: ""Incoming request URL as supplied by the transport.
principal_idstr | Nonedefault: NoneAuthenticated principal propagated by the transport when available.
The dataclass is frozen and slot-based, but mappings supplied by the caller are not copied or wrapped. Their contents can still change after the request object is constructed.