Merge current master into PR #106

This commit is contained in:
Tianyi Cui
2026-07-15 22:27:02 +08:00
159 changed files with 6096 additions and 3127 deletions
+2
View File
@@ -30,6 +30,8 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron
| `toolOrder` | — | explicit model-facing tool order (a name list with one `'<unlisted-tools>'` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` |
| `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-spine-demo` |
| `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-spine-demo` |
| `toolBash` | owner defaults | model-facing bash config routed through `dsh-agent-spine-demo`, including bash's producer-local `enableRunInBackground` |
| `toolTasks` | owner defaults | generic `task_output` wait bounds routed through `dsh-agent-spine-demo` |
| `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.
+7 -7
View File
@@ -44,6 +44,10 @@ export interface Config {
workspaceContext: agentCore.Config['workspaceContext']
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task control-tool config forwarded through agent-core. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
}
// Each front door owns a complete, directly readable config schema; extracting
@@ -62,6 +66,8 @@ export const Config: z<Config> = z.object({
persistenceRoot: z.string().default('./.sessions'),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: agentCore.ToolTasksConfigSchema,
})
/* jscpd:ignore-end */
@@ -73,13 +79,7 @@ export const Config: z<Config> = z.object({
* from `model`. No logger, no `hmr` — stdout stays pure.
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(agentCore, {
...config.persona !== undefined ? { persona: config.persona } : {},
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
...config.tools !== undefined ? { tools: config.tools } : {},
workspaceContext: config.workspaceContext,
...config.skills !== undefined ? { skills: config.skills } : {},
})
ctx.plugin(agentCore, agentCore.pickSpineConfig(config))
ctx.plugin(UserInteractionService)
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
ctx.plugin(acp, { model: config.model })
@@ -19,8 +19,9 @@ import * as acpAgent from '../src/index.ts'
* ACP operations end-to-end) is the keyless bin smoke in `load-path.e2e.ts`;
* this spec asserts the composition and the persistenceRoot default branch.
*/
async function mount(config: acpAgent.Config): Promise<Context> {
async function mount(config: acpAgent.Config, withBash = false): Promise<Context> {
const ctx = new Context()
if (withBash) ctx.provide('bash', { sandboxMode: undefined })
await ctx.plugin(acpAgent, config)
// The bundle mounts its children inside apply() (not awaited there); let their
// fibers settle so the spine services are ready.
@@ -124,6 +125,20 @@ describe('dsh-acp-demo composition', () => {
await ctx.fiber.dispose()
})
it('forwards bundled tool config into agent-core', async () => {
const ctx = await mount({
model: 'mock',
workspaceContext: false,
toolBash: { enableRunInBackground: false },
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
skills: await isolatedSkillsConfig(),
}, true)
const bash = ctx.tools.schemas().find(tool => tool.name === 'bash')
expect(Object.keys((bash!.parameters as { properties: Record<string, unknown> }).properties))
.not.toContain('run_in_background')
await ctx.fiber.dispose()
})
it('exposes its plugin shape', () => {
expect(acpAgent.name).toBe('acp-demo')
expect(acpAgent.Config).toBeDefined()
@@ -147,7 +162,7 @@ describe('dsh-acp-demo composition', () => {
})
}
const assembly = await ctx.get('systemPrompt')!.assemble()
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill'])
expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill', 'task_kill', 'task_list', 'task_output'])
await ctx.fiber.dispose()
})
@@ -96,8 +96,19 @@ let consumer: string | undefined
let child: ReturnType<typeof spawn> | undefined
afterEach(async () => {
if (child !== undefined) { child.kill('SIGKILL'); child = undefined }
if (consumer !== undefined) await rm(consumer, { recursive: true, force: true })
if (child !== undefined) {
const proc = child
child = undefined
// Windows retains the child's cwd and session-log handles until process
// teardown completes, so await exit before removing the temp directory.
if (proc.exitCode === null && proc.signalCode === null) {
const exited = new Promise<void>((resolve) => { proc.once('exit', () => { resolve() }) })
proc.kill('SIGKILL')
await exited
}
}
// Windows can briefly retain released handles after exit; retry removal.
if (consumer !== undefined) await rm(consumer, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
consumer = undefined
})