Skip to content

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).

Commitments live in the unified linkworld_memory table (extension in migration 278):

ColumnNotes
tenant_id, scope='agent', owner_id=agent_idSame as other memory
category='commitment'Distinguishes from learnings / facts
kindOptional sub-type (“follow_up”, “review”, …)
due_atTIMESTAMPTZ — when delivery should fire
statepending / attempted / sent / dismissed
delivery_targetJSONB — channel + ref for delivery
tags, title, contentFree-form

The state column has a CHECK constraint paired with category — non- commitment memory rows have state = NULL.

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"],
)
FieldRequiredNotes
from_agentyesAgent slug owning this commitment
titlenoShort label for inbox display
contentyesFree-form description
due_at_isoyesISO-8601 with timezone
delivery_targetno{kind, …} — see below
tagsnoList of strings

Returns the created commitment row.

KindFieldsBehavior on due
roomroom_idPost into the named group room
dmuser_idSend DM via channel system
chatconversation_idPost into a specific user-facing conversation
(unset)System message in agent’s main conversation
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.

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_at bounds: 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'.

When due_at is reached, the heartbeat invoker (or a dedicated delivery worker) runs:

  1. 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.
  2. Try delivery based on delivery_target.kind. On success → state='sent'. On failure → leave at attempted, log the error, retry on the next heartbeat tick (with cooldown).

The atomic claim prevents double-delivery between concurrent workers.

Every substantive heartbeat tick (where the agent did respond with non-HEARTBEAT_OK content):

1. Detect HEARTBEAT_OK / escalation marker / record conversation
2. 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).

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 identity

Move commitment writes to a private route or a tool handler.

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) == 1
assert 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.

  • Max commitments per list call: 200
  • Max tags per commitment: 10
  • Content size: 10 000 chars
  • due_at window: −1 h to +365 d from now()
  • Inferred commitments per tick: 5