@tduyng/pi-ask 0.0.1 → 0.0.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/README.md +23 -13
- package/package.json +2 -2
- package/src/index.ts +27 -8
package/README.md
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
1
|
# @tduyng/pi-ask
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One tool that stops and waits. The model asks up to four questions, you answer with a keystroke, and the turn carries on instead of guessing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What it costs
|
|
6
|
+
|
|
7
|
+
`ask_user` is 202 tokens a request, and it is the only thing this package adds. No system prompt block, no statusline, no background work.
|
|
8
|
+
|
|
9
|
+
The saving is the turns it removes. A model facing a fork it cannot resolve writes a paragraph of options, waits for you to answer, then re-reads that paragraph every turn after. One question in a dialog costs one exchange and leaves one short `Q:` and `A:` block in the transcript.
|
|
6
10
|
|
|
7
11
|
## The tool
|
|
8
12
|
|
|
9
13
|
`ask_user` takes `questions`, an array of at most four. Each question carries:
|
|
10
14
|
|
|
11
|
-
| Field
|
|
12
|
-
|
|
|
13
|
-
| `question` | The question to ask
|
|
14
|
-
| `guess`
|
|
15
|
-
| `options`
|
|
15
|
+
| Field | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `question` | The question to ask |
|
|
18
|
+
| `guess` | Your best guess, one line of reasoning, shown in the dialog |
|
|
19
|
+
| `options` | Predefined choices, single select, plus a free-text escape |
|
|
16
20
|
|
|
17
|
-
```
|
|
21
|
+
```text
|
|
18
22
|
ask_user(questions: [
|
|
19
23
|
{ question: "Which Windows surface first?", guess: "WSL gwt plus Windows Terminal",
|
|
20
24
|
options: ["WSL first", "native gwt.exe first", "both"] }
|
|
21
25
|
])
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
The
|
|
28
|
+
The guess sits under the question, so a wrong guess is corrected in one keystroke. The call blocks until every question is answered or cancelled.
|
|
25
29
|
|
|
26
|
-
##
|
|
30
|
+
## What lands in the transcript
|
|
27
31
|
|
|
28
32
|
One `Q:` and `A:` block per question:
|
|
29
33
|
|
|
30
|
-
```
|
|
34
|
+
```text
|
|
31
35
|
Q: Which Windows surface first?
|
|
32
36
|
A: WSL first
|
|
33
37
|
```
|
|
@@ -38,16 +42,22 @@ A cancelled dialog records `(skipped)`, so the transcript shows a question that
|
|
|
38
42
|
|
|
39
43
|
Headless, RPC, print, and subagent runs have nobody to answer. The tool says so once and tells the model to proceed on each question's stated guess and record it as an assumption.
|
|
40
44
|
|
|
45
|
+
That is why `guess` is worth writing even when you are at the keyboard: it is the fallback the model acts on when the dialog cannot open.
|
|
46
|
+
|
|
47
|
+
## Why not just ask in prose
|
|
48
|
+
|
|
49
|
+
pi core ships no question tool. Claude Code has `AskUserQuestion`, opencode ships its own, pi has none, so a model facing a fork falls back to a paragraph: a turn spent, a block that renders badly, and a question that reads as a guess rather than a choice.
|
|
50
|
+
|
|
41
51
|
## Install
|
|
42
52
|
|
|
43
53
|
```bash
|
|
44
54
|
pi install npm:@tduyng/pi-ask
|
|
45
55
|
```
|
|
46
56
|
|
|
47
|
-
Local checkout: `pi install /path/to/extensions/pi-ask`
|
|
57
|
+
Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-ask`
|
|
48
58
|
|
|
49
59
|
## Tests
|
|
50
60
|
|
|
51
61
|
```bash
|
|
52
|
-
|
|
62
|
+
bun test extensions/pi-ask
|
|
53
63
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tduyng/pi-ask",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Ask the user up to 4 questions and wait for the answer. One tool, 202 tokens a request.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
package/src/index.ts
CHANGED
|
@@ -29,24 +29,43 @@ function title(question: AskParams['questions'][number]): string {
|
|
|
29
29
|
export async function askUser(params: AskParams, ctx: Dialog): Promise<string> {
|
|
30
30
|
if (!ctx.hasUI) return NO_UI_ANSWER
|
|
31
31
|
const lines: string[] = []
|
|
32
|
-
|
|
32
|
+
const questions = params.questions.slice(0, MAX_QUESTIONS)
|
|
33
|
+
for (let index = 0; index < questions.length; index++) {
|
|
34
|
+
const question = questions[index]!
|
|
33
35
|
let answer: string
|
|
34
36
|
if (question.options && question.options.length > 0) {
|
|
35
37
|
const pick = await ctx.ui.select(title(question), [...question.options, OTHER])
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (pick === undefined) {
|
|
39
|
+
lines.push(...cancelled(questions.slice(index)))
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
if (pick === OTHER) {
|
|
43
|
+
const typed = await ctx.ui.input(title(question), 'your answer')
|
|
44
|
+
if (typed === undefined) {
|
|
45
|
+
lines.push(...cancelled(questions.slice(index)))
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
answer = typed.trim() || '(skipped)'
|
|
49
|
+
} else {
|
|
50
|
+
answer = pick
|
|
51
|
+
}
|
|
42
52
|
} else {
|
|
43
|
-
|
|
53
|
+
const typed = await ctx.ui.input(title(question))
|
|
54
|
+
if (typed === undefined) {
|
|
55
|
+
lines.push(...cancelled(questions.slice(index)))
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
answer = typed.trim() || '(skipped)'
|
|
44
59
|
}
|
|
45
60
|
lines.push(`Q: ${question.question}\nA: ${answer}`)
|
|
46
61
|
}
|
|
47
62
|
return lines.join('\n\n') || 'No well-formed questions were provided.'
|
|
48
63
|
}
|
|
49
64
|
|
|
65
|
+
function cancelled(questions: AskParams['questions']): string[] {
|
|
66
|
+
return questions.map((question) => `Q: ${question.question}\nA: (cancelled)`)
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
export default function askUserExtension(pi: Pick<ExtensionAPI, 'registerTool'>): void {
|
|
51
70
|
pi.registerTool({
|
|
52
71
|
name: 'ask_user',
|