@vibe-forge/mcp 2.0.0 → 2.0.2-alpha.0
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/__tests__/task-manager.spec.ts +234 -0
- package/__tests__/task-tool.spec.ts +132 -1
- package/package.json +6 -6
- package/src/tools/task/index.ts +3 -2
- package/src/tools/task/manager.ts +108 -10
- package/src/tools/task/presentation.ts +31 -6
- package/src/tools/task/register-task-runtime-tools.ts +88 -8
|
@@ -208,6 +208,240 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
208
208
|
})
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
+
it('sends a follow-up message directly to a running task without server sync', async () => {
|
|
212
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
213
|
+
const emit = vi.fn()
|
|
214
|
+
|
|
215
|
+
mocks.run.mockResolvedValueOnce({
|
|
216
|
+
session: {
|
|
217
|
+
emit,
|
|
218
|
+
kill: vi.fn()
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const managedTaskManager = new TaskManager()
|
|
223
|
+
await managedTaskManager.startTask({
|
|
224
|
+
taskId: 'task-send-local',
|
|
225
|
+
description: 'trigger'
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await managedTaskManager.sendTaskMessage({
|
|
229
|
+
taskId: 'task-send-local',
|
|
230
|
+
message: 'keep going'
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const task = managedTaskManager.getTask('task-send-local')
|
|
234
|
+
expect(emit).toHaveBeenNthCalledWith(2, {
|
|
235
|
+
type: 'message',
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: 'keep going'
|
|
239
|
+
}]
|
|
240
|
+
})
|
|
241
|
+
expect(task?.logs).toContain('User message submitted (direct): keep going')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('syncs follow-up messages through the child session when server sync is enabled', async () => {
|
|
245
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
246
|
+
const emit = vi.fn()
|
|
247
|
+
|
|
248
|
+
mocks.run.mockResolvedValueOnce({
|
|
249
|
+
session: {
|
|
250
|
+
emit,
|
|
251
|
+
kill: vi.fn()
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const managedTaskManager = new TaskManager()
|
|
256
|
+
await managedTaskManager.startTask({
|
|
257
|
+
taskId: 'task-send-synced',
|
|
258
|
+
description: 'trigger',
|
|
259
|
+
enableServerSync: true
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
await managedTaskManager.sendTaskMessage({
|
|
263
|
+
taskId: 'task-send-synced',
|
|
264
|
+
message: 'keep going'
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const task = managedTaskManager.getTask('task-send-synced')
|
|
268
|
+
expect(emit).toHaveBeenCalledTimes(1)
|
|
269
|
+
expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-send-synced', {
|
|
270
|
+
type: 'message',
|
|
271
|
+
data: expect.objectContaining({
|
|
272
|
+
role: 'user',
|
|
273
|
+
content: 'keep going'
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
expect(task?.logs).toContain('User message submitted (direct): keep going')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('queues steer follow-up messages and resumes the same task after natural completion', async () => {
|
|
280
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
281
|
+
let onEvent: ((event: any) => void) | undefined
|
|
282
|
+
const resumedEmit = vi.fn()
|
|
283
|
+
|
|
284
|
+
mocks.run
|
|
285
|
+
.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
286
|
+
onEvent = adapterOptions.onEvent
|
|
287
|
+
return {
|
|
288
|
+
session: {
|
|
289
|
+
emit: vi.fn(),
|
|
290
|
+
kill: vi.fn()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
.mockResolvedValueOnce({
|
|
295
|
+
session: {
|
|
296
|
+
emit: resumedEmit,
|
|
297
|
+
kill: vi.fn()
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const managedTaskManager = new TaskManager()
|
|
302
|
+
await managedTaskManager.startTask({
|
|
303
|
+
taskId: 'task-send-steer',
|
|
304
|
+
description: 'trigger'
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
await managedTaskManager.sendTaskMessage({
|
|
308
|
+
taskId: 'task-send-steer',
|
|
309
|
+
message: 'after you finish, summarize blockers',
|
|
310
|
+
mode: 'steer'
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
onEvent?.({
|
|
314
|
+
type: 'stop',
|
|
315
|
+
data: undefined
|
|
316
|
+
})
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
318
|
+
|
|
319
|
+
const task = managedTaskManager.getTask('task-send-steer')
|
|
320
|
+
expect(mocks.run).toHaveBeenCalledTimes(2)
|
|
321
|
+
expect(resumedEmit).toHaveBeenCalledWith({
|
|
322
|
+
type: 'message',
|
|
323
|
+
content: [{
|
|
324
|
+
type: 'text',
|
|
325
|
+
text: 'after you finish, summarize blockers'
|
|
326
|
+
}]
|
|
327
|
+
})
|
|
328
|
+
expect(task?.logs).toContain('Queued task message (steer): after you finish, summarize blockers')
|
|
329
|
+
expect(task?.logs).toContain('Resuming task from steer queue: after you finish, summarize blockers')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('rejects follow-up messages when a task is waiting for input', async () => {
|
|
333
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
334
|
+
|
|
335
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
336
|
+
const session = {
|
|
337
|
+
emit: vi.fn(() => {
|
|
338
|
+
adapterOptions.onEvent({
|
|
339
|
+
type: 'interaction_request',
|
|
340
|
+
data: {
|
|
341
|
+
id: 'interaction-send-blocked',
|
|
342
|
+
payload: {
|
|
343
|
+
sessionId: 'task-send-blocked',
|
|
344
|
+
kind: 'permission',
|
|
345
|
+
question: 'Allow editing files?',
|
|
346
|
+
options: [
|
|
347
|
+
{ label: 'Allow once', value: 'allow_once' }
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
}),
|
|
353
|
+
kill: vi.fn(),
|
|
354
|
+
respondInteraction: vi.fn()
|
|
355
|
+
}
|
|
356
|
+
return { session }
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const managedTaskManager = new TaskManager()
|
|
360
|
+
await managedTaskManager.startTask({
|
|
361
|
+
taskId: 'task-send-blocked',
|
|
362
|
+
description: 'trigger',
|
|
363
|
+
background: false
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
await expect(managedTaskManager.sendTaskMessage({
|
|
367
|
+
taskId: 'task-send-blocked',
|
|
368
|
+
message: 'continue'
|
|
369
|
+
})).rejects.toThrow('Task task-send-blocked is waiting for input. Use SubmitTaskInput instead.')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('rejects steer follow-up messages when a task is waiting for input', async () => {
|
|
373
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
374
|
+
|
|
375
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
376
|
+
const session = {
|
|
377
|
+
emit: vi.fn(() => {
|
|
378
|
+
adapterOptions.onEvent({
|
|
379
|
+
type: 'interaction_request',
|
|
380
|
+
data: {
|
|
381
|
+
id: 'interaction-send-steer-blocked',
|
|
382
|
+
payload: {
|
|
383
|
+
sessionId: 'task-send-steer-blocked',
|
|
384
|
+
kind: 'permission',
|
|
385
|
+
question: 'Allow editing files?',
|
|
386
|
+
options: [
|
|
387
|
+
{ label: 'Allow once', value: 'allow_once' }
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
}),
|
|
393
|
+
kill: vi.fn(),
|
|
394
|
+
respondInteraction: vi.fn()
|
|
395
|
+
}
|
|
396
|
+
return { session }
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const managedTaskManager = new TaskManager()
|
|
400
|
+
await managedTaskManager.startTask({
|
|
401
|
+
taskId: 'task-send-steer-blocked',
|
|
402
|
+
description: 'trigger',
|
|
403
|
+
background: false
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await expect(managedTaskManager.sendTaskMessage({
|
|
407
|
+
taskId: 'task-send-steer-blocked',
|
|
408
|
+
message: 'summarize blockers after this',
|
|
409
|
+
mode: 'steer'
|
|
410
|
+
})).rejects.toThrow('Task task-send-steer-blocked is waiting for input. Use SubmitTaskInput instead.')
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('rejects steer follow-up messages after a task has already completed', async () => {
|
|
414
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
415
|
+
let onEvent: ((event: any) => void) | undefined
|
|
416
|
+
|
|
417
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
418
|
+
onEvent = adapterOptions.onEvent
|
|
419
|
+
return {
|
|
420
|
+
session: {
|
|
421
|
+
emit: vi.fn(),
|
|
422
|
+
kill: vi.fn()
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const managedTaskManager = new TaskManager()
|
|
428
|
+
await managedTaskManager.startTask({
|
|
429
|
+
taskId: 'task-send-completed',
|
|
430
|
+
description: 'trigger'
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
onEvent?.({
|
|
434
|
+
type: 'stop',
|
|
435
|
+
data: undefined
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
await expect(managedTaskManager.sendTaskMessage({
|
|
439
|
+
taskId: 'task-send-completed',
|
|
440
|
+
message: 'queue this for later',
|
|
441
|
+
mode: 'steer'
|
|
442
|
+
})).rejects.toThrow('Task task-send-completed is not active. Start a new task instead.')
|
|
443
|
+
})
|
|
444
|
+
|
|
211
445
|
it('builds synthetic permission recovery for claude-code and resumes after SubmitTaskInput', async () => {
|
|
212
446
|
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
213
447
|
const resumedEmit = vi.fn()
|
|
@@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => {
|
|
|
8
8
|
createChildSession: vi.fn(),
|
|
9
9
|
getParentSessionId: vi.fn(),
|
|
10
10
|
startTask: vi.fn(),
|
|
11
|
+
sendTaskMessage: vi.fn(),
|
|
11
12
|
submitTaskInput: vi.fn(),
|
|
12
13
|
respondToTaskInteraction: vi.fn(),
|
|
13
14
|
getTask: vi.fn(),
|
|
@@ -33,6 +34,7 @@ vi.mock('#~/sync.js', () => ({
|
|
|
33
34
|
vi.mock('#~/tools/task/manager.js', () => ({
|
|
34
35
|
TaskManager: class {
|
|
35
36
|
startTask = mocks.startTask
|
|
37
|
+
sendTaskMessage = mocks.sendTaskMessage
|
|
36
38
|
submitTaskInput = mocks.submitTaskInput
|
|
37
39
|
respondToTaskInteraction = mocks.respondToTaskInteraction
|
|
38
40
|
getTask = mocks.getTask
|
|
@@ -52,6 +54,7 @@ describe('task tool integration', () => {
|
|
|
52
54
|
mocks.getParentSessionId.mockReturnValue(undefined)
|
|
53
55
|
mocks.createChildSession.mockResolvedValue({})
|
|
54
56
|
mocks.startTask.mockResolvedValue(undefined)
|
|
57
|
+
mocks.sendTaskMessage.mockResolvedValue(undefined)
|
|
55
58
|
mocks.submitTaskInput.mockResolvedValue(undefined)
|
|
56
59
|
mocks.respondToTaskInteraction.mockResolvedValue(undefined)
|
|
57
60
|
mocks.getTask.mockImplementation((taskId: string) => ({
|
|
@@ -94,6 +97,28 @@ describe('task tool integration', () => {
|
|
|
94
97
|
}))
|
|
95
98
|
})
|
|
96
99
|
|
|
100
|
+
it('accepts workspace tasks without a separate workspace tool', async () => {
|
|
101
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
102
|
+
|
|
103
|
+
const tester = createToolTester()
|
|
104
|
+
createTaskRegister()(tester.mockRegister)
|
|
105
|
+
|
|
106
|
+
await tester.callTool('StartTasks', {
|
|
107
|
+
tasks: [{
|
|
108
|
+
description: 'fix billing',
|
|
109
|
+
type: 'workspace',
|
|
110
|
+
name: 'billing'
|
|
111
|
+
}]
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
115
|
+
taskId: 'task-1',
|
|
116
|
+
description: 'fix billing',
|
|
117
|
+
type: 'workspace',
|
|
118
|
+
name: 'billing'
|
|
119
|
+
}))
|
|
120
|
+
})
|
|
121
|
+
|
|
97
122
|
it('inherits the parent permission mode when the task does not specify one', async () => {
|
|
98
123
|
process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
|
|
99
124
|
mocks.getParentSessionId.mockReturnValue('parent-session')
|
|
@@ -159,15 +184,96 @@ describe('task tool integration', () => {
|
|
|
159
184
|
const tester = createToolTester()
|
|
160
185
|
createTaskRegister()(tester.mockRegister)
|
|
161
186
|
|
|
187
|
+
expect(tester.getRegisteredTools()).toContain('SendTaskMessage')
|
|
162
188
|
expect(tester.getRegisteredTools()).toContain('SubmitTaskInput')
|
|
163
189
|
expect(tester.getRegisteredTools()).toContain('RespondTaskInteraction')
|
|
164
190
|
expect(tester.getToolInfo('StartTasks')?.description).toContain('GetTaskInfo')
|
|
165
|
-
expect(tester.getToolInfo('
|
|
191
|
+
expect(tester.getToolInfo('StartTasks')?.description).toContain('SendTaskMessage')
|
|
192
|
+
expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('10 most recent logs')
|
|
193
|
+
expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('logOrder')
|
|
194
|
+
expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('SendTaskMessage')
|
|
195
|
+
expect(tester.getToolInfo('SendTaskMessage')?.description).toContain('mode "direct"')
|
|
196
|
+
expect(tester.getToolInfo('SendTaskMessage')?.description).toContain('mode "steer"')
|
|
197
|
+
expect(tester.getToolInfo('ListTasks')?.description).toContain('10 most recent logs')
|
|
198
|
+
expect(tester.getToolInfo('ListTasks')?.description).toContain('SendTaskMessage')
|
|
166
199
|
expect(tester.getToolInfo('ListTasks')?.description).toContain('pendingInput')
|
|
200
|
+
expect(tester.getToolInfo('SubmitTaskInput')?.description).toContain('SendTaskMessage')
|
|
167
201
|
expect(tester.getToolInfo('SubmitTaskInput')?.description).toContain('allow_once')
|
|
168
202
|
expect(tester.getToolInfo('RespondTaskInteraction')?.description).toContain('Deprecated alias')
|
|
169
203
|
})
|
|
170
204
|
|
|
205
|
+
it('returns the 10 most recent logs in descending order by default', async () => {
|
|
206
|
+
mocks.getTask.mockReturnValue({
|
|
207
|
+
taskId: 'task-1',
|
|
208
|
+
status: 'running',
|
|
209
|
+
logs: Array.from({ length: 12 }, (_, index) => `log-${index + 1}`)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
213
|
+
|
|
214
|
+
const tester = createToolTester()
|
|
215
|
+
createTaskRegister()(tester.mockRegister)
|
|
216
|
+
|
|
217
|
+
const result = await tester.callTool('GetTaskInfo', {
|
|
218
|
+
taskId: 'task-1'
|
|
219
|
+
}) as { content: Array<{ text: string }> }
|
|
220
|
+
const [task] = JSON.parse(result.content[0].text) as Array<{ logs: string[] }>
|
|
221
|
+
|
|
222
|
+
expect(task.logs).toEqual([
|
|
223
|
+
'log-12',
|
|
224
|
+
'log-11',
|
|
225
|
+
'log-10',
|
|
226
|
+
'log-9',
|
|
227
|
+
'log-8',
|
|
228
|
+
'log-7',
|
|
229
|
+
'log-6',
|
|
230
|
+
'log-5',
|
|
231
|
+
'log-4',
|
|
232
|
+
'log-3'
|
|
233
|
+
])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('supports custom log windows and ascending order in ListTasks', async () => {
|
|
237
|
+
mocks.getAllTasks.mockReturnValue([
|
|
238
|
+
{
|
|
239
|
+
taskId: 'task-1',
|
|
240
|
+
status: 'running',
|
|
241
|
+
logs: ['a', 'b', 'c', 'd']
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
taskId: 'task-2',
|
|
245
|
+
status: 'completed',
|
|
246
|
+
logs: ['1', '2', '3']
|
|
247
|
+
}
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
251
|
+
|
|
252
|
+
const tester = createToolTester()
|
|
253
|
+
createTaskRegister()(tester.mockRegister)
|
|
254
|
+
|
|
255
|
+
const result = await tester.callTool('ListTasks', {
|
|
256
|
+
logLimit: 2,
|
|
257
|
+
logOrder: 'asc'
|
|
258
|
+
}) as { content: Array<{ text: string }> }
|
|
259
|
+
const tasks = JSON.parse(result.content[0].text) as Array<{ taskId: string; logs: string[] }>
|
|
260
|
+
|
|
261
|
+
expect(tasks).toEqual([
|
|
262
|
+
{
|
|
263
|
+
taskId: 'task-1',
|
|
264
|
+
status: 'running',
|
|
265
|
+
logs: ['c', 'd'],
|
|
266
|
+
guidance: []
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
taskId: 'task-2',
|
|
270
|
+
status: 'completed',
|
|
271
|
+
logs: ['2', '3'],
|
|
272
|
+
guidance: []
|
|
273
|
+
}
|
|
274
|
+
])
|
|
275
|
+
})
|
|
276
|
+
|
|
171
277
|
it('forwards SubmitTaskInput to the task manager', async () => {
|
|
172
278
|
mocks.getTask.mockReturnValue({
|
|
173
279
|
taskId: 'task-1',
|
|
@@ -192,6 +298,31 @@ describe('task tool integration', () => {
|
|
|
192
298
|
})
|
|
193
299
|
})
|
|
194
300
|
|
|
301
|
+
it('forwards SendTaskMessage to the task manager', async () => {
|
|
302
|
+
mocks.getTask.mockReturnValue({
|
|
303
|
+
taskId: 'task-1',
|
|
304
|
+
status: 'running',
|
|
305
|
+
logs: ['Queued task message (steer): keep checking logs']
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
309
|
+
|
|
310
|
+
const tester = createToolTester()
|
|
311
|
+
createTaskRegister()(tester.mockRegister)
|
|
312
|
+
|
|
313
|
+
await tester.callTool('SendTaskMessage', {
|
|
314
|
+
taskId: 'task-1',
|
|
315
|
+
message: 'keep checking logs',
|
|
316
|
+
mode: 'steer'
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
expect(mocks.sendTaskMessage).toHaveBeenCalledWith({
|
|
320
|
+
taskId: 'task-1',
|
|
321
|
+
message: 'keep checking logs',
|
|
322
|
+
mode: 'steer'
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
195
326
|
it('keeps RespondTaskInteraction as a deprecated alias', async () => {
|
|
196
327
|
mocks.getTask.mockReturnValue({
|
|
197
328
|
taskId: 'task-1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-forge/mcp",
|
|
3
|
-
"version": "2.0.0",
|
|
3
|
+
"version": "2.0.2-alpha.0",
|
|
4
4
|
"description": "Vibe Forge MCP server",
|
|
5
5
|
"imports": {
|
|
6
6
|
"#~/*.js": {
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
34
|
+
"@vibe-forge/config": "^2.0.2",
|
|
34
35
|
"commander": "^12.1.0",
|
|
35
36
|
"zod": "^3.24.1",
|
|
37
|
+
"@vibe-forge/cli-helper": "^2.0.0",
|
|
36
38
|
"@vibe-forge/hooks": "^2.0.0",
|
|
37
|
-
"@vibe-forge/
|
|
38
|
-
"@vibe-forge/
|
|
39
|
-
"@vibe-forge/task": "^2.0.0",
|
|
40
|
-
"@vibe-forge/utils": "^2.0.0",
|
|
39
|
+
"@vibe-forge/task": "2.0.1-alpha.3",
|
|
40
|
+
"@vibe-forge/types": "^2.0.2",
|
|
41
41
|
"@vibe-forge/register": "^2.0.0",
|
|
42
|
-
"@vibe-forge/
|
|
42
|
+
"@vibe-forge/utils": "2.0.4-alpha.1"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project bundler packages/mcp/__tests__"
|
package/src/tools/task/index.ts
CHANGED
|
@@ -38,9 +38,10 @@ export const createTaskRegister = () => {
|
|
|
38
38
|
.enum([
|
|
39
39
|
'default',
|
|
40
40
|
'spec',
|
|
41
|
-
'entity'
|
|
41
|
+
'entity',
|
|
42
|
+
'workspace'
|
|
42
43
|
])
|
|
43
|
-
.describe('The type of definition to load (default, spec or
|
|
44
|
+
.describe('The type of definition to load (default, spec, entity or workspace)'),
|
|
44
45
|
name: z
|
|
45
46
|
.string()
|
|
46
47
|
.describe('The name of the spec or entity to load, if type is spec or entity. Otherwise, ignored.')
|
|
@@ -52,14 +52,16 @@ export interface TaskInfo {
|
|
|
52
52
|
taskId: string
|
|
53
53
|
adapter?: string
|
|
54
54
|
description: string
|
|
55
|
-
type?: 'default' | 'spec' | 'entity'
|
|
55
|
+
type?: 'default' | 'spec' | 'entity' | 'workspace'
|
|
56
56
|
name?: string
|
|
57
|
+
workspaceCwd?: string
|
|
57
58
|
permissionMode?: SessionPermissionMode
|
|
58
59
|
background?: boolean
|
|
59
60
|
status: 'running' | 'waiting_input' | 'completed' | 'failed'
|
|
60
61
|
exitCode?: number
|
|
61
62
|
logs: string[]
|
|
62
63
|
permissionState: SessionPermissionState
|
|
64
|
+
queuedSteerMessages: string[]
|
|
63
65
|
pendingInteraction?: PendingTaskInteraction
|
|
64
66
|
lastError?: AdapterErrorData
|
|
65
67
|
session?: ManagedTaskSession
|
|
@@ -230,7 +232,7 @@ export class TaskManager {
|
|
|
230
232
|
public async startTask(options: {
|
|
231
233
|
taskId: string
|
|
232
234
|
description: string
|
|
233
|
-
type?: 'default' | 'spec' | 'entity'
|
|
235
|
+
type?: 'default' | 'spec' | 'entity' | 'workspace'
|
|
234
236
|
name?: string
|
|
235
237
|
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
|
|
236
238
|
adapter?: string
|
|
@@ -251,6 +253,7 @@ export class TaskManager {
|
|
|
251
253
|
status: 'running',
|
|
252
254
|
logs: [],
|
|
253
255
|
permissionState: createEmptySessionPermissionState(),
|
|
256
|
+
queuedSteerMessages: [],
|
|
254
257
|
createdAt: Date.now()
|
|
255
258
|
}
|
|
256
259
|
if (enableServerSync) {
|
|
@@ -284,8 +287,9 @@ export class TaskManager {
|
|
|
284
287
|
return { taskId }
|
|
285
288
|
}
|
|
286
289
|
|
|
287
|
-
private async launchTask(task: TaskInfo, runType: 'create' | 'resume') {
|
|
290
|
+
private async launchTask(task: TaskInfo, runType: 'create' | 'resume', resumeMessage?: string) {
|
|
288
291
|
try {
|
|
292
|
+
const rootCwd = process.cwd()
|
|
289
293
|
const promptType = task.type !== 'default' ? task.type : undefined
|
|
290
294
|
const promptName = task.name
|
|
291
295
|
const promptCWD = process.cwd()
|
|
@@ -297,25 +301,29 @@ export class TaskManager {
|
|
|
297
301
|
adapter: task.adapter
|
|
298
302
|
}
|
|
299
303
|
)
|
|
304
|
+
const taskCwd = resolvedConfig.workspace?.cwd ?? promptCWD
|
|
305
|
+
task.workspaceCwd = resolvedConfig.workspace?.cwd
|
|
300
306
|
const env = {
|
|
301
307
|
...process.env,
|
|
302
|
-
__VF_PROJECT_AI_CTX_ID__: process.env.__VF_PROJECT_AI_CTX_ID__ ?? task.taskId
|
|
308
|
+
__VF_PROJECT_AI_CTX_ID__: process.env.__VF_PROJECT_AI_CTX_ID__ ?? task.taskId,
|
|
309
|
+
__VF_PROJECT_WORKSPACE_FOLDER__: taskCwd,
|
|
310
|
+
__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__: rootCwd
|
|
303
311
|
}
|
|
304
312
|
await callHook('GenerateSystemPrompt', {
|
|
305
|
-
cwd:
|
|
313
|
+
cwd: taskCwd,
|
|
306
314
|
sessionId: task.taskId,
|
|
307
315
|
type: promptType,
|
|
308
316
|
name: promptName,
|
|
309
317
|
data
|
|
310
318
|
}, env)
|
|
311
319
|
|
|
312
|
-
const injectDefaultSystemPrompt = await loadInjectDefaultSystemPromptValue(
|
|
320
|
+
const injectDefaultSystemPrompt = await loadInjectDefaultSystemPromptValue(taskCwd)
|
|
313
321
|
const ctxId = process.env.__VF_PROJECT_AI_CTX_ID__ ?? task.taskId
|
|
314
322
|
const { session, resolvedAdapter } = await run({
|
|
315
323
|
adapter: task.adapter,
|
|
316
|
-
cwd:
|
|
324
|
+
cwd: taskCwd,
|
|
317
325
|
env: {
|
|
318
|
-
...
|
|
326
|
+
...env,
|
|
319
327
|
__VF_PROJECT_AI_CTX_ID__: ctxId
|
|
320
328
|
}
|
|
321
329
|
}, {
|
|
@@ -352,7 +360,7 @@ export class TaskManager {
|
|
|
352
360
|
current.lastError = undefined
|
|
353
361
|
try {
|
|
354
362
|
await syncTaskPermissionStateMirror({
|
|
355
|
-
cwd:
|
|
363
|
+
cwd: taskCwd,
|
|
356
364
|
adapter: current.adapter,
|
|
357
365
|
sessionId: current.taskId,
|
|
358
366
|
permissionState: current.permissionState
|
|
@@ -368,7 +376,7 @@ export class TaskManager {
|
|
|
368
376
|
type: 'message',
|
|
369
377
|
content: [{
|
|
370
378
|
type: 'text',
|
|
371
|
-
text: runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description
|
|
379
|
+
text: resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
|
|
372
380
|
}]
|
|
373
381
|
})
|
|
374
382
|
} catch (err) {
|
|
@@ -461,6 +469,17 @@ export class TaskManager {
|
|
|
461
469
|
task.status = 'completed'
|
|
462
470
|
task.pendingInteraction = undefined
|
|
463
471
|
this.stopServerPolling(taskId)
|
|
472
|
+
if (task.queuedSteerMessages.length > 0) {
|
|
473
|
+
void this.dispatchQueuedSteerMessage(task).catch((error) => {
|
|
474
|
+
task.status = 'failed'
|
|
475
|
+
appendTaskLog(
|
|
476
|
+
task,
|
|
477
|
+
`Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
|
|
478
|
+
)
|
|
479
|
+
task.onStop?.()
|
|
480
|
+
})
|
|
481
|
+
break
|
|
482
|
+
}
|
|
464
483
|
task.onStop?.()
|
|
465
484
|
break
|
|
466
485
|
}
|
|
@@ -483,6 +502,17 @@ export class TaskManager {
|
|
|
483
502
|
task.pendingInteraction = undefined
|
|
484
503
|
appendTaskLog(task, `Process exited with code ${event.data.exitCode}`)
|
|
485
504
|
this.stopServerPolling(taskId)
|
|
505
|
+
if (task.status === 'completed' && task.queuedSteerMessages.length > 0) {
|
|
506
|
+
void this.dispatchQueuedSteerMessage(task).catch((error) => {
|
|
507
|
+
task.status = 'failed'
|
|
508
|
+
appendTaskLog(
|
|
509
|
+
task,
|
|
510
|
+
`Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
|
|
511
|
+
)
|
|
512
|
+
task.onStop?.()
|
|
513
|
+
})
|
|
514
|
+
break
|
|
515
|
+
}
|
|
486
516
|
task.onStop?.()
|
|
487
517
|
break
|
|
488
518
|
default:
|
|
@@ -575,6 +605,74 @@ export class TaskManager {
|
|
|
575
605
|
return Array.from(this.tasks.values())
|
|
576
606
|
}
|
|
577
607
|
|
|
608
|
+
public async sendTaskMessage(params: {
|
|
609
|
+
taskId: string
|
|
610
|
+
message: string
|
|
611
|
+
mode?: 'direct' | 'steer'
|
|
612
|
+
}): Promise<void> {
|
|
613
|
+
const task = this.tasks.get(params.taskId)
|
|
614
|
+
if (task == null) {
|
|
615
|
+
throw new Error(`Task ${params.taskId} not found.`)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const message = params.message.trim()
|
|
619
|
+
const mode = params.mode ?? 'direct'
|
|
620
|
+
if (message === '') {
|
|
621
|
+
throw new Error('Task message cannot be empty.')
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (task.pendingInteraction != null || task.status === 'waiting_input') {
|
|
625
|
+
throw new Error(`Task ${params.taskId} is waiting for input. Use SubmitTaskInput instead.`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (mode === 'steer') {
|
|
629
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
630
|
+
throw new Error(`Task ${params.taskId} is not active. Start a new task instead.`)
|
|
631
|
+
}
|
|
632
|
+
task.queuedSteerMessages.push(message)
|
|
633
|
+
appendTaskLog(task, `Queued task message (${mode}): ${message}`)
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (task.status !== 'running' || task.session == null) {
|
|
638
|
+
throw new Error(`Task ${params.taskId} is not running. Start a new task instead.`)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (task.serverSync != null) {
|
|
642
|
+
await postSessionEvent(task.serverSync.sessionId, {
|
|
643
|
+
type: 'message',
|
|
644
|
+
data: {
|
|
645
|
+
id: `task-user:${task.taskId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
646
|
+
role: 'user',
|
|
647
|
+
content: message,
|
|
648
|
+
createdAt: Date.now()
|
|
649
|
+
}
|
|
650
|
+
})
|
|
651
|
+
} else {
|
|
652
|
+
task.session.emit({
|
|
653
|
+
type: 'message',
|
|
654
|
+
content: [{
|
|
655
|
+
type: 'text',
|
|
656
|
+
text: message
|
|
657
|
+
}]
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
appendTaskLog(task, `User message submitted (${mode}): ${message}`)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private async dispatchQueuedSteerMessage(task: TaskInfo) {
|
|
665
|
+
const nextMessage = task.queuedSteerMessages.shift()
|
|
666
|
+
if (nextMessage == null || nextMessage.trim() === '') {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
task.status = 'running'
|
|
671
|
+
appendTaskLog(task, `Resuming task from steer queue: ${nextMessage}`)
|
|
672
|
+
await this.launchTask(task, 'resume', nextMessage)
|
|
673
|
+
return true
|
|
674
|
+
}
|
|
675
|
+
|
|
578
676
|
public async submitTaskInput(params: {
|
|
579
677
|
taskId: string
|
|
580
678
|
interactionId?: string
|
|
@@ -5,15 +5,21 @@ import type { SessionPermissionMode } from '@vibe-forge/types'
|
|
|
5
5
|
import type { TaskInfo } from './manager'
|
|
6
6
|
|
|
7
7
|
export const SESSION_PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'] as const
|
|
8
|
+
export const TASK_LOG_ORDERS = ['asc', 'desc'] as const
|
|
9
|
+
export const DEFAULT_TASK_LOG_LIMIT = 10
|
|
10
|
+
export type TaskLogsOrder = typeof TASK_LOG_ORDERS[number]
|
|
8
11
|
|
|
9
12
|
export const START_TASKS_DESCRIPTION =
|
|
10
|
-
'Start multiple tasks in background or foreground. If a task stalls, fails, or asks for permission/input, call GetTaskInfo. If GetTaskInfo returns pendingInput or pendingInteraction, resolve it with SubmitTaskInput. If logs show permission_required, you can answer the recovery prompt with SubmitTaskInput instead of restarting manually.'
|
|
13
|
+
'Start multiple tasks in background or foreground. Use type "workspace" plus name to run in a configured workspace. If a task stalls, fails, or asks for permission/input, call GetTaskInfo. GetTaskInfo returns the 10 most recent logs by default in descending order, so newer log lines appear earlier in the logs array. If you need to add another instruction to a task that is still running, use SendTaskMessage instead of starting a replacement task. If GetTaskInfo returns pendingInput or pendingInteraction, resolve it with SubmitTaskInput. If logs show permission_required, you can answer the recovery prompt with SubmitTaskInput instead of restarting manually.'
|
|
11
14
|
|
|
12
15
|
export const GET_TASK_INFO_DESCRIPTION =
|
|
13
|
-
'Get the detailed status, logs, pendingInput, pendingInteraction, lastError, and guidance for a task.
|
|
16
|
+
'Get the detailed status, logs, pendingInput, pendingInteraction, lastError, and guidance for a task. By default this returns the 10 most recent logs in descending order, so newer log lines appear earlier in the logs array. Use logLimit to inspect a different number of recent logs, and set logOrder to "asc" when you want the selected log window in oldest-to-newest order. If the task is still active and you need to continue it, call SendTaskMessage with mode "direct" or "steer" depending on timing. If pendingInput is present, answer it with SubmitTaskInput.'
|
|
17
|
+
|
|
18
|
+
export const SEND_TASK_MESSAGE_DESCRIPTION =
|
|
19
|
+
'Send a follow-up user message to a managed task. Use mode "direct" (default) to continue the current running task immediately. Use mode "steer" to queue a follow-up that should run after the current task finishes naturally. Do not use this to answer pendingInput or pendingInteraction; use SubmitTaskInput for that. If the task already completed or failed, start a new task instead.'
|
|
14
20
|
|
|
15
21
|
export const SUBMIT_TASK_INPUT_DESCRIPTION =
|
|
16
|
-
'Submit input for a task that is blocked waiting for permission or user input. First call GetTaskInfo or ListTasks, then use taskId plus one of pendingInput.payload.options[].value when available. Common permission answers are allow_once, allow_session, allow_project, deny_once, deny_session, or deny_project.'
|
|
22
|
+
'Submit input for a task that is blocked waiting for permission or user input. First call GetTaskInfo or ListTasks, then use taskId plus one of pendingInput.payload.options[].value when available. Do not use this for ordinary follow-up instructions to a running task or queued steer messages; use SendTaskMessage instead. Common permission answers are allow_once, allow_session, allow_project, deny_once, deny_session, or deny_project.'
|
|
17
23
|
|
|
18
24
|
export const RESPOND_TASK_INTERACTION_DESCRIPTION =
|
|
19
25
|
'Deprecated alias of SubmitTaskInput. Use SubmitTaskInput for both permission prompts and generic task input.'
|
|
@@ -22,7 +28,7 @@ export const STOP_TASK_DESCRIPTION =
|
|
|
22
28
|
'Stop a running or blocked task. Use this when the task is no longer needed or cannot recover cleanly.'
|
|
23
29
|
|
|
24
30
|
export const LIST_TASKS_DESCRIPTION =
|
|
25
|
-
'List all managed tasks with status, pendingInput, pendingInteraction, lastError, and guidance. Use
|
|
31
|
+
'List all managed tasks with status, logs, pendingInput, pendingInteraction, lastError, and guidance. Each task returns the 10 most recent logs by default in descending order, so newer log lines appear earlier in the logs array. Use logLimit and logOrder to adjust the recent log window for every listed task. If a listed task needs another instruction, call SendTaskMessage with mode "direct" or "steer". If it is waiting for input, call GetTaskInfo for details or SubmitTaskInput to answer it.'
|
|
26
32
|
|
|
27
33
|
export const TASK_PERMISSION_MODE_DESCRIPTION =
|
|
28
34
|
'Permission mode for the task. If omitted, inherits the parent session. Raise it only when the task is blocked by permission errors.'
|
|
@@ -30,6 +36,12 @@ export const TASK_PERMISSION_MODE_DESCRIPTION =
|
|
|
30
36
|
export const TASK_BACKGROUND_DESCRIPTION =
|
|
31
37
|
'Whether to run in background (default: true). If false, waits until the task completes, fails, or becomes blocked waiting for input, then returns the current logs.'
|
|
32
38
|
|
|
39
|
+
export const TASK_LOG_LIMIT_DESCRIPTION =
|
|
40
|
+
`How many recent log entries to include. Defaults to ${DEFAULT_TASK_LOG_LIMIT}.`
|
|
41
|
+
|
|
42
|
+
export const TASK_LOG_ORDER_DESCRIPTION =
|
|
43
|
+
'Order of the selected log window. Defaults to "desc", which returns newer log lines first. Use "asc" for oldest-to-newest order.'
|
|
44
|
+
|
|
33
45
|
export const resolveInheritedPermissionMode = (): SessionPermissionMode | undefined => {
|
|
34
46
|
const value = process.env.__VF_PROJECT_AI_PERMISSION_MODE__?.trim()
|
|
35
47
|
if (value == null || value === '') return undefined
|
|
@@ -74,20 +86,33 @@ export const serializeTaskInfo = (params: {
|
|
|
74
86
|
description?: string
|
|
75
87
|
status?: TaskInfo['status']
|
|
76
88
|
info?: TaskInfo
|
|
89
|
+
logLimit?: number
|
|
90
|
+
logOrder?: TaskLogsOrder
|
|
77
91
|
}) => {
|
|
78
92
|
const info = params.info
|
|
79
93
|
const safeInfo = (() => {
|
|
80
94
|
if (info == null) {
|
|
81
95
|
return undefined
|
|
82
96
|
}
|
|
83
|
-
const { session, onStop, serverSync, createdAt, ...rest } = info
|
|
97
|
+
const { session, onStop, serverSync, createdAt, logs, ...rest } = info
|
|
84
98
|
return rest
|
|
85
99
|
})()
|
|
100
|
+
const selectedLogs = (() => {
|
|
101
|
+
const logs = info?.logs ?? []
|
|
102
|
+
const limit = params.logLimit
|
|
103
|
+
const windowedLogs = limit == null
|
|
104
|
+
? logs
|
|
105
|
+
: logs.slice(Math.max(0, logs.length - limit))
|
|
106
|
+
return params.logOrder === 'desc'
|
|
107
|
+
? [...windowedLogs].reverse()
|
|
108
|
+
: windowedLogs
|
|
109
|
+
})()
|
|
110
|
+
|
|
86
111
|
return {
|
|
87
112
|
taskId: params.taskId,
|
|
88
113
|
description: params.description ?? info?.description,
|
|
89
114
|
status: info?.status ?? params.status,
|
|
90
|
-
logs:
|
|
115
|
+
logs: selectedLogs,
|
|
91
116
|
pendingInput: safeInfo?.pendingInteraction,
|
|
92
117
|
...safeInfo,
|
|
93
118
|
guidance: buildTaskGuidance(safeInfo ?? {})
|
|
@@ -3,11 +3,16 @@ import { z } from 'zod'
|
|
|
3
3
|
import type { Register } from '../types'
|
|
4
4
|
import type { TaskManager } from './manager'
|
|
5
5
|
import {
|
|
6
|
+
DEFAULT_TASK_LOG_LIMIT,
|
|
6
7
|
GET_TASK_INFO_DESCRIPTION,
|
|
7
8
|
LIST_TASKS_DESCRIPTION,
|
|
8
9
|
RESPOND_TASK_INTERACTION_DESCRIPTION,
|
|
10
|
+
SEND_TASK_MESSAGE_DESCRIPTION,
|
|
9
11
|
STOP_TASK_DESCRIPTION,
|
|
10
12
|
SUBMIT_TASK_INPUT_DESCRIPTION,
|
|
13
|
+
TASK_LOG_LIMIT_DESCRIPTION,
|
|
14
|
+
TASK_LOG_ORDER_DESCRIPTION,
|
|
15
|
+
TASK_LOG_ORDERS,
|
|
11
16
|
serializeTaskInfo
|
|
12
17
|
} from './presentation'
|
|
13
18
|
|
|
@@ -21,10 +26,20 @@ export const registerTaskRuntimeTools = (
|
|
|
21
26
|
title: 'Get Task Info',
|
|
22
27
|
description: GET_TASK_INFO_DESCRIPTION,
|
|
23
28
|
inputSchema: z.object({
|
|
24
|
-
taskId: z.string().describe('The ID of the task to check')
|
|
29
|
+
taskId: z.string().describe('The ID of the task to check'),
|
|
30
|
+
logLimit: z
|
|
31
|
+
.number()
|
|
32
|
+
.int()
|
|
33
|
+
.min(1)
|
|
34
|
+
.describe(TASK_LOG_LIMIT_DESCRIPTION)
|
|
35
|
+
.default(DEFAULT_TASK_LOG_LIMIT),
|
|
36
|
+
logOrder: z
|
|
37
|
+
.enum(TASK_LOG_ORDERS)
|
|
38
|
+
.describe(TASK_LOG_ORDER_DESCRIPTION)
|
|
39
|
+
.default('desc')
|
|
25
40
|
})
|
|
26
41
|
},
|
|
27
|
-
async ({ taskId }) => {
|
|
42
|
+
async ({ taskId, logLimit, logOrder }) => {
|
|
28
43
|
const task = taskManager.getTask(taskId)
|
|
29
44
|
if (!task) {
|
|
30
45
|
return {
|
|
@@ -35,7 +50,46 @@ export const registerTaskRuntimeTools = (
|
|
|
35
50
|
return {
|
|
36
51
|
content: [{
|
|
37
52
|
type: 'text',
|
|
38
|
-
text: JSON.stringify([serializeTaskInfo({ taskId, info: task })])
|
|
53
|
+
text: JSON.stringify([serializeTaskInfo({ taskId, info: task, logLimit, logOrder })])
|
|
54
|
+
}]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
server.registerTool(
|
|
60
|
+
'SendTaskMessage',
|
|
61
|
+
{
|
|
62
|
+
title: 'Send Task Message',
|
|
63
|
+
description: SEND_TASK_MESSAGE_DESCRIPTION,
|
|
64
|
+
inputSchema: z.object({
|
|
65
|
+
taskId: z.string().describe('The ID of the running task to continue'),
|
|
66
|
+
message: z
|
|
67
|
+
.string()
|
|
68
|
+
.trim()
|
|
69
|
+
.min(1)
|
|
70
|
+
.describe('The follow-up instruction to send to the task'),
|
|
71
|
+
mode: z
|
|
72
|
+
.enum(['direct', 'steer'])
|
|
73
|
+
.describe('How to deliver the message: direct (default) or steer')
|
|
74
|
+
.default('direct')
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
async ({ taskId, message, mode }) => {
|
|
78
|
+
await taskManager.sendTaskMessage({
|
|
79
|
+
taskId,
|
|
80
|
+
message,
|
|
81
|
+
mode
|
|
82
|
+
})
|
|
83
|
+
const task = taskManager.getTask(taskId)
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: JSON.stringify([serializeTaskInfo({
|
|
88
|
+
taskId,
|
|
89
|
+
info: task,
|
|
90
|
+
logLimit: DEFAULT_TASK_LOG_LIMIT,
|
|
91
|
+
logOrder: 'desc'
|
|
92
|
+
})])
|
|
39
93
|
}]
|
|
40
94
|
}
|
|
41
95
|
}
|
|
@@ -67,7 +121,12 @@ export const registerTaskRuntimeTools = (
|
|
|
67
121
|
return {
|
|
68
122
|
content: [{
|
|
69
123
|
type: 'text',
|
|
70
|
-
text: JSON.stringify([serializeTaskInfo({
|
|
124
|
+
text: JSON.stringify([serializeTaskInfo({
|
|
125
|
+
taskId,
|
|
126
|
+
info: task,
|
|
127
|
+
logLimit: DEFAULT_TASK_LOG_LIMIT,
|
|
128
|
+
logOrder: 'desc'
|
|
129
|
+
})])
|
|
71
130
|
}]
|
|
72
131
|
}
|
|
73
132
|
}
|
|
@@ -99,7 +158,12 @@ export const registerTaskRuntimeTools = (
|
|
|
99
158
|
return {
|
|
100
159
|
content: [{
|
|
101
160
|
type: 'text',
|
|
102
|
-
text: JSON.stringify([serializeTaskInfo({
|
|
161
|
+
text: JSON.stringify([serializeTaskInfo({
|
|
162
|
+
taskId,
|
|
163
|
+
info: task,
|
|
164
|
+
logLimit: DEFAULT_TASK_LOG_LIMIT,
|
|
165
|
+
logOrder: 'desc'
|
|
166
|
+
})])
|
|
103
167
|
}]
|
|
104
168
|
}
|
|
105
169
|
}
|
|
@@ -130,14 +194,30 @@ export const registerTaskRuntimeTools = (
|
|
|
130
194
|
{
|
|
131
195
|
title: 'List Tasks',
|
|
132
196
|
description: LIST_TASKS_DESCRIPTION,
|
|
133
|
-
inputSchema: z.object({
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
logLimit: z
|
|
199
|
+
.number()
|
|
200
|
+
.int()
|
|
201
|
+
.min(1)
|
|
202
|
+
.describe(TASK_LOG_LIMIT_DESCRIPTION)
|
|
203
|
+
.default(DEFAULT_TASK_LOG_LIMIT),
|
|
204
|
+
logOrder: z
|
|
205
|
+
.enum(TASK_LOG_ORDERS)
|
|
206
|
+
.describe(TASK_LOG_ORDER_DESCRIPTION)
|
|
207
|
+
.default('desc')
|
|
208
|
+
})
|
|
134
209
|
},
|
|
135
|
-
async () => {
|
|
210
|
+
async ({ logLimit, logOrder }) => {
|
|
136
211
|
const tasks = taskManager.getAllTasks()
|
|
137
212
|
return {
|
|
138
213
|
content: [{
|
|
139
214
|
type: 'text',
|
|
140
|
-
text: JSON.stringify(tasks.map(task => serializeTaskInfo({
|
|
215
|
+
text: JSON.stringify(tasks.map(task => serializeTaskInfo({
|
|
216
|
+
taskId: task.taskId,
|
|
217
|
+
info: task,
|
|
218
|
+
logLimit,
|
|
219
|
+
logOrder
|
|
220
|
+
})))
|
|
141
221
|
}]
|
|
142
222
|
}
|
|
143
223
|
}
|