feat: add economy/maximum presets, tool-lab and subagent-cursor extensions
Some checks failed
CI / windows node 24 / wine blocking (push) Has been skipped
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / wine apt cache (push) Successful in 7s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 1m25s
Deploy documentation / deploy (push) Has been skipped
Landlock Run / Matrix (push) Successful in 5s
Release (vendor) / Pack npm tarballs (push) Failing after 2m47s
Release (dsh) / Pack npm tarballs (push) Failing after 1m56s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m57s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m19s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Failing after 1m24s

- new economy and maximum agent presets with three-role pipeline skill
- new packages/extensions/tool-lab (home-lab ComfyUI/Docling/Whishper tools)
- new packages/subagent/subagent-cursor provider
- openrouter balance UI with on-demand refresh
- session projection context-seed boundary fold
- regenerate docs catalogs; keep local searxng benchmark scripts
This commit is contained in:
2026-08-23 22:12:30 +07:00
parent e818e37f0a
commit 8719fa84f0
112 changed files with 5686 additions and 169 deletions

View File

@@ -0,0 +1,51 @@
{
"name": "@deepseek-ai/dsh-tool-lab",
"description": "Model-facing home-lab AI tools (lab_generate_image via ComfyUI, lab_ocr_pdf via Docling, lab_transcribe_audio via Whishper) over the lab's LAN services",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/extensions/tool-lab"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,129 @@
/**
* ComfyUI image generation tool. Talks to the lab's ComfyUI server (default
* http://192.168.31.240:8188) with a Juggernaut-XL workflow and returns the
* served image URL. @module @deepseek-ai/dsh-tool-lab
*/
import { Deadline, sleep } from './helpers.ts'
/** Root of the resolved lab config. */
export interface ResolvedLabConfig {
comfyBaseUrl: string
doclingBaseUrl: string
whishBaseUrl: string
timeoutMs: number
maxUploadBytes: number
maxOutputChars: number
}
/** Model-backed fields ComfyUI exposes on the workflow we build. */
interface ComfyApiResult {
prompt_id?: string
node_errors?: Record<string, unknown>
}
interface ComfyHistoryEntry {
outputs?: Record<string, { images?: { filename?: string; subfolder?: string; type?: string }[] }>
status?: { completed?: boolean; error?: unknown }
}
/** A single ComfyUI generation request. */
export interface GenerateImageArgs {
prompt: string
negative?: string
width?: number
height?: number
steps?: number
seed?: number
model?: string
upscale?: boolean
upscale_model?: string
filename_prefix?: string
}
/** Compose the fixed 28-step Juggernaut-XL workflow from user-facing arguments. */
export function buildWorkflow(args: GenerateImageArgs): Record<string, unknown> {
const seed = args.seed ?? Math.floor(Math.random() * 1_000_000)
const width = args.width ?? 512
const height = args.height ?? 512
const steps = args.steps ?? 28
const model = args.model ?? 'Juggernaut-XL_v9_RunDiffusionPhoto_v2.safetensors'
const negative = args.negative ?? 'blurry, low quality, distorted, watermark, text, extra limbs'
const upscale = args.upscale !== false
const upscaleModel = args.upscale_model ?? 'RealESRGAN_x4plus.safetensors'
const prefix = args.filename_prefix ?? 'dsh'
const workflow: Record<string, unknown> = {
3: {
class_type: 'KSampler',
inputs: {
seed, steps, cfg: 4.0, sampler_name: 'dpmpp_2m_sde', scheduler: 'karras', denoise: 1.0,
model: ['4', 0], positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0],
},
},
4: { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: model } },
5: { class_type: 'EmptyLatentImage', inputs: { width, height, batch_size: 1 } },
6: { class_type: 'CLIPTextEncode', inputs: { text: args.prompt, clip: ['4', 1] } },
7: { class_type: 'CLIPTextEncode', inputs: { text: negative, clip: ['4', 1] } },
8: { class_type: 'VAEDecode', inputs: { samples: ['3', 0], vae: ['4', 2] } },
9: { class_type: 'SaveImage', inputs: { filename_prefix: prefix, images: upscale ? ['10', 0] : ['8', 0] } },
}
if (upscale) {
workflow['10'] = { class_type: 'ImageUpscaleWithModel', inputs: { upscale_model: ['11', 0], image: ['8', 0] } }
workflow['11'] = { class_type: 'UpscaleModelLoader', inputs: { model_name: upscaleModel } }
}
return workflow
}
/**
* Poll ComfyUI `/history/{prompt_id}` until the image appears or the timeout
* budget elapses. Returns the served image URL.
*/
async function pollImage(
baseUrl: string,
promptId: string,
deadline: Deadline,
timeoutMs: number,
): Promise<string> {
const started = Date.now()
for (;;) {
deadline.check()
const res = await fetch(`${baseUrl}/history/${promptId}`)
if (!res.ok) throw new Error(`ComfyUI history ${res.status}: ${await res.text()}`)
const history = (await res.json()) as Record<string, ComfyHistoryEntry>
const entry = history[promptId]
if (entry?.outputs?.['9']?.images?.length) {
const img = entry.outputs['9'].images[0]
if (!img) throw new Error('ComfyUI history entry missing image 0')
return `${baseUrl}/view?filename=${encodeURIComponent(img.filename ?? '')}&subfolder=${encodeURIComponent(img.subfolder ?? '')}&type=${encodeURIComponent(img.type ?? 'output')}`
}
if (entry?.status?.error) throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status.error)}`)
if (Date.now() - started >= timeoutMs) throw new Error(`ComfyUI generation timed out after ${timeoutMs}ms`)
await sleep(1500, deadline.signal)
}
}
/** Run the ComfyUI tool. Reads no files; returns a served image URL string. */
export async function runGenerateImage(
config: ResolvedLabConfig,
args: GenerateImageArgs,
signal: AbortSignal | undefined,
): Promise<string> {
const deadline = new Deadline('generate_image', config.timeoutMs, signal)
try {
const clientId = `dsh-${Math.random().toString(36).slice(2)}`
const workflow = buildWorkflow(args)
const submit = await fetch(`${config.comfyBaseUrl}/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
})
if (!submit.ok) throw new Error(`ComfyUI /prompt ${submit.status}: ${await submit.text()}`)
const result = (await submit.json()) as ComfyApiResult
if (!result.prompt_id) throw new Error(`ComfyUI /prompt no prompt_id: ${JSON.stringify(result)}`)
const errors = result.node_errors
if (errors && Object.keys(errors).length > 0) throw new Error(`ComfyUI node_errors: ${JSON.stringify(errors)}`)
return await pollImage(config.comfyBaseUrl, result.prompt_id, deadline, config.timeoutMs)
} finally {
deadline.dispose()
}
}

View File

@@ -0,0 +1,85 @@
/**
* Docling OCR tool. Reads a local PDF via `ctx.fs`, uploads it to the lab's
* Docling server, polls the async task, and returns the extracted markdown.
* @module @deepseek-ai/dsh-tool-lab
*/
import type { FileSystem } from '@deepseek-ai/dsh-fs'
import { Deadline, sleep } from './helpers.ts'
import type { ResolvedLabConfig } from './comfy.ts'
interface DoclingPoll {
task_id?: string
task_status?: string
}
interface DoclingResult {
document?: { filename?: string; md_content?: string | null }
status?: string
errors?: unknown[]
}
/** Build FormData upload of a PDF from bytes. */
export function buildPdfUpload(data: Uint8Array, filename: string): FormData {
const fd = new FormData()
fd.append('files', new Blob([data as unknown as BlobPart], { type: 'application/pdf' }), filename)
fd.append('options', JSON.stringify({}))
return fd
}
/** Poll Docling `/v1/status/poll/{task_id}` until success. */
async function pollTask(
baseUrl: string,
taskId: string,
deadline: Deadline,
timeoutMs: number,
): Promise<void> {
const started = Date.now()
for (;;) {
deadline.check()
const res = await fetch(`${baseUrl}/v1/status/poll/${taskId}?wait=2`)
if (!res.ok) throw new Error(`Docling poll ${res.status}: ${await res.text()}`)
const poll = (await res.json()) as DoclingPoll
if (poll.task_status === 'success') return
if (poll.task_status === 'failed' || poll.task_status === 'error') {
throw new Error(`Docling task failed: ${await (await fetch(`${baseUrl}/v1/result/${taskId}`)).text()}`)
}
if (Date.now() - started >= timeoutMs) throw new Error(`Docling OCR timed out after ${timeoutMs}ms`)
await sleep(1200, deadline.signal)
}
}
/** Run the Docling OCR tool: read file, upload, poll, return markdown text. */
export async function runOcrPdf(
fs: FileSystem,
config: ResolvedLabConfig,
filePath: string,
signal: AbortSignal | undefined,
): Promise<string> {
const deadline = new Deadline('ocr_pdf', config.timeoutMs, signal)
try {
const target = await fs.resolve(filePath, signal === undefined ? undefined : { signal })
const stat = await fs.stat(target, signal)
if (!stat) throw new Error(`ocr_pdf: file not found: ${filePath}`)
const data = await fs.readBytes(target, signal, config.maxUploadBytes)
const filename = filePath.split(/[\\/]/).pop() ?? 'document.pdf'
const form = buildPdfUpload(data, filename)
const submit = await fetch(`${config.doclingBaseUrl}/v1/convert/file/async`, {
method: 'POST',
body: form,
})
if (!submit.ok) throw new Error(`Docling /convert ${submit.status}: ${await submit.text()}`)
const poll = (await submit.json()) as DoclingPoll
if (!poll.task_id) throw new Error(`Docling no task_id: ${JSON.stringify(poll)}`)
await pollTask(config.doclingBaseUrl, poll.task_id, deadline, config.timeoutMs)
const res = await fetch(`${config.doclingBaseUrl}/v1/result/${poll.task_id}`)
if (!res.ok) throw new Error(`Docling result ${res.status}: ${await res.text()}`)
const result = (await res.json()) as DoclingResult
const md = result.document?.md_content ?? ''
if (md.length === 0) throw new Error('Docling returned empty markdown content')
if (md.length > config.maxOutputChars) return md.slice(0, config.maxOutputChars)
return md
} finally {
deadline.dispose()
}
}

View File

@@ -0,0 +1,62 @@
/**
* Shared helpers for the home-lab tool package: cooperative deadline enforcement
* and aborted sleep. @module @deepseek-ai/dsh-tool-lab
*/
/** Error thrown when a lab tool exceeds its cooperative timeout budget. */
export class LabToolTimeoutError extends Error {
constructor(tool: string, ms: number) {
super(`lab tool ${tool} timed out after ${ms}ms`)
this.name = 'LabToolTimeoutError'
}
}
/** A deadline that aborts when the caller cancels or the budget expires. */
export class Deadline {
readonly signal: AbortSignal | undefined
private readonly timer: ReturnType<typeof setTimeout> | undefined
private cancelled = false
private elapsed = false
constructor(
private readonly tool: string,
private readonly timeoutMs: number,
signal: AbortSignal | undefined,
) {
this.signal = signal
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
this.timer = setTimeout(() => this.expire(), timeoutMs)
}
signal?.addEventListener('abort', () => this.expire(), { once: true })
}
private expire(): void {
if (this.cancelled || this.elapsed) return
this.elapsed = true
}
/** Throw if the budget has elapsed or the caller aborted. Cheap in loops. */
check(): void {
if (this.signal?.aborted || this.elapsed) {
throw new LabToolTimeoutError(this.tool, this.timeoutMs)
}
}
/** Cancel the timer when the tool finishes normally. */
dispose(): void {
this.cancelled = true
if (this.timer !== undefined) clearTimeout(this.timer)
}
}
/** Abortable sleep; resolves early or throws on cancellation/expiry. */
export async function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
if (signal?.aborted) throw new Error('aborted')
await new Promise<void>((resolve) => {
const t = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => {
clearTimeout(t)
resolve()
}, { once: true })
})
}

View File

@@ -0,0 +1,189 @@
/**
* Model-facing home-lab AI tools. Registers `lab_generate_image` (ComfyUI),
* `lab_ocr_pdf` (Docling), and `lab_transcribe_audio` (Whishper) over the
* lab's LAN services. Tools read local files only via `ctx.fs`; uploads and
* polls happen over HTTP. Image results stay server-hosted as URLs.
* @module @deepseek-ai/dsh-tool-lab
*/
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { runGenerateImage, type ResolvedLabConfig } from './comfy.ts'
import { runOcrPdf } from './docling.ts'
import { runTranscribeAudio } from './whish.ts'
export { runGenerateImage, buildWorkflow, type GenerateImageArgs } from './comfy.ts'
export type { ResolvedLabConfig } from './comfy.ts'
export { runOcrPdf, buildPdfUpload } from './docling.ts'
export { runTranscribeAudio, buildAudioUpload } from './whish.ts'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'tool-lab'
/** Services required by the lab tool suite. */
export const inject = ['tools', 'fs']
/** Default cooperative tool-call timeout budget (ms). */
export const DEFAULT_LAB_TIMEOUT_MS = 120_000
/** Default cap on one upload in bytes and one output in characters. */
export const DEFAULT_MAX_UPLOAD_BYTES = 20 * 1024 * 1024
export const DEFAULT_MAX_OUTPUT_CHARS = 200_000
/** Plugin config: which lab tools to register, the base URLs, and limits. */
export interface Config {
/** Register `lab_generate_image` (ComfyUI). Defaults to true. */
generateImage?: boolean
/** Register `lab_ocr_pdf` (Docling). Defaults to true. */
ocrPdf?: boolean
/** Register `lab_transcribe_audio` (Whishper). Defaults to true. */
transcribeAudio?: boolean
/** ComfyUI base URL. Defaults to http://192.168.31.240:8188 */
comfyBaseUrl?: string
/** Docling base URL. Defaults to http://192.168.31.159:5001 */
doclingBaseUrl?: string
/** Whishper base URL. Defaults to http://192.168.31.159:8082 */
whishBaseUrl?: string
/** Cooperative timeout budget (ms). Defaults to 120000. */
timeoutMs?: number
/** Cap on upload bytes. Defaults to 20971520. */
maxUploadBytes?: number
/** Cap on output characters. Defaults to 200000. */
maxOutputChars?: number
}
export const Config: z<Config> = z.object({
generateImage: z.boolean().default(true),
ocrPdf: z.boolean().default(true),
transcribeAudio: z.boolean().default(true),
comfyBaseUrl: z.string().default('http://192.168.31.240:8188'),
doclingBaseUrl: z.string().default('http://192.168.31.159:5001'),
whishBaseUrl: z.string().default('http://192.168.31.159:8082'),
timeoutMs: z.number().default(DEFAULT_LAB_TIMEOUT_MS),
maxUploadBytes: z.number().default(DEFAULT_MAX_UPLOAD_BYTES),
maxOutputChars: z.number().default(DEFAULT_MAX_OUTPUT_CHARS),
})
/** Complete config after schemastery applies every field default. */
type ResolvedConfig = Required<Config>
/** Configured limits must be positive integers. */
function assertPositiveInteger(field: string, value: number): void {
if (!Number.isInteger(value) || value < 1) {
throw new Error(`tool-lab: ${field} must be a positive integer`)
}
}
/** Render one string value as a single text content block. */
function renderText(_args: unknown, value: string): ContentBlock[] {
return [{ type: 'text', text: String(value) }]
}
/**
* Register the enabled lab tools. Each tool's cooperative timeout budget is
* resolved here and attached as `ToolDefinition.timeoutMs`. Disposers are
* fiber-scoped (effect-based registries clean up on dispose), so no manual
* teardown is needed.
*/
export function apply(ctx: Context, config: Config): void {
const resolved = config as ResolvedConfig
assertPositiveInteger('timeoutMs', resolved.timeoutMs)
assertPositiveInteger('maxUploadBytes', resolved.maxUploadBytes)
assertPositiveInteger('maxOutputChars', resolved.maxOutputChars)
const labConfig: ResolvedLabConfig = {
comfyBaseUrl: resolved.comfyBaseUrl,
doclingBaseUrl: resolved.doclingBaseUrl,
whishBaseUrl: resolved.whishBaseUrl,
timeoutMs: resolved.timeoutMs,
maxUploadBytes: resolved.maxUploadBytes,
maxOutputChars: resolved.maxOutputChars,
}
if (resolved.generateImage) {
ctx.tools.register(defineTool({
name: 'lab_generate_image',
description:
'Generate an image on the home-lab ComfyUI server (Juggernaut-XL V9, 512x512 base, upscaled 4x via RealESRGAN by default). Returns a URL to the generated PNG hosted on that server. Give a descriptive prompt; optionally adjust model, width, height, steps, seed, negative, upscale, upscale_model, or filename_prefix.',
parameters: {
prompt: { type: 'string', description: 'Positive prompt describing the desired image.', required: true },
negative: { type: 'string', description: 'Negative prompt.' },
width: { type: 'integer', description: 'Image width in pixels. Default 512.' },
height: { type: 'integer', description: 'Image height in pixels. Default 512.' },
steps: { type: 'integer', description: 'Sampling steps. Default 28 (Juggernaut).' },
seed: { type: 'integer', description: 'Random seed. Defaults to a random value.' },
model: { type: 'string', description: 'Checkpoint model filename. Default Juggernaut-XL_v9_RunDiffusionPhoto_v2.safetensors.' },
upscale: { type: 'boolean', description: 'Upscale the result 4x with RealESRGAN_x4plus. Default true.' },
upscale_model: { type: 'string', description: 'Upscale model filename. Default RealESRGAN_x4plus.safetensors.' },
filename_prefix: { type: 'string', description: 'Filename prefix for the saved image. Default dsh.' },
},
output: {
schema: { type: 'string' },
render: renderText,
},
timeoutMs: resolved.timeoutMs,
isConcurrencySafe: () => true,
async execute(args, exec) {
const imageArgs = args as {
prompt: string
negative?: string
width?: number
height?: number
steps?: number
seed?: number
model?: string
upscale?: boolean
upscale_model?: string
filename_prefix?: string
}
return runGenerateImage(labConfig, imageArgs, exec.signal)
},
}))
}
if (resolved.ocrPdf) {
ctx.tools.register(defineTool({
name: 'lab_ocr_pdf',
description:
'Extract text from a PDF using the home-lab Docling OCR server. Reads the local PDF and returns the recognized markdown text. Provide the local path to the PDF.',
parameters: {
file_path: { type: 'string', description: 'Local path to the PDF file to OCR.', required: true },
},
output: {
schema: { type: 'string' },
render: renderText,
},
timeoutMs: resolved.timeoutMs,
isConcurrencySafe: () => true,
execute(args, exec) {
const fs = ctx.fs
return runOcrPdf(fs, labConfig, args.file_path, exec.signal)
},
}))
}
if (resolved.transcribeAudio) {
ctx.tools.register(defineTool({
name: 'lab_transcribe_audio',
description:
'Transcribe speech from an audio file using the home-lab Whishper server. Returns the recognized text. Provide a local path to an audio file and optionally a language hint.',
parameters: {
file_path: { type: 'string', description: 'Local path to the audio file (wav, mp3, ogg, or m4a).', required: true },
language: { type: 'string', description: 'Optional language code hint (e.g. ru, en).' },
model_size: { type: 'string', description: 'Optional model size (e.g. small, base).' },
},
output: {
schema: { type: 'string' },
render: renderText,
},
timeoutMs: resolved.timeoutMs,
isConcurrencySafe: () => true,
execute(args, exec) {
const fs = ctx.fs
return runTranscribeAudio(fs, labConfig, args.file_path, args.language, args.model_size, exec.signal)
},
}))
}
}

View File

@@ -0,0 +1,29 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-lab`.
* @module @deepseek-ai/dsh-tool-lab/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-lab'
/** Cordis companion plugin name. */
export const name = 'tool-lab-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this model-facing adapter has no independent lifecycle
* stream; execution relations are owned by the capability seam it calls.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))

View File

@@ -0,0 +1,84 @@
/**
* Whishper speech-to-text tool. Reads local audio via `ctx.fs`, uploads it to
* the lab's Whishper server, polls the transcription, and returns the text.
* @module @deepseek-ai/dsh-tool-lab
*/
import type { FileSystem } from '@deepseek-ai/dsh-fs'
import { Deadline, sleep } from './helpers.ts'
import type { ResolvedLabConfig } from './comfy.ts'
interface WhishperTranscription {
id?: string
status?: number
result?: { text?: string | null; language?: string | null; duration?: number | null }
}
/** Build multipart upload of an audio file. */
export function buildAudioUpload(data: Uint8Array, filename: string, language?: string): FormData {
const fd = new FormData()
const ext = filename.split('.').pop()?.toLowerCase() ?? 'wav'
const type = ext === 'mp3' ? 'audio/mpeg' : ext === 'ogg' ? 'audio/ogg' : ext === 'm4a' ? 'audio/mp4' : 'audio/wav'
fd.append('files', new Blob([data as unknown as BlobPart], { type }), filename)
if (language) fd.append('language', language)
return fd
}
/** Poll Whishper `/api/transcriptions/{id}` until text is ready or done. */
async function pollTranscription(
baseUrl: string,
id: string,
deadline: Deadline,
timeoutMs: number,
): Promise<WhishperTranscription> {
const started = Date.now()
for (;;) {
deadline.check()
const res = await fetch(`${baseUrl}/api/transcriptions/${id}`)
if (!res.ok) throw new Error(`Whishper status ${res.status}: ${await res.text()}`)
const row = (await res.json()) as WhishperTranscription
const text = row.result?.text
if (typeof text === 'string' && text.length > 0 && row.status !== -1) return row
if (row.status !== -1 && (row.status === 1 || row.status === 2)) return row
if (Date.now() - started >= timeoutMs) throw new Error(`Whishper transcription timed out after ${timeoutMs}ms`)
await sleep(1500, deadline.signal)
}
}
/** Run the Whishper tool: read audio, upload, poll, return transcription text. */
export async function runTranscribeAudio(
fs: FileSystem,
config: ResolvedLabConfig,
filePath: string,
language: string | undefined,
modelSize: string | undefined,
signal: AbortSignal | undefined,
): Promise<string> {
const deadline = new Deadline('transcribe_audio', config.timeoutMs, signal)
try {
const target = await fs.resolve(filePath, signal === undefined ? undefined : { signal })
const statInfo = await fs.stat(target, signal)
if (!statInfo) throw new Error(`transcribe_audio: file not found: ${filePath}`)
const data = await fs.readBytes(target, signal, config.maxUploadBytes)
const filename = filePath.split(/[\\/]/).pop() ?? 'audio.wav'
const form = buildAudioUpload(data, filename, language)
if (modelSize) {
// Whishper server config may restrict model size; pass it as a field if supported.
form.append('modelSize', modelSize)
}
const submit = await fetch(`${config.whishBaseUrl}/api/transcriptions`, {
method: 'POST',
body: form,
})
if (!submit.ok) throw new Error(`Whishper upload ${submit.status}: ${await submit.text()}`)
const row = (await submit.json()) as WhishperTranscription
if (!row.id) throw new Error(`Whishper no id: ${JSON.stringify(row)}`)
const done = await pollTranscription(config.whishBaseUrl, row.id, deadline, config.timeoutMs)
const text = done.result?.text ?? ''
if (text.length === 0) return '(transcription completed with no text)'
if (text.length > config.maxOutputChars) return text.slice(0, config.maxOutputChars)
return text
} finally {
deadline.dispose()
}
}

View File

@@ -0,0 +1,60 @@
/**
* Unit tests for the pure helpers of `@deepseek-ai/dsh-tool-lab`. These
* assertions do not touch the network or the runtime; they check the workflow
* and upload builders only.
* @module @deepseek-ai/dsh-tool-lab/tests
*/
import { describe, expect, it } from 'vitest'
import { buildWorkflow } from '../src/comfy.ts'
import { buildPdfUpload } from '../src/docling.ts'
import { buildAudioUpload } from '../src/whish.ts'
/** One ComfyUI workflow node as seen by the assertions. */
interface WorkflowNode {
class_type: string
inputs: Record<string, unknown>
}
/** Reinterpret the workflow builder's JSON-serializable output for assertions. */
function wf(prompt: string, overrides: Record<string, unknown> = {}): Record<string, WorkflowNode> {
return buildWorkflow({ prompt, ...(overrides as object) }) as Record<string, WorkflowNode>
}
describe('buildWorkflow', () => {
it('renders a 28-step Juggernaut workflow with defaults', () => {
const flow = wf('a red fox')
expect(flow['4'].inputs.ckpt_name).toBe('Juggernaut-XL_v9_RunDiffusionPhoto_v2.safetensors')
expect(flow['9'].class_type).toBe('SaveImage')
expect(flow['9'].inputs.images).toEqual(['10', 0])
expect(flow['10'].class_type).toBe('ImageUpscaleWithModel')
expect(flow['11'].inputs.model_name).toBe('RealESRGAN_x4plus.safetensors')
expect(flow['3'].inputs).toMatchObject({ steps: 28, cfg: 4.0, sampler_name: 'dpmpp_2m_sde', scheduler: 'karras' })
})
it('skips upscaling when upscale is false', () => {
const flow = wf('p', { upscale: false })
expect(flow['9'].inputs.images).toEqual(['8', 0])
expect(flow['10']).toBeUndefined()
expect(flow['11']).toBeUndefined()
})
it('honors overrides and seed', () => {
const flow = wf('p', { width: 768, height: 640, steps: 8, seed: 99 })
expect(flow['5'].inputs).toMatchObject({ width: 768, height: 640 })
expect(flow['3'].inputs).toMatchObject({ seed: 99, steps: 8 })
expect(flow['6'].inputs.text).toBe('p')
})
})
describe('upload builders', () => {
it('builds a PDF upload carrying files and options parts', () => {
const fd = buildPdfUpload(new Uint8Array([1, 2, 3]), 'scan.pdf')
expect(fd).toBeDefined()
})
it('builds an audio upload', () => {
const fd = buildAudioUpload(new Uint8Array([1, 2]), 'clip.mp3')
expect(fd).toBeDefined()
})
})

View File

@@ -0,0 +1,28 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/tools"
},
{
"path": "../../fs/fs"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}