Quadro is a pattern language for enterprise multi-agent coordination, not a
generic agent framework. The code in this repository is a reference implementation
that proves the patterns are coherent and implementable. Read README.md before
continuing here.
This roadmap tracks the reference implementation only. The patterns themselves are
described in README.md and specified in QUADRO_SPEC.md.
| Milestone | Focus | Status |
|---|---|---|
| M1 | Board core + lifecycle validator | ✅ Complete |
| M2 | Chief event loop + decision application | ✅ Complete |
| M3 | Worker registration + dispatch | ✅ Complete |
| Track A | Ordering system example + three architectural gaps | ✅ Complete |
| M4.0 | Telemetry query intents | ✅ Complete |
| M4.1 | Ombudsman | ✅ Complete |
| M4.2 | Idempotency deduplication | ✅ Complete |
| M4.3 | Revision path integration test | ✅ Complete |
| M5 | Board UI + observability | ✅ Complete |
board/records.py—TaskRecord,AgentRecord,EventRecorddataclasses withTaskStatusandAgentStatusasStrEnumboard/state_machine.py— lifecycle profile validator (review_required,fast) with_expand_with_globalforFAILED/ON_HOLDtransitionsboard/board.py—QuadroBoardwith all mutating and read intents,_append_eventguarded by frozen taxonomyboard/backends/base.py— abstractBoardBackendboard/backends/sqlite.py— in-memory and file-backed SQLite backend with inline row parsers (no N+1)
- Valid transition → persists state → emits exactly one immutable event
- Invalid transition → persists nothing → emits no event
- Every event has monotonically increasing sequence id, timestamp, transition metadata
task_heartbeatstored in event log but classified asOPERATIONAL_EVENT_TYPES, notCHIEF_WAKEUP_EVENT_TYPES
agents/chief.py—ChiefAgentwith serializednudge()loop, heartbeat filtering, AgentCard-based worker discovery from board registry, andpolicycallback seamagents/hydration.py—hydrate_chief_contextandhydrate_worker_contextwith deterministicsnapshot_hasha2a/events.py—EventSubscriberwith cursor-based polling
- Chief wakes only on
CHIEF_WAKEUP_EVENT_TYPES(heartbeats filtered before policy) - Concurrent calls to
nudge()serialize viathreading.Lock;max_concurrent_loops == 1 - Worker
a2a_urlread from board'sAgentRecordat dispatch time PENDING_REVIEW → IN_PROGRESSassigns reviewer asassigned_tobefore dispatch
agents/worker.py—WorkerAgentwith AgentCard registration, two-argumentexecute_fn(ctx, board_fn)signature, heartbeat posting, reviewer modea2a/dispatch.py—LocalA2ANetworkin-process transporta2a/contracts.py— frozen intent whitelist, event taxonomy sets, typed envelopes
- Workers register via
board.register_agentwith required AgentCard fields - Workers read task context from board at invocation time (hydration)
execute_fnreceives(context, board_fn)— operational workers call board intents directly viaboard_fn; simple workers ignore the second argument- Result posting transitions task to the correct next state by profile
Three architectural gaps resolved before the ordering example was built:
Gap 1 — Custom lifecycle profiles
build_custom_profile()instate_machine.py— string-based transition setsvalidate_transitionaccepts optionalcustom_profilesdictQuadroBoard.__init__acceptscustom_profilesparameter- SQLite backend handles status values not in
TaskStatusenum
Gap 2 — Board data store
board.put_data/board.get_dataintents — arbitrary key-value storagedata_entriestable in SQLite backend- Data entries emit no events
board.get_full_stateincludes data under"data"key
Gap 3 — Operational worker context
execute_fn(ctx, board_fn)signature — workers can call board intents during execution- Backward-compatible: simple workers accept
(ctx, _)and ignore the second argument - Worker checks if task was already transitioned by
execute_fnbefore callingworker.post_result
Ordering system example (examples/ordering_system.py)
- Single file, ~260 lines
- Custom order lifecycle profile (
placed → accepted → awaiting_stock → stock_ready → delivering → delivered) - Warehouse inventory as board data (not tasks)
- Stock handler uses
board_fnto read inventory, route conditionally, replenish from reserve
tests/unit/test_state_machine.py—test_custom_profile_validates_correctlytests/unit/test_board_data_store.py— 5 teststests/integration/test_worker_board_access.py— 2 teststests/integration/test_revision_cycle.py— full revision cycle with reviewer rejection and re-assignment
The audit trail is already in the events table from M1. This milestone adds two
read intents to query it by task or by agent. No schema changes. No new events.
Foundation for the BoardUI in M5 and for execution reports.
board/backends/base.py — two new abstract methods:
@abstractmethod
def list_events_for_task(self, task_id: str) -> list[EventRecord]: ...
@abstractmethod
def list_events_for_agent(self, agent_id: str) -> list[EventRecord]: ...board/backends/sqlite.py — implement both using SELECT ... WHERE ... ORDER BY sequence_id ASC against the existing events table. Parse rows exactly as
list_events_since does.
a2a/contracts.py — add to ALLOWED_INTENTS:
"board.get_task_history",
"board.get_agent_activity",board/board.py — routing and two private methods:
def _get_task_history(self, payload: dict) -> dict:
# Returns {"task_id": str, "events": list[dict]}
def _get_agent_activity(self, payload: dict) -> dict:
# Returns {"agent_id": str, "events": list[dict]}test_get_task_history_returns_only_that_tasks_events— two tasks, verify filteringtest_get_task_history_includes_heartbeats— heartbeat events appear in historytest_get_agent_activity_returns_only_that_agents_events— two agents, verify filteringtest_get_task_history_empty_for_unknown_task— unknown task_id returns empty list, not an error
- Both intents return events in
sequence_idascending order - No cross-task or cross-agent contamination in results
- Unknown IDs return
{"events": []}, not an error response - No new tables, no schema changes, no new events emitted
- All 28 existing tests continue to pass
ombudsman.py—Ombudsmanwith configurableheartbeat_timeout_seconds. ScansIN_PROGRESStasks and transitions stale ones toSTALEvia normal board update path.working_statusesparameter — extends Ombudsman to scan custom-profile statuses (e.g. "writing", "researching") and transition stale tasks toFAILED.tests/unit/test_ombudsman_custom_statuses.py— 2 tests covering both paths.tests/integration/test_ombudsman.py— 4 integration tests.
board/idempotency.py—IdempotencyStorewith SQLite-backedcheck()andstore(). Uses_stable_hash(payload)as fingerprint.ConflictErrorinerrors.py— raised on key collision with different payload.QuadroBoardaccepts optionalidempotency_storeparameter. Mutating intents withidempotency_keycheck the store before executing and cache the result after.idempotency_keystable created inSqliteBoardBackend.init().tests/unit/test_idempotency.py— 4 tests covering cached return, conflict detection, no-key passthrough, and backward compatibility without a store.
tests/integration/test_revision_path.py walks a review_required task through
the full revision cycle verifying the assigned_to audit trail at each phase:
UNASSIGNED → IN_PROGRESS (writer)
→ PENDING_REVIEW → IN_PROGRESS (reviewer rejects → REVISION_NEEDED)
→ IN_PROGRESS (writer again) → PENDING_REVIEW
→ IN_PROGRESS (reviewer approves) → APPROVED → COMPLETE
ui.py— zero-dependency board UI server (stdlib only, no npm, no React). Serves a live Kanban view athttp://localhost:8080.- Two usage modes: programmatic (
serve_board(board_client)) and CLI (python -m quadro.ui path/to/board.db). - Live SSE event feed — board updates without polling the page.
- Chief telemetry panel — shows status (thinking/acting/sleeping), cycle count, last cycle duration, and a sparkline of recent durations.
- Per-task drawer — click any card to see the full event timeline and output.
- Agent status panel — IDLE/BUSY state with current task ID.
- Board data section — displays non-internal key-value entries.
- Dark/light theme toggle.
- Column order resolved from: explicit arg >
_col_orderboard data key > event history > current task statuses.
- Board is the single source of truth
- A2A-only boundaries — no direct method calls between board/chief/workers
- Single transition, single event — data store operations emit no events
- Deterministic hydration — same snapshot → same hash
- Chief serialization — one loop at a time
- Idempotent writes —
idempotency_keyaccepted on all mutating task intents
Do not introduce:
- Generic trigger registry or event routing table
- Wildcard event subscriptions
- Non-A2A shortcut paths between components
- Framework-level orchestration replacement
Now open for contribution (see TODO.md):
- PostgreSQL, MySQL, Redis, and DynamoDB backends
- Idempotency deduplication (M4.2)
- All integration tests use
LocalA2ANetwork— no HTTP, no external processes - Tests interact with the board only through
network.request()+ typed envelopes - No test calls production code's private methods (prefixed
_) - New unit tests in
tests/unit/, new integration tests intests/integration/
- All milestone acceptance criteria pass (M1 through M4.3)
- All six architecture invariants verifiable through tests
- A2A-only transport policy has no bypasses
- Event taxonomy and lifecycle profiles unchanged from frozen spec
README.md,QUADRO_SPEC.md, and this roadmap consistent with each other- Both example scripts run end-to-end without modification