@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,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, LoopStatus, Trigger } from "./types.js";
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
- update(id: string, fields: {
23
- status?: LoopStatus;
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
- update(id, fields) {
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 (fields.fireCount !== undefined) {
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 };
@@ -1,4 +1,4 @@
1
- import type { TaskEntry, TaskStatus } from "./task-types.js";
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
- update(id: string, fields: {
17
- status?: TaskStatus;
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
- sweepCompleted(): number;
25
+ pruneCompleted(): number;
24
26
  }
@@ -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
- update(id, fields) {
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
- const now = Date.now();
150
- if (fields.status === "in_progress") {
151
- this.applyReducerEvent({
152
- type: "TASK_STARTED",
153
- at: now,
154
- source: "tool",
155
- entityType: "task",
156
- entityId: id,
157
- payload: { id },
158
- });
159
- }
160
- else if (fields.status === "completed") {
161
- this.applyReducerEvent({
162
- type: "TASK_COMPLETED",
163
- at: now,
164
- source: "tool",
165
- entityType: "task",
166
- entityId: id,
167
- payload: { id },
168
- });
169
- }
170
- else if (fields.status === "pending") {
171
- this.applyReducerEvent({
172
- type: "TASK_REOPENED",
173
- at: now,
174
- source: "tool",
175
- entityType: "task",
176
- entityId: id,
177
- payload: { id },
178
- });
179
- }
180
- if (fields.subject !== undefined || fields.description !== undefined) {
181
- this.applyReducerEvent({
182
- type: "TASK_UPDATED",
183
- at: now,
184
- source: "tool",
185
- entityType: "task",
186
- entityId: id,
187
- payload: {
188
- id,
189
- subject: fields.subject,
190
- description: fields.description,
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
- sweepCompleted() {
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 {};