@toddzheng024/dscode-bundle 0.7.5 → 0.7.7
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +26 -5
- package/package.json +4 -4
- package/plugins/auto-review/index.mjs +6 -1
- package/plugins/code-review/index.mjs +9 -4
- package/plugins/compaction/tetris.mjs +65 -0
- package/plugins/compaction/threshold.mjs +46 -0
- package/plugins/credentials/index.mjs +2 -2
- package/plugins/dscode/index.mjs +4 -10
- package/plugins/exec/cli.mjs +3 -2
- package/plugins/exec/index.mjs +6 -1
- package/plugins/i18n/messages.mjs +18 -0
- package/plugins/memory/index.mjs +7 -3
- package/plugins/openrouter/adapter.mjs +157 -0
- package/plugins/openrouter/index.mjs +112 -0
- package/plugins/openrouter/models.mjs +151 -0
- package/plugins/openrouter/search.mjs +109 -0
- package/plugins/openrouter/wire.mjs +413 -0
- package/plugins/providers/catalog.mjs +28 -26
- package/plugins/providers/effort.mjs +35 -0
- package/plugins/providers/openrouter-account.mjs +171 -0
- package/plugins/session-cards/index.mjs +5 -1
- package/plugins/session-metrics/balance.mjs +29 -19
- package/plugins/session-metrics/index.mjs +19 -6
- package/plugins/session-metrics/pricing.mjs +45 -14
- package/plugins/session-metrics/view.mjs +1 -1
- package/plugins/tui-tools/doctor.mjs +3 -1
- package/plugins/tui-tools/index.mjs +1 -1
- package/plugins/ultra/policy.mjs +0 -16
- package/presets/dscode/agent.cordis.yml +1 -1
- package/vendor/compaction-basic/index.js +983 -0
- package/vendor/compaction-basic/types/config.d.ts +37 -0
- package/vendor/compaction-basic/types/index.d.ts +84 -0
- package/vendor/compaction-basic/types/region.d.ts +65 -0
- package/vendor/compaction-basic/types/summarizer.d.ts +64 -0
- package/vendor/compaction-basic/types/types.d.ts +73 -0
- package/vendor/deepseek/index.js +1 -1
- package/vendor/subagent/index.js +3 -3
- package/vendor/tui/dscode-providers/catalog.mjs +28 -26
- package/vendor/tui/dscode-providers/effort.mjs +35 -0
- package/vendor/tui/dscode-providers/openrouter-account.mjs +171 -0
- package/vendor/tui/index.mjs +395 -133
- package/vendor/pi-ai/index.js +0 -2702
- package/vendor/pi-ai/types/adapter.d.ts +0 -105
- package/vendor/pi-ai/types/auth.d.ts +0 -60
- package/vendor/pi-ai/types/catalog.d.ts +0 -355
- package/vendor/pi-ai/types/config.d.ts +0 -208
- package/vendor/pi-ai/types/context.d.ts +0 -42
- package/vendor/pi-ai/types/discovery.d.ts +0 -43
- package/vendor/pi-ai/types/index.d.ts +0 -69
- package/vendor/pi-ai/types/login.d.ts +0 -21
- package/vendor/pi-ai/types/provider.d.ts +0 -59
- package/vendor/pi-ai/types/replay.d.ts +0 -63
- package/vendor/pi-ai/types/stream.d.ts +0 -43
- /package/vendor/{pi-ai → compaction-basic}/LICENSE +0 -0
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -19,9 +19,6 @@ Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
|
19
19
|
@deepseek-ai/dsh-llm-deepseek@0.1.5-rc.1: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/llm/llm-deepseek"}
|
|
20
20
|
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
21
21
|
|
|
22
|
-
@deepseek-ai/dsh-llm-pi-ai@0.1.5-rc.1: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/llm/llm-pi-ai"}
|
|
23
|
-
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
24
|
-
|
|
25
22
|
@deepseek-ai/dsh-tool-bash@0.1.5-rc.1: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/shell/tool-bash"}
|
|
26
23
|
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
27
24
|
|
|
@@ -30,3 +27,6 @@ Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
|
30
27
|
|
|
31
28
|
@deepseek-ai/dsh-terminal-bash@0.1.5-rc.1: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/terminal/terminal-bash"}
|
|
32
29
|
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
30
|
+
|
|
31
|
+
@deepseek-ai/dsh-compaction-basic@0.1.5-rc.1: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/compaction/compaction-basic"}
|
|
32
|
+
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
package/cordis.patch.yml
CHANGED
|
@@ -662,6 +662,29 @@
|
|
|
662
662
|
config:
|
|
663
663
|
policy: ask
|
|
664
664
|
|
|
665
|
+
# OpenRouter is DSCODE's own adapter (plugins/openrouter); the pi-ai route that
|
|
666
|
+
# served it before stays unmounted so the two never claim the same route.
|
|
667
|
+
- id: llm-pi-ai
|
|
668
|
+
disabled: true
|
|
669
|
+
|
|
670
|
+
# Web search follows the session's route: OpenRouter's web plugin for an
|
|
671
|
+
# OpenRouter session, DeepSeek's native search for a DeepSeek one. A patch
|
|
672
|
+
# replaces the row's whole config, so fetch keeps its provider here.
|
|
673
|
+
- id: web
|
|
674
|
+
config:
|
|
675
|
+
searchProvider: dscode-web
|
|
676
|
+
fetchProvider: http
|
|
677
|
+
|
|
678
|
+
# A reasoning model spends tokens before its title; 64 left most titles empty.
|
|
679
|
+
# DeepSeek's route disables thinking for titles, so its usage stays small.
|
|
680
|
+
- id: session-title-llm
|
|
681
|
+
config:
|
|
682
|
+
targetWords: 5
|
|
683
|
+
targetCjkCharacters: 10
|
|
684
|
+
maxInputBytes: 4096
|
|
685
|
+
maxOutputTokens: 1024
|
|
686
|
+
timeoutMs: 60000
|
|
687
|
+
|
|
665
688
|
- id: computer-use
|
|
666
689
|
config:
|
|
667
690
|
observationTtlMs: 30000
|
|
@@ -702,11 +725,13 @@
|
|
|
702
725
|
- id: credentials
|
|
703
726
|
name: "@toddzheng024/dscode-bundle/credentials"
|
|
704
727
|
- insert:
|
|
728
|
+
- id: dscode-openrouter
|
|
729
|
+
name: "@toddzheng024/dscode-bundle/openrouter"
|
|
705
730
|
- id: dscode-auto-review
|
|
706
731
|
name: "@toddzheng024/dscode-bundle/auto-review"
|
|
707
732
|
config:
|
|
708
733
|
timeoutMs: 30000
|
|
709
|
-
maxOutputTokens:
|
|
734
|
+
maxOutputTokens: 4096
|
|
710
735
|
maxReviewsPerTurn: 20
|
|
711
736
|
- id: dscode-session-metrics
|
|
712
737
|
name: "@toddzheng024/dscode-bundle/session-metrics"
|
|
@@ -738,12 +763,8 @@
|
|
|
738
763
|
|
|
739
764
|
- id: llm-deepseek
|
|
740
765
|
disabled: true
|
|
741
|
-
- id: llm-pi-ai
|
|
742
|
-
disabled: true
|
|
743
766
|
- insert:
|
|
744
767
|
- id: dscode-deepseek
|
|
745
768
|
name: '@toddzheng024/dscode-bundle/deepseek'
|
|
746
|
-
- id: dscode-pi-ai
|
|
747
|
-
name: '@toddzheng024/dscode-bundle/pi-ai'
|
|
748
769
|
- id: dscode-bootstrap
|
|
749
770
|
name: '@toddzheng024/dscode-bundle/bootstrap'
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.7",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Todd Zheng",
|
|
@@ -44,14 +44,15 @@
|
|
|
44
44
|
"./tui": "./vendor/tui/index.mjs",
|
|
45
45
|
"./startup": "./vendor/tui/startup.mjs",
|
|
46
46
|
"./deepseek": "./vendor/deepseek/index.js",
|
|
47
|
-
"./pi-ai": "./vendor/pi-ai/index.js",
|
|
48
47
|
"./bash": "./vendor/bash/index.js",
|
|
49
48
|
"./persistent": "./vendor/persistent/index.js",
|
|
50
49
|
"./terminal": "./vendor/terminal/index.js",
|
|
50
|
+
"./compaction-basic": "./vendor/compaction-basic/index.js",
|
|
51
51
|
"./policy": "./plugins/dscode/index.mjs",
|
|
52
52
|
"./code-review": "./plugins/code-review/index.mjs",
|
|
53
53
|
"./auto-review": "./plugins/auto-review/index.mjs",
|
|
54
54
|
"./session-metrics": "./plugins/session-metrics/index.mjs",
|
|
55
|
+
"./openrouter": "./plugins/openrouter/index.mjs",
|
|
55
56
|
"./tui-tools": "./plugins/tui-tools/index.mjs"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
@@ -305,8 +306,7 @@
|
|
|
305
306
|
"nodemailer": "10.0.9",
|
|
306
307
|
"react": "18.3.1",
|
|
307
308
|
"commander": "15.0.0",
|
|
308
|
-
"eventsource-parser": "3.1.1"
|
|
309
|
-
"@earendil-works/pi-ai": "0.85.1"
|
|
309
|
+
"eventsource-parser": "3.1.1"
|
|
310
310
|
},
|
|
311
311
|
"dsh": {
|
|
312
312
|
"bundle": {
|
|
@@ -3,13 +3,15 @@ import { BlockAssembler, createSystemMessage, createUserMessage } from '@deepsee
|
|
|
3
3
|
import { REVIEW_POLICY, escalationDiagnosticGrant, needsMcpApproval, redact, fingerprint, contextFor, parseDecision } from './policy.mjs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { auditStore } from './audit.mjs';
|
|
6
|
+
import { effortFor } from '../providers/effort.mjs';
|
|
6
7
|
|
|
7
8
|
export const name = 'dscode-auto-review';
|
|
8
9
|
export const inject = ['approval', 'permissionPresets', 'tools', 'llm', 'commands'];
|
|
9
10
|
export const Config = z.object({
|
|
10
11
|
provider: z.string().default(''), model: z.string().default(''),
|
|
11
12
|
timeoutMs: z.number().min(100).max(120000).default(30000),
|
|
12
|
-
|
|
13
|
+
// Reasoning tokens count against the cap: a reasoning model needs room before its verdict.
|
|
14
|
+
maxOutputTokens: z.number().step(1).min(128).max(16384).default(4096),
|
|
13
15
|
maxReviewsPerTurn: z.number().step(1).min(1).max(100).default(20),
|
|
14
16
|
maxEscalationGrantsPerTurn: z.number().step(1).min(0).max(10).default(2),
|
|
15
17
|
auditDirectory: z.string(),
|
|
@@ -105,8 +107,11 @@ export function apply(ctx, config) {
|
|
|
105
107
|
// file reads, or main-agent reasoning are added to the reviewer context.
|
|
106
108
|
const operation = (async () => {
|
|
107
109
|
let terminal = false;
|
|
110
|
+
// A verdict needs little deliberation: the lowest level near low the reviewer model offers.
|
|
111
|
+
const effort = await effortFor(ctx.llm, target, 'low', signal);
|
|
108
112
|
for await (const chunk of ctx.llm.stream({
|
|
109
113
|
provider: target.provider, model: target.model, sessionId: req.agent.session.id,
|
|
114
|
+
...(effort === undefined ? {} : { reasoningEffort: effort }),
|
|
110
115
|
messages: [createSystemMessage(REVIEW_POLICY, name), createUserMessage({
|
|
111
116
|
content: [{ type: 'text', text: JSON.stringify({ action, context }) }],
|
|
112
117
|
source: { kind: 'plugin', plugin: name },
|
|
@@ -5,6 +5,7 @@ import { collectReviewDiff, parseReviewCommand, isGitAvailableSync, isGitWorkspa
|
|
|
5
5
|
import { baselineStore } from './baseline.mjs';
|
|
6
6
|
import { redact } from '../auto-review/policy.mjs';
|
|
7
7
|
import { chargeTo } from '../session-metrics/attribution.mjs';
|
|
8
|
+
import { effortFor } from '../providers/effort.mjs';
|
|
8
9
|
|
|
9
10
|
export const name = 'dscode-code-review';
|
|
10
11
|
export const inject = ['tools', 'commands', 'llm', 'systemPrompt'];
|
|
@@ -47,8 +48,11 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
47
48
|
const diffHash = createHash('sha256').update(JSON.stringify({ diff, task, label, model: route.model })).digest('hex').slice(0, 16);
|
|
48
49
|
const prior = results.get(agent);
|
|
49
50
|
if (prior?.diffHash === diffHash) return { ...prior.result, cached: true };
|
|
50
|
-
|
|
51
|
+
// A reasoning reviewer writing a long report needs minutes, not seconds; the caller's signal still cancels at once.
|
|
52
|
+
const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(10 * 60 * 1000)]);
|
|
51
53
|
const request = { task, scope: label, diff: redact(diff) };
|
|
54
|
+
// Ultra is the session's collaboration mode, not a reviewer level: review at high, or the nearest level the model offers.
|
|
55
|
+
const reasoningEffort = route.reasoningEffort === 'ultra' ? await effortFor(ctx.llm, route, 'high', deadline) : route.reasoningEffort;
|
|
52
56
|
// One model attempt: returns the assembler plus whether the stream delivered a finish chunk.
|
|
53
57
|
// Charged to this session's ledger; the request itself carries no sessionId.
|
|
54
58
|
const attempt = () => chargeTo(agent.session.id, 'review', async () => {
|
|
@@ -56,8 +60,9 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
56
60
|
const operation = (async () => {
|
|
57
61
|
let finished = false;
|
|
58
62
|
for await (const chunk of ctx.llm.stream({
|
|
59
|
-
provider: route.provider, model: route.model, reasoningEffort
|
|
60
|
-
|
|
63
|
+
provider: route.provider, model: route.model, reasoningEffort, purpose: 'review',
|
|
64
|
+
// No output cap of our own: the model's route default applies (256k on DeepSeek, the catalog limit on OpenRouter).
|
|
65
|
+
system: POLICY, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(request) }], source: { kind: 'plugin', plugin: name } })], signal: deadline,
|
|
61
66
|
})) {
|
|
62
67
|
deadline.throwIfAborted();
|
|
63
68
|
assembler.push(chunk);
|
|
@@ -92,7 +97,7 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
92
97
|
if (blocks.some(block => !['text', 'reasoning'].includes(block.type))) throw Error('Code reviewer returned unexpected output.');
|
|
93
98
|
const report = blocks.filter(block => block.type === 'text').map(block => block.text).join('').trim();
|
|
94
99
|
if (!report) throw Error(truncated ? 'Code reviewer ran out of output tokens before writing the report; narrow the diff with path and retry.' : 'Code reviewer returned an empty report.');
|
|
95
|
-
const result = { status: omitted.length || truncated ? 'partial' : 'reviewed', scope: label, report: `${redact(report).slice(0,
|
|
100
|
+
const result = { status: omitted.length || truncated ? 'partial' : 'reviewed', scope: label, report: `${redact(report).slice(0, 64000)}${omitted.length ? `\n\nReview incomplete: ${omitted.length} file(s) were omitted or binary and could not be inspected from the diff.` : ''}${truncated ? '\n\nReview incomplete: the reviewer hit its output limit; later findings may be missing. Narrow the diff with path for a complete pass.' : ''}`, diffHash, usage: assembler.usage ?? null };
|
|
96
101
|
results.set(agent, { diffHash, result });
|
|
97
102
|
return result;
|
|
98
103
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The compaction indicator: a small scripted Tetris bot. It lines each piece up
|
|
2
|
+
// over its slot, drops it and clears the full rows, the way compaction clears
|
|
3
|
+
// older history. The script tiles the well, so every run ends empty and loops.
|
|
4
|
+
// Cells: '.' empty, '@' falling piece, '#' settled, '=' a row being cleared.
|
|
5
|
+
export const TETRIS_WIDTH = 8;
|
|
6
|
+
export const TETRIS_HEIGHT = 4;
|
|
7
|
+
export const TETRIS_TICK_MS = 160;
|
|
8
|
+
|
|
9
|
+
const EMPTY_ROW = '.'.repeat(TETRIS_WIDTH);
|
|
10
|
+
// [row, column] cells inside each piece's box, row 0 on top.
|
|
11
|
+
const O = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
12
|
+
const I = [[0, 0], [0, 1], [0, 2], [0, 3]];
|
|
13
|
+
const J = [[0, 0], [1, 0], [1, 1], [1, 2]];
|
|
14
|
+
const HOOK = [[0, 0], [0, 1], [0, 2], [1, 2]];
|
|
15
|
+
// Two rows cleared at once, then a single row whose leftovers fall and clear next.
|
|
16
|
+
const SCRIPT = [
|
|
17
|
+
{ cells: O, x: 0 }, { cells: J, x: 2 }, { cells: HOOK, x: 3 }, { cells: O, x: 6 },
|
|
18
|
+
{ cells: I, x: 0 }, { cells: O, x: 4 }, { cells: O, x: 6 }, { cells: I, x: 0 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function fits(board, cells, x, y) {
|
|
22
|
+
return cells.every(([row, column]) => y + row < TETRIS_HEIGHT && x + column >= 0 && x + column < TETRIS_WIDTH && board[y + row][x + column] === '.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function paint(board, cells, x, y, mark) {
|
|
26
|
+
const rows = board.map(row => [...row]);
|
|
27
|
+
for (const [row, column] of cells) rows[y + row][x + column] = mark;
|
|
28
|
+
return rows.map(row => row.join(''));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Every frame of one loop, simulated from the script; throws when the script stops tiling the well. */
|
|
32
|
+
export function tetrisFrames() {
|
|
33
|
+
let board = Array.from({ length: TETRIS_HEIGHT }, () => EMPTY_ROW);
|
|
34
|
+
const frames = [];
|
|
35
|
+
for (const { cells, x: target } of SCRIPT) {
|
|
36
|
+
const width = Math.max(...cells.map(([, column]) => column)) + 1;
|
|
37
|
+
let x = Math.floor((TETRIS_WIDTH - width) / 2), y = 0;
|
|
38
|
+
if (!fits(board, cells, x, y)) throw Error('Tetris script spawns into the stack');
|
|
39
|
+
frames.push(paint(board, cells, x, y, '@'));
|
|
40
|
+
while (x !== target) {
|
|
41
|
+
x += Math.sign(target - x);
|
|
42
|
+
if (!fits(board, cells, x, y)) throw Error('Tetris script steers into the stack');
|
|
43
|
+
frames.push(paint(board, cells, x, y, '@'));
|
|
44
|
+
}
|
|
45
|
+
while (fits(board, cells, x, y + 1)) frames.push(paint(board, cells, x, ++y, '@'));
|
|
46
|
+
board = paint(board, cells, x, y, '#');
|
|
47
|
+
const kept = board.filter(row => row.includes('.'));
|
|
48
|
+
if (kept.length < TETRIS_HEIGHT) {
|
|
49
|
+
const flash = board.map(row => row.includes('.') ? row : '='.repeat(TETRIS_WIDTH));
|
|
50
|
+
frames.push(flash, flash);
|
|
51
|
+
board = [...Array.from({ length: TETRIS_HEIGHT - kept.length }, () => EMPTY_ROW), ...kept];
|
|
52
|
+
}
|
|
53
|
+
frames.push(board);
|
|
54
|
+
}
|
|
55
|
+
if (board.some(row => row !== EMPTY_ROW)) throw Error('Tetris script does not clear the well');
|
|
56
|
+
return frames;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FRAMES = tetrisFrames();
|
|
60
|
+
|
|
61
|
+
/** The board rows for an animation tick; the loop wraps. */
|
|
62
|
+
export function tetrisFrame(tick) {
|
|
63
|
+
const index = Math.floor(tick) % FRAMES.length;
|
|
64
|
+
return FRAMES[index < 0 ? index + FRAMES.length : index];
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cacheReadRatio } from '../session-metrics/pricing.mjs';
|
|
2
|
+
import { ensureOpenRouterModels } from '../openrouter/models.mjs';
|
|
3
|
+
|
|
4
|
+
// Auto-compaction threshold priced from the route's cache discount. Staying put
|
|
5
|
+
// re-reads the whole context at the cache-read price; compacting pays for a
|
|
6
|
+
// summary call and re-reads the kept tail at the full input price once. With a
|
|
7
|
+
// deep discount (r = cache read / input price) a long context is cheap to carry,
|
|
8
|
+
// so compaction waits; with no discount it pays off early.
|
|
9
|
+
export const DEFAULT_THRESHOLD_RATIO = 0.8;
|
|
10
|
+
|
|
11
|
+
/** Threshold ratio for a cache-read / input price ratio; unknown prices keep the upstream default. */
|
|
12
|
+
export function thresholdForCacheRatio(ratio) {
|
|
13
|
+
if (!Number.isFinite(ratio) || ratio < 0) return DEFAULT_THRESHOLD_RATIO;
|
|
14
|
+
// Prices are decimal fractions: 0.3 / 3 must land on the 0.1 boundary, not just below it.
|
|
15
|
+
const rounded = Math.round(ratio * 1e9) / 1e9;
|
|
16
|
+
return rounded < 0.1 ? 0.9 : rounded < 0.5 ? 0.8 : 0.6;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The route's threshold ratio, waiting for the OpenRouter listing when it has not loaded yet. */
|
|
20
|
+
export async function pricedThresholdRatio(provider, model, now = Date.now()) {
|
|
21
|
+
if (provider === 'openrouter') await ensureOpenRouterModels({ home: process.env.DSH_HOME, now });
|
|
22
|
+
return thresholdForCacheRatio(cacheReadRatio(provider, model, now));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A compaction-basic route policy with the priced threshold. A threshold the
|
|
27
|
+
* deployment configured, globally or for this route, is left as it is.
|
|
28
|
+
* @param config - resolved compaction-basic config; `dscodePricedThreshold` marks an unset top-level threshold.
|
|
29
|
+
* @param policy - that route's merged policy.
|
|
30
|
+
*/
|
|
31
|
+
export async function pricedCompactionPolicy(config, policy, now = Date.now()) {
|
|
32
|
+
const { provider, model } = policy.target;
|
|
33
|
+
const override = config.modelPolicies?.find(entry => entry.provider === provider && entry.model === model);
|
|
34
|
+
if (config.dscodePricedThreshold !== true || override?.thresholdRatio !== undefined) return policy;
|
|
35
|
+
const thresholdRatio = await pricedThresholdRatio(provider, model, now);
|
|
36
|
+
// The retained tail must stay below the threshold, so a configured tail that large keeps the configured policy.
|
|
37
|
+
if (policy.retainRatio !== undefined && policy.retainRatio >= thresholdRatio) return policy;
|
|
38
|
+
return { ...policy, thresholdRatio };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether the session's current context would compact on a route's next step, or undefined when either size is unknown. */
|
|
42
|
+
export function compactionPreview({ used, contextWindow, thresholdRatio, label = '' }) {
|
|
43
|
+
if (!Number.isFinite(used) || used <= 0 || !Number.isInteger(contextWindow) || contextWindow <= 0) return undefined;
|
|
44
|
+
const threshold = Math.floor(contextWindow * thresholdRatio);
|
|
45
|
+
return { label, used, contextWindow, thresholdRatio, threshold, compacts: used >= threshold, overflows: used >= contextWindow };
|
|
46
|
+
}
|
|
@@ -5,8 +5,8 @@ import { Context, Service } from '@deepseek-ai/cordis';
|
|
|
5
5
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
6
6
|
import { PROVIDERS } from '../providers/catalog.mjs';
|
|
7
7
|
|
|
8
|
-
// The `/provider` keys (DeepSeek, OpenRouter) live in the shared store.
|
|
9
|
-
const SHARED = new Set(PROVIDERS.
|
|
8
|
+
// The `/provider` keys (DeepSeek, OpenRouter and its management key) live in the shared store.
|
|
9
|
+
const SHARED = new Set(PROVIDERS.flatMap(provider => [provider.credentialRef, provider.managementRef].filter(Boolean)));
|
|
10
10
|
|
|
11
11
|
// Use the native locked, watched, owner-only store. Keep this independent of
|
|
12
12
|
// the installed profile so upgrades and different projects share credentials.
|
package/plugins/dscode/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ export const inject = ['systemPrompt', 'tools', 'agents', 'commands', 'terminals
|
|
|
3
3
|
export const SHELL_POLICY = `Use bash as the persistent shell for reading, searching and modifying files. Prefer rg/rg --files, sed and standard CLI tools. Use apply_patch with a standard unified diff on stdin (git apply format, a/ and b/ paths); apply_patch --check validates before writing. It is not the *** Begin Patch format. Quote heredoc delimiters to avoid shell interpolation.
|
|
4
4
|
Each agent has its own persistent shell, initially in the session workspace. cd, exported variables, functions and background jobs persist only while this shell lives. Timeout, cancellation, exit, /shell reset and process restart discard shell state; resume restores conversation, not an OS process. Never assume an environment from a past session still exists. Inspect pwd when paths matter. Keep long-running processes controlled and clean them up when done.
|
|
5
5
|
Normal bash remains confined by the active sandbox. After a genuine sandbox denial, shell_retry provides a fresh, one-shot shell with the existing approval/escalation mechanism. Set an explicit absolute workdir and reconstruct needed non-secret setup; it does not inherit the persistent shell's cd, exports, functions or jobs. Use existing credential-aware CLIs; never paste secrets into arguments. Approval rejection is final for that action; do not work around it.
|
|
6
|
-
Delegation to child agents (subagent, subagent_fork)
|
|
6
|
+
Delegation to child agents (subagent, subagent_fork) is available at every effort. Below ultra, delegation is the exception: do the work in this agent by default. Delegate only a substantial, independent part of the task whose parallel work clearly shortens completion, or a broad read-only investigation that would otherwise crowd this context; never delegate a bounded edit, a single-file change, a quick lookup, one test run or a routine review. Below ultra run at most one child at a time, give it a bounded objective, choose the lowest reasoning_effort the model offers that fits, and verify and integrate its result yourself. Ultra adds its own delegation guidance to the request; the preset allows one delegation level and the runtime caps a parent at three concurrently running children.`;
|
|
7
7
|
|
|
8
8
|
/** Child names: 1-10 characters, letters/digits/underscores, starting and ending with a letter. */
|
|
9
9
|
export const CHILD_NAME = /^[A-Za-z](?:[A-Za-z0-9_]{0,8}[A-Za-z])?$/;
|
|
@@ -46,19 +46,13 @@ export function apply(ctx) {
|
|
|
46
46
|
if (exec.name === 'send_message' || exec.name === 'interrupt_agent') exec.arguments.agent_id = resolveAgentPath(owner, exec.arguments.agent_id);
|
|
47
47
|
if (exec.name === 'interrupt_agent') return next();
|
|
48
48
|
if (owner.session.header.origin === 'subagent' && DELEGATION_TOOLS.includes(exec.name)) throw new Error('Child agents cannot delegate again. Complete the assigned work and report to the parent.');
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
if (DELEGATION_TOOLS.includes(exec.name)) throw new Error('Child-agent work requires Ultra. Select /effort ultra before delegating.');
|
|
52
|
-
const target = ctx.agents.get(exec.arguments.agent_id);
|
|
53
|
-
if (target?.status !== 'running' && target?.session.header.parentSession === owner.session.id) throw new Error('Waking a child agent requires Ultra. Select /effort ultra first.');
|
|
54
|
-
return next();
|
|
55
|
-
}
|
|
56
|
-
if (['workflow', 'ralph'].includes(exec.name)) throw new Error('Use capped subagent/subagent_fork delegation in Ultra; workflow and ralph are unavailable in dscode.');
|
|
49
|
+
// Delegation is open at every effort: the shell policy keeps it rare below Ultra, and the cap below applies throughout.
|
|
50
|
+
if (['workflow', 'ralph'].includes(exec.name)) throw new Error('Use capped subagent/subagent_fork delegation; workflow and ralph are unavailable in dscode.');
|
|
57
51
|
const target = exec.name === 'send_message' ? ctx.agents.get(exec.arguments.agent_id) : undefined;
|
|
58
52
|
if (exec.name === 'send_message' && (exec.arguments.agent_id === owner.session.header.parentSession || target?.status === 'running')) return next();
|
|
59
53
|
const id = owner.session.id;
|
|
60
54
|
const running = ctx.agents.list().filter(a => a.session.header.origin === 'subagent' && a.session.header.parentSession === id && a.status === 'running').length;
|
|
61
|
-
if (running + (reservations.get(id) ?? 0) >= 3) throw new Error('
|
|
55
|
+
if (running + (reservations.get(id) ?? 0) >= 3) throw new Error('Concurrent child limit reached (3). Wait for a child to settle, then delegate or send more work.');
|
|
62
56
|
const childName = exec.name === 'send_message' ? undefined : exec.arguments.name;
|
|
63
57
|
if (childName !== undefined) {
|
|
64
58
|
if (typeof childName !== 'string' || !CHILD_NAME.test(childName)) throw new Error(CHILD_NAME_RULE);
|
package/plugins/exec/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ The prompt is read from stdin when omitted or given as "-".
|
|
|
8
8
|
Options:
|
|
9
9
|
--cwd DIR workspace for the agent (default: current directory)
|
|
10
10
|
--model PROVIDER/ID model route (default: the saved default model)
|
|
11
|
-
--effort LEVEL reasoning effort: low, high, max or ultra
|
|
11
|
+
--effort LEVEL reasoning effort the model offers: off, minimal, low, medium, high, xhigh, max or ultra
|
|
12
12
|
--permission PRESET permission preset: auto, ask, workspace-write, read-only, danger-full-access
|
|
13
13
|
--approve-all answer every approval request with allow (no human is present)
|
|
14
14
|
--resume SESSION_ID continue an existing session instead of starting a new one
|
|
@@ -44,7 +44,8 @@ export function parseExecArgs(argv) {
|
|
|
44
44
|
words.push(arg);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// Only the level name is checked here; the Host checks it against the model. This file ships alone in the launcher, so the names are inlined.
|
|
48
|
+
if (options.effort !== undefined && !['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(options.effort)) throw new Error('--effort expects off, minimal, low, medium, high, xhigh, max or ultra');
|
|
48
49
|
if (options.model !== undefined && !/^[^/]+\/.+$/.test(options.model)) throw new Error('--model expects provider/model');
|
|
49
50
|
options.prompt = words.join(' ');
|
|
50
51
|
return options;
|
package/plugins/exec/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
7
7
|
|
|
8
8
|
export const name = 'dscode-exec';
|
|
9
|
-
export const inject = ['agents', 'agentPresets', 'agentDefaultModel', 'permissionPresets'];
|
|
9
|
+
export const inject = ['agents', 'agentPresets', 'agentDefaultModel', 'permissionPresets', 'llm'];
|
|
10
10
|
|
|
11
11
|
export function apply(ctx) {
|
|
12
12
|
void run(ctx).catch(error => { process.stderr.write(`dscode exec: ${error.message}\n`); ctx.get('appExit')(1); });
|
|
@@ -39,6 +39,11 @@ async function run(ctx) {
|
|
|
39
39
|
const selection = ctx.agentDefaultModel.currentSelection();
|
|
40
40
|
const [provider, model] = options.model ? splitRoute(options.model) : [selection.provider, selection.model];
|
|
41
41
|
const effort = options.effort ?? selection.reasoningEffort;
|
|
42
|
+
// Levels belong to the model: refuse one it does not offer before the turn, not at its first request.
|
|
43
|
+
if (options.effort) {
|
|
44
|
+
const offered = (await ctx.llm.resolveModelInfo(provider, model)).reasoning?.efforts.map(level => level.id) ?? [];
|
|
45
|
+
if (!offered.includes(options.effort)) throw new Error(`--effort ${options.effort} is not offered by ${provider}/${model}${offered.length ? `; it offers ${offered.join(', ')}` : ', which has no reasoning levels'}`);
|
|
46
|
+
}
|
|
42
47
|
const agentOptions = { provider, model, ...(effort ? { reasoningEffort: effort } : {}) };
|
|
43
48
|
const setup = async agentCtx => { await ctx.agentPresets.mount(agentCtx, 'dscode'); };
|
|
44
49
|
const handle = options.resume
|
|
@@ -37,6 +37,9 @@ export const MESSAGES = {
|
|
|
37
37
|
'language.saveFailed': 'language save failed: {error}',
|
|
38
38
|
'doctor.logs.new': 'Only warnings/errors since this TUI version started are recorded.',
|
|
39
39
|
'footer.current': 'current', 'footer.average': 'average', 'footer.context': 'context', 'footer.cache': 'cache hit', 'composer.placeholder': 'type a message',
|
|
40
|
+
'compaction.running': 'Compacting context, please wait', 'compaction.confirm.title': 'Switching to {model} will compact the conversation',
|
|
41
|
+
'compaction.confirm.usage': 'context {used} ≥ compaction threshold {threshold} ({window} × {ratio})', 'compaction.confirm.next': 'the next step compacts older history first',
|
|
42
|
+
'compaction.confirm.overflow': 'it already exceeds the {window} window: the next request compacts and retries', 'compaction.confirm.hint': 'y switch · n/esc back',
|
|
40
43
|
'doctor.nearTimeout': 'Assessment: {count} Bash calls ended at about 300 seconds, which matches the tool timeout; traces alone cannot prove the root cause. Next, check those calls\' shell completion markers and terminal errors.',
|
|
41
44
|
'doctor.logs.none': 'No log file yet; earlier console logs were not persisted and cannot be recovered.',
|
|
42
45
|
'doctor.evidence': 'Diagnostic evidence: {traces} recent sessions, {logs} warnings/errors.',
|
|
@@ -58,6 +61,9 @@ export const MESSAGES = {
|
|
|
58
61
|
'language.saveFailed': '语言设置保存失败:{error}',
|
|
59
62
|
'doctor.logs.new': '仅记录新版 TUI 启动后的 warning/error。',
|
|
60
63
|
'footer.current': '当前', 'footer.average': '平均', 'footer.context': '上下文', 'footer.cache': '缓存命中', 'composer.placeholder': '输入消息',
|
|
64
|
+
'compaction.running': '正在压缩上下文,请稍后', 'compaction.confirm.title': '切换到 {model} 会触发自动压缩',
|
|
65
|
+
'compaction.confirm.usage': '当前上下文 {used} ≥ 压缩阈值 {threshold}({window} × {ratio})', 'compaction.confirm.next': '下一步开始前会先压缩较早的对话',
|
|
66
|
+
'compaction.confirm.overflow': '已超出新模型的 {window} 上下文窗口:下一次请求会先压缩再重试', 'compaction.confirm.hint': 'y 确认切换 · n/esc 返回',
|
|
61
67
|
'doctor.nearTimeout': '判断:{count} 次 Bash 调用在约 300 秒结束,符合工具超时特征;trace 不能单独证明触发超时的根因。下一步检查这些调用的 shell 完成标记和终端错误。',
|
|
62
68
|
'doctor.logs.none': '日志文件尚未建立;旧版控制台日志未持久化,无法回溯。',
|
|
63
69
|
'doctor.evidence': '诊断证据:{traces} 个近期会话、{logs} 条 warning/error。',
|
|
@@ -79,6 +85,9 @@ export const MESSAGES = {
|
|
|
79
85
|
'language.saveFailed': '語言設定儲存失敗:{error}',
|
|
80
86
|
'doctor.logs.new': '僅記錄新版 TUI 啟動後的 warning/error。',
|
|
81
87
|
'footer.current': '目前', 'footer.average': '平均', 'footer.context': '上下文', 'footer.cache': '快取命中', 'composer.placeholder': '輸入訊息',
|
|
88
|
+
'compaction.running': '正在壓縮上下文,請稍候', 'compaction.confirm.title': '切換到 {model} 會觸發自動壓縮',
|
|
89
|
+
'compaction.confirm.usage': '目前上下文 {used} ≥ 壓縮閾值 {threshold}({window} × {ratio})', 'compaction.confirm.next': '下一步開始前會先壓縮較早的對話',
|
|
90
|
+
'compaction.confirm.overflow': '已超出新模型的 {window} 上下文視窗:下一次請求會先壓縮再重試', 'compaction.confirm.hint': 'y 確認切換 · n/esc 返回',
|
|
82
91
|
'doctor.nearTimeout': '判斷:{count} 次 Bash 呼叫在約 300 秒結束,符合工具逾時特徵;trace 無法單獨證明觸發逾時的根因。下一步檢查這些呼叫的 shell 完成標記和終端錯誤。',
|
|
83
92
|
'doctor.logs.none': '日誌檔尚未建立;舊版主控台日誌未持久化,無法回溯。',
|
|
84
93
|
'doctor.evidence': '診斷證據:{traces} 個近期工作階段、{logs} 筆 warning/error。',
|
|
@@ -100,6 +109,9 @@ export const MESSAGES = {
|
|
|
100
109
|
'language.saveFailed': '言語設定の保存に失敗しました:{error}',
|
|
101
110
|
'doctor.logs.new': 'この TUI バージョンの起動以降の warning/error のみ記録されています。',
|
|
102
111
|
'footer.current': '現在', 'footer.average': '平均', 'footer.context': 'コンテキスト', 'footer.cache': 'キャッシュヒット', 'composer.placeholder': 'メッセージを入力',
|
|
112
|
+
'compaction.running': 'コンテキストを圧縮しています。しばらくお待ちください', 'compaction.confirm.title': '{model} に切り替えると自動圧縮が実行されます',
|
|
113
|
+
'compaction.confirm.usage': '現在のコンテキスト {used} ≥ 圧縮しきい値 {threshold}({window} × {ratio})', 'compaction.confirm.next': '次のステップの前に古い履歴を圧縮します',
|
|
114
|
+
'compaction.confirm.overflow': '新しいモデルの {window} ウィンドウを超えています。次のリクエストで圧縮して再試行します', 'compaction.confirm.hint': 'y 切り替え · n/esc 戻る',
|
|
103
115
|
'doctor.nearTimeout': '判断:Bash 呼び出し {count} 件が約 300 秒で終了しており、ツールのタイムアウトの特徴に一致します。トレースだけでは根本原因を証明できません。次はこれらの呼び出しのシェル完了マーカーと端末エラーを確認してください。',
|
|
104
116
|
'doctor.logs.none': 'ログファイルはまだありません。以前のコンソールログは保存されておらず、遡れません。',
|
|
105
117
|
'doctor.evidence': '診断の根拠:直近のセッション {traces} 件、warning/error {logs} 件。',
|
|
@@ -121,6 +133,9 @@ export const MESSAGES = {
|
|
|
121
133
|
'language.saveFailed': '언어 설정 저장 실패: {error}',
|
|
122
134
|
'doctor.logs.new': '이 TUI 버전 시작 이후의 warning/error만 기록됩니다.',
|
|
123
135
|
'footer.current': '현재', 'footer.average': '평균', 'footer.context': '컨텍스트', 'footer.cache': '캐시 적중', 'composer.placeholder': '메시지를 입력하세요',
|
|
136
|
+
'compaction.running': '컨텍스트를 압축하는 중입니다. 잠시만 기다려 주세요', 'compaction.confirm.title': '{model}(으)로 전환하면 자동 압축이 실행됩니다',
|
|
137
|
+
'compaction.confirm.usage': '현재 컨텍스트 {used} ≥ 압축 임계값 {threshold} ({window} × {ratio})', 'compaction.confirm.next': '다음 단계 전에 이전 대화를 먼저 압축합니다',
|
|
138
|
+
'compaction.confirm.overflow': '새 모델의 {window} 창을 이미 넘었습니다. 다음 요청에서 압축한 뒤 재시도합니다', 'compaction.confirm.hint': 'y 전환 · n/esc 뒤로',
|
|
124
139
|
'doctor.nearTimeout': '판단: Bash 호출 {count}건이 약 300초에 종료되어 도구 시간 초과 특징과 일치합니다. 트레이스만으로는 근본 원인을 증명할 수 없습니다. 다음으로 해당 호출의 셸 완료 표시와 터미널 오류를 확인하세요.',
|
|
125
140
|
'doctor.logs.none': '아직 로그 파일이 없습니다. 이전 콘솔 로그는 저장되지 않아 복구할 수 없습니다.',
|
|
126
141
|
'doctor.evidence': '진단 근거: 최근 세션 {traces}개, warning/error {logs}건.',
|
|
@@ -142,6 +157,9 @@ export const MESSAGES = {
|
|
|
142
157
|
'language.saveFailed': 'no se pudo guardar el idioma: {error}',
|
|
143
158
|
'doctor.logs.new': 'Solo se registran los warnings/errores desde que arrancó esta versión del TUI.',
|
|
144
159
|
'footer.current': 'actual', 'footer.average': 'promedio', 'footer.context': 'contexto', 'footer.cache': 'éxitos de caché', 'composer.placeholder': 'escribe un mensaje',
|
|
160
|
+
'compaction.running': 'Compactando el contexto, espera un momento', 'compaction.confirm.title': 'Cambiar a {model} compactará la conversación',
|
|
161
|
+
'compaction.confirm.usage': 'contexto {used} ≥ umbral de compactación {threshold} ({window} × {ratio})', 'compaction.confirm.next': 'el siguiente paso compacta antes el historial antiguo',
|
|
162
|
+
'compaction.confirm.overflow': 'ya supera la ventana de {window}: la siguiente petición compacta y reintenta', 'compaction.confirm.hint': 'y cambiar · n/esc volver',
|
|
145
163
|
'doctor.nearTimeout': 'Valoración: {count} llamadas a Bash terminaron a unos 300 segundos, lo que coincide con el tiempo de espera de la herramienta; las trazas por sí solas no prueban la causa raíz. A continuación, revisa los marcadores de finalización del shell y los errores de terminal de esas llamadas.',
|
|
146
164
|
'doctor.logs.none': 'Aún no hay archivo de registro; los registros de consola anteriores no se conservaron y no se pueden recuperar.',
|
|
147
165
|
'doctor.evidence': 'Evidencia del diagnóstico: {traces} sesiones recientes, {logs} warnings/errores.',
|
package/plugins/memory/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
6
6
|
import { MemoryStore } from './store.mjs';
|
|
7
7
|
import { defaults, runPipeline } from './pipeline.mjs';
|
|
8
8
|
import { chargeTo } from '../session-metrics/attribution.mjs';
|
|
9
|
+
import { EFFORT_LEVELS, effortFor } from '../providers/effort.mjs';
|
|
9
10
|
|
|
10
11
|
export const name = 'dscode-memory';
|
|
11
12
|
export const inject = ['llm', 'sessions', 'sessionPersistence', 'systemPrompt', 'tools', 'commands'];
|
|
@@ -20,7 +21,7 @@ export function resolveConfig(options = {}) {
|
|
|
20
21
|
for (const key of ['maxPerRun', 'maxCandidates', 'maxInputChars', 'maxConsolidationChars', 'timeoutMs']) {
|
|
21
22
|
if (!Number.isSafeInteger(config[key])) throw Error(`Invalid memory ${key}: expected a safe integer`);
|
|
22
23
|
}
|
|
23
|
-
for (const key of ['extractEffort', 'consolidationEffort']) if (!
|
|
24
|
+
for (const key of ['extractEffort', 'consolidationEffort']) if (!EFFORT_LEVELS.includes(config[key])) throw Error(`Invalid memory ${key}`);
|
|
24
25
|
if (!!config.provider !== !!config.model) throw Error('Memory provider and model must be configured together');
|
|
25
26
|
if (typeof config.generate !== 'boolean' || typeof config.use !== 'boolean') throw Error('Invalid memory switches');
|
|
26
27
|
return config;
|
|
@@ -39,11 +40,14 @@ export function apply(ctx, options = {}) {
|
|
|
39
40
|
const deadline = AbortSignal.any([signal, AbortSignal.timeout(config.timeoutMs)]);
|
|
40
41
|
const assembler = new BlockAssembler();
|
|
41
42
|
let terminal = false, usage;
|
|
43
|
+
const target = { provider: config.provider ?? route.provider, model: config.model ?? route.model };
|
|
44
|
+
// The configured level, or the nearest one the memory model offers.
|
|
45
|
+
const reasoningEffort = await effortFor(ctx.llm, target, effort, deadline);
|
|
42
46
|
try {
|
|
43
47
|
// Background work is charged to the live session that scheduled it.
|
|
44
48
|
await chargeTo(live.has(lastSession) ? lastSession : undefined, 'memory', async () => {
|
|
45
49
|
for await (const chunk of ctx.llm.stream({
|
|
46
|
-
|
|
50
|
+
...target, ...(reasoningEffort ? { reasoningEffort } : {}),
|
|
47
51
|
system, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
|
|
48
52
|
maxTokens: 12000, signal: deadline,
|
|
49
53
|
})) {
|
|
@@ -58,7 +62,7 @@ export function apply(ctx, options = {}) {
|
|
|
58
62
|
const text = blocks.filter(b => b.type === 'text').map(b => b.text).join('').trim();
|
|
59
63
|
return JSON.parse(text.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, ''));
|
|
60
64
|
} finally {
|
|
61
|
-
if (!controller.signal.aborted) store.recordCall({ time: Date.now(),
|
|
65
|
+
if (!controller.signal.aborted) store.recordCall({ time: Date.now(), ...target, effort: reasoningEffort, usage: usage ?? null });
|
|
62
66
|
}
|
|
63
67
|
};
|
|
64
68
|
const schedule = (route, sessionId) => {
|