@trevonistrevon/pi-loop 0.6.0 → 0.6.2
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 +22 -0
- package/README.md +28 -143
- package/dist/commands/loop-command.d.ts +10 -2
- package/dist/commands/loop-command.js +84 -51
- package/dist/index.d.ts +2 -1
- package/dist/index.js +44 -16
- package/dist/loop-format.d.ts +3 -0
- package/dist/loop-format.js +20 -0
- package/dist/loop-parse.d.ts +1 -0
- package/dist/loop-parse.js +45 -1
- package/dist/loop-reducer.d.ts +13 -1
- package/dist/loop-reducer.js +26 -0
- package/dist/monitor-manager.d.ts +1 -1
- package/dist/monitor-manager.js +13 -4
- package/dist/notification-reducer.d.ts +2 -0
- package/dist/runtime/monitor-ondone-runtime.js +1 -1
- package/dist/runtime/notification-runtime.d.ts +2 -1
- package/dist/runtime/notification-runtime.js +19 -7
- package/dist/runtime/task-backlog-runtime.d.ts +2 -1
- package/dist/runtime/task-backlog-runtime.js +13 -9
- package/dist/scheduler.js +25 -7
- package/dist/store.d.ts +9 -1
- package/dist/store.js +57 -0
- package/dist/tools/loop-tools.d.ts +8 -0
- package/dist/tools/loop-tools.js +104 -12
- package/dist/tools/monitor-tools.js +1 -1
- package/dist/trigger-system.js +1 -1
- package/dist/types.d.ts +24 -1
- package/docs/USAGE_GUIDE.md +185 -0
- package/package.json +13 -4
- package/src/commands/loop-command.ts +102 -54
- package/src/index.ts +46 -16
- package/src/loop-format.ts +22 -0
- package/src/loop-parse.ts +44 -1
- package/src/loop-reducer.ts +41 -1
- package/src/monitor-manager.ts +12 -4
- package/src/notification-reducer.ts +3 -0
- package/src/runtime/monitor-ondone-runtime.ts +1 -1
- package/src/runtime/notification-runtime.ts +21 -8
- package/src/runtime/task-backlog-runtime.ts +15 -11
- package/src/scheduler.ts +25 -8
- package/src/store.ts +59 -2
- package/src/tools/loop-tools.ts +128 -12
- package/src/tools/monitor-tools.ts +1 -1
- package/src/trigger-system.ts +1 -1
- package/src/types.ts +29 -1
- package/.release-please-manifest.json +0 -3
- package/AGENTS.md +0 -123
- package/CONTRIBUTING.md +0 -79
- package/Dockerfile +0 -17
- package/benchmarks/experiment-sim.js +0 -30
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -36
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -191
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/api.ts.html +0 -193
- package/coverage/src/commands/index.html +0 -131
- package/coverage/src/commands/loop-command.ts.html +0 -634
- package/coverage/src/commands/tasks-command.ts.html +0 -490
- package/coverage/src/coordinator.ts.html +0 -430
- package/coverage/src/index.html +0 -341
- package/coverage/src/index.ts.html +0 -1249
- package/coverage/src/loop-parse.ts.html +0 -607
- package/coverage/src/loop-reducer.ts.html +0 -559
- package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
- package/coverage/src/monitor-manager.ts.html +0 -964
- package/coverage/src/monitor-reducer.ts.html +0 -583
- package/coverage/src/notification-reducer.ts.html +0 -550
- package/coverage/src/reducer-backed-store.ts.html +0 -589
- package/coverage/src/rpc/channels.ts.html +0 -376
- package/coverage/src/rpc/cross-extension-rpc.ts.html +0 -541
- package/coverage/src/rpc/index.html +0 -131
- package/coverage/src/runtime/index.html +0 -251
- package/coverage/src/runtime/loop-events.ts.html +0 -265
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
- package/coverage/src/runtime/native-task-rpc.ts.html +0 -502
- package/coverage/src/runtime/notification-runtime.ts.html +0 -721
- package/coverage/src/runtime/scope.ts.html +0 -196
- package/coverage/src/runtime/session-runtime.ts.html +0 -679
- package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -616
- package/coverage/src/runtime/task-events.ts.html +0 -208
- package/coverage/src/runtime/task-mutations.ts.html +0 -394
- package/coverage/src/runtime/task-rpc.ts.html +0 -523
- package/coverage/src/scheduler.ts.html +0 -400
- package/coverage/src/store.ts.html +0 -667
- package/coverage/src/task-backlog-coordinator.ts.html +0 -181
- package/coverage/src/task-reducer.ts.html +0 -550
- package/coverage/src/task-store.ts.html +0 -526
- package/coverage/src/tools/index.html +0 -161
- package/coverage/src/tools/loop-tools.ts.html +0 -991
- package/coverage/src/tools/monitor-tools.ts.html +0 -538
- package/coverage/src/tools/native-task-tools.ts.html +0 -493
- package/coverage/src/tools/tool-result.ts.html +0 -97
- package/coverage/src/trigger-system.ts.html +0 -547
- package/coverage/src/ui/index.html +0 -116
- package/coverage/src/ui/widget.ts.html +0 -292
- package/docker-compose.yml +0 -12
- package/docs/architecture/state-machine-migration.md +0 -502
- package/docs/architecture/state-machine-reducer-event-model.md +0 -822
- package/docs/architecture/state-machine-test-matrix.md +0 -249
- package/docs/architecture/state-machine-transition-map.md +0 -436
- package/release-please-config.json +0 -9
- package/vitest.config.ts +0 -29
package/dist/tools/loop-tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { formatTrigger } from "../loop-format.js";
|
|
2
3
|
import { parseInterval } from "../loop-parse.js";
|
|
3
4
|
import { textResult } from "./tool-result.js";
|
|
4
5
|
function validateTrigger(trigger) {
|
|
@@ -40,6 +41,67 @@ function formatRemaining(ms) {
|
|
|
40
41
|
return `${Math.round(ms / 60000)}m`;
|
|
41
42
|
return `${Math.round(ms / 3600000)}h`;
|
|
42
43
|
}
|
|
44
|
+
function parseDelayMs(input) {
|
|
45
|
+
const match = input.trim().match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
46
|
+
if (!match)
|
|
47
|
+
return undefined;
|
|
48
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
49
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
50
|
+
const multiplier = unit === "s" ? 1000 : unit === "m" ? 60000 : unit === "h" ? 3600000 : 86400000;
|
|
51
|
+
return value * multiplier;
|
|
52
|
+
}
|
|
53
|
+
function resolveNextWakeAt(nextInterval) {
|
|
54
|
+
if (!nextInterval)
|
|
55
|
+
return { nextWakeAt: undefined };
|
|
56
|
+
const parsedDelayMs = parseDelayMs(nextInterval);
|
|
57
|
+
if (!parsedDelayMs)
|
|
58
|
+
return { error: `Invalid nextInterval "${nextInterval}". Use formats like 3m, 30s, or 1h.` };
|
|
59
|
+
return { nextWakeAt: Date.now() + parsedDelayMs };
|
|
60
|
+
}
|
|
61
|
+
function formatDynamicUpdateResult(id, iteration, nextWakeAt) {
|
|
62
|
+
const mode = nextWakeAt === undefined
|
|
63
|
+
? "Next wake: when idle"
|
|
64
|
+
: `Next wake: ${formatRemaining(Math.max(0, nextWakeAt - Date.now()))}`;
|
|
65
|
+
return `Dynamic loop #${id} updated\n` +
|
|
66
|
+
`Iteration: ${iteration ?? "?"}` +
|
|
67
|
+
`\n${mode}`;
|
|
68
|
+
}
|
|
69
|
+
function formatDeletionTombstone(id, tombstone) {
|
|
70
|
+
const detail = tombstone.pendingCount === undefined ? "" : ` (pending: ${tombstone.pendingCount})`;
|
|
71
|
+
return `Loop #${id} already auto-deleted: ${tombstone.reason}${detail}`;
|
|
72
|
+
}
|
|
73
|
+
function continueDynamicLoop(params, entry, store, triggerSystem) {
|
|
74
|
+
const { nextWakeAt, error } = resolveNextWakeAt(params.nextInterval);
|
|
75
|
+
if (error)
|
|
76
|
+
return error;
|
|
77
|
+
const updated = store.updateDynamic(params.id, {
|
|
78
|
+
prompt: params.prompt,
|
|
79
|
+
dynamic: {
|
|
80
|
+
goal: params.prompt ?? entry.dynamic.goal,
|
|
81
|
+
state: params.state,
|
|
82
|
+
metrics: params.metrics,
|
|
83
|
+
doneCriteria: params.doneCriteria,
|
|
84
|
+
iteration: (entry.dynamic.iteration ?? 0) + 1,
|
|
85
|
+
nextWakeAt,
|
|
86
|
+
awaitingUpdate: false,
|
|
87
|
+
lastUpdatedAt: Date.now(),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (updated) {
|
|
91
|
+
triggerSystem.remove(params.id);
|
|
92
|
+
triggerSystem.add(updated);
|
|
93
|
+
}
|
|
94
|
+
return formatDynamicUpdateResult(params.id, updated?.dynamic?.iteration, nextWakeAt);
|
|
95
|
+
}
|
|
96
|
+
function stopDynamicLoop(params, store, triggerSystem) {
|
|
97
|
+
triggerSystem.remove(params.id);
|
|
98
|
+
if (params.status === "completed") {
|
|
99
|
+
store.delete(params.id);
|
|
100
|
+
return `Dynamic loop #${params.id} completed and deleted`;
|
|
101
|
+
}
|
|
102
|
+
store.pause(params.id);
|
|
103
|
+
return `Dynamic loop #${params.id} paused`;
|
|
104
|
+
}
|
|
43
105
|
export function registerLoopTools(options) {
|
|
44
106
|
const { pi, getStore, getTriggerSystem, getScheduler, getMonitorManager, updateWidget, maybeBootstrapTaskLoop, isTaskSystemReady, } = options;
|
|
45
107
|
pi.registerTool({
|
|
@@ -160,11 +222,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
160
222
|
}
|
|
161
223
|
const bootstrapped = await maybeBootstrapTaskLoop(entry);
|
|
162
224
|
updateWidget();
|
|
163
|
-
const triggerDesc = trigger
|
|
164
|
-
? `schedule: ${trigger.schedule}`
|
|
165
|
-
: trigger.type === "event"
|
|
166
|
-
? `event: ${trigger.source}`
|
|
167
|
-
: `hybrid: cron ${trigger.cron} + event ${trigger.event.source}`;
|
|
225
|
+
const triggerDesc = formatTrigger(trigger, "create");
|
|
168
226
|
return Promise.resolve(textResult(`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
|
|
169
227
|
`Trigger: ${triggerDesc}\n` +
|
|
170
228
|
`Recurring: ${entry.recurring}\n` +
|
|
@@ -188,12 +246,8 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
188
246
|
return Promise.resolve(textResult("No loops configured. Use LoopCreate to set up a schedule."));
|
|
189
247
|
const lines = [];
|
|
190
248
|
for (const entry of loops) {
|
|
191
|
-
const triggerDesc = entry.trigger
|
|
192
|
-
|
|
193
|
-
: entry.trigger.type === "event"
|
|
194
|
-
? `event: ${entry.trigger.source}`
|
|
195
|
-
: `hybrid: ${entry.trigger.cron} + ${entry.trigger.event.source}`;
|
|
196
|
-
const nextFire = entry.trigger.type !== "event" ? getScheduler().nextFire(entry.id) : undefined;
|
|
249
|
+
const triggerDesc = formatTrigger(entry.trigger, "list");
|
|
250
|
+
const nextFire = entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.dynamic?.nextWakeAt !== undefined ? getScheduler().nextFire(entry.id) : undefined;
|
|
197
251
|
const statusIcon = entry.status === "active" ? "*" : entry.status === "paused" ? "-" : "x";
|
|
198
252
|
let line = `${statusIcon} #${entry.id} [${entry.status}] ${entry.prompt.slice(0, 60)}`;
|
|
199
253
|
line += ` (${triggerDesc})`;
|
|
@@ -210,6 +264,37 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
210
264
|
return Promise.resolve(textResult(lines.join("\n")));
|
|
211
265
|
},
|
|
212
266
|
});
|
|
267
|
+
pi.registerTool({
|
|
268
|
+
name: "LoopUpdate",
|
|
269
|
+
label: "LoopUpdate",
|
|
270
|
+
description: `Update progress for a dynamic loop.
|
|
271
|
+
|
|
272
|
+
Use this after a dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval, "completed" when the goal is done, or "paused" to stop temporarily.`,
|
|
273
|
+
parameters: Type.Object({
|
|
274
|
+
id: Type.String({ description: "Dynamic loop ID to update" }),
|
|
275
|
+
status: Type.String({ description: "continue, completed, or paused", enum: ["continue", "completed", "paused"] }),
|
|
276
|
+
state: Type.Optional(Type.String({ description: "Current progress/state summary" })),
|
|
277
|
+
metrics: Type.Optional(Type.String({ description: "Current metrics/check results" })),
|
|
278
|
+
doneCriteria: Type.Optional(Type.String({ description: "Definition of done for the dynamic loop" })),
|
|
279
|
+
nextInterval: Type.Optional(Type.String({ description: "When to wake next, e.g. 3m, 30s, 1h" })),
|
|
280
|
+
prompt: Type.Optional(Type.String({ description: "Optional updated goal/prompt text" })),
|
|
281
|
+
}),
|
|
282
|
+
execute(_toolCallId, params) {
|
|
283
|
+
const store = getStore();
|
|
284
|
+
const triggerSystem = getTriggerSystem();
|
|
285
|
+
const entry = store.get(params.id);
|
|
286
|
+
if (!entry)
|
|
287
|
+
return Promise.resolve(textResult(`Loop #${params.id} not found`));
|
|
288
|
+
if (entry.trigger.type !== "dynamic" || !entry.dynamic) {
|
|
289
|
+
return Promise.resolve(textResult(`Loop #${params.id} is not a dynamic loop`));
|
|
290
|
+
}
|
|
291
|
+
const message = params.status === "continue"
|
|
292
|
+
? continueDynamicLoop(params, entry, store, triggerSystem)
|
|
293
|
+
: stopDynamicLoop(params, store, triggerSystem);
|
|
294
|
+
updateWidget();
|
|
295
|
+
return Promise.resolve(textResult(message));
|
|
296
|
+
},
|
|
297
|
+
});
|
|
213
298
|
pi.registerTool({
|
|
214
299
|
name: "LoopDelete",
|
|
215
300
|
label: "LoopDelete",
|
|
@@ -224,8 +309,12 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
|
|
|
224
309
|
const { id, action } = params;
|
|
225
310
|
if (action === "pause") {
|
|
226
311
|
const entry = getStore().pause(id);
|
|
227
|
-
if (!entry)
|
|
312
|
+
if (!entry) {
|
|
313
|
+
const tombstone = getStore().getDeletionTombstone(id);
|
|
314
|
+
if (tombstone)
|
|
315
|
+
return Promise.resolve(textResult(formatDeletionTombstone(id, tombstone)));
|
|
228
316
|
return Promise.resolve(textResult(`Loop #${id} not found`));
|
|
317
|
+
}
|
|
229
318
|
getTriggerSystem().remove(id);
|
|
230
319
|
updateWidget();
|
|
231
320
|
return Promise.resolve(textResult(`Loop #${id} paused`));
|
|
@@ -235,6 +324,9 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
|
|
|
235
324
|
updateWidget();
|
|
236
325
|
if (deleted)
|
|
237
326
|
return Promise.resolve(textResult(`Loop #${id} deleted`));
|
|
327
|
+
const tombstone = getStore().getDeletionTombstone(id);
|
|
328
|
+
if (tombstone)
|
|
329
|
+
return Promise.resolve(textResult(formatDeletionTombstone(id, tombstone)));
|
|
238
330
|
return Promise.resolve(textResult(`Loop #${id} not found`));
|
|
239
331
|
},
|
|
240
332
|
});
|
|
@@ -30,7 +30,7 @@ Default to MonitorCreate for any long-running or background work:\n- Watch a CI/
|
|
|
30
30
|
|
|
31
31
|
## onDone — auto-notify on completion
|
|
32
32
|
|
|
33
|
-
Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits. The completion wake
|
|
33
|
+
Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits, fails, or times out. The completion wake lets the agent inspect the final status and buffered output.\n\nExample: MonitorCreate command="python train.py" onDone="Check training results and report best loss"\nExample: MonitorCreate command="hut builds show 1769753" onDone="Analyze the build result and report status"`,
|
|
34
34
|
promptGuidelines: [
|
|
35
35
|
"Default to MonitorCreate for any long-running or background command — releases the agent to keep working on other tasks in parallel.",
|
|
36
36
|
"When the user asks to monitor CI builds, watch a build, check a remote job, or run an experiment, use MonitorCreate instead of inline bash/curl/wait.",
|
package/dist/trigger-system.js
CHANGED
|
@@ -28,7 +28,7 @@ export class TriggerSystem {
|
|
|
28
28
|
this.hybridTimers.clear();
|
|
29
29
|
}
|
|
30
30
|
add(entry) {
|
|
31
|
-
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
|
|
31
|
+
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.trigger.type === "dynamic") {
|
|
32
32
|
this.scheduler.add(entry);
|
|
33
33
|
}
|
|
34
34
|
if (entry.trigger.type === "event" || entry.trigger.type === "hybrid") {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type LoopDeletionReason = "task_backlog_empty";
|
|
2
|
+
export interface LoopDeletionTombstone {
|
|
3
|
+
id: string;
|
|
4
|
+
reason: LoopDeletionReason;
|
|
5
|
+
deletedAt: number;
|
|
6
|
+
prompt: string;
|
|
7
|
+
pendingCount?: number;
|
|
8
|
+
}
|
|
9
|
+
export type LoopDeletionTombstoneInput = Omit<LoopDeletionTombstone, "id" | "deletedAt" | "prompt">;
|
|
1
10
|
export type LoopStatus = "active" | "paused";
|
|
2
11
|
export interface CronTrigger {
|
|
3
12
|
type: "cron";
|
|
@@ -17,7 +26,20 @@ export interface HybridTrigger {
|
|
|
17
26
|
};
|
|
18
27
|
debounceMs: number;
|
|
19
28
|
}
|
|
20
|
-
export
|
|
29
|
+
export interface DynamicTrigger {
|
|
30
|
+
type: "dynamic";
|
|
31
|
+
}
|
|
32
|
+
export type Trigger = CronTrigger | EventTrigger | HybridTrigger | DynamicTrigger;
|
|
33
|
+
export interface DynamicLoopState {
|
|
34
|
+
goal: string;
|
|
35
|
+
state?: string;
|
|
36
|
+
metrics?: string;
|
|
37
|
+
doneCriteria?: string;
|
|
38
|
+
iteration: number;
|
|
39
|
+
nextWakeAt?: number;
|
|
40
|
+
awaitingUpdate?: boolean;
|
|
41
|
+
lastUpdatedAt?: number;
|
|
42
|
+
}
|
|
21
43
|
export interface LoopEntry {
|
|
22
44
|
id: string;
|
|
23
45
|
prompt: string;
|
|
@@ -32,6 +54,7 @@ export interface LoopEntry {
|
|
|
32
54
|
readOnly?: boolean;
|
|
33
55
|
maxFires?: number;
|
|
34
56
|
fireCount?: number;
|
|
57
|
+
dynamic?: DynamicLoopState;
|
|
35
58
|
}
|
|
36
59
|
export interface LoopStoreData {
|
|
37
60
|
nextId: number;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# pi-loop usage guide
|
|
2
|
+
|
|
3
|
+
`pi-loop` re-wakes a pi agent from schedules and events, runs self-paced goal loops, and monitors background commands. This guide covers the operational details; see the [README](../README.md) for installation and a quick start.
|
|
4
|
+
|
|
5
|
+
## Loops
|
|
6
|
+
|
|
7
|
+
### Scheduled loops
|
|
8
|
+
|
|
9
|
+
Use `/loop <interval> <prompt>` or `LoopCreate`:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
/loop 5m check the deploy
|
|
13
|
+
LoopCreate trigger="0 9 * * 1-5" prompt="Review weekday alerts" maxFires=10
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Intervals such as `5m`, `2h`, and `1d` are converted to cron expressions. Full five-field cron expressions are also accepted. Cron and hybrid loops track their next fire time and deliver only when the agent is idle.
|
|
17
|
+
|
|
18
|
+
Use `maxFires` for polling or other bounded work so a loop cannot run indefinitely. Recurring loops expire after seven days.
|
|
19
|
+
|
|
20
|
+
### Event loops
|
|
21
|
+
|
|
22
|
+
Event loops react to pi events instead of polling:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
/loop event tasks:created process the backlog
|
|
26
|
+
LoopCreate trigger="tool_execution_end" prompt="Review the completed tool call" triggerType="event" recurring=true
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`LoopCreate` event loops are one-shot by default. Set `recurring=true` to keep listening. Prefer events when a relevant source exists; use hybrid triggers when an event needs a scheduled safety net.
|
|
30
|
+
|
|
31
|
+
### Hybrid loops
|
|
32
|
+
|
|
33
|
+
A hybrid trigger combines cron and event delivery with a debounce window:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
LoopCreate trigger="cron:5m event:tasks:created" prompt="Process pending tasks" triggerType="hybrid" debounceMs=30000
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Dynamic goal loops
|
|
40
|
+
|
|
41
|
+
Free text passed to `/loop` creates a self-paced dynamic goal:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/loop finish the release
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The first wake is immediate when the agent is idle. After each iteration, the agent calls `LoopUpdate`:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
LoopUpdate id="1" status="continue" state="Tests pass; reviewing package"
|
|
51
|
+
LoopUpdate id="1" status="continue" nextInterval="3m"
|
|
52
|
+
LoopUpdate id="1" status="paused" state="Waiting for credentials"
|
|
53
|
+
LoopUpdate id="1" status="completed"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `continue` saves progress and wakes again when idle.
|
|
57
|
+
- `continue` with `nextInterval` schedules a timed wake.
|
|
58
|
+
- `paused` preserves progress without firing.
|
|
59
|
+
- `completed` finishes and deletes the loop.
|
|
60
|
+
|
|
61
|
+
Paused dynamic loops can be resumed from the `/loop` menu. If an in-memory wake is lost during a restart or session switch, persisted dynamic state recovers it.
|
|
62
|
+
|
|
63
|
+
### Inspecting and stopping loops
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
LoopList
|
|
67
|
+
LoopDelete id="1" action="pause"
|
|
68
|
+
LoopDelete id="1" action="delete"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`LoopDelete` defaults to `action="delete"`.
|
|
72
|
+
|
|
73
|
+
## Background monitors
|
|
74
|
+
|
|
75
|
+
`MonitorCreate` runs a shell command without blocking the agent:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
MonitorCreate command="npm test" description="Run test suite" onDone="Inspect the result and fix failures"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Output is buffered and emitted as `monitor:output`. A monitor finishes as one of:
|
|
82
|
+
|
|
83
|
+
- clean exit: emits `monitor:done`
|
|
84
|
+
- nonzero exit or spawn failure: emits `monitor:error`
|
|
85
|
+
- timeout: stops the process, emits `monitor:error`, and reports the timeout
|
|
86
|
+
- explicit `MonitorStop`: cancels the monitor without an `onDone` wake
|
|
87
|
+
|
|
88
|
+
Pass `onDone` whenever the agent should resume work after completion. Its one-shot wake fires on success, failure, or timeout. The default timeout is five minutes; use `timeout=0` to disable it.
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
MonitorList
|
|
92
|
+
MonitorStop monitorId="1"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`MonitorList` includes status, exit code when available, output count, and the last five buffered lines. Finished monitors remain briefly available before pruning.
|
|
96
|
+
|
|
97
|
+
## Task integration
|
|
98
|
+
|
|
99
|
+
### With pi-tasks
|
|
100
|
+
|
|
101
|
+
When [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks) is available, `pi-loop` uses it over the task RPC channels. Set `autoTask=true` on `LoopCreate` to create a tracked task for each fire.
|
|
102
|
+
|
|
103
|
+
### Native fallback
|
|
104
|
+
|
|
105
|
+
If `pi-tasks` does not answer during startup detection, `pi-loop` registers:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
TaskCreate subject="Fix deploy polling" description="Replace polling with an event-driven loop"
|
|
109
|
+
TaskList
|
|
110
|
+
TaskUpdate id="1" status="in_progress"
|
|
111
|
+
TaskUpdate id="1" status="completed"
|
|
112
|
+
TaskDelete id="1"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The native provider is selected for the session and exposes `/tasks`, compact status-line tracking, persisted task state, lifecycle events, and task RPC replies.
|
|
116
|
+
|
|
117
|
+
Set `taskBacklog=true` on a loop that processes existing pending tasks. Backlog workers bootstrap when tasks already exist and delete themselves when the queue drains. `autoTask` serves a different purpose: it creates a new task on each loop fire.
|
|
118
|
+
|
|
119
|
+
## Events
|
|
120
|
+
|
|
121
|
+
Monitor events:
|
|
122
|
+
|
|
123
|
+
- `monitor:output`
|
|
124
|
+
- `monitor:done`
|
|
125
|
+
- `monitor:error`
|
|
126
|
+
|
|
127
|
+
Native task lifecycle events:
|
|
128
|
+
|
|
129
|
+
- `tasks:created`
|
|
130
|
+
- `tasks:started`
|
|
131
|
+
- `tasks:completed`
|
|
132
|
+
- `tasks:reopened`
|
|
133
|
+
- `tasks:updated`
|
|
134
|
+
- `tasks:deleted`
|
|
135
|
+
- `tasks:backlog_empty`
|
|
136
|
+
- `loops:autodeleted`
|
|
137
|
+
|
|
138
|
+
Task event payloads include `previousStatus`. Transition events report the status before the transition; details-only `tasks:updated` events report the status current at edit time.
|
|
139
|
+
|
|
140
|
+
## Cross-extension task RPC
|
|
141
|
+
|
|
142
|
+
External consumers should import only from `@trevonistrevon/pi-loop/api`; deep `src/` imports are blocked by the package export map.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { TASKS_RPC, rpcCall } from "@trevonistrevon/pi-loop/api";
|
|
146
|
+
|
|
147
|
+
const { id, task } = await rpcCall(pi.events, TASKS_RPC.create, {
|
|
148
|
+
subject: "Fix deploy polling",
|
|
149
|
+
description: "Replace polling with an event-driven loop",
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`rpcCall` rejects on failure or timeout. The native provider supports:
|
|
154
|
+
|
|
155
|
+
| Channel | Request | Reply |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `tasks:rpc:ping` | `{}` | `{ version, provider }` |
|
|
158
|
+
| `tasks:rpc:pending` | `{}` | `{ pending }` |
|
|
159
|
+
| `tasks:rpc:create` | `{ subject, description, metadata? }` | `{ id, task }` |
|
|
160
|
+
| `tasks:rpc:clean` | `{}` | `{ pruned }` |
|
|
161
|
+
| `tasks:rpc:update` | `{ id, status?, subject?, description? }` | `{ task }` |
|
|
162
|
+
|
|
163
|
+
Requests include `requestId`; replies arrive on `<channel>:reply:<requestId>` as `{ success: true, data }` or `{ success: false, error }`.
|
|
164
|
+
|
|
165
|
+
## Persistence and configuration
|
|
166
|
+
|
|
167
|
+
| Variable | Effect | Default |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `PI_LOOP` | Store path override; use `off` to disable persistence | derived from scope |
|
|
170
|
+
| `PI_LOOP_SCOPE` | `memory`, `session`, or `project` | `session` |
|
|
171
|
+
| `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
|
|
172
|
+
|
|
173
|
+
Scope behavior:
|
|
174
|
+
|
|
175
|
+
- `session`: persists loops and tasks per session ID while isolating concurrent sessions and worktrees
|
|
176
|
+
- `memory`: keeps all state ephemeral
|
|
177
|
+
- `project`: shares persisted automation across sessions in the repository
|
|
178
|
+
|
|
179
|
+
Session files live under `.pi/loops/` and `.pi/tasks/`. Keep `session` as the normal default; use `project` only when shared automation is intentional.
|
|
180
|
+
|
|
181
|
+
## Status line and limits
|
|
182
|
+
|
|
183
|
+
The TUI status line summarizes active loops, monitors, and native tasks. Use `LoopList`, `MonitorList`, and `/tasks` for detail. The status clears when no work is active.
|
|
184
|
+
|
|
185
|
+
The runtime allows at most 25 active loops and 25 running monitors.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trevonistrevon/pi-loop",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
|
|
5
5
|
"author": "trevonistrevon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,20 +17,29 @@
|
|
|
17
17
|
"monitor",
|
|
18
18
|
"scheduler"
|
|
19
19
|
],
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"dist/",
|
|
23
|
+
"docs/USAGE_GUIDE.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
20
28
|
"exports": {
|
|
21
29
|
".": "./src/index.ts",
|
|
22
30
|
"./api": "./src/api.ts",
|
|
23
31
|
"./package.json": "./package.json"
|
|
24
32
|
},
|
|
25
33
|
"peerDependencies": {
|
|
26
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
27
|
-
"@earendil-works/pi-tui": "^0.
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
35
|
+
"@earendil-works/pi-tui": "^0.80.10"
|
|
28
36
|
},
|
|
29
37
|
"dependencies": {
|
|
30
38
|
"typebox": "^1.1.34"
|
|
31
39
|
},
|
|
32
40
|
"scripts": {
|
|
33
|
-
"
|
|
41
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
42
|
+
"build": "npm run clean && tsc",
|
|
34
43
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
35
44
|
"test": "vitest run",
|
|
36
45
|
"test:coverage": "vitest run --coverage",
|