@sema-agent/client-core 0.3.0 → 0.4.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.
Files changed (56) hide show
  1. package/README.md +23 -1
  2. package/dist/agentsWireCaps.d.ts +112 -0
  3. package/dist/agentsWireCaps.js +319 -0
  4. package/dist/attachmentsWireCaps.d.ts +47 -0
  5. package/dist/attachmentsWireCaps.js +46 -0
  6. package/dist/classifierVerdictWire.d.ts +45 -0
  7. package/dist/classifierVerdictWire.js +76 -0
  8. package/dist/clientContextWireCaps.d.ts +46 -0
  9. package/dist/clientContextWireCaps.js +48 -0
  10. package/dist/cloudConfigWireCaps.d.ts +110 -0
  11. package/dist/cloudConfigWireCaps.js +228 -0
  12. package/dist/controlRouter.d.ts +191 -0
  13. package/dist/controlRouter.js +244 -0
  14. package/dist/effortWire.d.ts +34 -0
  15. package/dist/effortWire.js +38 -0
  16. package/dist/engineWireSdk.d.ts +49 -0
  17. package/dist/engineWireSdk.js +61 -0
  18. package/dist/forkWireCaps.d.ts +25 -0
  19. package/dist/forkWireCaps.js +42 -0
  20. package/dist/hostEnv.d.ts +16 -0
  21. package/dist/hostEnv.js +17 -0
  22. package/dist/imagesWireCaps.d.ts +28 -0
  23. package/dist/imagesWireCaps.js +49 -0
  24. package/dist/index.d.ts +35 -0
  25. package/dist/index.js +41 -0
  26. package/dist/liveModelCatalog.d.ts +62 -0
  27. package/dist/liveModelCatalog.js +79 -0
  28. package/dist/mcpWireCaps.d.ts +41 -0
  29. package/dist/mcpWireCaps.js +51 -0
  30. package/dist/modelBudgetRule.d.ts +44 -0
  31. package/dist/modelBudgetRule.js +73 -0
  32. package/dist/modelWireCaps.d.ts +17 -0
  33. package/dist/modelWireCaps.js +16 -0
  34. package/dist/notifications.d.ts +150 -4
  35. package/dist/notifications.js +472 -0
  36. package/dist/permissionWireCaps.d.ts +37 -0
  37. package/dist/permissionWireCaps.js +46 -0
  38. package/dist/promptProfileWireCaps.d.ts +17 -0
  39. package/dist/promptProfileWireCaps.js +20 -0
  40. package/dist/retainBackgroundWireCaps.d.ts +50 -0
  41. package/dist/retainBackgroundWireCaps.js +58 -0
  42. package/dist/rewindWireCaps.d.ts +36 -0
  43. package/dist/rewindWireCaps.js +41 -0
  44. package/dist/selfOrchestrationWireCaps.d.ts +40 -0
  45. package/dist/selfOrchestrationWireCaps.js +48 -0
  46. package/dist/sessionModelLatch.d.ts +35 -0
  47. package/dist/sessionModelLatch.js +63 -0
  48. package/dist/skillsWireCaps.d.ts +47 -0
  49. package/dist/skillsWireCaps.js +41 -0
  50. package/dist/sseIdleTriage.d.ts +94 -0
  51. package/dist/sseIdleTriage.js +142 -0
  52. package/dist/ultracodeWireCaps.d.ts +90 -0
  53. package/dist/ultracodeWireCaps.js +159 -0
  54. package/dist/webSearchWireCaps.d.ts +54 -0
  55. package/dist/webSearchWireCaps.js +76 -0
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -84,13 +84,35 @@ keys).
84
84
  0.2.0 is the repo/name migration: **no behaviour change**, both guard suites carried over and
85
85
  green (22 + 203 checks).
86
86
 
87
+ 0.3.0 moved in the **dependency-free pure functions and side-channel ledgers** (subagent content,
88
+ engine agent-panel, fleet panel projection, live question store, inline task stats, print
89
+ tool-result frame, caps cache, tool-label store, task-description projection) with 140 new
90
+ behaviour checks.
91
+
92
+ 0.4.0 moved in the **request-shaping gate family and the notification family**:
93
+
94
+ - `notifications.ts` absorbed the whole background-completion subsystem — the process-lifetime
95
+ dedup ledgers (model-notification vs completion-card are **separate key spaces**), the idle
96
+ completion watcher for workflow + background-agent runs, the outstanding-run counter with its
97
+ subscription, the own-run ledger used for frame ownership, the delivery-failure supplement, and
98
+ the `hook_notice` classifier half (ownership fail-closed mirrored from the server).
99
+ - 17 `*WireCaps` projections (attachments, client context, cloud config, fork, images, mcp, model,
100
+ permission, prompt profile, retain-background, rewind, self-orchestration, skills, ultracode,
101
+ web search, agents, plus the wire client factory), the SSE idle triage, the classifier-verdict
102
+ recogniser, the control router, and the model-budget/catalog/latch/effort rules.
103
+ - New `hostEnv.ts`: the single host-environment read port. The moved projections used to default
104
+ their `env` parameter to `process.env`, which throws in a browser; they now default to
105
+ `hostEnv()` (the very same object under Node, `{}` where there is no `process`).
106
+ - 🔴 One host assembly duty: `installNotificationQueuePort()`. Without it, task-notification
107
+ delivery is dropped — `notificationQueuePortMisses()` must stay 0.
108
+
87
109
  Known intentional deltas from the CLI are enumerated in `ADAPTER_DIVERGENCES`.
88
110
 
89
111
  ## Guards
90
112
 
91
113
  ```bash
92
114
  npm install
93
- node scripts/run-client-core-pure-test.mjs # 22 consumer-view checks; no CLI tree needed
115
+ node scripts/run-client-core-pure-test.mjs # 370 consumer-view checks (22 + 140 + 208); no CLI tree needed
94
116
  node scripts/run-client-core-diff-test.mjs # 203 checks: differential + replay-id + ledger
95
117
  ```
96
118
 
@@ -0,0 +1,112 @@
1
+ export declare const MAX_TASK_AGENTS = 32;
2
+ export declare const MAX_AGENT_NAME_CHARS = 64;
3
+ export declare const MAX_AGENT_TEXT_CHARS = 4096;
4
+ export declare const MAX_AGENT_SYSTEM_PROMPT_CHARS = 65536;
5
+ export declare const MAX_AGENT_TOOLS = 64;
6
+ export declare const MAX_AGENT_TOOL_NAME_CHARS = 128;
7
+ export declare const MAX_AGENT_SKILLS = 10;
8
+ export declare const MAX_AGENT_SKILL_CONTENT_CHARS = 1048576;
9
+ export declare const MAX_AGENT_MAX_TURNS = 1000;
10
+ /** server 1.205 TASK_AGENT_FIELD_SHAPES 的 15 字段白名单(permissionMode 显式拒收,不在表内)。 */
11
+ export declare const TASK_AGENT_WIRE_FIELDS: readonly ["name", "whenToUse", "whenToUseLean", "allowTools", "denyTools", "skills", "background", "isolation", "model", "thinking", "systemPrompt", "maxTurns", "memory", "observer", "observerMessage"];
12
+ /** core ThinkingLevel 词表(effortWire.ReasoningEffort 与之完全一致)。 */
13
+ export type WireThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
14
+ export type TaskAgentWireSkill = {
15
+ name: string;
16
+ description: string;
17
+ content: string;
18
+ };
19
+ /** 发往 server 的单个 agent 定义(白名单内字段的壳侧子集——memory/observer 面壳无语义,恒不发)。 */
20
+ export type TaskAgentWireItem = {
21
+ name: string;
22
+ whenToUse?: string;
23
+ allowTools?: string[];
24
+ denyTools?: string[];
25
+ skills?: TaskAgentWireSkill[];
26
+ background?: boolean;
27
+ isolation?: 'worktree';
28
+ model?: string;
29
+ thinking?: WireThinkingLevel;
30
+ systemPrompt?: string;
31
+ maxTurns?: number;
32
+ };
33
+ /** 投影输入的结构型(loadAgentsDir.AgentDefinition 的窄切片——纯模块不拉运行时依赖图)。 */
34
+ export type AgentDefLike = {
35
+ agentType: string;
36
+ whenToUse?: string;
37
+ tools?: string[];
38
+ disallowedTools?: string[];
39
+ skills?: string[];
40
+ model?: string;
41
+ effort?: unknown;
42
+ maxTurns?: number;
43
+ background?: boolean;
44
+ isolation?: 'worktree' | 'remote';
45
+ source: string;
46
+ getSystemPrompt?: (arg?: unknown) => string;
47
+ };
48
+ /** 纯投影的注入读面(单测注入 fixture;产品缺省真读面在 prepare 里拼)。 */
49
+ export type ProjectAgentsDeps = {
50
+ /** F5a 中央解析器切片:frontmatter model 词 → 真模型名或 'inherit'(warning 一次性)。 */
51
+ resolveModel: (frontmatterModel: string | undefined) => {
52
+ model: string;
53
+ warning?: string;
54
+ };
55
+ /** 壳 effort 值 → ThinkingLevel(词表外 → undefined 不发)。 */
56
+ effortToThinking: (effort: unknown) => WireThinkingLevel | undefined;
57
+ /** 已加载的 skill 全文(名字 join 用);undefined = 本次不点亮 skills 字段。 */
58
+ skillSpecs?: TaskAgentWireSkill[];
59
+ };
60
+ export type ProjectAgentsResult = {
61
+ agents: TaskAgentWireItem[] | undefined;
62
+ /** 剪裁/降级告警(调用层一次性打 stderr)。 */
63
+ warnings: string[];
64
+ };
65
+ /**
66
+ * 纯投影:壳 AgentDefinition[](F5a activeAgents)→ server 白名单载荷。built-in 恒过滤(引擎有
67
+ * 自己的内置 roster);形状违规壳侧收紧(server 是整请求 400,不能把脏字段交出去)。
68
+ */
69
+ export declare function projectAgentsForWire(defs: readonly AgentDefLike[], deps: ProjectAgentsDeps): ProjectAgentsResult;
70
+ /**
71
+ * SDK 0.0.52 client.capabilities()(GET /v1/capabilities,Capabilities 全类型面)→ 顶层
72
+ * `taskAgents` 布尔(SDK 宪法迁移批:raw fetch + body 裸读退役,改 typed 读)。
73
+ * true = 确证支持;false = 确证不支持(200 但 taskAgents !== true:旧 server 无该字段,或多租
74
+ * requirePrincipal fail-closed 报 false);undefined = 无法确证(不可达/超时/非 2xx/不可
75
+ * 解析 —— SDK 对这些统一抛错,catch 归一)⇒ 调用层 fail-soft 照发(旧 server 静默剥字段
76
+ * 无害;引擎真死 stream 自己 fail-loud)。
77
+ * 等价性:3s 预算改经 client timeoutMs;maxRetries=0 单发(否则 503 会按 SDK 缺省重试,探测预算
78
+ * 语义变形)。差分:SDK 恒 stamp x-agent-principal(缺省 'anon:shell-live'),原手抄缺 principal
79
+ * 时省略该头——principal-first 宪法姿势,fail-open 改善(engineWireSdk 头注记账)。
80
+ */
81
+ export declare function engineSupportsTaskAgents(baseUrl: string, opts?: {
82
+ fetchImpl?: typeof fetch;
83
+ timeoutMs?: number;
84
+ authToken?: string;
85
+ principal?: string;
86
+ }): Promise<boolean | undefined>;
87
+ export type WireConfig = {
88
+ baseUrl: string;
89
+ authToken?: string;
90
+ principal?: string;
91
+ };
92
+ /** IO 半场注入的产品投影闭包(F5a 定义加载 + model 解析 + skills join)。 */
93
+ export type ProjectorFn = () => Promise<ProjectAgentsResult>;
94
+ /**
95
+ * kick 探测 + 加载 + 投影(createLiveConversationClient 构造时调;幂等——同 baseUrl 只跑一次)。
96
+ */
97
+ export declare function prepareTaskAgentsWireWith(cfg: WireConfig, projector: ProjectorFn): Promise<void>;
98
+ /**
99
+ * 有界就绪门(liveClient.stream 首帧前 await;缺省 4s 上限)。prepare 未 kick(mock 车道/测试)或
100
+ * 超时 ⇒ 立即放行(本 turn 不带 agents,fail-soft)。永不抛。
101
+ */
102
+ export declare function awaitTaskAgentsWire(timeoutMs?: number): Promise<void>;
103
+ /**
104
+ * 同步读面(liveClient.toLiveRequest 消费):当前投影好的 agents 载荷。每次读取后 kick 一次后台
105
+ * re-project(热加载 next-turn freshness:watcher clear loadAgentsDir memoize 后,下一 turn 拿新定义;
106
+ * 缓存未清时 re-project 近零开销)。能力已确证 false ⇒ 恒 {}。
107
+ */
108
+ export declare function taskAgentsField(): {
109
+ agents?: TaskAgentWireItem[];
110
+ };
111
+ /** 测试钩子:清空模块态(prepare 缓存/一次性告警集)。 */
112
+ export declare function _resetAgentsWireForTest(): void;
@@ -0,0 +1,319 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/agentsWireCaps.ts 逐字搬入(依赖 engineWireSdk,同批)。
3
+ */
4
+ /**
5
+ * src/sema/agentsWireCaps.ts — F5b agents wire 纯半场(skillsWireCaps 同款拆分:规则/投影/探测/
6
+ * 模块态全在这,零产品依赖图;IO 半场见 agentsWire.ts——产品读面 dynamic import 后注入 projector)。
7
+ * 契约与决策的完整记账见 agentsWire.ts 头注释。
8
+ * (engineWireSdk 亦是纯半场——只 import 外部零依赖 SDK,不破本模块零产品图纪律。)
9
+ */
10
+ import { hostEnv } from './hostEnv.js';
11
+ import { makeEngineWireClient } from './engineWireSdk.js';
12
+ // ── server 1.205 契约常量(实拆 dist/spec-fields.js,数值逐字)────────────────────────────────────
13
+ export const MAX_TASK_AGENTS = 32;
14
+ export const MAX_AGENT_NAME_CHARS = 64;
15
+ export const MAX_AGENT_TEXT_CHARS = 4_096;
16
+ export const MAX_AGENT_SYSTEM_PROMPT_CHARS = 65_536;
17
+ export const MAX_AGENT_TOOLS = 64;
18
+ export const MAX_AGENT_TOOL_NAME_CHARS = 128;
19
+ export const MAX_AGENT_SKILLS = 10;
20
+ export const MAX_AGENT_SKILL_CONTENT_CHARS = 1_048_576;
21
+ export const MAX_AGENT_MAX_TURNS = 1_000;
22
+ /** server 1.205 TASK_AGENT_FIELD_SHAPES 的 15 字段白名单(permissionMode 显式拒收,不在表内)。 */
23
+ export const TASK_AGENT_WIRE_FIELDS = [
24
+ 'name',
25
+ 'whenToUse',
26
+ 'whenToUseLean',
27
+ 'allowTools',
28
+ 'denyTools',
29
+ 'skills',
30
+ 'background',
31
+ 'isolation',
32
+ 'model',
33
+ 'thinking',
34
+ 'systemPrompt',
35
+ 'maxTurns',
36
+ 'memory',
37
+ 'observer',
38
+ 'observerMessage',
39
+ ];
40
+ /** 五档来源优先级(managed > --agents flag > project > user > plugin)——32 帽裁剪的排序键。 */
41
+ const SOURCE_RANK = {
42
+ policySettings: 0,
43
+ flagSettings: 1,
44
+ projectSettings: 2,
45
+ userSettings: 3,
46
+ plugin: 4,
47
+ };
48
+ function sourceRank(source) {
49
+ return SOURCE_RANK[source] ?? 5;
50
+ }
51
+ /**
52
+ * 纯投影:壳 AgentDefinition[](F5a activeAgents)→ server 白名单载荷。built-in 恒过滤(引擎有
53
+ * 自己的内置 roster);形状违规壳侧收紧(server 是整请求 400,不能把脏字段交出去)。
54
+ */
55
+ export function projectAgentsForWire(defs, deps) {
56
+ const warnings = [];
57
+ const customs = defs
58
+ .filter(d => d.source !== 'built-in')
59
+ .slice()
60
+ .sort((a, b) => sourceRank(a.source) - sourceRank(b.source));
61
+ let capped = customs;
62
+ if (customs.length > MAX_TASK_AGENTS) {
63
+ capped = customs.slice(0, MAX_TASK_AGENTS);
64
+ const dropped = customs.slice(MAX_TASK_AGENTS).map(d => d.agentType);
65
+ warnings.push(`agents wire: ${customs.length} custom agents exceed the engine cap of ${MAX_TASK_AGENTS} — keeping the ${MAX_TASK_AGENTS} highest-precedence definitions, dropped: ${dropped.join(', ')}`);
66
+ }
67
+ const out = [];
68
+ const seen = new Set();
69
+ for (const def of capped) {
70
+ const name = def.agentType;
71
+ if (typeof name !== 'string' || name.length === 0)
72
+ continue;
73
+ if (name.length > MAX_AGENT_NAME_CHARS) {
74
+ // name 是身份,截断可能撞名——整个丢弃 + warn(server 对超长 name 是整请求 400)。
75
+ warnings.push(`agents wire: agent name "${name.slice(0, 40)}…" exceeds ${MAX_AGENT_NAME_CHARS} chars — definition skipped`);
76
+ continue;
77
+ }
78
+ if (seen.has(name))
79
+ continue; // server 对 duplicate name 整请求 400——防御性去重(activeAgents 已按名唯一)
80
+ seen.add(name);
81
+ const item = { name };
82
+ if (typeof def.whenToUse === 'string' && def.whenToUse.length > 0) {
83
+ item.whenToUse =
84
+ def.whenToUse.length > MAX_AGENT_TEXT_CHARS
85
+ ? def.whenToUse.slice(0, MAX_AGENT_TEXT_CHARS)
86
+ : def.whenToUse;
87
+ if (def.whenToUse.length > MAX_AGENT_TEXT_CHARS) {
88
+ warnings.push(`agents wire: agent "${name}" description truncated to ${MAX_AGENT_TEXT_CHARS} chars`);
89
+ }
90
+ }
91
+ const projectTools = (list, label) => {
92
+ if (!Array.isArray(list) || list.length === 0)
93
+ return undefined;
94
+ const kept = list.filter(t => typeof t === 'string' && t.length > 0 && t.length <= MAX_AGENT_TOOL_NAME_CHARS);
95
+ if (kept.length !== list.length) {
96
+ warnings.push(`agents wire: agent "${name}" ${label} had invalid entries — dropped`);
97
+ }
98
+ if (kept.length === 0)
99
+ return undefined; // 全剪空 ≠ 显式空单(空 allow 语义歧义,宁可回落继承)
100
+ if (kept.length > MAX_AGENT_TOOLS) {
101
+ warnings.push(`agents wire: agent "${name}" ${label} truncated to ${MAX_AGENT_TOOLS} entries`);
102
+ return kept.slice(0, MAX_AGENT_TOOLS);
103
+ }
104
+ return kept;
105
+ };
106
+ const allowTools = projectTools(def.tools, 'tools');
107
+ if (allowTools)
108
+ item.allowTools = allowTools;
109
+ const denyTools = projectTools(def.disallowedTools, 'disallowedTools');
110
+ if (denyTools)
111
+ item.denyTools = denyTools;
112
+ // skills:名字 → 已加载全文对象 join;join 不上剪掉 + warn。
113
+ if (Array.isArray(def.skills) && def.skills.length > 0 && deps.skillSpecs) {
114
+ const byName = new Map(deps.skillSpecs.map(s => [s.name.toLowerCase(), s]));
115
+ const joined = [];
116
+ const missing = [];
117
+ for (const skillName of def.skills) {
118
+ if (typeof skillName !== 'string' || skillName.length === 0)
119
+ continue;
120
+ const spec = byName.get(skillName.toLowerCase());
121
+ if (spec &&
122
+ spec.name.length <= MAX_AGENT_NAME_CHARS &&
123
+ spec.content.length > 0 &&
124
+ spec.content.length <= MAX_AGENT_SKILL_CONTENT_CHARS) {
125
+ joined.push(spec);
126
+ }
127
+ else {
128
+ missing.push(skillName);
129
+ }
130
+ }
131
+ if (missing.length > 0) {
132
+ warnings.push(`agents wire: agent "${name}" skills not found in the loaded skill set (dropped): ${missing.join(', ')}`);
133
+ }
134
+ if (joined.length > MAX_AGENT_SKILLS) {
135
+ warnings.push(`agents wire: agent "${name}" skills truncated to ${MAX_AGENT_SKILLS}`);
136
+ }
137
+ if (joined.length > 0)
138
+ item.skills = joined.slice(0, MAX_AGENT_SKILLS);
139
+ }
140
+ if (typeof def.background === 'boolean')
141
+ item.background = def.background;
142
+ if (def.isolation === 'worktree') {
143
+ item.isolation = 'worktree';
144
+ }
145
+ else if (def.isolation !== undefined) {
146
+ warnings.push(`agents wire: agent "${name}" isolation "${String(def.isolation)}" is not accepted by the engine (only "worktree") — dropped`);
147
+ }
148
+ // model:F5a 中央解析器已把档位词/CC 别名/catalog 名/完整 ID 解析成真模型名;'inherit' 不发
149
+ // (core 对 def.model 走 resolveModel fail-loud,'inherit' 字面会被判 unknown_model)。
150
+ const resolution = deps.resolveModel(def.model);
151
+ if (resolution.warning)
152
+ warnings.push(resolution.warning);
153
+ if (resolution.model !== 'inherit')
154
+ item.model = resolution.model;
155
+ const thinking = deps.effortToThinking(def.effort);
156
+ if (thinking)
157
+ item.thinking = thinking;
158
+ if (typeof def.getSystemPrompt === 'function') {
159
+ let prompt;
160
+ try {
161
+ prompt = def.getSystemPrompt();
162
+ }
163
+ catch {
164
+ prompt = undefined; // prompt 闭包抛错 fail-soft:agent 仍发(引擎给基本 env),别拖死整个载荷
165
+ }
166
+ if (typeof prompt === 'string' && prompt.length > 0) {
167
+ if (prompt.length > MAX_AGENT_SYSTEM_PROMPT_CHARS) {
168
+ warnings.push(`agents wire: agent "${name}" system prompt truncated to ${MAX_AGENT_SYSTEM_PROMPT_CHARS} chars`);
169
+ prompt = prompt.slice(0, MAX_AGENT_SYSTEM_PROMPT_CHARS);
170
+ }
171
+ item.systemPrompt = prompt;
172
+ }
173
+ }
174
+ if (def.maxTurns !== undefined) {
175
+ if (typeof def.maxTurns === 'number' &&
176
+ Number.isInteger(def.maxTurns) &&
177
+ def.maxTurns >= 1 &&
178
+ def.maxTurns <= MAX_AGENT_MAX_TURNS) {
179
+ item.maxTurns = def.maxTurns;
180
+ }
181
+ else {
182
+ warnings.push(`agents wire: agent "${name}" maxTurns ${String(def.maxTurns)} out of range (1..${MAX_AGENT_MAX_TURNS}) — dropped`);
183
+ }
184
+ }
185
+ out.push(item);
186
+ }
187
+ return { agents: out.length > 0 ? out : undefined, warnings };
188
+ }
189
+ // ── 能力探测(taskAgents capability,按 baseUrl 缓存)────────────────────────────────────────────
190
+ /**
191
+ * SDK 0.0.52 client.capabilities()(GET /v1/capabilities,Capabilities 全类型面)→ 顶层
192
+ * `taskAgents` 布尔(SDK 宪法迁移批:raw fetch + body 裸读退役,改 typed 读)。
193
+ * true = 确证支持;false = 确证不支持(200 但 taskAgents !== true:旧 server 无该字段,或多租
194
+ * requirePrincipal fail-closed 报 false);undefined = 无法确证(不可达/超时/非 2xx/不可
195
+ * 解析 —— SDK 对这些统一抛错,catch 归一)⇒ 调用层 fail-soft 照发(旧 server 静默剥字段
196
+ * 无害;引擎真死 stream 自己 fail-loud)。
197
+ * 等价性:3s 预算改经 client timeoutMs;maxRetries=0 单发(否则 503 会按 SDK 缺省重试,探测预算
198
+ * 语义变形)。差分:SDK 恒 stamp x-agent-principal(缺省 'anon:shell-live'),原手抄缺 principal
199
+ * 时省略该头——principal-first 宪法姿势,fail-open 改善(engineWireSdk 头注记账)。
200
+ */
201
+ export async function engineSupportsTaskAgents(baseUrl, opts) {
202
+ const client = makeEngineWireClient({
203
+ baseUrl,
204
+ ...(opts?.authToken ? { token: opts.authToken } : {}),
205
+ ...(opts?.principal ? { principal: opts.principal } : {}),
206
+ timeoutMs: opts?.timeoutMs ?? 3000,
207
+ ...(opts?.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
208
+ });
209
+ if (!client)
210
+ return undefined;
211
+ try {
212
+ const caps = await client.capabilities();
213
+ return caps?.taskAgents === true;
214
+ }
215
+ catch {
216
+ return undefined;
217
+ }
218
+ }
219
+ let activeProjector = null;
220
+ let preparedKey = null;
221
+ let preparedPromise = null;
222
+ let payload;
223
+ let capsVerdict; // 按 prepare 时的 baseUrl 缓存
224
+ const emittedWarnings = new Set();
225
+ function warnOnce(msg) {
226
+ if (emittedWarnings.has(msg))
227
+ return;
228
+ emittedWarnings.add(msg);
229
+ // eslint-disable-next-line no-console
230
+ console.error(`[sema] ${msg}`);
231
+ }
232
+ async function runPrepare(cfg, projector) {
233
+ try {
234
+ const [caps, projected] = await Promise.all([
235
+ engineSupportsTaskAgents(cfg.baseUrl, {
236
+ authToken: cfg.authToken,
237
+ principal: cfg.principal,
238
+ }),
239
+ projector(),
240
+ ]);
241
+ capsVerdict = caps;
242
+ if (projected.agents && caps === false) {
243
+ // 确证无能力:不发 + 一次性诚实告警(用户会误信自定义 agents 已生效)。
244
+ warnOnce(`agents wire: this engine does not support per-task custom agents (capabilities.taskAgents absent/false — server <1.205 or multi-tenant) — ${projected.agents.length} custom agent definition(s) will NOT be sent`);
245
+ payload = undefined;
246
+ return;
247
+ }
248
+ for (const w of projected.warnings)
249
+ warnOnce(w);
250
+ payload = projected.agents;
251
+ if (payload && hostEnv().SEMA_DEBUG) {
252
+ // eslint-disable-next-line no-console
253
+ console.error(`[sema] agents wire: ${payload.length} custom agent(s) → TaskRequest.agents (${payload.map(a => a.name).join(', ')})`);
254
+ }
255
+ }
256
+ catch (e) {
257
+ // 任何 prepare 失败 fail-soft:不发 agents,绝不拖死 turn。
258
+ payload = undefined;
259
+ if (hostEnv().SEMA_DEBUG) {
260
+ // eslint-disable-next-line no-console
261
+ console.error('[sema] agents wire prepare soft-failed (no agents stamped):', e);
262
+ }
263
+ }
264
+ }
265
+ /**
266
+ * kick 探测 + 加载 + 投影(createLiveConversationClient 构造时调;幂等——同 baseUrl 只跑一次)。
267
+ */
268
+ export function prepareTaskAgentsWireWith(cfg, projector) {
269
+ const key = cfg.baseUrl;
270
+ if (preparedPromise && preparedKey === key)
271
+ return preparedPromise;
272
+ preparedKey = key;
273
+ preparedPromise = runPrepare(cfg, projector);
274
+ activeProjector = projector;
275
+ return preparedPromise;
276
+ }
277
+ /**
278
+ * 有界就绪门(liveClient.stream 首帧前 await;缺省 4s 上限)。prepare 未 kick(mock 车道/测试)或
279
+ * 超时 ⇒ 立即放行(本 turn 不带 agents,fail-soft)。永不抛。
280
+ */
281
+ export async function awaitTaskAgentsWire(timeoutMs = 4000) {
282
+ if (!preparedPromise)
283
+ return;
284
+ await Promise.race([
285
+ preparedPromise.catch(() => { }),
286
+ new Promise(resolve => {
287
+ const t = setTimeout(resolve, timeoutMs);
288
+ t.unref?.();
289
+ }),
290
+ ]);
291
+ }
292
+ /**
293
+ * 同步读面(liveClient.toLiveRequest 消费):当前投影好的 agents 载荷。每次读取后 kick 一次后台
294
+ * re-project(热加载 next-turn freshness:watcher clear loadAgentsDir memoize 后,下一 turn 拿新定义;
295
+ * 缓存未清时 re-project 近零开销)。能力已确证 false ⇒ 恒 {}。
296
+ */
297
+ export function taskAgentsField() {
298
+ const current = payload;
299
+ // 后台刷新(fire-and-forget;capsVerdict 沿用——能力不随热加载变)
300
+ if (preparedPromise && capsVerdict !== false && activeProjector) {
301
+ void activeProjector()
302
+ .then(p => {
303
+ for (const w of p.warnings)
304
+ warnOnce(w);
305
+ payload = p.agents;
306
+ })
307
+ .catch(() => { });
308
+ }
309
+ return current ? { agents: current } : {};
310
+ }
311
+ /** 测试钩子:清空模块态(prepare 缓存/一次性告警集)。 */
312
+ export function _resetAgentsWireForTest() {
313
+ activeProjector = null;
314
+ preparedKey = null;
315
+ preparedPromise = null;
316
+ payload = undefined;
317
+ capsVerdict = undefined;
318
+ emittedWarnings.clear();
319
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/attachmentsWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/attachmentsWireCaps.ts — design/133 + G1 turn-boundary attachments WIRE pure-projection:
6
+ * the shell's intent → the top-level `TaskRequest.attachments` object the service normalizes
7
+ * (spec-fields.ts normalizeAttachments, @sema-ai/server 1.128.0) → core `TaskSpec.attachments`
8
+ * (all producers opt-in engine-side; core 1.253).
9
+ *
10
+ * DEFAULT ON for the TOC live path ([487]② — CC parity): CC's own /compact announces the still
11
+ * pending/running background tasks after compaction and notices newly-available deferred tools at
12
+ * boundaries — a TOC user gets that behavior without flipping anything. The default therefore
13
+ * carries the G1 announce pair `{backgroundTasks, toolsDelta}`:
14
+ * - both are ONE-SHOT/quiet engine-side (no compaction ⇒ backgroundTasks permanently silent; no
15
+ * deferred-tool materialization ⇒ toolsDelta permanently silent), so the default costs nothing
16
+ * on an ordinary session;
17
+ * - the design/133 reminder trio (todoReminder/changedFiles/planModeReminder) stays OPT-IN
18
+ * (`SEMA_ATTACHMENTS=full`) — core's own default-face judgment (evidence from autonomous legs
19
+ * only; interactive deployments stay byte-identical) is respected until a behavior leg says
20
+ * otherwise.
21
+ *
22
+ * Core wire contract (board [479] ask-2): literal-true unions — "off = DELETE the key, never send
23
+ * false". This projection never emits `false`; OFF ⇒ `undefined` ⇒ no `attachments` field at all.
24
+ *
25
+ * PURE — no env/file IO of its own (callers pass `env`), the verified wireCaps pattern. The CALLER
26
+ * (seamQuery / the print-mode ask) gates the read to LIVE mode so the mock request shape never
27
+ * changes.
28
+ */
29
+ import { type EnvLike } from './hostEnv.js';
30
+ /** The env key the shell reads. `off|0|false|none` ⇒ no stamp; `full|all` ⇒ all five producers;
31
+ * unset/anything else ⇒ the CC-parity default pair. */
32
+ export declare const ATTACHMENTS_ENV: "SEMA_ATTACHMENTS";
33
+ /** Mirror of core `TaskSpec.attachments` (types.ts:1139) — literal-true unions. */
34
+ export interface AttachmentsSpec {
35
+ todoReminder?: true;
36
+ changedFiles?: true | {
37
+ maxFiles?: number;
38
+ };
39
+ planModeReminder?: true;
40
+ backgroundTasks?: true;
41
+ toolsDelta?: true;
42
+ }
43
+ /**
44
+ * ENV source → the `TaskRequest.attachments` stamp. Returns `undefined` (⇒ no field) on explicit
45
+ * opt-out; the G1 pair by default; all five on `full`. Never `false` values (core contract).
46
+ */
47
+ export declare function attachmentsForRequest(env?: EnvLike): AttachmentsSpec | undefined;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/attachmentsWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/attachmentsWireCaps.ts — design/133 + G1 turn-boundary attachments WIRE pure-projection:
6
+ * the shell's intent → the top-level `TaskRequest.attachments` object the service normalizes
7
+ * (spec-fields.ts normalizeAttachments, @sema-ai/server 1.128.0) → core `TaskSpec.attachments`
8
+ * (all producers opt-in engine-side; core 1.253).
9
+ *
10
+ * DEFAULT ON for the TOC live path ([487]② — CC parity): CC's own /compact announces the still
11
+ * pending/running background tasks after compaction and notices newly-available deferred tools at
12
+ * boundaries — a TOC user gets that behavior without flipping anything. The default therefore
13
+ * carries the G1 announce pair `{backgroundTasks, toolsDelta}`:
14
+ * - both are ONE-SHOT/quiet engine-side (no compaction ⇒ backgroundTasks permanently silent; no
15
+ * deferred-tool materialization ⇒ toolsDelta permanently silent), so the default costs nothing
16
+ * on an ordinary session;
17
+ * - the design/133 reminder trio (todoReminder/changedFiles/planModeReminder) stays OPT-IN
18
+ * (`SEMA_ATTACHMENTS=full`) — core's own default-face judgment (evidence from autonomous legs
19
+ * only; interactive deployments stay byte-identical) is respected until a behavior leg says
20
+ * otherwise.
21
+ *
22
+ * Core wire contract (board [479] ask-2): literal-true unions — "off = DELETE the key, never send
23
+ * false". This projection never emits `false`; OFF ⇒ `undefined` ⇒ no `attachments` field at all.
24
+ *
25
+ * PURE — no env/file IO of its own (callers pass `env`), the verified wireCaps pattern. The CALLER
26
+ * (seamQuery / the print-mode ask) gates the read to LIVE mode so the mock request shape never
27
+ * changes.
28
+ */
29
+ import { hostEnv } from './hostEnv.js';
30
+ /** The env key the shell reads. `off|0|false|none` ⇒ no stamp; `full|all` ⇒ all five producers;
31
+ * unset/anything else ⇒ the CC-parity default pair. */
32
+ export const ATTACHMENTS_ENV = 'SEMA_ATTACHMENTS';
33
+ /**
34
+ * ENV source → the `TaskRequest.attachments` stamp. Returns `undefined` (⇒ no field) on explicit
35
+ * opt-out; the G1 pair by default; all five on `full`. Never `false` values (core contract).
36
+ */
37
+ export function attachmentsForRequest(env = hostEnv()) {
38
+ const raw = (env[ATTACHMENTS_ENV] ?? '').trim().toLowerCase();
39
+ if (raw === 'off' || raw === '0' || raw === 'false' || raw === 'none')
40
+ return undefined;
41
+ if (raw === 'full' || raw === 'all') {
42
+ return { todoReminder: true, changedFiles: true, planModeReminder: true, backgroundTasks: true, toolsDelta: true };
43
+ }
44
+ // default (TOC CC parity): post-compact background-task recap + deferred-tools boundary notice.
45
+ return { backgroundTasks: true, toolsDelta: true };
46
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/classifierVerdictWire.ts 的
3
+ * **判定半场**搬入(帧识别 + 文案),`surfaceClassifierDeny()` 的**执行半场**留宿主
4
+ * (它写 Ink `AppState.notifications` 并 lazy `require('../utils/autoModeDenials.js')`——
5
+ * 两者都是 TUI/Node 私有面,进不了端无关包)。
6
+ * 🔴 本文件零模块级可变状态(只有常量),故不受 B1「写读口必须同实例」纪律约束:壳侧留
7
+ * 执行半场、判定走本包,是安全的拆缝(B6 T35/T36/T44 同款「判定半场搬入」姿势)。
8
+ * 逐字纪律:CLASSIFIER_DENY_SIGNATURE / NOTICE_REASON_MAX(80)/ 通知行三段格式一字节不改。
9
+ */
10
+ /**
11
+ * src/sema/classifierVerdictWire.ts — auto-mode 分类器裁决帧的壳侧消费(分类器批3 壳半场,黑板 [907])。
12
+ *
13
+ * WIRE 形状(gap-check 2026-07-16 实勘,core 1.279 dist 亲核):
14
+ * 引擎侧分类器 block(core hooks.js runToolGate)→ decision = deny + decisionReason:"classifier",
15
+ * permissionDenied 钩子收 source:"classifier"(server 1.209 只作 metrics 计量,不出独立 deny 帧)——
16
+ * 壳能看见的唯一 wire 事实 = `tool_end` 帧:
17
+ * { type:'tool_end', isError:true, output: [{type:'text', text:'<system-reminder>\n
18
+ * auto-mode classifier blocked this call[: <reason>|: [<category>]]\n</system-reminder>'}] }
19
+ * (core hooks.js L138 deny message 逐字 + formatHookFeedback 包 system-reminder;
20
+ * agent-loop createErrorToolResult 落 content 数组;runtask toolOutputFrom 原样上 wire。)
21
+ * ⇒ 消费面按此签名结构性识别,等价于「deny 帧带 source:'classifier'」的判定(签名字符串是
22
+ * core 的机器产文面,非模型作文——模型 echo 不会以该签名做 tool_end 错误体开头)。
23
+ *
24
+ * 🔴 fail-soft 铁律:识别不中一律静默走原路,绝不伤害事件流。
25
+ * 🔴 UNTRUSTED:reason 为分类器(模型)产文,只渲染绝不回喂;通知行截断 ≤80(207 同款)。
26
+ */
27
+ /** core hooks.js 分类器 deny message 的机器签名(逐字锚;lockstep:core 改文案这里必须跟)。 */
28
+ export declare const CLASSIFIER_DENY_SIGNATURE = "auto-mode classifier blocked this call";
29
+ export interface ClassifierDenyVerdict {
30
+ /** core 裁决原文(已去 system-reminder 包裹),= render 面 stamp 体。 */
31
+ message: string;
32
+ /** 签名后的 reason 尾巴(`: ` 之后),无则 undefined。 */
33
+ reason?: string;
34
+ }
35
+ /**
36
+ * 结构性识别一张 tool_end 帧是否为分类器 deny 裁决帧。
37
+ * 判定 = isError:true ∧ 错误体(去包裹后)以 core 机器签名开头。识别不中 ⇒ undefined(原路)。
38
+ */
39
+ export declare function classifierDenyFromToolEnd(ev: {
40
+ type?: unknown;
41
+ isError?: unknown;
42
+ output?: unknown;
43
+ }): ClassifierDenyVerdict | undefined;
44
+ /** 通知行文案(exported for tests;CC 207 三段 text 化)。 */
45
+ export declare function classifierDenyNoticeText(toolName: string, reason: string | undefined): string;