@agentuity/runtime 0.0.107 → 0.0.108

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.
@@ -51,30 +51,37 @@ export class LocalThreadProvider implements ThreadProvider {
51
51
  const threadId = await this.threadIDProvider.getThreadId(this.appState, ctx);
52
52
  validateThreadIdOrThrow(threadId);
53
53
 
54
- // Try to restore state from DB
55
- const row = this.db
56
- .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
57
- .get(threadId);
58
-
59
- // Parse the stored data, handling both old (flat) and new ({ state, metadata }) formats
60
- const { flatStateJson, metadata } = parseThreadData(row?.state);
61
-
62
- // Create thread with restored state and metadata
63
- const thread = new DefaultThread(this, threadId, flatStateJson, metadata);
64
-
65
- // Populate thread state from restored data
66
- if (flatStateJson) {
67
- try {
68
- const data = JSON.parse(flatStateJson);
69
- for (const [key, value] of Object.entries(data)) {
70
- thread.state.set(key, value);
54
+ // Create a restore function for lazy loading
55
+ const restoreFn = async (): Promise<{
56
+ state: Map<string, unknown>;
57
+ metadata: Record<string, unknown>;
58
+ }> => {
59
+ if (!this.db) {
60
+ return { state: new Map(), metadata: {} };
61
+ }
62
+
63
+ const row = this.db
64
+ .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
65
+ .get(threadId);
66
+
67
+ const { flatStateJson, metadata } = parseThreadData(row?.state);
68
+
69
+ const state = new Map<string, unknown>();
70
+ if (flatStateJson) {
71
+ try {
72
+ const data = JSON.parse(flatStateJson);
73
+ for (const [key, value] of Object.entries(data)) {
74
+ state.set(key, value);
75
+ }
76
+ } catch {
77
+ // Continue with empty state if parsing fails
71
78
  }
72
- } catch {
73
- // Continue with empty state if parsing fails
74
79
  }
75
- }
76
80
 
77
- return thread;
81
+ return { state, metadata: metadata || {} };
82
+ };
83
+
84
+ return new DefaultThread(this, threadId, restoreFn);
78
85
  }
79
86
 
80
87
  async save(thread: Thread): Promise<void> {
@@ -82,20 +89,102 @@ export class LocalThreadProvider implements ThreadProvider {
82
89
  return;
83
90
  }
84
91
 
85
- // Only save if state was modified
86
- if (!thread.isDirty()) {
92
+ const saveMode = thread.getSaveMode();
93
+ if (saveMode === 'none') {
87
94
  return;
88
95
  }
89
96
 
90
- const stateJson = thread.getSerializedState();
91
97
  const now = Date.now();
92
98
 
93
- // Upsert thread state
94
- this.db.run(
95
- `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
96
- ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
97
- [thread.id, stateJson, now, stateJson, now]
98
- );
99
+ if (saveMode === 'merge') {
100
+ // For merge, we need to load existing state, apply operations, then save
101
+ const operations = thread.getPendingOperations();
102
+ const metadata = thread.getMetadataForSave();
103
+
104
+ // Load existing state
105
+ const row = this.db
106
+ .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
107
+ .get(thread.id);
108
+
109
+ const { flatStateJson, metadata: existingMetadata } = parseThreadData(row?.state);
110
+
111
+ const state: Record<string, unknown> = {};
112
+ if (flatStateJson) {
113
+ try {
114
+ Object.assign(state, JSON.parse(flatStateJson));
115
+ } catch {
116
+ // Continue with empty state if parsing fails
117
+ }
118
+ }
119
+
120
+ // Apply operations
121
+ for (const op of operations) {
122
+ switch (op.op) {
123
+ case 'clear':
124
+ for (const key of Object.keys(state)) {
125
+ delete state[key];
126
+ }
127
+ break;
128
+ case 'set':
129
+ if (op.key !== undefined) {
130
+ state[op.key] = op.value;
131
+ }
132
+ break;
133
+ case 'delete':
134
+ if (op.key !== undefined) {
135
+ delete state[op.key];
136
+ }
137
+ break;
138
+ case 'push':
139
+ if (op.key !== undefined) {
140
+ const existing = state[op.key];
141
+ let arr: unknown[];
142
+ if (Array.isArray(existing)) {
143
+ existing.push(op.value);
144
+ arr = existing;
145
+ } else if (existing === undefined) {
146
+ arr = [op.value];
147
+ state[op.key] = arr;
148
+ } else {
149
+ // If non-array, silently skip
150
+ continue;
151
+ }
152
+ // Apply maxRecords limit
153
+ if (op.maxRecords !== undefined && arr.length > op.maxRecords) {
154
+ state[op.key] = arr.slice(arr.length - op.maxRecords);
155
+ }
156
+ }
157
+ break;
158
+ }
159
+ }
160
+
161
+ // Build final data
162
+ const finalMetadata = metadata || existingMetadata || {};
163
+ const hasState = Object.keys(state).length > 0;
164
+ const hasMetadata = Object.keys(finalMetadata).length > 0;
165
+
166
+ let stateJson = '';
167
+ if (hasState || hasMetadata) {
168
+ const data: { state?: Record<string, unknown>; metadata?: Record<string, unknown> } = {};
169
+ if (hasState) data.state = state;
170
+ if (hasMetadata) data.metadata = finalMetadata;
171
+ stateJson = JSON.stringify(data);
172
+ }
173
+
174
+ this.db.run(
175
+ `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
176
+ ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
177
+ [thread.id, stateJson, now, stateJson, now]
178
+ );
179
+ } else {
180
+ // Full save
181
+ const stateJson = await thread.getSerializedState();
182
+ this.db.run(
183
+ `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
184
+ ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
185
+ [thread.id, stateJson, now, stateJson, now]
186
+ );
187
+ }
99
188
  }
100
189
 
101
190
  async destroy(thread: Thread): Promise<void> {