agentgui 1.0.31 → 1.0.33

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.
@@ -0,0 +1,360 @@
1
+ /**
2
+ * StateManager - Explicit state machine for all prompt processing
3
+ * Ensures predictable, auditable state transitions with no surprises
4
+ */
5
+
6
+ export class StateManager {
7
+ // Valid session states
8
+ static STATES = {
9
+ PENDING: 'pending',
10
+ ACQUIRING_ACP: 'acquiring_acp',
11
+ ACP_ACQUIRED: 'acp_acquired',
12
+ SENDING_PROMPT: 'sending_prompt',
13
+ PROCESSING: 'processing',
14
+ COMPLETED: 'completed',
15
+ ERROR: 'error',
16
+ TIMEOUT: 'timeout',
17
+ CANCELLED: 'cancelled'
18
+ };
19
+
20
+ // Valid state transitions - only these are allowed
21
+ static VALID_TRANSITIONS = {
22
+ [this.STATES.PENDING]: [
23
+ this.STATES.ACQUIRING_ACP,
24
+ this.STATES.CANCELLED
25
+ ],
26
+ [this.STATES.ACQUIRING_ACP]: [
27
+ this.STATES.ACP_ACQUIRED,
28
+ this.STATES.ERROR,
29
+ this.STATES.TIMEOUT,
30
+ this.STATES.CANCELLED
31
+ ],
32
+ [this.STATES.ACP_ACQUIRED]: [
33
+ this.STATES.SENDING_PROMPT,
34
+ this.STATES.ERROR,
35
+ this.STATES.TIMEOUT,
36
+ this.STATES.CANCELLED
37
+ ],
38
+ [this.STATES.SENDING_PROMPT]: [
39
+ this.STATES.PROCESSING,
40
+ this.STATES.ERROR,
41
+ this.STATES.TIMEOUT,
42
+ this.STATES.CANCELLED
43
+ ],
44
+ [this.STATES.PROCESSING]: [
45
+ this.STATES.COMPLETED,
46
+ this.STATES.ERROR,
47
+ this.STATES.TIMEOUT,
48
+ this.STATES.CANCELLED
49
+ ],
50
+ [this.STATES.COMPLETED]: [],
51
+ [this.STATES.ERROR]: [],
52
+ [this.STATES.TIMEOUT]: [],
53
+ [this.STATES.CANCELLED]: []
54
+ };
55
+
56
+ constructor(sessionId, conversationId, messageId, timeout = 120000) {
57
+ this.sessionId = sessionId;
58
+ this.conversationId = conversationId;
59
+ this.messageId = messageId;
60
+ this.timeout = timeout;
61
+
62
+ // State tracking
63
+ this.state = this.constructor.STATES.PENDING;
64
+ this.previousState = null;
65
+ this.stateHistory = [{ state: this.state, timestamp: Date.now(), reason: 'initialized' }];
66
+
67
+ // Data tracking
68
+ this.data = {
69
+ acpConnectionTime: null,
70
+ promptSentTime: null,
71
+ responseReceivedTime: null,
72
+ fullText: '',
73
+ blocks: [],
74
+ error: null,
75
+ stackTrace: null
76
+ };
77
+
78
+ // Promise resolution
79
+ this.promiseResolve = null;
80
+ this.promiseReject = null;
81
+ this.completionPromise = new Promise((resolve, reject) => {
82
+ this.promiseResolve = resolve;
83
+ this.promiseReject = reject;
84
+ });
85
+
86
+ // Start timeout
87
+ this.startTimeout();
88
+
89
+ console.log(`[StateManager] Session ${sessionId} initialized (timeout: ${timeout}ms)`);
90
+ }
91
+
92
+ /**
93
+ * Transition to a new state with validation
94
+ * @param {string} newState - Target state
95
+ * @param {object} data - State-specific data
96
+ * @throws {Error} If transition is invalid
97
+ */
98
+ transition(newState, data = {}) {
99
+ const validTransitions = this.constructor.VALID_TRANSITIONS[this.state] || [];
100
+
101
+ if (!validTransitions.includes(newState)) {
102
+ const error = `Invalid state transition: ${this.state} → ${newState}. Valid: [${validTransitions.join(', ')}]`;
103
+ console.error(`[StateManager] ${error}`);
104
+ throw new Error(error);
105
+ }
106
+
107
+ this.previousState = this.state;
108
+ this.state = newState;
109
+
110
+ // Record transition
111
+ this.stateHistory.push({
112
+ state: newState,
113
+ timestamp: Date.now(),
114
+ reason: data.reason || 'manual transition',
115
+ details: data.details || {}
116
+ });
117
+
118
+ // Update data
119
+ Object.assign(this.data, data.data || {});
120
+
121
+ // Log transition
122
+ const duration = this.stateHistory.length > 1
123
+ ? Date.now() - this.stateHistory[this.stateHistory.length - 2].timestamp
124
+ : 0;
125
+
126
+ console.log(`[StateManager] ${this.sessionId} transitioned: ${this.previousState} → ${newState} (+${duration}ms) | ${data.reason || ''}`);
127
+
128
+ // Handle terminal states
129
+ if (newState === this.constructor.STATES.COMPLETED) {
130
+ this.completeSuccess(data.data);
131
+ } else if (newState === this.constructor.STATES.ERROR) {
132
+ this.completeError(data.data?.error, data.data?.stackTrace);
133
+ } else if (newState === this.constructor.STATES.TIMEOUT) {
134
+ this.completeError('Operation timeout', data.data?.stackTrace);
135
+ } else if (newState === this.constructor.STATES.CANCELLED) {
136
+ this.completeError('Operation cancelled', null);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Start timeout watchdog
142
+ */
143
+ startTimeout() {
144
+ this.timeoutHandle = setTimeout(() => {
145
+ if (![
146
+ this.constructor.STATES.COMPLETED,
147
+ this.constructor.STATES.ERROR,
148
+ this.constructor.STATES.CANCELLED,
149
+ this.constructor.STATES.TIMEOUT
150
+ ].includes(this.state)) {
151
+ console.error(`[StateManager] ${this.sessionId} TIMEOUT after ${this.timeout}ms in state: ${this.state}`);
152
+ this.transition(this.constructor.STATES.TIMEOUT, {
153
+ reason: 'timeout watchdog fired',
154
+ data: { error: 'Operation exceeded timeout', timeout: this.timeout }
155
+ });
156
+ }
157
+ }, this.timeout);
158
+ }
159
+
160
+ /**
161
+ * Cancel the timeout
162
+ */
163
+ cancelTimeout() {
164
+ if (this.timeoutHandle) {
165
+ clearTimeout(this.timeoutHandle);
166
+ this.timeoutHandle = null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Mark as successfully completed
172
+ */
173
+ completeSuccess(data) {
174
+ this.cancelTimeout();
175
+ this.data = { ...this.data, ...data };
176
+ if (this.promiseResolve) {
177
+ this.promiseResolve({ state: this.state, data: this.data });
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Mark as failed
183
+ */
184
+ completeError(error, stackTrace) {
185
+ this.cancelTimeout();
186
+ this.data.error = error;
187
+ this.data.stackTrace = stackTrace;
188
+ if (this.promiseReject) {
189
+ this.promiseReject(new Error(`Session failed: ${error}`));
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get current state
195
+ */
196
+ getState() {
197
+ return this.state;
198
+ }
199
+
200
+ /**
201
+ * Get full state history
202
+ */
203
+ getHistory() {
204
+ return this.stateHistory;
205
+ }
206
+
207
+ /**
208
+ * Get human-readable summary
209
+ */
210
+ getSummary() {
211
+ const duration = this.stateHistory[this.stateHistory.length - 1].timestamp - this.stateHistory[0].timestamp;
212
+ return {
213
+ sessionId: this.sessionId,
214
+ conversationId: this.conversationId,
215
+ messageId: this.messageId,
216
+ state: this.state,
217
+ previousState: this.previousState,
218
+ duration: `${duration}ms`,
219
+ historyLength: this.stateHistory.length,
220
+ history: this.stateHistory.map(h => `${h.timestamp - this.stateHistory[0].timestamp}ms: ${h.state} (${h.reason})`),
221
+ data: {
222
+ fullTextLength: this.data.fullText.length,
223
+ blocksCount: this.data.blocks.length,
224
+ error: this.data.error,
225
+ hasStackTrace: !!this.data.stackTrace
226
+ }
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Wait for completion
232
+ */
233
+ async waitForCompletion() {
234
+ return this.completionPromise;
235
+ }
236
+
237
+ /**
238
+ * Check if session is in a terminal state
239
+ */
240
+ isTerminal() {
241
+ return [
242
+ this.constructor.STATES.COMPLETED,
243
+ this.constructor.STATES.ERROR,
244
+ this.constructor.STATES.TIMEOUT,
245
+ this.constructor.STATES.CANCELLED
246
+ ].includes(this.state);
247
+ }
248
+
249
+ /**
250
+ * Check if session is in a running state
251
+ */
252
+ isRunning() {
253
+ return !this.isTerminal();
254
+ }
255
+
256
+ /**
257
+ * Assert session is in specific state
258
+ */
259
+ assertState(expectedState) {
260
+ if (this.state !== expectedState) {
261
+ throw new Error(`Expected state ${expectedState}, got ${this.state}`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Assert session can transition to state
267
+ */
268
+ assertCanTransition(targetState) {
269
+ const validTransitions = this.constructor.VALID_TRANSITIONS[this.state] || [];
270
+ if (!validTransitions.includes(targetState)) {
271
+ throw new Error(`Cannot transition from ${this.state} to ${targetState}`);
272
+ }
273
+ }
274
+ }
275
+
276
+ export class SessionStateStore {
277
+ constructor() {
278
+ this.sessions = new Map(); // sessionId -> StateManager
279
+ }
280
+
281
+ create(sessionId, conversationId, messageId, timeout) {
282
+ const stateManager = new StateManager(sessionId, conversationId, messageId, timeout);
283
+ this.sessions.set(sessionId, stateManager);
284
+ return stateManager;
285
+ }
286
+
287
+ get(sessionId) {
288
+ return this.sessions.get(sessionId);
289
+ }
290
+
291
+ getOrThrow(sessionId) {
292
+ const manager = this.sessions.get(sessionId);
293
+ if (!manager) {
294
+ throw new Error(`Session ${sessionId} not found in state store`);
295
+ }
296
+ return manager;
297
+ }
298
+
299
+ remove(sessionId) {
300
+ const manager = this.sessions.get(sessionId);
301
+ if (manager) {
302
+ manager.cancelTimeout();
303
+ this.sessions.delete(sessionId);
304
+ }
305
+ }
306
+
307
+ getAll() {
308
+ return Array.from(this.sessions.values());
309
+ }
310
+
311
+ getAllActive() {
312
+ return this.getAll().filter(m => m.isRunning());
313
+ }
314
+
315
+ getAllTerminal() {
316
+ return this.getAll().filter(m => m.isTerminal());
317
+ }
318
+
319
+ /**
320
+ * Get diagnostic summary of all sessions
321
+ */
322
+ getDiagnostics() {
323
+ const active = this.getAllActive();
324
+ const terminal = this.getAllTerminal();
325
+ return {
326
+ timestamp: new Date().toISOString(),
327
+ activeSessions: active.length,
328
+ terminalSessions: terminal.length,
329
+ totalSessions: this.sessions.size,
330
+ active: active.map(m => ({
331
+ sessionId: m.sessionId,
332
+ state: m.state,
333
+ uptime: Date.now() - m.stateHistory[0].timestamp
334
+ })),
335
+ recentTerminal: terminal.slice(-5).map(m => m.getSummary())
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Cleanup old terminal sessions (older than ttl)
341
+ */
342
+ cleanup(ttl = 3600000) {
343
+ const now = Date.now();
344
+ const toDelete = [];
345
+
346
+ for (const [sessionId, manager] of this.sessions) {
347
+ if (manager.isTerminal()) {
348
+ const age = now - manager.stateHistory[manager.stateHistory.length - 1].timestamp;
349
+ if (age > ttl) {
350
+ toDelete.push(sessionId);
351
+ }
352
+ }
353
+ }
354
+
355
+ toDelete.forEach(sessionId => this.remove(sessionId));
356
+ if (toDelete.length > 0) {
357
+ console.log(`[SessionStateStore] Cleaned up ${toDelete.length} old sessions`);
358
+ }
359
+ }
360
+ }