@zhushanwen/pi-rename-session 0.11.2 → 0.12.0
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/README.md +5 -1
- package/package.json +5 -3
- package/skills/rename-session-ext-config/SKILL.md +13 -1
- package/src/__tests__/llm.test.ts +43 -0
- package/src/__tests__/pure.test.ts +54 -1
- package/src/llm.ts +58 -3
- package/src/pure.ts +49 -3
package/README.md
CHANGED
|
@@ -41,7 +41,9 @@ pi install npm:@zhushanwen/pi-rename-session
|
|
|
41
41
|
"model": { "type": "ref", "ref": "deepseek/deepseek-chat" },
|
|
42
42
|
"mode": "first-stop",
|
|
43
43
|
"maxTitleLength": 50,
|
|
44
|
-
"thinkingLevel": "off"
|
|
44
|
+
"thinkingLevel": "off",
|
|
45
|
+
"style": "slug",
|
|
46
|
+
"customInstruction": ""
|
|
45
47
|
}
|
|
46
48
|
```
|
|
47
49
|
|
|
@@ -52,6 +54,8 @@ pi install npm:@zhushanwen/pi-rename-session
|
|
|
52
54
|
| `mode` | `"first-prompt" \| "first-stop" \| "agent-tool"` | `"first-stop"` | 触发模式(三值互斥):首条请求即命名 / 首个成功 round 末命名(现状)/ agent 自主经 `rename_session` 工具命名。事件面每次事件 live 读(GUI 切换对活跃 session 的自动命名即时生效);工具注册面在 extension 工厂执行时求值(每 session 创建一次——taiji 每 session 一个 pi 进程;原生 pi CLI 同进程 /fork、/session 新建 session 亦重新求值),session 存活期内不回溯(pi 无 unregisterTool),切换后已存活 session 的残留工具由 execute 内 live mode 守卫拒绝并给恢复指引 |
|
|
53
55
|
| `maxTitleLength` | `number` | `50` | 标题最大长度(Unicode 码点数,须正整数) |
|
|
54
56
|
| `thinkingLevel` | `ModelThinkingLevel` | `"off"` | 标题 LLM 的 thinking 级别(`off` = 不传 reasoning,provider 默认) |
|
|
57
|
+
| `style` | `"slug" \| "sentence" \| "emoji" \| "custom"` | `"slug"` | 标题提示词风格:非 custom 枚举 = 内置词表(随版本升级自动跟随);`custom` = 消费 `customInstruction`(taiji 设置页「会话重命名」可视化配置) |
|
|
58
|
+
| `customInstruction` | `string` | `""` | 自定义标题提示词自由文本(仅 `style: "custom"` 时生效;**空串回落内置 slug 默认**;超 2000 码点防御截断;custom 非空时 systemPrompt 降级为「只输出标题文本」通用骨架,风格权威 = 用户指令) |
|
|
55
59
|
|
|
56
60
|
文件缺失/坏 JSON 返回默认值,不抛错。改完保存即生效(mtime 读时刷新,每个 `turn_end` / `message_end` 重新 load)。
|
|
57
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-rename-session",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"mode": "first-stop",
|
|
18
18
|
"maxTitleLength": 50,
|
|
19
|
-
"thinkingLevel": "off"
|
|
19
|
+
"thinkingLevel": "off",
|
|
20
|
+
"style": "slug",
|
|
21
|
+
"customInstruction": ""
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
]
|
|
@@ -33,8 +35,8 @@
|
|
|
33
35
|
"pi-package"
|
|
34
36
|
],
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@zhushanwen/pi-ext-guards": "0.4.4",
|
|
37
38
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
39
|
+
"@zhushanwen/pi-ext-guards": "0.4.4",
|
|
38
40
|
"@zhushanwen/pi-llm-shared": "0.11.2"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
@@ -45,9 +45,14 @@ interface RenameSessionConfig {
|
|
|
45
45
|
mode: "first-prompt" | "first-stop" | "agent-tool"; // 触发模式,默认 "first-stop"
|
|
46
46
|
maxTitleLength: number; // 标题最大长度(Unicode 码点),默认 50
|
|
47
47
|
thinkingLevel: ModelThinkingLevel; // 标题 LLM 的 thinking 级别,默认 "off"
|
|
48
|
+
style: "slug" | "sentence" | "emoji" | "custom"; // 标题提示词风格,默认 "slug"
|
|
49
|
+
customInstruction: string; // 自定义提示词自由文本(仅 custom 生效),默认 ""
|
|
48
50
|
}
|
|
49
51
|
```
|
|
50
52
|
|
|
53
|
+
> taiji 设置页(独立「会话重命名」菜单页)只暴露 first-prompt / first-stop 两档触发模式;
|
|
54
|
+
> `agent-tool` 与 prompt 配置仍可经 pi CLI / 手改配置文件使用(底层 rename_session 工具保留)。
|
|
55
|
+
|
|
51
56
|
### ModelSelector(仅支持 ref 精确指定)
|
|
52
57
|
|
|
53
58
|
| type | 形式 | 语义 |
|
|
@@ -66,10 +71,17 @@ interface RenameSessionConfig {
|
|
|
66
71
|
|
|
67
72
|
标题 LLM 的 thinking 级别,枚举 `off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(pi 的 `ModelThinkingLevel`)。默认 `"off"`:直接透传给 llm-shared,由 llm-shared 映射为不传 reasoning(provider 默认行为);`minimal`~`max` 透传给 reasoning(provider 不支持时静默忽略)。缺失或非法值回落 `"off"`。
|
|
68
73
|
|
|
74
|
+
### style + customInstruction(标题提示词风格)
|
|
75
|
+
|
|
76
|
+
- `style` 枚举 `slug` / `sentence` / `emoji` / `custom`,默认 `"slug"`(现状行为,零迁移):非 custom 枚举由内置词表(llm.ts `BUILTIN_TEMPLATES`)承载 user 指令,随 extension 版本升级自动跟随;`custom` 消费 `customInstruction`(taiji 设置页「会话重命名」可视化配置的写入面)
|
|
77
|
+
- `customInstruction`:自定义自由文本,**空串/全空白回落内置 slug 默认**(「空 = 内置默认」语义,不要把默认文本写死进配置);超 2000 码点防御截断(与 runtime 写侧/runtime RENAME_INSTRUCTION_MAX_LENGTH 同限)
|
|
78
|
+
- systemPrompt 分流:非 custom → slug 风格 systemPrompt(与词表自洽);custom 且指令非空 → 「只输出标题文本」通用骨架(风格权威 = 用户指令,消解双处风格冲突)
|
|
79
|
+
- 缺失/非法 `style` 回落 `"slug"`;非 string `customInstruction` 回落空串(粒度容错,坏字段不影响其他字段)
|
|
80
|
+
|
|
69
81
|
## 默认值
|
|
70
82
|
|
|
71
83
|
```json
|
|
72
|
-
{ "enabled": false, "model": { "type": "ref", "ref": "" }, "mode": "first-stop", "maxTitleLength": 50, "thinkingLevel": "off" }
|
|
84
|
+
{ "enabled": false, "model": { "type": "ref", "ref": "" }, "mode": "first-stop", "maxTitleLength": 50, "thinkingLevel": "off", "style": "slug", "customInstruction": "" }
|
|
73
85
|
```
|
|
74
86
|
|
|
75
87
|
## 配置示例
|
|
@@ -32,6 +32,8 @@ vi.mock("../pure.js", async (importActual) => {
|
|
|
32
32
|
import { callLLM, resolveModel } from "@zhushanwen/pi-llm-shared";
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
|
+
BUILTIN_TEMPLATES,
|
|
36
|
+
GENERIC_SYSTEM_PROMPT,
|
|
35
37
|
RENAME_INSTRUCTION,
|
|
36
38
|
RENAME_SYSTEM_PROMPT,
|
|
37
39
|
buildTitleMessages,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
extractFinalText,
|
|
40
42
|
extractUserPromptText,
|
|
41
43
|
isSubagentSession,
|
|
44
|
+
resolveTitlePrompt,
|
|
42
45
|
truncateForTitle,
|
|
43
46
|
} from "../llm.js";
|
|
44
47
|
import { cleanTitle, type RenameSessionConfig } from "../pure.js";
|
|
@@ -258,6 +261,44 @@ describe("RENAME_SYSTEM_PROMPT / RENAME_INSTRUCTION", () => {
|
|
|
258
261
|
});
|
|
259
262
|
});
|
|
260
263
|
|
|
264
|
+
// resolveTitlePrompt(rename-prompt-config 设计:风格 → 词表 / custom 自定义,system 骨架分流)
|
|
265
|
+
// ────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe("resolveTitlePrompt", () => {
|
|
268
|
+
const baseCfg = { ...BASE_CONFIG };
|
|
269
|
+
|
|
270
|
+
it("slug/sentence/emoji → BUILTIN_TEMPLATES 词表 + slug systemPrompt", () => {
|
|
271
|
+
expect(resolveTitlePrompt({ ...baseCfg, style: "slug" })).toEqual({
|
|
272
|
+
systemPrompt: RENAME_SYSTEM_PROMPT,
|
|
273
|
+
instruction: RENAME_INSTRUCTION,
|
|
274
|
+
});
|
|
275
|
+
expect(resolveTitlePrompt({ ...baseCfg, style: "sentence" }).instruction).toBe(BUILTIN_TEMPLATES.sentence);
|
|
276
|
+
expect(resolveTitlePrompt({ ...baseCfg, style: "emoji" }).instruction).toBe(BUILTIN_TEMPLATES.emoji);
|
|
277
|
+
// 非 custom 风格的 system 恒为 slug 版(词表与 slug 约束自洽)
|
|
278
|
+
expect(resolveTitlePrompt({ ...baseCfg, style: "sentence" }).systemPrompt).toBe(RENAME_SYSTEM_PROMPT);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("custom 且指令非空 → 自定义指令 + GENERIC_SYSTEM_PROMPT(风格权威 = 用户指令)", () => {
|
|
282
|
+
const r = resolveTitlePrompt({ ...baseCfg, style: "custom", customInstruction: "生成不超过 12 字的中文标题" });
|
|
283
|
+
expect(r.instruction).toBe("生成不超过 12 字的中文标题");
|
|
284
|
+
expect(r.systemPrompt).toBe(GENERIC_SYSTEM_PROMPT);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("custom 但指令为空/全空白 → 回落 slug 默认(「空 = 内置默认」语义)", () => {
|
|
288
|
+
const r = resolveTitlePrompt({ ...baseCfg, style: "custom", customInstruction: " " });
|
|
289
|
+
expect(r.instruction).toBe(RENAME_INSTRUCTION);
|
|
290
|
+
expect(r.systemPrompt).toBe(RENAME_SYSTEM_PROMPT);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("内置词表非空且含「只输出标题文本」输出契约(防模板回归退化为空串)", () => {
|
|
294
|
+
for (const t of Object.values(BUILTIN_TEMPLATES)) {
|
|
295
|
+
expect(t.length).toBeGreaterThan(0);
|
|
296
|
+
expect(t).toContain("只输出标题文本");
|
|
297
|
+
}
|
|
298
|
+
expect(GENERIC_SYSTEM_PROMPT).toContain("只输出标题文本");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
261
302
|
// ────────────────────────────────────────────────────
|
|
262
303
|
// 空 ref fallback(空 ref → ctx.model 跟随会话主模型;非空无效 ref → 静默跳过 + warn)
|
|
263
304
|
// ────────────────────────────────────────────────────
|
|
@@ -395,6 +436,8 @@ const BASE_CONFIG: RenameSessionConfig = {
|
|
|
395
436
|
mode: "first-stop",
|
|
396
437
|
maxTitleLength: 50,
|
|
397
438
|
thinkingLevel: "off",
|
|
439
|
+
style: "slug",
|
|
440
|
+
customInstruction: "",
|
|
398
441
|
};
|
|
399
442
|
|
|
400
443
|
/** 触发 turn 的最终 assistant message(turn_end 的 event.message)——thinking 混排验证只取 text。 */
|
|
@@ -8,6 +8,7 @@ import { clearConfigCache, getConfigPath } from "@zhushanwen/pi-llm-shared";
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RENAME_CONFIG,
|
|
11
|
+
RENAME_INSTRUCTION_MAX_LENGTH,
|
|
11
12
|
cleanTitle,
|
|
12
13
|
countSuccessfulAssistantReplies,
|
|
13
14
|
countUserMessages,
|
|
@@ -207,12 +208,21 @@ describe("cleanTitle", () => {
|
|
|
207
208
|
// ────────────────────────────────────────────────────
|
|
208
209
|
|
|
209
210
|
describe("DEFAULT_RENAME_CONFIG", () => {
|
|
210
|
-
it("默认值:enabled=false / model=空 ref / mode=first-stop / maxTitleLength=50 / thinkingLevel=off", () => {
|
|
211
|
+
it("默认值:enabled=false / model=空 ref / mode=first-stop / maxTitleLength=50 / thinkingLevel=off / style=slug / customInstruction=空串", () => {
|
|
211
212
|
expect(DEFAULT_RENAME_CONFIG.enabled).toBe(false);
|
|
212
213
|
expect(DEFAULT_RENAME_CONFIG.model).toEqual({ type: "ref", ref: "" });
|
|
213
214
|
expect(DEFAULT_RENAME_CONFIG.mode).toBe("first-stop");
|
|
214
215
|
expect(DEFAULT_RENAME_CONFIG.maxTitleLength).toBe(50);
|
|
215
216
|
expect(DEFAULT_RENAME_CONFIG.thinkingLevel).toBe("off");
|
|
217
|
+
// 三处默认值真相(本常量 / runtime rename-session-config.ts 镜像 / package.json
|
|
218
|
+
// startupConfig.content)同批收敛的字面值锁定:改任一侧须双侧同步(runtime 测试侧
|
|
219
|
+
// 以建文件基底断言锁定同值,单侧改动必红)
|
|
220
|
+
expect(DEFAULT_RENAME_CONFIG.style).toBe("slug");
|
|
221
|
+
expect(DEFAULT_RENAME_CONFIG.customInstruction).toBe("");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("RENAME_INSTRUCTION_MAX_LENGTH = 2000(与 @taiji/shared 协议字面值同值的手写镜像锁定,改任一侧双侧同步)", () => {
|
|
225
|
+
expect(RENAME_INSTRUCTION_MAX_LENGTH).toBe(2000);
|
|
216
226
|
});
|
|
217
227
|
});
|
|
218
228
|
|
|
@@ -238,6 +248,8 @@ describe("normalizeRenameConfig", () => {
|
|
|
238
248
|
mode: "first-prompt",
|
|
239
249
|
maxTitleLength: 30,
|
|
240
250
|
thinkingLevel: "high",
|
|
251
|
+
style: "emoji",
|
|
252
|
+
customInstruction: "自定义指令",
|
|
241
253
|
};
|
|
242
254
|
expect(normalizeRenameConfig(cfg)).toEqual(cfg);
|
|
243
255
|
});
|
|
@@ -266,6 +278,8 @@ describe("normalizeRenameConfig", () => {
|
|
|
266
278
|
mode: "first-stop",
|
|
267
279
|
maxTitleLength: 20,
|
|
268
280
|
thinkingLevel: "off",
|
|
281
|
+
style: "slug",
|
|
282
|
+
customInstruction: "",
|
|
269
283
|
});
|
|
270
284
|
});
|
|
271
285
|
|
|
@@ -330,8 +344,43 @@ describe("normalizeRenameConfig", () => {
|
|
|
330
344
|
mode: "first-stop",
|
|
331
345
|
maxTitleLength: 20,
|
|
332
346
|
thinkingLevel: "off",
|
|
347
|
+
style: "slug",
|
|
348
|
+
customInstruction: "",
|
|
333
349
|
});
|
|
334
350
|
});
|
|
351
|
+
|
|
352
|
+
// ── style + customInstruction(rename-prompt-config 设计)──
|
|
353
|
+
|
|
354
|
+
it("style 四个合法值(slug/sentence/emoji/custom)→ 原样保留", () => {
|
|
355
|
+
for (const style of ["slug", "sentence", "emoji", "custom"] as const) {
|
|
356
|
+
expect(normalizeRenameConfig({ style }).style).toBe(style);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("style 缺失/非法 → 默认 slug(零迁移,现状行为)", () => {
|
|
361
|
+
expect(normalizeRenameConfig({}).style).toBe("slug");
|
|
362
|
+
expect(normalizeRenameConfig({ style: "fancy" }).style).toBe("slug");
|
|
363
|
+
expect(normalizeRenameConfig({ style: 42 }).style).toBe("slug");
|
|
364
|
+
expect(normalizeRenameConfig({ style: null }).style).toBe("slug");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("customInstruction 非 string → 空串;合法值原样保留", () => {
|
|
368
|
+
expect(normalizeRenameConfig({ customInstruction: 42 }).customInstruction).toBe("");
|
|
369
|
+
expect(normalizeRenameConfig({ customInstruction: null }).customInstruction).toBe("");
|
|
370
|
+
expect(normalizeRenameConfig({ customInstruction: "生成标题" }).customInstruction).toBe("生成标题");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("customInstruction 超长防御截断(码点数,不劈开代理对)", () => {
|
|
374
|
+
const long = "a".repeat(RENAME_INSTRUCTION_MAX_LENGTH + 10);
|
|
375
|
+
expect(Array.from(normalizeRenameConfig({ customInstruction: long }).customInstruction)).toHaveLength(
|
|
376
|
+
RENAME_INSTRUCTION_MAX_LENGTH,
|
|
377
|
+
);
|
|
378
|
+
// emoji 星面字符(占 2 UTF-16 码元)按码点截断后仍可无损重组(无未配对代理残留)
|
|
379
|
+
const emojiPad = "😀".repeat(RENAME_INSTRUCTION_MAX_LENGTH + 1);
|
|
380
|
+
const truncated = normalizeRenameConfig({ customInstruction: emojiPad }).customInstruction;
|
|
381
|
+
expect(Array.from(truncated)).toHaveLength(RENAME_INSTRUCTION_MAX_LENGTH);
|
|
382
|
+
expect(Array.from(truncated).join("")).toBe(truncated);
|
|
383
|
+
});
|
|
335
384
|
});
|
|
336
385
|
|
|
337
386
|
// ────────────────────────────────────────────────────
|
|
@@ -427,6 +476,8 @@ describe("loadRenameConfig / saveRenameConfig", () => {
|
|
|
427
476
|
mode: "first-prompt" as const,
|
|
428
477
|
maxTitleLength: 30,
|
|
429
478
|
thinkingLevel: "off",
|
|
479
|
+
style: "custom" as const,
|
|
480
|
+
customInstruction: "自定义指令文本",
|
|
430
481
|
};
|
|
431
482
|
const saveResult = saveRenameConfig(cfg);
|
|
432
483
|
expect(saveResult.success).toBe(true);
|
|
@@ -441,6 +492,8 @@ describe("loadRenameConfig / saveRenameConfig", () => {
|
|
|
441
492
|
mode: "agent-tool",
|
|
442
493
|
maxTitleLength: 50,
|
|
443
494
|
thinkingLevel: "off",
|
|
495
|
+
style: "slug",
|
|
496
|
+
customInstruction: "",
|
|
444
497
|
});
|
|
445
498
|
const filePath = path.join(tmpAgentDir, "config", "rename-session-ext-config.json");
|
|
446
499
|
expect(fs.existsSync(filePath)).toBe(true);
|
package/src/llm.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { isRecord } from "@zhushanwen/pi-ext-guards";
|
|
|
7
7
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
8
8
|
import { callLLM, joinTextBlocks, resolveModel } from "@zhushanwen/pi-llm-shared";
|
|
9
9
|
|
|
10
|
-
import { type RenameSessionConfig, cleanTitle, truncateCodePoints } from "./pure.js";
|
|
10
|
+
import { type RenameSessionConfig, type RenameStyle, cleanTitle, truncateCodePoints } from "./pure.js";
|
|
11
11
|
|
|
12
12
|
const logger = getLogger("rename-session");
|
|
13
13
|
|
|
@@ -32,6 +32,57 @@ export const RENAME_SYSTEM_PROMPT =
|
|
|
32
32
|
export const RENAME_INSTRUCTION =
|
|
33
33
|
"根据以上对话,为这个会话生成一个 slug 式标题。要求:\n- 名词或动名词词组,例:「修复登录超时」「重构配置加载」「refactor-config-loader」\n- 反例(错误):「我帮你修复了登录 bug」「This session is about fixing bugs」\n- 英文小写 kebab-case,中文直接用词组,不要句号\n使用对话所用的语言。只输出标题文本。";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 「完整短句」风格内置词表(rename-prompt-config 设计):不超过 20 字的完整短句,概括任务
|
|
37
|
+
* 目标或结果。与 slug 风格同为 user 指令形态(追加在对话末尾),正反例锚定与 RENAME_INSTRUCTION 同范式。
|
|
38
|
+
* 仅 BUILTIN_TEMPLATES 消费(renderer 侧是跨包字面量对拍测试,不 import 本模块)。
|
|
39
|
+
*/
|
|
40
|
+
const SENTENCE_INSTRUCTION =
|
|
41
|
+
"根据以上对话,为这个会话生成一个简短标题:一个不超过 20 字的完整短句,概括本次任务的目标或结果,不加句尾标点。反例(错误):「重构」。使用对话所用的语言。只输出标题文本。";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 「Emoji 点缀」风格内置词表(rename-prompt-config 设计):以贴切 emoji 开头 + 不超过 15 字
|
|
45
|
+
* 词组或短句。正例锚定输出形态,避免模型漏掉 emoji 或输出多行。
|
|
46
|
+
* 仅 BUILTIN_TEMPLATES 消费(同 SENTENCE_INSTRUCTION,renderer 侧字面量对拍不 import)。
|
|
47
|
+
*/
|
|
48
|
+
const EMOJI_INSTRUCTION =
|
|
49
|
+
"根据以上对话,为这个会话生成一个标题:以一个贴切的 emoji 开头,后接不超过 15 字的词组或短句。例:「🔧 重构配置加载」「🐛 修复登录超时」。使用对话所用的语言。只输出标题文本。";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 自定义指令时的通用 systemPrompt 骨架(rename-prompt-config 设计):只保留「你是标题生成器 +
|
|
53
|
+
* 只输出标题文本」的输出契约,剥离 slug 风格细节——避免「system 说 slug、user 自定义说短句」
|
|
54
|
+
* 的风格冲突(用户自定义指令时唯一风格权威 = 用户指令)。
|
|
55
|
+
*/
|
|
56
|
+
export const GENERIC_SYSTEM_PROMPT =
|
|
57
|
+
"你是会话标题生成器。根据对话生成一个简短标题,只输出标题文本。";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 内置风格词表(rename-prompt-config 设计):style 非 custom 时的 user 指令映射。
|
|
61
|
+
* custom 不在此表(消费 config.customInstruction,空串回落 slug 默认)。
|
|
62
|
+
*/
|
|
63
|
+
export const BUILTIN_TEMPLATES: Readonly<Record<Exclude<RenameStyle, "custom">, string>> = {
|
|
64
|
+
slug: RENAME_INSTRUCTION,
|
|
65
|
+
sentence: SENTENCE_INSTRUCTION,
|
|
66
|
+
emoji: EMOJI_INSTRUCTION,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 按配置解析生效的标题提示词(rename-prompt-config 设计核心收敛点):
|
|
71
|
+
* - 非 custom 枚举风格 → BUILTIN_TEMPLATES 词表 + RENAME_SYSTEM_PROMPT(slug 约束与词表自洽)
|
|
72
|
+
* - custom 且自定义指令非空 → 自定义指令 + GENERIC_SYSTEM_PROMPT(风格权威 = 用户指令,
|
|
73
|
+
* system 降级为输出契约骨架,消解双处风格冲突)
|
|
74
|
+
* - custom 但自定义指令为空(含全空白)→ 回落 slug 默认(与「空 = 内置默认」语义一致)
|
|
75
|
+
*/
|
|
76
|
+
export function resolveTitlePrompt(config: RenameSessionConfig): { systemPrompt: string; instruction: string } {
|
|
77
|
+
if (config.style === "custom" && config.customInstruction.trim() !== "") {
|
|
78
|
+
return { systemPrompt: GENERIC_SYSTEM_PROMPT, instruction: config.customInstruction };
|
|
79
|
+
}
|
|
80
|
+
// custom 但自定义指令为空(含全空白)→ 回落 slug 默认(「空 = 内置默认」语义);
|
|
81
|
+
// 其余枚举 → 词表直查(Record 键完备,无 fallback 分支)
|
|
82
|
+
const style: Exclude<RenameStyle, "custom"> = config.style === "custom" ? "slug" : config.style;
|
|
83
|
+
return { systemPrompt: RENAME_SYSTEM_PROMPT, instruction: BUILTIN_TEMPLATES[style] };
|
|
84
|
+
}
|
|
85
|
+
|
|
35
86
|
// ──────────────────────── 纯函数 ────────────────────────
|
|
36
87
|
|
|
37
88
|
/**
|
|
@@ -283,11 +334,15 @@ export async function callRenameLLM(
|
|
|
283
334
|
}
|
|
284
335
|
const finalText = extractFinalText(finalMessage);
|
|
285
336
|
|
|
337
|
+
// 提示词解析(rename-prompt-config):内置风格 → 词表 + slug system;custom 非空 →
|
|
338
|
+
// 自定义指令 + 通用骨架 system(风格权威 = 用户指令)。事件面 live 读 config 后每次调用重解析。
|
|
339
|
+
const { systemPrompt, instruction } = resolveTitlePrompt(config);
|
|
340
|
+
|
|
286
341
|
// 两段输入信号各截断 4000 码点(成本可控,标题语义足够)
|
|
287
342
|
const messages = buildTitleMessages(
|
|
288
343
|
truncateForTitle(userPrompt),
|
|
289
344
|
truncateForTitle(finalText),
|
|
290
|
-
|
|
345
|
+
instruction,
|
|
291
346
|
);
|
|
292
347
|
|
|
293
348
|
// debug 内省:日志与 callLLM 收到的是同一 messages 对象,日志内容即 LLM 收到的内容;
|
|
@@ -300,7 +355,7 @@ export async function callRenameLLM(
|
|
|
300
355
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
301
356
|
const result = await callLLM(ctx, {
|
|
302
357
|
model,
|
|
303
|
-
systemPrompt
|
|
358
|
+
systemPrompt,
|
|
304
359
|
messages,
|
|
305
360
|
// 输出预算必须覆盖「thinking + 标题」两部分:thinking 计入 maxTokens 的模型
|
|
306
361
|
//(StepFun step-5-preview 实测 thinking 600-1500 tokens 且不可禁用/不可压低,
|
package/src/pure.ts
CHANGED
|
@@ -17,6 +17,20 @@ import {
|
|
|
17
17
|
/** 触发模式三值枚举:互斥,默认 "first-stop"(零行为迁移)。 */
|
|
18
18
|
export type RenameMode = "first-prompt" | "first-stop" | "agent-tool";
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 标题提示词风格枚举(设计 rename-prompt-config):slug/sentence/emoji = 内置词表(随 extension
|
|
22
|
+
* 版本升级自动跟随),custom = 用户自定义自由文本(customInstruction 承载,空串回落内置 slug)。
|
|
23
|
+
* 与 @taiji/shared RenameStyle 值域同构(跨包不 import,本包 role=universal)。
|
|
24
|
+
*/
|
|
25
|
+
export type RenameStyle = "slug" | "sentence" | "emoji" | "custom";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 自定义标题提示词长度上限(Unicode 码点数)——与 @taiji/shared RENAME_INSTRUCTION_MAX_LENGTH
|
|
29
|
+
* 同值(跨包不 import 的镜像,改值须双侧同步)。三消费点:本包 normalize 防御截断 /
|
|
30
|
+
* runtime setRenamePrompt 写侧归一 / renderer 编辑器计数。
|
|
31
|
+
*/
|
|
32
|
+
export const RENAME_INSTRUCTION_MAX_LENGTH = 2000;
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* rename-session 配置 schema(落盘到 `<agentDir>/config/rename-session-ext-config.json`,路径由 llm-shared 推导)。
|
|
22
36
|
*
|
|
@@ -25,7 +39,11 @@ export type RenameMode = "first-prompt" | "first-stop" | "agent-tool";
|
|
|
25
39
|
* auto-rename-enabled flag 文件(live 覆盖源,存在即开,见下方 [COMPAT] 契约)
|
|
26
40
|
* - model 从「搭便车 ctx.model」改为独立 `ModelSelector`(仅支持 ref 精确指定;空 ref 跟随会话主模型,见 llm.ts 的空 ref fallback)
|
|
27
41
|
* - maxTitleLength 保留(默认 50)
|
|
28
|
-
* -
|
|
42
|
+
* - style + customInstruction:标题提示词风格(rename-prompt-config 设计):非 custom 枚举 =
|
|
43
|
+
* 内置词表(llm.ts BUILTIN_TEMPLATES,随版本升级),custom = 自定义自由文本(空串回落
|
|
44
|
+
* 内置 slug 默认)。GUI 配置页(taiji RenameSessionPage)经 runtime config.get/setRenamePrompt
|
|
45
|
+
* 读写,pi CLI 用户亦可直接改本文件
|
|
46
|
+
* - renameInstruction 内置默认由代码常量 RENAME_INSTRUCTION 承载(llm.ts)
|
|
29
47
|
* - mode:first-prompt 首条 user 消息触发 / first-stop 首个成功 round 末触发(默认,现状)/
|
|
30
48
|
* agent-tool 不自动生成、注册 rename_session 工具由 agent 自主改名
|
|
31
49
|
*/
|
|
@@ -38,6 +56,13 @@ export interface RenameSessionConfig {
|
|
|
38
56
|
mode: RenameMode;
|
|
39
57
|
/** 标题最大长度(Unicode 码点数)。 */
|
|
40
58
|
maxTitleLength: number;
|
|
59
|
+
/**
|
|
60
|
+
* 标题提示词风格(默认 "slug",现状行为)。非 custom 枚举由 llm.ts BUILTIN_TEMPLATES
|
|
61
|
+
* 提供词表;custom 消费 customInstruction(空串回落内置 slug 默认)。
|
|
62
|
+
*/
|
|
63
|
+
style: RenameStyle;
|
|
64
|
+
/** 自定义标题提示词自由文本(仅 style=custom 时生效;超长由 normalize 防御截断)。 */
|
|
65
|
+
customInstruction: string;
|
|
41
66
|
/**
|
|
42
67
|
* 标题生成 LLM 的 thinking 级别(字符串透传;合法档位按模型 supportedLevels 在调用点判定)。
|
|
43
68
|
* 默认 "off":不传 pi-ai reasoning(provider 默认行为,与旧版本一致);
|
|
@@ -51,6 +76,9 @@ export interface RenameSessionConfig {
|
|
|
51
76
|
/** 合法触发模式清单(与 RenameMode 一致;normalize 校验用)。Set 免 as 断言。 */
|
|
52
77
|
const RENAME_MODES: ReadonlySet<string> = new Set(["first-prompt", "first-stop", "agent-tool"]);
|
|
53
78
|
|
|
79
|
+
/** 合法提示词风格清单(与 RenameStyle 一致;normalize 校验用)。 */
|
|
80
|
+
const RENAME_STYLES: ReadonlySet<string> = new Set(["slug", "sentence", "emoji", "custom"]);
|
|
81
|
+
|
|
54
82
|
/**
|
|
55
83
|
* 类型谓词:unknown 是否为合法触发模式(normalizeRenameConfig 校验用,单点断言)。
|
|
56
84
|
* Set.has 运行时兜底 + 类型收窄,调用方无需再断言。
|
|
@@ -59,13 +87,20 @@ function isRenameMode(raw: unknown): raw is RenameMode {
|
|
|
59
87
|
return typeof raw === "string" && RENAME_MODES.has(raw);
|
|
60
88
|
}
|
|
61
89
|
|
|
62
|
-
/**
|
|
90
|
+
/** 类型谓词:unknown 是否为合法提示词风格(与 isRenameMode 同款范式,单点断言)。 */
|
|
91
|
+
function isRenameStyle(raw: unknown): raw is RenameStyle {
|
|
92
|
+
return typeof raw === "string" && RENAME_STYLES.has(raw);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 默认配置:关闭、空 ref(跟随会话主模型,见 llm.ts 的空 ref fallback)、first-stop 触发、标题上限 50、不启用 thinking、slug 内置风格、无自定义指令。 */
|
|
63
96
|
export const DEFAULT_RENAME_CONFIG: RenameSessionConfig = {
|
|
64
97
|
enabled: false,
|
|
65
98
|
model: { type: "ref", ref: "" },
|
|
66
99
|
mode: "first-stop",
|
|
67
100
|
maxTitleLength: 50,
|
|
68
101
|
thinkingLevel: "off",
|
|
102
|
+
style: "slug",
|
|
103
|
+
customInstruction: "",
|
|
69
104
|
};
|
|
70
105
|
|
|
71
106
|
/** llm-shared loadConfig/saveConfig 的包名(决定文件名 rename-session-ext-config.json,llm-shared getConfigPath 追加 -ext-config.json 后缀)。 */
|
|
@@ -145,7 +180,18 @@ export function normalizeRenameConfig(raw: unknown): RenameSessionConfig {
|
|
|
145
180
|
|
|
146
181
|
const thinkingLevel = normalizeThinkingLevel(obj.thinkingLevel) ?? DEFAULT_RENAME_CONFIG.thinkingLevel;
|
|
147
182
|
|
|
148
|
-
|
|
183
|
+
// style 逐字段校验:旧 config 无字段 / 非法值 → 默认 slug(零迁移,现状行为)
|
|
184
|
+
const style = isRenameStyle(obj.style) ? obj.style : DEFAULT_RENAME_CONFIG.style;
|
|
185
|
+
|
|
186
|
+
// customInstruction:非字符串 → 空串;超长防御截断(码点数,与 runtime 写侧归一同限)
|
|
187
|
+
const rawInstruction = typeof obj.customInstruction === "string" ? obj.customInstruction : "";
|
|
188
|
+
const instructionChars = Array.from(rawInstruction);
|
|
189
|
+
const customInstruction =
|
|
190
|
+
instructionChars.length > RENAME_INSTRUCTION_MAX_LENGTH
|
|
191
|
+
? instructionChars.slice(0, RENAME_INSTRUCTION_MAX_LENGTH).join("")
|
|
192
|
+
: rawInstruction;
|
|
193
|
+
|
|
194
|
+
return { enabled, model, mode, maxTitleLength, thinkingLevel, style, customInstruction };
|
|
149
195
|
}
|
|
150
196
|
|
|
151
197
|
/**
|