The core idea: state is conveyed through meaning, not through pre-defined structures.
Traditional protocols pre-define a set of states (pending/running/complete) and force participants to pick one. This requires the protocol designer to anticipate every possible state at design time — impossible in practice. Each unanticipated state becomes a "patch" to the protocol.
IOA's approach: participants describe state, context, and intent in content using natural language or free-form structured data. Receivers (AI or human) extract information by understanding semantics. The protocol defines no business semantics — only the mechanism to transmit them.
This isn't laziness about schema — it's the correct use of AI's capability. When the receiver has semantic understanding, pre-defined structure is redundant constraint. When it doesn't, the optional content_schema provides structure at the application layer.
| Traditional Protocol | IOA |
|---|---|
| Pre-defined task states (5/10/20 states) | State is natural language in content — unlimited |
| Pre-defined fields (priority, deadline, assignee...) | Participants freely define fields in content |
| Fixed message types (request/response/notification) | Message type emerges from content and graph structure |
| Metadata carries business semantics | content carries all semantics; system fields are strictly non-semantic |
The protocol doesn't distinguish Agent from Human. Both are Node — equal participants.
Traditional systems cast Humans as "approvers" and Agents as "executors", hardcoding role asymmetry into the protocol layer. IOA's position: the protocol only cares about address and message format, not whether the participant is carbon or silicon.
A Human can initiate tasks like an Agent. An Agent can approve work like a Human. Capability differences (response speed, concurrency, expertise) are declared in metadata, not enforced by protocol.
The entire protocol:
| Layer | Concepts | Count |
|---|---|---|
| L0 (Infrastructure) | Space | 1 |
| L1 (Protocol) | Node, Message, Ref | 3 |
| L2 (Application) | Checkpoint, Team, Handoff, Swarm... | Emergent from L0+L1 |
Tools exposed to participants:
| Tool | Description |
|---|---|
ioa_space |
Declare entry into a collaboration domain |
ioa_send |
Write a message |
ioa_read |
Read messages |
Participants need to understand 3 core concepts and 3 tools. All complex interaction patterns (approval flows, parallel dispatch, DAG convergence, task handoff) compose from primitives — no additional protocol-level concepts needed.
The protocol defines mechanism, not policy.
- Mechanism: Messages reference other Messages and Nodes through Refs, forming graph structure; Spaces provide isolation boundaries
- Policy: Whether to accept, how to handle timeout, how to compensate failure — decided by the application layer
Optimized for AI agent tool-use:
- Small, complete tool closure:
ioa_space,ioa_send,ioa_readare sufficient for full participation - No implicit session state: Message context lives in the Space's Message Graph, not in connection state
- Composition over memory: Complex patterns emerge from composing simple operations; AI doesn't need to remember special APIs
L0 (Infrastructure) Space — isolation boundary, server-managed
L1 (Protocol) Node Message Ref — participants, communication, association
L2 (Application) Checkpoint Team Handoff Swarm ... — emergent patterns from L0+L1
- L0 is infrastructure — isolation, authentication, Message Graph container. Managed by the server, transparent to participants.
- L1 is the protocol core — participants (Node), communication units (Message), and their references (Ref). Participants interact directly with L1.
- L2 is collaboration patterns — emerging from L0+L1 composition, not part of the protocol itself.
Space is the Message Graph's container and isolation boundary.
Space { id: string, name: string }
Why L0: Space is a prerequisite for all interaction — without it, Messages have nowhere to be stored, Refs have nothing to point to. It's infrastructure, not something participants interact with directly.
Key properties:
- Idempotent by name: same name always returns the same Space
- Isolation is unbreakable:
refs.messagescannot cross Space boundaries. Each Space's Message Graph is self-contained - Declarative join:
ioa_space(name, description)joins and declares intent; produces no Message, doesn't alter the Message Graph - No Space-to-Space relations: the protocol doesn't define parent/child, bridge, or nesting between Spaces. Cross-Space communication happens via Nodes sending messages in different Spaces
Semantic isolation: when a Team discusses internally in Space B, their discussion doesn't pollute Space A's message graph. Space A only sees the final conclusion when a Node explicitly sends it there.
Node { id: string, name: string, meta: object }
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Globally unique; SDK- or server-generated |
name |
string | yes | Human-readable name |
meta |
object | no | Arbitrary metadata, default {} |
- No distinction between Agent and Human —
meta.kindis metadata, doesn't change protocol behavior - Can exist in multiple Spaces simultaneously
descriptionis per-Space (declared viaioa_space), not a global Node property
External identity is an SDK extension, not another protocol entity. An
integrating system implements protocols.Identity; Client.Bind keeps its
IdentityBinding in memory and includes it in registration. The server uniquely
indexes (namespace, subject), so a new process can resolve the same IOA Node
from the identity owned by its host system.
Message { id, sender, created_at, content, refs }
| Field | Type | Description |
|---|---|---|
id |
string | Globally unique, server-generated |
sender |
string | Sender Node ID |
created_at |
string | Server write time, RFC3339 UTC |
content |
object | Arbitrary structured payload, cannot be null |
refs |
Ref | References |
5 public fields only. Implementations may maintain internal fields (space_id, append position) but must not expose them.
Immutable: once written, cannot be modified or deleted. Space is an append-only log.
Ref { messages: string[], nodes: string[] }
Two forms, one mechanism — pointer arrays on Messages:
| Form | Points to | Semantics |
|---|---|---|
refs.messages |
Message IDs (same Space) | Causal chain, builds graph structure |
refs.nodes |
Node IDs | Recipient routing |
Both are arrays: supporting DAG merges (multiple parents) and multiple recipients. Empty [] means no reference.
Why only two forms: Ref → Space is unnecessary. Space reachability is conveyed through content (e.g. including a Space ID in the message body). Adding Ref → Space would force the protocol to define "what does referencing a Space mean" — violating mechanism/policy separation.
Messages form a directed graph through refs.messages. The protocol doesn't prescribe the shape — structure emerges from usage:
Root Thread Tree DAG
[M1] [M1] [M1] [M1] [M2]
↑ ↗ ↖ ↖ ↗
[M2] [M2] [M3] [M3]
↑
[M3]
| Pattern | Formation | Use case |
|---|---|---|
| Root | refs.messages = [] and refs.nodes = [] |
Public entry point |
| Thread | Each message refs exactly one parent | Linear conversation |
| Tree | Multiple messages ref the same parent | Task decomposition |
| DAG | One message refs multiple parents | Result aggregation |
Context = traversing all ancestors along refs.messages from any Message. New Nodes replay Message history to get full context — "state" at any moment is a projection of the Message Graph.
| Param | Type | Required |
|---|---|---|
name |
string | yes |
description |
string | yes |
- Auto-register caller Node if not yet registered
- Create or get Space by name (idempotent)
- Add caller to Space, record
description(can be updated) - Return Space state + root messages
| Param | Type | Required |
|---|---|---|
space_id |
string | yes |
content |
object | yes |
refs |
Ref | no |
content_schema |
object | no |
- Auto-ensure Node is registered
contentcannot be null- If
content_schemaexists: validate it's a legal JSON Schema; store as declarative metadata on the Message (does not validate any Message's content) refsdefaults to{messages:[], nodes:[]}- Validate refs (messages must exist in same Space; nodes must be registered)
- Generate Message ID, append to Space
- Return complete Message
| Param | Type | Required |
|---|---|---|
space_id |
string | yes |
message_id |
string | no |
direction |
string | no |
after |
string | no |
limit |
int | no |
all |
bool | no |
listen |
bool | no |
Read modes (by priority):
| Condition | Returns |
|---|---|
message_id set |
Related subgraph (ancestors + descendants) |
all = true |
All messages in Space |
| Caller identity known | Messages where refs.nodes includes caller |
| Default | Root messages |
Direction (with message_id): upstream (ancestors only), downstream (descendants only), omit for both.
SSE streaming: listen = true opens a long-lived connection pushing new messages.
Pagination: after (cursor) + limit (max count).
| Category | Trigger |
|---|---|
| not_found | Node / Space / Message doesn't exist |
| invalid_input | Validation failure: empty name, null content, invalid refs, non-positive limit, invalid JSON Schema |
| internal | Implementation error |
| Dimension | MCP | A2A | IOA |
|---|---|---|---|
| Core relation | Model ↔ Tool | Agent ↔ Agent | Node ↔ Space ↔ Node |
| Participants | Asymmetric (Host/Client/Server) | Asymmetric (Client/Remote) | Fully equal |
| Human participation | Out of scope | Out of scope | First-class |
| Multi-party | No | Bilateral | Any number |
| Graph structure | None | None | Thread / Tree / DAG |
| State model | Context window | Task object | Append-only Message Graph |
| Concept count | ~10 | ~8 | 4 |
| Theory | Core Insight | IOA Correspondence |
|---|---|---|
| π-calculus (Milner 1992) | Channel names can be sent on channels | Space ID can be sent in Message content |
| Actor Model (Hewitt 1973) | Everything is an Actor; Create/Send/Become | Everything is a Node; ioa_space/ioa_send/ioa_read |
| Event Sourcing | Event sequence is the single source of truth | Message Graph is the single source of truth |
| Lamport Causality (1978) | Only causal relationships are reliable ordering | refs.messages establishes causal chains |
| Graph Theory | DAG topological sort ensures acyclic traversal | Context traversal = DAG ancestor topo-sort |