@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,150 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TaskStore } from "../task-store.js";
|
|
4
|
+
import type { LoopEntry } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export interface TaskRuntimeBridgeOptions {
|
|
7
|
+
pi: ExtensionAPI;
|
|
8
|
+
isTasksAvailable: () => boolean;
|
|
9
|
+
setTasksAvailable: (available: boolean) => void;
|
|
10
|
+
getNativeTaskStore: () => TaskStore | undefined;
|
|
11
|
+
onNativeTaskCreated?: (taskStore: TaskStore) => void;
|
|
12
|
+
onNativeTasksPruned?: (taskStore: TaskStore) => Promise<void> | void;
|
|
13
|
+
debug?: (...args: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TaskRuntimeBridge {
|
|
17
|
+
checkTasksVersion(): void;
|
|
18
|
+
autoCreateTask(entry: LoopEntry): Promise<string | undefined>;
|
|
19
|
+
hasPendingTasks(): Promise<number>;
|
|
20
|
+
cleanDoneTasks(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): TaskRuntimeBridge {
|
|
24
|
+
const {
|
|
25
|
+
pi,
|
|
26
|
+
isTasksAvailable,
|
|
27
|
+
setTasksAvailable,
|
|
28
|
+
getNativeTaskStore,
|
|
29
|
+
onNativeTaskCreated,
|
|
30
|
+
onNativeTasksPruned,
|
|
31
|
+
debug,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
function checkTasksVersion() {
|
|
35
|
+
const requestId = randomUUID();
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
unsub();
|
|
38
|
+
}, 5000);
|
|
39
|
+
const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw: unknown) => {
|
|
40
|
+
unsub();
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
const remoteVersion = (raw as { data?: { version?: number } })?.data?.version;
|
|
43
|
+
if (remoteVersion !== undefined) setTasksAvailable(true);
|
|
44
|
+
});
|
|
45
|
+
pi.events.emit("tasks:rpc:ping", { requestId });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function autoCreateTask(entry: LoopEntry): Promise<string | undefined> {
|
|
49
|
+
if (!entry.autoTask) return undefined;
|
|
50
|
+
if (isTasksAvailable()) {
|
|
51
|
+
try {
|
|
52
|
+
const requestId = randomUUID();
|
|
53
|
+
const taskId = await new Promise<string | undefined>((resolve) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
unsub();
|
|
56
|
+
resolve(undefined);
|
|
57
|
+
}, 5000);
|
|
58
|
+
const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
|
|
59
|
+
unsub();
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
const reply = raw as { success: boolean; data?: { id: string } };
|
|
62
|
+
if (reply.success && reply.data) resolve(reply.data.id);
|
|
63
|
+
else resolve(undefined);
|
|
64
|
+
});
|
|
65
|
+
pi.events.emit("tasks:rpc:create", {
|
|
66
|
+
requestId,
|
|
67
|
+
subject: entry.prompt.slice(0, 80),
|
|
68
|
+
description: `Auto-created from loop #${entry.id}`,
|
|
69
|
+
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return taskId;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
79
|
+
if (!nativeTaskStore) return undefined;
|
|
80
|
+
const task = nativeTaskStore.create(
|
|
81
|
+
entry.prompt.slice(0, 80),
|
|
82
|
+
`Auto-created from loop #${entry.id}`,
|
|
83
|
+
{ loopId: entry.id, trigger: entry.trigger },
|
|
84
|
+
);
|
|
85
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
86
|
+
return task.id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function hasPendingTasks(): Promise<number> {
|
|
90
|
+
if (isTasksAvailable()) {
|
|
91
|
+
try {
|
|
92
|
+
const requestId = randomUUID();
|
|
93
|
+
const count = await new Promise<number>((resolve) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
unsub();
|
|
96
|
+
resolve(-1);
|
|
97
|
+
}, 3000);
|
|
98
|
+
const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
|
|
99
|
+
unsub();
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
const reply = raw as { success: boolean; data?: { pending: number } };
|
|
102
|
+
resolve(reply.success && reply.data ? reply.data.pending : -1);
|
|
103
|
+
});
|
|
104
|
+
pi.events.emit("tasks:rpc:pending", { requestId });
|
|
105
|
+
});
|
|
106
|
+
return count;
|
|
107
|
+
} catch {
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return getNativeTaskStore()?.pendingCount() ?? -1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cleanDoneTasks(): Promise<void> {
|
|
116
|
+
if (isTasksAvailable()) {
|
|
117
|
+
try {
|
|
118
|
+
const requestId = randomUUID();
|
|
119
|
+
await new Promise<void>((resolve) => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
unsub();
|
|
122
|
+
resolve();
|
|
123
|
+
}, 3000);
|
|
124
|
+
const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
|
|
125
|
+
unsub();
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
debug?.("tasks:rpc:clean — done tasks swept");
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
pi.events.emit("tasks:rpc:clean", { requestId });
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
// timeout or error, ignore
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
139
|
+
if (!nativeTaskStore) return;
|
|
140
|
+
nativeTaskStore.pruneCompleted();
|
|
141
|
+
await onNativeTasksPruned?.(nativeTaskStore);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
checkTasksVersion,
|
|
146
|
+
autoCreateTask,
|
|
147
|
+
hasPendingTasks,
|
|
148
|
+
cleanDoneTasks,
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { type LoopReducerEvent, type LoopReducerState, reduceLoopState } from "./loop-reducer.js";
|
|
5
|
-
import type { LoopEntry,
|
|
5
|
+
import type { LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
6
6
|
|
|
7
7
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
8
8
|
const LOCK_RETRY_MS = 50;
|
|
@@ -143,38 +143,61 @@ export class LoopStore {
|
|
|
143
143
|
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
pause(id: string): LoopEntry | undefined {
|
|
147
147
|
return this.withLock(() => {
|
|
148
148
|
const entry = this.loops.get(id);
|
|
149
|
-
if (!entry) return
|
|
149
|
+
if (!entry) return undefined;
|
|
150
|
+
this.applyReducerEvent({
|
|
151
|
+
type: "LOOP_PAUSED",
|
|
152
|
+
at: Date.now(),
|
|
153
|
+
source: "tool",
|
|
154
|
+
entityType: "loop",
|
|
155
|
+
entityId: id,
|
|
156
|
+
payload: { id },
|
|
157
|
+
});
|
|
158
|
+
return this.loops.get(id);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
resume(id: string): LoopEntry | undefined {
|
|
163
|
+
return this.withLock(() => {
|
|
164
|
+
const entry = this.loops.get(id);
|
|
165
|
+
if (!entry) return undefined;
|
|
166
|
+
this.applyReducerEvent({
|
|
167
|
+
type: "LOOP_RESUMED",
|
|
168
|
+
at: Date.now(),
|
|
169
|
+
source: "tool",
|
|
170
|
+
entityType: "loop",
|
|
171
|
+
entityId: id,
|
|
172
|
+
payload: { id },
|
|
173
|
+
});
|
|
174
|
+
return this.loops.get(id);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
153
177
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
entityType: "loop",
|
|
170
|
-
entityId: id,
|
|
171
|
-
payload: { id },
|
|
172
|
-
});
|
|
173
|
-
changedFields.push("status");
|
|
174
|
-
}
|
|
178
|
+
fire(id: string): LoopEntry | undefined {
|
|
179
|
+
return this.withLock(() => {
|
|
180
|
+
const entry = this.loops.get(id);
|
|
181
|
+
if (!entry) return undefined;
|
|
182
|
+
this.applyReducerEvent({
|
|
183
|
+
type: "LOOP_FIRED",
|
|
184
|
+
at: Date.now(),
|
|
185
|
+
source: "system",
|
|
186
|
+
entityType: "loop",
|
|
187
|
+
entityId: id,
|
|
188
|
+
payload: { id },
|
|
189
|
+
});
|
|
190
|
+
return this.loops.get(id);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
175
193
|
|
|
194
|
+
updateMetadata(id: string, fields: { trigger?: Trigger; prompt?: string }): { entry: LoopEntry | undefined; changedFields: string[] } {
|
|
195
|
+
return this.withLock(() => {
|
|
176
196
|
const current = this.loops.get(id);
|
|
177
|
-
if (!current) return { entry: undefined, changedFields };
|
|
197
|
+
if (!current) return { entry: undefined, changedFields: [] };
|
|
198
|
+
|
|
199
|
+
const changedFields: string[] = [];
|
|
200
|
+
const now = Date.now();
|
|
178
201
|
|
|
179
202
|
if (fields.trigger !== undefined) {
|
|
180
203
|
current.trigger = fields.trigger;
|
|
@@ -184,24 +207,7 @@ export class LoopStore {
|
|
|
184
207
|
current.prompt = fields.prompt;
|
|
185
208
|
changedFields.push("prompt");
|
|
186
209
|
}
|
|
187
|
-
if (
|
|
188
|
-
if (fields.fireCount === (current.fireCount ?? 0) + 1) {
|
|
189
|
-
this.applyReducerEvent({
|
|
190
|
-
type: "LOOP_FIRED",
|
|
191
|
-
at: now,
|
|
192
|
-
source: "system",
|
|
193
|
-
entityType: "loop",
|
|
194
|
-
entityId: id,
|
|
195
|
-
payload: { id },
|
|
196
|
-
});
|
|
197
|
-
} else {
|
|
198
|
-
current.fireCount = fields.fireCount;
|
|
199
|
-
current.updatedAt = now;
|
|
200
|
-
}
|
|
201
|
-
changedFields.push("fireCount");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
210
|
+
if (changedFields.length > 0) {
|
|
205
211
|
current.updatedAt = now;
|
|
206
212
|
}
|
|
207
213
|
|
package/src/task-store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceTaskState, type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
5
|
-
import type { TaskEntry,
|
|
5
|
+
import type { TaskEntry, TaskStoreData } from "./task-types.js";
|
|
6
6
|
|
|
7
7
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
8
8
|
const LOCK_RETRY_MS = 50;
|
|
@@ -135,56 +135,71 @@ export class TaskStore {
|
|
|
135
135
|
return Array.from(this.tasks.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
start(id: string): TaskEntry | undefined {
|
|
139
139
|
return this.withLock(() => {
|
|
140
140
|
const entry = this.tasks.get(id);
|
|
141
141
|
if (!entry) return undefined;
|
|
142
|
+
this.applyReducerEvent({
|
|
143
|
+
type: "TASK_STARTED",
|
|
144
|
+
at: Date.now(),
|
|
145
|
+
source: "tool",
|
|
146
|
+
entityType: "task",
|
|
147
|
+
entityId: id,
|
|
148
|
+
payload: { id },
|
|
149
|
+
});
|
|
150
|
+
return this.tasks.get(id);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
entityType: "task",
|
|
159
|
-
entityId: id,
|
|
160
|
-
payload: { id },
|
|
161
|
-
});
|
|
162
|
-
} else if (fields.status === "pending") {
|
|
163
|
-
this.applyReducerEvent({
|
|
164
|
-
type: "TASK_REOPENED",
|
|
165
|
-
at: now,
|
|
166
|
-
source: "tool",
|
|
167
|
-
entityType: "task",
|
|
168
|
-
entityId: id,
|
|
169
|
-
payload: { id },
|
|
170
|
-
});
|
|
171
|
-
}
|
|
154
|
+
complete(id: string): TaskEntry | undefined {
|
|
155
|
+
return this.withLock(() => {
|
|
156
|
+
const entry = this.tasks.get(id);
|
|
157
|
+
if (!entry) return undefined;
|
|
158
|
+
this.applyReducerEvent({
|
|
159
|
+
type: "TASK_COMPLETED",
|
|
160
|
+
at: Date.now(),
|
|
161
|
+
source: "tool",
|
|
162
|
+
entityType: "task",
|
|
163
|
+
entityId: id,
|
|
164
|
+
payload: { id },
|
|
165
|
+
});
|
|
166
|
+
return this.tasks.get(id);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
reopen(id: string): TaskEntry | undefined {
|
|
171
|
+
return this.withLock(() => {
|
|
172
|
+
const entry = this.tasks.get(id);
|
|
173
|
+
if (!entry) return undefined;
|
|
174
|
+
this.applyReducerEvent({
|
|
175
|
+
type: "TASK_REOPENED",
|
|
176
|
+
at: Date.now(),
|
|
177
|
+
source: "tool",
|
|
178
|
+
entityType: "task",
|
|
179
|
+
entityId: id,
|
|
180
|
+
payload: { id },
|
|
181
|
+
});
|
|
182
|
+
return this.tasks.get(id);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
187
185
|
|
|
186
|
+
updateDetails(id: string, fields: { subject?: string; description?: string }): TaskEntry | undefined {
|
|
187
|
+
return this.withLock(() => {
|
|
188
|
+
const entry = this.tasks.get(id);
|
|
189
|
+
if (!entry) return undefined;
|
|
190
|
+
if (fields.subject === undefined && fields.description === undefined) return entry;
|
|
191
|
+
this.applyReducerEvent({
|
|
192
|
+
type: "TASK_UPDATED",
|
|
193
|
+
at: Date.now(),
|
|
194
|
+
source: "tool",
|
|
195
|
+
entityType: "task",
|
|
196
|
+
entityId: id,
|
|
197
|
+
payload: {
|
|
198
|
+
id,
|
|
199
|
+
subject: fields.subject,
|
|
200
|
+
description: fields.description,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
188
203
|
return this.tasks.get(id);
|
|
189
204
|
});
|
|
190
205
|
}
|
|
@@ -212,7 +227,7 @@ export class TaskStore {
|
|
|
212
227
|
return count;
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
|
|
230
|
+
pruneCompleted(): number {
|
|
216
231
|
return this.withLock(() => {
|
|
217
232
|
const before = this.tasks.size;
|
|
218
233
|
this.applyReducerEvent({
|