@trevonistrevon/pi-loop 0.4.10 → 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 +11 -1
- 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 +182 -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 +209 -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;
|
|
@@ -403,6 +509,18 @@ export default function (pi) {
|
|
|
403
509
|
showPersistedLoops(isResume);
|
|
404
510
|
widget.update();
|
|
405
511
|
});
|
|
512
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
513
|
+
_latestCtx = ctx;
|
|
514
|
+
widget.setUICtx(ctx.ui);
|
|
515
|
+
if (event?.toolName !== "bash" || event?.isError)
|
|
516
|
+
return;
|
|
517
|
+
const command = event?.args?.command ?? event?.input?.command;
|
|
518
|
+
if (typeof command !== "string")
|
|
519
|
+
return;
|
|
520
|
+
if (!/^\s*git\s+commit\b/i.test(command))
|
|
521
|
+
return;
|
|
522
|
+
await cleanDoneTasks();
|
|
523
|
+
});
|
|
406
524
|
// ── Dynamic loop pump — fires cron/hybrid loops on idle instead of wall-clock timers ──
|
|
407
525
|
async function pumpLoops() {
|
|
408
526
|
const pendingTasks = new Map();
|
|
@@ -572,12 +690,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
572
690
|
});
|
|
573
691
|
function handleMonitorDoneLoop(doneLoop, monitorId) {
|
|
574
692
|
const deliver = () => {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
+
});
|
|
581
701
|
};
|
|
582
702
|
const registered = monitorManager.onComplete(monitorId, deliver);
|
|
583
703
|
if (registered)
|
|
@@ -1000,6 +1120,25 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1000
1120
|
widget.update();
|
|
1001
1121
|
return { entry, created: true };
|
|
1002
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
|
+
}
|
|
1003
1142
|
async function createNativeTaskInteractively(ui) {
|
|
1004
1143
|
if (!nativeTaskStore) {
|
|
1005
1144
|
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
@@ -1016,11 +1155,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1016
1155
|
description: entry.description,
|
|
1017
1156
|
status: entry.status,
|
|
1018
1157
|
});
|
|
1019
|
-
const
|
|
1158
|
+
const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1020
1159
|
widget.update();
|
|
1021
1160
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
1022
|
-
if (
|
|
1023
|
-
ui.notify(`Worker loop #${
|
|
1161
|
+
if (backlog.created && backlog.entry) {
|
|
1162
|
+
ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
1024
1163
|
}
|
|
1025
1164
|
}
|
|
1026
1165
|
async function viewNativeTasks(ui) {
|
|
@@ -1081,7 +1220,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1081
1220
|
ui.notify(`Task #${task.id} reopened`, "info");
|
|
1082
1221
|
}
|
|
1083
1222
|
widget.update();
|
|
1084
|
-
await
|
|
1223
|
+
await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1085
1224
|
return viewNativeTasks(ui);
|
|
1086
1225
|
}
|
|
1087
1226
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
@@ -1107,11 +1246,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1107
1246
|
description: entry.description,
|
|
1108
1247
|
status: entry.status,
|
|
1109
1248
|
});
|
|
1110
|
-
const
|
|
1249
|
+
const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
|
|
1111
1250
|
widget.update();
|
|
1112
1251
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
1113
|
-
if (
|
|
1114
|
-
ctx.ui.notify(`Worker loop #${
|
|
1252
|
+
if (backlog.created && backlog.entry) {
|
|
1253
|
+
ctx.ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
1115
1254
|
}
|
|
1116
1255
|
return;
|
|
1117
1256
|
}
|
|
@@ -1144,10 +1283,10 @@ Fields:
|
|
|
1144
1283
|
description: entry.description,
|
|
1145
1284
|
status: entry.status,
|
|
1146
1285
|
});
|
|
1147
|
-
const
|
|
1286
|
+
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1148
1287
|
widget.update();
|
|
1149
|
-
const autoLoopMsg =
|
|
1150
|
-
? `\nWorker loop #${
|
|
1288
|
+
const autoLoopMsg = backlog.created && backlog.entry
|
|
1289
|
+
? `\nWorker loop #${backlog.entry.id} auto-created`
|
|
1151
1290
|
: "";
|
|
1152
1291
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
1153
1292
|
},
|
|
@@ -1204,9 +1343,12 @@ Parameters: id (required), status, subject, description`,
|
|
|
1204
1343
|
if (!entry)
|
|
1205
1344
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
1206
1345
|
widget.update();
|
|
1207
|
-
await
|
|
1346
|
+
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1208
1347
|
const statusMsg = status ? ` → ${status}` : "";
|
|
1209
|
-
|
|
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}`));
|
|
1210
1352
|
},
|
|
1211
1353
|
});
|
|
1212
1354
|
pi.registerTool({
|
|
@@ -1220,7 +1362,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1220
1362
|
const deleted = taskStore.delete(params.id);
|
|
1221
1363
|
widget.update();
|
|
1222
1364
|
if (deleted) {
|
|
1223
|
-
await
|
|
1365
|
+
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
1224
1366
|
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
1225
1367
|
}
|
|
1226
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[];
|