Blog

Compose AI Context: One Tool Call, Three Upstream Sources

Kin Lane ·May 7, 2026
Table of contents

I am still watching the same pattern play out every time a team tries to get an agent to answer a real business question. The agent calls one tool, then another, then a third, then tries to stitch the three responses together in a prompt. Sometimes it works. Often it does not. Always it is expensive.

This is where the fifth Naftiko Framework use case earns its keep. Compose AI context. Combine data from multiple APIs into one capability to deliver richer, task-ready context to AI clients.

Fifth post of nine. Same three-dimensional lens I have been using — technology, business, politics.

The problem is not the APIs. It is the orchestration surface.

A classic example. A support agent wants to answer “how healthy is this customer?” That question lives across at least three systems — billing (are they paid up?), CRM (who are they, what tier?), and support (how many open tickets, what severity?).

In a glue-code world you give the agent three tools. get-billing-status. get-crm-profile. list-open-tickets. The agent has to know to call all three. It has to pass the customer ID through each one. It has to reconcile the results in its own head and then try to produce a coherent answer.

Every one of those steps is a place for the model to get it wrong. Wrong ID propagated. Missing call. Confabulated join. And every one of those steps is a round trip the user is paying for — in latency and in tokens.

The question is not “how do we give the agent better tools?” The question is “why is the agent doing orchestration at all?”

Steps belong in the capability, not in the prompt

The Naftiko capability spec has a steps construct for exactly this. Inside an exposed tool you declare an ordered list of steps — call a consumed operation, lookup across previously fetched data — and wire their outputs together with with (input injection) and mappings (JSONPath bridging from prior step outputs).

The agent sees one tool. One input. One composed output. The orchestration is in Git, reviewed, linted, and owned by the platform team.

Here is the shape of a customer-health capability that pulls from three sources and returns one composed output model.

naftiko: "1.0.0-alpha1"

info:
  title: Customer Health
  description: "Composed customer health view across billing, CRM, and support"

capability:
  consumes:
    - namespace: billing
      type: http
      baseUri: "https://api.billing.internal"
      authentication:
        type: bearer
        token: ""
      resources:
        - name: accounts
          path: "/v1/accounts/{accountId}"
          operations:
            - name: get-billing-status
              method: GET
              inputParameters:
                - name: accountId
                  in: path
                  type: string
                  required: true

    - namespace: crm
      type: http
      baseUri: "https://api.crm.internal"
      authentication:
        type: bearer
        token: ""
      resources:
        - name: customers
          path: "/v2/customers/{customerId}"
          operations:
            - name: get-crm-profile
              method: GET
              inputParameters:
                - name: customerId
                  in: path
                  type: string
                  required: true

    - namespace: support
      type: http
      baseUri: "https://api.support.internal"
      authentication:
        type: bearer
        token: ""
      resources:
        - name: tickets
          path: "/v1/tickets"
          operations:
            - name: list-open-tickets
              method: GET
              inputParameters:
                - name: accountId
                  in: query
                  type: string
                  required: true

  exposes:
    - type: mcp
      namespace: customer-health
      tools:
        - name: get-customer-health
          description: "Composed customer health across billing, CRM, and open support tickets"
          hints:
            readOnly: true
            idempotent: true
          inputParameters:
            - name: customerId
              type: string
              required: true
          steps:
            - name: profile
              call: crm.get-crm-profile
              with:
                customerId: ""

            - name: billing
              call: billing.get-billing-status
              with:
                accountId:
                  mapping: "$.steps.profile.accountId"

            - name: tickets
              call: support.list-open-tickets
              with:
                accountId:
                  mapping: "$.steps.profile.accountId"

          outputParameters:
            - name: customerId
              type: string
              mapping: "$.inputParameters.customerId"
            - name: name
              type: string
              mapping: "$.steps.profile.name"
            - name: tier
              type: string
              mapping: "$.steps.profile.tier"
            - name: billing
              type: object
              properties:
                - name: status
                  type: string
                  mapping: "$.steps.billing.status"
                - name: balance
                  type: number
                  mapping: "$.steps.billing.balance"
                - name: daysPastDue
                  type: integer
                  mapping: "$.steps.billing.daysPastDue"
            - name: openTickets
              type: array
              mapping: "$.steps.tickets.tickets"
              items:
                type: object
                properties:
                  - name: ticketId
                    type: string
                    mapping: "$.id"
                  - name: severity
                    type: string
                    mapping: "$.severity"
                  - name: openedAt
                    type: string
                    mapping: "$.openedAt"
            - name: composedAt
              type: string
              const: "runtime"

One tool. Three consumes. Three ordered steps. One composed output. The agent asks once, the capability orchestrates, the model gets a clean object.

Three dimensions, one composed view

Technology. This is where orchestration stops being a prompt-engineering problem and starts being a declared contract. The with block feeds step inputs — either literal values or JSONPath expressions that point at previous step outputs. The mappings under each output parameter pull fields from any prior step by name. The engine manages the call graph. The agent does not know, and should not need to know, that three systems are involved.

Business. The conversation with leadership changes when the composed view is a file. Someone can read get-customer-health.yaml and see every upstream system that contributes to a “customer health” answer. Compliance knows which systems are touched. Data teams know which fields are exposed. Nothing is hidden inside a prompt. Nothing is hidden inside bespoke adapter code.

Politics. This is the part most teams are not ready for. When orchestration lives in capabilities, the platform team owns the question “what does customer health mean?” Not the agent team. Not whichever product team shipped their own version last quarter. The composed output becomes a shared definition, version-controlled and reviewable. The political win is not technical — it is that a cross-system concept finally has a home.

Features that matter for this one

From the wiki for use case 5:

  • Multiple consumed adapters with unique namespaces, each with its own auth, baseUri, and operations
  • Ordered steps with call (invoke a consumed operation) and lookup (in-memory join) kinds
  • Step output bridging via with for input injection and JSONPath mappings for cross-step data flow
  • Composed output model with named parameters drawing from any step
  • Nested objects and typed arrays in the composed output
  • Const values for computed or runtime-injected fields

These are the building blocks for a capability that replaces a chain of prompt-orchestrated tool calls with one reviewed, lintable file.

Where I am using this

Every partner capability I have been writing lately runs into this. An integration against a single system is easy. The interesting capabilities — the ones that actually answer a business question — almost always cross two or three systems. Customer health. Order status that has to hit the warehouse. Compliance checks that touch identity and payment at once.

I used to ship those as multiple tools and hope the agent stitched them together. I do not do that anymore. If a single business question spans multiple systems, the spec composes across them.

Next

Next post in the series is Rightsize a set of microservices. Same lens. What it looks like technically. What it changes for the platform team. And what it quietly shifts politically inside the organization.

Still walking the list.