@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  3. package/dist/api.d.ts +2 -1
  4. package/dist/index.js +10 -0
  5. package/dist/loop-reducer.d.ts +24 -1
  6. package/dist/loop-reducer.js +31 -0
  7. package/dist/notification-reducer.d.ts +2 -1
  8. package/dist/runtime/notification-runtime.d.ts +2 -1
  9. package/dist/runtime/notification-runtime.js +18 -0
  10. package/dist/runtime/task-events.d.ts +2 -1
  11. package/dist/runtime/task-events.js +1 -0
  12. package/dist/runtime/task-rpc.d.ts +4 -0
  13. package/dist/runtime/task-rpc.js +62 -1
  14. package/dist/store.d.ts +10 -1
  15. package/dist/store.js +51 -0
  16. package/dist/task-reducer.d.ts +2 -1
  17. package/dist/task-reducer.js +1 -0
  18. package/dist/task-store.d.ts +2 -2
  19. package/dist/task-store.js +2 -2
  20. package/dist/task-types.d.ts +6 -0
  21. package/dist/tools/loop-tools.d.ts +17 -1
  22. package/dist/tools/loop-tools.js +320 -21
  23. package/dist/tools/monitor-tools.js +43 -9
  24. package/dist/tools/native-task-tools.js +56 -11
  25. package/dist/tools/tool-result.d.ts +12 -2
  26. package/dist/tools/tool-result.js +7 -3
  27. package/dist/types.d.ts +35 -0
  28. package/dist/ui/tool-renderer.d.ts +7 -0
  29. package/dist/ui/tool-renderer.js +34 -0
  30. package/dist/ui/widget.js +4 -4
  31. package/dist/workflow-reducer.d.ts +18 -0
  32. package/dist/workflow-reducer.js +82 -0
  33. package/docs/USAGE_GUIDE.md +41 -0
  34. package/package.json +3 -3
  35. package/src/api.ts +9 -1
  36. package/src/index.ts +10 -0
  37. package/src/loop-reducer.ts +53 -1
  38. package/src/notification-reducer.ts +2 -1
  39. package/src/runtime/notification-runtime.ts +21 -1
  40. package/src/runtime/task-events.ts +3 -1
  41. package/src/runtime/task-rpc.ts +66 -0
  42. package/src/store.ts +51 -2
  43. package/src/task-reducer.ts +3 -1
  44. package/src/task-store.ts +3 -3
  45. package/src/task-types.ts +7 -0
  46. package/src/tools/loop-tools.ts +365 -15
  47. package/src/tools/monitor-tools.ts +44 -7
  48. package/src/tools/native-task-tools.ts +56 -8
  49. package/src/tools/tool-result.ts +18 -2
  50. package/src/types.ts +41 -0
  51. package/src/ui/tool-renderer.ts +45 -0
  52. package/src/ui/widget.ts +4 -4
  53. package/src/workflow-reducer.ts +107 -0
@@ -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> }): LoopEntry {
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;
@@ -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>): TaskEntry {
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 {