@pi9/ask 0.2.0 → 0.3.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 +11 -0
- package/package.json +1 -1
- package/src/component.ts +5 -5
- package/src/deadline.ts +29 -4
- package/src/domain.ts +144 -0
- package/src/index.ts +32 -60
- package/src/questionnaire.ts +2 -2
- package/src/rpc.ts +24 -38
- package/src/session.ts +254 -0
- package/src/state.ts +21 -25
- package/src/config.ts +0 -30
- package/src/context.ts +0 -209
- package/src/replay-renderer.ts +0 -85
- package/src/replay.ts +0 -100
- package/src/response.ts +0 -31
- package/src/schema.ts +0 -43
- package/src/types.ts +0 -33
- package/src/validation.ts +0 -48
package/src/replay.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { SessionEntry, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Check } from "typebox/value";
|
|
3
|
-
|
|
4
|
-
import { AskAnswerSchema, AskParamsSchema, AskReplayDetailsSchema } from "./schema.js";
|
|
5
|
-
import { formatAskAnswer } from "./response.js";
|
|
6
|
-
import type { AskAnswer, AskParams, AskReplayDetails, ValidatedAskParams } from "./types.js";
|
|
7
|
-
import { validateAskParams } from "./validation.js";
|
|
8
|
-
|
|
9
|
-
export const ASK_REPLAY_CUSTOM_TYPE = "ask:reanswer" as const;
|
|
10
|
-
|
|
11
|
-
export type { AskReplayDetails } from "./types.js";
|
|
12
|
-
|
|
13
|
-
export type AskReplayMessage = {
|
|
14
|
-
customType: typeof ASK_REPLAY_CUSTOM_TYPE;
|
|
15
|
-
content: string;
|
|
16
|
-
display: false;
|
|
17
|
-
details: AskReplayDetails;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/** Parse the canonical answer shape used by native results and replay details. */
|
|
21
|
-
export function parseAskAnswer(value: unknown): AskAnswer | undefined {
|
|
22
|
-
return Check(AskAnswerSchema, value) ? value : undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Parse canonical replay details, without validating the containing message. */
|
|
26
|
-
export function parseAskReplayDetails(value: unknown): AskReplayDetails | undefined {
|
|
27
|
-
return Check(AskReplayDetailsSchema, value) ? value : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type AskReplayResolution =
|
|
31
|
-
| { status: "resolved"; toolCallId: string; params: ValidatedAskParams }
|
|
32
|
-
| {
|
|
33
|
-
status: "not-replayable";
|
|
34
|
-
reason: "no-entry" | "not-assistant" | "not-ask" | "multiple-tool-calls" | "mixed-tools" | "invalid-arguments";
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export function buildAskReplayMessage(toolCallId: string, params: ValidatedAskParams, answer: AskAnswer): AskReplayMessage {
|
|
38
|
-
return {
|
|
39
|
-
customType: ASK_REPLAY_CUSTOM_TYPE,
|
|
40
|
-
content: formatAskAnswer(answer).replaceAll("\n", " · "),
|
|
41
|
-
display: false,
|
|
42
|
-
details: {
|
|
43
|
-
toolCallId,
|
|
44
|
-
question: params.question,
|
|
45
|
-
...(params.context !== undefined ? { context: params.context } : {}),
|
|
46
|
-
allowMultiple: params.allowMultiple,
|
|
47
|
-
answer,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Resolve only the selected leaf, or its immediate parent when Pi created a branch summary. */
|
|
53
|
-
export function resolveAskReplayTarget(
|
|
54
|
-
event: Pick<SessionTreeEvent, "newLeafId" | "summaryEntry">,
|
|
55
|
-
getEntry: (id: string) => SessionEntry | undefined | null,
|
|
56
|
-
): AskReplayResolution {
|
|
57
|
-
if (!event.newLeafId) return rejected("no-entry");
|
|
58
|
-
|
|
59
|
-
let entry = getEntry(event.newLeafId);
|
|
60
|
-
const summary = event.summaryEntry?.id === event.newLeafId
|
|
61
|
-
? event.summaryEntry
|
|
62
|
-
: entry?.type === "branch_summary" ? entry : undefined;
|
|
63
|
-
if (summary) entry = summary.parentId ? getEntry(summary.parentId) : undefined;
|
|
64
|
-
if (!entry) return rejected("no-entry");
|
|
65
|
-
|
|
66
|
-
let selectedToolCallId: string | undefined;
|
|
67
|
-
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "ask") {
|
|
68
|
-
selectedToolCallId = entry.message.toolCallId;
|
|
69
|
-
entry = entry.parentId ? getEntry(entry.parentId) : undefined;
|
|
70
|
-
}
|
|
71
|
-
if (!entry) return rejected("no-entry");
|
|
72
|
-
if (entry.type !== "message" || entry.message.role !== "assistant") return rejected("not-assistant");
|
|
73
|
-
|
|
74
|
-
const calls = entry.message.content.filter((item) => item.type === "toolCall");
|
|
75
|
-
const askCalls = calls.filter((call) => call.name === "ask");
|
|
76
|
-
if (calls.length !== 1) {
|
|
77
|
-
if (askCalls.length > 0 && askCalls.length < calls.length) return rejected("mixed-tools");
|
|
78
|
-
if (askCalls.length > 1) return rejected("multiple-tool-calls");
|
|
79
|
-
return rejected("not-ask");
|
|
80
|
-
}
|
|
81
|
-
const call = calls[0];
|
|
82
|
-
if (call.name !== "ask" || (selectedToolCallId !== undefined && call.id !== selectedToolCallId)) return rejected("not-ask");
|
|
83
|
-
|
|
84
|
-
const params = validateStoredArgs(call.arguments);
|
|
85
|
-
if (!params) return rejected("invalid-arguments");
|
|
86
|
-
return { status: "resolved", toolCallId: call.id, params };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function validateStoredArgs(value: unknown): ValidatedAskParams | undefined {
|
|
90
|
-
if (!Check(AskParamsSchema, value)) return undefined;
|
|
91
|
-
try {
|
|
92
|
-
return validateAskParams(value as AskParams);
|
|
93
|
-
} catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function rejected(reason: Exclude<AskReplayResolution, { status: "resolved" }>["reason"]): AskReplayResolution {
|
|
99
|
-
return { status: "not-replayable", reason };
|
|
100
|
-
}
|
package/src/response.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { AskAnswer, AskResponse, AskToolDetails } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export function formatAskAnswer(answer: AskAnswer): string {
|
|
4
|
-
const lines = answer.selections.map((selection) => {
|
|
5
|
-
const description = selection.description ? ` — ${selection.description}` : "";
|
|
6
|
-
const comment = selection.comment ? ` (${selection.comment})` : "";
|
|
7
|
-
return `Selected: ${selection.label}${description}${comment}`;
|
|
8
|
-
});
|
|
9
|
-
if (answer.freeform) lines.push(`Freeform: ${answer.freeform}`);
|
|
10
|
-
return lines.join("\n") || "No answer provided.";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function buildAnsweredResponse(question: string, answer: AskAnswer): AskResponse {
|
|
14
|
-
return buildResponse(formatAskAnswer(answer), { status: "answered", question, answer });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildUnansweredResponse(question: string): AskResponse {
|
|
18
|
-
return buildResponse("The question timed out without an answer.", { status: "unanswered", question });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function buildCancelledResponse(question: string): AskResponse {
|
|
22
|
-
return buildResponse("User cancelled the question.", { status: "cancelled", question });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function buildUiUnavailableResponse(question: string): AskResponse {
|
|
26
|
-
return buildResponse("Interactive UI is unavailable.", { status: "ui_unavailable", question });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildResponse(text: string, details: AskToolDetails): AskResponse {
|
|
30
|
-
return { content: [{ type: "text", text }], details };
|
|
31
|
-
}
|
package/src/schema.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
|
|
3
|
-
import { MAX_TIMEOUT_MS } from "./config.js";
|
|
4
|
-
|
|
5
|
-
export const AskOptionSchema = Type.Object({
|
|
6
|
-
label: Type.String({ minLength: 1, description: "Short option title; keep distinct across options." }),
|
|
7
|
-
description: Type.Optional(Type.String({ description: "Brief explanation when the label alone is ambiguous." })),
|
|
8
|
-
preview: Type.Optional(Type.String({ description: "Markdown shown to the user, e.g. code, plans, or comparisons; not returned in the answer." })),
|
|
9
|
-
}, { additionalProperties: false });
|
|
10
|
-
|
|
11
|
-
export const AskParamsSchema = Type.Object({
|
|
12
|
-
question: Type.String({ minLength: 1 }),
|
|
13
|
-
context: Type.Optional(Type.String({ description: "Optional background shown above the question, e.g. what prompted the ask." })),
|
|
14
|
-
options: Type.Array(AskOptionSchema, { minItems: 1, description: "Suggested answers." }),
|
|
15
|
-
allowMultiple: Type.Optional(Type.Boolean({ default: false, description: "Allow selecting multiple options. Defaults to false." })),
|
|
16
|
-
allowFreeform: Type.Optional(Type.Boolean({ default: true, description: "Allow a typed response. Defaults to true." })),
|
|
17
|
-
timeout: Type.Optional(Type.Integer({ minimum: 0, maximum: MAX_TIMEOUT_MS, description: "Timeout in ms for answers that would go stale; the call returns unanswered on expiry. Zero disables any default." })),
|
|
18
|
-
}, { additionalProperties: false });
|
|
19
|
-
|
|
20
|
-
export const AskSelectionSchema = Type.Object({
|
|
21
|
-
label: Type.String({ minLength: 1 }),
|
|
22
|
-
description: Type.Optional(Type.String({ minLength: 1 })),
|
|
23
|
-
comment: Type.Optional(Type.String({ minLength: 1 })),
|
|
24
|
-
}, { additionalProperties: false });
|
|
25
|
-
|
|
26
|
-
export const AskAnswerSchema = Type.Object({
|
|
27
|
-
selections: Type.Array(AskSelectionSchema),
|
|
28
|
-
freeform: Type.Optional(Type.String({ minLength: 1 })),
|
|
29
|
-
}, { additionalProperties: false });
|
|
30
|
-
|
|
31
|
-
export const AskReplayDetailsSchema = Type.Object({
|
|
32
|
-
toolCallId: Type.String({ minLength: 1 }),
|
|
33
|
-
question: Type.String({ minLength: 1 }),
|
|
34
|
-
context: Type.Optional(Type.String({ minLength: 1 })),
|
|
35
|
-
allowMultiple: Type.Boolean(),
|
|
36
|
-
answer: AskAnswerSchema,
|
|
37
|
-
}, { additionalProperties: false });
|
|
38
|
-
|
|
39
|
-
export const AskAnsweredDetailsSchema = Type.Object({
|
|
40
|
-
status: Type.Literal("answered"),
|
|
41
|
-
question: Type.String({ minLength: 1 }),
|
|
42
|
-
answer: AskAnswerSchema,
|
|
43
|
-
}, { additionalProperties: false });
|
package/src/types.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Static } from "typebox";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
AskAnswerSchema,
|
|
5
|
-
AskAnsweredDetailsSchema,
|
|
6
|
-
AskOptionSchema,
|
|
7
|
-
AskParamsSchema,
|
|
8
|
-
AskReplayDetailsSchema,
|
|
9
|
-
AskSelectionSchema,
|
|
10
|
-
} from "./schema.js";
|
|
11
|
-
|
|
12
|
-
export type AskOption = Static<typeof AskOptionSchema>;
|
|
13
|
-
export type AskParams = Static<typeof AskParamsSchema>;
|
|
14
|
-
export type AskSelection = Static<typeof AskSelectionSchema>;
|
|
15
|
-
export type AskAnswer = Static<typeof AskAnswerSchema>;
|
|
16
|
-
export type AskReplayDetails = Static<typeof AskReplayDetailsSchema>;
|
|
17
|
-
export type AskAnsweredDetails = Static<typeof AskAnsweredDetailsSchema>;
|
|
18
|
-
|
|
19
|
-
export type ValidatedAskParams = Omit<AskParams, "allowMultiple" | "allowFreeform"> & {
|
|
20
|
-
allowMultiple: boolean;
|
|
21
|
-
allowFreeform: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type AskToolDetails =
|
|
25
|
-
| AskAnsweredDetails
|
|
26
|
-
| { status: "unanswered"; question: string }
|
|
27
|
-
| { status: "cancelled"; question: string }
|
|
28
|
-
| { status: "ui_unavailable"; question: string };
|
|
29
|
-
|
|
30
|
-
export type AskResponse = {
|
|
31
|
-
content: Array<{ type: "text"; text: string }>;
|
|
32
|
-
details: AskToolDetails;
|
|
33
|
-
};
|
package/src/validation.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { AskOption, AskParams, ValidatedAskParams } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export function validateAskParams(params: AskParams): ValidatedAskParams {
|
|
4
|
-
const question = params.question.trim();
|
|
5
|
-
if (!question) throw new Error("Ask question must not be empty.");
|
|
6
|
-
|
|
7
|
-
const context = trimOptional(params.context);
|
|
8
|
-
const options = params.options.map(normalizeOption);
|
|
9
|
-
const labels = new Set<string>();
|
|
10
|
-
for (const option of options) {
|
|
11
|
-
if (labels.has(option.label)) throw new Error(`Ask options contain duplicate label: ${option.label}.`);
|
|
12
|
-
labels.add(option.label);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const allowFreeform = params.allowFreeform ?? true;
|
|
16
|
-
if (options.length === 0) throw new Error("Ask needs at least one option.");
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
question,
|
|
20
|
-
...(context === undefined ? {} : { context }),
|
|
21
|
-
options,
|
|
22
|
-
allowMultiple: params.allowMultiple ?? false,
|
|
23
|
-
allowFreeform,
|
|
24
|
-
...(params.timeout === undefined ? {} : { timeout: params.timeout }),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeOption(option: AskOption): AskOption {
|
|
29
|
-
const label = option.label.trim();
|
|
30
|
-
if (!label) throw new Error("Ask option label must not be empty.");
|
|
31
|
-
const description = trimOptional(option.description);
|
|
32
|
-
const preview = preserveOptional(option.preview);
|
|
33
|
-
return {
|
|
34
|
-
label,
|
|
35
|
-
...(description === undefined ? {} : { description }),
|
|
36
|
-
...(preview === undefined ? {} : { preview }),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function trimOptional(value: string | undefined): string | undefined {
|
|
41
|
-
if (value === undefined) return undefined;
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
return trimmed || undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function preserveOptional(value: string | undefined): string | undefined {
|
|
47
|
-
return value?.trim() ? value : undefined;
|
|
48
|
-
}
|