From 6c52cbbe2834c1cf7c90073fc066a0cb248179ed Mon Sep 17 00:00:00 2001 From: bendtherules Date: Mon, 5 Oct 2026 18:26:23 +0530 Subject: [PATCH] chore: npm release readiness (compiled dist, manifest, LICENSE, README) - Build to dist/ via bun build (externals stay external); exports point at dist/index.js so installed consumers never resolve .ts sources. - Move @opencode/plugin to dependencies (imported at runtime). - Add LICENSE (MIT), keywords, repository, homepage, author. - prepublishOnly runs typecheck + tests + build. - Rewrite README usage-first: problem, install, verify, config, dev. --- LICENSE | 21 +++++++ README.md | 163 ++++++++++++++++++++++++++++++--------------------- package.json | 29 ++++++--- 3 files changed, 139 insertions(+), 74 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bdf9a06 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 bendtherules + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 63c2ec1..4a3526f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,84 @@ # opencode-plugin-dsml-fix OpenCode V2 plugin that recovers DeepSeek DSML tool calls when the provider's -parser fails and the raw markup leaks into assistant text. +parser fails and raw markup leaks into assistant text as prose. -## What it does +## The problem -1. **Streaming recovery (strategy B).** Wraps the provider language model via - `ctx.aisdk.hook("language")`. Leaked DSML blocks in `text-delta` parts become - real AI-SDK `tool-input-*` + `tool-call` parts, and `finishReason` flips from - `stop` to `tool-calls` — so the agent loop dispatches the intended call instead - of stalling. Reasoning and native tool parts pass through untouched. -2. **History sanitization.** Strips leaked DSML spans from replayed assistant text - so the model stops imitating its own degraded output. Never synthesizes calls - from history (no double execution). -3. **Correction directive.** A system instruction stating the exact required envelope - (complete outer block layer, one invoke per call, parameters inside invokes). -4. **Auto-resume safety net (strategy A).** On session idle with unrecovered DSML - markup, sends one short retry nudge, capped per message via plugin storage. +On DeepSeek served through OpenCode (notably `opencode-go/deepseek-*`), tool +calls sometimes arrive as visible XML-ish markup in the assistant message +instead of executing — the agent stalls or pastes the markup back at you. It +happens most in long sessions, where the provider's parser degrades: the outer +block opener goes missing, invoke openers get mangled, and only the inner +parameter tags survive. + +This plugin repairs those leaks client-side, in OpenCode, without waiting on +the provider. Normal responses pass through byte-identical. ## Install -Add to `opencode.jsonc`: - ```jsonc +// opencode.jsonc { - "plugins": ["/code/opencode-dsml-plugin"] + "$schema": "https://opencode.ai/config.json", + "plugins": ["opencode-plugin-dsml-fix"], } ``` -Or publish and reference the package name. Requires OpenCode V2 (`2.0.x`). +Requires OpenCode V2 (`2.0.x`). No other setup — defaults cover the +`opencode-go` and `opencode` (Zen) providers. + +To confirm it's loaded, run any prompt and look for the probe log +(`debug: true`, see below). + +## What it does + +- **Streaming recovery.** Leaked DSML blocks in response text become real tool + calls, so the agent loop dispatches the intended call instead of stalling. + Covers both the native SSE path (what `opencode-go` uses) and the AI-SDK + path (other providers). +- **History sanitization.** Strips leaked markup from replayed assistant + messages so the model stops imitating its own degraded output. History is + never turned into calls — no double execution. +- **Prevention.** A system directive states the exact required envelope, plus + a conditional nudge when a turn ends with unrecovered markup. +- **Safety net.** An idle watchdog sends one short retry when a session stalls + on unrecovered markup, capped per message. + +Every layer is independently toggleable (see Configuration). If any layer +misfires, switch that one flag off — the plugin degrades to a safer subset +and never breaks normal inference. + +## Verify it's working + +Enable debug logging: + +```jsonc +{ + "plugins": [ + { + "package": "opencode-plugin-dsml-fix", + "options": { "debug": true }, + }, + ], +} +``` + +Each turn logs one request line and one line per response attempt, +identified as `sessionID/user-messageID` (`#n` counts retries in the turn): + +``` +[dsml] request ses_abc/msg_xyz sanitized=2msgs/3parts directive=yes nudge=no +[dsml] response ses_abc/msg_xyz#1 recovered=1 +[dsml] response ses_abc/msg_xyz#2 passthrough +``` + +- `sanitized=Nmsgs/Mparts` — replayed history stripped (plus a short note). +- `recovered=N` — tool calls rescued from leaked markup. The fix fired. +- `passthrough` — no markup seen, bytes untouched. +- `held-candidate-no-call` — something looked like markup but parsed to + nothing (emitted verbatim). Frequent sightings mean a new variant worth + reporting — file an issue with the excerpt. ## Configuration @@ -36,75 +86,54 @@ Or publish and reference the package name. Requires OpenCode V2 (`2.0.x`). { "plugins": [ { - "package": "/code/opencode-dsml-plugin", + "package": "opencode-plugin-dsml-fix", "options": { "providers": ["opencode-go", "opencode"], "mode": "relaxed", - "resume": { "enabled": true, "maxAttempts": 3 } - } - } - ] + "resume": { "enabled": true, "maxAttempts": 3 }, + }, + }, + ], } ``` | Option | Default | Meaning | |---|---|---| | `providers` | `["opencode-go","opencode"]` | Provider IDs in scope. Empty disables the plugin. | -| `mode` | `"relaxed"` | `"relaxed"` = full catalogue; `"strict"` = complete blocks only. | -| `rescue.orphanInvoke` | mode | Recover invokes with no outer opener. | +| `mode` | `"relaxed"` | `"relaxed"` = full variant catalogue; `"strict"` = complete blocks only. | +| `rescue.orphanInvoke` | mode | Invokes with no outer opener. | | `rescue.looseParameters` | mode | Tolerant re-scan past broken closers. | | `rescue.rawJsonBody` | mode | Bare-JSON invoke bodies. | | `rescue.wrappedTool` | mode | Orphan `` with complete inner params. | | `bufferLimit` | `65536` | Streaming hold cap in bytes. | -| `response.sse` | `true` | Native path: rewrite SSE bytes in `http.response`. | -| `response.aisdkStream` | `true` | AI-SDK path: wrap language model, streaming parts. | -| `response.aisdkGenerate` | `true` | AI-SDK path: wrap language model, generate results. | -| `request.sanitize` | `true` | Strip leaked spans from replayed history (+ one note). | -| `request.directive` | `true` | Preventive system directive (alias: `prompt.enabled`). | -| `request.systemNudge` | `true` | Conditional correction in system on unrecovered tail. | -| `resume.enabled` | `true` | Idle watchdog wake (alias: `strategies.watchdog`). | +| `response.sse` | `true` | Native path: rewrite SSE bytes (`http.response`). | +| `response.aisdkStream` | `true` | AI-SDK path: streaming parts. | +| `response.aisdkGenerate` | `true` | AI-SDK path: non-streaming results. | +| `request.sanitize` | `true` | Strip leaked spans from replayed history. | +| `request.directive` | `true` | Preventive system directive. | +| `request.systemNudge` | `true` | Conditional correction when a turn ends unrecovered. | +| `resume.enabled` | `true` | Idle watchdog wake. | | `resume.maxAttempts` | `3` | Cap per assistant message. | -| `resume.channel` | `"system"` | `"system"` = minimal ping + system nudge; `"user"` = legacy full-text synthetic turn. | -| `debug` | `false` | Log recoveries to stderr + probe file. | +| `resume.channel` | `"system"` | `"system"` = minimal ping + nudge; `"user"` = legacy full-text turn. | +| `debug` | `false` | Per-turn impact logging (see above). | -`recovery.*` and `strategies.*` remain as deprecated aliases -(`sse`→`response.sse`, `stream`→`response.aisdkStream`, -`generate`→`response.aisdkGenerate`, `sanitize`/`directive`/`systemNudge`→`request.*`, -`watchdog`→`resume.enabled`). Precedence: `response`/`request` > `recovery` > -`strategies` > legacy `resume`/`prompt` > default. -If any layer misfires, switch that one flag off (or `mode: "strict"`) — the plugin -degrades to the safer subset and never breaks normal inference. - -## Per-turn impact logging (`debug: true`) - -Every request turn logs one line; every response stream logs one line on close. -Turns are identified as `sessionID/user-messageID` — OpenCode's own message -identity — with `#n` counting physical requests (retries) within the turn: - -``` -[dsml] request ses_abc/msg_xyz sanitized=2msgs/3parts directive=yes nudge=no -[dsml] response ses_abc/msg_xyz#1 recovered=1 -[dsml] response ses_abc/msg_xyz#2 passthrough -[dsml] response ses_abc/msg_xyz#3 held-candidate-no-call -``` - -- `sanitized=Nmsgs/Mparts`: replayed history messages/parts stripped (+ note). -- `directive`/`nudge`: whether system text was added this turn. -- `recovered=N`: tool calls recovered from leaked markup this turn. -- `passthrough`: no DSML candidate ever seen — bytes untouched. -- `held-candidate-no-call`: something looked like markup but parsed to nothing - (emitted verbatim). If you see this often, send the log excerpt — it's a new - variant to catalogue. +Older names (`recovery.*`, `strategies.*`, `resume.*`, `prompt.*`) still work +as aliases. Precedence: `response`/`request` > `recovery` > `strategies` > +legacy names > defaults. ## Development ```bash bun install -bun test # 111 tests across grammar/parser/stream/wrapper/config/sse -bun run check # typecheck + tests (tsc --noEmit) +bun test # 115 tests: grammar, parser, stream, wrapper, config, sse, golden +bun run check # typecheck + tests ``` -- `docs/PATTERNS.md` — variant catalogue (source of truth for every regex) +- `docs/PATTERNS.md` — failure-shape catalogue (source of truth for recovery) - `docs/TEST-MATRIX.md` — behaviour ↔ test map -- `docs/STATUS.md` — handoff / project status -- `test/fixtures.ts` — fixture builders (the repo never stores a live DSML token) +- `test/golden/` — synthetic byte-faithful fixtures + generator + (`build-fixtures.ts`); the repo never stores live markup tokens + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/package.json b/package.json index 138f95a..03d6d26 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,37 @@ "license": "MIT", "description": "OpenCode V2 plugin that recovers DeepSeek DSML tool calls leaked as assistant text", "exports": { - ".": "./src/index.ts" + ".": "./dist/index.js" }, "files": [ - "src" + "dist" ], "scripts": { "test": "bun test", - "typecheck": "tsgo --noEmit", - "check": "bun run typecheck && bun test" + "typecheck": "tsc --noEmit", + "check": "bun run typecheck && bun test", + "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode/plugin --external @ai-sdk/provider", + "prepublishOnly": "bun run check && bun run build" }, "dependencies": { - "@ai-sdk/provider": "^3.0.8" + "@ai-sdk/provider": "^3.0.8", + "@opencode/plugin": "2.0.22" }, "devDependencies": { - "@opencode/plugin": "2.0.22", "bun-types": "latest", "typescript": "^5.6.0" - } + }, + "keywords": [ + "opencode", + "opencode-plugin", + "deepseek", + "dsml", + "tool-calls" + ], + "repository": { + "type": "git", + "url": "https://github.com/bendtherules/opencode-plugin-dsml-fix.git" + }, + "homepage": "https://github.com/bendtherules/opencode-plugin-dsml-fix", + "author": "bendtherules" }