@soederpop/luca 0.0.23 → 0.0.25
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/AGENTS.md +1 -1
- package/CLAUDE.md +6 -1
- package/assistants/codingAssistant/hooks.ts +0 -1
- package/assistants/lucaExpert/CORE.md +37 -0
- package/assistants/lucaExpert/hooks.ts +9 -0
- package/assistants/lucaExpert/tools.ts +177 -0
- package/commands/build-bootstrap.ts +41 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -1
- package/docs/apis/clients/rest.md +5 -5
- package/docs/apis/features/agi/assistant.md +1 -1
- package/docs/apis/features/agi/conversation-history.md +6 -7
- package/docs/apis/features/agi/conversation.md +1 -1
- package/docs/apis/features/agi/semantic-search.md +1 -1
- package/docs/bootstrap/CLAUDE.md +1 -1
- package/docs/bootstrap/SKILL.md +7 -3
- package/docs/bootstrap/templates/luca-cli.ts +5 -0
- package/docs/mcp/readme.md +1 -1
- package/docs/tutorials/00-bootstrap.md +18 -0
- package/package.json +2 -2
- package/scripts/stamp-build.sh +12 -0
- package/scripts/test-docs-reader.ts +10 -0
- package/src/agi/container.server.ts +8 -5
- package/src/agi/features/assistant.ts +208 -55
- package/src/agi/features/assistants-manager.ts +138 -66
- package/src/agi/features/conversation.ts +46 -14
- package/src/agi/features/docs-reader.ts +142 -0
- package/src/agi/features/openapi.ts +1 -1
- package/src/agi/features/skills-library.ts +257 -313
- package/src/bootstrap/generated.ts +8163 -6
- package/src/cli/build-info.ts +4 -0
- package/src/cli/cli.ts +2 -1
- package/src/commands/bootstrap.ts +16 -1
- package/src/commands/eval.ts +6 -1
- package/src/commands/sandbox-mcp.ts +17 -7
- package/src/helper.ts +56 -2
- package/src/introspection/generated.agi.ts +2409 -1608
- package/src/introspection/generated.node.ts +902 -594
- package/src/introspection/generated.web.ts +1 -1
- package/src/node/container.ts +1 -1
- package/src/node/features/content-db.ts +251 -13
- package/src/node/features/git.ts +90 -0
- package/src/node/features/grep.ts +1 -1
- package/src/node/features/proc.ts +1 -0
- package/src/node/features/tts.ts +1 -1
- package/src/node/features/vm.ts +48 -0
- package/src/scaffolds/generated.ts +2 -2
- package/assistants/architect/CORE.md +0 -3
- package/assistants/architect/hooks.ts +0 -3
- package/assistants/architect/tools.ts +0 -10
- package/docs/apis/features/agi/skills-library.md +0 -234
- package/docs/reports/assistant-bugs.md +0 -38
- package/docs/reports/attach-pattern-usage.md +0 -18
- package/docs/reports/code-audit-results.md +0 -391
- package/docs/reports/console-hmr-design.md +0 -170
- package/docs/reports/helper-semantic-search.md +0 -72
- package/docs/reports/introspection-audit-tasks.md +0 -378
- package/docs/reports/luca-mcp-improvements.md +0 -128
- package/test-integration/skills-library.test.ts +0 -157
|
@@ -38,15 +38,26 @@ export const AssistantStateSchema = FeatureStateSchema.extend({
|
|
|
38
38
|
docsFolder: z.string().describe('The resolved docs folder'),
|
|
39
39
|
conversationId: z.string().optional().describe('The active conversation persistence ID'),
|
|
40
40
|
threadId: z.string().optional().describe('The active thread ID'),
|
|
41
|
+
systemPrompt: z.string().describe('The loaded system prompt text'),
|
|
42
|
+
meta: z.record(z.string(), z.any()).describe('Parsed YAML frontmatter from CORE.md'),
|
|
43
|
+
tools: z.record(z.string(), z.any()).describe('Registered tool implementations'),
|
|
44
|
+
hooks: z.record(z.string(), z.any()).describe('Loaded event hook functions'),
|
|
45
|
+
resumeThreadId: z.string().optional().describe('Thread ID override for resume'),
|
|
46
|
+
pendingPlugins: z.array(z.any()).describe('Pending async plugin promises'),
|
|
47
|
+
conversation: z.any().nullable().describe('The active Conversation feature instance'),
|
|
48
|
+
subagents: z.record(z.string(), z.any()).describe('Cached subagent instances'),
|
|
41
49
|
})
|
|
42
50
|
|
|
43
51
|
export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
44
|
-
/** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts) */
|
|
45
|
-
folder: z.string().describe('The folder containing the assistant definition'),
|
|
52
|
+
/** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts). Optional for runtime-created assistants. */
|
|
53
|
+
folder: z.string().default('.').describe('The folder containing the assistant definition. Defaults to cwd for runtime-created assistants.'),
|
|
46
54
|
|
|
47
55
|
/** If the docs folder is different from folder/docs */
|
|
48
56
|
docsFolder: z.string().optional().describe('The folder containing the assistant documentation'),
|
|
49
57
|
|
|
58
|
+
/** Provide a complete system prompt directly, bypassing CORE.md. Useful for runtime-created assistants. */
|
|
59
|
+
systemPrompt: z.string().optional().describe('Provide a complete system prompt directly, bypassing CORE.md'),
|
|
60
|
+
|
|
50
61
|
/** Text to prepend to the system prompt from CORE.md */
|
|
51
62
|
prependPrompt: z.string().optional().describe('Text to prepend to the system prompt'),
|
|
52
63
|
|
|
@@ -108,6 +119,14 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
108
119
|
conversationCount: 0,
|
|
109
120
|
lastResponse: '',
|
|
110
121
|
folder: this.resolvedFolder,
|
|
122
|
+
systemPrompt: '',
|
|
123
|
+
meta: {},
|
|
124
|
+
tools: {},
|
|
125
|
+
hooks: {},
|
|
126
|
+
resumeThreadId: undefined,
|
|
127
|
+
pendingPlugins: [],
|
|
128
|
+
conversation: null,
|
|
129
|
+
subagents: {},
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
|
|
@@ -177,15 +196,6 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
177
196
|
return this.container.feature('contentDb', { rootPath: this.resolvedDocsFolder })
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
private _conversation?: Conversation
|
|
181
|
-
private _resumeThreadId?: string
|
|
182
|
-
|
|
183
|
-
// Using `declare` to prevent class field initializers from overwriting
|
|
184
|
-
// values set during afterInitialize() (called from the base constructor).
|
|
185
|
-
declare private _tools: Record<string, ConversationTool>
|
|
186
|
-
declare private _hooks: Record<string, (...args: any[]) => any>
|
|
187
|
-
declare private _systemPrompt: string
|
|
188
|
-
declare private _pendingPlugins: Promise<void>[]
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
201
|
* Called immediately after the assistant is constructed. Synchronously loads
|
|
@@ -193,14 +203,14 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
193
203
|
* so every emitted event automatically invokes its corresponding hook.
|
|
194
204
|
*/
|
|
195
205
|
override afterInitialize() {
|
|
196
|
-
this.
|
|
206
|
+
this.state.set('pendingPlugins', [])
|
|
197
207
|
|
|
198
208
|
// Load system prompt synchronously
|
|
199
|
-
this.
|
|
209
|
+
this.state.set('systemPrompt', this.loadSystemPrompt())
|
|
200
210
|
|
|
201
211
|
// Load tools and hooks synchronously via vm.performSync
|
|
202
|
-
this.
|
|
203
|
-
this.
|
|
212
|
+
this.state.set('tools', this.loadTools())
|
|
213
|
+
this.state.set('hooks', this.loadHooks())
|
|
204
214
|
|
|
205
215
|
// Bind hooks to events BEFORE emitting created so the created hook fires
|
|
206
216
|
this.bindHooksToEvents()
|
|
@@ -209,18 +219,25 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
get conversation(): Conversation {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
let conv = this.state.get('conversation') as Conversation | null
|
|
223
|
+
if (!conv) {
|
|
224
|
+
conv = this.container.feature('conversation', {
|
|
225
|
+
model: this.options.model || 'gpt-5.4',
|
|
215
226
|
local: !!this.options.local,
|
|
216
|
-
tools: this.
|
|
227
|
+
tools: this.tools,
|
|
228
|
+
api: 'chat',
|
|
217
229
|
...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
|
|
218
230
|
history: [
|
|
219
|
-
{ role: 'system', content: this.
|
|
231
|
+
{ role: 'system', content: this.systemPrompt || this.loadSystemPrompt() },
|
|
220
232
|
],
|
|
221
233
|
})
|
|
234
|
+
this.state.set('conversation', conv)
|
|
222
235
|
}
|
|
223
|
-
return
|
|
236
|
+
return conv
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get availableTools() {
|
|
240
|
+
return Object.keys(this.tools)
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
get messages() {
|
|
@@ -234,32 +251,51 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
234
251
|
|
|
235
252
|
/** The current system prompt text. */
|
|
236
253
|
get systemPrompt(): string {
|
|
237
|
-
return this.
|
|
254
|
+
return this.state.get('systemPrompt') || ''
|
|
238
255
|
}
|
|
239
256
|
|
|
240
257
|
/** The tools registered with this assistant. */
|
|
241
258
|
get tools(): Record<string, ConversationTool> {
|
|
242
|
-
return this.
|
|
259
|
+
return (this.state.get('tools') || {}) as Record<string, ConversationTool>
|
|
243
260
|
}
|
|
244
261
|
|
|
245
262
|
/**
|
|
246
|
-
* Apply a setup function to this assistant.
|
|
247
|
-
* assistant instance and can configure tools, hooks, event listeners, etc.
|
|
263
|
+
* Apply a setup function or a Helper instance to this assistant.
|
|
248
264
|
*
|
|
249
|
-
*
|
|
265
|
+
* When passed a function, it receives the assistant and can configure
|
|
266
|
+
* tools, hooks, event listeners, etc.
|
|
267
|
+
*
|
|
268
|
+
* When passed a Helper instance that exposes tools via toTools(),
|
|
269
|
+
* those tools are automatically added to this assistant.
|
|
270
|
+
*
|
|
271
|
+
* @param fnOrHelper - Setup function or Helper instance
|
|
250
272
|
* @returns this, for chaining
|
|
251
273
|
*
|
|
252
274
|
* @example
|
|
253
275
|
* ```typescript
|
|
254
276
|
* assistant
|
|
255
277
|
* .use(setupLogging)
|
|
256
|
-
* .use(
|
|
278
|
+
* .use(container.feature('git'))
|
|
257
279
|
* ```
|
|
258
280
|
*/
|
|
259
|
-
use(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } }): this {
|
|
282
|
+
if (typeof fnOrHelper === 'function') {
|
|
283
|
+
const result = fnOrHelper(this)
|
|
284
|
+
if (result && typeof (result as any).then === 'function') {
|
|
285
|
+
const pending = this.state.get('pendingPlugins') as Promise<void>[]
|
|
286
|
+
this.state.set('pendingPlugins', [...pending, result as Promise<void>])
|
|
287
|
+
}
|
|
288
|
+
} else if (fnOrHelper && typeof fnOrHelper.toTools === 'function') {
|
|
289
|
+
const { schemas, handlers } = fnOrHelper.toTools()
|
|
290
|
+
for (const name of Object.keys(schemas)) {
|
|
291
|
+
if(typeof handlers[name] === 'function') {
|
|
292
|
+
this.addTool(
|
|
293
|
+
name,
|
|
294
|
+
handlers[name] as any,
|
|
295
|
+
schemas[name],
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
263
299
|
}
|
|
264
300
|
return this
|
|
265
301
|
}
|
|
@@ -279,29 +315,37 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
279
315
|
* }, z.object({ city: z.string() }).describe('Get weather for a city'))
|
|
280
316
|
* ```
|
|
281
317
|
*/
|
|
282
|
-
addTool(handler: (...args: any[]) => any, schema?: z.ZodType): this {
|
|
283
|
-
const name = handler.name
|
|
318
|
+
addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this {
|
|
284
319
|
if (!name) throw new Error('addTool handler must be a named function')
|
|
285
320
|
|
|
321
|
+
const current = { ...this.tools }
|
|
322
|
+
|
|
286
323
|
if (schema) {
|
|
287
324
|
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
288
|
-
|
|
325
|
+
// OpenAI requires `required` to list ALL property keys — optional params
|
|
326
|
+
// must still appear in `required` but use a default value in the schema.
|
|
327
|
+
const properties = jsonSchema.properties || {}
|
|
328
|
+
const required = Object.keys(properties)
|
|
329
|
+
current[name] = {
|
|
289
330
|
handler: handler as ConversationTool['handler'],
|
|
290
331
|
description: jsonSchema.description || name,
|
|
291
332
|
parameters: {
|
|
292
333
|
type: jsonSchema.type || 'object',
|
|
293
|
-
properties
|
|
294
|
-
|
|
334
|
+
properties,
|
|
335
|
+
required,
|
|
295
336
|
},
|
|
296
337
|
}
|
|
297
338
|
} else {
|
|
298
|
-
|
|
339
|
+
current[name] = {
|
|
299
340
|
handler: handler as ConversationTool['handler'],
|
|
300
341
|
description: name,
|
|
301
342
|
parameters: { type: 'object', properties: {} },
|
|
302
343
|
}
|
|
303
344
|
}
|
|
304
345
|
|
|
346
|
+
this.state.set('tools', current)
|
|
347
|
+
this.emit('toolsChanged')
|
|
348
|
+
|
|
305
349
|
return this
|
|
306
350
|
}
|
|
307
351
|
|
|
@@ -312,17 +356,22 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
312
356
|
* @returns this, for chaining
|
|
313
357
|
*/
|
|
314
358
|
removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
|
|
359
|
+
const current = { ...this.tools }
|
|
360
|
+
|
|
315
361
|
if (typeof nameOrHandler === 'string') {
|
|
316
|
-
delete
|
|
362
|
+
delete current[nameOrHandler]
|
|
317
363
|
} else {
|
|
318
|
-
for (const [name, tool] of Object.entries(
|
|
364
|
+
for (const [name, tool] of Object.entries(current)) {
|
|
319
365
|
if (tool.handler === nameOrHandler) {
|
|
320
|
-
delete
|
|
366
|
+
delete current[name]
|
|
321
367
|
break
|
|
322
368
|
}
|
|
323
369
|
}
|
|
324
370
|
}
|
|
325
371
|
|
|
372
|
+
this.state.set('tools', current)
|
|
373
|
+
this.emit('toolsChanged')
|
|
374
|
+
|
|
326
375
|
return this
|
|
327
376
|
}
|
|
328
377
|
|
|
@@ -384,17 +433,38 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
384
433
|
return this
|
|
385
434
|
}
|
|
386
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Parsed YAML frontmatter from CORE.md, or empty object if none.
|
|
438
|
+
*/
|
|
439
|
+
get meta(): Record<string, any> {
|
|
440
|
+
return (this.state.get('meta') || {}) as Record<string, any>
|
|
441
|
+
}
|
|
442
|
+
|
|
387
443
|
/**
|
|
388
444
|
* Load the system prompt from CORE.md, applying any prepend/append options.
|
|
445
|
+
* YAML frontmatter (between --- fences) is stripped from the prompt and
|
|
446
|
+
* stored in `_meta`.
|
|
389
447
|
*
|
|
390
448
|
* @returns {string} The assembled system prompt
|
|
391
449
|
*/
|
|
392
450
|
loadSystemPrompt(): string {
|
|
393
451
|
const { fs } = this.container
|
|
394
452
|
let prompt = ''
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
453
|
+
this.state.set('meta', {})
|
|
454
|
+
|
|
455
|
+
if (this.options.systemPrompt) {
|
|
456
|
+
prompt = this.options.systemPrompt
|
|
457
|
+
} else if (fs.exists(this.corePromptPath)) {
|
|
458
|
+
const raw = fs.readFile(this.corePromptPath).toString()
|
|
459
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
460
|
+
|
|
461
|
+
if (fmMatch) {
|
|
462
|
+
const yaml = this.container.feature('yaml')
|
|
463
|
+
this.state.set('meta', yaml.parse(fmMatch[1]!) ?? {})
|
|
464
|
+
prompt = raw.slice(fmMatch[0].length)
|
|
465
|
+
} else {
|
|
466
|
+
prompt = raw
|
|
467
|
+
}
|
|
398
468
|
}
|
|
399
469
|
|
|
400
470
|
if (this.options.prependPrompt) {
|
|
@@ -426,6 +496,19 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
426
496
|
*/
|
|
427
497
|
loadTools(): Record<string, ConversationTool> {
|
|
428
498
|
const tools: Record<string, ConversationTool> = {}
|
|
499
|
+
|
|
500
|
+
// Skip loading if no tools file exists (runtime-created assistants)
|
|
501
|
+
if (!this.container.fs.exists(this.toolsModulePath)) {
|
|
502
|
+
return this.mergeOptionTools(tools)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Ensure virtual modules (zod, @soederpop/luca, etc.) are seeded so tools
|
|
506
|
+
// files outside the project tree can resolve them through the VM
|
|
507
|
+
if (this.container.features.has('helpers')) {
|
|
508
|
+
const helpers = this.container.feature('helpers') as any
|
|
509
|
+
helpers.seedVirtualModules()
|
|
510
|
+
}
|
|
511
|
+
|
|
429
512
|
const vm = this.container.feature('vm')
|
|
430
513
|
|
|
431
514
|
let moduleExports: Record<string, any>
|
|
@@ -441,7 +524,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
441
524
|
console.error(`Failed to load tools from ${this.toolsModulePath}`)
|
|
442
525
|
console.error(`There may be a syntax error in this file. Please check it.`)
|
|
443
526
|
console.error(err.message || err)
|
|
444
|
-
return tools
|
|
527
|
+
return this.mergeOptionTools(tools)
|
|
445
528
|
}
|
|
446
529
|
|
|
447
530
|
if (Object.keys(moduleExports).length) {
|
|
@@ -472,7 +555,14 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
472
555
|
}
|
|
473
556
|
}
|
|
474
557
|
|
|
475
|
-
|
|
558
|
+
return this.mergeOptionTools(tools)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Merge tools provided via constructor options into the tool map.
|
|
563
|
+
* This allows runtime-created assistants to define tools entirely via options.
|
|
564
|
+
*/
|
|
565
|
+
private mergeOptionTools(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
|
|
476
566
|
if (this.options.tools) {
|
|
477
567
|
const optionSchemas = this.options.schemas || {}
|
|
478
568
|
|
|
@@ -513,6 +603,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
513
603
|
*/
|
|
514
604
|
loadHooks(): Record<string, (...args: any[]) => any> {
|
|
515
605
|
const hooks: Record<string, (...args: any[]) => any> = {}
|
|
606
|
+
|
|
607
|
+
// Skip loading if no hooks file exists (runtime-created assistants)
|
|
608
|
+
if (!this.container.fs.exists(this.hooksModulePath)) {
|
|
609
|
+
return hooks
|
|
610
|
+
}
|
|
611
|
+
|
|
516
612
|
const vm = this.container.feature('vm')
|
|
517
613
|
|
|
518
614
|
let moduleExports: Record<string, any>
|
|
@@ -626,7 +722,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
626
722
|
* @returns this, for chaining
|
|
627
723
|
*/
|
|
628
724
|
resumeThread(threadId: string): this {
|
|
629
|
-
this.
|
|
725
|
+
this.state.set('resumeThreadId', threadId)
|
|
630
726
|
return this
|
|
631
727
|
}
|
|
632
728
|
|
|
@@ -659,7 +755,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
659
755
|
const mode = this.options.historyMode || 'lifecycle'
|
|
660
756
|
if (mode === 'lifecycle') return
|
|
661
757
|
|
|
662
|
-
const threadId = this.
|
|
758
|
+
const threadId = (this.state.get('resumeThreadId') as string | undefined) || this.buildThreadId(mode)
|
|
663
759
|
this.state.set('threadId', threadId)
|
|
664
760
|
|
|
665
761
|
const existing = await this.conversationHistory.findByThread(threadId)
|
|
@@ -670,7 +766,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
670
766
|
|
|
671
767
|
// Swap in fresh system prompt if it changed
|
|
672
768
|
if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
|
|
673
|
-
messages[0] = { role: messages[0]!.role, content: this.
|
|
769
|
+
messages[0] = { role: messages[0]!.role, content: this.systemPrompt }
|
|
674
770
|
}
|
|
675
771
|
|
|
676
772
|
this.conversation.state.set('id', existing.id)
|
|
@@ -699,7 +795,8 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
699
795
|
|
|
700
796
|
private bindHooksToEvents() {
|
|
701
797
|
const assistant = this
|
|
702
|
-
|
|
798
|
+
const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
|
|
799
|
+
for (const [eventName, hookFn] of Object.entries(hooks)) {
|
|
703
800
|
if (Assistant.lifecycleHooks.has(eventName)) continue
|
|
704
801
|
this.on(eventName as any, (...args: any[]) => {
|
|
705
802
|
this.emit('hookFired', eventName)
|
|
@@ -720,17 +817,19 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
720
817
|
if (this.isStarted) return this
|
|
721
818
|
|
|
722
819
|
// Wait for any async .use() plugins to finish before starting
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
820
|
+
const pending = this.state.get('pendingPlugins') as Promise<void>[]
|
|
821
|
+
if (pending.length) {
|
|
822
|
+
await Promise.all(pending)
|
|
823
|
+
this.state.set('pendingPlugins', [])
|
|
726
824
|
}
|
|
727
825
|
|
|
728
826
|
// Allow hooks.ts to export a formatSystemPrompt(assistant, prompt) => string
|
|
729
827
|
// that transforms the system prompt before the conversation is created.
|
|
730
|
-
|
|
731
|
-
|
|
828
|
+
const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
|
|
829
|
+
if (hooks.formatSystemPrompt) {
|
|
830
|
+
const result = await hooks.formatSystemPrompt(this, this.systemPrompt)
|
|
732
831
|
if (typeof result === 'string') {
|
|
733
|
-
this.
|
|
832
|
+
this.state.set('systemPrompt', result)
|
|
734
833
|
}
|
|
735
834
|
}
|
|
736
835
|
|
|
@@ -758,6 +857,13 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
758
857
|
if (mode === 'daily' || mode === 'persistent') {
|
|
759
858
|
(this.conversation.options as any).autoCompact = true
|
|
760
859
|
}
|
|
860
|
+
|
|
861
|
+
this.on('toolsChanged', () => {
|
|
862
|
+
const conv = this.state.get('conversation') as Conversation | null
|
|
863
|
+
if (conv) {
|
|
864
|
+
conv.updateTools(this.tools)
|
|
865
|
+
}
|
|
866
|
+
})
|
|
761
867
|
|
|
762
868
|
this.state.set('started', true)
|
|
763
869
|
this.emit('started')
|
|
@@ -818,6 +924,53 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
818
924
|
|
|
819
925
|
return this.conversation.save(opts)
|
|
820
926
|
}
|
|
927
|
+
|
|
928
|
+
// -- Subagent API --
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Names of assistants available as subagents, discovered via the assistantsManager.
|
|
932
|
+
*
|
|
933
|
+
* @returns {string[]} Available assistant names
|
|
934
|
+
*/
|
|
935
|
+
get availableSubagents(): string[] {
|
|
936
|
+
try {
|
|
937
|
+
const manager = this.container.feature('assistantsManager')
|
|
938
|
+
return manager.available
|
|
939
|
+
} catch {
|
|
940
|
+
return []
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Get or create a subagent assistant. Uses the assistantsManager to discover
|
|
946
|
+
* and create the assistant, then caches the instance for reuse across tool calls.
|
|
947
|
+
*
|
|
948
|
+
* @param id - The assistant name (e.g. 'codingAssistant')
|
|
949
|
+
* @param options - Additional options to pass to the assistant constructor
|
|
950
|
+
* @returns {Promise<Assistant>} The subagent assistant instance, started and ready
|
|
951
|
+
*
|
|
952
|
+
* @example
|
|
953
|
+
* ```typescript
|
|
954
|
+
* const researcher = await assistant.subagent('codingAssistant')
|
|
955
|
+
* const answer = await researcher.ask('Find all usages of container.feature("fs")')
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
async subagent(id: string, options: Record<string, any> = {}): Promise<Assistant> {
|
|
959
|
+
const subagents = (this.state.get('subagents') || {}) as Record<string, Assistant>
|
|
960
|
+
if (subagents[id]) return subagents[id]
|
|
961
|
+
|
|
962
|
+
const manager = this.container.feature('assistantsManager')
|
|
963
|
+
|
|
964
|
+
if (!manager.state.get('discovered')) {
|
|
965
|
+
await manager.discover()
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const instance = manager.create(id, options)
|
|
969
|
+
await instance.start()
|
|
970
|
+
|
|
971
|
+
this.state.set('subagents', { ...subagents, [id]: instance })
|
|
972
|
+
return instance
|
|
973
|
+
}
|
|
821
974
|
}
|
|
822
975
|
|
|
823
976
|
export default Assistant
|