@sema-agent/client-core 0.2.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 (80) 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/engineAgentPanelStore.d.ts +111 -0
  17. package/dist/engineAgentPanelStore.js +93 -0
  18. package/dist/engineCapsCache.d.ts +32 -0
  19. package/dist/engineCapsCache.js +64 -0
  20. package/dist/engineInlineTaskStats.d.ts +66 -0
  21. package/dist/engineInlineTaskStats.js +97 -0
  22. package/dist/engineToolLabelStore.d.ts +32 -0
  23. package/dist/engineToolLabelStore.js +55 -0
  24. package/dist/engineWireSdk.d.ts +49 -0
  25. package/dist/engineWireSdk.js +61 -0
  26. package/dist/fleetAgentPanelProjection.d.ts +31 -0
  27. package/dist/fleetAgentPanelProjection.js +0 -0
  28. package/dist/fleetTaskDesc.d.ts +20 -0
  29. package/dist/fleetTaskDesc.js +35 -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 +56 -1
  37. package/dist/index.js +62 -1
  38. package/dist/liveModelCatalog.d.ts +62 -0
  39. package/dist/liveModelCatalog.js +79 -0
  40. package/dist/liveQuestionStore.d.ts +101 -0
  41. package/dist/liveQuestionStore.js +109 -0
  42. package/dist/mcpWireCaps.d.ts +41 -0
  43. package/dist/mcpWireCaps.js +51 -0
  44. package/dist/modelBudgetRule.d.ts +44 -0
  45. package/dist/modelBudgetRule.js +73 -0
  46. package/dist/modelWireCaps.d.ts +17 -0
  47. package/dist/modelWireCaps.js +16 -0
  48. package/dist/notifications.d.ts +150 -4
  49. package/dist/notifications.js +472 -0
  50. package/dist/permissionWireCaps.d.ts +37 -0
  51. package/dist/permissionWireCaps.js +46 -0
  52. package/dist/printToolResultFrame.d.ts +59 -0
  53. package/dist/printToolResultFrame.js +65 -0
  54. package/dist/promptProfileWireCaps.d.ts +17 -0
  55. package/dist/promptProfileWireCaps.js +20 -0
  56. package/dist/retainBackgroundWireCaps.d.ts +50 -0
  57. package/dist/retainBackgroundWireCaps.js +58 -0
  58. package/dist/rewindWireCaps.d.ts +36 -0
  59. package/dist/rewindWireCaps.js +41 -0
  60. package/dist/selfOrchestrationWireCaps.d.ts +40 -0
  61. package/dist/selfOrchestrationWireCaps.js +48 -0
  62. package/dist/sessionModelLatch.d.ts +35 -0
  63. package/dist/sessionModelLatch.js +63 -0
  64. package/dist/skillsWireCaps.d.ts +47 -0
  65. package/dist/skillsWireCaps.js +41 -0
  66. package/dist/sseIdleTriage.d.ts +94 -0
  67. package/dist/sseIdleTriage.js +142 -0
  68. package/dist/steering.d.ts +4 -0
  69. package/dist/steering.js +4 -0
  70. package/dist/subagentContentStore.d.ts +131 -0
  71. package/dist/subagentContentStore.js +322 -0
  72. package/dist/types/engineState.d.ts +116 -0
  73. package/dist/types/engineState.js +17 -0
  74. package/dist/ultracodeWireCaps.d.ts +90 -0
  75. package/dist/ultracodeWireCaps.js +159 -0
  76. package/dist/webSearchWireCaps.d.ts +54 -0
  77. package/dist/webSearchWireCaps.js +76 -0
  78. package/dist/workflow.d.ts +40 -8
  79. package/dist/workflow.js +69 -11
  80. package/package.json +1 -1
@@ -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
+ }
@@ -1,18 +1,50 @@
1
1
  /**
2
- * workflow 车道纯函数(抽自 sema-cli footerRowBelt/workflowTaskOutputProjection,围栏见
3
- * cli 仓 frameLaneGuards/taskOutputWorkflowEnvelope 套;[1617] 幽灵行家族的判别资产)。
2
+ * workflow 车道纯函数 + workflow-agent 台账。
3
+ *
4
+ * 沿革(B1 批,2026-07-27):0.2.0 时本文件是 cli `footerRowBelt.ts` / `workflowTaskOutputProjection.ts`
5
+ * 的**双份抄件**;B1 批按设计稿 §2.4.1-A/B 裁决「以壳侧现文件为真身、包内不留两份实现」:
6
+ * · `parseWorkflowPollEnvelope` / `projectWorkflowTaskOutput` = cli `workflowTaskOutputProjection.ts`
7
+ * 的逐字搬入(0.2.0 抄件漏了 `agent_runs[].errorCode` 类型位,已按真身补齐);
8
+ * · `rowIdTail` / `isWorkflowAgentTaskId` / `recordWorkflowAgentTaskId` / `isKnownWorkflowAgentTaskId`
9
+ * = cli `footerRowBelt.ts` 的**belt 半场**(判据①④),按设计稿 §2.4.1-B「拆」搬入。
10
+ * ⚠️ 留在 TUI 的是 `isFooterTaskRow` / `filterFooterTaskRows`(判据②③)—— 那两条含
11
+ * 「丢弃所有无 parentId 顶层行」的**端专属决策**(主 run 头部行由壳侧 fleetClient 合成渲染,
12
+ * 设计稿 §2.4.2-B),壳侧文件改成 import 本包的四个出口 + 自留那两个函数。
13
+ * 🔴 台账(`workflowAgentTaskIds`)是 module 级单实例:壳侧 bridge 的 task_progress lane 门写、
14
+ * 壳侧 belt 的 `isFooterTaskRow` 读 —— 两边都必须走**本包同一个模块实例**,否则台账写进一个
15
+ * 副本、读另一个副本(判据④ 静默失效,幽灵行回归)。
16
+ *
17
+ * 围栏见 cli 仓 frameLaneGuards / taskOutputWorkflowEnvelope / footerRowBelt 套;
18
+ * [1617] 幽灵行家族的判别资产。
4
19
  */
5
- /** core 1.356 BCE 合成 workflow-agent id 形:wa+16hex
20
+ /** 复合行键(`<runId> <taskId>`,fleet-bus fleetRunPublisher.childId)的尾段 = 引擎 taskId
21
+ * 导出给壳侧 belt 半场复用(判据②③ 的行 id 归一);包内 fleetAgentPanelProjection 保留了它
22
+ * 自己的同语义私有副本(零依赖叶模块纪律,drift 由两侧测试各自锁)。 */
23
+ export declare function rowIdTail(id: string): string;
24
+ /** core 1.356 BCE 合成 workflow-agent id 形:wa+16hex(判据①)。
6
25
  * 真实 fleet 子行 id 是 `${runId} ${engineTaskId}` 复合形——必须取空白分隔尾段再匹配
7
- * (cli footerRowBelt 原版语义;codex 对抗复审抓的等价性破坏,0.1.1 修)。 */
26
+ * (cli footerRowBelt 原版语义;codex 对抗复审抓的等价性破坏,0.1.1 修)。
27
+ * upstreamBridge 的 task_progress 前台 lane 门在 wire 无 workflowRunId 时(server <a76bd77,
28
+ * 黑板 [1619]:1.272.0 投影白名单仍剥该键)以此为次级判别——与 belt 双判据同模板。 */
8
29
  export declare function isWorkflowAgentTaskId(taskId: string): boolean;
9
- /** TaskOutput workflow 轮询信封识别(core task-registry formatWorkflowRun 真形:
10
- * {task_id, type:"workflow", status, name?, …};type+task_id 双门,非该形返回 null)。 */
30
+ export declare function recordWorkflowAgentTaskId(taskId: string): void;
31
+ export declare function isKnownWorkflowAgentTaskId(taskId: string): boolean;
32
+ /** 测试钩:清台账(壳侧原版无此口——module 级 Set 在壳里靠进程边界隔离;包内多组断言同进程跑,
33
+ * 必须能清,否则前一组的登记污染后一组)。 */
34
+ export declare function __resetWorkflowAgentTaskIdsForTests(): void;
35
+ /**
36
+ * TaskOutput legacy 腿(tool_end 无 structured)的 workflow 信封识别(#49 C3)。
37
+ * 判据=core task-registry formatWorkflowRun 真形:`{task_id, type:"workflow", status, name?, …}`
38
+ * ——type 判别位 + task_id 字符串双门,非该形返回 null(绝不误吞 bash/agent 轮询)。
39
+ */
11
40
  export declare function parseWorkflowPollEnvelope(body: string): {
12
41
  taskId: string;
13
42
  status: string;
14
43
  name?: string;
15
44
  } | null;
16
- /** workflow 轮询 JSON → 人话多行投影(agents 汇总行 + per-agent ✓/✗ 明细 + note/error 尾透传);
17
- * JSON 原样返回(fail-soft)。 */
45
+ /**
46
+ * workflow 轮询 JSON → 人话多行投影(agents 汇总行 + per-agent ✓/✗ 明细 + note/error 尾透传);
47
+ * 非 JSON 原样返回(fail-soft,`description [status]` 行仍在)。
48
+ * 输入=引擎 formatWorkflowRun 的 JSON 原文(core 1.352 起带 agent_runs per-agent 明细 + 失败 note 指路)。
49
+ */
18
50
  export declare function projectWorkflowTaskOutput(jsonSrc: string): string;
package/dist/workflow.js CHANGED
@@ -1,16 +1,66 @@
1
1
  /**
2
- * workflow 车道纯函数(抽自 sema-cli footerRowBelt/workflowTaskOutputProjection,围栏见
3
- * cli 仓 frameLaneGuards/taskOutputWorkflowEnvelope 套;[1617] 幽灵行家族的判别资产)。
2
+ * workflow 车道纯函数 + workflow-agent 台账。
3
+ *
4
+ * 沿革(B1 批,2026-07-27):0.2.0 时本文件是 cli `footerRowBelt.ts` / `workflowTaskOutputProjection.ts`
5
+ * 的**双份抄件**;B1 批按设计稿 §2.4.1-A/B 裁决「以壳侧现文件为真身、包内不留两份实现」:
6
+ * · `parseWorkflowPollEnvelope` / `projectWorkflowTaskOutput` = cli `workflowTaskOutputProjection.ts`
7
+ * 的逐字搬入(0.2.0 抄件漏了 `agent_runs[].errorCode` 类型位,已按真身补齐);
8
+ * · `rowIdTail` / `isWorkflowAgentTaskId` / `recordWorkflowAgentTaskId` / `isKnownWorkflowAgentTaskId`
9
+ * = cli `footerRowBelt.ts` 的**belt 半场**(判据①④),按设计稿 §2.4.1-B「拆」搬入。
10
+ * ⚠️ 留在 TUI 的是 `isFooterTaskRow` / `filterFooterTaskRows`(判据②③)—— 那两条含
11
+ * 「丢弃所有无 parentId 顶层行」的**端专属决策**(主 run 头部行由壳侧 fleetClient 合成渲染,
12
+ * 设计稿 §2.4.2-B),壳侧文件改成 import 本包的四个出口 + 自留那两个函数。
13
+ * 🔴 台账(`workflowAgentTaskIds`)是 module 级单实例:壳侧 bridge 的 task_progress lane 门写、
14
+ * 壳侧 belt 的 `isFooterTaskRow` 读 —— 两边都必须走**本包同一个模块实例**,否则台账写进一个
15
+ * 副本、读另一个副本(判据④ 静默失效,幽灵行回归)。
16
+ *
17
+ * 围栏见 cli 仓 frameLaneGuards / taskOutputWorkflowEnvelope / footerRowBelt 套;
18
+ * [1617] 幽灵行家族的判别资产。
4
19
  */
5
- /** core 1.356 BCE 合成 workflow-agent id 形:wa+16hex
20
+ /** 复合行键(`<runId> <taskId>`,fleet-bus fleetRunPublisher.childId)的尾段 = 引擎 taskId
21
+ * 导出给壳侧 belt 半场复用(判据②③ 的行 id 归一);包内 fleetAgentPanelProjection 保留了它
22
+ * 自己的同语义私有副本(零依赖叶模块纪律,drift 由两侧测试各自锁)。 */
23
+ export function rowIdTail(id) {
24
+ return id.split(/\s+/).pop() ?? id;
25
+ }
26
+ /** core 1.356 BCE 合成 workflow-agent id 形:wa+16hex(判据①)。
6
27
  * 真实 fleet 子行 id 是 `${runId} ${engineTaskId}` 复合形——必须取空白分隔尾段再匹配
7
- * (cli footerRowBelt 原版语义;codex 对抗复审抓的等价性破坏,0.1.1 修)。 */
28
+ * (cli footerRowBelt 原版语义;codex 对抗复审抓的等价性破坏,0.1.1 修)。
29
+ * upstreamBridge 的 task_progress 前台 lane 门在 wire 无 workflowRunId 时(server <a76bd77,
30
+ * 黑板 [1619]:1.272.0 投影白名单仍剥该键)以此为次级判别——与 belt 双判据同模板。 */
8
31
  export function isWorkflowAgentTaskId(taskId) {
9
- const tail = taskId.split(/\s+/).pop() ?? taskId;
10
- return /^wa[0-9a-f]{16}$/.test(tail);
32
+ return /^wa[0-9a-f]{16}$/.test(rowIdTail(taskId));
33
+ }
34
+ // ── 判据④:bridge task_progress lane 门台账([1641])────────────────────────────────────────────
35
+ // bridge 的 workflow lane 门(帧带 workflowRunId 的权威判定)把子代 taskId 登记进来,行按尾段查。
36
+ // ③④ 互为兜底:③(集合级双生识别,TUI 半场)不依赖时序但要求 wa 行在场;④ 不依赖 wa 行但要求
37
+ // 本壳在读该 run 的会话流。
38
+ const workflowAgentTaskIds = new Set();
39
+ const MAX_WORKFLOW_TASK_IDS = 4096;
40
+ export function recordWorkflowAgentTaskId(taskId) {
41
+ if (!taskId)
42
+ return;
43
+ workflowAgentTaskIds.delete(taskId);
44
+ workflowAgentTaskIds.add(taskId);
45
+ if (workflowAgentTaskIds.size > MAX_WORKFLOW_TASK_IDS) {
46
+ const oldest = workflowAgentTaskIds.values().next().value;
47
+ if (oldest !== undefined)
48
+ workflowAgentTaskIds.delete(oldest);
49
+ }
50
+ }
51
+ export function isKnownWorkflowAgentTaskId(taskId) {
52
+ return workflowAgentTaskIds.has(taskId);
53
+ }
54
+ /** 测试钩:清台账(壳侧原版无此口——module 级 Set 在壳里靠进程边界隔离;包内多组断言同进程跑,
55
+ * 必须能清,否则前一组的登记污染后一组)。 */
56
+ export function __resetWorkflowAgentTaskIdsForTests() {
57
+ workflowAgentTaskIds.clear();
11
58
  }
12
- /** TaskOutput workflow 轮询信封识别(core task-registry formatWorkflowRun 真形:
13
- * {task_id, type:"workflow", status, name?, …};type+task_id 双门,非该形返回 null)。 */
59
+ /**
60
+ * TaskOutput legacy 腿(tool_end structured)的 workflow 信封识别(#49 C3)。
61
+ * 判据=core task-registry formatWorkflowRun 真形:`{task_id, type:"workflow", status, name?, …}`
62
+ * ——type 判别位 + task_id 字符串双门,非该形返回 null(绝不误吞 bash/agent 轮询)。
63
+ */
14
64
  export function parseWorkflowPollEnvelope(body) {
15
65
  try {
16
66
  const j = JSON.parse(body);
@@ -26,8 +76,11 @@ export function parseWorkflowPollEnvelope(body) {
26
76
  return null;
27
77
  }
28
78
  }
29
- /** workflow 轮询 JSON → 人话多行投影(agents 汇总行 + per-agent ✓/✗ 明细 + note/error 尾透传);
30
- * JSON 原样返回(fail-soft)。 */
79
+ /**
80
+ * workflow 轮询 JSON → 人话多行投影(agents 汇总行 + per-agent ✓/✗ 明细 + note/error 尾透传);
81
+ * 非 JSON 原样返回(fail-soft,`description [status]` 行仍在)。
82
+ * 输入=引擎 formatWorkflowRun 的 JSON 原文(core 1.352 起带 agent_runs per-agent 明细 + 失败 note 指路)。
83
+ */
31
84
  export function projectWorkflowTaskOutput(jsonSrc) {
32
85
  try {
33
86
  const j = JSON.parse(jsonSrc);
@@ -35,19 +88,24 @@ export function projectWorkflowTaskOutput(jsonSrc) {
35
88
  if (j.agents && typeof j.agents.done === 'number' && typeof j.agents.total === 'number') {
36
89
  parts.push(`agents: ${j.agents.done}/${j.agents.total} done${typeof j.agents.failed === 'number' && j.agents.failed > 0 ? ` (${j.agents.failed} failed)` : ''}`);
37
90
  }
91
+ // per-agent 明细([1411]③/[1408] 围栏 M1):✓/✗ label [status] — error 首行/output 摘要。
92
+ // 缺席(老 core)零行=形状不变。
38
93
  if (Array.isArray(j.agent_runs)) {
39
94
  for (const r of j.agent_runs) {
40
95
  if (!r || typeof r !== 'object')
41
96
  continue;
42
97
  const label = typeof r.label === 'string' && r.label.length > 0 ? r.label : '(agent)';
43
98
  const st = typeof r.status === 'string' ? r.status : '?';
44
- const errLine = typeof r.error === 'string' && r.error.length > 0 ? ` — ${r.error.split('\n')[0].slice(0, 120)}` : '';
99
+ const errLine = typeof r.error === 'string' && r.error.length > 0
100
+ ? ` — ${r.error.split('\n')[0].slice(0, 120)}`
101
+ : '';
45
102
  const outLine = !errLine && typeof r.output === 'string' && r.output.length > 0
46
103
  ? ` — ${r.output.split('\n')[0].slice(0, 80)}`
47
104
  : '';
48
105
  parts.push(` ${st === 'completed' ? '✓' : st === 'failed' ? '✗' : '·'} ${label} [${st}]${errLine}${outLine}`);
49
106
  }
50
107
  }
108
+ // note 尾透传(core 失败指路:TaskOutput+resumeFromRunId)——不留死胡同(围栏 M2)。
51
109
  if (typeof j.note === 'string' && j.note.length > 0)
52
110
  parts.push(j.note);
53
111
  if (typeof j.error === 'string' && j.error.length > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",