feat(python-sdk): support bundled preset runtime dependencies

This commit is contained in:
fz
2026-08-14 13:06:21 +08:00
parent 39ada2e763
commit e20f560992
18 changed files with 410 additions and 50 deletions
+11 -7
View File
@@ -19,6 +19,7 @@
* @module @deepseek-ai/dsh-tool-fs-search/search-core
*/
import { existsSync } from 'node:fs'
import { isAbsolute, relative, sep } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
@@ -158,18 +159,21 @@ let rgPathPromise: Promise<string> | undefined
/**
* The packaged ripgrep binary path, resolved lazily once per process.
*
* `@vscode/ripgrep` resolves its platform package (`@vscode/ripgrep-<platform>
* -<arch>`) at module evaluation, so a static import would turn a missing or
* corrupt platform package (`pnpm install --omit=optional`, partial install)
* into a failure of the whole Loader composition. Resolving at the call
* boundary keeps that failure at the first search call as `SEARCH_FAILED` —
* the package's documented no-load-time-probe contract.
* A single-file runtime uses the executable's `-rg` sidecar because a native
* helper cannot be spawned from pkg's virtual filesystem. Node-mode builds
* fall back to the platform package selected by `@vscode/ripgrep`. Resolving
* at the call boundary keeps a missing or corrupt binary at the first search
* call as `SEARCH_FAILED`, rather than failing the Loader composition.
*
* @returns the packaged binary's absolute path; the memoized promise rejects
* when the platform package cannot be resolved.
*/
export function resolveRgPath(): Promise<string> {
rgPathPromise ??= import('@vscode/ripgrep').then(module => module.rgPath)
rgPathPromise ??= Promise.resolve().then(async () => {
const executableSidecar = `${process.execPath}-rg`
if (existsSync(executableSidecar)) return executableSidecar
return (await import('@vscode/ripgrep')).rgPath
})
return rgPathPromise
}
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
import type { TerminalReadResult, TerminalSendResult, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -15,7 +16,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
const TRUNCATED_MESSAGE = '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>'
const LOST_PREFIX_MESSAGE = '<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n'
const SHELL_RESET_MESSAGE = 'The persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.'
const SHELL_PROMPT = '__DSH_PERSISTENT_BASH_PROMPT__ '
const SHELL_PROMPT = CONTROLLED_PROMPT
const TIMEOUT_CODE = 'PERSISTENT_BASH_TIMEOUT'
// One page is enough to find a just-emitted completion marker; the full
// scrollback is assembled only when a command settles or needs partial output.
@@ -84,8 +84,8 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
' config:',
' pollIntervalMs: 10',
' exactProbeAfterMs: 20',
' idleSilenceMs: 100',
' handoffGraceMs: 100',
' idleSilenceMs: 3000',
' handoffGraceMs: 500',
' scrollbackLines: 20000',
' timeoutMs: 2000',
' disposeGraceMs: 500',
@@ -131,7 +131,9 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
})
expect(context.tools.schemas().map(schema => schema.name)).toEqual(['bash'])
const startedAt = Date.now()
await execute('state', 'export KEEP=loader; mkdir -p nested; cd nested')
expect(Date.now() - startedAt).toBeLessThan(2_000)
const observed = text(await execute('observe', 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"'))
expect(observed).toContain(`cwd=${join(root, 'nested')} keep=loader`)
expect(observed).not.toContain('DSH_PERSISTENT_BASH')
@@ -4,7 +4,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import TerminalSessionService from '@deepseek-ai/dsh-terminal'
import TerminalSessionService, { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
import type {
TerminalBackend,
TerminalBackendSession,
@@ -100,7 +100,7 @@ type StubMode =
| 'paged-scrollback'
class StubPtySession implements TerminalBackendSession {
readonly motd = '__DSH_PERSISTENT_BASH_PROMPT__ '
readonly motd = CONTROLLED_PROMPT
readonly pid = 123
statusValue: TerminalSessionStatus = { kind: 'running' }
scrollback = this.motd
@@ -13,9 +13,9 @@ export interface ProcessIdentity {
/** Injectable OS process operations used by one local PTY session. */
export interface ProcessInspector {
foregroundPgid(shellPid: number): number | undefined
isStdinWaiting(pgid: number): boolean
isStdinWaiting(pgid: number, scanNamespace?: boolean): boolean
/** Return the root and its current transitive descendants, children first. */
processTree(rootPid: number): ProcessIdentity[]
processTree(rootPid: number, scanNamespace?: boolean): ProcessIdentity[]
/** Return current members of one POSIX process session when the platform exposes them. */
processSession(sessionId: number): ProcessIdentity[]
/** Return whether the exact identity remains a non-quiescent process. */
@@ -226,12 +226,26 @@ function syscallWaitsOnStdin(
return false
}
function processWaitsOnStdin(
internals: ProcessInspectorInternals,
pid: number,
processGroupId: number,
table: SyscallTable,
): boolean {
if (readLinuxStat(internals, pid)?.pgrp !== processGroupId) return false
for (const tid of numericEntries(internals, `/proc/${pid}/task`)) {
const syscall = readSyscall(internals, pid, tid)
if (syscall !== undefined && syscallWaitsOnStdin(internals, pid, syscall, table)) return true
}
return false
}
abstract class PosixProcessInspector implements ProcessInspector {
constructor(protected readonly internals: ProcessInspectorInternals) {}
abstract foregroundPgid(shellPid: number): number | undefined
abstract isStdinWaiting(pgid: number): boolean
abstract processTree(rootPid: number): ProcessIdentity[]
abstract isStdinWaiting(pgid: number, scanNamespace?: boolean): boolean
abstract processTree(rootPid: number, scanNamespace?: boolean): ProcessIdentity[]
abstract processSession(sessionId: number): ProcessIdentity[]
abstract isAlive(identity: ProcessIdentity): boolean
@@ -270,6 +284,34 @@ function processTree(entries: ProcessTreeEntry[], rootPid: number): ProcessIdent
return result
}
function linuxProcessTreeFromChildren(
internals: ProcessInspectorInternals,
rootPid: number,
): ProcessIdentity[] | undefined {
const root = readLinuxStat(internals, rootPid)
if (root === undefined) return []
const visited = new Set<number>()
const result: ProcessIdentity[] = []
const visit = (entry: ProcStat): boolean => {
if (visited.has(entry.pid)) return true
visited.add(entry.pid)
let children: string
try {
children = internals.readFile(`/proc/${entry.pid}/task/${entry.pid}/children`)
} catch (_unreadableChildren) {
return false
}
for (const token of children.trim().split(/\s+/)) {
if (token.length === 0 || !/^\d+$/.test(token)) continue
const child = readLinuxStat(internals, Number(token))
if (child !== undefined && !visit(child)) return false
}
result.push({ pid: entry.pid, started: entry.started })
return true
}
return visit(root) ? result : undefined
}
class LinuxProcessInspector extends PosixProcessInspector {
constructor(
private readonly arch: NodeJS.Architecture,
@@ -283,20 +325,32 @@ class LinuxProcessInspector extends PosixProcessInspector {
return tpgid !== undefined && tpgid > 0 ? tpgid : undefined
}
isStdinWaiting(pgid: number): boolean {
isStdinWaiting(pgid: number, scanNamespace = true): boolean {
const table = SYSCALLS[this.arch]
if (table === undefined) return false
// A POSIX process group is normally led by PID == PGID. Interactive shells
// wait on stdin in that leader, so inspect it before walking the whole PID
// namespace. Large container PID namespaces otherwise make every PTY
// readiness poll scan thousands of unrelated processes.
if (processWaitsOnStdin(this.internals, pgid, pgid, table)) return true
if (!scanNamespace) return false
for (const pid of numericEntries(this.internals, '/proc')) {
if (readLinuxStat(this.internals, pid)?.pgrp !== pgid) continue
for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
const syscall = readSyscall(this.internals, pid, tid)
if (syscall !== undefined && syscallWaitsOnStdin(this.internals, pid, syscall, table)) return true
}
if (pid === pgid) continue
if (processWaitsOnStdin(this.internals, pid, pgid, table)) return true
}
return false
}
processTree(rootPid: number): ProcessIdentity[] {
processTree(rootPid: number, scanNamespace = true): ProcessIdentity[] {
// Linux exposes each process's direct children without requiring a scan of
// the container's whole PID namespace. Fall back for kernels or procfs
// mounts that do not provide the children file.
const rooted = linuxProcessTreeFromChildren(this.internals, rootPid)
if (rooted !== undefined) return rooted
if (!scanNamespace) {
const root = readLinuxStat(this.internals, rootPid)
return root === undefined ? [] : [{ pid: root.pid, started: root.started }]
}
const entries = numericEntries(this.internals, '/proc').flatMap((pid) => {
const stat = readLinuxStat(this.internals, pid)
return stat === undefined ? [] : [{ pid, parentPid: stat.parentPid, started: stat.started }]
@@ -338,11 +392,11 @@ class MacProcessInspector extends PosixProcessInspector {
}
}
isStdinWaiting(_pgid: number): boolean {
isStdinWaiting(_pgid: number, _scanNamespace = true): boolean {
return false
}
processTree(rootPid: number): ProcessIdentity[] {
processTree(rootPid: number, _scanNamespace = true): ProcessIdentity[] {
return processTree(macProcessTable(this.internals), rootPid)
}
@@ -57,7 +57,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
private readonly graceMs: number,
) {
this.pid = terminal.pid
this.rootIdentity = inspector.processTree(this.pid).find(member => member.pid === this.pid)
this.rootIdentity = inspector.processTree(this.pid, false).find(member => member.pid === this.pid)
this.done = this.outcome.promise
this.dataDisposable = terminal.onData((data) => { this.output.write(Buffer.from(data, 'utf8')) })
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
@@ -81,12 +81,14 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
// Local inspection is synchronous; the seam returns a promise for remote transports.
// oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract.
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
this.descendants()
// Readiness polling may run every few milliseconds. Track the rooted tree
// here, but reserve the full process-session sweep for teardown.
this.descendants(false, false)
const processGroupId = this.inspector.foregroundPgid(this.pid)
if (processGroupId === undefined) return undefined
return {
processGroupId,
inputWaiting: this.inspector.isStdinWaiting(processGroupId),
inputWaiting: this.inspector.isStdinWaiting(processGroupId, false),
}
}
@@ -141,20 +143,22 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
return members.filter(member => this.inspector.isAlive(member))
}
private descendants(): ProcessIdentity[] {
private descendants(includeSession = true, scanNamespace = true): ProcessIdentity[] {
// Adopt newly scanned members only while the numeric root pid provably
// still carries the spawned shell's start identity: after the shell dies,
// a recycled pid's tree and session must not donate an unrelated
// process's children to this session's signalling. Already-adopted
// members keep their own start identities, which every signal rechecks.
const tree = this.inspector.processTree(this.pid)
const tree = this.inspector.processTree(this.pid, scanNamespace)
const root = tree.find(member => member.pid === this.pid)
const rootVerified = this.rootIdentity !== undefined
&& root !== undefined
&& root.started === this.rootIdentity.started
this.trackedDescendants = this.survivors(this.unionMembers(
this.trackedDescendants,
...rootVerified ? [tree, this.inspector.processSession(this.pid)] : [],
...rootVerified
? [tree, ...includeSession ? [this.inspector.processSession(this.pid)] : []]
: [],
).filter(member => member.pid !== this.pid))
return this.trackedDescendants
}
@@ -95,6 +95,9 @@ describe('Linux process inspector', () => {
fake.files.set('/proc/11/stat', stat(11, 21, 30, -1, '501'))
fake.files.set('/proc/12/stat', stat(12, 22, 30, -1, '502', 10))
fake.files.set('/proc/13/stat', stat(13, 23, 30, -1, '503', 12))
fake.files.set('/proc/10/task/10/children', '12')
fake.files.set('/proc/12/task/12/children', '13')
fake.files.set('/proc/13/task/13/children', '')
const inspector = createProcessInspector('linux', 'x64', fake.internals)
expect(inspector.foregroundPgid(10)).toBe(40)
expect(inspector.foregroundPgid(11)).toBeUndefined()
@@ -124,6 +127,28 @@ describe('Linux process inspector', () => {
expect(fake.kills).toEqual([[-40, 'SIGINT'], [10, 'SIGTERM']])
})
it('walks a rooted process tree without enumerating the PID namespace', () => {
const fake = fakeInternals()
fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
fake.files.set('/proc/10/task/10/children', '11')
fake.files.set('/proc/11/stat', stat(11, 10, 10, 10, '501', 10))
fake.files.set('/proc/11/task/11/children', '')
expect(createProcessInspector('linux', 'x64', fake.internals).processTree(10)).toEqual([
{ pid: 11, started: '501' },
{ pid: 10, started: '500' },
])
})
it('keeps readiness inspection local when procfs has no children index', () => {
const fake = fakeInternals()
fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
const inspector = createProcessInspector('linux', 'x64', fake.internals)
expect(inspector.processTree(10, false)).toEqual([{ pid: 10, started: '500' }])
expect(inspector.isStdinWaiting(10, false)).toBe(false)
})
it('detects read, select, poll, and epoll waits across non-leader threads', () => {
const fake = fakeInternals()
fake.dirs.set('/proc', ['100', '101'])
@@ -156,6 +181,16 @@ describe('Linux process inspector', () => {
expect(inspector.isStdinWaiting(77)).toBe(true)
})
it('checks a waiting process-group leader without scanning the PID namespace', () => {
const fake = fakeInternals()
fake.files.set('/proc/77/stat', stat(77, 77, 77, 77, '1'))
fake.dirs.set('/proc/77/task', ['77'])
fake.files.set('/proc/77/task/77/syscall', syscall(0, 0))
const inspector = createProcessInspector('linux', 'x64', fake.internals)
expect(inspector.isStdinWaiting(77)).toBe(true)
})
it('fails closed on unsupported, malformed, unreadable, or non-stdin waits', () => {
const fake = fakeInternals()
fake.dirs.set('/proc', ['100'])
@@ -2,12 +2,11 @@
import { Buffer } from 'node:buffer'
export { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
/** OSC marker emitted by the controlled bash before each prompt. */
export const PROMPT_MARKER_PREFIX = '133;D;'
/** Exact printable prompt emitted after the private marker. */
export const CONTROLLED_PROMPT = 'dsh> '
/** One sanitized chunk plus whether it contained the owned prompt marker. */
export interface SanitizedChunk {
text: string
+3
View File
@@ -42,6 +42,9 @@ export type {
} from './types.ts'
export { TerminalBackendCleanupError } from './types.ts'
/** Printable prompt shared by terminal backends and persistent shell consumers. */
export const CONTROLLED_PROMPT = 'dsh> '
/** Opaque identity minted by {@link TerminalSessionService} for one live PTY session. */
export type TerminalSessionId = TerminalSessionIdValue
+18
View File
@@ -8340,6 +8340,9 @@ importers:
'@deepseek-ai/dsh-agent-spine-demo':
specifier: workspace:^
version: link:../../packages/examples/agent-spine-demo
'@deepseek-ai/dsh-agent-tool-presentation':
specifier: workspace:^
version: link:../../packages/core/agent-tool-presentation
'@deepseek-ai/dsh-anonymous-user-id':
specifier: workspace:^
version: link:../../packages/identity/anonymous-user-id
@@ -8361,6 +8364,9 @@ importers:
'@deepseek-ai/dsh-code-runtime-worker-thread':
specifier: workspace:^
version: link:../../packages/code-runtime/code-runtime-worker-thread
'@deepseek-ai/dsh-command-compact':
specifier: workspace:^
version: link:../../packages/compaction/command-compact
'@deepseek-ai/dsh-command-goal':
specifier: workspace:^
version: link:../../packages/goal/command-goal
@@ -8442,6 +8448,9 @@ importers:
'@deepseek-ai/dsh-permission-presets':
specifier: workspace:^
version: link:../../packages/interaction/permission-presets
'@deepseek-ai/dsh-persona':
specifier: workspace:^
version: link:../../packages/preset/persona
'@deepseek-ai/dsh-plan-mode':
specifier: workspace:^
version: link:../../packages/plan/plan-mode
@@ -8514,6 +8523,9 @@ importers:
'@deepseek-ai/dsh-skill-filesystem':
specifier: workspace:^
version: link:../../packages/skill/skill-filesystem
'@deepseek-ai/dsh-spill':
specifier: workspace:^
version: link:../../packages/spill/spill
'@deepseek-ai/dsh-subagent':
specifier: workspace:^
version: link:../../packages/subagent/subagent
@@ -8568,12 +8580,18 @@ importers:
'@deepseek-ai/dsh-tool-fs':
specifier: workspace:^
version: link:../../packages/fs/tool-fs
'@deepseek-ai/dsh-tool-fs-search':
specifier: workspace:^
version: link:../../packages/fs/tool-fs-search
'@deepseek-ai/dsh-tool-goal':
specifier: workspace:^
version: link:../../packages/goal/tool-goal
'@deepseek-ai/dsh-tool-jobs':
specifier: workspace:^
version: link:../../packages/jobs/tool-jobs
'@deepseek-ai/dsh-tool-ralph':
specifier: workspace:^
version: link:../../packages/workflow/tool-ralph
'@deepseek-ai/dsh-tool-skill':
specifier: workspace:^
version: link:../../packages/skill/tool-skill
+1 -1
View File
@@ -67,7 +67,7 @@ class RuntimeBuildHook(BuildHookInterface):
expected_executable = matches[0][1]
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
expected_files = [expected_executable]
expected_files = [expected_executable, f"{expected_executable}-rg"]
if "-macos-" in expected_executable:
expected_files.append(f"{expected_executable}-spawn-helper")
found_files = [path.name for path in runtime_files]
+6
View File
@@ -14,6 +14,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-shell": "workspace:^",
@@ -22,6 +23,7 @@
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^",
"@deepseek-ai/dsh-command-compact": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-compaction": "workspace:^",
@@ -49,6 +51,7 @@
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-permission-presets": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-persona": "workspace:^",
"@deepseek-ai/dsh-terminal": "workspace:^",
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
"@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
@@ -71,6 +74,7 @@
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
"@deepseek-ai/dsh-spill": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
@@ -89,7 +93,9 @@
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
"@deepseek-ai/dsh-tool-fs": "workspace:^",
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
"@deepseek-ai/dsh-tool-goal": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
@@ -5,8 +5,8 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
- **exe (production)**: single-file Node executables named
``dsh-jsonrpc-agent-pkg-<platform>-<arch>`` (platform in {linux, macos}, arch in
{x64, arm64}); macOS also uses a sibling ``-spawn-helper``. The target machine
needs no Node installation.
{x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling
``-spawn-helper``. The target machine needs no Node installation.
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
(``package.json`` + ``node_modules/``), executed as ``node
runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`` on a
@@ -83,6 +83,12 @@ def bundled_runtime_path() -> Path:
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
+ _EXE_ACQUISITION_HINT
)
ripgrep = Path(f"{path}-rg")
if not ripgrep.is_file():
raise FileNotFoundError(
f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
+ _EXE_ACQUISITION_HINT
)
if tag.startswith("macos-"):
helper = Path(f"{path}-spawn-helper")
if not helper.is_file():
+4
View File
@@ -92,6 +92,10 @@ def test_stage_runtime_copies_platform_payload(
executable.write_bytes(b"runtime")
executable.chmod(0o755)
expected = {executable.name: b"runtime"}
ripgrep = Path(f"{executable}-rg")
ripgrep.write_bytes(b"ripgrep")
ripgrep.chmod(0o755)
expected[ripgrep.name] = b"ripgrep"
if with_helper:
spawn_helper = Path(f"{executable}-spawn-helper")
spawn_helper.write_bytes(b"helper")
+27 -2
View File
@@ -396,7 +396,8 @@ class SingleExeBuild {
if (!this.cli.dryRun && !existsSync(product)) {
throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
}
if (target.platform !== 'macos') return [product]
const ripgrep = await this.copyRipgrepSidecar(target, product)
if (target.platform !== 'macos') return [product, ripgrep]
const spawnHelper = `${product}-spawn-helper`
const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
if (this.cli.dryRun) {
@@ -405,7 +406,31 @@ class SingleExeBuild {
await copyFile(source, spawnHelper)
await chmod(spawnHelper, 0o755)
}
return [product, spawnHelper]
return [product, ripgrep, spawnHelper]
}
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
const platform = target.platform === 'macos' ? 'darwin' : target.platform
const source = join(
this.staging,
'node_modules',
'@vscode',
`ripgrep-${platform}-${target.arch}`,
'bin',
'rg',
)
const destination = `${product}-rg`
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
return destination
}
if (!existsSync(source)) {
throw new Error(`build-exe-for-python-sdk: target ripgrep binary is missing at ${source}.`)
}
await copyFile(source, destination)
await chmod(destination, 0o755)
return destination
}
/**
+2 -1
View File
@@ -48,7 +48,8 @@ PLATFORMS = load_platforms()
def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
return ("", "-spawn-helper") if "-macos-" in executable_name else ("",)
suffixes = ("", "-rg")
return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
def main() -> None:
+93 -2
View File
@@ -29,6 +29,9 @@ MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent Bash and stri
MINIMAL_TEXT = "minimal agent smoke ok"
MINIMAL_EDITOR_PATH_PREFIX = "Editor path: "
MINIMAL_SYSTEM_PROMPT = "You are a helpful software engineer assistant."
FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools."
FS_SEARCH_TEXT = "filesystem search smoke ok"
FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK"
MINIMAL_CORDIS = (
Path(__file__).resolve().parent.parent / "examples" / "jsonrpc-agent" / "minimal.cordis.yml"
)
@@ -109,6 +112,29 @@ CUSTOM_CORDIS = """\
- id: cordis-tool
name: '@deepseek-ai/dsh-tool-cordis'
"""
FS_SEARCH_CORDIS = """\
- id: sdk-jsonrpc-server
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
- id: agent-core
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
workspaceContext: false
skills:
enabled: false
toolBash: false
toolJobs: false
- id: sessions
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: !!js process.env.DSH_SESSION_ROOT
compression: 'none'
- id: subprocess
name: '@deepseek-ai/dsh-subprocess-local'
- id: fs-search
name: '@deepseek-ai/dsh-tool-fs-search'
config:
sampleOverCapGlobResults: false
"""
class MockModelHandler(BaseHTTPRequestHandler):
"""Return deterministic text, worker, and orchestration completions."""
@@ -143,6 +169,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
if latest.get("role") == "tool":
call_id, tool_name = latest_tool_call(messages)
tool_text = message_text(latest.get("content"))
fs_search = fs_search_tool_followup(call_id, tool_name, tool_text)
if fs_search is not None:
return fs_search
minimal = minimal_tool_followup(body, call_id, tool_name, tool_text)
if minimal is not None:
return minimal
@@ -192,6 +221,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
SNAPSHOT_PROMPT,
CODE_PROMPT,
WORKFLOW_PROMPT,
FS_SEARCH_PROMPT,
}
prompt = next(
(candidate for candidate in user_prompts if candidate in scenario_prompts),
@@ -233,9 +263,40 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
},
},
)
if prompt == FS_SEARCH_PROMPT:
assert_advertised_tool(body, "grep")
assert_advertised_tool(body, "glob")
return tool_call_chunks(
"fs-search-grep",
"grep",
{"pattern": FS_SEARCH_MARKER, "path": "."},
)
return text_chunks(EXPECTED_TEXT)
def fs_search_tool_followup(
call_id: str,
tool_name: str,
tool_text: str,
) -> list[dict[str, object]] | None:
"""Exercise both ripgrep-backed tools through the packaged executable."""
if not call_id.startswith("fs-search-"):
return None
if call_id == "fs-search-grep" and tool_name == "grep":
if "needle.txt" not in tool_text or FS_SEARCH_MARKER not in tool_text:
raise AssertionError(f"packaged grep returned no marker: {tool_text}")
return tool_call_chunks(
"fs-search-glob",
"glob",
{"pattern": "**/*.txt"},
)
if call_id == "fs-search-glob" and tool_name == "glob":
if "needle.txt" not in tool_text:
raise AssertionError(f"packaged glob returned no fixture path: {tool_text}")
return text_chunks(FS_SEARCH_TEXT)
raise AssertionError(f"unexpected filesystem-search follow-up: {call_id} {tool_name}: {tool_text}")
def minimal_tool_followup(
body: dict[str, object],
call_id: str,
@@ -477,13 +538,13 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--scenario",
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"),
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "direct"),
default="all",
)
parser.add_argument("--exe", type=Path)
parser.add_argument("--update-snapshots", action="store_true")
args = parser.parse_args()
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"} and args.exe is None:
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios")
if args.update_snapshots and args.scenario not in {"all", "sdk-snapshot"}:
parser.error("--update-snapshots requires --scenario sdk-snapshot or all")
@@ -499,6 +560,9 @@ def main() -> None:
if args.scenario in {"all", "sdk-minimal"}:
assert args.exe is not None
smoke_sdk_minimal(model.url, args.exe.resolve())
if args.scenario in {"all", "sdk-fs-search"}:
assert args.exe is not None
smoke_sdk_fs_search(model.url, args.exe.resolve())
if args.scenario in {"all", "sdk-snapshot"}:
assert args.exe is not None
smoke_sdk_snapshot(model.url, args.exe.resolve(), args.update_snapshots)
@@ -588,6 +652,33 @@ def smoke_sdk_minimal(base_url: str, executable: Path) -> None:
assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2 CWD=/tmp")
def smoke_sdk_fs_search(base_url: str, executable: Path) -> None:
"""Exercise real grep and glob spawns through the packaged executable."""
from deepseek_harness import DeepSeekHarness
with tempfile.TemporaryDirectory(prefix="dsh-sdk-fs-search-") as temporary:
root = Path(temporary).resolve()
(root / "needle.txt").write_text(f"{FS_SEARCH_MARKER}\n")
sessions = root / "sessions"
cordis = root / "cordis.yml"
cordis.write_text(FS_SEARCH_CORDIS)
with DeepSeekHarness(
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
session_root=str(sessions),
cordis=str(cordis),
runtime_bin=str(executable),
api_key="sk-keyless-smoke",
base_url=base_url,
request_timeout_seconds=60,
) as harness:
result = harness.run(FS_SEARCH_PROMPT, session_id="fs-search-smoke")
assert result.final_response == FS_SEARCH_TEXT, result.final_response
assert_session_log(sessions, root, FS_SEARCH_TEXT, FS_SEARCH_MARKER, "needle.txt")
def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None:
"""Drive and compare the advanced SDK/executable behavioral snapshot."""
from deepseek_harness import DeepSeekHarness
+115 -8
View File
@@ -1,12 +1,18 @@
/**
* Verify that the executable deploy manifest supplies every required workspace
* peer in its dependency graph. With auto peer installation disabled, a missing
* root peer can otherwise fail only when Cordis loads the packaged plugin.
* Verify that the executable deploy manifest supplies every plugin referenced
* by a shipped agent preset and every required workspace peer in its dependency
* graph. With auto peer installation disabled, either omission can otherwise
* fail only when Cordis loads the packaged plugin.
*/
import { globSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { basename, dirname, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import * as yaml from 'js-yaml'
interface JsExpr {
__jsExpr: string
}
interface PackageManifest {
name?: string
@@ -21,6 +27,23 @@ interface WorkspacePackage {
manifest: PackageManifest
}
interface RuntimePlatform {
tag: string
executable: string
}
type RuntimePlatformManifest = Record<string, RuntimePlatform>
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
kind: 'scalar',
resolve: data => typeof data === 'string',
construct: (data: unknown): JsExpr => {
if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
return { __jsExpr: data }
},
})
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
const root = resolve(import.meta.dirname, '..')
const { values } = parseArgs({
args: process.argv.slice(2),
@@ -31,6 +54,7 @@ const runtimeManifest = await loadManifest(runtimeManifestPath)
const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
const workspace = await loadWorkspacePackages()
const runtimeDependencies = runtimeManifest.dependencies ?? {}
const platforms = await loadJson<RuntimePlatformManifest>(resolve(root, 'python/sdk-runtime/platforms.json'))
const parents = new Map<string, string | undefined>()
const queue: string[] = []
@@ -40,7 +64,7 @@ for (const dependency of Object.keys(runtimeDependencies).sort()) {
queue.push(dependency)
}
const failures: string[] = []
const failures = await missingPresetPlugins(runtimeDependencies, platforms)
for (let index = 0; index < queue.length; index += 1) {
const packageName = queue[index]
if (packageName === undefined) continue
@@ -65,12 +89,91 @@ for (let index = 0; index < queue.length; index += 1) {
}
if (failures.length > 0) {
console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
console.error('verify-runtime-closure: preset plugins or required workspace peers are missing from python/sdk-runtime dependencies:')
for (const failure of failures) console.error(` ${failure}`)
process.exit(1)
}
console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
const presetCount = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).length
console.log(
`verify-runtime-closure: ${presetCount} agent presets and ${queue.length} workspace packages form a closed runtime dependency graph.`,
)
async function missingPresetPlugins(
runtimeDependencies: Readonly<Record<string, string>>,
platforms: RuntimePlatformManifest,
): Promise<string[]> {
const missing = new Map<string, Set<string>>()
const failures: string[] = []
const presetPaths = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).sort()
for (const presetPath of presetPaths) {
const document: unknown = yaml.load(await readFile(resolve(root, presetPath), 'utf8'), { schema })
if (!Array.isArray(document)) {
failures.push(`${presetPath}: preset root must be a Loader entry array`)
continue
}
for (const target of Object.keys(platforms).sort()) {
const processPlatform = processPlatformForTarget(target)
for (const plugin of activeBarePluginPackages(document, processPlatform)) {
if (runtimeDependencies[plugin] !== undefined) continue
const preset = basename(dirname(presetPath))
const key = `${preset} preset -> ${plugin}`
const targets = missing.get(key) ?? new Set<string>()
targets.add(target)
missing.set(key, targets)
}
}
}
failures.push(...[...missing.entries()].map(([chain, targets]) =>
`${chain} (${[...targets].sort().join(', ')})`))
return failures
}
function activeBarePluginPackages(entries: unknown[], processPlatform: string): Set<string> {
const packages = new Set<string>()
const visit = (value: unknown, parentDisabled: boolean): void => {
if (!isRecord(value)) return
const disabled = parentDisabled || disabledOnPlatform(value.disabled, processPlatform)
if (disabled) return
if (typeof value.name === 'string') {
const packageName = barePackageName(value.name)
if (packageName !== undefined) packages.add(packageName)
}
if (Array.isArray(value.config)) {
for (const child of value.config) visit(child, disabled)
}
}
for (const entry of entries) visit(entry, false)
return packages
}
function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
if (typeof value === 'boolean') return value
if (!isRecord(value) || typeof value.__jsExpr !== 'string') return false
const match = /^process\.platform\s*(===|!==)\s*(['"])(win32|linux|darwin)\2$/.exec(value.__jsExpr.trim())
if (match === null) return false
const [, operator, , expected] = match
return operator === '===' ? processPlatform === expected : processPlatform !== expected
}
function processPlatformForTarget(target: string): string {
if (target.startsWith('linux-')) return 'linux'
if (target.startsWith('macos-')) return 'darwin'
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
}
function barePackageName(specifier: string): string | undefined {
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':')) return undefined
const parts = specifier.split('/')
if (specifier.startsWith('@')) {
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined
}
return parts[0] || undefined
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
const paths = globSync(['packages/*/*/package.json', 'vendor/*/package.json'], { cwd: root })
@@ -85,7 +188,11 @@ async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
}
async function loadManifest(path: string): Promise<PackageManifest> {
return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
return loadJson<PackageManifest>(path)
}
async function loadJson<T>(path: string): Promise<T> {
return JSON.parse(await readFile(path, 'utf8')) as T
}
function formatChain(