Skip to content

Track 3 — Agent Orchestration

The voyage is planned. The crew is confirmed. The cargo is scheduled. And now operations walks in with the final ask: "I need one document. Voyage, ship, crew, cargo — all of it, resolved, in one payload. Before the ship leaves."

Three separate calls and client-side stitching would work. But we already have the patterns (Track 1 Step 7) and the abstraction (Track 2 Step 9). This track scales them up: four call steps and three lookup steps, defined once as an aggregate function, referenced from MCP, Skill, and REST simultaneously — and split across multiple files for maintainability.

Prerequisites. Track 1 and Track 2 completed. Files live in ikanos-docs/tutorial/ in the public Ikanos repository.


Step 11 — The Fleet Manifest

Files:

Shared consumes

The Fleet Manifest needs the Maritime Registry, the Crew API, the Cargo API, and a Legacy Vessels XML feed. Rather than redeclaring consumes in every capability, factor them into shared files imported via from: / import:.

# step-11-shipyard-fleet-manifest.yml
ikanos: "1.0.0-alpha3"

capability:
  consumes:
    - from: "shared/step11-registry-consumes.yml"
      import: registry
    - from: "shared/legacy-consumes.yaml"
      import: legacy

This is the same trick that keeps a multi-capability fleet maintainable — one team owns the consumes declaration for an API, every capability reuses it.

The seven-step aggregate

capability:
  aggregates:                                # alpha3 — keyed map by namespace
    manifest:
      display: "Voyage Manifest"
      flows:                                 # alpha3 — keyed map by flow name (was `functions`)
        build-fleet-manifest:
          description: "Resolve voyage + ship + crew + cargo into a single manifest."
          semantics: { safe: true, idempotent: true, cacheable: true }
          inputParameters:
            voyageId: { type: string, required: true }
          steps:                             # alpha3 — keyed map by step name
            get-voyage:
              type: call
              call: registry.get-voyage
              with: { voyage_id: manifest.voyageId }
            get-ship:
              type: call
              call: registry.get-ship
              with: { imo_number: "$.get-voyage.imo_number" }
            list-crew:
              type: call
              call: registry.list-crew
            get-cargo:
              type: call
              call: registry.get-cargo
              with: { voyage_id: manifest.voyageId }
            resolve-captain:
              type: lookup
              index: list-crew
              match: crewId
              lookupValue: "$.get-ship.captainId"
              outputParameters: ["fullName", "certifications"]
            resolve-crew:
              type: lookup
              index: list-crew
              match: crewId
              lookupValue: "$.get-ship.assignedCrew"
              outputParameters: ["fullName", "role"]
            resolve-legacy:
              type: lookup
              index: legacy.list-vessels
              match: imoNumber
              lookupValue: "$.get-ship.imo_number"
              outputParameters: ["lastSurvey"]
          mappings:
            - { target: voyage,     value: "$.get-voyage" }
            - { target: ship,       value: "$.get-ship" }
            - { target: captain,    value: "$.resolve-captain" }
            - { target: crew,       value: "$.resolve-crew" }
            - { target: cargo,      value: "$.get-cargo" }
            - { target: lastSurvey, value: "$.resolve-legacy.lastSurvey" }

Seven steps, three data sources (registry, crew, legacy), all stitched server-side. The agent gets a single JSON document.

One aggregate, three doors

The same manifest.build-fleet-manifest aggregate appears on all three expose adapters:

exposes:
  - type: mcp
    namespace: shipyard-tools
    tools:                                  # alpha3 — keyed map
      build-fleet-manifest:
        ref: manifest.build-fleet-manifest
        inputParameters:
          voyageId: { type: string, required: true }
        with: { voyageId: shipyard-tools.voyageId }

  - type: skill
    namespace: shipyard-skills
    skills:                               # alpha3 — keyed map
      ops-handover:
        description: "End-to-end voyage handover documents"
        tools:                            # alpha3 — keyed map
          build-fleet-manifest:
            from: { sourceNamespace: shipyard-tools, action: build-fleet-manifest }

  - type: rest
    namespace: shipyard-api
    resources:                              # alpha3 — keyed map
      voyage-manifest:
        path: /voyages/{voyageId}/manifest
        operations:                         # alpha3 — keyed map
          get-manifest:
            method: GET
            ref: manifest.build-fleet-manifest
            inputParameters:
              voyageId: { in: path, required: true }
            with: { voyageId: shipyard-api.voyageId }

🧭 What you learned: unified from/import for shared consumes, multi-step orchestration at scale, mixing call and lookup steps, the same aggregate exposed via MCP + Skill + REST.


Patterns to take away

Pattern When to reach for it
from: / import: shared consumes More than one capability uses the same API
aggregates + ref Same logic on more than one expose adapter
type: lookup steps Joining two API responses in-memory
semantics: Anything read-only, idempotent, or cacheable
Multi-adapter expose Mixed audience (AI agents + web app + partner)

Next steps