@trevonistrevon/pi-loop 0.6.3 → 0.6.4
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 +14 -0
- package/README.md +3 -2
- package/dist/api.d.ts +2 -1
- package/dist/index.js +10 -0
- package/dist/loop-reducer.d.ts +24 -1
- package/dist/loop-reducer.js +31 -0
- package/dist/notification-reducer.d.ts +2 -1
- package/dist/runtime/notification-runtime.d.ts +2 -1
- package/dist/runtime/notification-runtime.js +18 -0
- package/dist/runtime/task-events.d.ts +2 -1
- package/dist/runtime/task-events.js +1 -0
- package/dist/runtime/task-rpc.d.ts +4 -0
- package/dist/runtime/task-rpc.js +62 -1
- package/dist/store.d.ts +10 -1
- package/dist/store.js +51 -0
- package/dist/task-reducer.d.ts +2 -1
- package/dist/task-reducer.js +1 -0
- package/dist/task-store.d.ts +2 -2
- package/dist/task-store.js +2 -2
- package/dist/task-types.d.ts +6 -0
- package/dist/tools/loop-tools.d.ts +17 -1
- package/dist/tools/loop-tools.js +320 -21
- package/dist/tools/monitor-tools.js +43 -9
- package/dist/tools/native-task-tools.js +56 -11
- package/dist/tools/tool-result.d.ts +12 -2
- package/dist/tools/tool-result.js +7 -3
- package/dist/types.d.ts +35 -0
- package/dist/ui/tool-renderer.d.ts +7 -0
- package/dist/ui/tool-renderer.js +34 -0
- package/dist/ui/widget.js +4 -4
- package/dist/workflow-reducer.d.ts +18 -0
- package/dist/workflow-reducer.js +82 -0
- package/docs/USAGE_GUIDE.md +41 -0
- package/package.json +3 -3
- package/src/api.ts +9 -1
- package/src/index.ts +10 -0
- package/src/loop-reducer.ts +53 -1
- package/src/notification-reducer.ts +2 -1
- package/src/runtime/notification-runtime.ts +21 -1
- package/src/runtime/task-events.ts +3 -1
- package/src/runtime/task-rpc.ts +66 -0
- package/src/store.ts +51 -2
- package/src/task-reducer.ts +3 -1
- package/src/task-store.ts +3 -3
- package/src/task-types.ts +7 -0
- package/src/tools/loop-tools.ts +365 -15
- package/src/tools/monitor-tools.ts +44 -7
- package/src/tools/native-task-tools.ts +56 -8
- package/src/tools/tool-result.ts +18 -2
- package/src/types.ts +41 -0
- package/src/ui/tool-renderer.ts +45 -0
- package/src/ui/widget.ts +4 -4
- package/src/workflow-reducer.ts +107 -0
package/src/runtime/task-rpc.ts
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
type PingReply,
|
|
8
8
|
replyChannel,
|
|
9
9
|
TASKS_RPC,
|
|
10
|
+
type UpdateTaskReply,
|
|
10
11
|
} from "../rpc/channels.js";
|
|
11
12
|
import { type RpcReply, rpcCall } from "../rpc/cross-extension-rpc.js";
|
|
12
13
|
import type { TaskStore } from "../task-store.js";
|
|
14
|
+
import type { TaskWorkflowLink } from "../task-types.js";
|
|
13
15
|
import type { LoopEntry } from "../types.js";
|
|
14
16
|
import { NATIVE_TASKS_PROVIDER } from "./native-task-rpc.js";
|
|
15
17
|
import { emitNativeTaskEvent } from "./task-events.js";
|
|
@@ -20,7 +22,9 @@ export interface TaskRuntimeBridgeOptions {
|
|
|
20
22
|
setTasksAvailable: (available: boolean) => void;
|
|
21
23
|
getNativeTaskStore: () => TaskStore | undefined;
|
|
22
24
|
onNativeTaskCreated?: (taskStore: TaskStore) => void;
|
|
25
|
+
onNativeTaskCompleted?: (taskStore: TaskStore) => Promise<void> | void;
|
|
23
26
|
onNativeTasksPruned?: (taskStore: TaskStore) => Promise<void> | void;
|
|
27
|
+
isDetectionSettled?: () => boolean;
|
|
24
28
|
/** Called when a detection window opens. */
|
|
25
29
|
onDetectionStarted?: () => void;
|
|
26
30
|
/** Called when a detection window closes (provider found or probe timed out). */
|
|
@@ -31,6 +35,8 @@ export interface TaskRuntimeBridgeOptions {
|
|
|
31
35
|
export interface TaskRuntimeBridge {
|
|
32
36
|
checkTasksVersion(): void;
|
|
33
37
|
autoCreateTask(entry: LoopEntry): Promise<string | undefined>;
|
|
38
|
+
createWorkflowTask(entry: LoopEntry): Promise<string | undefined>;
|
|
39
|
+
completeWorkflowTask(taskId: string): Promise<boolean>;
|
|
34
40
|
hasPendingTasks(): Promise<number>;
|
|
35
41
|
cleanDoneTasks(): Promise<void>;
|
|
36
42
|
}
|
|
@@ -42,7 +48,9 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
|
|
|
42
48
|
setTasksAvailable,
|
|
43
49
|
getNativeTaskStore,
|
|
44
50
|
onNativeTaskCreated,
|
|
51
|
+
onNativeTaskCompleted,
|
|
45
52
|
onNativeTasksPruned,
|
|
53
|
+
isDetectionSettled,
|
|
46
54
|
onDetectionStarted,
|
|
47
55
|
onDetectionSettled,
|
|
48
56
|
debug,
|
|
@@ -105,6 +113,62 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
|
|
|
105
113
|
return task.id;
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
async function createWorkflowTask(entry: LoopEntry): Promise<string | undefined> {
|
|
117
|
+
const workflow = entry.workflow;
|
|
118
|
+
if (!workflow) return undefined;
|
|
119
|
+
const state = workflow.definition.states[workflow.currentState];
|
|
120
|
+
if (!state?.task || state.terminal) return undefined;
|
|
121
|
+
|
|
122
|
+
const link: TaskWorkflowLink = {
|
|
123
|
+
loopId: entry.id,
|
|
124
|
+
stateId: workflow.currentState,
|
|
125
|
+
transitionSeq: workflow.transitionSeq,
|
|
126
|
+
};
|
|
127
|
+
const metadata = { workflow: link };
|
|
128
|
+
if (isTasksAvailable()) {
|
|
129
|
+
try {
|
|
130
|
+
const reply = await rpcCall<CreateTaskReply>(pi.events, TASKS_RPC.create, {
|
|
131
|
+
subject: state.task.subject,
|
|
132
|
+
description: state.task.description,
|
|
133
|
+
metadata,
|
|
134
|
+
}, 5000);
|
|
135
|
+
return reply.id;
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isDetectionSettled && !isDetectionSettled()) return undefined;
|
|
142
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
143
|
+
if (!nativeTaskStore) return undefined;
|
|
144
|
+
const task = nativeTaskStore.create(state.task.subject, state.task.description, metadata, link);
|
|
145
|
+
emitNativeTaskEvent(pi, "tasks:created", task);
|
|
146
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
147
|
+
return task.id;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function completeWorkflowTask(taskId: string): Promise<boolean> {
|
|
151
|
+
if (isTasksAvailable()) {
|
|
152
|
+
try {
|
|
153
|
+
await rpcCall<UpdateTaskReply>(pi.events, TASKS_RPC.update, { id: taskId, status: "completed" }, 5000);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
161
|
+
const existing = nativeTaskStore?.get(taskId);
|
|
162
|
+
if (!nativeTaskStore || !existing) return false;
|
|
163
|
+
if (existing.status === "completed") return true;
|
|
164
|
+
|
|
165
|
+
const completed = nativeTaskStore.complete(taskId);
|
|
166
|
+
if (!completed) return false;
|
|
167
|
+
emitNativeTaskEvent(pi, "tasks:completed", completed, existing.status);
|
|
168
|
+
await onNativeTaskCompleted?.(nativeTaskStore);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
108
172
|
async function hasPendingTasks(): Promise<number> {
|
|
109
173
|
if (isTasksAvailable()) {
|
|
110
174
|
// -1 is this bridge's "unknown" sentinel, consumed by notification-runtime;
|
|
@@ -140,6 +204,8 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
|
|
|
140
204
|
return {
|
|
141
205
|
checkTasksVersion,
|
|
142
206
|
autoCreateTask,
|
|
207
|
+
createWorkflowTask,
|
|
208
|
+
completeWorkflowTask,
|
|
143
209
|
hasPendingTasks,
|
|
144
210
|
cleanDoneTasks,
|
|
145
211
|
};
|
package/src/store.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { type LoopReducerEvent, type LoopReducerState, reduceLoopState } from "./loop-reducer.js";
|
|
4
4
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
5
|
-
import type { DynamicLoopState, LoopDeletionTombstone, LoopDeletionTombstoneInput, LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
5
|
+
import type { DynamicLoopState, LoopDeletionTombstone, LoopDeletionTombstoneInput, LoopEntry, LoopStoreData, Trigger, WorkflowDefinition, WorkflowTerminalStatus } from "./types.js";
|
|
6
|
+
import { transitionWorkflowRun, validateWorkflowDefinition, type WorkflowTransitionInput } from "./workflow-reducer.js";
|
|
6
7
|
|
|
7
8
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
8
9
|
const MAX_LOOPS = 25;
|
|
@@ -25,11 +26,16 @@ export class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducerState, L
|
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; taskBacklog?: boolean; readOnly?: boolean; maxFires?: number; dynamic?: Partial<DynamicLoopState
|
|
29
|
+
create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; taskBacklog?: boolean; readOnly?: boolean; maxFires?: number; dynamic?: Partial<DynamicLoopState>; workflow?: WorkflowDefinition }): LoopEntry {
|
|
29
30
|
return this.withLock(() => {
|
|
30
31
|
if (this.entries.size >= MAX_LOOPS) {
|
|
31
32
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
32
33
|
}
|
|
34
|
+
if (opts.workflow) {
|
|
35
|
+
if (trigger.type !== "dynamic") throw new Error("Workflow loops require a dynamic trigger.");
|
|
36
|
+
const validationError = validateWorkflowDefinition(opts.workflow);
|
|
37
|
+
if (validationError) throw new Error(`Invalid workflow: ${validationError}`);
|
|
38
|
+
}
|
|
33
39
|
const now = Date.now();
|
|
34
40
|
this.applyReducerEvent({
|
|
35
41
|
type: "LOOP_CREATED",
|
|
@@ -45,6 +51,7 @@ export class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducerState, L
|
|
|
45
51
|
readOnly: opts.readOnly,
|
|
46
52
|
maxFires: opts.maxFires,
|
|
47
53
|
dynamic: opts.dynamic,
|
|
54
|
+
workflow: opts.workflow,
|
|
48
55
|
},
|
|
49
56
|
});
|
|
50
57
|
return this.entries.get(String(this.nextId - 1))!;
|
|
@@ -154,6 +161,48 @@ export class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducerState, L
|
|
|
154
161
|
});
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
transitionWorkflow(id: string, input: WorkflowTransitionInput): { entry?: LoopEntry; applied: boolean; error?: string; terminal?: WorkflowTerminalStatus } {
|
|
165
|
+
return this.withLock(() => {
|
|
166
|
+
const entry = this.entries.get(id);
|
|
167
|
+
if (!entry) return { applied: false, error: `Loop #${id} not found` };
|
|
168
|
+
if (!entry.workflow) return { applied: false, error: `Loop #${id} is not a workflow loop` };
|
|
169
|
+
|
|
170
|
+
const result = transitionWorkflowRun(entry.workflow, input, Date.now());
|
|
171
|
+
if (!result.applied) return { applied: false, error: result.error };
|
|
172
|
+
|
|
173
|
+
this.applyReducerEvent({
|
|
174
|
+
type: "LOOP_WORKFLOW_TRANSITION",
|
|
175
|
+
at: result.run.stateEnteredAt,
|
|
176
|
+
source: "tool",
|
|
177
|
+
entityType: "loop",
|
|
178
|
+
entityId: id,
|
|
179
|
+
payload: {
|
|
180
|
+
id,
|
|
181
|
+
outcome: input.outcome,
|
|
182
|
+
evidence: input.evidence,
|
|
183
|
+
activeTaskId: input.activeTaskId,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return { entry: this.entries.get(id), applied: true, terminal: result.terminal };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setWorkflowActiveTask(id: string, taskId?: string): LoopEntry | undefined {
|
|
191
|
+
return this.withLock(() => {
|
|
192
|
+
const entry = this.entries.get(id);
|
|
193
|
+
if (!entry?.workflow) return undefined;
|
|
194
|
+
this.applyReducerEvent({
|
|
195
|
+
type: "LOOP_WORKFLOW_TASK_SET",
|
|
196
|
+
at: Date.now(),
|
|
197
|
+
source: "tool",
|
|
198
|
+
entityType: "loop",
|
|
199
|
+
entityId: id,
|
|
200
|
+
payload: { id, taskId },
|
|
201
|
+
});
|
|
202
|
+
return this.entries.get(id);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
157
206
|
getDeletionTombstone(id: string): LoopDeletionTombstone | undefined {
|
|
158
207
|
const tombstone = this.tombstones.get(id);
|
|
159
208
|
if (!tombstone) return undefined;
|
package/src/task-reducer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskEntry } from "./task-types.js";
|
|
1
|
+
import type { TaskEntry, TaskWorkflowLink } from "./task-types.js";
|
|
2
2
|
|
|
3
3
|
export interface TaskReducerState {
|
|
4
4
|
nextId: number;
|
|
@@ -16,6 +16,7 @@ export type TaskReducerEvent =
|
|
|
16
16
|
subject: string;
|
|
17
17
|
description: string;
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
|
+
workflow?: TaskWorkflowLink;
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
| {
|
|
@@ -87,6 +88,7 @@ export function reduceTaskState(state: TaskReducerState, event: TaskReducerEvent
|
|
|
87
88
|
createdAt: event.at,
|
|
88
89
|
updatedAt: event.at,
|
|
89
90
|
metadata: event.payload.metadata,
|
|
91
|
+
workflow: event.payload.workflow,
|
|
90
92
|
};
|
|
91
93
|
next.tasksById[id] = task;
|
|
92
94
|
return {
|
package/src/task-store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
4
4
|
import { reduceTaskState, type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
5
|
-
import type { TaskEntry, TaskStoreData } from "./task-types.js";
|
|
5
|
+
import type { TaskEntry, TaskStoreData, TaskWorkflowLink } from "./task-types.js";
|
|
6
6
|
|
|
7
7
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
8
8
|
const MAX_TASKS = 200;
|
|
@@ -22,7 +22,7 @@ export class TaskStore extends ReducerBackedStore<TaskEntry, TaskReducerState, T
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
create(subject: string, description: string, metadata?: Record<string, unknown
|
|
25
|
+
create(subject: string, description: string, metadata?: Record<string, unknown>, workflow?: TaskWorkflowLink): TaskEntry {
|
|
26
26
|
return this.withLock(() => {
|
|
27
27
|
if (this.entries.size >= MAX_TASKS) {
|
|
28
28
|
throw new Error(`Maximum of ${MAX_TASKS} tasks reached. Delete some before creating new ones.`);
|
|
@@ -33,7 +33,7 @@ export class TaskStore extends ReducerBackedStore<TaskEntry, TaskReducerState, T
|
|
|
33
33
|
at: now,
|
|
34
34
|
source: "tool",
|
|
35
35
|
entityType: "task",
|
|
36
|
-
payload: { subject, description, metadata },
|
|
36
|
+
payload: { subject, description, metadata, workflow },
|
|
37
37
|
});
|
|
38
38
|
return this.entries.get(String(this.nextId - 1))!;
|
|
39
39
|
});
|
package/src/task-types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type TaskStatus = "pending" | "in_progress" | "completed";
|
|
2
2
|
|
|
3
|
+
export interface TaskWorkflowLink {
|
|
4
|
+
loopId: string;
|
|
5
|
+
stateId: string;
|
|
6
|
+
transitionSeq: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export interface TaskEntry {
|
|
4
10
|
id: string;
|
|
5
11
|
subject: string;
|
|
@@ -9,6 +15,7 @@ export interface TaskEntry {
|
|
|
9
15
|
updatedAt: number;
|
|
10
16
|
completedAt?: number;
|
|
11
17
|
metadata?: Record<string, unknown>;
|
|
18
|
+
workflow?: TaskWorkflowLink;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export interface TaskStoreData {
|