@vibe-forge/core 0.7.3 → 0.7.5
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/package.json +1 -1
- package/src/adapter/type.ts +5 -0
- package/src/config/types.ts +1 -1
- package/src/controllers/task/generate-adapter-query-options.ts +10 -203
- package/src/controllers/task/prepare.ts +7 -1
- package/src/controllers/task/run.ts +52 -12
- package/src/hooks/bridge.ts +368 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/loader.ts +5 -1
- package/src/hooks/native.ts +116 -0
- package/src/hooks/runtime.ts +2 -2
- package/src/hooks/type.ts +30 -5
- package/src/utils/workspace-assets.ts +919 -0
package/package.json
CHANGED
package/src/adapter/type.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Cache, Config } from '@vibe-forge/core'
|
|
2
2
|
|
|
3
3
|
import type { Logger } from '#~/utils/create-logger.js'
|
|
4
|
+
import type { AdapterAssetPlan, AssetDiagnostic, WorkspaceAssetBundle } from '#~/utils/workspace-assets.js'
|
|
4
5
|
|
|
5
6
|
import type { ChatMessage, ChatMessageContent } from '../types'
|
|
6
7
|
|
|
@@ -35,6 +36,7 @@ export interface SessionInitInfo {
|
|
|
35
36
|
cwd: string
|
|
36
37
|
agents: string[]
|
|
37
38
|
title?: string
|
|
39
|
+
assetDiagnostics?: AssetDiagnostic[]
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface SessionSummaryInfo {
|
|
@@ -62,6 +64,7 @@ export interface AdapterCtx {
|
|
|
62
64
|
logger: Logger
|
|
63
65
|
|
|
64
66
|
configs: [Config?, Config?]
|
|
67
|
+
assets?: WorkspaceAssetBundle
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface AdapterQueryOptions {
|
|
@@ -91,6 +94,8 @@ export interface AdapterQueryOptions {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
extraOptions?: string[]
|
|
97
|
+
promptAssetIds?: string[]
|
|
98
|
+
assetPlan?: AdapterAssetPlan
|
|
94
99
|
|
|
95
100
|
onEvent: (event: AdapterOutputEvent) => void
|
|
96
101
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface ModelServiceConfig {
|
|
|
43
43
|
timeoutMs?: number
|
|
44
44
|
/**
|
|
45
45
|
* 模型服务默认最大输出 token。
|
|
46
|
-
* - Codex:
|
|
46
|
+
* - Codex: 对 routed model service 通过本地代理写入 Responses API `max_output_tokens`
|
|
47
47
|
* - Claude Code Router: 映射为 `maxtoken` transformer
|
|
48
48
|
* - OpenCode: 映射为 model `options.maxOutputTokens` / `limit.output`
|
|
49
49
|
*/
|
|
@@ -1,102 +1,7 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import { basename, dirname } from 'node:path'
|
|
3
2
|
|
|
4
3
|
import type { AdapterQueryOptions } from '#~/adapter/type.js'
|
|
5
|
-
import {
|
|
6
|
-
import type { Definition, Entity, Filter, Skill, Spec } from '#~/utils/definition-loader.js'
|
|
7
|
-
|
|
8
|
-
const filterSkills = (
|
|
9
|
-
skills: Definition<Skill>[],
|
|
10
|
-
selection?: AdapterQueryOptions['skills']
|
|
11
|
-
) => {
|
|
12
|
-
if (selection == null) return skills
|
|
13
|
-
|
|
14
|
-
const include = selection.include != null && selection.include.length > 0
|
|
15
|
-
? new Set(selection.include)
|
|
16
|
-
: undefined
|
|
17
|
-
const exclude = new Set(selection.exclude ?? [])
|
|
18
|
-
|
|
19
|
-
return skills.filter((skill) => {
|
|
20
|
-
const name = basename(dirname(skill.path))
|
|
21
|
-
return (include == null || include.has(name)) && !exclude.has(name)
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const dedupeSkills = (skills: Definition<Skill>[]) => {
|
|
26
|
-
const seen = new Set<string>()
|
|
27
|
-
return skills.filter((skill) => {
|
|
28
|
-
if (seen.has(skill.path)) return false
|
|
29
|
-
seen.add(skill.path)
|
|
30
|
-
return true
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type SkillSelectionInput =
|
|
35
|
-
| AdapterQueryOptions['skills']
|
|
36
|
-
| Entity['skills']
|
|
37
|
-
| Spec['skills']
|
|
38
|
-
|
|
39
|
-
const toNormalizedSkillSelection = (
|
|
40
|
-
selection?: SkillSelectionInput
|
|
41
|
-
): AdapterQueryOptions['skills'] | undefined => {
|
|
42
|
-
if (selection == null) return undefined
|
|
43
|
-
|
|
44
|
-
if (Array.isArray(selection)) {
|
|
45
|
-
return selection.length > 0
|
|
46
|
-
? {
|
|
47
|
-
include: selection
|
|
48
|
-
}
|
|
49
|
-
: undefined
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if ('type' in selection && Array.isArray(selection.list)) {
|
|
53
|
-
const list = selection.list.filter((item): item is string => typeof item === 'string')
|
|
54
|
-
return selection.type === 'include'
|
|
55
|
-
? {
|
|
56
|
-
include: list
|
|
57
|
-
}
|
|
58
|
-
: {
|
|
59
|
-
exclude: list
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return selection
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const mergeSkillSelections = (
|
|
67
|
-
...selections: Array<SkillSelectionInput | undefined>
|
|
68
|
-
): AdapterQueryOptions['skills'] | undefined => {
|
|
69
|
-
let include: Set<string> | undefined
|
|
70
|
-
const exclude = new Set<string>()
|
|
71
|
-
|
|
72
|
-
for (const selection of selections) {
|
|
73
|
-
const normalized = toNormalizedSkillSelection(selection)
|
|
74
|
-
if (normalized == null) continue
|
|
75
|
-
|
|
76
|
-
if (normalized.include != null && normalized.include.length > 0) {
|
|
77
|
-
const current = new Set(normalized.include)
|
|
78
|
-
include = include == null
|
|
79
|
-
? current
|
|
80
|
-
: new Set([...include].filter(item => current.has(item)))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const item of normalized.exclude ?? []) {
|
|
84
|
-
exclude.add(item)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (include == null && exclude.size === 0) return undefined
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
include: include == null ? undefined : [...include],
|
|
92
|
-
exclude: exclude.size === 0 ? undefined : [...exclude]
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const getIncludedSkillNames = (selection?: SkillSelectionInput): string[] => {
|
|
97
|
-
const normalized = toNormalizedSkillSelection(selection)
|
|
98
|
-
return normalized?.include ?? []
|
|
99
|
-
}
|
|
4
|
+
import { resolvePromptAssetSelection, resolveWorkspaceAssetBundle } from '#~/utils/workspace-assets.js'
|
|
100
5
|
|
|
101
6
|
export async function generateAdapterQueryOptions(
|
|
102
7
|
type: 'spec' | 'entity' | undefined,
|
|
@@ -106,113 +11,15 @@ export async function generateAdapterQueryOptions(
|
|
|
106
11
|
skills?: AdapterQueryOptions['skills']
|
|
107
12
|
}
|
|
108
13
|
) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const entities = type !== 'entity'
|
|
117
|
-
? await loader.loadDefaultEntities()
|
|
118
|
-
: []
|
|
119
|
-
const defaultSkills = await loader.loadDefaultSkills()
|
|
120
|
-
const rules = await loader.loadDefaultRules()
|
|
121
|
-
const specs = await loader.loadDefaultSpecs()
|
|
122
|
-
|
|
123
|
-
// 1.2 获取指定数据
|
|
124
|
-
const targetSkills: Definition<Skill>[] = []
|
|
125
|
-
let targetBody = ''
|
|
126
|
-
let targetToolsFilter: Filter | undefined
|
|
127
|
-
let targetMcpServersFilter: Filter | undefined
|
|
128
|
-
if (type && name) {
|
|
129
|
-
const data = {
|
|
130
|
-
spec: await loader.loadSpec(name),
|
|
131
|
-
entity: await loader.loadEntity(name)
|
|
132
|
-
}[type]
|
|
133
|
-
if (!data) {
|
|
134
|
-
throw new Error(`Failed to load ${type} ${name}`)
|
|
135
|
-
}
|
|
136
|
-
const { attributes, body } = data
|
|
137
|
-
if (
|
|
138
|
-
attributes.rules
|
|
139
|
-
) {
|
|
140
|
-
// always load spec or entity tagged rules
|
|
141
|
-
rules.push(
|
|
142
|
-
...(
|
|
143
|
-
await loader.loadRules(attributes.rules, {
|
|
144
|
-
baseDir: dirname(data.path)
|
|
145
|
-
})
|
|
146
|
-
).map((rule) => ({
|
|
147
|
-
...rule,
|
|
148
|
-
attributes: {
|
|
149
|
-
...rule.attributes,
|
|
150
|
-
// 实体或流程中的规则为默认加载
|
|
151
|
-
always: true
|
|
152
|
-
}
|
|
153
|
-
}))
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
if (
|
|
157
|
-
attributes.skills
|
|
158
|
-
) {
|
|
159
|
-
effectiveSkillSelection = mergeSkillSelections(
|
|
160
|
-
input?.skills,
|
|
161
|
-
attributes.skills
|
|
162
|
-
)
|
|
163
|
-
targetSkills.push(
|
|
164
|
-
...filterSkills(
|
|
165
|
-
await loader.loadSkills(getIncludedSkillNames(attributes.skills)),
|
|
166
|
-
effectiveSkillSelection
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
targetBody = body
|
|
172
|
-
targetToolsFilter = attributes.tools
|
|
173
|
-
targetMcpServersFilter = attributes.mcpServers
|
|
174
|
-
}
|
|
175
|
-
const skills = filterSkills(defaultSkills, effectiveSkillSelection)
|
|
176
|
-
let selectedSkillsPrompt: Definition<Skill>[] = []
|
|
177
|
-
if (input?.skills?.include != null && input.skills.include.length > 0) {
|
|
178
|
-
selectedSkillsPrompt = dedupeSkills(
|
|
179
|
-
filterSkills(
|
|
180
|
-
await loader.loadSkills(input.skills.include),
|
|
181
|
-
effectiveSkillSelection
|
|
182
|
-
)
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 2. 基于数据生成上下文
|
|
187
|
-
// 2.1 加载关联上下文
|
|
188
|
-
systemPromptParts.push(loader.generateRulesPrompt(rules))
|
|
189
|
-
systemPromptParts.push(loader.generateSkillsPrompt(dedupeSkills(targetSkills)))
|
|
190
|
-
systemPromptParts.push(loader.generateSkillsPrompt(
|
|
191
|
-
selectedSkillsPrompt.filter(skill => !targetSkills.some(target => target.path === skill.path))
|
|
192
|
-
))
|
|
193
|
-
// 2.2 加载上下文路由
|
|
194
|
-
systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
|
|
195
|
-
systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
|
|
196
|
-
systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
|
|
197
|
-
// 2.3 加载目标上下文与配置
|
|
198
|
-
systemPromptParts.push(targetBody)
|
|
199
|
-
targetToolsFilter && (
|
|
200
|
-
options.tools = targetToolsFilter
|
|
201
|
-
)
|
|
202
|
-
targetMcpServersFilter && (
|
|
203
|
-
options.mcpServers = targetMcpServersFilter
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
options.systemPrompt = systemPromptParts.join('\n\n')
|
|
14
|
+
const bundle = await resolveWorkspaceAssetBundle({ cwd })
|
|
15
|
+
const [data, resolvedOptions] = await resolvePromptAssetSelection({
|
|
16
|
+
bundle,
|
|
17
|
+
type,
|
|
18
|
+
name,
|
|
19
|
+
input
|
|
20
|
+
})
|
|
207
21
|
return [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
targetSkills,
|
|
211
|
-
entities,
|
|
212
|
-
skills,
|
|
213
|
-
specs,
|
|
214
|
-
targetBody
|
|
215
|
-
},
|
|
216
|
-
options
|
|
22
|
+
data,
|
|
23
|
+
resolvedOptions as Partial<AdapterQueryOptions>
|
|
217
24
|
] as const
|
|
218
25
|
}
|
|
@@ -5,6 +5,7 @@ import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/core/adapter'
|
|
|
5
5
|
import { getCache, setCache } from '@vibe-forge/core/utils/cache'
|
|
6
6
|
import { createLogger } from '@vibe-forge/core/utils/create-logger'
|
|
7
7
|
import { uuid } from '@vibe-forge/core/utils/uuid'
|
|
8
|
+
import { resolveWorkspaceAssetBundle } from '@vibe-forge/core/utils/workspace-assets'
|
|
8
9
|
|
|
9
10
|
import { resolveServerLogLevel } from '#~/env.js'
|
|
10
11
|
|
|
@@ -52,6 +53,10 @@ export const prepare = async (
|
|
|
52
53
|
__VF_PROJECT_WORKSPACE_FOLDER__: cwd
|
|
53
54
|
}
|
|
54
55
|
const [config, userConfig] = await loadConfig({ jsonVariables })
|
|
56
|
+
const assets = await resolveWorkspaceAssetBundle({
|
|
57
|
+
cwd,
|
|
58
|
+
configs: [config, userConfig]
|
|
59
|
+
})
|
|
55
60
|
return [
|
|
56
61
|
{
|
|
57
62
|
ctxId,
|
|
@@ -62,7 +67,8 @@ export const prepare = async (
|
|
|
62
67
|
get: (key) => getCache(cwd, ctxId, sessionId, key)
|
|
63
68
|
},
|
|
64
69
|
logger,
|
|
65
|
-
configs: [config, userConfig]
|
|
70
|
+
configs: [config, userConfig],
|
|
71
|
+
assets
|
|
66
72
|
} satisfies AdapterCtx
|
|
67
73
|
] as const
|
|
68
74
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions } from '#~/adapter/index.js'
|
|
2
2
|
import { loadAdapter } from '#~/adapter/index.js'
|
|
3
3
|
import type { ModelServiceConfig } from '#~/config.js'
|
|
4
|
+
import { createAdapterHookBridge } from '#~/hooks/bridge.js'
|
|
4
5
|
import { callHook } from '#~/hooks/call.js'
|
|
6
|
+
import { buildAdapterAssetPlan } from '#~/utils/workspace-assets.js'
|
|
5
7
|
import type { TaskDetail } from '#~/types.js'
|
|
6
8
|
|
|
7
9
|
import { prepare } from './prepare'
|
|
@@ -127,14 +129,54 @@ export const run = async (
|
|
|
127
129
|
return adapterNames[0]
|
|
128
130
|
})()
|
|
129
131
|
|
|
132
|
+
const adapter = await loadAdapter(adapterType)
|
|
133
|
+
await adapter.init?.(ctx)
|
|
134
|
+
|
|
135
|
+
const resolvedModel = resolveQueryModel({
|
|
136
|
+
config,
|
|
137
|
+
userConfig,
|
|
138
|
+
inputModel: adapterOptions.model
|
|
139
|
+
})
|
|
140
|
+
|
|
130
141
|
const originalOnEvent = adapterOptions.onEvent
|
|
142
|
+
const supportedAssetPlanAdapters = new Set(['claude-code', 'codex', 'opencode'])
|
|
143
|
+
const assetPlan = ctx.assets == null || !supportedAssetPlanAdapters.has(adapterType)
|
|
144
|
+
? undefined
|
|
145
|
+
: buildAdapterAssetPlan({
|
|
146
|
+
adapter: adapterType as 'claude-code' | 'codex' | 'opencode',
|
|
147
|
+
bundle: ctx.assets,
|
|
148
|
+
options: {
|
|
149
|
+
mcpServers: adapterOptions.mcpServers,
|
|
150
|
+
skills: adapterOptions.skills,
|
|
151
|
+
promptAssetIds: adapterOptions.promptAssetIds
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
const nativeBridgeDisabledEvents = adapterType === 'codex' && ctx.env.__VF_PROJECT_AI_CODEX_NATIVE_HOOKS_AVAILABLE__ === '1'
|
|
155
|
+
? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop']
|
|
156
|
+
: adapterType === 'claude-code' && ctx.env.__VF_PROJECT_AI_CLAUDE_NATIVE_HOOKS_AVAILABLE__ === '1'
|
|
157
|
+
? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop']
|
|
158
|
+
: adapterType === 'opencode' && ctx.env.__VF_PROJECT_AI_OPENCODE_NATIVE_HOOKS_AVAILABLE__ === '1'
|
|
159
|
+
? ['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop']
|
|
160
|
+
: []
|
|
161
|
+
const hookBridge = createAdapterHookBridge({
|
|
162
|
+
ctx,
|
|
163
|
+
adapter: adapterType,
|
|
164
|
+
runtime: adapterOptions.runtime,
|
|
165
|
+
sessionId: adapterOptions.sessionId,
|
|
166
|
+
type: adapterOptions.type,
|
|
167
|
+
model: resolvedModel,
|
|
168
|
+
disabledEvents: nativeBridgeDisabledEvents
|
|
169
|
+
})
|
|
131
170
|
const wrappedOnEvent = (event: AdapterOutputEvent) => {
|
|
171
|
+
hookBridge.handleOutput(event)
|
|
172
|
+
|
|
132
173
|
if (event.type === 'init') {
|
|
133
174
|
originalOnEvent({
|
|
134
175
|
...event,
|
|
135
176
|
data: {
|
|
136
177
|
...event.data,
|
|
137
|
-
adapter: adapterType
|
|
178
|
+
adapter: adapterType,
|
|
179
|
+
assetDiagnostics: assetPlan?.diagnostics ?? event.data.assetDiagnostics
|
|
138
180
|
}
|
|
139
181
|
})
|
|
140
182
|
return
|
|
@@ -161,16 +203,7 @@ export const run = async (
|
|
|
161
203
|
originalOnEvent(event)
|
|
162
204
|
}
|
|
163
205
|
|
|
164
|
-
const
|
|
165
|
-
await adapter.init?.(ctx)
|
|
166
|
-
|
|
167
|
-
const resolvedModel = resolveQueryModel({
|
|
168
|
-
config,
|
|
169
|
-
userConfig,
|
|
170
|
-
inputModel: adapterOptions.model
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
await callHook('TaskStart', {
|
|
206
|
+
const taskStartOutput = await callHook('TaskStart', {
|
|
174
207
|
adapter: adapterType,
|
|
175
208
|
cwd: ctx.cwd,
|
|
176
209
|
sessionId: adapterOptions.sessionId,
|
|
@@ -178,14 +211,21 @@ export const run = async (
|
|
|
178
211
|
options,
|
|
179
212
|
adapterOptions
|
|
180
213
|
}, ctx.env)
|
|
214
|
+
if (taskStartOutput?.continue === false) {
|
|
215
|
+
throw new Error(taskStartOutput.stopReason ?? 'TaskStart hook blocked task startup')
|
|
216
|
+
}
|
|
217
|
+
await hookBridge.start()
|
|
218
|
+
const description = await hookBridge.prepareInitialPrompt(adapterOptions.description)
|
|
181
219
|
const session = await adapter.query(
|
|
182
220
|
ctx,
|
|
183
221
|
{
|
|
184
222
|
...adapterOptions,
|
|
223
|
+
assetPlan,
|
|
224
|
+
description,
|
|
185
225
|
model: resolvedModel,
|
|
186
226
|
onEvent: wrappedOnEvent
|
|
187
227
|
}
|
|
188
228
|
)
|
|
189
229
|
|
|
190
|
-
return { session, ctx }
|
|
230
|
+
return { session: hookBridge.wrapSession(session), ctx }
|
|
191
231
|
}
|