OpenAI 计算机使用
计算机使用允许智能体浏览网站、操作浏览器界面,以测试网站、收集信息或使用应用。
Agents API在OpenAI托管环境运行浏览器。应用启动会话、跟踪事件,智能体依据浏览器观察决定下一步。
浏览器任务流程:
- 创建浏览器会话,保存ID。
- 先订阅事件,再提交任务。
- 处理每个网站源访问请求;需要账户时处理登录。
- 等主智能体轮次结束并验证结果。断线后先恢复同一会话,再决定是否重试。
- 检查保存的浏览器活动,结束后删除会话。
配置浏览器
按快速开始前置条件创建密钥并导出 OPENAI_API_KEY,安装对应语言SDK。JavaScript示例安装 openai、prompt-sync;cURL示例需要Bash和jq。
- 在
agent.tools添加{ "type": "computer_use" }。 - 设
environment.type: "openai_hosted"与environment.desktop.enabled: true。
JavaScript示例组成一套连续操作:创建会话、处理网站批准、提交任务和打印答案。先创建启用截图的浏览器会话,此步骤尚未启动任务:
来源示例 1(bash):
程序文字说明18:在浏览器阅读公开文档;不登录、不修改网站数据,报告所找到网页的标题和URL。
程序文字说明19:此拼接指令限定浏览器只读公开文档、不登录及不改数据,并报告标题和URL。
## [中文] 示例 1:配置浏览器。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions;示例占位地址需替换为实际资源。
## 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"
来源示例 2(javascript):
沿用程序文字说明18。
沿用程序文字说明19。
// [中文] 示例 2:配置浏览器。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
import OpenAI from "openai";
import { open } from "node:fs/promises";
const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
agent: {
model: "gpt-6-astra",
instructions:
"Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
tools: [{ type: "computer_use", include_screenshots: true }],
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" },
},
});
console.log("Session ID:", session.id);
来源示例 3(python):
沿用程序文字说明18。
沿用程序文字说明19。
## [中文] 示例 3:配置浏览器。
## [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
import 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
来源示例 4(go):
沿用程序文字说明18。
沿用程序文字说明19。
// [中文] 示例 4:配置浏览器。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
import (
"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)
来源示例 5(java):
沿用程序文字说明19。
// [中文] 示例 5:配置浏览器。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
import 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());
来源示例 6(csharp):
沿用程序文字说明18。
沿用程序文字说明19。
// [中文] 示例 6:配置浏览器。
// [中文] C# 使用 AgentClient 创建启用 desktop 的托管浏览器会话,并保存 ID;这一创建示例尚未提交浏览任务。
using 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}");
来源示例 7(ruby):
沿用程序文字说明18。
沿用程序文字说明19。
## [中文] 示例 7:配置浏览器。
## [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
require "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。
源批准不强制每项具体动作前确认
若应用必须保证购买、破坏性变更或其他有实际后果动作先获确认,应将托管浏览器限制到无法执行这些动作的资源,或采用自己控制的浏览器运行时。通过函数工具请求确认仍依赖智能体确实调用该工具。
网站内容是不可信数据,不能授予权限或覆盖用户指令。自管运行时的设计见用户确认与同意。
先定义下列辅助函数,再运行任务。它处理源批准,并取消登录请求,因为本任务只读公开页面:
来源示例 8(bash):
## [中文] 示例 8:处理网站源访问。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions/$session_id、https://api.openai.com/v1/agents/sessions/$session_id/events;示例占位地址需替换为实际资源。
## [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
## 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" }
}]
}'
来源示例 9(javascript):
// [中文] 示例 9:处理网站源访问。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
// [中文] 使用 Node.js readline 的异步迭代器读取批准决定;输入流结束或留空时默认拒绝。
import { 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}`);
}
}
来源示例 10(python):
## [中文] 示例 10:处理网站源访问。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
def 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,
}
],
)
来源示例 11(go):
// [中文] 示例 11:处理网站源访问。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 按请求嵌套类型处理源访问批准或取消不需要的认证请求;提交原 request_id 对应的响应。
func 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))
}
来源示例 12(java):
// [中文] 从同一BufferedReader读取源访问批准,空输入默认拒绝;认证请求在本示例中取消,不能默认批准。
static 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());
}
来源示例 13(csharp):
// [中文] 示例 13:处理网站源访问。
// [中文] C# 从控制台取得源访问决定并响应当前请求;没有交互输入时不能默认批准。
static 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)]
)
);
}
来源示例 14(ruby):
## [中文] 示例 14:处理网站源访问。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
def 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 消失后,移除对应批准控件。取消批准请求不等于取消任务。
202 表示决定已接受,不表示导航已完成。
运行浏览器任务
让智能体查找官方开发者网站的Agents API快速开始,并报告标题与URL。
先打开事件流,再发送任务;到达的源访问请求需持续处理:
来源示例 15(bash):
程序文字说明20:在浏览器打开https://developers.openai.com,查找Agents API快速开始,报告网页标题和URL。
## [中文] 示例 15:运行浏览器任务。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions/$session_id/events、https://developers.openai.com;示例占位地址需替换为实际资源。
## 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."
}]
}]
}]
}'
来源示例 16(javascript):
沿用程序文字说明20。
// [中文] 示例 16:运行浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
// [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
let 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();
}
来源示例 17(python):
沿用程序文字说明20。
## [中文] 示例 17:运行浏览器任务。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
## [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
## [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
last_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.")
来源示例 18(go):
沿用程序文字说明20。
// [中文] 示例 18:运行浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
var 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")
}
来源示例 19(java):
// [中文] 示例 19:运行浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
var 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.");
}
}
来源示例 20(csharp):
沿用程序文字说明20。
// [中文] 示例 20:运行浏览器任务。
// [中文] 本段涉及的请求或服务地址:https://developers.openai.com;示例占位地址需替换为实际资源。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
byte[]? 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.");
}
来源示例 21(ruby):
沿用程序文字说明20。
## [中文] 示例 21:运行浏览器任务。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
## [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
last_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 |
可用时的截图输出 |
用标题与状态显示进度。活动项目描述单次工具操作,不是最终回答,也不是整轮状态。会话事件模型见事件与项目。
包含截图
在 computer_use 工具设置 include_screenshots: true,可在应用展示浏览器进度。默认API输出不含截图,但智能体仍可观察截图。
已完成浏览器活动会在可用时,将最后一次发出的截图放入 item.output:
来源示例 22(json):
{
"type": "computer_screenshot",
"image_url": "data:image/jpeg;base64,..."
}
通过 image_url 渲染。即使启用截图,有些操作仍返回 output: null,应用须兼容无图活动。
截图可能含敏感网页或账户数据,只向授权用户展示,不写入应用日志。
消费事件流时,从 agent.session.turn.item.done 捕获可用截图。历史读取可能省略图像字节,应在应用保存,不依赖之后的项目列表。轮次结束后,SDK示例列出浏览器活动并保存最近捕获截图,多数使用 browser-screenshot.jpg,Java使用临时文件:
来源示例 23(bash):
## [中文] 示例 23:包含截图。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions/$session_id/items;示例占位地址需替换为实际资源。
## [中文] 有后续页时将当前 last_id 作为下一页 after;会话列表和追踪列表游标不能混用。
## 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.
来源示例 24(javascript):
// [中文] 示例 24:包含截图。
// [中文] 读取已保存会话项目;它用于恢复历史,不是实时事件重放。
// [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
for await (const item of client.beta.agents.sessions.items.list(session.id, {
order: "asc",
limit: 100,
})) {
if (item.type !== "computer_use_call") continue;
console.log(item.title ?? "Browser activity", item.status);
}
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.");
}
来源示例 25(python):
## [中文] 示例 25:包含截图。
## [中文] 读取已保存会话项目;它用于恢复历史,不是实时事件重放。
## [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
for 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
来源示例 26(go):
// [中文] 示例 26:包含截图。
// [中文] 读取已保存会话项目;它用于恢复历史,不是实时事件重放。
// [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
items := 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
来源示例 27(java):
// [中文] 示例 27:包含截图。
// [中文] 读取已保存会话项目;它用于恢复历史,不是实时事件重放。
// [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
var 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.");
}
来源示例 28(csharp):
// [中文] 示例 28:包含截图。
// [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
await 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.");
}
来源示例 29(ruby):
## [中文] 示例 29:包含截图。
## [中文] 读取已保存会话项目;它用于恢复历史,不是实时事件重放。
## [中文] 保存可用截图时避免覆盖已有文件;历史读取未必包含图像字节,截图可含敏感信息。
items = 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问题等任务需要已登录浏览器。由应用处理登录,用户可在聊天之外选择方式并填写凭证。
登录可能有多个请求:先选邮箱登录,再填邮箱,再填验证码。界面须依据每次请求的登录选项和字段生成。
只有主智能体可以请求浏览器认证,子智能体不能。支持邮箱、密码、验证码,不支持通行密钥或扫码;强制使用不受支持方式的网站无法通过此流程登录。
保持任务事件流打开,并处理源批准。收到 agent.session.requires_action,读取当前动作,找出嵌套 request.type: "browser_authentication" 的 computer_use_approval_request。
按嵌套 request 构建界面:
| 字段 | 用途 |
|---|---|
reason |
解释为何需要输入,可为null |
credential_origin |
显示凭证目标,可为null |
fields |
根据各字段id、label、type、required生成输入框,可为空 |
options |
登录方式,每项有id、label及关联输入field_ids |
有登录方式则让用户选择,再显示关联字段;没有选项时直接显示请求字段。
用户只能向可验证目标填写凭证。目标缺失或陌生且无法独立验证时,取消认证请求。
用动作外层 request_id 提交用户输入,或在拒绝时取消。继续处理后续请求。提交回应不证明登录成功,应跟到任务完成并核对结果。
示例:先选方式,再填验证码
支持邮箱验证码和密码的网站,可能先只要求选择方式而不要求字段:
来源示例 30(json):
程序文字说明21:选择登录问题跟踪系统的方式。
{
"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": [] }
]
}
}
选择邮箱验证码时,使用当前 request_id 发送 selected_option: "email_code" 与 fields: []。之后网站可能分别请求邮箱和验证码。每次都按新请求字段和ID渲染,只在当前请求有选项时提供 selected_option。
返回用户输入
通过会话事件端点发送回应。action 为 submit,请求ID和字段ID必须来自当前批准请求。邮箱输入示例:
来源示例 31(json):
{
"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" }]
}
}
]
}
有登录选项时,response.selected_option 使用所选ID,只提交其 field_ids 内字段,每个必填值非空。该方式无字段时发 fields: []。
没有登录选项则省略 selected_option,直接提交请求字段。只要请求列出字段,即使全为可选,也至少提供一个。
登录值只通过此专用事件发送,既不进入模型输入,也不会出现在历史中的认证响应项目里。
包括邮箱在内,每个值都视为敏感数据:输入时遮蔽,不进入日志、分析或保存的界面状态,提交后清空表单。不要通过普通消息或函数结果传凭证。
提交事件不带 turn_id。字段与载荷限制见认证提交限制。
空体 202 只说明提交被接受,不说明登录成功;继续跟随事件。关闭凭证提交的HTTP或SDK自动重试,接受与否不明时先刷新会话。
允许用户取消
用户拒绝登录时,以原 request_id 发送 action: "cancel",省略 fields 和 selected_option:
来源示例 32(json):
{
"events": [
{
"type": "agent.session.input.computer_use_approval_request_result",
"request_id": "REQUEST_ID",
"response": { "type": "browser_authentication", "action": "cancel" }
}
]
}
这只取消认证请求。要停止任务,另行取消轮次。
运行需要认证的浏览器任务
应用在跟随事件时处理源批准与登录。下列辅助函数先展示目标,再隐藏输入值进行收集与提交;用户拒绝则取消:
来源示例 33(bash):
## [中文] 示例 33:运行需要认证的浏览器任务。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions/$session_id、https://api.openai.com/v1/agents/sessions/$session_id/events;示例占位地址需替换为实际资源。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
## 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
来源示例 34(javascript):
// [中文] 示例 34:运行需要认证的浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
/** @param {OpenAI} client */
async function respondToComputerUseApproval(client, sessionId, approval) {
const request = approval.request;
if (request.type === "browser_origin_access") {
console.log("Requested origin:", request.origin);
console.log(request.reason ?? "The browser needs access to this origin.");
let input;
do {
input =
prompt("Allow access? [approve/deny/cancel, default deny] ")
.trim()
.toLowerCase() || "deny";
} while (!["approve", "deny", "cancel"].includes(input));
const decision =
input === "approve" ? "approve" : input === "cancel" ? "cancel" : "deny";
await client.beta.agents.sessions.events.create(sessionId, {
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_origin_access", decision },
},
],
});
return;
}
if (request.type !== "browser_authentication") {
throw new Error(`Unsupported approval request: ${request.type}`);
}
async function cancelSignIn() {
await client.beta.agents.sessions.events.create(
sessionId,
{
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: { type: "browser_authentication", action: "cancel" },
},
],
},
{ maxRetries: 0 }
);
}
console.log(request.reason ?? "Sign in to continue");
console.log(
"Credential origin:",
request.credential_origin ?? "Not supplied"
);
const consent = prompt("Have you verified the sign-in destination? [y/N] ")
.trim()
.toLowerCase();
if (!["y", "yes"].includes(consent)) {
await cancelSignIn();
return;
}
let selectedOption;
let activeFields = request.fields;
if (request.options.length > 0) {
request.options.forEach((option, index) => {
console.log(`${index + 1}. ${option.label}`);
});
let choice;
do {
const input = prompt("Choose a sign-in method, or enter cancel: ")
.trim()
.toLowerCase();
if (input === "cancel") {
await cancelSignIn();
return;
}
choice = Number(input);
} while (
!Number.isInteger(choice) ||
choice < 1 ||
choice > request.options.length
);
selectedOption = request.options[choice - 1];
activeFields = request.fields.filter((field) =>
selectedOption.field_ids.includes(field.id)
);
}
const fields = [];
do {
for (const field of activeFields) {
while (true) {
const value = prompt.hide(
`${field.label}${field.required ? "" : " (optional)"} (leave blank for options): `
);
if (value.length > 0) {
fields.push({ field_id: field.id, value });
break;
}
let action;
do {
action = prompt(
field.required
? "Enter a value or cancel sign-in? [enter/cancel, default enter] "
: "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] "
)
.trim()
.toLowerCase();
} while (
![
"",
"enter",
"cancel",
...(field.required ? [] : ["skip"]),
].includes(action)
);
if (action === "cancel") {
await cancelSignIn();
return;
}
if (!field.required && (action === "" || action === "skip")) break;
}
}
if (!selectedOption && activeFields.length > 0 && fields.length === 0) {
console.log(
"This form requires at least one field. Enter a value or cancel sign-in."
);
}
} while (!selectedOption && activeFields.length > 0 && fields.length === 0);
await client.beta.agents.sessions.events.create(
sessionId,
{
events: [
{
type: "agent.session.input.computer_use_approval_request_result",
request_id: approval.request_id,
response: {
type: "browser_authentication",
action: "submit",
fields,
...(selectedOption ? { selected_option: selectedOption.id } : {}),
},
},
],
},
{ maxRetries: 0 }
);
}
来源示例 35(python):
程序文字说明22:智能体需要你登录。
## [中文] 示例 35:运行需要认证的浏览器任务。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
from 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.
来源示例 36(go):
沿用程序文字说明22。
// [中文] 示例 36:运行需要认证的浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] Go 辅助函数逐次处理源批准和登录表单,隐藏凭证输入,按当前请求的字段与选项提交,并关闭凭证自动重试。
import (
"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,
})
}
来源示例 37(java):
沿用程序文字说明22。
// [中文] 示例 37:运行需要认证的浏览器任务。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
import 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.
}
来源示例 38(csharp):
沿用程序文字说明22。
// [中文] 示例 38:运行需要认证的浏览器任务。
// [中文] C# 辅助函数显示凭证目标、读取隐藏字段输入并提交专用认证回应;仍需跟随事件确认登录及任务结果。
using 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.
}
来源示例 39(ruby):
沿用程序文字说明22。
## [中文] 示例 39:运行需要认证的浏览器任务。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 以当前 request_id 关联批准回应;源访问决定与登录字段使用不同 response 类型,不用普通消息提交凭证。
require "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 换为自己可访问的问题页。先开流再提交任务,需要输入时调用辅助函数:
来源示例 40(bash):
程序文字说明23:在浏览器读取指定GitHub问题列表,需要时请求登录;不创建、编辑、评论或关闭问题。
程序文字说明24:打开示例私有仓库问题页,需要时登录,报告最近更新的未关闭问题的标题和URL,不作修改。
程序文字说明25:此拼接指令要求读取GitHub问题列表、必要时请求登录,并禁止创建、编辑、评论和关闭问题。
## [中文] 示例 40:运行需要认证的浏览器任务。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions、https://github.com/acme/private-repo/issues;示例占位地址需替换为实际资源。
## 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.
来源示例 41(javascript):
沿用程序文字说明23。
沿用程序文字说明24。
沿用程序文字说明25。
// [中文] 示例 41:运行需要认证的浏览器任务。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
// [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
import OpenAI from "openai";
const client = new OpenAI();
const session = await client.beta.agents.sessions.create({
agent: {
model: "gpt-6-astra",
instructions:
"Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.",
tools: [{ type: "computer_use", include_screenshots: false }],
},
environment: {
type: "openai_hosted",
desktop: { enabled: true },
network: { access: "enabled" },
},
});
console.log("Session ID:", session.id);
let completed = false;
let readyToDelete = false;
try {
const events = await client.beta.agents.sessions.events.stream(session.id);
const handledRequests = new Set();
try {
await client.beta.agents.sessions.events.create(session.id, {
events: [
{
type: "agent.session.input.message",
input: [
{
role: "user",
content: [
{
type: "input_text",
// Replace this illustrative URL with your private repository.
text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.",
},
],
},
],
},
],
});
for await (const event of events) {
switch (event.type) {
case "agent.session.requires_action": {
const current = await client.beta.agents.sessions.retrieve(
session.id
);
for (const approval of current.required_actions) {
if (
approval.type === "computer_use_approval_request" &&
!handledRequests.has(approval.request_id)
) {
await respondToComputerUseApproval(client, session.id, approval);
handledRequests.add(approval.request_id);
}
}
break;
}
case "agent.session.turn.output_text.done":
console.log(event.text);
break;
case "error":
throw new Error(event.error.message);
case "agent.session.failed":
case "agent.session.environment.failed":
throw new Error(`Agent lifecycle failure: ${event.type}`);
case "agent.session.turn.failed":
if (event.turn.subagent_id === null) {
throw new Error(event.turn.error?.message ?? "Browser task failed");
}
break;
case "agent.session.turn.cancelled":
if (event.turn.subagent_id === null) {
throw new Error("Browser task was cancelled");
}
break;
case "agent.session.turn.completed":
if (event.turn.subagent_id === null) completed = true;
break;
}
if (completed) break;
}
if (!completed) {
throw new Error("Stream closed before the browser task finished");
}
console.log();
} finally {
events.controller.abort();
}
readyToDelete = completed;
} catch (error) {
console.error(
`Session ${session.id} was kept. Use the same ID to check its status before trying again.`
);
throw error;
} finally {
if (readyToDelete) await client.beta.agents.sessions.delete(session.id);
}
来源示例 42(python):
沿用程序文字说明23。
沿用程序文字说明24。
沿用程序文字说明25。
## [中文] 示例 42:运行需要认证的浏览器任务。
## [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
## [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
## [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
## [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
from 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()
来源示例 43(go):
沿用程序文字说明23。
沿用程序文字说明24。
沿用程序文字说明25。
// [中文] 示例 43:运行需要认证的浏览器任务。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
// [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
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 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")
来源示例 44(java):
沿用程序文字说明25。
// [中文] 示例 44:运行需要认证的浏览器任务。
// [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
// [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
// [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
// [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
var 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();
}
来源示例 45(csharp):
沿用程序文字说明23。
沿用程序文字说明24。
沿用程序文字说明25。
// [中文] 示例 45:运行需要认证的浏览器任务。
// [中文] 本段涉及的请求或服务地址:https://github.com/acme/private-repo/issues;示例占位地址需替换为实际资源。
// [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
string 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);
来源示例 46(ruby):
沿用程序文字说明23。
沿用程序文字说明24。
沿用程序文字说明25。
## [中文] 示例 46:运行需要认证的浏览器任务。
## [中文] 创建会话,保存返回会话 ID;设置 stream 时继续读取事件,创建请求本身不保证任务成功。
## [中文] 向会话事件端点提交输入;各事件的关联标识按载荷保留。
## [中文] 订阅会话事件流;在提交后续任务前建立订阅,避免遗漏早期事件。
## [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
## [中文] 读取当前会话状态和 required_actions;历史项目不代表动作仍待处理。
## [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 ID。
require "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,由应用提供名称、参数结构和界面。
收到 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。只恢复仍待处理表单,移除已不存在请求的控件。
事件流按断流恢复重连,不能自动重发任务或批准回应。
| 结果 | 处理方式 |
|---|---|
202 |
清空已提交值,继续跟随结果事件。 |
400 |
对照待处理请求检查响应类型、选项、字段ID和必填值;取消及源访问响应不包含字段。 |
404 |
检查会话ID、请求ID和响应类型,再读取会话;请求可能已不可用。 |
409 |
刷新会话,可能请求过期、轮次结束或另一回应已被接受。 |
| 确认前断连 | 接受状态未知,先重连读取会话再决定重试。 |
认证请求五分钟过期,用户输入期间所属轮次也可能结束,恢复表单前先刷新会话。
需重试认证提交时,使用相同 request_id、选项与字段值映射。相同重试不会再次填写浏览器表单;接受后更改值返回 409,新登录尝试需智能体发新请求。
源批准投递失败时重试相同决定。接受后改变决定返回 409;源请求在所属轮次期间待处理,没有认证请求的五分钟超时。
控制网络访问
托管环境 network 同时控制浏览器和代码的出站访问,需允许目标网站及资源、重定向所需域名。
源批准是独立用户决定,不覆盖网络策略,见网络设置。
继续与清理
依赖浏览器状态的后续任务复用同一会话。登录Cookie可能过期,环境回收会清空浏览器状态。
结束后先取回结果,再删除会话请求环境清理:
来源示例 47(bash):
## [中文] 示例 47:继续与清理。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions/$session_id;示例占位地址需替换为实际资源。
## 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"
来源示例 48(javascript):
// [中文] 示例 48:继续与清理。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
await client.beta.agents.sessions.delete(session.id);
来源示例 49(python):
## [中文] 示例 49:继续与清理。
## [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
if ready_to_delete:
client.beta.agents.sessions.delete(session.id)
来源示例 50(go):
// [中文] 示例 50:继续与清理。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
// [中文] 从当前 required_actions 判断待处理请求,并保留所需调用或批准 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)
}
}()
来源示例 51(java):
// [中文] 示例 51:继续与清理。
// [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
client.beta().agents().sessions().delete(session.id());
来源示例 52(csharp):
// [中文] 示例 52:继续与清理。
// [中文] 本段清理指定资源;会话与提供商沙箱是独立资源,按本节要求分别处理。
await client.DeleteAgentSessionAsync(session.Id);
来源示例 53(ruby):
## [中文] 示例 53:继续与清理。
## [中文] 删除指定会话;自托管提供商计算资源还需独立清理。
begin
client.beta.agents.sessions.delete(session.id) if completed
rescue => error
warn "Could not delete completed session #{session.id}: #{error.message}"
raise
end
寿命与删除行为见托管沙箱。
请求处理参考
认证提交限制
每次最多六个字段值,每字段只出现一次;单值最多16,384字符,序列化字段值与选项合计不超过120 KiB。
使用待处理请求中的请求及字段ID,必填值非空。结构见会话事件API参考。
展开的cURL请求与事件流处理
此版本区分传输失败、HTTP错误及无效创建响应,筛选流输出并报告错误类型和代码。与首例同样使用两个终端,第二个终端随到随处理批准:
来源示例 54(bash):
沿用程序文字说明18。
沿用程序文字说明19。
沿用程序文字说明20。
## [中文] 示例 54:认证提交限制。
## [中文] 本段涉及的请求或服务地址:https://api.openai.com/v1/agents/sessions、https://api.openai.com/v1/agents/sessions/$session_id/events;示例占位地址需替换为实际资源。
## [中文] 按主智能体轮次的终结事件判断结果,不把 idle 或子智能体完成当成整轮成功。
## [中文] 这个双终端 Bash 示例先检查创建响应和传输错误,再筛选流事件;批准在第二终端处理,不自动重发登录值。
create_browser_session() {
unset session_id
local result http_status body curl_status
if result=$(curl --silent --fail-with-body --write-out '\n%{http_code}' \
https://api.openai.com/v1/agents/sessions \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"model": "gpt-6-astra",
"instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.",
"tools": [{ "type": "computer_use", "include_screenshots": true }]
},
"environment": {
"type": "openai_hosted",
"desktop": { "enabled": true },
"network": { "access": "enabled" }
}
}'); then curl_status=0; else curl_status=$?; fi
http_status=$(printf '%s\n' "$result" | tail -n 1)
body=$(printf '%s\n' "$result" | sed '$d')
[[ "$http_status" =~ ^[0-9]{3}$ ]] || http_status=000
if [ "$curl_status" -ne 0 ] || [[ ! "$http_status" =~ ^2[0-9][0-9]$ ]]; then
printf '%s' "$body" | jq --raw-input --slurp --compact-output \
--arg status "$http_status" --arg curl_status "$curl_status" '
def identifier:
if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
(try fromjson catch {}) as $response
| (if ($response | type) == "object" then $response.error // $response else {} end) as $error
| if $curl_status != "0" and $curl_status != "22" then
{status: $status, type: "transport_error", code: ("curl_" + $curl_status)}
else
{status: $status, type: ((try ($error.type | identifier) catch null) // "http_error"),
code: (try ($error.code | identifier) catch null)}
end' >&2
return 1
fi
if ! session_id=$(printf '%s' "$body" | jq --exit-status --raw-output \
'select(type == "object") | .id | select(type == "string" and length > 0)' 2>/dev/null); then
unset session_id
printf '{"status":"%s","type":"invalid_response","code":"missing_session_id"}\n' "$http_status" >&2
return 1
fi
printf 'Session ID: %s\n' "$session_id"
}
create_browser_session
## Terminal 1: use session_id from the creation request.
## Keep this stream open. Wait for HTTP 200 before sending input.
set -o pipefail
curl --silent --show-error --dump-header - --suppress-connect-headers \
--no-buffer --fail-with-body \
"https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Accept: text/event-stream" \
| jq --null-input --raw-input --compact-output --unbuffered '
def identifier:
if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end;
def safe_error:
if type == "object" then {type: (.type | identifier), code: (.code | identifier)} else null end;
def public_event:
if .type == "agent.session.turn.output_text.done" then {type, text}
elif .type == "agent.session.turn.completed" or .type == "agent.session.turn.failed" or .type == "agent.session.turn.cancelled" then
{type, turn_id: .turn.id, subagent_id: .turn.subagent_id, error: (.turn.error | safe_error)}
elif .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" then
{type, error: ((.error // .session.error // .environment.error) | safe_error)}
elif .type == "agent.session.requires_action" then {type}
else empty end;
foreach inputs as $raw (
{body: "", headers: false, failed: false, output: []};
.output = []
| ($raw | rtrimstr([13] | implode)) as $line
| if ($line | startswith("HTTP/")) then
(try ($line | capture("^(?<protocol>HTTP/[0-9.]+) (?<status>[0-9]{3})(?: |$)")) catch null) as $http
| if $http == null then . else
.body = "" | .headers = true | .status = $http.status
| .failed = (($http.status | tonumber) >= 300)
| .output = [$http.protocol + " " + $http.status]
end
elif .headers then
if $line == "" then .headers = false else . end
elif ($line | startswith("data:")) then
(try ($line | ltrimstr("data:") | fromjson) catch null) as $event
| .output = [$event | select(type == "object") | public_event]
elif .failed and .body != null then
.body += ($line + "\n")
| (try (.body | fromjson) catch null) as $body
| if ($body | type) == "object" then
(($body.error // $body) | safe_error) as $error
| .output = [{status: .status, type: ($error.type // "http_error"), code: $error.code}]
| .body = null
else . end
else . end;
.output[])'
## Terminal 2: replace sess_123 with the ID printed in terminal 1.
## Export OPENAI_API_KEY in this terminal too.
session_id="sess_123"
curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \
-H "OpenAI-Beta: agents=v1" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"type": "agent.session.input.message",
"input": [{
"role": "user",
"content": [{
"type": "input_text",
"text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL."
}]
}]
}]
}'
TRAE-AI编程


更多建议: