← Back to home@Dinosaur-MC

dsh-decision-tools

DSH plugin that turns a local Ollama System One model into two agent tools: decision for probability-only routing, classification, filtering and scoring, and monitor for polling files, jobs, commands or job events until an event happens. Background monitors stream progress and fire one-line trigger notices; they observe, never act.

Stars
0
Language
JavaScript
Created
Oct 4, 2026
Updated
Oct 19, 2026

Introduction

dsh-decision-tools

Ask a local model for probabilities, not prose. Two DSH tools turn bounded judgements into cheap, repeatable calls:

ToolWhat it answersTypical use
decisionOne criterion over 1–64 items, or up to 64 different questions — in one callFilter a list, classify tickets, route requests, rank candidates, "does this log mean failure?"
monitorRe-samples one source until your criterion holdsWait for a build to finish, a line to appear, a job to fail

Judgements run on your own Ollama (/v1/systemone). Nothing goes to a cloud provider, and the material never has to enter your conversation.

Why

  • Cheap by design — the rubric is sent once, and one call judges up to 64 items. Looping one call per item is the expensive anti-pattern.
  • Material stays out of your context — pass a workspace file, a glob, or another tool's raw output; you only read back the answers.
  • Honest output — every answer is a probability plus confidence, partial work is reported in coverage, and notes says what the plugin did.

Requirements

  • DSH desktop with this plugin enabled (see Install).
  • A local Ollama serving the decision model — default tev1:0.8b, override per call with model.
  • No API key; the judgement itself makes no outbound call.
  • For image material: a vision-capable model (clef / clef-flash). tev1 / nimble return does not support vision.

Install

From npm, once the package is published:

dsh plugin --profile web add dsh-decision-tools

For development from a checkout, link it into the profile:

dsh plugin --profile web add link:/absolute/path/to/dsh-decision-tools

Then confirm the tools are visible:

cordis_inspect_query(platform="host", provider="Tool", method="listTools")

The companion skill decision-usage is registered with the tools and documents every parameter; it loads on demand.

Using a different local model

The shipped defaults (baseUrl = http://localhost:11434, defaultModel = tev1:0.8b, and the four-rung ladder) are a snapshot. Any Ollama model that answers the System One endpoint works:

  • pin one for a call — model: "<name>" (disables capacity picking and escalation);
  • change the session default — defaultModel;
  • point at another host/port — baseUrl;
  • control escalation — ladder: [{ model, numCtx }] (see Configuration).

To see what is actually installed, and get a paste-ready ladder:

node scripts/list-models.mjs          # name · context · parameters · size
node scripts/list-models.mjs --json   # { models, ladder }

numCtx comes from /api/show's declared context length — an upper bound; the runtime num_ctx can be smaller.

Quick start

// 1) Filter a list: one rubric, N items
{ "material": "<the list or its context>", "rubric": "Is this a bug report?",
  "items": ["app crashes on save", "please add dark mode"], "type": "noul" }

// 2) Ask different questions about one file
{ "material": { "file": { "path": "build.log", "startLine": 1, "endLine": 200 } },
  "questions": [
    { "name": "ok", "type": "noul", "instructions": "Did the build succeed?" },
    { "name": "why", "type": "choice", "instructions": "What failed?",
      "criteria": { "compile": "compile error", "test": "a test failed", "env": "missing dependency" } }
  ] }

// 3) Judge another tool's output without reading it
{ "material": { "tool": { "name": "git_bash", "arguments": { "command": "npm test" } } },
  "rubric": "Did the test suite pass?", "items": ["the test suite output"], "type": "noul" }

// 4) Judge an image (needs a vision model)
{ "material": { "images": ["screenshots/checkout-error.png"] },
  "model": "clef-flash:latest",
  "questions": [ { "name": "has_error", "type": "noul", "instructions": "Does this screenshot show an error state?" } ] }

// 5) Wait for a background job, and get a notice on a hit
{ "source": { "kind": "job", "id": "<jobId>" },
  "rubric": "Did the job finish successfully?", "background": true, "notify": "inbox" }

decision: one call, one mode

Give material plus exactly one mode:

ModeShapeMeaning
Batchrubric + items + type (+ criteria)One shared criterion, judged independently per item
MixedquestionsUp to 64 different questions about the same material
ParameterRequiredNotes
materialyesInline string, {file}, {glob}, {tool}, {images}, or {text, images}
rubricbatchThe one shared criterion; the wire layer repeats it for the model
itemsbatch1–64 self-contained items; plain strings are valid
questionsmixed1–64 {name, type, instructions, criteria?}
typebatchchoice / noul / score (mixed: per question)
criteriachoice/scoreLabels once: top-level in batch, per question in mixed
depthnoquick (no escalation/batching), normal (default), deep (may chunk); ignored for images
modelnoPin a model; disables capacity choice and escalation

Material forms: inline text · { file: { path, startLine?, endLine? } } · { glob: { pattern, limit? } } · { images: ["a.png"] } (or { text, images }) · { tool: { name, arguments? } }. A tool's raw output never enters your context; the full text is spilled and returned as material.spillPath. text/file/glob may be combined with images; tool cannot be combined with any other form, and the plugin's own tools are refused.

Images: workspace-relative PNG/JPEG/WebP paths (1–8; ≤16 MiB each, ≤24 MiB total raw, request body ≤32 MiB). The plugin reads, magic-checks and base64-encodes them; the bytes never enter your context. Image requests require a vision model and run as a single request — depth is ignored (no escalation, batching or scanning). Output includes material.images (count/bytes/encodedBytes/formats).

Read the result: results[] (id, type, value, probabilities?, confidence?) · coverage (requested/answered/batches/truncated/truncatedBy/failed/aborted) · cost (model/inputTokens/bodyBytes/wallMs/escalated/attempts) · notes (first line = model-selection decision).

noul returns a boolean value; its P(true) is in probabilities.true (0.475 = 47.5% yes). score is on the 0..N-1 scale; choice returns the chosen label. confidence is concentration, not accuracy — when notes flags low-confidence items, read probabilities instead of the label.

monitor: wait for an event

Samples one source on an interval, asks the same rubric each time, and counts hits. once returns when countThreshold is reached (or the deadline expires); continuous runs to the deadline and returns the accumulated count. It never kills, retries, or cancels anything.

SourceKeysReads
jobid?, select? (newest/oldest), jobKind?A background job's output (default: newest of this session)
filepath, startLine?, endLine?Workspace-relative file
globpattern, limit?Matched files, concatenated
commandargvRe-runs a host command through the plugin's upstream pipe
eventstypes, limit?Job-event ring buffer (falls back to the session's current job list)

Parameters: description (required; job description; label = source.kind + description) · source (required) · rubric (required) · type (noul only) · criteria (noul optional false/true descriptions) · hitThreshold (0.5) · tailBytes (4096, max 65536) · mode (once|continuous) · countMode (cumulative|consecutive, default cumulative) · notifyEvery (continuous, default 1) · intervalMs (5000) · timeoutMs (120000) · countThreshold (1; once only) · background · notify · coalesce · maxNotificationsPerMinute (6).

  • Empty samples: file/glob with empty content return immediately (samples: 1, emptySource: true, no model call); job/events/command empty samples count as P=0 and keep waiting for the event.
  • Details: the host finished [...] shows hit/P|EP/dur (once) or trigger_num/EP/dur (continuous); the job label is source.kind + description. For once, EP = Σ(hit P)/countThreshold (missing hits count as 0) and a failed once shows 1-EP; for continuous, EP is the mean of per-trigger P (a trigger P = Σ(window hit P)/notifyEvery; 0 triggers ⇒ 0). Durations are ms. A boolean-only verdict ({hit:true} without P) counts as 0.00, so detail/notice/stream never show - for a hit. description is folded to one line before it becomes the job label. Permanent source errors (invalid descriptor, outside root, explicit job id missing, file too large) fail fast; transient read errors are retried.
  • Background: background: true returns a jobId; job_output streams progress lines (config/hit/trigger/summary) and is consumed on read. notify: "inbox" posts a one-line notice only at a trigger (once: threshold; continuous: every notifyEvery hits) and requires background. Job status: abort ⇒ killed; all probes errored (no valid verdict) ⇒ failed; timeout / valid verdict ⇒ completed. Abort interrupts an in-flight decide and sleep, so killed is prompt.
  • command argv: argv[0] picks the interpreter (pwsh, git_bash/bash/sh/zsh); interpreter flags are stripped; quote arguments containing spaces yourself.
  • events.types is validated against a fixed vocabulary (job.registered, job.stopping, job.output, job.progress, job.settled, job.completed, job.failed, job.killed, job.removed, job.running, job.unknown); a typo fails as source_invalid.

Configuration

The plugin is four rows: a core row, a decision row, a monitor row and a skill row. Each row has its own switch in the Plugins page; the core/decision/monitor rows also have their own Configure page. GUI-editable fields are declared .volatile() and apply to the next call without reloading the row. toolName / monitorToolName / ladder / spillDirCap are registration-time or advanced YAML fields: edit them in cordis.patch.yml and reload that row.

Core row — decision-tools-plugin (shared model & runtime)

KeyDefaultMeaning
baseUrlhttp://localhost:11434Ollama endpoint; path prefixes are preserved; GUI-editable.
defaultModeltev1:0.8bModel used when a tool row has no modelOverride; GUI-editable.
timeoutMs120000Per-request budget, including cold model load; GUI-editable.
keepAliveunsetPassed as keep_alive; unset uses the server default; GUI-editable.
apiKeyunsetOptional Bearer token for remote / System One-compatible endpoints; GUI-editable.
modelsPath/api/tagsModel discovery path; use /v1/models for OpenAI-compatible endpoints; GUI-editable.
systemOnePathv1/systemoneJudgement endpoint path relative to baseUrl; GUI-editable.
modelOrderunsetComma-separated model priority; model@numCtx sets context; unknown models use 8192; GUI-editable.
escalationEnabledtrueAllow automatic escalation on context overflow; GUI-editable.
ladderunset (DEFAULT_LADDER)Ordered {model,numCtx}[]; YAML only; reload the core row after editing.
toolNamedecisionModel-visible decision tool name; reload the decision row after editing.
monitorToolNamemonitorModel-visible monitor tool name; reload the monitor row and the decision row after editing (both snapshot the name).
spillDirCapunset (16 MiB)Byte cap for the plugin's spill directory; YAML only.

decision row — decision-tools-decision

KeyDefaultMeaning
modelOverrideunset (core default)Blank uses the core defaultModel; GUI-editable.
defaultDepthnormalquick / normal / deep; used when the call omits depth; GUI-editable.
bodyWarningtruefalse suppresses size/text-shape notes; GUI-editable.
softBodyBytesunset (4096)Body size above which notes[] carries a size note; 0 warns every call; GUI-editable.
minConfidenceunset (0.25)choice/score confidence below this (or missing) is named in notes[]; 0 disables; GUI-editable.
maxChoiceOptionsunset (255)Upper bound on choice options; GUI-editable.
escalationMaxAttempts3Extra escalation attempts after the first call; deep adds 2; blank = 3; GUI-editable.
scanFirstChunkBytesunset (4096)First scan chunk byte budget; GUI-editable.
scanMaxChunksunset (32)Maximum chunks per scan; GUI-editable.
scanMaxTotalBytesunset (4 MiB)Cumulative byte cap per scan; GUI-editable.
scanDeadlineMsunset (180000)Wall-clock budget per scan; GUI-editable.
scanAggregatenonenone / any; any adds a provisional cross-chunk max probability; GUI-editable.

monitor row — decision-tools-monitor

KeyDefaultMeaning
modelOverrideunset (core default)Blank uses the core defaultModel; GUI-editable.
minProbeIntervalMsunset (1000)Lower bound for the monitor probe interval; GUI-editable.
notifyCoalesceWindowMsunset (5000)Same-kind notice coalescing window; 0 disables coalescing; GUI-editable.
maxNotificationsPerMinuteunset (6)Per-minute notice cap; GUI-editable.

Override an existing row with a bare id and no insert; fields go under config. config is replaced wholesale, never deep-merged, so restate every field the row needs.

- id: decision-tools-plugin
  config:
    baseUrl: 'http://localhost:11434'
    defaultModel: 'tev1:0.8b'
    timeoutMs: 180000
    keepAlive: '30m'
- id: decision-tools-decision
  config:
    modelOverride: 'tev1:0.8b'
    defaultDepth: normal
- id: decision-tools-monitor
  config:
    modelOverride: 'nimble:latest'

The three Config schemas live in lib/configs.js.

Troubleshooting

SymptomLikely causeFix
context_overflowMaterial + questions exceed the model's windowUse depth: "deep" (chunking), smaller material, or fewer questions
Tool not listedPlugin not enabled in the profileLink it into the profile and reload
ERR_MODULE_NOT_FOUND after a DSH upgradeWorkspace node_modules still points at the old installRun scripts/rebuild-node-modules.mjs inside DSH, then re-run the tests
monitor gives hit:false with errors: 0Source was empty, or types matched nothingCheck emptySource; notes states the reason
monitor gives errors: N > 0Reads or judgements failedInspect the error text; failed probes are also rendered
source.kind=events failsEvent source unavailable in this compositionFalls back to the session's current job list when possible
invalid_request / mode_invalidMalformed argumentsThe message lists legal values; mode_invalid = mode/count/shape, invalid_request = a field

Limits and safety

  • These are probabilities, not proof. Never treat one as correctness evidence, and never let it be the sole basis for a pass/allow decision — that needs artifact-level evidence.
  • The tool must not decide routing, and must not be the basis of any fail-open or fail-closed policy; those belong to deterministic rules.
  • tev1 returns only probabilities and no text — use a generative model when you need a human-readable rationale.
  • Raw tool output is spilled to a plugin-owned temp directory (16 MiB cap, spillDirCap); the path is returned in the result.
  • Judgements are local; the plugin makes no cloud calls of its own.

Development

node --test test/*.test.mjs     # full suite
node --check lib/*.js          # syntax gate

After editing lib/, reload the plugin in DSH before drawing conclusions from real calls: the host runs the last loaded snapshot.

Licence

MIT — see LICENSE.

No compatibility layer

This is a one-time structural upgrade, not a version transition. Old row ids (decision-tools-tool) and old config keys (decisionEnabled / monitorEnabled) are not accepted; use the four rows and three Config tables above. See docs/architecture.md for the as-is contract and docs/use-cases.md for worked examples.