c0de-agent 1.0.0 → 1.1.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 (42) 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/loop.d.ts +10 -0
  7. package/dist/core/loop.js +559 -295
  8. package/dist/core/slash.js +3 -4
  9. package/dist/core/title.js +3 -2
  10. package/dist/core/types.d.ts +2 -0
  11. package/dist/core/worktree.js +6 -4
  12. package/dist/dap/session.js +3 -3
  13. package/dist/llm/provider.js +5 -1
  14. package/dist/plugins/loader.js +3 -2
  15. package/dist/server/agent-manager.d.ts +2 -0
  16. package/dist/server/agent-manager.js +8 -0
  17. package/dist/server/app.js +3 -0
  18. package/dist/server/context.js +2 -0
  19. package/dist/server/dev.d.ts +4 -1
  20. package/dist/server/dev.js +92 -27
  21. package/dist/server/permission/store.d.ts +2 -0
  22. package/dist/server/permission/store.js +9 -0
  23. package/dist/server/routes/chat.js +37 -1
  24. package/dist/server/routes/session.js +75 -1
  25. package/dist/server/routes/terminal.d.ts +5 -0
  26. package/dist/server/routes/terminal.js +66 -0
  27. package/dist/server/server.d.ts +14 -1
  28. package/dist/server/server.js +100 -28
  29. package/dist/server/terminal/pty-manager.d.ts +53 -0
  30. package/dist/server/terminal/pty-manager.js +160 -0
  31. package/dist/server/types.d.ts +3 -0
  32. package/dist/session/archive.d.ts +1 -1
  33. package/dist/session/compaction.d.ts +8 -2
  34. package/dist/session/compaction.js +85 -16
  35. package/dist/session/shake.d.ts +66 -0
  36. package/dist/session/shake.js +304 -0
  37. package/dist/session/types.d.ts +1 -1
  38. package/dist/shared/types/agent.d.ts +27 -0
  39. package/dist/shared/types/config.d.ts +10 -0
  40. package/dist/shared/types/llm.d.ts +1 -0
  41. package/dist/shared/types/tool.d.ts +3 -0
  42. package/package.json +11 -3
@@ -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.1.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
  }