pi-quests 0.6.2 → 0.8.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.
@@ -44,6 +44,12 @@ export function formatToggleResult(id: string, done: boolean): string {
44
44
  return `Quest [${id}] ${done ? "done" : "undone"}`;
45
45
  }
46
46
 
47
+ export function formatBatchToggleResult(toggled: { id: string; done: boolean }[]): string {
48
+ return `Toggled ${toggled.length} tasks:\n${toggled
49
+ .map((quest) => `[${quest.id}] ${quest.done ? "done" : "undone"}`)
50
+ .join("\n")}`;
51
+ }
52
+
47
53
  export function formatUpdateResult(q: { id: string; description: string }): string {
48
54
  return `Updated quest [${q.id}]: ${q.description}`;
49
55
  }
@@ -52,6 +58,10 @@ export function formatDeleteResult(q: { id: string; description: string }): stri
52
58
  return `Deleted quest [${q.id}]: ${q.description}`;
53
59
  }
54
60
 
61
+ export function formatBatchDeleteResult(deleted: { id: string; description: string }[]): string {
62
+ return `Deleted ${deleted.length} tasks:\n${deleted.map((quest) => `[${quest.id}] ${quest.description}`).join("\n")}`;
63
+ }
64
+
55
65
  export function formatNotFound(id: string): string {
56
66
  return `Quest [${id}] not found. IDs are random hex strings shown in brackets. Use the list action to see valid IDs.`;
57
67
  }
@@ -100,8 +110,12 @@ export function formatUnknownActionError(action: string): string {
100
110
  return `Unknown action: ${action}. Use the list action or check the tool schema for supported actions.`;
101
111
  }
102
112
 
103
- export function formatNothingToRevertError(): string {
104
- return `Nothing to revert. The history is empty because no mutating actions have been performed yet.`;
113
+ export function formatNothingToUndoError(): string {
114
+ return `Nothing to undo. The history is empty because no mutating actions have been performed yet.`;
115
+ }
116
+
117
+ export function formatNothingToRedoError(): string {
118
+ return `Nothing to redo. The redo stack is empty because no actions have been undone yet.`;
105
119
  }
106
120
 
107
121
  export function formatReorderedQuestNotFoundError(): string {
@@ -1,34 +1,32 @@
1
- import type { ResolvedConfig } from "../config.js";
2
- import { logger } from "../logger.js";
3
-
4
- const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
5
-
6
- type NudgeCandidate = { index: number; message: string };
1
+ export interface QuestUsageStats {
2
+ readonly totalToolCalls: number;
3
+ readonly questToolCalls: number;
4
+ readonly nonQuestToolCalls: number;
5
+ readonly consecutiveNonQuestToolCalls: number;
6
+ readonly hasUsedQuestTool: boolean;
7
+ readonly lastQuestToolTimestamp: number | undefined;
8
+ }
7
9
 
8
10
  export class QuestUsageTracker {
9
11
  private totalToolCalls = 0;
12
+ private questToolCalls = 0;
13
+ private nonQuestToolCalls = 0;
10
14
  private consecutiveNonQuestToolCalls = 0;
11
15
  private hasEverUsedQuestTool = false;
12
- private lastQuestToolTime = 0;
13
- private nudgedThisTurn = false;
14
- private lastNudgeTime = 0;
15
- private lastNudgeIndex = -1;
16
-
17
- constructor(private readonly config: ResolvedConfig) {}
16
+ private lastQuestToolTimestamp: number | undefined;
18
17
 
19
18
  onToolExecution(toolName: string): void {
20
19
  this.totalToolCalls++;
21
20
  if (toolName === "quest") {
21
+ this.questToolCalls++;
22
22
  this.hasEverUsedQuestTool = true;
23
- this.lastQuestToolTime = Date.now();
23
+ this.lastQuestToolTimestamp = Date.now();
24
24
  this.consecutiveNonQuestToolCalls = 0;
25
- } else {
26
- this.consecutiveNonQuestToolCalls++;
25
+ return;
27
26
  }
28
- }
29
27
 
30
- clearTurnNudge(): void {
31
- this.nudgedThisTurn = false;
28
+ this.nonQuestToolCalls++;
29
+ this.consecutiveNonQuestToolCalls++;
32
30
  }
33
31
 
34
32
  get hasUsedQuestTool(): boolean {
@@ -37,141 +35,17 @@ export class QuestUsageTracker {
37
35
 
38
36
  markQuestToolUsed(): void {
39
37
  this.hasEverUsedQuestTool = true;
40
- this.lastQuestToolTime = Date.now();
41
- }
42
-
43
- getNudge(
44
- activeQuestCount: number,
45
- latestPrompt?: string,
46
- hasTopLevelQuestWithoutSubs?: boolean,
47
- ): string | undefined {
48
- if (this.nudgedThisTurn) {
49
- logger.debug("quests:tracker", "nudge-suppressed", { reason: "turn-limit" });
50
- return undefined;
51
- }
52
-
53
- const now = Date.now();
54
- const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
55
- if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
56
- logger.debug("quests:tracker", "nudge-suppressed", {
57
- reason: "cooldown",
58
- elapsed: now - this.lastNudgeTime,
59
- cooldownMs,
60
- });
61
- return undefined;
62
- }
63
-
64
- const eligible = this.getEligibleNudges(
65
- activeQuestCount,
66
- latestPrompt,
67
- hasTopLevelQuestWithoutSubs,
68
- );
69
-
70
- logger.debug("quests:tracker", "nudge-candidates", {
71
- eligible: eligible.length,
72
- indices: eligible.map((c) => c.index),
73
- });
74
-
75
- // Rotate priority: start checking from the nudge after the last one that fired
76
- const rotated = [
77
- ...eligible.filter((n) => n.index > this.lastNudgeIndex),
78
- ...eligible.filter((n) => n.index <= this.lastNudgeIndex),
79
- ];
80
-
81
- const winner = rotated[0];
82
- if (winner) {
83
- this.nudgedThisTurn = true;
84
- this.lastNudgeTime = now;
85
-
86
- logger.debug("quests:tracker", "nudge-fired", {
87
- winner: winner.index,
88
- rotatedFrom: this.lastNudgeIndex,
89
- });
90
- this.lastNudgeIndex = winner.index;
91
- return winner.message;
92
- }
93
-
94
- return undefined;
95
- }
96
-
97
- private getEligibleNudges(
98
- activeQuestCount: number,
99
- latestPrompt?: string,
100
- hasTopLevelQuestWithoutSubs?: boolean,
101
- ): NudgeCandidate[] {
102
- const candidates: NudgeCandidate[] = [];
103
-
104
- // 0. Initialization nudge
105
- if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
106
- candidates.push({
107
- index: 0,
108
- message: `QUEST REMINDER: You have made multiple 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}`,
109
- });
110
- }
111
-
112
- // 1. Complex-task entrypoint nudge
113
- if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
114
- candidates.push({
115
- index: 1,
116
- message: `QUEST REMINDER: Your latest prompt looks like a complex task, but there are 0 active quests. USE the quest tool to break it into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
117
- });
118
- }
119
-
120
- // 2. Time-based alignment nudge
121
- if (
122
- this.hasEverUsedQuestTool &&
123
- this.lastQuestToolTime > 0 &&
124
- this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
125
- Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
126
- ) {
127
- candidates.push({
128
- index: 2,
129
- message: `QUEST REMINDER: It has been a while since your last quest tool use and many tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`,
130
- });
131
- }
132
-
133
- // 3. Zero-active sustained-work nudge
134
- if (
135
- this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
136
- activeQuestCount === 0
137
- ) {
138
- candidates.push({
139
- index: 3,
140
- message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`,
141
- });
142
- }
143
-
144
- // 4. Sub-quest suggestion nudge
145
- if (
146
- this.hasEverUsedQuestTool &&
147
- this.consecutiveNonQuestToolCalls >= this.config.nudges.stepSuggestionToolCallThreshold &&
148
- activeQuestCount > 0 &&
149
- hasTopLevelQuestWithoutSubs
150
- ) {
151
- candidates.push({
152
- index: 4,
153
- message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and have active top-level quests without steps. Consider whether decomposing them with the \`split\` action would help track progress. ${ACKNOWLEDGEMENT}`,
154
- });
155
- }
156
-
157
- // 5. Stale-progress sustained-work nudge (with time-gate)
158
- if (
159
- this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
160
- activeQuestCount > 0 &&
161
- this.lastQuestToolTime > 0 &&
162
- Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
163
- ) {
164
- candidates.push({
165
- index: 5,
166
- message: `QUEST REMINDER: You have made many consecutive tool calls without using the quest tool despite having active quests. UPDATE your quest progress to reflect current status. ${ACKNOWLEDGEMENT}`,
167
- });
168
- }
169
-
170
- return candidates;
38
+ this.lastQuestToolTimestamp = Date.now();
171
39
  }
172
40
 
173
- private isComplexPrompt(prompt: string): boolean {
174
- const lower = prompt.toLowerCase();
175
- return this.config.nudges.complexTaskKeywords.some((kw) => lower.includes(kw));
41
+ getStats(): QuestUsageStats {
42
+ return {
43
+ totalToolCalls: this.totalToolCalls,
44
+ questToolCalls: this.questToolCalls,
45
+ nonQuestToolCalls: this.nonQuestToolCalls,
46
+ consecutiveNonQuestToolCalls: this.consecutiveNonQuestToolCalls,
47
+ hasUsedQuestTool: this.hasEverUsedQuestTool,
48
+ lastQuestToolTimestamp: this.lastQuestToolTimestamp,
49
+ };
176
50
  }
177
51
  }
@@ -10,7 +10,8 @@ export const QUEST_ACTIONS = {
10
10
  delete: "delete",
11
11
  clear: "clear",
12
12
  reorder: "reorder",
13
- revert: "revert",
13
+ undo: "undo",
14
+ redo: "redo",
14
15
  reparent: "reparent",
15
16
  rules: "rules",
16
17
  skill: "skill",
@@ -26,7 +27,8 @@ export const QUEST_ACTION_VALUES = [
26
27
  QUEST_ACTIONS.delete,
27
28
  QUEST_ACTIONS.clear,
28
29
  QUEST_ACTIONS.reorder,
29
- QUEST_ACTIONS.revert,
30
+ QUEST_ACTIONS.undo,
31
+ QUEST_ACTIONS.redo,
30
32
  QUEST_ACTIONS.reparent,
31
33
  QUEST_ACTIONS.rules,
32
34
  QUEST_ACTIONS.skill,
@@ -1,7 +1,7 @@
1
- import type { MessageRenderer, MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
2
- import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
3
- import type { Component } from "@mariozechner/pi-tui";
4
- import { Markdown } from "@mariozechner/pi-tui";
1
+ import type { MessageRenderer, MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
3
+ import type { Component } from "@earendil-works/pi-tui";
4
+ import { Markdown } from "@earendil-works/pi-tui";
5
5
  import { logger } from "../logger.js";
6
6
 
7
7
  export const questChangelogRenderer: MessageRenderer<{ content: string }> = (
@@ -1,5 +1,5 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { QuestLog } from "../quest/dataplane.js";
@@ -1,5 +1,5 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { visibleWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { Quest, Step } from "../quest/types.js";
4
4
 
5
5
  export function formatQuestRow(
@@ -1,4 +1,4 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { QuestLog } from "../quest/dataplane.js";
3
3
 
4
4
  export class QuestStatusWidget {
@@ -1,5 +1,5 @@
1
- import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
2
- import { Text } from "@mariozechner/pi-tui";
1
+ import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { Quest } from "../quest/types.js";
@@ -11,6 +11,7 @@ type QuestArgs = {
11
11
  descriptions?: string[];
12
12
  targetId?: string;
13
13
  id?: string;
14
+ ids?: string[];
14
15
  all?: boolean;
15
16
  };
16
17
 
@@ -31,12 +32,29 @@ export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown
31
32
  );
32
33
  }
33
34
 
34
- if (
35
- (args.action === QUEST_ACTIONS.toggle ||
36
- args.action === QUEST_ACTIONS.update ||
37
- args.action === QUEST_ACTIONS.delete) &&
38
- args.id !== undefined
39
- ) {
35
+ if (args.action === QUEST_ACTIONS.toggle) {
36
+ if (args.ids && args.ids.length > 1) {
37
+ return new Text(`${actionText} ${theme.fg("muted", `[${args.ids.length} tasks]`)}`, 0, 0);
38
+ }
39
+
40
+ const toggleId = args.id ?? args.ids?.[0];
41
+ if (toggleId !== undefined) {
42
+ return new Text(`${actionText} ${theme.fg("muted", `[${toggleId}]`)}`, 0, 0);
43
+ }
44
+ }
45
+
46
+ if (args.action === QUEST_ACTIONS.delete) {
47
+ if (args.ids && args.ids.length > 1) {
48
+ return new Text(`${actionText} ${theme.fg("muted", `[${args.ids.length} tasks]`)}`, 0, 0);
49
+ }
50
+
51
+ const deleteId = args.id ?? args.ids?.[0];
52
+ if (deleteId !== undefined) {
53
+ return new Text(`${actionText} ${theme.fg("muted", `[${deleteId}]`)}`, 0, 0);
54
+ }
55
+ }
56
+
57
+ if (args.action === QUEST_ACTIONS.update && args.id !== undefined) {
40
58
  return new Text(`${actionText} ${theme.fg("muted", `[${args.id}]`)}`, 0, 0);
41
59
  }
42
60
 
@@ -59,8 +77,11 @@ export function renderQuestResult(config: ResolvedConfig) {
59
77
  isPartial: options.isPartial,
60
78
  });
61
79
  const details = result.details as Record<string, unknown> | undefined;
62
- const allQuests = details?.quests as Quest[] | undefined;
63
- const questsToRender = (details?.displayQuests ?? details?.quests) as Quest[] | undefined;
80
+ const snapshotQuests = details?.snapshotQuests as Quest[] | undefined;
81
+ const allQuests = (snapshotQuests ?? details?.quests) as Quest[] | undefined;
82
+ const questsToRender = (snapshotQuests ?? details?.displayQuests ?? details?.quests) as
83
+ | Quest[]
84
+ | undefined;
64
85
 
65
86
  if (
66
87
  !Array.isArray(questsToRender) ||
@@ -1,17 +1,18 @@
1
- import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { type Static, Type } from "@sinclair/typebox";
1
+ import type { AgentToolResult, ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Static, Type } from "typebox";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  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
- import { QUEST_ACTIONS } from "../quest/types.js";
7
+ import { QUEST_ACTIONS, type QuestActionType } from "../quest/types.js";
8
8
 
9
9
  const SPLIT_DISPLAY_ACTIONS = [
10
10
  QUEST_ACTIONS.add,
11
11
  QUEST_ACTIONS.list,
12
12
  QUEST_ACTIONS.split,
13
13
  QUEST_ACTIONS.add_step,
14
- QUEST_ACTIONS.revert,
14
+ QUEST_ACTIONS.undo,
15
+ QUEST_ACTIONS.redo,
15
16
  ] as const;
16
17
 
17
18
  import { invalidateQuestListWidget } from "../renderers/commands.js";
@@ -20,75 +21,72 @@ import { createQuestParams, type QuestParamsType } from "./params.js";
20
21
 
21
22
  type QuestToolParams = Static<QuestParamsType>;
22
23
 
23
- const toolHandlers: {
24
- [K in QuestToolParams["action"]]: (
25
- questLog: QuestLog,
26
- params: QuestToolParams,
27
- toolCallId: string,
28
- ) => AgentToolResult<unknown>;
29
- } = {
30
- [QUEST_ACTIONS.add](questLog, params, toolCallId) {
31
- return runTool(questLog, toolCallId, {
24
+ type ToolHandler = (
25
+ questLog: QuestLog,
26
+ params: QuestToolParams,
27
+ toolCallId: string,
28
+ ) => AgentToolResult<unknown>;
29
+
30
+ const toolHandlers: Record<QuestActionType, ToolHandler> = {
31
+ [QUEST_ACTIONS.add]: (questLog, params, toolCallId) =>
32
+ runTool(questLog, toolCallId, {
32
33
  type: QUEST_ACTIONS.add,
33
34
  descriptions: params.descriptions,
34
- });
35
- },
36
- [QUEST_ACTIONS.split](questLog, params, toolCallId) {
37
- return runTool(questLog, toolCallId, {
35
+ }),
36
+ [QUEST_ACTIONS.split]: (questLog, params, toolCallId) =>
37
+ runTool(questLog, toolCallId, {
38
38
  type: QUEST_ACTIONS.split,
39
39
  id: params.id,
40
40
  descriptions: params.descriptions,
41
- });
42
- },
43
- [QUEST_ACTIONS.add_step](questLog, params, toolCallId) {
44
- return runTool(questLog, toolCallId, {
41
+ }),
42
+ [QUEST_ACTIONS.add_step]: (questLog, params, toolCallId) =>
43
+ runTool(questLog, toolCallId, {
45
44
  type: QUEST_ACTIONS.add_step,
46
45
  id: params.id,
47
46
  descriptions: params.descriptions,
48
- });
49
- },
50
- [QUEST_ACTIONS.list](questLog, _params, toolCallId) {
51
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list });
52
- },
53
- [QUEST_ACTIONS.toggle](questLog, params, toolCallId) {
54
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.toggle, id: params.id });
55
- },
56
- [QUEST_ACTIONS.update](questLog, params, toolCallId) {
57
- return runTool(questLog, toolCallId, {
47
+ }),
48
+ [QUEST_ACTIONS.list]: (questLog, _params, toolCallId) =>
49
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list }),
50
+ [QUEST_ACTIONS.toggle]: (questLog, params, toolCallId) =>
51
+ runTool(questLog, toolCallId, {
52
+ type: QUEST_ACTIONS.toggle,
53
+ id: params.id,
54
+ ids: params.ids,
55
+ }),
56
+ [QUEST_ACTIONS.update]: (questLog, params, toolCallId) =>
57
+ runTool(questLog, toolCallId, {
58
58
  type: QUEST_ACTIONS.update,
59
59
  id: params.id,
60
60
  description: params.description,
61
- });
62
- },
63
- [QUEST_ACTIONS.delete](questLog, params, toolCallId) {
64
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.delete, id: params.id });
65
- },
66
- [QUEST_ACTIONS.clear](questLog, params, toolCallId) {
67
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all });
68
- },
69
- [QUEST_ACTIONS.reorder](questLog, params, toolCallId) {
70
- return runTool(questLog, toolCallId, {
61
+ }),
62
+ [QUEST_ACTIONS.delete]: (questLog, params, toolCallId) =>
63
+ runTool(questLog, toolCallId, {
64
+ type: QUEST_ACTIONS.delete,
65
+ id: params.id,
66
+ ids: params.ids,
67
+ }),
68
+ [QUEST_ACTIONS.clear]: (questLog, params, toolCallId) =>
69
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all }),
70
+ [QUEST_ACTIONS.reorder]: (questLog, params, toolCallId) =>
71
+ runTool(questLog, toolCallId, {
71
72
  type: QUEST_ACTIONS.reorder,
72
73
  id: params.id,
73
74
  targetId: params.targetId,
74
- });
75
- },
76
- [QUEST_ACTIONS.reparent](questLog, params, toolCallId) {
77
- return runTool(questLog, toolCallId, {
75
+ }),
76
+ [QUEST_ACTIONS.reparent]: (questLog, params, toolCallId) =>
77
+ runTool(questLog, toolCallId, {
78
78
  type: QUEST_ACTIONS.reparent,
79
79
  id: params.id,
80
80
  parentId: params.parentId,
81
- });
82
- },
83
- [QUEST_ACTIONS.rules](questLog, _params, toolCallId) {
84
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules });
85
- },
86
- [QUEST_ACTIONS.skill](questLog, _params, toolCallId) {
87
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill });
88
- },
89
- [QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
90
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
91
- },
81
+ }),
82
+ [QUEST_ACTIONS.rules]: (questLog, _params, toolCallId) =>
83
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules }),
84
+ [QUEST_ACTIONS.skill]: (questLog, _params, toolCallId) =>
85
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill }),
86
+ [QUEST_ACTIONS.undo]: (questLog, _params, toolCallId) =>
87
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.undo }),
88
+ [QUEST_ACTIONS.redo]: (questLog, _params, toolCallId) =>
89
+ runTool(questLog, toolCallId, { type: QUEST_ACTIONS.redo }),
92
90
  };
93
91
 
94
92
  function runTool(
@@ -104,11 +102,10 @@ function runTool(
104
102
  action.type as (typeof SPLIT_DISPLAY_ACTIONS)[number],
105
103
  )
106
104
  ? questLog.getAll()
107
- : result.quest
108
- ? [result.quest]
109
- : [];
105
+ : (result.quests ?? (result.quest ? [result.quest] : []));
106
+ const snapshotQuests = action.type === QUEST_ACTIONS.delete ? displayQuests : undefined;
110
107
 
111
- return makeToolResult(result.message, questLog, displayQuests);
108
+ return makeToolResult(result.message, questLog, displayQuests, snapshotQuests);
112
109
  }
113
110
 
114
111
  export async function questToolExecute(
@@ -116,8 +113,13 @@ export async function questToolExecute(
116
113
  toolCallId: string,
117
114
  params: QuestToolParams,
118
115
  ): Promise<AgentToolResult<unknown>> {
119
- logger.debug("quests:tool", "execute", { toolCallId, action: params.action, id: params.id });
120
- const handler = toolHandlers[params.action];
116
+ logger.debug("quests:tool", "execute", {
117
+ toolCallId,
118
+ action: params.action,
119
+ id: params.id,
120
+ ids: params.ids,
121
+ });
122
+ const handler = toolHandlers[params.action as QuestActionType];
121
123
  return handler(questLog, params, toolCallId);
122
124
  }
123
125
 
@@ -140,7 +142,11 @@ export function registerQuestTool(
140
142
  parameters: createQuestParams(config.ids.length),
141
143
  execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
142
144
  questToolExecute(questLog, toolCallId, params),
143
- renderCall: renderQuestCall,
145
+ renderCall: renderQuestCall as unknown as (
146
+ args: object,
147
+ theme: Theme,
148
+ context: unknown,
149
+ ) => ReturnType<typeof renderQuestCall>,
144
150
  renderResult: renderQuestResult(config),
145
151
  });
146
152
 
@@ -159,8 +165,12 @@ export function registerQuestTool(
159
165
  ),
160
166
  execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) =>
161
167
  questToolExecute(questLog, "learn_quests", { action: QUEST_ACTIONS.skill }),
162
- renderCall: (_args, theme, context) =>
163
- renderQuestCall({ action: QUEST_ACTIONS.skill }, theme, context),
168
+ renderCall: ((_args: object, theme: Theme, context: unknown) =>
169
+ renderQuestCall({ action: QUEST_ACTIONS.skill }, theme, context)) as unknown as (
170
+ args: object,
171
+ theme: Theme,
172
+ context: unknown,
173
+ ) => ReturnType<typeof renderQuestCall>,
164
174
  renderResult: renderQuestResult(config),
165
175
  });
166
176
  }
@@ -1,5 +1,5 @@
1
- import { StringEnum } from "@mariozechner/pi-ai";
2
- import { Type } from "@sinclair/typebox";
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { Type } from "typebox";
3
3
  import { QUEST_ACTION_VALUES } from "../quest/types.js";
4
4
 
5
5
  export function createQuestParams(idLength: number) {
@@ -22,7 +22,13 @@ export function createQuestParams(idLength: number) {
22
22
  id: Type.Optional(
23
23
  Type.String({
24
24
  pattern,
25
- description: `Quest ID (required for toggle, update, delete, reorder actions). ALWAYS use the ${idLength}-digit hex ID shown in brackets, never the positional number.`,
25
+ description: `Quest ID (required for update, reorder, and reparent actions. For toggle and delete, provide either id or ids. ALWAYS use the ${idLength}-digit hex ID shown in brackets, never the positional number.`,
26
+ }),
27
+ ),
28
+ ids: Type.Optional(
29
+ Type.Array(Type.String({ pattern }), {
30
+ minItems: 1,
31
+ description: `Quest IDs for batch toggle or delete actions. Use this when operating on more than one ${idLength}-digit hex quest ID at once.`,
26
32
  }),
27
33
  ),
28
34
  targetId: Type.Optional(