feat(host-runtime,llm-replay): keyless llm seam + replay pacing/consumption handle
BootHostOptions.llm: 'deepseek' | false — false mounts no adapter, boots keyless, and leaves the llm capability seam open for the embedder to fill on RunningHost.ctx (now the third sanctioned ctx use, JSDoc + README amended); an unfilled seam fails loud with NO_ADAPTER at the first stream. dsh-llm-replay grows two additive surfaces for the web browser e2e lane: paceMs (validated per-chunk delay so a real transport shows incremental delivery; abort during a pace wait cancels promptly) and a ReplayHandle return — dispose() plus assertConsumed(), the teardown check that every recorded script bound and drained, converting silent fixture underruns into diagnostics. Existing callers updated; config catalog regenerated.
This commit is contained in:
8 files changed
+201
-18
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Host runtime assembly for `dsh`: `bootHost` composes the core plugin spine (LLM service + DeepSeek adapter, sessions with JSONL persistence and immediate fallback titles, optional first-message model summaries, system prompt, tools, agents, agent loop, workspace instructions, local bash, and the provider-neutral user-interaction service), `createApiProxy` implements the [`dsh-host-apiproxy`](../apiproxy/README.md) contract over that composition, and `startHost` is the one-step shell seam returning `{ api, handler, defaults, ctx, dispose }`.
|
||||
|
||||
Which plugins mount and with what defaults is decided only here — shells must not `ctx.plugin` to alter the assembly. `RunningHost.ctx` is a formal seam with exactly two sanctioned uses: mounting protocol front-door plugins (e.g. a future `dsh acp`) and headless session-event subscription; consuming clients must not bypass `api` through it.
|
||||
Which plugins mount and with what defaults is decided only here — shells must not `ctx.plugin` to alter the assembly. `RunningHost.ctx` is a formal seam with exactly three sanctioned uses: mounting protocol front-door plugins (e.g. a future `dsh acp`), headless session-event subscription, and filling a capability seam the boot options deliberately left open (`llm: false` → the embedder installs its own LLM backend, e.g. the keyless web e2e harness's replay); consuming clients must not bypass `api` through it.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -10,6 +10,7 @@ Which plugins mount and with what defaults is decided only here — shells must
|
||||
|---|---:|---|
|
||||
| `persistenceRoot` | (required) | Root directory for JSONL session persistence. |
|
||||
| `workspaceContext` | (required) | [`AGENTS.md`/`CLAUDE.md` loader](../../context/workspace-context/README.md) config with an explicit `maxBytes`, or `false` to disable it. |
|
||||
| `llm` | `'deepseek'` | LLM adapter selection: `'deepseek'` mounts the DeepSeek adapter (API key required at load); `false` mounts none, boots keyless, and leaves the `llm` seam open for the embedder — an unfilled seam fails loud with `NO_ADAPTER` at the first stream. |
|
||||
| `provider` | `'deepseek'` | Default provider route injected as agentOptions on create/resume and reported by `host.describe`. |
|
||||
| `model` | `'deepseek-v4-flash'` | Default model id, same single source as `provider`. |
|
||||
| `cwd` | `process.cwd()` | Default project directory for a session whose create request omits `cwd`. |
|
||||
|
||||
@@ -65,6 +65,15 @@ export interface BootHostOptions {
|
||||
persistenceRoot: string
|
||||
/** Workspace-instruction byte budget/config, or false to disable AGENTS.md/CLAUDE.md loading. */
|
||||
workspaceContext: workspaceContext.Config | false
|
||||
/**
|
||||
* LLM adapter selection: `'deepseek'` (default) mounts the DeepSeek adapter
|
||||
* (requires an API key at load), `false` mounts no adapter and leaves the
|
||||
* `llm` capability seam open for the embedder to fill on the returned ctx
|
||||
* (e.g. the keyless web e2e harness installing a replay backend). With
|
||||
* `false` and nothing filled, the first stream fails loud with NO_ADAPTER —
|
||||
* the earliest resolvable point for an open capability seam.
|
||||
*/
|
||||
llm?: 'deepseek' | false
|
||||
/** Default provider route for created/resumed agents (defaults to 'deepseek', the only adapter bootHost registers). */
|
||||
provider?: string
|
||||
/** Default model id (defaults to 'deepseek-v4-flash', matching the demos). */
|
||||
@@ -129,7 +138,7 @@ export async function bootHost(options: BootHostOptions): Promise<HostHandle> {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(TaskService)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(LlmDeepSeek, {})
|
||||
if (options.llm !== false) await ctx.plugin(LlmDeepSeek, {})
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root: options.persistenceRoot })
|
||||
await ctx.plugin(LocalBashExecutor, {})
|
||||
// Tool suite mirroring the demo:repl composition (repl-agent/cordis.yml +
|
||||
|
||||
@@ -34,9 +34,12 @@ export interface RunningHost {
|
||||
/**
|
||||
* Root context — a formal seam, not an escape hatch: (1) the mount point for
|
||||
* protocol front-door plugins (`dsh acp` = startHost() → ctx.plugin(uiAcp, config));
|
||||
* (2) headless session-event subscription. Discipline: consuming clients must
|
||||
* not bypass `api` through ctx; shells must not ctx.plugin to alter the
|
||||
* assembly (mounting a front door is the shell's own shape, not an assembly change).
|
||||
* (2) headless session-event subscription; (3) filling a capability seam the
|
||||
* boot options deliberately left open (`llm: false` → the embedder installs
|
||||
* its own LLM backend, e.g. keyless replay). Discipline: consuming clients
|
||||
* must not bypass `api` through ctx; shells must not ctx.plugin to alter the
|
||||
* assembly (mounting a front door or filling an explicitly-open seam is the
|
||||
* shell's own shape, not an assembly change).
|
||||
*/
|
||||
ctx: Context
|
||||
/** Single shutdown exit (ctx.fiber.dispose()). Idempotent: a second call returns the same promise. */
|
||||
|
||||
@@ -205,6 +205,42 @@ describe('bootHost / startHost', () => {
|
||||
expect((await ctx.sessionTitle.refresh(agent.session))?.source).toEqual({ kind: 'fallback' })
|
||||
expect(agent.session.events.some(event => event.type === 'session/title-llm-request')).toBe(false)
|
||||
})
|
||||
|
||||
it('llm: false boots keyless with no adapter and fails loud at the first stream', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
||||
const handle: HostHandle = await bootHost({
|
||||
persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-boot-keyless-')),
|
||||
workspaceContext: false,
|
||||
llm: false,
|
||||
})
|
||||
// The seam is open: nothing routes 'deepseek', so misuse surfaces at the
|
||||
// earliest resolvable point instead of silently doing provider I/O.
|
||||
await expect(async () => {
|
||||
for await (const chunk of handle.ctx.llm.stream({
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-v4-flash',
|
||||
messages: [],
|
||||
})) void chunk
|
||||
}).rejects.toThrow(/NO_ADAPTER|no adapter/i)
|
||||
// The embedder can fill the open seam on the returned ctx (the sanctioned
|
||||
// RunningHost.ctx use) and streams route through the filled adapter.
|
||||
class ProbeAdapter extends LlmAdapter {
|
||||
async * stream(): AsyncIterable<StreamChunk> {
|
||||
yield * textResponse('keyless-ok')
|
||||
}
|
||||
}
|
||||
handle.ctx.llm.registerAdapter(['deepseek'], new ProbeAdapter())
|
||||
const collected: string[] = []
|
||||
for await (const chunk of handle.ctx.llm.stream({
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-v4-flash',
|
||||
messages: [],
|
||||
})) {
|
||||
if (chunk.type === 'text-delta') collected.push(chunk.text)
|
||||
}
|
||||
expect(collected.join('')).toBe('keyless-ok')
|
||||
await handle.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('host.describe', () => {
|
||||
|
||||
Reference in New Issue
Block a user