mocode-ai 0.5.4 → 0.5.5
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.
- package/dist/agent/core.js +30 -46
- package/dist/agent/index.js +84 -53
- package/dist/config/index.js +0 -1
- package/dist/context/budget.js +22 -33
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +1 -55
- package/dist/context/relevance.js +1 -62
- package/dist/context/utils.js +68 -0
- package/dist/repl/index.js +27 -21
- package/dist/rollback/index.js +1 -13
- package/dist/session/compact.js +9 -14
- package/dist/session/drop.js +1 -43
- package/dist/session/scheduler.js +3 -3
- package/dist/tools/builtins/todolist.js +2 -4
- package/dist/ui/batch.js +142 -207
- package/dist/ui/content.js +6 -0
- package/dist/ui/layout.js +29 -31
- package/dist/ui/markdown.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 跨上下文模块共享的小工具函数。
|
|
2
|
+
//
|
|
3
|
+
// 目的:消除 `relevance.ts` / `lifecycle.ts` / `budget.ts` / `compact.ts` / `session/drop.ts`
|
|
4
|
+
// 之间的 copy-paste。这些函数逻辑完全一致,统一维护于此,避免行为漂移。
|
|
5
|
+
//
|
|
6
|
+
// 不变量:
|
|
7
|
+
// - 永不抛错(对齐「调度器永不抛错」契约)。
|
|
8
|
+
// - 仅依赖 `ChatMessage` 的最小形状,不反向 import agent / session / tools。
|
|
9
|
+
/** 把消息 content 拍平成字符串(OpenAI 可能 string / null / 多模态数组)。
|
|
10
|
+
* 用于估算 token、内容匹配、stub 拼接等场景。 */
|
|
11
|
+
export function toText(content) {
|
|
12
|
+
if (content == null)
|
|
13
|
+
return '';
|
|
14
|
+
if (typeof content === 'string')
|
|
15
|
+
return content;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(content);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return String(content);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
|
|
24
|
+
* 用于划定「当前轮保护区」——该 user 及其之后的消息(agent 本轮还在用)不动。
|
|
25
|
+
* 注:history[0] 是 system,故下界为 1。 */
|
|
26
|
+
export function lastUserIndex(history) {
|
|
27
|
+
for (let i = history.length - 1; i >= 1; i--) {
|
|
28
|
+
if (history[i].role === 'user')
|
|
29
|
+
return i;
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
/** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。
|
|
34
|
+
* 返回 null 表示找不到(孤儿 tool 消息,极少见),调用方保守跳过。 */
|
|
35
|
+
export function toolNameOf(history, idx) {
|
|
36
|
+
const tcId = history[idx].tool_call_id;
|
|
37
|
+
if (!tcId)
|
|
38
|
+
return null;
|
|
39
|
+
for (let j = idx - 1; j >= 1; j--) {
|
|
40
|
+
const m = history[j];
|
|
41
|
+
if (m.role !== 'assistant')
|
|
42
|
+
continue;
|
|
43
|
+
const tcs = m.tool_calls;
|
|
44
|
+
if (!tcs)
|
|
45
|
+
continue;
|
|
46
|
+
const hit = tcs.find((tc) => tc?.id === tcId);
|
|
47
|
+
if (hit)
|
|
48
|
+
return hit.function?.name ?? null;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** 解析工具 arguments JSON,只取 `path` 字段;非法 / 无 path 返 null。
|
|
53
|
+
* 用于 read_file / edit_file / write_file 等以 path 为关键字的工具,供跨消息关联使用。 */
|
|
54
|
+
export function extractPath(argsRaw) {
|
|
55
|
+
if (!argsRaw)
|
|
56
|
+
return null;
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(argsRaw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!parsed || typeof parsed !== 'object')
|
|
65
|
+
return null;
|
|
66
|
+
const p = parsed.path;
|
|
67
|
+
return typeof p === 'string' && p ? p : null;
|
|
68
|
+
}
|
package/dist/repl/index.js
CHANGED
|
@@ -455,14 +455,15 @@ function textOf(c) {
|
|
|
455
455
|
* 回放默认全折叠;用户可鼠标点击摘要行展开(由 BatchRenderer 接管,见 ui/batch.ts)。
|
|
456
456
|
*/
|
|
457
457
|
export function renderHistory(history) {
|
|
458
|
-
const
|
|
459
|
-
//
|
|
460
|
-
let
|
|
458
|
+
const idToEntry = new Map();
|
|
459
|
+
// 普通工具可跨 assistant 步聚合;mutation 各自占一个 group,并切断前后普通工具。
|
|
460
|
+
let pendingBatches = [];
|
|
461
|
+
let normalBatch = null;
|
|
461
462
|
const flushBatch = () => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
for (const entries of pendingBatches)
|
|
464
|
+
batch.writeSummaryOnly(entries, layout);
|
|
465
|
+
pendingBatches = [];
|
|
466
|
+
normalBatch = null;
|
|
466
467
|
};
|
|
467
468
|
for (let idx = 0; idx < history.length; idx++) {
|
|
468
469
|
const m = history[idx];
|
|
@@ -505,14 +506,25 @@ export function renderHistory(history) {
|
|
|
505
506
|
for (const tc of tcs) {
|
|
506
507
|
const name = tc?.function?.name ?? '';
|
|
507
508
|
const args = tc?.function?.arguments ?? '';
|
|
508
|
-
|
|
509
|
-
idToName.set(tc.id, name);
|
|
510
|
-
pendingBatch.push({
|
|
509
|
+
const entry = {
|
|
511
510
|
name,
|
|
512
511
|
callSummary: summarizeToolCall(name, args),
|
|
513
512
|
resultSummary: '',
|
|
514
513
|
diffBlock: null,
|
|
515
|
-
}
|
|
514
|
+
};
|
|
515
|
+
if (batch.isMutationToolName(name)) {
|
|
516
|
+
normalBatch = null;
|
|
517
|
+
pendingBatches.push([entry]);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
if (!normalBatch) {
|
|
521
|
+
normalBatch = [];
|
|
522
|
+
pendingBatches.push(normalBatch);
|
|
523
|
+
}
|
|
524
|
+
normalBatch.push(entry);
|
|
525
|
+
}
|
|
526
|
+
if (tc?.id)
|
|
527
|
+
idToEntry.set(tc.id, entry);
|
|
516
528
|
}
|
|
517
529
|
continue; // 跳过后续 tool 消息处理循环(由下一分支填 result)
|
|
518
530
|
}
|
|
@@ -522,18 +534,10 @@ export function renderHistory(history) {
|
|
|
522
534
|
}
|
|
523
535
|
if (m.role === 'tool') {
|
|
524
536
|
const id = m.tool_call_id ?? '';
|
|
525
|
-
const
|
|
537
|
+
const target = idToEntry.get(id);
|
|
538
|
+
const name = target?.name ?? '';
|
|
526
539
|
const output = textOf(m.content);
|
|
527
540
|
const preview = summarizeToolResult(name, output);
|
|
528
|
-
// 匹配 pendingBatch 中尚未填 result 的同名 entry;同名前缀 tool 较罕见(并行工具同 id 不同名)
|
|
529
|
-
let target;
|
|
530
|
-
for (let i = pendingBatch.length - 1; i >= 0; i--) {
|
|
531
|
-
const e = pendingBatch[i];
|
|
532
|
-
if (e.name === name && !e.resultSummary) {
|
|
533
|
-
target = e;
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
541
|
if (target) {
|
|
538
542
|
target.resultSummary = preview;
|
|
539
543
|
target.fullOutput = output;
|
|
@@ -853,6 +857,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
853
857
|
if (!loadSnapshots(loaded.id))
|
|
854
858
|
rebuildFromHistory(history);
|
|
855
859
|
contextState.lastUsage = undefined;
|
|
860
|
+
contextState.correction = 1;
|
|
856
861
|
lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
|
|
857
862
|
layout.clearContent();
|
|
858
863
|
renderHistory(history);
|
|
@@ -985,6 +990,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
985
990
|
currentSessionId = undefined; // 下轮起新会话文件
|
|
986
991
|
turnCount = 0; // 反思 cadence 重新计数
|
|
987
992
|
contextState.lastUsage = undefined;
|
|
993
|
+
contextState.correction = 1;
|
|
988
994
|
lastTurnUsage = undefined; // 清空旧轮的 token 累计
|
|
989
995
|
pendingAttachments = []; // 一并清空待发图片
|
|
990
996
|
layout.clearContent();
|
package/dist/rollback/index.js
CHANGED
|
@@ -2,24 +2,12 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { config } from '../config/index.js';
|
|
4
4
|
import { truncateDisplay } from '../ui/render.js';
|
|
5
|
+
import { toText } from '../context/utils.js';
|
|
5
6
|
let turnIdCounter = 0;
|
|
6
7
|
let currentTurnId = 0;
|
|
7
8
|
let turns = [];
|
|
8
9
|
let snapshots = [];
|
|
9
10
|
const MUTATION_TOOLS = new Set(['write_file', 'edit_file']);
|
|
10
|
-
/** 把任意 content 拍平成字符串(OpenAI 可能是 string / 多模态数组)。 */
|
|
11
|
-
function toText(content) {
|
|
12
|
-
if (content == null)
|
|
13
|
-
return '';
|
|
14
|
-
if (typeof content === 'string')
|
|
15
|
-
return content;
|
|
16
|
-
try {
|
|
17
|
-
return JSON.stringify(content);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return String(content);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
11
|
/** 规整成 cwd 相对路径(快照存相对,跨 /resume 同项目可识别;resolve 已归一 ./ 和 ..)。 */
|
|
24
12
|
function toRel(p) {
|
|
25
13
|
try {
|
package/dist/session/compact.js
CHANGED
|
@@ -5,10 +5,14 @@ import { ui } from '../ui/theme.js';
|
|
|
5
5
|
import { Spinner } from '../ui/spinner.js';
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
7
7
|
import { pruneAfterCompaction } from '../rollback/index.js';
|
|
8
|
+
import { toText } from '../context/utils.js';
|
|
8
9
|
/** 跨模块共享的上下文状态:agent 写 lastUsage,compact 写 lastEstimate,repl 的 /context 读。
|
|
9
|
-
* scheduler.ts 写最近一次调度日志(可选,repl 可读不到时 no-op)。
|
|
10
|
+
* scheduler.ts 写最近一次调度日志(可选,repl 可读不到时 no-op)。
|
|
11
|
+
* correction:API 实测 token / 估算 token 的校正系数(1.0 = 无偏差;>1 = 低估;<1 = 高估)。
|
|
12
|
+
* 由 agent/core.ts 在每次 chat 响应后更新;compact/repl 在 usage 失效时同步重置。 */
|
|
10
13
|
export const contextState = {
|
|
11
14
|
lastEstimate: 0,
|
|
15
|
+
correction: 1,
|
|
12
16
|
};
|
|
13
17
|
/** 中截:text 太长时保 head + 标记 + tail,总长 ≤ max。 */
|
|
14
18
|
export function truncateMid(text, max) {
|
|
@@ -97,19 +101,7 @@ export function capToolResultForHistory(name, output) {
|
|
|
97
101
|
return output;
|
|
98
102
|
return truncateMid(output, MAX_HISTORY_RESULT);
|
|
99
103
|
}
|
|
100
|
-
// ──
|
|
101
|
-
function toText(content) {
|
|
102
|
-
if (content == null)
|
|
103
|
-
return '';
|
|
104
|
-
if (typeof content === 'string')
|
|
105
|
-
return content;
|
|
106
|
-
try {
|
|
107
|
-
return JSON.stringify(content);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return String(content);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
104
|
+
// ── 内部:group 划分(toText 已移至 context/utils.ts 统一维护) ────────────
|
|
113
105
|
/**
|
|
114
106
|
* 把多模态 content 拍平成纯文本(供摘要 transcript 用):text parts 拼接;image_url parts
|
|
115
107
|
* 替换为 `[图片已剥离: <mime>]` stub,避免 base64 进摘要 prompt(LLM 看到也无意义,反而撑爆 token)。
|
|
@@ -323,6 +315,7 @@ export async function compactHistory(history, opts) {
|
|
|
323
315
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
324
316
|
contextState.lastEstimate = estimateAfter;
|
|
325
317
|
contextState.lastUsage = undefined;
|
|
318
|
+
contextState.correction = 1;
|
|
326
319
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
327
320
|
return {
|
|
328
321
|
compacted: true,
|
|
@@ -409,6 +402,7 @@ export async function compactHistory(history, opts) {
|
|
|
409
402
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
410
403
|
contextState.lastEstimate = estimateAfter;
|
|
411
404
|
contextState.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
|
|
405
|
+
contextState.correction = 1;
|
|
412
406
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
413
407
|
// 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
|
|
414
408
|
if (estimateAfter >= opts.threshold * opts.window) {
|
|
@@ -426,6 +420,7 @@ export async function compactHistory(history, opts) {
|
|
|
426
420
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
427
421
|
contextState.lastEstimate = estimateAfter;
|
|
428
422
|
contextState.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
|
|
423
|
+
contextState.correction = 1;
|
|
429
424
|
if (microcompactDone) {
|
|
430
425
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
431
426
|
return {
|
package/dist/session/drop.js
CHANGED
|
@@ -11,49 +11,7 @@
|
|
|
11
11
|
// - 原地修改 history(同 compact:length=0;push 重建,repl 持有同一引用)。
|
|
12
12
|
// - 永不抛错(对齐「调度器永不抛错」契约);无匹配 / 无可剔除 → 返 dropped=0。
|
|
13
13
|
import { messageTokens, estimateTokens, } from '../llm/index.js';
|
|
14
|
-
|
|
15
|
-
function toText(content) {
|
|
16
|
-
if (content == null)
|
|
17
|
-
return '';
|
|
18
|
-
if (typeof content === 'string')
|
|
19
|
-
return content;
|
|
20
|
-
try {
|
|
21
|
-
return JSON.stringify(content);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return String(content);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
|
|
29
|
-
* 用于划定"当前轮保护区"——该 user 及其之后的消息一律不剔除。
|
|
30
|
-
*/
|
|
31
|
-
function lastUserIndex(history) {
|
|
32
|
-
for (let i = history.length - 1; i >= 1; i--) {
|
|
33
|
-
if (history[i].role === 'user')
|
|
34
|
-
return i;
|
|
35
|
-
}
|
|
36
|
-
return -1;
|
|
37
|
-
}
|
|
38
|
-
/** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。 */
|
|
39
|
-
function toolNameOf(history, idx) {
|
|
40
|
-
const tcId = history[idx].tool_call_id;
|
|
41
|
-
if (!tcId)
|
|
42
|
-
return null;
|
|
43
|
-
for (let j = idx - 1; j >= 1; j--) {
|
|
44
|
-
const m = history[j];
|
|
45
|
-
if (m.role !== 'assistant')
|
|
46
|
-
continue;
|
|
47
|
-
const tcs = m
|
|
48
|
-
.tool_calls;
|
|
49
|
-
if (!tcs)
|
|
50
|
-
continue;
|
|
51
|
-
const hit = tcs.find((tc) => tc?.id === tcId);
|
|
52
|
-
if (hit)
|
|
53
|
-
return hit.function?.name ?? null;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
14
|
+
import { lastUserIndex, toText, toolNameOf } from '../context/utils.js';
|
|
57
15
|
/**
|
|
58
16
|
* 剔除历史里命中的旧 tool 结果(原地修改 history)。
|
|
59
17
|
*
|
|
@@ -45,7 +45,7 @@ export function createBudgetScheduler() {
|
|
|
45
45
|
// 占位:push-time 三闸(cap / pruner / lifecycle)已自动跑;此接缝供将来演进。
|
|
46
46
|
},
|
|
47
47
|
async runStep(history, step) {
|
|
48
|
-
const report = evaluateBudget(history, config.contextWindowTokens, step);
|
|
48
|
+
const report = evaluateBudget(history, config.contextWindowTokens, step, contextState.correction);
|
|
49
49
|
const actions = scheduleActions(report);
|
|
50
50
|
let compactHistoryCalled = false;
|
|
51
51
|
for (const a of actions) {
|
|
@@ -104,7 +104,7 @@ export async function manualCompact(history, focus, opts) {
|
|
|
104
104
|
}));
|
|
105
105
|
return {
|
|
106
106
|
step: -1,
|
|
107
|
-
report: evaluateBudget(history, config.contextWindowTokens, -1),
|
|
107
|
+
report: evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction),
|
|
108
108
|
actions: [{ kind: 'compact_history', focus }],
|
|
109
109
|
compactHistoryCalled: true,
|
|
110
110
|
ts: Date.now(),
|
|
@@ -118,7 +118,7 @@ export async function manualCompact(history, focus, opts) {
|
|
|
118
118
|
},
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
-
const report = evaluateBudget(history, config.contextWindowTokens, -1);
|
|
121
|
+
const report = evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction);
|
|
122
122
|
let actions = scheduleActions(report);
|
|
123
123
|
// 用户显式说「要压」:即使 report 不含 history 触发,仍追加 compact_history
|
|
124
124
|
const hasCompact = actions.some(a => a.kind === 'compact_history');
|
|
@@ -36,10 +36,8 @@ export const todolistTool = {
|
|
|
36
36
|
'BAD: "打开文件A → 修改函数X → 保存文件A → 运行测试" (too fine-grained, just do it)',
|
|
37
37
|
'',
|
|
38
38
|
'## UPDATE WORKFLOW',
|
|
39
|
-
'Update
|
|
40
|
-
'
|
|
41
|
-
'- Use `update` for single steps; `batch_update` ONLY when 2-3 steps finish simultaneously.',
|
|
42
|
-
'- ⛔ NEVER defer all updates to the end — this defeats the purpose of the progress chip.',
|
|
39
|
+
'Update in real-time after completing each step (one step = one update, or batch_update for 2-3 at once).',
|
|
40
|
+
'Do NOT batch all updates at the end — update as you go so the chip reflects real progress.',
|
|
43
41
|
'All steps done/skipped → plan auto-finishes, archives, and chip disappears.',
|
|
44
42
|
'',
|
|
45
43
|
'## LIFECYCLE',
|