@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.4](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.3...pi-loop-v0.6.4) (2026-07-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **ui:** render compact status cards ([02925ae](https://github.com/trvon/pi-loop/commit/02925ae2af1404fd86f8daab4f5ecef3782550e3))
|
|
9
|
+
* **workflow:** add state loop foundation ([2fddbe9](https://github.com/trvon/pi-loop/commit/2fddbe904cf3b413618572b8488e8aa9357590cd))
|
|
10
|
+
* **workflow:** add task-driven state loops ([f174211](https://github.com/trvon/pi-loop/commit/f1742117a66c9700b08427aa6528234546f5b9cc))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **deps:** raise Pi security baseline ([5f538cf](https://github.com/trvon/pi-loop/commit/5f538cfb44cbb5e254ec87b5cd35f6ece1535fda))
|
|
16
|
+
|
|
3
17
|
## [0.6.3](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.2...pi-loop-v0.6.3) (2026-07-17)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ MonitorStop monitorId="1"
|
|
|
38
38
|
|
|
39
39
|
## What it provides
|
|
40
40
|
|
|
41
|
-
- Cron, event, hybrid,
|
|
41
|
+
- Cron, event, hybrid, dynamic goal, and opt-in workflow loops
|
|
42
42
|
- Idle-safe agent re-wakes with dynamic-loop restart/session-switch recovery
|
|
43
43
|
- Background command monitoring with buffered output and `onDone` wakes
|
|
44
44
|
- Optional `pi-tasks` integration and a native task fallback
|
|
@@ -50,7 +50,8 @@ MonitorStop monitorId="1"
|
|
|
50
50
|
|---|---|
|
|
51
51
|
| `/loop` | Create or manage scheduled, event, and dynamic goal loops |
|
|
52
52
|
| `/tasks` | Manage native fallback tasks when `pi-tasks` is absent |
|
|
53
|
-
| `LoopCreate`, `LoopList`, `LoopUpdate`, `LoopDelete` | Create and control loops |
|
|
53
|
+
| `LoopCreate`, `LoopList`, `LoopUpdate`, `LoopDelete` | Create and control ordinary loops |
|
|
54
|
+
| `WorkflowCreate`, `WorkflowList`, `WorkflowTransition` | Create and advance opt-in task-driven workflows |
|
|
54
55
|
| `MonitorCreate`, `MonitorList`, `MonitorStop` | Run and inspect background commands |
|
|
55
56
|
| `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete` | Native fallback task management |
|
|
56
57
|
|
package/dist/api.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export { type HandleRpcOptions, handleRpc, PROTOCOL_VERSION, RpcError, type RpcE
|
|
|
8
8
|
export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
|
|
9
9
|
export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
10
10
|
export { TaskStore } from "./task-store.js";
|
|
11
|
-
export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
|
|
11
|
+
export type { TaskEntry, TaskStatus, TaskStoreData, TaskWorkflowLink } from "./task-types.js";
|
|
12
|
+
export type { WorkflowDefinition, WorkflowRunState, WorkflowStateDefinition, WorkflowTaskDefinition, WorkflowTerminalStatus, WorkflowTransitionRecord, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -116,6 +116,10 @@ export default function (pi) {
|
|
|
116
116
|
onDetectionSettled: () => {
|
|
117
117
|
tasksDetectionSettled = true;
|
|
118
118
|
},
|
|
119
|
+
isDetectionSettled: () => tasksDetectionSettled,
|
|
120
|
+
onNativeTaskCompleted: () => {
|
|
121
|
+
widget.update();
|
|
122
|
+
},
|
|
119
123
|
debug,
|
|
120
124
|
});
|
|
121
125
|
// The RPC server registers at init (not behind the 6s tool-fallback timer) so
|
|
@@ -256,6 +260,7 @@ export default function (pi) {
|
|
|
256
260
|
autoTask: firedEntry.autoTask,
|
|
257
261
|
taskBacklog: firedEntry.taskBacklog,
|
|
258
262
|
dynamic: firedEntry.dynamic,
|
|
263
|
+
workflow: firedEntry.workflow,
|
|
259
264
|
});
|
|
260
265
|
}
|
|
261
266
|
// ── Session lifecycle ──
|
|
@@ -315,6 +320,11 @@ export default function (pi) {
|
|
|
315
320
|
},
|
|
316
321
|
maybeBootstrapTaskLoop,
|
|
317
322
|
isTaskSystemReady: () => tasksAvailable || nativeTasksRegistered,
|
|
323
|
+
onDynamicLoopActivated: (entry) => {
|
|
324
|
+
onLoopFire(entry);
|
|
325
|
+
},
|
|
326
|
+
createWorkflowTask: (entry) => taskRuntime.createWorkflowTask(entry),
|
|
327
|
+
completeWorkflowTask: (taskId) => taskRuntime.completeWorkflowTask(taskId),
|
|
318
328
|
});
|
|
319
329
|
function handleMonitorDoneLoop(doneLoop, monitorId) {
|
|
320
330
|
monitorOnDoneRuntime.register(doneLoop, monitorId);
|
package/dist/loop-reducer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DynamicLoopState, LoopEntry, Trigger } from "./types.js";
|
|
1
|
+
import type { DynamicLoopState, LoopEntry, Trigger, WorkflowDefinition } from "./types.js";
|
|
2
2
|
export declare const MAX_LOOP_EXPIRY_MS: number;
|
|
3
3
|
/**
|
|
4
4
|
* Whether a loop has reached its fire cap. Single source of truth for the
|
|
@@ -27,6 +27,7 @@ export type LoopReducerEvent = {
|
|
|
27
27
|
readOnly?: boolean;
|
|
28
28
|
maxFires?: number;
|
|
29
29
|
dynamic?: Partial<DynamicLoopState>;
|
|
30
|
+
workflow?: WorkflowDefinition;
|
|
30
31
|
};
|
|
31
32
|
} | {
|
|
32
33
|
type: "LOOP_PAUSED" | "LOOP_RESUMED" | "LOOP_FIRED" | "LOOP_DELETED" | "LOOP_MAX_FIRES_REACHED" | "LOOP_BACKLOG_EMPTY";
|
|
@@ -58,6 +59,28 @@ export type LoopReducerEvent = {
|
|
|
58
59
|
prompt?: string;
|
|
59
60
|
dynamic: Partial<DynamicLoopState>;
|
|
60
61
|
};
|
|
62
|
+
} | {
|
|
63
|
+
type: "LOOP_WORKFLOW_TRANSITION";
|
|
64
|
+
at: number;
|
|
65
|
+
source: ReducerSource;
|
|
66
|
+
entityType?: "loop";
|
|
67
|
+
entityId?: string;
|
|
68
|
+
payload: {
|
|
69
|
+
id: string;
|
|
70
|
+
outcome: string;
|
|
71
|
+
evidence?: string;
|
|
72
|
+
activeTaskId?: string;
|
|
73
|
+
};
|
|
74
|
+
} | {
|
|
75
|
+
type: "LOOP_WORKFLOW_TASK_SET";
|
|
76
|
+
at: number;
|
|
77
|
+
source: ReducerSource;
|
|
78
|
+
entityType?: "loop";
|
|
79
|
+
entityId?: string;
|
|
80
|
+
payload: {
|
|
81
|
+
id: string;
|
|
82
|
+
taskId?: string;
|
|
83
|
+
};
|
|
61
84
|
};
|
|
62
85
|
export type LoopReducerEffect = {
|
|
63
86
|
type: "PERSIST_LOOP";
|
package/dist/loop-reducer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createWorkflowRun, transitionWorkflowRun } from "./workflow-reducer.js";
|
|
1
2
|
export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
2
3
|
/**
|
|
3
4
|
* Whether a loop has reached its fire cap. Single source of truth for the
|
|
@@ -44,6 +45,7 @@ export function reduceLoopState(state, event) {
|
|
|
44
45
|
lastUpdatedAt: event.payload.dynamic?.lastUpdatedAt ?? event.at,
|
|
45
46
|
}
|
|
46
47
|
: undefined,
|
|
48
|
+
workflow: event.payload.workflow ? createWorkflowRun(event.payload.workflow, event.at) : undefined,
|
|
47
49
|
};
|
|
48
50
|
next.loopsById[id] = loop;
|
|
49
51
|
return {
|
|
@@ -94,6 +96,35 @@ export function reduceLoopState(state, event) {
|
|
|
94
96
|
};
|
|
95
97
|
loop.updatedAt = event.at;
|
|
96
98
|
}
|
|
99
|
+
if (event.type === "LOOP_WORKFLOW_TRANSITION") {
|
|
100
|
+
if (!loop.workflow)
|
|
101
|
+
return { state, effects: [] };
|
|
102
|
+
const result = transitionWorkflowRun(loop.workflow, {
|
|
103
|
+
outcome: event.payload.outcome,
|
|
104
|
+
evidence: event.payload.evidence,
|
|
105
|
+
activeTaskId: event.payload.activeTaskId,
|
|
106
|
+
}, event.at);
|
|
107
|
+
if (!result.applied)
|
|
108
|
+
return { state, effects: [] };
|
|
109
|
+
loop.workflow = result.run;
|
|
110
|
+
loop.dynamic = {
|
|
111
|
+
goal: loop.dynamic?.goal ?? loop.prompt,
|
|
112
|
+
state: result.run.currentState,
|
|
113
|
+
metrics: loop.dynamic?.metrics,
|
|
114
|
+
doneCriteria: loop.dynamic?.doneCriteria,
|
|
115
|
+
iteration: (loop.dynamic?.iteration ?? 0) + 1,
|
|
116
|
+
nextWakeAt: undefined,
|
|
117
|
+
awaitingUpdate: false,
|
|
118
|
+
lastUpdatedAt: event.at,
|
|
119
|
+
};
|
|
120
|
+
loop.updatedAt = event.at;
|
|
121
|
+
}
|
|
122
|
+
if (event.type === "LOOP_WORKFLOW_TASK_SET") {
|
|
123
|
+
if (!loop.workflow)
|
|
124
|
+
return { state, effects: [] };
|
|
125
|
+
loop.workflow = { ...loop.workflow, activeTaskId: event.payload.taskId };
|
|
126
|
+
loop.updatedAt = event.at;
|
|
127
|
+
}
|
|
97
128
|
next.loopsById[id] = loop;
|
|
98
129
|
return {
|
|
99
130
|
state: next,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DynamicLoopState } from "./types.js";
|
|
1
|
+
import type { DynamicLoopState, WorkflowRunState } from "./types.js";
|
|
2
2
|
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
3
3
|
export interface ReducerNotification {
|
|
4
4
|
key: string;
|
|
@@ -12,6 +12,7 @@ export interface ReducerNotification {
|
|
|
12
12
|
taskBacklog?: boolean;
|
|
13
13
|
readOnly?: boolean;
|
|
14
14
|
dynamic?: DynamicLoopState;
|
|
15
|
+
workflow?: WorkflowRunState;
|
|
15
16
|
}
|
|
16
17
|
export interface NotificationReducerState {
|
|
17
18
|
notificationsByKey: Record<string, ReducerNotification>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { DynamicLoopState, Trigger } from "../types.js";
|
|
2
|
+
import type { DynamicLoopState, Trigger, WorkflowRunState } from "../types.js";
|
|
3
3
|
export interface LoopFireEvent {
|
|
4
4
|
loopId: string;
|
|
5
5
|
prompt: string;
|
|
@@ -11,6 +11,7 @@ export interface LoopFireEvent {
|
|
|
11
11
|
autoTask?: boolean;
|
|
12
12
|
taskBacklog?: boolean;
|
|
13
13
|
dynamic?: DynamicLoopState;
|
|
14
|
+
workflow?: WorkflowRunState;
|
|
14
15
|
}
|
|
15
16
|
export interface PendingNotification extends LoopFireEvent {
|
|
16
17
|
key: string;
|
|
@@ -50,6 +50,23 @@ export function createNotificationRuntime(options) {
|
|
|
50
50
|
const constraint = data.readOnly
|
|
51
51
|
? "\n\nREAD-ONLY MODE — use only read tools (Read, TaskList, LoopList, MonitorList, etc.). No file writes, shell execution, or destructive changes."
|
|
52
52
|
: "";
|
|
53
|
+
if (data.workflow) {
|
|
54
|
+
const state = data.workflow.definition.states[data.workflow.currentState];
|
|
55
|
+
const outcomes = Object.keys(state?.on ?? {});
|
|
56
|
+
const lines = [
|
|
57
|
+
`[pi-loop] Loop #${loopId} fired (workflow).${constraint}`,
|
|
58
|
+
`Goal: ${data.prompt || data.workflow.definition.initialState}`,
|
|
59
|
+
`State: ${data.workflow.currentState}`,
|
|
60
|
+
];
|
|
61
|
+
if (state?.prompt)
|
|
62
|
+
lines.push(`State instructions: ${state.prompt}`);
|
|
63
|
+
if (data.workflow.activeTaskId)
|
|
64
|
+
lines.push(`Active task: #${data.workflow.activeTaskId}`);
|
|
65
|
+
if (outcomes.length > 0)
|
|
66
|
+
lines.push(`Allowed outcomes: ${outcomes.join(", ")}`);
|
|
67
|
+
lines.push(`Workflow lifecycle: Loop #${loopId} is an opt-in state controller. Do not call LoopDelete after this state.`, "Before ending this turn, call WorkflowTransition exactly once with this workflow id and one allowed outcome. Include evidence for the branch decision. Terminal outcomes complete or pause the workflow automatically.");
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
53
70
|
if (data.dynamic || (typeof data.trigger !== "string" && data.trigger?.type === "dynamic")) {
|
|
54
71
|
const dynamic = data.dynamic;
|
|
55
72
|
const lines = [
|
|
@@ -108,6 +125,7 @@ export function createNotificationRuntime(options) {
|
|
|
108
125
|
autoTask: notification.autoTask,
|
|
109
126
|
taskBacklog: notification.taskBacklog,
|
|
110
127
|
dynamic: notification.dynamic,
|
|
128
|
+
workflow: notification.workflow,
|
|
111
129
|
timestamp: notification.timestamp,
|
|
112
130
|
},
|
|
113
131
|
}, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { TaskEntry, TaskStatus } from "../task-types.js";
|
|
2
|
+
import type { TaskEntry, TaskStatus, TaskWorkflowLink } from "../task-types.js";
|
|
3
3
|
export type NativeTaskEventName = "tasks:created" | "tasks:started" | "tasks:completed" | "tasks:reopened" | "tasks:updated" | "tasks:deleted";
|
|
4
4
|
export interface NativeTaskEventPayload {
|
|
5
5
|
taskId: string;
|
|
@@ -11,5 +11,6 @@ export interface NativeTaskEventPayload {
|
|
|
11
11
|
updatedAt: number;
|
|
12
12
|
completedAt?: number;
|
|
13
13
|
metadata?: Record<string, unknown>;
|
|
14
|
+
workflow?: TaskWorkflowLink;
|
|
14
15
|
}
|
|
15
16
|
export declare function emitNativeTaskEvent(pi: ExtensionAPI, name: NativeTaskEventName, entry: TaskEntry, previousStatus?: TaskStatus): void;
|
|
@@ -7,7 +7,9 @@ export interface TaskRuntimeBridgeOptions {
|
|
|
7
7
|
setTasksAvailable: (available: boolean) => void;
|
|
8
8
|
getNativeTaskStore: () => TaskStore | undefined;
|
|
9
9
|
onNativeTaskCreated?: (taskStore: TaskStore) => void;
|
|
10
|
+
onNativeTaskCompleted?: (taskStore: TaskStore) => Promise<void> | void;
|
|
10
11
|
onNativeTasksPruned?: (taskStore: TaskStore) => Promise<void> | void;
|
|
12
|
+
isDetectionSettled?: () => boolean;
|
|
11
13
|
/** Called when a detection window opens. */
|
|
12
14
|
onDetectionStarted?: () => void;
|
|
13
15
|
/** Called when a detection window closes (provider found or probe timed out). */
|
|
@@ -17,6 +19,8 @@ export interface TaskRuntimeBridgeOptions {
|
|
|
17
19
|
export interface TaskRuntimeBridge {
|
|
18
20
|
checkTasksVersion(): void;
|
|
19
21
|
autoCreateTask(entry: LoopEntry): Promise<string | undefined>;
|
|
22
|
+
createWorkflowTask(entry: LoopEntry): Promise<string | undefined>;
|
|
23
|
+
completeWorkflowTask(taskId: string): Promise<boolean>;
|
|
20
24
|
hasPendingTasks(): Promise<number>;
|
|
21
25
|
cleanDoneTasks(): Promise<void>;
|
|
22
26
|
}
|
package/dist/runtime/task-rpc.js
CHANGED
|
@@ -4,7 +4,7 @@ import { rpcCall } from "../rpc/cross-extension-rpc.js";
|
|
|
4
4
|
import { NATIVE_TASKS_PROVIDER } from "./native-task-rpc.js";
|
|
5
5
|
import { emitNativeTaskEvent } from "./task-events.js";
|
|
6
6
|
export function createTaskRuntimeBridge(options) {
|
|
7
|
-
const { pi, isTasksAvailable, setTasksAvailable, getNativeTaskStore, onNativeTaskCreated, onNativeTasksPruned, onDetectionStarted, onDetectionSettled, debug, } = options;
|
|
7
|
+
const { pi, isTasksAvailable, setTasksAvailable, getNativeTaskStore, onNativeTaskCreated, onNativeTaskCompleted, onNativeTasksPruned, isDetectionSettled, onDetectionStarted, onDetectionSettled, debug, } = options;
|
|
8
8
|
let detectionEpoch = 0;
|
|
9
9
|
function checkTasksVersion() {
|
|
10
10
|
// Not rpcProbe: pi-loop's own native server also answers this ping, and a
|
|
@@ -62,6 +62,65 @@ export function createTaskRuntimeBridge(options) {
|
|
|
62
62
|
onNativeTaskCreated?.(nativeTaskStore);
|
|
63
63
|
return task.id;
|
|
64
64
|
}
|
|
65
|
+
async function createWorkflowTask(entry) {
|
|
66
|
+
const workflow = entry.workflow;
|
|
67
|
+
if (!workflow)
|
|
68
|
+
return undefined;
|
|
69
|
+
const state = workflow.definition.states[workflow.currentState];
|
|
70
|
+
if (!state?.task || state.terminal)
|
|
71
|
+
return undefined;
|
|
72
|
+
const link = {
|
|
73
|
+
loopId: entry.id,
|
|
74
|
+
stateId: workflow.currentState,
|
|
75
|
+
transitionSeq: workflow.transitionSeq,
|
|
76
|
+
};
|
|
77
|
+
const metadata = { workflow: link };
|
|
78
|
+
if (isTasksAvailable()) {
|
|
79
|
+
try {
|
|
80
|
+
const reply = await rpcCall(pi.events, TASKS_RPC.create, {
|
|
81
|
+
subject: state.task.subject,
|
|
82
|
+
description: state.task.description,
|
|
83
|
+
metadata,
|
|
84
|
+
}, 5000);
|
|
85
|
+
return reply.id;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (isDetectionSettled && !isDetectionSettled())
|
|
92
|
+
return undefined;
|
|
93
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
94
|
+
if (!nativeTaskStore)
|
|
95
|
+
return undefined;
|
|
96
|
+
const task = nativeTaskStore.create(state.task.subject, state.task.description, metadata, link);
|
|
97
|
+
emitNativeTaskEvent(pi, "tasks:created", task);
|
|
98
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
99
|
+
return task.id;
|
|
100
|
+
}
|
|
101
|
+
async function completeWorkflowTask(taskId) {
|
|
102
|
+
if (isTasksAvailable()) {
|
|
103
|
+
try {
|
|
104
|
+
await rpcCall(pi.events, TASKS_RPC.update, { id: taskId, status: "completed" }, 5000);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
112
|
+
const existing = nativeTaskStore?.get(taskId);
|
|
113
|
+
if (!nativeTaskStore || !existing)
|
|
114
|
+
return false;
|
|
115
|
+
if (existing.status === "completed")
|
|
116
|
+
return true;
|
|
117
|
+
const completed = nativeTaskStore.complete(taskId);
|
|
118
|
+
if (!completed)
|
|
119
|
+
return false;
|
|
120
|
+
emitNativeTaskEvent(pi, "tasks:completed", completed, existing.status);
|
|
121
|
+
await onNativeTaskCompleted?.(nativeTaskStore);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
65
124
|
async function hasPendingTasks() {
|
|
66
125
|
if (isTasksAvailable()) {
|
|
67
126
|
// -1 is this bridge's "unknown" sentinel, consumed by notification-runtime;
|
|
@@ -96,6 +155,8 @@ export function createTaskRuntimeBridge(options) {
|
|
|
96
155
|
return {
|
|
97
156
|
checkTasksVersion,
|
|
98
157
|
autoCreateTask,
|
|
158
|
+
createWorkflowTask,
|
|
159
|
+
completeWorkflowTask,
|
|
99
160
|
hasPendingTasks,
|
|
100
161
|
cleanDoneTasks,
|
|
101
162
|
};
|
package/dist/store.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type LoopReducerEvent, type LoopReducerState } from "./loop-reducer.js";
|
|
2
2
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
3
|
-
import type { DynamicLoopState, LoopDeletionTombstone, LoopDeletionTombstoneInput, LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
3
|
+
import type { DynamicLoopState, LoopDeletionTombstone, LoopDeletionTombstoneInput, LoopEntry, LoopStoreData, Trigger, WorkflowDefinition, WorkflowTerminalStatus } from "./types.js";
|
|
4
|
+
import { type WorkflowTransitionInput } from "./workflow-reducer.js";
|
|
4
5
|
export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducerState, LoopReducerEvent, LoopStoreData> {
|
|
5
6
|
private tombstones;
|
|
6
7
|
constructor(listIdOrPath?: string);
|
|
@@ -11,6 +12,7 @@ export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducer
|
|
|
11
12
|
readOnly?: boolean;
|
|
12
13
|
maxFires?: number;
|
|
13
14
|
dynamic?: Partial<DynamicLoopState>;
|
|
15
|
+
workflow?: WorkflowDefinition;
|
|
14
16
|
}): LoopEntry;
|
|
15
17
|
pause(id: string): LoopEntry | undefined;
|
|
16
18
|
resume(id: string): LoopEntry | undefined;
|
|
@@ -26,6 +28,13 @@ export declare class LoopStore extends ReducerBackedStore<LoopEntry, LoopReducer
|
|
|
26
28
|
prompt?: string;
|
|
27
29
|
dynamic: Partial<DynamicLoopState>;
|
|
28
30
|
}): LoopEntry | undefined;
|
|
31
|
+
transitionWorkflow(id: string, input: WorkflowTransitionInput): {
|
|
32
|
+
entry?: LoopEntry;
|
|
33
|
+
applied: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
terminal?: WorkflowTerminalStatus;
|
|
36
|
+
};
|
|
37
|
+
setWorkflowActiveTask(id: string, taskId?: string): LoopEntry | undefined;
|
|
29
38
|
getDeletionTombstone(id: string): LoopDeletionTombstone | undefined;
|
|
30
39
|
recordDeletionTombstone(id: string, input: LoopDeletionTombstoneInput): LoopDeletionTombstone | undefined;
|
|
31
40
|
delete(id: string): boolean;
|
package/dist/store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { reduceLoopState } from "./loop-reducer.js";
|
|
4
4
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
5
|
+
import { transitionWorkflowRun, validateWorkflowDefinition } from "./workflow-reducer.js";
|
|
5
6
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
6
7
|
const MAX_LOOPS = 25;
|
|
7
8
|
const TOMBSTONE_TTL_MS = 10 * 60 * 1000;
|
|
@@ -22,6 +23,13 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
22
23
|
if (this.entries.size >= MAX_LOOPS) {
|
|
23
24
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
24
25
|
}
|
|
26
|
+
if (opts.workflow) {
|
|
27
|
+
if (trigger.type !== "dynamic")
|
|
28
|
+
throw new Error("Workflow loops require a dynamic trigger.");
|
|
29
|
+
const validationError = validateWorkflowDefinition(opts.workflow);
|
|
30
|
+
if (validationError)
|
|
31
|
+
throw new Error(`Invalid workflow: ${validationError}`);
|
|
32
|
+
}
|
|
25
33
|
const now = Date.now();
|
|
26
34
|
this.applyReducerEvent({
|
|
27
35
|
type: "LOOP_CREATED",
|
|
@@ -37,6 +45,7 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
37
45
|
readOnly: opts.readOnly,
|
|
38
46
|
maxFires: opts.maxFires,
|
|
39
47
|
dynamic: opts.dynamic,
|
|
48
|
+
workflow: opts.workflow,
|
|
40
49
|
},
|
|
41
50
|
});
|
|
42
51
|
return this.entries.get(String(this.nextId - 1));
|
|
@@ -142,6 +151,48 @@ export class LoopStore extends ReducerBackedStore {
|
|
|
142
151
|
return this.entries.get(id);
|
|
143
152
|
});
|
|
144
153
|
}
|
|
154
|
+
transitionWorkflow(id, input) {
|
|
155
|
+
return this.withLock(() => {
|
|
156
|
+
const entry = this.entries.get(id);
|
|
157
|
+
if (!entry)
|
|
158
|
+
return { applied: false, error: `Loop #${id} not found` };
|
|
159
|
+
if (!entry.workflow)
|
|
160
|
+
return { applied: false, error: `Loop #${id} is not a workflow loop` };
|
|
161
|
+
const result = transitionWorkflowRun(entry.workflow, input, Date.now());
|
|
162
|
+
if (!result.applied)
|
|
163
|
+
return { applied: false, error: result.error };
|
|
164
|
+
this.applyReducerEvent({
|
|
165
|
+
type: "LOOP_WORKFLOW_TRANSITION",
|
|
166
|
+
at: result.run.stateEnteredAt,
|
|
167
|
+
source: "tool",
|
|
168
|
+
entityType: "loop",
|
|
169
|
+
entityId: id,
|
|
170
|
+
payload: {
|
|
171
|
+
id,
|
|
172
|
+
outcome: input.outcome,
|
|
173
|
+
evidence: input.evidence,
|
|
174
|
+
activeTaskId: input.activeTaskId,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
return { entry: this.entries.get(id), applied: true, terminal: result.terminal };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
setWorkflowActiveTask(id, taskId) {
|
|
181
|
+
return this.withLock(() => {
|
|
182
|
+
const entry = this.entries.get(id);
|
|
183
|
+
if (!entry?.workflow)
|
|
184
|
+
return undefined;
|
|
185
|
+
this.applyReducerEvent({
|
|
186
|
+
type: "LOOP_WORKFLOW_TASK_SET",
|
|
187
|
+
at: Date.now(),
|
|
188
|
+
source: "tool",
|
|
189
|
+
entityType: "loop",
|
|
190
|
+
entityId: id,
|
|
191
|
+
payload: { id, taskId },
|
|
192
|
+
});
|
|
193
|
+
return this.entries.get(id);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
145
196
|
getDeletionTombstone(id) {
|
|
146
197
|
const tombstone = this.tombstones.get(id);
|
|
147
198
|
if (!tombstone)
|
package/dist/task-reducer.d.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
|
export interface TaskReducerState {
|
|
3
3
|
nextId: number;
|
|
4
4
|
tasksById: Record<string, TaskEntry>;
|
|
@@ -13,6 +13,7 @@ export type TaskReducerEvent = {
|
|
|
13
13
|
subject: string;
|
|
14
14
|
description: string;
|
|
15
15
|
metadata?: Record<string, unknown>;
|
|
16
|
+
workflow?: TaskWorkflowLink;
|
|
16
17
|
};
|
|
17
18
|
} | {
|
|
18
19
|
type: "TASK_STARTED" | "TASK_COMPLETED" | "TASK_REOPENED" | "TASK_DELETED";
|
package/dist/task-reducer.js
CHANGED
package/dist/task-store.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
2
2
|
import { type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
3
|
-
import type { TaskEntry, TaskStoreData } from "./task-types.js";
|
|
3
|
+
import type { TaskEntry, TaskStoreData, TaskWorkflowLink } from "./task-types.js";
|
|
4
4
|
export declare class TaskStore extends ReducerBackedStore<TaskEntry, TaskReducerState, TaskReducerEvent, TaskStoreData> {
|
|
5
5
|
constructor(listIdOrPath?: string);
|
|
6
|
-
create(subject: string, description: string, metadata?: Record<string, unknown
|
|
6
|
+
create(subject: string, description: string, metadata?: Record<string, unknown>, workflow?: TaskWorkflowLink): TaskEntry;
|
|
7
7
|
start(id: string): TaskEntry | undefined;
|
|
8
8
|
complete(id: string): TaskEntry | undefined;
|
|
9
9
|
reopen(id: string): TaskEntry | undefined;
|
package/dist/task-store.js
CHANGED
|
@@ -15,7 +15,7 @@ export class TaskStore extends ReducerBackedStore {
|
|
|
15
15
|
deserialize: (data) => ({ nextId: data.nextId, entries: new Map(data.tasks.map((t) => [t.id, t])) }),
|
|
16
16
|
}, listIdOrPath);
|
|
17
17
|
}
|
|
18
|
-
create(subject, description, metadata) {
|
|
18
|
+
create(subject, description, metadata, workflow) {
|
|
19
19
|
return this.withLock(() => {
|
|
20
20
|
if (this.entries.size >= MAX_TASKS) {
|
|
21
21
|
throw new Error(`Maximum of ${MAX_TASKS} tasks reached. Delete some before creating new ones.`);
|
|
@@ -26,7 +26,7 @@ export class TaskStore extends ReducerBackedStore {
|
|
|
26
26
|
at: now,
|
|
27
27
|
source: "tool",
|
|
28
28
|
entityType: "task",
|
|
29
|
-
payload: { subject, description, metadata },
|
|
29
|
+
payload: { subject, description, metadata, workflow },
|
|
30
30
|
});
|
|
31
31
|
return this.entries.get(String(this.nextId - 1));
|
|
32
32
|
});
|
package/dist/task-types.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type TaskStatus = "pending" | "in_progress" | "completed";
|
|
2
|
+
export interface TaskWorkflowLink {
|
|
3
|
+
loopId: string;
|
|
4
|
+
stateId: string;
|
|
5
|
+
transitionSeq: number;
|
|
6
|
+
}
|
|
2
7
|
export interface TaskEntry {
|
|
3
8
|
id: string;
|
|
4
9
|
subject: string;
|
|
@@ -8,6 +13,7 @@ export interface TaskEntry {
|
|
|
8
13
|
updatedAt: number;
|
|
9
14
|
completedAt?: number;
|
|
10
15
|
metadata?: Record<string, unknown>;
|
|
16
|
+
workflow?: TaskWorkflowLink;
|
|
11
17
|
}
|
|
12
18
|
export interface TaskStoreData {
|
|
13
19
|
nextId: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { LoopEntry, Trigger } from "../types.js";
|
|
2
|
+
import type { LoopEntry, Trigger, WorkflowDefinition } from "../types.js";
|
|
3
3
|
interface LoopStoreLike {
|
|
4
4
|
list(): LoopEntry[];
|
|
5
5
|
get(id: string): LoopEntry | undefined;
|
|
@@ -9,12 +9,25 @@ interface LoopStoreLike {
|
|
|
9
9
|
taskBacklog?: boolean;
|
|
10
10
|
readOnly?: boolean;
|
|
11
11
|
maxFires?: number;
|
|
12
|
+
dynamic?: Partial<NonNullable<LoopEntry["dynamic"]>>;
|
|
13
|
+
workflow?: WorkflowDefinition;
|
|
12
14
|
}): LoopEntry;
|
|
13
15
|
pause(id: string): LoopEntry | undefined;
|
|
14
16
|
updateDynamic(id: string, fields: {
|
|
15
17
|
prompt?: string;
|
|
16
18
|
dynamic: Partial<NonNullable<LoopEntry["dynamic"]>>;
|
|
17
19
|
}): LoopEntry | undefined;
|
|
20
|
+
transitionWorkflow(id: string, input: {
|
|
21
|
+
outcome: string;
|
|
22
|
+
evidence?: string;
|
|
23
|
+
activeTaskId?: string;
|
|
24
|
+
}): {
|
|
25
|
+
entry?: LoopEntry;
|
|
26
|
+
applied: boolean;
|
|
27
|
+
error?: string;
|
|
28
|
+
terminal?: "completed" | "paused";
|
|
29
|
+
};
|
|
30
|
+
setWorkflowActiveTask(id: string, taskId?: string): LoopEntry | undefined;
|
|
18
31
|
getDeletionTombstone(id: string): {
|
|
19
32
|
reason: string;
|
|
20
33
|
pendingCount?: number;
|
|
@@ -44,6 +57,9 @@ export interface LoopToolsOptions {
|
|
|
44
57
|
updateWidget: () => void;
|
|
45
58
|
maybeBootstrapTaskLoop: (entry: LoopEntry) => Promise<boolean>;
|
|
46
59
|
isTaskSystemReady: () => boolean;
|
|
60
|
+
onDynamicLoopActivated?: (entry: LoopEntry) => void;
|
|
61
|
+
createWorkflowTask?: (entry: LoopEntry) => Promise<string | undefined>;
|
|
62
|
+
completeWorkflowTask?: (taskId: string) => Promise<boolean>;
|
|
47
63
|
}
|
|
48
64
|
export declare function registerLoopTools(options: LoopToolsOptions): void;
|
|
49
65
|
export {};
|