@abgov/nx-adsp 12.8.0-beta.1 → 12.8.0-beta.3
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/package.json +1 -1
- package/src/generators/express-service/express-service.js +10 -8
- package/src/generators/express-service/express-service.js.map +1 -1
- package/src/utils/agent.d.ts +7 -0
- package/src/utils/agent.js +213 -146
- package/src/utils/agent.js.map +1 -1
- package/src/utils/agent.spec.ts +19 -2
package/package.json
CHANGED
|
@@ -79,10 +79,11 @@ function default_1(host, options) {
|
|
|
79
79
|
// which are applied directly to the Nx Tree.
|
|
80
80
|
// Falls back silently if agent-service is unreachable or no accessToken.
|
|
81
81
|
if (normalizedOptions.adsp) {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// When --tenant was provided, normalizedOptions.accessToken holds the token
|
|
83
|
+
// from the single realm login already performed during normalizeOptions.
|
|
84
|
+
// token from the single realm login. Fall back to a new login only when the
|
|
85
|
+
// full interactive flow was used and no token is available.
|
|
86
|
+
const accessToken = (_a = normalizedOptions.accessToken) !== null && _a !== void 0 ? _a : (yield (0, nx_oc_1.realmLogin)(normalizedOptions.adsp.accessServiceUrl, normalizedOptions.adsp.tenantRealm).catch((err) => {
|
|
86
87
|
var _a;
|
|
87
88
|
process.stdout.write(`Agent sign-in failed (${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}) — skipping agent interaction.\n`);
|
|
88
89
|
return undefined;
|
|
@@ -99,15 +100,16 @@ function default_1(host, options) {
|
|
|
99
100
|
'src/environment.ts': environmentTs,
|
|
100
101
|
},
|
|
101
102
|
}, host, normalizedOptions.projectRoot);
|
|
102
|
-
// When the
|
|
103
|
-
//
|
|
104
|
-
|
|
103
|
+
// When the agent interaction ended without generating files — whether the
|
|
104
|
+
// user was in a conversation or Ctrl+C'd before the agent responded —
|
|
105
|
+
// confirm whether to proceed. Default to false when Ctrl+C was used.
|
|
106
|
+
if (agentResult && agentResult.filesWritten === 0) {
|
|
105
107
|
const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
|
|
106
108
|
const { proceed } = yield prompt({
|
|
107
109
|
type: 'confirm',
|
|
108
110
|
name: 'proceed',
|
|
109
111
|
message: 'Agent interaction ended without generating files. Continue with base scaffolding?',
|
|
110
|
-
initial:
|
|
112
|
+
initial: !agentResult.interrupted,
|
|
111
113
|
});
|
|
112
114
|
if (!proceed) {
|
|
113
115
|
throw new Error('Generation aborted.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express-service.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/express-service/express-service.ts"],"names":[],"mappings":";;AAsFA,
|
|
1
|
+
{"version":3,"file":"express-service.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/express-service/express-service.ts"],"names":[],"mappings":"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"}
|
package/src/utils/agent.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface AgentResult {
|
|
|
16
16
|
filesWritten: number;
|
|
17
17
|
/** True when the user was in an active conversation before it ended. */
|
|
18
18
|
userInteracted: boolean;
|
|
19
|
+
/** True when the conversation ended via Ctrl+C (SIGINT). */
|
|
20
|
+
interrupted?: boolean;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Connect to the ADSP agent-service and conduct a multi-turn conversation
|
|
@@ -23,6 +25,11 @@ export interface AgentResult {
|
|
|
23
25
|
* generated and modified files; this function retrieves the workspace state
|
|
24
26
|
* after the conversation and applies all files to the Nx Tree.
|
|
25
27
|
*
|
|
28
|
+
* The socket connection and file upload start immediately. While the files
|
|
29
|
+
* are uploading, the developer is prompted for a brief project description.
|
|
30
|
+
* The initial message is sent as soon as both the upload and the description
|
|
31
|
+
* are ready — whichever finishes last.
|
|
32
|
+
*
|
|
26
33
|
* Returns null if agent-service is unavailable — callers should skip the
|
|
27
34
|
* agent step gracefully in that case.
|
|
28
35
|
*/
|
package/src/utils/agent.js
CHANGED
|
@@ -13,11 +13,17 @@ const AGENT_ID = 'nxAdspAgent';
|
|
|
13
13
|
* generated and modified files; this function retrieves the workspace state
|
|
14
14
|
* after the conversation and applies all files to the Nx Tree.
|
|
15
15
|
*
|
|
16
|
+
* The socket connection and file upload start immediately. While the files
|
|
17
|
+
* are uploading, the developer is prompted for a brief project description.
|
|
18
|
+
* The initial message is sent as soon as both the upload and the description
|
|
19
|
+
* are ready — whichever finishes last.
|
|
20
|
+
*
|
|
16
21
|
* Returns null if agent-service is unavailable — callers should skip the
|
|
17
22
|
* agent step gracefully in that case.
|
|
18
23
|
*/
|
|
19
24
|
function consultAgent(directoryServiceUrl, accessToken, projectContext, host, projectRoot) {
|
|
20
25
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
var _a;
|
|
21
27
|
if (!accessToken) {
|
|
22
28
|
process.stdout.write('\n[nx-adsp] No access token — skipping agent interaction.\n');
|
|
23
29
|
return null;
|
|
@@ -28,52 +34,140 @@ function consultAgent(directoryServiceUrl, accessToken, projectContext, host, pr
|
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
30
36
|
process.stdout.write(`\n[nx-adsp] Connecting to agent at ${agentServiceUrl}...\n`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
const threadId = crypto.randomUUID();
|
|
38
|
+
// Start socket connection immediately so file upload overlaps with the description prompt.
|
|
39
|
+
const socket = (0, socket_io_client_1.io)(agentServiceUrl, {
|
|
40
|
+
auth: { token: accessToken },
|
|
41
|
+
// Skip polling — go directly to WebSocket to avoid ARO ingress rejecting
|
|
42
|
+
// the polling POST with HTTP 400.
|
|
43
|
+
transports: ['websocket'],
|
|
44
|
+
timeout: 30000,
|
|
45
|
+
reconnection: false,
|
|
46
|
+
});
|
|
47
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
48
|
+
// Coordination: sendInitialMessage is called once BOTH conditions are met:
|
|
49
|
+
// 1. description prompt has been answered (descriptionReady = true)
|
|
50
|
+
// 2. workspace files have been uploaded (workspaceReady = true)
|
|
51
|
+
// Whichever condition is satisfied last triggers the send.
|
|
52
|
+
let description;
|
|
53
|
+
let descriptionReady = false;
|
|
54
|
+
let workspaceReady = false;
|
|
55
|
+
let conversationStarted = false;
|
|
56
|
+
let buffer = '';
|
|
57
|
+
let conversationDone = false;
|
|
58
|
+
let agentHasResponded = false;
|
|
59
|
+
let interrupted = false;
|
|
60
|
+
let resolveConversation;
|
|
61
|
+
const conversationPromise = new Promise((r) => {
|
|
62
|
+
resolveConversation = r;
|
|
63
|
+
});
|
|
64
|
+
const cleanup = (filesWritten) => {
|
|
65
|
+
conversationDone = true;
|
|
66
|
+
rl.close();
|
|
67
|
+
socket.disconnect();
|
|
68
|
+
// Return null only for truly silent skips (no agent, no token, connection failed).
|
|
69
|
+
// Return a result whenever the user was actively engaged (agent responded)
|
|
70
|
+
// OR when they explicitly interrupted (Ctrl+C), so the caller always gets
|
|
71
|
+
// a chance to confirm before proceeding.
|
|
72
|
+
resolveConversation(agentHasResponded || interrupted
|
|
73
|
+
? { filesWritten, userInteracted: agentHasResponded, interrupted }
|
|
74
|
+
: null);
|
|
75
|
+
};
|
|
76
|
+
const buildInitialMessage = () => {
|
|
77
|
+
const fileNames = Object.keys(projectContext.existingFiles).join(', ');
|
|
78
|
+
const descriptionLine = description
|
|
79
|
+
? `It is described as: "${description}". `
|
|
80
|
+
: '';
|
|
81
|
+
return (`I am setting up a new ${projectContext.projectType} called "${projectContext.projectName}" ` +
|
|
82
|
+
`for ADSP tenant "${projectContext.tenant}" (nx-adsp plugin version ${projectContext.pluginVersion}). ` +
|
|
83
|
+
descriptionLine +
|
|
84
|
+
`The project files (${fileNames}) have been uploaded to your workspace. ` +
|
|
85
|
+
`What ADSP capabilities would be useful to integrate into this service?`);
|
|
86
|
+
};
|
|
87
|
+
const sendInitialMessage = () => {
|
|
88
|
+
if (conversationStarted)
|
|
89
|
+
return;
|
|
90
|
+
conversationStarted = true;
|
|
91
|
+
process.stdout.write('[nx-adsp] Type your replies at the > prompt. Press Ctrl+D or leave blank to apply generated files.\n\n');
|
|
92
|
+
socket.emit('message', {
|
|
93
|
+
agent: AGENT_ID,
|
|
94
|
+
threadId,
|
|
95
|
+
content: buildInitialMessage(),
|
|
96
|
+
rawChunks: true,
|
|
51
97
|
});
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
if (!conversationDone && !
|
|
98
|
+
// Show periodic dots while waiting for first agent response, then warn at 2 minutes.
|
|
99
|
+
const dotInterval = setInterval(() => {
|
|
100
|
+
if (!conversationDone && !agentHasResponded)
|
|
101
|
+
process.stdout.write('.');
|
|
102
|
+
else
|
|
103
|
+
clearInterval(dotInterval);
|
|
104
|
+
}, 3000);
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
clearInterval(dotInterval);
|
|
107
|
+
if (!conversationDone && !agentHasResponded) {
|
|
108
|
+
process.stdout.write('\n[nx-adsp] No response after 2 minutes. ' +
|
|
109
|
+
'The nxAdspAgent may still be deploying or the LLM is unresponsive. ' +
|
|
110
|
+
'Press Ctrl+C to skip and continue generation.\n');
|
|
111
|
+
}
|
|
112
|
+
}, 120000);
|
|
113
|
+
};
|
|
114
|
+
const requestWorkspaceState = () => {
|
|
115
|
+
socket.emit('workspace-read', { agent: AGENT_ID, threadId });
|
|
116
|
+
};
|
|
117
|
+
const promptUser = () => {
|
|
118
|
+
rl.question('\n> ', (input) => {
|
|
119
|
+
const trimmed = input.trim();
|
|
120
|
+
if (trimmed) {
|
|
121
|
+
socket.emit('message', {
|
|
122
|
+
agent: AGENT_ID,
|
|
123
|
+
threadId,
|
|
124
|
+
content: trimmed,
|
|
125
|
+
rawChunks: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Empty input — apply whatever the agent has generated.
|
|
55
130
|
requestWorkspaceState();
|
|
56
131
|
}
|
|
57
132
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
};
|
|
134
|
+
const applyWorkspaceFiles = (files) => {
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
// Skip files we uploaded that the agent hasn't modified — only apply
|
|
138
|
+
// new files and agent-modified versions of existing files.
|
|
139
|
+
const original = projectContext.existingFiles[file.path];
|
|
140
|
+
if (original !== undefined && original === file.content) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const fullPath = `${projectRoot}/${file.path}`;
|
|
144
|
+
host.write(fullPath, file.content);
|
|
145
|
+
count++;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
};
|
|
149
|
+
// Register all socket and readline handlers upfront so they are active
|
|
150
|
+
// while the description prompt is being shown.
|
|
151
|
+
rl.on('SIGINT', () => {
|
|
152
|
+
interrupted = true;
|
|
153
|
+
process.stdout.write('\n');
|
|
154
|
+
cleanup(0);
|
|
155
|
+
});
|
|
156
|
+
rl.on('close', () => {
|
|
157
|
+
if (!conversationDone && !interrupted) {
|
|
158
|
+
requestWorkspaceState();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
socket.on('connect', () => {
|
|
162
|
+
process.stdout.write('[nx-adsp] Connected to agent-service.\n');
|
|
163
|
+
process.stdout.write('[nx-adsp] Uploading project files to workspace...\n');
|
|
164
|
+
// The server registers its 'workspace-update' handler only after an async
|
|
165
|
+
// getServiceConfiguration() call completes (router.ts:215). It signals
|
|
166
|
+
// readiness by calling socket.send() (router.ts:217), which arrives as a
|
|
167
|
+
// 'message' event on the client — by then all server handlers are registered.
|
|
168
|
+
// Emitting workspace-update immediately on 'connect' races that await and
|
|
169
|
+
// the event is silently dropped when the server wins.
|
|
170
|
+
socket.once('message', () => {
|
|
77
171
|
socket.emit('workspace-update', {
|
|
78
172
|
agent: AGENT_ID,
|
|
79
173
|
threadId,
|
|
@@ -83,114 +177,87 @@ function consultAgent(directoryServiceUrl, accessToken, projectContext, host, pr
|
|
|
83
177
|
})),
|
|
84
178
|
deletes: [],
|
|
85
179
|
});
|
|
86
|
-
};
|
|
87
|
-
const promptUser = () => {
|
|
88
|
-
rl.question('\n> ', (input) => {
|
|
89
|
-
const trimmed = input.trim();
|
|
90
|
-
if (trimmed) {
|
|
91
|
-
sendMessage(trimmed);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// Empty input — apply whatever the agent has generated.
|
|
95
|
-
requestWorkspaceState();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
const applyWorkspaceFiles = (files) => {
|
|
100
|
-
let count = 0;
|
|
101
|
-
for (const file of files) {
|
|
102
|
-
// Skip files we uploaded that the agent hasn't modified — only apply
|
|
103
|
-
// new files and agent-modified versions of existing files.
|
|
104
|
-
const original = projectContext.existingFiles[file.path];
|
|
105
|
-
if (original !== undefined && original === file.content) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const fullPath = `${projectRoot}/${file.path}`;
|
|
109
|
-
host.write(fullPath, file.content);
|
|
110
|
-
count++;
|
|
111
|
-
}
|
|
112
|
-
return count;
|
|
113
|
-
};
|
|
114
|
-
const cleanup = (filesWritten) => {
|
|
115
|
-
conversationDone = true;
|
|
116
|
-
rl.close();
|
|
117
|
-
socket.disconnect();
|
|
118
|
-
// Return null for silent skips (no agent, no token, connection failed).
|
|
119
|
-
// Return a result with userInteracted:true when the user was in a
|
|
120
|
-
// conversation so the caller can ask whether to proceed.
|
|
121
|
-
resolve(agentHasResponded ? { filesWritten, userInteracted: true } : null);
|
|
122
|
-
};
|
|
123
|
-
socket.on('workspace-updated', () => {
|
|
124
|
-
process.stdout.write('[nx-adsp] Project files uploaded to workspace.\n');
|
|
125
|
-
process.stdout.write('[nx-adsp] Type your replies at the > prompt. Press Ctrl+D or leave blank to apply generated files.\n\n');
|
|
126
|
-
sendMessage(buildInitialMessage());
|
|
127
|
-
});
|
|
128
|
-
socket.on('connect', () => {
|
|
129
|
-
process.stdout.write('[nx-adsp] Connected to agent-service.\n');
|
|
130
|
-
process.stdout.write('[nx-adsp] Uploading project files to workspace...\n');
|
|
131
|
-
uploadFilesToWorkspace();
|
|
132
|
-
// Show periodic dots while waiting for first response, then a warning at 2 minutes.
|
|
133
|
-
const dotInterval = setInterval(() => {
|
|
134
|
-
if (!conversationDone && !agentHasResponded)
|
|
135
|
-
process.stdout.write('.');
|
|
136
|
-
else
|
|
137
|
-
clearInterval(dotInterval);
|
|
138
|
-
}, 3000);
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
clearInterval(dotInterval);
|
|
141
|
-
if (!conversationDone && !agentHasResponded) {
|
|
142
|
-
process.stdout.write('\n[nx-adsp] No response after 2 minutes. ' +
|
|
143
|
-
'The nxAdspAgent may still be deploying or the LLM is unresponsive. ' +
|
|
144
|
-
'Press Ctrl+C to skip and continue generation.\n');
|
|
145
|
-
}
|
|
146
|
-
}, 120000);
|
|
147
|
-
});
|
|
148
|
-
socket.on('stream', ({ chunk, done }) => {
|
|
149
|
-
var _a, _b;
|
|
150
|
-
if ((chunk === null || chunk === void 0 ? void 0 : chunk.type) === 'text-delta') {
|
|
151
|
-
const text = (_b = (_a = chunk.payload) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
152
|
-
buffer += text;
|
|
153
|
-
agentHasResponded = true;
|
|
154
|
-
process.stdout.write(text);
|
|
155
|
-
}
|
|
156
|
-
if (done) {
|
|
157
|
-
if (buffer.length > 0 && !buffer.endsWith('\n')) {
|
|
158
|
-
process.stdout.write('\n');
|
|
159
|
-
}
|
|
160
|
-
buffer = '';
|
|
161
|
-
// Agent finished its turn — prompt the user to continue the conversation
|
|
162
|
-
// or press Enter to apply whatever the agent has written so far.
|
|
163
|
-
promptUser();
|
|
164
|
-
}
|
|
165
180
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
});
|
|
182
|
+
socket.on('workspace-updated', () => {
|
|
183
|
+
workspaceReady = true;
|
|
184
|
+
if (descriptionReady) {
|
|
185
|
+
// Description was entered before upload finished — send now.
|
|
186
|
+
sendInitialMessage();
|
|
187
|
+
}
|
|
188
|
+
// else: description prompt is still open — post-prompt code will call sendInitialMessage.
|
|
189
|
+
});
|
|
190
|
+
socket.on('stream', ({ chunk, done }) => {
|
|
191
|
+
var _a, _b;
|
|
192
|
+
if ((chunk === null || chunk === void 0 ? void 0 : chunk.type) === 'text-delta') {
|
|
193
|
+
const text = (_b = (_a = chunk.payload) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
194
|
+
if (!agentHasResponded) {
|
|
195
|
+
// Clear the dots line before the first response starts.
|
|
196
|
+
process.stdout.write('\n');
|
|
171
197
|
}
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
buffer += text;
|
|
199
|
+
agentHasResponded = true;
|
|
200
|
+
process.stdout.write(text);
|
|
201
|
+
}
|
|
202
|
+
if (done) {
|
|
203
|
+
if (buffer.length > 0 && !buffer.endsWith('\n')) {
|
|
204
|
+
process.stdout.write('\n');
|
|
174
205
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
process.stdout.write(`\
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
buffer = '';
|
|
207
|
+
// Agent finished its turn — prompt the user to continue the conversation
|
|
208
|
+
// or press Enter to apply whatever the agent has written so far.
|
|
209
|
+
promptUser();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
socket.on('workspace-state', ({ files }) => {
|
|
213
|
+
// Only apply files the agent added or modified — not unchanged uploaded files.
|
|
214
|
+
const written = applyWorkspaceFiles(files !== null && files !== void 0 ? files : []);
|
|
215
|
+
if (written > 0) {
|
|
216
|
+
process.stdout.write(`\nApplied ${written} file(s) from agent workspace.\n`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
process.stdout.write('\nNo files generated by agent.\n');
|
|
220
|
+
}
|
|
221
|
+
cleanup(written);
|
|
222
|
+
});
|
|
223
|
+
socket.on('session-expired', () => {
|
|
224
|
+
process.stdout.write('\nAgent session expired.\n');
|
|
225
|
+
requestWorkspaceState();
|
|
226
|
+
});
|
|
227
|
+
socket.on('connect_error', (err) => {
|
|
228
|
+
var _a, _b, _c, _d;
|
|
229
|
+
const errAny = err;
|
|
230
|
+
process.stdout.write(`\n[nx-adsp] Connection failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}\n`);
|
|
231
|
+
process.stdout.write(`[nx-adsp] Error detail: ${JSON.stringify((_d = (_c = (_b = errAny === null || errAny === void 0 ? void 0 : errAny.description) !== null && _b !== void 0 ? _b : errAny === null || errAny === void 0 ? void 0 : errAny.context) !== null && _c !== void 0 ? _c : errAny === null || errAny === void 0 ? void 0 : errAny.cause) !== null && _d !== void 0 ? _d : 'none')}\n`);
|
|
232
|
+
cleanup(0);
|
|
233
|
+
});
|
|
234
|
+
socket.on('error', (err) => {
|
|
235
|
+
process.stdout.write(`\n[nx-adsp] Agent error: ${JSON.stringify(err)}\n`);
|
|
236
|
+
requestWorkspaceState();
|
|
193
237
|
});
|
|
238
|
+
// Show description prompt while socket connects and uploads files in the background.
|
|
239
|
+
try {
|
|
240
|
+
const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
|
|
241
|
+
const answer = yield prompt({
|
|
242
|
+
type: 'input',
|
|
243
|
+
name: 'description',
|
|
244
|
+
message: `Briefly describe what ${projectContext.projectName} does:`,
|
|
245
|
+
});
|
|
246
|
+
description = ((_a = answer.description) === null || _a === void 0 ? void 0 : _a.trim()) || undefined;
|
|
247
|
+
descriptionReady = true;
|
|
248
|
+
}
|
|
249
|
+
catch (_b) {
|
|
250
|
+
// Ctrl+C or cancellation during the description prompt.
|
|
251
|
+
interrupted = true;
|
|
252
|
+
cleanup(0);
|
|
253
|
+
return conversationPromise;
|
|
254
|
+
}
|
|
255
|
+
if (workspaceReady) {
|
|
256
|
+
// Upload finished while the user was typing — send immediately.
|
|
257
|
+
sendInitialMessage();
|
|
258
|
+
}
|
|
259
|
+
// else: workspace-updated handler will call sendInitialMessage once upload completes.
|
|
260
|
+
return conversationPromise;
|
|
194
261
|
});
|
|
195
262
|
}
|
|
196
263
|
function resolveAgentServiceUrl(directoryServiceUrl) {
|
package/src/utils/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/agent.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/agent.ts"],"names":[],"mappings":"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"}
|
package/src/utils/agent.spec.ts
CHANGED
|
@@ -12,6 +12,12 @@ jest.mock('socket.io-client');
|
|
|
12
12
|
jest.mock('@abgov/nx-oc', () => ({ getServiceUrls: jest.fn() }));
|
|
13
13
|
jest.mock('@nx/devkit', () => ({ Tree: jest.fn() }));
|
|
14
14
|
|
|
15
|
+
// Enquirer is dynamically imported inside consultAgent; mock it here so the
|
|
16
|
+
// prompt resolves immediately with an empty description in all tests.
|
|
17
|
+
jest.mock('enquirer', () => ({
|
|
18
|
+
prompt: jest.fn().mockResolvedValue({ description: '' }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
15
21
|
import { io } from 'socket.io-client';
|
|
16
22
|
import { getServiceUrls } from '@abgov/nx-oc';
|
|
17
23
|
|
|
@@ -37,6 +43,9 @@ function makeMockSocket() {
|
|
|
37
43
|
on: jest.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
38
44
|
handlers[event] = handler;
|
|
39
45
|
}),
|
|
46
|
+
once: jest.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
47
|
+
handlers[event] = handler;
|
|
48
|
+
}),
|
|
40
49
|
emit: jest.fn(),
|
|
41
50
|
disconnect: jest.fn(),
|
|
42
51
|
_handlers: handlers,
|
|
@@ -46,6 +55,9 @@ function makeMockSocket() {
|
|
|
46
55
|
return socket;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
// Flush the microtask queue so async operations inside consultAgent settle.
|
|
59
|
+
// One tick is enough: the dynamic import and the enquirer mock both resolve
|
|
60
|
+
// as microtasks, so after this all handlers are registered and descriptionReady=true.
|
|
49
61
|
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
50
62
|
|
|
51
63
|
describe('consultAgent', () => {
|
|
@@ -66,6 +78,7 @@ describe('consultAgent', () => {
|
|
|
66
78
|
});
|
|
67
79
|
const socket = makeMockSocket();
|
|
68
80
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
81
|
+
// After one tick: enquirer mock resolves, descriptionReady=true, conversationPromise returned.
|
|
69
82
|
await flushPromises();
|
|
70
83
|
socket._handlers['connect_error']?.();
|
|
71
84
|
expect(await resultPromise).toBeNull();
|
|
@@ -79,7 +92,10 @@ describe('consultAgent', () => {
|
|
|
79
92
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
80
93
|
await flushPromises();
|
|
81
94
|
|
|
95
|
+
// descriptionReady=true at this point; workspace-updated fires last → triggers sendInitialMessage.
|
|
82
96
|
socket._handlers['connect']?.();
|
|
97
|
+
// Server signals readiness via 'message' before workspace-update is safe to send.
|
|
98
|
+
socket._handlers['message']?.('Connected as user...');
|
|
83
99
|
socket._handlers['workspace-updated']?.();
|
|
84
100
|
// Simulate agent responding with text (sets agentHasResponded = true)
|
|
85
101
|
socket._handlers['stream']?.({ chunk: { type: 'text-delta', payload: { text: 'I will add events.' } }, done: false });
|
|
@@ -92,7 +108,7 @@ describe('consultAgent', () => {
|
|
|
92
108
|
});
|
|
93
109
|
|
|
94
110
|
const result = await resultPromise;
|
|
95
|
-
expect(result).toEqual({ filesWritten: 2, userInteracted: true });
|
|
111
|
+
expect(result).toEqual({ filesWritten: 2, userInteracted: true, interrupted: false });
|
|
96
112
|
expect(mockHost.write).toHaveBeenCalledWith('apps/test-service/src/roles.ts', 'export enum ServiceRoles {}');
|
|
97
113
|
expect(mockHost.write).toHaveBeenCalledWith('apps/test-service/src/main.ts', 'updated main.ts');
|
|
98
114
|
});
|
|
@@ -105,8 +121,9 @@ describe('consultAgent', () => {
|
|
|
105
121
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
106
122
|
await flushPromises();
|
|
107
123
|
|
|
108
|
-
// connect → workspace-update emitted
|
|
124
|
+
// connect → server sends 'message' readiness signal → workspace-update emitted → workspace-updated → initial message sent
|
|
109
125
|
socket._handlers['connect']?.();
|
|
126
|
+
socket._handlers['message']?.('Connected as user...');
|
|
110
127
|
socket._handlers['workspace-updated']?.();
|
|
111
128
|
socket._handlers['stream']?.({ chunk: { type: 'text-delta', payload: { text: 'What does this service do?' } }, done: false });
|
|
112
129
|
socket._handlers['stream']?.({ chunk: null, done: true });
|