@trevonistrevon/pi-loop 0.6.2 → 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 (58) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -2
  3. package/dist/api.d.ts +2 -1
  4. package/dist/index.js +14 -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 +4 -1
  8. package/dist/runtime/notification-runtime.d.ts +4 -1
  9. package/dist/runtime/notification-runtime.js +27 -1
  10. package/dist/runtime/session-runtime.js +14 -3
  11. package/dist/runtime/task-backlog-runtime.d.ts +1 -1
  12. package/dist/runtime/task-backlog-runtime.js +1 -1
  13. package/dist/runtime/task-events.d.ts +2 -1
  14. package/dist/runtime/task-events.js +1 -0
  15. package/dist/runtime/task-rpc.d.ts +4 -0
  16. package/dist/runtime/task-rpc.js +62 -1
  17. package/dist/store.d.ts +10 -1
  18. package/dist/store.js +51 -0
  19. package/dist/task-reducer.d.ts +2 -1
  20. package/dist/task-reducer.js +1 -0
  21. package/dist/task-store.d.ts +2 -2
  22. package/dist/task-store.js +2 -2
  23. package/dist/task-types.d.ts +6 -0
  24. package/dist/tools/loop-tools.d.ts +17 -1
  25. package/dist/tools/loop-tools.js +331 -26
  26. package/dist/tools/monitor-tools.js +43 -9
  27. package/dist/tools/native-task-tools.js +56 -11
  28. package/dist/tools/tool-result.d.ts +12 -2
  29. package/dist/tools/tool-result.js +7 -3
  30. package/dist/types.d.ts +35 -0
  31. package/dist/ui/tool-renderer.d.ts +7 -0
  32. package/dist/ui/tool-renderer.js +34 -0
  33. package/dist/ui/widget.js +4 -4
  34. package/dist/workflow-reducer.d.ts +18 -0
  35. package/dist/workflow-reducer.js +82 -0
  36. package/docs/USAGE_GUIDE.md +41 -0
  37. package/package.json +3 -3
  38. package/src/api.ts +9 -1
  39. package/src/index.ts +14 -0
  40. package/src/loop-reducer.ts +53 -1
  41. package/src/notification-reducer.ts +4 -1
  42. package/src/runtime/notification-runtime.ts +34 -2
  43. package/src/runtime/session-runtime.ts +15 -3
  44. package/src/runtime/task-backlog-runtime.ts +1 -1
  45. package/src/runtime/task-events.ts +3 -1
  46. package/src/runtime/task-rpc.ts +66 -0
  47. package/src/store.ts +51 -2
  48. package/src/task-reducer.ts +3 -1
  49. package/src/task-store.ts +3 -3
  50. package/src/task-types.ts +7 -0
  51. package/src/tools/loop-tools.ts +376 -20
  52. package/src/tools/monitor-tools.ts +44 -7
  53. package/src/tools/native-task-tools.ts +56 -8
  54. package/src/tools/tool-result.ts +18 -2
  55. package/src/types.ts +41 -0
  56. package/src/ui/tool-renderer.ts +45 -0
  57. package/src/ui/widget.ts +4 -4
  58. package/src/workflow-reducer.ts +107 -0
@@ -12,7 +12,7 @@ import {
12
12
  type ReducerNotification,
13
13
  reduceNotificationState,
14
14
  } from "../notification-reducer.js";
15
- import type { DynamicLoopState, Trigger } from "../types.js";
15
+ import type { DynamicLoopState, Trigger, WorkflowRunState } from "../types.js";
16
16
 
17
17
  export interface LoopFireEvent {
18
18
  loopId: string;
@@ -21,8 +21,11 @@ export interface LoopFireEvent {
21
21
  timestamp: number;
22
22
  readOnly?: boolean;
23
23
  recurring?: boolean;
24
+ persistent?: boolean;
24
25
  autoTask?: boolean;
26
+ taskBacklog?: boolean;
25
27
  dynamic?: DynamicLoopState;
28
+ workflow?: WorkflowRunState;
26
29
  }
27
30
 
28
31
  export interface PendingNotification extends LoopFireEvent {
@@ -104,6 +107,24 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
104
107
  ? "\n\nREAD-ONLY MODE — use only read tools (Read, TaskList, LoopList, MonitorList, etc.). No file writes, shell execution, or destructive changes."
105
108
  : "";
106
109
 
110
+ if (data.workflow) {
111
+ const state = data.workflow.definition.states[data.workflow.currentState];
112
+ const outcomes = Object.keys(state?.on ?? {});
113
+ const lines = [
114
+ `[pi-loop] Loop #${loopId} fired (workflow).${constraint}`,
115
+ `Goal: ${data.prompt || data.workflow.definition.initialState}`,
116
+ `State: ${data.workflow.currentState}`,
117
+ ];
118
+ if (state?.prompt) lines.push(`State instructions: ${state.prompt}`);
119
+ if (data.workflow.activeTaskId) lines.push(`Active task: #${data.workflow.activeTaskId}`);
120
+ if (outcomes.length > 0) lines.push(`Allowed outcomes: ${outcomes.join(", ")}`);
121
+ lines.push(
122
+ `Workflow lifecycle: Loop #${loopId} is an opt-in state controller. Do not call LoopDelete after this state.`,
123
+ "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.",
124
+ );
125
+ return lines.join("\n");
126
+ }
127
+
107
128
  if (data.dynamic || (typeof data.trigger !== "string" && data.trigger?.type === "dynamic")) {
108
129
  const dynamic = data.dynamic;
109
130
  const lines = [
@@ -115,14 +136,22 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
115
136
  if (dynamic?.metrics) lines.push(`Metrics: ${dynamic.metrics}`);
116
137
  if (dynamic?.doneCriteria) lines.push(`Done criteria: ${dynamic.doneCriteria}`);
117
138
  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\".",
139
+ `Loop lifecycle: Loop #${loopId} is the persistent controller for the overall goal. Do not call LoopDelete after this iteration.`,
140
+ "Before ending this turn, call LoopUpdate exactly once: use status=\"completed\" only when the overall goal and done criteria are satisfied; use status=\"continue\" when any work remains, with state/metrics and optional nextInterval; use status=\"paused\" only when genuinely blocked. Omit nextInterval for an idle-driven rewake.",
119
141
  );
120
142
  return lines.join("\n");
121
143
  }
122
144
 
145
+ const lifecycle = data.taskBacklog
146
+ ? `Backlog lifecycle: Loop #${loopId} is managed automatically. Do not call LoopDelete; when no pending tasks remain, report that and end this iteration.`
147
+ : (data.persistent ?? data.recurring)
148
+ ? `Loop lifecycle: Loop #${loopId} is recurring and remains active after this iteration. Do not call LoopDelete or pause it merely because this run finished, found no changes, or has no immediate work. Stop it only when the user or the loop prompt explicitly requires cancellation.`
149
+ : `Loop lifecycle: Loop #${loopId} is a one-shot wake and cleanup is automatic. Do not call LoopDelete.`;
150
+
123
151
  return [
124
152
  `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
125
153
  prompt,
154
+ lifecycle,
126
155
  ].join("\n");
127
156
  }
128
157
 
@@ -154,9 +183,12 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
154
183
  loopId: notification.loopId,
155
184
  trigger: notification.trigger,
156
185
  recurring: notification.recurring,
186
+ persistent: notification.persistent,
157
187
  readOnly: notification.readOnly,
158
188
  autoTask: notification.autoTask,
189
+ taskBacklog: notification.taskBacklog,
159
190
  dynamic: notification.dynamic,
191
+ workflow: notification.workflow,
160
192
  timestamp: notification.timestamp,
161
193
  },
162
194
  }, {
@@ -62,8 +62,11 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
62
62
  if (heartbeatTimer) return;
63
63
  heartbeatTimer = setInterval(() => {
64
64
  // Swallow pump failures so a transient error never surfaces as an
65
- // unhandled rejection; the next tick retries.
66
- void pumpLoops().catch(() => {});
65
+ // unhandled rejection; repaint still runs so cleared harness UI heals.
66
+ void pumpLoops()
67
+ .catch(() => {})
68
+ .then(() => widget.update())
69
+ .catch(() => {});
67
70
  }, HEARTBEAT_MS);
68
71
  heartbeatTimer.unref?.();
69
72
  }
@@ -92,7 +95,6 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
92
95
  getStore().expireEventLoops(sessionStartedAt);
93
96
  getTriggerSystem().start();
94
97
  ensureHeartbeat();
95
- widget.update();
96
98
  }
97
99
  }
98
100
 
@@ -110,6 +112,16 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
110
112
  getScheduler().pump(Date.now(), (entry) => !pendingTasks.has(entry.id));
111
113
  }
112
114
 
115
+ pi.on("session_start", async (_event, ctx) => {
116
+ setLatestCtx(ctx);
117
+ setSessionId(ctx.sessionManager.getSessionId());
118
+ widget.setUICtx(ctx.ui);
119
+ upgradeStoreIfNeeded(ctx);
120
+ ensureHeartbeat();
121
+ showPersistedLoops();
122
+ widget.update();
123
+ });
124
+
113
125
  pi.on("turn_start", async (_event, ctx) => {
114
126
  setLatestCtx(ctx);
115
127
  setSessionId(ctx.sessionManager.getSessionId());
@@ -17,7 +17,7 @@ import {
17
17
  } from "./loop-events.js";
18
18
 
19
19
  export const AUTO_TASK_WORKER_THRESHOLD = 5;
20
- export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
20
+ export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, and complete it. If no pending tasks remain, report that and end this iteration; pi-loop manages the worker lifecycle automatically.";
21
21
 
22
22
  export interface TaskBacklogRuntimeOptions {
23
23
  getLoops: () => LoopEntry[];
@@ -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
 
4
4
  export type NativeTaskEventName =
5
5
  | "tasks:created"
@@ -19,6 +19,7 @@ export interface NativeTaskEventPayload {
19
19
  updatedAt: number;
20
20
  completedAt?: number;
21
21
  metadata?: Record<string, unknown>;
22
+ workflow?: TaskWorkflowLink;
22
23
  }
23
24
 
24
25
  export function emitNativeTaskEvent(
@@ -37,5 +38,6 @@ export function emitNativeTaskEvent(
37
38
  updatedAt: entry.updatedAt,
38
39
  completedAt: entry.completedAt,
39
40
  metadata: entry.metadata,
41
+ workflow: entry.workflow,
40
42
  } satisfies NativeTaskEventPayload);
41
43
  }
@@ -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 {