pi-subagents 0.3.0 → 0.3.1
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/CHANGELOG.md +21 -0
- package/agents.ts +0 -10
- package/async-execution.ts +261 -0
- package/chain-execution.ts +436 -0
- package/execution.ts +352 -0
- package/formatters.ts +111 -0
- package/index.ts +72 -1614
- package/package.json +2 -2
- package/render.ts +283 -0
- package/schemas.ts +90 -0
- package/settings.ts +2 -166
- package/types.ts +166 -0
- package/utils.ts +287 -0
package/types.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the subagent extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Basic Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
1
11
|
export interface MaxOutputConfig {
|
|
2
12
|
bytes?: number;
|
|
3
13
|
lines?: number;
|
|
@@ -11,6 +21,25 @@ export interface TruncationResult {
|
|
|
11
21
|
artifactPath?: string;
|
|
12
22
|
}
|
|
13
23
|
|
|
24
|
+
export interface Usage {
|
|
25
|
+
input: number;
|
|
26
|
+
output: number;
|
|
27
|
+
cacheRead: number;
|
|
28
|
+
cacheWrite: number;
|
|
29
|
+
cost: number;
|
|
30
|
+
turns: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TokenUsage {
|
|
34
|
+
input: number;
|
|
35
|
+
output: number;
|
|
36
|
+
total: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Progress Tracking
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
14
43
|
export interface AgentProgress {
|
|
15
44
|
index: number;
|
|
16
45
|
agent: string;
|
|
@@ -33,6 +62,52 @@ export interface ProgressSummary {
|
|
|
33
62
|
durationMs: number;
|
|
34
63
|
}
|
|
35
64
|
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Results
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export interface SingleResult {
|
|
70
|
+
agent: string;
|
|
71
|
+
task: string;
|
|
72
|
+
exitCode: number;
|
|
73
|
+
messages: Message[];
|
|
74
|
+
usage: Usage;
|
|
75
|
+
model?: string;
|
|
76
|
+
error?: string;
|
|
77
|
+
sessionFile?: string;
|
|
78
|
+
progress?: AgentProgress;
|
|
79
|
+
progressSummary?: ProgressSummary;
|
|
80
|
+
artifactPaths?: ArtifactPaths;
|
|
81
|
+
truncation?: TruncationResult;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface Details {
|
|
85
|
+
mode: "single" | "parallel" | "chain";
|
|
86
|
+
results: SingleResult[];
|
|
87
|
+
asyncId?: string;
|
|
88
|
+
asyncDir?: string;
|
|
89
|
+
progress?: AgentProgress[];
|
|
90
|
+
progressSummary?: ProgressSummary;
|
|
91
|
+
artifacts?: {
|
|
92
|
+
dir: string;
|
|
93
|
+
files: ArtifactPaths[];
|
|
94
|
+
};
|
|
95
|
+
truncation?: {
|
|
96
|
+
truncated: boolean;
|
|
97
|
+
originalBytes?: number;
|
|
98
|
+
originalLines?: number;
|
|
99
|
+
artifactPath?: string;
|
|
100
|
+
};
|
|
101
|
+
// Chain metadata for observability
|
|
102
|
+
chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
|
|
103
|
+
totalSteps?: number; // Total steps in chain
|
|
104
|
+
currentStepIndex?: number; // 0-indexed current step (for running chains)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Artifacts
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
36
111
|
export interface ArtifactPaths {
|
|
37
112
|
inputPath: string;
|
|
38
113
|
outputPath: string;
|
|
@@ -49,6 +124,85 @@ export interface ArtifactConfig {
|
|
|
49
124
|
cleanupDays: number;
|
|
50
125
|
}
|
|
51
126
|
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Async Execution
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
export interface AsyncStatus {
|
|
132
|
+
runId: string;
|
|
133
|
+
mode: "single" | "chain";
|
|
134
|
+
state: "queued" | "running" | "complete" | "failed";
|
|
135
|
+
startedAt: number;
|
|
136
|
+
endedAt?: number;
|
|
137
|
+
lastUpdate?: number;
|
|
138
|
+
currentStep?: number;
|
|
139
|
+
steps?: Array<{ agent: string; status: string; durationMs?: number; tokens?: TokenUsage }>;
|
|
140
|
+
sessionDir?: string;
|
|
141
|
+
outputFile?: string;
|
|
142
|
+
totalTokens?: TokenUsage;
|
|
143
|
+
sessionFile?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface AsyncJobState {
|
|
147
|
+
asyncId: string;
|
|
148
|
+
asyncDir: string;
|
|
149
|
+
status: "queued" | "running" | "complete" | "failed";
|
|
150
|
+
mode?: "single" | "chain";
|
|
151
|
+
agents?: string[];
|
|
152
|
+
currentStep?: number;
|
|
153
|
+
stepsTotal?: number;
|
|
154
|
+
startedAt?: number;
|
|
155
|
+
updatedAt?: number;
|
|
156
|
+
sessionDir?: string;
|
|
157
|
+
outputFile?: string;
|
|
158
|
+
totalTokens?: TokenUsage;
|
|
159
|
+
sessionFile?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Display
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
export type DisplayItem =
|
|
167
|
+
| { type: "text"; text: string }
|
|
168
|
+
| { type: "tool"; name: string; args: Record<string, unknown> };
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Error Handling
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
export interface ErrorInfo {
|
|
175
|
+
hasError: boolean;
|
|
176
|
+
exitCode?: number;
|
|
177
|
+
errorType?: string;
|
|
178
|
+
details?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Execution Options
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
export interface RunSyncOptions {
|
|
186
|
+
cwd?: string;
|
|
187
|
+
signal?: AbortSignal;
|
|
188
|
+
onUpdate?: (r: import("@mariozechner/pi-agent-core").AgentToolResult<Details>) => void;
|
|
189
|
+
maxOutput?: MaxOutputConfig;
|
|
190
|
+
artifactsDir?: string;
|
|
191
|
+
artifactConfig?: ArtifactConfig;
|
|
192
|
+
runId: string;
|
|
193
|
+
index?: number;
|
|
194
|
+
sessionDir?: string;
|
|
195
|
+
share?: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ExtensionConfig {
|
|
199
|
+
asyncByDefault?: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Constants
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
52
206
|
export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
|
|
53
207
|
bytes: 200 * 1024,
|
|
54
208
|
lines: 5000,
|
|
@@ -63,6 +217,18 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
|
63
217
|
cleanupDays: 7,
|
|
64
218
|
};
|
|
65
219
|
|
|
220
|
+
export const MAX_PARALLEL = 8;
|
|
221
|
+
export const MAX_CONCURRENCY = 4;
|
|
222
|
+
export const RESULTS_DIR = "/tmp/pi-async-subagent-results";
|
|
223
|
+
export const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
|
|
224
|
+
export const WIDGET_KEY = "subagent-async";
|
|
225
|
+
export const POLL_INTERVAL_MS = 1000;
|
|
226
|
+
export const MAX_WIDGET_JOBS = 4;
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Utility Functions
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
66
232
|
export function formatBytes(bytes: number): string {
|
|
67
233
|
if (bytes < 1024) return `${bytes}B`;
|
|
68
234
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
package/utils.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General utility functions for the subagent extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { AsyncStatus, DisplayItem, ErrorInfo } from "./types.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// File System Utilities
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read async job status from disk
|
|
17
|
+
*/
|
|
18
|
+
export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
19
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
20
|
+
if (!fs.existsSync(statusPath)) return null;
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(statusPath, "utf-8");
|
|
23
|
+
return JSON.parse(content) as AsyncStatus;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the last N lines from an output file
|
|
31
|
+
*/
|
|
32
|
+
export function getOutputTail(outputFile: string | undefined, maxLines: number = 3): string[] {
|
|
33
|
+
if (!outputFile || !fs.existsSync(outputFile)) return [];
|
|
34
|
+
let fd: number | null = null;
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs.statSync(outputFile);
|
|
37
|
+
if (stat.size === 0) return [];
|
|
38
|
+
const tailBytes = 4096;
|
|
39
|
+
const start = Math.max(0, stat.size - tailBytes);
|
|
40
|
+
fd = fs.openSync(outputFile, "r");
|
|
41
|
+
const buffer = Buffer.alloc(Math.min(tailBytes, stat.size));
|
|
42
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
43
|
+
const content = buffer.toString("utf-8");
|
|
44
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
45
|
+
return lines.slice(-maxLines).map((l) => l.slice(0, 120) + (l.length > 120 ? "..." : ""));
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
} finally {
|
|
49
|
+
if (fd !== null) {
|
|
50
|
+
try {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get human-readable last activity time for a file
|
|
59
|
+
*/
|
|
60
|
+
export function getLastActivity(outputFile: string | undefined): string {
|
|
61
|
+
if (!outputFile || !fs.existsSync(outputFile)) return "";
|
|
62
|
+
try {
|
|
63
|
+
const stat = fs.statSync(outputFile);
|
|
64
|
+
const ago = Date.now() - stat.mtimeMs;
|
|
65
|
+
if (ago < 1000) return "active now";
|
|
66
|
+
if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
|
|
67
|
+
return `active ${Math.floor(ago / 60000)}m ago`;
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find a file/directory by prefix in a directory
|
|
75
|
+
*/
|
|
76
|
+
export function findByPrefix(dir: string, prefix: string, suffix?: string): string | null {
|
|
77
|
+
if (!fs.existsSync(dir)) return null;
|
|
78
|
+
const entries = fs.readdirSync(dir).filter((entry) => entry.startsWith(prefix));
|
|
79
|
+
if (suffix) {
|
|
80
|
+
const withSuffix = entries.filter((entry) => entry.endsWith(suffix));
|
|
81
|
+
if (withSuffix.length > 0) return path.join(dir, withSuffix.sort()[0]);
|
|
82
|
+
}
|
|
83
|
+
if (entries.length === 0) return null;
|
|
84
|
+
return path.join(dir, entries.sort()[0]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find the latest session file in a directory
|
|
89
|
+
*/
|
|
90
|
+
export function findLatestSessionFile(sessionDir: string): string | null {
|
|
91
|
+
if (!fs.existsSync(sessionDir)) return null;
|
|
92
|
+
const files = fs.readdirSync(sessionDir)
|
|
93
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
94
|
+
.map((f) => ({
|
|
95
|
+
name: f,
|
|
96
|
+
path: path.join(sessionDir, f),
|
|
97
|
+
mtime: fs.statSync(path.join(sessionDir, f)).mtimeMs,
|
|
98
|
+
}))
|
|
99
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
100
|
+
return files.length > 0 ? files[0].path : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write a prompt to a temporary file
|
|
105
|
+
*/
|
|
106
|
+
export function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
|
|
107
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
108
|
+
const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
|
|
109
|
+
fs.writeFileSync(p, prompt, { mode: 0o600 });
|
|
110
|
+
return { dir, path: p };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Message Parsing Utilities
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the final text output from a list of messages
|
|
119
|
+
*/
|
|
120
|
+
export function getFinalOutput(messages: Message[]): string {
|
|
121
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
122
|
+
const msg = messages[i];
|
|
123
|
+
if (msg.role === "assistant") {
|
|
124
|
+
for (const part of msg.content) {
|
|
125
|
+
if (part.type === "text") return part.text;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract display items (text and tool calls) from messages
|
|
134
|
+
*/
|
|
135
|
+
export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
136
|
+
const items: DisplayItem[] = [];
|
|
137
|
+
for (const msg of messages) {
|
|
138
|
+
if (msg.role === "assistant") {
|
|
139
|
+
for (const part of msg.content) {
|
|
140
|
+
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
141
|
+
else if (part.type === "toolCall") items.push({ type: "tool", name: part.name, args: part.arguments });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return items;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detect errors in subagent execution from messages
|
|
150
|
+
*/
|
|
151
|
+
export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
152
|
+
for (const msg of messages) {
|
|
153
|
+
if (msg.role === "toolResult" && (msg as any).isError) {
|
|
154
|
+
const text = msg.content.find((c) => c.type === "text");
|
|
155
|
+
const details = text && "text" in text ? text.text : undefined;
|
|
156
|
+
const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
157
|
+
return {
|
|
158
|
+
hasError: true,
|
|
159
|
+
exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
|
|
160
|
+
errorType: (msg as any).toolName || "tool",
|
|
161
|
+
details: details?.slice(0, 200),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const msg of messages) {
|
|
167
|
+
if (msg.role !== "toolResult") continue;
|
|
168
|
+
const toolName = (msg as any).toolName;
|
|
169
|
+
if (toolName !== "bash") continue;
|
|
170
|
+
|
|
171
|
+
const text = msg.content.find((c) => c.type === "text");
|
|
172
|
+
if (!text || !("text" in text)) continue;
|
|
173
|
+
const output = text.text;
|
|
174
|
+
|
|
175
|
+
const exitMatch = output.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
176
|
+
if (exitMatch) {
|
|
177
|
+
const code = parseInt(exitMatch[1], 10);
|
|
178
|
+
if (code !== 0) {
|
|
179
|
+
return { hasError: true, exitCode: code, errorType: "bash", details: output.slice(0, 200) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const errorPatterns = [
|
|
184
|
+
/command not found/i,
|
|
185
|
+
/permission denied/i,
|
|
186
|
+
/no such file or directory/i,
|
|
187
|
+
/segmentation fault/i,
|
|
188
|
+
/killed|terminated/i,
|
|
189
|
+
/out of memory/i,
|
|
190
|
+
/connection refused/i,
|
|
191
|
+
/timeout/i,
|
|
192
|
+
];
|
|
193
|
+
for (const pattern of errorPatterns) {
|
|
194
|
+
if (pattern.test(output)) {
|
|
195
|
+
return { hasError: true, exitCode: 1, errorType: "bash", details: output.slice(0, 200) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { hasError: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract a preview of tool arguments for display
|
|
205
|
+
*/
|
|
206
|
+
export function extractToolArgsPreview(args: Record<string, unknown>): string {
|
|
207
|
+
// Handle MCP tool calls - show server/tool info
|
|
208
|
+
if (args.tool && typeof args.tool === "string") {
|
|
209
|
+
const server = args.server && typeof args.server === "string" ? `${args.server}/` : "";
|
|
210
|
+
const toolArgs = args.args && typeof args.args === "string" ? ` ${args.args.slice(0, 40)}` : "";
|
|
211
|
+
return `${server}${args.tool}${toolArgs}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task", "describe", "search"];
|
|
215
|
+
for (const key of previewKeys) {
|
|
216
|
+
if (args[key] && typeof args[key] === "string") {
|
|
217
|
+
const value = args[key] as string;
|
|
218
|
+
return value.length > 60 ? `${value.slice(0, 57)}...` : value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Fallback: show first string value found
|
|
223
|
+
for (const [key, value] of Object.entries(args)) {
|
|
224
|
+
if (typeof value === "string" && value.length > 0) {
|
|
225
|
+
const preview = value.length > 50 ? `${value.slice(0, 47)}...` : value;
|
|
226
|
+
return `${key}=${preview}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Extract text content from various message content formats
|
|
234
|
+
*/
|
|
235
|
+
export function extractTextFromContent(content: unknown): string {
|
|
236
|
+
if (!content) return "";
|
|
237
|
+
// Handle string content directly
|
|
238
|
+
if (typeof content === "string") return content;
|
|
239
|
+
// Handle array content
|
|
240
|
+
if (!Array.isArray(content)) return "";
|
|
241
|
+
const texts: string[] = [];
|
|
242
|
+
for (const part of content) {
|
|
243
|
+
if (part && typeof part === "object") {
|
|
244
|
+
// Handle { type: "text", text: "..." }
|
|
245
|
+
if ("type" in part && part.type === "text" && "text" in part) {
|
|
246
|
+
texts.push(String(part.text));
|
|
247
|
+
}
|
|
248
|
+
// Handle { type: "tool_result", content: "..." }
|
|
249
|
+
else if ("type" in part && part.type === "tool_result" && "content" in part) {
|
|
250
|
+
const inner = extractTextFromContent(part.content);
|
|
251
|
+
if (inner) texts.push(inner);
|
|
252
|
+
}
|
|
253
|
+
// Handle { text: "..." } without type
|
|
254
|
+
else if ("text" in part) {
|
|
255
|
+
texts.push(String(part.text));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return texts.join("\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Concurrency Utilities
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Map over items with limited concurrency
|
|
268
|
+
*/
|
|
269
|
+
export async function mapConcurrent<T, R>(
|
|
270
|
+
items: T[],
|
|
271
|
+
limit: number,
|
|
272
|
+
fn: (item: T, i: number) => Promise<R>,
|
|
273
|
+
): Promise<R[]> {
|
|
274
|
+
const results: R[] = new Array(items.length);
|
|
275
|
+
let next = 0;
|
|
276
|
+
|
|
277
|
+
async function worker(): Promise<void> {
|
|
278
|
+
while (next < items.length) {
|
|
279
|
+
const i = next++;
|
|
280
|
+
results[i] = await fn(items[i], i);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
285
|
+
await Promise.all(workers);
|
|
286
|
+
return results;
|
|
287
|
+
}
|