Merge remote-tracking branch 'origin/master' into codex/skill-system
# Conflicts: # docs/event-producer-consumer.md # docs/module-graph.md # docs/rfc/README.md # packages/core/agent-core/package.json # packages/core/agent-core/src/index.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/index.ts # packages/ui/acp-agent/src/index.ts # packages/ui/acp-agent/tests/acp-agent.spec.ts # packages/ui/stdio-agent/README.md # packages/ui/stdio-agent/src/index.ts # packages/ui/stdio-agent/tests/stdio-agent.spec.ts
This commit is contained in:
@@ -23,7 +23,7 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `model` | (required) | the per-session agent template the bridge creates agents from |
|
||||
| `systemPrompt` | (required) | the per-session agent's system prompt |
|
||||
| `persona` | — | the deployment persona template (may reference `{{model}}`/`{{cwd}}`), routed to `dsh-system-prompt` |
|
||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||
|
||||
The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay) and a bash executor (`bash-local`).
|
||||
|
||||
@@ -39,16 +39,17 @@ import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
export const name = 'acp-agent'
|
||||
|
||||
/**
|
||||
* App config: the swappable per-deployment values. `model`/`systemPrompt`
|
||||
* configure the agent template the ACP bridge creates each session's agent from
|
||||
* (NOT a pre-created agent — ACP creates agents at `session/new`);
|
||||
* App config: the swappable per-deployment values. `model` configures the
|
||||
* agent template the ACP bridge creates each session's agent from (NOT a
|
||||
* pre-created agent — ACP creates agents at `session/new`); `persona` is the
|
||||
* deployment persona (forwarded to the system-prompt plugin);
|
||||
* `persistenceRoot` is the JSONL backend's directory.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Model name for ACP-created agents (must have a registered adapter). */
|
||||
model: string
|
||||
/** Per-agent system prompt for ACP-created agents. */
|
||||
systemPrompt: string
|
||||
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
||||
persona?: string
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** Skill discovery config forwarded to the shared agent-core spine. */
|
||||
@@ -57,20 +58,23 @@ export interface Config {
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().required(),
|
||||
systemPrompt: z.string().required(),
|
||||
persona: z.string(),
|
||||
persistenceRoot: z.string().default('./.sessions'),
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Compose the spine with the ACP front door. The agent-core bundle pre-creates
|
||||
* NO agents (its `agents` list defaults to `[]`); the JSONL backend persists
|
||||
* under `persistenceRoot`; the ACP bridge owns stdout for JSON-RPC and creates
|
||||
* one agent per `session/new` from `model`/`systemPrompt`. No logger, no `hmr` —
|
||||
* stdout stays pure.
|
||||
* NO agents (its `agents` list defaults to `[]`) and carries the deployment
|
||||
* `persona`; the JSONL backend persists under `persistenceRoot`; the ACP
|
||||
* bridge owns stdout for JSON-RPC and creates one agent per `session/new`
|
||||
* from `model`. No logger, no `hmr` — stdout stays pure.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(agentCore, { agents: [], ...config.skills === undefined ? {} : { skills: config.skills } })
|
||||
ctx.plugin(agentCore, {
|
||||
...config.persona !== undefined ? { persona: config.persona } : {},
|
||||
...config.skills !== undefined ? { skills: config.skills } : {},
|
||||
})
|
||||
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
|
||||
ctx.plugin(acp, { model: config.model, systemPrompt: config.systemPrompt })
|
||||
ctx.plugin(acp, { model: config.model })
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
|
||||
|
||||
describe('dsh-acp-agent composition', () => {
|
||||
it('brings up the spine + persistence + the ACP bridge', async () => {
|
||||
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test', skills: await isolatedSkillsConfig() })
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test', skills: await isolatedSkillsConfig() })
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('sessions')).toBeDefined()
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
@@ -70,7 +70,8 @@ describe('dsh-acp-agent composition', () => {
|
||||
// `ctx.plugin`, which validates+defaults the config first) with no
|
||||
// persistenceRoot, so the runtime fallback is the one that fires.
|
||||
const ctx = new Context()
|
||||
acpAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
|
||||
// No persona: covers the omitted-persona forwarding branch too.
|
||||
acpAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
await ctx.fiber.dispose()
|
||||
@@ -79,7 +80,7 @@ describe('dsh-acp-agent composition', () => {
|
||||
it('uses default skill config when apply is called directly without skills', async () => {
|
||||
await withIsolatedSkillHomes(async () => {
|
||||
const ctx = new Context()
|
||||
acpAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi' })
|
||||
acpAgent.apply(ctx, { model: 'mock' })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(expect.arrayContaining([
|
||||
@@ -91,7 +92,7 @@ describe('dsh-acp-agent composition', () => {
|
||||
})
|
||||
|
||||
it('forwards skill config into agent-core', async () => {
|
||||
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig() })
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -99,7 +99,7 @@ async function makeConsumer(): Promise<string> {
|
||||
' name: \'@deepseek-ai/dsh-acp-agent\'',
|
||||
' config:',
|
||||
' model: deepseek-v4-flash',
|
||||
' systemPrompt: \'test agent\'',
|
||||
' persona: \'test agent\'',
|
||||
'',
|
||||
].join('\n'))
|
||||
return dir
|
||||
|
||||
@@ -54,7 +54,7 @@ const CORDIS_YML = `
|
||||
name: '@deepseek-ai/dsh-acp-agent'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
systemPrompt: 'You are a test agent.'
|
||||
persona: 'You are a test agent.'
|
||||
`
|
||||
|
||||
interface Spawned {
|
||||
|
||||
@@ -15,7 +15,8 @@ It is a **client-driver / UI plugin**, the structured analogue of the readline `
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `model` | — | Model name for created agents (must have a registered adapter). |
|
||||
| `systemPrompt` | — | Per-agent system prompt. |
|
||||
|
||||
(No persona key: the deployment persona is `dsh-system-prompt`'s own `persona` config — a context-wide section, so ACP-created agents render it without the bridge carrying prompt text.)
|
||||
|
||||
The `initialize` handshake reports a fixed server identity (`agentInfo: { name: 'deepseek-harness-acp', version: '0.0.1' }`) — branding is a literal at the `initialize` site, not config.
|
||||
|
||||
|
||||
27
packages/ui/acp/snapshot-replay.md
Normal file
27
packages/ui/acp/snapshot-replay.md
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
|
||||
Run `pnpm run gen-doc-graphs` to regenerate. -->
|
||||
|
||||
# ACP Snapshot Replay
|
||||
|
||||
This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, ACP stdout is normalized and diffed, and scenario workspaces preserve tool side effects that the UI stream alone cannot prove.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Recorder as Real API recording
|
||||
participant Fixture as snapshot fixture
|
||||
participant Workspace
|
||||
participant Replay as llm-replay adapter
|
||||
participant ACP as acp-agent subprocess
|
||||
participant Golden as stdout golden
|
||||
Recorder->>Fixture: session.jsonl + workspace inputs
|
||||
Fixture->>Workspace: seed files and hook configs
|
||||
Fixture->>Replay: recorded StreamChunk script
|
||||
Replay->>ACP: deterministic <code>llm/stream</code> chunks
|
||||
ACP->>Workspace: bash, fs, and hook side effects
|
||||
ACP->>Golden: normalized sessionUpdate stream
|
||||
Golden-->>ACP: diff must be empty
|
||||
```
|
||||
|
||||
The fs and hook snapshot matrix is valuable because it proves world state, hook decisions, and failed tool-card rendering, not just that replay returns text.
|
||||
|
||||
Maintenance mode: curated Mermaid sequence based on the snapshot test harness.
|
||||
@@ -115,8 +115,6 @@ function sameWorkspaceCwd(left: string, right: string): boolean {
|
||||
export interface AcpConfig {
|
||||
/** Model name for created agents (must have a registered adapter). */
|
||||
model?: string
|
||||
/** Per-agent system prompt. */
|
||||
systemPrompt?: string
|
||||
/**
|
||||
* Transport stream override. Production omits this (the plugin wires
|
||||
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
|
||||
@@ -129,7 +127,6 @@ export interface AcpConfig {
|
||||
|
||||
export const Config: Schema<AcpConfig> = Schema.object({
|
||||
model: Schema.string(),
|
||||
systemPrompt: Schema.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -705,10 +702,9 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
* (exactOptionalPropertyTypes: never assign `undefined` to an optional key).
|
||||
* Exported for unit coverage of both the present and absent branches.
|
||||
*/
|
||||
export function agentOptions(config: AcpConfig): { model?: string; systemPrompt?: string } {
|
||||
export function agentOptions(config: AcpConfig): { model?: string } {
|
||||
return {
|
||||
...config.model !== undefined ? { model: config.model } : {},
|
||||
...config.systemPrompt !== undefined ? { systemPrompt: config.systemPrompt } : {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,15 +148,15 @@ describe('acp bridge', () => {
|
||||
await expect(harness.client.authenticate({ methodId: 'whatever' })).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
it('honors systemPrompt config', async () => {
|
||||
it('renders the deployment persona into ACP-created agents\' requests', async () => {
|
||||
harness = await makeBridgeHarness({
|
||||
storageDir,
|
||||
script: [textResponse('ok')],
|
||||
config: { systemPrompt: 'be terse' },
|
||||
persona: 'be terse',
|
||||
})
|
||||
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
// Create + prompt so the systemPrompt config flows through agentOptions and
|
||||
// reaches the model request.
|
||||
// Create + prompt so the system-prompt plugin's persona section reaches
|
||||
// the model request of an agent the BRIDGE created (session/new).
|
||||
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
||||
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'hi' }] })
|
||||
expect(harness.adapter.requests[0]?.system).toContain('be terse')
|
||||
|
||||
@@ -153,6 +153,8 @@ export interface BridgeHarness {
|
||||
export async function makeBridgeHarness(options: {
|
||||
script?: (StreamChunk[] | 'hang')[]
|
||||
config?: Partial<AcpConfig>
|
||||
/** Deployment persona for the tree (the system-prompt plugin's config). */
|
||||
persona?: string
|
||||
storageDir: string
|
||||
/**
|
||||
* Plug the REAL `dsh-bash-local` executor + `dsh-tool-bash` tools (instead of
|
||||
@@ -183,7 +185,7 @@ export async function makeBridgeHarness(options: {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(SystemPrompt, { persona: options.persona ?? '' })
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
|
||||
@@ -818,7 +818,5 @@ describe('agentOptions', () => {
|
||||
it('includes only the fields present in config', () => {
|
||||
expect(agentOptions({})).toEqual({})
|
||||
expect(agentOptions({ model: 'm' })).toEqual({ model: 'm' })
|
||||
expect(agentOptions({ systemPrompt: 'sp' })).toEqual({ systemPrompt: 'sp' })
|
||||
expect(agentOptions({ model: 'm', systemPrompt: 'sp' })).toEqual({ model: 'm', systemPrompt: 'sp' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
|
||||
| Plugin | Why it is here |
|
||||
|---|---|
|
||||
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
|
||||
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` with `process.cwd()` as the fresh session cwd |
|
||||
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model` with `process.cwd()` as the fresh session cwd and carrying its `persona` |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||
| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
|
||||
|
||||
@@ -24,7 +24,7 @@ The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapte
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `model` | (required) | the pre-created `main` agent's model |
|
||||
| `systemPrompt` | (required) | the `main` agent's system prompt |
|
||||
| `persona` | — | the deployment persona template (may reference `{{model}}`), routed to `dsh-system-prompt` |
|
||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||
| `welcome` | `ready.` | the stdin-chat banner |
|
||||
| `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) |
|
||||
@@ -56,7 +56,7 @@ Fresh stdio sessions use the process launch directory as `session.header.cwd`, s
|
||||
name: '@deepseek-ai/dsh-stdio-agent'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
systemPrompt: 'You are a CLI coding assistant. Your only tools are bash…'
|
||||
persona: 'You are a coding assistant powered by the {{model}} model.'
|
||||
```
|
||||
|
||||
Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app".
|
||||
|
||||
@@ -51,17 +51,18 @@ export const name = 'stdio-agent'
|
||||
|
||||
/**
|
||||
* App config: the swappable per-demo values, each routed to where the app wires
|
||||
* it. `model`/`systemPrompt`/`resumeSessionId` configure the pre-created `main`
|
||||
* agent (through {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list).
|
||||
* Fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
|
||||
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
|
||||
* {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list); `persona` is
|
||||
* the deployment persona (forwarded to the system-prompt plugin);
|
||||
* fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
|
||||
* keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory;
|
||||
* `welcome` is the UI banner.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Model name for the `main` agent (must have a registered adapter). */
|
||||
model: string
|
||||
/** System prompt for the `main` agent. */
|
||||
systemPrompt: string
|
||||
/** Deployment persona (the system-prompt plugin's `persona` config). */
|
||||
persona?: string
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
|
||||
@@ -78,7 +79,7 @@ export interface Config {
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().required(),
|
||||
systemPrompt: z.string().required(),
|
||||
persona: z.string(),
|
||||
persistenceRoot: z.string().default('./.sessions'),
|
||||
welcome: z.string().default('ready.'),
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
@@ -88,17 +89,17 @@ export const Config: z<Config> = z.object({
|
||||
/**
|
||||
* Compose the spine with the stdio front door. The console logger comes first
|
||||
* (infra), then the agent-core bundle pre-creating the `main` agent from this
|
||||
* app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then
|
||||
* the readline UI bound to `main`. The `hmr` dev-reload plugin is a leaf
|
||||
* concern (see the module doc), so it is not mounted here.
|
||||
* app's `model`/`resumeSessionId` with the deployment `persona`, then the JSONL
|
||||
* backend, then the readline UI bound to `main`. The `hmr` dev-reload plugin is
|
||||
* a leaf concern (see the module doc), so it is not mounted here.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(ConsoleExporter)
|
||||
ctx.plugin(agentCore, {
|
||||
...config.persona !== undefined ? { persona: config.persona } : {},
|
||||
agents: [{
|
||||
id: AgentId('main'),
|
||||
model: config.model,
|
||||
systemPrompt: config.systemPrompt,
|
||||
cwd: process.cwd(),
|
||||
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
||||
}],
|
||||
|
||||
@@ -91,7 +91,7 @@ async function makeConsumer(welcome: string, disabledBrokenEntry = false): Promi
|
||||
' name: \'@deepseek-ai/dsh-stdio-agent\'',
|
||||
' config:',
|
||||
' model: mock-echo',
|
||||
' systemPrompt: \'demo\'',
|
||||
' persona: \'demo\'',
|
||||
` welcome: '${welcome}'`,
|
||||
...disabledBrokenEntry
|
||||
? ['- id: off', ' name: \'./src/does-not-exist.ts\'', ' disabled: true']
|
||||
|
||||
@@ -11,8 +11,9 @@ import * as stdioAgent from '../src/index.ts'
|
||||
* Unit coverage for the @deepseek-ai/dsh-stdio-agent app plugin: mounting it
|
||||
* composes the console logger, the agent-core spine (pre-creating the `main`
|
||||
* agent from the app config), the JSONL backend, and the readline UI in one
|
||||
* `ctx.plugin`. The forwarded `model`/`systemPrompt` reach the pre-created
|
||||
* agent; `persistenceRoot`/`welcome`/`resumeSessionId` route to their backends.
|
||||
* `ctx.plugin`. The forwarded `model` reaches the pre-created agent and
|
||||
* `persona` the system-prompt plugin; `persistenceRoot`/`welcome`/
|
||||
* `resumeSessionId` route to their backends.
|
||||
*
|
||||
* `hmr` is NOT part of this plugin (it is a leaf entry — a Loader-only dev
|
||||
* plugin the in-process tier cannot import); the keyless echo smoke in
|
||||
@@ -60,7 +61,7 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
|
||||
|
||||
describe('dsh-stdio-agent app', () => {
|
||||
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
|
||||
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec', skills: await isolatedSkillsConfig() })
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec', skills: await isolatedSkillsConfig() })
|
||||
// The spine services (brought up by the agent-core bundle) are all present.
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
@@ -78,7 +79,8 @@ describe('dsh-stdio-agent app', () => {
|
||||
// apply()'s last two lines are the ones that fire — covering a
|
||||
// schema-bypassing direct-mount caller.
|
||||
const ctx = new Context()
|
||||
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
|
||||
// No persona: covers the omitted-persona forwarding branch too.
|
||||
stdioAgent.apply(ctx, { model: 'mock', skills: await isolatedSkillsConfig() })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
@@ -88,7 +90,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
it('uses default skill config when apply is called directly without skills', async () => {
|
||||
await withIsolatedSkillHomes(async () => {
|
||||
const ctx = new Context()
|
||||
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi' })
|
||||
stdioAgent.apply(ctx, { model: 'mock' })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(expect.arrayContaining([
|
||||
@@ -105,7 +107,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
// the branch that maps resumeSessionId through is what this covers.
|
||||
const ctx = await mount({
|
||||
model: 'mock',
|
||||
systemPrompt: 'hi',
|
||||
persona: 'hi',
|
||||
persistenceRoot: '/tmp/dsh-stdio-agent-spec-resume',
|
||||
resumeSessionId: 'no-such-session',
|
||||
skills: await isolatedSkillsConfig(),
|
||||
@@ -115,7 +117,7 @@ describe('dsh-stdio-agent app', () => {
|
||||
})
|
||||
|
||||
it('forwards skill config into agent-core', async () => {
|
||||
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
|
||||
const ctx = await mount({ model: 'mock', persona: 'hi', skills: await isolatedSkillsConfig() })
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user