Adapt workspace context to session prefixes
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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 ?? [] })
|
||||
}
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../prompt/project-instructions"
|
||||
"path": "../../prompt/workspace-context"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent-loop"
|
||||
|
||||
Reference in New Issue
Block a user