agent-state-machine 2.1.9 → 2.2.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/README.md +55 -5
- package/bin/cli.js +48 -0
- package/lib/file-tree.js +366 -0
- package/lib/index.js +109 -2
- package/lib/llm.js +12 -0
- package/lib/remote/client.js +37 -8
- package/lib/runtime/agent.js +146 -118
- package/lib/runtime/runtime.js +13 -2
- package/lib/runtime/track-changes.js +252 -0
- package/package.json +1 -1
- package/templates/project-builder/README.md +304 -56
- package/templates/project-builder/agents/sanity-runner.js +3 -1
- package/templates/project-builder/config.js +12 -1
- package/templates/starter/README.md +291 -42
- package/templates/starter/config.js +12 -1
- package/vercel-server/api/submit/[token].js +2 -2
- package/vercel-server/api/ws/cli.js +40 -2
- package/vercel-server/local-server.js +32 -3
- package/vercel-server/public/remote/assets/{index-BOKpYANC.js → index-CbgeVnKw.js} +28 -28
- package/vercel-server/public/remote/index.html +1 -1
- package/vercel-server/ui/src/App.jsx +0 -43
package/lib/remote/client.js
CHANGED
|
@@ -29,9 +29,13 @@ export function generateSessionToken() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Make an HTTP/HTTPS request
|
|
32
|
+
* Make an HTTP/HTTPS request with timeout
|
|
33
|
+
* @param {string} url - Request URL
|
|
34
|
+
* @param {object} options - Request options
|
|
35
|
+
* @param {object|null} body - Request body
|
|
36
|
+
* @param {number} timeoutMs - Request timeout in milliseconds
|
|
33
37
|
*/
|
|
34
|
-
function makeRequest(url, options, body = null) {
|
|
38
|
+
function makeRequest(url, options, body = null, timeoutMs = 60000) {
|
|
35
39
|
return new Promise((resolve, reject) => {
|
|
36
40
|
const parsedUrl = new URL(url);
|
|
37
41
|
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
@@ -60,6 +64,12 @@ function makeRequest(url, options, body = null) {
|
|
|
60
64
|
});
|
|
61
65
|
});
|
|
62
66
|
|
|
67
|
+
// Timeout prevents hanging on sleep/wake cycles
|
|
68
|
+
req.setTimeout(timeoutMs, () => {
|
|
69
|
+
req.destroy();
|
|
70
|
+
reject(new Error('Request timeout'));
|
|
71
|
+
});
|
|
72
|
+
|
|
63
73
|
req.on('error', reject);
|
|
64
74
|
|
|
65
75
|
if (body) {
|
|
@@ -222,28 +232,47 @@ export class RemoteClient {
|
|
|
222
232
|
|
|
223
233
|
/**
|
|
224
234
|
* Poll for interaction responses
|
|
235
|
+
* Uses 35s timeout to stay under Vercel's 50s limit with buffer
|
|
225
236
|
*/
|
|
226
237
|
async poll() {
|
|
238
|
+
let consecutiveErrors = 0;
|
|
239
|
+
|
|
227
240
|
while (this.polling && this.connected) {
|
|
228
241
|
try {
|
|
242
|
+
// Request 30s poll from server, with 35s client timeout
|
|
229
243
|
const url = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}&timeout=30000`;
|
|
230
|
-
const response = await makeRequest(url, { method: 'GET' });
|
|
244
|
+
const response = await makeRequest(url, { method: 'GET' }, null, 35000);
|
|
245
|
+
|
|
246
|
+
consecutiveErrors = 0; // Reset on success
|
|
231
247
|
|
|
232
248
|
if (response.status === 200 && response.data) {
|
|
233
249
|
const { type, slug, targetKey, response: interactionResponse } = response.data;
|
|
234
250
|
|
|
235
251
|
if (type === 'interaction_response' && this.onInteractionResponse) {
|
|
252
|
+
// Confirm receipt BEFORE processing - removes from Redis pending queue
|
|
253
|
+
// This ensures we don't lose the interaction if processing fails
|
|
254
|
+
try {
|
|
255
|
+
const confirmUrl = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}`;
|
|
256
|
+
await makeRequest(confirmUrl, { method: 'DELETE' }, null, 10000);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
// Non-fatal - interaction will be re-delivered on next poll
|
|
259
|
+
console.error(`${C.dim}Remote: Failed to confirm receipt: ${err.message}${C.reset}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
236
262
|
this.onInteractionResponse(slug, targetKey, interactionResponse);
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
// If 204 (no content), just continue polling
|
|
241
|
-
// Small delay
|
|
242
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
266
|
+
// If 204 (no content), just continue polling immediately
|
|
267
|
+
// Small delay only on success to prevent tight loop
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
243
269
|
|
|
244
270
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
consecutiveErrors++;
|
|
272
|
+
|
|
273
|
+
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
274
|
+
const backoff = Math.min(1000 * Math.pow(2, consecutiveErrors - 1), 10000);
|
|
275
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
247
276
|
}
|
|
248
277
|
}
|
|
249
278
|
}
|
package/lib/runtime/agent.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import { pathToFileURL } from 'url';
|
|
13
13
|
import { getCurrentRuntime } from './runtime.js';
|
|
14
14
|
import { formatInteractionPrompt } from './interaction.js';
|
|
15
|
+
import { withChangeTracking } from './track-changes.js';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
|
|
@@ -164,25 +165,37 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
|
|
|
164
165
|
_config: {
|
|
165
166
|
models: runtime.workflowConfig.models,
|
|
166
167
|
apiKeys: runtime.workflowConfig.apiKeys,
|
|
167
|
-
workflowDir: runtime.workflowDir
|
|
168
|
+
workflowDir: runtime.workflowDir,
|
|
169
|
+
projectRoot: runtime.workflowConfig.projectRoot
|
|
168
170
|
}
|
|
169
171
|
};
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
173
|
+
// Execute handler with optional file change tracking
|
|
174
|
+
const executeHandler = async () => {
|
|
175
|
+
let result = await handler(context);
|
|
176
|
+
let interactionDepth = 0;
|
|
177
|
+
|
|
178
|
+
// Handle interaction response from JS agent (support multiple rounds)
|
|
179
|
+
while (result && result._interaction) {
|
|
180
|
+
const interactionResponse = await handleInteraction(runtime, result._interaction, name);
|
|
181
|
+
const resumedContext = { ...context, userResponse: interactionResponse };
|
|
182
|
+
await logAgentStart(runtime, name);
|
|
183
|
+
result = await handler(resumedContext);
|
|
184
|
+
interactionDepth += 1;
|
|
185
|
+
if (interactionDepth > 5) {
|
|
186
|
+
throw new Error(`Agent ${name} exceeded maximum interaction depth`);
|
|
187
|
+
}
|
|
183
188
|
}
|
|
184
|
-
}
|
|
185
189
|
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let result;
|
|
194
|
+
if (runtime.workflowConfig.fileTracking !== false) {
|
|
195
|
+
result = await withChangeTracking(runtime, name, executeHandler);
|
|
196
|
+
} else {
|
|
197
|
+
result = await executeHandler();
|
|
198
|
+
}
|
|
186
199
|
|
|
187
200
|
// Clean internal properties from result
|
|
188
201
|
if (result && typeof result === 'object') {
|
|
@@ -191,6 +204,7 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
|
|
|
191
204
|
delete cleanResult._config;
|
|
192
205
|
delete cleanResult._loop;
|
|
193
206
|
delete cleanResult._interaction;
|
|
207
|
+
delete cleanResult._files;
|
|
194
208
|
|
|
195
209
|
// If agent returned a context-like object, only return non-internal keys
|
|
196
210
|
const meaningfulKeys = Object.keys(cleanResult).filter((k) => !k.startsWith('_'));
|
|
@@ -235,127 +249,141 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
|
|
|
235
249
|
? runtime.loadSteeringFiles(steeringNames)
|
|
236
250
|
: runtime.steering;
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const context = {
|
|
246
|
-
...currentParams,
|
|
247
|
-
_steering: steeringContext,
|
|
248
|
-
_config: {
|
|
249
|
-
models: runtime.workflowConfig.models,
|
|
250
|
-
apiKeys: runtime.workflowConfig.apiKeys,
|
|
251
|
-
workflowDir: runtime.workflowDir
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// Interpolate variables in prompt
|
|
256
|
-
const interpolatedPrompt = interpolatePrompt(prompt, context);
|
|
252
|
+
// Build base config (used for all iterations)
|
|
253
|
+
const baseConfig = {
|
|
254
|
+
models: runtime.workflowConfig.models,
|
|
255
|
+
apiKeys: runtime.workflowConfig.apiKeys,
|
|
256
|
+
workflowDir: runtime.workflowDir,
|
|
257
|
+
projectRoot: runtime.workflowConfig.projectRoot
|
|
258
|
+
};
|
|
257
259
|
|
|
258
|
-
|
|
260
|
+
// Execute the MD agent core logic
|
|
261
|
+
const executeMDAgentCore = async () => {
|
|
262
|
+
let response = null;
|
|
263
|
+
let output = null;
|
|
264
|
+
let interactionDepth = 0;
|
|
265
|
+
let currentParams = params;
|
|
266
|
+
|
|
267
|
+
while (true) {
|
|
268
|
+
// Build context - only spread params, NOT memory (explicit context passing)
|
|
269
|
+
const context = {
|
|
270
|
+
...currentParams,
|
|
271
|
+
_steering: steeringContext,
|
|
272
|
+
_config: baseConfig
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Interpolate variables in prompt
|
|
276
|
+
const interpolatedPrompt = interpolatePrompt(prompt, context);
|
|
277
|
+
|
|
278
|
+
const model = config.model || 'fast';
|
|
279
|
+
|
|
280
|
+
const fullPrompt = buildPrompt(context, {
|
|
281
|
+
model,
|
|
282
|
+
prompt: interpolatedPrompt,
|
|
283
|
+
includeContext: config.includeContext !== 'false',
|
|
284
|
+
responseType: config.response
|
|
285
|
+
});
|
|
259
286
|
|
|
260
|
-
|
|
261
|
-
model,
|
|
262
|
-
prompt: interpolatedPrompt,
|
|
263
|
-
includeContext: config.includeContext !== 'false',
|
|
264
|
-
responseType: config.response
|
|
265
|
-
});
|
|
287
|
+
await logAgentStart(runtime, name, fullPrompt);
|
|
266
288
|
|
|
267
|
-
|
|
289
|
+
console.log(` Using model: ${model}`);
|
|
268
290
|
|
|
269
|
-
|
|
291
|
+
response = await llm(context, {
|
|
292
|
+
model: model,
|
|
293
|
+
prompt: interpolatedPrompt,
|
|
294
|
+
includeContext: config.includeContext !== 'false',
|
|
295
|
+
responseType: config.response
|
|
296
|
+
});
|
|
270
297
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
// Parse output based on format
|
|
299
|
+
output = response.text;
|
|
300
|
+
if (config.format === 'json') {
|
|
301
|
+
try {
|
|
302
|
+
output = parseJSON(response.text);
|
|
303
|
+
} catch {
|
|
304
|
+
console.warn(` Warning: Failed to parse JSON output`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
277
307
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
308
|
+
if (output && typeof output === 'object' && output._interaction) {
|
|
309
|
+
const interactionResponse = await handleInteraction(runtime, output._interaction, name);
|
|
310
|
+
currentParams = { ...params, userResponse: interactionResponse };
|
|
311
|
+
interactionDepth += 1;
|
|
312
|
+
if (interactionDepth > 5) {
|
|
313
|
+
throw new Error(`Agent ${name} exceeded maximum interaction depth`);
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
285
316
|
}
|
|
317
|
+
|
|
318
|
+
break;
|
|
286
319
|
}
|
|
287
320
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
321
|
+
// Check for interaction request
|
|
322
|
+
const parsedInteraction = parseInteractionRequest(response.text);
|
|
323
|
+
const structuredInteraction =
|
|
324
|
+
config.autoInteract !== 'false' && parsedInteraction.isInteraction;
|
|
325
|
+
|
|
326
|
+
// Check if agent returned an 'interact' object in its JSON response
|
|
327
|
+
const hasInteractKey = output && typeof output === 'object' && output.interact;
|
|
328
|
+
|
|
329
|
+
// Explicit interaction mode (format: interaction OR interaction: true)
|
|
330
|
+
// But only trigger if agent actually wants to interact (has interact key or parsed interaction)
|
|
331
|
+
const explicitInteraction =
|
|
332
|
+
config.format === 'interaction' ||
|
|
333
|
+
((config.interaction === 'true' || (typeof config.interaction === 'string' && config.interaction.length > 0)) &&
|
|
334
|
+
(hasInteractKey || structuredInteraction));
|
|
335
|
+
|
|
336
|
+
if (explicitInteraction || structuredInteraction) {
|
|
337
|
+
// Use interact object if present, otherwise fall back to parsed/raw
|
|
338
|
+
const interactionData = hasInteractKey ? output.interact : (structuredInteraction ? parsedInteraction : null);
|
|
339
|
+
|
|
340
|
+
const slugRaw =
|
|
341
|
+
interactionData?.slug ||
|
|
342
|
+
(typeof config.interaction === 'string' && config.interaction !== 'true'
|
|
343
|
+
? config.interaction
|
|
344
|
+
: null) ||
|
|
345
|
+
config.interactionSlug ||
|
|
346
|
+
config.interactionKey ||
|
|
347
|
+
name;
|
|
348
|
+
|
|
349
|
+
const slug = sanitizeSlug(slugRaw);
|
|
350
|
+
const targetKey = config.interactionKey || outputKey || slug;
|
|
351
|
+
|
|
352
|
+
// Build interaction object with full metadata
|
|
353
|
+
const interactionObj = hasInteractKey ? {
|
|
354
|
+
...output.interact,
|
|
355
|
+
slug,
|
|
356
|
+
targetKey
|
|
357
|
+
} : {
|
|
358
|
+
slug,
|
|
359
|
+
targetKey,
|
|
360
|
+
content: structuredInteraction ? parsedInteraction.question : response.text
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const userResponse = await handleInteraction(runtime, interactionObj, name);
|
|
364
|
+
|
|
365
|
+
// Return the user's response as the agent result
|
|
366
|
+
if (outputKey) {
|
|
367
|
+
return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
|
|
294
368
|
}
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
369
|
|
|
298
|
-
|
|
299
|
-
|
|
370
|
+
return userResponse;
|
|
371
|
+
}
|
|
300
372
|
|
|
301
|
-
|
|
302
|
-
const parsedInteraction = parseInteractionRequest(response.text);
|
|
303
|
-
const structuredInteraction =
|
|
304
|
-
config.autoInteract !== 'false' && parsedInteraction.isInteraction;
|
|
305
|
-
|
|
306
|
-
// Check if agent returned an 'interact' object in its JSON response
|
|
307
|
-
const hasInteractKey = output && typeof output === 'object' && output.interact;
|
|
308
|
-
|
|
309
|
-
// Explicit interaction mode (format: interaction OR interaction: true)
|
|
310
|
-
// But only trigger if agent actually wants to interact (has interact key or parsed interaction)
|
|
311
|
-
const explicitInteraction =
|
|
312
|
-
config.format === 'interaction' ||
|
|
313
|
-
((config.interaction === 'true' || (typeof config.interaction === 'string' && config.interaction.length > 0)) &&
|
|
314
|
-
(hasInteractKey || structuredInteraction));
|
|
315
|
-
|
|
316
|
-
if (explicitInteraction || structuredInteraction) {
|
|
317
|
-
// Use interact object if present, otherwise fall back to parsed/raw
|
|
318
|
-
const interactionData = hasInteractKey ? output.interact : (structuredInteraction ? parsedInteraction : null);
|
|
319
|
-
|
|
320
|
-
const slugRaw =
|
|
321
|
-
interactionData?.slug ||
|
|
322
|
-
(typeof config.interaction === 'string' && config.interaction !== 'true'
|
|
323
|
-
? config.interaction
|
|
324
|
-
: null) ||
|
|
325
|
-
config.interactionSlug ||
|
|
326
|
-
config.interactionKey ||
|
|
327
|
-
name;
|
|
328
|
-
|
|
329
|
-
const slug = sanitizeSlug(slugRaw);
|
|
330
|
-
const targetKey = config.interactionKey || outputKey || slug;
|
|
331
|
-
|
|
332
|
-
// Build interaction object with full metadata
|
|
333
|
-
const interactionObj = hasInteractKey ? {
|
|
334
|
-
...output.interact,
|
|
335
|
-
slug,
|
|
336
|
-
targetKey
|
|
337
|
-
} : {
|
|
338
|
-
slug,
|
|
339
|
-
targetKey,
|
|
340
|
-
content: structuredInteraction ? parsedInteraction.question : response.text
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const userResponse = await handleInteraction(runtime, interactionObj, name);
|
|
344
|
-
|
|
345
|
-
// Return the user's response as the agent result
|
|
373
|
+
// Return result object
|
|
346
374
|
if (outputKey) {
|
|
347
|
-
return { [outputKey]:
|
|
375
|
+
return { [outputKey]: output, _debug_prompt: response.fullPrompt };
|
|
348
376
|
}
|
|
349
377
|
|
|
350
|
-
return
|
|
351
|
-
}
|
|
378
|
+
return output;
|
|
379
|
+
};
|
|
352
380
|
|
|
353
|
-
//
|
|
354
|
-
if (
|
|
355
|
-
return
|
|
381
|
+
// Execute with optional file change tracking
|
|
382
|
+
if (runtime.workflowConfig.fileTracking !== false) {
|
|
383
|
+
return withChangeTracking(runtime, name, executeMDAgentCore);
|
|
384
|
+
} else {
|
|
385
|
+
return executeMDAgentCore();
|
|
356
386
|
}
|
|
357
|
-
|
|
358
|
-
return output;
|
|
359
387
|
}
|
|
360
388
|
|
|
361
389
|
/**
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ import readline from 'readline';
|
|
|
12
12
|
import { pathToFileURL } from 'url';
|
|
13
13
|
import { createMemoryProxy } from './memory.js';
|
|
14
14
|
import { RemoteClient } from '../remote/client.js';
|
|
15
|
+
import { DEFAULT_IGNORE } from '../file-tree.js';
|
|
15
16
|
|
|
16
17
|
// Global runtime reference for agent() and memory access
|
|
17
18
|
// stored on globalThis to ensure singleton access across different module instances (CLI vs local)
|
|
@@ -74,7 +75,12 @@ export class WorkflowRuntime {
|
|
|
74
75
|
this.workflowConfig = {
|
|
75
76
|
models: {},
|
|
76
77
|
apiKeys: {},
|
|
77
|
-
description: ''
|
|
78
|
+
description: '',
|
|
79
|
+
// File tracking defaults
|
|
80
|
+
projectRoot: path.resolve(workflowDir, '../..'),
|
|
81
|
+
fileTracking: true,
|
|
82
|
+
fileTrackingIgnore: DEFAULT_IGNORE,
|
|
83
|
+
fileTrackingKeepDeleted: false
|
|
78
84
|
};
|
|
79
85
|
|
|
80
86
|
// Load steering
|
|
@@ -317,7 +323,12 @@ export class WorkflowRuntime {
|
|
|
317
323
|
this.workflowConfig = {
|
|
318
324
|
models: cfg.models || {},
|
|
319
325
|
apiKeys: cfg.apiKeys || {},
|
|
320
|
-
description: cfg.description || ''
|
|
326
|
+
description: cfg.description || '',
|
|
327
|
+
// File tracking configuration
|
|
328
|
+
projectRoot: cfg.projectRoot || path.resolve(this.workflowDir, '../..'),
|
|
329
|
+
fileTracking: cfg.fileTracking ?? true,
|
|
330
|
+
fileTrackingIgnore: cfg.fileTrackingIgnore || DEFAULT_IGNORE,
|
|
331
|
+
fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false
|
|
321
332
|
};
|
|
322
333
|
|
|
323
334
|
// Import workflow module
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/track-changes.js
|
|
3
|
+
*
|
|
4
|
+
* Wraps agent execution with file change tracking.
|
|
5
|
+
* Captures baseline before agent runs, detects changes after,
|
|
6
|
+
* and updates memory.fileTree with the results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import {
|
|
11
|
+
captureBaseline,
|
|
12
|
+
detectChanges,
|
|
13
|
+
normalizePath,
|
|
14
|
+
extractExportsFromFile,
|
|
15
|
+
DEFAULT_IGNORE
|
|
16
|
+
} from '../file-tree.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap an async function with file change tracking.
|
|
20
|
+
* Captures baseline before execution, detects changes after,
|
|
21
|
+
* and updates the runtime's fileTree.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} runtime - The workflow runtime instance
|
|
24
|
+
* @param {string} agentName - Name of the agent (for attribution)
|
|
25
|
+
* @param {Function} fn - Async function to execute
|
|
26
|
+
* @returns {Promise<any>} - Result of the function
|
|
27
|
+
*/
|
|
28
|
+
export async function withChangeTracking(runtime, agentName, fn) {
|
|
29
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
30
|
+
const ignorePatterns = runtime.workflowConfig.fileTrackingIgnore || DEFAULT_IGNORE;
|
|
31
|
+
|
|
32
|
+
// Capture baseline before agent runs
|
|
33
|
+
const baseline = await captureBaseline(projectRoot, ignorePatterns);
|
|
34
|
+
|
|
35
|
+
// Run the agent
|
|
36
|
+
const result = await fn();
|
|
37
|
+
|
|
38
|
+
// Detect changes made during agent execution
|
|
39
|
+
const changes = await detectChanges(projectRoot, baseline, ignorePatterns);
|
|
40
|
+
|
|
41
|
+
// Update fileTree with detected changes
|
|
42
|
+
applyChangesToFileTree(runtime, changes, agentName);
|
|
43
|
+
|
|
44
|
+
// Merge _files annotations if present (preserves existing data unless explicitly overwritten)
|
|
45
|
+
if (result && typeof result === 'object' && Array.isArray(result._files)) {
|
|
46
|
+
mergeAnnotations(runtime, result._files);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply detected file changes to the runtime's fileTree.
|
|
54
|
+
*/
|
|
55
|
+
function applyChangesToFileTree(runtime, changes, agentName) {
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
58
|
+
|
|
59
|
+
// Initialize fileTree if needed
|
|
60
|
+
if (!runtime._rawMemory.fileTree) {
|
|
61
|
+
runtime._rawMemory.fileTree = {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle created files
|
|
65
|
+
for (const filePath of changes.created) {
|
|
66
|
+
try {
|
|
67
|
+
const normalized = normalizePath(filePath, projectRoot);
|
|
68
|
+
runtime._rawMemory.fileTree[normalized] = {
|
|
69
|
+
path: normalized,
|
|
70
|
+
status: 'created',
|
|
71
|
+
createdBy: agentName,
|
|
72
|
+
lastModifiedBy: agentName,
|
|
73
|
+
createdAt: now,
|
|
74
|
+
updatedAt: now
|
|
75
|
+
};
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Skip files with invalid paths
|
|
78
|
+
console.warn(`[file-tree] Skipping invalid path: ${filePath} - ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle modified files
|
|
83
|
+
for (const filePath of changes.modified) {
|
|
84
|
+
try {
|
|
85
|
+
const normalized = normalizePath(filePath, projectRoot);
|
|
86
|
+
const existing = runtime._rawMemory.fileTree[normalized] || {};
|
|
87
|
+
runtime._rawMemory.fileTree[normalized] = {
|
|
88
|
+
...existing,
|
|
89
|
+
path: normalized,
|
|
90
|
+
status: 'modified',
|
|
91
|
+
lastModifiedBy: agentName,
|
|
92
|
+
updatedAt: now,
|
|
93
|
+
createdAt: existing.createdAt || now,
|
|
94
|
+
createdBy: existing.createdBy || agentName
|
|
95
|
+
};
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn(`[file-tree] Skipping invalid path: ${filePath} - ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle renamed files (MVP: from/to pairs)
|
|
102
|
+
for (const { from, to } of changes.renamed) {
|
|
103
|
+
try {
|
|
104
|
+
const normalizedFrom = normalizePath(from, projectRoot);
|
|
105
|
+
const normalizedTo = normalizePath(to, projectRoot);
|
|
106
|
+
const existing = runtime._rawMemory.fileTree[normalizedFrom] || {};
|
|
107
|
+
delete runtime._rawMemory.fileTree[normalizedFrom];
|
|
108
|
+
runtime._rawMemory.fileTree[normalizedTo] = {
|
|
109
|
+
...existing,
|
|
110
|
+
path: normalizedTo,
|
|
111
|
+
status: 'renamed',
|
|
112
|
+
renamedFrom: normalizedFrom,
|
|
113
|
+
lastModifiedBy: agentName,
|
|
114
|
+
updatedAt: now
|
|
115
|
+
};
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.warn(`[file-tree] Skipping invalid rename: ${from} -> ${to} - ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle deleted files
|
|
122
|
+
for (const filePath of changes.deleted) {
|
|
123
|
+
try {
|
|
124
|
+
const normalized = normalizePath(filePath, projectRoot);
|
|
125
|
+
if (runtime.workflowConfig.fileTrackingKeepDeleted) {
|
|
126
|
+
runtime._rawMemory.fileTree[normalized] = {
|
|
127
|
+
...runtime._rawMemory.fileTree[normalized],
|
|
128
|
+
status: 'deleted',
|
|
129
|
+
deletedBy: agentName,
|
|
130
|
+
deletedAt: now
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
delete runtime._rawMemory.fileTree[normalized];
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.warn(`[file-tree] Skipping invalid path: ${filePath} - ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Trigger persistence
|
|
141
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Merge _files annotations from agent result into fileTree.
|
|
146
|
+
* Only annotates files that were actually detected by change tracking.
|
|
147
|
+
* Preserves existing values unless explicitly overwritten.
|
|
148
|
+
*/
|
|
149
|
+
function mergeAnnotations(runtime, files) {
|
|
150
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
151
|
+
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
if (!file.path) continue;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const normalizedPath = normalizePath(file.path, projectRoot);
|
|
157
|
+
const existing = runtime._rawMemory.fileTree?.[normalizedPath];
|
|
158
|
+
|
|
159
|
+
if (!existing) continue; // Only annotate files that were actually detected
|
|
160
|
+
|
|
161
|
+
// Preserve existing values unless _files explicitly provides new ones
|
|
162
|
+
if (file.caption !== undefined) existing.caption = file.caption;
|
|
163
|
+
if (file.exports !== undefined) existing.exports = file.exports;
|
|
164
|
+
if (file.metadata !== undefined) {
|
|
165
|
+
existing.metadata = { ...existing.metadata, ...file.metadata };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Trigger best-effort export extraction if requested
|
|
169
|
+
if (file.extractExports) {
|
|
170
|
+
const absolutePath = path.resolve(projectRoot, normalizedPath);
|
|
171
|
+
const exports = extractExportsFromFile(absolutePath);
|
|
172
|
+
if (exports) existing.exports = exports;
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.warn(`[file-tree] Skipping invalid annotation path: ${file.path} - ${e.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Trigger persistence
|
|
180
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Manually track a file in the fileTree.
|
|
185
|
+
* Used for files created outside of agent execution.
|
|
186
|
+
*/
|
|
187
|
+
export function trackFile(runtime, relativePath, options = {}) {
|
|
188
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
189
|
+
const normalized = normalizePath(relativePath, projectRoot);
|
|
190
|
+
const now = new Date().toISOString();
|
|
191
|
+
const agentName = options.agentName || 'manual';
|
|
192
|
+
|
|
193
|
+
// Initialize fileTree if needed
|
|
194
|
+
if (!runtime._rawMemory.fileTree) {
|
|
195
|
+
runtime._rawMemory.fileTree = {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const existing = runtime._rawMemory.fileTree[normalized];
|
|
199
|
+
|
|
200
|
+
const entry = {
|
|
201
|
+
path: normalized,
|
|
202
|
+
status: existing?.status || 'created',
|
|
203
|
+
createdBy: existing?.createdBy || agentName,
|
|
204
|
+
lastModifiedBy: agentName,
|
|
205
|
+
createdAt: existing?.createdAt || now,
|
|
206
|
+
updatedAt: now
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Apply options
|
|
210
|
+
if (options.caption !== undefined) entry.caption = options.caption;
|
|
211
|
+
if (options.exports !== undefined) entry.exports = options.exports;
|
|
212
|
+
if (options.metadata !== undefined) {
|
|
213
|
+
entry.metadata = { ...existing?.metadata, ...options.metadata };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Extract exports if requested
|
|
217
|
+
if (options.extractExports) {
|
|
218
|
+
const absolutePath = path.resolve(projectRoot, normalized);
|
|
219
|
+
const exports = extractExportsFromFile(absolutePath);
|
|
220
|
+
if (exports) entry.exports = exports;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
runtime._rawMemory.fileTree[normalized] = entry;
|
|
224
|
+
|
|
225
|
+
// Trigger persistence
|
|
226
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
227
|
+
|
|
228
|
+
return entry;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the current fileTree.
|
|
233
|
+
*/
|
|
234
|
+
export function getFileTree(runtime) {
|
|
235
|
+
return runtime._rawMemory.fileTree || {};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Remove a file from tracking.
|
|
240
|
+
*/
|
|
241
|
+
export function untrackFile(runtime, relativePath) {
|
|
242
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
243
|
+
const normalized = normalizePath(relativePath, projectRoot);
|
|
244
|
+
|
|
245
|
+
if (runtime._rawMemory.fileTree?.[normalized]) {
|
|
246
|
+
delete runtime._rawMemory.fileTree[normalized];
|
|
247
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return false;
|
|
252
|
+
}
|