Blog • Open Responses

After Open Responses joins the AAIF, which type, arguments, and status checks come before you replay a tool-call item to another model?

On October 22, 2026 the Agentic AI Foundation made Open Responses a hosted project. The spec packs one model turn into items: message, function_call, and function_call_output are the portable core; a type with a provider prefix belongs to that implementation.

Before you send a logged item to another model, check three fields locally. A colon in type means it is not a portable function_call. In the spec examples arguments is a JSON string: parse it, then check the tool JSON Schema. If status is not completed, do not append function_call_output.

Yesterday's Bedrock structured outputs covers the shape of model text. Chat Completions often stores arguments as a string too, in a different envelope—see AI Agent and JSON Schema. A schema pass still does not mean you may run the tool—see the permission decision. This article covers only the Open Responses item.

Keep this in mind: AAIF accepted a shared item shape. One JSON record can still belong to a single provider. The sections below follow type, then arguments, then status, and show which field stops a replay.

On October 22 the shared item moved under open governance

At AGNTCon and MCPCon that day, AAIF announced four new projects. Open Responses, contributed by OpenAI, sits beside MCP and A2A under Linux Foundation governance. The announcement is at the AAIF announcement. The same day also brought Anthropic Agent Skills, Hugging Face OpenEnv, and Alibaba OpenSandbox. This article does not cover those catalogs or the skill format. It only covers Open Responses item JSON.

The same day, an AWS open-source post said Amazon Bedrock supports the Responses API and is participating in the spec. See the AWS open-source post. Field meanings follow the Open Responses specification. Non-streaming bodies are application/json, and items are distinguished by type. The practical question: a function_call is already in the log; before you switch providers or replay it the next day, which three fields should stop the next request.

Which fields does a replayable function_call need?

A function_call that can move to the next check carries at least type, id, name, call_id, status, and arguments. The arguments value is a string; the tool parameters live inside that string. The sample below uses region apac and is checked again with the same schema.

A function_call that can continue. arguments is a string.
{
  "type": "function_call",
  "id": "fc_001",
  "name": "get_latest_sales_report",
  "call_id": "call_123",
  "status": "completed",
  "arguments": "{\"region\":\"apac\"}"
}

After the tool runs on your side, the next request appends function_call_output, not a Chat Completions tool_calls array. The spec continuation example keeps the same call_id and adds output. If the server later returns that result as an item, id and status are filled in then.

function_call_output on the next request, with the same call_id.
{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "sales report for apac"
}

Why does a colon in type block a cross-provider run?

The spec lets an implementation add its own items, but type must carry a provider slug prefix, for example openai:web_search_call. That record can stay in the original session and go back to the same provider. The spec says not to assume it is portable to another Open Responses server. Local rule: a colon marks the item namespaced, and you do not emit function_call_output.

A provider-prefixed item. Do not run it again as get_latest_sales_report.
{
  "type": "openai:web_search_call",
  "id": "ws_001",
  "status": "completed",
  "action": { "type": "search", "query": "apac sales report" }
}
type you seeReplay on another provider?Local action
function_callContinue to the next checkCheck arguments and status next.
messageCan go into inputTreat it as conversation text, not a tool result.
openai:web_search_callNot portableKeep it with the original provider, or split it out of the cross-provider log.

Why must arguments be a string before the schema check?

The streaming event response.function_call_arguments.delta appends text before the object is finished, so stored arguments is a string, not an object. A log that puts an object in arguments fails the next parse or is rejected by another implementation. Confirm the value is a string, then json.loads, then the tool JSON Schema. The schema below allows only apac, emea, and amer.

Tool parameter schema. region must be apac, emea, or amer.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["region"],
  "properties": {
    "region": {
      "type": "string",
      "enum": ["apac", "emea", "amer"]
    }
  }
}

The string below parses, but region is global, which is not in the enum. The gate should return arguments_schema, and the tool should not run.

Parse succeeds, schema fails. Do not guess a legal region.
{
  "type": "function_call",
  "id": "fc_009",
  "name": "get_latest_sales_report",
  "call_id": "call_129",
  "status": "completed",
  "arguments": "{\"region\":\"global\"}"
}
arguments you seeGate resultLocal action
A string whose object has region apacokRun the tool, then write function_call_output.
arguments is an object, not a stringarguments_typeReject. Turn it back into a string before parse.
The string parses, but region is globalarguments_schemaDo not run. Write the validation error back; do not guess a region.

Why skip the tool result when status is not completed?

The spec treats each item as a state machine. in_progress means output is still arriving. incomplete is terminal, must be the last item in that response, and the response itself must be incomplete. After completed, the item no longer changes. On incomplete, arguments may be cut off; a partial JSON value should not run.

statusWrite function_call_output?
in_progressNo. Wait until the stream finishes.
incompleteNo. Terminal, and the arguments may be truncated.
completedYes. Then confirm type has no colon, arguments passes the schema, and call_id matches the next item.
Any other value, including failedNo. Do not run a side effect.

The gate below is the same code that checks this article's samples. It returns a reason in order: colon, not a function_call, status, arguments type, parse, then schema. Only ok may run the tool and append function_call_output.

Local gate before replay. Anything other than ok does not run the tool.
def actual_type(data: object) -> str:
    if data is None:
        return "null"
    if isinstance(data, bool):
        return "boolean"
    if isinstance(data, list):
        return "array"
    if isinstance(data, int):
        return "integer"
    if isinstance(data, float):
        return "number"
    if isinstance(data, str):
        return "string"
    if isinstance(data, dict):
        return "object"
    raise TypeError(type(data))

def validate(data: object, schema: dict) -> list[str]:
    errors: list[str] = []
    if not isinstance(schema, dict):
        return errors
    types = schema.get("type")
    if isinstance(types, str):
        types = [types]
    actual = actual_type(data)
    if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
        errors.append("type")
    if "enum" in schema and not any(value == data for value in schema["enum"]):
        errors.append("enum")
    if isinstance(data, dict) and schema.get("properties"):
        for key in schema.get("required") or []:
            if key not in data:
                errors.append(f"required:{key}")
        if schema.get("additionalProperties") is False:
            for key in data:
                if key not in schema["properties"]:
                    errors.append(f"additional:{key}")
        for key, sub in schema["properties"].items():
            if key in data:
                errors.extend(validate(data[key], sub))
    if isinstance(data, list) and schema.get("items"):
        for item in data:
            errors.extend(validate(item, schema["items"]))
    return errors

def replay_gate(item: dict, schema: dict) -> str:
    item_type = item.get("type")
    if not isinstance(item_type, str) or ":" in item_type:
        return "namespaced"
    if item_type != "function_call":
        return "not_function_call"
    if item.get("status") != "completed":
        return "status"
    arguments = item.get("arguments")
    if not isinstance(arguments, str):
        return "arguments_type"
    try:
        parsed = json.loads(arguments)
    except json.JSONDecodeError:
        return "arguments_parse"
    if validate(parsed, schema):
        return "arguments_schema"
    return "ok"

How do you inspect the three fields in JSONNote?

  1. 1
    See whether arguments is a string

    Open JSON Format. Paste the whole item. If arguments expands into an object instead of one string, the stored shape is already wrong and the gate returns arguments_type.

  2. 2
    Then validate the parsed parameters

    Paste the object parsed from the arguments string into JSON Schema. Use the region enum above as the schema. global is flagged; do not rewrite it to apac before the tool runs.

  3. 3
    Then diff the two providers' items

    Open JSON Diff. Put the original provider item on the left and the item you plan to replay on the right. If type changes from function_call to a colon-prefixed extension, or call_id does not match, do not send the next request.

Common questions

Once Open Responses is in the AAIF, can the same JSON go straight to Bedrock or another implementation?

No. The portable core is message, function_call, and function_call_output. Do not assume a provider-prefixed item replays on another server. Claude structured outputs on Bedrock constrain a different layer: JSON Schema for model text, not this item envelope.

Why is arguments not stored as an object?

The spec examples store arguments as a JSON string. Streaming appends it with function_call_arguments.delta, and until status is completed that string may still be incomplete. Do not parse early and run the tool.

If status is incomplete, can I run the tool anyway?

No. incomplete is terminal. The spec requires it to be the last item of that response, and the response itself must be incomplete. A partial arguments value usually fails the schema; even if it happens to pass, do not treat it as a finished call.

If allowed_tools already limits tool names, do I still need a local schema?

Yes. allowed_tools limits which tool names may actually run on this request; the full tool list can still sit in tools. It does not check whether region inside arguments is in the enum. An allowed name still needs parse plus schema.

Can Chat Completions tool_calls sit in the same array as Open Responses items?

No. Open Responses uses type, call_id, and a string arguments. Chat Completions uses a tool_calls array. Mixed into one log, a replay sends the wrong envelope to the next provider. Diff the two documents separately.

Summary

After Open Responses joined the AAIF, providers can share the item shape. They do not share every log line.

Decision: a colon in type means it is not a portable function_call; confirm arguments is a string, then validate it with the tool schema; if status is not completed, do not write function_call_output.

Next: format the item to see if arguments is a string, validate the parsed parameters with Schema, and diff the two providers' items.

Back to the blog