diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 9536e421ba..d7f4b74b77 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 docs/config-catalog.md -config-catalog.md: 14610ca9c06d5c4beb6ccb74795e5859952c58a4 +config-catalog.md: b588a80d6ee773fde52d9c3050b9d76438810d02 config-catalog.zh.md: d092947d31cfe4b24cae5d0ee8570dda39d7a287 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 14610ca9c0..b588a80d6e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -122,7 +122,7 @@ export interface Config { Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) -Source: [`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:255`](../packages/core/agent-loop/src/index.ts) ## `@deepseek-ai/dsh-agent-presets` diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index b8d32e778c..a0b7d947e6 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -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 docs/event-producer-consumer.md -event-producer-consumer.md: 622df039a42bccb1ecf8dcaf11a1c68ea111dec1 +event-producer-consumer.md: b9f8fbaf92e1d3da99bd233496c717d66a0f55e6 event-producer-consumer.zh.md: 976f41d7798e9182b60366d77e546d79c4a66a13 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 622df039a4..b9f8fbaf92 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,7 +7,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 84e060f8a8..c38702ce4b 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -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 docs/module-graph.md -module-graph.md: 65741283b5a48817bd01c42fe7982fff6c89511a -module-graph.zh.md: 00ea2925c6636a65e224c899ee264a4e87d4a2cd +module-graph.md: 949f8813f1d8f49e5a433049eb2f61c7235bf571 +module-graph.zh.md: 70085ab7301cf3ec81494189aea065c9a487b04c diff --git a/docs/module-graph.md b/docs/module-graph.md index 65741283b5..949f8813f1 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -759,6 +759,7 @@ flowchart TD pkg_agent_loop --> pkg_scope pkg_agent_loop --> pkg_session pkg_agent_loop --> pkg_session_persistence + pkg_agent_loop --> pkg_settings pkg_agent_loop --> pkg_system_prompt pkg_agent_loop --> pkg_tools pkg_agent_tool_mode --> pkg_invariants @@ -1375,7 +1376,7 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) | | [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | -| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-mode`](../packages/core/agent-tool-mode) | `core` | [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 00ea2925c6..70085ab730 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -761,6 +761,7 @@ flowchart TD pkg_agent_loop --> pkg_scope pkg_agent_loop --> pkg_session pkg_agent_loop --> pkg_session_persistence + pkg_agent_loop --> pkg_settings pkg_agent_loop --> pkg_system_prompt pkg_agent_loop --> pkg_tools pkg_agent_tool_mode --> pkg_invariants @@ -1377,7 +1378,7 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) | | [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | -| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-mode`](../packages/core/agent-tool-mode) | `core` | [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 0b2b87a954..3df05a5871 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -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 docs/subsystems/core.md -core.md: af27484160769156836f377e5b3aba2521280005 -core.zh.md: 12935f4d881f371cfe2c3c5bed85ef88f57ec71a +core.md: a900192f4cee2b53bbf4d2f6d00d9586d87d438a +core.zh.md: 17ad5f03b34b8acff37689c38ed4323096774c0a diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index af27484160..a900192f4c 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -375,7 +375,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise @@ -1002,5 +1002,5 @@ A declarative agent entry failed before it could publish a live agent. Consumers 'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void ``` -Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:183`](../../packages/core/agent-loop/src/index.ts) diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 12935f4d88..17ad5f03b3 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -383,7 +383,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise @@ -1010,5 +1010,5 @@ A declarative agent entry failed before it could publish a live agent. Consumers 'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void ``` -Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:183`](../../packages/core/agent-loop/src/index.ts) diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 60ceaa7572..12bdb09626 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -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/core/agent-loop/README.md -README.md: 6092363fae2853d6c5d92aaf8cd01e41e18e0b52 -README.zh.md: b65b5334d735a1e0b51fa517ce41c0c953f87cf7 +README.md: b16ba8186254468a335f6d55ce550de86a3436ef +README.zh.md: ad8fc9478eb45620cee303eb7295b6b3fe8b0ed3 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 6092363fae..b16ba81862 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -49,7 +49,7 @@ interface Config { } ``` -Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`. +Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`; it is also the whole of the `agent-loop` Settings section, so a user layer over this entry caps the next tool group without a restart, and a value that is not a positive integer is refused at the write rather than at that group. `agents` is deliberately absent from that section — it is consumed once when the service starts, so a stored change could only look like it had an effect. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`. ### Internal concrete driver diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index b65b5334d7..ad8fc9478e 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -49,7 +49,7 @@ interface Config { } ``` -通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。可选的正数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件为每个 agent 提供 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。 +通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。可选的正数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`;它同时也是 `agent-loop` Settings 段的全部内容,因此叠加在该条目之上的用户层无需重启即可限制下一组工具调用,而非正整数的值会在写入时被拒绝,而不是到那一组时才失败。`agents` 刻意不在该段中——它在服务启动时被消费一次,所以存储的改动只会看起来生效。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件为每个 agent 提供 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。 ### 包内部具体驱动器 diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 68c30d9e55..35c5be0145 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -30,6 +30,7 @@ "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", + "@deepseek-ai/dsh-settings": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -45,6 +46,7 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index e7c840e296..62fa565843 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -20,6 +20,7 @@ import type { SessionStartSource, } from '@deepseek-ai/dsh-agent' import { errorChain } from '@deepseek-ai/dsh-llm' +import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings' import { SessionId, SessionPreparation } from '@deepseek-ai/dsh-session' import type { Session, SessionHeader } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-system-prompt' @@ -232,6 +233,24 @@ function applyLauncherIdentities( }) } +/** Settings namespace carrying the tool-call parallelism a user owns. */ +export const AGENT_LOOP_SETTINGS_NAMESPACE = settingsNamespace('agent-loop') + +/** + * The agent-loop fields a user owns. Deliberately a strict subset of + * {@link Config}: `agents` is a boot-time composition array consumed once when + * the service starts, so a stored change could only look like it had an effect. + */ +export interface AgentLoopSettings { + /** Maximum parallel-safe calls in flight per agent step. */ + maxParallelToolCalls: number +} + +/** Schema of the agent-loop settings section. */ +export const AGENT_LOOP_SETTINGS_SCHEMA: z = z.object({ + maxParallelToolCalls: z.number().step(1).min(1).default(DEFAULT_MAX_PARALLEL_TOOL_CALLS), +}) + /** Agent-loop plugin configuration. */ export interface Config { /** @@ -299,11 +318,31 @@ export class AgentLoop extends Service implements AgentFactory { constructor(ctx: Context, config: Config) { super(ctx, 'agentLoop') + const entry: AgentLoopSettings = { + maxParallelToolCalls: resolveMaxParallelToolCalls(config.maxParallelToolCalls), + } + let source: () => AgentLoopSettings = () => entry this.config = { ...config, agents: applyLauncherIdentities(config.agents, ctx.get(CONFIGURED_AGENT_IDENTITIES_KEY)), - maxParallelToolCalls: resolveMaxParallelToolCalls(config.maxParallelToolCalls), + // Read through on every scheduler decision: `tool-calls.ts` destructures + // this at the start of each group, so a committed change caps the next + // group without disturbing the one in flight. + get maxParallelToolCalls() { + return source().maxParallelToolCalls + }, } + installSettingsSection(ctx, AGENT_LOOP_SETTINGS_NAMESPACE, AGENT_LOOP_SETTINGS_SCHEMA, entry, { + // The schema admits any integer above zero; `resolveMaxParallelToolCalls` + // owns the whole rule, so refusing here keeps the running scheduler on + // its last good cap instead of failing at the next tool group. + validate: value => void resolveMaxParallelToolCalls(value.maxParallelToolCalls), + setSource: (current) => { + source = current + }, + // Nothing is derived from the cap: the getter above is the only reader. + onChange: () => {}, + }) validateConfiguredAgents(this.config.agents) this.ownership = new FactoryOwnership(ctx.fiber) this.runtime = { ctx } diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index 74608e5f1a..1ad8ac1043 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -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() }) diff --git a/packages/core/agent-loop/tests/settings.spec.ts b/packages/core/agent-loop/tests/settings.spec.ts new file mode 100644 index 0000000000..f9f52be226 --- /dev/null +++ b/packages/core/agent-loop/tests/settings.spec.ts @@ -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 = {} + + get writable(): boolean { + return true + } + + protected load(): Promise> { + return Promise.resolve(structuredClone(this.doc)) + } + + protected persist(ns: SettingsNamespace, section: Record): Promise { + 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() + }) +}) diff --git a/packages/core/agent-loop/tsconfig.json b/packages/core/agent-loop/tsconfig.json index c3504e4eb4..724f0c6741 100644 --- a/packages/core/agent-loop/tsconfig.json +++ b/packages/core/agent-loop/tsconfig.json @@ -38,6 +38,9 @@ { "path": "../../core/scope" }, + { + "path": "../../settings/settings" + }, { "path": "../../support/invariants" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4600aca05c..fe6f610dff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3301,6 +3301,9 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../system-prompt