correspond#
correspond: a channel facade for AI agents.
Read, listen, write and identify the sender over GitHub, email, push notifications, Telegram and a web inbox, through one model and one set of verbs:
>>> import correspond
>>> messages = correspond.read("github:octocat/hello-world#1")
>>> messages[0].author.handle, messages[0].authenticity.grade.value
('octocat', 'platform')
>>> correspond.send("ntfy:", "backup finished", dry_run=True).plan
A conversation reference is <channel>:<id>. Each channel implements the operations it
can (read, listen, send, edit, react, upload, verify,
audience, label, unlabel, close, reopen); asking for one it lacks raises NotSupported, and
capabilities() says so in advance. audience() is the exception: it never
refuses, because an audience nobody can compute is public.
correspond knows no people: a message’s author is what the platform attests, and its
authenticity is how sure the channel is.
The command line (correspond read github:octocat/hello-world#1) and the MCP server use
the same verbs, through correspond.tools.
Functions
|
Who can read a conversation, asked of its channel now. |
|
What a channel can do, graded, with its limits. |
The process registry: built on first use, then shared. |
|
|
What would make a binding never match, found when bindings are loaded instead of by messages quietly going unrouted. |
|
What a channel needs and what is missing: the install command, binaries, platform, and every setting's source. |
|
Close a conversation (an issue or a discussion), with an optional |
|
Replace the text of a message correspond's account wrote; the |
|
The adapter registered under |
|
Add |
|
Events since the cursor stored for |
|
A rule sending messages to |
|
A reference normalised by its channel's adapter (kind and parent filled in, the id validated). |
|
Add a reaction to a message (the channel's capabilities list the reactions it accepts); the |
|
The messages of a conversation, oldest first ( |
|
Add an adapter (tests, or a channel defined outside correspond). |
|
Reopen a closed conversation; the |
|
Run the chain (bindings, thread continuity, metadata rules, classifier) and return the first decision, or |
|
Send |
|
Remove |
|
Remove a channel from the registry. |
|
Send a file to a conversation. |
|
Grade an inbound delivery on |
Classes
|
The credentialed endpoint correspond acts through: whose name a write goes out under. |
|
A file that came with a message: referenced by |
|
Who can read a conversation, now and plausibly later, as far as its channel can tell. |
|
Who can read a conversation, now and plausibly later, asked of the platform at the time of the call. |
|
A grade and its evidence, computed on correspond's side of the hop and never read from a payload. |
|
What a channel can do, graded, with its limits. |
|
Who a channel says sent something: its native id and what the platform attests about it. |
|
Where a conversation lives: a channel and that channel's own id, encoded |
|
What to write: the text, and the few things channels share (a title, the message answered, a priority, copies). |
|
Replace the text of a message correspond's account wrote. |
|
Something that happened on a channel, as a listener reports it: dedupe on |
|
How sure the channel is about who sent a message: a vocabulary, not a ranking. |
|
How far back |
|
Settle a conversation (close an issue or a discussion), optionally saying why. |
|
Add labels to a conversation, apart from its opening send. |
|
Events since |
|
One message in a conversation, normalised, with the channel's own fields kept in |
|
Add a reaction to a message. |
|
Messages of a conversation, oldest first; |
|
Unsettle a closed conversation. |
|
Where a message goes, which rule decided, and why. |
|
The widest class of reader a conversation can reach. |
|
What a write did, or would do ( |
|
How well a channel does something: |
|
Remove labels from a conversation. |
|
Send a file to a conversation. |
|
Grade an inbound delivery (a webhook, a posted report) from its headers and raw body. |
|
Send a draft to a conversation. |
Exceptions
|
A platform call that failed, classified so the caller can decide what to do. |
An expected failure, with a message meant for the person or agent that asked. |
|
A conversation reference that does not parse, or that its channel rejects. |
|
|
Something the channel needs is not here: a credential, a binary, an extra, an OS. |
|
Draft to operator: the write waits for the operator's approval. |
|
The channel does not have this operation (or this feature of it). |
|
Block: this draft does not go to this conversation as written. |
|
A write the |
|
A channel name that no registered adapter answers to. |
- class correspond.Account(*, channel, id, acts_as='user')[source]#
Bases:
objectThe credentialed endpoint correspond acts through: whose name a write goes out under.
- class correspond.Attachment(*, ref, media_type='application/octet-stream', name=None, size=None, sha256=None, loader=None)[source]#
Bases:
objectA file that came with a message: referenced by
ref, fetched only oncontent(), never inlined.- content()[source]#
The bytes, fetched now through the channel that produced the attachment.
- Return type:
- class correspond.Audience(*, ref, scope, readers=(), complete=False, classes=(), external=None, retractable=False, durability=(), widening=(), as_of=_now(), evidence=(), defaulted=False)[source]#
Bases:
objectWho can read a conversation, now and plausibly later, as far as its channel can tell.
scopeis the widest class of reader it can reach.readersare the channel identities known to read it, a lower bound unlesscomplete;classesname, in words, the readers that cannot be listed.externalsays whether readers outside the operator’s own accounts exist (None: unknown).durabilityis what a send leaves behind (DURABILITY),wideninghow the readership can grow (WIDENING).evidencerecords the calls made and why anything was assumed;defaultedis true when an unknown resolved topublic, which it always does.Set-valued fields (
readers,classes,durability,widening) are stored deduplicated and sorted, so the same audience compares and hashes alike whatever order a platform listed it in. A durability or widening flag this version does not know is kept, never dropped. A public audience is never complete, retractable or free of external readers, and a defaulted one is always public.>>> a = Audience(ref="github:example/app#12", scope="public", classes=["watchers and participants receive the body by email"], ... durability=["indexed", "archived_by_others", "copies_pushed", "edit_history_visible"], as_of="2026-09-15T12:00:00Z") >>> a.in_words() 'world-readable; emailed to watchers and participants; archived by others; edits keep a visible history; not retractable' >>> Audience.from_dict(a.to_dict()) == a True
- classmethod from_dict(data)[source]#
The inverse of
to_dict(); unknown keys are ignored.- Return type:
- property hash: str#
The SHA-256, in hex, of the canonical JSON of
to_dict()withoutAUDIENCE_UNHASHED.datais exactly whatto_dict()returns, derivedreaders[].addressincluded. Canonical JSON isjson.dumps(data, sort_keys=True, separators=(",", ":"))(Python’s default ASCII escaping), encoded as UTF-8. Approvals bind to this value, so the construction is a contract. Ato_dictoutput can be hashed as it stands; any other dict should go throughAudience.from_dict(d).hash, which normalises.
- class correspond.AudienceReader(*args, **kwargs)[source]#
Bases:
ProtocolWho can read a conversation, now and plausibly later, asked of the platform at the time of the call.
- class correspond.Authenticity(*, grade, evidence=dict())[source]#
Bases:
objectA grade and its evidence, computed on correspond’s side of the hop and never read from a payload.
>>> Authenticity(grade="bound", evidence={"method": "hmac-sha256"}).to_dict() {'grade': 'bound', 'evidence': {'method': 'hmac-sha256'}}
- class correspond.Capabilities(*, channel, read=Support.NONE, listen=Support.NONE, send=Support.NONE, edit=Support.NONE, react=Support.NONE, upload=Support.NONE, verify=Support.NONE, audience=Support.NONE, label=Support.NONE, unlabel=Support.NONE, close=Support.NONE, reopen=Support.NONE, initiate=Support.NONE, reply=Support.NONE, priority=Support.NONE, cc=Support.NONE, notify=Support.NONE, history_depth=HistoryDepth.NONE, listen_modes=(), grades=(), max_text_length=None, max_title_length=None, edit_max_age_s=None, reactions=(), reactions_per_message=None, max_upload_bytes=None, max_attachments=0, formats=('plain',), draft_fields=(), upload_fields=(), native_fields=None, rate_limits=(), notes=())[source]#
Bases:
objectWhat a channel can do, graded, with its limits.
One
Supportper operation inOPERATIONS(audience: can the channel say who reads a conversation;label/unlabel: can labels be added to or removed from a conversation, apart from its opening send;close/reopen: can a conversation be settled, and unsettled), plus five features of writing:initiate(can a write start a conversation; Telegram bots cannot),reply(can a draft answer a specific message),priority,cc(can a draft copy further recipients,Draft.ccandDraft.bcc), andnotify(can the writer guarantee that the people inDraft.notifyare notified: a mention it adds, an address it copies).history_depthsays how far backreadsees;gradesare the authenticity grades the channel can attest;native_fieldsare the keys its messages may carry innative(what a routing condition can test), orNonewhen the channel does not declare them;draft_fieldsare the keys aDraftmay carry in itsnativefor this channel (GitHub:labels,assignees,milestonewhen opening an issue), and any other key is refused rather than dropped;upload_fieldsare the optionsuploadtakes in itsnative.max_attachmentsis how many files a send’sDraft.attachmentsmay carry (0: a send takes none, thoughuploadmay still send a file on its own).
- exception correspond.ChannelError(message, *, kind, retryable=False, retry_after=None)[source]#
Bases:
CorrespondErrorA platform call that failed, classified so the caller can decide what to do.
- class correspond.ChannelIdentity(*, channel, native_id, handle=None, display_name=None, is_bot=False, is_self=False, authority=None)[source]#
Bases:
objectWho a channel says sent something: its native id and what the platform attests about it. Not a person.
- property address: str#
the form a people registry resolves.
>>> ChannelIdentity(channel="github", native_id="583231", handle="octocat").address 'github:octocat'
- Type:
<channel>:<handle>, or the native id when there is no handle
- class correspond.Closer(*args, **kwargs)[source]#
Bases:
ProtocolSettle a conversation (close an issue or a discussion), optionally saying why.
- class correspond.ConversationRef(*, channel, id='', kind='', parent=None)[source]#
Bases:
objectWhere a conversation lives: a channel and that channel’s own id, encoded
<channel>:<id>.Two references are equal when channel and id are.
kind(issue,discussion,chat,address, …) andparentdescribe the conversation and are filled in by the channel’s adapter, so a freshly parsed reference equals its normalised form.>>> ConversationRef.parse("telegram:-4001/7") == ConversationRef(channel="telegram", id="-4001/7", kind="topic") True >>> ConversationRef.parse("nonsense") Traceback (most recent call last): ... correspond.errors.InvalidRef: 'nonsense' is not a conversation reference: the form is <channel>:<id>, e.g. github:octocat/hello-world#1
- classmethod from_dict(data)[source]#
The inverse of
to_dict(); a bare encoded string also works.- Return type:
- exception correspond.CorrespondError[source]#
Bases:
ExceptionAn expected failure, with a message meant for the person or agent that asked.
- class correspond.Draft(*, text, title=None, reply_to=None, priority=None, cc=(), bcc=(), native=dict(), notify=(), attachments=())[source]#
Bases:
objectWhat to write: the text, and the few things channels share (a title, the message answered, a priority, copies).
ccandbccare further recipients, on channels whose capabilities gradecc(email). They count in the conversation’s audience.>>> Draft(text="hi", cc=["bob@example.org", " "]).cc ('bob@example.org',)
nativecarries the fields only one channel has, in that channel’s vocabulary – the write-side mirror ofMessage.native. Which keys a channel takes isCapabilities.draft_fields;sendrefuses any other key rather than dropping it.>>> Draft(text="hi", native={"labels": ["question"]}).native {'labels': ['question']}
notifynames who must be notified: handles on a mention platform (octocator@octocat), addresses on email. The writer guarantees it – a mention added when the text lacks one, an address put in Cc when it is not in To or Cc – and a channel that cannot raisesNotSupported("notify")(seecorrespond.notify).>>> Draft(text="hi", notify=["octocat", " ", "Octocat", "hubot"]).notify ('octocat', 'hubot')
attachmentsare files that go with the text, each anAttachmentwhosecontent()gives its bytes, on channels whose capabilities gradeupload. Thebefore_sendcheck sees them (name, media type, size, SHA-256,ref), never their bytes.
- class correspond.Editor(*args, **kwargs)[source]#
Bases:
ProtocolReplace the text of a message correspond’s account wrote.
- class correspond.Event(*, kind, channel, delivery_id, cursor=None, message=None, payload=dict())[source]#
Bases:
objectSomething that happened on a channel, as a listener reports it: dedupe on
delivery_id, resume fromcursor.
- class correspond.Grade(*values)[source]#
Bases:
StrEnumHow sure the channel is about who sent a message: a vocabulary, not a ranking.
forged: verification was attempted and failed.claimed: nothing verifiable (a typed name, an unauthenticated From header, anonymous input).platform: the platform authenticated the account (GitHub, Telegram).domain: email authenticated by your own receiving server’s Authentication-Results.bound: a host application signed an assertion about its logged-in user.crypto: a signed payload (a GitHub webhook’sX-Hub-Signature-256).
A policy names the grades it accepts for a permission. Comparing grades with
<raises, becausedomainis neither stronger nor weaker thanplatform.>>> Grade.CLAIMED < Grade.PLATFORM Traceback (most recent call last): ... TypeError: authenticity grades are not ranked; test membership in the grades a policy accepts
- class correspond.HistoryDepth(*values)[source]#
Bases:
StrEnumHow far back
readsees: all history, a 24-hour buffer, only what correspond has seen since it was linked or started listening, or nothing.
- exception correspond.InvalidRef[source]#
Bases:
CorrespondError,ValueErrorA conversation reference that does not parse, or that its channel rejects.
- class correspond.Labeler(*args, **kwargs)[source]#
Bases:
ProtocolAdd labels to a conversation, apart from its opening send.
- class correspond.Listener(*args, **kwargs)[source]#
Bases:
ProtocolEvents since
cursor, oldest first, each carrying the cursor to resume after it.
- class correspond.Message(*, id, conversation, author, authenticity, sent_at, text, body=None, body_format='plain', attachments=(), reply_to=None, thread_root=None, edited_at=None, url=None, native=dict(), raw=None)[source]#
Bases:
objectOne message in a conversation, normalised, with the channel’s own fields kept in
native.
- exception correspond.MissingRequirement(channel, missing, *, fix, kind='unavailable')[source]#
Bases:
ChannelErrorSomething the channel needs is not here: a credential, a binary, an extra, an OS.
- exception correspond.NeedsApproval(reason, **details)[source]#
Bases:
StoppedDraft to operator: the write waits for the operator’s approval. Raised by a
before_sendcheck.
- exception correspond.NotSupported(operation, channel, *, alternatives=())[source]#
Bases:
CorrespondErrorThe channel does not have this operation (or this feature of it). Never a silent no-op.
- class correspond.Reader(*args, **kwargs)[source]#
Bases:
ProtocolMessages of a conversation, oldest first;
limitkeeps the most recent.
- exception correspond.Refused(reason, **details)[source]#
Bases:
StoppedBlock: this draft does not go to this conversation as written. Raised by a
before_sendcheck.
- class correspond.RouteDecision(*, target, rule, reason)[source]#
Bases:
objectWhere a message goes, which rule decided, and why.
- class correspond.Scope(*values)[source]#
Bases:
StrEnumThe widest class of reader a conversation can reach.
operator: only the operator’s own devices (a notification banner).named: exactly the explicit recipients (an email, a user’s private repository).group: a bounded membership (a Telegram group).org: an organisation or workspace (a private organisation repository).public: anyone. What an unknown audience resolves to.
- class correspond.SendResult(*, ok, channel, conversation, operation='send', dry_run=False, message_id=None, url=None, account=None, plan=dict(), error=None, error_kind=None, retryable=False, retry_after=None)[source]#
Bases:
objectWhat a write did, or would do (
dry_run), or why it failed and whether a retry could help.- classmethod failure(error, *, channel, conversation, operation='send', dry_run=False, plan=None)[source]#
A result carrying a
ChannelError’s classification.- Return type:
- exception correspond.Stopped(reason, **details)[source]#
Bases:
CorrespondErrorA write the
before_sendcheck did not let leave:error_kindsays how,reasonsays why.Keyword
details(an approval id, a draft hash) travel with the result, in the plan’sbefore_send_details.
- class correspond.Support(*values)[source]#
Bases:
StrEnumHow well a channel does something:
full,partial(with limits innotes), ornone.
- exception correspond.UnknownChannel(channel, *, known=(), hint='')[source]#
Bases:
CorrespondError,LookupErrorA channel name that no registered adapter answers to.
- class correspond.Unlabeler(*args, **kwargs)[source]#
Bases:
ProtocolRemove labels from a conversation.
- class correspond.Verifier(*args, **kwargs)[source]#
Bases:
ProtocolGrade an inbound delivery (a webhook, a posted report) from its headers and raw body.
- correspond.audience(ref, draft=None, *, registry=None)[source]#
Who can read a conversation, asked of its channel now. Never raises: unknown resolves to public.
A malformed reference, an unknown or planned channel, an adapter without an audience reader, and any error while computing all give
scope="public",complete=False,defaulted=True, with the reason inevidence. Nothing is cached: call it again right before sending.- Return type:
- correspond.capabilities(channel, *, registry=None)[source]#
What a channel can do, graded, with its limits.
- Return type:
- correspond.channel_registry()#
The process registry: built on first use, then shared.
- Return type:
- correspond.check_binding(pattern, *, registry=None)[source]#
What would make a binding never match, found when bindings are loaded instead of by messages quietly going unrouted.
Reports a pattern without a channel, an unknown channel, a ref that is not in the channel’s canonical form (e.g. GitHub references are lower-cased by
parse_ref(), sobinding_matches, which compares the pattern literally, never matches a differently-cased pattern against it), and a condition on a field the channel’s messages never carry (itsnative_fields, plusauthorandgrade). A channel written as a wildcard, or one that does not declare its fields (native_fieldsisNone), is not checked for fields. A ref with wildcards, or one the channel’s own parser rejects outright, is not checked for canonical form – a malformed ref is a different problem than a mis-cased one.>>> from correspond.channels.github import GitHub >>> check_binding("github:example/app?labels=bug", registry={"github": GitHub()}) [] >>> check_binding("github:example/app?label=bug", registry={"github": GitHub()})[0].split(":")[0] 'the condition label=bug never matches' >>> check_binding("github:Example/App", registry={"github": GitHub()}) ["github:Example/App is not in canonical form: github references are normalised to 'github:example/app'; binding_matches compares refs literally, so this pattern will not match github:example/app"]
- correspond.check_requirements(channel, *, registry=None, config=None, run=<function run>)[source]#
What a channel needs and what is missing: the install command, binaries, platform, and every setting’s source. Never a secret’s value.
- Return type:
- correspond.close(ref, *, reason=None, dry_run=False, registry=None, before_send=None)[source]#
Close a conversation (an issue or a discussion), with an optional
reasonin the channel’s vocabulary; thebefore_sendcheck sees the reason (orclose) as the draft’s text.- Return type:
- correspond.edit(ref, message_id, text, *, dry_run=False, registry=None, before_send=None)[source]#
Replace the text of a message correspond’s account wrote; the
before_sendcheck runs first, as forsend().- Return type:
- correspond.get_channel(name, *, registry=None)[source]#
The adapter registered under
name;UnknownChannelsays what to do if there is none.- Return type:
- correspond.label(ref, labels, *, dry_run=False, registry=None, before_send=None)[source]#
Add
labelsto a conversation; thebefore_sendcheck sees them, comma-joined, as the draft’s text.- Return type:
- correspond.listen(ref, *, cursors=None, limit=None, commit=True, registry=None)[source]#
Events since the cursor stored for
ref; each cursor is stored once the consumer moves past its event.cursorsdefaults to files under the data root. Events can repeat after a crash or an earlybreak: deduplicate ondelivery_id.
- correspond.metadata_rule(target, *, name=None, **conditions)[source]#
A rule sending messages to
targetwhen everyfield=globcondition holds.>>> from correspond.testing import demo_message >>> rule = metadata_rule("urgent-queue", labels="priority:high") >>> rule(demo_message(labels=["priority:high"])), rule(demo_message()) ('urgent-queue', None)
- correspond.parse_ref(ref, *, registry=None)[source]#
A reference normalised by its channel’s adapter (kind and parent filled in, the id validated).
- Return type:
- correspond.react(ref, message_id, reaction, *, dry_run=False, registry=None, before_send=None)[source]#
Add a reaction to a message (the channel’s capabilities list the reactions it accepts); the
before_sendcheck sees the reaction as the draft’s text.- Return type:
- correspond.read(ref, *, since=None, limit=None, registry=None)[source]#
The messages of a conversation, oldest first (
limitkeeps the most recent ones).
- correspond.register_channel(adapter, *, name=None, replace=False, registry=None)[source]#
Add an adapter (tests, or a channel defined outside correspond).
replaceswaps out an existing one.- Return type:
- correspond.reopen(ref, *, dry_run=False, registry=None, before_send=None)[source]#
Reopen a closed conversation; the
before_sendcheck seesreopenas the draft’s text.- Return type:
- correspond.route(message, *, bindings=None, threads=None, rules=(), classifier=None)[source]#
Run the chain (bindings, thread continuity, metadata rules, classifier) and return the first decision, or
None.- Return type:
- correspond.send(ref, text, *, title=None, reply_to=None, priority=None, cc=(), bcc=(), native=None, notify=(), attachments=(), dry_run=False, registry=None, before_send=None, idempotency_key=None, sends=None)[source]#
Send
text(or aDraft) to a conversation;dry_runshows the plan and sends nothing.attachments(Draft.attachments) are files that go with the text, on channels that gradeupload, each within itsmax_upload_bytes.ccandbcccopy further recipients, on channels that gradecc(email).nativecarries channel-specific fields (Draft.native), checked against the channel’scapabilities.draft_fields: an unknown key is a validation failure, never silently dropped.notifynames who must be notified (Draft.notify); the channel’s writer guarantees it (a mention added, an address copied) or the send fails withNotSupported("notify").before_send(ref, draft, audience)runs first, on the dry run too; whenNone, the config’sbefore_sendreference, elsecorrespond.outbound.notice().idempotency_keymakes sending the same message again safe: a key that already sent answers that send’s result and posts nothing, and one whose earlier attempt may have gone out is confirmed by reading the conversation back, or refused asunconfirmed(correspond.idempotency).sendsis where keys are kept: by default files under the data root (correspond.stores.send_store()). A mapping with aclaim(key, record) -> bool(as that store has) is exclusive across processes; a plain one is checked, then set, which covers one process.- Return type:
- correspond.unlabel(ref, labels, *, dry_run=False, registry=None, before_send=None)[source]#
Remove
labelsfrom a conversation; thebefore_sendcheck sees them, comma-joined, as the draft’s text.- Return type:
- correspond.unregister_channel(name, *, registry=None)[source]#
Remove a channel from the registry.
- Return type:
- correspond.upload(ref, name, data, *, media_type='application/octet-stream', dry_run=False, source=None, native=None, registry=None, before_send=None, idempotency_key=None, sends=None)[source]#
Send a file to a conversation.
nativecarries the channel’s own options, the keys itscapabilities.draft_fieldslist (Telegram:{"as_document": true}sends an image as a file, not a photo).idempotency_keykeeps a retry from posting the file twice, as forsend(); an upload is not read back, so one whose earlier attempt may have gone out is refused asunconfirmed.The
before_sendcheck sees the file name as the draft’s text (operation="upload") and the file’s metadata indraft.native["upload"]:name,media_type,bytes,sha256and, when the caller read it from disk,source(its path), so a check can hold a large, unknown or out-of-place file. It never sees the contents: a check that must vet them should hold or refuse.- Return type:
- correspond.verify(channel, headers, body, *, registry=None)[source]#
Grade an inbound delivery on
channelfrom its headers and raw body.- Return type:
Modules
What the sender of an email actually wrote: their text, apart from quoted replies, forwarded blocks, signatures and disclaimers. |
|
Serve listeners as a Claude Code channel: new messages pushed into a running session, replies through the ordinary tools. |
|
The built-in channel adapters, one module each. |
|
What correspond raises, and the vocabulary a failed write reports. |
|
Idempotent sends: an |
|
MCP over stdio: the same tools, for Claude Desktop and other local MCP clients. |
|
The data model every channel is described in. |
|
Make a draft's |
|
The operations: small protocols an adapter implements a subset of, and the verbs that call them. |
|
The |
|
Which channels exist: the built-in channel table, the registry built from it, and what each channel needs. |
|
Turning a tool's result into terminal output: |
|
Deciding what a message is about: a transparent rule chain. |
|
Where correspond keeps state and reads configuration, and how it finds a secret. |
|
The state stores: listen cursors, the web inbox's reports and blobs, the Telegram log. |
|
An in-memory channel for tests and rehearsals, and |
|
The single source of truth for every surface: plain functions, flat arguments in, JSON-ready dicts out. |