dsh-nextsteps 1.4.2 → 1.4.4
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/client.js +20 -8
- package/index.js +30 -12
- package/package.json +1 -1
- package/scripts/suite/host.test.mjs +1 -1
package/client.js
CHANGED
|
@@ -226,11 +226,9 @@ window.__ModuleLoader__.load({
|
|
|
226
226
|
function captureForTurn(sessionId, turn) {
|
|
227
227
|
const entries = captures.get(sessionId);
|
|
228
228
|
if (entries === undefined) return null;
|
|
229
|
-
const external = typeof pluginSettings.model === 'string' && pluginSettings.model !== '';
|
|
230
229
|
let best = null;
|
|
231
230
|
for (const [callId, entry] of entries.entries()) {
|
|
232
231
|
if (entry.turn !== turn) continue;
|
|
233
|
-
if (external && !callId.startsWith('generated-')) continue;
|
|
234
232
|
best = entry;
|
|
235
233
|
}
|
|
236
234
|
return best;
|
|
@@ -492,7 +490,7 @@ window.__ModuleLoader__.load({
|
|
|
492
490
|
}, []);
|
|
493
491
|
const generatedRef = React.useRef(null);
|
|
494
492
|
React.useEffect(() => {
|
|
495
|
-
if (
|
|
493
|
+
if (sessionId === '' || turn === undefined || afterSeq === undefined) return undefined;
|
|
496
494
|
const key = sessionId + '|' + turn;
|
|
497
495
|
if (generatedRef.current === key) return undefined;
|
|
498
496
|
if (captureForTurn(sessionId, turn) !== null) return undefined;
|
|
@@ -565,17 +563,31 @@ window.__ModuleLoader__.load({
|
|
|
565
563
|
const install = async () => {
|
|
566
564
|
const { info } = update;
|
|
567
565
|
let pluginManager;
|
|
568
|
-
try {
|
|
569
|
-
|
|
566
|
+
try {
|
|
567
|
+
pluginManager = ctx.remote?.pluginManager ?? ctx.get?.('remote')?.pluginManager;
|
|
568
|
+
} catch { pluginManager = undefined; }
|
|
569
|
+
if (info === undefined) return;
|
|
570
|
+
if (pluginManager === undefined || typeof pluginManager.installBundle !== 'function') {
|
|
571
|
+
setUpdate({ status: 'error', info, error: 'the plugin manager is not available in this install' });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
570
574
|
setUpdate({ status: 'installing', info });
|
|
571
575
|
try {
|
|
576
|
+
const requestId = NS + '-update-' + info.latest + '-' + Date.now();
|
|
572
577
|
const attempt = async (spec) => {
|
|
573
|
-
const reply = await pluginManager.installBundle(spec, {
|
|
578
|
+
const reply = await pluginManager.installBundle(spec, { enabled: true, requestId });
|
|
574
579
|
if (reply === null || typeof reply !== 'object' || reply.ok !== true) throw new Error(reply?.error?.message ?? 'plugin manager did not answer');
|
|
575
580
|
return reply.value;
|
|
576
581
|
};
|
|
577
|
-
let result
|
|
578
|
-
if (
|
|
582
|
+
let result;
|
|
583
|
+
if (typeof pluginManager.updateBundle === 'function') {
|
|
584
|
+
const reply = await pluginManager.updateBundle(NPM_NAME, info.latest, { requestId });
|
|
585
|
+
if (reply === null || typeof reply !== 'object' || reply.ok !== true) throw new Error(reply?.error?.message ?? 'plugin manager did not answer');
|
|
586
|
+
result = reply.value;
|
|
587
|
+
} else {
|
|
588
|
+
result = await attempt(NPM_NAME + '@' + info.latest);
|
|
589
|
+
if (result !== null && typeof result === 'object' && result.error?.code === 'already-installed') result = await attempt('https://registry.npmjs.org/' + NPM_NAME + '/-/' + NPM_NAME + '-' + info.latest + '.tgz');
|
|
590
|
+
}
|
|
579
591
|
const applied = result !== null && typeof result === 'object' && (result.application === 'applied' || result.application === 'restart-required' || result.version === info.latest);
|
|
580
592
|
if (!applied) {
|
|
581
593
|
const diagnostic = typeof result?.error?.diagnostic === 'string' ? result.error.diagnostic.split('\n').find((entry) => entry.trim() !== '') : undefined;
|
package/index.js
CHANGED
|
@@ -124,7 +124,16 @@ const PARAMETER_SCHEMA = {
|
|
|
124
124
|
required: ['followups']
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
function
|
|
127
|
+
function languageRuleOf(settings) {
|
|
128
|
+
const language = LANGUAGE_NAMES[settings.lang] ?? LANGUAGE_NAMES.auto;
|
|
129
|
+
if (settings.lang === 'auto') {
|
|
130
|
+
return 'Write every prompt and label in the same language the conversation has been using.';
|
|
131
|
+
}
|
|
132
|
+
return `LANGUAGE OVERRIDE, applies to the suggest_followups tool only: write every prompt and label in ${language} — even if the conversation or the session history is in another language. Never copy the conversation's language here.`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toolDescription(settings = { lang: 'auto', maxFollowups: 3, model: '' }) {
|
|
136
|
+
const external = typeof settings.model === 'string' && settings.model !== '';
|
|
128
137
|
if (external) {
|
|
129
138
|
return [
|
|
130
139
|
'This tool is not used any more: the host generates the followup suggestions after your turn with a separate model.',
|
|
@@ -133,10 +142,10 @@ function toolDescription(external = false) {
|
|
|
133
142
|
}
|
|
134
143
|
return [
|
|
135
144
|
'MANDATORY in every turn: once all real work and other tool calls are done, call this tool BEFORE writing your final answer — the answer must come after this tool returns, so the turn ends cleanly right after it. Never write your final answer before calling it.',
|
|
136
|
-
|
|
145
|
+
`Suggest exactly ${settings.maxFollowups} followup prompts when you call suggest_followups.`,
|
|
137
146
|
'Good suggestions include alternatives to the latest implementation, related features, cleanup opportunities, tests to add, or verification steps you could run yourself; if plan steps remain, one suggestion may be continuing with the next step.',
|
|
138
147
|
`Each prompt must be short (usually one sentence), goal-oriented — name the outcome, not the steps — stand alone without conversation context, and be phrased in the user's first-person voice.`,
|
|
139
|
-
|
|
148
|
+
languageRuleOf(settings),
|
|
140
149
|
'Never suggest actions the user would carry out themselves outside this session (e.g. "commit these changes"); judge by the tools you actually have.',
|
|
141
150
|
'This never waits for an answer: its result only confirms the suggestions were recorded.',
|
|
142
151
|
'Example input:',
|
|
@@ -144,21 +153,22 @@ function toolDescription(external = false) {
|
|
|
144
153
|
].join('\n');
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
const CURRENT_STATEMENT = 'This is the current setting for this turn; if an earlier message in this conversation stated a different one, ignore that earlier statement.';
|
|
157
|
+
|
|
147
158
|
function nextstepsSectionText() {
|
|
148
159
|
const settings = settingsLoad();
|
|
149
160
|
if (typeof settings.model === 'string' && settings.model !== '') {
|
|
150
161
|
return [
|
|
151
|
-
'nextsteps (the followup suggestion cards):
|
|
162
|
+
'nextsteps (the followup suggestion cards):',
|
|
163
|
+
`- ${CURRENT_STATEMENT}`,
|
|
164
|
+
'- The host generates them after your turn with a separate model, so you must not call any followup tool. Just finish your answer as usual.'
|
|
152
165
|
].join('\n');
|
|
153
166
|
}
|
|
154
|
-
const language = LANGUAGE_NAMES[settings.lang] ?? LANGUAGE_NAMES.auto;
|
|
155
|
-
const languageRule = settings.lang === 'auto'
|
|
156
|
-
? `Write every prompt and label in the same language the conversation has been using.`
|
|
157
|
-
: `LANGUAGE OVERRIDE, applies to the suggest_followups tool only: write every prompt and label in ${language} — even if the conversation or the session history is in another language. Never copy the conversation's language here.`;
|
|
158
167
|
return [
|
|
159
168
|
'nextsteps (the followup suggestion cards):',
|
|
169
|
+
`- ${CURRENT_STATEMENT}`,
|
|
160
170
|
`- Suggest exactly ${settings.maxFollowups} followup prompts when you call suggest_followups.`,
|
|
161
|
-
`- ${
|
|
171
|
+
`- ${languageRuleOf(settings)}`
|
|
162
172
|
].join('\n');
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -168,7 +178,7 @@ function toolSpec(settings = { lang: 'auto', maxFollowups: 3, model: '' }) {
|
|
|
168
178
|
const external = typeof settings.model === 'string' && settings.model !== '';
|
|
169
179
|
return {
|
|
170
180
|
name: TOOL_NAME,
|
|
171
|
-
description: toolDescription(
|
|
181
|
+
description: toolDescription(settings),
|
|
172
182
|
parameters: PARAMETER_SCHEMA,
|
|
173
183
|
output: {
|
|
174
184
|
schema: { type: 'string' },
|
|
@@ -283,8 +293,15 @@ async function generateForTurn(ctx, settings, sessionId, afterSeq, signal) {
|
|
|
283
293
|
let transcript;
|
|
284
294
|
try {
|
|
285
295
|
const events = observation.events ?? [];
|
|
286
|
-
const
|
|
287
|
-
|
|
296
|
+
const turnEnd = events.find((entry) => {
|
|
297
|
+
const event = entry?.event ?? entry;
|
|
298
|
+
return event?.type === 'turn/end' && event.seq === afterSeq;
|
|
299
|
+
});
|
|
300
|
+
const newerTurn = events.some((entry) => {
|
|
301
|
+
const event = entry?.event ?? entry;
|
|
302
|
+
return event?.type === 'turn/end' && Number.isSafeInteger(event.seq) && event.seq > afterSeq;
|
|
303
|
+
});
|
|
304
|
+
if (turnEnd === undefined || newerTurn || turnEnd.data?.reason?.kind !== 'completed') {
|
|
288
305
|
throw Object.assign(new Error('the chat moved on'), { status: 409 });
|
|
289
306
|
}
|
|
290
307
|
transcript = turnTranscript(events, afterSeq);
|
|
@@ -292,6 +309,7 @@ async function generateForTurn(ctx, settings, sessionId, afterSeq, signal) {
|
|
|
292
309
|
observation[Symbol.dispose]?.();
|
|
293
310
|
}
|
|
294
311
|
if (transcript.trim() === '') return { followups: [] };
|
|
312
|
+
if (typeof settings.model !== 'string' || settings.model === '') return { followups: [] };
|
|
295
313
|
const catalog = await modelCatalog(ctx);
|
|
296
314
|
const route = settings.model !== ''
|
|
297
315
|
? (() => {
|
package/package.json
CHANGED
|
@@ -79,7 +79,7 @@ test("the description survives a host without a system prompt service", async ()
|
|
|
79
79
|
const mod = await import(moduleUrl.href);
|
|
80
80
|
await mod.apply(ctx);
|
|
81
81
|
const description = registered[0].description;
|
|
82
|
-
|
|
82
|
+
assertMatch(description, /write every prompt and label in/i, "the description must carry the language rule without the system prompt section");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test("the bundle does not require optional host services to load", async () => {
|