@sema-agent/client-core 0.11.15 → 0.11.17
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 +2 -1
- package/dist/adapt.js +19 -0
- package/dist/agentSession/backgroundView.d.ts +76 -0
- package/dist/agentSession/backgroundView.js +254 -0
- package/dist/agentSession/contract.d.ts +152 -0
- package/dist/agentSession/contract.js +13 -0
- package/dist/controlRouter.d.ts +8 -0
- package/dist/controlRouter.js +4 -2
- package/dist/engineAgentPanelStore.d.ts +6 -0
- package/dist/engineWireTarget.d.ts +7 -0
- package/dist/engineWireTarget.js +19 -5
- package/dist/fleetAgentPanelProjection.d.ts +3 -0
- package/dist/fleetAgentPanelProjection.js +8 -1
- package/dist/hitl/askGateWire.d.ts +6 -0
- package/dist/hitl/askGateWire.js +32 -15
- package/dist/hitl/toolApprovalWire.d.ts +19 -0
- package/dist/hitl/toolApprovalWire.js +48 -12
- package/dist/host.d.ts +16 -0
- package/dist/host.js +68 -31
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/liveQuestionStore.d.ts +7 -5
- package/dist/liveQuestionStore.js +23 -42
- package/dist/notifications.d.ts +3 -1
- package/dist/notifications.js +21 -2
- package/dist/request/taskRequest.d.ts +2 -0
- package/dist/request/taskRequest.js +5 -0
- package/dist/sessionSlot.d.ts +26 -0
- package/dist/sessionSlot.js +31 -0
- package/dist/toolResult.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
23
23
|
|
|
24
24
|
## Scope
|
|
25
25
|
|
|
26
|
-
**Version:** 0.11.
|
|
26
|
+
**Version:** 0.11.17
|
|
27
27
|
|
|
28
28
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
29
29
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -214,6 +214,7 @@ itself (FAILED names + skipped names + arithmetic reconciliation). All-SKIP repo
|
|
|
214
214
|
| `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
|
|
215
215
|
| `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
|
|
216
216
|
| `scripts/run-streamjson-timing-honesty-test.mjs` | Stream timing & terminal honesty ([2084]): held errored fs-write results release on model progress; wall-clock timeout maps to `error_during_execution` with a truthful salvage note; the synthetic API-error assistant row carries the `<synthetic>` in-message sentinel |
|
|
217
|
+
| `scripts/run-background-view-test.mjs` | `createBackgroundView` lifecycle: polling/notify pairing, per-source degrade (`501 → not-configured` vs `unavailable`), the capabilities `scheduler` probe, and dispose really aborting the in-flight fleet snapshot (pure projection lives in the pure suite's W-A segment) |
|
|
217
218
|
| `scripts/run-public-surface-test.mjs` | The outward promises: the npm export surface baseline, the peer floor witness, and this README's claims |
|
|
218
219
|
|
|
219
220
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
package/dist/adapt.js
CHANGED
|
@@ -705,6 +705,11 @@ class WireToCcAdapterImpl {
|
|
|
705
705
|
// 代价是失去 for-await 的隐式 cleanup ⇒ finally 里手动 return()(T58,少了它 = SSE 不拆)。
|
|
706
706
|
const iterator = frames[Symbol.asyncIterator]();
|
|
707
707
|
let iteratorDone = false;
|
|
708
|
+
// R1(clay 五报 07-31):retry 覆盖层的四个清除点全在 turn 边界,缺「重试成功→清」的转移 ——
|
|
709
|
+
// 引擎重试成功那一刻什么都不发(BrainStatusPhase 无恢复终态,候 core [2163]),所以恢复只能由
|
|
710
|
+
// 「主 lane 又有内容在流」这个事实来证明。挂旗后首个主 lane 内容帧即清,CC 同位 = 重试循环
|
|
711
|
+
// 退出即 onRetryStatus(null)(pretty220 finally 清)。
|
|
712
|
+
let retryOverlayActive = false;
|
|
708
713
|
try {
|
|
709
714
|
for (;;) {
|
|
710
715
|
const nextPromise = iterator.next();
|
|
@@ -755,6 +760,19 @@ class WireToCcAdapterImpl {
|
|
|
755
760
|
const m = rawFrame;
|
|
756
761
|
if (m === null || typeof m !== 'object')
|
|
757
762
|
continue;
|
|
763
|
+
if (retryOverlayActive &&
|
|
764
|
+
(m.type === 'stream_event' ||
|
|
765
|
+
m.type === 'assistant' ||
|
|
766
|
+
m.type === 'tool_end_result' ||
|
|
767
|
+
m.type === 'turn_usage') &&
|
|
768
|
+
typeof m.parentToolCallId !== 'string' &&
|
|
769
|
+
// stream_event 的子流标记在 event 里(:1230 thinking 臂同款判位),帧顶层没有。
|
|
770
|
+
typeof m.event?.parentToolCallId !== 'string') {
|
|
771
|
+
// 恢复即清:主 lane 内容帧到达 = 引擎那次重试已成功;子流帧(parentToolCallId)不算证据
|
|
772
|
+
// (主 brain 仍可能在退避等待,#47 矩阵 #5 同族判别)。
|
|
773
|
+
retryOverlayActive = false;
|
|
774
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
775
|
+
}
|
|
758
776
|
if (m.type === 'stream_event' && !messageStartEmitted) {
|
|
759
777
|
messageStartEmitted = true;
|
|
760
778
|
yield chrome({
|
|
@@ -1362,6 +1380,7 @@ class WireToCcAdapterImpl {
|
|
|
1362
1380
|
...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
|
|
1363
1381
|
}, ctx.now()),
|
|
1364
1382
|
});
|
|
1383
|
+
retryOverlayActive = true;
|
|
1365
1384
|
break;
|
|
1366
1385
|
}
|
|
1367
1386
|
default:
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* backgroundView — design/161 平行入口 `createBackgroundView(client)` 的实现(实现批 A,2026-07-30)。
|
|
3
|
+
*
|
|
4
|
+
* 契约锚 = ./contract.ts(类型已冻结,clay 二裁/终裁):`assistant.tasks` 与 `fleet` 都是**会话外
|
|
5
|
+
* 读面**(assistant 跨 session;fleet 是 idle 壳唯一保持订阅的常驻 SSE),生命周期不绑任何会话的
|
|
6
|
+
* dispose(),所以归一投影在 createAgentSession 之外单开一门。
|
|
7
|
+
*
|
|
8
|
+
* ── 两源两张映射表(第三套中性 7 值词表,不用任一方词表)────────────────────────────────────
|
|
9
|
+
* · assistant(SDK types.d.ts AssistantTask.status,3 值闭集):活跃调度行词表 —— 无终态臂。
|
|
10
|
+
* · fleet(SDK resources/fleet.d.ts FleetTaskStatus,9 值 wire 闭集 / 读侧开)。
|
|
11
|
+
* 两张表各自显式穷举 + `never` 卫兵:上游加词当天**编译红**,不静默落兜底;wire 真发了未知词
|
|
12
|
+
* (更新的 server × 旧 SDK)时走注释里的 open-set 回落 —— 与 workflowClient coerce 族同姿势。
|
|
13
|
+
*
|
|
14
|
+
* ── 拉取姿势(照 workflowClient 0.11.15 的订阅纪律)─────────────────────────────────────────
|
|
15
|
+
* · 轮询(可配 intervalMs 缺省 2000)+ **解析即通知**(首帧不等宿主 tick)+ **订阅即对账**
|
|
16
|
+
* (已解析后订阅同步补一发 —— 配对机制必须共享前提);订阅者抛异常吞;dispose abort 在飞
|
|
17
|
+
* 请求(含 fleet.snapshot 的 SSE 短连)并停循环。
|
|
18
|
+
* · 两源**独立拉取独立降级**:一源挂另一源照常;本源绝不 throw 进宿主。
|
|
19
|
+
* · fleet 快照走 SDK `fleet.snapshot()`(G19 一次性快照 verb:开流取首 snapshot 帧即断 ——
|
|
20
|
+
* fleet 面无独立 REST 快照端点,这是契约形不是绕路)。
|
|
21
|
+
*
|
|
22
|
+
* ── 'not-configured' 判别(诚实缺席纪律)────────────────────────────────────────────────────
|
|
23
|
+
* · fleet:面 gated off 时路由在任何帧之前答 **501**(typed)—— 501 ⇒ 'not-configured',
|
|
24
|
+
* 其它错 ⇒ 'unavailable'(拒绝形分档;错误判型走 controlRouter.errCodes 单源)。
|
|
25
|
+
* · assistant:无 TiDB run store 时 worker 回 `{tasks:[]}` **原样透传** —— wire 上与真空
|
|
26
|
+
* 不可区分。能探则探:`GET /v1/capabilities` 的 `scheduler` 位(server `schedulerEnabled &&
|
|
27
|
+
* requirePrincipal!==true && remoteExec==="host"` 三与)为 false/缺席 ⇒ 'not-configured';
|
|
28
|
+
* caps 面自己不可达 ⇒ 判别缺席,**如实回 'ok'**(绝不编造判别)。🔴 残余缺口候引擎位:
|
|
29
|
+
* scheduler:true 而 run store 未配的 `{tasks:[]}` 仍与真空同形 —— 需要 wire 长出判别位
|
|
30
|
+
* (caps 或响应信封)才能收口,本实现不猜。
|
|
31
|
+
*
|
|
32
|
+
* 字段纪律:缺席=键不在场(undefined),🔴 绝不 `?? 0` / `?? false`;ISO→epoch ms 只在
|
|
33
|
+
* assistant 行边界转一次;fleet 复合 id("<runId> <taskId>")整串不拆;fleet 子行(parentId
|
|
34
|
+
* 在场)不进 rows(子代不是用户的独立后台任务)。
|
|
35
|
+
*/
|
|
36
|
+
import type { AgentClient, AssistantTask, FleetTaskRow, FleetTaskStatus } from '@sema-agent/sdk';
|
|
37
|
+
import type { BackgroundRow, BackgroundSourceHealth, BackgroundStatus, BackgroundView, CreateBackgroundView } from './contract.js';
|
|
38
|
+
/** assistant 3 值 → 中性 7 值。suspended(durable HITL park)与 needs_review(plan_review/dry-run
|
|
39
|
+
* park)都是「引擎停着等人」⇒ 同落 'waiting-on-human'(区分度由行上 gate.kind 携带,不铸新词)。 */
|
|
40
|
+
export declare function backgroundStatusFromAssistant(s: AssistantTask['status']): BackgroundStatus;
|
|
41
|
+
/** fleet 9 值 → 中性 7 值。'waiting'(durable suspend)与 'awaiting approval'(needs-review /
|
|
42
|
+
* plan-approval park)都在等人;'idle' 是「活着但此刻没在推进」—— 中性词表无此臂,按活跃行渲
|
|
43
|
+
* 'running'(诚实取舍:它既不 queued 也不终态,更不是在等人)。 */
|
|
44
|
+
export declare function backgroundStatusFromFleet(s: FleetTaskStatus): BackgroundStatus;
|
|
45
|
+
/** 一行 AssistantTask → BackgroundRow。gate 是 `AssistantGate | null`(wire 显式 null 形)——
|
|
46
|
+
* null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。 */
|
|
47
|
+
export declare function assistantTaskToBackgroundRow(t: AssistantTask): BackgroundRow;
|
|
48
|
+
/** 一行 FleetTaskRow → BackgroundRow;子行(parentId 在场)⇒ null(调用方剔除)。
|
|
49
|
+
* 复合 id 不拆;`currentTool` 契约位是一行字符串 —— 优先 server 已 redact 的 `currentAction`
|
|
50
|
+
* 原文,缺席时从结构化 `currentTool` 合成;`needsAttention` fleet wire 无显式位,从 park 态
|
|
51
|
+
* 推导(awaiting approval / awaitingPlanApproval / waiting),非 park 行键缺席(缺席≠false)。 */
|
|
52
|
+
export declare function fleetTaskToBackgroundRow(r: FleetTaskRow): BackgroundRow | null;
|
|
53
|
+
export interface BackgroundViewInputs {
|
|
54
|
+
readonly assistant: {
|
|
55
|
+
readonly health: BackgroundSourceHealth;
|
|
56
|
+
readonly tasks: readonly AssistantTask[];
|
|
57
|
+
};
|
|
58
|
+
readonly fleet: {
|
|
59
|
+
readonly health: BackgroundSourceHealth;
|
|
60
|
+
readonly tasks: readonly FleetTaskRow[];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** 纯组合:两源(各带三态健康位)→ BackgroundView。行序 = assistant 先(core 已按 triage 排,
|
|
64
|
+
* 消费方不再排)、fleet 后(帧到达序);非 'ok' 源的行不出(那份列表不是证词);sources 三态
|
|
65
|
+
* 恒在场 —— 源不可用/未配置与「空列表」是三件事,端必须渲它。 */
|
|
66
|
+
export declare function projectBackgroundView(i: BackgroundViewInputs): BackgroundView;
|
|
67
|
+
export interface BackgroundViewOptions {
|
|
68
|
+
/** 轮询间隔(缺省 2000ms)。 */
|
|
69
|
+
readonly intervalMs?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* createBackgroundView(client, opts?) — 后台任务归一视图(契约 `CreateBackgroundView`;第二参是
|
|
73
|
+
* 实现旋钮,可选参不缩窄契约形)。生命周期:构造即起后台轮询;`snapshot()` 首解析前 null;
|
|
74
|
+
* `subscribe` 解析即通知 + 订阅即对账;`dispose()` abort 在飞请求、停循环、清订阅。
|
|
75
|
+
*/
|
|
76
|
+
export declare function createBackgroundView(client: AgentClient, opts?: BackgroundViewOptions): ReturnType<CreateBackgroundView>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { errCodes } from '../controlRouter.js';
|
|
2
|
+
import { engineWireDebugEnabled } from '../engineWireTarget.js';
|
|
3
|
+
import { hostLog } from '../host.js';
|
|
4
|
+
// ── status 映射表 ×2(显式穷举 + never 卫兵)──────────────────────────────────────────────────
|
|
5
|
+
/** assistant 3 值 → 中性 7 值。suspended(durable HITL park)与 needs_review(plan_review/dry-run
|
|
6
|
+
* park)都是「引擎停着等人」⇒ 同落 'waiting-on-human'(区分度由行上 gate.kind 携带,不铸新词)。 */
|
|
7
|
+
export function backgroundStatusFromAssistant(s) {
|
|
8
|
+
switch (s) {
|
|
9
|
+
case 'running':
|
|
10
|
+
return 'running';
|
|
11
|
+
case 'suspended':
|
|
12
|
+
return 'waiting-on-human';
|
|
13
|
+
case 'needs_review':
|
|
14
|
+
return 'waiting-on-human';
|
|
15
|
+
default: {
|
|
16
|
+
const exhaustive = s; // 上游加词 ⇒ 这里编译红(闭集穷举卫兵)
|
|
17
|
+
void exhaustive;
|
|
18
|
+
// wire 开集回落:assistant.tasks 只列活跃调度行,未知新词按「活着」渲(同 workflowClient coerce 族)。
|
|
19
|
+
return 'running';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** fleet 9 值 → 中性 7 值。'waiting'(durable suspend)与 'awaiting approval'(needs-review /
|
|
24
|
+
* plan-approval park)都在等人;'idle' 是「活着但此刻没在推进」—— 中性词表无此臂,按活跃行渲
|
|
25
|
+
* 'running'(诚实取舍:它既不 queued 也不终态,更不是在等人)。 */
|
|
26
|
+
export function backgroundStatusFromFleet(s) {
|
|
27
|
+
switch (s) {
|
|
28
|
+
case 'queued':
|
|
29
|
+
return 'queued';
|
|
30
|
+
case 'running':
|
|
31
|
+
return 'running';
|
|
32
|
+
case 'waiting':
|
|
33
|
+
return 'waiting-on-human';
|
|
34
|
+
case 'stopping':
|
|
35
|
+
return 'stopping';
|
|
36
|
+
case 'awaiting approval':
|
|
37
|
+
return 'waiting-on-human';
|
|
38
|
+
case 'idle':
|
|
39
|
+
return 'running';
|
|
40
|
+
case 'completed':
|
|
41
|
+
return 'done';
|
|
42
|
+
case 'failed':
|
|
43
|
+
return 'failed';
|
|
44
|
+
case 'killed':
|
|
45
|
+
return 'stopped';
|
|
46
|
+
default: {
|
|
47
|
+
const exhaustive = s; // 上游加词 ⇒ 编译红;wire 未知词(读侧开集)⇒ 按活跃行回落
|
|
48
|
+
void exhaustive;
|
|
49
|
+
return 'running';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ── 行投影 ×2 ────────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
/** ISO 串 → epoch ms,只在这一处转;烂串 ⇒ undefined(诚实缺席,绝不 NaN/0 外泄)。 */
|
|
55
|
+
function isoToEpochMs(iso) {
|
|
56
|
+
const ms = Date.parse(iso);
|
|
57
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** 一行 AssistantTask → BackgroundRow。gate 是 `AssistantGate | null`(wire 显式 null 形)——
|
|
60
|
+
* null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。 */
|
|
61
|
+
export function assistantTaskToBackgroundRow(t) {
|
|
62
|
+
const updatedAtMs = isoToEpochMs(t.updatedAt);
|
|
63
|
+
return {
|
|
64
|
+
key: { source: 'assistant', taskId: t.taskId },
|
|
65
|
+
status: backgroundStatusFromAssistant(t.status),
|
|
66
|
+
...(updatedAtMs !== undefined ? { updatedAtMs } : {}),
|
|
67
|
+
needsAttention: t.needsAttention,
|
|
68
|
+
...(t.gate !== null ? { gate: t.gate } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** 一行 FleetTaskRow → BackgroundRow;子行(parentId 在场)⇒ null(调用方剔除)。
|
|
72
|
+
* 复合 id 不拆;`currentTool` 契约位是一行字符串 —— 优先 server 已 redact 的 `currentAction`
|
|
73
|
+
* 原文,缺席时从结构化 `currentTool` 合成;`needsAttention` fleet wire 无显式位,从 park 态
|
|
74
|
+
* 推导(awaiting approval / awaitingPlanApproval / waiting),非 park 行键缺席(缺席≠false)。 */
|
|
75
|
+
export function fleetTaskToBackgroundRow(r) {
|
|
76
|
+
if (typeof r.parentId === 'string' && r.parentId !== '')
|
|
77
|
+
return null;
|
|
78
|
+
const currentTool = typeof r.currentAction === 'string' && r.currentAction !== ''
|
|
79
|
+
? r.currentAction
|
|
80
|
+
: r.currentTool !== undefined
|
|
81
|
+
? `${r.currentTool.toolName}${r.currentTool.target !== undefined ? ` ${r.currentTool.target}` : ''}`
|
|
82
|
+
: undefined;
|
|
83
|
+
const parked = r.status === 'awaiting approval' || r.status === 'waiting' || r.awaitingPlanApproval === true;
|
|
84
|
+
return {
|
|
85
|
+
key: { source: 'fleet', rowId: r.id },
|
|
86
|
+
status: backgroundStatusFromFleet(r.status),
|
|
87
|
+
...(parked ? { needsAttention: true } : {}),
|
|
88
|
+
...(typeof r.name === 'string' && r.name !== '' ? { name: r.name } : {}),
|
|
89
|
+
...(typeof r.description === 'string' && r.description !== '' ? { description: r.description } : {}),
|
|
90
|
+
...(typeof r.tokens === 'number' ? { tokens: r.tokens } : {}),
|
|
91
|
+
...(typeof r.toolUses === 'number' ? { toolUses: r.toolUses } : {}),
|
|
92
|
+
...(currentTool !== undefined ? { currentTool } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** 纯组合:两源(各带三态健康位)→ BackgroundView。行序 = assistant 先(core 已按 triage 排,
|
|
96
|
+
* 消费方不再排)、fleet 后(帧到达序);非 'ok' 源的行不出(那份列表不是证词);sources 三态
|
|
97
|
+
* 恒在场 —— 源不可用/未配置与「空列表」是三件事,端必须渲它。 */
|
|
98
|
+
export function projectBackgroundView(i) {
|
|
99
|
+
const rows = [];
|
|
100
|
+
if (i.assistant.health === 'ok') {
|
|
101
|
+
for (const t of i.assistant.tasks)
|
|
102
|
+
rows.push(assistantTaskToBackgroundRow(t));
|
|
103
|
+
}
|
|
104
|
+
if (i.fleet.health === 'ok') {
|
|
105
|
+
for (const r of i.fleet.tasks) {
|
|
106
|
+
const row = fleetTaskToBackgroundRow(r);
|
|
107
|
+
if (row !== null)
|
|
108
|
+
rows.push(row);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
sources: { assistant: i.assistant.health, fleet: i.fleet.health },
|
|
113
|
+
rows,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 501 判读走 errCodes 单源(SDK typed APIError 的 status;`statusCode` 变体同吃)。 */
|
|
117
|
+
function isNotImplemented(err) {
|
|
118
|
+
const { status } = errCodes(err);
|
|
119
|
+
return status === 501;
|
|
120
|
+
}
|
|
121
|
+
function debugLog(msg) {
|
|
122
|
+
if (engineWireDebugEnabled())
|
|
123
|
+
hostLog('debug', `[client-core][backgroundView] ${msg}`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* createBackgroundView(client, opts?) — 后台任务归一视图(契约 `CreateBackgroundView`;第二参是
|
|
127
|
+
* 实现旋钮,可选参不缩窄契约形)。生命周期:构造即起后台轮询;`snapshot()` 首解析前 null;
|
|
128
|
+
* `subscribe` 解析即通知 + 订阅即对账;`dispose()` abort 在飞请求、停循环、清订阅。
|
|
129
|
+
*/
|
|
130
|
+
export function createBackgroundView(client, opts) {
|
|
131
|
+
const intervalMs = opts?.intervalMs ?? 2000;
|
|
132
|
+
let disposed = false;
|
|
133
|
+
let current = null;
|
|
134
|
+
let lastSerialized = null;
|
|
135
|
+
/** caps 探针结果:null = 还没探到/caps 面不可达(判别缺席);boolean = scheduler 位。 */
|
|
136
|
+
let capsScheduler = null;
|
|
137
|
+
const listeners = new Set();
|
|
138
|
+
const abort = new AbortController();
|
|
139
|
+
function notify() {
|
|
140
|
+
if (disposed)
|
|
141
|
+
return;
|
|
142
|
+
for (const l of [...listeners]) {
|
|
143
|
+
try {
|
|
144
|
+
l();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
debugLog(`subscriber threw (ignored): ${String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** caps 只需探到一次;失败保持 null 下轮重试(caps 面挂 ≠ assistant 面挂,判别缺席不编造)。 */
|
|
152
|
+
async function probeCaps() {
|
|
153
|
+
if (capsScheduler !== null)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
const caps = await client.capabilities({ signal: abort.signal });
|
|
157
|
+
capsScheduler = caps.scheduler === true;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
debugLog(`caps probe unavailable (scheduler 判别缺席,保持 ok 口径): ${String(err)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function pullAssistant() {
|
|
164
|
+
try {
|
|
165
|
+
const { tasks } = await client.assistant.tasks({ signal: abort.signal });
|
|
166
|
+
return { health: capsScheduler === false ? 'not-configured' : 'ok', tasks: tasks ?? [] };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
debugLog(`assistant.tasks degrade: ${String(err)}`);
|
|
170
|
+
const health = capsScheduler === false || isNotImplemented(err) ? 'not-configured' : 'unavailable';
|
|
171
|
+
return { health, tasks: [] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function pullFleet() {
|
|
175
|
+
try {
|
|
176
|
+
// G19 一次性快照 verb:SDK 内部开 SSE 流取首 snapshot 帧即断;abort.signal 直通 ⇒ dispose 能掐在飞连接。
|
|
177
|
+
const { tasks } = await client.fleet.snapshot({ signal: abort.signal });
|
|
178
|
+
return { health: 'ok', tasks: tasks ?? [] };
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
debugLog(`fleet.snapshot degrade: ${String(err)}`);
|
|
182
|
+
return { health: isNotImplemented(err) ? 'not-configured' : 'unavailable', tasks: [] };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function pollOnce() {
|
|
186
|
+
await probeCaps();
|
|
187
|
+
// 两源并行、各自 try/catch —— 独立拉取独立降级,一源挂另一源照常。
|
|
188
|
+
const [assistant, fleet] = await Promise.all([pullAssistant(), pullFleet()]);
|
|
189
|
+
if (disposed || abort.signal.aborted)
|
|
190
|
+
return;
|
|
191
|
+
current = projectBackgroundView({ assistant, fleet });
|
|
192
|
+
const serialized = JSON.stringify(current);
|
|
193
|
+
if (serialized !== lastSerialized) {
|
|
194
|
+
lastSerialized = serialized;
|
|
195
|
+
notify();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** abort 感知的 sleep:dispose 立即唤醒并清定时器(不留悬挂 timer 拖住宿主进程)。 */
|
|
199
|
+
function sleep(ms) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const timer = setTimeout(() => {
|
|
202
|
+
abort.signal.removeEventListener('abort', onAbort);
|
|
203
|
+
resolve();
|
|
204
|
+
}, ms);
|
|
205
|
+
const onAbort = () => {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
abort.signal.addEventListener('abort', onAbort, { once: true });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function loop() {
|
|
213
|
+
while (!disposed && !abort.signal.aborted) {
|
|
214
|
+
try {
|
|
215
|
+
await pollOnce();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// pullAssistant/pullFleet 各自兜底,这里只剩投影/通知的意外 —— 记录不外抛(本源绝不 throw 进宿主)。
|
|
219
|
+
debugLog(`poll cycle error (kept alive): ${String(err)}`);
|
|
220
|
+
}
|
|
221
|
+
if (disposed || abort.signal.aborted)
|
|
222
|
+
return;
|
|
223
|
+
await sleep(intervalMs);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
void loop();
|
|
227
|
+
return {
|
|
228
|
+
snapshot() {
|
|
229
|
+
return current;
|
|
230
|
+
},
|
|
231
|
+
subscribe(onChange) {
|
|
232
|
+
listeners.add(onChange);
|
|
233
|
+
// 订阅即对账:订阅前已解析的话,notify 那一发已经打空 —— 同步补一发,宿主不用白等一个 tick。
|
|
234
|
+
if (current !== null && !disposed) {
|
|
235
|
+
try {
|
|
236
|
+
onChange();
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
debugLog(`subscriber threw on reconcile (ignored): ${String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return () => {
|
|
243
|
+
listeners.delete(onChange);
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
dispose() {
|
|
247
|
+
disposed = true;
|
|
248
|
+
abort.abort();
|
|
249
|
+
listeners.clear();
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// 契约锚(编译期钉):实现必须可赋给冻结契约形 —— 参数缩窄/返回面缺员在这里红,不等运行期。
|
|
254
|
+
void createBackgroundView;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/161 — client-core 顶层单入口:**契约收敛物**(纯类型,零运行时;实现分批跟进)。
|
|
3
|
+
*
|
|
4
|
+
* 这是立项的唯一权威签名源(clay 三裁 2026-07-30 + 对抗复审 r1 收敛;两份文档签名互不相容的
|
|
5
|
+
* 教训 = 收敛物必须是 tsc 管得住的代码,见 sema-internal design-161-review-r1.md §〇)。
|
|
6
|
+
* 实现工单以本文件为锚:W1 per-session 化 → 入口实现 → W5 冷接 → 三端迁移(desktop → sema-web
|
|
7
|
+
* 聊天区 → web-client)。**本文件不进 index.ts barrel,实现落地一件出口一件。**
|
|
8
|
+
*
|
|
9
|
+
* ══ 三个入口,不是一个 ═══════════════════════════════════════════════════════════════════
|
|
10
|
+
* 1. `createAgentSession(config)` —— per-session 会话流面(AgentEvent → SDKMessage + HITL 桥 +
|
|
11
|
+
* 终态投影)。wire 只有一种:SDK 的 `AgentClientConfig` 判别联合(client/relay 本就是它的两臂,
|
|
12
|
+
* r1 E3 —— 在这里再判一次就是复制 SDK)。
|
|
13
|
+
* 2. `attachSeatSession(seatApi)` —— 层 2 远程代理:座位(Electron renderer / web seat lane)侧
|
|
14
|
+
* **没有 AgentEvent 流**(收到的已是 main 进程 adapt 后的 SDKMessage,seatContract:49-56),
|
|
15
|
+
* 所以它不是第三个 wire 臂,是层 1 返回面的**同型转发**(内部零 wire、零 adapt)。
|
|
16
|
+
* 3. `createBackgroundView(client)` —— 平行入口(clay 二裁 2026-07-30):`assistant.tasks` 与
|
|
17
|
+
* `fleet` 都是**会话外读面**(assistant 跨 session;fleet 是 idle 壳唯一保持订阅的常驻 SSE),
|
|
18
|
+
* 生命周期不该绑在某个会话的 dispose() 上,所以归一投影不进 createAgentSession。
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentClient, AgentClientConfig } from '@sema-agent/sdk';
|
|
21
|
+
/**
|
|
22
|
+
* 已构造 client 的接入臂 **必须同时带构造参数** —— `AgentClient.config` 是 private
|
|
23
|
+
* (sdk client.d.ts:124),包拿不回 baseUrl/authToken/principal,而入口要用它们做四件事:
|
|
24
|
+
* caps 探针 kick ×2、task-agents wire 预备、durable 读面的 client 另建(planReview 同款)。
|
|
25
|
+
* 不带这三样的「裸 client」臂在 r1 E3-2 已证伪。
|
|
26
|
+
*/
|
|
27
|
+
export interface ProvidedClientWire {
|
|
28
|
+
readonly client: AgentClient;
|
|
29
|
+
readonly baseUrl: string;
|
|
30
|
+
readonly authToken: AgentClientConfig['authToken'];
|
|
31
|
+
readonly principal?: string;
|
|
32
|
+
}
|
|
33
|
+
/** wire 入参:给配置则入口自建 client;给 ProvidedClientWire 则复用宿主的(参数齐备)。 */
|
|
34
|
+
export type AgentSessionWire = AgentClientConfig | ProvidedClientWire;
|
|
35
|
+
export interface AgentSessionHostSurface {
|
|
36
|
+
/** 转录消费口(SDKMessage 流的宿主端)。缺席 ⇒ 入口拒绝启动(没有它整个入口无意义)。 */
|
|
37
|
+
onTranscript(msg: unknown): void;
|
|
38
|
+
/**
|
|
39
|
+
* chrome 装饰臂(seam.ts CHROME_ARMS 表)。缺席的 required 臂 = 功能静默不出现
|
|
40
|
+
* (desktop 今天缺 7/8,r1 E8c)——所以本口的自检是**表驱动**的:实现必须提供
|
|
41
|
+
* `chromeArmMisses()`,对 `required:true` 且未被消费的臂逐臂计数,门断言恒 0。
|
|
42
|
+
*/
|
|
43
|
+
onChrome?(event: unknown): void;
|
|
44
|
+
/**
|
|
45
|
+
* 工具审批卡口。两条腿:
|
|
46
|
+
* sync-frame 腿:缺席 ⇒ 主动 respond deny(现行为,保留);
|
|
47
|
+
* durable-park 腿:缺席 ⇒ 🔴 **主动 decide deny + 一条诚实告知行**(行为变更,替换今天的
|
|
48
|
+
* fail-stuck;candidates 见下方 OPEN-1 —— headless「留 park 待接手」场景是否豁免候 clay)。
|
|
49
|
+
*/
|
|
50
|
+
approvalCard?: unknown;
|
|
51
|
+
/** AskUserQuestion overlay 口。durable 腿缺席后果同上(deny 语义 = respond 取消形)。 */
|
|
52
|
+
askQuestion?: unknown;
|
|
53
|
+
/**
|
|
54
|
+
* plan review 口。缺席 ⇒ 今天是「卡永不出现且静默」(planReviewWire.ts:77 fail-soft)——
|
|
55
|
+
* 改为:至少产出一条可见终态行(引擎在等,人必须知道)。卡的**皮肤归端**(desktop 的
|
|
56
|
+
* ExitPlanMode 权限卡形 vs cli 的 overlay 形都合法,包只出数据与决断口;OPEN-2)。
|
|
57
|
+
*/
|
|
58
|
+
planReview?: unknown;
|
|
59
|
+
/** 通知队列口(缺席=计 miss,静默可接受 —— 引擎不等)。 */
|
|
60
|
+
notify?: unknown;
|
|
61
|
+
/** 后台任务转发口(会话自己流上的 task_notification / subagent_lifecycle;跨会话视图归 createBackgroundView)。 */
|
|
62
|
+
onBackgroundTask?(event: unknown): void;
|
|
63
|
+
/** 身份采集口(meta.taskId + done.result.sessionId;三端各写一份的第 5 缺口)。 */
|
|
64
|
+
onIdentity?(identity: AgentSessionIdentity): void;
|
|
65
|
+
/** 未知帧告警口(包内四层 default 静默的补偿;milestone 节流姿势收编 web-client frame-warnings)。 */
|
|
66
|
+
onUnknownFrame?(frame: unknown): void;
|
|
67
|
+
/** turn 级错误口(9 类 taxonomy + 状态码链 + wire-code 闭集表 —— 收编 web-client 三件套并接
|
|
68
|
+
* controlRouter.errCodes 单源,r1 E7:不是「三分诊」)。 */
|
|
69
|
+
onTurnError?(err: unknown): void;
|
|
70
|
+
}
|
|
71
|
+
export interface AgentSessionIdentity {
|
|
72
|
+
readonly taskId?: string;
|
|
73
|
+
readonly engineSessionId?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface AgentSessionConfig {
|
|
76
|
+
readonly wire: AgentSessionWire;
|
|
77
|
+
/** 请求车道(taskRequest.ts 的 RequestLane;web 传输 lane 是另一个轴,别混,r1 角度2)。 */
|
|
78
|
+
readonly lane: 'interactive' | 'print';
|
|
79
|
+
readonly hostSurface: AgentSessionHostSurface;
|
|
80
|
+
/**
|
|
81
|
+
* per-session 键(W1):包内单槽位全部改「sessionKey → 注册表」,零参旧 API = default-key
|
|
82
|
+
* 兼容层(cli 的 module-load 装配不动)。多会话宿主(desktop)每会话一键,互不顶盖。
|
|
83
|
+
*/
|
|
84
|
+
readonly sessionKey?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface AgentSession {
|
|
87
|
+
/** 发起一个 turn(内部:buildTaskRequest 唯一构造口 + runStream 内建去重 + HITL 桥)。 */
|
|
88
|
+
query(input: unknown): AsyncIterable<unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* 冷接(W5,r1 E9):按调用方给的 id 接上一个**不是本进程发起**的在飞 run(页面刷新 / 桌面
|
|
91
|
+
* 重启 / 跨端接手)。与 query() 同一消费面;park 帧在冷接腿上同样可决断。
|
|
92
|
+
* 这是 sema-web stream-core 旧腿退役的前置能力 —— 没有它退役 = 删掉唯一的刷新恢复路径。
|
|
93
|
+
*/
|
|
94
|
+
attach(target: {
|
|
95
|
+
taskId: string;
|
|
96
|
+
sessionId?: string;
|
|
97
|
+
}): AsyncIterable<unknown>;
|
|
98
|
+
interrupt(): Promise<void>;
|
|
99
|
+
steer(text: string): Promise<void>;
|
|
100
|
+
resumeAfterDecision(): Promise<void>;
|
|
101
|
+
/** 面缺席自检出口(测试面,恒 0 断言;不是运行期告警 —— Ink 吞 console,r1 E6)。 */
|
|
102
|
+
surfaceMisses(): Readonly<Record<string, number>>;
|
|
103
|
+
identity(): AgentSessionIdentity;
|
|
104
|
+
dispose(): void;
|
|
105
|
+
}
|
|
106
|
+
/** 层 1:唯一的 wire 入口。 */
|
|
107
|
+
export type CreateAgentSession = (config: AgentSessionConfig) => AgentSession;
|
|
108
|
+
/**
|
|
109
|
+
* 层 2:座位远程代理 —— 与 AgentSession **同型**(同一个接口),内部把每个方法转发给座位 IPC
|
|
110
|
+
* (seatContract 的 verb 面),零 wire 零 adapt。座位可达字段集是 9 键闭集(< interactive 的
|
|
111
|
+
* ~25 键)—— 该不对称必须**显式暴露**(supportedFields),端上的控件按它显隐,不许各自再判。
|
|
112
|
+
*/
|
|
113
|
+
export type AttachSeatSession = (seatApi: unknown) => AgentSession & {
|
|
114
|
+
readonly supportedFields: readonly string[];
|
|
115
|
+
};
|
|
116
|
+
export type BackgroundSource = 'assistant' | 'fleet';
|
|
117
|
+
/** not-configured = assistant 无 TiDB 时的 `{tasks:[]}` 形 —— 与「真没有任务」必须可区分。 */
|
|
118
|
+
export type BackgroundSourceHealth = 'ok' | 'unavailable' | 'not-configured';
|
|
119
|
+
/** 第三套中性词表(不用任一方词表:3 值压不住 9 值,9 值会造 assistant 侧不存在的态)。 */
|
|
120
|
+
export type BackgroundStatus = 'running' | 'waiting-on-human' | 'queued' | 'stopping' | 'done' | 'failed' | 'stopped';
|
|
121
|
+
export interface BackgroundRow {
|
|
122
|
+
/** 带来源判别;fleet 复合 id("<runId> <taskId>")不拆 —— 裸 id union 会重复计数。 */
|
|
123
|
+
readonly key: {
|
|
124
|
+
readonly source: 'assistant';
|
|
125
|
+
readonly taskId: string;
|
|
126
|
+
} | {
|
|
127
|
+
readonly source: 'fleet';
|
|
128
|
+
readonly rowId: string;
|
|
129
|
+
};
|
|
130
|
+
readonly status: BackgroundStatus;
|
|
131
|
+
/** 统一 epoch ms;assistant 的 ISO 串只在边界转一次。 */
|
|
132
|
+
readonly updatedAtMs?: number;
|
|
133
|
+
readonly needsAttention?: boolean;
|
|
134
|
+
readonly name?: string;
|
|
135
|
+
readonly description?: string;
|
|
136
|
+
readonly parentId?: string;
|
|
137
|
+
readonly tokens?: number;
|
|
138
|
+
readonly toolUses?: number;
|
|
139
|
+
readonly currentTool?: string;
|
|
140
|
+
readonly gate?: unknown;
|
|
141
|
+
}
|
|
142
|
+
export interface BackgroundView {
|
|
143
|
+
/** 🔴 一等返回位,端必须渲它 —— 源不可用/未配置与「空列表」是三件事。 */
|
|
144
|
+
readonly sources: Readonly<Record<BackgroundSource, BackgroundSourceHealth>>;
|
|
145
|
+
/** fleet 子行(parentId 在场)不进跨 session 视图(子代不是用户的独立后台任务)。 */
|
|
146
|
+
readonly rows: readonly BackgroundRow[];
|
|
147
|
+
}
|
|
148
|
+
export type CreateBackgroundView = (client: AgentClient) => {
|
|
149
|
+
snapshot(): BackgroundView | null;
|
|
150
|
+
subscribe(onChange: () => void): () => void;
|
|
151
|
+
dispose(): void;
|
|
152
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
// ── 已裁定(clay 终裁 2026-07-31,档 sema-internal e2b874c;实现必须遵守)──────────────────────
|
|
3
|
+
// RULED-1 durable park 腿面缺席 = **分形**:
|
|
4
|
+
// · 面「注册过但坏了」(卡口异常)→ 主动 decide deny + 诚实告知行(解锁优先);
|
|
5
|
+
// · 面「从未注册」(print/headless 本无卡口)→ 保留 park + 打印含 resume 指令的告知行
|
|
6
|
+
// (接手优先 —— headless 故意留 park 换 REPL/desktop 决断是合法场景)。
|
|
7
|
+
// 🔴 附加不变式(clay 原话「danger 权限保证全部畅通即可」):bypassPermissions/danger 模式下
|
|
8
|
+
// 审批链全程畅通 —— 零 park、零卡、零 deny,本缺席矩阵整表不适用;实现必带一条 danger-lane
|
|
9
|
+
// e2e 断言(danger 会话跑到审批点零停顿直通)。
|
|
10
|
+
// RULED-2 plan review 卡形 = **皮肤归端**:包只出数据+决断口;desktop 保持 ExitPlanMode 权限
|
|
11
|
+
// 卡形(CC 对位),cli 保持 overlay 形,不强制统一 UI。
|
|
12
|
+
// OPEN-3(实现期):file_link / live elicitation 两条 wire 缺口先补进 adapt(E8a/E8b,
|
|
13
|
+
// elicitation 方向 = 把 desktop/web 那份**上交**成包件)。
|
package/dist/controlRouter.d.ts
CHANGED
|
@@ -189,3 +189,11 @@ export declare class ControlRouter {
|
|
|
189
189
|
}): Promise<RunReceipt | null>;
|
|
190
190
|
private nextId;
|
|
191
191
|
}
|
|
192
|
+
/** Read a normalized error code/status off any SDK error shape variant (errorCode / code / name / status).
|
|
193
|
+
* design/161 批 A 起导出:错误判型单源 —— 包内新读面(backgroundView 等)一律吃这一口,不再各自手摸
|
|
194
|
+
* `(e as any).status`。 */
|
|
195
|
+
export declare function errCodes(e: unknown): {
|
|
196
|
+
code?: unknown;
|
|
197
|
+
name?: unknown;
|
|
198
|
+
status?: unknown;
|
|
199
|
+
};
|
package/dist/controlRouter.js
CHANGED
|
@@ -214,8 +214,10 @@ function uuid() {
|
|
|
214
214
|
return v.toString(16);
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
|
-
/** Read a normalized error code/status off any SDK error shape variant (errorCode / code / name / status).
|
|
218
|
-
|
|
217
|
+
/** Read a normalized error code/status off any SDK error shape variant (errorCode / code / name / status).
|
|
218
|
+
* design/161 批 A 起导出:错误判型单源 —— 包内新读面(backgroundView 等)一律吃这一口,不再各自手摸
|
|
219
|
+
* `(e as any).status`。 */
|
|
220
|
+
export function errCodes(e) {
|
|
219
221
|
if (!e || typeof e !== 'object')
|
|
220
222
|
return {};
|
|
221
223
|
const o = e;
|
|
@@ -88,6 +88,12 @@ export type EngineAgentPanelEvent = {
|
|
|
88
88
|
* 修「详情页 Prompt 段恒空」。缺席 ⇒ 维持现状(诚实留白)。
|
|
89
89
|
*/
|
|
90
90
|
transcriptId?: string;
|
|
91
|
+
/**
|
|
92
|
+
* server 1.250 起行帧带的 launch 真锚(epoch ms)。R4(clay 五报 07-31):此前投影层丢弃它,
|
|
93
|
+
* 详情页 elapsed 只能锚「壳建行时刻」(createTaskStateBase 的 Date.now())——遗留/重挂行上
|
|
94
|
+
* 渲出 1h+ 假时长。消费端拿到即覆盖行的 startTime;缺席 ⇒ 老引擎,维持现状。
|
|
95
|
+
*/
|
|
96
|
+
startedAt?: number;
|
|
91
97
|
} | {
|
|
92
98
|
kind: 'end';
|
|
93
99
|
taskId: string;
|
|
@@ -5,10 +5,17 @@ export interface EngineWireTarget {
|
|
|
5
5
|
}
|
|
6
6
|
/** 装/卸引擎 wire 目标。传 null 卸回 env 派生。返回还原函数。 */
|
|
7
7
|
export declare function installEngineWireTarget(t: EngineWireTarget | null): () => void;
|
|
8
|
+
/** W1 带 key 变体:装/卸某会话的引擎 wire 目标(多会话宿主每会话一键,互不顶盖)。 */
|
|
9
|
+
export declare function installEngineWireTargetFor(sessionKey: string, t: EngineWireTarget | null): () => void;
|
|
8
10
|
/**
|
|
9
11
|
* 当前引擎 wire 目标。null = 无 wire(离线/mock/浏览器未装)—— 调用方走各自的回落路径,
|
|
10
12
|
* **不是**错误态。
|
|
11
13
|
*/
|
|
12
14
|
export declare function engineWireTarget(): EngineWireTarget | null;
|
|
15
|
+
/**
|
|
16
|
+
* W1 带 key 变体。env 派生回落是**进程级**(键无关):该键未显式装过 ⇒ 与零参同一条 env
|
|
17
|
+
* 回落路径(Node 壳单会话语义不变;浏览器宿主 env 恒空 ⇒ null)。
|
|
18
|
+
*/
|
|
19
|
+
export declare function engineWireTargetFor(sessionKey: string): EngineWireTarget | null;
|
|
13
20
|
/** 诊断开关(壳侧 `process.env.SEMA_DEBUG` 的等价读;本包零 process)。 */
|
|
14
21
|
export declare function engineWireDebugEnabled(): boolean;
|
package/dist/engineWireTarget.js
CHANGED
|
@@ -18,14 +18,20 @@
|
|
|
18
18
|
* ⚠️ web/桌面宿主要用这条 wire,**不能靠 env** —— 用 `installEngineWireTarget()` 显式装。
|
|
19
19
|
*/
|
|
20
20
|
import { hostEnv } from './hostEnv.js';
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
import { createSessionSlot, DEFAULT_SESSION_KEY } from './sessionSlot.js';
|
|
22
|
+
/** 显式装配(非 Node 宿主唯一的入口;装了就**优先于** env,便于桌面/web 一页多引擎)。
|
|
23
|
+
* W1(design/161):sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY 兼容层。 */
|
|
24
|
+
const installedByKey = createSessionSlot();
|
|
23
25
|
/** 装/卸引擎 wire 目标。传 null 卸回 env 派生。返回还原函数。 */
|
|
24
26
|
export function installEngineWireTarget(t) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
return installEngineWireTargetFor(DEFAULT_SESSION_KEY, t);
|
|
28
|
+
}
|
|
29
|
+
/** W1 带 key 变体:装/卸某会话的引擎 wire 目标(多会话宿主每会话一键,互不顶盖)。 */
|
|
30
|
+
export function installEngineWireTargetFor(sessionKey, t) {
|
|
31
|
+
const prev = installedByKey.get(sessionKey);
|
|
32
|
+
installedByKey.set(sessionKey, t);
|
|
27
33
|
return () => {
|
|
28
|
-
|
|
34
|
+
installedByKey.set(sessionKey, prev);
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
@@ -33,6 +39,14 @@ export function installEngineWireTarget(t) {
|
|
|
33
39
|
* **不是**错误态。
|
|
34
40
|
*/
|
|
35
41
|
export function engineWireTarget() {
|
|
42
|
+
return engineWireTargetFor(DEFAULT_SESSION_KEY);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* W1 带 key 变体。env 派生回落是**进程级**(键无关):该键未显式装过 ⇒ 与零参同一条 env
|
|
46
|
+
* 回落路径(Node 壳单会话语义不变;浏览器宿主 env 恒空 ⇒ null)。
|
|
47
|
+
*/
|
|
48
|
+
export function engineWireTargetFor(sessionKey) {
|
|
49
|
+
const installed = installedByKey.get(sessionKey) ?? null;
|
|
36
50
|
if (installed !== null)
|
|
37
51
|
return installed;
|
|
38
52
|
const env = hostEnv();
|