diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 35a7a90..1d24cb8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,9 +46,10 @@ jobs:
- name: Verify built bundles load
run: pnpm verify:dist
- # The committed agent manifest must match what the generator produces
- # from discovery + contracts. Fails loud if a contributor changed an
- # agent or contract without regenerating `agents.manifest.json`.
+ # The committed agent manifest, navigation graph, and agent-map HTML
+ # must match what the generator produces from discovery + contracts.
+ # Fails loud if a contributor changed an agent or contract without
+ # regenerating via `pnpm gen:manifest`.
- name: Verify agent manifest is up to date
run: pnpm gen:manifest --check
diff --git a/agents.graph.json b/agents.graph.json
new file mode 100644
index 0000000..56d5270
--- /dev/null
+++ b/agents.graph.json
@@ -0,0 +1,926 @@
+{
+ "generated_by": "scripts/generate-manifest.ts",
+ "total_nodes": 77,
+ "total_edges": 27,
+ "nodes": [
+ {
+ "id": "0.1",
+ "name": "Airport/City Code Resolver",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/reference/src/airport-code-resolver/index.ts"
+ },
+ {
+ "id": "0.2",
+ "name": "Airline Code & Alliance Mapper",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/reference/src/airline-code-mapper/index.ts"
+ },
+ {
+ "id": "0.3",
+ "name": "Fare Basis Code Decoder",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/reference/src/fare-basis-decoder/index.ts"
+ },
+ {
+ "id": "0.4",
+ "name": "Class of Service Mapper",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reference/src/class-of-service-mapper/index.ts"
+ },
+ {
+ "id": "0.5",
+ "name": "Equipment Type Resolver",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reference/src/equipment-type-resolver/index.ts"
+ },
+ {
+ "id": "0.6",
+ "name": "Currency & Tax Code Resolver",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reference/src/currency-tax-resolver/index.ts"
+ },
+ {
+ "id": "0.7",
+ "name": "Country Regulatory Resolver",
+ "stage": "reference",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reference/src/country-regulatory-resolver/index.ts"
+ },
+ {
+ "id": "1.1",
+ "name": "Availability Search",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/search/src/availability-search/index.ts"
+ },
+ {
+ "id": "1.2",
+ "name": "Schedule Lookup",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/search/src/schedule-lookup/index.ts"
+ },
+ {
+ "id": "1.3",
+ "name": "Connection Builder",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/search/src/connection-builder/index.ts"
+ },
+ {
+ "id": "1.4",
+ "name": "Fare Shopping",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/search/src/fare-shopping/index.ts"
+ },
+ {
+ "id": "1.5",
+ "name": "Ancillary Shopping",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/search/src/ancillary-shopping/index.ts"
+ },
+ {
+ "id": "1.6",
+ "name": "Multi-Source Aggregator",
+ "stage": "search",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/search/src/multi-source-aggregator/index.ts"
+ },
+ {
+ "id": "1.7",
+ "name": "Hotel & Car Search",
+ "stage": "search",
+ "version": "0.2.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/search/src/hotel-car-search/index.ts"
+ },
+ {
+ "id": "1.8",
+ "name": "AI Travel Advisor",
+ "stage": "search",
+ "version": "0.2.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/search/src/ai-travel-advisor/index.ts"
+ },
+ {
+ "id": "1.9",
+ "name": "Offer Evaluator",
+ "stage": "core",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/core/src/agents/shopping/offer-evaluator/index.ts"
+ },
+ {
+ "id": "2.1",
+ "name": "Fare Rule Agent",
+ "stage": "pricing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/pricing/src/fare-rule-agent/index.ts"
+ },
+ {
+ "id": "2.2",
+ "name": "Fare Construction",
+ "stage": "pricing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/pricing/src/fare-construction/index.ts"
+ },
+ {
+ "id": "2.3",
+ "name": "Tax Calculation",
+ "stage": "pricing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/pricing/src/tax-calculation/index.ts"
+ },
+ {
+ "id": "2.4",
+ "name": "Offer Builder",
+ "stage": "pricing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/pricing/src/offer-builder/index.ts"
+ },
+ {
+ "id": "2.5",
+ "name": "Corporate Policy Validation",
+ "stage": "pricing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/pricing/src/corporate-policy-validation/index.ts"
+ },
+ {
+ "id": "2.6",
+ "name": "Dynamic Pricing",
+ "stage": "pricing",
+ "version": "0.0.0",
+ "contract_status": "stub",
+ "has_contract": false,
+ "source_path": "packages/agents/pricing/src/dynamic-pricing/index.ts"
+ },
+ {
+ "id": "2.7",
+ "name": "Revenue Management",
+ "stage": "pricing",
+ "version": "0.0.0",
+ "contract_status": "stub",
+ "has_contract": false,
+ "source_path": "packages/agents/pricing/src/revenue-management/index.ts"
+ },
+ {
+ "id": "3.1",
+ "name": "GDS/NDC Router",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/booking/src/gds-ndc-router/index.ts"
+ },
+ {
+ "id": "3.2",
+ "name": "PNR Builder",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/booking/src/pnr-builder/index.ts"
+ },
+ {
+ "id": "3.3",
+ "name": "PNR Validation",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/booking/src/pnr-validation/index.ts"
+ },
+ {
+ "id": "3.4",
+ "name": "Queue Management",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/booking/src/queue-management/index.ts"
+ },
+ {
+ "id": "3.5",
+ "name": "API Abstraction",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/booking/src/api-abstraction/index.ts"
+ },
+ {
+ "id": "3.6",
+ "name": "Order Management",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/booking/src/order-management/index.ts"
+ },
+ {
+ "id": "3.7",
+ "name": "Payment Processing",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/booking/src/payment-processing/index.ts"
+ },
+ {
+ "id": "3.8",
+ "name": "PNR Retrieval",
+ "stage": "booking",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/booking/src/pnr-retrieval/index.ts"
+ },
+ {
+ "id": "4.1",
+ "name": "Ticket Issuance",
+ "stage": "ticketing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/ticketing/src/ticket-issuance/index.ts"
+ },
+ {
+ "id": "4.2",
+ "name": "EMD Management",
+ "stage": "ticketing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/ticketing/src/emd-management/index.ts"
+ },
+ {
+ "id": "4.3",
+ "name": "Void Agent",
+ "stage": "ticketing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/ticketing/src/void-agent/index.ts"
+ },
+ {
+ "id": "4.4",
+ "name": "Itinerary Delivery",
+ "stage": "ticketing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/ticketing/src/itinerary-delivery/index.ts"
+ },
+ {
+ "id": "4.5",
+ "name": "Document Verification",
+ "stage": "ticketing",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/ticketing/src/document-verification/index.ts"
+ },
+ {
+ "id": "5.1",
+ "name": "Change Management",
+ "stage": "exchange",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/exchange/src/change-management/index.ts"
+ },
+ {
+ "id": "5.2",
+ "name": "Exchange/Reissue",
+ "stage": "exchange",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/exchange/src/exchange-reissue/index.ts"
+ },
+ {
+ "id": "5.3",
+ "name": "Involuntary Rebook",
+ "stage": "exchange",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/exchange/src/involuntary-rebook/index.ts"
+ },
+ {
+ "id": "5.4",
+ "name": "Disruption Response",
+ "stage": "exchange",
+ "version": "0.0.0",
+ "contract_status": "stub",
+ "has_contract": false,
+ "source_path": "packages/agents/exchange/src/disruption-response/index.ts"
+ },
+ {
+ "id": "5.5",
+ "name": "Self-Service Rebooking",
+ "stage": "exchange",
+ "version": "0.2.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/exchange/src/self-service-rebooking/index.ts"
+ },
+ {
+ "id": "5.6",
+ "name": "Waitlist Management",
+ "stage": "exchange",
+ "version": "0.2.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents/exchange/src/waitlist-management/index.ts"
+ },
+ {
+ "id": "6.1",
+ "name": "Refund Processing",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/refund-processing/index.ts"
+ },
+ {
+ "id": "6.2",
+ "name": "ADM Prevention",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/adm-prevention/index.ts"
+ },
+ {
+ "id": "6.3",
+ "name": "ADM/ACM Processing",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/adm-acm-processing/index.ts"
+ },
+ {
+ "id": "6.4",
+ "name": "Customer Communication",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/customer-communication/index.ts"
+ },
+ {
+ "id": "6.5",
+ "name": "Feedback & Complaint",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/feedback-complaint/index.ts"
+ },
+ {
+ "id": "6.6",
+ "name": "Loyalty Mileage",
+ "stage": "settlement",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/settlement/src/loyalty-mileage/index.ts"
+ },
+ {
+ "id": "7.1",
+ "name": "BSP Reconciliation",
+ "stage": "reconciliation",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/bsp-reconciliation/index.ts"
+ },
+ {
+ "id": "7.2",
+ "name": "ARC Reconciliation",
+ "stage": "reconciliation",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/arc-reconciliation/index.ts"
+ },
+ {
+ "id": "7.3",
+ "name": "Commission Management",
+ "stage": "reconciliation",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/commission-management/index.ts"
+ },
+ {
+ "id": "7.4",
+ "name": "Interline Settlement",
+ "stage": "reconciliation",
+ "version": "0.0.0",
+ "contract_status": "stub",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/interline-settlement/index.ts"
+ },
+ {
+ "id": "7.5",
+ "name": "Financial Reporting",
+ "stage": "reconciliation",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/financial-reporting/index.ts"
+ },
+ {
+ "id": "7.6",
+ "name": "Revenue Accounting",
+ "stage": "reconciliation",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/reconciliation/src/revenue-accounting/index.ts"
+ },
+ {
+ "id": "8.1",
+ "name": "Traveler Profile",
+ "stage": "tmc",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-tmc/src/traveler-profile/index.ts"
+ },
+ {
+ "id": "8.2",
+ "name": "Corporate Account",
+ "stage": "tmc",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-tmc/src/corporate-account/index.ts"
+ },
+ {
+ "id": "8.3",
+ "name": "Mid-Office Automation",
+ "stage": "tmc",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-tmc/src/mid-office/index.ts"
+ },
+ {
+ "id": "8.4",
+ "name": "Reporting & Analytics",
+ "stage": "tmc",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-tmc/src/reporting/index.ts"
+ },
+ {
+ "id": "8.5",
+ "name": "Duty of Care",
+ "stage": "tmc",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-tmc/src/duty-of-care/index.ts"
+ },
+ {
+ "id": "9.1",
+ "name": "Orchestrator",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-platform/src/orchestrator/index.ts"
+ },
+ {
+ "id": "9.2",
+ "name": "Knowledge Retrieval",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-platform/src/knowledge/index.ts"
+ },
+ {
+ "id": "9.3",
+ "name": "Monitoring & Alerting",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-platform/src/monitoring/index.ts"
+ },
+ {
+ "id": "9.4",
+ "name": "Audit & Compliance",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-platform/src/audit/index.ts"
+ },
+ {
+ "id": "9.5",
+ "name": "Plugin Manager",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents-platform/src/plugin-manager/index.ts"
+ },
+ {
+ "id": "9.6",
+ "name": "Performance Audit",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents-platform/src/performance-audit/index.ts"
+ },
+ {
+ "id": "9.7",
+ "name": "Routing Audit",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents-platform/src/routing-audit/index.ts"
+ },
+ {
+ "id": "9.8",
+ "name": "Recommendation",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents-platform/src/recommendation/index.ts"
+ },
+ {
+ "id": "9.9",
+ "name": "Alert",
+ "stage": "platform",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": true,
+ "source_path": "packages/agents-platform/src/alert/index.ts"
+ },
+ {
+ "id": "20.1",
+ "name": "Hotel Search Aggregator",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/hotel-search/index.ts"
+ },
+ {
+ "id": "20.2",
+ "name": "Property Deduplication",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/property-dedup/index.ts"
+ },
+ {
+ "id": "20.3",
+ "name": "Hotel Content Normalization",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/content-normalization/index.ts"
+ },
+ {
+ "id": "20.4",
+ "name": "Hotel Rate Comparison",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/rate-comparison/index.ts"
+ },
+ {
+ "id": "20.5",
+ "name": "Hotel Booking",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/hotel-booking/index.ts"
+ },
+ {
+ "id": "20.6",
+ "name": "Hotel Modification & Cancellation",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/hotel-modification/index.ts"
+ },
+ {
+ "id": "20.7",
+ "name": "Confirmation Verification",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/confirmation-verification/index.ts"
+ },
+ {
+ "id": "20.8",
+ "name": "Activity Search",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/activity-search/index.ts"
+ },
+ {
+ "id": "20.9",
+ "name": "Transfer Search",
+ "stage": "lodging",
+ "version": "0.1.0",
+ "contract_status": "active",
+ "has_contract": false,
+ "source_path": "packages/agents/lodging/src/transfer-search/index.ts"
+ }
+ ],
+ "edges": [
+ {
+ "source": "1.1",
+ "target": "1.4",
+ "kind": "workflow",
+ "label": "search_to_price"
+ },
+ {
+ "source": "1.4",
+ "target": "2.1",
+ "kind": "workflow",
+ "label": "search_to_price"
+ },
+ {
+ "source": "2.1",
+ "target": "2.2",
+ "kind": "workflow",
+ "label": "search_to_price"
+ },
+ {
+ "source": "2.2",
+ "target": "2.3",
+ "kind": "workflow",
+ "label": "search_to_price"
+ },
+ {
+ "source": "3.3",
+ "target": "3.1",
+ "kind": "workflow",
+ "label": "book_to_ticket"
+ },
+ {
+ "source": "3.1",
+ "target": "3.2",
+ "kind": "workflow",
+ "label": "book_to_ticket"
+ },
+ {
+ "source": "3.2",
+ "target": "4.1",
+ "kind": "workflow",
+ "label": "book_to_ticket"
+ },
+ {
+ "source": "1.1",
+ "target": "1.4",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "1.4",
+ "target": "2.1",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "2.1",
+ "target": "2.2",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "2.2",
+ "target": "2.3",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "2.3",
+ "target": "3.3",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "3.3",
+ "target": "3.1",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "3.1",
+ "target": "3.2",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "3.2",
+ "target": "4.1",
+ "kind": "workflow",
+ "label": "full_booking"
+ },
+ {
+ "source": "5.1",
+ "target": "5.2",
+ "kind": "workflow",
+ "label": "exchange_flow"
+ },
+ {
+ "source": "6.1",
+ "target": "6.2",
+ "kind": "workflow",
+ "label": "refund_flow"
+ },
+ {
+ "source": "1.1",
+ "target": "0.1",
+ "kind": "package",
+ "label": "@otaip/agents-search → @otaip/agents-reference"
+ },
+ {
+ "source": "2.1",
+ "target": "0.1",
+ "kind": "package",
+ "label": "@otaip/agents-pricing → @otaip/agents-reference"
+ },
+ {
+ "source": "3.1",
+ "target": "0.1",
+ "kind": "package",
+ "label": "@otaip/agents-booking → @otaip/agents-reference"
+ },
+ {
+ "source": "3.1",
+ "target": "1.1",
+ "kind": "package",
+ "label": "@otaip/agents-booking → @otaip/agents-search"
+ },
+ {
+ "source": "3.1",
+ "target": "2.1",
+ "kind": "package",
+ "label": "@otaip/agents-booking → @otaip/agents-pricing"
+ },
+ {
+ "source": "4.1",
+ "target": "3.1",
+ "kind": "package",
+ "label": "@otaip/agents-ticketing → @otaip/agents-booking"
+ },
+ {
+ "source": "5.1",
+ "target": "1.1",
+ "kind": "package",
+ "label": "@otaip/agents-exchange → @otaip/agents-search"
+ },
+ {
+ "source": "5.1",
+ "target": "4.1",
+ "kind": "package",
+ "label": "@otaip/agents-exchange → @otaip/agents-ticketing"
+ },
+ {
+ "source": "6.1",
+ "target": "4.1",
+ "kind": "package",
+ "label": "@otaip/agents-settlement → @otaip/agents-ticketing"
+ },
+ {
+ "source": "7.1",
+ "target": "0.1",
+ "kind": "package",
+ "label": "@otaip/agents-reconciliation → @otaip/agents-reference"
+ }
+ ],
+ "package_deps": [
+ {
+ "from_package": "@otaip/agents-search",
+ "to_package": "@otaip/agents-reference",
+ "from_stage": "search",
+ "to_stage": "reference"
+ },
+ {
+ "from_package": "@otaip/agents-pricing",
+ "to_package": "@otaip/agents-reference",
+ "from_stage": "pricing",
+ "to_stage": "reference"
+ },
+ {
+ "from_package": "@otaip/agents-booking",
+ "to_package": "@otaip/agents-reference",
+ "from_stage": "booking",
+ "to_stage": "reference"
+ },
+ {
+ "from_package": "@otaip/agents-booking",
+ "to_package": "@otaip/agents-search",
+ "from_stage": "booking",
+ "to_stage": "search"
+ },
+ {
+ "from_package": "@otaip/agents-booking",
+ "to_package": "@otaip/agents-pricing",
+ "from_stage": "booking",
+ "to_stage": "pricing"
+ },
+ {
+ "from_package": "@otaip/agents-ticketing",
+ "to_package": "@otaip/agents-booking",
+ "from_stage": "ticketing",
+ "to_stage": "booking"
+ },
+ {
+ "from_package": "@otaip/agents-exchange",
+ "to_package": "@otaip/agents-search",
+ "from_stage": "exchange",
+ "to_stage": "search"
+ },
+ {
+ "from_package": "@otaip/agents-exchange",
+ "to_package": "@otaip/agents-ticketing",
+ "from_stage": "exchange",
+ "to_stage": "ticketing"
+ },
+ {
+ "from_package": "@otaip/agents-settlement",
+ "to_package": "@otaip/agents-ticketing",
+ "from_stage": "settlement",
+ "to_stage": "ticketing"
+ },
+ {
+ "from_package": "@otaip/agents-reconciliation",
+ "to_package": "@otaip/agents-reference",
+ "from_stage": "reconciliation",
+ "to_stage": "reference"
+ }
+ ]
+}
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index b0a7a0c..0958bed 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -262,6 +262,29 @@ const agent = new HotelSearchAgent({
---
+## Agent navigation map
+
+OTAIP ships a first-party **agent map** for browsing the ~77 discovered agents by stage, with edges from Orchestrator workflows and agent-package workspace deps.
+
+| Artifact | Role |
+|---|---|
+| `agents.manifest.json` | Authoritative agent roster |
+| `agents.graph.json` | Nodes + workflow/package edges for navigation |
+| `docs/agent-map.html` | Standalone browsable map (no build step) |
+| Platform UI → Map | Same graph via `GET /api/platform/agent-graph` |
+
+Regenerate all three (plus the HTML) with:
+
+```bash
+pnpm gen:manifest
+```
+
+CI runs `pnpm gen:manifest --check` so stale artifacts fail the build.
+
+This is **not** a travel-domain knowledge graph and is unrelated to Graphify / RAG. Do not treat map edges as fare, tax, or lodging domain logic — they only describe how agents are wired in this repo.
+
+---
+
## Testing
```bash
@@ -299,6 +322,7 @@ pnpm lint
| `pnpm test` | Run all tests |
| `pnpm typecheck` | TypeScript strict check |
| `pnpm lint` | ESLint + Prettier |
+| `pnpm gen:manifest` | Regenerate agent manifest, graph, and `docs/agent-map.html` |
| `pnpm run data:download` | Download reference datasets |
Requirements: Node 20+, pnpm 9+.
diff --git a/docs/agent-map.html b/docs/agent-map.html
new file mode 100644
index 0000000..fdb8601
--- /dev/null
+++ b/docs/agent-map.html
@@ -0,0 +1,755 @@
+
+
+
+
+
+ OTAIP Agent Map
+
+
+
+
+
+
+
+
+
+
+ reference 7
+
+
+ 0.1
+ Airport/City Code Resolver
+
+
+ 0.2
+ Airline Code & Alliance Mapper
+
+
+ 0.3
+ Fare Basis Code Decoder
+
+
+ 0.4
+ Class of Service Mapper
+
+
+ 0.5
+ Equipment Type Resolver
+
+
+ 0.6
+ Currency & Tax Code Resolver
+
+
+ 0.7
+ Country Regulatory Resolver
+
+
+
+
+ search 8
+
+
+ 1.1
+ Availability Search
+
+
+ 1.2
+ Schedule Lookup
+
+
+ 1.3
+ Connection Builder
+
+
+ 1.4
+ Fare Shopping
+
+
+ 1.5
+ Ancillary Shopping
+
+
+ 1.6
+ Multi-Source Aggregator
+
+
+ 1.7
+ Hotel & Car Search
+
+
+ 1.8
+ AI Travel Advisor
+
+
+
+
+ pricing 7
+
+
+ 2.1
+ Fare Rule Agent
+
+
+ 2.2
+ Fare Construction
+
+
+ 2.3
+ Tax Calculation
+
+
+ 2.4
+ Offer Builder
+
+
+ 2.5
+ Corporate Policy Validation
+
+
+ 2.6
+ Dynamic Pricing
+
+
+ 2.7
+ Revenue Management
+
+
+
+
+ booking 8
+
+
+ 3.1
+ GDS/NDC Router
+
+
+ 3.2
+ PNR Builder
+
+
+ 3.3
+ PNR Validation
+
+
+ 3.4
+ Queue Management
+
+
+ 3.5
+ API Abstraction
+
+
+ 3.6
+ Order Management
+
+
+ 3.7
+ Payment Processing
+
+
+ 3.8
+ PNR Retrieval
+
+
+
+
+ ticketing 5
+
+
+ 4.1
+ Ticket Issuance
+
+
+ 4.2
+ EMD Management
+
+
+ 4.3
+ Void Agent
+
+
+ 4.4
+ Itinerary Delivery
+
+
+ 4.5
+ Document Verification
+
+
+
+
+ exchange 6
+
+
+ 5.1
+ Change Management
+
+
+ 5.2
+ Exchange/Reissue
+
+
+ 5.3
+ Involuntary Rebook
+
+
+ 5.4
+ Disruption Response
+
+
+ 5.5
+ Self-Service Rebooking
+
+
+ 5.6
+ Waitlist Management
+
+
+
+
+ settlement 6
+
+
+ 6.1
+ Refund Processing
+
+
+ 6.2
+ ADM Prevention
+
+
+ 6.3
+ ADM/ACM Processing
+
+
+ 6.4
+ Customer Communication
+
+
+ 6.5
+ Feedback & Complaint
+
+
+ 6.6
+ Loyalty Mileage
+
+
+
+
+ reconciliation 6
+
+
+ 7.1
+ BSP Reconciliation
+
+
+ 7.2
+ ARC Reconciliation
+
+
+ 7.3
+ Commission Management
+
+
+ 7.4
+ Interline Settlement
+
+
+ 7.5
+ Financial Reporting
+
+
+ 7.6
+ Revenue Accounting
+
+
+
+
+ tmc 5
+
+
+ 8.1
+ Traveler Profile
+
+
+ 8.2
+ Corporate Account
+
+
+ 8.3
+ Mid-Office Automation
+
+
+ 8.4
+ Reporting & Analytics
+
+
+ 8.5
+ Duty of Care
+
+
+
+
+ platform 9
+
+
+ 9.1
+ Orchestrator
+
+
+ 9.2
+ Knowledge Retrieval
+
+
+ 9.3
+ Monitoring & Alerting
+
+
+ 9.4
+ Audit & Compliance
+
+
+ 9.5
+ Plugin Manager
+
+
+ 9.6
+ Performance Audit
+
+
+ 9.7
+ Routing Audit
+
+
+ 9.8
+ Recommendation
+
+
+ 9.9
+ Alert
+
+
+
+
+ core 1
+
+
+ 1.9
+ Offer Evaluator
+
+
+
+
+ lodging 9
+
+
+ 20.1
+ Hotel Search Aggregator
+
+
+ 20.2
+ Property Deduplication
+
+
+ 20.3
+ Hotel Content Normalization
+
+
+ 20.4
+ Hotel Rate Comparison
+
+
+ 20.5
+ Hotel Booking
+
+
+ 20.6
+ Hotel Modification & Cancellation
+
+
+ 20.7
+ Confirmation Verification
+
+
+ 20.8
+ Activity Search
+
+
+ 20.9
+ Transfer Search
+
+
+
+
+
+
+
+
+ Generated by pnpm gen:manifest from agents.graph.json
+ (77 agents, 27 edges). Do not hand-edit.
+
+
+
+
+
diff --git a/docs/agents.md b/docs/agents.md
index fe5bf6c..afeac9f 100644
--- a/docs/agents.md
+++ b/docs/agents.md
@@ -2,6 +2,8 @@
> 75 agents across 12 stages. 14 have pipeline contracts (marked with checkmark).
> Counts are computed from source by `pnpm run count:agents` — do not hand-edit the totals.
+>
+> **Visual map:** open [agent-map.html](./agent-map.html) (generated by `pnpm gen:manifest`) or use **Map** on the Platform UI dashboard. That map is agent navigation only — not a travel-domain knowledge graph.
## Stage 0 -- Reference (7 agents)
diff --git a/examples/ota/src/__tests__/platform.test.ts b/examples/ota/src/__tests__/platform.test.ts
index f575a62..7c00411 100644
--- a/examples/ota/src/__tests__/platform.test.ts
+++ b/examples/ota/src/__tests__/platform.test.ts
@@ -57,6 +57,30 @@ describe('GET /api/platform/agents', () => {
});
});
+describe('GET /api/platform/agent-graph', () => {
+ it('returns the committed agent navigation graph', async () => {
+ const res = await app.inject({ method: 'GET', url: '/api/platform/agent-graph' });
+ expect(res.statusCode).toBe(200);
+ const body = res.json() as {
+ nodes: Array<{ id: string; stage: string; has_contract: boolean }>;
+ edges: Array<{ source: string; target: string; kind: string; label: string }>;
+ package_deps: Array<{ from_stage: string; to_stage: string }>;
+ total_nodes: number;
+ total_edges: number;
+ };
+ expect(body.nodes.length).toBe(body.total_nodes);
+ expect(body.edges.length).toBe(body.total_edges);
+ expect(body.nodes.length).toBeGreaterThan(0);
+ expect(body.edges.some((e) => e.kind === 'workflow')).toBe(true);
+ expect(body.package_deps.length).toBeGreaterThan(0);
+ const ids = new Set(body.nodes.map((n) => n.id));
+ for (const e of body.edges) {
+ expect(ids.has(e.source)).toBe(true);
+ expect(ids.has(e.target)).toBe(true);
+ }
+ });
+});
+
describe('GET /api/platform/adapters', () => {
it('lists every documented adapter with env-derived configured flags', async () => {
const res = await app.inject({ method: 'GET', url: '/api/platform/adapters' });
diff --git a/examples/ota/src/routes/platform.ts b/examples/ota/src/routes/platform.ts
index a1afbfb..cdfc083 100644
--- a/examples/ota/src/routes/platform.ts
+++ b/examples/ota/src/routes/platform.ts
@@ -18,6 +18,17 @@ export function registerPlatformRoutes(
return reply.send(platform.agents());
});
+ app.get('/api/platform/agent-graph', { config: { rateLimit: HIGH_RATE_LIMIT } }, async (_req, reply) => {
+ const graph = platform.agentGraph();
+ if (!graph) {
+ return reply.status(404).send({
+ error: 'agent_graph_missing',
+ hint: 'Run `pnpm gen:manifest` to generate agents.graph.json.',
+ });
+ }
+ return reply.send(graph);
+ });
+
app.get('/api/platform/adapters', { config: { rateLimit: HIGH_RATE_LIMIT } }, async (_req, reply) => {
return reply.send({ adapters: platform.adapters() });
});
diff --git a/examples/ota/src/services/platform-service.ts b/examples/ota/src/services/platform-service.ts
index 6a145a9..f08c1fc 100644
--- a/examples/ota/src/services/platform-service.ts
+++ b/examples/ota/src/services/platform-service.ts
@@ -6,7 +6,7 @@
* mutated through this service; all data is in-memory or filesystem-only.
*/
-import { readFileSync } from 'node:fs';
+import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { discoverAgents, type DiscoveredAgent } from '@otaip/core';
@@ -22,6 +22,40 @@ export interface AgentRollup {
totals: { total: number; active: number; stub: number };
}
+export interface AgentGraphNode {
+ id: string;
+ name: string;
+ stage: string;
+ version: string;
+ contract_status: 'active' | 'stub';
+ has_contract: boolean;
+ source_path: string;
+}
+
+export interface AgentGraphEdge {
+ source: string;
+ target: string;
+ kind: 'workflow' | 'package';
+ label: string;
+}
+
+export interface PackageDep {
+ from_package: string;
+ to_package: string;
+ from_stage: string;
+ to_stage: string;
+}
+
+/** Navigation graph committed as `agents.graph.json` (regen via `pnpm gen:manifest`). */
+export interface AgentGraph {
+ generated_by: string;
+ total_nodes: number;
+ total_edges: number;
+ nodes: AgentGraphNode[];
+ edges: AgentGraphEdge[];
+ package_deps: PackageDep[];
+}
+
export interface AdapterDescriptor {
id: string;
name: string;
@@ -80,10 +114,23 @@ function envIsSet(name: string): boolean {
// whole tracks it.
// ---------------------------------------------------------------------------
+const HERE = dirname(fileURLToPath(import.meta.url));
+
+/** Walk up from examples/ota/src/services to the workspace root. */
+function repoRoot(): string {
+ let cur = HERE;
+ for (let i = 0; i < 8; i++) {
+ if (existsSync(join(cur, 'pnpm-workspace.yaml'))) return cur;
+ const parent = dirname(cur);
+ if (parent === cur) break;
+ cur = parent;
+ }
+ return join(HERE, '..', '..', '..', '..');
+}
+
const OTAIP_VERSION = (() => {
try {
- const here = dirname(fileURLToPath(import.meta.url));
- const pkgPath = join(here, '..', '..', 'package.json');
+ const pkgPath = join(HERE, '..', '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string };
return pkg.version ?? 'unknown';
} catch {
@@ -91,6 +138,15 @@ const OTAIP_VERSION = (() => {
}
})();
+const AGENT_GRAPH: AgentGraph | null = (() => {
+ try {
+ const path = join(repoRoot(), 'agents.graph.json');
+ return JSON.parse(readFileSync(path, 'utf8')) as AgentGraph;
+ } catch {
+ return null;
+ }
+})();
+
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
@@ -129,6 +185,15 @@ export class PlatformService {
};
}
+ /**
+ * Agent navigation graph (workflow + package edges). Returns null when
+ * `agents.graph.json` is missing — callers should treat that as "run
+ * pnpm gen:manifest".
+ */
+ agentGraph(): AgentGraph | null {
+ return AGENT_GRAPH;
+ }
+
adapters(): AdapterDescriptor[] {
return ADAPTERS.map((a) => ({
...a,
diff --git a/examples/platform-ui/src/api/client.ts b/examples/platform-ui/src/api/client.ts
index b7f82f0..362ec81 100644
--- a/examples/platform-ui/src/api/client.ts
+++ b/examples/platform-ui/src/api/client.ts
@@ -5,6 +5,7 @@
import type {
AgentRollup,
+ AgentGraph,
AdapterDescriptor,
ApiError,
HealthReport,
@@ -48,6 +49,7 @@ async function request(path: string, init: RequestInit = {}): Promise {
export const api = {
// Platform — read-only telemetry
agents: () => request('/api/platform/agents'),
+ agentGraph: () => request('/api/platform/agent-graph'),
adapters: () => request<{ adapters: AdapterDescriptor[] }>('/api/platform/adapters'),
health: () => request('/api/platform/health'),
stats: () => request('/api/platform/stats'),
diff --git a/examples/platform-ui/src/api/types.ts b/examples/platform-ui/src/api/types.ts
index 78f9e3f..e48db92 100644
--- a/examples/platform-ui/src/api/types.ts
+++ b/examples/platform-ui/src/api/types.ts
@@ -19,6 +19,39 @@ export interface AgentRollup {
totals: { total: number; active: number; stub: number };
}
+export interface AgentGraphNode {
+ id: string;
+ name: string;
+ stage: string;
+ version: string;
+ contract_status: 'active' | 'stub';
+ has_contract: boolean;
+ source_path: string;
+}
+
+export interface AgentGraphEdge {
+ source: string;
+ target: string;
+ kind: 'workflow' | 'package';
+ label: string;
+}
+
+export interface PackageDep {
+ from_package: string;
+ to_package: string;
+ from_stage: string;
+ to_stage: string;
+}
+
+export interface AgentGraph {
+ generated_by: string;
+ total_nodes: number;
+ total_edges: number;
+ nodes: AgentGraphNode[];
+ edges: AgentGraphEdge[];
+ package_deps: PackageDep[];
+}
+
export interface AdapterDescriptor {
id: string;
name: string;
diff --git a/examples/platform-ui/src/components/AgentMap.tsx b/examples/platform-ui/src/components/AgentMap.tsx
new file mode 100644
index 0000000..45b54a9
--- /dev/null
+++ b/examples/platform-ui/src/components/AgentMap.tsx
@@ -0,0 +1,323 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import type { AgentGraph, AgentGraphNode } from '../api/types';
+
+interface AgentMapProps {
+ graph: AgentGraph;
+}
+
+type StatusFilter = 'all' | 'active' | 'stub';
+
+const STAGE_ORDER = [
+ 'reference',
+ 'search',
+ 'pricing',
+ 'booking',
+ 'ticketing',
+ 'exchange',
+ 'settlement',
+ 'reconciliation',
+ 'tmc',
+ 'platform',
+ 'core',
+ 'lodging',
+] as const;
+
+function compareIds(a: string, b: string): number {
+ const [aMajor = 0, aMinor = 0] = a.split('.').map(Number);
+ const [bMajor = 0, bMinor = 0] = b.split('.').map(Number);
+ return aMajor - bMajor || aMinor - bMinor || a.localeCompare(b);
+}
+
+function stageKey(stage: string): number {
+ const idx = (STAGE_ORDER as readonly string[]).indexOf(stage);
+ return idx === -1 ? 1000 : idx;
+}
+
+export default function AgentMap({ graph }: AgentMapProps) {
+ const [search, setSearch] = useState('');
+ const [stage, setStage] = useState('all');
+ const [status, setStatus] = useState('all');
+ const [workflow, setWorkflow] = useState('all');
+ const [selected, setSelected] = useState(null);
+ const boardRef = useRef(null);
+ const wrapRef = useRef(null);
+ const [paths, setPaths] = useState>([]);
+ const [svgSize, setSvgSize] = useState({ w: 1, h: 1 });
+
+ const stages = useMemo(() => {
+ const set = new Set(graph.nodes.map((n) => n.stage));
+ return ['all', ...[...set].sort((a, b) => stageKey(a) - stageKey(b) || a.localeCompare(b))];
+ }, [graph.nodes]);
+
+ const workflows = useMemo(() => {
+ const set = new Set(
+ graph.edges.filter((e) => e.kind === 'workflow').map((e) => e.label),
+ );
+ return ['all', ...[...set].sort()];
+ }, [graph.edges]);
+
+ const nodesById = useMemo(() => {
+ const m = new Map();
+ for (const n of graph.nodes) m.set(n.id, n);
+ return m;
+ }, [graph.nodes]);
+
+ const workflowPathIds = useMemo(() => {
+ if (workflow === 'all') return null;
+ const ids = new Set();
+ for (const e of graph.edges) {
+ if (e.kind === 'workflow' && e.label === workflow) {
+ ids.add(e.source);
+ ids.add(e.target);
+ }
+ }
+ return ids;
+ }, [graph.edges, workflow]);
+
+ const visibleIds = useMemo(() => {
+ const q = search.trim().toLowerCase();
+ const ids = new Set();
+ for (const n of graph.nodes) {
+ if (stage !== 'all' && n.stage !== stage) continue;
+ if (status !== 'all' && n.contract_status !== status) continue;
+ if (q && !(n.id.includes(q) || n.name.toLowerCase().includes(q))) continue;
+ if (workflowPathIds && !workflowPathIds.has(n.id)) continue;
+ ids.add(n.id);
+ }
+ return ids;
+ }, [graph.nodes, search, stage, status, workflowPathIds]);
+
+ const columns = useMemo(() => {
+ const map = new Map();
+ for (const n of graph.nodes) {
+ if (!visibleIds.has(n.id)) continue;
+ const list = map.get(n.stage) ?? [];
+ list.push(n);
+ map.set(n.stage, list);
+ }
+ for (const list of map.values()) list.sort((a, b) => compareIds(a.id, b.id));
+ return [...map.entries()].sort(([a], [b]) => stageKey(a) - stageKey(b) || a.localeCompare(b));
+ }, [graph.nodes, visibleIds]);
+
+ const selectedNode = selected ? nodesById.get(selected) ?? null : null;
+
+ const selectedWorkflows = useMemo(() => {
+ if (!selected) return [] as string[];
+ const set = new Set();
+ for (const e of graph.edges) {
+ if (e.kind === 'workflow' && (e.source === selected || e.target === selected)) {
+ set.add(e.label);
+ }
+ }
+ return [...set].sort();
+ }, [graph.edges, selected]);
+
+ const neighborIds = useMemo(() => {
+ if (!selected) return new Set();
+ const set = new Set();
+ for (const e of graph.edges) {
+ if (e.source === selected) set.add(e.target);
+ if (e.target === selected) set.add(e.source);
+ }
+ return set;
+ }, [graph.edges, selected]);
+
+ useEffect(() => {
+ function redraw() {
+ const wrap = wrapRef.current;
+ const board = boardRef.current;
+ if (!wrap || !board) {
+ setPaths([]);
+ return;
+ }
+ const wrapBox = wrap.getBoundingClientRect();
+ setSvgSize({
+ w: Math.max(wrap.scrollWidth, 1),
+ h: Math.max(wrap.clientHeight, board.scrollHeight, 1),
+ });
+ const centers = new Map();
+ for (const id of visibleIds) {
+ const el = board.querySelector(`[data-agent-id="${CSS.escape(id)}"]`);
+ if (!(el instanceof HTMLElement)) continue;
+ const r = el.getBoundingClientRect();
+ centers.set(id, {
+ x: r.left - wrapBox.left + wrap.scrollLeft + r.width / 2,
+ y: r.top - wrapBox.top + wrap.scrollTop + r.height / 2,
+ });
+ }
+ const next: Array<{ d: string; kind: string; hl: boolean }> = [];
+ for (const e of graph.edges) {
+ if (e.kind === 'workflow' && workflow !== 'all' && e.label !== workflow) continue;
+ if (e.kind === 'package' && workflow !== 'all') continue;
+ if (!visibleIds.has(e.source) || !visibleIds.has(e.target)) continue;
+ const a = centers.get(e.source);
+ const b = centers.get(e.target);
+ if (!a || !b) continue;
+ const dx = Math.max(40, Math.abs(b.x - a.x) * 0.35);
+ const d = `M ${a.x} ${a.y} C ${a.x + dx} ${a.y}, ${b.x - dx} ${b.y}, ${b.x} ${b.y}`;
+ const hl = Boolean(selected && (e.source === selected || e.target === selected));
+ next.push({ d, kind: e.kind, hl });
+ }
+ setPaths(next);
+ }
+
+ redraw();
+ const wrap = wrapRef.current;
+ wrap?.addEventListener('scroll', redraw);
+ window.addEventListener('resize', redraw);
+ return () => {
+ wrap?.removeEventListener('scroll', redraw);
+ window.removeEventListener('resize', redraw);
+ };
+ }, [graph.edges, visibleIds, workflow, selected, columns]);
+
+ return (
+
+
+
+
+
+
+ {paths.map((p, i) => (
+
+ ))}
+
+
+ {columns.length === 0 ? (
+
No agents match these filters.
+ ) : null}
+ {columns.map(([colStage, agents]) => (
+
+
+ {colStage} {agents.length}
+
+
+ {agents.map((a) => {
+ const isSelected = selected === a.id;
+ const dim = Boolean(selected) && !isSelected && !neighborIds.has(a.id);
+ const onPath = Boolean(workflowPathIds?.has(a.id));
+ return (
+
setSelected(a.id)}
+ className={[
+ 'rounded-md border bg-white px-2 py-1.5 text-left transition',
+ a.contract_status === 'active' ? 'border-l-[3px] border-l-teal-700' : 'border-l-[3px] border-l-stone-400',
+ isSelected || onPath ? 'border-teal-600 shadow-[0_0_0_1px_#0d9488]' : 'border-slate-200',
+ dim ? 'opacity-30' : '',
+ ].join(' ')}
+ >
+ {a.id}
+ {a.name}
+
+ );
+ })}
+
+
+ ))}
+
+
+
+
+ {!selectedNode ? (
+ Select an agent to see details and connected workflows.
+ ) : (
+
+
{selectedNode.name}
+
+ ID
+ {selectedNode.id}
+ Stage
+ {selectedNode.stage}
+ Version
+ {selectedNode.version}
+ Status
+ {selectedNode.contract_status}
+ Contract
+ {selectedNode.has_contract ? 'yes' : 'no'}
+ Source
+ {selectedNode.source_path}
+
+
+
Workflows
+ {selectedWorkflows.length === 0 ? (
+
Not in a built-in workflow.
+ ) : (
+
+ {selectedWorkflows.map((w) => (
+ {w}
+ ))}
+
+ )}
+
+
+ )}
+
+ Solid arcs = orchestrator workflows. Dashed arcs = package workspace deps (stage hubs).
+ Navigation only — not a domain knowledge graph.
+
+
+
+
+ );
+}
diff --git a/examples/platform-ui/src/pages/Dashboard.tsx b/examples/platform-ui/src/pages/Dashboard.tsx
index 5ae6838..89de7ec 100644
--- a/examples/platform-ui/src/pages/Dashboard.tsx
+++ b/examples/platform-ui/src/pages/Dashboard.tsx
@@ -1,35 +1,42 @@
import { useEffect, useState } from 'react';
import { api } from '../api/client';
-import type { AdapterDescriptor, AgentRollup, HealthReport, PlatformStats } from '../api/types';
+import type { AdapterDescriptor, AgentGraph, AgentRollup, HealthReport, PlatformStats } from '../api/types';
import StatCard from '../components/StatCard';
import AgentTable from '../components/AgentTable';
+import AgentMap from '../components/AgentMap';
import AdapterCard from '../components/AdapterCard';
import HealthSidebar from '../components/HealthSidebar';
+type AgentView = 'table' | 'map';
+
export default function Dashboard() {
const [agents, setAgents] = useState(null);
+ const [graph, setGraph] = useState(null);
const [adapters, setAdapters] = useState(null);
const [stats, setStats] = useState(null);
const [health, setHealth] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
+ const [agentView, setAgentView] = useState('table');
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
try {
- const [a, ad, s, h] = await Promise.all([
+ const [a, ad, s, h, g] = await Promise.all([
api.agents(),
api.adapters(),
api.stats(),
api.health(),
+ api.agentGraph().catch(() => null),
]);
if (cancelled) return;
setAgents(a);
setAdapters(ad.adapters);
setStats(s);
setHealth(h);
+ setGraph(g);
setError(null);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load dashboard');
@@ -91,13 +98,48 @@ export default function Dashboard() {
- {agents ? (
-
- ) : (
-
- {loading ? 'Loading agents…' : 'No agent data.'}
+
+
+ setAgentView('table')}
+ className={[
+ 'rounded px-3 py-1.5 font-medium transition',
+ agentView === 'table' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900',
+ ].join(' ')}
+ >
+ Table
+
+ setAgentView('map')}
+ className={[
+ 'rounded px-3 py-1.5 font-medium transition',
+ agentView === 'map' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900',
+ ].join(' ')}
+ >
+ Map
+
- )}
+
+ {agentView === 'table' ? (
+ agents ? (
+
+ ) : (
+
+ {loading ? 'Loading agents…' : 'No agent data.'}
+
+ )
+ ) : graph ? (
+
+ ) : (
+
+ {loading
+ ? 'Loading agent map…'
+ : 'Agent map unavailable. Run `pnpm gen:manifest` to generate agents.graph.json.'}
+
+ )}
+
diff --git a/package.json b/package.json
index b670bf5..34ec8c5 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"data:download": "tsx scripts/download-airport-data.ts",
"count:agents": "tsx scripts/count-agents.ts",
"gen:manifest": "tsx scripts/generate-manifest.ts",
+ "gen:agent-map": "tsx scripts/generate-agent-map-html.ts",
"verify:dist": "tsx scripts/verify-dist.ts",
"verify": "pnpm -r run build && pnpm verify:dist",
"ligare": "pnpm --filter @otaip/example-ligare demo",
@@ -35,13 +36,13 @@
"onlyBuiltDependencies": [],
"overrides": {
"hono": ">=4.12.25",
- "ip-address": ">=10.1.1",
- "fast-uri": ">=3.1.3",
+ "ip-address": ">=10.3.1",
+ "fast-uri": ">=4.1.2",
"vite": "^8.0.16",
"esbuild": ">=0.28.1",
"qs": ">=6.15.2",
"postcss": ">=8.5.18",
- "brace-expansion": ">=5.0.8",
+ "brace-expansion": ">=5.0.9",
"find-my-way": ">=9.7.0",
"@fastify/static": ">=10.1.1",
"@hono/node-server": ">=2.0.5",
diff --git a/packages/agents-platform/src/index.ts b/packages/agents-platform/src/index.ts
index f3bc9b5..cdf27a8 100644
--- a/packages/agents-platform/src/index.ts
+++ b/packages/agents-platform/src/index.ts
@@ -6,6 +6,7 @@
export { OrchestratorAgent } from './orchestrator/index.js';
export { PipelineBuilder } from './orchestrator/index.js';
+export { WORKFLOW_PIPELINES } from './orchestrator/index.js';
export type {
OrchestratorInput,
OrchestratorOutput,
diff --git a/packages/agents-platform/src/orchestrator/index.ts b/packages/agents-platform/src/orchestrator/index.ts
index c2f1c0f..ff53473 100644
--- a/packages/agents-platform/src/orchestrator/index.ts
+++ b/packages/agents-platform/src/orchestrator/index.ts
@@ -11,20 +11,12 @@ import type {
OrchestratorOutput,
WorkflowStep,
StepExecutor,
- WorkflowName,
PipelineDefinition,
PipelineEntry,
PipelineStep,
ParallelStep,
} from './types.js';
-
-const WORKFLOW_PIPELINES: Record = {
- search_to_price: ['1.1', '1.4', '2.1', '2.2', '2.3'],
- book_to_ticket: ['3.3', '3.1', '3.2', '4.1'],
- full_booking: ['1.1', '1.4', '2.1', '2.2', '2.3', '3.3', '3.1', '3.2', '4.1'],
- exchange_flow: ['5.1', '5.2'],
- refund_flow: ['6.1', '6.2'],
-};
+import { WORKFLOW_PIPELINES } from './workflows.js';
const VALID_WORKFLOWS = new Set(Object.keys(WORKFLOW_PIPELINES));
@@ -332,4 +324,5 @@ export type {
PipelineDefinition,
} from './types.js';
+export { WORKFLOW_PIPELINES } from './workflows.js';
export { PipelineBuilder } from './pipeline-builder.js';
diff --git a/packages/agents-platform/src/orchestrator/workflows.ts b/packages/agents-platform/src/orchestrator/workflows.ts
new file mode 100644
index 0000000..70db574
--- /dev/null
+++ b/packages/agents-platform/src/orchestrator/workflows.ts
@@ -0,0 +1,18 @@
+/**
+ * Built-in orchestrator workflow pipelines.
+ *
+ * Kept in a dedicated module so tooling (agent graph generator) can import
+ * the same source of truth the OrchestratorAgent executes — no duplicated
+ * agent-id sequences.
+ */
+
+import type { WorkflowName } from './types.js';
+
+/** Ordered agent ids for each built-in workflow name. */
+export const WORKFLOW_PIPELINES: Record = {
+ search_to_price: ['1.1', '1.4', '2.1', '2.2', '2.3'],
+ book_to_ticket: ['3.3', '3.1', '3.2', '4.1'],
+ full_booking: ['1.1', '1.4', '2.1', '2.2', '2.3', '3.3', '3.1', '3.2', '4.1'],
+ exchange_flow: ['5.1', '5.2'],
+ refund_flow: ['6.1', '6.2'],
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a16579c..47cc992 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,13 +6,13 @@ settings:
overrides:
hono: '>=4.12.25'
- ip-address: '>=10.1.1'
- fast-uri: '>=3.1.3'
+ ip-address: '>=10.3.1'
+ fast-uri: '>=4.1.2'
vite: ^8.0.16
esbuild: '>=0.28.1'
qs: '>=6.15.2'
postcss: '>=8.5.18'
- brace-expansion: '>=5.0.8'
+ brace-expansion: '>=5.0.9'
find-my-way: '>=9.7.0'
'@fastify/static': '>=10.1.1'
'@hono/node-server': '>=2.0.5'
@@ -1351,8 +1351,8 @@ packages:
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
engines: {node: '>=18'}
- brace-expansion@5.0.8:
- resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
+ brace-expansion@5.0.9:
+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
braces@3.0.3:
@@ -1648,8 +1648,8 @@ packages:
fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
- fast-uri@4.1.1:
- resolution: {integrity: sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==}
+ fast-uri@4.1.2:
+ resolution: {integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==}
fastify-plugin@5.1.0:
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
@@ -1791,8 +1791,8 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
- ip-address@10.2.0:
- resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
+ ip-address@10.4.0:
+ resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
@@ -2811,7 +2811,7 @@ snapshots:
dependencies:
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
- fast-uri: 4.1.1
+ fast-uri: 4.1.2
'@fastify/cors@11.2.0':
dependencies:
@@ -3339,7 +3339,7 @@ snapshots:
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
- fast-uri: 4.1.1
+ fast-uri: 4.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -3397,7 +3397,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- brace-expansion@5.0.8:
+ brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
@@ -3657,7 +3657,7 @@ snapshots:
express-rate-limit@8.3.2(express@5.2.1):
dependencies:
express: 5.2.1
- ip-address: 10.2.0
+ ip-address: 10.4.0
express@5.2.1:
dependencies:
@@ -3711,7 +3711,7 @@ snapshots:
'@fastify/merge-json-schemas': 0.2.1
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
- fast-uri: 4.1.1
+ fast-uri: 4.1.2
json-schema-ref-resolver: 3.0.0
rfdc: 1.4.1
@@ -3721,7 +3721,7 @@ snapshots:
dependencies:
fast-decode-uri-component: 1.0.1
- fast-uri@4.1.1: {}
+ fast-uri@4.1.2: {}
fastify-plugin@5.1.0: {}
@@ -3877,7 +3877,7 @@ snapshots:
inherits@2.0.4: {}
- ip-address@10.2.0: {}
+ ip-address@10.4.0: {}
ipaddr.js@1.9.1: {}
@@ -4036,7 +4036,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
- brace-expansion: 5.0.8
+ brace-expansion: 5.0.9
minipass@7.1.3: {}
diff --git a/scripts/agent-graph.ts b/scripts/agent-graph.ts
new file mode 100644
index 0000000..83246ed
--- /dev/null
+++ b/scripts/agent-graph.ts
@@ -0,0 +1,174 @@
+/**
+ * Shared agent-graph builders used by `generate-manifest.ts` and
+ * `generate-agent-map-html.ts`.
+ *
+ * Nodes come from the agent manifest. Edges are explicit only:
+ * - workflow: consecutive steps in Orchestrator WORKFLOW_PIPELINES
+ * - package: stage-hub links derived from workspace package.json deps
+ *
+ * This is NOT a domain knowledge graph and not Graphify — it maps OTAIP
+ * agents for navigation only.
+ */
+
+import { WORKFLOW_PIPELINES } from '../packages/agents-platform/src/orchestrator/workflows.js';
+
+export interface AgentGraphNode {
+ readonly id: string;
+ readonly name: string;
+ readonly stage: string;
+ readonly version: string;
+ readonly contract_status: 'active' | 'stub';
+ readonly has_contract: boolean;
+ readonly source_path: string;
+}
+
+export interface AgentGraphEdge {
+ readonly source: string;
+ readonly target: string;
+ readonly kind: 'workflow' | 'package';
+ readonly label: string;
+}
+
+export interface PackageDep {
+ readonly from_package: string;
+ readonly to_package: string;
+ readonly from_stage: string;
+ readonly to_stage: string;
+}
+
+export interface AgentGraph {
+ readonly generated_by: string;
+ readonly total_nodes: number;
+ readonly total_edges: number;
+ readonly nodes: readonly AgentGraphNode[];
+ readonly edges: readonly AgentGraphEdge[];
+ readonly package_deps: readonly PackageDep[];
+}
+
+/** npm package name → discovery stage name. */
+export const PACKAGE_TO_STAGE: Readonly> = {
+ '@otaip/agents-reference': 'reference',
+ '@otaip/agents-search': 'search',
+ '@otaip/agents-pricing': 'pricing',
+ '@otaip/agents-booking': 'booking',
+ '@otaip/agents-ticketing': 'ticketing',
+ '@otaip/agents-exchange': 'exchange',
+ '@otaip/agents-settlement': 'settlement',
+ '@otaip/agents-reconciliation': 'reconciliation',
+ '@otaip/agents-lodging': 'lodging',
+ '@otaip/agents-platform': 'platform',
+ '@otaip/agents-tmc': 'tmc',
+};
+
+/**
+ * Workspace agent-package dependencies (mirrors package.json files).
+ * Kept here as a table so graph generation does not need to re-read every
+ * package.json; update when agent package deps change.
+ */
+export const AGENT_PACKAGE_DEPS: ReadonlyArray<{ from: string; to: string }> = [
+ { from: '@otaip/agents-search', to: '@otaip/agents-reference' },
+ { from: '@otaip/agents-pricing', to: '@otaip/agents-reference' },
+ { from: '@otaip/agents-booking', to: '@otaip/agents-reference' },
+ { from: '@otaip/agents-booking', to: '@otaip/agents-search' },
+ { from: '@otaip/agents-booking', to: '@otaip/agents-pricing' },
+ { from: '@otaip/agents-ticketing', to: '@otaip/agents-booking' },
+ { from: '@otaip/agents-exchange', to: '@otaip/agents-search' },
+ { from: '@otaip/agents-exchange', to: '@otaip/agents-ticketing' },
+ { from: '@otaip/agents-settlement', to: '@otaip/agents-ticketing' },
+ { from: '@otaip/agents-reconciliation', to: '@otaip/agents-reference' },
+];
+
+export interface ManifestAgentLike {
+ readonly id: string;
+ readonly name: string;
+ readonly stage: string;
+ readonly version: string;
+ readonly contract_status: 'active' | 'stub';
+ readonly has_contract: boolean;
+ readonly source_path: string;
+}
+
+function compareAgentIds(a: string, b: string): number {
+ const [aMajor = 0, aMinor = 0] = a.split('.').map(Number);
+ const [bMajor = 0, bMinor = 0] = b.split('.').map(Number);
+ return aMajor - bMajor || aMinor - bMinor || a.localeCompare(b);
+}
+
+function stageHubId(
+ nodesByStage: Map,
+ stage: string,
+): string | undefined {
+ const list = nodesByStage.get(stage);
+ if (!list || list.length === 0) return undefined;
+ return [...list].sort((a, b) => compareAgentIds(a.id, b.id))[0]?.id;
+}
+
+export function buildAgentGraph(agents: readonly ManifestAgentLike[]): AgentGraph {
+ const nodes: AgentGraphNode[] = agents.map((a) => ({
+ id: a.id,
+ name: a.name,
+ stage: a.stage,
+ version: a.version,
+ contract_status: a.contract_status,
+ has_contract: a.has_contract,
+ source_path: a.source_path,
+ }));
+
+ const nodeIds = new Set(nodes.map((n) => n.id));
+ const nodesByStage = new Map();
+ for (const n of nodes) {
+ const list = nodesByStage.get(n.stage) ?? [];
+ list.push(n);
+ nodesByStage.set(n.stage, list);
+ }
+
+ const edges: AgentGraphEdge[] = [];
+ const seen = new Set();
+
+ const pushEdge = (edge: AgentGraphEdge): void => {
+ const key = `${edge.kind}|${edge.source}|${edge.target}|${edge.label}`;
+ if (seen.has(key)) return;
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) return;
+ seen.add(key);
+ edges.push(edge);
+ };
+
+ for (const [workflow, pipeline] of Object.entries(WORKFLOW_PIPELINES)) {
+ for (let i = 0; i < pipeline.length - 1; i++) {
+ const source = pipeline[i]!;
+ const target = pipeline[i + 1]!;
+ pushEdge({ source, target, kind: 'workflow', label: workflow });
+ }
+ }
+
+ const package_deps: PackageDep[] = [];
+ for (const dep of AGENT_PACKAGE_DEPS) {
+ const from_stage = PACKAGE_TO_STAGE[dep.from];
+ const to_stage = PACKAGE_TO_STAGE[dep.to];
+ if (!from_stage || !to_stage) continue;
+ package_deps.push({
+ from_package: dep.from,
+ to_package: dep.to,
+ from_stage,
+ to_stage,
+ });
+ const source = stageHubId(nodesByStage, from_stage);
+ const target = stageHubId(nodesByStage, to_stage);
+ if (!source || !target) continue;
+ pushEdge({
+ source,
+ target,
+ kind: 'package',
+ label: `${dep.from} → ${dep.to}`,
+ });
+ }
+
+ return {
+ generated_by: 'scripts/generate-manifest.ts',
+ total_nodes: nodes.length,
+ total_edges: edges.length,
+ nodes: [...nodes].sort((a, b) => compareAgentIds(a.id, b.id)),
+ edges,
+ package_deps,
+ };
+}
diff --git a/scripts/generate-agent-map-html.ts b/scripts/generate-agent-map-html.ts
new file mode 100644
index 0000000..d1a6105
--- /dev/null
+++ b/scripts/generate-agent-map-html.ts
@@ -0,0 +1,520 @@
+#!/usr/bin/env tsx
+/**
+ * Render `docs/agent-map.html` from an AgentGraph.
+ *
+ * Callable standalone (`pnpm gen:agent-map`) or from `generate-manifest.ts`
+ * so CI `--check` keeps the HTML in sync with the graph.
+ *
+ * Self-contained: no external JS/CSS. Stage columns + SVG workflow arcs.
+ * Not a domain knowledge graph — agent navigation only.
+ */
+
+import { existsSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type { AgentGraph, AgentGraphNode } from './agent-graph.js';
+
+const GRAPH_PATH = 'agents.graph.json';
+const MAP_HTML_PATH = 'docs/agent-map.html';
+
+/** Display order for stage columns. Unknown stages append alphabetically. */
+const STAGE_ORDER = [
+ 'reference',
+ 'search',
+ 'pricing',
+ 'booking',
+ 'ticketing',
+ 'exchange',
+ 'settlement',
+ 'reconciliation',
+ 'tmc',
+ 'platform',
+ 'core',
+ 'lodging',
+] as const;
+
+function repoRoot(): string {
+ let cur = dirname(fileURLToPath(import.meta.url));
+ for (let i = 0; i < 8; i++) {
+ if (existsSync(join(cur, 'pnpm-workspace.yaml'))) return cur;
+ const parent = dirname(cur);
+ if (parent === cur) break;
+ cur = parent;
+ }
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
+}
+
+function compareAgentIds(a: string, b: string): number {
+ const [aMajor = 0, aMinor = 0] = a.split('.').map(Number);
+ const [bMajor = 0, bMinor = 0] = b.split('.').map(Number);
+ return aMajor - bMajor || aMinor - bMinor || a.localeCompare(b);
+}
+
+function stageSortKey(stage: string): number {
+ const idx = (STAGE_ORDER as readonly string[]).indexOf(stage);
+ return idx === -1 ? 1000 : idx;
+}
+
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function groupByStage(nodes: readonly AgentGraphNode[]): Array<[string, AgentGraphNode[]]> {
+ const map = new Map();
+ for (const n of nodes) {
+ const list = map.get(n.stage) ?? [];
+ list.push(n);
+ map.set(n.stage, list);
+ }
+ for (const list of map.values()) {
+ list.sort((a, b) => compareAgentIds(a.id, b.id));
+ }
+ return [...map.entries()].sort(([a], [b]) => stageSortKey(a) - stageSortKey(b) || a.localeCompare(b));
+}
+
+/**
+ * Build a standalone HTML page that embeds the graph JSON and renders
+ * stage columns with interactive selection.
+ */
+export function renderAgentMapHtml(graph: AgentGraph): string {
+ const stages = groupByStage(graph.nodes);
+ const columnsHtml = stages
+ .map(([stage, agents]) => {
+ const cards = agents
+ .map((a) => {
+ const status = a.contract_status === 'active' ? 'active' : 'stub';
+ const contract = a.has_contract ? 'contracted' : 'no-contract';
+ return `
+ ${escapeHtml(a.id)}
+ ${escapeHtml(a.name)}
+ `;
+ })
+ .join('\n');
+ return `
+ ${escapeHtml(stage)} ${agents.length}
+
+${cards}
+
+ `;
+ })
+ .join('\n');
+
+ const payload = JSON.stringify(graph);
+
+ return `
+
+
+
+
+ OTAIP Agent Map
+
+
+
+
+
+
+
+ Generated by pnpm gen:manifest from agents.graph.json
+ (${graph.total_nodes} agents, ${graph.total_edges} edges). Do not hand-edit.
+
+
+
+
+
+`;
+}
+
+function main(): void {
+ const repo = repoRoot();
+ const graphPath = join(repo, GRAPH_PATH);
+ if (!existsSync(graphPath)) {
+ console.error(`${GRAPH_PATH} missing. Run \`pnpm gen:manifest\` first.`);
+ process.exit(1);
+ }
+ const graph = JSON.parse(readFileSync(graphPath, 'utf8')) as AgentGraph;
+ const html = renderAgentMapHtml(graph);
+ const out = join(repo, MAP_HTML_PATH);
+ writeFileSync(out, html);
+ console.log(`Wrote ${MAP_HTML_PATH} (${graph.total_nodes} agents).`);
+}
+
+// Only run as CLI when invoked directly (not when imported by generate-manifest).
+const isDirect =
+ process.argv[1] !== undefined &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url);
+
+if (isDirect) {
+ main();
+}
diff --git a/scripts/generate-manifest.ts b/scripts/generate-manifest.ts
index 83d8822..fc0c7b0 100644
--- a/scripts/generate-manifest.ts
+++ b/scripts/generate-manifest.ts
@@ -1,6 +1,7 @@
#!/usr/bin/env tsx
/**
- * Generate `agents.manifest.json` — the single authoritative roster.
+ * Generate `agents.manifest.json` — the single authoritative roster —
+ * and `agents.graph.json` — a navigation graph of agents + workflow edges.
*
* OTAIP historically had four overlapping notions of "which agents exist":
* - `discoverAgents()` (filesystem walk, all 75)
@@ -17,13 +18,17 @@
* have contracts and some don't, and now that's machine-readable instead of
* inferred.
*
+ * `agents.graph.json` is derived from the same roster plus Orchestrator
+ * workflow pipelines and agent-package workspace deps. It is for visual
+ * navigation only — not a travel-domain knowledge graph.
+ *
* Requires a prior build (contracts import `@otaip/core`, which resolves to
* its built `dist`). CI runs this after `pnpm -r run build` and fails if the
* committed manifest is stale (see the freshness check in CI).
*
* Usage:
- * pnpm gen:manifest # write agents.manifest.json
- * pnpm gen:manifest --check # exit 1 if the file would change (CI)
+ * pnpm gen:manifest # write agents.manifest.json + agents.graph.json + docs/agent-map.html
+ * pnpm gen:manifest --check # exit 1 if any generated file would change (CI)
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -42,8 +47,12 @@ import {
type DiscoveredAgent,
discoverAgents,
} from '../packages/core/src/discovery/agent-discovery.js';
+import { buildAgentGraph } from './agent-graph.js';
+import { renderAgentMapHtml } from './generate-agent-map-html.js';
const MANIFEST_PATH = 'agents.manifest.json';
+const GRAPH_PATH = 'agents.graph.json';
+const MAP_HTML_PATH = 'docs/agent-map.html';
interface ManifestAgent {
readonly id: string;
@@ -153,28 +162,58 @@ async function buildManifest(): Promise {
};
}
+function checkOrWrite(path: string, contents: string, check: boolean): boolean {
+ if (check) {
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
+ return current === contents;
+ }
+ writeFileSync(path, contents);
+ return true;
+}
+
async function main(): Promise {
const repo = repoRoot();
- const outPath = join(repo, MANIFEST_PATH);
+ const check = process.argv.includes('--check');
const manifest = await buildManifest();
- const json = JSON.stringify(manifest, null, 2) + '\n';
+ const graph = buildAgentGraph(manifest.agents);
+
+ const manifestJson = JSON.stringify(manifest, null, 2) + '\n';
+ const graphJson = JSON.stringify(graph, null, 2) + '\n';
+ const mapHtml = renderAgentMapHtml(graph);
- if (process.argv.includes('--check')) {
- const current = existsSync(outPath) ? readFileSync(outPath, 'utf8') : '';
- if (current !== json) {
+ const manifestPath = join(repo, MANIFEST_PATH);
+ const graphPath = join(repo, GRAPH_PATH);
+ const mapPath = join(repo, MAP_HTML_PATH);
+
+ const okManifest = checkOrWrite(manifestPath, manifestJson, check);
+ const okGraph = checkOrWrite(graphPath, graphJson, check);
+ const okMap = checkOrWrite(mapPath, mapHtml, check);
+
+ if (check) {
+ const stale: string[] = [];
+ if (!okManifest) stale.push(MANIFEST_PATH);
+ if (!okGraph) stale.push(GRAPH_PATH);
+ if (!okMap) stale.push(MAP_HTML_PATH);
+ if (stale.length > 0) {
console.error(
- `${MANIFEST_PATH} is stale. Run \`pnpm gen:manifest\` and commit the result.`,
+ `${stale.join(', ')} stale. Run \`pnpm gen:manifest\` and commit the result.`,
);
process.exit(1);
}
- console.log(`${MANIFEST_PATH} is up to date (${manifest.total} agents, ${manifest.with_contract} contracted).`);
+ console.log(
+ `${MANIFEST_PATH}, ${GRAPH_PATH}, and ${MAP_HTML_PATH} are up to date ` +
+ `(${manifest.total} agents, ${manifest.with_contract} contracted, ${graph.total_edges} edges).`,
+ );
return;
}
- writeFileSync(outPath, json);
console.log(
`Wrote ${MANIFEST_PATH}: ${manifest.total} agents, ${manifest.with_contract} with contracts.`,
);
+ console.log(
+ `Wrote ${GRAPH_PATH}: ${graph.total_nodes} nodes, ${graph.total_edges} edges.`,
+ );
+ console.log(`Wrote ${MAP_HTML_PATH}.`);
}
main().catch((err) => {