@sema-agent/client-core 0.6.0 → 0.8.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 (68) hide show
  1. package/README.md +31 -11
  2. package/dist/adapt.d.ts +4 -2
  3. package/dist/adapt.js +225 -25
  4. package/dist/compensations.d.ts +63 -0
  5. package/dist/compensations.js +138 -0
  6. package/dist/detachWire.d.ts +141 -0
  7. package/dist/detachWire.js +182 -0
  8. package/dist/diff/patch.d.ts +29 -0
  9. package/dist/diff/patch.js +45 -0
  10. package/dist/engineSessionParam.d.ts +11 -0
  11. package/dist/engineSessionParam.js +41 -0
  12. package/dist/engineWireTarget.d.ts +14 -0
  13. package/dist/engineWireTarget.js +53 -0
  14. package/dist/finalVerifyWire.d.ts +74 -0
  15. package/dist/finalVerifyWire.js +63 -0
  16. package/dist/fleet/fleetLedger.d.ts +132 -0
  17. package/dist/fleet/fleetLedger.js +393 -0
  18. package/dist/fleet/fleetProjection.d.ts +181 -0
  19. package/dist/fleet/fleetProjection.js +258 -0
  20. package/dist/headlessPermissionModeWire.d.ts +55 -0
  21. package/dist/headlessPermissionModeWire.js +111 -0
  22. package/dist/headlessReconnectWire.d.ts +96 -0
  23. package/dist/headlessReconnectWire.js +141 -0
  24. package/dist/hooksWireCaps.d.ts +91 -0
  25. package/dist/hooksWireCaps.js +359 -0
  26. package/dist/host.d.ts +34 -2
  27. package/dist/host.js +6 -0
  28. package/dist/index.d.ts +74 -1
  29. package/dist/index.js +104 -1
  30. package/dist/interactiveToolsWire.d.ts +48 -0
  31. package/dist/interactiveToolsWire.js +86 -0
  32. package/dist/limitsWire.d.ts +89 -0
  33. package/dist/limitsWire.js +225 -0
  34. package/dist/liveInitToolFace.d.ts +53 -0
  35. package/dist/liveInitToolFace.js +251 -0
  36. package/dist/model/modelFamilies.json +63 -0
  37. package/dist/model/providerPresets.d.ts +46 -0
  38. package/dist/model/providerPresets.js +160 -0
  39. package/dist/model/providerPresets.json +1179 -0
  40. package/dist/notifications.d.ts +7 -0
  41. package/dist/notifications.js +32 -0
  42. package/dist/sandboxWire.d.ts +75 -0
  43. package/dist/sandboxWire.js +138 -0
  44. package/dist/scenarioWire.d.ts +62 -0
  45. package/dist/scenarioWire.js +115 -0
  46. package/dist/seam.d.ts +52 -5
  47. package/dist/seam.js +15 -2
  48. package/dist/subagent/engineCompactWire.d.ts +12 -0
  49. package/dist/subagent/engineCompactWire.js +176 -0
  50. package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
  51. package/dist/subagent/engineDelegatedPrompt.js +205 -0
  52. package/dist/subagent/engineRowStopGate.d.ts +18 -0
  53. package/dist/subagent/engineRowStopGate.js +54 -0
  54. package/dist/subagent/engineSubagentOutput.d.ts +15 -0
  55. package/dist/subagent/engineSubagentOutput.js +98 -0
  56. package/dist/subagent/engineSubagentSteer.d.ts +10 -0
  57. package/dist/subagent/engineSubagentSteer.js +68 -0
  58. package/dist/subagent/engineSubagentTail.d.ts +40 -0
  59. package/dist/subagent/engineSubagentTail.js +241 -0
  60. package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
  61. package/dist/subagent/engineTaskHandleWire.js +212 -0
  62. package/dist/toolResult.d.ts +118 -0
  63. package/dist/toolResult.js +774 -0
  64. package/dist/workflowClient.d.ts +42 -0
  65. package/dist/workflowClient.js +467 -0
  66. package/dist/workflowMonitor.d.ts +94 -0
  67. package/dist/workflowMonitor.js +43 -0
  68. package/package.json +5 -3
@@ -0,0 +1,42 @@
1
+ import type { WorkflowRun } from '@sema-agent/sdk';
2
+ import type { WorkflowRunState } from './workflowMonitor.js';
3
+ /** The live controller the host drives — a projected-snapshot reader + diagnostics + teardown. */
4
+ export interface LiveWorkflowController {
5
+ /** the latest projected run state, or null (not yet resolved / degraded 501-404 / empty). */
6
+ snapshot(): WorkflowRunState | null;
7
+ /** connection + degrade diagnostics for the empty/degraded render + verify cross-check. */
8
+ status(): {
9
+ connected: boolean;
10
+ /** true when the endpoint is unavailable (501 = S8 flag off) / the run is absent (404) / the list is empty. */
11
+ degraded: boolean;
12
+ /** the HTTP status behind a degrade (501 / 404), else null. */
13
+ code: number | null;
14
+ /** the resolved workflow run id (null until list()/config resolves one). */
15
+ runId: string | null;
16
+ lastError: string | null;
17
+ };
18
+ /** abort the underlying SSE stream + stop the background loop (host unmount). */
19
+ dispose(): void;
20
+ }
21
+ export interface LiveWorkflowConfig {
22
+ baseUrl: string;
23
+ authToken: string;
24
+ /** SDK Principal — e.g. 'anon:sema' (workflows are owner-scoped by principal). */
25
+ principal: string;
26
+ /** explicit workflow run id; when absent the source picks the first RUNNING run from list() (else the first). */
27
+ workflowId?: string;
28
+ }
29
+ /** THE projector: SDK `WorkflowRun` (client.workflows.get) → the monitor's `WorkflowRunState`. Pure/total —
30
+ * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
31
+ * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
32
+ export declare function projectWorkflowRun(run: WorkflowRun): WorkflowRunState;
33
+ /**
34
+ * createLiveWorkflowSource({ baseUrl, authToken, principal, workflowId? }) — the LIVE workflow-monitor source.
35
+ *
36
+ * Constructs the AgentClient and kicks off a background loop that (1) resolves a run id (explicit config, else
37
+ * the first running run from `list()`), (2) snapshots it via `get()` → project → store, and (3) follows
38
+ * `stream()` as a change-ticker (re-`get()` throttled on each frame). Every step is degrade-safe: a 501 (S8
39
+ * off) / 404 (non-owner/absent) / empty list / connection error is RECORDED (status().degraded/code) and
40
+ * leaves `snapshot()` null so the host falls back to the mock/empty state — the source never throws into it.
41
+ */
42
+ export declare function createLiveWorkflowSource(config: LiveWorkflowConfig): LiveWorkflowController;
@@ -0,0 +1,467 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/workflowClient.ts` 整搬(依赖面 = SDK + env,
3
+ * §3 表登记 472 行)。
4
+ *
5
+ * 🔴 搬迁四处差分(端资产上移 / 单一构造点,行为面逐条记账):
6
+ * ① `new AgentClient({…})` 直造 → `makeEngineWireClient()`(§8-2 定谳的唯一构造点;凭证过
7
+ * `resolveWireAuth` 三态)。**`maxRetries: 2` 显式传** —— 工厂缺省是 0(raw fetch 单发语义),
8
+ * 而这里原本吃的是 SDK 缺省 2;不显式传就等于**偷偷改了这条观测环路的重试次数**。
9
+ * 唯一残余差分:`authToken` 为**空串**时,原版原样交给 SDK,收编后走三态(loopback ⇒ 零
10
+ * Authorization 头 / 非 loopback ⇒ 'anon')—— 与另外 11 处构造点同一处置,记在 §8-2。
11
+ * 工厂 fail-soft 返回 null(SDK 拒绝构造)⇒ 本源恒 degraded、`snapshot()` 恒 null,宿主回落
12
+ * mock/空态 —— 与「501/404/空列表」同一条不抛的路。
13
+ * ② `process.env.SEMA_DEBUG` → `engineWireDebugEnabled()`;`console.error` → `hostLog('debug', …)`
14
+ * (本包零 `process`、零 console —— portability 门盯着)。
15
+ * ③ 投影产物的类型契约(`WorkflowRunState` 族)改 import 本包 `workflowMonitor.ts`(同批搬入),
16
+ * 不再从 TUI 的 `overrides/workflow-detail-dialog.js` 反向依赖渲染件。
17
+ * ④ `export default createLiveWorkflowSource` **不带过来**:本包是具名导出面(index 做 `export *`,
18
+ * default 不在其中)。壳侧那一行 default import 若存在,shim 里改具名。
19
+ *
20
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
21
+ *
22
+ * workflowClient — MF-W LIVE WIRE source (the workflow-monitor TWIN of fleetClient.ts).
23
+ *
24
+ * WHY THIS FILE EXISTS
25
+ * --------------------
26
+ * mock/workflow/workflowRun.ts synthesizes ONE representative WorkflowRunState from a scripted run (offline/
27
+ * demo). This file is its LIVE TWIN: it constructs the @sema-ai/sdk `AgentClient`, consumes the §workflow
28
+ * observability surface shipped in @sema-ai/sdk 0.0.40 (`client.workflows.list/get/stream`, resources/
29
+ * workflows.ts — GET /v1/workflows*, the S8 self-orchestration-observability endpoints), and PROJECTS the
30
+ * SDK's `WorkflowRun` (phases / agents / stats) to the SAME `WorkflowRunState` the reconstructed monitor
31
+ * (src/sema/overrides/workflow-detail-dialog.tsx) already consumes. ZERO renderer change — exactly the
32
+ * fleetClient.ts pattern (a projector, not a port).
33
+ *
34
+ * SDK CONTRACT (resources/workflows.ts + types.ts):
35
+ * - `list({status?,limit?})` → `{ workflows: WorkflowRunSummary[] }` (OWNER-scoped: only the caller's runs).
36
+ * - `get(id)` → full `WorkflowRun` (phases: unknown[] structured {title,status,done,total};
37
+ * agents: WorkflowAgentRow[] with activity[].arg; stats.tokens own+nested).
38
+ * - `stream(id)` → SSE of `WorkflowStreamEvent` (first frame `meta`, then core WorkflowEvent
39
+ * frames). REPLICA-LOCAL + in-process, NOT resumable — a drop is a FULL restart
40
+ * (re-`get()` for the snapshot), never a resume.
41
+ *
42
+ * 🔴 GRACEFUL DEGRADE (the load-bearing half — see the mount seam's verification-debt note): the LIVE
43
+ * endpoints are gated behind the server's S8 (self-orchestration-observability) flag. With S8 OFF the
44
+ * routes answer **501** (typed APIError) BEFORE any frame; a non-owner / absent run answers **404**; an
45
+ * owner with no runs yields an **empty** list. ALL THREE are EXPECTED, NOT crashes: this source records
46
+ * them (status().degraded/code) and keeps `snapshot()` null so the host can fall back to the mock/empty
47
+ * state. A raw connection error (server down) is treated the same way — the stream/loop just backs off and
48
+ * retries; the source NEVER throws into the host.
49
+ *
50
+ * 🔴 THE SDK PRODUCES FRAMES, IT DOES NOT MAINTAIN STATE. We take the AUTHORITATIVE snapshot from `get()` and
51
+ * treat `stream()` as a "something changed" TICKER: on each frame we re-`get()` the owner-scoped detail
52
+ * (throttled) and re-project, rather than reduce the permissive/core-internal WorkflowEvent shapes on the
53
+ * client. On stream drop we re-`get()` for a fresh snapshot (not resumable) with a small backoff.
54
+ *
55
+ * DROP-IN SHAPE: `snapshot()` returns the latest projected `WorkflowRunState | null` (null until the first
56
+ * successful `get()`, or while degraded) — the host polls it on an interval and swaps the mock for the live
57
+ * state once one resolves. `dispose()` aborts the stream + stops the background loop on host unmount.
58
+ */
59
+ import { APIError } from '@sema-agent/sdk';
60
+ import { engineSessionParamSpread } from './engineSessionParam.js';
61
+ import { makeEngineWireClient } from './engineWireSdk.js';
62
+ import { engineWireDebugEnabled } from './engineWireTarget.js';
63
+ import { hostLog } from './host.js';
64
+ // ── SDK WorkflowRun → monitor WorkflowRunState projectors ────────────────────────────────────────────────
65
+ // The SDK shapes (types.ts) are PERMISSIVE (open index sigs). The monitor's WorkflowRunState/Phase/Agent
66
+ // (workflowMonitor.ts) is the 187-faithful consumed set. We read the known fields, coerce
67
+ // the vocabularies, and fill the shell-local render defaults the wire does not carry — same discipline as
68
+ // projectTasks/projectWorkflows in fleet/fleetProjection.ts.
69
+ /** run status → the monitor's run-level vocabulary. SDK: queued|running|completed|failed|(open). The monitor
70
+ * run status is running|done|failed|stopped|paused — there is no run-level 'queued', so a queued run reads as
71
+ * 'running' (the header shows the raw status text anyway; the box treats non-done/failed as active). */
72
+ function coerceRunStatus(s) {
73
+ switch (s) {
74
+ case 'completed':
75
+ return 'done';
76
+ case 'failed':
77
+ return 'failed';
78
+ case 'running':
79
+ return 'running';
80
+ default:
81
+ return 'running'; // queued / unknown-future → render as an active run (open-set fallback)
82
+ }
83
+ }
84
+ /** detail phase status (service projects {title,status,done,total}) → the monitor's phase vocabulary. */
85
+ function coercePhaseStatus(s) {
86
+ switch (s) {
87
+ case 'done':
88
+ case 'completed':
89
+ return 'done';
90
+ case 'failed':
91
+ return 'failed';
92
+ case 'not-started':
93
+ case 'queued':
94
+ case 'pending':
95
+ return 'not-started';
96
+ default:
97
+ return 'running';
98
+ }
99
+ }
100
+ /** SDK WorkflowAgentRow.status ("the input vocab the display derivation reads", types.ts:352) → the monitor's
101
+ * AgentState input set (start|progress|queued|done|error|inactive — the WPe INPUT vocabulary the monitor
102
+ * derives DisplayStatus from). We pass the atlas vocab through and tolerate service display-synonyms. */
103
+ function coerceAgentState(s) {
104
+ switch (s) {
105
+ case 'start':
106
+ case 'progress':
107
+ case 'queued':
108
+ case 'done':
109
+ case 'error':
110
+ case 'inactive':
111
+ return s;
112
+ // service display-vocab synonyms (should the wire emit them) → the monitor's input state.
113
+ case 'running':
114
+ case 'active':
115
+ return 'progress';
116
+ case 'completed':
117
+ return 'done';
118
+ case 'failed':
119
+ return 'error';
120
+ // service detail `displayStatus` (core deriveAgentDisplayStatus): `interrupted` = a non-terminal agent
121
+ // under a terminated run → the monitor's `inactive` input (WPe lands it on 'interrupted' display via
122
+ // !workflowActive — the run IS terminal whenever core emits this).
123
+ case 'interrupted':
124
+ return 'inactive';
125
+ default:
126
+ return 'progress'; // unknown-future live status → render as an active agent (open-set fallback)
127
+ }
128
+ }
129
+ /** Project the bounded activity beats (WorkflowActivityBeat, start/end PAIRS) → the monitor's per-call
130
+ * WorkflowToolCall[]. The 'start' beat carries the SHORT secret-scrubbed primary-arg summary (types.ts:339);
131
+ * we fold each tool CALL (keyed by toolCallId) to one row {name, summary?}, keeping the first non-empty arg
132
+ * as the summary. Beats without a toolName are dropped (heartbeat/partial). Order preserved. */
133
+ /** clay ⑤ 案(2026-07-22 截图定谳):beat.arg 契约上是 SHORT 摘要,但 StructuredOutput 等工具的
134
+ * 主参数=整段多行内容(server 侧 scrub 不折行)——Activity 行渲染用 truncateToWidth 只截宽度
135
+ * 不清换行,首个换行之后的行顶格渲出面板(样式逃框)。数据层单点单行化:取首行,有后续行缀 …;
136
+ * 两个消费点(Activity 行 + lastToolSummary)一并被护。 */
137
+ function oneLineSummary(s) {
138
+ const nl = s.search(/\r?\n/);
139
+ if (nl < 0)
140
+ return s;
141
+ const head = s.slice(0, nl).trimEnd();
142
+ return head ? `${head}…` : '…';
143
+ }
144
+ function projectActivity(beats) {
145
+ if (!beats?.length)
146
+ return [];
147
+ const byCall = new Map();
148
+ const order = [];
149
+ let anon = 0;
150
+ for (const b of beats) {
151
+ const name = typeof b.toolName === 'string' ? b.toolName.trim() : '';
152
+ if (!name)
153
+ continue;
154
+ const key = typeof b.toolCallId === 'string' && b.toolCallId ? b.toolCallId : `#${anon++}`;
155
+ if (!byCall.has(key))
156
+ order.push(key);
157
+ const prev = byCall.get(key);
158
+ const arg = typeof b.arg === 'string' && b.arg.trim() ? oneLineSummary(b.arg.trim()) : prev?.summary;
159
+ const call = { name };
160
+ if (arg)
161
+ call.summary = arg;
162
+ byCall.set(key, { ...prev, ...call });
163
+ }
164
+ return order.map(k => byCall.get(k)).filter((c) => !!c);
165
+ }
166
+ /** one WorkflowAgentRow → the monitor's WorkflowAgent (+ a private `_phase` carrier for phase bucketing). */
167
+ function projectAgent(row, index) {
168
+ // WorkflowAgentRow carries no TYPED id — read a stable id from the permissive fields, else index-derive.
169
+ const rec = row;
170
+ const id = String(rec.id ?? rec.agentId ?? `agent-${index}`);
171
+ const agentId = typeof rec.agentId === 'string' && rec.agentId ? rec.agentId : id;
172
+ // Prefer the service detail's `displayStatus` (core's shared deriveAgentDisplayStatus:
173
+ // queued|running|done|failed|interrupted) over the RAW record `status` (running|completed|failed): the raw
174
+ // vocabulary carries no queued/interrupted arm — a queued agent's record status is literally 'running', so
175
+ // coercing `row.status` alone rendered every queued agent as "Running" (the [840] parity gap).
176
+ const displayStatus = typeof rec.displayStatus === 'string' ? rec.displayStatus : undefined;
177
+ const state = coerceAgentState(displayStatus ?? row.status);
178
+ const activity = projectActivity(row.activity);
179
+ const last = activity.length ? activity[activity.length - 1] : undefined;
180
+ const label = typeof row.label === 'string' && row.label.trim() ? row.label.trim() : id || 'agent';
181
+ const agent = { id, agentId, label, state };
182
+ if (typeof row.model === 'string')
183
+ agent.model = row.model;
184
+ if (typeof row.tokens === 'number')
185
+ agent.tokens = row.tokens;
186
+ if (typeof row.toolCalls === 'number')
187
+ agent.toolCalls = row.toolCalls;
188
+ if (activity.length)
189
+ agent.activity = activity;
190
+ if (last) {
191
+ agent.lastToolName = last.name;
192
+ if (last.summary)
193
+ agent.lastToolSummary = last.summary;
194
+ }
195
+ if (typeof row.prompt === 'string')
196
+ agent.promptPreview = row.prompt;
197
+ if (typeof row.output === 'string')
198
+ agent.resultPreview = row.output;
199
+ // WPe (agentDisplayStatus) derives queued-vs-running from queuedAt/startedAt on the active path. The service
200
+ // detail ALWAYS sends `queuedAt` (MF-W 1.150: enqueue time) and sends `startedAt` only once the agent acquired
201
+ // a concurrency slot — so PASS BOTH THROUGH unconditionally and WPe derives faithfully (queuedAt set +
202
+ // startedAt absent → 'queued'; both set → 'running'). The old projection dropped queuedAt unless the coerced
203
+ // state was literally 'queued' (which the raw record vocabulary never produces) — every queued agent lost its
204
+ // timestamps and rendered 'running'. Synthesize queuedAt only for a wire that says 'queued' without one.
205
+ const startedAt = typeof rec.startedAt === 'number' ? rec.startedAt : undefined;
206
+ const queuedAt = typeof rec.queuedAt === 'number' ? rec.queuedAt : undefined;
207
+ const durationMs = typeof rec.durationMs === 'number' ? rec.durationMs : undefined;
208
+ const lastProgressAt = typeof rec.lastProgressAt === 'number' ? rec.lastProgressAt : undefined;
209
+ if (durationMs != null)
210
+ agent.durationMs = durationMs;
211
+ if (lastProgressAt != null)
212
+ agent.lastProgressAt = lastProgressAt;
213
+ if (queuedAt != null)
214
+ agent.queuedAt = queuedAt;
215
+ if (startedAt != null)
216
+ agent.startedAt = startedAt;
217
+ if (state === 'queued' && agent.queuedAt == null)
218
+ agent.queuedAt = Date.now();
219
+ const phase = typeof row.phase === 'string' && row.phase.trim() ? row.phase.trim() : undefined;
220
+ return Object.assign(agent, phase ? { _phase: phase } : {});
221
+ }
222
+ /** read the (permissive) structured phase rows the service detail projects (`{title,status,done,total,…}`). */
223
+ function readPhases(run) {
224
+ const out = [];
225
+ for (const p of run.phases ?? []) {
226
+ if (p && typeof p === 'object') {
227
+ const rec = p;
228
+ out.push({
229
+ title: typeof rec.title === 'string' && rec.title.trim() ? rec.title.trim() : '',
230
+ status: rec.status,
231
+ done: typeof rec.done === 'number' ? rec.done : undefined,
232
+ total: typeof rec.total === 'number' ? rec.total : undefined,
233
+ });
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+ function stripPhase(a) {
239
+ const { _phase: _ignored, ...rest } = a;
240
+ void _ignored;
241
+ return rest;
242
+ }
243
+ /** Build the monitor's WorkflowPhase[] from the run's structured phases + the agents' `_phase` tags. Three
244
+ * paths, all degrade-safe: (1) structured phases on the wire → bucket agents by their phase title (an agent
245
+ * whose phase matches none lands in the first phase); (2) no structured phases but agents carry phase tags →
246
+ * one phase per distinct tag; (3) neither → a single synthetic 'Workflow' phase holding every agent. */
247
+ function projectPhases(run, agents) {
248
+ const raw = readPhases(run);
249
+ if (raw.length > 0) {
250
+ const known = new Set(raw.map(p => p.title));
251
+ const buckets = new Map();
252
+ for (const p of raw)
253
+ buckets.set(p.title, []);
254
+ for (const a of agents) {
255
+ const t = a._phase && known.has(a._phase) ? a._phase : raw[0].title;
256
+ buckets.get(t).push(a);
257
+ }
258
+ return raw.map(p => {
259
+ const bucket = buckets.get(p.title) ?? [];
260
+ return {
261
+ title: p.title || 'Phase',
262
+ status: coercePhaseStatus(p.status),
263
+ doneCount: p.done ?? bucket.filter(a => a.state === 'done').length,
264
+ totalCount: p.total ?? bucket.length,
265
+ agents: bucket.map(stripPhase),
266
+ };
267
+ });
268
+ }
269
+ const distinct = [...new Set(agents.map(a => a._phase).filter((t) => !!t))];
270
+ if (distinct.length > 0) {
271
+ return distinct.map(title => {
272
+ const bucket = agents.filter(a => a._phase === title);
273
+ const allDone = bucket.every(a => a.state === 'done');
274
+ return {
275
+ title,
276
+ status: bucket.some(a => a.state === 'error') ? 'failed' : allDone ? 'done' : 'running',
277
+ doneCount: bucket.filter(a => a.state === 'done').length,
278
+ totalCount: bucket.length,
279
+ agents: bucket.map(stripPhase),
280
+ };
281
+ });
282
+ }
283
+ return [
284
+ {
285
+ title: 'Workflow',
286
+ status: run.status === 'completed' ? 'done' : run.status === 'failed' ? 'failed' : 'running',
287
+ doneCount: agents.filter(a => a.state === 'done').length,
288
+ totalCount: agents.length,
289
+ agents: agents.map(stripPhase),
290
+ },
291
+ ];
292
+ }
293
+ /** THE projector: SDK `WorkflowRun` (client.workflows.get) → the monitor's `WorkflowRunState`. Pure/total —
294
+ * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
295
+ * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
296
+ export function projectWorkflowRun(run) {
297
+ const agents = (run.agents ?? []).map((r, i) => projectAgent(r, i));
298
+ const phases = projectPhases(run, agents);
299
+ const ownTokens = run.stats?.tokens ?? 0;
300
+ const nestedTokens = run.stats?.nested?.tokens ?? 0;
301
+ const totalTokens = run.stats?.tokens != null || run.stats?.nested?.tokens != null
302
+ ? ownTokens + nestedTokens
303
+ : agents.reduce((s, a) => s + (a.tokens ?? 0), 0); // no stats on the wire → sum the agent rows
304
+ const state = {
305
+ workflowRunId: run.id,
306
+ status: coerceRunStatus(run.status),
307
+ // The authored script TEXT is not projected to the wire (types.ts:375 — service does not emit it), so leave
308
+ // it empty: the monitor's "save script" affordance stays off (canSave = script.length > 0). Never fabricate
309
+ // a script we do not have. scriptPath is likewise absent until the service projects it.
310
+ script: '',
311
+ agentCount: agents.length,
312
+ totalTokens,
313
+ phases,
314
+ };
315
+ if (run.name)
316
+ state.name = run.name;
317
+ if (run.description)
318
+ state.description = run.description;
319
+ return state;
320
+ }
321
+ /**
322
+ * createLiveWorkflowSource({ baseUrl, authToken, principal, workflowId? }) — the LIVE workflow-monitor source.
323
+ *
324
+ * Constructs the AgentClient and kicks off a background loop that (1) resolves a run id (explicit config, else
325
+ * the first running run from `list()`), (2) snapshots it via `get()` → project → store, and (3) follows
326
+ * `stream()` as a change-ticker (re-`get()` throttled on each frame). Every step is degrade-safe: a 501 (S8
327
+ * off) / 404 (non-owner/absent) / empty list / connection error is RECORDED (status().degraded/code) and
328
+ * leaves `snapshot()` null so the host falls back to the mock/empty state — the source never throws into it.
329
+ */
330
+ export function createLiveWorkflowSource(config) {
331
+ let current = null;
332
+ let connected = false;
333
+ let degraded = false;
334
+ let code = null;
335
+ let runId = config.workflowId ?? null;
336
+ let lastError = null;
337
+ let disposed = false;
338
+ let lastGetAt = 0;
339
+ // 差分① 见头注:唯一构造点 + 显式 maxRetries: 2(= 收编前吃的 SDK 缺省;工厂缺省 0 是单发探针语义)。
340
+ const client = makeEngineWireClient({
341
+ baseUrl: config.baseUrl,
342
+ ...(config.authToken ? { token: config.authToken } : {}),
343
+ principal: config.principal,
344
+ maxRetries: 2,
345
+ });
346
+ const abort = new AbortController();
347
+ /** record a degrade/error WITHOUT throwing: 501 (S8 off) / 404 (non-owner|absent) are EXPECTED degrades;
348
+ * anything else (connection refused, parse) is a soft error — either way the host keeps the mock/empty. */
349
+ function noteDegrade(err) {
350
+ connected = false;
351
+ if (err instanceof APIError) {
352
+ code = err.status;
353
+ degraded = err.status === 501 || err.status === 404;
354
+ lastError = `${err.status} ${err.message}`;
355
+ }
356
+ else {
357
+ lastError = err instanceof Error ? err.message : String(err);
358
+ }
359
+ if (engineWireDebugEnabled()) {
360
+ hostLog('debug', `[sema][workflow] degrade/err (host falls back to mock/empty): ${lastError}`);
361
+ }
362
+ }
363
+ /** resolve the run id to monitor: explicit config → the first RUNNING run → the first run → null (empty). */
364
+ async function resolveRunId(signal) {
365
+ if (config.workflowId)
366
+ return config.workflowId;
367
+ const { workflows } = await client.workflows.list({ signal, ...engineSessionParamSpread() });
368
+ if (!workflows?.length)
369
+ return null;
370
+ const running = workflows.find(w => w.status === 'running');
371
+ return (running ?? workflows[0]).id;
372
+ }
373
+ /** take the authoritative snapshot + project it; records degrade on failure (never throws). */
374
+ async function refresh(id, signal) {
375
+ try {
376
+ const run = await client.workflows.get(id, { signal, ...engineSessionParamSpread() });
377
+ lastGetAt = Date.now();
378
+ current = projectWorkflowRun(run);
379
+ runId = run.id;
380
+ connected = true;
381
+ degraded = false;
382
+ code = null;
383
+ lastError = null;
384
+ }
385
+ catch (err) {
386
+ if (disposed || signal.aborted)
387
+ return;
388
+ noteDegrade(err);
389
+ }
390
+ }
391
+ /** background loop: resolve → snapshot → follow the stream (re-get on frames), degrade-safe throughout. */
392
+ async function consume() {
393
+ while (!disposed && !abort.signal.aborted) {
394
+ try {
395
+ const id = runId ?? (await resolveRunId(abort.signal));
396
+ if (!id) {
397
+ // owner has no runs → empty/degraded (not an error). Back off and re-poll (a run may appear).
398
+ degraded = true;
399
+ code = null;
400
+ connected = false;
401
+ await sleep(3000);
402
+ continue;
403
+ }
404
+ await refresh(id, abort.signal);
405
+ // snapshot degraded (501 S8-off / 404 gone) → don't attempt the stream; back off and retry.
406
+ if (degraded) {
407
+ await sleep(3000);
408
+ continue;
409
+ }
410
+ try {
411
+ // stream = "something changed" ticker. Re-GET the authoritative owner-scoped detail (throttled to
412
+ // ~400ms) rather than reduce the permissive core-internal WorkflowEvent shapes on the client. NOT
413
+ // resumable — a drop ends the iterator; the outer loop re-snapshots.
414
+ for await (const _frame of client.workflows.stream(id, { signal: abort.signal, ...engineSessionParamSpread() })) {
415
+ void _frame;
416
+ if (disposed)
417
+ return;
418
+ connected = true;
419
+ if (Date.now() - lastGetAt >= 400)
420
+ await refresh(id, abort.signal);
421
+ if (degraded)
422
+ break;
423
+ }
424
+ }
425
+ catch (err) {
426
+ if (disposed || abort.signal.aborted)
427
+ return;
428
+ noteDegrade(err); // 501 (S8 off) / 404 (gone) thrown BEFORE any frame, or a mid-stream transport drop
429
+ }
430
+ // stream ended (drop, not resumable) → re-snapshot on the next loop after a small backoff.
431
+ if (!disposed && !abort.signal.aborted)
432
+ await sleep(1000);
433
+ }
434
+ catch (err) {
435
+ if (disposed || abort.signal.aborted)
436
+ return;
437
+ noteDegrade(err);
438
+ await sleep(3000);
439
+ }
440
+ }
441
+ }
442
+ function sleep(ms) {
443
+ return new Promise(r => setTimeout(r, ms));
444
+ }
445
+ // 差分① 的 fail-soft 半场:工厂造不出 client(SDK 拒绝构造)⇒ 不起循环、恒 degraded,
446
+ // `snapshot()` 恒 null ⇒ 宿主走它既有的 mock/空态回落。**绝不抛进宿主**(与 501/404/空列表同路)。
447
+ if (client === null) {
448
+ degraded = true;
449
+ lastError = 'client construction failed (invalid baseUrl/credentials)';
450
+ }
451
+ else {
452
+ // kick off the background consumer (fire-and-forget; all failures captured into status()).
453
+ void consume();
454
+ }
455
+ return {
456
+ snapshot() {
457
+ return current;
458
+ },
459
+ status() {
460
+ return { connected, degraded, code, runId, lastError };
461
+ },
462
+ dispose() {
463
+ disposed = true;
464
+ abort.abort();
465
+ },
466
+ };
467
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):workflow 监视器的**状态契约**(MF-W ·
3
+ * contract/MF-W-workflow-monitor.md)从 cli `src/sema/overrides/workflow-detail-dialog.tsx:118-200`
4
+ * 搬入 —— 逐字,一个字段都没动。
5
+ *
6
+ * 🔴 **为什么类型必须先过来**:`workflowClient.projectWorkflowRun()`(同批搬入)的**产物**就是这套
7
+ * 形状。类型留在一个 `.tsx` 渲染文件里,等于「库产出的东西,它的契约住在 TUI 里」——
8
+ * web/桌面要消费同一份投影就得手抄一遍 shape,而手抄的那份**没有任何东西**会在它漂移时报红。
9
+ * 定位公理([1832])说的「CC 皮肤形状全收本包」,指的正是这种东西。
10
+ *
11
+ * 🔴 **拆缝**:`workflow-detail-dialog.tsx` 本身是 Ink 渲染件(设计稿 §2 分类表「Ink 渲染」),
12
+ * **留 TUI**;搬过来的只有 ① 六个契约类型 ② `agentDisplayStatus`(WPe —— 从 wire 输入词表
13
+ * 派生显示态的那条**纯**规则,是本契约的消费侧半场,端端都要用)。
14
+ * 留在 TUI 的:`computeVisibleWindow`(滚动窗口数学 = 视口概念,端专属)、glyph/color 映射、
15
+ * 以及整棵 JSX。壳侧 shim = 那个 tsx 改成从本包 re-export 这两样,自己不再定义。
16
+ */
17
+ export type AgentState = 'start' | 'progress' | 'queued' | 'done' | 'error' | 'inactive';
18
+ export type DisplayStatus = 'queued' | 'running' | 'done' | 'failed' | 'skipped' | 'interrupted';
19
+ export interface WorkflowToolCall {
20
+ name: string;
21
+ summary?: string;
22
+ }
23
+ export interface WorkflowAgent {
24
+ id: string;
25
+ label: string;
26
+ model?: string;
27
+ fallbackModel?: string;
28
+ agentType?: string;
29
+ isolation?: string;
30
+ remoteSessionId?: string;
31
+ cached?: boolean;
32
+ state: AgentState;
33
+ /** WPe maps state→display; `skipped` distinguishes an error that was a user-skip. */
34
+ skipped?: boolean;
35
+ tokens?: number;
36
+ toolCalls?: number;
37
+ durationMs?: number;
38
+ queuedAt?: number;
39
+ startedAt?: number;
40
+ /** ofm/C0l: `idle <t>` when running and lastProgressAt is ≥30s ago. (contract: idleSince) */
41
+ lastProgressAt?: number;
42
+ attempt?: number;
43
+ lastAttemptReason?: 'throttled' | 'user-retry' | 'stalled';
44
+ error?: string;
45
+ agentId?: string;
46
+ /** detail "Prompt" body; promptPreview is the fallback when no transcript is loaded. */
47
+ promptPreview?: string;
48
+ /** detail "Activity" rows. */
49
+ activity?: WorkflowToolCall[];
50
+ lastToolName?: string;
51
+ lastToolSummary?: string;
52
+ /** detail "Outcome" body for done agents (the StructuredOutput JSON / final text). */
53
+ resultPreview?: string;
54
+ }
55
+ export interface WorkflowPhase {
56
+ title: string;
57
+ status: 'not-started' | 'running' | 'done' | 'failed';
58
+ doneCount: number;
59
+ totalCount: number;
60
+ agents: WorkflowAgent[];
61
+ }
62
+ export interface WorkflowRunState {
63
+ workflowRunId?: string;
64
+ name?: string;
65
+ description?: string;
66
+ summary?: string;
67
+ script: string;
68
+ scriptPath?: string;
69
+ status: 'running' | 'done' | 'failed' | 'stopped' | 'paused';
70
+ cloud?: boolean;
71
+ agentCount: number;
72
+ totalTokens: number;
73
+ /** phases as authored; the component derives the active/clamped windows itself. */
74
+ phases: WorkflowPhase[];
75
+ /** the 187 reads e.workflowProgress to recompute phases; the mock pre-builds them. */
76
+ workflowProgress?: unknown;
77
+ }
78
+ /**
79
+ * WPe — agentDisplayStatus: agent.state (+ workflowActive) → display status.
80
+ *
81
+ * FAITHFUL to the atlas state SET (atlas 0476_076:30-34). The atlas `WPe` reads ONLY these inputs:
82
+ * if(state==="done") → "done"
83
+ * if(state==="error") → skipped?"skipped":"failed"
84
+ * if(!workflowActive) → "interrupted" // the inactive/non-active fallback
85
+ * else → (queuedAt!=null && startedAt==null) ? "queued" : "running"
86
+ * The atlas runner's agent-state enum the monitor sees is {start, progress, queued, done, error,
87
+ * inactive} — there is NO `failed`/`skipped`/`interrupted`/`stopped`/`running` INPUT state; those are
88
+ * DISPLAY statuses derived here: `failed`/`skipped` from `error` (+ the `skipped` flag), `interrupted` from
89
+ * `!workflowActive` (the `inactive`/non-active fallback), and `queued`/`running` from the queuedAt/startedAt
90
+ * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
91
+ * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
92
+ * DisplayStatus set + glyph/color mapping ($jn) is identical.
93
+ */
94
+ export declare function agentDisplayStatus(a: WorkflowAgent, workflowActive: boolean): DisplayStatus;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):workflow 监视器的**状态契约**(MF-W ·
3
+ * contract/MF-W-workflow-monitor.md)从 cli `src/sema/overrides/workflow-detail-dialog.tsx:118-200`
4
+ * 搬入 —— 逐字,一个字段都没动。
5
+ *
6
+ * 🔴 **为什么类型必须先过来**:`workflowClient.projectWorkflowRun()`(同批搬入)的**产物**就是这套
7
+ * 形状。类型留在一个 `.tsx` 渲染文件里,等于「库产出的东西,它的契约住在 TUI 里」——
8
+ * web/桌面要消费同一份投影就得手抄一遍 shape,而手抄的那份**没有任何东西**会在它漂移时报红。
9
+ * 定位公理([1832])说的「CC 皮肤形状全收本包」,指的正是这种东西。
10
+ *
11
+ * 🔴 **拆缝**:`workflow-detail-dialog.tsx` 本身是 Ink 渲染件(设计稿 §2 分类表「Ink 渲染」),
12
+ * **留 TUI**;搬过来的只有 ① 六个契约类型 ② `agentDisplayStatus`(WPe —— 从 wire 输入词表
13
+ * 派生显示态的那条**纯**规则,是本契约的消费侧半场,端端都要用)。
14
+ * 留在 TUI 的:`computeVisibleWindow`(滚动窗口数学 = 视口概念,端专属)、glyph/color 映射、
15
+ * 以及整棵 JSX。壳侧 shim = 那个 tsx 改成从本包 re-export 这两样,自己不再定义。
16
+ */
17
+ /**
18
+ * WPe — agentDisplayStatus: agent.state (+ workflowActive) → display status.
19
+ *
20
+ * FAITHFUL to the atlas state SET (atlas 0476_076:30-34). The atlas `WPe` reads ONLY these inputs:
21
+ * if(state==="done") → "done"
22
+ * if(state==="error") → skipped?"skipped":"failed"
23
+ * if(!workflowActive) → "interrupted" // the inactive/non-active fallback
24
+ * else → (queuedAt!=null && startedAt==null) ? "queued" : "running"
25
+ * The atlas runner's agent-state enum the monitor sees is {start, progress, queued, done, error,
26
+ * inactive} — there is NO `failed`/`skipped`/`interrupted`/`stopped`/`running` INPUT state; those are
27
+ * DISPLAY statuses derived here: `failed`/`skipped` from `error` (+ the `skipped` flag), `interrupted` from
28
+ * `!workflowActive` (the `inactive`/non-active fallback), and `queued`/`running` from the queuedAt/startedAt
29
+ * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
30
+ * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
31
+ * DisplayStatus set + glyph/color mapping ($jn) is identical.
32
+ */
33
+ export function agentDisplayStatus(a, workflowActive) {
34
+ if (a.state === 'done')
35
+ return 'done';
36
+ // atlas: `state==="error"` → skipped?"skipped":"failed".
37
+ if (a.state === 'error')
38
+ return a.skipped ? 'skipped' : 'failed';
39
+ // atlas `!t` (workflow not active) → interrupted (also the `inactive`-state landing).
40
+ if (!workflowActive)
41
+ return 'interrupted';
42
+ return a.queuedAt != null && a.startedAt == null ? 'queued' : 'running';
43
+ }