@dsh-jev/effort 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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @dsh-jev/effort
2
+
3
+ dsh Web-only plugin: [jev System One](https://typesafe.ai) lowers the
4
+ reasoning effort to `low` for simple turns, with a dismissible composer hint.
5
+
6
+ Modeled on the mature `dsh-quote-followup` plugin pattern:
7
+
8
+ - **Two halves.** The host face (`lib/index.js`) hooks the `agent/request`
9
+ waterfall: before a turn's first request it asks jev `choice(keep|lower)`
10
+ with the newest user text as context; a "lower" pick rewrites
11
+ `reasoningEffort` to `low` (configurable via `targetEffort`). The browser
12
+ face (`lib/client.js`, mounted via the package's `dsh.client`
13
+ `platform: web` declaration) renders a lightweight, dismissible hint above
14
+ the composer explaining the behavior.
15
+ - **Never touches provider/model.** Only `reasoningEffort` is rewritten —
16
+ route selection is deliberately out of scope (that's `@dsh-jev/router`'s
17
+ job, with its own registration gates). No LLM route is ever registered.
18
+ - **Silent fallback.** jev unreachable / timeout / non-2xx → the resolved
19
+ config is returned unchanged (original effort kept), logged only. The
20
+ listener never throws into the request path.
21
+ - **Composer hint is dismissible** (persisted in `localStorage`), localized
22
+ via the injected DSH `locale` service with English fallback, and rendered
23
+ as a plain sibling DOM node — no contenteditable or Lexical internals are
24
+ touched (the two known contenteditable/paste footguns do not apply).
25
+ - **Hot-swap hardening.** Versioned global state + element ownership
26
+ (`data-dsh-jev-effort-version`) so an old listener from a long-lived tab
27
+ cannot tear down the new UI after a client bundle swap; old hosts without
28
+ `locale` degrade to English copy, without a composer the hint stays hidden.
29
+
30
+ Known seam limitation (v1): the composer hint is informational — the dynamic
31
+ jev verdict lives host-side (visible in stdout logs), because the browser
32
+ must not hold the jev API key.
33
+
34
+ ## Install into the Web profile
35
+
36
+ ```sh
37
+ dsh plugin --profile web add dsh-jev-effort # once published
38
+ ```
39
+
40
+ or via the bundle patch in `cordis.patch.yml` (`- insert: - id: jev-effort`).
41
+
42
+ ## Development
43
+
44
+ ```sh
45
+ pnpm --filter @dsh-jev/effort test # syntax checks + 11 vitest cases
46
+ ```
@@ -0,0 +1,11 @@
1
+ # dsh-jev-effort Web bundle patch.
2
+ # The host row carries the effort-lowering request waterfall; the browser
3
+ # client entry (mounted via dsh.client) renders the composer hint.
4
+
5
+ - insert:
6
+ - id: jev-effort
7
+ name: dsh-jev-effort
8
+ config:
9
+ enabled: true
10
+ # targetEffort: low
11
+ # endpoint / apiKey / timeoutMs are honored via @dsh-jev/core env defaults
package/lib/client.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * dsh-jev-effort — browser half (Web face).
3
+ *
4
+ * A lightweight, dismissible hint above the composer telling the user that
5
+ * simple turns are automatically lowered to `low` reasoning effort by jev
6
+ * (the host half does the actual judgment at `agent/request`; the browser
7
+ * cannot hold the API key). Dismissal persists in localStorage.
8
+ *
9
+ * Follows the dsh-quote-followup hardening pattern (see ~/.config/dsh/AGENTS.md):
10
+ * 1. never touches contenteditable internals — the hint is a sibling DOM node;
11
+ * 2. no synthetic clipboard / paste commands needed at all;
12
+ * 3. versioned global state + element ownership survive client hot-swaps;
13
+ * 4. old hosts degrade silently (no locale → English copy, no composer → hidden).
14
+ *
15
+ * @module dsh-jev-effort/client
16
+ */
17
+ window.__ModuleLoader__.load({
18
+ id: "dsh-jev-effort",
19
+ factory: () => {
20
+ const exports = {};
21
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
22
+ const CLIENT_VERSION = "0.1.0";
23
+ const HINT_ID = "dsh-jev-effort-hint";
24
+ const OWNERSHIP_ATTR = "data-dsh-jev-effort-version";
25
+ const DISMISS_KEY = "dsh-jev-effort.hint.dismissed";
26
+ const COMPOSER_CONTAINER_SELECTORS = [
27
+ '[data-slot="conversation.composer"]',
28
+ "[data-composer-card]",
29
+ '[data-input-scroll]'
30
+ ];
31
+ const LOCALE_NS = "jev-effort";
32
+ const LOCALE_DICT = {
33
+ zh: {
34
+ "hint.text": "⚡ jev:简单问题发送时将自动降低为 low 推理档",
35
+ "hint.dismiss": "知道了"
36
+ },
37
+ en: {
38
+ "hint.text": "⚡ jev: simple turns are automatically lowered to low reasoning effort",
39
+ "hint.dismiss": "Got it"
40
+ }
41
+ };
42
+ const fallbackT = (key) => LOCALE_DICT.en[key] ?? key;
43
+ let currentT = fallbackT;
44
+ const isDismissed = () => {
45
+ try {
46
+ return globalThis.localStorage?.getItem(DISMISS_KEY) === "1";
47
+ } catch {
48
+ return false;
49
+ }
50
+ };
51
+ const findComposerContainer = () => {
52
+ for (const selector of COMPOSER_CONTAINER_SELECTORS) {
53
+ const element = document.querySelector(selector);
54
+ if (element !== null)
55
+ return element;
56
+ }
57
+ return null;
58
+ };
59
+ const styleOnce = (() => {
60
+ let done = false;
61
+ return () => {
62
+ if (done)
63
+ return;
64
+ done = true;
65
+ const style = document.createElement("style");
66
+ style.id = "dsh-jev-effort-style";
67
+ style.textContent = `#${HINT_ID}{display:flex;align-items:center;gap:8px;margin:0 0 6px;padding:4px 10px;font-size:12px;line-height:1.4;border-radius:8px;background:color-mix(in srgb, var(--ds-primary, #4c6ef5) 8%, transparent);color:inherit;opacity:.85}
68
+ #${HINT_ID} button{margin-left:auto;border:none;background:transparent;color:inherit;font:inherit;cursor:pointer;opacity:.7;text-decoration:underline;padding:0}`;
69
+ document.head.append(style);
70
+ };
71
+ })();
72
+ /**
73
+ * (Re)create the hint bar. Ownership: only the state whose version matches
74
+ * the live element's stamped version may remove it — a stale hot-swapped
75
+ * listener cannot tear down the new UI.
76
+ */
77
+ const renderHint = () => {
78
+ if (isDismissed())
79
+ return;
80
+ const container = findComposerContainer();
81
+ if (container === null)
82
+ return;
83
+ styleOnce();
84
+ const existing = document.getElementById(HINT_ID);
85
+ if (existing?.getAttribute(OWNERSHIP_ATTR) === CLIENT_VERSION)
86
+ return;
87
+ existing?.remove();
88
+ const bar = document.createElement("div");
89
+ bar.id = HINT_ID;
90
+ bar.setAttribute(OWNERSHIP_ATTR, CLIENT_VERSION);
91
+ const text = document.createElement("span");
92
+ text.textContent = currentT("hint.text");
93
+ const dismiss = document.createElement("button");
94
+ dismiss.type = "button";
95
+ dismiss.textContent = currentT("hint.dismiss");
96
+ dismiss.addEventListener("click", () => {
97
+ try {
98
+ globalThis.localStorage?.setItem(DISMISS_KEY, "1");
99
+ } catch {}
100
+ if (bar.getAttribute(OWNERSHIP_ATTR) === CLIENT_VERSION)
101
+ bar.remove();
102
+ });
103
+ bar.append(text, dismiss);
104
+ container.parentElement?.insertBefore(bar, container);
105
+ };
106
+ const STATE = Symbol.for("dsh-jev-effort.state");
107
+ function apply(ctx) {
108
+ const previous = globalThis[STATE];
109
+ if (previous?.version === CLIENT_VERSION)
110
+ return;
111
+ if (typeof previous?.dispose === "function")
112
+ previous.dispose();
113
+ let unregisterLocale = null;
114
+ let observer = null;
115
+ let disposed = false;
116
+ let state;
117
+ try {
118
+ const locale = typeof ctx?.get === "function" ? ctx.get("locale") : null;
119
+ if (typeof locale?.register === "function" && typeof locale?.bind === "function") {
120
+ unregisterLocale = locale.register(LOCALE_NS, LOCALE_DICT);
121
+ currentT = locale.bind(LOCALE_NS);
122
+ } else {
123
+ currentT = fallbackT;
124
+ }
125
+ } catch {
126
+ currentT = fallbackT;
127
+ }
128
+ renderHint();
129
+ // The composer mounts asynchronously; watch for it and late layouts.
130
+ try {
131
+ observer = new MutationObserver(() => renderHint());
132
+ observer.observe(document.body, { childList: true, subtree: true });
133
+ } catch {}
134
+ const cleanup = () => {
135
+ if (disposed)
136
+ return;
137
+ disposed = true;
138
+ observer?.disconnect();
139
+ if (typeof unregisterLocale === "function")
140
+ unregisterLocale();
141
+ currentT = fallbackT;
142
+ const bar = document.getElementById(HINT_ID);
143
+ if (bar?.getAttribute(OWNERSHIP_ATTR) === CLIENT_VERSION)
144
+ bar.remove();
145
+ if (globalThis[STATE] === state)
146
+ delete globalThis[STATE];
147
+ };
148
+ state = { version: CLIENT_VERSION, dispose: cleanup };
149
+ globalThis[STATE] = state;
150
+ if (ctx !== undefined && typeof ctx.effect === "function")
151
+ ctx.effect(() => cleanup, "dsh-jev-effort: composer hint");
152
+ }
153
+ exports.inject = ["locale"];
154
+ exports.apply = apply;
155
+ return exports;
156
+ }
157
+ });
package/lib/index.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * dsh-jev-effort — host face.
3
+ *
4
+ * Web-only feature (the bundle patch is only inserted into the Web profile),
5
+ * but this host entry is the half that can actually see the request: on the
6
+ * `agent/request` waterfall it asks jev System One whether the current turn
7
+ * is simple enough to lower `reasoningEffort` to `low`. It NEVER touches
8
+ * provider/model — route selection belongs elsewhere (dsh-jev-router does
9
+ * that job deliberately, with its own gates) — and on any jev failure it
10
+ * silently returns the resolved config unchanged (original effort kept).
11
+ *
12
+ * The browser half (lib/client.js) renders the dismissible composer hint.
13
+ */
14
+
15
+ import { createJevClient } from '@dsh-jev/core';
16
+
17
+ /** Rank: lower number = more effort. Unknown efforts never get lowered. */
18
+ export const EFFORT_RANK = { max: 0, high: 1, medium: 2, low: 3 };
19
+
20
+ export function canLowerEffort(current, target = 'low') {
21
+ const a = EFFORT_RANK[current];
22
+ const b = EFFORT_RANK[target];
23
+ if (a === undefined || b === undefined) return false;
24
+ return b > a;
25
+ }
26
+
27
+ /** Pure decision used by the request listener (unit-tested directly). */
28
+ export function decideEffort(
29
+ outcome /* JevOutcome<ChoiceAnswer> */,
30
+ resolved /* frozen LlmCallConfig */,
31
+ target = 'low'
32
+ ) {
33
+ if (!outcome.ok) {
34
+ return { effort: resolved.reasoningEffort, lowered: false, reason: `jev degraded (${outcome.error ?? 'unknown'}) — keeping effort` };
35
+ }
36
+ if (outcome.value.picked !== 'lower') {
37
+ return { effort: resolved.reasoningEffort, lowered: false, reason: 'jev says keep' };
38
+ }
39
+ if (!canLowerEffort(resolved.reasoningEffort, target)) {
40
+ return { effort: resolved.reasoningEffort, lowered: false, reason: `effort ${String(resolved.reasoningEffort)} not lowerable to ${target}` };
41
+ }
42
+ return { effort: target, lowered: true, reason: 'ok' };
43
+ }
44
+
45
+ export const name = 'jev-effort';
46
+ export const inject = [];
47
+
48
+ export function apply(ctx, config = {}) {
49
+ const enabled = config.enabled !== false;
50
+ const target = config.targetEffort ?? 'low';
51
+ const log = config.log ?? (() => {});
52
+ const jev = createJevClient({
53
+ ...(config.endpoint !== undefined ? { endpoint: config.endpoint } : {}),
54
+ ...(config.apiKey !== undefined ? { apiKey: config.apiKey } : {}),
55
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
56
+ ...(config.fetchImpl !== undefined ? { fetchImpl: config.fetchImpl } : {}),
57
+ });
58
+
59
+ /** Newest user text per agent per turn, captured at pre-step. */
60
+ const turnText = new WeakMap();
61
+
62
+ ctx.on('agent/pre-step', async (payload, next) => {
63
+ const decision = await next();
64
+ const perTurn = turnText.get(payload.agent) ?? new Map();
65
+ perTurn.set(payload.turn, latestUserText(payload.messages));
66
+ turnText.set(payload.agent, perTurn);
67
+ return decision;
68
+ });
69
+
70
+ ctx.on('agent/request', async (payload, next) => {
71
+ const resolved = await next();
72
+ if (!enabled) return resolved;
73
+ let outcome;
74
+ try {
75
+ const text = turnText.get(payload.agent)?.get(payload.turn) ?? '';
76
+ outcome = await jev.choice(
77
+ {
78
+ question:
79
+ 'Should this assistant turn run with reduced reasoning effort? ' +
80
+ 'Answer "lower" only for simple turns (short factual Q&A, formatting, chit-chat) ' +
81
+ 'where deep reasoning adds latency without value. Otherwise answer "keep".',
82
+ options: ['keep', 'lower'],
83
+ context: text ? { userTurn: text } : {},
84
+ },
85
+ { pickedIndex: 0, picked: 'keep' } // degraded outcomes keep the original effort
86
+ );
87
+ } catch {
88
+ return resolved; // defensive: JevClient never throws, but never break a request
89
+ }
90
+ const { effort, lowered, reason } = decideEffort(outcome, resolved, target);
91
+ if (lowered) {
92
+ log(`[dsh-jev-effort] turn=${payload.turn} step=${payload.step} ${String(resolved.reasoningEffort)} → ${effort} (jev)`);
93
+ return { ...resolved, reasoningEffort: effort };
94
+ }
95
+ log(`[dsh-jev-effort] turn=${payload.turn} step=${payload.step} keep (${reason})`);
96
+ return resolved;
97
+ });
98
+ }
99
+
100
+ /** Concatenate the newest user message's text blocks, bounded for context. */
101
+ export function latestUserText(messages) {
102
+ for (let i = messages.length - 1; i >= 0; i--) {
103
+ const blocks = messages[i]?.content;
104
+ if (!Array.isArray(blocks)) continue;
105
+ const text = blocks
106
+ .filter((b) => b?.type === 'text' && typeof b.text === 'string')
107
+ .map((b) => b.text)
108
+ .join('\n')
109
+ .trim();
110
+ if (text) return text.slice(0, 4000);
111
+ }
112
+ return '';
113
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@dsh-jev/effort",
3
+ "version": "0.1.0",
4
+ "description": "dsh Web-only plugin: jev System One lowers reasoning effort to low for simple turns, with a dismissible composer hint",
5
+ "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "web", "jev"],
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./cordis.patch.yml": "./cordis.patch.yml",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": ["lib", "cordis.patch.yml", "README.md"],
15
+ "scripts": {
16
+ "test": "node --check lib/index.js && node --check lib/client.js && vitest run",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@dsh-jev/core": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "typescript": "^5.6.0",
25
+ "vitest": "^2.1.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "dsh": {
31
+ "engines": {
32
+ "dsh": ">=0.1.2-rc.1"
33
+ },
34
+ "bundle": {
35
+ "patch": "./cordis.patch.yml"
36
+ },
37
+ "client": {
38
+ "inject": ["locale"],
39
+ "platform": "web"
40
+ }
41
+ },
42
+ "license": "MIT"
43
+ }