fix(subagent): publish inherited policy facts to telemetry
Inherited sandbox and approval events were part of the constructor seed. Session.firstLiveSeq classifies every constructor event as replayed history, so telemetry adoption skipped these child-only creation facts even though no parent or prior process had exported them. Capture the parent overrides at the same synchronous delegation boundary, but append the events during the child factory setup while the session is still unpublished. They remain ordered after fork history, persist with the first child batch, and retain last-event-wins behavior while landing on the live side of the telemetry boundary. This uses the existing setup and session append contracts instead of adding another seed category or telemetry special case. Add regression coverage for exporting an unpublished suffix without re-exporting constructor history, assert the spawn and fork firstLiveSeq boundaries, and restore the public seed documentation to replay/fork history only.
This commit is contained in:
@@ -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: 02d9bd7d2dc792055a13d51402572313855ff1ff
|
||||
README.zh.md: f55467b37a948ef5a0a34bd45d8a11d7a004ff29
|
||||
README.md: 980bc18de088c41dfe2f57a5ff0882a60892fc9f
|
||||
README.zh.md: 1ceb628371c3ae9cee6d8afa6bc1d95ba4cda8ae
|
||||
|
||||
@@ -18,7 +18,7 @@ The driver follows this sequence:
|
||||
|
||||
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.
|
||||
|
||||
When the optional sandbox-policy or approval service is composed, the driver snapshots the parent's explicit session override before child creation and seeds a source-tagged event after any fork prefix. It never copies deployment defaults or one-shot grants; later child switches still win. See the [policy-inheritance decision](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md).
|
||||
When the optional sandbox-policy or approval service is composed, the driver snapshots the parent's explicit session override before child creation and appends a source-tagged event during unpublished setup, after any fork history and before session publication. It never copies deployment defaults or one-shot grants; later child switches still win. See the [policy-inheritance decision](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md).
|
||||
|
||||
## Cancellation and ownership
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。
|
||||
|
||||
当组合中挂载了可选的沙箱策略或审批服务时,驱动器会在创建子 agent 前对父级的显式会话覆盖项获取快照,并在任意 fork 前缀之后预置一条带来源标记的事件。它绝不复制部署默认值或一次性授权;子 agent 后续的切换仍然优先。参见[策略继承决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。
|
||||
当组合中挂载了可选的沙箱策略或审批服务时,驱动器会在创建子 agent 前对父级的显式会话覆盖项获取快照,并在未发布的设置阶段追加一条带来源标记的事件,使其位于所有 fork 历史之后、会话发布之前。它绝不复制部署默认值或一次性授权;子 agent 后续的切换仍然优先。参见[策略继承决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。
|
||||
|
||||
## 取消与所有权
|
||||
|
||||
|
||||
@@ -103,30 +103,19 @@ export async function startInProcessRun(
|
||||
}
|
||||
|
||||
// Capture before the first await: a later parent switch belongs to the
|
||||
// parent's future. Appending after the fork prefix makes the captured
|
||||
// values the child's initial overrides without another storage plane.
|
||||
// parent's future.
|
||||
const inheritedMode = parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session)
|
||||
const inheritedPolicy = parent.ctx.get('approval')?.overrideOf(parent.session)
|
||||
const seed: SessionEvent[] = [...options.seed ?? []]
|
||||
if (inheritedMode !== undefined) {
|
||||
seed.push({
|
||||
type: 'sandbox/mode',
|
||||
seq: seed.length,
|
||||
time: Date.now(),
|
||||
data: { mode: inheritedMode, source: 'delegation' },
|
||||
})
|
||||
}
|
||||
if (inheritedPolicy !== undefined) {
|
||||
seed.push({
|
||||
type: 'approval/policy',
|
||||
seq: seed.length,
|
||||
time: Date.now(),
|
||||
data: { policy: inheritedPolicy, source: 'delegation' },
|
||||
})
|
||||
}
|
||||
|
||||
let structured: StructuredAttachment | undefined
|
||||
const setup = (childCtx: Context): void => {
|
||||
const childSession = (childCtx.agent as Agent).session
|
||||
if (inheritedMode !== undefined) {
|
||||
childSession.append('sandbox/mode', { mode: inheritedMode, source: 'delegation' })
|
||||
}
|
||||
if (inheritedPolicy !== undefined) {
|
||||
childSession.append('approval/policy', { policy: inheritedPolicy, source: 'delegation' })
|
||||
}
|
||||
if (request.persona !== undefined) {
|
||||
childCtx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: request.persona })
|
||||
}
|
||||
@@ -146,7 +135,7 @@ export async function startInProcessRun(
|
||||
delegationDepth: childDepth,
|
||||
...seedLength > 0 ? { seedLength } : {},
|
||||
},
|
||||
...(options.seed !== undefined || seed.length > 0) ? { seed } : {},
|
||||
...options.seed === undefined ? {} : { seed: options.seed },
|
||||
agentOptions,
|
||||
signal: request.signal,
|
||||
setup,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Policy inheritance through constructor-seeded child session events. */
|
||||
/** Policy inheritance through child session events appended before publication. */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises'
|
||||
@@ -69,7 +69,7 @@ function toolResultTexts(agent: Agent): string[] {
|
||||
}
|
||||
|
||||
describe('in-process policy inheritance', () => {
|
||||
it('seeds parent overrides into a spawn child before its first request', async () => {
|
||||
it('records parent overrides before publishing a spawn child', async () => {
|
||||
const script: Script = []
|
||||
const { ctx, parent } = await setupWalled(script)
|
||||
const blocked = join(workspace, 'spawn-blocked.txt')
|
||||
@@ -93,7 +93,7 @@ describe('in-process policy inheritance', () => {
|
||||
{ type: 'sandbox/mode', seq: 0, data: { mode: 'read-only', source: 'delegation' } },
|
||||
{ type: 'approval/policy', seq: 1, data: { policy: 'never', source: 'delegation' } },
|
||||
])
|
||||
expect(child.session.firstLiveSeq).toBe(2)
|
||||
expect(child.session.firstLiveSeq).toBe(0)
|
||||
expect(child.session.header.seedLength).toBeUndefined()
|
||||
expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only')
|
||||
expect(ctx.approval.overrideOf(child.session)).toBe('never')
|
||||
@@ -125,6 +125,7 @@ describe('in-process policy inheritance', () => {
|
||||
const child = run.localAgent as Agent
|
||||
|
||||
expect(child.session.header.seedLength).toBe(1)
|
||||
expect(child.session.firstLiveSeq).toBe(seed.length)
|
||||
expect(child.session.events.filter(event => event.type === 'sandbox/mode')).toMatchObject([
|
||||
{ seq: 0, data: { mode: 'workspace-write' } },
|
||||
{ seq: 1, data: { mode: 'read-only', source: 'delegation' } },
|
||||
|
||||
Reference in New Issue
Block a user