블로그 • Open Responses
Open Responses가 AAIF에 들어온 뒤, 도구 호출을 다른 모델로 재생하기 전에 JSON의 type, arguments, status를 무엇을 먼저 보나?
2026년 10월 22일 Agentic AI Foundation은 Open Responses를 호스트 프로젝트로 받았다. 명세는 한 턴을 item으로 묶는다. message, function_call, function_call_output이 이식 가능한 핵심이고, type에 공급자 접두사가 있으면 그 구현 전용이다.
로그의 item을 다른 모델로 보내기 전에 로컬에서 세 칸을 본다. type에 콜론이 있으면 범용 function_call로 보지 않는다. 명세 예시의 arguments는 JSON 문자열이므로 먼저 파싱한 뒤 도구 JSON Schema와 맞춘다. status가 completed가 아니면 function_call_output을 붙이지 않는다.
어제의 Bedrock 구조화 출력 은 모델 본문 형태를 다룬다. Chat Completions의 arguments도 자주 문자열이지만 봉투가 다르다. AI Agent와 JSON Schema. schema 통과가 실행 허가는 아니다. 권한 결정. 이 글은 Open Responses item만 다룬다.
- 이식 가능한 type은 message, function_call, function_call_output이다.
- 콜론이 있는 type은 공급자 확장이다. 범용 도구 호출로 재생하지 않는다.
- arguments는 문자열이어야 한다. 파싱한 객체를 도구 schema로 region 같은 필드까지 검증한다.
- status가 in_progress 또는 incomplete이면 멈춘다. completed일 때만 function_call_output을 쓰고 call_id를 맞춘다.
먼저 이것만: AAIF가 받은 것은 공유 item 형태다. JSON 한 건이 여전히 한 공급자 전용일 수 있다. 아래는 type, arguments, status 순서로, 재생 전에 어느 칸이 막는지 본다.
10월 22일, 공유 item이 공개 거버넌스로
그날 AGNTCon과 MCPCon에서 AAIF는 프로젝트 넷을 발표했다. OpenAI가 기여한 Open Responses는 MCP, A2A와 나란히 Linux Foundation 거버넌스에 들어간다. 발표는 AAIF 발표. 같은 날 Anthropic Agent Skills, Hugging Face OpenEnv, 알리바바 OpenSandbox도 들어왔다. 이 글은 그 목록이나 스킬 형식을 다루지 않고 Open Responses item JSON만 다룬다.
같은 날 AWS 오픈소스 글은 Amazon Bedrock이 Responses API를 지원하고 명세에 참여한다고 썼다. 참고: AWS 오픈소스 글. 필드 의미는 Open Responses 명세 를 따른다. 비스트리밍 본문은 application/json이고 item은 type으로 구분한다. 로그에 function_call이 있을 때, 공급자를 바꾸거나 다음 날 재생하기 전에 어느 세 필드가 다음 요청을 막아야 하는지.
재생 가능한 function_call에 최소 어떤 필드가 있나?
다음 검사로 갈 수 있는 function_call은 최소한 type, id, name, call_id, status, arguments를 가진다. arguments 값은 문자열이고, 그 안에 도구 인자가 있다. 아래 예는 region이 apac이며 같은 schema로 다시 검증한다.
{
"type": "function_call",
"id": "fc_001",
"name": "get_latest_sales_report",
"call_id": "call_123",
"status": "completed",
"arguments": "{\"region\":\"apac\"}"
}
도구를 이쪽에서 실행한 뒤 다음 요청에 붙이는 것은 function_call_output이지, Chat Completions의 tool_calls 배열이 아니다. 명세의 이어쓰기 예는 같은 call_id와 output을 가진다. 서버가 결과를 item으로 돌려줄 때 id와 status가 붙는다.
{
"type": "function_call_output",
"call_id": "call_123",
"output": "sales report for apac"
}
type에 콜론이 있으면 왜 공급자를 바꿔 실행하면 안 되나?
명세는 구현이 자체 item을 더하게 두지만, type에는 공급자 slug 접두사가 필요하다. 예는 openai:web_search_call. 이 기록은 원래 세션에 두고 같은 공급자로 되돌릴 수 있다. 다른 Open Responses 서버로 이식된다고 보지 않는다. 로컬 규칙: 콜론이 있으면 namespaced로 표시하고 function_call_output을 만들지 않는다.
{
"type": "openai:web_search_call",
"id": "ws_001",
"status": "completed",
"action": { "type": "search", "query": "apac sales report" }
}
| 보이는 type | 다른 공급자로 재생 가능한가 | 로컬 동작 |
|---|---|---|
function_call | 다음 검사로 진행 | 이어서 arguments와 status를 본다. |
message | input에 넣을 수 있음 | 대화 텍스트로 보고 도구 결과로 보지 않는다. |
openai:web_search_call | 이식하지 않음 | 원래 공급자에 두거나 공급자 간 로그에서 분리한다. |
왜 arguments는 먼저 문자열이고 그다음 schema인가?
스트림 이벤트 response.function_call_arguments.delta는 객체가 끝나기 전에 텍스트를 붙인다. 그래서 저장된 arguments는 객체가 아니라 문자열이다. 객체를 직접 넣은 로그는 다음 파싱에서 실패하거나 다른 구현이 거부한다. 먼저 string인지 확인하고, json.loads 다음에 도구 JSON Schema를 적용한다. 아래 schema는 apac, emea, amer만 허용한다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["region"],
"properties": {
"region": {
"type": "string",
"enum": ["apac", "emea", "amer"]
}
}
}
아래 문자열은 파싱되지만 region이 global이라 enum에 없다. 게이트는 arguments_schema를 반환하고 도구는 실행하지 않는다.
{
"type": "function_call",
"id": "fc_009",
"name": "get_latest_sales_report",
"call_id": "call_129",
"status": "completed",
"arguments": "{\"region\":\"global\"}"
}
| 보이는 arguments | 게이트 결과 | 로컬 동작 |
|---|---|---|
| 내용의 region이 apac인 문자열 | ok | 도구를 실행한 뒤 function_call_output을 쓴다. |
| arguments 자체가 객체 | arguments_type | 거부. 문자열로 되돌린 뒤 파싱. |
| 파싱되지만 region이 global | arguments_schema | 실행하지 않는다. 검증 오류를 돌려주고 지역을 추측하지 않는다. |
status가 completed가 아니면 왜 도구 결과를 쓰지 않나?
명세는 item을 상태 기계로 둔다. in_progress는 아직 출력 중이다. incomplete는 종단 상태이고 그 응답의 마지막 item이어야 하며, response 자체도 incomplete여야 한다. completed 이후에는 그 item을 바꾸지 않는다. incomplete이면 arguments가 잘렸을 수 있어 절반짜리 JSON은 실행하지 않는다.
| status | function_call_output을 써도 되나 |
|---|---|
in_progress | 아니오. 스트림이 끝날 때까지 기다린다. |
incomplete | 아니오. 종단 상태이며 인자가 잘렸을 수 있다. |
completed | 예. 그다음 type에 콜론이 없는지, arguments가 schema를 통과하는지, call_id가 다음과 같은지 확인한다. |
| 그 밖의 값, failed 포함 | 아니오. 부수 효과를 실행하지 않는다. |
아래 게이트는 이 글의 샘플을 검사하는 코드와 같다. 콜론, function_call이 아님, status, arguments 타입, 파싱, schema 순으로 이유를 반환한다. ok일 때만 도구를 실행하고 function_call_output을 붙인다.
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"
JSONNote에서 이 세 칸을 어떻게 나눠 보나?
- 1arguments가 문자열인지 먼저 본다
JSON 포맷. item 전체를 붙인다. arguments가 한 줄 문자열이 아니라 객체로 펼쳐지면 저장 형태가 이미 어긋났고, 게이트는 arguments_type을 반환한다.
- 2이어서 풀어낸 인자를 검증
arguments 문자열을 푼 객체를 JSON Schema 에 붙인다. schema는 위의 region enum. global은 표시된다. 실행 전에 apac로 고치지 않는다.
- 3마지막으로 두 공급자의 item을 비교
JSON Diff. 왼쪽에는 원래 공급자 item, 오른쪽에는 재생할 item. type이 function_call에서 콜론 접두사로 바뀌거나 call_id가 맞지 않으면 다음 요청을 보내지 않는다.
자주 묻는 질문
Open Responses가 AAIF에 들어가면 같은 JSON을 Bedrock이나 다른 구현으로 바로 보낼 수 있나?
아니다. 이식되는 핵심은 message, function_call, function_call_output이다. 접두사 item이 다른 서버에서 재생된다고 보지 않는다. Bedrock의 Claude 구조화 출력은 다른 층이다. 모델 본문의 JSON Schema이지 이 item 봉투가 아니다.
arguments를 왜 객체로 저장하지 않나?
명세 예시는 arguments를 JSON 문자열로 쓴다. 스트림에서는 function_call_arguments.delta로 이어 붙이며, status가 completed가 되기 전에는 문자열이 아직 불완전할 수 있다. 일찍 파싱해서 실행하지 않는다.
status가 incomplete일 때 도구를 먼저 실행해도 되나?
하지 않는다. incomplete는 종단 상태다. 명세는 그것이 해당 응답의 마지막 item이고 response 자체도 incomplete일 것을 요구한다. 잘린 arguments는 대개 schema를 통과하지 못한다. 우연히 통과해도 완전한 호출로 실행하지 않는다.
allowed_tools가 도구 이름을 제한하는데도 로컬 schema가 필요한가?
필요하다. allowed_tools는 이 요청에서 실제로 실행할 수 있는 도구 이름을 제한하고, 도구 정의는 tools에 전부 둘 수 있다. arguments 안의 region이 enum에 있는지는 보지 않는다. 이름이 허용돼도 parse와 schema는 남는다.
Chat Completions의 tool_calls와 Open Responses item을 같은 배열에 넣을 수 있나?
넣지 않는다. Open Responses는 type, call_id, 문자열 arguments를 쓴다. Chat Completions는 tool_calls 배열을 쓴다. 한 로그에 섞으면 재생 때 잘못된 봉투가 다음 공급자로 간다. Diff로 두 문서를 나눠 비교한다.
정리
Open Responses가 AAIF에 들어온 뒤 공급자 간에 공유되는 것은 item 형태이지, 모든 로그 줄은 아니다.
결론: type에 콜론이 있으면 범용 function_call로 보지 않는다. arguments가 문자열인지 확인한 뒤 도구 schema로 검증한다. status가 completed가 아니면 function_call_output을 쓰지 않는다.
다음: 포맷으로 arguments가 문자열인지 보고, Schema로 푼 인자를 검증하고, Diff로 두 item을 비교한다.