lua-cli 3.54.0 → 3.56.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/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # lua-cli v3.54.0
1
+ # lua-cli v3.56.0
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.54.0. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.56.0. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## 📚 Documentation Index
6
6
 
@@ -263,7 +263,7 @@ Each API doc includes:
263
263
 
264
264
  ---
265
265
 
266
- **Version:** 3.54.0
266
+ **Version:** 3.56.0
267
267
  **Last Updated:** October 2026
268
268
 
269
269
  ---
package/docs/api/Data.md CHANGED
@@ -93,7 +93,7 @@ for (const movie of results) {
93
93
  }
94
94
  ```
95
95
 
96
- ### `Data.create(collectionName, data, searchText?)`
96
+ ### `Data.create(collectionName, data, searchTextOrOptions?)`
97
97
 
98
98
  Creates a new entry in a collection.
99
99
 
@@ -101,7 +101,14 @@ Creates a new entry in a collection.
101
101
 
102
102
  - `collectionName` (string) - Collection name
103
103
  - `data` (object) - Data to store
104
- - `searchText` (string, optional) - Text to use for vector search indexing
104
+ - `searchTextOrOptions` (string or `{ searchText?, index? }`, optional) - A bare string is the text to use for
105
+ vector search indexing. The options object adds `index`: the `data` fields you filter on, as a path
106
+ (`'business_id'`) or a compound of up to 2 paths (`['country', 'business_id']`); at most 3 per call and 5 built
107
+ per agent (per shared collection on `Data.shared`). The platform builds each index within minutes, reports it in
108
+ `collections()`, and drops it 14 days after the last declaration or filtered read. The same in `lua test`,
109
+ deployed agents (skills, jobs, webhooks, workflow steps other than `tier: 'job'`, which have no `Data`) and voice
110
+ hooks. `Data.update` (4th argument),
111
+ `Data.patch` (`mutation.index`), `entry.update` and `entry.save` accept it too.
105
112
 
106
113
  **Returns:** `Promise<DataEntryInstance>`
107
114
 
@@ -309,6 +316,14 @@ const names = await map.collections();
309
316
  can only ever reach its own organization's shared collections.
310
317
  - **Audit.** Every write records the agent, the collection and the entry id (never the value) in the
311
318
  organization's audit log.
319
+ - **Managed indexes.** A writer declares them like on `Data`, on its writes:
320
+ `map.create('api_endpoints', row, { index: [['repoKey', 'service']] })` (also `update`, `patch`, `entry.update`).
321
+ The index belongs to the shared collection, not to the agent that declared it: at most 5 are built across all its
322
+ sub-collections (`'per-shared-collection cap (5) reached'` beyond that), they serve every reader's filtered `get`,
323
+ and they are dropped 14 days after the last declaration or filtered read by any agent. `collections()` reports
324
+ each one's status (`pending`, `building`, `ready`, `failed`, `rejected`) to readers and writers. Readers cannot
325
+ declare (they cannot write). Declare compound indexes so their first field serves the single-field filters too:
326
+ `[repoKey, service]` already serves a filter on `repoKey` alone in the same sub-collection.
312
327
  - **Test sessions** may read a shared collection but not write it (`shared_data_test_session_write_refused`, 409).
313
328
  - **Local runs** (`lua test`) have no org ACL to consult: every call rejects with
314
329
  `code === 'shared_data_unavailable_offline'`.
@@ -370,7 +385,7 @@ A change applies to the next `Data.shared` call. Re-creating a deleted name star
370
385
 
371
386
  Data entries have convenient methods:
372
387
 
373
- ### `entry.update(data, searchText?)`
388
+ ### `entry.update(data, searchTextOrOptions?)`
374
389
 
375
390
  Updates this entry.
376
391
 
@@ -401,7 +416,7 @@ if (oldMovie) {
401
416
  }
402
417
  ```
403
418
 
404
- ### `entry.save(searchText?)`
419
+ ### `entry.save(searchTextOrOptions?)`
405
420
 
406
421
  Saves modifications to this entry.
407
422
 
@@ -11,6 +11,7 @@ import { PreProcessor } from 'lua-cli';
11
11
  ## Overview
12
12
 
13
13
  PreProcessors enable:
14
+
14
15
  - Filter and block inappropriate content
15
16
  - Modify messages before agent processing
16
17
  - Add context and enrichment to messages
@@ -60,11 +61,7 @@ interface PreProcessorConfig {
60
61
  /**
61
62
  * Function that processes messages before sending to agent.
62
63
  */
63
- execute: (
64
- user: UserDataInstance,
65
- messages: ChatMessage[],
66
- channel: string
67
- ) => Promise<PreProcessorResult>;
64
+ execute: (user: UserDataInstance, messages: ChatMessage[], channel: string) => Promise<PreProcessorResult>;
68
65
  }
69
66
  ```
70
67
 
@@ -90,10 +87,22 @@ Continue to the next preprocessor or agent:
90
87
  {
91
88
  action: 'proceed',
92
89
  modifiedMessage?: ChatMessage[], // Optional modified messages
93
- metadata?: Record<string, any> // Optional metadata
90
+ metadata?: { turnContext?: string; [key: string]: any } // Optional metadata
94
91
  }
95
92
  ```
96
93
 
94
+ #### Turn context
95
+
96
+ Set `metadata.turnContext` to give the model a short note for this turn only, such as state read from your own data:
97
+
98
+ ```typescript
99
+ return { action: 'proceed', metadata: { turnContext: 'Checklist in progress: section 12 of 48.' } };
100
+ ```
101
+
102
+ The platform adds it after the conversation, labelled as system-provided context the user did not write. It is never stored, never in chat history and never shown to the user. Notes from several preprocessors are joined in run order and capped at 2,000 characters in total; anything that is not a string is ignored.
103
+
104
+ The model is told the note is data, not instructions, because it may quote text users wrote. Do not use it to steer the agent's behaviour; put rules in the persona or a skill.
105
+
97
106
  ## Message Types
98
107
 
99
108
  ```typescript
@@ -113,7 +122,7 @@ import { PreProcessor } from 'lua-cli';
113
122
  export const contentFilter = new PreProcessor({
114
123
  name: 'content-filter',
115
124
  description: 'Filters inappropriate content',
116
- priority: 10, // Run early
125
+ priority: 10, // Run early
117
126
 
118
127
  execute: async (user, messages, channel) => {
119
128
  const blockedWords = ['spam', 'scam', 'phishing'];
@@ -130,8 +139,8 @@ export const contentFilter = new PreProcessor({
130
139
  response: 'Your message contains inappropriate content and cannot be processed.',
131
140
  metadata: {
132
141
  reason: 'blocked_word',
133
- word
134
- }
142
+ word,
143
+ },
135
144
  };
136
145
  }
137
146
  }
@@ -139,7 +148,7 @@ export const contentFilter = new PreProcessor({
139
148
  }
140
149
 
141
150
  return { action: 'proceed' };
142
- }
151
+ },
143
152
  });
144
153
  ```
145
154
 
@@ -151,7 +160,7 @@ import { PreProcessor } from 'lua-cli';
151
160
  export const userVerification = new PreProcessor({
152
161
  name: 'user-verification',
153
162
  description: 'Verifies user has completed onboarding',
154
- priority: 5, // Run very early
163
+ priority: 5, // Run very early
155
164
 
156
165
  execute: async (user, messages, channel) => {
157
166
  const userData = await user.get();
@@ -162,8 +171,8 @@ export const userVerification = new PreProcessor({
162
171
  action: 'block',
163
172
  response: 'Please complete your profile setup before using the assistant. Type "/start" to begin.',
164
173
  metadata: {
165
- reason: 'onboarding_incomplete'
166
- }
174
+ reason: 'onboarding_incomplete',
175
+ },
167
176
  };
168
177
  }
169
178
 
@@ -173,13 +182,13 @@ export const userVerification = new PreProcessor({
173
182
  action: 'block',
174
183
  response: 'Your account has been suspended. Please contact support.',
175
184
  metadata: {
176
- reason: 'account_suspended'
177
- }
185
+ reason: 'account_suspended',
186
+ },
178
187
  };
179
188
  }
180
189
 
181
190
  return { action: 'proceed' };
182
- }
191
+ },
183
192
  });
184
193
  ```
185
194
 
@@ -199,7 +208,7 @@ export const messageEnrichment = new PreProcessor({
199
208
  // Add context message with user info
200
209
  const contextMessage = {
201
210
  type: 'text' as const,
202
- text: `[User Context: Name: ${userData.data?.name || 'Unknown'}, Plan: ${userData.data?.plan || 'free'}, Location: ${userData.data?.location || 'Unknown'}]`
211
+ text: `[User Context: Name: ${userData.data?.name || 'Unknown'}, Plan: ${userData.data?.plan || 'free'}, Location: ${userData.data?.location || 'Unknown'}]`,
203
212
  };
204
213
 
205
214
  // Prepend context to messages
@@ -210,10 +219,10 @@ export const messageEnrichment = new PreProcessor({
210
219
  modifiedMessage: enrichedMessages,
211
220
  metadata: {
212
221
  enriched: true,
213
- userPlan: userData.data?.plan
214
- }
222
+ userPlan: userData.data?.plan,
223
+ },
215
224
  };
216
- }
225
+ },
217
226
  });
218
227
  ```
219
228
 
@@ -230,27 +239,26 @@ export const rateLimiter = new PreProcessor({
230
239
  execute: async (user, messages, channel) => {
231
240
  const userId = user.id;
232
241
  const now = Date.now();
233
- const windowMs = 60 * 1000; // 1 minute
242
+ const windowMs = 60 * 1000; // 1 minute
234
243
  const maxMessages = 10;
235
244
 
236
245
  // Get user's recent messages
237
246
  const rateLimitKey = `rate_limit_${userId}`;
238
247
  const records = await Data.get('rate_limits');
239
- let userRecord = records.find(r => r.data.key === rateLimitKey);
248
+ let userRecord = records.find((r) => r.data.key === rateLimitKey);
240
249
 
241
250
  if (!userRecord) {
242
251
  // Create new record
243
252
  await Data.create('rate_limits', {
244
253
  key: rateLimitKey,
245
254
  messages: [now],
246
- windowStart: now
255
+ windowStart: now,
247
256
  });
248
257
  return { action: 'proceed' };
249
258
  }
250
259
 
251
260
  // Filter messages within window
252
- const recentMessages = userRecord.data.messages
253
- .filter((timestamp: number) => now - timestamp < windowMs);
261
+ const recentMessages = userRecord.data.messages.filter((timestamp: number) => now - timestamp < windowMs);
254
262
 
255
263
  // Check if limit exceeded
256
264
  if (recentMessages.length >= maxMessages) {
@@ -259,19 +267,19 @@ export const rateLimiter = new PreProcessor({
259
267
  response: 'You are sending messages too quickly. Please wait a moment and try again.',
260
268
  metadata: {
261
269
  reason: 'rate_limit_exceeded',
262
- retryAfter: windowMs - (now - recentMessages[0])
263
- }
270
+ retryAfter: windowMs - (now - recentMessages[0]),
271
+ },
264
272
  };
265
273
  }
266
274
 
267
275
  // Update record
268
276
  await Data.update('rate_limits', userRecord.id, {
269
277
  ...userRecord.data,
270
- messages: [...recentMessages, now]
278
+ messages: [...recentMessages, now],
271
279
  });
272
280
 
273
281
  return { action: 'proceed' };
274
- }
282
+ },
275
283
  });
276
284
  ```
277
285
 
@@ -284,17 +292,17 @@ export const spamDetector = new PreProcessor({
284
292
  name: 'spam-detector',
285
293
  description: 'AI-powered spam detection',
286
294
  priority: 20,
287
- async: true, // Can run in background
295
+ async: true, // Can run in background
288
296
 
289
297
  execute: async (user, messages, channel) => {
290
298
  // Only check text messages
291
- const textMessages = messages.filter(m => m.type === 'text');
299
+ const textMessages = messages.filter((m) => m.type === 'text');
292
300
 
293
301
  if (textMessages.length === 0) {
294
302
  return { action: 'proceed' };
295
303
  }
296
304
 
297
- const messageText = textMessages.map(m => m.text).join(' ');
305
+ const messageText = textMessages.map((m) => m.text).join(' ');
298
306
 
299
307
  // Use AI to detect spam
300
308
  const analysis = await AI.generate(
@@ -312,8 +320,8 @@ export const spamDetector = new PreProcessor({
312
320
  metadata: {
313
321
  reason: 'spam_detected',
314
322
  confidence: result.confidence,
315
- analysis: result.reason
316
- }
323
+ analysis: result.reason,
324
+ },
317
325
  };
318
326
  }
319
327
  } catch (error) {
@@ -322,7 +330,7 @@ export const spamDetector = new PreProcessor({
322
330
  }
323
331
 
324
332
  return { action: 'proceed' };
325
- }
333
+ },
326
334
  });
327
335
  ```
328
336
 
@@ -349,7 +357,7 @@ export const businessHoursChecker = new PreProcessor({
349
357
  // Add notice to message
350
358
  const noticeMessage = {
351
359
  type: 'text' as const,
352
- text: '[Note: You are contacting us outside business hours. Our team will respond when we return (Monday-Friday, 9 AM - 5 PM).]'
360
+ text: '[Note: You are contacting us outside business hours. Our team will respond when we return (Monday-Friday, 9 AM - 5 PM).]',
353
361
  };
354
362
 
355
363
  return {
@@ -357,13 +365,13 @@ export const businessHoursChecker = new PreProcessor({
357
365
  modifiedMessage: [noticeMessage, ...messages],
358
366
  metadata: {
359
367
  outsideBusinessHours: true,
360
- timestamp: now.toISOString()
361
- }
368
+ timestamp: now.toISOString(),
369
+ },
362
370
  };
363
371
  }
364
372
 
365
373
  return { action: 'proceed' };
366
- }
374
+ },
367
375
  });
368
376
  ```
369
377
 
@@ -379,7 +387,7 @@ export const languageTranslator = new PreProcessor({
379
387
  async: true,
380
388
 
381
389
  execute: async (user, messages, channel) => {
382
- const textMessages = messages.filter(m => m.type === 'text');
390
+ const textMessages = messages.filter((m) => m.type === 'text');
383
391
 
384
392
  if (textMessages.length === 0) {
385
393
  return { action: 'proceed' };
@@ -403,7 +411,7 @@ export const languageTranslator = new PreProcessor({
403
411
  );
404
412
 
405
413
  // Replace with translated text
406
- const translatedMessages = messages.map(msg => {
414
+ const translatedMessages = messages.map((msg) => {
407
415
  if (msg.type === 'text') {
408
416
  return { type: 'text' as const, text: translation };
409
417
  }
@@ -415,13 +423,13 @@ export const languageTranslator = new PreProcessor({
415
423
  modifiedMessage: translatedMessages,
416
424
  metadata: {
417
425
  originalLanguage: language,
418
- translated: true
419
- }
426
+ translated: true,
427
+ },
420
428
  };
421
429
  }
422
430
 
423
431
  return { action: 'proceed' };
424
- }
432
+ },
425
433
  });
426
434
  ```
427
435
 
@@ -448,7 +456,7 @@ export const priorityRouter = new PreProcessor({
448
456
  // Mark as high priority
449
457
  const priorityMessage = {
450
458
  type: 'text' as const,
451
- text: `[HIGH PRIORITY] ${message.text}`
459
+ text: `[HIGH PRIORITY] ${message.text}`,
452
460
  };
453
461
 
454
462
  return {
@@ -457,8 +465,8 @@ export const priorityRouter = new PreProcessor({
457
465
  metadata: {
458
466
  priority: 'high',
459
467
  keyword,
460
- flagged: true
461
- }
468
+ flagged: true,
469
+ },
462
470
  };
463
471
  }
464
472
  }
@@ -468,10 +476,10 @@ export const priorityRouter = new PreProcessor({
468
476
  return {
469
477
  action: 'proceed',
470
478
  metadata: {
471
- priority: 'normal'
472
- }
479
+ priority: 'normal',
480
+ },
473
481
  };
474
- }
482
+ },
475
483
  });
476
484
  ```
477
485
 
@@ -493,11 +501,13 @@ new PreProcessor({ priority: 50, ... });
493
501
  ## Async vs Sync
494
502
 
495
503
  ### Synchronous (default)
504
+
496
505
  - Blocks message processing until complete
497
506
  - Best for fast operations (< 100ms)
498
507
  - Use for filtering, validation, simple transforms
499
508
 
500
509
  ### Asynchronous
510
+
501
511
  - Runs in background, doesn't block
502
512
  - Best for slow operations (API calls, AI)
503
513
  - Set `async: true` in config
@@ -568,13 +578,13 @@ export const safePreProcessor = new PreProcessor({
568
578
  await Data.create('preprocessor_errors', {
569
579
  error: error.message,
570
580
  userId: user.id,
571
- timestamp: new Date().toISOString()
581
+ timestamp: new Date().toISOString(),
572
582
  });
573
583
 
574
584
  // Proceed anyway (fail open)
575
585
  return { action: 'proceed' };
576
586
  }
577
- }
587
+ },
578
588
  });
579
589
  ```
580
590
 
@@ -586,7 +596,7 @@ export const safePreProcessor = new PreProcessor({
586
596
  if (shouldBlock) {
587
597
  return {
588
598
  action: 'block',
589
- response: 'Message blocked'
599
+ response: 'Message blocked',
590
600
  };
591
601
  }
592
602
  return { action: 'proceed' };
@@ -595,7 +605,7 @@ return { action: 'proceed' };
595
605
  ### Message Modification
596
606
 
597
607
  ```typescript
598
- const modifiedMessages = messages.map(msg => {
608
+ const modifiedMessages = messages.map((msg) => {
599
609
  if (msg.type === 'text') {
600
610
  return { type: 'text', text: msg.text.toUpperCase() };
601
611
  }
@@ -604,7 +614,7 @@ const modifiedMessages = messages.map(msg => {
604
614
 
605
615
  return {
606
616
  action: 'proceed',
607
- modifiedMessage: modifiedMessages
617
+ modifiedMessage: modifiedMessages,
608
618
  };
609
619
  ```
610
620
 
@@ -614,7 +624,7 @@ return {
614
624
  const contextMsg = { type: 'text' as const, text: '[Context info]' };
615
625
  return {
616
626
  action: 'proceed',
617
- modifiedMessage: [contextMsg, ...messages]
627
+ modifiedMessage: [contextMsg, ...messages],
618
628
  };
619
629
  ```
620
630
 
@@ -120,3 +120,17 @@ await Workflows.start('refund', input, { originThreadId: threadId, callback: tru
120
120
  ```
121
121
 
122
122
  This works only for the run's own user. A user-less, agent-level run cannot target a home thread yet.
123
+
124
+ ## Threads.resetContext
125
+
126
+ ```typescript
127
+ // In a tool, after a checklist section is recorded:
128
+ await Threads.resetContext(); // { status: 'reset' } or { status: 'skipped', reason }
129
+ ```
130
+
131
+ Starts the agent's context afresh from the current turn. From the next turn on, the agent reads this turn and what follows, not the earlier messages or the observation log built from them. The person still sees the whole conversation, and nothing is deleted.
132
+
133
+ - **Where.** Only from a tool or processor of a 1:1 chat turn. A job, webhook, trigger, room turn or workflow step gets `skipped: not_chat_turn`.
134
+ - **When it applies.** The agent's `contextReset` feature must be on, and the agent must use observational memory (the default). The same feature also resets once a day: the first turn after 04:00 in the person's timezone drops everything from earlier days.
135
+ - **Never throws.** A reset is advisory. It answers `skipped` with a reason when the feature is off (`disabled`), memory is off (`memory_off`), nothing older is left (`nothing_to_reset`), memory was busy (`busy`; the next reset will apply), or offline in `lua test` / `lua dev` (`unavailable`).
136
+ - **Give the agent what it still needs.** After a reset the agent no longer sees earlier turns. Put where the person is in a task in your own data and add it to each turn, for example in a preprocessor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.54.0",
3
+ "version": "3.56.0",
4
4
  "description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
5
5
  "readmeFilename": "README.md",
6
6
  "main": "dist/api-exports.js",
@@ -114,6 +114,7 @@
114
114
  "stripe": "^19.2.0",
115
115
  "ts-node": "^10.9.2",
116
116
  "tsup": "^8.5.1",
117
+ "tsx": "4.21.0",
117
118
  "@lua/sandbox-runtime": "0.0.1",
118
119
  "@lua/shared-sandbox": "0.0.1",
119
120
  "@lua/shared-source-sync": "0.0.1",
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.54.0",
23
+ "lua-cli": "^3.56.0",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"
@@ -8,9 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@base-ui/react": "~1.8.0",
11
- "@lua-ai-global/app-chat": "~0.0.1",
12
- "@lua-ai-global/app-client": "~0.0.3",
13
- "@lua-ai-global/ui": "~0.2.3",
11
+ "@lua-ai-global/app-client": "~0.1.0",
12
+ "@lua-ai-global/ui": "~0.3.0",
14
13
  "@phosphor-icons/react": "~2.1.10",
15
14
  "react": "~19.3.0",
16
15
  "react-dom": "~19.3.0"
@@ -1,5 +1,3 @@
1
1
  @import '@lua-ai-global/ui/styles.css';
2
- /* The agent pane's styles: after the design system's, on its tokens. */
3
- @import '@lua-ai-global/app-chat/styles.css';
4
2
  /* Tailwind scans the design system's compiled components for the classes they use. */
5
3
  @source '../node_modules/@lua-ai-global/ui/dist';
@@ -2,19 +2,11 @@ import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import { App } from './App';
4
4
  import { lua } from '@lua-ai-global/app-client';
5
- import { AgentPaneLayout } from '@lua-ai-global/app-chat';
6
5
  import './index.css';
7
6
 
8
- // Theme follows the shell: first from the bootstrap, then from the host's `init`.
9
- document.documentElement.classList.toggle('dark', lua.theme === 'dark');
10
- lua.onInit((init) => document.documentElement.classList.toggle('dark', init.theme === 'dark'));
11
-
12
7
  createRoot(document.getElementById('root')!).render(
13
8
  <StrictMode>
14
- {/* The agent pane: the person's own conversation with this app's agent, docked beside the page. */}
15
- <AgentPaneLayout>
16
- <App />
17
- </AgentPaneLayout>
9
+ <App />
18
10
  </StrictMode>
19
11
  );
20
12
  void lua.start();