Skip to main content

What It Is

GrpcAgentClient is a Python class that looks like a function. You call it with messages, it returns a string or dict. Internally, it makes a gRPC call to a remote process in another language. But the caller doesn’t know that. This is the trick that makes the entire language-agnostic system work without changing a single line in ManifestWorker.

The Problem It Solves

ManifestWorker has this line:
raw_results = self.manifest.run(message_history or [])
For Python agents, manifest.run is a wrapper around the developer’s handler function. It takes a list of message dicts, returns a string or dict. For TypeScript/Kotlin agents, we need that same call to go over the network. But we can’t change ManifestWorker — it handles task state transitions, error handling, tracing, payment settlement. Touching it risks breaking everything. Solution: make GrpcAgentClient a callable that quacks like a handler function.

How It Works

class GrpcAgentClient:
    def __init__(self, callback_address: str, timeout: float = 30.0):
        self._address = callback_address  # e.g., "localhost:50052"
        self._timeout = timeout

    def __call__(self, messages, **kwargs):
        # 1. Convert Python dicts to protobuf
        proto_msgs = [ChatMessage(role=m["role"], content=m["content"]) for m in messages]
        request = HandleRequest(messages=proto_msgs)

        # 2. Call the SDK's AgentHandler over gRPC
        response = self._stub.HandleMessages(request, timeout=self._timeout)

        # 3. Convert back to what ManifestWorker expects
        if response.state:
            return {"state": response.state, "prompt": response.prompt}
        else:
            return response.content
Three steps: convert, call, convert back. That’s the entire bridge.

The Response Contract

ManifestWorker doesn’t care how the response was produced. It only cares about the type:
Handler returnsManifestWorker doesTask state
"The capital of France is Paris."Creates message + artifactcompleted
{"state": "input-required", "prompt": "Can you clarify?"}Creates message, keeps task openinput-required
{"state": "auth-required"}Creates message, keeps task openauth-required
GrpcAgentClient returns exactly these types. The downstream code — ResultProcessor, ResponseDetector, ArtifactBuilder — processes them identically to a local Python handler’s output.

Real Example: What Happens When a User Asks a Question

A user sends “What is quantum computing?” to a TypeScript agent:
ManifestWorker calls manifest.run(messages)
  → GrpcAgentClient.__call__([{"role": "user", "content": "What is quantum computing?"}])
    → Converts to protobuf: ChatMessage(role="user", content="What is quantum computing?")
    → gRPC call: AgentHandler.HandleMessages(HandleRequest{messages: [...]})
    → TypeScript SDK receives the call
    → Developer's handler runs: await openai.chat.completions.create(...)
    → OpenAI returns: "Quantum computing is a type of computation..."
    → SDK returns: HandleResponse{content: "Quantum computing is...", state: ""}
  → GrpcAgentClient sees state is empty, returns the string
→ ManifestWorker receives "Quantum computing is..." (same as a local handler)
→ ResultProcessor normalizes → ResponseDetector says "completed"
→ ArtifactBuilder creates DID-signed artifact
→ User gets the response
The GrpcAgentClient is the only component that knows gRPC exists. Everything above and below it is oblivious.

When It’s Created

During RegisterAgent, the gRPC service creates a GrpcAgentClient and attaches it to the manifest:
# In BinduServiceImpl.RegisterAgent():
grpc_client = GrpcAgentClient(request.grpc_callback_address)

# In create_manifest():
manifest.run = grpc_client  # GrpcAgentClient IS the handler now
From this point on, every task for this agent flows through the client.

Connection Lifecycle

The client connects lazily — the gRPC channel is created on the first call, not during initialization. This avoids connection errors during registration if the SDK’s server isn’t fully ready yet. When the SDK disconnects (Ctrl+C, crash), the next HandleMessages call fails with grpc.StatusCode.UNAVAILABLE. ManifestWorker’s existing error handling catches this and marks the task as failed. No special handling needed.

Health Checks and Capabilities

grpc_client.health_check()       # Is the SDK still running? Returns True/False
grpc_client.get_capabilities()   # What can the SDK do? Returns name, version, etc.
Used during heartbeat processing and capability discovery.

What It Doesn’t Do Yet

Current Limitations
  • Streaming — proto defines HandleMessagesStream but the client doesn’t implement it. Remote agents can only return complete responses.
  • Reconnection — if the SDK crashes, the client doesn’t retry. The agent must be re-registered.
  • TLS — uses insecure channels. Only safe on localhost or trusted networks.