@trevonistrevon/pi-loop 0.5.0 → 0.5.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/README.md +11 -10
- package/dist/commands/loop-command.d.ts +22 -0
- package/dist/commands/loop-command.js +148 -0
- package/dist/commands/tasks-command.d.ts +15 -0
- package/dist/commands/tasks-command.js +117 -0
- package/dist/goal-coordinator.d.ts +22 -0
- package/dist/goal-coordinator.js +28 -0
- package/dist/goal-reducer.d.ts +89 -0
- package/dist/goal-reducer.js +181 -0
- package/dist/goal-store.d.ts +31 -0
- package/dist/goal-store.js +298 -0
- package/dist/goal-types.d.ts +4 -0
- package/dist/index.js +125 -1212
- package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
- package/dist/runtime/monitor-ondone-runtime.js +49 -0
- package/dist/runtime/notification-runtime.d.ts +34 -0
- package/dist/runtime/notification-runtime.js +152 -0
- package/dist/runtime/scope.d.ts +8 -0
- package/dist/runtime/scope.js +33 -0
- package/dist/runtime/session-runtime.d.ts +39 -0
- package/dist/runtime/session-runtime.js +110 -0
- package/dist/runtime/task-backlog-runtime.d.ts +36 -0
- package/dist/runtime/task-backlog-runtime.js +105 -0
- package/dist/runtime/task-rpc.d.ts +19 -0
- package/dist/runtime/task-rpc.js +118 -0
- package/dist/store.d.ts +5 -4
- package/dist/store.js +50 -44
- package/dist/task-store.d.ts +6 -4
- package/dist/task-store.js +64 -47
- package/dist/tools/loop-tools.d.ts +41 -0
- package/dist/tools/loop-tools.js +241 -0
- package/dist/tools/monitor-tools.d.ts +25 -0
- package/dist/tools/monitor-tools.js +110 -0
- package/dist/tools/native-task-tools.d.ts +15 -0
- package/dist/tools/native-task-tools.js +127 -0
- package/package.json +1 -1
- package/src/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -0
- package/src/goal-coordinator.ts +58 -0
- package/src/goal-reducer.ts +280 -0
- package/src/goal-store.ts +315 -0
- package/src/goal-types.ts +5 -0
- package/src/index.ts +129 -1299
- package/src/runtime/monitor-ondone-runtime.ts +75 -0
- package/src/runtime/notification-runtime.ts +212 -0
- package/src/runtime/scope.ts +37 -0
- package/src/runtime/session-runtime.ts +168 -0
- package/src/runtime/task-backlog-runtime.ts +163 -0
- package/src/runtime/task-rpc.ts +150 -0
- package/src/store.ts +51 -45
- package/src/task-store.ts +61 -46
- package/src/tools/loop-tools.ts +304 -0
- package/src/tools/monitor-tools.ts +145 -0
- package/src/tools/native-task-tools.ts +144 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function createTaskRuntimeBridge(options) {
|
|
3
|
+
const { pi, isTasksAvailable, setTasksAvailable, getNativeTaskStore, onNativeTaskCreated, onNativeTasksPruned, debug, } = options;
|
|
4
|
+
function checkTasksVersion() {
|
|
5
|
+
const requestId = randomUUID();
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
unsub();
|
|
8
|
+
}, 5000);
|
|
9
|
+
const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw) => {
|
|
10
|
+
unsub();
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
const remoteVersion = raw?.data?.version;
|
|
13
|
+
if (remoteVersion !== undefined)
|
|
14
|
+
setTasksAvailable(true);
|
|
15
|
+
});
|
|
16
|
+
pi.events.emit("tasks:rpc:ping", { requestId });
|
|
17
|
+
}
|
|
18
|
+
async function autoCreateTask(entry) {
|
|
19
|
+
if (!entry.autoTask)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (isTasksAvailable()) {
|
|
22
|
+
try {
|
|
23
|
+
const requestId = randomUUID();
|
|
24
|
+
const taskId = await new Promise((resolve) => {
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
unsub();
|
|
27
|
+
resolve(undefined);
|
|
28
|
+
}, 5000);
|
|
29
|
+
const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw) => {
|
|
30
|
+
unsub();
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
const reply = raw;
|
|
33
|
+
if (reply.success && reply.data)
|
|
34
|
+
resolve(reply.data.id);
|
|
35
|
+
else
|
|
36
|
+
resolve(undefined);
|
|
37
|
+
});
|
|
38
|
+
pi.events.emit("tasks:rpc:create", {
|
|
39
|
+
requestId,
|
|
40
|
+
subject: entry.prompt.slice(0, 80),
|
|
41
|
+
description: `Auto-created from loop #${entry.id}`,
|
|
42
|
+
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return taskId;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
52
|
+
if (!nativeTaskStore)
|
|
53
|
+
return undefined;
|
|
54
|
+
const task = nativeTaskStore.create(entry.prompt.slice(0, 80), `Auto-created from loop #${entry.id}`, { loopId: entry.id, trigger: entry.trigger });
|
|
55
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
56
|
+
return task.id;
|
|
57
|
+
}
|
|
58
|
+
async function hasPendingTasks() {
|
|
59
|
+
if (isTasksAvailable()) {
|
|
60
|
+
try {
|
|
61
|
+
const requestId = randomUUID();
|
|
62
|
+
const count = await new Promise((resolve) => {
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
unsub();
|
|
65
|
+
resolve(-1);
|
|
66
|
+
}, 3000);
|
|
67
|
+
const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw) => {
|
|
68
|
+
unsub();
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
const reply = raw;
|
|
71
|
+
resolve(reply.success && reply.data ? reply.data.pending : -1);
|
|
72
|
+
});
|
|
73
|
+
pi.events.emit("tasks:rpc:pending", { requestId });
|
|
74
|
+
});
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return getNativeTaskStore()?.pendingCount() ?? -1;
|
|
82
|
+
}
|
|
83
|
+
async function cleanDoneTasks() {
|
|
84
|
+
if (isTasksAvailable()) {
|
|
85
|
+
try {
|
|
86
|
+
const requestId = randomUUID();
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
unsub();
|
|
90
|
+
resolve();
|
|
91
|
+
}, 3000);
|
|
92
|
+
const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
|
|
93
|
+
unsub();
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
debug?.("tasks:rpc:clean — done tasks swept");
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
pi.events.emit("tasks:rpc:clean", { requestId });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// timeout or error, ignore
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
107
|
+
if (!nativeTaskStore)
|
|
108
|
+
return;
|
|
109
|
+
nativeTaskStore.pruneCompleted();
|
|
110
|
+
await onNativeTasksPruned?.(nativeTaskStore);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
checkTasksVersion,
|
|
114
|
+
autoCreateTask,
|
|
115
|
+
hasPendingTasks,
|
|
116
|
+
cleanDoneTasks,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopEntry,
|
|
1
|
+
import type { LoopEntry, Trigger } from "./types.js";
|
|
2
2
|
export declare class LoopStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
@@ -19,11 +19,12 @@ export declare class LoopStore {
|
|
|
19
19
|
}): LoopEntry;
|
|
20
20
|
get(id: string): LoopEntry | undefined;
|
|
21
21
|
list(): LoopEntry[];
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
pause(id: string): LoopEntry | undefined;
|
|
23
|
+
resume(id: string): LoopEntry | undefined;
|
|
24
|
+
fire(id: string): LoopEntry | undefined;
|
|
25
|
+
updateMetadata(id: string, fields: {
|
|
24
26
|
trigger?: Trigger;
|
|
25
27
|
prompt?: string;
|
|
26
|
-
fireCount?: number;
|
|
27
28
|
}): {
|
|
28
29
|
entry: LoopEntry | undefined;
|
|
29
30
|
changedFields: string[];
|
package/dist/store.js
CHANGED
|
@@ -149,38 +149,61 @@ export class LoopStore {
|
|
|
149
149
|
this.load();
|
|
150
150
|
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
pause(id) {
|
|
153
153
|
return this.withLock(() => {
|
|
154
154
|
const entry = this.loops.get(id);
|
|
155
155
|
if (!entry)
|
|
156
|
+
return undefined;
|
|
157
|
+
this.applyReducerEvent({
|
|
158
|
+
type: "LOOP_PAUSED",
|
|
159
|
+
at: Date.now(),
|
|
160
|
+
source: "tool",
|
|
161
|
+
entityType: "loop",
|
|
162
|
+
entityId: id,
|
|
163
|
+
payload: { id },
|
|
164
|
+
});
|
|
165
|
+
return this.loops.get(id);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
resume(id) {
|
|
169
|
+
return this.withLock(() => {
|
|
170
|
+
const entry = this.loops.get(id);
|
|
171
|
+
if (!entry)
|
|
172
|
+
return undefined;
|
|
173
|
+
this.applyReducerEvent({
|
|
174
|
+
type: "LOOP_RESUMED",
|
|
175
|
+
at: Date.now(),
|
|
176
|
+
source: "tool",
|
|
177
|
+
entityType: "loop",
|
|
178
|
+
entityId: id,
|
|
179
|
+
payload: { id },
|
|
180
|
+
});
|
|
181
|
+
return this.loops.get(id);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
fire(id) {
|
|
185
|
+
return this.withLock(() => {
|
|
186
|
+
const entry = this.loops.get(id);
|
|
187
|
+
if (!entry)
|
|
188
|
+
return undefined;
|
|
189
|
+
this.applyReducerEvent({
|
|
190
|
+
type: "LOOP_FIRED",
|
|
191
|
+
at: Date.now(),
|
|
192
|
+
source: "system",
|
|
193
|
+
entityType: "loop",
|
|
194
|
+
entityId: id,
|
|
195
|
+
payload: { id },
|
|
196
|
+
});
|
|
197
|
+
return this.loops.get(id);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
updateMetadata(id, fields) {
|
|
201
|
+
return this.withLock(() => {
|
|
202
|
+
const current = this.loops.get(id);
|
|
203
|
+
if (!current)
|
|
156
204
|
return { entry: undefined, changedFields: [] };
|
|
157
205
|
const changedFields = [];
|
|
158
206
|
const now = Date.now();
|
|
159
|
-
if (fields.status === "paused") {
|
|
160
|
-
this.applyReducerEvent({
|
|
161
|
-
type: "LOOP_PAUSED",
|
|
162
|
-
at: now,
|
|
163
|
-
source: "tool",
|
|
164
|
-
entityType: "loop",
|
|
165
|
-
entityId: id,
|
|
166
|
-
payload: { id },
|
|
167
|
-
});
|
|
168
|
-
changedFields.push("status");
|
|
169
|
-
}
|
|
170
|
-
else if (fields.status === "active") {
|
|
171
|
-
this.applyReducerEvent({
|
|
172
|
-
type: "LOOP_RESUMED",
|
|
173
|
-
at: now,
|
|
174
|
-
source: "tool",
|
|
175
|
-
entityType: "loop",
|
|
176
|
-
entityId: id,
|
|
177
|
-
payload: { id },
|
|
178
|
-
});
|
|
179
|
-
changedFields.push("status");
|
|
180
|
-
}
|
|
181
|
-
const current = this.loops.get(id);
|
|
182
|
-
if (!current)
|
|
183
|
-
return { entry: undefined, changedFields };
|
|
184
207
|
if (fields.trigger !== undefined) {
|
|
185
208
|
current.trigger = fields.trigger;
|
|
186
209
|
changedFields.push("trigger");
|
|
@@ -189,24 +212,7 @@ export class LoopStore {
|
|
|
189
212
|
current.prompt = fields.prompt;
|
|
190
213
|
changedFields.push("prompt");
|
|
191
214
|
}
|
|
192
|
-
if (
|
|
193
|
-
if (fields.fireCount === (current.fireCount ?? 0) + 1) {
|
|
194
|
-
this.applyReducerEvent({
|
|
195
|
-
type: "LOOP_FIRED",
|
|
196
|
-
at: now,
|
|
197
|
-
source: "system",
|
|
198
|
-
entityType: "loop",
|
|
199
|
-
entityId: id,
|
|
200
|
-
payload: { id },
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
current.fireCount = fields.fireCount;
|
|
205
|
-
current.updatedAt = now;
|
|
206
|
-
}
|
|
207
|
-
changedFields.push("fireCount");
|
|
208
|
-
}
|
|
209
|
-
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
215
|
+
if (changedFields.length > 0) {
|
|
210
216
|
current.updatedAt = now;
|
|
211
217
|
}
|
|
212
218
|
return { entry: this.loops.get(id), changedFields };
|
package/dist/task-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskEntry
|
|
1
|
+
import type { TaskEntry } from "./task-types.js";
|
|
2
2
|
export declare class TaskStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
@@ -13,12 +13,14 @@ export declare class TaskStore {
|
|
|
13
13
|
create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry;
|
|
14
14
|
get(id: string): TaskEntry | undefined;
|
|
15
15
|
list(): TaskEntry[];
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
start(id: string): TaskEntry | undefined;
|
|
17
|
+
complete(id: string): TaskEntry | undefined;
|
|
18
|
+
reopen(id: string): TaskEntry | undefined;
|
|
19
|
+
updateDetails(id: string, fields: {
|
|
18
20
|
subject?: string;
|
|
19
21
|
description?: string;
|
|
20
22
|
}): TaskEntry | undefined;
|
|
21
23
|
delete(id: string): boolean;
|
|
22
24
|
pendingCount(): number;
|
|
23
|
-
|
|
25
|
+
pruneCompleted(): number;
|
|
24
26
|
}
|
package/dist/task-store.js
CHANGED
|
@@ -141,56 +141,73 @@ export class TaskStore {
|
|
|
141
141
|
this.load();
|
|
142
142
|
return Array.from(this.tasks.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
start(id) {
|
|
145
145
|
return this.withLock(() => {
|
|
146
146
|
const entry = this.tasks.get(id);
|
|
147
147
|
if (!entry)
|
|
148
148
|
return undefined;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
149
|
+
this.applyReducerEvent({
|
|
150
|
+
type: "TASK_STARTED",
|
|
151
|
+
at: Date.now(),
|
|
152
|
+
source: "tool",
|
|
153
|
+
entityType: "task",
|
|
154
|
+
entityId: id,
|
|
155
|
+
payload: { id },
|
|
156
|
+
});
|
|
157
|
+
return this.tasks.get(id);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
complete(id) {
|
|
161
|
+
return this.withLock(() => {
|
|
162
|
+
const entry = this.tasks.get(id);
|
|
163
|
+
if (!entry)
|
|
164
|
+
return undefined;
|
|
165
|
+
this.applyReducerEvent({
|
|
166
|
+
type: "TASK_COMPLETED",
|
|
167
|
+
at: Date.now(),
|
|
168
|
+
source: "tool",
|
|
169
|
+
entityType: "task",
|
|
170
|
+
entityId: id,
|
|
171
|
+
payload: { id },
|
|
172
|
+
});
|
|
173
|
+
return this.tasks.get(id);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
reopen(id) {
|
|
177
|
+
return this.withLock(() => {
|
|
178
|
+
const entry = this.tasks.get(id);
|
|
179
|
+
if (!entry)
|
|
180
|
+
return undefined;
|
|
181
|
+
this.applyReducerEvent({
|
|
182
|
+
type: "TASK_REOPENED",
|
|
183
|
+
at: Date.now(),
|
|
184
|
+
source: "tool",
|
|
185
|
+
entityType: "task",
|
|
186
|
+
entityId: id,
|
|
187
|
+
payload: { id },
|
|
188
|
+
});
|
|
189
|
+
return this.tasks.get(id);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
updateDetails(id, fields) {
|
|
193
|
+
return this.withLock(() => {
|
|
194
|
+
const entry = this.tasks.get(id);
|
|
195
|
+
if (!entry)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (fields.subject === undefined && fields.description === undefined)
|
|
198
|
+
return entry;
|
|
199
|
+
this.applyReducerEvent({
|
|
200
|
+
type: "TASK_UPDATED",
|
|
201
|
+
at: Date.now(),
|
|
202
|
+
source: "tool",
|
|
203
|
+
entityType: "task",
|
|
204
|
+
entityId: id,
|
|
205
|
+
payload: {
|
|
206
|
+
id,
|
|
207
|
+
subject: fields.subject,
|
|
208
|
+
description: fields.description,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
194
211
|
return this.tasks.get(id);
|
|
195
212
|
});
|
|
196
213
|
}
|
|
@@ -217,7 +234,7 @@ export class TaskStore {
|
|
|
217
234
|
}
|
|
218
235
|
return count;
|
|
219
236
|
}
|
|
220
|
-
|
|
237
|
+
pruneCompleted() {
|
|
221
238
|
return this.withLock(() => {
|
|
222
239
|
const before = this.tasks.size;
|
|
223
240
|
this.applyReducerEvent({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { LoopEntry, Trigger } from "../types.js";
|
|
3
|
+
interface LoopStoreLike {
|
|
4
|
+
list(): LoopEntry[];
|
|
5
|
+
get(id: string): LoopEntry | undefined;
|
|
6
|
+
create(trigger: Trigger, prompt: string, opts: {
|
|
7
|
+
recurring: boolean;
|
|
8
|
+
autoTask?: boolean;
|
|
9
|
+
taskBacklog?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
maxFires?: number;
|
|
12
|
+
}): LoopEntry;
|
|
13
|
+
pause(id: string): LoopEntry | undefined;
|
|
14
|
+
delete(id: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
interface TriggerSystemLike {
|
|
17
|
+
add(entry: LoopEntry): void;
|
|
18
|
+
remove(id: string): void;
|
|
19
|
+
}
|
|
20
|
+
interface SchedulerLike {
|
|
21
|
+
nextFire(id: string): number | undefined;
|
|
22
|
+
}
|
|
23
|
+
interface MonitorLike {
|
|
24
|
+
id: string;
|
|
25
|
+
status: string;
|
|
26
|
+
}
|
|
27
|
+
interface MonitorManagerLike {
|
|
28
|
+
get(id: string): MonitorLike | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface LoopToolsOptions {
|
|
31
|
+
pi: ExtensionAPI;
|
|
32
|
+
getStore: () => LoopStoreLike;
|
|
33
|
+
getTriggerSystem: () => TriggerSystemLike;
|
|
34
|
+
getScheduler: () => SchedulerLike;
|
|
35
|
+
getMonitorManager: () => MonitorManagerLike;
|
|
36
|
+
updateWidget: () => void;
|
|
37
|
+
maybeBootstrapTaskLoop: (entry: LoopEntry) => Promise<boolean>;
|
|
38
|
+
isTaskSystemReady: () => boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function registerLoopTools(options: LoopToolsOptions): void;
|
|
41
|
+
export {};
|