Building Cross-Language .NET and Python Agent Applications with AutoGen

AutoGen enables cross-language agent applications by using a gRPC-based worker runtime that allows .NET and Python agents to communicate through a shared message bus using protobuf serialization.

Microsoft AutoGen is a multi-agent framework designed for building complex AI applications in the microsoft/autogen repository. Whether you are working in Python, .NET, or both, AutoGen provides a unified runtime that enables building cross-language .NET and Python agent applications through a shared communication layer and language-agnostic primitives.

Architecture Overview

AutoGen organizes its cross-language capabilities into a layered architecture that separates core runtime mechanics from high-level agent implementations:

Layer Purpose Core Source
Core API Language-agnostic primitives for agents, topics, subscriptions, message routing, and state persistence Python: autogen_core/_agent.py.NET: InProcessRuntime.cs
AgentChat API Opinionated, high-level helper classes (e.g., AssistantAgent, ToolAgent) for rapid prototyping Python: autogen_agentchat/agents.py.NET: BaseAgent.cs
Extensions Concrete implementations of LLM clients, tools, and runtime transports (gRPC, HTTP) Python: autogen_ext/runtimes/grpc.py.NET: GrpcWorkerAgentRuntime.cs
Samples Ready-to-run demos showing cross-language interoperability Python: hello_python_agent.py.NET: HelloAgent.cs

How Cross-Language Communication Works

The cross-language flow relies on a gRPC worker runtime that acts as a bridge between Python and .NET processes:

  1. Runtime Transport – A GrpcWorkerAgentRuntime runs as a sidecar process in both Python (autogen_ext/runtimes/grpc.py) and .NET (GrpcWorkerAgentRuntime.cs). It exposes the IAgentRuntime contract over gRPC and forwards messages to the local runtime implementation (InProcessRuntime for .NET or the Python equivalent).

  2. Agent Registration – Each language registers its agents with the remote runtime via add_subscription and register_agent APIs. Subscriptions map a topic to an agent type, enabling the runtime to route messages correctly.

  3. Message Exchange – When an agent publishes a message (runtime.publish_message), the runtime serializes it (JSON or protobuf) and sends it across the gRPC channel. The receiving runtime deserializes the payload, looks up the subscription, resolves the target agent using its AgentId, and invokes the on_message handler.

  4. State and Lifecycle – Agents can persist state using save_state and load_state methods. The runtime guarantees graceful shutdown through stop_when_signal and stop_when_idle methods.

Key Architectural Components

Understanding these core primitives is essential for building cross-language .NET and Python agent applications:

  • AgentId – A pair of (type, key) that uniquely identifies an agent across processes and languages.

  • TopicId – A hierarchical string (e.g., "HelloTopic") that drives message routing between agents.

  • Subscription – Either a type subscription (TypeSubscription(topic_type, agent_type)) or a default subscription (DefaultSubscription(agent_type)) that determines which agents receive which messages.

  • MessageContext – Carries metadata including sender information, cancellation tokens, and RPC flags.

  • InProcessRuntime – The C# implementation handling message queues, agent factories, and local transport plumbing.

  • GrpcWorkerAgentRuntime – The concrete implementation in both languages that handles cross-process gRPC communication.

Building a Cross-Language Application

Python Agent Implementation

The Python side uses GrpcWorkerAgentRuntime to connect to a .NET host and exchange protobuf messages. Here is the implementation from hello_python_agent.py:

import asyncio, logging, os, sys
from autogen_core import (
    PROTOBUF_DATA_CONTENT_TYPE,
    AgentId,
    DefaultSubscription,
    DefaultTopicId,
    TypeSubscription,
    try_get_known_serializers_for_type,
)
from autogen_ext.runtimes.grpc import GrpcWorkerAgentRuntime
from protos.agent_events_pb2 import NewMessageReceived, Output   # protobuf definitions

from user_input import UserProxy                                 # tiny console UI

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("autogen_core")

async def main() -> None:
    # ① Connect to the .NET Aspire host (the gRPC endpoint)

    host = os.getenv("AGENT_HOST", "http://localhost:50673")
    host = host.replace("http://", "").replace("https://", "")
    runtime = GrpcWorkerAgentRuntime(host_address=host,
                                      payload_serialization_format=PROTOBUF_DATA_CONTENT_TYPE)

    await runtime.start()
    # Register protobuf serializers for the message types we will send/receive

    runtime.add_message_serializer(try_get_known_serializers_for_type(NewMessageReceived))
    runtime.add_message_serializer(try_get_known_serializers_for_type(Output))

    # ② Register a *user proxy* that forwards console input to the .NET "HelloAgent"

    await UserProxy.register(runtime, "HelloAgent", lambda: UserProxy())

    # ③ Subscribe to the topics the .NET side will publish

    await runtime.add_subscription(DefaultSubscription(agent_type="HelloAgent"))
    await runtime.add_subscription(TypeSubscription(topic_type="HelloTopic", agent_type="HelloAgent"))
    await runtime.add_subscription(TypeSubscription(topic_type="agents.NewMessageReceived", agent_type="HelloAgent"))
    await runtime.add_subscription(TypeSubscription(topic_type="agents.ConversationClosed", agent_type="HelloAgent"))
    await runtime.add_subscription(TypeSubscription(topic_type="agents.Output", agent_type="HelloAgent"))

    # ④ Send a greeting to the .NET agent

    await runtime.publish_message(
        message=NewMessageReceived(message="Hello from Python!"),
        topic_id=DefaultTopicId("HelloTopic", "HelloAgents/python"),
        sender=AgentId("HelloAgents", "python"),
    )
    # Send a second "output" message (demonstrates protobuf payload switching)

    await runtime.publish_message(
        message=Output(message="*** Python says ***"),
        topic_id=DefaultTopicId("HelloTopic", "HelloAgents/python"),
        sender=AgentId("HelloAgents", "python"),
    )

    # ⑤ Keep the process alive until the .NET side signals shutdown

    await runtime.stop_when_signal()

if __name__ == "__main__":
    asyncio.run(main())

.NET Agent Implementation

The .NET side implements HelloAgent in HelloAgent.cs using the BaseAgent class and handles protobuf messages from Python:

using Microsoft.AutoGen.Agents;
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Samples;

[TypeSubscription("HelloTopic")]
public class HelloAgent(
    IHostApplicationLifetime hostApplicationLifetime,
    AgentId id,
    IAgentRuntime runtime,
    Logger<BaseAgent>? logger = null) 
    : BaseAgent(id, runtime, "Hello Agent", logger),
      IHandle<NewMessageReceived>,
      IHandle<ConversationClosed>,
      IHandle<Shutdown>
{
    // Handles the greeting received from Python
    public async ValueTask HandleAsync(NewMessageReceived item, MessageContext ctx)
    {
        Console.Out.WriteLine(item.Message); // prints "Hello from Python!"
        var goodbye = new ConversationClosed
        {
            UserId = this.Id.Type,
            UserMessage = "Goodbye"
        };
        await this.PublishMessageAsync(goodbye, new TopicId("HelloTopic"));
    }

    // Handles the goodbye message and triggers shutdown unless overridden
    public async ValueTask HandleAsync(ConversationClosed item, MessageContext ctx)
    {
        Console.Out.WriteLine($"{item.UserId} said {item.UserMessage}");
        if (Environment.GetEnvironmentVariable("STAY_ALIVE_ON_GOODBYE") != "true")
            await this.PublishMessageAsync(new Shutdown(), new TopicId("HelloTopic"));
    }

    // Called when a Shutdown message arrives – shuts down the Aspire host
    public async ValueTask HandleAsync(Shutdown _, MessageContext __)
    {
        Console.WriteLine("Shutting down...");
        hostApplicationLifetime.StopApplication();
    }
}

Running the Integration

To run the complete cross-language demo:


# 1️⃣ Start the .NET Aspire host (the integration test AppHost)

cd autogen/dotnet/samples/Hello/Hello.AppHost
dotnet run   # launches InProcessRuntime + HelloAgent

# 2️⃣ In another terminal, launch the Python side

cd autogen/dotnet/test/Microsoft.AutoGen.Integration.Tests.AppHosts/core_xlang_hello_python_agent
python hello_python_agent.py

The Python console prints the greeting while the .NET console echoes the message, demonstrating successful cross-language communication followed by a graceful shutdown sequence.

Key Source Files

Understanding the implementation requires familiarity with these specific files in the microsoft/autogen repository:

Summary

Building cross-language .NET and Python agent applications with AutoGen relies on a language-agnostic core architecture:

  • gRPC Worker Runtime enables communication between Python and .NET processes through a shared transport layer using protobuf serialization.
  • AgentId and TopicId provide universal addressing that works across language boundaries.
  • Subscriptions define routing rules that allow agents in one language to receive messages from agents in another.
  • InProcessRuntime and GrpcWorkerAgentRuntime implement the same interface in both languages, ensuring consistent behavior whether running locally or distributed.

Frequently Asked Questions

How do agents in different languages find each other?

Agents find each other through the TopicId subscription system. When a Python agent publishes a message to a topic like "HelloTopic", the gRPC runtime routes it to any subscribed .NET agents regardless of which process they run in. The AgentId (composed of type and key) ensures unique addressing across the distributed system.

What serialization format does AutoGen use for cross-language messages?

AutoGen uses Protocol Buffers (protobuf) for cross-language serialization. The Python runtime uses PROTOBUF_DATA_CONTENT_TYPE when configuring the GrpcWorkerAgentRuntime, and both sides register message serializers using try_get_known_serializers_for_type (Python) or equivalent generated code (C#). This ensures type-safe message exchange between languages.

Can I run the Python and .NET agents on different machines?

Yes. The gRPC worker runtime architecture supports distributed deployment. The Python GrpcWorkerAgentRuntime connects to a host address (configured via AGENT_HOST environment variable), which can be a remote .NET Aspire host or another Python runtime. The transport layer handles network communication, serialization, and message routing across machine boundaries.

How do I handle graceful shutdown in a cross-language scenario?

Use the stop_when_signal() method in Python and the IHostApplicationLifetime interface in .NET. The sample demonstrates this pattern: when the .NET HelloAgent receives a Shutdown message, it calls hostApplicationLifetime.StopApplication(), which signals the Python side to exit via the stop_when_signal() awaitable, ensuring both runtimes shut down cleanly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →