Merge remote-tracking branch 'origin/master' into xtr/identified-immutable-messages

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent/README.i18n.yaml
This commit is contained in:
_Kerman
2026-07-28 18:49:23 +08:00
59 changed files with 364 additions and 97 deletions

View File

@@ -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/subagent/subagent-dsh-sdk/README.md
README.md: 95ddd154c8262e8854280e74618bdd9be9c938c0
README.zh.md: c10145f34cd785d10f4b660a5f6414455ad42464
README.md: e904ce3c09a1b44f8f5a0072b9ca85812898e74f
README.zh.md: b11cb9c8e0bf2577be71230292d73878b22896a0

View File

@@ -6,7 +6,7 @@ The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a
## Start and ownership
`start(request)` resolves the child's working directory, spawns the runtime through `DeepSeekHarness`, and completes the `initialize` handshake (with the configured `provider`/`model` route) before it fulfills. Fulfillment therefore means the child runtime is ready and ownership has transferred to the caller. A spawn, handshake, or pre-publication cancellation failure rejects only after the subprocess has been reaped; a working-directory resolution failure rejects before anything is spawned.
`start(request)` resolves the child's working directory, spawns the runtime through `DeepSeekHarness`, and completes the `initialize` handshake (with the configured `provider`/`model` route and optional `maxTokens` output cap) before it fulfills. Fulfillment therefore means the child runtime is ready and ownership has transferred to the caller. A spawn, handshake, or pre-publication cancellation failure rejects only after the subprocess has been reaped; a working-directory resolution failure rejects before anything is spawned.
The working directory resolves exactly like the ACP backend, through the seam's shared out-of-process helpers ([`dsh-subagent`](../subagent/README.md)): the configured `cwd` override when set (validated once at load), else the delegating parent session's cwd — never the server process's own cwd. The resolved path becomes the child process cwd and the workspace cwd of its SDK session.
@@ -32,6 +32,7 @@ The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/
| `cwd` | parent session cwd | Working-directory override; same validation as [`subagent-acp`](../subagent-acp/README.md). |
| `provider` | `deepseek` | Provider route sent in the child's `initialize`. |
| `model` | `deepseek-v4-flash` | Model sent in the child's `initialize`. |
| `maxTokens` | provider default | Per-request output-token cap sent in the child's `initialize`; it applies to the child root agent and its in-process descendants. |
| `env` | `{}` | Explicit child environment layered over a credential-scrubbed parent environment (e.g. the child's own `DEEPSEEK_API_KEY`, or `DSH_CORDIS_CONFIG`). |
| `shutdownTimeoutMs` | `1000` | Bound on the protocol `shutdown` exchange during dispose. |
| `disposeEofGraceMs` | `6000` | Grace after stdin EOF before platform termination. |
@@ -44,6 +45,7 @@ The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/
providerName: dsh-sdk
command: node
args: ['./packages/examples/jsonrpc-demo/lib/bin.js', './examples/jsonrpc-agent/cordis.yml']
maxTokens: 49152
env:
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
- id: tool-subagent

View File

@@ -6,7 +6,7 @@ SDK provider 把每个子代理作为一个完整的 DeepSeek Harness 运行时
## 启动与所有权
`start(request)` 先解析子进程工作目录,经 `DeepSeekHarness` 生成运行时,并在履行前完成 `initialize` 握手(携带配置的 `provider`/`model` 路由)。因此履行意味着子运行时已就绪、所有权已移交调用方。生成、握手或发布前取消的失败只在子进程被收割之后拒绝;工作目录解析失败在生成任何东西之前拒绝。
`start(request)` 先解析子进程工作目录,经 `DeepSeekHarness` 生成运行时,并在履行前完成 `initialize` 握手(携带配置的 `provider`/`model` 路由及可选的 `maxTokens` 输出上限)。因此履行意味着子运行时已就绪、所有权已移交调用方。生成、握手或发布前取消的失败只在子进程被收割之后拒绝;工作目录解析失败在生成任何东西之前拒绝。
工作目录的解析与 ACP 后端完全一致,经由接缝共享的进程外助手([`dsh-subagent`](../subagent/README.md)):设置了 `cwd` 覆盖则用之(加载时校验一次),否则用发起委托的父会话 cwd——绝不用服务器进程自己的 cwd。解析出的路径同时成为子进程 cwd 与其 SDK 会话的工作区 cwd。
@@ -32,6 +32,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
| `cwd` | 父会话 cwd | 工作目录覆盖;校验规则与 [`subagent-acp`](../subagent-acp/README.md) 相同。 |
| `provider` | `deepseek` | 写入子进程 `initialize` 的 provider 路由。 |
| `model` | `deepseek-v4-flash` | 写入子进程 `initialize` 的模型。 |
| `maxTokens` | provider 默认值 | 写入子进程 `initialize` 的单次请求输出 token 上限;对子根 Agent 及其进程内后代生效。 |
| `env` | `{}` | 在凭据擦除后的父环境之上叠加的显式子环境(例如子进程自己的 `DEEPSEEK_API_KEY`,或 `DSH_CORDIS_CONFIG`)。 |
| `shutdownTimeoutMs` | `1000` | 处置期间协议 `shutdown` 交换的时限。 |
| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限。 |
@@ -44,6 +45,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
providerName: dsh-sdk
command: node
args: ['./packages/examples/jsonrpc-demo/lib/bin.js', './examples/jsonrpc-agent/cordis.yml']
maxTokens: 49152
env:
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
- id: tool-subagent

View File

@@ -46,6 +46,8 @@ export interface Config {
provider: string
/** Model the child runtime initializes with (default `deepseek-v4-flash`). */
model: string
/** Optional per-request output-token cap for the child runtime. */
maxTokens?: number
/**
* Extra environment variables for the child process — e.g. the child
* runtime's own `DEEPSEEK_API_KEY`, or `DSH_CORDIS_CONFIG` naming its
@@ -73,14 +75,15 @@ export const Config: z<Config> = z.object({
cwd: z.string(),
provider: z.string().default('deepseek'),
model: z.string().default('deepseek-v4-flash'),
maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
env: z.dict(z.string()).default({}),
shutdownTimeoutMs: z.number().default(DEFAULT_SHUTDOWN_TIMEOUT_MS),
disposeEofGraceMs: z.number().default(DEFAULT_DISPOSE_EOF_GRACE_MS),
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
})
/** The shape after schemastery applied the defaults (cwd has none). */
type ResolvedConfig = Required<Omit<Config, 'cwd'>> & Pick<Config, 'cwd'>
/** The shape after schemastery applied the defaults (`cwd` and `maxTokens` have none). */
type ResolvedConfig = Required<Omit<Config, 'cwd' | 'maxTokens'>> & Pick<Config, 'cwd' | 'maxTokens'>
/**
* The SDK provider. Advertises NO start-time capabilities: an out-of-process
@@ -101,6 +104,7 @@ class SdkProvider implements SubagentProvider {
cwd: resolveChildCwd('subagent-dsh-sdk', this.config.cwd, request.parent.session.header.cwd),
provider: this.config.provider,
model: this.config.model,
...this.config.maxTokens === undefined ? {} : { maxTokens: this.config.maxTokens },
env: this.config.env,
shutdownTimeoutMs: this.config.shutdownTimeoutMs,
disposeEofGraceMs: this.config.disposeEofGraceMs,
@@ -121,6 +125,9 @@ export function apply(ctx: Context, config: Config): void {
assertPositiveFinite('subagent-dsh-sdk', 'shutdownTimeoutMs', resolved.shutdownTimeoutMs)
assertPositiveFinite('subagent-dsh-sdk', 'disposeEofGraceMs', resolved.disposeEofGraceMs)
assertPositiveFinite('subagent-dsh-sdk', 'disposeGraceMs', resolved.disposeGraceMs)
if (resolved.maxTokens !== undefined && (!Number.isSafeInteger(resolved.maxTokens) || resolved.maxTokens <= 0)) {
throw new TypeError('subagent-dsh-sdk maxTokens must be a positive safe integer')
}
// Interpret a relative configured cwd against the harness launch directory
// ONCE, at load, and fail a misconfigured directory here — not per start.
const configuredCwd = validateConfiguredCwd('subagent-dsh-sdk', resolved.cwd)

View File

@@ -35,6 +35,8 @@ export interface SdkRunSpec {
provider: string
/** Model the child runtime initializes with. */
model: string
/** Optional per-request output-token cap sent in the child runtime's initialize handshake. */
maxTokens?: number
/**
* Extra environment variables to ADD for the child (e.g. the child
* runtime's own `DEEPSEEK_API_KEY`, or `DSH_CORDIS_CONFIG`). Merged after
@@ -126,6 +128,7 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
cwd: spec.cwd,
provider: spec.provider,
model: spec.model,
...spec.maxTokens === undefined ? {} : { maxTokens: spec.maxTokens },
})
// Cancellation settles the result without waiting for a cooperative child.

View File

@@ -105,17 +105,22 @@ describe('dsh-subagent-dsh-sdk provider', () => {
await ctx.fiber.dispose()
})
it('initializes the child with the configured provider/model and the parent cwd', async () => {
it('initializes the child with the configured provider/model/maxTokens and the parent cwd', async () => {
const tmp = mkdtempSync(join(tmpdir(), 'subagent-dsh-sdk-init-'))
const recordFile = join(tmp, 'init.jsonl')
try {
const ctx = await setup({ FAKE_RECORD_INIT: recordFile })
const ctx = await setup({ FAKE_RECORD_INIT: recordFile }, { maxTokens: 4096 })
const run = await ctx.subagents.start('dsh-sdk', request())
await run.result
await run.dispose()
const { readFileSync } = await import('node:fs')
const records = readFileSync(recordFile, 'utf8').trim().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
expect(records).toEqual([{ cwd: process.cwd(), provider: 'fake-provider', model: 'fake-model' }])
expect(records).toEqual([{
cwd: process.cwd(),
provider: 'fake-provider',
model: 'fake-model',
maxTokens: 4096,
}])
await ctx.fiber.dispose()
} finally {
rmSync(tmp, { recursive: true, force: true })
@@ -364,6 +369,45 @@ describe('dsh-subagent-dsh-sdk provider', () => {
await ctx.fiber.dispose()
})
it.each([0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
'rejects invalid maxTokens %s at load',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await expect(ctx.plugin(sdk, {
providerName: 'sdk',
command: 'true',
args: [],
provider: 'p',
model: 'm',
maxTokens,
env: {},
})).rejects.toThrow('maxTokens')
await ctx.fiber.dispose()
},
)
it.each([0, 1.5])(
'defensively rejects invalid maxTokens %s when apply is called directly',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
expect(() => { sdk.apply(ctx, {
providerName: 'sdk',
command: 'true',
args: [],
provider: 'p',
model: 'm',
maxTokens,
env: {},
shutdownTimeoutMs: DEFAULT_SHUTDOWN_TIMEOUT_MS,
disposeEofGraceMs: DEFAULT_DISPOSE_EOF_GRACE_MS,
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
}) }).toThrow('maxTokens must be a positive safe integer')
await ctx.fiber.dispose()
},
)
it('rejects an empty config cwd at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)