Merge remote-tracking branch 'origin/master' into codex/ask-user-question

# Conflicts:
#	docs/rfc/README.md
#	packages/ui/acp-agent/src/index.ts
This commit is contained in:
Yichen Jiang
2026-07-06 10:08:58 +08:00
217 changed files with 2924 additions and 1384 deletions
+1 -1
View File
@@ -25,7 +25,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`).
+15 -12
View File
@@ -41,37 +41,40 @@ import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
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
}
export const Config: z<Config> = z.object({
model: z.string().required(),
systemPrompt: z.string().required(),
persona: z.string(),
persistenceRoot: z.string().default('./.sessions'),
})
/**
* 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)
ctx.plugin(agentCore, {
...config.persona !== undefined ? { persona: config.persona } : {},
})
ctx.plugin(UserInteractionService)
ctx.plugin(toolAskUser)
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
ctx.plugin(acp, { model: config.model, systemPrompt: config.systemPrompt })
ctx.plugin(acp, { model: config.model })
}
@@ -24,7 +24,7 @@ async function mount(config: acpAgent.Config): Promise<Context> {
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' })
const ctx = await mount({ model: 'mock', persona: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test' })
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('sessions')).toBeDefined()
expect(ctx.get('sessionPersistence')).toBeDefined()
@@ -42,7 +42,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' })
// No persona: covers the omitted-persona forwarding branch too.
acpAgent.apply(ctx, { model: 'mock' })
await new Promise(resolve => setTimeout(resolve, 50))
expect(ctx.get('sessionPersistence')).toBeDefined()
await ctx.fiber.dispose()