@deepblend/dsh-blender-tool 0.1.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/index.js +277 -0
- package/lib/render-tools.js +579 -0
- package/lib/shared.js +458 -0
- package/lib/tools.js +988 -0
- package/lib/visual-tools.js +444 -0
- package/package.json +24 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepblend/dsh-blender-tool
|
|
3
|
+
*
|
|
4
|
+
* The **Agent-preset plane** consumer (SPEC §4.2, §5.1, §11).
|
|
5
|
+
*
|
|
6
|
+
* Registers the model-visible DeepBlend tools. This package publishes NO
|
|
7
|
+
* service — it only consumes `blenderStudio` from the Host composition — which
|
|
8
|
+
* is exactly what makes it legal to list as a row inside an agent preset: a
|
|
9
|
+
* preset row that provided a process-global service would collide on the second
|
|
10
|
+
* session (SPEC §4.4).
|
|
11
|
+
*
|
|
12
|
+
* TOOLS BY MILESTONE
|
|
13
|
+
* ------------------
|
|
14
|
+
* M0 blender_capabilities
|
|
15
|
+
* M1 blender_project_create, blender_project_get, blender_scene_get,
|
|
16
|
+
* blender_scene_patch, blender_preview_render, blender_scene_validate
|
|
17
|
+
* M2 blender_preview_views, blender_visual_review, blender_visual_autofix
|
|
18
|
+
* M3 blender_final_render, blender_export, blender_job_status,
|
|
19
|
+
* blender_job_cancel
|
|
20
|
+
* M5 blender_revision_restore, blender_asset_ingest
|
|
21
|
+
*
|
|
22
|
+
* As of M5 the SPEC §11 inventory is COMPLETE: every tool that section names is
|
|
23
|
+
* registered here. The rule that produced the earlier absences still holds and is why
|
|
24
|
+
* it took this long — a tool the model can see is a promise the runtime must keep, so
|
|
25
|
+
* `blender_revision_restore` waited for a suite that calls it and
|
|
26
|
+
* `blender_asset_ingest` waited for `asset.add` to exist in the ScenePatch vocabulary
|
|
27
|
+
* and for an approval plane to gate a network fetch. Everything registered here can be
|
|
28
|
+
* kept.
|
|
29
|
+
*
|
|
30
|
+
* `blender_capabilities` stays in this file because it is the M0 tool and its
|
|
31
|
+
* behaviour is pinned by the M0 acceptance suite.
|
|
32
|
+
*
|
|
33
|
+
* Owner: DeepBlend Studio — M3
|
|
34
|
+
* Plane: Agent preset
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
38
|
+
|
|
39
|
+
import { BlenderError, BlenderErrorCode } from '@deepblend/dsh-blender-contracts'
|
|
40
|
+
|
|
41
|
+
import { TOOL_OUTPUT, resolveStudio } from './shared.js'
|
|
42
|
+
import { apply as applySceneTools } from './tools.js'
|
|
43
|
+
import { applyRenderTools } from './render-tools.js'
|
|
44
|
+
import { applyVisualTools } from './visual-tools.js'
|
|
45
|
+
|
|
46
|
+
/** Plugin name, surfaced in loader diagnostics. */
|
|
47
|
+
export const name = 'deepblend-blender-tool'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `tools` is required to register at all.
|
|
51
|
+
*
|
|
52
|
+
* `blenderStudio` is deliberately NOT a hard injection, and this is a real
|
|
53
|
+
* trade-off worth stating. Hard-injecting it makes the row park in `waiting`
|
|
54
|
+
* whenever the DeepBlend Host Bundle is absent — an honest, diagnosable state,
|
|
55
|
+
* and the option that hides least. But it also makes the preset UNMOUNTABLE
|
|
56
|
+
* until the host bundle is composed: mount validation fails, and a session on
|
|
57
|
+
* this preset cannot start at all.
|
|
58
|
+
*
|
|
59
|
+
* The Host Bundle is a process-level composition change that must be installed
|
|
60
|
+
* by the operator, so it only takes effect on the next profile boot. Making the
|
|
61
|
+
* tools' absence a mount-time failure would mean a half-installed deployment
|
|
62
|
+
* cannot even open a session to diagnose itself.
|
|
63
|
+
*
|
|
64
|
+
* So the service is resolved per call from `ctx`. Every tool is always visible
|
|
65
|
+
* in the catalog and its description says what it does; if the host half is
|
|
66
|
+
* missing, calling it returns a stable `BLENDER_RUNTIME_UNAVAILABLE` error
|
|
67
|
+
* explaining exactly which bundle to compose. Nothing fails silently.
|
|
68
|
+
*/
|
|
69
|
+
export const inject = ['tools']
|
|
70
|
+
/** No configuration; present so a composition row may carry `config:` later. */
|
|
71
|
+
export const Config = undefined
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The per-job prose block, re-exported for the contract layer.
|
|
75
|
+
*
|
|
76
|
+
* This package's job is to REGISTER tools, not to publish an API, so this is the one deliberate
|
|
77
|
+
* exception and it is narrow: `describeJobLines` is where a job record's facts reach a model, and a
|
|
78
|
+
* rule reachable only through a real render is a rule nobody has checked (the same reasoning
|
|
79
|
+
* `host/lib/frame-ledger.js` records for `sampleFrame`). `contract/render-job.test.mjs` drives it.
|
|
80
|
+
*/
|
|
81
|
+
export { describeJobLines } from './render-tools.js'
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The per-review prose block, re-exported for the same reason and with the same narrow scope.
|
|
85
|
+
*
|
|
86
|
+
* `describeReviewNotes` is the other place where a fact a model acts on is turned into English, and
|
|
87
|
+
* it is the branchier of the two: seven independent "is this part of the review present" questions.
|
|
88
|
+
* Reaching any of those branches for real needs a render AND a vision-model call, so before this
|
|
89
|
+
* export existed the coverage reading had every finding-bearing branch dark (round 40).
|
|
90
|
+
*/
|
|
91
|
+
export { describeReviewNotes } from './visual-tools.js'
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The other two prose blocks of the visual plane: the repair loop's round log, and the two lines that
|
|
95
|
+
* describe one measured issue.
|
|
96
|
+
*
|
|
97
|
+
* `describeLoopNotes` travels with the review notes for the same reason. `describeIssueLines` is here
|
|
98
|
+
* because it is SHARED — the review's issue list and the loop's open-issue list both use it, and while
|
|
99
|
+
* it had two copies a change to one of them would have silently disagreed with the other (D126).
|
|
100
|
+
*/
|
|
101
|
+
export { describeLoopNotes, describeIssueLines } from './visual-tools.js'
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The four boundary helpers whose answer CHANGES when a dependency is missing.
|
|
105
|
+
*
|
|
106
|
+
* Same narrow exception as the prose blocks above, and the reason is the same: each of these has a
|
|
107
|
+
* branch that a fully-equipped machine never produces — an attachment store that is not composed, an
|
|
108
|
+
* approval service that is not composed, a value with no JSON representation crossing the harness
|
|
109
|
+
* boundary — and the only other way to reach one is a real deployment that lacks the service. Finding
|
|
110
|
+
* out what the model is told THEN is exactly what a contract test is for.
|
|
111
|
+
*/
|
|
112
|
+
export { canonicalData, losslessJson, persistImage, requestApproval } from './shared.js'
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The service name this plane binds to, re-exported so the cross-plane tie is CHECKABLE.
|
|
116
|
+
*
|
|
117
|
+
* The tool resolves `blenderStudio` through `ctx.get()`, and the host registers it. Two literals in two
|
|
118
|
+
* packages with nothing comparing them is how a rename turns into "the host bundle is missing" at
|
|
119
|
+
* runtime; `contract/export-usage.test.mjs` now compares them, which needs this name on the surface.
|
|
120
|
+
*/
|
|
121
|
+
export { STUDIO_SERVICE } from './shared.js'
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register the DeepBlend tools for the calling agent scope.
|
|
125
|
+
*
|
|
126
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
127
|
+
*/
|
|
128
|
+
export function apply(ctx) {
|
|
129
|
+
// Registration is fiber-scoped: Cordis disposes these registrations when the
|
|
130
|
+
// preset's subtree unmounts, so no manual teardown is needed (and adding one
|
|
131
|
+
// would risk double-disposal on reload).
|
|
132
|
+
ctx.tools.register(capabilitiesTool(ctx))
|
|
133
|
+
applySceneTools(ctx)
|
|
134
|
+
applyVisualTools(ctx)
|
|
135
|
+
applyRenderTools(ctx)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Render the canonical capability report as compact, model-readable text.
|
|
140
|
+
*
|
|
141
|
+
* The JSON document is appended verbatim because SPEC §11.1 requires Canonical
|
|
142
|
+
* JSON in the tool result: the model must be able to read exact values rather
|
|
143
|
+
* than a paraphrase, and downstream code must not have to parse prose.
|
|
144
|
+
*
|
|
145
|
+
* @param {Record<string, any>} value
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function renderCapabilityText(value) {
|
|
149
|
+
const lines = []
|
|
150
|
+
const engineEntries = Object.entries(value.engines ?? {})
|
|
151
|
+
const available = engineEntries.filter(([, probe]) => probe.available).map(([id]) => id)
|
|
152
|
+
|
|
153
|
+
if (!value.installed) {
|
|
154
|
+
lines.push('Blender: NOT INSTALLED')
|
|
155
|
+
lines.push(`Configured path: ${value.executable?.requested ?? '<unset>'}`)
|
|
156
|
+
lines.push('No Blender executable could be resolved. DeepBlend cannot build or render until one is available.')
|
|
157
|
+
// The same sentence the settings card shows, composed once in the provider: a human and a model
|
|
158
|
+
// looking at the same broken install must not be told different things to do.
|
|
159
|
+
if (typeof value.executable?.advice === 'string' && value.executable.advice.length > 0) {
|
|
160
|
+
lines.push(`Fix: ${value.executable.advice}`)
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
lines.push(`Blender: ${value.version ?? 'unknown version'}`)
|
|
164
|
+
lines.push(`Python: ${value.pythonVersion ?? 'unknown'}`)
|
|
165
|
+
lines.push(`Binary: ${value.binaryPath ?? value.executable?.resolved ?? 'unknown'}`)
|
|
166
|
+
lines.push(`Engines: ${available.length > 0 ? available.join(', ') : 'none assignable'}`)
|
|
167
|
+
lines.push(
|
|
168
|
+
`Engine enum (diagnostic only): ${
|
|
169
|
+
Array.isArray(value.engineEnumItems) && value.engineEnumItems.length > 0
|
|
170
|
+
? value.engineEnumItems.join(', ')
|
|
171
|
+
: '<empty>'
|
|
172
|
+
}`,
|
|
173
|
+
)
|
|
174
|
+
const gpuDevices = value.gpu?.devices ?? []
|
|
175
|
+
const backend = value.gpu?.preferredBackend
|
|
176
|
+
lines.push(
|
|
177
|
+
`GPU: ${
|
|
178
|
+
value.gpu?.available
|
|
179
|
+
? `${gpuDevices.join(', ')}${backend ? ` via ${backend}` : ''}`
|
|
180
|
+
: 'none detected (CPU rendering)'
|
|
181
|
+
}`,
|
|
182
|
+
)
|
|
183
|
+
lines.push(
|
|
184
|
+
`Import: ${(value.formats?.import ?? []).join(', ') || 'none'}`,
|
|
185
|
+
)
|
|
186
|
+
lines.push(
|
|
187
|
+
`Export: ${(value.formats?.export ?? []).join(', ') || 'none'}`,
|
|
188
|
+
)
|
|
189
|
+
const smoke = value.renderSmokeTest
|
|
190
|
+
if (smoke) {
|
|
191
|
+
lines.push(
|
|
192
|
+
`Headless render probe: ${smoke.ok ? `ok via ${smoke.engine} (${smoke.bytes} bytes)` : `FAILED${smoke.error ? ` — ${smoke.error}` : ''}`}`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const warnings = Array.isArray(value.warnings) ? value.warnings : []
|
|
198
|
+
if (warnings.length > 0) {
|
|
199
|
+
lines.push('')
|
|
200
|
+
lines.push(`Warnings (${warnings.length}):`)
|
|
201
|
+
for (const entry of warnings) lines.push(` - [${entry.code}] ${entry.message}`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lines.push('')
|
|
205
|
+
lines.push('Canonical JSON:')
|
|
206
|
+
lines.push(JSON.stringify(value, null, 2))
|
|
207
|
+
return lines.join('\n')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The M0 capability tool, unchanged in behaviour. */
|
|
211
|
+
function capabilitiesTool(ctx) {
|
|
212
|
+
return defineTool({
|
|
213
|
+
name: 'blender_capabilities',
|
|
214
|
+
description:
|
|
215
|
+
'Report what the local Blender runtime can actually do: version, Python version, which render ' +
|
|
216
|
+
'engines are genuinely available, GPU compute devices, supported import/export formats, and whether ' +
|
|
217
|
+
'a headless render probe succeeded. Call this before planning any modelling, material or render work, ' +
|
|
218
|
+
'and again after the operator changes Blender settings. Engine availability is reported behaviorally — ' +
|
|
219
|
+
'a listed engine has been verified assignable, and absence from the diagnostic engine enum does not ' +
|
|
220
|
+
'mean an engine is unavailable.',
|
|
221
|
+
parameters: {
|
|
222
|
+
refresh: {
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
description:
|
|
225
|
+
'Re-probe Blender instead of serving the short-lived cached report. Use after changing the ' +
|
|
226
|
+
'Blender path or installing a different Blender build.',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
output: TOOL_OUTPUT,
|
|
230
|
+
/**
|
|
231
|
+
* @param {{ refresh?: boolean }} args
|
|
232
|
+
* @param {import('@deepseek-ai/dsh-tools').ToolRunContext} exec
|
|
233
|
+
*/
|
|
234
|
+
async execute(args, exec) {
|
|
235
|
+
// Resolved per call rather than injected — see the `inject` note above.
|
|
236
|
+
const resolved = resolveStudio(ctx)
|
|
237
|
+
if (resolved.unavailable !== undefined) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
text: `Blender capability probe unavailable.\n${resolved.unavailable.text}`,
|
|
241
|
+
data: resolved.unavailable.data,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const data = await resolved.studio.describeCapabilities({
|
|
247
|
+
refresh: args?.refresh === true,
|
|
248
|
+
signal: exec.signal,
|
|
249
|
+
})
|
|
250
|
+
return { ok: true, text: renderCapabilityText(data), data }
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
// A probe failure must still reach the model as readable text plus a
|
|
253
|
+
// stable code, not as an opaque tool crash (SPEC §11.1).
|
|
254
|
+
const error = cause instanceof BlenderError
|
|
255
|
+
? cause
|
|
256
|
+
: new BlenderError(
|
|
257
|
+
BlenderErrorCode.CAPABILITY_PROBE_FAILED,
|
|
258
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
259
|
+
{ cause },
|
|
260
|
+
)
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
text:
|
|
264
|
+
`Blender capability probe failed.\n` +
|
|
265
|
+
`errorCode: ${error.code}\n` +
|
|
266
|
+
`message: ${error.message}\n` +
|
|
267
|
+
(error.detail !== undefined ? `detail: ${JSON.stringify(error.detail)}\n` : ''),
|
|
268
|
+
data: { ok: false, errorCode: error.code, message: error.message, detail: error.detail ?? null },
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
presentCall: args =>
|
|
273
|
+
args?.refresh === true
|
|
274
|
+
? { card: 'generic', title: 'Re-probe Blender capabilities', kind: 'other' }
|
|
275
|
+
: { card: 'generic', title: 'Check Blender capabilities', kind: 'read' },
|
|
276
|
+
})
|
|
277
|
+
}
|