@zhushanwen/pi-rename-session 0.14.3 → 0.14.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/README.md +6 -6
- package/package.json +4 -4
- package/src/__tests__/index.test.ts +0 -76
- package/src/index.ts +6 -15
- package/src/llm.ts +3 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Pi rename-session 扩展 — 三种触发模式下自动/自主为会话生成 s
|
|
|
11
11
|
- **触发时机(first-stop 路径)**:只在 round 的最终 turn(`stopReason === "stop"`)评估——工具中间轮 / error / aborted / length 轮不评估,error 轮延迟到下一个成功轮命名
|
|
12
12
|
- **两段输入(自动路径)**:`[user(首条 prompt), assistant(最终回复)]` 两段信号(各截断 4000 码点),不含 toolCall/toolResult 过程数据,token 成本不随工具数增长;first-prompt 模式无 assistant 段(降级两条)
|
|
13
13
|
- **独立选模**:标题生成用独立的 `ModelSelector` 配置(仅支持 `ref` 精确指定 provider/model);**未配置(空 ref)时跟随会话主模型**(`ctx.model`),开箱即用
|
|
14
|
-
-
|
|
14
|
+
- **可靠性行为**:LLM 调用不设包内超时(ADR-0122——挂死由 provider SDK/HTTP 层默认超时直报,无 30s 类包内兜底);写入前重查手动名(防覆盖 LLM 调用窗口内的竞态,agent-tool 工具路径除外——显式改名允许覆盖);任何失败静默跳过保留原 label,绝不阻断 agent 循环
|
|
15
15
|
- 标题直接 `setSessionName` 写入,不进 session history(不污染对话记录)
|
|
16
16
|
- **子 session 自动排除**:subagent 子进程 session 不触发 rename(避免给临时产物起名)
|
|
17
17
|
|
|
@@ -84,7 +84,7 @@ pi install npm:@zhushanwen/pi-rename-session
|
|
|
84
84
|
3. **O(1) 快速路径(first-stop)**:只有 `stopReason === "stop"` 的 turn 才继续——**rename 一定在 round 末触发**(最终 turn 的 message 即最终 assistant 回复,final text 零遍历可得),不会在首个 iteration 中途命名。first-prompt 入口的对应检查:首条 user 判定 = handler 执行时 `getEntries()` 中 user message 计数 === 0(pi 的 extension handler 先于该条 message 的 entries append 执行,本条即 session 首条 user;steering/follow-up 消息到达时首条已入 entries,天然不重复触发)。
|
|
85
85
|
4. **首 round 判定(first-stop)**:session entries 中成功(stop)assistant 回复数 === 1 才触发(后续 round 不重复 rename;error 轮的 assistant 回复不计数,延迟到下一个成功轮)。
|
|
86
86
|
5. **两段输入构造**:`[user(首条 prompt), assistant(最终回复文本), user(instruction)]`——任务意图 + 轮次结论恰好与标题语义对齐,不含 toolCall/toolResult 过程数据;两段文本各截断 4000 Unicode 码点(中文场景约 4k token/段,成本可控且不随工具数增长)。assistant 段为空(first-prompt 模式 / 纯工具结束的 round)时降级为两条——标题主信号本就是 prompt。
|
|
87
|
-
6. **LLM 生成 slug 标题**:独立精简 system prompt(<200 字符的 slug 词组约束,非整个 agent prompt)+ instruction(正反例 few-shot,作为追加 user message 发送)+ `tools: []` + `maxTokens: 64`,按 `config.model` 独立选模(空 ref 跟随会话主模型)发起一次 LLM
|
|
87
|
+
6. **LLM 生成 slug 标题**:独立精简 system prompt(<200 字符的 slug 词组约束,非整个 agent prompt)+ instruction(正反例 few-shot,作为追加 user message 发送)+ `tools: []` + `maxTokens: 64`,按 `config.model` 独立选模(空 ref 跟随会话主模型)发起一次 LLM 调用;不设包内超时(ADR-0122——挂死由 provider SDK 默认超时直报,OpenAI/Anthropic SDK 默认 10 分钟;失败归一走静默跳过)。
|
|
88
88
|
7. **写入**:cleanTitle 清洗(去首尾引号 / markdown 强调标记 / 句尾标点、空白归一、按码点截断)后 `setSessionName` 写入。**写入前重查** `pi.getSessionName()`——LLM 调用窗口(2-30s)内用户手动命名的竞态由此兜住,已有名则 skip 不覆盖。**不**写入 session history,对话记录不受影响。
|
|
89
89
|
8. **`rename_session` 工具(agent-tool)**:execute 内检查链依次为——先拒绝 subagent session(子会话是临时产物,subagent 排除检查覆盖 message_end / turn_end / 工具三入口,C-ext-21);再 live 读 config 检查——mode 已切走时拒绝(错误文案含恢复指引:切回 agent-tool / 手动改名);title 经 cleanTitle 清洗,空值拒绝;非空直接 `setSessionName` **不走防覆盖检查**(agent 显式调用 = 代表用户的意图,允许覆盖任何既有名,含自动名/语义名)。
|
|
90
90
|
|
|
@@ -99,7 +99,7 @@ rename 是 best-effort 副作用,任何失败静默跳过、绝不阻断 agent
|
|
|
99
99
|
| 非 round-1(成功回复数 ≠ 1) | skip(count=N),一次性语义 |
|
|
100
100
|
| 非 first-prompt 模式收到 user message_end | skip(mode=<mode>),事件路径 live 分派 |
|
|
101
101
|
| 非 session 首条 user message(first-prompt) | skip(userCount=N),一次性语义 |
|
|
102
|
-
| LLM
|
|
102
|
+
| LLM 调用失败(含 provider SDK 默认超时,10 分钟级) | 记录失败日志,保留原 label(不做重试;用户可手动命名) |
|
|
103
103
|
| 标题清洗后为空 | skip(title empty) |
|
|
104
104
|
| 写入前发现已有手动名(自动路径) | skip(name exists),不覆盖 |
|
|
105
105
|
| `rename_session` 工具被调用但 mode 已切走 | execute 检查拒绝(isError 文案含恢复指引) |
|
|
@@ -129,11 +129,11 @@ rename 是 best-effort 副作用,任何失败静默跳过、绝不阻断 agent
|
|
|
129
129
|
| 11 | `skip: empty prompt` | message_end handler(`firstPrompt skip: empty prompt`) | user message 载荷无文本(理论不发生) |
|
|
130
130
|
| 12 | `tool renamed to "<title>"` | rename_session 工具 execute | agent-tool 路径写入成功(`setSessionName` 之后打出,无防覆盖前缀) |
|
|
131
131
|
|
|
132
|
-
另有三条**非 debug 常开**日志(无条件经 `logger.warn` 落 appendEntry entry,任意环境可 `grep "rename-session:log" <session.jsonl>` 排障):`rename LLM call failed`(`logger.warn(msg, { error })` 形态,error 详情在结构化 data
|
|
132
|
+
另有三条**非 debug 常开**日志(无条件经 `logger.warn` 落 appendEntry entry,任意环境可 `grep "rename-session:log" <session.jsonl>` 排障):`rename LLM call failed`(`logger.warn(msg, { error })` 形态,error 详情在结构化 data 字段;挂死类失败(SDK/HTTP 层默认超时)错误文本为空时,llm-shared callLLM 内部的 extractText 将其归一为 `unknown error`——extension 侧 `result.error ?? "unknown error"` 只兜 null/undefined,空串兜底发生在 llm-shared 层)、`model not available, skipping`(选模失败)、`title empty after clean, skipping rename`(调用成功但标题清洗后为空,结构化 data 含 `model` / `stopReason` / `raw`(≤100 码点标题候选预览)——`stopReason=length` 表示输出预算被 thinking 吃尽,是上调 `maxTokens` 常量的数据信号)。handler 侧日志 message 带 `t=<ISO时间>` 与 `turnIndex`;llm 侧带 `t=<ISO时间>`、无 turnIndex。文件日志通道(`<agentDir>/logs/`)需 `TAIJI_AGENT_DEBUG=1`,独立 pi 用户排障优先 grep session 文件(无需复跑)。
|
|
133
133
|
|
|
134
134
|
## E2E 验收
|
|
135
135
|
|
|
136
|
-
E2E 是本地人工触发的验收资产(真实 pi 进程 + 真实模型,不进常规 CI),覆盖七个场景:A1 触发时机证据链(流序/内容匹配/负向/行序/结构五重——结构断言 = 仅一条 LLM request + [user,assistant,user] 三元组)、A2 slug 风格 ×3、A3 防覆盖(静态/竞态/一次性)、A4 error 轮两阶段(`--session` 续跑)、A5
|
|
136
|
+
E2E 是本地人工触发的验收资产(真实 pi 进程 + 真实模型,不进常规 CI),覆盖七个场景:A1 触发时机证据链(流序/内容匹配/负向/行序/结构五重——结构断言 = 仅一条 LLM request + [user,assistant,user] 三元组)、A2 slug 风格 ×3、A3 防覆盖(静态/竞态/一次性)、A4 error 轮两阶段(`--session` 续跑)、A5 挂死直报(hang provider——无包内超时(ADR-0122),挂死调用由 provider SDK 默认超时(10 分钟级)报失败,pi 存活)、A6 first-prompt 模式(触发时点 = 首条 assistant `message_start` 前 rename LLM 请求已发出 + 标题仅基于 prompt + 后续 round 不再改名;完成序不做 gate)、A7 agent-tool 模式(工具改名即时写入覆盖既有名 + first-stop 对照进程零 `rename_session` toolCall entry)。
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
cd extensions/universal/rename-session
|
|
@@ -173,7 +173,7 @@ rename-session/
|
|
|
173
173
|
├── index.ts # 工厂入口(message_end/turn_end handler 按模式分派 + rename_session 工具注册 + /auto-rename 命令)
|
|
174
174
|
├── landing.ts # landTitle:两入口共用写入管道(起 LLM → 防覆盖重查 → setSessionName → renamed to 日志 + usage 记录)
|
|
175
175
|
├── commands.ts # /auto-rename on|off|status 命令(enable/disable 别名)
|
|
176
|
-
├── llm.ts # callRenameLLM / 两段输入构造 / debug 内省 /
|
|
176
|
+
├── llm.ts # callRenameLLM / 两段输入构造 / debug 内省 / 档位判定
|
|
177
177
|
├── pure.ts # 纯函数(配置 / 首轮计数 / cleanTitle)
|
|
178
178
|
└── __tests__/ # 单测(pure / commands / llm mock / index 集成)
|
|
179
179
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-rename-session",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"pi-package"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zhushanwen/extension-protocol": "0.
|
|
39
|
-
"@zhushanwen/pi-ext-guards": "0.5.
|
|
38
|
+
"@zhushanwen/extension-protocol": "0.27.0",
|
|
39
|
+
"@zhushanwen/pi-ext-guards": "0.5.1",
|
|
40
40
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
41
|
-
"@zhushanwen/pi-llm-shared": "0.12.
|
|
41
|
+
"@zhushanwen/pi-llm-shared": "0.12.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@vitest/coverage-v8": "^4.1.9",
|
|
@@ -922,79 +922,3 @@ describe("rename_session 工具注册与 execute 检查(D3)", () => {
|
|
|
922
922
|
expect(setup.setSessionNameMock).toHaveBeenCalledWith("正常标题");
|
|
923
923
|
});
|
|
924
924
|
});
|
|
925
|
-
|
|
926
|
-
// ────────────────────────────────────────────────────
|
|
927
|
-
// first-prompt 在途去重(工厂闭包级——pi extensionCache 缓存 factory,
|
|
928
|
-
// 每次 session 创建重执行工厂,闭包变量 = per-session 生命周期)
|
|
929
|
-
// ────────────────────────────────────────────────────
|
|
930
|
-
|
|
931
|
-
describe("first-prompt in-flight 去重(工厂闭包级)", () => {
|
|
932
|
-
beforeEach(() => {
|
|
933
|
-
vi.clearAllMocks();
|
|
934
|
-
vi.mocked(loadRenameConfig).mockReset();
|
|
935
|
-
vi.mocked(resolveModel).mockReset();
|
|
936
|
-
vi.mocked(callLLM).mockReset();
|
|
937
|
-
vi.mocked(loadRenameConfig).mockReturnValue(FIRST_PROMPT_CONFIG);
|
|
938
|
-
vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
it("TC-I1: LLM 调用窗口内同 session 第二条 message_end 被拦(callLLM 恒 1 次),settle 后标志释放(再触发可发起)", async () => {
|
|
942
|
-
const setup = createMockPi();
|
|
943
|
-
renameSessionExtension(setup.pi);
|
|
944
|
-
// entries 恒空:模拟「首条 user 尚未 append」的极端交错窗口(首条判定不拦,靠 in-flight)
|
|
945
|
-
const ctx = createMockCtx({ entries: [] });
|
|
946
|
-
let resolveLLM!: (value: { ok: true; content: string }) => void;
|
|
947
|
-
vi.mocked(callLLM).mockImplementation(
|
|
948
|
-
() =>
|
|
949
|
-
new Promise((res) => {
|
|
950
|
-
resolveLLM = res;
|
|
951
|
-
}),
|
|
952
|
-
);
|
|
953
|
-
|
|
954
|
-
await fireMessageEnd(setup, ctx);
|
|
955
|
-
await vi.waitFor(() => expect(callLLM).toHaveBeenCalledTimes(1));
|
|
956
|
-
|
|
957
|
-
// in-flight 窗口内第二条:被标志拦截,不二次发起
|
|
958
|
-
await fireMessageEnd(setup, ctx);
|
|
959
|
-
await new Promise((r) => setTimeout(r, 0)); // flush 微任务链
|
|
960
|
-
expect(callLLM).toHaveBeenCalledTimes(1);
|
|
961
|
-
|
|
962
|
-
// settle → finally 释放标志 → 再到达的 message_end 可再次发起(真实时序下会被
|
|
963
|
-
// userCount ≥1 拦截,此处 entries 恒空以孤立验证释放逻辑本身)
|
|
964
|
-
resolveLLM({ ok: true, content: "标题一" });
|
|
965
|
-
await vi.waitFor(() => expect(setup.setSessionNameMock).toHaveBeenCalledWith("标题一"));
|
|
966
|
-
await fireMessageEnd(setup, ctx);
|
|
967
|
-
await vi.waitFor(() => expect(callLLM).toHaveBeenCalledTimes(2));
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
it("TC-I2: 工厂闭包级(非模块级)——同进程第二个 session(工厂重执行)的 in-flight 独立,不被第一个 session 吞掉命名机会", async () => {
|
|
971
|
-
const setupA = createMockPi();
|
|
972
|
-
renameSessionExtension(setupA.pi);
|
|
973
|
-
const setupB = createMockPi();
|
|
974
|
-
renameSessionExtension(setupB.pi);
|
|
975
|
-
|
|
976
|
-
let resolveA!: (value: { ok: true; content: string }) => void;
|
|
977
|
-
let resolveB!: (value: { ok: true; content: string }) => void;
|
|
978
|
-
let callCount = 0;
|
|
979
|
-
vi.mocked(callLLM).mockImplementation(() => {
|
|
980
|
-
callCount++;
|
|
981
|
-
return new Promise((res) => {
|
|
982
|
-
if (callCount === 1) resolveA = res;
|
|
983
|
-
else resolveB = res;
|
|
984
|
-
});
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// session A 触发并停在其 LLM 窗口内(A 的 in-flight=true)
|
|
988
|
-
await fireMessageEnd(setupA, createMockCtx({ entries: [] }));
|
|
989
|
-
await vi.waitFor(() => expect(callLLM).toHaveBeenCalledTimes(1));
|
|
990
|
-
|
|
991
|
-
// session B(/fork、/session 切换后工厂重执行)同窗口触发:B 的闭包标志独立 → 照常发起
|
|
992
|
-
await fireMessageEnd(setupB, createMockCtx({ entries: [] }));
|
|
993
|
-
await vi.waitFor(() => expect(callLLM).toHaveBeenCalledTimes(2));
|
|
994
|
-
|
|
995
|
-
resolveA({ ok: true, content: "A 的标题" });
|
|
996
|
-
resolveB({ ok: true, content: "B 的标题" });
|
|
997
|
-
await vi.waitFor(() => expect(setupA.setSessionNameMock).toHaveBeenCalledWith("A 的标题"));
|
|
998
|
-
await vi.waitFor(() => expect(setupB.setSessionNameMock).toHaveBeenCalledWith("B 的标题"));
|
|
999
|
-
});
|
|
1000
|
-
});
|
package/src/index.ts
CHANGED
|
@@ -153,11 +153,6 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
|
|
|
153
153
|
registerRenameSessionTool(pi);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// 在途去重标志:工厂闭包级而非模块级——pi 的 extensionCache 缓存 factory、每次
|
|
157
|
-
// session 创建重执行工厂函数,闭包变量 = per-session 生命周期;模块顶层变量进程级存活,
|
|
158
|
-
// 进程内 /fork、/session 切换 session 时会跨 session 污染、吞掉新 session 的唯一命名机会。
|
|
159
|
-
let firstPromptInFlight = false;
|
|
160
|
-
|
|
161
156
|
// message_end 入口(first-prompt 模式):首条 user 消息发出即命名,不等回复完成。
|
|
162
157
|
pi.on("message_end", async (event: MessageEndLikeEvent, ctx: ExtensionContext) => {
|
|
163
158
|
// handler 侧 debug 日志(C3 同款契约):firstPrompt 前缀区分触发入口(与 turn_end 路径
|
|
@@ -201,17 +196,13 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
|
|
|
201
196
|
return;
|
|
202
197
|
}
|
|
203
198
|
|
|
204
|
-
// 7.
|
|
205
|
-
// 标志兜住极端交错双发;settle 后释放(首条窗口已消费,释放后新到达靠步 5 拦截)
|
|
206
|
-
if (firstPromptInFlight) return;
|
|
207
|
-
firstPromptInFlight = true;
|
|
208
|
-
|
|
209
|
-
// 8. fire-and-forget(同 turn_end 契约):handler 立即 resolve,LLM 调用与 setSessionName
|
|
199
|
+
// 7. fire-and-forget(同 turn_end 契约):handler 立即 resolve,LLM 调用与 setSessionName
|
|
210
200
|
// 在后台异步完成。finalMessage 传空 content——first-prompt 语义即「不等回复」,
|
|
211
|
-
// finalText 为空串走 buildTitleMessages 两条降级(标题只基于 prompt
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
201
|
+
// finalText 为空串走 buildTitleMessages 两条降级(标题只基于 prompt)。
|
|
202
|
+
// 重复触发由步 5 计数拦截(pi 事件链 emit await 先于 append,第二条 user 到达
|
|
203
|
+
// 时首条已入 entries,计数 ≥1);旧 firstPromptInFlight 在途标志已删——pi 单进程
|
|
204
|
+
// 事件模型下「首条未 append 且第二条 message_end 已到」不可达。
|
|
205
|
+
void landTitle(pi, ctx, config, { content: [] }, { debugLog, promptText });
|
|
215
206
|
// rename 是 best-effort,任何 LLM 失败(网络/提取/auth/model 不可用)都静默跳过保留原 label,
|
|
216
207
|
// 不进 session history。
|
|
217
208
|
} catch (e) {
|
package/src/llm.ts
CHANGED
|
@@ -339,8 +339,10 @@ export async function callRenameLLM(
|
|
|
339
339
|
logger.warn("model not available, skipping");
|
|
340
340
|
return null;
|
|
341
341
|
}
|
|
342
|
+
// config.thinkingLevel 类型为 string 且 normalizeRenameConfig 恒回填默认值("off"),
|
|
343
|
+
// 生产路径不存在 undefined 形态,无需 !== undefined 谓词。
|
|
342
344
|
const reasoningLevel = getSupportedThinkingLevels(model).find((l) => l === config.thinkingLevel);
|
|
343
|
-
if (
|
|
345
|
+
if (reasoningLevel === undefined) {
|
|
344
346
|
logger.warn(
|
|
345
347
|
`configured thinkingLevel "${config.thinkingLevel}" is not supported by ${model.provider}/${model.id}; calling without a thinking level`,
|
|
346
348
|
);
|