add project instruction file loading
This commit is contained in:
@@ -17,6 +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-agent-loop THE concrete loop (gets the forwarded `agents`)
|
||||
```
|
||||
|
||||
|
||||
@@ -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 + 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 + project-instructions + agent-loop)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -27,6 +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-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
|
||||
@@ -39,6 +40,7 @@
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-project-instructions": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* 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, 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.
|
||||
* schemas, project instruction 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.
|
||||
*
|
||||
* It is deliberately NOT the whole app: the swappable choices stay OUTSIDE the
|
||||
* bundle, picked by whatever loads it.
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import Timer from '@cordisjs/plugin-timer'
|
||||
import z from 'schemastery'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
@@ -51,29 +52,41 @@ 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 AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop'
|
||||
|
||||
export const name = 'agent-core'
|
||||
|
||||
/**
|
||||
* Bundle config: the agent-loop `agents` list, forwarded verbatim. Default `[]`
|
||||
* — an app that pre-creates no agents (the ACP bridge creates them on demand at
|
||||
* `session/new`) simply omits it; an app that needs a pre-created `main` (the
|
||||
* stdio chat) supplies one. This IS {@link AgentLoopConfig}, so the schema and
|
||||
* the forwarded shape can never drift.
|
||||
* Bundle config: the agent-loop `agents` list plus project-instruction loader
|
||||
* controls. `agents` defaults to `[]` — an app that pre-creates no agents (the
|
||||
* ACP bridge creates them on demand at `session/new`) simply omits it; an app
|
||||
* that needs a pre-created `main` (the stdio chat) supplies one.
|
||||
*/
|
||||
export type Config = AgentLoopConfig
|
||||
export interface Config {
|
||||
agents?: AgentLoopConfig['agents']
|
||||
projectInstructions?: projectInstructions.Config | false
|
||||
}
|
||||
|
||||
/** Forward the loop's own schema so validation + defaulting stay identical. */
|
||||
export const Config = AgentLoop.Config
|
||||
const AgentsConfig = z.array(z.object({
|
||||
id: z.string().required(),
|
||||
model: z.string(),
|
||||
systemPrompt: z.string(),
|
||||
resumeSessionId: z.string(),
|
||||
})).default([])
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
agents: AgentsConfig,
|
||||
projectInstructions: z.union([z.const(false), projectInstructions.Config]),
|
||||
}) 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. 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 and core registries first, then the dev tripwire and the bash tool
|
||||
* consumer, then the loop that drives them.
|
||||
* vocabulary and core registries first, then extension plugins that wrap the
|
||||
* request/tool seams, then the loop that drives them.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(Timer)
|
||||
@@ -84,5 +97,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(AgentRegistry)
|
||||
ctx.plugin(invariants)
|
||||
ctx.plugin(toolBash)
|
||||
ctx.plugin(AgentLoop, { agents: config.agents })
|
||||
if (config.projectInstructions !== false) {
|
||||
ctx.plugin(projectInstructions, config.projectInstructions ?? {})
|
||||
}
|
||||
ctx.plugin(AgentLoop, { agents: config.agents ?? [] })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import * as agentCore from '../src/index.ts'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { MockAdapter, textResponse } from '../../agent-loop/tests/mock-adapter.ts'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/**
|
||||
* Unit coverage for the @deepseek-ai/dsh-agent-core bundle: mounting it brings
|
||||
@@ -23,6 +29,22 @@ async function mount(config?: agentCore.Config): Promise<Context> {
|
||||
return ctx
|
||||
}
|
||||
|
||||
function waitForMainIdle(ctx: Context): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (agent, status) => {
|
||||
if (agent.id === 'main' && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function firstText(message: Message | undefined): string | undefined {
|
||||
const block = message?.content[0]
|
||||
return block?.type === 'text' ? block.text : undefined
|
||||
}
|
||||
|
||||
describe('dsh-agent-core bundle', () => {
|
||||
it('brings up the full providerless spine', async () => {
|
||||
const ctx = await mount()
|
||||
@@ -51,6 +73,61 @@ 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-'))
|
||||
try {
|
||||
await mkdir(join(root, '.git'), { recursive: true })
|
||||
await writeFile(join(root, 'AGENTS.md'), 'bundled project rule')
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await mount()
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const handle = ctx.agents.create({
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { model: 'mock' },
|
||||
})
|
||||
const agent = handle.agent
|
||||
|
||||
agent.send([{ type: 'text', text: 'hi' }])
|
||||
await waitForMainIdle(ctx)
|
||||
|
||||
expect(adapter.requests[0]?.messages[0]?.role).toBe('user')
|
||||
expect(firstText(adapter.requests[0]?.messages[0])).toContain('bundled project rule')
|
||||
expect(adapter.requests[0]?.system).toBeUndefined()
|
||||
await handle.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('forwards project-instructions config to the bundled loader', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-core-project-instructions-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 } })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const handle = ctx.agents.create({
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-disabled-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { model: 'mock' },
|
||||
})
|
||||
|
||||
handle.agent.send([{ type: 'text', text: 'hi' }])
|
||||
await waitForMainIdle(ctx)
|
||||
|
||||
expect(adapter.requests[0]?.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
|
||||
await handle.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('re-exports the loop config schema as its own', () => {
|
||||
expect(agentCore.Config).toBeDefined()
|
||||
expect(agentCore.name).toBe('agent-core')
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../core/project-instructions"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent-loop"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user