Merge remote-tracking branch 'origin/master' into feature/subagent-policy-inheritance

# Conflicts:
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/subagent/subagent-inprocess/README.i18n.yaml
This commit is contained in:
kingwl
2026-07-28 18:37:46 +08:00
59 changed files with 362 additions and 95 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-inprocess/README.md
README.md: cb5149de732970873caa292db05508d5d4e24fe7
README.zh.md: 150a69b1191d57cd3cef8cef0d4d58178e7f73da
README.md: 95a45cd7a1f4510601f7f8d8bf396e7262f1a3cf
README.zh.md: c20f5106d0009f9c5507e830361c0fcba54d6280

View File

@@ -16,7 +16,7 @@ The driver follows this sequence:
4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.followup(prompt)` followed by `child.whenIdle()`.
5. Read the child's own last assistant message and latest message-triggered turn reason, excluding any fork seed and later between-turn records.
The child gets the parent's working-directory/session lineage and inherits the parent model unless `request.agentOptions` overrides it. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset.
The child gets the parent's working-directory/session lineage and inherits the parent provider, model, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset.
The child also inherits the parent's session POLICY overrides. The driver captures `ctx.sandboxPolicy.overrideOf(parent.session)` and `ctx.approval.overrideOf(parent.session)` synchronously before its first await — the delegation moment is the snapshot point, so a parent switch racing the asynchronous child creation belongs to the parent's future — and carries the captured values in the child's creation meta into its immutable `SessionHeader` (`sandboxMode`/`approvalPolicy`), durable from the moment the session exists: no listener ordering can starve the baseline and no crash window can lose it, including an idle SessionStart-style injection persisting a complete turn before any prompt turn opens. Both services are consumed opportunistically — compositions without them delegate policy-free. Only the override chain is copied, so an unswitched parent writes no baseline and the child follows the live deployment default; `overrideOf` folds only events past the seed boundary, so a fork seed's stale switch is subsumed by the baseline while the child's own later switches outrank it. Nesting composes: each capture resolves the delegating session's own chain ([rationale](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)).

View File

@@ -16,7 +16,7 @@
4. 发布子 agent,保留返回的 `AgentHandle`,并通过先调用 `child.followup(prompt)`、再调用 `child.whenIdle()` 来驱动一项任务。
5. 读取子 agent 自身最后一条 assistant 消息,以及由消息触发的最新轮次原因;排除任何 fork 初始内容和后续轮次间记录。
子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 模型。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。
子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。
子 agent 还会继承父 agent 的会话策略覆盖项。驱动器在自己的第一个 await 之前同步捕获 `ctx.sandboxPolicy.overrideOf(parent.session)` 与 `ctx.approval.overrideOf(parent.session)`——委派时刻即快照点,因此与异步的子 agent 创建过程赛跑的父 agent 切换属于父 agent 的未来——并把捕获值作为创建元数据带入子 agent 不可变的 `SessionHeader`(`sandboxMode`/`approvalPolicy`),从会话存在的那一刻起就具备持久性:任何监听器顺序都不可能饿死该基线,任何崩溃窗口也不可能丢失它,包括空闲时的 SessionStart 式注入在任何提示词轮次开启之前就持久化一个完整轮次的情况。两个服务均以可选方式消费:未挂载它们的组合照旧进行无策略委派。只复制覆盖链,因此未切换过的父 agent 不写入任何基线,子 agent 继续跟随实时部署默认值;`overrideOf` 只折叠初始内容边界之后的事件,因此 fork 初始内容携带的陈旧切换已被基线所涵盖,而子 agent 自己之后的切换仍优先于基线。嵌套按构造即可组合:每次捕获解析的都是发起委派的会话自身的覆盖链(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md))。

View File

@@ -93,9 +93,11 @@ export async function startInProcessRun(
const parentHeader = parent.session.header
const parentProvider = parent.options.provider
const parentModel = parent.options.model
const parentMaxTokens = parent.options.maxTokens
const agentOptions: AgentOptions = {
...parentProvider !== undefined ? { provider: parentProvider } : {},
...parentModel !== undefined ? { model: parentModel } : {},
...parentMaxTokens !== undefined ? { maxTokens: parentMaxTokens } : {},
...request.agentOptions,
subagentDepth: childDepth,
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { type Agent } from '@deepseek-ai/dsh-agent'
import { type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
@@ -21,7 +21,7 @@ async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(AgentLoopInvariant)
}
async function setup(script: Script) {
async function setup(script: Script, parentOptions: Partial<AgentOptions> = {}) {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
@@ -29,7 +29,7 @@ async function setup(script: Script) {
await ctx.plugin(SubagentService)
const adapter = new MockAdapter(script)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock', ...parentOptions })
return { ctx, parent, adapter }
}
@@ -110,6 +110,27 @@ describe('startInProcessRun', () => {
await run.dispose()
})
it('inherits the parent output-token cap and accepts an explicit child override', async () => {
const { ctx, parent, adapter } = await setup(
[textResponse('inherited'), textResponse('overridden')],
{ maxTokens: 111 },
)
const inherited = await startInProcessRun(request(parent), {})
await inherited.result
expect(adapter.requests[0]?.maxTokens).toBe(111)
expect(ctx.agents.get(inherited.id)?.options.maxTokens).toBe(111)
await inherited.dispose()
const overridden = await startInProcessRun({
...request(parent),
agentOptions: { maxTokens: 222 },
}, {})
await overridden.result
expect(adapter.requests[1]?.maxTokens).toBe(222)
expect(ctx.agents.get(overridden.id)?.options.maxTokens).toBe(222)
await overridden.dispose()
})
it('counts a RESUMED child by its persisted header depth, not the absent runtime depth', async () => {
// Resume rebuilds runtime options, so the durable header must keep this
// depth-1 child from delegating as though it were top-level.