@trevonistrevon/pi-loop 0.5.9 → 0.6.1
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 +72 -0
- package/README.md +64 -5
- package/dist/api.d.ts +11 -0
- package/dist/api.js +10 -0
- package/dist/commands/loop-command.d.ts +9 -2
- package/dist/commands/loop-command.js +81 -51
- package/dist/commands/tasks-command.js +3 -3
- package/dist/coordinator.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +89 -19
- 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 +59 -6
- package/dist/loop-reducer.d.ts +13 -1
- package/dist/loop-reducer.js +26 -0
- package/dist/monitor-manager.js +1 -1
- package/dist/notification-reducer.d.ts +2 -0
- package/dist/reducer-backed-store.d.ts +1 -1
- package/dist/reducer-backed-store.js +1 -1
- package/dist/rpc/channels.d.ts +73 -0
- package/dist/rpc/channels.js +30 -0
- package/dist/rpc/cross-extension-rpc.d.ts +59 -0
- package/dist/rpc/cross-extension-rpc.js +117 -0
- package/dist/runtime/loop-events.d.ts +26 -0
- package/dist/runtime/loop-events.js +26 -0
- package/dist/runtime/monitor-ondone-runtime.js +1 -1
- package/dist/runtime/native-task-rpc.d.ts +22 -0
- package/dist/runtime/native-task-rpc.js +50 -0
- 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 +5 -1
- package/dist/runtime/task-backlog-runtime.js +14 -7
- package/dist/runtime/task-mutations.d.ts +39 -0
- package/dist/runtime/task-mutations.js +68 -0
- package/dist/runtime/task-rpc.d.ts +4 -0
- package/dist/runtime/task-rpc.js +40 -58
- 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 +105 -15
- package/dist/tools/monitor-tools.js +1 -3
- package/dist/tools/native-task-tools.d.ts +2 -6
- package/dist/tools/native-task-tools.js +25 -55
- package/dist/tools/tool-result.d.ts +8 -0
- package/dist/tools/tool-result.js +4 -0
- package/dist/trigger-system.js +1 -1
- package/dist/types.d.ts +24 -1
- package/package.json +15 -2
- package/src/api.ts +36 -0
- package/src/commands/loop-command.ts +100 -55
- package/src/commands/tasks-command.ts +3 -3
- package/src/coordinator.ts +1 -1
- package/src/index.ts +92 -19
- package/src/loop-format.ts +22 -0
- package/src/loop-parse.ts +62 -8
- package/src/loop-reducer.ts +41 -1
- package/src/monitor-manager.ts +1 -1
- package/src/notification-reducer.ts +3 -0
- package/src/reducer-backed-store.ts +3 -3
- package/src/rpc/channels.ts +97 -0
- package/src/rpc/cross-extension-rpc.ts +152 -0
- package/src/runtime/loop-events.ts +60 -0
- package/src/runtime/monitor-ondone-runtime.ts +1 -1
- package/src/runtime/native-task-rpc.ts +139 -0
- package/src/runtime/notification-runtime.ts +21 -8
- package/src/runtime/task-backlog-runtime.ts +25 -7
- package/src/runtime/task-mutations.ts +103 -0
- package/src/runtime/task-rpc.ts +48 -54
- package/src/scheduler.ts +25 -8
- package/src/store.ts +59 -2
- package/src/tools/loop-tools.ts +129 -16
- package/src/tools/monitor-tools.ts +1 -4
- package/src/tools/native-task-tools.ts +31 -52
- package/src/tools/tool-result.ts +4 -0
- package/src/trigger-system.ts +5 -5
- package/src/types.ts +29 -1
- package/.release-please-manifest.json +0 -3
- package/AGENTS.md +0 -83
- package/CONTRIBUTING.md +0 -69
- 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 -33
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- 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/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/goal-coordinator.ts.html +0 -259
- package/coverage/src/goal-reducer.ts.html +0 -982
- package/coverage/src/goal-store.ts.html +0 -742
- package/coverage/src/goal-verifier.ts.html +0 -808
- package/coverage/src/index.html +0 -386
- package/coverage/src/index.ts.html +0 -1111
- package/coverage/src/loop-parse.ts.html +0 -574
- 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 -940
- 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/runtime/index.html +0 -206
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
- 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 -574
- package/coverage/src/runtime/task-events.ts.html +0 -208
- package/coverage/src/runtime/task-rpc.ts.html +0 -541
- 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 -146
- package/coverage/src/tools/loop-tools.ts.html +0 -1000
- package/coverage/src/tools/monitor-tools.ts.html +0 -547
- package/coverage/src/tools/native-task-tools.ts.html +0 -556
- 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/dist/goal-coordinator.d.ts +0 -22
- package/dist/goal-coordinator.js +0 -28
- package/dist/goal-reducer.d.ts +0 -98
- package/dist/goal-reducer.js +0 -190
- package/dist/goal-store.d.ts +0 -22
- package/dist/goal-store.js +0 -188
- package/dist/goal-types.d.ts +0 -82
- package/dist/goal-types.js +0 -1
- package/dist/goal-verifier.d.ts +0 -20
- package/dist/goal-verifier.js +0 -198
- package/docker-compose.yml +0 -12
- package/docs/architecture/goal-state-schema.md +0 -505
- package/docs/architecture/state-machine-migration.md +0 -546
- package/docs/architecture/state-machine-reducer-event-model.md +0 -823
- 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/src/goal-coordinator.ts +0 -58
- package/src/goal-reducer.ts +0 -299
- package/src/goal-store.ts +0 -219
- package/src/goal-types.ts +0 -104
- package/src/goal-verifier.ts +0 -241
- package/vitest.config.ts +0 -29
package/dist/scheduler.js
CHANGED
|
@@ -4,6 +4,9 @@ function computeNextFire(entry) {
|
|
|
4
4
|
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
|
|
5
5
|
return cronToNextFire(entry.trigger.type === "hybrid" ? entry.trigger.cron : entry.trigger.schedule);
|
|
6
6
|
}
|
|
7
|
+
if (entry.trigger.type === "dynamic") {
|
|
8
|
+
return new Date(entry.dynamic?.nextWakeAt ?? Date.now());
|
|
9
|
+
}
|
|
7
10
|
return new Date(Date.now() + 60000);
|
|
8
11
|
}
|
|
9
12
|
export class CronScheduler {
|
|
@@ -15,10 +18,20 @@ export class CronScheduler {
|
|
|
15
18
|
this.onFire = onFire;
|
|
16
19
|
}
|
|
17
20
|
start() {
|
|
18
|
-
for (const
|
|
21
|
+
for (const storedEntry of this.store.list()) {
|
|
22
|
+
let entry = storedEntry;
|
|
19
23
|
if (entry.status !== "active")
|
|
20
24
|
continue;
|
|
21
|
-
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
|
|
25
|
+
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.trigger.type === "dynamic") {
|
|
26
|
+
if (entry.trigger.type === "dynamic" && entry.dynamic?.awaitingUpdate && !this.fireTimes.has(entry.id)) {
|
|
27
|
+
entry = this.store.updateDynamic(entry.id, {
|
|
28
|
+
dynamic: {
|
|
29
|
+
awaitingUpdate: false,
|
|
30
|
+
nextWakeAt: undefined,
|
|
31
|
+
lastUpdatedAt: Date.now(),
|
|
32
|
+
},
|
|
33
|
+
}) ?? entry;
|
|
34
|
+
}
|
|
22
35
|
this.armTimer(entry);
|
|
23
36
|
}
|
|
24
37
|
}
|
|
@@ -27,7 +40,7 @@ export class CronScheduler {
|
|
|
27
40
|
this.fireTimes.clear();
|
|
28
41
|
}
|
|
29
42
|
add(entry) {
|
|
30
|
-
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
|
|
43
|
+
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.trigger.type === "dynamic") {
|
|
31
44
|
this.armTimer(entry);
|
|
32
45
|
}
|
|
33
46
|
}
|
|
@@ -38,11 +51,14 @@ export class CronScheduler {
|
|
|
38
51
|
return this.fireTimes.get(id);
|
|
39
52
|
}
|
|
40
53
|
armTimer(entry) {
|
|
41
|
-
const _scheduleExpr = entry.trigger.type === "hybrid" ? entry.trigger.cron : entry.trigger.schedule;
|
|
42
54
|
const nextFire = computeNextFire(entry);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
let jitter = 0;
|
|
56
|
+
if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
|
|
57
|
+
const scheduleExpr = entry.trigger.type === "hybrid" ? entry.trigger.cron : entry.trigger.schedule;
|
|
58
|
+
const minuteField = scheduleExpr.trim().split(/\s+/)[0] ?? "";
|
|
59
|
+
const minuteStep = minuteField.startsWith("*/") ? parseInt(minuteField.slice(2), 10) || 30 : 30;
|
|
60
|
+
jitter = computeJitter(entry.id, entry.recurring, minuteStep);
|
|
61
|
+
}
|
|
46
62
|
const fireTime = nextFire.getTime() + jitter;
|
|
47
63
|
if (fireTime > entry.expiresAt) {
|
|
48
64
|
this.store.delete(entry.id);
|
|
@@ -59,6 +75,8 @@ export class CronScheduler {
|
|
|
59
75
|
this.fireTimes.delete(id);
|
|
60
76
|
continue;
|
|
61
77
|
}
|
|
78
|
+
if (entry.trigger.type === "dynamic" && entry.dynamic?.awaitingUpdate)
|
|
79
|
+
continue;
|
|
62
80
|
if (filter && !filter(entry))
|
|
63
81
|
continue;
|
|
64
82
|
if (now >= entry.expiresAt) {
|
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type LoopReducerEvent, type LoopReducerState } from "./loop-reducer.js";
|
|
2
2
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
3
|
-
import type { LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
3
|
+
import type { DynamicLoopState, LoopDeletionTombstone, LoopDeletionTombstoneInput, LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
4
4
|
export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducerState, LoopReducerEvent, LoopStoreData> {
|
|
5
|
+
private tombstones;
|
|
5
6
|
constructor(listIdOrPath?: string);
|
|
6
7
|
create(trigger: Trigger, prompt: string, opts: {
|
|
7
8
|
recurring: boolean;
|
|
@@ -9,6 +10,7 @@ export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducer
|
|
|
9
10
|
taskBacklog?: boolean;
|
|
10
11
|
readOnly?: boolean;
|
|
11
12
|
maxFires?: number;
|
|
13
|
+
dynamic?: Partial<DynamicLoopState>;
|
|
12
14
|
}): LoopEntry;
|
|
13
15
|
pause(id: string): LoopEntry | undefined;
|
|
14
16
|
resume(id: string): LoopEntry | undefined;
|
|
@@ -20,6 +22,12 @@ export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducer
|
|
|
20
22
|
entry: LoopEntry | undefined;
|
|
21
23
|
changedFields: string[];
|
|
22
24
|
};
|
|
25
|
+
updateDynamic(id: string, fields: {
|
|
26
|
+
prompt?: string;
|
|
27
|
+
dynamic: Partial<DynamicLoopState>;
|
|
28
|
+
}): LoopEntry | undefined;
|
|
29
|
+
getDeletionTombstone(id: string): LoopDeletionTombstone | undefined;
|
|
30
|
+
recordDeletionTombstone(id: string, input: LoopDeletionTombstoneInput): LoopDeletionTombstone | undefined;
|
|
23
31
|
delete(id: string): boolean;
|
|
24
32
|
clearExpired(): number;
|
|
25
33
|
expireEventLoops(sessionStartedAt: number): number;
|
package/dist/store.js
CHANGED
|
@@ -4,7 +4,9 @@ import { reduceLoopState } from "./loop-reducer.js";
|
|
|
4
4
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
5
5
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
6
6
|
const MAX_LOOPS = 25;
|
|
7
|
+
const TOMBSTONE_TTL_MS = 10 * 60 * 1000;
|
|
7
8
|
export class LoopStore extends ReducerBackedStore {
|
|
9
|
+
tombstones = new Map();
|
|
8
10
|
constructor(listIdOrPath) {
|
|
9
11
|
super({
|
|
10
12
|
baseDir: LOOPS_DIR,
|
|
@@ -34,6 +36,7 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
34
36
|
taskBacklog: opts.taskBacklog,
|
|
35
37
|
readOnly: opts.readOnly,
|
|
36
38
|
maxFires: opts.maxFires,
|
|
39
|
+
dynamic: opts.dynamic,
|
|
37
40
|
},
|
|
38
41
|
});
|
|
39
42
|
return this.entries.get(String(this.nextId - 1));
|
|
@@ -68,6 +71,22 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
68
71
|
entityId: id,
|
|
69
72
|
payload: { id },
|
|
70
73
|
});
|
|
74
|
+
if (entry.trigger.type === "dynamic" && entry.dynamic?.awaitingUpdate) {
|
|
75
|
+
this.applyReducerEvent({
|
|
76
|
+
type: "LOOP_DYNAMIC_UPDATED",
|
|
77
|
+
at: Date.now(),
|
|
78
|
+
source: "tool",
|
|
79
|
+
entityType: "loop",
|
|
80
|
+
entityId: id,
|
|
81
|
+
payload: {
|
|
82
|
+
id,
|
|
83
|
+
dynamic: {
|
|
84
|
+
awaitingUpdate: false,
|
|
85
|
+
lastUpdatedAt: Date.now(),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
71
90
|
return this.entries.get(id);
|
|
72
91
|
});
|
|
73
92
|
}
|
|
@@ -108,6 +127,44 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
108
127
|
return { entry: this.entries.get(id), changedFields };
|
|
109
128
|
});
|
|
110
129
|
}
|
|
130
|
+
updateDynamic(id, fields) {
|
|
131
|
+
return this.withLock(() => {
|
|
132
|
+
if (!this.entries.has(id))
|
|
133
|
+
return undefined;
|
|
134
|
+
this.applyReducerEvent({
|
|
135
|
+
type: "LOOP_DYNAMIC_UPDATED",
|
|
136
|
+
at: Date.now(),
|
|
137
|
+
source: "tool",
|
|
138
|
+
entityType: "loop",
|
|
139
|
+
entityId: id,
|
|
140
|
+
payload: { id, prompt: fields.prompt, dynamic: fields.dynamic },
|
|
141
|
+
});
|
|
142
|
+
return this.entries.get(id);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
getDeletionTombstone(id) {
|
|
146
|
+
const tombstone = this.tombstones.get(id);
|
|
147
|
+
if (!tombstone)
|
|
148
|
+
return undefined;
|
|
149
|
+
if (Date.now() - tombstone.deletedAt <= TOMBSTONE_TTL_MS)
|
|
150
|
+
return tombstone;
|
|
151
|
+
this.tombstones.delete(id);
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
recordDeletionTombstone(id, input) {
|
|
155
|
+
const entry = this.entries.get(id);
|
|
156
|
+
if (!entry)
|
|
157
|
+
return undefined;
|
|
158
|
+
const tombstone = {
|
|
159
|
+
id,
|
|
160
|
+
reason: input.reason,
|
|
161
|
+
pendingCount: input.pendingCount,
|
|
162
|
+
deletedAt: Date.now(),
|
|
163
|
+
prompt: entry.prompt,
|
|
164
|
+
};
|
|
165
|
+
this.tombstones.set(id, tombstone);
|
|
166
|
+
return tombstone;
|
|
167
|
+
}
|
|
111
168
|
delete(id) {
|
|
112
169
|
return this.withLock(() => {
|
|
113
170
|
if (!this.entries.has(id))
|
|
@@ -11,6 +11,14 @@ interface LoopStoreLike {
|
|
|
11
11
|
maxFires?: number;
|
|
12
12
|
}): LoopEntry;
|
|
13
13
|
pause(id: string): LoopEntry | undefined;
|
|
14
|
+
updateDynamic(id: string, fields: {
|
|
15
|
+
prompt?: string;
|
|
16
|
+
dynamic: Partial<NonNullable<LoopEntry["dynamic"]>>;
|
|
17
|
+
}): LoopEntry | undefined;
|
|
18
|
+
getDeletionTombstone(id: string): {
|
|
19
|
+
reason: string;
|
|
20
|
+
pendingCount?: number;
|
|
21
|
+
} | undefined;
|
|
14
22
|
delete(id: string): boolean;
|
|
15
23
|
}
|
|
16
24
|
interface TriggerSystemLike {
|
package/dist/tools/loop-tools.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { formatTrigger } from "../loop-format.js";
|
|
2
3
|
import { parseInterval } from "../loop-parse.js";
|
|
3
|
-
|
|
4
|
-
return { content: [{ type: "text", text: msg }], details: undefined };
|
|
5
|
-
}
|
|
4
|
+
import { textResult } from "./tool-result.js";
|
|
6
5
|
function validateTrigger(trigger) {
|
|
7
6
|
if (trigger.type === "cron") {
|
|
8
7
|
const parts = trigger.schedule.trim().split(/\s+/);
|
|
@@ -42,6 +41,67 @@ function formatRemaining(ms) {
|
|
|
42
41
|
return `${Math.round(ms / 60000)}m`;
|
|
43
42
|
return `${Math.round(ms / 3600000)}h`;
|
|
44
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
|
+
}
|
|
45
105
|
export function registerLoopTools(options) {
|
|
46
106
|
const { pi, getStore, getTriggerSystem, getScheduler, getMonitorManager, updateWidget, maybeBootstrapTaskLoop, isTaskSystemReady, } = options;
|
|
47
107
|
pi.registerTool({
|
|
@@ -162,11 +222,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
162
222
|
}
|
|
163
223
|
const bootstrapped = await maybeBootstrapTaskLoop(entry);
|
|
164
224
|
updateWidget();
|
|
165
|
-
const triggerDesc = trigger
|
|
166
|
-
? `schedule: ${trigger.schedule}`
|
|
167
|
-
: trigger.type === "event"
|
|
168
|
-
? `event: ${trigger.source}`
|
|
169
|
-
: `hybrid: cron ${trigger.cron} + event ${trigger.event.source}`;
|
|
225
|
+
const triggerDesc = formatTrigger(trigger, "create");
|
|
170
226
|
return Promise.resolve(textResult(`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
|
|
171
227
|
`Trigger: ${triggerDesc}\n` +
|
|
172
228
|
`Recurring: ${entry.recurring}\n` +
|
|
@@ -190,12 +246,8 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
190
246
|
return Promise.resolve(textResult("No loops configured. Use LoopCreate to set up a schedule."));
|
|
191
247
|
const lines = [];
|
|
192
248
|
for (const entry of loops) {
|
|
193
|
-
const triggerDesc = entry.trigger
|
|
194
|
-
|
|
195
|
-
: entry.trigger.type === "event"
|
|
196
|
-
? `event: ${entry.trigger.source}`
|
|
197
|
-
: `hybrid: ${entry.trigger.cron} + ${entry.trigger.event.source}`;
|
|
198
|
-
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;
|
|
199
251
|
const statusIcon = entry.status === "active" ? "*" : entry.status === "paused" ? "-" : "x";
|
|
200
252
|
let line = `${statusIcon} #${entry.id} [${entry.status}] ${entry.prompt.slice(0, 60)}`;
|
|
201
253
|
line += ` (${triggerDesc})`;
|
|
@@ -212,6 +264,37 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
212
264
|
return Promise.resolve(textResult(lines.join("\n")));
|
|
213
265
|
},
|
|
214
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
|
+
});
|
|
215
298
|
pi.registerTool({
|
|
216
299
|
name: "LoopDelete",
|
|
217
300
|
label: "LoopDelete",
|
|
@@ -226,8 +309,12 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
|
|
|
226
309
|
const { id, action } = params;
|
|
227
310
|
if (action === "pause") {
|
|
228
311
|
const entry = getStore().pause(id);
|
|
229
|
-
if (!entry)
|
|
312
|
+
if (!entry) {
|
|
313
|
+
const tombstone = getStore().getDeletionTombstone(id);
|
|
314
|
+
if (tombstone)
|
|
315
|
+
return Promise.resolve(textResult(formatDeletionTombstone(id, tombstone)));
|
|
230
316
|
return Promise.resolve(textResult(`Loop #${id} not found`));
|
|
317
|
+
}
|
|
231
318
|
getTriggerSystem().remove(id);
|
|
232
319
|
updateWidget();
|
|
233
320
|
return Promise.resolve(textResult(`Loop #${id} paused`));
|
|
@@ -237,6 +324,9 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
|
|
|
237
324
|
updateWidget();
|
|
238
325
|
if (deleted)
|
|
239
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)));
|
|
240
330
|
return Promise.resolve(textResult(`Loop #${id} not found`));
|
|
241
331
|
},
|
|
242
332
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
|
|
3
|
-
return { content: [{ type: "text", text: msg }], details: undefined };
|
|
4
|
-
}
|
|
2
|
+
import { textResult } from "./tool-result.js";
|
|
5
3
|
function formatRemaining(ms) {
|
|
6
4
|
if (ms < 60000)
|
|
7
5
|
return `${Math.round(ms / 1000)}s`;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type TaskBacklogResult } from "../runtime/task-mutations.js";
|
|
2
3
|
import { TaskStore } from "../task-store.js";
|
|
3
|
-
export
|
|
4
|
-
created: boolean;
|
|
5
|
-
entry?: {
|
|
6
|
-
id: string;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
4
|
+
export type { TaskBacklogResult };
|
|
9
5
|
export interface NativeTaskToolsOptions {
|
|
10
6
|
pi: ExtensionAPI;
|
|
11
7
|
taskStore: TaskStore;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { createTask, deleteTask, updateTask, } from "../runtime/task-mutations.js";
|
|
3
|
+
import { textResult } from "./tool-result.js";
|
|
4
|
+
function backlogSuffix(backlog) {
|
|
5
|
+
return backlog.created && backlog.entry
|
|
6
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
7
|
+
: "";
|
|
5
8
|
}
|
|
6
9
|
export function registerNativeTaskTools(options) {
|
|
7
10
|
const { pi, taskStore, evaluateTaskBacklog, updateWidget } = options;
|
|
11
|
+
const mutationCtx = { pi, taskStore, evaluateTaskBacklog, updateWidget };
|
|
8
12
|
pi.registerTool({
|
|
9
13
|
name: "TaskCreate",
|
|
10
14
|
label: "TaskCreate",
|
|
@@ -30,14 +34,11 @@ Fields:
|
|
|
30
34
|
description: Type.String({ description: "Detailed description of what needs to be done" }),
|
|
31
35
|
}),
|
|
32
36
|
async execute(_toolCallId, params) {
|
|
33
|
-
const entry =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
39
|
-
: "";
|
|
40
|
-
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
37
|
+
const { entry, backlog } = await createTask(mutationCtx, {
|
|
38
|
+
subject: params.subject,
|
|
39
|
+
description: params.description,
|
|
40
|
+
});
|
|
41
|
+
return textResult(`Task #${entry.id} created: ${entry.subject}${backlogSuffix(backlog)}`);
|
|
41
42
|
},
|
|
42
43
|
});
|
|
43
44
|
pi.registerTool({
|
|
@@ -84,41 +85,16 @@ Parameters: id (required), status, subject, description`,
|
|
|
84
85
|
}),
|
|
85
86
|
async execute(_toolCallId, params) {
|
|
86
87
|
const { id, status, subject, description } = params;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
else if (status === "completed") {
|
|
97
|
-
entry = taskStore.complete(id);
|
|
98
|
-
if (entry)
|
|
99
|
-
emitNativeTaskEvent(pi, "tasks:completed", entry, previousStatus);
|
|
100
|
-
}
|
|
101
|
-
else if (status === "pending") {
|
|
102
|
-
entry = taskStore.reopen(id);
|
|
103
|
-
if (entry)
|
|
104
|
-
emitNativeTaskEvent(pi, "tasks:reopened", entry, previousStatus);
|
|
105
|
-
}
|
|
106
|
-
if (!entry)
|
|
107
|
-
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
108
|
-
if (subject !== undefined || description !== undefined) {
|
|
109
|
-
entry = taskStore.updateDetails(id, { subject, description });
|
|
110
|
-
if (entry)
|
|
111
|
-
emitNativeTaskEvent(pi, "tasks:updated", entry, previousStatus);
|
|
112
|
-
}
|
|
113
|
-
if (!entry)
|
|
114
|
-
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
115
|
-
updateWidget();
|
|
116
|
-
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
88
|
+
const result = await updateTask(mutationCtx, {
|
|
89
|
+
id,
|
|
90
|
+
status: status,
|
|
91
|
+
subject,
|
|
92
|
+
description,
|
|
93
|
+
});
|
|
94
|
+
if (!result)
|
|
95
|
+
return textResult(`Task #${id} not found`);
|
|
117
96
|
const statusMsg = status ? ` → ${status}` : "";
|
|
118
|
-
|
|
119
|
-
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
120
|
-
: "";
|
|
121
|
-
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
97
|
+
return textResult(`Task #${id} updated${statusMsg}${backlogSuffix(result.backlog)}`);
|
|
122
98
|
},
|
|
123
99
|
});
|
|
124
100
|
pi.registerTool({
|
|
@@ -129,16 +105,10 @@ Parameters: id (required), status, subject, description`,
|
|
|
129
105
|
id: Type.String({ description: "Task ID to delete" }),
|
|
130
106
|
}),
|
|
131
107
|
async execute(_toolCallId, params) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (existing)
|
|
137
|
-
emitNativeTaskEvent(pi, "tasks:deleted", existing, existing.status);
|
|
138
|
-
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
139
|
-
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
140
|
-
}
|
|
141
|
-
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
108
|
+
const result = await deleteTask(mutationCtx, params.id);
|
|
109
|
+
if (!result)
|
|
110
|
+
return textResult(`Task #${params.id} not found`);
|
|
111
|
+
return textResult(`Task #${params.id} deleted`);
|
|
142
112
|
},
|
|
143
113
|
});
|
|
144
114
|
}
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trevonistrevon/pi-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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,6 +17,18 @@
|
|
|
17
17
|
"monitor",
|
|
18
18
|
"scheduler"
|
|
19
19
|
],
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"dist/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts",
|
|
29
|
+
"./api": "./src/api.ts",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
20
32
|
"peerDependencies": {
|
|
21
33
|
"@earendil-works/pi-coding-agent": "^0.75.5",
|
|
22
34
|
"@earendil-works/pi-tui": "^0.75.5"
|
|
@@ -25,7 +37,8 @@
|
|
|
25
37
|
"typebox": "^1.1.34"
|
|
26
38
|
},
|
|
27
39
|
"scripts": {
|
|
28
|
-
"
|
|
40
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
41
|
+
"build": "npm run clean && tsc",
|
|
29
42
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
30
43
|
"test": "vitest run",
|
|
31
44
|
"test:coverage": "vitest run --coverage",
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API surface for sibling extensions (imported as
|
|
3
|
+
* `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
|
|
4
|
+
* the package `exports` map deliberately blocks deep imports.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
type CleanReply,
|
|
8
|
+
type CreateTaskParams,
|
|
9
|
+
type CreateTaskReply,
|
|
10
|
+
type PendingReply,
|
|
11
|
+
type PingReply,
|
|
12
|
+
replyChannel,
|
|
13
|
+
type SpawnParams,
|
|
14
|
+
type SpawnReply,
|
|
15
|
+
SUBAGENTS_RPC,
|
|
16
|
+
TASK_EVENTS,
|
|
17
|
+
TASKS_RPC,
|
|
18
|
+
type TaskEntryWire,
|
|
19
|
+
type TaskStatusWire,
|
|
20
|
+
type UpdateTaskParams,
|
|
21
|
+
type UpdateTaskReply,
|
|
22
|
+
} from "./rpc/channels.js";
|
|
23
|
+
export {
|
|
24
|
+
type HandleRpcOptions,
|
|
25
|
+
handleRpc,
|
|
26
|
+
PROTOCOL_VERSION,
|
|
27
|
+
RpcError,
|
|
28
|
+
type RpcEventBus,
|
|
29
|
+
type RpcReply,
|
|
30
|
+
rpcCall,
|
|
31
|
+
rpcProbe,
|
|
32
|
+
} from "./rpc/cross-extension-rpc.js";
|
|
33
|
+
export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
|
|
34
|
+
export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
35
|
+
export { TaskStore } from "./task-store.js";
|
|
36
|
+
export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
|