c0de-agent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli/deps.d.ts +16 -5
  2. package/dist/cli/deps.js +18 -5
  3. package/dist/cli/index.js +8 -2
  4. package/dist/core/agent.js +5 -0
  5. package/dist/core/config.js +1 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.js +1 -0
  8. package/dist/core/loop.d.ts +10 -0
  9. package/dist/core/loop.js +559 -295
  10. package/dist/core/slash.js +3 -4
  11. package/dist/core/title.js +3 -2
  12. package/dist/core/types.d.ts +2 -0
  13. package/dist/core/workflow.d.ts +25 -0
  14. package/dist/core/workflow.js +98 -0
  15. package/dist/core/worktree.js +6 -4
  16. package/dist/dap/session.js +3 -3
  17. package/dist/llm/provider.js +5 -1
  18. package/dist/plugins/loader.js +3 -2
  19. package/dist/server/agent-manager.d.ts +2 -0
  20. package/dist/server/agent-manager.js +8 -0
  21. package/dist/server/app.js +3 -0
  22. package/dist/server/context.js +2 -0
  23. package/dist/server/dev.d.ts +4 -1
  24. package/dist/server/dev.js +92 -27
  25. package/dist/server/permission/store.d.ts +2 -0
  26. package/dist/server/permission/store.js +9 -0
  27. package/dist/server/routes/chat.js +44 -1
  28. package/dist/server/routes/session.js +75 -1
  29. package/dist/server/routes/terminal.d.ts +5 -0
  30. package/dist/server/routes/terminal.js +66 -0
  31. package/dist/server/server.d.ts +14 -1
  32. package/dist/server/server.js +100 -28
  33. package/dist/server/terminal/pty-manager.d.ts +53 -0
  34. package/dist/server/terminal/pty-manager.js +160 -0
  35. package/dist/server/types.d.ts +3 -0
  36. package/dist/session/archive.d.ts +1 -1
  37. package/dist/session/compaction.d.ts +8 -2
  38. package/dist/session/compaction.js +85 -16
  39. package/dist/session/shake.d.ts +66 -0
  40. package/dist/session/shake.js +304 -0
  41. package/dist/session/types.d.ts +1 -1
  42. package/dist/shared/types/agent.d.ts +27 -0
  43. package/dist/shared/types/config.d.ts +10 -0
  44. package/dist/shared/types/llm.d.ts +1 -0
  45. package/dist/shared/types/tool.d.ts +3 -0
  46. package/package.json +11 -3
@@ -1,6 +1,6 @@
1
1
  import { generateId } from '../shared/index.js';
2
2
  import { archiveOriginalEntries } from './archive.js';
3
- import { deleteEntriesByIds, getMessages, insertEntry } from './message.js';
3
+ import { deleteEntriesByIds, getEntries, getMessages, insertEntry } from './message.js';
4
4
  import { upsertFileSnapshot } from './snapshot.js';
5
5
  import { estimateMessageTokens, estimateTokens } from './token.js';
6
6
  /**
@@ -51,30 +51,80 @@ function extractHotFiles(messages) {
51
51
  }
52
52
  return hot.sort((a, b) => b.accessCount - a.accessCount).slice(0, 10);
53
53
  }
54
- /** Build the LLM summarization prompt for a set of messages. */
55
- function buildCompactionPrompt(messages) {
54
+ /** 工具输出在压缩 prompt 中保留的最大字符数(参考 opencode)。 */
55
+ const TOOL_OUTPUT_MAX_CHARS = 2000;
56
+ /** 截断超长字符串:head 60% + "[truncated]" + tail 40%,而非硬切。 */
57
+ function truncateToolOutput(s) {
58
+ if (s.length <= TOOL_OUTPUT_MAX_CHARS)
59
+ return s;
60
+ const head = Math.floor(TOOL_OUTPUT_MAX_CHARS * 0.6);
61
+ const tail = TOOL_OUTPUT_MAX_CHARS - head;
62
+ return `${s.slice(0, head)}[truncated]${s.slice(-tail)}`;
63
+ }
64
+ /** 序列化单个 content part:text/thinking 保持原样,tool 输出超长时截断。 */
65
+ function serializePart(p) {
66
+ if (p._tag === 'text' || p._tag === 'thinking')
67
+ return p.text;
68
+ if (p._tag === 'tool_call') {
69
+ const input = truncateToolOutput(JSON.stringify(p.input));
70
+ return JSON.stringify({ _tag: p._tag, id: p.id, tool: p.tool, input });
71
+ }
72
+ if (p._tag === 'tool_result') {
73
+ const output = truncateToolOutput(JSON.stringify(p.output));
74
+ return JSON.stringify({ _tag: p._tag, id: p.id, tool: p.tool, output });
75
+ }
76
+ return JSON.stringify(p);
77
+ }
78
+ /**
79
+ * Build the LLM summarization prompt for a set of messages.
80
+ *
81
+ * 当存在 previousSummary(上一次压缩生成的摘要)时,prompt 头部改为增量更新指令,
82
+ * 引导模型在已有摘要上叠加新事实、剔除过时信息,从而避免连续多次压缩导致的
83
+ * 信息逐次丢失。无 previousSummary 时退回原始的从零压缩指令。
84
+ */
85
+ function buildCompactionPrompt(messages, previousSummary) {
56
86
  const history = messages
57
- .map((m) => `[${m.role}] ${m.content.map((p) => (p._tag === 'text' ? p.text : JSON.stringify(p))).join(' ')}`)
87
+ .map((m) => `[${m.role}] ${m.content.map((p) => serializePart(p)).join(' ')}`)
58
88
  .join('\n');
59
- return `将以下对话历史压缩为结构化摘要。保留关键信息,丢弃冗余细节。
89
+ const sections = `## Agenda
90
+ 逐条列出对话中出现的议题/任务,按处理顺序排列。每条格式:
91
+ - **[议题标题]** — ✅已解决 / ⏳进行中 / 🔒阻塞 / 📋待办
92
+ - ✅/🔒 → 一行:最终结论或卡点
93
+ - ⏳/📋 → 完整保留:目标、约束、已尝试方向、相关文件路径、关键决策、待确认问题
60
94
 
61
95
  ## Goal
62
- 用户的目标是什么
96
+ 用户此次会话的总体目标(若 Agenda 已涵盖,写"见 Agenda")
63
97
 
64
- ## Progress
65
- 已完成的工作
98
+ ## Constraints & Preferences
99
+ 用户约束、偏好、规范要求(或"(none)")
66
100
 
67
- ## Decisions
68
- 做出的关键决策
69
-
70
- ## Next Steps
71
- 接下来要做什么
101
+ ## Key Decisions
102
+ 做出的关键决策及原因
72
103
 
73
104
  ## Critical Context
74
- 必须记住的上下文(文件路径、变量名、错误信息等)
105
+ 必须记住的技术事实(文件路径、变量名、命令、错误信息、未解决问题)
75
106
 
76
107
  ## Modified Files
77
- 修改过的文件列表及变更摘要
108
+ 修改过的文件路径及变更摘要
109
+
110
+ ## Relevant Files
111
+ 对任务重要的文件/目录路径及原因`;
112
+ const header = previousSummary
113
+ ? `更新以下已有【议题驱动】摘要。重点:
114
+ - 已解决的议题:状态更新为✅并压缩为一行结论;
115
+ - 新增议题:补入 Agenda 并完整保留其描述与约束;
116
+ - 尚未解决的议题:保持其原有描述与约束不变,只叠加本轮新进展。
117
+
118
+ <previous-summary>
119
+ ${previousSummary}
120
+ </previous-summary>`
121
+ : `将以下对话历史压缩为一份【议题驱动】的结构化摘要。
122
+ 核心原则——非对称保留:已解决的议题只留一行结论;尚未解决/待办的议题
123
+ 必须完整保留其描述、约束、已尝试方向、相关文件与待确认问题,它们是后续
124
+ 工作的蓝图,绝不可被稀释。`;
125
+ return `${header}
126
+
127
+ ${sections}
78
128
 
79
129
  ---
80
130
  对话历史:
@@ -115,6 +165,22 @@ function findKeepRecentStart(messages, keepRecentTokens) {
115
165
  // Always retain at least the most recent message.
116
166
  return Math.min(start, messages.length - 1);
117
167
  }
168
+ /**
169
+ * 查找 session 最新的 compaction 摘要,用于增量摘要(P0-2)。
170
+ *
171
+ * 从全部 entries 中筛选 tag='compaction' 的记录,取最后一条(时间序最晚)的
172
+ * summary。无历史摘要时返回 undefined,buildCompactionPrompt 据此退回从零压缩模式。
173
+ */
174
+ async function findPreviousSummary(handle, sessionId) {
175
+ const entries = await getEntries(handle, sessionId);
176
+ for (let i = entries.length - 1; i >= 0; i--) {
177
+ const entry = entries[i];
178
+ if (entry && '_tag' in entry && entry._tag === 'compaction') {
179
+ return entry.summary;
180
+ }
181
+ }
182
+ return undefined;
183
+ }
118
184
  /**
119
185
  * Compact a session: summarize old messages, archive them, keep recent ones.
120
186
  *
@@ -144,7 +210,10 @@ async function compactSession(handle, sessionId, summarizer, config) {
144
210
  if (compactMessages.length === 0) {
145
211
  return { compacted: false, reason: 'nothing_to_compact' };
146
212
  }
147
- const prompt = buildCompactionPrompt(compactMessages);
213
+ // 查询当前 session 最新的 compaction 摘要,作为增量更新的基线(P0-2)。
214
+ // 这样连续多次压缩时,新摘要会基于已有摘要叠加而非从零重建,避免早期信息逐次丢失。
215
+ const previousSummary = await findPreviousSummary(handle, sessionId);
216
+ const prompt = buildCompactionPrompt(compactMessages, previousSummary);
148
217
  // Run the (slow, network) summarizer OUTSIDE the transaction so we don't
149
218
  // hold a DB transaction open across an LLM call.
150
219
  const summary = await summarizer(prompt);
@@ -0,0 +1,66 @@
1
+ import type { Message } from '../shared/types/message.js';
2
+ export interface ShakeConfig {
3
+ /** 保护最近 N token 的上下文不被 shake。 */
4
+ protectTokens: number;
5
+ /** 总节省 token < minSavings 时不 shake(preview 路径用)。 */
6
+ minSavings: number;
7
+ /** fenced/XML block 的最小 token 阈值。 */
8
+ fenceMinTokens: number;
9
+ /** 受保护的工具名列表(其 tool_result 不被 shake)。 */
10
+ protectedTools: string[];
11
+ }
12
+ /** Auto-shake 默认配置:保护活跃尾部,保守阈值。 */
13
+ export declare const DEFAULT_SHAKE_CONFIG: ShakeConfig;
14
+ export type ShakeRegion = {
15
+ kind: 'toolResult';
16
+ id: string;
17
+ messageId: string;
18
+ messageIndex: number;
19
+ partIndex: number;
20
+ tokens: number;
21
+ originalText: string;
22
+ label: string;
23
+ /** tool_call_id(tool_result part.id),前端用于跨消息合并后匹配渲染块。 */
24
+ toolCallId: string;
25
+ } | {
26
+ kind: 'block';
27
+ id: string;
28
+ messageId: string;
29
+ messageIndex: number;
30
+ partIndex: number;
31
+ start: number;
32
+ end: number;
33
+ tokens: number;
34
+ originalText: string;
35
+ label: string;
36
+ };
37
+ /** API 返回给前端的区域视图。 */
38
+ export type ShakeRegionView = {
39
+ id: string;
40
+ kind: 'toolResult' | 'block';
41
+ messageId: string;
42
+ messageIndex: number;
43
+ partIndex: number;
44
+ tokens: number;
45
+ label: string;
46
+ preview: string;
47
+ placeholder: string;
48
+ isAfterProtectWindow: boolean;
49
+ /** tool_result 的 tool_call_id(仅 toolResult 类别),前端跨消息合并后匹配渲染块。 */
50
+ toolCallId?: string;
51
+ };
52
+ /**
53
+ * 定位 fenced 代码块和顶层 XML 元素 span。返回字符偏移 [start, end) 数组,
54
+ * 覆盖完整块(含围栏/标签行,不含尾换行)。围栏内抑制 XML 检测。
55
+ * 未闭合围栏/标签不产生 range(保守策略)。
56
+ */
57
+ export declare function scanTextForBlockRanges(text: string): Array<{
58
+ start: number;
59
+ end: number;
60
+ }>;
61
+ /** 收集可 shake 的区域。纯函数,不修改输入。 */
62
+ export declare function collectShakeRegions(messages: Message[], config: ShakeConfig): ShakeRegion[];
63
+ /** 原位替换选中区域。返回新数组,不修改原数组。 */
64
+ export declare function applyShakeRegions(messages: Message[], regions: ShakeRegion[]): Message[];
65
+ /** 区域转 API 视图。protectWindow 窗口内的标记 isAfterProtectWindow=false。 */
66
+ export declare function toRegionViews(regions: ShakeRegion[], config: ShakeConfig, messages: Message[]): ShakeRegionView[];
@@ -0,0 +1,304 @@
1
+ import { estimateTokens } from './token.js';
2
+ /** Rough token cost of a placeholder line; used only for the savings gate. */
3
+ const PLACEHOLDER_TOKEN_ESTIMATE = 16;
4
+ /** 识别顶层 XML 元素(小写 tag,保守策略)。 */
5
+ const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
6
+ const CLOSING_XML = /^<\/([a-z_-]+)>$/;
7
+ /** Auto-shake 默认配置:保护活跃尾部,保守阈值。 */
8
+ export const DEFAULT_SHAKE_CONFIG = {
9
+ protectTokens: 16_000,
10
+ minSavings: 4_000,
11
+ fenceMinTokens: 400,
12
+ protectedTools: [],
13
+ };
14
+ /**
15
+ * 定位 fenced 代码块和顶层 XML 元素 span。返回字符偏移 [start, end) 数组,
16
+ * 覆盖完整块(含围栏/标签行,不含尾换行)。围栏内抑制 XML 检测。
17
+ * 未闭合围栏/标签不产生 range(保守策略)。
18
+ */
19
+ export function scanTextForBlockRanges(text) {
20
+ const ranges = [];
21
+ let inFence = false;
22
+ let fenceStart = -1;
23
+ const tagStack = [];
24
+ let xmlStart = -1;
25
+ let lineStart = 0;
26
+ for (let i = 0; i <= text.length; i++) {
27
+ if (i !== text.length && text[i] !== '\n')
28
+ continue;
29
+ const line = text.slice(lineStart, i);
30
+ const lineEnd = i;
31
+ const trimmedStart = line.trimStart();
32
+ const isFenceLine = trimmedStart.startsWith('```') || trimmedStart.startsWith('~~~');
33
+ if (isFenceLine) {
34
+ if (!inFence) {
35
+ inFence = true;
36
+ fenceStart = lineStart;
37
+ }
38
+ else {
39
+ inFence = false;
40
+ ranges.push({ start: fenceStart, end: lineEnd });
41
+ fenceStart = -1;
42
+ }
43
+ lineStart = i + 1;
44
+ continue;
45
+ }
46
+ if (!inFence) {
47
+ // Only recognize top-level XML (no leading whitespace)
48
+ if (line.length === trimmedStart.length) {
49
+ const openingMatch = OPENING_XML.exec(trimmedStart);
50
+ if (openingMatch) {
51
+ const tagName = openingMatch[1];
52
+ if (tagName) {
53
+ if (tagStack.length === 0)
54
+ xmlStart = lineStart;
55
+ tagStack.push(tagName);
56
+ }
57
+ }
58
+ else {
59
+ const closingMatch = CLOSING_XML.exec(trimmedStart);
60
+ if (closingMatch &&
61
+ tagStack.length > 0 &&
62
+ tagStack[tagStack.length - 1] === closingMatch[1]) {
63
+ tagStack.pop();
64
+ if (tagStack.length === 0 && xmlStart >= 0) {
65
+ ranges.push({ start: xmlStart, end: lineEnd });
66
+ xmlStart = -1;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ lineStart = i + 1;
73
+ }
74
+ return mergeRanges(ranges);
75
+ }
76
+ /** 按 start 升序,丢弃与已保留范围重叠的(嵌套取最外层)。 */
77
+ function mergeRanges(ranges) {
78
+ if (ranges.length <= 1)
79
+ return ranges;
80
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
81
+ const kept = [];
82
+ let lastEnd = -1;
83
+ for (const range of sorted) {
84
+ if (range.start < lastEnd)
85
+ continue;
86
+ kept.push(range);
87
+ lastEnd = range.end;
88
+ }
89
+ return kept;
90
+ }
91
+ /** 单条消息的 token 估算(优先用缓存的 tokenCount)。 */
92
+ function messageTokens(m) {
93
+ if (m.tokenCount > 0)
94
+ return m.tokenCount;
95
+ let total = 0;
96
+ for (const part of m.content) {
97
+ switch (part._tag) {
98
+ case 'text':
99
+ case 'thinking':
100
+ case 'steering':
101
+ total += estimateTokens(part.text);
102
+ break;
103
+ case 'tool_call':
104
+ total += estimateTokens(JSON.stringify(part.input));
105
+ break;
106
+ case 'tool_result':
107
+ total += estimateTokens(JSON.stringify(part.output));
108
+ break;
109
+ }
110
+ }
111
+ return total;
112
+ }
113
+ /** tool_result part 的输出文本。 */
114
+ function toolResultText(output) {
115
+ if (output._tag === 'success')
116
+ return output.output;
117
+ if (output._tag === 'error')
118
+ return output.error;
119
+ if (output._tag === 'truncated')
120
+ return output.output;
121
+ return '';
122
+ }
123
+ /** 收集可 shake 的区域。纯函数,不修改输入。 */
124
+ export function collectShakeRegions(messages, config) {
125
+ const n = messages.length;
126
+ if (n === 0)
127
+ return [];
128
+ // accumulatedAfter[i] = i 之后所有 message 的 token 总和
129
+ const accumulatedAfter = new Array(n);
130
+ let acc = 0;
131
+ for (let i = n - 1; i >= 0; i--) {
132
+ accumulatedAfter[i] = acc;
133
+ const m = messages[i];
134
+ if (m)
135
+ acc += messageTokens(m);
136
+ }
137
+ const regions = [];
138
+ for (let i = 0; i < n; i++) {
139
+ const msg = messages[i];
140
+ if (!msg)
141
+ continue;
142
+ const afterTokens = accumulatedAfter[i] ?? 0;
143
+ const isAfterProtectWindow = afterTokens >= config.protectTokens;
144
+ if (!isAfterProtectWindow)
145
+ continue;
146
+ for (let partIndex = 0; partIndex < msg.content.length; partIndex++) {
147
+ const part = msg.content[partIndex];
148
+ if (!part)
149
+ continue;
150
+ // tool_result 区域
151
+ if (part._tag === 'tool_result') {
152
+ // 已 shaken 跳过
153
+ if ('shakenAt' in part.output && part.output.shakenAt)
154
+ continue;
155
+ // protectedTools 跳过
156
+ if (config.protectedTools.includes(part.tool))
157
+ continue;
158
+ const text = toolResultText(part.output);
159
+ if (text.length === 0)
160
+ continue;
161
+ const tokens = estimateTokens(text);
162
+ if (tokens < config.fenceMinTokens)
163
+ continue;
164
+ regions.push({
165
+ kind: 'toolResult',
166
+ id: `${msg.id}:toolResult:${partIndex}`,
167
+ messageId: msg.id,
168
+ messageIndex: i,
169
+ partIndex,
170
+ tokens,
171
+ originalText: text,
172
+ label: part.tool,
173
+ toolCallId: part.id,
174
+ });
175
+ continue;
176
+ }
177
+ // text/thinking block 区域
178
+ if (part._tag === 'text' || part._tag === 'thinking') {
179
+ for (const range of scanTextForBlockRanges(part.text)) {
180
+ const slice = part.text.slice(range.start, range.end);
181
+ if (slice.length === 0)
182
+ continue;
183
+ const tokens = estimateTokens(slice);
184
+ if (tokens < config.fenceMinTokens)
185
+ continue;
186
+ regions.push({
187
+ kind: 'block',
188
+ id: `${msg.id}:block:${partIndex}:${range.start}`,
189
+ messageId: msg.id,
190
+ messageIndex: i,
191
+ partIndex,
192
+ start: range.start,
193
+ end: range.end,
194
+ tokens,
195
+ originalText: slice,
196
+ label: msg.role,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // minSavings 门控
203
+ let savings = 0;
204
+ for (const region of regions)
205
+ savings += Math.max(0, region.tokens - PLACEHOLDER_TOKEN_ESTIMATE);
206
+ if (savings < config.minSavings)
207
+ return [];
208
+ return regions;
209
+ }
210
+ /** 为区域生成占位符文本。 */
211
+ function placeholderFor(region) {
212
+ if (region.kind === 'toolResult') {
213
+ return `[shaken: ${region.label}, ${region.tokens} tokens]`;
214
+ }
215
+ return '[shaken]';
216
+ }
217
+ /** 原位替换选中区域。返回新数组,不修改原数组。 */
218
+ export function applyShakeRegions(messages, regions) {
219
+ if (regions.length === 0)
220
+ return messages;
221
+ // 按 messageId 分组
222
+ const byMessage = new Map();
223
+ for (const region of regions) {
224
+ const list = byMessage.get(region.messageId) ?? [];
225
+ list.push(region);
226
+ byMessage.set(region.messageId, list);
227
+ }
228
+ return messages.map((msg) => {
229
+ const msgRegions = byMessage.get(msg.id);
230
+ if (!msgRegions)
231
+ return msg;
232
+ // 深拷贝 content(保证不修改原对象)
233
+ const newContent = structuredClone(msg.content);
234
+ const now = Date.now();
235
+ // toolResult 区域:替换 output 文本
236
+ for (const region of msgRegions) {
237
+ if (region.kind !== 'toolResult')
238
+ continue;
239
+ const part = newContent[region.partIndex];
240
+ if (!part)
241
+ continue;
242
+ if (part._tag !== 'tool_result')
243
+ continue;
244
+ const placeholder = placeholderFor(region);
245
+ if (part.output._tag === 'success') {
246
+ part.output = { ...part.output, output: placeholder, shakenAt: now };
247
+ }
248
+ else if (part.output._tag === 'error') {
249
+ part.output = { ...part.output, error: placeholder, shakenAt: now };
250
+ }
251
+ else if (part.output._tag === 'truncated') {
252
+ part.output = { ...part.output, output: placeholder, shakenAt: now };
253
+ }
254
+ }
255
+ // block 区域:按 partIndex 分组,同一 text 内按 start 降序 splice
256
+ const blockByPart = new Map();
257
+ for (const region of msgRegions) {
258
+ if (region.kind !== 'block')
259
+ continue;
260
+ const list = blockByPart.get(region.partIndex) ?? [];
261
+ list.push(region);
262
+ blockByPart.set(region.partIndex, list);
263
+ }
264
+ for (const [partIndex, blockRegions] of blockByPart) {
265
+ const part = newContent[partIndex];
266
+ if (!part || (part._tag !== 'text' && part._tag !== 'thinking'))
267
+ continue;
268
+ const sorted = [...blockRegions].sort((a, b) => b.start - a.start);
269
+ let text = part.text;
270
+ for (const br of sorted) {
271
+ text = text.slice(0, br.start) + placeholderFor(br) + text.slice(br.end);
272
+ }
273
+ part.text = text;
274
+ }
275
+ return { ...msg, content: newContent };
276
+ });
277
+ }
278
+ /** 区域转 API 视图。protectWindow 窗口内的标记 isAfterProtectWindow=false。 */
279
+ export function toRegionViews(regions, config, messages) {
280
+ const n = messages.length;
281
+ const accumulatedAfter = new Array(n);
282
+ let acc = 0;
283
+ for (let i = n - 1; i >= 0; i--) {
284
+ accumulatedAfter[i] = acc;
285
+ const m = messages[i];
286
+ if (m)
287
+ acc += messageTokens(m);
288
+ }
289
+ return regions.map((region) => ({
290
+ id: region.id,
291
+ kind: region.kind,
292
+ messageId: region.messageId,
293
+ messageIndex: region.messageIndex,
294
+ partIndex: region.partIndex,
295
+ tokens: region.tokens,
296
+ label: region.label,
297
+ preview: region.originalText.slice(0, 200),
298
+ placeholder: region.kind === 'toolResult'
299
+ ? `[shaken: ${region.label}, ${region.tokens} tokens]`
300
+ : '[shaken]',
301
+ isAfterProtectWindow: (accumulatedAfter[region.messageIndex] ?? 0) >= config.protectTokens,
302
+ ...(region.kind === 'toolResult' ? { toolCallId: region.toolCallId } : {}),
303
+ }));
304
+ }
@@ -101,7 +101,7 @@ type CompactionArchive = {
101
101
  id: string;
102
102
  sessionId: string;
103
103
  compactionId: string;
104
- archiveType: 'compaction' | 'squash';
104
+ archiveType: 'compaction' | 'squash' | 'shake';
105
105
  originalEntries: SessionEntry[];
106
106
  summary: string;
107
107
  tokenCount: number;
@@ -172,6 +172,18 @@ type AgentEvent = {
172
172
  agentType: string;
173
173
  success: boolean;
174
174
  output?: string;
175
+ }
176
+ /**
177
+ * 会话压缩成功后发出(spec: plugin-hooks `session:compact`)。
178
+ * 实际发生压缩(runCompaction 返回 compacted=true)时才 yield;
179
+ * nothing_to_compact 不发。archiveId/summary 仅在真实压缩时存在。
180
+ */
181
+ | {
182
+ _tag: 'compaction_done';
183
+ summary: string;
184
+ archiveId?: string;
185
+ compactedCount: number;
186
+ keptCount: number;
175
187
  } | {
176
188
  _tag: 'done';
177
189
  };
@@ -198,5 +210,20 @@ type AgentState = {
198
210
  provider: string;
199
211
  model: string;
200
212
  };
213
+ /**
214
+ * 压缩死锁标记:自动压缩成功后仍超阈值(如 keepRecentTokens 本身已超限)时置真,
215
+ * 暂停后续自动压缩以防每轮重复触发(无限循环)。收到新用户消息(agentLoop 重入)时重置。
216
+ */
217
+ compactionDeadEnd?: boolean;
218
+ /**
219
+ * 压缩退化监测器:压缩成功后初始化,监测接下来若干轮 assistant 回复。
220
+ * 若连续产生空回复(无实质文本且无 tool_call),发出非致命警告(不中断循环),
221
+ * 提示 agent 可能在"沉默退化"。remaining 耗尽即清除;新一轮用户输入
222
+ * (agentLoop 重入)时不会自动重置——它由压缩成功单独建立。
223
+ */
224
+ postCompactionMonitor?: {
225
+ remaining: number;
226
+ noTextStreak: number;
227
+ };
201
228
  };
202
229
  export type { AgentConfig, AgentError, AgentEvent, AgentState, AgentStatus, LLMCall, LLMSegment, PendingToolCall, SegmentTrigger, TokenBudget, };
@@ -16,6 +16,16 @@ type CompactionConfig = {
16
16
  reserveTokens: number;
17
17
  /** Token budget for retaining recent messages verbatim. */
18
18
  keepRecentTokens: number;
19
+ /** 中轮压缩(mid-run compaction):单个 turn 内工具执行后、下一次 LLM 请求前
20
+ * 按阈值静默压缩。与 turn-end 自动压缩独立——以本开关为闸门复用 shouldCompact
21
+ * 的阈值逻辑(不受 `enabled` 影响),默认关闭(保守开启)。 */
22
+ midTurnEnabled?: boolean;
23
+ /** 压缩摘要使用的模型覆盖。未设置时回退到会话主模型。
24
+ * 摘要任务对推理能力要求低,可指定便宜/快速模型以降低成本。 */
25
+ compactionModel?: {
26
+ provider: string;
27
+ model: string;
28
+ };
19
29
  };
20
30
  /** Tool-mode auto-selection configuration (spec §16.5). */
21
31
  type ToolMetricsConfig = {
@@ -117,6 +117,7 @@ type StreamChunk = {
117
117
  error: {
118
118
  message: string;
119
119
  retryable?: boolean;
120
+ classification?: 'context-overflow';
120
121
  };
121
122
  };
122
123
  export type { ChatMessage, ChatRequest, ChatTool, ContentPart, FinishReason, ModelCapabilities, ModelOverride, ModelRole, ProviderConfig, ProviderProtocol, StreamChunk, };
@@ -6,9 +6,11 @@ type ToolResult = {
6
6
  _tag: 'success';
7
7
  output: string;
8
8
  metadata?: Record<string, unknown>;
9
+ shakenAt?: number;
9
10
  } | {
10
11
  _tag: 'error';
11
12
  error: string;
13
+ shakenAt?: number;
12
14
  } | {
13
15
  _tag: 'permission_required';
14
16
  reason: string;
@@ -17,6 +19,7 @@ type ToolResult = {
17
19
  output: string;
18
20
  truncated: boolean;
19
21
  totalLines: number;
22
+ shakenAt?: number;
20
23
  };
21
24
  /** Context passed to a URL resolver (skill://, agent://, pr://, …). */
22
25
  type URLResolveContext = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c0de-agent",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Open-source AI coding assistant with Browser-Server architecture",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,7 +50,8 @@
50
50
  },
51
51
  "pnpm": {
52
52
  "onlyBuiltDependencies": [
53
- "esbuild"
53
+ "esbuild",
54
+ "node-pty"
54
55
  ]
55
56
  },
56
57
  "devDependencies": {
@@ -69,10 +70,12 @@
69
70
  "@types/pg": "^8.20.0",
70
71
  "@types/react": "^19.2.17",
71
72
  "@types/react-dom": "^19.2.3",
73
+ "@types/ws": "^8.18.1",
72
74
  "@vitejs/plugin-react": "^6.0.3",
73
75
  "@wyw-in-js/vite": "^2.1.0",
74
76
  "drizzle-kit": "^0.31.10",
75
77
  "happy-dom": "^20.10.6",
78
+ "node-gyp": "^13.0.1",
76
79
  "pg": "^8.22.0",
77
80
  "tsx": "^4.22.4",
78
81
  "typescript": "^6.0.3",
@@ -89,6 +92,9 @@
89
92
  "@native-router/core": "^1.1.0",
90
93
  "@native-router/react": "^1.1.2",
91
94
  "@tanstack/react-query": "^5.101.2",
95
+ "@xterm/addon-fit": "^0.11.0",
96
+ "@xterm/addon-web-links": "^0.12.0",
97
+ "@xterm/xterm": "^6.0.0",
92
98
  "diff": "^9.0.0",
93
99
  "drizzle-orm": "^0.45.2",
94
100
  "fuzzysort": "^3.1.0",
@@ -96,10 +102,12 @@
96
102
  "hono": "^4.12.27",
97
103
  "lucide-react": "^1.22.0",
98
104
  "marked": "^18.0.5",
105
+ "node-pty": "^1.1.0",
99
106
  "react": "^19.2.7",
100
107
  "react-dom": "^19.2.7",
101
108
  "react-router-dom": "^7.18.0",
102
109
  "shiki": "^4.3.0",
103
- "undici": "^8.5.0"
110
+ "undici": "^8.5.0",
111
+ "ws": "^8.21.0"
104
112
  }
105
113
  }