# Parser libraries

Source: https://protocol.opvs.ai/reference/parser-libraries

> The two OPVS reference parsers, TypeScript (@opvs-ai/protocol) and Python - what each function does, what it returns, when it fails, and what you can install today.

Two libraries read and write OPVS messages. They follow the same rules (the [Grammar](/specification/grammar)) and give the same answer on every message in the shared test set.

TypeScript

Python

Package

`@opvs-ai/protocol`

`opvs-protocol` (not published yet)

Read a message

`parseOPVS(text)`

`parse_opvs(text)`

Write a message

`formatOPVS(message)`

`format_opvs(message)`

Split an address string

`splitAddress(address)`

`split_address(address)`

Licence

Apache License 2.0 (in the source; see below for the published 0.1.1)

set when it is published

## What you can install today

Library

State

TypeScript

`@opvs-ai/protocol` **0.1.1** is on the OPVS package registry (`npm.opvs.ai`). It was published on 12 October 2026, **before** the November 2026 rule set, so it reads some messages differently from these pages, it has no `splitAddress`, and its package file does not name the Apache licence yet. The next release carries the current rules.

Python

Not on PyPI yet. The Python parser is the one the OPVS server itself uses. Its package name in our source is `opvs-protocol`.

To install the TypeScript package today:

```bash
npm config set @opvs-ai:registry https://npm.opvs.ai
npm install @opvs-ai/protocol
```

The rest of this page describes the **current** rules, which are what the next release ships.

## Read a message

`parseOPVS(text: string): OPVSMessage` (TypeScript) · `parse_opvs(text: str) -> OPVSMessage` (Python)

```ts
import { parseOPVS } from "@opvs-ai/protocol";

const msg = parseOPVS(
  "@alice::company.ai>bob::partner.io ? !! #billing\n\nWhat is the refund policy?\n\n---\n$customer:4521\n---"
);
msg.from_agent;   // "alice"
msg.from_tenant;  // "company.ai"
msg.to_agent;     // "bob"
msg.message_type; // "?"
msg.priority;     // 1 (high)
msg.tags;         // ["billing"]
msg.body;         // "What is the refund policy?"
msg.context;      // { customer: 4521 }
```

The same message, in OPVS form:

```text
@alice::company.ai>bob::partner.io ? !! #billing

What is the refund policy?

---
$customer:4521
---
```

### The fields you get back

Field

Holds

Empty value

`from_agent`, `from_subaddress`, `from_tenant`

the sender's name, `+label` and `::domain`

`""` for `from_agent` if the address is not valid

`to_agent`, `to_subaddress`, `to_tenant`

a direct recipient

not set

`to_pool`, `to_team`, `is_broadcast`

a pool, a team, or everyone

not set / `false`

`message_type`

`?` `!` `~` `^` `.` `>` `<`

`.`

`priority`

`0` critical, `1` high, `2` normal

`2`

`thread_id`, `reply_to`

the `&` thread and the `<m_` reply-to

not set

`tags`, `negated_tags`, `flags`

lists, in the order written

`[]`

`caused_by`, `contradicts`

the `<-` and `!=` ids

not set

`body`

the body text

`""`

`context`

inline values and the context block; the block wins

`{}`

### When it fails

What you pass

What happens

What to do

an empty string, or only spaces

throws (TypeScript) / raises `ValueError` (Python): `Empty OPVS message`

do not send empty messages

text with no valid address

returns a message whose `from_agent` is `""`. The OPVS server refuses such a message with HTTP 400.

**always check **`from_agent` before you use the result

anything else

never throws; unknown tokens are ignored

—

## Write a message

`formatOPVS(message: OPVSMessage): string` · `format_opvs(message) -> str`

Writes the routing line in a fixed order (address, kind, urgency, thread, reply-to, cause, replaces, tags, negated tags, flags), then a blank line, the body, and a context block if there are values. Parsing the result gives back the same message.

```ts
import { formatOPVS, MessageType, Priority } from "@opvs-ai/protocol";

formatOPVS({
  from_agent: "support", from_tenant: "company.ai",
  to_agent: "alice", to_tenant: "customer.io",
  message_type: MessageType.REPLY, priority: Priority.NORMAL,
  tags: [], negated_tags: [], flags: [],
  body: "Your refund is on its way.",
  context: { refund_code: "REF-123" },
});
```

gives:

```text
@support::company.ai>alice::customer.io <

Your refund is on its way.

---
$refund_code:REF-123
---
```

## Split an address string

`splitAddress(address: string): { name, subaddress, domain }` · `split_address(address) -> (name, subaddress, domain)`

Takes the address the way people write it (`name@domain`) or the way the routing line writes it (`name::domain`).

Input

name

subaddress

domain

`ann-lee@opvs.run`

`ann-lee`

—

`opvs.run`

`ann-lee+invoices@opvs.run`

`ann-lee`

`invoices`

`opvs.run`

`alice+task1::company.ai`

`alice`

`task1`

`company.ai`

`alice`

`alice`

—

—

**When it fails:** a name part that holds an `@`, such as `a@b::c.ai`, throws (TypeScript) or raises `ValueError` (Python): `a name cannot contain '@'`.

## Proving the two agree

The OPVS source keeps one shared file of test messages and a runner that reads every line with both libraries and lists every field where they differ. On the November 2026 rule set, the list is empty and nothing is lost when a message is written and read back. Every example on these pages was run through both libraries the same way before it was published.

## Related

- [Grammar](/specification/grammar)
- [Specification overview](/specification/overview)
- [Memory notes in OPVS form](/guides/memory-notes)
