@sema-agent/server 3.17.0 → 3.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +18 -2
- package/dist/plugins/web-search.d.ts +24 -2
- package/dist/plugins/web-search.js +63 -12
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
|
|
4
|
+
import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend } from "@sema-agent/core";
|
|
5
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
6
6
|
import { posIntEnv } from "./session-watch.js";
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
@@ -12,7 +12,7 @@ import { isPricingConfigured } from "./budget.js"; // 缝合审 M3:capabilities.
|
|
|
12
12
|
import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
|
|
13
13
|
import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
|
|
14
14
|
import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
|
|
15
|
-
import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver } from "./plugins/web-search.js";
|
|
15
|
+
import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver, shouldProbeWebSearchOnBoot } from "./plugins/web-search.js";
|
|
16
16
|
import { createAuthorizer } from "./security.js";
|
|
17
17
|
import { createDurableAskPolicy, hasOperatorGateIntent } from "./approval.js";
|
|
18
18
|
import { loadSkills } from "./capabilities/skills.js";
|
|
@@ -506,6 +506,22 @@ async function main() {
|
|
|
506
506
|
const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
|
|
507
507
|
if (webSearch)
|
|
508
508
|
logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
|
|
509
|
+
// #81④ 探活(boot 期一次性,**默认 OFF**):部署方此前只能靠「跑一个真任务」验证搜索配没配对 ——
|
|
510
|
+
// cli 的一键装 SearXNG 向导([2226] 裁 1)装完那一步正需要这个。消费 core 的 `probeSearchBackend`
|
|
511
|
+
// (不自铸第二真源,与 searxng adapter 同一条纪律)。
|
|
512
|
+
// 🔴 **失败只 warn,不拒启**:搜索是可选工具,配错不该挡住整个服务(功能型能力缺席 ⇒ 降级;
|
|
513
|
+
// 与本仓「保护型 ⇒ fail-closed」的方向判据是同一条判据的两面)。
|
|
514
|
+
// 🔴 **不 await**:探活是一次真出网,让它拖住 boot 就是把一个可选工具变成启动路径上的依赖。
|
|
515
|
+
if (webSearch && shouldProbeWebSearchOnBoot()) {
|
|
516
|
+
void probeSearchBackend(webSearch.search, { timeoutMs: 10_000 })
|
|
517
|
+
.then((r) => {
|
|
518
|
+
if (r.ok)
|
|
519
|
+
logger.info("web_search_probe_ok", { provider: webSearchCfg.provider, results: r.results });
|
|
520
|
+
else
|
|
521
|
+
logger.warn("web_search_probe_failed", { provider: webSearchCfg.provider, error: r.error, note: "WebSearch stays mounted — the tool will fail per-call until this is fixed; the service is not blocked by it." });
|
|
522
|
+
})
|
|
523
|
+
.catch((e) => logger.warn("web_search_probe_failed", { provider: webSearchCfg.provider, error: e instanceof Error ? e.message : String(e) }));
|
|
524
|
+
}
|
|
509
525
|
// core 1.382([1561] 提货单①②,design/153 parked 状态机)「同车必接」的 ensureChildSessionDurable 挂点:
|
|
510
526
|
// 只在 checkpointStore 真在场时构造(与 core parkEligible 门「两者全在场才成立」的判据同源——checkpoint
|
|
511
527
|
// 不在场时构造这个闭包毫无意义,连带把「local/无 durable backend 部署不该跑这条 acquire/release」的
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
* ⚠️ 判据留痕:我第一轮把这条判成了「约束被静默丢弃」的安全缺口,是**亲读 core 结果侧代码**
|
|
25
25
|
* 才推翻的。「别处已经处理了」这种声明必须验证 —— 这次它是真的,但真假只能靠读。
|
|
26
26
|
*
|
|
27
|
-
* ②
|
|
27
|
+
* ② ~~**searxng 腿是自铸的第二真源**~~ —— ✅ **已还(2026-08-01,[2229] 裁定 searxng 为国内可达性
|
|
28
|
+
* 主路线之后)**:本腿改为消费 core 的 `createSearxngSearchBackend`,外层保住 maxResults 截断 /
|
|
29
|
+
* 统一形状 / 坏 payload 观测器三样(各有红先钉 + 变异实测),白得 `site:` 原生下推、`extraParams`、
|
|
30
|
+
* 自带 timeout。以下为原始立案文本,留档:
|
|
31
|
+
* core 2.10 起已导出 `createSearxngSearchBackend`
|
|
28
32
|
* (+`SearxngBackendOptions`),而且比本文件的实现多三样:`allowedDomains` 拼 `site:` 原生下推、
|
|
29
33
|
* `extraParams`(实例特定的 `engines=`/`language=` 等)、**自带 timeout**
|
|
30
34
|
* (`AbortSignal.any([signal, timeout])`;本文件的 searxng 腿只吃外部 signal)。
|
|
@@ -37,7 +41,11 @@
|
|
|
37
41
|
* 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。
|
|
38
42
|
* ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。
|
|
39
43
|
*
|
|
40
|
-
* ④
|
|
44
|
+
* ④ ~~**探活 verb 缺席**~~ —— ✅ **已还(2026-08-01)**:`shouldProbeWebSearchOnBoot` 门控 +
|
|
45
|
+
* `main.ts` 消费 core 的 `probeSearchBackend`。**默认 OFF**(开着=给每个既有部署的每次启动凭空
|
|
46
|
+
* 加一次出网)、**只认明确真值**(含糊值当没开,别替部署方猜)、**失败只 warn 不拒启**(功能型
|
|
47
|
+
* 能力缺席 ⇒ 降级)、**不 await**(拖住 boot 就把可选工具变成启动依赖)。以下为原始立案文本,留档:
|
|
48
|
+
* core 2.10 起导出 `probeSearchBackend(search, {timeoutMs}) →
|
|
41
49
|
* {ok:true,results:n} | {ok:false,error}`,正好填这条:部署方现在只能靠「跑一个真任务」
|
|
42
50
|
* 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
|
|
43
51
|
* 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
|
|
@@ -48,6 +56,9 @@ export interface WebSearchBackendConfig {
|
|
|
48
56
|
readonly provider: WebSearchProvider;
|
|
49
57
|
/** API key (brave/tavily). From `WEB_SEARCH_API_KEY` — never reaches the model. */
|
|
50
58
|
readonly apiKey?: string;
|
|
59
|
+
/** SearXNG 实例特定查询参数(`engines=` / `language=` 等)——透传给 core adapter 的 `extraParams`。
|
|
60
|
+
* 一键装 SearXNG 的向导用它把「选了哪些上游引擎」落到引擎侧。仅 searxng 腿消费。 */
|
|
61
|
+
readonly searxngParams?: Record<string, string>;
|
|
51
62
|
/** SearXNG instance base URL (REQUIRED for searxng); for brave/tavily an optional base-URL override (proxy/test). */
|
|
52
63
|
readonly endpoint?: string;
|
|
53
64
|
/** Max results returned to the model (clamped 1..20; default 10). */
|
|
@@ -79,6 +90,17 @@ export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) =
|
|
|
79
90
|
* stays captured in this closure — it is never surfaced to the model or the tool args.
|
|
80
91
|
*/
|
|
81
92
|
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend;
|
|
93
|
+
/**
|
|
94
|
+
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
95
|
+
*
|
|
96
|
+
* 为什么默认 OFF:探活是一次**真出网**。开着就等于给每个既有部署的每次启动凭空加一次外呼 ——
|
|
97
|
+
* 多租/离线部署不能被这样动。这与 #81③「任何新的开启通道默认 OFF」同源:
|
|
98
|
+
* 一个只配了模型 key 的部署,不该因为升级而多出网络行为。
|
|
99
|
+
*
|
|
100
|
+
* 为什么只认明确真值:`"yes"`/`"on"` 这类**含糊值当没开**。含糊值上放行 = 替部署方猜意图,
|
|
101
|
+
* 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。
|
|
102
|
+
*/
|
|
103
|
+
export declare function shouldProbeWebSearchOnBoot(env?: NodeJS.ProcessEnv): boolean;
|
|
82
104
|
/** Parse the deployment env into a backend config; `undefined` when no provider is set (⇒ WebSearch not assembled). */
|
|
83
105
|
export declare function webSearchConfigFromEnv(env?: NodeJS.ProcessEnv): WebSearchBackendConfig | undefined;
|
|
84
106
|
/**
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
* ⚠️ 判据留痕:我第一轮把这条判成了「约束被静默丢弃」的安全缺口,是**亲读 core 结果侧代码**
|
|
25
25
|
* 才推翻的。「别处已经处理了」这种声明必须验证 —— 这次它是真的,但真假只能靠读。
|
|
26
26
|
*
|
|
27
|
-
* ②
|
|
27
|
+
* ② ~~**searxng 腿是自铸的第二真源**~~ —— ✅ **已还(2026-08-01,[2229] 裁定 searxng 为国内可达性
|
|
28
|
+
* 主路线之后)**:本腿改为消费 core 的 `createSearxngSearchBackend`,外层保住 maxResults 截断 /
|
|
29
|
+
* 统一形状 / 坏 payload 观测器三样(各有红先钉 + 变异实测),白得 `site:` 原生下推、`extraParams`、
|
|
30
|
+
* 自带 timeout。以下为原始立案文本,留档:
|
|
31
|
+
* core 2.10 起已导出 `createSearxngSearchBackend`
|
|
28
32
|
* (+`SearxngBackendOptions`),而且比本文件的实现多三样:`allowedDomains` 拼 `site:` 原生下推、
|
|
29
33
|
* `extraParams`(实例特定的 `engines=`/`language=` 等)、**自带 timeout**
|
|
30
34
|
* (`AbortSignal.any([signal, timeout])`;本文件的 searxng 腿只吃外部 signal)。
|
|
@@ -37,11 +41,16 @@
|
|
|
37
41
|
* 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。
|
|
38
42
|
* ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。
|
|
39
43
|
*
|
|
40
|
-
* ④
|
|
44
|
+
* ④ ~~**探活 verb 缺席**~~ —— ✅ **已还(2026-08-01)**:`shouldProbeWebSearchOnBoot` 门控 +
|
|
45
|
+
* `main.ts` 消费 core 的 `probeSearchBackend`。**默认 OFF**(开着=给每个既有部署的每次启动凭空
|
|
46
|
+
* 加一次出网)、**只认明确真值**(含糊值当没开,别替部署方猜)、**失败只 warn 不拒启**(功能型
|
|
47
|
+
* 能力缺席 ⇒ 降级)、**不 await**(拖住 boot 就把可选工具变成启动依赖)。以下为原始立案文本,留档:
|
|
48
|
+
* core 2.10 起导出 `probeSearchBackend(search, {timeoutMs}) →
|
|
41
49
|
* {ok:true,results:n} | {ok:false,error}`,正好填这条:部署方现在只能靠「跑一个真任务」
|
|
42
50
|
* 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
|
|
43
51
|
* 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
|
|
44
52
|
*/
|
|
53
|
+
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
45
54
|
const DEFAULT_MAX_RESULTS = 10;
|
|
46
55
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
47
56
|
const BRAVE_BASE = "https://api.search.brave.com/res/v1/web/search";
|
|
@@ -130,17 +139,45 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
130
139
|
const data = (await res.json());
|
|
131
140
|
return normalize(data.results, (r) => ({ title: r.title, url: r.url, snippet: r.content }), max, "tavily");
|
|
132
141
|
}
|
|
133
|
-
|
|
142
|
+
/**
|
|
143
|
+
* SearXNG 腿 —— **消费 core 的 `createSearxngSearchBackend`,不再自铸**(#81② 还债;[2190] 表态、
|
|
144
|
+
* [2229] 裁定 searxng 是国内可达性主路线之后该还的那笔)。
|
|
145
|
+
*
|
|
146
|
+
* core 的 adapter 比本文件此前的手写实现多三样,换过来就白得:
|
|
147
|
+
* · `allowedDomains` **原生下推**成 `site:a OR site:b` 前缀(此前本腿完全忽略 opts —— core 工具层
|
|
148
|
+
* 有结果侧 FLOOR 兜底所以不是正确性缺口,但白搜一趟、白花配额,极端情况下限域搜索回 0 条);
|
|
149
|
+
* · `extraParams`(实例特定的 `engines=` / `language=` 等)——一键装 SearXNG 的向导要用;
|
|
150
|
+
* · **自带 timeout**(`AbortSignal.any([signal, timeout])`;此前本腿只吃外部 signal)。
|
|
151
|
+
*
|
|
152
|
+
* ⚠️ core adapter **不管**这三样,必须留在外层(换真源最容易丢的就是它们,已上红先钉):
|
|
153
|
+
* ① `maxResults` 截断 ② 统一形状 `{title,url,snippet}` ③ 坏 payload 观测器。
|
|
154
|
+
*/
|
|
155
|
+
async function searxngSearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
134
156
|
if (!cfg.endpoint)
|
|
135
157
|
throw new Error("WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
const search = createSearxngSearchBackend(cfg.endpoint, {
|
|
159
|
+
fetchImpl,
|
|
160
|
+
...(cfg.searxngParams ? { extraParams: cfg.searxngParams } : {}),
|
|
161
|
+
});
|
|
162
|
+
let rows;
|
|
163
|
+
try {
|
|
164
|
+
rows = await search(query, signal, opts);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
// core 对「results 不是数组」抛具名错;本腿的既有契约是**不抛给上层、走观测器**(review LOW:
|
|
168
|
+
// 畸形 payload 不该变成裸 TypeError 上抛)。保持既有契约,把它翻译回观测器 + 空结果。
|
|
169
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
170
|
+
if (/no results array/i.test(msg)) {
|
|
171
|
+
try {
|
|
172
|
+
onBadPayload?.("searxng");
|
|
173
|
+
}
|
|
174
|
+
catch { /* observability never breaks search */ }
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
// core 已回 `{title,url,snippet}` 形;仍过一遍 normalize —— ① 截断 ② 形状兜底 ③ 观测器同源。
|
|
180
|
+
return normalize(rows, (r) => ({ title: r.title, url: r.url, snippet: r.snippet }), max, "searxng");
|
|
144
181
|
}
|
|
145
182
|
/**
|
|
146
183
|
* Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
|
|
@@ -159,7 +196,7 @@ export function createWebSearchBackend(cfg) {
|
|
|
159
196
|
switch (cfg.provider) {
|
|
160
197
|
case "brave": return await braveSearch(fetchImpl, cfg, q, maxResults, sig);
|
|
161
198
|
case "tavily": return await tavilySearch(fetchImpl, cfg, q, maxResults, sig, opts);
|
|
162
|
-
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig);
|
|
199
|
+
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig, opts);
|
|
163
200
|
}
|
|
164
201
|
}
|
|
165
202
|
finally {
|
|
@@ -168,6 +205,20 @@ export function createWebSearchBackend(cfg) {
|
|
|
168
205
|
};
|
|
169
206
|
return { search, maxResults };
|
|
170
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* #81④ 探活门控 —— **默认 OFF**,显式开才探。
|
|
210
|
+
*
|
|
211
|
+
* 为什么默认 OFF:探活是一次**真出网**。开着就等于给每个既有部署的每次启动凭空加一次外呼 ——
|
|
212
|
+
* 多租/离线部署不能被这样动。这与 #81③「任何新的开启通道默认 OFF」同源:
|
|
213
|
+
* 一个只配了模型 key 的部署,不该因为升级而多出网络行为。
|
|
214
|
+
*
|
|
215
|
+
* 为什么只认明确真值:`"yes"`/`"on"` 这类**含糊值当没开**。含糊值上放行 = 替部署方猜意图,
|
|
216
|
+
* 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。
|
|
217
|
+
*/
|
|
218
|
+
export function shouldProbeWebSearchOnBoot(env = process.env) {
|
|
219
|
+
const raw = env.WEB_SEARCH_PROBE_ON_BOOT?.trim().toLowerCase();
|
|
220
|
+
return raw === "true" || raw === "1";
|
|
221
|
+
}
|
|
171
222
|
/** Parse the deployment env into a backend config; `undefined` when no provider is set (⇒ WebSearch not assembled). */
|
|
172
223
|
export function webSearchConfigFromEnv(env = process.env) {
|
|
173
224
|
const provider = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|