@auxiora/workflows 1.0.0

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/src/engine.ts ADDED
@@ -0,0 +1,296 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ import { getLogger } from '@auxiora/logger';
5
+ import { audit } from '@auxiora/audit';
6
+ import { getAuxioraDir } from '@auxiora/core';
7
+ import type {
8
+ HumanWorkflow,
9
+ WorkflowStep,
10
+ WorkflowStatus,
11
+ WorkflowEvent,
12
+ ReminderConfig,
13
+ EscalationPolicy,
14
+ AutonomousAction,
15
+ } from './types.js';
16
+
17
+ const logger = getLogger('workflows:engine');
18
+
19
+ export interface CreateWorkflowOptions {
20
+ name: string;
21
+ description: string;
22
+ createdBy: string;
23
+ steps: Array<{
24
+ name: string;
25
+ description: string;
26
+ assigneeId: string;
27
+ dependsOn?: string[];
28
+ action?: AutonomousAction;
29
+ }>;
30
+ reminder?: Partial<ReminderConfig>;
31
+ escalation?: Partial<EscalationPolicy>;
32
+ autonomous?: boolean;
33
+ }
34
+
35
+ const DEFAULT_REMINDER: ReminderConfig = {
36
+ enabled: false,
37
+ intervalMs: 3600_000, // 1 hour
38
+ maxReminders: 3,
39
+ };
40
+
41
+ const DEFAULT_ESCALATION: EscalationPolicy = {
42
+ enabled: false,
43
+ escalateAfterMs: 86400_000, // 24 hours
44
+ escalateToUserId: '',
45
+ maxEscalations: 1,
46
+ };
47
+
48
+ export class WorkflowEngine {
49
+ private filePath: string;
50
+
51
+ constructor(options?: { dir?: string }) {
52
+ const dir = options?.dir ?? path.join(getAuxioraDir(), 'workflows');
53
+ this.filePath = path.join(dir, 'workflows.json');
54
+ }
55
+
56
+ async createWorkflow(options: CreateWorkflowOptions): Promise<HumanWorkflow> {
57
+ const workflows = await this.readFile();
58
+ const now = Date.now();
59
+ const workflowId = `wf-${crypto.randomUUID().slice(0, 8)}`;
60
+
61
+ const steps: WorkflowStep[] = options.steps.map((s, i) => ({
62
+ id: `step-${i + 1}`,
63
+ name: s.name,
64
+ description: s.description,
65
+ assigneeId: s.assigneeId,
66
+ status: 'pending',
67
+ dependsOn: s.dependsOn ?? [],
68
+ ...(s.action ? { action: s.action } : {}),
69
+ }));
70
+
71
+ const workflow: HumanWorkflow = {
72
+ id: workflowId,
73
+ name: options.name,
74
+ description: options.description,
75
+ createdBy: options.createdBy,
76
+ status: 'pending',
77
+ steps,
78
+ reminder: { ...DEFAULT_REMINDER, ...options.reminder },
79
+ escalation: { ...DEFAULT_ESCALATION, ...options.escalation },
80
+ events: [],
81
+ createdAt: now,
82
+ updatedAt: now,
83
+ ...(options.autonomous ? { autonomous: true } : {}),
84
+ };
85
+
86
+ workflow.events.push(this.createEvent(workflowId, 'created', { userId: options.createdBy }));
87
+
88
+ workflows.push(workflow);
89
+ await this.writeFile(workflows);
90
+ void audit('workflow.created', { id: workflowId, name: options.name });
91
+ logger.debug('Created workflow', { id: workflowId });
92
+ return workflow;
93
+ }
94
+
95
+ async startWorkflow(workflowId: string): Promise<HumanWorkflow | undefined> {
96
+ const workflows = await this.readFile();
97
+ const workflow = workflows.find(w => w.id === workflowId);
98
+ if (!workflow || workflow.status !== 'pending') return undefined;
99
+
100
+ workflow.status = 'active';
101
+ workflow.updatedAt = Date.now();
102
+
103
+ // Activate steps with no dependencies
104
+ for (const step of workflow.steps) {
105
+ if (step.dependsOn.length === 0) {
106
+ step.status = 'active';
107
+ }
108
+ }
109
+
110
+ await this.writeFile(workflows);
111
+ void audit('workflow.started', { id: workflowId });
112
+ return workflow;
113
+ }
114
+
115
+ async completeStep(
116
+ workflowId: string,
117
+ stepId: string,
118
+ completedBy: string,
119
+ result?: string,
120
+ ): Promise<HumanWorkflow | undefined> {
121
+ const workflows = await this.readFile();
122
+ const workflow = workflows.find(w => w.id === workflowId);
123
+ if (!workflow || workflow.status !== 'active') return undefined;
124
+
125
+ const step = workflow.steps.find(s => s.id === stepId);
126
+ if (!step || step.status !== 'active') return undefined;
127
+
128
+ step.status = 'completed';
129
+ step.completedAt = Date.now();
130
+ step.completedBy = completedBy;
131
+ step.result = result;
132
+
133
+ workflow.events.push(
134
+ this.createEvent(workflowId, 'step_completed', {
135
+ stepId,
136
+ userId: completedBy,
137
+ details: result,
138
+ }),
139
+ );
140
+
141
+ // Advance: activate any steps whose dependencies are now met
142
+ this.advanceWorkflow(workflow);
143
+
144
+ workflow.updatedAt = Date.now();
145
+ await this.writeFile(workflows);
146
+ void audit('workflow.step_completed', { workflowId, stepId, completedBy });
147
+ return workflow;
148
+ }
149
+
150
+ async failStep(
151
+ workflowId: string,
152
+ stepId: string,
153
+ reason: string,
154
+ ): Promise<HumanWorkflow | undefined> {
155
+ const workflows = await this.readFile();
156
+ const workflow = workflows.find(w => w.id === workflowId);
157
+ if (!workflow || workflow.status !== 'active') return undefined;
158
+
159
+ const step = workflow.steps.find(s => s.id === stepId);
160
+ if (!step || step.status !== 'active') return undefined;
161
+
162
+ step.status = 'failed';
163
+ workflow.events.push(
164
+ this.createEvent(workflowId, 'step_failed', { stepId, details: reason }),
165
+ );
166
+
167
+ // Check if workflow should fail
168
+ const hasActiveSteps = workflow.steps.some(s => s.status === 'active' || s.status === 'pending');
169
+ if (!hasActiveSteps) {
170
+ workflow.status = 'failed';
171
+ }
172
+
173
+ workflow.updatedAt = Date.now();
174
+ await this.writeFile(workflows);
175
+ return workflow;
176
+ }
177
+
178
+ async cancelWorkflow(workflowId: string): Promise<boolean> {
179
+ const workflows = await this.readFile();
180
+ const workflow = workflows.find(w => w.id === workflowId);
181
+ if (!workflow || workflow.status === 'completed' || workflow.status === 'cancelled') return false;
182
+
183
+ workflow.status = 'cancelled';
184
+ workflow.updatedAt = Date.now();
185
+ workflow.events.push(this.createEvent(workflowId, 'cancelled'));
186
+
187
+ await this.writeFile(workflows);
188
+ void audit('workflow.cancelled', { id: workflowId });
189
+ return true;
190
+ }
191
+
192
+ async getWorkflow(workflowId: string): Promise<HumanWorkflow | undefined> {
193
+ const workflows = await this.readFile();
194
+ return workflows.find(w => w.id === workflowId);
195
+ }
196
+
197
+ async getStatus(workflowId: string): Promise<{ workflow: HumanWorkflow; progress: number } | undefined> {
198
+ const workflow = await this.getWorkflow(workflowId);
199
+ if (!workflow) return undefined;
200
+
201
+ const completed = workflow.steps.filter(s => s.status === 'completed').length;
202
+ const progress = workflow.steps.length > 0 ? completed / workflow.steps.length : 0;
203
+
204
+ return { workflow, progress };
205
+ }
206
+
207
+ async listActive(): Promise<HumanWorkflow[]> {
208
+ const workflows = await this.readFile();
209
+ return workflows.filter(w => w.status === 'active' || w.status === 'pending');
210
+ }
211
+
212
+ async listAll(): Promise<HumanWorkflow[]> {
213
+ return this.readFile();
214
+ }
215
+
216
+ async addEvent(
217
+ workflowId: string,
218
+ type: WorkflowEvent['type'],
219
+ extra?: { stepId?: string; userId?: string; details?: string },
220
+ ): Promise<boolean> {
221
+ const workflows = await this.readFile();
222
+ const workflow = workflows.find(w => w.id === workflowId);
223
+ if (!workflow) return false;
224
+
225
+ workflow.events.push(this.createEvent(workflowId, type, extra));
226
+ workflow.updatedAt = Date.now();
227
+ await this.writeFile(workflows);
228
+ return true;
229
+ }
230
+
231
+ async listByUser(userId: string): Promise<HumanWorkflow[]> {
232
+ const workflows = await this.readFile();
233
+ return workflows.filter(
234
+ w => w.createdBy === userId || w.steps.some(s => s.assigneeId === userId),
235
+ );
236
+ }
237
+
238
+ private advanceWorkflow(workflow: HumanWorkflow): void {
239
+ const completedIds = new Set(
240
+ workflow.steps.filter(s => s.status === 'completed').map(s => s.id),
241
+ );
242
+
243
+ for (const step of workflow.steps) {
244
+ if (step.status !== 'pending') continue;
245
+
246
+ const depsComplete = step.dependsOn.every(dep => completedIds.has(dep));
247
+ if (depsComplete) {
248
+ step.status = 'active';
249
+ }
250
+ }
251
+
252
+ // Check if all steps are completed
253
+ const allDone = workflow.steps.every(
254
+ s => s.status === 'completed' || s.status === 'skipped',
255
+ );
256
+ if (allDone) {
257
+ workflow.status = 'completed';
258
+ workflow.completedAt = Date.now();
259
+ workflow.events.push(this.createEvent(workflow.id, 'completed'));
260
+ }
261
+ }
262
+
263
+ private createEvent(
264
+ workflowId: string,
265
+ type: WorkflowEvent['type'],
266
+ extra?: { stepId?: string; userId?: string; details?: string },
267
+ ): WorkflowEvent {
268
+ return {
269
+ id: `evt-${crypto.randomUUID().slice(0, 8)}`,
270
+ workflowId,
271
+ type,
272
+ stepId: extra?.stepId,
273
+ userId: extra?.userId,
274
+ details: extra?.details,
275
+ timestamp: Date.now(),
276
+ };
277
+ }
278
+
279
+ private async readFile(): Promise<HumanWorkflow[]> {
280
+ try {
281
+ const content = await fs.readFile(this.filePath, 'utf-8');
282
+ return JSON.parse(content) as HumanWorkflow[];
283
+ } catch (error) {
284
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
285
+ return [];
286
+ }
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ private async writeFile(workflows: HumanWorkflow[]): Promise<void> {
292
+ const dir = path.dirname(this.filePath);
293
+ await fs.mkdir(dir, { recursive: true });
294
+ await fs.writeFile(this.filePath, JSON.stringify(workflows, null, 2), 'utf-8');
295
+ }
296
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type {
2
+ HumanWorkflow,
3
+ WorkflowStep,
4
+ WorkflowStatus,
5
+ StepStatus,
6
+ ReminderConfig,
7
+ EscalationPolicy,
8
+ WorkflowEvent,
9
+ AutonomousAction,
10
+ } from './types.js';
11
+ export { WorkflowEngine } from './engine.js';
12
+ export type { CreateWorkflowOptions } from './engine.js';
13
+ export { ReminderService } from './reminder.js';
14
+ export type { ReminderTarget, ReminderSender } from './reminder.js';
15
+ export { ApprovalManager } from './approval.js';
16
+ export type { ApprovalRequest, ApprovalStatus } from './approval.js';
17
+ export {
18
+ AutonomousExecutor,
19
+ type AutonomousExecutorDeps,
20
+ type TickResult,
21
+ type ToolResult as AutonomousToolResult,
22
+ type GateCheckResult,
23
+ } from './autonomous-executor.js';
@@ -0,0 +1,129 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { HumanWorkflow, ReminderConfig } from './types.js';
3
+
4
+ const logger = getLogger('workflows:reminder');
5
+
6
+ export interface ReminderTarget {
7
+ userId: string;
8
+ workflowId: string;
9
+ stepId: string;
10
+ stepName: string;
11
+ workflowName: string;
12
+ }
13
+
14
+ export interface ReminderSender {
15
+ sendReminder(target: ReminderTarget, channelType?: string): Promise<void>;
16
+ }
17
+
18
+ interface ScheduledReminder {
19
+ workflowId: string;
20
+ stepId: string;
21
+ timer: ReturnType<typeof setInterval>;
22
+ count: number;
23
+ maxReminders: number;
24
+ }
25
+
26
+ export class ReminderService {
27
+ private reminders = new Map<string, ScheduledReminder>();
28
+ private sender?: ReminderSender;
29
+
30
+ setSender(sender: ReminderSender): void {
31
+ this.sender = sender;
32
+ }
33
+
34
+ scheduleReminder(
35
+ workflow: HumanWorkflow,
36
+ stepId: string,
37
+ config?: Partial<ReminderConfig>,
38
+ ): void {
39
+ const key = `${workflow.id}:${stepId}`;
40
+ if (this.reminders.has(key)) return;
41
+
42
+ const step = workflow.steps.find(s => s.id === stepId);
43
+ if (!step) return;
44
+
45
+ const intervalMs = config?.intervalMs ?? workflow.reminder.intervalMs;
46
+ const maxReminders = config?.maxReminders ?? workflow.reminder.maxReminders;
47
+
48
+ const scheduled: ScheduledReminder = {
49
+ workflowId: workflow.id,
50
+ stepId,
51
+ count: 0,
52
+ maxReminders,
53
+ timer: setInterval(() => {
54
+ void this.sendReminder(workflow, stepId, scheduled);
55
+ }, intervalMs),
56
+ };
57
+
58
+ this.reminders.set(key, scheduled);
59
+ logger.debug('Scheduled reminder', { workflowId: workflow.id, stepId, intervalMs });
60
+ }
61
+
62
+ cancelReminder(workflowId: string, stepId: string): void {
63
+ const key = `${workflowId}:${stepId}`;
64
+ const reminder = this.reminders.get(key);
65
+ if (reminder) {
66
+ clearInterval(reminder.timer);
67
+ this.reminders.delete(key);
68
+ logger.debug('Cancelled reminder', { workflowId, stepId });
69
+ }
70
+ }
71
+
72
+ cancelAllForWorkflow(workflowId: string): void {
73
+ for (const [key, reminder] of this.reminders) {
74
+ if (reminder.workflowId === workflowId) {
75
+ clearInterval(reminder.timer);
76
+ this.reminders.delete(key);
77
+ }
78
+ }
79
+ }
80
+
81
+ shutdown(): void {
82
+ for (const [, reminder] of this.reminders) {
83
+ clearInterval(reminder.timer);
84
+ }
85
+ this.reminders.clear();
86
+ }
87
+
88
+ getActiveCount(): number {
89
+ return this.reminders.size;
90
+ }
91
+
92
+ private async sendReminder(
93
+ workflow: HumanWorkflow,
94
+ stepId: string,
95
+ scheduled: ScheduledReminder,
96
+ ): Promise<void> {
97
+ scheduled.count++;
98
+
99
+ if (scheduled.count > scheduled.maxReminders) {
100
+ this.cancelReminder(workflow.id, stepId);
101
+ return;
102
+ }
103
+
104
+ const step = workflow.steps.find(s => s.id === stepId);
105
+ if (!step || step.status !== 'active') {
106
+ this.cancelReminder(workflow.id, stepId);
107
+ return;
108
+ }
109
+
110
+ if (this.sender) {
111
+ try {
112
+ await this.sender.sendReminder({
113
+ userId: step.assigneeId,
114
+ workflowId: workflow.id,
115
+ stepId,
116
+ stepName: step.name,
117
+ workflowName: workflow.name,
118
+ });
119
+ logger.debug('Sent reminder', {
120
+ workflowId: workflow.id,
121
+ stepId,
122
+ count: scheduled.count,
123
+ });
124
+ } catch (error) {
125
+ logger.debug('Failed to send reminder', { error: error as Error });
126
+ }
127
+ }
128
+ }
129
+ }
package/src/types.ts ADDED
@@ -0,0 +1,73 @@
1
+ export type WorkflowStatus = 'pending' | 'active' | 'completed' | 'failed' | 'cancelled';
2
+ export type StepStatus = 'pending' | 'active' | 'completed' | 'failed' | 'skipped';
3
+
4
+ /** An action that can be auto-executed by the AutonomousExecutor. */
5
+ export interface AutonomousAction {
6
+ /** Tool name to execute (e.g. 'file_read', 'bash', 'email_compose'). */
7
+ tool: string;
8
+ /** Parameters to pass to the tool. */
9
+ params: Record<string, unknown>;
10
+ /** Trust domain for gate checking (e.g. 'files', 'shell', 'email'). */
11
+ trustDomain: string;
12
+ /** Minimum trust level required (0-4). */
13
+ trustRequired: number;
14
+ /** Optional tool to call for rollback on failure. */
15
+ rollbackTool?: string;
16
+ /** Optional params for the rollback tool. */
17
+ rollbackParams?: Record<string, unknown>;
18
+ }
19
+
20
+ export interface WorkflowStep {
21
+ id: string;
22
+ name: string;
23
+ description: string;
24
+ assigneeId: string;
25
+ status: StepStatus;
26
+ dependsOn: string[];
27
+ completedAt?: number;
28
+ completedBy?: string;
29
+ result?: string;
30
+ /** If present, step can be auto-executed by AutonomousExecutor. */
31
+ action?: AutonomousAction;
32
+ }
33
+
34
+ export interface ReminderConfig {
35
+ enabled: boolean;
36
+ intervalMs: number;
37
+ maxReminders: number;
38
+ channelType?: string;
39
+ }
40
+
41
+ export interface EscalationPolicy {
42
+ enabled: boolean;
43
+ escalateAfterMs: number;
44
+ escalateToUserId: string;
45
+ maxEscalations: number;
46
+ }
47
+
48
+ export interface WorkflowEvent {
49
+ id: string;
50
+ workflowId: string;
51
+ type: 'created' | 'step_completed' | 'step_failed' | 'step_trust_denied' | 'step_rolled_back' | 'reminder_sent' | 'escalated' | 'completed' | 'cancelled' | 'approval_requested' | 'approved' | 'rejected';
52
+ stepId?: string;
53
+ userId?: string;
54
+ details?: string;
55
+ timestamp: number;
56
+ }
57
+
58
+ export interface HumanWorkflow {
59
+ id: string;
60
+ name: string;
61
+ description: string;
62
+ createdBy: string;
63
+ status: WorkflowStatus;
64
+ steps: WorkflowStep[];
65
+ reminder: ReminderConfig;
66
+ escalation: EscalationPolicy;
67
+ events: WorkflowEvent[];
68
+ createdAt: number;
69
+ updatedAt: number;
70
+ completedAt?: number;
71
+ /** If true, steps with actions are auto-executed by AutonomousExecutor. */
72
+ autonomous?: boolean;
73
+ }