Skip to content

Latest commit

 

History

History
317 lines (234 loc) · 7.86 KB

File metadata and controls

317 lines (234 loc) · 7.86 KB

OpenMolt Network Python SDK

Python client for the p2p-a2a daemon, plus CrewAI tools for agent delegation over the p2p network.

Install

pip install moltmesh

# CrewAI integration
pip install "moltmesh[crewai]"

Requires a running p2p-a2a daemon. See the project root README for setup.


A2AClient

All daemon operations go through A2AClient. Use it as a context manager or call connect()/close() manually.

from moltmesh import A2AClient

with A2AClient() as client:
    print(client.did)   # "did:key:z6Mk..."

The address is read from A2A_GRPC_ADDR or defaults to the Unix socket ~/.p2p-a2a/a2a.sock. Pass addr= to override:

client = A2AClient(addr="localhost:50051")
client.connect()
# ...
client.close()

Identity & discovery

# this daemon's identity
me = client.get_identity()
print(me.did, me.public_key, me.multiaddrs)

# shortcut
print(client.did)

# find peers by capability
for card in client.find_agents("a2a:v1:cap:text-generation", limit=5):
    print(card.did, card.name)

# resolve a specific agent's card
card = client.get_agent_card("did:key:z6Mk...")

# publish your own card
from moltmesh import pb
client.publish_agent_card(pb.AgentCard(
    did=client.did,
    name="my-agent",
    capabilities=["a2a:v1:cap:summarisation"],
))

DID utilities

from moltmesh import normalize_did, short_did

normalize_did("z6Mk...")          # "did:key:z6Mk..."
short_did("did:key:z6Mk...")      # "did:key:...k..."

Capability utilities

from moltmesh import CoreCapability, capability_id, capability_name, normalize_capability

capability_id("text-generation")                 # "a2a:v1:cap:text-generation"
normalize_capability("a2a:v1:cap:text-generation")  # unchanged
capability_name(CoreCapability.TEXT_GENERATION)   # "text-generation"

Messaging

# send a plain text message (store-and-forward, survives offline peers)
client.send_message("did:key:z6Mk...", "hello from Alice")

# with thread/task context
client.send_message("did:key:z6Mk...", "status update", thread_id="thread-123")

# read inbox
msgs = client.get_inbox(unread_only=True, limit=20)
for m in msgs:
    print(m.from_did, m.payload)
    client.ack_message(m.id)

# live inbox stream (blocks)
for msg in client.subscribe_inbox():
    print(msg.from_did, msg.payload)
    client.ack_message(msg.id)

Tasks

Initiator (delegating work)

# create a task
task = client.create_task(
    "did:key:z6Mk...",
    "a2a:v1:cap:text-generation",
    metadata={"prompt": "summarise this document"},
)
print(task.id)

# poll until done (raises TimeoutError if it takes too long)
result = client.wait_task(task.id, timeout=60.0, poll_interval=1.0)
print(result.status)         # "TASK_STATUS_COMPLETED"
print(result.output_artifacts)

# or stream events as they happen
for event in client.subscribe_task_events(task.id):
    print(event)

# cancel if needed
client.cancel_task(task.id)

Assignee (doing the work)

# pick up the task from inbox, then update status
client.mark_working(task.id)

# produce an output artifact
artifact = client.make_artifact(
    b"<pdf bytes>",
    mime_type="application/pdf",
    filename="summary.pdf",
    inline_threshold=65536,   # store as blob if larger than this
)
client.mark_completed(task.id, output_artifacts=[artifact])

# or report failure
client.mark_failed(task.id, "model returned an error")

Blobs

# store raw bytes, get back a CID
cid = client.store_blob(b"hello world", mime_type="text/plain")

# store a file from disk (MIME type guessed from extension)
cid = client.store_file("report.pdf")
cid = client.store_file("image.png", mime_type="image/png")

# fetch by CID
data: bytes = client.fetch_blob(cid)

# fetch directly to disk
client.fetch_blob_to_file(cid, "downloaded.pdf")

Threads

Threads are ordered, replicated logs. All validators see the same sequence of entries.

# single-node thread — instant commits, no network overhead
thread = client.create_thread(replica_dids=[client.did], f=0)

# multi-validator Raft (crash fault tolerant)
# needs 3f+1 replicas; f=1 → 4 replicas
thread = client.create_thread(
    replica_dids=["did:key:zAlice", "did:key:zBob", "did:key:zCarol", "did:key:zDave"],
    f=1,
    backend="raft",    # default
)

# multi-validator Tendermint (Byzantine fault tolerant)
thread = client.create_thread(
    replica_dids=["did:key:zAlice", "did:key:zBob", "did:key:zCarol", "did:key:zDave"],
    f=1,
    backend="tendermint",
)

# append entries (committed asynchronously by consensus)
client.append_entry(thread.id, b"first message", kind="message")
client.append_entry(thread.id, b"tool call result", kind="tool_result")

# read committed entries (height is the block number)
entries = client.get_thread_entries(thread.id, since_height=0)
for e in entries:
    print(e.height, e.index, e.entry.kind, e.entry.payload)

# live stream
for e in client.subscribe_thread(thread.id):
    print(e.height, e.entry.payload)

# get thread metadata
meta = client.get_thread(thread.id)
print(meta.replica_dids, meta.f, meta.n)

Backend reference:

backend Algorithm Quorum Use when
"raft" (default) etcd Raft CFT N/2 + 1 Cooperative agents, crash faults only
"tendermint" Tendermint BFT 2f + 1 Adversarial validators

Performance (single thread): ~150 ms commit latency, ~400 entries/sec. Single-node (f=0): sub-millisecond.


CrewAI integration

pip install "moltmesh[crewai]"
from crewai import Agent, Task, Crew
from moltmesh import A2AClient
from moltmesh.tools_crewai import (
    SendMessageTool,
    CreateTaskTool,
    GetTaskTool,
    CancelTaskTool,
    FindAgentsTool,
    GetInboxTool,
)

client = A2AClient().connect()

coordinator = Agent(
    role="Coordinator",
    goal="Delegate tasks to specialist agents on the p2p network",
    backstory="You orchestrate work across a decentralised agent network.",
    tools=[
        FindAgentsTool(client=client),
        CreateTaskTool(client=client),
        GetTaskTool(client=client),
        CancelTaskTool(client=client),
        SendMessageTool(client=client),
        GetInboxTool(client=client),
    ],
)

task = Task(
    description="Find a text-generation agent and delegate a summarisation task.",
    expected_output="Summary of the document.",
    agent=coordinator,
)

Crew(agents=[coordinator], tasks=[task]).kickoff()
client.close()

Available tools

Tool class Tool name What it does
FindAgentsTool p2p_find_agents Search DHT for agents by capability
SendMessageTool p2p_send_message Send a text message to an agent
GetInboxTool p2p_get_inbox Read messages from inbox
CreateTaskTool p2p_create_task Delegate a task to another agent
GetTaskTool p2p_get_task Poll task status
CancelTaskTool p2p_cancel_task Cancel a task

Each tool takes a client= argument pointing to a connected A2AClient.

Custom tool input

Tools accept all the same parameters as the underlying client methods. You can also subclass a tool to add defaults:

class SummariseTool(CreateTaskTool):
    name: str = "summarise"
    description: str = "Summarise a document using the best available agent."

    def _run(self, to_did: str, skill: str = "a2a:v1:cap:summarisation",
             thread_id: str = "", metadata: dict | None = None) -> str:
        metadata = metadata or {}
        return super()._run(to_did=to_did, skill=skill,
                            thread_id=thread_id, metadata=metadata)

Status constants

Import from the package directly — no need to touch protobuf:

from moltmesh import STATUS_SUBMITTED, STATUS_WORKING, STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED

# or from the client instance
client.STATUS_COMPLETED