@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.
- package/README.md +11 -10
- 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/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 +4 -0
- package/dist/index.js +125 -1212
- 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 +5 -4
- package/dist/store.js +50 -44
- package/dist/task-store.d.ts +6 -4
- package/dist/task-store.js +64 -47
- 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/package.json +1 -1
- package/src/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -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 +5 -0
- package/src/index.ts +129 -1299
- 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 +51 -45
- package/src/task-store.ts +61 -46
- 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
package/README.md
CHANGED
|
@@ -77,37 +77,38 @@ Works with [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks). Pass `a
|
|
|
77
77
|
|
|
78
78
|
If `pi-tasks` does not respond during startup detection, `pi-loop` registers a native fallback task system for the session:
|
|
79
79
|
|
|
80
|
-
-
|
|
80
|
+
- session- or project-scoped task files under `.pi/tasks/` depending on `PI_LOOP_SCOPE`
|
|
81
81
|
- `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete`
|
|
82
82
|
- `/tasks` interactive viewer
|
|
83
|
-
- compact
|
|
83
|
+
- compact status-line task tracking
|
|
84
84
|
|
|
85
85
|
This fallback is session-sticky: `pi-loop` decides once at startup whether `pi-tasks` or native tasks own task management for that session.
|
|
86
86
|
|
|
87
|
-
##
|
|
87
|
+
## Status line
|
|
88
88
|
|
|
89
|
-
`pi-loop` keeps a compact persistent
|
|
89
|
+
`pi-loop` keeps a compact persistent status line in the TUI.
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
When active work exists, it shows a single focus-friendly line such as:
|
|
92
92
|
|
|
93
93
|
```text
|
|
94
|
-
none
|
|
95
94
|
1 loop · 1 monitor
|
|
96
95
|
2 tasks | active: Fix deploy polling
|
|
97
96
|
1 loop · 2 monitors · 3 tasks | next: Update README
|
|
98
97
|
```
|
|
99
98
|
|
|
100
|
-
|
|
99
|
+
When no loops, monitors, or native tasks are active, the status line clears completely.
|
|
100
|
+
|
|
101
|
+
Only task counts and the single active/next task are shown there so attention stays on what is currently happening. Use `LoopList`, `MonitorList`, and `/tasks` for detail.
|
|
101
102
|
|
|
102
103
|
## Configuration
|
|
103
104
|
|
|
104
105
|
| Variable | Effect | Default |
|
|
105
106
|
|---|---|---|
|
|
106
|
-
| `PI_LOOP` | Store path. `off` to disable, absolute or project-relative path |
|
|
107
|
+
| `PI_LOOP` | Store path override. `off` to disable, absolute or project-relative path | unset → derived from `PI_LOOP_SCOPE` |
|
|
107
108
|
| `PI_LOOP_SCOPE` | `memory` (ephemeral), `session` (per-session file), `project` (shared) | `session` |
|
|
108
109
|
| `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
|
|
109
110
|
|
|
110
|
-
In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
|
|
111
|
+
In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/loops/loops-<sessionId>.json` and `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
|
|
111
112
|
|
|
112
113
|
### Recommended scope policy
|
|
113
114
|
|
|
@@ -117,7 +118,7 @@ Keep `PI_LOOP_SCOPE=session` as the default.
|
|
|
117
118
|
- `memory` is best for disposable scratch work, tests, or situations where you explicitly do not want any persisted loop/task state.
|
|
118
119
|
- `project` should be opt-in for intentionally shared automation, because it allows multiple sessions in the same repo to see the same persisted state.
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
|
|
122
123
|
## Limits
|
|
123
124
|
|
|
@@ -0,0 +1,22 @@
|
|
|
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, options?: Partial<LoopEntry>): LoopEntry;
|
|
7
|
+
pause(id: string): LoopEntry | undefined;
|
|
8
|
+
resume(id: string): LoopEntry | undefined;
|
|
9
|
+
delete(id: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
interface TriggerSystemLike {
|
|
12
|
+
add(entry: LoopEntry): void;
|
|
13
|
+
remove(id: string): void;
|
|
14
|
+
}
|
|
15
|
+
export interface LoopCommandOptions {
|
|
16
|
+
pi: ExtensionAPI;
|
|
17
|
+
getStore: () => LoopStoreLike;
|
|
18
|
+
getTriggerSystem: () => TriggerSystemLike;
|
|
19
|
+
updateWidget: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function registerLoopCommand(options: LoopCommandOptions): void;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { parseInterval } from "../loop-parse.js";
|
|
2
|
+
export function registerLoopCommand(options) {
|
|
3
|
+
const { pi, getStore, getTriggerSystem, updateWidget } = options;
|
|
4
|
+
async function scheduleLoop(ui, prompt) {
|
|
5
|
+
const p = prompt || await ui.input("Prompt (what should the agent check?)");
|
|
6
|
+
if (!p)
|
|
7
|
+
return;
|
|
8
|
+
const interval = await ui.input("Interval (e.g., 5m, 2h, 1d)");
|
|
9
|
+
if (!interval)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = parseInterval(interval);
|
|
13
|
+
const trigger = { type: "cron", schedule: parsed.cron };
|
|
14
|
+
const entry = getStore().create(trigger, p, { recurring: true });
|
|
15
|
+
getTriggerSystem().add(entry);
|
|
16
|
+
updateWidget();
|
|
17
|
+
ui.notify(`Loop #${entry.id} created: every ${parsed.description}`, "info");
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
ui.notify(err.message, "error");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function eventLoop(ui, prompt) {
|
|
24
|
+
const p = prompt || await ui.input("Prompt");
|
|
25
|
+
if (!p)
|
|
26
|
+
return;
|
|
27
|
+
const source = await ui.input("Pi event source (e.g., tool_execution_start, before_agent_start)");
|
|
28
|
+
if (!source)
|
|
29
|
+
return;
|
|
30
|
+
const trigger = { type: "event", source };
|
|
31
|
+
const entry = getStore().create(trigger, p, { recurring: true });
|
|
32
|
+
getTriggerSystem().add(entry);
|
|
33
|
+
updateWidget();
|
|
34
|
+
ui.notify(`Event loop #${entry.id} created: fires on "${source}"`, "info");
|
|
35
|
+
}
|
|
36
|
+
async function viewLoops(ui) {
|
|
37
|
+
const loops = getStore().list();
|
|
38
|
+
if (loops.length === 0) {
|
|
39
|
+
await ui.select("No active loops", ["< Back"]);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const choices = loops.map((l) => {
|
|
43
|
+
const icon = l.status === "active" ? "*" : l.status === "paused" ? "-" : "x";
|
|
44
|
+
const triggerDesc = l.trigger.type === "cron"
|
|
45
|
+
? `cron: ${l.trigger.schedule}`
|
|
46
|
+
: l.trigger.type === "event"
|
|
47
|
+
? `event: ${l.trigger.source}`
|
|
48
|
+
: `hybrid: ${l.trigger.cron}`;
|
|
49
|
+
return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${triggerDesc})`;
|
|
50
|
+
});
|
|
51
|
+
choices.push("< Back");
|
|
52
|
+
const selected = await ui.select("Active Loops", choices);
|
|
53
|
+
if (!selected || selected === "< Back")
|
|
54
|
+
return;
|
|
55
|
+
const match = selected.match(/#(\d+)/);
|
|
56
|
+
if (match) {
|
|
57
|
+
const entry = getStore().get(match[1]);
|
|
58
|
+
if (entry) {
|
|
59
|
+
const actions = ["x Delete"];
|
|
60
|
+
if (entry.status === "active")
|
|
61
|
+
actions.unshift("- Pause");
|
|
62
|
+
else if (entry.status === "paused")
|
|
63
|
+
actions.unshift("* Resume");
|
|
64
|
+
actions.push("< Back");
|
|
65
|
+
const action = await ui.select(`#${entry.id}: ${entry.prompt}\nTrigger: ${JSON.stringify(entry.trigger)}`, actions);
|
|
66
|
+
if (action === "x Delete") {
|
|
67
|
+
getTriggerSystem().remove(entry.id);
|
|
68
|
+
getStore().delete(entry.id);
|
|
69
|
+
updateWidget();
|
|
70
|
+
ui.notify(`Loop #${entry.id} deleted`, "info");
|
|
71
|
+
}
|
|
72
|
+
else if (action === "- Pause") {
|
|
73
|
+
getStore().pause(entry.id);
|
|
74
|
+
getTriggerSystem().remove(entry.id);
|
|
75
|
+
updateWidget();
|
|
76
|
+
ui.notify(`Loop #${entry.id} paused`, "info");
|
|
77
|
+
}
|
|
78
|
+
else if (action === "* Resume") {
|
|
79
|
+
getStore().resume(entry.id);
|
|
80
|
+
getTriggerSystem().add(entry);
|
|
81
|
+
updateWidget();
|
|
82
|
+
ui.notify(`Loop #${entry.id} resumed`, "info");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return viewLoops(ui);
|
|
87
|
+
}
|
|
88
|
+
async function settings(ui) {
|
|
89
|
+
const loops = getStore().list();
|
|
90
|
+
const active = loops.filter((l) => l.status === "active").length;
|
|
91
|
+
ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
|
|
92
|
+
}
|
|
93
|
+
pi.registerCommand("loop", {
|
|
94
|
+
description: "Create a repeating scheduled task: /loop [interval] [prompt]. E.g., /loop 5m check the deploy, /loop 30s am I still here",
|
|
95
|
+
handler: async (args, ctx) => {
|
|
96
|
+
const trimmed = args.trim();
|
|
97
|
+
const ui = ctx.ui;
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
const choice = await ui.select("Loop", [
|
|
100
|
+
"Create scheduled loop",
|
|
101
|
+
"Create event-triggered loop",
|
|
102
|
+
"View active loops",
|
|
103
|
+
"Settings",
|
|
104
|
+
]);
|
|
105
|
+
if (!choice)
|
|
106
|
+
return;
|
|
107
|
+
if (choice.startsWith("Create scheduled"))
|
|
108
|
+
return scheduleLoop(ui);
|
|
109
|
+
if (choice.startsWith("Create event"))
|
|
110
|
+
return eventLoop(ui);
|
|
111
|
+
if (choice.startsWith("View active"))
|
|
112
|
+
return viewLoops(ui);
|
|
113
|
+
return settings(ui);
|
|
114
|
+
}
|
|
115
|
+
const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
|
|
116
|
+
if (intervalMatch) {
|
|
117
|
+
const interval = intervalMatch[1];
|
|
118
|
+
const prompt = trimmed.slice(intervalMatch[0].length).trim();
|
|
119
|
+
if (!prompt) {
|
|
120
|
+
ui.notify("Provide a prompt after the interval, e.g., /loop 5m check the deploy", "warning");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const parsed = parseInterval(interval);
|
|
125
|
+
const trigger = { type: "cron", schedule: parsed.cron };
|
|
126
|
+
const entry = getStore().create(trigger, prompt, { recurring: true });
|
|
127
|
+
getTriggerSystem().add(entry);
|
|
128
|
+
updateWidget();
|
|
129
|
+
ui.notify(`Loop #${entry.id} created: every ${parsed.description} — ${prompt.slice(0, 50)}`, "info");
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
ui.notify(err.message, "error");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const choice = await ui.select("Loop mode", [
|
|
137
|
+
`Scheduled: "${trimmed.slice(0, 50)}"`,
|
|
138
|
+
`Event-triggered: "${trimmed.slice(0, 50)}"`,
|
|
139
|
+
`Self-paced: "${trimmed.slice(0, 50)}"`,
|
|
140
|
+
]);
|
|
141
|
+
if (!choice)
|
|
142
|
+
return;
|
|
143
|
+
if (choice.startsWith("Event"))
|
|
144
|
+
return eventLoop(ui, trimmed);
|
|
145
|
+
return scheduleLoop(ui, trimmed);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { TaskStore } from "../task-store.js";
|
|
3
|
+
export interface TaskBacklogResult {
|
|
4
|
+
created: boolean;
|
|
5
|
+
entry?: {
|
|
6
|
+
id: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface TasksCommandOptions {
|
|
10
|
+
pi: ExtensionAPI;
|
|
11
|
+
getNativeTaskStore: () => TaskStore | undefined;
|
|
12
|
+
evaluateTaskBacklog: (taskStore: TaskStore, pendingCount: number) => Promise<TaskBacklogResult>;
|
|
13
|
+
updateWidget: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function registerTasksCommand(options: TasksCommandOptions): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export function registerTasksCommand(options) {
|
|
2
|
+
const { pi, getNativeTaskStore, evaluateTaskBacklog, updateWidget } = options;
|
|
3
|
+
async function emitCreated(entry) {
|
|
4
|
+
pi.events.emit("tasks:created", {
|
|
5
|
+
taskId: entry.id,
|
|
6
|
+
subject: entry.subject,
|
|
7
|
+
description: entry.description,
|
|
8
|
+
status: entry.status,
|
|
9
|
+
});
|
|
10
|
+
const taskStore = getNativeTaskStore();
|
|
11
|
+
if (!taskStore)
|
|
12
|
+
return { created: false };
|
|
13
|
+
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
14
|
+
updateWidget();
|
|
15
|
+
return backlog;
|
|
16
|
+
}
|
|
17
|
+
async function createNativeTaskInteractively(ui) {
|
|
18
|
+
const taskStore = getNativeTaskStore();
|
|
19
|
+
if (!taskStore) {
|
|
20
|
+
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const subject = await ui.input("Task subject");
|
|
24
|
+
if (!subject)
|
|
25
|
+
return;
|
|
26
|
+
const description = await ui.input("Task description") || subject;
|
|
27
|
+
const entry = taskStore.create(subject, description);
|
|
28
|
+
const backlog = await emitCreated(entry);
|
|
29
|
+
ui.notify(`Task #${entry.id} created`, "info");
|
|
30
|
+
if (backlog.created && backlog.entry) {
|
|
31
|
+
ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function viewNativeTasks(ui) {
|
|
35
|
+
const taskStore = getNativeTaskStore();
|
|
36
|
+
if (!taskStore) {
|
|
37
|
+
ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const tasks = taskStore.list();
|
|
41
|
+
const choices = tasks.map((task) => {
|
|
42
|
+
const icon = task.status === "in_progress" ? ">" : task.status === "completed" ? "ok" : "*";
|
|
43
|
+
return `${icon} #${task.id} [${task.status}] ${task.subject.slice(0, 60)}`;
|
|
44
|
+
});
|
|
45
|
+
choices.unshift("+ Create task");
|
|
46
|
+
choices.push("< Back");
|
|
47
|
+
const selected = await ui.select("Native Tasks", choices);
|
|
48
|
+
if (!selected || selected === "< Back")
|
|
49
|
+
return;
|
|
50
|
+
if (selected === "+ Create task") {
|
|
51
|
+
await createNativeTaskInteractively(ui);
|
|
52
|
+
return viewNativeTasks(ui);
|
|
53
|
+
}
|
|
54
|
+
const match = selected.match(/#(\d+)/);
|
|
55
|
+
if (!match)
|
|
56
|
+
return viewNativeTasks(ui);
|
|
57
|
+
const task = taskStore.get(match[1]);
|
|
58
|
+
if (!task)
|
|
59
|
+
return viewNativeTasks(ui);
|
|
60
|
+
const actions = ["x Delete"];
|
|
61
|
+
if (task.status === "pending") {
|
|
62
|
+
actions.unshift("ok Complete");
|
|
63
|
+
actions.unshift("> Start");
|
|
64
|
+
}
|
|
65
|
+
else if (task.status === "in_progress") {
|
|
66
|
+
actions.unshift("ok Complete");
|
|
67
|
+
actions.unshift("* Return to pending");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
actions.unshift("* Reopen");
|
|
71
|
+
}
|
|
72
|
+
actions.push("< Back");
|
|
73
|
+
const action = await ui.select(`#${task.id}: ${task.subject}\n\n${task.description}`, actions);
|
|
74
|
+
if (!action || action === "< Back")
|
|
75
|
+
return viewNativeTasks(ui);
|
|
76
|
+
if (action === "x Delete") {
|
|
77
|
+
taskStore.delete(task.id);
|
|
78
|
+
ui.notify(`Task #${task.id} deleted`, "info");
|
|
79
|
+
}
|
|
80
|
+
else if (action === "> Start") {
|
|
81
|
+
taskStore.start(task.id);
|
|
82
|
+
ui.notify(`Task #${task.id} started`, "info");
|
|
83
|
+
}
|
|
84
|
+
else if (action === "ok Complete") {
|
|
85
|
+
taskStore.complete(task.id);
|
|
86
|
+
ui.notify(`Task #${task.id} completed`, "info");
|
|
87
|
+
}
|
|
88
|
+
else if (action === "* Return to pending" || action === "* Reopen") {
|
|
89
|
+
taskStore.reopen(task.id);
|
|
90
|
+
ui.notify(`Task #${task.id} reopened`, "info");
|
|
91
|
+
}
|
|
92
|
+
updateWidget();
|
|
93
|
+
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
94
|
+
return viewNativeTasks(ui);
|
|
95
|
+
}
|
|
96
|
+
pi.registerCommand("tasks", {
|
|
97
|
+
description: "View or manage native pi-loop tasks when pi-tasks is not installed",
|
|
98
|
+
handler: async (args, ctx) => {
|
|
99
|
+
const trimmed = args.trim();
|
|
100
|
+
const taskStore = getNativeTaskStore();
|
|
101
|
+
if (!taskStore) {
|
|
102
|
+
ctx.ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (trimmed) {
|
|
106
|
+
const entry = taskStore.create(trimmed.slice(0, 80), trimmed);
|
|
107
|
+
const backlog = await emitCreated(entry);
|
|
108
|
+
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
109
|
+
if (backlog.created && backlog.entry) {
|
|
110
|
+
ctx.ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await viewNativeTasks(ctx.ui);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
|
|
2
|
+
import type { GoalReducerState } from "./goal-types.js";
|
|
3
|
+
import type { LoopReducerState } from "./loop-reducer.js";
|
|
4
|
+
import type { MonitorReducerState } from "./monitor-reducer.js";
|
|
5
|
+
import type { TaskReducerState } from "./task-reducer.js";
|
|
6
|
+
export type GoalVerificationRequestedEvent = ReducerEvent<"GOAL_VERIFICATION_REQUESTED", {
|
|
7
|
+
id: string;
|
|
8
|
+
}>;
|
|
9
|
+
export interface GoalCoordinatorSnapshot {
|
|
10
|
+
goalState: GoalReducerState;
|
|
11
|
+
taskState: TaskReducerState;
|
|
12
|
+
loopState: LoopReducerState;
|
|
13
|
+
monitorState: MonitorReducerState;
|
|
14
|
+
}
|
|
15
|
+
export declare function reduceGoalVerificationRequest(event: GoalVerificationRequestedEvent, snapshot: GoalCoordinatorSnapshot): ReducerEffect[];
|
|
16
|
+
export interface GoalCoordinatorOptions {
|
|
17
|
+
getGoalState: () => GoalReducerState;
|
|
18
|
+
getTaskState: () => TaskReducerState;
|
|
19
|
+
getLoopState: () => LoopReducerState;
|
|
20
|
+
getMonitorState: () => MonitorReducerState;
|
|
21
|
+
}
|
|
22
|
+
export declare function createGoalCoordinatorReducer(options: GoalCoordinatorOptions): (event: ReducerEvent) => ReducerEffect[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { verifyGoal } from "./goal-verifier.js";
|
|
2
|
+
export function reduceGoalVerificationRequest(event, snapshot) {
|
|
3
|
+
if (event.type !== "GOAL_VERIFICATION_REQUESTED")
|
|
4
|
+
return [];
|
|
5
|
+
const goal = snapshot.goalState.goalsById[event.payload.id];
|
|
6
|
+
if (!goal)
|
|
7
|
+
return [];
|
|
8
|
+
return verifyGoal({
|
|
9
|
+
goal,
|
|
10
|
+
taskState: snapshot.taskState,
|
|
11
|
+
loopState: snapshot.loopState,
|
|
12
|
+
monitorState: snapshot.monitorState,
|
|
13
|
+
at: event.at,
|
|
14
|
+
}).effects;
|
|
15
|
+
}
|
|
16
|
+
export function createGoalCoordinatorReducer(options) {
|
|
17
|
+
const { getGoalState, getTaskState, getLoopState, getMonitorState } = options;
|
|
18
|
+
return (event) => {
|
|
19
|
+
if (event.type !== "GOAL_VERIFICATION_REQUESTED")
|
|
20
|
+
return [];
|
|
21
|
+
return reduceGoalVerificationRequest(event, {
|
|
22
|
+
goalState: getGoalState(),
|
|
23
|
+
taskState: getTaskState(),
|
|
24
|
+
loopState: getLoopState(),
|
|
25
|
+
monitorState: getMonitorState(),
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { GoalCriteria, GoalEntry, GoalProgressSnapshot, GoalReducerState, GoalScope } from "./goal-types.js";
|
|
2
|
+
export type GoalReducerEvent = {
|
|
3
|
+
type: "GOAL_CREATED";
|
|
4
|
+
at: number;
|
|
5
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
6
|
+
entityType?: "goal";
|
|
7
|
+
entityId?: string;
|
|
8
|
+
payload: {
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
scope: GoalScope;
|
|
12
|
+
criteria: GoalCriteria;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
} | {
|
|
16
|
+
type: "GOAL_ACTIVATED" | "GOAL_VERIFICATION_STARTED" | "GOAL_UNBLOCKED";
|
|
17
|
+
at: number;
|
|
18
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
19
|
+
entityType?: "goal";
|
|
20
|
+
entityId?: string;
|
|
21
|
+
payload: {
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
type: "GOAL_PROGRESS_RECORDED";
|
|
26
|
+
at: number;
|
|
27
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
28
|
+
entityType?: "goal";
|
|
29
|
+
entityId?: string;
|
|
30
|
+
payload: {
|
|
31
|
+
id: string;
|
|
32
|
+
progress: GoalProgressSnapshot;
|
|
33
|
+
};
|
|
34
|
+
} | {
|
|
35
|
+
type: "GOAL_VERIFICATION_PASSED" | "GOAL_VERIFICATION_FAILED" | "GOAL_BLOCKED" | "GOAL_FAILED";
|
|
36
|
+
at: number;
|
|
37
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
38
|
+
entityType?: "goal";
|
|
39
|
+
entityId?: string;
|
|
40
|
+
payload: {
|
|
41
|
+
id: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
progress?: GoalProgressSnapshot;
|
|
44
|
+
};
|
|
45
|
+
} | {
|
|
46
|
+
type: "GOAL_SATISFIED" | "GOAL_ARCHIVED";
|
|
47
|
+
at: number;
|
|
48
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
49
|
+
entityType?: "goal";
|
|
50
|
+
entityId?: string;
|
|
51
|
+
payload: {
|
|
52
|
+
id: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
};
|
|
55
|
+
} | {
|
|
56
|
+
type: "GOAL_UPDATED";
|
|
57
|
+
at: number;
|
|
58
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
59
|
+
entityType?: "goal";
|
|
60
|
+
entityId?: string;
|
|
61
|
+
payload: {
|
|
62
|
+
id: string;
|
|
63
|
+
title?: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
scope?: GoalScope;
|
|
66
|
+
criteria?: GoalCriteria;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export type GoalReducerEffect = {
|
|
71
|
+
type: "PERSIST_GOAL";
|
|
72
|
+
entityType: "goal";
|
|
73
|
+
entityId: string;
|
|
74
|
+
payload: {
|
|
75
|
+
goal: GoalEntry;
|
|
76
|
+
};
|
|
77
|
+
} | {
|
|
78
|
+
type: "REQUEST_GOAL_VERIFICATION";
|
|
79
|
+
entityType: "goal";
|
|
80
|
+
entityId: string;
|
|
81
|
+
payload: {
|
|
82
|
+
id: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export interface GoalReduceResult {
|
|
86
|
+
state: GoalReducerState;
|
|
87
|
+
effects: GoalReducerEffect[];
|
|
88
|
+
}
|
|
89
|
+
export declare function reduceGoalState(state: GoalReducerState, event: GoalReducerEvent): GoalReduceResult;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
function cloneState(state) {
|
|
2
|
+
return {
|
|
3
|
+
nextId: state.nextId,
|
|
4
|
+
goalsById: { ...state.goalsById },
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function emptyProgress() {
|
|
8
|
+
return {
|
|
9
|
+
totalTasks: 0,
|
|
10
|
+
pendingTasks: 0,
|
|
11
|
+
inProgressTasks: 0,
|
|
12
|
+
completedTasks: 0,
|
|
13
|
+
activeLoops: 0,
|
|
14
|
+
pausedLoops: 0,
|
|
15
|
+
runningMonitors: 0,
|
|
16
|
+
completedMonitors: 0,
|
|
17
|
+
erroredMonitors: 0,
|
|
18
|
+
stoppedMonitors: 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function emptyVerification() {
|
|
22
|
+
return {
|
|
23
|
+
attempts: 0,
|
|
24
|
+
passes: 0,
|
|
25
|
+
failures: 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function persist(goal) {
|
|
29
|
+
return {
|
|
30
|
+
type: "PERSIST_GOAL",
|
|
31
|
+
entityType: "goal",
|
|
32
|
+
entityId: goal.id,
|
|
33
|
+
payload: { goal },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function isTerminal(goal) {
|
|
37
|
+
return goal.status === "satisfied" || goal.status === "failed" || goal.status === "archived";
|
|
38
|
+
}
|
|
39
|
+
export function reduceGoalState(state, event) {
|
|
40
|
+
if (event.type === "GOAL_CREATED") {
|
|
41
|
+
const next = cloneState(state);
|
|
42
|
+
const id = String(next.nextId++);
|
|
43
|
+
const goal = {
|
|
44
|
+
id,
|
|
45
|
+
title: event.payload.title,
|
|
46
|
+
description: event.payload.description,
|
|
47
|
+
status: "pending",
|
|
48
|
+
verificationStatus: "unknown",
|
|
49
|
+
createdAt: event.at,
|
|
50
|
+
updatedAt: event.at,
|
|
51
|
+
scope: event.payload.scope,
|
|
52
|
+
criteria: event.payload.criteria,
|
|
53
|
+
progress: emptyProgress(),
|
|
54
|
+
verification: emptyVerification(),
|
|
55
|
+
metadata: event.payload.metadata,
|
|
56
|
+
};
|
|
57
|
+
next.goalsById[id] = goal;
|
|
58
|
+
return {
|
|
59
|
+
state: next,
|
|
60
|
+
effects: [persist(goal)],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const id = event.payload.id;
|
|
64
|
+
const current = state.goalsById[id];
|
|
65
|
+
if (!current)
|
|
66
|
+
return { state, effects: [] };
|
|
67
|
+
if (isTerminal(current) && event.type !== "GOAL_ARCHIVED")
|
|
68
|
+
return { state, effects: [] };
|
|
69
|
+
const next = cloneState(state);
|
|
70
|
+
const goal = {
|
|
71
|
+
...current,
|
|
72
|
+
progress: { ...current.progress },
|
|
73
|
+
verification: { ...current.verification },
|
|
74
|
+
};
|
|
75
|
+
let extraEffects = [];
|
|
76
|
+
if (event.type === "GOAL_ACTIVATED") {
|
|
77
|
+
goal.status = "active";
|
|
78
|
+
goal.activatedAt ??= event.at;
|
|
79
|
+
goal.updatedAt = event.at;
|
|
80
|
+
extraEffects = [{
|
|
81
|
+
type: "REQUEST_GOAL_VERIFICATION",
|
|
82
|
+
entityType: "goal",
|
|
83
|
+
entityId: id,
|
|
84
|
+
payload: { id },
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
if (event.type === "GOAL_PROGRESS_RECORDED") {
|
|
88
|
+
goal.progress = event.payload.progress;
|
|
89
|
+
goal.updatedAt = event.at;
|
|
90
|
+
}
|
|
91
|
+
if (event.type === "GOAL_VERIFICATION_STARTED") {
|
|
92
|
+
goal.verificationStatus = "checking";
|
|
93
|
+
goal.verification.attempts += 1;
|
|
94
|
+
goal.verification.lastCheckedAt = event.at;
|
|
95
|
+
goal.updatedAt = event.at;
|
|
96
|
+
}
|
|
97
|
+
if (event.type === "GOAL_VERIFICATION_PASSED") {
|
|
98
|
+
if (event.payload.progress)
|
|
99
|
+
goal.progress = event.payload.progress;
|
|
100
|
+
goal.status = "satisfied";
|
|
101
|
+
goal.verificationStatus = "verified";
|
|
102
|
+
goal.verification.passes += 1;
|
|
103
|
+
goal.verification.lastPassedAt = event.at;
|
|
104
|
+
goal.verification.lastCheckedAt = event.at;
|
|
105
|
+
goal.verification.lastReason = event.payload.reason;
|
|
106
|
+
goal.resolvedAt = event.at;
|
|
107
|
+
goal.updatedAt = event.at;
|
|
108
|
+
}
|
|
109
|
+
if (event.type === "GOAL_VERIFICATION_FAILED") {
|
|
110
|
+
if (event.payload.progress)
|
|
111
|
+
goal.progress = event.payload.progress;
|
|
112
|
+
goal.verificationStatus = "unverified";
|
|
113
|
+
goal.verification.failures += 1;
|
|
114
|
+
goal.verification.lastFailedAt = event.at;
|
|
115
|
+
goal.verification.lastCheckedAt = event.at;
|
|
116
|
+
goal.verification.lastReason = event.payload.reason;
|
|
117
|
+
if (goal.status === "pending")
|
|
118
|
+
goal.status = "active";
|
|
119
|
+
goal.updatedAt = event.at;
|
|
120
|
+
}
|
|
121
|
+
if (event.type === "GOAL_BLOCKED") {
|
|
122
|
+
if (event.payload.progress)
|
|
123
|
+
goal.progress = event.payload.progress;
|
|
124
|
+
goal.status = "blocked";
|
|
125
|
+
goal.verificationStatus = "inconclusive";
|
|
126
|
+
goal.verification.failures += 1;
|
|
127
|
+
goal.verification.lastFailedAt = event.at;
|
|
128
|
+
goal.verification.lastCheckedAt = event.at;
|
|
129
|
+
goal.verification.lastReason = event.payload.reason;
|
|
130
|
+
goal.updatedAt = event.at;
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "GOAL_UNBLOCKED") {
|
|
133
|
+
goal.status = "active";
|
|
134
|
+
goal.updatedAt = event.at;
|
|
135
|
+
}
|
|
136
|
+
if (event.type === "GOAL_SATISFIED") {
|
|
137
|
+
goal.status = "satisfied";
|
|
138
|
+
goal.verificationStatus = "verified";
|
|
139
|
+
goal.verification.lastReason = event.payload.reason ?? goal.verification.lastReason;
|
|
140
|
+
goal.resolvedAt = event.at;
|
|
141
|
+
goal.updatedAt = event.at;
|
|
142
|
+
}
|
|
143
|
+
if (event.type === "GOAL_FAILED") {
|
|
144
|
+
if (event.payload.progress)
|
|
145
|
+
goal.progress = event.payload.progress;
|
|
146
|
+
goal.status = "failed";
|
|
147
|
+
goal.verificationStatus = "unverified";
|
|
148
|
+
goal.verification.failures += 1;
|
|
149
|
+
goal.verification.lastFailedAt = event.at;
|
|
150
|
+
goal.verification.lastCheckedAt = event.at;
|
|
151
|
+
goal.verification.lastReason = event.payload.reason;
|
|
152
|
+
goal.resolvedAt = event.at;
|
|
153
|
+
goal.updatedAt = event.at;
|
|
154
|
+
}
|
|
155
|
+
if (event.type === "GOAL_ARCHIVED") {
|
|
156
|
+
goal.status = "archived";
|
|
157
|
+
goal.resolvedAt = event.at;
|
|
158
|
+
goal.updatedAt = event.at;
|
|
159
|
+
if (event.payload.reason !== undefined) {
|
|
160
|
+
goal.verification.lastReason = event.payload.reason;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (event.type === "GOAL_UPDATED") {
|
|
164
|
+
if (event.payload.title !== undefined)
|
|
165
|
+
goal.title = event.payload.title;
|
|
166
|
+
if (event.payload.description !== undefined)
|
|
167
|
+
goal.description = event.payload.description;
|
|
168
|
+
if (event.payload.scope !== undefined)
|
|
169
|
+
goal.scope = event.payload.scope;
|
|
170
|
+
if (event.payload.criteria !== undefined)
|
|
171
|
+
goal.criteria = event.payload.criteria;
|
|
172
|
+
if (event.payload.metadata !== undefined)
|
|
173
|
+
goal.metadata = event.payload.metadata;
|
|
174
|
+
goal.updatedAt = event.at;
|
|
175
|
+
}
|
|
176
|
+
next.goalsById[id] = goal;
|
|
177
|
+
return {
|
|
178
|
+
state: next,
|
|
179
|
+
effects: [persist(goal), ...extraEffects],
|
|
180
|
+
};
|
|
181
|
+
}
|