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 + + + +
+

OTAIP Agent Map

+

+ Stage columns for every discovered agent. Solid arcs are Orchestrator + workflow steps; dashed arcs are package workspace dependencies (stage hubs). + This is navigation only — not a travel-domain knowledge graph. +

+
+
+ + + + +
+ Active + Stub + Workflow + Package dep +
+
+
+
+ +
+
+

reference 7

+
+ + + + + + + +
+
+
+

search 8

+
+ + + + + + + + +
+
+
+

pricing 7

+
+ + + + + + + +
+
+
+

booking 8

+
+ + + + + + + + +
+
+
+

ticketing 5

+
+ + + + + +
+
+
+

exchange 6

+
+ + + + + + +
+
+
+

settlement 6

+
+ + + + + + +
+
+
+

reconciliation 6

+
+ + + + + + +
+
+
+

tmc 5

+
+ + + + + +
+
+
+

platform 9

+
+ + + + + + + + + +
+
+
+

core 1

+
+ +
+
+
+

lodging 9

+
+ + + + + + + + + +
+
+
+
+ +
+

+ 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 ( +
+
+

Agent Map

+ + {visibleIds.size} of {graph.total_nodes} · {graph.total_edges} edges + +
+ setSearch(e.target.value)} + placeholder="Search…" + className="w-44 rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm placeholder:text-slate-400 focus:border-slate-500 focus:outline-none" + /> + + + +
+
+ +
+
+ + {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 ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+ ); +} 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.'} +
+
+ +
- )} + + {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 ` `; + }) + .join('\n'); + return `
+

${escapeHtml(stage)} ${agents.length}

+
+${cards} +
+
`; + }) + .join('\n'); + + const payload = JSON.stringify(graph); + + return ` + + + + + OTAIP Agent Map + + + +
+

OTAIP Agent Map

+

+ Stage columns for every discovered agent. Solid arcs are Orchestrator + workflow steps; dashed arcs are package workspace dependencies (stage hubs). + This is navigation only — not a travel-domain knowledge graph. +

+
+
+ + + + +
+ Active + Stub + Workflow + Package dep +
+
+
+
+ +
+${columnsHtml} +
+
+ +
+

+ 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) => {