El uso de la computadora permite que un agente navegue por sitios web e interactúe con las interfaces del navegador para probar un sitio, recopilar información o usar una aplicación a través de su interfaz.
La API de agentes ejecuta el navegador en un entorno alojado por OpenAI. Tu aplicación inicia la sesión y sigue sus eventos. El agente decide qué hacer a continuación según lo que observa en el navegador.
Para ejecutar una tarea en el navegador:
- Crea una sesión de navegador y guarda el ID de la sesión.
- Sigue los eventos de la sesión y envíale una tarea al agente.
- Atiende cada solicitud de acceso a un sitio web. Si la tarea requiere una cuenta, gestiona el inicio de sesión.
- Espera a que termine el turno del agente principal y verifica el resultado. Si se interrumpe la conexión, recupera la misma sesión antes de volver a intentarlo.
- Revisa la actividad guardada del navegador y, cuando termines, elimina la sesión.
Sigue los pasos que se indican en los requisitos previos de la guía de inicio rápido de la API de agentes para crear una clave de API y exportar OPENAI_API_KEY. Luego, instala el SDK de OpenAI para tu lenguaje de programación. Para los ejemplos de JavaScript, instala openai y prompt-sync. Los ejemplos de cURL requieren Bash y jq.
Para habilitar el acceso al navegador:
- Agrega
{ "type": "computer_use" } a agent.tools.
- Establece
environment.type en openai_hosted y environment.desktop.enabled en true.
Los siguientes ejemplos de JavaScript se ejecutan en secuencia. Primero creas una sesión y gestionas las aprobaciones de acceso a sitios web. Luego envías una tarea e imprimes la respuesta. Para empezar, crea una sesión de navegador con las capturas de pantalla habilitadas. Este paso no inicia ninguna tarea.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# Requires Bash and jq. Keep the session ID for follow-up requests.
set -o pipefail
if ! session_id=$(curl --silent --show-error --fail https://api.openai.com/v1/agents/sessions \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"model": "gpt-6-astra",
"instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
"tools": [{ "type": "computer_use", "include_screenshots": true }]
},
"environment": {
"type": "openai_hosted",
"desktop": { "enabled": true },
"network": { "access": "enabled" }
}
}' | jq --exit-status --raw-output '.id // empty'); then
echo "Session creation failed or its outcome is unknown. Do not retry automatically." >&2
exit 1
fi
printf 'Session ID: %s\n' "$session_id"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import OpenAI from "openai";
import { open } from "node:fs/promises";
const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
agent: {
model: "gpt-6-astra",
instructions:
"Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
tools: [{ type: "computer_use", include_screenshots: true }],
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" },
},
});
console.log("Session ID:", session.id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import base64
import os
from pathlib import Path
from openai import OpenAI
client = OpenAI()
session = client.beta.agents.sessions.create(
agent={
"model": "gpt-6-astra",
"instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
"tools": [{"type": "computer_use", "include_screenshots": True}],
},
environment={
"type": "openai_hosted",
"desktop": {"enabled": True},
"network": {"access": "enabled"},
},
)
print("Session ID:", session.id, flush=True)
ready_to_delete = False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import (
"bufio"
"context"
"encoding/base64"
"fmt"
"os"
"strings"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
)
ctx := context.Background()
client := openai.NewClient()
session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{
Agent: openai.BetaAgentSessionNewParamsAgent{
Model: openai.String("gpt-6-astra"),
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."),
Tools: []openai.AgentToolParamUnion{{
OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(true)},
}},
},
Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{
Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: true},
Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"},
}},
})
if err != nil {
return err
}
fmt.Println("Session ID:", session.ID)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.beta.agents.AgentBrowserAuthenticationCancelParam;
import com.openai.models.beta.agents.AgentBrowserOriginAccessParam;
import com.openai.models.beta.agents.AgentSession;
import com.openai.models.beta.agents.AgentSessionInputMessageParam;
import com.openai.models.beta.agents.AgentSessionInputParam;
import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult;
import com.openai.models.beta.agents.AgentToolParam;
import com.openai.models.beta.agents.EnvironmentParam;
import com.openai.models.beta.agents.sessions.SessionCreateParams;
import com.openai.models.beta.agents.sessions.events.EventCreateParams;
import com.openai.models.beta.agents.sessions.items.ItemListParams;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
var client = OpenAIOkHttpClient.fromEnv();
var session =
client
.beta()
.agents()
.sessions()
.create(
SessionCreateParams.builder()
.agent(
SessionCreateParams.Agent.builder()
.model("gpt-6-astra")
.instructions(
"Read public documentation in the browser. Do not sign in or change"
+ " any website data. Report the page title and URL you find.")
.addTool(
AgentToolParam.ComputerUse.builder()
.includeScreenshots(true)
.build())
.build())
.environment(
EnvironmentParam.OpenAIHosted.builder()
.desktop(
EnvironmentParam.OpenAIHosted.Desktop.builder()
.enabled(true)
.build())
.network(
EnvironmentParam.OpenAIHosted.Network.builder()
.access(EnvironmentParam.OpenAIHosted.Network.Access.ENABLED)
.build())
.build())
.build());
System.out.println("Session ID: " + session.id());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22require "base64"
require "openai"
client = OpenAI::Client.new
session = client.beta.agents.sessions.create(
agent: {
model: "gpt-6-astra",
instructions: "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
tools: [
{
type: "computer_use",
include_screenshots: true
}
]
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" }
}
)
puts "Session ID: #{session.id}"
El navegador necesita la aprobación del usuario antes de acceder a cada nuevo origen web, incluso si el sitio es público. Habilitar el acceso a la red no aprueba estas solicitudes.
Aunque habilites la revisión automática, el navegador seguirá solicitando la aprobación del usuario para acceder a nuevos orígenes y enviando solicitudes de inicio de sesión. Sigue atendiendo ambos tipos de solicitudes en tu aplicación.
Para gestionar la aprobación de acceso a un origen:
- Cuando recibas
agent.session.requires_action, recupera la sesión y revisa sus required_actions actuales.
- Busca las entradas pendientes de tipo
computer_use_approval_request cuyo campo anidado request.type tenga el valor browser_origin_access.
- Muestra el origen solicitado (
origin) y el motivo (reason), si se proporcionó. Luego, pide al usuario que elija entre approve, deny y cancel. Envía la decisión a través del punto de acceso de eventos de la sesión con el mismo request_id y un objeto response anidado que contenga type: "browser_origin_access" y decision, como se muestra a continuación.
Aprobar el acceso a un origen no exige confirmar cada acción
Si tu aplicación debe garantizar que el usuario confirme las compras, los cambios destructivos u otras acciones con consecuencias importantes antes de que se lleven a cabo, limita el acceso del navegador alojado a recursos que no permitan realizar esas acciones o usa un entorno de ejecución del navegador que controles. Si solicitas confirmación mediante una herramienta de función, dependes de que el agente llame a esa función.
Trata el contenido de los sitios web como contenido no confiable. No puede conceder permisos ni anular las instrucciones del usuario. Consulta la guía sobre confirmación y consentimiento para un entorno de ejecución que controles.
Define esta función auxiliar antes del código de la tarea. Gestiona las aprobaciones de acceso a orígenes y cancela las solicitudes de inicio de sesión porque esta tarea solo lee páginas públicas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# Run in terminal 2 after the task reports agent.session.requires_action.
# Reuse the task's session_id and OPENAI_API_KEY.
curl --silent --show-error --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
| jq '.required_actions[] | select(.type == "computer_use_approval_request")'
# Read request.origin and request.reason before deciding.
# Only use this response for request.type == "browser_origin_access".
# Replace REQUEST_ID and choose approve, deny, or cancel.
curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": { "type": "browser_origin_access", "decision": "approve" }
}]
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import { createInterface } from "node:readline";
const inputLines = createInterface({
input: process.stdin,
crlfDelay: Infinity,
});
const answers = inputLines[Symbol.asyncIterator]();
/** @param {OpenAI} client */
async function respondToOriginApproval(client, sessionId, approval) {
const request = approval.request;
if (request.type === "browser_origin_access") {
console.log("Requested origin:", request.origin);
console.log(request.reason ?? "The browser needs access to this origin.");
let input;
do {
process.stdout.write(
"Allow access? [approve/deny/cancel, default deny] "
);
const answer = await answers.next();
input = (answer.done ? "" : answer.value).trim().toLowerCase() || "deny";
} while (!["approve", "deny", "cancel"].includes(input));
const decision =
input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
await client.beta.agents.sessions.events.create(sessionId, {
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_origin_access", decision },
},
],
});
} else if (request.type === "browser_authentication") {
// This public-page task must not sign in.
await client.beta.agents.sessions.events.create(sessionId, {
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_authentication", action: "cancel" },
},
],
});
} else {
throw new Error(`Unsupported approval request: ${request.type}`);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32def respond_to_origin_approval(client, session_id, approval):
request = approval.request
if request.type == "browser_origin_access":
print(request.reason or "The browser needs access to an origin.")
print("Origin:", request.origin)
while True:
decision = (
input("Allow this origin? [approve/deny/cancel; default: deny] ")
.strip()
.lower()
or "deny"
)
if decision in {"approve", "deny", "cancel"}:
break
print("Enter approve, deny, or cancel.")
response = {"type": "browser_origin_access", "decision": decision}
elif request.type == "browser_authentication":
print("This public-page task does not sign in; cancelling the request.")
response = {"type": "browser_authentication", "action": "cancel"}
else:
raise RuntimeError(f"Unsupported computer-use approval: {request.type}")
client.beta.agents.sessions.events.create(
session_id,
events=[
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": approval.request_id,
"response": response,
}
],
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46func respondToOriginApproval(ctx context.Context, client *openai.Client, sessionID string, approval openai.AgentSessionRequiredActionComputerUseApprovalRequest) error {
response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{}
switch approval.Request.Type {
case "browser_origin_access":
request := approval.Request.AsBrowserOriginAccess()
fmt.Println("Requested origin:", request.Origin)
if request.Reason != "" {
fmt.Println(request.Reason)
}
originResponse := openai.AgentBrowserOriginAccessParam{Decision: "deny"}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Allow this origin? [approve/deny/cancel; default: deny] ")
choice, err := reader.ReadString('\n')
if err != nil {
return err
}
switch strings.ToLower(strings.TrimSpace(choice)) {
case "approve":
originResponse.Decision = "approve"
case "", "deny":
originResponse.Decision = "deny"
case "cancel":
originResponse.Decision = "cancel"
default:
fmt.Println("Enter approve, deny, or cancel.")
continue
}
break
}
response.OfBrowserOriginAccess = &originResponse
case "browser_authentication":
fmt.Println("This public-page task does not sign in; cancelling the sign-in request.")
response.OfBrowserAuthenticationCancel = &openai.AgentBrowserAuthenticationCancelParam{}
default:
return fmt.Errorf("unsupported computer-use approval: %s", approval.Request.Type)
}
return client.Beta.Agents.Sessions.Events.New(ctx, sessionID, openai.BetaAgentSessionEventNewParams{
Events: []openai.AgentSessionInputParamUnion{{
OfParamAgentSessionInputComputerUseApprovalRequestResult: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResult{
RequestID: approval.RequestID,
Response: response,
},
}},
}, option.WithMaxRetries(0))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41static void respondToOriginApproval(
OpenAIClient client,
String sessionId,
AgentSession.RequiredAction.ComputerUseApprovalRequest approval,
BufferedReader approvals)
throws IOException {
var result =
AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId());
if (approval.request().browserOriginAccess().isPresent()) {
var request = approval.request().browserOriginAccess().get();
System.out.printf("Origin: %s%n", request.origin());
System.out.printf("Reason: %s%n", request.reason().orElse("Not supplied"));
String decision;
while (true) {
System.out.print("Allow browser access? [approve/deny/cancel; default deny] ");
System.out.flush();
String input = approvals.readLine();
if (input == null) throw new IllegalStateException("Approval input closed.");
decision = input.strip().toLowerCase(java.util.Locale.ROOT);
if (decision.isEmpty()) decision = "deny";
if (List.of("approve", "deny", "cancel").contains(decision)) break;
System.out.printf("Enter approve, deny, or cancel.%n");
}
result.response(
AgentBrowserOriginAccessParam.builder()
.decision(AgentBrowserOriginAccessParam.Decision.of(decision))
.build());
} else if (approval.request().browserAuthentication().isPresent()) {
System.out.printf("Sign-in is outside this public-page task; cancelling the request.%n");
result.response(AgentBrowserAuthenticationCancelParam.builder().build());
} else {
throw new IllegalStateException("Unsupported computer-use approval request.");
}
client
.withOptions(options -> options.maxRetries(0))
.beta()
.agents()
.sessions()
.events()
.create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40def respond_to_origin_approval(client, session_id, approval)
request = approval.request
case request.type.to_s
when "browser_origin_access"
puts request.reason || "The browser needs access to an origin."
puts "Origin: #{request.origin}"
decision = loop do
print "Allow this origin? [approve/deny/cancel; default: deny] "
input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.")
choice = input.strip.downcase
choice = "deny" if choice.empty?
break choice if ["approve", "deny", "cancel"].include?(choice)
puts "Enter approve, deny, or cancel."
end
response = {
type: "browser_origin_access",
decision: decision
}
when "browser_authentication"
puts "This public-page task does not sign in; cancelling the request."
response = {
type: "browser_authentication",
action: "cancel"
}
else
raise "Unsupported computer-use approval: #{request.type}"
end
client.beta.agents.sessions.events.create(
session_id,
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: response
}
],
request_options: { max_retries: 0 }
)
end
Mantén abierto el flujo de eventos y atiende todas las solicitudes de aprobación pendientes. Usa request_id para dar seguimiento a las solicitudes y retira los controles de aprobación cuando una solicitud deje de aparecer en required_actions de la sesión. Cancelar una solicitud de aprobación no cancela la tarea.
Una respuesta 202 indica que se aceptó la decisión. La navegación aún podría estar en curso.
Pídele al agente que busque la guía de inicio rápido de la API de agentes en el sitio público para desarrolladores e indique su título y su URL.
Abre el flujo de eventos antes de enviar la tarea para que tu aplicación reciba los primeros eventos de progreso. Atiende las solicitudes de aprobación de acceso a orígenes a medida que lleguen para que el navegador pueda continuar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46# Terminal 1: use session_id from the creation request.
# Keep this stream open. Wait for HTTP 200 before sending input.
set -o pipefail
# Retain screenshot bytes privately even if saved history omits them.
stream_file=$(mktemp -t openai-browser-stream.XXXXXX) || exit 1
printf 'Saved event stream: %s\n' "$stream_file" >&2
curl --silent --show-error --fail --no-buffer --dump-header - \
--suppress-connect-headers \
"https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Accept: text/event-stream" \
| tee "$stream_file" \
| jq --raw-input --unbuffered '
if startswith("HTTP/") then .
elif startswith("data:") then
(ltrimstr("data:") | fromjson?) |
if .type == "agent.session.turn.output_text.done" then {type, text}
elif .type == "agent.session.requires_action"
or .type == "error"
or .type == "agent.session.failed"
or .type == "agent.session.environment.failed"
or (.type | test("^agent.session.turn.(completed|failed|cancelled)$"))
then {type, turn_id: .turn.id, subagent_id: .turn.subagent_id}
else empty end
else empty end'
# Terminal 2: replace sess_123 with the ID printed in terminal 1.
# Export OPENAI_API_KEY in this terminal too.
session_id="sess_123"
curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"type": "agent.session.input.message",
"input": [{
"role": "user",
"content": [{
"type": "input_text",
"text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
}]
}]
}]
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88let screenshot;
const events = await client.beta.agents.sessions.events.stream(session.id);
const handledRequests = new Set();
let completed = false;
try {
await client.beta.agents.sessions.events.create(session.id, {
events: [
{
type: "agent.session.input.message",
input: [
{
role: "user",
content: [
{
type: "input_text",
text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
},
],
},
],
},
],
});
for await (const event of events) {
switch (event.type) {
case "agent.session.requires_action": {
const current = await client.beta.agents.sessions.retrieve(
session.id
);
for (const approval of current.required_actions) {
if (
approval.type === "computer_use_approval_request" &&
!handledRequests.has(approval.request_id)
) {
await respondToOriginApproval(client, session.id, approval);
handledRequests.add(approval.request_id);
}
}
break;
}
case "agent.session.turn.item.done": {
// Preserve streamed screenshots; retrieved history may omit image bytes.
const item = event.item;
if (item.type === "computer_use_call") {
const output = item.output;
if (
output?.type === "computer_screenshot" &&
output.image_url.startsWith("data:image/jpeg;base64,")
) {
screenshot = Buffer.from(
output.image_url.split(",", 2)[1],
"base64"
);
}
}
break;
}
case "agent.session.turn.output_text.done":
console.log(event.text);
break;
case "error":
throw new Error(event.error.message);
case "agent.session.failed":
case "agent.session.environment.failed":
throw new Error(`Agent lifecycle failure: ${event.type}`);
case "agent.session.turn.failed":
if (event.turn.subagent_id === null) {
throw new Error(event.turn.error?.message ?? "Browser task failed");
}
break;
case "agent.session.turn.cancelled":
if (event.turn.subagent_id === null) {
throw new Error("Browser task was cancelled");
}
break;
case "agent.session.turn.completed":
if (event.turn.subagent_id === null) completed = true;
break;
}
if (completed) break;
}
if (!completed) {
throw new Error("Stream closed before the browser task finished");
}
console.log();
} finally {
events.controller.abort();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66last_screenshot = None
handled_requests = set()
completed = False
with client.beta.agents.sessions.events.stream(session.id) as events:
client.beta.agents.sessions.events.create(
session.id,
events=[
{
"type": "agent.session.input.message",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
}
],
}
],
}
],
)
for event in events:
if event.type == "agent.session.requires_action":
current = client.beta.agents.sessions.retrieve(session.id)
for approval in current.required_actions:
if (
approval.type == "computer_use_approval_request"
and approval.request_id not in handled_requests
):
respond_to_origin_approval(client, session.id, approval)
handled_requests.add(approval.request_id)
elif event.type == "agent.session.turn.item.done":
# Preserve streamed screenshots; retrieved history may omit image bytes.
item = event.item
if item.type == "computer_use_call":
output = getattr(item, "output", None)
if output is not None and output.type == "computer_screenshot":
prefix = "data:image/jpeg;base64,"
if output.image_url.startswith(prefix):
last_screenshot = base64.b64decode(
output.image_url[len(prefix) :], validate=True
)
elif event.type == "agent.session.turn.output_text.done":
print(event.text, flush=True)
elif event.type == "agent.session.turn.completed":
if event.turn.subagent_id is None:
completed = True
print()
break
elif event.type in {
"agent.session.turn.failed",
"agent.session.turn.cancelled",
}:
if event.turn.subagent_id is None:
raise RuntimeError(f"Browser task ended: {event.type}")
elif event.type == "error":
raise RuntimeError(event.error.message)
elif event.type in {
"agent.session.failed",
"agent.session.environment.failed",
}:
raise RuntimeError(f"Session failed: {event.type}")
else:
raise RuntimeError("Stream closed before the browser task finished.")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82var lastScreenshot []byte
handledRequests := map[string]bool{}
events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID)
defer events.Close()
if err := events.Err(); err != nil {
return err
}
err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{
Events: []openai.AgentSessionInputParamUnion{{
OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{
Input: []openai.AgentSessionInputMessageParam{{
Role: "user",
Content: []openai.InputContentParamUnion{{
OfParamInputText: &openai.InputContentParamInputText{
Text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.",
},
}},
}},
},
}},
})
if err != nil {
return err
}
completed := false
eventLoop:
for events.Next() {
event := events.Current()
switch event.Type {
case "agent.session.requires_action":
current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID)
if err != nil {
return err
}
for _, action := range current.RequiredActions {
if action.Type != "computer_use_approval_request" {
continue
}
approval := action.AsComputerUseApprovalRequest()
if handledRequests[approval.RequestID] {
continue
}
if err := respondToOriginApproval(ctx, &client, session.ID, approval); err != nil {
return err
}
handledRequests[approval.RequestID] = true
}
case "agent.session.turn.item.done":
// Preserve streamed screenshots; retrieved history may omit image bytes.
item := event.Item
if item.Type == "computer_use_call" && item.Output.Type == "computer_screenshot" {
const prefix = "data:image/jpeg;base64,"
if strings.HasPrefix(item.Output.ImageURL, prefix) {
lastScreenshot, err = base64.StdEncoding.DecodeString(strings.TrimPrefix(item.Output.ImageURL, prefix))
if err != nil {
return err
}
}
}
case "agent.session.turn.output_text.done":
fmt.Println(event.Text)
case "agent.session.turn.completed":
if event.Turn.SubagentID == "" {
completed = true
break eventLoop
}
case "agent.session.turn.failed", "agent.session.turn.cancelled":
if event.Turn.SubagentID == "" {
return fmt.Errorf("browser task ended: %s", event.Type)
}
case "error":
return fmt.Errorf("agent error: %s", event.Error.Message)
case "agent.session.failed", "agent.session.environment.failed":
return fmt.Errorf("session failed: %s", event.Type)
}
}
if err := events.Err(); err != nil {
return err
}
if !completed {
return fmt.Errorf("stream closed before the browser task finished")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71var approvals = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
byte[] lastScreenshot = null;
var handledRequests = new HashSet<String>();
try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) {
client
.beta()
.agents()
.sessions()
.events()
.create(
EventCreateParams.builder()
.sessionId(session.id())
.addEvent(
AgentSessionInputParam.AgentSessionInputMessage.builder()
.addInput(
AgentSessionInputMessageParam.builder()
.addInputTextContent(
"Open https://developers.openai.com in the browser. Find"
+ " the Agents API quickstart, then report its page"
+ " title and URL.")
.build())
.build())
.build());
boolean completed = false;
var iterator = events.stream().iterator();
while (iterator.hasNext()) {
var event = iterator.next();
if (event.requiresAction().isPresent()) {
var current = client.beta().agents().sessions().retrieve(session.id());
for (var action : current.requiredActions()) {
if (action.computerUseApprovalRequest().isEmpty()) continue;
var approval = action.computerUseApprovalRequest().get();
if (!handledRequests.contains(approval.requestId())) {
respondToOriginApproval(client, session.id(), approval, approvals);
handledRequests.add(approval.requestId());
}
}
}
// Preserve streamed screenshots; retrieved history may omit image bytes.
if (event.turnItemDone().isPresent()
&& event.turnItemDone().get().item().computerUseCall().isPresent()) {
var activity = event.turnItemDone().get().item().computerUseCall().get();
var output = activity.output();
if (output.isPresent()) {
String imageUrl = output.get().imageUrl();
String prefix = "data:image/jpeg;base64,";
if (imageUrl.startsWith(prefix)) {
lastScreenshot = Base64.getDecoder().decode(imageUrl.substring(prefix.length()));
}
}
}
event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text()));
if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
completed = true;
break;
}
if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent()
|| event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
throw new IllegalStateException("Browser task failed or was cancelled.");
}
if (event.error().isPresent()) {
throw new IllegalStateException(event.error().get().error().message());
}
if (event.failed().isPresent() || event.environmentFailed().isPresent()) {
throw new IllegalStateException("The browser session failed.");
}
}
if (!completed) {
throw new IllegalStateException("Stream closed before the browser task finished.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66last_screenshot = String.new(encoding: Encoding::BINARY)
handled_requests = Set.new
events = client.beta.agents.sessions.events.stream_streaming(session.id)
begin
client.beta.agents.sessions.events.create(
session.id,
events: [
{
type: "agent.session.input.message",
input: [
{
role: "user",
content: [
{
type: "input_text",
text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
}
]
}
]
}
]
)
completed = events.any? do |event|
case event
when OpenAI::Beta::AgentSessionRequiresActionEvent
current = client.beta.agents.sessions.retrieve(session.id)
current.required_actions.each do |approval|
next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest)
next if handled_requests.include?(approval.request_id)
respond_to_origin_approval(client, session.id, approval)
handled_requests.add(approval.request_id)
end
false
when OpenAI::Beta::AgentSessionTurnItemDoneEvent
# Preserve streamed screenshots; retrieved history may omit image bytes.
item = event.item
if item.is_a?(OpenAI::Beta::AgentOutputItem::ComputerUseCall)
output = item.output
if output&.type.to_s == "computer_screenshot"
prefix = "data:image/jpeg;base64,"
if output.image_url.start_with?(prefix)
last_screenshot.replace(Base64.strict_decode64(output.image_url.delete_prefix(prefix)))
end
end
end
false
when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent
puts event.text
when OpenAI::Beta::AgentSessionTurnCompletedEvent
event.turn.subagent_id.nil?
when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent
raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil?
when OpenAI::Beta::AgentSessionErrorEvent
raise event.error.message
when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent
raise "Session failed: #{event.type}"
else
false
end
end
raise "Stream closed before the browser task finished." unless completed
ensure
events.close
end
El ejemplo imprime la respuesta del agente. Comprueba que incluya el título y la URL de la guía de inicio rápido.
Cerrar el flujo de eventos no detiene la tarea. Para detenerla, cancela el turno. Si falla la conexión o no tienes certeza del resultado, sigue las instrucciones de recuperación. El ejemplo ampliado de cURL incluye información de diagnóstico sobre el transporte y los errores.
Las operaciones del navegador aparecen como elementos computer_use_call en la salida de la sesión. Los elementos de actividad tienen la misma estructura en el flujo de eventos y en el historial guardado de la sesión.
| Campo |
Significado |
id |
Identificador del elemento de actividad. |
turn_id |
Turno en el que se produjo la actividad. |
title |
Descripción de la actividad del navegador, o null. |
status |
in_progress, completed, failed o incomplete. |
output |
Captura de pantalla, si está disponible. |
Usa el título y el estado para mostrar el progreso en tu aplicación. Un elemento de actividad del navegador describe una operación de la herramienta. No contiene la respuesta final del agente ni indica si el turno completo ha finalizado. Consulta Eventos y elementos para conocer el modelo de eventos de la sesión.
Para mostrar el progreso del navegador en tu aplicación, establece include_screenshots en true en la herramienta computer_use. De forma predeterminada, la salida de la API omite las capturas de pantalla, aunque el agente puede verlas.
Cuando se completa una actividad del navegador, el evento correspondiente incluye en item.output la última captura de pantalla emitida, si está disponible:
1234{
"type": "computer_screenshot",
"image_url": "data:image/jpeg;base64,..."
}
Usa image_url para mostrar la captura de pantalla. Algunas operaciones devuelven output: null incluso si las capturas están habilitadas, por lo que tu aplicación debe poder gestionar elementos de actividad sin imagen.
Las capturas de pantalla pueden contener datos sensibles de la página o de la cuenta. Muéstralas solo a usuarios autorizados y evita que se guarden en los registros de la aplicación.
Guarda las capturas de pantalla disponibles en los eventos agent.session.turn.item.done mientras procesas el flujo. El historial que recuperes podría omitir los bytes de la imagen. Conserva la imagen en tu aplicación para no depender de una solicitud posterior de la lista de elementos. Una vez que termina el turno, los ejemplos de los SDK enumeran las operaciones del navegador y guardan la última captura obtenida. La mayoría usa browser-screenshot.jpg. El ejemplo de Java usa un archivo temporal.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# Screenshot bytes are retained in the private event stream file printed in terminal 1.
# Keep that file: saved history may omit image bytes.
# Save the first page privately; image data stays out of terminal output.
activity_file=$(mktemp)
curl --silent --show-error --fail-with-body --get \
"https://api.openai.com/v1/agents/sessions/$session_id/items" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
--data-urlencode "order=asc" --data-urlencode "limit=100" \
--output "$activity_file"
jq '.data[] | select(.type == "computer_use_call") |
{id, title: (.title // "Browser activity"), status,
has_screenshot: (.output != null)}' "$activity_file"
jq '{has_more, last_id}' "$activity_file"
printf 'Saved activity JSON: %s\n' "$activity_file"
# If has_more is true, repeat the GET with --data-urlencode "after=LAST_ID".
# Use the returned last_id and keep order=asc on every page.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18for await (const item of client.beta.agents.sessions.items.list(session.id, {
order: "asc",
limit: 100,
})) {
if (item.type !== "computer_use_call") continue;
console.log(item.title ?? "Browser activity", item.status);
}
if (screenshot) {
const file = await open("browser-screenshot.jpg", "wx", 0o600);
try {
await file.writeFile(screenshot);
} finally {
await file.close();
}
console.log("Saved browser-screenshot.jpg");
} else {
console.log("No browser screenshot was returned.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18for item in client.beta.agents.sessions.items.list(
session.id, order="asc", limit=100
):
if item.type != "computer_use_call":
continue
print(item.title or "Browser activity", item.status)
if last_screenshot is not None:
screenshot_path = Path("browser-screenshot.jpg")
descriptor = os.open(
screenshot_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600
)
with os.fdopen(descriptor, "wb") as screenshot_file:
screenshot_file.write(last_screenshot)
print("Saved screenshot:", screenshot_path)
else:
print("No screenshot was returned.")
ready_to_delete = completed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35items := client.Beta.Agents.Sessions.Items.ListAutoPaging(ctx, session.ID, openai.BetaAgentSessionItemListParams{
Order: "asc", Limit: openai.Int(100),
})
for items.Next() {
item := items.Current()
if item.Type != "computer_use_call" {
continue
}
activity := item.AsComputerUseCall()
title := activity.Title
if title == "" {
title = "Browser activity"
}
fmt.Println(title, activity.Status)
}
if err := items.Err(); err != nil {
return err
}
if lastScreenshot != nil {
file, err := os.OpenFile("browser-screenshot.jpg", os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
if err != nil {
return err
}
defer file.Close()
if err := file.Chmod(0o600); err != nil {
return err
}
if _, err := file.Write(lastScreenshot); err != nil {
return err
}
fmt.Println("Saved screenshot: browser-screenshot.jpg")
} else {
fmt.Println("No screenshot was returned.")
}
readyToDelete = true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var items =
client
.beta()
.agents()
.sessions()
.items()
.list(
ItemListParams.builder()
.sessionId(session.id())
.order(ItemListParams.Order.ASC)
.limit(100L)
.build());
for (var item : items.autoPager()) {
if (item.computerUseCall().isEmpty()) continue;
var activity = item.computerUseCall().get();
System.out.println(activity.title().orElse("Browser activity") + " " + activity.status());
}
if (lastScreenshot != null) {
var screenshotPath = Files.createTempFile("browser-screenshot-", ".jpg");
Files.write(screenshotPath, lastScreenshot);
System.out.println("Saved screenshot: " + screenshotPath);
} else {
System.out.println("No screenshot was returned.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22items = client.beta.agents.sessions.items.list(
session.id,
order: "asc",
limit: 100
)
items.auto_paging_each do |item|
next unless item.is_a?(OpenAI::Beta::AgentSessionItem::ComputerUseCall)
puts "#{item.title || "Browser activity"}: #{item.status}"
end
if last_screenshot.empty?
puts "No screenshot was returned."
else
screenshot_path = "browser-screenshot.jpg"
File.open(screenshot_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
file.chmod(0o600)
file.binmode
file.write(last_screenshot)
end
puts "Saved screenshot: #{screenshot_path}"
end
Los ejemplos de los SDK no sobrescriben archivos existentes. Mueve o elimina browser-screenshot.jpg antes de volver a ejecutarlos.
Para realizar tareas como leer los issues de un repositorio privado de GitHub, es necesario iniciar sesión en el navegador. Tu aplicación gestiona el inicio de sesión para que los usuarios puedan elegir un método e ingresar sus credenciales fuera del chat.
El inicio de sesión puede requerir varias solicitudes. Por ejemplo, un sitio puede pedirle al usuario que elija el inicio de sesión por correo electrónico, ingrese su dirección de correo y luego ingrese un código de verificación. Crea la interfaz a partir de los métodos de inicio de sesión y los campos de cada solicitud.
Solo el agente principal puede solicitar la autenticación en el navegador. Los subagentes no pueden hacerlo. Este flujo admite direcciones de correo electrónico, contraseñas y códigos de verificación. No admite llaves de acceso ni el inicio de sesión mediante códigos QR. Si un sitio requiere un método no compatible, no se podrá completar el inicio de sesión con este flujo.
Mantén abierto el flujo de eventos de la tarea y atiende las solicitudes de aprobación de acceso a orígenes a medida que lleguen. Cuando recibas agent.session.requires_action, recupera la sesión y busca en sus required_actions actuales las entradas de tipo computer_use_approval_request cuyo campo anidado request.type tenga el valor browser_authentication.
Usa el objeto request anidado para mostrar la interfaz de inicio de sesión:
| Campo |
Cómo usarlo |
reason |
Si se proporciona un motivo, úsalo para explicar por qué se necesitan los datos. Puede ser null. |
credential_origin |
Muestra el destino para el que se solicitan las credenciales. Puede ser null. |
fields |
Muestra los campos de entrada según los valores de id, label, type y required de cada campo. La lista puede estar vacía. |
options |
Muestra los métodos de inicio de sesión disponibles. Cada opción tiene un id, un label y una lista field_ids que identifica sus campos de entrada. |
Si la solicitud ofrece métodos de inicio de sesión, permite que el usuario elija uno y muestra los campos correspondientes. Si no ofrece métodos, muestra directamente los campos de la solicitud.
Pide a los usuarios que ingresen sus credenciales solo si pueden verificar el destino. Si el origen de las credenciales no se indica o les resulta desconocido, cancela la solicitud de autenticación cuando no puedan verificarlo por otros medios.
Envía los datos ingresados por el usuario con el request_id de la acción que contiene la solicitud, o cancela la solicitud de autenticación si el usuario decide no iniciar sesión. Sigue atendiendo las solicitudes a medida que lleguen. Enviar una respuesta no garantiza que el inicio de sesión se haya completado. Sigue la tarea hasta que termine y comprueba el resultado.
Un sitio que permita iniciar sesión con un código enviado por correo electrónico o con una contraseña podría pedirle primero al usuario que elija un método, sin solicitarle que complete ningún campo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
"type": "computer_use_approval_request",
"turn_id": "turn_example",
"request_id": "request_choose_method",
"request": {
"type": "browser_authentication",
"reason": "Choose how to sign in to the issue tracker",
"credential_origin": "https://issues.example.com",
"fields": [],
"options": [
{ "id": "email_code", "label": "Email me a code", "field_ids": [] },
{ "id": "password", "label": "Use a password", "field_ids": [] }
]
}
}
Si el usuario elige iniciar sesión con un código enviado por correo electrónico, envía selected_option: "email_code" con fields: [] usando el request_id de esta solicitud. Luego, el sitio podría pedir la dirección de correo electrónico y el código de verificación en solicitudes separadas. Muestra cada nueva solicitud con sus propios campos e identificadores e incluye selected_option solo cuando esa solicitud ofrezca opciones.
Cuando el usuario complete una solicitud de inicio de sesión, envía su respuesta a través del punto de acceso de eventos de la sesión. Establece action en submit y usa los identificadores de la solicitud y de los campos de la aprobación pendiente. Por ejemplo, así se responde a una solicitud de dirección de correo electrónico:
12345678910111213{
"events": [
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": {
"type": "browser_authentication",
"action": "submit",
"fields": [{ "field_id": "email", "value": "USER_ENTERED_VALUE" }]
}
}
]
}
Si la solicitud ofrece métodos de inicio de sesión, incluye el ID del método elegido en response.selected_option. Envía únicamente los campos que aparecen en field_ids de ese método, con un valor no vacío para cada campo obligatorio. Si el método no tiene campos, envía fields: [].
Si no se ofrecen métodos de inicio de sesión, omite selected_option y envía directamente los campos de la solicitud. Cuando la solicitud incluya campos, envía al menos uno, incluso si todos son opcionales.
Envía los datos de inicio de sesión únicamente a través de este evento específico. Los valores enviados quedan fuera de los datos de entrada del modelo del agente y se omiten en los elementos de respuesta de autenticación del historial de la sesión.
Trata todos los valores como datos sensibles, incluidas las direcciones de correo electrónico. Oculta los valores ingresados, evita que se guarden en registros, sistemas de análisis o en el estado guardado de la interfaz, y vacía el formulario después del envío. No envíes credenciales en mensajes comunes ni en los resultados de herramientas de función.
Omite turn_id en el evento de envío. Consulta los límites de envío de datos de autenticación para conocer las restricciones de los campos y de la carga útil.
Una respuesta 202 con el cuerpo vacío confirma que se aceptaron los datos enviados. El inicio de sesión aún podría estar pendiente. Sigue los eventos de la sesión para ver si llegan más solicitudes o si se reanuda la tarea. Deshabilita los reintentos automáticos de HTTP o del SDK al enviar credenciales. Si no tienes certeza de que se hayan aceptado los datos, vuelve a consultar la sesión antes de continuar.
Si el usuario decide no iniciar sesión, responde a la solicitud pendiente con action: "cancel". Usa su request_id y omite fields y selected_option:
123456789{
"events": [
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": { "type": "browser_authentication", "action": "cancel" }
}
]
}
Esto cancela la solicitud de autenticación. Para detener la tarea, cancela el turno.
Tu aplicación gestiona las aprobaciones de acceso a orígenes y las solicitudes de inicio de sesión mientras sigue los eventos de la sesión. Define la siguiente función auxiliar antes de ejecutar la tarea. La función muestra el destino, recopila los datos sin mostrar los valores ingresados y envía la respuesta. Si el usuario decide no continuar, la función cancela la solicitud de inicio de sesión.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# Run in a second terminal when the private task requires input.
# Set session_id to that task's actual session ID first.
curl --silent --show-error --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
| jq '.required_actions[] | select(.type == "computer_use_approval_request")'
# Save a submission or cancellation payload from the preceding sections
# to browser-auth-response.json using the pending request and field IDs.
# Restrict file access to your user; remove it after submission.
curl --fail-with-body --retry 0 "https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @browser-auth-response.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
/** @param {OpenAI} client */
async function respondToComputerUseApproval(client, sessionId, approval) {
const request = approval.request;
if (request.type === "browser_origin_access") {
console.log("Requested origin:", request.origin);
console.log(request.reason ?? "The browser needs access to this origin.");
let input;
do {
input =
prompt("Allow access? [approve/deny/cancel, default deny] ")
.trim()
.toLowerCase() || "deny";
} while (!["approve", "deny", "cancel"].includes(input));
const decision =
input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
await client.beta.agents.sessions.events.create(sessionId, {
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_origin_access", decision },
},
],
});
return;
}
if (request.type !== "browser_authentication") {
throw new Error(`Unsupported approval request: ${request.type}`);
}
async function cancelSignIn() {
await client.beta.agents.sessions.events.create(
sessionId,
{
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_authentication", action: "cancel" },
},
],
},
{ maxRetries: 0 }
);
}
console.log(request.reason ?? "Sign in to continue");
console.log(
"Credential origin:",
request.credential_origin ?? "Not supplied"
);
const consent = prompt("Have you verified the sign-in destination? [y/N] ")
.trim()
.toLowerCase();
if (!["y", "yes"].includes(consent)) {
await cancelSignIn();
return;
}
let selectedOption;
let activeFields = request.fields;
if (request.options.length > 0) {
request.options.forEach((option, index) => {
console.log(`${index + 1}. ${option.label}`);
});
let choice;
do {
const input = prompt("Choose a sign-in method, or enter cancel: ")
.trim()
.toLowerCase();
if (input === "cancel") {
await cancelSignIn();
return;
}
choice = Number(input);
} while (
!Number.isInteger(choice) ||
choice < 1 ||
choice > request.options.length
);
selectedOption = request.options[choice - 1];
activeFields = request.fields.filter((field) =>
selectedOption.field_ids.includes(field.id)
);
}
const fields = [];
do {
for (const field of activeFields) {
while (true) {
const value = prompt.hide(
`${field.label}${field.required ? "" : " (optional)"} (leave blank for options): `
);
if (value.length > 0) {
fields.push({ field_id: field.id, value });
break;
}
let action;
do {
action = prompt(
field.required
? "Enter a value or cancel sign-in? [enter/cancel, default enter] "
: "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
)
.trim()
.toLowerCase();
} while (
![
"",
"enter",
"cancel",
...(field.required ? [] : ["skip"]),
].includes(action)
);
if (action === "cancel") {
await cancelSignIn();
return;
}
if (!field.required && (action === "" || action === "skip")) break;
}
}
if (!selectedOption && activeFields.length > 0 && fields.length === 0) {
console.log(
"This form requires at least one field. Enter a value or cancel sign-in."
);
}
} while (!selectedOption && activeFields.length > 0 && fields.length === 0);
await client.beta.agents.sessions.events.create(
sessionId,
{
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: {
type: "browser_authentication",
action: "submit",
fields,
...(selectedOption ? { selected_option: selectedOption.id } : {}),
},
},
],
},
{ maxRetries: 0 }
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99from getpass import getpass
def read_authentication_response(request):
cancel = {"type": "browser_authentication", "action": "cancel"}
print(request.reason or "The agent needs you to sign in.")
print("Credential origin:", request.credential_origin or "Not supplied")
consent = input("Have you verified the sign-in destination? [y/N] ")
if consent.strip().lower() not in {"y", "yes"}:
return cancel
selected_option = None
active_fields = request.fields
if request.options:
for index, option in enumerate(request.options, start=1):
print(f"{index}. {option.label}")
while True:
choice = input("Choose a sign-in method, or enter cancel: ").strip().lower()
if choice == "cancel":
return cancel
if choice.isdigit() and 1 <= int(choice) <= len(request.options):
option = request.options[int(choice) - 1]
break
print("Enter a method number from the list, or cancel.")
selected_option = option.id
fields_by_id = {field.id: field for field in request.fields}
active_fields = [fields_by_id[field_id] for field_id in option.field_ids]
values = []
while True:
for field in active_fields:
while True:
label = field.label if field.required else f"{field.label} (optional)"
value = getpass(f"{label} (leave blank for options): ")
if value:
values.append({"field_id": field.id, "value": value})
break
choices = {"", "enter", "cancel"}
if field.required:
question = "Enter a value or cancel sign-in? [enter/cancel, default enter] "
else:
choices.add("skip")
question = "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
while True:
action = input(question).strip().lower()
if action in choices:
break
if action == "cancel":
return cancel
if not field.required and action in {"", "skip"}:
break
if selected_option is not None or not active_fields or values:
break
print("This form requires at least one field. Enter a value or cancel sign-in.")
response = {
"type": "browser_authentication",
"action": "submit",
"fields": values,
}
if selected_option is not None:
response["selected_option"] = selected_option
return response
def respond_to_computer_use_approval(client, session_id, approval):
request = approval.request
approval_client = client
if request.type == "browser_origin_access":
print(request.reason or "The browser needs access to an origin.")
print("Origin:", request.origin)
while True:
decision = (
input("Allow this origin? [approve/deny/cancel; default: deny] ")
.strip()
.lower()
or "deny"
)
if decision in {"approve", "deny", "cancel"}:
break
print("Enter approve, deny, or cancel.")
response = {"type": "browser_origin_access", "decision": decision}
elif request.type == "browser_authentication":
response = read_authentication_response(request)
approval_client = client.with_options(max_retries=0)
else:
raise RuntimeError(f"Unsupported computer-use approval: {request.type}")
approval_client.beta.agents.sessions.events.create(
session_id,
events=[
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": approval.request_id,
"response": response,
}
],
)
# Admission does not establish sign-in or navigation success; keep reading events.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203import (
"context"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"golang.org/x/term"
)
func respondToComputerUseApproval(ctx context.Context, client *openai.Client, sessionID string, approval openai.AgentSessionRequiredActionComputerUseApprovalRequest) error {
send := func(response openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion) error {
return client.Beta.Agents.Sessions.Events.New(ctx, sessionID, openai.BetaAgentSessionEventNewParams{
Events: []openai.AgentSessionInputParamUnion{{
OfParamAgentSessionInputComputerUseApprovalRequestResult: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResult{
RequestID: approval.RequestID,
Response: response,
},
}},
}, option.WithMaxRetries(0))
}
readLine := func(prompt string) (string, error) {
fmt.Print(prompt)
var value strings.Builder
var input [1]byte
for {
if _, err := io.ReadFull(os.Stdin, input[:]); err != nil {
return "", err
}
if input[0] == '\n' {
return strings.TrimSpace(value.String()), nil
}
value.WriteByte(input[0])
}
}
cancelAuthentication := func() error {
return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{
OfBrowserAuthenticationCancel: &openai.AgentBrowserAuthenticationCancelParam{},
})
}
switch approval.Request.Type {
case "browser_origin_access":
request := approval.Request.AsBrowserOriginAccess()
fmt.Println("Requested origin:", request.Origin)
if request.Reason != "" {
fmt.Println(request.Reason)
}
response := openai.AgentBrowserOriginAccessParam{Decision: "deny"}
for {
choice, err := readLine("Allow this origin? [approve/deny/cancel; default: deny] ")
if err != nil {
return err
}
switch strings.ToLower(choice) {
case "approve":
response.Decision = "approve"
case "", "deny":
response.Decision = "deny"
case "cancel":
response.Decision = "cancel"
default:
fmt.Println("Enter approve, deny, or cancel.")
continue
}
break
}
return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{OfBrowserOriginAccess: &response})
case "browser_authentication":
// Collect credentials only for an authentication request.
default:
return fmt.Errorf("unsupported computer-use approval: %s", approval.Request.Type)
}
challenge := approval.Request.AsBrowserAuthentication()
reason := challenge.Reason
if reason == "" {
reason = "The agent needs you to sign in."
}
origin := challenge.CredentialOrigin
if origin == "" {
origin = "Not supplied"
}
fmt.Println(reason)
fmt.Println("Credential origin:", origin)
consent, err := readLine("Have you verified the sign-in destination? [y/N] ")
if err != nil {
return err
}
if strings.ToLower(consent) != "y" && strings.ToLower(consent) != "yes" {
return cancelAuthentication()
}
response := openai.AgentBrowserAuthenticationSubmitParam{
Action: "submit",
Fields: []openai.AgentBrowserAuthenticationSubmitParamField{},
}
activeFields := challenge.Fields
if len(challenge.Options) > 0 {
for index, option := range challenge.Options {
fmt.Printf("%d. %s\n", index+1, option.Label)
}
for {
choice, err := readLine("Choose a sign-in method [number/cancel]: ")
if err != nil {
return err
}
if strings.EqualFold(choice, "cancel") {
return cancelAuthentication()
}
index, err := strconv.Atoi(choice)
if err != nil || index < 1 || index > len(challenge.Options) {
fmt.Println("Enter a method number from the list, or enter cancel.")
continue
}
option := challenge.Options[index-1]
response.SelectedOption = openai.String(option.ID)
activeFields = nil
for _, fieldID := range option.FieldIDs {
found := false
for _, field := range challenge.Fields {
if field.ID == fieldID {
activeFields = append(activeFields, field)
found = true
break
}
}
if !found {
return fmt.Errorf("sign-in method references an unknown field: %s", fieldID)
}
}
break
}
}
collectFields:
for {
response.Fields = response.Fields[:0]
for _, field := range activeFields {
fieldInput:
for {
choices := "enter/cancel; default: enter"
if !field.Required {
choices = "enter/skip/cancel; default: enter"
}
choice, err := readLine(fmt.Sprintf("%s [%s]: ", field.Label, choices))
if err != nil {
return err
}
switch strings.ToLower(choice) {
case "cancel":
return cancelAuthentication()
case "skip":
if field.Required {
fmt.Println("This field is required. Enter a value or cancel sign-in.")
continue
}
break fieldInput
case "", "enter":
fmt.Printf("%s (hidden): ", field.Label)
value, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return err
}
if len(value) == 0 {
fmt.Println("No value entered. Choose an action for this field.")
continue
}
response.Fields = append(response.Fields, openai.AgentBrowserAuthenticationSubmitParamField{
FieldID: field.ID, Value: string(value),
})
clear(value)
break fieldInput
default:
fmt.Println("Choose one of the listed actions.")
}
}
}
if len(challenge.Options) > 0 || len(challenge.Fields) == 0 || len(response.Fields) > 0 {
break
}
for {
choice, err := readLine("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ")
if err != nil {
return err
}
switch strings.ToLower(choice) {
case "retry":
continue collectFields
case "", "cancel":
return cancelAuthentication()
default:
fmt.Println("Enter retry or cancel.")
}
}
}
// Admission does not establish login success; keep following the session.
return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{
OfBrowserAuthenticationSubmit: &response,
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.beta.agents.AgentBrowserAuthenticationCancelParam;
import com.openai.models.beta.agents.AgentBrowserAuthenticationSubmitParam;
import com.openai.models.beta.agents.AgentBrowserOriginAccessParam;
import com.openai.models.beta.agents.AgentSession;
import com.openai.models.beta.agents.AgentSessionInputMessageParam;
import com.openai.models.beta.agents.AgentSessionInputParam;
import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult;
import com.openai.models.beta.agents.AgentToolParam;
import com.openai.models.beta.agents.EnvironmentParam;
import com.openai.models.beta.agents.sessions.SessionCreateParams;
import com.openai.models.beta.agents.sessions.events.EventCreateParams;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
static void cancelAuthentication(OpenAIClient client, String sessionId, String requestId) {
var cancellation =
AgentSessionInputComputerUseApprovalRequestResult.builder()
.requestId(requestId)
.response(AgentBrowserAuthenticationCancelParam.builder().build())
.build();
client
.withOptions(options -> options.maxRetries(0))
.beta()
.agents()
.sessions()
.events()
.create(EventCreateParams.builder().sessionId(sessionId).addEvent(cancellation).build());
}
static void respondToComputerUseApproval(
OpenAIClient client,
String sessionId,
AgentSession.RequiredAction.ComputerUseApprovalRequest approval) {
var console = System.console();
if (console == null)
throw new IllegalStateException("Run this example in an interactive terminal.");
var result =
AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId());
if (approval.request().browserOriginAccess().isPresent()) {
var request = approval.request().browserOriginAccess().get();
console.printf("Origin: %s%n", request.origin());
console.printf("Reason: %s%n", request.reason().orElse("Not supplied"));
String decision;
while (true) {
String input =
console.readLine("Allow browser access? [approve/deny/cancel; default deny] ");
if (input == null) throw new IllegalStateException("Approval input closed.");
decision = input.strip().toLowerCase(java.util.Locale.ROOT);
if (decision.isEmpty()) decision = "deny";
if (List.of("approve", "deny", "cancel").contains(decision)) break;
console.printf("Enter approve, deny, or cancel.%n");
}
result.response(
AgentBrowserOriginAccessParam.builder()
.decision(AgentBrowserOriginAccessParam.Decision.of(decision))
.build());
} else if (approval.request().browserAuthentication().isPresent()) {
var challenge = approval.request().browserAuthentication().get();
console.printf("%s%n", challenge.reason().orElse("The agent needs you to sign in."));
console.printf(
"Credential origin: %s%n", challenge.credentialOrigin().orElse("Not supplied"));
String consent = console.readLine("Have you verified the sign-in destination? [y/N] ");
if (consent == null
|| !List.of("y", "yes").contains(consent.strip().toLowerCase(java.util.Locale.ROOT))) {
cancelAuthentication(client, sessionId, approval.requestId());
return;
}
var response = AgentBrowserAuthenticationSubmitParam.builder().fields(List.of());
var activeFields = challenge.fields();
if (!challenge.options().isEmpty()) {
for (int i = 0; i < challenge.options().size(); i++) {
console.printf("%d. %s%n", i + 1, challenge.options().get(i).label());
}
while (true) {
String choice = console.readLine("Choose a sign-in method, or enter cancel: ");
if (choice == null || choice.strip().equalsIgnoreCase("cancel")) {
cancelAuthentication(client, sessionId, approval.requestId());
return;
}
int index;
try {
index = Integer.parseInt(choice.strip()) - 1;
} catch (NumberFormatException e) {
console.printf("Enter a method number from the list, or cancel.%n");
continue;
}
if (index < 0 || index >= challenge.options().size()) {
console.printf("Enter a method number from the list, or cancel.%n");
continue;
}
var option = challenge.options().get(index);
response.selectedOption(option.id());
activeFields =
challenge.fields().stream()
.filter(field -> option.fieldIds().contains(field.id()))
.toList();
break;
}
}
while (true) {
int fieldsSubmitted = 0;
for (var field : activeFields) {
while (true) {
String choice =
console.readLine(
field.required()
? "%s [enter/cancel; default enter]: "
: "%s [enter/skip/cancel; default skip]: ",
field.label());
if (choice == null || choice.strip().equalsIgnoreCase("cancel")) {
cancelAuthentication(client, sessionId, approval.requestId());
return;
}
choice = choice.strip().toLowerCase(java.util.Locale.ROOT);
if (choice.isEmpty()) choice = field.required() ? "enter" : "skip";
if (choice.equals("skip") && !field.required()) break;
if (!choice.equals("enter")) {
console.printf("Choose one of the displayed options.%n");
continue;
}
char[] characters = console.readPassword("%s: ", field.label());
if (characters == null) {
cancelAuthentication(client, sessionId, approval.requestId());
return;
}
String value = new String(characters);
Arrays.fill(characters, '\0');
if (value.isEmpty()) {
if (!field.required()) break;
console.printf("This field is required.%n");
continue;
}
response.addField(
AgentBrowserAuthenticationSubmitParam.Field.builder()
.fieldId(field.id())
.value(value)
.build());
fieldsSubmitted++;
break;
}
}
if (!challenge.options().isEmpty() || activeFields.isEmpty() || fieldsSubmitted > 0) break;
console.printf("Enter at least one field to submit this form, or cancel.%n");
}
result.response(response.build());
} else {
throw new IllegalStateException("Unsupported computer-use approval request.");
}
// An uncertain approval must not resend credentials automatically.
client
.withOptions(options -> options.maxRetries(0))
.beta()
.agents()
.sessions()
.events()
.create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build());
// Admission does not establish sign-in or navigation success; keep following the session.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125require "io/console"
def computer_use_sign_in_choice(prompt)
print prompt
input = $stdin.gets || raise(EOFError, "Input closed before a sign-in choice.")
input.strip.downcase
end
def computer_use_authentication_response(request)
cancel = {
type: "browser_authentication",
action: "cancel"
}
puts request.reason || "The agent needs you to sign in."
puts "Credential origin: #{request.credential_origin || "Not supplied"}"
consent = computer_use_sign_in_choice("Have you verified the sign-in destination? [y/N] ")
return cancel unless ["y", "yes"].include?(consent)
selected_option = nil
active_fields = request.fields
unless request.options.empty?
request.options.each_with_index do |option, index|
puts "#{index + 1}. #{option.label}"
end
option = loop do
choice = computer_use_sign_in_choice("Choose a sign-in method [number/cancel]: ")
return cancel if choice == "cancel"
if choice.match?(/\A\d+\z/) && choice.to_i.between?(1, request.options.length)
break request.options[choice.to_i - 1]
end
puts "Enter a method number from the list, or enter cancel."
end
selected_option = option.id
fields_by_id = request.fields.to_h { |field| [field.id, field] }
active_fields = option.field_ids.map { |field_id| fields_by_id.fetch(field_id) }
end
values = []
loop do
values.clear
active_fields.each do |field|
loop do
choices = field.required ? "enter/cancel; default: enter" : "enter/skip/cancel; default: enter"
choice = computer_use_sign_in_choice("#{field.label} [#{choices}]: ")
case choice
when "cancel"
return cancel
when "skip"
break unless field.required
puts "This field is required. Enter a value or cancel sign-in."
when "", "enter"
value = $stdin.getpass("#{field.label} (hidden): ")
if value.empty?
puts "No value entered. Choose an action for this field."
next
end
values << {
field_id: field.id,
value: value
}
break
else
puts "Choose one of the listed actions."
end
end
end
break unless request.options.empty? && !request.fields.empty? && values.empty?
loop do
choice = computer_use_sign_in_choice("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ")
return cancel if ["", "cancel"].include?(choice)
break if choice == "retry"
puts "Enter retry or cancel."
end
end
response = {
type: "browser_authentication",
action: "submit",
fields: values
}
response[:selected_option] = selected_option unless selected_option.nil?
response
end
def respond_to_computer_use_approval(client, session_id, approval)
request = approval.request
case request.type.to_s
when "browser_origin_access"
puts request.reason || "The browser needs access to an origin."
puts "Origin: #{request.origin}"
decision = loop do
print "Allow this origin? [approve/deny/cancel; default: deny] "
input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.")
choice = input.strip.downcase
choice = "deny" if choice.empty?
break choice if ["approve", "deny", "cancel"].include?(choice)
puts "Enter approve, deny, or cancel."
end
response = {
type: "browser_origin_access",
decision: decision
}
when "browser_authentication"
response = computer_use_authentication_response(request)
else
raise "Unsupported computer-use approval: #{request.type}"
end
client.beta.agents.sessions.events.create(
session_id,
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: response
}
],
request_options: { max_retries: 0 }
)
# Admission does not establish sign-in or navigation success; keep reading events.
end
El siguiente ejemplo le pide al agente que lea los issues de un repositorio privado de GitHub. Reemplaza https://github.com/acme/private-repo/issues por la URL de una página de issues a la que tengas acceso.
Abre el flujo de eventos antes de enviar la tarea y llama a la función auxiliar cada vez que la sesión requiera datos del usuario.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# Terminal 1: create a new session and stream its first task.
# Replace the illustrative repository URL with one you can access.
curl --no-buffer --fail-with-body https://api.openai.com/v1/agents/sessions \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"agent": {
"model": "gpt-6-astra",
"instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
"tools": [{ "type": "computer_use", "include_screenshots": false }]
},
"environment": {
"type": "openai_hosted",
"desktop": { "enabled": true },
"network": { "access": "enabled" }
},
"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.",
"stream": true
}'
# Copy session.id from agent.session.created into terminal 2.
# Keep this stream open while responding to origin and sign-in approvals there.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100import OpenAI from "openai";
const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
agent: {
model: "gpt-6-astra",
instructions:
"Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
tools: [{ type: "computer_use", include_screenshots: false }],
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" },
},
});
console.log("Session ID:", session.id);
let completed = false;
let readyToDelete = false;
try {
const events = await client.beta.agents.sessions.events.stream(session.id);
const handledRequests = new Set();
try {
await client.beta.agents.sessions.events.create(session.id, {
events: [
{
type: "agent.session.input.message",
input: [
{
role: "user",
content: [
{
type: "input_text",
// Replace this illustrative URL with your private repository.
text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.",
},
],
},
],
},
],
});
for await (const event of events) {
switch (event.type) {
case "agent.session.requires_action": {
const current = await client.beta.agents.sessions.retrieve(
session.id
);
for (const approval of current.required_actions) {
if (
approval.type === "computer_use_approval_request" &&
!handledRequests.has(approval.request_id)
) {
await respondToComputerUseApproval(client, session.id, approval);
handledRequests.add(approval.request_id);
}
}
break;
}
case "agent.session.turn.output_text.done":
console.log(event.text);
break;
case "error":
throw new Error(event.error.message);
case "agent.session.failed":
case "agent.session.environment.failed":
throw new Error(`Agent lifecycle failure: ${event.type}`);
case "agent.session.turn.failed":
if (event.turn.subagent_id === null) {
throw new Error(event.turn.error?.message ?? "Browser task failed");
}
break;
case "agent.session.turn.cancelled":
if (event.turn.subagent_id === null) {
throw new Error("Browser task was cancelled");
}
break;
case "agent.session.turn.completed":
if (event.turn.subagent_id === null) completed = true;
break;
}
if (completed) break;
}
if (!completed) {
throw new Error("Stream closed before the browser task finished");
}
console.log();
} finally {
events.controller.abort();
}
readyToDelete = completed;
} catch (error) {
console.error(
`Session ${session.id} was kept. Use the same ID to check its status before trying again.`
);
throw error;
} finally {
if (readyToDelete) await client.beta.agents.sessions.delete(session.id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85from openai import OpenAI
client = OpenAI()
session = client.beta.agents.sessions.create(
agent={
"model": "gpt-6-astra",
"instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
"tools": [{"type": "computer_use", "include_screenshots": False}],
},
environment={
"type": "openai_hosted",
"desktop": {"enabled": True},
"network": {"access": "enabled"},
},
)
print("Session ID:", session.id, flush=True)
handled_requests = set()
completed = False
ready_to_delete = False
try:
with client.beta.agents.sessions.events.stream(session.id) as events:
client.beta.agents.sessions.events.create(
session.id,
events=[
{
"type": "agent.session.input.message",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
# Replace this illustrative URL with your private repository.
"text": "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.",
}
],
}
],
}
],
)
for event in events:
if event.type == "agent.session.requires_action":
current = client.beta.agents.sessions.retrieve(session.id)
for approval in current.required_actions:
if (
approval.type == "computer_use_approval_request"
and approval.request_id not in handled_requests
):
respond_to_computer_use_approval(client, session.id, approval)
handled_requests.add(approval.request_id)
elif event.type == "agent.session.turn.output_text.done":
print(event.text, flush=True)
elif event.type == "agent.session.turn.completed":
if event.turn.subagent_id is None:
completed = True
print()
break
elif event.type in {
"agent.session.turn.failed",
"agent.session.turn.cancelled",
}:
if event.turn.subagent_id is None:
raise RuntimeError(f"Browser task ended: {event.type}")
elif event.type == "error":
raise RuntimeError(event.error.message)
elif event.type in {
"agent.session.failed",
"agent.session.environment.failed",
}:
raise RuntimeError(f"Session failed: {event.type}")
else:
raise RuntimeError("Stream closed before the browser task finished.")
ready_to_delete = completed
except (Exception, KeyboardInterrupt):
print(
f"Session {session.id} was kept. Use the same ID to check its status before trying again.",
flush=True,
)
raise
finally:
if ready_to_delete:
client.beta.agents.sessions.delete(session.id)
client.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95ctx := context.Background()
client := openai.NewClient()
session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{
Agent: openai.BetaAgentSessionNewParamsAgent{
Model: openai.String("gpt-6-astra"),
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."),
Tools: []openai.AgentToolParamUnion{{
OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(false)},
}},
},
Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{
Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: true},
Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"},
}},
})
if err != nil {
return err
}
fmt.Println("Session ID:", session.ID)
completed := false
defer func() {
if !completed {
fmt.Fprintf(os.Stderr, "Session %s was not deleted. Retrieve it and check required_actions before attempting recovery.\n", session.ID)
return
}
if _, err := client.Beta.Agents.Sessions.Delete(ctx, session.ID); err != nil {
fmt.Fprintf(os.Stderr, "Could not delete completed session %s: %v\n", session.ID, err)
}
}()
handledRequests := map[string]bool{}
events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID)
defer events.Close()
if err := events.Err(); err != nil {
return err
}
err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{
Events: []openai.AgentSessionInputParamUnion{{
OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{
Input: []openai.AgentSessionInputMessageParam{{
Role: "user",
Content: []openai.InputContentParamUnion{{
OfParamInputText: &openai.InputContentParamInputText{
// Replace this illustrative URL with your private repository.
Text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.",
},
}},
}},
},
}},
})
if err != nil {
return err
}
for events.Next() {
event := events.Current()
switch event.Type {
case "agent.session.requires_action":
current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID)
if err != nil {
return err
}
for _, action := range current.RequiredActions {
if action.Type != "computer_use_approval_request" {
continue
}
approval := action.AsComputerUseApprovalRequest()
if handledRequests[approval.RequestID] {
continue
}
if err := respondToComputerUseApproval(ctx, &client, session.ID, approval); err != nil {
return err
}
handledRequests[approval.RequestID] = true
}
case "agent.session.turn.output_text.done":
fmt.Println(event.Text)
case "agent.session.turn.completed":
if event.Turn.SubagentID == "" {
completed = true
return nil
}
case "agent.session.turn.failed", "agent.session.turn.cancelled":
if event.Turn.SubagentID == "" {
return fmt.Errorf("browser task ended: %s", event.Type)
}
case "error":
return fmt.Errorf("agent error: %s", event.Error.Message)
case "agent.session.failed", "agent.session.environment.failed":
return fmt.Errorf("session failed: %s", event.Type)
}
}
if err := events.Err(); err != nil {
return err
}
return fmt.Errorf("stream closed before the browser task finished")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102var client = OpenAIOkHttpClient.fromEnv();
var session =
client
.beta()
.agents()
.sessions()
.create(
SessionCreateParams.builder()
.agent(
SessionCreateParams.Agent.builder()
.model("gpt-6-astra")
.instructions(
"Read the requested GitHub issue list in the browser. Request"
+ " sign-in when needed. Do not create, edit, comment on, or"
+ " close issues.")
.addTool(
AgentToolParam.ComputerUse.builder()
.includeScreenshots(false)
.build())
.build())
.environment(
EnvironmentParam.OpenAIHosted.builder()
.desktop(
EnvironmentParam.OpenAIHosted.Desktop.builder()
.enabled(true)
.build())
.network(
EnvironmentParam.OpenAIHosted.Network.builder()
.access(EnvironmentParam.OpenAIHosted.Network.Access.ENABLED)
.build())
.build())
.build());
System.out.println("Session ID: " + session.id());
var handledRequests = new HashSet<String>();
try {
try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) {
client
.beta()
.agents()
.sessions()
.events()
.create(
EventCreateParams.builder()
.sessionId(session.id())
.addEvent(
AgentSessionInputParam.AgentSessionInputMessage.builder()
.addInput(
AgentSessionInputMessageParam.builder()
// Replace this illustrative URL with your private repository.
.addInputTextContent(
"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.")
.build())
.build())
.build());
boolean completed = false;
var iterator = events.stream().iterator();
while (iterator.hasNext()) {
var event = iterator.next();
if (event.requiresAction().isPresent()) {
var current = client.beta().agents().sessions().retrieve(session.id());
for (var action : current.requiredActions()) {
if (action.computerUseApprovalRequest().isEmpty()) continue;
var approval = action.computerUseApprovalRequest().get();
if (!handledRequests.contains(approval.requestId())) {
respondToComputerUseApproval(client, session.id(), approval);
handledRequests.add(approval.requestId());
}
}
}
event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text()));
if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
completed = true;
break;
}
if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent()
|| event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) {
throw new IllegalStateException("Browser task failed or was cancelled.");
}
if (event.error().isPresent()) {
throw new IllegalStateException(event.error().get().error().message());
}
if (event.failed().isPresent() || event.environmentFailed().isPresent()) {
throw new IllegalStateException("The browser session failed.");
}
}
if (!completed) {
throw new IllegalStateException("Stream closed before the browser task finished.");
}
} catch (Exception error) {
System.err.printf(
"Session %s was kept. Retrieve it and check pending actions before sending anything"
+ " again.%n",
session.id());
throw error;
}
client.beta().agents().sessions().delete(session.id());
} finally {
client.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87require "openai"
client = OpenAI::Client.new
session = client.beta.agents.sessions.create(
agent: {
model: "gpt-6-astra",
instructions: "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
tools: [
{
type: "computer_use",
include_screenshots: false
}
]
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" }
}
)
puts "Session ID: #{session.id}"
handled_requests = Set.new
begin
events = client.beta.agents.sessions.events.stream_streaming(session.id)
begin
client.beta.agents.sessions.events.create(
session.id,
events: [
{
type: "agent.session.input.message",
input: [
{
role: "user",
content: [
{
type: "input_text",
# Replace this illustrative URL with your private repository.
text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes."
}
]
}
]
}
]
)
completed = events.any? do |event|
case event
when OpenAI::Beta::AgentSessionRequiresActionEvent
current = client.beta.agents.sessions.retrieve(session.id)
current.required_actions.each do |approval|
next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest)
next if handled_requests.include?(approval.request_id)
respond_to_computer_use_approval(client, session.id, approval)
handled_requests.add(approval.request_id)
end
false
when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent
puts event.text
when OpenAI::Beta::AgentSessionTurnCompletedEvent
event.turn.subagent_id.nil?
when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent
raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil?
when OpenAI::Beta::AgentSessionErrorEvent
raise event.error.message
when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent
raise "Session failed: #{event.type}"
else
false
end
end
raise "Stream closed before the browser task finished." unless completed
ensure
events.close
end
rescue StandardError, Interrupt
warn "Session #{session.id} was not deleted. Retrieve it and check required_actions before attempting recovery."
raise
else
begin
client.beta.agents.sessions.delete(session.id)
rescue => error
warn "Could not delete completed session #{session.id}: #{error.message}"
raise
end
end
El inicio de sesión puede requerir varias solicitudes. Sigue atendiendo las solicitudes de aprobación hasta que termine la tarea. Comprueba que el resultado del agente corresponda a la tarea solicitada. En este ejemplo, verifica el título y la URL del issue que indicó el agente.
Las solicitudes de autenticación y las respuestas aceptadas aparecen en el historial de la sesión y en los eventos agent.session.turn.item.added. Los elementos de solicitud contienen los metadatos del formulario de inicio de sesión. Los elementos de respuesta registran la acción aceptada y omiten los valores de las credenciales enviadas.
Usa el historial para revisar interacciones anteriores. Para saber si un formulario de inicio de sesión aún requiere datos del usuario, consulta los valores actuales de required_actions de la sesión. El registro de una respuesta no garantiza que el inicio de sesión se haya completado.
El historial no tiene elementos específicos para las solicitudes y respuestas de aprobación de acceso a orígenes. Gestiona estas aprobaciones mediante required_actions.
Para pedir una aclaración o permitir que el usuario elija una opción durante una tarea, define una herramienta de función en agent.tools. Por ejemplo, puedes definir request_user_response para mostrar una pregunta y recopilar la respuesta. Tu aplicación proporciona el nombre de la herramienta, el esquema de sus argumentos y la interfaz de usuario.
Cuando recibas agent.session.requires_action, busca la llamada pendiente de tipo function_call para tu herramienta y usa sus arguments para mostrar la pregunta. Devuelve la respuesta a través de agent.session.input.tool_result con el turn_id y el call_id de la acción. Establece success: true y coloca la respuesta en output. Si la respuesta es estructurada, serialízala como una cadena JSON. Si el usuario decide no responder, devuelve success: false con un mensaje de error en error.
Sigue los eventos de la sesión después de devolver la respuesta. Las funciones auxiliares de aprobación del navegador que se mostraron antes gestionan el acceso a orígenes y el inicio de sesión. Amplía tu controlador de eventos para que también gestione la herramienta que hace las preguntas.
El modelo puede ver los resultados de las herramientas de función y estos se guardan en el historial de la sesión. Recopila las contraseñas y los códigos de verificación mediante la autenticación en el navegador.
Si tu aplicación se desconecta o falla una respuesta a una solicitud de aprobación, recupera la misma sesión y revisa sus required_actions actuales antes de continuar. Vuelve a crear los formularios únicamente para las solicitudes que sigan pendientes y retira los controles de las solicitudes que ya no aparezcan.
Si se desconecta el flujo de eventos, sigue las instrucciones de recuperación del flujo para volver a recibir eventos. Al restablecer la conexión, la aplicación no debe reenviar automáticamente la tarea ni una respuesta de aprobación.
Usa el estado de la respuesta para decidir cómo continuar:
| Resultado |
Qué debe hacer tu aplicación |
202 |
Borra los valores enviados y sigue los eventos de la sesión para conocer el resultado. |
400 |
Comprueba que el tipo de respuesta, la opción seleccionada, los ID de los campos y los valores obligatorios coincidan con la solicitud pendiente. Omite los campos en las respuestas de cancelación y de acceso a orígenes. |
404 |
Verifica el ID de la sesión, el ID de la solicitud y el tipo de respuesta. Vuelve a recuperar la sesión, ya que la solicitud podría haber dejado de estar disponible. |
409 |
Vuelve a consultar la sesión. Es posible que la solicitud haya vencido, que su turno haya terminado o que ya se haya aceptado otra respuesta. |
| Se perdió la conexión antes de recibir la confirmación |
Si se pierde la conexión en ese punto, se desconoce si se aceptó la respuesta. Restablece la conexión y recupera la sesión antes de decidir si vuelves a intentarlo. |
Las solicitudes de autenticación vencen después de cinco minutos. El turno al que pertenecen puede terminar mientras el usuario ingresa los datos. Vuelve a consultar la sesión antes de restaurar un formulario de inicio de sesión.
Si reintentas un envío de datos de autenticación, usa el mismo request_id, la misma opción seleccionada y el mismo valor para cada campo. Un reintento idéntico no vuelve a llenar el formulario del navegador. Si cambias los valores después de que se haya aceptado el envío, recibirás 409. Para volver a intentar el inicio de sesión, el agente debe generar una nueva solicitud.
Si falla el envío de una aprobación de acceso a un origen, vuelve a enviar la misma decisión. Si cambias una decisión que ya se aceptó, recibirás 409. Las solicitudes de acceso a orígenes permanecen pendientes durante el turno al que pertenecen. El plazo de cinco minutos de las solicitudes de autenticación no se aplica a estas solicitudes.
Usa la configuración network del entorno alojado para controlar el acceso saliente tanto del navegador como del código que se ejecuta en el entorno. Permite el acceso al sitio web de destino y a los dominios necesarios para cargar los recursos de la página o seguir redirecciones.
La aprobación de acceso a un origen es una decisión independiente del usuario y no anula la política de red. Consulta la configuración de acceso a la red para configurar el entorno.
Reutiliza la misma sesión para las tareas posteriores que necesiten el estado del navegador. Las cookies de inicio de sesión pueden vencer y, al reciclar el entorno, se borra el estado del navegador.
Cuando termines, recupera los resultados que necesites y elimina la sesión para solicitar la limpieza del entorno:
# Run after the root turn completes, fails, or is cancelled.
curl --fail-with-body -X DELETE "https://api.openai.com/v1/agents/sessions/$session_id" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY"
await client.beta.agents.sessions.delete(session.id);
if ready_to_delete:
client.beta.agents.sessions.delete(session.id)
readyToDelete := false
defer func() {
if !readyToDelete {
fmt.Fprintf(os.Stderr, "Session %s was not deleted. Retrieve it and check required_actions before attempting recovery.\n", session.ID)
return
}
if _, err := client.Beta.Agents.Sessions.Delete(ctx, session.ID); err != nil {
fmt.Fprintf(os.Stderr, "Could not delete completed session %s: %v\n", session.ID, err)
}
}()
client.beta().agents().sessions().delete(session.id());
begin
client.beta.agents.sessions.delete(session.id) if completed
rescue => error
warn "Could not delete completed session #{session.id}: #{error.message}"
raise
end
Consulta Sandboxes alojados por OpenAI para saber cuánto dura el entorno y qué ocurre al eliminarlo.
Cada envío puede incluir hasta seis valores de campo y cada campo puede aparecer una sola vez. Cada valor admite hasta 16 384 caracteres. En conjunto, los valores de los campos serializados y la opción seleccionada no deben superar los 120 KiB.
Usa los identificadores de la solicitud y de los campos de la aprobación pendiente y proporciona un valor no vacío para cada campo obligatorio. Consulta la referencia de la API de eventos de la sesión para ver el esquema de la solicitud.
Ejemplo ampliado de gestión de solicitudes y del flujo de eventos con cURL
Usa esta versión cuando necesites distinguir entre fallas de transporte, errores HTTP y respuestas no válidas a solicitudes de creación de sesiones. También filtra la salida del flujo de eventos e indica los tipos y códigos de error. Sigue el mismo procedimiento con dos terminales que el primer ejemplo. Atiende las solicitudes de aprobación en la segunda terminal a medida que lleguen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117create_browser_session() {
unset session_id
local result http_status body curl_status
if result=$(curl --silent --fail-with-body --write-out '\n%{http_code}' \
https://api.openai.com/v1/agents/sessions \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"model": "gpt-6-astra",
"instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
"tools": [{ "type": "computer_use", "include_screenshots": true }]
},
"environment": {
"type": "openai_hosted",
"desktop": { "enabled": true },
"network": { "access": "enabled" }
}
}'); then curl_status=0; else curl_status=$?; fi
http_status=$(printf '%s\n' "$result" | tail -n 1)
body=$(printf '%s\n' "$result" | sed '$d')
[[ "$http_status" =~ ^[0-9]{3}$ ]] || http_status=000
if [ "$curl_status" -ne 0 ] || [[ ! "$http_status" =~ ^2[0-9][0-9]$ ]]; then
printf '%s' "$body" | jq --raw-input --slurp --compact-output \
--arg status "$http_status" --arg curl_status "$curl_status" '
def identifier:
if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
(try fromjson catch {}) as $response
| (if ($response | type) == "object" then $response.error // $response else {} end) as $error
| if $curl_status != "0" and $curl_status != "22" then
{status: $status, type: "transport_error", code: ("curl_" + $curl_status)}
else
{status: $status, type: ((try ($error.type | identifier) catch null) // "http_error"),
code: (try ($error.code | identifier) catch null)}
end' >&2
return 1
fi
if ! session_id=$(printf '%s' "$body" | jq --exit-status --raw-output \
'select(type == "object") | .id | select(type == "string" and length > 0)' 2>/dev/null); then
unset session_id
printf '{"status":"%s","type":"invalid_response","code":"missing_session_id"}\n' "$http_status" >&2
return 1
fi
printf 'Session ID: %s\n' "$session_id"
}
create_browser_session
# Terminal 1: use session_id from the creation request.
# Keep this stream open. Wait for HTTP 200 before sending input.
set -o pipefail
curl --silent --show-error --dump-header - --suppress-connect-headers \
--no-buffer --fail-with-body \
"https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Accept: text/event-stream" \
| jq --null-input --raw-input --compact-output --unbuffered '
def identifier:
if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
def safe_error:
if type == "object" then {type: (.type | identifier), code: (.code | identifier)} else null end;
def public_event:
if .type == "agent.session.turn.output_text.done" then {type, text}
elif .type == "agent.session.turn.completed" or .type == "agent.session.turn.failed" or .type == "agent.session.turn.cancelled" then
{type, turn_id: .turn.id, subagent_id: .turn.subagent_id, error: (.turn.error | safe_error)}
elif .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" then
{type, error: ((.error // .session.error // .environment.error) | safe_error)}
elif .type == "agent.session.requires_action" then {type}
else empty end;
foreach inputs as $raw (
{body: "", headers: false, failed: false, output: []};
.output = []
| ($raw | rtrimstr([13] | implode)) as $line
| if ($line | startswith("HTTP/")) then
(try ($line | capture("^(?<protocol>HTTP/[0-9.]+) (?<status>[0-9]{3})(?: |$)")) catch null) as $http
| if $http == null then . else
.body = "" | .headers = true | .status = $http.status
| .failed = (($http.status | tonumber) >= 300)
| .output = [$http.protocol + " " + $http.status]
end
elif .headers then
if $line == "" then .headers = false else . end
elif ($line | startswith("data:")) then
(try ($line | ltrimstr("data:") | fromjson) catch null) as $event
| .output = [$event | select(type == "object") | public_event]
elif .failed and .body != null then
.body += ($line + "\n")
| (try (.body | fromjson) catch null) as $body
| if ($body | type) == "object" then
(($body.error // $body) | safe_error) as $error
| .output = [{status: .status, type: ($error.type // "http_error"), code: $error.code}]
| .body = null
else . end
else . end;
.output[])'
# Terminal 2: replace sess_123 with the ID printed in terminal 1.
# Export OPENAI_API_KEY in this terminal too.
session_id="sess_123"
curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"type": "agent.session.input.message",
"input": [{
"role": "user",
"content": [{
"type": "input_text",
"text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
}]
}]
}]
}'