dsh-vision-router 1.7.5 → 1.7.7
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/entry.js +55 -25
- package/lib/adapter-update-coalescer.js +84 -0
- package/lib/legacy-core-vision-policy-bridge.js +274 -0
- package/lib/local-vision-stabilizer.js +30 -6
- package/lib/native-image-coexistence.js +395 -76
- package/lib/session-vision-state.js +20 -3
- package/lib/vision-backend-runtime-policy.js +388 -0
- package/lib/vision-backend-smoke-test-client.js +24 -3
- package/lib/vision-backend-smoke-test.js +66 -20
- package/lib/vision-tool-runtime-boundary.js +17 -7
- package/package.json +3 -3
package/entry.js
CHANGED
|
@@ -12,7 +12,9 @@ import { installVisionRouterFileLogging } from './lib/file-logger.js'
|
|
|
12
12
|
import { contextWithDelegatedReplay } from './lib/replay-delegation.js'
|
|
13
13
|
import { contextWithReplayEnvelopeV2Compat } from './lib/replay-envelope-v2-compat.js'
|
|
14
14
|
import { contextWithVisionExecutionPolicy } from './lib/vision-execution-policy.js'
|
|
15
|
+
import { contextWithVisionBackendRuntimePolicy } from './lib/vision-backend-runtime-policy.js'
|
|
15
16
|
import { contextWithNativeImageCoexistence } from './lib/native-image-coexistence.js'
|
|
17
|
+
import { installLegacyCoreVisionPolicyBridge } from './lib/legacy-core-vision-policy-bridge.js'
|
|
16
18
|
import { installPiAiBridgeWireCompat } from './lib/pi-ai-bridge-wire-compat.js'
|
|
17
19
|
import { installLiveModelDiscovery } from './lib/live-model-discovery.js'
|
|
18
20
|
import { installVisionModelRegistry } from './lib/vision-model-registry.js'
|
|
@@ -56,10 +58,13 @@ export const SETTINGS_CONTRACT_REVISION = 4
|
|
|
56
58
|
// for the settings namespace, so composition config and settings validation
|
|
57
59
|
// agree on the same default.
|
|
58
60
|
core.Config.set('progressiveTools', z.boolean().default(false))
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
61
|
+
// Keep the three timeout layers coherent: one provider call may use up to 120s,
|
|
62
|
+
// one visual task (including fallbacks) shares 120s, and one visual turn shares
|
|
63
|
+
// 120s. The runtime policy below reserves the final quarter of a multi-backend
|
|
64
|
+
// task for fallback, so raising the task ceiling does not revive the historical
|
|
65
|
+
// "120s per backend" stall that #117 removed.
|
|
66
|
+
core.Config.set('visionTaskTimeoutMs', z.number().step(1000).min(1000).max(180000).default(120000))
|
|
67
|
+
core.Config.set('visionTurnBudgetMs', z.number().step(1000).min(10000).max(600000).default(120000))
|
|
63
68
|
|
|
64
69
|
// Both visible entry points — Settings > Vision Router and the legacy
|
|
65
70
|
// Settings > Plugins compatibility card — edit the same Host-owned namespace.
|
|
@@ -113,7 +118,12 @@ export function apply(ctx, config = {}) {
|
|
|
113
118
|
// route is registered. The wrapper patches only webServer.register and hands
|
|
114
119
|
// every injection callback the original child context identity unchanged.
|
|
115
120
|
const localMutationCtx = installLocalMutationRouteBoundary(ctx)
|
|
116
|
-
|
|
121
|
+
// Normalize DSH 0.1.1's prepareCall contract at the deepest private LLM
|
|
122
|
+
// boundary. Higher Vision Router layers can finish wrapping adapter.stream
|
|
123
|
+
// before this boundary captures the final Host-visible adapter, so prepareCall
|
|
124
|
+
// cannot bypass local/runtime/replay stream behavior.
|
|
125
|
+
const adapterContractCtx = contextWithCoalescedAdapterUpdates(localMutationCtx)
|
|
126
|
+
const logging = installVisionRouterFileLogging(adapterContractCtx)
|
|
117
127
|
const delegatedReplayCtx = contextWithDelegatedReplay(logging.ctx, {
|
|
118
128
|
wrapperRoute:
|
|
119
129
|
typeof config.wrapperRoute === 'string' && config.wrapperRoute !== ''
|
|
@@ -144,7 +154,7 @@ export function apply(ctx, config = {}) {
|
|
|
144
154
|
// inference. Install this boundary before the local-vision stabilizer so the
|
|
145
155
|
// final local vision-http adapter is observed after stabilization. Primary
|
|
146
156
|
// local-Ollama image turns finish a cold model load in pre-step, before the
|
|
147
|
-
//
|
|
157
|
+
// visual task budget begins; fallback Ollama warms in the background.
|
|
148
158
|
const ollamaColdStartCtx = installOllamaColdStartGuard(hardenedCtx, hardenedConfig, core)
|
|
149
159
|
// #141 stabilization boundary: keep the recently merged local-vision
|
|
150
160
|
// behavior isolated from main's existing provider/router semantics. It
|
|
@@ -160,10 +170,14 @@ export function apply(ctx, config = {}) {
|
|
|
160
170
|
...bootConfig,
|
|
161
171
|
progressiveTools: hardenedConfig.progressiveTools === true,
|
|
162
172
|
guidanceOverrides: normalizeGuidanceOverrides(bootConfig.guidanceOverrides ?? hardenedConfig.guidanceOverrides),
|
|
173
|
+
visionTaskTimeoutMs:
|
|
174
|
+
Number.isFinite(Number(bootConfig.visionTaskTimeoutMs))
|
|
175
|
+
? Number(bootConfig.visionTaskTimeoutMs)
|
|
176
|
+
: 120000,
|
|
163
177
|
visionTurnBudgetMs:
|
|
164
178
|
Number.isFinite(Number(bootConfig.visionTurnBudgetMs))
|
|
165
179
|
? Number(bootConfig.visionTurnBudgetMs)
|
|
166
|
-
:
|
|
180
|
+
: 120000,
|
|
167
181
|
}
|
|
168
182
|
// The batch-attachment API is the released, non-incidental discriminator
|
|
169
183
|
// between the minimum Host contract and the newer Host-owned integration
|
|
@@ -204,23 +218,32 @@ export function apply(ctx, config = {}) {
|
|
|
204
218
|
// so rc.7/rc.8's synthetic settings injection is visible to the boundary.
|
|
205
219
|
// The secure screenshot renderer owns its exact FsTarget and active browser
|
|
206
220
|
// cancellation directly, so it does not depend on this placement.
|
|
207
|
-
const toolRuntimeCtx = installVisionToolRuntimeBoundary(attachmentCompatCtx)
|
|
221
|
+
const toolRuntimeCtx = installVisionToolRuntimeBoundary(attachmentCompatCtx, runtimeConfig)
|
|
208
222
|
// DSH 0.1.1 publishes an exact native image-capable DeepSeek model. Do not
|
|
209
223
|
// put it ahead of Vision Router's own configured chain: only when the user
|
|
210
224
|
// has explicitly selected any Host-native image route, preserve raw pixels
|
|
211
|
-
// and skip the hidden instant-local caption pass for that turn.
|
|
212
|
-
//
|
|
225
|
+
// and skip the hidden instant-local caption pass for that turn. Ownership is
|
|
226
|
+
// AsyncLocalStorage-scoped and never mutates settings or provider order.
|
|
213
227
|
const nativeImageCompat = contextWithNativeImageCoexistence(toolRuntimeCtx, runtimeConfig)
|
|
228
|
+
// index.js still carries legacy global wrapper/tool gates. Feed the new
|
|
229
|
+
// session policy into that core through one narrow compatibility bridge:
|
|
230
|
+
// native/owned/unknown routes preserve raw pixels, text-only routes are
|
|
231
|
+
// normalized by the core's own rewriteHistoryImages implementation, and the
|
|
232
|
+
// boot-only tool projection ends immediately after core.apply wires schema.
|
|
233
|
+
const legacyCoreCompat = installLegacyCoreVisionPolicyBridge(
|
|
234
|
+
nativeImageCompat.ctx,
|
|
235
|
+
nativeImageCompat.config,
|
|
236
|
+
{ rewriteHistoryImages: core.rewriteHistoryImages },
|
|
237
|
+
)
|
|
214
238
|
// Final structured-flow guard sits closest to core.apply so it sees the
|
|
215
239
|
// actual tool registrations and pre-step listener. It makes bootstrap
|
|
216
240
|
// one-shot, enforces fast/standard/deep/custom quotas, tracks mixed branches,
|
|
217
241
|
// rejects empty/non-evidence results, and applies one shared visual deadline.
|
|
218
|
-
const structuredCtx = installStructuredFlowHardening(
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
const reconciledCtx = contextWithCoalescedAdapterUpdates(structuredCtx)
|
|
242
|
+
const structuredCtx = installStructuredFlowHardening(legacyCoreCompat.ctx, legacyCoreCompat.config)
|
|
243
|
+
// Adapter reconciliation + prepareCall normalization are already installed at
|
|
244
|
+
// the final Host registration boundary above. Wrapping again here would make
|
|
245
|
+
// prepareCall capture a pre-wrapper stream.
|
|
246
|
+
const reconciledCtx = structuredCtx
|
|
224
247
|
// Discover the provider's actual /models list independently of DSH's static
|
|
225
248
|
// catalog. The Host owns credentials/networking/cache; the browser receives
|
|
226
249
|
// model ids only. A live hit is also the evidence required before an
|
|
@@ -259,22 +282,27 @@ export function apply(ctx, config = {}) {
|
|
|
259
282
|
// maxTokensField + route headers at its final fetch boundary. Ordinary DSH
|
|
260
283
|
// streams and unrelated Vision Router HTTP providers remain byte-identical.
|
|
261
284
|
installPiAiBridgeWireCompat(reconciledCtx, logging.logger)
|
|
262
|
-
//
|
|
263
|
-
// pre-wire
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// failures remain authoritative and cannot be retried through a second path.
|
|
285
|
+
// Existing execution policy stays authoritative for adapter-observed failures:
|
|
286
|
+
// only exact local pre-wire admission failures may unlock the post-failure
|
|
287
|
+
// bridge. The outer runtime policy added below handles the complementary case
|
|
288
|
+
// where DSH would silently project pixels to SHA text before the adapter ever
|
|
289
|
+
// gets a chance to reject them.
|
|
268
290
|
const executionCtx = contextWithVisionExecutionPolicy(reconciledCtx, {
|
|
269
291
|
isBridgeEvidence: (provider, model) => liveDiscovery.hasModel(provider, model),
|
|
270
292
|
evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
|
|
271
293
|
logger: logging.logger,
|
|
272
294
|
})
|
|
295
|
+
const backendRuntimeCtx = contextWithVisionBackendRuntimePolicy(executionCtx, {
|
|
296
|
+
config: runtimeConfig,
|
|
297
|
+
core,
|
|
298
|
+
evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
|
|
299
|
+
logger: logging.logger,
|
|
300
|
+
})
|
|
273
301
|
// The smoke-test route sends only a built-in probe image to the exact selected
|
|
274
302
|
// backend. It never walks the configured fallback chain, so a healthy OVH
|
|
275
303
|
// fallback can no longer make a broken custom model look healthy. Its narrow
|
|
276
304
|
// compatibility bridge uses the same live-discovery evidence gate as runtime.
|
|
277
|
-
installVisionBackendSmokeTest(
|
|
305
|
+
installVisionBackendSmokeTest(backendRuntimeCtx, runtimeConfig, core, {
|
|
278
306
|
logger: logging.logger,
|
|
279
307
|
isBridgeEvidence: (provider, model) => liveDiscovery.hasModel(provider, model),
|
|
280
308
|
})
|
|
@@ -283,7 +311,7 @@ export function apply(ctx, config = {}) {
|
|
|
283
311
|
// for data until the OCR slice expires. Materialize only this exact
|
|
284
312
|
// Tesseract-stdin call to a temporary image file; all other execFile calls
|
|
285
313
|
// keep their native behavior.
|
|
286
|
-
installTesseractExecFileCompat(
|
|
314
|
+
installTesseractExecFileCompat(backendRuntimeCtx)
|
|
287
315
|
|
|
288
316
|
// 启动诊断摘要只描述 composition/apply 的基础配置。设置服务可能稍后
|
|
289
317
|
// 覆盖这些值;每个图片轮还会记录 current() 的实时决策,避免把这个
|
|
@@ -304,7 +332,8 @@ export function apply(ctx, config = {}) {
|
|
|
304
332
|
/* diagnostics must never break apply */
|
|
305
333
|
}
|
|
306
334
|
try {
|
|
307
|
-
const result = core.apply(
|
|
335
|
+
const result = core.apply(backendRuntimeCtx, legacyCoreCompat.config)
|
|
336
|
+
legacyCoreCompat.finishSchemaBootstrap()
|
|
308
337
|
// On newer Hosts the Settings -> Models surface is backed by the
|
|
309
338
|
// configurable-provider directory, not by the live adapter registry alone.
|
|
310
339
|
// Publish the main DeepSeek + 自动识图 route as a derived alias of official
|
|
@@ -321,6 +350,7 @@ export function apply(ctx, config = {}) {
|
|
|
321
350
|
}
|
|
322
351
|
return result
|
|
323
352
|
} catch (error) {
|
|
353
|
+
legacyCoreCompat.finishSchemaBootstrap()
|
|
324
354
|
logging.logger.error(
|
|
325
355
|
'vision-router: plugin apply failed: %s',
|
|
326
356
|
error && error.stack ? error.stack : error && error.message ? error.message : String(error),
|
|
@@ -1,7 +1,84 @@
|
|
|
1
1
|
const wrappedContexts = new WeakMap()
|
|
2
|
+
const wrappedLlmServices = new WeakMap()
|
|
3
|
+
const preparedAdapterCompat = new WeakMap()
|
|
2
4
|
|
|
3
5
|
export const DEFAULT_ADAPTER_RECONCILE_MAX_PASSES = 32
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* DSH 0.1.1 dispatches every model call through adapter.prepareCall(). Older
|
|
9
|
+
* Vision Router adapters are intentionally duck-typed objects and therefore do
|
|
10
|
+
* not inherit LlmAdapter's default implementation. Normalize only adapters
|
|
11
|
+
* registered through Vision Router's wrapped context so host/foreign adapters
|
|
12
|
+
* keep their native identity and behavior.
|
|
13
|
+
*/
|
|
14
|
+
export function ensureAdapterPrepareCall(adapter) {
|
|
15
|
+
if (!adapter || (typeof adapter !== 'object' && typeof adapter !== 'function')) return adapter
|
|
16
|
+
if (typeof adapter.prepareCall === 'function') return adapter
|
|
17
|
+
|
|
18
|
+
const cached = preparedAdapterCompat.get(adapter)
|
|
19
|
+
if (cached) return cached
|
|
20
|
+
|
|
21
|
+
const prepareCall = async (provider, model, signal) => {
|
|
22
|
+
const fallbackModel = { provider, id: model, name: model }
|
|
23
|
+
const resolved =
|
|
24
|
+
typeof adapter.resolveModel === 'function'
|
|
25
|
+
? await adapter.resolveModel.call(adapter, provider, model, signal)
|
|
26
|
+
: fallbackModel
|
|
27
|
+
return {
|
|
28
|
+
model: resolved && typeof resolved === 'object' ? resolved : fallbackModel,
|
|
29
|
+
stream(options) {
|
|
30
|
+
return adapter.stream.call(adapter, options)
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Preserve the exact adapter object whenever possible. Some runtime paths
|
|
36
|
+
// retain registration.adapter and compare/reuse it later, so mutation is less
|
|
37
|
+
// surprising than substituting a wrapper. Frozen/sealed adapters fall back to
|
|
38
|
+
// a proxy that exposes only the missing contract method.
|
|
39
|
+
try {
|
|
40
|
+
Object.defineProperty(adapter, 'prepareCall', {
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: prepareCall,
|
|
44
|
+
})
|
|
45
|
+
preparedAdapterCompat.set(adapter, adapter)
|
|
46
|
+
return adapter
|
|
47
|
+
} catch {
|
|
48
|
+
const wrapped = new Proxy(adapter, {
|
|
49
|
+
get(target, property) {
|
|
50
|
+
if (property === 'prepareCall') return prepareCall
|
|
51
|
+
const value = Reflect.get(target, property, target)
|
|
52
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
preparedAdapterCompat.set(adapter, wrapped)
|
|
56
|
+
return wrapped
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function wrapLlmService(llm) {
|
|
61
|
+
if (!llm || (typeof llm !== 'object' && typeof llm !== 'function')) return llm
|
|
62
|
+
const cached = wrappedLlmServices.get(llm)
|
|
63
|
+
if (cached) return cached
|
|
64
|
+
|
|
65
|
+
const wrapped = new Proxy(llm, {
|
|
66
|
+
get(target, property) {
|
|
67
|
+
if (property === 'registerAdapter') {
|
|
68
|
+
const registerAdapter = Reflect.get(target, property, target)
|
|
69
|
+
if (typeof registerAdapter !== 'function') return registerAdapter
|
|
70
|
+
return (providers, adapter, ...rest) =>
|
|
71
|
+
registerAdapter.call(target, providers, ensureAdapterPrepareCall(adapter), ...rest)
|
|
72
|
+
}
|
|
73
|
+
const value = Reflect.get(target, property, target)
|
|
74
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
wrappedLlmServices.set(llm, wrapped)
|
|
79
|
+
return wrapped
|
|
80
|
+
}
|
|
81
|
+
|
|
5
82
|
/**
|
|
6
83
|
* Turn a synchronous reconciliation pass into a bounded fixed-point runner.
|
|
7
84
|
*
|
|
@@ -76,6 +153,10 @@ export function createCoalescingRunner(run, options = {}) {
|
|
|
76
153
|
* registerAdapter() returns. Only Vision Router listeners registered through
|
|
77
154
|
* this wrapped context are coalesced; host and other plugin listeners keep
|
|
78
155
|
* their native event semantics.
|
|
156
|
+
*
|
|
157
|
+
* The same boundary also normalizes Vision Router-owned adapter registrations
|
|
158
|
+
* to the DSH 0.1.1 prepareCall contract. This stays scoped to the wrapped
|
|
159
|
+
* context instead of monkey-patching the Host LLM service process-wide.
|
|
79
160
|
*/
|
|
80
161
|
export function contextWithCoalescedAdapterUpdates(ctx) {
|
|
81
162
|
if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return ctx
|
|
@@ -84,6 +165,9 @@ export function contextWithCoalescedAdapterUpdates(ctx) {
|
|
|
84
165
|
|
|
85
166
|
const wrapped = new Proxy(ctx, {
|
|
86
167
|
get(target, property) {
|
|
168
|
+
if (property === 'llm') {
|
|
169
|
+
return wrapLlmService(Reflect.get(target, property, target))
|
|
170
|
+
}
|
|
87
171
|
if (property === 'on') {
|
|
88
172
|
const on = Reflect.get(target, property, target)
|
|
89
173
|
if (typeof on !== 'function') return on
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMAGE_OWNERSHIP,
|
|
3
|
+
currentSessionVisionPolicy,
|
|
4
|
+
} from './native-image-coexistence.js'
|
|
5
|
+
import { knownSessionVisionMemory } from './session-vision-state.js'
|
|
6
|
+
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return value !== null && typeof value === 'object'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function projectedConfig(value, state) {
|
|
12
|
+
if (!isObject(value) || Array.isArray(value)) return value
|
|
13
|
+
const policy = currentSessionVisionPolicy()
|
|
14
|
+
let changed = false
|
|
15
|
+
const overrides = {}
|
|
16
|
+
|
|
17
|
+
// index.js still constructs the vision-tool definitions behind a boot-time
|
|
18
|
+
// `if (toolEnabled())`. Keep that legacy construction gate open only while
|
|
19
|
+
// apply() is wiring the schema. The runtime boundary observes the unprojected
|
|
20
|
+
// Settings service and remains authoritative for live execution permission.
|
|
21
|
+
if (state.schemaBootstrapping && value.tool === false) {
|
|
22
|
+
overrides.tool = true
|
|
23
|
+
changed = true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Session ownership is stronger evidence than index.js's historical global
|
|
27
|
+
// wrapperRegistered flag. A native, Vision-Router-owned, or metadata-unknown
|
|
28
|
+
// route must not have raw pixels destructively rewritten by the legacy core.
|
|
29
|
+
if (policy?.preserveRawImages === true && value.rewriteImages !== false) {
|
|
30
|
+
overrides.rewriteImages = false
|
|
31
|
+
changed = true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Native multimodal routes already see the pixels. Avoid the hidden local
|
|
35
|
+
// caption pass and the generic tool auto-mount reminder, while leaving the
|
|
36
|
+
// explicit structured 1+x flow untouched.
|
|
37
|
+
if (policy?.ownership === IMAGE_OWNERSHIP.NATIVE) {
|
|
38
|
+
if (value.instantDescribe !== false) {
|
|
39
|
+
overrides.instantDescribe = false
|
|
40
|
+
changed = true
|
|
41
|
+
}
|
|
42
|
+
if (value.autoActivateOnImage !== false) {
|
|
43
|
+
overrides.autoActivateOnImage = false
|
|
44
|
+
changed = true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return changed ? { ...value, ...overrides } : value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function configView(config, state) {
|
|
52
|
+
if (!isObject(config)) return config
|
|
53
|
+
// Do not use the caller's config as the Proxy target. DSH or a test Host may
|
|
54
|
+
// freeze parsed config objects; projecting tool/rewrite values through a
|
|
55
|
+
// frozen target would violate Proxy invariants for non-configurable,
|
|
56
|
+
// non-writable properties. An empty extensible facade keeps projection
|
|
57
|
+
// virtual while writes/deletes still forward to the original object.
|
|
58
|
+
return new Proxy({}, {
|
|
59
|
+
get(_target, property) {
|
|
60
|
+
const projected = projectedConfig(config, state)
|
|
61
|
+
const value = Reflect.get(projected, property, projected)
|
|
62
|
+
return typeof value === 'function' ? value.bind(projected) : value
|
|
63
|
+
},
|
|
64
|
+
has(_target, property) {
|
|
65
|
+
return property in projectedConfig(config, state)
|
|
66
|
+
},
|
|
67
|
+
ownKeys() {
|
|
68
|
+
return Reflect.ownKeys(projectedConfig(config, state))
|
|
69
|
+
},
|
|
70
|
+
getOwnPropertyDescriptor(_target, property) {
|
|
71
|
+
const projected = projectedConfig(config, state)
|
|
72
|
+
const descriptor = Object.getOwnPropertyDescriptor(projected, property)
|
|
73
|
+
return descriptor === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: { ...descriptor, configurable: true }
|
|
76
|
+
},
|
|
77
|
+
getPrototypeOf() {
|
|
78
|
+
return Reflect.getPrototypeOf(config)
|
|
79
|
+
},
|
|
80
|
+
set(_target, property, value) {
|
|
81
|
+
return Reflect.set(config, property, value, config)
|
|
82
|
+
},
|
|
83
|
+
deleteProperty(_target, property) {
|
|
84
|
+
return Reflect.deleteProperty(config, property)
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scopeView(scope, state) {
|
|
90
|
+
if (!isObject(scope)) return scope
|
|
91
|
+
return new Proxy(scope, {
|
|
92
|
+
get(target, property) {
|
|
93
|
+
if (property === 'get') {
|
|
94
|
+
const get = Reflect.get(target, property, target)
|
|
95
|
+
if (typeof get !== 'function') return get
|
|
96
|
+
return (...args) => projectedConfig(get.apply(target, args), state)
|
|
97
|
+
}
|
|
98
|
+
if (property === 'watch') {
|
|
99
|
+
const watch = Reflect.get(target, property, target)
|
|
100
|
+
if (typeof watch !== 'function') return watch
|
|
101
|
+
return (callback, ...rest) =>
|
|
102
|
+
watch.call(
|
|
103
|
+
target,
|
|
104
|
+
(...args) => {
|
|
105
|
+
if (typeof callback !== 'function') return undefined
|
|
106
|
+
if (args.length === 0) return callback()
|
|
107
|
+
const [value, ...tail] = args
|
|
108
|
+
return callback(projectedConfig(value, state), ...tail)
|
|
109
|
+
},
|
|
110
|
+
...rest,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const value = Reflect.get(target, property, target)
|
|
114
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function settingsView(settings, state) {
|
|
120
|
+
if (!isObject(settings)) return settings
|
|
121
|
+
return new Proxy(settings, {
|
|
122
|
+
get(target, property) {
|
|
123
|
+
if (property === 'get') {
|
|
124
|
+
const get = Reflect.get(target, property, target)
|
|
125
|
+
if (typeof get !== 'function') return get
|
|
126
|
+
return (namespace, ...args) => {
|
|
127
|
+
const value = get.call(target, namespace, ...args)
|
|
128
|
+
return namespace === 'vision-router' ? projectedConfig(value, state) : value
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (property === 'register') {
|
|
132
|
+
const register = Reflect.get(target, property, target)
|
|
133
|
+
if (typeof register !== 'function') return register
|
|
134
|
+
return (namespace, ...args) => {
|
|
135
|
+
const scope = register.call(target, namespace, ...args)
|
|
136
|
+
return namespace === 'vision-router' ? scopeView(scope, state) : scope
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const value = Reflect.get(target, property, target)
|
|
140
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function childContextView(child, state) {
|
|
146
|
+
if (!isObject(child)) return child
|
|
147
|
+
const settings = settingsView(child.settings, state)
|
|
148
|
+
return new Proxy(child, {
|
|
149
|
+
get(target, property) {
|
|
150
|
+
if (property === 'settings') return settings
|
|
151
|
+
const value = Reflect.get(target, property, target)
|
|
152
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rewriteTextOnlyDecision(payload, decision, rewriteHistoryImages) {
|
|
158
|
+
const policy = currentSessionVisionPolicy()
|
|
159
|
+
if (
|
|
160
|
+
decision?.kind === 'reject' ||
|
|
161
|
+
policy?.rewriteCurrentImages !== true ||
|
|
162
|
+
typeof rewriteHistoryImages !== 'function'
|
|
163
|
+
) {
|
|
164
|
+
return decision
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const source = Array.isArray(decision?.messages)
|
|
168
|
+
? decision.messages
|
|
169
|
+
: Array.isArray(payload?.messages)
|
|
170
|
+
? payload.messages
|
|
171
|
+
: undefined
|
|
172
|
+
if (!source) return decision
|
|
173
|
+
|
|
174
|
+
// Core registers the exact SessionMemoryView while processing this same
|
|
175
|
+
// pre-step. Reuse it here so a text-only fallback preserves cached visual
|
|
176
|
+
// descriptions instead of degrading them back to a generic attachment marker.
|
|
177
|
+
const memory = knownSessionVisionMemory(payload?.agent?.session)
|
|
178
|
+
const rewritten = rewriteHistoryImages(source, memory)
|
|
179
|
+
const messages = rewritten?.messages
|
|
180
|
+
if (!Array.isArray(messages) || messages === source) return decision
|
|
181
|
+
if (isObject(decision)) return { ...decision, messages }
|
|
182
|
+
return { kind: 'continue', messages }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Adapt the session-scoped ownership policy to the legacy monolithic core.
|
|
187
|
+
*
|
|
188
|
+
* The policy layer remains read-only. This bridge only projects legacy config
|
|
189
|
+
* reads while one pre-step is active and post-processes an explicitly text-only
|
|
190
|
+
* decision through index.js's exported rewriteHistoryImages implementation and
|
|
191
|
+
* the exact SessionMemoryView registered by core for that pre-step. No marker,
|
|
192
|
+
* rewrite algorithm, or cross-session image state is duplicated here.
|
|
193
|
+
*/
|
|
194
|
+
export function installLegacyCoreVisionPolicyBridge(
|
|
195
|
+
ctx,
|
|
196
|
+
config = {},
|
|
197
|
+
{ rewriteHistoryImages } = {},
|
|
198
|
+
) {
|
|
199
|
+
if (!isObject(ctx)) {
|
|
200
|
+
return {
|
|
201
|
+
ctx,
|
|
202
|
+
config,
|
|
203
|
+
finishSchemaBootstrap() {},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const state = { schemaBootstrapping: true }
|
|
208
|
+
const projectedBootConfig = configView(config, state)
|
|
209
|
+
const settingsCache = new WeakMap()
|
|
210
|
+
|
|
211
|
+
const wrapSettings = (settings) => {
|
|
212
|
+
if (!isObject(settings)) return settings
|
|
213
|
+
const cached = settingsCache.get(settings)
|
|
214
|
+
if (cached) return cached
|
|
215
|
+
const wrapped = settingsView(settings, state)
|
|
216
|
+
settingsCache.set(settings, wrapped)
|
|
217
|
+
return wrapped
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const wrappedCtx = new Proxy(ctx, {
|
|
221
|
+
get(target, property) {
|
|
222
|
+
if (property === 'get') {
|
|
223
|
+
const get = Reflect.get(target, property, target)
|
|
224
|
+
if (typeof get !== 'function') return get
|
|
225
|
+
return (name, ...args) => {
|
|
226
|
+
const value = get.call(target, name, ...args)
|
|
227
|
+
return name === 'settings' ? wrapSettings(value) : value
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (property === 'inject') {
|
|
231
|
+
const inject = Reflect.get(target, property, target)
|
|
232
|
+
if (typeof inject !== 'function') return inject
|
|
233
|
+
return (dependencies, callback, ...rest) => {
|
|
234
|
+
if (
|
|
235
|
+
!Array.isArray(dependencies) ||
|
|
236
|
+
!dependencies.includes('settings') ||
|
|
237
|
+
typeof callback !== 'function'
|
|
238
|
+
) {
|
|
239
|
+
return inject.call(target, dependencies, callback, ...rest)
|
|
240
|
+
}
|
|
241
|
+
return inject.call(
|
|
242
|
+
target,
|
|
243
|
+
dependencies,
|
|
244
|
+
(child) => callback(childContextView(child, state)),
|
|
245
|
+
...rest,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (property === 'on') {
|
|
250
|
+
const on = Reflect.get(target, property, target)
|
|
251
|
+
if (typeof on !== 'function') return on
|
|
252
|
+
return (event, handler, ...rest) => {
|
|
253
|
+
if (event !== 'agent/pre-step' || typeof handler !== 'function') {
|
|
254
|
+
return on.call(target, event, handler, ...rest)
|
|
255
|
+
}
|
|
256
|
+
return on.call(target, event, async function legacyCoreVisionPolicyPreStep(payload, next) {
|
|
257
|
+
const decision = await handler.call(this, payload, next)
|
|
258
|
+
return rewriteTextOnlyDecision(payload, decision, rewriteHistoryImages)
|
|
259
|
+
}, ...rest)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const value = Reflect.get(target, property, target)
|
|
263
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ctx: wrappedCtx,
|
|
269
|
+
config: projectedBootConfig,
|
|
270
|
+
finishSchemaBootstrap() {
|
|
271
|
+
state.schemaBootstrapping = false
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -100,7 +100,11 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
|
|
|
100
100
|
|
|
101
101
|
const syncScreenshot = () => {
|
|
102
102
|
if (!screenshotCandidate || !rawTools || typeof rawTools.register !== 'function') return
|
|
103
|
-
|
|
103
|
+
const current = actualConfig()
|
|
104
|
+
// desktopScreenshot owns schema exposure. The global tool toggle is a live
|
|
105
|
+
// execution permission enforced by the runtime boundary (and the captured
|
|
106
|
+
// execute guard below), so flipping it must not churn the tool schema.
|
|
107
|
+
if (current.desktopScreenshot !== true) {
|
|
104
108
|
unmountScreenshot()
|
|
105
109
|
return
|
|
106
110
|
}
|
|
@@ -125,14 +129,28 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
|
|
|
125
129
|
}
|
|
126
130
|
return (def) => {
|
|
127
131
|
if (def && def.name === 'vision_screenshot') {
|
|
128
|
-
|
|
132
|
+
const candidate =
|
|
133
|
+
typeof def.execute === 'function'
|
|
134
|
+
? {
|
|
135
|
+
...def,
|
|
136
|
+
async execute(args, exec) {
|
|
137
|
+
if (actualConfig().tool === false) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'vision_screenshot: vision tools are disabled in the Vision Router settings',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return def.execute.call(this, args, exec)
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: def
|
|
146
|
+
screenshotCandidate = candidate
|
|
129
147
|
syncScreenshot()
|
|
130
148
|
let active = true
|
|
131
149
|
return () => {
|
|
132
150
|
if (!active) return
|
|
133
151
|
active = false
|
|
134
|
-
if (screenshotCandidate ===
|
|
135
|
-
if (screenshotMountedDef ===
|
|
152
|
+
if (screenshotCandidate === candidate) screenshotCandidate = undefined
|
|
153
|
+
if (screenshotMountedDef === candidate) unmountScreenshot()
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
return target.register(def)
|
|
@@ -393,7 +411,13 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
|
|
|
393
411
|
res.end(JSON.stringify({ ok: false, error: 'cross-origin screenshot permission request rejected' }))
|
|
394
412
|
return
|
|
395
413
|
}
|
|
396
|
-
|
|
414
|
+
const current = actualConfig()
|
|
415
|
+
if (current.tool === false) {
|
|
416
|
+
res.writeHead(409, { 'content-type': 'application/json' })
|
|
417
|
+
res.end(JSON.stringify({ ok: false, error: 'vision tools are disabled' }))
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
if (current.desktopScreenshot !== true) {
|
|
397
421
|
res.writeHead(409, { 'content-type': 'application/json' })
|
|
398
422
|
res.end(JSON.stringify({ ok: false, error: 'desktop screenshot is disabled' }))
|
|
399
423
|
return
|
|
@@ -493,4 +517,4 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
|
|
|
493
517
|
})
|
|
494
518
|
|
|
495
519
|
return { ctx: stabilizedCtx, bootConfig }
|
|
496
|
-
}
|
|
520
|
+
}
|