Merge remote-tracking branch 'origin/codex/invariant-service-seam' into codex/invariant-package-registration-gate

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml
#	docs/core-data-structures/session.md
#	docs/module-graph.md
#	package.json
#	packages/sdk/scripts/tsconfig.json
This commit is contained in:
Tianyi Cui
2026-07-20 00:35:06 +08:00
677 changed files with 6993 additions and 2418 deletions
+8 -4
View File
@@ -39,9 +39,9 @@ The spine is everything COMMON to every front door. The swappable and front-door
- **the LLM adapter** — the bundle ships the abstract `llm` service; the leaf registers a concrete adapter on `ctx.llm` (`llm-deepseek`, `llm-pi-ai`, `llm-replay`).
- **the bash executor** — the bundle ships `tool-bash` (the consumer schema); the leaf provides `ctx.bash` (`bash-local` or a sandboxed impl).
- **non-local skill providers** — the bundle ships the skill registry, the local filesystem provider, and the `skill` tool; deployments can add other providers such as embedded or remote catalogs as siblings.
- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside.
- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../stdio-demo/README.md), [`dsh-acp-demo`](../acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside.
This is the [interface/implementation/consumer seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door.
This is the [interface/implementation/consumer seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door.
## Config
@@ -51,7 +51,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — a stdio app pre-creates `main`, while the ACP app creates agents on demand at `session/new`; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer, while `toolTasks` controls generic `task_output` wait bounds; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — a stdio app pre-creates `main`, while the ACP app creates agents on demand at `session/new`; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service without exposing task-control tools. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
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.
@@ -63,7 +63,11 @@ A YAML include can deduplicate config but cannot own a bin or provide front-door
Indirectly, through `dsh-system-prompt`, `dsh-tool-skill`, `dsh-tool-bash`, and `dsh-tools`, which this bundle mounts without adding model-bound wrapper content.
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **The spine set is fixed in code** — `apply()` mounts every child unconditionally (including `tool-bash`); no config excludes or replaces one, so swapping the loop or dropping a spine member means composing a different bundle.
- **Most of the spine set is fixed in code** — `apply()` always mounts the core services and `tool-bash`; config can omit the bundled skills and task-control tools, but swapping the loop or dropping another spine member means composing a different bundle.
- **The invariant seam and companions remain fixed members** — `invariants.enabled: false` or package filters suppress checks but do not remove the service or companion registrations; Session's always-on validation and freezing are separate.
@@ -35,6 +35,8 @@ export const name = 'agent-spine-demo'
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
/** Mount the bundled local skill provider and model-facing skill tool (default true). */
enabled?: boolean
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
@@ -78,14 +80,15 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, including this producer's background opt-in. */
toolBash?: toolBash.Config
/** Generic background-task control-tool wait bounds. */
toolTasks?: toolTasks.Config
/** 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. */
invariants?: InvariantConfig
}
/** The skill config schema exported for app packages that forward `skills`. */
export const SkillConfigSchema: z<SkillConfig> = z.object({
enabled: z.boolean().default(true),
registry: SkillService.Config,
local: SkillLocal.Config,
tool: toolSkill.Config,
@@ -107,7 +110,7 @@ export const Config = z.intersect([
skills: SkillConfigSchema,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
toolBash: ToolBashConfigSchema,
toolTasks: ToolTasksConfigSchema,
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
invariants: InvariantService.Config,
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'invariants'>>,
]) as unknown as z<Config>
@@ -159,8 +162,11 @@ export function apply(ctx: Context, config: Config): void {
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
})
ctx.plugin(ToolRegistry, config.tools ?? {})
ctx.plugin(SkillService, config.skills?.registry ?? {})
ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome }))
const skillsEnabled = config.skills?.enabled ?? true
if (skillsEnabled) {
ctx.plugin(SkillService, config.skills?.registry ?? {})
ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome }))
}
ctx.plugin(AgentRegistry)
ctx.plugin(TaskService)
ctx.plugin(InvariantService, config.invariants ?? {})
@@ -174,8 +180,8 @@ export function apply(ctx: Context, config: Config): void {
}
// Both plugins prepend session-prefix messages. Registration order is the
// rendered order, so workspace instructions must precede the skill catalog.
ctx.plugin(toolSkill, config.skills?.tool ?? {})
ctx.plugin(toolTasks, config.toolTasks ?? {})
if (skillsEnabled) ctx.plugin(toolSkill, config.skills?.tool ?? {})
if (config.toolTasks !== false) ctx.plugin(toolTasks, config.toolTasks ?? {})
ctx.plugin(AgentLoop, {
agents: config.agents ?? [],
...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {},
@@ -371,6 +371,21 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.fiber.dispose()
})
it('can omit skills and model-facing task controls for a foreground-only deployment', async () => {
const ctx = await mount({
workspaceContext: false,
skills: { enabled: false },
toolBash: { enableRunInBackground: false },
toolTasks: false,
}, true)
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['bash'])
expect(ctx.get('skills')).toBeUndefined()
expect(ctx.get('tasks')).toBeDefined()
await ctx.fiber.dispose()
})
it('picks shared spine config without leaking front-door fields', () => {
const appConfig = {
model: 'front-door-only',
@@ -379,9 +394,9 @@ describe('dsh-agent-spine-demo bundle', () => {
tools: { mode: 'native' as const },
dshHome: '/tmp/dsh-home',
workspaceContext: false as const,
skills: {},
skills: { enabled: false },
toolBash: { enableRunInBackground: false },
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
toolTasks: false as const,
invariants: { enabled: false },
}
@@ -391,7 +406,7 @@ describe('dsh-agent-spine-demo bundle', () => {
tools: appConfig.tools,
dshHome: appConfig.dshHome,
workspaceContext: false,
skills: {},
skills: appConfig.skills,
toolBash: appConfig.toolBash,
toolTasks: appConfig.toolTasks,
invariants: appConfig.invariants,