@kapeta/local-cluster-service 0.49.0 → 0.50.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.
@@ -184,4 +184,16 @@ export interface StormEventDefinitionChange {
184
184
  created: number;
185
185
  payload: StormDefinitions;
186
186
  }
187
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady;
187
+ export declare enum StormEventPhaseType {
188
+ META = "META",
189
+ DEFINITIONS = "DEFINITIONS",
190
+ IMPLEMENTATION = "IMPLEMENTATION"
191
+ }
192
+ export interface StormEventPhases {
193
+ type: 'PHASE_START' | 'PHASE_END';
194
+ created: number;
195
+ payload: {
196
+ phaseType: StormEventPhaseType;
197
+ };
198
+ }
199
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StormEventPhaseType = void 0;
4
+ var StormEventPhaseType;
5
+ (function (StormEventPhaseType) {
6
+ StormEventPhaseType["META"] = "META";
7
+ StormEventPhaseType["DEFINITIONS"] = "DEFINITIONS";
8
+ StormEventPhaseType["IMPLEMENTATION"] = "IMPLEMENTATION";
9
+ })(StormEventPhaseType || (exports.StormEventPhaseType = StormEventPhaseType = {}));
@@ -12,6 +12,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const cors_1 = require("../middleware/cors");
13
13
  const stringBody_1 = require("../middleware/stringBody");
14
14
  const stormClient_1 = require("./stormClient");
15
+ const events_1 = require("./events");
15
16
  const event_parser_1 = require("./event-parser");
16
17
  const codegen_1 = require("./codegen");
17
18
  const assetManager_1 = require("../assetManager");
@@ -27,15 +28,41 @@ router.post('/:handle/all', async (req, res) => {
27
28
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
28
29
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
29
30
  const metaStream = await stormClient_1.stormClient.createMetadata(aiRequest.prompt, conversationId);
31
+ onRequestAborted(req, res, () => {
32
+ metaStream.abort();
33
+ });
30
34
  res.set('Content-Type', 'application/x-ndjson');
31
35
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
32
36
  res.set(stormClient_1.ConversationIdHeader, metaStream.getConversationId());
37
+ let currentPhase = events_1.StormEventPhaseType.META;
33
38
  metaStream.on('data', (data) => {
34
39
  const result = eventParser.processEvent(req.params.handle, data);
40
+ switch (data.type) {
41
+ case 'CREATE_API':
42
+ case 'CREATE_MODEL':
43
+ case 'CREATE_TYPE':
44
+ if (currentPhase !== events_1.StormEventPhaseType.DEFINITIONS) {
45
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.META));
46
+ currentPhase = events_1.StormEventPhaseType.DEFINITIONS;
47
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.DEFINITIONS));
48
+ }
49
+ break;
50
+ }
35
51
  sendEvent(res, data);
36
52
  sendDefinitions(res, result);
37
53
  });
38
- await waitForStormStream(metaStream);
54
+ try {
55
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.META));
56
+ await waitForStormStream(metaStream);
57
+ }
58
+ finally {
59
+ if (!metaStream.isAborted()) {
60
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(currentPhase));
61
+ }
62
+ }
63
+ if (metaStream.isAborted()) {
64
+ return;
65
+ }
39
66
  if (!eventParser.isValid()) {
40
67
  // We can't continue if the meta stream is invalid
41
68
  sendEvent(res, {
@@ -48,32 +75,45 @@ router.post('/:handle/all', async (req, res) => {
48
75
  return;
49
76
  }
50
77
  const result = eventParser.toResult(handle);
78
+ if (metaStream.isAborted()) {
79
+ return;
80
+ }
51
81
  sendDefinitions(res, result);
52
82
  if (!req.query.skipCodegen) {
53
- const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
54
- const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
55
- await stormCodegen.process();
56
- await codegenPromise;
83
+ try {
84
+ sendEvent(res, (0, event_parser_1.createPhaseStartEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
85
+ const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
86
+ onRequestAborted(req, res, () => {
87
+ stormCodegen.abort();
88
+ });
89
+ const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
90
+ await stormCodegen.process();
91
+ await codegenPromise;
92
+ }
93
+ finally {
94
+ if (!metaStream.isAborted()) {
95
+ sendEvent(res, (0, event_parser_1.createPhaseEndEvent)(events_1.StormEventPhaseType.IMPLEMENTATION));
96
+ }
97
+ }
57
98
  }
58
99
  sendDone(res);
59
100
  }
60
101
  catch (err) {
61
102
  sendError(err, res);
62
- res.end();
103
+ if (!res.closed) {
104
+ res.end();
105
+ }
63
106
  }
64
107
  });
65
108
  router.post('/block/create', async (req, res) => {
66
109
  const createRequest = JSON.parse(req.stringBody ?? '{}');
67
110
  try {
68
111
  const ymlPath = path_1.default.join(createRequest.newPath, 'kapeta.yml');
69
- console.log('Creating block at', ymlPath);
70
112
  const [asset] = await assetManager_1.assetManager.createAsset(ymlPath, createRequest.definition);
71
113
  if (await fs_extra_1.default.pathExists(createRequest.tmpPath)) {
72
- console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
73
114
  await fs_extra_1.default.move(createRequest.tmpPath, createRequest.newPath, {
74
115
  overwrite: true,
75
116
  });
76
- console.log('Updating asset', asset.ref);
77
117
  res.send(await assetManager_1.assetManager.updateAsset(asset.ref, createRequest.definition));
78
118
  }
79
119
  else {
@@ -93,6 +133,9 @@ function sendDefinitions(res, result) {
93
133
  });
94
134
  }
95
135
  function sendDone(res) {
136
+ if (res.closed) {
137
+ return;
138
+ }
96
139
  sendEvent(res, {
97
140
  type: 'DONE',
98
141
  created: Date.now(),
@@ -100,6 +143,9 @@ function sendDone(res) {
100
143
  res.end();
101
144
  }
102
145
  function sendError(err, res) {
146
+ if (res.closed) {
147
+ return;
148
+ }
103
149
  console.error('Failed to send prompt', err);
104
150
  if (res.headersSent) {
105
151
  sendEvent(res, {
@@ -137,6 +183,17 @@ function streamStormPartialResponse(result, res) {
137
183
  });
138
184
  }
139
185
  function sendEvent(res, evt) {
186
+ if (res.closed) {
187
+ return;
188
+ }
140
189
  res.write(JSON.stringify(evt) + '\n');
141
190
  }
191
+ function onRequestAborted(req, res, onAborted) {
192
+ req.on('close', () => {
193
+ onAborted();
194
+ });
195
+ res.on('close', () => {
196
+ onAborted();
197
+ });
198
+ }
142
199
  exports.default = router;
@@ -46,12 +46,13 @@ class StormClient {
46
46
  prompt: stringPrompt,
47
47
  conversationId: body.conversationId,
48
48
  });
49
+ const abort = new AbortController();
50
+ options.signal = abort.signal;
49
51
  const response = await fetch(options.url, options);
50
52
  if (response.status !== 200) {
51
53
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
52
54
  }
53
55
  const conversationId = response.headers.get(exports.ConversationIdHeader);
54
- console.log('Received conversationId', conversationId);
55
56
  const out = new stream_1.StormStream(stringPrompt, conversationId);
56
57
  const jsonLStream = promises_1.default.createInterface(node_stream_1.Readable.fromWeb(response.body));
57
58
  jsonLStream.on('line', (line) => {
@@ -63,6 +64,9 @@ class StormClient {
63
64
  jsonLStream.on('close', () => {
64
65
  out.end();
65
66
  });
67
+ out.on('aborted', () => {
68
+ abort.abort();
69
+ });
66
70
  return out;
67
71
  }
68
72
  createMetadata(prompt, conversationId) {
@@ -10,17 +10,22 @@ import { BlockDefinition } from '@kapeta/schemas';
10
10
  export declare class StormStream extends EventEmitter {
11
11
  private conversationId;
12
12
  private lines;
13
+ private aborted;
13
14
  constructor(prompt?: string, conversationId?: string | null);
14
15
  getConversationId(): string;
16
+ isAborted(): boolean;
15
17
  addJSONLine(line: string): void;
16
18
  end(): void;
17
19
  on(event: 'end', listener: () => void): this;
20
+ on(event: 'aborted', listener: () => void): this;
18
21
  on(event: 'error', listener: (e: Error) => void): this;
19
22
  on(event: 'data', listener: (data: StormEvent) => void): this;
20
23
  emit(event: 'end'): boolean;
24
+ emit(event: 'aborted'): void;
21
25
  emit(event: 'error', e: Error): boolean;
22
26
  emit(event: 'data', data: StormEvent): boolean;
23
27
  waitForDone(): Promise<void>;
28
+ abort(): void;
24
29
  }
25
30
  export interface ConversationItem {
26
31
  role: 'user' | 'model';
@@ -9,6 +9,7 @@ const node_events_1 = require("node:events");
9
9
  class StormStream extends node_events_1.EventEmitter {
10
10
  conversationId = '';
11
11
  lines = [];
12
+ aborted = false;
12
13
  constructor(prompt = '', conversationId) {
13
14
  super();
14
15
  this.conversationId = conversationId || '';
@@ -16,6 +17,9 @@ class StormStream extends node_events_1.EventEmitter {
16
17
  getConversationId() {
17
18
  return this.conversationId;
18
19
  }
20
+ isAborted() {
21
+ return this.aborted;
22
+ }
19
23
  addJSONLine(line) {
20
24
  try {
21
25
  this.lines.push(line);
@@ -48,5 +52,12 @@ class StormStream extends node_events_1.EventEmitter {
48
52
  });
49
53
  });
50
54
  }
55
+ abort() {
56
+ if (this.aborted) {
57
+ return;
58
+ }
59
+ this.aborted = true;
60
+ this.emit('aborted');
61
+ }
51
62
  }
52
63
  exports.StormStream = StormStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -51,6 +51,10 @@ export class StormCodegen {
51
51
  this.out.end();
52
52
  }
53
53
 
54
+ isAborted() {
55
+ return this.out.isAborted();
56
+ }
57
+
54
58
  public getStream() {
55
59
  return this.out;
56
60
  }
@@ -110,6 +114,9 @@ export class StormCodegen {
110
114
  * Generates the code for a block and sends it to the AI
111
115
  */
112
116
  private async processBlockCode(block: BlockDefinitionInfo) {
117
+ if (this.isAborted()) {
118
+ return;
119
+ }
113
120
  // Generate the code for the block using the standard codegen templates
114
121
  const generatedResult = await this.generateBlock(block.content);
115
122
  if (!generatedResult) {
@@ -121,6 +128,10 @@ export class StormCodegen {
121
128
  // Send all the non-ai files to the stream
122
129
  this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
123
130
 
131
+ if (this.isAborted()) {
132
+ return;
133
+ }
134
+
124
135
  const relevantFiles: StormFileInfo[] = allFiles.filter(
125
136
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
126
137
  );
@@ -138,9 +149,17 @@ export class StormCodegen {
138
149
  this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
139
150
  });
140
151
 
152
+ this.out.on('aborted', () => {
153
+ uiStream.abort();
154
+ });
155
+
141
156
  await uiStream.waitForDone();
142
157
  }
143
158
 
159
+ if (this.isAborted()) {
160
+ return;
161
+ }
162
+
144
163
  // Gather the context files for implementation. These will be all be passed to the AI
145
164
  const contextFiles: StormFileInfo[] = relevantFiles.filter(
146
165
  (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
@@ -160,6 +179,10 @@ export class StormCodegen {
160
179
 
161
180
  const basePath = this.getBasePath(block.content.metadata.name);
162
181
 
182
+ if (this.isAborted()) {
183
+ return;
184
+ }
185
+
163
186
  for (const serviceFile of serviceFiles) {
164
187
  const filePath = join(basePath, serviceFile.filename);
165
188
  await writeFile(filePath, serviceFile.content);
@@ -216,6 +239,10 @@ export class StormCodegen {
216
239
  const errorStream = await stormClient.createErrorClassification(result.error, []);
217
240
  const fixes = new Map<string, Promise<string>>();
218
241
 
242
+ this.out.on('aborted', () => {
243
+ errorStream.abort();
244
+ });
245
+
219
246
  errorStream.on('data', (evt) => {
220
247
  if (evt.type === 'ERROR_CLASSIFIER') {
221
248
  // find the file that caused the error
@@ -232,8 +259,8 @@ export class StormCodegen {
232
259
  const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
233
260
  .map((e) => e.filename)
234
261
  .join('\n')}\n---\n${content}`;
235
- console.log(`trying to fix the code in ${eventFileName}`);
236
- console.debug(`with the fix:\n${fix}`);
262
+ //console.log(`trying to fix the code in ${eventFileName}`);
263
+ //console.debug(`with the fix:\n${fix}`);
237
264
  const code = this.codeFix(fix);
238
265
  fixes.set(join(basePath, eventFileName), code);
239
266
  }
@@ -278,6 +305,9 @@ export class StormCodegen {
278
305
  resolve(evt.payload.content);
279
306
  }
280
307
  });
308
+ this.out.on('aborted', () => {
309
+ fixStream.abort();
310
+ });
281
311
  fixStream.on('error', (err) => {
282
312
  reject(err);
283
313
  });
@@ -353,6 +383,10 @@ export class StormCodegen {
353
383
 
354
384
  const files: StormEventFile[] = [];
355
385
 
386
+ this.out.on('aborted', () => {
387
+ stream.abort();
388
+ });
389
+
356
390
  stream.on('data', (evt) => {
357
391
  const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
358
392
  if (file) {
@@ -411,6 +445,9 @@ export class StormCodegen {
411
445
  * Generates the code using codegen for a given block.
412
446
  */
413
447
  private async generateBlock(yamlContent: Definition) {
448
+ if (this.isAborted()) {
449
+ return;
450
+ }
414
451
  if (!yamlContent.spec.target?.kind) {
415
452
  //Not all block types have targets
416
453
  return;
@@ -427,4 +464,8 @@ export class StormCodegen {
427
464
  new CodeWriter(basePath).write(generatedResult);
428
465
  return generatedResult;
429
466
  }
467
+
468
+ abort() {
469
+ this.out.abort();
470
+ }
430
471
  }
@@ -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
 
@@ -325,7 +351,7 @@ export class StormEventParser {
325
351
  }
326
352
 
327
353
  public toResult(handle: string): StormDefinitions {
328
- const planRef = StormEventParser.toRef(handle, this.planName ?? 'undefined');
354
+ const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
329
355
  const blockDefinitions = this.toBlockDefinitions(handle);
330
356
  const refIdMap: { [key: string]: string } = {};
331
357
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
@@ -337,7 +363,7 @@ export class StormEventParser {
337
363
  block: {
338
364
  ref,
339
365
  },
340
- name: block.content.metadata.title ?? block.content.metadata.name,
366
+ name: block.content.metadata.title || block.content.metadata.name,
341
367
  dimensions: {
342
368
  left: 0,
343
369
  top: 0,
@@ -220,6 +220,20 @@ export interface StormEventDefinitionChange {
220
220
  payload: StormDefinitions;
221
221
  }
222
222
 
223
+ export enum StormEventPhaseType {
224
+ META = 'META',
225
+ DEFINITIONS = 'DEFINITIONS',
226
+ IMPLEMENTATION = 'IMPLEMENTATION',
227
+ }
228
+
229
+ export interface StormEventPhases {
230
+ type: 'PHASE_START' | 'PHASE_END';
231
+ created: number;
232
+ payload: {
233
+ phaseType: StormEventPhaseType;
234
+ };
235
+ }
236
+
223
237
  export type StormEvent =
224
238
  | StormEventCreateBlock
225
239
  | StormEventCreateConnection
@@ -236,4 +250,5 @@ export type StormEvent =
236
250
  | StormEventDefinitionChange
237
251
  | StormEventErrorClassifier
238
252
  | StormEventCodeFix
239
- | StormEventBlockReady;
253
+ | StormEventBlockReady
254
+ | StormEventPhases;
@@ -11,12 +11,17 @@ import { stringBody } from '../middleware/stringBody';
11
11
  import { KapetaBodyRequest } from '../types';
12
12
  import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
13
13
  import { ConversationIdHeader, stormClient } from './stormClient';
14
- import { StormEvent } from './events';
15
- import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
14
+ import { StormEvent, StormEventPhaseType } from './events';
15
+ import {
16
+ createPhaseEndEvent,
17
+ createPhaseStartEvent,
18
+ resolveOptions,
19
+ StormDefinitions,
20
+ StormEventParser,
21
+ } from './event-parser';
16
22
  import { StormCodegen } from './codegen';
17
23
  import { assetManager } from '../assetManager';
18
24
  import Path from 'path';
19
- import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
20
25
 
21
26
  const router = Router();
22
27
 
@@ -36,18 +41,48 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
36
41
  const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
37
42
  const metaStream = await stormClient.createMetadata(aiRequest.prompt, conversationId);
38
43
 
44
+ onRequestAborted(req, res, () => {
45
+ metaStream.abort();
46
+ });
47
+
39
48
  res.set('Content-Type', 'application/x-ndjson');
40
49
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
41
50
  res.set(ConversationIdHeader, metaStream.getConversationId());
42
51
 
52
+ let currentPhase = StormEventPhaseType.META;
53
+
43
54
  metaStream.on('data', (data: StormEvent) => {
44
55
  const result = eventParser.processEvent(req.params.handle, data);
45
56
 
57
+ switch (data.type) {
58
+ case 'CREATE_API':
59
+ case 'CREATE_MODEL':
60
+ case 'CREATE_TYPE':
61
+ if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
62
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
63
+ currentPhase = StormEventPhaseType.DEFINITIONS;
64
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
65
+ }
66
+ break;
67
+ }
68
+
46
69
  sendEvent(res, data);
47
70
  sendDefinitions(res, result);
48
71
  });
49
72
 
50
- await waitForStormStream(metaStream);
73
+ try {
74
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.META));
75
+
76
+ await waitForStormStream(metaStream);
77
+ } finally {
78
+ if (!metaStream.isAborted()) {
79
+ sendEvent(res, createPhaseEndEvent(currentPhase));
80
+ }
81
+ }
82
+
83
+ if (metaStream.isAborted()) {
84
+ return;
85
+ }
51
86
 
52
87
  if (!eventParser.isValid()) {
53
88
  // We can't continue if the meta stream is invalid
@@ -63,27 +98,44 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
63
98
 
64
99
  const result = eventParser.toResult(handle);
65
100
 
101
+ if (metaStream.isAborted()) {
102
+ return;
103
+ }
104
+
66
105
  sendDefinitions(res, result);
67
106
 
68
107
  if (!req.query.skipCodegen) {
69
- const stormCodegen = new StormCodegen(
70
- metaStream.getConversationId(),
71
- aiRequest.prompt,
72
- result.blocks,
73
- eventParser.getEvents()
74
- );
75
-
76
- const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
77
-
78
- await stormCodegen.process();
79
-
80
- await codegenPromise;
108
+ try {
109
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENTATION));
110
+ const stormCodegen = new StormCodegen(
111
+ metaStream.getConversationId(),
112
+ aiRequest.prompt,
113
+ result.blocks,
114
+ eventParser.getEvents()
115
+ );
116
+
117
+ onRequestAborted(req, res, () => {
118
+ stormCodegen.abort();
119
+ });
120
+
121
+ const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
122
+
123
+ await stormCodegen.process();
124
+
125
+ await codegenPromise;
126
+ } finally {
127
+ if (!metaStream.isAborted()) {
128
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.IMPLEMENTATION));
129
+ }
130
+ }
81
131
  }
82
132
 
83
133
  sendDone(res);
84
134
  } catch (err: any) {
85
135
  sendError(err, res);
86
- res.end();
136
+ if (!res.closed) {
137
+ res.end();
138
+ }
87
139
  }
88
140
  });
89
141
 
@@ -93,19 +145,13 @@ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
93
145
  try {
94
146
  const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
95
147
 
96
- console.log('Creating block at', ymlPath);
97
-
98
148
  const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
99
149
 
100
150
  if (await FS.pathExists(createRequest.tmpPath)) {
101
- console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
102
-
103
151
  await FS.move(createRequest.tmpPath, createRequest.newPath, {
104
152
  overwrite: true,
105
153
  });
106
154
 
107
- console.log('Updating asset', asset.ref);
108
-
109
155
  res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
110
156
  } else {
111
157
  res.send(asset);
@@ -125,6 +171,9 @@ function sendDefinitions(res: Response, result: StormDefinitions) {
125
171
  }
126
172
 
127
173
  function sendDone(res: Response) {
174
+ if (res.closed) {
175
+ return;
176
+ }
128
177
  sendEvent(res, {
129
178
  type: 'DONE',
130
179
  created: Date.now(),
@@ -134,6 +183,9 @@ function sendDone(res: Response) {
134
183
  }
135
184
 
136
185
  function sendError(err: Error, res: Response) {
186
+ if (res.closed) {
187
+ return;
188
+ }
137
189
  console.error('Failed to send prompt', err);
138
190
  if (res.headersSent) {
139
191
  sendEvent(res, {
@@ -175,7 +227,19 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
175
227
  }
176
228
 
177
229
  function sendEvent(res: Response, evt: StormEvent) {
230
+ if (res.closed) {
231
+ return;
232
+ }
178
233
  res.write(JSON.stringify(evt) + '\n');
179
234
  }
180
235
 
236
+ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () => void) {
237
+ req.on('close', () => {
238
+ onAborted();
239
+ });
240
+ res.on('close', () => {
241
+ onAborted();
242
+ });
243
+ }
244
+
181
245
  export default router;