@sema-agent/client-core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,17 @@
4
4
  * seam 设计对签:[1651]/[1652]/[1653];依赖方向:sdk(wire)+agent-types(词汇表)→ 本包,永不反向。
5
5
  * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
6
  * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
- * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包)
7
+ * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
8
+ * 0.3.0 = **B1 批**(多端改造设计稿 §3 批次表第二批):纯函数/台账 11 文件 + 1 纯类型文件搬入
9
+ * (壳侧 shim 替换随后批)。B1 只搬现行为、零收编、零行为改动;唯一的形状订正是 workflow.ts
10
+ * 补齐 `agent_runs[].errorCode` 类型位(0.2.0 抄件漏项)。
11
+ *
12
+ * 🔴 **单实例纪律**(B1 起本包不再只有纯函数):下列模块持 module 级可变状态,写方与读方必须解析到
13
+ * **同一个包实例** —— 壳侧本地旧副本与 npm 包同时进 bundle = 状态分裂(台账写进一份、读另一份,
14
+ * 症状是「台账恒空」的静默失效,不报错)。清单见各文件头 🔴 段:
15
+ * workflow.ts(workflowAgentTaskIds)· subagentContentStore.ts · engineAgentPanelStore.ts ·
16
+ * fleetAgentPanelProjection.ts · liveQuestionStore.ts · engineInlineTaskStats.ts ·
17
+ * engineCapsCache.ts · engineToolLabelStore.ts
8
18
  */
9
19
  export * from './seam.js';
10
20
  export * from './workflow.js';
@@ -13,3 +23,13 @@ export * from './steering.js';
13
23
  export * from './diagnostics.js';
14
24
  export * from './retryStatus.js';
15
25
  export * from './adapt.js';
26
+ export * from './subagentContentStore.js';
27
+ export * from './engineAgentPanelStore.js';
28
+ export * from './fleetAgentPanelProjection.js';
29
+ export * from './liveQuestionStore.js';
30
+ export * from './engineInlineTaskStats.js';
31
+ export * from './printToolResultFrame.js';
32
+ export * from './engineCapsCache.js';
33
+ export * from './engineToolLabelStore.js';
34
+ export * from './fleetTaskDesc.js';
35
+ export type * from './types/engineState.js';
package/dist/index.js CHANGED
@@ -4,7 +4,17 @@
4
4
  * seam 设计对签:[1651]/[1652]/[1653];依赖方向:sdk(wire)+agent-types(词汇表)→ 本包,永不反向。
5
5
  * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
6
  * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
- * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包)
7
+ * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
8
+ * 0.3.0 = **B1 批**(多端改造设计稿 §3 批次表第二批):纯函数/台账 11 文件 + 1 纯类型文件搬入
9
+ * (壳侧 shim 替换随后批)。B1 只搬现行为、零收编、零行为改动;唯一的形状订正是 workflow.ts
10
+ * 补齐 `agent_runs[].errorCode` 类型位(0.2.0 抄件漏项)。
11
+ *
12
+ * 🔴 **单实例纪律**(B1 起本包不再只有纯函数):下列模块持 module 级可变状态,写方与读方必须解析到
13
+ * **同一个包实例** —— 壳侧本地旧副本与 npm 包同时进 bundle = 状态分裂(台账写进一份、读另一份,
14
+ * 症状是「台账恒空」的静默失效,不报错)。清单见各文件头 🔴 段:
15
+ * workflow.ts(workflowAgentTaskIds)· subagentContentStore.ts · engineAgentPanelStore.ts ·
16
+ * fleetAgentPanelProjection.ts · liveQuestionStore.ts · engineInlineTaskStats.ts ·
17
+ * engineCapsCache.ts · engineToolLabelStore.ts
8
18
  */
9
19
  export * from './seam.js';
10
20
  export * from './workflow.js';
@@ -13,3 +23,13 @@ export * from './steering.js';
13
23
  export * from './diagnostics.js';
14
24
  export * from './retryStatus.js';
15
25
  export * from './adapt.js';
26
+ // ── B1 批:纯函数 / 侧信道台账 / 投影闸(2026-07-27)──────────────────────────────────────────
27
+ export * from './subagentContentStore.js';
28
+ export * from './engineAgentPanelStore.js';
29
+ export * from './fleetAgentPanelProjection.js';
30
+ export * from './liveQuestionStore.js';
31
+ export * from './engineInlineTaskStats.js';
32
+ export * from './printToolResultFrame.js';
33
+ export * from './engineCapsCache.js';
34
+ export * from './engineToolLabelStore.js';
35
+ export * from './fleetTaskDesc.js';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/liveQuestionStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 本模块自持 AskUserQuestion 的 wire 词汇(SDK 0.0.44 删掉了 questions 资源导出),搬入后这套
4
+ * 类型随包发布 —— web/桌面端的 HITL 覆盖层与 TUI 共用同一份 QuestionFrame/QuestionAnswer 形。
5
+ */
6
+ /**
7
+ * src/sema/liveQuestionStore.ts — the SIDE-CHANNEL for §4④ live-stream AskUserQuestion (the conversation-seam
8
+ * twin of liveSessionStore.ts).
9
+ *
10
+ * WHY THIS EXISTS
11
+ * ---------------
12
+ * In TOC (two-process) the MODEL runs in the engine; its `AskUserQuestion` tool fires on core's
13
+ * `RunnerDeps.onQuestion` seam and the service (question.ts, 1.61.0) emits a NAMED SSE frame
14
+ * (`event: question` / `event: question_complete`) that INTERLEAVES on the same live token stream as the
15
+ * AgentEvents. That frame is NOT an AgentEvent arm (it is a DISTINCT vocabulary, exactly like elicitation) —
16
+ * so it must be DEMUXED out of the transcript-render pipeline and routed to an interactive DIALOG overlay
17
+ * instead. This tiny, dependency-FREE module is that route: liveClient WRITES the demuxed frame here (on each
18
+ * `question`/`question_complete`) and publishes its `client.questions.respond` binding; the UI overlay
19
+ * (main.tsx / REPL) READS the frames and answers via `respondToQuestion`. Same shape as liveSessionStore /
20
+ * rewindAnchorStore — the overlay stays testable (no @sema-ai/sdk graph) and seamQuery/normalizeToolNames are
21
+ * untouched.
22
+ *
23
+ * 🔴 LIVE-ONLY + SAME-REPLICA (the `steer()`-class of HITL): the service parks the answer promise in-memory,
24
+ * so a respond that lands after the TTL (5 min) / after answer / on another replica → 404. No durable resume
25
+ * anchor — a mid-question disconnect just lets the run proceed with the headless default (the model never
26
+ * hangs). The overlay must therefore treat `respondToQuestion` as best-effort (a 404 = "already released",
27
+ * dismiss the dialog, never retry).
28
+ *
29
+ * 🔴 UNTRUSTED: the `questions` payload is model-authored (service secret-redacted, but still UNTRUSTED for
30
+ * display). The overlay RENDERS it; it must NEVER be re-fed to a model. The answer the overlay sends back is
31
+ * fenced by CORE (`selected ⊆ options`, `note` untrusted-fenced) — the overlay must echo the EXACT option
32
+ * labels the user saw, never fabricate one.
33
+ */
34
+ /** One option of an AskUserQuestion question (CC AskUserQuestionTool option shape). */
35
+ export interface AskQuestionOption {
36
+ /** EXACT display label — the answer echoes labels verbatim (core fences `selected ⊆ options`). */
37
+ label: string;
38
+ description?: string;
39
+ }
40
+ /** One model-authored question (UNTRUSTED: render-only, never re-fed to a model). */
41
+ export interface AskQuestion {
42
+ /** The question text (the CC dialog keys its answers Record by this). */
43
+ question: string;
44
+ /** Short header — the WIRE answer is keyed by this (see {@link QuestionAnswer}). */
45
+ header: string;
46
+ options: AskQuestionOption[];
47
+ multiSelect: boolean;
48
+ }
49
+ /** A demuxed/synthetic live question frame (legacy 1.61 wire payload `{type,questionId,questions?,outcome?}`;
50
+ * today also synthesized locally by liveHitlAskWire (`hitl-ask:*`) and planReviewWire (`plan-review:*`)).
51
+ * `question` opens the dialog overlay; `question_complete` dismisses a still-open one. */
52
+ export interface QuestionFrame {
53
+ type: 'question' | 'question_complete';
54
+ questionId: string;
55
+ /** Present on `question` frames; may be absent when the service dropped an over-cap payload. */
56
+ questions?: AskQuestion[];
57
+ /** `question_complete` only (legacy wire): why the ask was released (answered/expired/aborted). */
58
+ outcome?: string;
59
+ }
60
+ /** The answer the overlay sends back — keyed by HEADER; `selected` echoes exact option labels
61
+ * (`selected ⊆ options`, core's fence); off-list / free-text input rides `note` (untrusted-fenced).
62
+ * Entry shape === hitlBridge's AskAnswer (the T23 ApprovalDecision.answer leg consumes it as-is). */
63
+ export interface QuestionAnswer {
64
+ answers: Array<{
65
+ header: string;
66
+ selected: string[];
67
+ note?: string;
68
+ }>;
69
+ }
70
+ type QuestionFrameHandler = (frame: QuestionFrame) => void;
71
+ type RespondFn = (id: string, answer: QuestionAnswer, opts?: {
72
+ signal?: AbortSignal;
73
+ }) => Promise<unknown>;
74
+ /** Register a one-shot local responder for a SYNTHETIC question frame (e.g. `plan-review:<taskId>`).
75
+ * Returns an unregister fn (dialog dismissed without answering ⇒ caller cleans up). */
76
+ export declare function registerLocalQuestionResponder(id: string, fn: RespondFn): () => void;
77
+ /**
78
+ * Publish a demuxed live question frame (called from liveClient's demuxQuestionFrames on each
79
+ * `question`/`question_complete`). Best-effort: a UI handler error is swallowed so a broken overlay can NEVER
80
+ * break the live stream drain (the run must keep flowing; a lost frame just headless-defaults the ask).
81
+ */
82
+ export declare function publishQuestionFrame(frame: QuestionFrame): void;
83
+ /** Whether a UI overlay is currently subscribed (liveHitlAskWire gates on this: print/non-REPL mode has no
84
+ * dialog to park on, so the suspended terminal must fall through honestly instead of hanging the stream). */
85
+ export declare function hasQuestionOverlay(): boolean;
86
+ /** Subscribe the UI overlay to live question frames. Returns an unsubscribe. Last writer wins (one overlay). */
87
+ export declare function onQuestionFrame(handler: QuestionFrameHandler): () => void;
88
+ /** Publish the live client's `questions.respond` binding (called from createLiveConversationClient). `null`
89
+ * clears it (offline/mock → respondToQuestion rejects cleanly). */
90
+ export declare function setQuestionRespond(fn: RespondFn | null): void;
91
+ /**
92
+ * Answer a parked live question (POST /v1/questions/:id/respond via the live client). Rejects when no live
93
+ * client is wired (offline/mock) so the overlay can fall back / dismiss. The 404 (answered/expired/wrong
94
+ * replica) surfaces as a thrown ApiError — the overlay treats it as "already released", never a retry.
95
+ */
96
+ export declare function respondToQuestion(id: string, answer: QuestionAnswer, opts?: {
97
+ signal?: AbortSignal;
98
+ }): Promise<unknown>;
99
+ /** Test-only reset. */
100
+ export declare function _resetLiveQuestionStore(): void;
101
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/liveQuestionStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 本模块自持 AskUserQuestion 的 wire 词汇(SDK 0.0.44 删掉了 questions 资源导出),搬入后这套
4
+ * 类型随包发布 —— web/桌面端的 HITL 覆盖层与 TUI 共用同一份 QuestionFrame/QuestionAnswer 形。
5
+ */
6
+ /**
7
+ * src/sema/liveQuestionStore.ts — the SIDE-CHANNEL for §4④ live-stream AskUserQuestion (the conversation-seam
8
+ * twin of liveSessionStore.ts).
9
+ *
10
+ * WHY THIS EXISTS
11
+ * ---------------
12
+ * In TOC (two-process) the MODEL runs in the engine; its `AskUserQuestion` tool fires on core's
13
+ * `RunnerDeps.onQuestion` seam and the service (question.ts, 1.61.0) emits a NAMED SSE frame
14
+ * (`event: question` / `event: question_complete`) that INTERLEAVES on the same live token stream as the
15
+ * AgentEvents. That frame is NOT an AgentEvent arm (it is a DISTINCT vocabulary, exactly like elicitation) —
16
+ * so it must be DEMUXED out of the transcript-render pipeline and routed to an interactive DIALOG overlay
17
+ * instead. This tiny, dependency-FREE module is that route: liveClient WRITES the demuxed frame here (on each
18
+ * `question`/`question_complete`) and publishes its `client.questions.respond` binding; the UI overlay
19
+ * (main.tsx / REPL) READS the frames and answers via `respondToQuestion`. Same shape as liveSessionStore /
20
+ * rewindAnchorStore — the overlay stays testable (no @sema-ai/sdk graph) and seamQuery/normalizeToolNames are
21
+ * untouched.
22
+ *
23
+ * 🔴 LIVE-ONLY + SAME-REPLICA (the `steer()`-class of HITL): the service parks the answer promise in-memory,
24
+ * so a respond that lands after the TTL (5 min) / after answer / on another replica → 404. No durable resume
25
+ * anchor — a mid-question disconnect just lets the run proceed with the headless default (the model never
26
+ * hangs). The overlay must therefore treat `respondToQuestion` as best-effort (a 404 = "already released",
27
+ * dismiss the dialog, never retry).
28
+ *
29
+ * 🔴 UNTRUSTED: the `questions` payload is model-authored (service secret-redacted, but still UNTRUSTED for
30
+ * display). The overlay RENDERS it; it must NEVER be re-fed to a model. The answer the overlay sends back is
31
+ * fenced by CORE (`selected ⊆ options`, `note` untrusted-fenced) — the overlay must echo the EXACT option
32
+ * labels the user saw, never fabricate one.
33
+ */
34
+ /* ── LOCAL wire types (owned HERE, not @sema-ai/sdk) ─────────────────────────────────────────────
35
+ * SDK 0.0.44 DELETED the `questions` resource (engine 1.163 parks the run `suspended` instead of
36
+ * emitting `question` SSE frames — see liveHitlAskWire.ts), and with it the QuestionFrame /
37
+ * QuestionAnswer / AskQuestion exports. The shell still speaks this vocabulary INTERNALLY (synthetic
38
+ * frames from liveHitlAskWire / planReviewWire + the legacy 1.61-era SSE demux in liveClient), so the
39
+ * types now live in THIS dependency-free module — the store this whole seam already routes through.
40
+ * Shapes are the ones the shell actually produces/consumes (askq-wire-probe + overlay usage), byte-
41
+ * compatible with the old wire payload `{type,questionId,questions?,outcome?}`. */
42
+ let frameHandler = null;
43
+ let respondFn = null;
44
+ /** SHELL-LOCAL question responders (plan-review approval cards etc. — synthetic frames whose id does
45
+ * NOT exist engine-side, so POST /v1/questions/:id/respond would 404). Registered per-questionId by
46
+ * the frame's publisher; consumed once (respond removes the entry). Checked BEFORE the live respond. */
47
+ const localResponders = new Map();
48
+ /** Register a one-shot local responder for a SYNTHETIC question frame (e.g. `plan-review:<taskId>`).
49
+ * Returns an unregister fn (dialog dismissed without answering ⇒ caller cleans up). */
50
+ export function registerLocalQuestionResponder(id, fn) {
51
+ localResponders.set(id, fn);
52
+ return () => {
53
+ localResponders.delete(id);
54
+ };
55
+ }
56
+ /**
57
+ * Publish a demuxed live question frame (called from liveClient's demuxQuestionFrames on each
58
+ * `question`/`question_complete`). Best-effort: a UI handler error is swallowed so a broken overlay can NEVER
59
+ * break the live stream drain (the run must keep flowing; a lost frame just headless-defaults the ask).
60
+ */
61
+ export function publishQuestionFrame(frame) {
62
+ if (!frameHandler)
63
+ return;
64
+ try {
65
+ frameHandler(frame);
66
+ }
67
+ catch {
68
+ /* a UI handler throw must never propagate into the AgentEvent drain */
69
+ }
70
+ }
71
+ /** Whether a UI overlay is currently subscribed (liveHitlAskWire gates on this: print/non-REPL mode has no
72
+ * dialog to park on, so the suspended terminal must fall through honestly instead of hanging the stream). */
73
+ export function hasQuestionOverlay() {
74
+ return frameHandler !== null;
75
+ }
76
+ /** Subscribe the UI overlay to live question frames. Returns an unsubscribe. Last writer wins (one overlay). */
77
+ export function onQuestionFrame(handler) {
78
+ frameHandler = handler;
79
+ return () => {
80
+ if (frameHandler === handler)
81
+ frameHandler = null;
82
+ };
83
+ }
84
+ /** Publish the live client's `questions.respond` binding (called from createLiveConversationClient). `null`
85
+ * clears it (offline/mock → respondToQuestion rejects cleanly). */
86
+ export function setQuestionRespond(fn) {
87
+ respondFn = fn;
88
+ }
89
+ /**
90
+ * Answer a parked live question (POST /v1/questions/:id/respond via the live client). Rejects when no live
91
+ * client is wired (offline/mock) so the overlay can fall back / dismiss. The 404 (answered/expired/wrong
92
+ * replica) surfaces as a thrown ApiError — the overlay treats it as "already released", never a retry.
93
+ */
94
+ export function respondToQuestion(id, answer, opts) {
95
+ const local = localResponders.get(id);
96
+ if (local) {
97
+ localResponders.delete(id); // one-shot — a second respond on the same card must not double-fire
98
+ return local(id, answer, opts);
99
+ }
100
+ if (!respondFn) {
101
+ return Promise.reject(new Error('no live question respond wired (offline/mock path)'));
102
+ }
103
+ return respondFn(id, answer, opts);
104
+ }
105
+ /** Test-only reset. */
106
+ export function _resetLiveQuestionStore() {
107
+ frameHandler = null;
108
+ respondFn = null;
109
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/printToolResultFrame.ts 逐字搬入,
3
+ * 唯一改动 = `SDKMessage` 的 type-only import 从壳内 `../seam/adapter/index.js` 改指
4
+ * `@sema-agent/agent-types`(type-only,零运行时边)。
5
+ *
6
+ * 🔴 **这两个 SDKMessage 不是同一个 symbol**(实测,别沿用「re-export」的想当然):壳侧
7
+ * `src/seam/adapter/types.ts:35` 是 `z.infer<ReturnType<typeof SDKMessageSchema>>` —— CC 自己
8
+ * `src/entrypoints/sdk/coreSchemas.ts` 里 zod schema 的推断型;本包用的是 agent-types 手写 `.d.ts`
9
+ * 的同名镜像联合。两者都镜像 CC 的同一个 union,但**是两条独立的类型来源**。
10
+ * 本函数返回值本来就走 `as unknown as SDKMessage`(见文件末),所以搬迁没有改变任何运行时字节;
11
+ * ⚠️ 但**壳侧 shim 时要过一道 tsc**:`seamQueryEngine.ask` 那个消费点期望的是 zod 推断型,
12
+ * 换成 agent-types 型后若不结构兼容,tsc 会在 shim 那一刻报——那是真信号,不要用 any 压掉。
13
+ *
14
+ * ⚠️ 设计稿 §8-5 把 print lane 的归属列为未决裁量点 —— 本文件是纯投影(tool_end arm → CC user 帧),
15
+ * 任何端的「工具失败」判据都要它,故按 B1 清单搬入;`-p` 车道的**消费方**仍在壳侧。
16
+ */
17
+ /**
18
+ * src/sema/printToolResultFrame.ts — headless `-p` 车道的 CC `user`/tool_result 帧合成
19
+ * ([909]B1「工具错误置 is_error」半句勘察定案,2026-07-16)。
20
+ *
21
+ * 勘察结论(真行为实证,dist 自 spawn 引擎 + 失败 Bash 调用):`-p --output-format stream-json
22
+ * --verbose` 的 stdout 里【零】tool_result 帧——既不是壳写死 is_error:false,也不是引擎帧失真:
23
+ * 引擎 tool_end.isError 一路如实到壳(eventToSdkMessage.ts tool_end 臂 → 内部 tool_end_result arm),
24
+ * 但 seamQueryEngine.ask 的消费循环把该 arm 整帧吞掉(此前 :506 `continue`,swallow 注释自证)。
25
+ * TB「~4000 个 Bash 调用 is_error:true 零次」= 语料里只有 assistant/tool_use 帧,工具结果面整体缺席。
26
+ *
27
+ * CC 2.1.207 对表:`-p --verbose` 的 stream-json 在每个工具收尾后发
28
+ * {type:'user', message:{role:'user', content:[{type:'tool_result', tool_use_id, content,
29
+ * is_error}]}, parent_tool_use_id, uuid, session_id}
30
+ * (SDKUserMessage 信封;交互 REPL 侧 upstreamBridge.ts:1323-1343 已按同款形状合成给渲染面)。
31
+ * 本模块把内部 arm 投影成该形状,`-p` 集成面从此能从流里辨认工具失败(is_error 如实透传)。
32
+ *
33
+ * 车道纪律:只被 seamQueryEngine.ask(headless `-p`)消费;交互 REPL 走 upstreamBridge 的
34
+ * deferred-close 管线,一根毛不动。
35
+ */
36
+ import type { SDKMessage } from '@sema-agent/agent-types';
37
+ /**
38
+ * Flatten the §E1 wire `output`(NON-UNIFORM: `string` | `(TextContent|ImageContent)[]`)into one
39
+ * plain-text blob(upstreamBridge.flattenWireOutput 同款语义的轻量副本——避免把重量级 REPL 桥
40
+ * 拖进 `-p` 车道的 import 图)。🔴 UNTRUSTED / OBSERVABILITY-ONLY:只呈现,绝不回喂模型。
41
+ */
42
+ export declare function flattenToolOutput(output: unknown): string;
43
+ /** 内部 tool_end_result arm(eventToSdkMessage.ts:213-231 铸造)的防御性读形。 */
44
+ export interface ToolEndResultArmLike {
45
+ type?: unknown;
46
+ toolCallId?: unknown;
47
+ toolName?: unknown;
48
+ isError?: unknown;
49
+ output?: unknown;
50
+ truncated?: unknown;
51
+ parentToolCallId?: unknown;
52
+ uuid?: unknown;
53
+ session_id?: unknown;
54
+ }
55
+ /**
56
+ * tool_end_result arm → CC `user`/tool_result SDK 帧。toolCallId 缺失(防御:未来 wire 变形)
57
+ * 返回 null,调用方维持原 swallow 行为(绝不发无法对账的孤儿 tool_result)。
58
+ */
59
+ export declare function toolEndResultToUserFrame(arm: ToolEndResultArmLike): SDKMessage | null;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Flatten the §E1 wire `output`(NON-UNIFORM: `string` | `(TextContent|ImageContent)[]`)into one
3
+ * plain-text blob(upstreamBridge.flattenWireOutput 同款语义的轻量副本——避免把重量级 REPL 桥
4
+ * 拖进 `-p` 车道的 import 图)。🔴 UNTRUSTED / OBSERVABILITY-ONLY:只呈现,绝不回喂模型。
5
+ */
6
+ export function flattenToolOutput(output) {
7
+ if (typeof output === 'string')
8
+ return output;
9
+ if (Array.isArray(output)) {
10
+ const parts = [];
11
+ for (const block of output) {
12
+ if (block && typeof block === 'object') {
13
+ const b = block;
14
+ if (b.type === 'text' && typeof b.text === 'string')
15
+ parts.push(b.text);
16
+ else if (b.type === 'image')
17
+ parts.push('[image]');
18
+ }
19
+ }
20
+ return parts.join('');
21
+ }
22
+ return '';
23
+ }
24
+ /**
25
+ * Bash 臂的 is_error 派生(真行为实证,2026-07-16):引擎 tool_end.isError 语义 = 「工具本身
26
+ * 是否执行失败」——命令非零退出时工具照常返回模型面框架文本(core runShell `exit code: N\n---
27
+ * stdout ---…`)且 isError:false。CC 2.1.207 的 Bash tool_result 对非零退出置 is_error:true;
28
+ * 交互 REPL 桥已按同款派生(upstreamBridge.ts:384 `failed = isError || exitCode !== 0`)。
29
+ * 这里对 Bash 输出的框架头做同源轻量嗅探(不拖 REPL 桥的重 import 图)。
30
+ */
31
+ function bashExitCodeFailed(toolName, text) {
32
+ if (typeof toolName !== 'string' || toolName.toLowerCase() !== 'bash')
33
+ return false;
34
+ const m = /^exit code: (\d+)\b/.exec(text);
35
+ return m !== null && m[1] !== '0';
36
+ }
37
+ /**
38
+ * tool_end_result arm → CC `user`/tool_result SDK 帧。toolCallId 缺失(防御:未来 wire 变形)
39
+ * 返回 null,调用方维持原 swallow 行为(绝不发无法对账的孤儿 tool_result)。
40
+ */
41
+ export function toolEndResultToUserFrame(arm) {
42
+ if (typeof arm.toolCallId !== 'string' || arm.toolCallId.length === 0)
43
+ return null;
44
+ const text = flattenToolOutput(arm.output);
45
+ return {
46
+ type: 'user',
47
+ message: {
48
+ role: 'user',
49
+ content: [
50
+ {
51
+ type: 'tool_result',
52
+ tool_use_id: arm.toolCallId,
53
+ // truncated 输出如实标注(观察面注记;截断体绝不回喂模型——upstreamBridge 同款)。
54
+ content: arm.truncated === true ? `${text}\n…(truncated)` : text,
55
+ is_error: arm.isError === true || bashExitCodeFailed(arm.toolName, text),
56
+ },
57
+ ],
58
+ },
59
+ parent_tool_use_id: typeof arm.parentToolCallId === 'string' ? arm.parentToolCallId : null,
60
+ // eventToSdkMessage 已 stamp 的 uuid/session_id 原样骑帧(uuid = core-minted eventId 优先级
61
+ // 在 stamp 里已定);缺失时留 undefined,print 面容忍(CC 消费者按可选读)。
62
+ uuid: arm.uuid,
63
+ session_id: arm.session_id,
64
+ };
65
+ }
@@ -2,6 +2,10 @@
2
2
  * steering.ts — `steering_injected` → renderable attachment projection (PURE)。
3
3
  * cli src/sema/steeringAttachments.ts 的逐字搬运(#52a;差分守卫钉两侧等价)。
4
4
  *
5
+ * B1 批(2026-07-27):本文件正文与 cli `steeringAttachments.ts` 实测**逐字节相同**(`diff` 空),
6
+ * 按设计稿 §2.4.1-A 裁决「不留两份实现」⇒ 本文件即唯一实现,壳侧文件改成本包的 re-export。
7
+ * 双份消解方式是「壳侧让位」而非合并 —— 无需改动本文件任何一行行为码。
8
+ *
5
9
  * core 1.251 (design/133, board [479] ask-1) echoes every engine-side SYSTEM steering injection as a
6
10
  * `steering_injected` wire event `{source, preview}` — `preview` = the injected body's first 220 chars
7
11
  * (runtask.ts:2320), echo-only (the model-facing message is unchanged). Sources today:
package/dist/steering.js CHANGED
@@ -2,6 +2,10 @@
2
2
  * steering.ts — `steering_injected` → renderable attachment projection (PURE)。
3
3
  * cli src/sema/steeringAttachments.ts 的逐字搬运(#52a;差分守卫钉两侧等价)。
4
4
  *
5
+ * B1 批(2026-07-27):本文件正文与 cli `steeringAttachments.ts` 实测**逐字节相同**(`diff` 空),
6
+ * 按设计稿 §2.4.1-A 裁决「不留两份实现」⇒ 本文件即唯一实现,壳侧文件改成本包的 re-export。
7
+ * 双份消解方式是「壳侧让位」而非合并 —— 无需改动本文件任何一行行为码。
8
+ *
5
9
  * core 1.251 (design/133, board [479] ask-1) echoes every engine-side SYSTEM steering injection as a
6
10
  * `steering_injected` wire event `{source, preview}` — `preview` = the injected body's first 220 chars
7
11
  * (runtask.ts:2320), echo-only (the model-facing message is unchanged). Sources today:
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/subagentContentStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 module 级台账(tasks/parentToTask/ownEngineRuns/bgFacts)= 单实例语义:写方(seam adapter
4
+ * runStream 的子代内容分流 + liveClient 的 run_started own 台账)与读方(查看态/帧级 own 判别)
5
+ * 必须走**同一个模块实例**,否则 own 判别恒 false(帧级校验静默失效)。
6
+ */
7
+ /**
8
+ * subagentContentStore — dep-free side-channel store for ENGINE subagent CONTENT events (C1 consumption,
9
+ * service 1.89 `forwardSubagentEvents` + core 1.219; blackboard [381]⑤/[383]③).
10
+ *
11
+ * WIRE FACTS: with `forwardSubagentEvents: true` on the task request, the live stream carries the
12
+ * subagent's text_delta / reasoning_delta / tool_start / tool_end stamped with EventIdentity
13
+ * {taskId, parentToolCallId} (§E1 redact upstream; §E2 identity). The seam adapter (runStream.ts)
14
+ * DIVERTS those events here — they must never reach the main-transcript pipeline (a subagent's tokens
15
+ * rendering as the leader's text is the pollution failure mode).
16
+ *
17
+ * WHAT IT KEEPS (bounded, per task):
18
+ * - a NEUTRAL item log: {kind:'text'|'thinking',text} | {kind:'tool',name,input,output,isError,id}
19
+ * (ring-capped) — the 查看态 transcript backlog, so entering a running agent mid-flight still shows
20
+ * history. Message SHAPES are built by the consumers (engineAgentView/chrome-agentprogress own the
21
+ * CC message creators; this store stays dep-free like engineAgentPanelStore).
22
+ * - live accumulation buffers (streaming text/thinking not yet segment-closed).
23
+ * - taskId → parentToolCallId (the C2 steer target — service routes subagent steer by parentToolCallId).
24
+ *
25
+ * NOTIFY: coalesced (~250ms) per-task notify so a per-token delta storm never turns into per-token
26
+ * AppState writes; consumers pull a snapshot on notify.
27
+ */
28
+ export type SubagentContentItem = {
29
+ kind: 'text';
30
+ text: string;
31
+ } | {
32
+ kind: 'thinking';
33
+ text: string;
34
+ } | {
35
+ kind: 'tool';
36
+ id: string;
37
+ name: string;
38
+ input: unknown;
39
+ output?: string;
40
+ isError?: boolean;
41
+ }
42
+ /** local user echo (查看态 composer 插话) — rides in the item log so a full rebuild keeps it in order */
43
+ | {
44
+ kind: 'echo';
45
+ text: string;
46
+ };
47
+ export interface SubagentContentEvent {
48
+ type: 'text_delta' | 'reasoning_delta' | 'tool_start' | 'tool_end';
49
+ taskId: string;
50
+ parentToolCallId: string;
51
+ delta?: string;
52
+ toolCallId?: string;
53
+ toolName?: string;
54
+ args?: unknown;
55
+ output?: string;
56
+ isError?: boolean;
57
+ }
58
+ /** Register the parentToolCallId ↔ engine-taskId pair (from task_progress, which carries both).
59
+ * Content parked under the parent key migrates to the canonical task key. */
60
+ export declare function registerSubagentAlias(parentToolCallId: string, taskId: string): void;
61
+ export declare function publishSubagentContentEvent(ev: SubagentContentEvent): void;
62
+ /** 查看态 composer echo (C2 steer optimistic display) — segment-closes the buffers first so the echo
63
+ * lands AFTER the text streamed before it. taskId may be unseen (row entered before any content):
64
+ * parentToolCallId may then be unknown — pass '' and the steer path resolves the target separately. */
65
+ export declare function pushSubagentLocalEcho(taskId: string, text: string): void;
66
+ export interface SubagentContentSnapshot {
67
+ items: SubagentContentItem[];
68
+ /** streaming tails not yet segment-closed (render as the live trailing message) */
69
+ textBuf: string;
70
+ thinkBuf: string;
71
+ }
72
+ export declare function getSubagentContentSnapshot(taskId: string): SubagentContentSnapshot | null;
73
+ /**
74
+ * 一个查看态转录槽位。`slot` = 稳定 key(消费端拿去当 message uuid —— 250ms 一次的重组不换 key);
75
+ * `live: true` = 尚未段闭合的流式尾巴(下一次段闭合后由同内容的真 item 接位)。
76
+ */
77
+ export type SubagentViewSlot = {
78
+ kind: 'text';
79
+ slot: string;
80
+ text: string;
81
+ live?: true;
82
+ } | {
83
+ kind: 'thinking';
84
+ slot: string;
85
+ text: string;
86
+ live?: true;
87
+ } | {
88
+ kind: 'echo';
89
+ slot: string;
90
+ text: string;
91
+ } | {
92
+ kind: 'tool';
93
+ slot: string;
94
+ id: string;
95
+ name: string;
96
+ input: unknown;
97
+ /** undefined = tool_start 已到、tool_end 未到(在飞工具:卡先上屏,结果就地补) */
98
+ output?: string;
99
+ isError?: boolean;
100
+ };
101
+ /**
102
+ * 把一个子代的内容账本投影成查看态渲染计划。
103
+ *
104
+ * [1568] 之前的形(壳侧 engineAgentView 里)有两处「等段闭合」:①遇到第一个未闭合工具就 break;
105
+ * ②textBuf/thinkBuf 完全不投影。实测(engine 1.275.0 真跑):数据面 ~23 帧/s 的 delta 连续到壳,
106
+ * 查看态却只在 tool 边界更新(进查看态后 6.0s 全空,之后每 5–15s 才动一次)。现形:在飞工具照投,
107
+ * 流式尾巴作为 live 槽投 —— 屏面粒度 = wire 粒度(store notify 已按 250ms 合并)。
108
+ */
109
+ export declare function planSubagentViewSlots(taskId: string): SubagentViewSlot[];
110
+ /** C2 steer target: the service routes subagent steer by the delegating tool call's id. */
111
+ export declare function parentToolCallIdOf(taskId: string): string | undefined;
112
+ /** Settle a task's streaming buffers (turn ended / run settled) so the snapshot is complete. */
113
+ export declare function settleSubagentContent(taskId: string): void;
114
+ export declare function clearSubagentContent(taskId: string): void;
115
+ export interface BgTerminalFacts {
116
+ status: string;
117
+ summary?: string;
118
+ recentSteps?: Array<{
119
+ tool?: string;
120
+ target?: string;
121
+ outcome?: string;
122
+ }>;
123
+ }
124
+ export declare function recordBgTerminalFacts(taskId: string, facts: BgTerminalFacts): void;
125
+ export declare function getBgTerminalFacts(taskId: string): BgTerminalFacts | undefined;
126
+ export declare function recordBgParentRun(taskId: string, runId: string): void;
127
+ export declare function getBgParentRun(taskId: string): string | undefined;
128
+ export declare function recordOwnEngineRun(runId: string): void;
129
+ export declare function isOwnEngineRun(runId: string): boolean;
130
+ /** Single consumer (the panel hook). Returns an unsubscribe. */
131
+ export declare function subscribeSubagentContent(fn: (taskId: string) => void): () => void;