dsh-session-bridge 0.3.0 → 0.3.2-alpha.1
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 +18 -1
- package/README.zh.md +16 -0
- package/dsh.plugin.json +2 -2
- package/lib/index.js +498 -89
- package/package.json +21 -20
- package/scripts/build.sh +18 -0
- package/scripts/check-dsh-compat.mjs +173 -0
- package/src/core.ts +77 -13
- package/src/tools.ts +30 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2-alpha.1",
|
|
4
4
|
"description": "会话桥(dsh-session-bridge):通过提示词创建新的主会话(顶层 UI 会话)、向任意会话发送消息、等待会话的下一条回复、读取会话消息,并按会话名或 id 跨工作区查找会话;此外支持监控/调度主任务与归档会话。",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -52,28 +52,28 @@
|
|
|
52
52
|
},
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
56
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
57
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
58
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
59
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
60
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
61
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
62
|
-
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
55
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
56
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
57
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
59
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
60
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
61
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
62
|
+
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
63
63
|
"cordis": "^4.0.0-rc.7"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
67
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
68
|
-
"@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
69
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
70
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
71
|
-
"@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
72
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
73
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
74
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
75
|
-
"@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
76
|
-
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
|
|
66
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
67
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
68
|
+
"@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
69
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
70
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
71
|
+
"@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
72
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
73
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
74
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
75
|
+
"@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
76
|
+
"@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
|
|
77
77
|
"@types/node": "^24.0.0",
|
|
78
78
|
"cordis": "^4.0.0-rc.7",
|
|
79
79
|
"tsdown": "^0.22.14",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"scripts": {
|
|
83
83
|
"build": "bash scripts/build.sh",
|
|
84
84
|
"build:client": "tsdown",
|
|
85
|
+
"check:compat": "node scripts/check-dsh-compat.mjs",
|
|
85
86
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
86
87
|
}
|
|
87
88
|
}
|
package/scripts/build.sh
CHANGED
|
@@ -14,6 +14,24 @@ if [ -z "$CHECKOUT" ] || [ ! -d "$CHECKOUT/packages" ]; then
|
|
|
14
14
|
exit 1
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
+
# The type-check below runs against $DSH_CHECKOUT, which commonly lags the dsh
|
|
18
|
+
# that actually loads this plugin — a stale checkout type-checks green while the
|
|
19
|
+
# running harness has already moved on. Warn loudly rather than implying support.
|
|
20
|
+
read_pkg_version() {
|
|
21
|
+
node -e "try{process.stdout.write(String(require(process.argv[1]).version))}catch(e){process.stdout.write('unknown')}" "$1" 2>/dev/null || echo unknown
|
|
22
|
+
}
|
|
23
|
+
CHECKOUT_VERSION="$(read_pkg_version "$CHECKOUT/package.json")"
|
|
24
|
+
INSTALLED_VERSION="unknown"
|
|
25
|
+
GLOBAL_ROOT="$(npm root -g 2>/dev/null | head -n1 || true)"
|
|
26
|
+
if [ -n "$GLOBAL_ROOT" ] && [ -f "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json" ]; then
|
|
27
|
+
INSTALLED_VERSION="$(read_pkg_version "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json")"
|
|
28
|
+
fi
|
|
29
|
+
if [ "$INSTALLED_VERSION" != "unknown" ] && [ "$CHECKOUT_VERSION" != "$INSTALLED_VERSION" ]; then
|
|
30
|
+
echo "build: WARNING — checkout is $CHECKOUT_VERSION but the installed dsh is $INSTALLED_VERSION." >&2
|
|
31
|
+
echo "build: the type-check below therefore does NOT cover the running harness;" >&2
|
|
32
|
+
echo "build: run 'npm run check:compat' to type-check against the installed dsh." >&2
|
|
33
|
+
fi
|
|
34
|
+
|
|
17
35
|
TSC="$CHECKOUT/node_modules/.bin/tsc"
|
|
18
36
|
if [ ! -x "$TSC" ]; then
|
|
19
37
|
echo "build: tsc not found at $TSC" >&2
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Type-check src/ against the DSH packages that are ACTUALLY INSTALLED,
|
|
4
|
+
* rather than against $DSH_CHECKOUT.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: scripts/build.sh type-checks against a DSH *source* checkout.
|
|
7
|
+
* That checkout routinely lags the harness the plugin is loaded into (e.g. the
|
|
8
|
+
* checkout sat at 0.1.5-alpha.2 while the running harness was 0.1.6-alpha.1), so
|
|
9
|
+
* a green `build.sh` silently proves nothing about the running version. This
|
|
10
|
+
* script reads the `lib/types/*.d.ts` that ship inside the installed DSH npm
|
|
11
|
+
* package, i.e. the exact API surface the plugin loads against — so an upstream
|
|
12
|
+
* break shows up here even when the checkout is stale.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/check-dsh-compat.mjs [--dsh <path>]
|
|
16
|
+
*
|
|
17
|
+
* <path> may be either the DSH package directory or the `@deepseek-ai` scope
|
|
18
|
+
* directory that holds dsh-session, dsh-agent, ... . When omitted, the installed
|
|
19
|
+
* location is probed (DSH_INSTALLED_MODULES, then `npm root -g`).
|
|
20
|
+
*
|
|
21
|
+
* Exits 0 when the plugin's src/ type-checks against those types, 1 otherwise.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync, execSync } from 'node:child_process'
|
|
24
|
+
import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, join, resolve } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
|
|
28
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
29
|
+
|
|
30
|
+
/** Read a JSON file, or return undefined when it is absent/unreadable. */
|
|
31
|
+
function readJson(file) {
|
|
32
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return undefined }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when `dir` looks like the `@deepseek-ai` scope directory. */
|
|
36
|
+
function isScopeDir(dir) {
|
|
37
|
+
return dir !== undefined && existsSync(join(dir, 'dsh-session', 'package.json'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the `@deepseek-ai` scope directory holding the installed DSH packages.
|
|
42
|
+
* Accepts an explicit path (package dir or scope dir) and otherwise probes.
|
|
43
|
+
*/
|
|
44
|
+
function resolveScopeDir(argv) {
|
|
45
|
+
const flagAt = argv.indexOf('--dsh')
|
|
46
|
+
const explicit = flagAt !== -1 ? argv[flagAt + 1] : (process.env.DSH_INSTALLED_MODULES ?? undefined)
|
|
47
|
+
const candidates = []
|
|
48
|
+
if (explicit !== undefined && explicit !== '') {
|
|
49
|
+
const base = resolve(explicit)
|
|
50
|
+
// Accept the scope dir itself, the dsh package dir, or a node_modules tree.
|
|
51
|
+
candidates.push(base, join(base, 'node_modules', '@deepseek-ai'), join(base, '@deepseek-ai'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Anchor 1: the global install root. execSync (string command) rather than
|
|
55
|
+
// execFileSync, because npm is a .cmd shim on Windows that cannot be spawned
|
|
56
|
+
// without a shell.
|
|
57
|
+
let globalRoot
|
|
58
|
+
try {
|
|
59
|
+
globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
60
|
+
} catch { globalRoot = undefined }
|
|
61
|
+
if (globalRoot !== undefined && globalRoot !== '') {
|
|
62
|
+
// npm layout, then the common "dsh nests its own deps" layout.
|
|
63
|
+
candidates.push(join(globalRoot, '@deepseek-ai'), join(globalRoot, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Anchor 2: walk up from the `dsh` executable itself. Its path is usually a
|
|
67
|
+
// symlink (scoop/volta), so resolve it first or the walk misses the real tree.
|
|
68
|
+
try {
|
|
69
|
+
const found = execSync(process.platform === 'win32' ? 'where dsh' : 'which dsh', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
70
|
+
.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== '')
|
|
71
|
+
for (const line of found) {
|
|
72
|
+
let dir
|
|
73
|
+
try { dir = dirname(realpathSync(line)) } catch { dir = dirname(line) }
|
|
74
|
+
for (let hop = 0; hop < 4 && dir !== dirname(dir); hop++) {
|
|
75
|
+
candidates.push(join(dir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
|
|
76
|
+
candidates.push(join(dir, 'node_modules', '@deepseek-ai'))
|
|
77
|
+
dir = dirname(dir)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch { /* dsh not on PATH; the other anchors decide */ }
|
|
81
|
+
|
|
82
|
+
for (const candidate of candidates) if (isScopeDir(candidate)) return candidate
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolve a package's types entry point inside the scope directory. */
|
|
87
|
+
function typesEntry(scope, pkg) {
|
|
88
|
+
const dir = join(scope, pkg)
|
|
89
|
+
const manifest = readJson(join(dir, 'package.json'))
|
|
90
|
+
if (manifest === undefined) return undefined
|
|
91
|
+
const declared = typeof manifest.types === 'string' ? manifest.types : undefined
|
|
92
|
+
const candidates = [
|
|
93
|
+
...(declared !== undefined ? [join(dir, declared)] : []),
|
|
94
|
+
join(dir, 'lib', 'types', 'index.d.ts'),
|
|
95
|
+
join(dir, 'index.d.ts'),
|
|
96
|
+
]
|
|
97
|
+
return candidates.find((c) => existsSync(c))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fail(message) {
|
|
101
|
+
console.error('check-dsh-compat: ' + message)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scope = resolveScopeDir(process.argv.slice(2))
|
|
106
|
+
if (scope === undefined) {
|
|
107
|
+
fail('could not locate the installed DSH packages. Pass --dsh <path> (the DSH package dir or its @deepseek-ai scope dir) or set DSH_INSTALLED_MODULES.')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read the version of the `dsh` package that owns this scope directory. */
|
|
111
|
+
function dshVersionOf(scopeDir) {
|
|
112
|
+
// Nested layout puts dsh two levels above its own scope dir; a flat global
|
|
113
|
+
// install puts it beside the other packages.
|
|
114
|
+
for (const candidate of [join(scopeDir, 'dsh', 'package.json'), join(scopeDir, '..', '..', 'package.json')]) {
|
|
115
|
+
const manifest = readJson(candidate)
|
|
116
|
+
if (manifest?.name === '@deepseek-ai/dsh' && typeof manifest.version === 'string') return manifest.version
|
|
117
|
+
}
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dshVersion = dshVersionOf(scope) ?? '(unknown)'
|
|
122
|
+
|
|
123
|
+
// Every DSH package src/ imports, plus the plain `cordis` specifier that
|
|
124
|
+
// package.json declares as a peer.
|
|
125
|
+
const manifest = readJson(join(ROOT, 'package.json')) ?? {}
|
|
126
|
+
const declared = [...Object.keys(manifest.peerDependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
|
|
127
|
+
const wanted = new Set(declared.filter((name) => name.startsWith('@deepseek-ai/')))
|
|
128
|
+
wanted.add('@deepseek-ai/cordis')
|
|
129
|
+
wanted.add('@deepseek-ai/schemastery')
|
|
130
|
+
|
|
131
|
+
const paths = {}
|
|
132
|
+
const missing = []
|
|
133
|
+
for (const name of [...wanted].sort()) {
|
|
134
|
+
const entry = typesEntry(scope, name.slice('@deepseek-ai/'.length))
|
|
135
|
+
if (entry === undefined) { missing.push(name); continue }
|
|
136
|
+
paths[name] = [entry]
|
|
137
|
+
}
|
|
138
|
+
// tsconfig.json maps the bare `cordis` specifier; keep that working under the
|
|
139
|
+
// installed layout too, where cordis is published scoped.
|
|
140
|
+
if (paths['@deepseek-ai/cordis'] !== undefined) paths['cordis'] = paths['@deepseek-ai/cordis']
|
|
141
|
+
|
|
142
|
+
if (missing.length > 0) {
|
|
143
|
+
fail('installed DSH is missing the packages this plugin imports: ' + missing.join(', ') + '\n (looked in ' + scope + ')')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The generated config must live in the repo root: tsconfig.json's `include`
|
|
147
|
+
// is relative to the config that declares it, so a config placed elsewhere
|
|
148
|
+
// would look for src/ in the wrong directory. Every mapped path is absolute,
|
|
149
|
+
// so only `include`/`extends` depend on the location. Removed again below.
|
|
150
|
+
const configPath = join(ROOT, '.dsh-compat.tsconfig.json')
|
|
151
|
+
writeFileSync(configPath, JSON.stringify({ extends: './tsconfig.json', compilerOptions: { paths } }, null, 2))
|
|
152
|
+
|
|
153
|
+
// Run tsc's JS entry through the current node binary. Spawning the .bin shell
|
|
154
|
+
// shim instead would need a shell on Windows (npm .cmd/.bat shims cannot be
|
|
155
|
+
// spawned directly), and this way the exit code and output stay unambiguous.
|
|
156
|
+
const tsc = join(ROOT, 'node_modules', 'typescript', 'bin', 'tsc')
|
|
157
|
+
if (!existsSync(tsc)) { rmSync(configPath, { force: true }); fail('local typescript not found at ' + tsc + ' (run the package manager install first)') }
|
|
158
|
+
|
|
159
|
+
console.log('check-dsh-compat: type-checking src/ against installed dsh ' + dshVersion)
|
|
160
|
+
console.log('check-dsh-compat: scope ' + scope)
|
|
161
|
+
let failure
|
|
162
|
+
try {
|
|
163
|
+
execFileSync(process.execPath, [tsc, '-p', configPath, '--noEmit'], { cwd: ROOT, stdio: 'inherit' })
|
|
164
|
+
} catch (error) {
|
|
165
|
+
failure = error
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(configPath, { force: true })
|
|
168
|
+
}
|
|
169
|
+
if (failure !== undefined) {
|
|
170
|
+
const status = typeof failure.status === 'number' ? ' (tsc exit ' + failure.status + ')' : ''
|
|
171
|
+
fail('src/ does NOT type-check against installed dsh ' + dshVersion + status + ' — this is a real incompatibility with the running harness.')
|
|
172
|
+
}
|
|
173
|
+
console.log('check-dsh-compat: OK — src/ type-checks against installed dsh ' + dshVersion)
|
package/src/core.ts
CHANGED
|
@@ -21,7 +21,9 @@ export interface LiveAgentLike {
|
|
|
21
21
|
whenIdle?(): Promise<void>
|
|
22
22
|
readonly ctx: Context
|
|
23
23
|
readonly inbox?: {
|
|
24
|
-
hasPending
|
|
24
|
+
// dsh 0.1.2 及之前:`hasPending` getter;dsh 0.1.5 起已删除,仅有
|
|
25
|
+
// nextTurn / nextStep 只读数组(Inbox 成为 driver 持有的接口)。
|
|
26
|
+
hasPending?: boolean
|
|
25
27
|
nextTurn?: readonly unknown[]
|
|
26
28
|
nextStep?: readonly unknown[]
|
|
27
29
|
}
|
|
@@ -86,6 +88,34 @@ export function asEventList(events: unknown): readonly SessionEvent[] {
|
|
|
86
88
|
return Array.isArray(events) ? events : []
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 读取一个持久化(离线)会话的事件日志与头部元数据。兼容两代 persistence API:
|
|
93
|
+
* - 旧版:`sessionPersistence.inspect(id)` 直接返回 { events, meta };
|
|
94
|
+
* - dsh 0.1.5 之后:`inspect` 已删除,改用 `open(id, 'read')` → `handle.read()`
|
|
95
|
+
* (+ `handle.header`),读句柄不抢写所有权,读后必须 close()。
|
|
96
|
+
* 会话不存在时两个 API 都抛错,调用方按原语义处理。
|
|
97
|
+
*/
|
|
98
|
+
export async function inspectPersistedSession(ctx: Context, id: string): Promise<{ events: readonly SessionEvent[]; meta: { cwd?: string } }> {
|
|
99
|
+
const persistence = ctx.sessionPersistence as unknown as {
|
|
100
|
+
inspect?: (sid: string) => Promise<{ events: readonly SessionEvent[]; meta: { cwd?: string } }>
|
|
101
|
+
open?: (sid: string, access: 'read') => Promise<{
|
|
102
|
+
header: { cwd?: string }
|
|
103
|
+
read(): Promise<{ events: readonly SessionEvent[] }>
|
|
104
|
+
close(): Promise<void>
|
|
105
|
+
}>
|
|
106
|
+
}
|
|
107
|
+
if (typeof persistence.inspect === 'function') {
|
|
108
|
+
return await persistence.inspect(id)
|
|
109
|
+
}
|
|
110
|
+
const handle = await persistence.open!(id, 'read')
|
|
111
|
+
try {
|
|
112
|
+
const { events } = await handle.read()
|
|
113
|
+
return { events, meta: handle.header }
|
|
114
|
+
} finally {
|
|
115
|
+
await handle.close()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
/** agent.followup / steer 接受的用户消息值。 */
|
|
90
120
|
export function userMessage(text: string): unknown {
|
|
91
121
|
return {
|
|
@@ -237,18 +267,20 @@ export interface CoTLiveSlice {
|
|
|
237
267
|
}
|
|
238
268
|
|
|
239
269
|
/**
|
|
240
|
-
* 对一个 live 会话的事件日志折叠出其"
|
|
241
|
-
* -
|
|
242
|
-
*
|
|
243
|
-
* -
|
|
244
|
-
* -
|
|
245
|
-
*
|
|
270
|
+
* 对一个 live 会话的事件日志折叠出其"最近推理/文本增量流"。兼容两代日志形状:
|
|
271
|
+
* - dsh 0.1.5 之后:turn 内不再发布独立 chunk 事件,流式增量内嵌在已定型
|
|
272
|
+
* assistant/message(或未定型 assistant/attempt)事件的 `stream` 记录里
|
|
273
|
+
* (reasoning-chunks / text-chunks 批量段 + 原始 chunk:reasoning-delta / text-delta);
|
|
274
|
+
* - 0.1.2-rc 之前:独立的 assistant/chunk 事件(turn 中途即可见、增量,
|
|
275
|
+
* 这正是旧版监控思维链并按规则提前终止所需的粒度)。
|
|
276
|
+
* 两者兼读、取最新一份;无任何增量(会话空闲、或该 provider 不流式推理/无推理)
|
|
277
|
+
* 返回 null,调用方应回落为 foldMessages 里已定型的 message.reasoning。
|
|
246
278
|
* 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
|
|
247
279
|
*/
|
|
248
280
|
export function liveReasoningSnapshot(events: readonly SessionEvent[]): CoTLiveSlice | null {
|
|
249
281
|
const list = asEventList(events)
|
|
250
|
-
//
|
|
251
|
-
//
|
|
282
|
+
// 旧格式只累积"最近一条已定型 assistant/message 之后"的流式增量(in-flight 窗口);
|
|
283
|
+
// 新格式的 stream 随定型事件落地,直接以最新携带 stream 的事件为准。
|
|
252
284
|
let lastFinalizedSeq = -1
|
|
253
285
|
let reasoning = ''
|
|
254
286
|
let text = ''
|
|
@@ -258,11 +290,42 @@ export function liveReasoningSnapshot(events: readonly SessionEvent[]): CoTLiveS
|
|
|
258
290
|
let step = 0
|
|
259
291
|
let foundDelta = false
|
|
260
292
|
for (const event of list) {
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
const type = event.type as string
|
|
294
|
+
if (type === 'assistant/message' || type === 'assistant/attempt') {
|
|
295
|
+
const data = event.data as { turn?: unknown; step?: unknown; stream?: unknown } | null | undefined
|
|
296
|
+
if (data !== null && data !== undefined && typeof data === 'object' && Array.isArray(data.stream)) {
|
|
297
|
+
let r = ''
|
|
298
|
+
let t = ''
|
|
299
|
+
for (const raw of data.stream as unknown[]) {
|
|
300
|
+
const record = raw as { type?: unknown; texts?: unknown; text?: unknown; chunk?: unknown } | null
|
|
301
|
+
if (record === null || typeof record !== 'object') continue
|
|
302
|
+
if (record.type === 'reasoning-chunks' && Array.isArray(record.texts)) {
|
|
303
|
+
for (const s of record.texts as unknown[]) if (typeof s === 'string') r += s
|
|
304
|
+
} else if (record.type === 'text-chunks' && Array.isArray(record.texts)) {
|
|
305
|
+
for (const s of record.texts as unknown[]) if (typeof s === 'string') t += s
|
|
306
|
+
} else if (record.type === 'chunk') {
|
|
307
|
+
const c = record.chunk as { type?: unknown; text?: unknown } | null
|
|
308
|
+
if (c !== null && typeof c === 'object' && typeof c.type === 'string' && typeof c.text === 'string') {
|
|
309
|
+
if (c.type === 'reasoning-delta') r += c.text
|
|
310
|
+
else if (c.type === 'text-delta') t += c.text
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (r !== '' || t !== '') {
|
|
315
|
+
if (typeof data.turn === 'number') turn = data.turn
|
|
316
|
+
if (typeof data.step === 'number') step = data.step
|
|
317
|
+
reasoning = r
|
|
318
|
+
text = t
|
|
319
|
+
seq = event.seq
|
|
320
|
+
time = event.time
|
|
321
|
+
foundDelta = true
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (type === 'assistant/message') lastFinalizedSeq = event.seq
|
|
263
325
|
continue
|
|
264
326
|
}
|
|
265
|
-
|
|
327
|
+
// 旧格式:独立 assistant/chunk 事件。
|
|
328
|
+
if (type !== 'assistant/chunk') continue
|
|
266
329
|
if (event.seq <= lastFinalizedSeq) continue
|
|
267
330
|
const data = event.data as { turn?: unknown; step?: unknown; chunk?: unknown } | null | undefined
|
|
268
331
|
if (data === null || data === undefined || typeof data !== 'object') continue
|
|
@@ -559,7 +622,8 @@ export function statusSnapshot(ctx: Context, agent: LiveAgentLike): BridgeStatus
|
|
|
559
622
|
lastTurn,
|
|
560
623
|
lastActivityAt,
|
|
561
624
|
stalledMs: lastActivityAt === null ? null : Date.now() - lastActivityAt,
|
|
562
|
-
|
|
625
|
+
// 双兼容:0.1.2 用 hasPending getter;0.1.5 起只有 nextTurn/nextStep 数组。
|
|
626
|
+
pendingWork: inbox?.hasPending === true || (inbox?.nextTurn?.length ?? 0) + (inbox?.nextStep?.length ?? 0) > 0,
|
|
563
627
|
nextTurnCount: inbox?.nextTurn?.length ?? 0,
|
|
564
628
|
nextStepCount: inbox?.nextStep?.length ?? 0,
|
|
565
629
|
...(lastAssistantText === undefined ? {} : { lastAssistantText }),
|
package/src/tools.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
8
8
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
9
9
|
import type {} from '@deepseek-ai/dsh-agent-presets'
|
|
10
|
-
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
10
|
+
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
11
11
|
import * as agentApi from '@deepseek-ai/dsh-agent'
|
|
12
12
|
import type {
|
|
13
13
|
BridgeFindItem,
|
|
@@ -15,11 +15,11 @@ import type {
|
|
|
15
15
|
BridgeStatusSnapshot,
|
|
16
16
|
BridgeWaitResult,
|
|
17
17
|
LiveAgentLike,
|
|
18
|
-
WaitForReplyOptions,
|
|
19
18
|
} from './core.ts'
|
|
20
19
|
import {
|
|
21
20
|
attachSessionToWorkspace,
|
|
22
21
|
foldMessages,
|
|
22
|
+
inspectPersistedSession,
|
|
23
23
|
maxSeq,
|
|
24
24
|
resolveTargetCwd,
|
|
25
25
|
segmentsSince,
|
|
@@ -102,10 +102,10 @@ interface WaitArgs {
|
|
|
102
102
|
timeoutMs?: number
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async function maybeWait(env: BridgeEnv, session:
|
|
105
|
+
async function maybeWait(env: BridgeEnv, session: Session, baselineSeq: number, args: WaitArgs, signal: AbortSignal): Promise<ReturnType<typeof renderWait> | undefined> {
|
|
106
106
|
if (args.waitForReply !== true) return undefined
|
|
107
107
|
const result = await waitForReply({
|
|
108
|
-
session
|
|
108
|
+
session,
|
|
109
109
|
baselineSeq,
|
|
110
110
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
111
111
|
signal,
|
|
@@ -194,8 +194,16 @@ function registerCreate(env: BridgeEnv): void {
|
|
|
194
194
|
...(model !== '' ? { model } : {}),
|
|
195
195
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
196
196
|
},
|
|
197
|
-
setup: (agentCtx: Context) => {
|
|
198
|
-
|
|
197
|
+
setup: async (agentCtx: Context) => {
|
|
198
|
+
const requestedPreset = typeof args.agentPreset === 'string' && args.agentPreset.trim() !== '' ? args.agentPreset.trim() : undefined
|
|
199
|
+
if (requestedPreset !== undefined) {
|
|
200
|
+
// An explicit agentPreset composes THAT preset. Falling through to the
|
|
201
|
+
// caller-join below would silently ignore the request: the id would
|
|
202
|
+
// still reach the session header, while the agent kept running on the
|
|
203
|
+
// caller's standing composition and its tools.
|
|
204
|
+
const presets = env.ctx.agentPresets as unknown as { mount(agentCtx: Context, id: string): Promise<unknown> }
|
|
205
|
+
await presets.mount(agentCtx, requestedPreset)
|
|
206
|
+
} else if (caller !== undefined) {
|
|
199
207
|
const presets = env.ctx.agentPresets as unknown as { composeFrom(agentCtx: Context, parentCtx: Context): string | undefined }
|
|
200
208
|
presets.composeFrom(agentCtx, caller.ctx)
|
|
201
209
|
}
|
|
@@ -354,8 +362,13 @@ function registerResume(env: BridgeEnv): void {
|
|
|
354
362
|
}
|
|
355
363
|
let headers: readonly { id: string; cwd?: string; createdAt: number }[] = []
|
|
356
364
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
365
|
+
// dsh >= 0.1.5-alpha.1:list() 返回 { header, ... } 快照;映射回平铺字段。
|
|
366
|
+
const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; createdAt: number } }[]> }
|
|
367
|
+
headers = (await persistence.list()).map((rec) => ({
|
|
368
|
+
id: rec.header.id,
|
|
369
|
+
...(rec.header.cwd === undefined ? {} : { cwd: rec.header.cwd }),
|
|
370
|
+
createdAt: rec.header.createdAt,
|
|
371
|
+
}))
|
|
359
372
|
} catch (error) {
|
|
360
373
|
throw new Error('session persistence unavailable: ' + (error instanceof Error ? error.message : String(error)))
|
|
361
374
|
}
|
|
@@ -502,8 +515,7 @@ function registerSegments(env: BridgeEnv): void {
|
|
|
502
515
|
live = true
|
|
503
516
|
} else {
|
|
504
517
|
try {
|
|
505
|
-
const
|
|
506
|
-
const inspection = await persistence.inspect(args.sessionId)
|
|
518
|
+
const inspection = await inspectPersistedSession(env.ctx, args.sessionId)
|
|
507
519
|
events = inspection.events
|
|
508
520
|
live = false
|
|
509
521
|
} catch (error) {
|
|
@@ -580,8 +592,7 @@ function registerRead(env: BridgeEnv): void {
|
|
|
580
592
|
} else {
|
|
581
593
|
let inspection: { events: readonly SessionEvent[]; meta: { cwd?: string } }
|
|
582
594
|
try {
|
|
583
|
-
|
|
584
|
-
inspection = await persistence.inspect(args.sessionId)
|
|
595
|
+
inspection = await inspectPersistedSession(env.ctx, args.sessionId)
|
|
585
596
|
} catch (error) {
|
|
586
597
|
throw new Error('cannot read session ' + JSON.stringify(args.sessionId) + ': ' + (error instanceof Error ? error.message : String(error)))
|
|
587
598
|
}
|
|
@@ -685,8 +696,11 @@ function registerFind(env: BridgeEnv): void {
|
|
|
685
696
|
}
|
|
686
697
|
if (args.liveOnly !== true) {
|
|
687
698
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
699
|
+
// dsh >= 0.1.5-alpha.1:list() 返回 { header: SessionHeader, revision, ... } 快照,
|
|
700
|
+
// 不再平铺 id/cwd;旧版直接返回 header 平铺字段。两者都按新形状读,向下兼容。
|
|
701
|
+
const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; parentSession?: string; origin?: 'subagent'; createdAt: number; agentPreset?: string } }[]> }
|
|
702
|
+
for (const rec of await persistence.list()) {
|
|
703
|
+
const header = rec.header
|
|
690
704
|
if (items.some((item) => item.sessionId === header.id)) continue
|
|
691
705
|
items.push({
|
|
692
706
|
sessionId: header.id,
|
|
@@ -720,13 +734,12 @@ function registerFind(env: BridgeEnv): void {
|
|
|
720
734
|
const cwdFilter = typeof args.cwd === 'string' ? args.cwd.trim().toLowerCase() : ''
|
|
721
735
|
const needsOfflineTitle = titleFilter !== '' || (query !== '' && items.some((item) => !item.live))
|
|
722
736
|
if (needsOfflineTitle) {
|
|
723
|
-
const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
|
|
724
737
|
let inspected = 0
|
|
725
738
|
for (const item of items) {
|
|
726
739
|
if (inspected >= 30) break
|
|
727
740
|
if (item.live || item.title !== undefined) continue
|
|
728
741
|
try {
|
|
729
|
-
const inspection = await
|
|
742
|
+
const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
|
|
730
743
|
item.title = titleOf(inspection.events)
|
|
731
744
|
inspected += 1
|
|
732
745
|
} catch {
|
|
@@ -1118,7 +1131,6 @@ function registerArchive(env: BridgeEnv): void {
|
|
|
1118
1131
|
const archived = env.ctx.workspaceRegistry.archivedSessionIds.map(String)
|
|
1119
1132
|
const items: Array<{ sessionId: string; title?: string }> = archived.map((id) => ({ sessionId: id }))
|
|
1120
1133
|
if (args.resolveTitles === true && items.length > 0) {
|
|
1121
|
-
const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
|
|
1122
1134
|
const registryTitles = new Map<string, string>()
|
|
1123
1135
|
try {
|
|
1124
1136
|
const records = await env.registry.all()
|
|
@@ -1130,7 +1142,7 @@ function registerArchive(env: BridgeEnv): void {
|
|
|
1130
1142
|
continue
|
|
1131
1143
|
}
|
|
1132
1144
|
try {
|
|
1133
|
-
const inspection = await
|
|
1145
|
+
const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
|
|
1134
1146
|
item.title = titleOf(inspection.events)
|
|
1135
1147
|
} catch { /* offline title unavailable */ }
|
|
1136
1148
|
}
|