@botpress/zai 2.5.18 → 2.6.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/dist/context.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from "./emitter";
2
+ import { fastHash } from "./utils";
2
3
  export class ZaiContext {
3
4
  _startedAt = Date.now();
4
5
  _inputCost = 0;
@@ -15,8 +16,10 @@ export class ZaiContext {
15
16
  adapter;
16
17
  source;
17
18
  _eventEmitter;
19
+ _memoizer;
18
20
  controller = new AbortController();
19
21
  _client;
22
+ static _noopMemoizer = { run: (_id, fn) => fn() };
20
23
  constructor(props) {
21
24
  this._client = props.client.clone();
22
25
  this.taskId = props.taskId;
@@ -24,6 +27,7 @@ export class ZaiContext {
24
27
  this.adapter = props.adapter;
25
28
  this.source = props.source;
26
29
  this.taskType = props.taskType;
30
+ this._memoizer = props.memoizer ?? ZaiContext._noopMemoizer;
27
31
  this._eventEmitter = new EventEmitter();
28
32
  this._client.on("request", () => {
29
33
  this._totalRequests++;
@@ -57,6 +61,16 @@ export class ZaiContext {
57
61
  this._eventEmitter.clear();
58
62
  }
59
63
  async generateContent(props) {
64
+ const memoKey = `zai:memo:${this.taskType}:${this.taskId || "default"}:${fastHash(
65
+ JSON.stringify({
66
+ s: props.systemPrompt,
67
+ m: props.messages?.map((m) => "content" in m ? m.content : ""),
68
+ st: props.stopSequences
69
+ })
70
+ )}`;
71
+ return this._memoizer.run(memoKey, () => this._generateContentInner(props));
72
+ }
73
+ async _generateContentInner(props) {
60
74
  const maxRetries = Math.max(props.maxRetries ?? 3, 0);
61
75
  const transform = props.transform;
62
76
  let lastError = null;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,16 @@ declare abstract class Adapter {
41
41
  abstract saveExample<TInput, TOutput>(props: SaveExampleProps<TInput, TOutput>): Promise<void>;
42
42
  }
43
43
 
44
+ /**
45
+ * A memoizer that caches the result of async operations by a unique key.
46
+ *
47
+ * When used with the Botpress ADK workflow `step` function, this enables
48
+ * Zai operations to resume where they left off if a workflow is interrupted.
49
+ *
50
+ */
51
+ type Memoizer = {
52
+ run: <T>(id: string, fn: () => Promise<T>) => Promise<T>;
53
+ };
44
54
  /**
45
55
  * Active learning configuration for improving AI operations over time.
46
56
  *
@@ -98,6 +108,16 @@ type ZaiConfig = {
98
108
  activeLearning?: ActiveLearning;
99
109
  /** Namespace for organizing tasks (default: 'zai') */
100
110
  namespace?: string;
111
+ /**
112
+ * Memoizer (or factory returning one) for caching cognitive call results.
113
+ *
114
+ * When provided, all LLM calls are wrapped in the memoizer, allowing results
115
+ * to be cached and replayed. This is useful for resuming workflow runs where
116
+ * Zai operations have already completed their cognitive calls.
117
+ *
118
+ * If a factory function is provided, it is called once per Zai operation invocation.
119
+ */
120
+ memoize?: Memoizer | (() => Memoizer);
101
121
  };
102
122
  /**
103
123
  * Zai - A type-safe LLM utility library for production-ready AI operations.
@@ -171,6 +191,7 @@ declare class Zai {
171
191
  protected namespace: string;
172
192
  protected adapter: Adapter;
173
193
  protected activeLearning: ActiveLearning;
194
+ protected _memoize?: Memoizer | (() => Memoizer);
174
195
  /**
175
196
  * Creates a new Zai instance with the specified configuration.
176
197
  *
@@ -195,6 +216,8 @@ declare class Zai {
195
216
  constructor(config: ZaiConfig);
196
217
  /** @internal */
197
218
  protected callModel(props: Parameters<Cognitive['generateContent']>[0]): ReturnType<Cognitive['generateContent']>;
219
+ /** @internal */
220
+ protected _resolveMemoizer(): Memoizer | undefined;
198
221
  protected getTokenizer(): Promise<TextTokenizer>;
199
222
  protected fetchModelDetails(): Promise<void>;
200
223
  protected get taskId(): string;
@@ -299,6 +322,7 @@ type ZaiContextProps = {
299
322
  modelId: string;
300
323
  adapter?: Adapter;
301
324
  source?: GenerateContentInput['meta'];
325
+ memoizer?: Memoizer;
302
326
  };
303
327
  /**
304
328
  * Usage statistics tracking tokens, cost, and request metrics for an operation.
@@ -370,8 +394,10 @@ declare class ZaiContext {
370
394
  adapter?: Adapter;
371
395
  source?: GenerateContentInput['meta'];
372
396
  private _eventEmitter;
397
+ private _memoizer;
373
398
  controller: AbortController;
374
399
  private _client;
400
+ private static _noopMemoizer;
375
401
  constructor(props: ZaiContextProps);
376
402
  getModel(): Promise<Model>;
377
403
  on<K extends keyof ContextEvents>(type: K, listener: (event: ContextEvents[K]) => void): this;
@@ -382,6 +408,7 @@ declare class ZaiContext {
382
408
  text: string | undefined;
383
409
  extracted: Out;
384
410
  }>;
411
+ private _generateContentInner;
385
412
  get elapsedTime(): number;
386
413
  get usage(): Usage;
387
414
  }
@@ -2143,4 +2170,4 @@ declare module '@botpress/zai' {
2143
2170
  }
2144
2171
  }
2145
2172
 
2146
- export { Zai };
2173
+ export { type Memoizer, Zai };
@@ -373,7 +373,8 @@ Zai.prototype.answer = function(documents, question, _options) {
373
373
  modelId: this.Model,
374
374
  taskId: this.taskId,
375
375
  taskType: "zai.answer",
376
- adapter: this.adapter
376
+ adapter: this.adapter,
377
+ memoizer: this._resolveMemoizer()
377
378
  });
378
379
  return new Response(
379
380
  context,
@@ -181,7 +181,8 @@ Zai.prototype.check = function(input, condition, _options) {
181
181
  modelId: this.Model,
182
182
  taskId: this.taskId,
183
183
  taskType: "zai.check",
184
- adapter: this.adapter
184
+ adapter: this.adapter,
185
+ memoizer: this._resolveMemoizer()
185
186
  });
186
187
  return new Response(context, check(input, condition, options, context), (result) => result.value);
187
188
  };
@@ -313,7 +313,8 @@ Zai.prototype.extract = function(input, schema, _options) {
313
313
  modelId: this.Model,
314
314
  taskId: this.taskId,
315
315
  taskType: "zai.extract",
316
- adapter: this.adapter
316
+ adapter: this.adapter,
317
+ memoizer: this._resolveMemoizer()
317
318
  });
318
319
  return new Response(context, extract(input, schema, _options, context), (result) => result);
319
320
  };
@@ -202,7 +202,8 @@ Zai.prototype.filter = function(input, condition, _options) {
202
202
  modelId: this.Model,
203
203
  taskId: this.taskId,
204
204
  taskType: "zai.filter",
205
- adapter: this.adapter
205
+ adapter: this.adapter,
206
+ memoizer: this._resolveMemoizer()
206
207
  });
207
208
  return new Response(context, filter(input, condition, _options, context), (result) => result);
208
209
  };
@@ -541,7 +541,8 @@ Zai.prototype.group = function(input, _options) {
541
541
  modelId: this.Model,
542
542
  taskId: this.taskId,
543
543
  taskType: "zai.group",
544
- adapter: this.adapter
544
+ adapter: this.adapter,
545
+ memoizer: this._resolveMemoizer()
545
546
  });
546
547
  return new Response(context, group(input, _options, context), (result) => {
547
548
  const merged = {};
@@ -276,7 +276,8 @@ Zai.prototype.label = function(input, labels, _options) {
276
276
  modelId: this.Model,
277
277
  taskId: this.taskId,
278
278
  taskType: "zai.label",
279
- adapter: this.adapter
279
+ adapter: this.adapter,
280
+ memoizer: this._resolveMemoizer()
280
281
  });
281
282
  return new Response(
282
283
  context,
@@ -392,7 +392,8 @@ Zai.prototype.patch = function(files, instructions, _options) {
392
392
  modelId: this.Model,
393
393
  taskId: this.taskId,
394
394
  taskType: "zai.patch",
395
- adapter: this.adapter
395
+ adapter: this.adapter,
396
+ memoizer: this._resolveMemoizer()
396
397
  });
397
398
  return new Response(context, patch(files, instructions, _options, context), (result) => result);
398
399
  };
@@ -335,7 +335,8 @@ Zai.prototype.rate = function(input, instructions, _options) {
335
335
  modelId: this.Model,
336
336
  taskId: this.taskId,
337
337
  taskType: "zai.rate",
338
- adapter: this.adapter
338
+ adapter: this.adapter,
339
+ memoizer: this._resolveMemoizer()
339
340
  });
340
341
  return new Response(
341
342
  context,
@@ -136,7 +136,8 @@ Zai.prototype.rewrite = function(original, prompt, _options) {
136
136
  modelId: this.Model,
137
137
  taskId: this.taskId,
138
138
  taskType: "zai.rewrite",
139
- adapter: this.adapter
139
+ adapter: this.adapter,
140
+ memoizer: this._resolveMemoizer()
140
141
  });
141
142
  return new Response(context, rewrite(original, prompt, _options, context), (result) => result);
142
143
  };
@@ -511,7 +511,8 @@ Zai.prototype.sort = function(input, instructions, _options) {
511
511
  modelId: this.Model,
512
512
  taskId: this.taskId,
513
513
  taskType: "zai.sort",
514
- adapter: this.adapter
514
+ adapter: this.adapter,
515
+ memoizer: this._resolveMemoizer()
515
516
  });
516
517
  return new Response(
517
518
  context,
@@ -148,7 +148,8 @@ Zai.prototype.summarize = function(original, _options) {
148
148
  modelId: this.Model,
149
149
  taskId: this.taskId,
150
150
  taskType: "summarize",
151
- adapter: this.adapter
151
+ adapter: this.adapter,
152
+ memoizer: this._resolveMemoizer()
152
153
  });
153
154
  return new Response(context, summarize(original, options, context), (value) => value);
154
155
  };
@@ -60,7 +60,8 @@ Zai.prototype.text = function(prompt, _options) {
60
60
  modelId: this.Model,
61
61
  taskId: this.taskId,
62
62
  taskType: "zai.text",
63
- adapter: this.adapter
63
+ adapter: this.adapter,
64
+ memoizer: this._resolveMemoizer()
64
65
  });
65
66
  return new Response(context, text(prompt, _options, context), (result) => result);
66
67
  };
package/dist/zai.js CHANGED
@@ -47,6 +47,7 @@ export class Zai {
47
47
  namespace;
48
48
  adapter;
49
49
  activeLearning;
50
+ _memoize;
50
51
  /**
51
52
  * Creates a new Zai instance with the specified configuration.
52
53
  *
@@ -80,6 +81,7 @@ export class Zai {
80
81
  client: this.client.client,
81
82
  tableName: parsed.activeLearning.tableName
82
83
  }) : new MemoryAdapter([]);
84
+ this._memoize = config.memoize;
83
85
  }
84
86
  /** @internal */
85
87
  async callModel(props) {
@@ -90,6 +92,13 @@ export class Zai {
90
92
  userId: this._userId
91
93
  });
92
94
  }
95
+ /** @internal */
96
+ _resolveMemoizer() {
97
+ if (!this._memoize) {
98
+ return void 0;
99
+ }
100
+ return typeof this._memoize === "function" ? this._memoize() : this._memoize;
101
+ }
93
102
  async getTokenizer() {
94
103
  Zai.tokenizer ??= await (async () => {
95
104
  while (!getWasmTokenizer) {
@@ -1971,3 +1971,8 @@
1971
1971
  {"key":"f8a39096","input":"{\"body\":{\"messages\":[{\"content\":\"You are grouping elements into cohesive groups.\\n\\n**Instructions:** Group by food type\\n\\n\\n**Important:**\\n- Each element gets exactly ONE group label\\n- Use EXACT SAME label for similar items (case-sensitive)\\n- Create new descriptive labels when needed\\n\\n**Output Format:**\\nOne line per element:\\n■0:Group Label■\\n■1:Group Label■\\n■END■\",\"role\":\"system\"},{\"content\":\"**Elements (■0 to ■11):**\\n■0: apple■\\n■1: banana■\\n■2: orange■\\n■3: mango■\\n■4: grape■\\n■5: carrot■\\n■6: broccoli■\\n■7: spinach■\\n■8: celery■\\n■9: kale■\\n■10: rice■\\n■11: wheat■\\n\\n**Task:** For each element, output one line with its group label.\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.group\",\"promptSource\":\"zai:zai.group:default\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:Fruit■ \n■1:Fruit■ \n■2:Fruit■ \n■3:Fruit■ \n■4:Fruit■ \n■5:Vegetable■ \n■6:Vegetable■ \n■7:Vegetable■ \n■8:Vegetable■ \n■9:Vegetable■ \n■10:Grain■ \n■11:Grain■ \n","metadata":{"provider":"cerebras","usage":{"inputTokens":250,"outputTokens":220,"inputCost":0.0000875,"outputCost":0.000165},"model":"cerebras:gpt-oss-120b","ttft":150,"latency":281,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0002525}}}
1972
1972
  {"key":"f5cec64f","input":"{\"body\":{\"messages\":[{\"content\":\"You are grouping elements into cohesive groups.\\n\\n**Instructions:** Group by food type\\n\\n\\n**Important:**\\n- Each element gets exactly ONE group label\\n- Use EXACT SAME label for similar items (case-sensitive)\\n- Create new descriptive labels when needed\\n\\n**Output Format:**\\nOne line per element:\\n■0:Group Label■\\n■1:Group Label■\\n■END■\",\"role\":\"system\"},{\"content\":\"**Existing Groups (prefer reusing these):**\\n- Fruit\\n- Vegetable\\n- Grain\\n\\n**Elements (■0 to ■11):**\\n■0: apple■\\n■1: banana■\\n■2: orange■\\n■3: mango■\\n■4: grape■\\n■5: carrot■\\n■6: broccoli■\\n■7: spinach■\\n■8: celery■\\n■9: kale■\\n■10: rice■\\n■11: wheat■\\n\\n**Task:** For each element, output one line with its group label.\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.group\",\"promptSource\":\"zai:zai.group:default\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:Fruit■ \n■1:Fruit■ \n■2:Fruit■ \n■3:Fruit■ \n■4:Fruit■ \n■5:Vegetable■ \n■6:Vegetable■ \n■7:Vegetable■ \n■8:Vegetable■ \n■9:Vegetable■ \n■10:Grain■ \n■11:Grain■ \n","metadata":{"provider":"cerebras","usage":{"inputTokens":269,"outputTokens":222,"inputCost":0.00009415,"outputCost":0.0001665},"model":"cerebras:gpt-oss-120b","ttft":154,"latency":296,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.00026065}}}
1973
1973
  {"key":"b2435b9e","input":"{\"body\":{\"messages\":[{\"content\":\"You are grouping elements into cohesive groups.\\n\\n**Instructions:** Group by food type\\n\\n\\n**Important:**\\n- Each element gets exactly ONE group label\\n- Use EXACT SAME label for similar items (case-sensitive)\\n- Create new descriptive labels when needed\\n\\n**Output Format:**\\nOne line per element:\\n■0:Group Label■\\n■1:Group Label■\\n■END■\",\"role\":\"system\"},{\"content\":\"**Existing Groups (prefer reusing these):**\\n- Fruit\\n- Vegetable\\n\\n**Elements (■0 to ■1):**\\n■0: rice■\\n■1: wheat■\\n\\n**Task:** For each element, output one line with its group label.\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.group\",\"promptSource\":\"zai:zai.group:default\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:Grain■\n■1:Grain■\n","metadata":{"provider":"cerebras","usage":{"inputTokens":206,"outputTokens":172,"inputCost":0.0000721,"outputCost":0.000129},"model":"cerebras:gpt-oss-120b","ttft":192,"latency":2310,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0002011}}}
1974
+ {"key":"f0425e37","input":"{\"body\":{\"messages\":[{\"content\":\"You are rating items based on evaluation criteria.\\n\\nEvaluation Criteria:\\n**relevance**:\\n - very_bad (1): Content rarely relevant to your interests.\\n - bad (2): Mostly irrelevant, occasional useful info.\\n - average (3): Balanced relevance, some useful content.\\n - good (4): Often relevant, aligns with interests.\\n - very_good (5): Consistently highly relevant and valuable.\\n\\n**authority**:\\n - very_bad (1): Sender lacks credibility, unknown source.\\n - bad (2): Low credibility, questionable expertise overall.\\n - average (3): Moderate credibility, recognized but not expert.\\n - good (4): Credible source, recognized expertise in industry.\\n - very_good (5): High authority, leading expert in field.\\n\\n**frequency**:\\n - very_bad (1): Excessive emails, overwhelming inbox daily.\\n - bad (2): Too many emails, frequent interruptions.\\n - average (3): Moderate volume, acceptable cadence for work.\\n - good (4): Well-paced, occasional useful messages that add value.\\n - very_good (5): Sporadic, only essential communications when needed.\\n\\n**responsiveness**:\\n - very_bad (1): Never replies, unresponsive to queries.\\n - bad (2): Rarely replies, slow response times.\\n - average (3): Occasional replies, average speed in normal timeframe.\\n - good (4): Usually responsive, timely replies within days.\\n - very_good (5): Rapid, consistently helpful responses to all requests.\\n\\nFor each item, rate it on EACH criterion using one of these labels:\\nvery_bad, bad, average, good, very_good\\n\\nOutput format:\\n■0:criterion1=label;criterion2=label;criterion3=label■\\n■1:criterion1=label;criterion2=label;criterion3=label■\\n■END■\\n\\nIMPORTANT:\\n- Rate every item (■0 to ■3)\\n- Use exact criterion names: relevance, authority, frequency, responsiveness\\n- Use exact label names: very_bad, bad, average, good, very_good\\n- Use semicolons (;) between criteria\\n- Use equals (=) between criterion and label\",\"role\":\"system\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"partner@sequoia.vc\\\",\\\"subject\\\":\\\"Q4 Review\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @sequoia.vc is our investor - highest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"analyst@bankofamerica.com\\\",\\\"subject\\\":\\\"Market Report\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_bad■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: analyst@* prefix is spam - lowest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"ben@a16z.com\\\",\\\"subject\\\":\\\"Investment Discussion\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @a16z.com is potential investor - high importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"team@google.com\\\",\\\"subject\\\":\\\"Partnership Proposal\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=average■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @google.com is competitor - medium importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"roelof@sequoia.vc\\\",\\\"subject\\\":\\\"Portfolio Update\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @sequoia.vc is our investor - highest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Items to rate (■0 to ■3):\\n■0: {\\\"from\\\":\\\"sarah@sequoia.vc\\\",\\\"subject\\\":\\\"Board Meeting\\\"}■\\n■1: {\\\"from\\\":\\\"analyst@goldmansachs.com\\\",\\\"subject\\\":\\\"Earnings Report\\\"}■\\n■2: {\\\"from\\\":\\\"marc@a16z.com\\\",\\\"subject\\\":\\\"Funding Round\\\"}■\\n■3: {\\\"from\\\":\\\"recruiter@google.com\\\",\\\"subject\\\":\\\"Hiring\\\"}■\\n\\nRate each item on all criteria.\\nOutput format: ■index:criterion1=label;criterion2=label■\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.rate\",\"promptSource\":\"zai:zai.rate:zai/rate\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:relevance=very_good;authority=very_good;frequency=good;responsiveness=good■\n■1:relevance=good;authority=good;frequency=average;responsiveness=average■\n■2:relevance=good;authority=good;frequency=good;responsiveness=good■\n■3:relevance=average;authority=average;frequency=average;responsiveness=average■\n","metadata":{"provider":"cerebras","usage":{"inputTokens":1049,"outputTokens":617,"inputCost":0.00036715,"outputCost":0.00046275},"model":"cerebras:gpt-oss-120b","ttft":207,"latency":494,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0008299}}}
1975
+ {"key":"61f23c23","input":"{\"body\":{\"messages\":[{\"content\":\"You are rating items based on evaluation criteria.\\n\\nEvaluation Criteria:\\n**relevance**:\\n - very_bad (1): Content rarely relevant to your interests.\\n - bad (2): Mostly irrelevant, occasional useful info.\\n - average (3): Balanced relevance, some useful content.\\n - good (4): Often relevant, aligns with interests.\\n - very_good (5): Consistently highly relevant and valuable.\\n\\n**authority**:\\n - very_bad (1): Sender lacks credibility, unknown source.\\n - bad (2): Low credibility, questionable expertise overall.\\n - average (3): Moderate credibility, recognized but not expert.\\n - good (4): Credible source, recognized expertise in industry.\\n - very_good (5): High authority, leading expert in field.\\n\\n**frequency**:\\n - very_bad (1): Excessive emails, overwhelming inbox daily.\\n - bad (2): Too many emails, frequent interruptions.\\n - average (3): Moderate volume, acceptable cadence for work.\\n - good (4): Well-paced, occasional useful messages that add value.\\n - very_good (5): Sporadic, only essential communications when needed.\\n\\n**responsiveness**:\\n - very_bad (1): Never replies, unresponsive to queries.\\n - bad (2): Rarely replies, slow response times.\\n - average (3): Occasional replies, average speed in normal timeframe.\\n - good (4): Usually responsive, timely replies within days.\\n - very_good (5): Rapid, consistently helpful responses to all requests.\\n\\nFor each item, rate it on EACH criterion using one of these labels:\\nvery_bad, bad, average, good, very_good\\n\\nOutput format:\\n■0:criterion1=label;criterion2=label;criterion3=label■\\n■1:criterion1=label;criterion2=label;criterion3=label■\\n■END■\\n\\nIMPORTANT:\\n- Rate every item (■0 to ■3)\\n- Use exact criterion names: relevance, authority, frequency, responsiveness\\n- Use exact label names: very_bad, bad, average, good, very_good\\n- Use semicolons (;) between criteria\\n- Use equals (=) between criterion and label\",\"role\":\"system\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"analyst@bankofamerica.com\\\",\\\"subject\\\":\\\"Market Report\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_bad■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: analyst@* prefix is spam - lowest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"ben@a16z.com\\\",\\\"subject\\\":\\\"Investment Discussion\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @a16z.com is potential investor - high importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"partner@sequoia.vc\\\",\\\"subject\\\":\\\"Q4 Review\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @sequoia.vc is our investor - highest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"team@google.com\\\",\\\"subject\\\":\\\"Partnership Proposal\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=average■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @google.com is competitor - medium importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"from\\\":\\\"roelof@sequoia.vc\\\",\\\"subject\\\":\\\"Portfolio Update\\\"}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:relevance=very_good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: RULE: @sequoia.vc is our investor - highest importance rating\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Items to rate (■0 to ■3):\\n■0: {\\\"from\\\":\\\"sarah@sequoia.vc\\\",\\\"subject\\\":\\\"Board Meeting\\\"}■\\n■1: {\\\"from\\\":\\\"analyst@goldmansachs.com\\\",\\\"subject\\\":\\\"Earnings Report\\\"}■\\n■2: {\\\"from\\\":\\\"marc@a16z.com\\\",\\\"subject\\\":\\\"Funding Round\\\"}■\\n■3: {\\\"from\\\":\\\"recruiter@google.com\\\",\\\"subject\\\":\\\"Hiring\\\"}■\\n\\nRate each item on all criteria.\\nOutput format: ■index:criterion1=label;criterion2=label■\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.rate\",\"promptSource\":\"zai:zai.rate:zai/rate\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:relevance=very_good;authority=very_good;frequency=very_good;responsiveness=good■\n■1:relevance=average;authority=good;frequency=average;responsiveness=average■\n■2:relevance=good;authority=very_good;frequency=very_good;responsiveness=good■\n■3:relevance=average;authority=good;frequency=average;responsiveness=average■\n","metadata":{"provider":"cerebras","usage":{"inputTokens":1049,"outputTokens":727,"inputCost":0.00036715,"outputCost":0.00054525},"model":"cerebras:gpt-oss-120b","ttft":155,"latency":695,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0009124}}}
1976
+ {"key":"85d80390","input":"{\"body\":{\"messages\":[{\"content\":\"You are rating items based on evaluation criteria.\\n\\nEvaluation Criteria:\\n**length**:\\n - very_bad (1): Less than 4 characters\\n - bad (2): 4 to 5 characters\\n - average (3): 6 to 7 characters\\n - good (4): 8 to 11 characters\\n - very_good (5): 12 or more characters\\n\\n**complexity**:\\n - very_bad (1): Only one character type used\\n - bad (2): Two character types present\\n - average (3): Three character types present\\n - good (4): All four types, but predictable\\n - very_good (5): All four types, highly random\\n\\n**strength**:\\n - very_bad (1): Easily guessable, weak overall\\n - bad (2): Low entropy, vulnerable to attacks\\n - average (3): Moderate security, some protections\\n - good (4): Strong security, resistant to cracking\\n - very_good (5): Very high security, excellent protection\\n\\nFor each item, rate it on EACH criterion using one of these labels:\\nvery_bad, bad, average, good, very_good\\n\\nOutput format:\\n■0:criterion1=label;criterion2=label;criterion3=label■\\n■1:criterion1=label;criterion2=label;criterion3=label■\\n■END■\\n\\nIMPORTANT:\\n- Rate every item (■0 to ■1)\\n- Use exact criterion names: length, complexity, strength\\n- Use exact label names: very_bad, bad, average, good, very_good\\n- Use semicolons (;) between criteria\\n- Use equals (=) between criterion and label\",\"role\":\"system\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"password\\\":\\\"Str0ng!P@ss#2024\\\",\\\"length\\\":16,\\\"hasAll\\\":true}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:length=very_good;complexity=very_good;strength=very_good■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: Strong password: 16 chars, all character types\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example - Items to rate:\\n■0: {\\\"password\\\":\\\"weak\\\",\\\"length\\\":4,\\\"hasAll\\\":false}■\\n\\nRate each item on all criteria.\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■0:length=very_bad;complexity=very_bad;strength=very_bad■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Reasoning: Weak password: only 4 chars, missing character types\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Items to rate (■0 to ■1):\\n■0: {\\\"password\\\":\\\"MyStr0ng!Pass\\\",\\\"length\\\":13,\\\"hasAll\\\":true}■\\n■1: {\\\"password\\\":\\\"bad\\\",\\\"length\\\":3,\\\"hasAll\\\":false}■\\n\\nRate each item on all criteria.\\nOutput format: ■index:criterion1=label;criterion2=label■\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.rate\",\"promptSource\":\"zai:zai.rate:zai/rate\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■0:length=very_good;complexity=very_good;strength=very_good■\n■1:length=very_bad;complexity=very_bad;strength=very_bad■\n","metadata":{"provider":"cerebras","usage":{"inputTokens":682,"outputTokens":323,"inputCost":0.0002387,"outputCost":0.00024225},"model":"cerebras:gpt-oss-120b","ttft":171,"latency":357,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.00048095}}}
1977
+ {"key":"c99aa38f","input":"{\"body\":{\"messages\":[{\"content\":\"Check if the following condition is true or false for the given input. Before answering, make sure to read the input and the condition carefully.\\nJustify your answer, then answer with either ■TRUE■ or ■FALSE■ at the very end, then add ■END■ to finish the response.\\nIMPORTANT: Make sure to answer with either ■TRUE■ or ■FALSE■ at the end of your response, but NOT both.\\n---\\nExpert Examples (#1 to #3):\\n- You have been provided with examples from previous experts. Make sure to read them carefully before making your decision.\\n- Make sure to refer to the examples provided by the experts to justify your decision (when applicable).\\n- When in doubt, ground your decision on the examples provided by the experts instead of your own intuition.\\n- When no example is similar to the input, make sure to provide a clear justification for your decision while inferring the decision-making process from the examples provided by the experts.\",\"role\":\"system\"},{\"content\":\"Expert Example #1\\n<|start_input|>\\nwhat is up\\n<|end_input|>\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"Analysis: \\\"What's up\\\" in our business scenario is NOT considered an official greeting.\\nFinal Answer: ■FALSE■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example #2\\n<|start_input|>\\nhello! how are you?\\n<|end_input|>\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"Analysis: \\\"hello!\\\" is a common greeting in English.\\nFinal Answer: ■TRUE■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Expert Example #3\\n<|start_input|>\\nwassup\\n<|end_input|>\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"Analysis: \\\"wassup\\\" is a slang term and not considered a formal greeting. It is therefore NOT considered a greeting.\\nFinal Answer: ■FALSE■\\n■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Considering the below input and above examples, is the following condition true or false?\\nExpert Example #4\\n<|start_input|>\\nWhat's up\\n<|end_input|>\\nIn your \\\"Analysis\\\", please refer to the Expert Examples # to justify your decision.\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.check\",\"promptSource\":\"zai:zai.check:zai/check\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"**Analysis:** \nThe task is to decide whether the provided text qualifies as a greeting. The expert examples illustrate the decision criteria:\n\n- **Example #1** (“what is up”) – judged **FALSE** because this informal phrase is not counted as a greeting. \n- **Example #2** (“hello! how are you?”) – judged **TRUE** as it contains a standard greeting (“hello”). \n- **Example #3** (“wassup”) – judged **FALSE** since it is slang and not a formal greeting.\n\nThe input in **Example #4** is “What’s up”. This expression is essentially the same informal phrase as “what is up” and “wassup”, both of which were classified as **FALSE** in the earlier examples. Therefore, following the established pattern, “What’s up” should also be considered **not** a greeting.\n\n**Conclusion:** The condition (that the input is a greeting) is **false** for the given input.\n\n■FALSE■ \n","metadata":{"provider":"cerebras","usage":{"inputTokens":497,"outputTokens":373,"inputCost":0.00017395,"outputCost":0.00027975},"model":"cerebras:gpt-oss-120b","ttft":171,"latency":415,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0004537}}}
1978
+ {"key":"b8f325eb","input":"{\"body\":{\"messages\":[{\"content\":\"Rewrite the text between the ■START■ and ■END■ tags to match the user prompt.\",\"role\":\"system\"},{\"content\":\"Prompt: write it like we want it\\n\\n■START■\\nMicrosoft is a big company\\n■END■\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■START■# MICROSOFT IS A BIG COMPANY■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Prompt: write it like we want it\\n\\n■START■\\nGoogle is an evil company\\n■END■\",\"role\":\"user\",\"type\":\"text\"},{\"content\":\"■START■# GOOGLE IS AN EVIL COMPANY■END■\",\"role\":\"assistant\",\"type\":\"text\"},{\"content\":\"Prompt: write it like we want it\\n\\n■START■\\nBotpress is awesome\\n■END■\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.rewrite\",\"promptSource\":\"zai:zai.rewrite:zai/rewrite\"},\"model\":\"fast\",\"signal\":{},\"stopSequences\":[\"■END■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■START■# BOTPRESS IS AWESOME","metadata":{"provider":"cerebras","usage":{"inputTokens":205,"outputTokens":171,"inputCost":0.00007175,"outputCost":0.00012825},"model":"cerebras:gpt-oss-120b","ttft":174,"latency":953,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0002}}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@botpress/zai",
3
3
  "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API",
4
- "version": "2.5.18",
4
+ "version": "2.6.0",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
package/src/context.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Cognitive, Model, GenerateContentInput, GenerateContentOutput } from '@botpress/cognitive'
2
2
  import { Adapter } from './adapters/adapter'
3
3
  import { EventEmitter } from './emitter'
4
+ import { fastHash } from './utils'
5
+ import type { Memoizer } from './zai'
4
6
 
5
7
  type Meta = Awaited<ReturnType<Cognitive['generateContent']>>['meta']
6
8
 
@@ -16,6 +18,7 @@ export type ZaiContextProps = {
16
18
  modelId: string
17
19
  adapter?: Adapter
18
20
  source?: GenerateContentInput['meta']
21
+ memoizer?: Memoizer
19
22
  }
20
23
 
21
24
  /**
@@ -94,10 +97,13 @@ export class ZaiContext {
94
97
  public source?: GenerateContentInput['meta']
95
98
 
96
99
  private _eventEmitter: EventEmitter<ContextEvents>
100
+ private _memoizer: Memoizer
97
101
 
98
102
  public controller: AbortController = new AbortController()
99
103
  private _client: Cognitive
100
104
 
105
+ private static _noopMemoizer: Memoizer = { run: (_id, fn) => fn() }
106
+
101
107
  public constructor(props: ZaiContextProps) {
102
108
  this._client = props.client.clone()
103
109
  this.taskId = props.taskId
@@ -105,6 +111,7 @@ export class ZaiContext {
105
111
  this.adapter = props.adapter
106
112
  this.source = props.source
107
113
  this.taskType = props.taskType
114
+ this._memoizer = props.memoizer ?? ZaiContext._noopMemoizer
108
115
  this._eventEmitter = new EventEmitter<ContextEvents>()
109
116
 
110
117
  this._client.on('request', () => {
@@ -148,6 +155,20 @@ export class ZaiContext {
148
155
 
149
156
  public async generateContent<Out = string>(
150
157
  props: GenerateContentProps<Out>
158
+ ): Promise<{ meta: Meta; output: GenerateContentOutput; text: string | undefined; extracted: Out }> {
159
+ const memoKey = `zai:memo:${this.taskType}:${this.taskId || 'default'}:${fastHash(
160
+ JSON.stringify({
161
+ s: props.systemPrompt,
162
+ m: props.messages?.map((m) => ('content' in m ? m.content : '')),
163
+ st: props.stopSequences,
164
+ })
165
+ )}`
166
+
167
+ return this._memoizer.run(memoKey, () => this._generateContentInner(props))
168
+ }
169
+
170
+ private async _generateContentInner<Out = string>(
171
+ props: GenerateContentProps<Out>
151
172
  ): Promise<{ meta: Meta; output: GenerateContentOutput; text: string | undefined; extracted: Out }> {
152
173
  const maxRetries = Math.max(props.maxRetries ?? 3, 0)
153
174
  const transform = props.transform
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Zai } from './zai'
1
+ import { Zai, type Memoizer } from './zai'
2
2
 
3
3
  import './operations/text'
4
4
  import './operations/rewrite'
@@ -14,3 +14,4 @@ import './operations/answer'
14
14
  import './operations/patch'
15
15
 
16
16
  export { Zai }
17
+ export type { Memoizer }
@@ -816,6 +816,7 @@ Zai.prototype.answer = function <T>(
816
816
  taskId: this.taskId,
817
817
  taskType: 'zai.answer',
818
818
  adapter: this.adapter,
819
+ memoizer: this._resolveMemoizer(),
819
820
  })
820
821
 
821
822
  return new Response<AnswerResult<T>, AnswerResult<T>>(
@@ -354,6 +354,7 @@ Zai.prototype.check = function (
354
354
  taskId: this.taskId,
355
355
  taskType: 'zai.check',
356
356
  adapter: this.adapter,
357
+ memoizer: this._resolveMemoizer(),
357
358
  })
358
359
 
359
360
  return new Response<
@@ -484,6 +484,7 @@ Zai.prototype.extract = function <S extends OfType<AnyObjectOrArray>>(
484
484
  taskId: this.taskId,
485
485
  taskType: 'zai.extract',
486
486
  adapter: this.adapter,
487
+ memoizer: this._resolveMemoizer(),
487
488
  })
488
489
 
489
490
  return new Response<S['_output']>(context, extract(input, schema, _options, context), (result) => result)
@@ -363,6 +363,7 @@ Zai.prototype.filter = function <T>(
363
363
  taskId: this.taskId,
364
364
  taskType: 'zai.filter',
365
365
  adapter: this.adapter,
366
+ memoizer: this._resolveMemoizer(),
366
367
  })
367
368
 
368
369
  return new Response<Array<T>>(context, filter(input, condition, _options, context), (result) => result)
@@ -955,6 +955,7 @@ Zai.prototype.group = function <T>(
955
955
  taskId: this.taskId,
956
956
  taskType: 'zai.group',
957
957
  adapter: this.adapter,
958
+ memoizer: this._resolveMemoizer(),
958
959
  })
959
960
 
960
961
  return new Response<Array<Group<T>>, Record<string, T[]>>(context, group(input, _options, context), (result) => {
@@ -542,6 +542,7 @@ Zai.prototype.label = function <T extends string>(
542
542
  taskId: this.taskId,
543
543
  taskType: 'zai.label',
544
544
  adapter: this.adapter,
545
+ memoizer: this._resolveMemoizer(),
545
546
  })
546
547
 
547
548
  return new Response<
@@ -650,6 +650,7 @@ Zai.prototype.patch = function (
650
650
  taskId: this.taskId,
651
651
  taskType: 'zai.patch',
652
652
  adapter: this.adapter,
653
+ memoizer: this._resolveMemoizer(),
653
654
  })
654
655
 
655
656
  return new Response<Array<File>>(context, patch(files, instructions, _options, context), (result) => result)
@@ -611,6 +611,7 @@ Zai.prototype.rate = function <T, I extends RatingInstructions>(
611
611
  taskId: this.taskId,
612
612
  taskType: 'zai.rate',
613
613
  adapter: this.adapter,
614
+ memoizer: this._resolveMemoizer(),
614
615
  })
615
616
 
616
617
  return new Response<Array<RatingResult<I>>, Array<SimplifiedRatingResult<I>>>(
@@ -277,6 +277,7 @@ Zai.prototype.rewrite = function (this: Zai, original: string, prompt: string, _
277
277
  taskId: this.taskId,
278
278
  taskType: 'zai.rewrite',
279
279
  adapter: this.adapter,
280
+ memoizer: this._resolveMemoizer(),
280
281
  })
281
282
 
282
283
  return new Response<string>(context, rewrite(original, prompt, _options, context), (result) => result)
@@ -800,6 +800,7 @@ Zai.prototype.sort = function <T>(
800
800
  taskId: this.taskId,
801
801
  taskType: 'zai.sort',
802
802
  adapter: this.adapter,
803
+ memoizer: this._resolveMemoizer(),
803
804
  })
804
805
 
805
806
  return new Response<Array<T>, Array<T>>(
@@ -306,6 +306,7 @@ Zai.prototype.summarize = function (this: Zai, original, _options): Response<str
306
306
  taskId: this.taskId,
307
307
  taskType: 'summarize',
308
308
  adapter: this.adapter,
309
+ memoizer: this._resolveMemoizer(),
309
310
  })
310
311
 
311
312
  return new Response<string, string>(context, summarize(original, options, context), (value) => value)
@@ -135,6 +135,7 @@ Zai.prototype.text = function (this: Zai, prompt: string, _options?: Options): R
135
135
  taskId: this.taskId,
136
136
  taskType: 'zai.text',
137
137
  adapter: this.adapter,
138
+ memoizer: this._resolveMemoizer(),
138
139
  })
139
140
 
140
141
  return new Response<string>(context, text(prompt, _options, context), (result) => result)
package/src/zai.ts CHANGED
@@ -8,6 +8,17 @@ import { Adapter } from './adapters/adapter'
8
8
  import { TableAdapter } from './adapters/botpress-table'
9
9
  import { MemoryAdapter } from './adapters/memory'
10
10
 
11
+ /**
12
+ * A memoizer that caches the result of async operations by a unique key.
13
+ *
14
+ * When used with the Botpress ADK workflow `step` function, this enables
15
+ * Zai operations to resume where they left off if a workflow is interrupted.
16
+ *
17
+ */
18
+ export type Memoizer = {
19
+ run: <T>(id: string, fn: () => Promise<T>) => Promise<T>
20
+ }
21
+
11
22
  /**
12
23
  * Active learning configuration for improving AI operations over time.
13
24
  *
@@ -86,6 +97,16 @@ type ZaiConfig = {
86
97
  activeLearning?: ActiveLearning
87
98
  /** Namespace for organizing tasks (default: 'zai') */
88
99
  namespace?: string
100
+ /**
101
+ * Memoizer (or factory returning one) for caching cognitive call results.
102
+ *
103
+ * When provided, all LLM calls are wrapped in the memoizer, allowing results
104
+ * to be cached and replayed. This is useful for resuming workflow runs where
105
+ * Zai operations have already completed their cognitive calls.
106
+ *
107
+ * If a factory function is provided, it is called once per Zai operation invocation.
108
+ */
109
+ memoize?: Memoizer | (() => Memoizer)
89
110
  }
90
111
 
91
112
  const _ZaiConfig = z.object({
@@ -195,6 +216,7 @@ export class Zai {
195
216
  protected namespace: string
196
217
  protected adapter: Adapter
197
218
  protected activeLearning: ActiveLearning
219
+ protected _memoize?: Memoizer | (() => Memoizer)
198
220
 
199
221
  /**
200
222
  * Creates a new Zai instance with the specified configuration.
@@ -236,6 +258,8 @@ export class Zai {
236
258
  tableName: parsed.activeLearning.tableName,
237
259
  })
238
260
  : new MemoryAdapter([])
261
+
262
+ this._memoize = config.memoize
239
263
  }
240
264
 
241
265
  /** @internal */
@@ -250,6 +274,14 @@ export class Zai {
250
274
  })
251
275
  }
252
276
 
277
+ /** @internal */
278
+ protected _resolveMemoizer(): Memoizer | undefined {
279
+ if (!this._memoize) {
280
+ return undefined
281
+ }
282
+ return typeof this._memoize === 'function' ? this._memoize() : this._memoize
283
+ }
284
+
253
285
  protected async getTokenizer() {
254
286
  Zai.tokenizer ??= await (async () => {
255
287
  while (!getWasmTokenizer) {