fix(tools): harden persistent tool integrations

This commit is contained in:
Yichen Jiang
2026-07-29 15:21:56 +08:00
parent 73379d9c68
commit 260ea24594
26 changed files with 927 additions and 112 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 6ee4e9d824315bde76b7a534679f018df9a6d3e8
README.zh.md: dc9b6233e7074e7a9b13bf10bcd2f310b0ad7bf3
# pnpm run verify-translation-pairing --write examples/jsonrpc-agent/README.md
README.md: 9a4c715e8988f52b647dbbd6b14a478cc1357d92
README.zh.md: fa792f9cf6bdad4f32a980f478a131f19de97611

View File

@@ -25,3 +25,16 @@ The surrounding runtime also loads JSONL session persistence and automatic conte
| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona |
Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js.
## Persistent tools variant
[`persistent-tools.cordis.yml`](persistent-tools.cordis.yml) is a minimal runnable variant whose model-facing surface is exactly:
- owner-scoped persistent `bash`
- `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`
It composes the real local PTY, filesystem intent policy, and session sandbox policy. The keyless behavior snapshot drives the shipped JSON-RPC runtime through both tools and proves that shell cwd/environment survive across calls:
```bash
pnpm exec vitest run examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts
```

View File

@@ -25,3 +25,16 @@
| `DSH_SYSTEM_PROMPT` | 由部署提供的编码人格 |
通过 Python SDK 的 `cordis` 选项或 `DSH_CORDIS_CONFIG` 传入配置路径。内置可执行文件已携带此文件命名的每个插件;目标机器无需 Node.js。
## 持久工具变体
[`persistent-tools.cordis.yml`](persistent-tools.cordis.yml) 是一个最小可运行变体,面向模型的能力严格只有:
- agent 独占、状态持久的 `bash`
- 提供 `view``create``str_replace``insert``str_replace_editor`
它组合真实本地 PTY、文件系统 intent 策略与 session 沙箱策略。无密钥行为快照会通过正式 JSON-RPC runtime 驱动这两个工具,并验证 shell 的 cwd 与环境变量能跨调用保留:
```bash
pnpm exec vitest run examples/jsonrpc-agent/tests/persistent-tools.snapshot.spec.ts
```

View File

@@ -0,0 +1,58 @@
# Minimal unattended composition for the persistent Bash and string-replace
# editor. It is runnable through the JSON-RPC example runtime and intentionally
# keeps the model-facing surface to exactly these two tools.
- id: jsonrpc
name: '@deepseek-ai/dsh-jsonrpc'
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd()
- id: pty
name: '@deepseek-ai/dsh-pty'
- id: pty-local
name: '@deepseek-ai/dsh-pty-local'
- id: fs-sandbox
name: '@deepseek-ai/dsh-fs-sandbox'
config:
cwd: !!js process.env.DSH_CWD ?? process.cwd()
- id: fs-policy
name: '@deepseek-ai/dsh-fs-policy'
- id: agent-spine
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
includeHarnessIdentity: false
persona: 'You are a helpful software engineer assistant.'
workspaceContext: false
skills:
enabled: false
toolBash: false
toolTasks: false
- id: persistent-bash
name: '@deepseek-ai/dsh-tool-bash-persistent'
- id: str-replace-editor
name: '@deepseek-ai/dsh-tool-str-replace-editor'
- id: sessions
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
compression: none

View File

@@ -0,0 +1,214 @@
import { createServer } from 'node:http'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
const configPath = fileURLToPath(new URL('../persistent-tools.cordis.yml', import.meta.url))
const runtimeBin = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url))
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const expectedPath = fileURLToPath(new URL('./snapshots/persistent-tools/behavior.expected.json', import.meta.url))
interface ModelRequest {
messages?: Array<Record<string, unknown>>
tools?: Array<{ function?: { name?: string; parameters?: { required?: string[] } } }>
}
function sseToolCall(id: string, name: string, args: Record<string, unknown>): string[] {
return [
'data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n',
`data: ${JSON.stringify({
choices: [{
delta: {
tool_calls: [{
index: 0,
id,
type: 'function',
function: { name, arguments: JSON.stringify(args) },
}],
},
}],
})}\n\n`,
'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":3,"completion_tokens":3}}\n\n',
'data: [DONE]\n\n',
]
}
function sseText(text: string): string[] {
return [
'data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n',
`data: ${JSON.stringify({ choices: [{ delta: { content: text } }] })}\n\n`,
'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":3}}\n\n',
'data: [DONE]\n\n',
]
}
function messageText(content: unknown): string {
if (typeof content === 'string') return content
if (!Array.isArray(content)) return ''
return content.flatMap((block) => {
if (typeof block !== 'object' || block === null) return []
const text = (block as { text?: unknown }).text
return typeof text === 'string' ? [text] : []
}).join('')
}
function latestToolCall(messages: Array<Record<string, unknown>>): { id: string; name: string } {
for (const message of messages.toReversed()) {
const calls = message.tool_calls
if (!Array.isArray(calls)) continue
const call = (calls as unknown[]).at(-1)
if (typeof call !== 'object' || call === null) continue
const id = (call as { id?: unknown }).id
const fn = (call as { function?: { name?: unknown } }).function
if (typeof id === 'string' && typeof fn?.name === 'string') return { id, name: fn.name }
}
throw new Error('model request has no preceding tool call')
}
function normalize(value: string, cwd: string): string {
return value.replaceAll(cwd, '{{cwd}}')
}
describe('jsonrpc persistent tools snapshot', () => {
it('runs persistent shell state and editor mutations keylessly', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-persistent-tools-'))
const sessionRoot = join(cwd, '.sessions')
const target = join(cwd, 'note.txt')
const requests: ModelRequest[] = []
const modelServer = createServer((request, response) => {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk: string) => { body += chunk })
request.on('end', () => {
const parsed = JSON.parse(body) as ModelRequest
requests.push(parsed)
const messages = parsed.messages ?? []
const latest = messages.at(-1)
if (latest === undefined) throw new Error('model request has no messages')
let chunks: string[]
if (latest.role !== 'tool') {
chunks = sseToolCall('bash-1', 'bash', {
command: 'cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf "COUNT=%s CWD=%s\\n" "$DSH_EXAMPLE_COUNT" "$PWD"',
})
} else {
const call = latestToolCall(messages)
const toolText = messageText(latest.content)
if (call.id === 'bash-1') {
expect(toolText).toContain('COUNT=1 CWD=/tmp')
chunks = sseToolCall('bash-2', 'bash', {
command: 'DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf "COUNT=%s CWD=%s\\n" "$DSH_EXAMPLE_COUNT" "$PWD"',
})
} else if (call.id === 'bash-2') {
expect(toolText).toContain('COUNT=2 CWD=/tmp')
chunks = sseToolCall('editor-create', 'str_replace_editor', {
command: 'create',
path: target,
file_text: 'alpha\n',
})
} else if (call.id === 'editor-create') {
expect(toolText).toContain('New file created successfully')
chunks = sseToolCall('editor-replace', 'str_replace_editor', {
command: 'str_replace',
path: target,
old_str: 'alpha',
new_str: 'beta',
})
} else if (call.id === 'editor-replace') {
expect(toolText).toContain('has been edited successfully')
chunks = sseText('PERSISTENT_TOOLS_OK')
} else {
throw new Error(`unexpected tool call ${call.id}`)
}
}
response.writeHead(200, { 'content-type': 'text/event-stream' })
for (const chunk of chunks) response.write(chunk)
response.end()
})
})
await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve))
const address = modelServer.address()
if (address === null || typeof address === 'string') throw new Error('model server did not bind')
const launch = resolveExampleLaunch({
srcBin: runtimeBin,
configArgs: [],
tsconfigPath: repoTsconfig,
})
const harness = new DeepSeekHarness({
launch: {
command: launch.command,
args: launch.args,
cwd: repoRoot,
env: {
...Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)) as Record<string, string>,
...Object.fromEntries(Object.entries(launch.env).filter(([, value]) => value !== undefined)) as Record<string, string>,
DSH_CORDIS_CONFIG: configPath,
DSH_CWD: cwd,
DSH_SESSION_ROOT: sessionRoot,
DEEPSEEK_API_KEY: 'keyless-local-mock',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
},
requestTimeoutMs: 60_000,
},
cwd,
provider: 'deepseek',
model: 'deepseek-v4-flash',
})
try {
const result = await harness.run(
'Prove that bash state persists, then create and edit note.txt.',
{ sessionId: 'persistent-tools-snapshot' },
)
const calls = result.events.flatMap((event) => {
if (event.type !== 'tool/call') return []
return [{
name: event.data.name,
arguments: normalize(event.data.arguments, cwd),
}]
})
const results = result.events.flatMap((event) => {
if (event.type !== 'tool/result') return []
return event.data.message.content.flatMap((block) => {
if (block.type !== 'tool-result') return []
return block.content.flatMap(content =>
content.type === 'text'
? [{ text: normalize(content.text, cwd) }]
: [])
})
})
const tools = (requests[0]?.tools ?? []).map(tool => ({
name: tool.function?.name,
required: tool.function?.parameters?.required ?? [],
})).sort((left, right) => {
const leftName = String(left.name)
const rightName = String(right.name)
return leftName < rightName ? -1 : leftName > rightName ? 1 : 0
})
const behavior = {
tools,
calls,
results,
final: {
status: result.status,
reason: result.reason,
response: result.finalResponse,
file: await readFile(target, 'utf8'),
},
}
if (process.env.DSH_SNAPSHOT === 'refresh') {
await writeFile(expectedPath, `${JSON.stringify(behavior, null, 2)}\n`)
}
expect(behavior).toEqual(JSON.parse(await readFile(expectedPath, 'utf8')))
} finally {
await harness.close()
await new Promise<void>(resolve => modelServer.close(() => { resolve() }))
await rm(cwd, { recursive: true, force: true })
}
}, 75_000)
})

View File

@@ -0,0 +1,57 @@
{
"tools": [
{
"name": "bash",
"required": [
"command"
]
},
{
"name": "str_replace_editor",
"required": [
"command",
"path"
]
}
],
"calls": [
{
"name": "bash",
"arguments": "{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"
},
{
"name": "bash",
"arguments": "{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"
},
{
"name": "str_replace_editor",
"arguments": "{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"alpha\\n\"}"
},
{
"name": "str_replace_editor",
"arguments": "{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"alpha\",\"new_str\":\"beta\"}"
}
],
"results": [
{
"text": "COUNT=1 CWD=/tmp"
},
{
"text": "COUNT=2 CWD=/tmp"
},
{
"text": "New file created successfully at: {{cwd}}/note.txt"
},
{
"text": "The file {{cwd}}/note.txt has been edited successfully."
}
],
"final": {
"status": "ok",
"reason": {
"kind": "completed"
},
"response": "PERSISTENT_TOOLS_OK",
"file": "beta\n"
}
}

View File

@@ -56,6 +56,7 @@
"@deepseek-ai/dsh-timeout-policy": "workspace:*",
"@deepseek-ai/dsh-token-meter": "workspace:*",
"@deepseek-ai/dsh-tool-ask-user": "workspace:*",
"@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:*",
@@ -64,6 +65,7 @@
"@deepseek-ai/dsh-tool-pty": "workspace:*",
"@deepseek-ai/dsh-tool-ralph": "workspace:*",
"@deepseek-ai/dsh-tool-session-query": "workspace:*",
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:*",
"@deepseek-ai/dsh-tool-subagent": "workspace:*",
"@deepseek-ai/dsh-tool-todo": "workspace:*",
"@deepseek-ai/dsh-tool-web": "workspace:*",