@soederpop/luca 0.0.31 → 0.0.34

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 (86) hide show
  1. package/README.md +241 -36
  2. package/bun.lock +24 -5
  3. package/commands/build-python-bridge.ts +43 -0
  4. package/docs/apis/clients/rest.md +7 -7
  5. package/docs/apis/clients/websocket.md +23 -10
  6. package/docs/apis/features/agi/assistant.md +155 -8
  7. package/docs/apis/features/agi/assistants-manager.md +90 -22
  8. package/docs/apis/features/agi/auto-assistant.md +377 -0
  9. package/docs/apis/features/agi/browser-use.md +802 -0
  10. package/docs/apis/features/agi/claude-code.md +6 -1
  11. package/docs/apis/features/agi/conversation-history.md +7 -6
  12. package/docs/apis/features/agi/conversation.md +111 -38
  13. package/docs/apis/features/agi/docs-reader.md +35 -57
  14. package/docs/apis/features/agi/file-tools.md +163 -0
  15. package/docs/apis/features/agi/openapi.md +2 -2
  16. package/docs/apis/features/agi/skills-library.md +227 -0
  17. package/docs/apis/features/node/content-db.md +125 -4
  18. package/docs/apis/features/node/disk-cache.md +11 -11
  19. package/docs/apis/features/node/downloader.md +1 -1
  20. package/docs/apis/features/node/file-manager.md +15 -15
  21. package/docs/apis/features/node/fs.md +78 -21
  22. package/docs/apis/features/node/git.md +50 -10
  23. package/docs/apis/features/node/google-calendar.md +3 -0
  24. package/docs/apis/features/node/google-docs.md +10 -1
  25. package/docs/apis/features/node/google-drive.md +3 -0
  26. package/docs/apis/features/node/google-mail.md +214 -0
  27. package/docs/apis/features/node/google-sheets.md +3 -0
  28. package/docs/apis/features/node/ink.md +10 -10
  29. package/docs/apis/features/node/ipc-socket.md +83 -93
  30. package/docs/apis/features/node/networking.md +5 -5
  31. package/docs/apis/features/node/os.md +7 -7
  32. package/docs/apis/features/node/package-finder.md +14 -14
  33. package/docs/apis/features/node/proc.md +2 -1
  34. package/docs/apis/features/node/process-manager.md +70 -3
  35. package/docs/apis/features/node/python.md +265 -9
  36. package/docs/apis/features/node/redis.md +380 -0
  37. package/docs/apis/features/node/ui.md +13 -13
  38. package/docs/apis/servers/express.md +35 -7
  39. package/docs/apis/servers/mcp.md +3 -3
  40. package/docs/apis/servers/websocket.md +51 -8
  41. package/docs/bootstrap/CLAUDE.md +1 -1
  42. package/docs/bootstrap/SKILL.md +93 -7
  43. package/docs/examples/feature-as-tool-provider.md +143 -0
  44. package/docs/examples/python.md +42 -1
  45. package/docs/introspection.md +15 -5
  46. package/docs/tutorials/00-bootstrap.md +3 -3
  47. package/docs/tutorials/02-container.md +2 -2
  48. package/docs/tutorials/10-creating-features.md +5 -0
  49. package/docs/tutorials/13-introspection.md +12 -2
  50. package/docs/tutorials/19-python-sessions.md +401 -0
  51. package/package.json +8 -4
  52. package/src/agi/container.server.ts +8 -0
  53. package/src/agi/features/assistant.ts +19 -0
  54. package/src/agi/features/autonomous-assistant.ts +435 -0
  55. package/src/agi/features/conversation.ts +58 -6
  56. package/src/agi/features/file-tools.ts +286 -0
  57. package/src/agi/features/luca-coder.ts +643 -0
  58. package/src/bootstrap/generated.ts +705 -17
  59. package/src/cli/build-info.ts +2 -2
  60. package/src/cli/cli.ts +22 -13
  61. package/src/commands/bootstrap.ts +49 -6
  62. package/src/commands/code.ts +369 -0
  63. package/src/commands/describe.ts +7 -2
  64. package/src/commands/index.ts +1 -0
  65. package/src/commands/sandbox-mcp.ts +7 -7
  66. package/src/commands/save-api-docs.ts +1 -1
  67. package/src/container-describer.ts +4 -4
  68. package/src/container.ts +10 -19
  69. package/src/helper.ts +24 -33
  70. package/src/introspection/generated.agi.ts +2499 -63
  71. package/src/introspection/generated.node.ts +1625 -688
  72. package/src/introspection/generated.web.ts +15 -57
  73. package/src/node/container.ts +5 -0
  74. package/src/node/features/figlet-fonts.ts +597 -0
  75. package/src/node/features/fs.ts +3 -9
  76. package/src/node/features/helpers.ts +20 -0
  77. package/src/node/features/python.ts +429 -16
  78. package/src/node/features/redis.ts +446 -0
  79. package/src/node/features/ui.ts +4 -11
  80. package/src/python/bridge.py +220 -0
  81. package/src/python/generated.ts +227 -0
  82. package/src/scaffolds/generated.ts +1 -1
  83. package/test/python-session.test.ts +105 -0
  84. package/assistants/lucaExpert/CORE.md +0 -37
  85. package/assistants/lucaExpert/hooks.ts +0 -9
  86. package/assistants/lucaExpert/tools.ts +0 -177
@@ -7,7 +7,7 @@ Luca's introspection system lets you discover everything about a container and i
7
7
  The container knows what it is, what registries it has, and what's available in each one.
8
8
 
9
9
  ```ts
10
- const info = container.inspect()
10
+ const info = container.introspect()
11
11
  console.log(info.className)
12
12
  console.log(info.registries.map(r => `${r.name}: ${r.available.length} available`))
13
13
  console.log('factories:', info.factoryNames)
@@ -17,17 +17,17 @@ console.log('enabled features:', info.enabledFeatures)
17
17
  You can get the full introspection as markdown:
18
18
 
19
19
  ```ts
20
- console.log(container.inspectAsText())
20
+ console.log(container.introspectAsText())
21
21
  ```
22
22
 
23
23
  Or request a single section:
24
24
 
25
25
  ```ts
26
- console.log(container.inspectAsText('methods'))
26
+ console.log(container.introspectAsText('methods'))
27
27
  ```
28
28
 
29
29
  ```ts
30
- console.log(container.inspectAsText('getters'))
30
+ console.log(container.introspectAsText('getters'))
31
31
  ```
32
32
 
33
33
  ## Querying Registries
@@ -60,6 +60,16 @@ console.log(`${allFeatureDocs.length} features documented`)
60
60
  console.log(allFeatureDocs[0].slice(0, 200) + '...')
61
61
  ```
62
62
 
63
+ ## Helper Introspection — Quick Discovery
64
+
65
+ Every helper instance exposes `$methods` and `$getters` for a quick look at what's available:
66
+
67
+ ```ts
68
+ const git = container.feature('git')
69
+ console.log('methods:', git.$methods)
70
+ console.log('getters:', git.$getters)
71
+ ```
72
+
63
73
  ## Helper Introspection — Structured Data
64
74
 
65
75
  Every helper instance (feature, client, server, etc.) can introspect itself. The result is a typed object you can traverse programmatically.
@@ -126,7 +136,7 @@ const git = container.feature('git')
126
136
  console.log(git.introspectAsText('methods', 3)) // headings start at ###
127
137
  ```
128
138
 
129
- ## Inspecting State and Options
139
+ ## Introspecting State and Options
130
140
 
131
141
  Features that declare Zod schemas for state and options expose them through introspection:
132
142
 
@@ -101,8 +101,8 @@ fs.introspectAsText()
101
101
  The container itself is introspectable:
102
102
 
103
103
  ```js
104
- container.inspect() // structured object with all registries, state, events
105
- container.inspectAsText() // full markdown overview
104
+ container.introspect() // structured object with all registries, state, events
105
+ container.introspectAsText() // full markdown overview
106
106
  ```
107
107
 
108
108
  ## The REPL
@@ -137,7 +137,7 @@ luca eval "grep.search('.', 'TODO')"
137
137
  | Structured introspection? | `feature.introspect()` |
138
138
  | What state does it have? | `feature.state.current` |
139
139
  | What events does it emit? | `feature.introspect().events` |
140
- | Full container overview? | `container.inspectAsText()` |
140
+ | Full container overview? | `container.introspectAsText()` |
141
141
  | CLI docs for a helper? | `luca describe <name>` |
142
142
 
143
143
  ## Gotchas
@@ -201,10 +201,10 @@ Discover everything about the container at runtime:
201
201
 
202
202
  ```typescript
203
203
  // Structured introspection data
204
- const info = container.inspect()
204
+ const info = container.introspect()
205
205
 
206
206
  // Human-readable markdown
207
- const docs = container.inspectAsText()
207
+ const docs = container.introspectAsText()
208
208
  ```
209
209
 
210
210
  This is what makes Luca especially powerful for AI agents -- they can discover the entire API surface at runtime without reading documentation.
@@ -187,6 +187,11 @@ Then anyone (human or AI) can discover your feature:
187
187
  ```typescript
188
188
  container.features.describe('sessionManager')
189
189
  // Returns the full markdown documentation extracted from your JSDoc
190
+
191
+ // Quick discovery — list available methods and getters
192
+ const session = container.feature('sessionManager')
193
+ session.$methods // => ['createSession', ...]
194
+ session.$getters // => ['activeCount', ...]
190
195
  ```
191
196
 
192
197
  ## Best Practices
@@ -18,11 +18,11 @@ Introspection serves two audiences:
18
18
 
19
19
  ```typescript
20
20
  // Structured data about the entire container
21
- const info = container.inspect()
21
+ const info = container.introspect()
22
22
  // Returns: registries, enabled features, state schema, available helpers
23
23
 
24
24
  // Human-readable markdown
25
- const docs = container.inspectAsText()
25
+ const docs = container.introspectAsText()
26
26
  ```
27
27
 
28
28
  ## Registry-Level Discovery
@@ -73,6 +73,16 @@ const info = fs.introspect()
73
73
  const docs = fs.introspectAsText()
74
74
  ```
75
75
 
76
+ ### Quick Discovery with $getters and $methods
77
+
78
+ Every helper exposes `$getters` and `$methods` — string arrays listing what's available on the instance. Useful for quick exploration without parsing the full introspection object:
79
+
80
+ ```typescript
81
+ const fs = container.feature('fs')
82
+ fs.$methods // => ['readFile', 'writeFile', 'walk', 'readdir', ...]
83
+ fs.$getters // => ['cwd', 'sep', ...]
84
+ ```
85
+
76
86
  ### What's in the Introspection Data?
77
87
 
78
88
  - **Class name** and description (from JSDoc)
@@ -0,0 +1,401 @@
1
+ ---
2
+ title: Working with Python Projects
3
+ tags: [python, sessions, persistent, bridge, codebase, interop, data-science]
4
+ ---
5
+
6
+ # Working with Python Projects
7
+
8
+ Luca's `python` feature has two modes: **stateless** execution (fire-and-forget, one process per call) and **persistent sessions** (a long-lived Python process that maintains state across calls). This tutorial focuses on sessions — the mode that lets you actually work inside a Python codebase.
9
+
10
+ ## When to Use Sessions
11
+
12
+ Stateless `execute()` is fine for one-off scripts. But if you need any of these, you want a session:
13
+
14
+ - **Imports that persist** — load `pandas` once, use it across many calls
15
+ - **State that builds up** — query a database, filter results, then export
16
+ - **Working inside a real project** — import your own modules, call your own functions
17
+ - **Expensive setup** — ML model loading, database connections, API client initialization
18
+
19
+ ## Quick Start
20
+
21
+ ```ts skip
22
+ const python = container.feature('python', { dir: '/path/to/my-python-project' })
23
+ await python.enable()
24
+ await python.startSession()
25
+
26
+ // Everything below runs in the same Python process.
27
+ // Variables, imports, and state persist across calls.
28
+
29
+ await python.run('import pandas as pd')
30
+ await python.run('df = pd.read_csv("data/sales.csv")')
31
+
32
+ const result = await python.run('print(df.shape)')
33
+ console.log(result.stdout) // '(1000, 12)\n'
34
+
35
+ const total = await python.eval('df["revenue"].sum()')
36
+ console.log('Total revenue:', total)
37
+
38
+ await python.stopSession()
39
+ ```
40
+
41
+ ## Project Directory
42
+
43
+ The `dir` option tells Luca where the Python project lives. This determines:
44
+
45
+ 1. **sys.path** — the bridge adds the project root (and `src/`, `lib/` if they exist) so your imports work
46
+ 2. **Environment detection** — Luca looks for `uv.lock`, `pyproject.toml`, `venv/`, etc. in this directory
47
+ 3. **Working directory** — the bridge process runs with `cwd` set to this path
48
+
49
+ ```ts skip
50
+ // Explicit project directory
51
+ const python = container.feature('python', { dir: '/Users/me/projects/my-api' })
52
+
53
+ // Or defaults to wherever luca was invoked from
54
+ const python = container.feature('python')
55
+ ```
56
+
57
+ If your project uses a `src/` layout (common in modern Python), the bridge automatically adds it to `sys.path`:
58
+
59
+ ```
60
+ my-project/
61
+ src/
62
+ myapp/
63
+ __init__.py
64
+ models.py
65
+ pyproject.toml
66
+ ```
67
+
68
+ ```ts skip
69
+ await python.startSession()
70
+ // This works because src/ was added to sys.path
71
+ await python.importModule('myapp.models', 'models')
72
+ ```
73
+
74
+ ## Session Lifecycle
75
+
76
+ ### Starting
77
+
78
+ `startSession()` spawns a Python bridge process that talks to Luca over stdin/stdout using a JSON-line protocol. The bridge sets up `sys.path` and signals when it's ready.
79
+
80
+ ```ts skip
81
+ await python.enable()
82
+ await python.startSession()
83
+
84
+ console.log(python.state.get('sessionActive')) // true
85
+ console.log(python.state.get('sessionId')) // uuid
86
+ ```
87
+
88
+ ### Stopping
89
+
90
+ `stopSession()` kills the bridge process and cleans up. Any pending requests are rejected.
91
+
92
+ ```ts skip
93
+ await python.stopSession()
94
+ console.log(python.state.get('sessionActive')) // false
95
+ ```
96
+
97
+ ### Crash Recovery
98
+
99
+ If the Python process dies unexpectedly (segfault, killed externally), the feature:
100
+ - Sets `sessionActive` to `false`
101
+ - Rejects all pending requests
102
+ - Emits a `sessionError` event
103
+
104
+ ```ts skip
105
+ python.on('sessionError', ({ error, sessionId }) => {
106
+ console.error('Python session error:', error)
107
+ // You could restart: await python.startSession()
108
+ })
109
+ ```
110
+
111
+ ## The Session API
112
+
113
+ ### run(code, variables?)
114
+
115
+ Execute Python code in the persistent namespace. This is the workhorse method.
116
+
117
+ ```ts skip
118
+ // Simple execution
119
+ const result = await python.run('print("hello")')
120
+ // result.ok === true
121
+ // result.stdout === 'hello\n'
122
+
123
+ // With variable injection
124
+ const result = await python.run('print(f"Processing {count} items")', { count: 42 })
125
+
126
+ // Errors don't crash the session
127
+ const bad = await python.run('raise ValueError("oops")')
128
+ // bad.ok === false
129
+ // bad.error === 'oops'
130
+ // bad.traceback === 'Traceback (most recent call last):\n...'
131
+
132
+ // Session still alive after error
133
+ const good = await python.run('print("still here")')
134
+ // good.ok === true
135
+ ```
136
+
137
+ ### eval(expression)
138
+
139
+ Evaluate a Python expression and return its value to JavaScript.
140
+
141
+ ```ts skip
142
+ await python.run('x = [1, 2, 3]')
143
+ const length = await python.eval('len(x)') // 3
144
+ const doubled = await python.eval('[i*2 for i in x]') // [2, 4, 6]
145
+ ```
146
+
147
+ Values are JSON-serialized. Complex types that can't be serialized come back as their `repr()` string.
148
+
149
+ ### importModule(name, alias?)
150
+
151
+ Import a module into the session namespace. The alias defaults to the last segment of the module path.
152
+
153
+ ```ts skip
154
+ await python.importModule('json') // import json
155
+ await python.importModule('myapp.models', 'models') // import myapp.models as models
156
+ await python.importModule('os.path') // import os.path (available as "path")
157
+ ```
158
+
159
+ ### call(funcPath, args?, kwargs?)
160
+
161
+ Call a function by its dotted path in the namespace.
162
+
163
+ ```ts skip
164
+ await python.importModule('json')
165
+ const encoded = await python.call('json.dumps', [{ a: 1 }], { indent: 2 })
166
+ // '{\n "a": 1\n}'
167
+
168
+ // Works with your own functions too
169
+ await python.run('def add(a, b): return a + b')
170
+ const sum = await python.call('add', [3, 4]) // 7
171
+ ```
172
+
173
+ ### getLocals()
174
+
175
+ Inspect everything in the session namespace.
176
+
177
+ ```ts skip
178
+ await python.run('x = 42')
179
+ await python.importModule('json')
180
+ const locals = await python.getLocals()
181
+ // { x: 42, json: '<module ...>' }
182
+ ```
183
+
184
+ ### resetSession()
185
+
186
+ Clear all variables and imports without restarting the process.
187
+
188
+ ```ts skip
189
+ await python.run('big_model = load_model()')
190
+ await python.resetSession()
191
+ // big_model is gone, but the session process is still running
192
+ ```
193
+
194
+ ## Real-World Patterns
195
+
196
+ ### Data Analysis Pipeline
197
+
198
+ ```ts skip
199
+ const python = container.feature('python', { dir: '/path/to/analytics' })
200
+ await python.enable()
201
+ await python.startSession()
202
+
203
+ // Setup
204
+ await python.run('import pandas as pd')
205
+ await python.run('import matplotlib')
206
+ await python.run('matplotlib.use("Agg")') // headless
207
+ await python.run('import matplotlib.pyplot as plt')
208
+
209
+ // Load and analyze
210
+ await python.run('df = pd.read_csv("data/events.csv")')
211
+ const shape = await python.eval('list(df.shape)')
212
+ console.log(`Loaded ${shape[0]} rows, ${shape[1]} columns`)
213
+
214
+ const columns = await python.eval('list(df.columns)')
215
+ console.log('Columns:', columns)
216
+
217
+ // Filter and aggregate
218
+ await python.run(`
219
+ filtered = df[df["status"] == "completed"]
220
+ summary = filtered.groupby("category")["amount"].agg(["sum", "mean", "count"])
221
+ `)
222
+
223
+ const summary = await python.eval('summary.to_dict()')
224
+ console.log('Summary:', summary)
225
+
226
+ // Generate a chart
227
+ await python.run(`
228
+ fig, ax = plt.subplots(figsize=(10, 6))
229
+ summary["sum"].plot(kind="bar", ax=ax)
230
+ ax.set_title("Revenue by Category")
231
+ fig.savefig("output/revenue.png", dpi=150, bbox_inches="tight")
232
+ plt.close(fig)
233
+ `)
234
+
235
+ await python.stopSession()
236
+ ```
237
+
238
+ ### Working with a Django Project
239
+
240
+ ```ts skip
241
+ const python = container.feature('python', { dir: '/path/to/django-project' })
242
+ await python.enable()
243
+ await python.startSession()
244
+
245
+ // Django requires this before you can import models
246
+ await python.run(`
247
+ import os
248
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
249
+
250
+ import django
251
+ django.setup()
252
+ `)
253
+
254
+ // Now you can work with the ORM
255
+ await python.run('from myapp.models import User, Order')
256
+
257
+ const userCount = await python.eval('User.objects.count()')
258
+ console.log(`${userCount} users in database`)
259
+
260
+ const recentOrders = await python.eval(`
261
+ list(Order.objects.filter(status="pending").values("id", "total", "created_at")[:10])
262
+ `)
263
+ console.log('Recent pending orders:', recentOrders)
264
+
265
+ await python.stopSession()
266
+ ```
267
+
268
+ ### ML Model Interaction
269
+
270
+ ```ts skip
271
+ const python = container.feature('python', { dir: '/path/to/ml-project' })
272
+ await python.enable()
273
+ await python.startSession()
274
+
275
+ // Expensive setup — only happens once
276
+ await python.run(`
277
+ from transformers import pipeline
278
+ classifier = pipeline("sentiment-analysis")
279
+ print("Model loaded")
280
+ `)
281
+
282
+ // Now you can call it cheaply many times
283
+ async function classify(text: string) {
284
+ return python.call('classifier', [text])
285
+ }
286
+
287
+ const results = await Promise.all([
288
+ classify('I love this product!'),
289
+ classify('Terrible experience.'),
290
+ classify('It was okay, nothing special.'),
291
+ ])
292
+
293
+ console.log(results)
294
+ // [
295
+ // [{ label: 'POSITIVE', score: 0.9998 }],
296
+ // [{ label: 'NEGATIVE', score: 0.9994 }],
297
+ // [{ label: 'NEGATIVE', score: 0.7231 }],
298
+ // ]
299
+
300
+ await python.stopSession()
301
+ ```
302
+
303
+ ### Luca Command That Uses Python
304
+
305
+ ```ts skip
306
+ // commands/analyze.ts
307
+ import { z } from 'zod'
308
+ import type { ContainerContext } from '@soederpop/luca'
309
+ import { CommandOptionsSchema } from '@soederpop/luca/schemas'
310
+
311
+ export const positionals = ['target']
312
+ export const argsSchema = CommandOptionsSchema.extend({
313
+ target: z.string().describe('Path to CSV file to analyze'),
314
+ })
315
+
316
+ async function handler(options: z.infer<typeof argsSchema>, context: ContainerContext) {
317
+ const container = context.container as any
318
+ const python = container.feature('python')
319
+ await python.enable()
320
+ await python.startSession()
321
+
322
+ try {
323
+ await python.run('import pandas as pd')
324
+ await python.run(`df = pd.read_csv("${options.target}")`)
325
+
326
+ const shape = await python.eval('list(df.shape)')
327
+ const dtypes = await python.eval('dict(df.dtypes.astype(str))')
328
+ const nulls = await python.eval('dict(df.isnull().sum())')
329
+
330
+ console.log(`Rows: ${shape[0]}, Columns: ${shape[1]}`)
331
+ console.log('Column types:', dtypes)
332
+ console.log('Null counts:', nulls)
333
+ } finally {
334
+ await python.stopSession()
335
+ }
336
+ }
337
+
338
+ export default {
339
+ description: 'Analyze a CSV file using pandas',
340
+ argsSchema,
341
+ handler,
342
+ }
343
+ ```
344
+
345
+ ```bash
346
+ luca analyze data/sales.csv
347
+ ```
348
+
349
+ ## Stateless vs. Session: Choosing the Right Mode
350
+
351
+ | | `execute()` (stateless) | `run()` (session) |
352
+ |---|---|---|
353
+ | Process | Fresh per call | Shared, long-lived |
354
+ | State | None — each call starts clean | Persists across calls |
355
+ | Imports | Re-imported every time | Imported once, reused |
356
+ | Startup cost | ~50-200ms per call | ~200ms once, then ~1ms per call |
357
+ | Use case | One-off scripts, simple eval | Real projects, data pipelines, REPL-like |
358
+ | Error isolation | Perfect — crash is contained | Errors caught, session survives |
359
+
360
+ Both modes use the same environment detection (uv, conda, venv, system) and respect the same `dir` and `pythonPath` options.
361
+
362
+ ## Environment Detection
363
+
364
+ The feature detects Python environments in this order:
365
+
366
+ 1. **Explicit** — `pythonPath` option overrides everything
367
+ 2. **uv** — `uv.lock` or `pyproject.toml` present, `uv run python` works
368
+ 3. **conda** — `environment.yml` or `conda.yml` present
369
+ 4. **venv** — `venv/` or `.venv/` directory with a Python binary inside
370
+ 5. **system** — falls back to `python3` or `python` on PATH
371
+
372
+ ```ts skip
373
+ const python = container.feature('python', { dir: '/path/to/project' })
374
+ await python.enable()
375
+ console.log(python.environmentType) // 'uv' | 'conda' | 'venv' | 'system'
376
+ console.log(python.pythonPath) // e.g. '/Users/me/.local/bin/uv run python'
377
+ ```
378
+
379
+ ## Events
380
+
381
+ The session emits events you can listen to for monitoring and debugging:
382
+
383
+ ```ts skip
384
+ python.on('sessionStarted', ({ sessionId }) => {
385
+ console.log('Session started:', sessionId)
386
+ })
387
+
388
+ python.on('sessionStopped', ({ sessionId }) => {
389
+ console.log('Session stopped:', sessionId)
390
+ })
391
+
392
+ python.on('sessionError', ({ error, sessionId }) => {
393
+ console.error('Session error:', error)
394
+ })
395
+ ```
396
+
397
+ ## What's Next
398
+
399
+ - [Creating Features](./10-creating-features.md) — build your own feature that wraps a Python service
400
+ - [Commands](./08-commands.md) — create CLI commands that leverage Python
401
+ - [Servers and Endpoints](./06-servers.md) — expose Python-powered analysis via HTTP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soederpop/luca",
3
- "version": "0.0.31",
3
+ "version": "0.0.34",
4
4
  "website": "https://luca.soederpop.com",
5
5
  "description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
6
6
  "author": "jon soeder aka the people's champ <jon@soederpop.com>",
@@ -34,6 +34,8 @@
34
34
  "./container": "./src/container.ts",
35
35
  "./client": "./src/client.ts",
36
36
  "./clients/*": "./src/clients/*",
37
+ "./server": "./src/server.ts",
38
+ "./servers/*": "./src/servers/*",
37
39
  "./feature": "./src/feature.ts",
38
40
  "./react": "./src/react/index.ts",
39
41
  "./introspection": "./src/introspection/index.ts"
@@ -48,13 +50,14 @@
48
50
  "clean": "rm -rf dist build package",
49
51
  "build:web": "bun run scripts/build-web.ts",
50
52
  "build:introspection": "bun run src/cli/cli.ts introspect",
51
- "compile": "bun run build:introspection && bun run build:scaffolds && bun run build:bootstrap && bash scripts/stamp-build.sh && bun build ./src/cli/cli.ts --compile --outfile dist/luca --external node-llama-cpp",
53
+ "compile": "bun run build:introspection && bun run build:scaffolds && bun run build:bootstrap && bun run build:python-bridge && bash scripts/stamp-build.sh && bun build ./src/cli/cli.ts --compile --outfile dist/luca --external node-llama-cpp",
52
54
  "typecheck": "tsc -p tsconfig.json --noEmit",
53
55
  "build:scaffolds": "bun run src/cli/cli.ts build-scaffolds",
54
56
  "build:bootstrap": "bun run src/cli/cli.ts build-bootstrap",
57
+ "build:python-bridge": "bun run src/cli/cli.ts build-python-bridge",
55
58
  "test": "bun test test/*.test.ts",
56
- "update-all-docs": "bun run test && bun run build:introspection && bun run src/cli/cli.ts generate-api-docs && bun run build:scaffolds && bun run build:bootstrap",
57
- "precommit": "bun run update-all-docs && git add docs/apis/ src/introspection/generated.*.ts src/scaffolds/generated.ts src/bootstrap/generated.ts && bun compile",
59
+ "update-all-docs": "bun run test && bun run build:introspection && bun run src/cli/cli.ts generate-api-docs && bun run build:scaffolds && bun run build:bootstrap && bun run build:python-bridge",
60
+ "precommit": "bun run update-all-docs && git add docs/apis/ src/introspection/generated.*.ts src/scaffolds/generated.ts src/bootstrap/generated.ts src/python/generated.ts && bun compile",
58
61
  "test:integration": "bun test ./test-integration/"
59
62
  },
60
63
  "devDependencies": {
@@ -117,6 +120,7 @@
117
120
  "inflect": "^0.5.0",
118
121
  "ink": "^6.7.0",
119
122
  "inquirer": "^9.1.5",
123
+ "ioredis": "^5.10.1",
120
124
  "isomorphic-vm": "^0.0.1",
121
125
  "isomorphic-ws": "^5.0.0",
122
126
  "js-tiktoken": "^1.0.21",
@@ -14,6 +14,8 @@ import { SkillsLibrary } from './features/skills-library'
14
14
  import { BrowserUse } from './features/browser-use'
15
15
  import { SemanticSearch } from '@soederpop/luca/node/features/semantic-search'
16
16
  import { ContentDb } from '@soederpop/luca/node/features/content-db'
17
+ import { FileTools } from './features/file-tools'
18
+ import { LucaCoder } from './features/luca-coder'
17
19
 
18
20
  import type { ConversationTool } from './features/conversation'
19
21
  import type { ZodType } from 'zod'
@@ -28,6 +30,8 @@ export {
28
30
  DocsReader,
29
31
  SkillsLibrary,
30
32
  BrowserUse,
33
+ FileTools,
34
+ LucaCoder,
31
35
  SemanticSearch,
32
36
  ContentDb,
33
37
  NodeContainer,
@@ -52,6 +56,8 @@ export interface AGIFeatures extends NodeFeatures {
52
56
  docsReader: typeof DocsReader
53
57
  skillsLibrary: typeof SkillsLibrary
54
58
  browserUse: typeof BrowserUse
59
+ fileTools: typeof FileTools
60
+ lucaCoder: typeof LucaCoder
55
61
  }
56
62
 
57
63
  export interface ConversationFactoryOptions {
@@ -125,6 +131,8 @@ const container = new AGIContainer()
125
131
  .use(DocsReader)
126
132
  .use(SkillsLibrary)
127
133
  .use(BrowserUse)
134
+ .use(FileTools)
135
+ .use(LucaCoder)
128
136
  .use(SemanticSearch)
129
137
 
130
138
  container.docs = container.feature('contentDb', {
@@ -79,6 +79,18 @@ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
79
79
  /** Maximum number of output tokens per completion */
80
80
 
81
81
  maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
82
+ /** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
83
+ temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2)'),
84
+ /** Nucleus sampling cutoff (0-1). */
85
+ topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1)'),
86
+ /** Top-K sampling. Only supported by local/Anthropic models. */
87
+ topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
88
+ /** Frequency penalty (-2 to 2). */
89
+ frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2)'),
90
+ /** Presence penalty (-2 to 2). */
91
+ presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2)'),
92
+ /** Stop sequences. */
93
+ stop: z.array(z.string()).optional().describe('Stop sequences'),
82
94
 
83
95
  local: z.boolean().default(false).describe('Whether to use our local models for this'),
84
96
 
@@ -261,6 +273,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
261
273
  tools: this.tools,
262
274
  api: 'chat',
263
275
  ...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
276
+ ...(this.options.temperature != null ? { temperature: this.options.temperature } : {}),
277
+ ...(this.options.topP != null ? { topP: this.options.topP } : {}),
278
+ ...(this.options.topK != null ? { topK: this.options.topK } : {}),
279
+ ...(this.options.frequencyPenalty != null ? { frequencyPenalty: this.options.frequencyPenalty } : {}),
280
+ ...(this.options.presencePenalty != null ? { presencePenalty: this.options.presencePenalty } : {}),
281
+ ...(this.options.stop ? { stop: this.options.stop } : {}),
264
282
  history: [
265
283
  { role: 'system', content: this.effectiveSystemPrompt },
266
284
  ],
@@ -962,6 +980,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
962
980
  }
963
981
 
964
982
  private unbindHooksFromEvents() {
983
+ if (!this._boundHookListeners) return
965
984
  for (const { event, listener } of this._boundHookListeners) {
966
985
  this.off(event as any, listener)
967
986
  }