mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(sdk): remove unused root tool filter
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.md: dcb4f77048d516f0e187b611dc09c224fba47b58
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 505b49506ae4c4be4809d588448b2c593d3ebaf5
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.md: 4af7812db6818b65c754a43ec1a7f973d1cbcbf9
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 155e7d2ae0b0ba3e4163dd85a31de90bef9d588a
|
||||
|
||||
@@ -24,7 +24,7 @@ Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME
|
||||
|
||||
Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile <name> ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/<name>/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile.
|
||||
|
||||
The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition rather than a server filter; dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices.
|
||||
The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition; the shared JSON-RPC server exposes no root-agent tool filter. Dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices.
|
||||
|
||||
The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`.
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
|
||||
|
||||
持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile <name> ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/<name>/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。
|
||||
|
||||
[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在,而不是通过 server 筛选;动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。
|
||||
[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在;共享 JSON-RPC server 不暴露根 agent 工具筛选器。动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。
|
||||
|
||||
运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/config-catalog.md
|
||||
config-catalog.md: 19f292ee40861ffa6f8e7fdef4d8717189a2149c
|
||||
config-catalog.zh.md: 96f9721cae20a1a0ed172349c0189bc47576c75d
|
||||
config-catalog.md: 54cf64bdf6ff5c8ee51b6ecedaf218267778d19f
|
||||
config-catalog.zh.md: e887edf989d279435ae04beb40172341f5125b1e
|
||||
|
||||
@@ -1714,13 +1714,6 @@ Requires: `agents`
|
||||
export interface JsonRpcConfig {
|
||||
/** Report max-token turn/subagent termination as a successful SDK result. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
|
||||
toolFilter?: {
|
||||
/** Global tool names that remain visible. */
|
||||
allow?: string[]
|
||||
/** Global tool names removed from visibility. */
|
||||
deny?: string[]
|
||||
}
|
||||
/** Transport input override; production uses `process.stdin`. */
|
||||
input?: Readable
|
||||
/** Transport output override; production uses `process.stdout`. */
|
||||
|
||||
@@ -1716,13 +1716,6 @@ export interface Config {
|
||||
export interface JsonRpcConfig {
|
||||
/** Report max-token turn/subagent termination as a successful SDK result. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
|
||||
toolFilter?: {
|
||||
/** Global tool names that remain visible. */
|
||||
allow?: string[]
|
||||
/** Global tool names removed from visibility. */
|
||||
deny?: string[]
|
||||
}
|
||||
/** Transport input override; production uses `process.stdin`. */
|
||||
input?: Readable
|
||||
/** Transport output override; production uses `process.stdout`. */
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 packages/sdk/server/README.md
|
||||
README.md: e6d36df7e387e895941c082226ac955c69f16ca4
|
||||
README.zh.md: 13c026edc491ebdbec298b8c9cd330b03d675db7
|
||||
README.md: f10c45e2df06383d6726f249bb6db2015a7fb418
|
||||
README.zh.md: 7bf3bcdab2a318fbfcba7b5a6c3a60ff883f47b6
|
||||
|
||||
@@ -10,7 +10,7 @@ The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-proc
|
||||
|
||||
## Config
|
||||
|
||||
`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. Optional `toolFilter.allow` and `toolFilter.deny` restrict each SDK-created root agent through `ctx.tools.restrict()`. An allow list excludes later global tool registrations that it does not name, so a fixed SDK deployment cannot silently gain model-facing tools when its base bundle expands. Unknown names and an empty filter fail when the first session is created. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`.
|
||||
`maxTokensAsSuccess` defaults to `false` and affects only the deployment-mapped status on `subagent.finished`; root-session prompts have no prompt-level status. The profile composition owns each root agent's tools. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport hooks; production uses process stdio and `process.exit`.
|
||||
|
||||
## stdout is the protocol
|
||||
|
||||
@@ -30,7 +30,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s
|
||||
|
||||
#### What the model sees
|
||||
|
||||
For each accepted `session/prompt`, text and durable content references enter one user message verbatim. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. A configured `toolFilter` projects that composition's global tool registry before the request is assembled and executed.
|
||||
For each accepted `session/prompt`, text and durable content references enter one user message verbatim. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 配置
|
||||
|
||||
`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。可选的 `toolFilter.allow` 与 `toolFilter.deny` 通过 `ctx.tools.restrict()` 限制每个由 SDK 创建的根 agent。Allow 列表会排除之后出现但未指名的全局工具,因此固定的 SDK 部署不会在基础 bundle 扩展时静默获得面向模型的新工具。未知名称与空筛选器会在创建首个会话时明确失败。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。
|
||||
`maxTokensAsSuccess` 默认为 `false`,且只影响 `subagent.finished` 上由部署映射的状态;根会话提示词没有提示词级状态。Profile 组合负责每个根 agent 的工具。`JsonRpcConfig.input`、`output` 和 `exit` 是仅供运行时使用的传输钩子;生产环境使用进程 stdio 和 `process.exit`。
|
||||
|
||||
## stdout 即协议
|
||||
|
||||
@@ -30,7 +30,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
对于每个已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。配置的 `toolFilter` 会在请求组装与执行前投影该组合的全局工具注册表。
|
||||
对于每个已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -25,13 +25,6 @@ export const inject = ['agents']
|
||||
export interface JsonRpcConfig {
|
||||
/** Report max-token turn/subagent termination as a successful SDK result. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
|
||||
toolFilter?: {
|
||||
/** Global tool names that remain visible. */
|
||||
allow?: string[]
|
||||
/** Global tool names removed from visibility. */
|
||||
deny?: string[]
|
||||
}
|
||||
/** Transport input override; production uses `process.stdin`. */
|
||||
input?: Readable
|
||||
/** Transport output override; production uses `process.stdout`. */
|
||||
@@ -42,11 +35,6 @@ export interface JsonRpcConfig {
|
||||
|
||||
export const Config: Schema<JsonRpcConfig> = Schema.object({
|
||||
maxTokensAsSuccess: Schema.boolean().default(false),
|
||||
// Preserve omission; Schemastery's materialized empty object is not a valid restriction.
|
||||
toolFilter: Schema.object({
|
||||
allow: Schema.array(Schema.string()).default(undefined as unknown as string[]),
|
||||
deny: Schema.array(Schema.string()).default(undefined as unknown as string[]),
|
||||
}).default(undefined as unknown as { allow: string[]; deny: string[] }),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -71,7 +59,6 @@ export function apply(ctx: Context, config: JsonRpcConfig): void {
|
||||
const transport = new JsonRpcLineTransport(input, output)
|
||||
const server = new HarnessSdkJsonRpcServer(ctx, transport, {
|
||||
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
|
||||
...resolvedConfig.toolFilter === undefined ? {} : { toolFilter: resolvedConfig.toolFilter },
|
||||
})
|
||||
|
||||
// Share one exit task so racing shutdown requests cannot dispose the root or
|
||||
|
||||
@@ -15,7 +15,6 @@ import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
|
||||
import type { ToolRestriction } from '@deepseek-ai/dsh-tools'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import type {
|
||||
InitializeParams,
|
||||
@@ -61,8 +60,6 @@ function subagentParentOf(carrier: Scoped<SubagentRuntime>): Agent {
|
||||
export interface HarnessSdkJsonRpcServerOptions {
|
||||
/** Report max-token termination as an accepted result instead of an infrastructure error. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Restrict each SDK-created root agent to an explicit subset of global tools. */
|
||||
toolFilter?: ToolRestriction
|
||||
}
|
||||
|
||||
function successStatus(reason: string, options: HarnessSdkJsonRpcServerOptions): 'ok' | 'error' {
|
||||
@@ -256,7 +253,6 @@ export class HarnessSdkJsonRpcServer {
|
||||
// rows in the host plane, so this agent reads them from the global layer. A
|
||||
// deployment that configures a roster has to join one here first
|
||||
// (@deepseek-ai/dsh-agent-presets README, "Composing a child agent").
|
||||
const toolFilter = this.options.toolFilter
|
||||
const handle = await this.ctx.agents.create({
|
||||
sessionId: SessionId(sessionId),
|
||||
meta: { cwd: this.cwd },
|
||||
@@ -265,9 +261,6 @@ export class HarnessSdkJsonRpcServer {
|
||||
model: this.model,
|
||||
...this.maxTokens === undefined ? {} : { maxTokens: this.maxTokens },
|
||||
},
|
||||
...toolFilter === undefined
|
||||
? {}
|
||||
: { setup: (agentCtx: Context) => { agentCtx.tools.restrict(toolFilter) } },
|
||||
})
|
||||
const rec: SessionRecord = { handle }
|
||||
this.sessions.set(sessionId, rec)
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import * as jsonrpc from '../src/index.ts'
|
||||
|
||||
/**
|
||||
@@ -72,7 +71,6 @@ async function mountPlugin(
|
||||
writeDelayMs?: number
|
||||
failFlush?: boolean
|
||||
beforeServer?: (ctx: Context) => Promise<void> | void
|
||||
toolFilter?: jsonrpc.JsonRpcConfig['toolFilter']
|
||||
} = {},
|
||||
): Promise<ApplyHarness> {
|
||||
const ctx = new Context()
|
||||
@@ -122,7 +120,6 @@ async function mountPlugin(
|
||||
input,
|
||||
output,
|
||||
exit,
|
||||
...options.toolFilter === undefined ? {} : { toolFilter: options.toolFilter },
|
||||
})
|
||||
|
||||
const frames = (): Record<string, unknown>[] =>
|
||||
@@ -289,51 +286,6 @@ describe('dsh-sdk-jsonrpc-server plugin apply', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('applies the configured root-agent tool filter through the Loader plugin', async () => {
|
||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-tool-filter-'))
|
||||
const llmServer = await mockCompletionServer()
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
|
||||
const harness = await mountPlugin(storageDir, {
|
||||
toolFilter: { allow: ['kept'] },
|
||||
beforeServer: (ctx) => {
|
||||
for (const name of ['kept', 'excluded']) {
|
||||
ctx.tools.register(defineTool({
|
||||
name,
|
||||
description: name,
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
execute: async () => name,
|
||||
}))
|
||||
}
|
||||
},
|
||||
})
|
||||
try {
|
||||
harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' } })
|
||||
await harness.waitForFrame(frame => frame.id === 1, 'initialize response')
|
||||
harness.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'session/prompt',
|
||||
params: { sessionId: 'filtered', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
|
||||
})
|
||||
await harness.waitForFrame(
|
||||
frame => frame.method === 'session.status'
|
||||
&& (frame.params as { status?: string } | undefined)?.status === 'idle',
|
||||
'filtered session idle status',
|
||||
)
|
||||
|
||||
const request = llmServer.requests[0] as { tools?: Array<{ function?: { name?: string } }> }
|
||||
expect(request.tools?.map(entry => entry.function?.name)).toEqual(['kept'])
|
||||
} finally {
|
||||
await harness.dispose()
|
||||
await rm(storageDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('answers shutdown before exiting 0 exactly once, even against a racing second shutdown', async () => {
|
||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-shutdown-'))
|
||||
const harness = await mountPlugin(storageDir, { writeDelayMs: 10 })
|
||||
|
||||
@@ -14,7 +14,6 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import SubagentRuntime, { type SubagentResult, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
|
||||
import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { HarnessSdkJsonRpcServer } from '../src/index.ts'
|
||||
|
||||
class FakeTransport implements JsonRpcTransportPeer {
|
||||
@@ -172,46 +171,6 @@ describe('HarnessSdkJsonRpcServer', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('allowlists each root session against current and later global tools', { timeout: 15_000 }, async () => {
|
||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-tool-filter-'))
|
||||
const llmServer = await mockCompletionServer()
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
|
||||
const ctx = await makeHarness(storageDir)
|
||||
const tool = (name: string) => defineTool({
|
||||
name,
|
||||
description: name,
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: { type: 'string' as const },
|
||||
render: (_args, value) => [{ type: 'text' as const, text: value }],
|
||||
},
|
||||
execute: async () => name,
|
||||
})
|
||||
ctx.tools.register(tool('kept'))
|
||||
ctx.tools.register(tool('excluded'))
|
||||
const server = new HarnessSdkJsonRpcServer(ctx, new FakeTransport(), {
|
||||
toolFilter: { allow: ['kept'] },
|
||||
})
|
||||
try {
|
||||
await server.initialize({ cwd: storageDir, provider: 'deepseek-official', model: 'filtered-model' })
|
||||
await server.prompt({ sessionId: 'first', contentBlocks: [{ type: 'text', text: 'first' }] })
|
||||
await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(1) })
|
||||
ctx.tools.register(tool('future'))
|
||||
await server.prompt({ sessionId: 'second', contentBlocks: [{ type: 'text', text: 'second' }] })
|
||||
await vi.waitFor(() => { expect(llmServer.requests).toHaveLength(2) })
|
||||
|
||||
expect(llmServer.requests.map((request) => {
|
||||
const tools = (request as { tools?: Array<{ function?: { name?: string } }> }).tools ?? []
|
||||
return tools.map(entry => entry.function?.name)
|
||||
})).toEqual([['kept'], ['kept']])
|
||||
await server.shutdown()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
await rm(storageDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('queues overlapping prompts for one session without blocking other sessions', async () => {
|
||||
const mainFollowup = vi.fn<Agent['followup']>()
|
||||
const mainAgent = ({
|
||||
|
||||
Reference in New Issue
Block a user