@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 +46 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +157 -0
- package/lib/index.js +113 -0
- package/package.json +43 -0
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
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
}
|