rivet-design 0.14.9 → 0.14.11
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/agent-variants/SessionStore.d.ts +11 -1
- package/dist/agent-variants/SessionStore.d.ts.map +1 -1
- package/dist/agent-variants/SessionStore.js +18 -0
- package/dist/agent-variants/SessionStore.js.map +1 -1
- package/dist/agent-variants/WorkerExecutorService.d.ts +77 -9
- package/dist/agent-variants/WorkerExecutorService.d.ts.map +1 -1
- package/dist/agent-variants/WorkerExecutorService.js +202 -104
- package/dist/agent-variants/WorkerExecutorService.js.map +1 -1
- package/dist/agent-variants/WorktreeOrchestrator.d.ts +13 -0
- package/dist/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
- package/dist/agent-variants/WorktreeOrchestrator.js +25 -0
- package/dist/agent-variants/WorktreeOrchestrator.js.map +1 -1
- package/dist/agent-variants/contracts.d.ts +22 -0
- package/dist/agent-variants/contracts.d.ts.map +1 -1
- package/dist/agent-variants/contracts.js.map +1 -1
- package/dist/agent-variants/directionAuthoring.d.ts.map +1 -1
- package/dist/agent-variants/directionAuthoring.js +12 -0
- package/dist/agent-variants/directionAuthoring.js.map +1 -1
- package/dist/agent-variants/workItemBriefing.d.ts.map +1 -1
- package/dist/agent-variants/workItemBriefing.js +48 -16
- package/dist/agent-variants/workItemBriefing.js.map +1 -1
- package/dist/agent-variants/workerRuntime.d.ts +230 -0
- package/dist/agent-variants/workerRuntime.d.ts.map +1 -0
- package/dist/agent-variants/workerRuntime.js +705 -0
- package/dist/agent-variants/workerRuntime.js.map +1 -0
- package/dist/cli/commands/mcpServe.d.ts.map +1 -1
- package/dist/cli/commands/mcpServe.js +16 -1
- package/dist/cli/commands/mcpServe.js.map +1 -1
- package/dist/cli/commands/variants.d.ts.map +1 -1
- package/dist/cli/commands/variants.js +26 -0
- package/dist/cli/commands/variants.js.map +1 -1
- package/dist/cli/hostWorkNextAction.d.ts +4 -0
- package/dist/cli/hostWorkNextAction.d.ts.map +1 -1
- package/dist/cli/hostWorkNextAction.js.map +1 -1
- package/dist/cli/router.js +1 -1
- package/dist/cli/router.js.map +1 -1
- package/dist/routes/agentVariants.d.ts.map +1 -1
- package/dist/routes/agentVariants.js +15 -0
- package/dist/routes/agentVariants.js.map +1 -1
- package/dist/services/ConfigManager.d.ts +18 -0
- package/dist/services/ConfigManager.d.ts.map +1 -1
- package/dist/services/ConfigManager.js +15 -0
- package/dist/services/ConfigManager.js.map +1 -1
- package/dist/services/VariantGenerationService.d.ts +5 -0
- package/dist/services/VariantGenerationService.d.ts.map +1 -1
- package/dist/services/VariantGenerationService.js +42 -11
- package/dist/services/VariantGenerationService.js.map +1 -1
- package/dist/services/VariantHistoryService.d.ts +7 -0
- package/dist/services/VariantHistoryService.d.ts.map +1 -1
- package/dist/services/VariantHistoryService.js +1 -0
- package/dist/services/VariantHistoryService.js.map +1 -1
- package/dist/services/VariantRunService.d.ts.map +1 -1
- package/dist/services/VariantRunService.js +17 -3
- package/dist/services/VariantRunService.js.map +1 -1
- package/dist/services/VariantsRuntime.d.ts.map +1 -1
- package/dist/services/VariantsRuntime.js +6 -0
- package/dist/services/VariantsRuntime.js.map +1 -1
- package/dist/types/change-request-types.d.ts +22 -0
- package/dist/types/change-request-types.d.ts.map +1 -1
- package/dist/utils/hostAgentEnv.d.ts +19 -0
- package/dist/utils/hostAgentEnv.d.ts.map +1 -0
- package/dist/utils/hostAgentEnv.js +32 -0
- package/dist/utils/hostAgentEnv.js.map +1 -0
- package/dist/utils/skills/claude-skill.d.ts +2 -2
- package/dist/utils/skills/claude-skill.d.ts.map +1 -1
- package/dist/utils/skills/claude-skill.js +1 -1
- package/dist/utils/skills/cli-guidance.d.ts +2 -2
- package/dist/utils/skills/cli-guidance.d.ts.map +1 -1
- package/dist/utils/skills/cli-guidance.js +4 -2
- package/dist/utils/skills/cli-guidance.js.map +1 -1
- package/dist/utils/skills/cursor-rules.d.ts +2 -2
- package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
- package/dist/utils/skills/cursor-rules.js +1 -1
- package/package.json +1 -1
- package/src/ui/dist/assets/{main-CQho_sDZ.js → main-EGX1lAi3.js} +2 -2
- package/src/ui/dist/index.html +1 -1
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.describeWorkerRuntimeDecision = exports.WorkerRuntimeResolver = exports.adapterForVendor = exports.makeCursorAdapter = exports.parseCursorWorkerResult = exports.makeCodexAdapter = exports.makeClaudeAdapter = exports.parseCodexWorkerResult = exports.parseClaudeWorkerResult = exports.PREFLIGHT_TIERS = exports.EXECUTOR_FIDELITY_TIERS = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const hostAgentEnv_1 = require("../utils/hostAgentEnv");
|
|
11
|
+
const runnerSelection_1 = require("../cli/runnerSelection");
|
|
12
|
+
const workerDefaults_1 = require("../hosts/workerDefaults");
|
|
13
|
+
const log = (0, logger_1.createLogger)('WorkerRuntime');
|
|
14
|
+
/**
|
|
15
|
+
* Fidelity → tier, per vendor. Claude mirrors the host registry (one model,
|
|
16
|
+
* effort varies — PR #965). Codex maps fidelity onto reasoning effort, its
|
|
17
|
+
* native analogue. Cursor cannot vary quality per invocation, so fidelity is
|
|
18
|
+
* intentionally a no-op there until a per-model benchmark says otherwise.
|
|
19
|
+
*/
|
|
20
|
+
exports.EXECUTOR_FIDELITY_TIERS = {
|
|
21
|
+
claude: {
|
|
22
|
+
low: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.low },
|
|
23
|
+
medium: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.medium },
|
|
24
|
+
high: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.high },
|
|
25
|
+
},
|
|
26
|
+
codex: {
|
|
27
|
+
low: { vendor: 'codex', reasoningEffort: 'low' },
|
|
28
|
+
medium: { vendor: 'codex', reasoningEffort: 'medium' },
|
|
29
|
+
high: { vendor: 'codex', reasoningEffort: 'high' },
|
|
30
|
+
},
|
|
31
|
+
cursor: {
|
|
32
|
+
low: { vendor: 'cursor' },
|
|
33
|
+
medium: { vendor: 'cursor' },
|
|
34
|
+
high: { vendor: 'cursor' },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** Cheapest valid tier per vendor — what the preflight probe runs with. Same
|
|
38
|
+
* flags as a real worker (the probe must test the exact invocation), minimum
|
|
39
|
+
* spend. */
|
|
40
|
+
exports.PREFLIGHT_TIERS = {
|
|
41
|
+
claude: exports.EXECUTOR_FIDELITY_TIERS.claude.low,
|
|
42
|
+
codex: exports.EXECUTOR_FIDELITY_TIERS.codex.low,
|
|
43
|
+
cursor: exports.EXECUTOR_FIDELITY_TIERS.cursor.low,
|
|
44
|
+
};
|
|
45
|
+
const requireVendorTier = (tier, vendor) => {
|
|
46
|
+
if (tier.vendor !== vendor) {
|
|
47
|
+
throw new Error(`executor tier/adapter mismatch: ${tier.vendor} tier handed to ${vendor} adapter`);
|
|
48
|
+
}
|
|
49
|
+
return tier;
|
|
50
|
+
};
|
|
51
|
+
/** Claude's `--output-format json`: one trailing JSON result object. */
|
|
52
|
+
const parseClaudeWorkerResult = (stdout) => {
|
|
53
|
+
try {
|
|
54
|
+
const trimmed = stdout.trim();
|
|
55
|
+
const start = trimmed.indexOf('{');
|
|
56
|
+
if (start === -1)
|
|
57
|
+
return {};
|
|
58
|
+
const parsed = JSON.parse(trimmed.slice(start));
|
|
59
|
+
const sum = (...values) => values.reduce((total, value) => total + (typeof value === 'number' ? value : 0), 0);
|
|
60
|
+
const usage = parsed.usage;
|
|
61
|
+
const errorParts = [parsed.subtype, parsed.result, parsed.error]
|
|
62
|
+
.filter((value) => typeof value === 'string')
|
|
63
|
+
.join(': ');
|
|
64
|
+
return {
|
|
65
|
+
...(usage || typeof parsed.total_cost_usd === 'number'
|
|
66
|
+
? {
|
|
67
|
+
usage: {
|
|
68
|
+
...(usage
|
|
69
|
+
? {
|
|
70
|
+
tokensIn: sum(usage.input_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens),
|
|
71
|
+
tokensOut: sum(usage.output_tokens),
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
...(typeof parsed.total_cost_usd === 'number'
|
|
75
|
+
? { costUsd: parsed.total_cost_usd }
|
|
76
|
+
: {}),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
...(errorParts.length > 0 ? { resultText: errorParts } : {}),
|
|
81
|
+
...(typeof parsed.api_error_status === 'number'
|
|
82
|
+
? { apiErrorStatus: parsed.api_error_status }
|
|
83
|
+
: {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
exports.parseClaudeWorkerResult = parseClaudeWorkerResult;
|
|
91
|
+
/** Codex's `exec --json`: JSONL events. Usage rides `turn.completed`; failures
|
|
92
|
+
* surface as `item.completed` items of type `error` (and `turn.failed`). */
|
|
93
|
+
const parseCodexWorkerResult = (stdout) => {
|
|
94
|
+
const info = {};
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const line of stdout.split('\n')) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed.startsWith('{'))
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
const event = JSON.parse(trimmed);
|
|
102
|
+
if (event.type === 'turn.completed' && event.usage) {
|
|
103
|
+
info.usage = {
|
|
104
|
+
tokensIn: event.usage.input_tokens ?? 0,
|
|
105
|
+
tokensOut: (event.usage.output_tokens ?? 0) +
|
|
106
|
+
(event.usage.reasoning_output_tokens ?? 0),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (event.type === 'item.completed' &&
|
|
110
|
+
event.item?.type === 'error' &&
|
|
111
|
+
typeof event.item.message === 'string') {
|
|
112
|
+
errors.push(event.item.message);
|
|
113
|
+
}
|
|
114
|
+
if (event.type === 'turn.failed' || event.type === 'error') {
|
|
115
|
+
const message = typeof event.error === 'string' ? event.error : event.error?.message;
|
|
116
|
+
if (message)
|
|
117
|
+
errors.push(message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Interleaved non-JSON log lines are expected; skip them.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (errors.length > 0)
|
|
125
|
+
info.resultText = errors.join(': ');
|
|
126
|
+
return info;
|
|
127
|
+
};
|
|
128
|
+
exports.parseCodexWorkerResult = parseCodexWorkerResult;
|
|
129
|
+
const makeClaudeAdapter = (binary = 'claude') => ({
|
|
130
|
+
vendor: 'claude',
|
|
131
|
+
binary,
|
|
132
|
+
promptDelivery: 'stdin',
|
|
133
|
+
buildArgs(tier) {
|
|
134
|
+
const claudeTier = requireVendorTier(tier, 'claude');
|
|
135
|
+
return [
|
|
136
|
+
'--print',
|
|
137
|
+
'--output-format',
|
|
138
|
+
'json',
|
|
139
|
+
'--model',
|
|
140
|
+
claudeTier.model,
|
|
141
|
+
'--effort',
|
|
142
|
+
claudeTier.effort,
|
|
143
|
+
'--dangerously-skip-permissions',
|
|
144
|
+
'--allowed-tools',
|
|
145
|
+
'Read,Write,Edit,Bash,Glob,Grep',
|
|
146
|
+
'--exclude-dynamic-system-prompt-sections',
|
|
147
|
+
];
|
|
148
|
+
},
|
|
149
|
+
parseResult: exports.parseClaudeWorkerResult,
|
|
150
|
+
supportsModelFallback: true,
|
|
151
|
+
// Strip nested-session markers: a `claude` worker spawned from a server
|
|
152
|
+
// that was itself started inside a Claude Code session would otherwise hit
|
|
153
|
+
// the refuse-to-nest guard (see hostAgentEnv.ts).
|
|
154
|
+
spawnEnv: () => (0, hostAgentEnv_1.sanitizeHostAgentEnv)(process.env),
|
|
155
|
+
});
|
|
156
|
+
exports.makeClaudeAdapter = makeClaudeAdapter;
|
|
157
|
+
const makeCodexAdapter = (binary = runnerSelection_1.RUNNER_VENDOR_BINARIES.codex) => ({
|
|
158
|
+
vendor: 'codex',
|
|
159
|
+
binary,
|
|
160
|
+
promptDelivery: 'stdin',
|
|
161
|
+
buildArgs(tier) {
|
|
162
|
+
const codexTier = requireVendorTier(tier, 'codex');
|
|
163
|
+
return [
|
|
164
|
+
'exec',
|
|
165
|
+
'--json',
|
|
166
|
+
'--skip-git-repo-check',
|
|
167
|
+
// Same posture as claude's --dangerously-skip-permissions; the worker
|
|
168
|
+
// is already confined to its own workspace by the briefing contract.
|
|
169
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
170
|
+
...(codexTier.model ? ['--model', codexTier.model] : []),
|
|
171
|
+
'-c',
|
|
172
|
+
`model_reasoning_effort=${codexTier.reasoningEffort}`,
|
|
173
|
+
'-',
|
|
174
|
+
];
|
|
175
|
+
},
|
|
176
|
+
parseResult: exports.parseCodexWorkerResult,
|
|
177
|
+
supportsModelFallback: false,
|
|
178
|
+
spawnEnv: () => process.env,
|
|
179
|
+
});
|
|
180
|
+
exports.makeCodexAdapter = makeCodexAdapter;
|
|
181
|
+
/** Cursor's `-p --output-format json`: one trailing JSON result object, like
|
|
182
|
+
* claude's — but usage keys are camelCase (`inputTokens`) and `subtype`/
|
|
183
|
+
* `result` are populated on success too ("success"/"ok"), so error text is
|
|
184
|
+
* taken only when `is_error` is set. Shape live-verified 2026-07-23 against
|
|
185
|
+
* cursor-agent 2026.07.23-e383d2b. */
|
|
186
|
+
const parseCursorWorkerResult = (stdout) => {
|
|
187
|
+
try {
|
|
188
|
+
const trimmed = stdout.trim();
|
|
189
|
+
const start = trimmed.indexOf('{');
|
|
190
|
+
if (start === -1)
|
|
191
|
+
return {};
|
|
192
|
+
const parsed = JSON.parse(trimmed.slice(start));
|
|
193
|
+
const sum = (...values) => values.reduce((total, value) => total + (typeof value === 'number' ? value : 0), 0);
|
|
194
|
+
const usage = parsed.usage;
|
|
195
|
+
const errorParts = parsed.is_error === true
|
|
196
|
+
? [parsed.subtype, parsed.result]
|
|
197
|
+
.filter((value) => typeof value === 'string')
|
|
198
|
+
.join(': ')
|
|
199
|
+
: '';
|
|
200
|
+
return {
|
|
201
|
+
...(usage
|
|
202
|
+
? {
|
|
203
|
+
usage: {
|
|
204
|
+
tokensIn: sum(usage.inputTokens, usage.cacheReadTokens, usage.cacheWriteTokens),
|
|
205
|
+
tokensOut: sum(usage.outputTokens),
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
: {}),
|
|
209
|
+
...(errorParts.length > 0 ? { resultText: errorParts } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
exports.parseCursorWorkerResult = parseCursorWorkerResult;
|
|
217
|
+
const makeCursorAdapter = (binary = runnerSelection_1.RUNNER_VENDOR_BINARIES.cursor) => ({
|
|
218
|
+
vendor: 'cursor',
|
|
219
|
+
binary,
|
|
220
|
+
// cursor-agent's print mode does not read the prompt from stdin — it goes
|
|
221
|
+
// in argv. Briefings are ~16-20KB, well inside ARG_MAX.
|
|
222
|
+
promptDelivery: 'argv',
|
|
223
|
+
buildArgs(tier) {
|
|
224
|
+
const cursorTier = requireVendorTier(tier, 'cursor');
|
|
225
|
+
return [
|
|
226
|
+
'-p',
|
|
227
|
+
'--output-format',
|
|
228
|
+
'json',
|
|
229
|
+
...(cursorTier.model ? ['-m', cursorTier.model] : []),
|
|
230
|
+
'--force',
|
|
231
|
+
];
|
|
232
|
+
},
|
|
233
|
+
parseResult: exports.parseCursorWorkerResult,
|
|
234
|
+
supportsModelFallback: false,
|
|
235
|
+
spawnEnv: () => process.env,
|
|
236
|
+
});
|
|
237
|
+
exports.makeCursorAdapter = makeCursorAdapter;
|
|
238
|
+
const adapterForVendor = (vendor, binaryOverride) => {
|
|
239
|
+
switch (vendor) {
|
|
240
|
+
case 'claude':
|
|
241
|
+
return (0, exports.makeClaudeAdapter)(binaryOverride);
|
|
242
|
+
case 'codex':
|
|
243
|
+
return (0, exports.makeCodexAdapter)(binaryOverride);
|
|
244
|
+
case 'cursor':
|
|
245
|
+
return (0, exports.makeCursorAdapter)(binaryOverride);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
exports.adapterForVendor = adapterForVendor;
|
|
249
|
+
const PREFLIGHT_PROMPT = 'Reply with the single word: ok. Do not use any tools.';
|
|
250
|
+
const DEFAULT_PREFLIGHT_TIMEOUT_MS = 30_000;
|
|
251
|
+
/** Vendor selections arrive as user-typed strings (config file, env var) —
|
|
252
|
+
* trim and case-fold before matching so `"Claude "` or `CODEX` still select
|
|
253
|
+
* the vendor instead of silently falling through to PATH probing. */
|
|
254
|
+
const asExecutableVendor = (value) => {
|
|
255
|
+
if (typeof value !== 'string')
|
|
256
|
+
return undefined;
|
|
257
|
+
const normalized = value.trim().toLowerCase();
|
|
258
|
+
return runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.includes(normalized)
|
|
259
|
+
? normalized
|
|
260
|
+
: undefined;
|
|
261
|
+
};
|
|
262
|
+
/** Strip ANSI escapes / control characters so a TUI splash (cursor-agent's
|
|
263
|
+
* sign-in banner renders box art to stdout) reads as one diagnosable line. */
|
|
264
|
+
const sanitizeProbeText = (text) => text
|
|
265
|
+
// eslint-disable-next-line no-control-regex
|
|
266
|
+
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '')
|
|
267
|
+
// eslint-disable-next-line no-control-regex
|
|
268
|
+
.replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
269
|
+
// eslint-disable-next-line no-control-regex
|
|
270
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ' ')
|
|
271
|
+
.replace(/\s+/g, ' ')
|
|
272
|
+
.trim();
|
|
273
|
+
/** An installed-but-logged-out CLI is the common first-run state (the CLI
|
|
274
|
+
* login is separate from the IDE's). Every vendor prints some "sign in"
|
|
275
|
+
* variant; match it so the degrade message says what to actually do. */
|
|
276
|
+
const LOGIN_MARKERS = /sign[ -]?in|not logged in|not signed in|unauthenticated|not authenticated|authentication required|invalid api key|please run \/login|401/i;
|
|
277
|
+
const VENDOR_LOGIN_HINTS = {
|
|
278
|
+
claude: 'not logged in — run /login inside `claude`',
|
|
279
|
+
codex: 'not logged in — run `codex login`',
|
|
280
|
+
cursor: 'not logged in — run `cursor-agent login`',
|
|
281
|
+
};
|
|
282
|
+
/** Health-cache identity of one probe: same vendor with a different binary or
|
|
283
|
+
* resolved model is a different runtime and must preflight independently.
|
|
284
|
+
* Request-PINNED probes get their own identity too: they run without the
|
|
285
|
+
* fallback-model rung, so their verdicts and ordinary (fallback-eligible)
|
|
286
|
+
* verdicts are not interchangeable in either direction — a pinned request
|
|
287
|
+
* must not ride a fallback-earned healthy, and an ordinary resolve must not
|
|
288
|
+
* join a pinned probe and lose its fallback rung. Distinct keys make both
|
|
289
|
+
* impossible and remove any in-flight sharing between the two modes. */
|
|
290
|
+
const probeCacheKey = (adapter, tier, requestPinnedModel) => `${adapter.vendor}|${adapter.binary}|${'model' in tier && tier.model ? tier.model : 'default'}${requestPinnedModel ? '|pinned' : ''}`;
|
|
291
|
+
const defaultPreflightSpawn = ({ command, args, cwd, env }) => (0, child_process_1.spawn)(command, args, {
|
|
292
|
+
cwd,
|
|
293
|
+
env,
|
|
294
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
295
|
+
detached: process.platform !== 'win32',
|
|
296
|
+
});
|
|
297
|
+
const killProcessTree = (pid) => {
|
|
298
|
+
if (!pid)
|
|
299
|
+
return;
|
|
300
|
+
try {
|
|
301
|
+
if (process.platform === 'win32') {
|
|
302
|
+
process.kill(pid, 'SIGKILL');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
process.kill(-pid, 'SIGKILL');
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
try {
|
|
309
|
+
process.kill(pid, 'SIGKILL');
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Already gone.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Resolves and preflights the executor's worker runtime, per request.
|
|
318
|
+
*
|
|
319
|
+
* Precedence: env override (`RIVET_EXECUTOR_VENDOR`, or legacy
|
|
320
|
+
* `RIVET_EXECUTOR_COMMAND` = claude-grammar custom binary) > user config
|
|
321
|
+
* `workerVendor` > the request initiator's agent > (human / no preference)
|
|
322
|
+
* probe installed vendors in registry order.
|
|
323
|
+
*
|
|
324
|
+
* Note this is deliberately PER-REQUEST, unlike apply-runner resolution
|
|
325
|
+
* (`resolveRunner`, fixed for the server's lifetime): one live session can
|
|
326
|
+
* serve requests initiated by different agents, and each request's workers
|
|
327
|
+
* should run on its initiator's runtime.
|
|
328
|
+
*
|
|
329
|
+
* Preflight lifecycle: one in-flight probe per runtime identity (vendor +
|
|
330
|
+
* binary + resolved model — concurrent resolves join it); a healthy result
|
|
331
|
+
* is cached until a runtime-shaped worker failure invalidates it
|
|
332
|
+
* (`markUnhealthy`) or a config edit changes the resolved model; an
|
|
333
|
+
* unhealthy result is re-probed on the next resolve — so a user who fixes
|
|
334
|
+
* their auth is not stuck on the host path, and each new run retries a
|
|
335
|
+
* broken vendor at most once.
|
|
336
|
+
*/
|
|
337
|
+
class WorkerRuntimeResolver {
|
|
338
|
+
options;
|
|
339
|
+
health = new Map();
|
|
340
|
+
inFlight = new Map();
|
|
341
|
+
constructor(options = {}) {
|
|
342
|
+
this.options = options;
|
|
343
|
+
}
|
|
344
|
+
/** Invalidate a cached-healthy vendor after a runtime-shaped worker failure
|
|
345
|
+
* (spawn error, unknown CLI option, auth rejection) so the next run
|
|
346
|
+
* re-probes instead of leasing four more items to a dead runtime. */
|
|
347
|
+
markUnhealthy(vendor, reason) {
|
|
348
|
+
for (const key of this.health.keys()) {
|
|
349
|
+
if (key.startsWith(`${vendor}|`)) {
|
|
350
|
+
this.health.set(key, {
|
|
351
|
+
healthy: false,
|
|
352
|
+
reason,
|
|
353
|
+
category: 'worker_failure',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
log.warn(`worker runtime ${vendor} marked unhealthy: ${reason}`);
|
|
358
|
+
}
|
|
359
|
+
async resolve(args) {
|
|
360
|
+
const { candidates, source, strict } = this.candidatesFor(args?.initiator, args?.selection);
|
|
361
|
+
const diagnostics = [];
|
|
362
|
+
for (const candidate of candidates) {
|
|
363
|
+
// The request model pin binds to ONE runtime: the requested provider,
|
|
364
|
+
// or — for a model-only request — the primary candidate the request
|
|
365
|
+
// resolved against. Fallback / later probe-path candidates are probed
|
|
366
|
+
// (and would run) without it: another CLI's model id is meaningless
|
|
367
|
+
// there, and a silent re-target would hand the user a runtime they
|
|
368
|
+
// didn't pick. When the RIVET_EXECUTOR_MODEL ops escape hatch is set,
|
|
369
|
+
// the pin is inert (the env model is what preflights and runs), so it
|
|
370
|
+
// must not travel as modelOverride — that would disable the ops
|
|
371
|
+
// model-fallback rung over a pin that is not even in effect.
|
|
372
|
+
const envModel = (this.options.env ?? process.env).RIVET_EXECUTOR_MODEL;
|
|
373
|
+
const modelPinApplies = Boolean(!envModel &&
|
|
374
|
+
args?.selection?.model &&
|
|
375
|
+
(args.selection.provider
|
|
376
|
+
? args.selection.provider === candidate.vendor
|
|
377
|
+
: candidate === candidates[0]));
|
|
378
|
+
const outcome = await this.preflight(candidate, args?.signal, modelPinApplies ? args?.selection : undefined);
|
|
379
|
+
if (outcome.healthy) {
|
|
380
|
+
return {
|
|
381
|
+
mode: 'executor',
|
|
382
|
+
adapter: candidate,
|
|
383
|
+
// A strict resolution that lands on a non-primary candidate got
|
|
384
|
+
// there via the cross-vendor fallback opt-in — label it as such.
|
|
385
|
+
source: strict && candidate !== candidates[0] ? 'fallback' : source,
|
|
386
|
+
...(modelPinApplies ? { modelOverride: args.selection.model } : {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
diagnostics.push({
|
|
390
|
+
vendor: candidate.vendor,
|
|
391
|
+
binary: candidate.binary,
|
|
392
|
+
reason: outcome.reason,
|
|
393
|
+
});
|
|
394
|
+
if (strict && !(this.options.crossVendorFallback?.() ?? false))
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
return { mode: 'host', diagnostics };
|
|
398
|
+
}
|
|
399
|
+
/** Candidate adapters in trial order, plus whether the choice was explicit
|
|
400
|
+
* (strict = unhealthy means host path, not another vendor). */
|
|
401
|
+
candidatesFor(initiator, selection) {
|
|
402
|
+
const env = this.options.env ?? process.env;
|
|
403
|
+
const probe = this.options.binaryProbe ?? runnerSelection_1.isBinaryOnPath;
|
|
404
|
+
// Legacy ops escape hatch: a custom command is assumed claude-grammar.
|
|
405
|
+
const legacyCommand = env.RIVET_EXECUTOR_COMMAND;
|
|
406
|
+
if (legacyCommand) {
|
|
407
|
+
return {
|
|
408
|
+
candidates: [(0, exports.makeClaudeAdapter)(legacyCommand)],
|
|
409
|
+
source: 'env',
|
|
410
|
+
strict: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const envVendor = asExecutableVendor(env.RIVET_EXECUTOR_VENDOR);
|
|
414
|
+
if (envVendor) {
|
|
415
|
+
return {
|
|
416
|
+
candidates: this.withFallbackCandidates(envVendor, probe),
|
|
417
|
+
source: 'env',
|
|
418
|
+
strict: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// Explicit per-request choice: the user's live intent ("--worker-provider
|
|
422
|
+
// claude" relayed by the host agent) beats a sticky config default, and
|
|
423
|
+
// only the ops env escape hatches beat it. Strict like every explicit
|
|
424
|
+
// choice — an unhealthy selection lands on the host path with the reason.
|
|
425
|
+
const requested = asExecutableVendor(selection?.provider);
|
|
426
|
+
if (requested) {
|
|
427
|
+
return {
|
|
428
|
+
candidates: this.withFallbackCandidates(requested, probe),
|
|
429
|
+
source: 'request',
|
|
430
|
+
strict: true,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const configured = asExecutableVendor(this.options.configuredVendor?.());
|
|
434
|
+
if (configured) {
|
|
435
|
+
return {
|
|
436
|
+
candidates: this.withFallbackCandidates(configured, probe),
|
|
437
|
+
source: 'user_config',
|
|
438
|
+
strict: true,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const initiatorVendor = initiator?.kind === 'agent'
|
|
442
|
+
? asExecutableVendor(initiator.agent)
|
|
443
|
+
: undefined;
|
|
444
|
+
if (initiatorVendor) {
|
|
445
|
+
// Strict even when the initiator's CLI is not installed: the missing
|
|
446
|
+
// binary fails preflight and the request degrades to the host path —
|
|
447
|
+
// never to another vendor's CLI without the fallback opt-in.
|
|
448
|
+
return {
|
|
449
|
+
candidates: this.withFallbackCandidates(initiatorVendor, probe),
|
|
450
|
+
source: 'initiator',
|
|
451
|
+
strict: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// Human request / unknown agent: no prior vendor expectation exists, so
|
|
455
|
+
// probing across installed vendors is fine.
|
|
456
|
+
return {
|
|
457
|
+
candidates: runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((vendor) => probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[vendor])).map((vendor) => this.adapterFor(vendor)),
|
|
458
|
+
source: 'probe',
|
|
459
|
+
strict: false,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
withFallbackCandidates(vendor, probe) {
|
|
463
|
+
const primary = this.adapterFor(vendor);
|
|
464
|
+
if (!(this.options.crossVendorFallback?.() ?? false))
|
|
465
|
+
return [primary];
|
|
466
|
+
const rest = runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((candidate) => candidate !== vendor && probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[candidate])).map((candidate) => this.adapterFor(candidate));
|
|
467
|
+
return [primary, ...rest];
|
|
468
|
+
}
|
|
469
|
+
adapterFor(vendor) {
|
|
470
|
+
return (0, exports.adapterForVendor)(vendor);
|
|
471
|
+
}
|
|
472
|
+
/** Tier the probe runs with — cheapest per vendor, with the same model
|
|
473
|
+
* override precedence a real worker gets (`RIVET_EXECUTOR_MODEL` env >
|
|
474
|
+
* request `--worker-model` > `workerModels.<vendor>` config) so the probe
|
|
475
|
+
* exercises the exact model a real worker would run, not the CLI
|
|
476
|
+
* default. */
|
|
477
|
+
probeTierFor(adapter, selection) {
|
|
478
|
+
const base = exports.PREFLIGHT_TIERS[adapter.vendor];
|
|
479
|
+
const env = this.options.env ?? process.env;
|
|
480
|
+
// The request model applies only to the vendor it was requested with —
|
|
481
|
+
// probing a cross-vendor fallback candidate with another CLI's model id
|
|
482
|
+
// would wrongly fail the fallback.
|
|
483
|
+
const requestModel = selection?.model &&
|
|
484
|
+
(!selection.provider || selection.provider === adapter.vendor)
|
|
485
|
+
? selection.model
|
|
486
|
+
: undefined;
|
|
487
|
+
const model = env.RIVET_EXECUTOR_MODEL ??
|
|
488
|
+
requestModel ??
|
|
489
|
+
this.options.configuredModelFor?.(adapter.vendor);
|
|
490
|
+
return model ? { ...base, model } : base;
|
|
491
|
+
}
|
|
492
|
+
async preflight(adapter, signal, selection) {
|
|
493
|
+
const tier = this.probeTierFor(adapter, selection);
|
|
494
|
+
// Cache key includes the binary and resolved model, not just the vendor:
|
|
495
|
+
// `workerModels.<vendor>` is read lazily, so a config edit while the
|
|
496
|
+
// server is live must re-probe rather than ride a stale healthy result
|
|
497
|
+
// for a model preflight never exercised.
|
|
498
|
+
// A request-pinned model must never silently degrade: if the user asked
|
|
499
|
+
// for a specific model and it fails preflight, the honest outcome is
|
|
500
|
+
// unhealthy (→ host path with the reason), not a fallback-model run the
|
|
501
|
+
// user didn't pick. resolve() only passes `selection` through when the
|
|
502
|
+
// pin applies to this adapter, so its presence is the signal. Pinned
|
|
503
|
+
// probes carry their own cache identity (see probeCacheKey), so pinned
|
|
504
|
+
// and ordinary resolves never share verdicts or in-flight probes.
|
|
505
|
+
const requestPinnedModel = Boolean(selection?.model);
|
|
506
|
+
const key = probeCacheKey(adapter, tier, requestPinnedModel);
|
|
507
|
+
const cached = this.health.get(key);
|
|
508
|
+
// Healthy results are reused; unhealthy results are re-probed (each new
|
|
509
|
+
// resolve gives a fixed vendor one fresh chance).
|
|
510
|
+
if (cached?.healthy)
|
|
511
|
+
return cached;
|
|
512
|
+
const inFlight = this.inFlight.get(key);
|
|
513
|
+
if (inFlight)
|
|
514
|
+
return inFlight;
|
|
515
|
+
const startedAt = Date.now();
|
|
516
|
+
const probePromise = (requestPinnedModel
|
|
517
|
+
? this.runProbe(adapter, tier, signal)
|
|
518
|
+
: this.probeWithModelFallback(adapter, tier, signal))
|
|
519
|
+
.then((outcome) => {
|
|
520
|
+
this.health.set(key, outcome);
|
|
521
|
+
// Telemetry gets the coarse category only: the raw reason is built
|
|
522
|
+
// from stderr/spawn errors and can carry local paths or account
|
|
523
|
+
// fragments. Full detail stays in server.log and run feedback.
|
|
524
|
+
this.options.telemetry?.track('agent_variants.executor_preflight', {
|
|
525
|
+
source: 'mcp',
|
|
526
|
+
vendor: adapter.vendor,
|
|
527
|
+
healthy: outcome.healthy,
|
|
528
|
+
durationMs: Date.now() - startedAt,
|
|
529
|
+
reason: outcome.healthy ? null : outcome.category,
|
|
530
|
+
});
|
|
531
|
+
if (!outcome.healthy) {
|
|
532
|
+
log.warn(`worker runtime preflight failed for ${adapter.vendor} ` +
|
|
533
|
+
`(${adapter.binary}): ${outcome.reason}`);
|
|
534
|
+
}
|
|
535
|
+
return outcome;
|
|
536
|
+
})
|
|
537
|
+
.finally(() => {
|
|
538
|
+
this.inFlight.delete(key);
|
|
539
|
+
});
|
|
540
|
+
this.inFlight.set(key, probePromise);
|
|
541
|
+
return probePromise;
|
|
542
|
+
}
|
|
543
|
+
/** One probe — plus, for claude (the only vendor with a structured
|
|
544
|
+
* fallback-model rung), a second probe on the fallback model when the
|
|
545
|
+
* primary MODEL is what failed. A healthy fallback probe marks the
|
|
546
|
+
* runtime healthy under the primary identity: real workers still start
|
|
547
|
+
* on the primary tier and degrade per item via the executor's one-shot
|
|
548
|
+
* fallback retry, exactly the pre-preflight behavior. */
|
|
549
|
+
async probeWithModelFallback(adapter, tier, signal) {
|
|
550
|
+
const outcome = await this.runProbe(adapter, tier, signal);
|
|
551
|
+
const fallback = this.options.probeModelFallback;
|
|
552
|
+
if (outcome.healthy ||
|
|
553
|
+
!fallback ||
|
|
554
|
+
!adapter.supportsModelFallback ||
|
|
555
|
+
outcome.category !== 'exit_nonzero') {
|
|
556
|
+
return outcome;
|
|
557
|
+
}
|
|
558
|
+
const primaryModel = 'model' in tier ? tier.model : undefined;
|
|
559
|
+
const fallbackModel = fallback.fallbackModel();
|
|
560
|
+
if (!primaryModel ||
|
|
561
|
+
!fallbackModel ||
|
|
562
|
+
fallbackModel === primaryModel ||
|
|
563
|
+
!fallback.isModelAvailabilityFailure({
|
|
564
|
+
message: outcome.reason,
|
|
565
|
+
...(outcome.apiErrorStatus !== undefined
|
|
566
|
+
? { apiErrorStatus: outcome.apiErrorStatus }
|
|
567
|
+
: {}),
|
|
568
|
+
model: primaryModel,
|
|
569
|
+
})) {
|
|
570
|
+
return outcome;
|
|
571
|
+
}
|
|
572
|
+
log.warn(`worker runtime preflight: ${adapter.vendor} model '${primaryModel}' ` +
|
|
573
|
+
`unavailable — probing fallback model '${fallbackModel}'`);
|
|
574
|
+
const fallbackOutcome = await this.runProbe(adapter, { ...tier, model: fallbackModel }, signal);
|
|
575
|
+
return fallbackOutcome.healthy ? fallbackOutcome : outcome;
|
|
576
|
+
}
|
|
577
|
+
runProbe(adapter, tier, signal) {
|
|
578
|
+
const timeoutMs = this.options.probeTimeoutMs ??
|
|
579
|
+
Number((this.options.env ?? process.env)
|
|
580
|
+
.RIVET_EXECUTOR_PREFLIGHT_TIMEOUT_MS ?? DEFAULT_PREFLIGHT_TIMEOUT_MS);
|
|
581
|
+
const spawnProbe = this.options.spawnProbe ?? defaultPreflightSpawn;
|
|
582
|
+
const args = adapter.buildArgs(tier);
|
|
583
|
+
if (adapter.promptDelivery === 'argv')
|
|
584
|
+
args.push(PREFLIGHT_PROMPT);
|
|
585
|
+
return new Promise((resolve) => {
|
|
586
|
+
// A signal that aborted before we attached would otherwise leave the
|
|
587
|
+
// probe running to its full timeout — check up front.
|
|
588
|
+
if (signal?.aborted) {
|
|
589
|
+
resolve({
|
|
590
|
+
healthy: false,
|
|
591
|
+
reason: 'probe aborted (run cancelled)',
|
|
592
|
+
category: 'aborted',
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let child;
|
|
597
|
+
try {
|
|
598
|
+
child = spawnProbe({
|
|
599
|
+
command: adapter.binary,
|
|
600
|
+
args,
|
|
601
|
+
cwd: os_1.default.tmpdir(),
|
|
602
|
+
env: adapter.spawnEnv(),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
resolve({
|
|
607
|
+
healthy: false,
|
|
608
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
609
|
+
category: 'spawn_error',
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
let settled = false;
|
|
614
|
+
let stderrTail = '';
|
|
615
|
+
let stdoutTail = '';
|
|
616
|
+
const settle = (outcome) => {
|
|
617
|
+
if (settled)
|
|
618
|
+
return;
|
|
619
|
+
settled = true;
|
|
620
|
+
clearTimeout(timer);
|
|
621
|
+
signal?.removeEventListener('abort', onAbort);
|
|
622
|
+
resolve(outcome);
|
|
623
|
+
};
|
|
624
|
+
const onAbort = () => {
|
|
625
|
+
killProcessTree(child.pid);
|
|
626
|
+
settle({
|
|
627
|
+
healthy: false,
|
|
628
|
+
reason: 'probe aborted (run cancelled)',
|
|
629
|
+
category: 'aborted',
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
const timer = setTimeout(() => {
|
|
633
|
+
killProcessTree(child.pid);
|
|
634
|
+
settle({
|
|
635
|
+
healthy: false,
|
|
636
|
+
reason: `probe timed out after ${timeoutMs}ms`,
|
|
637
|
+
category: 'timeout',
|
|
638
|
+
});
|
|
639
|
+
}, timeoutMs);
|
|
640
|
+
// Note: a dedup-shared probe can be aborted by the first joiner's
|
|
641
|
+
// signal. Accepted: the outcome is cached as unhealthy and the next
|
|
642
|
+
// resolve re-probes.
|
|
643
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
644
|
+
child.stderr?.on('data', (chunk) => {
|
|
645
|
+
stderrTail = (stderrTail + chunk.toString()).slice(-2000);
|
|
646
|
+
});
|
|
647
|
+
child.stdout?.on('data', (chunk) => {
|
|
648
|
+
stdoutTail = (stdoutTail + chunk.toString()).slice(-16384);
|
|
649
|
+
});
|
|
650
|
+
child.on('error', (error) => {
|
|
651
|
+
settle({ healthy: false, reason: error.message, category: 'spawn_error' });
|
|
652
|
+
});
|
|
653
|
+
child.on('close', (code) => {
|
|
654
|
+
if (code === 0) {
|
|
655
|
+
settle({ healthy: true });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const { resultText, apiErrorStatus } = adapter.parseResult(stdoutTail);
|
|
659
|
+
const stderrDetail = sanitizeProbeText(stderrTail);
|
|
660
|
+
// A logged-out CLI often reports nothing on stderr and no parseable
|
|
661
|
+
// result — just a sign-in splash on stdout. Fall back to that tail
|
|
662
|
+
// so the reason is never an empty "probe exited 1: ".
|
|
663
|
+
const stdoutDetail = sanitizeProbeText(stdoutTail);
|
|
664
|
+
const detail = stderrDetail || (resultText ? '' : stdoutDetail);
|
|
665
|
+
const loginRequired = LOGIN_MARKERS.test(`${stderrDetail}\n${resultText ?? ''}\n${stdoutDetail}`);
|
|
666
|
+
// Once diagnosed as a login problem the hint IS the reason — the raw
|
|
667
|
+
// output is a sign-in banner (ASCII art on cursor-agent) that only
|
|
668
|
+
// buries the actionable part.
|
|
669
|
+
settle(loginRequired
|
|
670
|
+
? {
|
|
671
|
+
healthy: false,
|
|
672
|
+
category: 'login_required',
|
|
673
|
+
reason: `${adapter.binary} is ${VENDOR_LOGIN_HINTS[adapter.vendor]} (probe exited ${code})`,
|
|
674
|
+
}
|
|
675
|
+
: {
|
|
676
|
+
healthy: false,
|
|
677
|
+
category: 'exit_nonzero',
|
|
678
|
+
...(apiErrorStatus !== undefined ? { apiErrorStatus } : {}),
|
|
679
|
+
reason: `probe exited ${code}: ${detail.slice(-400)}${resultText ? ` | result: ${resultText.slice(-500)}` : ''}`,
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
// Always close stdin: argv-delivery CLIs (cursor-agent print mode) can
|
|
683
|
+
// block on an open stdin pipe — the real worker spawn ends stdin
|
|
684
|
+
// unconditionally for the same reason.
|
|
685
|
+
if (adapter.promptDelivery === 'stdin') {
|
|
686
|
+
child.stdin?.write(PREFLIGHT_PROMPT);
|
|
687
|
+
}
|
|
688
|
+
child.stdin?.end();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
exports.WorkerRuntimeResolver = WorkerRuntimeResolver;
|
|
693
|
+
/** One-line human summary of a decision, for run feedback and server.log:
|
|
694
|
+
* `codex` / `host path (claude: probe exited 1: …)`. */
|
|
695
|
+
const describeWorkerRuntimeDecision = (decision) => {
|
|
696
|
+
if (decision.mode === 'executor') {
|
|
697
|
+
return decision.adapter.vendor;
|
|
698
|
+
}
|
|
699
|
+
const reasons = decision.diagnostics
|
|
700
|
+
.map((d) => `${d.vendor}: ${d.reason}`)
|
|
701
|
+
.join('; ');
|
|
702
|
+
return `host path${reasons ? ` (${reasons})` : ' (no worker runtime installed)'}`;
|
|
703
|
+
};
|
|
704
|
+
exports.describeWorkerRuntimeDecision = describeWorkerRuntimeDecision;
|
|
705
|
+
//# sourceMappingURL=workerRuntime.js.map
|