@sema-agent/client-core 0.3.0 → 0.5.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 (69) hide show
  1. package/README.md +57 -4
  2. package/dist/adapt.d.ts +30 -3
  3. package/dist/adapt.js +490 -354
  4. package/dist/adapter/downstream/eventToSdkMessage.d.ts +81 -0
  5. package/dist/adapter/downstream/eventToSdkMessage.js +330 -0
  6. package/dist/adapter/downstream/terminalToSdkResult.d.ts +71 -0
  7. package/dist/adapter/downstream/terminalToSdkResult.js +370 -0
  8. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +26 -0
  9. package/dist/adapter/downstream/turnUsageToModelUsage.js +12 -0
  10. package/dist/adapter/runStream.d.ts +38 -0
  11. package/dist/adapter/runStream.js +177 -0
  12. package/dist/adapter/types.d.ts +73 -0
  13. package/dist/adapter/types.js +23 -0
  14. package/dist/agentsWireCaps.d.ts +112 -0
  15. package/dist/agentsWireCaps.js +319 -0
  16. package/dist/attachmentsWireCaps.d.ts +47 -0
  17. package/dist/attachmentsWireCaps.js +46 -0
  18. package/dist/classifierVerdictWire.d.ts +45 -0
  19. package/dist/classifierVerdictWire.js +76 -0
  20. package/dist/clientContextWireCaps.d.ts +46 -0
  21. package/dist/clientContextWireCaps.js +48 -0
  22. package/dist/cloudConfigWireCaps.d.ts +110 -0
  23. package/dist/cloudConfigWireCaps.js +228 -0
  24. package/dist/controlRouter.d.ts +191 -0
  25. package/dist/controlRouter.js +244 -0
  26. package/dist/effortWire.d.ts +34 -0
  27. package/dist/effortWire.js +38 -0
  28. package/dist/engineWireSdk.d.ts +49 -0
  29. package/dist/engineWireSdk.js +61 -0
  30. package/dist/forkWireCaps.d.ts +25 -0
  31. package/dist/forkWireCaps.js +42 -0
  32. package/dist/hostEnv.d.ts +16 -0
  33. package/dist/hostEnv.js +17 -0
  34. package/dist/imagesWireCaps.d.ts +28 -0
  35. package/dist/imagesWireCaps.js +49 -0
  36. package/dist/index.d.ts +52 -0
  37. package/dist/index.js +62 -0
  38. package/dist/liveModelCatalog.d.ts +62 -0
  39. package/dist/liveModelCatalog.js +79 -0
  40. package/dist/mcpWireCaps.d.ts +41 -0
  41. package/dist/mcpWireCaps.js +51 -0
  42. package/dist/modelBudgetRule.d.ts +44 -0
  43. package/dist/modelBudgetRule.js +73 -0
  44. package/dist/modelWireCaps.d.ts +17 -0
  45. package/dist/modelWireCaps.js +16 -0
  46. package/dist/notifications.d.ts +150 -4
  47. package/dist/notifications.js +472 -0
  48. package/dist/permissionWireCaps.d.ts +37 -0
  49. package/dist/permissionWireCaps.js +46 -0
  50. package/dist/promptProfileWireCaps.d.ts +17 -0
  51. package/dist/promptProfileWireCaps.js +20 -0
  52. package/dist/retainBackgroundWireCaps.d.ts +50 -0
  53. package/dist/retainBackgroundWireCaps.js +58 -0
  54. package/dist/rewindWireCaps.d.ts +36 -0
  55. package/dist/rewindWireCaps.js +41 -0
  56. package/dist/seam.d.ts +97 -2
  57. package/dist/selfOrchestrationWireCaps.d.ts +40 -0
  58. package/dist/selfOrchestrationWireCaps.js +48 -0
  59. package/dist/sessionModelLatch.d.ts +35 -0
  60. package/dist/sessionModelLatch.js +63 -0
  61. package/dist/skillsWireCaps.d.ts +47 -0
  62. package/dist/skillsWireCaps.js +41 -0
  63. package/dist/sseIdleTriage.d.ts +94 -0
  64. package/dist/sseIdleTriage.js +142 -0
  65. package/dist/ultracodeWireCaps.d.ts +90 -0
  66. package/dist/ultracodeWireCaps.js +159 -0
  67. package/dist/webSearchWireCaps.d.ts +54 -0
  68. package/dist/webSearchWireCaps.js +76 -0
  69. package/package.json +2 -2
@@ -0,0 +1,142 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/sseIdleTriage.ts 逐字搬入(只 import SDK)。
3
+ */
4
+ /**
5
+ * src/sema/sseIdleTriage.ts — turn-stream SSE idle-silence triage(TB2.0 反馈批 D · P0-2 壳半场,2026-07-15)。
6
+ *
7
+ * WHY: TB2.0 bench 真实案例 —— agent 长跑前台编译 ~9.5min,工具完成前壳侧 45s 空闲监测器
8
+ * (`@sema-agent/sdk` readSseFrames HEARTBEAT_GRACE_MS)命中 `SSE: no data for 45000ms (heartbeat
9
+ * silence)` → 整轮 rc=1 丢弃,引擎不知情继续跑完(access log 499)。分诊报告判决①修正段拍定:
10
+ * 壳侧独立可发 = S1(SDK grace 透传+typed SseIdleError,0.0.47 已发)+ S4(grace 旋钮)+
11
+ * S3 缩水版(engine-alive-idle 分诊,只改报错文案,不重连)。本模块 = S4 + S3 的共享件。
12
+ *
13
+ * S4 · SEMA_SSE_HEARTBEAT_GRACE_MS(settings.json `env` 块 → 1a-envseed → process.env,其它 wire
14
+ * 旋钮同一车道):整数毫秒,合法域 [5000, 600000];非法/缺省 = 不透传(SDK 默认 45s)——静默
15
+ * fail-soft,一个 settings typo 绝不 brick 任何 run(scenarioWire settings-lane 纪律)。
16
+ * ⚠️ 作用面 = 会话 turn 流(tasks.stream / liveClient)一条车道。SDK 控制通道(SSETransport.ts:21
17
+ * LIVENESS_TIMEOUT_MS=45s)自带重连+give-up 自愈,本旋钮【不】达那条车道——这不是全局 grace。
18
+ *
19
+ * S3 缩水版 · engine-alive-idle 分诊:liveClient 的 tasks.stream 包装 catch 到 SseIdleError 时,
20
+ * 先判 signal.aborted(用户中止契约先行,seamQuery.ts:1073——Esc 撕流绝不被分诊吞),再 GET
21
+ * `/health`(非 /healthz;server dist server.js:290)短超时探引擎活性:引擎死 → 原样抛(现行
22
+ * engine-down/not-restart 路径不变);引擎活 → 给同一错误对象打 engine-alive-idle 标记,
23
+ * seamQuery 的 tearVerdict 恢复腿据此渲染诚实分诊文案(engineAliveIdleContent)。
24
+ *
25
+ * 【本模块绝不重连】(判决①修正段圣旨,范围随 M2 精化):非 detach 车道上 tasks/stream 客户端
26
+ * 断连 = server onDisconnect ac.abort() 终止 run(anti token-burn by design),且该车道帧无 id
27
+ * 不可位置续传——空闲监测器抛出的那一刻 finally 已 cancel res.body,run 已被这次断连自身中止,
28
+ * 同 key in-flight 重放拿到的只是 aborted 结局。前台另有既有裁决:mid-turn 工具副作用非幂等,
29
+ * 绝不自动重发。引擎 E2(server ≥1.196 x-detach-on-disconnect,[854]①)已到货:headless `-p`
30
+ * detach 车道的 R1 换道重连 = 重连腿 M2,住在 headlessReconnectWire.ts(liveClient 管线最内层,
31
+ * 只在 detach 头已发出的流上武装);本模块保持「探活+打标,不重连」——交互 REPL 与一切非
32
+ * detach 车道的取消契约一根毛不动,M2 耗尽预算后判死文案也仍由这里的打标驱动。
33
+ */
34
+ import { hostEnv } from './hostEnv.js';
35
+ import { SseIdleError, probeHealth as sdkProbeHealth } from '@sema-agent/sdk';
36
+ /** Settings-lane env knob(settings.json `env` 块 → 1a-envseed → process.env)。整数毫秒。 */
37
+ export const SSE_HEARTBEAT_GRACE_ENV = 'SEMA_SSE_HEARTBEAT_GRACE_MS';
38
+ /** 合法域下界:低于 5s 会把正常 token 间隙误判成静默。 */
39
+ export const SSE_GRACE_MIN_MS = 5_000;
40
+ /** 合法域上界:10min——超过引擎自身各级 idle 顶,再长没有意义。 */
41
+ export const SSE_GRACE_MAX_MS = 600_000;
42
+ /** SDK 默认(readSseFrames HEARTBEAT_GRACE_MS)——文案引用用,不在本模块生效。 */
43
+ export const SDK_DEFAULT_GRACE_MS = 45_000;
44
+ /**
45
+ * S4 — resolve the turn-stream heartbeat grace override. Unset/empty → undefined(SDK 默认 45s,
46
+ * 不透传);非纯十进制整数 / 超出 [5000,600000] → undefined(静默落默认,fail-soft)。
47
+ */
48
+ export function resolveSseHeartbeatGraceMs(env = hostEnv()) {
49
+ const raw = env[SSE_HEARTBEAT_GRACE_ENV]?.trim();
50
+ if (!raw)
51
+ return undefined;
52
+ if (!/^\d+$/.test(raw))
53
+ return undefined; // 纯十进制整数——'45e3'/'45000.5'/'-1' 全部落默认
54
+ const n = Number(raw);
55
+ if (!Number.isSafeInteger(n) || n < SSE_GRACE_MIN_MS || n > SSE_GRACE_MAX_MS)
56
+ return undefined;
57
+ return n;
58
+ }
59
+ /** 给 SseIdleError 打 engine-alive-idle 标(引擎 /health 已确认活着)。返回同一对象。 */
60
+ export function markEngineAliveIdle(err) {
61
+ ;
62
+ err.semaTearVerdict = 'engine-alive-idle';
63
+ return err;
64
+ }
65
+ /**
66
+ * engine-alive-idle 判型:命中返回静默时长 elapsedMs(SseIdleError 0.0.47 自带;缺失兜 0),
67
+ * 未命中返回 undefined。seamQuery tearVerdict 腿与 headless `-p` 的 stderr 面共用。
68
+ */
69
+ export function engineAliveIdleElapsedMs(err) {
70
+ const e = err;
71
+ if (!e || e.semaTearVerdict !== 'engine-alive-idle')
72
+ return undefined;
73
+ return typeof e.elapsedMs === 'number' && Number.isFinite(e.elapsedMs) ? e.elapsedMs : 0;
74
+ }
75
+ /**
76
+ * SseIdleError 判型:instanceof 为主;双包风险(bundle 内外两份 SDK class identity 分叉)下
77
+ * name 兜底——name 由 SDK 构造器冻结为 'SseIdleError',非我方铸造。
78
+ */
79
+ export function isSseIdleError(err) {
80
+ if (err instanceof SseIdleError)
81
+ return true;
82
+ return err?.name === 'SseIdleError';
83
+ }
84
+ /**
85
+ * 引擎活性探针 — SDK probeHealth(GET `${baseUrl}/health`;端点是 /health 非 /healthz),默认 2s
86
+ * 预算,永不抛:alive = 200 且 status==='ok';非 2xx / 不可达 / 超时 = not alive(引擎死 → 现行
87
+ * 失败路径,分诊绝不让真死引擎空等)。与 engineUpgradeCoordinator 的 classifyStreamTear 同款姿势。
88
+ * SDK 宪法迁移批差分记账:原实现只看 res.ok(200 非 JSON / status!=='ok' 也判活);SDK 额外要
89
+ * status==='ok'——service /health 200 恒报 status:'ok',契约面等价,对「200 但引擎自述非 ok」的
90
+ * 病态更严格诚实。
91
+ */
92
+ export async function probeEngineAlive(baseUrl, timeoutMs = 2_000) {
93
+ return (await sdkProbeHealth(baseUrl, timeoutMs)).alive;
94
+ }
95
+ /**
96
+ * S3 缩水版 — the tasks.stream diagnose wrapper(liveClient 出口整流罩)。
97
+ * 透传 inner 的每个事件;仅当 inner 以 SseIdleError 收场时介入:
98
+ * 1. signal.aborted 先行 — 用户中止(Esc)撕流原样抛,绝不探活/绝不改判(seamQuery.ts:1073 契约);
99
+ * 2. GET /health 探引擎活性(2s 预算);
100
+ * 3. 引擎活 → 同一错误对象打 engine-alive-idle 标后重抛(下游 tearVerdict 腿改文案;detach `-p`
101
+ * 车道的 R1 重连发生在更内层的 headlessReconnectWire——错误能到这里 = 重连已耗尽/不可换道);
102
+ * 引擎死/探测失败 → 原样重抛(现行 classifyStreamTear 会诚实判 engine-down)。
103
+ * 非 SseIdleError 一律原样穿透。
104
+ */
105
+ export async function* diagnoseSseIdleTear(inner, baseUrl, signal) {
106
+ try {
107
+ yield* inner;
108
+ }
109
+ catch (err) {
110
+ if (!signal?.aborted && isSseIdleError(err) && (await probeEngineAlive(baseUrl))) {
111
+ throw markEngineAliveIdle(err);
112
+ }
113
+ throw err;
114
+ }
115
+ }
116
+ /**
117
+ * engine-alive-idle 的用户可见分诊文案(升级自裸 "API Error: SSE: no data for …" 行)。
118
+ * 措辞如实(判决②契约),按 detach 态分叉:
119
+ * · 非 detach(默认/旋钮关/版本门拒/交互车道):tasks/stream 断连 = server 端 abort 这次 run——
120
+ * 不许说"任务仍在继续"的安慰话;能给的出路只有 grace 旋钮 + 手动重发。字节与批 D 发运稿一致
121
+ * (存量脚本可能 grep)。
122
+ * · detach 态(重连腿 M1,`x-detach-on-disconnect: true` 已随首次 POST 发出,server ≥1.196):
123
+ * 断连【不再】abort run——run 已 detach,引擎会把它后台跑完并落 durable 账本;文案如实改述并
124
+ * 指 `GET /v1/runs/<id>`(taskId 已知时带上)/ `sema resume` 查终局。M1 不重连(M2/M3),
125
+ * 本轮前台 rc 仍非零——这是净改善:结果不再整轮丢弃。
126
+ */
127
+ export function engineAliveIdleContent(elapsedMs, detach) {
128
+ const secs = Math.max(1, Math.round((elapsedMs > 0 ? elapsedMs : SDK_DEFAULT_GRACE_MS) / 1000));
129
+ const graceTail = `If your tasks legitimately go quiet this long (e.g. a long build), raise the idle grace: set ${SSE_HEARTBEAT_GRACE_ENV} ` +
130
+ `(settings env, integer ms, ${SSE_GRACE_MIN_MS}-${SSE_GRACE_MAX_MS}; default ${SDK_DEFAULT_GRACE_MS}).`;
131
+ if (detach?.detached) {
132
+ const runRef = detach.taskId ? `GET /v1/runs/${detach.taskId}` : 'GET /v1/runs/<task-id>';
133
+ return (`The engine is still running, but this turn's event stream went silent for ${secs}s, so the connection was closed as idle. ` +
134
+ `This run was DETACHED (x-detach-on-disconnect): the disconnect did NOT abort it — the engine keeps running it to completion in the background ` +
135
+ `and records the outcome durably. Check the final result with \`${runRef}\` on the engine, or \`sema resume\` to pick the session back up. ` +
136
+ graceTail);
137
+ }
138
+ return (`The engine is still running, but this turn's event stream went silent for ${secs}s, so the connection was closed as idle — ` +
139
+ `and closing the stream aborts the run on the engine side (this turn did NOT finish; a long-running tool may have been killed mid-flight). ` +
140
+ graceTail +
141
+ ` The session is intact — resend the prompt to retry.`);
142
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/ultracodeWireCaps.ts 逐字搬入(零依赖;🔴 模块级 ultracodePreset 是 sticky 单实例态,写口 setUltracodePreset 与读口 ultracodeForWire 必须同实例)。
3
+ */
4
+ /**
5
+ * src/sema/ultracodeWireCaps.ts — the shell's TWO opt-in sources for the CC-parity `settings.ultracode` PRESET,
6
+ * projected onto `TaskRequest.settings.ultracode` (a typed boolean the SERVICE — ai-agent-service 1.48.0 — reads
7
+ * and EXPANDS to `thinking:"max"` + `selfOrchestration:true`; both core's existing orthogonal axes, fail-closed
8
+ * gates unchanged). design/111 (core/search AI 2026-06-30): ultracode is a STANDALONE settings boolean folding
9
+ * "max thinking × workflow orchestration", surfaced in sema as the top notch of the `/effort` dial.
10
+ *
11
+ * Two sources, ONE structured-spec output (`settings.ultracode:true` — never prompt text):
12
+ *
13
+ * ① L2 dial preset (STICKY): the `/effort ultracode` pick. The effort dial COERCES the reasoning axis to `xhigh`
14
+ * (overrides/effort.tsx executeEffortSema — "ultracode runs at xhigh effort"), which would LOSE the
15
+ * orchestration axis. So the dial separately latches this module-level preset: ON when `ultracode` is picked,
16
+ * OFF when any OTHER effort level is picked (a sticky session setting, like CC's settings.ultracode boolean).
17
+ *
18
+ * ② L3 keyword (PER-TURN): a client-side scan of the REAL human turn input for the literal word "ultracode".
19
+ * 🔴 RED LINE (design/111 L3, the ONLY injection risk): scan ONLY the human's current-turn submission, BEFORE
20
+ * any untrusted content (tool results / file reads / web / subagent / MCP) is concatenated. NEVER scan the
21
+ * assembled prompt server-side — a malicious file containing "ultracode" would be a privilege-escalation
22
+ * injection. The keyword is a UX convenience switch, NOT a security decision point: privilege STILL passes the
23
+ * service's fail-closed per-principal `allowWorkflows` gate (core deliberately refuses prompt scanning,
24
+ * prepare-task.ts:1612 — that boundary is correct). The output is a STRUCTURED spec (the boolean), never
25
+ * injected prompt text.
26
+ *
27
+ * Both sources are OR'd live-side by `toTaskRequest`; the SAME `SEMA_LIVE_BASEURL` gate the other capability wires
28
+ * use keeps the mock request shape unchanged (the stamp IS the opt-in; absent ⇒ the engine never expands ultracode).
29
+ *
30
+ * The PRESET state is module-level (mirrors seamQuery's seam* injectors): the dial (a React override) latches it,
31
+ * the seam reads it. The KEYWORD scan is a PURE function (caller passes the human input) so it bundles + unit-tests
32
+ * without React/AppState.
33
+ */
34
+ import { type EnvLike } from './hostEnv.js';
35
+ /** Latch the dial preset. `effort.tsx` calls this whenever an effort level is APPLIED: `true` for the `ultracode`
36
+ * notch, `false` for every other level (so switching the dial OFF ultracode clears the workflow-orchestration axis
37
+ * while the reasoning axis follows the picked level). */
38
+ export declare function setUltracodePreset(on: boolean): void;
39
+ /** Read the sticky dial preset (the L2 source). */
40
+ export declare function getUltracodePreset(): boolean;
41
+ /** Test/boot reset — clears the latched preset (e.g. a fresh session). */
42
+ export declare function _resetUltracodePreset(): void;
43
+ /**
44
+ * L3 keyword source: `true` iff the REAL human turn input contains the "ultracode" keyword, else `undefined`.
45
+ * 🔴 The caller MUST pass ONLY the human's current-turn submission (seamQuery's `objective`, derived from the
46
+ * projectable user message) — NOT any assembled/untrusted content. Returns `true | undefined` (never `false`) to
47
+ * mirror the strict additive-stamp discipline of the other wire caps (a stamped `false` would needlessly change
48
+ * the default request shape).
49
+ */
50
+ export declare function ultracodeFromInput(humanInput: string | undefined): true | undefined;
51
+ /**
52
+ * Combine the two sources into the structured spec the seam stamps. `true` when EITHER the sticky dial preset is on
53
+ * OR the human's current-turn input contains the keyword; otherwise `undefined` (⇒ no stamp ⇒ default behaviour).
54
+ * Returns `true | undefined` so the seam can spread `...(ultracode ? { ultracode } : {})` without changing the
55
+ * default request shape. The CALLER gates this to LIVE mode (mock parity).
56
+ */
57
+ export declare function ultracodeForRequest(humanInput: string | undefined): true | undefined;
58
+ export declare function workflowFromInput(humanInput: string | undefined): true | undefined;
59
+ /** Env escape hatch for the deferred-Workflow factory default: `SEMA_WORKFLOW_DEFER=0/false/no/off` restores the
60
+ * pre-[1052] always-exposed shape (schema bytes back in the prefix). Anything else (incl. unset) = defer ON. */
61
+ export declare const WORKFLOW_DEFER_ENV: "SEMA_WORKFLOW_DEFER";
62
+ /**
63
+ * [1052]① 收官形(clay 裁 B 的 core deferred 机制件,cli 接线半场):Workflow「全局默认开但不暴露」——
64
+ * 未命中激活源的 TaskRequest 带 `deferTools:["Workflow"]`(core 1.314 对已挂载工具做 design/36 延迟披露:
65
+ * 占位符上 wire、schema 字节撤出缓存前缀、ToolSearch 可激活=中途激活通路);命中任一激活源 ⇒ `undefined`
66
+ * (不 defer=名字+schema 全量上 wire):
67
+ * a) 用户当轮说 "workflow"(workflowFromInput,只扫真人输入);
68
+ * b) "ultracode" 关键词(L3,同一红线);
69
+ * c) `/effort ultracode` sticky dial(L2 preset)。
70
+ * CALLER 前置两门:live 门(mock 形不变)+ selfOrchestration stamp 存在(opt-out 时 Workflow 根本不挂载,
71
+ * defer 无意义)。前缀缓存注记:defer↔不 defer 的翻转只发生在真人激活当轮,与 CC 209 ToolSearch materialize
72
+ * 同一形——非激活轮的前缀指纹稳定([1044]② clay 硬约束 ii 的解)。
73
+ */
74
+ export declare function workflowDeferForRequest(humanInput: string | undefined, env?: EnvLike): ['Workflow'] | undefined;
75
+ /** [1077]④ 低频工具惰性注入:`SEMA_DEFER_TOOLS=<csv>`(wire 名)追加进 deferTools(core 1.314
76
+ * 延迟披露腿)。**缺省翻转已裁(clay [1212]⑤,2026-07-19)**:九件低频工具缺省 defer——判决数=
77
+ * -2,484 tok/首调用+多轮 gate 零行为退化;defer≠禁用(占位符在,模型 ToolSearch 随取)。
78
+ * 逃生门:`SEMA_DEFER_LOWFREQ=0/false/no/off` 关缺省名单(九件恢复全量上前缀)。 */
79
+ export declare const DEFER_TOOLS_ENV: "SEMA_DEFER_TOOLS";
80
+ export declare const DEFER_LOWFREQ_ENV: "SEMA_DEFER_LOWFREQ";
81
+ /** clay 裁定的缺省惰性名单([1076]④ 评测九件原单,[1090]② 判决数)。 */
82
+ export declare const DEFAULT_LOWFREQ_DEFER: readonly string[];
83
+ /**
84
+ * The FULL deferTools list for a request: the Workflow default-defer lane (activation sources, see
85
+ * {@link workflowDeferForRequest}) + the DEFAULT low-frequency list (clay [1212]⑤, opt-out via
86
+ * SEMA_DEFER_LOWFREQ) + the SEMA_DEFER_TOOLS extra list, deduped. An activation hit removes ONLY
87
+ * Workflow (the user asked for it by name); the other lanes are source-independent. Empty ⇒
88
+ * `undefined` (no stamp, request shape unchanged).
89
+ */
90
+ export declare function deferToolsForRequest(humanInput: string | undefined, env?: EnvLike): string[] | undefined;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/ultracodeWireCaps.ts 逐字搬入(零依赖;🔴 模块级 ultracodePreset 是 sticky 单实例态,写口 setUltracodePreset 与读口 ultracodeForWire 必须同实例)。
3
+ */
4
+ /**
5
+ * src/sema/ultracodeWireCaps.ts — the shell's TWO opt-in sources for the CC-parity `settings.ultracode` PRESET,
6
+ * projected onto `TaskRequest.settings.ultracode` (a typed boolean the SERVICE — ai-agent-service 1.48.0 — reads
7
+ * and EXPANDS to `thinking:"max"` + `selfOrchestration:true`; both core's existing orthogonal axes, fail-closed
8
+ * gates unchanged). design/111 (core/search AI 2026-06-30): ultracode is a STANDALONE settings boolean folding
9
+ * "max thinking × workflow orchestration", surfaced in sema as the top notch of the `/effort` dial.
10
+ *
11
+ * Two sources, ONE structured-spec output (`settings.ultracode:true` — never prompt text):
12
+ *
13
+ * ① L2 dial preset (STICKY): the `/effort ultracode` pick. The effort dial COERCES the reasoning axis to `xhigh`
14
+ * (overrides/effort.tsx executeEffortSema — "ultracode runs at xhigh effort"), which would LOSE the
15
+ * orchestration axis. So the dial separately latches this module-level preset: ON when `ultracode` is picked,
16
+ * OFF when any OTHER effort level is picked (a sticky session setting, like CC's settings.ultracode boolean).
17
+ *
18
+ * ② L3 keyword (PER-TURN): a client-side scan of the REAL human turn input for the literal word "ultracode".
19
+ * 🔴 RED LINE (design/111 L3, the ONLY injection risk): scan ONLY the human's current-turn submission, BEFORE
20
+ * any untrusted content (tool results / file reads / web / subagent / MCP) is concatenated. NEVER scan the
21
+ * assembled prompt server-side — a malicious file containing "ultracode" would be a privilege-escalation
22
+ * injection. The keyword is a UX convenience switch, NOT a security decision point: privilege STILL passes the
23
+ * service's fail-closed per-principal `allowWorkflows` gate (core deliberately refuses prompt scanning,
24
+ * prepare-task.ts:1612 — that boundary is correct). The output is a STRUCTURED spec (the boolean), never
25
+ * injected prompt text.
26
+ *
27
+ * Both sources are OR'd live-side by `toTaskRequest`; the SAME `SEMA_LIVE_BASEURL` gate the other capability wires
28
+ * use keeps the mock request shape unchanged (the stamp IS the opt-in; absent ⇒ the engine never expands ultracode).
29
+ *
30
+ * The PRESET state is module-level (mirrors seamQuery's seam* injectors): the dial (a React override) latches it,
31
+ * the seam reads it. The KEYWORD scan is a PURE function (caller passes the human input) so it bundles + unit-tests
32
+ * without React/AppState.
33
+ */
34
+ import { hostEnv } from './hostEnv.js';
35
+ /** STICKY L2 preset: latched ON by the `/effort ultracode` dial pick, OFF by any other effort pick. Module-level
36
+ * (one shell process = one session) — the same single-namespace assumption seamQuery's session state uses. */
37
+ let ultracodePreset = false;
38
+ /** Latch the dial preset. `effort.tsx` calls this whenever an effort level is APPLIED: `true` for the `ultracode`
39
+ * notch, `false` for every other level (so switching the dial OFF ultracode clears the workflow-orchestration axis
40
+ * while the reasoning axis follows the picked level). */
41
+ export function setUltracodePreset(on) {
42
+ ultracodePreset = on === true;
43
+ }
44
+ /** Read the sticky dial preset (the L2 source). */
45
+ export function getUltracodePreset() {
46
+ return ultracodePreset;
47
+ }
48
+ /** Test/boot reset — clears the latched preset (e.g. a fresh session). */
49
+ export function _resetUltracodePreset() {
50
+ ultracodePreset = false;
51
+ }
52
+ /** Word-boundary, case-insensitive scan for the literal keyword. A bare mention ("ultracode") anywhere in the
53
+ * human's own turn text counts (CC-style UX toggle); the server's `allowWorkflows` gate is the real guard, so a
54
+ * false-positive (the user discussing the word) only requests the preset — it cannot escalate past the gate. */
55
+ const ULTRACODE_KEYWORD = /\bultracode\b/i;
56
+ /**
57
+ * L3 keyword source: `true` iff the REAL human turn input contains the "ultracode" keyword, else `undefined`.
58
+ * 🔴 The caller MUST pass ONLY the human's current-turn submission (seamQuery's `objective`, derived from the
59
+ * projectable user message) — NOT any assembled/untrusted content. Returns `true | undefined` (never `false`) to
60
+ * mirror the strict additive-stamp discipline of the other wire caps (a stamped `false` would needlessly change
61
+ * the default request shape).
62
+ */
63
+ export function ultracodeFromInput(humanInput) {
64
+ return typeof humanInput === 'string' && ULTRACODE_KEYWORD.test(humanInput) ? true : undefined;
65
+ }
66
+ /**
67
+ * Combine the two sources into the structured spec the seam stamps. `true` when EITHER the sticky dial preset is on
68
+ * OR the human's current-turn input contains the keyword; otherwise `undefined` (⇒ no stamp ⇒ default behaviour).
69
+ * Returns `true | undefined` so the seam can spread `...(ultracode ? { ultracode } : {})` without changing the
70
+ * default request shape. The CALLER gates this to LIVE mode (mock parity).
71
+ */
72
+ export function ultracodeForRequest(humanInput) {
73
+ return getUltracodePreset() || ultracodeFromInput(humanInput) ? true : undefined;
74
+ }
75
+ /** [1044]②裁 B 激活源 a(clay 拍):用户当轮说 "workflow"(词边界、大小写不敏感)⇒ 该 turn 激活
76
+ * Workflow 暴露。与 ultracode 线同一注入红线:只扫真人当轮输入(objective),绝不扫组装后内容。
77
+ * 消费接线=[1052]① 收官形(workflowDeferForRequest,core 1.314 deferTools 首班到货后点亮)。
78
+ * [1486] 词表扩(2026-07-21,「启动 Agent Team 走成三 fork」白盒定谳):多 agent 语义族也激活——
79
+ * run_workflow 描述里的路由映射句(multi-agent orchestration…fan out agents)defer 后模型读不到,
80
+ * 「agent team/multi-agent/swarm/多agent/编排」类输入落进可见工具面的理性变通(三 fork)。命中即
81
+ * 当轮全量上 wire(翻转只发生在激活当轮,前缀缓存语义不变)。终态=Agent Team 真入口(案A,
82
+ * [1484]/[1485])落地后此词有真归宿,本词表是过渡补丁。 */
83
+ const WORKFLOW_KEYWORD = /\bworkflows?\b|\bagent[ -]?teams?\b|\bmulti[ -]?agent\b|\bswarms?\b|多\s?agent|多智能体|编排/i;
84
+ export function workflowFromInput(humanInput) {
85
+ return typeof humanInput === 'string' && WORKFLOW_KEYWORD.test(humanInput) ? true : undefined;
86
+ }
87
+ /** Env escape hatch for the deferred-Workflow factory default: `SEMA_WORKFLOW_DEFER=0/false/no/off` restores the
88
+ * pre-[1052] always-exposed shape (schema bytes back in the prefix). Anything else (incl. unset) = defer ON. */
89
+ export const WORKFLOW_DEFER_ENV = 'SEMA_WORKFLOW_DEFER';
90
+ function deferOptedOut(env) {
91
+ const raw = env[WORKFLOW_DEFER_ENV];
92
+ const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
93
+ return s === '0' || s === 'false' || s === 'no' || s === 'off';
94
+ }
95
+ /**
96
+ * [1052]① 收官形(clay 裁 B 的 core deferred 机制件,cli 接线半场):Workflow「全局默认开但不暴露」——
97
+ * 未命中激活源的 TaskRequest 带 `deferTools:["Workflow"]`(core 1.314 对已挂载工具做 design/36 延迟披露:
98
+ * 占位符上 wire、schema 字节撤出缓存前缀、ToolSearch 可激活=中途激活通路);命中任一激活源 ⇒ `undefined`
99
+ * (不 defer=名字+schema 全量上 wire):
100
+ * a) 用户当轮说 "workflow"(workflowFromInput,只扫真人输入);
101
+ * b) "ultracode" 关键词(L3,同一红线);
102
+ * c) `/effort ultracode` sticky dial(L2 preset)。
103
+ * CALLER 前置两门:live 门(mock 形不变)+ selfOrchestration stamp 存在(opt-out 时 Workflow 根本不挂载,
104
+ * defer 无意义)。前缀缓存注记:defer↔不 defer 的翻转只发生在真人激活当轮,与 CC 209 ToolSearch materialize
105
+ * 同一形——非激活轮的前缀指纹稳定([1044]② clay 硬约束 ii 的解)。
106
+ */
107
+ export function workflowDeferForRequest(humanInput, env = hostEnv()) {
108
+ if (deferOptedOut(env))
109
+ return undefined;
110
+ if (getUltracodePreset() || ultracodeFromInput(humanInput) || workflowFromInput(humanInput))
111
+ return undefined;
112
+ return ['Workflow'];
113
+ }
114
+ /** [1077]④ 低频工具惰性注入:`SEMA_DEFER_TOOLS=<csv>`(wire 名)追加进 deferTools(core 1.314
115
+ * 延迟披露腿)。**缺省翻转已裁(clay [1212]⑤,2026-07-19)**:九件低频工具缺省 defer——判决数=
116
+ * -2,484 tok/首调用+多轮 gate 零行为退化;defer≠禁用(占位符在,模型 ToolSearch 随取)。
117
+ * 逃生门:`SEMA_DEFER_LOWFREQ=0/false/no/off` 关缺省名单(九件恢复全量上前缀)。 */
118
+ export const DEFER_TOOLS_ENV = 'SEMA_DEFER_TOOLS';
119
+ export const DEFER_LOWFREQ_ENV = 'SEMA_DEFER_LOWFREQ';
120
+ /** clay 裁定的缺省惰性名单([1076]④ 评测九件原单,[1090]② 判决数)。 */
121
+ export const DEFAULT_LOWFREQ_DEFER = [
122
+ 'Glob',
123
+ 'Grep',
124
+ 'LSP',
125
+ 'RepoMap',
126
+ 'Monitor',
127
+ 'Now',
128
+ 'AgentTranscript',
129
+ 'ReadToolResult',
130
+ 'ReportBlocked',
131
+ ];
132
+ function extraDeferFromEnv(env) {
133
+ const raw = env[DEFER_TOOLS_ENV];
134
+ if (typeof raw !== 'string' || raw.trim().length === 0)
135
+ return [];
136
+ return raw
137
+ .split(',')
138
+ .map((s) => s.trim())
139
+ .filter((s) => s.length > 0);
140
+ }
141
+ function lowFreqOptedOut(env) {
142
+ const raw = env[DEFER_LOWFREQ_ENV];
143
+ const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
144
+ return s === '0' || s === 'false' || s === 'no' || s === 'off';
145
+ }
146
+ /**
147
+ * The FULL deferTools list for a request: the Workflow default-defer lane (activation sources, see
148
+ * {@link workflowDeferForRequest}) + the DEFAULT low-frequency list (clay [1212]⑤, opt-out via
149
+ * SEMA_DEFER_LOWFREQ) + the SEMA_DEFER_TOOLS extra list, deduped. An activation hit removes ONLY
150
+ * Workflow (the user asked for it by name); the other lanes are source-independent. Empty ⇒
151
+ * `undefined` (no stamp, request shape unchanged).
152
+ */
153
+ export function deferToolsForRequest(humanInput, env = hostEnv()) {
154
+ const workflowLane = workflowDeferForRequest(humanInput, env) ?? [];
155
+ const lowFreqLane = lowFreqOptedOut(env) ? [] : DEFAULT_LOWFREQ_DEFER;
156
+ const extra = extraDeferFromEnv(env).filter((name) => name !== 'Workflow' || workflowLane.length > 0);
157
+ const merged = [...new Set([...workflowLane, ...lowFreqLane, ...extra])];
158
+ return merged.length > 0 ? merged : undefined;
159
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/webSearchWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/webSearchWireCaps.ts — PURE projection: the shell's WebSearch backend config → the @sema-ai
6
+ * `SemaSettings.webSearch` wire shape carried on `TaskRequest.settings.webSearch`.
7
+ *
8
+ * The SERVICE consumes `body.settings.webSearch` — ai-agent-service `src/plugins/web-search.ts`
9
+ * `webSearchConfigFromSettings(raw)` reads a per-request `{ provider, apiKey?, endpoint?, maxResults? }` that
10
+ * WINS over the deployment-env backend, gated to the single-user host lane (a per-request endpoint/apiKey is a
11
+ * capability config; on multi-tenant it would be SSRF, so multi-tenant uses ONLY the deploy-env backend —
12
+ * design/107). The service validates `provider ∈ brave | tavily | searxng` and DROPS a missing/invalid provider
13
+ * (it never throws — a bad config falls back to the deploy-env backend). We mirror that exact validation here so
14
+ * the shell never stamps a config the service would reject.
15
+ *
16
+ * Two config sources, ENV wins (the primary / verify lane):
17
+ * - ENV: `SEMA_WEBSEARCH_PROVIDER` / `_ENDPOINT` / `_API_KEY` / `_MAX_RESULTS` (SEMA_-namespaced, distinct from
18
+ * the SERVICE's own deploy `WEB_SEARCH_*` env so the shell config never collides with the worker's).
19
+ * - settings.json: a CC-parity `webSearch` section (the SettingsSchema outer `.passthrough()` preserves it).
20
+ *
21
+ * PURE — no env/file IO of its OWN (callers pass `env` / `raw`), so it bundles + unit-tests without the heavy
22
+ * settings graph, exactly like mcpWireCaps. The CALLER (seamQuery / seamQueryEngine) gates the env read to LIVE
23
+ * mode so the mock request shape never changes; webSearch presence IS the opt-in (no extra flag needed).
24
+ */
25
+ import { type EnvLike } from './hostEnv.js';
26
+ /** The search providers the service backend speaks (web-search.ts `WebSearchProvider`). */
27
+ export type WebSearchProvider = 'brave' | 'tavily' | 'searxng';
28
+ /** The `SemaSettings.webSearch` wire shape — mirrors the service's per-request parser input
29
+ * (web-search.ts `webSearchConfigFromSettings`: `{ provider, apiKey?, endpoint?, maxResults? }`). */
30
+ export interface WebSearchSettings {
31
+ provider: WebSearchProvider;
32
+ /** API key (brave/tavily). The service keeps it in a closure — it never reaches the model or the tool args. */
33
+ apiKey?: string;
34
+ /** SearXNG base URL (REQUIRED for searxng); for brave/tavily an optional base-URL override. */
35
+ endpoint?: string;
36
+ /** Max results returned to the model (the service clamps 1..20; default 10). */
37
+ maxResults?: number;
38
+ }
39
+ /** The four env keys the shell reads. SEMA_-namespaced so they never collide with the worker's deploy
40
+ * `WEB_SEARCH_*` env (the service reads BOTH — a per-request body.settings.webSearch WINS over its env). */
41
+ export declare const WEBSEARCH_ENV: {
42
+ readonly provider: "SEMA_WEBSEARCH_PROVIDER";
43
+ readonly endpoint: "SEMA_WEBSEARCH_ENDPOINT";
44
+ readonly apiKey: "SEMA_WEBSEARCH_API_KEY";
45
+ readonly maxResults: "SEMA_WEBSEARCH_MAX_RESULTS";
46
+ };
47
+ /** ENV source → WebSearchSettings, or undefined when no/invalid provider (⇒ no stamp; service uses its env/none). */
48
+ export declare function webSearchFromEnv(env?: EnvLike): WebSearchSettings | undefined;
49
+ /** settings.json `webSearch` section (UNTRUSTED passthrough value) → WebSearchSettings, mirroring the service's
50
+ * `webSearchConfigFromSettings`. Missing/invalid provider ⇒ undefined (omit). NEVER throws. */
51
+ export declare function webSearchFromSettings(raw: unknown): WebSearchSettings | undefined;
52
+ /** Resolve the effective WebSearch config: ENV wins (the primary lane), else the settings.json section.
53
+ * Undefined ⇒ no stamp (the service falls back to its deploy-env backend, or ships no WebSearch tool). */
54
+ export declare function resolveWebSearch(env?: EnvLike, settingsRaw?: unknown): WebSearchSettings | undefined;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/webSearchWireCaps.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/webSearchWireCaps.ts — PURE projection: the shell's WebSearch backend config → the @sema-ai
6
+ * `SemaSettings.webSearch` wire shape carried on `TaskRequest.settings.webSearch`.
7
+ *
8
+ * The SERVICE consumes `body.settings.webSearch` — ai-agent-service `src/plugins/web-search.ts`
9
+ * `webSearchConfigFromSettings(raw)` reads a per-request `{ provider, apiKey?, endpoint?, maxResults? }` that
10
+ * WINS over the deployment-env backend, gated to the single-user host lane (a per-request endpoint/apiKey is a
11
+ * capability config; on multi-tenant it would be SSRF, so multi-tenant uses ONLY the deploy-env backend —
12
+ * design/107). The service validates `provider ∈ brave | tavily | searxng` and DROPS a missing/invalid provider
13
+ * (it never throws — a bad config falls back to the deploy-env backend). We mirror that exact validation here so
14
+ * the shell never stamps a config the service would reject.
15
+ *
16
+ * Two config sources, ENV wins (the primary / verify lane):
17
+ * - ENV: `SEMA_WEBSEARCH_PROVIDER` / `_ENDPOINT` / `_API_KEY` / `_MAX_RESULTS` (SEMA_-namespaced, distinct from
18
+ * the SERVICE's own deploy `WEB_SEARCH_*` env so the shell config never collides with the worker's).
19
+ * - settings.json: a CC-parity `webSearch` section (the SettingsSchema outer `.passthrough()` preserves it).
20
+ *
21
+ * PURE — no env/file IO of its OWN (callers pass `env` / `raw`), so it bundles + unit-tests without the heavy
22
+ * settings graph, exactly like mcpWireCaps. The CALLER (seamQuery / seamQueryEngine) gates the env read to LIVE
23
+ * mode so the mock request shape never changes; webSearch presence IS the opt-in (no extra flag needed).
24
+ */
25
+ import { hostEnv } from './hostEnv.js';
26
+ /** The four env keys the shell reads. SEMA_-namespaced so they never collide with the worker's deploy
27
+ * `WEB_SEARCH_*` env (the service reads BOTH — a per-request body.settings.webSearch WINS over its env). */
28
+ export const WEBSEARCH_ENV = {
29
+ provider: 'SEMA_WEBSEARCH_PROVIDER',
30
+ endpoint: 'SEMA_WEBSEARCH_ENDPOINT',
31
+ apiKey: 'SEMA_WEBSEARCH_API_KEY',
32
+ maxResults: 'SEMA_WEBSEARCH_MAX_RESULTS',
33
+ };
34
+ /** Validate a provider EXACTLY as the service does (trim + lowercase; only the three known providers). */
35
+ function normProvider(v) {
36
+ const p = typeof v === 'string' ? v.trim().toLowerCase() : '';
37
+ return p === 'brave' || p === 'tavily' || p === 'searxng' ? p : undefined;
38
+ }
39
+ /** Parse a positive integer maxResults (string env or number); undefined when absent/invalid (service clamps). */
40
+ function normMax(v) {
41
+ const n = typeof v === 'number' ? v : Number(v);
42
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
43
+ }
44
+ /** Assemble a `WebSearchSettings`, omitting empty optionals (never stamp `apiKey:''`/`endpoint:''`/`maxResults:0`). */
45
+ function build(provider, apiKey, endpoint, maxResults) {
46
+ const max = normMax(maxResults);
47
+ return {
48
+ provider,
49
+ ...(typeof apiKey === 'string' && apiKey ? { apiKey } : {}),
50
+ ...(typeof endpoint === 'string' && endpoint ? { endpoint } : {}),
51
+ ...(max ? { maxResults: max } : {}),
52
+ };
53
+ }
54
+ /** ENV source → WebSearchSettings, or undefined when no/invalid provider (⇒ no stamp; service uses its env/none). */
55
+ export function webSearchFromEnv(env = hostEnv()) {
56
+ const provider = normProvider(env[WEBSEARCH_ENV.provider]);
57
+ if (!provider)
58
+ return undefined;
59
+ return build(provider, env[WEBSEARCH_ENV.apiKey], env[WEBSEARCH_ENV.endpoint], env[WEBSEARCH_ENV.maxResults]);
60
+ }
61
+ /** settings.json `webSearch` section (UNTRUSTED passthrough value) → WebSearchSettings, mirroring the service's
62
+ * `webSearchConfigFromSettings`. Missing/invalid provider ⇒ undefined (omit). NEVER throws. */
63
+ export function webSearchFromSettings(raw) {
64
+ if (!raw || typeof raw !== 'object')
65
+ return undefined;
66
+ const s = raw;
67
+ const provider = normProvider(s.provider);
68
+ if (!provider)
69
+ return undefined;
70
+ return build(provider, s.apiKey, s.endpoint, s.maxResults);
71
+ }
72
+ /** Resolve the effective WebSearch config: ENV wins (the primary lane), else the settings.json section.
73
+ * Undefined ⇒ no stamp (the service falls back to its deploy-env backend, or ships no WebSearch tool). */
74
+ export function resolveWebSearch(env = hostEnv(), settingsRaw) {
75
+ return webSearchFromEnv(env) ?? webSearchFromSettings(settingsRaw);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
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",
@@ -14,7 +14,7 @@
14
14
  "sideEffects": false,
15
15
  "scripts": {
16
16
  "build": "tsc -p tsconfig.json",
17
- "test": "node scripts/run-client-core-pure-test.mjs && node scripts/run-client-core-diff-test.mjs"
17
+ "test": "node scripts/run-client-core-pure-test.mjs && node scripts/run-client-core-portability-test.mjs && node scripts/run-client-core-diff-test.mjs"
18
18
  },
19
19
  "dependencies": {},
20
20
  "peerDependencies": {