For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme

컴퓨터 사용

에이전트가 OpenAI에서 호스팅하는 브라우저로 작업을 수행하도록 하세요.

컴퓨터 사용 기능을 이용하면 에이전트가 웹사이트를 탐색하고 브라우저 인터페이스를 조작할 수 있습니다. 이를 통해 웹사이트를 테스트하거나 정보를 수집하거나 애플리케이션을 UI로 사용할 수 있습니다.

Agents API는 OpenAI에서 호스팅하는 환경에서 브라우저를 실행합니다. 애플리케이션은 세션을 시작하고 세션 이벤트를 수신합니다. 에이전트는 브라우저에서 관찰한 내용을 바탕으로 다음 행동을 결정합니다.

브라우저 작업을 실행하려면 다음 단계를 따르세요.

  1. 브라우저 세션을 생성하고 세션 ID를 저장하세요.
  2. 세션 이벤트를 수신하고 에이전트에 작업을 보내세요.
  3. 웹사이트 접근 요청을 각각 처리하세요. 계정이 필요한 작업이라면 로그인을 처리하세요.
  4. 주 에이전트의 턴이 끝날 때까지 기다린 뒤 결과를 확인하세요. 연결이 끊기면 재시도하기 전에 같은 세션을 복구하세요.
  5. 저장된 브라우저 활동을 검토한 뒤 작업을 마치면 세션을 삭제하세요.

브라우저 설정

Agents API 빠른 시작의 사전 준비 단계에 따라 API 키를 생성하고 OPENAI_API_KEY를 환경 변수로 내보낸 뒤, 사용하는 언어에 맞는 OpenAI SDK를 설치하세요. JavaScript 예제를 실행하려면 openai와 prompt-sync를 설치하세요. cURL 예제에는 Bash와 jq가 필요합니다.

브라우저 접근을 활성화하려면 다음과 같이 설정하세요.

  • { "type": "computer_use" }를 agent.tools에 추가하세요.
  • environment.type을 openai_hosted로, environment.desktop.enabled를 true로 설정하세요.

아래 JavaScript 예제는 순서대로 이어집니다. 세션을 생성하고 웹사이트 접근 승인 요청을 처리한 뒤, 작업을 보내고 답변을 출력합니다. 먼저 스크린샷을 활성화한 상태로 브라우저 세션을 생성하세요. 세션을 생성하는 것만으로는 작업이 시작되지 않습니다.

브라우저 세션 생성
import OpenAI from "openai";
import { open } from "node:fs/promises";

const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
  agent: {
    model: "gpt-6-astra",
    instructions:
      "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
    tools: [{ type: "computer_use", include_screenshots: true }],
  },
  environment: {
    type: "openai_hosted",
    desktop: { enabled: true },
    network: { access: "enabled" },
  },
});
console.log("Session ID:", session.id);

오리진 접근 처리

브라우저가 새로운 웹사이트 오리진에 접근하려면 매번 사용자의 승인이 필요합니다. 공개 웹사이트도 마찬가지입니다. 네트워크 접근을 활성화해도 이러한 요청이 자동으로 승인되지는 않습니다.

자동 검토를 활성화해도 브라우저의 오리진 접근 승인 요청과 로그인 요청은 계속 발생합니다. 애플리케이션에서 두 유형의 요청을 계속 처리하세요.

오리진 접근 승인 요청은 다음과 같이 처리하세요.

  1. agent.session.requires_action 이벤트를 받으면 세션을 조회하고 현재 required_actions를 확인하세요.
  2. 대기 중인 computer_use_approval_request 항목에서 중첩된 request.type 값이 browser_origin_access인 항목을 찾으세요.
  3. 요청에 지정된 origin과 reason(있는 경우)을 표시하고 사용자가 approve, deny, cancel 중 하나를 선택하게 하세요. 아래 예제처럼 같은 request_id를 사용하고 중첩된 response에 type: "browser_origin_access"와 decision을 담아 세션 이벤트 엔드포인트로 제출하세요.
오리진 접근을 승인해도 개별 동작 전에 사용자 확인을 거치도록 강제할 수는 없습니다

구매, 데이터 삭제와 같은 파괴적 변경, 그 밖에 중대한 결과를 초래하는 동작 전에 반드시 사용자의 확인을 받아야 한다면, 호스팅되는 브라우저가 해당 동작을 수행할 수 없는 리소스에만 접근하도록 제한하거나 직접 제어하는 브라우저 런타임을 사용하세요. 함수 도구로 확인을 요청하는 방식은 에이전트가 해당 함수를 호출해야만 작동합니다.

웹사이트 콘텐츠는 신뢰할 수 없는 데이터로 취급하세요. 웹사이트 콘텐츠는 권한을 부여하거나 사용자의 지시를 무효화할 수 없습니다. 직접 제어하는 런타임에서 사용자 확인과 동의를 처리하는 방법을 참고하세요.

작업 코드 앞에 다음 헬퍼 함수를 정의하세요. 이 함수는 오리진 접근 승인 요청을 처리합니다. 이 작업은 공개 페이지만 읽으므로 로그인 요청은 취소합니다.

오리진 접근 요청에 응답
import { createInterface } from "node:readline";

const inputLines = createInterface({
  input: process.stdin,
  crlfDelay: Infinity,
});
const answers = inputLines[Symbol.asyncIterator]();

/** @param {OpenAI} client */
async function respondToOriginApproval(client, sessionId, approval) {
  const request = approval.request;
  if (request.type === "browser_origin_access") {
    console.log("Requested origin:", request.origin);
    console.log(request.reason ?? "The browser needs access to this origin.");
    let input;
    do {
      process.stdout.write(
        "Allow access? [approve/deny/cancel, default deny] "
      );
      const answer = await answers.next();
      input = (answer.done ? "" : answer.value).trim().toLowerCase() || "deny";
    } while (!["approve", "deny", "cancel"].includes(input));
    const decision =
      input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
    await client.beta.agents.sessions.events.create(sessionId, {
      events: [
        {
          type: "agent.session.input.computer_use_approval_request_result",
          request_id: approval.request_id,
          response: { type: "browser_origin_access", decision },
        },
      ],
    });
  } else if (request.type === "browser_authentication") {
    // This public-page task must not sign in.
    await client.beta.agents.sessions.events.create(sessionId, {
      events: [
        {
          type: "agent.session.input.computer_use_approval_request_result",
          request_id: approval.request_id,
          response: { type: "browser_authentication", action: "cancel" },
        },
      ],
    });
  } else {
    throw new Error(`Unsupported approval request: ${request.type}`);
  }
}

스트림을 열린 상태로 유지하고 대기 중인 승인 요청을 모두 처리하세요. request_id로 요청을 추적하고, 세션의 required_actions에서 요청이 사라지면 해당 승인 UI를 제거하세요. 승인 요청을 취소해도 작업은 취소되지 않습니다.

202 응답은 서버가 사용자의 결정을 수락했다는 뜻입니다. 브라우저가 페이지 이동을 마쳤다는 뜻은 아닙니다.

브라우저 작업 실행

에이전트에 공개 개발자 사이트에서 Agents API 빠른 시작 문서를 찾아 제목과 URL을 알려 달라고 요청하세요.

애플리케이션이 첫 진행 이벤트부터 수신할 수 있도록 작업을 보내기 전에 이벤트 스트림을 여세요. 브라우저가 작업을 계속할 수 있도록 오리진 접근 승인 요청이 들어올 때마다 처리하세요.

작업을 보내고 결과 확인
let screenshot;
const events = await client.beta.agents.sessions.events.stream(session.id);
const handledRequests = new Set();
let completed = false;
try {
  await client.beta.agents.sessions.events.create(session.id, {
    events: [
      {
        type: "agent.session.input.message",
        input: [
          {
            role: "user",
            content: [
              {
                type: "input_text",
                text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
              },
            ],
          },
        ],
      },
    ],
  });
  for await (const event of events) {
    switch (event.type) {
      case "agent.session.requires_action": {
        const current = await client.beta.agents.sessions.retrieve(
          session.id
        );
        for (const approval of current.required_actions) {
          if (
            approval.type === "computer_use_approval_request" &&
            !handledRequests.has(approval.request_id)
          ) {
            await respondToOriginApproval(client, session.id, approval);
            handledRequests.add(approval.request_id);
          }
        }
        break;
      }
      case "agent.session.turn.item.done": {
        // Preserve streamed screenshots; retrieved history may omit image bytes.
        const item = event.item;
        if (item.type === "computer_use_call") {
          const output = item.output;
          if (
            output?.type === "computer_screenshot" &&
            output.image_url.startsWith("data:image/jpeg;base64,")
          ) {
            screenshot = Buffer.from(
              output.image_url.split(",", 2)[1],
              "base64"
            );
          }
        }
        break;
      }
      case "agent.session.turn.output_text.done":
        console.log(event.text);
        break;
      case "error":
        throw new Error(event.error.message);
      case "agent.session.failed":
      case "agent.session.environment.failed":
        throw new Error(`Agent lifecycle failure: ${event.type}`);
      case "agent.session.turn.failed":
        if (event.turn.subagent_id === null) {
          throw new Error(event.turn.error?.message ?? "Browser task failed");
        }
        break;
      case "agent.session.turn.cancelled":
        if (event.turn.subagent_id === null) {
          throw new Error("Browser task was cancelled");
        }
        break;
      case "agent.session.turn.completed":
        if (event.turn.subagent_id === null) completed = true;
        break;
    }
    if (completed) break;
  }
  if (!completed) {
    throw new Error("Stream closed before the browser task finished");
  }
  console.log();
} finally {
  events.controller.abort();
}

이 예제는 에이전트의 답변을 출력합니다. 답변에 빠른 시작 문서의 제목과 URL이 포함되어 있는지 확인하세요.

이벤트 스트림을 닫아도 작업은 멈추지 않습니다. 작업을 중지하려면 턴을 취소하세요. 연결이 끊기거나 결과가 불확실하면 복구 절차를 따르세요. 상세 cURL 예제에서는 전송 문제와 오류를 진단하는 방법도 보여 줍니다.

브라우저 활동 추적

브라우저에서 수행한 동작은 세션 출력에 computer_use_call 항목으로 표시됩니다. 스트림으로 수신하는 활동 항목과 저장된 세션 기록의 활동 항목은 구조가 같습니다.

필드 의미
id 활동 항목의 식별자입니다.
turn_id 해당 활동이 발생한 턴입니다.
title 브라우저 활동에 대한 설명입니다. 값이 null일 수 있습니다.
status in_progress, completed, failed, incomplete 중 하나입니다.
output 스크린샷이 있으면 여기에 포함됩니다.

제목과 상태를 사용해 애플리케이션에 진행 상황을 표시하세요. 브라우저 활동 항목은 개별 도구 동작을 설명합니다. 에이전트의 최종 답변이나 전체 턴의 완료 상태를 나타내지는 않습니다. 세션 이벤트 모델은 이벤트와 항목을 참고하세요.

스크린샷 포함

애플리케이션에서 브라우저의 진행 상황을 보여 주려면 include_screenshots를 true로 설정하세요. 이 설정은 computer_use 도구에 적용합니다. 기본적으로 API 출력에는 스크린샷이 포함되지 않지만 에이전트는 스크린샷을 볼 수 있습니다.

브라우저 활동 완료 이벤트에는 해당 활동에서 마지막으로 내보낸 스크린샷이 있는 경우 item.output에 다음과 같이 포함됩니다.

{
  "type": "computer_screenshot",
  "image_url": "data:image/jpeg;base64,..."
}

image_url을 사용해 스크린샷을 표시하세요. 스크린샷을 활성화해도 일부 동작은 output: null을 반환하므로 애플리케이션에서 이미지가 없는 활동 항목도 처리해야 합니다.

스크린샷에는 페이지나 계정의 민감한 데이터가 포함될 수 있습니다. 권한이 있는 사용자에게만 표시하고 애플리케이션 로그에 남기지 마세요.

스트림을 읽으면서 agent.session.turn.item.done 이벤트에 스크린샷이 있으면 보관하세요. 세션 기록을 조회할 때 이미지 바이트가 생략될 수 있으므로 나중에 항목 목록을 요청해서 가져오려 하지 말고 애플리케이션에 이미지를 보관하세요. SDK 예제는 턴이 끝나면 브라우저 활동 목록을 조회하고 마지막으로 캡처한 스크린샷을 저장합니다. 대부분의 예제는 browser-screenshot.jpg에 저장하며 Java 예제는 임시 파일을 사용합니다.

브라우저 활동 조회
for await (const item of client.beta.agents.sessions.items.list(session.id, {
  order: "asc",
  limit: 100,
})) {
  if (item.type !== "computer_use_call") continue;
  console.log(item.title ?? "Browser activity", item.status);
}
if (screenshot) {
  const file = await open("browser-screenshot.jpg", "wx", 0o600);
  try {
    await file.writeFile(screenshot);
  } finally {
    await file.close();
  }
  console.log("Saved browser-screenshot.jpg");
} else {
  console.log("No browser screenshot was returned.");
}

SDK 예제는 기존 파일을 덮어쓰지 않습니다. 예제를 다시 실행하기 전에 browser-screenshot.jpg를 옮기거나 삭제하세요.

로그인 처리

비공개 GitHub 리포지터리의 이슈를 읽는 작업에는 로그인을 마친 브라우저가 필요합니다. 애플리케이션에서 로그인을 처리하면 사용자가 채팅 밖에서 로그인 방식을 선택하고 자격 증명을 입력할 수 있습니다.

로그인 과정에서 여러 요청이 발생할 수 있습니다. 예를 들어 사이트에서 사용자에게 이메일 로그인을 선택한 다음 이메일 주소와 인증 코드를 차례로 입력하도록 요청할 수 있습니다. 각 요청에 포함된 로그인 방식과 필드를 바탕으로 UI를 구성하세요.

주 에이전트만 브라우저 인증을 요청할 수 있습니다. 하위 에이전트는 요청할 수 없습니다. 이 플로우는 이메일 주소, 비밀번호, 인증 코드를 지원하며 패스키나 QR 코드 로그인은 지원하지 않습니다. 지원하지 않는 방식을 필수로 요구하는 사이트에서는 이 플로우로 로그인을 완료할 수 없습니다.

작업의 이벤트 스트림을 열린 상태로 유지하고 오리진 접근 승인 요청이 들어올 때마다 처리하세요. agent.session.requires_action 이벤트를 받으면 세션을 조회하세요. 현재 required_actions의 computer_use_approval_request 항목 중 중첩된 request.type 값이 browser_authentication인 항목을 찾으세요.

중첩된 request를 사용해 로그인 UI를 구성하세요.

필드 사용 방법
reason 값이 있으면 입력이 필요한 이유를 설명하세요. 값이 null일 수 있습니다.
credential_origin 자격 증명을 사용할 대상을 표시하세요. 값이 null일 수 있습니다.
fields 각 필드의 id, label, type, required 값에 맞게 입력란을 표시하세요. 목록이 비어 있을 수 있습니다.
options 사용 가능한 로그인 방식을 표시하세요. 각 옵션에는 id, label, 해당 옵션의 입력 필드를 식별하는 field_ids가 있습니다.

요청에 로그인 방식이 제시되어 있으면 사용자가 하나를 선택하게 하고 선택한 방식에 해당하는 필드를 표시하세요. 그렇지 않으면 요청에 포함된 필드를 바로 표시하세요.

사용자가 자격 증명을 사용할 대상을 확인할 수 있을 때만 입력을 요청하세요. 자격 증명을 사용할 오리진이 누락되었거나 낯선 오리진인 경우, 사용자가 그 대상을 별도로 확인할 수 없다면 인증 요청을 취소하세요.

사용자 입력을 제출할 때는 바깥쪽 액션의 request_id를 사용하세요. 사용자가 거부하면 인증 요청을 취소하세요. 이후에도 요청이 들어올 때마다 처리하세요. 응답을 제출했다는 사실만으로는 로그인 성공 여부를 알 수 없습니다. 작업이 끝날 때까지 진행 상황을 추적하고 결과를 확인하세요.

예제: 로그인 방식 선택 후 코드 입력

이메일 코드 로그인과 비밀번호 로그인을 제공하는 사이트는 처음에는 입력 필드를 요청하지 않고 로그인 방식만 선택하도록 요청할 수 있습니다.

로그인 방식 선택
{
  "type": "computer_use_approval_request",
  "turn_id": "turn_example",
  "request_id": "request_choose_method",
  "request": {
    "type": "browser_authentication",
    "reason": "Choose how to sign in to the issue tracker",
    "credential_origin": "https://issues.example.com",
    "fields": [],
    "options": [
      { "id": "email_code", "label": "Email me a code", "field_ids": [] },
      { "id": "password", "label": "Use a password", "field_ids": [] }
    ]
  }
}

사용자가 이메일 코드 로그인을 선택하면 selected_option: "email_code"와 fields: []를 이 요청의 request_id로 제출하세요. 이후 사이트에서 이메일 주소와 인증 코드를 별도로 요청할 수 있습니다. 새 요청마다 해당 요청의 필드와 ID로 UI를 구성하고, 요청에 옵션이 제시된 경우에만 selected_option을 포함하세요.

사용자 입력 반환

사용자가 로그인 요청에 필요한 입력을 마치면 세션 이벤트 엔드포인트로 응답을 보내세요. action을 submit으로 설정하고 대기 중인 승인 요청의 요청 ID와 필드 ID를 사용하세요. 다음은 이메일 주소 요청에 대한 응답 예시입니다.

{
  "events": [
    {
      "type": "agent.session.input.computer_use_approval_request_result",
      "request_id": "REQUEST_ID",
      "response": {
        "type": "browser_authentication",
        "action": "submit",
        "fields": [{ "field_id": "email", "value": "USER_ENTERED_VALUE" }]
      }
    }
  ]
}

요청에 로그인 방식이 제시된 경우 선택한 방식의 ID를 response.selected_option에 포함하세요. 해당 방식의 field_ids에 나열된 필드만 제출하고 각 필수 필드에는 비어 있지 않은 값을 넣으세요. 해당 방식에 필드가 없으면 fields: []를 보내세요.

로그인 방식이 제시되지 않은 경우 selected_option을 생략하고 요청의 필드를 바로 제출하세요. 요청에 필드가 나열되어 있다면 모두 선택 사항이더라도 하나 이상 포함하세요.

로그인 입력값은 반드시 이 전용 이벤트로만 보내세요. 제출한 값은 에이전트의 모델 입력에 포함되지 않으며 세션 기록의 인증 응답 항목에서도 생략됩니다.

이메일 주소를 비롯한 모든 값을 민감한 정보로 취급하세요. 입력값을 마스킹하고 로그, 분석 데이터, 저장된 UI 상태에 남기지 마세요. 제출 후에는 양식을 비우세요. 일반 메시지나 함수 도구의 결과로 자격 증명을 보내지 마세요.

제출 이벤트에서는 turn_id를 생략하세요. 필드와 페이로드의 제약 조건은 인증 응답 제출 제한을 참고하세요.

본문이 비어 있는 202 응답은 서버가 제출을 수락했다는 뜻입니다. 로그인 성공 여부는 별도로 확인해야 합니다. 세션 이벤트를 계속 수신하면서 추가 요청이 있는지, 작업이 재개되는지 확인하세요. 자격 증명을 제출할 때는 HTTP 또는 SDK의 자동 재시도를 비활성화하세요. 제출이 수락되었는지 확실하지 않다면 계속하기 전에 세션을 새로 조회하세요.

사용자가 취소할 수 있도록 설정

사용자가 로그인을 거부하면 대기 중인 요청에 action: "cancel"로 응답하세요. 해당 요청의 request_id를 사용하고 fields와 selected_option은 생략하세요.

{
  "events": [
    {
      "type": "agent.session.input.computer_use_approval_request_result",
      "request_id": "REQUEST_ID",
      "response": { "type": "browser_authentication", "action": "cancel" }
    }
  ]
}

이렇게 하면 인증 요청이 취소됩니다. 작업 자체를 중지하려면 턴을 취소하세요.

인증이 필요한 브라우저 작업 실행

애플리케이션은 세션 이벤트를 수신하면서 오리진 접근 승인 요청과 로그인 요청을 처리합니다. 작업을 실행하기 전에 아래 헬퍼 함수를 정의하세요. 이 함수는 자격 증명을 사용할 대상을 표시하고, 입력값이 화면에 보이지 않게 사용자 입력을 받은 뒤 응답을 제출합니다. 사용자가 거부하면 로그인 요청을 취소합니다.

브라우저 접근 승인 및 로그인 처리
import promptSync from "prompt-sync";

const prompt = promptSync({ sigint: true });

/** @param {OpenAI} client */
async function respondToComputerUseApproval(client, sessionId, approval) {
  const request = approval.request;
  if (request.type === "browser_origin_access") {
    console.log("Requested origin:", request.origin);
    console.log(request.reason ?? "The browser needs access to this origin.");
    let input;
    do {
      input =
        prompt("Allow access? [approve/deny/cancel, default deny] ")
          .trim()
          .toLowerCase() || "deny";
    } while (!["approve", "deny", "cancel"].includes(input));
    const decision =
      input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
    await client.beta.agents.sessions.events.create(sessionId, {
      events: [
        {
          type: "agent.session.input.computer_use_approval_request_result",
          request_id: approval.request_id,
          response: { type: "browser_origin_access", decision },
        },
      ],
    });
    return;
  }
  if (request.type !== "browser_authentication") {
    throw new Error(`Unsupported approval request: ${request.type}`);
  }
  async function cancelSignIn() {
    await client.beta.agents.sessions.events.create(
      sessionId,
      {
        events: [
          {
            type: "agent.session.input.computer_use_approval_request_result",
            request_id: approval.request_id,
            response: { type: "browser_authentication", action: "cancel" },
          },
        ],
      },
      { maxRetries: 0 }
    );
  }
  console.log(request.reason ?? "Sign in to continue");
  console.log(
    "Credential origin:",
    request.credential_origin ?? "Not supplied"
  );
  const consent = prompt("Have you verified the sign-in destination? [y/N] ")
    .trim()
    .toLowerCase();
  if (!["y", "yes"].includes(consent)) {
    await cancelSignIn();
    return;
  }

  let selectedOption;
  let activeFields = request.fields;
  if (request.options.length > 0) {
    request.options.forEach((option, index) => {
      console.log(`${index + 1}. ${option.label}`);
    });
    let choice;
    do {
      const input = prompt("Choose a sign-in method, or enter cancel: ")
        .trim()
        .toLowerCase();
      if (input === "cancel") {
        await cancelSignIn();
        return;
      }
      choice = Number(input);
    } while (
      !Number.isInteger(choice) ||
      choice < 1 ||
      choice > request.options.length
    );
    selectedOption = request.options[choice - 1];
    activeFields = request.fields.filter((field) =>
      selectedOption.field_ids.includes(field.id)
    );
  }

  const fields = [];
  do {
    for (const field of activeFields) {
      while (true) {
        const value = prompt.hide(
          `${field.label}${field.required ? "" : " (optional)"} (leave blank for options): `
        );
        if (value.length > 0) {
          fields.push({ field_id: field.id, value });
          break;
        }
        let action;
        do {
          action = prompt(
            field.required
              ? "Enter a value or cancel sign-in? [enter/cancel, default enter] "
              : "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
          )
            .trim()
            .toLowerCase();
        } while (
          ![
            "",
            "enter",
            "cancel",
            ...(field.required ? [] : ["skip"]),
          ].includes(action)
        );
        if (action === "cancel") {
          await cancelSignIn();
          return;
        }
        if (!field.required && (action === "" || action === "skip")) break;
      }
    }
    if (!selectedOption && activeFields.length > 0 && fields.length === 0) {
      console.log(
        "This form requires at least one field. Enter a value or cancel sign-in."
      );
    }
  } while (!selectedOption && activeFields.length > 0 && fields.length === 0);
  await client.beta.agents.sessions.events.create(
    sessionId,
    {
      events: [
        {
          type: "agent.session.input.computer_use_approval_request_result",
          request_id: approval.request_id,
          response: {
            type: "browser_authentication",
            action: "submit",
            fields,
            ...(selectedOption ? { selected_option: selectedOption.id } : {}),
          },
        },
      ],
    },
    { maxRetries: 0 }
  );
}

다음 예제에서는 에이전트에 비공개 GitHub 리포지터리의 이슈를 읽어 달라고 요청합니다. https://github.com/acme/private-repo/issues를 접근할 수 있는 이슈 페이지의 URL로 바꾸세요.

작업을 보내기 전에 이벤트 스트림을 열고 세션에 입력이 필요할 때마다 헬퍼 함수를 호출하세요.

비공개 리포지터리의 이슈 읽기
import OpenAI from "openai";

const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
  agent: {
    model: "gpt-6-astra",
    instructions:
      "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
    tools: [{ type: "computer_use", include_screenshots: false }],
  },
  environment: {
    type: "openai_hosted",
    desktop: { enabled: true },
    network: { access: "enabled" },
  },
});
console.log("Session ID:", session.id);

let completed = false;
let readyToDelete = false;
try {
  const events = await client.beta.agents.sessions.events.stream(session.id);
  const handledRequests = new Set();
  try {
    await client.beta.agents.sessions.events.create(session.id, {
      events: [
        {
          type: "agent.session.input.message",
          input: [
            {
              role: "user",
              content: [
                {
                  type: "input_text",
                  // Replace this illustrative URL with your private repository.
                  text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.",
                },
              ],
            },
          ],
        },
      ],
    });
    for await (const event of events) {
      switch (event.type) {
        case "agent.session.requires_action": {
          const current = await client.beta.agents.sessions.retrieve(
            session.id
          );
          for (const approval of current.required_actions) {
            if (
              approval.type === "computer_use_approval_request" &&
              !handledRequests.has(approval.request_id)
            ) {
              await respondToComputerUseApproval(client, session.id, approval);
              handledRequests.add(approval.request_id);
            }
          }
          break;
        }
        case "agent.session.turn.output_text.done":
          console.log(event.text);
          break;
        case "error":
          throw new Error(event.error.message);
        case "agent.session.failed":
        case "agent.session.environment.failed":
          throw new Error(`Agent lifecycle failure: ${event.type}`);
        case "agent.session.turn.failed":
          if (event.turn.subagent_id === null) {
            throw new Error(event.turn.error?.message ?? "Browser task failed");
          }
          break;
        case "agent.session.turn.cancelled":
          if (event.turn.subagent_id === null) {
            throw new Error("Browser task was cancelled");
          }
          break;
        case "agent.session.turn.completed":
          if (event.turn.subagent_id === null) completed = true;
          break;
      }
      if (completed) break;
    }
    if (!completed) {
      throw new Error("Stream closed before the browser task finished");
    }
    console.log();
  } finally {
    events.controller.abort();
  }
  readyToDelete = completed;
} catch (error) {
  console.error(
    `Session ${session.id} was kept. Use the same ID to check its status before trying again.`
  );
  throw error;
} finally {
  if (readyToDelete) await client.beta.agents.sessions.delete(session.id);
}

로그인 과정에서 여러 요청이 발생할 수 있으므로 작업이 끝날 때까지 승인 요청을 계속 처리하세요. 에이전트의 결과가 요청한 작업과 일치하는지 확인하세요. 이 예제에서는 답변에 나온 이슈 제목과 URL을 검증하세요.

로그인 기록 조회

인증 요청과 수락된 응답은 세션 기록과 agent.session.turn.item.added 이벤트에 나타납니다. 요청 항목에는 로그인 양식의 메타데이터가 포함됩니다. 응답 항목에는 수락된 액션이 기록되며 제출한 자격 증명 값은 포함되지 않습니다.

이 기록으로 이전 상호작용을 검토하세요. 로그인 양식에 여전히 입력이 필요한지 확인하려면 세션의 현재 required_actions를 조회하세요. 응답이 기록되었다는 사실만으로는 로그인 성공 여부를 알 수 없습니다.

오리진 접근 승인에는 별도의 요청 또는 응답 기록 항목이 없습니다. required_actions를 통해 처리하세요.

사용자에게 질문하기

작업 중에 추가 설명을 요청하거나 사용자에게 선택권을 주려면 함수 도구를 agent.tools에 정의하세요. 예를 들어 request_user_response를 정의해 질문을 표시하고 답변을 받을 수 있습니다. 애플리케이션에서 도구 이름, 인수 스키마, UI를 제공합니다.

agent.session.requires_action 이벤트를 받으면 해당 도구의 대기 중인 function_call을 찾고 그 안의 arguments를 사용해 질문을 표시하세요. agent.session.input.tool_result로 답변을 반환할 때는 해당 액션의 turn_id와 call_id를 사용하세요. success: true를 설정하고 output에 답변을 넣으세요. 구조화된 답변은 JSON 문자열로 직렬화하세요. 사용자가 답변을 거부하면 success: false와 함께 error 메시지를 반환하세요.

답변을 반환한 뒤에도 세션 이벤트를 계속 수신하세요. 앞에서 정의한 브라우저 승인 헬퍼 함수는 오리진 접근과 로그인을 처리합니다. 질문 도구도 처리하도록 이벤트 핸들러를 확장하세요.

함수 도구의 결과는 모델에 전달되고 세션 기록에 저장됩니다. 비밀번호와 인증 코드는 브라우저 인증을 통해 입력받으세요.

승인 처리 복구

애플리케이션의 연결이 끊기거나 승인 응답에 실패하면 계속하기 전에 같은 세션을 조회하고 현재 required_actions를 확인하세요. 아직 대기 중인 요청에 대해서만 양식을 다시 구성하고 더 이상 목록에 없는 요청의 UI는 제거하세요.

이벤트 스트림 연결이 끊기면 스트림 복구 절차에 따라 이벤트 수신을 재개하세요. 다시 연결할 때 작업이나 승인 응답을 자동으로 재전송해서는 안 됩니다.

응답 상태에 따라 다음과 같이 처리하세요.

결과 애플리케이션에서 수행할 작업
202 제출한 값을 지우고 세션 이벤트를 수신해 결과를 확인하세요.
400 응답 유형, 선택한 옵션, 필드 ID, 필수 값이 대기 중인 요청과 일치하는지 확인하세요. 취소 응답과 오리진 접근 응답에서는 필드를 생략하세요.
404 세션 ID, 요청 ID, 응답 유형을 확인하세요. 해당 요청이 더 이상 유효하지 않을 수 있으므로 세션을 다시 조회하세요.
409 세션을 새로 조회하세요. 요청이 만료되었거나 해당 턴이 끝났거나 서버가 이미 다른 응답을 수락했을 수 있습니다.
수락 여부를 확인하기 전에 연결이 끊김 수락 여부를 알 수 없는 상태로 처리하세요. 다시 연결해 세션을 조회한 뒤 재시도할지 결정하세요.

인증 요청은 5분 후에 만료되며 사용자가 입력하는 도중에도 해당 턴이 끝날 수 있습니다. 로그인 양식을 복원하기 전에 세션을 새로 조회하세요.

인증 응답 제출을 재시도할 때는 같은 request_id와 선택한 옵션을 사용하고 각 필드에 같은 값을 보내세요. 동일한 응답을 재전송해도 브라우저 양식에 값이 다시 입력되지는 않습니다. 제출이 수락된 뒤 값을 변경하면 409가 반환됩니다. 로그인을 새로 시도하려면 에이전트가 새 요청을 보내야 합니다.

오리진 접근 승인 응답을 전달하지 못한 경우에는 같은 결정을 다시 보내세요. 이미 수락된 결정을 변경하면 409가 반환됩니다. 오리진 접근 요청은 해당 턴이 진행되는 동안 응답을 기다리며, 인증 요청의 5분 제한은 적용되지 않습니다.

네트워크 접근 제어

호스팅 환경의 network 설정으로 브라우저와 해당 환경에서 실행되는 코드의 아웃바운드 접근을 제어하세요. 대상 웹사이트뿐 아니라 페이지 리소스를 불러오거나 리디렉션을 처리하는 데 필요한 도메인도 허용하세요.

오리진 접근 승인은 사용자가 별도로 내리는 결정이며 네트워크 정책을 무효화하지 않습니다. 환경을 구성하려면 네트워크 접근 설정을 참고하세요.

후속 작업 및 정리

현재 브라우저 상태가 필요한 후속 작업에는 같은 세션을 재사용하세요. 로그인 쿠키는 만료될 수 있으며 환경이 재활용되면 브라우저 상태가 초기화됩니다.

작업을 마치면 필요한 결과를 가져온 뒤 세션을 삭제해 환경 정리를 요청하세요.

브라우저 세션 삭제
await client.beta.agents.sessions.delete(session.id);

환경의 수명과 삭제 시 동작은 OpenAI에서 호스팅하는 샌드박스를 참고하세요.

요청 처리 참고 자료

인증 응답 제출 제한

한 번에 최대 6개의 필드 값을 제출할 수 있으며 각 필드는 한 번만 포함할 수 있습니다. 값 하나의 최대 길이는 16,384자입니다. 직렬화한 필드 값과 선택한 옵션의 크기를 합해 120 KiB 이하여야 합니다.

대기 중인 승인 요청의 요청 ID와 필드 ID를 사용하고 각 필수 필드에는 비어 있지 않은 값을 입력하세요. 요청 스키마는 세션 이벤트 API 레퍼런스를 참고하세요.

cURL 요청 및 스트림 처리 상세 예제

전송 실패, HTTP 오류, 잘못된 세션 생성 응답을 구분해야 할 때는 이 예제를 사용하세요. 스트림 출력도 필터링하며 오류 유형과 코드를 알려 줍니다. 첫 번째 예제와 마찬가지로 터미널 두 개를 사용하는 워크플로우입니다. 승인 요청이 들어올 때마다 두 번째 터미널에서 처리하세요.

세션 생성 및 스트림 오류 확인
create_browser_session() {
  unset session_id
  local result http_status body curl_status
  if result=$(curl --silent --fail-with-body --write-out '\n%{http_code}' \
    https://api.openai.com/v1/agents/sessions \
    -H "OpenAI-Beta: agents=v1" \
    -H "Authorization: Bearer $OPENAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "agent": {
        "model": "gpt-6-astra",
        "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
        "tools": [{ "type": "computer_use", "include_screenshots": true }]
      },
      "environment": {
        "type": "openai_hosted",
        "desktop": { "enabled": true },
        "network": { "access": "enabled" }
      }
    }'); then curl_status=0; else curl_status=$?; fi
  http_status=$(printf '%s\n' "$result" | tail -n 1)
  body=$(printf '%s\n' "$result" | sed '$d')
  [[ "$http_status" =~ ^[0-9]{3}$ ]] || http_status=000

  if [ "$curl_status" -ne 0 ] || [[ ! "$http_status" =~ ^2[0-9][0-9]$ ]]; then
    printf '%s' "$body" | jq --raw-input --slurp --compact-output \
      --arg status "$http_status" --arg curl_status "$curl_status" '
        def identifier:
          if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
        (try fromjson catch {}) as $response
        | (if ($response | type) == "object" then $response.error // $response else {} end) as $error
        | if $curl_status != "0" and $curl_status != "22" then
            {status: $status, type: "transport_error", code: ("curl_" + $curl_status)}
          else
            {status: $status, type: ((try ($error.type | identifier) catch null) // "http_error"),
             code: (try ($error.code | identifier) catch null)}
          end' >&2
    return 1
  fi
  if ! session_id=$(printf '%s' "$body" | jq --exit-status --raw-output \
    'select(type == "object") | .id | select(type == "string" and length > 0)' 2>/dev/null); then
    unset session_id
    printf '{"status":"%s","type":"invalid_response","code":"missing_session_id"}\n' "$http_status" >&2
    return 1
  fi
  printf 'Session ID: %s\n' "$session_id"
}
create_browser_session

# Terminal 1: use session_id from the creation request.
# Keep this stream open. Wait for HTTP 200 before sending input.
set -o pipefail
curl --silent --show-error --dump-header - --suppress-connect-headers \
  --no-buffer --fail-with-body \
  "https://api.openai.com/v1/agents/sessions/$session_id/events" \
  -H "OpenAI-Beta: agents=v1" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Accept: text/event-stream" \
  | jq --null-input --raw-input --compact-output --unbuffered '
      def identifier:
        if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
      def safe_error:
        if type == "object" then {type: (.type | identifier), code: (.code | identifier)} else null end;
      def public_event:
        if .type == "agent.session.turn.output_text.done" then {type, text}
        elif .type == "agent.session.turn.completed" or .type == "agent.session.turn.failed" or .type == "agent.session.turn.cancelled" then
          {type, turn_id: .turn.id, subagent_id: .turn.subagent_id, error: (.turn.error | safe_error)}
        elif .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" then
          {type, error: ((.error // .session.error // .environment.error) | safe_error)}
        elif .type == "agent.session.requires_action" then {type}
        else empty end;
      foreach inputs as $raw (
        {body: "", headers: false, failed: false, output: []};
        .output = []
        | ($raw | rtrimstr([13] | implode)) as $line
        | if ($line | startswith("HTTP/")) then
            (try ($line | capture("^(?<protocol>HTTP/[0-9.]+) (?<status>[0-9]{3})(?: |$)")) catch null) as $http
            | if $http == null then . else
                .body = "" | .headers = true | .status = $http.status
                | .failed = (($http.status | tonumber) >= 300)
                | .output = [$http.protocol + " " + $http.status]
              end
          elif .headers then
            if $line == "" then .headers = false else . end
          elif ($line | startswith("data:")) then
            (try ($line | ltrimstr("data:") | fromjson) catch null) as $event
            | .output = [$event | select(type == "object") | public_event]
          elif .failed and .body != null then
            .body += ($line + "\n")
            | (try (.body | fromjson) catch null) as $body
            | if ($body | type) == "object" then
                (($body.error // $body) | safe_error) as $error
                | .output = [{status: .status, type: ($error.type // "http_error"), code: $error.code}]
                | .body = null
              else . end
          else . end;
        .output[])'

# Terminal 2: replace sess_123 with the ID printed in terminal 1.
# Export OPENAI_API_KEY in this terminal too.
session_id="sess_123"
curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
  -H "OpenAI-Beta: agents=v1" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "type": "agent.session.input.message",
      "input": [{
        "role": "user",
        "content": [{
          "type": "input_text",
          "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
        }]
      }]
    }]
  }'