@trevonistrevon/pi-loop 0.5.9 → 0.6.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/CHANGELOG.md +72 -0
- package/README.md +64 -5
- package/dist/api.d.ts +11 -0
- package/dist/api.js +10 -0
- package/dist/commands/loop-command.d.ts +9 -2
- package/dist/commands/loop-command.js +81 -51
- package/dist/commands/tasks-command.js +3 -3
- package/dist/coordinator.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +89 -19
- package/dist/loop-format.d.ts +3 -0
- package/dist/loop-format.js +20 -0
- package/dist/loop-parse.d.ts +1 -0
- package/dist/loop-parse.js +59 -6
- package/dist/loop-reducer.d.ts +13 -1
- package/dist/loop-reducer.js +26 -0
- package/dist/monitor-manager.js +1 -1
- package/dist/notification-reducer.d.ts +2 -0
- package/dist/reducer-backed-store.d.ts +1 -1
- package/dist/reducer-backed-store.js +1 -1
- package/dist/rpc/channels.d.ts +73 -0
- package/dist/rpc/channels.js +30 -0
- package/dist/rpc/cross-extension-rpc.d.ts +59 -0
- package/dist/rpc/cross-extension-rpc.js +117 -0
- package/dist/runtime/loop-events.d.ts +26 -0
- package/dist/runtime/loop-events.js +26 -0
- package/dist/runtime/monitor-ondone-runtime.js +1 -1
- package/dist/runtime/native-task-rpc.d.ts +22 -0
- package/dist/runtime/native-task-rpc.js +50 -0
- package/dist/runtime/notification-runtime.d.ts +2 -1
- package/dist/runtime/notification-runtime.js +19 -7
- package/dist/runtime/task-backlog-runtime.d.ts +5 -1
- package/dist/runtime/task-backlog-runtime.js +14 -7
- package/dist/runtime/task-mutations.d.ts +39 -0
- package/dist/runtime/task-mutations.js +68 -0
- package/dist/runtime/task-rpc.d.ts +4 -0
- package/dist/runtime/task-rpc.js +40 -58
- package/dist/scheduler.js +25 -7
- package/dist/store.d.ts +9 -1
- package/dist/store.js +57 -0
- package/dist/tools/loop-tools.d.ts +8 -0
- package/dist/tools/loop-tools.js +105 -15
- package/dist/tools/monitor-tools.js +1 -3
- package/dist/tools/native-task-tools.d.ts +2 -6
- package/dist/tools/native-task-tools.js +25 -55
- package/dist/tools/tool-result.d.ts +8 -0
- package/dist/tools/tool-result.js +4 -0
- package/dist/trigger-system.js +1 -1
- package/dist/types.d.ts +24 -1
- package/package.json +15 -2
- package/src/api.ts +36 -0
- package/src/commands/loop-command.ts +100 -55
- package/src/commands/tasks-command.ts +3 -3
- package/src/coordinator.ts +1 -1
- package/src/index.ts +92 -19
- package/src/loop-format.ts +22 -0
- package/src/loop-parse.ts +62 -8
- package/src/loop-reducer.ts +41 -1
- package/src/monitor-manager.ts +1 -1
- package/src/notification-reducer.ts +3 -0
- package/src/reducer-backed-store.ts +3 -3
- package/src/rpc/channels.ts +97 -0
- package/src/rpc/cross-extension-rpc.ts +152 -0
- package/src/runtime/loop-events.ts +60 -0
- package/src/runtime/monitor-ondone-runtime.ts +1 -1
- package/src/runtime/native-task-rpc.ts +139 -0
- package/src/runtime/notification-runtime.ts +21 -8
- package/src/runtime/task-backlog-runtime.ts +25 -7
- package/src/runtime/task-mutations.ts +103 -0
- package/src/runtime/task-rpc.ts +48 -54
- package/src/scheduler.ts +25 -8
- package/src/store.ts +59 -2
- package/src/tools/loop-tools.ts +129 -16
- package/src/tools/monitor-tools.ts +1 -4
- package/src/tools/native-task-tools.ts +31 -52
- package/src/tools/tool-result.ts +4 -0
- package/src/trigger-system.ts +5 -5
- package/src/types.ts +29 -1
- package/.release-please-manifest.json +0 -3
- package/AGENTS.md +0 -83
- package/CONTRIBUTING.md +0 -69
- package/Dockerfile +0 -17
- package/benchmarks/experiment-sim.js +0 -30
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -33
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/commands/index.html +0 -131
- package/coverage/src/commands/loop-command.ts.html +0 -634
- package/coverage/src/commands/tasks-command.ts.html +0 -490
- package/coverage/src/coordinator.ts.html +0 -430
- package/coverage/src/goal-coordinator.ts.html +0 -259
- package/coverage/src/goal-reducer.ts.html +0 -982
- package/coverage/src/goal-store.ts.html +0 -742
- package/coverage/src/goal-verifier.ts.html +0 -808
- package/coverage/src/index.html +0 -386
- package/coverage/src/index.ts.html +0 -1111
- package/coverage/src/loop-parse.ts.html +0 -574
- package/coverage/src/loop-reducer.ts.html +0 -559
- package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
- package/coverage/src/monitor-manager.ts.html +0 -940
- package/coverage/src/monitor-reducer.ts.html +0 -583
- package/coverage/src/notification-reducer.ts.html +0 -550
- package/coverage/src/reducer-backed-store.ts.html +0 -589
- package/coverage/src/runtime/index.html +0 -206
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
- package/coverage/src/runtime/notification-runtime.ts.html +0 -721
- package/coverage/src/runtime/scope.ts.html +0 -196
- package/coverage/src/runtime/session-runtime.ts.html +0 -679
- package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -574
- package/coverage/src/runtime/task-events.ts.html +0 -208
- package/coverage/src/runtime/task-rpc.ts.html +0 -541
- package/coverage/src/scheduler.ts.html +0 -400
- package/coverage/src/store.ts.html +0 -667
- package/coverage/src/task-backlog-coordinator.ts.html +0 -181
- package/coverage/src/task-reducer.ts.html +0 -550
- package/coverage/src/task-store.ts.html +0 -526
- package/coverage/src/tools/index.html +0 -146
- package/coverage/src/tools/loop-tools.ts.html +0 -1000
- package/coverage/src/tools/monitor-tools.ts.html +0 -547
- package/coverage/src/tools/native-task-tools.ts.html +0 -556
- package/coverage/src/trigger-system.ts.html +0 -547
- package/coverage/src/ui/index.html +0 -116
- package/coverage/src/ui/widget.ts.html +0 -292
- package/dist/goal-coordinator.d.ts +0 -22
- package/dist/goal-coordinator.js +0 -28
- package/dist/goal-reducer.d.ts +0 -98
- package/dist/goal-reducer.js +0 -190
- package/dist/goal-store.d.ts +0 -22
- package/dist/goal-store.js +0 -188
- package/dist/goal-types.d.ts +0 -82
- package/dist/goal-types.js +0 -1
- package/dist/goal-verifier.d.ts +0 -20
- package/dist/goal-verifier.js +0 -198
- package/docker-compose.yml +0 -12
- package/docs/architecture/goal-state-schema.md +0 -505
- package/docs/architecture/state-machine-migration.md +0 -546
- package/docs/architecture/state-machine-reducer-event-model.md +0 -823
- package/docs/architecture/state-machine-test-matrix.md +0 -249
- package/docs/architecture/state-machine-transition-map.md +0 -436
- package/release-please-config.json +0 -9
- package/src/goal-coordinator.ts +0 -58
- package/src/goal-reducer.ts +0 -299
- package/src/goal-store.ts +0 -219
- package/src/goal-types.ts +0 -104
- package/src/goal-verifier.ts +0 -241
- package/vitest.config.ts +0 -29
package/src/loop-reducer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopEntry, Trigger } from "./types.js";
|
|
1
|
+
import type { DynamicLoopState, LoopEntry, Trigger } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4
4
|
|
|
@@ -34,6 +34,7 @@ export type LoopReducerEvent =
|
|
|
34
34
|
taskBacklog?: boolean;
|
|
35
35
|
readOnly?: boolean;
|
|
36
36
|
maxFires?: number;
|
|
37
|
+
dynamic?: Partial<DynamicLoopState>;
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
| {
|
|
@@ -60,6 +61,18 @@ export type LoopReducerEvent =
|
|
|
60
61
|
id: string;
|
|
61
62
|
reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
|
|
62
63
|
};
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
type: "LOOP_DYNAMIC_UPDATED";
|
|
67
|
+
at: number;
|
|
68
|
+
source: ReducerSource;
|
|
69
|
+
entityType?: "loop";
|
|
70
|
+
entityId?: string;
|
|
71
|
+
payload: {
|
|
72
|
+
id: string;
|
|
73
|
+
prompt?: string;
|
|
74
|
+
dynamic: Partial<DynamicLoopState>;
|
|
75
|
+
};
|
|
63
76
|
};
|
|
64
77
|
|
|
65
78
|
export type LoopReducerEffect =
|
|
@@ -106,6 +119,18 @@ export function reduceLoopState(state: LoopReducerState, event: LoopReducerEvent
|
|
|
106
119
|
readOnly: event.payload.readOnly,
|
|
107
120
|
maxFires: event.payload.maxFires,
|
|
108
121
|
fireCount: 0,
|
|
122
|
+
dynamic: event.payload.trigger.type === "dynamic" || event.payload.dynamic
|
|
123
|
+
? {
|
|
124
|
+
goal: event.payload.dynamic?.goal ?? event.payload.prompt,
|
|
125
|
+
state: event.payload.dynamic?.state,
|
|
126
|
+
metrics: event.payload.dynamic?.metrics,
|
|
127
|
+
doneCriteria: event.payload.dynamic?.doneCriteria,
|
|
128
|
+
iteration: event.payload.dynamic?.iteration ?? 0,
|
|
129
|
+
nextWakeAt: event.payload.dynamic?.nextWakeAt,
|
|
130
|
+
awaitingUpdate: event.payload.dynamic?.awaitingUpdate ?? false,
|
|
131
|
+
lastUpdatedAt: event.payload.dynamic?.lastUpdatedAt ?? event.at,
|
|
132
|
+
}
|
|
133
|
+
: undefined,
|
|
109
134
|
};
|
|
110
135
|
next.loopsById[id] = loop;
|
|
111
136
|
return {
|
|
@@ -150,6 +175,21 @@ export function reduceLoopState(state: LoopReducerState, event: LoopReducerEvent
|
|
|
150
175
|
loop.updatedAt = event.at;
|
|
151
176
|
}
|
|
152
177
|
|
|
178
|
+
if (event.type === "LOOP_DYNAMIC_UPDATED") {
|
|
179
|
+
loop.prompt = event.payload.prompt ?? loop.prompt;
|
|
180
|
+
loop.dynamic = {
|
|
181
|
+
goal: event.payload.dynamic.goal ?? loop.dynamic?.goal ?? loop.prompt,
|
|
182
|
+
state: event.payload.dynamic.state ?? loop.dynamic?.state,
|
|
183
|
+
metrics: event.payload.dynamic.metrics ?? loop.dynamic?.metrics,
|
|
184
|
+
doneCriteria: event.payload.dynamic.doneCriteria ?? loop.dynamic?.doneCriteria,
|
|
185
|
+
iteration: event.payload.dynamic.iteration ?? loop.dynamic?.iteration ?? 0,
|
|
186
|
+
nextWakeAt: "nextWakeAt" in event.payload.dynamic ? event.payload.dynamic.nextWakeAt : loop.dynamic?.nextWakeAt,
|
|
187
|
+
awaitingUpdate: event.payload.dynamic.awaitingUpdate ?? loop.dynamic?.awaitingUpdate ?? false,
|
|
188
|
+
lastUpdatedAt: event.payload.dynamic.lastUpdatedAt ?? event.at,
|
|
189
|
+
};
|
|
190
|
+
loop.updatedAt = event.at;
|
|
191
|
+
}
|
|
192
|
+
|
|
153
193
|
next.loopsById[id] = loop;
|
|
154
194
|
return {
|
|
155
195
|
state: next,
|
package/src/monitor-manager.ts
CHANGED
|
@@ -270,7 +270,7 @@ export class MonitorManager {
|
|
|
270
270
|
onComplete(id: string, callback: () => void): boolean {
|
|
271
271
|
const bp = this.processes.get(id);
|
|
272
272
|
if (!bp) return false;
|
|
273
|
-
if (bp.entry.status === "completed") {
|
|
273
|
+
if (bp.entry.status === "completed" || bp.entry.status === "error") {
|
|
274
274
|
callback();
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DynamicLoopState } from "./types.js";
|
|
2
|
+
|
|
1
3
|
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
2
4
|
|
|
3
5
|
export interface ReducerNotification {
|
|
@@ -9,6 +11,7 @@ export interface ReducerNotification {
|
|
|
9
11
|
recurring?: boolean;
|
|
10
12
|
autoTask?: boolean;
|
|
11
13
|
readOnly?: boolean;
|
|
14
|
+
dynamic?: DynamicLoopState;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface NotificationReducerState {
|
|
@@ -11,8 +11,8 @@ function acquireLock(lockPath: string): void {
|
|
|
11
11
|
try {
|
|
12
12
|
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
13
13
|
return;
|
|
14
|
-
} catch (e
|
|
15
|
-
if (e.code === "EEXIST") {
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if ((e as NodeJS.ErrnoException).code === "EEXIST") {
|
|
16
16
|
try {
|
|
17
17
|
const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
|
|
18
18
|
if (!pid || !isProcessRunning(pid)) {
|
|
@@ -60,7 +60,7 @@ export interface ReducerBackedStoreConfig<TEntry, TState, TEvent, TData> {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Shared persistence + reducer-dispatch machinery for the file-backed entity
|
|
63
|
-
* stores (loops, tasks
|
|
63
|
+
* stores (loops, tasks). Owns file locking, signature-gated load, atomic
|
|
64
64
|
* save, and reducer application; subclasses add only their entity-specific
|
|
65
65
|
* command methods.
|
|
66
66
|
*
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// VENDORED MODULE — canonical copy shared verbatim by pi-loop and pi-orca.
|
|
2
|
+
// If you edit this file, copy it to the sibling repo and bump VENDOR_REV.
|
|
3
|
+
// VENDOR_REV: 1
|
|
4
|
+
|
|
5
|
+
/** Cross-extension RPC channels served by a tasks provider (pi-tasks or pi-loop native). */
|
|
6
|
+
export const TASKS_RPC = {
|
|
7
|
+
ping: "tasks:rpc:ping",
|
|
8
|
+
create: "tasks:rpc:create",
|
|
9
|
+
update: "tasks:rpc:update",
|
|
10
|
+
pending: "tasks:rpc:pending",
|
|
11
|
+
clean: "tasks:rpc:clean",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
/** Cross-extension RPC channels served by @tintinweb/pi-subagents. */
|
|
15
|
+
export const SUBAGENTS_RPC = {
|
|
16
|
+
ping: "subagents:rpc:ping",
|
|
17
|
+
spawn: "subagents:rpc:spawn",
|
|
18
|
+
stop: "subagents:rpc:stop",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/** Broadcast (fire-and-forget) task lifecycle events. */
|
|
22
|
+
export const TASK_EVENTS = {
|
|
23
|
+
ready: "tasks:ready",
|
|
24
|
+
created: "tasks:created",
|
|
25
|
+
started: "tasks:started",
|
|
26
|
+
completed: "tasks:completed",
|
|
27
|
+
reopened: "tasks:reopened",
|
|
28
|
+
updated: "tasks:updated",
|
|
29
|
+
deleted: "tasks:deleted",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export function replyChannel(channel: string, requestId: string): string {
|
|
33
|
+
return `${channel}:reply:${requestId}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Wire-level DTOs ──
|
|
37
|
+
// Structural: pi-loop's TaskEntry satisfies TaskEntryWire; consumers on the
|
|
38
|
+
// other side of the bus depend only on these shapes, never on store internals.
|
|
39
|
+
|
|
40
|
+
export type TaskStatusWire = "pending" | "in_progress" | "completed";
|
|
41
|
+
|
|
42
|
+
export interface TaskEntryWire {
|
|
43
|
+
id: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
description: string;
|
|
46
|
+
status: TaskStatusWire;
|
|
47
|
+
createdAt: number;
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
completedAt?: number;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CreateTaskParams {
|
|
54
|
+
subject: string;
|
|
55
|
+
description: string;
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CreateTaskReply {
|
|
60
|
+
id: string;
|
|
61
|
+
task: TaskEntryWire;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UpdateTaskParams {
|
|
65
|
+
id: string;
|
|
66
|
+
status?: TaskStatusWire;
|
|
67
|
+
subject?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface UpdateTaskReply {
|
|
72
|
+
task: TaskEntryWire;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PendingReply {
|
|
76
|
+
pending: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CleanReply {
|
|
80
|
+
pruned: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PingReply {
|
|
84
|
+
version: number;
|
|
85
|
+
/** Identifies which extension answered; lets a provider ignore its own reply. */
|
|
86
|
+
provider?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SpawnParams {
|
|
90
|
+
type: string;
|
|
91
|
+
prompt: string;
|
|
92
|
+
options?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface SpawnReply {
|
|
96
|
+
id: string;
|
|
97
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// VENDORED MODULE — canonical copy shared verbatim by pi-loop and pi-orca.
|
|
2
|
+
// If you edit this file, copy it to the sibling repo and bump VENDOR_REV.
|
|
3
|
+
// VENDOR_REV: 2
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { type PingReply, replyChannel } from "./channels.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cross-extension request/reply over the in-process pi event bus.
|
|
10
|
+
*
|
|
11
|
+
* Wire contract (matches the pi-mono convention used by @tintinweb/pi-subagents):
|
|
12
|
+
* request: emit(channel, { requestId, ...params })
|
|
13
|
+
* reply: emit(`${channel}:reply:${requestId}`, RpcReply)
|
|
14
|
+
*/
|
|
15
|
+
export const PROTOCOL_VERSION = 2;
|
|
16
|
+
|
|
17
|
+
export type RpcReply<T = void> =
|
|
18
|
+
| { success: true; data?: T }
|
|
19
|
+
| { success: false; error: string };
|
|
20
|
+
|
|
21
|
+
export class RpcError extends Error {
|
|
22
|
+
constructor(
|
|
23
|
+
readonly channel: string,
|
|
24
|
+
message: string,
|
|
25
|
+
readonly timedOut = false,
|
|
26
|
+
) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "RpcError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Minimal event-bus surface — satisfied by pi.events. */
|
|
33
|
+
export interface RpcEventBus {
|
|
34
|
+
on(event: string, handler: (data: unknown) => void): () => void;
|
|
35
|
+
emit(event: string, data: unknown): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Call a remote handler and await its reply.
|
|
40
|
+
*
|
|
41
|
+
* Rejects with RpcError on a failure envelope or on timeout (timedOut=true).
|
|
42
|
+
* Callers that treat failure as "fall back" wrap this in try/catch at the
|
|
43
|
+
* layer that knows the fallback — the RPC layer itself never returns sentinels.
|
|
44
|
+
*/
|
|
45
|
+
export function rpcCall<T = void>(
|
|
46
|
+
bus: RpcEventBus,
|
|
47
|
+
channel: string,
|
|
48
|
+
params: Record<string, unknown> = {},
|
|
49
|
+
timeoutMs = 5000,
|
|
50
|
+
): Promise<T> {
|
|
51
|
+
const requestId = randomUUID();
|
|
52
|
+
return new Promise<T>((resolve, reject) => {
|
|
53
|
+
let settled = false;
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
if (settled) return;
|
|
56
|
+
settled = true;
|
|
57
|
+
unsub();
|
|
58
|
+
reject(new RpcError(channel, `${channel} timed out after ${timeoutMs}ms`, true));
|
|
59
|
+
}, timeoutMs);
|
|
60
|
+
const unsub = bus.on(replyChannel(channel, requestId), (raw) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
unsub();
|
|
65
|
+
const reply = raw as RpcReply<T> | undefined;
|
|
66
|
+
if (reply?.success) {
|
|
67
|
+
resolve(reply.data as T);
|
|
68
|
+
} else {
|
|
69
|
+
const error = reply && "error" in reply ? reply.error : undefined;
|
|
70
|
+
reject(new RpcError(channel, error ?? `${channel} replied with a malformed envelope`));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
bus.emit(channel, { requestId, ...params });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!settled) {
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
unsub();
|
|
80
|
+
reject(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detection probe: resolves undefined when nobody answers the ping channel.
|
|
88
|
+
* Absence of a provider is expected, not an error.
|
|
89
|
+
*
|
|
90
|
+
* First reply wins. If the probing extension also serves this channel, its
|
|
91
|
+
* own reply will settle the probe — in that case listen on the reply channel
|
|
92
|
+
* directly for the full window and filter by PingReply.provider instead.
|
|
93
|
+
*/
|
|
94
|
+
export async function rpcProbe(
|
|
95
|
+
bus: RpcEventBus,
|
|
96
|
+
pingChannel: string,
|
|
97
|
+
timeoutMs = 5000,
|
|
98
|
+
): Promise<PingReply | undefined> {
|
|
99
|
+
try {
|
|
100
|
+
return await rpcCall<PingReply>(bus, pingChannel, {}, timeoutMs);
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface HandleRpcOptions {
|
|
107
|
+
/**
|
|
108
|
+
* When false, the handler is a silent no-op: another extension owns the
|
|
109
|
+
* channel and will reply; emitting a failure here would race its reply.
|
|
110
|
+
*/
|
|
111
|
+
enabled?: () => boolean;
|
|
112
|
+
debug?: (...args: unknown[]) => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Register a server handler for one RPC channel. Returns the unsubscribe fn.
|
|
117
|
+
*
|
|
118
|
+
* Malformed requests get a failure reply, never a silent drop — the only
|
|
119
|
+
* silent case is a missing requestId, which leaves no reply address.
|
|
120
|
+
* A thrown/rejected fn becomes a failure envelope.
|
|
121
|
+
*/
|
|
122
|
+
export function handleRpc<P extends object, R = unknown>(
|
|
123
|
+
bus: RpcEventBus,
|
|
124
|
+
channel: string,
|
|
125
|
+
fn: (params: P) => R | Promise<R>,
|
|
126
|
+
opts?: HandleRpcOptions,
|
|
127
|
+
): () => void {
|
|
128
|
+
return bus.on(channel, async (raw) => {
|
|
129
|
+
if (opts?.enabled && !opts.enabled()) return;
|
|
130
|
+
|
|
131
|
+
const requestId =
|
|
132
|
+
raw && typeof raw === "object"
|
|
133
|
+
? (raw as { requestId?: unknown }).requestId
|
|
134
|
+
: undefined;
|
|
135
|
+
if (typeof requestId !== "string" || requestId.length === 0) {
|
|
136
|
+
opts?.debug?.(`${channel} — dropped request without requestId`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let reply: RpcReply<R>;
|
|
141
|
+
try {
|
|
142
|
+
const result = await fn(raw as P);
|
|
143
|
+
reply = result === undefined ? { success: true } : { success: true, data: result };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
reply = {
|
|
146
|
+
success: false,
|
|
147
|
+
error: error instanceof Error ? error.message : String(error),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
bus.emit(replyChannel(channel, requestId), reply);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LoopEntry } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export type LoopAutoDeleteReason = "task_backlog_empty";
|
|
4
|
+
|
|
5
|
+
export interface LoopAutodeletedPayload {
|
|
6
|
+
loopId: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
trigger: LoopEntry["trigger"];
|
|
9
|
+
recurring: boolean;
|
|
10
|
+
autoTask?: boolean;
|
|
11
|
+
taskBacklog?: boolean;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
maxFires?: number;
|
|
14
|
+
fireCount?: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
source: "task_backlog_runtime";
|
|
19
|
+
reason: LoopAutoDeleteReason;
|
|
20
|
+
pendingCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TaskBacklogEmptyPayload {
|
|
24
|
+
pendingCount: 0;
|
|
25
|
+
deletedLoopIds: string[];
|
|
26
|
+
source: "task_backlog_runtime";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildLoopAutodeletedPayload(
|
|
30
|
+
entry: LoopEntry,
|
|
31
|
+
pendingCount: number,
|
|
32
|
+
): LoopAutodeletedPayload {
|
|
33
|
+
return {
|
|
34
|
+
loopId: entry.id,
|
|
35
|
+
prompt: entry.prompt,
|
|
36
|
+
trigger: entry.trigger,
|
|
37
|
+
recurring: entry.recurring,
|
|
38
|
+
autoTask: entry.autoTask,
|
|
39
|
+
taskBacklog: entry.taskBacklog,
|
|
40
|
+
readOnly: entry.readOnly,
|
|
41
|
+
maxFires: entry.maxFires,
|
|
42
|
+
fireCount: entry.fireCount,
|
|
43
|
+
createdAt: entry.createdAt,
|
|
44
|
+
updatedAt: entry.updatedAt,
|
|
45
|
+
expiresAt: entry.expiresAt,
|
|
46
|
+
source: "task_backlog_runtime",
|
|
47
|
+
reason: "task_backlog_empty",
|
|
48
|
+
pendingCount,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildTaskBacklogEmptyPayload(
|
|
53
|
+
deletedLoopIds: string[],
|
|
54
|
+
): TaskBacklogEmptyPayload {
|
|
55
|
+
return {
|
|
56
|
+
pendingCount: 0,
|
|
57
|
+
deletedLoopIds,
|
|
58
|
+
source: "task_backlog_runtime",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -62,7 +62,7 @@ export function createMonitorOnDoneRuntime(options: MonitorOnDoneRuntimeOptions)
|
|
|
62
62
|
|
|
63
63
|
const monitor = monitorManager.get(monitorId);
|
|
64
64
|
if (monitor && monitor.status !== "running") {
|
|
65
|
-
if (monitor.status === "completed") {
|
|
65
|
+
if (monitor.status === "completed" || monitor.status === "error") {
|
|
66
66
|
deliver();
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type CleanReply,
|
|
4
|
+
type CreateTaskReply,
|
|
5
|
+
type PendingReply,
|
|
6
|
+
type PingReply,
|
|
7
|
+
TASKS_RPC,
|
|
8
|
+
type UpdateTaskReply,
|
|
9
|
+
} from "../rpc/channels.js";
|
|
10
|
+
import { handleRpc, PROTOCOL_VERSION } from "../rpc/cross-extension-rpc.js";
|
|
11
|
+
import type { TaskStore } from "../task-store.js";
|
|
12
|
+
import type { TaskStatus } from "../task-types.js";
|
|
13
|
+
import {
|
|
14
|
+
cleanTasks,
|
|
15
|
+
createTask,
|
|
16
|
+
type TaskBacklogResult,
|
|
17
|
+
type TaskMutationContext,
|
|
18
|
+
updateTask,
|
|
19
|
+
} from "./task-mutations.js";
|
|
20
|
+
|
|
21
|
+
/** Discriminates pi-loop's own ping replies from an external pi-tasks provider. */
|
|
22
|
+
export const NATIVE_TASKS_PROVIDER = "pi-loop-native";
|
|
23
|
+
|
|
24
|
+
interface CreateTaskRequest {
|
|
25
|
+
requestId: string;
|
|
26
|
+
subject?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface UpdateTaskRequest {
|
|
32
|
+
requestId: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
status?: TaskStatus;
|
|
35
|
+
subject?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NativeTaskRpcOptions {
|
|
40
|
+
pi: ExtensionAPI;
|
|
41
|
+
getNativeTaskStore: () => TaskStore | undefined;
|
|
42
|
+
/** Stands the server down (silent no-op) when an external pi-tasks owns the channels. */
|
|
43
|
+
isEnabled?: () => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Gates the mutating/reading verbs (create/update/pending/clean) until the
|
|
46
|
+
* pi-tasks detection probe has settled. Ping stays answerable throughout so
|
|
47
|
+
* providers remain discoverable, but no state diverges while an external
|
|
48
|
+
* provider might still claim the channels. Defaults to settled.
|
|
49
|
+
*/
|
|
50
|
+
isDetectionSettled?: () => boolean;
|
|
51
|
+
evaluateTaskBacklog: (
|
|
52
|
+
taskStore: TaskStore,
|
|
53
|
+
pendingCount: number,
|
|
54
|
+
) => Promise<TaskBacklogResult>;
|
|
55
|
+
updateWidget: () => void;
|
|
56
|
+
debug?: (...args: unknown[]) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function registerNativeTaskRpc(options: NativeTaskRpcOptions): void {
|
|
60
|
+
const {
|
|
61
|
+
pi,
|
|
62
|
+
getNativeTaskStore,
|
|
63
|
+
isEnabled,
|
|
64
|
+
isDetectionSettled,
|
|
65
|
+
evaluateTaskBacklog,
|
|
66
|
+
updateWidget,
|
|
67
|
+
debug,
|
|
68
|
+
} = options;
|
|
69
|
+
const rpcOpts = { enabled: isEnabled, debug };
|
|
70
|
+
const settledRpcOpts = {
|
|
71
|
+
enabled: () => (isEnabled ? isEnabled() : true) && (isDetectionSettled ? isDetectionSettled() : true),
|
|
72
|
+
debug,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function requireMutationContext(): TaskMutationContext {
|
|
76
|
+
const taskStore = getNativeTaskStore();
|
|
77
|
+
if (!taskStore) throw new Error("native task store unavailable");
|
|
78
|
+
return { pi, taskStore, evaluateTaskBacklog, updateWidget };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
handleRpc<{ requestId: string }, PingReply>(
|
|
82
|
+
pi.events,
|
|
83
|
+
TASKS_RPC.ping,
|
|
84
|
+
() => ({ version: PROTOCOL_VERSION, provider: NATIVE_TASKS_PROVIDER }),
|
|
85
|
+
rpcOpts,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
handleRpc<{ requestId: string }, PendingReply>(
|
|
89
|
+
pi.events,
|
|
90
|
+
TASKS_RPC.pending,
|
|
91
|
+
() => ({ pending: requireMutationContext().taskStore.pendingCount() }),
|
|
92
|
+
settledRpcOpts,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
handleRpc<CreateTaskRequest, CreateTaskReply>(
|
|
96
|
+
pi.events,
|
|
97
|
+
TASKS_RPC.create,
|
|
98
|
+
async (request) => {
|
|
99
|
+
if (!request.subject || !request.description) {
|
|
100
|
+
throw new Error("subject and description are required");
|
|
101
|
+
}
|
|
102
|
+
const { entry } = await createTask(requireMutationContext(), {
|
|
103
|
+
subject: request.subject,
|
|
104
|
+
description: request.description,
|
|
105
|
+
metadata: request.metadata,
|
|
106
|
+
});
|
|
107
|
+
return { id: entry.id, task: entry };
|
|
108
|
+
},
|
|
109
|
+
settledRpcOpts,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
handleRpc<{ requestId: string }, CleanReply>(
|
|
113
|
+
pi.events,
|
|
114
|
+
TASKS_RPC.clean,
|
|
115
|
+
async () => {
|
|
116
|
+
const pruned = await cleanTasks(requireMutationContext());
|
|
117
|
+
debug?.(`${TASKS_RPC.clean} — pruned ${pruned} completed task(s)`);
|
|
118
|
+
return { pruned };
|
|
119
|
+
},
|
|
120
|
+
settledRpcOpts,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
handleRpc<UpdateTaskRequest, UpdateTaskReply>(
|
|
124
|
+
pi.events,
|
|
125
|
+
TASKS_RPC.update,
|
|
126
|
+
async (request) => {
|
|
127
|
+
if (!request.id) throw new Error("id is required");
|
|
128
|
+
const result = await updateTask(requireMutationContext(), {
|
|
129
|
+
id: request.id,
|
|
130
|
+
status: request.status,
|
|
131
|
+
subject: request.subject,
|
|
132
|
+
description: request.description,
|
|
133
|
+
});
|
|
134
|
+
if (!result) throw new Error(`Task #${request.id} not found`);
|
|
135
|
+
return { task: result.entry };
|
|
136
|
+
},
|
|
137
|
+
settledRpcOpts,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -5,13 +5,14 @@ import {
|
|
|
5
5
|
type ReducerEvent,
|
|
6
6
|
type ReducerHandler,
|
|
7
7
|
} from "../coordinator.js";
|
|
8
|
+
import { formatTrigger } from "../loop-format.js";
|
|
8
9
|
import {
|
|
9
10
|
type NotificationReducerEvent,
|
|
10
11
|
type NotificationReducerState,
|
|
11
12
|
type ReducerNotification,
|
|
12
13
|
reduceNotificationState,
|
|
13
14
|
} from "../notification-reducer.js";
|
|
14
|
-
import type { Trigger } from "../types.js";
|
|
15
|
+
import type { DynamicLoopState, Trigger } from "../types.js";
|
|
15
16
|
|
|
16
17
|
export interface LoopFireEvent {
|
|
17
18
|
loopId: string;
|
|
@@ -21,6 +22,7 @@ export interface LoopFireEvent {
|
|
|
21
22
|
readOnly?: boolean;
|
|
22
23
|
recurring?: boolean;
|
|
23
24
|
autoTask?: boolean;
|
|
25
|
+
dynamic?: DynamicLoopState;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface PendingNotification extends LoopFireEvent {
|
|
@@ -94,13 +96,7 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
function buildLoopFireMessage(data: LoopFireEvent): string {
|
|
97
|
-
const triggerInfo =
|
|
98
|
-
? data.trigger
|
|
99
|
-
: data.trigger?.type === "cron"
|
|
100
|
-
? `schedule: ${data.trigger.schedule}`
|
|
101
|
-
: data.trigger?.type === "event"
|
|
102
|
-
? `event: ${data.trigger.source}`
|
|
103
|
-
: "hybrid";
|
|
99
|
+
const triggerInfo = formatTrigger(data.trigger, "notification");
|
|
104
100
|
|
|
105
101
|
const loopId = data.loopId || "?";
|
|
106
102
|
const prompt = data.prompt || "loop fired";
|
|
@@ -108,6 +104,22 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
|
|
|
108
104
|
? "\n\nREAD-ONLY MODE — use only read tools (Read, TaskList, LoopList, MonitorList, etc.). No file writes, shell execution, or destructive changes."
|
|
109
105
|
: "";
|
|
110
106
|
|
|
107
|
+
if (data.dynamic || (typeof data.trigger !== "string" && data.trigger?.type === "dynamic")) {
|
|
108
|
+
const dynamic = data.dynamic;
|
|
109
|
+
const lines = [
|
|
110
|
+
`[pi-loop] Loop #${loopId} fired (dynamic).${constraint}`,
|
|
111
|
+
`Goal: ${dynamic?.goal ?? prompt}`,
|
|
112
|
+
`Iteration: ${dynamic?.iteration ?? 0}`,
|
|
113
|
+
];
|
|
114
|
+
if (dynamic?.state) lines.push(`State: ${dynamic.state}`);
|
|
115
|
+
if (dynamic?.metrics) lines.push(`Metrics: ${dynamic.metrics}`);
|
|
116
|
+
if (dynamic?.doneCriteria) lines.push(`Done criteria: ${dynamic.doneCriteria}`);
|
|
117
|
+
lines.push(
|
|
118
|
+
"Continue toward the goal. When done call LoopUpdate with status=\"completed\". If more work remains, call LoopUpdate with status=\"continue\" plus state/metrics. Omit nextInterval for idle-driven rewake; include nextInterval for a timed wake. If blocked, use status=\"paused\".",
|
|
119
|
+
);
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
return [
|
|
112
124
|
`[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
|
|
113
125
|
prompt,
|
|
@@ -144,6 +156,7 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
|
|
|
144
156
|
recurring: notification.recurring,
|
|
145
157
|
readOnly: notification.readOnly,
|
|
146
158
|
autoTask: notification.autoTask,
|
|
159
|
+
dynamic: notification.dynamic,
|
|
147
160
|
timestamp: notification.timestamp,
|
|
148
161
|
},
|
|
149
162
|
}, {
|