@sema-agent/client-core 0.2.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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * diagnostics.ts — 引擎 LSP `diagnostics` 帧 → CC `diagnostics` attachment 载荷(PURE)。
3
+ * cli upstreamBridge case 'diagnostics' 的等价搬运(design/121):
4
+ * · wire severity 是 LSP 数字(1=Error 2=Warning 3=Info 4=Hint)→ CC 字符串枚举;
5
+ * 缺席/越界 → 'Error'(可见胜过隐身);
6
+ * · range 缺席 → 零 range 占位(渲染器必读字段);source/code 存在才铸(且一律 String 化);
7
+ * · 只留 `file://` 前缀且有诊断项的文件;全滤光 ⇒ 返回 null(宿主不渲空行)。
8
+ */
9
+ declare const SEV: readonly ["Error", "Warning", "Info", "Hint"];
10
+ export interface DiagnosticsAttachment {
11
+ type: 'diagnostics';
12
+ files: Array<{
13
+ uri: string;
14
+ diagnostics: Array<{
15
+ message: string;
16
+ severity: (typeof SEV)[number];
17
+ range: {
18
+ start: {
19
+ line: number;
20
+ character: number;
21
+ };
22
+ end: {
23
+ line: number;
24
+ character: number;
25
+ };
26
+ };
27
+ source?: string;
28
+ code?: string;
29
+ }>;
30
+ }>;
31
+ isNew: boolean;
32
+ }
33
+ export declare function projectDiagnosticsFrame(frame: {
34
+ files?: unknown;
35
+ isNew?: unknown;
36
+ }): DiagnosticsAttachment | null;
37
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * diagnostics.ts — 引擎 LSP `diagnostics` 帧 → CC `diagnostics` attachment 载荷(PURE)。
3
+ * cli upstreamBridge case 'diagnostics' 的等价搬运(design/121):
4
+ * · wire severity 是 LSP 数字(1=Error 2=Warning 3=Info 4=Hint)→ CC 字符串枚举;
5
+ * 缺席/越界 → 'Error'(可见胜过隐身);
6
+ * · range 缺席 → 零 range 占位(渲染器必读字段);source/code 存在才铸(且一律 String 化);
7
+ * · 只留 `file://` 前缀且有诊断项的文件;全滤光 ⇒ 返回 null(宿主不渲空行)。
8
+ */
9
+ const SEV = ['Error', 'Warning', 'Info', 'Hint'];
10
+ export function projectDiagnosticsFrame(frame) {
11
+ const zeroRange = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
12
+ const files = (Array.isArray(frame.files) ? frame.files : [])
13
+ .map(f => ({
14
+ uri: typeof f?.uri === 'string' ? f.uri : '',
15
+ diagnostics: (Array.isArray(f?.diagnostics)
16
+ ? f.diagnostics
17
+ : []).map(d => ({
18
+ message: typeof d?.message === 'string' ? d.message : '',
19
+ severity: SEV[(typeof d?.severity === 'number' ? d.severity : 1) - 1] ?? 'Error',
20
+ range: (d?.range ?? zeroRange),
21
+ ...(d?.source !== undefined ? { source: String(d.source) } : {}),
22
+ ...(d?.code !== undefined ? { code: String(d.code) } : {}),
23
+ })),
24
+ }))
25
+ .filter(f => f.uri.startsWith('file://') && f.diagnostics.length > 0);
26
+ if (files.length === 0)
27
+ return null;
28
+ return { type: 'diagnostics', files, isNew: frame.isNew === true };
29
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @sema-agent/client-core — 客户端会话运行时(sema wire → CC 会话词汇表 + 端无关会话逻辑)。
3
+ * 定位公理:黑板 [1832](CC 皮肤形状全收本包、wire 保持中性;TUI/web/桌面共用同一个包)。
4
+ * seam 设计对签:[1651]/[1652]/[1653];依赖方向:sdk(wire)+agent-types(词汇表)→ 本包,永不反向。
5
+ * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
+ * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
+ * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包)。
8
+ */
9
+ export * from './seam.js';
10
+ export * from './workflow.js';
11
+ export * from './notifications.js';
12
+ export * from './steering.js';
13
+ export * from './diagnostics.js';
14
+ export * from './retryStatus.js';
15
+ export * from './adapt.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @sema-agent/client-core — 客户端会话运行时(sema wire → CC 会话词汇表 + 端无关会话逻辑)。
3
+ * 定位公理:黑板 [1832](CC 皮肤形状全收本包、wire 保持中性;TUI/web/桌面共用同一个包)。
4
+ * seam 设计对签:[1651]/[1652]/[1653];依赖方向:sdk(wire)+agent-types(词汇表)→ 本包,永不反向。
5
+ * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
+ * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
+ * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包)。
8
+ */
9
+ export * from './seam.js';
10
+ export * from './workflow.js';
11
+ export * from './notifications.js';
12
+ export * from './steering.js';
13
+ export * from './diagnostics.js';
14
+ export * from './retryStatus.js';
15
+ export * from './adapt.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 通知投影纯函数(抽自 sema-cli engineTaskNotification/upstreamBridge 通知臂;
3
+ * result 段纪律=cli #49 C1:帧带 result 必入 XML,core renderTaskNotificationXml 同名 tag 同段序)。
4
+ */
5
+ /**
6
+ * ⚠️ 调用方契约(交叉复审 [MED] 登记):cli 原版在渲染前有两条默认值回填——
7
+ * summary 缺省 ⇒ `Background task "${taskId}" ${status}`;status 非串 ⇒ 'completed'。
8
+ * 本纯函数不做回填(职责在 adapt() 输入侧),直接透传引擎原始 JSON 会渲出空 <summary>。
9
+ * 落 adapt() 时必须先补齐这两个字段再调本函数。
10
+ * 另注:<status> 段转义为**有意统一变更**(cli 原版曾不转义;2026-07-25 双侧统一为转义,
11
+ * status 为受控枚举零实际影响,顺手关闭注入面)。
12
+ */
13
+ export interface TaskNotificationFields {
14
+ taskId: string;
15
+ status: string;
16
+ summary: string;
17
+ taskType?: string;
18
+ toolUseId?: string;
19
+ outputFile?: string;
20
+ diagnostics?: string;
21
+ result?: string;
22
+ stoppedBy?: string;
23
+ partial?: boolean;
24
+ exitCode?: number;
25
+ lines?: string[];
26
+ recentSteps?: string[];
27
+ resumable?: boolean;
28
+ externalSource?: string;
29
+ }
30
+ /** 通知 user-turn XML 渲染(core task-notification 同形;external 帧开标签带 from= 属性)。 */
31
+ export declare function renderTaskNotificationXml(n: TaskNotificationFields): string;
32
+ /**
33
+ * core 1.262.0 残局包([558]A/D)— 从 task_notification 帧派生残局显示行,与 core 进程内
34
+ * renderResidual(core/task-notification.ts:75-86)**逐字节对齐**:先 `- tool target → outcome`
35
+ * 步骤行,再 ONE `edited: path (×N), …` 行(editedFiles 折进 recent-steps,core 不铸独立
36
+ * <edited-files> tag——别自创)。字段是 additive 的,老引擎没有 ⇒ []。返回裸串,转义由调用方按
37
+ * 自己的面做(XML/Ink 行)。cli deriveNotificationResidualLines 的等价搬运。
38
+ */
39
+ export declare function deriveNotificationResidualLines(n: Record<string, unknown>): string[];
40
+ /**
41
+ * 引擎 task_notification 原始 payload → renderTaskNotificationXml 的入参(**含上面注释里那两条
42
+ * 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ 'completed')。
43
+ * task_id 空串/缺失 ⇒ null(RB-75 同族:空 id 会塌成 ':status' 共享桶互吞,无 id 的通知本就无意义)。
44
+ * 字段名映射按 wire 真形:task_id/output_file/task_type/source 是 snake/wire 名,其余同名。
45
+ */
46
+ export declare function normalizeTaskNotification(n: Record<string, unknown>): TaskNotificationFields | null;
47
+ /** 通知去重键——cli engineTaskNotificationDedupKey **逐字等价**(codex 对抗复审抓的
48
+ * 等价性破坏,0.1.1 修):无 seq 不追加段(与 seq:1 是不同键);仅 external 用
49
+ * `external:` 前缀;seq 收 number|string。格式:`[external:]taskId:status[:seq]`。 */
50
+ export declare function taskNotificationDedupKey(n: {
51
+ taskId: string;
52
+ status: string;
53
+ seq?: number | string;
54
+ taskType?: string;
55
+ }): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 通知投影纯函数(抽自 sema-cli engineTaskNotification/upstreamBridge 通知臂;
3
+ * result 段纪律=cli #49 C1:帧带 result 必入 XML,core renderTaskNotificationXml 同名 tag 同段序)。
4
+ */
5
+ function escapeXml(v) {
6
+ return v.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7
+ }
8
+ /** 通知 user-turn XML 渲染(core task-notification 同形;external 帧开标签带 from= 属性)。 */
9
+ export function renderTaskNotificationXml(n) {
10
+ const openTag = n.taskType === 'external' && n.externalSource
11
+ ? `<task-notification type="external" from="${escapeXml(n.externalSource.replace(/\s+/g, ' ').slice(0, 120)).replace(/"/g, '&quot;')}">`
12
+ : '<task-notification>';
13
+ const seg = (tag, v) => (v !== undefined && v.length > 0 ? `\n<${tag}>${escapeXml(v)}</${tag}>` : '');
14
+ const linesBlock = n.lines && n.lines.length > 0 ? `\n<output-lines>\n${n.lines.map(escapeXml).join('\n')}\n</output-lines>` : '';
15
+ const stepsBlock = n.recentSteps && n.recentSteps.length > 0 ? `\n<recent-steps>${n.recentSteps.map(escapeXml).join('\n')}</recent-steps>` : '';
16
+ return `${openTag}
17
+ <task-id>${escapeXml(n.taskId)}</task-id>${seg('tool-use-id', n.toolUseId)}${seg('output-file', n.outputFile)}${seg('diagnostics', n.diagnostics)}
18
+ <status>${escapeXml(n.status)}</status>${seg('stopped-by', n.stoppedBy)}${n.partial === true ? '\n<partial>true</partial>' : ''}${typeof n.exitCode === 'number' ? `\n<exit-code>${n.exitCode}</exit-code>` : ''}
19
+ <summary>${escapeXml(n.summary)}</summary>${seg('result', n.result)}${linesBlock}${stepsBlock}${typeof n.resumable === 'boolean' ? `\n<resumable>${n.resumable}</resumable>` : ''}
20
+ </task-notification>`;
21
+ }
22
+ /**
23
+ * core 1.262.0 残局包([558]A/D)— 从 task_notification 帧派生残局显示行,与 core 进程内
24
+ * renderResidual(core/task-notification.ts:75-86)**逐字节对齐**:先 `- tool target → outcome`
25
+ * 步骤行,再 ONE `edited: path (×N), …` 行(editedFiles 折进 recent-steps,core 不铸独立
26
+ * <edited-files> tag——别自创)。字段是 additive 的,老引擎没有 ⇒ []。返回裸串,转义由调用方按
27
+ * 自己的面做(XML/Ink 行)。cli deriveNotificationResidualLines 的等价搬运。
28
+ */
29
+ export function deriveNotificationResidualLines(n) {
30
+ const out = [];
31
+ if (Array.isArray(n.recentSteps)) {
32
+ for (const s of n.recentSteps) {
33
+ const step = (s ?? {});
34
+ if (typeof step.tool !== 'string' || step.tool.length === 0)
35
+ continue;
36
+ const target = typeof step.target === 'string' && step.target.length > 0 ? ` ${step.target}` : '';
37
+ const outcome = typeof step.outcome === 'string' && step.outcome.length > 0 ? ` → ${step.outcome}` : '';
38
+ out.push(`- ${step.tool}${target}${outcome}`);
39
+ }
40
+ }
41
+ if (Array.isArray(n.editedFiles)) {
42
+ const edited = n.editedFiles
43
+ .map(f => (f ?? {}))
44
+ .filter((f) => typeof f.path === 'string' && f.path.length > 0)
45
+ .map(f => `${f.path}${typeof f.edits === 'number' && f.edits > 1 ? ` (×${f.edits})` : ''}`);
46
+ if (edited.length > 0)
47
+ out.push(`edited: ${edited.join(', ')}`);
48
+ }
49
+ return out;
50
+ }
51
+ /**
52
+ * 引擎 task_notification 原始 payload → renderTaskNotificationXml 的入参(**含上面注释里那两条
53
+ * 回填**:summary 缺省 ⇒ `Background task "<id>" <status>`;status 非串 ⇒ 'completed')。
54
+ * task_id 空串/缺失 ⇒ null(RB-75 同族:空 id 会塌成 ':status' 共享桶互吞,无 id 的通知本就无意义)。
55
+ * 字段名映射按 wire 真形:task_id/output_file/task_type/source 是 snake/wire 名,其余同名。
56
+ */
57
+ export function normalizeTaskNotification(n) {
58
+ const taskId = typeof n.task_id === 'string' && n.task_id.length > 0 ? n.task_id : undefined;
59
+ if (taskId === undefined)
60
+ return null;
61
+ const status = typeof n.status === 'string' ? n.status : 'completed';
62
+ const lines = Array.isArray(n.lines)
63
+ ? n.lines.filter((l) => typeof l === 'string')
64
+ : [];
65
+ const recentSteps = deriveNotificationResidualLines(n);
66
+ return {
67
+ taskId,
68
+ status,
69
+ summary: typeof n.summary === 'string' && n.summary.length > 0
70
+ ? n.summary
71
+ : `Background task "${taskId}" ${status}`,
72
+ ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
73
+ ...(typeof n.toolUseId === 'string' && n.toolUseId.length > 0 ? { toolUseId: n.toolUseId } : {}),
74
+ ...(typeof n.output_file === 'string' && n.output_file.length > 0
75
+ ? { outputFile: n.output_file }
76
+ : {}),
77
+ ...(typeof n.diagnostics === 'string' && n.diagnostics.length > 0
78
+ ? { diagnostics: n.diagnostics }
79
+ : {}),
80
+ ...(typeof n.result === 'string' && n.result.length > 0 ? { result: n.result } : {}),
81
+ ...(typeof n.stoppedBy === 'string' && n.stoppedBy.length > 0 ? { stoppedBy: n.stoppedBy } : {}),
82
+ ...(n.partial === true ? { partial: true } : {}),
83
+ ...(typeof n.exitCode === 'number' ? { exitCode: n.exitCode } : {}),
84
+ ...(lines.length > 0 ? { lines } : {}),
85
+ ...(recentSteps.length > 0 ? { recentSteps } : {}),
86
+ ...(typeof n.resumable === 'boolean' ? { resumable: n.resumable } : {}),
87
+ ...(typeof n.source === 'string' && n.source.length > 0 ? { externalSource: n.source } : {}),
88
+ };
89
+ }
90
+ /** 通知去重键——cli engineTaskNotificationDedupKey **逐字等价**(codex 对抗复审抓的
91
+ * 等价性破坏,0.1.1 修):无 seq 不追加段(与 seq:1 是不同键);仅 external 用
92
+ * `external:` 前缀;seq 收 number|string。格式:`[external:]taskId:status[:seq]`。 */
93
+ export function taskNotificationDedupKey(n) {
94
+ const seq = typeof n.seq === 'number' || typeof n.seq === 'string' ? String(n.seq) : '';
95
+ const lane = n.taskType === 'external' ? 'external:' : '';
96
+ return `${lane}${n.taskId}:${n.status}${seq ? `:${seq}` : ''}`;
97
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * retryStatus.ts — BrainStatus liveness(§E3/§E10 `status` 臂)→ spinner retry 行状态(PURE)。
3
+ * cli src/sema/retryStatusStore.ts 的 mapBrainStatusToRetry 逐字搬运,唯一改动 = **时钟注入**
4
+ * (原版读 Date.now(),库里由调用方给 nowMs,跨端确定性 + 可测)。187 语义对位:
5
+ * reconnecting → stalled → '✻ Waiting for API response · will retry in Ns · check your network'
6
+ * retrying → error → '✻ API error · Retrying in Ns'(非终态;187 硬编码 'API error')
7
+ * rate_limited → error → '✻ Usage limit reached · Retrying in Ns'(rateLimits 真 ⇒ 终态)
8
+ * circuit_open → error → '✻ <detail|Service temporarily unavailable> · Retrying in Ns'
9
+ * 绝不捏造 attempt 计数——只用引擎真给的 phase/detail/retryInSec。
10
+ */
11
+ export type RetryStatus = {
12
+ kind: 'stalled';
13
+ deadline: number;
14
+ } | {
15
+ kind: 'error';
16
+ deadline: number;
17
+ attempt?: number;
18
+ maxRetries?: number;
19
+ error: {
20
+ formatted: string;
21
+ isNetworkDown?: boolean;
22
+ connection?: {
23
+ isSSLError?: boolean;
24
+ };
25
+ rateLimits?: {
26
+ resetsAt?: number;
27
+ rateLimitType?: string;
28
+ } | null;
29
+ };
30
+ };
31
+ export interface BrainStatusPayload {
32
+ phase: string;
33
+ detail?: string;
34
+ retryInSec?: number;
35
+ }
36
+ export declare function mapBrainStatusToRetry(p: BrainStatusPayload, nowMs: number): RetryStatus;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * retryStatus.ts — BrainStatus liveness(§E3/§E10 `status` 臂)→ spinner retry 行状态(PURE)。
3
+ * cli src/sema/retryStatusStore.ts 的 mapBrainStatusToRetry 逐字搬运,唯一改动 = **时钟注入**
4
+ * (原版读 Date.now(),库里由调用方给 nowMs,跨端确定性 + 可测)。187 语义对位:
5
+ * reconnecting → stalled → '✻ Waiting for API response · will retry in Ns · check your network'
6
+ * retrying → error → '✻ API error · Retrying in Ns'(非终态;187 硬编码 'API error')
7
+ * rate_limited → error → '✻ Usage limit reached · Retrying in Ns'(rateLimits 真 ⇒ 终态)
8
+ * circuit_open → error → '✻ <detail|Service temporarily unavailable> · Retrying in Ns'
9
+ * 绝不捏造 attempt 计数——只用引擎真给的 phase/detail/retryInSec。
10
+ */
11
+ export function mapBrainStatusToRetry(p, nowMs) {
12
+ const deadline = nowMs + Math.max(0, p.retryInSec ?? 0) * 1000;
13
+ switch (p.phase) {
14
+ case 'reconnecting':
15
+ return { kind: 'stalled', deadline };
16
+ case 'rate_limited':
17
+ return { kind: 'error', deadline, error: { formatted: '', rateLimits: {} } };
18
+ case 'circuit_open':
19
+ return {
20
+ kind: 'error',
21
+ deadline,
22
+ error: { formatted: p.detail ?? 'Service temporarily unavailable', isNetworkDown: true },
23
+ };
24
+ case 'retrying':
25
+ default:
26
+ return { kind: 'error', deadline, error: { formatted: '' } };
27
+ }
28
+ }
package/dist/seam.d.ts ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * client-core seam types — 黑板 [1651] 设计稿 v1.1 已对签形([1652] core 签/[1653] 钉版)。
3
+ *
4
+ * 平面判据(成文,[1652]):「重启后还该看得见的进 transcript,纯瞬态进 chrome。」
5
+ * transcript id 纪律([1653] v1 定案,后改=BREAKING):优先从 wire 稳定键确定性派生
6
+ * (StampedAgentEvent.id/seq/toolCallId),ctx.uuid() 仅兜底无稳定键合成帧;
7
+ * 往返守卫不变量:同流重放 ⇒ 同 id 序列。
8
+ */
9
+ import type { SDKMessage } from '@sema-agent/agent-types';
10
+ /** 宿主注入的适配上下文(时钟/铸号可测,B 形跨端确定性)。 */
11
+ export interface AdapterContext {
12
+ sessionId: string;
13
+ /** 兜底铸号——仅当帧无稳定键时使用(见 deriveTranscriptId)。 */
14
+ uuid(): string;
15
+ now(): number;
16
+ }
17
+ /** chrome 平面事件 v1 枚举(cli 现役 UI 面归纳;additive 演进,臂只增不改)。
18
+ *
19
+ * v1.1 增臂(#52a adapt() 首批落码,2026-07-25):下方 8 臂随 adapt() 管线一起加入——
20
+ * 它们全是 cli sdkMessagesToCcEvents 里「壳态耦合」的副作用面(store 写 / hook fire /
21
+ * 队列操作 / 瞬态 render 事件),按 [1652] 判据(纯瞬态进 chrome)投影成事件让宿主消费。
22
+ * 每臂的「宿主消费义务」写在臂上方注释里——不实现该义务 = 该 UI 面在宿主上是哑的。 */
23
+ export type ChromeEvent = {
24
+ kind: 'retry_status';
25
+ laneProof: LaneProof;
26
+ status: unknown;
27
+ } | {
28
+ kind: 'panel_task';
29
+ laneProof: LaneProof;
30
+ event: unknown;
31
+ } | {
32
+ kind: 'bgshell_settle';
33
+ laneProof: LaneProof;
34
+ taskId: string;
35
+ status: string;
36
+ outputPath?: string;
37
+ } | {
38
+ kind: 'tasks_expand';
39
+ laneProof: LaneProof;
40
+ } | {
41
+ kind: 'thinking_activity';
42
+ laneProof: LaneProof;
43
+ active: boolean;
44
+ }
45
+ /**
46
+ * 一个 turn 开场(cli `yield {type:'stream_request_start'}` 的等价面)。
47
+ * 宿主消费义务:把 spinner 置 'requesting' 态(CC query.ts:337 同形);不落转录。
48
+ */
49
+ | {
50
+ kind: 'request_start';
51
+ laneProof: LaneProof;
52
+ }
53
+ /**
54
+ * 合并后的活体增量(cli COALESCER 的 content_block_start/delta 语义面)。`opening`=该
55
+ * channel 本段首帧(宿主应先发 content_block_start 再发 delta);`estimatedTokens`=CJK 加权
56
+ * token 估算(cli estimateCjkTokens 同权重),给 spinner 计数用。
57
+ * 宿主消费义务:① 渲活体预览(streamingText/streamingThinking)② 推进 responseLength/
58
+ * spinner 模式 ③ 绝不落转录——committed 形随后由 transcript 平面的 assistant 消息给出。
59
+ */
60
+ | {
61
+ kind: 'stream_delta';
62
+ laneProof: LaneProof;
63
+ channel: 'text' | 'thinking';
64
+ text: string;
65
+ estimatedTokens: number;
66
+ opening: boolean;
67
+ }
68
+ /**
69
+ * 187 C1a 计量喂料(cli messageStartEvt / messageDeltaEndEvt 的语义面)。
70
+ * phase='start' 携 ttftMs(首 token 时延),phase='end' 携该 model round 的 outputTokens。
71
+ * 宿主消费义务:驱动自己的 responseLength reducer(start=压基线,end=按真 output_tokens 对账);
72
+ * 不落转录。
73
+ */
74
+ | {
75
+ kind: 'response_metrics';
76
+ laneProof: LaneProof;
77
+ phase: 'start' | 'end';
78
+ ttftMs?: number;
79
+ outputTokens?: number;
80
+ }
81
+ /**
82
+ * 非持久 attachment 行(cli 的 diagnostics / steering_injected / workspace_changed /
83
+ * compact_file_reference 四类 attachment render 事件)。按 [1652] 判据归 chrome:
84
+ * attachment 不进壳 jsonl,重启后本就该消失。
85
+ * 宿主消费义务:渲成自己的 attachment 行(CC 宿主=AttachmentMessage,按 attachment.type 分臂,
86
+ * 必须带 default 臂——core 可加新 source);`id` 为确定性行键(列表 key 用);不落转录。
87
+ */
88
+ | {
89
+ kind: 'attachment';
90
+ laneProof: LaneProof;
91
+ id: string;
92
+ attachment: unknown;
93
+ }
94
+ /**
95
+ * 一条终态 task_notification 抵达(引擎已 server-side steer-inject 过,壳侧只做记账)。
96
+ * 宿主消费义务(cli 现役三件,缺一即回归):
97
+ * ① drop 壳队列里同 run 的 Path B 条目(workflow_complete/probe feeder),否则 idle
98
+ * auto-submit 会把同一完成再喂模型一遍(跨进程双投的壳侧半场);
99
+ * ② 非 background_bash:清 resident 台账(面板行终态由本帧落,不由 turn sweep 假结)。
100
+ * 跨通道去重(「补发通道不得再注入模型一遍」)由**适配器自持台账**负责,不摊给宿主——
101
+ * 见 adapt.ts 的 notifiedRuns / workflow_notification_enqueue 臂的发射门。
102
+ * 注:面板行 end 事件本身走 panel_task 臂、SubagentStop 走 subagent_lifecycle 臂,不重复。
103
+ */
104
+ | {
105
+ kind: 'notification_terminal';
106
+ laneProof: LaneProof;
107
+ taskId: string;
108
+ status: string;
109
+ taskType?: string;
110
+ }
111
+ /**
112
+ * 子代生命周期 hook 触发点(cli fireSubagentStartHook / fireSubagentStopHook)。
113
+ * `guard:'if-started'` = 宿主必须先查自己的「已 fire Start」台账,没 fire 过 Start 的
114
+ * taskId(workflow runId / 外来 id)绝不 fire Stop。
115
+ * 宿主消费义务:fire SubagentStart/SubagentStop hooks(observe-only,fail-soft,永不阻塞流);
116
+ * Start 对一个 taskId 一生只许一次(进程级台账去重,frame-lane-matrix #4)。
117
+ */
118
+ | {
119
+ kind: 'subagent_lifecycle';
120
+ laneProof: LaneProof;
121
+ phase: 'start' | 'stop';
122
+ taskId: string;
123
+ agentType?: string;
124
+ guard?: 'if-started';
125
+ }
126
+ /**
127
+ * 带外后台 workflow 完成推送(service 1.75 Path B:引擎**没有** server-side 注入,壳负责喂模型)。
128
+ * 宿主消费义务:把 `message`(模型面 `<task-notification>` 正文,适配器已按 core 同形铸好)
129
+ * 入「完成通知队列」,idle 时 auto-submit 给模型(CC RemoteAgentTask 先例,默认 'later' 优先级)。
130
+ * 跨通道去重(同一 run 已由推送帧记账 ⇒ 本臂根本不会发)由适配器台账保证,宿主无需再判。
131
+ */
132
+ | {
133
+ kind: 'workflow_notification_enqueue';
134
+ laneProof: LaneProof;
135
+ runId: string;
136
+ status: 'completed' | 'failed';
137
+ summary: string;
138
+ message: string;
139
+ }
140
+ /**
141
+ * 引擎侧 task 族工具调用的台账同步点(cli handleEngineTaskToolUse:引擎模式下壳工具的 call()
142
+ * 从不执行,ctrl+t 面板的本地 store 只能靠这条侧信道)。
143
+ * 宿主消费义务:按 toolUseId 幂等地把 TaskCreate/TaskUpdate 落自己的 task store(畸形 input
144
+ * fail-soft 丢弃);不落转录。
145
+ */
146
+ | {
147
+ kind: 'task_ledger_sync';
148
+ laneProof: LaneProof;
149
+ source: 'tool_use';
150
+ toolName: string;
151
+ toolUseId: string;
152
+ input: unknown;
153
+ };
154
+ /** 车道纪律一等公民([1617] 家族固化):chrome 事件构造必须携判别证明。 */
155
+ export type LaneProof = {
156
+ lane: 'main';
157
+ } | {
158
+ lane: 'subagent';
159
+ parentToolCallId: string;
160
+ } | {
161
+ lane: 'workflow';
162
+ via: 'workflowRunId' | 'wa-id' | 'workflow-card-parent';
163
+ key: string;
164
+ };
165
+ /** 双平面输出。 */
166
+ export type AdapterOutput = {
167
+ plane: 'transcript';
168
+ message: SDKMessage;
169
+ } | {
170
+ plane: 'chrome';
171
+ event: ChromeEvent;
172
+ };
173
+ /** L3 适配器接口(实现分批落码;首版先出纯函数面,见 index.ts)。 */
174
+ export interface WireToCcAdapter {
175
+ adapt(frames: AsyncIterable<unknown>, ctx: AdapterContext): AsyncGenerator<AdapterOutput>;
176
+ /** 台账序列化位(#9 定谳的工程化:重启恢复=台账加载+纯重放)。 */
177
+ exportLedger(): Record<string, unknown>;
178
+ }
179
+ /**
180
+ * transcript id 确定性派生([1653] 定案):稳定键优先序 = 帧 id > seq > toolCallId;
181
+ * 全缺才用 ctx.uuid()。同流重放 ⇒ 同 id 序列(往返守卫钉此不变量)。
182
+ */
183
+ export declare function deriveTranscriptId(frame: {
184
+ id?: unknown;
185
+ seq?: unknown;
186
+ toolCallId?: unknown;
187
+ }, ctx: Pick<AdapterContext, 'uuid'>): string;
package/dist/seam.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * transcript id 确定性派生([1653] 定案):稳定键优先序 = 帧 id > seq > toolCallId;
3
+ * 全缺才用 ctx.uuid()。同流重放 ⇒ 同 id 序列(往返守卫钉此不变量)。
4
+ */
5
+ export function deriveTranscriptId(frame, ctx) {
6
+ if (typeof frame.id === 'string' && frame.id.length > 0)
7
+ return `wid_${frame.id}`;
8
+ if (typeof frame.seq === 'number')
9
+ return `wseq_${frame.seq}`;
10
+ if (typeof frame.toolCallId === 'string' && frame.toolCallId.length > 0)
11
+ return `wtc_${frame.toolCallId}`;
12
+ return ctx.uuid();
13
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * steering.ts — `steering_injected` → renderable attachment projection (PURE)。
3
+ * cli src/sema/steeringAttachments.ts 的逐字搬运(#52a;差分守卫钉两侧等价)。
4
+ *
5
+ * core 1.251 (design/133, board [479] ask-1) echoes every engine-side SYSTEM steering injection as a
6
+ * `steering_injected` wire event `{source, preview}` — `preview` = the injected body's first 220 chars
7
+ * (runtask.ts:2320), echo-only (the model-facing message is unchanged). Sources today:
8
+ * `deadline_nudge` / `finalize` (TB telemetry B3) and the design/133 turn-boundary attachment
9
+ * producers `todo_reminder` / `task_reminder` / `changed_files` / `plan_mode`, plus the G1 [482]
10
+ * members `background_tasks` / `tools_delta` (core 1.253).
11
+ *
12
+ * [479] ask-1 contract: a renderer that branches on `source` MUST carry a default arm — core may add
13
+ * sources without a cross-repo lockstep. This mapper is that branch point: known sources get their
14
+ * copy (the 四值文案), everything else falls to the generic default row (never dropped, never a
15
+ * crash). PURE (no env/IO) so it unit-tests without the render graph — the wireCaps pattern.
16
+ *
17
+ * Render policy (CC 2.1.198 parity): CC NULL-renders its own turn-boundary reminders
18
+ * (`todo_reminder` / `task_reminder` / `plan_mode` are in NULL_RENDERING_TYPES — the user never sees
19
+ * them in the normal transcript). The projected `steering_injected` attachment therefore renders
20
+ * ONLY in transcript (ctrl+o) / verbose mode (AttachmentMessage arm) — normal-mode output stays
21
+ * byte-identical to CC, while sema's two-process split keeps engine-side injections observable.
22
+ */
23
+ /** A sema `steering_injected` attachment (transcript/verbose-only dim row). */
24
+ export interface SteeringInjectedAttachment {
25
+ type: 'steering_injected';
26
+ /** The wire source verbatim (open set — core may extend). */
27
+ source: string;
28
+ /** Per-source display copy (the [479] ask-1 四值 + defaults). */
29
+ label: string;
30
+ /** The injected body's first 220 chars (wire-clipped), newline-flattened by the renderer. */
31
+ preview: string;
32
+ }
33
+ /**
34
+ * The CC `task_status` attachment shape ([487]② G1 backgroundTasks): the SAME attachment the LOCAL
35
+ * post-compact path produces (services/compact/compact.ts task_status rows), so the engine's
36
+ * post-compact recap renders through the SAME GenericTaskStatus (vbl) component — `Task "{desc}"
37
+ * completed in background / still running in background / stopped`, 198 verbatim. VISIBLE (CC
38
+ * renders these, unlike the reminders). `outputFilePath` omitted — the task lives engine-side.
39
+ */
40
+ export interface EngineTaskStatusAttachment {
41
+ type: 'task_status';
42
+ taskId: string;
43
+ taskType: 'local_bash';
44
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'killed';
45
+ description: string;
46
+ deltaSummary: null;
47
+ }
48
+ export type SteeringRenderable = SteeringInjectedAttachment | EngineTaskStatusAttachment;
49
+ /** Default-arm label for a source this shell build predates. */
50
+ export declare function steeringLabel(source: string): string;
51
+ /**
52
+ * Project one `steering_injected` wire event into its renderable attachment(s). Always returns at
53
+ * least one attachment (default 兜底) — an engine injection is never silently invisible in
54
+ * transcript mode, whatever the source.
55
+ *
56
+ * `background_tasks` ([487]② G1): the preview lines parse into CC `task_status` attachments — the
57
+ * SAME (VISIBLE) GenericTaskStatus/vbl rendering the LOCAL post-compact path uses, three-state copy
58
+ * 198 verbatim, `[task_id]` addressable via TaskOutput/TaskStop. A preview clipped beyond
59
+ * recognition falls back to the generic (transcript-only) row — never invisible, never a crash.
60
+ */
61
+ export declare function steeringInjectedToAttachments(source: string, preview: string): SteeringRenderable[];