AgentOffice is a Pi extension. The Pi runtime (pi-coding-agent) manages the conversation loop, tool execution, provider authentication, and streaming. AgentOffice adds the orchestration layer: semantic routing, cross-team transport (coms-net or NATS), a real-time portal, and the A2A protocol.
// AgentOffice as a Pi extension
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { createHub } from "./hub/index.js";
import { HybridScorer } from "./routing/hybrid.js";
import { startPortal } from "./portal/server.js";
export default function agentOffice(api: ExtensionAPI) {
const scorer = new HybridScorer(api.agents);
const hub = createHub(api, scorer);
// Tool: send a message to another agent
api.registerTool("send_message", async (args) => {
const target = await scorer.route(args.message);
return hub.dispatch(target.id, args.message);
});
// Command: team control panel
api.registerCommand("office", async (args) => {
return hub.status();
});
// Web portal on port 3000
startPortal({ hub, port: 3000 });
}
The routing pipeline combines sparse and dense retrieval, fusing the rankings with Reciprocal Rank Fusion. Zero configuration required — with optional cross-encoder reranking for maximum precision.
Per-field sparse retrieval — title, tags, and description weighted individually.
Dense embeddings for semantic similarity. Runs locally, with no external calls.
Reciprocal Rank Fusion — merges the two rankings with no manually tuned parameters.
ms-marco-MiniLM reranking. Enabled via ROUTING_RERANK=true.
For 1000+ agents: enable via QDRANT_URL. The system automatically migrates from the in-memory HybridScorer to the QdrantScorer, with no downtime and no practical limit on vectors.
// SSE stream — connect via GET /stream/:sessionId
event: routing:decision
data: {
"query": "need help deploying to k8s",
"winner": {
"id": "devops-agent",
"score": 0.94,
"method":"hybrid"
},
"candidates": [
{ "id": "devops-agent", "bm25": 0.89, "embed": 0.91, "rrf": 0.94 },
{ "id": "backend-agent", "bm25": 0.61, "embed": 0.58, "rrf": 0.62 }
],
"latencyMs": 4,
"reranked": false
}
The web portal runs on port 3000, the REST API on port 3777. Both operate in parallel — the portal consumes the same API you use in your own code.
| METHOD | ENDPOINT | DESCRIPTION |
|---|---|---|
| GET | /agents | Lists available agents with their capabilities |
| POST | /message | Sends a message, returns an SSE stream |
| GET | /agents/:id/status | Current agent state (idle / busy / error) |
| GET | /stream/:id | SSE stream for an active session |
| POST | /team | Creates an agent team with routing policies |
| GET | /teams | Lists teams and active members |
| DEL | /session/:id | Closes the session and releases resources |
| GET | /routing/log | History of routing decisions |
| POST | /session | Opens a new session with a specific agent |
| GET | /sessions | Lists open sessions by team |
| POST | /agents/:id/reload | Reloads the agent's context and skills |
| GET | /metrics | Routing, latency, and throughput metrics |
| PUT | /agents/:id/skills | Updates an agent's local skills at runtime |
| POST | /chain | Executes a multi-step chain across agents |
| GET | /schedules | Lists active schedules and upcoming runs |
| POST | /schedules | Creates a schedule with a cron expression |
| DEL | /schedules/:id | Removes a schedule |
| GET | /.well-known/agent.json | A2A protocol Agent Card |
Loading agent-office into a Pi creates a team. Any agent on that team can, in turn, load its own agent-office extension and become a team of its own — with its own agents, extensions, and configuration. No limit on depth, no limit on how many teams exist in the ecosystem.
Three mechanisms, three roles — not stacked layers, but alternatives for different needs:
| MECHANISM | ROLE | WHEN TO USE |
|---|---|---|
| coms-net hub | Centralized HTTP transport between teams/instances | Default — simple, no broker infrastructure |
| NATS JetStream | Decentralized pub/sub transport between teams/instances | Alternative to coms-net — larger topologies, at-least-once delivery |
| A2A Protocol | Interoperability with agents/frameworks outside the Pi ecosystem | When the other end doesn't speak AgentOffice's internal protocol |
PI_NATS_URL is set, NATS takes over discovery and dispatch between teams; without it, the coms-net HTTP hub covers the same function as a fallback. Semantic routing (messageRouter) decides who responds regardless of which transport is active underneath.
Each AgentOffice instance connects to the NATS cluster on startup. Peers are discovered immediately — no warm-up, no manual registration. Messages persist for 1 hour to guarantee delivery.
| SUBJECT | DIRECTION | FUNCTION |
|---|---|---|
| tasks.route | Pub | New task sent for routing |
| tasks.result | Sub | Final result of the agent's execution |
| tasks.stream | Sub | Incremental result streaming (chunks) |
| tasks.cancel | Pub | Cancellation of a task in progress |
| agents.ping | Pub/Sub | Peer discovery — emitted immediately on connect |
| agents.capabilities | Sub | Capabilities announced by remote peers |
Per-agent .creds credentials — identity isolation and granular permissions across the cluster.
PI_NATS_URL=nats://cluster:4222
NATS_CREDS_DEVOPS=./creds/devops.creds
NATS_CREDS_BUILDER=./creds/builder.creds
Single token for all agents — ideal for local development and testing environments.
PI_NATS_URL=nats://localhost:4222
PI_NATS_TOKEN=my-secret-token
Each agent is built from context layers with a defined priority. Lower-numbered layers take precedence — AGENTS.md defines who does what, SOUL.md persists the personality.
| PRIO | FILE | FUNCTION |
|---|---|---|
| 10 | AGENTS.md | Agent definitions and routing |
| 20 | SOUL.md | Persistent personality traits |
| 30 | IDENTITY.md | Agent role and specialization |
| 40 | USER.md | User preferences and history |
| 50 | TOOLS.md | Available tools and instructions |
| 60 | global skills | Capabilities shared across agents |
| 70 | local skills | Agent-exclusive capabilities |
# Skills available to agents
extensions/skills/
├── global/ ← available to everyone
│ ├── code-review.md
│ ├── git-workflow.md
│ └── security-check.md
└── agents/
├── devops/ ← devops agent only
│ ├── deploy.md
│ └── k8s-debug.md
└── builder/ ← builder agent only
├── scaffold.md
└── test-runner.md
Any A2A-compatible external agent can discover and interact with AgentOffice — and vice versa. The handshake starts via the public Agent Card at /.well-known/agent.json.
tasks/send
Sends a task and waits for the full response. Blocking — ideal for short tasks or when the result is needed immediately.
tasks/sendSubscribe
Sends and receives the result as an SSE stream. Ideal for long-running tasks — the agent emits chunks as it processes.
tasks/get
Checks the status of an async task by ID. Use alongside tasks/send in non-blocking mode.
tasks/cancel
Cancels a running task. The agent receives the cancellation signal and releases resources — consistent with the NATS tasks.cancel signal.
{
"name": "AgentOffice Hub",
"description": "Multi-agent orchestration platform built on Pi runtime",
"url": "https://ao.platformteam.dev",
"version": "1.0.0",
"capabilities": {
"streaming": true,
"pushNotifications": false,
"stateTransitionHistory": true
},
"skills": [
{
"id": "route-message",
"name": "Route Message",
"description": "Route a message to the best-matched agent via HybridScorer",
"tags": ["routing", "multi-agent", "nlp"]
}
]
}
Chains link steps across different agents with result interpolation. Schedules trigger chains or standalone messages at times defined by a cron expression.
Each step sends a message to an agent and captures the result. A step's result is available as {{stepN.result}} in subsequent steps.
name: code-review-pipeline
steps:
- id: fetch-diff
agent: devops
message: "Generate the diff for PR #{{input.pr_number}}"
- id: review-code
agent: reviewer
message: |
Review the diff below and flag issues:
{{fetch-diff.result}}
- id: write-comment
agent: github-bot
message: "Post on the PR: {{review-code.result}}"
Besides result, you can access status, agentId, and latencyMs for each step.
Schedules use standard cron expressions and support time zones. They can trigger full chains or standalone messages to specific agents.
name: daily-standup-report
cron: "0 9 * * 1-5" # Mon–Fri at 9am
timezone: America/Sao_Paulo
chain: standup-pipeline
input:
channel: general
format: markdown
---
name: security-scan
cron: "0 2 * * *" # every day at 2am
timezone: UTC
agent: security
message: "Run a full scan and send the report"
If the server is offline at the scheduled time, the run is skipped — there's no automatic catch-up. Use NATS for durability in distributed setups.