Skip to content

โ† Back to index

5. Functions to write (selector & task-evaluation)

Like Orcade's PQT/evaluator functions, these are not code inside Xchange. They are entries in the same external function registry (FUNCTION_REGISTRY_URL), referenced purely by function_id string, and resolved/invoked through the exact same client code Orcade uses: FunctionCaller (exchange-db/core/functions.py) โ†’ AgentFunctions (exchange-db/core/agents_functions/__init__.py), which supports the same three execution modes per FunctionEntry โ€” external (plain HTTP POST), stateful (long-lived deployment, cached per function_id for the process lifetime), and stateless (one-off registry-managed job). See Orcade's own functions doc, ยง4.1 for the full AIOSv1PolicyRule authoring contract (__init__(rule_id, settings, parameters) / eval(parameters, input_data, context)) โ€” it applies unchanged here, since it's the same registry and the same client code.

Xchange uses this mechanism for two different kinds of function, one per assignment mode:

5.1 Selector function (function mode)

Purpose: given a task and the full set of subjects in the exchange, pick exactly one subject to do the work.

When it's called: once per task, synchronously, inline inside POST /tasks, only for task_assignment_type: "function". See ยง4.2.

Input contract:

{
  "task_data": { "...": "the submitted task's task_data, verbatim" },
  "subjects": [ { "...": "full ExchangeSubjects.to_dict()" }, "... every subject in the exchange" ]
}

parameters from task_metadata.task_assignment.parameters is also forwarded via FunctionCaller.call(..., parameters=...) โ€” for a stateful or stateless function this arrives as both the parameters argument to eval() and merged into input_data["parameters"]; for an external function it is silently dropped (the HTTP POST body is input_data only, with no parameters key added) โ€” same caveat Orcade documents for its own evaluator calls.

Output contract:

Key Type Required Meaning
selected_subject_id string yes Must be truthy. If missing/falsy, TaskAssignmentService raises, POST /tasks returns 400, and the task is marked failed โ€” no subject is ever notified. There is no validation that this ID actually matches one of the subjects passed in subjects; an invalid ID simply fails later at the delegate-system call (_submit_and_save), marking the task failed at that point instead.

Example input:

{
  "task_data": {"instruction": "summarize this document", "doc_url": "https://storage.example.com/report.pdf"},
  "subjects": [
    {
      "subject_id": "agent-summarizer-01",
      "subject_metadata": {"owner": "team-nlp"},
      "task_evaluation_function": "",
      "subject_capabilities": {"max_doc_tokens": 50000}
    },
    {
      "subject_id": "agent-summarizer-02",
      "subject_metadata": {"owner": "team-nlp"},
      "task_evaluation_function": "",
      "subject_capabilities": {"max_doc_tokens": 200000}
    }
  ]
}

Example output:

{ "selected_subject_id": "agent-summarizer-02" }

Reference implementation (AIOSv1PolicyRule, picks the subject with the largest declared capacity, for a stateful or stateless function):

import logging


class AIOSv1PolicyRule:

    def __init__(self, rule_id, settings, parameters):
        self.rule_id = rule_id
        self.settings = settings
        self.parameters = parameters

    def eval(self, parameters, input_data, context):
        logging.info("selector input: {}".format(input_data))

        subjects = input_data["subjects"]
        best = max(subjects, key=lambda s: s.get("subject_capabilities", {}).get("max_doc_tokens", 0))

        return {"selected_subject_id": best["subject_id"]}

Alternative reference implementation (external HTTP mode, is_external=True):

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/")
def select():
    payload = request.get_json()
    subjects = payload["subjects"]

    best = max(subjects, key=lambda s: s.get("subject_capabilities", {}).get("max_doc_tokens", 0))

    return jsonify({"data": {"selected_subject_id": best["subject_id"]}})

5.2 Task evaluation function (open mode)

Purpose: ask a single subject whether it will accept a given task. Unlike the selector function, this is per-subject: it is stored on the subject itself (ExchangeSubjects.task_evaluation_function), not on the task.

When it's called: once per candidate subject, in sequence, entirely on a background thread, only for task_assignment_type: "open". Stops at the first "accepted" response โ€” later subjects in the list are never asked. Subjects with an empty task_evaluation_function are skipped and never called at all. See ยง4.3.

Input contract:

{
  "task_data": { "...": "the submitted task's task_data, verbatim" },
  "subject": { "...": "full ExchangeSubjects.to_dict() for just this one subject" }
}

No parameters are passed for this call (FunctionCaller.call() is invoked with no parameters= kwarg, so it defaults to None).

Output contract:

Key Type Required Meaning
status string yes (compared exactly) "accepted" โ†’ this subject wins; the task is submitted to it and evaluation stops. Anything else (including missing) โ†’ treated as a decline, next subject is tried.
(anything else) any no Ignored by Xchange today; free to add a reason field for your own logging.

Example input:

{
  "task_data": {"instruction": "summarize this document", "doc_url": "https://storage.example.com/report.pdf"},
  "subject": {
    "subject_id": "agent-summarizer-01",
    "subject_metadata": {"owner": "team-nlp"},
    "task_evaluation_function": "accepts-if-capacity:1.0-stable",
    "subject_capabilities": {"max_doc_tokens": 50000}
  }
}

Example output (accept):

{ "status": "accepted" }

Example output (reject):

{ "status": "rejected", "reason": "at capacity" }

Reference implementation (AIOSv1PolicyRule, accepts if the subject's own declared capacity covers the task, for a stateful or stateless function):

import logging


class AIOSv1PolicyRule:

    def __init__(self, rule_id, settings, parameters):
        self.rule_id = rule_id
        self.settings = settings
        self.parameters = parameters

    def eval(self, parameters, input_data, context):
        logging.info("task evaluation input: {}".format(input_data))

        subject = input_data["subject"]
        task_data = input_data["task_data"]

        capacity = subject.get("subject_capabilities", {}).get("max_doc_tokens", 0)
        required = task_data.get("estimated_tokens", 0)

        if required > capacity:
            return {"status": "rejected", "reason": f"requires {required} tokens, capacity is {capacity}"}

        return {"status": "accepted"}

Alternative reference implementation (external HTTP mode, is_external=True):

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/")
def evaluate():
    payload = request.get_json()
    subject = payload["subject"]
    task_data = payload["task_data"]

    capacity = subject.get("subject_capabilities", {}).get("max_doc_tokens", 0)
    required = task_data.get("estimated_tokens", 0)

    if required > capacity:
        return jsonify({"data": {"status": "rejected", "reason": f"requires {required} tokens, capacity is {capacity}"}})

    return jsonify({"data": {"status": "accepted"}})

5.3 Registering a function

Same as Orcade: whatever you write must already exist in the function registry under a function_id before it's referenced (as task_metadata.task_assignment.function_id for function mode, or as ExchangeSubjects.task_evaluation_function for open mode). AgentFunctions.add() is called automatically, lazily, the first time FunctionCaller.call() sees a given function_id, and raises ValueError if the registry has nothing registered under it โ€” for function mode this surfaces as the 400 on POST /tasks; for open mode it's caught per-subject and logged, treated as that subject declining (see ยง4.3). Registering the function itself happens against the function registry service directly, outside Xchange's own API surface โ€” identical to how it's done for Orcade.


โ† Previous: Assignment modes ยท Back to index ยท Next: Delegate system โ†’