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
7 changes: 5 additions & 2 deletions packages/dmoss-agent/src/context/compaction-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const SUMMARIZATION_SYSTEM_PROMPT = `你是上下文摘要助手。你的
- 用户明确批准或拒绝的结论(例如同意改某路径、否决某方案)必须点名写出对象(路径/命令/工具名),不得只写「已确认」。
- 仍待用户拍板或工具链在等待的事项,单独记在对应章节,避免后续模型误当成已完成。
- 保留精确的文件路径、函数名、命令行、HTTP/API 端点、错误栈/退出码、版本号、哈希、端口号、IP——后续模型靠这些字面量继续排查。
- 事实强度必须分级:用户明确确认、工具结果、官方文档/源码中的内容可写为“已验证”;assistant 旧回答、记忆、未带来源的产品定义/设备状态/版本/路径只能写为“历史说法/待核对”,不得升级为当前事实。
- 对产品介绍、设备状态、价格/渠道、版本、当前路径、执行结果这类易变信息,摘要应保留“需要用工具或官方来源复核”的要求,而不是把旧回答压成结论。
- 对话主要使用中文时,摘要主体用中文;标识符、路径、日志片段保持原文,不要翻译。
- 对于嵌入式 / 机器人开发场景,优先保留:设备 ID 或别名、型号、IP/主机、SSH 是否通、设备端 Agent 版本或安装与否、加速器/部署相关路径与状态、最近一次失败命令或 stdout/stderr 的关键行(可截断但保留头尾特征)。
- 若输入里已经包含此前的“上下文检查点摘要”或“CONTEXT CHECKPOINT”,必须抽取其中的历史主线(做过什么、关键决策和原因、结果、方向)写入新的“## 0. 历史脉络”中;每次压缩追加 3-5 句,禁止把早期压缩的关键决策静默丢弃。`;
Expand Down Expand Up @@ -52,7 +54,7 @@ export const SUMMARIZATION_PROMPT = `以上消息是一段对话,请生成结
[已列出但尚未动手的事项]

## 6. 设备与环境状态
[设备标识、IP/主机、型号、SSH/凭据是否就绪、设备端 Agent 服务状态、重要工作目录]
[设备标识、IP/主机、型号、SSH/凭据是否就绪、设备端 Agent 服务状态、重要工作目录;未由工具/用户明确确认的状态必须标「未验证/历史说法」]

## 7. 关键文件与路径
[读写过的配置文件、模型、脚本路径;与问题直接相关的绝对路径;写明“为什么读/改这个文件”和仍需依赖的符号/函数名]
Expand All @@ -65,6 +67,7 @@ export const SUMMARIZATION_PROMPT = `以上消息是一段对话,请生成结
</summary>

写作要求:宁可多一行具体名词,也不要用「之前讨论过」类模糊指代。工具调用只需保留对后续有意义的名称与参数片段,不要把整段 JSON 原文塞进摘要。
重要事实边界:摘要是后续模型的弱上下文检查点,不是事实数据库。assistant 历史回答中的产品定义、设备判断、版本/路径/命令成功与否,除非被工具结果、用户确认或官方来源支撑,否则必须标「待核对」,并在第 8 或第 9 节写出复核入口。

压缩质量红线(必须满足):
- 摘要必须能让后续模型不重新收集同一批上下文即可继续:至少保留“当前最小工作集”(文件路径 + 符号/函数 + 已验证结论)。
Expand All @@ -77,7 +80,7 @@ export const UPDATE_SUMMARIZATION_PROMPT = `以上消息是需要纳入已有摘
先在 <analysis> 中分析新对话带来了哪些变化,然后在 <summary> 中输出更新后的完整摘要。

请在保留已有摘要信息的前提下进行更新,使用相同的段落格式(每段无内容写「(无)」)。规则:
- 默认保留旧摘要中的约束与事实,除非新对话**明确**推翻;推翻时写「更新:…(旧:…)」,不要静默删除。
- 默认保留旧摘要中的用户约束与已验证事实,除非新对话**明确**推翻;旧摘要里没有来源支撑的“事实”只能作为「历史说法/待核对」保留,不能在更新时升级成当前事实。推翻时写「更新:…(旧:…)」,不要静默删除。
- 如果旧摘要中有“## 0. 历史脉络”或此前压缩摘要,必须保留其关键主线,并追加本次压缩前的新里程碑;每次追加 3-5 句即可,但不得删除早期关键决策。
- 追加新进展、新决策、新错误;用户新增的禁止/必须句并入第 2 节对应条目。
- 已完成的事项从「当前进行中」移到「已完成的工作」;新待办写入第 5 节。
Expand Down
41 changes: 41 additions & 0 deletions packages/dmoss-agent/src/context/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,23 @@ export type SummarizeFn = (params: {
system: string;
userPrompt: string;
maxTokens: number;
abortSignal?: AbortSignal;
}) => Promise<string>;

function throwIfCompactionAborted(signal?: AbortSignal): void {
if (!signal?.aborted) {
return;
}
if (signal.reason instanceof Error) {
throw signal.reason;
}
throw new Error(
signal.reason === undefined
? "compaction aborted"
: `compaction aborted: ${String(signal.reason)}`,
);
}

function normalizeParts(parts: number, messageCount: number): number {
if (!Number.isFinite(parts) || parts <= 1) {
return 1;
Expand Down Expand Up @@ -451,7 +466,9 @@ async function generateSummary(params: {
maxTokens: number;
customInstructions?: string;
previousSummary?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
throwIfCompactionAborted(params.abortSignal);
let basePrompt = params.previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
if (params.customInstructions) {
basePrompt = `${basePrompt}\n\nAdditional focus: ${params.customInstructions}`;
Expand All @@ -467,8 +484,10 @@ async function generateSummary(params: {
system: SUMMARIZATION_SYSTEM_PROMPT,
userPrompt: prompt,
maxTokens: params.maxTokens,
abortSignal: params.abortSignal,
});

throwIfCompactionAborted(params.abortSignal);
// 提取 <summary> 标签内容(如果 LLM 遵循了两段式输出格式)
return extractSummaryTag(raw);
}
Expand All @@ -480,6 +499,7 @@ async function summarizeChunks(params: {
maxChunkTokens: number;
customInstructions?: string;
previousSummary?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
if (params.messages.length === 0) {
return params.previousSummary ?? DEFAULT_SUMMARY_FALLBACK;
Expand All @@ -493,6 +513,7 @@ async function summarizeChunks(params: {
maxTokens: params.maxTokens,
customInstructions: params.customInstructions,
previousSummary: summary,
abortSignal: params.abortSignal,
});
}
return summary ?? DEFAULT_SUMMARY_FALLBACK;
Expand All @@ -506,6 +527,7 @@ async function summarizeWithFallback(params: {
contextWindow: number;
customInstructions?: string;
previousSummary?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
if (params.messages.length === 0) {
return params.previousSummary ?? DEFAULT_SUMMARY_FALLBACK;
Expand All @@ -514,6 +536,7 @@ async function summarizeWithFallback(params: {
try {
return await summarizeChunks(params);
} catch (e) {
throwIfCompactionAborted(params.abortSignal);
log.warn('summarizeChunks failed, falling back to smaller chunks', {
error: e instanceof Error ? e.message : String(e),
});
Expand All @@ -539,6 +562,7 @@ async function summarizeWithFallback(params: {
const notes = oversizedNotes.length > 0 ? `\n\n${oversizedNotes.join("\n")}` : "";
return partial + notes;
} catch (e) {
throwIfCompactionAborted(params.abortSignal);
log.warn('smaller-chunks fallback also failed', {
error: e instanceof Error ? e.message : String(e),
});
Expand All @@ -559,7 +583,9 @@ export async function summarizeInStages(params: {
previousSummary?: string;
parts?: number;
minMessagesForSplit?: number;
abortSignal?: AbortSignal;
}): Promise<string> {
throwIfCompactionAborted(params.abortSignal);
const { messages } = params;
if (messages.length === 0) {
return params.previousSummary ?? DEFAULT_SUMMARY_FALLBACK;
Expand All @@ -580,6 +606,7 @@ export async function summarizeInStages(params: {

const partialSummaries: string[] = [];
for (const chunk of splits) {
throwIfCompactionAborted(params.abortSignal);
partialSummaries.push(
await summarizeWithFallback({
...params,
Expand Down Expand Up @@ -676,7 +703,9 @@ export async function buildCompactionSummary(params: {
maxTokens?: number;
reserveTokens?: number;
customInstructions?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
throwIfCompactionAborted(params.abortSignal);
if (params.messages.length === 0) {
return DEFAULT_SUMMARY_FALLBACK;
}
Expand All @@ -692,6 +721,7 @@ export async function buildCompactionSummary(params: {
maxChunkTokens,
contextWindow: params.contextWindowTokens,
customInstructions: params.customInstructions,
abortSignal: params.abortSignal,
});
}

Expand All @@ -703,6 +733,7 @@ async function runRemoteCompaction(params: {
customInstructions?: string;
droppedMessages: Message[];
systemPrompt?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
const { hybridCompact } = await import("./remote-compaction.js");
const hybrid = await hybridCompact(
Expand All @@ -712,6 +743,7 @@ async function runRemoteCompaction(params: {
contextWindowTokens: params.contextWindowTokens,
reserveTokens: params.reserveTokens,
customInstructions: params.customInstructions,
abortSignal: params.abortSignal,
},
params.droppedMessages,
params.systemPrompt,
Expand All @@ -727,6 +759,7 @@ async function runLlmCompaction(params: {
maxTokens?: number;
reserveTokens: number;
customInstructions?: string;
abortSignal?: AbortSignal;
}): Promise<string> {
return buildCompactionSummary({
summarize: params.summarize,
Expand All @@ -735,6 +768,7 @@ async function runLlmCompaction(params: {
maxTokens: params.maxTokens,
reserveTokens: params.reserveTokens,
customInstructions: params.customInstructions,
abortSignal: params.abortSignal,
});
}

Expand All @@ -752,6 +786,7 @@ export async function compactHistoryIfNeeded(params: {
remoteCompactProvider?: RemoteCompactProvider;
customInstructions?: string;
includeThinking?: boolean;
abortSignal?: AbortSignal;
/** M3: Optional workspace directory for file ops scope isolation. */
workspaceDir?: string;
}): Promise<{
Expand All @@ -760,6 +795,7 @@ export async function compactHistoryIfNeeded(params: {
pruneResult: PruneResult;
degraded?: boolean;
}> {
throwIfCompactionAborted(params.abortSignal);
const charsPerUnitBase = Math.max(1, params.charsPerTokenUnit ?? CHARS_PER_TOKEN_ESTIMATE);
const estimateOptions = { includeThinking: params.includeThinking };
const rawTotalChars = estimateMessagesChars(params.messages, estimateOptions) + (params.systemPrompt?.length ?? 0);
Expand Down Expand Up @@ -821,6 +857,7 @@ export async function compactHistoryIfNeeded(params: {
if (!shouldCompact) {
return { pruneResult };
}
throwIfCompactionAborted(params.abortSignal);

if (pruneResult.droppedMessages.length === 0) {
const totalTokens = estimateMessagesTokens(params.messages, estimateOptions);
Expand Down Expand Up @@ -867,6 +904,7 @@ export async function compactHistoryIfNeeded(params: {
customInstructions: params.customInstructions,
droppedMessages: pruneResult.droppedMessages,
systemPrompt: params.systemPrompt,
abortSignal: params.abortSignal,
});
} else {
summary = await runLlmCompaction({
Expand All @@ -876,9 +914,11 @@ export async function compactHistoryIfNeeded(params: {
maxTokens: params.maxTokens,
reserveTokens: resolvedSettings.reserveTokens,
customInstructions: params.customInstructions,
abortSignal: params.abortSignal,
});
}
} catch (err) {
throwIfCompactionAborted(params.abortSignal);
log.warn("LLM compaction failed; using deterministic fallback summary", {
error: err instanceof Error ? err.message : String(err),
});
Expand All @@ -899,6 +939,7 @@ export async function compactHistoryIfNeeded(params: {
);
degraded = true;
}
throwIfCompactionAborted(params.abortSignal);
summary = mergePriorCompactionSummaries(summary, priorCompactionSummaries);
const fileOps = createFileOps();
for (const message of pruneResult.droppedMessages) {
Expand Down
4 changes: 2 additions & 2 deletions packages/dmoss-agent/src/context/deterministic-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,15 @@ export function buildDeterministicCompactionSummary(
"## 1. 主要目标",
primaryGoal,
"## 2. 关键决策与约束",
"保留摘录中的用户原话、路径、命令、错误和工具调用参数;不要把未确认事项当作已完成。",
"保留摘录中的用户原话、路径、命令、错误和工具调用参数;不要把未确认事项当作已完成。assistant 历史回答只是历史说法,除非摘录中有工具结果或用户确认支撑,否则继续前需要复核。",
"## 3. 已完成的工作",
"见第 9 节中 assistant/tool_result 摘录。",
"## 4. 当前进行中",
"当前上下文发生压缩;继续时应结合保留尾部消息和本摘要。",
"## 5. 待办事项",
"若保留尾部消息没有明确下一步,应先复述理解并请求用户确认。",
"## 6. 设备与环境状态",
"未由本地规则可靠判定;以保留尾部消息和后续工具查询为准。",
"未由本地规则可靠判定;以保留尾部消息和后续工具查询为准。设备当前状态、路径、版本与执行结果必须重新用工具确认。",
"## 7. 关键文件与路径",
"见第 9 节工具调用参数与消息摘录中的 path/file_path/cmd/url 字面量。",
"## 8. 错误与问题",
Expand Down
1 change: 1 addition & 0 deletions packages/dmoss-agent/src/context/remote-compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export async function hybridCompact(
maxTokens: maxOutputTokens,
reserveTokens,
customInstructions: config.customInstructions,
abortSignal: config.abortSignal,
});

const summaryTokens = Math.ceil(summary.length / 4);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function mergePriorCompactionSummaries(summary: string, priorSummaries: s
MERGED_PRIOR_SUMMARY_MAX_CHARS,
);
return [
"## 已合并的早期检查点",
"## 已合并的早期检查点(弱上下文,未标明来源的事实需复核)",
mergedPrior,
"## 本次压缩新增摘要",
summary.trim(),
Expand Down
5 changes: 4 additions & 1 deletion packages/dmoss-agent/src/core/agent/dmoss-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ export class DmossAgent {
systemPrompt: params.system,
messages: [{ role: 'user', content: params.userPrompt }],
maxTokens: params.maxTokens,
abortSignal: params.abortSignal,
});
return resp.content
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
Expand Down Expand Up @@ -793,7 +794,7 @@ export class DmossAgent {
// Type bridge: InternalMessage and LLMMessage have compatible runtime shapes but different type definitions due to module boundaries
await store.replaceMessages(key, nextMessages as unknown as LLMMessage[]);
},
prepareCompaction: async ({ messages: compactMessages, forceCompaction }) => {
prepareCompaction: async ({ messages: compactMessages, forceCompaction, includeThinking, abortSignal }) => {
const compactResult = await compactHistoryIfNeeded({
summarize,
messages: compactMessages,
Expand All @@ -804,6 +805,8 @@ export class DmossAgent {
charsPerTokenUnit: resolveContextCharsPerTokenUnit(),
forceCompaction,
remoteCompactProvider: this.remoteCompactProvider,
includeThinking,
abortSignal,
});
if (!compactResult.summary || !compactResult.summaryMessage) return {};
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/dmoss-agent/src/core/llm/summarization-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function createSummarizeFnFromLlmProvider(params: {
systemPrompt: request.system,
messages: [{ role: 'user', content: request.userPrompt }],
maxTokens: request.maxTokens,
abortSignal: request.abortSignal,
});
return response.content
.filter((block): block is { type: 'text'; text: string } => block.type === 'text')
Expand Down Expand Up @@ -97,6 +98,7 @@ export function createClientLlmSummarizationStrategy(params: {
maxTokens: input.maxTokens,
skipLlmCompaction: input.skipLlmCompaction,
forceCompaction: input.forceCompaction,
abortSignal: input.abortSignal,
});

if (result.summary && result.summaryMessage) {
Expand Down
42 changes: 34 additions & 8 deletions packages/dmoss-agent/src/core/loop/agent-loop-compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './compact-hooks.js';
import type { Message } from '../session/session-jsonl.js';
import { summarizeDroppedMessages } from './agent-loop-context-prep.js';
import { runWithCompactionPrepareTimeout } from './compaction-timeout.js';

export interface AgentLoopPrepareCompaction {
(params: {
Expand Down Expand Up @@ -78,22 +79,30 @@ async function runCompactionCore(
errorLabel,
} = params;

let preHookRan = false;
let postHookRan = false;

try {
await compactHooks?.runPreHooks({
sessionKey,
runId,
messages: currentMessages,
reason: hookReason,
});
preHookRan = true;

const prep = await prepareCompaction({
messages: currentMessages,
sessionKey,
runId,
forceCompaction,
includeThinking,
abortSignal,
});
const prep = await runWithCompactionPrepareTimeout(
(prepareAbortSignal) =>
prepareCompaction({
messages: currentMessages,
sessionKey,
runId,
forceCompaction,
includeThinking,
abortSignal: prepareAbortSignal,
}),
{ abortSignal, label: errorLabel },
);

const checkpointOutline =
prep.checkpointOutline ?? buildCompactionCheckpointOutline(prep.summary);
Expand All @@ -108,6 +117,7 @@ async function runCompactionCore(
success: Boolean(prep.summary && prep.summaryMessage),
...(checkpointOutline ? { checkpointOutline } : {}),
});
postHookRan = true;

if (!prep.summary || !prep.summaryMessage) {
return { attempted: true, succeeded: false, retrySameTurn: false };
Expand Down Expand Up @@ -162,6 +172,22 @@ async function runCompactionCore(
compactionSummary,
};
} catch (error) {
if (preHookRan && !postHookRan) {
try {
await compactHooks?.runPostHooks({
sessionKey,
runId,
summaryChars: 0,
droppedMessages: 0,
reason: hookReason,
success: false,
});
} catch (hookError) {
onWarn?.(`${errorLabel} compaction post hook failed`, {
error: describeError(hookError),
});
}
}
onWarn?.(`${errorLabel} compaction failed`, {
error: describeError(error),
});
Expand Down
Loading