engineering 9 min read

Non-LLM operations in PAP: the glue agent primitives

PAP ships eight agents that will never make an inference call. Not because they're simple, but because they don't need one. The work is structural: typed input goes in, a different typed shape comes out, deterministically, every time.

PAP ships eight agents that will never make an inference call. Not because they're simple — the Trip Assembler merges flight, hotel, and activity reservations into a single typed itinerary, and the Type Router dispatches to downstream agents by inspecting a schema.org @type field. They skip the model because they don't need one. The work is structural: typed input goes in, a different typed shape comes out, deterministically, every time.

These are glue agents. They're the operations that live between the reasoning steps in an agentic pipeline, and they're a first-class primitive in PAP's agent registry.


What a glue agent looks like

Every agent in PAP, whether it calls an LLM, hits an HTTP endpoint, or runs a pure function, implements the same two-piece interface: AgentMeta declares what the agent is, and AgentExecutor::execute is what it does.

For a glue agent, AgentMeta tells the whole story:

AgentMeta {
    name: "Type Router",
    version: "0.1.0",
    provider: "PAP",
    action: "schema:ChooseAction",
    object_types: &["schema:Thing"],
    requires_disclosure: &[],
    returns: &["schema:ChooseAction"],
    configurable_properties: vec![],
    network_required: false,
}

Two fields stand out. requires_disclosure: &[] means this agent requests no principal data. It will never see a credential, a calendar entry, or a location, because it never asked for one. network_required: false means no outbound connection is made. These aren't flags you set to be polite; they're the fields the orchestrator reads when it builds the mandate. An agent that declares requires_disclosure: &[] gets a mandate with an empty disclosure scope by construction. There's nothing to leak because there's nothing granted.

The execution side is equally plain. Here's the complete implementation of TypeRouterExecutor:

fn execute(&self, query: &str) -> Result<Value, TransportError> {
    let input: Value = serde_json::from_str(query)?;

    // Form 1: explicit routing table + data
    if let (Some(routes), Some(data)) = (
        input.get("routes"),
        input.get("data")
    ) {
        let input_type = data
            .get("@type")
            .and_then(|v| v.as_str())
            .unwrap_or("schema:Thing");

        let selected = routes
            .get(input_type)
            .and_then(|v| v.as_str())
            .map(str::to_owned);

        return Ok(json!({
            "@type": "schema:ChooseAction",
            "selectedAgent": selected,
            "matched": selected.is_some(),
            "data": data,
        }));
    }

    // Form 2: bare object — report its type, no routing table
    let input_type = input
        .get("@type")
        .and_then(|v| v.as_str())
        .unwrap_or("schema:Thing");

    Ok(json!({
        "@type": "schema:ChooseAction",
        "inputType": input_type,
        "selectedAgent": null,
        "matched": false,
        "data": input,
    }))
}

No async. No network call. No prompt construction. The function reads a @type field, looks it up in a table, and returns a typed schema:ChooseAction the pipeline can act on. The entire reasoning budget for this step is a hash map lookup.

This is what it means for an operation to not need a model: not that it's simple, but that its inputs and outputs have known shapes and the transformation between them is a function, not a judgment.


The four patterns

PAP's glue agents cover four structural operations. Each maps to a schema.org action type, not by convention, but because these action types already exist in the vocabulary and describe exactly what the operation does.

Transform — schema:ConvertAction

A transform agent takes one typed shape and produces a different typed shape. No interpretation, no inference about meaning, just a deterministic mapping from source schema to target schema.

PAP ships three:

AgentInputOutput
JsonToMarkdownExecutorany schema:Thingschema:Text
ItemListToCsvExecutorschema:ItemListschema:Dataset
FlattenArrayExecutorschema:DataFeed or bare arrayschema:ItemList

The CSV converter is a good example of why this matters in practice. An upstream agent returns a schema:ItemList of flight options. A downstream agent needs a table. Without a glue agent, that conversion either goes through an LLM prompt ("format this as CSV") or gets hardcoded into the pipeline. With a glue agent, it's a typed contract: ItemList in, Dataset out, headers inferred from the first element's keys, missing fields emitted as empty strings. Deterministic, testable, zero inference cost.

Reduce — schema:CombineAction

A reduce agent merges multiple typed inputs into one. PAP's TripAssemblerExecutor takes an array of reservations (schema:FlightReservation, schema:LodgingReservation, schema:EventReservation) and assembles them into a single schema:Trip, sorted by date, with overlapping segments detected and flagged.

This is the operation most likely to get routed to a model unnecessarily. "Combine these three results" sounds like it needs judgment. It doesn't, not when the inputs are typed. A schema:Offer has a price field. A schema:AggregateOffer has a lowPrice, highPrice, and offerCount. The merge is a fold over known fields, not an interpretation of unstructured text.

Route — schema:ChooseAction

A route agent inspects a typed object and returns a dispatch directive: which downstream agent should handle it, based on the object's @type or field values. The routing table is part of the input:

{
  "routes": {
    "schema:FlightReservation": "Trip Assembler",
    "schema:LodgingReservation": "Trip Assembler",
    "schema:NewsArticle": "Article Summarizer"
  },
  "data": { "@type": "schema:FlightReservation", "reservationId": "AA123" }
}

Output:

{
  "@type": "schema:ChooseAction",
  "selectedAgent": "Trip Assembler",
  "inputType": "schema:FlightReservation",
  "matched": true,
  "data": { "..." }
}

The pipeline acts on selectedAgent. If matched is false, it falls through to a default. The routing logic is inspectable at any point. It's a JSON table, not a prompt, so you can read it, test it, and change it without touching inference.

Probe — schema:CheckAction

A probe agent validates a typed object before it proceeds. PAP ships two: SchemaValidatorExecutor checks structural integrity (@type present, at least one non-type field), and FieldPresenceExecutor checks that specific required fields exist and are non-null.

{
  "fields": ["price", "availability", "validThrough"],
  "data": { "@type": "schema:Offer", "price": 299, "availability": "InStock" }
}

Output:

{
  "@type": "schema:CheckAction",
  "present": ["price", "availability"],
  "missing": ["validThrough"],
  "all_present": false
}

A probe that returns all_present: false is a hard stop. The pipeline doesn't proceed to checkout with an offer missing its expiry date. This is the kind of validation that otherwise gets skipped, hardcoded into the consuming agent, or sent to a model to "check if this looks complete." A model that hallucinates validThrough into existence is strictly worse than a probe that fails closed.


Zero-disclosure by construction

The phrase "zero-disclosure" in PAP has a specific technical meaning. It's not a policy. It's a structural outcome of how mandates are built.

When the orchestrator evaluates an agent for a pipeline step, it reads requires_disclosure from AgentMeta and constructs the mandate's disclosure scope from that list. An LLM agent that needs the user's location and name declares requires_disclosure: &["schema:location", "schema:name"]. The orchestrator surfaces those fields to the principal for approval before the step runs, and the SD-JWT selective disclosure envelope reveals only those two properties.

A glue agent that declares requires_disclosure: &[] gets a mandate with an empty disclosure scope. The orchestrator doesn't ask the principal for approval over data fields because there are none to approve. There is no SD-JWT envelope to construct because there is nothing to disclose. The agent receives exactly what was passed in the pipeline step's typed payload and nothing else.

This means the zero-disclosure property doesn't depend on trusting the glue agent implementation. Even if the TypeRouterExecutor were somehow compromised, it has no path to principal data: no credential was ever included in its mandate, no disclosure scope was ever opened, no ambient context was passed. The isolation is enforced at the protocol layer before the agent code runs.

That's the distinction between policy-based privacy ("this agent promises not to use your data") and structural privacy ("this agent was never given your data"). PAP's glue agents are the latter. Evidence, not faith.


How registries advertise glue agents

Glue agents aren't special-cased in the registry. They advertise the same AgentAdvertisement as every other agent, signed by the provider's DID, including action, object_types, and returns, and they appear in the same federation index that syncs to connected clients.

What makes them useful in the intent index is exactly the properties that make them cheap to run. When Papillon receives a user's intent and queries the semantic index to find matching agents, it's building a candidate set from the advertised action and object_types fields across every agent in the index: local catalog agents, registry agents from federated peers, and glue agents. The routing and transform agents surface on schema:ChooseAction and schema:ConvertAction queries. The probe agents surface on schema:CheckAction.

This matters because pipelines can be assembled from agents that span multiple registries, with glue agents contributed by any peer that advertises them, without central coordination. A registry that specializes in travel data can advertise a TripAssemblerExecutor that understands its own reservation format. A client that has already routed to that registry's flight search agent can find the compatible reduce agent in the same index sweep, without knowing ahead of time that it exists.

The semantic routing in Papillon's intent index, built on ordvec's two-stage sign-probe with exact cosine rescore, is sophisticated enough to distinguish between a schema:CombineAction that merges trip reservations and one that merges product offers, because the object_types field carries that specificity. The vocabulary does the disambiguation work that would otherwise require another model call.

The result is that glue agents are discoverable the same way capabilities are discoverable: through the registry's advertised schema, not through hardcoded pipeline wiring. A pipeline assembled for one set of agents can pick up a better transform or a stricter probe from a newly-federated registry without any configuration change. The type contract is the interface; the registry is the directory; the intent index is the resolver. No coordinator required.


What this adds up to

An agentic pipeline built with PAP's glue primitives has a property that's hard to get otherwise: every junction between steps is a typed contract you can inspect statically, not a black box you have to trust ran correctly.

The reasoning steps, the ones that actually need a model, are bounded on both sides by typed shapes. The model receives a well-formed schema:Action and returns a well-formed schema:Thing. Before that result moves to the next step, a probe checks it. If it needs to change shape for the consuming agent, a transform handles it. If it needs to go to one of several downstream agents, a route dispatches it. None of those intermediate steps cost inference. None of them carry mandate scope beyond the payload they're explicitly handling.

The inference calls in the pipeline are exactly the ones that need inference. Everything else is a function.

Baur Software

AI governance and technical advisory firm based in San Diego, CA. We solve problems fast.

About us →

Get Started

Ready to put this into practice?

Let's discuss how these ideas apply to your organization.

Book a Demo

Related posts

engineering

Glue agents: the missing primitive in agentic pipelines

Every agent framework treats every step the same way: send it to an LLM. You end up paying inference cost and latency for work a fifty-line function would do faster, cheaper, and deterministically. That's not a tooling gap — it's a missing primitive.

· 7 min read