- adapters/websearch: Brave client (X-Subscription-Token, error classes), result normalization with redaction/truncation, SSRF guard, HTML→text fetch (10s, 512KB, text/html|text/plain only) - modules/websearch: 10-min PG cache (region websearch), cross-process per-minute limit, web.fetch allowlist = URLs from the caller's latest web.search; POST /websearch/test - settings.agent.webSearch (key write-only → secret_ref); settings card under Agent; web.* offered to the model only when enabled, plus a system prompt rule; outputs marked untrusted - capability count 27 → 29; e2e E13; runbook docs/runbook/websearch.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
231 lines
8.7 KiB
TypeScript
231 lines
8.7 KiB
TypeScript
import { AgentToolsBlock } from "./agent-tools.tsx";
|
||
import { WEBSEARCH_CARD } from "./agent-websearch.tsx";
|
||
import { PiUpstreamsBlock } from "./pi-upstreams.tsx";
|
||
import type { FieldValue } from "./resource-section.tsx";
|
||
import { num, str } from "./resource-section.tsx";
|
||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||
|
||
// Agent 分区:上游(pi_upstream)+ 上下文与子代理(pi)+ 预算(budget.agent* 与单次上限)+ 自动调查 + 网页搜索 + 高级
|
||
|
||
/** 可清空的数字:空串 → null */
|
||
const nullNum = (v: FieldValue | undefined) => (str(v) === "" ? null : Number(str(v)));
|
||
|
||
const RUNTIME_CARD: CardSpec = {
|
||
anchor: "runtime",
|
||
title: "上下文与子会话",
|
||
fields: [
|
||
{
|
||
name: "pi.contextBudget",
|
||
label: "上下文预算(token)",
|
||
kind: "number",
|
||
hint: "4096–2000000,超过模型窗口时按窗口算",
|
||
},
|
||
{ name: "pi.subagentsEnabled", label: "允许子会话", kind: "switch" },
|
||
{ name: "pi.compaction.enabled", label: "上下文压缩", kind: "switch" },
|
||
{
|
||
name: "pi.compaction.thresholdRatio",
|
||
label: "压缩触发比例",
|
||
kind: "number",
|
||
hint: "0.3–0.95,用到预算的这个比例时压缩",
|
||
visible: (v) => v["pi.compaction.enabled"] === true,
|
||
},
|
||
],
|
||
fromSettings: (s) => ({
|
||
"pi.contextBudget": n(s.pi.contextBudget),
|
||
"pi.subagentsEnabled": s.pi.subagentsEnabled,
|
||
"pi.compaction.enabled": s.pi.compaction.enabled,
|
||
"pi.compaction.thresholdRatio": n(s.pi.compaction.thresholdRatio),
|
||
}),
|
||
toPatch: (v) => ({
|
||
pi: {
|
||
contextBudget: num(v["pi.contextBudget"]),
|
||
subagentsEnabled: v["pi.subagentsEnabled"] === true,
|
||
compaction: {
|
||
enabled: v["pi.compaction.enabled"] === true,
|
||
thresholdRatio: num(v["pi.compaction.thresholdRatio"]),
|
||
},
|
||
},
|
||
}),
|
||
};
|
||
|
||
const BUDGET_CARD: CardSpec = {
|
||
anchor: "budget",
|
||
title: "预算",
|
||
description: "超出每日预算的运行停在「超出预算」,等你放行。",
|
||
fields: [
|
||
{ name: "budget.agentMaxRunsPerDay", label: "每日运行次数", kind: "number" },
|
||
{ name: "budget.agentMaxCostUsdPerDay", label: "每日费用(USD)", kind: "number" },
|
||
{ name: "agent.maxToolCalls", label: "单次工具调用上限", kind: "number", hint: "1–500" },
|
||
{ name: "agent.maxDurationSec", label: "单次时长上限(秒)", kind: "number" },
|
||
{ name: "agent.maxTokensPerRun", label: "单次 token 上限", kind: "number", hint: "留空不限" },
|
||
{ name: "agent.maxCostUsdPerRun", label: "单次费用上限(USD)", kind: "number", hint: "留空不限" },
|
||
],
|
||
fromSettings: (s) => ({
|
||
"budget.agentMaxRunsPerDay": n(s.budget.agentMaxRunsPerDay),
|
||
"budget.agentMaxCostUsdPerDay": n(s.budget.agentMaxCostUsdPerDay),
|
||
"agent.maxToolCalls": n(s.agent.maxToolCalls),
|
||
"agent.maxDurationSec": n(s.agent.maxDurationSec),
|
||
"agent.maxTokensPerRun": n(s.agent.maxTokensPerRun),
|
||
"agent.maxCostUsdPerRun": n(s.agent.maxCostUsdPerRun),
|
||
}),
|
||
toPatch: (v) => ({
|
||
budget: {
|
||
agentMaxRunsPerDay: num(v["budget.agentMaxRunsPerDay"]),
|
||
agentMaxCostUsdPerDay: num(v["budget.agentMaxCostUsdPerDay"]),
|
||
},
|
||
agent: {
|
||
maxToolCalls: num(v["agent.maxToolCalls"]),
|
||
maxDurationSec: num(v["agent.maxDurationSec"]),
|
||
maxTokensPerRun: nullNum(v["agent.maxTokensPerRun"]),
|
||
maxCostUsdPerRun: nullNum(v["agent.maxCostUsdPerRun"]),
|
||
},
|
||
}),
|
||
};
|
||
|
||
const INVESTIGATE_CARD: CardSpec = {
|
||
anchor: "investigate",
|
||
title: "自动调查",
|
||
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
|
||
fields: [
|
||
{ name: "agent.autoInvestigate", label: "自动调查", kind: "switch", wide: true },
|
||
{
|
||
name: "agent.autoInvestigateMode",
|
||
label: "模式",
|
||
kind: "select",
|
||
options: [
|
||
{ value: "investigate", label: "只读调查,结论交还给你" },
|
||
{ value: "recover", label: "授权内恢复(重试任务、重新校验、确认识别)" },
|
||
],
|
||
hint: "恢复只在已发布策略或持续授权覆盖时执行,换源、删除、清理始终排除;没有授权时按只读调查",
|
||
wide: true,
|
||
visible: (v) => v["agent.autoInvestigate"] === true,
|
||
},
|
||
{
|
||
name: "agent.investigateMaxToolCalls",
|
||
label: "工具调用上限",
|
||
kind: "number",
|
||
hint: "1–100",
|
||
visible: (v) => v["agent.autoInvestigate"] === true,
|
||
},
|
||
{
|
||
name: "agent.investigateMaxAttempts",
|
||
label: "同一根因最多恢复次数",
|
||
kind: "number",
|
||
hint: "1–10,用完后只交还给你",
|
||
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
|
||
},
|
||
{
|
||
name: "agent.recoverVerifySec",
|
||
label: "恢复后核实时长(秒)",
|
||
kind: "number",
|
||
hint: "等下载或任务状态变化的最长时间",
|
||
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
|
||
},
|
||
],
|
||
fromSettings: (s) => ({
|
||
"agent.autoInvestigate": s.agent.autoInvestigate,
|
||
"agent.autoInvestigateMode": s.agent.autoInvestigateMode,
|
||
"agent.investigateMaxToolCalls": n(s.agent.investigateMaxToolCalls),
|
||
"agent.investigateMaxAttempts": n(s.agent.investigateMaxAttempts),
|
||
"agent.recoverVerifySec": n(s.agent.recoverVerifySec),
|
||
}),
|
||
toPatch: (v) => ({
|
||
agent: {
|
||
autoInvestigate: v["agent.autoInvestigate"] === true,
|
||
autoInvestigateMode: v["agent.autoInvestigateMode"] === "recover" ? "recover" : "investigate",
|
||
investigateMaxToolCalls: num(v["agent.investigateMaxToolCalls"]),
|
||
investigateMaxAttempts: num(v["agent.investigateMaxAttempts"]),
|
||
recoverVerifySec: num(v["agent.recoverVerifySec"]),
|
||
},
|
||
}),
|
||
};
|
||
|
||
const ADVANCED_CARD: CardSpec = {
|
||
anchor: "advanced",
|
||
title: "高级",
|
||
fields: [
|
||
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
|
||
{
|
||
name: "agent.goalWaitHours",
|
||
label: "目标最长等待(小时)",
|
||
kind: "number",
|
||
hint: "超时后按「仍在等待」汇报结束",
|
||
},
|
||
{
|
||
name: "agent.optionsTimeoutSec",
|
||
label: "方案选择等待(秒)",
|
||
kind: "number",
|
||
hint: "Agent 给出方案后等你选择的最长时间",
|
||
},
|
||
{
|
||
name: "agent.optionsTimeoutAction",
|
||
label: "超时未选",
|
||
kind: "select",
|
||
options: [
|
||
{ value: "recommended", label: "自动选推荐项" },
|
||
{ value: "stop", label: "不选,Agent 收尾" },
|
||
],
|
||
},
|
||
{
|
||
name: "agent.approvalTimeoutSec",
|
||
label: "会话内审批有效期(秒)",
|
||
kind: "number",
|
||
hint: "步骤处的审批卡过期未批准,按拒绝处理",
|
||
},
|
||
{
|
||
name: "agent.sessionDir",
|
||
label: "会话目录",
|
||
kind: "text",
|
||
wide: true,
|
||
placeholder: "留空用 DATA_DIR/agent-sessions",
|
||
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
|
||
},
|
||
{
|
||
name: "agent.fakeScript",
|
||
label: "fake 脚本(JSON)",
|
||
kind: "textarea",
|
||
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
|
||
},
|
||
],
|
||
fromSettings: (s) => ({
|
||
"agent.goalPollSec": n(s.agent.goalPollSec),
|
||
"agent.goalWaitHours": n(s.agent.goalWaitHours),
|
||
"agent.optionsTimeoutSec": n(s.agent.optionsTimeoutSec),
|
||
"agent.optionsTimeoutAction": s.agent.optionsTimeoutAction,
|
||
"agent.approvalTimeoutSec": n(s.agent.approvalTimeoutSec),
|
||
"agent.sessionDir": s.agent.sessionDir ?? "",
|
||
"agent.fakeScript": s.agent.fakeScript ?? "",
|
||
}),
|
||
toPatch: (v) => ({
|
||
agent: {
|
||
goalPollSec: num(v["agent.goalPollSec"]),
|
||
goalWaitHours: num(v["agent.goalWaitHours"]),
|
||
optionsTimeoutSec: num(v["agent.optionsTimeoutSec"]),
|
||
optionsTimeoutAction: v["agent.optionsTimeoutAction"] === "stop" ? "stop" : "recommended",
|
||
approvalTimeoutSec: num(v["agent.approvalTimeoutSec"]),
|
||
sessionDir: str(v["agent.sessionDir"]) || null,
|
||
fakeScript: str(v["agent.fakeScript"]) || null,
|
||
},
|
||
}),
|
||
};
|
||
|
||
export function AgentSection() {
|
||
return (
|
||
<div className="grid gap-8">
|
||
<PiUpstreamsBlock />
|
||
<SettingsGate>
|
||
{(s) => (
|
||
<>
|
||
<SettingsCard section="agent" spec={RUNTIME_CARD} settings={s} />
|
||
<SettingsCard section="agent" spec={BUDGET_CARD} settings={s} />
|
||
<SettingsCard section="agent" spec={INVESTIGATE_CARD} settings={s} />
|
||
<SettingsCard section="agent" spec={WEBSEARCH_CARD} settings={s} />
|
||
<SettingsCard section="agent" spec={ADVANCED_CARD} settings={s} />
|
||
</>
|
||
)}
|
||
</SettingsGate>
|
||
<AgentToolsBlock />
|
||
</div>
|
||
);
|
||
}
|