feat(agent-loop): make the parallel tool-call cap a user setting
The section is a strict subset of the plugin config: `agents` is consumed once when the service starts, so a stored change there could only look like it had an effect. The cap resolves through a getter over the settings source, which the scheduler destructures at the start of each tool group, so a committed change bounds the next group without disturbing the one in flight. `resolveMaxParallelToolCalls` becomes the section validator, refusing a value at the write instead of at that group. The deferred-resume effect-shape assertion now allows the one plugin effect the optional settings wiring adds at the fiber's own level; a resumed agent joining it there is still the regression it pins.
This commit is contained in:
@@ -344,7 +344,10 @@ describe('config-driven session id', () => {
|
||||
|
||||
const resumeEffect = loopFiber.getEffects().find(effect => effect.label === 'agentLoop.resume(main)')
|
||||
expect(resumeEffect?.children.map(child => child.label)).toEqual(['ctx.plugin()'])
|
||||
expect(loopFiber.getEffects().filter(effect => effect.label === 'ctx.plugin()')).toEqual([])
|
||||
// Exactly one plugin effect sits at the fiber's own level — the optional
|
||||
// settings wiring, whose `ctx.inject` cordis labels like any other plugin.
|
||||
// A resumed agent joining it there is the regression this pins.
|
||||
expect(loopFiber.getEffects().filter(effect => effect.label === 'ctx.plugin()')).toHaveLength(1)
|
||||
|
||||
await loopFiber.dispose()
|
||||
})
|
||||
|
||||
106
packages/core/agent-loop/tests/settings.spec.ts
Normal file
106
packages/core/agent-loop/tests/settings.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/** The `agent-loop` settings section layered over the composition entry. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Fiber } from 'cordis'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import { Settings } from '@deepseek-ai/dsh-settings'
|
||||
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import AgentLoop, { AGENT_LOOP_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-loop'
|
||||
|
||||
/** The smallest real provider: one in-memory document, always writable. */
|
||||
class MemorySettings extends Settings {
|
||||
doc: Record<string, unknown> = {}
|
||||
|
||||
get writable(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
protected load(): Promise<Record<string, unknown>> {
|
||||
return Promise.resolve(structuredClone(this.doc))
|
||||
}
|
||||
|
||||
protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
|
||||
this.doc = { ...this.doc, [ns]: structuredClone(section) }
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber: Fiber }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const settingsFiber = ctx.plugin(MemorySettings)
|
||||
await settingsFiber.await()
|
||||
const loopFiber = ctx.plugin(AgentLoop, { agents: [], maxParallelToolCalls: 4 })
|
||||
await loopFiber.await()
|
||||
return { ctx, settingsFiber, loopFiber }
|
||||
}
|
||||
|
||||
describe('agent-loop settings section', () => {
|
||||
it('layers the stored parallel cap over the composition entry', async () => {
|
||||
const bench = await boot()
|
||||
expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
|
||||
|
||||
await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 1 })
|
||||
|
||||
expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(1)
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('refuses a non-positive cap at the write', async () => {
|
||||
const bench = await boot()
|
||||
|
||||
await expect(bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 0 }))
|
||||
.rejects.toThrow()
|
||||
|
||||
expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('never offers the composed agents array to the settings document', async () => {
|
||||
const bench = await boot()
|
||||
|
||||
const descriptor = bench.ctx.settings.describe().find(row => String(row.ns) === 'agent-loop')
|
||||
|
||||
expect(Object.keys(descriptor?.value as object)).toEqual(['maxParallelToolCalls'])
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('keeps serving the composed agents array to its own consumers', async () => {
|
||||
const bench = await boot()
|
||||
|
||||
await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 2 })
|
||||
|
||||
expect(bench.ctx.agentLoop.config.agents).toEqual([])
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('falls back to the composition entry when the settings provider detaches', async () => {
|
||||
const bench = await boot()
|
||||
await bench.ctx.settings.update(AGENT_LOOP_SETTINGS_NAMESPACE, { maxParallelToolCalls: 1 })
|
||||
expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(1)
|
||||
|
||||
await bench.settingsFiber.dispose()
|
||||
|
||||
expect(bench.ctx.agentLoop.config.maxParallelToolCalls).toBe(4)
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('releases the namespace when the service unloads', async () => {
|
||||
const bench = await boot()
|
||||
expect(bench.ctx.settings.describe().map(row => String(row.ns))).toContain('agent-loop')
|
||||
|
||||
await bench.loopFiber.dispose()
|
||||
|
||||
expect(bench.ctx.settings.describe().map(row => String(row.ns))).not.toContain('agent-loop')
|
||||
await bench.ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user