Merge remote-tracking branch 'origin/master' into fix/continuable-subagent-policy-inheritance
# Conflicts: # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # packages/subagent/subagent-inprocess/src/index.ts # packages/subagent/subagent/package.json # packages/subagent/subagent/src/child-agent.ts # packages/subagent/subagent/src/continuation.ts # packages/subagent/subagent/tsconfig.json
This commit is contained in:
@@ -35,9 +35,12 @@
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-include": "^1.0.4",
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
|
||||
@@ -113,7 +113,7 @@ export async function startInProcessRun(
|
||||
let structured: StructuredAttachment | undefined
|
||||
const setup = (childCtx: Context): void => {
|
||||
appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, inherited)
|
||||
applyChildComposition(childCtx, {
|
||||
applyChildComposition(childCtx, parent, {
|
||||
persona: request.persona,
|
||||
toolFilter: request.toolFilter,
|
||||
})
|
||||
|
||||
20
packages/subagent/subagent-inprocess/tests/fixtures/plugins/preset-tool.js
vendored
Normal file
20
packages/subagent/subagent-inprocess/tests/fixtures/plugins/preset-tool.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// A preset row standing in for the agent-plane tool rows a real preset mounts.
|
||||
// Import-free on purpose — the Loader resolves entry modules through Node's ESM
|
||||
// resolver, which cannot see this workspace's TypeScript sources.
|
||||
export const name = 'preset-tool'
|
||||
export const inject = ['tools', 'systemPrompt']
|
||||
|
||||
export function apply(ctx, config) {
|
||||
ctx.effect(() => ctx.tools.register({
|
||||
name: config.tool,
|
||||
description: `fixture tool ${config.tool}`,
|
||||
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
||||
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
|
||||
execute: () => Promise.resolve(config.tool),
|
||||
}))
|
||||
ctx.effect(() => ctx.systemPrompt.section({
|
||||
name: `preset:${config.tool}`,
|
||||
order: 10,
|
||||
text: `section for ${config.tool}`,
|
||||
}))
|
||||
}
|
||||
5
packages/subagent/subagent-inprocess/tests/fixtures/presets/coding/agent.cordis.yml
vendored
Normal file
5
packages/subagent/subagent-inprocess/tests/fixtures/presets/coding/agent.cordis.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Agent-plane composition: the model-facing row lives here, not in the host.
|
||||
- id: only
|
||||
name: ../../plugins/preset-tool.js
|
||||
config:
|
||||
tool: preset_only
|
||||
6
packages/subagent/subagent-inprocess/tests/fixtures/presets/reviewing/agent.cordis.yml
vendored
Normal file
6
packages/subagent/subagent-inprocess/tests/fixtures/presets/reviewing/agent.cordis.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# A second agent-plane composition, so a switch is a real switch: the tool a
|
||||
# joined child sees has to change with it.
|
||||
- id: only
|
||||
name: ../../plugins/preset-tool.js
|
||||
config:
|
||||
tool: reviewing_only
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Composition inheritance: a child runs on the preset its parent runs on.
|
||||
*
|
||||
* With every model-facing row on the agent plane, the tool registry's global
|
||||
* layer is empty, so a child that joins no preset reaches the model with no
|
||||
* tools at all. These assert the model-visible result — the schemas in the
|
||||
* child's own request — rather than the join that produces it.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include from '@cordisjs/plugin-include'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import AgentPresets from '@deepseek-ai/dsh-agent-presets'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { startInProcessRun } from '../src/index.ts'
|
||||
|
||||
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
|
||||
const ROOTS = [{ path: join(FIXTURES, 'presets'), trust: 'system' as const }]
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
/** A host composition carrying no model-facing rows, plus the preset roster. */
|
||||
async function setupPresetHost(): Promise<{ ctx: Context; adapter: MockAdapter; parent: Agent }> {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.include = Include
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(AgentPresets, { default: 'coding', roots: ROOTS })
|
||||
const adapter = new MockAdapter([textResponse('parent idle'), textResponse('child done')])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('parent'),
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'coding'),
|
||||
})
|
||||
return { ctx, adapter, parent: handle.agent }
|
||||
}
|
||||
|
||||
/** The one-shot spawn request shape both in-process providers build. */
|
||||
function spawnRequest(parent: Agent) {
|
||||
return {
|
||||
label: 'child task',
|
||||
prompt: [{ type: 'text' as const, text: 'child task' }],
|
||||
parent,
|
||||
signal: new AbortController().signal,
|
||||
descriptor: snapshotSubagentDescriptor({
|
||||
mode: 'one-shot' as const,
|
||||
provider: 'spawn',
|
||||
label: 'child task',
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
describe('a child agent composed in-process', () => {
|
||||
it('reaches the model with its parent\'s preset tools', async () => {
|
||||
const { ctx, adapter, parent } = await setupPresetHost()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
await run.result
|
||||
|
||||
const childRequest = adapter.requests.at(-1)
|
||||
expect(childRequest?.tools?.map(tool => tool.name)).toEqual(['preset_only'])
|
||||
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual(['preset_only'])
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('carries its parent\'s prompt sections', async () => {
|
||||
const { parent } = await setupPresetHost()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
await run.result
|
||||
|
||||
expect(run.localAgent?.session.events.some(event =>
|
||||
event.type === 'request/header'
|
||||
&& JSON.stringify(event.data).includes('section for preset_only'))).toBe(true)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('records the composition it ran under on the child header', async () => {
|
||||
const { parent } = await setupPresetHost()
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
await run.result
|
||||
|
||||
// Without this the child's own history reads back under the deployment
|
||||
// default, which is a different tool set than the one it actually used.
|
||||
expect(run.localAgent?.session.header.agentPreset).toBe('coding')
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('honours a tool filter over the preset tools it inherited', async () => {
|
||||
const { ctx, parent } = await setupPresetHost()
|
||||
|
||||
const run = await startInProcessRun(
|
||||
{ ...spawnRequest(parent), toolFilter: { deny: ['preset_only'] } },
|
||||
{},
|
||||
)
|
||||
await run.result
|
||||
|
||||
// The capability filter is the only thing bounding a delegated child, and
|
||||
// every tool it can name now arrives from the preset rather than the host.
|
||||
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual([])
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('follows a parent that switched preset while blank', async () => {
|
||||
const { ctx, parent } = await setupPresetHost()
|
||||
// A DIFFERENT preset, so the assertion below distinguishes reading the
|
||||
// parent's live scope chain from reading its creation header — re-linking
|
||||
// to the same id would pass either way.
|
||||
await ctx.agentPresets.recompose(parent.ctx, 'reviewing')
|
||||
|
||||
const run = await startInProcessRun(spawnRequest(parent), {})
|
||||
await run.result
|
||||
|
||||
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual(['reviewing_only'])
|
||||
expect(run.localAgent?.session.header.agentPreset).toBe('reviewing')
|
||||
await run.dispose()
|
||||
})
|
||||
})
|
||||
@@ -298,7 +298,7 @@ describe('startInProcessRun', () => {
|
||||
await expect(startInProcessRun({
|
||||
...request(parent),
|
||||
toolFilter: { deny: ['unknown-tool'] },
|
||||
}, {})).rejects.toThrow('unknown global tool')
|
||||
}, {})).rejects.toThrow('unknown inherited tool')
|
||||
expect(ctx.agents.list()).toHaveLength(beforeAgents)
|
||||
expect(ctx.sessions.list()).toHaveLength(beforeSessions)
|
||||
})
|
||||
|
||||
@@ -436,7 +436,7 @@ describe('dsh-subagent-spawn', () => {
|
||||
prompt: [{ type: 'text', text: 'do X' }],
|
||||
parent,
|
||||
toolFilter: { deny: ['no_such_tool'] },
|
||||
})).rejects.toThrow(/unknown global tool "no_such_tool"/)
|
||||
})).rejects.toThrow(/unknown inherited tool "no_such_tool"/)
|
||||
expect(ctx.agents.list().length).toBe(before)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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/README.md
|
||||
README.md: 30ecd187b08cd3d098ce791535914f80e4be9aed
|
||||
README.zh.md: 5e32a74469a67e02a0eb927c368080768e27d508
|
||||
README.md: 42a10adeccb8e299e25ff0a5e0a918ef09b79617
|
||||
README.zh.md: 34e2ed6c1ca23df9b3158f3caea10cd19bafa841
|
||||
|
||||
@@ -40,6 +40,10 @@ Start-time features are advertised in `provider.capabilities` because the servic
|
||||
- `toolFilter` — apply the requested child tool restriction.
|
||||
- `persona` — apply a per-child persona.
|
||||
|
||||
Every in-process child is composed by one call, `applyChildComposition(childCtx, parent, composition)`, which joins the parent's agent-preset composition before applying the child's own persona and tool filter. The join is what gives the child its capabilities: with every model-facing row on the agent plane, a child that joined nothing would reach the model with an empty tool registry ([`dsh-agent-presets`](../../preset/agent-presets/README.md)). Taking the parent as a parameter is deliberate — it makes composing a child WITHOUT that join unrepresentable at the call sites, which is the defect the one call exists to prevent. A deployment composing no preset roster joins nothing and needs nothing: its model-facing rows sit in the host composition, where the child already resolves them through the tool registry's global layer.
|
||||
|
||||
`childSessionMeta()` records the joined preset id on the child's durable header for the same reason a top-level session records its own: the preset decides the tool schemas and prompt sections the model saw, so a cold read of the child's history has to rebuild that composition rather than the deployment default. It is read from the parent's live scope chain, not from the parent header, because a parent that switched preset while blank runs on the newer composition while its header still names the older one.
|
||||
|
||||
Continuable creation is the optional `SubagentProvider.prepareContinuable?()` method: its presence is the capability check, so the service rejects a configured continuable start on a provider without it, while a provider that has it may still serve ordinary one-shot delegations. The method returns only a detached `ContinuableCreateSpec` (`{ seed? }`) — data, never a capability: it carries no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation, because the continuation manager owns identity reservation, composition, Agent creation, prompt delivery, cold resume, ownership, and disposal after preparation. A one-shot `SubagentRun` represents one disposable foreground delegation with one result and no cold-resume operation.
|
||||
|
||||
## The durable descriptor
|
||||
|
||||
@@ -40,6 +40,10 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
- `toolFilter`:应用请求的子 agent 工具限制;
|
||||
- `persona`:应用每个子 agent 独立的 persona。
|
||||
|
||||
每个进程内子 agent 都由一次调用完成组装:`applyChildComposition(childCtx, parent, composition)` 先加入父方的 agent-preset 组装,再应用该子 agent 自己的 persona 与工具限制。加入组装正是子 agent 获得能力的途径:所有面向模型的行都在 agent 平面,没有加入任何组装的子 agent 抵达模型时工具注册表是空的(见 [`dsh-agent-presets`](../../preset/agent-presets/README.md))。把父方作为参数是刻意的——这让"组装一个子 agent 却不做该加入"在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组装、也不需要加入:它的面向模型的行位于宿主组装中,子 agent 已经能通过工具注册表的全局层解析到它们。
|
||||
|
||||
`childSessionMeta()` 把所加入的 preset id 记在子 agent 的持久化 header 上,理由与顶层会话记录自己的那一个相同:preset 决定了模型所见的工具 schema 与提示段,因此冷读子 agent 的历史时必须重建那份组装,而不是部署默认值。该值从父方**活着的** scope 链读取,而不是从父方 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。
|
||||
|
||||
可继续创建对应可选的 `SubagentProvider.prepareContinuable?()` 方法:方法是否存在就是能力检查,因此服务会在没有该方法的提供方上拒绝已配置的可继续启动,而具备该方法的提供方仍可服务普通一次性委派。该方法只返回分离的 `ContinuableCreateSpec`(`{ seed? }`)——这是数据,绝非能力:它不携带任何 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作,因为准备之后,继续执行管理器拥有身份预留、组合、Agent 创建、提示词投递、冷恢复、所有权和 dispose。一次性 `SubagentRun` 表示一次可 dispose 的前台委派,只有一个结果,且没有冷恢复操作。
|
||||
|
||||
## 持久化描述符
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-presets": "^0.0.1",
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
@@ -50,6 +51,9 @@
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-agent-presets": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-sandbox": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -74,6 +78,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
|
||||
@@ -19,6 +19,12 @@ import type { ToolRestriction } from '@deepseek-ai/dsh-tools'
|
||||
// and merge the `sandbox/mode` / `approval/policy` session-event payloads.
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
// Type-only: make `ctx.get('agentPresets')` resolve to the preset roster when
|
||||
// composed — a child inherits its parent's composition opportunistically (the
|
||||
// documented `ctx.get` pattern), never as a hard dep. A rosterless deployment
|
||||
// keeps its model-facing rows on the host plane, where the child already sees
|
||||
// them through the tool registry's global layer.
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
import { delegationDepthOf } from './depth.ts'
|
||||
|
||||
/** Thrown when starting a child would exceed the requested depth cap. */
|
||||
@@ -79,8 +85,15 @@ export function resolveChildAgentOptions(
|
||||
/**
|
||||
* Build the child session's durable creation metadata: the parent's workspace,
|
||||
* its direct lineage, coarse product origin, the recursion budget that must
|
||||
* survive persistence, and the seed boundary that separates inherited parent
|
||||
* history from child work.
|
||||
* survive persistence, the seed boundary that separates inherited parent
|
||||
* history from child work, and the composition the child runs under.
|
||||
*
|
||||
* The preset is read from the parent's LIVE scope chain rather than from its
|
||||
* header, because a parent that switched preset while blank runs on the newer
|
||||
* composition and its header still names the older one. Recording it is what
|
||||
* makes a child's history reconstructable: without it a cold read of the child
|
||||
* resolves the deployment default and rebuilds turns under a tool set the
|
||||
* child never had.
|
||||
* @param parent - the delegating parent agent.
|
||||
* @param childDepth - the resolved delegation depth to persist.
|
||||
* @param lineageSeedLength - how many leading events came from the parent's log.
|
||||
@@ -92,8 +105,10 @@ export function childSessionMeta(
|
||||
lineageSeedLength: number,
|
||||
): NonNullable<CreateAgentOptions['meta']> {
|
||||
const parentHeader = parent.session.header
|
||||
const agentPreset = parent.ctx.get('agentPresets')?.composedPreset(parent.ctx)
|
||||
return {
|
||||
...parentHeader.cwd !== undefined ? { cwd: parentHeader.cwd } : {},
|
||||
...agentPreset === undefined ? {} : { agentPreset },
|
||||
parentSession: parentHeader.id,
|
||||
// Navigation classification only; the descriptor remains the authority
|
||||
// for mode and continuation capability.
|
||||
@@ -124,14 +139,33 @@ export const SUBAGENT_DELEGATION_CONTEXT
|
||||
+ 'limitation in your reply so the delegating agent can handle it.'
|
||||
|
||||
/**
|
||||
* Apply one child's scoped composition inside its creation window: the fixed
|
||||
* delegation-scope statement, a shadowing persona section, and a tool
|
||||
* restriction, all owned by the child's scope and therefore invisible to its
|
||||
* parent and siblings. Creation and cold resume both pass through here.
|
||||
* Compose one child inside its creation window: join its parent's preset,
|
||||
* register the fixed delegation-scope statement, then apply the child's own
|
||||
* shadowing persona section and tool restriction, all owned by the child's
|
||||
* scope and therefore invisible to its parent and siblings. Creation and cold
|
||||
* resume both pass through here.
|
||||
*
|
||||
* The join comes first and the child's own registrations second, which is the
|
||||
* order the layering already implies — the nearest scope wins a name, and a
|
||||
* per-child restriction intersects with everything its chain admits — but
|
||||
* stating it here keeps the two steps from being read as independent.
|
||||
*
|
||||
* The join and the per-child registrations live in ONE call because a child
|
||||
* composed without the join is exactly the defect this function exists to
|
||||
* prevent: with every model-facing row on the agent plane, a child that joins
|
||||
* no preset sees an empty tool registry and none of its parent's prompt
|
||||
* sections. Taking the parent as a parameter is what makes that omission
|
||||
* unrepresentable at the call sites.
|
||||
* @param childCtx - the child agent's scoped creation context.
|
||||
* @param composition - the persona and tool filter to install.
|
||||
* @param parent - the delegating parent whose composition the child joins.
|
||||
* @param composition - the per-child persona and tool filter to install.
|
||||
*/
|
||||
export function applyChildComposition(childCtx: Context, composition: ChildComposition): void {
|
||||
export function applyChildComposition(
|
||||
childCtx: Context,
|
||||
parent: Agent,
|
||||
composition: ChildComposition,
|
||||
): void {
|
||||
childCtx.get('agentPresets')?.composeFrom(childCtx, parent.ctx)
|
||||
// Order 120: after the sandbox:policy (110) and approval:policy (115) sentences.
|
||||
childCtx.systemPrompt.context({ name: 'subagent:delegation', order: 120, text: SUBAGENT_DELEGATION_CONTEXT })
|
||||
if (composition.persona !== undefined) {
|
||||
|
||||
@@ -906,7 +906,7 @@ export class SubagentContinuationManager {
|
||||
if (create !== undefined) {
|
||||
appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, create.delegatedPolicies)
|
||||
}
|
||||
applyChildComposition(childCtx, inputs.composition)
|
||||
applyChildComposition(childCtx, parent, inputs.composition)
|
||||
return this.setupRegistry.apply(childCtx)
|
||||
}
|
||||
const observer = this.host.observeActivation(provider, childId, parent)
|
||||
|
||||
@@ -63,7 +63,7 @@ function startSpec(parent: Agent, provider = 'spawn') {
|
||||
async function waitNoActivation(ctx: Context, childId: SessionId): Promise<void> {
|
||||
await vi.waitFor(() => {
|
||||
expect(ctx.agents.get(childId)).toBeUndefined()
|
||||
}, { timeout: 5_000 })
|
||||
}, { timeout: 15_000 })
|
||||
}
|
||||
|
||||
function policyEvents(events: readonly SessionEvent[]) {
|
||||
@@ -71,7 +71,7 @@ function policyEvents(events: readonly SessionEvent[]) {
|
||||
}
|
||||
|
||||
describe('continuable policy inheritance', () => {
|
||||
it('seeds the parent sandbox override and pins approval to never', async () => {
|
||||
it('seeds the parent sandbox override and pins approval to never', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('child done')])
|
||||
setSandboxMode(parent.session, 'danger-full-access')
|
||||
// No parent approval override: the child pin must not depend on one.
|
||||
@@ -109,7 +109,7 @@ describe('continuable policy inheritance', () => {
|
||||
expect(contextText).toContain('You are a delegated subagent')
|
||||
})
|
||||
|
||||
it('captures policy at delegation before asynchronous child creation', async () => {
|
||||
it('captures policy at delegation before asynchronous child creation', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('child done')])
|
||||
setSandboxMode(parent.session, 'read-only')
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('continuable policy inheritance', () => {
|
||||
expect(effectiveSandboxMode(loaded.events)).toBe('read-only')
|
||||
})
|
||||
|
||||
it('leaves an unswitched sandbox on the deployment default while still pinning approval', async () => {
|
||||
it('leaves an unswitched sandbox on the deployment default while still pinning approval', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('child done')])
|
||||
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
@@ -138,7 +138,7 @@ describe('continuable policy inheritance', () => {
|
||||
expect(effectiveSandboxMode(loaded.events)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('pins approval after the fork prefix of an unswitched fork child', async () => {
|
||||
it('pins approval after the fork prefix of an unswitched fork child', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('forked child')])
|
||||
parent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'parent work' }],
|
||||
@@ -157,7 +157,7 @@ describe('continuable policy inheritance', () => {
|
||||
expect(effectiveSandboxMode(loaded.events)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('lets a later child-side switch win over the delegation snapshot', async () => {
|
||||
it('lets a later child-side switch win over the delegation snapshot', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('child done')])
|
||||
setSandboxMode(parent.session, 'danger-full-access')
|
||||
let child: Agent | undefined
|
||||
@@ -177,7 +177,7 @@ describe('continuable policy inheritance', () => {
|
||||
expect(effectiveSandboxMode(loaded.events)).toBe('read-only')
|
||||
})
|
||||
|
||||
it('cold-resumes on the persisted snapshot without re-capturing the parent', async () => {
|
||||
it('cold-resumes on the persisted snapshot without re-capturing the parent', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('first'), textResponse('after resume')])
|
||||
setSandboxMode(parent.session, 'read-only')
|
||||
const started = await ctx.subagents.startContinuable(startSpec(parent))
|
||||
@@ -203,7 +203,7 @@ describe('continuable policy inheritance', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('places inherited events after a fork prefix so fresh policy wins stale seed state', async () => {
|
||||
it('places inherited events after a fork prefix so fresh policy wins stale seed state', { timeout: 20_000 }, async () => {
|
||||
const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('forked child')])
|
||||
// The stale mode lands inside the completed turn the fork seed replays.
|
||||
setSandboxMode(parent.session, 'workspace-write')
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
{
|
||||
"path": "../../interaction/user-approval"
|
||||
},
|
||||
{
|
||||
"path": "../../preset/agent-presets"
|
||||
},
|
||||
{
|
||||
"path": "../../sandbox/sandbox"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user