@hotmeshio/hotmesh 0.5.0 β†’ 0.5.1

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/README.md CHANGED
@@ -1,44 +1,52 @@
1
1
  # HotMesh
2
2
 
3
- **Permanent-Memory Workflows & AI Agents**
3
+ **🧠 Workflow That Remembers**
4
4
 
5
5
  ![beta release](https://img.shields.io/badge/release-beta-blue.svg) ![made with typescript](https://img.shields.io/badge/built%20with-typescript-lightblue.svg)
6
6
 
7
- **HotMesh** is a Temporal-style workflow engine that runs natively on PostgreSQL β€” with a powerful twist: every workflow maintains permanent, state that persists independently of the workflow itself.
7
+ HotMesh brings a **memory model** to your automation: durable entities that hold context, support concurrency, and evolve across runs. Built on PostgreSQL, it treats your database not just as storageβ€”but as the runtime hub for agents, pipelines, and long-lived processes.
8
8
 
9
- This means:
9
+ Use HotMesh to:
10
10
 
11
- * Any number of lightweight, thread-safe **hook workers** can attach to the same workflow record at any time.
12
- * These hooks can safely **read and write** to shared state.
13
- * The result is a **durable execution model** with **evolving memory**, ideal for **human-in-the-loop processes** and **AI agents that learn over time**.
11
+ * **Store Evolving State** – Retain memory/state between executions
12
+ * **Coordinate Distributed Work** – Safely allow multiple workers to act on shared state
13
+ * **Track and Replay** – Full audit history and replay support by default
14
14
 
15
15
  ---
16
16
 
17
17
  ## Table of Contents
18
18
 
19
- 1. πŸš€ Quick Start
20
- 2. 🧠 How Permanent Memory Works
21
- 3. πŸ”Œ Hooks & Entity API
22
- 4. πŸ€– Building Durable AI Agents
23
- 5. πŸ”¬ Advanced Patterns & Recipes
24
- 6. πŸ“š Documentation & Links
19
+ 1. [Quick Start](#-quick-start)
20
+ 2. [Permanent Memory Architecture](#-permanent-memory-architecture)
21
+ 3. [Durable AI Agents](#-durable-ai-agents)
22
+ 4. [Building Pipelines with State](#-building-pipelines-with-state)
23
+ 5. [Documentation & Links](#-documentation--links)
25
24
 
26
25
  ---
27
26
 
28
- ## πŸš€ Quick Start
27
+ ## Quick Start
28
+
29
+ ### Prerequisites
30
+
31
+ * PostgreSQL (or Supabase)
32
+ * Node.js 16+
29
33
 
30
34
  ### Install
35
+
31
36
  ```bash
32
37
  npm install @hotmeshio/hotmesh
33
38
  ```
34
39
 
35
- ### Start a workflow
36
- ```typescript
37
- // index.ts
40
+ ### Connect to a Database
41
+
42
+ HotMesh leverages Temporal.io's developer-friendly syntax for authoring workers, workflows, and clients. The `init` and `start` methods should look familiar.
43
+
44
+ ```ts
38
45
  import { MemFlow } from '@hotmeshio/hotmesh';
39
46
  import { Client as Postgres } from 'pg';
40
47
 
41
48
  async function main() {
49
+ // MemFlow will auto-provision the database upon init
42
50
  const mf = await MemFlow.init({
43
51
  appId: 'my-app',
44
52
  engine: {
@@ -49,13 +57,13 @@ async function main() {
49
57
  }
50
58
  });
51
59
 
52
- // Kick off a workflow
60
+ // Start a workflow with an assigned ID and arguments
53
61
  const handle = await mf.workflow.start({
54
- entity: 'user',
55
- workflowName: 'userExample',
56
- workflowId: 'jane@hotmesh.com',
57
- args: ['Jane'],
58
- taskQueue: 'entityqueue'
62
+ entity: 'research-agent',
63
+ workflowName: 'researchAgent',
64
+ workflowId: 'agent-session-jane-001',
65
+ args: ['What are the long-term impacts of renewable energy subsidies?'],
66
+ taskQueue: 'agents'
59
67
  });
60
68
 
61
69
  console.log('Result:', await handle.result());
@@ -64,168 +72,53 @@ async function main() {
64
72
  main().catch(console.error);
65
73
  ```
66
74
 
67
- ---
68
-
69
- ## 🧠 How Permanent Memory Works
70
-
71
- * **Entity = persistent JSON record** – each workflow's memory is stored as a JSONB row in your Postgres database
72
- * **Atomic operations** (`set`, `merge`, `append`, `increment`, `toggle`, `delete`, …)
73
- * **Transactional** – every update participates in the workflow/DB transaction
74
- * **Time-travel-safe** – full replay compatibility; side-effect detector guarantees determinism
75
- * **Hook-friendly** – any worker with the record ID can attach and mutate its slice of the JSON
76
- * **Index-friendly** - entity data is stored as JSONB; add partial indexes for improved query analysis.
75
+ ### System Benefits
77
76
 
78
- **Example: Adding a Partial Index for Specific Entity Types**
79
- ```sql
80
- -- Create a partial index for 'user' entities with specific entity values
81
- CREATE INDEX idx_user_premium ON your_app.jobs (id)
82
- WHERE entity = 'user' AND (context->>'isPremium')::boolean = true;
83
- ```
84
- This index will only be used for queries that match both conditions, making lookups for premium users much faster.
77
+ * **No Setup Required** – Tables and indexes are provisioned automatically
78
+ * **Shared State** – Every worker shares access to the same entity memory
79
+ * **Coordination by Design** – PostgreSQL handles consistency and isolation
80
+ * **Tenant Isolation** – Each app maintains its own schema
81
+ * **Scalable Defaults** – Partitioned tables and index support included
85
82
 
86
83
  ---
87
84
 
88
- ## πŸ”Œ Hooks & Entity API – Full Example
89
-
90
- HotMesh hooks are powerful because they can be called both internally (from within a workflow) and externally (from outside, even after the workflow completes). This means you can:
91
-
92
- * Start a workflow that sets up initial state
93
- * Have the workflow call some hooks internally
94
- * Let the workflow complete
95
- * Continue to update the workflow's entity state from the outside via hooks
96
- * Build long-running processes that evolve over time
97
-
98
- Here's a complete example showing both internal and external hook usage:
99
-
100
- ```typescript
101
- import { MemFlow } from '@hotmeshio/hotmesh';
102
-
103
- /* ------------ Main workflow ------------ */
104
- export async function userExample(name: string): Promise<any> {
105
- //the entity method provides transactional, replayable access to shared job state
106
- const entity = await MemFlow.workflow.entity();
107
-
108
- //create the initial entity (even arrays are supported)
109
- await entity.set({
110
- user: { name },
111
- hooks: {},
112
- metrics: { count: 0 }
113
- });
114
-
115
- // Call one hook internally
116
- const result1 = await MemFlow.workflow.execHook({
117
- taskQueue: 'entityqueue',
118
- workflowName: 'hook1',
119
- args: [name, 'hook1'],
120
- signalId: 'hook1-complete'
121
- });
122
-
123
- // merge the result
124
- await entity.merge({ hooks: { r1: result1 } });
125
- await entity.increment('metrics.count', 1);
126
-
127
- return "The main has completed; the db record persists and can be hydrated; hook in from the outside!";
128
- }
129
-
130
- /* ------------ Hook 1 (hooks have access to methods like sleepFor) ------------ */
131
- export async function hook1(name: string, kind: string): Promise<any> {
132
- await MemFlow.workflow.sleepFor('2 seconds');
133
- const res = { kind, processed: true, at: Date.now() };
134
- await MemFlow.workflow.signal('hook1-complete', res);
135
- }
136
-
137
- /* ------------ Hook 2 (hooks can access shared job entity) ------------ */
138
- export async function hook2(name: string, kind: string): Promise<void> {
139
- const entity = await MemFlow.workflow.entity();
140
- await entity.merge({ user: { lastSeen: new Date().toISOString() } });
141
- await MemFlow.workflow.signal('hook2-complete', { ok: true });
142
- }
143
-
144
- /* ------------ Worker/Hook Registration ------------ */
145
- async function startWorker() {
146
- const mf = await MemFlow.init({
147
- appId: 'my-app',
148
- engine: {
149
- connection: {
150
- class: Postgres,
151
- options: { connectionString: process.env.DATABASE_URL }
152
- }
153
- }
154
- });
85
+ ## Permanent Memory Architecture
155
86
 
156
- const worker = await mf.worker.create({
157
- taskQueue: 'entityqueue',
158
- workflow: example
159
- });
87
+ Every workflow in HotMesh is backed by an "entity": a versioned, JSONB record that tracks its memory and state transitions.
160
88
 
161
- await mf.worker.create({
162
- taskQueue: 'entityqueue',
163
- workflow: hook1
164
- });
89
+ * **Entities** – Represent long-lived state for a workflow or agent
90
+ * **Commands** – Modify state with methods like `set`, `merge`, `append`, `increment`
91
+ * **Consistency** – All updates are transactional with Postgres
92
+ * **Replay Safety** – Protects against duplicated side effects during re-execution
93
+ * **Partial Indexing** – Optimized querying of fields within large JSON structures
165
94
 
166
- await mf.worker.create({
167
- taskQueue: 'entityqueue',
168
- workflow: hook2
169
- });
95
+ ### Example: Partial Index for Premium Users
170
96
 
171
- console.log('Workers and hooks started and listening...');
172
- }
97
+ ```sql
98
+ -- Index only those user entities that are marked as premium
99
+ CREATE INDEX idx_user_premium ON your_app.jobs (id)
100
+ WHERE entity = 'user' AND (context->>'isPremium')::boolean = true;
173
101
  ```
174
102
 
175
- ### The Power of External Hooks
176
-
177
- One of HotMesh's most powerful features is that workflow entities remain accessible even after the main workflow completes. By providing the original workflow ID, any authorized client can:
178
-
179
- * Hook into existing workflow entities
180
- * Update state and trigger new processing
181
- * Build evolving, long-running processes
182
- * Enable human-in-the-loop workflows
183
- * Create AI agents that learn over time
184
-
185
- Here's how to hook into an existing workflow from the outside:
186
-
187
- ```typescript
188
- /* ------------ External Hook Example ------------ */
189
- async function externalHookExample() {
190
- const client = new MemFlow.Client({
191
- appId: 'my-app',
192
- engine: {
193
- connection: {
194
- class: Postgres,
195
- options: { connectionString: process.env.DATABASE_URL }
196
- }
197
- }
198
- });
199
-
200
- // Start hook2 externally by providing the original workflow ID
201
- await client.workflow.hook({
202
- workflowId: 'jane@hotmesh.com', //id of the target workflow
203
- taskQueue: 'entityqueue',
204
- workflowName: 'hook2',
205
- args: [name, 'external-hook']
206
- });
207
- }
208
- ```
103
+ This index improves performance for filtered queries while reducing index size.
209
104
 
210
105
  ---
211
106
 
212
- ## πŸ€– Building Durable AI Agents
107
+ ## Durable AI Agents
108
+
109
+ Agents often require memoryβ€”context that persists between invocations, spans multiple perspectives, or outlives a single process. HotMesh supports that natively.
213
110
 
214
- HotMesh's permanent memory enables a revolutionary approach to AI agents: **perspective-oriented intelligence**. Instead of monolithic agents, you build **multi-faceted agents** where:
111
+ The following example builds a "research agent" that runs sub-flows and self-reflects on the results.
215
112
 
216
- * **Entity state** = the agent's living, evolving context
217
- * **Hooks** = different perspectives that operate on that context
218
- * **Child workflows** = specialized co-agents with their own entity types
219
- * **Shared memory** = enables rich self-reflection and multi-agent collaboration
113
+ ### Research Agent Example
220
114
 
221
- ### Example: A Research Agent with Multiple Perspectives
115
+ #### Main Coordinator Agent
222
116
 
223
- ```typescript
224
- /* ------------ Main Research Agent ------------ */
117
+ ```ts
225
118
  export async function researchAgent(query: string): Promise<any> {
226
119
  const entity = await MemFlow.workflow.entity();
227
-
228
- // Initialize agent context
120
+
121
+ // Set up shared memory for this agent session
229
122
  await entity.set({
230
123
  query,
231
124
  findings: [],
@@ -234,20 +127,20 @@ export async function researchAgent(query: string): Promise<any> {
234
127
  status: 'researching'
235
128
  });
236
129
 
237
- // Spawn perspective hooks that operate on shared context
238
- const optimisticView = MemFlow.workflow.execHook({
239
- taskQueue: 'perspectives',
130
+ // Launch perspective hooks in parallel (no need to await here)
131
+ const optimistic = MemFlow.workflow.execHook({
132
+ taskQueue: 'agents',
240
133
  workflowName: 'optimisticPerspective',
241
134
  args: [query]
242
135
  });
243
136
 
244
- const skepticalView = MemFlow.workflow.execHook({
245
- taskQueue: 'perspectives',
137
+ const skeptical = MemFlow.workflow.execHook({
138
+ taskQueue: 'agents',
246
139
  workflowName: 'skepticalPerspective',
247
140
  args: [query]
248
141
  });
249
142
 
250
- // Spawn a fact-checker child agent with its own entity type
143
+ // Launch a child workflow with its own isolated entity/state
251
144
  const factChecker = await MemFlow.workflow.execChild({
252
145
  entity: 'fact-checker',
253
146
  workflowName: 'factCheckAgent',
@@ -256,121 +149,221 @@ export async function researchAgent(query: string): Promise<any> {
256
149
  taskQueue: 'agents'
257
150
  });
258
151
 
259
- // Wait for all perspectives to contribute
260
- await Promise.all([optimisticView, skepticalView, factChecker]);
152
+ // Wait for all views to complete before analyzing
153
+ await Promise.all([optimistic, skeptical, factChecker]);
261
154
 
262
- // Self-reflection: analyze conflicting perspectives
155
+ // Final synthesis: aggregate and compare all perspectives
263
156
  await MemFlow.workflow.execHook({
264
157
  taskQueue: 'perspectives',
265
158
  workflowName: 'synthesizePerspectives',
266
159
  args: []
267
160
  });
268
161
 
269
- const finalContext = await entity.get();
270
- return finalContext;
162
+ return await entity.get();
271
163
  }
164
+ ```
165
+
166
+ #### Hooks: Perspectives
272
167
 
273
- /* ------------ Optimistic Perspective Hook ------------ */
274
- export async function optimisticPerspective(query: string): Promise<void> {
168
+ ```ts
169
+ // Optimistic hook looks for affirming evidence
170
+ export async function optimisticPerspective(query: string, config: {signal: string}): Promise<void> {
275
171
  const entity = await MemFlow.workflow.entity();
276
-
277
- // Optimistic perspective: look for supporting evidence
278
172
  const findings = await searchForSupportingEvidence(query);
279
-
280
- await entity.merge({
281
- perspectives: {
282
- optimistic: {
283
- findings,
284
- confidence: 0.8,
285
- bias: 'Tends to emphasize positive evidence'
286
- }
287
- }
288
- });
173
+ await entity.merge({ perspectives: { optimistic: { findings, confidence: 0.8 }}});
174
+ //signal the caller to notify all done
175
+ await MemFlow.workflow.signal(config.signal, {});
289
176
  }
290
177
 
291
- /* ------------ Skeptical Perspective Hook ------------ */
292
- export async function skepticalPerspective(query: string): Promise<void> {
178
+ // Skeptical hook seeks out contradictions and counterpoints
179
+ export async function skepticalPerspective(query: string, config: {signal: string}): Promise<void> {
293
180
  const entity = await MemFlow.workflow.entity();
294
-
295
- // Skeptical perspective: challenge assumptions
296
181
  const counterEvidence = await searchForCounterEvidence(query);
297
-
298
- await entity.merge({
299
- perspectives: {
300
- skeptical: {
301
- counterEvidence,
302
- confidence: 0.6,
303
- bias: 'Challenges assumptions and seeks contradictory evidence'
304
- }
305
- }
306
- });
182
+ await entity.merge({ perspectives: { skeptical: { counterEvidence, confidence: 0.6 }}});
183
+ await MemFlow.workflow.signal(config.signal, {});
307
184
  }
185
+ ```
186
+
187
+ #### Child Agent: Fact Checker
308
188
 
309
- /* ------------ Fact-Checker Child Agent ------------ */
189
+ ```ts
190
+ // A dedicated child agent with its own entity type and context
310
191
  export async function factCheckAgent(query: string): Promise<any> {
311
- // This child has its own entity type for specialized fact-checking context
312
192
  const entity = await MemFlow.workflow.entity();
313
-
314
- await entity.set({
315
- query,
316
- sources: [],
317
- verifications: [],
318
- credibilityScore: 0
319
- });
193
+ await entity.set({ query, sources: [], verifications: [] });
320
194
 
321
- // Fact-checker can spawn its own specialized hooks
322
195
  await MemFlow.workflow.execHook({
323
- taskQueue: 'verification',
196
+ taskQueue: 'agents',
324
197
  workflowName: 'verifySourceCredibility',
325
198
  args: [query]
326
199
  });
327
200
 
328
201
  return await entity.get();
329
202
  }
203
+ ```
330
204
 
331
- /* ------------ Synthesis Perspective Hook ------------ */
332
- export async function synthesizePerspectives(): Promise<void> {
205
+ #### Synthesis
206
+
207
+ ```ts
208
+ // Synthesis hook aggregates different viewpoints
209
+ export async function synthesizePerspectives(config: {signal: string}): Promise<void> {
333
210
  const entity = await MemFlow.workflow.entity();
334
211
  const context = await entity.get();
335
-
336
- // Analyze conflicting perspectives and synthesize
337
- const synthesis = await analyzePerspectives(context.perspectives);
338
-
212
+
213
+ const result = await analyzePerspectives(context.perspectives);
214
+
339
215
  await entity.merge({
340
216
  perspectives: {
341
217
  synthesis: {
342
- finalAssessment: synthesis,
343
- confidence: calculateConfidence(context.perspectives),
344
- reasoning: 'Balanced analysis of optimistic and skeptical viewpoints'
218
+ finalAssessment: result,
219
+ confidence: calculateConfidence(context.perspectives)
345
220
  }
346
221
  },
347
222
  status: 'completed'
348
223
  });
224
+ await MemFlow.workflow.signal(config.signal, {});
349
225
  }
350
226
  ```
351
227
 
352
- ### The Power of Perspective-Oriented Agents
228
+ ---
229
+
230
+ ## Building Pipelines with State
353
231
 
354
- This approach enables agents that can:
232
+ HotMesh treats pipelines as long-lived records, not ephemeral jobs. Every pipeline run is stateful, resumable, and traceable.
355
233
 
356
- * **Question themselves** – different hooks challenge each other's assumptions
357
- * **Spawn specialized co-agents** – child workflows with their own entity types tackle specific domains
358
- * **Maintain rich context** – all perspectives contribute to a shared, evolving knowledge base
359
- * **Scale horizontally** – each perspective can run on different workers
360
- * **Learn continuously** – entity state accumulates insights across all interactions
361
- * **Self-reflect** – synthesis hooks analyze conflicting perspectives and improve decision-making
234
+ ### Setup a Data Pipeline
235
+
236
+ ```ts
237
+ export async function dataPipeline(source: string): Promise<void> {
238
+ const entity = await MemFlow.workflow.entity();
362
239
 
363
- Because every perspective operates on the same durable entity, agents can pause, restart, and evolve their world-model indefinitely while maintaining coherent, multi-faceted intelligence.
240
+ // Initial policy and tracking setup
241
+ await entity.set({
242
+ source,
243
+ pipeline: { version: 1, policy: { refreshInterval: '24 hours' } },
244
+ changeLog: []
245
+ });
246
+
247
+ // Trigger the recurring orchestration pipeline
248
+ await MemFlow.workflow.execHook({
249
+ taskQueue: 'pipeline',
250
+ workflowName: 'runPipeline',
251
+ args: [true]
252
+ });
253
+ }
254
+ ```
255
+
256
+ ### Orchestration Hook
257
+
258
+ ```ts
259
+ export async function runPipeline(repeat = false): Promise<void> {
260
+ do {
261
+ // Perform transformation step
262
+ await MemFlow.workflow.execHook({
263
+ taskQueue: 'transform',
264
+ workflowName: 'cleanData',
265
+ args: []
266
+ });
267
+
268
+ if (repeat) {
269
+ // Schedule next execution
270
+ await MemFlow.workflow.execHook({
271
+ taskQueue: 'scheduler',
272
+ workflowName: 'scheduleRefresh',
273
+ args: []
274
+ });
275
+ }
276
+ } while (repeat)
277
+ }
278
+
279
+ /**
280
+ * Hook to clean and transform data
281
+ */
282
+ export async function cleanData(signalInfo?: { signal: string }): Promise<void> {
283
+ const entity = await MemFlow.workflow.entity();
284
+
285
+ // Simulate data cleaning
286
+ await entity.merge({
287
+ status: 'cleaning',
288
+ lastCleanedAt: new Date().toISOString()
289
+ });
290
+
291
+ // Add to changelog
292
+ await entity.append('changeLog', {
293
+ action: 'clean',
294
+ timestamp: new Date().toISOString()
295
+ });
296
+
297
+ // Signal completion if called via execHook
298
+ if (signalInfo?.signal) {
299
+ await MemFlow.workflow.signal(signalInfo.signal, {
300
+ status: 'cleaned',
301
+ timestamp: new Date().toISOString()
302
+ });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Hook to schedule the next refresh based on policy
308
+ */
309
+ export async function scheduleRefresh(signalInfo?: { signal: string }): Promise<void> {
310
+ const entity = await MemFlow.workflow.entity();
311
+
312
+ // Get refresh interval from policy
313
+ const currentEntity = await entity.get();
314
+ const refreshInterval = currentEntity.pipeline.policy.refreshInterval;
315
+
316
+ // Sleep for the configured interval
317
+ await MemFlow.workflow.sleepFor(refreshInterval);
318
+
319
+ // Update status after sleep
320
+ await entity.merge({
321
+ status: 'ready_for_refresh',
322
+ nextRefreshAt: new Date().toISOString()
323
+ });
324
+
325
+ // Add to changelog
326
+ await entity.append('changeLog', {
327
+ action: 'schedule_refresh',
328
+ timestamp: new Date().toISOString(),
329
+ nextRefresh: new Date().toISOString()
330
+ });
331
+
332
+ // Signal completion if called via execHook
333
+ if (signalInfo?.signal) {
334
+ await MemFlow.workflow.signal(signalInfo.signal, {
335
+ status: 'scheduled',
336
+ nextRefresh: new Date().toISOString()
337
+ });
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### Trigger from Outside
343
+
344
+ ```ts
345
+ // External systems can trigger a single pipeline run
346
+ export async function triggerRefresh() {
347
+ const client = new MemFlow.Client({/*...*/});
348
+
349
+ await client.workflow.hook({
350
+ workflowId: 'pipeline-123',
351
+ taskQueue: 'pipeline',
352
+ workflowName: 'runPipeline',
353
+ args: []
354
+ });
355
+ }
356
+ ```
364
357
 
365
358
  ---
366
359
 
367
- ## πŸ“š Documentation & Links
360
+ ## Documentation & Links
368
361
 
369
- * SDK API – [https://hotmeshio.github.io/sdk-typescript](https://hotmeshio.github.io/sdk-typescript)
370
- * Examples – [https://github.com/hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript)
362
+ * SDK Reference – [hotmeshio.github.io/sdk-typescript](https://hotmeshio.github.io/sdk-typescript)
363
+ * Examples – [github.com/hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript)
371
364
 
372
365
  ---
373
366
 
374
367
  ## License
375
368
 
376
- Apache 2.0 – see `LICENSE` for details.
369
+ Apache 2.0 – See `LICENSE` for details.
@@ -15,6 +15,7 @@ declare class MemFlowWaitForError extends Error {
15
15
  workflowId: string;
16
16
  index: number;
17
17
  workflowDimension: string;
18
+ type: string;
18
19
  constructor(params: MemFlowWaitForErrorType);
19
20
  }
20
21
  declare class MemFlowProxyError extends Error {
@@ -31,6 +32,7 @@ declare class MemFlowProxyError extends Error {
31
32
  workflowDimension: string;
32
33
  workflowId: string;
33
34
  workflowTopic: string;
35
+ type: string;
34
36
  constructor(params: MemFlowProxyErrorType);
35
37
  }
36
38
  declare class MemFlowChildError extends Error {
@@ -50,6 +52,7 @@ declare class MemFlowChildError extends Error {
50
52
  parentWorkflowId: string;
51
53
  workflowId: string;
52
54
  workflowTopic: string;
55
+ type: string;
53
56
  constructor(params: MemFlowChildErrorType);
54
57
  }
55
58
  declare class MemFlowWaitForAllError extends Error {
@@ -62,6 +65,7 @@ declare class MemFlowWaitForAllError extends Error {
62
65
  parentWorkflowId: string;
63
66
  workflowId: string;
64
67
  workflowTopic: string;
68
+ type: string;
65
69
  constructor(params: MemFlowWaitForAllErrorType);
66
70
  }
67
71
  declare class MemFlowSleepError extends Error {
@@ -70,22 +74,27 @@ declare class MemFlowSleepError extends Error {
70
74
  duration: number;
71
75
  index: number;
72
76
  workflowDimension: string;
77
+ type: string;
73
78
  constructor(params: MemFlowSleepErrorType);
74
79
  }
75
80
  declare class MemFlowTimeoutError extends Error {
76
81
  code: number;
82
+ type: string;
77
83
  constructor(message: string, stack?: string);
78
84
  }
79
85
  declare class MemFlowMaxedError extends Error {
80
86
  code: number;
87
+ type: string;
81
88
  constructor(message: string, stackTrace?: string);
82
89
  }
83
90
  declare class MemFlowFatalError extends Error {
84
91
  code: number;
92
+ type: string;
85
93
  constructor(message: string, stackTrace?: string);
86
94
  }
87
95
  declare class MemFlowRetryError extends Error {
88
96
  code: number;
97
+ type: string;
89
98
  constructor(message: string, stackTrace?: string);
90
99
  }
91
100
  declare class MapDataError extends Error {
@@ -19,6 +19,7 @@ exports.SetStateError = SetStateError;
19
19
  class MemFlowWaitForError extends Error {
20
20
  constructor(params) {
21
21
  super(`WaitFor Interruption`);
22
+ this.type = 'MemFlowWaitForError';
22
23
  this.signalId = params.signalId;
23
24
  this.index = params.index;
24
25
  this.workflowDimension = params.workflowDimension;
@@ -29,6 +30,7 @@ exports.MemFlowWaitForError = MemFlowWaitForError;
29
30
  class MemFlowProxyError extends Error {
30
31
  constructor(params) {
31
32
  super(`ProxyActivity Interruption`);
33
+ this.type = 'MemFlowProxyError';
32
34
  this.arguments = params.arguments;
33
35
  this.workflowId = params.workflowId;
34
36
  this.workflowTopic = params.workflowTopic;
@@ -48,6 +50,7 @@ exports.MemFlowProxyError = MemFlowProxyError;
48
50
  class MemFlowChildError extends Error {
49
51
  constructor(params) {
50
52
  super(`ExecChild Interruption`);
53
+ this.type = 'MemFlowChildError';
51
54
  this.arguments = params.arguments;
52
55
  this.workflowId = params.workflowId;
53
56
  this.workflowTopic = params.workflowTopic;
@@ -70,6 +73,7 @@ exports.MemFlowChildError = MemFlowChildError;
70
73
  class MemFlowWaitForAllError extends Error {
71
74
  constructor(params) {
72
75
  super(`Collation Interruption`);
76
+ this.type = 'MemFlowWaitForAllError';
73
77
  this.items = params.items;
74
78
  this.size = params.size;
75
79
  this.workflowId = params.workflowId;
@@ -85,6 +89,7 @@ exports.MemFlowWaitForAllError = MemFlowWaitForAllError;
85
89
  class MemFlowSleepError extends Error {
86
90
  constructor(params) {
87
91
  super(`SleepFor Interruption`);
92
+ this.type = 'MemFlowSleepError';
88
93
  this.duration = params.duration;
89
94
  this.workflowId = params.workflowId;
90
95
  this.index = params.index;
@@ -96,6 +101,7 @@ exports.MemFlowSleepError = MemFlowSleepError;
96
101
  class MemFlowTimeoutError extends Error {
97
102
  constructor(message, stack) {
98
103
  super(message);
104
+ this.type = 'MemFlowTimeoutError';
99
105
  if (this.stack) {
100
106
  this.stack = stack;
101
107
  }
@@ -106,6 +112,7 @@ exports.MemFlowTimeoutError = MemFlowTimeoutError;
106
112
  class MemFlowMaxedError extends Error {
107
113
  constructor(message, stackTrace) {
108
114
  super(message);
115
+ this.type = 'MemFlowMaxedError';
109
116
  if (stackTrace) {
110
117
  this.stack = stackTrace;
111
118
  }
@@ -116,6 +123,7 @@ exports.MemFlowMaxedError = MemFlowMaxedError;
116
123
  class MemFlowFatalError extends Error {
117
124
  constructor(message, stackTrace) {
118
125
  super(message);
126
+ this.type = 'MemFlowFatalError';
119
127
  if (stackTrace) {
120
128
  this.stack = stackTrace;
121
129
  }
@@ -126,6 +134,7 @@ exports.MemFlowFatalError = MemFlowFatalError;
126
134
  class MemFlowRetryError extends Error {
127
135
  constructor(message, stackTrace) {
128
136
  super(message);
137
+ this.type = 'MemFlowRetryError';
129
138
  if (stackTrace) {
130
139
  this.stack = stackTrace;
131
140
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -6,6 +6,7 @@ import { Entity } from './entity';
6
6
  import { WorkerService } from './worker';
7
7
  import { WorkflowService } from './workflow';
8
8
  import { WorkflowHandleService } from './handle';
9
+ import { didInterrupt } from './workflow/interruption';
9
10
  /**
10
11
  * The MemFlow service is a collection of services that
11
12
  * emulate Temporal's capabilities, but instead are
@@ -106,6 +107,13 @@ declare class MemFlowClass {
106
107
  * including: `execChild`, `waitFor`, `sleep`, etc
107
108
  */
108
109
  static workflow: typeof WorkflowService;
110
+ /**
111
+ * Checks if an error is a HotMesh reserved error type that indicates
112
+ * a workflow interruption rather than a true error condition.
113
+ *
114
+ * @see {@link utils/interruption.didInterrupt} for detailed documentation
115
+ */
116
+ static didInterrupt: typeof didInterrupt;
109
117
  /**
110
118
  * Shutdown everything. All connections, workers, and clients will be closed.
111
119
  * Include in your signal handlers to ensure a clean shutdown.
@@ -9,6 +9,7 @@ const entity_1 = require("./entity");
9
9
  const worker_1 = require("./worker");
10
10
  const workflow_1 = require("./workflow");
11
11
  const handle_1 = require("./handle");
12
+ const interruption_1 = require("./workflow/interruption");
12
13
  /**
13
14
  * The MemFlow service is a collection of services that
14
15
  * emulate Temporal's capabilities, but instead are
@@ -120,3 +121,10 @@ MemFlowClass.Worker = worker_1.WorkerService;
120
121
  * including: `execChild`, `waitFor`, `sleep`, etc
121
122
  */
122
123
  MemFlowClass.workflow = workflow_1.WorkflowService;
124
+ /**
125
+ * Checks if an error is a HotMesh reserved error type that indicates
126
+ * a workflow interruption rather than a true error condition.
127
+ *
128
+ * @see {@link utils/interruption.didInterrupt} for detailed documentation
129
+ */
130
+ MemFlowClass.didInterrupt = interruption_1.didInterrupt;
@@ -322,6 +322,31 @@ class WorkerService {
322
322
  const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
323
323
  return await workflowFunction.apply(this, workflowInput.arguments);
324
324
  });
325
+ //if the embedded function has a try/catch, it can interrup the throw
326
+ // throw here to interrupt the workflow if the embedded function caught and suppressed
327
+ if (interruptionRegistry.length > 0) {
328
+ const payload = interruptionRegistry[0];
329
+ switch (payload.type) {
330
+ case 'MemFlowWaitForError':
331
+ throw new errors_1.MemFlowWaitForError(payload);
332
+ case 'MemFlowProxyError':
333
+ throw new errors_1.MemFlowProxyError(payload);
334
+ case 'MemFlowChildError':
335
+ throw new errors_1.MemFlowChildError(payload);
336
+ case 'MemFlowSleepError':
337
+ throw new errors_1.MemFlowSleepError(payload);
338
+ case 'MemFlowTimeoutError':
339
+ throw new errors_1.MemFlowTimeoutError(payload.message, payload.stack);
340
+ case 'MemFlowMaxedError':
341
+ throw new errors_1.MemFlowMaxedError(payload.message, payload.stack);
342
+ case 'MemFlowFatalError':
343
+ throw new errors_1.MemFlowFatalError(payload.message, payload.stack);
344
+ case 'MemFlowRetryError':
345
+ throw new errors_1.MemFlowRetryError(payload.message, payload.stack);
346
+ default:
347
+ throw new errors_1.MemFlowRetryError(`Unknown interruption type: ${payload.type}`);
348
+ }
349
+ }
325
350
  return {
326
351
  code: 200,
327
352
  status: stream_1.StreamStatus.SUCCESS,
@@ -82,6 +82,7 @@ async function execChild(options) {
82
82
  const interruptionMessage = getChildInterruptPayload(context, options, execIndex);
83
83
  interruptionRegistry.push({
84
84
  code: common_1.HMSH_CODE_MEMFLOW_CHILD,
85
+ type: 'MemFlowChildError',
85
86
  ...interruptionMessage,
86
87
  });
87
88
  await (0, common_1.sleepImmediate)();
@@ -3,8 +3,8 @@ import { HookOptions } from './common';
3
3
  * Extended hook options that include signal configuration
4
4
  */
5
5
  export interface ExecHookOptions extends HookOptions {
6
- /** Signal ID to send after hook execution */
7
- signalId: string;
6
+ /** Signal ID to send after hook execution; if not provided, a random one will be generated */
7
+ signalId?: string;
8
8
  }
9
9
  /**
10
10
  * Executes a hook function and awaits the signal response.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.execHook = void 0;
4
4
  const hook_1 = require("./hook");
5
5
  const waitFor_1 = require("./waitFor");
6
+ const interruption_1 = require("./interruption");
6
7
  /**
7
8
  * Executes a hook function and awaits the signal response.
8
9
  * This is a convenience method that combines `hook()` and `waitFor()` operations.
@@ -60,14 +61,23 @@ const waitFor_1 = require("./waitFor");
60
61
  * ```
61
62
  */
62
63
  async function execHook(options) {
63
- // Create hook options with signal field added to args
64
- const hookOptions = {
65
- ...options,
66
- args: [...options.args, { signal: options.signalId }]
67
- };
68
- // Execute the hook with the signal information
69
- await (0, hook_1.hook)(hookOptions);
70
- // Wait for the signal response and return it
71
- return await (0, waitFor_1.waitFor)(options.signalId);
64
+ try {
65
+ if (!options.signalId) {
66
+ options.signalId = 'memflow-hook-' + crypto.randomUUID();
67
+ }
68
+ const hookOptions = {
69
+ ...options,
70
+ args: [...options.args, { signal: options.signalId, $memflow: true }]
71
+ };
72
+ // Execute the hook with the signal information
73
+ await (0, hook_1.hook)(hookOptions);
74
+ // Wait for the signal response and return it
75
+ return await (0, waitFor_1.waitFor)(options.signalId);
76
+ }
77
+ catch (error) {
78
+ if ((0, interruption_1.didInterrupt)(error)) {
79
+ throw error;
80
+ }
81
+ }
72
82
  }
73
83
  exports.execHook = execHook;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Checks if an error is a HotMesh reserved error type that indicates
3
+ * a workflow interruption rather than a true error condition.
4
+ *
5
+ * When this returns true, you can safely return from your workflow function.
6
+ * The workflow engine will handle the interruption automatically.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * try {
11
+ * await someWorkflowOperation();
12
+ * } catch (error) {
13
+ * // Check if this is a HotMesh interruption
14
+ * if (didInterrupt(error)) {
15
+ * // Rethrow the error if HotMesh interruption
16
+ * throw error;
17
+ * }
18
+ * // Handle actual error
19
+ * console.error('Workflow failed:', error);
20
+ * }
21
+ * ```
22
+ *
23
+ * @param error - The error to check
24
+ * @returns true if the error is a HotMesh interruption
25
+ */
26
+ export declare function didInterrupt(error: Error): boolean;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.didInterrupt = void 0;
4
+ const errors_1 = require("../../../modules/errors");
5
+ /**
6
+ * Checks if an error is a HotMesh reserved error type that indicates
7
+ * a workflow interruption rather than a true error condition.
8
+ *
9
+ * When this returns true, you can safely return from your workflow function.
10
+ * The workflow engine will handle the interruption automatically.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * try {
15
+ * await someWorkflowOperation();
16
+ * } catch (error) {
17
+ * // Check if this is a HotMesh interruption
18
+ * if (didInterrupt(error)) {
19
+ * // Rethrow the error if HotMesh interruption
20
+ * throw error;
21
+ * }
22
+ * // Handle actual error
23
+ * console.error('Workflow failed:', error);
24
+ * }
25
+ * ```
26
+ *
27
+ * @param error - The error to check
28
+ * @returns true if the error is a HotMesh interruption
29
+ */
30
+ function didInterrupt(error) {
31
+ return (error instanceof errors_1.MemFlowChildError ||
32
+ error instanceof errors_1.MemFlowFatalError ||
33
+ error instanceof errors_1.MemFlowMaxedError ||
34
+ error instanceof errors_1.MemFlowProxyError ||
35
+ error instanceof errors_1.MemFlowRetryError ||
36
+ error instanceof errors_1.MemFlowSleepError ||
37
+ error instanceof errors_1.MemFlowTimeoutError ||
38
+ error instanceof errors_1.MemFlowWaitForError ||
39
+ error instanceof errors_1.MemFlowWaitForAllError);
40
+ }
41
+ exports.didInterrupt = didInterrupt;
@@ -67,6 +67,7 @@ function wrapActivity(activityName, options) {
67
67
  const interruptionMessage = getProxyInterruptPayload(context, activityName, execIndex, args, options);
68
68
  interruptionRegistry.push({
69
69
  code: common_1.HMSH_CODE_MEMFLOW_PROXY,
70
+ type: 'MemFlowProxyError',
70
71
  ...interruptionMessage,
71
72
  });
72
73
  await (0, common_1.sleepImmediate)();
@@ -43,6 +43,7 @@ async function sleepFor(duration) {
43
43
  };
44
44
  interruptionRegistry.push({
45
45
  code: common_1.HMSH_CODE_MEMFLOW_SLEEP,
46
+ type: 'MemFlowSleepError',
46
47
  ...interruptionMessage,
47
48
  });
48
49
  await (0, common_1.sleepImmediate)();
@@ -45,11 +45,10 @@ async function waitFor(signalId) {
45
45
  signalId,
46
46
  index: execIndex,
47
47
  workflowDimension,
48
- };
49
- interruptionRegistry.push({
48
+ type: 'MemFlowWaitForError',
50
49
  code: common_1.HMSH_CODE_MEMFLOW_WAIT,
51
- ...interruptionMessage,
52
- });
50
+ };
51
+ interruptionRegistry.push(interruptionMessage);
53
52
  await (0, common_1.sleepImmediate)();
54
53
  throw new common_1.MemFlowWaitForError(interruptionMessage);
55
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",