Skip to content

gardener

Deterministic planning from needs and offers.

coalitionkit_gardener

The Gardener's deterministic planning core.

Offer dataclass

One participant's offer for one task: minutes, money in minor units, effort.

Overlap dataclass

The slots two or more participants share, as PSI revealed them.

Problem dataclass

Everything the strategies read.

shares are positive integer weights; a participant without one cannot be assigned. answers maps (participant, task_id) to a vault reply: "no" removes the bid, a Cost replaces the offered cost, "yes" or no reply leaves the offer as it is. max_ratio caps hours per share unit for any participant (None means no cap).

without
without(participants: frozenset[str]) -> Problem

The same problem with these participants' offers removed.

bid_table
bid_table() -> dict[tuple[str, str], Bid]

Eligible bids keyed by (task_id, participant); the cheapest offer wins a duplicate.

Change dataclass

Tasks that changed directly, and participants who left or can no longer serve.

AuctionStrategy dataclass

Contract-Net: each task goes to the bid with the lowest cost plus lambda times the bidder's share ratio after winning, within the Problem.max_ratio cap.

The search is depth-first over bids in score order, so the first complete allocation is the greedy one and a dead end backtracks. Ties break by participant ID. After node_limit nodes without a complete allocation it falls back to a greedy partial one.

MatchStrategy dataclass

Assigns each task the earliest slot every party shares, from a PSI overlap result.

Outcome dataclass

What a strategy chose (task to assignee, slot or option) and what it could not.

Strategy

Bases: Protocol

Settles task_ids while the fixed assignments stay as they are.

Unresolved dataclass

A task a strategy could not settle. options lists the tied choices, if any.

VoteStrategy dataclass

Plurality over Problem.ballots. A tie settles nothing and lists the tied options.

TradeoffPolicy dataclass

A plan set holds a tension when an objective's (max - min) / max reaches min_gap.

build_propose

build_propose(
    plan_set: PlanSet,
    *,
    deadline: str,
    proposal_set_id: str,
    revision: int = 1,
) -> ProposePayload

The coalition.propose payload for a non-empty plan set.

decision_deadline

decision_deadline(at: datetime, window: timedelta) -> str

An absolute RFC 3339 UTC deadline window after at.

dominates

dominates(a: Objectives, b: Objectives) -> bool

a is no worse everywhere and strictly better somewhere (all objectives minimise).

generate_plans

generate_plans(
    problem: Problem,
    third: ThirdObjective | None = None,
    config: PlanConfig | None = None,
) -> PlanSet

Up to config.max_plans complete, cap-respecting, mutually non-dominated plans.

invalidated

invalidated(
    change: Change, graph: TaskGraph, current: Assignments
) -> frozenset[str]

The directly changed tasks, the tasks held by a departed participant, and every task that depends on them, directly or through other tasks. Nothing else.

replan

replan(
    problem: Problem,
    current: Assignments,
    invalid: frozenset[str],
    *,
    departed: frozenset[str] = frozenset(),
    strategy: Strategy | None = None,
) -> ReplanResult

Re-plan only invalid tasks against the updated problem. Every other assignment is returned unchanged and counts toward the fairness load.

ripple_payload

ripple_payload(
    change: Change, invalid: frozenset[str]
) -> RipplePayload

The coalition.ripple payload; task IDs must be ULIDs.

tension_for_plan_set

tension_for_plan_set(
    plan_set: PlanSet,
    *,
    deciders: Sequence[str],
    deadline: str,
    policy: TradeoffPolicy | None = None,
    revision: int = 1,
) -> list[TensionPayload]

Tensions for an empty plan set (hard constraints) or for a set with big trade-offs.

tension_from_plans

tension_from_plans(
    plan_set: PlanSet,
    *,
    deciders: Sequence[str],
    deadline: str,
    policy: TradeoffPolicy | None = None,
    revision: int = 1,
) -> TensionPayload | None

A tension when the Pareto plans differ a lot on some objective, else None.

tensions_from_unresolved

tensions_from_unresolved(
    unresolved: Sequence[Unresolved],
    *,
    deciders: Sequence[str],
    deadline: str,
    revision: int = 1,
) -> list[TensionPayload]

One tension per tied vote, plus one for all tasks no plan can cover.