コンピューターの使用を有効にすると、エージェントはウェブサイトを巡回し、ブラウザ画面を操作できます。ウェブサイトのテストや情報収集のほか、UI を介したアプリケーションの操作にも利用できます。
Agents API は、OpenAI がホストする環境でブラウザを実行します。アプリケーションはセッションを開始してイベントを受信し、エージェントはブラウザで確認した内容をもとに次の操作を決めます。
ブラウザタスクは次の手順で実行します。
- ブラウザセッションを作成し、セッション ID を保存します。
- セッションイベントを受信し、エージェントにタスクを送信します。
- ウェブサイトへのアクセスリクエストを個別に処理します。タスクにアカウントが必要な場合は、サインインを処理します。
- メインエージェントのターンが終了するのを待ち、結果を確認します。接続が切れた場合は、再試行する前に同じセッションを復旧します。
- 保存されたブラウザの操作履歴を確認し、作業が終わったらセッションを削除します。
Agents API クイックスタートの前提条件に従って API キーを作成し、OPENAI_API_KEY をエクスポートしてから、使用する言語の OpenAI SDK をインストールします。JavaScript の例では openai と prompt-sync をインストールしてください。cURL の例には Bash と jq が必要です。
ブラウザへのアクセスを有効にするには、次のように設定します。
{ "type": "computer_use" } を agent.tools に追加します。
environment.type を openai_hosted に、environment.desktop.enabled を true に設定します。
以下の JavaScript の例は一連の手順になっています。セッションを作成し、ウェブサイトへのアクセス承認を処理できるようにしてから、タスクを送信して回答を出力します。まず、スクリーンショットを有効にしたブラウザセッションを作成します。この時点ではタスクは開始されません。
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
22
23
24
25
26
27using System.ClientModel;
using System.ClientModel.Primitives;
using System.Text.Json;
using OpenAI;
using OpenAI.Agents;
#pragma warning disable OPENAI001
string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };
AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();
AgentSession session = await client.CreateAgentSessionAsync(
new AgentSessionCreationOptions
{
Agent = new SessionAgentConfigParam
{
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 = [new AgentToolConfigParamComputerUse { IncludeScreenshots = true }],
},
Environment = new EnvironmentParamOpenaiHosted
{
Desktop = new DesktopParam(true),
Network = new NetworkPolicyParam(NetworkAccessParam.Enabled),
},
}
);
Console.WriteLine($"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}"
ブラウザがウェブサイトの新しいオリジンにアクセスするには、オリジンごとにユーザーの承認が必要です。公開ウェブサイトも対象です。ネットワークアクセスを有効にしても、これらのリクエストは承認されません。
承認リクエストの自動レビューを有効にしても、ブラウザが新しいオリジンにアクセスするにはユーザーの承認が必要です。サインインのリクエストも引き続き届きます。どちらのリクエストもアプリケーション側で処理してください。
オリジンへのアクセス承認は次の手順で処理します。
agent.session.requires_action を受信したら、セッションを取得し、現在の required_actions を確認します。
- 保留中の
computer_use_approval_request のうち、内部の request.type が browser_origin_access のものを探します。
- アクセス先の
origin と、指定されていれば reason を表示し、ユーザーに approve、deny、cancel のいずれかを選んでもらいます。以下の例のように、同じ request_id を使い、ネストされた response に type: "browser_origin_access" と decision を含めて、セッションイベントのエンドポイントに選択結果を送信します。
オリジンへのアクセスを承認しても、個々の操作前の確認は必須になりません
購入や破壊的な変更など、重大な影響を及ぼす操作の前に必ずユーザーの確認を得る必要がある場合は、ホスト型ブラウザのアクセス先を、それらの操作ができないリソースに限定してください。または、自分で制御できるブラウザ実行環境を使用してください。関数ツールで確認を求める方式では、エージェントがその関数を呼び出した場合にのみ確認が行われます。
ウェブサイトのコンテンツは信頼できないものとして扱ってください。コンテンツには権限を付与したり、ユーザーの指示を上書きしたりする効力はありません。自分で制御する実行環境での確認と同意に関するガイドを参照してください。
タスクのコードの前に、次のヘルパー関数を定義します。このタスクで読み取るのは公開ページだけなので、ヘルパー関数はオリジンへのアクセス承認を処理し、サインインリクエストはキャンセルします。
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
40
41
42
43
44
45
46static async Task RespondToOriginApprovalAsync(
AgentClient client, string sessionId,
SessionRequiredActionResourceComputerUseApprovalRequest approval)
{
if (Console.IsInputRedirected)
{
throw new InvalidOperationException("Run this example in an interactive terminal.");
}
ComputerUseApprovalResponseParam response;
if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin)
{
Console.WriteLine($"Origin: {origin.Origin}");
Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}");
string choice;
while (true)
{
Console.Write("Allow browser access? [approve/deny/cancel; default deny] ");
choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant();
if (choice.Length == 0) choice = "deny";
if (choice is "approve" or "deny" or "cancel") break;
Console.WriteLine("Enter approve, deny, or cancel.");
}
BrowserOriginAccessDecisionParam decision = choice switch
{
"approve" => BrowserOriginAccessDecisionParam.Approve,
"cancel" => BrowserOriginAccessDecisionParam.Cancel,
_ => BrowserOriginAccessDecisionParam.Deny,
};
response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision);
}
else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication)
{
Console.WriteLine("Sign-in is outside this public-page task; cancelling the request.");
response = new ComputerUseApprovalResponseParamBrowserAuthenticationCancel();
}
else
{
throw new InvalidOperationException("Unsupported computer-use approval request.");
}
await client.CreateAgentSessionEventsAsync(
sessionId,
new CreateSessionEventsParams(
[new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, 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
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
ストリームを開いたまま、保留中の承認リクエストをすべて処理します。request_id で各リクエストを追跡し、セッションの required_actions からリクエストがなくなったら、対応する承認用の UI を削除します。承認リクエストをキャンセルしても、タスクはキャンセルされません。
202 レスポンスは、ユーザーの選択が受理されたことを示します。ページへの移動が完了したことを示すものではありません。
開発者向けの公開サイトで Agents API のクイックスタートを探し、そのタイトルと 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# 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85byte[]? lastScreenshot = null;
HashSet<string> handledRequests = new(StringComparer.Ordinal);
await using var events = await client.GetAgentSessionEventsAsync(session.Id);
await client.CreateAgentSessionEventsAsync(
session.Id,
new CreateSessionEventsParams(
[
new SessionInputParamAgentSessionInputMessage(
[
new InputMessageParam(
[new InputContentParamInputText("Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.")]
),
]
),
]
)
);
bool completed = false;
await foreach (var message in events)
{
using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory());
JsonElement current = document.RootElement;
string? type = current.GetProperty("type").GetString();
if (type == "agent.session.requires_action")
{
AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id);
foreach (SessionRequiredActionResource action in latest.RequiredActions)
{
if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval
&& !handledRequests.Contains(approval.RequestId))
{
await RespondToOriginApprovalAsync(client, session.Id, approval);
handledRequests.Add(approval.RequestId);
}
}
}
else if (type == "agent.session.turn.item.done")
{
// Preserve streamed screenshots; retrieved history may omit image bytes.
JsonElement item = current.GetProperty("item");
if (item.GetProperty("type").GetString() == "computer_use_call"
&& item.TryGetProperty("output", out JsonElement output)
&& output.ValueKind == JsonValueKind.Object
&& output.GetProperty("type").GetString() == "computer_screenshot")
{
const string prefix = "data:image/jpeg;base64,";
string imageUrl = output.GetProperty("image_url").GetString()!;
if (imageUrl.StartsWith(prefix, StringComparison.Ordinal))
{
lastScreenshot = Convert.FromBase64String(imageUrl[prefix.Length..]);
}
}
}
else if (type == "agent.session.turn.output_text.done")
{
Console.WriteLine(current.GetProperty("text").GetString());
}
else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled")
{
JsonElement turn = current.GetProperty("turn");
if (turn.TryGetProperty("subagent_id", out JsonElement subagent)
&& subagent.ValueKind != JsonValueKind.Null)
{
continue;
}
if (type != "agent.session.turn.completed")
{
throw new InvalidOperationException($"Browser task ended: {type}");
}
completed = true;
break;
}
else if (type == "error")
{
throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString());
}
else if (type is "agent.session.failed" or "agent.session.environment.failed")
{
throw new InvalidOperationException($"Session failed: {type}");
}
}
if (!completed)
{
throw new InvalidOperationException("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
この例ではエージェントの回答が出力されます。クイックスタートのタイトルと URL が含まれていることを確認してください。
イベントストリームを閉じてもタスクは停止しません。タスクを停止するには、ターンをキャンセルします。接続に失敗した場合や処理結果が不明な場合は、復旧手順に従ってください。詳細な cURL の例には、通信障害やエラーを診断する処理も含まれています。
ブラウザの操作は、セッションの出力に computer_use_call アイテムとして表示されます。ストリームで受信する操作情報と、保存されたセッション履歴の操作情報は、同じアイテム形式です。
| フィールド |
意味 |
id |
操作アイテムの識別子です。 |
turn_id |
操作が行われたターンを示します。 |
title |
ブラウザ操作の説明です。null の場合もあります。 |
status |
in_progress、completed、failed、incomplete のいずれかです。 |
output |
スクリーンショットがある場合は、その出力が入ります。 |
タイトルとステータスを使って、アプリケーションに進捗を表示します。ブラウザの操作アイテムが表すのは、ツールによる個々の操作です。エージェントの最終回答や、ターン全体の完了状況は示しません。セッションのイベントモデルについては、イベントとアイテムを参照してください。
アプリケーションにブラウザの進捗を表示するには、include_screenshots を true に設定します。この設定は computer_use ツールに指定します。デフォルトでは API の出力にスクリーンショットは含まれませんが、エージェントはスクリーンショットを確認できます。
ブラウザ操作の完了イベントでは、その操作で最後に出力されたスクリーンショットがあれば、item.output に含まれます。
1234{
"type": "computer_screenshot",
"image_url": "data:image/jpeg;base64,..."
}
image_url を使ってスクリーンショットを表示します。スクリーンショットを有効にしていても output: null を返す操作があるため、アプリケーションでは画像のない操作アイテムも処理できるようにしてください。
スクリーンショットには、ページやアカウントに関する機密情報が含まれることがあります。閲覧権限のあるユーザーにのみ表示し、アプリケーションのログには記録しないでください。
ストリームを受信しながら、agent.session.turn.item.done イベントに含まれるスクリーンショットを取得します。後から取得した履歴には画像データが含まれないことがあるため、後でアイテム一覧を取得するリクエストに頼らず、アプリケーション側で画像を保持してください。SDK の例では、ターンの完了後にブラウザの操作一覧を表示し、取得した最新のスクリーンショットを保存します。ほとんどの例では browser-screenshot.jpg に保存し、Java の例では一時ファイルを使用します。
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
22
23
24
25
26
27
28
29
30
31
32
33
34await foreach (AgentSessionItem item in client.GetAgentSessionItemsAsync(
session.Id, limit: 100, order: AgentSessionItemCollectionOrder.Ascending))
{
if (item is not ComputerUseCallItemResource activity)
{
continue;
}
Console.WriteLine($"{activity.Title ?? "Browser activity"}: {activity.Status}");
}
if (lastScreenshot is not null)
{
FileStreamOptions fileOptions = new()
{
Mode = FileMode.CreateNew,
Access = FileAccess.Write,
Share = FileShare.None,
};
if (!OperatingSystem.IsWindows())
{
fileOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
}
await using FileStream file = new("browser-screenshot.jpg", fileOptions);
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(file.SafeFileHandle, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
await file.WriteAsync(lastScreenshot);
Console.WriteLine("Saved screenshot: browser-screenshot.jpg");
}
else
{
Console.WriteLine("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
SDK の例では既存のファイルを上書きしません。再実行する前に browser-screenshot.jpg を移動または削除してください。
GitHub のプライベートリポジトリのイシューを読み取る場合など、タスクによっては認証済みのブラウザが必要です。アプリケーション側でサインインを処理することで、ユーザーはチャットの外でログイン方法を選び、認証情報を入力できます。
サインインには複数のリクエストが必要な場合があります。たとえば、サイトがユーザーにメールでのサインインを選択するよう求め、次にメールアドレス、続いて確認コードの入力を求めることがあります。各リクエストに含まれるログイン方法とフィールドに合わせて UI を構成してください。
ブラウザ認証をリクエストできるのはメインエージェントだけです。サブエージェントはリクエストできません。このフローはメールアドレス、パスワード、確認コードに対応していますが、パスキーや QR コードによるサインインには対応していません。未対応の方法が必須のサイトでは、このフローでサインインを完了できません。
タスクのイベントストリームを開いたまま、オリジンへのアクセス承認のリクエストが届くたびに処理します。agent.session.requires_action を受信したら、セッションを取得します。現在の required_actions にある computer_use_approval_request のうち、内部の request.type が browser_authentication のものを探します。
ネストされた request を使って、サインイン用の UI を表示します。
| フィールド |
使用方法 |
reason |
理由が指定されていれば、入力が必要な理由をユーザーに説明します。null の場合もあります。 |
credential_origin |
認証情報の送信先を表示します。null の場合もあります。 |
fields |
各フィールドの id、label、type、required の値に従って入力欄を表示します。空の場合もあります。 |
options |
利用できるログイン方法を表示します。各選択肢には id、label、および対応する入力欄を特定する field_ids が含まれます。 |
リクエストにログイン方法の選択肢がある場合は、ユーザーに 1 つ選んでもらい、その方法に対応する入力欄を表示します。選択肢がない場合は、リクエストのフィールドをそのまま表示します。
認証情報の入力を求めるのは、ユーザー自身が送信先を確認できる場合だけにしてください。送信先のオリジンが指定されていない場合や見覚えがない場合に、ユーザーが別の手段でも送信先を確認できなければ、認証リクエストをキャンセルしてください。
ユーザーの入力を送信する際は、外側のアクションの request_id を使います。ユーザーがサインインを辞退した場合は、認証リクエストをキャンセルします。その後もリクエストが届くたびに処理してください。応答を送信しただけでは、サインインが成功したかどうかはわかりません。タスクが完了するまで進捗を追い、結果を確認してください。
メールで届くコードとパスワードのどちらでもサインインできるサイトでは、最初はフィールドへの入力を求めず、サインイン方法の選択だけを求めることがあります。
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": [] }
]
}
}
ユーザーがメールで届くコードを選んだ場合は、selected_option: "email_code" と fields: [] を、このリクエストの request_id を使って送信します。その後、サイトからメールアドレスと確認コードの入力を別々のリクエストで求められる場合があります。新しいリクエストが届くたびに、そのリクエストのフィールドと ID を使って入力欄を表示します。selected_option を含めるのは、そのリクエストに選択肢がある場合だけです。
ユーザーがサインインリクエストへの入力を終えたら、セッションイベントのエンドポイントに応答を送信します。action を submit に設定し、保留中の承認リクエストに含まれるリクエスト ID とフィールド ID を使います。たとえば、メールアドレスを求めるリクエストへの応答は次のようになります。
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" }]
}
}
]
}
リクエストにログイン方法の選択肢がある場合は、選択した方法の ID を response.selected_option に指定します。その方法の field_ids に含まれるフィールドだけを送信し、必須フィールドにはそれぞれ空でない値を指定してください。その方法にフィールドがない場合は、fields: [] を送信します。
ログイン方法の選択肢がない場合は、selected_option を省略し、リクエストのフィールドをそのまま送信します。リクエストにフィールドが含まれている場合は、すべて任意であっても、少なくとも 1 つは送信してください。
サインインの入力値は、この専用イベントでのみ送信してください。送信した値はエージェントのモデル入力には含まれず、セッション履歴の認証応答アイテムにも記録されません。
メールアドレスを含むすべての値を機密情報として扱ってください。入力値はマスクし、ログ、分析データ、保存される UI の状態に含めず、送信後はフォームをクリアしてください。通常のメッセージや関数ツールの結果で認証情報を送信しないでください。
送信イベントでは turn_id を省略します。フィールドとペイロードの制約については、認証情報の送信制限を参照してください。
本文が空の 202 レスポンスは、送信内容が受理されたことを示します。サインインの成功を示すものではありません。引き続きセッションイベントを受信し、追加のリクエストや作業の再開を確認してください。認証情報の送信では、HTTP または SDK による自動再試行を無効にしてください。送信内容が受理されたかどうか不明な場合は、続行する前にセッションを再取得してください。
ユーザーがサインインを辞退した場合は、保留中のリクエストに action: "cancel" で応答します。そのリクエストの request_id を使い、fields と selected_option は省略します。
123456789{
"events": [
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": { "type": "browser_authentication", "action": "cancel" }
}
]
}
これで認証リクエストがキャンセルされます。タスク自体を停止するには、ターンをキャンセルします。
アプリケーションでは、セッションイベントを受信しながら、オリジンへのアクセス承認とサインインリクエストを処理します。タスクを実行する前に、以下のヘルパー関数を定義してください。この関数は認証情報の送信先を表示し、入力値をマスクした状態でユーザーの入力を受け付け、応答を送信します。ユーザーが辞退した場合は、サインインリクエストをキャンセルします。
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
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
176using System.ClientModel;
using System.ClientModel.Primitives;
using System.Globalization;
using System.Text;
using System.Text.Json;
using OpenAI;
using OpenAI.Agents;
#pragma warning disable OPENAI001
static string ReadLine(string prompt)
{
Console.Write(prompt);
return Console.ReadLine() ?? "cancel";
}
static string ReadHidden(string prompt)
{
if (Console.IsInputRedirected)
{
throw new InvalidOperationException("Run this sign-in example in a terminal.");
}
Console.Write(prompt);
StringBuilder value = new();
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter)
{
Console.WriteLine();
return value.ToString();
}
if (key.Key == ConsoleKey.Backspace)
{
if (value.Length > 0)
{
value.Length--;
}
}
else if (!char.IsControl(key.KeyChar))
{
value.Append(key.KeyChar);
}
}
}
static async Task RespondToComputerUseApprovalAsync(
AgentClient client, string sessionId,
SessionRequiredActionResourceComputerUseApprovalRequest approval)
{
if (Console.IsInputRedirected)
{
throw new InvalidOperationException("Run this example in an interactive terminal.");
}
ComputerUseApprovalResponseParam response;
if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin)
{
Console.WriteLine($"Origin: {origin.Origin}");
Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}");
string choice;
while (true)
{
Console.Write("Allow browser access? [approve/deny/cancel; default deny] ");
choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant();
if (choice.Length == 0) choice = "deny";
if (choice is "approve" or "deny" or "cancel") break;
Console.WriteLine("Enter approve, deny, or cancel.");
}
BrowserOriginAccessDecisionParam decision = choice switch
{
"approve" => BrowserOriginAccessDecisionParam.Approve,
"cancel" => BrowserOriginAccessDecisionParam.Cancel,
_ => BrowserOriginAccessDecisionParam.Deny,
};
response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision);
}
else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication challenge)
{
async Task CancelAuthenticationAsync()
{
await client.CreateAgentSessionEventsAsync(
sessionId,
new CreateSessionEventsParams(
[new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(
approval.RequestId, new ComputerUseApprovalResponseParamBrowserAuthenticationCancel())]
)
);
}
Console.WriteLine(challenge.Reason ?? "The agent needs you to sign in.");
Console.WriteLine($"Credential origin: {challenge.CredentialOrigin ?? "Not supplied"}");
string consent = ReadLine("Have you verified the sign-in destination? [y/N] ").Trim();
if (!consent.Equals("y", StringComparison.OrdinalIgnoreCase)
&& !consent.Equals("yes", StringComparison.OrdinalIgnoreCase))
{
await CancelAuthenticationAsync();
return;
}
ComputerUseApprovalResponseParamBrowserAuthenticationSubmit submission = new([]);
var activeFields = challenge.Fields.ToList();
if (challenge.Options.Count > 0)
{
for (int index = 0; index < challenge.Options.Count; index++)
{
Console.WriteLine($"{index + 1}. {challenge.Options[index].Label}");
}
while (true)
{
string choice = ReadLine("Choose a sign-in method, or enter cancel: ").Trim();
if (choice.Equals("cancel", StringComparison.OrdinalIgnoreCase))
{
await CancelAuthenticationAsync();
return;
}
if (!int.TryParse(choice, NumberStyles.None, CultureInfo.InvariantCulture, out int index)
|| index < 1 || index > challenge.Options.Count)
{
Console.WriteLine("Enter a method number from the list, or cancel.");
continue;
}
var option = challenge.Options[index - 1];
submission.SelectedOption = option.Id;
var fieldsById = challenge.Fields.ToDictionary(field => field.Id, StringComparer.Ordinal);
activeFields = option.FieldIds.Select(id => fieldsById[id]).ToList();
break;
}
}
while (true)
{
foreach (var field in activeFields)
{
while (true)
{
string choices = field.Required
? "enter/cancel; default enter" : "enter/skip/cancel; default skip";
string choice = ReadLine($"{field.Label} [{choices}]: ").Trim().ToLowerInvariant();
if (choice == "cancel")
{
await CancelAuthenticationAsync();
return;
}
if (choice.Length == 0) choice = field.Required ? "enter" : "skip";
if (choice == "skip" && !field.Required) break;
if (choice != "enter")
{
Console.WriteLine("Choose one of the displayed options.");
continue;
}
string value = ReadHidden($"{field.Label}: ");
if (value.Length == 0)
{
if (!field.Required) break;
Console.WriteLine("This field is required.");
continue;
}
submission.Fields.Add(new BrowserAuthenticationFieldValueParam(field.Id, value));
break;
}
}
if (challenge.Options.Count > 0 || activeFields.Count == 0 || submission.Fields.Count > 0) break;
Console.WriteLine("Enter at least one field to submit this form, or cancel.");
}
response = submission;
}
else
{
throw new InvalidOperationException("Unsupported computer-use approval request.");
}
await client.CreateAgentSessionEventsAsync(
sessionId,
new CreateSessionEventsParams(
[new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, response)]
)
);
// 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
次の例では、GitHub のプライベートリポジトリからイシューを読み取るようエージェントに依頼します。https://github.com/acme/private-repo/issues は、自分がアクセスできるイシューのページの 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# 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
87
88
89
90
91
92
93
94
95
96
97
98string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
// An uncertain approval must not resend credentials automatically.
OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };
AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();
AgentSession session = await client.CreateAgentSessionAsync(
new AgentSessionCreationOptions
{
Agent = new SessionAgentConfigParam
{
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 = [new AgentToolConfigParamComputerUse { IncludeScreenshots = false }],
},
Environment = new EnvironmentParamOpenaiHosted
{
Desktop = new DesktopParam(true),
Network = new NetworkPolicyParam(NetworkAccessParam.Enabled),
},
}
);
Console.WriteLine($"Session ID: {session.Id}");
HashSet<string> handledRequests = new(StringComparer.Ordinal);
try
{
await using var events = await client.GetAgentSessionEventsAsync(session.Id);
await client.CreateAgentSessionEventsAsync(
session.Id,
new CreateSessionEventsParams(
[
new SessionInputParamAgentSessionInputMessage(
[
new InputMessageParam(
// Replace this illustrative URL with your private repository.
[new InputContentParamInputText("Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.")]
),
]
),
]
)
);
bool completed = false;
await foreach (var message in events)
{
using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory());
JsonElement current = document.RootElement;
string? type = current.GetProperty("type").GetString();
if (type == "agent.session.requires_action")
{
AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id);
foreach (SessionRequiredActionResource action in latest.RequiredActions)
{
if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval
&& !handledRequests.Contains(approval.RequestId))
{
await RespondToComputerUseApprovalAsync(client, session.Id, approval);
handledRequests.Add(approval.RequestId);
}
}
}
else if (type == "agent.session.turn.output_text.done")
{
Console.WriteLine(current.GetProperty("text").GetString());
}
else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled")
{
JsonElement turn = current.GetProperty("turn");
if (turn.TryGetProperty("subagent_id", out JsonElement subagent)
&& subagent.ValueKind != JsonValueKind.Null)
{
continue;
}
if (type != "agent.session.turn.completed")
{
throw new InvalidOperationException($"Browser task ended: {type}");
}
completed = true;
break;
}
else if (type == "error")
{
throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString());
}
else if (type is "agent.session.failed" or "agent.session.environment.failed")
{
throw new InvalidOperationException($"Session failed: {type}");
}
}
if (!completed)
{
throw new InvalidOperationException("Stream closed before the browser task finished.");
}
}
catch
{
Console.Error.WriteLine($"Session {session.Id} was kept. Retrieve it and check pending actions before sending anything again.");
throw;
}
await client.DeleteAgentSessionAsync(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
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
サインインには複数のリクエストが必要な場合があるため、タスクが終了するまで承認リクエストの処理を続けてください。エージェントが返した結果が依頼内容に合っているか確認します。この例では、報告されたイシューのタイトルと URL を確認してください。
認証リクエストと受理された応答は、セッション履歴と agent.session.turn.item.added イベントに表示されます。リクエストアイテムにはサインインフォームのメタデータが含まれます。応答アイテムには受理されたアクションが記録されますが、送信した認証情報の値は含まれません。
この履歴で過去のやり取りを確認できます。サインインフォームへの入力がまだ必要かどうかを判断するには、セッションの現在の required_actions を取得してください。応答が記録されていても、サインインが成功したとは限りません。
オリジンへのアクセス承認には、リクエストや応答を記録する専用の履歴アイテムがありません。required_actions を使って処理してください。
タスクの途中で不明点を確認したり、ユーザーに選択を求めたりするには、関数ツールを agent.tools に定義します。たとえば、request_user_response を定義して質問を表示し、回答を受け取ることができます。ツールの名前、引数のスキーマ、UI はアプリケーション側で用意します。
agent.session.requires_action を受信したら、そのツールに対する保留中の function_call を探し、arguments を使って質問を表示します。agent.session.input.tool_result で回答を返す際は、そのアクションの turn_id と call_id を使います。success: true を設定し、回答を output に格納します。構造化された回答は JSON 文字列にシリアライズしてください。ユーザーが回答を辞退した場合は、success: false と error メッセージを返します。
回答を返した後も、セッションイベントを引き続き受信してください。前述のブラウザ承認用のヘルパー関数は、オリジンへのアクセスとサインインを処理します。質問用のツールも処理できるよう、イベントハンドラーを拡張してください。
関数ツールの結果はモデルに渡され、セッション履歴にも保存されます。パスワードと確認コードの入力にはブラウザ認証を使用してください。
アプリケーションの接続が切れた場合や承認リクエストへの応答に失敗した場合は、処理を続ける前に同じセッションを取得し、現在の required_actions を確認します。フォームを再表示するのは、まだ保留中のリクエストだけです。一覧からなくなったリクエストの操作用 UI は削除してください。
イベントストリームが切断された場合は、ストリームの復旧手順に従ってイベントの受信を再開します。再接続時にタスクや承認リクエストへの応答を自動で再送しないでください。
レスポンスのステータスに応じて、次のように処理します。
| 結果 |
アプリケーション側の対応 |
202 |
送信済みの入力値をクリアし、セッションイベントを受信して結果を確認します。 |
400 |
応答の種類、選択したオプション、フィールド ID、必須フィールドの値を保留中のリクエストと照合します。キャンセル時やオリジンへのアクセスリクエストに応答する際は、フィールドを省略します。 |
404 |
セッション ID、リクエスト ID、応答の種類を確認します。リクエストがすでに利用できなくなっている可能性があるため、セッションを再取得します。 |
409 |
セッションを再取得します。リクエストの有効期限が切れた、そのリクエストが属するターンが終了した、または別の応答がすでに受理された可能性があります。 |
| 受理を確認する前に接続が切れた場合 |
受理されたかどうかは不明として扱います。再接続してセッションを取得してから、再試行するかどうかを判断します。 |
認証リクエストの有効期限は 5 分です。ユーザーの入力中に、そのリクエストが属するターンが終了することもあります。サインインフォームを再表示する前に、セッションを再取得してください。
認証情報の送信を再試行する場合は、同じ request_id、同じ選択肢、各フィールドに対応する同じ値を使います。まったく同じ内容を再送しても、ブラウザのフォームに再入力されることはありません。送信内容が受理された後に値を変更して送ると、409 が返されます。サインインをやり直すには、エージェントからの新しいリクエストが必要です。
オリジンへのアクセス承認で応答の送信に失敗した場合は、同じ選択結果を再送してください。受理済みの選択結果を変更すると 409 が返されます。オリジンへのアクセスリクエストは、そのリクエストが属するターンが続く間は保留されます。認証リクエストに適用される 5 分のタイムアウトはありません。
ホスト環境の network 設定を使って、ブラウザと環境内で実行するコードの両方について、外部へのアクセスを制御します。アクセス先のウェブサイトに加え、ページのリソースの読み込みやリダイレクトに必要なドメインも許可してください。
オリジンへのアクセス承認は、ネットワークポリシーとは別にユーザーが判断するもので、ポリシーを上書きすることはありません。環境の設定方法は、ネットワークアクセスの設定を参照してください。
後続のタスクでブラウザの状態を引き継ぐには、同じセッションを再利用します。ログイン Cookie の有効期限が切れることがあります。また、環境が再作成されるとブラウザの状態は消去されます。
作業が終わったら、必要な結果を取得し、セッションを削除して環境のクリーンアップをリクエストします。
# 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());
await client.DeleteAgentSessionAsync(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
環境の有効期間と削除時の動作については、OpenAI がホストするサンドボックスを参照してください。
1 回の送信に含められるフィールド値は最大 6 個で、各フィールドを指定できるのは 1 回だけです。各値の長さは最大 16,384 文字です。シリアライズしたフィールド値と選択したオプションの合計サイズは 120 KiB 以内でなければなりません。
保留中の承認リクエストに含まれるリクエスト ID とフィールド ID を使い、各必須フィールドに空でない値を指定してください。リクエストのスキーマは、セッションイベントの API リファレンスを参照してください。
cURL によるリクエストとストリーム処理の詳細例
通信障害、HTTP エラー、セッション作成時の不正なレスポンスを区別する必要がある場合は、こちらの例を使用してください。ストリームの出力をフィルタリングし、エラーの種類とコードも報告します。最初の例と同様に、ターミナルを 2 つ使用します。承認リクエストが届くたびに、2 つ目のターミナルで処理してください。
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."
}]
}]
}]
}'