@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.
Files changed (54) hide show
  1. package/README.md +11 -10
  2. package/dist/commands/loop-command.d.ts +22 -0
  3. package/dist/commands/loop-command.js +148 -0
  4. package/dist/commands/tasks-command.d.ts +15 -0
  5. package/dist/commands/tasks-command.js +117 -0
  6. package/dist/goal-coordinator.d.ts +22 -0
  7. package/dist/goal-coordinator.js +28 -0
  8. package/dist/goal-reducer.d.ts +89 -0
  9. package/dist/goal-reducer.js +181 -0
  10. package/dist/goal-store.d.ts +31 -0
  11. package/dist/goal-store.js +298 -0
  12. package/dist/goal-types.d.ts +4 -0
  13. package/dist/index.js +125 -1212
  14. package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
  15. package/dist/runtime/monitor-ondone-runtime.js +49 -0
  16. package/dist/runtime/notification-runtime.d.ts +34 -0
  17. package/dist/runtime/notification-runtime.js +152 -0
  18. package/dist/runtime/scope.d.ts +8 -0
  19. package/dist/runtime/scope.js +33 -0
  20. package/dist/runtime/session-runtime.d.ts +39 -0
  21. package/dist/runtime/session-runtime.js +110 -0
  22. package/dist/runtime/task-backlog-runtime.d.ts +36 -0
  23. package/dist/runtime/task-backlog-runtime.js +105 -0
  24. package/dist/runtime/task-rpc.d.ts +19 -0
  25. package/dist/runtime/task-rpc.js +118 -0
  26. package/dist/store.d.ts +5 -4
  27. package/dist/store.js +50 -44
  28. package/dist/task-store.d.ts +6 -4
  29. package/dist/task-store.js +64 -47
  30. package/dist/tools/loop-tools.d.ts +41 -0
  31. package/dist/tools/loop-tools.js +241 -0
  32. package/dist/tools/monitor-tools.d.ts +25 -0
  33. package/dist/tools/monitor-tools.js +110 -0
  34. package/dist/tools/native-task-tools.d.ts +15 -0
  35. package/dist/tools/native-task-tools.js +127 -0
  36. package/package.json +1 -1
  37. package/src/commands/loop-command.ts +184 -0
  38. package/src/commands/tasks-command.ts +135 -0
  39. package/src/goal-coordinator.ts +58 -0
  40. package/src/goal-reducer.ts +280 -0
  41. package/src/goal-store.ts +315 -0
  42. package/src/goal-types.ts +5 -0
  43. package/src/index.ts +129 -1299
  44. package/src/runtime/monitor-ondone-runtime.ts +75 -0
  45. package/src/runtime/notification-runtime.ts +212 -0
  46. package/src/runtime/scope.ts +37 -0
  47. package/src/runtime/session-runtime.ts +168 -0
  48. package/src/runtime/task-backlog-runtime.ts +163 -0
  49. package/src/runtime/task-rpc.ts +150 -0
  50. package/src/store.ts +51 -45
  51. package/src/task-store.ts +61 -46
  52. package/src/tools/loop-tools.ts +304 -0
  53. package/src/tools/monitor-tools.ts +145 -0
  54. 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, LoopStatus, LoopStoreData, Trigger } from "./types.js";
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
- update(id: string, fields: { status?: LoopStatus; trigger?: Trigger; prompt?: string; fireCount?: number }): { entry: LoopEntry | undefined; changedFields: string[] } {
146
+ pause(id: string): LoopEntry | undefined {
147
147
  return this.withLock(() => {
148
148
  const entry = this.loops.get(id);
149
- if (!entry) return { entry: undefined, changedFields: [] };
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
- const changedFields: string[] = [];
152
- const now = Date.now();
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
- if (fields.status === "paused") {
155
- this.applyReducerEvent({
156
- type: "LOOP_PAUSED",
157
- at: now,
158
- source: "tool",
159
- entityType: "loop",
160
- entityId: id,
161
- payload: { id },
162
- });
163
- changedFields.push("status");
164
- } else if (fields.status === "active") {
165
- this.applyReducerEvent({
166
- type: "LOOP_RESUMED",
167
- at: now,
168
- source: "tool",
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 (fields.fireCount !== undefined) {
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, TaskStatus, TaskStoreData } from "./task-types.js";
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
- update(id: string, fields: { status?: TaskStatus; subject?: string; description?: string }): TaskEntry | undefined {
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
- const now = Date.now();
144
- if (fields.status === "in_progress") {
145
- this.applyReducerEvent({
146
- type: "TASK_STARTED",
147
- at: now,
148
- source: "tool",
149
- entityType: "task",
150
- entityId: id,
151
- payload: { id },
152
- });
153
- } else if (fields.status === "completed") {
154
- this.applyReducerEvent({
155
- type: "TASK_COMPLETED",
156
- at: now,
157
- source: "tool",
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
- if (fields.subject !== undefined || fields.description !== undefined) {
174
- this.applyReducerEvent({
175
- type: "TASK_UPDATED",
176
- at: now,
177
- source: "tool",
178
- entityType: "task",
179
- entityId: id,
180
- payload: {
181
- id,
182
- subject: fields.subject,
183
- description: fields.description,
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
- sweepCompleted(): number {
230
+ pruneCompleted(): number {
216
231
  return this.withLock(() => {
217
232
  const before = this.tasks.size;
218
233
  this.applyReducerEvent({