Przeglądaj źródła

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

Tianyi Cui 2 miesięcy temu
rodzic
commit
29f293675a

+ 1 - 1
apps/cli/README.md

@@ -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;
 - 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)
 

+ 1 - 0
apps/cli/src/web.ts

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

+ 4 - 4
packages/host/runtime/README.md

@@ -1,6 +1,6 @@
 # @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.
 
@@ -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`. |
 | `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. |
-| `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
 
@@ -22,11 +22,11 @@ Unary methods take the narrow `RpcRequest<P>` and echo `request.rpcId`; a prompt
 
 ## 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
 
-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
 

+ 8 - 3
packages/host/runtime/src/boot.ts

@@ -70,8 +70,8 @@ export interface BootHostOptions {
   model?: string
   /** Deterministic fallback-title limits. */
   sessionTitle?: SessionTitleConfig
-  /** First-message model-title policy; omitted provider/model inherit the session's logged main-request route. */
-  sessionTitleLlm?: SessionTitleLlmConfig
+  /** Opt-in first-message model-title policy; `true` selects host defaults and an explicit config overrides them. */
+  sessionTitleLlm?: true | SessionTitleLlmConfig
   /**
    * Default project directory for sessions created without an explicit cwd
    * (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(SessionStore)
   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(ToolRegistry)
   await ctx.plugin(AgentRegistry)

+ 20 - 3
packages/host/runtime/tests/host-runtime.spec.ts

@@ -104,7 +104,7 @@ afterEach(async () => {
 async function boot(
   script: (StreamChunk[] | 'hang')[] = [],
   sessionTitle?: SessionTitleConfig,
-  sessionTitleLlm?: SessionTitleLlmConfig,
+  sessionTitleLlm?: true | SessionTitleLlmConfig,
 ): Promise<RunningHost> {
   host = await startHost({
     boot: {
@@ -188,6 +188,23 @@ describe('bootHost / startHost', () => {
     expect(requestText).toContain('Instructions from: AGENTS.md')
     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', () => {
@@ -218,7 +235,7 @@ describe('sessions.create / list', () => {
 
 describe('sessions.prompt / cancel', () => {
   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',
       config: {
@@ -233,7 +250,7 @@ describe('sessions.prompt / cancel', () => {
     },
   ] satisfies {
     name: string
-    config: SessionTitleLlmConfig | undefined
+    config: true | SessionTitleLlmConfig
     target: string
     maxTokens: number
   }[])('replaces the fallback with a model-backed first-message title using the $name', async ({ config, target, maxTokens }) => {