fix(host): opt in to model titles from web
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user