For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Primary navigation

Computer use

Let an agent complete tasks in an OpenAI-hosted browser.

Computer use lets an agent navigate websites and interact with browser interfaces to test a website, collect information, or use an application through its UI.

The Agents API runs the browser in an OpenAI-hosted environment. Your application starts the session and follows its events; the agent uses what it observes in the browser to decide what to do next.

To run a browser task:

  1. Create a browser session and save the session ID.
  2. Follow session events and send the agent a task.
  3. Handle each website access request. If the task needs an account, handle sign-in.
  4. Wait for the main agent’s turn to finish and verify its result. If the connection drops, recover the same session before retrying.
  5. Review saved browser activity, then delete the session when you’re finished.

Configure the browser

Follow the Agents API quickstart prerequisites to create an API key and export OPENAI_API_KEY, then install the OpenAI SDK for your language. For the JavaScript examples, install openai and prompt-sync. The cURL examples require Bash and jq.

To enable browser access:

  • Add { "type": "computer_use" } to agent.tools.
  • Set environment.type to openai_hosted and environment.desktop.enabled to true.

The JavaScript examples below form one walkthrough: create a session, handle website approvals, then send a task and print the answer. Start by creating a browser session with screenshots enabled. This does not start a task.

Create a browser session
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);

Handle origin access

The browser requires the user’s approval before accessing each new website origin, including public websites. Enabling network access does not approve these requests.

To handle an origin approval:

  1. On agent.session.requires_action, retrieve the session and inspect its current required_actions.
  2. Find pending computer_use_approval_request entries whose nested request.type is browser_origin_access.
  3. Show the requested origin and reason (if provided), then collect an approve, deny, or cancel decision. Submit it through the session events endpoint using the same request_id and a nested response containing type: "browser_origin_access" and decision, as shown below.
Origin approval does not enforce confirmation before individual actions

If your application must guarantee confirmation before purchases, destructive changes, or other consequential actions, restrict the hosted browser to resources that cannot perform them, or use a browser runtime you control. Asking for confirmation through a function tool relies on the agent calling that function.

Treat website content as untrusted. It cannot grant permission or override the user’s instructions. See the confirmation and consent guidance for a runtime you control.

Define this helper before the task code. It handles origin approvals and cancels sign-in requests because this task only reads public pages.

Respond to origin access requests
import promptSync from "prompt-sync";

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

/** @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 {
      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 },
        },
      ],
    });
  } 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}`);
  }
}

Keep the stream open and handle every pending approval. Use request_id to track requests, and remove approval controls when a request is no longer in the session’s required_actions. Cancelling an approval request does not cancel the task.

A 202 response means the decision was accepted, not that navigation has completed.

Run a browser task

Ask the agent to find the Agents API quickstart on the public developer site and report its title and URL.

Open the event stream before sending the task so your application receives the first progress events. Handle origin approvals as they arrive to let the browser continue.

Send the task and follow its result
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.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();
}

The example prints the agent’s answer. Check that it includes the title and URL of the quickstart.

Closing the event stream does not stop the task. To stop it, cancel the turn. For connection failures or uncertain outcomes, follow recovery guidance. The expanded cURL example includes transport and error diagnostics.

Follow browser activity

Browser operations appear as computer_use_call items in session output. Streamed activity and saved session history use the same item shape.

Field Meaning
id The activity item’s identifier.
turn_id The turn that produced the activity.
title A description of the browser activity, or null.
status in_progress, completed, failed, or incomplete.
output Screenshot output, when available.

Use the title and status to show progress in your application. A browser activity item describes a tool operation; it’s not the agent’s final answer or the completion status of the whole turn. See Events and items for the session event model.

Include screenshots

To display the browser’s progress in your application, set include_screenshots to true on the computer_use tool. Screenshots are excluded from API output by default; the agent can still observe them.

Each browser operation returns its last emitted screenshot in output, when available:

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

Use image_url to render the screenshot. Some operations return output: null, even with screenshots enabled, so your application should handle activity items without an image.

Screenshots can contain sensitive page or account data. Show them only to authorized users and keep them out of application logs.

Retrieve saved browser activity after the turn completes. The SDK examples save the latest available screenshot to browser-screenshot.jpg.

Read browser activity
let screenshot;
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);
  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");
  }
}
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.");
}

The SDK examples do not overwrite existing files. Move or remove browser-screenshot.jpg before running them again.

Handle sign-in

Tasks such as reading issues in a private GitHub repository require an authenticated browser. Your application handles sign-in so users can choose a login method and enter credentials outside the chat.

Sign-in can involve several requests. For example, a site might ask the user to choose email sign-in, enter an email address, and then enter a verification code. Build your UI from each request’s login methods and fields.

Only the main agent can request browser authentication; subagents cannot. This flow supports email addresses, passwords, and verification codes, but not passkeys or QR-code sign-in. Sites that require an unsupported method cannot complete sign-in through this flow.

Keep the task’s event stream open and handle origin approvals as they arrive. On agent.session.requires_action, retrieve the session and look in its current required_actions for computer_use_approval_request entries whose nested request.type is browser_authentication.

Use the nested request to render your sign-in UI:

Field How to use it
reason Explain why input is needed, if provided. Can be null.
credential_origin Show the destination the credentials are for. Can be null.
fields Render inputs using each field’s id, label, type, and required values. Can be empty.
options Show the available login methods. Each option has an id, label, and field_ids identifying its inputs.

If the request offers login methods, let the user choose one and show its associated fields. Otherwise, show the request’s fields directly.

Ask users to enter credentials only for a destination they can verify. If the credential origin is missing or unfamiliar and they cannot verify it independently, cancel the authentication request.

Submit the user’s input using the outer action’s request_id, or cancel the authentication request if they decline. Continue handling requests as they arrive. Submitting a response does not establish that sign-in succeeded; follow the task through completion and check its result.

Example: Choose a method, then enter a code

A site offering email-code and password sign-in might first ask the user to choose a method, without requesting any fields:

Choose a sign-in method
{
  "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": [] }
    ]
  }
}

If the user chooses email-code sign-in, submit selected_option: "email_code" with fields: [] using this request’s request_id. The site may then request an email address and verification code in separate requests. Render each new request using its own fields and IDs, and include selected_option only when that request offers options.

Return the user’s input

When the user completes a sign-in request, send their response through the session events endpoint. Set action to submit and use the request and field IDs from the pending approval. For example, a response to a request for an email address looks like this:

{
  "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" }]
      }
    }
  ]
}

If the request offers login methods, include the chosen method’s ID in response.selected_option. Submit only the fields listed in that method’s field_ids, with a nonempty value for each required field. If the method has no fields, send fields: [].

If no login methods are offered, omit selected_option and submit the request’s fields directly. When the request lists fields, include at least one, even if all are optional.

Send sign-in values only through this dedicated event. Submitted values stay outside the agent’s model input and are omitted from authentication response items in session history.

Treat every value, including email addresses, as sensitive. Mask entered values, keep them out of logs, analytics, and saved UI state, and clear the form after submission. Do not send credentials in ordinary messages or function-tool results.

Omit turn_id from the submission event. See authentication submission limits for field and payload constraints.

A 202 response with an empty body confirms that the submission was accepted, not that sign-in succeeded. Continue following session events for further requests or resumed work. Disable automatic HTTP or SDK retries for credential submissions. If you’re unsure whether a submission was accepted, refresh the session before continuing.

Let the user cancel

If the user declines to sign in, respond to the pending request with action: "cancel". Use its request_id and omit fields and selected_option:

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

This cancels the authentication request. To stop the task itself, cancel the turn.

Run an authenticated browser task

Your application handles origin approvals and sign-in requests while following session events. Define the helper below before running the task. It shows the destination, collects input with entered values hidden, and submits the response. If the user declines, it cancels the sign-in request.

Handle browser approvals and sign-in
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 }
  );
}

The following example asks the agent to read issues from a private GitHub repository. Replace https://github.com/acme/private-repo/issues with an issue page you can access.

Open the event stream before sending the task, and call the helper whenever the session requires input.

Read private repository issues
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);
}

Sign-in may require several requests, so keep handling approvals until the task finishes. Check the agent’s result against the requested task—for this example, verify the reported issue title and URL.

Read sign-in history

Authentication requests and accepted responses appear in session history and in agent.session.turn.item.added events. Request items contain the sign-in form metadata; response items record the accepted action without submitted credential values.

Use this history to review past interactions. To determine whether a sign-in form still needs input, retrieve the session’s current required_actions. A recorded response does not establish that sign-in succeeded.

Origin approvals have no dedicated request or response history items. Handle them through required_actions.

Ask the user a question

To ask for clarification or let the user make a choice during a task, define a function tool in agent.tools. For example, you could define request_user_response to present a question and collect an answer. Your application supplies the tool’s name, argument schema, and UI.

When you receive agent.session.requires_action, find the pending function_call for your tool and use its arguments to display the question. Return the answer through agent.session.input.tool_result, using the action’s turn_id and call_id. Set success: true and put the answer in output, serializing structured answers as a JSON string. If the user declines, return success: false with an error message.

Continue following session events after returning the answer. The browser approval helpers shown earlier handle origin access and sign-in; extend your event handler to handle your question tool as well.

Function-tool results are visible to the model and saved in session history. Collect passwords and verification codes through browser authentication.

Recover approval handling

If your application disconnects or an approval response fails, retrieve the same session and inspect its current required_actions before continuing. Rebuild forms only for requests that are still pending, and remove controls for requests that are no longer present.

For a disconnected event stream, follow stream recovery to resume receiving events. Reconnecting must not automatically resend the task or an approval response.

Use the response status to decide what to do next:

Result What your application should do
202 Clear submitted values and follow session events for the outcome.
400 Check the response type, selected option, field IDs, and required values against the pending request. Omit fields for cancellation and origin-access responses.
404 Check the session ID, request ID, and response type. Retrieve the session again; the request may no longer be available.
409 Refresh the session. The request may have expired, its turn may have ended, or a different response may already have been accepted.
Connection lost before acknowledgement Treat acceptance as unknown. Reconnect and retrieve the session before deciding whether to retry.

Authentication requests expire after five minutes, and their owning turn can end while the user is entering input. Refresh the session before restoring a sign-in form.

If you retry an authentication submission, use the same request_id, selected option, and field-value mapping. An identical retry does not fill the browser form again. Changing values after a submission has been accepted returns 409; a new sign-in attempt requires a new request from the agent.

For origin approvals, retry the same decision if delivery fails. Changing an accepted decision returns 409. Origin requests remain pending for their owning turn and do not have the five-minute authentication timeout.

Control network access

Use the hosted environment’s network configuration to control outbound access for both the browser and code running in the environment. Allow the destination website and any domains needed for page resources or redirects.

Origin approval is a separate user decision and does not override the network policy. See network access settings to configure the environment.

Continue and clean up

Reuse the same session for follow-up tasks that need its browser state. Login cookies can expire, and recycling the environment clears the browser state.

When you’re finished, retrieve any results you need and delete the session to request environment cleanup:

Delete the browser session
await client.beta.agents.sessions.delete(session.id);

See OpenAI-hosted sandboxes for environment lifetime and deletion behavior.

Request handling reference

Authentication submission limits

Each submission can include up to six field values, with each field included once. Values can contain up to 16,384 characters each; the serialized field values and selected option must fit within 120 KiB.

Use the request and field IDs from the pending approval and provide a nonempty value for each required field. See the session events API reference for the request schema.

Expanded cURL request and stream handling

Use this version when you need to distinguish transport failures, HTTP errors, and invalid session-creation responses. It also filters streamed output and reports error types and codes. It follows the same two-terminal workflow as the first example; handle approvals in the second terminal as they arrive.

Create a session and inspect stream failures
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."
        }]
      }]
    }]
  }'