@vibe-forge/mcp 3.1.3 → 3.2.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,172 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AdapterErrorData,
|
|
3
|
-
AskUserQuestionParams,
|
|
4
|
-
PermissionInteractionContext,
|
|
5
|
-
PermissionInteractionDecision,
|
|
6
|
-
SessionPermissionMode
|
|
7
|
-
} from '@vibe-forge/types'
|
|
8
|
-
import { normalizePermissionToolName } from '@vibe-forge/utils'
|
|
9
|
-
import type { PermissionToolSubject } from '@vibe-forge/utils'
|
|
10
|
-
|
|
11
|
-
export { applyTaskPermissionDecision, syncTaskPermissionStateMirror } from './permission-state'
|
|
12
|
-
|
|
13
|
-
const PERMISSION_PROJECT_CONFIG_PATH = '.ai.config.json'
|
|
14
|
-
export const PERMISSION_DECISION_CANCEL = 'cancel'
|
|
15
|
-
|
|
16
|
-
const uniqueStrings = (values: string[]) => [...new Set(values)]
|
|
17
|
-
|
|
18
|
-
const normalizeKeys = (values: string[]) =>
|
|
19
|
-
uniqueStrings(
|
|
20
|
-
values
|
|
21
|
-
.map((value) => normalizePermissionToolName(value)?.key ?? value.trim())
|
|
22
|
-
.filter((value): value is string => value.trim() !== '')
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const buildPermissionOption = (
|
|
26
|
-
label: string,
|
|
27
|
-
value: PermissionInteractionDecision,
|
|
28
|
-
description: string
|
|
29
|
-
) => ({ label, value, description })
|
|
30
|
-
|
|
31
|
-
const resolvePermissionToolSubject = (value: string): PermissionToolSubject | undefined => (
|
|
32
|
-
normalizePermissionToolName(value)
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
export interface PermissionErrorContext {
|
|
36
|
-
subjectKeys: string[]
|
|
37
|
-
deniedTools: string[]
|
|
38
|
-
reasons: string[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const extractPermissionErrorContext = (error: AdapterErrorData): PermissionErrorContext => {
|
|
42
|
-
const details = error.details != null && typeof error.details === 'object'
|
|
43
|
-
? error.details as Record<string, unknown>
|
|
44
|
-
: {}
|
|
45
|
-
const rawDeniedTools = new Set<string>()
|
|
46
|
-
const reasons = new Set<string>()
|
|
47
|
-
|
|
48
|
-
const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
|
|
49
|
-
for (const denial of permissionDenials) {
|
|
50
|
-
if (denial == null || typeof denial !== 'object') continue
|
|
51
|
-
const record = denial as Record<string, unknown>
|
|
52
|
-
if (typeof record.message === 'string' && record.message.trim() !== '') {
|
|
53
|
-
reasons.add(record.message.trim())
|
|
54
|
-
}
|
|
55
|
-
if (Array.isArray(record.deniedTools)) {
|
|
56
|
-
for (const tool of record.deniedTools) {
|
|
57
|
-
if (typeof tool === 'string' && tool.trim() !== '') {
|
|
58
|
-
rawDeniedTools.add(tool.trim())
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (Array.isArray(details.deniedTools)) {
|
|
65
|
-
for (const tool of details.deniedTools) {
|
|
66
|
-
if (typeof tool === 'string' && tool.trim() !== '') {
|
|
67
|
-
rawDeniedTools.add(tool.trim())
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof details.toolName === 'string' && details.toolName.trim() !== '') {
|
|
73
|
-
rawDeniedTools.add(details.toolName.trim())
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (typeof error.message === 'string' && error.message.trim() !== '') {
|
|
77
|
-
reasons.add(error.message.trim())
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const deniedTools = [...rawDeniedTools]
|
|
81
|
-
const subjectKeys = uniqueStrings(
|
|
82
|
-
deniedTools
|
|
83
|
-
.map(tool => resolvePermissionToolSubject(tool)?.key)
|
|
84
|
-
.filter((key): key is string => key != null && key.trim() !== '')
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
subjectKeys,
|
|
89
|
-
deniedTools,
|
|
90
|
-
reasons: [...reasons]
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const resolvePermissionInteractionDecision = (
|
|
95
|
-
answer: string | string[]
|
|
96
|
-
): PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL | undefined => {
|
|
97
|
-
const normalizedAnswer = Array.isArray(answer) ? answer[0] : answer
|
|
98
|
-
if (typeof normalizedAnswer !== 'string') return undefined
|
|
99
|
-
|
|
100
|
-
const raw = normalizedAnswer.trim()
|
|
101
|
-
if (raw === '') return undefined
|
|
102
|
-
if (raw === PERMISSION_DECISION_CANCEL) return PERMISSION_DECISION_CANCEL
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
raw === 'allow_once' ||
|
|
106
|
-
raw === 'allow_session' ||
|
|
107
|
-
raw === 'allow_project' ||
|
|
108
|
-
raw === 'deny_once' ||
|
|
109
|
-
raw === 'deny_session' ||
|
|
110
|
-
raw === 'deny_project'
|
|
111
|
-
) {
|
|
112
|
-
return raw
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return undefined
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const buildPermissionRecoveryPayload = (params: {
|
|
119
|
-
sessionId: string
|
|
120
|
-
adapter?: string
|
|
121
|
-
currentMode?: SessionPermissionMode
|
|
122
|
-
context: PermissionErrorContext
|
|
123
|
-
}): AskUserQuestionParams | undefined => {
|
|
124
|
-
const subjectKeys = normalizeKeys(params.context.subjectKeys)
|
|
125
|
-
if (subjectKeys.length === 0) {
|
|
126
|
-
return undefined
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const primarySubjectKey = subjectKeys[0] ?? 'UnknownTool'
|
|
130
|
-
const subjectLabel = subjectKeys.length <= 1
|
|
131
|
-
? primarySubjectKey
|
|
132
|
-
: `${subjectKeys[0]} 等 ${subjectKeys.length} 项工具`
|
|
133
|
-
const deniedTools = uniqueStrings([
|
|
134
|
-
...params.context.deniedTools,
|
|
135
|
-
...subjectKeys
|
|
136
|
-
])
|
|
137
|
-
const permissionContext: PermissionInteractionContext = {
|
|
138
|
-
adapter: params.adapter,
|
|
139
|
-
currentMode: params.currentMode,
|
|
140
|
-
deniedTools,
|
|
141
|
-
reasons: uniqueStrings(params.context.reasons),
|
|
142
|
-
subjectKey: primarySubjectKey,
|
|
143
|
-
subjectLabel,
|
|
144
|
-
scope: 'tool',
|
|
145
|
-
projectConfigPath: PERMISSION_PROJECT_CONFIG_PATH
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
sessionId: params.sessionId,
|
|
150
|
-
kind: 'permission',
|
|
151
|
-
question: subjectKeys.length <= 1
|
|
152
|
-
? `当前任务需要使用 ${subjectLabel} 才能继续,请选择处理方式。`
|
|
153
|
-
: `当前任务涉及 ${subjectKeys.join('、')} 等工具,请选择处理方式。`,
|
|
154
|
-
options: [
|
|
155
|
-
buildPermissionOption('同意本次', 'allow_once', '仅继续这次被拦截的操作。'),
|
|
156
|
-
buildPermissionOption('同意并在当前会话忽略类似调用', 'allow_session', '本会话内同类工具不再重复询问。'),
|
|
157
|
-
buildPermissionOption(
|
|
158
|
-
'同意并在当前项目忽略类似调用',
|
|
159
|
-
'allow_project',
|
|
160
|
-
`写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
|
|
161
|
-
),
|
|
162
|
-
buildPermissionOption('拒绝本次', 'deny_once', '拒绝当前这次操作。'),
|
|
163
|
-
buildPermissionOption('拒绝并在当前会话阻止类似调用', 'deny_session', '本会话内同类工具直接拒绝。'),
|
|
164
|
-
buildPermissionOption(
|
|
165
|
-
'拒绝并在当前项目阻止类似调用',
|
|
166
|
-
'deny_project',
|
|
167
|
-
`写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
|
|
168
|
-
)
|
|
169
|
-
],
|
|
170
|
-
permissionContext
|
|
171
|
-
}
|
|
172
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
|
|
5
|
-
import { buildConfigJsonVariables, loadConfig, updateConfigFile } from '@vibe-forge/config'
|
|
6
|
-
import type { Config, PermissionInteractionDecision } from '@vibe-forge/types'
|
|
7
|
-
import {
|
|
8
|
-
normalizePermissionToolName,
|
|
9
|
-
normalizeSessionPermissionState,
|
|
10
|
-
resolvePermissionMirrorPath
|
|
11
|
-
} from '@vibe-forge/utils'
|
|
12
|
-
import type { SessionPermissionState } from '@vibe-forge/utils'
|
|
13
|
-
|
|
14
|
-
const PERMISSION_PROJECT_CONFIG_PATH = '.ai.config.json'
|
|
15
|
-
|
|
16
|
-
const uniqueStrings = (values: string[]) => [...new Set(values)]
|
|
17
|
-
|
|
18
|
-
const normalizeKeys = (values: string[]) =>
|
|
19
|
-
uniqueStrings(
|
|
20
|
-
values
|
|
21
|
-
.map((value) => normalizePermissionToolName(value)?.key ?? value.trim())
|
|
22
|
-
.filter((value): value is string => value.trim() !== '')
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const removeKeys = (values: string[], keys: Set<string>) => (
|
|
26
|
-
values.filter((value) => {
|
|
27
|
-
const normalized = normalizePermissionToolName(value)?.key ?? value.trim()
|
|
28
|
-
return !keys.has(normalized)
|
|
29
|
-
})
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const buildGeneralSectionValue = (config: Config | undefined, permissions: Config['permissions']) => ({
|
|
33
|
-
baseDir: config?.baseDir,
|
|
34
|
-
effort: config?.effort,
|
|
35
|
-
defaultAdapter: config?.defaultAdapter,
|
|
36
|
-
defaultModelService: config?.defaultModelService,
|
|
37
|
-
defaultModel: config?.defaultModel,
|
|
38
|
-
recommendedModels: config?.recommendedModels,
|
|
39
|
-
interfaceLanguage: config?.interfaceLanguage,
|
|
40
|
-
modelLanguage: config?.modelLanguage,
|
|
41
|
-
announcements: config?.announcements,
|
|
42
|
-
permissions,
|
|
43
|
-
env: config?.env,
|
|
44
|
-
notifications: config?.notifications,
|
|
45
|
-
shortcuts: config?.shortcuts
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const mutateSessionPermissionState = (
|
|
49
|
-
state: SessionPermissionState,
|
|
50
|
-
keys: string[],
|
|
51
|
-
action: PermissionInteractionDecision
|
|
52
|
-
): SessionPermissionState => {
|
|
53
|
-
const targetKeys = normalizeKeys(keys)
|
|
54
|
-
const keySet = new Set(targetKeys)
|
|
55
|
-
const next = normalizeSessionPermissionState(state)
|
|
56
|
-
|
|
57
|
-
switch (action) {
|
|
58
|
-
case 'allow_once':
|
|
59
|
-
next.onceAllow = uniqueStrings([...removeKeys(next.onceAllow, keySet), ...targetKeys])
|
|
60
|
-
next.onceDeny = removeKeys(next.onceDeny, keySet)
|
|
61
|
-
return next
|
|
62
|
-
case 'allow_session':
|
|
63
|
-
next.allow = uniqueStrings([...removeKeys(next.allow, keySet), ...targetKeys])
|
|
64
|
-
next.deny = removeKeys(next.deny, keySet)
|
|
65
|
-
next.onceDeny = removeKeys(next.onceDeny, keySet)
|
|
66
|
-
next.onceAllow = removeKeys(next.onceAllow, keySet)
|
|
67
|
-
return next
|
|
68
|
-
case 'allow_project':
|
|
69
|
-
next.allow = uniqueStrings([...removeKeys(next.allow, keySet), ...targetKeys])
|
|
70
|
-
next.deny = removeKeys(next.deny, keySet)
|
|
71
|
-
next.onceDeny = removeKeys(next.onceDeny, keySet)
|
|
72
|
-
next.onceAllow = removeKeys(next.onceAllow, keySet)
|
|
73
|
-
return next
|
|
74
|
-
case 'deny_once':
|
|
75
|
-
return next
|
|
76
|
-
case 'deny_session':
|
|
77
|
-
next.deny = uniqueStrings([...removeKeys(next.deny, keySet), ...targetKeys])
|
|
78
|
-
next.allow = removeKeys(next.allow, keySet)
|
|
79
|
-
next.onceAllow = removeKeys(next.onceAllow, keySet)
|
|
80
|
-
next.onceDeny = removeKeys(next.onceDeny, keySet)
|
|
81
|
-
return next
|
|
82
|
-
case 'deny_project':
|
|
83
|
-
next.deny = uniqueStrings([...removeKeys(next.deny, keySet), ...targetKeys])
|
|
84
|
-
next.allow = removeKeys(next.allow, keySet)
|
|
85
|
-
next.onceAllow = removeKeys(next.onceAllow, keySet)
|
|
86
|
-
next.onceDeny = removeKeys(next.onceDeny, keySet)
|
|
87
|
-
return next
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const loadTaskConfig = async (cwd: string) =>
|
|
92
|
-
await loadConfig({
|
|
93
|
-
cwd,
|
|
94
|
-
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const buildMergedProjectPermissions = async (cwd: string) => {
|
|
98
|
-
const [projectConfig, userConfig] = await loadTaskConfig(cwd)
|
|
99
|
-
return {
|
|
100
|
-
allow: [
|
|
101
|
-
...(projectConfig?.permissions?.allow ?? []),
|
|
102
|
-
...(userConfig?.permissions?.allow ?? [])
|
|
103
|
-
],
|
|
104
|
-
deny: [
|
|
105
|
-
...(projectConfig?.permissions?.deny ?? []),
|
|
106
|
-
...(userConfig?.permissions?.deny ?? [])
|
|
107
|
-
],
|
|
108
|
-
ask: [
|
|
109
|
-
...(projectConfig?.permissions?.ask ?? []),
|
|
110
|
-
...(userConfig?.permissions?.ask ?? [])
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const updateProjectPermissionLists = async (
|
|
116
|
-
cwd: string,
|
|
117
|
-
keys: string[],
|
|
118
|
-
target: 'allow' | 'deny'
|
|
119
|
-
) => {
|
|
120
|
-
const targetKeys = normalizeKeys(keys)
|
|
121
|
-
const keySet = new Set(targetKeys)
|
|
122
|
-
const [projectConfig] = await loadTaskConfig(cwd)
|
|
123
|
-
const existingPermissions = projectConfig?.permissions ?? {}
|
|
124
|
-
const nextPermissions: Config['permissions'] = {
|
|
125
|
-
...existingPermissions,
|
|
126
|
-
allow: removeKeys(existingPermissions.allow ?? [], keySet),
|
|
127
|
-
deny: removeKeys(existingPermissions.deny ?? [], keySet),
|
|
128
|
-
ask: removeKeys(existingPermissions.ask ?? [], keySet)
|
|
129
|
-
}
|
|
130
|
-
nextPermissions[target] = uniqueStrings([...(nextPermissions[target] ?? []), ...targetKeys])
|
|
131
|
-
|
|
132
|
-
await updateConfigFile({
|
|
133
|
-
workspaceFolder: cwd,
|
|
134
|
-
source: 'project',
|
|
135
|
-
section: 'general',
|
|
136
|
-
value: buildGeneralSectionValue(projectConfig, nextPermissions)
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const syncTaskPermissionStateMirror = async (params: {
|
|
141
|
-
cwd: string
|
|
142
|
-
adapter?: string
|
|
143
|
-
sessionId: string
|
|
144
|
-
permissionState: SessionPermissionState
|
|
145
|
-
}) => {
|
|
146
|
-
if (params.adapter !== 'claude-code' && params.adapter !== 'opencode') {
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const mirrorPath = resolvePermissionMirrorPath(params.cwd, params.adapter, params.sessionId)
|
|
151
|
-
const projectPermissions = await buildMergedProjectPermissions(params.cwd)
|
|
152
|
-
await mkdir(dirname(mirrorPath), { recursive: true })
|
|
153
|
-
await writeFile(
|
|
154
|
-
mirrorPath,
|
|
155
|
-
`${
|
|
156
|
-
JSON.stringify(
|
|
157
|
-
{
|
|
158
|
-
sessionId: params.sessionId,
|
|
159
|
-
adapter: params.adapter,
|
|
160
|
-
permissionState: normalizeSessionPermissionState(params.permissionState),
|
|
161
|
-
projectPermissions,
|
|
162
|
-
updatedAt: Date.now()
|
|
163
|
-
},
|
|
164
|
-
null,
|
|
165
|
-
2
|
|
166
|
-
)
|
|
167
|
-
}\n`,
|
|
168
|
-
'utf8'
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export const applyTaskPermissionDecision = async (params: {
|
|
173
|
-
cwd: string
|
|
174
|
-
sessionId: string
|
|
175
|
-
adapter?: string
|
|
176
|
-
permissionState: SessionPermissionState
|
|
177
|
-
subjectKeys: string[]
|
|
178
|
-
action: PermissionInteractionDecision
|
|
179
|
-
}) => {
|
|
180
|
-
const subjectKeys = normalizeKeys(params.subjectKeys)
|
|
181
|
-
if (subjectKeys.length === 0) {
|
|
182
|
-
return normalizeSessionPermissionState(params.permissionState)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (params.action === 'allow_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'allow')
|
|
186
|
-
if (params.action === 'deny_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'deny')
|
|
187
|
-
|
|
188
|
-
const nextState = mutateSessionPermissionState(
|
|
189
|
-
params.permissionState,
|
|
190
|
-
subjectKeys,
|
|
191
|
-
params.action
|
|
192
|
-
)
|
|
193
|
-
await syncTaskPermissionStateMirror({
|
|
194
|
-
cwd: params.cwd,
|
|
195
|
-
adapter: params.adapter,
|
|
196
|
-
sessionId: params.sessionId,
|
|
197
|
-
permissionState: nextState
|
|
198
|
-
})
|
|
199
|
-
return nextState
|
|
200
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
|
-
|
|
3
|
-
import type { SessionPermissionMode } from '@vibe-forge/types'
|
|
4
|
-
|
|
5
|
-
import type { TaskInfo } from './manager'
|
|
6
|
-
|
|
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]
|
|
11
|
-
|
|
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, 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
|
-
|
|
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 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
|
-
|
|
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. 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
|
-
|
|
21
|
-
export const SUBMIT_TASK_INPUT_DESCRIPTION =
|
|
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.'
|
|
23
|
-
|
|
24
|
-
export const RESPOND_TASK_INTERACTION_DESCRIPTION =
|
|
25
|
-
'Deprecated alias of SubmitTaskInput. Use SubmitTaskInput for both permission prompts and generic task input.'
|
|
26
|
-
|
|
27
|
-
export const STOP_TASK_DESCRIPTION =
|
|
28
|
-
'Stop a running or blocked task. Use this when the task is no longer needed or cannot recover cleanly.'
|
|
29
|
-
|
|
30
|
-
export const LIST_TASKS_DESCRIPTION =
|
|
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.'
|
|
32
|
-
|
|
33
|
-
export const TASK_PERMISSION_MODE_DESCRIPTION =
|
|
34
|
-
'Permission mode for the task. If omitted, inherits the parent session. Raise it only when the task is blocked by permission errors.'
|
|
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
|
-
|
|
39
|
-
export const TASK_BACKGROUND_DESCRIPTION =
|
|
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.'
|
|
41
|
-
|
|
42
|
-
export const TASK_LOG_LIMIT_DESCRIPTION =
|
|
43
|
-
`How many recent log entries to include. Defaults to ${DEFAULT_TASK_LOG_LIMIT}.`
|
|
44
|
-
|
|
45
|
-
export const TASK_LOG_ORDER_DESCRIPTION =
|
|
46
|
-
'Order of the selected log window. Defaults to "desc", which returns newer log lines first. Use "asc" for oldest-to-newest order.'
|
|
47
|
-
|
|
48
|
-
export const resolveInheritedPermissionMode = (): SessionPermissionMode | undefined => {
|
|
49
|
-
const value = process.env.__VF_PROJECT_AI_PERMISSION_MODE__?.trim()
|
|
50
|
-
if (value == null || value === '') return undefined
|
|
51
|
-
return (SESSION_PERMISSION_MODES as readonly string[]).includes(value)
|
|
52
|
-
? value as SessionPermissionMode
|
|
53
|
-
: undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const buildTaskGuidance = (task: {
|
|
57
|
-
status?: string
|
|
58
|
-
pendingInteraction?: { payload?: { options?: Array<{ label: string; value?: string }> } }
|
|
59
|
-
lastError?: { code?: string }
|
|
60
|
-
}) => {
|
|
61
|
-
const hints: string[] = []
|
|
62
|
-
|
|
63
|
-
if (task.pendingInteraction != null) {
|
|
64
|
-
const optionValues = task.pendingInteraction.payload?.options
|
|
65
|
-
?.map(option => option.value ?? option.label)
|
|
66
|
-
.filter((value): value is string => value.trim() !== '')
|
|
67
|
-
hints.push(
|
|
68
|
-
optionValues != null && optionValues.length > 0
|
|
69
|
-
? `Task is waiting for input. Call SubmitTaskInput with one of: ${optionValues.join(', ')}.`
|
|
70
|
-
: 'Task is waiting for input. Call SubmitTaskInput with the desired answer.'
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (task.lastError?.code === 'permission_required') {
|
|
75
|
-
hints.push(
|
|
76
|
-
'Task hit a permission error. Retry StartTasks with a more suitable permissionMode, or update project permissions before retrying.'
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (task.status === 'failed' && hints.length === 0) {
|
|
81
|
-
hints.push(
|
|
82
|
-
'Task failed. Inspect logs and lastError, then use SendTaskMessage to resume it or StartTasks to replace it if needed.'
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return hints
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const serializeTaskInfo = (params: {
|
|
90
|
-
taskId: string
|
|
91
|
-
description?: string
|
|
92
|
-
status?: TaskInfo['status']
|
|
93
|
-
info?: TaskInfo
|
|
94
|
-
logLimit?: number
|
|
95
|
-
logOrder?: TaskLogsOrder
|
|
96
|
-
}) => {
|
|
97
|
-
const info = params.info
|
|
98
|
-
const safeInfo = (() => {
|
|
99
|
-
if (info == null) {
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
|
-
const { session, onStop, serverSync, createdAt, logs, ...rest } = info
|
|
103
|
-
return rest
|
|
104
|
-
})()
|
|
105
|
-
const selectedLogs = (() => {
|
|
106
|
-
const logs = info?.logs ?? []
|
|
107
|
-
const limit = params.logLimit
|
|
108
|
-
const windowedLogs = limit == null
|
|
109
|
-
? logs
|
|
110
|
-
: logs.slice(Math.max(0, logs.length - limit))
|
|
111
|
-
return params.logOrder === 'desc'
|
|
112
|
-
? [...windowedLogs].reverse()
|
|
113
|
-
: windowedLogs
|
|
114
|
-
})()
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
taskId: params.taskId,
|
|
118
|
-
description: params.description ?? info?.description,
|
|
119
|
-
status: info?.status ?? params.status,
|
|
120
|
-
logs: selectedLogs,
|
|
121
|
-
pendingInput: safeInfo?.pendingInteraction,
|
|
122
|
-
...safeInfo,
|
|
123
|
-
guidance: buildTaskGuidance(safeInfo ?? {})
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
import type { Register } from '../types'
|
|
4
|
-
import type { TaskManager } from './manager'
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_TASK_LOG_LIMIT,
|
|
7
|
-
GET_TASK_INFO_DESCRIPTION,
|
|
8
|
-
LIST_TASKS_DESCRIPTION,
|
|
9
|
-
RESPOND_TASK_INTERACTION_DESCRIPTION,
|
|
10
|
-
SEND_TASK_MESSAGE_DESCRIPTION,
|
|
11
|
-
STOP_TASK_DESCRIPTION,
|
|
12
|
-
SUBMIT_TASK_INPUT_DESCRIPTION,
|
|
13
|
-
TASK_LOG_LIMIT_DESCRIPTION,
|
|
14
|
-
TASK_LOG_ORDERS,
|
|
15
|
-
TASK_LOG_ORDER_DESCRIPTION
|
|
16
|
-
} from './presentation'
|
|
17
|
-
import {
|
|
18
|
-
createSerializedTaskInfoContent,
|
|
19
|
-
createSerializedTaskListContent,
|
|
20
|
-
createTextContent
|
|
21
|
-
} from './task-tool-responses'
|
|
22
|
-
|
|
23
|
-
export const registerTaskRuntimeTools = (
|
|
24
|
-
server: Parameters<Register>[0],
|
|
25
|
-
taskManager: TaskManager
|
|
26
|
-
) => {
|
|
27
|
-
server.registerTool(
|
|
28
|
-
'GetTaskInfo',
|
|
29
|
-
{
|
|
30
|
-
title: 'Get Task Info',
|
|
31
|
-
description: GET_TASK_INFO_DESCRIPTION,
|
|
32
|
-
inputSchema: z.object({
|
|
33
|
-
taskId: z.string().describe('The ID of the task to check'),
|
|
34
|
-
logLimit: z
|
|
35
|
-
.number()
|
|
36
|
-
.int()
|
|
37
|
-
.min(1)
|
|
38
|
-
.describe(TASK_LOG_LIMIT_DESCRIPTION)
|
|
39
|
-
.default(DEFAULT_TASK_LOG_LIMIT),
|
|
40
|
-
logOrder: z
|
|
41
|
-
.enum(TASK_LOG_ORDERS)
|
|
42
|
-
.describe(TASK_LOG_ORDER_DESCRIPTION)
|
|
43
|
-
.default('desc')
|
|
44
|
-
})
|
|
45
|
-
},
|
|
46
|
-
async ({ taskId, logLimit, logOrder }) => {
|
|
47
|
-
const task = taskManager.getTask(taskId)
|
|
48
|
-
if (!task) {
|
|
49
|
-
return {
|
|
50
|
-
content: createTextContent(`Task ${taskId} not found.`),
|
|
51
|
-
isError: true
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
content: createSerializedTaskInfoContent({ taskId, info: task, logLimit, logOrder })
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
server.registerTool(
|
|
61
|
-
'SendTaskMessage',
|
|
62
|
-
{
|
|
63
|
-
title: 'Send Task Message',
|
|
64
|
-
description: SEND_TASK_MESSAGE_DESCRIPTION,
|
|
65
|
-
inputSchema: z.object({
|
|
66
|
-
taskId: z.string().describe('The ID of the running task to continue'),
|
|
67
|
-
message: z
|
|
68
|
-
.string()
|
|
69
|
-
.trim()
|
|
70
|
-
.min(1)
|
|
71
|
-
.describe('The follow-up instruction to send to the task'),
|
|
72
|
-
mode: z
|
|
73
|
-
.enum(['direct', 'steer'])
|
|
74
|
-
.describe('How to deliver the message: direct (default) or steer')
|
|
75
|
-
.default('direct')
|
|
76
|
-
})
|
|
77
|
-
},
|
|
78
|
-
async ({ taskId, message, mode }) => {
|
|
79
|
-
await taskManager.sendTaskMessage({
|
|
80
|
-
taskId,
|
|
81
|
-
message,
|
|
82
|
-
mode
|
|
83
|
-
})
|
|
84
|
-
const task = taskManager.getTask(taskId)
|
|
85
|
-
return {
|
|
86
|
-
content: createSerializedTaskInfoContent({ taskId, info: task })
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
server.registerTool(
|
|
92
|
-
'SubmitTaskInput',
|
|
93
|
-
{
|
|
94
|
-
title: 'Submit Task Input',
|
|
95
|
-
description: SUBMIT_TASK_INPUT_DESCRIPTION,
|
|
96
|
-
inputSchema: z.object({
|
|
97
|
-
taskId: z.string().describe('The ID of the task that is waiting for input'),
|
|
98
|
-
interactionId: z
|
|
99
|
-
.string()
|
|
100
|
-
.describe('Optional interaction ID. Omit it when the task only has one pending input.')
|
|
101
|
-
.optional(),
|
|
102
|
-
data: z
|
|
103
|
-
.union([z.string(), z.array(z.string()).min(1)])
|
|
104
|
-
.describe('The input to submit. Prefer pendingInput.payload.options[].value when available.')
|
|
105
|
-
})
|
|
106
|
-
},
|
|
107
|
-
async ({ taskId, interactionId, data }) => {
|
|
108
|
-
await taskManager.submitTaskInput({
|
|
109
|
-
taskId,
|
|
110
|
-
interactionId,
|
|
111
|
-
data
|
|
112
|
-
})
|
|
113
|
-
const task = taskManager.getTask(taskId)
|
|
114
|
-
return {
|
|
115
|
-
content: createSerializedTaskInfoContent({ taskId, info: task })
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
server.registerTool(
|
|
121
|
-
'RespondTaskInteraction',
|
|
122
|
-
{
|
|
123
|
-
title: 'Respond Task Interaction',
|
|
124
|
-
description: RESPOND_TASK_INTERACTION_DESCRIPTION,
|
|
125
|
-
inputSchema: z.object({
|
|
126
|
-
taskId: z.string().describe('The ID of the task that is waiting for input'),
|
|
127
|
-
interactionId: z
|
|
128
|
-
.string()
|
|
129
|
-
.describe('Optional interaction ID. Omit it when the task only has one pending interaction.')
|
|
130
|
-
.optional(),
|
|
131
|
-
response: z
|
|
132
|
-
.union([z.string(), z.array(z.string()).min(1)])
|
|
133
|
-
.describe('The selected answer. Prefer pendingInteraction.options[].value when available.')
|
|
134
|
-
})
|
|
135
|
-
},
|
|
136
|
-
async ({ taskId, interactionId, response }) => {
|
|
137
|
-
await taskManager.submitTaskInput({
|
|
138
|
-
taskId,
|
|
139
|
-
interactionId,
|
|
140
|
-
data: response
|
|
141
|
-
})
|
|
142
|
-
const task = taskManager.getTask(taskId)
|
|
143
|
-
return {
|
|
144
|
-
content: createSerializedTaskInfoContent({ taskId, info: task })
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
server.registerTool(
|
|
150
|
-
'StopTask',
|
|
151
|
-
{
|
|
152
|
-
title: 'Stop Task',
|
|
153
|
-
description: STOP_TASK_DESCRIPTION,
|
|
154
|
-
inputSchema: z.object({
|
|
155
|
-
taskId: z.string().describe('The ID of the task to stop')
|
|
156
|
-
})
|
|
157
|
-
},
|
|
158
|
-
async ({ taskId }) => {
|
|
159
|
-
const success = taskManager.stopTask(taskId)
|
|
160
|
-
return {
|
|
161
|
-
content: createTextContent(
|
|
162
|
-
success ? `Task ${taskId} stopped.` : `Failed to stop task ${taskId} (not found or already stopped).`
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
server.registerTool(
|
|
169
|
-
'ListTasks',
|
|
170
|
-
{
|
|
171
|
-
title: 'List Tasks',
|
|
172
|
-
description: LIST_TASKS_DESCRIPTION,
|
|
173
|
-
inputSchema: z.object({
|
|
174
|
-
logLimit: z
|
|
175
|
-
.number()
|
|
176
|
-
.int()
|
|
177
|
-
.min(1)
|
|
178
|
-
.describe(TASK_LOG_LIMIT_DESCRIPTION)
|
|
179
|
-
.default(DEFAULT_TASK_LOG_LIMIT),
|
|
180
|
-
logOrder: z
|
|
181
|
-
.enum(TASK_LOG_ORDERS)
|
|
182
|
-
.describe(TASK_LOG_ORDER_DESCRIPTION)
|
|
183
|
-
.default('desc')
|
|
184
|
-
})
|
|
185
|
-
},
|
|
186
|
-
async ({ logLimit, logOrder }) => {
|
|
187
|
-
const tasks = taskManager.getAllTasks()
|
|
188
|
-
return {
|
|
189
|
-
content: createSerializedTaskListContent(tasks, logLimit, logOrder)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
)
|
|
193
|
-
}
|