@trevonistrevon/pi-loop 0.4.2 → 0.4.3
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/dist/index.js +47 -3
- package/package.json +1 -1
- package/src/index.ts +48 -3
package/dist/index.js
CHANGED
|
@@ -209,6 +209,35 @@ export default function (pi) {
|
|
|
209
209
|
message: buildLoopFireMessage(data),
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
function triggerHasEventSource(trigger, source) {
|
|
213
|
+
if (typeof trigger === "string")
|
|
214
|
+
return false;
|
|
215
|
+
return trigger.type === "event"
|
|
216
|
+
? trigger.source === source
|
|
217
|
+
: trigger.type === "hybrid"
|
|
218
|
+
? trigger.event.source === source
|
|
219
|
+
: false;
|
|
220
|
+
}
|
|
221
|
+
async function maybeBootstrapTaskLoop(entry) {
|
|
222
|
+
if (!entry.recurring)
|
|
223
|
+
return false;
|
|
224
|
+
if (!triggerHasEventSource(entry.trigger, "tasks:created"))
|
|
225
|
+
return false;
|
|
226
|
+
const pending = await hasPendingTasks();
|
|
227
|
+
if (pending <= 0)
|
|
228
|
+
return false;
|
|
229
|
+
debug(`loop #${entry.id} — bootstrapping existing pending tasks (${pending})`);
|
|
230
|
+
await queueOrDeliverNotification({
|
|
231
|
+
loopId: entry.id,
|
|
232
|
+
prompt: entry.prompt,
|
|
233
|
+
trigger: entry.trigger,
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
readOnly: entry.readOnly,
|
|
236
|
+
recurring: false,
|
|
237
|
+
autoTask: true,
|
|
238
|
+
});
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
212
241
|
async function deliverNotification(notification) {
|
|
213
242
|
if (notification.autoTask) {
|
|
214
243
|
const pending = await hasPendingTasks();
|
|
@@ -425,8 +454,9 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
425
454
|
"## readOnly mode",
|
|
426
455
|
"Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
|
|
427
456
|
"## Task-driven workflows",
|
|
428
|
-
"
|
|
429
|
-
"
|
|
457
|
+
"Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
|
|
458
|
+
"Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
|
|
459
|
+
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
430
460
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
431
461
|
],
|
|
432
462
|
parameters: Type.Object({
|
|
@@ -439,7 +469,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
439
469
|
readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
|
|
440
470
|
maxFires: Type.Optional(Type.Number({ description: "Auto-stop after N fires. Prevents infinite token burn on polling loops." })),
|
|
441
471
|
}),
|
|
442
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
472
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
443
473
|
const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
444
474
|
let trigger;
|
|
445
475
|
const inferred = triggerType ?? inferTriggerType(triggerInput);
|
|
@@ -487,6 +517,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
487
517
|
}
|
|
488
518
|
catch { /* filter parse failure, ignore */ }
|
|
489
519
|
}
|
|
520
|
+
const bootstrapped = await maybeBootstrapTaskLoop(entry);
|
|
490
521
|
widget.update();
|
|
491
522
|
const triggerDesc = trigger.type === "cron"
|
|
492
523
|
? `schedule: ${trigger.schedule}`
|
|
@@ -497,6 +528,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
497
528
|
`Trigger: ${triggerDesc}\n` +
|
|
498
529
|
`Recurring: ${entry.recurring}\n` +
|
|
499
530
|
(entry.autoTask ? `Auto-task: enabled\n` : "") +
|
|
531
|
+
(bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
|
|
500
532
|
((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
|
|
501
533
|
`ID: ${entry.id} (use LoopDelete to cancel)`));
|
|
502
534
|
},
|
|
@@ -956,6 +988,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
956
988
|
}
|
|
957
989
|
if (trimmed) {
|
|
958
990
|
const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
|
|
991
|
+
pi.events.emit("tasks:created", {
|
|
992
|
+
taskId: entry.id,
|
|
993
|
+
subject: entry.subject,
|
|
994
|
+
description: entry.description,
|
|
995
|
+
status: entry.status,
|
|
996
|
+
});
|
|
959
997
|
widget.update();
|
|
960
998
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
961
999
|
return;
|
|
@@ -982,6 +1020,12 @@ Fields:
|
|
|
982
1020
|
}),
|
|
983
1021
|
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
984
1022
|
const entry = taskStore.create(params.subject, params.description);
|
|
1023
|
+
pi.events.emit("tasks:created", {
|
|
1024
|
+
taskId: entry.id,
|
|
1025
|
+
subject: entry.subject,
|
|
1026
|
+
description: entry.description,
|
|
1027
|
+
status: entry.status,
|
|
1028
|
+
});
|
|
985
1029
|
widget.update();
|
|
986
1030
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
|
|
987
1031
|
},
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -229,6 +229,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function triggerHasEventSource(trigger: Trigger | string, source: string): boolean {
|
|
233
|
+
if (typeof trigger === "string") return false;
|
|
234
|
+
return trigger.type === "event"
|
|
235
|
+
? trigger.source === source
|
|
236
|
+
: trigger.type === "hybrid"
|
|
237
|
+
? trigger.event.source === source
|
|
238
|
+
: false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function maybeBootstrapTaskLoop(entry: LoopEntry): Promise<boolean> {
|
|
242
|
+
if (!entry.recurring) return false;
|
|
243
|
+
if (!triggerHasEventSource(entry.trigger, "tasks:created")) return false;
|
|
244
|
+
|
|
245
|
+
const pending = await hasPendingTasks();
|
|
246
|
+
if (pending <= 0) return false;
|
|
247
|
+
|
|
248
|
+
debug(`loop #${entry.id} — bootstrapping existing pending tasks (${pending})`);
|
|
249
|
+
await queueOrDeliverNotification({
|
|
250
|
+
loopId: entry.id,
|
|
251
|
+
prompt: entry.prompt,
|
|
252
|
+
trigger: entry.trigger,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
readOnly: entry.readOnly,
|
|
255
|
+
recurring: false,
|
|
256
|
+
autoTask: true,
|
|
257
|
+
});
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
232
261
|
async function deliverNotification(notification: PendingNotification): Promise<boolean> {
|
|
233
262
|
if (notification.autoTask) {
|
|
234
263
|
const pending = await hasPendingTasks();
|
|
@@ -470,8 +499,9 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
470
499
|
"## readOnly mode",
|
|
471
500
|
"Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
|
|
472
501
|
"## Task-driven workflows",
|
|
473
|
-
"
|
|
474
|
-
"
|
|
502
|
+
"Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
|
|
503
|
+
"Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
|
|
504
|
+
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
475
505
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
476
506
|
],
|
|
477
507
|
parameters: Type.Object({
|
|
@@ -485,7 +515,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
485
515
|
maxFires: Type.Optional(Type.Number({ description: "Auto-stop after N fires. Prevents infinite token burn on polling loops." })),
|
|
486
516
|
}),
|
|
487
517
|
|
|
488
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
518
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
489
519
|
const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
490
520
|
|
|
491
521
|
let trigger: Trigger;
|
|
@@ -536,6 +566,8 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
536
566
|
} catch { /* filter parse failure, ignore */ }
|
|
537
567
|
}
|
|
538
568
|
|
|
569
|
+
const bootstrapped = await maybeBootstrapTaskLoop(entry);
|
|
570
|
+
|
|
539
571
|
widget.update();
|
|
540
572
|
|
|
541
573
|
const triggerDesc = trigger.type === "cron"
|
|
@@ -549,6 +581,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
549
581
|
`Trigger: ${triggerDesc}\n` +
|
|
550
582
|
`Recurring: ${entry.recurring}\n` +
|
|
551
583
|
(entry.autoTask ? `Auto-task: enabled\n` : "") +
|
|
584
|
+
(bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
|
|
552
585
|
((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
|
|
553
586
|
`ID: ${entry.id} (use LoopDelete to cancel)`
|
|
554
587
|
));
|
|
@@ -1040,6 +1073,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1040
1073
|
}
|
|
1041
1074
|
if (trimmed) {
|
|
1042
1075
|
const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
|
|
1076
|
+
pi.events.emit("tasks:created", {
|
|
1077
|
+
taskId: entry.id,
|
|
1078
|
+
subject: entry.subject,
|
|
1079
|
+
description: entry.description,
|
|
1080
|
+
status: entry.status,
|
|
1081
|
+
});
|
|
1043
1082
|
widget.update();
|
|
1044
1083
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
1045
1084
|
return;
|
|
@@ -1067,6 +1106,12 @@ Fields:
|
|
|
1067
1106
|
}),
|
|
1068
1107
|
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1069
1108
|
const entry = taskStore.create(params.subject, params.description);
|
|
1109
|
+
pi.events.emit("tasks:created", {
|
|
1110
|
+
taskId: entry.id,
|
|
1111
|
+
subject: entry.subject,
|
|
1112
|
+
description: entry.description,
|
|
1113
|
+
status: entry.status,
|
|
1114
|
+
});
|
|
1070
1115
|
widget.update();
|
|
1071
1116
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
|
|
1072
1117
|
},
|