@trevonistrevon/pi-loop 0.4.11 → 0.5.0
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 +10 -0
- package/dist/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -0
- package/dist/goal-types.d.ts +78 -0
- package/dist/goal-types.js +1 -0
- package/dist/goal-verifier.d.ts +20 -0
- package/dist/goal-verifier.js +198 -0
- package/dist/index.js +170 -40
- package/dist/loop-reducer.d.ts +63 -0
- package/dist/loop-reducer.js +67 -0
- package/dist/monitor-completion-coordinator.d.ts +10 -0
- package/dist/monitor-completion-coordinator.js +13 -0
- package/dist/monitor-manager.d.ts +2 -0
- package/dist/monitor-manager.js +107 -29
- package/dist/monitor-reducer.d.ts +82 -0
- package/dist/monitor-reducer.js +69 -0
- package/dist/notification-reducer.d.ts +81 -0
- package/dist/notification-reducer.js +65 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +118 -44
- package/dist/task-backlog-coordinator.d.ts +12 -0
- package/dist/task-backlog-coordinator.js +22 -0
- package/dist/task-reducer.d.ts +66 -0
- package/dist/task-reducer.js +76 -0
- package/dist/task-store.d.ts +2 -0
- package/dist/task-store.js +82 -30
- package/docs/architecture/goal-state-schema.md +505 -0
- package/docs/architecture/state-machine-migration.md +546 -0
- package/docs/architecture/state-machine-reducer-event-model.md +823 -0
- package/docs/architecture/state-machine-test-matrix.md +249 -0
- package/docs/architecture/state-machine-transition-map.md +436 -0
- package/package.json +1 -1
- package/src/coordinator.ts +115 -0
- package/src/goal-types.ts +99 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +196 -39
- package/src/loop-reducer.ts +148 -0
- package/src/monitor-completion-coordinator.ts +24 -0
- package/src/monitor-manager.ts +115 -27
- package/src/monitor-reducer.ts +166 -0
- package/src/notification-reducer.ts +155 -0
- package/src/store.ts +119 -43
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +84 -27
package/dist/index.js
CHANGED
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { join, resolve } from "node:path";
|
|
18
18
|
import { Type } from "typebox";
|
|
19
|
+
import { createCoordinator, } from "./coordinator.js";
|
|
19
20
|
import { parseInterval } from "./loop-parse.js";
|
|
21
|
+
import { reduceMonitorCompletionEvent, } from "./monitor-completion-coordinator.js";
|
|
20
22
|
import { MonitorManager } from "./monitor-manager.js";
|
|
23
|
+
import { reduceNotificationState, } from "./notification-reducer.js";
|
|
21
24
|
import { CronScheduler } from "./scheduler.js";
|
|
22
25
|
import { LoopStore } from "./store.js";
|
|
26
|
+
import { reduceTaskBacklogEvent, } from "./task-backlog-coordinator.js";
|
|
23
27
|
import { TaskStore } from "./task-store.js";
|
|
24
28
|
import { TriggerSystem } from "./trigger-system.js";
|
|
25
29
|
import { LoopWidget } from "./ui/widget.js";
|
|
@@ -183,12 +187,89 @@ export default function (pi) {
|
|
|
183
187
|
if (nativeTaskStore) {
|
|
184
188
|
nativeTaskStore.sweepCompleted();
|
|
185
189
|
widget.update();
|
|
186
|
-
await
|
|
190
|
+
await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
|
-
let
|
|
190
|
-
|
|
193
|
+
let notificationState = {
|
|
194
|
+
notificationsByKey: {},
|
|
195
|
+
agentRunning: false,
|
|
196
|
+
hasPendingMessages: false,
|
|
197
|
+
};
|
|
191
198
|
let flushPromise;
|
|
199
|
+
let notificationCoordinatorDelivered = false;
|
|
200
|
+
let notificationCoordinatorDeliveredSuccessfully = false;
|
|
201
|
+
const notificationReducerHandler = (incoming) => {
|
|
202
|
+
const result = reduceNotificationState(notificationState, incoming);
|
|
203
|
+
notificationState = result.state;
|
|
204
|
+
return result.effects;
|
|
205
|
+
};
|
|
206
|
+
const notificationCoordinator = createCoordinator({
|
|
207
|
+
reducers: [notificationReducerHandler],
|
|
208
|
+
effectHandlers: {
|
|
209
|
+
REQUEST_NOTIFICATION_FLUSH: () => { },
|
|
210
|
+
DELIVER_NOTIFICATION: async (effect) => {
|
|
211
|
+
notificationCoordinatorDelivered = true;
|
|
212
|
+
notificationCoordinatorDeliveredSuccessfully = await deliverNotification(effect.payload.notification);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const monitorCompletionReducerHandler = (incoming) => {
|
|
217
|
+
if (incoming.type !== "MONITOR_ONDONE_TRIGGERED")
|
|
218
|
+
return [];
|
|
219
|
+
return reduceMonitorCompletionEvent(incoming);
|
|
220
|
+
};
|
|
221
|
+
const monitorCompletionCoordinator = createCoordinator({
|
|
222
|
+
reducers: [monitorCompletionReducerHandler],
|
|
223
|
+
effectHandlers: {
|
|
224
|
+
DELIVER_MONITOR_ONDONE_WAKE: (effect) => {
|
|
225
|
+
const { loopId, monitorId } = effect.payload;
|
|
226
|
+
const current = store.get(loopId);
|
|
227
|
+
if (!current)
|
|
228
|
+
return;
|
|
229
|
+
debug(`onDone loop #${loopId} — monitor #${monitorId} completed, delivering through coordinator`);
|
|
230
|
+
onLoopFire(current);
|
|
231
|
+
store.delete(loopId);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
let taskBacklogCoordinatorStore;
|
|
236
|
+
let taskBacklogCoordinatorWorker = { created: false };
|
|
237
|
+
let taskBacklogCoordinatorCleanupCount = 0;
|
|
238
|
+
const taskBacklogReducerHandler = (incoming) => {
|
|
239
|
+
if (incoming.type !== "TASK_BACKLOG_EVALUATED")
|
|
240
|
+
return [];
|
|
241
|
+
return reduceTaskBacklogEvent(incoming);
|
|
242
|
+
};
|
|
243
|
+
const taskBacklogCoordinator = createCoordinator({
|
|
244
|
+
reducers: [taskBacklogReducerHandler],
|
|
245
|
+
effectHandlers: {
|
|
246
|
+
ENSURE_AUTO_TASK_WORKER: async () => {
|
|
247
|
+
if (!taskBacklogCoordinatorStore)
|
|
248
|
+
return;
|
|
249
|
+
taskBacklogCoordinatorWorker = await ensureAutoTaskWorkerLoop(taskBacklogCoordinatorStore);
|
|
250
|
+
},
|
|
251
|
+
CLEANUP_TASK_BACKLOG_LOOPS: async () => {
|
|
252
|
+
taskBacklogCoordinatorCleanupCount = await cleanupTaskBacklogLoops();
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
function applyNotificationEvent(event) {
|
|
257
|
+
const result = reduceNotificationState(notificationState, event);
|
|
258
|
+
notificationState = result.state;
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
function syncNotificationRuntimeState(options) {
|
|
262
|
+
applyNotificationEvent({
|
|
263
|
+
type: "NOTIFICATION_RUNTIME_UPDATED",
|
|
264
|
+
at: Date.now(),
|
|
265
|
+
source: "system",
|
|
266
|
+
entityType: "notification",
|
|
267
|
+
payload: {
|
|
268
|
+
agentRunning: options?.agentRunning ?? notificationState.agentRunning,
|
|
269
|
+
hasPendingMessages: options?.hasPendingMessages ?? (_latestCtx?.hasPendingMessages() ?? false),
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
192
273
|
function buildLoopFireMessage(data) {
|
|
193
274
|
const triggerInfo = typeof data.trigger === "string"
|
|
194
275
|
? data.trigger
|
|
@@ -253,7 +334,7 @@ export default function (pi) {
|
|
|
253
334
|
return false;
|
|
254
335
|
}
|
|
255
336
|
}
|
|
256
|
-
agentRunning
|
|
337
|
+
syncNotificationRuntimeState({ agentRunning: true });
|
|
257
338
|
pi.sendMessage({
|
|
258
339
|
customType: "pi-loop",
|
|
259
340
|
content: notification.message,
|
|
@@ -276,16 +357,22 @@ export default function (pi) {
|
|
|
276
357
|
if (flushPromise)
|
|
277
358
|
return flushPromise;
|
|
278
359
|
flushPromise = (async () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
360
|
+
syncNotificationRuntimeState({
|
|
361
|
+
hasPendingMessages: _latestCtx?.hasPendingMessages() ?? false,
|
|
362
|
+
});
|
|
363
|
+
while (true) {
|
|
364
|
+
notificationCoordinatorDelivered = false;
|
|
365
|
+
notificationCoordinatorDeliveredSuccessfully = false;
|
|
366
|
+
await notificationCoordinator.dispatch({
|
|
367
|
+
type: "NOTIFICATION_FLUSH_REQUESTED",
|
|
368
|
+
at: Date.now(),
|
|
369
|
+
source: "system",
|
|
370
|
+
entityType: "notification",
|
|
371
|
+
payload: { ignorePendingMessages: options?.ignorePendingMessages },
|
|
372
|
+
});
|
|
373
|
+
if (!notificationCoordinatorDelivered)
|
|
374
|
+
return;
|
|
375
|
+
if (notificationCoordinatorDeliveredSuccessfully)
|
|
289
376
|
return;
|
|
290
377
|
}
|
|
291
378
|
})().finally(() => {
|
|
@@ -295,7 +382,14 @@ export default function (pi) {
|
|
|
295
382
|
}
|
|
296
383
|
async function queueOrDeliverNotification(data) {
|
|
297
384
|
const notification = buildPendingNotification(data);
|
|
298
|
-
|
|
385
|
+
applyNotificationEvent({
|
|
386
|
+
type: "NOTIFICATION_QUEUED",
|
|
387
|
+
at: notification.timestamp,
|
|
388
|
+
source: "system",
|
|
389
|
+
entityType: "notification",
|
|
390
|
+
entityId: notification.key,
|
|
391
|
+
payload: { notification },
|
|
392
|
+
});
|
|
299
393
|
await flushPendingNotifications();
|
|
300
394
|
}
|
|
301
395
|
// ── Loop fire handler ──
|
|
@@ -370,28 +464,40 @@ export default function (pi) {
|
|
|
370
464
|
widget.update();
|
|
371
465
|
});
|
|
372
466
|
pi.on("agent_start", async (_event, ctx) => {
|
|
373
|
-
agentRunning
|
|
467
|
+
syncNotificationRuntimeState({ agentRunning: true, hasPendingMessages: ctx.hasPendingMessages() });
|
|
374
468
|
_latestCtx = ctx;
|
|
375
469
|
widget.setUICtx(ctx.ui);
|
|
376
470
|
});
|
|
377
471
|
pi.on("agent_end", async (_event, ctx) => {
|
|
378
|
-
agentRunning = false;
|
|
379
472
|
_latestCtx = ctx;
|
|
380
473
|
widget.setUICtx(ctx.ui);
|
|
474
|
+
syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: ctx.hasPendingMessages() });
|
|
381
475
|
await flushPendingNotifications({ ignorePendingMessages: true });
|
|
382
476
|
await cleanupTaskBacklogLoops();
|
|
383
477
|
await pumpLoops();
|
|
384
478
|
});
|
|
385
479
|
pi.on("session_shutdown", async () => {
|
|
386
|
-
agentRunning
|
|
387
|
-
|
|
480
|
+
syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: false });
|
|
481
|
+
applyNotificationEvent({
|
|
482
|
+
type: "NOTIFICATION_CLEARED",
|
|
483
|
+
at: Date.now(),
|
|
484
|
+
source: "session",
|
|
485
|
+
entityType: "notification",
|
|
486
|
+
payload: { reason: "session_shutdown" },
|
|
487
|
+
});
|
|
388
488
|
});
|
|
389
489
|
pi.on("session_switch", async (event, ctx) => {
|
|
390
490
|
_latestCtx = ctx;
|
|
391
491
|
widget.setUICtx(ctx.ui);
|
|
392
492
|
triggerSystem.stop();
|
|
393
|
-
agentRunning
|
|
394
|
-
|
|
493
|
+
syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: false });
|
|
494
|
+
applyNotificationEvent({
|
|
495
|
+
type: "NOTIFICATION_CLEARED",
|
|
496
|
+
at: Date.now(),
|
|
497
|
+
source: "session",
|
|
498
|
+
entityType: "notification",
|
|
499
|
+
payload: { reason: "session_switch" },
|
|
500
|
+
});
|
|
395
501
|
_sessionId = undefined;
|
|
396
502
|
const isResume = event?.reason === "resume";
|
|
397
503
|
storeUpgraded = false;
|
|
@@ -584,12 +690,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
584
690
|
});
|
|
585
691
|
function handleMonitorDoneLoop(doneLoop, monitorId) {
|
|
586
692
|
const deliver = () => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
693
|
+
void monitorCompletionCoordinator.dispatch({
|
|
694
|
+
type: "MONITOR_ONDONE_TRIGGERED",
|
|
695
|
+
at: Date.now(),
|
|
696
|
+
source: "monitor",
|
|
697
|
+
entityType: "monitor",
|
|
698
|
+
entityId: monitorId,
|
|
699
|
+
payload: { loopId: doneLoop.id, monitorId },
|
|
700
|
+
});
|
|
593
701
|
};
|
|
594
702
|
const registered = monitorManager.onComplete(monitorId, deliver);
|
|
595
703
|
if (registered)
|
|
@@ -1012,6 +1120,25 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1012
1120
|
widget.update();
|
|
1013
1121
|
return { entry, created: true };
|
|
1014
1122
|
}
|
|
1123
|
+
async function evaluateTaskBacklog(taskStore, pendingCount) {
|
|
1124
|
+
const resolvedPending = pendingCount ?? (taskStore ? taskStore.pendingCount() : await hasPendingTasks());
|
|
1125
|
+
taskBacklogCoordinatorStore = taskStore;
|
|
1126
|
+
taskBacklogCoordinatorWorker = { created: false };
|
|
1127
|
+
taskBacklogCoordinatorCleanupCount = 0;
|
|
1128
|
+
await taskBacklogCoordinator.dispatch({
|
|
1129
|
+
type: "TASK_BACKLOG_EVALUATED",
|
|
1130
|
+
at: Date.now(),
|
|
1131
|
+
source: "system",
|
|
1132
|
+
entityType: "task",
|
|
1133
|
+
payload: { pendingCount: resolvedPending, threshold: AUTO_TASK_WORKER_THRESHOLD },
|
|
1134
|
+
});
|
|
1135
|
+
taskBacklogCoordinatorStore = undefined;
|
|
1136
|
+
return {
|
|
1137
|
+
entry: taskBacklogCoordinatorWorker.entry,
|
|
1138
|
+
created: taskBacklogCoordinatorWorker.created,
|
|
1139
|
+
cleaned: taskBacklogCoordinatorCleanupCount,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1015
1142
|
async function createNativeTaskInteractively(ui) {
|
|
1016
1143
|
if (!nativeTaskStore) {
|
|
1017
1144
|
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
@@ -1028,11 +1155,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1028
1155
|
description: entry.description,
|
|
1029
1156
|
status: entry.status,
|
|
1030
1157
|
});
|
|
1031
|
-
const
|
|
1158
|
+
const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1032
1159
|
widget.update();
|
|
1033
1160
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
1034
|
-
if (
|
|
1035
|
-
ui.notify(`Worker loop #${
|
|
1161
|
+
if (backlog.created && backlog.entry) {
|
|
1162
|
+
ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
1036
1163
|
}
|
|
1037
1164
|
}
|
|
1038
1165
|
async function viewNativeTasks(ui) {
|
|
@@ -1093,7 +1220,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1093
1220
|
ui.notify(`Task #${task.id} reopened`, "info");
|
|
1094
1221
|
}
|
|
1095
1222
|
widget.update();
|
|
1096
|
-
await
|
|
1223
|
+
await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1097
1224
|
return viewNativeTasks(ui);
|
|
1098
1225
|
}
|
|
1099
1226
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
@@ -1119,11 +1246,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1119
1246
|
description: entry.description,
|
|
1120
1247
|
status: entry.status,
|
|
1121
1248
|
});
|
|
1122
|
-
const
|
|
1249
|
+
const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1123
1250
|
widget.update();
|
|
1124
1251
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
1125
|
-
if (
|
|
1126
|
-
ctx.ui.notify(`Worker loop #${
|
|
1252
|
+
if (backlog.created && backlog.entry) {
|
|
1253
|
+
ctx.ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
1127
1254
|
}
|
|
1128
1255
|
return;
|
|
1129
1256
|
}
|
|
@@ -1156,10 +1283,10 @@ Fields:
|
|
|
1156
1283
|
description: entry.description,
|
|
1157
1284
|
status: entry.status,
|
|
1158
1285
|
});
|
|
1159
|
-
const
|
|
1286
|
+
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1160
1287
|
widget.update();
|
|
1161
|
-
const autoLoopMsg =
|
|
1162
|
-
? `\nWorker loop #${
|
|
1288
|
+
const autoLoopMsg = backlog.created && backlog.entry
|
|
1289
|
+
? `\nWorker loop #${backlog.entry.id} auto-created`
|
|
1163
1290
|
: "";
|
|
1164
1291
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
1165
1292
|
},
|
|
@@ -1216,9 +1343,12 @@ Parameters: id (required), status, subject, description`,
|
|
|
1216
1343
|
if (!entry)
|
|
1217
1344
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
1218
1345
|
widget.update();
|
|
1219
|
-
await
|
|
1346
|
+
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1220
1347
|
const statusMsg = status ? ` → ${status}` : "";
|
|
1221
|
-
|
|
1348
|
+
const autoLoopMsg = backlog.created && backlog.entry
|
|
1349
|
+
? `\nWorker loop #${backlog.entry.id} auto-created`
|
|
1350
|
+
: "";
|
|
1351
|
+
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
1222
1352
|
},
|
|
1223
1353
|
});
|
|
1224
1354
|
pi.registerTool({
|
|
@@ -1232,7 +1362,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1232
1362
|
const deleted = taskStore.delete(params.id);
|
|
1233
1363
|
widget.update();
|
|
1234
1364
|
if (deleted) {
|
|
1235
|
-
await
|
|
1365
|
+
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1236
1366
|
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
1237
1367
|
}
|
|
1238
1368
|
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LoopEntry, Trigger } from "./types.js";
|
|
2
|
+
export declare const MAX_LOOP_EXPIRY_MS: number;
|
|
3
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
4
|
+
export interface LoopReducerState {
|
|
5
|
+
nextId: number;
|
|
6
|
+
loopsById: Record<string, LoopEntry>;
|
|
7
|
+
}
|
|
8
|
+
export type LoopReducerEvent = {
|
|
9
|
+
type: "LOOP_CREATED";
|
|
10
|
+
at: number;
|
|
11
|
+
source: ReducerSource;
|
|
12
|
+
entityType?: "loop";
|
|
13
|
+
entityId?: string;
|
|
14
|
+
payload: {
|
|
15
|
+
prompt: string;
|
|
16
|
+
trigger: Trigger;
|
|
17
|
+
recurring: boolean;
|
|
18
|
+
autoTask?: boolean;
|
|
19
|
+
taskBacklog?: boolean;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
maxFires?: number;
|
|
22
|
+
};
|
|
23
|
+
} | {
|
|
24
|
+
type: "LOOP_PAUSED" | "LOOP_RESUMED" | "LOOP_FIRED" | "LOOP_DELETED" | "LOOP_MAX_FIRES_REACHED" | "LOOP_BACKLOG_EMPTY";
|
|
25
|
+
at: number;
|
|
26
|
+
source: ReducerSource;
|
|
27
|
+
entityType?: "loop";
|
|
28
|
+
entityId?: string;
|
|
29
|
+
payload: {
|
|
30
|
+
id: string;
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
type: "LOOP_EXPIRED";
|
|
34
|
+
at: number;
|
|
35
|
+
source: ReducerSource;
|
|
36
|
+
entityType?: "loop";
|
|
37
|
+
entityId?: string;
|
|
38
|
+
payload: {
|
|
39
|
+
id: string;
|
|
40
|
+
reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type LoopReducerEffect = {
|
|
44
|
+
type: "PERSIST_LOOP";
|
|
45
|
+
entityType: "loop";
|
|
46
|
+
entityId: string;
|
|
47
|
+
payload: {
|
|
48
|
+
loop: LoopEntry;
|
|
49
|
+
};
|
|
50
|
+
} | {
|
|
51
|
+
type: "DELETE_LOOP";
|
|
52
|
+
entityType: "loop";
|
|
53
|
+
entityId: string;
|
|
54
|
+
payload: {
|
|
55
|
+
id: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export interface LoopReduceResult {
|
|
59
|
+
state: LoopReducerState;
|
|
60
|
+
effects: LoopReducerEffect[];
|
|
61
|
+
}
|
|
62
|
+
export declare function reduceLoopState(state: LoopReducerState, event: LoopReducerEvent): LoopReduceResult;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
2
|
+
function cloneState(state) {
|
|
3
|
+
return {
|
|
4
|
+
nextId: state.nextId,
|
|
5
|
+
loopsById: { ...state.loopsById },
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function reduceLoopState(state, event) {
|
|
9
|
+
if (event.type === "LOOP_CREATED") {
|
|
10
|
+
const next = cloneState(state);
|
|
11
|
+
const id = String(next.nextId++);
|
|
12
|
+
const loop = {
|
|
13
|
+
id,
|
|
14
|
+
prompt: event.payload.prompt,
|
|
15
|
+
trigger: event.payload.trigger,
|
|
16
|
+
status: "active",
|
|
17
|
+
recurring: event.payload.recurring,
|
|
18
|
+
createdAt: event.at,
|
|
19
|
+
updatedAt: event.at,
|
|
20
|
+
expiresAt: event.at + MAX_LOOP_EXPIRY_MS,
|
|
21
|
+
autoTask: event.payload.autoTask,
|
|
22
|
+
taskBacklog: event.payload.taskBacklog,
|
|
23
|
+
readOnly: event.payload.readOnly,
|
|
24
|
+
maxFires: event.payload.maxFires,
|
|
25
|
+
fireCount: 0,
|
|
26
|
+
};
|
|
27
|
+
next.loopsById[id] = loop;
|
|
28
|
+
return {
|
|
29
|
+
state: next,
|
|
30
|
+
effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const id = event.payload.id;
|
|
34
|
+
const current = state.loopsById[id];
|
|
35
|
+
if (!current)
|
|
36
|
+
return { state, effects: [] };
|
|
37
|
+
if (event.type === "LOOP_DELETED"
|
|
38
|
+
|| event.type === "LOOP_MAX_FIRES_REACHED"
|
|
39
|
+
|| event.type === "LOOP_EXPIRED"
|
|
40
|
+
|| event.type === "LOOP_BACKLOG_EMPTY") {
|
|
41
|
+
const next = cloneState(state);
|
|
42
|
+
delete next.loopsById[id];
|
|
43
|
+
return {
|
|
44
|
+
state: next,
|
|
45
|
+
effects: [{ type: "DELETE_LOOP", entityType: "loop", entityId: id, payload: { id } }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const next = cloneState(state);
|
|
49
|
+
const loop = { ...current };
|
|
50
|
+
if (event.type === "LOOP_PAUSED") {
|
|
51
|
+
loop.status = "paused";
|
|
52
|
+
loop.updatedAt = event.at;
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "LOOP_RESUMED") {
|
|
55
|
+
loop.status = "active";
|
|
56
|
+
loop.updatedAt = event.at;
|
|
57
|
+
}
|
|
58
|
+
if (event.type === "LOOP_FIRED") {
|
|
59
|
+
loop.fireCount = (loop.fireCount ?? 0) + 1;
|
|
60
|
+
loop.updatedAt = event.at;
|
|
61
|
+
}
|
|
62
|
+
next.loopsById[id] = loop;
|
|
63
|
+
return {
|
|
64
|
+
state: next,
|
|
65
|
+
effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
|
|
2
|
+
export type MonitorCompletionEvent = ReducerEvent<"MONITOR_ONDONE_TRIGGERED", {
|
|
3
|
+
loopId: string;
|
|
4
|
+
monitorId: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type MonitorCompletionEffect = ReducerEffect<"DELIVER_MONITOR_ONDONE_WAKE", {
|
|
7
|
+
loopId: string;
|
|
8
|
+
monitorId: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function reduceMonitorCompletionEvent(event: MonitorCompletionEvent): MonitorCompletionEffect[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function reduceMonitorCompletionEvent(event) {
|
|
2
|
+
if (event.type !== "MONITOR_ONDONE_TRIGGERED")
|
|
3
|
+
return [];
|
|
4
|
+
return [{
|
|
5
|
+
type: "DELIVER_MONITOR_ONDONE_WAKE",
|
|
6
|
+
entityType: "monitor",
|
|
7
|
+
entityId: event.payload.monitorId,
|
|
8
|
+
payload: {
|
|
9
|
+
loopId: event.payload.loopId,
|
|
10
|
+
monitorId: event.payload.monitorId,
|
|
11
|
+
},
|
|
12
|
+
}];
|
|
13
|
+
}
|
|
@@ -5,6 +5,8 @@ export declare class MonitorManager {
|
|
|
5
5
|
private processes;
|
|
6
6
|
private nextId;
|
|
7
7
|
constructor(pi: ExtensionAPI);
|
|
8
|
+
private toReducerState;
|
|
9
|
+
private applyReducerEvent;
|
|
8
10
|
create(command: string, description?: string, timeout?: number): MonitorEntry;
|
|
9
11
|
get(id: string): MonitorEntry | undefined;
|
|
10
12
|
list(): MonitorEntry[];
|