Adapt workspace context to session prefixes

This commit is contained in:
Yichen Jiang
2026-07-10 14:32:44 +08:00
parent 7b54768ee7
commit 3fe196a751
61 changed files with 2313 additions and 1155 deletions

View File

@@ -2,7 +2,7 @@
The **providerless, executor-less, UI-less agent spine** as ONE Cordis bundle plugin. It loads the fixed set of services every harness agent needs and forwards the loop's `agents` list as its own config — so an app package composes a working agent by adding only a front door and the swappable backends.
This is the package to read to see **the whole plugin tree at once** — the teaching role the inlined `echo-agent` `cordis.yml` used to play before the spine moved behind this bundle.
This is the package to read to see **the whole plugin tree at once** and the canonical teaching map for the shared spine.
## The tree it loads
@@ -17,7 +17,7 @@ This is the package to read to see **the whole plugin tree at once** — the tea
@deepseek-ai/dsh-agent agent registry + agent/* event vocabulary
@deepseek-ai/dsh-invariants dev-mode event-contract assertions
@deepseek-ai/dsh-tool-bash the model-facing bash/bash_output/bash_kill schemas
@deepseek-ai/dsh-project-instructions AGENTS.md/CLAUDE.md workspace context loader
@deepseek-ai/dsh-workspace-context AGENTS.md/CLAUDE.md workspace context loader
@deepseek-ai/dsh-agent-loop THE concrete loop (gets the forwarded `agents`)
(dsh-system-prompt gets the forwarded `persona`)
```
@@ -36,12 +36,12 @@ This is the [interface/implementation/consumer seam](../../../docs/rfc/implement
```ts
import type { Config } from '@deepseek-ai/dsh-agent-core'
// { agents?, persona?, toolOrder? } — the schema is z.intersect([AgentLoop.Config, SystemPrompt.Config]),
// { agents?, persona?, toolOrder?, workspaceContext? } — the schema intersects the child owners,
// so validation and defaulting can never drift from the owners'.
```
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), the deployment's persona section — and `toolOrder` to `dsh-system-prompt` (absent — lexicographic), the explicit model-facing tool order. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` to `dsh-system-prompt` (default `''`), the deployment's persona section — `toolOrder` to `dsh-system-prompt` (absent — lexicographic), the explicit model-facing tool order — and `workspaceContext` to `dsh-workspace-context` (`false` disables automatic instruction-file loading). Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
## Why a code bundle, not a shared YAML include
A YAML include can dedupe the config, but it cannot OWN a `bin`, and it can only *describe* the front-door coupling in a comment and trust each leaf to obey. Moving the spine into a package, and the front-door cluster into the app packages, means the default leaf for an ACP server has no logger entry to copy wrong — "the ACP app never logs to stdout" stops being a prose warning a leaf must remember and becomes the app package's default shape (a leaf can still add a sibling logger, so the rule stays documented — but it has nothing to get wrong by default). Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor) exactly as a nested `plugin-include` subtree's services were — cordis gates every read on `inject`, never on load order.
A YAML include can dedupe config, but it cannot own a `bin` or enforce front-door coupling. The app packages own that cluster, so the default ACP shape contains no stdout logger entry for a leaf to reproduce; a deployment can still add a sibling logger explicitly. Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor); Cordis gates every read on `inject`, never on load order.

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-agent-core",
"description": "The providerless/executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + agents + invariants + tool-bash + project-instructions + agent-loop)",
"description": "The providerless/executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + agents + invariants + tool-bash + workspace-context + agent-loop)",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -27,7 +27,7 @@
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-project-instructions": "^0.0.1",
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
@@ -41,7 +41,7 @@
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-project-instructions": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",

View File

@@ -4,7 +4,7 @@
* Loads the fixed set of services every harness agent needs — `timer`, the LLM
* service, the session store, system-prompt assembly, the tool registry, the
* agent registry, the dev-mode invariants, the model-facing `bash` tool
* schemas, project instruction loading, and the concrete `agent-loop` — and
* schemas, workspace-context loading, and the concrete `agent-loop` — and
* forwards the loop's `agents` list as its OWN config (default `[]`), so each
* app supplies its own pre-created agents.
*
@@ -28,10 +28,9 @@
*
* Services register in the root store keyed by their isolate symbol, so a child
* loaded here via `ctx.plugin(...)` is visible to the bundle's SIBLINGS (the
* leaf's adapter and executor) exactly as a nested `plugin-include` subtree's
* services were before this bundle existed — cordis gates every read on
* `inject`, never on load order, so the fixed child set resolves regardless of
* which entry loads first.
* leaf's adapter and executor). Cordis gates every read on `inject`, never on
* load order, so the fixed child set resolves regardless of which entry loads
* first.
*
* Plugin export shape: named `name`/`Config`/`apply`, NO default export — the
* cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray
@@ -52,7 +51,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import * as invariants from '@deepseek-ai/dsh-invariants'
import * as toolBash from '@deepseek-ai/dsh-tool-bash'
import * as projectInstructions from '@deepseek-ai/dsh-project-instructions'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop'
export const name = 'agent-core'
@@ -62,7 +61,7 @@ export const name = 'agent-core'
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
* plugin (the deployment's persona section and the explicit model-facing tool
* order), and `projectInstructions` to the project-instructions plugin. Every
* order), and `workspaceContext` to the workspace-context plugin. Every
* field is optional INPUT here because each owner's schema supplies the
* default (`[]` / `''` / absent — lexicographic / loader defaults); the schema
* is the INTERSECTION of the owners' own schemas, so validation and defaulting
@@ -75,25 +74,23 @@ export interface Config {
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** Project-instruction loader controls; set `false` for hermetic prompts. */
projectInstructions?: projectInstructions.Config | false
/** Workspace-context loader controls; set `false` for hermetic prompts. */
workspaceContext?: workspaceContext.Config | false
}
const ProjectInstructionsConfig = z.object({
projectInstructions: z.union([z.const(false), projectInstructions.Config]),
}) as unknown as z<Pick<Config, 'projectInstructions'>>
/** Intersect the owners' schemas so validation + defaulting stay identical. */
export const Config = z.intersect([
AgentLoop.Config,
SystemPrompt.Config,
ProjectInstructionsConfig,
z.object({
workspaceContext: z.union([z.const(false), workspaceContext.Config]),
}) as unknown as z<Pick<Config, 'workspaceContext'>>,
]) as unknown as z<Config>
/**
* Load the spine. Each `ctx.plugin(...)` mounts one child of the bundle fiber;
* `agent-loop` receives the forwarded `agents` list and `system-prompt` the
* forwarded `persona` and `toolOrder`. Project-instructions receives its own
* forwarded `persona` and `toolOrder`. Workspace-context receives its own
* forwarded config or loads with defaults. Load order is irrelevant (cordis
* pends each fiber on its `inject` until the services it needs exist), but the
* listing mirrors the dependency layering for readability: the LLM vocabulary
@@ -118,8 +115,8 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(AgentRegistry)
ctx.plugin(invariants)
ctx.plugin(toolBash)
if (config.projectInstructions !== false) {
ctx.plugin(projectInstructions, config.projectInstructions ?? {})
if (config.workspaceContext !== false) {
ctx.plugin(workspaceContext, config.workspaceContext ?? {})
}
ctx.plugin(AgentLoop, { agents: config.agents ?? [] })
}

View File

@@ -90,8 +90,8 @@ describe('dsh-agent-core bundle', () => {
await ctx.fiber.dispose()
})
it('loads project instructions into requests through the bundled spine', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-core-project-instructions-'))
it('loads workspace instructions into requests through the bundled spine', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-core-workspace-context-'))
try {
await mkdir(join(root, '.git'), { recursive: true })
await writeFile(join(root, 'AGENTS.md'), 'bundled project rule')
@@ -122,13 +122,13 @@ describe('dsh-agent-core bundle', () => {
}
})
it('forwards project-instructions config to the bundled loader', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-core-project-instructions-disabled-'))
it('forwards workspace-context config to the bundled loader', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-core-workspace-context-disabled-'))
try {
await mkdir(join(root, '.git'), { recursive: true })
await writeFile(join(root, 'AGENTS.md'), 'must not be injected')
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await mount({ projectInstructions: { baselineMaxBytes: 0 } })
const ctx = await mount({ workspaceContext: { maxBytes: 0 } })
ctx.llm.registerAdapter(['mock'], adapter)
const handle = ctx.agents.create({
agentId: AgentId('main'),
@@ -165,9 +165,9 @@ describe('dsh-agent-core bundle', () => {
await ctx.fiber.dispose()
})
it('supports direct apply with project instructions disabled and no forwarded agents', async () => {
it('supports direct apply with workspace instructions disabled and no forwarded agents', async () => {
const ctx = new Context()
agentCore.apply(ctx, { projectInstructions: false })
agentCore.apply(ctx, { workspaceContext: false })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('agents')?.list()).toEqual([])

View File

@@ -33,7 +33,7 @@
"path": "../../core/agent"
},
{
"path": "../../prompt/project-instructions"
"path": "../../prompt/workspace-context"
},
{
"path": "../../core/agent-loop"