@sema-agent/server 7.6.0 → 7.7.1
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/USAGE.md +85 -23
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.js +2 -1
- package/dist/boot/resolve-spec.d.ts +5 -0
- package/dist/boot/resolve-spec.js +15 -6
- package/dist/capabilities/hands-lane.d.ts +100 -0
- package/dist/capabilities/hands-lane.js +113 -0
- package/dist/capabilities/scenarios.d.ts +49 -2
- package/dist/capabilities/scenarios.js +84 -16
- package/dist/fleet/fleet-terminal-window.js +9 -6
- package/dist/hooks/hook-runner.js +11 -0
- package/dist/http/route-ctx.d.ts +7 -1
- package/dist/http/routes/fleet.js +6 -2
- package/dist/http/routes/runs.js +2 -1
- package/dist/http/routes/tasks.js +5 -4
- package/dist/http/server.d.ts +6 -0
- package/dist/http/server.js +28 -12
- package/dist/main.js +58 -7
- package/dist/observability/tool-trace.d.ts +13 -0
- package/dist/observability/tool-trace.js +14 -0
- package/dist/run-local.js +15 -5
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@ import { stablePrompt } from "./prompt.js";
|
|
|
7
7
|
import { createTeamTool, teamCoordinatorPrompt, getTeam, teamNames } from "./team.js";
|
|
8
8
|
import { createWebSearchBackend, webSearchConfigFromSettings } from "../plugins/web-search.js";
|
|
9
9
|
import { skillsForScenario } from "./skills.js";
|
|
10
|
+
import { pickHandsRunner } from "./hands-lane.js";
|
|
10
11
|
/**
|
|
11
12
|
* Sema product identity, prepended to the core default base for the `default` scenario when
|
|
12
13
|
* {@link ScenarioDeps.brandIdentity} is on. Honest about the substrate (the engine runs whichever model
|
|
@@ -88,6 +89,11 @@ export function codePromptProvider(brand) {
|
|
|
88
89
|
// [2400] CAPS-OPS-11:autonomousPromptProvider 别名导出已随场景退役删除(真身=codePromptProvider)。
|
|
89
90
|
// 其 `@deprecated` JSDoc 曾残留于此,被 tsc 误挂到下方 buildScenarios(三处调用点全告 deprecated
|
|
90
91
|
// ——TODO-3 立案的「迁移」实为孤儿注释,随删除清账;buildScenarios 本身非退役面)。
|
|
92
|
+
/** #196 唯一取用口:场景按自己的判别位取子任务 Runner。场景体内一律不直接摸 `deps.subRunner` /
|
|
93
|
+
* `deps.handslessSubRunner` —— 两处各挑一次就是漂移的成因,判别位必须是唯一开关。 */
|
|
94
|
+
function subRunnerFor(deps, hands) {
|
|
95
|
+
return pickHandsRunner(hands, { full: deps.subRunner, handsless: deps.handslessSubRunner });
|
|
96
|
+
}
|
|
91
97
|
export function buildScenarios(deps) {
|
|
92
98
|
// The default scenario filters skills by "default" — NOT by req.scenario (which may be an unknown name
|
|
93
99
|
// that fell back here): the bundle must be a coherent "default" set, matching how main.ts resolves an
|
|
@@ -137,7 +143,9 @@ export function buildScenarios(deps) {
|
|
|
137
143
|
// core 1.382 checkpointStore/ensureChildSessionDurable = design/153 parked 状态机的「同车必接」
|
|
138
144
|
// 一对([1561] 提货单①②,ScenarioDeps 字段注释详述);两者任一缺席 = 净是 pre-153 行为。
|
|
139
145
|
subagent: {
|
|
140
|
-
|
|
146
|
+
// #196:default/code 的委派子代**要**手(CC parity;main.ts subRunner 注里那条 "subagent
|
|
147
|
+
// couldn't run pwd" 缺陷就是这条腿缺手),故按本场景判别位取 full 支。
|
|
148
|
+
runner: subRunnerFor(deps, "full"),
|
|
141
149
|
background: {
|
|
142
150
|
registry: defaultTaskRegistry,
|
|
143
151
|
owner: "unattributed",
|
|
@@ -158,6 +166,9 @@ export function buildScenarios(deps) {
|
|
|
158
166
|
...(webSearch ? { webSearch } : {}),
|
|
159
167
|
}).filter((t) => t.name !== "WebFetch" || deps.requirePrincipal !== true),
|
|
160
168
|
],
|
|
169
|
+
// default/code 是手带并集的**故意消费者**(上方 rank-1 BLOCKER 注:CC agent loop MUST have the
|
|
170
|
+
// full-body roster)——#196 判别位在这里表态 full,收窄只落在声明了 none 的场景上。
|
|
171
|
+
hands: "full",
|
|
161
172
|
skills: skillsForScenario(deps.skills, "default"),
|
|
162
173
|
// Local/TOC deployment brands the neutral default scenario as Sema (so the local engine answers "who
|
|
163
174
|
// are you" as Sema, not the model's hallucinated identity). Cloud leaves it neutral unless opted in.
|
|
@@ -190,13 +201,15 @@ export function buildScenarios(deps) {
|
|
|
190
201
|
// would let runtime-distributed content rewrite the deployment-pinned system prompt and break the
|
|
191
202
|
// stablePrompt STABLE→VARIABLE prefix-cache discipline (design/12/17).
|
|
192
203
|
scan: (req) => {
|
|
193
|
-
|
|
194
|
-
throw new HttpError(501, "scan scenario needs GIT_API_BASEURL configured");
|
|
204
|
+
const repoClient = requireRepoClient(deps, "scan");
|
|
195
205
|
if (typeof req.repo !== "string" || !req.repo.trim()) {
|
|
196
206
|
throw new HttpError(400, "scan scenario requires a 'repo' field (owner/name or repo URL)");
|
|
197
207
|
}
|
|
198
208
|
return {
|
|
199
|
-
tools: [...repoToolsFor(
|
|
209
|
+
tools: [...repoToolsFor(repoClient, parseRepo(req.repo)), nowTool()],
|
|
210
|
+
// #196:头注的「工具集即边界」自此在**最终 roster** 上成立(修前 core 把手带 band 并集进来,
|
|
211
|
+
// clone-free 承诺只存在于这份声明里)。
|
|
212
|
+
hands: "none",
|
|
200
213
|
skills: skillsForScenario(deps.skills, "scan"),
|
|
201
214
|
};
|
|
202
215
|
},
|
|
@@ -212,8 +225,11 @@ export function buildScenarios(deps) {
|
|
|
212
225
|
}
|
|
213
226
|
const objective = typeof req.objective === "string" ? req.objective : "Discuss the task.";
|
|
214
227
|
const businessContext = typeof req.businessContext === "string" ? req.businessContext : undefined;
|
|
228
|
+
// #196:成员/synthesizer 是纯讨论人格(模板声明零工具),跑在无手 subRunner 上。
|
|
229
|
+
const hands = "none";
|
|
215
230
|
return {
|
|
216
|
-
tools: [createTeamTool(deps
|
|
231
|
+
tools: [createTeamTool(subRunnerFor(deps, hands), template, { objective, businessContext })],
|
|
232
|
+
hands,
|
|
217
233
|
skills: skillsForScenario(deps.skills, "team"),
|
|
218
234
|
promptProvider: teamCoordinatorPrompt,
|
|
219
235
|
};
|
|
@@ -224,6 +240,11 @@ export function buildScenarios(deps) {
|
|
|
224
240
|
export function selectScenario(scenarios, name) {
|
|
225
241
|
return (name && scenarios[name]) || scenarios.default;
|
|
226
242
|
}
|
|
243
|
+
/** 把属主的判别式摊平成 wire 上的两个键(可用臂整键省略原因,契约=「缺席即没有理由」)。 */
|
|
244
|
+
function availabilityFields(deps, toolset) {
|
|
245
|
+
const verdict = scenarioAvailability(deps, toolset);
|
|
246
|
+
return verdict.available ? { available: true } : { available: false, unavailableReason: verdict.reason };
|
|
247
|
+
}
|
|
227
248
|
/** 内建五场景详情。default/code/team 工厂对良性请求无副作用可真调(拿真实工具名单);code-review/scan 是
|
|
228
249
|
* fail-loud 语义(缺 GIT_API 配置/principal 即 throw)→ 探针失败落静态表兜底(表↔工厂一致性由测试锁:
|
|
229
250
|
* 测试喂 fake deps 真调工厂对账工具名)。enabled 对内建恒 true(约定②)。 */
|
|
@@ -237,7 +258,12 @@ export function builtinScenarioDetails(scenarios, deps) {
|
|
|
237
258
|
}
|
|
238
259
|
};
|
|
239
260
|
const REPO_TOOLS = ["repo_tree", "repo_read_file", "repo_pull_diff", "Now"];
|
|
240
|
-
const mk = (name, summary, toolset, tools, promptSummary) => ({
|
|
261
|
+
const mk = (name, summary, toolset, tools, promptSummary) => ({
|
|
262
|
+
name, source: "builtin", builtin: true, summary, toolset, tools, promptSummary, enabled: true,
|
|
263
|
+
// [C132]:可用性与上面的静态回退表分家 —— 回退表答的是「工具面长什么样」(缺配置时也照画),
|
|
264
|
+
// 这两个键答的是「现在点下去会不会 501」。同一份属主判据,内建与 center 两条产线共用。
|
|
265
|
+
...availabilityFields(deps, toolset),
|
|
266
|
+
});
|
|
241
267
|
return {
|
|
242
268
|
code: mk("code", "编码主场景([891] 出厂缺省;CC 编码 persona 蒸馏版,default 全量工具面;终验旋钮独立)", "full-body", probe("code", {}, undefined, ["Now"]), deps.brandIdentity
|
|
243
269
|
? "Sema 产品身份 + core 编码完全体提示词(CODE_AGENT persona+自治自检;CC 血统对表)"
|
|
@@ -250,7 +276,7 @@ export function builtinScenarioDetails(scenarios, deps) {
|
|
|
250
276
|
}
|
|
251
277
|
/** center 条目详情(有效性判定与 centerScenarios 完全同款:无效条目既不进 overlay 也不进详情——
|
|
252
278
|
* 保证约定①「详情显示的来源=运行实际用的定义」永不错位)。 */
|
|
253
|
-
export function centerScenarioDetails(specs, builtinNames) {
|
|
279
|
+
export function centerScenarioDetails(specs, builtinNames, deps) {
|
|
254
280
|
const out = {};
|
|
255
281
|
for (const spec of specs ?? []) {
|
|
256
282
|
if (spec.enabled === false)
|
|
@@ -270,24 +296,62 @@ export function centerScenarioDetails(specs, builtinNames) {
|
|
|
270
296
|
// 恒空串;字段本身保留(builtin 条目仍有真值,wire 形不变)。
|
|
271
297
|
promptSummary: "",
|
|
272
298
|
enabled: true,
|
|
299
|
+
// [C132]:center 声明的 repo 型场景与内建的 scan/code-review 受**同一份**判据管辖
|
|
300
|
+
// ——overlay 不是判据的第二属主。
|
|
301
|
+
...availabilityFields(deps, spec.toolset),
|
|
273
302
|
};
|
|
274
303
|
}
|
|
275
304
|
return out;
|
|
276
305
|
}
|
|
277
306
|
const TOOLSETS = {
|
|
278
|
-
none: { build: () => [nowTool()] },
|
|
307
|
+
none: { build: () => [nowTool()], hands: "none" },
|
|
279
308
|
"repo-readonly": {
|
|
280
309
|
requiresRepo: true,
|
|
310
|
+
hands: "none",
|
|
281
311
|
build: (deps, req) => {
|
|
282
|
-
|
|
283
|
-
throw new HttpError(501, "this scenario needs GIT_API_BASEURL configured");
|
|
312
|
+
const repoClient = requireRepoClient(deps, "this");
|
|
284
313
|
if (typeof req.repo !== "string" || !req.repo.trim()) {
|
|
285
314
|
throw new HttpError(400, "this scenario requires a 'repo' field (owner/name or repo URL)");
|
|
286
315
|
}
|
|
287
|
-
return [...repoToolsFor(
|
|
316
|
+
return [...repoToolsFor(repoClient, parseRepo(req.repo)), nowTool()];
|
|
288
317
|
},
|
|
289
318
|
},
|
|
290
319
|
};
|
|
320
|
+
const SCENARIO_AVAILABLE = { available: true };
|
|
321
|
+
/**
|
|
322
|
+
* 🔴 场景可用性的**唯一属主**。列举面(`ScenarioDetail.available` / `unavailableReason`)与请求面
|
|
323
|
+
* ({@link requireRepoClient} 的 501 拒绝臂,三处调用点)都只从这里取值——两处各写一份就是本仓反复
|
|
324
|
+
* 吃过的「同一语义两个属主」病:判据一漂,列举面开始说谎而没人先红。一致性由 capabilities.test 的
|
|
325
|
+
* **对表格**逐名钉住(`available:false` ⟺ 良性请求真吃 501),而不是靠这段注释。
|
|
326
|
+
*
|
|
327
|
+
* 判据键 = **toolset**:内建详情与 center 条目都带这个字段,故两条产线天然共用同一份判据。
|
|
328
|
+
* `requiresRepo` 的 toolset 需要部署配好 git 后端(`GIT_API_BASEURL` ⇒ `deps.repoClient`)。
|
|
329
|
+
* 词表外的 toolset(内建的 `full-body`/`team`)不依赖后端 ⇒ 恒可用。
|
|
330
|
+
*/
|
|
331
|
+
export function scenarioAvailability(deps, toolset) {
|
|
332
|
+
if (TOOLSETS[toolset]?.requiresRepo === true && deps.repoClient === undefined) {
|
|
333
|
+
return { available: false, reason: "git_client_unconfigured" };
|
|
334
|
+
}
|
|
335
|
+
return SCENARIO_AVAILABLE;
|
|
336
|
+
}
|
|
337
|
+
/** 拒绝文案的唯一属主:闭集穷举 switch(新增原因词不在这里表态即编译红)。 */
|
|
338
|
+
export function scenarioUnavailableMessage(reason, scenarioLabel) {
|
|
339
|
+
switch (reason) {
|
|
340
|
+
case "git_client_unconfigured":
|
|
341
|
+
return `${scenarioLabel} scenario needs GIT_API_BASEURL configured`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** repo 型场景取 git 客户端的唯一口(三处场景工厂同源)。不可用 ⇒ 按属主给的原因与文案 501 响亮拒。 */
|
|
345
|
+
function requireRepoClient(deps, scenarioLabel) {
|
|
346
|
+
const client = deps.repoClient;
|
|
347
|
+
if (client !== undefined)
|
|
348
|
+
return client;
|
|
349
|
+
const verdict = scenarioAvailability(deps, "repo-readonly");
|
|
350
|
+
// 判成 available 却没有客户端 = 属主与消费面脱钩(对表格会先红)。此处仍 fail-loud,不静默放行。
|
|
351
|
+
throw new HttpError(501, verdict.available
|
|
352
|
+
? `${scenarioLabel} scenario has no repo client although the availability owner reports it available (wiring defect)`
|
|
353
|
+
: scenarioUnavailableMessage(verdict.reason, scenarioLabel));
|
|
354
|
+
}
|
|
291
355
|
export const SCENARIO_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
|
|
292
356
|
// ([1053] registry-core 0.10.15:SCENARIO_PROMPT_MAX 随 ScenarioEntry.prompt 一并退役)
|
|
293
357
|
/**
|
|
@@ -316,6 +380,7 @@ export function centerScenarios(deps, specs, builtinNames, logger) {
|
|
|
316
380
|
const name = spec.name;
|
|
317
381
|
overlay[name] = (req) => ({
|
|
318
382
|
tools: toolset.build(deps, req),
|
|
383
|
+
hands: toolset.hands, // #196:表态随 toolset 走(center 声明面不可自选 hands —— 能力仍是代码定死的)
|
|
319
384
|
skills: skillsForScenario(deps.skills, name),
|
|
320
385
|
});
|
|
321
386
|
if (builtinNames.includes(name))
|
|
@@ -324,13 +389,12 @@ export function centerScenarios(deps, specs, builtinNames, logger) {
|
|
|
324
389
|
return { overlay, shadows };
|
|
325
390
|
}
|
|
326
391
|
function codeReview(deps, req) {
|
|
327
|
-
|
|
328
|
-
throw new HttpError(501, "code-review scenario needs GIT_API_BASEURL configured");
|
|
392
|
+
const repoClient = requireRepoClient(deps, "code-review");
|
|
329
393
|
if (typeof req.repo !== "string" || !req.repo.trim()) {
|
|
330
394
|
throw new HttpError(400, "code-review scenario requires a 'repo' field (owner/name or repo URL)");
|
|
331
395
|
}
|
|
332
396
|
const coords = parseRepo(req.repo);
|
|
333
|
-
const repoTools = repoToolsFor(
|
|
397
|
+
const repoTools = repoToolsFor(repoClient, coords);
|
|
334
398
|
const skills = skillsForScenario(deps.skills, "code-review");
|
|
335
399
|
const objective = typeof req.objective === "string" ? req.objective : "Review this repository.";
|
|
336
400
|
// Tiering: `council: true` runs the multi-lens council (L1 parallel lenses + L3 arbiter) — for
|
|
@@ -340,11 +404,14 @@ function codeReview(deps, req) {
|
|
|
340
404
|
// (deterministic + cheaper + cacheable; see design/35). For OPEN-ENDED tasks where the decomposition
|
|
341
405
|
// isn't known, core's `createSubagentTool` (model-driven self-delegation, with maxDepth / isolated
|
|
342
406
|
// context / compressed-report guardrails) is the right tool — wire it in a dedicated scenario then.
|
|
407
|
+
// #196:两条腿(直评 / council)都声明 none —— 直评腿的 lead 只读仓库工具,council 腿的 lead 只调
|
|
408
|
+
// run_council;lens/arbiter 子任务同样只读仓库,故 council 工具拿无手 subRunner。
|
|
409
|
+
const hands = "none";
|
|
343
410
|
if (req.council === true) {
|
|
344
411
|
const rounds = clampRounds(req.rounds); // finite-guarded (NaN/±Inf → undefined → council default 1)
|
|
345
412
|
return {
|
|
346
413
|
tools: [
|
|
347
|
-
createCouncilTool(deps
|
|
414
|
+
createCouncilTool(subRunnerFor(deps, hands), repoTools, objective, {
|
|
348
415
|
metrics: deps.metrics,
|
|
349
416
|
logger: deps.logger,
|
|
350
417
|
debate: req.debate === true, // L1+L2+L3 (peer debate) vs L1+L3
|
|
@@ -355,11 +422,12 @@ function codeReview(deps, req) {
|
|
|
355
422
|
...(rounds !== undefined ? { rounds } : {}),
|
|
356
423
|
}),
|
|
357
424
|
],
|
|
425
|
+
hands,
|
|
358
426
|
skills,
|
|
359
427
|
promptProvider: coordinatorPrompt,
|
|
360
428
|
};
|
|
361
429
|
}
|
|
362
|
-
return { tools: repoTools, skills, promptProvider: directReviewerPrompt };
|
|
430
|
+
return { tools: repoTools, hands, skills, promptProvider: directReviewerPrompt };
|
|
363
431
|
}
|
|
364
432
|
const reviewBody = "Produce ONE prioritized review: a short summary, then findings grouped by severity " +
|
|
365
433
|
"(blocker / major / minor), each with `path:line`, the problem, and a concrete fix. Cite real code — never invent files or symbols.";
|
|
@@ -144,8 +144,10 @@ function evictIfIdle(store, scope, st) {
|
|
|
144
144
|
*/
|
|
145
145
|
/** [3156] 轮2 真红的修:进程内终态行 seed 缓存 —— **写路径顺手喂**(publishFleet 的 terminal 臂),
|
|
146
146
|
* 读侧零库查询零时序竞态。存在理由:durable pull 的查询键是 caller 的 scope,而 fleetWide 连接
|
|
147
|
-
* (无 auth 部署恒是)的可见性判据放行**全部** scope ——
|
|
148
|
-
*
|
|
147
|
+
* (无 auth 部署恒是)的可见性判据放行**全部** scope —— 查询键与可见性判据不同源。行 scope 的值
|
|
148
|
+
* = 提交方自报的 principal 经引擎透传("anon:shell-live" 是 cli 壳在本地单用户模式**自发的 header
|
|
149
|
+
* 值**,引擎只做 scope=principal 赋值,不发明该字面量;[3225] 后续侦察定谳,勿再写「引擎铸」),
|
|
150
|
+
* 它永远不等于 caller 读侧的 "default",pull 恒 miss。seed 缓存按「发生过什么」
|
|
149
151
|
* 记(与订阅方无关),读侧再过各连接自己的 visW。不是活跃集:bus 的 Map 零触碰,单写者不变量原样。
|
|
150
152
|
* 有界:行数 ≤ MAX_ROWS×2、窗长同 WINDOW_MS,超界丢最旧。 */
|
|
151
153
|
const seededTerminalRows = [];
|
|
@@ -174,11 +176,12 @@ function seededRowsInWindow() {
|
|
|
174
176
|
export async function recentTerminalWorkflowRows(store, scope, pullScope) {
|
|
175
177
|
// [3156]:seed 缓存 = 本进程生命周期内全部终态(含 boot recover 判死)的同源可达径——fleetWide 连接
|
|
176
178
|
// (scope=null,可见性判据放行全 scope)取 seed **全量**,并以 `pullScope`(caller principal,尽力形)
|
|
177
|
-
// 做 durable pull 兜底:pull
|
|
178
|
-
// "anon:shell-live" 恒 miss),中了是增益、中不了还有 seed
|
|
179
|
+
// 做 durable pull 兜底:pull 的查询键与行 scope(提交方自报 principal 的透传值)可能不同源
|
|
180
|
+
// (cli 轮2 实测 "default" vs 壳自发的 "anon:shell-live" 恒 miss),中了是增益、中不了还有 seed
|
|
181
|
+
// ——两径并集,谁都不当唯一真源。
|
|
179
182
|
// scoped 连接:seed 按本 scope 滤 + durable pull 本 scope(跨进程兜底)。
|
|
180
|
-
// [3193] 病理3:seed 侧不再按 caller scope 预滤——那是第二份可见性判据(F1 病族)
|
|
181
|
-
//
|
|
183
|
+
// [3193] 病理3:seed 侧不再按 caller scope 预滤——那是第二份可见性判据(F1 病族),与行 scope
|
|
184
|
+
// (壳自发 "anon:shell-live" vs caller "default")恒 miss。本函数只产**候选集**;可见/擦除由路由的
|
|
182
185
|
// visW/stripW 同一套判据裁(与快照活行、增量帧同源),窗与 MAX_ROWS 双界仍在。
|
|
183
186
|
const seeded = seededRowsInWindow();
|
|
184
187
|
const effectiveScope = scope ?? pullScope;
|
|
@@ -1395,6 +1395,17 @@ export function composeHooks(deployment, task) {
|
|
|
1395
1395
|
const t = task;
|
|
1396
1396
|
const out = {};
|
|
1397
1397
|
if (d.preToolUse || t.preToolUse) {
|
|
1398
|
+
// 🔴 观察标(core 5.20.0 `Hooks.preToolUseObservational`)的传承折:复合体只有在**每一只在场的
|
|
1399
|
+
// `preToolUse` 都声明了纯观察**时才继续声明。方向是 fail-closed 的——
|
|
1400
|
+
// · 漏传承(该声明没声明)= 退回 5.19.0 行为:多一条链条目,跨副本赎回被响亮拒(吵,但不失守);
|
|
1401
|
+
// · 错传承(有真裁决者却声明了)= core 把复合体的判词整条**拒绝采纳**,客户端 `settings.hooks`
|
|
1402
|
+
// 的 deny/ask 静默蒸发 —— 这是安全轴上的静默 fail-open,比原缺陷坏得多。
|
|
1403
|
+
// 故判据是「无人反对」而不是「有人赞成」:任一在场面未声明 ⇒ 复合体不声明。
|
|
1404
|
+
// 缺席的槽不算反对票(`d.preToolUse` 不在 = 部署侧没有面可裁决,不影响 task 侧的性质)。
|
|
1405
|
+
const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
|
|
1406
|
+
(t.preToolUse === undefined || t.preToolUseObservational === true);
|
|
1407
|
+
if (bothObservational)
|
|
1408
|
+
out.preToolUseObservational = true;
|
|
1398
1409
|
out.preToolUse = async (toolName, input, ctx) => {
|
|
1399
1410
|
let current = input;
|
|
1400
1411
|
const contexts = [];
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 本文件对 server.ts 的引用一律 `import type`(tsc 擦除,非装载边)。
|
|
16
16
|
*/
|
|
17
17
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
-
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome } from "@sema-agent/core";
|
|
18
|
+
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
|
|
19
19
|
import type { FlatServiceDeps, RequestAuth } from "./server.js";
|
|
20
20
|
import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
|
|
21
21
|
import type { IdempotencyCache } from "./idempotency.js";
|
|
@@ -82,6 +82,9 @@ export interface RouteRequestState {
|
|
|
82
82
|
* 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
|
|
83
83
|
export interface PreparedTaskSubmission {
|
|
84
84
|
spec: TaskSpec;
|
|
85
|
+
/** #196:本次提交的执行 Runner —— 场景 hands=none 时是无手孪生(不挂 executionEnvFactory)。
|
|
86
|
+
* 两个消费域一律用它,不再直接摸 `deps.runner`(那正是「场景表态了、执行点没听」的缝)。 */
|
|
87
|
+
runner: Runner;
|
|
85
88
|
auth?: RequestAuth;
|
|
86
89
|
verify?: VerifyRoundsSpec;
|
|
87
90
|
cascade?: boolean;
|
|
@@ -91,6 +94,9 @@ export interface PreparedTaskSubmission {
|
|
|
91
94
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
92
95
|
export interface DriveResumeArgs {
|
|
93
96
|
token: CheckpointToken;
|
|
97
|
+
/** #196:续跑腿的执行 Runner。resume 会用同一条 resolveSpec 重解析场景,故与首跑选同一只 —— 否则
|
|
98
|
+
* 一次续跑就把手带 band 重新 mount 回来(收窄只在首跑成立 = 没成立)。 */
|
|
99
|
+
runner: Runner;
|
|
94
100
|
sessionId: string;
|
|
95
101
|
principal: string | undefined;
|
|
96
102
|
fleetScope: string;
|
|
@@ -48,8 +48,12 @@ async function handleFleetBody(req, res, url, ctx, miss) {
|
|
|
48
48
|
// #189:终态行窗的 durable 读交给 streamFleet 在**订阅注册之后**做(见那里的缓冲段)——放在这里
|
|
49
49
|
// (开流之前)会让"帧已扇出而本连接尚未订阅"的窗口凭空多出一个读的时长,codex R1-H1 红先复现过。
|
|
50
50
|
// [3156] 轮2 真红修:终态窗的查询键必须与可见性判据**同源**——fleetWide 连接(callerScope=null,
|
|
51
|
-
// visW 放行全 scope)此前窗却按 caller principal("default")查,而行的 scope
|
|
52
|
-
// ("anon:shell-live"
|
|
51
|
+
// visW 放行全 scope)此前窗却按 caller principal("default")查,而行的 scope 是提交方自报
|
|
52
|
+
// principal 的透传值(cli 壳本地单用户模式自发 "anon:shell-live";引擎不发明该字面量,[3225]
|
|
53
|
+
// 后续侦察定谳)⇒ durable pull 恒 miss。fleetWide ⇒ null(窗走进程内 seed 缓存,全 scope)。
|
|
54
|
+
// 读侧 `?? "default"` 三处与 core 写侧铸链同源(core prepare-task `spec.principal ?? "default"`,
|
|
55
|
+
// 其 CHANGELOG 成文「Single-tenant spelling is an explicit scope:"default"」)——在 core 撤其
|
|
56
|
+
// 写侧铸点前,server 单方面撤读侧会把无主部署的 durable pull 面弄瞎,故保持同源不动。
|
|
53
57
|
await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
|
|
54
58
|
return;
|
|
55
59
|
}
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -326,7 +326,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
326
326
|
rootTaskId: taskId,
|
|
327
327
|
...fleetRunLabels(prepared.spec.objective), // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
|
|
328
328
|
});
|
|
329
|
-
void runInBackground(
|
|
329
|
+
void runInBackground(prepared.runner, // #196:场景 hands 判别位选出的 Runner(hands=none ⇒ 无手孪生)
|
|
330
|
+
{ ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
|
|
330
331
|
gatedPrincipal(req, deps.config) ?? null, // E23: the VERIFIED principal that may answer (same source the respond gate uses — never the spoofable header)
|
|
331
332
|
captureTurnAnchor, // E18: per-turn (message eventId → leaf entryId) anchor capture
|
|
332
333
|
fleetPub, // MF-Fleet: the run-scoped fleet-row publisher (onStart/onEvent/onTerminal across all legs)
|
|
@@ -601,7 +601,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
601
601
|
// (mid-flight ctrl+b) and, as a natural consequence, steer/compact — now reach a mid-turn
|
|
602
602
|
// INTERACTIVE run too (previously only the bg + resume legs registered). Deregistered in the
|
|
603
603
|
// outer finally (with the fleet settle), so an early throw can't leak a stale handle.
|
|
604
|
-
const liveStream =
|
|
604
|
+
const liveStream = prepared.runner.runTaskStream({ ...prepared.spec, signal: ac.signal }, undefined, fwdInternals); // #196:场景 hands 判别位选出的 Runner
|
|
605
605
|
liveStreamRef = liveStream;
|
|
606
606
|
if (durableTaskId)
|
|
607
607
|
steerableRuns.set(durableTaskId, liveStream);
|
|
@@ -1283,9 +1283,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1283
1283
|
// ── B3 ── 原文此处是一处三层三元,把三条模型驱动腿挤在五行里。三腿具名化后判别顺序
|
|
1284
1284
|
// (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
|
|
1285
1285
|
// 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1286
|
+
// #196:三腿一律跑 `prepared.runner`(场景 hands 判别位选出的那只),不再直接摸 deps.runner。
|
|
1287
|
+
const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
|
|
1288
|
+
const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
|
|
1289
|
+
const plainLeg = () => prepared.runner.runTask(specWithSignal);
|
|
1289
1290
|
result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
|
|
1290
1291
|
}
|
|
1291
1292
|
finally {
|
package/dist/http/server.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export interface ServiceCoreDeps {
|
|
|
77
77
|
resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
|
|
78
78
|
leg?: "fresh" | "resume";
|
|
79
79
|
}) => TaskSpec | Promise<TaskSpec>;
|
|
80
|
+
/**
|
|
81
|
+
* #196 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
|
|
82
|
+
* 装配点(main.ts)把「场景判别位 → Runner 对」的映射整只闭包传进来 —— 本层不解释场景,只取用。
|
|
83
|
+
* 缺席 ⇒ 恒 {@link ServiceDeps.runner}(单 Runner 部署形 = #196 之前的行为,逐字不变;测试桩即此形)。
|
|
84
|
+
*/
|
|
85
|
+
runnerFor?: (spec: TaskSpec) => Runner;
|
|
80
86
|
}
|
|
81
87
|
/** **durable 持久面**:每个键 present ⇔ 对应路由/能力位在场,absent ⇒ 501(诚实缺席),
|
|
82
88
|
* 绝不静默降级。装配点 = main.ts 的 store backend 开箱段。 */
|
package/dist/http/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
4
|
+
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, RETIRED_TOOL_NAMES, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
6
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
7
|
import {} from "../config-center/facade.js";
|
|
@@ -107,7 +107,10 @@ const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024; // a single inline image's base6
|
|
|
107
107
|
// Hand (executionEnv) tool names core mounts at the runner when an executionEnvFactory is configured — the
|
|
108
108
|
// authoritative set is core `HAND_TOOL_EFFECTS` (root-exported since 1.70). These are NEVER in spec.tools, so
|
|
109
109
|
// the durable-resume satisfiability pre-check must union them when remote exec is on.
|
|
110
|
-
|
|
110
|
+
// [3248]: filter RETIRED_TOOL_NAMES — core 5.21.0 left retired "MultiEdit" in HAND_TOOL_EFFECTS; a retired
|
|
111
|
+
// name is never mounted, so keeping it here would let the pre-check claim satisfiability for a tool that
|
|
112
|
+
// cannot exist (same derivation hygiene as hands-lane.ts HANDS_BAND_TOOL_NAMES).
|
|
113
|
+
const HAND_TOOL_NAMES = Object.keys(HAND_TOOL_EFFECTS).filter((n) => !RETIRED_TOOL_NAMES.has(n));
|
|
111
114
|
/** [1245] codex-3/4 — the REOPEN-class resume failures: core's runtask reopened the checkpoint (store.reopen,
|
|
112
115
|
* dist-read) BEFORE returning this failed result, so the park is STILL PENDING and decidable. Everything
|
|
113
116
|
* downstream must treat these as a re-park, never terminal: the durable event log writes `suspended` (not
|
|
@@ -615,6 +618,11 @@ export function createHttpServer(rawDeps) {
|
|
|
615
618
|
return;
|
|
616
619
|
sendError(res, 404, "not_found.route", "not found");
|
|
617
620
|
}
|
|
621
|
+
/** #196:本任务跑哪只 Runner —— 由装配点按场景 hands 判别位裁定(main.ts 的 `runnerFor` 闭包)。
|
|
622
|
+
* seam 缺席 = 单 Runner 部署形(#196 之前的行为逐字不变),不是降级臂。 */
|
|
623
|
+
function runnerFor(spec) {
|
|
624
|
+
return deps.runnerFor?.(spec) ?? deps.runner;
|
|
625
|
+
}
|
|
618
626
|
/** Read + validate the body, authorize, and build the spec. Returns null if it already responded. */
|
|
619
627
|
async function prepareSpec(req, res) {
|
|
620
628
|
const body = (await readJson(req));
|
|
@@ -1205,7 +1213,10 @@ export function createHttpServer(rawDeps) {
|
|
|
1205
1213
|
}
|
|
1206
1214
|
try {
|
|
1207
1215
|
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
1208
|
-
|
|
1216
|
+
const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh" });
|
|
1217
|
+
// #196:执行 Runner 与 spec 同一次解析产出 —— 两个消费域(tasks 同步/流式、runs 异步)拿的都是这一只,
|
|
1218
|
+
// 不各自再问一遍(再问 = 两处判别,正是漂移的成因)。
|
|
1219
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body };
|
|
1209
1220
|
}
|
|
1210
1221
|
catch (err) {
|
|
1211
1222
|
if (err instanceof HttpError) {
|
|
@@ -1413,9 +1424,12 @@ export function createHttpServer(rawDeps) {
|
|
|
1413
1424
|
// Typed pre-check: the pending tool must still exist in the rebuilt config; a scenario
|
|
1414
1425
|
// redeploy that removed it means the approval can no longer be applied — fail clearly, never a 500/silent.
|
|
1415
1426
|
// The "hand" tools are NOT in spec.tools — core mounts them at the runner from the executionEnvFactory
|
|
1416
|
-
// (
|
|
1417
|
-
// has remote exec configured the rebuilt task WILL expose them, so union them in; else
|
|
1418
|
-
// (the common code-agent case) would always 422 on resume.
|
|
1427
|
+
// (`HAND_TOOL_NAMES` above derives from core's root-exported `HAND_TOOL_EFFECTS`; nothing is mirrored here).
|
|
1428
|
+
// When this deployment has remote exec configured the rebuilt task WILL expose them, so union them in; else
|
|
1429
|
+
// gating a hand tool (the common code-agent case) would always 422 on resume.
|
|
1430
|
+
// #196 旁注:本 union 只看部署有没有 remoteExec,不看本任务的 hands lane —— 对 hands=none 的任务这是
|
|
1431
|
+
// **过近似**(那些工具根本没 mount,也就不可能有它们的 park 行),只影响 422 的 UX 宽严,不放大任何权限;
|
|
1432
|
+
// band 的**完整**名单(含 Monitor/EnterWorktree/… 这些不在 effects 表里的)在 capabilities/hands-lane.ts。
|
|
1419
1433
|
// [2380] RB-476(5.0.0):Q6 折叠预检退役——满足性匹配按 RAW 名。pre-floor 旧名 checkpoint
|
|
1420
1434
|
// (`bash`/`run_workflow` 等)在此 422 诚实拒,与 core 自己的 resume.tool_unavailable 同方向
|
|
1421
1435
|
// ([2364] 令:耐久面响亮牺牲 + P-7 重开,不静默兜)。现役名恒自映射。
|
|
@@ -1510,7 +1524,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1510
1524
|
// heartbeat, the resumeStream / resumeWithVerification loop, CheckpointError classification, re-suspend vs
|
|
1511
1525
|
// terminal drive. This HITL /decide path supplies a `policy_ask` outcome; the scheduler preempt path supplies
|
|
1512
1526
|
// a `resource_limit`/continue one — sharing this prevents the two resume entries from drifting.
|
|
1513
|
-
return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}) });
|
|
1527
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}) });
|
|
1514
1528
|
}
|
|
1515
1529
|
/**
|
|
1516
1530
|
* The resume-drive machinery SHARED by `resumeCheckpoint` (operator /decide, `policy_ask`) and `resumePreempted`
|
|
@@ -1524,7 +1538,8 @@ export function createHttpServer(rawDeps) {
|
|
|
1524
1538
|
* (setSuspended, keep the lock) vs a terminal (setTerminal), and returns the HTTP {status, body}.
|
|
1525
1539
|
*/
|
|
1526
1540
|
async function driveResumeIntoRunLog(args) {
|
|
1527
|
-
|
|
1541
|
+
// #196:`runner` = 本次续跑的执行 Runner(场景 hands 判别位裁定,与首跑同源;见 DriveResumeArgs 注)。
|
|
1542
|
+
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
|
|
1528
1543
|
// ── #151 车3 刀 3b:resume 腿的 `legKey` 真值(设计稿 §3.2′)──────────────────────────────────
|
|
1529
1544
|
// 一次 park→resume 的 checkpoint token **就是**这条腿的天然身份:同一 token 重投 = 同一条腿(幂等,
|
|
1530
1545
|
// askId 不变);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId ⇒ **重新征询**(§3.3「resume 只重放未完成兄弟」)。
|
|
@@ -1732,7 +1747,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1732
1747
|
if (verifyRounds !== undefined) {
|
|
1733
1748
|
// resumeWithVerification runs the SAME pre-CAS guard + atomic CAS as resume() internally, so a lost
|
|
1734
1749
|
// CAS rejects with CheckpointError before any work — handled by the catch below, same as the stream path.
|
|
1735
|
-
const vr = await resumeWithVerification(
|
|
1750
|
+
const vr = await resumeWithVerification(legRunner, token, outcome, { ...taskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds); // 快审 F1:整对象直传
|
|
1736
1751
|
let safe = stripCheckpointToken(vr);
|
|
1737
1752
|
// 🔴 A re-suspend on a LATER gate is NOT terminal: core maps it to status:"failed" +
|
|
1738
1753
|
// verification.unverifiedReason:"suspended" with the checkpoint token still live. Treat it exactly like
|
|
@@ -1828,7 +1843,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1828
1843
|
// (`++seq` is a sync increment — sink-vs-loop appends get unique seqs); only when a durable log exists.
|
|
1829
1844
|
// false once this leg settles — notifications after that take the durable-inbox path.
|
|
1830
1845
|
resumeLegLive = true;
|
|
1831
|
-
const stream = await
|
|
1846
|
+
const stream = await legRunner.resumeStream(token, outcome, { ...taskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, {
|
|
1832
1847
|
onForwardEvent: (e) => {
|
|
1833
1848
|
fleetPub?.onForwardEvent(e);
|
|
1834
1849
|
// S2 live tail(复审 #1:forward sink 有三条腿——resume 腿上 spawn 的 bg 子代同样带
|
|
@@ -2256,7 +2271,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2256
2271
|
// A resource-suspended task that was submitted with verify:true is re-gated on completion (parity with the
|
|
2257
2272
|
// HITL path — the flag rides the persisted ctx.body).
|
|
2258
2273
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
2259
|
-
return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds });
|
|
2274
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds });
|
|
2260
2275
|
}
|
|
2261
2276
|
/** design/144 §3:wake 一个 task_done 纯 park——非门决策,解除 park 消费消息续跑。owner 门:非 operator
|
|
2262
2277
|
* 只能 wake 自己 scope 的 park(404 无 oracle);core 端 gate_pending/nothing_to_deliver 双拒兜底。 */
|
|
@@ -2314,6 +2329,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2314
2329
|
const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
|
|
2315
2330
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
2316
2331
|
return driveResumeIntoRunLog({
|
|
2332
|
+
runner: runnerFor(spec),
|
|
2317
2333
|
token,
|
|
2318
2334
|
sessionId: cp.sessionId,
|
|
2319
2335
|
principal: auth.principal,
|
|
@@ -2379,7 +2395,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2379
2395
|
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
2380
2396
|
...(reason ? { reason } : {}),
|
|
2381
2397
|
};
|
|
2382
|
-
return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds });
|
|
2398
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds });
|
|
2383
2399
|
}
|
|
2384
2400
|
function recordTaskResult(result) {
|
|
2385
2401
|
deps.metrics?.inc("tasks_total", { status: result.status });
|