@sema-agent/client-core 0.6.0 → 0.7.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/dist/adapt.d.ts +4 -2
- package/dist/adapt.js +192 -25
- package/dist/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +7 -0
- package/dist/engineSessionParam.js +35 -0
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -0
- package/dist/headlessPermissionModeWire.d.ts +55 -0
- package/dist/headlessPermissionModeWire.js +111 -0
- package/dist/headlessReconnectWire.d.ts +96 -0
- package/dist/headlessReconnectWire.js +141 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +22 -0
- package/dist/interactiveToolsWire.d.ts +48 -0
- package/dist/interactiveToolsWire.js +86 -0
- package/dist/limitsWire.d.ts +89 -0
- package/dist/limitsWire.js +225 -0
- package/dist/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/sandboxWire.d.ts +75 -0
- package/dist/sandboxWire.js +138 -0
- package/dist/scenarioWire.d.ts +62 -0
- package/dist/scenarioWire.js +115 -0
- package/dist/seam.d.ts +33 -2
- package/dist/seam.js +1 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -0
- package/package.json +4 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/limitsWire.ts — headless `-p` run-limits wire (TB2.0 反馈批 E · P2-3-b, 2026-07-15):
|
|
3
|
+
* expose the engine's REQUEST-LEVEL pacing limits on the headless `-p` path via `TaskRequest.limits`.
|
|
4
|
+
* The server accepts `limits:{timeoutSec?,maxOutputTokens?,maxTurns?}` since 1.196.0 (positive-integer
|
|
5
|
+
* 400 gate, board [857]) and feeds the EXISTING pacing machinery (deadlineNudge / callCapByDeadline /
|
|
6
|
+
* gracefulFinalize) — the shell simply never stamped the field. This module is the CLI face.
|
|
7
|
+
*
|
|
8
|
+
* TWO FLAGS, TWO ALIGNMENT BUCKETS:
|
|
9
|
+
* · `--deadline <sec>` → limits.timeoutSec. Upstream CC 2.1.207 has NO deadline flag (nearest kin:
|
|
10
|
+
* --max-turns / --task-budget / --max-budget-usd) → SUPERSET, help copy says so.
|
|
11
|
+
* · `--max-turns <n>` → limits.maxTurns. Upstream CC 2.1.207 HAS `--max-turns` (source 199676 /
|
|
12
|
+
* 793380) → ALIGNMENT-GAP fill: the flag was already registered (main.tsx CC-inherited surface) and
|
|
13
|
+
* drove the local query loop, but the seam path executes turns ENGINE-side — this wire finally
|
|
14
|
+
* carries the cap to where the turns actually run.
|
|
15
|
+
*
|
|
16
|
+
* PRECEDENCE per knob (flag > settings > none), scenarioWire 三件套同款:
|
|
17
|
+
* a) explicit flag → strict validation, FAIL-LOUD on an invalid
|
|
18
|
+
* value (integer domain below; a mis-typed pacing bound must not quietly run unbounded).
|
|
19
|
+
* b) settings env lane (settings.json `env` block → 1a-envseed → process.env):
|
|
20
|
+
* `SEMA_HEADLESS_DEADLINE_SEC` / `SEMA_HEADLESS_MAX_TURNS` → SILENT degrade on an invalid value
|
|
21
|
+
* (spec'd 静默降级 — a settings typo must never brick every headless run; unlike scenarioWire we
|
|
22
|
+
* don't even warn, per the batch-E triage shape. SEMA_DEBUG surfaces the drop for diagnosis).
|
|
23
|
+
* c) neither → NO stamp = today's behaviour, verbatim.
|
|
24
|
+
*
|
|
25
|
+
* DOMAINS (integer, inclusive): timeoutSec 30..86400 · maxTurns 1..1000.
|
|
26
|
+
*
|
|
27
|
+
* VERSION GATE (triage risk ③ — an OLD server SILENTLY SWALLOWS unknown body fields, worse than not
|
|
28
|
+
* shipping): the self-spawned engine is release-pinned ≥1.197.0 so the dev/release lanes are safe by
|
|
29
|
+
* construction; but a user-supplied SEMA_ENGINE_URL may point at an older engine. When the user
|
|
30
|
+
* EXPLICITLY passed a flag (env-lane defaults stay quiet) and SEMA_ENGINE_URL is in play, the caller
|
|
31
|
+
* probes `${baseUrl}/health` (the engine self-describes `version` since 1.96) and prints ONE honest
|
|
32
|
+
* stderr line when version <1.196.0 — the request still goes out unchanged (warn, don't block).
|
|
33
|
+
* Unreachable /health or an unparsable version stays silent (can't confirm staleness — fail-soft).
|
|
34
|
+
*/
|
|
35
|
+
import { type EnvLike } from './hostEnv.js';
|
|
36
|
+
/** Settings-lane env knobs (settings.json `env` block → 1a-envseed → process.env). */
|
|
37
|
+
export declare const HEADLESS_DEADLINE_ENV = "SEMA_HEADLESS_DEADLINE_SEC";
|
|
38
|
+
export declare const HEADLESS_MAX_TURNS_ENV = "SEMA_HEADLESS_MAX_TURNS";
|
|
39
|
+
/** Integer domains (inclusive). timeoutSec: 30s..24h; maxTurns: 1..1000. */
|
|
40
|
+
export declare const DEADLINE_SEC_MIN = 30;
|
|
41
|
+
export declare const DEADLINE_SEC_MAX = 86400;
|
|
42
|
+
export declare const MAX_TURNS_MIN = 1;
|
|
43
|
+
export declare const MAX_TURNS_MAX = 1000;
|
|
44
|
+
/** First server version whose /v1/tasks|/v1/tasks/stream accept body.limits (board [857]). */
|
|
45
|
+
export declare const LIMITS_WIRE_MIN_ENGINE: readonly [number, number, number];
|
|
46
|
+
export interface HeadlessLimits {
|
|
47
|
+
timeoutSec?: number;
|
|
48
|
+
maxTurns?: number;
|
|
49
|
+
}
|
|
50
|
+
export type LimitsParseResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
limits?: HeadlessLimits;
|
|
53
|
+
flaggedFlags: string[];
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Parse `--deadline` + `--max-turns` out of an argv slice. No flags ⇒ `{ok:true}` with no limits.
|
|
60
|
+
* Repeated flag ⇒ LAST wins (commander convention). Missing value / non-integer / out-of-domain ⇒
|
|
61
|
+
* fail-LOUD parse error (never a silent default — same stance as `--scenario`/`--sandbox`).
|
|
62
|
+
* `flaggedFlags` names the flags that EXPLICITLY contributed (the version-gate probe trigger).
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseLimitsArgv(argv: string[]): LimitsParseResult;
|
|
65
|
+
/**
|
|
66
|
+
* The settings-lane defaults. SILENT degrade on a bad value (batch-E triage shape — stricter than
|
|
67
|
+
* scenarioWire's warn-and-ignore: these are pacing hints, and a settings typo must neither brick nor
|
|
68
|
+
* spam every headless run). SEMA_DEBUG surfaces the drop.
|
|
69
|
+
*/
|
|
70
|
+
export declare function headlessLimitsFromEnv(env?: EnvLike): HeadlessLimits | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the limits for a headless `-p` submit: explicit flags > settings env defaults > none
|
|
73
|
+
* (= no stamp, today's behaviour). Merge is PER-KNOB: `--deadline 300` + SEMA_HEADLESS_MAX_TURNS=50
|
|
74
|
+
* yields {timeoutSec:300, maxTurns:50}. Flag parse errors propagate fail-loud; the caller exits.
|
|
75
|
+
*/
|
|
76
|
+
export declare function limitsForPrint(argv: string[], env?: EnvLike): LimitsParseResult;
|
|
77
|
+
/** True when the engine version string satisfies the limits-wire minimum (≥1.196.0). Unparsable ⇒ false. */
|
|
78
|
+
export declare function versionSupportsLimits(v: string | undefined): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Version-gate probe (triage risk ③): GET `${baseUrl}/health`, read `version`, and return ONE honest
|
|
81
|
+
* stderr line when the engine is confirmed <1.196.0 — the caller prints it and SENDS ANYWAY (the old
|
|
82
|
+
* server drops the field server-side; warn, don't block). Returns undefined when the engine is new
|
|
83
|
+
* enough, unreachable, or self-describes no parsable version (can't confirm staleness — fail-soft
|
|
84
|
+
* silent; the release-pinned self-spawn lane never even reaches this probe).
|
|
85
|
+
*/
|
|
86
|
+
export declare function engineLimitsSupportWarning(baseUrl: string, flaggedFlags: string[], opts?: {
|
|
87
|
+
fetchImpl?: typeof fetch;
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/limitsWire.ts — headless `-p` run-limits wire (TB2.0 反馈批 E · P2-3-b, 2026-07-15):
|
|
3
|
+
* expose the engine's REQUEST-LEVEL pacing limits on the headless `-p` path via `TaskRequest.limits`.
|
|
4
|
+
* The server accepts `limits:{timeoutSec?,maxOutputTokens?,maxTurns?}` since 1.196.0 (positive-integer
|
|
5
|
+
* 400 gate, board [857]) and feeds the EXISTING pacing machinery (deadlineNudge / callCapByDeadline /
|
|
6
|
+
* gracefulFinalize) — the shell simply never stamped the field. This module is the CLI face.
|
|
7
|
+
*
|
|
8
|
+
* TWO FLAGS, TWO ALIGNMENT BUCKETS:
|
|
9
|
+
* · `--deadline <sec>` → limits.timeoutSec. Upstream CC 2.1.207 has NO deadline flag (nearest kin:
|
|
10
|
+
* --max-turns / --task-budget / --max-budget-usd) → SUPERSET, help copy says so.
|
|
11
|
+
* · `--max-turns <n>` → limits.maxTurns. Upstream CC 2.1.207 HAS `--max-turns` (source 199676 /
|
|
12
|
+
* 793380) → ALIGNMENT-GAP fill: the flag was already registered (main.tsx CC-inherited surface) and
|
|
13
|
+
* drove the local query loop, but the seam path executes turns ENGINE-side — this wire finally
|
|
14
|
+
* carries the cap to where the turns actually run.
|
|
15
|
+
*
|
|
16
|
+
* PRECEDENCE per knob (flag > settings > none), scenarioWire 三件套同款:
|
|
17
|
+
* a) explicit flag → strict validation, FAIL-LOUD on an invalid
|
|
18
|
+
* value (integer domain below; a mis-typed pacing bound must not quietly run unbounded).
|
|
19
|
+
* b) settings env lane (settings.json `env` block → 1a-envseed → process.env):
|
|
20
|
+
* `SEMA_HEADLESS_DEADLINE_SEC` / `SEMA_HEADLESS_MAX_TURNS` → SILENT degrade on an invalid value
|
|
21
|
+
* (spec'd 静默降级 — a settings typo must never brick every headless run; unlike scenarioWire we
|
|
22
|
+
* don't even warn, per the batch-E triage shape. SEMA_DEBUG surfaces the drop for diagnosis).
|
|
23
|
+
* c) neither → NO stamp = today's behaviour, verbatim.
|
|
24
|
+
*
|
|
25
|
+
* DOMAINS (integer, inclusive): timeoutSec 30..86400 · maxTurns 1..1000.
|
|
26
|
+
*
|
|
27
|
+
* VERSION GATE (triage risk ③ — an OLD server SILENTLY SWALLOWS unknown body fields, worse than not
|
|
28
|
+
* shipping): the self-spawned engine is release-pinned ≥1.197.0 so the dev/release lanes are safe by
|
|
29
|
+
* construction; but a user-supplied SEMA_ENGINE_URL may point at an older engine. When the user
|
|
30
|
+
* EXPLICITLY passed a flag (env-lane defaults stay quiet) and SEMA_ENGINE_URL is in play, the caller
|
|
31
|
+
* probes `${baseUrl}/health` (the engine self-describes `version` since 1.96) and prints ONE honest
|
|
32
|
+
* stderr line when version <1.196.0 — the request still goes out unchanged (warn, don't block).
|
|
33
|
+
* Unreachable /health or an unparsable version stays silent (can't confirm staleness — fail-soft).
|
|
34
|
+
*/
|
|
35
|
+
import { hostEnv } from './hostEnv.js';
|
|
36
|
+
/** Settings-lane env knobs (settings.json `env` block → 1a-envseed → process.env). */
|
|
37
|
+
export const HEADLESS_DEADLINE_ENV = 'SEMA_HEADLESS_DEADLINE_SEC';
|
|
38
|
+
export const HEADLESS_MAX_TURNS_ENV = 'SEMA_HEADLESS_MAX_TURNS';
|
|
39
|
+
/** Integer domains (inclusive). timeoutSec: 30s..24h; maxTurns: 1..1000. */
|
|
40
|
+
export const DEADLINE_SEC_MIN = 30;
|
|
41
|
+
export const DEADLINE_SEC_MAX = 86_400;
|
|
42
|
+
export const MAX_TURNS_MIN = 1;
|
|
43
|
+
export const MAX_TURNS_MAX = 1_000;
|
|
44
|
+
/** First server version whose /v1/tasks|/v1/tasks/stream accept body.limits (board [857]). */
|
|
45
|
+
export const LIMITS_WIRE_MIN_ENGINE = [1, 196, 0];
|
|
46
|
+
const DEADLINE_USAGE = `sema: --deadline expects a whole number of seconds between ${DEADLINE_SEC_MIN} and ${DEADLINE_SEC_MAX}.\n` +
|
|
47
|
+
' --deadline <sec> soft wall-clock budget for this run (sema superset — no upstream equivalent):\n' +
|
|
48
|
+
' the engine paces itself (nudges + call caps) and finalizes gracefully near the deadline\n' +
|
|
49
|
+
' (no flag) no deadline — today\'s behaviour';
|
|
50
|
+
const MAX_TURNS_USAGE = `sema: --max-turns expects a whole number of turns between ${MAX_TURNS_MIN} and ${MAX_TURNS_MAX}.\n` +
|
|
51
|
+
' --max-turns <n> maximum number of agentic turns for this run (upstream-aligned flag,\n' +
|
|
52
|
+
' enforced engine-side on the sema seam)\n' +
|
|
53
|
+
' (no flag) no turn cap — today\'s behaviour';
|
|
54
|
+
/** STRICT integer shape for explicit flag values: ASCII digits only (no sign/decimal/exponent/space). */
|
|
55
|
+
const INT_RE = /^\d+$/;
|
|
56
|
+
/**
|
|
57
|
+
* Pull the LAST value of a `--name <v>` / `--name=<v>` flag out of an argv slice (everything before a
|
|
58
|
+
* bare `--` — positionals are never flags; scenarioWire 纪律). Returns:
|
|
59
|
+
* {present:false} — flag absent
|
|
60
|
+
* {present:true, raw} — flag present with a value token
|
|
61
|
+
* {present:true, raw:undefined} — flag present but the value is missing/flag-shaped (fail-loud at caller)
|
|
62
|
+
*/
|
|
63
|
+
function lastFlagValue(argv, name) {
|
|
64
|
+
const eq = `${name}=`;
|
|
65
|
+
let present = false;
|
|
66
|
+
let raw;
|
|
67
|
+
let valueMissing = false;
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const a = argv[i];
|
|
70
|
+
if (a === '--')
|
|
71
|
+
break; // positionals — never flags
|
|
72
|
+
if (a === name) {
|
|
73
|
+
present = true;
|
|
74
|
+
const nxt = argv[i + 1];
|
|
75
|
+
if (nxt === undefined || nxt.startsWith('-')) {
|
|
76
|
+
// note: a NEGATIVE number would look flag-shaped too — the domains are all-positive, so the
|
|
77
|
+
// fail-loud usage hint is the right answer for `--deadline -5` as well.
|
|
78
|
+
valueMissing = true;
|
|
79
|
+
raw = undefined;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
valueMissing = false;
|
|
83
|
+
raw = nxt;
|
|
84
|
+
i++; // consume the value token
|
|
85
|
+
}
|
|
86
|
+
else if (a.startsWith(eq)) {
|
|
87
|
+
present = true;
|
|
88
|
+
valueMissing = false;
|
|
89
|
+
raw = a.slice(eq.length);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (present && (valueMissing || raw === undefined))
|
|
93
|
+
return { present: true };
|
|
94
|
+
return present ? { present: true, raw } : { present: false };
|
|
95
|
+
}
|
|
96
|
+
/** Validate a RAW flag value against an inclusive integer domain. Returns the number or null. */
|
|
97
|
+
function parseIntInDomain(raw, min, max) {
|
|
98
|
+
const v = raw.trim();
|
|
99
|
+
if (!INT_RE.test(v))
|
|
100
|
+
return null;
|
|
101
|
+
const n = Number(v);
|
|
102
|
+
if (!Number.isSafeInteger(n) || n < min || n > max)
|
|
103
|
+
return null;
|
|
104
|
+
return n;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse `--deadline` + `--max-turns` out of an argv slice. No flags ⇒ `{ok:true}` with no limits.
|
|
108
|
+
* Repeated flag ⇒ LAST wins (commander convention). Missing value / non-integer / out-of-domain ⇒
|
|
109
|
+
* fail-LOUD parse error (never a silent default — same stance as `--scenario`/`--sandbox`).
|
|
110
|
+
* `flaggedFlags` names the flags that EXPLICITLY contributed (the version-gate probe trigger).
|
|
111
|
+
*/
|
|
112
|
+
export function parseLimitsArgv(argv) {
|
|
113
|
+
const limits = {};
|
|
114
|
+
const flaggedFlags = [];
|
|
115
|
+
const dl = lastFlagValue(argv, '--deadline');
|
|
116
|
+
if (dl.present) {
|
|
117
|
+
const n = dl.raw !== undefined ? parseIntInDomain(dl.raw, DEADLINE_SEC_MIN, DEADLINE_SEC_MAX) : null;
|
|
118
|
+
if (n === null)
|
|
119
|
+
return { ok: false, error: DEADLINE_USAGE };
|
|
120
|
+
limits.timeoutSec = n;
|
|
121
|
+
flaggedFlags.push('--deadline');
|
|
122
|
+
}
|
|
123
|
+
const mt = lastFlagValue(argv, '--max-turns');
|
|
124
|
+
if (mt.present) {
|
|
125
|
+
const n = mt.raw !== undefined ? parseIntInDomain(mt.raw, MAX_TURNS_MIN, MAX_TURNS_MAX) : null;
|
|
126
|
+
if (n === null)
|
|
127
|
+
return { ok: false, error: MAX_TURNS_USAGE };
|
|
128
|
+
limits.maxTurns = n;
|
|
129
|
+
flaggedFlags.push('--max-turns');
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
...(flaggedFlags.length > 0 ? { limits } : {}),
|
|
134
|
+
flaggedFlags,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The settings-lane defaults. SILENT degrade on a bad value (batch-E triage shape — stricter than
|
|
139
|
+
* scenarioWire's warn-and-ignore: these are pacing hints, and a settings typo must neither brick nor
|
|
140
|
+
* spam every headless run). SEMA_DEBUG surfaces the drop.
|
|
141
|
+
*/
|
|
142
|
+
export function headlessLimitsFromEnv(env = hostEnv()) {
|
|
143
|
+
const out = {};
|
|
144
|
+
const dl = env[HEADLESS_DEADLINE_ENV]?.trim();
|
|
145
|
+
if (dl) {
|
|
146
|
+
const n = parseIntInDomain(dl, DEADLINE_SEC_MIN, DEADLINE_SEC_MAX);
|
|
147
|
+
if (n !== null)
|
|
148
|
+
out.timeoutSec = n;
|
|
149
|
+
else if (env.SEMA_DEBUG) {
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.error(`[sema] ${HEADLESS_DEADLINE_ENV}="${dl}" is not an integer in ${DEADLINE_SEC_MIN}..${DEADLINE_SEC_MAX} — ignoring the settings default`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const mt = env[HEADLESS_MAX_TURNS_ENV]?.trim();
|
|
155
|
+
if (mt) {
|
|
156
|
+
const n = parseIntInDomain(mt, MAX_TURNS_MIN, MAX_TURNS_MAX);
|
|
157
|
+
if (n !== null)
|
|
158
|
+
out.maxTurns = n;
|
|
159
|
+
else if (env.SEMA_DEBUG) {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.error(`[sema] ${HEADLESS_MAX_TURNS_ENV}="${mt}" is not an integer in ${MAX_TURNS_MIN}..${MAX_TURNS_MAX} — ignoring the settings default`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out.timeoutSec !== undefined || out.maxTurns !== undefined ? out : undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the limits for a headless `-p` submit: explicit flags > settings env defaults > none
|
|
168
|
+
* (= no stamp, today's behaviour). Merge is PER-KNOB: `--deadline 300` + SEMA_HEADLESS_MAX_TURNS=50
|
|
169
|
+
* yields {timeoutSec:300, maxTurns:50}. Flag parse errors propagate fail-loud; the caller exits.
|
|
170
|
+
*/
|
|
171
|
+
export function limitsForPrint(argv, env = hostEnv()) {
|
|
172
|
+
const parsed = parseLimitsArgv(argv);
|
|
173
|
+
if (!parsed.ok)
|
|
174
|
+
return parsed;
|
|
175
|
+
const fromEnv = headlessLimitsFromEnv(env);
|
|
176
|
+
const merged = { ...(fromEnv ?? {}), ...(parsed.limits ?? {}) };
|
|
177
|
+
const any = merged.timeoutSec !== undefined || merged.maxTurns !== undefined;
|
|
178
|
+
return { ok: true, ...(any ? { limits: merged } : {}), flaggedFlags: parsed.flaggedFlags };
|
|
179
|
+
}
|
|
180
|
+
/** True when the engine version string satisfies the limits-wire minimum (≥1.196.0). Unparsable ⇒ false. */
|
|
181
|
+
export function versionSupportsLimits(v) {
|
|
182
|
+
const m = v ? /^(\d+)\.(\d+)\.(\d+)/.exec(v) : null;
|
|
183
|
+
if (!m)
|
|
184
|
+
return false;
|
|
185
|
+
const [a, b, c] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
186
|
+
const [ma, mb, mc] = LIMITS_WIRE_MIN_ENGINE;
|
|
187
|
+
if (a !== ma)
|
|
188
|
+
return a > ma;
|
|
189
|
+
if (b !== mb)
|
|
190
|
+
return b > mb;
|
|
191
|
+
return c >= mc;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Version-gate probe (triage risk ③): GET `${baseUrl}/health`, read `version`, and return ONE honest
|
|
195
|
+
* stderr line when the engine is confirmed <1.196.0 — the caller prints it and SENDS ANYWAY (the old
|
|
196
|
+
* server drops the field server-side; warn, don't block). Returns undefined when the engine is new
|
|
197
|
+
* enough, unreachable, or self-describes no parsable version (can't confirm staleness — fail-soft
|
|
198
|
+
* silent; the release-pinned self-spawn lane never even reaches this probe).
|
|
199
|
+
*/
|
|
200
|
+
export async function engineLimitsSupportWarning(baseUrl, flaggedFlags, opts) {
|
|
201
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 1500);
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/health`, {
|
|
206
|
+
method: 'GET',
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
});
|
|
209
|
+
if (!res.ok)
|
|
210
|
+
return undefined;
|
|
211
|
+
const body = (await res.json());
|
|
212
|
+
const version = typeof body?.version === 'string' ? body.version : undefined;
|
|
213
|
+
// No version field = pre-1.96 engine — definitely older than 1.196.0, warn honestly.
|
|
214
|
+
if (versionSupportsLimits(version))
|
|
215
|
+
return undefined;
|
|
216
|
+
const flags = flaggedFlags.length > 0 ? flaggedFlags.join('/') : '--deadline/--max-turns';
|
|
217
|
+
return `[sema] engine ${version ?? '(unversioned, pre-1.96)'} does not support ${flags} (needs ≥1.196.0); the flag will be ignored by the engine`;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined; // unreachable/timeout — cannot confirm staleness, stay silent
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
}
|
|
225
|
+
}
|
package/dist/notifications.d.ts
CHANGED
|
@@ -90,6 +90,13 @@ export declare function dropQueuedNotificationsForRun(taskId: string): number;
|
|
|
90
90
|
* steer-inject 的 task_notification 帧)即预标记,Channel A 对同 runId 的补发直接丢弃。
|
|
91
91
|
*/
|
|
92
92
|
export declare function markEngineWorkflowNotified(runId: string): void;
|
|
93
|
+
/** 跨通道去重的**读**口(适配器 workflow_complete 臂的发射门;cli 同域)。 */
|
|
94
|
+
export declare function isEngineWorkflowNotified(runId: string): boolean;
|
|
95
|
+
/** 「这个 run 的完成卡已入队」的**写**口(cli 侧等价物 = 两个 enqueue 函数内的 `cardEnqueuedRunIds.add`)。 */
|
|
96
|
+
export declare function noteWorkflowCompletionCardEnqueued(runId: string): void;
|
|
97
|
+
/** 台账序列化面(`exportLedger`)—— 只读快照,顺序 = 插入序。 */
|
|
98
|
+
export declare function listNotifiedRuns(): string[];
|
|
99
|
+
export declare function listWorkflowCompletionCardsEnqueued(): string[];
|
|
93
100
|
export declare function isWorkflowCompletionCardEnqueued(runId: string): boolean;
|
|
94
101
|
type WorkflowStatusProbe = (runId: string) => Promise<{
|
|
95
102
|
terminal: boolean;
|
package/dist/notifications.js
CHANGED
|
@@ -215,6 +215,38 @@ export function markEngineWorkflowNotified(runId) {
|
|
|
215
215
|
if (outstandingRuns.delete(runId))
|
|
216
216
|
notifyOutstanding();
|
|
217
217
|
}
|
|
218
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
219
|
+
// B5 ①(0.7.0)—— **配对台账折回**([1857];记忆 paired-mechanisms-must-share-premise)。
|
|
220
|
+
// 0.6.0 把 `notifiedRuns` / `cardEnqueuedRuns` 放在**适配器实例**上,而与它们配对的另一半全在
|
|
221
|
+
// 本模块的 module 台账上:清账的 `dropQueuedNotificationsForRun`、记账的
|
|
222
|
+
// `enqueueBgChildNotification` / `enqueueEngineWorkflowNotification`、以及 idle watcher 那条
|
|
223
|
+
// (壳根本拦不到的调用点)。两半前提不共享 = 四个方向同时错:
|
|
224
|
+
// ① module 记过账 ⇒ 实例不知道 ⇒ 推送帧渲第二张卡(双卡)
|
|
225
|
+
// ② 宿主回钩写实例、drop 清 module ⇒ 实例只增不减 ⇒ 那一行永不上屏(a0110e88 复发)
|
|
226
|
+
// ③④ 跨通道去重两向失效 ⇒ 同一完成二次喂模型
|
|
227
|
+
// 修法 = 台账**整体**回到 module(与 cli 的 module 级 Set 逐字同域),适配器只经下面这四个口读写;
|
|
228
|
+
// 实例上只留 `renderedNotifications`(渲染去重,天然 per-session)与 `firedSubagentStartHookTaskIds`
|
|
229
|
+
// (DIVERGENCE-6 有意的实例级)。
|
|
230
|
+
// 🔴 多 session 宿主(web 一个页面两个会话)上本对台账是**进程级共享**的 —— 这与 cli 一致,
|
|
231
|
+
// 且必须如此:队列 port、idle watcher、outstanding 药丸也都是进程级的,台账单独 per-session
|
|
232
|
+
// 就又造出一对「前提不共享」的机制。
|
|
233
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
234
|
+
/** 跨通道去重的**读**口(适配器 workflow_complete 臂的发射门;cli 同域)。 */
|
|
235
|
+
export function isEngineWorkflowNotified(runId) {
|
|
236
|
+
return notifiedRunIds.has(runId);
|
|
237
|
+
}
|
|
238
|
+
/** 「这个 run 的完成卡已入队」的**写**口(cli 侧等价物 = 两个 enqueue 函数内的 `cardEnqueuedRunIds.add`)。 */
|
|
239
|
+
export function noteWorkflowCompletionCardEnqueued(runId) {
|
|
240
|
+
if (runId.length > 0)
|
|
241
|
+
cardEnqueuedRunIds.add(runId);
|
|
242
|
+
}
|
|
243
|
+
/** 台账序列化面(`exportLedger`)—— 只读快照,顺序 = 插入序。 */
|
|
244
|
+
export function listNotifiedRuns() {
|
|
245
|
+
return [...notifiedRunIds];
|
|
246
|
+
}
|
|
247
|
+
export function listWorkflowCompletionCardsEnqueued() {
|
|
248
|
+
return [...cardEnqueuedRunIds];
|
|
249
|
+
}
|
|
218
250
|
/** 双通道「完成卡」去重(clay dogfood 2026-07-19 双通知案):与 notifiedRunIds(模型通知去重,
|
|
219
251
|
* external mark 也写它)**分键**——本集只由 probe 合成 enqueue 写入(workflow+bg agent 两族,
|
|
220
252
|
* 对抗复审§1:bg 合成不写此集时 probe 先到→后到推送帧仍渲第二张卡)、只由推送帧渲染前读,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/sandboxWire.ts — [686]② `--sandbox` TRI-STATE flag (clay 拍, 2026-07-13): the CLI face of the
|
|
3
|
+
* per-task SANDBOX IMAGE PROFILE the server already ships end-to-end (`TaskRequest.sandboxImageProfile`
|
|
4
|
+
* per-task binding + the model-facing `SelectEnvironment` tool + the `GET /v1/images` catalog).
|
|
5
|
+
*
|
|
6
|
+
* TRI-STATE (clay 口径):
|
|
7
|
+
* a) DEFAULT (no flag) → NO `sandboxImageProfile` on the POST body — the worker's default image runs.
|
|
8
|
+
* b) `--sandbox <profile>` → EXPLICIT: stamp `sandboxImageProfile` (SDK types.ts:115 → service
|
|
9
|
+
* main.ts:2316 resolves profile→digest FAIL-CLOSED with the caller's principal — we only ever send the
|
|
10
|
+
* PROFILE, never a digest). Vocabulary = the live catalog (`sema cloud images list` / GET /v1/images) —
|
|
11
|
+
* the shell does NOT pre-validate against it (fail-closed admission is the server's job; a typo comes
|
|
12
|
+
* back as an honest 404, mapped to a hint by sandboxDegradeHint below).
|
|
13
|
+
* c) `--sandbox auto` → ADVISORY: the submit carries NO profile; instead the FIRST live turn's
|
|
14
|
+
* objective is prefixed with an advisory block asking the model to drive the `SelectEnvironment` tool
|
|
15
|
+
* itself (list → pick → report `Sandbox: <profile> — <reason>` in its reply, so the choice + reason are
|
|
16
|
+
* visible on the run card and correctable by the user). NOT a hard route — server keeps zero new
|
|
17
|
+
* endpoints; the tool's session binding takes effect from the NEXT task boundary ([440] F3 contract).
|
|
18
|
+
*
|
|
19
|
+
* LANE HONESTY (诚实不撒谎 UI, [686] constraint): the profile only works on the k8s sandbox lane — an
|
|
20
|
+
* e2b/host-lane worker (including the local TOC self-spawned engine) 400s the submit honestly
|
|
21
|
+
* (service main.ts:2323 "sandboxImageProfile is only supported on the k8s sandbox backend"). The shell does
|
|
22
|
+
* NOT pre-judge the lane (no caps sniffing before submit — per [686]: "不预判 lane,收到 400 再提示");
|
|
23
|
+
* it maps the server's honest reject into a degradation hint (sandboxDegradeHint) rendered on the error row.
|
|
24
|
+
* So on the local self-spawned engine `--sandbox <profile>` = a LOUD per-turn failure with an actionable
|
|
25
|
+
* hint — never a silent ignore (silently dropping the user's explicit environment choice would be lying).
|
|
26
|
+
*/
|
|
27
|
+
export type SandboxSelection = {
|
|
28
|
+
mode: 'default';
|
|
29
|
+
} | {
|
|
30
|
+
mode: 'auto';
|
|
31
|
+
} | {
|
|
32
|
+
mode: 'explicit';
|
|
33
|
+
profile: string;
|
|
34
|
+
};
|
|
35
|
+
export type SandboxParseResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
sel: SandboxSelection;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Parse `--sandbox` out of an argv slice (everything before a bare `--`; both `--sandbox <v>` and
|
|
44
|
+
* `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
|
|
45
|
+
* Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
|
|
46
|
+
* a mis-typed environment choice must not quietly run on the wrong image).
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSandboxArgv(argv: string[]): SandboxParseResult;
|
|
49
|
+
/** The request-body stamp for a selection: EXPLICIT ⇒ `{sandboxImageProfile}`, default/auto ⇒ nothing
|
|
50
|
+
* (auto is advisory-by-prompt — the submit itself never carries a profile, per clay's c) 口径). */
|
|
51
|
+
export declare function sandboxRequestFields(sel: SandboxSelection | undefined): {
|
|
52
|
+
sandboxImageProfile?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The `--sandbox auto` advisory block. Design choices (each deliberate):
|
|
56
|
+
* - Conditional on the tool being mounted: `SelectEnvironment` reaches the model via the deployment's
|
|
57
|
+
* `spec.tools` injection (service select-environment-tool.ts RFC A2) — a worker without it must degrade
|
|
58
|
+
* HONESTLY (model says so and continues on the default), not hallucinate a selection.
|
|
59
|
+
* - The `Sandbox: <profile> — <reason>` reply line IS the run-card echo (可观测可纠正): the tool call
|
|
60
|
+
* renders as a normal tool card and the choice+reason line lands in the assistant reply — zero new
|
|
61
|
+
* renderer machinery, zero new server endpoints.
|
|
62
|
+
* - Boundary honesty: the server applies a session's selection from the NEXT task boundary ([440] F3),
|
|
63
|
+
* so the advisory says so — the model must not promise the current turn already runs on the new image.
|
|
64
|
+
*/
|
|
65
|
+
export declare const AUTO_SANDBOX_ADVISORY: string;
|
|
66
|
+
/** Prefix an objective with the auto-select advisory (one-shot — the caller owns the latch). */
|
|
67
|
+
export declare function wrapObjectiveWithAutoSandboxAdvisory(objective: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Map a submit failure → the sandbox DEGRADATION HINT, or undefined when the error is not sandbox-shaped.
|
|
70
|
+
* Keys on the server's honest reject texts (ai-agent-service main.ts:2323-2330, per-task-image.ts:52,
|
|
71
|
+
* server.ts:4651/4670) — status+substring, the same recognize-by-wire-shape idiom the quota/scenario
|
|
72
|
+
* copy modules use. The caller renders it under the raw "API Error: …" row (original message preserved —
|
|
73
|
+
* the hint supplements, never masks, the server's own words).
|
|
74
|
+
*/
|
|
75
|
+
export declare function sandboxDegradeHint(err: unknown): string | undefined;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/sandboxWire.ts — [686]② `--sandbox` TRI-STATE flag (clay 拍, 2026-07-13): the CLI face of the
|
|
3
|
+
* per-task SANDBOX IMAGE PROFILE the server already ships end-to-end (`TaskRequest.sandboxImageProfile`
|
|
4
|
+
* per-task binding + the model-facing `SelectEnvironment` tool + the `GET /v1/images` catalog).
|
|
5
|
+
*
|
|
6
|
+
* TRI-STATE (clay 口径):
|
|
7
|
+
* a) DEFAULT (no flag) → NO `sandboxImageProfile` on the POST body — the worker's default image runs.
|
|
8
|
+
* b) `--sandbox <profile>` → EXPLICIT: stamp `sandboxImageProfile` (SDK types.ts:115 → service
|
|
9
|
+
* main.ts:2316 resolves profile→digest FAIL-CLOSED with the caller's principal — we only ever send the
|
|
10
|
+
* PROFILE, never a digest). Vocabulary = the live catalog (`sema cloud images list` / GET /v1/images) —
|
|
11
|
+
* the shell does NOT pre-validate against it (fail-closed admission is the server's job; a typo comes
|
|
12
|
+
* back as an honest 404, mapped to a hint by sandboxDegradeHint below).
|
|
13
|
+
* c) `--sandbox auto` → ADVISORY: the submit carries NO profile; instead the FIRST live turn's
|
|
14
|
+
* objective is prefixed with an advisory block asking the model to drive the `SelectEnvironment` tool
|
|
15
|
+
* itself (list → pick → report `Sandbox: <profile> — <reason>` in its reply, so the choice + reason are
|
|
16
|
+
* visible on the run card and correctable by the user). NOT a hard route — server keeps zero new
|
|
17
|
+
* endpoints; the tool's session binding takes effect from the NEXT task boundary ([440] F3 contract).
|
|
18
|
+
*
|
|
19
|
+
* LANE HONESTY (诚实不撒谎 UI, [686] constraint): the profile only works on the k8s sandbox lane — an
|
|
20
|
+
* e2b/host-lane worker (including the local TOC self-spawned engine) 400s the submit honestly
|
|
21
|
+
* (service main.ts:2323 "sandboxImageProfile is only supported on the k8s sandbox backend"). The shell does
|
|
22
|
+
* NOT pre-judge the lane (no caps sniffing before submit — per [686]: "不预判 lane,收到 400 再提示");
|
|
23
|
+
* it maps the server's honest reject into a degradation hint (sandboxDegradeHint) rendered on the error row.
|
|
24
|
+
* So on the local self-spawned engine `--sandbox <profile>` = a LOUD per-turn failure with an actionable
|
|
25
|
+
* hint — never a silent ignore (silently dropping the user's explicit environment choice would be lying).
|
|
26
|
+
*/
|
|
27
|
+
/** Mirror of the server's own body validation (server.ts:4651): non-empty, ≤128 chars. Validating locally
|
|
28
|
+
* just converts a guaranteed 400 into an immediate CLI error — same rule, earlier and cheaper. */
|
|
29
|
+
const MAX_PROFILE_LEN = 128;
|
|
30
|
+
const USAGE_HINT = "sema: --sandbox expects a profile name or 'auto'.\n" +
|
|
31
|
+
' --sandbox <profile> run on that sandbox image profile (k8s-lane workers; list: sema cloud images list)\n' +
|
|
32
|
+
' --sandbox auto let the model pick via its SelectEnvironment tool (advisory — choice + reason shown in the reply)\n' +
|
|
33
|
+
' (no flag) the worker default image';
|
|
34
|
+
/**
|
|
35
|
+
* Parse `--sandbox` out of an argv slice (everything before a bare `--`; both `--sandbox <v>` and
|
|
36
|
+
* `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
|
|
37
|
+
* Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
|
|
38
|
+
* a mis-typed environment choice must not quietly run on the wrong image).
|
|
39
|
+
*/
|
|
40
|
+
export function parseSandboxArgv(argv) {
|
|
41
|
+
let sel = { mode: 'default' };
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === '--')
|
|
45
|
+
break; // positionals — never flags
|
|
46
|
+
let raw;
|
|
47
|
+
if (a === '--sandbox') {
|
|
48
|
+
const nxt = argv[i + 1];
|
|
49
|
+
if (nxt === undefined || nxt.startsWith('-')) {
|
|
50
|
+
return { ok: false, error: USAGE_HINT };
|
|
51
|
+
}
|
|
52
|
+
raw = nxt;
|
|
53
|
+
i++; // consume the value token
|
|
54
|
+
}
|
|
55
|
+
else if (a.startsWith('--sandbox=')) {
|
|
56
|
+
raw = a.slice('--sandbox='.length);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const v = raw.trim();
|
|
62
|
+
if (v.length === 0)
|
|
63
|
+
return { ok: false, error: USAGE_HINT };
|
|
64
|
+
// audit #17: a bare `--sandbox` EATS the following positional — `-p --sandbox 'say hi'`
|
|
65
|
+
// consumed the prompt as the profile, then died on an unrelated "Input must be provided…"
|
|
66
|
+
// error. Profile names never contain whitespace; a value that does is almost certainly the
|
|
67
|
+
// user's prompt — say exactly that instead of silently mis-parsing.
|
|
68
|
+
if (/\s/.test(v)) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `Invalid sandbox profile "${v}". Profile names cannot contain spaces — if you meant it as the prompt, ` +
|
|
72
|
+
`pass --sandbox a profile name (or 'auto') first, or omit --sandbox to use the worker default image.\n` +
|
|
73
|
+
USAGE_HINT,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (v.length > MAX_PROFILE_LEN) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: `sema: --sandbox profile must be at most ${MAX_PROFILE_LEN} characters (same rule the server enforces).`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
sel = v === 'auto' ? { mode: 'auto' } : { mode: 'explicit', profile: v };
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, sel };
|
|
85
|
+
}
|
|
86
|
+
/** The request-body stamp for a selection: EXPLICIT ⇒ `{sandboxImageProfile}`, default/auto ⇒ nothing
|
|
87
|
+
* (auto is advisory-by-prompt — the submit itself never carries a profile, per clay's c) 口径). */
|
|
88
|
+
export function sandboxRequestFields(sel) {
|
|
89
|
+
return sel?.mode === 'explicit' ? { sandboxImageProfile: sel.profile } : {};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The `--sandbox auto` advisory block. Design choices (each deliberate):
|
|
93
|
+
* - Conditional on the tool being mounted: `SelectEnvironment` reaches the model via the deployment's
|
|
94
|
+
* `spec.tools` injection (service select-environment-tool.ts RFC A2) — a worker without it must degrade
|
|
95
|
+
* HONESTLY (model says so and continues on the default), not hallucinate a selection.
|
|
96
|
+
* - The `Sandbox: <profile> — <reason>` reply line IS the run-card echo (可观测可纠正): the tool call
|
|
97
|
+
* renders as a normal tool card and the choice+reason line lands in the assistant reply — zero new
|
|
98
|
+
* renderer machinery, zero new server endpoints.
|
|
99
|
+
* - Boundary honesty: the server applies a session's selection from the NEXT task boundary ([440] F3),
|
|
100
|
+
* so the advisory says so — the model must not promise the current turn already runs on the new image.
|
|
101
|
+
*/
|
|
102
|
+
export const AUTO_SANDBOX_ADVISORY = '<sandbox-auto-select>\n' +
|
|
103
|
+
'The user launched this session with `--sandbox auto`: choose the best sandbox environment (image profile) for the work in this session yourself.\n' +
|
|
104
|
+
'If a SelectEnvironment tool is available: call it with no arguments to list the available profiles and their capabilities, then select the one that best fits the task (keep the default if nothing fits better). After deciding, report your choice on its own line, exactly in this form: `Sandbox: <profile> — <one-line reason>` (use `Sandbox: default — <reason>` if you kept the default). Note the selection takes effect from the next task boundary — the current turn keeps the environment it started with.\n' +
|
|
105
|
+
'If no SelectEnvironment tool is available, briefly say so and continue in the default environment.\n' +
|
|
106
|
+
'</sandbox-auto-select>';
|
|
107
|
+
/** Prefix an objective with the auto-select advisory (one-shot — the caller owns the latch). */
|
|
108
|
+
export function wrapObjectiveWithAutoSandboxAdvisory(objective) {
|
|
109
|
+
return `${AUTO_SANDBOX_ADVISORY}\n\n${objective}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Map a submit failure → the sandbox DEGRADATION HINT, or undefined when the error is not sandbox-shaped.
|
|
113
|
+
* Keys on the server's honest reject texts (ai-agent-service main.ts:2323-2330, per-task-image.ts:52,
|
|
114
|
+
* server.ts:4651/4670) — status+substring, the same recognize-by-wire-shape idiom the quota/scenario
|
|
115
|
+
* copy modules use. The caller renders it under the raw "API Error: …" row (original message preserved —
|
|
116
|
+
* the hint supplements, never masks, the server's own words).
|
|
117
|
+
*/
|
|
118
|
+
export function sandboxDegradeHint(err) {
|
|
119
|
+
const e = err;
|
|
120
|
+
if (typeof e?.status !== 'number' || typeof e?.message !== 'string')
|
|
121
|
+
return undefined;
|
|
122
|
+
const msg = e.message;
|
|
123
|
+
if (e.status === 400 && msg.includes('only supported on the k8s sandbox backend')) {
|
|
124
|
+
return ('--sandbox <profile> only applies on k8s-lane workers; this engine runs a host/e2b sandbox lane, so the request was honestly rejected.\n' +
|
|
125
|
+
'Re-run without --sandbox (worker default image), or point the shell at a k8s-lane worker (sema cloud engine connect <url>).');
|
|
126
|
+
}
|
|
127
|
+
if (e.status === 400 && msg.includes('requires the image index')) {
|
|
128
|
+
return ('This worker has no sandbox image index configured (sema-registry backend absent), so per-task profiles cannot resolve.\n' +
|
|
129
|
+
'Re-run without --sandbox, or use a worker with the image index enabled.');
|
|
130
|
+
}
|
|
131
|
+
if (e.status === 404 && msg.includes('no published sandbox image for profile')) {
|
|
132
|
+
return 'No published image matches that profile for your principal. List what you can use: sema cloud images list';
|
|
133
|
+
}
|
|
134
|
+
if (e.status === 400 && msg.includes('not supported together with cascade/verify')) {
|
|
135
|
+
return '--sandbox cannot combine with cascade/verify submits in v1 (the per-task image would not bind to the sub-runs). Drop one of the two.';
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sema/scenarioWire.ts — `--scenario <name>` wire (clay 拍, 2026-07-15): expose the engine's
|
|
3
|
+
* REQUEST-LEVEL scenario routing axis on the headless `-p` path. The server has shipped scenario
|
|
4
|
+
* routing since day one (ai-agent-service capabilities/scenarios.ts — `TaskRequest.scenario` picks the
|
|
5
|
+
* tool roster + prompt provider per request: default / code-review / scan / oa / team + center-declared
|
|
6
|
+
* overlays); the shell simply never exposed the field to users. This module is the CLI face.
|
|
7
|
+
*
|
|
8
|
+
* PRECEDENCE (flag > settings > default):
|
|
9
|
+
* a) `--scenario <name>` (explicit flag) → stamp `TaskRequest.scenario`.
|
|
10
|
+
* b) `SEMA_HEADLESS_SCENARIO` env — the settings lane: → stamp when no flag. Set it in the user
|
|
11
|
+
* settings.json `env` block (seeded into process.env by the SAME 1a-envseed both the REPL boot and
|
|
12
|
+
* printModeEngine run), the lane every other wire config rides (SEMA_SELF_ORCHESTRATION,
|
|
13
|
+
* SEMA_WEBSEARCH_*, SEMA_ENABLE_FORK). One mechanism serves both "env var" and "settings file".
|
|
14
|
+
* c) neither → NO stamp = today's behaviour, verbatim.
|
|
15
|
+
*
|
|
16
|
+
* VOCABULARY = the server's scenario registry (builtin + center-declared). The shell does NOT
|
|
17
|
+
* pre-validate against it (the registry is deployment-side truth): an UNKNOWN name falls back to the
|
|
18
|
+
* `default` scenario server-side (scenarios.ts selectScenario — safe), and a center allowlist violation
|
|
19
|
+
* comes back as an honest 400 `scenario_not_allowed` (already rendered by scenarioNotAllowedCopy.ts).
|
|
20
|
+
* We only mirror the server's NAME-SHAPE rule (scenarios.ts:318 SCENARIO_NAME_RE) locally — same rule,
|
|
21
|
+
* earlier and cheaper, and it keeps prompt-looking values (whitespace etc.) from being eaten as a name.
|
|
22
|
+
*
|
|
23
|
+
* NOTE (mock-key shadow): the seam also uses `scenario` as the MOCK transcript selector
|
|
24
|
+
* (seamQuery.selectScenario / SEMA_SCENARIO). liveClient strips those mock keys before a live forward;
|
|
25
|
+
* a `--scenario` value that collides with a mock key (e.g. `bash`) is therefore equivalent to `default`
|
|
26
|
+
* on the live engine — which is exactly what the server would resolve for it anyway (no such scenario).
|
|
27
|
+
*
|
|
28
|
+
* SEMA_SCENARIO (the mock harness opt-in — "never spawn a real engine", printModeEngine.ts) is
|
|
29
|
+
* deliberately NOT reused here: it selects offline mock transcripts, not live engine routing.
|
|
30
|
+
*/
|
|
31
|
+
import { type EnvLike } from './hostEnv.js';
|
|
32
|
+
/** Mirror of the server's scenario NAME shape (ai-agent-service scenarios.ts:318). */
|
|
33
|
+
export declare const SCENARIO_NAME_RE: RegExp;
|
|
34
|
+
/** Settings-lane env knob (settings.json `env` block → 1a-envseed → process.env). */
|
|
35
|
+
export declare const HEADLESS_SCENARIO_ENV = "SEMA_HEADLESS_SCENARIO";
|
|
36
|
+
export type ScenarioParseResult = {
|
|
37
|
+
ok: true;
|
|
38
|
+
scenario?: string;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Parse `--scenario` out of an argv slice (everything before a bare `--`; both `--scenario <v>` and
|
|
45
|
+
* `--scenario=<v>` forms — the sandboxWire idiom). No flag ⇒ `{ok:true}` with no scenario. Repeated
|
|
46
|
+
* flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
|
|
47
|
+
* fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
|
|
48
|
+
* the wrong tool surface; same stance as `--sandbox`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseScenarioArgv(argv: string[]): ScenarioParseResult;
|
|
51
|
+
/**
|
|
52
|
+
* The settings-lane default (`SEMA_HEADLESS_SCENARIO`). FAIL-SOFT on a bad value (stderr warning +
|
|
53
|
+
* no stamp): a settings-file typo must not hard-brick every headless run the way an explicit flag
|
|
54
|
+
* typo fails the one run it was typed on.
|
|
55
|
+
*/
|
|
56
|
+
export declare function headlessScenarioFromEnv(env?: EnvLike): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the scenario for a headless `-p` submit: explicit `--scenario` flag > `SEMA_HEADLESS_SCENARIO`
|
|
59
|
+
* settings default > none (= the engine's default scenario, today's behaviour). Flag parse errors
|
|
60
|
+
* propagate fail-loud; the caller exits.
|
|
61
|
+
*/
|
|
62
|
+
export declare function scenarioForPrint(argv: string[], env?: EnvLike): ScenarioParseResult;
|