agentgui 1.0.796 → 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
|
@@ -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
|
}
|