@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,191 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/seam/adapter/upstream/controlRouter.ts 逐字搬入(闭包 1 文件,只 import SDK)。
3
+ */
4
+ /**
5
+ * src/seam/adapter/upstream/controlRouter.ts — the UPSTREAM control-VERB router (NEW file, no CC-logic edit).
6
+ *
7
+ * Where `hitlBridge.ts` owns the suspended → decide → resume HITL loop (the gate-resolution verbs:
8
+ * `approvals.decide` / `assistant.planReview` / `assistant.resume`), THIS module owns the OTHER half of the
9
+ * upstream control channel (contract/04 §5/§6, contract/08 IH-7/IH-8): the SUPERVISION verbs that act on a
10
+ * run *while it works* — submit a turn, steer it mid-flight, kill it, and the client-side QUEUED-COMMAND
11
+ * ordering that has no wire of its own. It is the single choke point that maps a shell control INTENT onto
12
+ * the right `client.runs.*` verb, branching the §9.1 error taxonomy fail-closed.
13
+ *
14
+ * The five routes (contract/04 §5/§6; 08 IH-7/IH-8; the catalog rows it binds):
15
+ *
16
+ * 1. **submit** (IH-1/IH-2, catalog L73/L74). A turn → its OWN durable run: `runs.create(TaskRequest)` →
17
+ * 202 `RunReceipt`. The SAME `sessionId` continues the conversation (the service serializes turns per
18
+ * session); the SAME `jobId` groups the sub-runs into ONE Task in the work view (contract/04 §5.2,
19
+ * types.ts:59-63). `runs.create` is a SUBMIT → it carries an idempotency key and the SDK may retry it.
20
+ *
21
+ * 2. **steer** (IH-7, §5.1, catalog L81). Inject mid-flight direction into a RUNNING durable run:
22
+ * `runs.steer(taskId, { text, mode? })`. The text is untrusted DATA — the shell sends it RAW and the
23
+ * SERVER fences it (the shell does NOT pre-sanitize control markers). AT-MOST-ONCE: steer is NOT
24
+ * idempotent, so it is NEVER retried (runs.ts:43). `mode` drains queued steers at the TURN boundary
25
+ * ("all" = inject all next turn; "one-at-a-time" = one per turn, FIFO). Errors are fail-closed:
26
+ * `SteeringNotRunningError` (run suspended/terminal — resolve its gate, don't steer) and
27
+ * `SteeringInvalidContentError` (the text carried a control-plane escape — SURFACE, never strip-and-retry).
28
+ *
29
+ * 3. **cancel** (IH-8, §5.3, catalog L84). HARD-STOP an async run: `runs.cancel(taskId)` → 202 `CancelAck`
30
+ * (`status:"cancelling"` or a terminal no-op). The run then SETTLES to `failed` + `errorCode:"cancelled"`
31
+ * (NOT a new status — the UI shows "cancelled", not an error). A 409 means the run is SUSPENDED → it must
32
+ * be cancelled by DENYING its approval (`hitlBridge.decideTool({decision:"deny"})`), never by `cancel`;
33
+ * the router surfaces a `ControlSafetyError('cancel_suspended')` telling the shell to route to deny. 404 =
34
+ * non-owner/unknown (no existence leak). Server-idempotent; not a submit → no SDK retry.
35
+ *
36
+ * 4. **queued commands** (§5.2, catalog L82 `contract-extension`). CC enqueues next turns with a per-message
37
+ * `priority: 'now'|'next'|'later'` and drops a queued message by uuid (`cancel_async_message`). The SDK
38
+ * has NEITHER: no `priority` on `TaskRequest`, no drop-queued verb. The faithful behavior is CLIENT-SIDE:
39
+ * hold a local priority-ordered queue, allow a DEQUEUE *before* submit (the only stop a queued message
40
+ * has — once submitted the nearest lever is `cancel`, which kills the whole run, not one message), and
41
+ * drain by submitting each queued turn as its own run under the same sessionId+jobId.
42
+ *
43
+ * Provider- and presentation-agnostic (contract/04 laws 2-3; 08 invariants): every verb stays at
44
+ * `TaskRequest` / `RunReceipt` / `CancelAck` altitude. No Anthropic `effort`/`fast_mode`, no claude.ai
45
+ * control-message shape, no widget/glyph crosses this seam. The router emits the SDK verb; the shell owns the
46
+ * keybinding, the queue chrome, and the "cancelled" badge.
47
+ */
48
+ import type { TaskRequest, RunReceipt, CancelAck, Scenario } from '@sema-agent/sdk';
49
+ export interface RunsResourceLike {
50
+ /** POST /v1/runs → 202 RunReceipt. SUBMIT-class: carries an idempotency key, SDK may retry (runs.ts:13). */
51
+ create(req: TaskRequest, opts?: {
52
+ idempotencyKey?: string | null;
53
+ signal?: AbortSignal;
54
+ }): Promise<RunReceipt>;
55
+ /** POST /v1/runs/:id/steer — inject mid-flight direction into a RUNNING run. AT-MOST-ONCE: NOT a submit,
56
+ * NEVER retried (runs.ts:51). The text is untrusted DATA the server fences. */
57
+ steer(taskId: string, steer: {
58
+ text: string;
59
+ mode?: 'all' | 'one-at-a-time';
60
+ }, opts?: {
61
+ signal?: AbortSignal;
62
+ }): Promise<unknown>;
63
+ /** POST /v1/runs/:id/cancel → 202 CancelAck. Hard-stop; server-idempotent; not a submit → no retry
64
+ * (runs.ts:34). 409 if suspended (deny instead); 404 non-owner. */
65
+ cancel(taskId: string, opts?: {
66
+ signal?: AbortSignal;
67
+ }): Promise<CancelAck>;
68
+ }
69
+ export interface ControlClientLike {
70
+ runs: RunsResourceLike;
71
+ }
72
+ /** A supervision-verb stop the shell must HANDLE, not retry (contract/04 §9.1). The `code` is stable so the
73
+ * shell can branch: `not_running` (steer a non-running run) | `invalid_content` (steer carried an escape) |
74
+ * `cancel_suspended` (cancel a suspended run — route to DENY instead) | `not_found` (non-owner/unknown). */
75
+ export declare class ControlSafetyError extends Error {
76
+ readonly code: 'not_running' | 'invalid_content' | 'cancel_suspended' | 'not_found';
77
+ /** The original SDK error, for logging (never re-thrown blind). */
78
+ readonly cause?: unknown | undefined;
79
+ constructor(message: string, code: 'not_running' | 'invalid_content' | 'cancel_suspended' | 'not_found',
80
+ /** The original SDK error, for logging (never re-thrown blind). */
81
+ cause?: unknown | undefined);
82
+ }
83
+ export type QueuePriority = 'now' | 'next' | 'later';
84
+ /** A turn the user queued but has not yet submitted. `id` is a local uuid the shell uses to DEQUEUE it. */
85
+ export interface QueuedCommand {
86
+ id: string;
87
+ text: string;
88
+ priority: QueuePriority;
89
+ }
90
+ export interface ControlRouterOptions {
91
+ /** Continue a conversation: the SAME sessionId across turns (contract/04 §5.2; types.ts:59). Omit to start
92
+ * fresh — the service mints one and the shell adopts it from the first `RunReceipt.sessionId`. */
93
+ sessionId?: string;
94
+ /** Group the sub-runs of one logical job into one Task (contract/04 §5.2; types.ts:62). */
95
+ jobId?: string;
96
+ /** Scenario passthrough (default/oa/code-review/team) applied to every submitted turn. */
97
+ scenario?: Scenario;
98
+ /** Per-submit system prompt the integrator owns (types.ts:64 — stable cacheable prefix). */
99
+ systemPrompt?: string;
100
+ }
101
+ export declare class ControlRouter {
102
+ private readonly client;
103
+ /** The run currently in flight — the target of steer/cancel. Null before the first submit. */
104
+ private taskId;
105
+ /** Adopted from the first RunReceipt when the caller did not pin one (service-minted continuity). */
106
+ private sessionId;
107
+ private readonly jobId;
108
+ private readonly scenario;
109
+ private readonly systemPrompt;
110
+ /** The client-side queued turns (contract/04 §5.2 — no wire equivalent). */
111
+ private readonly queue;
112
+ private seq;
113
+ constructor(client: ControlClientLike, opts?: ControlRouterOptions);
114
+ /** The run currently in flight, if any. The shell reads this to know what steer/cancel will target. */
115
+ activeTaskId(): string | null;
116
+ /** The session token threading the conversation (service-minted after the first submit). */
117
+ activeSessionId(): string | undefined;
118
+ /**
119
+ * Submit a turn as a durable async run (`runs.create` → 202 `RunReceipt`). The SAME `sessionId` continues
120
+ * the conversation (the service serializes turns per session — a session-CAS conflict while a prior run
121
+ * holds the claim → 409 `Conflict`, contract/04 §5.2 / openapi:173); the SAME `jobId` groups the sub-runs
122
+ * into one Task in the work view. `runs.create` is a SUBMIT, so the idempotency key rides along (the SDK
123
+ * auto-generates one and may safely retry on transport failure).
124
+ *
125
+ * Adopts the service-minted `sessionId` from the receipt when the caller did not pin one (continuity), and
126
+ * latches the new run as the active steer/cancel target.
127
+ */
128
+ submit(text: string, opts?: {
129
+ signal?: AbortSignal;
130
+ }): Promise<RunReceipt>;
131
+ /** Project the session context + the typed turn into the `@sema-ai` `TaskRequest` wire shape. Secret
132
+ * discipline (types.ts:54-58): NEVER put tokens in `objective`/`systemPrompt`. */
133
+ private buildTaskRequest;
134
+ /**
135
+ * Steer the RUNNING run: `runs.steer(taskId, { text, mode? })`. Hard rules this method enforces:
136
+ *
137
+ * - **The text is untrusted DATA — sent RAW.** The shell does NOT pre-sanitize control markers; the SERVER
138
+ * fences the text (contract/04 §5.1). A text carrying a control-plane escape → 422
139
+ * `SteeringInvalidContentError` → surfaced as `ControlSafetyError('invalid_content')` (SURFACE the error;
140
+ * never strip-and-retry).
141
+ * - **`trusted` is NOT a client field** — the server derives trust from the authenticated operator role;
142
+ * this method has no way to assert it, by construction.
143
+ * - **AT-MOST-ONCE — never retried.** `runs.steer` is not idempotent and not a submit, so a transport
144
+ * failure propagates; the router does NOT auto-resend (a double-steer must be impossible).
145
+ * - **Steer needs a RUNNING run.** A suspended/terminal run → 409 `SteeringNotRunningError` → surfaced as
146
+ * `ControlSafetyError('not_running')` (resolve its gate via the hitlBridge `decide`, don't steer).
147
+ * - **`mode` drains at the TURN boundary** (FORWARD-DRAFT per-call mapping, runs.ts:49-50): `"all"` injects
148
+ * all queued steers at the next turn; `"one-at-a-time"` (default) one per turn, FIFO. Steer applies at
149
+ * the next turn boundary, not instantly.
150
+ */
151
+ steer(text: string, mode?: 'all' | 'one-at-a-time', opts?: {
152
+ signal?: AbortSignal;
153
+ }): Promise<unknown>;
154
+ /**
155
+ * Cancel (kill) the in-flight run: `runs.cancel(taskId)` → 202 `CancelAck` (`status:"cancelling"` or a
156
+ * terminal no-op). The run then SETTLES to `failed` + `errorCode:"cancelled"` — the UI shows "cancelled",
157
+ * NOT an error (the router does not synthesize that terminal; the downstream stream / `terminalToSdkResult`
158
+ * does). Branching (contract/04 §9.1):
159
+ *
160
+ * - **409 = the run is SUSPENDED.** A suspended run is cancelled by DENYING its approval, NOT by `cancel`
161
+ * (which 409s). Surfaced as `ControlSafetyError('cancel_suspended')` so the shell routes the kill to
162
+ * `hitlBridge.decideTool({decision:"deny"})` instead.
163
+ * - **404 = non-owner / unknown.** No existence oracle (404, not 403) → `ControlSafetyError('not_found')`.
164
+ *
165
+ * Server-idempotent and NOT a submit, so a successful cancel is safe to repeat (a terminal run no-ops);
166
+ * the router still does not auto-retry on transport failure (the shell re-invokes on the user's repeat).
167
+ */
168
+ cancel(opts?: {
169
+ signal?: AbortSignal;
170
+ }): Promise<CancelAck>;
171
+ /** Enqueue a turn for later submission. `now` jumps the line, `next` (default) is FIFO, `later` trails.
172
+ * Returns the queued row (its `id` is the dequeue handle). Nothing leaves the machine until `drainNext`. */
173
+ enqueue(text: string, priority?: QueuePriority): QueuedCommand;
174
+ /** Drop a STILL-QUEUED turn by id BEFORE it is submitted (the only stop a queued message has —
175
+ * contract/04 §5.2). Returns true if it was in the queue, false if absent / already drained (mirrors CC's
176
+ * `cancel_async_message` → `{cancelled:false}` "not in the queue / already dequeued"). */
177
+ dequeue(id: string): boolean;
178
+ /** A stable, priority-banded view of the queue (the shell renders this; drain order matches it). */
179
+ pending(): readonly QueuedCommand[];
180
+ /**
181
+ * Drain the highest-priority queued turn: dequeue it and `submit` it as its own durable run (same session
182
+ * → continues the conversation; same jobId → one Task). Returns the `RunReceipt`, or null when the queue is
183
+ * empty. The session serializes turns, so the caller drains one at a time (submit, await the run, drain the
184
+ * next); submitting while a prior run holds the session claim surfaces the SDK's 409 `ConflictError` from
185
+ * `submit` — the shell waits and re-drains (contract/04 §5.2; openapi:173).
186
+ */
187
+ drainNext(opts?: {
188
+ signal?: AbortSignal;
189
+ }): Promise<RunReceipt | null>;
190
+ private nextId;
191
+ }
@@ -0,0 +1,244 @@
1
+ // ── Errors the router surfaces (fail-closed; never auto-retried) ───────────────
2
+ //
3
+ // Mirrors hitlBridge.HitlSafetyError: every safety-relevant control error is a signal to re-present to the
4
+ // human or surface the error, NEVER to silently retry or auto-decide (contract/04 §9.1 fail-closed law). The
5
+ // router catches the SDK's typed errors and re-raises a stable, shell-branchable code.
6
+ /** A supervision-verb stop the shell must HANDLE, not retry (contract/04 §9.1). The `code` is stable so the
7
+ * shell can branch: `not_running` (steer a non-running run) | `invalid_content` (steer carried an escape) |
8
+ * `cancel_suspended` (cancel a suspended run — route to DENY instead) | `not_found` (non-owner/unknown). */
9
+ export class ControlSafetyError extends Error {
10
+ code;
11
+ cause;
12
+ constructor(message, code,
13
+ /** The original SDK error, for logging (never re-thrown blind). */
14
+ cause) {
15
+ super(message);
16
+ this.code = code;
17
+ this.cause = cause;
18
+ this.name = 'ControlSafetyError';
19
+ }
20
+ }
21
+ /** Band rank for stable priority ordering: lower drains first. */
22
+ const BAND_RANK = { now: 0, next: 1, later: 2 };
23
+ export class ControlRouter {
24
+ client;
25
+ /** The run currently in flight — the target of steer/cancel. Null before the first submit. */
26
+ taskId = null;
27
+ /** Adopted from the first RunReceipt when the caller did not pin one (service-minted continuity). */
28
+ sessionId;
29
+ jobId;
30
+ scenario;
31
+ systemPrompt;
32
+ /** The client-side queued turns (contract/04 §5.2 — no wire equivalent). */
33
+ queue = [];
34
+ seq = 0;
35
+ constructor(client, opts = {}) {
36
+ this.client = client;
37
+ this.sessionId = opts.sessionId;
38
+ this.jobId = opts.jobId;
39
+ this.scenario = opts.scenario;
40
+ this.systemPrompt = opts.systemPrompt;
41
+ }
42
+ /** The run currently in flight, if any. The shell reads this to know what steer/cancel will target. */
43
+ activeTaskId() {
44
+ return this.taskId;
45
+ }
46
+ /** The session token threading the conversation (service-minted after the first submit). */
47
+ activeSessionId() {
48
+ return this.sessionId;
49
+ }
50
+ // ── §1. submit — a turn → its own durable run (contract/04 §5.2; 08 IH-2, catalog L74) ───────────────────
51
+ /**
52
+ * Submit a turn as a durable async run (`runs.create` → 202 `RunReceipt`). The SAME `sessionId` continues
53
+ * the conversation (the service serializes turns per session — a session-CAS conflict while a prior run
54
+ * holds the claim → 409 `Conflict`, contract/04 §5.2 / openapi:173); the SAME `jobId` groups the sub-runs
55
+ * into one Task in the work view. `runs.create` is a SUBMIT, so the idempotency key rides along (the SDK
56
+ * auto-generates one and may safely retry on transport failure).
57
+ *
58
+ * Adopts the service-minted `sessionId` from the receipt when the caller did not pin one (continuity), and
59
+ * latches the new run as the active steer/cancel target.
60
+ */
61
+ async submit(text, opts) {
62
+ const req = this.buildTaskRequest(text);
63
+ const receipt = await this.client.runs.create(req, opts ? { signal: opts.signal } : undefined);
64
+ // Adopt service-minted continuity + latch the in-flight run.
65
+ if (this.sessionId === undefined)
66
+ this.sessionId = receipt.sessionId;
67
+ this.taskId = receipt.taskId;
68
+ return receipt;
69
+ }
70
+ /** Project the session context + the typed turn into the `@sema-ai` `TaskRequest` wire shape. Secret
71
+ * discipline (types.ts:54-58): NEVER put tokens in `objective`/`systemPrompt`. */
72
+ buildTaskRequest(text) {
73
+ // 空内容守卫(2026-07-16 唤醒 400 批,同病同修):SDK/控制面递来的空/全空白 user_message 绝不作为
74
+ // 空 objective 上 wire——引擎会把空 user 消息写进会话持久历史(provider 400 + session 毒化)。
75
+ // 与 CC createUserMessage 的 NO_CONTENT_MESSAGE 同语义占位。
76
+ const req = { objective: text.trim().length > 0 ? text : '(no content)' };
77
+ if (this.sessionId !== undefined)
78
+ req.sessionId = this.sessionId;
79
+ if (this.jobId !== undefined)
80
+ req.jobId = this.jobId;
81
+ if (this.scenario !== undefined)
82
+ req.scenario = this.scenario;
83
+ if (this.systemPrompt !== undefined)
84
+ req.systemPrompt = this.systemPrompt;
85
+ return req;
86
+ }
87
+ // ── §2. steer — inject mid-flight direction (contract/04 §5.1; 08 IH-7, catalog L81) ─────────────────────
88
+ /**
89
+ * Steer the RUNNING run: `runs.steer(taskId, { text, mode? })`. Hard rules this method enforces:
90
+ *
91
+ * - **The text is untrusted DATA — sent RAW.** The shell does NOT pre-sanitize control markers; the SERVER
92
+ * fences the text (contract/04 §5.1). A text carrying a control-plane escape → 422
93
+ * `SteeringInvalidContentError` → surfaced as `ControlSafetyError('invalid_content')` (SURFACE the error;
94
+ * never strip-and-retry).
95
+ * - **`trusted` is NOT a client field** — the server derives trust from the authenticated operator role;
96
+ * this method has no way to assert it, by construction.
97
+ * - **AT-MOST-ONCE — never retried.** `runs.steer` is not idempotent and not a submit, so a transport
98
+ * failure propagates; the router does NOT auto-resend (a double-steer must be impossible).
99
+ * - **Steer needs a RUNNING run.** A suspended/terminal run → 409 `SteeringNotRunningError` → surfaced as
100
+ * `ControlSafetyError('not_running')` (resolve its gate via the hitlBridge `decide`, don't steer).
101
+ * - **`mode` drains at the TURN boundary** (FORWARD-DRAFT per-call mapping, runs.ts:49-50): `"all"` injects
102
+ * all queued steers at the next turn; `"one-at-a-time"` (default) one per turn, FIFO. Steer applies at
103
+ * the next turn boundary, not instantly.
104
+ */
105
+ async steer(text, mode, opts) {
106
+ if (this.taskId === null) {
107
+ throw new ControlSafetyError('no run in flight to steer — submit a turn first', 'not_running');
108
+ }
109
+ // Raw text, no pre-sanitize (the server fences). `mode` omitted → server default one-at-a-time.
110
+ const body = mode !== undefined ? { text, mode } : { text };
111
+ try {
112
+ return await this.client.runs.steer(this.taskId, body, opts ? { signal: opts.signal } : undefined);
113
+ }
114
+ catch (e) {
115
+ if (isSteeringNotRunning(e)) {
116
+ throw new ControlSafetyError('steering.not_running — the run is suspended/terminal; resolve its gate via decide, do not steer', 'not_running', e);
117
+ }
118
+ if (isSteeringInvalidContent(e)) {
119
+ throw new ControlSafetyError('steering.invalid_content — the steer text carried a control-plane escape; surface it, do not strip-and-retry', 'invalid_content', e);
120
+ }
121
+ // Any other error (auth/transport): NOT retried (steer is at-most-once) — propagate to the shell.
122
+ throw e;
123
+ }
124
+ }
125
+ // ── §3. cancel — hard-stop / kill the run (contract/04 §5.3; 08 IH-8, catalog L84) ───────────────────────
126
+ /**
127
+ * Cancel (kill) the in-flight run: `runs.cancel(taskId)` → 202 `CancelAck` (`status:"cancelling"` or a
128
+ * terminal no-op). The run then SETTLES to `failed` + `errorCode:"cancelled"` — the UI shows "cancelled",
129
+ * NOT an error (the router does not synthesize that terminal; the downstream stream / `terminalToSdkResult`
130
+ * does). Branching (contract/04 §9.1):
131
+ *
132
+ * - **409 = the run is SUSPENDED.** A suspended run is cancelled by DENYING its approval, NOT by `cancel`
133
+ * (which 409s). Surfaced as `ControlSafetyError('cancel_suspended')` so the shell routes the kill to
134
+ * `hitlBridge.decideTool({decision:"deny"})` instead.
135
+ * - **404 = non-owner / unknown.** No existence oracle (404, not 403) → `ControlSafetyError('not_found')`.
136
+ *
137
+ * Server-idempotent and NOT a submit, so a successful cancel is safe to repeat (a terminal run no-ops);
138
+ * the router still does not auto-retry on transport failure (the shell re-invokes on the user's repeat).
139
+ */
140
+ async cancel(opts) {
141
+ if (this.taskId === null) {
142
+ throw new ControlSafetyError('no run in flight to cancel', 'not_found');
143
+ }
144
+ try {
145
+ return await this.client.runs.cancel(this.taskId, opts ? { signal: opts.signal } : undefined);
146
+ }
147
+ catch (e) {
148
+ if (isCancelSuspendedConflict(e)) {
149
+ throw new ControlSafetyError('cancel on a SUSPENDED run (409) — cancel it by DENYING its approval (decide), not by runs.cancel', 'cancel_suspended', e);
150
+ }
151
+ if (isNotFound(e)) {
152
+ throw new ControlSafetyError('runs.cancel 404 — non-owner / unknown run (no existence leak)', 'not_found', e);
153
+ }
154
+ throw e;
155
+ }
156
+ }
157
+ // ── §4. queued commands — client-side priority + drop (contract/04 §5.2, catalog L82) ────────────────────
158
+ //
159
+ // The SDK has no per-message priority and no drop-queued verb. The router holds the queue locally: enqueue
160
+ // by priority band, dequeue a still-queued turn by its local id, and drain by submitting each as its own
161
+ // run under the same session. A submitted turn LEAVES the queue — its only after-submit stop is `cancel`.
162
+ /** Enqueue a turn for later submission. `now` jumps the line, `next` (default) is FIFO, `later` trails.
163
+ * Returns the queued row (its `id` is the dequeue handle). Nothing leaves the machine until `drainNext`. */
164
+ enqueue(text, priority = 'next') {
165
+ const cmd = { id: this.nextId(), text, priority };
166
+ this.queue.push(cmd);
167
+ return cmd;
168
+ }
169
+ /** Drop a STILL-QUEUED turn by id BEFORE it is submitted (the only stop a queued message has —
170
+ * contract/04 §5.2). Returns true if it was in the queue, false if absent / already drained (mirrors CC's
171
+ * `cancel_async_message` → `{cancelled:false}` "not in the queue / already dequeued"). */
172
+ dequeue(id) {
173
+ const i = this.queue.findIndex((c) => c.id === id);
174
+ if (i < 0)
175
+ return false;
176
+ this.queue.splice(i, 1);
177
+ return true;
178
+ }
179
+ /** A stable, priority-banded view of the queue (the shell renders this; drain order matches it). */
180
+ pending() {
181
+ return [...this.queue].sort((a, b) => BAND_RANK[a.priority] - BAND_RANK[b.priority]);
182
+ }
183
+ /**
184
+ * Drain the highest-priority queued turn: dequeue it and `submit` it as its own durable run (same session
185
+ * → continues the conversation; same jobId → one Task). Returns the `RunReceipt`, or null when the queue is
186
+ * empty. The session serializes turns, so the caller drains one at a time (submit, await the run, drain the
187
+ * next); submitting while a prior run holds the session claim surfaces the SDK's 409 `ConflictError` from
188
+ * `submit` — the shell waits and re-drains (contract/04 §5.2; openapi:173).
189
+ */
190
+ async drainNext(opts) {
191
+ const ordered = this.pending();
192
+ const head = ordered[0];
193
+ if (!head)
194
+ return null;
195
+ this.dequeue(head.id);
196
+ return this.submit(head.text, opts);
197
+ }
198
+ nextId() {
199
+ this.seq += 1;
200
+ return `q-${this.seq}-${uuid()}`;
201
+ }
202
+ }
203
+ // ── helpers ───────────────────────────────────────────────────────────────────
204
+ /** A v4-shaped uuid for the local queue handles (never on the wire). Mirrors types.ts/seamQuery's helper. */
205
+ function uuid() {
206
+ const g = typeof globalThis !== 'undefined'
207
+ ? globalThis.crypto
208
+ : undefined;
209
+ if (g && typeof g.randomUUID === 'function')
210
+ return g.randomUUID();
211
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
212
+ const r = (Math.random() * 16) | 0;
213
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
214
+ return v.toString(16);
215
+ });
216
+ }
217
+ /** Read a normalized error code/status off any SDK error shape variant (errorCode / code / name / status). */
218
+ function errCodes(e) {
219
+ if (!e || typeof e !== 'object')
220
+ return {};
221
+ const o = e;
222
+ return { code: o.errorCode ?? o.code, name: o.name, status: o.status ?? o.statusCode };
223
+ }
224
+ /** 409 `steering.not_running` / `SteeringNotRunningError` — the run is suspended/terminal (runs.ts:45). */
225
+ function isSteeringNotRunning(e) {
226
+ const { code, name } = errCodes(e);
227
+ return code === 'steering.not_running' || name === 'SteeringNotRunningError';
228
+ }
229
+ /** 422 `steering.invalid_content` / `SteeringInvalidContentError` — control-plane escape (runs.ts:46). */
230
+ function isSteeringInvalidContent(e) {
231
+ const { code, name } = errCodes(e);
232
+ return code === 'steering.invalid_content' || name === 'SteeringInvalidContentError';
233
+ }
234
+ /** A 409 on `cancel` means the run is SUSPENDED → cancel-by-deny instead (runs.ts:36; openapi:371-372).
235
+ * The SDK raises a generic `ConflictError` (status 409) for this case — there is no dedicated subclass. */
236
+ function isCancelSuspendedConflict(e) {
237
+ const { name, status } = errCodes(e);
238
+ return name === 'ConflictError' || status === 409;
239
+ }
240
+ /** 404 — non-owner / unknown run; the SDK raises `NotFoundError` (no existence oracle, contract/04 §9.1). */
241
+ function isNotFound(e) {
242
+ const { name, status } = errCodes(e);
243
+ return name === 'NotFoundError' || status === 404;
244
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/effortWire.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/effortWire.ts — the EFFORT/REASONING-STRENGTH ENGINE WIRE (per-turn `/effort` → engine).
6
+ *
7
+ * WHY THIS FILE EXISTS (the parity gap it closes):
8
+ * CC's `/effort` slider sets a per-turn reasoning strength; sema's picker writes it to `AppState.effortValue`
9
+ * (overrides/effort.tsx:302) but the seam's `toTaskRequest` never put it on the wire, so the picked effort
10
+ * reached the UI yet NOT the engine. Grounding (2026-06-29) showed the seam is ALREADY complete downstream:
11
+ * - core `TaskSpec.thinking: ThinkingLevel` = off|minimal|low|medium|high|xhigh|max (7 档,covers CC effort)
12
+ * — `core/src/core/types.ts:466`; consumed at `prepare-task.ts:1663` (→brain) / :1471 (xhigh/max→awareness).
13
+ * - service `reasoningEffort` → `thinking`, UNKNOWN value 400 fail-loud — `server.ts:234/3576`, `main.ts:1270`.
14
+ * So this is a SHELL-only wire: map the picked `EffortValue` → the wire `reasoningEffort` (a core ThinkingLevel)
15
+ * and stamp it on `TaskRequest`. Pure projection here (testable); the AppState read is inline in seamQuery.
16
+ *
17
+ * MAPPING: sema's picker can yield low|medium|high|max (base EffortLevel) + xhigh|ultracode|auto (187 override
18
+ * accept-set) + numeric. The wire `reasoningEffort` accepts a core ThinkingLevel only. So pass a value IFF it
19
+ * is a ThinkingLevel (off|minimal|low|medium|high|xhigh|max); `auto` (let engine default), `ultracode` (a
20
+ * separate mode, future slice), numeric and unset all OMIT — the engine then uses its own default.
21
+ */
22
+ /**
23
+ * The core ThinkingLevel = the service `reasoningEffort` accept-set (`server.ts:3576`) = the SDK
24
+ * `TaskRequest.reasoningEffort` union. Declared as a literal union (not bare `string`) so the projection's
25
+ * result narrows to exactly what `TaskRequest.reasoningEffort` accepts — a bare `string` would not be
26
+ * assignable to that enum (the false-green tsc hid this; see memory shell-tsc-check-broken-false-green).
27
+ */
28
+ export type ReasoningEffort = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
29
+ /**
30
+ * Project a picked `EffortValue` (string level | number | undefined) → the wire `reasoningEffort`, or
31
+ * undefined to OMIT. Returns the value verbatim iff it is a core ThinkingLevel; everything else (auto,
32
+ * ultracode, numeric efforts, undefined) → undefined so the engine applies its own default.
33
+ */
34
+ export declare function effortToReasoningEffort(effort: unknown): ReasoningEffort | undefined;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/effortWire.ts 逐字搬入(零依赖)。
3
+ */
4
+ /**
5
+ * src/sema/effortWire.ts — the EFFORT/REASONING-STRENGTH ENGINE WIRE (per-turn `/effort` → engine).
6
+ *
7
+ * WHY THIS FILE EXISTS (the parity gap it closes):
8
+ * CC's `/effort` slider sets a per-turn reasoning strength; sema's picker writes it to `AppState.effortValue`
9
+ * (overrides/effort.tsx:302) but the seam's `toTaskRequest` never put it on the wire, so the picked effort
10
+ * reached the UI yet NOT the engine. Grounding (2026-06-29) showed the seam is ALREADY complete downstream:
11
+ * - core `TaskSpec.thinking: ThinkingLevel` = off|minimal|low|medium|high|xhigh|max (7 档,covers CC effort)
12
+ * — `core/src/core/types.ts:466`; consumed at `prepare-task.ts:1663` (→brain) / :1471 (xhigh/max→awareness).
13
+ * - service `reasoningEffort` → `thinking`, UNKNOWN value 400 fail-loud — `server.ts:234/3576`, `main.ts:1270`.
14
+ * So this is a SHELL-only wire: map the picked `EffortValue` → the wire `reasoningEffort` (a core ThinkingLevel)
15
+ * and stamp it on `TaskRequest`. Pure projection here (testable); the AppState read is inline in seamQuery.
16
+ *
17
+ * MAPPING: sema's picker can yield low|medium|high|max (base EffortLevel) + xhigh|ultracode|auto (187 override
18
+ * accept-set) + numeric. The wire `reasoningEffort` accepts a core ThinkingLevel only. So pass a value IFF it
19
+ * is a ThinkingLevel (off|minimal|low|medium|high|xhigh|max); `auto` (let engine default), `ultracode` (a
20
+ * separate mode, future slice), numeric and unset all OMIT — the engine then uses its own default.
21
+ */
22
+ const REASONING_EFFORTS = new Set([
23
+ 'off',
24
+ 'minimal',
25
+ 'low',
26
+ 'medium',
27
+ 'high',
28
+ 'xhigh',
29
+ 'max',
30
+ ]);
31
+ /**
32
+ * Project a picked `EffortValue` (string level | number | undefined) → the wire `reasoningEffort`, or
33
+ * undefined to OMIT. Returns the value verbatim iff it is a core ThinkingLevel; everything else (auto,
34
+ * ultracode, numeric efforts, undefined) → undefined so the engine applies its own default.
35
+ */
36
+ export function effortToReasoningEffort(effort) {
37
+ return typeof effort === 'string' && REASONING_EFFORTS.has(effort) ? effort : undefined;
38
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineAgentPanelStore.ts 逐字搬入(零 import;壳侧改 re-export)。
3
+ * 🔴 同 subagentContentStore:buffer/listener/residentTaskIds 是单实例语义,发布侧(bridge/fleet
4
+ * 投影)与订阅侧(chrome-agentprogress 的面板 hook)必须同实例,否则事件发进一个副本、面板订另一个。
5
+ */
6
+ /**
7
+ * engineAgentPanelStore — dep-free side-channel store bridging ENGINE-side subagent progress
8
+ * (`task_progress` ticks, design/99 MF-10) to CC's CoordinatorTaskPanel (the footer live-agent panel).
9
+ *
10
+ * WHY A STORE (the retryStatusStore / liveQuestionStore pattern): the upstream bridge is a plain
11
+ * generator with no React/AppState access, while the panel reads `AppState.tasks` — so the bridge
12
+ * PUBLISHES neutral events here and a sema-owned hook inside chrome-agentprogress.tsx (already mounted
13
+ * under the footer) SUBSCRIBES and applies them to AppState via registerTask/updateTaskState. CC files
14
+ * stay verbatim; the AppState write side lives in the sema override that owns the panel.
15
+ *
16
+ * WHY THE PANEL (CC-187 parity): a local sync AgentTool renders an inline progress TRAIL from the
17
+ * subagent's real messages. An engine tick carries ONLY rollup numbers ({toolUses, totalTokens}) — the
18
+ * exact shape of CC's `AgentProgress` — and CC's own precedent for out-of-process agents (remote agent
19
+ * tasks) is the TASK PANEL + task-notification, not a synthetic inline trail. Faithful mapping, no
20
+ * fabricated subagent messages.
21
+ *
22
+ * Buffering: ticks published before the panel mounts are buffered (bounded) and replayed to the first
23
+ * subscriber, so early frames in a fast turn aren't lost to mount timing.
24
+ */
25
+ export type EngineAgentPanelEvent = {
26
+ kind: 'tick';
27
+ taskId: string;
28
+ /** Subagent display name off the wire tick (redacted upstream); absent ⇒ keep taskId. */
29
+ name?: string;
30
+ /** The delegating Task tool_use's `description` (the card title), when the bridge could bind one. */
31
+ description?: string;
32
+ /** The delegated prompt off the bound tool_use input, when present. */
33
+ prompt?: string;
34
+ /** core 1.262.0 [558]B (service 1.154.0 wire) — the child's most recent tool intent as ONE
35
+ * human line ("Bash npm test"), service-redacted. The panel consumer maps it onto the row's
36
+ * `progress.summary` (CC's own live-activity display slot); absent ⇒ slot untouched. */
37
+ currentAction?: string;
38
+ toolUses: number;
39
+ totalTokens: number;
40
+ } | {
41
+ /**
42
+ * 行帧 lane 投影(2026-07-26 「后台 Agent 子代不进 Background 面板」案)。
43
+ *
44
+ * 一手取证(verify/_bgagent-panel-lane-live-pty.mjs 首跑,engine 1.277.4 真机):两个
45
+ * `run_in_background:true` 的 fork 子代**活跃干活整跑**,`task_progress` tick 探针 **0 条**、
46
+ * fleet 行帧探针 **24 条** —— 父 turn 秒收口即关 SSE,子代的 tick 根本没有通道到壳;行帧
47
+ * lane(常开 fleet stream)才是后台子代唯一在场的 lane。面板的 local_agent 行此前**只**由
48
+ * tick 注册 ⇒ 产品自印的「N background agents launched (↓ to manage)」指向一个看不到它们的
49
+ * 面板(还顺带让 `ctrl+x ctrl+k … stop all agents` 的渲染门在真产品里永不成立)。
50
+ *
51
+ * 为什么是独立事件形而不是复用 'tick':两条 lane 的**可选性**不同 —— tick 的 `toolUses` 是
52
+ * 必填数,行帧的是**可选**位(server 1.278.0 才有,老引擎没有)。复用 tick 会强迫本 lane 在
53
+ * 缺席时编一个 0,把前台子代由真 tick 写进去的进度数字每秒清一次。消费端对本事件的规矩:
54
+ * 只建/只喂**本 lane 亲手建的行**,tick 一到就永久让位(fleetOwnedRowIds 摘除)。
55
+ */
56
+ kind: 'fleet-row';
57
+ taskId: string;
58
+ /** 行帧 agentType(fleet 行 label 列,如 'fork')→ 行的 agentType。 */
59
+ name?: string;
60
+ /** 行帧投影出的任务描述(如 'alpha-probe')→ 行的 description(面板行 label)。 */
61
+ description?: string;
62
+ /** core 1.429 → server 1.288([1831]②):子代当前工具的结构化形。`toolName` verbatim
63
+ * (查壳工具注册表),`target` 已上游 redact + 300 bound。缺席=旧端或子代从未跑过工具。 */
64
+ currentTool?: {
65
+ toolName: string;
66
+ target?: string;
67
+ };
68
+ /** 行帧累计 token。 */
69
+ totalTokens?: number;
70
+ /**
71
+ * server 1.278.0 起行帧也带的**累计**工具调用数(修「后台子代详情页 tool 计数恒 0」)。
72
+ *
73
+ * 🔴 三态,别写 `?? 0`:
74
+ * · 键在 ⇒ 真值(含真 0 = 引擎说「一个工具都没跑」);
75
+ * · 键不在 ⇒ 老引擎没这个位 = **不知道**,消费端如实留空/保留行上已有值,绝不写 0。
76
+ * 🔴 **累计不是增量**:bg sink 的 usage 是累计口径(与 `turn_end` 的每轮增量相反,后者在
77
+ * upstreamBridge 里要累加)——消费端直接赋值,绝不 `+=`。
78
+ *
79
+ * ⚠️ 时序差(server 主动提醒,不是接错):1.278.0 只在 **bg lane** 上线;workflow/subagent
80
+ * 子代走的 `task_progress`(tick lane)的 `usage.toolUses` 在 core 类型里本来就必填、数据一直
81
+ * 在,只是那条 lane 也没发 —— 已补齐但搭 1.278.0 之后的下一车。所以短期内「有的行有、有的
82
+ * 行没有」是**预期形**,两条 lane 的接线口径已按同一套三态写好。
83
+ */
84
+ toolUses?: number;
85
+ /**
86
+ * server 1.278.0 `transcriptId`(= core 铸的子代 childSessionId)—— 转录锚,**不是** prompt。
87
+ * 委派 prompt 走它:壳侧凭这个 id 去转录读面取首条 user 消息(engineDelegatedPrompt.ts),
88
+ * 修「详情页 Prompt 段恒空」。缺席 ⇒ 维持现状(诚实留白)。
89
+ */
90
+ transcriptId?: string;
91
+ } | {
92
+ kind: 'end';
93
+ taskId: string;
94
+ isError: boolean;
95
+ /** The subagent's FINAL REPORT — the delegating Task/Agent card's `tool_end.output` text (only the
96
+ * explicit close arm carries it; the defensive sweeps settle without one). The consumer persists it
97
+ * on the row (task.result) + appends it to a live-viewed transcript (拍1 查看态 seed). */
98
+ report?: string;
99
+ } | {
100
+ /** Exception-path safety net (published by seamQuery after every turn, throw or return):
101
+ * the consumer settles ALL rows it owns that are still running. Idempotent. */
102
+ kind: 'sweep';
103
+ };
104
+ export declare function markEnginePanelTaskResident(taskId: string): void;
105
+ export declare function clearEnginePanelTaskResident(taskId: string): void;
106
+ export declare function isEnginePanelTaskResident(taskId: string): boolean;
107
+ type Listener = (ev: EngineAgentPanelEvent) => void;
108
+ export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;
109
+ /** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
110
+ export declare function subscribeEngineAgentPanel(fn: Listener): () => void;
111
+ export {};