@trevonistrevon/pi-loop 0.4.11 → 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 +20 -9
- 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/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -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 +82 -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 +130 -1087
- 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/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 +7 -4
- package/dist/store.js +129 -49
- 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 +8 -4
- package/dist/task-store.js +102 -33
- 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/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/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -0
- package/src/coordinator.ts +115 -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 +104 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +134 -1147
- 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/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 +132 -50
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +103 -31
- 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,148 @@
|
|
|
1
|
+
import type { LoopEntry, Trigger } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
6
|
+
|
|
7
|
+
export interface LoopReducerState {
|
|
8
|
+
nextId: number;
|
|
9
|
+
loopsById: Record<string, LoopEntry>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type LoopReducerEvent =
|
|
13
|
+
| {
|
|
14
|
+
type: "LOOP_CREATED";
|
|
15
|
+
at: number;
|
|
16
|
+
source: ReducerSource;
|
|
17
|
+
entityType?: "loop";
|
|
18
|
+
entityId?: string;
|
|
19
|
+
payload: {
|
|
20
|
+
prompt: string;
|
|
21
|
+
trigger: Trigger;
|
|
22
|
+
recurring: boolean;
|
|
23
|
+
autoTask?: boolean;
|
|
24
|
+
taskBacklog?: boolean;
|
|
25
|
+
readOnly?: boolean;
|
|
26
|
+
maxFires?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type:
|
|
31
|
+
| "LOOP_PAUSED"
|
|
32
|
+
| "LOOP_RESUMED"
|
|
33
|
+
| "LOOP_FIRED"
|
|
34
|
+
| "LOOP_DELETED"
|
|
35
|
+
| "LOOP_MAX_FIRES_REACHED"
|
|
36
|
+
| "LOOP_BACKLOG_EMPTY";
|
|
37
|
+
at: number;
|
|
38
|
+
source: ReducerSource;
|
|
39
|
+
entityType?: "loop";
|
|
40
|
+
entityId?: string;
|
|
41
|
+
payload: { id: string };
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: "LOOP_EXPIRED";
|
|
45
|
+
at: number;
|
|
46
|
+
source: ReducerSource;
|
|
47
|
+
entityType?: "loop";
|
|
48
|
+
entityId?: string;
|
|
49
|
+
payload: {
|
|
50
|
+
id: string;
|
|
51
|
+
reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type LoopReducerEffect =
|
|
56
|
+
| {
|
|
57
|
+
type: "PERSIST_LOOP";
|
|
58
|
+
entityType: "loop";
|
|
59
|
+
entityId: string;
|
|
60
|
+
payload: { loop: LoopEntry };
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
type: "DELETE_LOOP";
|
|
64
|
+
entityType: "loop";
|
|
65
|
+
entityId: string;
|
|
66
|
+
payload: { id: string };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface LoopReduceResult {
|
|
70
|
+
state: LoopReducerState;
|
|
71
|
+
effects: LoopReducerEffect[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cloneState(state: LoopReducerState): LoopReducerState {
|
|
75
|
+
return {
|
|
76
|
+
nextId: state.nextId,
|
|
77
|
+
loopsById: { ...state.loopsById },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function reduceLoopState(state: LoopReducerState, event: LoopReducerEvent): LoopReduceResult {
|
|
82
|
+
if (event.type === "LOOP_CREATED") {
|
|
83
|
+
const next = cloneState(state);
|
|
84
|
+
const id = String(next.nextId++);
|
|
85
|
+
const loop: LoopEntry = {
|
|
86
|
+
id,
|
|
87
|
+
prompt: event.payload.prompt,
|
|
88
|
+
trigger: event.payload.trigger,
|
|
89
|
+
status: "active",
|
|
90
|
+
recurring: event.payload.recurring,
|
|
91
|
+
createdAt: event.at,
|
|
92
|
+
updatedAt: event.at,
|
|
93
|
+
expiresAt: event.at + MAX_LOOP_EXPIRY_MS,
|
|
94
|
+
autoTask: event.payload.autoTask,
|
|
95
|
+
taskBacklog: event.payload.taskBacklog,
|
|
96
|
+
readOnly: event.payload.readOnly,
|
|
97
|
+
maxFires: event.payload.maxFires,
|
|
98
|
+
fireCount: 0,
|
|
99
|
+
};
|
|
100
|
+
next.loopsById[id] = loop;
|
|
101
|
+
return {
|
|
102
|
+
state: next,
|
|
103
|
+
effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const id = event.payload.id;
|
|
108
|
+
const current = state.loopsById[id];
|
|
109
|
+
if (!current) return { state, effects: [] };
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
event.type === "LOOP_DELETED"
|
|
113
|
+
|| event.type === "LOOP_MAX_FIRES_REACHED"
|
|
114
|
+
|| event.type === "LOOP_EXPIRED"
|
|
115
|
+
|| event.type === "LOOP_BACKLOG_EMPTY"
|
|
116
|
+
) {
|
|
117
|
+
const next = cloneState(state);
|
|
118
|
+
delete next.loopsById[id];
|
|
119
|
+
return {
|
|
120
|
+
state: next,
|
|
121
|
+
effects: [{ type: "DELETE_LOOP", entityType: "loop", entityId: id, payload: { id } }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const next = cloneState(state);
|
|
126
|
+
const loop: LoopEntry = { ...current };
|
|
127
|
+
|
|
128
|
+
if (event.type === "LOOP_PAUSED") {
|
|
129
|
+
loop.status = "paused";
|
|
130
|
+
loop.updatedAt = event.at;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event.type === "LOOP_RESUMED") {
|
|
134
|
+
loop.status = "active";
|
|
135
|
+
loop.updatedAt = event.at;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (event.type === "LOOP_FIRED") {
|
|
139
|
+
loop.fireCount = (loop.fireCount ?? 0) + 1;
|
|
140
|
+
loop.updatedAt = event.at;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
next.loopsById[id] = loop;
|
|
144
|
+
return {
|
|
145
|
+
state: next,
|
|
146
|
+
effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
|
|
2
|
+
|
|
3
|
+
export type MonitorCompletionEvent = ReducerEvent<
|
|
4
|
+
"MONITOR_ONDONE_TRIGGERED",
|
|
5
|
+
{ loopId: string; monitorId: string }
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export type MonitorCompletionEffect = ReducerEffect<
|
|
9
|
+
"DELIVER_MONITOR_ONDONE_WAKE",
|
|
10
|
+
{ loopId: string; monitorId: string }
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
export function reduceMonitorCompletionEvent(event: MonitorCompletionEvent): MonitorCompletionEffect[] {
|
|
14
|
+
if (event.type !== "MONITOR_ONDONE_TRIGGERED") return [];
|
|
15
|
+
return [{
|
|
16
|
+
type: "DELIVER_MONITOR_ONDONE_WAKE",
|
|
17
|
+
entityType: "monitor",
|
|
18
|
+
entityId: event.payload.monitorId,
|
|
19
|
+
payload: {
|
|
20
|
+
loopId: event.payload.loopId,
|
|
21
|
+
monitorId: event.payload.monitorId,
|
|
22
|
+
},
|
|
23
|
+
}];
|
|
24
|
+
}
|
package/src/monitor-manager.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
type MonitorReducerEvent,
|
|
6
|
+
type MonitorReducerState,
|
|
7
|
+
reduceMonitorState,
|
|
8
|
+
} from "./monitor-reducer.js";
|
|
4
9
|
import type { MonitorEntry, MonitorProcess } from "./types.js";
|
|
5
10
|
|
|
6
11
|
export class MonitorManager {
|
|
@@ -9,18 +14,45 @@ export class MonitorManager {
|
|
|
9
14
|
|
|
10
15
|
constructor(private pi: ExtensionAPI) {}
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
timeout,
|
|
19
|
-
status: "running",
|
|
20
|
-
startedAt: Date.now(),
|
|
21
|
-
outputLines: 0,
|
|
22
|
-
outputBuffer: [],
|
|
17
|
+
private toReducerState(): MonitorReducerState {
|
|
18
|
+
return {
|
|
19
|
+
nextId: this.nextId,
|
|
20
|
+
monitorsById: Object.fromEntries(
|
|
21
|
+
Array.from(this.processes.entries()).map(([id, process]) => [id, process.entry]),
|
|
22
|
+
),
|
|
23
23
|
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private applyReducerEvent(event: MonitorReducerEvent) {
|
|
27
|
+
const result = reduceMonitorState(this.toReducerState(), event);
|
|
28
|
+
this.nextId = result.state.nextId;
|
|
29
|
+
for (const [id, process] of [...this.processes.entries()]) {
|
|
30
|
+
const updated = result.state.monitorsById[id];
|
|
31
|
+
if (!updated) {
|
|
32
|
+
this.processes.delete(id);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
process.entry = updated;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
create(command: string, description?: string, timeout = 300000): MonitorEntry {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const result = reduceMonitorState(this.toReducerState(), {
|
|
43
|
+
type: "MONITOR_CREATED",
|
|
44
|
+
at: now,
|
|
45
|
+
source: "tool",
|
|
46
|
+
entityType: "monitor",
|
|
47
|
+
payload: {
|
|
48
|
+
command,
|
|
49
|
+
description,
|
|
50
|
+
timeout,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
this.nextId = result.state.nextId;
|
|
54
|
+
const id = String(this.nextId - 1);
|
|
55
|
+
const entry = result.state.monitorsById[id]!;
|
|
24
56
|
|
|
25
57
|
const abortController = new AbortController();
|
|
26
58
|
const child = spawn("sh", ["-c", command], {
|
|
@@ -42,8 +74,14 @@ export class MonitorManager {
|
|
|
42
74
|
const lines = data.toString().split("\n");
|
|
43
75
|
for (const line of lines) {
|
|
44
76
|
if (line.length === 0) continue;
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
this.applyReducerEvent({
|
|
78
|
+
type: "MONITOR_OUTPUT",
|
|
79
|
+
at: Date.now(),
|
|
80
|
+
source: "monitor",
|
|
81
|
+
entityType: "monitor",
|
|
82
|
+
entityId: id,
|
|
83
|
+
payload: { id, line },
|
|
84
|
+
});
|
|
47
85
|
this.pi.events.emit("monitor:output", {
|
|
48
86
|
monitorId: id,
|
|
49
87
|
line,
|
|
@@ -56,8 +94,14 @@ export class MonitorManager {
|
|
|
56
94
|
const lines = data.toString().split("\n");
|
|
57
95
|
for (const line of lines) {
|
|
58
96
|
if (line.length === 0) continue;
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
this.applyReducerEvent({
|
|
98
|
+
type: "MONITOR_OUTPUT",
|
|
99
|
+
at: Date.now(),
|
|
100
|
+
source: "monitor",
|
|
101
|
+
entityType: "monitor",
|
|
102
|
+
entityId: id,
|
|
103
|
+
payload: { id, line },
|
|
104
|
+
});
|
|
61
105
|
this.pi.events.emit("monitor:output", {
|
|
62
106
|
monitorId: id,
|
|
63
107
|
line,
|
|
@@ -67,13 +111,22 @@ export class MonitorManager {
|
|
|
67
111
|
});
|
|
68
112
|
|
|
69
113
|
const finish = (code: number | null, status: "completed" | "error") => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
this.applyReducerEvent({
|
|
115
|
+
type: status === "completed" ? "MONITOR_COMPLETED" : "MONITOR_ERRORED",
|
|
116
|
+
at: Date.now(),
|
|
117
|
+
source: "monitor",
|
|
118
|
+
entityType: "monitor",
|
|
119
|
+
entityId: id,
|
|
120
|
+
payload: {
|
|
121
|
+
id,
|
|
122
|
+
exitCode: code ?? undefined,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const current = this.get(id)!;
|
|
73
126
|
this.pi.events.emit(status === "completed" ? "monitor:done" : "monitor:error", {
|
|
74
127
|
monitorId: id,
|
|
75
128
|
exitCode: code,
|
|
76
|
-
outputLines:
|
|
129
|
+
outputLines: current.outputLines,
|
|
77
130
|
});
|
|
78
131
|
if (status === "completed") {
|
|
79
132
|
for (const callback of bp.completionCallbacks) callback();
|
|
@@ -83,19 +136,37 @@ export class MonitorManager {
|
|
|
83
136
|
bp.waiters = [];
|
|
84
137
|
// Remove completed/errored monitors after a brief delay so tool
|
|
85
138
|
// consumers have time to read the final state via MonitorList.
|
|
86
|
-
setTimeout(() => {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
this.applyReducerEvent({
|
|
141
|
+
type: "MONITOR_PRUNED",
|
|
142
|
+
at: Date.now(),
|
|
143
|
+
source: "system",
|
|
144
|
+
entityType: "monitor",
|
|
145
|
+
entityId: id,
|
|
146
|
+
payload: { id },
|
|
147
|
+
});
|
|
148
|
+
}, 30000);
|
|
87
149
|
};
|
|
88
150
|
|
|
89
151
|
child.on("close", (code) => {
|
|
90
|
-
if (entry.status === "running") {
|
|
152
|
+
if (bp.entry.status === "running") {
|
|
91
153
|
finish(code, code === 0 ? "completed" : "error");
|
|
92
154
|
}
|
|
93
155
|
});
|
|
94
156
|
|
|
95
157
|
child.on("error", (err) => {
|
|
96
|
-
if (entry.status === "running") {
|
|
97
|
-
|
|
98
|
-
|
|
158
|
+
if (bp.entry.status === "running") {
|
|
159
|
+
this.applyReducerEvent({
|
|
160
|
+
type: "MONITOR_ERRORED",
|
|
161
|
+
at: Date.now(),
|
|
162
|
+
source: "monitor",
|
|
163
|
+
entityType: "monitor",
|
|
164
|
+
entityId: id,
|
|
165
|
+
payload: {
|
|
166
|
+
id,
|
|
167
|
+
error: err.message,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
99
170
|
this.pi.events.emit("monitor:error", {
|
|
100
171
|
monitorId: id,
|
|
101
172
|
error: err.message,
|
|
@@ -108,7 +179,7 @@ export class MonitorManager {
|
|
|
108
179
|
|
|
109
180
|
if (timeout > 0) {
|
|
110
181
|
setTimeout(() => {
|
|
111
|
-
if (entry.status === "running") {
|
|
182
|
+
if (bp.entry.status === "running") {
|
|
112
183
|
this.stop(id);
|
|
113
184
|
}
|
|
114
185
|
}, timeout);
|
|
@@ -133,7 +204,17 @@ export class MonitorManager {
|
|
|
133
204
|
const bp = this.processes.get(id);
|
|
134
205
|
if (!bp || bp.entry.status !== "running") return false;
|
|
135
206
|
|
|
136
|
-
|
|
207
|
+
this.applyReducerEvent({
|
|
208
|
+
type: "MONITOR_STOPPED",
|
|
209
|
+
at: Date.now(),
|
|
210
|
+
source: "tool",
|
|
211
|
+
entityType: "monitor",
|
|
212
|
+
entityId: id,
|
|
213
|
+
payload: {
|
|
214
|
+
id,
|
|
215
|
+
reason: "manual",
|
|
216
|
+
},
|
|
217
|
+
});
|
|
137
218
|
bp.proc.kill("SIGTERM");
|
|
138
219
|
|
|
139
220
|
await new Promise<void>((resolve) => {
|
|
@@ -148,7 +229,6 @@ export class MonitorManager {
|
|
|
148
229
|
});
|
|
149
230
|
});
|
|
150
231
|
|
|
151
|
-
bp.entry.completedAt = Date.now();
|
|
152
232
|
bp.completionCallbacks = [];
|
|
153
233
|
for (const resolve of bp.waiters) resolve();
|
|
154
234
|
bp.waiters = [];
|
|
@@ -163,6 +243,14 @@ export class MonitorManager {
|
|
|
163
243
|
return true;
|
|
164
244
|
}
|
|
165
245
|
if (bp.entry.status !== "running") return false;
|
|
246
|
+
this.applyReducerEvent({
|
|
247
|
+
type: "MONITOR_ONDONE_REGISTERED",
|
|
248
|
+
at: Date.now(),
|
|
249
|
+
source: "tool",
|
|
250
|
+
entityType: "monitor",
|
|
251
|
+
entityId: id,
|
|
252
|
+
payload: { id },
|
|
253
|
+
});
|
|
166
254
|
bp.completionCallbacks.push(callback);
|
|
167
255
|
return true;
|
|
168
256
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { MonitorEntry } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
4
|
+
|
|
5
|
+
export interface MonitorReducerEntry extends MonitorEntry {
|
|
6
|
+
onDoneRegistered?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MonitorReducerState {
|
|
10
|
+
nextId: number;
|
|
11
|
+
monitorsById: Record<string, MonitorReducerEntry>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type MonitorReducerEvent =
|
|
15
|
+
| {
|
|
16
|
+
type: "MONITOR_CREATED";
|
|
17
|
+
at: number;
|
|
18
|
+
source: ReducerSource;
|
|
19
|
+
entityType?: "monitor";
|
|
20
|
+
entityId?: string;
|
|
21
|
+
payload: {
|
|
22
|
+
command: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
timeout: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: "MONITOR_OUTPUT";
|
|
29
|
+
at: number;
|
|
30
|
+
source: ReducerSource;
|
|
31
|
+
entityType?: "monitor";
|
|
32
|
+
entityId?: string;
|
|
33
|
+
payload: {
|
|
34
|
+
id: string;
|
|
35
|
+
line: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
type: "MONITOR_COMPLETED" | "MONITOR_ERRORED";
|
|
40
|
+
at: number;
|
|
41
|
+
source: ReducerSource;
|
|
42
|
+
entityType?: "monitor";
|
|
43
|
+
entityId?: string;
|
|
44
|
+
payload: {
|
|
45
|
+
id: string;
|
|
46
|
+
exitCode?: number;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
type: "MONITOR_STOPPED";
|
|
52
|
+
at: number;
|
|
53
|
+
source: ReducerSource;
|
|
54
|
+
entityType?: "monitor";
|
|
55
|
+
entityId?: string;
|
|
56
|
+
payload: {
|
|
57
|
+
id: string;
|
|
58
|
+
reason: "manual" | "timeout";
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
type: "MONITOR_PRUNED" | "MONITOR_ONDONE_REGISTERED";
|
|
63
|
+
at: number;
|
|
64
|
+
source: ReducerSource;
|
|
65
|
+
entityType?: "monitor";
|
|
66
|
+
entityId?: string;
|
|
67
|
+
payload: {
|
|
68
|
+
id: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type MonitorReducerEffect =
|
|
73
|
+
| {
|
|
74
|
+
type: "PERSIST_MONITOR";
|
|
75
|
+
entityType: "monitor";
|
|
76
|
+
entityId: string;
|
|
77
|
+
payload: { monitor: MonitorReducerEntry };
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
type: "DELETE_MONITOR";
|
|
81
|
+
entityType: "monitor";
|
|
82
|
+
entityId: string;
|
|
83
|
+
payload: { id: string };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export interface MonitorReduceResult {
|
|
87
|
+
state: MonitorReducerState;
|
|
88
|
+
effects: MonitorReducerEffect[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cloneState(state: MonitorReducerState): MonitorReducerState {
|
|
92
|
+
return {
|
|
93
|
+
nextId: state.nextId,
|
|
94
|
+
monitorsById: { ...state.monitorsById },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function reduceMonitorState(state: MonitorReducerState, event: MonitorReducerEvent): MonitorReduceResult {
|
|
99
|
+
if (event.type === "MONITOR_CREATED") {
|
|
100
|
+
const next = cloneState(state);
|
|
101
|
+
const id = String(next.nextId++);
|
|
102
|
+
const monitor: MonitorReducerEntry = {
|
|
103
|
+
id,
|
|
104
|
+
command: event.payload.command,
|
|
105
|
+
description: event.payload.description,
|
|
106
|
+
timeout: event.payload.timeout,
|
|
107
|
+
status: "running",
|
|
108
|
+
startedAt: event.at,
|
|
109
|
+
outputLines: 0,
|
|
110
|
+
outputBuffer: [],
|
|
111
|
+
};
|
|
112
|
+
next.monitorsById[id] = monitor;
|
|
113
|
+
return {
|
|
114
|
+
state: next,
|
|
115
|
+
effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const id = event.payload.id;
|
|
120
|
+
const current = state.monitorsById[id];
|
|
121
|
+
if (!current) return { state, effects: [] };
|
|
122
|
+
|
|
123
|
+
if (event.type === "MONITOR_PRUNED") {
|
|
124
|
+
const next = cloneState(state);
|
|
125
|
+
delete next.monitorsById[id];
|
|
126
|
+
return {
|
|
127
|
+
state: next,
|
|
128
|
+
effects: [{ type: "DELETE_MONITOR", entityType: "monitor", entityId: id, payload: { id } }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const next = cloneState(state);
|
|
133
|
+
const monitor: MonitorReducerEntry = { ...current };
|
|
134
|
+
|
|
135
|
+
if (event.type === "MONITOR_OUTPUT") {
|
|
136
|
+
monitor.outputLines++;
|
|
137
|
+
if (monitor.outputBuffer.length < 200) monitor.outputBuffer = [...monitor.outputBuffer, event.payload.line];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (event.type === "MONITOR_COMPLETED") {
|
|
141
|
+
monitor.status = "completed";
|
|
142
|
+
monitor.exitCode = event.payload.exitCode;
|
|
143
|
+
monitor.completedAt = event.at;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event.type === "MONITOR_ERRORED") {
|
|
147
|
+
monitor.status = "error";
|
|
148
|
+
if (event.payload.exitCode !== undefined) monitor.exitCode = event.payload.exitCode;
|
|
149
|
+
monitor.completedAt = event.at;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (event.type === "MONITOR_STOPPED") {
|
|
153
|
+
monitor.status = "stopped";
|
|
154
|
+
monitor.completedAt = event.at;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (event.type === "MONITOR_ONDONE_REGISTERED") {
|
|
158
|
+
monitor.onDoneRegistered = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
next.monitorsById[id] = monitor;
|
|
162
|
+
return {
|
|
163
|
+
state: next,
|
|
164
|
+
effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
2
|
+
|
|
3
|
+
export interface ReducerNotification {
|
|
4
|
+
key: string;
|
|
5
|
+
loopId: string;
|
|
6
|
+
message: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
trigger: unknown;
|
|
9
|
+
recurring?: boolean;
|
|
10
|
+
autoTask?: boolean;
|
|
11
|
+
readOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NotificationReducerState {
|
|
15
|
+
notificationsByKey: Record<string, ReducerNotification>;
|
|
16
|
+
agentRunning: boolean;
|
|
17
|
+
hasPendingMessages: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type NotificationReducerEvent =
|
|
21
|
+
| {
|
|
22
|
+
type: "NOTIFICATION_QUEUED";
|
|
23
|
+
at: number;
|
|
24
|
+
source: ReducerSource;
|
|
25
|
+
entityType?: "notification";
|
|
26
|
+
entityId?: string;
|
|
27
|
+
payload: { notification: ReducerNotification };
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "NOTIFICATION_DROPPED";
|
|
31
|
+
at: number;
|
|
32
|
+
source: ReducerSource;
|
|
33
|
+
entityType?: "notification";
|
|
34
|
+
entityId?: string;
|
|
35
|
+
payload: {
|
|
36
|
+
key: string;
|
|
37
|
+
reason: "zero_pending_tasks" | "session_switch" | "session_shutdown" | "superseded";
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: "NOTIFICATION_CLEARED";
|
|
42
|
+
at: number;
|
|
43
|
+
source: ReducerSource;
|
|
44
|
+
entityType?: "notification";
|
|
45
|
+
entityId?: string;
|
|
46
|
+
payload: { reason: "session_switch" | "session_shutdown" };
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
type: "NOTIFICATION_FLUSH_REQUESTED";
|
|
50
|
+
at: number;
|
|
51
|
+
source: ReducerSource;
|
|
52
|
+
entityType?: "notification";
|
|
53
|
+
entityId?: string;
|
|
54
|
+
payload: { ignorePendingMessages?: boolean };
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: "NOTIFICATION_RUNTIME_UPDATED";
|
|
58
|
+
at: number;
|
|
59
|
+
source: ReducerSource;
|
|
60
|
+
entityType?: "notification";
|
|
61
|
+
entityId?: string;
|
|
62
|
+
payload: { agentRunning: boolean; hasPendingMessages: boolean };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type NotificationReducerEffect =
|
|
66
|
+
| {
|
|
67
|
+
type: "REQUEST_NOTIFICATION_FLUSH";
|
|
68
|
+
payload: Record<string, never>;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: "DELIVER_NOTIFICATION";
|
|
72
|
+
entityType: "notification";
|
|
73
|
+
entityId: string;
|
|
74
|
+
payload: { notification: ReducerNotification };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface NotificationReduceResult {
|
|
78
|
+
state: NotificationReducerState;
|
|
79
|
+
effects: NotificationReducerEffect[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cloneState(state: NotificationReducerState): NotificationReducerState {
|
|
83
|
+
return {
|
|
84
|
+
notificationsByKey: { ...state.notificationsByKey },
|
|
85
|
+
agentRunning: state.agentRunning,
|
|
86
|
+
hasPendingMessages: state.hasPendingMessages,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function reduceNotificationState(
|
|
91
|
+
state: NotificationReducerState,
|
|
92
|
+
event: NotificationReducerEvent,
|
|
93
|
+
): NotificationReduceResult {
|
|
94
|
+
if (event.type === "NOTIFICATION_QUEUED") {
|
|
95
|
+
const next = cloneState(state);
|
|
96
|
+
next.notificationsByKey[event.payload.notification.key] = event.payload.notification;
|
|
97
|
+
return {
|
|
98
|
+
state: next,
|
|
99
|
+
effects: [{ type: "REQUEST_NOTIFICATION_FLUSH", payload: {} }],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === "NOTIFICATION_DROPPED") {
|
|
104
|
+
const next = cloneState(state);
|
|
105
|
+
delete next.notificationsByKey[event.payload.key];
|
|
106
|
+
return { state: next, effects: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (event.type === "NOTIFICATION_CLEARED") {
|
|
110
|
+
return {
|
|
111
|
+
state: {
|
|
112
|
+
...state,
|
|
113
|
+
notificationsByKey: {},
|
|
114
|
+
},
|
|
115
|
+
effects: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.type === "NOTIFICATION_RUNTIME_UPDATED") {
|
|
120
|
+
return {
|
|
121
|
+
state: {
|
|
122
|
+
...state,
|
|
123
|
+
agentRunning: event.payload.agentRunning,
|
|
124
|
+
hasPendingMessages: event.payload.hasPendingMessages,
|
|
125
|
+
},
|
|
126
|
+
effects: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (event.type === "NOTIFICATION_FLUSH_REQUESTED") {
|
|
131
|
+
if (state.agentRunning) return { state, effects: [] };
|
|
132
|
+
if (!event.payload.ignorePendingMessages && state.hasPendingMessages) {
|
|
133
|
+
return { state, effects: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const queued = Object.values(state.notificationsByKey)
|
|
137
|
+
.sort((left, right) => left.timestamp - right.timestamp);
|
|
138
|
+
const nextNotification = queued[0];
|
|
139
|
+
if (!nextNotification) return { state, effects: [] };
|
|
140
|
+
|
|
141
|
+
const next = cloneState(state);
|
|
142
|
+
delete next.notificationsByKey[nextNotification.key];
|
|
143
|
+
return {
|
|
144
|
+
state: next,
|
|
145
|
+
effects: [{
|
|
146
|
+
type: "DELIVER_NOTIFICATION",
|
|
147
|
+
entityType: "notification",
|
|
148
|
+
entityId: nextNotification.key,
|
|
149
|
+
payload: { notification: nextNotification },
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { state, effects: [] };
|
|
155
|
+
}
|