
How AxiOwl Sends Messages Between Agents
AxiOwl does not treat an agent-to-agent message as a loose chat copy operation. The current C++ implementation routes every normal send through a small set of explicit boundaries: a CLI or MCP request enters AxiOwl, sender and target identity are resolved, the final visible body is built, provider delivery is handed to a worker, and provider-specific code writes to the selected provider surface.
That structure matters because "accepted by AxiOwl" is not the same thing as "the provider accepted the message" and neither one is the same thing as "the target replied." AxiOwl keeps those boundaries separate on purpose.
Sources read
README.mddocs/reference/architecture-overview.mddocs/developer/README.mdapps/windows-desktop/src/models.hppapps/windows-desktop/src/cli.cppapps/windows-desktop/src/mcp_server.cppapps/windows-desktop/src/registry.cppapps/windows-desktop/src/message_pipeline.cppapps/windows-desktop/src/final_visible_body_builder.cppapps/windows-desktop/src/provider_edges.cppapps/windows-desktop/src/provider_modules.hppapps/windows-desktop/src/delivery_worker.cppapps/windows-desktop/src/provider_codex.cppapps/windows-desktop/src/provider_vscode_native.cppapps/windows-desktop/src/provider_antigravity.cppapps/windows-desktop/tests/core_tests.cppapps/windows-desktop/tests/mcp_conformance.ps1
Two front doors, one send path
AxiOwl exposes a CLI send command and an MCP tool.
The CLI shape is:
axiowl send --to "Receiver Agent" --body "hello" --from "Sender Agent"
For multiline content, the CLI also supports --stdin. In cli.cpp, run_send reads --to or --target, --body or stdin, --run-id, --strict, --delivery-method, --source-context, --from, and --from-node. Then it constructs a MessageRequest and calls MessagePipeline::send.
The MCP surface is axiowl_send_message. Its tool description says success is an MCP-to-AxiOwl handoff receipt only, not provider delivery or a reply. That is not marketing caution; it is how the code behaves. In mcp_server.cpp, handle_send_message reads the tool arguments, resolves the host identity from MCP metadata, registers the sender when possible, and then calls the same captured CLI path:
send --to <target> --from <resolved sender> --from-node <node> --stdin
The MCP server does not ask the agent to invent a --from value. It uses the host MCP session ID as the stable sender identity key, maps that to the registry, and fails loudly when required metadata is missing. The server handles multiple host kinds, including Codex, VS Code, VS Code Copilot-backed, Cursor, Cursor CLI, Antigravity, Antigravity CLI, Claude Code CLI, Copilot CLI, and OpenCode CLI.
The registry is the routing table
A message target is not just a display string. A registry row has a display name, aliases, provider, provider session ID, node ID, enabled flag, sendable flag, source, timestamps, and last error. Registry::find_agent prefers exact sendable session matches, then enabled session matches, then sendable name or alias matches, then enabled name or alias matches, choosing the newest matching row when duplicates exist.
That lets a human say "Receiver Agent" while AxiOwl routes to the provider-owned session behind that name. It also prevents stale rows from being treated as stronger than fresh provider evidence.
Sender identity gets similar care. sender_from_registry_or_explicit resolves an explicit sender through the registry when it can. If the sender looks like a session identifier but does not match a registered sender, it does not silently pretend the identity is resolved. The message pipeline can run a targeted sender discovery repair once, then it either proceeds with a resolved sender or rejects the send.
What the message pipeline proves
MessagePipeline::send starts by creating a new message ID and writing a delivery stage that says the MCP or CLI request entered AxiOwl. That stage still does not imply provider delivery.
The pipeline then validates the target and body. A missing target fails as target is required. An empty body fails as message body is empty.
Next it resolves sender identity. For normal sends, unresolved sender identity fails before provider dispatch. For final-visible relay bodies, the sender identity must already be carried with the relay request. The pipeline then resolves the target. If the target is missing or needs repair, it runs one targeted discovery pass and checks the registry again. If the target still is not usable, the send is rejected with a discovery summary.
Only after the sender is resolved, the target is resolved, and the target is marked sendable does AxiOwl set:
accepted_by_axiowl = true
state = accepted_by_axiowl
detail = axiowl_receipt_only
The evidence log records the receipt boundary as:
after_target_resolution_before_delivery_worker_or_provider
That exact boundary is important. It means the user-facing send receipt is a local AxiOwl handoff receipt. It proves the request got through validation and routing. It does not claim the provider has accepted the message.
The test suite locks this behavior down. core_tests.cpp verifies that a successful send returns accepted_by_axiowl, does not set accepted_by_provider, logs delivery_receipt as axiowl_only, logs the receipt boundary before worker/provider work, and starts the background handoff afterward.
The final visible body
After the AxiOwl receipt boundary is established, the pipeline builds the text that the target agent will actually see. This happens in final_visible_body_builder.cpp.
For a resolved sender, the final visible body has this shape:
Message from <sender agent>
<original body>
If requested to, respond with this exact MCP tool call:
mcp__axiowl.axiowl_send_message({
"to": "<sender agent>",
"body": "<full reply>"
})
When the pipeline has a run ID or message ID, the helper also includes runId and receiptForMessageId. That gives the target a concrete way to correlate a reply with the original send.
The body builder does not append a shell fallback command. The current tests explicitly reject older helper wording that mentioned axiowl send, --from, or command-only fallback instructions. The supported reply instruction is the MCP tool call.
This design keeps the provider edge simple. Provider modules receive final_visible_body. They do not rebuild helper text, summarize the body, invent a sender, or rewrite the return path. If a provider needs quoting or transport encoding, it can do that, but the visible content is already decided.
Provider edges choose the actual delivery adapter
provider_edges.cpp is the dispatch point. It lowercases and trims the target provider from the registry row, then calls the matching provider module. Current delivery branches include:
codexcodex_clivscode_nativevscode_copilot_backedantigravityantigravity_clicursorcursor_cliclaude_code_clicopilot_cliopencode_cli
Unknown providers fail with an explicit unknown_provider result. In this build, remote delivery returns an out-of-scope result instead of pretending to work.
Each provider module has its own delivery mechanics. The Codex code can write the final visible body to a temporary file and resume a Codex thread, or use Codex Desktop IPC on Windows when that path is available. The VS Code native provider writes a bridge command, triggers a vscode:// bridge URI, and waits for a result file when the path is configured to wait. The Antigravity provider finds the live language server, derives the AgentAPI endpoint, calls agentapi send-message, and treats the send as accepted only when the echoed content matches the final visible body.
Those are different transports, but they share the same contract: take a ProviderRequest, use the target provider session ID, send the final visible body, and return a ProviderResult with provider, state, accepted, degraded, method_used, evidence, and error.
The delivery worker keeps receipts honest
Normal sends use handoff_provider_request_through_delivery_worker. The pipeline writes a temporary JSON request containing the run ID, message ID, target display name, provider, provider session ID, node ID, sender identity, requested delivery method, strict flag, and final visible body. Then it starts:
axiowl delivery-worker --request <request-json-file>
as a detached background process.
That is why the sender gets an AxiOwl receipt before provider delivery completes. The pipeline can report that the delivery worker handoff started, but it does not wait for the provider module to finish.
Inside run_delivery_worker_cli, the worker parses the request, validates it, logs that provider writing has started, calls deliver_to_provider, prefixes the provider method with delivery_worker:, prints a delivery_worker_result, appends evidence, records a delivery stage, and removes the request file. If the provider accepts through a degraded or unverified path, the worker records that as provider_handoff_started_unverified; otherwise it records success or failure according to the provider result.
This is the practical difference between a receipt and proof. A send receipt tells the sender that AxiOwl accepted the request and began handoff. A provider result tells the logs what the provider edge could prove. A response over MCP from the target is stronger proof that the target received the message, acted on it, and had enough provider-owned identity to route a reply.
Why this matters in real use
Agent-to-agent messaging gets unreliable when systems blur identity, delivery, and reply status into one vague "sent" message. AxiOwl avoids that by keeping the path inspectable.
If a message fails before routing, the operator sees a target, body, sender, or discovery problem. If AxiOwl accepts the request but the provider later fails, the delivery worker and provider evidence show where the failure happened. If a provider can only prove that a bridge command was queued, the result can be degraded or unverified instead of being mislabeled as a complete delivery.
The result is not magic transport between arbitrary chats. It is a disciplined local coordinator: MCP and CLI enter the same pipeline, registry rows define routable agents, final visible bodies preserve the reply path, provider edges do provider-specific work, and delivery receipts say only what the system has actually proven.
Image prompt:
Create a polished graphic image related to: AxiOwl sending messages between agents.
Subject: a technical routing scene with two desktop terminal panels connected through a compact message switch, glowing packet trails, a small receipt ledger icon, and provider adapter modules shown as abstract hardware blocks; no readable labels.
Style: halfway between a clean symbolic icon and a realistic product/technical illustration; professional SaaS/technical marketing style; crisp edges; high detail; no text; no logos.
Background: solid #00ff00 chroma key green screen background covering the full canvas edge to edge.
Restrictions: no owl, no axolotl, no birds, no animals, no mascot, no text, no watermark.