@sema-agent/client-core 0.11.13 → 0.11.15
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 +1 -1
- package/dist/hitl/askGateWire.d.ts +3 -0
- package/dist/hitl/askGateWire.js +23 -12
- package/dist/workflowClient.d.ts +20 -0
- package/dist/workflowClient.js +55 -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.15
|
|
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
|
|
@@ -100,6 +100,9 @@ export interface AskGateWireDeps {
|
|
|
100
100
|
* tool_approval 帧(帧被吞、引擎按自身 fail-closed TTL 自决)——mock/旧引擎路径零影响。 */
|
|
101
101
|
respondToolApproval?: RespondToolApprovalFn;
|
|
102
102
|
}
|
|
103
|
+
/** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
|
|
104
|
+
* 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
|
|
105
|
+
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
103
106
|
/** CC AskUserQuestion outputSchema 形状(答过的问题卡):toolResult 的 `ask-user-question` arm
|
|
104
107
|
* 消费它渲真实答案卡(否则「诚实缺席」路径会把这张卡渲成结果不可用)。 */
|
|
105
108
|
export interface AskAnsweredOutput {
|
package/dist/hitl/askGateWire.js
CHANGED
|
@@ -38,6 +38,9 @@ function rejectMessageForRender() {
|
|
|
38
38
|
}
|
|
39
39
|
/** 一 turn 内最多循环这么多次 park(防御:引擎/模型病态连环提问时不无限 attach)。 */
|
|
40
40
|
const MAX_GATE_HOPS = 24;
|
|
41
|
+
/** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
|
|
42
|
+
* 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
|
|
43
|
+
export const ENGINE_ABORT_TOOL_RESULT = 'Operation aborted';
|
|
41
44
|
function isAskTool(name) {
|
|
42
45
|
return typeof name === 'string' && name.replace(/[\s_-]+/g, '').toLowerCase() === 'askuserquestion';
|
|
43
46
|
}
|
|
@@ -297,18 +300,18 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
|
|
|
297
300
|
const seq = ev.id;
|
|
298
301
|
if (typeof seq === 'string' && seq.length > 0)
|
|
299
302
|
lastSeq = seq;
|
|
300
|
-
// ── [2084]① 扣留帧「模型推进即放行」(2026-07-30)
|
|
301
|
-
// HOLD
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
303
|
+
// ── [2084]① 扣留帧「模型推进即放行」(2026-07-30;同日 web 复审纠偏收窄触发集)──────────
|
|
304
|
+
// HOLD 只护 park 窗口。放行触发集 = host-lane **模型推进**帧:text_delta/reasoning_delta/
|
|
305
|
+
// tool_start —— park 路径上这三种在 done{suspended} 之前不会出现,真错路径上下一轮必来其一。
|
|
306
|
+
// 🔴 turn_end 明确**不在**触发集(初版在,web 复审真机轨迹逮红):core agent-loop 的 turn_end
|
|
307
|
+
// emit 在 executeToolCalls 之后**无 abort/suspend 守卫**(邻臂都有,唯此一处没有)⇒ 真 park
|
|
308
|
+
// 的帧序是 tool_end(毒化)→ message_committed → turn_end → done{suspended},turn_end 若放行
|
|
309
|
+
// 会把毒化帧漏上屏,且 flushHeld 的 endedCalls 记账让 decide 后重放的解答帧被当已收口丢弃
|
|
310
|
+
// (真实答案卡永不出现)。代价 = turn 末尾无后续推进帧的 abort 形帧等到终帧 flush,与修前同。
|
|
306
311
|
// parentToolCallId 在场 = sub-agent 行帧,park 期间孙代照常说话,不算 host 推进(负控①C)。
|
|
312
|
+
// (普通工具错自 [2084]①-b 谓词收窄后根本不进 HOLD,零滞后不依赖本触发集。)
|
|
307
313
|
if (heldAskEnds.size > 0 &&
|
|
308
|
-
(ev.type === 'text_delta' ||
|
|
309
|
-
ev.type === 'reasoning_delta' ||
|
|
310
|
-
ev.type === 'tool_start' ||
|
|
311
|
-
ev.type === 'turn_end') &&
|
|
314
|
+
(ev.type === 'text_delta' || ev.type === 'reasoning_delta' || ev.type === 'tool_start') &&
|
|
312
315
|
ev.parentToolCallId === undefined) {
|
|
313
316
|
yield* flushHeld();
|
|
314
317
|
}
|
|
@@ -426,8 +429,16 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
|
|
|
426
429
|
yield { ...ev, output: rejectMessageForRender(), structured: undefined };
|
|
427
430
|
continue;
|
|
428
431
|
}
|
|
429
|
-
|
|
430
|
-
|
|
432
|
+
// [2084]①-b HOLD 谓词收窄(2026-07-30,desktop 复审建议采纳):park 毒化帧的载体是
|
|
433
|
+
// **确切的引擎 abort 标记**(core 对被 gate/连坐 abort 的 call 铸
|
|
434
|
+
// `tool_end{isError:true, output:"Operation aborted"}`;desktop session-host 2026-07-29
|
|
435
|
+
// 对真引擎实测同款)。普通工具错(old_string 不命中/read-gate 拒绝…)输出是各自的错误
|
|
436
|
+
// 文案,**根本不该进 HOLD**——收窄后它们零滞后直达转录,不再依赖「推进信号放行」。
|
|
437
|
+
if (ev.output === ENGINE_ABORT_TOOL_RESULT) {
|
|
438
|
+
heldAskEnds.set(ev.toolCallId, ev); // park 毒化帧,先 HOLD(gate 定性前不渲 Error 卡)
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
// 普通失败:落到下方通用收口路径,立即上屏。
|
|
431
442
|
}
|
|
432
443
|
endedCalls.add(ev.toolCallId);
|
|
433
444
|
heldAskEnds.delete(ev.toolCallId); // 正常收口(重放的 isError:false 解答帧)赢
|
package/dist/workflowClient.d.ts
CHANGED
|
@@ -15,6 +15,26 @@ export interface LiveWorkflowController {
|
|
|
15
15
|
runId: string | null;
|
|
16
16
|
lastError: string | null;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to "the projected snapshot changed" — fired on the FIRST successful `get()` and on every
|
|
20
|
+
* subsequent re-project. Returns an unsubscribe.
|
|
21
|
+
*
|
|
22
|
+
* WHY (clay 2026-07-30 症状②「入场蹲卡」): every host polls `snapshot()` on its own 1s interval, so a
|
|
23
|
+
* `get()` that answered in 50ms still took up to 1s to reach the screen — a purely self-inflicted delay,
|
|
24
|
+
* and during that window the panel renders its not-yet-resolved fill. The interval stays (it also drives
|
|
25
|
+
* the wall-clock cells: idle/duration/waiting), but the FIRST resolve no longer waits for a tick.
|
|
26
|
+
*
|
|
27
|
+
* 🔴 SUBSCRIBE-AND-RECONCILE: if a snapshot has ALREADY resolved by the time you subscribe, the callback fires
|
|
28
|
+
* ONCE synchronously from `subscribe` itself. Without this the notify arm and the subscribe arm do not share a
|
|
29
|
+
* premise — a host that renders (snapshot null) → subscribes in an effect loses the notify entirely whenever
|
|
30
|
+
* `get()` won that race, and falls back to waiting a full poll tick. That is exactly the residual outlier the
|
|
31
|
+
* entry-latency fence caught (2 of 3 runs ≈200-340ms, 1 run ≈1026ms = one whole tick).
|
|
32
|
+
*
|
|
33
|
+
* Contract: the callback runs INSIDE the source's own loop (or synchronously from `subscribe`) — hosts must only
|
|
34
|
+
* schedule a re-render from it, never re-enter the controller synchronously. A throwing callback is swallowed so
|
|
35
|
+
* one bad subscriber cannot kill the background consumer.
|
|
36
|
+
*/
|
|
37
|
+
subscribe(onChange: () => void): () => void;
|
|
18
38
|
/** abort the underlying SSE stream + stop the background loop (host unmount). */
|
|
19
39
|
dispose(): void;
|
|
20
40
|
}
|
package/dist/workflowClient.js
CHANGED
|
@@ -336,6 +336,24 @@ export function createLiveWorkflowSource(config) {
|
|
|
336
336
|
let lastError = null;
|
|
337
337
|
let disposed = false;
|
|
338
338
|
let lastGetAt = 0;
|
|
339
|
+
/** 入场打点用:只有第一发 get 是「入场」,后续都是 ticker 触发的 re-GET。 */
|
|
340
|
+
let first = true;
|
|
341
|
+
const listeners = new Set();
|
|
342
|
+
/** 通知订阅者「快照变了」。一个抛异常的订阅者不许弄死后台消费循环(fail-soft 与本源全篇一致)。 */
|
|
343
|
+
function notify() {
|
|
344
|
+
if (disposed)
|
|
345
|
+
return;
|
|
346
|
+
for (const l of [...listeners]) {
|
|
347
|
+
try {
|
|
348
|
+
l();
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (engineWireDebugEnabled()) {
|
|
352
|
+
hostLog('debug', `[sema][workflow] subscriber threw (ignored): ${String(err)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
339
357
|
// 差分① 见头注:唯一构造点 + 显式 maxRetries: 2(= 收编前吃的 SDK 缺省;工厂缺省 0 是单发探针语义)。
|
|
340
358
|
const client = makeEngineWireClient({
|
|
341
359
|
baseUrl: config.baseUrl,
|
|
@@ -360,11 +378,22 @@ export function createLiveWorkflowSource(config) {
|
|
|
360
378
|
hostLog('debug', `[sema][workflow] degrade/err (host falls back to mock/empty): ${lastError}`);
|
|
361
379
|
}
|
|
362
380
|
}
|
|
363
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* resolve the run id to monitor: explicit config → the first RUNNING run → the first run → null (empty).
|
|
383
|
+
*
|
|
384
|
+
* ⚠️ 入场热路径记账(#79 串行普查):`config.workflowId` 在场时**直接返回,不打 list()** —— 三个
|
|
385
|
+
* 面板宿主(FleetView 内联 / `/workflows` / footer 内联)都是从 fleet 行拿到 id 才开的,所以入场
|
|
386
|
+
* 只有 **get 一跳**;list→get 的两跳串行只发生在 `SEMA_SCENARIO=workflow` 独立宿主且未给
|
|
387
|
+
* SEMA_WORKFLOW_ID 的路径上(那条路没有行可依,list 是唯一的 id 来源 —— 必须串行)。
|
|
388
|
+
*/
|
|
364
389
|
async function resolveRunId(signal) {
|
|
365
390
|
if (config.workflowId)
|
|
366
391
|
return config.workflowId;
|
|
392
|
+
const t0 = Date.now();
|
|
367
393
|
const { workflows } = await client.workflows.list({ signal, ...engineSessionParamSpread() });
|
|
394
|
+
if (engineWireDebugEnabled()) {
|
|
395
|
+
hostLog('debug', `[sema][workflow][perf] list ${Date.now() - t0}ms rows=${workflows?.length ?? 0}`);
|
|
396
|
+
}
|
|
368
397
|
if (!workflows?.length)
|
|
369
398
|
return null;
|
|
370
399
|
const running = workflows.find(w => w.status === 'running');
|
|
@@ -372,6 +401,7 @@ export function createLiveWorkflowSource(config) {
|
|
|
372
401
|
}
|
|
373
402
|
/** take the authoritative snapshot + project it; records degrade on failure (never throws). */
|
|
374
403
|
async function refresh(id, signal) {
|
|
404
|
+
const t0 = Date.now();
|
|
375
405
|
try {
|
|
376
406
|
const run = await client.workflows.get(id, { signal, ...engineSessionParamSpread() });
|
|
377
407
|
lastGetAt = Date.now();
|
|
@@ -381,6 +411,13 @@ export function createLiveWorkflowSource(config) {
|
|
|
381
411
|
degraded = false;
|
|
382
412
|
code = null;
|
|
383
413
|
lastError = null;
|
|
414
|
+
if (engineWireDebugEnabled()) {
|
|
415
|
+
// 入场打点(#79):first=true 的那一行就是「壳拿到第一份权威快照」的耗时。
|
|
416
|
+
hostLog('debug', `[sema][workflow][perf] get ${Date.now() - t0}ms first=${first} agents=${current.agentCount} phases=${current.phases.length}`);
|
|
417
|
+
}
|
|
418
|
+
first = false;
|
|
419
|
+
// 解析即通知(首帧不等宿主的 1s tick;后续每次 re-project 同样通知,SSE ticker 的 re-GET 也走这里)。
|
|
420
|
+
notify();
|
|
384
421
|
}
|
|
385
422
|
catch (err) {
|
|
386
423
|
if (disposed || signal.aborted)
|
|
@@ -459,9 +496,26 @@ export function createLiveWorkflowSource(config) {
|
|
|
459
496
|
status() {
|
|
460
497
|
return { connected, degraded, code, runId, lastError };
|
|
461
498
|
},
|
|
499
|
+
subscribe(onChange) {
|
|
500
|
+
listeners.add(onChange);
|
|
501
|
+
// 订阅即对账:订阅前就已经解析完的话,notify() 那一发已经打空了(当时还没有订阅者)——
|
|
502
|
+
// 不在这里补一发,宿主就要白等一整个轮询 tick(配对机制必须共享前提)。
|
|
503
|
+
if (current !== null && !disposed) {
|
|
504
|
+
try {
|
|
505
|
+
onChange();
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
/* 订阅者自己的问题,不许弄坏本源 */
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return () => {
|
|
512
|
+
listeners.delete(onChange);
|
|
513
|
+
};
|
|
514
|
+
},
|
|
462
515
|
dispose() {
|
|
463
516
|
disposed = true;
|
|
464
517
|
abort.abort();
|
|
518
|
+
listeners.clear();
|
|
465
519
|
},
|
|
466
520
|
};
|
|
467
521
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.15",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|