@vibe-forge/mcp 3.1.3 → 3.3.0-rc.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/AGENTS.md +4 -9
- package/__tests__/tools.spec.ts +6 -2
- package/package.json +4 -8
- package/src/index.ts +1 -13
- package/src/tools/index.ts +1 -3
- package/src/types.ts +0 -13
- package/__tests__/sync.spec.ts +0 -23
- package/__tests__/task-manager.spec.ts +0 -871
- package/__tests__/task-tool.spec.ts +0 -378
- package/src/sync.ts +0 -70
- package/src/tools/task/index.ts +0 -126
- package/src/tools/task/manager.ts +0 -875
- package/src/tools/task/permission-recovery.ts +0 -172
- package/src/tools/task/permission-state.ts +0 -200
- package/src/tools/task/presentation.ts +0 -125
- package/src/tools/task/register-task-runtime-tools.ts +0 -193
- package/src/tools/task/task-tool-responses.ts +0 -40
|
@@ -1,875 +0,0 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
|
-
|
|
3
|
-
import { loadInjectDefaultSystemPromptValue, mergeSystemPrompts } from '@vibe-forge/config'
|
|
4
|
-
import { callHook } from '@vibe-forge/hooks'
|
|
5
|
-
import { generateAdapterQueryOptions, run } from '@vibe-forge/task'
|
|
6
|
-
import type {
|
|
7
|
-
AdapterErrorData,
|
|
8
|
-
AdapterOutputEvent,
|
|
9
|
-
AskUserQuestionParams,
|
|
10
|
-
ChatMessage,
|
|
11
|
-
McpTaskSession,
|
|
12
|
-
PermissionInteractionDecision,
|
|
13
|
-
SessionPermissionMode
|
|
14
|
-
} from '@vibe-forge/types'
|
|
15
|
-
import { createEmptySessionPermissionState, normalizePermissionToolName } from '@vibe-forge/utils'
|
|
16
|
-
import type { SessionPermissionState } from '@vibe-forge/utils'
|
|
17
|
-
import { extractTextFromMessage } from '@vibe-forge/utils/chat-message'
|
|
18
|
-
|
|
19
|
-
import { fetchSessionMessages, postSessionEvent } from '#~/sync.js'
|
|
20
|
-
import {
|
|
21
|
-
PERMISSION_DECISION_CANCEL,
|
|
22
|
-
applyTaskPermissionDecision,
|
|
23
|
-
buildPermissionRecoveryPayload,
|
|
24
|
-
extractPermissionErrorContext,
|
|
25
|
-
resolvePermissionInteractionDecision,
|
|
26
|
-
syncTaskPermissionStateMirror
|
|
27
|
-
} from './permission-recovery'
|
|
28
|
-
|
|
29
|
-
const TASK_PERMISSION_CONTINUE_PROMPT = '权限规则已更新。请继续刚才被权限拦截的工作,并重试被阻止的操作。'
|
|
30
|
-
|
|
31
|
-
interface ServerSyncState {
|
|
32
|
-
sessionId: string
|
|
33
|
-
lastEventIndex: number
|
|
34
|
-
lastAssistantMessageId?: string
|
|
35
|
-
seenMessageIds: Set<string>
|
|
36
|
-
lastPollError?: string
|
|
37
|
-
poller?: NodeJS.Timeout
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface PendingTaskInteraction {
|
|
41
|
-
id: string
|
|
42
|
-
payload: AskUserQuestionParams
|
|
43
|
-
source: 'adapter' | 'permission_recovery'
|
|
44
|
-
subjectKeys?: string[]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type ManagedTaskSession = McpTaskSession & {
|
|
48
|
-
respondInteraction?: (interactionId: string, data: string | string[]) => void | Promise<void>
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface TaskInfo {
|
|
52
|
-
taskId: string
|
|
53
|
-
adapter?: string
|
|
54
|
-
model?: string
|
|
55
|
-
description: string
|
|
56
|
-
type?: 'default' | 'spec' | 'entity' | 'workspace'
|
|
57
|
-
name?: string
|
|
58
|
-
workspaceCwd?: string
|
|
59
|
-
permissionMode?: SessionPermissionMode
|
|
60
|
-
background?: boolean
|
|
61
|
-
status: 'running' | 'waiting_input' | 'completed' | 'failed'
|
|
62
|
-
exitCode?: number
|
|
63
|
-
logs: string[]
|
|
64
|
-
permissionState: SessionPermissionState
|
|
65
|
-
queuedSteerMessages: string[]
|
|
66
|
-
pendingInteraction?: PendingTaskInteraction
|
|
67
|
-
lastError?: AdapterErrorData
|
|
68
|
-
session?: ManagedTaskSession
|
|
69
|
-
createdAt: number
|
|
70
|
-
onStop?: () => void
|
|
71
|
-
serverSync?: ServerSyncState
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const appendTaskLog = (task: TaskInfo, message: string | undefined) => {
|
|
75
|
-
const normalized = message?.trim()
|
|
76
|
-
if (normalized == null || normalized === '') return
|
|
77
|
-
if (task.logs.at(-1) === normalized) return
|
|
78
|
-
task.logs.push(normalized)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const extractMessageLogText = (message: ChatMessage) => {
|
|
82
|
-
const extracted = extractTextFromMessage(message)?.trim()
|
|
83
|
-
if (extracted != null && extracted !== '') {
|
|
84
|
-
return extracted
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!Array.isArray(message.content)) {
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
for (const item of message.content) {
|
|
92
|
-
if (item.type === 'tool_result') {
|
|
93
|
-
if (typeof item.content === 'string' && item.content.trim() !== '') {
|
|
94
|
-
return item.content.trim()
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
const serialized = JSON.stringify(item.content)
|
|
98
|
-
return serialized === '""' ? undefined : serialized
|
|
99
|
-
} catch {
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return undefined
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const formatInteractionLog = (payload: AskUserQuestionParams) => {
|
|
109
|
-
const options = payload.options
|
|
110
|
-
?.map(option => option.value ?? option.label)
|
|
111
|
-
.filter((value): value is string => value.trim() !== '')
|
|
112
|
-
const optionsSuffix = options != null && options.length > 0
|
|
113
|
-
? ` Available responses: ${options.join(', ')}.`
|
|
114
|
-
: ''
|
|
115
|
-
return `Waiting for ${
|
|
116
|
-
payload.kind === 'permission' ? 'permission' : 'user'
|
|
117
|
-
} input: ${payload.question}${optionsSuffix}`
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const formatPermissionErrorLog = (error: AdapterErrorData) => {
|
|
121
|
-
if (error.code !== 'permission_required' || error.details == null || typeof error.details !== 'object') {
|
|
122
|
-
return undefined
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const details = error.details as {
|
|
126
|
-
permissionDenials?: Array<{ message?: string; deniedTools?: string[] }>
|
|
127
|
-
}
|
|
128
|
-
const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
|
|
129
|
-
const deniedTools = [
|
|
130
|
-
...new Set(permissionDenials.flatMap(item => Array.isArray(item.deniedTools) ? item.deniedTools : []))
|
|
131
|
-
]
|
|
132
|
-
const reasons = permissionDenials
|
|
133
|
-
.map(item => item.message?.trim())
|
|
134
|
-
.filter((value): value is string => value != null && value !== '')
|
|
135
|
-
|
|
136
|
-
const parts = []
|
|
137
|
-
if (deniedTools.length > 0) {
|
|
138
|
-
parts.push(`Denied tools: ${deniedTools.join(', ')}`)
|
|
139
|
-
}
|
|
140
|
-
if (reasons.length > 0) {
|
|
141
|
-
parts.push(`Reasons: ${reasons.join(' | ')}`)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return parts.length > 0 ? parts.join(' | ') : undefined
|
|
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
|
-
|
|
166
|
-
export class TaskManager {
|
|
167
|
-
private tasks: Map<string, TaskInfo> = new Map()
|
|
168
|
-
private permissionToolUseCache = new Map<string, Map<string, string>>()
|
|
169
|
-
|
|
170
|
-
private getPermissionToolUseCache(taskId: string) {
|
|
171
|
-
let cache = this.permissionToolUseCache.get(taskId)
|
|
172
|
-
if (cache == null) {
|
|
173
|
-
cache = new Map<string, string>()
|
|
174
|
-
this.permissionToolUseCache.set(taskId, cache)
|
|
175
|
-
}
|
|
176
|
-
return cache
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private trimPermissionToolUseCache(taskId: string, maxSize = 128) {
|
|
180
|
-
const cache = this.permissionToolUseCache.get(taskId)
|
|
181
|
-
if (cache == null) return
|
|
182
|
-
|
|
183
|
-
while (cache.size > maxSize) {
|
|
184
|
-
const firstKey = cache.keys().next().value as string | undefined
|
|
185
|
-
if (firstKey == null) break
|
|
186
|
-
cache.delete(firstKey)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private rememberPermissionToolUses(taskId: string, message: ChatMessage) {
|
|
191
|
-
if (!Array.isArray(message.content)) {
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const cache = this.getPermissionToolUseCache(taskId)
|
|
196
|
-
for (const item of message.content) {
|
|
197
|
-
if (
|
|
198
|
-
item == null ||
|
|
199
|
-
typeof item !== 'object' ||
|
|
200
|
-
item.type !== 'tool_use' ||
|
|
201
|
-
typeof item.id !== 'string' ||
|
|
202
|
-
item.id.trim() === ''
|
|
203
|
-
) {
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const rawName = typeof item.name === 'string' && item.name.trim() !== ''
|
|
208
|
-
? item.name.trim()
|
|
209
|
-
: undefined
|
|
210
|
-
const normalizedToolName = rawName?.startsWith('adapter:')
|
|
211
|
-
? rawName.split(':').at(-1)?.trim() ?? rawName
|
|
212
|
-
: rawName
|
|
213
|
-
const subject = normalizePermissionToolName(normalizedToolName ?? rawName)
|
|
214
|
-
if (subject == null) {
|
|
215
|
-
continue
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
cache.set(item.id.trim(), subject.key)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.trimPermissionToolUseCache(taskId)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private resolvePermissionErrorContext(taskId: string, error: AdapterErrorData) {
|
|
225
|
-
const context = extractPermissionErrorContext(error)
|
|
226
|
-
if (context.subjectKeys.length > 0) {
|
|
227
|
-
return context
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const details = error.details != null && typeof error.details === 'object'
|
|
231
|
-
? error.details as Record<string, unknown>
|
|
232
|
-
: {}
|
|
233
|
-
const toolUseId = typeof details.toolUseId === 'string' && details.toolUseId.trim() !== ''
|
|
234
|
-
? details.toolUseId.trim()
|
|
235
|
-
: undefined
|
|
236
|
-
if (toolUseId == null) {
|
|
237
|
-
return context
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const cachedSubjectKey = this.permissionToolUseCache.get(taskId)?.get(toolUseId)
|
|
241
|
-
if (cachedSubjectKey == null || cachedSubjectKey.trim() === '') {
|
|
242
|
-
return context
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
subjectKeys: [...new Set([...context.subjectKeys, cachedSubjectKey])],
|
|
247
|
-
deniedTools: [...new Set([...context.deniedTools, cachedSubjectKey])],
|
|
248
|
-
reasons: context.reasons
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
public async startTask(options: {
|
|
253
|
-
taskId: string
|
|
254
|
-
description: string
|
|
255
|
-
type?: 'default' | 'spec' | 'entity' | 'workspace'
|
|
256
|
-
name?: string
|
|
257
|
-
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
|
|
258
|
-
adapter?: string
|
|
259
|
-
model?: string
|
|
260
|
-
background?: boolean
|
|
261
|
-
enableServerSync?: boolean
|
|
262
|
-
}): Promise<{ taskId: string; logs?: string[] }> {
|
|
263
|
-
const {
|
|
264
|
-
taskId,
|
|
265
|
-
adapter,
|
|
266
|
-
model,
|
|
267
|
-
description,
|
|
268
|
-
type,
|
|
269
|
-
name,
|
|
270
|
-
permissionMode,
|
|
271
|
-
background = true,
|
|
272
|
-
enableServerSync
|
|
273
|
-
} = options
|
|
274
|
-
|
|
275
|
-
// Initialize Task Info
|
|
276
|
-
const taskInfo: TaskInfo = {
|
|
277
|
-
taskId,
|
|
278
|
-
adapter,
|
|
279
|
-
model,
|
|
280
|
-
description,
|
|
281
|
-
type,
|
|
282
|
-
name,
|
|
283
|
-
permissionMode,
|
|
284
|
-
background,
|
|
285
|
-
status: 'running',
|
|
286
|
-
logs: [],
|
|
287
|
-
permissionState: createEmptySessionPermissionState(),
|
|
288
|
-
queuedSteerMessages: [],
|
|
289
|
-
createdAt: Date.now()
|
|
290
|
-
}
|
|
291
|
-
if (enableServerSync) {
|
|
292
|
-
taskInfo.serverSync = {
|
|
293
|
-
sessionId: taskId,
|
|
294
|
-
lastEventIndex: 0,
|
|
295
|
-
seenMessageIds: new Set()
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
this.tasks.set(taskId, taskInfo)
|
|
299
|
-
this.permissionToolUseCache.set(taskId, new Map())
|
|
300
|
-
|
|
301
|
-
await this.launchTask(taskInfo, 'create')
|
|
302
|
-
|
|
303
|
-
if (!background) {
|
|
304
|
-
await new Promise<void>((resolve) => {
|
|
305
|
-
const task = this.tasks.get(taskId)
|
|
306
|
-
if (!task) {
|
|
307
|
-
resolve()
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
if (task.status !== 'running') {
|
|
311
|
-
resolve()
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
task.onStop = resolve
|
|
315
|
-
})
|
|
316
|
-
return { taskId, logs: taskInfo.logs }
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return { taskId }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private async launchTask(
|
|
323
|
-
task: TaskInfo,
|
|
324
|
-
runType: 'create' | 'resume',
|
|
325
|
-
resumeMessage?: string
|
|
326
|
-
) {
|
|
327
|
-
try {
|
|
328
|
-
const rootCwd = process.cwd()
|
|
329
|
-
const promptType = task.type !== 'default' ? task.type : undefined
|
|
330
|
-
const promptName = task.name
|
|
331
|
-
const promptCWD = process.cwd()
|
|
332
|
-
const [data, resolvedConfig] = await generateAdapterQueryOptions(
|
|
333
|
-
promptType,
|
|
334
|
-
promptName,
|
|
335
|
-
promptCWD,
|
|
336
|
-
{
|
|
337
|
-
adapter: task.adapter,
|
|
338
|
-
model: task.model
|
|
339
|
-
}
|
|
340
|
-
)
|
|
341
|
-
const taskCwd = resolvedConfig.workspace?.cwd ?? promptCWD
|
|
342
|
-
task.workspaceCwd = resolvedConfig.workspace?.cwd
|
|
343
|
-
const env = {
|
|
344
|
-
...process.env,
|
|
345
|
-
__VF_PROJECT_AI_CTX_ID__: task.taskId,
|
|
346
|
-
__VF_PROJECT_WORKSPACE_FOLDER__: taskCwd,
|
|
347
|
-
__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__: rootCwd
|
|
348
|
-
}
|
|
349
|
-
await callHook('GenerateSystemPrompt', {
|
|
350
|
-
cwd: taskCwd,
|
|
351
|
-
sessionId: task.taskId,
|
|
352
|
-
type: promptType,
|
|
353
|
-
name: promptName,
|
|
354
|
-
data
|
|
355
|
-
}, env)
|
|
356
|
-
|
|
357
|
-
const injectDefaultSystemPrompt = await loadInjectDefaultSystemPromptValue(taskCwd)
|
|
358
|
-
const ctxId = task.taskId
|
|
359
|
-
const { session, resolvedAdapter } = await run({
|
|
360
|
-
adapter: task.adapter,
|
|
361
|
-
cwd: taskCwd,
|
|
362
|
-
env: {
|
|
363
|
-
...env,
|
|
364
|
-
__VF_PROJECT_AI_CTX_ID__: ctxId
|
|
365
|
-
}
|
|
366
|
-
}, {
|
|
367
|
-
type: runType,
|
|
368
|
-
runtime: 'mcp',
|
|
369
|
-
mode: 'stream',
|
|
370
|
-
sessionId: task.taskId,
|
|
371
|
-
model: task.model,
|
|
372
|
-
systemPrompt: mergeSystemPrompts({
|
|
373
|
-
generatedSystemPrompt: resolvedConfig.systemPrompt,
|
|
374
|
-
injectDefaultSystemPrompt
|
|
375
|
-
}),
|
|
376
|
-
permissionMode: task.permissionMode,
|
|
377
|
-
tools: resolvedConfig.tools,
|
|
378
|
-
skills: resolvedConfig.skills,
|
|
379
|
-
mcpServers: resolvedConfig.mcpServers,
|
|
380
|
-
promptAssetIds: resolvedConfig.promptAssetIds,
|
|
381
|
-
assetBundle: resolvedConfig.assetBundle,
|
|
382
|
-
onEvent: (event: AdapterOutputEvent) => {
|
|
383
|
-
this.handleEvent(task.taskId, event)
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
const current = this.tasks.get(task.taskId)
|
|
388
|
-
if (current == null) {
|
|
389
|
-
session.kill()
|
|
390
|
-
return
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
current.adapter = resolvedAdapter ?? current.adapter
|
|
394
|
-
current.session = session as ManagedTaskSession
|
|
395
|
-
current.status = 'running'
|
|
396
|
-
current.exitCode = undefined
|
|
397
|
-
current.pendingInteraction = undefined
|
|
398
|
-
current.lastError = undefined
|
|
399
|
-
try {
|
|
400
|
-
await syncTaskPermissionStateMirror({
|
|
401
|
-
cwd: taskCwd,
|
|
402
|
-
adapter: current.adapter,
|
|
403
|
-
sessionId: current.taskId,
|
|
404
|
-
permissionState: current.permissionState
|
|
405
|
-
})
|
|
406
|
-
} catch (error) {
|
|
407
|
-
appendTaskLog(
|
|
408
|
-
current,
|
|
409
|
-
`Permission mirror sync failed: ${error instanceof Error ? error.message : String(error)}`
|
|
410
|
-
)
|
|
411
|
-
}
|
|
412
|
-
this.startServerPolling(task.taskId)
|
|
413
|
-
this.emitTaskUserMessage(
|
|
414
|
-
current,
|
|
415
|
-
current.session,
|
|
416
|
-
resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
|
|
417
|
-
)
|
|
418
|
-
} catch (err) {
|
|
419
|
-
const current = this.tasks.get(task.taskId)
|
|
420
|
-
if (current) {
|
|
421
|
-
current.status = 'failed'
|
|
422
|
-
appendTaskLog(current, `Failed to start task: ${err instanceof Error ? err.message : String(err)}`)
|
|
423
|
-
current.onStop?.()
|
|
424
|
-
}
|
|
425
|
-
throw err
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private handleEvent(taskId: string, event: AdapterOutputEvent) {
|
|
430
|
-
const task = this.tasks.get(taskId)
|
|
431
|
-
if (!task) return
|
|
432
|
-
|
|
433
|
-
const shouldSyncEvent = !(task.status === 'failed' && (event.type === 'exit' || event.type === 'stop'))
|
|
434
|
-
if (shouldSyncEvent) {
|
|
435
|
-
void this.syncEvent(task, event)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
switch (event.type) {
|
|
439
|
-
case 'message': {
|
|
440
|
-
const message = event.data as ChatMessage
|
|
441
|
-
this.rememberPermissionToolUses(taskId, message)
|
|
442
|
-
if (message?.id) {
|
|
443
|
-
task.serverSync?.seenMessageIds.add(message.id)
|
|
444
|
-
}
|
|
445
|
-
if (message?.role === 'assistant' && message.id) {
|
|
446
|
-
if (task.serverSync) {
|
|
447
|
-
task.serverSync.lastAssistantMessageId = message.id
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
appendTaskLog(task, extractMessageLogText(message))
|
|
451
|
-
break
|
|
452
|
-
}
|
|
453
|
-
case 'interaction_request':
|
|
454
|
-
task.status = 'waiting_input'
|
|
455
|
-
task.pendingInteraction = {
|
|
456
|
-
id: event.data.id,
|
|
457
|
-
payload: event.data.payload,
|
|
458
|
-
source: 'adapter'
|
|
459
|
-
}
|
|
460
|
-
appendTaskLog(task, formatInteractionLog(event.data.payload))
|
|
461
|
-
task.onStop?.()
|
|
462
|
-
break
|
|
463
|
-
case 'error': {
|
|
464
|
-
task.lastError = event.data
|
|
465
|
-
appendTaskLog(task, event.data.message)
|
|
466
|
-
appendTaskLog(task, formatPermissionErrorLog(event.data))
|
|
467
|
-
if (event.data.code === 'permission_required') {
|
|
468
|
-
const permissionContext = this.resolvePermissionErrorContext(taskId, event.data)
|
|
469
|
-
const payload = buildPermissionRecoveryPayload({
|
|
470
|
-
sessionId: task.taskId,
|
|
471
|
-
adapter: task.adapter,
|
|
472
|
-
currentMode: task.permissionMode,
|
|
473
|
-
context: permissionContext
|
|
474
|
-
})
|
|
475
|
-
if (payload != null) {
|
|
476
|
-
task.status = 'waiting_input'
|
|
477
|
-
task.pendingInteraction = {
|
|
478
|
-
id: `task-recovery:${task.taskId}:${Date.now()}`,
|
|
479
|
-
payload,
|
|
480
|
-
source: 'permission_recovery',
|
|
481
|
-
subjectKeys: permissionContext.subjectKeys
|
|
482
|
-
}
|
|
483
|
-
appendTaskLog(task, formatInteractionLog(payload))
|
|
484
|
-
this.stopServerPolling(taskId)
|
|
485
|
-
void this.syncSyntheticInteraction(task)
|
|
486
|
-
task.onStop?.()
|
|
487
|
-
break
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (event.data.fatal !== false) {
|
|
491
|
-
task.status = 'failed'
|
|
492
|
-
task.pendingInteraction = undefined
|
|
493
|
-
this.stopServerPolling(taskId)
|
|
494
|
-
task.onStop?.()
|
|
495
|
-
}
|
|
496
|
-
break
|
|
497
|
-
}
|
|
498
|
-
case 'stop': {
|
|
499
|
-
task.session = undefined
|
|
500
|
-
if (task.status === 'failed') {
|
|
501
|
-
this.stopServerPolling(taskId)
|
|
502
|
-
task.onStop?.()
|
|
503
|
-
break
|
|
504
|
-
}
|
|
505
|
-
task.status = 'completed'
|
|
506
|
-
task.pendingInteraction = undefined
|
|
507
|
-
this.stopServerPolling(taskId)
|
|
508
|
-
if (task.queuedSteerMessages.length > 0) {
|
|
509
|
-
void this.dispatchQueuedSteerMessage(task).catch((error) => {
|
|
510
|
-
task.status = 'failed'
|
|
511
|
-
appendTaskLog(
|
|
512
|
-
task,
|
|
513
|
-
`Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
|
|
514
|
-
)
|
|
515
|
-
task.onStop?.()
|
|
516
|
-
})
|
|
517
|
-
break
|
|
518
|
-
}
|
|
519
|
-
task.onStop?.()
|
|
520
|
-
break
|
|
521
|
-
}
|
|
522
|
-
case 'exit':
|
|
523
|
-
task.session = undefined
|
|
524
|
-
if (task.status === 'failed') {
|
|
525
|
-
task.exitCode = event.data.exitCode ?? undefined
|
|
526
|
-
this.stopServerPolling(taskId)
|
|
527
|
-
task.onStop?.()
|
|
528
|
-
break
|
|
529
|
-
}
|
|
530
|
-
if (task.status === 'waiting_input') {
|
|
531
|
-
task.exitCode = event.data.exitCode ?? undefined
|
|
532
|
-
this.stopServerPolling(taskId)
|
|
533
|
-
task.onStop?.()
|
|
534
|
-
break
|
|
535
|
-
}
|
|
536
|
-
task.status = event.data.exitCode === 0 ? 'completed' : 'failed'
|
|
537
|
-
task.exitCode = event.data.exitCode ?? undefined
|
|
538
|
-
task.pendingInteraction = undefined
|
|
539
|
-
appendTaskLog(task, `Process exited with code ${event.data.exitCode}`)
|
|
540
|
-
this.stopServerPolling(taskId)
|
|
541
|
-
if (task.status === 'completed' && task.queuedSteerMessages.length > 0) {
|
|
542
|
-
void this.dispatchQueuedSteerMessage(task).catch((error) => {
|
|
543
|
-
task.status = 'failed'
|
|
544
|
-
appendTaskLog(
|
|
545
|
-
task,
|
|
546
|
-
`Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
|
|
547
|
-
)
|
|
548
|
-
task.onStop?.()
|
|
549
|
-
})
|
|
550
|
-
break
|
|
551
|
-
}
|
|
552
|
-
task.onStop?.()
|
|
553
|
-
break
|
|
554
|
-
default:
|
|
555
|
-
break
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
private startServerPolling(taskId: string) {
|
|
560
|
-
const task = this.tasks.get(taskId)
|
|
561
|
-
if (!task?.serverSync) return
|
|
562
|
-
if (task.serverSync.poller) return
|
|
563
|
-
|
|
564
|
-
const poll = async () => {
|
|
565
|
-
const current = this.tasks.get(taskId)
|
|
566
|
-
if (!current?.serverSync || !current.session) return
|
|
567
|
-
try {
|
|
568
|
-
const events = await fetchSessionMessages(current.serverSync.sessionId)
|
|
569
|
-
current.serverSync.lastPollError = undefined
|
|
570
|
-
const startIndex = current.serverSync.lastEventIndex
|
|
571
|
-
const newEvents = events.slice(startIndex)
|
|
572
|
-
current.serverSync.lastEventIndex = events.length
|
|
573
|
-
|
|
574
|
-
for (const ev of newEvents) {
|
|
575
|
-
if (ev.type !== 'message') continue
|
|
576
|
-
if (ev.message.role !== 'user') continue
|
|
577
|
-
if (ev.message.id && current.serverSync.seenMessageIds.has(ev.message.id)) {
|
|
578
|
-
continue
|
|
579
|
-
}
|
|
580
|
-
if (ev.message.id) {
|
|
581
|
-
current.serverSync.seenMessageIds.add(ev.message.id)
|
|
582
|
-
}
|
|
583
|
-
const text = extractTextFromMessage(ev.message).trim()
|
|
584
|
-
if (text === '') continue
|
|
585
|
-
current.session.emit({
|
|
586
|
-
type: 'message',
|
|
587
|
-
content: [{ type: 'text', text }],
|
|
588
|
-
parentUuid: current.serverSync.lastAssistantMessageId
|
|
589
|
-
})
|
|
590
|
-
}
|
|
591
|
-
} catch (error) {
|
|
592
|
-
const message = `Sync poll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
593
|
-
if (current.serverSync.lastPollError !== message) {
|
|
594
|
-
current.serverSync.lastPollError = message
|
|
595
|
-
appendTaskLog(current, message)
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
task.serverSync.poller = setInterval(() => {
|
|
601
|
-
void poll()
|
|
602
|
-
}, 1000)
|
|
603
|
-
void poll()
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
private stopServerPolling(taskId: string) {
|
|
607
|
-
const task = this.tasks.get(taskId)
|
|
608
|
-
if (task?.serverSync?.poller) {
|
|
609
|
-
clearInterval(task.serverSync.poller)
|
|
610
|
-
task.serverSync.poller = undefined
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private async syncEvent(task: TaskInfo, event: AdapterOutputEvent) {
|
|
615
|
-
if (!task.serverSync) return
|
|
616
|
-
try {
|
|
617
|
-
await postSessionEvent(task.serverSync.sessionId, event as unknown as Record<string, unknown>)
|
|
618
|
-
} catch (err) {
|
|
619
|
-
appendTaskLog(task, `Sync event failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
private async syncSyntheticInteraction(task: TaskInfo) {
|
|
624
|
-
if (task.serverSync == null || task.pendingInteraction == null) return
|
|
625
|
-
try {
|
|
626
|
-
await postSessionEvent(task.serverSync.sessionId, {
|
|
627
|
-
type: 'interaction_request',
|
|
628
|
-
id: task.pendingInteraction.id,
|
|
629
|
-
payload: task.pendingInteraction.payload
|
|
630
|
-
})
|
|
631
|
-
} catch (error) {
|
|
632
|
-
appendTaskLog(task, `Sync interaction request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
public getTask(taskId: string): TaskInfo | undefined {
|
|
637
|
-
return this.tasks.get(taskId)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
public getAllTasks(): TaskInfo[] {
|
|
641
|
-
return Array.from(this.tasks.values())
|
|
642
|
-
}
|
|
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
|
-
|
|
665
|
-
public async sendTaskMessage(params: {
|
|
666
|
-
taskId: string
|
|
667
|
-
message: string
|
|
668
|
-
mode?: 'direct' | 'steer'
|
|
669
|
-
}): Promise<void> {
|
|
670
|
-
const task = this.tasks.get(params.taskId)
|
|
671
|
-
if (task == null) {
|
|
672
|
-
throw new Error(`Task ${params.taskId} not found.`)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const message = params.message.trim()
|
|
676
|
-
const mode = params.mode ?? 'direct'
|
|
677
|
-
if (message === '') {
|
|
678
|
-
throw new Error('Task message cannot be empty.')
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (task.pendingInteraction != null || task.status === 'waiting_input') {
|
|
682
|
-
throw new Error(`Task ${params.taskId} is waiting for input. Use SubmitTaskInput instead.`)
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (mode === 'steer') {
|
|
686
|
-
if (task.status === 'completed' || task.status === 'failed') {
|
|
687
|
-
await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
task.queuedSteerMessages.push(message)
|
|
691
|
-
appendTaskLog(task, `Queued task message (${mode}): ${message}`)
|
|
692
|
-
return
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (task.status === 'completed' || task.status === 'failed') {
|
|
696
|
-
await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
|
|
697
|
-
return
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (task.status !== 'running' || task.session == null) {
|
|
701
|
-
throw new Error(`Task ${params.taskId} is not running. Start a new task instead.`)
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
await this.syncTaskUserMessage(task, message)
|
|
705
|
-
this.emitTaskUserMessage(task, task.session, message)
|
|
706
|
-
|
|
707
|
-
appendTaskLog(task, `User message submitted (${mode}): ${message}`)
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async dispatchQueuedSteerMessage(task: TaskInfo) {
|
|
711
|
-
const nextMessage = task.queuedSteerMessages.shift()
|
|
712
|
-
if (nextMessage == null || nextMessage.trim() === '') {
|
|
713
|
-
return false
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
await this.resumeTaskWithMessage(task, nextMessage, `Resuming task from steer queue: ${nextMessage}`)
|
|
717
|
-
return true
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
public async submitTaskInput(params: {
|
|
721
|
-
taskId: string
|
|
722
|
-
interactionId?: string
|
|
723
|
-
data: string | string[]
|
|
724
|
-
}): Promise<void> {
|
|
725
|
-
const task = this.tasks.get(params.taskId)
|
|
726
|
-
if (task == null) {
|
|
727
|
-
throw new Error(`Task ${params.taskId} not found.`)
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const pendingInteraction = task.pendingInteraction
|
|
731
|
-
if (pendingInteraction == null) {
|
|
732
|
-
throw new Error(`Task ${params.taskId} does not have a pending interaction.`)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const interactionId = params.interactionId ?? pendingInteraction.id
|
|
736
|
-
if (interactionId !== pendingInteraction.id) {
|
|
737
|
-
throw new Error(`Interaction ${interactionId} is not pending for task ${params.taskId}.`)
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (pendingInteraction.source === 'adapter') {
|
|
741
|
-
if (task.session?.respondInteraction == null) {
|
|
742
|
-
throw new Error(`Task ${params.taskId} does not support interaction responses.`)
|
|
743
|
-
}
|
|
744
|
-
await task.session.respondInteraction(interactionId, params.data)
|
|
745
|
-
await this.syncTaskInputResponse(task, interactionId, params.data)
|
|
746
|
-
task.pendingInteraction = undefined
|
|
747
|
-
task.status = 'running'
|
|
748
|
-
const responseText = Array.isArray(params.data) ? params.data.join(', ') : params.data
|
|
749
|
-
appendTaskLog(task, `Interaction response submitted: ${responseText}`)
|
|
750
|
-
return
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const decision = resolvePermissionInteractionDecision(params.data)
|
|
754
|
-
if (decision == null) {
|
|
755
|
-
throw new Error(`Task ${params.taskId} requires a permission decision response.`)
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
await this.syncTaskInputResponse(task, interactionId, params.data)
|
|
759
|
-
if (decision === PERMISSION_DECISION_CANCEL) {
|
|
760
|
-
task.pendingInteraction = undefined
|
|
761
|
-
task.status = 'failed'
|
|
762
|
-
appendTaskLog(task, 'Permission recovery cancelled. Task will not continue.')
|
|
763
|
-
task.onStop?.()
|
|
764
|
-
return
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
task.permissionState = await applyTaskPermissionDecision({
|
|
768
|
-
cwd: process.cwd(),
|
|
769
|
-
sessionId: task.taskId,
|
|
770
|
-
adapter: task.adapter,
|
|
771
|
-
permissionState: task.permissionState,
|
|
772
|
-
subjectKeys: pendingInteraction.subjectKeys ?? [],
|
|
773
|
-
action: decision as PermissionInteractionDecision
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
decision === 'deny_once' ||
|
|
778
|
-
decision === 'deny_session' ||
|
|
779
|
-
decision === 'deny_project'
|
|
780
|
-
) {
|
|
781
|
-
task.pendingInteraction = undefined
|
|
782
|
-
task.status = 'failed'
|
|
783
|
-
appendTaskLog(task, `Permission decision applied: ${decision}. Task will not continue.`)
|
|
784
|
-
task.onStop?.()
|
|
785
|
-
return
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
task.pendingInteraction = undefined
|
|
789
|
-
task.status = 'running'
|
|
790
|
-
appendTaskLog(task, `Permission decision applied: ${decision}. Restarting task.`)
|
|
791
|
-
await this.launchTask(task, 'resume')
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
public async respondToTaskInteraction(params: {
|
|
795
|
-
taskId: string
|
|
796
|
-
interactionId?: string
|
|
797
|
-
data: string | string[]
|
|
798
|
-
}): Promise<void> {
|
|
799
|
-
await this.submitTaskInput(params)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private async syncTaskInputResponse(task: TaskInfo, interactionId: string, data: string | string[]) {
|
|
803
|
-
if (!task.serverSync) return
|
|
804
|
-
try {
|
|
805
|
-
await postSessionEvent(task.serverSync.sessionId, {
|
|
806
|
-
type: 'interaction_response',
|
|
807
|
-
id: interactionId,
|
|
808
|
-
data
|
|
809
|
-
})
|
|
810
|
-
} catch (error) {
|
|
811
|
-
appendTaskLog(
|
|
812
|
-
task,
|
|
813
|
-
`Sync interaction response failed: ${error instanceof Error ? error.message : String(error)}`
|
|
814
|
-
)
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
private async syncStoppedTask(params: {
|
|
819
|
-
task: TaskInfo
|
|
820
|
-
pendingInteraction?: PendingTaskInteraction
|
|
821
|
-
}) {
|
|
822
|
-
const { task, pendingInteraction } = params
|
|
823
|
-
if (!task.serverSync) return
|
|
824
|
-
|
|
825
|
-
if (pendingInteraction != null) {
|
|
826
|
-
try {
|
|
827
|
-
await postSessionEvent(task.serverSync.sessionId, {
|
|
828
|
-
type: 'interaction_response',
|
|
829
|
-
id: pendingInteraction.id,
|
|
830
|
-
data: PERMISSION_DECISION_CANCEL
|
|
831
|
-
})
|
|
832
|
-
} catch (error) {
|
|
833
|
-
appendTaskLog(
|
|
834
|
-
task,
|
|
835
|
-
`Sync interaction cancellation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
836
|
-
)
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
try {
|
|
841
|
-
await postSessionEvent(task.serverSync.sessionId, {
|
|
842
|
-
type: 'error',
|
|
843
|
-
data: {
|
|
844
|
-
message: 'Task stopped by user',
|
|
845
|
-
fatal: true
|
|
846
|
-
}
|
|
847
|
-
})
|
|
848
|
-
} catch (error) {
|
|
849
|
-
appendTaskLog(
|
|
850
|
-
task,
|
|
851
|
-
`Sync stop event failed: ${error instanceof Error ? error.message : String(error)}`
|
|
852
|
-
)
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
public stopTask(taskId: string): boolean {
|
|
857
|
-
const task = this.tasks.get(taskId)
|
|
858
|
-
if (task && (task.session != null || task.pendingInteraction != null || task.status === 'waiting_input')) {
|
|
859
|
-
const pendingInteraction = task.pendingInteraction
|
|
860
|
-
task.session?.kill()
|
|
861
|
-
task.session = undefined
|
|
862
|
-
task.pendingInteraction = undefined
|
|
863
|
-
appendTaskLog(task, 'Task stopped by user')
|
|
864
|
-
task.status = 'failed' // or 'stopped' if we had that status
|
|
865
|
-
this.stopServerPolling(taskId)
|
|
866
|
-
void this.syncStoppedTask({
|
|
867
|
-
task,
|
|
868
|
-
pendingInteraction
|
|
869
|
-
})
|
|
870
|
-
if (task.onStop) task.onStop()
|
|
871
|
-
return true
|
|
872
|
-
}
|
|
873
|
-
return false
|
|
874
|
-
}
|
|
875
|
-
}
|