@soederpop/luca 0.0.23 → 0.0.26

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 (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +6 -1
  3. package/assistants/codingAssistant/hooks.ts +0 -1
  4. package/assistants/lucaExpert/CORE.md +37 -0
  5. package/assistants/lucaExpert/hooks.ts +9 -0
  6. package/assistants/lucaExpert/tools.ts +177 -0
  7. package/commands/build-bootstrap.ts +41 -1
  8. package/docs/TABLE-OF-CONTENTS.md +0 -1
  9. package/docs/apis/clients/rest.md +5 -5
  10. package/docs/apis/features/agi/assistant.md +1 -1
  11. package/docs/apis/features/agi/conversation-history.md +6 -7
  12. package/docs/apis/features/agi/conversation.md +1 -1
  13. package/docs/apis/features/agi/semantic-search.md +1 -1
  14. package/docs/bootstrap/CLAUDE.md +1 -1
  15. package/docs/bootstrap/SKILL.md +7 -3
  16. package/docs/bootstrap/templates/luca-cli.ts +5 -0
  17. package/docs/mcp/readme.md +1 -1
  18. package/docs/tutorials/00-bootstrap.md +18 -0
  19. package/package.json +2 -2
  20. package/scripts/stamp-build.sh +12 -0
  21. package/scripts/test-docs-reader.ts +10 -0
  22. package/src/agi/container.server.ts +8 -5
  23. package/src/agi/features/assistant.ts +210 -55
  24. package/src/agi/features/assistants-manager.ts +138 -66
  25. package/src/agi/features/conversation.ts +46 -14
  26. package/src/agi/features/docs-reader.ts +166 -0
  27. package/src/agi/features/openapi.ts +1 -1
  28. package/src/agi/features/skills-library.ts +257 -313
  29. package/src/bootstrap/generated.ts +8163 -6
  30. package/src/cli/build-info.ts +4 -0
  31. package/src/cli/cli.ts +2 -1
  32. package/src/command.ts +75 -0
  33. package/src/commands/bootstrap.ts +16 -1
  34. package/src/commands/describe.ts +29 -1089
  35. package/src/commands/eval.ts +6 -1
  36. package/src/commands/sandbox-mcp.ts +17 -7
  37. package/src/container-describer.ts +1098 -0
  38. package/src/container.ts +11 -0
  39. package/src/helper.ts +56 -2
  40. package/src/introspection/generated.agi.ts +1684 -799
  41. package/src/introspection/generated.node.ts +964 -572
  42. package/src/introspection/generated.web.ts +9 -1
  43. package/src/node/container.ts +1 -1
  44. package/src/node/features/content-db.ts +268 -13
  45. package/src/node/features/fs.ts +18 -0
  46. package/src/node/features/git.ts +90 -0
  47. package/src/node/features/grep.ts +1 -1
  48. package/src/node/features/proc.ts +1 -0
  49. package/src/node/features/tts.ts +1 -1
  50. package/src/node/features/vm.ts +48 -0
  51. package/src/scaffolds/generated.ts +2 -2
  52. package/src/server.ts +40 -0
  53. package/src/servers/express.ts +2 -0
  54. package/src/servers/mcp.ts +1 -0
  55. package/src/servers/socket.ts +2 -0
  56. package/assistants/architect/CORE.md +0 -3
  57. package/assistants/architect/hooks.ts +0 -3
  58. package/assistants/architect/tools.ts +0 -10
  59. package/docs/apis/features/agi/skills-library.md +0 -234
  60. package/docs/reports/assistant-bugs.md +0 -38
  61. package/docs/reports/attach-pattern-usage.md +0 -18
  62. package/docs/reports/code-audit-results.md +0 -391
  63. package/docs/reports/console-hmr-design.md +0 -170
  64. package/docs/reports/helper-semantic-search.md +0 -72
  65. package/docs/reports/introspection-audit-tasks.md +0 -378
  66. package/docs/reports/luca-mcp-improvements.md +0 -128
  67. 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._pendingPlugins = []
206
+ this.state.set('pendingPlugins', [])
197
207
 
198
208
  // Load system prompt synchronously
199
- this._systemPrompt = this.loadSystemPrompt()
209
+ this.state.set('systemPrompt', this.loadSystemPrompt())
200
210
 
201
211
  // Load tools and hooks synchronously via vm.performSync
202
- this._tools = this.loadTools()
203
- this._hooks = this.loadHooks()
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
- if (!this._conversation) {
213
- this._conversation = this.container.feature('conversation', {
214
- model: this.options.model || 'gpt-5.2',
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._tools || this.loadTools(),
227
+ tools: this.tools,
228
+ api: 'chat',
217
229
  ...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
218
230
  history: [
219
- { role: 'system', content: this._systemPrompt || this.loadSystemPrompt() },
231
+ { role: 'system', content: this.systemPrompt || this.loadSystemPrompt() },
220
232
  ],
221
233
  })
234
+ this.state.set('conversation', conv)
222
235
  }
223
- return this._conversation
236
+ return conv
237
+ }
238
+
239
+ get availableTools() {
240
+ return Object.keys(this.tools)
224
241
  }
225
242
 
226
243
  get messages() {
@@ -234,36 +251,57 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
234
251
 
235
252
  /** The current system prompt text. */
236
253
  get systemPrompt(): string {
237
- return this._systemPrompt
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._tools
259
+ return (this.state.get('tools') || {}) as Record<string, ConversationTool>
243
260
  }
244
261
 
245
262
  /**
246
- * Apply a setup function to this assistant. The function receives the
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
- * @param fn - Setup function that receives this assistant
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(addAnalyticsTools)
278
+ * .use(container.feature('git'))
257
279
  * ```
258
280
  */
259
- use(fn: (assistant: this) => void | Promise<void>): this {
260
- const result = fn(this)
261
- if (result && typeof (result as any).then === 'function') {
262
- this._pendingPlugins.push(result as Promise<void>)
281
+ use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } } | { 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 as any).toTools === 'function') {
289
+ this._registerTools((fnOrHelper as any).toTools())
290
+ } else if (fnOrHelper && 'schemas' in fnOrHelper && 'handlers' in fnOrHelper) {
291
+ this._registerTools(fnOrHelper as { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> })
263
292
  }
264
293
  return this
265
294
  }
266
295
 
296
+ /** Register tools from a `{ schemas, handlers }` object. */
297
+ private _registerTools({ schemas, handlers }: { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }) {
298
+ for (const name of Object.keys(schemas)) {
299
+ if (typeof handlers[name] === 'function') {
300
+ this.addTool(name, handlers[name] as any, schemas[name])
301
+ }
302
+ }
303
+ }
304
+
267
305
  /**
268
306
  * Add a tool to this assistant. The tool name is derived from the
269
307
  * handler's function name.
@@ -279,29 +317,37 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
279
317
  * }, z.object({ city: z.string() }).describe('Get weather for a city'))
280
318
  * ```
281
319
  */
282
- addTool(handler: (...args: any[]) => any, schema?: z.ZodType): this {
283
- const name = handler.name
320
+ addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this {
284
321
  if (!name) throw new Error('addTool handler must be a named function')
285
322
 
323
+ const current = { ...this.tools }
324
+
286
325
  if (schema) {
287
326
  const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
288
- this._tools[name] = {
327
+ // OpenAI requires `required` to list ALL property keys — optional params
328
+ // must still appear in `required` but use a default value in the schema.
329
+ const properties = jsonSchema.properties || {}
330
+ const required = Object.keys(properties)
331
+ current[name] = {
289
332
  handler: handler as ConversationTool['handler'],
290
333
  description: jsonSchema.description || name,
291
334
  parameters: {
292
335
  type: jsonSchema.type || 'object',
293
- properties: jsonSchema.properties || {},
294
- ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
336
+ properties,
337
+ required,
295
338
  },
296
339
  }
297
340
  } else {
298
- this._tools[name] = {
341
+ current[name] = {
299
342
  handler: handler as ConversationTool['handler'],
300
343
  description: name,
301
344
  parameters: { type: 'object', properties: {} },
302
345
  }
303
346
  }
304
347
 
348
+ this.state.set('tools', current)
349
+ this.emit('toolsChanged')
350
+
305
351
  return this
306
352
  }
307
353
 
@@ -312,17 +358,22 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
312
358
  * @returns this, for chaining
313
359
  */
314
360
  removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
361
+ const current = { ...this.tools }
362
+
315
363
  if (typeof nameOrHandler === 'string') {
316
- delete this._tools[nameOrHandler]
364
+ delete current[nameOrHandler]
317
365
  } else {
318
- for (const [name, tool] of Object.entries(this._tools)) {
366
+ for (const [name, tool] of Object.entries(current)) {
319
367
  if (tool.handler === nameOrHandler) {
320
- delete this._tools[name]
368
+ delete current[name]
321
369
  break
322
370
  }
323
371
  }
324
372
  }
325
373
 
374
+ this.state.set('tools', current)
375
+ this.emit('toolsChanged')
376
+
326
377
  return this
327
378
  }
328
379
 
@@ -384,17 +435,38 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
384
435
  return this
385
436
  }
386
437
 
438
+ /**
439
+ * Parsed YAML frontmatter from CORE.md, or empty object if none.
440
+ */
441
+ get meta(): Record<string, any> {
442
+ return (this.state.get('meta') || {}) as Record<string, any>
443
+ }
444
+
387
445
  /**
388
446
  * Load the system prompt from CORE.md, applying any prepend/append options.
447
+ * YAML frontmatter (between --- fences) is stripped from the prompt and
448
+ * stored in `_meta`.
389
449
  *
390
450
  * @returns {string} The assembled system prompt
391
451
  */
392
452
  loadSystemPrompt(): string {
393
453
  const { fs } = this.container
394
454
  let prompt = ''
395
-
396
- if (fs.exists(this.corePromptPath)) {
397
- prompt = fs.readFile(this.corePromptPath)
455
+ this.state.set('meta', {})
456
+
457
+ if (this.options.systemPrompt) {
458
+ prompt = this.options.systemPrompt
459
+ } else if (fs.exists(this.corePromptPath)) {
460
+ const raw = fs.readFile(this.corePromptPath).toString()
461
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
462
+
463
+ if (fmMatch) {
464
+ const yaml = this.container.feature('yaml')
465
+ this.state.set('meta', yaml.parse(fmMatch[1]!) ?? {})
466
+ prompt = raw.slice(fmMatch[0].length)
467
+ } else {
468
+ prompt = raw
469
+ }
398
470
  }
399
471
 
400
472
  if (this.options.prependPrompt) {
@@ -426,6 +498,19 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
426
498
  */
427
499
  loadTools(): Record<string, ConversationTool> {
428
500
  const tools: Record<string, ConversationTool> = {}
501
+
502
+ // Skip loading if no tools file exists (runtime-created assistants)
503
+ if (!this.container.fs.exists(this.toolsModulePath)) {
504
+ return this.mergeOptionTools(tools)
505
+ }
506
+
507
+ // Ensure virtual modules (zod, @soederpop/luca, etc.) are seeded so tools
508
+ // files outside the project tree can resolve them through the VM
509
+ if (this.container.features.has('helpers')) {
510
+ const helpers = this.container.feature('helpers') as any
511
+ helpers.seedVirtualModules()
512
+ }
513
+
429
514
  const vm = this.container.feature('vm')
430
515
 
431
516
  let moduleExports: Record<string, any>
@@ -441,7 +526,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
441
526
  console.error(`Failed to load tools from ${this.toolsModulePath}`)
442
527
  console.error(`There may be a syntax error in this file. Please check it.`)
443
528
  console.error(err.message || err)
444
- return tools
529
+ return this.mergeOptionTools(tools)
445
530
  }
446
531
 
447
532
  if (Object.keys(moduleExports).length) {
@@ -472,7 +557,14 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
472
557
  }
473
558
  }
474
559
 
475
- // Merge in option-provided tools and schemas
560
+ return this.mergeOptionTools(tools)
561
+ }
562
+
563
+ /**
564
+ * Merge tools provided via constructor options into the tool map.
565
+ * This allows runtime-created assistants to define tools entirely via options.
566
+ */
567
+ private mergeOptionTools(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
476
568
  if (this.options.tools) {
477
569
  const optionSchemas = this.options.schemas || {}
478
570
 
@@ -513,6 +605,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
513
605
  */
514
606
  loadHooks(): Record<string, (...args: any[]) => any> {
515
607
  const hooks: Record<string, (...args: any[]) => any> = {}
608
+
609
+ // Skip loading if no hooks file exists (runtime-created assistants)
610
+ if (!this.container.fs.exists(this.hooksModulePath)) {
611
+ return hooks
612
+ }
613
+
516
614
  const vm = this.container.feature('vm')
517
615
 
518
616
  let moduleExports: Record<string, any>
@@ -626,7 +724,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
626
724
  * @returns this, for chaining
627
725
  */
628
726
  resumeThread(threadId: string): this {
629
- this._resumeThreadId = threadId
727
+ this.state.set('resumeThreadId', threadId)
630
728
  return this
631
729
  }
632
730
 
@@ -659,7 +757,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
659
757
  const mode = this.options.historyMode || 'lifecycle'
660
758
  if (mode === 'lifecycle') return
661
759
 
662
- const threadId = this._resumeThreadId || this.buildThreadId(mode)
760
+ const threadId = (this.state.get('resumeThreadId') as string | undefined) || this.buildThreadId(mode)
663
761
  this.state.set('threadId', threadId)
664
762
 
665
763
  const existing = await this.conversationHistory.findByThread(threadId)
@@ -670,7 +768,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
670
768
 
671
769
  // Swap in fresh system prompt if it changed
672
770
  if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
673
- messages[0] = { role: messages[0]!.role, content: this._systemPrompt }
771
+ messages[0] = { role: messages[0]!.role, content: this.systemPrompt }
674
772
  }
675
773
 
676
774
  this.conversation.state.set('id', existing.id)
@@ -699,7 +797,8 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
699
797
 
700
798
  private bindHooksToEvents() {
701
799
  const assistant = this
702
- for (const [eventName, hookFn] of Object.entries(this._hooks)) {
800
+ const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
801
+ for (const [eventName, hookFn] of Object.entries(hooks)) {
703
802
  if (Assistant.lifecycleHooks.has(eventName)) continue
704
803
  this.on(eventName as any, (...args: any[]) => {
705
804
  this.emit('hookFired', eventName)
@@ -720,17 +819,19 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
720
819
  if (this.isStarted) return this
721
820
 
722
821
  // Wait for any async .use() plugins to finish before starting
723
- if (this._pendingPlugins.length) {
724
- await Promise.all(this._pendingPlugins)
725
- this._pendingPlugins = []
822
+ const pending = this.state.get('pendingPlugins') as Promise<void>[]
823
+ if (pending.length) {
824
+ await Promise.all(pending)
825
+ this.state.set('pendingPlugins', [])
726
826
  }
727
827
 
728
828
  // Allow hooks.ts to export a formatSystemPrompt(assistant, prompt) => string
729
829
  // that transforms the system prompt before the conversation is created.
730
- if (this._hooks.formatSystemPrompt) {
731
- const result = await this._hooks.formatSystemPrompt(this, this._systemPrompt)
830
+ const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
831
+ if (hooks.formatSystemPrompt) {
832
+ const result = await hooks.formatSystemPrompt(this, this.systemPrompt)
732
833
  if (typeof result === 'string') {
733
- this._systemPrompt = result
834
+ this.state.set('systemPrompt', result)
734
835
  }
735
836
  }
736
837
 
@@ -758,6 +859,13 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
758
859
  if (mode === 'daily' || mode === 'persistent') {
759
860
  (this.conversation.options as any).autoCompact = true
760
861
  }
862
+
863
+ this.on('toolsChanged', () => {
864
+ const conv = this.state.get('conversation') as Conversation | null
865
+ if (conv) {
866
+ conv.updateTools(this.tools)
867
+ }
868
+ })
761
869
 
762
870
  this.state.set('started', true)
763
871
  this.emit('started')
@@ -818,6 +926,53 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
818
926
 
819
927
  return this.conversation.save(opts)
820
928
  }
929
+
930
+ // -- Subagent API --
931
+
932
+ /**
933
+ * Names of assistants available as subagents, discovered via the assistantsManager.
934
+ *
935
+ * @returns {string[]} Available assistant names
936
+ */
937
+ get availableSubagents(): string[] {
938
+ try {
939
+ const manager = this.container.feature('assistantsManager')
940
+ return manager.available
941
+ } catch {
942
+ return []
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Get or create a subagent assistant. Uses the assistantsManager to discover
948
+ * and create the assistant, then caches the instance for reuse across tool calls.
949
+ *
950
+ * @param id - The assistant name (e.g. 'codingAssistant')
951
+ * @param options - Additional options to pass to the assistant constructor
952
+ * @returns {Promise<Assistant>} The subagent assistant instance, started and ready
953
+ *
954
+ * @example
955
+ * ```typescript
956
+ * const researcher = await assistant.subagent('codingAssistant')
957
+ * const answer = await researcher.ask('Find all usages of container.feature("fs")')
958
+ * ```
959
+ */
960
+ async subagent(id: string, options: Record<string, any> = {}): Promise<Assistant> {
961
+ const subagents = (this.state.get('subagents') || {}) as Record<string, Assistant>
962
+ if (subagents[id]) return subagents[id]
963
+
964
+ const manager = this.container.feature('assistantsManager')
965
+
966
+ if (!manager.state.get('discovered')) {
967
+ await manager.discover()
968
+ }
969
+
970
+ const instance = manager.create(id, options)
971
+ await instance.start()
972
+
973
+ this.state.set('subagents', { ...subagents, [id]: instance })
974
+ return instance
975
+ }
821
976
  }
822
977
 
823
978
  export default Assistant