@trevonistrevon/pi-loop 0.3.0 → 0.4.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/DIFFERENTIAL_REVIEW_REPORT.md +59 -0
- package/README.md +50 -6
- package/dist/index.js +301 -62
- package/dist/scheduler.js +3 -3
- package/dist/task-store.d.ts +22 -0
- package/dist/task-store.js +181 -0
- package/dist/task-types.d.ts +15 -0
- package/dist/task-types.js +1 -0
- package/dist/trigger-system.d.ts +2 -1
- package/dist/trigger-system.js +14 -16
- package/dist/types.d.ts +1 -1
- package/dist/ui/widget.d.ts +8 -4
- package/dist/ui/widget.js +28 -74
- package/package.json +1 -1
- package/src/index.ts +300 -57
- package/src/scheduler.ts +3 -3
- package/src/task-store.ts +171 -0
- package/src/task-types.ts +17 -0
- package/src/trigger-system.ts +14 -16
- package/src/types.ts +1 -1
- package/src/ui/widget.ts +27 -70
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { parseInterval } from "./loop-parse.js";
|
|
|
22
22
|
import { MonitorManager } from "./monitor-manager.js";
|
|
23
23
|
import { CronScheduler } from "./scheduler.js";
|
|
24
24
|
import { LoopStore } from "./store.js";
|
|
25
|
+
import { TaskStore } from "./task-store.js";
|
|
25
26
|
import { TriggerSystem } from "./trigger-system.js";
|
|
26
27
|
import type { LoopEntry, Trigger } from "./types.js";
|
|
27
28
|
import { LoopWidget } from "./ui/widget.js";
|
|
@@ -68,19 +69,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
68
69
|
return join(process.cwd(), ".pi", "loops", "loops.json");
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function resolveTaskStorePath(): string | undefined {
|
|
73
|
+
if (loopScope === "memory") return undefined;
|
|
74
|
+
return join(process.cwd(), ".pi", "tasks", "tasks.json");
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
let store = new LoopStore(resolveStorePath());
|
|
72
78
|
const monitorManager = new MonitorManager(pi);
|
|
73
79
|
let scheduler: CronScheduler;
|
|
74
80
|
let triggerSystem: TriggerSystem;
|
|
75
|
-
const widget = new LoopWidget(store,
|
|
81
|
+
const widget = new LoopWidget(store, monitorManager);
|
|
82
|
+
widget.setTaskSummaryProvider(() => {
|
|
83
|
+
if (!nativeTaskStore) return { count: 0 };
|
|
84
|
+
const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
|
|
85
|
+
const active = tasks.find(t => t.status === "in_progress");
|
|
86
|
+
const next = tasks.find(t => t.status === "pending");
|
|
87
|
+
const focus = active
|
|
88
|
+
? `active: ${active.subject.slice(0, 50)}`
|
|
89
|
+
: next
|
|
90
|
+
? `next: ${next.subject.slice(0, 50)}`
|
|
91
|
+
: undefined;
|
|
92
|
+
return { count: tasks.length, focusText: focus };
|
|
93
|
+
});
|
|
76
94
|
|
|
77
95
|
scheduler = new CronScheduler(store, onLoopFire);
|
|
78
|
-
|
|
79
|
-
triggerSystem = new TriggerSystem(pi, scheduler, store);
|
|
96
|
+
triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
|
|
80
97
|
|
|
81
98
|
// ── pi-tasks integration ──
|
|
82
99
|
let tasksAvailable = false;
|
|
83
|
-
|
|
100
|
+
let nativeTaskStore: TaskStore | undefined;
|
|
101
|
+
let nativeTasksRegistered = false;
|
|
84
102
|
|
|
85
103
|
function checkTasksVersion() {
|
|
86
104
|
const requestId = randomUUID();
|
|
@@ -97,46 +115,79 @@ export default function (pi: ExtensionAPI) {
|
|
|
97
115
|
pi.events.on("tasks:ready", () => checkTasksVersion());
|
|
98
116
|
|
|
99
117
|
async function autoCreateTask(entry: LoopEntry): Promise<string | undefined> {
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
unsub(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
if (!entry.autoTask) return undefined;
|
|
119
|
+
if (tasksAvailable) {
|
|
120
|
+
try {
|
|
121
|
+
const requestId = randomUUID();
|
|
122
|
+
const taskId = await new Promise<string | undefined>((resolve, _reject) => {
|
|
123
|
+
const timer = setTimeout(() => { unsub(); resolve(undefined); }, 5000);
|
|
124
|
+
const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
|
|
125
|
+
unsub(); clearTimeout(timer);
|
|
126
|
+
const reply = raw as { success: boolean; data?: { id: string }; error?: string };
|
|
127
|
+
if (reply.success && reply.data) resolve(reply.data.id);
|
|
128
|
+
else resolve(undefined);
|
|
129
|
+
});
|
|
130
|
+
pi.events.emit("tasks:rpc:create", {
|
|
131
|
+
requestId,
|
|
132
|
+
subject: entry.prompt.slice(0, 80),
|
|
133
|
+
description: `Auto-created from loop #${entry.id}`,
|
|
134
|
+
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
135
|
+
});
|
|
110
136
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
return taskId;
|
|
119
|
-
} catch {
|
|
120
|
-
return undefined;
|
|
137
|
+
return taskId;
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
121
141
|
}
|
|
142
|
+
if (!nativeTaskStore) return undefined;
|
|
143
|
+
const task = nativeTaskStore.create(entry.prompt.slice(0, 80), `Auto-created from loop #${entry.id}`, {
|
|
144
|
+
loopId: entry.id,
|
|
145
|
+
trigger: entry.trigger,
|
|
146
|
+
});
|
|
147
|
+
widget.update();
|
|
148
|
+
return task.id;
|
|
122
149
|
}
|
|
123
150
|
|
|
124
151
|
async function hasPendingTasks(): Promise<number> {
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
if (tasksAvailable) {
|
|
153
|
+
try {
|
|
154
|
+
const requestId = randomUUID();
|
|
155
|
+
const count = await new Promise<number>((resolve) => {
|
|
156
|
+
const timer = setTimeout(() => { unsub(); resolve(-1); }, 3000);
|
|
157
|
+
const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
|
|
158
|
+
unsub(); clearTimeout(timer);
|
|
159
|
+
const reply = raw as { success: boolean; data?: { pending: number }; error?: string };
|
|
160
|
+
resolve(reply.success && reply.data ? reply.data.pending : -1);
|
|
161
|
+
});
|
|
162
|
+
pi.events.emit("tasks:rpc:pending", { requestId });
|
|
134
163
|
});
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
164
|
+
return count;
|
|
165
|
+
} catch {
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return nativeTaskStore ? nativeTaskStore.pendingCount() : -1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function cleanDoneTasks(): Promise<void> {
|
|
173
|
+
if (tasksAvailable) {
|
|
174
|
+
try {
|
|
175
|
+
const requestId = randomUUID();
|
|
176
|
+
await new Promise<void>((resolve) => {
|
|
177
|
+
const timer = setTimeout(() => { unsub(); resolve(); }, 3000);
|
|
178
|
+
const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
|
|
179
|
+
unsub(); clearTimeout(timer);
|
|
180
|
+
debug("tasks:rpc:clean — done tasks swept");
|
|
181
|
+
resolve();
|
|
182
|
+
});
|
|
183
|
+
pi.events.emit("tasks:rpc:clean", { requestId });
|
|
184
|
+
});
|
|
185
|
+
} catch { /* timeout or error, ignore */ }
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (nativeTaskStore) {
|
|
189
|
+
nativeTaskStore.sweepCompleted();
|
|
190
|
+
widget.update();
|
|
140
191
|
}
|
|
141
192
|
}
|
|
142
193
|
|
|
@@ -147,7 +198,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
198
|
|
|
148
199
|
if (entry.maxFires && (entry.fireCount ?? 0) >= entry.maxFires) {
|
|
149
200
|
debug(`loop #${entry.id} — reached maxFires ${entry.maxFires}, expiring`);
|
|
150
|
-
store.
|
|
201
|
+
store.delete(entry.id);
|
|
151
202
|
return;
|
|
152
203
|
}
|
|
153
204
|
store.update(entry.id, { fireCount: (entry.fireCount ?? 0) + 1 });
|
|
@@ -183,8 +234,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
234
|
store = new LoopStore(path);
|
|
184
235
|
widget.setStore(store);
|
|
185
236
|
scheduler = new CronScheduler(store, onLoopFire);
|
|
186
|
-
|
|
187
|
-
triggerSystem = new TriggerSystem(pi, scheduler, store);
|
|
237
|
+
triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
|
|
188
238
|
}
|
|
189
239
|
storeUpgraded = true;
|
|
190
240
|
}
|
|
@@ -206,6 +256,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
206
256
|
_latestCtx = ctx;
|
|
207
257
|
widget.setUICtx(ctx.ui);
|
|
208
258
|
upgradeStoreIfNeeded(ctx);
|
|
259
|
+
widget.update();
|
|
209
260
|
});
|
|
210
261
|
|
|
211
262
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
@@ -213,6 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
264
|
widget.setUICtx(ctx.ui);
|
|
214
265
|
upgradeStoreIfNeeded(ctx);
|
|
215
266
|
showPersistedLoops();
|
|
267
|
+
widget.update();
|
|
216
268
|
});
|
|
217
269
|
|
|
218
270
|
pi.on("session_switch" as any, async (event: SessionSwitchEvent, ctx: ExtensionContext) => {
|
|
@@ -230,6 +282,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
282
|
|
|
231
283
|
upgradeStoreIfNeeded(ctx);
|
|
232
284
|
showPersistedLoops(isResume);
|
|
285
|
+
widget.update();
|
|
233
286
|
});
|
|
234
287
|
|
|
235
288
|
// ── Loop fire handler — sends a user message to re-wake the agent ──
|
|
@@ -245,7 +298,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
298
|
if (data.autoTask) {
|
|
246
299
|
const pending = await hasPendingTasks();
|
|
247
300
|
if (pending === 0) {
|
|
248
|
-
debug(`loop:fire #${data.loopId} — no pending tasks, skipping`);
|
|
301
|
+
debug(`loop:fire #${data.loopId} — no pending tasks, skipping, requesting cleanup`);
|
|
302
|
+
cleanDoneTasks();
|
|
249
303
|
return;
|
|
250
304
|
}
|
|
251
305
|
}
|
|
@@ -382,7 +436,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
382
436
|
if (monitor && monitor.status !== "running") {
|
|
383
437
|
debug(`loop #${entry.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
|
|
384
438
|
triggerSystem.remove(entry.id);
|
|
385
|
-
store.
|
|
439
|
+
store.delete(entry.id);
|
|
386
440
|
}
|
|
387
441
|
}
|
|
388
442
|
} catch { /* filter parse failure, ignore */ }
|
|
@@ -413,7 +467,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
413
467
|
if (monitor && monitor.status !== "running") {
|
|
414
468
|
debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
|
|
415
469
|
triggerSystem.remove(doneLoop.id);
|
|
416
|
-
store.
|
|
470
|
+
store.delete(doneLoop.id);
|
|
417
471
|
}
|
|
418
472
|
}
|
|
419
473
|
|
|
@@ -474,7 +528,7 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
474
528
|
? scheduler.nextFire(entry.id)
|
|
475
529
|
: undefined;
|
|
476
530
|
|
|
477
|
-
const statusIcon = entry.status === "active" ? "
|
|
531
|
+
const statusIcon = entry.status === "active" ? "*" : entry.status === "paused" ? "-" : "x";
|
|
478
532
|
let line = `${statusIcon} #${entry.id} [${entry.status}] ${entry.prompt.slice(0, 60)}`;
|
|
479
533
|
line += ` (${triggerDesc})`;
|
|
480
534
|
if (nextFire) {
|
|
@@ -607,7 +661,7 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
607
661
|
|
|
608
662
|
const lines: string[] = [];
|
|
609
663
|
for (const m of monitors) {
|
|
610
|
-
const icon = m.status === "running" ? "
|
|
664
|
+
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "!!";
|
|
611
665
|
const age = Date.now() - m.startedAt;
|
|
612
666
|
const ageStr = formatRemaining(age);
|
|
613
667
|
let line = `${icon} #${m.id} [${m.status}] ${m.command.slice(0, 60)} — ${m.outputLines} lines (${ageStr})`;
|
|
@@ -744,45 +798,45 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
744
798
|
async function viewLoops(ui: ExtensionUIContext) {
|
|
745
799
|
const loops = store.list();
|
|
746
800
|
if (loops.length === 0) {
|
|
747
|
-
await ui.select("No active loops", ["
|
|
801
|
+
await ui.select("No active loops", ["< Back"]);
|
|
748
802
|
return;
|
|
749
803
|
}
|
|
750
804
|
|
|
751
805
|
const choices = loops.map((l: LoopEntry) => {
|
|
752
|
-
const icon = l.status === "active" ? "
|
|
806
|
+
const icon = l.status === "active" ? "*" : l.status === "paused" ? "-" : "x";
|
|
753
807
|
const triggerDesc = l.trigger.type === "cron" ? `cron: ${l.trigger.schedule}` : l.trigger.type === "event" ? `event: ${l.trigger.source}` : `hybrid: ${l.trigger.cron}`;
|
|
754
808
|
return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${triggerDesc})`;
|
|
755
809
|
});
|
|
756
|
-
choices.push("
|
|
810
|
+
choices.push("< Back");
|
|
757
811
|
|
|
758
812
|
const selected = await ui.select("Active Loops", choices);
|
|
759
|
-
if (!selected || selected === "
|
|
813
|
+
if (!selected || selected === "< Back") return;
|
|
760
814
|
|
|
761
815
|
const match = selected.match(/#(\d+)/);
|
|
762
816
|
if (match) {
|
|
763
817
|
const entry = store.get(match[1]);
|
|
764
818
|
if (entry) {
|
|
765
|
-
const actions = ["
|
|
766
|
-
if (entry.status === "active") actions.unshift("
|
|
767
|
-
else if (entry.status === "paused") actions.unshift("
|
|
768
|
-
actions.push("
|
|
819
|
+
const actions = ["x Delete"];
|
|
820
|
+
if (entry.status === "active") actions.unshift("- Pause");
|
|
821
|
+
else if (entry.status === "paused") actions.unshift("* Resume");
|
|
822
|
+
actions.push("< Back");
|
|
769
823
|
|
|
770
824
|
const action = await ui.select(
|
|
771
825
|
`#${entry.id}: ${entry.prompt}\nTrigger: ${JSON.stringify(entry.trigger)}`,
|
|
772
826
|
actions,
|
|
773
827
|
);
|
|
774
828
|
|
|
775
|
-
if (action === "
|
|
829
|
+
if (action === "x Delete") {
|
|
776
830
|
triggerSystem.remove(entry.id);
|
|
777
831
|
store.delete(entry.id);
|
|
778
832
|
widget.update();
|
|
779
833
|
ui.notify(`Loop #${entry.id} deleted`, "info");
|
|
780
|
-
} else if (action === "
|
|
834
|
+
} else if (action === "- Pause") {
|
|
781
835
|
store.update(entry.id, { status: "paused" });
|
|
782
836
|
triggerSystem.remove(entry.id);
|
|
783
837
|
widget.update();
|
|
784
838
|
ui.notify(`Loop #${entry.id} paused`, "info");
|
|
785
|
-
} else if (action === "
|
|
839
|
+
} else if (action === "* Resume") {
|
|
786
840
|
store.update(entry.id, { status: "active" });
|
|
787
841
|
triggerSystem.add(entry);
|
|
788
842
|
widget.update();
|
|
@@ -799,4 +853,193 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
799
853
|
const active = loops.filter(l => l.status === "active").length;
|
|
800
854
|
ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
|
|
801
855
|
}
|
|
856
|
+
|
|
857
|
+
async function createNativeTaskInteractively(ui: ExtensionUIContext) {
|
|
858
|
+
if (!nativeTaskStore) {
|
|
859
|
+
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const subject = await ui.input("Task subject");
|
|
864
|
+
if (!subject) return;
|
|
865
|
+
const description = await ui.input("Task description") || subject;
|
|
866
|
+
const entry = nativeTaskStore.create(subject, description);
|
|
867
|
+
widget.update();
|
|
868
|
+
ui.notify(`Task #${entry.id} created`, "info");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function viewNativeTasks(ui: ExtensionUIContext): Promise<void> {
|
|
872
|
+
if (!nativeTaskStore) {
|
|
873
|
+
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const tasks = nativeTaskStore.list();
|
|
878
|
+
const choices = tasks.map((task) => {
|
|
879
|
+
const icon = task.status === "in_progress" ? ">" : task.status === "completed" ? "ok" : "*";
|
|
880
|
+
return `${icon} #${task.id} [${task.status}] ${task.subject.slice(0, 60)}`;
|
|
881
|
+
});
|
|
882
|
+
choices.unshift("+ Create task");
|
|
883
|
+
choices.push("< Back");
|
|
884
|
+
|
|
885
|
+
const selected = await ui.select("Native Tasks", choices);
|
|
886
|
+
if (!selected || selected === "< Back") return;
|
|
887
|
+
if (selected === "+ Create task") {
|
|
888
|
+
await createNativeTaskInteractively(ui);
|
|
889
|
+
return viewNativeTasks(ui);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const match = selected.match(/#(\d+)/);
|
|
893
|
+
if (!match) return viewNativeTasks(ui);
|
|
894
|
+
|
|
895
|
+
const task = nativeTaskStore.get(match[1]);
|
|
896
|
+
if (!task) return viewNativeTasks(ui);
|
|
897
|
+
|
|
898
|
+
const actions = ["x Delete"];
|
|
899
|
+
if (task.status === "pending") {
|
|
900
|
+
actions.unshift("ok Complete");
|
|
901
|
+
actions.unshift("> Start");
|
|
902
|
+
} else if (task.status === "in_progress") {
|
|
903
|
+
actions.unshift("ok Complete");
|
|
904
|
+
actions.unshift("* Return to pending");
|
|
905
|
+
} else {
|
|
906
|
+
actions.unshift("* Reopen");
|
|
907
|
+
}
|
|
908
|
+
actions.push("< Back");
|
|
909
|
+
|
|
910
|
+
const action = await ui.select(`#${task.id}: ${task.subject}\n\n${task.description}`, actions);
|
|
911
|
+
if (!action || action === "< Back") return viewNativeTasks(ui);
|
|
912
|
+
|
|
913
|
+
if (action === "x Delete") {
|
|
914
|
+
nativeTaskStore.delete(task.id);
|
|
915
|
+
ui.notify(`Task #${task.id} deleted`, "info");
|
|
916
|
+
} else if (action === "> Start") {
|
|
917
|
+
nativeTaskStore.update(task.id, { status: "in_progress" });
|
|
918
|
+
ui.notify(`Task #${task.id} started`, "info");
|
|
919
|
+
} else if (action === "ok Complete") {
|
|
920
|
+
nativeTaskStore.update(task.id, { status: "completed" });
|
|
921
|
+
ui.notify(`Task #${task.id} completed`, "info");
|
|
922
|
+
} else if (action === "* Return to pending" || action === "* Reopen") {
|
|
923
|
+
nativeTaskStore.update(task.id, { status: "pending" });
|
|
924
|
+
ui.notify(`Task #${task.id} reopened`, "info");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
widget.update();
|
|
928
|
+
return viewNativeTasks(ui);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Native task tools (only when pi-tasks is absent) ──
|
|
932
|
+
|
|
933
|
+
setTimeout(async () => {
|
|
934
|
+
if (tasksAvailable || nativeTasksRegistered) return;
|
|
935
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath());
|
|
936
|
+
nativeTasksRegistered = true;
|
|
937
|
+
const taskStore = nativeTaskStore;
|
|
938
|
+
|
|
939
|
+
pi.registerCommand("tasks", {
|
|
940
|
+
description: "View or manage native pi-loop tasks when pi-tasks is not installed",
|
|
941
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
942
|
+
const trimmed = args.trim();
|
|
943
|
+
if (!nativeTaskStore) {
|
|
944
|
+
ctx.ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (trimmed) {
|
|
948
|
+
const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
|
|
949
|
+
widget.update();
|
|
950
|
+
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
await viewNativeTasks(ctx.ui);
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
pi.registerTool({
|
|
958
|
+
name: "TaskCreate",
|
|
959
|
+
label: "TaskCreate",
|
|
960
|
+
description: `Create a task for tracking work across turns. Use when you need to track progress on complex multi-step tasks.
|
|
961
|
+
|
|
962
|
+
Fields:
|
|
963
|
+
- subject: brief actionable title
|
|
964
|
+
- description: detailed requirements
|
|
965
|
+
- metadata: optional tags/metadata`,
|
|
966
|
+
parameters: Type.Object({
|
|
967
|
+
subject: Type.String({ description: "Brief actionable title for the task" }),
|
|
968
|
+
description: Type.String({ description: "Detailed description of what needs to be done" }),
|
|
969
|
+
}),
|
|
970
|
+
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
971
|
+
const entry = taskStore.create(params.subject, params.description);
|
|
972
|
+
widget.update();
|
|
973
|
+
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
pi.registerTool({
|
|
978
|
+
name: "TaskList",
|
|
979
|
+
label: "TaskList",
|
|
980
|
+
description: `List all tasks with status. Use to check progress and find available work.`,
|
|
981
|
+
parameters: Type.Object({}),
|
|
982
|
+
execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
983
|
+
const tasks = taskStore.list();
|
|
984
|
+
if (tasks.length === 0) return Promise.resolve(textResult("No tasks."));
|
|
985
|
+
|
|
986
|
+
const lines: string[] = [];
|
|
987
|
+
const statuses: Record<"pending" | "in_progress" | "completed", number> = {
|
|
988
|
+
pending: 0,
|
|
989
|
+
in_progress: 0,
|
|
990
|
+
completed: 0,
|
|
991
|
+
};
|
|
992
|
+
for (const t of tasks) {
|
|
993
|
+
statuses[t.status]++;
|
|
994
|
+
const icon = t.status === "completed" ? "ok" : t.status === "in_progress" ? ">" : "*";
|
|
995
|
+
lines.push(`${icon} #${t.id} [${t.status}] ${t.subject.slice(0, 80)}`);
|
|
996
|
+
}
|
|
997
|
+
lines.unshift(`${tasks.length} tasks (${statuses.pending} pending, ${statuses.in_progress} in progress, ${statuses.completed} done)`);
|
|
998
|
+
return Promise.resolve(textResult(lines.join("\n")));
|
|
999
|
+
},
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
pi.registerTool({
|
|
1003
|
+
name: "TaskUpdate",
|
|
1004
|
+
label: "TaskUpdate",
|
|
1005
|
+
description: `Update task status or details. Set status to "in_progress" before starting work, "completed" when done.
|
|
1006
|
+
|
|
1007
|
+
Statuses: pending → in_progress → completed`,
|
|
1008
|
+
parameters: Type.Object({
|
|
1009
|
+
id: Type.String({ description: "Task ID to update" }),
|
|
1010
|
+
status: Type.Optional(Type.String({ description: "New status", enum: ["pending", "in_progress", "completed"] })),
|
|
1011
|
+
subject: Type.Optional(Type.String({ description: "New title" })),
|
|
1012
|
+
description: Type.Optional(Type.String({ description: "New description" })),
|
|
1013
|
+
}),
|
|
1014
|
+
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1015
|
+
const { id, status, subject, description } = params;
|
|
1016
|
+
const entry = taskStore.update(id, {
|
|
1017
|
+
status: status as "pending" | "in_progress" | "completed" | undefined,
|
|
1018
|
+
subject,
|
|
1019
|
+
description,
|
|
1020
|
+
});
|
|
1021
|
+
if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
|
|
1022
|
+
widget.update();
|
|
1023
|
+
const statusMsg = status ? ` → ${status}` : "";
|
|
1024
|
+
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}`));
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
pi.registerTool({
|
|
1029
|
+
name: "TaskDelete",
|
|
1030
|
+
label: "TaskDelete",
|
|
1031
|
+
description: `Delete a task by ID. Use for cleaning up completed or irrelevant tasks.`,
|
|
1032
|
+
parameters: Type.Object({
|
|
1033
|
+
id: Type.String({ description: "Task ID to delete" }),
|
|
1034
|
+
}),
|
|
1035
|
+
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1036
|
+
const deleted = taskStore.delete(params.id);
|
|
1037
|
+
widget.update();
|
|
1038
|
+
if (deleted) return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
1039
|
+
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
debug("native task tools registered (pi-tasks not detected)");
|
|
1044
|
+
}, 6000);
|
|
802
1045
|
}
|
package/src/scheduler.ts
CHANGED
|
@@ -65,7 +65,7 @@ export class CronScheduler {
|
|
|
65
65
|
const now = Date.now();
|
|
66
66
|
|
|
67
67
|
if (fireTime > entry.expiresAt) {
|
|
68
|
-
this.store.
|
|
68
|
+
this.store.delete(entry.id);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -85,7 +85,7 @@ export class CronScheduler {
|
|
|
85
85
|
|
|
86
86
|
const now2 = Date.now();
|
|
87
87
|
if (now2 >= current.expiresAt) {
|
|
88
|
-
this.store.
|
|
88
|
+
this.store.delete(entry.id);
|
|
89
89
|
this.timers.delete(entry.id);
|
|
90
90
|
this.fireTimes.delete(entry.id);
|
|
91
91
|
return;
|
|
@@ -96,7 +96,7 @@ export class CronScheduler {
|
|
|
96
96
|
if (current.recurring) {
|
|
97
97
|
const fresh = this.store.get(entry.id);
|
|
98
98
|
if (fresh?.maxFires && (fresh.fireCount ?? 0) >= fresh.maxFires) {
|
|
99
|
-
this.store.
|
|
99
|
+
this.store.delete(entry.id);
|
|
100
100
|
this.timers.delete(entry.id);
|
|
101
101
|
this.fireTimes.delete(entry.id);
|
|
102
102
|
return;
|