@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.
- package/README.md +23 -1
- package/dist/agentsWireCaps.d.ts +112 -0
- package/dist/agentsWireCaps.js +319 -0
- package/dist/attachmentsWireCaps.d.ts +47 -0
- package/dist/attachmentsWireCaps.js +46 -0
- package/dist/classifierVerdictWire.d.ts +45 -0
- package/dist/classifierVerdictWire.js +76 -0
- package/dist/clientContextWireCaps.d.ts +46 -0
- package/dist/clientContextWireCaps.js +48 -0
- package/dist/cloudConfigWireCaps.d.ts +110 -0
- package/dist/cloudConfigWireCaps.js +228 -0
- package/dist/controlRouter.d.ts +191 -0
- package/dist/controlRouter.js +244 -0
- package/dist/effortWire.d.ts +34 -0
- package/dist/effortWire.js +38 -0
- package/dist/engineAgentPanelStore.d.ts +111 -0
- package/dist/engineAgentPanelStore.js +93 -0
- package/dist/engineCapsCache.d.ts +32 -0
- package/dist/engineCapsCache.js +64 -0
- package/dist/engineInlineTaskStats.d.ts +66 -0
- package/dist/engineInlineTaskStats.js +97 -0
- package/dist/engineToolLabelStore.d.ts +32 -0
- package/dist/engineToolLabelStore.js +55 -0
- package/dist/engineWireSdk.d.ts +49 -0
- package/dist/engineWireSdk.js +61 -0
- package/dist/fleetAgentPanelProjection.d.ts +31 -0
- package/dist/fleetAgentPanelProjection.js +0 -0
- package/dist/fleetTaskDesc.d.ts +20 -0
- package/dist/fleetTaskDesc.js +35 -0
- package/dist/forkWireCaps.d.ts +25 -0
- package/dist/forkWireCaps.js +42 -0
- package/dist/hostEnv.d.ts +16 -0
- package/dist/hostEnv.js +17 -0
- package/dist/imagesWireCaps.d.ts +28 -0
- package/dist/imagesWireCaps.js +49 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +62 -1
- package/dist/liveModelCatalog.d.ts +62 -0
- package/dist/liveModelCatalog.js +79 -0
- package/dist/liveQuestionStore.d.ts +101 -0
- package/dist/liveQuestionStore.js +109 -0
- package/dist/mcpWireCaps.d.ts +41 -0
- package/dist/mcpWireCaps.js +51 -0
- package/dist/modelBudgetRule.d.ts +44 -0
- package/dist/modelBudgetRule.js +73 -0
- package/dist/modelWireCaps.d.ts +17 -0
- package/dist/modelWireCaps.js +16 -0
- package/dist/notifications.d.ts +150 -4
- package/dist/notifications.js +472 -0
- package/dist/permissionWireCaps.d.ts +37 -0
- package/dist/permissionWireCaps.js +46 -0
- package/dist/printToolResultFrame.d.ts +59 -0
- package/dist/printToolResultFrame.js +65 -0
- package/dist/promptProfileWireCaps.d.ts +17 -0
- package/dist/promptProfileWireCaps.js +20 -0
- package/dist/retainBackgroundWireCaps.d.ts +50 -0
- package/dist/retainBackgroundWireCaps.js +58 -0
- package/dist/rewindWireCaps.d.ts +36 -0
- package/dist/rewindWireCaps.js +41 -0
- package/dist/selfOrchestrationWireCaps.d.ts +40 -0
- package/dist/selfOrchestrationWireCaps.js +48 -0
- package/dist/sessionModelLatch.d.ts +35 -0
- package/dist/sessionModelLatch.js +63 -0
- package/dist/skillsWireCaps.d.ts +47 -0
- package/dist/skillsWireCaps.js +41 -0
- package/dist/sseIdleTriage.d.ts +94 -0
- package/dist/sseIdleTriage.js +142 -0
- package/dist/steering.d.ts +4 -0
- package/dist/steering.js +4 -0
- package/dist/subagentContentStore.d.ts +131 -0
- package/dist/subagentContentStore.js +322 -0
- package/dist/types/engineState.d.ts +116 -0
- package/dist/types/engineState.js +17 -0
- package/dist/ultracodeWireCaps.d.ts +90 -0
- package/dist/ultracodeWireCaps.js +159 -0
- package/dist/webSearchWireCaps.d.ts +54 -0
- package/dist/webSearchWireCaps.js +76 -0
- package/dist/workflow.d.ts +40 -8
- package/dist/workflow.js +69 -11
- 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 {};
|