saico 2.2.3 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.2.3",
3
+ "version": "2.4.0",
4
4
  "main": "index.js",
5
5
  "type": "commonjs",
6
6
  "description": "Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts",
@@ -16,7 +16,9 @@
16
16
  "index.js",
17
17
  "itask.js",
18
18
  "context.js",
19
- "sid.js",
19
+ "msgs.js",
20
+ "saico.js",
21
+ "dynamo.js",
20
22
  "openai.js",
21
23
  "util.js",
22
24
  "redis.js",
@@ -32,6 +34,16 @@
32
34
  "tiktoken": "^1.0.17",
33
35
  "redis": "^4.7.0"
34
36
  },
37
+ "peerDependencies": {
38
+ "@aws-sdk/client-dynamodb": "^3.0.0",
39
+ "@aws-sdk/lib-dynamodb": "^3.0.0",
40
+ "@aws-sdk/util-dynamodb": "^3.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@aws-sdk/client-dynamodb": { "optional": true },
44
+ "@aws-sdk/lib-dynamodb": { "optional": true },
45
+ "@aws-sdk/util-dynamodb": { "optional": true }
46
+ },
35
47
  "devDependencies": {
36
48
  "chai": "^4.5.0",
37
49
  "chai-http": "^4.4.0",
package/saico.js ADDED
@@ -0,0 +1,617 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const Itask = require('./itask.js');
5
+ const { Context } = require('./msgs.js');
6
+ const { Store } = require('./store.js');
7
+
8
+ /**
9
+ * Saico — Master class for building AI-powered services.
10
+ *
11
+ * External users extend this class instead of Itask. It separates object
12
+ * lifecycle from task activation:
13
+ *
14
+ * - Construction: sets up storage (Redis observable + optional DynamoDB),
15
+ * class-level prompt, tool config. No Itask is created yet.
16
+ * - activate(opts): creates the internal Itask and optionally attaches a
17
+ * message Q context (when opts.createQ is true).
18
+ * - DB access works before and after activation.
19
+ *
20
+ * Saico orchestrates the full message payload sent to the LLM by walking its
21
+ * parent chain to aggregate prompts, tools, digests, and state summaries.
22
+ *
23
+ * `new Saico(opt)` returns a Redis observable proxy of the instance when
24
+ * Redis is available, enabling automatic persistence of public properties.
25
+ */
26
+ class Saico {
27
+ /**
28
+ * @param {Object} opt
29
+ * @param {string} [opt.id] - Instance ID (auto-generated if omitted)
30
+ * @param {string} [opt.name] - Instance name (defaults to class name)
31
+ * @param {string} [opt.prompt] - Class-level system prompt
32
+ * @param {Function} [opt.tool_handler] - Tool handler function
33
+ * @param {Array} [opt.functions] - Available AI functions
34
+ * @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
35
+ * @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
36
+ * @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
37
+ * @param {string} [opt.dynamodb_table] - DynamoDB table name (enables db accessor)
38
+ * @param {string} [opt.dynamodb_region] - AWS region for DynamoDB
39
+ * @param {Object} [opt.dynamodb_client] - Injectable DynamoDB client (for testing)
40
+ * @param {Object} [opt.db] - Pluggable DB backend
41
+ * @param {Object} [opt.store] - Store instance override
42
+ * @param {Object} [opt.userData] - Initial user data
43
+ * @param {Object} [opt.sessionConfig] - Session config overrides
44
+ */
45
+ constructor(opt = {}) {
46
+ // Internal properties (underscore-prefixed, not persisted to Redis)
47
+ this._id = opt.id || crypto.randomBytes(8).toString('hex');
48
+ this._task = null;
49
+ this._store = opt.store || Store.instance || null;
50
+ this._opt = opt;
51
+ this._isolate = opt.isolate || false;
52
+
53
+ // Public configuration
54
+ this.name = opt.name || this.constructor.name || 'saico';
55
+ this.prompt = opt.prompt || '';
56
+ this.tool_handler = opt.tool_handler || null;
57
+ this.functions = opt.functions || null;
58
+
59
+ // Absorbed from Sid
60
+ this.userData = opt.userData || {};
61
+ this.tm_create = Date.now();
62
+ this.sessionConfig = {
63
+ token_limit: opt.token_limit,
64
+ max_depth: opt.max_depth,
65
+ max_tool_repetition: opt.max_tool_repetition,
66
+ queue_limit: opt.queue_limit,
67
+ min_chat_messages: opt.min_chat_messages,
68
+ ...opt.sessionConfig,
69
+ };
70
+
71
+ // DB backend — pluggable storage adapter.
72
+ this._db = opt.db || null;
73
+ if (!this._db && opt.dynamodb_table) {
74
+ const { DynamoDBAdapter } = require('./dynamo.js');
75
+ this._db = new DynamoDBAdapter({
76
+ table: opt.dynamodb_table,
77
+ region: opt.dynamodb_region,
78
+ client: opt.dynamodb_client,
79
+ });
80
+ }
81
+
82
+ // Return Redis observable proxy (must be last in constructor).
83
+ // Subclasses calling super() will receive the proxy as `this`.
84
+ try {
85
+ const redis = require('./redis.js');
86
+ if (redis.rclient && opt.redis !== false) {
87
+ const key = 'saico:' + (opt.key || this._id);
88
+ return redis.createObservableForRedis(key, this);
89
+ }
90
+ } catch (e) { /* redis not available */ }
91
+ }
92
+
93
+ /**
94
+ * Create the internal Itask and optionally a message Q context.
95
+ *
96
+ * @param {Object} opts
97
+ * @param {boolean} [opts.createQ] - If true, attach a message Q (Context)
98
+ * @param {string} [opts.prompt] - Additional prompt (appended to class-level)
99
+ * @param {Function} [opts.tool_handler] - Override tool handler
100
+ * @param {Array} [opts.functions] - Override functions
101
+ * @param {Array} [opts.states] - Task state functions
102
+ * @param {Itask} [opts.parent] - Parent task to spawn under
103
+ * @param {string} [opts.taskId] - Custom task ID
104
+ * @param {number} [opts.token_limit] - Token limit for context
105
+ * @param {number} [opts.max_depth] - Max tool call depth
106
+ * @param {number} [opts.max_tool_repetition] - Max tool repetition
107
+ * @param {number} [opts.queue_limit] - Message queue limit
108
+ * @param {number} [opts.min_chat_messages] - Min chat messages in queue
109
+ * @param {boolean} [opts.sequential_mode] - Sequential message processing
110
+ * @param {Array} [opts.msgs] - Initial messages
111
+ * @param {*} [opts.chat_history] - Chat history to restore
112
+ * @param {Object} [opts.contextConfig] - Additional Context config overrides
113
+ * @returns {Saico} this instance (for chaining)
114
+ */
115
+ activate(opts = {}) {
116
+ if (this._task)
117
+ throw new Error('Already activated. Call deactivate() first.');
118
+
119
+ const states = opts.states || [];
120
+
121
+ // Build effective prompt: class-level + activation-level
122
+ const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
123
+
124
+ const taskOpt = {
125
+ name: this.name,
126
+ id: opts.taskId,
127
+ async: true,
128
+ store: this._store,
129
+ tool_handler: opts.tool_handler || this.tool_handler,
130
+ functions: opts.functions || this.functions,
131
+ bind: this, // State functions run with Saico instance as `this`
132
+ };
133
+
134
+ if (opts.parent)
135
+ taskOpt.spawn_parent = opts.parent;
136
+
137
+ this._task = new Itask(taskOpt, states);
138
+
139
+ // Store Saico reference on task for parent chain traversal
140
+ this._task._saico = this;
141
+
142
+ // Create message Q context if requested (only via createQ flag, NOT prompt)
143
+ if (opts.createQ) {
144
+ const contextConfig = {
145
+ tag: opts.tag || this._task.id,
146
+ token_limit: opts.token_limit ?? this.sessionConfig.token_limit,
147
+ max_depth: opts.max_depth ?? this.sessionConfig.max_depth,
148
+ max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
149
+ queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
150
+ min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
151
+ tool_handler: taskOpt.tool_handler,
152
+ functions: taskOpt.functions,
153
+ sequential_mode: opts.sequential_mode,
154
+ msgs: opts.msgs,
155
+ chat_history: opts.chat_history,
156
+ ...opts.contextConfig,
157
+ };
158
+
159
+ const augmentedPrompt = effectivePrompt
160
+ ? effectivePrompt + Itask.BACKEND_EXPLANATION
161
+ : '';
162
+ const context = new Context(augmentedPrompt, this._task, contextConfig);
163
+ this._task.setContext(context);
164
+ }
165
+
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * Deactivate — bubble cleaned messages to parent, close context, cancel task.
171
+ * Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
172
+ * then closes the context without the default summary bubbling.
173
+ */
174
+ async deactivate() {
175
+ if (!this._task) return;
176
+ if (this._task.context) {
177
+ // Find parent context to bubble cleaned messages
178
+ let parentTask = this._task.parent;
179
+ let parentCtx = null;
180
+ while (parentTask) {
181
+ if (parentTask.context) { parentCtx = parentTask.context; break; }
182
+ parentTask = parentTask.parent;
183
+ }
184
+ if (parentCtx) {
185
+ const cleaned = this.getRecentMessages(Infinity);
186
+ for (const msg of cleaned)
187
+ parentCtx.push(msg);
188
+ }
189
+ // Clean tool calls and close context without additional summary bubbling.
190
+ // We already pushed cleaned messages above — closeContext's own
191
+ // summarization would double-bubble.
192
+ if (this._task.context_id && typeof this._task.context.cleanToolCallsByTag === 'function')
193
+ this._task.context.cleanToolCallsByTag(this._task.context_id);
194
+ this._task.context = null;
195
+ }
196
+ this._task._ecancel();
197
+ this._task = null;
198
+ }
199
+
200
+ // ---- Saico parent chain traversal ----
201
+
202
+ /**
203
+ * Walk up the Saico parent chain (stop at isolate boundary or root).
204
+ * Returns array ordered root -> ... -> this.
205
+ */
206
+ _getSaicoAncestors() {
207
+ const chain = [this];
208
+ if (this._isolate) return chain;
209
+ let task = this._task?.parent;
210
+ while (task) {
211
+ if (task._saico) {
212
+ chain.unshift(task._saico);
213
+ if (task._saico._isolate) break;
214
+ }
215
+ task = task.parent;
216
+ }
217
+ return chain; // root -> ... -> this
218
+ }
219
+
220
+ /**
221
+ * Build preamble and aggregated functions by walking the Saico chain.
222
+ * @param {Context} activeCtx - The deepest active context (for state summary logic)
223
+ * @returns {{ preamble: Array, allFunctions: Array }}
224
+ */
225
+ _buildPreamble(activeCtx) {
226
+ const chain = this._getSaicoAncestors();
227
+ const preamble = [];
228
+ const allFunctions = [];
229
+
230
+ for (const saico of chain) {
231
+ // Prompt
232
+ if (saico.prompt)
233
+ preamble.push({ role: 'system', content: saico.prompt });
234
+
235
+ // State summary (can return array)
236
+ const summary = saico._getStateSummary(activeCtx);
237
+ if (Array.isArray(summary)) {
238
+ for (const item of summary) {
239
+ if (typeof item === 'string')
240
+ preamble.push({ role: 'system', content: '[State Summary]\n' + item });
241
+ else
242
+ preamble.push(item); // {role, content} message object
243
+ }
244
+ } else if (summary) {
245
+ preamble.push({ role: 'system', content: '[State Summary]\n' + summary });
246
+ }
247
+
248
+ // Tools digest
249
+ if (saico.context?.tool_digest?.length > 0) {
250
+ const digestText = saico.context.tool_digest.map(entry =>
251
+ `[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
252
+ ).join('\n');
253
+ preamble.push({ role: 'system', content: '[Tool Activity Log]\n' + digestText });
254
+ }
255
+
256
+ // Collect functions
257
+ if (saico.functions) allFunctions.push(...saico.functions);
258
+ }
259
+
260
+ return { preamble, allFunctions };
261
+ }
262
+
263
+ // ---- Message orchestration ----
264
+
265
+ async sendMessage(content, functions, opts) {
266
+ if (!this._task)
267
+ throw new Error('Not activated. Call activate() first.');
268
+
269
+ // Find the active context (own or walk up)
270
+ let ctx = this._task.getContext() || this._task.findContext();
271
+ if (!ctx)
272
+ throw new Error('No context available');
273
+
274
+ // Build preamble by walking Saico chain
275
+ const activeCtx = this._task.findDeepestContext() || ctx;
276
+ const { preamble, allFunctions } = this._buildPreamble(activeCtx);
277
+
278
+ // Merge with call-specific functions
279
+ if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
280
+
281
+ opts = Object.assign({}, opts, {
282
+ tag: this._task.context_id,
283
+ _preamble: preamble,
284
+ _aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
285
+ });
286
+ return ctx.sendMessage('user', '[BACKEND] ' + content, null, opts);
287
+ }
288
+
289
+ async recvChatMessage(content, opts) {
290
+ if (!this._task)
291
+ throw new Error('Not activated. Call activate() first.');
292
+
293
+ // Route DOWN to deepest descendant with a msg Q
294
+ const ctx = this._task.findDeepestContext();
295
+ if (!ctx)
296
+ throw new Error('No context available');
297
+
298
+ // Build preamble by walking Saico chain
299
+ const { preamble, allFunctions } = this._buildPreamble(ctx);
300
+
301
+ opts = Object.assign({}, opts, {
302
+ tag: ctx.tag,
303
+ _preamble: preamble,
304
+ _aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
305
+ });
306
+ return ctx.sendMessage('user', content, null, opts);
307
+ }
308
+
309
+ // ---- Task delegation ----
310
+
311
+ get task() { return this._task; }
312
+ get context() { return this._task?.context || null; }
313
+ get context_id() { return this._task?.context_id || null; }
314
+ get isActive() { return !!this._task && !this._task._completed; }
315
+
316
+ spawnTaskWithContext(opt, states) {
317
+ if (!this._task)
318
+ throw new Error('Not activated. Call activate() first.');
319
+ if (typeof opt === 'string')
320
+ opt = { name: opt };
321
+
322
+ const childTask = new Itask({
323
+ ...opt,
324
+ spawn_parent: this._task,
325
+ store: this._store,
326
+ async: true,
327
+ }, states || []);
328
+
329
+ if (opt.prompt) {
330
+ const childContext = new Context(opt.prompt, childTask, {
331
+ tag: opt.tag || childTask.id,
332
+ token_limit: opt.token_limit ?? this.sessionConfig.token_limit,
333
+ max_depth: opt.max_depth ?? this.sessionConfig.max_depth,
334
+ max_tool_repetition: opt.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
335
+ queue_limit: opt.queue_limit ?? this.sessionConfig.queue_limit,
336
+ min_chat_messages: opt.min_chat_messages ?? this.sessionConfig.min_chat_messages,
337
+ tool_handler: opt.tool_handler || this.tool_handler,
338
+ functions: opt.functions || this.functions,
339
+ });
340
+ childTask.setContext(childContext);
341
+ }
342
+
343
+ process.nextTick(() => {
344
+ try { childTask._run(); } catch (e) { console.error(e); }
345
+ });
346
+
347
+ return childTask;
348
+ }
349
+
350
+ spawnTask(opt, states) {
351
+ if (!this._task)
352
+ throw new Error('Not activated. Call activate() first.');
353
+ if (typeof opt === 'string')
354
+ opt = { name: opt };
355
+
356
+ const childTask = new Itask({
357
+ ...opt,
358
+ spawn_parent: this._task,
359
+ store: this._store,
360
+ async: true,
361
+ }, states || []);
362
+
363
+ process.nextTick(() => {
364
+ try { childTask._run(); } catch (e) { console.error(e); }
365
+ });
366
+
367
+ return childTask;
368
+ }
369
+
370
+ // ---- State Summary ----
371
+
372
+ /**
373
+ * Override in subclasses to provide a state summary.
374
+ * @returns {string}
375
+ */
376
+ getStateSummary() { return ''; }
377
+
378
+ /**
379
+ * Get recent user/assistant messages (filtering out tool calls and BACKEND msgs).
380
+ * @param {number} n - Max number of messages to return
381
+ * @returns {Array<{role: string, content: string}>}
382
+ */
383
+ getRecentMessages(n = 5) {
384
+ if (!this.context) return [];
385
+ return this.context._msgs
386
+ .filter(m => {
387
+ if (m.msg.role === 'tool' || m.msg.tool_calls) return false;
388
+ if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
389
+ return m.msg.role === 'user' || m.msg.role === 'assistant';
390
+ })
391
+ .slice(-n)
392
+ .map(m => ({ role: m.msg.role, content: m.msg.content }));
393
+ }
394
+
395
+ /**
396
+ * Internal state summary builder. Includes own getStateSummary() and,
397
+ * if this context is NOT the active (deepest) Q, includes recent messages.
398
+ * @param {Context} activeCtx - The deepest active context
399
+ * @returns {Array|string|null}
400
+ */
401
+ _getStateSummary(activeCtx) {
402
+ const parts = [];
403
+ const own = this.getStateSummary();
404
+ if (own) parts.push(own);
405
+
406
+ // If this context is NOT the active (deepest) Q, include recent messages
407
+ if (this.context && activeCtx && this.context !== activeCtx) {
408
+ const recent = this.getRecentMessages(5);
409
+ if (recent.length > 0) parts.push(...recent);
410
+ }
411
+
412
+ return parts.length > 0 ? parts : null;
413
+ }
414
+
415
+ // ---- User Data (absorbed from Sid) ----
416
+
417
+ setUserData(key, value) {
418
+ this.userData[key] = value;
419
+ return this;
420
+ }
421
+
422
+ getUserData(key) {
423
+ return key ? this.userData[key] : this.userData;
424
+ }
425
+
426
+ clearUserData() {
427
+ this.userData = {};
428
+ return this;
429
+ }
430
+
431
+ // ---- Session Info ----
432
+
433
+ getSessionInfo() {
434
+ return {
435
+ id: this._id,
436
+ name: this.name,
437
+ running: this._task?.running || false,
438
+ completed: this._task?._completed || false,
439
+ messageCount: this.context?.length || 0,
440
+ childCount: this._task?.child?.size || 0,
441
+ userData: this.userData,
442
+ uptime: Date.now() - this.tm_create,
443
+ };
444
+ }
445
+
446
+ async closeSession() {
447
+ if (!this._task) return;
448
+ if (this._task.context)
449
+ await this._task.context.close();
450
+ this._task._ecancel();
451
+ }
452
+
453
+ // ---- Generic DB access ----
454
+
455
+ async dbPutItem(item, table) {
456
+ if (!this._db) return;
457
+ return this._db.put(item, table);
458
+ }
459
+
460
+ async dbGetItem(key, value, table) {
461
+ if (!this._db) return;
462
+ const result = await this._db.get(key, value, table);
463
+ return result ? this._deserializeRecord(result) : result;
464
+ }
465
+
466
+ async dbDeleteItem(key, value, table) {
467
+ if (!this._db) return;
468
+ return this._db.delete(key, value, table);
469
+ }
470
+
471
+ async dbQuery(index, key, value, table) {
472
+ if (!this._db) return;
473
+ const results = await this._db.query(index, key, value, table);
474
+ return Array.isArray(results)
475
+ ? results.map(r => this._deserializeRecord(r))
476
+ : results;
477
+ }
478
+
479
+ async dbGetAll(table) {
480
+ if (!this._db) return;
481
+ const results = await this._db.getAll(table);
482
+ return Array.isArray(results)
483
+ ? results.map(r => this._deserializeRecord(r))
484
+ : results;
485
+ }
486
+
487
+ async dbUpdate(key, keyValue, setKey, item, table) {
488
+ if (!this._db) return;
489
+ return this._db.update(key, keyValue, setKey, item, table);
490
+ }
491
+
492
+ async dbUpdatePath(key, keyValue, path, setKey, item, table) {
493
+ if (!this._db) return;
494
+ return this._db.updatePath(key, keyValue, path, setKey, item, table);
495
+ }
496
+
497
+ async dbListAppend(key, keyValue, setKey, item, table) {
498
+ if (!this._db) return;
499
+ return this._db.listAppend(key, keyValue, setKey, item, table);
500
+ }
501
+
502
+ async dbListAppendPath(key, keyValue, path, setKey, item, table) {
503
+ if (!this._db) return;
504
+ return this._db.listAppendPath(key, keyValue, path, setKey, item, table);
505
+ }
506
+
507
+ async dbNextCounterId(counter, table) {
508
+ if (!this._db) return;
509
+ return this._db.nextCounterId(counter, table);
510
+ }
511
+
512
+ async dbGetCounterValue(counter, table) {
513
+ if (!this._db) return;
514
+ return this._db.getCounterValue(counter, table);
515
+ }
516
+
517
+ async dbSetCounterValue(counter, value, table) {
518
+ if (!this._db) return;
519
+ return this._db.setCounterValue(counter, value, table);
520
+ }
521
+
522
+ async dbCountItems(table) {
523
+ if (!this._db) return;
524
+ return this._db.countItems(table);
525
+ }
526
+
527
+ // ---- DB deserialization hook ----
528
+
529
+ /**
530
+ * Override in subclasses to transform raw DB records (e.g. restore class instances).
531
+ * Called by dbGetItem, dbQuery, dbGetAll.
532
+ * @param {Object} raw - Raw record from DB
533
+ * @returns {Object} Transformed record
534
+ */
535
+ _deserializeRecord(raw) { return raw; }
536
+
537
+ // ---- Serialization ----
538
+
539
+ serialize() {
540
+ const data = {
541
+ id: this._id,
542
+ name: this.name,
543
+ prompt: this.prompt,
544
+ userData: this.userData,
545
+ sessionConfig: this.sessionConfig,
546
+ tm_create: this.tm_create,
547
+ isolate: this._isolate,
548
+ };
549
+ if (this._task) {
550
+ data.task = {
551
+ id: this._task.id,
552
+ context_id: this._task.context_id,
553
+ context: this._task.context ? {
554
+ tag: this._task.context.tag,
555
+ msgs: this._task.context._msgs,
556
+ functions: this._task.context.functions,
557
+ chat_history: this._task.context.chat_history,
558
+ tool_digest: this._task.context.tool_digest,
559
+ } : null,
560
+ };
561
+ }
562
+ return JSON.stringify(data);
563
+ }
564
+
565
+ /**
566
+ * Restore a Saico instance from serialized data.
567
+ * @param {string|Object} data - Serialized data (JSON string or object)
568
+ * @param {Object} opt - Options (tool_handler, functions, store, states, etc.)
569
+ * @returns {Saico}
570
+ */
571
+ static deserialize(data, opt = {}) {
572
+ const parsed = typeof data === 'string' ? JSON.parse(data) : data;
573
+
574
+ const instance = new Saico({
575
+ id: parsed.id,
576
+ name: parsed.name,
577
+ prompt: parsed.prompt,
578
+ userData: parsed.userData,
579
+ sessionConfig: parsed.sessionConfig,
580
+ isolate: parsed.isolate,
581
+ tool_handler: opt.tool_handler,
582
+ functions: opt.functions || parsed.task?.context?.functions,
583
+ store: opt.store,
584
+ redis: false, // No Redis proxy during deserialization
585
+ });
586
+
587
+ instance.tm_create = parsed.tm_create || instance.tm_create;
588
+
589
+ // Activate with restored context if task data exists
590
+ if (parsed.task) {
591
+ instance.activate({
592
+ createQ: !!parsed.task.context,
593
+ taskId: parsed.task.id,
594
+ tag: parsed.task.context?.tag,
595
+ chat_history: parsed.task.context?.chat_history,
596
+ tool_handler: opt.tool_handler,
597
+ functions: opt.functions || parsed.task.context?.functions,
598
+ states: opt.states || [],
599
+ ...opt,
600
+ });
601
+
602
+ // Restore messages to context
603
+ if (parsed.task.context?.msgs && instance._task.context) {
604
+ instance._task.context._msgs = parsed.task.context.msgs;
605
+ }
606
+
607
+ // Restore tool_digest
608
+ if (Array.isArray(parsed.task.context?.tool_digest) && instance._task.context) {
609
+ instance._task.context.tool_digest = parsed.task.context.tool_digest;
610
+ }
611
+ }
612
+
613
+ return instance;
614
+ }
615
+ }
616
+
617
+ module.exports = { Saico };