fix(session-title): preserve adequate conversation titles
This commit is contained in:
1 parent
8af3b410f2
commit
1d37aabe99
20 files changed
+485
-40
No files matched your search
@@ -308,9 +308,15 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
await expect.poll(() => titleRequests(following.agent).length).toBe(1)
|
||||
expect(titleRequests(following.agent)[0]?.data.titleProvider).toBe('session-title-all-prompts-llm')
|
||||
await expect.poll(() => ctx.sessionTitle.get(following.agent.session)?.source.kind).toBe('provider')
|
||||
const currentTitle = ctx.sessionTitle.get(following.agent.session)?.title
|
||||
const firstInput = titleRequests(following.agent)[0]?.data.messages[0]?.content[0]
|
||||
expect(firstInput?.type === 'text' && firstInput.text).toContain('Generate the session title from this JSON array')
|
||||
await turn(following.agent, 'Continue the growing conversation.', [textResponse('TWO')])
|
||||
await expect.poll(() => titleRequests(following.agent).length).toBe(2)
|
||||
expect(titleRequests(following.agent).every(event => event.data.titleProvider === 'session-title-all-prompts-llm')).toBe(true)
|
||||
const followingInput = titleRequests(following.agent)[1]?.data.messages[0]?.content[0]
|
||||
expect(followingInput?.type === 'text' && followingInput.text)
|
||||
.toContain(`Update the session title from this JSON object:\n{"currentTitle":${JSON.stringify(currentTitle)},`)
|
||||
await select(bundles.titles, false)
|
||||
const restored = await agent()
|
||||
await turn(restored.agent, 'Restore first-prompt titles.', [textResponse('ONE')])
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 2c0bccf753fe70eb
|
||||
zh: 36b96a547ddddfc1
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/use-this-package/when-titles-are-generated:
|
||||
en: 3c9e14df96e6000e
|
||||
zh: a13f12a3df426380
|
||||
en: 5750c08ae93b2c07
|
||||
zh: 130724e56a833217
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/use-this-package/configuration:
|
||||
en: 1965c9ce7dedb2e7
|
||||
zh: 9b9c9b68af995b51
|
||||
@@ -48,8 +48,8 @@
|
||||
en: 42380e2d029288cf
|
||||
zh: e9c9f21ac00e3c9d
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/model-experience/all-messages-title-request/what-the-model-sees:
|
||||
en: 39cb7059a3ab1fe6
|
||||
zh: a3fd93ac52f12a43
|
||||
en: 62c10ce23e949a9a
|
||||
zh: 96c97e2b57722c52
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/model-experience/all-messages-title-request/token-effect:
|
||||
en: 1a075b8073c16ce3
|
||||
zh: a51ff16156f9c137
|
||||
@@ -57,8 +57,8 @@
|
||||
en: 6a70f43f38b8c075
|
||||
zh: 2b823aff93344134
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/known-limitations-and-deferred-work:
|
||||
en: d0486c65afaebb85
|
||||
zh: 186a77e0d68bd968
|
||||
en: 48baddec2b04e7d2
|
||||
zh: 46e85efbead4a309
|
||||
/deepseek-ai-dsh-session-title-all-prompts-llm/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -31,6 +31,8 @@ Mount this plugin beside the title service when a session should be retitled as
|
||||
|
||||
A new revision starts after each new eligible human prompt, including prompts in child sessions; the generation folds all eligible messages through the current revision, seeded history included. A newer revision aborts and supersedes older work, so a stale completion can never commit. An automatic failure — including input over `maxInputBytes`, which fails instead of truncating history — warns and keeps the prior title; `ctx.sessionTitle.refresh()` is the explicit retry.
|
||||
|
||||
Once a provider-generated title exists, each request includes it and asks the model to preserve it exactly while it still describes the main topic or task. Follow-up questions, same-topic details, acknowledgements such as “thanks”, and requests to continue do not by themselves justify rewording. The model is instructed to update the title only when a material change or expansion of the main topic or task makes it inaccurate. A fallback title still receives ordinary initial generation; every eligible prompt still schedules a revision.
|
||||
|
||||
### Configuration
|
||||
|
||||
The plugin accepts the complete required [shared LLM configuration](../../session/session-title-llm/README.md#configuration): `targetWords`, `targetCjkCharacters`, `maxInputBytes`, `maxOutputTokens`, `timeoutMs`, and the optional paired `provider`/`model` route. Omit both to inherit the exact route from each current logged main request, or set both to route title generation independently. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-title-all-prompts-llm) is the exhaustive source for every accepted field.
|
||||
@@ -86,7 +88,7 @@ Read these pages when the provider contract is not enough. They move from the sh
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The title model receives the shared title instruction and a JSON array of all eligible human messages through the current revision, in log order with exact seqs. Seeded history is included.
|
||||
The title model receives all eligible human messages through the current revision, in log order with exact seqs, including seeded history. When a provider-generated title exists, the [shared LLM policy](../../session/session-title-llm/README.md#model-experience) adds it as `currentTitle` with preservation instructions. Both the messages and current title count toward `maxInputBytes`.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -104,7 +106,8 @@ No main-request invalidation. Auxiliary input grows or changes after each prompt
|
||||
These limits define how the provider treats long and heterogeneous sessions. They are current package constraints.
|
||||
|
||||
- **No summarization-of-summaries** — input overflow retains the prior title; this provider has no summarization-of-summaries or retention policy for very long sessions.
|
||||
- **Messages are treated equally** — it treats all eligible human messages alike and offers no weighting, filtering, or manual-title precedence.
|
||||
- **Messages are treated equally** — it includes every eligible human message without configurable weighting or filtering.
|
||||
- **Model-dependent stability** — title preservation is a model instruction, not a deterministic semantic check; the model may still change an adequate title.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -31,6 +31,8 @@ kind: "package-reference"
|
||||
|
||||
每条新的符合条件用户提示词之后都会启动新修订,包括子会话中的提示词;生成会折叠截至当前修订的所有符合条件消息,预置历史也包含在内。较新的修订会中止并取代旧工作,因此陈旧的完成结果永远无法提交。自动失败——包括输入超过 `maxInputBytes`(此时请求失败而非截断历史)——会发出警告并保留先前标题;`ctx.sessionTitle.refresh()` 是显式重试。
|
||||
|
||||
已有提供方生成的标题后,每次请求都会包含该标题,并要求模型在它仍能描述主要主题或任务时逐字保留。同主题追问、补充细节、致谢(如“谢谢”)和继续请求本身都不构成改写理由。指令要求模型仅在主要主题或任务发生实质变化或扩展、使旧标题不再准确时更新。回退标题仍会正常触发首次生成;每条符合条件的提示词仍会安排一次修订。
|
||||
|
||||
### 配置
|
||||
|
||||
插件接受完整必填的[共享 LLM 配置](../../session/session-title-llm/README.zh.md#configuration):`targetWords`、`targetCjkCharacters`、`maxInputBytes`、`maxOutputTokens`、`timeoutMs`,以及可选成对的 `provider`/`model` 路由。同时省略二者,会继承每个当前已记录主请求的确切路由;同时设置二者,则让标题生成使用独立路由。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-title-all-prompts-llm)是每个受支持字段的穷尽式真源。
|
||||
@@ -86,7 +88,7 @@ kind: "package-reference"
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
标题模型会收到共享标题指令,以及一个 JSON 数组,其中按日志顺序包含截至当前修订的所有符合条件用户消息和确切 seq。预置历史也包含在内。
|
||||
标题模型会收到截至当前修订的所有符合条件用户消息,按日志顺序排列并带有确切 seq,包括预置历史。已有提供方生成的标题时,[共享 LLM 策略](../../session/session-title-llm/README.zh.md#model-experience)会将其作为 `currentTitle` 传入,并添加保留标题的指令。消息与当前标题均计入 `maxInputBytes`。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -104,7 +106,8 @@ kind: "package-reference"
|
||||
这些限制说明提供方如何对待长会话与异构会话。它们是当前包约束。
|
||||
|
||||
- **没有基于摘要继续生成摘要的机制**——输入溢出时保留先前标题;对于很长的会话,此提供方没有基于摘要继续生成摘要的机制或保留策略。
|
||||
- **消息被平等对待**——它平等对待所有符合条件的用户消息,不提供权重、过滤或手动标题优先级。
|
||||
- **消息被平等对待**——它包含每条符合条件的用户消息,不提供可配置的权重或过滤。
|
||||
- **稳定性依赖模型**——保留标题依靠模型指令,没有确定性的语义校验;模型仍可能改动合适的标题。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import LlmRuntime, { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
|
||||
import SessionTitleService from '@deepseek-ai/dsh-session-title'
|
||||
import SessionTitleService, { SessionTitleProviderId, type SessionTitleProvider, type SessionTitleSource } from '@deepseek-ai/dsh-session-title'
|
||||
import * as providerPlugin from '@deepseek-ai/dsh-session-title-all-prompts-llm'
|
||||
|
||||
class RecordingAdapter extends LlmAdapter {
|
||||
@@ -32,6 +32,54 @@ async function settle(): Promise<void> {
|
||||
}
|
||||
|
||||
describe('all-messages LLM title provider', () => {
|
||||
it.each([
|
||||
undefined,
|
||||
{ kind: 'fallback' },
|
||||
{ kind: 'user' },
|
||||
{ kind: 'provider', provider: SessionTitleProviderId('previous-provider') },
|
||||
] satisfies Array<SessionTitleSource | undefined>)('preserves only an existing provider title (%j)', async (source) => {
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
|
||||
await ctx.plugin(SessionTitleService, TITLE_CONFIG)
|
||||
const adapter = new RecordingAdapter()
|
||||
ctx.llm.registerAdapter(['current-route'], adapter)
|
||||
let registered: SessionTitleProvider | undefined
|
||||
vi.spyOn(ctx.sessionTitle, 'register').mockImplementation((provider) => {
|
||||
registered = provider
|
||||
return async () => undefined
|
||||
})
|
||||
providerPlugin.apply(ctx, LLM_CONFIG)
|
||||
const session = ctx.sessions.create(SessionId(`title-source-${source?.kind ?? 'absent'}`))
|
||||
const first = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: '阳台种番茄和罗勒' }], source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
if (source !== undefined) {
|
||||
session.append('session/title', { title: '阳台种植建议', messageSeqs: [first.seq], source })
|
||||
}
|
||||
await registered!.generate({
|
||||
session,
|
||||
messages: [{ seq: first.seq, text: '阳台种番茄和罗勒' }],
|
||||
route: { provider: 'current-route', model: 'current-model' },
|
||||
signal: new AbortController().signal,
|
||||
})
|
||||
|
||||
const options = adapter.requests[0]!
|
||||
const content = options.messages[0]!.content[0]!
|
||||
if (content.type !== 'text') throw new Error('expected title input text')
|
||||
if (source?.kind === 'provider') {
|
||||
expect(content.text).toContain('"currentTitle":"阳台种植建议"')
|
||||
expect(options.system).toContain('return it exactly unchanged')
|
||||
} else {
|
||||
expect(content.text).toContain('Generate the session title from this JSON array')
|
||||
expect(content.text).not.toContain('currentTitle')
|
||||
expect(options.system).not.toContain('return it exactly unchanged')
|
||||
}
|
||||
})
|
||||
|
||||
it('includes seeded history and the latest prompt while inheriting the logged request route', async () => {
|
||||
const seeded = Session.create(SessionId('seed-source'))
|
||||
seeded.append('turn/start', { turn: 1 })
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 988e839059414d27
|
||||
/deepseek-ai-dsh-experimental-session-titles-bundle/use-this-package:
|
||||
en: 7a92ac057d492afa
|
||||
zh: 7f018e1611e4b6b9
|
||||
en: 3a843c2d148f94f1
|
||||
zh: e67b41828084eb86
|
||||
/deepseek-ai-dsh-experimental-session-titles-bundle/understand-the-implementation:
|
||||
en: f4a8d06bd561f7e9
|
||||
zh: de811cf2c131256c
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 1b2209bbbf869b1a
|
||||
zh: 5b9de62271343e4e
|
||||
/deepseek-ai-dsh-experimental-session-titles-bundle/known-limitations-and-deferred-work:
|
||||
en: 76117a232098ada0
|
||||
zh: 5b40afc9b66384fd
|
||||
en: 833386166a2ec27c
|
||||
zh: a70ebec93a3be891
|
||||
/deepseek-ai-dsh-experimental-session-titles-bundle/known-limitations-and-deferred-work/dev-note:
|
||||
en: 279d01435256cd2a
|
||||
zh: 83780964e5de2f54
|
||||
@@ -27,7 +27,7 @@ Generate experimental session titles from the conversation's eligible human prom
|
||||
|
||||
In Web or Desktop, open Plugins and enable **Conversation-following titles** in Official. Switch it off to remove its profile layer.
|
||||
|
||||
The switch changes the title provider for existing and new Sessions. Titles update on eligible human prompts or explicit refresh; automatic updates preserve user-pinned titles. Agent tool catalogs remain unchanged.
|
||||
The switch changes the title provider for existing and new Sessions. Eligible human prompts or explicit refresh trigger title requests; the model is asked to keep an existing generated title exactly while it still describes the main topic or task. Automatic updates preserve user-pinned titles. Agent tool catalogs remain unchanged.
|
||||
|
||||
-----
|
||||
|
||||
@@ -63,7 +63,7 @@ The layer adds no request content directly; the capability implementation owns c
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- Automatic title failures retain the previous title. The 4096-byte title-input budget rejects oversized input instead of truncating conversation history.
|
||||
- Automatic title failures retain the previous title. The 4096-byte title-input budget includes the current generated title when supplied and rejects oversized input instead of truncating conversation history.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-bundle"
|
||||
|
||||
在 Web 或 Desktop 中打开插件页,在官方分组启用 **随对话更新的标题**。关闭后移除其配置层。
|
||||
|
||||
开关会更改已有和新建 Session 的标题提供方。标题在合格的人类提示或显式刷新时更新;自动更新不会覆盖用户固定的标题。智能体的工具目录保持不变。
|
||||
开关会更改已有和新建 Session 的标题提供方。符合条件的用户提示词或显式刷新会触发标题请求;模型被要求在已有生成标题仍能描述主要主题或任务时逐字保留它。自动更新不会覆盖用户固定的标题。智能体的工具目录保持不变。
|
||||
|
||||
-----
|
||||
|
||||
@@ -63,7 +63,7 @@ kind: "package-bundle"
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- 自动标题生成失败时保留旧标题。标题输入预算为 4096 字节,超限时拒绝输入而不截断对话历史。
|
||||
- 自动标题生成失败时保留旧标题。标题输入预算为 4096 字节,包含传入的当前生成标题,超限时拒绝输入而不截断对话历史。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ describe('first-prompt LLM title provider', () => {
|
||||
const content = options.messages[0]?.content[0]
|
||||
expect(content?.type === 'text' && content.text).toContain('first input')
|
||||
expect(content?.type === 'text' && content.text).not.toContain('second input must be ignored')
|
||||
expect(content?.type === 'text' && content.text).not.toContain('currentTitle')
|
||||
expect(options.system).not.toContain('return it exactly unchanged')
|
||||
}
|
||||
expect(ctx.sessionTitle.get(session)).toMatchObject({ messageSeqs: [first.seq] })
|
||||
})
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 6ea51316a841bce7
|
||||
zh: df455b74ac9d4a91
|
||||
/deepseek-ai-dsh-session-title-llm/use-this-package/registering-a-provider:
|
||||
en: cf61c021b1bdb52f
|
||||
zh: dac17588e4143815
|
||||
en: 82ddf0a0e1a3f2e6
|
||||
zh: f93e848ee0ddacfd
|
||||
/deepseek-ai-dsh-session-title-llm/use-this-package/route-and-failure-contract:
|
||||
en: ef4c7003eaa84cf6
|
||||
zh: 5a13dde678ea3e14
|
||||
@@ -36,8 +36,8 @@
|
||||
en: 0fdc928704abdd3a
|
||||
zh: bd4c0a7dd47578cf
|
||||
/deepseek-ai-dsh-session-title-llm/understand-the-implementation/request-flow:
|
||||
en: 0a6d1cd67188ab21
|
||||
zh: 6259ae486230e88d
|
||||
en: a6a173b3f4be8102
|
||||
zh: 1c104d8099f93201
|
||||
/deepseek-ai-dsh-session-title-llm/further-exploration:
|
||||
en: d9844c0fc3e16c5c
|
||||
zh: 1e0c0b4b5d566972
|
||||
@@ -48,14 +48,14 @@
|
||||
en: 7dcc6f74474f78e7
|
||||
zh: 64cffa0b4d94c69d
|
||||
/deepseek-ai-dsh-session-title-llm/model-experience/auxiliary-title-request/what-the-model-sees:
|
||||
en: 780cf5c74d231b24
|
||||
zh: 4cf0e39c6a8475ad
|
||||
en: 85b79ec06dcaa104
|
||||
zh: 66bd534155a30bb6
|
||||
/deepseek-ai-dsh-session-title-llm/model-experience/auxiliary-title-request/token-effect:
|
||||
en: 43f7973c421d4155
|
||||
zh: c63ffe0858e5b71e
|
||||
/deepseek-ai-dsh-session-title-llm/model-experience/auxiliary-title-request/kv-cache-effect:
|
||||
en: 5d3b5b7c8d8b4b67
|
||||
zh: 1b231d96bf6ca545
|
||||
en: c15c8288a973bf88
|
||||
zh: dd742aeb138b288a
|
||||
/deepseek-ai-dsh-session-title-llm/known-limitations-and-deferred-work:
|
||||
en: eab2e1857d98680a
|
||||
zh: 02cd4a5d5f565e1b
|
||||
|
||||
@@ -31,6 +31,8 @@ As a deployment, configure this policy through the [first-prompt](../session-tit
|
||||
|
||||
A provider plugin calls `registerSessionTitleLlmProvider(ctx, config, id, automatic, selectMessages)`; the helper validates the shared config, registers the provider on `ctx.sessionTitle`, and runs every generation through the shared policy. The two shipped plugins register the `first-prompt` and `all-prompts` cadences with their message selectors, and a second registration on the service throws.
|
||||
|
||||
For `all-prompts`, the helper also supplies the current title when a provider generated it, including a title inherited from the first-prompt provider. It excludes fallback and user-supplied titles from this input. The first-prompt cadence uses only its selected messages.
|
||||
|
||||
### Route and failure contract
|
||||
|
||||
`provider` and `model` overrides are optional but must be supplied together as non-empty strings. Without that pair, the helper uses the exact provider/model route captured from the current session's logged `request/header`, so an explicit refresh before any route exists needs overrides. The helper measures the final JSON-framed user prompt against `maxInputBytes` before logging or dispatch instead of truncating it, and rechecks timeout and caller cancellation while consuming the stream and after it completes, so a late successful result cannot be accepted even if an interceptor or adapter ignores abort. Malformed or empty output, tool calls, and non-stop finish reasons reject; the session-title service decides whether that rejection is an automatic warning or an explicit caller failure. The accepted title is the first non-empty line of the model's text output, with one emphasis pair removed when it wraps that whole line, so commentary a model writes after the title cannot become the title. The title request uses `maxOutputTokens` independently of the conversation's cap. Its route must have a registered adapter so preparation can resolve the request before it is recorded.
|
||||
@@ -72,7 +74,7 @@ One shared policy so provider plugins cannot drift: config validation, route res
|
||||
|
||||
### Request flow
|
||||
|
||||
Each revision frames the selected messages as JSON and checks `maxInputBytes`. The title configuration function selects the first effort from the route's least-to-greatest list during `ctx.llm.prepareCall()`. The helper records the exact input, output cap, and resolved effort in `session/title-llm-request`, then dispatches through the same captured adapter generation under the shared deadline. `purpose: 'session-title'` supplies attribution only. The request has no agent-loop identity and does not enter conversation history. Generation failures preserve the request record.
|
||||
Each revision frames the selected messages and any current title as JSON and checks the complete input against `maxInputBytes`. The title configuration function selects the first effort from the route's least-to-greatest list during `ctx.llm.prepareCall()`. The helper records the exact input, output cap, and resolved effort in `session/title-llm-request`, then dispatches through the same captured adapter generation under the shared deadline. `purpose: 'session-title'` supplies attribution only. The request has no agent-loop identity and does not enter conversation history. Generation failures preserve the request record.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -98,7 +100,7 @@ Read these pages when the generation policy is not enough. They move from the se
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The title model receives a fixed system instruction to return one concise unadorned title in the input language, including the configured word and CJK-character targets and a short title instead of an explanation when the messages give little to name. Its one user message contains a JSON array of the exact selected human messages and their seqs.
|
||||
The title model receives a system instruction to return one concise unadorned title in the input language, including the configured word and CJK-character targets and a short title instead of an explanation when the messages give little to name. Its one user message contains a JSON array of the exact selected human messages and their seqs. When a current provider-generated title is supplied, the user message instead contains a JSON object with `currentTitle` and `messages`. The instruction asks the model to return an adequate current title exactly unchanged, taking priority over the target length, and to change it only when a material change or expansion of the main topic or task makes it inaccurate.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -106,7 +108,7 @@ The auxiliary request consumes tokens according to selected input size and `maxO
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No main-request invalidation. Auxiliary cache reuse is provider-specific; the fixed instruction is reusable while the JSON message array changes with each revision.
|
||||
No main-request invalidation. Auxiliary cache reuse is provider-specific; the instruction is reusable while the JSON input changes with each revision.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ kind: "package-library"
|
||||
|
||||
提供方插件调用 `registerSessionTitleLlmProvider(ctx, config, id, automatic, selectMessages)`;辅助函数验证共享配置、在 `ctx.sessionTitle` 上注册提供方,并让每次生成都经过共享策略。两个随附插件以各自的 `first-prompt` 与 `all-prompts` 节奏和消息选择器注册;服务上的第二次注册会立即抛出。
|
||||
|
||||
对于 `all-prompts`,如果当前标题由提供方生成,辅助函数还会传入该标题,包括从首消息提供方继承的标题。此输入不包含回退标题或用户指定的标题。首消息节奏只使用其选中的消息。
|
||||
|
||||
### 路由与失败约定
|
||||
|
||||
`provider` 与 `model` 覆盖项都是可选的,但必须同时作为非空字符串提供。如果没有这一对取值,辅助函数使用当前会话已记录 `request/header` 中捕获的确切提供方/模型路由,因此在任何路由出现前显式刷新时必须提供覆盖项。辅助函数在记录或分发前,依据 `maxInputBytes` 检查最终 JSON 封装用户提示词的大小,而不是将其截断,并在消费流期间与完成后重新检查超时与调用方取消,因此即使拦截器或适配器忽略 abort,也不能接受迟到的成功结果。格式错误或空输出、工具调用与非 stop 结束原因都会拒绝;会话标题服务决定该拒绝属于自动警告还是显式调用方失败。被接受的标题取模型文本输出的第一个非空行;当整行被一对强调标记包裹时去掉这对标记,因此模型写在标题之后的说明不会成为标题。标题请求独立使用 `maxOutputTokens`,不继承对话请求的上限。其路由必须注册适配器,以便在记录前完成请求准备。
|
||||
@@ -72,7 +74,7 @@ kind: "package-library"
|
||||
|
||||
### 请求流程
|
||||
|
||||
每次修订把选中的消息封装为 JSON,并检查 `maxInputBytes`。标题配置函数在 `ctx.llm.prepareCall()` 期间,从路由按强度递增排列的列表中选择首个强度。辅助函数把确切输入、输出上限与解析后的强度记录到 `session/title-llm-request`,然后在共享截止时间内通过同一捕获的适配器代次分发。`purpose: 'session-title'` 仅提供归属标注。请求不带 agent loop 身份,也不进入对话历史。生成失败仍保留请求记录。
|
||||
每次修订把选中的消息及传入的当前标题封装为 JSON,并依据 `maxInputBytes` 检查完整输入。标题配置函数在 `ctx.llm.prepareCall()` 期间,从路由按强度递增排列的列表中选择首个强度。辅助函数把确切输入、输出上限与解析后的强度记录到 `session/title-llm-request`,然后在共享截止时间内通过同一捕获的适配器代次分发。`purpose: 'session-title'` 仅提供归属标注。请求不带 agent loop 身份,也不进入对话历史。生成失败仍保留请求记录。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -98,7 +100,7 @@ kind: "package-library"
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
标题模型会收到固定系统指令,要求以输入语言返回一个简洁且无装饰的标题;该指令包含所配置的词数与 CJK 字符数目标,并在消息内容很少时要求给出简短标题而不是解释。它唯一的用户消息包含一个 JSON 数组,其中是精确选中的用户消息及其 seq。
|
||||
标题模型会收到系统指令,要求以输入语言返回一个简洁且无装饰的标题;该指令包含所配置的词数与 CJK 字符数目标,并在消息内容很少时要求给出简短标题而不是解释。它唯一的用户消息包含一个 JSON 数组,其中是精确选中的用户消息及其 seq。传入当前提供方生成的标题时,用户消息改为包含 `currentTitle` 与 `messages` 的 JSON 对象。指令要求模型逐字保留仍然合适的当前标题,优先于目标长度要求;只有主要主题或任务发生实质变化或扩展、使旧标题不再准确时才改名。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -106,7 +108,7 @@ kind: "package-library"
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会使主请求的 KV Cache 失效。辅助缓存复用由提供方决定;固定指令可复用,而 JSON 消息数组会随每次修订变化。
|
||||
不会使主请求的 KV Cache 失效。辅助缓存复用由提供方决定;指令可复用,而 JSON 输入会随每次修订变化。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -174,7 +174,15 @@ export function registerSessionTitleLlmProvider(
|
||||
id: titleProvider,
|
||||
automatic,
|
||||
async generate(request) {
|
||||
return generateSessionTitleWithLlm(ctx, resolved, request, selectMessages(request.messages), titleProvider)
|
||||
const current = automatic === 'all-prompts' ? ctx.sessionTitle.get(request.session) : undefined
|
||||
return generateSessionTitleWithLlm(
|
||||
ctx,
|
||||
resolved,
|
||||
request,
|
||||
selectMessages(request.messages),
|
||||
titleProvider,
|
||||
current?.source.kind === 'provider' ? current.title : undefined,
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -194,13 +202,19 @@ function resolveRoute(
|
||||
}
|
||||
|
||||
/** Stable language-aware system instruction shared by both provider plugins. */
|
||||
function systemPrompt(config: ResolvedSessionTitleLlmConfig): string {
|
||||
function systemPrompt(config: ResolvedSessionTitleLlmConfig, hasCurrentTitle: boolean): string {
|
||||
return [
|
||||
'Create a concise title for an AI coding-assistant session from the supplied human messages.',
|
||||
'Return only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.',
|
||||
'Use the language of the messages.',
|
||||
`Aim for about ${config.targetWords} words in non-CJK languages or ${config.targetCjkCharacters} CJK characters.`,
|
||||
'If the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.',
|
||||
...hasCurrentTitle ? [
|
||||
'An existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.',
|
||||
'Follow-up questions, additional details within the same topic, acknowledgements such as "thanks", and requests to continue do not by themselves justify a title change.',
|
||||
'Do not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.',
|
||||
'Change the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate.',
|
||||
] : [],
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -224,7 +238,10 @@ function titleFromOutput(text: string): string {
|
||||
}
|
||||
|
||||
/** Frame exact messages as JSON so user text cannot break structural delimiters. */
|
||||
function frameMessages(messages: readonly SessionTitleUserMessage[]): string {
|
||||
function frameMessages(messages: readonly SessionTitleUserMessage[], currentTitle?: string): string {
|
||||
if (currentTitle !== undefined) {
|
||||
return `Update the session title from this JSON object:\n${JSON.stringify({ currentTitle, messages })}`
|
||||
}
|
||||
return `Generate the session title from this JSON array of human messages:\n${JSON.stringify(messages)}`
|
||||
}
|
||||
|
||||
@@ -255,6 +272,7 @@ function finishError(finish: FinishReason): Error | undefined {
|
||||
* @param request - service-owned session, route, message snapshot, and cancellation.
|
||||
* @param selectedMessages - exact provider-selected subset to frame and attribute.
|
||||
* @param titleProvider - registered title-provider identity recorded with the request.
|
||||
* @param currentTitle - existing model-generated title to preserve while it still describes the main topic.
|
||||
* @returns normalized non-empty title, exact source seqs, and used model route.
|
||||
*/
|
||||
export async function generateSessionTitleWithLlm(
|
||||
@@ -263,12 +281,13 @@ export async function generateSessionTitleWithLlm(
|
||||
request: SessionTitleProviderRequest,
|
||||
selectedMessages: readonly SessionTitleUserMessage[],
|
||||
titleProvider: SessionTitleProviderId,
|
||||
currentTitle?: string,
|
||||
): Promise<SessionTitleProviderResult> {
|
||||
request.signal.throwIfAborted()
|
||||
if (selectedMessages.length === 0) {
|
||||
throw new Error('session-title-llm: at least one source message is required')
|
||||
}
|
||||
const framedInput = frameMessages(selectedMessages)
|
||||
const framedInput = frameMessages(selectedMessages, currentTitle)
|
||||
const inputBytes = Buffer.byteLength(framedInput, 'utf8')
|
||||
if (inputBytes > config.maxInputBytes) {
|
||||
throw new Error(`session-title-llm: input is ${inputBytes} bytes, exceeding maxInputBytes ${config.maxInputBytes}`)
|
||||
@@ -278,7 +297,7 @@ export async function generateSessionTitleWithLlm(
|
||||
content: [{ type: 'text', text: framedInput }],
|
||||
source: { kind: 'dsh-session-title-llm' },
|
||||
})]
|
||||
const system = systemPrompt(config)
|
||||
const system = systemPrompt(config, currentTitle !== undefined)
|
||||
using callDeadline = deadline(request.signal, config.timeoutMs, SESSION_TITLE_TIMEOUT_CODE)
|
||||
const maxTokens = config.maxOutputTokens
|
||||
const call = await ctx.llm.prepareCall({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import LlmRuntime, { createUserMessage, ToolCallId, isAgentLoopRequest, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import type { FinishReason, GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -221,6 +221,69 @@ describe('generateSessionTitleWithLlm', () => {
|
||||
.findLast(event => event.type === 'session/title-llm-request')?.data.maxTokens).toBe(32)
|
||||
})
|
||||
|
||||
it('frames the existing title as data and logs the exact stability instructions', async () => {
|
||||
const { ctx, adapter } = await withScript(SCRIPT)
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const providerRequest = request(ctx)
|
||||
const currentTitle = '阳台 "番茄" 与罗勒'
|
||||
|
||||
await generateSessionTitleWithLlm(
|
||||
ctx,
|
||||
resolveSessionTitleLlmConfig(CONFIG),
|
||||
providerRequest,
|
||||
providerRequest.messages,
|
||||
TITLE_PROVIDER,
|
||||
currentTitle,
|
||||
)
|
||||
|
||||
const options = adapter.requests[0]!
|
||||
const content = options.messages[0]!.content[0]!
|
||||
if (content.type !== 'text') throw new Error('expected title input text')
|
||||
expect(JSON.parse(content.text.slice(content.text.indexOf('\n') + 1))).toEqual({
|
||||
currentTitle,
|
||||
messages: providerRequest.messages,
|
||||
})
|
||||
expect(options.system).toContain('return it exactly unchanged')
|
||||
expect(options.system).toContain('acknowledgements such as "thanks"')
|
||||
expect(options.system).toContain('materially change or expand the main topic or task')
|
||||
expect(providerRequest.session.snapshotEvents()
|
||||
.findLast(event => event.type === 'session/title-llm-request')?.data).toMatchObject({
|
||||
system: options.system,
|
||||
messages: options.messages,
|
||||
})
|
||||
})
|
||||
|
||||
it('includes the existing title in the final input byte limit', async () => {
|
||||
const { ctx, adapter } = await withScript(SCRIPT)
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const providerRequest = request(ctx)
|
||||
const currentTitle = '阳台种植建议'
|
||||
const inputBytes = Buffer.byteLength(
|
||||
`Update the session title from this JSON object:\n${JSON.stringify({ currentTitle, messages: providerRequest.messages })}`,
|
||||
'utf8',
|
||||
)
|
||||
await expect(generateSessionTitleWithLlm(
|
||||
ctx,
|
||||
resolveSessionTitleLlmConfig({ ...CONFIG, maxInputBytes: inputBytes - 1 }),
|
||||
providerRequest,
|
||||
providerRequest.messages,
|
||||
TITLE_PROVIDER,
|
||||
currentTitle,
|
||||
)).rejects.toThrow(/input.*bytes.*maxInputBytes/i)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(providerRequest.session.snapshotEvents().some(event => event.type === 'session/title-llm-request')).toBe(false)
|
||||
|
||||
await generateSessionTitleWithLlm(
|
||||
ctx,
|
||||
resolveSessionTitleLlmConfig({ ...CONFIG, maxInputBytes: inputBytes }),
|
||||
providerRequest,
|
||||
providerRequest.messages,
|
||||
TITLE_PROVIDER,
|
||||
currentTitle,
|
||||
)
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps the configured output cap when the session request recorded a larger one', async () => {
|
||||
const { ctx, adapter } = await withScript(SCRIPT)
|
||||
const providerRequest = request(ctx, undefined, 4_096)
|
||||
|
||||
@@ -102,6 +102,8 @@ function dirOf(url: string): string {
|
||||
interface SdkAssertions {
|
||||
/** Additional profile patches applied after the shared composition. */
|
||||
patches?: readonly string[]
|
||||
/** Wait for the recorded provider title before sending each subsequent prompt. */
|
||||
waitForProviderTitle?: boolean
|
||||
/** Final response required from a completed turn before updating goldens. */
|
||||
expectedFinalResponse?: string
|
||||
/** Environment overrides passed to the runtime subprocess. */
|
||||
@@ -124,6 +126,7 @@ interface SdkAssertions {
|
||||
}
|
||||
|
||||
const SDK_ASSERTIONS: Readonly<Record<string, SdkAssertions>> = {
|
||||
'session-title-stability': { waitForProviderTitle: true },
|
||||
'dynamic-tool-updates': {
|
||||
expectedFinalResponse: 'DONE',
|
||||
},
|
||||
@@ -634,6 +637,11 @@ async function runScenario(scenario: CorpusScenario): Promise<{
|
||||
const liveSessions: (string | undefined)[] = [sessionId]
|
||||
await harness.start()
|
||||
const subscription = harness.client.subscribeSessionTree(sessionId)
|
||||
const titleSubscription = assertions.waitForProviderTitle === true
|
||||
? harness.client.subscribeSessionTree(sessionId)
|
||||
: undefined
|
||||
const expectedTitles = records(primaryFixture).filter(event => event.type === 'session/title'
|
||||
&& ((event.data as JsonObject).source as JsonObject).kind === 'provider')
|
||||
const completionSubscription = scenario.name === 'subagent-continuable'
|
||||
? harness.client.subscribeSessionTree(sessionId)
|
||||
: undefined
|
||||
@@ -663,6 +671,13 @@ async function runScenario(scenario: CorpusScenario): Promise<{
|
||||
},
|
||||
})
|
||||
results.push(result)
|
||||
if (titleSubscription !== undefined) {
|
||||
const expected = expectedTitles[results.length - 1]?.data as JsonObject | undefined
|
||||
expect(expected, 'each turn has a recorded provider title').toBeDefined()
|
||||
await waitForRootEvent(titleSubscription, sessionId, event => event.type === 'session/title'
|
||||
&& ((event.data as JsonObject).source as JsonObject).kind === 'provider'
|
||||
&& (event.data as JsonObject).title === expected?.title, observe)
|
||||
}
|
||||
if (completionSubscription !== undefined) {
|
||||
expect(result.finalResponse, 'run returns at the first parent idle').toBe('DONE')
|
||||
expect(result.events.filter(event => event.type === 'turn/end'))
|
||||
@@ -711,6 +726,7 @@ async function runScenario(scenario: CorpusScenario): Promise<{
|
||||
} finally {
|
||||
subscription.close()
|
||||
completionSubscription?.close()
|
||||
titleSubscription?.close()
|
||||
}
|
||||
await harness.close()
|
||||
const logs = (await Promise.all([
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Main responses derive from the session fixture; auxiliary title responses use
|
||||
# the explicit replay script. The SDK waits for each accepted title before sending
|
||||
# the next human message.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
disabled: true
|
||||
|
||||
- id: agent-default-model
|
||||
name: '@deepseek-ai/dsh-agent-default-model'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
|
||||
- id: session-persistence-jsonl
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js dshHomePath('sessions')
|
||||
compression: none
|
||||
|
||||
- id: agent-instructions
|
||||
name: '@deepseek-ai/dsh-agent-instructions'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
- id: system-prompt
|
||||
name: '@deepseek-ai/dsh-system-prompt'
|
||||
config:
|
||||
personaPrefix: |
|
||||
You are a coding assistant powered by the {{model}} model. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
|
||||
|
||||
Verify your work by running the code or tests. Keep answers brief and factual.
|
||||
|
||||
- insert:
|
||||
- id: llm-replay-main
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
overrideFile: ./.missing-main-replay-override.json
|
||||
providers:
|
||||
- id: deepseek-official
|
||||
name: DeepSeek
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
- id: llm-replay-title
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
providers:
|
||||
- id: title-replay
|
||||
name: Title replay
|
||||
models:
|
||||
- id: title-model
|
||||
reasoningEfforts: [low, high]
|
||||
defaultReasoningEffort: high
|
||||
- id: session-title-provider
|
||||
name: '@deepseek-ai/dsh-session-title-all-prompts-llm'
|
||||
config:
|
||||
targetWords: 5
|
||||
targetCjkCharacters: 10
|
||||
maxInputBytes: 4096
|
||||
maxOutputTokens: 32
|
||||
timeoutMs: 5000
|
||||
provider: title-replay
|
||||
model: title-model
|
||||
@@ -0,0 +1,12 @@
|
||||
# Continuous session-title composition with the shipped all-prompts provider.
|
||||
- insert:
|
||||
- id: session-title-provider
|
||||
name: '@deepseek-ai/dsh-session-title-all-prompts-llm'
|
||||
config:
|
||||
targetWords: 5
|
||||
targetCjkCharacters: 10
|
||||
maxInputBytes: 4096
|
||||
maxOutputTokens: 32
|
||||
timeoutMs: 5000
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
@@ -0,0 +1,146 @@
|
||||
[
|
||||
{
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{
|
||||
"type": "block-start",
|
||||
"index": 0,
|
||||
"blockType": "text"
|
||||
},
|
||||
{
|
||||
"type": "text-delta",
|
||||
"index": 0,
|
||||
"text": "阳台种番茄与罗勒"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
"index": 0,
|
||||
"block": {
|
||||
"type": "text",
|
||||
"text": "阳台种番茄与罗勒"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "usage",
|
||||
"usage": {
|
||||
"inputTokens": 12,
|
||||
"outputTokens": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{
|
||||
"type": "block-start",
|
||||
"index": 0,
|
||||
"blockType": "text"
|
||||
},
|
||||
{
|
||||
"type": "text-delta",
|
||||
"index": 0,
|
||||
"text": "阳台种番茄与罗勒"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
"index": 0,
|
||||
"block": {
|
||||
"type": "text",
|
||||
"text": "阳台种番茄与罗勒"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "usage",
|
||||
"usage": {
|
||||
"inputTokens": 12,
|
||||
"outputTokens": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{
|
||||
"type": "block-start",
|
||||
"index": 0,
|
||||
"blockType": "text"
|
||||
},
|
||||
{
|
||||
"type": "text-delta",
|
||||
"index": 0,
|
||||
"text": "阳台种番茄与罗勒"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
"index": 0,
|
||||
"block": {
|
||||
"type": "text",
|
||||
"text": "阳台种番茄与罗勒"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "usage",
|
||||
"usage": {
|
||||
"inputTokens": 12,
|
||||
"outputTokens": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{
|
||||
"type": "block-start",
|
||||
"index": 0,
|
||||
"blockType": "text"
|
||||
},
|
||||
{
|
||||
"type": "text-delta",
|
||||
"index": 0,
|
||||
"text": "制作网页待办清单"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
"index": 0,
|
||||
"block": {
|
||||
"type": "text",
|
||||
"text": "制作网页待办清单"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "usage",
|
||||
"usage": {
|
||||
"inputTokens": 12,
|
||||
"outputTokens": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
{"type":"session","version":4,"id":"{{session:1}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"我想在阳台种番茄和罗勒,请给入门建议。"}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"我想在阳台种番茄和罗勒,请给入门建议。"}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"我想在阳台种番茄和罗勒,请","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"session/title-llm-request","data":{"titleProvider":"session-title-all-prompts-llm","messageSeqs":[8],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"我想在阳台种番茄和罗勒,请给入门建议。\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:4}}"}],"maxTokens":32,"reasoningEffort":"low"}}
|
||||
{"type":"session/title","data":{"title":"阳台种番茄与罗勒","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-all-prompts-llm","model":{"provider":"title-replay","model":"title-model"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ACKNOWLEDGED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1791466311688,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791466311688,"index":0,"dt":[],"texts":["ACKNOWLEDGED"]},{"type":"chunk","time":1791466311688,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ACKNOWLEDGED"}}},{"type":"chunk","time":1791466311688,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1791466311688,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"花盆要多大?"}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":2}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"花盆要多大?"}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title-llm-request","data":{"titleProvider":"session-title-all-prompts-llm","messageSeqs":[8,22],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.\nAn existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.\nFollow-up questions, additional details within the same topic, acknowledgements such as \"thanks\", and requests to continue do not by themselves justify a title change.\nDo not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.\nChange the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate.","messages":[{"content":[{"type":"text","text":"Update the session title from this JSON object:\n{\"currentTitle\":\"阳台种番茄与罗勒\",\"messages\":[{\"seq\":8,\"text\":\"我想在阳台种番茄和罗勒,请给入门建议。\"},{\"seq\":22,\"text\":\"花盆要多大?\"}]}"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:7}}"}],"maxTokens":32,"reasoningEffort":"low"}}
|
||||
{"type":"session/title","data":{"title":"阳台种番茄与罗勒","messageSeqs":[8,22],"source":{"kind":"provider","provider":"session-title-all-prompts-llm","model":{"provider":"title-replay","model":"title-model"}}}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ACKNOWLEDGED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1791466311714,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791466311714,"index":0,"dt":[],"texts":["ACKNOWLEDGED"]},{"type":"chunk","time":1791466311714,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ACKNOWLEDGED"}}},{"type":"chunk","time":1791466311714,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1791466311714,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"谢谢"}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":3}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":3,"step":1}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"谢谢"}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title-llm-request","data":{"titleProvider":"session-title-all-prompts-llm","messageSeqs":[8,22,32],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.\nAn existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.\nFollow-up questions, additional details within the same topic, acknowledgements such as \"thanks\", and requests to continue do not by themselves justify a title change.\nDo not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.\nChange the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate.","messages":[{"content":[{"type":"text","text":"Update the session title from this JSON object:\n{\"currentTitle\":\"阳台种番茄与罗勒\",\"messages\":[{\"seq\":8,\"text\":\"我想在阳台种番茄和罗勒,请给入门建议。\"},{\"seq\":22,\"text\":\"花盆要多大?\"},{\"seq\":32,\"text\":\"谢谢\"}]}"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:10}}"}],"maxTokens":32,"reasoningEffort":"low"}}
|
||||
{"type":"session/title","data":{"title":"阳台种番茄与罗勒","messageSeqs":[8,22,32],"source":{"kind":"provider","provider":"session-title-all-prompts-llm","model":{"provider":"title-replay","model":"title-model"}}}}
|
||||
{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ACKNOWLEDGED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1791466311739,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791466311739,"index":0,"dt":[],"texts":["ACKNOWLEDGED"]},{"type":"chunk","time":1791466311739,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ACKNOWLEDGED"}}},{"type":"chunk","time":1791466311739,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1791466311739,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":3,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"换个话题:帮我制作一个网页待办清单。"}],"source":{"kind":"user"},"role":"user","id":"{{message:12}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":4}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":4,"step":1}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"换个话题:帮我制作一个网页待办清单。"}],"source":{"kind":"user"},"role":"user","id":"{{message:12}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title-llm-request","data":{"titleProvider":"session-title-all-prompts-llm","messageSeqs":[8,22,32,42],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.\nAn existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.\nFollow-up questions, additional details within the same topic, acknowledgements such as \"thanks\", and requests to continue do not by themselves justify a title change.\nDo not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.\nChange the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate.","messages":[{"content":[{"type":"text","text":"Update the session title from this JSON object:\n{\"currentTitle\":\"阳台种番茄与罗勒\",\"messages\":[{\"seq\":8,\"text\":\"我想在阳台种番茄和罗勒,请给入门建议。\"},{\"seq\":22,\"text\":\"花盆要多大?\"},{\"seq\":32,\"text\":\"谢谢\"},{\"seq\":42,\"text\":\"换个话题:帮我制作一个网页待办清单。\"}]}"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:13}}"}],"maxTokens":32,"reasoningEffort":"low"}}
|
||||
{"type":"session/title","data":{"title":"制作网页待办清单","messageSeqs":[8,22,32,42],"source":{"kind":"provider","provider":"session-title-all-prompts-llm","model":{"provider":"title-replay","model":"title-model"}}}}
|
||||
{"type":"assistant/message","data":{"turn":4,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ACKNOWLEDGED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:14}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1791466311763,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791466311763,"index":0,"dt":[],"texts":["ACKNOWLEDGED"]},{"type":"chunk","time":1791466311763,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ACKNOWLEDGED"}}},{"type":"chunk","time":1791466311763,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1791466311763,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":4,"step":1}}
|
||||
{"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1,12 @@
|
||||
version: 1
|
||||
scenario: session-title-stability
|
||||
profile: sdk
|
||||
composition: session-title-stability
|
||||
recording: authored
|
||||
header:
|
||||
class: session-title-stability
|
||||
pin: true
|
||||
systemPromptSource: session/text-turn
|
||||
toolSchemasSource: session/text-turn
|
||||
replay:
|
||||
override: true
|
||||
Reference in new issue
Block a user