agentgui 1.0.795 → 1.0.797
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/lib/model-download-machine.js +107 -0
- package/lib/rate-limit-machine.js +99 -0
- package/lib/script-machine.js +81 -0
- package/lib/ws-handlers-scripts.js +16 -9
- package/package.json +1 -1
- package/server.js +20 -9
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'model-download',
|
|
5
|
+
initial: 'idle',
|
|
6
|
+
context: {
|
|
7
|
+
progress: null,
|
|
8
|
+
error: null,
|
|
9
|
+
startTime: null,
|
|
10
|
+
waiters: [],
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
idle: {
|
|
14
|
+
on: {
|
|
15
|
+
START: {
|
|
16
|
+
target: 'downloading',
|
|
17
|
+
actions: assign({ startTime: () => Date.now(), error: null }),
|
|
18
|
+
},
|
|
19
|
+
WAIT: {
|
|
20
|
+
actions: assign(({ context, event }) => ({
|
|
21
|
+
waiters: [...context.waiters, event.resolve],
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
downloading: {
|
|
27
|
+
on: {
|
|
28
|
+
PROGRESS: {
|
|
29
|
+
actions: assign(({ event }) => ({ progress: event.progress })),
|
|
30
|
+
},
|
|
31
|
+
COMPLETE: { target: 'complete' },
|
|
32
|
+
ERROR: {
|
|
33
|
+
target: 'error',
|
|
34
|
+
actions: assign(({ event }) => ({ error: event.error })),
|
|
35
|
+
},
|
|
36
|
+
WAIT: {
|
|
37
|
+
actions: assign(({ context, event }) => ({
|
|
38
|
+
waiters: [...context.waiters, event.resolve],
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
complete: {
|
|
44
|
+
entry: assign(({ context }) => {
|
|
45
|
+
for (const resolve of context.waiters) resolve(true);
|
|
46
|
+
return { waiters: [] };
|
|
47
|
+
}),
|
|
48
|
+
on: {
|
|
49
|
+
RESET: { target: 'idle' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
error: {
|
|
53
|
+
entry: assign(({ context }) => {
|
|
54
|
+
for (const resolve of context.waiters) resolve(false);
|
|
55
|
+
return { waiters: [] };
|
|
56
|
+
}),
|
|
57
|
+
on: {
|
|
58
|
+
RESET: { target: 'idle' },
|
|
59
|
+
START: {
|
|
60
|
+
target: 'downloading',
|
|
61
|
+
actions: assign({ startTime: () => Date.now(), error: null }),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let actor = null;
|
|
69
|
+
|
|
70
|
+
export function getActor() {
|
|
71
|
+
if (!actor) {
|
|
72
|
+
actor = createActor(machine);
|
|
73
|
+
actor.start();
|
|
74
|
+
}
|
|
75
|
+
return actor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function send(event) {
|
|
79
|
+
return getActor().send(event);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function snapshot() {
|
|
83
|
+
return getActor().getSnapshot();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isDownloading() {
|
|
87
|
+
const s = snapshot();
|
|
88
|
+
return s.value === 'downloading';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isComplete() {
|
|
92
|
+
const s = snapshot();
|
|
93
|
+
return s.value === 'complete';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isError() {
|
|
97
|
+
const s = snapshot();
|
|
98
|
+
return s.value === 'error';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getState() {
|
|
102
|
+
return snapshot().value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getContext() {
|
|
106
|
+
return snapshot().context;
|
|
107
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'rate-limit',
|
|
5
|
+
initial: 'normal',
|
|
6
|
+
context: {
|
|
7
|
+
retryCount: 0,
|
|
8
|
+
retryAt: null,
|
|
9
|
+
cooldownMs: null,
|
|
10
|
+
isStreamDetected: false,
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
normal: {
|
|
14
|
+
entry: assign({ retryCount: 0, retryAt: null, cooldownMs: null, isStreamDetected: false }),
|
|
15
|
+
on: {
|
|
16
|
+
HIT: {
|
|
17
|
+
target: 'limited',
|
|
18
|
+
actions: assign(({ context, event }) => ({
|
|
19
|
+
retryCount: context.retryCount + 1,
|
|
20
|
+
retryAt: event.retryAt,
|
|
21
|
+
cooldownMs: event.cooldownMs,
|
|
22
|
+
isStreamDetected: event.isStreamDetected || false,
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
limited: {
|
|
28
|
+
on: {
|
|
29
|
+
CLEAR: [
|
|
30
|
+
{ guard: ({ context }) => context.retryCount >= 3, target: 'exceeded' },
|
|
31
|
+
{ target: 'normal' },
|
|
32
|
+
],
|
|
33
|
+
HIT: {
|
|
34
|
+
actions: assign(({ context, event }) => ({
|
|
35
|
+
retryCount: context.retryCount + 1,
|
|
36
|
+
retryAt: event.retryAt,
|
|
37
|
+
cooldownMs: event.cooldownMs,
|
|
38
|
+
isStreamDetected: event.isStreamDetected || false,
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
exceeded: {
|
|
44
|
+
on: {
|
|
45
|
+
RESET: { target: 'normal' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const actors = new Map();
|
|
52
|
+
|
|
53
|
+
export function getOrCreate(convId) {
|
|
54
|
+
if (actors.has(convId)) return actors.get(convId);
|
|
55
|
+
const actor = createActor(machine);
|
|
56
|
+
actor.start();
|
|
57
|
+
actors.set(convId, actor);
|
|
58
|
+
return actor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function get(convId) {
|
|
62
|
+
return actors.get(convId) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function remove(convId) {
|
|
66
|
+
const actor = actors.get(convId);
|
|
67
|
+
if (actor) { actor.stop(); actors.delete(convId); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function snapshot(convId) {
|
|
71
|
+
const actor = actors.get(convId);
|
|
72
|
+
return actor ? actor.getSnapshot() : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function send(convId, event) {
|
|
76
|
+
const actor = getOrCreate(convId);
|
|
77
|
+
actor.send(event);
|
|
78
|
+
return actor.getSnapshot();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isLimited(convId) {
|
|
82
|
+
const s = snapshot(convId);
|
|
83
|
+
return s ? s.value === 'limited' : false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isExceeded(convId) {
|
|
87
|
+
const s = snapshot(convId);
|
|
88
|
+
return s ? s.value === 'exceeded' : false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getContext(convId) {
|
|
92
|
+
const s = snapshot(convId);
|
|
93
|
+
return s ? s.context : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function has(convId) {
|
|
97
|
+
const s = snapshot(convId);
|
|
98
|
+
return s ? s.value !== 'normal' : false;
|
|
99
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
2
|
+
|
|
3
|
+
const machine = createMachine({
|
|
4
|
+
id: 'script',
|
|
5
|
+
initial: 'idle',
|
|
6
|
+
context: {
|
|
7
|
+
process: null,
|
|
8
|
+
script: null,
|
|
9
|
+
startTime: null,
|
|
10
|
+
pid: null,
|
|
11
|
+
},
|
|
12
|
+
states: {
|
|
13
|
+
idle: {
|
|
14
|
+
entry: assign({ process: null, script: null, startTime: null, pid: null }),
|
|
15
|
+
on: {
|
|
16
|
+
START: {
|
|
17
|
+
target: 'running',
|
|
18
|
+
actions: assign(({ event }) => ({
|
|
19
|
+
process: event.process,
|
|
20
|
+
script: event.script,
|
|
21
|
+
startTime: Date.now(),
|
|
22
|
+
pid: event.pid || null,
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
running: {
|
|
28
|
+
on: {
|
|
29
|
+
STOP: { target: 'stopping' },
|
|
30
|
+
CLOSE: { target: 'idle' },
|
|
31
|
+
ERROR: { target: 'idle' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
stopping: {
|
|
35
|
+
on: {
|
|
36
|
+
CLOSE: { target: 'idle' },
|
|
37
|
+
ERROR: { target: 'idle' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const actors = new Map();
|
|
44
|
+
|
|
45
|
+
export function getOrCreate(convId) {
|
|
46
|
+
if (actors.has(convId)) return actors.get(convId);
|
|
47
|
+
const actor = createActor(machine);
|
|
48
|
+
actor.start();
|
|
49
|
+
actors.set(convId, actor);
|
|
50
|
+
return actor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function get(convId) {
|
|
54
|
+
return actors.get(convId) || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function remove(convId) {
|
|
58
|
+
const actor = actors.get(convId);
|
|
59
|
+
if (actor) { actor.stop(); actors.delete(convId); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function snapshot(convId) {
|
|
63
|
+
const actor = actors.get(convId);
|
|
64
|
+
return actor ? actor.getSnapshot() : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function send(convId, event) {
|
|
68
|
+
const actor = getOrCreate(convId);
|
|
69
|
+
actor.send(event);
|
|
70
|
+
return actor.getSnapshot();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isRunning(convId) {
|
|
74
|
+
const s = snapshot(convId);
|
|
75
|
+
return s ? s.value === 'running' || s.value === 'stopping' : false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getContext(convId) {
|
|
79
|
+
const s = snapshot(convId);
|
|
80
|
+
return s ? s.context : null;
|
|
81
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import * as scriptMachine from './script-machine.js';
|
|
5
6
|
|
|
6
7
|
function err(code, message) { const e = new Error(message); e.code = code; throw e; }
|
|
7
8
|
|
|
@@ -19,15 +20,16 @@ export function register(router, deps) {
|
|
|
19
20
|
hasStart = !!scripts.start;
|
|
20
21
|
hasDev = !!scripts.dev;
|
|
21
22
|
} catch {}
|
|
22
|
-
const running =
|
|
23
|
-
const
|
|
23
|
+
const running = scriptMachine.isRunning(p.id);
|
|
24
|
+
const ctx = scriptMachine.getContext(p.id);
|
|
25
|
+
const runningScript = running ? ctx?.script : null;
|
|
24
26
|
return { hasStart, hasDev, running, runningScript };
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
router.handle('conv.run-script', (p) => {
|
|
28
30
|
const conv = queries.getConversation(p.id);
|
|
29
31
|
if (!conv) err(404, 'Not found');
|
|
30
|
-
if (
|
|
32
|
+
if (scriptMachine.isRunning(p.id)) err(409, 'Script already running');
|
|
31
33
|
const script = p.script;
|
|
32
34
|
if (script !== 'start' && script !== 'dev') err(400, 'Invalid script');
|
|
33
35
|
const wd = conv.workingDirectory || STARTUP_CWD;
|
|
@@ -39,21 +41,26 @@ export function register(router, deps) {
|
|
|
39
41
|
delete childEnv.PORT; delete childEnv.BASE_URL; delete childEnv.HOT_RELOAD;
|
|
40
42
|
const isWindows = os.platform() === 'win32';
|
|
41
43
|
const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
|
|
44
|
+
scriptMachine.send(p.id, { type: 'START', process: child, script, pid: child.pid });
|
|
42
45
|
activeScripts.set(p.id, { process: child, script, startTime: Date.now() });
|
|
43
46
|
broadcastSync({ type: 'script_started', conversationId: p.id, script, timestamp: Date.now() });
|
|
44
47
|
const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId: p.id, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
45
48
|
child.stdout.on('data', onData('stdout'));
|
|
46
49
|
child.stderr.on('data', onData('stderr'));
|
|
47
|
-
child.on('error', (e) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
|
|
48
|
-
child.on('close', (code) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
|
|
50
|
+
child.on('error', (e) => { scriptMachine.send(p.id, { type: 'ERROR' }); activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
|
|
51
|
+
child.on('close', (code) => { scriptMachine.send(p.id, { type: 'CLOSE' }); activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
|
|
49
52
|
return { ok: true, script, pid: child.pid };
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
router.handle('conv.stop-script', (p) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (!scriptMachine.isRunning(p.id)) err(404, 'No running script');
|
|
57
|
+
const ctx = scriptMachine.getContext(p.id);
|
|
58
|
+
const proc = ctx?.process;
|
|
59
|
+
scriptMachine.send(p.id, { type: 'STOP' });
|
|
60
|
+
if (proc) {
|
|
61
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch { try { proc.kill('SIGTERM'); } catch {} }
|
|
62
|
+
setTimeout(() => { try { process.kill(-proc.pid, 'SIGKILL'); } catch { try { proc.kill('SIGKILL'); } catch {} } }, 5000);
|
|
63
|
+
}
|
|
57
64
|
return { ok: true };
|
|
58
65
|
});
|
|
59
66
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -2072,27 +2072,38 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
2072
2072
|
} catch (e) {}
|
|
2073
2073
|
}
|
|
2074
2074
|
|
|
2075
|
-
rateLimitState.
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2075
|
+
const existingRetryCount2 = rateLimitState.get(conversationId)?.retryCount || 0;
|
|
2076
|
+
if (existingRetryCount2 >= 3) {
|
|
2077
|
+
debugLog(`[rate-limit] Conv ${conversationId} result rate limit hit ${existingRetryCount2 + 1} times, giving up`);
|
|
2078
|
+
batcher.drain();
|
|
2079
|
+
activeExecutions.delete(conversationId);
|
|
2080
|
+
queries.setIsStreaming(conversationId, false);
|
|
2081
|
+
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingRetryCount2 + 1} attempts. Please try again later.`);
|
|
2082
|
+
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
2083
|
+
broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
rateLimitState.set(conversationId, {
|
|
2087
|
+
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
2088
|
+
cooldownMs: retryAfterSec * 1000,
|
|
2089
|
+
retryCount: existingRetryCount2 + 1,
|
|
2079
2090
|
isStreamDetected: true
|
|
2080
2091
|
});
|
|
2081
|
-
|
|
2092
|
+
|
|
2082
2093
|
broadcastSync({
|
|
2083
2094
|
type: 'rate_limit_hit',
|
|
2084
2095
|
sessionId,
|
|
2085
2096
|
conversationId,
|
|
2086
2097
|
retryAfterMs: retryAfterSec * 1000,
|
|
2087
2098
|
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
2088
|
-
retryCount: 1,
|
|
2099
|
+
retryCount: existingRetryCount2 + 1,
|
|
2089
2100
|
timestamp: Date.now()
|
|
2090
2101
|
});
|
|
2091
|
-
|
|
2102
|
+
|
|
2092
2103
|
batcher.drain();
|
|
2093
2104
|
activeExecutions.delete(conversationId);
|
|
2094
2105
|
queries.setIsStreaming(conversationId, false);
|
|
2095
|
-
|
|
2106
|
+
|
|
2096
2107
|
setTimeout(() => {
|
|
2097
2108
|
rateLimitState.delete(conversationId);
|
|
2098
2109
|
broadcastSync({
|
|
@@ -2102,7 +2113,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
2102
2113
|
});
|
|
2103
2114
|
scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
|
|
2104
2115
|
}, retryAfterSec * 1000);
|
|
2105
|
-
|
|
2116
|
+
|
|
2106
2117
|
return;
|
|
2107
2118
|
}
|
|
2108
2119
|
|