Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,13 @@ deepchatへの貢献をご検討いただきありがとうございます!貢

## 🙏🏻 謝辞

このプロジェクトは、以下の素晴らしいライブラリの支援により構築されています
このプロジェクトは、以下の素晴らしいライブラリとプロジェクトの支援により構築されています

- [Vue](https://vuejs.org/)
- [Electron](https://www.electronjs.org/)
- [Electron-Vite](https://electron-vite.org/)
- [oxlint](https://github.com/oxc-project/oxc)
- [Bub](https://github.com/bubbuild/bub)。その tape model は DeepChat の session tape 設計に着想を与えました。基盤となる tape アーキテクチャに関心がある方は [tape.systems](https://tape.systems/) をご覧ください。

## 📃 ライセンス

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,13 @@ Thank you for considering contributing to deepchat! The contribution guide can b

## 🙏🏻 Thanks

This project is built with the help of these awesome libraries:
This project is built with the help of these awesome libraries and projects:

- [Vue](https://vuejs.org/)
- [Electron](https://www.electronjs.org/)
- [Electron-Vite](https://electron-vite.org/)
- [oxlint](https://github.com/oxc-project/oxc)
- [Bub](https://github.com/bubbuild/bub), whose tape model inspired DeepChat's session tape design. For the underlying tape architecture, visit [tape.systems](https://tape.systems/).

## 📃 License

Expand Down
3 changes: 2 additions & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,13 @@ DeepChat是一个活跃的开源社区项目,我们欢迎各种形式的贡献

## 🙏🏻 致谢

本项目的构建得益于这些优秀的开源库
本项目的构建得益于这些优秀的开源库和项目

- [Vue](https://vuejs.org/)
- [Electron](https://www.electronjs.org/)
- [Electron-Vite](https://electron-vite.org/)
- [oxlint](https://github.com/oxc-project/oxc)
- [Bub](https://github.com/bubbuild/bub),其 tape model 启发了 DeepChat 的 session tape 设计。如果你对底层 tape 架构感兴趣,推荐访问 [tape.systems](https://tape.systems/)。

## 📃 许可证

Expand Down
148 changes: 132 additions & 16 deletions src/main/presenter/agentRuntimePresenter/compactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import type {
import type { ChatMessage } from '@shared/types/core/chat-message'
import type { IConfigPresenter, ILlmProviderPresenter } from '@shared/presenter'
import type { DeepChatMessageStore } from './messageStore'
import type { DeepChatSessionStore, SessionSummaryState } from './sessionStore'
import type {
DeepChatSessionStore,
ReconstructionAnchorPromptState,
SessionSummaryState
} from './sessionStore'
import {
buildHistoryTurns,
buildUserMessageContent,
Expand Down Expand Up @@ -56,6 +60,13 @@ export type CompactionIntent = {
summaryBlocks: string[]
currentModel: ModelSpec
reserveTokens: number
anchorName?: string
summaryRange?: {
fromOrderSeq: number
toOrderSeq: number
} | null
sourceMessageIds?: string[]
summaryableTurnCount?: number
}

export type CompactionExecutionResult = {
Expand Down Expand Up @@ -109,6 +120,71 @@ export function appendSummarySection(
return composeSections([systemPrompt, summarySection])
}

function shouldExposeReconstructionAnchorState(anchorName: string): boolean {
return anchorName.startsWith('handoff/') || anchorName.startsWith('auto_handoff/')
}

function readPromptVisibleText(value: unknown): string | null {
if (typeof value !== 'string') {
return null
}

const trimmed = value.trim()
return trimmed || null
}

function visibleReconstructionState(
anchorName: string,
state: Record<string, unknown>
): Record<string, unknown> {
const result: Record<string, unknown> = {}

if (anchorName.startsWith('handoff/')) {
const summary = readPromptVisibleText(state.summary)
if (summary) {
result.summary = summary
}
return result
}

if (anchorName.startsWith('auto_handoff/')) {
const reason = readPromptVisibleText(state.reason)
if (reason) {
result.reason = reason
}
}

return result
}

export function appendReconstructionAnchorStateSection(
systemPrompt: string,
anchor: ReconstructionAnchorPromptState | null | undefined
): string {
if (!anchor || !shouldExposeReconstructionAnchorState(anchor.name)) {
return systemPrompt
}

const visibleState = visibleReconstructionState(anchor.name, anchor.state)
if (Object.keys(visibleState).length === 0) {
return systemPrompt
}

const stateJson = JSON.stringify(
{
anchor: anchor.name,
state: visibleState
},
null,
2
)
const anchorSection = composeSections([
'## Tape Handoff State',
buildUntrustedPromptBlock('Persisted tape handoff state', stateJson)
])
return composeSections([systemPrompt, anchorSection])
}

function parseAssistantBlocks(record: ChatMessageRecord): AssistantMessageBlock[] {
if (record.role !== 'assistant') {
return []
Expand Down Expand Up @@ -255,6 +331,7 @@ export class CompactionService {
preserveInterleavedReasoning: boolean
preserveEmptyInterleavedReasoning?: boolean
newUserContent: string | SendMessageInput
historyRecords?: ChatMessageRecord[]
signal?: AbortSignal
}): Promise<CompactionIntent | null> {
throwIfAbortRequested(params.signal)
Expand All @@ -264,8 +341,9 @@ export class CompactionService {
return null
}

const historyRecords = this.messageStore
.getMessages(params.sessionId)
const historyRecords = (
params.historyRecords ?? this.messageStore.getMessages(params.sessionId)
)
.filter(isContextHistoryRecord)
.sort((a, b) => a.orderSeq - b.orderSeq)

Expand All @@ -280,7 +358,8 @@ export class CompactionService {
params.supportsVision,
params.supportsAudioInput === true
)
]
],
anchorName: 'compaction/auto'
})
}

Expand All @@ -297,6 +376,7 @@ export class CompactionService {
supportsAudioInput?: boolean
preserveInterleavedReasoning: boolean
preserveEmptyInterleavedReasoning?: boolean
historyRecords?: ChatMessageRecord[]
signal?: AbortSignal
}): Promise<CompactionIntent | null> {
throwIfAbortRequested(params.signal)
Expand All @@ -306,8 +386,7 @@ export class CompactionService {
return null
}

const allMessages = this.messageStore
.getMessages(params.sessionId)
const allMessages = (params.historyRecords ?? this.messageStore.getMessages(params.sessionId))
.filter((record) => !isCompactionRecord(record))
.sort((a, b) => a.orderSeq - b.orderSeq)
const target = allMessages.find((record) => record.id === params.messageId)
Expand All @@ -330,7 +409,8 @@ export class CompactionService {
records: resumeRecords,
protectedTurnCount: settings.retainRecentPairs + 1,
triggerThreshold: settings.triggerThreshold,
projectedMessages: []
projectedMessages: [],
anchorName: 'compaction/resume'
})
}

Expand All @@ -347,6 +427,7 @@ export class CompactionService {
preserveInterleavedReasoning: boolean
preserveEmptyInterleavedReasoning?: boolean
projectedMessages: ChatMessage[]
historyRecords?: ChatMessageRecord[]
signal?: AbortSignal
}): Promise<CompactionIntent | null> {
throwIfAbortRequested(params.signal)
Expand All @@ -356,8 +437,9 @@ export class CompactionService {
return null
}

const historyRecords = this.messageStore
.getMessages(params.sessionId)
const historyRecords = (
params.historyRecords ?? this.messageStore.getMessages(params.sessionId)
)
.filter(isContextHistoryRecord)
.sort((a, b) => a.orderSeq - b.orderSeq)

Expand All @@ -367,7 +449,8 @@ export class CompactionService {
protectedTurnCount: settings.retainRecentPairs,
triggerThreshold: settings.triggerThreshold,
projectedMessages: params.projectedMessages,
force: true
force: true,
anchorName: 'auto_handoff/context_overflow'
})
}

Expand All @@ -383,12 +466,14 @@ export class CompactionService {
supportsAudioInput?: boolean
preserveInterleavedReasoning: boolean
preserveEmptyInterleavedReasoning?: boolean
historyRecords?: ChatMessageRecord[]
signal?: AbortSignal
}): Promise<CompactionIntent | null> {
throwIfAbortRequested(params.signal)

const historyRecords = this.messageStore
.getMessages(params.sessionId)
const historyRecords = (
params.historyRecords ?? this.messageStore.getMessages(params.sessionId)
)
.filter(isContextHistoryRecord)
.sort((a, b) => a.orderSeq - b.orderSeq)

Expand All @@ -398,7 +483,8 @@ export class CompactionService {
protectedTurnCount: 0,
triggerThreshold: 0,
projectedMessages: [],
force: true
force: true,
anchorName: 'compaction/manual'
})
}

Expand All @@ -416,17 +502,34 @@ export class CompactionService {
reserveTokens: intent.reserveTokens,
signal
})
const summaryUpdatedAt = Date.now()

const updatedState: SessionSummaryState = {
summaryText: nextSummary,
summaryCursorOrderSeq: Math.max(1, intent.targetCursorOrderSeq),
summaryUpdatedAt: Date.now()
summaryUpdatedAt
}

const compareAndSet = this.sessionStore.compareAndSetSummaryState(
intent.sessionId,
intent.previousState,
updatedState
updatedState,
{
name: intent.anchorName ?? 'compaction/auto',
state: {
summary: nextSummary,
cursorOrderSeq: updatedState.summaryCursorOrderSeq,
range: intent.summaryRange ?? null,
sourceMessageIds: intent.sourceMessageIds ?? [],
summaryableTurnCount: intent.summaryableTurnCount ?? intent.summaryBlocks.length,
previousSummaryUpdatedAt: intent.previousState.summaryUpdatedAt
},
meta: {
providerId: intent.currentModel.providerId,
modelId: intent.currentModel.modelId,
reserveTokens: intent.reserveTokens
}
}
)
if (compareAndSet.applied) {
return {
Expand Down Expand Up @@ -469,6 +572,7 @@ export class CompactionService {
triggerThreshold: number
projectedMessages: ChatMessage[]
force?: boolean
anchorName?: string
}): CompactionIntent | null {
const summaryState = this.sessionStore.getSummaryState(params.sessionId)
const scopedRecords = params.records.filter(
Expand Down Expand Up @@ -521,6 +625,14 @@ export class CompactionService {
const summaryBlocks = summaryableTurns.map((turn) =>
turn.records.map((record) => serializeRecord(record)).join('\n\n')
)
const summaryableRecords = summaryableTurns.flatMap((turn) => turn.records)
const summaryRange =
summaryableRecords.length > 0
? {
fromOrderSeq: summaryableRecords[0].orderSeq,
toOrderSeq: summaryableRecords[summaryableRecords.length - 1].orderSeq
}
: null

const nextCursor =
rawTailTurns[0]?.records[0]?.orderSeq ??
Expand All @@ -536,7 +648,11 @@ export class CompactionService {
params.modelId,
params.contextLength
),
reserveTokens: params.reserveTokens
reserveTokens: params.reserveTokens,
anchorName: params.anchorName ?? 'compaction/auto',
summaryRange,
sourceMessageIds: summaryableRecords.map((record) => record.id),
summaryableTurnCount: summaryableTurns.length
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/presenter/agentRuntimePresenter/contextBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,7 @@ export function buildResumeContext(
options: ContextBuildOptions = {}
): ChatMessage[] {
const supportsAudioInput = options.supportsAudioInput === true
const allMessages = messageStore.getMessages(sessionId)
const allMessages = options.historyRecords ?? messageStore.getMessages(sessionId)
const targetMessage = allMessages.find((message) => message.id === assistantMessageId)
const targetOrderSeq = targetMessage?.orderSeq
const cursor = Math.max(1, options.summaryCursorOrderSeq ?? 1)
Expand Down
Loading