@sema-agent/client-core 0.7.0 → 0.8.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/README.md +31 -11
- package/dist/adapt.js +36 -3
- package/dist/compensations.d.ts +63 -0
- package/dist/compensations.js +138 -0
- package/dist/detachWire.d.ts +141 -0
- package/dist/detachWire.js +182 -0
- package/dist/engineSessionParam.d.ts +6 -2
- package/dist/engineSessionParam.js +24 -18
- package/dist/engineWireTarget.d.ts +14 -0
- package/dist/engineWireTarget.js +53 -0
- package/dist/fleet/fleetLedger.d.ts +132 -0
- package/dist/fleet/fleetLedger.js +393 -0
- package/dist/fleet/fleetProjection.d.ts +181 -0
- package/dist/fleet/fleetProjection.js +258 -0
- package/dist/hooksWireCaps.d.ts +91 -0
- package/dist/hooksWireCaps.js +359 -0
- package/dist/host.d.ts +34 -2
- package/dist/host.js +6 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +82 -1
- package/dist/liveInitToolFace.d.ts +53 -0
- package/dist/liveInitToolFace.js +251 -0
- package/dist/model/modelFamilies.json +63 -0
- package/dist/model/providerPresets.d.ts +46 -0
- package/dist/model/providerPresets.js +160 -0
- package/dist/model/providerPresets.json +1179 -0
- package/dist/seam.d.ts +19 -3
- package/dist/seam.js +14 -2
- package/dist/subagent/engineCompactWire.d.ts +12 -0
- package/dist/subagent/engineCompactWire.js +176 -0
- package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
- package/dist/subagent/engineDelegatedPrompt.js +205 -0
- package/dist/subagent/engineRowStopGate.d.ts +18 -0
- package/dist/subagent/engineRowStopGate.js +54 -0
- package/dist/subagent/engineSubagentOutput.d.ts +15 -0
- package/dist/subagent/engineSubagentOutput.js +98 -0
- package/dist/subagent/engineSubagentSteer.d.ts +10 -0
- package/dist/subagent/engineSubagentSteer.js +68 -0
- package/dist/subagent/engineSubagentTail.d.ts +40 -0
- package/dist/subagent/engineSubagentTail.js +241 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +89 -0
- package/dist/subagent/engineTaskHandleWire.js +213 -0
- package/dist/workflowClient.d.ts +42 -0
- package/dist/workflowClient.js +467 -0
- package/dist/workflowMonitor.d.ts +94 -0
- package/dist/workflowMonitor.js +43 -0
- package/package.json +2 -2
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/detachWire.ts` **拆**搬 —— 判定半场 +
|
|
3
|
+
* cancel-arm 台账进包,**信号路径的裸 fetch 发射留宿主**(设计稿 §3 表 `S/detachWire.ts` 那条
|
|
4
|
+
* 脚注:`:221` 裸 cancel = TUI 留,宪法例外 census C1-4)。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 **拆缝画在哪、为什么**:`fireBestEffortDetachCancel` 是**信号处理器**里同步 DISPATCH 的
|
|
7
|
+
* fire-and-forget —— 它成立的前提是「进程正在被 SIGINT/SIGTERM 拆掉,不能 await、不能构造重对象」。
|
|
8
|
+
* 这套前提是 **Node 进程模型**的,浏览器/桌面渲染进程根本没有信号;而且它有意**绕开 SDK verb**
|
|
9
|
+
* (`AgentClient.runs.cancel` 是 await 型,包一层 void+catch 反而把纪律面弄模糊)——那是壳的
|
|
10
|
+
* 宪法例外,不该被搬成「库里有一处裸 fetch」。⇒ 发射留宿主,库出**台账 + 读口**。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **模块级状态与读写口同包**([multiclient-migration-batch-discipline]):`cancelArm` 一份,
|
|
13
|
+
* 写口 `armDetachCancel`(seamQueryEngine.ask 在头真拼上请求时调)、读口 `isDetachArmed`
|
|
14
|
+
* (`detachDurableOffHint` 的第一道门)/ `detachedTaskId`(分诊文案)/ `detachCancelArm`
|
|
15
|
+
* (**宿主发射腿**唯一取件口)全在本文件。壳侧 shim 的 `fireBestEffortDetachCancel` 从
|
|
16
|
+
* `detachCancelArm()` 取件 ⇒ 仍然只有一份台账;壳若自己再存一份 = 头发了但 hint 判不出、
|
|
17
|
+
* Ctrl+C 取消不掉(两个静默失效各一半)。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 env 读取走 `hostEnv()`(本包零 `process`,portability 门盯着);Node 下同一对象 ⇒ 壳侧
|
|
20
|
+
* 「不传 env 参数」的调用点行为一字节不变。
|
|
21
|
+
*
|
|
22
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* detachWire — headless `-p` detach-on-disconnect wire (重连腿 M1 壳半场, 2026-07-15,
|
|
25
|
+
* design/HEADLESS-RECONNECT-DESIGN.md §6 M1; mirrors finalVerifyWire/limitsWire in shape).
|
|
26
|
+
*
|
|
27
|
+
* WHY: the `/v1/tasks/stream` lane ABORTS the run on client disconnect by design (anti token-burn —
|
|
28
|
+
* this is also the interactive cancel mechanism). server ≥1.196 accepts an opt-in escape hatch: the
|
|
29
|
+
* `x-detach-on-disconnect: true` REQUEST HEADER makes a disconnect DETACH the run instead — it keeps
|
|
30
|
+
* running to its terminal and lands in the durable run ledger (`GET /v1/runs/:id`; a deployment with
|
|
31
|
+
* no durable ledger 400s fail-loud — the shell's self-spawned engine always sets DB_BACKEND=local,
|
|
32
|
+
* so the ledger is always there on that lane). M1 = header + cancel fallback ONLY; reconnect/resume
|
|
33
|
+
* (R1 runs.events lane-switch / R2 idempotent re-POST) is M2/M3 — NOT here.
|
|
34
|
+
*
|
|
35
|
+
* 铁律 1 (design §2.1): the header is stamped ONLY on the headless `-p` lane (seamQueryEngine.ask),
|
|
36
|
+
* live-gated on SEMA_LIVE_BASEURL exactly like finalVerifyWire — the interactive REPL lane
|
|
37
|
+
* (seamQuery.ts) never imports this module, so Esc/interrupt keeps its disconnect==cancel semantics
|
|
38
|
+
* verbatim, and the mock/pty lanes never see the header (offline projection byte-identical).
|
|
39
|
+
*
|
|
40
|
+
* 铁律 2 (design §2.2): once detach is on, a USER abort must carry an explicit best-effort cancel —
|
|
41
|
+
* a detached run would otherwise burn tokens to completion in the background after Ctrl+C. The
|
|
42
|
+
* cancel-arm half of this module (armDetachCancel / the host's fireBestEffortDetachCancel) is
|
|
43
|
+
* consumed by the `-p` SIGINT handler and the SIGTERM/SIGHUP tail-frame handler (cli/print.ts):
|
|
44
|
+
* fire-and-forget `POST /v1/runs/:id/cancel` (the SDK RunsResource.cancel verb, runs.js — verified:
|
|
45
|
+
* no body), never awaited on the signal path. taskId not yet captured (pre X-Task-Id window) ⇒
|
|
46
|
+
* ACCEPTED uncancellable window — detach's inherent cost, no waiting introduced (design §2.2 记账).
|
|
47
|
+
*
|
|
48
|
+
* PRECEDENCE (off wins; otherwise DEFAULT ON):
|
|
49
|
+
* a) `SEMA_HEADLESS_DETACH` env = 'false'/'0' (settings env lane, fail-soft — any other value
|
|
50
|
+
* including typos stays default-on, finalVerifyWire discipline) → off.
|
|
51
|
+
* b) VERSION GATE: SEMA_ENGINE_URL in play (user-supplied external engine) and `/health` CONFIRMS
|
|
52
|
+
* version <1.196.0 (or no version field = pre-1.96) → off + SEMA_DEBUG log.
|
|
53
|
+
* Unlike limitsWire (warn-and-send: an old server drops an unknown BODY field server-side), here
|
|
54
|
+
* we GATE: an old server silently ignores the unknown HEADER, so a disconnect still kills the
|
|
55
|
+
* run — sending it would only manufacture a false "detached" belief downstream (判死文案 would
|
|
56
|
+
* lie). Probe unreachable / unparsable response ⇒ fail-soft SEND (can't confirm staleness; the
|
|
57
|
+
* stream POST itself fails loudly if the engine is truly down).
|
|
58
|
+
* c) self-spawn lane (no SEMA_ENGINE_URL): release-pinned ≥1.197.0 — trusted, no probe, header on.
|
|
59
|
+
*/
|
|
60
|
+
import { hostEnv } from './hostEnv.js';
|
|
61
|
+
import { versionSupportsLimits } from './limitsWire.js';
|
|
62
|
+
/** The server ≥1.196 opt-in header: disconnect ⇒ detach (run continues to durable terminal). */
|
|
63
|
+
export const DETACH_HEADER = 'x-detach-on-disconnect';
|
|
64
|
+
/** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。'false'/'0' = off。 */
|
|
65
|
+
export const HEADLESS_DETACH_ENV = 'SEMA_HEADLESS_DETACH';
|
|
66
|
+
/**
|
|
67
|
+
* First server version that honors the detach header — 1.196.0, the SAME floor as the limits wire
|
|
68
|
+
* (both landed in the [857] batch), so the version predicate is limitsWire.versionSupportsLimits
|
|
69
|
+
* re-exported under the detach name (one comparator, two wires — 抽共用 per design).
|
|
70
|
+
*/
|
|
71
|
+
export const DETACH_WIRE_MIN_ENGINE = [1, 196, 0];
|
|
72
|
+
/** True when the engine version string satisfies the detach-wire minimum (≥1.196.0). Unparsable ⇒ false. */
|
|
73
|
+
export const versionSupportsDetach = versionSupportsLimits;
|
|
74
|
+
/**
|
|
75
|
+
* The settings-lane off-switch: `SEMA_HEADLESS_DETACH` = 'false' or '0' (case-insensitive) disables
|
|
76
|
+
* the header. Any other value (empty/'true'/typo) = default ON — fail-soft, a settings typo never
|
|
77
|
+
* flips the lane silently to the strict-alignment (disconnect-kills-run) mode (finalVerifyWire 纪律).
|
|
78
|
+
*/
|
|
79
|
+
export function headlessDetachDisabledByEnv(env = hostEnv()) {
|
|
80
|
+
const v = env[HEADLESS_DETACH_ENV]?.trim().toLowerCase();
|
|
81
|
+
return v === 'false' || v === '0';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Version-gate probe: GET `${baseUrl}/health`, read `version`. Returns FALSE only when the engine is
|
|
85
|
+
* CONFIRMED older than 1.196.0 (parsable old version, or a healthy response with no version field =
|
|
86
|
+
* pre-1.96). Unreachable / timeout / non-2xx / unparsable JSON ⇒ TRUE (fail-soft: can't confirm
|
|
87
|
+
* staleness — the header is harmless on the wire, and the stream POST fails loudly on a dead engine).
|
|
88
|
+
* Same probe shape as limitsWire.engineLimitsSupportWarning (1.5s budget, never throws).
|
|
89
|
+
*/
|
|
90
|
+
export async function engineSupportsDetach(baseUrl, opts) {
|
|
91
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 1500);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/health`, {
|
|
96
|
+
method: 'GET',
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
return true; // engine answered oddly — can't confirm staleness, fail-soft send
|
|
101
|
+
const body = (await res.json());
|
|
102
|
+
const version = typeof body?.version === 'string' ? body.version : undefined;
|
|
103
|
+
// No version field = pre-1.96 engine — definitely older than 1.196.0: gate the header.
|
|
104
|
+
return versionSupportsDetach(version);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return true; // unreachable/timeout — cannot confirm staleness, fail-soft send
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the detach header for a headless `-p` submit. DEFAULT ON; `SEMA_HEADLESS_DETACH`=false|0
|
|
115
|
+
* ⇒ off (env-off); SEMA_ENGINE_URL external engine confirmed <1.196.0 ⇒ off (old-engine — the caller
|
|
116
|
+
* SEMA_DEBUG-logs it). Self-spawn lane (no SEMA_ENGINE_URL) never probes (pin ≥1.197 trusted).
|
|
117
|
+
* The caller MUST live-gate on SEMA_LIVE_BASEURL (finalVerifyWire 同款) — mock/pty lanes never
|
|
118
|
+
* reach this resolver, so they never see the header.
|
|
119
|
+
*/
|
|
120
|
+
export async function resolveHeadlessDetach(baseUrl, env = hostEnv(), opts) {
|
|
121
|
+
if (headlessDetachDisabledByEnv(env))
|
|
122
|
+
return { on: false, reason: 'env-off' };
|
|
123
|
+
if (env.SEMA_ENGINE_URL) {
|
|
124
|
+
// user-supplied external engine — the only lane where an old server is possible
|
|
125
|
+
const ok = await engineSupportsDetach(baseUrl, opts);
|
|
126
|
+
if (!ok)
|
|
127
|
+
return { on: false, reason: 'old-engine' };
|
|
128
|
+
}
|
|
129
|
+
return { on: true, reason: 'default-on' };
|
|
130
|
+
}
|
|
131
|
+
// ── durable-off 400 的人话分诊([862]④,1.0.5 小件5) ─────────────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// server ≥1.196 对带 detach 头但无 durable 账本(无 runStore / 无 sessionId)的 submit 判 400 fail-loud
|
|
134
|
+
// (server http/server.ts [854]①b:静默忽略 = 断连仍杀 run,与 caller 声明语义相反;detach 成功了结果
|
|
135
|
+
// 也无处可查)。壳自 spawn 引擎恒 DB_BACKEND=local 永不命中;命中面 = SEMA_ENGINE_URL 外接引擎显式关
|
|
136
|
+
// durable。裸 `sema: API Error: 400 · x-detach-on-disconnect requires a durable run …` 是 server 视角
|
|
137
|
+
// 的契约话,用户不知道下一步 —— 追加一行人话(supplement, never mask:API Error 行字面保留)。
|
|
138
|
+
/** server 400 body 判别锚(server.ts sendJson 原文:"x-detach-on-disconnect requires a durable run
|
|
139
|
+
* (a run store + sessionId) — without one the detached result would be unqueryable")。 */
|
|
140
|
+
export const DETACH_DURABLE_OFF_400_ANCHOR = 'x-detach-on-disconnect requires a durable run';
|
|
141
|
+
/**
|
|
142
|
+
* detach 头引发的 durable-off 400 → 人话提示行;其它错误(非 400 / 非该 body / 头没发过)恒 null
|
|
143
|
+
* (绝不误伤其它 400 —— sandboxImageProfile 400 等各有自己的 hint)。判别三重:头确已发出
|
|
144
|
+
* (isDetachArmed —— armDetachCancel 只在头拼上请求时调)+ SDK APIError.status===400 + message 含
|
|
145
|
+
* server 原文锚(sdk errors.js toApiError: msg = body.error 原文)。
|
|
146
|
+
*/
|
|
147
|
+
export function detachDurableOffHint(err) {
|
|
148
|
+
if (!isDetachArmed())
|
|
149
|
+
return null;
|
|
150
|
+
const e = err;
|
|
151
|
+
if (e?.status !== 400)
|
|
152
|
+
return null;
|
|
153
|
+
const msg = typeof e?.message === 'string' ? e.message : '';
|
|
154
|
+
if (!msg.includes(DETACH_DURABLE_OFF_400_ANCHOR))
|
|
155
|
+
return null;
|
|
156
|
+
return 'engine has durable runs disabled — detach-on-disconnect unavailable; interrupted turns will abort (SEMA_HEADLESS_DETACH=false to silence)';
|
|
157
|
+
}
|
|
158
|
+
let cancelArm = null;
|
|
159
|
+
/** Arm the signal-path cancel fallback (called by ask when the detach header is actually sent). */
|
|
160
|
+
export function armDetachCancel(arm) {
|
|
161
|
+
cancelArm = arm;
|
|
162
|
+
}
|
|
163
|
+
/** Whether this process sent the detach header on its `-p` stream (the 判死文案 upgrade gate). */
|
|
164
|
+
export function isDetachArmed() {
|
|
165
|
+
return cancelArm !== null;
|
|
166
|
+
}
|
|
167
|
+
/** The detached run's durable taskId, when already captured (X-Task-Id) — for the triage copy. */
|
|
168
|
+
export function detachedTaskId() {
|
|
169
|
+
return cancelArm?.getTaskId() ?? null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 🔴 **宿主发射腿的唯一取件口**(拆缝的库这一侧)。宿主的 `fireBestEffortDetachCancel` 必须从这里
|
|
173
|
+
* 取 arm,**不许自己再存一份** —— 台账两份 = 头发了但 hint 判不出 / Ctrl+C 取消不掉。
|
|
174
|
+
* null = 没 arm 过(detach 关 / 版本门挡下 / mock 车道)⇒ 宿主零动作、零行为变化。
|
|
175
|
+
*/
|
|
176
|
+
export function detachCancelArm() {
|
|
177
|
+
return cancelArm;
|
|
178
|
+
}
|
|
179
|
+
/** Test-only reset (module state is process-lifetime; `-p` is a one-shot). */
|
|
180
|
+
export function _resetDetachWireForTest() {
|
|
181
|
+
cancelArm = null;
|
|
182
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/** 当前会话的 session 参数值;undefined =
|
|
2
|
-
* 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
|
|
1
|
+
/** 当前会话的 session 参数值;undefined = 端未装 SessionPort,或端自己也还没有值。 */
|
|
3
2
|
export declare function engineSessionParam(): string | undefined;
|
|
4
3
|
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
5
4
|
export declare function engineSessionParamSpread(): {
|
|
6
5
|
session?: string;
|
|
7
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* 当前活跃引擎 run 的 id(壳 = `engineToolDetach.getActiveEngineTaskId()`)。
|
|
9
|
+
* 库内多处 durable 动词要「宿主 run」做寻址第一跳,统一从这里取,不各自 import 端模块。
|
|
10
|
+
*/
|
|
11
|
+
export declare function activeEngineRunId(): string | undefined;
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,多端改造设计稿 §3 B6 + B4「四残件」之 session-id port):
|
|
3
|
+
* cli `src/sema/engineSessionParam.ts` 搬入,唯一改动 = **取值源换 port**。
|
|
4
|
+
*
|
|
2
5
|
* engineSessionParam — durable run 面 `?session=` 参数的统一派生([1501]§三,server 1.246 起
|
|
3
6
|
* legacy 八面软拍纪律:带了且不匹配=404,没带=warn-once 放行,二拍将把「没带」也 404;新面
|
|
4
7
|
* A/B(taskOutput/taskStop)与 subagentOutput 则出生即强制)。
|
|
5
8
|
*
|
|
6
|
-
* 取值语义 = run 绑定的会话(
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 取值语义 = run 绑定的会话(本端所有 durable 动词的目标 run 都是本会话自有 run)。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **搬迁的唯一差分**:壳里这里是「`getLiveSessionId()`(引擎捕获值)优先,退
|
|
12
|
+
* `bootstrap/state.getSessionId()`(壳 boot-time id),后者靠动态 import 落地」——**两级优先与
|
|
13
|
+
* 那个动态 import 都是宿主生命周期资产**,整体上移到 `SessionPort.currentSessionId()`
|
|
14
|
+
* (壳侧 shim 里原样保留那两级 + 动态 import,行为一字节不变)。库这边只剩「取一个值 + 空串归一」。
|
|
12
15
|
*
|
|
13
16
|
* 🔴 [1501] 空串陷阱:`?session=`(空值)按「带了且不匹配」处理=404 —— 本 helper 恒不返回
|
|
14
17
|
* 空串:要么真值要么 undefined(调用方 spread 姿势 `...(s ? { session: s } : {})`)。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 端漏装 SessionPort 的后果**不是**「少个查询参数」:session-bound run 的 taskOutput /
|
|
20
|
+
* taskStop / subagentOutput 三面一律 404 fail-closed ⇒ 子代读面整条静默哑掉。所以
|
|
21
|
+
* `hostSession()` 计 miss,宿主自检 `hostPortMisses()` 恒应为空。
|
|
15
22
|
*/
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
void import('../bootstrap/state.js')
|
|
19
|
-
.then(m => {
|
|
20
|
-
shellSessionIdFn = m.getSessionId;
|
|
21
|
-
})
|
|
22
|
-
.catch(() => {
|
|
23
|
-
// 极端环境缺模块:退化为只用 getLiveSessionId(),与 fleetClient/engineSubagentOutput 同款容错
|
|
24
|
-
});
|
|
25
|
-
/** 当前会话的 session 参数值;undefined = 两源都缺席(动态 import 未落地的头几毫秒)——
|
|
26
|
-
* 调用方此时不带参(legacy 面软拍放行;强制面 404 自回落,下次重试自然带上)。 */
|
|
23
|
+
import { hostSession } from './host.js';
|
|
24
|
+
/** 当前会话的 session 参数值;undefined = 端未装 SessionPort,或端自己也还没有值。 */
|
|
27
25
|
export function engineSessionParam() {
|
|
28
|
-
const sid =
|
|
26
|
+
const sid = hostSession()?.currentSessionId();
|
|
29
27
|
return typeof sid === 'string' && sid.length > 0 ? sid : undefined;
|
|
30
28
|
}
|
|
31
29
|
/** spread 便捷形:`{ ...engineSessionParamSpread() }` → `{ session: '<sid>' }` 或 `{}`。 */
|
|
@@ -33,3 +31,11 @@ export function engineSessionParamSpread() {
|
|
|
33
31
|
const s = engineSessionParam();
|
|
34
32
|
return s ? { session: s } : {};
|
|
35
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* 当前活跃引擎 run 的 id(壳 = `engineToolDetach.getActiveEngineTaskId()`)。
|
|
36
|
+
* 库内多处 durable 动词要「宿主 run」做寻址第一跳,统一从这里取,不各自 import 端模块。
|
|
37
|
+
*/
|
|
38
|
+
export function activeEngineRunId() {
|
|
39
|
+
const id = hostSession()?.activeRunId();
|
|
40
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EngineWireTarget {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
principal: string;
|
|
5
|
+
}
|
|
6
|
+
/** 装/卸引擎 wire 目标。传 null 卸回 env 派生。返回还原函数。 */
|
|
7
|
+
export declare function installEngineWireTarget(t: EngineWireTarget | null): () => void;
|
|
8
|
+
/**
|
|
9
|
+
* 当前引擎 wire 目标。null = 无 wire(离线/mock/浏览器未装)—— 调用方走各自的回落路径,
|
|
10
|
+
* **不是**错误态。
|
|
11
|
+
*/
|
|
12
|
+
export declare function engineWireTarget(): EngineWireTarget | null;
|
|
13
|
+
/** 诊断开关(壳侧 `process.env.SEMA_DEBUG` 的等价读;本包零 process)。 */
|
|
14
|
+
export declare function engineWireDebugEnabled(): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):子代 wire 族里**逐字重复**的 `wireConfig()`
|
|
3
|
+
* (SEMA_LIVE_BASEURL / SEMA_LIVE_TOKEN / SEMA_LIVE_PRINCIPAL 三读 + 'anon:shell-live' 缺省)
|
|
4
|
+
* 收成一份。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 收编前的事实(壳 HEAD 2aa1d3e 实测,`grep -rln 'function wireConfig' src/sema/`):
|
|
7
|
+
* **七**份而不是六份 —— `engineSubagentOutput.ts:30` · `engineSubagentSteer.ts:26` ·
|
|
8
|
+
* `engineTaskHandleWire.ts:40` · `engineCompactWire.ts:77` · `engineSubagentTail.ts:40` ·
|
|
9
|
+
* `engineDelegatedPrompt.ts:80` · **`planReviewWire.ts:36`**。
|
|
10
|
+
* ⚠️ 本批搬入的是前六份(子代/fleet 族);**第七份 `planReviewWire` 不在 B6 范围**,仍留壳内
|
|
11
|
+
* —— 它是下一批的收编候选,别以为已经收干净了(这条更正即为「按名字数数会数漏」的实例)。
|
|
12
|
+
* 七份漂移不是假想:`engineCompactWire` 那份已经多了一行 principal 注释,`engineSubagentSteer`
|
|
13
|
+
* 那份 token 读法与其它几份的 `...(cfg.token ? …)` spread 姿势不同步。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 env 读取走 `hostEnv()`(本包零 `process` —— portability 门盯着)。Node 下 `hostEnv()` 返回的
|
|
16
|
+
* 就是 `process.env` 那个同一对象 ⇒ 壳侧行为一字节不变;浏览器宿主返回 `{}` ⇒ `baseUrl`
|
|
17
|
+
* 缺席 ⇒ 每个 verb 走自己既有的「无 wire」回落路径(不是报错)。
|
|
18
|
+
* ⚠️ web/桌面宿主要用这条 wire,**不能靠 env** —— 用 `installEngineWireTarget()` 显式装。
|
|
19
|
+
*/
|
|
20
|
+
import { hostEnv } from './hostEnv.js';
|
|
21
|
+
/** 显式装配(非 Node 宿主唯一的入口;装了就**优先于** env,便于桌面/web 一页多引擎)。 */
|
|
22
|
+
let installed = null;
|
|
23
|
+
/** 装/卸引擎 wire 目标。传 null 卸回 env 派生。返回还原函数。 */
|
|
24
|
+
export function installEngineWireTarget(t) {
|
|
25
|
+
const prev = installed;
|
|
26
|
+
installed = t;
|
|
27
|
+
return () => {
|
|
28
|
+
installed = prev;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 当前引擎 wire 目标。null = 无 wire(离线/mock/浏览器未装)—— 调用方走各自的回落路径,
|
|
33
|
+
* **不是**错误态。
|
|
34
|
+
*/
|
|
35
|
+
export function engineWireTarget() {
|
|
36
|
+
if (installed !== null)
|
|
37
|
+
return installed;
|
|
38
|
+
const env = hostEnv();
|
|
39
|
+
const baseUrl = env.SEMA_LIVE_BASEURL;
|
|
40
|
+
if (!baseUrl)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
baseUrl,
|
|
44
|
+
// principal must match the run's owner (stamped by the live stream) — the owner-gated routes answer
|
|
45
|
+
// 404 for non-owners (no existence oracle), so a missing header reads as "run not found".
|
|
46
|
+
...(env.SEMA_LIVE_TOKEN ? { token: env.SEMA_LIVE_TOKEN } : {}),
|
|
47
|
+
principal: env.SEMA_LIVE_PRINCIPAL ?? 'anon:shell-live',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** 诊断开关(壳侧 `process.env.SEMA_DEBUG` 的等价读;本包零 process)。 */
|
|
51
|
+
export function engineWireDebugEnabled() {
|
|
52
|
+
return Boolean(hostEnv().SEMA_DEBUG);
|
|
53
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6 + §2.5 拆缝表):cli `src/sema/fleetClient.ts` 的
|
|
3
|
+
* **帧消费半场** —— `applyFrame` 的帧体 upsert + own/foreign 判别 + `recordOwnEngineRun` /
|
|
4
|
+
* `recordBgParentRun` / `recordBgTerminalFacts` 台账三写 + `retained` 终态留存池 + 帧间时钟
|
|
5
|
+
* 补偿 + 模块级 bg 读面(`readEngineActiveBgTasks` / `isFleetSessionScoped`)。
|
|
6
|
+
*
|
|
7
|
+
* 留在 TUI(设计稿 §2.4.2-B):`AgentClient` 构造与 consume/重连循环 · `main` 合成行 ·
|
|
8
|
+
* `selection` 游标 · `viewed` 的所有者(AppState)· `SEMA_DEBUG_FILE` 行字段落盘探针 ·
|
|
9
|
+
* hook_notice 的**写 store** 半场(判定在 notifications.ts,本文件只做分发)。
|
|
10
|
+
*
|
|
11
|
+
* ── B6 三处删债(每条都有直证,不是「看起来能删」)──────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **P1-4 / `makeSessionScopingFetch` 退役**:壳里为了给 fleet 流塞 `?session=` 注了一个自定义
|
|
14
|
+
* `fetch`(fleetClient.ts:201-227,注入点 :604)—— 归属过滤是最不该留在端上的东西。SDK 0.0.117
|
|
15
|
+
* `fleet.stream({ session })` / `fleet.snapshot({ session })` 已是一等参数(`resources/fleet.d.ts`
|
|
16
|
+
* 实读),本包出 `fleetStreamOptions()` 直接给出那个参数,**壳侧删掉整个 fetch 包装器**。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **G12 / `rowSessionTag` 整条删(比任务书说的更彻底,附直证)**:任务书写「ownerSessionId /
|
|
19
|
+
* rootSessionId 防御读删」。实测 server **1.291.0** `dist/http/server.js` 的 `streamFleet`:
|
|
20
|
+
* ```js
|
|
21
|
+
* const stripT = (r) => { const { scope: _s, sessionId: _sid, ...rest } = r; … }
|
|
22
|
+
* const stripW = (r) => { const { scope: _s, sessionId: _sid, ...rest } = r; … }
|
|
23
|
+
* ```
|
|
24
|
+
* ⇒ task/workflow **行**上 `scope` 与 `sessionId` **双双被剥**,而 `rootSessionId`/`ownerSessionId`
|
|
25
|
+
* 在 `FleetTaskRow`/`FleetWorkflowRow` 里**从来就不存在**(server 自己的 `fleet-bus.d.ts` 实读)。
|
|
26
|
+
* ⇒ `rowSessionTag(row)` 三个键**全部恒 undefined**,整条腿是死的 —— 不是「删两个键留一个」。
|
|
27
|
+
* 后果方向核过:这条腿在 `ownTaskRow` 里是**放行**位(命中即判 own),死 ⇒ 从不放行 ⇒ 删掉
|
|
28
|
+
* **零可观测差异**(其余两腿:祖链 own-run 台账 / `sessionScoped` 让位,原样保留)。
|
|
29
|
+
* ⚠️ 对照:`bg_notification` 帧上 server 只剥 `ownerScope`/`ownerSessionId`,**`rootSessionId`
|
|
30
|
+
* 与 `sessionId` 照发** ⇒ 通知臂的 `ownByRoot` 那条腿**是活的,保留**。同一个名字两条腿、
|
|
31
|
+
* 一死一活 —— 这正是「判据锚在决定结果的量上」要求逐条核而不是按名字一刀切的地方。
|
|
32
|
+
*
|
|
33
|
+
* 🔴 **G19 / 一次性快照**:`fleet.snapshot({session})` 已是 SDK 一等 verb(开流→取 snapshot 帧→
|
|
34
|
+
* 关流封装),壳的 `engineBgProbe.ts:8-9` 自述「引擎 fleet 面只有 SSE,没有一次性口」已过期。
|
|
35
|
+
* 本包出 `fleetSnapshotOptions()`;`engineBgProbe` 的手抄开流即断由壳侧换 verb(见交接报告)。
|
|
36
|
+
*
|
|
37
|
+
* ── 单实例纪律 ────────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
* `liveBgViews` 是 module 级 Set:写者 = 每个 `createFleetLedger()`,读者 = 引擎温切门
|
|
39
|
+
* (`readEngineActiveBgTasks`)与代际让位读面(`isFleetSessionScoped`)。两份实例 = 温切门恒读空
|
|
40
|
+
* ⇒ respawn 会静默杀掉在飞 bg 子代。
|
|
41
|
+
*/
|
|
42
|
+
import type { FleetFrame, FleetTaskRow } from '@sema-agent/sdk';
|
|
43
|
+
import { type FleetTaskView, type FleetWorkflowView, type ProjectTasksOptions } from './fleetProjection.js';
|
|
44
|
+
/** 终态行留存宽限窗(187 的 agents 页把完成会话留在列表;窗过即净,防长会话堆行)。 */
|
|
45
|
+
export declare const TERMINAL_RETAIN_MS = 60000;
|
|
46
|
+
export interface EngineActiveBgTask {
|
|
47
|
+
id: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
status: string;
|
|
50
|
+
/** fleet 行的 parentId(spawn 该子代的 run 行 id;顶层 run 行缺席)—— killed 残局合成通知的
|
|
51
|
+
* own/foreign 判别用。 */
|
|
52
|
+
parentId?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface EngineActiveBgReading {
|
|
55
|
+
tasks: EngineActiveBgTask[];
|
|
56
|
+
/** true = 至少一条 fleet 流当前连着(读数新鲜);false = 断线期 last-known(可能 stale)或无 live 源。 */
|
|
57
|
+
fresh: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* server 代际证据读面:任一 live fleet 源的 meta 报过 `sessionScoped=true`。
|
|
61
|
+
* true ⇒ 到达本连接的**行帧**已经过 server 会话过滤,端侧行级 own 判别可以让位。
|
|
62
|
+
* 🔴 只覆盖【行帧】:通知帧的 own-run 台账判别不看本标,恒开(1.244 对缺 ownerSessionId 的通知
|
|
63
|
+
* 仍广播,fail-closed 是 1.245+)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isFleetSessionScoped(): boolean;
|
|
66
|
+
/** 引擎侧在飞 bg 子代读数(union 所有未 dispose 的源,按 row.id 去重)。无 live 源 ⇒ `{[], false}`。 */
|
|
67
|
+
export declare function readEngineActiveBgTasks(): EngineActiveBgReading;
|
|
68
|
+
/**
|
|
69
|
+
* 清空所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号一到就调。
|
|
70
|
+
* R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一会话的
|
|
71
|
+
* 终态行(名字 = objective 派生)就是跨会话残影。
|
|
72
|
+
*/
|
|
73
|
+
export declare function clearAllRetainedFleetRows(): void;
|
|
74
|
+
/** 测试钩:摘掉所有登记的视图(同进程多组断言之间互不污染)。 */
|
|
75
|
+
export declare function __resetFleetLedgerRegistryForTests(): void;
|
|
76
|
+
/**
|
|
77
|
+
* `client.fleet.stream(...)` 的会话过滤参数。缺席(端未装 SessionPort / 还没有会话 id)⇒ `{}`
|
|
78
|
+
* = principal 级视图,**与 1.37 之前逐字同行为**(不是错误态)。
|
|
79
|
+
*/
|
|
80
|
+
export declare function fleetStreamOptions(): {
|
|
81
|
+
session?: string;
|
|
82
|
+
};
|
|
83
|
+
/** G19:`client.fleet.snapshot(...)` 的参数(同上;首屏/温切探针用)。 */
|
|
84
|
+
export declare function fleetSnapshotOptions(opts?: {
|
|
85
|
+
timeoutMs?: number;
|
|
86
|
+
}): {
|
|
87
|
+
session?: string;
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
};
|
|
90
|
+
export interface FleetLedgerHooks {
|
|
91
|
+
/**
|
|
92
|
+
* hook_notice 帧的**写 store** 半场(判定在 `classifyHookNoticeFrame`,本台账只做分发)。
|
|
93
|
+
* 返回值忽略;不给 = 该帧被静默丢弃(端没有承载面时的正确行为)。
|
|
94
|
+
*/
|
|
95
|
+
onHookNotice?(frame: unknown, sessionScoped: boolean): void;
|
|
96
|
+
}
|
|
97
|
+
export interface FleetLedgerStatus {
|
|
98
|
+
connected: boolean;
|
|
99
|
+
version: number | null;
|
|
100
|
+
scoped: boolean | null;
|
|
101
|
+
sessionScoped: boolean | undefined;
|
|
102
|
+
bgNotifyFailClosed: boolean;
|
|
103
|
+
taskCount: number;
|
|
104
|
+
workflowCount: number;
|
|
105
|
+
}
|
|
106
|
+
export interface FleetLedger {
|
|
107
|
+
/** 消费一帧(consumer-side active-set update-in-place / remove)。 */
|
|
108
|
+
applyFrame(frame: FleetFrame): void;
|
|
109
|
+
/** 连接态由宿主的 consume 循环告知(台账自己不开流)。 */
|
|
110
|
+
setConnected(v: boolean): void;
|
|
111
|
+
/**
|
|
112
|
+
* 把当前 Map 投影成渲染行。`nowMs` 驱动帧间时钟补偿与留存池过期。
|
|
113
|
+
* `rowFilter` 必填(端专属决策,见 fleetProjection 头注)。
|
|
114
|
+
*/
|
|
115
|
+
project(nowMs: number, opts: ProjectTasksOptions & {
|
|
116
|
+
/** 正在被查看的行不过期(用户读报告中途被抽走=清障变添堵)。 */
|
|
117
|
+
viewingTaskId?: string | undefined;
|
|
118
|
+
}): {
|
|
119
|
+
tasks: FleetTaskView[];
|
|
120
|
+
workflows: FleetWorkflowView[];
|
|
121
|
+
};
|
|
122
|
+
status(): FleetLedgerStatus;
|
|
123
|
+
/** 摘掉模块级读面登记(宿主 dispose 时必调,否则温切门读到僵尸源)。 */
|
|
124
|
+
dispose(): void;
|
|
125
|
+
/** 测试直取:当前 task 行原样(顺序 = 插入序)。 */
|
|
126
|
+
__rowsForTest(): FleetTaskRow[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
|
|
130
|
+
* (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
|
|
131
|
+
*/
|
|
132
|
+
export declare function createFleetLedger(hooks?: FleetLedgerHooks): FleetLedger;
|