Commitments
A commitment is a small obligation an agent infers during a
heartbeat tick: “I should ping Anita tomorrow about the Q4 plan”,
“Reply to legal’s email by Friday”. Commitments are stored as
agent-memory entries with category='commitment', a due_at
timestamp, and a state machine (pending → attempted → sent /
dismissed).
Storage
Section titled “Storage”Commitments live in the unified linkworld_memory table (extension
in migration 278):
| Column | Notes |
|---|---|
tenant_id, scope='agent', owner_id=agent_id | Same as other memory |
category='commitment' | Distinguishes from learnings / facts |
kind | Optional sub-type (“follow_up”, “review”, …) |
due_at | TIMESTAMPTZ — when delivery should fire |
state | pending / attempted / sent / dismissed |
delivery_target | JSONB — channel + ref for delivery |
tags, title, content | Free-form |
The state column has a CHECK constraint paired with category — non-
commitment memory rows have state = NULL.
SDK API
Section titled “SDK API”ctx.commitments.add
Section titled “ctx.commitments.add”async def schedule_followup(ctx: Context, lead_id: str): await ctx.commitments.add( from_agent="cmo", title="Q4 plan check-in with Anita", content="Recap launch metrics; confirm next sprint priority.", due_at_iso="2026-05-05T09:00:00Z", delivery_target={"kind": "room", "room_id": "<uuid>"}, tags=["q4", "anita"], )| Field | Required | Notes |
|---|---|---|
from_agent | yes | Agent slug owning this commitment |
title | no | Short label for inbox display |
content | yes | Free-form description |
due_at_iso | yes | ISO-8601 with timezone |
delivery_target | no | {kind, …} — see below |
tags | no | List of strings |
Returns the created commitment row.
Delivery target shapes
Section titled “Delivery target shapes”| Kind | Fields | Behavior on due |
|---|---|---|
room | room_id | Post into the named group room |
dm | user_id | Send DM via channel system |
chat | conversation_id | Post into a specific user-facing conversation |
| (unset) | — | System message in agent’s main conversation |
ctx.commitments.list
Section titled “ctx.commitments.list”async def review_my_commitments(ctx: Context): rows = await ctx.commitments.list( from_agent="cmo", state="", # default: pending+attempted; "" or omit = same # pass explicit state name to filter limit=50, ) for r in rows: ctx.logger.info("%s due=%s", r["title"], r["due_at"])Returns a list of commitment dicts. state="" (sentinel for “all
states”) or one of pending / attempted / sent / dismissed.
ctx.commitments.dismiss
Section titled “ctx.commitments.dismiss”await ctx.commitments.dismiss( commitment_id="<uuid>", from_agent="cmo",)Server-side validates the agent owns the commitment (caller’s app
matches the commitment’s owning agent) before flipping state to
dismissed.
Inference: where most commitments come from
Section titled “Inference: where most commitments come from”The heartbeat invoker runs a post-pass after every substantive
(non-HEARTBEAT_OK) heartbeat tick. The agent is asked, with a small
structured-output prompt:
Did anything in your last response commit you to follow-up action? Return JSON:
{commitments: [{title, content, due_at, delivery_target?, tags?}]}. If nothing →{commitments: []}.
The platform validates each inferred commitment:
due_atbounds: rejects > 1 h in the past or > 365 d in the future (cuts hallucinated dates)- Dedup: same agent + similar content within 24 h is silently skipped (no spam from repeated heartbeats)
- Cap per tick: maximum 5 commitments inferred per heartbeat run
Inferred commitments land in state='pending'.
Delivery flow (atomic claim)
Section titled “Delivery flow (atomic claim)”When due_at is reached, the heartbeat invoker (or a dedicated
delivery worker) runs:
- Atomic claim:
UPDATE … SET state='attempted' WHERE id=$1 AND state='pending' RETURNING *. If no row was updated (someone else got it first, or it’s been dismissed), skip. - Try delivery based on
delivery_target.kind. On success →state='sent'. On failure → leave atattempted, log the error, retry on the next heartbeat tick (with cooldown).
The atomic claim prevents double-delivery between concurrent workers.
Conversation post-pass on ticks
Section titled “Conversation post-pass on ticks”Every substantive heartbeat tick (where the agent did respond
with non-HEARTBEAT_OK content):
1. Detect HEARTBEAT_OK / escalation marker / record conversation2. Selective memory writeback (compact summary)3. Commitment inference (agent self-reflection, JSON output)4. Commitment delivery scan (any commitments due now?)The post-pass is wrapped in try/except so failures don’t break the heartbeat happy path. If inference or delivery errors out, the tick still succeeds (the user-visible reply still goes through).
Sandbox / public-route stubs
Section titled “Sandbox / public-route stubs”ctx.commitments is sandboxed on public HTTP routes:
@app.http_route(public=True, path="/webhook/stripe")async def stripe_callback(ctx: Context, req: Request): # ctx.commitments.add(...) raises ToolCallError here — # public callbacks have no agent identityMove commitment writes to a private route or a tool handler.
MockCommitments (testing)
Section titled “MockCommitments (testing)”from linkworld_sdk import MockCommitments
mock = MockCommitments()await mock.add( from_agent="cmo", content="follow up tomorrow", due_at_iso="2026-05-05T09:00:00Z",)assert len(mock.added) == 1assert mock.added[0]["from_agent"] == "cmo"In-process recording for unit tests. No actual DB writes; just captures the calls so you can assert your handler logic.
Limits
Section titled “Limits”- Max commitments per
listcall: 200 - Max tags per commitment: 10
- Content size: 10 000 chars
due_atwindow: −1 h to +365 d fromnow()- Inferred commitments per tick: 5
See also
Section titled “See also”- Platform-side: Commitments inbox for the user-facing UI
- Heartbeats — heartbeats are where most commitments come from
- Group rooms — the most common delivery target