La fonctionnalité Utilisation de l’ordinateur permet à un agent de naviguer sur des sites web et d’interagir avec les interfaces affichées dans le navigateur pour tester un site, recueillir des informations ou utiliser une application via son interface utilisateur.
L’API Agents exécute le navigateur dans un environnement hébergé par OpenAI. Votre application démarre la session et en suit les événements. L’agent choisit sa prochaine action en fonction de ce qu’il observe dans le navigateur.
Pour exécuter une tâche dans le navigateur :
- Créez une session de navigateur et enregistrez son identifiant.
- Suivez les événements de la session et envoyez une tâche à l’agent.
- Traitez chaque demande d’accès à un site. Si la tâche nécessite un compte, gérez la connexion au site.
- Attendez la fin du tour de l’agent principal et vérifiez son résultat. En cas de perte de connexion, reprenez la même session avant de réessayer.
- Consultez l’activité enregistrée du navigateur, puis supprimez la session lorsque vous avez terminé.
Suivez les étapes préalables du guide de démarrage rapide de l’API Agents pour créer une clé API et exporter OPENAI_API_KEY, puis installez le SDK OpenAI pour votre langage. Pour les exemples JavaScript, installez openai et prompt-sync. Les exemples cURL nécessitent Bash et jq.
Pour activer l’accès au navigateur :
- Ajoutez
{ "type": "computer_use" } à agent.tools.
- Définissez
environment.type sur openai_hosted et environment.desktop.enabled sur true.
Les exemples JavaScript ci-dessous se suivent : créez une session, traitez les demandes d’approbation d’accès aux sites, puis envoyez une tâche et affichez la réponse. Commencez par créer une session de navigateur en activant les captures d’écran. La création de la session ne lance aucune tâche.
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}"
Le navigateur doit obtenir l’approbation de l’utilisateur avant d’accéder à chaque nouvelle origine web, y compris pour les sites publics. Activer l’accès réseau ne suffit pas à approuver ces demandes.
Même si vous activez la révision automatique, le navigateur continue de demander l’approbation de l’utilisateur pour accéder aux origines et d’émettre des demandes de connexion. Continuez à traiter ces deux types de demandes dans votre application.
Pour traiter une demande d’approbation d’accès à une origine :
- À la réception de
agent.session.requires_action, récupérez la session et examinez le contenu actuel de required_actions.
- Repérez les entrées
computer_use_approval_request en attente dont le champ imbriqué request.type vaut browser_origin_access.
- Affichez l’origine demandée (
origin) et le motif (reason), s’il est fourni, puis recueillez la décision de l’utilisateur : approve, deny ou cancel. Envoyez cette décision au point de terminaison des événements de session avec le même request_id et un objet response imbriqué contenant type: "browser_origin_access" et decision, comme dans l’exemple ci-dessous.
L’approbation d’une origine n’impose pas de confirmation avant chaque action
Si votre application doit garantir une confirmation avant tout achat, toute modification destructrice ou toute autre action lourde de conséquences, limitez l’accès du navigateur hébergé aux ressources qui ne permettent pas ces actions, ou utilisez un environnement d’exécution du navigateur que vous contrôlez. Si vous utilisez un outil de type fonction pour demander confirmation, la demande dépend de l’appel de cette fonction par l’agent.
Considérez le contenu des sites web comme non fiable. Il ne peut ni accorder d’autorisation ni prévaloir sur les instructions de l’utilisateur. Consultez les consignes sur la confirmation et le consentement dans un environnement d’exécution que vous contrôlez.
Définissez la fonction auxiliaire suivante avant le code de la tâche. Elle traite les demandes d’approbation d’accès aux origines et annule les demandes de connexion, car cette tâche consulte uniquement des pages publiques.
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
Gardez le flux ouvert et traitez chaque demande d’approbation en attente. Utilisez request_id pour suivre les demandes et retirez les commandes d’approbation de l’interface dès qu’une demande ne figure plus dans required_actions pour la session. L’annulation d’une demande d’approbation n’annule pas la tâche.
Une réponse 202 indique que la décision a été acceptée. Elle ne garantit pas que le navigateur a fini de charger la page.
Demandez à l’agent de trouver le guide de démarrage rapide de l’API Agents sur le site public destiné aux développeurs et d’en indiquer le titre et l’URL.
Ouvrez le flux d’événements avant d’envoyer la tâche pour que votre application reçoive les premiers événements de progression. Traitez les demandes d’approbation d’accès aux origines à mesure qu’elles arrivent pour permettre au navigateur de poursuivre la tâche.
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
L’exemple affiche la réponse de l’agent. Vérifiez qu’elle contient le titre et l’URL du guide de démarrage rapide.
Fermer le flux d’événements n’arrête pas la tâche. Pour l’arrêter, annulez le tour. En cas de problème de connexion ou de doute sur le résultat, suivez la procédure de reprise. L’exemple cURL détaillé fournit des informations de diagnostic sur le transport et les erreurs.
Les opérations du navigateur apparaissent sous forme d’éléments computer_use_call dans la sortie de la session. Ces éléments ont la même structure dans le flux d’événements et dans l’historique enregistré de la session.
| Champ |
Signification |
id |
Identifiant de l’élément d’activité. |
turn_id |
Tour au cours duquel l’activité a eu lieu. |
title |
Description de l’activité du navigateur, ou null. |
status |
in_progress, completed, failed ou incomplete. |
output |
Capture d’écran, si elle est disponible. |
Utilisez le titre et le statut pour afficher la progression dans votre application. Un élément d’activité du navigateur décrit une opération de l’outil. Il ne contient ni la réponse finale de l’agent ni le statut du tour dans son ensemble. Consultez la section Événements et éléments pour en savoir plus sur le modèle d’événements de session.
Pour afficher la progression du navigateur dans votre application, définissez include_screenshots sur true pour l’outil computer_use. Par défaut, l’API n’inclut pas les captures d’écran dans sa sortie, mais l’agent peut quand même les voir.
Lorsqu’une activité du navigateur se termine, l’événement correspondant contient, dans item.output, la dernière capture d’écran émise si elle est disponible :
1234{
"type": "computer_screenshot",
"image_url": "data:image/jpeg;base64,..."
}
Utilisez image_url pour afficher la capture d’écran. Certaines opérations renvoient output: null, même lorsque les captures d’écran sont activées. Votre application doit donc pouvoir traiter les éléments d’activité sans image.
Les captures d’écran peuvent contenir des données sensibles provenant d’une page ou d’un compte. Affichez-les uniquement pour les utilisateurs autorisés et excluez-les des journaux de l’application.
Récupérez les captures d’écran disponibles dans les événements agent.session.turn.item.done à mesure que vous lisez le flux. L’historique récupéré peut ne pas contenir les octets de l’image. Conservez donc l’image dans votre application sans attendre une requête ultérieure pour lister les éléments. Une fois le tour terminé, les exemples utilisant un SDK listent les activités du navigateur et enregistrent la dernière capture d’écran récupérée. La plupart utilisent browser-screenshot.jpg. L’exemple Java utilise un fichier temporaire.
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
Les exemples utilisant un SDK n’écrasent pas les fichiers existants. Déplacez ou supprimez browser-screenshot.jpg avant de les exécuter à nouveau.
Certaines tâches, comme la consultation des issues d’un dépôt GitHub privé, nécessitent une session authentifiée dans le navigateur. Votre application gère la connexion pour que les utilisateurs puissent choisir une méthode de connexion et saisir leurs identifiants en dehors de la discussion.
La connexion peut nécessiter plusieurs demandes successives. Par exemple, un site peut demander à l’utilisateur de choisir la connexion par e-mail, de saisir une adresse e-mail, puis un code de vérification. Construisez votre interface à partir des méthodes de connexion et des champs de chaque demande.
Seul l’agent principal peut demander une authentification dans le navigateur. Les sous-agents ne le peuvent pas. Ce parcours prend en charge les adresses e-mail, les mots de passe et les codes de vérification, mais pas les clés d’accès ni la connexion par code QR. Si un site exige une méthode non prise en charge, l’utilisateur ne pourra pas s’y connecter via ce parcours.
Gardez le flux d’événements de la tâche ouvert et traitez les demandes d’approbation d’accès aux origines à mesure qu’elles arrivent. À la réception de agent.session.requires_action, récupérez la session et recherchez dans son champ required_actions les entrées computer_use_approval_request dont le champ imbriqué request.type vaut browser_authentication.
Utilisez l’objet request imbriqué pour afficher votre interface de connexion :
| Champ |
Utilisation |
reason |
Si un motif est fourni, indiquez à l’utilisateur pourquoi une saisie est nécessaire. Ce champ peut valoir null. |
credential_origin |
Affichez la destination à laquelle les identifiants sont destinés. Ce champ peut valoir null. |
fields |
Affichez les champs de saisie à partir des valeurs id, label, type et required de chaque champ. La liste peut être vide. |
options |
Affichez les méthodes de connexion disponibles. Chaque option possède un identifiant id, un libellé label et une liste field_ids qui identifie ses champs de saisie. |
Si la demande propose des méthodes de connexion, laissez l’utilisateur en choisir une et affichez les champs associés. Sinon, affichez directement les champs de la demande.
Demandez à l’utilisateur de saisir ses identifiants uniquement pour une destination qu’il peut vérifier. Si l’origine à laquelle les identifiants sont destinés est absente ou inconnue et que l’utilisateur ne peut pas la vérifier par un autre moyen, annulez la demande d’authentification.
Envoyez les données saisies par l’utilisateur avec le request_id de l’action qui contient la demande, ou annulez la demande d’authentification si l’utilisateur refuse. Continuez à traiter les demandes à mesure qu’elles arrivent. L’envoi d’une réponse ne garantit pas que la connexion a réussi. Suivez la tâche jusqu’à son terme et vérifiez son résultat.
Un site qui propose la connexion par code envoyé par e-mail ou par mot de passe peut d’abord demander à l’utilisateur de choisir une méthode, sans lui présenter de champs à remplir :
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 l’utilisateur choisit la connexion par code envoyé par e-mail, envoyez selected_option: "email_code" avec fields: [] en utilisant le request_id de cette demande. Le site peut ensuite demander une adresse e-mail et un code de vérification dans deux demandes distinctes. Affichez chaque nouvelle demande avec ses propres champs et identifiants, et incluez selected_option uniquement si cette demande propose des options.
Lorsque l’utilisateur a rempli le formulaire de connexion, envoyez sa réponse au point de terminaison des événements de session. Définissez action sur submit et utilisez les identifiants de la demande et des champs qui figurent dans la demande d’approbation en attente. Voici un exemple de réponse à une demande d’adresse e-mail :
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 demande propose des méthodes de connexion, incluez l’identifiant de la méthode choisie dans response.selected_option. Envoyez uniquement les champs répertoriés dans field_ids pour cette méthode, avec une valeur non vide pour chaque champ obligatoire. Si la méthode ne comporte aucun champ, envoyez fields: [].
Si aucune méthode de connexion n’est proposée, omettez selected_option et envoyez directement les champs de la demande. Lorsque la demande comporte des champs, incluez-en au moins un, même s’ils sont tous facultatifs.
Envoyez les données de connexion uniquement au moyen de cet événement dédié. Les valeurs envoyées ne sont pas transmises au modèle de l’agent et ne figurent pas dans les éléments de réponse aux demandes d’authentification dans l’historique de la session.
Traitez toutes les valeurs comme des données sensibles, y compris les adresses e-mail. Masquez les valeurs saisies, excluez-les des journaux, des données d’analyse et de l’état enregistré de l’interface, puis effacez le contenu du formulaire après l’envoi. N’envoyez pas d’identifiants dans des messages ordinaires ni dans les résultats d’outils de type fonction.
Omettez turn_id dans l’événement d’envoi. Consultez les limites d’envoi des données d’authentification pour connaître les contraintes applicables aux champs et à la charge utile.
Une réponse 202 au corps vide confirme que l’envoi a été accepté. Elle ne garantit pas que la connexion a réussi. Continuez à suivre les événements de session pour repérer de nouvelles demandes ou la reprise de la tâche. Désactivez les nouvelles tentatives automatiques du client HTTP ou du SDK pour l’envoi d’identifiants. En cas de doute sur l’acceptation d’un envoi, actualisez la session avant de continuer.
Si l’utilisateur refuse de se connecter, répondez à la demande en attente avec action: "cancel". Utilisez son request_id et omettez fields et selected_option :
123456789{
"events": [
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": { "type": "browser_authentication", "action": "cancel" }
}
]
}
Cette réponse annule la demande d’authentification. Pour arrêter la tâche elle-même, annulez le tour.
Votre application traite les demandes d’approbation d’accès aux origines et les demandes de connexion tout en suivant les événements de session. Définissez la fonction auxiliaire ci-dessous avant d’exécuter la tâche. Elle affiche la destination, recueille les données en masquant les valeurs saisies, puis envoie la réponse. Si l’utilisateur refuse, elle annule la demande de connexion.
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
L’exemple suivant demande à l’agent de consulter les issues d’un dépôt GitHub privé. Remplacez https://github.com/acme/private-repo/issues par l’URL d’une page d’issues à laquelle vous avez accès.
Ouvrez le flux d’événements avant d’envoyer la tâche et appelez la fonction auxiliaire chaque fois que la session attend une saisie.
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
La connexion peut nécessiter plusieurs demandes. Continuez donc à traiter les demandes d’approbation jusqu’à la fin de la tâche. Vérifiez que le résultat de l’agent correspond à la demande. Dans cet exemple, vérifiez le titre et l’URL de l’issue indiqués par l’agent.
Les demandes d’authentification et les réponses acceptées figurent dans l’historique de la session et dans les événements agent.session.turn.item.added. Les éléments de demande contiennent les métadonnées du formulaire de connexion. Les éléments de réponse indiquent l’action acceptée sans inclure les identifiants saisis.
Utilisez cet historique pour consulter les interactions passées. Pour savoir si un formulaire de connexion attend toujours une saisie, récupérez le contenu actuel de required_actions pour la session. La présence d’une réponse dans l’historique ne garantit pas que la connexion a réussi.
L’historique ne comporte pas d’éléments de demande ou de réponse propres aux approbations d’accès aux origines. Traitez ces approbations à partir de required_actions.
Pour demander une précision ou permettre à l’utilisateur de faire un choix pendant une tâche, définissez un outil de type fonction dans agent.tools. Par exemple, vous pouvez définir request_user_response pour afficher une question et recueillir la réponse. Votre application fournit le nom de l’outil, le schéma de ses arguments et son interface utilisateur.
À la réception de agent.session.requires_action, repérez l’élément function_call en attente qui correspond à votre outil et utilisez ses arguments pour afficher la question. Renvoyez la réponse via agent.session.input.tool_result, avec le turn_id et le call_id de l’action. Définissez success: true et placez la réponse dans output. Si la réponse est structurée, sérialisez-la sous forme de chaîne JSON. Si l’utilisateur refuse de répondre, renvoyez success: false avec un message dans error.
Continuez à suivre les événements de session après avoir renvoyé la réponse. Les fonctions auxiliaires présentées plus haut gèrent les demandes d’accès aux origines et de connexion. Étendez votre gestionnaire d’événements pour qu’il traite également les appels à l’outil qui pose la question.
Le modèle peut voir les résultats des outils de type fonction, et ces résultats sont enregistrés dans l’historique de la session. Recueillez les mots de passe et les codes de vérification via le parcours d’authentification du navigateur.
Si votre application perd la connexion ou si l’envoi d’une réponse à une demande d’approbation échoue, récupérez la même session et examinez le contenu actuel de required_actions avant de continuer. Rétablissez les formulaires uniquement pour les demandes toujours en attente et retirez de l’interface les commandes associées aux demandes qui n’y figurent plus.
Si le flux d’événements est interrompu, suivez la procédure de reprise du flux pour recevoir à nouveau les événements. La reconnexion ne doit pas déclencher le renvoi automatique de la tâche ou d’une réponse à une demande d’approbation.
Utilisez le statut de la réponse pour décider de la suite :
| Résultat |
Action à effectuer dans votre application |
202 |
Effacez les valeurs envoyées et suivez les événements de session pour connaître le résultat. |
400 |
Vérifiez que le type de réponse, l’option sélectionnée, les identifiants de champ et les valeurs obligatoires correspondent à la demande en attente. Omettez les champs de saisie dans les réponses d’annulation et les réponses aux demandes d’accès aux origines. |
404 |
Vérifiez l’identifiant de session, l’identifiant de demande et le type de réponse. Récupérez à nouveau la session, car la demande peut ne plus être disponible. |
409 |
Actualisez la session. Il est possible que la demande ait expiré, que le tour correspondant soit terminé ou qu’une autre réponse ait déjà été acceptée. |
| Connexion perdue avant l’accusé de réception |
Considérez que vous ignorez si la réponse a été acceptée. Rétablissez la connexion et récupérez la session avant de décider de réessayer. |
Les demandes d’authentification expirent au bout de cinq minutes, et le tour auquel elles sont rattachées peut se terminer pendant la saisie. Actualisez la session avant de rétablir un formulaire de connexion.
Si vous renvoyez une réponse à une demande d’authentification, utilisez le même request_id, la même option et les mêmes valeurs pour les mêmes champs. Un envoi à l’identique ne remplit pas une seconde fois le formulaire dans le navigateur. Si vous changez les valeurs après l’acceptation d’une réponse, l’API renvoie 409. Pour tenter à nouveau de se connecter, l’agent doit émettre une nouvelle demande.
Pour les approbations d’accès aux origines, renvoyez la même décision si l’envoi échoue. Si vous modifiez une décision déjà acceptée, l’API renvoie 409. Les demandes d’accès aux origines restent en attente pendant le tour auquel elles sont rattachées. Le délai de cinq minutes applicable à l’authentification ne s’applique pas à ces demandes.
Utilisez la configuration network de l’environnement hébergé pour contrôler l’accès réseau sortant du navigateur et du code exécuté dans cet environnement. Autorisez le site de destination ainsi que les domaines nécessaires au chargement des ressources de la page ou aux redirections.
L’approbation d’accès à une origine relève d’une décision distincte de l’utilisateur et ne prévaut pas sur la politique réseau. Consultez les paramètres d’accès réseau pour configurer l’environnement.
Réutilisez la même session pour les tâches suivantes si elles nécessitent de conserver l’état du navigateur. Les cookies de connexion peuvent expirer et le recyclage de l’environnement efface l’état du navigateur.
Lorsque vous avez terminé, récupérez les résultats dont vous avez besoin et supprimez la session pour demander le nettoyage de l’environnement :
# 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
Consultez la section Bacs à sable hébergés par OpenAI pour en savoir plus sur la durée de vie de l’environnement et les modalités de sa suppression.
Chaque envoi peut contenir jusqu’à six valeurs de champ. Chaque champ ne peut figurer qu’une seule fois. Chaque valeur peut contenir jusqu’à 16 384 caractères. Une fois sérialisées, les valeurs des champs et l’option sélectionnée ne doivent pas dépasser 120 Kio au total.
Utilisez les identifiants de la demande et des champs qui figurent dans la demande d’approbation en attente, et fournissez une valeur non vide pour chaque champ obligatoire. Consultez la référence de l’API des événements de session pour connaître le schéma de la requête.
Traitement détaillé des requêtes et du flux avec cURL
Utilisez cette version pour distinguer les problèmes de transport, les erreurs HTTP et les réponses non valides à la création d’une session. Elle filtre également les données reçues dans le flux et indique les types et les codes d’erreur. Comme le premier exemple, elle utilise deux terminaux. Traitez les demandes d’approbation dans le second terminal à mesure qu’elles arrivent.
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."
}]
}]
}]
}'