pi-quests 0.4.0 → 0.5.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +17 -2
- package/src/commands/parse-args.ts +31 -16
- package/src/config.ts +10 -10
- package/src/index.ts +6 -5
- package/src/prompts/skill.md +60 -0
- package/src/prompts.ts +13 -2
- package/src/quest/dataplane.ts +269 -128
- package/src/quest/formatters.ts +94 -5
- package/src/quest/tracker.ts +73 -22
- package/src/quest/types.ts +11 -1
- package/src/renderers/commands.ts +7 -7
- package/src/renderers/quests.ts +11 -11
- package/src/renderers/tools.ts +10 -10
- package/src/tools/handler.ts +47 -11
- package/src/tools/params.ts +7 -7
package/src/quest/formatters.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error message design guidelines
|
|
3
|
+
*
|
|
4
|
+
* All error messages should follow a consistent three-part pattern:
|
|
5
|
+
* 1. PROBLEM — state what went wrong clearly and concisely
|
|
6
|
+
* 2. RULE — explain the constraint or invariant that was violated
|
|
7
|
+
* 3. RECOVERY HINT — suggest the next valid action the caller can take
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* "Quest [01] has incomplete steps. A parent quest can only be marked
|
|
11
|
+
* done after all its steps are complete. Try toggling the steps first."
|
|
12
|
+
*
|
|
13
|
+
* Keep messages actionable. Agents should never be left guessing why something
|
|
14
|
+
* failed or what to do next.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
export function formatQuestList(
|
|
2
18
|
quests: { id: string; description: string; done: boolean; parentId?: string }[],
|
|
3
19
|
): string {
|
|
@@ -37,13 +53,86 @@ export function formatDeleteResult(q: { id: string; description: string }): stri
|
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export function formatNotFound(id: string): string {
|
|
40
|
-
return `Quest [${id}] not found
|
|
56
|
+
return `Quest [${id}] not found. IDs are random hex strings shown in brackets. Use the list action to see valid IDs.`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatBlockedBySteps(id: string): string {
|
|
60
|
+
return `Quest [${id}] has incomplete steps. A parent quest can only be marked done or deleted after all its steps are complete. Toggle the steps first, or delete them if they are no longer needed.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatStepCannotHaveSteps(id: string): string {
|
|
64
|
+
return `Step [${id}] cannot have nested steps. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatEmptyDescriptionsError(): string {
|
|
68
|
+
return `Every description in a batch must be non-empty. Remove empty strings or split the batch into separate add calls.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatMissingDescriptionsError(action = "add"): string {
|
|
72
|
+
return `At least one description is required for the ${action} action. Provide a description string to create a quest.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatIdRequiredError(action: string): string {
|
|
76
|
+
return `An id is required for the ${action} action. Use the list action to see valid quest IDs. IDs are the hex strings shown in square brackets.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatDescriptionRequiredError(): string {
|
|
80
|
+
return `A description is required for the update action. Provide a non-empty string to update the quest.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatTargetIdRequiredError(): string {
|
|
84
|
+
return `A targetId is required for the reorder action. Provide the hex ID of the quest to insert before.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatReorderNotFoundError(): string {
|
|
88
|
+
return `Quest not found or is a step. Reorder only works on top-level quests. Use the list action to verify the ID and ensure it is a top-level quest.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatParentNotFoundError(id: string): string {
|
|
92
|
+
return `Parent quest [${id}] not found. Use the list action to see valid parent IDs.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatParentDoneError(id: string): string {
|
|
96
|
+
return `Cannot add a step to completed parent quest [${id}]. Steps can only be added to open parents. Reopen the parent first if you need to add more work.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatUnknownActionError(action: string): string {
|
|
100
|
+
return `Unknown action: ${action}. Use the list action or check the tool schema for supported actions.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatNothingToRevertError(): string {
|
|
104
|
+
return `Nothing to revert. The history is empty because no mutating actions have been performed yet.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatReorderedQuestNotFoundError(): string {
|
|
108
|
+
return `Reordered quest not found. It may have been deleted or cleared since the reorder was recorded.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatReparentResult(
|
|
112
|
+
q: { id: string; description: string },
|
|
113
|
+
parentId?: string,
|
|
114
|
+
): string {
|
|
115
|
+
return parentId
|
|
116
|
+
? `Moved quest [${q.id}] under [${parentId}]: ${q.description}`
|
|
117
|
+
: `Promoted quest [${q.id}] to top-level: ${q.description}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatReparentTargetNotFoundError(id: string): string {
|
|
121
|
+
return `Target quest [${id}] not found. Use the list action to see valid parent IDs.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatReparentTargetIsStepError(id: string): string {
|
|
125
|
+
return `Step [${id}] cannot be a parent. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatReparentTargetDoneError(id: string): string {
|
|
129
|
+
return `Cannot reparent under completed parent [${id}]. Steps can only be added to open parents.`;
|
|
41
130
|
}
|
|
42
131
|
|
|
43
|
-
export function
|
|
44
|
-
return `Quest [${id}] has
|
|
132
|
+
export function formatReparentDemoteHasStepsError(id: string): string {
|
|
133
|
+
return `Quest [${id}] has steps and cannot be demoted to a step. Delete or reparent its steps first.`;
|
|
45
134
|
}
|
|
46
135
|
|
|
47
|
-
export function
|
|
48
|
-
return `
|
|
136
|
+
export function formatReparentSelfParentError(id: string): string {
|
|
137
|
+
return `Quest [${id}] cannot be its own parent. Choose a different parent ID.`;
|
|
49
138
|
}
|
package/src/quest/tracker.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config.js";
|
|
2
2
|
|
|
3
|
-
const ACKNOWLEDGEMENT =
|
|
4
|
-
|
|
3
|
+
const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
|
|
4
|
+
|
|
5
|
+
type NudgeCandidate = { index: number; message: string };
|
|
5
6
|
|
|
6
7
|
export class QuestUsageTracker {
|
|
7
8
|
private totalToolCalls = 0;
|
|
@@ -9,6 +10,8 @@ export class QuestUsageTracker {
|
|
|
9
10
|
private hasEverUsedQuestTool = false;
|
|
10
11
|
private lastQuestToolTime = 0;
|
|
11
12
|
private nudgedThisTurn = false;
|
|
13
|
+
private lastNudgeTime = 0;
|
|
14
|
+
private lastNudgeIndex = -1;
|
|
12
15
|
|
|
13
16
|
constructor(private readonly config: ResolvedConfig) {}
|
|
14
17
|
|
|
@@ -34,59 +37,107 @@ export class QuestUsageTracker {
|
|
|
34
37
|
): string | undefined {
|
|
35
38
|
if (this.nudgedThisTurn) return undefined;
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
|
|
42
|
+
if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const eligible = this.getEligibleNudges(
|
|
47
|
+
activeQuestCount,
|
|
48
|
+
latestPrompt,
|
|
49
|
+
hasTopLevelQuestWithoutSubs,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Rotate priority: start checking from the nudge after the last one that fired
|
|
53
|
+
const rotated = [
|
|
54
|
+
...eligible.filter((n) => n.index > this.lastNudgeIndex),
|
|
55
|
+
...eligible.filter((n) => n.index <= this.lastNudgeIndex),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const winner = rotated[0];
|
|
59
|
+
if (winner) {
|
|
39
60
|
this.nudgedThisTurn = true;
|
|
40
|
-
|
|
61
|
+
this.lastNudgeTime = now;
|
|
62
|
+
this.lastNudgeIndex = winner.index;
|
|
63
|
+
return winner.message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getEligibleNudges(
|
|
70
|
+
activeQuestCount: number,
|
|
71
|
+
latestPrompt?: string,
|
|
72
|
+
hasTopLevelQuestWithoutSubs?: boolean,
|
|
73
|
+
): NudgeCandidate[] {
|
|
74
|
+
const candidates: NudgeCandidate[] = [];
|
|
75
|
+
|
|
76
|
+
// 0. Initialization nudge
|
|
77
|
+
if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
|
|
78
|
+
candidates.push({
|
|
79
|
+
index: 0,
|
|
80
|
+
message: `QUEST REMINDER: You have made ${this.totalToolCalls} tool calls but have NEVER used the quest tool this session. USE the quest tool to initialize tracking and break your work into concrete steps. ${ACKNOWLEDGEMENT}`,
|
|
81
|
+
});
|
|
41
82
|
}
|
|
42
83
|
|
|
43
|
-
//
|
|
84
|
+
// 1. Complex-task entrypoint nudge
|
|
44
85
|
if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
candidates.push({
|
|
87
|
+
index: 1,
|
|
88
|
+
message: `QUEST REMINDER: Your latest prompt appears to be a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
|
|
89
|
+
});
|
|
47
90
|
}
|
|
48
91
|
|
|
49
|
-
//
|
|
92
|
+
// 2. Time-based alignment nudge
|
|
50
93
|
if (
|
|
51
94
|
this.hasEverUsedQuestTool &&
|
|
52
95
|
this.lastQuestToolTime > 0 &&
|
|
53
96
|
this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
|
|
54
97
|
Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
|
|
55
98
|
) {
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
candidates.push({
|
|
100
|
+
index: 2,
|
|
101
|
+
message: `QUEST REMINDER: It has been a while since your last quest tool use and ${this.consecutiveNonQuestToolCalls} tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`,
|
|
102
|
+
});
|
|
58
103
|
}
|
|
59
104
|
|
|
60
|
-
//
|
|
105
|
+
// 3. Zero-active sustained-work nudge
|
|
61
106
|
if (
|
|
62
107
|
this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
|
|
63
108
|
activeQuestCount === 0
|
|
64
109
|
) {
|
|
65
|
-
|
|
66
|
-
|
|
110
|
+
candidates.push({
|
|
111
|
+
index: 3,
|
|
112
|
+
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`,
|
|
113
|
+
});
|
|
67
114
|
}
|
|
68
115
|
|
|
69
|
-
//
|
|
116
|
+
// 4. Sub-quest suggestion nudge
|
|
70
117
|
if (
|
|
71
118
|
this.hasEverUsedQuestTool &&
|
|
72
|
-
this.consecutiveNonQuestToolCalls >= this.config.nudges.
|
|
119
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.stepSuggestionToolCallThreshold &&
|
|
73
120
|
activeQuestCount > 0 &&
|
|
74
121
|
hasTopLevelQuestWithoutSubs
|
|
75
122
|
) {
|
|
76
|
-
|
|
77
|
-
|
|
123
|
+
candidates.push({
|
|
124
|
+
index: 4,
|
|
125
|
+
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without steps. Use the \`split\` action to break down complex tasks into smaller, trackable steps. ${ACKNOWLEDGEMENT}`,
|
|
126
|
+
});
|
|
78
127
|
}
|
|
79
128
|
|
|
80
|
-
//
|
|
129
|
+
// 5. Stale-progress sustained-work nudge
|
|
81
130
|
if (
|
|
82
131
|
this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
|
|
83
132
|
activeQuestCount > 0
|
|
84
133
|
) {
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
candidates.push({
|
|
135
|
+
index: 5,
|
|
136
|
+
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests.\nUPDATE your quest progress to reflect current status.\nALWAYS use steps to break down a quest into smaller steps, and to group related tasks together. ${ACKNOWLEDGEMENT}`,
|
|
137
|
+
});
|
|
87
138
|
}
|
|
88
139
|
|
|
89
|
-
return
|
|
140
|
+
return candidates;
|
|
90
141
|
}
|
|
91
142
|
|
|
92
143
|
private isComplexPrompt(prompt: string): boolean {
|
package/src/quest/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export const QUEST_ACTIONS = {
|
|
|
11
11
|
clear: "clear",
|
|
12
12
|
reorder: "reorder",
|
|
13
13
|
revert: "revert",
|
|
14
|
+
reparent: "reparent",
|
|
15
|
+
rules: "rules",
|
|
16
|
+
skill: "skill",
|
|
17
|
+
split: "split",
|
|
18
|
+
add_step: "add_step",
|
|
14
19
|
} as const;
|
|
15
20
|
|
|
16
21
|
export const QUEST_ACTION_VALUES = [
|
|
@@ -22,6 +27,11 @@ export const QUEST_ACTION_VALUES = [
|
|
|
22
27
|
QUEST_ACTIONS.clear,
|
|
23
28
|
QUEST_ACTIONS.reorder,
|
|
24
29
|
QUEST_ACTIONS.revert,
|
|
30
|
+
QUEST_ACTIONS.reparent,
|
|
31
|
+
QUEST_ACTIONS.rules,
|
|
32
|
+
QUEST_ACTIONS.skill,
|
|
33
|
+
QUEST_ACTIONS.split,
|
|
34
|
+
QUEST_ACTIONS.add_step,
|
|
25
35
|
] as const;
|
|
26
36
|
|
|
27
37
|
export type QuestActionType = (typeof QUEST_ACTION_VALUES)[number];
|
|
@@ -33,6 +43,6 @@ export interface Quest {
|
|
|
33
43
|
createdAt: number;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
export interface
|
|
46
|
+
export interface Step extends Quest {
|
|
37
47
|
parentId: string;
|
|
38
48
|
}
|
|
@@ -3,7 +3,7 @@ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi
|
|
|
3
3
|
import type { ResolvedConfig } from "../config.js";
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
import type { QuestLog } from "../quest/dataplane.js";
|
|
6
|
-
import { formatQuestRow,
|
|
6
|
+
import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
|
|
7
7
|
|
|
8
8
|
export class QuestListWidget {
|
|
9
9
|
private cachedWidth?: number;
|
|
@@ -50,17 +50,17 @@ export class QuestListWidget {
|
|
|
50
50
|
|
|
51
51
|
for (let i = 0; i < parents.length; i++) {
|
|
52
52
|
const parent = parents[i];
|
|
53
|
-
const
|
|
53
|
+
const steps = this.questLog.getSteps(parent.id);
|
|
54
54
|
|
|
55
55
|
const row = formatQuestRow(th, parent, this.config.ids.length, i + 1);
|
|
56
56
|
lines.push(width > 0 ? truncateToWidth(row, width) : row);
|
|
57
57
|
|
|
58
|
-
if (
|
|
59
|
-
lines.push(
|
|
58
|
+
if (steps.length > 0) {
|
|
59
|
+
lines.push(formatStepSpacerLine(th, this.config.ids.length));
|
|
60
60
|
|
|
61
|
-
for (const
|
|
62
|
-
const
|
|
63
|
-
lines.push(width > 0 ? truncateToWidth(
|
|
61
|
+
for (const step of steps) {
|
|
62
|
+
const stepRow = formatQuestRow(th, step, this.config.ids.length);
|
|
63
|
+
lines.push(width > 0 ? truncateToWidth(stepRow, width) : stepRow);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
lines.push("");
|
package/src/renderers/quests.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { Quest,
|
|
3
|
+
import type { Quest, Step } from "../quest/types.js";
|
|
4
4
|
|
|
5
5
|
export function formatQuestRow(
|
|
6
6
|
theme: Theme,
|
|
7
|
-
q: Quest |
|
|
7
|
+
q: Quest | Step,
|
|
8
8
|
idLength: number,
|
|
9
9
|
pos?: number,
|
|
10
10
|
): string {
|
|
11
|
-
const
|
|
11
|
+
const isStep = "parentId" in q && q.parentId;
|
|
12
12
|
|
|
13
|
-
// position test is only relevant for quests, not
|
|
14
|
-
const posText =
|
|
13
|
+
// position test is only relevant for quests, not steps
|
|
14
|
+
const posText = isStep ? "" : pos !== undefined ? `#${pos}` : "";
|
|
15
15
|
const posWidth = visibleWidth(posText);
|
|
16
16
|
const idText = `[${q.id}]`;
|
|
17
17
|
|
|
@@ -19,13 +19,13 @@ export function formatQuestRow(
|
|
|
19
19
|
const spacing = " ".repeat(visibleWidth(`${16 ** idLength}`) - posWidth);
|
|
20
20
|
const idStr = `${theme.fg("muted", idText)} ${theme.fg(q.done ? "dim" : "accent", `${posText}`)}${spacing}`;
|
|
21
21
|
|
|
22
|
-
// for
|
|
23
|
-
const markerNotDone =
|
|
24
|
-
const markerDone =
|
|
22
|
+
// for steps, use a different marker and indent
|
|
23
|
+
const markerNotDone = isStep ? theme.fg("muted", " └── ○ ") : theme.fg("muted", " ○ ");
|
|
24
|
+
const markerDone = isStep ? theme.fg("muted", " └── ✓ ") : theme.fg("success", " ✓ ");
|
|
25
25
|
const marker = q.done ? markerDone : markerNotDone;
|
|
26
26
|
|
|
27
|
-
// for
|
|
28
|
-
const descColor = q.done ||
|
|
27
|
+
// for steps, use dim text to contrast with the parent quests
|
|
28
|
+
const descColor = q.done || isStep ? "dim" : "text";
|
|
29
29
|
const desc = q.done
|
|
30
30
|
? theme.fg(descColor, theme.strikethrough(q.description))
|
|
31
31
|
: theme.fg(descColor, q.description);
|
|
@@ -34,7 +34,7 @@ export function formatQuestRow(
|
|
|
34
34
|
return line;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function
|
|
37
|
+
export function formatStepSpacerLine(theme: Theme, idLength: number): string {
|
|
38
38
|
const idTextLength = visibleWidth(`${16 ** idLength}`);
|
|
39
39
|
// spacing for `[id](position string)`
|
|
40
40
|
const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(idTextLength)}`;
|
package/src/renderers/tools.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ResolvedConfig } from "../config.js";
|
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
import type { Quest } from "../quest/types.js";
|
|
6
6
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
7
|
-
import { formatQuestRow,
|
|
7
|
+
import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
|
|
8
8
|
|
|
9
9
|
type QuestArgs = {
|
|
10
10
|
action: string;
|
|
@@ -76,7 +76,7 @@ export function renderQuestResult(config: ResolvedConfig) {
|
|
|
76
76
|
const parents = quests.filter((q) => !(q as Quest & { parentId?: string }).parentId);
|
|
77
77
|
const lines: string[] = [];
|
|
78
78
|
|
|
79
|
-
function
|
|
79
|
+
function getSteps(parentId: string) {
|
|
80
80
|
return quests.filter((q) => (q as Quest & { parentId?: string }).parentId === parentId);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -85,8 +85,8 @@ export function renderQuestResult(config: ResolvedConfig) {
|
|
|
85
85
|
const p = parents[j];
|
|
86
86
|
if (renderedIds.has(p.id)) return true;
|
|
87
87
|
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
88
|
+
const steps = getSteps(p.id).filter((step) => renderedIds.has(step.id));
|
|
89
|
+
if (steps.length > 0) return true;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
return false;
|
|
@@ -95,20 +95,20 @@ export function renderQuestResult(config: ResolvedConfig) {
|
|
|
95
95
|
for (let i = 0; i < parents.length; i++) {
|
|
96
96
|
const parent = parents[i];
|
|
97
97
|
const parentIncluded = renderedIds.has(parent.id);
|
|
98
|
-
const
|
|
98
|
+
const includedSteps = getSteps(parent.id).filter((step) => renderedIds.has(step.id));
|
|
99
99
|
|
|
100
|
-
if (!parentIncluded &&
|
|
100
|
+
if (!parentIncluded && includedSteps.length === 0) continue;
|
|
101
101
|
|
|
102
102
|
if (parentIncluded) {
|
|
103
103
|
lines.push(formatQuestRow(theme, parent, config.ids.length, i + 1));
|
|
104
|
-
if (
|
|
104
|
+
if (includedSteps.length > 0) lines.push(formatStepSpacerLine(theme, config.ids.length));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
for (const
|
|
108
|
-
lines.push(formatQuestRow(theme,
|
|
107
|
+
for (const step of includedSteps) {
|
|
108
|
+
lines.push(formatQuestRow(theme, step, config.ids.length));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
if (
|
|
111
|
+
if (includedSteps.length > 0 && willRenderLater(i)) {
|
|
112
112
|
lines.push("");
|
|
113
113
|
}
|
|
114
114
|
}
|
package/src/tools/handler.ts
CHANGED
|
@@ -5,6 +5,15 @@ import { logger } from "../logger.js";
|
|
|
5
5
|
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
|
|
6
6
|
import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
|
|
7
7
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
8
|
+
|
|
9
|
+
const SPLIT_DISPLAY_ACTIONS = [
|
|
10
|
+
QUEST_ACTIONS.add,
|
|
11
|
+
QUEST_ACTIONS.list,
|
|
12
|
+
QUEST_ACTIONS.split,
|
|
13
|
+
QUEST_ACTIONS.add_step,
|
|
14
|
+
QUEST_ACTIONS.revert,
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
8
17
|
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
9
18
|
import { createQuestParams, type QuestParamsType } from "./params.js";
|
|
10
19
|
|
|
@@ -21,7 +30,20 @@ const toolHandlers: {
|
|
|
21
30
|
return runTool(questLog, toolCallId, {
|
|
22
31
|
type: QUEST_ACTIONS.add,
|
|
23
32
|
descriptions: params.descriptions,
|
|
24
|
-
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
[QUEST_ACTIONS.split](questLog, params, toolCallId) {
|
|
36
|
+
return runTool(questLog, toolCallId, {
|
|
37
|
+
type: QUEST_ACTIONS.split,
|
|
38
|
+
id: params.id,
|
|
39
|
+
descriptions: params.descriptions,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
[QUEST_ACTIONS.add_step](questLog, params, toolCallId) {
|
|
43
|
+
return runTool(questLog, toolCallId, {
|
|
44
|
+
type: QUEST_ACTIONS.add_step,
|
|
45
|
+
id: params.id,
|
|
46
|
+
descriptions: params.descriptions,
|
|
25
47
|
});
|
|
26
48
|
},
|
|
27
49
|
[QUEST_ACTIONS.list](questLog, _params, toolCallId) {
|
|
@@ -50,6 +72,19 @@ const toolHandlers: {
|
|
|
50
72
|
targetId: params.targetId,
|
|
51
73
|
});
|
|
52
74
|
},
|
|
75
|
+
[QUEST_ACTIONS.reparent](questLog, params, toolCallId) {
|
|
76
|
+
return runTool(questLog, toolCallId, {
|
|
77
|
+
type: QUEST_ACTIONS.reparent,
|
|
78
|
+
id: params.id,
|
|
79
|
+
parentId: params.parentId,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
[QUEST_ACTIONS.rules](questLog, _params, toolCallId) {
|
|
83
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules });
|
|
84
|
+
},
|
|
85
|
+
[QUEST_ACTIONS.skill](questLog, _params, toolCallId) {
|
|
86
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill });
|
|
87
|
+
},
|
|
53
88
|
[QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
|
|
54
89
|
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
|
|
55
90
|
},
|
|
@@ -63,14 +98,13 @@ function runTool(
|
|
|
63
98
|
const result = questLog.execute(action);
|
|
64
99
|
logger.debug("quests:tool", "execute-complete", { toolCallId, success: result.success });
|
|
65
100
|
|
|
66
|
-
const displayQuests =
|
|
67
|
-
action.type
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: undefined;
|
|
101
|
+
const displayQuests = SPLIT_DISPLAY_ACTIONS.includes(
|
|
102
|
+
action.type as (typeof SPLIT_DISPLAY_ACTIONS)[number],
|
|
103
|
+
)
|
|
104
|
+
? questLog.getAll()
|
|
105
|
+
: result.quest
|
|
106
|
+
? [result.quest]
|
|
107
|
+
: undefined;
|
|
74
108
|
|
|
75
109
|
return makeToolResult(result.message, questLog, displayQuests);
|
|
76
110
|
}
|
|
@@ -95,9 +129,11 @@ export function registerQuestTool(
|
|
|
95
129
|
name: "quest",
|
|
96
130
|
label: "Quest",
|
|
97
131
|
description:
|
|
98
|
-
"Manage the session quest log
|
|
132
|
+
"Manage the session quest log and retrieve the complete quest system documentation. " +
|
|
133
|
+
"Use this VERY frequently to track tasks, plans, and progress. " +
|
|
134
|
+
"When you need to understand quests, steps, rules, or best practices, use action: 'skill' or action: 'rules'.",
|
|
99
135
|
promptSnippet:
|
|
100
|
-
"
|
|
136
|
+
"Manage quests and steps, or retrieve quest rules and best practices via skill/rules",
|
|
101
137
|
promptGuidelines: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
|
|
102
138
|
parameters: createQuestParams(config.ids.length),
|
|
103
139
|
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
package/src/tools/params.ts
CHANGED
|
@@ -18,13 +18,7 @@ export function createQuestParams(idLength: number) {
|
|
|
18
18
|
description: "New description (required for update action)",
|
|
19
19
|
}),
|
|
20
20
|
),
|
|
21
|
-
|
|
22
|
-
Type.String({
|
|
23
|
-
pattern,
|
|
24
|
-
description:
|
|
25
|
-
"Parent quest hex ID. Use this to break a large task into smaller, trackable steps. When provided, the added quest becomes a sub-quest under the parent.",
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
21
|
+
|
|
28
22
|
id: Type.Optional(
|
|
29
23
|
Type.String({
|
|
30
24
|
pattern,
|
|
@@ -38,6 +32,12 @@ export function createQuestParams(idLength: number) {
|
|
|
38
32
|
"Target quest ID for reorder action. The quest will be moved to just before the target quest.",
|
|
39
33
|
}),
|
|
40
34
|
),
|
|
35
|
+
parentId: Type.Optional(
|
|
36
|
+
Type.String({
|
|
37
|
+
pattern,
|
|
38
|
+
description: "Parent quest ID for reparent action. Omit to promote to top-level.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
41
|
all: Type.Optional(
|
|
42
42
|
Type.Boolean({
|
|
43
43
|
description: "Clear all quests when true (defaults to clearing only completed quests)",
|