@sema-agent/client-core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/dist/adapt.d.ts +30 -3
- package/dist/adapt.js +490 -354
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +81 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +330 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +71 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +370 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +26 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +12 -0
- package/dist/adapter/runStream.d.ts +38 -0
- package/dist/adapter/runStream.js +177 -0
- package/dist/adapter/types.d.ts +73 -0
- package/dist/adapter/types.js +23 -0
- 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/engineWireSdk.d.ts +49 -0
- package/dist/engineWireSdk.js +61 -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 +52 -0
- package/dist/index.js +62 -0
- package/dist/liveModelCatalog.d.ts +62 -0
- package/dist/liveModelCatalog.js +79 -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/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/seam.d.ts +97 -2
- 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/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/package.json +2 -2
|
@@ -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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/engineWireSdk.ts 逐字搬入(纯半场 client 工厂;agentsWireCaps 的静态依赖,故同批过来)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* engineWireSdk — SDK 宪法迁移批(0.0.52)的纯半场 client 工厂:壳内各 wire 子模块(compact/steer/
|
|
6
|
+
* plan-review/auto-title/recap/caps 探测)从手抄 raw fetch 改接 @sema-agent/sdk AgentClient 的共用
|
|
7
|
+
* 构造点。ZERO 产品依赖(只 import 外部零依赖 SDK)——agentsWireCaps/liveInitToolFace 等「纯半场/
|
|
8
|
+
* 零产品图」模块可安全 import,不拖 engineTarget 的 fs 图,亦无循环依赖。
|
|
9
|
+
*
|
|
10
|
+
* 凭证解析 = engineTarget.resolveLiveAuthToken 的逐字镜像(#118 三态;那边是产品半场真源,这里为
|
|
11
|
+
* 纯半场复刻,两处语义必须一致):
|
|
12
|
+
* · token 有 → 原样 Bearer;
|
|
13
|
+
* · 无 token ∧ loopback → { mode: 'loopback-unauthed' }(出站零 Authorization 头,绝不伪造);
|
|
14
|
+
* · 无 token ∧ 非 loopback → 'anon'(fail-closed worker 换来诚实 401;与主车道 replEntry 同姿势)。
|
|
15
|
+
* ⚠️ 迁移差分记账:旧 raw fetch 在「无 token ∧ 非 loopback」时不带任何 Authorization 头;SDK 恒需
|
|
16
|
+
* 合法 authToken 形,这里按主车道姿势出 'anon'——该态本就是残破配置(connect 未登录),两种姿势对
|
|
17
|
+
* fail-closed worker 都是 401,对 owner-gated 路由都是 owner 不匹配,行为面等价。
|
|
18
|
+
*
|
|
19
|
+
* 缺省 maxRetries=0:raw fetch 原语义是单发不重试(探针有自己的预算窗;fire-and-forget 不该静默
|
|
20
|
+
* 翻倍打点)。要 SDK 缺省重试的调用方显式传。principal 缺省 'anon:shell-live'(replEntry live 车道
|
|
21
|
+
* 同款)——SDK Transport 恒 stamp x-agent-principal(principal-first 宪法);旧 raw fetch 在缺
|
|
22
|
+
* principal 时省略该头,迁移后恒出示,对 requirePrincipal 部署是 fail-open 改善,已记账。
|
|
23
|
+
*/
|
|
24
|
+
import { AgentClient } from '@sema-agent/sdk';
|
|
25
|
+
export type WireAuthToken = string | {
|
|
26
|
+
mode: 'loopback-unauthed';
|
|
27
|
+
};
|
|
28
|
+
/** loopback 判定(engineTarget.isLoopbackEngineUrl 逐字镜像;解析失败=非 loopback)。 */
|
|
29
|
+
export declare function isLoopbackWireUrl(url: string): boolean;
|
|
30
|
+
/** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
|
|
31
|
+
export declare function wireAuthTokenFor(baseUrl: string, token?: string): WireAuthToken;
|
|
32
|
+
export type EngineWireClientConfig = {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
/** 真 token 串;缺省经 wireAuthTokenFor 三态解析(loopback-unauthed / 'anon')。 */
|
|
35
|
+
token?: string;
|
|
36
|
+
/** 缺省 'anon:shell-live'(replEntry live 车道同款)。 */
|
|
37
|
+
principal?: string;
|
|
38
|
+
/** 每次请求的超时预算(SDK 缺省 60s)。 */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** 缺省 0(raw fetch 单发语义);要 SDK 重试的调用方显式传。 */
|
|
41
|
+
maxRetries?: number;
|
|
42
|
+
/** 测试注入面(探针单测的 fetch stub)。 */
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* 构造一个引擎 wire 用 AgentClient。null = 构造失败(SDK fail-closed 拒绝——不该发生:
|
|
47
|
+
* wireAuthTokenFor 恒给合法形;防御性 fail-soft,调用方按各自离线/探测失败路径处理)。
|
|
48
|
+
*/
|
|
49
|
+
export declare function makeEngineWireClient(cfg: EngineWireClientConfig): AgentClient | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/engineWireSdk.ts 逐字搬入(纯半场 client 工厂;agentsWireCaps 的静态依赖,故同批过来)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* engineWireSdk — SDK 宪法迁移批(0.0.52)的纯半场 client 工厂:壳内各 wire 子模块(compact/steer/
|
|
6
|
+
* plan-review/auto-title/recap/caps 探测)从手抄 raw fetch 改接 @sema-agent/sdk AgentClient 的共用
|
|
7
|
+
* 构造点。ZERO 产品依赖(只 import 外部零依赖 SDK)——agentsWireCaps/liveInitToolFace 等「纯半场/
|
|
8
|
+
* 零产品图」模块可安全 import,不拖 engineTarget 的 fs 图,亦无循环依赖。
|
|
9
|
+
*
|
|
10
|
+
* 凭证解析 = engineTarget.resolveLiveAuthToken 的逐字镜像(#118 三态;那边是产品半场真源,这里为
|
|
11
|
+
* 纯半场复刻,两处语义必须一致):
|
|
12
|
+
* · token 有 → 原样 Bearer;
|
|
13
|
+
* · 无 token ∧ loopback → { mode: 'loopback-unauthed' }(出站零 Authorization 头,绝不伪造);
|
|
14
|
+
* · 无 token ∧ 非 loopback → 'anon'(fail-closed worker 换来诚实 401;与主车道 replEntry 同姿势)。
|
|
15
|
+
* ⚠️ 迁移差分记账:旧 raw fetch 在「无 token ∧ 非 loopback」时不带任何 Authorization 头;SDK 恒需
|
|
16
|
+
* 合法 authToken 形,这里按主车道姿势出 'anon'——该态本就是残破配置(connect 未登录),两种姿势对
|
|
17
|
+
* fail-closed worker 都是 401,对 owner-gated 路由都是 owner 不匹配,行为面等价。
|
|
18
|
+
*
|
|
19
|
+
* 缺省 maxRetries=0:raw fetch 原语义是单发不重试(探针有自己的预算窗;fire-and-forget 不该静默
|
|
20
|
+
* 翻倍打点)。要 SDK 缺省重试的调用方显式传。principal 缺省 'anon:shell-live'(replEntry live 车道
|
|
21
|
+
* 同款)——SDK Transport 恒 stamp x-agent-principal(principal-first 宪法);旧 raw fetch 在缺
|
|
22
|
+
* principal 时省略该头,迁移后恒出示,对 requirePrincipal 部署是 fail-open 改善,已记账。
|
|
23
|
+
*/
|
|
24
|
+
import { AgentClient } from '@sema-agent/sdk';
|
|
25
|
+
/** loopback 判定(engineTarget.isLoopbackEngineUrl 逐字镜像;解析失败=非 loopback)。 */
|
|
26
|
+
export function isLoopbackWireUrl(url) {
|
|
27
|
+
try {
|
|
28
|
+
const h = new URL(url).hostname;
|
|
29
|
+
return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === '[::1]';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
|
|
36
|
+
export function wireAuthTokenFor(baseUrl, token) {
|
|
37
|
+
if (token)
|
|
38
|
+
return token;
|
|
39
|
+
if (isLoopbackWireUrl(baseUrl))
|
|
40
|
+
return { mode: 'loopback-unauthed' };
|
|
41
|
+
return 'anon';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 构造一个引擎 wire 用 AgentClient。null = 构造失败(SDK fail-closed 拒绝——不该发生:
|
|
45
|
+
* wireAuthTokenFor 恒给合法形;防御性 fail-soft,调用方按各自离线/探测失败路径处理)。
|
|
46
|
+
*/
|
|
47
|
+
export function makeEngineWireClient(cfg) {
|
|
48
|
+
try {
|
|
49
|
+
return new AgentClient({
|
|
50
|
+
baseUrl: cfg.baseUrl,
|
|
51
|
+
authToken: wireAuthTokenFor(cfg.baseUrl, cfg.token),
|
|
52
|
+
principal: cfg.principal ?? 'anon:shell-live',
|
|
53
|
+
...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
|
|
54
|
+
maxRetries: cfg.maxRetries ?? 0,
|
|
55
|
+
...(cfg.fetchImpl ? { fetch: cfg.fetchImpl } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/forkWireCaps.ts 逐字搬入(零依赖)。
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* src/sema/forkWireCaps.ts — Fork wire projection: the top-level `TaskRequest.enableFork` boolean →
|
|
6
|
+
* core fork governance → the model's Agent(subagent_type:"fork") availability (core 1.257 撤除独立
|
|
7
|
+
* Fork 工具,能力收编进 Agent;CC `subagent_type:fork`: the model forks ITSELF into a subagent that
|
|
8
|
+
* INHERITS the parent context + shares the prompt cache, vs Task's clean-context subagent).
|
|
9
|
+
*
|
|
10
|
+
* DEFAULT = CC parity(clay 2026-07-08 拍板,订正:此前壳里引用的「默认对 LLM 不开」是过时拍板):
|
|
11
|
+
* core ≥1.257 的 fork 治理是 opt-OUT——enableFork 缺省 = GRANTED(与 CC 一致)。壳默认 **不 stamp**,
|
|
12
|
+
* 让引擎默认生效(模型默认可用 Agent-fork)。`SEMA_ENABLE_FORK` 保留为显式开关:truthy ⇒ stamp true
|
|
13
|
+
* (对老引擎 <1.257 也点亮),显式 falsy('0'/'false'/'no'/'off')⇒ stamp false(用户关闭的口子,
|
|
14
|
+
* service [503]② 的 fail-OPEN 教训:关必须显式 false,缺省不再是"关")。
|
|
15
|
+
*
|
|
16
|
+
* PURE — no env/file IO of its OWN (callers pass `env`), so it bundles + unit-tests without the
|
|
17
|
+
* settings graph. The CALLER (seamQuery / the -p ask) gates the env read to LIVE mode so the mock
|
|
18
|
+
* request shape never changes.
|
|
19
|
+
*/
|
|
20
|
+
import { type EnvLike } from './hostEnv.js';
|
|
21
|
+
/** The env key the shell reads. SEMA_-namespaced so it never collides with a worker's own deploy env. */
|
|
22
|
+
export declare const ENABLE_FORK_ENV: "SEMA_ENABLE_FORK";
|
|
23
|
+
/** ENV source → explicit `true`/`false` when the user set SEMA_ENABLE_FORK either way, else
|
|
24
|
+
* `undefined` (⇒ no stamp ⇒ the engine default applies — GRANTED on core ≥1.257, CC parity). */
|
|
25
|
+
export declare function enableForkFromEnv(env?: EnvLike): boolean | undefined;
|