fix(tasks): bound per-session background work

This commit is contained in:
pku-xht
2026-08-11 16:23:05 +08:00
parent b6cd817aca
commit f32a51b284
46 changed files with 684 additions and 71 deletions

View File

@@ -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/examples/acp-demo/README.md
README.md: edc45c9857a631cef72eb41b1a98c390f112291e
README.zh.md: c2946aa3d1feaed558408cf0921e2480c031187d
README.md: c1a15a424d9d66b90bbec451e220198bfe0a45df
README.zh.md: 16a8d782c8fa38fe9e2ae1e634dbc406be1cbd12

View File

@@ -34,6 +34,7 @@ The app does not install commands, user interaction, session navigation, configu
| `workspaceContext` | required | Workspace-instruction byte budget/config, or `false`. |
| `skills` | owner defaults | Skill registry, local provider, and model-facing skill tool. |
| `toolBash` | owner defaults | Model-facing bash tool config. |
| `tasks` | `{ maxConcurrentTasksPerOwner: 10 }` | Process-local per-owner active-task admission. |
| `toolTasks` | owner defaults | Generic background-task control config, or `false`. |
| `goals` | owner defaults | Persisted same-session goal domain and model tools, or `false`. |

View File

@@ -34,6 +34,7 @@ ACPAgent Client Protocol自动化服务器应用默认 agent智能
| `workspaceContext` | 必填 | 工作区指令字节预算/配置,或 `false`。 |
| `skills` | 拥有者默认值 | skill 注册表、本地提供方和面向模型的 skill 工具。 |
| `toolBash` | 拥有者默认值 | 面向模型的 bash 工具配置。 |
| `tasks` | `{ maxConcurrentTasksPerOwner: 10 }` | 进程内按 owner 限制活动任务的准入配置。 |
| `toolTasks` | 拥有者默认值 | 通用后台任务控制配置,或 `false`。 |
| `goals` | 拥有者默认值 | 持久化的同会话目标领域与模型工具,或 `false`。 |

View File

@@ -65,6 +65,8 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Process-local background-task admission config forwarded through agent-core. */
tasks?: NonNullable<agentCore.Config['tasks']>
/** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
@@ -92,6 +94,7 @@ export const Config: z<Config> = z.object({
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
tasks: agentCore.TasksConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
})

View File

@@ -182,6 +182,19 @@ describe('dsh-acp-demo composition', () => {
await ctx.fiber.dispose()
})
it('forwards task admission config to the bundled task provider', async () => {
const ctx = await mount({
provider: 'mock',
model: 'mock',
tasks: { maxConcurrentTasksPerOwner: 2 },
skills: await isolatedSkillsConfig(),
workspaceContext: false,
})
expect((ctx.tasks as unknown as { config: { maxConcurrentTasksPerOwner: number } })
.config.maxConcurrentTasksPerOwner).toBe(2)
await ctx.fiber.dispose()
})
it('forwards bundled tool config into agent-core', async () => {
const ctx = await mount({
provider: 'mock',

View File

@@ -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/examples/agent-spine-demo/README.md
README.md: 5957d9a8e9218e18d5d7d0f620b6be811f2c230f
README.zh.md: 78372240764ff3c779ea0805aedd26a00baf1768
README.md: 789715e53038f610d1e2db79cf56f9aabd681fac
README.zh.md: 6561dd8948d9a4763123f9aae31e6dde64c54b27

View File

@@ -55,11 +55,11 @@ This applies the [Service Definition / Service provider / Consumer separation](.
```ts
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition.
The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition.
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.

View File

@@ -55,11 +55,11 @@
```ts
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider并与面向模型的 `toolTasks` 控制工具相互独立`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../support/invariants/README.md)。

View File

@@ -22,7 +22,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import GoalService, { type Config as GoalDomainConfig } from '@deepseek-ai/dsh-goal'
import * as goalSession from '@deepseek-ai/dsh-goal-session'
import * as toolGoal from '@deepseek-ai/dsh-tool-goal'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import LocalTaskService, { type Config as TasksConfig } from '@deepseek-ai/dsh-tasks-local'
import InvariantService, { type Config as InvariantConfig } from '@deepseek-ai/dsh-invariants'
import * as sessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as agentInvariant from '@deepseek-ai/dsh-agent/invariant'
@@ -75,9 +75,10 @@ export interface GoalConfig {
* `dshHome` to bash environment and local skill discovery, `sessionTitle` to
* the fallback title service, `skills` to the
* skill registry/local provider/tool consumer, `workspaceContext` to the
* workspace-context loader, and `toolBash`/`toolTasks` to the model-facing tool
* plugins this bundle owns. Provider adapters own their `retryPolicy`; this
* bundle always mounts its executor.
* workspace-context loader, `tasks` to the process-local task provider, and
* `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
* Provider adapters own their `retryPolicy`; this bundle always mounts its
* executor.
* `goals` opts into and configures the persisted goal domain plus its model tool
* and same-session driver; `invariants` configures global and package-filtered
* relational checks. Owner schemas supply defaults for optional input;
@@ -114,6 +115,8 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, or false when another plugin owns `bash`. */
toolBash?: toolBash.Config | false
/** Process-local background-task admission config. */
tasks?: TasksConfig
/** Generic background-task controls; set false to keep the task service without model-facing task tools. */
toolTasks?: toolTasks.Config | false
/** Global enablement and package-name filters for invariant companions. */
@@ -138,6 +141,9 @@ export const SessionTitleConfigSchema: z<SessionTitleConfig> = SessionTitleServi
export const ToolBashConfigSchema: z<toolBash.Config | false> =
z.union([z.const(false), toolBash.Config])
/** The process-local task registry schema exported for app packages that forward `tasks`. */
export const TasksConfigSchema: z<TasksConfig> = LocalTaskService.Config
/** The task-control-tool config schema exported for app packages that forward `toolTasks`. */
export const ToolTasksConfigSchema: z<toolTasks.Config> = toolTasks.Config
@@ -158,10 +164,11 @@ export const Config = z.intersect([
skills: SkillConfigSchema,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
toolBash: ToolBashConfigSchema,
tasks: TasksConfigSchema,
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
invariants: InvariantService.Config,
goals: z.union([z.const(false), GoalConfigSchema]),
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'sessionTitle' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants' | 'goals'>>,
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'sessionTitle' | 'skills' | 'workspaceContext' | 'toolBash' | 'tasks' | 'toolTasks' | 'invariants' | 'goals'>>,
]) as unknown as z<Config>
/**
@@ -181,6 +188,7 @@ export function pickSpineConfig(config: Omit<Config, 'agents'>): Omit<Config, 'a
workspaceContext: config.workspaceContext,
...config.skills !== undefined ? { skills: config.skills } : {},
...config.toolBash !== undefined ? { toolBash: config.toolBash } : {},
...config.tasks !== undefined ? { tasks: config.tasks } : {},
...config.toolTasks !== undefined ? { toolTasks: config.toolTasks } : {},
...config.invariants !== undefined ? { invariants: config.invariants } : {},
...config.goals !== undefined ? { goals: config.goals } : {},
@@ -228,7 +236,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(toolGoal, config.goals.tool ?? {})
ctx.plugin(goalSession)
}
ctx.plugin(LocalTaskService)
ctx.plugin(LocalTaskService, config.tasks ?? {})
ctx.plugin(InvariantService, config.invariants ?? {})
ctx.plugin(sessionInvariant)
ctx.plugin(agentInvariant)

View File

@@ -10,6 +10,7 @@ import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import {
@@ -157,6 +158,7 @@ describe('dsh-agent-spine-demo bundle', () => {
expect(ctx.get('skills')).toBeDefined()
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('tasks')).toBeDefined()
expect((ctx.tasks as LocalTaskService).config.maxConcurrentTasksPerOwner).toBe(10)
expect(ctx.get('invariants')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
expect(ctx.get('goals')).toBeUndefined()
@@ -300,6 +302,15 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.fiber.dispose()
})
it('forwards task admission config to the process-local provider', async () => {
const ctx = await mount({
tasks: { maxConcurrentTasksPerOwner: 3 },
workspaceContext: false,
})
expect((ctx.tasks as LocalTaskService).config.maxConcurrentTasksPerOwner).toBe(3)
await ctx.fiber.dispose()
})
it('tolerates a schema-bypassing direct apply (the ?? fallbacks fire)', async () => {
// ctx.plugin validates + defaults the bundle config first; a direct apply
// skips the schema, so the forwarding `?? []` / `?? ''` are what fire.
@@ -716,6 +727,7 @@ describe('dsh-agent-spine-demo bundle', () => {
workspaceContext: false as const,
skills: { enabled: false },
toolBash: { enableRunInBackground: false },
tasks: { maxConcurrentTasksPerOwner: 4 },
toolTasks: false as const,
invariants: { enabled: false },
}
@@ -730,6 +742,7 @@ describe('dsh-agent-spine-demo bundle', () => {
workspaceContext: false,
skills: appConfig.skills,
toolBash: appConfig.toolBash,
tasks: appConfig.tasks,
toolTasks: appConfig.toolTasks,
invariants: appConfig.invariants,
})