Merge remote-tracking branch 'origin/master' into codex/session-scoped-sandbox-roots
# Conflicts: # packages/examples/agent-spine-demo/package.json # pnpm-lock.yaml
This commit is contained in:
@@ -17,6 +17,9 @@ Read this package for the whole plugin tree and its composition order.
|
||||
@deepseek-ai/dsh-skill skill provider registry
|
||||
@deepseek-ai/dsh-skill-local local filesystem skill provider
|
||||
@deepseek-ai/dsh-agent agent registry + initiator scope + agent/* events
|
||||
@deepseek-ai/dsh-goal optional persisted same-session goal domain
|
||||
@deepseek-ai/dsh-tool-goal optional model-facing goal controls
|
||||
@deepseek-ai/dsh-goal-session optional same-session goal-round driver
|
||||
@deepseek-ai/dsh-llm-retry bounded transient request retry policy
|
||||
@deepseek-ai/dsh-tasks generic background-task registry
|
||||
@deepseek-ai/dsh-invariants dev-mode event-contract assertions
|
||||
@@ -43,11 +46,11 @@ This is the [interface/implementation/consumer seam](../../../.agents/notes/impl
|
||||
|
||||
```ts
|
||||
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
// { agents?, maxParallelToolCalls?, persona?, toolOrder?, tools?, dshHome?, skills?, workspaceContext, toolBash?, toolTasks?, llmRetry? }
|
||||
// { agents?, maxParallelToolCalls?, persona?, toolOrder?, tools?, dshHome?, skills?, workspaceContext, toolBash?, toolTasks?, goals?, llmRetry? }
|
||||
// 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 — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `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); 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 for foreground producers without exposing `task_output` / `task_list` / `task_kill`. 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.
|
||||
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 — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `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); and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. 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.
|
||||
|
||||
## Why a code bundle, not a shared YAML include
|
||||
|
||||
@@ -57,7 +60,7 @@ The bounded retry policy may repeat a transiently failed request in a new number
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-system-prompt`, `dsh-tool-skill`, `dsh-tool-bash`, `dsh-tools`, and `dsh-llm-retry`, which this bundle mounts without adding model-bound wrapper content.
|
||||
Indirectly, through `dsh-system-prompt`, `dsh-tool-skill`, `dsh-tool-bash`, `dsh-tools`, and `dsh-llm-retry`, plus `dsh-tool-goal` and goal-round prompts when `goals` is enabled. The bundle adds no model-bound wrapper content of its own.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
@@ -65,5 +68,5 @@ No direct invalidation; the named consumer owns any request-prefix changes.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **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.
|
||||
- **Most of the spine set is fixed in code** — `apply()` always mounts the core services and `tool-bash`; config can omit bundled goals, skills, and task-control tools, but swapping the loop or dropping another spine member means composing a different bundle.
|
||||
- **`dsh-invariants` mounts unconditionally** — this bundle has no toggle, so every composition using it pays the dev-mode relational assertions; Session's always-on validation and freezing are separate.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-agent-spine-demo",
|
||||
"description": "The default executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + skills + agents + bounded retry + tasks + invariants + tool-bash + workspace-context + tool-skill + tool-tasks + agent-loop)",
|
||||
"description": "The default executor-less/UI-less agent spine with bounded retry and optional persisted goals",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -25,6 +25,8 @@
|
||||
"@cordisjs/plugin-timer": "^1.1.2",
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-home": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
@@ -36,6 +38,7 @@
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-skill": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-tasks": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
@@ -49,6 +52,8 @@
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-home": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
@@ -63,6 +68,7 @@
|
||||
"@deepseek-ai/dsh-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Default executor-less, UI-less agent spine. It bundles the common services,
|
||||
* background-task registry and controls, concrete loop, local skill and
|
||||
* background-task registry and controls, optional persisted goals, concrete loop, local skill and
|
||||
* workspace-context providers, and model-facing bash/skill consumers;
|
||||
* deployments still choose the LLM adapter, bash executor, and presentation.
|
||||
* The plugin intentionally exposes named exports only because Loader default
|
||||
@@ -18,6 +18,9 @@ import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools
|
||||
import SkillService, { type Config as SkillRegistryConfig } from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
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 TaskService from '@deepseek-ai/dsh-tasks'
|
||||
import * as invariants from '@deepseek-ai/dsh-invariants'
|
||||
import * as toolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
@@ -42,6 +45,14 @@ export interface SkillConfig {
|
||||
tool?: toolSkill.Config
|
||||
}
|
||||
|
||||
/** Persisted goal domain, model-tool policy, and same-session driver config. */
|
||||
export interface GoalConfig {
|
||||
/** Goal-domain creation defaults. */
|
||||
domain?: GoalDomainConfig
|
||||
/** Model-facing goal-tool authority policy. */
|
||||
tool?: toolGoal.Config
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle config: each field forwarded verbatim to the child that owns it —
|
||||
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
|
||||
@@ -50,8 +61,10 @@ export interface SkillConfig {
|
||||
* order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* `dshHome` to bash environment and local skill discovery, `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. Owner schemas supply defaults for optional input;
|
||||
* workspace-context loader, `llmRetry` to the bounded request-recovery policy,
|
||||
* and `toolBash`/`toolTasks` to the model-facing tool plugins this bundle owns.
|
||||
* `goals` opts into and configures the persisted goal
|
||||
* domain plus its model tool and same-session driver. Owner schemas supply defaults for optional input;
|
||||
* workspace context instead requires an explicit byte budget or `false` because
|
||||
* it changes model-visible input. Producer opt-in stays producer-local:
|
||||
* `toolBash` configures bash only; independently composed producers keep their
|
||||
@@ -78,6 +91,8 @@ export interface Config {
|
||||
toolBash?: toolBash.Config
|
||||
/** Generic background-task controls; set false to keep the task service without model-facing task tools. */
|
||||
toolTasks?: toolTasks.Config | false
|
||||
/** Opt-in persisted same-session goal stack; set false or omit to leave it unmounted. */
|
||||
goals?: GoalConfig | false
|
||||
/** Bounded transient model-request retry policy. */
|
||||
llmRetry?: llmRetry.Config
|
||||
}
|
||||
@@ -96,6 +111,12 @@ export const ToolBashConfigSchema: z<toolBash.Config> = toolBash.Config
|
||||
/** The task-control-tool config schema exported for app packages that forward `toolTasks`. */
|
||||
export const ToolTasksConfigSchema: z<toolTasks.Config> = toolTasks.Config
|
||||
|
||||
/** The persisted-goal config schema exported for app packages that opt in. */
|
||||
export const GoalConfigSchema: z<GoalConfig> = z.object({
|
||||
domain: GoalService.Config,
|
||||
tool: toolGoal.Config,
|
||||
})
|
||||
|
||||
/** The bounded LLM retry schema exported for app packages that forward `llmRetry`. */
|
||||
export const LlmRetryConfigSchema: z<llmRetry.Config> = llmRetry.Config
|
||||
|
||||
@@ -110,8 +131,9 @@ export const Config = z.intersect([
|
||||
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
|
||||
toolBash: ToolBashConfigSchema,
|
||||
toolTasks: z.union([z.const(false), ToolTasksConfigSchema]),
|
||||
goals: z.union([z.const(false), GoalConfigSchema]),
|
||||
llmRetry: LlmRetryConfigSchema,
|
||||
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'llmRetry'>>,
|
||||
}) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks' | 'goals' | 'llmRetry'>>,
|
||||
]) as unknown as z<Config>
|
||||
|
||||
/**
|
||||
@@ -130,6 +152,7 @@ export function pickSpineConfig(config: Omit<Config, 'agents'>): Omit<Config, 'a
|
||||
...config.skills !== undefined ? { skills: config.skills } : {},
|
||||
...config.toolBash !== undefined ? { toolBash: config.toolBash } : {},
|
||||
...config.toolTasks !== undefined ? { toolTasks: config.toolTasks } : {},
|
||||
...config.goals !== undefined ? { goals: config.goals } : {},
|
||||
...config.llmRetry !== undefined ? { llmRetry: config.llmRetry } : {},
|
||||
}
|
||||
}
|
||||
@@ -168,6 +191,11 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
ctx.plugin(AgentRegistry)
|
||||
ctx.plugin(llmRetry, config.llmRetry ?? {})
|
||||
if (config.goals !== undefined && config.goals !== false) {
|
||||
ctx.plugin(GoalService, config.goals.domain ?? {})
|
||||
ctx.plugin(toolGoal, config.goals.tool ?? {})
|
||||
ctx.plugin(goalSession)
|
||||
}
|
||||
ctx.plugin(TaskService)
|
||||
ctx.plugin(invariants)
|
||||
ctx.plugin(toolBash, Object.assign({}, config.toolBash, { dshHome }))
|
||||
|
||||
@@ -124,6 +124,35 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('tasks')).toBeDefined()
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
expect(ctx.get('goals')).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('opts into the configured persisted-goal domain, tools, and same-session driver', async () => {
|
||||
const ctx = await mount({
|
||||
workspaceContext: false,
|
||||
agents: [{ id: SessionId('configured-goal'), provider: 'mock', model: 'mock' }],
|
||||
goals: {
|
||||
domain: { defaultMaxGoalRounds: 17 },
|
||||
tool: { blockedAfterConsecutiveRounds: 5 },
|
||||
},
|
||||
})
|
||||
const agent = ctx.agents.list()[0]
|
||||
if (agent === undefined) throw new Error('configured goal test has no live agent')
|
||||
expect(ctx.goals.create(agent, { objective: 'configured' })).toMatchObject({
|
||||
objective: 'configured', maxGoalRounds: 17,
|
||||
})
|
||||
expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name))
|
||||
.toEqual(['create_goal', 'get_goal', 'update_goal'])
|
||||
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'tool:goal')?.text)
|
||||
.toContain('at least 5 consecutive goal rounds')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('accepts an explicit false goal composition without mounting it', async () => {
|
||||
const ctx = await mount({ workspaceContext: false, goals: false })
|
||||
expect(ctx.get('goals')).toBeUndefined()
|
||||
expect(ctx.tools.get('get_goal')).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -211,6 +240,23 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('uses owner defaults for a schema-bypassing empty goal opt-in', async () => {
|
||||
const ctx = new Context()
|
||||
agentCore.apply(ctx, {
|
||||
workspaceContext: false,
|
||||
agents: [{ id: SessionId('defaulted-goal'), provider: 'mock', model: 'mock' }],
|
||||
goals: {},
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
const agent = ctx.agents.list()[0]
|
||||
if (agent === undefined) throw new Error('default goal test has no live agent')
|
||||
expect(ctx.goals.create(agent, { objective: 'defaulted' })).toMatchObject({
|
||||
objective: 'defaulted', maxGoalRounds: 256,
|
||||
})
|
||||
expect(ctx.tools.get('get_goal')).toBeDefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('loads workspace instructions into requests through the bundled spine', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-'))
|
||||
try {
|
||||
|
||||
@@ -41,6 +41,15 @@
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../goal/goal"
|
||||
},
|
||||
{
|
||||
"path": "../../goal/tool-goal"
|
||||
},
|
||||
{
|
||||
"path": "../../goal/goal-session"
|
||||
},
|
||||
{
|
||||
"path": "../../context/workspace-context"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user