{"slug": "show-hn-protolink-a-python-runtime-for-distributed-a2a-agents", "title": "Show HN: ProtoLink – A Python runtime for distributed A2A agents", "summary": "ProtoLink, a new Python framework for building distributed, LLM-powered autonomous agents that communicate via Google's Agent-to-Agent (A2A) protocol, has been released. The framework enables developers to create multi-agent systems with minimal boilerplate, supporting tool integration, context management, and structured workflows. Its 0.5.0 update introduces Structured Flows for deterministic agent orchestration.", "body_md": "📌 The framework is currently in\n\nbetaand is subject to change.\n\nProtoLink is a lightweight Python framework that allows you to build **autonomous, LLM-powered agents** that communicate directly, manage context, and **integrate tools seamlessly**. Build **distributed multi-agent systems** with minimal boilerplate and production-oriented architecture.\n\nEach ProtoLink **agent** is a **self-contained runtime** that can embed an **LLM**, manage execution context, expose and consume **tools** (native or via [MCP](https://modelcontextprotocol.io/docs/getting-started/intro)), and coordinate with other agents over a unified **transport layer**.\n\nProtoLink implements and extends [Google’s Agent-to-Agent (A2A)](https://a2a-protocol.org/v0.3.0/specification/?utm_source=chatgpt.com) specification for **agent identity, capability declaration, and discovery**, while **going beyond A2A** by enabling **LLM & tool integration**.\n\nProtoLink is an\n\nA2A-native agent runtimefor building distributed, typed, observable agent systems. LangChain composes model calls; ProtoLink runs agents.\n\nFind more in the Protolink [whitepaper](https://nmaroulis.github.io/protolink/whitepaper).\n\nThe framework emphasizes **minimal boilerplate**, **explicit control**, and **production-readiness**, making it suitable for both research and real-world systems.\n\n**Tool calling**, **agent delegation**, **LLM invocation**, and **task execution logic** are provided by Protolink. You describe the agent, its tools, and its capabilities; ProtoLink handles the infer loop, native tool calling where available, JSON fallback for smaller models, action validation, and deterministic execution.\n\nProtolink uses **dynamic semantic context injection**, automatically enriching agent prompts with **downstream agent capabilities, tools, and communication contracts** so agents remain **fully decoupled** while adapting their behavior at **runtime** without **hardcoded integrations**.\n\nFocus on your agent logic- ProtoLink handles communication, authentication, LLM integration, and tool management for you.\n\nFollow the API documentation here 📚 [documentation](https://nmaroulis.github.io/protolink/).\n\nThe following *articles* published on * Level Up Coding* on\n\n*give a hands-on guide and overview of Protolink:*\n\n**Medium**- 📝\n[Building My Own Local “Claude Code”: What I Learned Demystifying Agentic Coding under the Hood](https://levelup.gitconnected.com/building-my-own-local-claude-code-what-i-learned-demystifying-agentic-coding-under-the-hood-8772874b91b8) - 📝\n[Your First Autonomous Agent Mesh - Easier Than You Think](https://levelup.gitconnected.com/your-first-autonomous-agent-mesh-easier-than-you-think-ce697b3dd87a) - 📝\n[Build Easily Your Own “Claude Code” with Three Agents: Brain, Hands, and Coordinator](https://medium.com/gitconnected/build-easily-your-own-claude-code-with-three-agents-brain-hands-and-coordinator-5236b392ddf0)\n\nUpdate 0.5.0 introduces **Structured Flows**, a new feature that allows you to define **structured workflows** for your agents. Building on A2A transport layer, flows allow you to define **complex workflows** for your agents that can be executed in a **structured & deterministic manner**. Technically, each flow is a deterministic `Flow.execute(Task) -> Task`\n\nstate machine over A2A `Task`\n\n, `Message`\n\n, `Artifact`\n\n, and `AgentCard`\n\nprimitives, keeping orchestration protocol-native instead of creating a separate graph runtime. Again the **Semantic Context Injection** is handled by Protolink, meaning that the agents will **automatically** have the necessary context about the flow they are executing and what to pass down to next agents in the pipeline. See more here 🔀 [flows](https://nmaroulis.github.io/protolink/flows/).\n\nIn Protolink the agent is the central component that handles all the logic and incorporates the **LLM**, **tools**, **transport layer** through **AgentClient** and **AgentServer**, the **Storage** and **OpenTelemetry** for logging.\n\nEach of these components is a separate module that can be used independently or in combination with other modules.\nEach component is **pluggable** to the agent and can be replaced with your own implementation.\n\nHere's a simple example on how **easy** it is to **create an agent**, define the neccessary **modules**, **provided by Protolink** and **plug** 🔌 them to the agent. The agent is then ready to **discover other agents** and send & receive **Tasks**.\n\n``` python\nfrom protolink.agents import Agent\n\n# 1. Initialize & start the Registry for A2A Discovery (optional)\nfrom protolink.discovery import Registry\nregistry = Registry(url=\"http://127.0.0.1:9000\", transport=\"http\")\nregistry.start(background=True)\n\n# 2. Initialize OpenAI API LLM (optional)\nfrom protolink.llms.api import OpenAILLM\nllm = OpenAILLM(model=\"gpt-5.2\") # for local model use protolink.llms.server.OllamaLLM and more...\n\n# 3. Initialize Storage (optional)\nfrom protolink.storage import SQLiteStorage\nstorage = SQLiteStorage(db_path=\"agent.db\")\n\n# 4. Initialize Telemetry (optional)\nfrom protolink.telemetry import LangfuseTelemetry\ntelemetry = LangfuseTelemetry()\n\n# 5a. Define the agent card\nagent_card = {\n    \"url\": \"http://127.0.0.1:8020\",\n    \"name\": \"example_agent\",\n    \"description\": \"A dummy agent\",\n}\n\n# 5b. Initialize the agent (http transport will be created based on the agent card url)\n# Plug the modules to the agent\nagent = Agent(card=agent_card, transport=\"http\", llm=llm, registry=registry, storage=storage, telemetry=telemetry)\n\n# 6. Add Native tool (Tools from MCP can also be added easily using the MCPToolAdapter)\n@agent.tool(name=\"add\", description=\"Add two numbers\")\nasync def add_numbers(a: int, b: int):\n    return a + b\n\n# Start the agent.\nagent.start()\n```\n\nThe agent is now fully initialized and prepared to **discover & be discovered by peers**, send & receive **tasks** across your system.\n\nRunning tasks can be canceled by task ID across local and remote transports, with final canceled state propagated through `RunContext`\n\nand streamed runtime events.\n\n**Note**: `agent.start()`\n\nautomatically **adapts** to the current environment under the hood, working seamlessly in **standard Python scripts, async applications, and Jupyter notebooks** with both **blocking** and **background** execution modes. When `background=True`\n\n, it runs the agent non-blocking using an **event loop task** or a **dedicated background thread** depending on the runtime context. See how Protolink handles [agent execution lifecycle](https://nmaroulis.github.io/protolink/agent/#lifecycle-methods).\n\n**A2A Protocol Implementation**: Fully compatible with** Google's A2A specification****Extended Capabilities**:** Unified Client/Server Agent Model**: Single agent instance handles both client and server responsibilities, reducing complexity.** Transport Layer Flexibility**: Swap between*HTTP*,*WebSocket*,*gRPC*or*in-memory*transports with minimal code changes.**Simplified Agent Creation and Registration**: Create and register** autonomous AI agents**with just a few lines of code.** LLM-Ready**Architecture: Native support for integrating LLMs to agents (APIs & local) directly as agent modules, allowing agents to expose LLM calls, reasoning functions, and chain-of-thought utilities with zero friction.**Tooling**:** Native support**for integrating tools to agents (APIs & local) directly as agent modules. Native Adapter for** MCP tooling**.** Runtime Transport Layer**: In-process agent communication using a shared memory space. Agents can easily communicate with each other within the same process, making it easier to build and test agent systems.**Enhanced Security**: Native** TLS/mTLS**,** OAuth 2.0**, bearer tokens, Basic auth, and** API key support**.** Comprehensive Logging**: Built-in support for** console**(colored),** file-based**logging (including structured** JSON**output), and a no-op quiet logger.- Built-in support for streaming and async operations.\n\n**Planned Integrations**:** Advanced Orchestration Patterns**- Multi-step workflows, supervisory agents, role routing, and hierarchical control systems.\n\n**Runtime Control Plane**: Typed client/server requests for cancellation, state inspection/reset/compaction, and LLM history compaction without exposing maintenance operations to the model prompt.**Observable & Replayable Runs**:`RunContext`\n\n,`RunEvent`\n\n,`RunReport`\n\n, budgets, policy decisions, approvals, and redaction for production-facing UIs and tests.**Developer Tools**:`protolink doctor`\n\n, registry inspection, run replay, and a local dashboard with agent ping, HTTP chat, registry health, and a disabled Studio preview for future topology work.\n\nProtolink exposes a dashboard UI through the `protolink dashboard`\n\nCLI command, projecting run-store and registry state into a local browser view.\n\nRun the **dashboard** against an existing run store and a live registry (both optional):\n\n```\nprotolink dashboard --store runs.db --registry-url http://127.0.0.1:9010 --open\n```\n\nThe command serves a dependency-free local HTML dashboard over Protolink's public devtool collectors. It reads persisted task snapshots and `RunReport`\n\nrecords from `SQLiteRunStore`\n\n, pulls `AgentCard`\n\nentries from the registry, and exposes live dashboard actions through local endpoints for snapshot refresh, run replay, HTTP agent `/status`\n\nprobes, and standard agent chat. The page does not create agents; it projects the runtime state your agents already emit through `AgentCard`\n\n, `RunContext`\n\n, `RunEvent`\n\n, and `RunReport`\n\n.\n\nProtoLink implements Google’s A2A protocol at the **wire level**, while providing a higher-level agent runtime that unifies client, server, transport, tools, and LLMs into a single composable abstraction **the Agent**.\n\n| Concept | Google A2A | ProtoLink |\n|---|---|---|\n| Agent | Protocol-level concept | Runtime object |\n| Transport | External server concern | Agent-owned |\n| Client | Separate | Built-in |\n| LLM | Out of scope | First-class |\n| Tools | Out of scope | Native + MCP |\n| UX | Enterprise infra | Developer-first |\n\nProtolink takes a **centralized agent** approach compared to Google's A2A protocol, which separates client and server concerns. Here's how it differs:\n\n| Feature | Google's A2A | Protolink |\n|---|---|---|\nArchitecture |\nDecoupled client/server | Unified agent with built-in client/server |\nTransport |\nFactory-based with provider pattern | Direct interface implementation |\nDeployment |\nRequires managing separate services | Single process by default, scales to distributed |\nComplexity |\nHigher (needs orchestration) | Lower (simpler to reason about) |\nFlexibility |\nRuntime configuration via providers | Code-based implementation |\nUse Case |\nLarge-scale, distributed systems | Both simple and complex agent systems |\n\n**Simplified Development**: Manage a single agent runtime without separate client/server codebases.** Reduced Boilerplate**: Common functionality is handled by the base[Agent](/nMaroulis/protolink/blob/main)class, letting you focus on agent logic.**Flexible Deployment**: Start with a single process, scale to distributed when needed** Unified State Management**: Shared context between client and server operations** Maintainability**:- Direct code paths for easier debugging\n- Clear control flow with fewer abstraction layers\n- Type-safe interfaces for better IDE support\n\n**Extensibility**:- Easily add new transport implementations\n- Simple interface-based design\n- No complex configuration needed for common use cases\n\n**Real Multi-Agent Systems**: Build** autonomous agents**with embedded LLMs, tools, and memory that communicate directly.** Simple API**: Built from the ground-up for** minimal boilerplate**, letting you focus on agent logic rather than infrastructure.** Developer Friendly**: Clean abstractions and direct code paths make debugging and maintenance a breeze.** Production Oriented**: Designed for** performance, reliability, and scalability**in real-world deployments.** Extensible & Interoperable**: Add new agents, transports, or protocols easily; compatible with** A2A**and** MCP**standards.** Community Focused**: Designed for the open-source community with clear contribution guidelines.\n\nThis will install the base package without any optional dependencies.\n\n```\n# Using uv (recommended)\nuv add protolink\n\n# Using pip\npip install protolink\n```\n\nProtolink supports optional features through extras. Install them using square brackets:\nNote: `uv add`\n\ncan be replace with `pip install`\n\nif preferred.\n\n```\n# Install with all optional dependencies\nuv add \"protolink[all]\"\n\n# Install with HTTP support (for web-based agents)\nuv add \"protolink[http]\"\n\n# Install with gRPC support (for service-to-service agents)\nuv add \"protolink[grpc]\"\n\n# Install all the supported LLM libraries\nuv add \"protolink[llms]\"\n\n# For development (includes all optional dependencies and testing tools)\nuv add \"protolink[dev]\"\n```\n\nTo install from source and all optional dependencies:\n\n```\ngit clone https://github.com/nmaroulis/protolink.git\ncd protolink\nuv pip install -e \".[dev]\"\n```\n\n👉 The example found in the jupyter notebooks here: [Hello World Example](https://github.com/nMaroulis/protolink/tree/main/examples/notebooks/basic_example)\n\n``` python\nfrom protolink.agents import Agent\nfrom protolink.models import AgentCard\nfrom protolink.tools.adapters import MCPToolAdapter\nfrom protolink.llms.api import OpenAILLM\nfrom protolink.discovery import Registry\n\n# Initialize Registry for A2A Discovery\nregistry = Registry(url=\"http://127.0.0.1:9000\", transport=\"http\")\nregistry.start(background=True)\n\n# Define the agent card\nagent_card = AgentCard(\n    name=\"example_agent\",\n    description=\"A dummy agent\",\n    url=\"http://127.0.0.1:8020\",\n)\n\n# OpenAI API LLM\nllm = OpenAILLM(model=\"gpt-5.2\")\n\n# Initialize the agent\nagent = Agent(agent_card, transport=\"http\", llm=llm, registry=registry)\n\n# Add Native tool\n@agent.tool(name=\"add\", description=\"Add two numbers\")\nasync def add_numbers(a: int, b: int):\n    return a + b\n\n# Add MCP tools and return them as protolink native tools\nmcp_adapter = MCPToolAdapter(transport=\"sse\", url=\"https://api.example.com/mcp/sse\")\nmcp_tools = mcp_adapter.get_tools()\nfor mcp_tool in mcp_tools:\n    agent.add_tool(mcp_tool)\n\n# Start the agent\nagent.start()\n```\n\nWhen using an HTTP-compatible transport (`http`\n\n, `sse`\n\n, `json-rpc`\n\n, or `sse-json-rpc`\n\n), the **Agent** and **Registry** expose lightweight browser pages: registry status at `/status`\n\n, agent status at `/status`\n\n, and agent chat at `/chat`\n\nfor LLM-backed agents. WebSocket, gRPC, and runtime transports keep the same logical endpoints for clients, but they do not serve browser HTML directly.\n\n|\n|\n\nProtoLink's core philosophy is a **simple API with progressive control**: the shortest path should be enough for prototyping, while production features remain available through explicit, composable objects instead of adding every advanced option to `Agent`\n\n.\n\nFor a fast prototype, pass a transport name. ProtoLink builds the transport with safe defaults:\n\n``` python\nfrom protolink import Agent, AgentCard\n\ncard = AgentCard(\n    name=\"assistant\",\n    description=\"General-purpose assistant\",\n    url=\"http://127.0.0.1:8000\",\n)\n\nagent = Agent(card=card, transport=\"http\")\n```\n\nFor production, configure the transport directly and pass the completed object to the same Agent API:\n\n``` python\nfrom protolink import Agent, AgentCard, RetryPolicy, TLSConfig, TransportConfig, TransportLimits\nfrom protolink.transport import HTTPTransport\n\ncard = AgentCard(\n    name=\"assistant\",\n    description=\"Production assistant\",\n    url=\"https://agent.internal:8443\",\n)\ntransport = HTTPTransport(\n    url=card.url,\n    tls=TLSConfig(\n        certfile=\"certs/agent.pem\",\n        keyfile=\"certs/agent-key.pem\",\n        cafile=\"certs/ca.pem\",\n    ),\n    config=TransportConfig(\n        limits=TransportLimits(max_concurrent_requests=200),\n        retry=RetryPolicy(max_attempts=3),\n    ),\n)\n\nagent = Agent(card=card, transport=transport)\n```\n\nThis keeps Agent concerns such as reasoning, tools, state, and policy separate from transport concerns such as TLS, payload limits, retries, keepalive, and connection management. The same rule applies to `AgentClient`\n\nand `Registry`\n\n: pass a string for defaults or a configured transport object for advanced behavior. Every service boundary can therefore use its own certificates and capacity policy without expanding or coupling the high-level constructors.\n\nFollow the API documentation here: [Documentation](https://nmaroulis.github.io/protolink/)\n\nFor Agent-to-Agent & Agent-to-Registry communication:\n\n`http`\n\n·[HTTPTransport](https://github.com/nMaroulis/protolink/blob/main/protolink/transport/http_transport.py): Uses HTTP/HTTPS for synchronous requests. Two ASGI implementations are available.- Lightweight:\n`starlette`\n\n,`httpx`\n\n&`uvicorn`\n\n- Advanced | Schema Validation:\n`fastapi`\n\n,`pydantic`\n\n&`uvicorn`\n\n- Lightweight:\n`sse`\n\n,`json-rpc`\n\n,`sse-json-rpc`\n\n·[SSEJSONRPCTransport](https://github.com/nMaroulis/protolink/blob/main/protolink/transport/sse_jsonrpc_transport.py): Uses normal HTTP routes plus Server-Sent Events for streamed task updates.`websocket`\n\n·[WebSocketTransport](https://github.com/nMaroulis/protolink/blob/main/protolink/transport/websocket_transport.py): Uses WebSocket for streaming requests. [`websockets`\n\n]`grpc`\n\n·[GRPCTransport](https://github.com/nMaroulis/protolink/blob/main/protolink/transport/grpc_transport.py): Uses gRPC unary calls and unary-stream task events over compact JSON envelopes. [`grpcio`\n\n]`runtime`\n\n·[RuntimeTransport](https://github.com/nMaroulis/protolink/blob/main/protolink/transport/runtime_transport.py): Simple**in-process, in-memory transport**.\n\nAll network transports share one native TLS API. Use `https://`\n\n, `wss://`\n\n, or `grpcs://`\n\nand configure `TLSConfig`\n\non the transport:\n\n``` python\nfrom protolink import Agent, AgentCard, TLSConfig\nfrom protolink.transport import HTTPTransport\n\ntls = TLSConfig(\n    certfile=\"certs/agent.pem\",\n    keyfile=\"certs/agent-key.pem\",\n    cafile=\"certs/ca.pem\",\n    # require_client_cert=True,  # enable mutual TLS\n)\n\ncard = AgentCard(\n    name=\"secure-agent\",\n    description=\"Agent served over HTTPS\",\n    url=\"https://agent.internal:8443\",\n)\ntransport = HTTPTransport(\n    url=card.url,\n    tls=tls,\n)\nagent = Agent(card=card, transport=transport)\n```\n\n`TLSConfig`\n\nencrypts the connection and verifies certificates; `Authenticator`\n\nimplementations handle application credentials and authorization. They are independent and can be combined. See the [TLS and mutual TLS guide](https://nmaroulis.github.io/protolink/docs/transport/#tls-and-mutual-tls) and [ examples/tls_agent.py](https://github.com/nMaroulis/protolink/blob/main/examples/tls_agent.py).\n\nAll transports also share one production configuration surface:\n\n``` python\nfrom protolink import Agent, AgentCard, RetryPolicy, TransportConfig, TransportLimits\nfrom protolink.transport import HTTPTransport\n\nagent_card = AgentCard(\n    name=\"production-agent\",\n    description=\"Production task worker\",\n    url=\"http://127.0.0.1:8000\",\n)\ntransport_config = TransportConfig(\n    limits=TransportLimits(max_concurrent_requests=200, max_concurrent_streams=50),\n    retry=RetryPolicy(max_attempts=3),\n)\n\ntransport = HTTPTransport(url=agent_card.url, config=transport_config)\nagent = Agent(card=agent_card, transport=transport)\n```\n\nRetries remain off by default and run only for explicitly idempotent request specifications. Correlation IDs, server-side idempotency replay, bounded payloads/concurrency, loop-safe pooled-resource shutdown, WebSocket keepalive, typed transport errors, local metric snapshots, and `/healthz`\n\nand `/readyz`\n\nprobes use the same contract across HTTP, SSE JSON-RPC, WebSocket, gRPC, and RuntimeTransport. The `grpc`\n\nextra also installs standard gRPC health checking and reflection. See the [production transport guide](https://nmaroulis.github.io/protolink/docs/transport/#production-configuration) and [ examples/transport_production.py](https://github.com/nMaroulis/protolink/blob/main/examples/transport_production.py).\n\nThese controls exist so changing protocols does not change the Agent's production safety model. Limits prevent one payload or traffic burst from exhausting the process; retries recover explicitly safe operations from temporary connection failures; idempotency prevents those retries from executing the same operation twice; and metrics and health probes make the behavior visible. The defaults are suitable for getting started and do not enable retries automatically.\n\nThe application-facing transport types are exported directly from `protolink`\n\n:\n\n| API | Purpose |\n|---|---|\n`TransportConfig` |\nOne immutable configuration for limits, retries, keepalive, shutdown, idempotency replay, and metrics. |\n`TransportLimits` |\nRequest, response, stream-event, request-concurrency, and stream-concurrency bounds. |\n`RetryPolicy` |\nExplicit attempt count, exponential backoff, jitter, and retryable method set. |\n`TransportMetricsSnapshot` |\nDependency-free per-instance counters, byte totals, active work, retries, and cumulative latency. |\n`TransportError` and typed subclasses |\nStable connection, timeout, protocol, remote, and payload-limit failures with request metadata. |\n\nTransport authors can additionally import `Transport`\n\n, `TransportCapabilities`\n\n, and `TransportRequestContext`\n\nfrom `protolink.transport`\n\n. The [shared API reference](https://nmaroulis.github.io/protolink/docs/transport/#shared-transport-api-reference) lists every field, default, base-class hook, health payload, and protocol mapping.\n\nProtolink separates LLMs into three types: `api`\n\n, `local`\n\n, and `server`\n\n.\nThe following are the Protolink wrappers for each type. If you want to use another model, you can use it directly without going through Protolink’s `LLM`\n\nclass.\n\n[ API ] [ Server ] [ Local ]\n\n**API**, calls the API, requires an API key:[OpenAILLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/api/openai_client.py): Uses**OpenAI API** for sync & async requests.[AnthropicLLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/api/anthropic_client.py): Uses**Anthropic API** for sync & async requests.[GeminiLLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/api/gemini_client.py): Uses**Gemini API** for sync & async requests.[GrokLLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/api/grok_client.py): Uses**Grok API** for sync & async requests.[DeepSeekLLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/api/deepseek_client.py): Uses**DeepSeek API** for sync & async requests.\n\n**Local**, runs the model in runtime:[LlamaCPPLocalLLM](/nMaroulis/protolink/blob/main): Uses** local runtime llama-cpp-python**for sync & async requests.\n\n**Server**, connects to an LLM Server, deployed locally or remotely:[OllamaLLM](https://github.com/nMaroulis/protolink/blob/main/protolink/llms/server/ollama_client.py): Uses**Ollama** for sync & async requests.[LlamaCPPServerLLM](/nMaroulis/protolink/blob/main): Connects to**llama-server** for sync & async requests.\n\n[Native Tool](https://github.com/nMaroulis/protolink/blob/main/protolink/tools/tool.py): Uses native tools.[MCPToolAdapter](https://github.com/nMaroulis/protolink/blob/main/protolink/tools/adapters/mcp_adapter.py): Connects to MCP Server and registers MCP tools as native tools.\n\nThe `MCPToolAdapter`\n\nconnects to [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers and exposes their tools as Protolink-native callables, wrapping remote **MCP tools** as native `Tool`\n\ninstances. This enables seamless integration with the growing MCP ecosystem.\n\n**Supported transports:**\n\n`stdio`\n\n- Local subprocess communication (for MCP servers as scripts/executables)`sse`\n\n- Remote HTTP Server-Sent Events (for MCP servers as web services)\n\n``` python\nfrom protolink.tools.adapters import MCPToolAdapter\n\n# Connect to a local MCP server\nadapter = MCPToolAdapter(\n    transport=\"stdio\",\n    command=\"python\",\n    args=[\"my_mcp_server.py\"]\n)\n\n# Or connect to a remote MCP server\nadapter = MCPToolAdapter(\n    transport=\"sse\",\n    url=\"https://api.example.com/mcp/sse\"\n)\n\n# Get tools and add them to your agent\nfor tool in adapter.get_tools():\n    agent.add_tool(tool)\n\n# Or call tools directly\nadd_tool = adapter.get_callable(\"add\")\nresult = add_tool(a=5, b=7)\n```\n\nProtolink treats agentic systems as **distributed programs**, not probabilistic workflows.\nEvery interaction between models, tools, and agents becomes an explicit action that the runtime can validate, execute, observe, and replay.\nRuns can also carry a typed `RunContext`\n\nand emit versioned `RunEvent`\n\ns, giving applications stable session, trace, budget, permission, and progress metadata without custom glue.\n`RunAction`\n\n, capability policy, and typed approval checkpoints gate side effects while keeping approval previews and user experience application-owned.\n\nAt the heart of Protolink is the **infer loop**:\n\n- The LLM proposes exactly one next action.\n- Protolink validates that action.\n- The runtime executes the tool call, agent delegation, or final response.\n- The result is fed back into the conversation.\n- The loop repeats until the agent returns a final answer.\n\nThis is where Protolink removes the boilerplate most agent frameworks push onto you: provider-specific tool prompts, JSON parsing, schema validation, retries, routing, delegation, and error recovery.\n\nWhen a provider supports native tool calling, Protolink uses it. When a local or smaller model works better with simple JSON instructions, Protolink uses that instead. The user-facing contract stays the same.\n\nProtolink removes that complexity by standardizing all interactions through a small set of explicit primitives:\n\n**Task**- a shared unit of work** Message**- communication within a task** Part**- an atomic, machine-interpretable action or result\n\nAgents never infer behavior implicitly. Instead, they declare intent explicitly using structured Parts such as:\n\n**tool_call**- execute a local tool** agent_call**- delegate work to another agent** infer**- invoke LLM reasoning** text**- return user-facing output\n\nand more...\n\nFrom there, the runtime handles everything deterministically.\n\nYou do **not** need to:\n\n- Write provider-specific tool-calling prompts\n- Parse raw LLM text or JSON\n- Convert native tool calls into runtime actions\n- Write\n**routing** or**delegation logic** - Rebuild retry, validation, and self-correction loops\n\nThe runtime automatically:\n\n- Selects the right prompt/tool-calling mode for the model\n- Injects tool schemas and discovered agent capabilities\n- Validates every LLM action before execution\n- Executes tools and agent calls deterministically\n- Emits structured events for tracing and debugging\n\nTool calls, agent calls, and LLM invocations only happen when explicitly declared. All results are returned as structured Parts - no hidden side effects, no magic. From the user’s perspective:\n\n```\ntask = Task.create(\n    Message.user(\"What's the weather in Geneva?\")\n)\n```\n\nThat’s it.\n\nThis is not a black-box agent framework.\n\n- No hidden reasoning\n- No implicit planning\n- No speculative execution\n\nEvery action is **explicit, inspectable, and replayable**.\n\nIf a tool runs, you see a `tool_call`\n\n.\nIf an agent is contacted, you see an `agent_call`\n\n.\nIf an LLM is invoked, you see an `infer`\n\n.\n\nThis makes the system predictable, debuggable, composable, and production-ready.\n\nThe runtime is **fully LLM-agnostic**.\nAny model, **API-based, self-hosted, or local**, can be swapped in without changing behavior or results. OpenAI, Anthropic, local servers, or custom backends all operate through the same unified execution model.\n\nThe orchestration stays the same. The contracts stay the same. Only the model changes.\n\nThis lets you evolve providers, costs, latency, or deployment strategy without rewriting your agents.\n\nThis project uses a structured, Agent-to-Agent (A2A) style communication model. Understanding how **Tasks**, **Messages**, **Artifacts**, and **Parts** interact is key to using the agent effectively.\n\nA **Task** represents a unit of work or a conversation thread between agents.\n\n- It contains\n**Messages** and**Artifacts**. - Tracks lifecycle through\n`Task.state`\n\n(`submitted`\n\n,`working`\n\n,`completed`\n\n,`failed`\n\n, etc.). - Records successful state transitions in\n`metadata[\"state_history\"]`\n\n. - Tasks are sent between agents; each agent executes what is explicitly defined in the task.\n\nA **Message** is a communicative unit in a task.\n\n- Can be sent by a user or an agent.\n- Contains\n**Parts** representing atomic content. - Example roles:\n`\"user\"`\n\n- input from a human or another agent`\"agent\"`\n\n- output from an agent\n\nAn **Artifact** is a container for outputs generated by the agent.\n\n- Stores\n**Parts** that result from executing a tool (**tool_call**) or an LLM inference (** infer**). - Can include tool results, reasoning traces, or structured outputs.\n- Artifacts allow agents to append results without modifying the original message.\n\nA **Part** is the atomic content of a Message or Artifact.\n\n- Defines\n**what to do** or**what was produced**. - Example Part types (\n`PartType`\n\n):`\"text\"`\n\n: plain text content`\"json\"`\n\n: structured data`\"tool_call\"`\n\n: request to execute a registered tool`\"tool_output\"`\n\n: result outputfrom executing a tool`\"infer\"`\n\n: input to invoke the agent's LLM`\"status\"`\n\n,`\"error\"`\n\n,`\"image\"`\n\n,`\"audio\"`\n\n, etc.\n\n-\n**Task Creation**\n\nA user or agent creates a`Task`\n\nwith a`Message`\n\ncontaining one or more`Parts`\n\n. -\n**Task Execution**- The receiving agent inspects the\n**last message or artifact** in the task. - Executes each\n`Part`\n\nsequentially:`tool_call`\n\n→ executes a registered tool → produces`tool_output`\n\nPart in an Artifact.`infer`\n\n→ invokes the agent's LLM → produces`infer_output`\n\nPart in an Artifact.\n\n- The receiving agent inspects the\n-\n**Appending Outputs**- Results are appended to the Task as new\n**Artifacts**. - Lifecycle state transitions are applied to\n`Task.state`\n\nand recorded in`metadata[\"state_history\"]`\n\n.\n\n- Results are appended to the Task as new\n-\n**Sequential Processing**- Tasks are processed sequentially at the message/artifact level.\n- Parallel execution is possible\n**within the parts of a single message/artifact**, but not across multiple messages/artifacts in the same task.\n\n``` python\nfrom protolink.models import Message, Part, Task\n\n# 1️⃣ User creates a Task with a message containing a Part\ntask = Task.create(Message.user(\"What's the weather in Athens?\"))\n\n# 2️⃣ Add a tool call Part\ntool_part = Part.tool_call(tool_name=\"weather_api\", args={\"city\": \"Athens\"})\ntask.add_message(Message.agent(parts=[tool_part]))\n\n# 3️⃣ Agent executes the task\nresult_task = await agent.execute_task(task)\n\n# 4️⃣ Outputs are appended as artifacts\nfor artifact in result_task.artifacts:\n    for part in artifact.parts:\n        if part.type == \"tool_output\":\n            print(\"Tool Output:\", part.content)\n\n# 5️⃣ If needed, a infer Part can trigger the agent's LLM\ninfer_part = Part.infer(prompt=\"Summarize today's weather in Athens\")\ntask.add_message(Message.agent(parts=[infer_part]))\nresult_task = await agent.execute_task(task)\n```\n\n**Key Notes:**\n\n- Each\n`Task`\n\nmaintains the full history of messages and artifacts. - Agents execute the\n**last message or artifact** to determine the next action. - Parts inside a message or artifact can be executed in parallel if needed.\n- Agents\n**do not guess intent**; they execute exactly what is defined in the Parts.\n\nThis structured approach ensures predictable, deterministic agent behavior while still supporting multi-step interactions and LLM/tool execution.\n\nWhile frameworks like LangChain or LangGraph rely on complex implicit state machines or LLM-driven routing, Protolink's **Structured Flows** provide explicit, deterministic execution paths (`Pipeline`\n\n, `Parallel`\n\n, `Router`\n\n, `Graph`\n\n) without the heavy overhead.\n\nFlows allow you to define highly composable, rigid, and predictable multi-step workflows. `Pipeline`\n\n, `Parallel`\n\n, and `Graph`\n\ndefine deterministic topology, while `Router`\n\nbranches on structured `Part.route(...)`\n\ndecisions that are serializable, trace-visible, and testable. Legacy `[ROUTE: key]`\n\ntags are still accepted for older prompts.\n\nEven though flows are programmatically structured, agents executing inside them must remain **fully decoupled** from the overall flow topology. Protolink achieves this through **Dynamic Semantic Context Injection**:\n\n**Topological Analysis**: Before dispatching a step, the Flow orchestrator looks at the subsequent step in the topology (e.g., a single downstream agent, a parallel committee, or conditional routes).**Context-Aware Prompt Generation**: It automatically builds descriptive system instructions outlining the next target's capabilities, description, or routing choices (retrieved dynamically via their`AgentCard`\n\nfrom the Registry).**Flow State Injection**: It populates the prompt into`task.flow_state[\"prompt\"]`\n\n.**Execution**: The executing agent's LLM automatically merges these instructions into its system prompt during inference. The agent adapts its behavior at runtime to optimize its output format for the downstream receiver without any hardcoded integration!\n\nIn simple terms, the Flow tells the agents what to do and what format to use for their output based on the downstream agents' capabilities. In the Pipeline example below, the orchestrator tells the researcher that its output will be used by the summarizer, so it should format its output in a way that the summarizer can understand. This is done automatically and transparently to the developer!\n\nYou can define and run structured flows programmatically using both asynchronous and synchronous APIs.\n\nA `Pipeline`\n\nruns a predefined sequence of agents, passing the output of one agent as the input to the next:\n\n``` python\nfrom protolink.flows import Pipeline\nfrom protolink.models import Task\n\n# 1. Define a deterministic sequence of steps (can be Agent names, instances, or other sub-flows)\npipeline = Pipeline(\n    steps=[\"researcher\", \"summarizer\"],\n    registry=registry  # Optional: allows discovering string-based agents by name\n)\n\n# 2. Run the flow asynchronously\ntask = Task.create_infer(prompt=\"Research the future of Agentic computing.\")\nresult = await pipeline.execute(task)\n```\n\nPipelines support a fluid builder pattern for dynamic step configuration:\n\n```\npipeline = Pipeline(registry=registry).add_step(\"researcher\").add_step(\"summarizer\")\nresult = await pipeline.execute(task)\n```\n\nFor simple scripts, Jupyter Notebooks, or background workers where async syntax is not preferred, use the `.sync`\n\nblocking wrapper:\n\n```\n# Executes the flow synchronously in a clean event loop\nresult = pipeline.sync.execute(task)\nprint(result.get_last_part_content())\n```\n\nSince every flow primitive inherits from the base `Flow`\n\nclass, they are fully polymorphic. You can nest complex flow structures inside one another seamlessly:\n\n``` python\nfrom protolink.flows import Pipeline, Parallel\n\n# A parallel execution block containing multiple concurrent agents\nreview_committee = Parallel(branches=[\"editor\", \"fact_checker\"], registry=registry)\n\n# Embed the parallel block as a single step inside a pipeline!\norchestrated_flow = Pipeline(registry=registry) \\\n    .add_step(\"researcher\") \\\n    .add_step(review_committee) \\\n    .add_step(\"summarizer\")\n\nresult = await orchestrated_flow.execute(task)\n```\n\nProtolink provides non-invasive, out-of-the-box observability for agent task execution, LLM inferences, and tool calls. It utilizes Python's `contextvars`\n\nto automatically track execution states asynchronously without cluttering core method signatures.\n\nCurrently supported telemetry options:\n\n**LocalTraceTelemetry**- built-in local traces for replay/debugging, with optional JSONL output[Langfuse](https://langfuse.com/)[LangSmith](https://www.langchain.com/langsmith)\n\nFor local debugging, use `LocalTraceTelemetry`\n\nwithout any external service:\n\n``` python\nfrom protolink import Agent, AgentCard, LocalTraceTelemetry, Task, create_llm\n\ntelemetry = LocalTraceTelemetry(path=\"traces.jsonl\")\n\nagent = Agent(\n    card=AgentCard(name=\"debug_agent\", description=\"Local debug agent\", url=\"runtime://debug\"),\n    llm=create_llm(\n        \"mock\",\n        sequential_responses=[\n            {\"type\": \"final\", \"content\": \"done\"}\n        ],\n    ),\n    telemetry=telemetry,\n)\n\nresult = await agent.handle_task(Task.create_infer(prompt=\"Run a traced task\"))\ntrace = telemetry.recorder.replay()[-1]\n\nprint(result.get_last_part_content())  # done\nprint(trace[\"trace_id\"])\nprint([span[\"kind\"] for span in trace[\"spans\"]])  # task, llm, ...\n```\n\nLocal traces capture task spans, LLM events, tool calls, retry counts, redacted payloads, and model metadata. This makes the infer loop easy to inspect before sending anything to Langfuse, LangSmith, or another observability backend.\n\nFor hosted telemetry, install the optional dependency (`uv add \"protolink[telemetry]\"`\n\n) and inject the tracker to your agent. You can also broadcast events to multiple trackers using `MultiTelemetry`\n\n:\n\n``` python\nfrom protolink.telemetry import LangfuseTelemetry, LangSmithTelemetry, MultiTelemetry\nfrom protolink.agents import Agent\n\n# Traces, spans and generations will be automatically recorded to both Langfuse and LangSmith!\nagent = Agent(\n    card={\"name\": \"ObserverAgent\", \"url\": \"http://127.0.0.1:8000\"},\n    telemetry=MultiTelemetry([LangfuseTelemetry(), LangSmithTelemetry()])\n)\n```\n\nSee the [Telemetry Documentation](https://nmaroulis.github.io/protolink/docs/telemetry/) for full setup instructions and custom integrations.\n\nProtolink provides a flexible logging system to track agent activity across different outputs.\n\n[ConsoleLogger](https://github.com/nMaroulis/protolink/blob/main/protolink/logging/console.py): Provides colored, human-readable logs in the terminal.[FileLogger](https://github.com/nMaroulis/protolink/blob/main/protolink/logging/file.py): Persists logs to disk, with optional**JSON** formatting for easy ingestion into log management tools.[QuietLogger](https://github.com/nMaroulis/protolink/blob/main/protolink/logging/quiet.py): Implements the logger interface while intentionally dropping every log message.[BaseLogger](https://github.com/nMaroulis/protolink/blob/main/protolink/logging/base.py): Abstract base class for creating custom logging implementations.\n\nMIT\n\nAll contributions are more than welcome! Please see [CONTRIBUTING.md](https://github.com/nMaroulis/protolink/blob/main/CONTRIBUTING.md) for more information.", "url": "https://wpnews.pro/news/show-hn-protolink-a-python-runtime-for-distributed-a2a-agents", "canonical_source": "https://github.com/nMaroulis/protolink", "published_at": "2026-07-13 13:06:24+00:00", "updated_at": "2026-07-13 13:35:52.384493+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "ai-infrastructure", "developer-tools"], "entities": ["ProtoLink", "Google", "Agent-to-Agent (A2A)", "MCP", "Level Up Coding"], "alternates": {"html": "https://wpnews.pro/news/show-hn-protolink-a-python-runtime-for-distributed-a2a-agents", "markdown": "https://wpnews.pro/news/show-hn-protolink-a-python-runtime-for-distributed-a2a-agents.md", "text": "https://wpnews.pro/news/show-hn-protolink-a-python-runtime-for-distributed-a2a-agents.txt", "jsonld": "https://wpnews.pro/news/show-hn-protolink-a-python-runtime-for-distributed-a2a-agents.jsonld"}}