@deepblend/dsh-blender-tool 0.6.3 → 0.7.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/lib/attachment-image.js +47 -0
- package/lib/committed-preview.js +57 -0
- package/lib/image-delivery.js +23 -0
- package/lib/inspection.js +4 -12
- package/lib/shared.js +3 -0
- package/lib/tools.js +26 -11
- package/lib/visual-tools.js +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { persistImage } from './shared.js'
|
|
3
|
+
|
|
4
|
+
const MAX_IMAGE_BYTES = 32 * 1024 * 1024
|
|
5
|
+
|
|
6
|
+
/** Persist one already verified PNG; validate any normalized durable reference. */
|
|
7
|
+
export async function persistVerifiedPng(attachments, bytes, artifact, name, signal) {
|
|
8
|
+
const store = typeof attachments?.saveImage === 'function' ? {
|
|
9
|
+
async saveImage(input) {
|
|
10
|
+
const ref = await attachments.saveImage(input)
|
|
11
|
+
const exact = ref?.mediaType === 'image/png' && ref.bytes === artifact.bytes &&
|
|
12
|
+
ref.width === artifact.width && ref.height === artifact.height
|
|
13
|
+
const dimensionsMatch = ref?.width === artifact.width && ref?.height === artifact.height ||
|
|
14
|
+
ref?.originalDimensions?.width === artifact.width && ref?.originalDimensions?.height === artifact.height &&
|
|
15
|
+
ref.width <= artifact.width && ref.height <= artifact.height
|
|
16
|
+
if (typeof ref?.attachmentId !== 'string' || !ref.attachmentId.trim() ||
|
|
17
|
+
!['image/png', 'image/jpeg', 'image/webp'].includes(ref.mediaType) ||
|
|
18
|
+
!Number.isSafeInteger(ref.bytes) || ref.bytes <= 0 || ref.bytes > MAX_IMAGE_BYTES ||
|
|
19
|
+
!Number.isSafeInteger(ref.width) || ref.width <= 0 || !Number.isSafeInteger(ref.height) || ref.height <= 0 ||
|
|
20
|
+
!dimensionsMatch) {
|
|
21
|
+
throw new Error('attachment reference does not match the verified PNG')
|
|
22
|
+
}
|
|
23
|
+
// DSH may normalize the verified PNG into a smaller encoded image. Its
|
|
24
|
+
// read API verifies that durable image's digest, format and dimensions.
|
|
25
|
+
if (!exact) {
|
|
26
|
+
if (typeof attachments.readImage !== 'function') throw new Error('normalized attachment cannot be read back')
|
|
27
|
+
const stored = await attachments.readImage(ref, signal)
|
|
28
|
+
if (!(stored?.data instanceof Uint8Array) || stored.data.byteLength !== ref.bytes ||
|
|
29
|
+
['attachmentId', 'mediaType', 'bytes', 'width', 'height'].some(key => stored.ref?.[key] !== ref[key]) ||
|
|
30
|
+
ref.attachmentId.startsWith('sha256:') &&
|
|
31
|
+
createHash('sha256').update(stored.data).digest('hex') !== ref.attachmentId.slice(7)) {
|
|
32
|
+
throw new Error('normalized attachment bytes do not match its durable reference')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return ref
|
|
36
|
+
},
|
|
37
|
+
} : attachments
|
|
38
|
+
const attached = await persistImage(store, bytes, name)
|
|
39
|
+
let { image, note } = attached
|
|
40
|
+
if (image && (image.mediaType !== 'image/png' || image.bytes !== artifact.bytes ||
|
|
41
|
+
image.width !== artifact.width || image.height !== artifact.height)) {
|
|
42
|
+
note = 'The attached image is the attachment service normalization of the verified source PNG. ' +
|
|
43
|
+
`Attached format: ${image.mediaType}; dimensions: ${image.width}x${image.height}. ` +
|
|
44
|
+
'The original PNG and its source digest remain at the project path in this result.'
|
|
45
|
+
}
|
|
46
|
+
return { image, note }
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { BlenderError, BlenderErrorCode } from '@deepblend/dsh-blender-contracts'
|
|
3
|
+
import { persistVerifiedPng } from './attachment-image.js'
|
|
4
|
+
import { imageForCallingModel } from './image-delivery.js'
|
|
5
|
+
|
|
6
|
+
const MAX_IMAGE_BYTES = 32 * 1024 * 1024
|
|
7
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
|
8
|
+
|
|
9
|
+
/** Attach the receipt's existing preview, with no compile, render or output-time IO. */
|
|
10
|
+
export async function attachCommittedPreview(ctx, exec, resolved, value, { projectId, summary }) {
|
|
11
|
+
const artifact = summary?.previews?.[0]
|
|
12
|
+
if (!artifact) return value
|
|
13
|
+
const cancelled = () => {
|
|
14
|
+
if (exec?.signal?.aborted) throw new BlenderError(BlenderErrorCode.ABORTED,
|
|
15
|
+
'Preview attachment cancelled; the saved revision may already exist.')
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
cancelled()
|
|
19
|
+
const revision = summary.revision
|
|
20
|
+
if (typeof projectId !== 'string' || !projectId || !/^r[0-9]{4,}$/.test(revision) ||
|
|
21
|
+
!/^[a-f0-9]{64}$/.test(summary.digest) || artifact.kind !== 'preview' ||
|
|
22
|
+
artifact.sourceRevision !== revision || artifact.sourceDigest !== summary.digest ||
|
|
23
|
+
artifact.mime !== 'image/png' || typeof artifact.path !== 'string' ||
|
|
24
|
+
!artifact.path.startsWith(`revisions/${revision}/previews/`) ||
|
|
25
|
+
artifact.path.split('/').some(part => !part || part === '.' || part === '..') ||
|
|
26
|
+
!/^[a-f0-9]{64}$/.test(artifact.sha256) ||
|
|
27
|
+
!Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || artifact.bytes > MAX_IMAGE_BYTES ||
|
|
28
|
+
!Number.isSafeInteger(artifact.width) || artifact.width <= 0 || artifact.width > 2048 ||
|
|
29
|
+
!Number.isSafeInteger(artifact.height) || artifact.height <= 0 || artifact.height > 2048 ||
|
|
30
|
+
typeof artifact.cameraId !== 'string' || !artifact.cameraId || !Number.isSafeInteger(artifact.frame) ||
|
|
31
|
+
typeof resolved.studio.readArtifact !== 'function') throw new Error('Invalid saved preview receipt')
|
|
32
|
+
const read = await resolved.studio.readArtifact({ projectId, path: artifact.path })
|
|
33
|
+
cancelled()
|
|
34
|
+
if (read?.path !== artifact.path || read.contentType !== 'image/png' || !Buffer.isBuffer(read.bytes) ||
|
|
35
|
+
read.size !== artifact.bytes || read.bytes.length !== artifact.bytes || read.bytes.length < 24 ||
|
|
36
|
+
!read.bytes.subarray(0, 8).equals(PNG_SIGNATURE) ||
|
|
37
|
+
read.bytes.readUInt32BE(16) !== artifact.width || read.bytes.readUInt32BE(20) !== artifact.height ||
|
|
38
|
+
createHash('sha256').update(read.bytes).digest('hex') !== artifact.sha256) {
|
|
39
|
+
throw new Error('Saved PNG differs from its receipt')
|
|
40
|
+
}
|
|
41
|
+
const attached = await persistVerifiedPng(resolved.attachments, read.bytes, artifact,
|
|
42
|
+
`preview-${revision}-${artifact.cameraId}-${artifact.frame}.png`, exec?.signal)
|
|
43
|
+
cancelled()
|
|
44
|
+
return await imageForCallingModel(ctx, exec, {
|
|
45
|
+
...value, image: attached.image,
|
|
46
|
+
imageSource: { ...artifact, projectId, revision, digest: summary.digest },
|
|
47
|
+
text: value.text + '\n\nThe image uses the preview already saved with this revision; no extra render was started.' +
|
|
48
|
+
(attached.note ? '\n' + attached.note : ''),
|
|
49
|
+
})
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
cancelled()
|
|
52
|
+
if (cause?.code === BlenderErrorCode.ABORTED) throw cause
|
|
53
|
+
return { ...value, image: null,
|
|
54
|
+
text: value.text + '\n\nThe revision remains saved, but its preview could not be verified and attached here. ' +
|
|
55
|
+
'The model has not seen this image. Use the receipt to inspect the saved artifact; do not recreate the project or repeat the render to repair an attachment.' }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BlenderError, BlenderErrorCode } from '@deepblend/dsh-blender-contracts'
|
|
2
|
+
|
|
3
|
+
/** Keep rendered artifacts available without sending images to a known text-only route. */
|
|
4
|
+
export async function imageForCallingModel(ctx, exec, value) {
|
|
5
|
+
if (!value.image) return value
|
|
6
|
+
const route = exec?.agent?.session?.requestHeader?.()?.config
|
|
7
|
+
const llm = ctx.get('llm')
|
|
8
|
+
if (!route?.provider || !route.model || typeof llm?.listModels !== 'function') return value
|
|
9
|
+
let catalog
|
|
10
|
+
try { catalog = await llm.listModels(route.provider) } catch {
|
|
11
|
+
if (exec?.signal?.aborted) throw new BlenderError(BlenderErrorCode.ABORTED, 'Image delivery cancelled.')
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
if (exec?.signal?.aborted) throw new BlenderError(BlenderErrorCode.ABORTED, 'Image delivery cancelled.')
|
|
15
|
+
const model = catalog?.find(entry => entry.id === route.model)
|
|
16
|
+
if (!Array.isArray(model?.inputModalities) || model.inputModalities.includes('image')) return value
|
|
17
|
+
return { ...value, image: null,
|
|
18
|
+
imageDelivery: { provider: route.provider, model: route.model, deliveredToModel: false,
|
|
19
|
+
reason: 'The active request model declares no image input.' },
|
|
20
|
+
text: value.text + '\n\nThe rendered image remains saved at the path in this result. ' +
|
|
21
|
+
`It was not attached to the ${route.provider}/${route.model} conversation because this model does not accept images. ` +
|
|
22
|
+
'The model has not seen these pixels; report measurements separately from appearance and artistic approval.' }
|
|
23
|
+
}
|
package/lib/inspection.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { BlenderError, BlenderErrorCode, warning } from '@deepblend/dsh-blender-contracts'
|
|
3
|
-
import { canonicalCall, definedFields,
|
|
3
|
+
import { canonicalCall, definedFields, renderSuccess } from './shared.js'
|
|
4
|
+
|
|
5
|
+
import { persistVerifiedPng } from './attachment-image.js'
|
|
4
6
|
|
|
5
7
|
const REQUIRED_HOST_API = 6
|
|
6
8
|
const MAX_IMAGE_BYTES = 32 * 1024 * 1024
|
|
@@ -66,17 +68,7 @@ export async function renderInspection(resolved, request) {
|
|
|
66
68
|
let image = null
|
|
67
69
|
let note = null
|
|
68
70
|
try {
|
|
69
|
-
const
|
|
70
|
-
async saveImage(input) {
|
|
71
|
-
const ref = await attachments.saveImage(input)
|
|
72
|
-
if (typeof ref?.attachmentId !== 'string' || !ref.attachmentId.trim() || ref.mediaType !== 'image/png' ||
|
|
73
|
-
ref.bytes !== artifact.bytes || ref.width !== artifact.width || ref.height !== artifact.height) {
|
|
74
|
-
throw new Error('attachment reference does not match the verified PNG')
|
|
75
|
-
}
|
|
76
|
-
return ref
|
|
77
|
-
},
|
|
78
|
-
} : attachments
|
|
79
|
-
const attached = await persistImage(store, read.bytes, `${mode}-${revision}-${cameraId}-${frame}.png`)
|
|
71
|
+
const attached = await persistVerifiedPng(attachments, read.bytes, artifact, `${mode}-${revision}-${cameraId}-${frame}.png`, signal)
|
|
80
72
|
aborted(signal)
|
|
81
73
|
image = attached.image
|
|
82
74
|
note = attached.note
|
package/lib/shared.js
CHANGED
|
@@ -99,6 +99,9 @@ export async function persistImage(attachments, bytes, name) {
|
|
|
99
99
|
width: Number(ref.width),
|
|
100
100
|
height: Number(ref.height),
|
|
101
101
|
name: typeof ref.name === 'string' ? ref.name : name,
|
|
102
|
+
...(ref.originalDimensions ? { originalDimensions: {
|
|
103
|
+
width: Number(ref.originalDimensions.width), height: Number(ref.originalDimensions.height),
|
|
104
|
+
} } : {}),
|
|
102
105
|
},
|
|
103
106
|
note: null,
|
|
104
107
|
}
|
package/lib/tools.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
30
30
|
import { renderInspection } from './inspection.js'
|
|
31
|
+
import { imageForCallingModel } from './image-delivery.js'
|
|
32
|
+
import { attachCommittedPreview } from './committed-preview.js'
|
|
31
33
|
|
|
32
34
|
import {
|
|
33
35
|
SCENE_OPERATION_NAMES,
|
|
@@ -116,7 +118,7 @@ function recipeParameters(ctx) {
|
|
|
116
118
|
'Saving needs that exact baseRevision and numeric parameters. It updates the same project, preserves independent ' +
|
|
117
119
|
'materials, bevels, photography and history, and refuses manually conflicting controlled geometry. ' +
|
|
118
120
|
'A scene without declared assembly controls returns available:false; do not invent parameters or replace the project. ' +
|
|
119
|
-
'Saving compiles a checkpoint and preview by default; explicit false flags can request a data-only revision. ' +
|
|
121
|
+
'Saving compiles a checkpoint and preview by default and can attach that existing preview for inspection; explicit false flags can request a data-only revision. ' +
|
|
120
122
|
'Use a unique idempotencyKey per user save and reuse it only when retrying the same intent.',
|
|
121
123
|
parameters: {
|
|
122
124
|
action: { type: 'string', enum: ['read', 'save'], description: 'read (default) is read-only; save creates one atomic revision.' },
|
|
@@ -128,7 +130,7 @@ function recipeParameters(ctx) {
|
|
|
128
130
|
saveCheckpoint: { type: 'boolean', description: 'Save a Blender checkpoint (default true).' },
|
|
129
131
|
renderPreview: { type: 'boolean', description: 'Render an actual new preview (default true).' },
|
|
130
132
|
},
|
|
131
|
-
output:
|
|
133
|
+
output: TOOL_OUTPUT_WITH_IMAGE,
|
|
132
134
|
async execute(args, exec) {
|
|
133
135
|
const resolved = resolveStudio(ctx)
|
|
134
136
|
if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
|
|
@@ -143,7 +145,9 @@ function recipeParameters(ctx) {
|
|
|
143
145
|
saveCheckpoint: args.saveCheckpoint !== false, renderPreview: args.renderPreview !== false,
|
|
144
146
|
signal: exec?.signal })
|
|
145
147
|
const { data, canonicalWarnings } = await canonicalCall(result, warning)
|
|
146
|
-
|
|
148
|
+
const value = { ok: true, text: renderSuccess(action === 'read' ? 'Saved assembly controls.' : 'Assembly dimensions saved.', data, { warnings: canonicalWarnings }), data }
|
|
149
|
+
return action === 'save' && args.renderPreview !== false
|
|
150
|
+
? await attachCommittedPreview(ctx, exec, resolved, value, { projectId: args.projectId, summary: data }) : value
|
|
147
151
|
} catch (cause) { return { ok: false, ...renderFailure(cause, 'RECIPE_PARAMETER_FAILED') } }
|
|
148
152
|
},
|
|
149
153
|
presentCall: args => ({ card: 'generic', title: args?.action === 'save' ? 'Save assembly dimensions' : 'Read assembly dimensions',
|
|
@@ -157,7 +161,8 @@ function recipeList(ctx) {
|
|
|
157
161
|
description:
|
|
158
162
|
'List locally available, validated product recipes without launching Blender or creating a project. ' +
|
|
159
163
|
'Returns recipes with id, version, digest, title, author, declared license, source, preview URL and ' +
|
|
160
|
-
'
|
|
164
|
+
'parameter IDs, types, defaults and allowed ranges, plus errors for packages that could not be accepted. ' +
|
|
165
|
+
'Internal geometry binding maps stay in the Host and saved recipe source. Content hashes ' +
|
|
161
166
|
'check loaded bytes; they do not certify authorship or artistic quality. Use the returned id, version ' +
|
|
162
167
|
'and digest together in blender_project_create.recipe. Only pass parameter IDs and ranges declared ' +
|
|
163
168
|
'by that recipe. This tool never downloads or installs anything.',
|
|
@@ -168,7 +173,10 @@ function recipeList(ctx) {
|
|
|
168
173
|
if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
|
|
169
174
|
try {
|
|
170
175
|
const { data, canonicalWarnings } = await canonicalCall(resolved.studio.listRecipes(), warning)
|
|
171
|
-
|
|
176
|
+
const catalog = { ...data, recipes: data.recipes.map(recipe => ({ ...recipe,
|
|
177
|
+
parameters: recipe.parameters.map(({ bindings, ...parameter }) => parameter),
|
|
178
|
+
})) }
|
|
179
|
+
return { ok: true, text: renderSuccess('Available product recipes.', catalog, { warnings: canonicalWarnings }), data: catalog }
|
|
172
180
|
} catch (cause) {
|
|
173
181
|
return { ok: false, ...renderFailure(cause, 'RECIPE_LIST_FAILED') }
|
|
174
182
|
}
|
|
@@ -190,6 +198,9 @@ function projectCreate(ctx) {
|
|
|
190
198
|
'Give it the operator\'s brief as `goal`: it is stored with the project and never parsed, so write it ' +
|
|
191
199
|
'in full. For an existing product design, call blender_recipe_list and pass its id/version/digest ' +
|
|
192
200
|
'as recipe, with only its declared parameter overrides. recipe and sceneSpec are mutually exclusive. ' +
|
|
201
|
+
'When the brief asks for an editable Blender file and a preview, set saveCheckpoint:true and ' +
|
|
202
|
+
'renderPreview:true together to save both in this creation. The saved preview can be attached in this result; inspect it before rendering that view again. Inspect those returned receipts before ' +
|
|
203
|
+
'reading a full scene or starting another render. ' +
|
|
193
204
|
'Omit both to start from a minimal renderable scaffold and build up with ' +
|
|
194
205
|
'blender_scene_patch; pass one only when you already know the whole scene. ' +
|
|
195
206
|
'Set saveCheckpoint:false to skip the Blender compile and store the SceneSpec alone. The revision then ' +
|
|
@@ -242,7 +253,7 @@ function projectCreate(ctx) {
|
|
|
242
253
|
description: 'Optional stable key for this creation intent (1–128 characters, no control characters). Reuse it with the same inputs when retrying an uncertain result. A completed request opens the same project without another compile or render; a new project needs a new key. Recovery returns its current saved revision.',
|
|
243
254
|
},
|
|
244
255
|
},
|
|
245
|
-
output:
|
|
256
|
+
output: TOOL_OUTPUT_WITH_IMAGE,
|
|
246
257
|
async execute(args, exec) {
|
|
247
258
|
const resolved = resolveStudio(ctx)
|
|
248
259
|
if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
|
|
@@ -278,7 +289,7 @@ function projectCreate(ctx) {
|
|
|
278
289
|
notes.push('Compiler notes (these describe decisions made on your behalf):')
|
|
279
290
|
for (const entry of data.warnings) notes.push(` - ${entry.message}`)
|
|
280
291
|
}
|
|
281
|
-
|
|
292
|
+
const value = {
|
|
282
293
|
ok: true,
|
|
283
294
|
text: renderSuccess(`Created project "${data.projectId}" at revision ${data.revision.revision}.`, data, {
|
|
284
295
|
notes,
|
|
@@ -286,6 +297,8 @@ function projectCreate(ctx) {
|
|
|
286
297
|
}),
|
|
287
298
|
data,
|
|
288
299
|
}
|
|
300
|
+
return args.renderPreview === true
|
|
301
|
+
? await attachCommittedPreview(ctx, exec, resolved, value, { projectId: data.projectId, summary: data.revision }) : value
|
|
289
302
|
} catch (cause) {
|
|
290
303
|
const failure = renderFailure(cause, 'PROJECT_CREATE_FAILED')
|
|
291
304
|
return { ok: false, ...failure }
|
|
@@ -559,7 +572,7 @@ function scenePatch(ctx) {
|
|
|
559
572
|
description: 'Also render the preview profile as part of this commit, so the revision arrives with an image. Default false.',
|
|
560
573
|
},
|
|
561
574
|
},
|
|
562
|
-
output:
|
|
575
|
+
output: TOOL_OUTPUT_WITH_IMAGE,
|
|
563
576
|
async execute(args, exec) {
|
|
564
577
|
const resolved = resolveStudio(ctx)
|
|
565
578
|
if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
|
|
@@ -615,7 +628,7 @@ function scenePatch(ctx) {
|
|
|
615
628
|
}
|
|
616
629
|
const scene = data.scene ?? {}
|
|
617
630
|
if (scene.counts) notes.push(`Scene now: ${JSON.stringify(scene.counts)}`)
|
|
618
|
-
|
|
631
|
+
const value = {
|
|
619
632
|
ok: true,
|
|
620
633
|
text: renderSuccess(
|
|
621
634
|
`${data.idempotentReplay === true ? 'Reused' : 'Committed'} revision ${data.revision}.`,
|
|
@@ -624,6 +637,8 @@ function scenePatch(ctx) {
|
|
|
624
637
|
),
|
|
625
638
|
data,
|
|
626
639
|
}
|
|
640
|
+
return args.renderPreview === true
|
|
641
|
+
? await attachCommittedPreview(ctx, exec, resolved, value, { projectId: args.projectId, summary: data }) : value
|
|
627
642
|
} catch (cause) {
|
|
628
643
|
const failure = renderFailure(cause, 'SCENE_PATCH_FAILED')
|
|
629
644
|
return { ok: false, ...failure }
|
|
@@ -684,11 +699,11 @@ function previewRender(ctx) {
|
|
|
684
699
|
const resolved = resolveStudio(ctx)
|
|
685
700
|
if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
|
|
686
701
|
try {
|
|
687
|
-
if (args.mode !== undefined) return await renderInspection(resolved, {
|
|
702
|
+
if (args.mode !== undefined) return await imageForCallingModel(ctx, exec, await renderInspection(resolved, {
|
|
688
703
|
projectId: args.projectId, revision: args.revision, mode: args.mode,
|
|
689
704
|
cameraId: args.cameraId, frame: args.frame, width: args.width, height: args.height,
|
|
690
705
|
samples: args.samples, signal: exec.signal,
|
|
691
|
-
})
|
|
706
|
+
}))
|
|
692
707
|
const { data, canonicalWarnings } = await canonicalCall(resolved.studio.renderPreview({
|
|
693
708
|
...definedFields({
|
|
694
709
|
projectId: args.projectId,
|
package/lib/visual-tools.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
43
|
+
import { imageForCallingModel } from './image-delivery.js'
|
|
43
44
|
|
|
44
45
|
import { BlenderWarningCode, warning } from '@deepblend/dsh-blender-contracts'
|
|
45
46
|
|
|
@@ -229,12 +230,12 @@ function visualReview(ctx) {
|
|
|
229
230
|
)
|
|
230
231
|
if (persisted.note !== null) notes.push(persisted.note)
|
|
231
232
|
|
|
232
|
-
return {
|
|
233
|
+
return await imageForCallingModel(ctx, exec, {
|
|
233
234
|
ok: true,
|
|
234
235
|
text: renderSuccess(`Visual review of ${data.revision}: ${data.score}/100.`, data, { notes, warnings: [...(data.warnings ?? []), ...canonicalWarnings] }),
|
|
235
236
|
data,
|
|
236
237
|
image: persisted.image,
|
|
237
|
-
}
|
|
238
|
+
})
|
|
238
239
|
} catch (cause) {
|
|
239
240
|
const failure = renderFailure(cause, 'VISUAL_REVIEW_FAILED')
|
|
240
241
|
return { ok: false, ...failure, image: null }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-tool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-preset tool consumer: registers the model-visible DeepBlend Blender tools. Publishes no service.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@deepblend/dsh-blender-contracts": "0.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.7.0"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|