@zhushanwen/pi-ask-user 7.3.7 → 7.3.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-ask-user",
3
- "version": "7.3.7",
3
+ "version": "7.3.8",
4
4
  "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview and an inline free-text editor.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -42,7 +42,9 @@ interface RegisteredTool {
42
42
  interface MockPi {
43
43
  tool?: RegisteredTool;
44
44
  registerTool(tool: RegisteredTool): void;
45
- getAllTools(): { name: string }[];
45
+ getAllTools(): { name: string; exposure?: string }[];
46
+ /** 当前激活集(快照式禁用的基底,dmg-r1-2);缺省 = 全部 direct 工具激活 */
47
+ getActiveTools(): string[];
46
48
  activeTools?: string[] | null;
47
49
  setActiveTools(names: string[]): void;
48
50
  // session_start handler:factory 注册 ask_user channel handler 时调用(透传功能)。
@@ -57,7 +59,13 @@ const getTool = (overrides: Partial<MockPi> = {}): RegisteredTool => {
57
59
  this.tool = tool;
58
60
  },
59
61
  getAllTools() {
60
- return [{ name: "ask_user" }, { name: "other_tool" }];
62
+ return [
63
+ { name: "ask_user", exposure: "direct" },
64
+ { name: "other_tool", exposure: "direct" },
65
+ ];
66
+ },
67
+ getActiveTools() {
68
+ return ["ask_user", "other_tool"];
61
69
  },
62
70
  setActiveTools(names) {
63
71
  this.activeTools = names;
@@ -226,7 +234,17 @@ describe("execute — headless (FR-8 / AC-7)", () => {
226
234
  it("I-6: headless disables ask_user tool via setActiveTools (收尾先于 throw)", async () => {
227
235
  let captured: string[] | null = null;
228
236
  const tool2 = getTool({
229
- getAllTools: () => [{ name: "ask_user" }, { name: "other" }],
237
+ // 快照式禁用基底(dmg-r1-2):getActiveTools = 当前激活集;getAllTools 注册全集
238
+ // 含未激活的 plan(deferred) 与 tool_search(model-only + defaultActive:false,pi
239
+ // tool-search/index.js 实装)——禁用动作不得把它们点名激活(setActiveTools
240
+ // 点名即声明给模型)
241
+ getActiveTools: () => ["ask_user", "other"],
242
+ getAllTools: () => [
243
+ { name: "ask_user", exposure: "direct" },
244
+ { name: "other", exposure: "direct" },
245
+ { name: "plan", exposure: "deferred" },
246
+ { name: "tool_search", exposure: "model-only" },
247
+ ],
230
248
  setActiveTools: (names: string[]) => {
231
249
  captured = names;
232
250
  },
@@ -236,6 +254,10 @@ describe("execute — headless (FR-8 / AC-7)", () => {
236
254
  ).rejects.toThrow();
237
255
  expect(captured).not.toContain("ask_user");
238
256
  expect(captured).toContain("other");
257
+ // D4 回归锚:未激活工具不随禁用动作常驻——含 model-only 档但 defaultActive:false
258
+ // 的 tool_search(按档位过滤重建会点名激活它,快照式基底天然不含)
259
+ expect(captured).not.toContain("plan");
260
+ expect(captured).not.toContain("tool_search");
239
261
  });
240
262
  });
241
263
 
@@ -818,7 +840,11 @@ describe("execute — RPC mode (uiFormInteract via select channel)", () => {
818
840
  it("R-10: select 回显 payload(echo,旧 taiji 宿主)→ channel-error throw 带升级指引 + 工具禁用", async () => {
819
841
  let capturedActive: string[] | null = null;
820
842
  const tool = getTool({
821
- getAllTools: () => [{ name: "ask_user" }, { name: "other" }],
843
+ getActiveTools: () => ["ask_user", "other"],
844
+ getAllTools: () => [
845
+ { name: "ask_user", exposure: "direct" },
846
+ { name: "other", exposure: "direct" },
847
+ ],
822
848
  setActiveTools: (names: string[]) => {
823
849
  capturedActive = names;
824
850
  },
@@ -12,6 +12,20 @@
12
12
  //
13
13
  // 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把正例/反例/调用信号
14
14
  // 删掉或弱化。读源码而非 import,避免 mock 链(index.ts 依赖 pi-tui/ExtensionAPI 等)。
15
+ //
16
+ // 【锚词保留清单 — ask_user】(tool-declaration-slimming D2:压缩后的契约 = 逐锚词
17
+ // grep,替代原逐字断言。U-A1 终态:promptGuidelines 层已删——原 6 条与 description/
18
+ // InputSchema 字段说明逐条重复;调用信号 + 唯一正例 + 结构反例单点在 description):
19
+ // Q1 '"options":[{"label"' —— 单问题正例的对象数组形态(弱模型最高频误用面)
20
+ // Q2 '"header"' ≥2 —— 批量正例展示条件必填字段
21
+ // Q3 "(Recommended)" —— 推荐项前缀调用信号
22
+ // Q4 结构反例 ≥2:string array / Flatten / Other(描述里 Don't 段承载)
23
+ // Q5 "Use ONLY when" + "Do NOT use" —— 调用门槛与边界段
24
+ // Q6 validate.ts 三个 Correct 正例锚(runtime 友好纠错,未改动)
25
+ // Q7 示例必须与契约一致:options 元素必须 {label, description} 对象形态
26
+ // Q8 "mutually exclusive" —— 一问题一决策、选项互斥的提问纪律(单句并入推荐项
27
+ // 段落;U-A1 压缩曾删原纪律句,选项级互斥语义由 InputSchema options 字段说明
28
+ // 承载,问题级「one question = one decision」唯一承载点 = description 此句)
15
29
 
16
30
  import { readFileSync } from "node:fs";
17
31
  import { dirname,join } from "node:path";
@@ -75,6 +89,13 @@ describe("ask_user description — 调用信号保留(不要破坏亮点)",
75
89
  it("保留 'Do NOT use' 边界段", () => {
76
90
  expect(DESCRIPTION).toContain("Do NOT use");
77
91
  });
92
+
93
+ it("保留 'mutually exclusive' 提问纪律(Q8:一问题一决策、选项互斥)", () => {
94
+ // 问题级纪律没有 runtime 校验可拦(validate 只查结构),弱模型把多个决策
95
+ // 塞进一个 question、或选项互斥性存疑时只有这条声明面约束。
96
+ expect(DESCRIPTION).toContain("mutually exclusive");
97
+ expect(DESCRIPTION).toContain("one decision");
98
+ });
78
99
  });
79
100
 
80
101
  describe("validate.ts — runtime 友好纠错(A. 带正例)", () => {
package/src/index.ts CHANGED
@@ -36,14 +36,18 @@ import { validateInput } from "./validate";
36
36
  */
37
37
  type ExecuteResult = AgentToolResult<AskUserDetails>;
38
38
 
39
- /** 禁用 ask_user 工具(headless / RPC 通道不可用时调用,防 LLM 反复重试)。 */
39
+ /**
40
+ * 禁用 ask_user 工具(headless / RPC 通道不可用时调用,防 LLM 反复重试)。
41
+ *
42
+ * 快照式(D4 tool-declaration-slimming 修订):基底 = 当前激活集(getActiveTools),
43
+ * 排除 ask_user 的动作叠加其上——不点任何原本未激活的工具。pi 语义
44
+ * (setActiveTools 点名即声明给模型,agent-session _applyToolLoadout 只滤 hidden):
45
+ * 若按 getAllTools() 全量点名,defer 名单与未 opt-in 的 tool_search(实装
46
+ * model-only 档 + defaultActive:false 注册)会被点名激活,击穿「未开启 tool_search
47
+ * 时这些工具对模型不可见」的前提。
48
+ */
40
49
  function disableAskUser(pi: ExtensionAPI): void {
41
- pi.setActiveTools(
42
- pi
43
- .getAllTools()
44
- .map((t: { name: string }) => t.name)
45
- .filter((n: string) => n !== "ask_user"),
46
- );
50
+ pi.setActiveTools(pi.getActiveTools().filter((n: string) => n !== "ask_user"));
47
51
  }
48
52
 
49
53
  /** 构造 cancelled 结果(用户取消 / abort 共用;错误路径已改为 throw,不走此函数)。 */
@@ -229,32 +233,25 @@ export default function (pi: ExtensionAPI): void {
229
233
  pi.registerTool({
230
234
  name: "ask_user",
231
235
  label: "Ask User",
232
- description: `Ask the user to resolve ambiguity you cannot resolve yourself. Use ONLY when ALL hold: (1) the request has ≥2 reasonable approaches, (2) you have already gathered context (read/grep) and the answer is still genuinely ambiguous, and (3) picking wrong means redoing real work. One question = one decision with mutually exclusive options.
233
-
234
- Do NOT use this tool to outsource judgment you should make — if you can form a defensible recommendation from the codebase, proceed and state your choice. Do NOT use for trivia answerable by reading code/docs, or for simple confirmations ("I'll delete X") where plain text suffices. You cannot use this tool to collect free-form requirements, long-form feedback, or multi-paragraph input — it returns short selections only.
236
+ // tool-declaration-slimming D1 终态:调用信号 + 一个正例 + 结构反例在 description
237
+ // 一处;参数功能语义在 InputSchema 字段说明(types.ts);promptGuidelines 层删除
238
+ // (原 6 条与 description/schema 逐条重复)。锚词保留清单见 __tests__/prompt-quality.test.ts 头注。
239
+ description: `Ask the user to resolve ambiguity you cannot resolve. Use ONLY when ALL hold: (1) ≥2 reasonable approaches exist, (2) you already gathered context (read/grep) and it is still genuinely ambiguous, (3) picking wrong means redoing real work.
235
240
 
236
- If you recommend an option, prefix its label with "(Recommended)" and list it first. For structured multi-option decisions, prefer this tool over plain-text questions; for everything else, reply in plain text.
241
+ Do NOT use this tool to outsource judgment you should make — if you can form a defensible recommendation from the codebase, proceed and state it. Not for trivia answerable by reading code/docs, simple confirmations (plain text suffices), or free-form input — it returns short selections only.
237
242
 
238
- Examples:
239
- {"questions":[{"question":"Which DB?","context":"Need ACID + JSON columns.","options":[{"label":"(Recommended) Postgres","description":"Mature, strong consistency."},{"label":"SQLite","description":"Zero-ops, embedded."}]}]}
243
+ If you recommend an option, prefix its label with "(Recommended)" and list it first. One question = one decision with mutually exclusive options.
240
244
 
241
- {"questions":[{"header":"DB","question":"Which database?","options":[{"label":"Postgres","description":"..."},{"label":"SQLite","description":"..."}]},{"header":"Region","question":"Which region?","options":[{"label":"us-east-1","description":"..."},{"label":"eu-west-1","description":"..."}]}]}
245
+ Example:
246
+ {"questions":[{"header":"DB","question":"Which database?","context":"Need ACID + JSON columns.","options":[{"label":"(Recommended) Postgres","description":"Mature, strong consistency."},{"label":"SQLite","description":"Zero-ops, embedded."}]},{"header":"Region","question":"Which region?","options":[{"label":"us-east-1","description":"..."},{"label":"eu-west-1","description":"..."}]}]}
242
247
 
243
248
  Don't:
244
249
  - Passing options as a string array ("options":["A","B"]) — each option must be {"label","description"}.
245
- - Forgetting header in multi-question mode (questions.length > 1).
250
+ - Forgetting header when questions.length > 1.
246
251
  - Flattening question/header/options to the top level — wrap them in questions:[...].
247
252
  - Including an "Other" option — it is added automatically.`,
248
253
  promptSnippet:
249
254
  "Ask the user structured clarifying questions with options — only when you cannot resolve the ambiguity yourself",
250
- promptGuidelines: [
251
- "Use ask_user only when the request has ≥2 reasonable approaches you cannot resolve from context. Models over-ask because asking feels safer than deciding — resist this: if context makes the answer clear, proceed without asking.",
252
- "Gather context first (read/grep) and pass a short summary via the context field — don't ask blind. If the answer becomes clear after gathering context, proceed and state your choice.",
253
- "Ask focused questions; each question = one decision with mutually exclusive options. Batch related decisions into one call (1-4 questions).",
254
- "Do NOT use ask_user for trivia answerable by reading code/docs, or to confirm simple actions ('I'll delete X') — plain text suffices there.",
255
- "Do NOT outsource judgment you can make yourself: if you can form a defensible recommendation from the codebase, proceed and state it instead of asking.",
256
- "Do NOT include an 'Other' option yourself — it is always available automatically.",
257
- ],
258
255
  parameters: InputSchema,
259
256
 
260
257
  async execute(