fix(host): opt in to model titles from web

This commit is contained in:
Tianyi Cui
2026-07-23 20:08:33 +08:00
parent 41a6a07833
commit 29f293675a
5 changed files with 34 additions and 11 deletions

View File

@@ -10,7 +10,7 @@ The TUI surface:
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it; - tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree. - applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
The Web surface treats its invoking directory as the default project and loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget. The Web surface treats its invoking directory as the default project, loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opts into first-message model titles. The headless surface retains deterministic fallback titles without making the auxiliary title-model request.
## Install (developer machine) ## Install (developer machine)

View File

@@ -40,6 +40,7 @@ export async function runWeb(argv: string[]): Promise<void> {
boot: { boot: {
persistenceRoot: './.sessions', persistenceRoot: './.sessions',
workspaceContext: { maxBytes: 65_536 }, workspaceContext: { maxBytes: 65_536 },
sessionTitleLlm: true,
}, },
}) })

View File

@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-host-runtime # @deepseek-ai/dsh-host-runtime
Host runtime assembly for `dsh`: `bootHost` composes the core plugin spine (LLM service + DeepSeek adapter, sessions with JSONL persistence, immediate fallback titles and first-message model summaries, system prompt, tools, agents, agent loop, workspace instructions, local bash), `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 }`. 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), `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 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.
@@ -14,7 +14,7 @@ Which plugins mount and with what defaults is decided only here — shells must
| `model` | `'deepseek-v4-flash'` | Default model id, same single source as `provider`. | | `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`. | | `cwd` | `process.cwd()` | Default project directory for a session whose create request omits `cwd`. |
| `sessionTitle` | 5 words / 40 fallback bytes / 80 accepted bytes | Deterministic fallback and accepted-title limits. | | `sessionTitle` | 5 words / 40 fallback bytes / 80 accepted bytes | Deterministic fallback and accepted-title limits. |
| `sessionTitleLlm` | 5 words / 10 CJK chars / 4,096 input bytes / 64 output tokens / 60 s | First-message model-title policy. An omitted route inherits the logged main-request provider and model. | | `sessionTitleLlm` | disabled | `true` enables the 5-word / 10-CJK-character, 4,096-input-byte, 64-output-token, 60-second first-message policy; an explicit config overrides it. An omitted route inherits the logged main-request provider and model. |
## ApiProxy implementation notes ## ApiProxy implementation notes
@@ -22,11 +22,11 @@ Unary methods take the narrow `RpcRequest<P>` and echo `request.rpcId`; a prompt
## Model Experience ## Model Experience
Indirectly, through the non-blocking first-message title request owned by [`dsh-session-title-llm`](../../session-title/session-title-llm/README.md), the provider/model defaults injected into created and resumed agents, and the other model-facing plugins `bootHost` mounts. When `workspaceContext` is enabled, each agent-loop instance freezes the applicable workspace instructions into its logged request prefix; the owning package documents the exact [model-visible framing](../../context/workspace-context/README.md#prompt-shape). Indirectly, through the non-blocking first-message title request owned by [`dsh-session-title-llm`](../../session-title/session-title-llm/README.md) when `sessionTitleLlm` is enabled, the provider/model defaults injected into created and resumed agents, the other model-facing plugins `bootHost` mounts, and the logged [workspace-instruction prefix](../../context/workspace-context/README.md#prompt-shape) when `workspaceContext` is enabled.
#### KV Cache effect #### KV Cache effect
No main-request invalidation; the auxiliary title request has its own cache behavior and the conversation prefix remains unchanged. No main-request invalidation; when enabled, the auxiliary title request has its own cache behavior and leaves the conversation prefix unchanged.
## Known Limitations and Deferred Work ## Known Limitations and Deferred Work

View File

@@ -70,8 +70,8 @@ export interface BootHostOptions {
model?: string model?: string
/** Deterministic fallback-title limits. */ /** Deterministic fallback-title limits. */
sessionTitle?: SessionTitleConfig sessionTitle?: SessionTitleConfig
/** First-message model-title policy; omitted provider/model inherit the session's logged main-request route. */ /** Opt-in first-message model-title policy; `true` selects host defaults and an explicit config overrides them. */
sessionTitleLlm?: SessionTitleLlmConfig sessionTitleLlm?: true | SessionTitleLlmConfig
/** /**
* Default project directory for sessions created without an explicit cwd * Default project directory for sessions created without an explicit cwd
* (defaults to the host process working directory). A session's cwd is its * (defaults to the host process working directory). A session's cwd is its
@@ -116,7 +116,12 @@ export async function bootHost(options: BootHostOptions): Promise<HostHandle> {
await ctx.plugin(LlmService) await ctx.plugin(LlmService)
await ctx.plugin(SessionStore) await ctx.plugin(SessionStore)
await ctx.plugin(SessionTitleService, options.sessionTitle ?? DEFAULT_SESSION_TITLE_CONFIG) await ctx.plugin(SessionTitleService, options.sessionTitle ?? DEFAULT_SESSION_TITLE_CONFIG)
await ctx.plugin(SessionTitleFirstMessageLlm, options.sessionTitleLlm ?? DEFAULT_SESSION_TITLE_LLM_CONFIG) if (options.sessionTitleLlm !== undefined) {
await ctx.plugin(
SessionTitleFirstMessageLlm,
options.sessionTitleLlm === true ? DEFAULT_SESSION_TITLE_LLM_CONFIG : options.sessionTitleLlm,
)
}
await ctx.plugin(SystemPrompt, { persona: '' }) await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry) await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry) await ctx.plugin(AgentRegistry)

View File

@@ -104,7 +104,7 @@ afterEach(async () => {
async function boot( async function boot(
script: (StreamChunk[] | 'hang')[] = [], script: (StreamChunk[] | 'hang')[] = [],
sessionTitle?: SessionTitleConfig, sessionTitle?: SessionTitleConfig,
sessionTitleLlm?: SessionTitleLlmConfig, sessionTitleLlm?: true | SessionTitleLlmConfig,
): Promise<RunningHost> { ): Promise<RunningHost> {
host = await startHost({ host = await startHost({
boot: { boot: {
@@ -188,6 +188,23 @@ describe('bootHost / startHost', () => {
expect(requestText).toContain('Instructions from: AGENTS.md') expect(requestText).toContain('Instructions from: AGENTS.md')
expect(requestText).toContain('host-workspace-context-probe') expect(requestText).toContain('host-workspace-context-probe')
}) })
it('keeps model title generation disabled when sessionTitleLlm is omitted', async () => {
const running = await boot([textResponse('pong')])
const { api, ctx } = running
const { sessionId } = expectOk(await api.sessions.create(request({})))
const agent = ctx.agents.get(sessionId) as Agent
const idle = waitForIdle(ctx, agent)
expectOk(await api.sessions.prompt(request({
sessionId,
mode: 'queue' as const,
content: [{ type: 'text' as const, text: 'Explain durable session titles.' }],
})))
await idle
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)
})
}) })
describe('host.describe', () => { describe('host.describe', () => {
@@ -218,7 +235,7 @@ describe('sessions.create / list', () => {
describe('sessions.prompt / cancel', () => { describe('sessions.prompt / cancel', () => {
it.each([ it.each([
{ name: 'host default', config: undefined, target: '5 words', maxTokens: 64 }, { name: 'host default', config: true, target: '5 words', maxTokens: 64 },
{ {
name: 'configured policy', name: 'configured policy',
config: { config: {
@@ -233,7 +250,7 @@ describe('sessions.prompt / cancel', () => {
}, },
] satisfies { ] satisfies {
name: string name: string
config: SessionTitleLlmConfig | undefined config: true | SessionTitleLlmConfig
target: string target: string
maxTokens: number maxTokens: number
}[])('replaces the fallback with a model-backed first-message title using the $name', async ({ config, target, maxTokens }) => { }[])('replaces the fallback with a model-backed first-message title using the $name', async ({ config, target, maxTokens }) => {