@mystilleef/pi-subagent 0.4.0 → 0.6.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.
@@ -4,23 +4,21 @@ import type {
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { type Static, Type } from "typebox";
7
- import { getCachedAgentDiscovery } from "./agent-cache.js";
8
- import {
9
- type AgentConfig,
10
- type AgentScope,
11
- discoverAgents,
12
- type ThinkingLevel,
13
- } from "./agents.js";
14
- import { generateSubagentInstanceName } from "./instance-name.js";
15
- import { runSingleAgent } from "./process.js";
7
+ import { getCachedAgentDiscovery } from "../agent/agent-cache.js";
8
+ import type {
9
+ AgentConfig,
10
+ AgentScope,
11
+ ThinkingLevel,
12
+ } from "../agent/agents.js";
13
+ import { runSingleAgent } from "../child/process.js";
14
+ import { formatSubagentResultForParent } from "../output/summary.js";
16
15
  import {
17
16
  cancelProgressState,
18
17
  createProgressState,
19
18
  failProgressState,
20
19
  finalizeProgressState,
21
- formatElapsed,
22
20
  getProgressState,
23
- } from "./progress.js";
21
+ } from "../progress/progress.js";
24
22
  import {
25
23
  createSubagentError,
26
24
  getFeedbackSummaryText,
@@ -28,20 +26,20 @@ import {
28
26
  hasSubagentFailed,
29
27
  patchProgressFromDetails,
30
28
  sanitizeDetailsForDisplay,
31
- } from "./result-details.js";
29
+ } from "../progress/result-details.js";
30
+ import { generateSubagentInstanceName } from "../shared/instance-name.js";
31
+ import type {
32
+ OnUpdateCallback,
33
+ SingleResult,
34
+ SubagentDetails,
35
+ SubagentToolResult,
36
+ } from "../shared/types.js";
32
37
  import {
33
38
  listRunJobs,
34
39
  type RunJob,
35
40
  registerRunJob,
36
41
  removeRunJob,
37
42
  } from "./run-registry.js";
38
- import { formatSubagentResultForParent } from "./summary.js";
39
- import type {
40
- OnUpdateCallback,
41
- SingleResult,
42
- SubagentDetails,
43
- SubagentToolResult,
44
- } from "./types.js";
45
43
 
46
44
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
47
45
  description:
@@ -100,18 +98,15 @@ function sanitizeResultDetails(
100
98
  includeDebugMessages && (options?.includeMessages ?? true);
101
99
  const { messages, termination, progress, stderr, usage, ...core } = result;
102
100
  const { contextWindowTokens, ...usageBase } = usage;
103
-
104
101
  const sanitized: Record<string, unknown> = {
105
102
  ...core,
106
103
  stderr: includeDebugMessages ? stderr : "",
107
104
  usage: { ...usageBase },
108
105
  };
109
-
110
106
  if (contextWindowTokens !== undefined) {
111
107
  (sanitized.usage as Record<string, unknown>).contextWindowTokens =
112
108
  contextWindowTokens;
113
109
  }
114
-
115
110
  if (progress !== undefined) {
116
111
  const { activityText, lastToolPreview, ...progBase } = progress;
117
112
  sanitized.progress = {
@@ -123,14 +118,12 @@ function sanitizeResultDetails(
123
118
  ...(lastToolPreview !== undefined && { lastToolPreview }),
124
119
  };
125
120
  }
126
-
127
121
  if (includeMessages) {
128
122
  sanitized.messages = options?.recentMessages
129
123
  ? [...options.recentMessages]
130
124
  : messages !== undefined
131
125
  ? [...messages]
132
126
  : undefined;
133
-
134
127
  if (includeDebugMessages && termination !== undefined) {
135
128
  const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
136
129
  termination;
@@ -142,7 +135,6 @@ function sanitizeResultDetails(
142
135
  };
143
136
  }
144
137
  }
145
-
146
138
  return sanitized as unknown as SingleResult;
147
139
  }
148
140
 
@@ -175,30 +167,14 @@ function sendSubagentResultMessage(
175
167
  });
176
168
  }
177
169
 
178
- export function emitCompletionNotification(
179
- ctx: ExtensionContext,
170
+ export function emitCompletionAlert(
180
171
  state: ReturnType<typeof getProgressState>,
181
172
  ): void {
182
173
  if (!state) return;
183
174
  if (state.status === "cancelled") return;
184
- if (!ctx.ui?.notify) return;
185
175
  const tty = (process.stdout as { isTTY?: boolean }).isTTY;
186
176
  if (!tty) return;
187
- const icon = state.status === "error" ? "✗" : "✓";
188
- const severity =
189
- state.status === "error" ? ("error" as const) : ("info" as const);
190
- const elapsed = formatElapsed(
191
- state.durationMs ?? Date.now() - state.startTime,
192
- );
193
- const raw = state.errorText ?? state.finalOutput ?? "";
194
- const preview = raw.trim().slice(0, 80);
195
- const namePart = state.instanceName
196
- ? `${state.agent} ${state.instanceName}`
197
- : state.agent;
198
- const toast = `${namePart} ${icon} ${elapsed} — "${preview}"`;
199
- // Terminal bell to draw attention when all jobs complete
200
177
  process.stdout.write("\x07");
201
- ctx.ui.notify(toast, severity);
202
178
  }
203
179
 
204
180
  async function runSubagentWorker(
@@ -289,7 +265,7 @@ async function runSubagentWorker(
289
265
  if (listRunJobs().length === 0) {
290
266
  const state = getProgressState(requestId);
291
267
  if (state) {
292
- emitCompletionNotification(ctx, state);
268
+ emitCompletionAlert(state);
293
269
  }
294
270
  }
295
271
  }
@@ -340,7 +316,7 @@ export async function startSubagentJob(
340
316
  hostSignal: AbortSignal | undefined,
341
317
  ): Promise<StartJobResult> {
342
318
  const agentScope: AgentScope = params.agentScope ?? "both";
343
- const discovery = getCachedAgentDiscovery(ctx.cwd, agentScope);
319
+ const discovery = await getCachedAgentDiscovery(ctx.cwd, agentScope);
344
320
  const agents = discovery.agents;
345
321
  const debug = params.debug === true;
346
322
  const makeDetails = createDetailsBuilder(
@@ -361,7 +337,7 @@ export async function startSubagentJob(
361
337
  if (!confirmed) return { kind: "cancelled", makeDetails };
362
338
  }
363
339
  if (requested.source === "project") {
364
- const userAgents = discoverAgents(ctx.cwd, "user");
340
+ const userAgents = await getCachedAgentDiscovery(ctx.cwd, "user");
365
341
  const hasUserCollision = userAgents.agents.some(
366
342
  (a) => a.name === requested.name,
367
343
  );
@@ -32,27 +32,6 @@ export function extractSemanticToolTarget(
32
32
  return "";
33
33
  }
34
34
 
35
- export function isTranscriptNoiseLine(line: string): boolean {
36
- return /^(?:(?:hello|hi|hey)(?:[!,.?:;]+|\s|$)|reasoning:|raw log:|apolog(?:y|ies)|sorry\b)/i.test(
37
- line,
38
- );
39
- }
40
-
41
- export function isFailureDiagnosticLine(line: string): boolean {
42
- return /^(?:at\s+|error:|failed:|failure:|exception:|traceback\b|caused by:)/i.test(
43
- line,
44
- );
45
- }
46
-
47
- export function filterOutputLines(output: string): string[] {
48
- return output
49
- .split(/\r?\n/)
50
- .map((l) => l.trim())
51
- .filter(
52
- (l) => l && !isTranscriptNoiseLine(l) && !isFailureDiagnosticLine(l),
53
- );
54
- }
55
-
56
35
  export function stripTerminalStatusPrefixes(value: string): string {
57
36
  return value.replace(/^(?:(?:success|failure):\s*)+/i, "");
58
37
  }
@@ -1,5 +1,5 @@
1
+ import type { SingleResult } from "../shared/types.js";
1
2
  import { normalizeTerminalSentence } from "./normalize.js";
2
- import type { SingleResult } from "./types.js";
3
3
 
4
4
  export const FEEDBACK_UI_SUMMARY_MAX_CHARS = 120;
5
5
 
@@ -20,7 +20,9 @@ const FEEDBACK_UI_LABEL_PATTERN =
20
20
  /^\s*(outcome|project summary|result|summary|status|output|message|error|check):\s*/i;
21
21
 
22
22
  export function formatSubagentResultForParent(result: SingleResult): string {
23
- return result.finalOutput;
23
+ return result.thinkingWarning
24
+ ? `[thinking] ${result.thinkingWarning}\n\n${result.finalOutput}`
25
+ : result.finalOutput;
24
26
  }
25
27
 
26
28
  export function summarizeFeedbackUiFinalOutput(finalOutput: string): string {
@@ -7,11 +7,7 @@ import {
7
7
  type MarkdownTheme,
8
8
  Text,
9
9
  } from "@earendil-works/pi-tui";
10
- import type { AgentScope } from "./agents.js";
11
- import {
12
- extractSemanticToolTarget,
13
- normalizeSummaryValue,
14
- } from "./normalize.js";
10
+ import type { AgentScope } from "../agent/agents.js";
15
11
  import {
16
12
  formatContextPercent,
17
13
  formatElapsed,
@@ -21,9 +17,13 @@ import {
21
17
  STATUS_ICON,
22
18
  type SubagentProgressState,
23
19
  type ThemeBg,
24
- } from "./progress-state.js";
25
- import { hasSubagentFailed } from "./result-details.js";
26
- import type { SubagentDetails, UsageStats } from "./types.js";
20
+ } from "../progress/progress-state.js";
21
+ import { hasSubagentFailed } from "../progress/result-details.js";
22
+ import type { SubagentDetails, UsageStats } from "../shared/types.js";
23
+ import {
24
+ extractSemanticToolTarget,
25
+ normalizeSummaryValue,
26
+ } from "./normalize.js";
27
27
 
28
28
  export type { ThemeBg };
29
29
 
@@ -103,32 +103,16 @@ export function formatUsageStats(
103
103
  return parts.join(" · ");
104
104
  }
105
105
 
106
- /**
107
- * Formats millisecond durations into human-readable time strings.
108
- */
109
- export function formatDuration(ms: number): string {
110
- if (ms < 1000) return `${Math.floor(ms)}ms`;
111
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
112
- const minutes = Math.floor(ms / 60000);
113
- const seconds = Math.floor((ms % 60000) / 1000);
114
- return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
115
- }
116
-
117
106
  /**
118
107
  * Formats the footer for subagent result cards, including model, context, turns, and cost.
119
108
  */
120
- export function formatResultFooter(
121
- usage: UsageStats,
122
- model?: string,
123
- durationMs?: number,
124
- ): string {
109
+ export function formatResultFooter(usage: UsageStats, model?: string): string {
125
110
  const parts: string[] = [];
126
111
  if (model) parts.push(model);
127
- if (usage.contextTokens && usage.contextTokens > 0)
128
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
129
112
  if (usage.turns)
130
113
  parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
131
- if (typeof durationMs === "number") parts.push(formatDuration(durationMs));
114
+ if (usage.contextTokens && usage.contextTokens > 0)
115
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
132
116
  if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
133
117
  return parts.join(" · ");
134
118
  }
@@ -243,12 +227,20 @@ export function renderSubagentResult(
243
227
  const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
244
228
  const title = formatSubagentTitle(r.agent, r.instanceName, theme);
245
229
  const bodyText = stripOutcomeLineForResultUi(bodyOverride ?? finalOutput);
246
- const usageStr = formatResultFooter(r.usage, r.model, r.durationMs);
230
+ const toolCount = r.progress?.toolCalls?.length ?? 0;
231
+ const toolLabel = `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`;
232
+ const ctxPercent = formatContextPercent({
233
+ contextTokens: r.usage.contextTokens,
234
+ contextWindowTokens: r.usage.contextWindowTokens,
235
+ } as SubagentProgressState);
236
+ const metadata = `${toolLabel} · ${ctxPercent} ctx · ${formatElapsed(r.durationMs ?? 0)}`;
237
+ const usageStr = formatResultFooter(r.usage, r.model);
247
238
  return renderStatusCard(
248
239
  {
249
240
  status: resultStatus,
250
241
  title,
251
242
  variant: "full",
243
+ metadata,
252
244
  body: bodyText,
253
245
  footer: usageStr,
254
246
  },
@@ -7,8 +7,8 @@ import {
7
7
  normalizeTerminalSentence,
8
8
  TOOL_PREVIEW_MAX_CHARS,
9
9
  truncateText,
10
- } from "./normalize.js";
11
- import type { SubagentDetails } from "./types.js";
10
+ } from "../output/normalize.js";
11
+ import type { SubagentDetails } from "../shared/types.js";
12
12
 
13
13
  export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
14
14
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  import type { Component } from "@earendil-works/pi-tui";
20
20
  import { Box, Text } from "@earendil-works/pi-tui";
21
+ import { formatSubagentTitle, type SubagentTheme } from "../output/ui.js";
21
22
  import {
22
23
  formatHeaderStats,
23
24
  getProgressState,
@@ -28,9 +29,8 @@ import {
28
29
  type SubagentProgressState,
29
30
  type ThemeBg,
30
31
  } from "./progress-state.js";
31
- import { formatSubagentTitle, type SubagentTheme } from "./ui.js";
32
32
 
33
- export { makeToolPreview } from "./normalize.js";
33
+ export { makeToolPreview } from "../output/normalize.js";
34
34
  export {
35
35
  cancelProgressState,
36
36
  clearProgressState,
@@ -1,19 +1,19 @@
1
- import { TOOL_RESULT_FAILED_MESSAGE } from "./process.js";
2
- import {
3
- extractProgressFromDetails,
4
- getProgressState,
5
- patchProgressState,
6
- } from "./progress.js";
1
+ import { TOOL_RESULT_FAILED_MESSAGE } from "../child/process.js";
7
2
  import {
8
3
  formatSubagentResultForParent,
9
4
  summarizeFeedbackUiFinalOutput,
10
- } from "./summary.js";
5
+ } from "../output/summary.js";
11
6
  import type {
12
7
  SingleResult,
13
8
  SubagentDetails,
14
9
  SubagentToolResult,
15
- } from "./types.js";
16
- import { detectMessageError } from "./utils.js";
10
+ } from "../shared/types.js";
11
+ import { detectMessageError } from "../shared/utils.js";
12
+ import {
13
+ extractProgressFromDetails,
14
+ getProgressState,
15
+ patchProgressState,
16
+ } from "./progress.js";
17
17
 
18
18
  export function hasSubagentFailed(result: SingleResult): boolean {
19
19
  return (
@@ -0,0 +1,117 @@
1
+ const DEFAULT_ADJECTIVES = [
2
+ "able",
3
+ "agile",
4
+ "alert",
5
+ "amber",
6
+ "ample",
7
+ "apt",
8
+ "arctic",
9
+ "avid",
10
+ "bold",
11
+ "brave",
12
+ "bright",
13
+ "brisk",
14
+ "calm",
15
+ "clever",
16
+ "cosmic",
17
+ "crisp",
18
+ "daring",
19
+ "dawn",
20
+ "eager",
21
+ "early",
22
+ "fair",
23
+ "fast",
24
+ "fierce",
25
+ "fine",
26
+ "fresh",
27
+ "gentle",
28
+ "golden",
29
+ "grand",
30
+ "happy",
31
+ "honest",
32
+ "jolly",
33
+ "keen",
34
+ "kind",
35
+ "lively",
36
+ "lucky",
37
+ "merry",
38
+ "mighty",
39
+ "nimble",
40
+ "noble",
41
+ "novel",
42
+ "patient",
43
+ "proud",
44
+ "quick",
45
+ "quiet",
46
+ "rapid",
47
+ "ready",
48
+ "sharp",
49
+ "smart",
50
+ "solid",
51
+ "steady",
52
+ "swift",
53
+ "tidy",
54
+ "vivid",
55
+ "warm",
56
+ "wise",
57
+ ] as const;
58
+
59
+ const DEFAULT_NOUNS = [
60
+ "badger",
61
+ "beacon",
62
+ "bison",
63
+ "brook",
64
+ "cedar",
65
+ "comet",
66
+ "coral",
67
+ "coyote",
68
+ "crane",
69
+ "dolphin",
70
+ "eagle",
71
+ "ember",
72
+ "falcon",
73
+ "finch",
74
+ "forest",
75
+ "fox",
76
+ "gecko",
77
+ "glade",
78
+ "harbor",
79
+ "hawk",
80
+ "heron",
81
+ "island",
82
+ "jaguar",
83
+ "koala",
84
+ "lagoon",
85
+ "lemur",
86
+ "lynx",
87
+ "maple",
88
+ "meadow",
89
+ "otter",
90
+ "panda",
91
+ "panther",
92
+ "pelican",
93
+ "phoenix",
94
+ "puma",
95
+ "raven",
96
+ "reef",
97
+ "river",
98
+ "salmon",
99
+ "sparrow",
100
+ "summit",
101
+ "tiger",
102
+ "valley",
103
+ "violet",
104
+ "walrus",
105
+ "willow",
106
+ "wolf",
107
+ "wren",
108
+ "yak",
109
+ "zephyr",
110
+ ] as const;
111
+
112
+ export function generateSubagentInstanceName(): string {
113
+ const adj =
114
+ DEFAULT_ADJECTIVES[Math.floor(Math.random() * DEFAULT_ADJECTIVES.length)];
115
+ const noun = DEFAULT_NOUNS[Math.floor(Math.random() * DEFAULT_NOUNS.length)];
116
+ return `${adj}-${noun}`;
117
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "@earendil-works/pi-ai";
2
- import type { AgentScope } from "./agents.js";
3
- import type { TerminationMetadata } from "./termination.js";
2
+ import type { AgentScope } from "../agent/agents.js";
3
+ import type { TerminationMetadata } from "../child/termination.js";
4
4
 
5
5
  export interface UsageStats {
6
6
  input: number;
@@ -40,6 +40,7 @@ export interface SingleResult {
40
40
  progress?: StreamingProgress;
41
41
  messages?: Message[];
42
42
  termination?: TerminationMetadata;
43
+ thinkingWarning?: string;
43
44
  }
44
45
 
45
46
  export interface SubagentDetails {
@@ -5,7 +5,6 @@ import type { Message } from "@earendil-works/pi-ai";
5
5
  import {
6
6
  DefaultResourceLoader,
7
7
  getAgentDir,
8
- withFileMutationQueue,
9
8
  } from "@earendil-works/pi-coding-agent";
10
9
 
11
10
  export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
@@ -66,11 +65,10 @@ export async function writePromptToTempFile(
66
65
  );
67
66
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
68
67
  const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
69
- await withFileMutationQueue(filePath, async () => {
70
- await fs.promises.writeFile(filePath, prompt, {
71
- encoding: "utf-8",
72
- mode: 0o600,
73
- });
68
+ // mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
69
+ await fs.promises.writeFile(filePath, prompt, {
70
+ encoding: "utf-8",
71
+ mode: 0o600,
74
72
  });
75
73
  return { dir: tmpDir, filePath };
76
74
  }
@@ -1,164 +0,0 @@
1
- const DEFAULT_ADJECTIVES = [
2
- "able",
3
- "agile",
4
- "alert",
5
- "amber",
6
- "ample",
7
- "apt",
8
- "arctic",
9
- "avid",
10
- "bold",
11
- "brave",
12
- "bright",
13
- "brisk",
14
- "calm",
15
- "clever",
16
- "cosmic",
17
- "crisp",
18
- "daring",
19
- "dawn",
20
- "eager",
21
- "early",
22
- "fair",
23
- "fast",
24
- "fierce",
25
- "fine",
26
- "fresh",
27
- "gentle",
28
- "golden",
29
- "grand",
30
- "happy",
31
- "honest",
32
- "jolly",
33
- "keen",
34
- "kind",
35
- "lively",
36
- "lucky",
37
- "merry",
38
- "mighty",
39
- "nimble",
40
- "noble",
41
- "novel",
42
- "patient",
43
- "proud",
44
- "quick",
45
- "quiet",
46
- "rapid",
47
- "ready",
48
- "sharp",
49
- "smart",
50
- "solid",
51
- "steady",
52
- "swift",
53
- "tidy",
54
- "vivid",
55
- "warm",
56
- "wise",
57
- ] as const;
58
-
59
- const DEFAULT_NOUNS = [
60
- "badger",
61
- "beacon",
62
- "bison",
63
- "brook",
64
- "cedar",
65
- "comet",
66
- "coral",
67
- "coyote",
68
- "crane",
69
- "dolphin",
70
- "eagle",
71
- "ember",
72
- "falcon",
73
- "finch",
74
- "forest",
75
- "fox",
76
- "gecko",
77
- "glade",
78
- "harbor",
79
- "hawk",
80
- "heron",
81
- "island",
82
- "jaguar",
83
- "koala",
84
- "lagoon",
85
- "lemur",
86
- "lynx",
87
- "maple",
88
- "meadow",
89
- "otter",
90
- "panda",
91
- "panther",
92
- "pelican",
93
- "phoenix",
94
- "puma",
95
- "raven",
96
- "reef",
97
- "river",
98
- "salmon",
99
- "sparrow",
100
- "summit",
101
- "tiger",
102
- "valley",
103
- "violet",
104
- "walrus",
105
- "willow",
106
- "wolf",
107
- "wren",
108
- "yak",
109
- "zephyr",
110
- ] as const;
111
-
112
- const usedInstanceNames = new Set<string>();
113
-
114
- let adjectives: readonly string[] = DEFAULT_ADJECTIVES;
115
- let nouns: readonly string[] = DEFAULT_NOUNS;
116
- let randomSource: () => number = Math.random;
117
-
118
- function normalizeRandomIndex(limit: number): number {
119
- const value = randomSource();
120
- if (!Number.isFinite(value)) return 0;
121
- return Math.min(limit - 1, Math.max(0, Math.floor(value * limit)));
122
- }
123
-
124
- function nameAt(index: number): string {
125
- const adjective = adjectives[Math.floor(index / nouns.length)];
126
- const noun = nouns[index % nouns.length];
127
- return `${adjective}-${noun}`;
128
- }
129
-
130
- export function generateSubagentInstanceName(): string {
131
- const capacity = adjectives.length * nouns.length;
132
- if (usedInstanceNames.size >= capacity) {
133
- throw new Error(
134
- "No unused subagent instance names remain for this session.",
135
- );
136
- }
137
- const start = normalizeRandomIndex(capacity);
138
- for (let offset = 0; offset < capacity; offset += 1) {
139
- const candidate = nameAt((start + offset) % capacity);
140
- if (!usedInstanceNames.has(candidate)) {
141
- usedInstanceNames.add(candidate);
142
- return candidate;
143
- }
144
- }
145
- throw new Error("No unused subagent instance names remain for this session.");
146
- }
147
-
148
- export function resetSubagentInstanceNamesForTest() {
149
- usedInstanceNames.clear();
150
- adjectives = DEFAULT_ADJECTIVES;
151
- nouns = DEFAULT_NOUNS;
152
- randomSource = Math.random;
153
- }
154
-
155
- export function configureSubagentInstanceNamesForTest(options: {
156
- adjectives?: readonly string[];
157
- nouns?: readonly string[];
158
- randomSource?: () => number;
159
- }) {
160
- usedInstanceNames.clear();
161
- adjectives = options.adjectives ?? DEFAULT_ADJECTIVES;
162
- nouns = options.nouns ?? DEFAULT_NOUNS;
163
- randomSource = options.randomSource ?? Math.random;
164
- }
File without changes
File without changes