@liwu1995/afu-context 0.1.5 → 0.1.11
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 +80 -51
- package/README.zh-CN.md +80 -51
- package/bin/afu.mjs +54 -7
- package/bin/host-autoload.py +89 -25
- package/bin/install-guide.mjs +25 -3
- package/bin/platform.mjs +4 -4
- package/bin/runtime.mjs +5 -1
- package/package.json +1 -1
- package/python/{afu_context_local-0.1.5-py3-none-any.whl → afu_context_local-0.1.11-py3-none-any.whl} +0 -0
- package/release.json +3 -3
- package/scripts/dsh-overlay.mjs +8 -3
- package/src/afu_host_integrations/dsh/dist/index.js +29 -4
- package/src/afu_host_integrations/pi/dist/index.js +214 -10
- package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +6 -3
- package/src/afu_host_integrations/shared/runtime-client/dist/index.js +1 -1
- package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +25 -5
package/bin/host-autoload.py
CHANGED
|
@@ -13,23 +13,84 @@ from pathlib import Path
|
|
|
13
13
|
from afu_context import native_install as fs
|
|
14
14
|
from afu_context.diagnostics import state_root
|
|
15
15
|
from afu_context.management import report, validate_policy
|
|
16
|
+
from afu_context.host_command import host_command
|
|
17
|
+
from afu_context.platform_runtime import redirected
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def node_binary():
|
|
21
|
+
"""The Node.js running this launcher (npx), so Windows needs no PATH lookup."""
|
|
22
|
+
return os.environ.get("AFU_NODE") or shutil.which("node") or "node"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dsh_command():
|
|
26
|
+
"""argv that starts DSH, plus the path of its real entry script.
|
|
27
|
+
|
|
28
|
+
npm installs a POSIX symlink to lib/bin.js, but on Windows a dsh.cmd shim
|
|
29
|
+
that subprocess cannot run without a shell. Running node on the package's
|
|
30
|
+
own entry script works on both and needs no shell.
|
|
31
|
+
"""
|
|
32
|
+
found = shutil.which("dsh")
|
|
33
|
+
if not found:
|
|
34
|
+
raise ValueError("dsh is not installed")
|
|
35
|
+
if not WINDOWS():
|
|
36
|
+
return [found], os.path.realpath(found)
|
|
37
|
+
command = host_command(found, "dsh", platform="nt")
|
|
38
|
+
return command, command[-1]
|
|
16
39
|
|
|
17
40
|
|
|
18
41
|
def run(*args, stdin=None):
|
|
42
|
+
args = [str(a) for a in args]
|
|
43
|
+
if args and args[0] == "node":
|
|
44
|
+
args[0] = node_binary()
|
|
19
45
|
result = subprocess.run(
|
|
20
|
-
args, input=stdin, capture_output=True, text=True, timeout=30
|
|
46
|
+
args, input=stdin, capture_output=True, text=True, encoding="utf-8" if WINDOWS() else None, timeout=30
|
|
21
47
|
)
|
|
22
48
|
if result.returncode:
|
|
23
49
|
raise ValueError("Host configuration could not be loaded")
|
|
24
50
|
return result.stdout
|
|
25
51
|
|
|
26
52
|
|
|
53
|
+
def WINDOWS():
|
|
54
|
+
return os.name == "nt"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_link(path):
|
|
58
|
+
"""A symlink, or on Windows a directory junction (both point elsewhere)."""
|
|
59
|
+
if WINDOWS():
|
|
60
|
+
return redirected(path)
|
|
61
|
+
if path.is_symlink():
|
|
62
|
+
return True
|
|
63
|
+
is_junction = getattr(path, "is_junction", None)
|
|
64
|
+
return bool(is_junction and is_junction())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_junction(target, link):
|
|
68
|
+
import _winapi
|
|
69
|
+
# COSEC: native junction API preserves literal paths without cmd expansion.
|
|
70
|
+
_winapi.CreateJunction(target, link)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def link_directory(link, target):
|
|
74
|
+
"""Point link at target. Windows uses a junction: no admin rights needed."""
|
|
75
|
+
if WINDOWS():
|
|
76
|
+
create_junction(str(target), str(link))
|
|
77
|
+
return
|
|
78
|
+
link.symlink_to(target, target_is_directory=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def unlink_directory(link):
|
|
82
|
+
if WINDOWS() and not link.is_symlink():
|
|
83
|
+
os.rmdir(link) # Removes the junction itself, never the target files.
|
|
84
|
+
else:
|
|
85
|
+
link.unlink()
|
|
86
|
+
|
|
87
|
+
|
|
27
88
|
def write_owned(file, content, marker):
|
|
28
89
|
if file.is_symlink():
|
|
29
90
|
raise ValueError("Refusing to replace an extension symlink")
|
|
30
|
-
if file.exists() and file.read_text() == content:
|
|
91
|
+
if file.exists() and file.read_text(encoding="utf-8" if WINDOWS() else None) == content:
|
|
31
92
|
return
|
|
32
|
-
if file.exists() and not file.read_text().startswith(marker):
|
|
93
|
+
if file.exists() and not file.read_text(encoding="utf-8" if WINDOWS() else None).startswith(marker):
|
|
33
94
|
raise ValueError("Existing extension belongs to another installation")
|
|
34
95
|
fs.atomic(file, content)
|
|
35
96
|
|
|
@@ -53,8 +114,9 @@ def install_pi(root, adapter, settings, previous):
|
|
|
53
114
|
raise ValueError("Pi extension changed since installation; user file preserved")
|
|
54
115
|
marker = "// Managed by AFU " + root.as_uri() + "\n"
|
|
55
116
|
# The explicit launcher supplies its own extension; avoid installing hooks twice.
|
|
56
|
-
|
|
57
|
-
body
|
|
117
|
+
# A file: URL imports on every OS (a bare "C:\\..." path is not a valid specifier).
|
|
118
|
+
body = marker + "import register from " + json.dumps(Path(adapter).resolve().as_uri()) + ";\n"
|
|
119
|
+
body += "export default function(pi) {\n if (process.env.AFU_EXPLICIT_EXTENSION === 'pi' || (process.env.AFU_CONTEXT_COMPACT_WORKER_COMMAND && process.argv.some(a => a.replace(/\\\\/g, '/').endsWith('/afu_host_integrations/pi/dist/index.js')))) return;\n"
|
|
58
120
|
body += (
|
|
59
121
|
" Object.assign(process.env, "
|
|
60
122
|
+ json.dumps(settings)
|
|
@@ -64,18 +126,17 @@ def install_pi(root, adapter, settings, previous):
|
|
|
64
126
|
return {"entry": str(entry), "digest": hashlib.sha256(body.encode()).hexdigest()}
|
|
65
127
|
|
|
66
128
|
|
|
67
|
-
def profile_manifest(
|
|
68
|
-
|
|
69
|
-
tree = run(*args)
|
|
129
|
+
def profile_manifest(command, entry, platform, profile):
|
|
130
|
+
tree = run(*command, "--profile", profile, "--dump-config")
|
|
70
131
|
info = json.loads(
|
|
71
|
-
run("node", platform / "scripts/dsh-web-probe.mjs",
|
|
132
|
+
run("node", platform / "scripts/dsh-web-probe.mjs", entry, profile)
|
|
72
133
|
)
|
|
73
134
|
return Path(info["profileDir"]) / "package.json", tree
|
|
74
135
|
|
|
75
136
|
|
|
76
137
|
def install_dsh(root, platform, adapter, settings, profile, previous):
|
|
77
|
-
binary =
|
|
78
|
-
manifest, tree = profile_manifest(binary, platform, profile)
|
|
138
|
+
command, binary = dsh_command()
|
|
139
|
+
manifest, tree = profile_manifest(command, binary, platform, profile)
|
|
79
140
|
if previous and previous["manifest"] != str(manifest):
|
|
80
141
|
raise ValueError(
|
|
81
142
|
"DSH profile directory changed; existing registration preserved"
|
|
@@ -87,15 +148,15 @@ def install_dsh(root, platform, adapter, settings, profile, previous):
|
|
|
87
148
|
entry = bundle / "provider.mjs"
|
|
88
149
|
link = profile_dir / "node_modules" / bundle_name
|
|
89
150
|
marker = "// Managed by AFU " + root.as_uri() + "\n"
|
|
90
|
-
original = manifest.read_text()
|
|
151
|
+
original = manifest.read_text(encoding="utf-8" if WINDOWS() else None)
|
|
91
152
|
config = json.loads(original)
|
|
92
153
|
bundles = config["dsh"]["profile"]["bundles"]
|
|
93
154
|
deps = config.setdefault("dependencies", {})
|
|
94
155
|
owned_dep = "file:" + str(bundle)
|
|
95
|
-
if link.parent
|
|
156
|
+
if is_link(link.parent):
|
|
96
157
|
raise ValueError("Cannot register a bundle in a shared node_modules symlink")
|
|
97
|
-
if (link
|
|
98
|
-
link.exists() and not link
|
|
158
|
+
if (is_link(link) and link.resolve() != bundle.resolve()) or (
|
|
159
|
+
link.exists() and not is_link(link)
|
|
99
160
|
):
|
|
100
161
|
raise ValueError("Existing DSH bundle directory conflicts with AFU")
|
|
101
162
|
if bundle_name in deps and deps[bundle_name] != owned_dep:
|
|
@@ -115,8 +176,8 @@ def install_dsh(root, platform, adapter, settings, profile, previous):
|
|
|
115
176
|
)
|
|
116
177
|
preset = bundle / "presets.mjs"
|
|
117
178
|
if preset.exists():
|
|
118
|
-
old_arg = ", " + json.dumps(str(bundle)) + ");"
|
|
119
|
-
source = preset.read_text()
|
|
179
|
+
old_arg = ", " + json.dumps(str(bundle), ensure_ascii=not WINDOWS()) + ");"
|
|
180
|
+
source = preset.read_text(encoding="utf-8" if WINDOWS() else None)
|
|
120
181
|
if old_arg not in source:
|
|
121
182
|
raise ValueError("Unsupported preset loader format")
|
|
122
183
|
cleanup = (
|
|
@@ -142,18 +203,20 @@ def install_dsh(root, platform, adapter, settings, profile, previous):
|
|
|
142
203
|
write_owned(entry, content, marker)
|
|
143
204
|
if not link.exists():
|
|
144
205
|
link.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
-
link
|
|
206
|
+
link_directory(link, bundle)
|
|
146
207
|
if bundle_name not in bundles:
|
|
147
208
|
bundles.append(bundle_name)
|
|
148
209
|
deps[bundle_name] = owned_dep
|
|
149
|
-
if manifest.read_text() != original:
|
|
210
|
+
if manifest.read_text(encoding="utf-8" if WINDOWS() else None) != original:
|
|
150
211
|
raise ValueError("Concurrent DSH configuration change")
|
|
151
212
|
fs.write(manifest, config)
|
|
152
213
|
try:
|
|
153
|
-
composed = run(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
214
|
+
composed = run(*command, "--profile", profile, "--dump-config")
|
|
215
|
+
# The composed tree may print paths with either separator.
|
|
216
|
+
flat = composed.replace("\\\\", "/").replace("\\", "/")
|
|
217
|
+
if not any(
|
|
218
|
+
(str(p).replace("\\", "/") in flat or (WINDOWS() and p.as_uri() in composed))
|
|
219
|
+
for p in (entry, bundle / "presets.mjs")
|
|
157
220
|
):
|
|
158
221
|
raise ValueError("DSH did not resolve the AFU provider")
|
|
159
222
|
except Exception:
|
|
@@ -194,8 +257,8 @@ def remove_record(host, record):
|
|
|
194
257
|
fs.write(manifest, config)
|
|
195
258
|
# Keep bundle files for running sessions (some presets resolve lazily).
|
|
196
259
|
link = manifest.parent / "node_modules" / name
|
|
197
|
-
if link
|
|
198
|
-
link
|
|
260
|
+
if is_link(link) and link.resolve() == Path(record["bundle"]).resolve():
|
|
261
|
+
unlink_directory(link)
|
|
199
262
|
|
|
200
263
|
|
|
201
264
|
def main():
|
|
@@ -300,6 +363,7 @@ def main():
|
|
|
300
363
|
"window": window,
|
|
301
364
|
"ratio": ratio,
|
|
302
365
|
"build": fs.read(platform / "release.json")["build"],
|
|
366
|
+
"version": fs.read(platform / "release.json").get("version"),
|
|
303
367
|
"autoload": registrations,
|
|
304
368
|
},
|
|
305
369
|
)
|
package/bin/install-guide.mjs
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import {spawnSync} from 'node:child_process';
|
|
2
|
-
import {accessSync,statSync,constants,mkdirSync,rmSync,mkdtempSync} from 'node:fs';
|
|
3
|
-
import {join,delimiter,resolve} from 'node:path';
|
|
2
|
+
import {accessSync,statSync,constants,mkdirSync,rmSync,mkdtempSync,readFileSync,realpathSync} from 'node:fs';
|
|
3
|
+
import {join,delimiter,resolve,dirname,relative,isAbsolute} from 'node:path';
|
|
4
4
|
import {homedir} from 'node:os';
|
|
5
5
|
import {createInterface} from 'node:readline/promises';
|
|
6
|
+
export function hostCommand(host,env=process.env,platform=process.platform){
|
|
7
|
+
if(platform!=='win32')return [host];
|
|
8
|
+
const packages={pi:'@earendil-works/pi-coding-agent',dsh:'@deepseek-ai/dsh'};
|
|
9
|
+
if(!packages[host])return [host];
|
|
10
|
+
for(const dir of (env.PATH??env.Path??'').split(delimiter)){
|
|
11
|
+
for(const ext of ['.exe','.cmd','.bat']){
|
|
12
|
+
const binary=join(dir,host+ext);
|
|
13
|
+
try{if(!statSync(binary).isFile())continue;}catch{continue;}
|
|
14
|
+
if(ext==='.exe')return [binary];
|
|
15
|
+
const folder=dirname(realpathSync(binary));
|
|
16
|
+
for(const base of [join(folder,'node_modules',packages[host]),join(folder,'..',packages[host])]){
|
|
17
|
+
let data;try{data=JSON.parse(readFileSync(join(base,'package.json'),'utf8'));}catch(e){if(e.code==='ENOENT')continue;throw e;}
|
|
18
|
+
const entry=typeof data.bin==='string'?data.bin:data.bin?.[host];
|
|
19
|
+
if(data.name!==packages[host]||typeof entry!=='string')continue;
|
|
20
|
+
const target=realpathSync(resolve(base,entry)),path=relative(realpathSync(base),target);
|
|
21
|
+
if(path==='..'||path.startsWith('..\\')||path.startsWith('../')||isAbsolute(path)||!statSync(target).isFile())throw Error('INVALID_HOST_ENTRY');
|
|
22
|
+
return [process.execPath,target];
|
|
23
|
+
}
|
|
24
|
+
throw Error('HOST_NPM_ENTRY_UNAVAILABLE');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw Error('HOST_EXECUTABLE_UNAVAILABLE');
|
|
28
|
+
}
|
|
6
29
|
export function hostAvailable(host,env=process.env){
|
|
7
30
|
return (env.PATH??env.Path??'').split(delimiter).some(dir=>['',...(process.platform==='win32'?['.exe','.cmd']:[])].some(ext=>{try{accessSync(join(dir,host+ext),constants.X_OK);return statSync(join(dir,host+ext)).isFile();}catch{return false;}}));
|
|
8
31
|
}
|
|
@@ -22,7 +45,6 @@ export async function guide(args,env=process.env){
|
|
|
22
45
|
args=[args[0],host,...args.slice(1)];
|
|
23
46
|
}
|
|
24
47
|
if(!hostAvailable(host,env))throw Error('HOST_NOT_FOUND: '+host);
|
|
25
|
-
if(process.platform==='win32'&&host==='codex')throw Error('WINDOWS_CODEX_NATIVE_UNSUPPORTED');
|
|
26
48
|
const at=args.indexOf('--state');const state=at>=0?resolve(args[at+1]):env.AFU_STATE_DIR??join(homedir(),'.local/share/afu-context');
|
|
27
49
|
mkdirSync(state,{recursive:true,mode:0o700});const check=mkdtempSync(join(state,'.write-check-'));rmSync(check,{recursive:true});
|
|
28
50
|
if(['claude','codex'].includes(host)&&!args.includes('--no-autostart')){
|
package/bin/platform.mjs
CHANGED
|
@@ -2,13 +2,13 @@ import {obtainPack} from './prebuilt.mjs';
|
|
|
2
2
|
import {runInstall} from './install-process.mjs';
|
|
3
3
|
/** Host-only dependencies live outside the installed npm package. */
|
|
4
4
|
import {mkdirSync,readFileSync,writeFileSync,existsSync,rmSync,cpSync,renameSync,realpathSync} from 'node:fs';
|
|
5
|
-
import {join,dirname,delimiter} from 'node:path';
|
|
5
|
+
import {join,dirname,delimiter,basename} from 'node:path';
|
|
6
6
|
import {homedir} from 'node:os';
|
|
7
7
|
|
|
8
8
|
import {spawnSync} from 'node:child_process';
|
|
9
9
|
import {pathToFileURL} from 'node:url';
|
|
10
10
|
import {randomUUID} from 'node:crypto';
|
|
11
|
-
import {recoverLock} from './runtime.mjs';
|
|
11
|
+
import {recoverLock,importProbeSucceeded} from './runtime.mjs';
|
|
12
12
|
const read=p=>{try{return JSON.parse(readFileSync(p,'utf8'));}catch{return null;}};
|
|
13
13
|
export function npmInvocation(env=process.env,platform=process.platform){
|
|
14
14
|
if(platform!=='win32')return ['npm',[]];
|
|
@@ -28,7 +28,7 @@ export async function preparePlatform(root,manifest,args,env=process.env){
|
|
|
28
28
|
let target=canonical;
|
|
29
29
|
const refresh=()=>{const name=read(pointer)?.name;target=typeof name==='string'&&name.startsWith(manifest.build+'-'+host)&&!/[\\/]/.test(name)?join(base,name):canonical;};
|
|
30
30
|
refresh();
|
|
31
|
-
const ready=()=>read(join(target,'platform-ready.json'))?.build===manifest.build && Object.keys(dependencies).every(name=>existsSync(join(target,'node_modules',name,'package.json'))) && spawnSync(process.execPath,['--input-type=module','-e','await import(process.argv[1])',pathToFileURL(join(target,'src/afu_host_integrations',host,'dist/index.js')).href],{env,stdio:'ignore',timeout:10000})
|
|
31
|
+
const ready=()=>read(join(target,'platform-ready.json'))?.build===manifest.build && Object.keys(dependencies).every(name=>existsSync(join(target,'node_modules',name,'package.json'))) && importProbeSucceeded(spawnSync(process.execPath,['--input-type=module','-e','await import(process.argv[1])',pathToFileURL(join(target,'src/afu_host_integrations',host,'dist/index.js')).href],{env,stdio:'ignore',timeout:process.platform==='win32'?30000:10000}));
|
|
32
32
|
mkdirSync(base,{recursive:true,mode:0o700});
|
|
33
33
|
const deadline=Date.now()+180000;
|
|
34
34
|
while(true){
|
|
@@ -66,7 +66,7 @@ export async function preparePlatform(root,manifest,args,env=process.env){
|
|
|
66
66
|
// and corrupt transitive dependencies. No model request is made by imports.
|
|
67
67
|
await runInstall(process.execPath,['--input-type=module','-e','await import(process.argv[1])',pathToFileURL(join(target,'src/afu_host_integrations',host,'dist/index.js')).href],env);
|
|
68
68
|
writeFileSync(join(target,'platform-ready.json'),JSON.stringify({build:manifest.build,cache_protocol:1}),{mode:0o600});
|
|
69
|
-
const temp=pointer+'.'+token;writeFileSync(temp,JSON.stringify({name:target.slice(base.length+1)}),{mode:0o600});renameSync(temp,pointer);return target;
|
|
69
|
+
const temp=pointer+'.'+token;writeFileSync(temp,JSON.stringify({name:process.platform==='win32'?basename(target):target.slice(base.length+1)}),{mode:0o600});renameSync(temp,pointer);return target;
|
|
70
70
|
}catch(error){
|
|
71
71
|
if(owns()&&createdTarget)rmSync(createdTarget,{recursive:true,force:true});
|
|
72
72
|
throw error;
|
package/bin/runtime.mjs
CHANGED
|
@@ -33,12 +33,16 @@ export function recoverLock(lock, now=Date.now()) {
|
|
|
33
33
|
renameSync(lock,tomb);moved=true;rmSync(tomb,{recursive:true,force:true});return true;
|
|
34
34
|
}catch{return false;}finally{if(!moved)rmSync(guard,{recursive:true,force:true});}
|
|
35
35
|
}
|
|
36
|
+
export function importProbeSucceeded(result,platform=process.platform){
|
|
37
|
+
if(platform==='win32'&&result.error?.code==='ETIMEDOUT')throw Error('IMPORT_CHECK_TIMED_OUT: existing cache preserved; retry when the machine is less busy');
|
|
38
|
+
return result.status===0;
|
|
39
|
+
}
|
|
36
40
|
export function runtimeReady(location, build) {
|
|
37
41
|
const marker=read(join(location,'afu-ready.json'));
|
|
38
42
|
const python=join(location,process.platform==='win32'?'Scripts/python.exe':'bin/python');
|
|
39
43
|
if(marker?.build!==build || !existsSync(python))return false;
|
|
40
44
|
// A surviving marker is not proof that imports and the interpreter still work.
|
|
41
|
-
return spawnSync(python,['-c','import afu_context.local_cli, aiohttp, tiktoken'],{stdio:'ignore',timeout:10000})
|
|
45
|
+
return importProbeSucceeded(spawnSync(python,['-c','import afu_context.local_cli, aiohttp, tiktoken'],{stdio:'ignore',timeout:process.platform==='win32'?30000:10000}));
|
|
42
46
|
}
|
|
43
47
|
export async function prepareRuntime(root,manifest,env=process.env) {
|
|
44
48
|
const base=env.AFU_RUNTIME_DIR ?? join(homedir(),'.local/share/afu-context/runtimes');
|
package/package.json
CHANGED
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
3
|
-
"build": "
|
|
4
|
-
"wheel": "afu_context_local-0.1.
|
|
2
|
+
"version": "0.1.11",
|
|
3
|
+
"build": "00a744986d028b488f3f9a6396bf06cb1983e95e293eeae624f93aab1119377b",
|
|
4
|
+
"wheel": "afu_context_local-0.1.11-py3-none-any.whl",
|
|
5
5
|
"prebuilt": false
|
|
6
6
|
}
|
package/scripts/dsh-overlay.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/** Compose a disposable overlay using DSH's own YAML and patch semantics. */
|
|
2
2
|
import {readFileSync, writeFileSync, realpathSync} from 'node:fs';
|
|
3
3
|
import {createRequire} from 'node:module';
|
|
4
|
-
import {join} from 'node:path';
|
|
4
|
+
import {join,isAbsolute} from 'node:path';
|
|
5
5
|
import {pathToFileURL} from 'node:url';
|
|
6
6
|
import {load,dump} from 'js-yaml';
|
|
7
7
|
import {entryListSchema,applyEntryPatches} from '@deepseek-ai/cordis-plugin-include';
|
|
8
8
|
|
|
9
|
+
export function moduleReference(value) {
|
|
10
|
+
// COSEC: convert only local absolute paths, never reinterpret package names or URLs.
|
|
11
|
+
return process.platform === 'win32' && isAbsolute(value) ? pathToFileURL(value).href : value;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export function overlay(entries,module,presetModule) {
|
|
10
15
|
if (!Array.isArray(entries)) throw Error('Expected a composed DSH entry list');
|
|
11
16
|
const ids=new Set(), matches=[], presets=[];
|
|
@@ -43,10 +48,10 @@ if(process.argv[1] && import.meta.url===pathToFileURL(process.argv[1]).href) {
|
|
|
43
48
|
try {
|
|
44
49
|
const native=requireHost.resolve('@deepseek-ai/dsh-agent-presets');
|
|
45
50
|
presetModule=join(process.argv[3],'presets.mjs');
|
|
46
|
-
writeFileSync(presetModule, `import Native from ${JSON.stringify(pathToFileURL(native).href)};\nimport {presetAdapter} from ${JSON.stringify(new URL('./dsh-presets.mjs',import.meta.url).href)};\nexport default presetAdapter(Native, ${JSON.stringify(process.argv[2])}, ${JSON.stringify(process.argv[3])});\n`, {mode:0o600});
|
|
51
|
+
writeFileSync(presetModule, `import Native from ${JSON.stringify(pathToFileURL(native).href)};\nimport {presetAdapter} from ${JSON.stringify(new URL('./dsh-presets.mjs',import.meta.url).href)};\nexport default presetAdapter(Native, ${JSON.stringify(moduleReference(process.argv[2]))}, ${JSON.stringify(process.argv[3])});\n`, {mode:0o600});
|
|
47
52
|
} catch (error) { if(error.code!=='MODULE_NOT_FOUND') throw error; }
|
|
48
53
|
}
|
|
49
|
-
const patches=overlay(tree,process.argv[2],presetModule);
|
|
54
|
+
const patches=overlay(tree,moduleReference(process.argv[2]),presetModule && moduleReference(presetModule));
|
|
50
55
|
if (process.argv[5]==='web-compat') {
|
|
51
56
|
const picker=tree.find(row=>!row.disabled && row.name==='@deepseek-ai/dsh-host-directory-picker-auto');
|
|
52
57
|
if (picker) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BasicCompactionEngine, } from "@deepseek-ai/dsh-compaction-basic";
|
|
2
2
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import { journal, sessionHash, summaryPrefix, observationId, HostCompactPool, bindingKey, estimateContentTokens, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
3
|
+
import { journal, sessionHash, afuTrigger, summaryPrefix, observationId, HostCompactPool, bindingKey, estimateContentTokens, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* DSH-native compaction provider. Idle turns prepare Compact Artifacts off-path;
|
|
6
6
|
* the next pre-step commits only exact, still-contiguous coverage through DSH's
|
|
@@ -48,7 +48,17 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
48
48
|
ctx.on("agent/status", async ({ agent, status }) => {
|
|
49
49
|
if (status !== "idle")
|
|
50
50
|
return;
|
|
51
|
-
|
|
51
|
+
// Context size as DSH's own token meter prices it, plus the AFU trigger for the routed model.
|
|
52
|
+
let tokens, window;
|
|
53
|
+
tokens = dshContextTokens(ctx, agent);
|
|
54
|
+
try {
|
|
55
|
+
window = (await dshHostBinding(ctx, agent, () => this.generation)).contextWindow;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
window = undefined;
|
|
59
|
+
}
|
|
60
|
+
this.log("request_observed", { session: sessionHash(agent.session.id), phase: "host_turn_idle",
|
|
61
|
+
...(tokens === undefined ? {} : { estimated_input_tokens: tokens }), ...afuTrigger(window) });
|
|
52
62
|
try {
|
|
53
63
|
const request = await this.boundRequest(agent, agent.session.deriveMessages(), "sync");
|
|
54
64
|
this.idlePrecomputer.schedule(request);
|
|
@@ -58,7 +68,9 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
58
68
|
}
|
|
59
69
|
});
|
|
60
70
|
ctx.effect(() => async () => {
|
|
61
|
-
|
|
71
|
+
// DSH creates and disposes an engine for every agent scope (the web app
|
|
72
|
+
// does so per request); the process lives on. runtime_stopped is logged
|
|
73
|
+
// once, when the process exits (see telemetry.journal).
|
|
62
74
|
// Stop scheduling first so jobs cancelled by shutdown are not reported
|
|
63
75
|
// as preparation failures, then stop the host and let them settle.
|
|
64
76
|
this.idlePrecomputer.stop();
|
|
@@ -78,7 +90,9 @@ export class CompactDshEngine extends BasicCompactionEngine {
|
|
|
78
90
|
if (!this.bindings.has(binding.key) && this.bindings.size >= 16)
|
|
79
91
|
throw new Error("DSH binding limit reached");
|
|
80
92
|
this.bindings.set(binding.key, binding);
|
|
81
|
-
|
|
93
|
+
const request = dshMessagesToRequest(messages, binding.key, mode);
|
|
94
|
+
const tokens = dshContextTokens(this.ctx, agent);
|
|
95
|
+
return tokens === undefined ? request : { ...request, metadata: { ...request.metadata, host_context_tokens: tokens } };
|
|
82
96
|
}
|
|
83
97
|
async compactIfNeeded(agent, trigger, signal) {
|
|
84
98
|
const started = performance.now();
|
|
@@ -234,6 +248,17 @@ export async function dshHostBinding(ctx, agent, generation) {
|
|
|
234
248
|
},
|
|
235
249
|
};
|
|
236
250
|
}
|
|
251
|
+
/** DSH's own token meter (the measure its native compaction uses), if available. */
|
|
252
|
+
function dshContextTokens(ctx, agent) {
|
|
253
|
+
try {
|
|
254
|
+
const meter = ctx.tokenMeter;
|
|
255
|
+
const tokens = meter?.measure(agent.session).totalTokens;
|
|
256
|
+
return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
237
262
|
export function dshMessagesToRequest(messages, sessionId, mode = "active") {
|
|
238
263
|
const groups = toolCallGroups(messages);
|
|
239
264
|
const items = messages.map((message) => {
|