Programmatic Tool Calling

Programmatic Tool Calling (프로그래매틱 도구 호출)

프로그래매틱 도구 호출(Programmatic Tool Calling)은 모델이 도구를 조율하는 JavaScript를 작성·실행하게 해줘요. 프로그램은 도구를 병렬로 호출하고, 루프와 조건을 사용하며, 중간 결과를 호스티드 런타임에 보관할 수 있어요. 이는 작업에 관련 도구 호출 시퀀스가 필요하거나, 결과를 반환하기 전에 많은 양의 도구 출력을 처리해야 할 때 유용해요.

Responses API에서 여러분의 애플리케이션은 프로그래매틱 도구 호출을 사용 가능하게 할지, 그리고 모델이 직접 호출하거나 프로그램에서 호출하거나 둘 다 가능하게 할 적격 도구들을 결정해요. 클라이언트가 소유한 도구 호출은 계속 실행돼요. Agents API는 프로그래매틱 도구 호출을 기본적으로 활성화하고 에이전트 루프를 대신 관리해줘요.

프로그래매틱 도구 호출을 켜기 전에 모델 페이지를 확인하세요.

출처: 문서

본문

런타임 환경 이해하기

OpenAI는 생성된 각 프로그램을 깨끗하고 격리된 V8 런타임에서 실행해요. 이 런타임은 최상위 await가 있는 JavaScript를 지원하지만, Node.js, 패키지 설치, 직접 네트워크 접근, 일반 목적 파일시스템, 서브프로세스 실행, 콘솔, 그리고 프로그램 실행 간 영속적 JavaScript 상태는 제공하지 않아요. 프로그램은 요청에서 활성화된 도구를 통해서만 외부 시스템과 상호작용할 수 있고, text(...) 또는 image(...)로 출력을 내보낼 수 있어요.

Responses API 요청에서 프로그래매틱 도구 호출은 영속 코드 실행 컨테이너 없이도 ZDR(Zero Data Retention) 워크플로를 지원해요. ZDR은 조직 또는 프로젝트 수준에서 활성화되어야 해요. store: false를 설정하면 무상태(stateless) 연속은 가능하지만 그 자체로 ZDR을 켜지는 않아요. 적격성과 보존은 모델·도구·타사 서비스를 포함한 전체 요청에 따라 달라져요. 데이터 통제를 참고하세요.

프로그래매틱 도구 호출 사용 시점 고르기

제어 흐름이 예측 가능하고 코드가 더 작은 구조화 결과를 반환할 수 있는 단계라면 프로그래매틱 도구 호출을 사용하세요. 한 번의 호출로 충분하거나, 각 결과가 새로운 모델 판단을 요구하거나, 승인이나 인용·네이티브 아티팩트 보존이 필요한 작업이라면 직접 도구 호출을 사용하세요.

작업 형태 권장 모드
단일 조회 또는 단일 액션 직접 도구 호출을 사용하세요.
코드가 필터·조인·순위·중복 제거·집계·검증할 수 있는 여러 결과 프로그램이 더 작은 구조화 결과를 반환할 수 있다면 프로그래매틱 도구 호출을 사용하세요.
데이터 흐름이 예측 가능한 의존 호출 코드가 이후 인자를 유도할 수 있고 한도·실패 동작이 명시적이라면 프로그래매틱 도구 호출을 사용하세요.
적응형 검색 또는 의미 평가 각 결과가 모델의 다음 결정에 영향을 줘야 한다면 직접 도구 호출을 사용하세요.
쓰기 또는 승인 민감 액션 명확한 승인 경계를 보존하려면 기본적으로 직접 도구 호출을 사용하세요.
최종 인용 또는 네이티브 아티팩트 검증 프로그램이 네이티브 출력을 보존하고 필요한 모든 항목을 검증하지 않는 한 직접 도구 호출을 사용하세요.

프로그래매틱 도구 호출 구성

Responses API에서는 요청에 programmatic_tool_calling 호스티드 도구를 추가하세요. 그런 다음 프로그램이 호출할 수 있는 각 적격 도구에 allowed_callers를 설정하세요.

프로그래매틱 도구 호출 활성화

[
  {
    "type": "function",
    "name": "get_inventory",
    "description": "Return an object with sku (string) and available_units (number).",
    "parameters": {
      "type": "object",
      "properties": {
        "sku": { "type": "string" }
      },
      "required": ["sku"],
      "additionalProperties": false
    },
    "output_schema": {
      "type": "object",
      "properties": {
        "sku": { "type": "string" },
        "available_units": { "type": "number" }
      },
      "required": ["sku", "available_units"],
      "additionalProperties": false
    },
    "allowed_callers": ["programmatic"]
  },
  {
    "type": "programmatic_tool_calling"
  }
]

allowed_callers는 모델이 도구를 어떻게 호출할 수 있는지 제어해요.

값 동작
생략 또는 ["direct"] 모델이 도구를 직접 호출할 수 있어요.
["programmatic"] program 항목 안의 코드만 도구를 호출할 수 있어요.
["direct", "programmatic"] 모델이 도구를 직접 또는 프로그램에서 호출할 수 있어요.

parameters는 함수 인자를 설명해요. 함수가 예측 가능한 구조화 데이터를 반환할 때 output_schema는 function_call_output.output 문자열에 인코딩된 JSON 객체를 설명해요. 생성된 JavaScript가 반환된 필드를 신뢰성 있게 사용할 수 있도록 둘 다 정의하세요.

지원되는 도구

다음 도구 타입이 allowed_callers: ["programmatic"]를 지원해요.

  • function 및 custom
  • mcp
  • apply_patch
  • 로컬 및 호스티드 shell
  • code_interpreter

MCP 도구의 경우, 도구의 require_approval 정책이 여러분이 호출을 승인할 때까지 프로그램을 일시 중지할 수 있어요.

OpenAI 호스티드 도구의 경우, 프로그램에서 활성화하기 전에 도구의 데이터 보존·보안 지침을 검토하세요.

도구 검색과 결합

도구 검색은 생성된 JavaScript 내부가 아니라 최상위 Responses API 도구로 실행돼요. defer_loading: true인 function·custom·MCP 도구는 처음에는 프로그램이 사용할 수 없어요. 모델이 일치하는 도구를 로드한 후에는, 그 allowed_callers에 "programmatic"이 포함된 경우 이후 프로그램이 tools.*를 통해 호출할 수 있어요. 이미 실행 중인 프로그램은 도구 검색을 호출할 수 없으므로, 모델은 필요한 도구가 있는 프로그램을 시작하기 전에 지연 도구를 로드해야 해요.

두 모드를 모두 사용할 수 있을 때 라우팅 안내

애플리케이션이 모델이 함수를 직접 또는 프로그램에서 호출하게 하려면, 각 라우트를 특정 워크플로 단계에 할당하세요. "프로그래매틱 도구 호출을 효율적으로 사용해" 같은 일반적 지시는 의도된 경계를 식별하지 못해요. 예를 들어:

<tool_orchestration>
Use Programmatic Tool Calling for [bounded stage] using only [eligible tools].
Run independent calls concurrently when safe. Use only documented tool input
and output fields.

Process and reduce the intermediate results, then emit exactly [program result shape],
including the evidence needed for the final answer.

Stop when [condition] is met. Retry transient failures at most [R] times.
Do not repeat completed calls or perform side-effecting actions. If a required
result is still missing, return a clear structured failure.

Use direct tool calls for [semantic judgment, approval, or final validation].
</tool_orchestration>

이 템플릿 사용 예시는 다음과 같아요.

<tool_orchestration>
Use Programmatic Tool Calling to compare inventory with demand for sku_123
using only get_inventory and get_demand. Run both calls concurrently. Use
only documented tool input and output fields.

Process and reduce the intermediate results, then emit exactly one JSON object
with sku, available_units, requested_units, and shortage_units, where
shortage_units is max(requested_units - available_units, 0). Include
available_units and requested_units as evidence for the calculation.

Stop when both tool results contain the required fields. Retry transient
failures at most 1 time. Do not repeat completed calls or perform
side-effecting actions. If a required result is still missing, return a clear
structured failure.

Use direct tool calls only for approval before any inventory-changing action.
</tool_orchestration>

두 모드가 모두 필요한 워크플로라면 핸드오프(handoff)를 하나 정의하고 라우트를 바꾸거나 작업을 반복하지 마세요. 안전한 대체 경로(fallback)가 있다면 한 번 정의하고 재시도 횟수를 제한하세요.

프로그램 응답 항목 이해하기

각 API 호출은 여전히 표준 Responses API 객체를 반환해요. 프로그래매틱 도구 호출은 별도의 응답 봉투(envelope)를 도입하지 않아요. 모델이 프로그래매틱 도구 호출을 사용할 때 응답의 output 배열에는 다음이 포함될 수 있어요.

  • 생성된 JavaScript, call_id, 그리고 프로그램을 재개·재생하는 데 쓰는 불투명한(opaque) fingerprint를 담은 program 항목
  • 프로그램이 만든 function_call 항목. 이 항목은 함수 결과를 반환하는 데 애플리케이션이 사용하는 자체 call_id를 가지며, caller.caller_id가 프로그램의 call_id와 일치해요.
  • 프로그램의 최종 결과와 상태를 담은 program_output 항목. call_id가 프로그램의 call_id와 일치하고, status는 completed 또는 incomplete예요.

이들은 response.output의 별도 최상위 항목이며, caller 필드가 실행 관계를 기록해요.

예를 들어, 애플리케이션이 get_inventory와 get_demand를 실행하는 동안 프로그램이 일시 중지될 수 있어요.

프로그램과 중첩 함수 호출

[
  {
    "type": "program",
    "id": "prog_123",
    "call_id": "call_prog_123",
    "code": "const [stock, demand] = await Promise.all([tools.get_inventory({ sku: 'sku_123' }), tools.get_demand({ sku: 'sku_123' })]); text(JSON.stringify({ sku: stock.sku, available_units: stock.available_units, requested_units: demand.requested_units, shortage_units: Math.max(demand.requested_units - stock.available_units, 0) }));",
    "fingerprint": "opaque_replay_state"
  },
  {
    "type": "function_call",
    "id": "fc_123",
    "call_id": "call_inventory_123",
    "name": "get_inventory",
    "arguments": "{\\\"sku\\\":\\\"sku_123\\\"}",
    "caller": {
      "type": "program",
      "caller_id": "call_prog_123"
    }
  },
  {
    "type": "function_call",
    "id": "fc_456",
    "call_id": "call_demand_123",
    "name": "get_demand",
    "arguments": "{\\\"sku\\\":\\\"sku_123\\\"}",
    "caller": {
      "type": "program",
      "caller_id": "call_prog_123"
    }
  }
]

이 예시는 response.output에서 관련 항목만 보여주고 주변 표준 Responses 객체는 생략했어요. 애플리케이션이 중첩 함수 결과를 반환한 후, 이후 응답에 완전한 program_output 항목이 포함될 수 있어요.

프로그램 출력

{
  "type": "program_output",
  "id": "prog_out_123",
  "call_id": "call_prog_123",
  "result": "{\\\"sku\\\":\\\"sku_123\\\",\\\"available_units\\\":42,\\\"requested_units\\\":31,\\\"shortage_units\\\":0}",
  "status": "completed"
}

program_output.result의 JSON 문자열은 여러분의 지시에 따른 프로그램 결과 형태를 따르고, 주변 program_output 항목은 위에 보인 API 계약을 따르며, 이는 별개의 계약이에요. 최종 message는 프로그램 출력과 함께 도착하거나 이후 응답으로 도착할 수 있으므로, 그 메시지를 받을 때까지 계속하세요.

OpenAI는 모델이 생성한 JavaScript를 호스티드 런타임에서 실행해요. 애플리케이션은 반환된 클라이언트 소유 함수 호출을 실행할 뿐, 생성된 JavaScript는 실행하지 않아요.

함수 결과를 function_call_output으로 반환하세요. caller를 변경하지 않고 함수 호출에서 그대로 복사하세요. 서비스는 이 값을 사용해 올바른 프로그램을 재개해요.

클라이언트 소유 함수 호출 후 계속하기

프로그램은 클라이언트 소유 도구에 도달하면서 두 번 이상 일시 중지될 수 있어요. 응답에 최종 어시스턴트 메시지가 포함될 때까지 계속하세요.

  1. 호스티드 도구와 프로그래매틱 호출을 허용하는 함수를 담아 요청을 보내요.
  2. 반환된 모든 클라이언트 소유 함수 호출을 실행해요.
  3. 원래 call_id와 caller를 유지한 채 각 함수 결과를 반환해요.
  4. 계속하기 전에 미완료(incomplete) 응답을 처리해요.
  5. 응답에 대기 중인 function_call 항목도, 최종 message 항목도 없다면 그 응답에서 계속해요. store: false에서는 출력 항목을 재생하고, 저장된 응답이라면 previous_response_id를 사용해요.
  6. 응답에 최종 message 항목이 포함되면 멈춰요. response.output_text 또는 메시지의 refusal 콘텐츠를 읽어요.

다음 예시는 store: false를 사용하고 모든 응답 항목을 보존하며 각 함수 결과를 프로그램에 반환해요.

프로그래매틱 도구 호출 루프 실행

import OpenAI from "openai";
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";

const client = new OpenAI();

const implementations = {
  get_inventory: async ({ sku }) => ({ sku, available_units: 42 }),
  get_demand: async ({ sku }) => ({ sku, requested_units: 31 }),
};

const tools = [
  {
    type: "function",
    name: "get_inventory",
    description:
      "Return an object with sku (string) and available_units (number).",
    parameters: {
      type: "object",
      properties: { sku: { type: "string" } },
      required: ["sku"],
      additionalProperties: false,
    },
    output_schema: {
      type: "object",
      properties: {
        sku: { type: "string" },
        available_units: { type: "number" },
      },
      required: ["sku", "available_units"],
      additionalProperties: false,
    },
    allowed_callers: ["programmatic"],
    strict: true,
  },
  {
    type: "function",
    name: "get_demand",
    description:
      "Return an object with sku (string) and requested_units (number).",
    parameters: {
      type: "object",
      properties: { sku: { type: "string" } },
      required: ["sku"],
      additionalProperties: false,
    },
    output_schema: {
      type: "object",
      properties: {
        sku: { type: "string" },
        requested_units: { type: "number" },
      },
      required: ["sku", "requested_units"],
      additionalProperties: false,
    },
    allowed_callers: ["programmatic"],
    strict: true,
  },
  { type: "programmatic_tool_calling" },
];

const input = [
  {
    role: "user",
    content: "Compare inventory with demand for sku_123.",
  },
];

while (true) {
  const response = await client.responses.create({
    model: "YOUR_MODEL_ID",
    store: false,
    input,
    tools,
  });

  if (response.status !== "completed") {
    throw new Error(`Response ended with status ${response.status}`);
  }

  // Preserve replayable output, including program and reasoning items.
  input.push(...toResponseInputItems(response.output));

  const calls = response.output.filter((item) => item.type === "function_call");

  if (calls.length === 0) {
    const message = response.output.find((item) => item.type === "message");
    if (message) {
      const refusal = message.content.find((part) => part.type === "refusal");
      console.log(response.output_text || refusal?.refusal || "");
      break;
    }
    continue;
  }

  const outputs = await Promise.all(
    calls.map(async (call) => {
      const run = implementations[call.name];
      if (!run) throw new Error(`Unknown tool: ${call.name}`);

      const result = await run(JSON.parse(call.arguments));
      return {
        type: "function_call_output",
        call_id: call.call_id,
        output: JSON.stringify(result),
        // Preserve caller so the runtime can resume the correct program.
        caller: call.caller,
      };
    })
  );

  input.push(...outputs);
}
import json

from openai import OpenAI

client = OpenAI()
model = "gpt-6-astra"


def get_inventory(sku):
    return {"sku": sku, "available_units": 42}


def get_demand(sku):
    return {"sku": sku, "requested_units": 31}


implementations = {
    "get_inventory": get_inventory,
    "get_demand": get_demand,
}

tools = [
    {
        "type": "function",
        "name": "get_inventory",
        "description": "Return an object with sku (string) and available_units (number).",
        "parameters": {
            "type": "object",
            "properties": {"sku": {"type": "string"}},
            "required": ["sku"],
            "additionalProperties": False,
        },
        "output_schema": {
            "type": "object",
            "properties": {
                "sku": {"type": "string"},
                "available_units": {"type": "number"},
            },
            "required": ["sku", "available_units"],
            "additionalProperties": False,
        },
        "allowed_callers": ["programmatic"],
    },
    {
        "type": "function",
        "name": "get_demand",
        "description": "Return an object with sku (string) and requested_units (number).",
        "parameters": {
            "type": "object",
            "properties": {"sku": {"type": "string"}},
            "required": ["sku"],
            "additionalProperties": False,
        },
        "output_schema": {
            "type": "object",
            "properties": {
                "sku": {"type": "string"},
                "requested_units": {"type": "number"},
            },
            "required": ["sku", "requested_units"],
            "additionalProperties": False,
        },
        "allowed_callers": ["programmatic"],
    },
    {"type": "programmatic_tool_calling"},
]

input_items = [
    {
        "role": "user",
        "content": "Compare inventory with demand for sku_123.",
    }
]

while True:
    response = client.responses.create(
        model=model,
        store=False,
        input=input_items,
        tools=tools,
    )

    if response.status != "completed":
        raise RuntimeError(f"Response ended with status {response.status}")

    # Preserve every output item, including program and reasoning items.
    input_items.extend(item.model_dump(exclude_none=True) for item in response.output)

    calls = [item for item in response.output if item.type == "function_call"]
    if not calls:
        message = next(
            (item for item in response.output if item.type == "message"), None
        )
        if message:
            refusal = next(
                (part.refusal for part in message.content if part.type == "refusal"),
                "",
            )
            print(response.output_text or refusal)
            break
        continue

    for call in calls:
        run = implementations.get(call.name)
        if run is None:
            raise ValueError(f"Unknown tool: {call.name}")

        result = run(**json.loads(call.arguments))
        input_items.append(
            {
                "type": "function_call_output",
                "call_id": call.call_id,
                "output": json.dumps(result),
                # Preserve caller so the runtime can resume the correct program.
                "caller": call.caller.model_dump() if call.caller else None,
            }
        )
package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/responses"
)

type toolArguments struct {
	SKU string `json:"sku"`
}

func main() {
	client := openai.NewClient()
	input := responses.ResponseInputParam{
		responses.ResponseInputItemParamOfMessage(
			"Compare inventory with demand for sku_123.",
			responses.EasyInputMessageRoleUser,
		),
	}
	tools := []responses.ToolUnionParam{
		functionTool(
			"get_inventory",
			"Return an object with sku (string) and available_units (number).",
			"available_units",
		),
		functionTool(
			"get_demand",
			"Return an object with sku (string) and requested_units (number).",
			"requested_units",
		),
		programmaticTool(),
	}

	for {
		response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
			Model: "gpt-6-astra",
			Store: openai.Bool(false),
			Input: responses.ResponseNewParamsInputUnion{OfInputItemList: input},
			Tools: tools,
		})
		if err != nil {
			panic(err)
		}
		if response.Status != "completed" {
			panic(fmt.Errorf("response ended with status %s", response.Status))
		}

		// Preserve every output item, including program and reasoning items.
		input = append(input, outputAsInput(response.Output)...)

		calls := functionCalls(response.Output)
		if len(calls) == 0 {
			if text, ok := finalMessageText(response); ok {
				fmt.Println(text)
				break
			}
			continue
		}

		for _, call := range calls {
			result, err := runTool(call.Name, call.Arguments)
			if err != nil {
				panic(err)
			}
			output, err := json.Marshal(result)
			if err != nil {
				panic(err)
			}

			toolOutput := responses.ResponseInputItemParamOfFunctionCallOutput(string(output))
			toolOutput.OfFunctionCallOutput.CallID = openai.String(call.CallID)
			caller := call.Caller.AsProgram()
			if caller.CallerID == "" {
				panic("function call is missing its program caller")
			}
			// Preserve caller so the runtime can resume the correct program.
			toolOutput.OfFunctionCallOutput.Caller.OfProgram =
				&responses.ResponseInputItemFunctionCallOutputCallerProgramParam{
					CallerID: caller.CallerID,
				}
			input = append(input, toolOutput)
		}
	}
}

func functionTool(name, description, resultField string) responses.ToolUnionParam {
	parameters := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"sku": map[string]any{"type": "string"},
		},
		"required":             []string{"sku"},
		"additionalProperties": false,
	}
	outputSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"sku":       map[string]any{"type": "string"},
			resultField: map[string]any{"type": "number"},
		},
		"required":             []string{"sku", resultField},
		"additionalProperties": false,
	}
	tool := responses.ToolParamOfFunction(name, parameters, true)
	tool.OfFunction.Description = openai.String(description)
	tool.OfFunction.AllowedCallers = []string{"programmatic"}
	tool.OfFunction.OutputSchema = outputSchema
	return tool
}

func programmaticTool() responses.ToolUnionParam {
	tool := responses.NewToolProgrammaticToolCallingParam()
	return responses.ToolUnionParam{OfProgrammaticToolCalling: &tool}
}

func outputAsInput(
	output []responses.ResponseOutputItemUnion,
) []responses.ResponseInputItemUnionParam {
	input := make([]responses.ResponseInputItemUnionParam, 0, len(output))
	for _, item := range output {
		var converted responses.ResponseInputItemUnion
		if err := json.Unmarshal([]byte(item.RawJSON()), &converted); err != nil {
			panic(err)
		}
		input = append(input, converted.ToParam())
	}
	return input
}

func functionCalls(
	output []responses.ResponseOutputItemUnion,
) []responses.ResponseFunctionToolCall {
	calls := make([]responses.ResponseFunctionToolCall, 0)
	for _, item := range output {
		if item.Type == "function_call" {
			calls = append(calls, item.AsFunctionCall())
		}
	}
	return calls
}

func finalMessageText(response *responses.Response) (string, bool) {
	for _, item := range response.Output {
		if item.Type != "message" {
			continue
		}
		text := response.OutputText()
		if text != "" {
			return text, true
		}
		for _, content := range item.AsMessage().Content {
			if content.Type == "refusal" {
				return content.AsRefusal().Refusal, true
			}
		}
		return "", true
	}
	return "", false
}

func runTool(name, argumentsJSON string) (map[string]any, error) {
	var arguments toolArguments
	if err := json.Unmarshal([]byte(argumentsJSON), &arguments); err != nil {
		return nil, fmt.Errorf("parse %s arguments: %w", name, err)
	}

	switch name {
	case "get_inventory":
		return map[string]any{"sku": arguments.SKU, "available_units": 42}, nil
	case "get_demand":
		return map[string]any{"sku": arguments.SKU, "requested_units": 31}, nil
	default:
		return nil, fmt.Errorf("unknown tool: %s", name)
	}
}
require "json"
require "openai"

client = OpenAI::Client.new

def get_inventory(sku:)
  {
    sku: sku,
    available_units: 42
  }
end

def get_demand(sku:)
  {
    sku: sku,
    requested_units: 31
  }
end

implementations = {
  "get_inventory" => method(:get_inventory),
  "get_demand" => method(:get_demand)
}
tools = [
  {
    type: :function,
    name: "get_inventory",
    description: "Return an object with sku (string) and available_units (number).",
    parameters: {
      type: :object,
      properties: { sku: { type: :string } },
      required: ["sku"],
      additionalProperties: false
    },
    output_schema: {
      type: :object,
      properties: {
        sku: { type: :string },
        available_units: { type: :number }
      },
      required: %w[sku available_units],
      additionalProperties: false
    },
    allowed_callers: [:programmatic],
    strict: true
  },
  {
    type: :function,
    name: "get_demand",
    description: "Return an object with sku (string) and requested_units (number).",
    parameters: {
      type: :object,
      properties: { sku: { type: :string } },
      required: ["sku"],
      additionalProperties: false
    },
    output_schema: {
      type: :object,
      properties: {
        sku: { type: :string },
        requested_units: { type: :number }
      },
      required: %w[sku requested_units],
      additionalProperties: false
    },
    allowed_callers: [:programmatic],
    strict: true
  },
  { type: :programmatic_tool_calling }
]
input = [
  {
    role: :user,
    content: "Compare inventory with demand for sku_123."
  }
]

loop do
  response = client.responses.create(
    model: "gpt-6-astra",
    store: false,
    input: input,
    tools: tools
  )
  unless response.status == OpenAI::Responses::ResponseStatus::COMPLETED
    raise "Response ended with status #{response.status}"
  end

  # Preserve every output item, including program and reasoning items.
  input.concat(response.output)
  calls = response.output.grep(OpenAI::Models::Responses::ResponseFunctionToolCall)

  if calls.empty?
    message = response.output.find do |item|
      item.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)
    end
    next unless message.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)

    refusal = message.content.find do |content|
      content.is_a?(OpenAI::Models::Responses::ResponseOutputRefusal)
    end
    text = response.output_text
    if text.empty? &&
       refusal.is_a?(OpenAI::Models::Responses::ResponseOutputRefusal)
      text = refusal.refusal
    end
    puts(text)
    break
  end

  calls.each do |call|
    implementation = implementations.fetch(call.name) do
      raise ArgumentError, "Unknown tool: #{call.name}"
    end
    result = implementation.call(**JSON.parse(call.arguments, symbolize_names: true))
    output = {
      type: :function_call_output,
      call_id: call.call_id,
      output: JSON.generate(result)
    }
    # Preserve caller so the runtime can resume the correct program.
    output[:caller] = call.caller_.to_h if call.caller_
    input << output
  end
end

응답을 저장(storage)하면 이전 응답 항목을 모두 다시 보내는 대신 previous_response_id에서 계속할 수 있어요. 새 function_call_output 항목을 다음 입력으로 보내면 돼요. store: false를 쓰면 program, reasoning, function-call, function-call-output, program_output 항목을 포함한 전체 시퀀스를 순서대로 재생하세요.

무상태 reasoning 모델 요청의 경우 반환된 reasoning 항목을 모두 재생하세요. 각 항목에는 기본적으로 encrypted_content가 포함돼요. 일반적인 무상태 패턴은 대화 상태를 참고하세요.

프로그램용 도구 설계

  • JavaScript가 산문을 구문 분석하지 않고 검사할 수 있는 구조화되고 간결한 데이터를 반환하세요.
  • output_schema로 각 도구의 예상 반환 필드와 타입을 정의하고, 오류 동작을 문서화하세요. 반환 형태를 미리 알 수 없으면 도구를 직접(direct) 유지해서 모델이 결과를 검사하게 하세요.
  • 정확한 프로그램 결과 형태와 필요한 증거를 정의하세요. 프로그램이 유효한 결과를 만들 수 없을 때는 명확한 구조화 실패를 반환하세요.
  • 가능하면 함수 호출을 멱등(idempotent)하게 만드세요. 재시도나 재생이 안전하지 않은 부작용을 반복하지 않아야 해요.
  • 호스티드 프로그램에서 온 호출이라도 애플리케이션에서 각 호출의 인자와 권한을 검사하세요.
  • 모델이 올바르게 조합할 수 있도록 도구에 구체적인 이름과 설명을 주세요.
  • 호출자가 누구든 고영향 액션 전에는 애플리케이션 수준 승인을 요구하세요.

프로그래매틱 도구 호출 평가

프로그래매틱 도구 호출은 모델 컨텍스트에 추가되는 중간 도구 출력을 줄일 수 있지만, 그 효과는 작업과 도구 응답에 따라 달라져요. 기준선으로 직접 도구 호출부터 시작하고, 대표 작업에서 두 접근을 비교하세요.

효율성을 측정하기 전에 최종 답변 품질 기준과 필요한 증거를 정의하세요. 토큰 사용량과 도구 호출을 정확성·완전성·증거 커버리지와 함께 평가하고, 수용 가능한 품질 트레이드오프를 명시적으로 만들세요.

측정 항목:

  • 최종 답변의 정확성, 완전성, 증거 커버리지
  • 입력·총 토큰, 종단 간 지연 시간, 비용
  • 모델 턴, 도구 호출, 재시도, 복구 동작
  • 부작용과 승인 요구 사항을 특히 고려한 안전 결과
  • 실행된 라우트가 의도된 워크플로 단계와 일치했는지

Agents API

Agents API에서 프로그래매틱 도구 호출은 OpenAI가 관리하는 에이전트 하네스에서 실행되며 기본적으로 활성화돼요. 하네스는 에이전트에 exec 도구를 주고 기존 도구를 생성된 JavaScript 안에서 사용 가능하게 해줘요. 그 도구를 커맨드라인 프로그램으로 감싸거나 샌드박스에 설치할 필요는 없어요.

프로그래매틱 도구 호출을 비활성화하려면 agent.tools에 이 항목을 포함하세요.

{
  "type": "programmatic_tool_calling",
  "enabled": false
}

항목이나 그 enabled 필드를 생략하면 프로그래매틱 도구 호출은 활성 상태로 유지돼요. 타입만 있는 항목 { "type": "programmatic_tool_calling" }도 활성 상태로 유지해요. allowed_callers 구성과 위의 Responses 연속 루프는 Responses API 통합을 설명해요.

프로그래매틱 도구 호출은 environment.type이 none으로 설정된 대화 전용 세션에서도 동작해요. Bash, executor MCP, 그리고 샌드박스에서 실행되는 다른 도구는 여전히 실행 환경이 필요해요.

JavaScript에서 도구를 조율하는 것은 도구가 실행되는 위치를 바꾸지 않아요. 셸 호출은 샌드박스에서 명령을 실행하며, JavaScript 런타임 자체는 시스템 프로세스를 시작하지 않아요. Executor MCP는 여전히 샌드박스를 사용하고, function 도구는 여전히 애플리케이션 서버를 호출해요. 에이전트는 어떤 것이 모델 컨텍스트에 들어갈지 결정하기 전에 결과를 처리해요.

어떤 워크플로 단계가 코드를 사용해야 하는지 정의하려면 위의 라우팅 안내를 사용하세요. Agents API 구성과 호출 처리는 Functions와 MCP 연결을 따르세요.

관련 가이드

  • 함수 호출로 클라이언트 소유 함수를 정의하세요.
  • 도구 검색을 사용해 모델이 필요할 때까지 큰 도구 정의를 지연하세요.
  • 대화 상태로 저장되거나 무상태인 Responses API 요청을 계속하세요.
  • 저장 모드를 고르기 전에 데이터 통제를 검토하세요.