saico 2.3.0 → 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.
Files changed (7) hide show
  1. package/README.md +287 -300
  2. package/index.js +1 -4
  3. package/itask.js +16 -3
  4. package/msgs.js +35 -81
  5. package/package.json +1 -2
  6. package/saico.js +307 -35
  7. package/sid.js +0 -248
package/index.js CHANGED
@@ -12,15 +12,14 @@
12
12
  * - Storage persistence (Redis cache + optional DB backend)
13
13
  *
14
14
  * Main Components:
15
+ * - Saico: Master class (external users extend this)
15
16
  * - Itask: Base task class for all tasks (supports states, cancellation, promises)
16
17
  * - Context: Conversation context with message handling and tool calls
17
- * - Sid: Session root task (extends Itask, always has a context)
18
18
  * - Store: Storage abstraction layer (Redis + optional backends like DynamoDB)
19
19
  */
20
20
 
21
21
  const Itask = require('./itask.js');
22
22
  const { Context, createContext } = require('./msgs.js');
23
- const { Sid, createSid } = require('./sid.js');
24
23
  const { Store, DynamoBackend } = require('./store.js');
25
24
  const { Saico } = require('./saico.js');
26
25
  const { DynamoDBAdapter } = require('./dynamo.js');
@@ -144,7 +143,6 @@ module.exports = {
144
143
  // Core classes
145
144
  Itask,
146
145
  Context,
147
- Sid,
148
146
  Store,
149
147
  DynamoBackend,
150
148
 
@@ -153,7 +151,6 @@ module.exports = {
153
151
 
154
152
  // Factory functions
155
153
  createTask,
156
- createSid,
157
154
  createContext,
158
155
 
159
156
  // Legacy compatibility
package/itask.js CHANGED
@@ -746,9 +746,22 @@ Itask.prototype.closeContext = async function closeContext(){
746
746
  await this.context.close();
747
747
  };
748
748
 
749
- // Overridable: contextless tasks can provide a state summary that bubbles up
750
- // into the nearest ancestor context's _getStateSummary().
751
- Itask.prototype.getStateSummary = function getStateSummary(){ return ''; };
749
+ // Walk DOWN to find the deepest active descendant with a context
750
+ Itask.prototype.findDeepestContext = function findDeepestContext() {
751
+ let deepest = this.context ? { context: this.context, depth: 0 } : null;
752
+ const search = (task, depth) => {
753
+ for (const child of task.child) {
754
+ if (child._completed) continue;
755
+ if (child.context) {
756
+ if (!deepest || depth + 1 >= deepest.depth)
757
+ deepest = { context: child.context, depth: depth + 1 };
758
+ }
759
+ search(child, depth + 1);
760
+ }
761
+ };
762
+ search(this, 0);
763
+ return deepest ? deepest.context : null;
764
+ };
752
765
 
753
766
  // Reference to Context class (set by index.js to avoid circular dependency)
754
767
  Itask.Context = null;
package/msgs.js CHANGED
@@ -69,39 +69,6 @@ class Context {
69
69
  this.functions = task?.functions;
70
70
  }
71
71
 
72
- // Overridable: extending classes provide current state summary
73
- getStateSummary() { return ''; }
74
-
75
- // Recursively collect state summaries from child tasks that have no context
76
- // (no msg Q), stopping at children that do have one.
77
- _collectChildStateSummaries(task) {
78
- if (!task.child || !task.child.size) return '';
79
- const parts = [];
80
- for (const child of task.child) {
81
- if (child.context) continue; // has its own Q — boundary, stop here
82
- if (typeof child.getStateSummary === 'function') {
83
- const s = child.getStateSummary();
84
- if (s) parts.push(s);
85
- }
86
- const nested = this._collectChildStateSummaries(child);
87
- if (nested) parts.push(nested);
88
- }
89
- return parts.join('\n');
90
- }
91
-
92
- // Internal (not overridable): own getStateSummary() + summaries from all
93
- // contextless descendants, stopping at the first child that has its own Q.
94
- _getStateSummary() {
95
- const parts = [];
96
- const own = this.getStateSummary();
97
- if (own) parts.push(own);
98
- if (this.task) {
99
- const childSummaries = this._collectChildStateSummaries(this.task);
100
- if (childSummaries) parts.push(childSummaries);
101
- }
102
- return parts.join('\n');
103
- }
104
-
105
72
  // Snapshot all public (non-underscore) task properties for dirty detection.
106
73
  // Mirrors the observable proxy convention: _ prefix = internal, ignored.
107
74
  // Does NOT call serialize() — that is for persistence, not dirty detection.
@@ -822,52 +789,31 @@ class Context {
822
789
  return msgs.slice(startIdx);
823
790
  }
824
791
 
825
- // Build message queue — layered structure:
826
- // Layer 1: System prompts from ancestor hierarchy + own prompt (transient)
827
- // Layer 2: [State Summary] from getStateSummary() override (transient, if non-empty)
828
- // Layer 3: [Tool Activity Log] from tool_digest (transient, if non-empty)
829
- // Layer 4: Ancestor summaries only + last QUEUE_LIMIT of own messages (persistent)
830
- _createMsgQ(add_tag, tag_filter) {
831
- const fullQueue = [];
832
- const ancestorContexts = this.getAncestorContexts();
833
-
834
- // Layer 1+2: Each level's prompt followed immediately by its state summary
835
- for (const ctx of ancestorContexts) {
836
- if (ctx.prompt) {
837
- const prompt = {role: 'system', content: ctx.prompt};
838
- if (add_tag) prompt.tag = ctx.tag;
792
+ // Build message queue.
793
+ // When preamble is provided (by Saico orchestrator), it is prepended as-is
794
+ // and does NOT count against QUEUE_LIMIT. Otherwise falls back to standalone
795
+ // behavior: own prompt + tool digest + own messages.
796
+ _createMsgQ(preamble, add_tag, tag_filter) {
797
+ const fullQueue = [...(preamble || [])];
798
+
799
+ // Standalone fallback — when no preamble provided, add own prompt/digest
800
+ if (!preamble) {
801
+ if (this.prompt) {
802
+ const prompt = {role: 'system', content: this.prompt};
803
+ if (add_tag) prompt.tag = this.tag;
839
804
  fullQueue.push(prompt);
840
805
  }
841
- const ctxSummary = ctx._getStateSummary();
842
- if (ctxSummary)
843
- fullQueue.push({role: 'system', content: '[State Summary]\n' + ctxSummary});
844
- }
845
- if (this.prompt) {
846
- const prompt = {role: 'system', content: this.prompt};
847
- if (add_tag) prompt.tag = this.tag;
848
- fullQueue.push(prompt);
849
- }
850
- const stateSummary = this._getStateSummary();
851
- if (stateSummary)
852
- fullQueue.push({role: 'system', content: '[State Summary]\n' + stateSummary});
853
-
854
- // Layer 3: Tool digest (if non-empty)
855
- if (this.tool_digest.length > 0) {
856
- const digestText = this.tool_digest.map(entry =>
857
- `[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
858
- ).join('\n');
859
- fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
860
- }
861
806
 
862
- // Layer 4: Ancestor summaries only (no full ancestor messages)
863
- for (const ctx of ancestorContexts) {
864
- const summaries = ctx._msgs
865
- .filter(m => m.opts.summary)
866
- .map(m => add_tag ? Object.assign({}, m.msg, {tag: ctx.tag}) : m.msg);
867
- fullQueue.push(...summaries);
807
+ if (this.tool_digest.length > 0) {
808
+ const digestText = this.tool_digest.map(entry =>
809
+ `[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
810
+ ).join('\n');
811
+ fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
812
+ }
868
813
  }
869
814
 
870
815
  // Own messages — filter by tag if requested, then slice to QUEUE_LIMIT
816
+ // QUEUE_LIMIT only applies here — preamble is not counted
871
817
  let my_msgs;
872
818
  if (tag_filter !== undefined) {
873
819
  my_msgs = this._msgs.filter(m => {
@@ -948,7 +894,7 @@ class Context {
948
894
  _debugQDump(Q, functions) {
949
895
  if (util.is_mocha && process.env.PROD)
950
896
  return;
951
- const dbgQ = Q || this._createMsgQ(true);
897
+ const dbgQ = Q || this._createMsgQ(null, true);
952
898
  if (debug) {
953
899
  console.log('MSGQDEBUG - Q:', JSON.stringify(dbgQ.map(m => ({
954
900
  role: m.role,
@@ -973,14 +919,22 @@ class Context {
973
919
  this._processWaitingQueue();
974
920
  }
975
921
 
976
- Q = this._createMsgQ(false, o.opts?.tag);
922
+ Q = this._createMsgQ(o.opts?._preamble, false, o.opts?.tag);
977
923
 
978
- // Aggregate functions from hierarchy and merge with message-specific functions
979
- const hierarchyFuncs = this.getFunctions() || [];
980
- const messageFuncs = o.functions || [];
981
- const funcs = [...hierarchyFuncs, ...messageFuncs].length > 0
982
- ? [...hierarchyFuncs, ...messageFuncs]
983
- : null;
924
+ // Use aggregated functions from Saico if provided, else fall back to own
925
+ let funcs;
926
+ if (o.opts?._aggregatedFunctions) {
927
+ const messageFuncs = o.functions || [];
928
+ funcs = [...o.opts._aggregatedFunctions, ...messageFuncs].length > 0
929
+ ? [...o.opts._aggregatedFunctions, ...messageFuncs]
930
+ : null;
931
+ } else {
932
+ const hierarchyFuncs = this.getFunctions() || [];
933
+ const messageFuncs = o.functions || [];
934
+ funcs = [...hierarchyFuncs, ...messageFuncs].length > 0
935
+ ? [...hierarchyFuncs, ...messageFuncs]
936
+ : null;
937
+ }
984
938
 
985
939
  if (debug)
986
940
  this._debugQDump(Q, funcs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.3.0",
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",
@@ -17,7 +17,6 @@
17
17
  "itask.js",
18
18
  "context.js",
19
19
  "msgs.js",
20
- "sid.js",
21
20
  "saico.js",
22
21
  "dynamo.js",
23
22
  "openai.js",