1# Computer use
2
3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.
4
5Computer use lets an agent navigate websites and interact with browser interfaces
6to test a website, collect information, or use an application through its UI.
7
8The Agents API runs the browser in an OpenAI-hosted environment. Your application
9starts the session and follows its events; the agent uses what it observes in
10the browser to decide what to do next.
11
12To run a browser task:
13
141. [Create a browser session](#configure-the-browser) and save the session ID.
152. [Follow session events and send the agent a task](#run-a-browser-task).
163. [Handle each website access request](#handle-origin-access). If the task needs an account, [handle sign-in](#handle-sign-in).
174. Wait for the main agent's turn to finish and verify its result. If the connection drops, [recover the same session](#recover-approval-handling) before retrying.
185. [Review saved browser activity](#follow-browser-activity), then [delete the session](#continue-and-clean-up) when you're finished.
19
20## Configure the browser
21
22Follow the [Agents API quickstart prerequisites](https://developers.openai.com/api/docs/guides/agents-api/quickstart#prerequisites)
23to create an API key and export `OPENAI_API_KEY`, then install the
24[OpenAI SDK for your language](https://developers.openai.com/api/docs/libraries). For the JavaScript examples,
25install `openai` and `prompt-sync`. The cURL examples require Bash and `jq`.
26
27To enable browser access:
28
29- Add `{ "type": "computer_use" }` to `agent.tools`.
30- Set `environment.type` to `openai_hosted` and `environment.desktop.enabled` to `true`.
31
32The JavaScript examples below form one walkthrough: create a session, handle
33website approvals, then send a task and print the answer. Start by creating a
34browser session with screenshots enabled. This does not start a task.
35
36Create a browser session
37
38```bash
39# Requires Bash and jq. Keep the session ID for follow-up requests.
40set -o pipefail
41if ! session_id=$(curl --silent --show-error --fail https://api.openai.com/v1/agents/sessions \
42 -H "OpenAI-Beta: agents=v1" \
43 -H "Authorization: Bearer $OPENAI_API_KEY" \
44 -H "Content-Type: application/json" \
45 -d '{
46 "agent": {
47 "model": "gpt-6-astra",
48 "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
49 "tools": [{ "type": "computer_use", "include_screenshots": true }]
50 },
51 "environment": {
52 "type": "openai_hosted",
53 "desktop": { "enabled": true },
54 "network": { "access": "enabled" }
55 }
56 }' | jq --exit-status --raw-output '.id // empty'); then
57 echo "Session creation failed or its outcome is unknown. Do not retry automatically." >&2
58 exit 1
59fi
60printf 'Session ID: %s\n' "$session_id"
61```
62
63```javascript
64import OpenAI from "openai";
65import { open } from "node:fs/promises";
66
67const client = new OpenAI();
68const session = await client.beta.agents.sessions.create({
69 agent: {
70 model: "gpt-6-astra",
71 instructions:
72 "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
73 tools: [{ type: "computer_use", include_screenshots: true }],
74 },
75 environment: {
76 type: "openai_hosted",
77 desktop: { enabled: true },
78 network: { access: "enabled" },
79 },
80});
81console.log("Session ID:", session.id);
82```
83
84```python
85import base64
86import os
87from pathlib import Path
88
89from openai import OpenAI
90
91client = OpenAI()
92session = client.beta.agents.sessions.create(
93 agent={
94 "model": "gpt-6-astra",
95 "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
96 "tools": [{"type": "computer_use", "include_screenshots": True}],
97 },
98 environment={
99 "type": "openai_hosted",
100 "desktop": {"enabled": True},
101 "network": {"access": "enabled"},
102 },
103)
104print("Session ID:", session.id, flush=True)
105ready_to_delete = False
106```
107
108```go
109import (
110 "bufio"
111 "context"
112 "encoding/base64"
113 "fmt"
114 "os"
115 "strings"
116
117 "github.com/openai/openai-go/v3"
118 "github.com/openai/openai-go/v3/option"
119)
120
121ctx := context.Background()
122client := openai.NewClient()
123session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{
124 Agent: openai.BetaAgentSessionNewParamsAgent{
125 Model: openai.String("gpt-6-astra"),
126 Instructions: openai.String("Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find."),
127 Tools: []openai.AgentToolParamUnion{{
128 OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(true)},
129 }},
130 },
131 Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{
132 Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: openai.Bool(true)},
133 Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"},
134 }},
135})
136if err != nil {
137 return err
138}
139fmt.Println("Session ID:", session.ID)
140```
141
142```java
143import com.openai.client.OpenAIClient;
144import com.openai.client.okhttp.OpenAIOkHttpClient;
145import com.openai.models.beta.agents.AgentSession;
146import com.openai.models.beta.agents.AgentSessionInputMessageParam;
147import com.openai.models.beta.agents.AgentSessionInputParam;
148import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult;
149import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult.Response;
150import com.openai.models.beta.agents.AgentToolParam;
151import com.openai.models.beta.agents.EnvironmentParam;
152import com.openai.models.beta.agents.sessions.SessionCreateParams;
153import com.openai.models.beta.agents.sessions.events.EventCreateParams;
154import com.openai.models.beta.agents.sessions.items.ItemListParams;
155import java.nio.file.Files;
156import java.util.Base64;
157import java.util.HashSet;
158import java.util.List;
159
160var client = OpenAIOkHttpClient.fromEnv();
161var session =
162 client
163 .beta()
164 .agents()
165 .sessions()
166 .create(
167 SessionCreateParams.builder()
168 .agent(
169 SessionCreateParams.Agent.builder()
170 .model("gpt-6-astra")
171 .instructions(
172 "Read public documentation in the browser. Do not sign in or change"
173 + " any website data. Report the page title and URL you find.")
174 .addTool(
175 AgentToolParam.ComputerUse.builder()
176 .includeScreenshots(true)
177 .build())
178 .build())
179 .environment(
180 EnvironmentParam.OpenAIHosted.builder()
181 .desktop(
182 EnvironmentParam.OpenAIHosted.Desktop.builder()
183 .enabled(true)
184 .build())
185 .network(
186 EnvironmentParam.OpenAIHosted.Network.builder()
187 .access(EnvironmentParam.OpenAIHosted.Network.Access.ENABLED)
188 .build())
189 .build())
190 .build());
191System.out.println("Session ID: " + session.id());
192```
193
194```csharp
195using System.ClientModel;
196using System.ClientModel.Primitives;
197using System.Text.Json;
198using OpenAI;
199using OpenAI.Agents;
200#pragma warning disable OPENAI001
201
202string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
203OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };
204AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();
205AgentSession session = await client.CreateAgentSessionAsync(
206 new AgentSessionCreationOptions
207 {
208 Agent = new SessionAgentConfigParam
209 {
210 Model = "gpt-6-astra",
211 Instructions = "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
212 Tools = [new AgentToolConfigParamComputerUse { IncludeScreenshots = true }],
213 },
214 Environment = new EnvironmentParamOpenaiHosted
215 {
216 Desktop = new DesktopParam(true),
217 Network = new NetworkPolicyParam(NetworkAccessParam.Enabled),
218 },
219 }
220);
221Console.WriteLine($"Session ID: {session.Id}");
222```
223
224```ruby
225require "base64"
226require "openai"
227
228client = OpenAI::Client.new
229session = client.beta.agents.sessions.create(
230 agent: {
231 model: "gpt-6-astra",
232 instructions: "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
233 tools: [
234 {
235 type: "computer_use",
236 include_screenshots: true
237 }
238 ]
239 },
240 environment: {
241 type: "openai_hosted",
242 desktop: { enabled: true },
243 network: { access: "enabled" }
244 }
245)
246puts "Session ID: #{session.id}"
247```
248
249
250## Handle origin access
251
252The browser requires the user's approval before accessing each new website
253origin, including public websites. Enabling network access does not approve
254these requests.
255
256To handle an origin approval:
257
2581. On `agent.session.requires_action`, retrieve the session and inspect its current `required_actions`.
2592. Find pending `computer_use_approval_request` entries whose nested `request.type` is `browser_origin_access`.
2603. 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.
261
262<details>
263<summary>**Origin approval does not enforce confirmation before individual actions**</summary>
264
265If your application must guarantee confirmation before purchases, destructive
266changes, or other consequential actions, restrict the hosted browser to
267resources that cannot perform them, or use a browser runtime you control.
268Asking for confirmation through a function tool relies on the agent calling
269that function.
270
271Treat website content as untrusted. It cannot grant permission or override the
272user's instructions. See the [confirmation and consent guidance for a runtime you control](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#handle-user-confirmation-and-consent).
273
274</details>
275
276Define this helper before the task code. It handles origin approvals and cancels
277sign-in requests because this task only reads public pages.
278
279Respond to origin access requests
280
281```bash
282# Run in terminal 2 after the task reports agent.session.requires_action.
283# Reuse the task's session_id and OPENAI_API_KEY.
284curl --silent --show-error --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id" \
285 -H "OpenAI-Beta: agents=v1" \
286 -H "Authorization: Bearer $OPENAI_API_KEY" \
287 | jq '.required_actions[] | select(.type == "computer_use_approval_request")'
288
289# Read request.origin and request.reason before deciding.
290# Only use this response for request.type == "browser_origin_access".
291# Replace REQUEST_ID and choose approve, deny, or cancel.
292curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
293 -H "OpenAI-Beta: agents=v1" \
294 -H "Authorization: Bearer $OPENAI_API_KEY" \
295 -H "Content-Type: application/json" \
296 -d '{
297 "events": [{
298 "type": "agent.session.input.computer_use_approval_request_result",
299 "request_id": "REQUEST_ID",
300 "response": { "type": "browser_origin_access", "decision": "approve" }
301 }]
302 }'
303```
304
305```javascript
306import promptSync from "prompt-sync";
307
308const prompt = promptSync({ sigint: true });
309
310/** @param {OpenAI} client */
311async function respondToOriginApproval(client, sessionId, approval) {
312 const request = approval.request;
313 if (request.type === "browser_origin_access") {
314 console.log("Requested origin:", request.origin);
315 console.log(request.reason ?? "The browser needs access to this origin.");
316 let input;
317 do {
318 input =
319 prompt("Allow access? [approve/deny/cancel, default deny] ")
320 .trim()
321 .toLowerCase() || "deny";
322 } while (!["approve", "deny", "cancel"].includes(input));
323 const decision =
324 input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
325 await client.beta.agents.sessions.events.create(sessionId, {
326 events: [
327 {
328 type: "agent.session.input.computer_use_approval_request_result",
329 request_id: approval.request_id,
330 response: { type: "browser_origin_access", decision },
331 },
332 ],
333 });
334 } else if (request.type === "browser_authentication") {
335 // This public-page task must not sign in.
336 await client.beta.agents.sessions.events.create(sessionId, {
337 events: [
338 {
339 type: "agent.session.input.computer_use_approval_request_result",
340 request_id: approval.request_id,
341 response: { type: "browser_authentication", action: "cancel" },
342 },
343 ],
344 });
345 } else {
346 throw new Error(`Unsupported approval request: ${request.type}`);
347 }
348}
349```
350
351```python
352def respond_to_origin_approval(client, session_id, approval):
353 request = approval.request
354 if request.type == "browser_origin_access":
355 print(request.reason or "The browser needs access to an origin.")
356 print("Origin:", request.origin)
357 while True:
358 decision = (
359 input("Allow this origin? [approve/deny/cancel; default: deny] ")
360 .strip()
361 .lower()
362 or "deny"
363 )
364 if decision in {"approve", "deny", "cancel"}:
365 break
366 print("Enter approve, deny, or cancel.")
367 response = {"type": "browser_origin_access", "decision": decision}
368 elif request.type == "browser_authentication":
369 print("This public-page task does not sign in; cancelling the request.")
370 response = {"type": "browser_authentication", "action": "cancel"}
371 else:
372 raise RuntimeError(f"Unsupported computer-use approval: {request.type}")
373
374 client.beta.agents.sessions.events.create(
375 session_id,
376 events=[
377 {
378 "type": "agent.session.input.computer_use_approval_request_result",
379 "request_id": approval.request_id,
380 "response": response,
381 }
382 ],
383 )
384```
385
386```go
387func respondToOriginApproval(ctx context.Context, client *openai.Client, sessionID string, approval openai.AgentSessionRequiredActionComputerUseApprovalRequest) error {
388 response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{}
389 switch approval.Request.Type {
390 case "browser_origin_access":
391 request := approval.Request.AsBrowserOriginAccess()
392 fmt.Println("Requested origin:", request.Origin)
393 if request.Reason != "" {
394 fmt.Println(request.Reason)
395 }
396 originResponse := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserOriginAccess{Decision: "deny"}
397 reader := bufio.NewReader(os.Stdin)
398 for {
399 fmt.Print("Allow this origin? [approve/deny/cancel; default: deny] ")
400 choice, err := reader.ReadString('\n')
401 if err != nil {
402 return err
403 }
404 switch strings.ToLower(strings.TrimSpace(choice)) {
405 case "approve":
406 originResponse.Decision = "approve"
407 case "", "deny":
408 originResponse.Decision = "deny"
409 case "cancel":
410 originResponse.Decision = "cancel"
411 default:
412 fmt.Println("Enter approve, deny, or cancel.")
413 continue
414 }
415 break
416 }
417 response.OfBrowserOriginAccess = &originResponse
418 case "browser_authentication":
419 fmt.Println("This public-page task does not sign in; cancelling the sign-in request.")
420 response.OfBrowserAuthentication = &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthentication{Action: "cancel"}
421 default:
422 return fmt.Errorf("unsupported computer-use approval: %s", approval.Request.Type)
423 }
424 return client.Beta.Agents.Sessions.Events.New(ctx, sessionID, openai.BetaAgentSessionEventNewParams{
425 Events: []openai.AgentSessionInputParamUnion{{
426 OfParamAgentSessionInputComputerUseApprovalRequestResult: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResult{
427 RequestID: approval.RequestID,
428 Response: response,
429 },
430 }},
431 }, option.WithMaxRetries(0))
432}
433```
434
435```java
436static void respondToOriginApproval(
437 OpenAIClient client,
438 String sessionId,
439 AgentSession.RequiredAction.ComputerUseApprovalRequest approval) {
440 var console = System.console();
441 if (console == null)
442 throw new IllegalStateException("Run this example in an interactive terminal.");
443 var result =
444 AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId());
445 if (approval.request().browserOriginAccess().isPresent()) {
446 var request = approval.request().browserOriginAccess().get();
447 console.printf("Origin: %s%n", request.origin());
448 console.printf("Reason: %s%n", request.reason().orElse("Not supplied"));
449 String decision;
450 while (true) {
451 String input =
452 console.readLine("Allow browser access? [approve/deny/cancel; default deny] ");
453 if (input == null) throw new IllegalStateException("Approval input closed.");
454 decision = input.strip().toLowerCase(java.util.Locale.ROOT);
455 if (decision.isEmpty()) decision = "deny";
456 if (List.of("approve", "deny", "cancel").contains(decision)) break;
457 console.printf("Enter approve, deny, or cancel.%n");
458 }
459 result.response(
460 Response.BrowserOriginAccess.builder()
461 .decision(Response.BrowserOriginAccess.Decision.of(decision))
462 .build());
463 } else if (approval.request().browserAuthentication().isPresent()) {
464 console.printf("Sign-in is outside this public-page task; cancelling the request.%n");
465 result.response(
466 Response.BrowserAuthentication.ofCancel(
467 Response.BrowserAuthentication.Cancel.builder().build()));
468 } else {
469 throw new IllegalStateException("Unsupported computer-use approval request.");
470 }
471 client
472 .withOptions(options -> options.maxRetries(0))
473 .beta()
474 .agents()
475 .sessions()
476 .events()
477 .create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build());
478}
479```
480
481```csharp
482static async Task RespondToOriginApprovalAsync(
483 AgentClient client, string sessionId,
484 SessionRequiredActionResourceComputerUseApprovalRequest approval)
485{
486 if (Console.IsInputRedirected)
487 {
488 throw new InvalidOperationException("Run this example in an interactive terminal.");
489 }
490 ComputerUseApprovalResponseParam response;
491 if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin)
492 {
493 Console.WriteLine($"Origin: {origin.Origin}");
494 Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}");
495 string choice;
496 while (true)
497 {
498 Console.Write("Allow browser access? [approve/deny/cancel; default deny] ");
499 choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant();
500 if (choice.Length == 0) choice = "deny";
501 if (choice is "approve" or "deny" or "cancel") break;
502 Console.WriteLine("Enter approve, deny, or cancel.");
503 }
504 BrowserOriginAccessDecisionParam decision = choice switch
505 {
506 "approve" => BrowserOriginAccessDecisionParam.Approve,
507 "cancel" => BrowserOriginAccessDecisionParam.Cancel,
508 _ => BrowserOriginAccessDecisionParam.Deny,
509 };
510 response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision);
511 }
512 else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication)
513 {
514 Console.WriteLine("Sign-in is outside this public-page task; cancelling the request.");
515 response = new ComputerUseApprovalResponseParamBrowserAuthenticationCancel();
516 }
517 else
518 {
519 throw new InvalidOperationException("Unsupported computer-use approval request.");
520 }
521 await client.CreateAgentSessionEventsAsync(
522 sessionId,
523 new CreateSessionEventsParams(
524 [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, response)]
525 )
526 );
527}
528```
529
530```ruby
531def respond_to_origin_approval(client, session_id, approval)
532 request = approval.request
533 case request.type.to_s
534 when "browser_origin_access"
535 puts request.reason || "The browser needs access to an origin."
536 puts "Origin: #{request.origin}"
537 decision = loop do
538 print "Allow this origin? [approve/deny/cancel; default: deny] "
539 input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.")
540 choice = input.strip.downcase
541 choice = "deny" if choice.empty?
542 break choice if ["approve", "deny", "cancel"].include?(choice)
543
544 puts "Enter approve, deny, or cancel."
545 end
546 response = {
547 type: "browser_origin_access",
548 decision: decision
549 }
550 when "browser_authentication"
551 puts "This public-page task does not sign in; cancelling the request."
552 response = {
553 type: "browser_authentication",
554 action: "cancel"
555 }
556 else
557 raise "Unsupported computer-use approval: #{request.type}"
558 end
559 client.beta.agents.sessions.events.create(
560 session_id,
561 events: [
562 {
563 type: "agent.session.input.computer_use_approval_request_result",
564 request_id: approval.request_id,
565 response: response
566 }
567 ],
568 request_options: { max_retries: 0 }
569 )
570end
571```
572
573
574Keep the stream open and handle every pending approval. Use `request_id` to
575track requests, and remove approval controls when a request is no longer in the
576session's `required_actions`. Cancelling an approval request does not cancel
577the task.
578
579A `202` response means the decision was accepted, not that navigation has
580completed.
581
582## Run a browser task
583
584Ask the agent to find the Agents API quickstart on the public developer site and
585report its title and URL.
586
587Open the event stream before sending the task so your application receives the
588first progress events. Handle [origin approvals](#handle-origin-access) as they
589arrive to let the browser continue.
590
591Send the task and follow its result
592
593```bash
594# Terminal 1: use session_id from the creation request.
595# Keep this stream open. Wait for HTTP 200 before sending input.
596set -o pipefail
597curl --silent --show-error --fail --no-buffer --dump-header - \
598 --suppress-connect-headers \
599 "https://api.openai.com/v1/agents/sessions/$session_id/events" \
600 -H "OpenAI-Beta: agents=v1" \
601 -H "Authorization: Bearer $OPENAI_API_KEY" \
602 -H "Accept: text/event-stream" \
603 | jq --raw-input --unbuffered '
604 if startswith("HTTP/") then .
605 elif startswith("data:") then
606 (ltrimstr("data:") | fromjson?) |
607 if .type == "agent.session.turn.output_text.done" then {type, text}
608 elif .type == "agent.session.requires_action"
609 or .type == "error"
610 or .type == "agent.session.failed"
611 or .type == "agent.session.environment.failed"
612 or (.type | test("^agent.session.turn.(completed|failed|cancelled)$"))
613 then {type, turn_id: .turn.id, subagent_id: .turn.subagent_id}
614 else empty end
615 else empty end'
616
617# Terminal 2: replace sess_123 with the ID printed in terminal 1.
618# Export OPENAI_API_KEY in this terminal too.
619session_id="sess_123"
620curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
621 -H "OpenAI-Beta: agents=v1" \
622 -H "Authorization: Bearer $OPENAI_API_KEY" \
623 -H "Content-Type: application/json" \
624 -d '{
625 "events": [{
626 "type": "agent.session.input.message",
627 "input": [{
628 "role": "user",
629 "content": [{
630 "type": "input_text",
631 "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
632 }]
633 }]
634 }]
635 }'
636```
637
638```javascript
639const events = await client.beta.agents.sessions.events.stream(session.id);
640const handledRequests = new Set();
641let completed = false;
642try {
643 await client.beta.agents.sessions.events.create(session.id, {
644 events: [
645 {
646 type: "agent.session.input.message",
647 input: [
648 {
649 role: "user",
650 content: [
651 {
652 type: "input_text",
653 text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
654 },
655 ],
656 },
657 ],
658 },
659 ],
660 });
661 for await (const event of events) {
662 switch (event.type) {
663 case "agent.session.requires_action": {
664 const current = await client.beta.agents.sessions.retrieve(
665 session.id
666 );
667 for (const approval of current.required_actions) {
668 if (
669 approval.type === "computer_use_approval_request" &&
670 !handledRequests.has(approval.request_id)
671 ) {
672 await respondToOriginApproval(client, session.id, approval);
673 handledRequests.add(approval.request_id);
674 }
675 }
676 break;
677 }
678 case "agent.session.turn.output_text.done":
679 console.log(event.text);
680 break;
681 case "error":
682 throw new Error(event.error.message);
683 case "agent.session.failed":
684 case "agent.session.environment.failed":
685 throw new Error(`Agent lifecycle failure: ${event.type}`);
686 case "agent.session.turn.failed":
687 if (event.turn.subagent_id === null) {
688 throw new Error(event.turn.error?.message ?? "Browser task failed");
689 }
690 break;
691 case "agent.session.turn.cancelled":
692 if (event.turn.subagent_id === null) {
693 throw new Error("Browser task was cancelled");
694 }
695 break;
696 case "agent.session.turn.completed":
697 if (event.turn.subagent_id === null) completed = true;
698 break;
699 }
700 if (completed) break;
701 }
702 if (!completed) {
703 throw new Error("Stream closed before the browser task finished");
704 }
705 console.log();
706} finally {
707 events.controller.abort();
708}
709```
710
711```python
712handled_requests = set()
713completed = False
714with client.beta.agents.sessions.events.stream(session.id) as events:
715 client.beta.agents.sessions.events.create(
716 session.id,
717 events=[
718 {
719 "type": "agent.session.input.message",
720 "input": [
721 {
722 "role": "user",
723 "content": [
724 {
725 "type": "input_text",
726 "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
727 }
728 ],
729 }
730 ],
731 }
732 ],
733 )
734 for event in events:
735 if event.type == "agent.session.requires_action":
736 current = client.beta.agents.sessions.retrieve(session.id)
737 for approval in current.required_actions:
738 if (
739 approval.type == "computer_use_approval_request"
740 and approval.request_id not in handled_requests
741 ):
742 respond_to_origin_approval(client, session.id, approval)
743 handled_requests.add(approval.request_id)
744 elif event.type == "agent.session.turn.output_text.done":
745 print(event.text, flush=True)
746 elif event.type == "agent.session.turn.completed":
747 if event.turn.subagent_id is None:
748 completed = True
749 print()
750 break
751 elif event.type in {
752 "agent.session.turn.failed",
753 "agent.session.turn.cancelled",
754 }:
755 if event.turn.subagent_id is None:
756 raise RuntimeError(f"Browser task ended: {event.type}")
757 elif event.type == "error":
758 raise RuntimeError(event.error.message)
759 elif event.type in {
760 "agent.session.failed",
761 "agent.session.environment.failed",
762 }:
763 raise RuntimeError(f"Session failed: {event.type}")
764 else:
765 raise RuntimeError("Stream closed before the browser task finished.")
766```
767
768```go
769handledRequests := map[string]bool{}
770 events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID)
771 defer events.Close()
772 if err := events.Err(); err != nil {
773 return err
774 }
775 err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{
776 Events: []openai.AgentSessionInputParamUnion{{
777 OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{
778 Input: []openai.AgentSessionInputMessageParam{{
779 Role: "user",
780 Content: []openai.InputContentParamUnion{{
781 OfParamInputText: &openai.InputContentParamInputText{
782 Text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
783 },
784 }},
785 }},
786 },
787 }},
788 })
789 if err != nil {
790 return err
791 }
792 completed := false
793eventLoop:
794 for events.Next() {
795 event := events.Current()
796 switch event.Type {
797 case "agent.session.requires_action":
798 current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID)
799 if err != nil {
800 return err
801 }
802 for _, action := range current.RequiredActions {
803 if action.Type != "computer_use_approval_request" {
804 continue
805 }
806 approval := action.AsComputerUseApprovalRequest()
807 if handledRequests[approval.RequestID] {
808 continue
809 }
810 if err := respondToOriginApproval(ctx, &client, session.ID, approval); err != nil {
811 return err
812 }
813 handledRequests[approval.RequestID] = true
814 }
815 case "agent.session.turn.output_text.done":
816 fmt.Println(event.Text)
817 case "agent.session.turn.completed":
818 if event.Turn.SubagentID == "" {
819 completed = true
820 break eventLoop
821 }
822 case "agent.session.turn.failed", "agent.session.turn.cancelled":
823 if event.Turn.SubagentID == "" {
824 return fmt.Errorf("browser task ended: %s", event.Type)
825 }
826 case "error":
827 return fmt.Errorf("agent error: %s", event.Error.Message)
828 case "agent.session.failed", "agent.session.environment.failed":
829 return fmt.Errorf("session failed: %s", event.Type)
830 }
831 }
832 if err := events.Err(); err != nil {
833 return err
834 }
835 if !completed {
836 return fmt.Errorf("stream closed before the browser task finished")
837 }
838```
839
840```java
841var handledRequests = new HashSet<String>();
842try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) {
843 client
844 .beta()
845 .agents()
846 .sessions()
847 .events()
848 .create(
849 EventCreateParams.builder()
850 .sessionId(session.id())
851 .addEvent(
852 AgentSessionInputParam.AgentSessionInputMessage.builder()
853 .addInput(
854 AgentSessionInputMessageParam.builder()
855 .addInputTextContent(
856 "Open https://developers.openai.com in the browser. Find"
857 + " the Agents API quickstart, then report its page"
858 + " title and URL.")
859 .build())
860 .build())
861 .build());
862 boolean completed = false;
863 var iterator = events.stream().iterator();
864 while (iterator.hasNext()) {
865 var event = iterator.next();
866 if (event.requiresAction().isPresent()) {
867 var current = client.beta().agents().sessions().retrieve(session.id());
868 for (var action : current.requiredActions()) {
869 if (action.computerUseApprovalRequest().isEmpty()) continue;
870 var approval = action.computerUseApprovalRequest().get();
871 if (!handledRequests.contains(approval.requestId())) {
872 respondToOriginApproval(client, session.id(), approval);
873 handledRequests.add(approval.requestId());
874 }
875 }
876 }
877 event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text()));
878 if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
879 completed = true;
880 break;
881 }
882 if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent()
883 || event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
884 throw new IllegalStateException("Browser task failed or was cancelled.");
885 }
886 if (event.error().isPresent()) {
887 throw new IllegalStateException(event.error().get().error().message());
888 }
889 if (event.failed().isPresent() || event.environmentFailed().isPresent()) {
890 throw new IllegalStateException("The browser session failed.");
891 }
892 }
893 if (!completed) {
894 throw new IllegalStateException("Stream closed before the browser task finished.");
895 }
896}
897```
898
899```csharp
900HashSet<string> handledRequests = new(StringComparer.Ordinal);
901await using var events = await client.GetAgentSessionEventsAsync(session.Id);
902await client.CreateAgentSessionEventsAsync(
903 session.Id,
904 new CreateSessionEventsParams(
905 [
906 new SessionInputParamAgentSessionInputMessage(
907 [
908 new InputMessageParam(
909 [new InputContentParamInputText("Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.")]
910 ),
911 ]
912 ),
913 ]
914 )
915);
916bool completed = false;
917await foreach (var message in events)
918{
919 using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory());
920 JsonElement current = document.RootElement;
921 string? type = current.GetProperty("type").GetString();
922 if (type == "agent.session.requires_action")
923 {
924 AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id);
925 foreach (SessionRequiredActionResource action in latest.RequiredActions)
926 {
927 if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval
928 && !handledRequests.Contains(approval.RequestId))
929 {
930 await RespondToOriginApprovalAsync(client, session.Id, approval);
931 handledRequests.Add(approval.RequestId);
932 }
933 }
934 }
935 else if (type == "agent.session.turn.output_text.done")
936 {
937 Console.WriteLine(current.GetProperty("text").GetString());
938 }
939 else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled")
940 {
941 JsonElement turn = current.GetProperty("turn");
942 if (turn.TryGetProperty("subagent_id", out JsonElement subagent)
943 && subagent.ValueKind != JsonValueKind.Null)
944 {
945 continue;
946 }
947 if (type != "agent.session.turn.completed")
948 {
949 throw new InvalidOperationException($"Browser task ended: {type}");
950 }
951 completed = true;
952 break;
953 }
954 else if (type == "error")
955 {
956 throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString());
957 }
958 else if (type is "agent.session.failed" or "agent.session.environment.failed")
959 {
960 throw new InvalidOperationException($"Session failed: {type}");
961 }
962}
963if (!completed)
964{
965 throw new InvalidOperationException("Stream closed before the browser task finished.");
966}
967```
968
969```ruby
970handled_requests = Set.new
971events = client.beta.agents.sessions.events.stream_streaming(session.id)
972begin
973 client.beta.agents.sessions.events.create(
974 session.id,
975 events: [
976 {
977 type: "agent.session.input.message",
978 input: [
979 {
980 role: "user",
981 content: [
982 {
983 type: "input_text",
984 text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
985 }
986 ]
987 }
988 ]
989 }
990 ]
991 )
992 completed = events.any? do |event|
993 case event
994 when OpenAI::Beta::AgentSessionRequiresActionEvent
995 current = client.beta.agents.sessions.retrieve(session.id)
996 current.required_actions.each do |approval|
997 next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest)
998 next if handled_requests.include?(approval.request_id)
999
1000 respond_to_origin_approval(client, session.id, approval)
1001 handled_requests.add(approval.request_id)
1002 end
1003 false
1004 when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent
1005 puts event.text
1006 when OpenAI::Beta::AgentSessionTurnCompletedEvent
1007 event.turn.subagent_id.nil?
1008 when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent
1009 raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil?
1010 when OpenAI::Beta::AgentSessionErrorEvent
1011 raise event.error.message
1012 when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent
1013 raise "Session failed: #{event.type}"
1014 else
1015 false
1016 end
1017 end
1018 raise "Stream closed before the browser task finished." unless completed
1019ensure
1020 events.close
1021end
1022```
1023
1024
1025The example prints the agent's answer. Check that it includes the title and URL
1026of the quickstart.
1027
1028Closing the event stream does not stop the task. To stop it, cancel the turn.
1029For connection failures or uncertain outcomes, follow
1030[recovery guidance](#recover-approval-handling). The
1031[expanded cURL example](#request-handling-reference) includes transport and error
1032diagnostics.
1033
1034## Follow browser activity
1035
1036Browser operations appear as `computer_use_call` items in session output. Streamed
1037activity and saved session history use the same item shape.
1038
1039| Field | Meaning |
1040| --------- | ------------------------------------------------------ |
1041| `id` | The activity item's identifier. |
1042| `turn_id` | The turn that produced the activity. |
1043| `title` | A description of the browser activity, or `null`. |
1044| `status` | `in_progress`, `completed`, `failed`, or `incomplete`. |
1045| `output` | Screenshot output, when available. |
1046
1047Use the title and status to show progress in your application. A browser activity
1048item describes a tool operation; it's not the agent's final answer or the
1049completion status of the whole turn. See
1050[Events and items](https://developers.openai.com/api/docs/guides/agents-api/sessions/events) for the session event model.
1051
1052### Include screenshots
1053
1054To display the browser's progress in your application, set `include_screenshots`
1055to `true` on the `computer_use` tool. Screenshots are excluded from API output
1056by default; the agent can still observe them.
1057
1058Each browser operation returns its last emitted screenshot in `output`, when
1059available:
1060
1061```json
1062{
1063 "type": "computer_screenshot",
1064 "image_url": "data:image/jpeg;base64,..."
1065}
1066```
1067
1068Use `image_url` to render the screenshot. Some operations return `output: null`,
1069even with screenshots enabled, so your application should handle activity items
1070without an image.
1071
1072Screenshots can contain sensitive page or account data. Show them only to
1073 authorized users and keep them out of application logs.
1074
1075Retrieve saved browser activity after the turn completes. The SDK examples save
1076the latest available screenshot to `browser-screenshot.jpg`.
1077
1078Read browser activity
1079
1080```bash
1081# Save the first page privately; image data stays out of terminal output.
1082activity_file=$(mktemp)
1083curl --silent --show-error --fail-with-body --get \
1084 "https://api.openai.com/v1/agents/sessions/$session_id/items" \
1085 -H "OpenAI-Beta: agents=v1" \
1086 -H "Authorization: Bearer $OPENAI_API_KEY" \
1087 --data-urlencode "order=asc" --data-urlencode "limit=100" \
1088 --output "$activity_file"
1089
1090jq '.data[] | select(.type == "computer_use_call") |
1091 {id, title: (.title // "Browser activity"), status,
1092 has_screenshot: (.output != null)}' "$activity_file"
1093jq '{has_more, last_id}' "$activity_file"
1094printf 'Saved activity JSON: %s\n' "$activity_file"
1095
1096# If has_more is true, repeat the GET with --data-urlencode "after=LAST_ID".
1097# Use the returned last_id and keep order=asc on every page.
1098```
1099
1100```javascript
1101let screenshot;
1102for await (const item of client.beta.agents.sessions.items.list(session.id, {
1103 order: "asc",
1104 limit: 100,
1105})) {
1106 if (item.type !== "computer_use_call") continue;
1107 console.log(item.title ?? "Browser activity", item.status);
1108 const output = item.output;
1109 if (
1110 output?.type === "computer_screenshot" &&
1111 output.image_url.startsWith("data:image/jpeg;base64,")
1112 ) {
1113 screenshot = Buffer.from(output.image_url.split(",", 2)[1], "base64");
1114 }
1115}
1116if (screenshot) {
1117 const file = await open("browser-screenshot.jpg", "wx", 0o600);
1118 try {
1119 await file.writeFile(screenshot);
1120 } finally {
1121 await file.close();
1122 }
1123 console.log("Saved browser-screenshot.jpg");
1124} else {
1125 console.log("No browser screenshot was returned.");
1126}
1127```
1128
1129```python
1130last_screenshot = None
1131for item in client.beta.agents.sessions.items.list(
1132 session.id, order="asc", limit=100
1133):
1134 if item.type != "computer_use_call":
1135 continue
1136 print(item.title or "Browser activity", item.status)
1137 output = getattr(item, "output", None)
1138 if output is not None and output.type == "computer_screenshot":
1139 prefix = "data:image/jpeg;base64,"
1140 if output.image_url.startswith(prefix):
1141 last_screenshot = base64.b64decode(
1142 output.image_url[len(prefix) :], validate=True
1143 )
1144
1145if last_screenshot is not None:
1146 screenshot_path = Path("browser-screenshot.jpg")
1147 descriptor = os.open(
1148 screenshot_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600
1149 )
1150 with os.fdopen(descriptor, "wb") as screenshot_file:
1151 screenshot_file.write(last_screenshot)
1152 print("Saved screenshot:", screenshot_path)
1153else:
1154 print("No screenshot was returned.")
1155ready_to_delete = completed
1156```
1157
1158```go
1159var lastScreenshot []byte
1160items := client.Beta.Agents.Sessions.Items.ListAutoPaging(ctx, session.ID, openai.BetaAgentSessionItemListParams{
1161 Order: "asc", Limit: openai.Int(100),
1162})
1163for items.Next() {
1164 item := items.Current()
1165 if item.Type != "computer_use_call" {
1166 continue
1167 }
1168 activity := item.AsComputerUseCall()
1169 title := activity.Title
1170 if title == "" {
1171 title = "Browser activity"
1172 }
1173 fmt.Println(title, activity.Status)
1174 if !activity.JSON.Output.Valid() || activity.Output.Type != "computer_screenshot" {
1175 continue
1176 }
1177 const prefix = "data:image/jpeg;base64,"
1178 if strings.HasPrefix(activity.Output.ImageURL, prefix) {
1179 lastScreenshot, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(activity.Output.ImageURL, prefix))
1180 if err != nil {
1181 return err
1182 }
1183 }
1184}
1185if err := items.Err(); err != nil {
1186 return err
1187}
1188if lastScreenshot != nil {
1189 file, err := os.OpenFile("browser-screenshot.jpg", os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
1190 if err != nil {
1191 return err
1192 }
1193 defer file.Close()
1194 if err := file.Chmod(0o600); err != nil {
1195 return err
1196 }
1197 if _, err := file.Write(lastScreenshot); err != nil {
1198 return err
1199 }
1200 fmt.Println("Saved screenshot: browser-screenshot.jpg")
1201} else {
1202 fmt.Println("No screenshot was returned.")
1203}
1204readyToDelete = true
1205```
1206
1207```java
1208byte[] lastScreenshot = null;
1209var items =
1210 client
1211 .beta()
1212 .agents()
1213 .sessions()
1214 .items()
1215 .list(
1216 ItemListParams.builder()
1217 .sessionId(session.id())
1218 .order(ItemListParams.Order.ASC)
1219 .limit(100L)
1220 .build());
1221for (var item : items.autoPager()) {
1222 if (item.computerUseCall().isEmpty()) continue;
1223 var activity = item.computerUseCall().get();
1224 System.out.println(activity.title().orElse("Browser activity") + " " + activity.status());
1225 var output = activity.output();
1226 if (output.isPresent()) {
1227 String imageUrl = output.get().imageUrl();
1228 String prefix = "data:image/jpeg;base64,";
1229 if (imageUrl.startsWith(prefix)) {
1230 lastScreenshot = Base64.getDecoder().decode(imageUrl.substring(prefix.length()));
1231 }
1232 }
1233}
1234if (lastScreenshot != null) {
1235 var screenshotPath = Files.createTempFile("browser-screenshot-", ".jpg");
1236 Files.write(screenshotPath, lastScreenshot);
1237 System.out.println("Saved screenshot: " + screenshotPath);
1238} else {
1239 System.out.println("No screenshot was returned.");
1240}
1241```
1242
1243```csharp
1244byte[]? lastScreenshot = null;
1245await foreach (AgentSessionItem item in client.GetAgentSessionItemsAsync(
1246 session.Id, limit: 100, order: AgentSessionItemCollectionOrder.Ascending))
1247{
1248 if (item is not ComputerUseCallItemResource activity)
1249 {
1250 continue;
1251 }
1252 Console.WriteLine($"{activity.Title ?? "Browser activity"}: {activity.Status}");
1253 if (activity.Output is ComputerUseOutputResourceComputerScreenshot screenshot)
1254 {
1255 const string prefix = "data:image/jpeg;base64,";
1256 if (screenshot.ImageUrl.StartsWith(prefix, StringComparison.Ordinal))
1257 {
1258 lastScreenshot = Convert.FromBase64String(screenshot.ImageUrl[prefix.Length..]);
1259 }
1260 }
1261}
1262if (lastScreenshot is not null)
1263{
1264 FileStreamOptions fileOptions = new()
1265 {
1266 Mode = FileMode.CreateNew,
1267 Access = FileAccess.Write,
1268 Share = FileShare.None,
1269 };
1270 if (!OperatingSystem.IsWindows())
1271 {
1272 fileOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
1273 }
1274 await using FileStream file = new("browser-screenshot.jpg", fileOptions);
1275 if (!OperatingSystem.IsWindows())
1276 {
1277 File.SetUnixFileMode(file.SafeFileHandle, UnixFileMode.UserRead | UnixFileMode.UserWrite);
1278 }
1279 await file.WriteAsync(lastScreenshot);
1280 Console.WriteLine("Saved screenshot: browser-screenshot.jpg");
1281}
1282else
1283{
1284 Console.WriteLine("No screenshot was returned.");
1285}
1286```
1287
1288```ruby
1289last_screenshot = String.new(encoding: Encoding::BINARY)
1290items = client.beta.agents.sessions.items.list(
1291 session.id,
1292 order: "asc",
1293 limit: 100
1294)
1295items.auto_paging_each do |item|
1296 next unless item.is_a?(OpenAI::Beta::AgentComputerUseCallItem)
1297
1298 puts "#{item.title || "Browser activity"}: #{item.status}"
1299 output = item.output
1300 if output&.type.to_s == "computer_screenshot"
1301 prefix = "data:image/jpeg;base64,"
1302 if output.image_url.start_with?(prefix)
1303 last_screenshot.replace(Base64.strict_decode64(output.image_url.delete_prefix(prefix)))
1304 end
1305 end
1306end
1307
1308if last_screenshot.empty?
1309 puts "No screenshot was returned."
1310else
1311 screenshot_path = "browser-screenshot.jpg"
1312 File.open(screenshot_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
1313 file.chmod(0o600)
1314 file.binmode
1315 file.write(last_screenshot)
1316 end
1317 puts "Saved screenshot: #{screenshot_path}"
1318end
1319```
1320
1321
1322The SDK examples do not overwrite existing files. Move or remove
1323`browser-screenshot.jpg` before running them again.
1324
1325## Handle sign-in
1326
1327Tasks such as reading issues in a private GitHub repository require an
1328authenticated browser. Your application handles sign-in so users can choose a
1329login method and enter credentials outside the chat.
1330
1331Sign-in can involve several requests. For example, a site might ask the user to
1332choose email sign-in, enter an email address, and then enter a verification code.
1333Build your UI from each request's login methods and fields.
1334
1335Only the main agent can request browser authentication;
1336 [subagents](https://developers.openai.com/api/docs/guides/agents-api/multi-agent) cannot. This flow
1337 supports email addresses, passwords, and verification codes, but not passkeys
1338 or QR-code sign-in. Sites that require an unsupported method cannot complete
1339 sign-in through this flow.
1340
1341Keep the task's event stream open and handle
1342[origin approvals](#handle-origin-access) as they arrive. On
1343`agent.session.requires_action`, retrieve the session and look in its current
1344`required_actions` for `computer_use_approval_request` entries whose nested
1345`request.type` is `browser_authentication`.
1346
1347Use the nested `request` to render your sign-in UI:
1348
1349| Field | How to use it |
1350| ------------------- | ----------------------------------------------------------------------------------------------------------- |
1351| `reason` | Explain why input is needed, if provided. Can be `null`. |
1352| `credential_origin` | Show the destination the credentials are for. Can be `null`. |
1353| `fields` | Render inputs using each field's `id`, `label`, `type`, and `required` values. Can be empty. |
1354| `options` | Show the available login methods. Each option has an `id`, `label`, and `field_ids` identifying its inputs. |
1355
1356If the request offers login methods, let the user choose one and show its
1357associated fields. Otherwise, show the request's fields directly.
1358
1359Ask users to enter credentials only for a destination they can verify. If the
1360 credential origin is missing or unfamiliar and they cannot verify it
1361 independently, cancel the authentication request.
1362
1363[Submit the user's input](#return-the-users-input) using the outer action's
1364`request_id`, or [cancel the authentication request](#let-the-user-cancel) if they
1365decline. Continue handling requests as they arrive. Submitting a response does
1366not establish that sign-in succeeded; follow the task through completion and
1367check its result.
1368
1369### Example: Choose a method, then enter a code
1370
1371A site offering email-code and password sign-in might first ask the user to
1372choose a method, without requesting any fields:
1373
1374Choose a sign-in method
1375
1376```json
1377{
1378 "type": "computer_use_approval_request",
1379 "turn_id": "turn_example",
1380 "request_id": "request_choose_method",
1381 "request": {
1382 "type": "browser_authentication",
1383 "reason": "Choose how to sign in to the issue tracker",
1384 "credential_origin": "https://issues.example.com",
1385 "fields": [],
1386 "options": [
1387 { "id": "email_code", "label": "Email me a code", "field_ids": [] },
1388 { "id": "password", "label": "Use a password", "field_ids": [] }
1389 ]
1390 }
1391}
1392```
1393
1394
1395If the user chooses email-code sign-in, submit `selected_option: "email_code"`
1396with `fields: []` using this request's `request_id`. The site may then request an
1397email address and verification code in separate requests. Render each new request
1398using its own fields and IDs, and include `selected_option` only when that request
1399offers options.
1400
1401### Return the user's input
1402
1403When the user completes a sign-in request, send their response through the
1404[session events endpoint](https://developers.openai.com/api/reference/resources/beta/subresources/agents/subresources/sessions/subresources/events/methods/create).
1405Set `action` to `submit` and use the request and field IDs from the pending
1406approval. For example, a response to a request for an email address looks like
1407this:
1408
1409```json
1410{
1411 "events": [
1412 {
1413 "type": "agent.session.input.computer_use_approval_request_result",
1414 "request_id": "REQUEST_ID",
1415 "response": {
1416 "type": "browser_authentication",
1417 "action": "submit",
1418 "fields": [{ "field_id": "email", "value": "USER_ENTERED_VALUE" }]
1419 }
1420 }
1421 ]
1422}
1423```
1424
1425If the request offers login methods, include the chosen method's ID in
1426`response.selected_option`. Submit only the fields listed in that method's
1427`field_ids`, with a nonempty value for each required field. If the method has no
1428fields, send `fields: []`.
1429
1430If no login methods are offered, omit `selected_option` and submit the request's
1431fields directly. When the request lists fields, include at least one, even if all
1432are optional.
1433
1434Send sign-in values only through this dedicated event. Submitted values stay
1435 outside the agent's model input and are omitted from authentication response
1436 items in session history.
1437
1438Treat every value, including email addresses, as sensitive. Mask entered values,
1439keep them out of logs, analytics, and saved UI state, and clear the form after
1440submission. Do not send credentials in ordinary messages or function-tool
1441results.
1442
1443Omit `turn_id` from the submission event. See
1444[authentication submission limits](#authentication-submission-limits) for field
1445and payload constraints.
1446
1447A `202` response with an empty body confirms that the submission was accepted,
1448not that sign-in succeeded. Continue following session events for further
1449requests or resumed work. Disable automatic HTTP or SDK retries for credential
1450submissions. If you're unsure whether a submission was accepted,
1451[refresh the session before continuing](#recover-approval-handling).
1452
1453### Let the user cancel
1454
1455If the user declines to sign in, respond to the pending request with
1456`action: "cancel"`. Use its `request_id` and omit `fields` and `selected_option`:
1457
1458```json
1459{
1460 "events": [
1461 {
1462 "type": "agent.session.input.computer_use_approval_request_result",
1463 "request_id": "REQUEST_ID",
1464 "response": { "type": "browser_authentication", "action": "cancel" }
1465 }
1466 ]
1467}
1468```
1469
1470This cancels the authentication request. To stop the task itself, cancel the turn.
1471
1472### Run an authenticated browser task
1473
1474Your application handles origin approvals and sign-in requests while following
1475session events. Define the helper below before running the task. It shows the
1476destination, collects input with entered values hidden, and submits the response.
1477If the user declines, it cancels the sign-in request.
1478
1479Handle browser approvals and sign-in
1480
1481```bash
1482# Run in a second terminal when the private task requires input.
1483# Set session_id to that task's actual session ID first.
1484curl --silent --show-error --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id" \
1485 -H "OpenAI-Beta: agents=v1" \
1486 -H "Authorization: Bearer $OPENAI_API_KEY" \
1487 | jq '.required_actions[] | select(.type == "computer_use_approval_request")'
1488
1489# Save a submission or cancellation payload from the preceding sections
1490# to browser-auth-response.json using the pending request and field IDs.
1491# Restrict file access to your user; remove it after submission.
1492curl --fail-with-body --retry 0 "https://api.openai.com/v1/agents/sessions/$session_id/events" \
1493 -H "OpenAI-Beta: agents=v1" \
1494 -H "Authorization: Bearer $OPENAI_API_KEY" \
1495 -H "Content-Type: application/json" \
1496 --data-binary @browser-auth-response.json
1497```
1498
1499```javascript
1500import promptSync from "prompt-sync";
1501
1502const prompt = promptSync({ sigint: true });
1503
1504/** @param {OpenAI} client */
1505async function respondToComputerUseApproval(client, sessionId, approval) {
1506 const request = approval.request;
1507 if (request.type === "browser_origin_access") {
1508 console.log("Requested origin:", request.origin);
1509 console.log(request.reason ?? "The browser needs access to this origin.");
1510 let input;
1511 do {
1512 input =
1513 prompt("Allow access? [approve/deny/cancel, default deny] ")
1514 .trim()
1515 .toLowerCase() || "deny";
1516 } while (!["approve", "deny", "cancel"].includes(input));
1517 const decision =
1518 input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
1519 await client.beta.agents.sessions.events.create(sessionId, {
1520 events: [
1521 {
1522 type: "agent.session.input.computer_use_approval_request_result",
1523 request_id: approval.request_id,
1524 response: { type: "browser_origin_access", decision },
1525 },
1526 ],
1527 });
1528 return;
1529 }
1530 if (request.type !== "browser_authentication") {
1531 throw new Error(`Unsupported approval request: ${request.type}`);
1532 }
1533 async function cancelSignIn() {
1534 await client.beta.agents.sessions.events.create(
1535 sessionId,
1536 {
1537 events: [
1538 {
1539 type: "agent.session.input.computer_use_approval_request_result",
1540 request_id: approval.request_id,
1541 response: { type: "browser_authentication", action: "cancel" },
1542 },
1543 ],
1544 },
1545 { maxRetries: 0 }
1546 );
1547 }
1548 console.log(request.reason ?? "Sign in to continue");
1549 console.log(
1550 "Credential origin:",
1551 request.credential_origin ?? "Not supplied"
1552 );
1553 const consent = prompt("Have you verified the sign-in destination? [y/N] ")
1554 .trim()
1555 .toLowerCase();
1556 if (!["y", "yes"].includes(consent)) {
1557 await cancelSignIn();
1558 return;
1559 }
1560
1561 let selectedOption;
1562 let activeFields = request.fields;
1563 if (request.options.length > 0) {
1564 request.options.forEach((option, index) => {
1565 console.log(`${index + 1}. ${option.label}`);
1566 });
1567 let choice;
1568 do {
1569 const input = prompt("Choose a sign-in method, or enter cancel: ")
1570 .trim()
1571 .toLowerCase();
1572 if (input === "cancel") {
1573 await cancelSignIn();
1574 return;
1575 }
1576 choice = Number(input);
1577 } while (
1578 !Number.isInteger(choice) ||
1579 choice < 1 ||
1580 choice > request.options.length
1581 );
1582 selectedOption = request.options[choice - 1];
1583 activeFields = request.fields.filter((field) =>
1584 selectedOption.field_ids.includes(field.id)
1585 );
1586 }
1587
1588 const fields = [];
1589 do {
1590 for (const field of activeFields) {
1591 while (true) {
1592 const value = prompt.hide(
1593 `${field.label}${field.required ? "" : " (optional)"} (leave blank for options): `
1594 );
1595 if (value.length > 0) {
1596 fields.push({ field_id: field.id, value });
1597 break;
1598 }
1599 let action;
1600 do {
1601 action = prompt(
1602 field.required
1603 ? "Enter a value or cancel sign-in? [enter/cancel, default enter] "
1604 : "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
1605 )
1606 .trim()
1607 .toLowerCase();
1608 } while (
1609 ![
1610 "",
1611 "enter",
1612 "cancel",
1613 ...(field.required ? [] : ["skip"]),
1614 ].includes(action)
1615 );
1616 if (action === "cancel") {
1617 await cancelSignIn();
1618 return;
1619 }
1620 if (!field.required && (action === "" || action === "skip")) break;
1621 }
1622 }
1623 if (!selectedOption && activeFields.length > 0 && fields.length === 0) {
1624 console.log(
1625 "This form requires at least one field. Enter a value or cancel sign-in."
1626 );
1627 }
1628 } while (!selectedOption && activeFields.length > 0 && fields.length === 0);
1629 await client.beta.agents.sessions.events.create(
1630 sessionId,
1631 {
1632 events: [
1633 {
1634 type: "agent.session.input.computer_use_approval_request_result",
1635 request_id: approval.request_id,
1636 response: {
1637 type: "browser_authentication",
1638 action: "submit",
1639 fields,
1640 ...(selectedOption ? { selected_option: selectedOption.id } : {}),
1641 },
1642 },
1643 ],
1644 },
1645 { maxRetries: 0 }
1646 );
1647}
1648```
1649
1650```python
1651from getpass import getpass
1652
1653
1654def read_authentication_response(request):
1655 cancel = {"type": "browser_authentication", "action": "cancel"}
1656 print(request.reason or "The agent needs you to sign in.")
1657 print("Credential origin:", request.credential_origin or "Not supplied")
1658 consent = input("Have you verified the sign-in destination? [y/N] ")
1659 if consent.strip().lower() not in {"y", "yes"}:
1660 return cancel
1661
1662 selected_option = None
1663 active_fields = request.fields
1664 if request.options:
1665 for index, option in enumerate(request.options, start=1):
1666 print(f"{index}. {option.label}")
1667 while True:
1668 choice = input("Choose a sign-in method, or enter cancel: ").strip().lower()
1669 if choice == "cancel":
1670 return cancel
1671 if choice.isdigit() and 1 <= int(choice) <= len(request.options):
1672 option = request.options[int(choice) - 1]
1673 break
1674 print("Enter a method number from the list, or cancel.")
1675 selected_option = option.id
1676 fields_by_id = {field.id: field for field in request.fields}
1677 active_fields = [fields_by_id[field_id] for field_id in option.field_ids]
1678
1679 values = []
1680 while True:
1681 for field in active_fields:
1682 while True:
1683 label = field.label if field.required else f"{field.label} (optional)"
1684 value = getpass(f"{label} (leave blank for options): ")
1685 if value:
1686 values.append({"field_id": field.id, "value": value})
1687 break
1688 choices = {"", "enter", "cancel"}
1689 if field.required:
1690 question = "Enter a value or cancel sign-in? [enter/cancel, default enter] "
1691 else:
1692 choices.add("skip")
1693 question = "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
1694 while True:
1695 action = input(question).strip().lower()
1696 if action in choices:
1697 break
1698 if action == "cancel":
1699 return cancel
1700 if not field.required and action in {"", "skip"}:
1701 break
1702 if selected_option is not None or not active_fields or values:
1703 break
1704 print("This form requires at least one field. Enter a value or cancel sign-in.")
1705
1706 response = {
1707 "type": "browser_authentication",
1708 "action": "submit",
1709 "fields": values,
1710 }
1711 if selected_option is not None:
1712 response["selected_option"] = selected_option
1713 return response
1714
1715
1716def respond_to_computer_use_approval(client, session_id, approval):
1717 request = approval.request
1718 approval_client = client
1719 if request.type == "browser_origin_access":
1720 print(request.reason or "The browser needs access to an origin.")
1721 print("Origin:", request.origin)
1722 while True:
1723 decision = (
1724 input("Allow this origin? [approve/deny/cancel; default: deny] ")
1725 .strip()
1726 .lower()
1727 or "deny"
1728 )
1729 if decision in {"approve", "deny", "cancel"}:
1730 break
1731 print("Enter approve, deny, or cancel.")
1732 response = {"type": "browser_origin_access", "decision": decision}
1733 elif request.type == "browser_authentication":
1734 response = read_authentication_response(request)
1735 approval_client = client.with_options(max_retries=0)
1736 else:
1737 raise RuntimeError(f"Unsupported computer-use approval: {request.type}")
1738
1739 approval_client.beta.agents.sessions.events.create(
1740 session_id,
1741 events=[
1742 {
1743 "type": "agent.session.input.computer_use_approval_request_result",
1744 "request_id": approval.request_id,
1745 "response": response,
1746 }
1747 ],
1748 )
1749 # Admission does not establish sign-in or navigation success; keep reading events.
1750```
1751
1752```go
1753import (
1754 "context"
1755 "fmt"
1756 "io"
1757 "os"
1758 "strconv"
1759 "strings"
1760
1761 "github.com/openai/openai-go/v3"
1762 "github.com/openai/openai-go/v3/option"
1763 "golang.org/x/term"
1764)
1765
1766func respondToComputerUseApproval(ctx context.Context, client *openai.Client, sessionID string, approval openai.AgentSessionRequiredActionComputerUseApprovalRequest) error {
1767 send := func(response openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion) error {
1768 return client.Beta.Agents.Sessions.Events.New(ctx, sessionID, openai.BetaAgentSessionEventNewParams{
1769 Events: []openai.AgentSessionInputParamUnion{{
1770 OfParamAgentSessionInputComputerUseApprovalRequestResult: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResult{
1771 RequestID: approval.RequestID,
1772 Response: response,
1773 },
1774 }},
1775 }, option.WithMaxRetries(0))
1776 }
1777 readLine := func(prompt string) (string, error) {
1778 fmt.Print(prompt)
1779 var value strings.Builder
1780 var input [1]byte
1781 for {
1782 if _, err := io.ReadFull(os.Stdin, input[:]); err != nil {
1783 return "", err
1784 }
1785 if input[0] == '\n' {
1786 return strings.TrimSpace(value.String()), nil
1787 }
1788 value.WriteByte(input[0])
1789 }
1790 }
1791 cancelAuthentication := func() error {
1792 return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{
1793 OfBrowserAuthentication: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthentication{Action: "cancel"},
1794 })
1795 }
1796 switch approval.Request.Type {
1797 case "browser_origin_access":
1798 request := approval.Request.AsBrowserOriginAccess()
1799 fmt.Println("Requested origin:", request.Origin)
1800 if request.Reason != "" {
1801 fmt.Println(request.Reason)
1802 }
1803 response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserOriginAccess{Decision: "deny"}
1804 for {
1805 choice, err := readLine("Allow this origin? [approve/deny/cancel; default: deny] ")
1806 if err != nil {
1807 return err
1808 }
1809 switch strings.ToLower(choice) {
1810 case "approve":
1811 response.Decision = "approve"
1812 case "", "deny":
1813 response.Decision = "deny"
1814 case "cancel":
1815 response.Decision = "cancel"
1816 default:
1817 fmt.Println("Enter approve, deny, or cancel.")
1818 continue
1819 }
1820 break
1821 }
1822 return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{OfBrowserOriginAccess: &response})
1823 case "browser_authentication":
1824 // Collect credentials only for an authentication request.
1825 default:
1826 return fmt.Errorf("unsupported computer-use approval: %s", approval.Request.Type)
1827 }
1828 challenge := approval.Request.AsBrowserAuthentication()
1829 reason := challenge.Reason
1830 if reason == "" {
1831 reason = "The agent needs you to sign in."
1832 }
1833 origin := challenge.CredentialOrigin
1834 if origin == "" {
1835 origin = "Not supplied"
1836 }
1837 fmt.Println(reason)
1838 fmt.Println("Credential origin:", origin)
1839 consent, err := readLine("Have you verified the sign-in destination? [y/N] ")
1840 if err != nil {
1841 return err
1842 }
1843 if strings.ToLower(consent) != "y" && strings.ToLower(consent) != "yes" {
1844 return cancelAuthentication()
1845 }
1846
1847 response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthentication{
1848 Action: "submit",
1849 Fields: []openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthenticationField{},
1850 }
1851 activeFields := challenge.Fields
1852 if len(challenge.Options) > 0 {
1853 for index, option := range challenge.Options {
1854 fmt.Printf("%d. %s\n", index+1, option.Label)
1855 }
1856 for {
1857 choice, err := readLine("Choose a sign-in method [number/cancel]: ")
1858 if err != nil {
1859 return err
1860 }
1861 if strings.EqualFold(choice, "cancel") {
1862 return cancelAuthentication()
1863 }
1864 index, err := strconv.Atoi(choice)
1865 if err != nil || index < 1 || index > len(challenge.Options) {
1866 fmt.Println("Enter a method number from the list, or enter cancel.")
1867 continue
1868 }
1869 option := challenge.Options[index-1]
1870 response.SelectedOption = openai.String(option.ID)
1871 activeFields = nil
1872 for _, fieldID := range option.FieldIDs {
1873 found := false
1874 for _, field := range challenge.Fields {
1875 if field.ID == fieldID {
1876 activeFields = append(activeFields, field)
1877 found = true
1878 break
1879 }
1880 }
1881 if !found {
1882 return fmt.Errorf("sign-in method references an unknown field: %s", fieldID)
1883 }
1884 }
1885 break
1886 }
1887 }
1888
1889collectFields:
1890 for {
1891 response.Fields = response.Fields[:0]
1892 for _, field := range activeFields {
1893 fieldInput:
1894 for {
1895 choices := "enter/cancel; default: enter"
1896 if !field.Required {
1897 choices = "enter/skip/cancel; default: enter"
1898 }
1899 choice, err := readLine(fmt.Sprintf("%s [%s]: ", field.Label, choices))
1900 if err != nil {
1901 return err
1902 }
1903 switch strings.ToLower(choice) {
1904 case "cancel":
1905 return cancelAuthentication()
1906 case "skip":
1907 if field.Required {
1908 fmt.Println("This field is required. Enter a value or cancel sign-in.")
1909 continue
1910 }
1911 break fieldInput
1912 case "", "enter":
1913 fmt.Printf("%s (hidden): ", field.Label)
1914 value, err := term.ReadPassword(int(os.Stdin.Fd()))
1915 fmt.Println()
1916 if err != nil {
1917 return err
1918 }
1919 if len(value) == 0 {
1920 fmt.Println("No value entered. Choose an action for this field.")
1921 continue
1922 }
1923 response.Fields = append(response.Fields, openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthenticationField{
1924 FieldID: field.ID, Value: string(value),
1925 })
1926 clear(value)
1927 break fieldInput
1928 default:
1929 fmt.Println("Choose one of the listed actions.")
1930 }
1931 }
1932 }
1933 if len(challenge.Options) > 0 || len(challenge.Fields) == 0 || len(response.Fields) > 0 {
1934 break
1935 }
1936 for {
1937 choice, err := readLine("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ")
1938 if err != nil {
1939 return err
1940 }
1941 switch strings.ToLower(choice) {
1942 case "retry":
1943 continue collectFields
1944 case "", "cancel":
1945 return cancelAuthentication()
1946 default:
1947 fmt.Println("Enter retry or cancel.")
1948 }
1949 }
1950 }
1951 // Admission does not establish login success; keep following the session.
1952 return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{
1953 OfBrowserAuthentication: &response,
1954 })
1955}
1956```
1957
1958```java
1959import com.openai.client.OpenAIClient;
1960import com.openai.client.okhttp.OpenAIOkHttpClient;
1961import com.openai.models.beta.agents.AgentSession;
1962import com.openai.models.beta.agents.AgentSessionInputMessageParam;
1963import com.openai.models.beta.agents.AgentSessionInputParam;
1964import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult;
1965import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult.Response;
1966import com.openai.models.beta.agents.AgentToolParam;
1967import com.openai.models.beta.agents.EnvironmentParam;
1968import com.openai.models.beta.agents.sessions.SessionCreateParams;
1969import com.openai.models.beta.agents.sessions.events.EventCreateParams;
1970import java.util.Arrays;
1971import java.util.HashSet;
1972import java.util.List;
1973
1974static void cancelAuthentication(OpenAIClient client, String sessionId, String requestId) {
1975 var cancellation =
1976 AgentSessionInputComputerUseApprovalRequestResult.builder()
1977 .requestId(requestId)
1978 .response(
1979 Response.BrowserAuthentication.ofCancel(
1980 Response.BrowserAuthentication.Cancel.builder().build()))
1981 .build();
1982 client
1983 .withOptions(options -> options.maxRetries(0))
1984 .beta()
1985 .agents()
1986 .sessions()
1987 .events()
1988 .create(EventCreateParams.builder().sessionId(sessionId).addEvent(cancellation).build());
1989}
1990
1991static void respondToComputerUseApproval(
1992 OpenAIClient client,
1993 String sessionId,
1994 AgentSession.RequiredAction.ComputerUseApprovalRequest approval) {
1995 var console = System.console();
1996 if (console == null)
1997 throw new IllegalStateException("Run this example in an interactive terminal.");
1998 var result =
1999 AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId());
2000 if (approval.request().browserOriginAccess().isPresent()) {
2001 var request = approval.request().browserOriginAccess().get();
2002 console.printf("Origin: %s%n", request.origin());
2003 console.printf("Reason: %s%n", request.reason().orElse("Not supplied"));
2004 String decision;
2005 while (true) {
2006 String input =
2007 console.readLine("Allow browser access? [approve/deny/cancel; default deny] ");
2008 if (input == null) throw new IllegalStateException("Approval input closed.");
2009 decision = input.strip().toLowerCase(java.util.Locale.ROOT);
2010 if (decision.isEmpty()) decision = "deny";
2011 if (List.of("approve", "deny", "cancel").contains(decision)) break;
2012 console.printf("Enter approve, deny, or cancel.%n");
2013 }
2014 result.response(
2015 Response.BrowserOriginAccess.builder()
2016 .decision(Response.BrowserOriginAccess.Decision.of(decision))
2017 .build());
2018 } else if (approval.request().browserAuthentication().isPresent()) {
2019 var challenge = approval.request().browserAuthentication().get();
2020 console.printf("%s%n", challenge.reason().orElse("The agent needs you to sign in."));
2021 console.printf(
2022 "Credential origin: %s%n", challenge.credentialOrigin().orElse("Not supplied"));
2023 String consent = console.readLine("Have you verified the sign-in destination? [y/N] ");
2024 if (consent == null
2025 || !List.of("y", "yes").contains(consent.strip().toLowerCase(java.util.Locale.ROOT))) {
2026 cancelAuthentication(client, sessionId, approval.requestId());
2027 return;
2028 }
2029 var response = Response.BrowserAuthentication.Submit.builder().fields(List.of());
2030 var activeFields = challenge.fields();
2031 if (!challenge.options().isEmpty()) {
2032 for (int i = 0; i < challenge.options().size(); i++) {
2033 console.printf("%d. %s%n", i + 1, challenge.options().get(i).label());
2034 }
2035 while (true) {
2036 String choice = console.readLine("Choose a sign-in method, or enter cancel: ");
2037 if (choice == null || choice.strip().equalsIgnoreCase("cancel")) {
2038 cancelAuthentication(client, sessionId, approval.requestId());
2039 return;
2040 }
2041 int index;
2042 try {
2043 index = Integer.parseInt(choice.strip()) - 1;
2044 } catch (NumberFormatException e) {
2045 console.printf("Enter a method number from the list, or cancel.%n");
2046 continue;
2047 }
2048 if (index < 0 || index >= challenge.options().size()) {
2049 console.printf("Enter a method number from the list, or cancel.%n");
2050 continue;
2051 }
2052 var option = challenge.options().get(index);
2053 response.selectedOption(option.id());
2054 activeFields =
2055 challenge.fields().stream()
2056 .filter(field -> option.fieldIds().contains(field.id()))
2057 .toList();
2058 break;
2059 }
2060 }
2061 while (true) {
2062 int fieldsSubmitted = 0;
2063 for (var field : activeFields) {
2064 while (true) {
2065 String choice =
2066 console.readLine(
2067 field.required()
2068 ? "%s [enter/cancel; default enter]: "
2069 : "%s [enter/skip/cancel; default skip]: ",
2070 field.label());
2071 if (choice == null || choice.strip().equalsIgnoreCase("cancel")) {
2072 cancelAuthentication(client, sessionId, approval.requestId());
2073 return;
2074 }
2075 choice = choice.strip().toLowerCase(java.util.Locale.ROOT);
2076 if (choice.isEmpty()) choice = field.required() ? "enter" : "skip";
2077 if (choice.equals("skip") && !field.required()) break;
2078 if (!choice.equals("enter")) {
2079 console.printf("Choose one of the displayed options.%n");
2080 continue;
2081 }
2082 char[] characters = console.readPassword("%s: ", field.label());
2083 if (characters == null) {
2084 cancelAuthentication(client, sessionId, approval.requestId());
2085 return;
2086 }
2087 String value = new String(characters);
2088 Arrays.fill(characters, '\0');
2089 if (value.isEmpty()) {
2090 if (!field.required()) break;
2091 console.printf("This field is required.%n");
2092 continue;
2093 }
2094 response.addField(
2095 Response.BrowserAuthentication.Submit.Field.builder()
2096 .fieldId(field.id())
2097 .value(value)
2098 .build());
2099 fieldsSubmitted++;
2100 break;
2101 }
2102 }
2103 if (!challenge.options().isEmpty() || activeFields.isEmpty() || fieldsSubmitted > 0) break;
2104 console.printf("Enter at least one field to submit this form, or cancel.%n");
2105 }
2106 result.response(Response.BrowserAuthentication.ofSubmit(response.build()));
2107 } else {
2108 throw new IllegalStateException("Unsupported computer-use approval request.");
2109 }
2110 // An uncertain approval must not resend credentials automatically.
2111 client
2112 .withOptions(options -> options.maxRetries(0))
2113 .beta()
2114 .agents()
2115 .sessions()
2116 .events()
2117 .create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build());
2118 // Admission does not establish sign-in or navigation success; keep following the session.
2119}
2120```
2121
2122```csharp
2123using System.ClientModel;
2124using System.ClientModel.Primitives;
2125using System.Globalization;
2126using System.Text;
2127using System.Text.Json;
2128using OpenAI;
2129using OpenAI.Agents;
2130#pragma warning disable OPENAI001
2131
2132static string ReadLine(string prompt)
2133{
2134 Console.Write(prompt);
2135 return Console.ReadLine() ?? "cancel";
2136}
2137
2138static string ReadHidden(string prompt)
2139{
2140 if (Console.IsInputRedirected)
2141 {
2142 throw new InvalidOperationException("Run this sign-in example in a terminal.");
2143 }
2144 Console.Write(prompt);
2145 StringBuilder value = new();
2146 while (true)
2147 {
2148 ConsoleKeyInfo key = Console.ReadKey(intercept: true);
2149 if (key.Key == ConsoleKey.Enter)
2150 {
2151 Console.WriteLine();
2152 return value.ToString();
2153 }
2154 if (key.Key == ConsoleKey.Backspace)
2155 {
2156 if (value.Length > 0)
2157 {
2158 value.Length--;
2159 }
2160 }
2161 else if (!char.IsControl(key.KeyChar))
2162 {
2163 value.Append(key.KeyChar);
2164 }
2165 }
2166}
2167
2168static async Task RespondToComputerUseApprovalAsync(
2169 AgentClient client, string sessionId,
2170 SessionRequiredActionResourceComputerUseApprovalRequest approval)
2171{
2172 if (Console.IsInputRedirected)
2173 {
2174 throw new InvalidOperationException("Run this example in an interactive terminal.");
2175 }
2176 ComputerUseApprovalResponseParam response;
2177 if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin)
2178 {
2179 Console.WriteLine($"Origin: {origin.Origin}");
2180 Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}");
2181 string choice;
2182 while (true)
2183 {
2184 Console.Write("Allow browser access? [approve/deny/cancel; default deny] ");
2185 choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant();
2186 if (choice.Length == 0) choice = "deny";
2187 if (choice is "approve" or "deny" or "cancel") break;
2188 Console.WriteLine("Enter approve, deny, or cancel.");
2189 }
2190 BrowserOriginAccessDecisionParam decision = choice switch
2191 {
2192 "approve" => BrowserOriginAccessDecisionParam.Approve,
2193 "cancel" => BrowserOriginAccessDecisionParam.Cancel,
2194 _ => BrowserOriginAccessDecisionParam.Deny,
2195 };
2196 response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision);
2197 }
2198 else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication challenge)
2199 {
2200 async Task CancelAuthenticationAsync()
2201 {
2202 await client.CreateAgentSessionEventsAsync(
2203 sessionId,
2204 new CreateSessionEventsParams(
2205 [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(
2206 approval.RequestId, new ComputerUseApprovalResponseParamBrowserAuthenticationCancel())]
2207 )
2208 );
2209 }
2210
2211 Console.WriteLine(challenge.Reason ?? "The agent needs you to sign in.");
2212 Console.WriteLine($"Credential origin: {challenge.CredentialOrigin ?? "Not supplied"}");
2213 string consent = ReadLine("Have you verified the sign-in destination? [y/N] ").Trim();
2214 if (!consent.Equals("y", StringComparison.OrdinalIgnoreCase)
2215 && !consent.Equals("yes", StringComparison.OrdinalIgnoreCase))
2216 {
2217 await CancelAuthenticationAsync();
2218 return;
2219 }
2220
2221 ComputerUseApprovalResponseParamBrowserAuthenticationSubmit submission = new([]);
2222 var activeFields = challenge.Fields.ToList();
2223 if (challenge.Options.Count > 0)
2224 {
2225 for (int index = 0; index < challenge.Options.Count; index++)
2226 {
2227 Console.WriteLine($"{index + 1}. {challenge.Options[index].Label}");
2228 }
2229 while (true)
2230 {
2231 string choice = ReadLine("Choose a sign-in method, or enter cancel: ").Trim();
2232 if (choice.Equals("cancel", StringComparison.OrdinalIgnoreCase))
2233 {
2234 await CancelAuthenticationAsync();
2235 return;
2236 }
2237 if (!int.TryParse(choice, NumberStyles.None, CultureInfo.InvariantCulture, out int index)
2238 || index < 1 || index > challenge.Options.Count)
2239 {
2240 Console.WriteLine("Enter a method number from the list, or cancel.");
2241 continue;
2242 }
2243 var option = challenge.Options[index - 1];
2244 submission.SelectedOption = option.Id;
2245 var fieldsById = challenge.Fields.ToDictionary(field => field.Id, StringComparer.Ordinal);
2246 activeFields = option.FieldIds.Select(id => fieldsById[id]).ToList();
2247 break;
2248 }
2249 }
2250 while (true)
2251 {
2252 foreach (var field in activeFields)
2253 {
2254 while (true)
2255 {
2256 string choices = field.Required
2257 ? "enter/cancel; default enter" : "enter/skip/cancel; default skip";
2258 string choice = ReadLine($"{field.Label} [{choices}]: ").Trim().ToLowerInvariant();
2259 if (choice == "cancel")
2260 {
2261 await CancelAuthenticationAsync();
2262 return;
2263 }
2264 if (choice.Length == 0) choice = field.Required ? "enter" : "skip";
2265 if (choice == "skip" && !field.Required) break;
2266 if (choice != "enter")
2267 {
2268 Console.WriteLine("Choose one of the displayed options.");
2269 continue;
2270 }
2271 string value = ReadHidden($"{field.Label}: ");
2272 if (value.Length == 0)
2273 {
2274 if (!field.Required) break;
2275 Console.WriteLine("This field is required.");
2276 continue;
2277 }
2278 submission.Fields.Add(new BrowserAuthenticationFieldValueParam(field.Id, value));
2279 break;
2280 }
2281 }
2282 if (challenge.Options.Count > 0 || activeFields.Count == 0 || submission.Fields.Count > 0) break;
2283 Console.WriteLine("Enter at least one field to submit this form, or cancel.");
2284 }
2285 response = submission;
2286 }
2287 else
2288 {
2289 throw new InvalidOperationException("Unsupported computer-use approval request.");
2290 }
2291 await client.CreateAgentSessionEventsAsync(
2292 sessionId,
2293 new CreateSessionEventsParams(
2294 [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, response)]
2295 )
2296 );
2297 // Admission does not establish sign-in or navigation success; keep following the session.
2298}
2299```
2300
2301```ruby
2302require "io/console"
2303
2304def computer_use_sign_in_choice(prompt)
2305 print prompt
2306 input = $stdin.gets || raise(EOFError, "Input closed before a sign-in choice.")
2307 input.strip.downcase
2308end
2309
2310def computer_use_authentication_response(request)
2311 cancel = {
2312 type: "browser_authentication",
2313 action: "cancel"
2314 }
2315 puts request.reason || "The agent needs you to sign in."
2316 puts "Credential origin: #{request.credential_origin || "Not supplied"}"
2317 consent = computer_use_sign_in_choice("Have you verified the sign-in destination? [y/N] ")
2318 return cancel unless ["y", "yes"].include?(consent)
2319
2320 selected_option = nil
2321 active_fields = request.fields
2322 unless request.options.empty?
2323 request.options.each_with_index do |option, index|
2324 puts "#{index + 1}. #{option.label}"
2325 end
2326 option = loop do
2327 choice = computer_use_sign_in_choice("Choose a sign-in method [number/cancel]: ")
2328 return cancel if choice == "cancel"
2329 if choice.match?(/\A\d+\z/) && choice.to_i.between?(1, request.options.length)
2330 break request.options[choice.to_i - 1]
2331 end
2332
2333 puts "Enter a method number from the list, or enter cancel."
2334 end
2335 selected_option = option.id
2336 fields_by_id = request.fields.to_h { |field| [field.id, field] }
2337 active_fields = option.field_ids.map { |field_id| fields_by_id.fetch(field_id) }
2338 end
2339
2340 values = []
2341 loop do
2342 values.clear
2343 active_fields.each do |field|
2344 loop do
2345 choices = field.required ? "enter/cancel; default: enter" : "enter/skip/cancel; default: enter"
2346 choice = computer_use_sign_in_choice("#{field.label} [#{choices}]: ")
2347 case choice
2348 when "cancel"
2349 return cancel
2350 when "skip"
2351 break unless field.required
2352
2353 puts "This field is required. Enter a value or cancel sign-in."
2354 when "", "enter"
2355 value = $stdin.getpass("#{field.label} (hidden): ")
2356 if value.empty?
2357 puts "No value entered. Choose an action for this field."
2358 next
2359 end
2360 values << {
2361 field_id: field.id,
2362 value: value
2363 }
2364 break
2365 else
2366 puts "Choose one of the listed actions."
2367 end
2368 end
2369 end
2370 break unless request.options.empty? && !request.fields.empty? && values.empty?
2371
2372 loop do
2373 choice = computer_use_sign_in_choice("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ")
2374 return cancel if ["", "cancel"].include?(choice)
2375 break if choice == "retry"
2376
2377 puts "Enter retry or cancel."
2378 end
2379 end
2380
2381 response = {
2382 type: "browser_authentication",
2383 action: "submit",
2384 fields: values
2385 }
2386 response[:selected_option] = selected_option unless selected_option.nil?
2387 response
2388end
2389
2390def respond_to_computer_use_approval(client, session_id, approval)
2391 request = approval.request
2392 case request.type.to_s
2393 when "browser_origin_access"
2394 puts request.reason || "The browser needs access to an origin."
2395 puts "Origin: #{request.origin}"
2396 decision = loop do
2397 print "Allow this origin? [approve/deny/cancel; default: deny] "
2398 input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.")
2399 choice = input.strip.downcase
2400 choice = "deny" if choice.empty?
2401 break choice if ["approve", "deny", "cancel"].include?(choice)
2402
2403 puts "Enter approve, deny, or cancel."
2404 end
2405 response = {
2406 type: "browser_origin_access",
2407 decision: decision
2408 }
2409 when "browser_authentication"
2410 response = computer_use_authentication_response(request)
2411 else
2412 raise "Unsupported computer-use approval: #{request.type}"
2413 end
2414 client.beta.agents.sessions.events.create(
2415 session_id,
2416 events: [
2417 {
2418 type: "agent.session.input.computer_use_approval_request_result",
2419 request_id: approval.request_id,
2420 response: response
2421 }
2422 ],
2423 request_options: { max_retries: 0 }
2424 )
2425 # Admission does not establish sign-in or navigation success; keep reading events.
2426end
2427```
2428
2429
2430The following example asks the agent to read issues from a private GitHub
2431repository. Replace `https://github.com/acme/private-repo/issues` with an issue
2432page you can access.
2433
2434Open the event stream before sending the task, and call the helper whenever the
2435session requires input.
2436
2437Read private repository issues
2438
2439```bash
2440# Terminal 1: create a new session and stream its first task.
2441# Replace the illustrative repository URL with one you can access.
2442curl --no-buffer --fail-with-body https://api.openai.com/v1/agents/sessions \
2443 -H "OpenAI-Beta: agents=v1" \
2444 -H "Authorization: Bearer $OPENAI_API_KEY" \
2445 -H "Content-Type: application/json" \
2446 -H "Accept: text/event-stream" \
2447 -d '{
2448 "agent": {
2449 "model": "gpt-6-astra",
2450 "instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
2451 "tools": [{ "type": "computer_use", "include_screenshots": false }]
2452 },
2453 "environment": {
2454 "type": "openai_hosted",
2455 "desktop": { "enabled": true },
2456 "network": { "access": "enabled" }
2457 },
2458 "input": "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.",
2459 "stream": true
2460 }'
2461
2462# Copy session.id from agent.session.created into terminal 2.
2463# Keep this stream open while responding to origin and sign-in approvals there.
2464```
2465
2466```javascript
2467import OpenAI from "openai";
2468
2469const client = new OpenAI();
2470const session = await client.beta.agents.sessions.create({
2471 agent: {
2472 model: "gpt-6-astra",
2473 instructions:
2474 "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
2475 tools: [{ type: "computer_use", include_screenshots: false }],
2476 },
2477 environment: {
2478 type: "openai_hosted",
2479 desktop: { enabled: true },
2480 network: { access: "enabled" },
2481 },
2482});
2483console.log("Session ID:", session.id);
2484
2485let completed = false;
2486let readyToDelete = false;
2487try {
2488 const events = await client.beta.agents.sessions.events.stream(session.id);
2489 const handledRequests = new Set();
2490 try {
2491 await client.beta.agents.sessions.events.create(session.id, {
2492 events: [
2493 {
2494 type: "agent.session.input.message",
2495 input: [
2496 {
2497 role: "user",
2498 content: [
2499 {
2500 type: "input_text",
2501 // Replace this illustrative URL with your private repository.
2502 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.",
2503 },
2504 ],
2505 },
2506 ],
2507 },
2508 ],
2509 });
2510 for await (const event of events) {
2511 switch (event.type) {
2512 case "agent.session.requires_action": {
2513 const current = await client.beta.agents.sessions.retrieve(
2514 session.id
2515 );
2516 for (const approval of current.required_actions) {
2517 if (
2518 approval.type === "computer_use_approval_request" &&
2519 !handledRequests.has(approval.request_id)
2520 ) {
2521 await respondToComputerUseApproval(client, session.id, approval);
2522 handledRequests.add(approval.request_id);
2523 }
2524 }
2525 break;
2526 }
2527 case "agent.session.turn.output_text.done":
2528 console.log(event.text);
2529 break;
2530 case "error":
2531 throw new Error(event.error.message);
2532 case "agent.session.failed":
2533 case "agent.session.environment.failed":
2534 throw new Error(`Agent lifecycle failure: ${event.type}`);
2535 case "agent.session.turn.failed":
2536 if (event.turn.subagent_id === null) {
2537 throw new Error(event.turn.error?.message ?? "Browser task failed");
2538 }
2539 break;
2540 case "agent.session.turn.cancelled":
2541 if (event.turn.subagent_id === null) {
2542 throw new Error("Browser task was cancelled");
2543 }
2544 break;
2545 case "agent.session.turn.completed":
2546 if (event.turn.subagent_id === null) completed = true;
2547 break;
2548 }
2549 if (completed) break;
2550 }
2551 if (!completed) {
2552 throw new Error("Stream closed before the browser task finished");
2553 }
2554 console.log();
2555 } finally {
2556 events.controller.abort();
2557 }
2558 readyToDelete = completed;
2559} catch (error) {
2560 console.error(
2561 `Session ${session.id} was kept. Use the same ID to check its status before trying again.`
2562 );
2563 throw error;
2564} finally {
2565 if (readyToDelete) await client.beta.agents.sessions.delete(session.id);
2566}
2567```
2568
2569```python
2570from openai import OpenAI
2571
2572client = OpenAI()
2573session = client.beta.agents.sessions.create(
2574 agent={
2575 "model": "gpt-6-astra",
2576 "instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
2577 "tools": [{"type": "computer_use", "include_screenshots": False}],
2578 },
2579 environment={
2580 "type": "openai_hosted",
2581 "desktop": {"enabled": True},
2582 "network": {"access": "enabled"},
2583 },
2584)
2585print("Session ID:", session.id, flush=True)
2586handled_requests = set()
2587completed = False
2588ready_to_delete = False
2589
2590try:
2591 with client.beta.agents.sessions.events.stream(session.id) as events:
2592 client.beta.agents.sessions.events.create(
2593 session.id,
2594 events=[
2595 {
2596 "type": "agent.session.input.message",
2597 "input": [
2598 {
2599 "role": "user",
2600 "content": [
2601 {
2602 "type": "input_text",
2603 # Replace this illustrative URL with your private repository.
2604 "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.",
2605 }
2606 ],
2607 }
2608 ],
2609 }
2610 ],
2611 )
2612 for event in events:
2613 if event.type == "agent.session.requires_action":
2614 current = client.beta.agents.sessions.retrieve(session.id)
2615 for approval in current.required_actions:
2616 if (
2617 approval.type == "computer_use_approval_request"
2618 and approval.request_id not in handled_requests
2619 ):
2620 respond_to_computer_use_approval(client, session.id, approval)
2621 handled_requests.add(approval.request_id)
2622 elif event.type == "agent.session.turn.output_text.done":
2623 print(event.text, flush=True)
2624 elif event.type == "agent.session.turn.completed":
2625 if event.turn.subagent_id is None:
2626 completed = True
2627 print()
2628 break
2629 elif event.type in {
2630 "agent.session.turn.failed",
2631 "agent.session.turn.cancelled",
2632 }:
2633 if event.turn.subagent_id is None:
2634 raise RuntimeError(f"Browser task ended: {event.type}")
2635 elif event.type == "error":
2636 raise RuntimeError(event.error.message)
2637 elif event.type in {
2638 "agent.session.failed",
2639 "agent.session.environment.failed",
2640 }:
2641 raise RuntimeError(f"Session failed: {event.type}")
2642 else:
2643 raise RuntimeError("Stream closed before the browser task finished.")
2644 ready_to_delete = completed
2645except (Exception, KeyboardInterrupt):
2646 print(
2647 f"Session {session.id} was kept. Use the same ID to check its status before trying again.",
2648 flush=True,
2649 )
2650 raise
2651finally:
2652 if ready_to_delete:
2653 client.beta.agents.sessions.delete(session.id)
2654 client.close()
2655```
2656
2657```go
2658ctx := context.Background()
2659client := openai.NewClient()
2660session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{
2661 Agent: openai.BetaAgentSessionNewParamsAgent{
2662 Model: openai.String("gpt-6-astra"),
2663 Instructions: openai.String("Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues."),
2664 Tools: []openai.AgentToolParamUnion{{
2665 OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(false)},
2666 }},
2667 },
2668 Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{
2669 Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: openai.Bool(true)},
2670 Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"},
2671 }},
2672})
2673if err != nil {
2674 return err
2675}
2676fmt.Println("Session ID:", session.ID)
2677completed := false
2678defer func() {
2679 if !completed {
2680 fmt.Fprintf(os.Stderr, "Session %s was not deleted. Retrieve it and check required_actions before attempting recovery.\n", session.ID)
2681 return
2682 }
2683 if _, err := client.Beta.Agents.Sessions.Delete(ctx, session.ID); err != nil {
2684 fmt.Fprintf(os.Stderr, "Could not delete completed session %s: %v\n", session.ID, err)
2685 }
2686}()
2687handledRequests := map[string]bool{}
2688events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID)
2689defer events.Close()
2690if err := events.Err(); err != nil {
2691 return err
2692}
2693err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{
2694 Events: []openai.AgentSessionInputParamUnion{{
2695 OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{
2696 Input: []openai.AgentSessionInputMessageParam{{
2697 Role: "user",
2698 Content: []openai.InputContentParamUnion{{
2699 OfParamInputText: &openai.InputContentParamInputText{
2700 // Replace this illustrative URL with your private repository.
2701 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.",
2702 },
2703 }},
2704 }},
2705 },
2706 }},
2707})
2708if err != nil {
2709 return err
2710}
2711for events.Next() {
2712 event := events.Current()
2713 switch event.Type {
2714 case "agent.session.requires_action":
2715 current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID)
2716 if err != nil {
2717 return err
2718 }
2719 for _, action := range current.RequiredActions {
2720 if action.Type != "computer_use_approval_request" {
2721 continue
2722 }
2723 approval := action.AsComputerUseApprovalRequest()
2724 if handledRequests[approval.RequestID] {
2725 continue
2726 }
2727 if err := respondToComputerUseApproval(ctx, &client, session.ID, approval); err != nil {
2728 return err
2729 }
2730 handledRequests[approval.RequestID] = true
2731 }
2732 case "agent.session.turn.output_text.done":
2733 fmt.Println(event.Text)
2734 case "agent.session.turn.completed":
2735 if event.Turn.SubagentID == "" {
2736 completed = true
2737 return nil
2738 }
2739 case "agent.session.turn.failed", "agent.session.turn.cancelled":
2740 if event.Turn.SubagentID == "" {
2741 return fmt.Errorf("browser task ended: %s", event.Type)
2742 }
2743 case "error":
2744 return fmt.Errorf("agent error: %s", event.Error.Message)
2745 case "agent.session.failed", "agent.session.environment.failed":
2746 return fmt.Errorf("session failed: %s", event.Type)
2747 }
2748}
2749if err := events.Err(); err != nil {
2750 return err
2751}
2752return fmt.Errorf("stream closed before the browser task finished")
2753```
2754
2755```java
2756var client = OpenAIOkHttpClient.fromEnv();
2757var session =
2758 client
2759 .beta()
2760 .agents()
2761 .sessions()
2762 .create(
2763 SessionCreateParams.builder()
2764 .agent(
2765 SessionCreateParams.Agent.builder()
2766 .model("gpt-6-astra")
2767 .instructions(
2768 "Read the requested GitHub issue list in the browser. Request"
2769 + " sign-in when needed. Do not create, edit, comment on, or"
2770 + " close issues.")
2771 .addTool(
2772 AgentToolParam.ComputerUse.builder()
2773 .includeScreenshots(false)
2774 .build())
2775 .build())
2776 .environment(
2777 EnvironmentParam.OpenAIHosted.builder()
2778 .desktop(
2779 EnvironmentParam.OpenAIHosted.Desktop.builder()
2780 .enabled(true)
2781 .build())
2782 .network(
2783 EnvironmentParam.OpenAIHosted.Network.builder()
2784 .access(EnvironmentParam.OpenAIHosted.Network.Access.ENABLED)
2785 .build())
2786 .build())
2787 .build());
2788System.out.println("Session ID: " + session.id());
2789var handledRequests = new HashSet<String>();
2790try {
2791 try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) {
2792 client
2793 .beta()
2794 .agents()
2795 .sessions()
2796 .events()
2797 .create(
2798 EventCreateParams.builder()
2799 .sessionId(session.id())
2800 .addEvent(
2801 AgentSessionInputParam.AgentSessionInputMessage.builder()
2802 .addInput(
2803 AgentSessionInputMessageParam.builder()
2804 // Replace this illustrative URL with your private repository.
2805 .addInputTextContent(
2806 "Open https://github.com/acme/private-repo/issues in the"
2807 + " browser. Sign in if needed, then report the title"
2808 + " and URL of the most recently updated open issue. Do"
2809 + " not make changes.")
2810 .build())
2811 .build())
2812 .build());
2813 boolean completed = false;
2814 var iterator = events.stream().iterator();
2815 while (iterator.hasNext()) {
2816 var event = iterator.next();
2817 if (event.requiresAction().isPresent()) {
2818 var current = client.beta().agents().sessions().retrieve(session.id());
2819 for (var action : current.requiredActions()) {
2820 if (action.computerUseApprovalRequest().isEmpty()) continue;
2821 var approval = action.computerUseApprovalRequest().get();
2822 if (!handledRequests.contains(approval.requestId())) {
2823 respondToComputerUseApproval(client, session.id(), approval);
2824 handledRequests.add(approval.requestId());
2825 }
2826 }
2827 }
2828 event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text()));
2829 if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
2830 completed = true;
2831 break;
2832 }
2833 if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent()
2834 || event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
2835 throw new IllegalStateException("Browser task failed or was cancelled.");
2836 }
2837 if (event.error().isPresent()) {
2838 throw new IllegalStateException(event.error().get().error().message());
2839 }
2840 if (event.failed().isPresent() || event.environmentFailed().isPresent()) {
2841 throw new IllegalStateException("The browser session failed.");
2842 }
2843 }
2844 if (!completed) {
2845 throw new IllegalStateException("Stream closed before the browser task finished.");
2846 }
2847 } catch (Exception error) {
2848 System.err.printf(
2849 "Session %s was kept. Retrieve it and check pending actions before sending anything"
2850 + " again.%n",
2851 session.id());
2852 throw error;
2853 }
2854 client.beta().agents().sessions().delete(session.id());
2855} finally {
2856 client.close();
2857}
2858```
2859
2860```csharp
2861string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
2862// An uncertain approval must not resend credentials automatically.
2863OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };
2864AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();
2865AgentSession session = await client.CreateAgentSessionAsync(
2866 new AgentSessionCreationOptions
2867 {
2868 Agent = new SessionAgentConfigParam
2869 {
2870 Model = "gpt-6-astra",
2871 Instructions = "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
2872 Tools = [new AgentToolConfigParamComputerUse { IncludeScreenshots = false }],
2873 },
2874 Environment = new EnvironmentParamOpenaiHosted
2875 {
2876 Desktop = new DesktopParam(true),
2877 Network = new NetworkPolicyParam(NetworkAccessParam.Enabled),
2878 },
2879 }
2880);
2881Console.WriteLine($"Session ID: {session.Id}");
2882HashSet<string> handledRequests = new(StringComparer.Ordinal);
2883try
2884{
2885 await using var events = await client.GetAgentSessionEventsAsync(session.Id);
2886 await client.CreateAgentSessionEventsAsync(
2887 session.Id,
2888 new CreateSessionEventsParams(
2889 [
2890 new SessionInputParamAgentSessionInputMessage(
2891 [
2892 new InputMessageParam(
2893 // Replace this illustrative URL with your private repository.
2894 [new InputContentParamInputText("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.")]
2895 ),
2896 ]
2897 ),
2898 ]
2899 )
2900 );
2901 bool completed = false;
2902 await foreach (var message in events)
2903 {
2904 using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory());
2905 JsonElement current = document.RootElement;
2906 string? type = current.GetProperty("type").GetString();
2907 if (type == "agent.session.requires_action")
2908 {
2909 AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id);
2910 foreach (SessionRequiredActionResource action in latest.RequiredActions)
2911 {
2912 if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval
2913 && !handledRequests.Contains(approval.RequestId))
2914 {
2915 await RespondToComputerUseApprovalAsync(client, session.Id, approval);
2916 handledRequests.Add(approval.RequestId);
2917 }
2918 }
2919 }
2920 else if (type == "agent.session.turn.output_text.done")
2921 {
2922 Console.WriteLine(current.GetProperty("text").GetString());
2923 }
2924 else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled")
2925 {
2926 JsonElement turn = current.GetProperty("turn");
2927 if (turn.TryGetProperty("subagent_id", out JsonElement subagent)
2928 && subagent.ValueKind != JsonValueKind.Null)
2929 {
2930 continue;
2931 }
2932 if (type != "agent.session.turn.completed")
2933 {
2934 throw new InvalidOperationException($"Browser task ended: {type}");
2935 }
2936 completed = true;
2937 break;
2938 }
2939 else if (type == "error")
2940 {
2941 throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString());
2942 }
2943 else if (type is "agent.session.failed" or "agent.session.environment.failed")
2944 {
2945 throw new InvalidOperationException($"Session failed: {type}");
2946 }
2947 }
2948 if (!completed)
2949 {
2950 throw new InvalidOperationException("Stream closed before the browser task finished.");
2951 }
2952}
2953catch
2954{
2955 Console.Error.WriteLine($"Session {session.Id} was kept. Retrieve it and check pending actions before sending anything again.");
2956 throw;
2957}
2958await client.DeleteAgentSessionAsync(session.Id);
2959```
2960
2961```ruby
2962require "openai"
2963
2964client = OpenAI::Client.new
2965session = client.beta.agents.sessions.create(
2966 agent: {
2967 model: "gpt-6-astra",
2968 instructions: "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
2969 tools: [
2970 {
2971 type: "computer_use",
2972 include_screenshots: false
2973 }
2974 ]
2975 },
2976 environment: {
2977 type: "openai_hosted",
2978 desktop: { enabled: true },
2979 network: { access: "enabled" }
2980 }
2981)
2982puts "Session ID: #{session.id}"
2983handled_requests = Set.new
2984
2985begin
2986 events = client.beta.agents.sessions.events.stream_streaming(session.id)
2987 begin
2988 client.beta.agents.sessions.events.create(
2989 session.id,
2990 events: [
2991 {
2992 type: "agent.session.input.message",
2993 input: [
2994 {
2995 role: "user",
2996 content: [
2997 {
2998 type: "input_text",
2999 # Replace this illustrative URL with your private repository.
3000 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."
3001 }
3002 ]
3003 }
3004 ]
3005 }
3006 ]
3007 )
3008 completed = events.any? do |event|
3009 case event
3010 when OpenAI::Beta::AgentSessionRequiresActionEvent
3011 current = client.beta.agents.sessions.retrieve(session.id)
3012 current.required_actions.each do |approval|
3013 next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest)
3014 next if handled_requests.include?(approval.request_id)
3015
3016 respond_to_computer_use_approval(client, session.id, approval)
3017 handled_requests.add(approval.request_id)
3018 end
3019 false
3020 when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent
3021 puts event.text
3022 when OpenAI::Beta::AgentSessionTurnCompletedEvent
3023 event.turn.subagent_id.nil?
3024 when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent
3025 raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil?
3026 when OpenAI::Beta::AgentSessionErrorEvent
3027 raise event.error.message
3028 when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent
3029 raise "Session failed: #{event.type}"
3030 else
3031 false
3032 end
3033 end
3034 raise "Stream closed before the browser task finished." unless completed
3035 ensure
3036 events.close
3037 end
3038rescue StandardError, Interrupt
3039 warn "Session #{session.id} was not deleted. Retrieve it and check required_actions before attempting recovery."
3040 raise
3041else
3042 begin
3043 client.beta.agents.sessions.delete(session.id)
3044 rescue => error
3045 warn "Could not delete completed session #{session.id}: #{error.message}"
3046 raise
3047 end
3048end
3049```
3050
3051
3052Sign-in may require several requests, so keep handling approvals until the task
3053finishes. Check the agent's result against the requested task—for this example,
3054verify the reported issue title and URL.
3055
3056### Read sign-in history
3057
3058Authentication requests and accepted responses appear in session history and in
3059`agent.session.turn.item.added` events. Request items contain the sign-in form
3060metadata; response items record the accepted action without submitted credential
3061values.
3062
3063Use this history to review past interactions. To determine whether a sign-in form
3064still needs input, retrieve the session's current `required_actions`. A recorded
3065response does not establish that sign-in succeeded.
3066
3067Origin approvals have no dedicated request or response history items. Handle
3068them through `required_actions`.
3069
3070## Ask the user a question
3071
3072To ask for clarification or let the user make a choice during a task, define a
3073[function tool](https://developers.openai.com/api/docs/guides/agents-api/tools/functions) in `agent.tools`.
3074For example, you could define `request_user_response` to present a question and
3075collect an answer. Your application supplies the tool's name, argument schema,
3076and UI.
3077
3078When you receive `agent.session.requires_action`, find the pending
3079`function_call` for your tool and use its `arguments` to display the question.
3080Return the answer through `agent.session.input.tool_result`, using the action's
3081`turn_id` and `call_id`. Set `success: true` and put the answer in `output`,
3082serializing structured answers as a JSON string. If the user declines, return
3083`success: false` with an `error` message.
3084
3085Continue following session events after returning the answer. The browser
3086approval helpers shown earlier handle origin access and sign-in; extend your
3087event handler to handle your question tool as well.
3088
3089Function-tool results are visible to the model and saved in session history.
3090 Collect passwords and verification codes through [browser
3091 authentication](#handle-sign-in).
3092
3093## Recover approval handling
3094
3095If your application disconnects or an approval response fails, retrieve the same
3096session and inspect its current `required_actions` before continuing. Rebuild
3097forms only for requests that are still pending, and remove controls for requests
3098that are no longer present.
3099
3100For a disconnected event stream, follow
3101[stream recovery](https://developers.openai.com/api/docs/guides/agents-api/sessions/events#how-to-recover-a-disconnected-stream)
3102to resume receiving events. Reconnecting must not automatically resend the task
3103or an approval response.
3104
3105Use the response status to decide what to do next:
3106
3107| Result | What your application should do |
3108| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3109| `202` | Clear submitted values and follow session events for the outcome. |
3110| `400` | Check the response type, selected option, field IDs, and required values against the pending request. Omit fields for cancellation and origin-access responses. |
3111| `404` | Check the session ID, request ID, and response type. Retrieve the session again; the request may no longer be available. |
3112| `409` | Refresh the session. The request may have expired, its turn may have ended, or a different response may already have been accepted. |
3113| Connection lost before acknowledgement | Treat acceptance as unknown. Reconnect and retrieve the session before deciding whether to retry. |
3114
3115Authentication requests expire after five minutes, and their owning turn can end
3116while the user is entering input. Refresh the session before restoring a sign-in
3117form.
3118
3119If you retry an authentication submission, use the same `request_id`, selected
3120option, and field-value mapping. An identical retry does not fill the browser form
3121again. Changing values after a submission has been accepted returns `409`; a new
3122sign-in attempt requires a new request from the agent.
3123
3124For origin approvals, retry the same decision if delivery fails. Changing an
3125accepted decision returns `409`. Origin requests remain pending for their owning
3126turn and do not have the five-minute authentication timeout.
3127
3128## Control network access
3129
3130Use the hosted environment's `network` configuration to control outbound access
3131for both the browser and code running in the environment. Allow the destination
3132website and any domains needed for page resources or redirects.
3133
3134Origin approval is a separate user decision and does not override the network
3135policy. See
3136[network access settings](https://developers.openai.com/api/docs/guides/agents-api/environments/openai-hosted#control-network-access)
3137to configure the environment.
3138
3139## Continue and clean up
3140
3141Reuse the same session for follow-up tasks that need its browser state. Login
3142cookies can expire, and recycling the environment clears the browser state.
3143
3144When you're finished, retrieve any results you need and
3145[delete the session](https://developers.openai.com/api/docs/guides/agents-api/quickstart#4-clean-up) to request
3146environment cleanup:
3147
3148Delete the browser session
3149
3150```bash
3151# Run after the root turn completes, fails, or is cancelled.
3152curl --fail-with-body -X DELETE "https://api.openai.com/v1/agents/sessions/$session_id" \
3153 -H "OpenAI-Beta: agents=v1" \
3154 -H "Authorization: Bearer $OPENAI_API_KEY"
3155```
3156
3157```javascript
3158await client.beta.agents.sessions.delete(session.id);
3159```
3160
3161```python
3162if ready_to_delete:
3163 client.beta.agents.sessions.delete(session.id)
3164```
3165
3166```go
3167readyToDelete := false
3168defer func() {
3169 if !readyToDelete {
3170 fmt.Fprintf(os.Stderr, "Session %s was not deleted. Retrieve it and check required_actions before attempting recovery.\n", session.ID)
3171 return
3172 }
3173 if _, err := client.Beta.Agents.Sessions.Delete(ctx, session.ID); err != nil {
3174 fmt.Fprintf(os.Stderr, "Could not delete completed session %s: %v\n", session.ID, err)
3175 }
3176}()
3177```
3178
3179```java
3180client.beta().agents().sessions().delete(session.id());
3181```
3182
3183```csharp
3184await client.DeleteAgentSessionAsync(session.Id);
3185```
3186
3187```ruby
3188begin
3189 client.beta.agents.sessions.delete(session.id) if completed
3190rescue => error
3191 warn "Could not delete completed session #{session.id}: #{error.message}"
3192 raise
3193end
3194```
3195
3196
3197See [OpenAI-hosted sandboxes](https://developers.openai.com/api/docs/guides/agents-api/environments/openai-hosted)
3198for environment lifetime and deletion behavior.
3199
3200## Request handling reference
3201
3202### Authentication submission limits
3203
3204Each submission can include up to six field values, with each field included
3205once. Values can contain up to 16,384 characters each; the serialized field
3206values and selected option must fit within 120 KiB.
3207
3208Use the request and field IDs from the pending approval and provide a nonempty
3209value for each required field. See the
3210[session events API reference](https://developers.openai.com/api/reference/resources/beta/subresources/agents/subresources/sessions/subresources/events/methods/create)
3211for the request schema.
3212
3213<details>
3214<summary>Expanded cURL request and stream handling</summary>
3215
3216Use this version when you need to distinguish transport failures, HTTP errors,
3217and invalid session-creation responses. It also filters streamed output and
3218reports error types and codes. It follows the same two-terminal workflow as the
3219first example; handle approvals in the second terminal as they arrive.
3220
3221Create a session and inspect stream failures
3222
3223```bash
3224create_browser_session() {
3225 unset session_id
3226 local result http_status body curl_status
3227 if result=$(curl --silent --fail-with-body --write-out '\n%{http_code}' \
3228 https://api.openai.com/v1/agents/sessions \
3229 -H "OpenAI-Beta: agents=v1" \
3230 -H "Authorization: Bearer $OPENAI_API_KEY" \
3231 -H "Content-Type: application/json" \
3232 -d '{
3233 "agent": {
3234 "model": "gpt-6-astra",
3235 "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
3236 "tools": [{ "type": "computer_use", "include_screenshots": true }]
3237 },
3238 "environment": {
3239 "type": "openai_hosted",
3240 "desktop": { "enabled": true },
3241 "network": { "access": "enabled" }
3242 }
3243 }'); then curl_status=0; else curl_status=$?; fi
3244 http_status=$(printf '%s\n' "$result" | tail -n 1)
3245 body=$(printf '%s\n' "$result" | sed '$d')
3246 [[ "$http_status" =~ ^[0-9]{3}$ ]] || http_status=000
3247
3248 if [ "$curl_status" -ne 0 ] || [[ ! "$http_status" =~ ^2[0-9][0-9]$ ]]; then
3249 printf '%s' "$body" | jq --raw-input --slurp --compact-output \
3250 --arg status "$http_status" --arg curl_status "$curl_status" '
3251 def identifier:
3252 if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
3253 (try fromjson catch {}) as $response
3254 | (if ($response | type) == "object" then $response.error // $response else {} end) as $error
3255 | if $curl_status != "0" and $curl_status != "22" then
3256 {status: $status, type: "transport_error", code: ("curl_" + $curl_status)}
3257 else
3258 {status: $status, type: ((try ($error.type | identifier) catch null) // "http_error"),
3259 code: (try ($error.code | identifier) catch null)}
3260 end' >&2
3261 return 1
3262 fi
3263 if ! session_id=$(printf '%s' "$body" | jq --exit-status --raw-output \
3264 'select(type == "object") | .id | select(type == "string" and length > 0)' 2>/dev/null); then
3265 unset session_id
3266 printf '{"status":"%s","type":"invalid_response","code":"missing_session_id"}\n' "$http_status" >&2
3267 return 1
3268 fi
3269 printf 'Session ID: %s\n' "$session_id"
3270}
3271create_browser_session
3272
3273# Terminal 1: use session_id from the creation request.
3274# Keep this stream open. Wait for HTTP 200 before sending input.
3275set -o pipefail
3276curl --silent --show-error --dump-header - --suppress-connect-headers \
3277 --no-buffer --fail-with-body \
3278 "https://api.openai.com/v1/agents/sessions/$session_id/events" \
3279 -H "OpenAI-Beta: agents=v1" \
3280 -H "Authorization: Bearer $OPENAI_API_KEY" \
3281 -H "Accept: text/event-stream" \
3282 | jq --null-input --raw-input --compact-output --unbuffered '
3283 def identifier:
3284 if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
3285 def safe_error:
3286 if type == "object" then {type: (.type | identifier), code: (.code | identifier)} else null end;
3287 def public_event:
3288 if .type == "agent.session.turn.output_text.done" then {type, text}
3289 elif .type == "agent.session.turn.completed" or .type == "agent.session.turn.failed" or .type == "agent.session.turn.cancelled" then
3290 {type, turn_id: .turn.id, subagent_id: .turn.subagent_id, error: (.turn.error | safe_error)}
3291 elif .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" then
3292 {type, error: ((.error // .session.error // .environment.error) | safe_error)}
3293 elif .type == "agent.session.requires_action" then {type}
3294 else empty end;
3295 foreach inputs as $raw (
3296 {body: "", headers: false, failed: false, output: []};
3297 .output = []
3298 | ($raw | rtrimstr([13] | implode)) as $line
3299 | if ($line | startswith("HTTP/")) then
3300 (try ($line | capture("^(?<protocol>HTTP/[0-9.]+) (?<status>[0-9]{3})(?: |$)")) catch null) as $http
3301 | if $http == null then . else
3302 .body = "" | .headers = true | .status = $http.status
3303 | .failed = (($http.status | tonumber) >= 300)
3304 | .output = [$http.protocol + " " + $http.status]
3305 end
3306 elif .headers then
3307 if $line == "" then .headers = false else . end
3308 elif ($line | startswith("data:")) then
3309 (try ($line | ltrimstr("data:") | fromjson) catch null) as $event
3310 | .output = [$event | select(type == "object") | public_event]
3311 elif .failed and .body != null then
3312 .body += ($line + "\n")
3313 | (try (.body | fromjson) catch null) as $body
3314 | if ($body | type) == "object" then
3315 (($body.error // $body) | safe_error) as $error
3316 | .output = [{status: .status, type: ($error.type // "http_error"), code: $error.code}]
3317 | .body = null
3318 else . end
3319 else . end;
3320 .output[])'
3321
3322# Terminal 2: replace sess_123 with the ID printed in terminal 1.
3323# Export OPENAI_API_KEY in this terminal too.
3324session_id="sess_123"
3325curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
3326 -H "OpenAI-Beta: agents=v1" \
3327 -H "Authorization: Bearer $OPENAI_API_KEY" \
3328 -H "Content-Type: application/json" \
3329 -d '{
3330 "events": [{
3331 "type": "agent.session.input.message",
3332 "input": [{
3333 "role": "user",
3334 "content": [{
3335 "type": "input_text",
3336 "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
3337 }]
3338 }]
3339 }]
3340 }'
3341```
3342
3343
3344</details>