The Big Picture
A TypeScript developer writes an agent. They callbindufy(). Hereβs what happens:
Why Two Processes?
Because the alternative is worse. Option A: Rewrite Binduβs core in every language. DID, auth, x402, scheduler, storage, A2A protocol β in TypeScript, then Kotlin, then Rust. Thousands of lines, each time. Every bug fixed three times. Option B: Keep one core. Connect to it over a wire. The handler runs in the developerβs language. Everything else runs in Python. One codebase for infrastructure. Thin SDKs for each language. We chose B. The wire is gRPC.Two Services, Two Directions
gRPC isnβt one-way. Both sides are servers AND clients:BinduService (lives in the Python core on :3774)
The SDK calls this to register and manage its agent:
| Method | What it does |
|---|---|
RegisterAgent | βHereβs my config, skills, and callback address. Make me a microservice.β |
Heartbeat | βIβm still alive.β (every 30 seconds) |
UnregisterAgent | βIβm shutting down. Clean up.β |
AgentHandler (lives in the SDK on a dynamic port)
The core calls this when work arrives:| Method | What it does |
|---|---|
HandleMessages | βA user sent this message. Run your handler and give me the response.β |
GetCapabilities | βWhat can you do?β |
HealthCheck | βAre you still there?β |
Message Flow: What Happens When a User Sends a Message
A user sends βWhat is the capital of France?β to a TypeScript agent thatβs been bindufied:ManifestWorker picks up the task
Builds conversation history from storage, calls
manifest.run(messages)manifest.run is a GrpcAgentClient
Converts messages to protobuf, calls
HandleMessages on the SDKβs gRPC serverTypeScript SDK receives the call
Deserializes messages:
[{role: "user", content: "What is the capital of France?"}]
Calls the developerβs handler functionManifestWorker processes the result
ResultProcessor normalizes it β ResponseDetector determines task state β βcompletedβ
ArtifactBuilder creates a DID-signed artifact
GrpcAgentClient: The Invisible Bridge
This is the component that makes everything work. Itβs a Python class that pretends to be a handler function. InManifestWorker, line 171:
manifest.run is a local function. For a gRPC agent, itβs a GrpcAgentClient instance. The worker canβt tell the difference. It calls it the same way, gets the same types back, and processes the result identically.
This is why we didnβt change ManifestWorker, ResultProcessor, ResponseDetector, or any downstream code. The abstraction holds. A callable is a callable.
What the SDK Does When You Call bindufy()
Step by step, from the developer typing npx tsx index.ts to seeing βWaiting for messagesβ¦β:
When the developer presses
Ctrl+C, the SDK kills the Python child process and exits cleanly.
Python vs gRPC Agents: Whatβs Different?
| Python Agent | gRPC Agent | |
|---|---|---|
| Developer calls | bindufy(config, handler) | bindufy(config, handler) (identical) |
| Handler runs in | Same process as core | Separate process |
| Core started by | bindufy() directly | SDK spawns as child process |
| Communication | In-process function call | gRPC over localhost |
| Latency overhead | 0ms | 1-5ms |
| Language | Python only | Any language with gRPC |
| DID, auth, x402 | Full support | Full support (identical) |
| Skills | Loaded from filesystem | Sent as data during registration |
| Streaming | Supported | Not yet implemented |
The key insight: from the outside (A2A clients, other agents, the frontend), there is no visible difference. The agent card looks the same. The DID is generated the same way. The A2A responses have the same structure. The artifacts carry the same DID signatures. A client cannot tell whether the agent behind
:3773 is Python, TypeScript, or Kotlin.