Societies¶
A Society is the top-level container in Claw SDK. It holds agents (nodes) and typed edges (relationships), forming a directed graph that compiles down to per-agent prompts, context filtering, and action binding.
In Claw, the graph IS the program. You do not write imperative orchestration logic. Instead, you declare agents and connect them with semantically typed edges. The runtime reads the graph topology and handles execution, routing, and termination.
What Is a Society¶
A Society is a named, serializable graph with three responsibilities:
- Container -- it owns all agents and edges in the system.
- Builder -- it provides a Pythonic DSL for declaring the graph topology.
- Serialization boundary -- it can round-trip to JSON for version control, sharing, and visualization.
Every Claw program starts by creating a Society and ends by handing it to a Runtime.
from claw import Society, Agent, Delegation, Oversight, LocalRuntime
# 1. Declare the graph
s = Society(name="pr-review", description="Automated PR review cycle")
pm = Agent(name="pm", role="dispatcher", model="claude-sonnet")
coder = Agent(name="coder", role="implementer", model="claude-opus")
reviewer = Agent(name="reviewer", role="critic", model="claude-sonnet")
s.add(pm, coder, reviewer)
s.connect(pm, coder, Delegation())
s.connect(coder, reviewer, Oversight(max_rounds=3))
# 2. Hand it to a runtime
runtime = LocalRuntime(MockLLM())
result = await runtime.run(s, task="Review PR #42")
Building a Society¶
Creating agents and adding them¶
There are two ways to register agents in a Society.
Explicit add -- call add() with one or more agents:
s = Society(name="example")
pm = Agent(name="pm", role="dispatcher", model="claude-sonnet")
coder = Agent(name="coder", role="implementer", model="claude-opus")
reviewer = Agent(name="reviewer", role="critic", model="claude-sonnet")
s.add(pm, coder, reviewer)
Implicit add via connect -- when you call connect(), compete(), cooperate(), or negotiate(), any agents not already in the Society are added automatically:
s = Society(name="example")
pm = Agent(name="pm", role="dispatcher", model="claude-sonnet")
coder = Agent(name="coder", role="implementer", model="claude-opus")
# Both pm and coder are added to s automatically
s.connect(pm, coder, Delegation())
If you try to explicitly add an agent whose name already exists in the Society, a ValueError is raised:
Connecting agents with edges¶
Binary edges connect exactly two agents with a directed relationship:
from claw import Delegation, Oversight
s.connect(pm, coder, Delegation())
s.connect(coder, reviewer, Oversight(max_rounds=3))
The first argument is the source (originator), the second is the target (receiver). Direction matters: a Delegation edge from pm to coder means pm delegates tasks to coder, not the other way around.
connect() returns the created Edge object, which you can inspect or store:
edge = s.connect(pm, coder, Delegation())
print(edge.edge_id) # e.g. "edge-a1b2c3d4"
print(edge.source) # pm
print(edge.target) # coder
Group edges¶
Some edge types have natural N-ary semantics. Instead of creating pairwise binary edges, you create a single group edge (hyperedge) that connects multiple agents at once.
Three edge types support group semantics:
- Competition -- N agents work in isolation, a judge picks the best output.
- Cooperation -- N agents collaborate with full mutual visibility.
- Coopetition -- N agents cooperate on some aspects and compete on others.
Two edge types are binary only and cannot form group edges:
- Oversight -- always pairwise (one overseer, one overseen).
- Delegation -- always pairwise (one delegator, one worker).
Use the dedicated builder methods for group edges:
from claw import Competition, Cooperation, Coopetition
from claw.resolve import JudgePicks
judge = Agent(name="judge", role="evaluator", model="claude-sonnet")
# Competition: isolated work, judge picks winner
s.compete(
[coder1, coder2, coder3],
Competition(resolve=JudgePicks(judge=judge, criteria=["correctness", "readability"])),
)
# Cooperation: shared visibility, merged outputs
s.cooperate(
[designer, frontend_dev],
Cooperation(shared=["ui-mockups", "component-specs"]),
)
# Coopetition: cooperate on some aspects, compete on others
s.negotiate(
[agent_a, agent_b],
Coopetition(
cooperate_on=["shared-data"],
compete_on=["pricing-strategy"],
resolve=JudgePicks(judge=judge),
),
)
Each method validates that the edge type matches. Passing a Cooperation to compete() raises a TypeError.
Builder Methods¶
| Method | Purpose | Edge kind | Example |
|---|---|---|---|
add(*agents) |
Register agents in the society | -- | s.add(pm, coder) |
connect(src, tgt, edge_type) |
Create a binary edge | Edge |
s.connect(pm, coder, Delegation()) |
compete(agents, competition) |
Create a group Competition edge | GroupEdge |
s.compete([a, b, c], Competition(resolve=...)) |
cooperate(agents, cooperation) |
Create a group Cooperation edge | GroupEdge |
s.cooperate([a, b], Cooperation()) |
negotiate(agents, coopetition) |
Create a group Coopetition edge | GroupEdge |
s.negotiate([a, b], Coopetition(...)) |
All builder methods that create edges return the created Edge or GroupEdge object.
Configuration¶
A SocietyConfig controls global runtime behavior -- budgets, timeouts, and policies:
from claw import Society, SocietyConfig, BudgetExceededPolicy
from datetime import timedelta
s = Society(
name="my-society",
description="Example with custom config",
config=SocietyConfig(
max_llm_calls=100,
max_wall_time=timedelta(minutes=5),
on_budget_exceeded=BudgetExceededPolicy.COMMIT_PARTIAL,
agent_turn_timeout=timedelta(seconds=60),
),
)
Config fields¶
| Field | Type | Default | Description |
|---|---|---|---|
max_llm_calls |
int |
100 |
Hard cap on total LLM calls across all agents in a single run. |
max_wall_time |
timedelta |
30 min |
Hard cap on wall-clock time for the entire run. |
on_budget_exceeded |
BudgetExceededPolicy |
COMMIT_PARTIAL |
What happens when any budget limit is hit. |
agent_turn_timeout |
timedelta |
120 s |
Maximum time for a single agent turn before timeout. |
Budget exceeded policies¶
| Policy | Behavior |
|---|---|
BudgetExceededPolicy.COMMIT_PARTIAL |
Save whatever artifact state exists and return partial results. |
BudgetExceededPolicy.ROLLBACK |
Revert all artifact state to pre-run values. |
If no config is provided, the defaults above apply.
Graph Queries¶
Once a Society is built, you can introspect it using properties and query methods.
Listing agents and edges¶
s.agents # list[Agent] -- all agents in the society
s.edges # list[Edge] -- all binary edges
s.group_edges # list[GroupEdge] -- all group edges (hyperedges)
s.all_edges # list[Edge | GroupEdge] -- both combined
Looking up a specific agent¶
Finding edges involving an agent¶
# All edges (binary and group) where coder participates
coder_edges = s.edges_of(coder)
for e in coder_edges:
print(e.edge_id, type(e.type).__name__)
Finding the edge between two agents¶
edge = s.edge_between(coder, reviewer)
if edge is not None:
print(f"Edge type: {type(edge.type).__name__}")
edge_between() searches binary edges only and returns the first match. It checks both directions -- edge_between(a, b) finds an edge whether the source is a or b.
Serialization¶
A Society can be serialized to a JSON-compatible dictionary for storage, version control, or transmission:
The output includes all agents, binary edges, group edges, and the society config:
{
"name": "pr-review",
"description": "Automated PR review cycle",
"config": {
"max_llm_calls": 100,
"max_wall_time": "PT30M",
"on_budget_exceeded": "commit_partial",
"agent_turn_timeout": "PT2M"
},
"agents": [
{"name": "pm", "role": "dispatcher", "model": "claude-sonnet", ...},
{"name": "coder", "role": "implementer", "model": "claude-opus", ...}
],
"edges": [
{
"edge_id": "edge-a1b2c3d4",
"source": "pm",
"target": "coder",
"type": "Delegation",
"type_config": {...}
}
],
"group_edges": []
}
This makes it straightforward to check society definitions into version control, diff them between runs, or send them over the wire.
Common Patterns¶
Fan-out delegation¶
A single dispatcher assigns tasks to multiple workers in parallel:
pm = Agent(name="pm", role="dispatcher", model="claude-sonnet")
coder1 = Agent(name="coder-1", role="implementer", model="claude-opus")
coder2 = Agent(name="coder-2", role="implementer", model="claude-opus")
s = Society(name="fan-out")
s.connect(pm, coder1, Delegation())
s.connect(pm, coder2, Delegation())
The runtime sends the task to both workers. Each worker sees only its own delegation edge, not the other worker's.
Review chain¶
A delegator assigns work, which then goes through a review cycle:
pm = Agent(name="pm", role="dispatcher", model="claude-sonnet")
coder = Agent(name="coder", role="implementer", model="claude-opus")
reviewer = Agent(name="reviewer", role="critic", model="claude-sonnet")
s = Society(name="review-chain")
s.connect(pm, coder, Delegation())
s.connect(coder, reviewer, Oversight(max_rounds=3))
The coder receives the task from the PM, produces output, and the reviewer provides feedback. The max_rounds=3 cap prevents infinite review loops.
Competitive coding¶
Multiple agents solve the same problem independently, and a judge picks the best solution:
from claw.resolve import JudgePicks
coder1 = Agent(name="coder-1", role="implementer", model="claude-opus")
coder2 = Agent(name="coder-2", role="implementer", model="claude-opus")
coder3 = Agent(name="coder-3", role="implementer", model="claude-opus")
judge = Agent(name="reviewer", role="evaluator", model="claude-sonnet")
s = Society(name="coding-contest")
s.compete(
[coder1, coder2, coder3],
Competition(
resolve=JudgePicks(
judge=judge,
criteria=["correctness", "readability", "performance"],
),
),
)
The competing agents are isolated -- they cannot see each other's work. The judge receives all submissions and picks the winner.
Human in the loop¶
Human agents are first-class graph nodes. They participate through a runtime interface (CLI, GitHub, Slack, webhook) rather than an LLM:
coder = Agent(name="coder", role="implementer", model="claude-opus")
maintainer = Agent(name="maintainer", role="approver", human=True, interface="cli")
s = Society(name="human-review")
s.connect(coder, maintainer, Oversight(max_rounds=2))
The maintainer receives review prompts via CLI and provides human feedback. From the graph's perspective, human and AI agents are identical -- the difference is entirely in the runtime backend.
Cooperation with shared resources¶
Multiple agents collaborate with full mutual visibility on shared topics:
designer = Agent(name="designer", role="ui-designer", model="claude-sonnet")
frontend = Agent(name="frontend", role="frontend-dev", model="claude-opus")
s = Society(name="ui-collab")
s.cooperate(
[designer, frontend],
Cooperation(shared=["mockups", "component-api"]),
)
Both agents can see all shared artifacts and each other's work logs, enabling tight collaboration.
Edge Cases and Validation¶
Duplicate agent names¶
Agent names must be unique within a Society. Attempting to add two agents with the same name raises a ValueError:
s.add(Agent(name="a", role="x", model="m"))
s.add(Agent(name="a", role="y", model="m")) # ValueError
Self-loops¶
An agent cannot have an edge to itself:
Group edge minimum members¶
Group edges require at least 2 members:
Binary-only edge types in group context¶
Oversight and Delegation are binary only and cannot be used with group edges. Use connect() for pairwise relationships instead.
Next Steps¶
- Agents guide -- creating and configuring agents
- Edges guide -- deep dive into edge types and their semantics
- Artifacts guide -- versioned shared state between agents
- Runtime guide -- running societies and handling termination