model-selection-settings.spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. /** Default-off settings and per-session model-selection decisions. */
  2. import { describe, expect, it, vi } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { ToolCallId } from '@deepseek-ai/dsh-llm'
  5. import { Session, SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
  6. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  7. import { bindScopeParent, createScope, scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
  8. import { SettingsProvider } from '@deepseek-ai/dsh-settings'
  9. import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
  10. import InvariantRegistry from '@deepseek-ai/dsh-invariants'
  11. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  12. import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
  13. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  14. import SubagentRuntime from '@deepseek-ai/dsh-subagent'
  15. import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
  16. import * as tool from '../src/index.ts'
  17. import * as ToolInvariant from '../src/invariant.ts'
  18. import SubagentModelSelectionConfig, {
  19. SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE,
  20. } from '../src/model-selection-settings.ts'
  21. import {
  22. subagentModelSelectionPolicy,
  23. subagentModelSelectionProjectionDefinition,
  24. } from '../src/model-selection-state.ts'
  25. import { callSubagent, text } from './harness.ts'
  26. const ALLOWED_MODELS = [{ provider: 'alpha', model: 'fast-model' }]
  27. /** Writable in-memory settings provider for the package integration. */
  28. class MemorySettings extends SettingsProvider {
  29. doc: Record<string, unknown> = {}
  30. get writable(): boolean {
  31. return true
  32. }
  33. protected load(): Promise<Record<string, unknown>> {
  34. return Promise.resolve(structuredClone(this.doc))
  35. }
  36. protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
  37. this.doc = { ...this.doc, [ns]: structuredClone(section) }
  38. return Promise.resolve()
  39. }
  40. }
  41. /** Read whether one Agent's delegation definition contains route fields. */
  42. function selectable(ctx: Context, agent: Awaited<ReturnType<Context['agents']['create']>>['agent']): boolean {
  43. const schema = ctx.tools.schemas(agent).find(candidate => candidate.name === 'subagent')
  44. const properties = (schema?.parameters as { properties?: Record<string, unknown> } | undefined)?.properties
  45. return properties?.['provider'] !== undefined
  46. && properties['model'] !== undefined
  47. && properties['reasoning_effort'] !== undefined
  48. && ctx.tools.schemas(agent).some(candidate => candidate.name === 'list_subagent_models')
  49. }
  50. const modelSelectionPresets = new WeakMap<Context, ReturnType<typeof createScope>>()
  51. /** Mount the real settings, Agent, provider, and optional preset tool services. */
  52. async function boot(withPreset = true): Promise<Context> {
  53. const ctx = new Context()
  54. await ctx.plugin(MemorySettings)
  55. await ctx.plugin(SubagentModelSelectionConfig)
  56. await mountAgentLoopTestDependencies(ctx)
  57. await ctx.plugin(AgentLoop, { agents: [] })
  58. await ctx.plugin(SubagentRuntime)
  59. await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
  60. if (withPreset) {
  61. const preset = createScope(ctx, { preset: 'model-selection-test' })
  62. await preset.ctx.plugin(tool, {
  63. provider: 'spawn',
  64. modelSelectionSettings: true,
  65. backgroundMode: 'continuable',
  66. })
  67. modelSelectionPresets.set(ctx, preset)
  68. }
  69. return ctx
  70. }
  71. /** Create one Agent joined to the test's standing preset. */
  72. async function createAgent(ctx: Context, id: string, options: {
  73. meta?: { parentSession: SessionId; origin: 'subagent' }
  74. seed?: readonly SessionEvent[]
  75. } = {}) {
  76. const preset = modelSelectionPresets.get(ctx)
  77. if (preset === undefined) throw new Error('context has no model-selection preset')
  78. const handle = await ctx.agents.create({
  79. sessionId: SessionId(id),
  80. ...options,
  81. setup: (agentCtx) => {
  82. bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!)
  83. },
  84. })
  85. return handle.agent
  86. }
  87. describe('SubagentModelSelectionConfig', () => {
  88. it('uses the composed default without a settings provider', async () => {
  89. const ctx = new Context()
  90. await ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: ALLOWED_MODELS })
  91. expect(ctx.subagentModelSelection.current()).toEqual({ enabled: true, allowedModels: ALLOWED_MODELS })
  92. await ctx.fiber.dispose()
  93. })
  94. it('defaults off and follows the validated user layer', async () => {
  95. const ctx = new Context()
  96. await ctx.plugin(MemorySettings)
  97. await ctx.plugin(SubagentModelSelectionConfig)
  98. expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: [] })
  99. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  100. enabled: true,
  101. allowedModels: ALLOWED_MODELS,
  102. })
  103. expect(ctx.subagentModelSelection.current()).toEqual({ enabled: true, allowedModels: ALLOWED_MODELS })
  104. await ctx.fiber.dispose()
  105. })
  106. it('rejects duplicate routes, enabled empty settings, and an empty durable policy', async () => {
  107. const ctx = new Context()
  108. await ctx.plugin(MemorySettings)
  109. await ctx.plugin(SubagentModelSelectionConfig)
  110. await ctx.plugin(SessionProjectionRegistry)
  111. ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
  112. await expect(ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  113. allowedModels: [...ALLOWED_MODELS, ...ALLOWED_MODELS],
  114. })).rejects.toThrow('repeats route "alpha/fast-model"')
  115. await expect(ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  116. enabled: true,
  117. allowedModels: [],
  118. })).rejects.toThrow('enabled subagent model selection requires at least one allowed model')
  119. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  120. enabled: false,
  121. allowedModels: ALLOWED_MODELS,
  122. })
  123. expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: ALLOWED_MODELS })
  124. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { allowedModels: [] })
  125. expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: [] })
  126. const invalid = Session.create(SessionId('empty-policy'))
  127. invalid.append('subagent/model-selection-policy', { allowedModels: [] })
  128. expect(() => subagentModelSelectionPolicy(ctx.sessionProjections, invalid)).toThrow('requires at least one route')
  129. const malformed = Session.create(SessionId('malformed-policy'))
  130. malformed.append('subagent/model-selection-policy', {
  131. allowedModels: [{ provider: 1, model: 'fast-model' }],
  132. } as never)
  133. expect(() => subagentModelSelectionPolicy(ctx.sessionProjections, malformed))
  134. .toThrow('requires non-empty provider and model ids')
  135. await ctx.fiber.dispose()
  136. })
  137. it('samples each new root Session without changing existing definitions', async () => {
  138. const ctx = await boot()
  139. const disabled = await createAgent(ctx, 'disabled')
  140. expect(selectable(ctx, disabled)).toBe(false)
  141. expect(subagentModelSelectionPolicy(ctx.sessionProjections, disabled.session)).toBeUndefined()
  142. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  143. enabled: true,
  144. allowedModels: ALLOWED_MODELS,
  145. })
  146. const enabled = await createAgent(ctx, 'enabled')
  147. expect(subagentModelSelectionPolicy(ctx.sessionProjections, enabled.session)).toEqual(ALLOWED_MODELS)
  148. expect(selectable(ctx, enabled)).toBe(true)
  149. expect(selectable(ctx, disabled)).toBe(false)
  150. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
  151. const disabledAgain = await createAgent(ctx, 'disabled-again')
  152. expect(selectable(ctx, disabledAgain)).toBe(false)
  153. expect(selectable(ctx, enabled)).toBe(true)
  154. await ctx.fiber.dispose()
  155. })
  156. it('installs a direct Agent setup before Session publication', async () => {
  157. const ctx = await boot(false)
  158. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  159. enabled: true,
  160. allowedModels: ALLOWED_MODELS,
  161. })
  162. let prepared: Awaited<ReturnType<Context['agents']['create']>>['agent'] | undefined
  163. let visibleAtSessionCreated = false
  164. ctx.on('session/created', () => {
  165. visibleAtSessionCreated = prepared !== undefined && selectable(ctx, prepared)
  166. })
  167. const handle = await ctx.agents.create({
  168. sessionId: SessionId('direct-agent-setup'),
  169. setup: async (agentCtx, agent) => {
  170. prepared = agent
  171. const fiber = agentCtx.inject(tool.inject, (runtimeCtx) => {
  172. tool.apply(runtimeCtx, {
  173. provider: 'spawn',
  174. modelSelectionSettings: true,
  175. backgroundMode: 'continuable',
  176. }, agent.session)
  177. })
  178. await fiber.await()
  179. },
  180. })
  181. expect(visibleAtSessionCreated).toBe(true)
  182. expect(selectable(ctx, handle.agent)).toBe(true)
  183. await handle.dispose()
  184. await ctx.fiber.dispose()
  185. })
  186. it('installs one tool when recording the Session policy triggers a registry refresh', async () => {
  187. const ctx = await boot()
  188. try {
  189. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  190. enabled: true,
  191. allowedModels: ALLOWED_MODELS,
  192. })
  193. const preset = modelSelectionPresets.get(ctx)!
  194. ctx.on('session/event', (_session, event) => {
  195. if (event.type === 'subagent/model-selection-policy') {
  196. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  197. }
  198. })
  199. const register = vi.spyOn(ctx.tools, 'register')
  200. const agent = await createAgent(ctx, 'policy-refresh')
  201. expect(selectable(ctx, agent)).toBe(true)
  202. expect(register.mock.calls.filter(([definition]) => definition.name === 'subagent')).toHaveLength(1)
  203. } finally {
  204. await ctx.fiber.dispose()
  205. }
  206. })
  207. it('rejects a forced route outside the Session policy before child creation', async () => {
  208. const ctx = await boot()
  209. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  210. enabled: true,
  211. allowedModels: ALLOWED_MODELS,
  212. })
  213. const agent = await createAgent(ctx, 'enforced')
  214. const result = await ctx.tools.execute({
  215. signal: new AbortController().signal,
  216. callId: ToolCallId('disallowed-session-route'),
  217. name: 'subagent',
  218. arguments: {
  219. description: 'forced route',
  220. prompt: 'do it',
  221. provider: 'alpha',
  222. model: 'other-model',
  223. },
  224. agent,
  225. })
  226. expect(result.isError).toBe(true)
  227. expect(text(result)).toContain('is not allowed for this Session')
  228. await ctx.fiber.dispose()
  229. })
  230. it('installs per-Agent definitions for a shared preset scope', async () => {
  231. const ctx = await boot(false)
  232. await ctx.plugin(InvariantRegistry, { enabled: true })
  233. await ctx.plugin(ToolInvariant)
  234. const preset = createScope(ctx, { preset: 'standard' })
  235. const other = createScope(ctx, { preset: 'minimal' })
  236. await preset.ctx.plugin(tool, {
  237. provider: 'spawn',
  238. modelSelectionSettings: true,
  239. backgroundMode: 'continuable',
  240. })
  241. let enabledBinding: ReturnType<typeof bindScopeParent> | undefined
  242. const createComposed = async (id: string) => ctx.agents.create({
  243. sessionId: SessionId(id),
  244. setup: (agentCtx) => {
  245. const binding = bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!)
  246. if (id === 'preset-enabled') enabledBinding = binding
  247. },
  248. })
  249. const disabled = await createComposed('preset-disabled')
  250. expect(selectable(ctx, disabled.agent)).toBe(false)
  251. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  252. enabled: true,
  253. allowedModels: ALLOWED_MODELS,
  254. })
  255. const enabled = await createComposed('preset-enabled')
  256. expect(selectable(ctx, enabled.agent)).toBe(true)
  257. expect(selectable(ctx, disabled.agent)).toBe(false)
  258. enabledBinding!.rebind(scopeOf(other.ctx)!)
  259. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  260. await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(false) })
  261. const next = () => Promise.resolve({ kind: 'enter' as const, messages: [] })
  262. const payload = {
  263. agent: enabled.agent,
  264. messages: [],
  265. turn: 1,
  266. step: 1,
  267. signal: new AbortController().signal,
  268. }
  269. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
  270. .resolves.toEqual({ kind: 'enter', messages: [] })
  271. enabledBinding!.rebind(scopeOf(preset.ctx)!)
  272. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  273. await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(true) })
  274. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
  275. .resolves.toEqual({ kind: 'enter', messages: [] })
  276. await enabled.dispose()
  277. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  278. await disabled.dispose()
  279. await ctx.fiber.dispose()
  280. })
  281. it('releases a shared-preset installation reservation after policy selection fails', async () => {
  282. const ctx = await boot(false)
  283. const preset = createScope(ctx, { preset: 'standard' })
  284. const other = createScope(ctx, { preset: 'minimal' })
  285. await preset.ctx.plugin(tool, {
  286. provider: 'spawn',
  287. modelSelectionSettings: true,
  288. backgroundMode: 'continuable',
  289. })
  290. let binding: ReturnType<typeof bindScopeParent> | undefined
  291. const handle = await ctx.agents.create({
  292. sessionId: SessionId('preset-policy-retry'),
  293. setup: (agentCtx) => {
  294. binding = bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!)
  295. },
  296. })
  297. expect(selectable(ctx, handle.agent)).toBe(false)
  298. binding!.rebind(scopeOf(other.ctx)!)
  299. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  300. binding!.rebind(scopeOf(preset.ctx)!)
  301. vi.spyOn(ctx.subagentModelSelection, 'current')
  302. .mockImplementationOnce(() => { throw new Error('transient settings read') })
  303. .mockReturnValue({ enabled: true, allowedModels: ALLOWED_MODELS })
  304. expect(() => { ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') })
  305. .toThrow('transient settings read')
  306. ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
  307. await vi.waitFor(() => { expect(selectable(ctx, handle.agent)).toBe(true) })
  308. await handle.dispose()
  309. await ctx.fiber.dispose()
  310. })
  311. it('inherits the parent decision and preserves seeded decisions across composition', async () => {
  312. const ctx = await boot()
  313. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  314. enabled: true,
  315. allowedModels: ALLOWED_MODELS,
  316. })
  317. const parent = await createAgent(ctx, 'parent')
  318. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
  319. const child = await createAgent(ctx, 'child', {
  320. meta: { parentSession: parent.id, origin: 'subagent' },
  321. })
  322. expect(selectable(ctx, child)).toBe(true)
  323. expect(subagentModelSelectionPolicy(ctx.sessionProjections, child.session)).toEqual(ALLOWED_MODELS)
  324. const orphan = await createAgent(ctx, 'orphan', {
  325. meta: { parentSession: SessionId('missing-parent'), origin: 'subagent' },
  326. })
  327. expect(selectable(ctx, orphan)).toBe(false)
  328. const enabledSeed = Session.create(SessionId('enabled-seed'))
  329. enabledSeed.append('subagent/model-selection-policy', { allowedModels: ALLOWED_MODELS })
  330. const resumedEnabled = await createAgent(ctx, 'resumed-enabled', { seed: enabledSeed.snapshotEvents() })
  331. expect(selectable(ctx, resumedEnabled)).toBe(true)
  332. const oldSeed = Session.create(SessionId('old-seed'), [])
  333. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  334. enabled: true,
  335. allowedModels: ALLOWED_MODELS,
  336. })
  337. const resumedEmpty = await createAgent(ctx, 'resumed-empty', { seed: [] })
  338. expect(selectable(ctx, resumedEmpty)).toBe(false)
  339. expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedEmpty.session)).toBeUndefined()
  340. const resumedDisabled = await createAgent(ctx, 'resumed-disabled', { seed: oldSeed.snapshotEvents() })
  341. expect(selectable(ctx, resumedDisabled)).toBe(false)
  342. expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedDisabled.session)).toBeUndefined()
  343. await ctx.fiber.dispose()
  344. })
  345. it('requires both the Host setting owner and a scoped standing preset', async () => {
  346. const withoutSettings = new Context()
  347. await mountAgentLoopTestDependencies(withoutSettings)
  348. await withoutSettings.plugin(SubagentRuntime)
  349. expect(() => {
  350. tool.apply(withoutSettings, {
  351. provider: 'missing',
  352. modelSelectionSettings: true,
  353. maxDepth: 'provider-managed',
  354. })
  355. }).toThrow('requires @deepseek-ai/dsh-tool-subagent/model-selection-settings')
  356. await withoutSettings.fiber.dispose()
  357. const withoutAgent = await boot(false)
  358. expect(() => {
  359. tool.apply(withoutAgent, {
  360. provider: 'spawn',
  361. modelSelectionSettings: true,
  362. backgroundMode: 'continuable',
  363. })
  364. }).toThrow('requires a scoped preset Context')
  365. await withoutAgent.fiber.dispose()
  366. })
  367. it('requires the Session registry when a child inherits its parent policy', async () => {
  368. const ctx = new Context()
  369. try {
  370. await ctx.plugin(SubagentModelSelectionConfig)
  371. await ctx.plugin(SessionProjectionRegistry)
  372. await ctx.plugin(SubagentRuntime)
  373. const childId = SessionId('child-without-session-registry')
  374. const child = Session.create(childId, undefined, {
  375. version: SESSION_FORMAT_VERSION,
  376. id: childId,
  377. createdAt: 1,
  378. isSeeded: false,
  379. origin: 'subagent',
  380. parentSession: SessionId('missing-parent'),
  381. })
  382. expect(() => {
  383. tool.apply(ctx, {
  384. provider: 'missing',
  385. modelSelectionSettings: true,
  386. maxDepth: 'provider-managed',
  387. }, child)
  388. }).toThrow('child model-selection inheritance requires the Session registry')
  389. } finally {
  390. await ctx.fiber.dispose()
  391. }
  392. })
  393. it('checks model-selectable definitions without rejecting a policy-only preset', async () => {
  394. const ctx = await boot()
  395. await ctx.plugin(InvariantRegistry, { enabled: true })
  396. await ctx.plugin(ToolInvariant)
  397. const disabled = await createAgent(ctx, 'invariant-disabled')
  398. const next = () => Promise.resolve({ kind: 'enter' as const, messages: [] })
  399. const payload = {
  400. agent: disabled,
  401. messages: [],
  402. turn: 1,
  403. step: 1,
  404. signal: new AbortController().signal,
  405. }
  406. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next)).resolves.toEqual({
  407. kind: 'enter', messages: [],
  408. })
  409. disabled.session.append('subagent/model-selection-policy', { allowedModels: ALLOWED_MODELS })
  410. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
  411. .resolves.toEqual({ kind: 'enter', messages: [] })
  412. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
  413. enabled: true,
  414. allowedModels: ALLOWED_MODELS,
  415. })
  416. const enabled = await createAgent(ctx, 'invariant-enabled')
  417. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: enabled }, next))
  418. .resolves.toEqual({ kind: 'enter', messages: [] })
  419. const enabledSchemas = ctx.tools.schemas(enabled)
  420. const schemas = vi.spyOn(ctx.tools, 'schemas')
  421. schemas.mockReturnValue(enabledSchemas.filter(schema => schema.name !== 'list_subagent_models'))
  422. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: enabled }, next))
  423. .rejects.toThrow('require a durable policy, route fields, and list_subagent_models')
  424. schemas.mockReturnValue(enabledSchemas)
  425. await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
  426. const withoutPolicy = await createAgent(ctx, 'invariant-without-policy')
  427. await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: withoutPolicy }, next))
  428. .rejects.toThrow('require a durable policy, route fields, and list_subagent_models')
  429. await ctx.fiber.dispose()
  430. })
  431. })
  432. it('reads the saved default depth at each delegation without remounting the tool', async () => {
  433. const ctx = await boot(false)
  434. const depths: Array<number | undefined> = []
  435. try {
  436. ctx.subagents.registerProvider({
  437. name: 'capture-depth',
  438. capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
  439. inheritsParentContext: false,
  440. start: async (request) => {
  441. depths.push(request.maxDepth)
  442. return { id: SessionId(`depth-${depths.length}`), localAgent: undefined,
  443. result: Promise.resolve({ output: [], stopReason: 'completed' as const }), dispose: async () => {} }
  444. },
  445. })
  446. await ctx.plugin(tool, { provider: 'capture-depth' })
  447. await callSubagent(ctx, { description: 'first', prompt: 'work' })
  448. await ctx.settings.update('subagent', { maxDepth: 5 })
  449. await callSubagent(ctx, { description: 'second', prompt: 'work' })
  450. await ctx.settings.update('subagent', { maxDepth: 0 })
  451. await callSubagent(ctx, { description: 'disabled', prompt: 'work' })
  452. expect(depths).toEqual([1, 5, 0])
  453. } finally {
  454. await ctx.fiber.dispose()
  455. }
  456. })