Skip to content

Leasing & Concurrency#

Leasing assigns temporary ownership of an intent scope to a specific agent, preventing collisions when multiple agents work on the same intent. This is defined in RFC-0003.

Why Leasing?#

Without leasing, two agents could modify the same data simultaneously, causing lost updates and inconsistent state. Leases make ownership explicit and bounded by time.

sequenceDiagram
    participant A as Agent A
    participant S as Server
    participant B as Agent B

    A->>S: Acquire lease (scope: "research")
    S-->>A: Lease granted (60s)
    B->>S: Acquire lease (scope: "research")
    S-->>B: 409 Conflict — lease held by Agent A
    A->>S: Release lease
    B->>S: Acquire lease (scope: "research")
    S-->>B: Lease granted (60s)

Acquiring a Lease#

from openintent import OpenIntentClient

client = OpenIntentClient(
    base_url="http://localhost:8000",
    api_key="dev-user-key",
    agent_id="worker-1"
)

# Acquire a lease on a specific scope
lease = client.acquire_lease(
    intent_id=intent.id,
    scope="data_processing",
    ttl_seconds=120  # Lease expires after 2 minutes
)

print(f"Lease {lease.id} acquired, expires at {lease.expires_at}")

Lease Scopes#

Scopes are arbitrary strings that partition an intent's work. Only one agent can hold a lease on a given scope at a time.

Example Scope Use Case
"research" Only one agent researches at a time
"hotel_search" Exclusive access to hotel booking
"write" Prevent simultaneous edits
"*" Exclusive access to the entire intent

Working Under a Lease#

The lease context manager acquires and releases automatically:

from openintent.agents import Agent, on_assignment, on_lease_available

@Agent("exclusive-worker")
class ExclusiveWorker:

    @on_assignment
    async def handle(self, intent):
        async with self.lease(intent.id, scope="processing", ttl=60):
            # Only this agent can work on "processing" scope
            result = await do_exclusive_work(intent)
            return {"result": result}
        # Lease is automatically released here

    @on_lease_available("research")
    async def claim_research(self, intent, scope):
        """Called when a research lease becomes available."""
        async with self.lease(intent.id, scope):
            findings = await research(intent.description)
            return {"findings": findings}

Lease Lifecycle#

Status Description
active Lease is held by an agent
released Agent voluntarily released the lease
expired TTL elapsed without renewal
revoked Governance action terminated the lease

Renewing Leases#

For long-running work, renew before expiry:

# Manual renewal
lease = client.acquire_lease(intent.id, scope="analysis", ttl_seconds=60)

# ... do some work ...

# Extend the lease
renewed = client.renew_lease(intent.id, lease.id, ttl_seconds=60)
print(f"New expiry: {renewed.expires_at}")

Handling Conflicts#

When a lease is already held, the server returns a 409 Conflict:

from openintent.exceptions import ConflictError

try:
    lease = client.acquire_lease(intent.id, scope="research", ttl_seconds=60)
except ConflictError:
    print("Another agent holds this lease — waiting...")

Listing Active Leases#

# Get all leases for an intent
leases = client.get_leases(intent.id)

for lease in leases:
    print(f"Scope: {lease.scope}, Agent: {lease.agent_id}, "
          f"Status: {lease.status}, Expires: {lease.expires_at}")

Automatic heartbeats

When using @Agent(auto_heartbeat=True), the framework automatically renews leases for you. See Agent Lifecycle for details.

Next Steps#