@soederpop/luca 0.0.29 → 0.0.31
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/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/package.json +1 -1
- package/src/agi/container.server.ts +4 -0
- package/src/agi/features/assistant.ts +120 -3
- package/src/agi/features/browser-use.ts +623 -0
- package/src/bootstrap/generated.ts +236 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/command.ts +20 -1
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/commands/serve.ts +27 -0
- package/src/container.ts +411 -113
- package/src/endpoint.ts +6 -0
- package/src/helper.ts +226 -5
- package/src/introspection/generated.agi.ts +16089 -10021
- package/src/introspection/generated.node.ts +5102 -2077
- package/src/introspection/generated.web.ts +379 -291
- package/src/introspection/index.ts +7 -0
- package/src/introspection/scan.ts +224 -7
- package/src/node/container.ts +31 -10
- package/src/node/features/content-db.ts +7 -7
- package/src/node/features/disk-cache.ts +11 -11
- package/src/node/features/esbuild.ts +3 -3
- package/src/node/features/file-manager.ts +15 -15
- package/src/node/features/fs.ts +23 -22
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +5 -2
- package/src/node/features/ink.ts +13 -13
- package/src/node/features/ipc-socket.ts +8 -8
- package/src/node/features/networking.ts +3 -3
- package/src/node/features/os.ts +7 -7
- package/src/node/features/package-finder.ts +15 -15
- package/src/node/features/proc.ts +1 -1
- package/src/node/features/ui.ts +13 -13
- package/src/node/features/vm.ts +4 -4
- package/src/scaffolds/generated.ts +1 -1
- package/src/servers/express.ts +24 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/docs/apis/features/node/window-manager.md +0 -445
- package/docs/examples/window-manager-layouts.md +0 -180
- package/docs/examples/window-manager.md +0 -125
- package/docs/window-manager-fix.md +0 -249
- package/scripts/test-window-manager-lifecycle.ts +0 -86
- package/scripts/test-window-manager.ts +0 -43
- package/src/node/features/window-manager.ts +0 -1603
|
@@ -241,7 +241,7 @@ Be specific and actionable. Reference concrete file paths, APIs, and patterns. T
|
|
|
241
241
|
'--in-folder', sessionFolder,
|
|
242
242
|
'--out-file', `${sessionFolder}/logs/synthesis-session.md`,
|
|
243
243
|
'--dont-touch-file',
|
|
244
|
-
'--
|
|
244
|
+
'--include-frontmatter',
|
|
245
245
|
], {
|
|
246
246
|
onOutput: (str: string) => { process.stdout.write(str) },
|
|
247
247
|
onError: (str: string) => { process.stderr.write(str) },
|
|
@@ -57,8 +57,6 @@
|
|
|
57
57
|
- [ui](./examples/ui.md)
|
|
58
58
|
- [Vault](./examples/vault.md)
|
|
59
59
|
- [vm](./examples/vm.md)
|
|
60
|
-
- [Window Manager](./examples/window-manager.md)
|
|
61
|
-
- [Window Manager Layouts](./examples/window-manager-layouts.md)
|
|
62
60
|
- [YAML](./examples/yaml.md)
|
|
63
61
|
- [YAML Tree](./examples/yaml-tree.md)
|
|
64
62
|
|
|
@@ -159,7 +157,6 @@
|
|
|
159
157
|
- [UI (features.ui)](./apis/features/node/ui.md)
|
|
160
158
|
- [WebVault (features.vault)](./apis/features/node/vault.md)
|
|
161
159
|
- [VM (features.vm)](./apis/features/node/vm.md)
|
|
162
|
-
- [WindowManager (features.windowManager)](./apis/features/node/window-manager.md)
|
|
163
160
|
- [YAML (features.yaml)](./apis/features/node/yaml.md)
|
|
164
161
|
- [YamlTree (features.yamlTree)](./apis/features/node/yaml-tree.md)
|
|
165
162
|
- [AssetLoader (features.assetLoader)](./apis/features/web/asset-loader.md)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Browser: Import Luca from esm.sh"
|
|
3
|
+
tags:
|
|
4
|
+
- browser
|
|
5
|
+
- esm
|
|
6
|
+
- web
|
|
7
|
+
- quickstart
|
|
8
|
+
- cdn
|
|
9
|
+
---
|
|
10
|
+
# Browser: Import Luca from esm.sh
|
|
11
|
+
|
|
12
|
+
You can use Luca in any browser environment — no bundler, no build step. Import it from [esm.sh](https://esm.sh) and you get the singleton container on `window.luca`, ready to go. All the same APIs apply.
|
|
13
|
+
|
|
14
|
+
## Basic Setup
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script type="module">
|
|
18
|
+
import "https://esm.sh/@soederpop/luca/web"
|
|
19
|
+
|
|
20
|
+
const container = window.luca
|
|
21
|
+
console.log(container.uuid) // unique container ID
|
|
22
|
+
console.log(container.features.available) // ['assetLoader', 'voice', 'speech', 'network', 'vault', 'vm', 'esbuild', 'helpers', 'containerLink']
|
|
23
|
+
</script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The import triggers module evaluation, which creates the `WebContainer` singleton and attaches it to `window.luca`. That's it.
|
|
27
|
+
|
|
28
|
+
If you prefer a named import:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script type="module">
|
|
32
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
33
|
+
// container === window.luca
|
|
34
|
+
</script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Using Features
|
|
38
|
+
|
|
39
|
+
Once you have the container, features work exactly like they do on the server — lazy-loaded via `container.feature()`.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<script type="module">
|
|
43
|
+
import "https://esm.sh/@soederpop/luca/web"
|
|
44
|
+
const { luca: container } = window
|
|
45
|
+
|
|
46
|
+
// Load a script from a CDN
|
|
47
|
+
const assetLoader = container.feature('assetLoader')
|
|
48
|
+
await assetLoader.loadScript('https://cdn.jsdelivr.net/npm/chart.js')
|
|
49
|
+
|
|
50
|
+
// Load a stylesheet
|
|
51
|
+
await assetLoader.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
|
|
52
|
+
|
|
53
|
+
// Text-to-speech
|
|
54
|
+
const speech = container.feature('speech')
|
|
55
|
+
speech.speak('Hello from Luca')
|
|
56
|
+
|
|
57
|
+
// Voice recognition
|
|
58
|
+
const voice = container.feature('voice')
|
|
59
|
+
voice.on('transcript', ({ text }) => console.log('Heard:', text))
|
|
60
|
+
voice.start()
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## State and Events
|
|
65
|
+
|
|
66
|
+
The container is a state machine and event bus. This works identically to the server.
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<script type="module">
|
|
70
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
71
|
+
|
|
72
|
+
// Listen for state changes
|
|
73
|
+
container.on('stateChanged', ({ changes }) => {
|
|
74
|
+
console.log('State changed:', changes)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Feature-level state and events
|
|
78
|
+
const voice = container.feature('voice')
|
|
79
|
+
voice.on('stateChanged', ({ changes }) => {
|
|
80
|
+
document.getElementById('status').textContent = changes.listening ? 'Listening...' : 'Idle'
|
|
81
|
+
})
|
|
82
|
+
</script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## REST Client
|
|
86
|
+
|
|
87
|
+
Make HTTP requests with the built-in REST client. Methods return parsed JSON directly.
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<script type="module">
|
|
91
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
92
|
+
|
|
93
|
+
const api = container.client('rest', { baseURL: 'https://jsonplaceholder.typicode.com' })
|
|
94
|
+
const posts = await api.get('/posts')
|
|
95
|
+
console.log(posts) // array of post objects, not a Response wrapper
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## WebSocket Client
|
|
100
|
+
|
|
101
|
+
Connect to a WebSocket server:
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<script type="module">
|
|
105
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
106
|
+
|
|
107
|
+
const socket = container.client('socket', { url: 'ws://localhost:3000' })
|
|
108
|
+
socket.on('message', (data) => console.log('Received:', data))
|
|
109
|
+
socket.send({ type: 'hello' })
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Extending: Custom Features
|
|
114
|
+
|
|
115
|
+
The container exposes the `Feature` class directly, so you can create your own features without any additional imports.
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<script type="module">
|
|
119
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
120
|
+
|
|
121
|
+
const { Feature } = container
|
|
122
|
+
|
|
123
|
+
class Theme extends Feature {
|
|
124
|
+
static shortcut = 'features.theme'
|
|
125
|
+
static { Feature.register(this, 'theme') }
|
|
126
|
+
|
|
127
|
+
get current() {
|
|
128
|
+
return this.state.get('mode') || 'light'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
toggle() {
|
|
132
|
+
const next = this.current === 'light' ? 'dark' : 'light'
|
|
133
|
+
this.state.set('mode', next)
|
|
134
|
+
document.documentElement.setAttribute('data-theme', next)
|
|
135
|
+
this.emit('themeChanged', { mode: next })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const theme = container.feature('theme')
|
|
140
|
+
theme.on('themeChanged', ({ mode }) => console.log('Theme:', mode))
|
|
141
|
+
theme.toggle() // => Theme: dark
|
|
142
|
+
</script>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Utilities
|
|
146
|
+
|
|
147
|
+
The container's built-in utilities are available in the browser too.
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<script type="module">
|
|
151
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
152
|
+
|
|
153
|
+
// UUID generation
|
|
154
|
+
const id = container.utils.uuid()
|
|
155
|
+
|
|
156
|
+
// Lodash helpers
|
|
157
|
+
const { groupBy, keyBy, pick } = container.utils.lodash
|
|
158
|
+
|
|
159
|
+
// String utilities
|
|
160
|
+
const { camelCase, kebabCase } = container.utils.stringUtils
|
|
161
|
+
</script>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Full Example: A Minimal App
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<!DOCTYPE html>
|
|
168
|
+
<html lang="en">
|
|
169
|
+
<head>
|
|
170
|
+
<meta charset="UTF-8">
|
|
171
|
+
<title>Luca Browser Demo</title>
|
|
172
|
+
</head>
|
|
173
|
+
<body>
|
|
174
|
+
<h1>Luca Browser Demo</h1>
|
|
175
|
+
<button id="speak">Speak</button>
|
|
176
|
+
<button id="theme">Toggle Theme</button>
|
|
177
|
+
<pre id="output"></pre>
|
|
178
|
+
|
|
179
|
+
<script type="module">
|
|
180
|
+
import container from "https://esm.sh/@soederpop/luca/web"
|
|
181
|
+
|
|
182
|
+
const log = (msg) => {
|
|
183
|
+
document.getElementById('output').textContent += msg + '\n'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Load a stylesheet
|
|
187
|
+
const assets = container.feature('assetLoader')
|
|
188
|
+
await assets.loadStylesheet('https://cdn.jsdelivr.net/npm/water.css@2/out/water.css')
|
|
189
|
+
|
|
190
|
+
// Custom feature
|
|
191
|
+
const { Feature } = container
|
|
192
|
+
|
|
193
|
+
class Theme extends Feature {
|
|
194
|
+
static shortcut = 'features.theme'
|
|
195
|
+
static { Feature.register(this, 'theme') }
|
|
196
|
+
|
|
197
|
+
toggle() {
|
|
198
|
+
const next = (this.state.get('mode') || 'light') === 'light' ? 'dark' : 'light'
|
|
199
|
+
this.state.set('mode', next)
|
|
200
|
+
document.documentElement.style.colorScheme = next
|
|
201
|
+
this.emit('themeChanged', { mode: next })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const theme = container.feature('theme')
|
|
206
|
+
theme.on('themeChanged', ({ mode }) => log(`Theme: ${mode}`))
|
|
207
|
+
|
|
208
|
+
// Speech
|
|
209
|
+
const speech = container.feature('speech')
|
|
210
|
+
|
|
211
|
+
document.getElementById('speak').onclick = () => speech.speak('Hello from Luca')
|
|
212
|
+
document.getElementById('theme').onclick = () => theme.toggle()
|
|
213
|
+
|
|
214
|
+
log(`Container ID: ${container.uuid}`)
|
|
215
|
+
log(`Features: ${container.features.available.join(', ')}`)
|
|
216
|
+
</script>
|
|
217
|
+
</body>
|
|
218
|
+
</html>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Save this as an HTML file, open it in a browser, and everything works — no npm, no bundler, no build step.
|
|
222
|
+
|
|
223
|
+
## Gotchas
|
|
224
|
+
|
|
225
|
+
- **esm.sh caches aggressively.** Pin a version if you need stability: `https://esm.sh/@soederpop/luca@0.0.29/web`
|
|
226
|
+
- **Browser features only.** The web container doesn't include node-specific features like `fs`, `git`, `proc`, or `docker`. If you need server features, run Luca on the server and connect via the REST or WebSocket clients.
|
|
227
|
+
- **`window.luca` is the singleton.** Don't call `createContainer()` — it just warns and returns the same instance. If you need isolation, use `container.subcontainer()`.
|
|
228
|
+
- **CORS applies.** REST client requests from the browser are subject to browser CORS rules. Your API must send the right headers.
|
|
229
|
+
|
|
230
|
+
## What's Next
|
|
231
|
+
|
|
232
|
+
- [State and Events](./05-state-and-events.md) — deep dive into the state machine and event bus (works identically in the browser)
|
|
233
|
+
- [Creating Features](./10-creating-features.md) — full anatomy of a feature with schemas, state, and events
|
|
234
|
+
- [Clients](./09-clients.md) — REST and WebSocket client APIs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soederpop/luca",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
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>",
|
|
@@ -11,6 +11,7 @@ import { Assistant } from './features/assistant'
|
|
|
11
11
|
import { AssistantsManager } from './features/assistants-manager'
|
|
12
12
|
import { DocsReader } from './features/docs-reader'
|
|
13
13
|
import { SkillsLibrary } from './features/skills-library'
|
|
14
|
+
import { BrowserUse } from './features/browser-use'
|
|
14
15
|
import { SemanticSearch } from '@soederpop/luca/node/features/semantic-search'
|
|
15
16
|
import { ContentDb } from '@soederpop/luca/node/features/content-db'
|
|
16
17
|
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
AssistantsManager,
|
|
27
28
|
DocsReader,
|
|
28
29
|
SkillsLibrary,
|
|
30
|
+
BrowserUse,
|
|
29
31
|
SemanticSearch,
|
|
30
32
|
ContentDb,
|
|
31
33
|
NodeContainer,
|
|
@@ -49,6 +51,7 @@ export interface AGIFeatures extends NodeFeatures {
|
|
|
49
51
|
assistantsManager: typeof AssistantsManager
|
|
50
52
|
docsReader: typeof DocsReader
|
|
51
53
|
skillsLibrary: typeof SkillsLibrary
|
|
54
|
+
browserUse: typeof BrowserUse
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export interface ConversationFactoryOptions {
|
|
@@ -121,6 +124,7 @@ const container = new AGIContainer()
|
|
|
121
124
|
.use(AssistantsManager)
|
|
122
125
|
.use(DocsReader)
|
|
123
126
|
.use(SkillsLibrary)
|
|
127
|
+
.use(BrowserUse)
|
|
124
128
|
.use(SemanticSearch)
|
|
125
129
|
|
|
126
130
|
container.docs = container.feature('contentDb', {
|
|
@@ -29,6 +29,7 @@ export const AssistantEventsSchema = FeatureEventsSchema.extend({
|
|
|
29
29
|
toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
|
|
30
30
|
toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
|
|
31
31
|
hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
|
|
32
|
+
reloaded: z.tuple([]).describe('Emitted after tools, hooks, and system prompt are reloaded from disk'),
|
|
32
33
|
systemPromptExtensionsChanged: z.tuple([]).describe('Emitted when system prompt extensions are added or removed'),
|
|
33
34
|
})
|
|
34
35
|
|
|
@@ -86,6 +87,15 @@ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
86
87
|
|
|
87
88
|
/** When true, prepend a timestamp to each user message so the assistant can track the passage of time across sessions */
|
|
88
89
|
injectTimestamps: z.boolean().default(false).describe('Prepend timestamps to user messages so the assistant can perceive time passing between sessions'),
|
|
90
|
+
|
|
91
|
+
/** Strict allowlist of tool names to include. Only these tools will be available. Supports "*" glob matching. */
|
|
92
|
+
allowTools: z.array(z.string()).optional().describe('Strict allowlist of tool name patterns. Only matching tools are available. Supports * glob matching.'),
|
|
93
|
+
|
|
94
|
+
/** Denylist of tool names to exclude. Matching tools will be removed. Supports "*" glob matching. */
|
|
95
|
+
forbidTools: z.array(z.string()).optional().describe('Denylist of tool name patterns to exclude. Supports * glob matching.'),
|
|
96
|
+
|
|
97
|
+
/** Convenience alias for allowTools — an explicit list of tool names (exact matches only). */
|
|
98
|
+
toolNames: z.array(z.string()).optional().describe('Explicit list of tool names to include (exact match). Shorthand for allowTools without glob patterns.'),
|
|
89
99
|
})
|
|
90
100
|
|
|
91
101
|
export type AssistantState = z.infer<typeof AssistantStateSchema>
|
|
@@ -334,7 +344,59 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
334
344
|
|
|
335
345
|
/** The tools registered with this assistant. */
|
|
336
346
|
get tools(): Record<string, ConversationTool> {
|
|
337
|
-
|
|
347
|
+
const all = (this.state.get('tools') || {}) as Record<string, ConversationTool>
|
|
348
|
+
return this.applyToolFilters(all)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Apply allowTools, forbidTools, and toolNames filters from options.
|
|
353
|
+
* toolNames is treated as an exact-match allowlist. allowTools/forbidTools support "*" glob patterns.
|
|
354
|
+
* allowTools is applied first (strict allowlist), then forbidTools removes from whatever remains.
|
|
355
|
+
*/
|
|
356
|
+
private applyToolFilters(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
|
|
357
|
+
const { allowTools, forbidTools, toolNames } = this.options
|
|
358
|
+
if (!allowTools && !forbidTools && !toolNames) return tools
|
|
359
|
+
|
|
360
|
+
let names = Object.keys(tools)
|
|
361
|
+
|
|
362
|
+
// toolNames is a strict exact-match allowlist
|
|
363
|
+
if (toolNames) {
|
|
364
|
+
const allowed = new Set(toolNames)
|
|
365
|
+
names = names.filter(n => allowed.has(n))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// allowTools: only keep names matching at least one pattern
|
|
369
|
+
if (allowTools) {
|
|
370
|
+
names = names.filter(n => allowTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// forbidTools: remove names matching any pattern
|
|
374
|
+
if (forbidTools) {
|
|
375
|
+
names = names.filter(n => !forbidTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result: Record<string, ConversationTool> = {}
|
|
379
|
+
for (const n of names) {
|
|
380
|
+
result[n] = tools[n]
|
|
381
|
+
}
|
|
382
|
+
return result
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Match a tool name against a pattern that supports "*" as a wildcard.
|
|
387
|
+
* - "*" matches everything
|
|
388
|
+
* - "prefix*" matches names starting with prefix
|
|
389
|
+
* - "*suffix" matches names ending with suffix
|
|
390
|
+
* - "pre*suf" matches names starting with pre and ending with suf
|
|
391
|
+
* - exact string matches exactly
|
|
392
|
+
*/
|
|
393
|
+
private matchToolPattern(pattern: string, name: string): boolean {
|
|
394
|
+
if (pattern === '*') return true
|
|
395
|
+
if (!pattern.includes('*')) return pattern === name
|
|
396
|
+
|
|
397
|
+
// Convert glob pattern to regex: escape regex chars, replace * with .*
|
|
398
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
|
|
399
|
+
return new RegExp(`^${escaped}$`).test(name)
|
|
338
400
|
}
|
|
339
401
|
|
|
340
402
|
/**
|
|
@@ -400,6 +462,8 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
400
462
|
*/
|
|
401
463
|
addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this {
|
|
402
464
|
if (!name) throw new Error('addTool handler must be a named function')
|
|
465
|
+
if (!this._runtimeToolNames) this._runtimeToolNames = new Set()
|
|
466
|
+
this._runtimeToolNames.add(name)
|
|
403
467
|
|
|
404
468
|
const current = { ...this.tools }
|
|
405
469
|
|
|
@@ -443,10 +507,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
443
507
|
|
|
444
508
|
if (typeof nameOrHandler === 'string') {
|
|
445
509
|
delete current[nameOrHandler]
|
|
510
|
+
this._runtimeToolNames?.delete(nameOrHandler)
|
|
446
511
|
} else {
|
|
447
512
|
for (const [name, tool] of Object.entries(current)) {
|
|
448
513
|
if (tool.handler === nameOrHandler) {
|
|
449
514
|
delete current[name]
|
|
515
|
+
this._runtimeToolNames?.delete(name)
|
|
450
516
|
break
|
|
451
517
|
}
|
|
452
518
|
}
|
|
@@ -875,17 +941,68 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
875
941
|
*/
|
|
876
942
|
/** Hook names that are called directly during lifecycle, not bound as event listeners. */
|
|
877
943
|
private static lifecycleHooks = new Set(['formatSystemPrompt'])
|
|
944
|
+
/** Stored references to bound hook listeners so they can be unbound on reload. Lazily initialized because afterInitialize runs before field initializers. */
|
|
945
|
+
private _boundHookListeners!: Array<{ event: string; listener: (...args: any[]) => void }>
|
|
946
|
+
/** Tool names added at runtime via addTool()/use(), so reload() can preserve them. */
|
|
947
|
+
private _runtimeToolNames!: Set<string>
|
|
878
948
|
|
|
879
949
|
private bindHooksToEvents() {
|
|
950
|
+
if (!this._boundHookListeners) this._boundHookListeners = []
|
|
880
951
|
const assistant = this
|
|
881
952
|
const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
|
|
882
953
|
for (const [eventName, hookFn] of Object.entries(hooks)) {
|
|
883
954
|
if (Assistant.lifecycleHooks.has(eventName)) continue
|
|
884
|
-
|
|
955
|
+
const listener = (...args: any[]) => {
|
|
885
956
|
this.emit('hookFired', eventName)
|
|
886
957
|
hookFn(assistant, ...args)
|
|
887
|
-
}
|
|
958
|
+
}
|
|
959
|
+
this._boundHookListeners.push({ event: eventName, listener })
|
|
960
|
+
this.on(eventName as any, listener)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
private unbindHooksFromEvents() {
|
|
965
|
+
for (const { event, listener } of this._boundHookListeners) {
|
|
966
|
+
this.off(event as any, listener)
|
|
967
|
+
}
|
|
968
|
+
this._boundHookListeners = []
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Reload tools, hooks, and system prompt from disk. Useful during development
|
|
973
|
+
* or when tool/hook files have been modified and you want the assistant to
|
|
974
|
+
* pick up changes without restarting.
|
|
975
|
+
*
|
|
976
|
+
* @returns this, for chaining
|
|
977
|
+
*/
|
|
978
|
+
reload(): this {
|
|
979
|
+
// Unbind existing hook listeners
|
|
980
|
+
this.unbindHooksFromEvents()
|
|
981
|
+
|
|
982
|
+
// Snapshot runtime-added tools before reloading from disk
|
|
983
|
+
const runtimeTools: Record<string, ConversationTool> = {}
|
|
984
|
+
if (this._runtimeToolNames?.size) {
|
|
985
|
+
const current = this.tools
|
|
986
|
+
for (const name of this._runtimeToolNames) {
|
|
987
|
+
if (current[name]) runtimeTools[name] = current[name]
|
|
988
|
+
}
|
|
888
989
|
}
|
|
990
|
+
|
|
991
|
+
// Reload system prompt from disk
|
|
992
|
+
this.state.set('systemPrompt', this.loadSystemPrompt())
|
|
993
|
+
|
|
994
|
+
// Reload tools from disk (merges with option tools), then restore runtime tools
|
|
995
|
+
const diskTools = this.loadTools()
|
|
996
|
+
this.state.set('tools', { ...diskTools, ...runtimeTools })
|
|
997
|
+
this.emit('toolsChanged')
|
|
998
|
+
|
|
999
|
+
// Reload hooks from disk and rebind
|
|
1000
|
+
this.state.set('hooks', this.loadHooks())
|
|
1001
|
+
this.bindHooksToEvents()
|
|
1002
|
+
|
|
1003
|
+
this.emit('reloaded')
|
|
1004
|
+
|
|
1005
|
+
return this
|
|
889
1006
|
}
|
|
890
1007
|
|
|
891
1008
|
/**
|