@howaboua/pi-ask 0.0.10 → 0.0.12
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/CHANGELOG.md +9 -0
- package/ask/contracts.ts +2 -1
- package/ask/coordinator.ts +35 -7
- package/ask/normalize.ts +7 -0
- package/ask/pending.ts +8 -2
- package/ask/tool.ts +23 -7
- package/index.ts +18 -4
- package/package.json +7 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @howaboua/pi-ask
|
|
2
2
|
|
|
3
|
+
## 0.0.12
|
|
4
|
+
|
|
5
|
+
- ### Improvements
|
|
6
|
+
- Ask returns help when called without arguments, including an empty structured object. Explicit help remains supported.
|
|
7
|
+
|
|
8
|
+
## 0.0.11
|
|
9
|
+
|
|
10
|
+
- Parents can receive and answer asynchronous questions through Shepherdr while workers continue.
|
|
11
|
+
|
|
3
12
|
## 0.0.10
|
|
4
13
|
|
|
5
14
|
- Requires Pi 1.0.0 or later.
|
package/ask/contracts.ts
CHANGED
|
@@ -16,8 +16,9 @@ const PromptSchema = Type.Object({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export const AskParameters = Type.Object({
|
|
19
|
+
help: Type.Optional(Type.Literal(true)),
|
|
19
20
|
handoff: Type.Optional(Type.Boolean()),
|
|
20
|
-
prompts: Type.Array(PromptSchema),
|
|
21
|
+
prompts: Type.Optional(Type.Array(PromptSchema)),
|
|
21
22
|
delivery: Type.Optional(StringEnum(ASK_DELIVERIES)),
|
|
22
23
|
});
|
|
23
24
|
|
package/ask/coordinator.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AskPrompt, PendingAsk } from "./contracts.js";
|
|
2
|
+
import type { AskPrompt, AskResponse, PendingAsk } from "./contracts.js";
|
|
3
3
|
import { normalizeResponses, summarizeResponses } from "./normalize.js";
|
|
4
4
|
import type { PendingAskUpdate } from "./pending.js";
|
|
5
5
|
import { askWithPiUi } from "./pi-ui.js";
|
|
@@ -14,6 +14,7 @@ export interface AskCoordinatorOptions {
|
|
|
14
14
|
askInComposer?: AskInComposer;
|
|
15
15
|
deliverSteer?: (message: string) => void;
|
|
16
16
|
onPendingChange?: (update: PendingAskUpdate) => void;
|
|
17
|
+
onSteerActiveChange?: (request: PendingAsk, active: boolean) => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
interface PresentAskOptions {
|
|
@@ -34,11 +35,13 @@ export function createAskCoordinator({
|
|
|
34
35
|
askInComposer,
|
|
35
36
|
deliverSteer,
|
|
36
37
|
onPendingChange,
|
|
38
|
+
onSteerActiveChange,
|
|
37
39
|
}: AskCoordinatorOptions = {}) {
|
|
38
40
|
let generation = 0;
|
|
39
41
|
let sessionAbort = new AbortController();
|
|
40
42
|
let presentationTail = Promise.resolve();
|
|
41
43
|
const activeSteers = new Set<string>();
|
|
44
|
+
let presentedSteer: PendingAsk | undefined;
|
|
42
45
|
|
|
43
46
|
const present = (
|
|
44
47
|
ctx: ExtensionContext,
|
|
@@ -95,10 +98,19 @@ export function createAskCoordinator({
|
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
100
|
|
|
98
|
-
const close = (
|
|
101
|
+
const close = (
|
|
102
|
+
ctx: ExtensionContext,
|
|
103
|
+
id: string,
|
|
104
|
+
responses?: AskResponse[],
|
|
105
|
+
) => {
|
|
99
106
|
activeSteers.delete(id);
|
|
100
107
|
try {
|
|
101
|
-
onPendingChange?.({
|
|
108
|
+
onPendingChange?.({
|
|
109
|
+
version: 1,
|
|
110
|
+
state: "closed",
|
|
111
|
+
id,
|
|
112
|
+
...(responses ? { responses } : { dismissed: true }),
|
|
113
|
+
});
|
|
102
114
|
} catch (error) {
|
|
103
115
|
notifyFailure(ctx, error);
|
|
104
116
|
}
|
|
@@ -123,12 +135,22 @@ export function createAskCoordinator({
|
|
|
123
135
|
void present(ctx, request.prompts, {
|
|
124
136
|
steering: true,
|
|
125
137
|
signal: sessionAbort.signal,
|
|
138
|
+
onActiveChange: (active) => {
|
|
139
|
+
if (requestGeneration === generation && !sessionAbort.signal.aborted) {
|
|
140
|
+
presentedSteer = active ? request : undefined;
|
|
141
|
+
onSteerActiveChange?.(request, active);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
126
144
|
}).then(
|
|
127
145
|
(responses) => {
|
|
128
146
|
if (requestGeneration !== generation || sessionAbort.signal.aborted)
|
|
129
147
|
return;
|
|
130
148
|
if (deliver(ctx, steerResponse(request.prompts, responses)))
|
|
131
|
-
close(
|
|
149
|
+
close(
|
|
150
|
+
ctx,
|
|
151
|
+
request.id,
|
|
152
|
+
normalizeResponses(request.prompts, responses) ?? undefined,
|
|
153
|
+
);
|
|
132
154
|
},
|
|
133
155
|
(error: unknown) => {
|
|
134
156
|
if (requestGeneration !== generation || sessionAbort.signal.aborted)
|
|
@@ -153,6 +175,7 @@ export function createAskCoordinator({
|
|
|
153
175
|
},
|
|
154
176
|
restorePending(pending: readonly PendingAsk[], ctx: ExtensionContext) {
|
|
155
177
|
generation++;
|
|
178
|
+
presentedSteer = undefined;
|
|
156
179
|
sessionAbort.abort();
|
|
157
180
|
sessionAbort = new AbortController();
|
|
158
181
|
presentationTail = Promise.resolve();
|
|
@@ -161,9 +184,14 @@ export function createAskCoordinator({
|
|
|
161
184
|
for (const request of pending) startSteer(request, ctx, false);
|
|
162
185
|
},
|
|
163
186
|
shutdown() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
try {
|
|
188
|
+
if (presentedSteer) onSteerActiveChange?.(presentedSteer, false);
|
|
189
|
+
} finally {
|
|
190
|
+
presentedSteer = undefined;
|
|
191
|
+
generation++;
|
|
192
|
+
sessionAbort.abort();
|
|
193
|
+
activeSteers.clear();
|
|
194
|
+
}
|
|
167
195
|
},
|
|
168
196
|
get sessionSignal(): AbortSignal {
|
|
169
197
|
return sessionAbort.signal;
|
package/ask/normalize.ts
CHANGED
|
@@ -52,6 +52,13 @@ export function isSteeringAskInput(input: unknown): boolean {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export function isAskHelpInput(input: unknown): boolean {
|
|
56
|
+
if (input === undefined) return true;
|
|
57
|
+
if (!isRecord(input) || Array.isArray(input)) return false;
|
|
58
|
+
const keys = Object.keys(input);
|
|
59
|
+
return keys.length === 0 || (keys.length === 1 && input["help"] === true);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export function normalizeAskInput(params: unknown): {
|
|
56
63
|
delivery: "wait" | "steer";
|
|
57
64
|
handoff: boolean;
|
package/ask/pending.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AskPrompt, PendingAsk } from "./contracts.js";
|
|
2
|
+
import type { AskPrompt, AskResponse, PendingAsk } from "./contracts.js";
|
|
3
3
|
import { normalizeAskInput } from "./normalize.js";
|
|
4
4
|
|
|
5
5
|
export const PENDING_ASK_ENTRY_TYPE = "pi-ask-pending";
|
|
6
6
|
|
|
7
7
|
export type PendingAskUpdate =
|
|
8
8
|
| { version: 1; state: "pending"; id: string; prompts: AskPrompt[] }
|
|
9
|
-
| {
|
|
9
|
+
| {
|
|
10
|
+
version: 1;
|
|
11
|
+
state: "closed";
|
|
12
|
+
id: string;
|
|
13
|
+
responses?: AskResponse[];
|
|
14
|
+
dismissed?: boolean;
|
|
15
|
+
};
|
|
10
16
|
|
|
11
17
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
18
|
return typeof value === "object" && value !== null;
|
package/ask/tool.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createAskCoordinator,
|
|
7
7
|
} from "./coordinator.js";
|
|
8
8
|
import {
|
|
9
|
+
isAskHelpInput,
|
|
9
10
|
isSteeringAskInput,
|
|
10
11
|
normalizeAskInput,
|
|
11
12
|
normalizeResponses,
|
|
@@ -33,25 +34,38 @@ export function createAskRuntime({
|
|
|
33
34
|
deliverSteer,
|
|
34
35
|
onBlockedChange,
|
|
35
36
|
onPendingChange,
|
|
37
|
+
onSteerActiveChange,
|
|
36
38
|
}: AskToolOptions = {}) {
|
|
37
39
|
const coordinator = createAskCoordinator({
|
|
38
40
|
...(askInComposer ? { askInComposer } : {}),
|
|
39
41
|
...(deliverSteer ? { deliverSteer } : {}),
|
|
40
42
|
...(onPendingChange ? { onPendingChange } : {}),
|
|
43
|
+
...(onSteerActiveChange ? { onSteerActiveChange } : {}),
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
const tool = defineTool({
|
|
44
47
|
name: "ask",
|
|
45
48
|
label: "Ask",
|
|
46
|
-
description: "Request user input or action",
|
|
49
|
+
description: "Request user input or action; {} for help",
|
|
47
50
|
parameters: AskParameters,
|
|
48
|
-
promptGuidelines: [
|
|
49
|
-
"ask: Omit delivery to wait; steer only while reversible work can continue. Handoffs wait for user action; state the completion signal",
|
|
50
|
-
"ask: For reviews, make each finding a prompt with disposition choices; do not report first.",
|
|
51
|
-
"ask: Other/rephrase is automatic; omit choices for free text",
|
|
52
|
-
],
|
|
53
51
|
executionMode: "sequential",
|
|
54
52
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
53
|
+
if (isAskHelpInput(params)) {
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
textContent(
|
|
57
|
+
[
|
|
58
|
+
"ask({}) or ask({ help: true }) returns help",
|
|
59
|
+
'ask({ prompts: [{ title, body?, multiple?, choices?: [{ label, description? }] }], delivery?: "wait"|"steer", handoff? })',
|
|
60
|
+
"Omit delivery to wait; steer only while reversible work can continue. Handoffs wait for user action; state the completion signal",
|
|
61
|
+
"For reviews, make each finding a prompt with disposition choices; do not report first",
|
|
62
|
+
"Other/rephrase is automatic; omit choices for free text. Blank Other/rephrase means rephrase or follow up",
|
|
63
|
+
].join("\n"),
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
details: { kind: "help" },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
55
69
|
const { delivery, handoff, prompts } = normalizeAskInput(params);
|
|
56
70
|
if (prompts.length === 0) {
|
|
57
71
|
throw new Error(
|
|
@@ -70,7 +84,7 @@ export function createAskRuntime({
|
|
|
70
84
|
return {
|
|
71
85
|
content: [
|
|
72
86
|
textContent(
|
|
73
|
-
"Question
|
|
87
|
+
"Question queued. Continue working; the response will arrive as steering.",
|
|
74
88
|
),
|
|
75
89
|
],
|
|
76
90
|
details: { kind: "prompt", pending: true, id: toolCallId },
|
|
@@ -113,6 +127,8 @@ export function createAskRuntime({
|
|
|
113
127
|
};
|
|
114
128
|
},
|
|
115
129
|
renderCall(args, theme, context) {
|
|
130
|
+
if (isAskHelpInput(args))
|
|
131
|
+
return new Text(theme.fg("toolTitle", theme.bold("ask help")), 0, 0);
|
|
116
132
|
const count = Array.isArray(args.prompts) ? args.prompts.length : 0;
|
|
117
133
|
const label =
|
|
118
134
|
args.handoff === true
|
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createSteerDelivery,
|
|
10
10
|
type TryCodexDeveloperMessage,
|
|
11
11
|
} from "./ask/delivery.js";
|
|
12
|
-
import { isSteeringAskInput } from "./ask/normalize.js";
|
|
12
|
+
import { isAskHelpInput, isSteeringAskInput } from "./ask/normalize.js";
|
|
13
13
|
import { PENDING_ASK_ENTRY_TYPE, readPendingAsks } from "./ask/pending.js";
|
|
14
14
|
import { createAskRuntime, createAskTool } from "./ask/tool.js";
|
|
15
15
|
import registerPackageChangelog from "./changelog.js";
|
|
@@ -62,6 +62,20 @@ export default async function humanInTheLoop(pi: ExtensionAPI): Promise<void> {
|
|
|
62
62
|
onPendingChange: (update) => {
|
|
63
63
|
pi.appendEntry(PENDING_ASK_ENTRY_TYPE, update);
|
|
64
64
|
},
|
|
65
|
+
onSteerActiveChange: (request, active) => {
|
|
66
|
+
pi.appendEntry(
|
|
67
|
+
"pi-ask-active",
|
|
68
|
+
active
|
|
69
|
+
? {
|
|
70
|
+
version: 1,
|
|
71
|
+
state: "active",
|
|
72
|
+
id: request.id,
|
|
73
|
+
delivery: "steer",
|
|
74
|
+
prompts: request.prompts,
|
|
75
|
+
}
|
|
76
|
+
: { version: 1, state: "closed", id: request.id },
|
|
77
|
+
);
|
|
78
|
+
},
|
|
65
79
|
});
|
|
66
80
|
const ask = askRuntime.tool;
|
|
67
81
|
const restorePending = (ctx: ExtensionContext) => {
|
|
@@ -113,9 +127,9 @@ async function registerAskInCodeMode(
|
|
|
113
127
|
await import("@howaboua/pi-codex-conversion/code-mode");
|
|
114
128
|
const registration = registerCodeModeExtensionTools(pi, () => [
|
|
115
129
|
adaptToolForCodeMode(ask, {
|
|
116
|
-
blocking: (input) =>
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
blocking: (input) =>
|
|
131
|
+
!isAskHelpInput(input) && !isSteeringAskInput(input),
|
|
132
|
+
usage: "await tools.ask() // help",
|
|
119
133
|
}),
|
|
120
134
|
]);
|
|
121
135
|
pi.on("session_shutdown", () => registration.unregister());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-ask",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Interactive decisions, review triage, and human handoffs for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@howaboua/pi-codex-conversion": ">=3.0.
|
|
48
|
+
"@howaboua/pi-codex-conversion": ">=3.0.48",
|
|
49
49
|
"@earendil-works/pi-ai": ">=1.0.0",
|
|
50
50
|
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
51
51
|
"@earendil-works/pi-tui": ">=1.0.0",
|
|
@@ -57,18 +57,17 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@howaboua/pi-codex-conversion": "3.0.
|
|
60
|
+
"@howaboua/pi-codex-conversion": "3.0.48",
|
|
61
61
|
"@biomejs/biome": "^2.5.10",
|
|
62
|
-
"@earendil-works/pi-ai": "1.
|
|
63
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
64
|
-
"@earendil-works/pi-tui": "1.
|
|
62
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
64
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
65
65
|
"typebox": "1.3.18",
|
|
66
66
|
"typescript": "^7.0.2"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
|
-
"check": "bun run lint && bun run typecheck
|
|
69
|
+
"check": "bun run lint && bun run typecheck",
|
|
70
70
|
"prepack": "bun run check",
|
|
71
|
-
"test": "bun test",
|
|
72
71
|
"typecheck": "tsgo --noEmit",
|
|
73
72
|
"lint": "biome check .",
|
|
74
73
|
"format": "biome check --write .",
|