@kapeta/local-cluster-service 0.49.0 → 0.51.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.
@@ -15,7 +15,7 @@ import {
15
15
  import { BlockDefinition } from '@kapeta/schemas';
16
16
  import { codeGeneratorManager } from '../codeGeneratorManager';
17
17
  import { STORM_ID, stormClient } from './stormClient';
18
- import { StormEvent, StormEventFile } from './events';
18
+ import { StormEvent, StormEventFileContent, StormEventFileLogical } from './events';
19
19
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
20
20
  import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
21
21
  import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
@@ -27,6 +27,54 @@ import Path from 'path';
27
27
 
28
28
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
29
29
 
30
+ const SIMULATED_DELAY = 1000;
31
+
32
+ class SimulatedFileDelay {
33
+ private readonly file: StormEventFileContent;
34
+ public readonly stream;
35
+ constructor(file: StormEventFileContent, stream: StormStream) {
36
+ this.file = file;
37
+ this.stream = stream;
38
+ }
39
+
40
+ public async start() {
41
+ const commonPayload = {
42
+ filename: this.file.payload.filename,
43
+ path: this.file.payload.path,
44
+ blockName: this.file.payload.blockName,
45
+ blockRef: this.file.payload.blockRef,
46
+ instanceId: this.file.payload.instanceId,
47
+ };
48
+ this.stream.emit('data', {
49
+ type: 'FILE_START',
50
+ created: Date.now(),
51
+ reason: 'File start',
52
+ payload: commonPayload,
53
+ } satisfies StormEventFileLogical);
54
+
55
+ const lines = this.file.payload.content.split('\n');
56
+ const delayPerLine = SIMULATED_DELAY / lines.length;
57
+ for (const line of lines) {
58
+ await new Promise<void>((resolve) => {
59
+ setTimeout(() => {
60
+ this.stream.emit('data', {
61
+ type: 'FILE_CHUNK',
62
+ created: Date.now(),
63
+ reason: 'File chunk',
64
+ payload: {
65
+ ...commonPayload,
66
+ content: line,
67
+ },
68
+ } satisfies StormEventFileContent);
69
+ resolve();
70
+ }, delayPerLine);
71
+ });
72
+ }
73
+
74
+ this.stream.emit('data', this.file);
75
+ }
76
+ }
77
+
30
78
  export class StormCodegen {
31
79
  private readonly userPrompt: string;
32
80
  private readonly blocks: BlockDefinitionInfo[];
@@ -51,54 +99,158 @@ export class StormCodegen {
51
99
  this.out.end();
52
100
  }
53
101
 
102
+ isAborted() {
103
+ return this.out.isAborted();
104
+ }
105
+
54
106
  public getStream() {
55
107
  return this.out;
56
108
  }
57
109
 
58
- private handleTemplateFileOutput(blockUri: KapetaURI, aiName: string, template: StormFileInfo, data: StormEvent) {
110
+ private handleTemplateFileOutput(
111
+ blockUri: KapetaURI,
112
+ aiName: string,
113
+ template: StormFileInfo,
114
+ data: StormEvent
115
+ ): void {
116
+ if (this.handleFileEvents(blockUri, aiName, data)) {
117
+ return;
118
+ }
119
+
59
120
  switch (data.type) {
60
- case 'FILE':
121
+ case 'FILE_DONE':
61
122
  template.filename = data.payload.filename;
62
123
  template.content = data.payload.content;
63
- return this.handleFileOutput(blockUri, aiName, data);
124
+ this.handleFileDoneOutput(blockUri, aiName, data);
125
+ break;
64
126
  }
65
127
  }
66
128
 
67
- private handleUiOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
129
+ private handleUiOutput(blockUri: KapetaURI, blockName: string, data: StormEvent) {
130
+ const blockRef = blockUri.toNormalizedString();
131
+ const instanceId = StormEventParser.toInstanceIdFromRef(blockRef);
132
+
133
+ if (this.handleFileEvents(blockUri, blockName, data)) {
134
+ return;
135
+ }
136
+
68
137
  switch (data.type) {
69
138
  case 'SCREEN':
70
- const ref = blockUri.toNormalizedString();
71
139
  this.out.emit('data', {
72
140
  type: 'SCREEN',
73
141
  reason: data.reason,
74
142
  created: Date.now(),
75
143
  payload: {
76
144
  ...data.payload,
77
- blockName: aiName,
78
- blockRef: ref,
79
- instanceId: StormEventParser.toInstanceIdFromRef(ref),
145
+ blockName,
146
+ blockRef,
147
+ instanceId,
148
+ },
149
+ });
150
+ break;
151
+ case 'FILE_START':
152
+ case 'FILE_CHUNK_RESET':
153
+ this.out.emit('data', {
154
+ ...data,
155
+ payload: {
156
+ ...data.payload,
157
+ blockName,
158
+ blockRef,
159
+ instanceId,
160
+ },
161
+ });
162
+ break;
163
+ case 'FILE_CHUNK':
164
+ this.out.emit('data', {
165
+ ...data,
166
+ payload: {
167
+ ...data.payload,
168
+ blockName,
169
+ blockRef,
170
+ instanceId,
171
+ },
172
+ });
173
+ break;
174
+ case 'FILE_STATE':
175
+ this.out.emit('data', {
176
+ ...data,
177
+ payload: {
178
+ ...data.payload,
179
+ blockName,
180
+ blockRef,
181
+ instanceId,
182
+ },
183
+ });
184
+ break;
185
+ case 'FILE_DONE':
186
+ this.handleFileDoneOutput(blockUri, blockName, data);
187
+ break;
188
+ }
189
+ }
190
+
191
+ private handleFileEvents(blockUri: KapetaURI, blockName: string, data: StormEvent) {
192
+ const blockRef = blockUri.toNormalizedString();
193
+ const instanceId = StormEventParser.toInstanceIdFromRef(blockRef);
194
+ const basePath = this.getBasePath(blockUri.fullName);
195
+ switch (data.type) {
196
+ case 'FILE_START':
197
+ case 'FILE_CHUNK_RESET':
198
+ this.out.emit('data', {
199
+ ...data,
200
+ payload: {
201
+ ...data.payload,
202
+ path: join(basePath, data.payload.filename),
203
+ blockName,
204
+ blockRef,
205
+ instanceId,
206
+ },
207
+ });
208
+ return true;
209
+ case 'FILE_CHUNK':
210
+ this.out.emit('data', {
211
+ ...data,
212
+ payload: {
213
+ ...data.payload,
214
+ path: join(basePath, data.payload.filename),
215
+ blockName,
216
+ blockRef,
217
+ instanceId,
218
+ },
219
+ });
220
+ return true;
221
+ case 'FILE_STATE':
222
+ this.out.emit('data', {
223
+ ...data,
224
+ payload: {
225
+ ...data.payload,
226
+ path: join(basePath, data.payload.filename),
227
+ blockName,
228
+ blockRef,
229
+ instanceId,
80
230
  },
81
231
  });
82
- case 'FILE':
83
- return this.handleFileOutput(blockUri, aiName, data);
232
+ return true;
84
233
  }
234
+
235
+ return false;
85
236
  }
86
237
 
87
- private handleFileOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
238
+ private handleFileDoneOutput(blockUri: KapetaURI, aiName: string, data: StormEvent) {
88
239
  switch (data.type) {
89
- case 'FILE':
240
+ case 'FILE_DONE':
90
241
  const ref = blockUri.toNormalizedString();
91
242
  this.emitFile(blockUri, aiName, data.payload.filename, data.payload.content, data.reason);
92
243
  return {
93
- type: 'FILE',
244
+ type: 'FILE_DONE',
94
245
  created: Date.now(),
95
246
  payload: {
96
247
  filename: data.payload.filename,
248
+ path: join(this.getBasePath(blockUri.fullName), data.payload.filename),
97
249
  content: data.payload.content,
98
250
  blockRef: ref,
99
251
  instanceId: StormEventParser.toInstanceIdFromRef(ref),
100
252
  },
101
- } as StormEventFile;
253
+ } as StormEventFileContent;
102
254
  }
103
255
  }
104
256
 
@@ -110,6 +262,9 @@ export class StormCodegen {
110
262
  * Generates the code for a block and sends it to the AI
111
263
  */
112
264
  private async processBlockCode(block: BlockDefinitionInfo) {
265
+ if (this.isAborted()) {
266
+ return;
267
+ }
113
268
  // Generate the code for the block using the standard codegen templates
114
269
  const generatedResult = await this.generateBlock(block.content);
115
270
  if (!generatedResult) {
@@ -119,7 +274,11 @@ export class StormCodegen {
119
274
  const allFiles = this.toStormFiles(generatedResult);
120
275
 
121
276
  // Send all the non-ai files to the stream
122
- this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
277
+ await this.emitStaticFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
278
+
279
+ if (this.isAborted()) {
280
+ return;
281
+ }
123
282
 
124
283
  const relevantFiles: StormFileInfo[] = allFiles.filter(
125
284
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -138,9 +297,17 @@ export class StormCodegen {
138
297
  this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
139
298
  });
140
299
 
300
+ this.out.on('aborted', () => {
301
+ uiStream.abort();
302
+ });
303
+
141
304
  await uiStream.waitForDone();
142
305
  }
143
306
 
307
+ if (this.isAborted()) {
308
+ return;
309
+ }
310
+
144
311
  // Gather the context files for implementation. These will be all be passed to the AI
145
312
  const contextFiles: StormFileInfo[] = relevantFiles.filter(
146
313
  (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
@@ -160,6 +327,10 @@ export class StormCodegen {
160
327
 
161
328
  const basePath = this.getBasePath(block.content.metadata.name);
162
329
 
330
+ if (this.isAborted()) {
331
+ return;
332
+ }
333
+
163
334
  for (const serviceFile of serviceFiles) {
164
335
  const filePath = join(basePath, serviceFile.filename);
165
336
  await writeFile(filePath, serviceFile.content);
@@ -216,6 +387,10 @@ export class StormCodegen {
216
387
  const errorStream = await stormClient.createErrorClassification(result.error, []);
217
388
  const fixes = new Map<string, Promise<string>>();
218
389
 
390
+ this.out.on('aborted', () => {
391
+ errorStream.abort();
392
+ });
393
+
219
394
  errorStream.on('data', (evt) => {
220
395
  if (evt.type === 'ERROR_CLASSIFIER') {
221
396
  // find the file that caused the error
@@ -232,8 +407,8 @@ export class StormCodegen {
232
407
  const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
233
408
  .map((e) => e.filename)
234
409
  .join('\n')}\n---\n${content}`;
235
- console.log(`trying to fix the code in ${eventFileName}`);
236
- console.debug(`with the fix:\n${fix}`);
410
+ //console.log(`trying to fix the code in ${eventFileName}`);
411
+ //console.debug(`with the fix:\n${fix}`);
237
412
  const code = this.codeFix(fix);
238
413
  fixes.set(join(basePath, eventFileName), code);
239
414
  }
@@ -278,6 +453,9 @@ export class StormCodegen {
278
453
  resolve(evt.payload.content);
279
454
  }
280
455
  });
456
+ this.out.on('aborted', () => {
457
+ fixStream.abort();
458
+ });
281
459
  fixStream.on('error', (err) => {
282
460
  reject(err);
283
461
  });
@@ -288,8 +466,8 @@ export class StormCodegen {
288
466
  /**
289
467
  * Emits the text-based files to the stream
290
468
  */
291
- private emitFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
292
- files.forEach((file) => {
469
+ private async emitStaticFiles(uri: KapetaURI, aiName: string, files: StormFileInfo[]) {
470
+ const promises = files.map((file) => {
293
471
  if (!file.content || typeof file.content !== 'string') {
294
472
  return;
295
473
  }
@@ -306,8 +484,25 @@ export class StormCodegen {
306
484
  return;
307
485
  }
308
486
 
309
- this.emitFile(uri, aiName, file.filename, file.content);
487
+ const basePath = this.getBasePath(uri.fullName);
488
+ const ref = uri.toNormalizedString();
489
+ const fileEvent: StormEventFileContent = {
490
+ type: 'FILE_DONE',
491
+ reason: 'File generated',
492
+ created: Date.now(),
493
+ payload: {
494
+ filename: file.filename,
495
+ path: join(basePath, file.filename),
496
+ content: file.content,
497
+ blockName: aiName,
498
+ blockRef: ref,
499
+ instanceId: StormEventParser.toInstanceIdFromRef(ref),
500
+ },
501
+ };
502
+ return new SimulatedFileDelay(fileEvent, this.out).start();
310
503
  });
504
+
505
+ return Promise.all(promises);
311
506
  }
312
507
 
313
508
  private emitFile(
@@ -320,7 +515,7 @@ export class StormCodegen {
320
515
  const basePath = this.getBasePath(uri.fullName);
321
516
  const ref = uri.toNormalizedString();
322
517
  this.out.emit('data', {
323
- type: 'FILE',
518
+ type: 'FILE_DONE',
324
519
  reason,
325
520
  created: Date.now(),
326
521
  payload: {
@@ -331,7 +526,7 @@ export class StormCodegen {
331
526
  blockRef: ref,
332
527
  instanceId: StormEventParser.toInstanceIdFromRef(ref),
333
528
  },
334
- } satisfies StormEventFile);
529
+ } satisfies StormEventFileContent);
335
530
  }
336
531
 
337
532
  /**
@@ -343,7 +538,7 @@ export class StormCodegen {
343
538
  generator: ImplementationGenerator,
344
539
  templates: StormFileInfo[],
345
540
  contextFiles: StormFileInfo[]
346
- ) {
541
+ ): Promise<void> {
347
542
  const promises = templates.map(async (templateFile) => {
348
543
  const stream = await generator({
349
544
  context: contextFiles,
@@ -351,22 +546,18 @@ export class StormCodegen {
351
546
  prompt: this.userPrompt,
352
547
  });
353
548
 
354
- const files: StormEventFile[] = [];
549
+ this.out.on('aborted', () => {
550
+ stream.abort();
551
+ });
355
552
 
356
553
  stream.on('data', (evt) => {
357
- const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
358
- if (file) {
359
- files.push(file);
360
- }
554
+ this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
361
555
  });
362
556
 
363
557
  await stream.waitForDone();
364
- return files;
365
558
  });
366
559
 
367
- const fileChunks = await Promise.all(promises);
368
-
369
- return fileChunks.flat();
560
+ await Promise.all(promises);
370
561
  }
371
562
 
372
563
  /**
@@ -411,6 +602,9 @@ export class StormCodegen {
411
602
  * Generates the code using codegen for a given block.
412
603
  */
413
604
  private async generateBlock(yamlContent: Definition) {
605
+ if (this.isAborted()) {
606
+ return;
607
+ }
414
608
  if (!yamlContent.spec.target?.kind) {
415
609
  //Not all block types have targets
416
610
  return;
@@ -427,4 +621,8 @@ export class StormCodegen {
427
621
  new CodeWriter(basePath).write(generatedResult);
428
622
  return generatedResult;
429
623
  }
624
+
625
+ abort() {
626
+ this.out.abort();
627
+ }
430
628
  }
@@ -3,7 +3,15 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import { StormBlockInfoFilled, StormBlockType, StormConnection, StormEvent, StormResourceType } from './events';
6
+ import {
7
+ StormBlockInfoFilled,
8
+ StormBlockType,
9
+ StormConnection,
10
+ StormEvent,
11
+ StormEventPhases,
12
+ StormEventPhaseType,
13
+ StormResourceType,
14
+ } from './events';
7
15
  import {
8
16
  BlockDefinition,
9
17
  BlockInstance,
@@ -87,6 +95,24 @@ function prettifyKaplang(source: string) {
87
95
  }
88
96
  }
89
97
 
98
+ export function createPhaseStartEvent(type: StormEventPhaseType): StormEventPhases {
99
+ return createPhaseEvent(true, type);
100
+ }
101
+
102
+ export function createPhaseEndEvent(type: StormEventPhaseType): StormEventPhases {
103
+ return createPhaseEvent(false, type);
104
+ }
105
+
106
+ export function createPhaseEvent(start: boolean, type: StormEventPhaseType): StormEventPhases {
107
+ return {
108
+ type: start ? 'PHASE_START' : 'PHASE_END',
109
+ created: Date.now(),
110
+ payload: {
111
+ phaseType: type,
112
+ },
113
+ };
114
+ }
115
+
90
116
  export async function resolveOptions(): Promise<StormOptions> {
91
117
  // Predefined types for now - TODO: Allow user to select / change
92
118
 
@@ -299,11 +325,6 @@ export class StormEventParser {
299
325
  evt.payload.toBlockId = StormEventParser.toInstanceId(handle, evt.payload.toComponent);
300
326
  this.connections.push(evt.payload);
301
327
  break;
302
-
303
- default:
304
- case 'SCREEN_CANDIDATE':
305
- case 'FILE':
306
- break;
307
328
  }
308
329
 
309
330
  return this.toResult(handle);
@@ -325,7 +346,7 @@ export class StormEventParser {
325
346
  }
326
347
 
327
348
  public toResult(handle: string): StormDefinitions {
328
- const planRef = StormEventParser.toRef(handle, this.planName ?? 'undefined');
349
+ const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
329
350
  const blockDefinitions = this.toBlockDefinitions(handle);
330
351
  const refIdMap: { [key: string]: string } = {};
331
352
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
@@ -337,7 +358,7 @@ export class StormEventParser {
337
358
  block: {
338
359
  ref,
339
360
  },
340
- name: block.content.metadata.title ?? block.content.metadata.name,
361
+ name: block.content.metadata.title || block.content.metadata.name,
341
362
  dimensions: {
342
363
  left: 0,
343
364
  top: 0,
@@ -182,20 +182,41 @@ export interface StormEventScreenCandidate {
182
182
  };
183
183
  }
184
184
 
185
- export interface StormEventFile {
186
- type: 'FILE';
185
+ export interface StormEventFileBasePayload {
186
+ filename: string;
187
+ path: string;
188
+ blockName: string;
189
+ blockRef: string;
190
+ instanceId: string;
191
+ }
192
+
193
+ export interface StormEventFileBase {
194
+ type: string;
187
195
  reason: string;
188
196
  created: number;
189
- payload: {
190
- filename: string;
191
- path: string;
197
+ payload: StormEventFileBasePayload;
198
+ }
199
+
200
+ export interface StormEventFileLogical extends StormEventFileBase {
201
+ type: 'FILE_START' | 'FILE_CHUNK_RESET';
202
+ }
203
+
204
+ export interface StormEventFileState extends StormEventFileBase {
205
+ type: 'FILE_STATE';
206
+ payload: StormEventFileBasePayload & {
207
+ state: string;
208
+ };
209
+ }
210
+
211
+ export interface StormEventFileContent extends StormEventFileBase {
212
+ type: 'FILE_DONE' | 'FILE_CHUNK';
213
+ payload: StormEventFileBasePayload & {
192
214
  content: string;
193
- blockName: string;
194
- blockRef: string;
195
- instanceId: string;
196
215
  };
197
216
  }
198
217
 
218
+ export type StormEventFile = StormEventFileLogical | StormEventFileState | StormEventFileContent;
219
+
199
220
  export interface StormEventBlockReady {
200
221
  type: 'BLOCK_READY';
201
222
  reason: string;
@@ -220,6 +241,20 @@ export interface StormEventDefinitionChange {
220
241
  payload: StormDefinitions;
221
242
  }
222
243
 
244
+ export enum StormEventPhaseType {
245
+ META = 'META',
246
+ DEFINITIONS = 'DEFINITIONS',
247
+ IMPLEMENTATION = 'IMPLEMENTATION',
248
+ }
249
+
250
+ export interface StormEventPhases {
251
+ type: 'PHASE_START' | 'PHASE_END';
252
+ created: number;
253
+ payload: {
254
+ phaseType: StormEventPhaseType;
255
+ };
256
+ }
257
+
223
258
  export type StormEvent =
224
259
  | StormEventCreateBlock
225
260
  | StormEventCreateConnection
@@ -231,9 +266,12 @@ export type StormEvent =
231
266
  | StormEventError
232
267
  | StormEventScreen
233
268
  | StormEventScreenCandidate
234
- | StormEventFile
269
+ | StormEventFileLogical
270
+ | StormEventFileState
271
+ | StormEventFileContent
235
272
  | StormEventDone
236
273
  | StormEventDefinitionChange
237
274
  | StormEventErrorClassifier
238
275
  | StormEventCodeFix
239
- | StormEventBlockReady;
276
+ | StormEventBlockReady
277
+ | StormEventPhases;