Skip to content

Commit 3cd14ed

Browse files
committed
feat: purely in-isolate template unzip + inmemory storage
1 parent 1ac3a43 commit 3cd14ed

8 files changed

Lines changed: 146 additions & 79 deletions

File tree

src/routes/chat/utils/handle-websocket-message.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) {
152152
}));
153153
break;
154154
}
155-
case 'cf_agent_state': {
156-
const { state } = message;
157-
logger.debug('🔄 Agent state update received:', state);
158-
155+
case 'agent_connected': {
156+
const { state, templateDetails } = message;
157+
console.log('Agent connected', state, templateDetails);
158+
159159
if (!isInitialStateRestored) {
160160
logger.debug('📥 Performing initial state restoration');
161161

@@ -168,11 +168,11 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) {
168168
setQuery(state.query);
169169
}
170170

171-
if (state.templateDetails?.allFiles && bootstrapFiles.length === 0) {
172-
const files = Object.entries(state.templateDetails.allFiles).map(([filePath, fileContents]) => ({
171+
if (templateDetails?.allFiles && bootstrapFiles.length === 0) {
172+
const files = Object.entries(templateDetails.allFiles).map(([filePath, fileContents]) => ({
173173
filePath,
174174
fileContents,
175-
}));
175+
})).filter((file) => templateDetails.importantFiles.includes(file.filePath));
176176
logger.debug('📥 Restoring bootstrap files:', files);
177177
loadBootstrapFiles(files);
178178
}
@@ -254,6 +254,11 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) {
254254
sendWebSocketMessage(websocket, 'preview');
255255
}
256256
}
257+
break;
258+
}
259+
case 'cf_agent_state': {
260+
const { state } = message;
261+
logger.debug('🔄 Agent state update received:', state);
257262

258263
if (state.shouldBeGenerating) {
259264
logger.debug('🔄 shouldBeGenerating=true detected, auto-resuming generation');

worker/agents/core/simpleGeneratorAgent.ts

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Agent, AgentContext, Connection } from 'agents';
1+
import { Agent, AgentContext, Connection, ConnectionContext } from 'agents';
22
import {
33
Blueprint,
44
PhaseConceptGenerationSchemaType,
@@ -12,7 +12,7 @@ import { GitHubExportResult } from '../../services/github/types';
1212
import { CodeGenState, CurrentDevState, MAX_PHASES } from './state';
1313
import { AllIssues, AgentSummary, AgentInitArgs, PhaseExecutionResult, UserContext } from './types';
1414
import { PREVIEW_EXPIRED_ERROR, WebSocketMessageResponses } from '../constants';
15-
import { broadcastToConnections, handleWebSocketClose, handleWebSocketMessage } from './websocket';
15+
import { broadcastToConnections, handleWebSocketClose, handleWebSocketMessage, sendToConnection } from './websocket';
1616
import { createObjectLogger, StructuredLogger } from '../../logger';
1717
import { ProjectSetupAssistant } from '../assistants/projectsetup';
1818
import { UserConversationProcessor, RenderToolCall } from '../operations/UserConversationProcessor';
@@ -82,6 +82,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
8282
protected deploymentManager!: DeploymentManager;
8383

8484
private previewUrlCache: string = '';
85+
private templateDetailsCache: TemplateDetails | null = null;
8586

8687
// In-memory storage for user-uploaded images (not persisted in DO state)
8788
private pendingUserImages: ProcessedImageAttachment[] = []
@@ -131,7 +132,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
131132
generatedFilesMap: {},
132133
agentMode: 'deterministic',
133134
sandboxInstanceId: undefined,
134-
templateDetails: {} as TemplateDetails,
135+
templateName: '',
135136
commandsHistory: [],
136137
lastPackageJson: '',
137138
pendingUserInputs: [],
@@ -160,7 +161,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
160161
);
161162

162163
// Initialize FileManager
163-
this.fileManager = new FileManager(this.stateManager);
164+
this.fileManager = new FileManager(this.stateManager, () => this.getTemplateDetails());
164165

165166
// Initialize DeploymentManager first (manages sandbox client caching)
166167
// DeploymentManager will use its own getClient() override for caching
@@ -212,12 +213,14 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
212213
})
213214

214215
const packageJson = templateInfo.templateDetails?.allFiles['package.json'];
216+
217+
this.templateDetailsCache = templateInfo.templateDetails;
215218

216219
this.setState({
217220
...this.initialState,
218221
query,
219222
blueprint,
220-
templateDetails: templateInfo.templateDetails,
223+
templateName: templateInfo.templateDetails.name,
221224
sandboxInstanceId: undefined,
222225
generatedPhases: [],
223226
commandsHistory: [],
@@ -253,7 +256,59 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
253256

254257
async isInitialized() {
255258
return this.getAgentId() ? true : false
256-
}
259+
}
260+
261+
async onStart(_props?: Record<string, unknown> | undefined): Promise<void> {
262+
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart`);
263+
// Ignore if agent not initialized
264+
if (!this.isInitialized()) {
265+
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} not initialized, ignoring onStart`);
266+
return;
267+
}
268+
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart being processed`);
269+
// Fill the template cache
270+
await this.ensureTemplateDetails();
271+
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart processed successfully`);
272+
}
273+
274+
onStateUpdate(_state: CodeGenState, _source: "server" | Connection) {}
275+
276+
setState(state: CodeGenState): void {
277+
try {
278+
super.setState(state);
279+
} catch (error) {
280+
this.broadcastError("Error setting state", error);
281+
this.logger().error("State details:", {
282+
originalState: JSON.stringify(this.state, null, 2),
283+
newState: JSON.stringify(state, null, 2)
284+
});
285+
}
286+
}
287+
288+
onConnect(connection: Connection, ctx: ConnectionContext) {
289+
this.logger().info(`Agent connected for agent ${this.getAgentId()}`, { connection, ctx });
290+
sendToConnection(connection, 'agent_connected', {
291+
state: this.state,
292+
templateDetails: this.getTemplateDetails()
293+
});
294+
}
295+
296+
async ensureTemplateDetails() {
297+
if (!this.templateDetailsCache) {
298+
this.logger().info(`Template details being cached for template: ${this.state.templateName}`);
299+
const results = await BaseSandboxService.getTemplateDetails(this.state.templateName);
300+
if (!results.success || !results.templateDetails) {
301+
throw new Error(`Failed to get template details for template: ${this.state.templateName}`);
302+
}
303+
this.templateDetailsCache = results.templateDetails;
304+
this.logger().info(`Template details for template: ${this.state.templateName} cached successfully`);
305+
}
306+
return this.templateDetailsCache;
307+
}
308+
309+
private getTemplateDetails() {
310+
return this.templateDetailsCache!;
311+
}
257312

258313
/*
259314
* Each DO has 10 gb of sqlite storage. However, the way agents sdk works, it stores the 'state' object of the agent as a single row
@@ -333,20 +388,6 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
333388
});
334389
this.logger().info(`Agent initialized successfully for agent ${this.state.inferenceContext.agentId}`);
335390
}
336-
337-
onStateUpdate(_state: CodeGenState, _source: "server" | Connection) {}
338-
339-
setState(state: CodeGenState): void {
340-
try {
341-
super.setState(state);
342-
} catch (error) {
343-
this.broadcastError("Error setting state", error);
344-
this.logger().error("State details:", {
345-
originalState: JSON.stringify(this.state, null, 2),
346-
newState: JSON.stringify(state, null, 2)
347-
});
348-
}
349-
}
350391

351392
getPreviewUrlCache() {
352393
return this.previewUrlCache;
@@ -359,7 +400,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
359400
agentId: this.getAgentId(),
360401
query: this.state.query,
361402
blueprint: this.state.blueprint,
362-
template: this.state.templateDetails,
403+
template: this.getTemplateDetails(),
363404
inferenceContext: this.state.inferenceContext
364405
});
365406
}

worker/agents/core/state.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Blueprint, PhaseConceptType ,
22
FileOutputType,
33
} from '../schemas';
4-
import type { TemplateDetails } from '../../services/sandbox/sandboxTypes';
54
// import type { ScreenshotData } from './types';
65
import type { ConversationMessage } from '../inferutils/common';
76
import type { InferenceContext } from '../inferutils/config.types';
@@ -36,7 +35,7 @@ export interface CodeGenState {
3635
generatedPhases: PhaseState[];
3736
commandsHistory?: string[]; // History of commands run
3837
lastPackageJson?: string; // Last package.json file contents
39-
templateDetails: TemplateDetails; // TODO: Remove this from state and rely on directly fetching from sandbox
38+
templateName: string;
4039
sandboxInstanceId?: string;
4140

4241
shouldBeGenerating: boolean; // Persistent flag indicating generation should be active

worker/agents/core/stateMigration.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { CodeGenState, FileState } from './state';
22
import { StructuredLogger } from '../../logger';
3+
import { TemplateDetails } from 'worker/services/sandbox/sandboxTypes';
34

45
export class StateMigration {
56
static migrateIfNeeded(state: CodeGenState, logger: StructuredLogger): CodeGenState | null {
67
let needsMigration = false;
78

9+
//------------------------------------------------------------------------------------
10+
// Migrate files from old schema
11+
//------------------------------------------------------------------------------------
812
const migrateFile = (file: any): any => {
913
const hasOldFormat = 'file_path' in file || 'file_contents' in file || 'file_purpose' in file;
1014

@@ -34,27 +38,9 @@ export class StateMigration {
3438
}
3539
}
3640

37-
let migratedTemplateDetails = state.templateDetails;
38-
if ('files' in migratedTemplateDetails && migratedTemplateDetails?.files) {
39-
const migratedTemplateFiles = (migratedTemplateDetails.files as Array<any>).map(file => {
40-
const migratedFile = migrateFile(file);
41-
return migratedFile;
42-
});
43-
44-
const allFiles = migratedTemplateFiles.reduce((acc, file) => {
45-
acc[file.filePath] = file;
46-
return acc;
47-
}, {} as Record<string, any>);
48-
49-
migratedTemplateDetails = {
50-
...migratedTemplateDetails,
51-
allFiles
52-
};
53-
54-
// Remove 'files' property
55-
delete (migratedTemplateDetails as any).files;
56-
needsMigration = true;
57-
}
41+
//------------------------------------------------------------------------------------
42+
// Migrate conversations cleanups and internal memos
43+
//------------------------------------------------------------------------------------
5844

5945
let migratedConversationMessages = state.conversationMessages;
6046
const MIN_MESSAGES_FOR_CLEANUP = 25;
@@ -132,6 +118,9 @@ export class StateMigration {
132118
}
133119
}
134120

121+
//------------------------------------------------------------------------------------
122+
// Migrate inference context from old schema
123+
//------------------------------------------------------------------------------------
135124
let migratedInferenceContext = state.inferenceContext;
136125
if (migratedInferenceContext && 'userApiKeys' in migratedInferenceContext) {
137126
migratedInferenceContext = {
@@ -142,6 +131,9 @@ export class StateMigration {
142131
needsMigration = true;
143132
}
144133

134+
//------------------------------------------------------------------------------------
135+
// Migrate deprecated props
136+
//------------------------------------------------------------------------------------
145137
const stateHasDeprecatedProps = 'latestScreenshot' in (state as any);
146138
if (stateHasDeprecatedProps) {
147139
needsMigration = true;
@@ -151,19 +143,29 @@ export class StateMigration {
151143
if (!stateHasProjectUpdatesAccumulator) {
152144
needsMigration = true;
153145
}
146+
147+
//------------------------------------------------------------------------------------
148+
// Migrate Template Details -> remove template details and instead use template name
149+
//------------------------------------------------------------------------------------
150+
const hasTemplateDetails = 'templateDetails' in (state as any);
151+
if (hasTemplateDetails) {
152+
needsMigration = true;
153+
const templateDetails = (state as any).templateDetails;
154+
const templateName = (templateDetails as TemplateDetails).name;
155+
delete (state as any).templateDetails;
156+
(state as any).templateName = templateName;
157+
}
154158

155159
if (needsMigration) {
156160
logger.info('Migrating state: schema format, conversation cleanup, and security fixes', {
157161
generatedFilesCount: Object.keys(migratedFilesMap).length,
158-
templateFilesCount: migratedTemplateDetails?.allFiles?.length || 0,
159162
finalConversationCount: migratedConversationMessages?.length || 0,
160163
removedUserApiKeys: state.inferenceContext && 'userApiKeys' in state.inferenceContext
161164
});
162165

163166
const newState = {
164167
...state,
165168
generatedFilesMap: migratedFilesMap,
166-
templateDetails: migratedTemplateDetails,
167169
conversationMessages: migratedConversationMessages,
168170
inferenceContext: migratedInferenceContext,
169171
projectUpdatesAccumulator: []

worker/agents/services/implementations/DeploymentManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ export class DeploymentManager extends BaseAgentService implements IDeploymentMa
517517
*/
518518
private async createNewInstance(): Promise<BootstrapResponse | null> {
519519
const state = this.getState();
520-
const templateName = state.templateDetails?.name || 'scratch';
520+
const templateName = state.templateName || 'scratch';
521521

522522
// Generate unique project name
523523
let prefix = (state.blueprint?.projectName || templateName)
@@ -532,7 +532,7 @@ export class DeploymentManager extends BaseAgentService implements IDeploymentMa
532532

533533
// Add AI proxy vars if AI template
534534
let localEnvVars: Record<string, string> = {};
535-
if (state.templateDetails?.name?.includes('agents')) {
535+
if (state.templateName?.includes('agents')) {
536536
localEnvVars = {
537537
"CF_AI_BASE_URL": generateAppProxyUrl(this.env),
538538
"CF_AI_API_KEY": await generateAppProxyToken(

worker/agents/services/implementations/FileManager.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ import * as Diff from 'diff';
22
import { IFileManager } from '../interfaces/IFileManager';
33
import { IStateManager } from '../interfaces/IStateManager';
44
import { FileOutputType } from '../../schemas';
5-
// import { TemplateDetails } from '../../../services/sandbox/sandboxTypes';
65
import { FileProcessing } from '../../domain/pure/FileProcessing';
76
import { FileState } from 'worker/agents/core/state';
7+
import { TemplateDetails } from '../../../services/sandbox/sandboxTypes';
88

99
/**
1010
* Manages file operations for code generation
1111
* Handles both template and generated files
1212
*/
1313
export class FileManager implements IFileManager {
1414
constructor(
15-
private stateManager: IStateManager
15+
private stateManager: IStateManager,
16+
private getTemplateDetailsFunc: () => TemplateDetails
1617
) {}
1718

1819
getGeneratedFile(path: string): FileOutputType | null {
@@ -22,7 +23,7 @@ export class FileManager implements IFileManager {
2223

2324
getAllFiles(): FileOutputType[] {
2425
const state = this.stateManager.getState();
25-
return FileProcessing.getAllFiles(state.templateDetails, state.generatedFilesMap);
26+
return FileProcessing.getAllFiles(this.getTemplateDetailsFunc(), state.generatedFilesMap);
2627
}
2728

2829
saveGeneratedFile(file: FileOutputType): FileState {
@@ -86,6 +87,7 @@ export class FileManager implements IFileManager {
8687
generatedFilesMap: newFilesMap
8788
});
8889
}
90+
8991
fileExists(path: string): boolean {
9092
return !!this.getGeneratedFile(path)
9193
}

worker/api/websocketTypes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CodeReviewOutputType, FileConceptType, FileOutputType } from "../agents/schemas";
22
import type { CodeGenState } from "../agents/core/state";
33
import type { ConversationState } from "../agents/inferutils/common";
4-
import type { CodeIssue, RuntimeError, StaticAnalysisResponse } from "../services/sandbox/sandboxTypes";
4+
import type { CodeIssue, RuntimeError, StaticAnalysisResponse, TemplateDetails } from "../services/sandbox/sandboxTypes";
55
import type { CodeFixResult } from "../services/code-fixer";
66
import { IssueReport } from "../agents/domain/values/IssueReport";
77
import type { RateLimitExceededError } from 'shared/types/errors';
@@ -16,6 +16,12 @@ type StateMessage = {
1616
state: CodeGenState;
1717
};
1818

19+
type AgentConnectedMessage = {
20+
type: 'agent_connected';
21+
state: CodeGenState;
22+
templateDetails: TemplateDetails;
23+
};
24+
1925
type ConversationStateMessage = {
2026
type: 'conversation_state';
2127
state: ConversationState;
@@ -408,6 +414,7 @@ type ServerLogMessage = {
408414

409415
export type WebSocketMessage =
410416
| StateMessage
417+
| AgentConnectedMessage
411418
| ConversationStateMessage
412419
| GenerationStartedMessage
413420
| FileGeneratingMessage

0 commit comments

Comments
 (0)