@vibe-forge/mcp 2.0.2-alpha.1 → 3.0.0-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.
|
@@ -156,6 +156,47 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
156
156
|
)
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
+
it('forwards task model overrides into query resolution and runtime startup', async () => {
|
|
160
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
161
|
+
|
|
162
|
+
mocks.run.mockResolvedValueOnce({
|
|
163
|
+
session: {
|
|
164
|
+
emit: vi.fn(),
|
|
165
|
+
kill: vi.fn()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const managedTaskManager = new TaskManager()
|
|
170
|
+
await managedTaskManager.startTask({
|
|
171
|
+
taskId: 'task-model-override',
|
|
172
|
+
description: 'trigger',
|
|
173
|
+
adapter: 'codex',
|
|
174
|
+
model: 'openai,gpt-5.4-mini'
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(mocks.generateAdapterQueryOptions).toHaveBeenCalledWith(
|
|
178
|
+
undefined,
|
|
179
|
+
undefined,
|
|
180
|
+
expect.any(String),
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
adapter: 'codex',
|
|
183
|
+
model: 'openai,gpt-5.4-mini'
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
expect(mocks.run).toHaveBeenCalledWith(
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
adapter: 'codex'
|
|
189
|
+
}),
|
|
190
|
+
expect.objectContaining({
|
|
191
|
+
sessionId: 'task-model-override',
|
|
192
|
+
model: 'openai,gpt-5.4-mini'
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
expect(managedTaskManager.getTask('task-model-override')).toEqual(expect.objectContaining({
|
|
196
|
+
model: 'openai,gpt-5.4-mini'
|
|
197
|
+
}))
|
|
198
|
+
})
|
|
199
|
+
|
|
159
200
|
it('responds to pending interactions and syncs the response', async () => {
|
|
160
201
|
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
161
202
|
const respondInteraction = vi.fn()
|
|
@@ -265,7 +306,13 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
265
306
|
})
|
|
266
307
|
|
|
267
308
|
const task = managedTaskManager.getTask('task-send-synced')
|
|
268
|
-
expect(emit).
|
|
309
|
+
expect(emit).toHaveBeenNthCalledWith(2, {
|
|
310
|
+
type: 'message',
|
|
311
|
+
content: [{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: 'keep going'
|
|
314
|
+
}]
|
|
315
|
+
})
|
|
269
316
|
expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-send-synced', {
|
|
270
317
|
type: 'message',
|
|
271
318
|
data: expect.objectContaining({
|
|
@@ -276,6 +323,84 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
276
323
|
expect(task?.logs).toContain('User message submitted (direct): keep going')
|
|
277
324
|
})
|
|
278
325
|
|
|
326
|
+
it('does not replay stale synced messages after a failed resume attempt', async () => {
|
|
327
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
328
|
+
let onEvent: ((event: any) => void) | undefined
|
|
329
|
+
const resumedEmit = vi.fn()
|
|
330
|
+
const syncedEvents: Array<{ type: 'message'; message: Record<string, unknown> }> = []
|
|
331
|
+
|
|
332
|
+
mocks.postSessionEvent.mockImplementation(async (_sessionId: string, payload: any) => {
|
|
333
|
+
if (payload?.type === 'message' && payload.data != null) {
|
|
334
|
+
syncedEvents.push({
|
|
335
|
+
type: 'message',
|
|
336
|
+
message: payload.data
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
mocks.fetchSessionMessages.mockImplementation(async () => syncedEvents)
|
|
341
|
+
|
|
342
|
+
mocks.run
|
|
343
|
+
.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
344
|
+
onEvent = adapterOptions.onEvent
|
|
345
|
+
return {
|
|
346
|
+
session: {
|
|
347
|
+
emit: vi.fn(),
|
|
348
|
+
kill: vi.fn()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
.mockRejectedValueOnce(new Error('resume failed to start'))
|
|
353
|
+
.mockResolvedValueOnce({
|
|
354
|
+
session: {
|
|
355
|
+
emit: resumedEmit,
|
|
356
|
+
kill: vi.fn()
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const managedTaskManager = new TaskManager()
|
|
361
|
+
await managedTaskManager.startTask({
|
|
362
|
+
taskId: 'task-send-synced-retry',
|
|
363
|
+
description: 'trigger',
|
|
364
|
+
enableServerSync: true
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
onEvent?.({
|
|
368
|
+
type: 'stop',
|
|
369
|
+
data: undefined
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
await expect(managedTaskManager.sendTaskMessage({
|
|
373
|
+
taskId: 'task-send-synced-retry',
|
|
374
|
+
message: 'first retry'
|
|
375
|
+
})).rejects.toThrow('resume failed to start')
|
|
376
|
+
|
|
377
|
+
const firstMessage = syncedEvents.at(-1)?.message
|
|
378
|
+
const taskAfterFailure = managedTaskManager.getTask('task-send-synced-retry')
|
|
379
|
+
expect(firstMessage).toEqual(expect.objectContaining({
|
|
380
|
+
id: expect.any(String),
|
|
381
|
+
role: 'user',
|
|
382
|
+
content: 'first retry'
|
|
383
|
+
}))
|
|
384
|
+
expect(taskAfterFailure?.serverSync?.seenMessageIds.has(String(firstMessage?.id))).toBe(true)
|
|
385
|
+
|
|
386
|
+
await managedTaskManager.sendTaskMessage({
|
|
387
|
+
taskId: 'task-send-synced-retry',
|
|
388
|
+
message: 'second retry'
|
|
389
|
+
})
|
|
390
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
391
|
+
|
|
392
|
+
expect(resumedEmit).toHaveBeenCalledTimes(1)
|
|
393
|
+
expect(resumedEmit).toHaveBeenCalledWith({
|
|
394
|
+
type: 'message',
|
|
395
|
+
content: [{
|
|
396
|
+
type: 'text',
|
|
397
|
+
text: 'second retry'
|
|
398
|
+
}]
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
managedTaskManager.stopTask('task-send-synced-retry')
|
|
402
|
+
})
|
|
403
|
+
|
|
279
404
|
it('queues steer follow-up messages and resumes the same task after natural completion', async () => {
|
|
280
405
|
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
281
406
|
let onEvent: ((event: any) => void) | undefined
|
|
@@ -410,23 +535,81 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
410
535
|
})).rejects.toThrow('Task task-send-steer-blocked is waiting for input. Use SubmitTaskInput instead.')
|
|
411
536
|
})
|
|
412
537
|
|
|
413
|
-
it('
|
|
538
|
+
it('resumes completed tasks when sending a direct follow-up message', async () => {
|
|
414
539
|
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
415
540
|
let onEvent: ((event: any) => void) | undefined
|
|
541
|
+
const resumedEmit = vi.fn()
|
|
416
542
|
|
|
417
|
-
mocks.run
|
|
418
|
-
|
|
419
|
-
|
|
543
|
+
mocks.run
|
|
544
|
+
.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
545
|
+
onEvent = adapterOptions.onEvent
|
|
546
|
+
return {
|
|
547
|
+
session: {
|
|
548
|
+
emit: vi.fn(),
|
|
549
|
+
kill: vi.fn()
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
.mockResolvedValueOnce({
|
|
420
554
|
session: {
|
|
421
|
-
emit:
|
|
555
|
+
emit: resumedEmit,
|
|
422
556
|
kill: vi.fn()
|
|
423
557
|
}
|
|
424
|
-
}
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
const managedTaskManager = new TaskManager()
|
|
561
|
+
await managedTaskManager.startTask({
|
|
562
|
+
taskId: 'task-send-completed-direct',
|
|
563
|
+
description: 'trigger'
|
|
425
564
|
})
|
|
426
565
|
|
|
566
|
+
onEvent?.({
|
|
567
|
+
type: 'stop',
|
|
568
|
+
data: undefined
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
await managedTaskManager.sendTaskMessage({
|
|
572
|
+
taskId: 'task-send-completed-direct',
|
|
573
|
+
message: 'continue from the final summary'
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const task = managedTaskManager.getTask('task-send-completed-direct')
|
|
577
|
+
expect(mocks.run).toHaveBeenCalledTimes(2)
|
|
578
|
+
expect(resumedEmit).toHaveBeenCalledWith({
|
|
579
|
+
type: 'message',
|
|
580
|
+
content: [{
|
|
581
|
+
type: 'text',
|
|
582
|
+
text: 'continue from the final summary'
|
|
583
|
+
}]
|
|
584
|
+
})
|
|
585
|
+
expect(task?.logs).toContain('Resuming inactive task (direct): continue from the final summary')
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('resumes completed tasks when sending a steer follow-up message', async () => {
|
|
589
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
590
|
+
let onEvent: ((event: any) => void) | undefined
|
|
591
|
+
const resumedEmit = vi.fn()
|
|
592
|
+
|
|
593
|
+
mocks.run
|
|
594
|
+
.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
595
|
+
onEvent = adapterOptions.onEvent
|
|
596
|
+
return {
|
|
597
|
+
session: {
|
|
598
|
+
emit: vi.fn(),
|
|
599
|
+
kill: vi.fn()
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
.mockResolvedValueOnce({
|
|
604
|
+
session: {
|
|
605
|
+
emit: resumedEmit,
|
|
606
|
+
kill: vi.fn()
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
|
|
427
610
|
const managedTaskManager = new TaskManager()
|
|
428
611
|
await managedTaskManager.startTask({
|
|
429
|
-
taskId: 'task-send-completed',
|
|
612
|
+
taskId: 'task-send-completed-steer',
|
|
430
613
|
description: 'trigger'
|
|
431
614
|
})
|
|
432
615
|
|
|
@@ -435,11 +618,22 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
435
618
|
data: undefined
|
|
436
619
|
})
|
|
437
620
|
|
|
438
|
-
await
|
|
439
|
-
taskId: 'task-send-completed',
|
|
621
|
+
await managedTaskManager.sendTaskMessage({
|
|
622
|
+
taskId: 'task-send-completed-steer',
|
|
440
623
|
message: 'queue this for later',
|
|
441
624
|
mode: 'steer'
|
|
442
|
-
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
const task = managedTaskManager.getTask('task-send-completed-steer')
|
|
628
|
+
expect(mocks.run).toHaveBeenCalledTimes(2)
|
|
629
|
+
expect(resumedEmit).toHaveBeenCalledWith({
|
|
630
|
+
type: 'message',
|
|
631
|
+
content: [{
|
|
632
|
+
type: 'text',
|
|
633
|
+
text: 'queue this for later'
|
|
634
|
+
}]
|
|
635
|
+
})
|
|
636
|
+
expect(task?.logs).toContain('Resuming inactive task (steer): queue this for later')
|
|
443
637
|
})
|
|
444
638
|
|
|
445
639
|
it('builds synthetic permission recovery for claude-code and resumes after SubmitTaskInput', async () => {
|
|
@@ -119,6 +119,35 @@ describe('task tool integration', () => {
|
|
|
119
119
|
}))
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
+
it('passes explicit task model overrides to the hook and task manager', async () => {
|
|
123
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
124
|
+
|
|
125
|
+
const tester = createToolTester()
|
|
126
|
+
createTaskRegister()(tester.mockRegister)
|
|
127
|
+
|
|
128
|
+
await tester.callTool('StartTasks', {
|
|
129
|
+
tasks: [{
|
|
130
|
+
description: 'investigate flaky tests',
|
|
131
|
+
type: 'default',
|
|
132
|
+
model: 'gpt-5.4-mini'
|
|
133
|
+
}]
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(mocks.callHook).toHaveBeenCalledWith(
|
|
137
|
+
'StartTasks',
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
tasks: [expect.objectContaining({
|
|
140
|
+
taskId: 'task-1',
|
|
141
|
+
model: 'gpt-5.4-mini'
|
|
142
|
+
})]
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
146
|
+
taskId: 'task-1',
|
|
147
|
+
model: 'gpt-5.4-mini'
|
|
148
|
+
}))
|
|
149
|
+
})
|
|
150
|
+
|
|
122
151
|
it('inherits the parent permission mode when the task does not specify one', async () => {
|
|
123
152
|
process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
|
|
124
153
|
mocks.getParentSessionId.mockReturnValue('parent-session')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-forge/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-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",
|
|
35
34
|
"commander": "^12.1.0",
|
|
36
35
|
"zod": "^3.24.1",
|
|
37
|
-
"@vibe-forge/hooks": "
|
|
38
|
-
"@vibe-forge/
|
|
39
|
-
"@vibe-forge/
|
|
40
|
-
"@vibe-forge/register": "
|
|
41
|
-
"@vibe-forge/
|
|
42
|
-
"@vibe-forge/
|
|
36
|
+
"@vibe-forge/hooks": "3.0.0-alpha.0",
|
|
37
|
+
"@vibe-forge/types": "3.0.0-alpha.0",
|
|
38
|
+
"@vibe-forge/utils": "3.0.0-alpha.0",
|
|
39
|
+
"@vibe-forge/register": "3.0.0-alpha.0",
|
|
40
|
+
"@vibe-forge/cli-helper": "3.0.0-alpha.0",
|
|
41
|
+
"@vibe-forge/config": "3.0.0-alpha.0",
|
|
42
|
+
"@vibe-forge/task": "3.0.0-alpha.0"
|
|
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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SESSION_PERMISSION_MODES,
|
|
13
13
|
START_TASKS_DESCRIPTION,
|
|
14
14
|
TASK_BACKGROUND_DESCRIPTION,
|
|
15
|
+
TASK_MODEL_DESCRIPTION,
|
|
15
16
|
TASK_PERMISSION_MODE_DESCRIPTION,
|
|
16
17
|
resolveInheritedPermissionMode,
|
|
17
18
|
serializeTaskInfo
|
|
@@ -50,6 +51,10 @@ export const createTaskRegister = () => {
|
|
|
50
51
|
.string()
|
|
51
52
|
.describe('The adapter to use for the task (e.g. claude-code)')
|
|
52
53
|
.optional(),
|
|
54
|
+
model: z
|
|
55
|
+
.string()
|
|
56
|
+
.describe(TASK_MODEL_DESCRIPTION)
|
|
57
|
+
.optional(),
|
|
53
58
|
permissionMode: z
|
|
54
59
|
.enum(SESSION_PERMISSION_MODES)
|
|
55
60
|
.describe(TASK_PERMISSION_MODE_DESCRIPTION)
|
|
@@ -51,6 +51,7 @@ type ManagedTaskSession = McpTaskSession & {
|
|
|
51
51
|
export interface TaskInfo {
|
|
52
52
|
taskId: string
|
|
53
53
|
adapter?: string
|
|
54
|
+
model?: string
|
|
54
55
|
description: string
|
|
55
56
|
type?: 'default' | 'spec' | 'entity' | 'workspace'
|
|
56
57
|
name?: string
|
|
@@ -143,6 +144,25 @@ const formatPermissionErrorLog = (error: AdapterErrorData) => {
|
|
|
143
144
|
return parts.length > 0 ? parts.join(' | ') : undefined
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
const createTaskUserMessageEvent = (taskId: string, message: string) => ({
|
|
148
|
+
type: 'message' as const,
|
|
149
|
+
data: {
|
|
150
|
+
id: `task-user:${taskId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
151
|
+
role: 'user' as const,
|
|
152
|
+
content: message,
|
|
153
|
+
createdAt: Date.now()
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const createTaskSessionMessageEvent = (message: string, parentUuid?: string) => ({
|
|
158
|
+
type: 'message' as const,
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text' as const,
|
|
161
|
+
text: message
|
|
162
|
+
}],
|
|
163
|
+
...(parentUuid != null ? { parentUuid } : {})
|
|
164
|
+
})
|
|
165
|
+
|
|
146
166
|
export class TaskManager {
|
|
147
167
|
private tasks: Map<string, TaskInfo> = new Map()
|
|
148
168
|
private permissionToolUseCache = new Map<string, Map<string, string>>()
|
|
@@ -236,15 +256,27 @@ export class TaskManager {
|
|
|
236
256
|
name?: string
|
|
237
257
|
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
|
|
238
258
|
adapter?: string
|
|
259
|
+
model?: string
|
|
239
260
|
background?: boolean
|
|
240
261
|
enableServerSync?: boolean
|
|
241
262
|
}): Promise<{ taskId: string; logs?: string[] }> {
|
|
242
|
-
const {
|
|
263
|
+
const {
|
|
264
|
+
taskId,
|
|
265
|
+
adapter,
|
|
266
|
+
model,
|
|
267
|
+
description,
|
|
268
|
+
type,
|
|
269
|
+
name,
|
|
270
|
+
permissionMode,
|
|
271
|
+
background = true,
|
|
272
|
+
enableServerSync
|
|
273
|
+
} = options
|
|
243
274
|
|
|
244
275
|
// Initialize Task Info
|
|
245
276
|
const taskInfo: TaskInfo = {
|
|
246
277
|
taskId,
|
|
247
278
|
adapter,
|
|
279
|
+
model,
|
|
248
280
|
description,
|
|
249
281
|
type,
|
|
250
282
|
name,
|
|
@@ -287,7 +319,11 @@ export class TaskManager {
|
|
|
287
319
|
return { taskId }
|
|
288
320
|
}
|
|
289
321
|
|
|
290
|
-
private async launchTask(
|
|
322
|
+
private async launchTask(
|
|
323
|
+
task: TaskInfo,
|
|
324
|
+
runType: 'create' | 'resume',
|
|
325
|
+
resumeMessage?: string
|
|
326
|
+
) {
|
|
291
327
|
try {
|
|
292
328
|
const rootCwd = process.cwd()
|
|
293
329
|
const promptType = task.type !== 'default' ? task.type : undefined
|
|
@@ -298,7 +334,8 @@ export class TaskManager {
|
|
|
298
334
|
promptName,
|
|
299
335
|
promptCWD,
|
|
300
336
|
{
|
|
301
|
-
adapter: task.adapter
|
|
337
|
+
adapter: task.adapter,
|
|
338
|
+
model: task.model
|
|
302
339
|
}
|
|
303
340
|
)
|
|
304
341
|
const taskCwd = resolvedConfig.workspace?.cwd ?? promptCWD
|
|
@@ -331,6 +368,7 @@ export class TaskManager {
|
|
|
331
368
|
runtime: 'mcp',
|
|
332
369
|
mode: 'stream',
|
|
333
370
|
sessionId: task.taskId,
|
|
371
|
+
model: task.model,
|
|
334
372
|
systemPrompt: mergeSystemPrompts({
|
|
335
373
|
generatedSystemPrompt: resolvedConfig.systemPrompt,
|
|
336
374
|
injectDefaultSystemPrompt
|
|
@@ -372,13 +410,11 @@ export class TaskManager {
|
|
|
372
410
|
)
|
|
373
411
|
}
|
|
374
412
|
this.startServerPolling(task.taskId)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}]
|
|
381
|
-
})
|
|
413
|
+
this.emitTaskUserMessage(
|
|
414
|
+
current,
|
|
415
|
+
current.session,
|
|
416
|
+
resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
|
|
417
|
+
)
|
|
382
418
|
} catch (err) {
|
|
383
419
|
const current = this.tasks.get(task.taskId)
|
|
384
420
|
if (current) {
|
|
@@ -605,6 +641,27 @@ export class TaskManager {
|
|
|
605
641
|
return Array.from(this.tasks.values())
|
|
606
642
|
}
|
|
607
643
|
|
|
644
|
+
private async syncTaskUserMessage(task: TaskInfo, message: string) {
|
|
645
|
+
if (task.serverSync == null) {
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const event = createTaskUserMessageEvent(task.taskId, message)
|
|
650
|
+
await postSessionEvent(task.serverSync.sessionId, event)
|
|
651
|
+
task.serverSync.seenMessageIds.add(event.data.id)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private emitTaskUserMessage(task: TaskInfo, session: ManagedTaskSession, message: string) {
|
|
655
|
+
session.emit(createTaskSessionMessageEvent(message, task.serverSync?.lastAssistantMessageId))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private async resumeTaskWithMessage(task: TaskInfo, message: string, logMessage: string) {
|
|
659
|
+
await this.syncTaskUserMessage(task, message)
|
|
660
|
+
task.status = 'running'
|
|
661
|
+
appendTaskLog(task, logMessage)
|
|
662
|
+
await this.launchTask(task, 'resume', message)
|
|
663
|
+
}
|
|
664
|
+
|
|
608
665
|
public async sendTaskMessage(params: {
|
|
609
666
|
taskId: string
|
|
610
667
|
message: string
|
|
@@ -627,36 +684,25 @@ export class TaskManager {
|
|
|
627
684
|
|
|
628
685
|
if (mode === 'steer') {
|
|
629
686
|
if (task.status === 'completed' || task.status === 'failed') {
|
|
630
|
-
|
|
687
|
+
await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
|
|
688
|
+
return
|
|
631
689
|
}
|
|
632
690
|
task.queuedSteerMessages.push(message)
|
|
633
691
|
appendTaskLog(task, `Queued task message (${mode}): ${message}`)
|
|
634
692
|
return
|
|
635
693
|
}
|
|
636
694
|
|
|
695
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
696
|
+
await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
|
|
637
700
|
if (task.status !== 'running' || task.session == null) {
|
|
638
701
|
throw new Error(`Task ${params.taskId} is not running. Start a new task instead.`)
|
|
639
702
|
}
|
|
640
703
|
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
}
|
|
704
|
+
await this.syncTaskUserMessage(task, message)
|
|
705
|
+
this.emitTaskUserMessage(task, task.session, message)
|
|
660
706
|
|
|
661
707
|
appendTaskLog(task, `User message submitted (${mode}): ${message}`)
|
|
662
708
|
}
|
|
@@ -667,9 +713,7 @@ export class TaskManager {
|
|
|
667
713
|
return false
|
|
668
714
|
}
|
|
669
715
|
|
|
670
|
-
task
|
|
671
|
-
appendTaskLog(task, `Resuming task from steer queue: ${nextMessage}`)
|
|
672
|
-
await this.launchTask(task, 'resume', nextMessage)
|
|
716
|
+
await this.resumeTaskWithMessage(task, nextMessage, `Resuming task from steer queue: ${nextMessage}`)
|
|
673
717
|
return true
|
|
674
718
|
}
|
|
675
719
|
|
|
@@ -10,13 +10,13 @@ export const DEFAULT_TASK_LOG_LIMIT = 10
|
|
|
10
10
|
export type TaskLogsOrder = typeof TASK_LOG_ORDERS[number]
|
|
11
11
|
|
|
12
12
|
export const START_TASKS_DESCRIPTION =
|
|
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
|
|
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, use SendTaskMessage: running tasks continue immediately, while completed or failed tasks resume the same conversation instead of forcing 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.'
|
|
14
14
|
|
|
15
15
|
export const GET_TASK_INFO_DESCRIPTION =
|
|
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
|
|
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 you need to continue the task, call SendTaskMessage with mode "direct" or "steer": active tasks continue immediately, while completed or failed tasks resume the same conversation. If pendingInput is present, answer it with SubmitTaskInput.'
|
|
17
17
|
|
|
18
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.
|
|
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. If the task already completed or failed and you still want to keep working in that same conversation, SendTaskMessage resumes the same task session instead of starting a replacement task. Do not use this to answer pendingInput or pendingInteraction; use SubmitTaskInput for that.'
|
|
20
20
|
|
|
21
21
|
export const SUBMIT_TASK_INPUT_DESCRIPTION =
|
|
22
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.'
|
|
@@ -33,6 +33,9 @@ export const LIST_TASKS_DESCRIPTION =
|
|
|
33
33
|
export const TASK_PERMISSION_MODE_DESCRIPTION =
|
|
34
34
|
'Permission mode for the task. If omitted, inherits the parent session. Raise it only when the task is blocked by permission errors.'
|
|
35
35
|
|
|
36
|
+
export const TASK_MODEL_DESCRIPTION =
|
|
37
|
+
'Model override for the task. Uses the same model selector format as a normal session model setting.'
|
|
38
|
+
|
|
36
39
|
export const TASK_BACKGROUND_DESCRIPTION =
|
|
37
40
|
'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.'
|
|
38
41
|
|
|
@@ -75,7 +78,7 @@ const buildTaskGuidance = (task: {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
if (task.status === 'failed' && hints.length === 0) {
|
|
78
|
-
hints.push('Task failed. Inspect logs and lastError, then
|
|
81
|
+
hints.push('Task failed. Inspect logs and lastError, then use SendTaskMessage to resume it or StartTasks to replace it if needed.')
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
return hints
|