@soederpop/luca 0.2.2 → 0.2.3
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/assistants/codingAssistant/ABOUT.md +3 -1
- package/assistants/codingAssistant/CORE.md +2 -4
- package/assistants/codingAssistant/hooks.ts +9 -10
- package/assistants/codingAssistant/tools.ts +9 -0
- package/assistants/inkbot/ABOUT.md +13 -2
- package/assistants/inkbot/CORE.md +278 -39
- package/assistants/inkbot/hooks.ts +0 -8
- package/assistants/inkbot/tools.ts +24 -18
- package/assistants/researcher/ABOUT.md +5 -0
- package/assistants/researcher/CORE.md +46 -0
- package/assistants/researcher/hooks.ts +16 -0
- package/assistants/researcher/tools.ts +237 -0
- package/commands/inkbot.ts +526 -194
- package/docs/examples/assistant-hooks-reference.ts +171 -0
- package/package.json +1 -1
- package/public/slides-ai-native.html +902 -0
- package/public/slides-intro.html +974 -0
- package/src/agi/features/assistant.ts +432 -62
- package/src/agi/features/conversation.ts +170 -10
- package/src/bootstrap/generated.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/helper.ts +12 -3
- package/src/introspection/generated.agi.ts +1105 -873
- package/src/introspection/generated.node.ts +757 -757
- package/src/introspection/generated.web.ts +1 -1
- package/src/python/generated.ts +1 -1
- package/src/scaffolds/generated.ts +1 -1
- package/test/assistant-hooks.test.ts +306 -0
- package/test/assistant.test.ts +1 -1
- package/test/fork-and-research.test.ts +450 -0
- package/SPEC.md +0 -304
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { AGIContainer } from '../src/agi/container.server'
|
|
3
|
+
import type { Conversation } from '../src/agi/features/conversation'
|
|
4
|
+
import type { Assistant } from '../src/agi/features/assistant'
|
|
5
|
+
|
|
6
|
+
function makeConversation(opts: Record<string, any> = {}): Conversation {
|
|
7
|
+
const container = new AGIContainer()
|
|
8
|
+
return container.feature('conversation', {
|
|
9
|
+
model: 'gpt-5',
|
|
10
|
+
history: [
|
|
11
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
12
|
+
{ role: 'user', content: 'What is Luca?' },
|
|
13
|
+
{ role: 'assistant', content: 'Luca is a framework.' },
|
|
14
|
+
{ role: 'user', content: 'Tell me more.' },
|
|
15
|
+
{ role: 'assistant', content: 'It provides dependency injection for TypeScript apps.' },
|
|
16
|
+
{ role: 'user', content: 'How does the container work?' },
|
|
17
|
+
{ role: 'assistant', content: 'The container is a singleton that manages features, clients, and servers.' },
|
|
18
|
+
],
|
|
19
|
+
...opts,
|
|
20
|
+
}) as Conversation
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeAssistant(container?: AGIContainer): Assistant {
|
|
24
|
+
const c = container || new AGIContainer()
|
|
25
|
+
return c.feature('assistant', {
|
|
26
|
+
systemPrompt: 'You are a research assistant.',
|
|
27
|
+
model: 'gpt-5',
|
|
28
|
+
}) as Assistant
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('Conversation fork', () => {
|
|
32
|
+
it('fork with history: full copies all messages', () => {
|
|
33
|
+
const conv = makeConversation()
|
|
34
|
+
const fork = conv.fork({ history: 'full' })
|
|
35
|
+
expect(fork.messages).toHaveLength(conv.messages.length)
|
|
36
|
+
expect(fork.messages[0]).toEqual(conv.messages[0])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('fork with history: none keeps only the system message', () => {
|
|
40
|
+
const conv = makeConversation()
|
|
41
|
+
const fork = conv.fork({ history: 'none' })
|
|
42
|
+
expect(fork.messages).toHaveLength(1)
|
|
43
|
+
expect(fork.messages[0]!.role).toBe('system')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('fork with history: none on a conversation without system message returns empty', () => {
|
|
47
|
+
const container = new AGIContainer()
|
|
48
|
+
const conv = container.feature('conversation', {
|
|
49
|
+
model: 'gpt-5',
|
|
50
|
+
history: [
|
|
51
|
+
{ role: 'user', content: 'hello' },
|
|
52
|
+
{ role: 'assistant', content: 'hi' },
|
|
53
|
+
],
|
|
54
|
+
}) as Conversation
|
|
55
|
+
const fork = conv.fork({ history: 'none' })
|
|
56
|
+
expect(fork.messages).toHaveLength(0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('fork with history: 1 keeps system + last exchange', () => {
|
|
60
|
+
const conv = makeConversation()
|
|
61
|
+
const fork = conv.fork({ history: 1 })
|
|
62
|
+
// System + last user + last assistant = 3
|
|
63
|
+
expect(fork.messages).toHaveLength(3)
|
|
64
|
+
expect(fork.messages[0]!.role).toBe('system')
|
|
65
|
+
expect(fork.messages[1]!.role).toBe('user')
|
|
66
|
+
expect((fork.messages[1] as any).content).toBe('How does the container work?')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('fork with history: 2 keeps system + last 2 exchanges', () => {
|
|
70
|
+
const conv = makeConversation()
|
|
71
|
+
const fork = conv.fork({ history: 2 })
|
|
72
|
+
// System + 2 user/assistant pairs = 5
|
|
73
|
+
expect(fork.messages).toHaveLength(5)
|
|
74
|
+
expect(fork.messages[0]!.role).toBe('system')
|
|
75
|
+
expect((fork.messages[1] as any).content).toBe('Tell me more.')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('fork with model override changes the model', () => {
|
|
79
|
+
const conv = makeConversation()
|
|
80
|
+
const fork = conv.fork({ model: 'gpt-4o-mini' })
|
|
81
|
+
expect(fork.model).toBe('gpt-4o-mini')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('fork does not affect original conversation', () => {
|
|
85
|
+
const conv = makeConversation()
|
|
86
|
+
const originalLength = conv.messages.length
|
|
87
|
+
const fork = conv.fork({ history: 'none' })
|
|
88
|
+
fork.pushMessage({ role: 'user', content: 'new message on fork' })
|
|
89
|
+
expect(conv.messages).toHaveLength(originalLength)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('fork with array creates multiple independent forks', () => {
|
|
93
|
+
const conv = makeConversation()
|
|
94
|
+
const forks = conv.fork([
|
|
95
|
+
{ history: 'none' },
|
|
96
|
+
{ history: 1 },
|
|
97
|
+
{ history: 'full' },
|
|
98
|
+
])
|
|
99
|
+
expect(forks).toHaveLength(3)
|
|
100
|
+
expect(forks[0]!.messages).toHaveLength(1) // system only
|
|
101
|
+
expect(forks[1]!.messages).toHaveLength(3) // system + 1 exchange
|
|
102
|
+
expect(forks[2]!.messages).toHaveLength(conv.messages.length) // all
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('default fork (no options) copies all messages', () => {
|
|
106
|
+
const conv = makeConversation()
|
|
107
|
+
const fork = conv.fork()
|
|
108
|
+
expect(fork.messages).toHaveLength(conv.messages.length)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('Conversation research', () => {
|
|
113
|
+
it('fans out questions and returns results', async () => {
|
|
114
|
+
const conv = makeConversation()
|
|
115
|
+
conv.stub(/pros of A/, 'A is fast')
|
|
116
|
+
conv.stub(/pros of B/, 'B is safe')
|
|
117
|
+
|
|
118
|
+
const results = await conv.research([
|
|
119
|
+
'What are the pros of A?',
|
|
120
|
+
'What are the pros of B?',
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
expect(results).toHaveLength(2)
|
|
124
|
+
expect(results[0]).toBe('A is fast')
|
|
125
|
+
expect(results[1]).toBe('B is safe')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('forks inherit stubs from the parent', async () => {
|
|
129
|
+
const conv = makeConversation()
|
|
130
|
+
conv.stub(/.*/, 'stubbed')
|
|
131
|
+
|
|
132
|
+
const results = await conv.research([
|
|
133
|
+
'anything',
|
|
134
|
+
'something else',
|
|
135
|
+
], { history: 'none' })
|
|
136
|
+
|
|
137
|
+
expect(results).toEqual(['stubbed', 'stubbed'])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('respects per-fork history option', async () => {
|
|
141
|
+
const conv = makeConversation()
|
|
142
|
+
conv.stub(/.*/, 'stubbed')
|
|
143
|
+
|
|
144
|
+
const results = await conv.research([
|
|
145
|
+
{ question: 'q1', forkOptions: { history: 'none' } },
|
|
146
|
+
{ question: 'q2', forkOptions: { history: 1 } },
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
expect(results).toHaveLength(2)
|
|
150
|
+
expect(results[0]).toBe('stubbed')
|
|
151
|
+
expect(results[1]).toBe('stubbed')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('Assistant fork', () => {
|
|
156
|
+
it('creates an independent assistant with same system prompt', async () => {
|
|
157
|
+
const assistant = makeAssistant()
|
|
158
|
+
await assistant.start()
|
|
159
|
+
|
|
160
|
+
const fork = await assistant.fork({ history: 'none' })
|
|
161
|
+
expect(fork.systemPrompt).toBe(assistant.systemPrompt)
|
|
162
|
+
expect(fork.isStarted).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('fork preserves interceptors', async () => {
|
|
166
|
+
const assistant = makeAssistant()
|
|
167
|
+
const intercepted: string[] = []
|
|
168
|
+
|
|
169
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
170
|
+
intercepted.push('parent-interceptor')
|
|
171
|
+
ctx.result = 'intercepted'
|
|
172
|
+
await next()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await assistant.start()
|
|
176
|
+
const fork = await assistant.fork({ history: 'none' })
|
|
177
|
+
|
|
178
|
+
const result = await fork.ask('anything')
|
|
179
|
+
expect(result).toBe('intercepted')
|
|
180
|
+
expect(intercepted).toContain('parent-interceptor')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('fork preserves system prompt extensions', async () => {
|
|
184
|
+
const assistant = makeAssistant()
|
|
185
|
+
assistant.addSystemPromptExtension('extra', 'You also speak French.')
|
|
186
|
+
await assistant.start()
|
|
187
|
+
|
|
188
|
+
const fork = await assistant.fork({ history: 'none' })
|
|
189
|
+
expect(fork.effectiveSystemPrompt).toContain('You also speak French.')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('fork with model override', async () => {
|
|
193
|
+
const assistant = makeAssistant()
|
|
194
|
+
await assistant.start()
|
|
195
|
+
|
|
196
|
+
const fork = await assistant.fork({ model: 'gpt-4o-mini', history: 'none' })
|
|
197
|
+
expect(fork.conversation.model).toBe('gpt-4o-mini')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('fork array creates multiple assistants', async () => {
|
|
201
|
+
const assistant = makeAssistant()
|
|
202
|
+
await assistant.start()
|
|
203
|
+
|
|
204
|
+
const forks = await assistant.fork([
|
|
205
|
+
{ history: 'none' },
|
|
206
|
+
{ history: 'none' },
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
expect(forks).toHaveLength(2)
|
|
210
|
+
expect(forks[0]!.isStarted).toBe(true)
|
|
211
|
+
expect(forks[1]!.isStarted).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('Assistant createResearchJob', () => {
|
|
216
|
+
it('returns a job entity with running status', async () => {
|
|
217
|
+
const container = new AGIContainer()
|
|
218
|
+
const assistant = makeAssistant(container)
|
|
219
|
+
|
|
220
|
+
// Use interceptors to simulate responses with random delays
|
|
221
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
222
|
+
const delay = Math.floor(Math.random() * 50) + 10
|
|
223
|
+
await container.sleep(delay)
|
|
224
|
+
ctx.result = `Answer to: ${ctx.question}`
|
|
225
|
+
await next()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await assistant.start()
|
|
229
|
+
|
|
230
|
+
const job = await assistant.createResearchJob(
|
|
231
|
+
'Research context',
|
|
232
|
+
['Question 1', 'Question 2', 'Question 3'],
|
|
233
|
+
{ history: 'none' }
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
expect(job.state.get('status')).toBe('running')
|
|
237
|
+
expect(job.state.get('total')).toBe(3)
|
|
238
|
+
expect(job.id).toMatch(/^research:/)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('job is tracked in assistant.researchJobs', async () => {
|
|
242
|
+
const container = new AGIContainer()
|
|
243
|
+
const assistant = makeAssistant(container)
|
|
244
|
+
|
|
245
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
246
|
+
ctx.result = 'done'
|
|
247
|
+
await next()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
await assistant.start()
|
|
251
|
+
|
|
252
|
+
const job = await assistant.createResearchJob('', ['q1'], { history: 'none' })
|
|
253
|
+
expect(assistant.researchJobs.has(job.id)).toBe(true)
|
|
254
|
+
expect(assistant.researchJobs.get(job.id)).toBe(job)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('emits forkCompleted for each resolved fork', async () => {
|
|
258
|
+
const container = new AGIContainer()
|
|
259
|
+
const assistant = makeAssistant(container)
|
|
260
|
+
|
|
261
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
262
|
+
const delay = Math.floor(Math.random() * 30) + 10
|
|
263
|
+
await container.sleep(delay)
|
|
264
|
+
ctx.result = `Result for: ${ctx.question}`
|
|
265
|
+
await next()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
await assistant.start()
|
|
269
|
+
|
|
270
|
+
const job = await assistant.createResearchJob(
|
|
271
|
+
'',
|
|
272
|
+
['Q1', 'Q2'],
|
|
273
|
+
{ history: 'none' }
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const completed: number[] = []
|
|
277
|
+
job.on('forkCompleted', (index) => {
|
|
278
|
+
completed.push(index)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
await job.waitFor('completed')
|
|
282
|
+
|
|
283
|
+
expect(completed.sort()).toEqual([0, 1])
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('resolves with all results on completed event', async () => {
|
|
287
|
+
const container = new AGIContainer()
|
|
288
|
+
const assistant = makeAssistant(container)
|
|
289
|
+
|
|
290
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
291
|
+
const delay = Math.floor(Math.random() * 50) + 10
|
|
292
|
+
await container.sleep(delay)
|
|
293
|
+
ctx.result = `Answer: ${ctx.question}`
|
|
294
|
+
await next()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await assistant.start()
|
|
298
|
+
|
|
299
|
+
const job = await assistant.createResearchJob(
|
|
300
|
+
'',
|
|
301
|
+
['Alpha', 'Beta', 'Gamma'],
|
|
302
|
+
{ history: 'none' }
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const results = await new Promise<string[]>((resolve) => {
|
|
306
|
+
job.on('completed', (r) => resolve(r))
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
expect(results).toHaveLength(3)
|
|
310
|
+
expect(results[0]).toBe('Answer: Alpha')
|
|
311
|
+
expect(results[1]).toBe('Answer: Beta')
|
|
312
|
+
expect(results[2]).toBe('Answer: Gamma')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('tracks incremental progress in state', async () => {
|
|
316
|
+
const container = new AGIContainer()
|
|
317
|
+
const assistant = makeAssistant(container)
|
|
318
|
+
|
|
319
|
+
// Track completion order via events
|
|
320
|
+
const completedIndices: number[] = []
|
|
321
|
+
|
|
322
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
323
|
+
// Stagger the forks: first question takes longer
|
|
324
|
+
const delay = String(ctx.question).includes('A') ? 40 : 10
|
|
325
|
+
await container.sleep(delay)
|
|
326
|
+
ctx.result = `Done: ${ctx.question}`
|
|
327
|
+
await next()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
await assistant.start()
|
|
331
|
+
|
|
332
|
+
const job = await assistant.createResearchJob(
|
|
333
|
+
'',
|
|
334
|
+
['A', 'B'],
|
|
335
|
+
{ history: 'none' }
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
job.on('forkCompleted', (index) => {
|
|
339
|
+
completedIndices.push(index)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// B should finish first (10ms vs 40ms)
|
|
343
|
+
// Wait for first completion
|
|
344
|
+
await new Promise<void>(resolve => {
|
|
345
|
+
const check = () => {
|
|
346
|
+
if (job.state.get('completed')! >= 1) return resolve()
|
|
347
|
+
setTimeout(check, 5)
|
|
348
|
+
}
|
|
349
|
+
check()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
expect(job.state.get('completed')).toBe(1)
|
|
353
|
+
expect(job.state.get('status')).toBe('running')
|
|
354
|
+
|
|
355
|
+
// Wait for full completion
|
|
356
|
+
await job.waitFor('completed')
|
|
357
|
+
expect(job.state.get('completed')).toBe(2)
|
|
358
|
+
expect(job.state.get('status')).toBe('completed')
|
|
359
|
+
expect(job.state.get('results')![0]).toBe('Done: A')
|
|
360
|
+
expect(job.state.get('results')![1]).toBe('Done: B')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('shared prompt is added as system prompt extension on forks', async () => {
|
|
364
|
+
const container = new AGIContainer()
|
|
365
|
+
const assistant = makeAssistant(container)
|
|
366
|
+
|
|
367
|
+
let capturedPrompts: string[] = []
|
|
368
|
+
|
|
369
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
370
|
+
// Capture the effective system prompt from the fork
|
|
371
|
+
capturedPrompts.push('captured')
|
|
372
|
+
ctx.result = 'done'
|
|
373
|
+
await next()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
await assistant.start()
|
|
377
|
+
|
|
378
|
+
await assistant.createResearchJob(
|
|
379
|
+
'You are analyzing security vulnerabilities.',
|
|
380
|
+
['Check for XSS'],
|
|
381
|
+
{ history: 'none' }
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
await container.sleep(50)
|
|
385
|
+
expect(capturedPrompts.length).toBeGreaterThan(0)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('Assistant research (sugar)', () => {
|
|
390
|
+
it('blocks until all results are available', async () => {
|
|
391
|
+
const container = new AGIContainer()
|
|
392
|
+
const assistant = makeAssistant(container)
|
|
393
|
+
|
|
394
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
395
|
+
const delay = Math.floor(Math.random() * 30) + 10
|
|
396
|
+
await container.sleep(delay)
|
|
397
|
+
ctx.result = `R:${ctx.question}`
|
|
398
|
+
await next()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
await assistant.start()
|
|
402
|
+
|
|
403
|
+
const results = await assistant.research(
|
|
404
|
+
['Q1', 'Q2', 'Q3'],
|
|
405
|
+
{ history: 'none' }
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
expect(results).toHaveLength(3)
|
|
409
|
+
expect(results[0]).toBe('R:Q1')
|
|
410
|
+
expect(results[1]).toBe('R:Q2')
|
|
411
|
+
expect(results[2]).toBe('R:Q3')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('supports per-question fork overrides', async () => {
|
|
415
|
+
const container = new AGIContainer()
|
|
416
|
+
const assistant = makeAssistant(container)
|
|
417
|
+
|
|
418
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
419
|
+
ctx.result = `answered`
|
|
420
|
+
await next()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await assistant.start()
|
|
424
|
+
|
|
425
|
+
const results = await assistant.research([
|
|
426
|
+
'simple question',
|
|
427
|
+
{ question: 'contextual question', forkOptions: { history: 'full' } },
|
|
428
|
+
], { history: 'none' })
|
|
429
|
+
|
|
430
|
+
expect(results).toHaveLength(2)
|
|
431
|
+
expect(results[0]).toBe('answered')
|
|
432
|
+
expect(results[1]).toBe('answered')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('creates a tracked job under the hood', async () => {
|
|
436
|
+
const container = new AGIContainer()
|
|
437
|
+
const assistant = makeAssistant(container)
|
|
438
|
+
|
|
439
|
+
assistant.intercept('beforeAsk', async (ctx, next) => {
|
|
440
|
+
ctx.result = 'ok'
|
|
441
|
+
await next()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
await assistant.start()
|
|
445
|
+
|
|
446
|
+
expect(assistant.researchJobs.size).toBe(0)
|
|
447
|
+
await assistant.research(['q'], { history: 'none' })
|
|
448
|
+
expect(assistant.researchJobs.size).toBe(1)
|
|
449
|
+
})
|
|
450
|
+
})
|