Getting Started
This guide shows how to install and start using Protolink.
The following article on 📝 level-up coding on medium also gives a hands-on guide and overview of Protolink.
Installation
Protolink is published on PyPI and can be installed with either uv (recommended) or pip.
Basic Installation
This installs the base package without optional extras:
# Using uv (recommended)
uv add protolink
# Using pip
pip install protolink
Optional Dependencies
Protolink exposes several extras to enable additional functionality:
- uv
- pip
- Source checkout
# Install with all optional dependencies
uv add "protolink[all]"
# HTTP support (for web-based agents)
uv add "protolink[http]"
# One LLM provider (choose the one you use)
uv add "protolink[openai]"
# All supported LLM libraries, including local llama.cpp
uv add "protolink[llms]"
# Optional local PDF ingestion for RAG
uv add "protolink[rag-pdf]"
# YAML agent configuration
uv add "protolink[yaml]"
# Install with all optional dependencies
pip install "protolink[all]"
# HTTP support (for web-based agents)
pip install "protolink[http]"
# One LLM provider (choose the one you use)
pip install "protolink[openai]"
# All supported LLM libraries, including local llama.cpp
pip install "protolink[llms]"
# Optional local PDF ingestion for RAG
pip install "protolink[rag-pdf]"
# YAML agent configuration
pip install "protolink[yaml]"
git clone https://github.com/nmaroulis/protolink.git
cd protolink
# Editable install for local development
uv sync --locked
Choose the extras that match your use case, such as protolink[openai], protolink[anthropic], or protolink[ollama]. Combine them with a transport when needed: protolink[http,openai]. The existing protolink[llms] bundle still installs every supported LLM SDK, including local llama.cpp bindings. See LLMs for the full provider list. Dependency-free in-memory and SQLite RAG are part of the base package; protolink[rag-pdf] is needed only when the built-in loader reads local PDF files.
For development, the source-checkout command installs the locked test and tooling groups.
Add a provider only when needed, for example uv sync --locked --extra openai.
The existing protolink[dev] extra remains available as the full installation.
Even the smallest ProtoLink agent uses the A2A model: AgentCard declares identity and capabilities, while Task, Message, Part and Artifact carry work and results. transport="http" serves ProtoLink's native API by default. Add a2a=True when the agent should also expose and consume the A2A 1.0 JSON-RPC boundary. This is additive: native endpoints and native peer calls remain available. See A2A Core and 1.0 Compatibility for the exact scope.
First Agent
Save this as agent.py and run python agent.py. It uses only the base package:
from protolink import Agent
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
agent = Agent(name="calculator", tools=[add])
print(agent.sync.call_tool("add", a=2, b=3)) # 5
No model, API key, server process, or network connection is needed. Registration infers the tool name from add, the description from its cleaned docstring and the schemas from its type hints. Both synchronous and asynchronous functions work. @agent.tool, @agent.tool() and explicit metadata such as @agent.tool(name="sum", description="Add two numbers") are also supported.
The agent gets a generated card with description "Agent calculator" and URL
runtime://calculator. No transport is created unless you request one. Add
transport="runtime" for communication within the same process, using a unique
name for each endpoint. Use description= and url= to customize the generated
identity, or pass an explicit card= instead.
For an existing function, call agent.add_tool(add). The same call works on another agent and synchronous functions, asynchronous functions, bound methods and callable objects all use the same metadata inference. Use Tool.from_callable(add, ...) when you need custom schemas, tags, or permission metadata; see Native Tools.
call_tool() validates arguments, applies the Agent's policy and approval rules and returns the raw result. Calling add(2, 3) remains an ordinary Python call and bypasses those Agent controls. Registration never executes the function.
Keep the complete task
Use run_task() when you need lifecycle state, artifacts, run metadata, cancellation by task ID, or configured run storage:
from protolink import Task, TaskExecutionError
task = Task.create_tool_call(tool_name="add", args={"a": 2, "b": 3})
result = agent.sync.run_task(task)
try:
result.raise_for_status()
except TaskExecutionError as exc:
print(exc.task.state.value)
else:
print(result.state.value) # completed
print(result.get_last_part_content().result) # 5
run_task() returns the complete task, including a returned failed or canceled state. raise_for_status() returns the same task or raises TaskExecutionError for those two states; it does not wait for completion. Exceptions raised directly during execution still propagate with their original types. Direct call_tool() errors also propagate unchanged.
Add inference
invoke() lets an LLM answer or choose tools. Try the API with the provider-free mock first:
from protolink import create_llm
agent.llm = create_llm("mock", default_response="Hello from ProtoLink")
print(agent.sync.invoke("Say hello")) # Hello from ProtoLink
Replace the mock with a configured LLM backend for real inference. An LLM may require an installed SDK, a running local model server, or provider credentials; direct tool calls require none of them.
Provider aliases and provider:model strings also work directly:
# Requires protolink[ollama] and the model available on your Ollama server.
agent = Agent(name="calculator", llm="ollama:qwen3:4b", tools=[add])
Strings use provider defaults, including credentials from the environment when supported. Pass a configured LLM object for custom parameters or server URLs.
invoke() returns the final part's content and raises TaskExecutionError when its task is failed or canceled. It preserves structured results such as ToolOutput in explicit tool-call mode. Prefer call_tool() when you already know the tool and want its raw return value, or run_task() when your application needs the full task. ask() adds deterministic retrieval before inference and returns a RAGAnswer; see RAG.
Conversation memory is opt-in with state=["conversation"]. Supply an explicit session_id for each conversation in invoke() and ask(), especially when serving multiple users. Their convenience defaults reuse a shared session per method; they do not create an isolated conversation for every call.
Async applications and notebooks
The .sync facade is for blocking scripts. In an async function or a notebook with an active event loop, await the same methods directly:
result = await agent.call_tool("add", a=2, b=3)
response = await agent.invoke("Say hello", session_id="conversation-123")
Calling .sync from an active event loop raises an actionable RuntimeError before a coroutine is created.
Serve the agent over HTTP
Install the HTTP extra with uv add "protolink[http]", then create a service:
from protolink import Agent
agent = Agent(
name="calculator",
description="Adds numbers",
url="http://127.0.0.1:8020",
transport="http",
)
@agent.tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
agent.start()
start() runs the service until stopped. The same tool is now available to peers through the native task API. Add a2a=True for the supported A2A 1.0 boundary, or add an LLM for inference and browser chat. Registry discovery, built-in tools and MCP tools can be attached independently as your application grows.
Network aliases require an explicit URL with a compatible scheme and bind port.
Omitting url with transport="http" raises an actionable ValueError. A
configured HTTPTransport(url=...) can supply that URL instead. See
Progressive control for public URLs, TLS and explicit cards.
Using the CLI
Create a runnable one-file starter agent:
protolink init agent
uv run python agent.py
The default starter uses the top-level API:
from protolink import Agent, AgentCard, LocalTraceTelemetry, Task, create_llm
It runs locally without an API key by executing a tool call directly. If OPENAI_API_KEY is set, it also enables LLM inference. Use --template tool for a tool-only starter or --force to overwrite an existing file.