mocode-ai 1.4.2 → 1.4.3
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/README.md +13 -1
- package/dist/agent/core.js +14 -936
- package/dist/agent/index.js +37 -13
- package/dist/agent/model-turn.js +218 -0
- package/dist/agent/pipeline.js +18 -0
- package/dist/agent/run-contracts.js +1 -0
- package/dist/agent/run-coordinator.js +758 -0
- package/dist/agent/runtime-context.js +118 -24
- package/dist/agent/spawn.js +11 -7
- package/dist/agent/stages/context-trimmer.js +63 -0
- package/dist/agent/stages/contracts.js +12 -0
- package/dist/agent/stages/history-manager.js +178 -0
- package/dist/agent/stages/legacy-adapters.js +19 -0
- package/dist/agent/stages/model-runner.js +29 -0
- package/dist/agent/stages/run-policy.js +73 -0
- package/dist/agent/stages/tool-dispatcher.js +341 -0
- package/dist/agent/tool-helpers.js +12 -12
- package/dist/agent/tool-turn.js +87 -0
- package/dist/agent/trace-state.js +97 -101
- package/dist/agent/turn-lifecycle.js +110 -0
- package/dist/config/index.js +14 -0
- package/dist/host/stdio.js +101 -40
- package/dist/llm/index.js +51 -35
- package/dist/llm/providers/anthropic.js +16 -10
- package/dist/llm/runtime.js +1 -0
- package/dist/permissions/index.js +21 -5
- package/dist/repl/commands/compact.js +2 -2
- package/dist/repl/commands/session.js +3 -12
- package/dist/repl/message-format.js +5 -0
- package/dist/repl/runtime.js +95 -55
- package/dist/rollback/index.js +29 -624
- package/dist/rollback/store.js +593 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtime.js +307 -0
- package/dist/session/compact.js +22 -14
- package/dist/session/index.js +1 -0
- package/dist/session/persist.js +10 -146
- package/dist/session/scheduler.js +28 -16
- package/dist/session/state.js +16 -12
- package/dist/session/store.js +218 -0
- package/dist/session/trace.js +5 -15
- package/dist/tools/policy.js +19 -15
- package/dist/tools/registry.js +21 -229
- package/dist/tools/router.js +5 -3
- package/dist/tools/tool-runtime.js +267 -0
- package/dist/ui/layout-internal/content-write.js +4 -0
- package/package.json +7 -3
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { runAgentCore } from '../agent/core.js';
|
|
3
|
+
import { createAgentRuntimeContext, defaultAgentRuntimeContext, } from '../agent/runtime-context.js';
|
|
4
|
+
import { contextState as defaultContextState, createContextState, manualCompact, } from '../session/index.js';
|
|
5
|
+
const activeRuntimes = new AsyncLocalStorage();
|
|
6
|
+
/** Runtime facade active in the current agent async tree, used by nested agents to borrow the parent lifecycle. */
|
|
7
|
+
export function getActiveRuntime() {
|
|
8
|
+
return activeRuntimes.getStore();
|
|
9
|
+
}
|
|
10
|
+
function firstLineOf(input) {
|
|
11
|
+
const text = typeof input === 'string' ? input : (input.find((part) => part.type === 'text')?.text ?? '');
|
|
12
|
+
return Array.from(text.split('\n')[0] ?? '')
|
|
13
|
+
.slice(0, 40)
|
|
14
|
+
.join('');
|
|
15
|
+
}
|
|
16
|
+
function toChatTools(context) {
|
|
17
|
+
return context.toolRuntime.tools.map((tool) => ({
|
|
18
|
+
type: 'function',
|
|
19
|
+
function: {
|
|
20
|
+
name: tool.name,
|
|
21
|
+
description: tool.description,
|
|
22
|
+
parameters: tool.parameters,
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** High-level owner for one AgentRuntimeContext and all host-facing lifecycle operations. */
|
|
27
|
+
export class Runtime {
|
|
28
|
+
context;
|
|
29
|
+
contextState;
|
|
30
|
+
session;
|
|
31
|
+
rollback;
|
|
32
|
+
listeners = new Map();
|
|
33
|
+
activeControllers = new Set();
|
|
34
|
+
activeWork = new Set();
|
|
35
|
+
mainRunActive = false;
|
|
36
|
+
controlOperation = null;
|
|
37
|
+
onStart;
|
|
38
|
+
onClose;
|
|
39
|
+
startPromise = null;
|
|
40
|
+
closePromise = null;
|
|
41
|
+
closed = false;
|
|
42
|
+
constructor(init = {}) {
|
|
43
|
+
const { context, contextState, onStart, onClose, ...contextInit } = init;
|
|
44
|
+
this.context = context ?? createAgentRuntimeContext(contextInit);
|
|
45
|
+
this.contextState = contextState ?? createContextState();
|
|
46
|
+
this.onStart = onStart;
|
|
47
|
+
this.onClose = onClose;
|
|
48
|
+
this.session = {
|
|
49
|
+
create: () => this.createSession(false),
|
|
50
|
+
resume: (id) => this.resumeSession(id),
|
|
51
|
+
save: (history, id, queryHistory, lastToolGroups) => this.saveSession(history, id, queryHistory, lastToolGroups),
|
|
52
|
+
list: (limit) => {
|
|
53
|
+
this.assertOpen();
|
|
54
|
+
return this.context.sessionStore.list(limit);
|
|
55
|
+
},
|
|
56
|
+
clear: () => this.createSession(true),
|
|
57
|
+
};
|
|
58
|
+
this.rollback = {
|
|
59
|
+
list: () => {
|
|
60
|
+
this.assertOpen();
|
|
61
|
+
return this.context.rollbackStore.listTurns();
|
|
62
|
+
},
|
|
63
|
+
plan: (n, history) => {
|
|
64
|
+
this.assertIdle('plan rollback');
|
|
65
|
+
const plan = this.context.rollbackStore.planRollback(n, history);
|
|
66
|
+
this.emit('rollback.planned', { cutoffTurnId: plan.cutoffTurnId, retainedTurns: plan.n });
|
|
67
|
+
return plan;
|
|
68
|
+
},
|
|
69
|
+
apply: (plan, history, revertPaths = new Set()) => {
|
|
70
|
+
this.assertIdle('apply rollback');
|
|
71
|
+
const result = this.context.rollbackStore.applyRollback(plan, history, revertPaths);
|
|
72
|
+
this.emit('rollback.applied', {
|
|
73
|
+
cutoffTurnId: plan.cutoffTurnId,
|
|
74
|
+
retainedTurns: plan.n,
|
|
75
|
+
...result,
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
start() {
|
|
82
|
+
if (this.closed)
|
|
83
|
+
return Promise.reject(new Error('Runtime is closed.'));
|
|
84
|
+
if (this.startPromise)
|
|
85
|
+
return this.startPromise;
|
|
86
|
+
this.startPromise = Promise.resolve(this.onStart?.(this)).then(() => {
|
|
87
|
+
this.emit('lifecycle.started', { sandboxRoot: this.context.sandboxRoot });
|
|
88
|
+
});
|
|
89
|
+
return this.startPromise;
|
|
90
|
+
}
|
|
91
|
+
async run(options) {
|
|
92
|
+
await this.start();
|
|
93
|
+
this.assertOpen();
|
|
94
|
+
if (this.controlOperation) {
|
|
95
|
+
throw new Error(`Runtime cannot start a run while ${this.controlOperation} is active.`);
|
|
96
|
+
}
|
|
97
|
+
const { turn = 'new', turnLabel, signal: callerSignal, traceContext, ...coreOptions } = options;
|
|
98
|
+
let turnId = traceContext?.turnId;
|
|
99
|
+
if (turn === 'new') {
|
|
100
|
+
if (this.mainRunActive) {
|
|
101
|
+
throw new Error("A main Runtime turn is already active. Use turn: 'inherit' for nested agents.");
|
|
102
|
+
}
|
|
103
|
+
this.mainRunActive = true;
|
|
104
|
+
try {
|
|
105
|
+
turnId = this.context.beginTurn(turnLabel ?? firstLineOf(options.userInput));
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
this.mainRunActive = false;
|
|
109
|
+
throw cause;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (turn === 'inherit') {
|
|
113
|
+
turnId = this.context.getCurrentTurnId();
|
|
114
|
+
}
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const relayAbort = () => controller.abort(callerSignal?.reason);
|
|
117
|
+
if (callerSignal?.aborted)
|
|
118
|
+
relayAbort();
|
|
119
|
+
else
|
|
120
|
+
callerSignal?.addEventListener('abort', relayAbort, { once: true });
|
|
121
|
+
this.activeControllers.add(controller);
|
|
122
|
+
const finishWork = this.beginTrackedWork();
|
|
123
|
+
this.emit('run.started', { turn, turnId });
|
|
124
|
+
try {
|
|
125
|
+
const result = await activeRuntimes.run(this, () => runAgentCore({
|
|
126
|
+
...coreOptions,
|
|
127
|
+
contextState: coreOptions.contextState ?? this.contextState,
|
|
128
|
+
signal: controller.signal,
|
|
129
|
+
traceContext: turnId === undefined ? traceContext : { ...traceContext, turnId },
|
|
130
|
+
runtimeContext: this.context,
|
|
131
|
+
}));
|
|
132
|
+
this.emit('run.completed', {
|
|
133
|
+
turn,
|
|
134
|
+
turnId,
|
|
135
|
+
terminationReason: result.terminationReason,
|
|
136
|
+
completed: result.completed,
|
|
137
|
+
});
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
catch (cause) {
|
|
141
|
+
this.emit('run.failed', {
|
|
142
|
+
turn,
|
|
143
|
+
turnId,
|
|
144
|
+
aborted: controller.signal.aborted,
|
|
145
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
146
|
+
});
|
|
147
|
+
throw cause;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
callerSignal?.removeEventListener('abort', relayAbort);
|
|
151
|
+
this.activeControllers.delete(controller);
|
|
152
|
+
if (turn === 'new')
|
|
153
|
+
this.mainRunActive = false;
|
|
154
|
+
finishWork();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
cancel(reason) {
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const controller of this.activeControllers) {
|
|
160
|
+
if (controller.signal.aborted)
|
|
161
|
+
continue;
|
|
162
|
+
controller.abort(reason);
|
|
163
|
+
count += 1;
|
|
164
|
+
}
|
|
165
|
+
this.emit('run.cancel_requested', { count });
|
|
166
|
+
return count;
|
|
167
|
+
}
|
|
168
|
+
async compact(history, options = {}) {
|
|
169
|
+
await this.start();
|
|
170
|
+
this.assertIdle('compact');
|
|
171
|
+
this.controlOperation = 'compact';
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const relayAbort = () => controller.abort(options.signal?.reason);
|
|
174
|
+
if (options.signal?.aborted)
|
|
175
|
+
relayAbort();
|
|
176
|
+
else
|
|
177
|
+
options.signal?.addEventListener('abort', relayAbort, { once: true });
|
|
178
|
+
this.activeControllers.add(controller);
|
|
179
|
+
const finishWork = this.beginTrackedWork();
|
|
180
|
+
this.emit('compact.started', { focus: options.focus, force: options.force === true });
|
|
181
|
+
try {
|
|
182
|
+
const result = await this.context.runInScope(() => manualCompact(history, options.focus, {
|
|
183
|
+
force: options.force,
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
contextState: options.contextState ?? this.contextState,
|
|
186
|
+
activeTools: options.activeTools ?? toChatTools(this.context),
|
|
187
|
+
runtime: this.context,
|
|
188
|
+
}));
|
|
189
|
+
this.emit('compact.completed', {
|
|
190
|
+
compactHistoryCalled: result.compactHistoryCalled,
|
|
191
|
+
historyMutation: result.historyMutation,
|
|
192
|
+
reason: result.compactDetail?.reason,
|
|
193
|
+
});
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
catch (cause) {
|
|
197
|
+
this.emit('compact.failed', { message: cause instanceof Error ? cause.message : String(cause) });
|
|
198
|
+
throw cause;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
options.signal?.removeEventListener('abort', relayAbort);
|
|
202
|
+
this.activeControllers.delete(controller);
|
|
203
|
+
this.controlOperation = null;
|
|
204
|
+
finishWork();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
on(type, listener) {
|
|
208
|
+
const listeners = this.listeners.get(type) ?? new Set();
|
|
209
|
+
listeners.add(listener);
|
|
210
|
+
this.listeners.set(type, listeners);
|
|
211
|
+
return () => listeners.delete(listener);
|
|
212
|
+
}
|
|
213
|
+
close() {
|
|
214
|
+
if (this.closePromise)
|
|
215
|
+
return this.closePromise;
|
|
216
|
+
this.closed = true;
|
|
217
|
+
this.emit('lifecycle.closing', {});
|
|
218
|
+
this.closePromise = (async () => {
|
|
219
|
+
if (this.startPromise)
|
|
220
|
+
await this.startPromise.catch(() => undefined);
|
|
221
|
+
this.cancel(new DOMException('Runtime closed.', 'AbortError'));
|
|
222
|
+
await Promise.allSettled([...this.activeWork]);
|
|
223
|
+
try {
|
|
224
|
+
await this.onClose?.(this);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
this.emit('lifecycle.closed', {});
|
|
228
|
+
this.listeners.clear();
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
return this.closePromise;
|
|
232
|
+
}
|
|
233
|
+
createSession(cleared) {
|
|
234
|
+
this.assertIdle(cleared ? 'clear session' : 'create session');
|
|
235
|
+
const id = this.context.sessionStore.createId();
|
|
236
|
+
this.context.sessionStore.setCurrentSessionId(id);
|
|
237
|
+
this.context.rollbackStore.resetState();
|
|
238
|
+
this.emit(cleared ? 'session.cleared' : 'session.created', { sessionId: id });
|
|
239
|
+
return id;
|
|
240
|
+
}
|
|
241
|
+
resumeSession(id) {
|
|
242
|
+
this.assertIdle('resume session');
|
|
243
|
+
const loaded = this.context.sessionStore.load(id);
|
|
244
|
+
if (!loaded?.history.length)
|
|
245
|
+
return null;
|
|
246
|
+
this.context.sessionStore.setCurrentSessionId(loaded.id);
|
|
247
|
+
this.context.rollbackStore.resetState();
|
|
248
|
+
const snapshotsLoaded = this.context.rollbackStore.loadSnapshots(loaded.id);
|
|
249
|
+
if (!snapshotsLoaded)
|
|
250
|
+
this.context.rollbackStore.rebuildFromHistory(loaded.history);
|
|
251
|
+
this.emit('session.resumed', { sessionId: loaded.id, snapshotsLoaded });
|
|
252
|
+
return loaded;
|
|
253
|
+
}
|
|
254
|
+
saveSession(history, id = this.context.sessionStore.getCurrentSessionId(), queryHistory = [], lastToolGroups = []) {
|
|
255
|
+
this.assertIdle('save session');
|
|
256
|
+
if (!id)
|
|
257
|
+
throw new Error('Cannot save without an active session.');
|
|
258
|
+
this.context.sessionStore.setCurrentSessionId(id);
|
|
259
|
+
const meta = this.context.sessionStore.save(history, id, queryHistory, lastToolGroups);
|
|
260
|
+
this.context.rollbackStore.persistSnapshots(id);
|
|
261
|
+
this.emit('session.saved', { sessionId: id });
|
|
262
|
+
return meta;
|
|
263
|
+
}
|
|
264
|
+
assertOpen() {
|
|
265
|
+
if (this.closed)
|
|
266
|
+
throw new Error('Runtime is closed.');
|
|
267
|
+
}
|
|
268
|
+
assertIdle(operation) {
|
|
269
|
+
this.assertOpen();
|
|
270
|
+
if (this.activeControllers.size > 0 || this.controlOperation) {
|
|
271
|
+
const active = this.controlOperation ?? 'an agent run';
|
|
272
|
+
throw new Error(`Runtime cannot ${operation} while ${active} is active.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
beginTrackedWork() {
|
|
276
|
+
let resolve;
|
|
277
|
+
const settled = new Promise((done) => {
|
|
278
|
+
resolve = done;
|
|
279
|
+
});
|
|
280
|
+
this.activeWork.add(settled);
|
|
281
|
+
return () => {
|
|
282
|
+
this.activeWork.delete(settled);
|
|
283
|
+
resolve();
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
emit(type, data) {
|
|
287
|
+
const event = { type, timestamp: new Date().toISOString(), data };
|
|
288
|
+
for (const key of [type, '*']) {
|
|
289
|
+
for (const listener of this.listeners.get(key) ?? []) {
|
|
290
|
+
try {
|
|
291
|
+
const result = listener(event);
|
|
292
|
+
if (result && typeof result.then === 'function') {
|
|
293
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Runtime events are observational and must never affect hooks or lifecycle behavior.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export function createRuntime(init = {}) {
|
|
304
|
+
return new Runtime(init);
|
|
305
|
+
}
|
|
306
|
+
/** Compatibility facade explicitly bound to all historical process-level singletons. */
|
|
307
|
+
export const defaultRuntime = new Runtime({ context: defaultAgentRuntimeContext, contextState: defaultContextState });
|
package/dist/session/compact.js
CHANGED
|
@@ -4,11 +4,17 @@ import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RES
|
|
|
4
4
|
import { ui } from '../ui/theme.js';
|
|
5
5
|
import { Spinner } from '../ui/spinner.js';
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
7
|
-
import {
|
|
7
|
+
import { defaultRollbackStore } from '../rollback/index.js';
|
|
8
8
|
import { toText } from '../context/utils.js';
|
|
9
9
|
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
10
10
|
import { collectArtifactRefs } from '../context/artifacts.js';
|
|
11
11
|
import { writeCompactionSnapshot } from './notes.js';
|
|
12
|
+
/** 默认兼容依赖:旧调用方继续读取进程级 config/chat。 */
|
|
13
|
+
export const defaultCompactionRuntime = {
|
|
14
|
+
config,
|
|
15
|
+
modelTransport: chat,
|
|
16
|
+
rollbackStore: defaultRollbackStore,
|
|
17
|
+
};
|
|
12
18
|
export function createContextState() {
|
|
13
19
|
return { lastEstimate: 0, correction: 1, calibrationSamples: 0 };
|
|
14
20
|
}
|
|
@@ -424,7 +430,7 @@ function buildTranscript(stripped, scale) {
|
|
|
424
430
|
})
|
|
425
431
|
.join('\n');
|
|
426
432
|
}
|
|
427
|
-
async function defaultSummarize(older, focus, signal) {
|
|
433
|
+
async function defaultSummarize(older, focus, signal, runtime = defaultCompactionRuntime) {
|
|
428
434
|
// 已中断就别白拼转录了(几千 token 的字符串拼接 + 一去不回的 LLM 请求)。
|
|
429
435
|
if (signal?.aborted) {
|
|
430
436
|
throw new DOMException('This operation was aborted', 'AbortError');
|
|
@@ -435,13 +441,13 @@ async function defaultSummarize(older, focus, signal) {
|
|
|
435
441
|
// 不需要"先压一遍再摘要"。封顶策略:先按满额封顶;总量仍超窗口 55% 预算时等比缩小
|
|
436
442
|
// 配额重拼(优先保条数/每轮都有代表,其次保单条长度);再超才整段中截兜底(罕见)。
|
|
437
443
|
let transcript = buildTranscript(stripped, 1);
|
|
438
|
-
const tokenBudget = Math.floor(config.contextWindowTokens * SUMMARY_TRANSCRIPT_WINDOW_RATIO);
|
|
444
|
+
const tokenBudget = Math.floor(runtime.config.contextWindowTokens * SUMMARY_TRANSCRIPT_WINDOW_RATIO);
|
|
439
445
|
let tokens = estimateTokens(transcript);
|
|
440
446
|
if (tokens > tokenBudget) {
|
|
441
447
|
transcript = buildTranscript(stripped, tokenBudget / tokens);
|
|
442
448
|
tokens = estimateTokens(transcript);
|
|
443
|
-
if (tokens > Math.floor(config.contextWindowTokens * 0.6)) {
|
|
444
|
-
transcript = truncateMid(transcript, Math.floor(config.contextWindowTokens * 0.5));
|
|
449
|
+
if (tokens > Math.floor(runtime.config.contextWindowTokens * 0.6)) {
|
|
450
|
+
transcript = truncateMid(transcript, Math.floor(runtime.config.contextWindowTokens * 0.5));
|
|
445
451
|
}
|
|
446
452
|
}
|
|
447
453
|
const sysMsg = {
|
|
@@ -473,7 +479,7 @@ async function defaultSummarize(older, focus, signal) {
|
|
|
473
479
|
// signal 必须透传:摘要是几十秒的 LLM 调用,不串进来 Ctrl+C 只能干等它跑完。
|
|
474
480
|
// 空 handlers:不打印、不外显流式;tools=[] 不带工具表——摘要纯文本任务,
|
|
475
481
|
// 全量工具 schema 白占几千 token 窗口,还诱导幻觉工具调用。
|
|
476
|
-
const r = await
|
|
482
|
+
const r = await runtime.modelTransport([sysMsg, userMsg], {}, signal, []);
|
|
477
483
|
// 推理模型可能只返 reasoning_content(content 为 null),或幻觉出 tool_calls → 视为失败
|
|
478
484
|
if (r.toolCalls.length > 0 || !r.content)
|
|
479
485
|
return null;
|
|
@@ -622,7 +628,8 @@ export async function compactHistory(history, opts) {
|
|
|
622
628
|
...older,
|
|
623
629
|
];
|
|
624
630
|
}
|
|
625
|
-
const summarizeFn = opts.summarize ??
|
|
631
|
+
const summarizeFn = opts.summarize ??
|
|
632
|
+
((older, focus, signal) => defaultSummarize(older, focus, signal, opts.runtime ?? defaultCompactionRuntime));
|
|
626
633
|
let summary = null;
|
|
627
634
|
try {
|
|
628
635
|
summary = await summarizeFn(older, opts.focus, opts.signal);
|
|
@@ -661,7 +668,7 @@ export async function compactHistory(history, opts) {
|
|
|
661
668
|
const rebuilt = [systemMsg, summaryMsg, ...flattenGroups(kept)];
|
|
662
669
|
history.length = 0;
|
|
663
670
|
history.push(...rebuilt);
|
|
664
|
-
pruneAfterCompaction(history);
|
|
671
|
+
(opts.runtime?.rollbackStore ?? defaultRollbackStore).pruneAfterCompaction(history);
|
|
665
672
|
const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
|
|
666
673
|
state.lastEstimate = estimateAfter;
|
|
667
674
|
state.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用校正估算
|
|
@@ -735,8 +742,8 @@ export async function compactHistory(history, opts) {
|
|
|
735
742
|
* 默认 manual=false 自动路径完全不变。
|
|
736
743
|
*/
|
|
737
744
|
export async function maybeCompact(history, report, manualOpts, state = contextState, activeTools = chatTools,
|
|
738
|
-
/** 主 agent 的 abort signal;透传给 compactHistory → 摘要器 →
|
|
739
|
-
signal) {
|
|
745
|
+
/** 主 agent 的 abort signal;透传给 compactHistory → 摘要器 → model transport。 */
|
|
746
|
+
signal, runtime = defaultCompactionRuntime) {
|
|
740
747
|
const est = estimatePromptTokens(history, activeTools, state.correction);
|
|
741
748
|
state.lastEstimate = est;
|
|
742
749
|
const isManual = manualOpts?.manual === true;
|
|
@@ -747,7 +754,7 @@ signal) {
|
|
|
747
754
|
report.rawTotal >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * report.window;
|
|
748
755
|
// 手动路径:旁路 autoCompact / report / 总阈三重门
|
|
749
756
|
if (!isManual) {
|
|
750
|
-
if (!hardCap && !config.autoCompact)
|
|
757
|
+
if (!hardCap && !runtime.config.autoCompact)
|
|
751
758
|
return;
|
|
752
759
|
if (report) {
|
|
753
760
|
// Scheduler mode: the shared pressure report is the sole automatic trigger.
|
|
@@ -758,14 +765,14 @@ signal) {
|
|
|
758
765
|
else {
|
|
759
766
|
// Fallback mode uses the same threshold; there is no second compact gate.
|
|
760
767
|
const raw = estimatePromptTokens(history, activeTools, 1);
|
|
761
|
-
hardCap = raw >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
|
|
762
|
-
const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
|
|
768
|
+
hardCap = raw >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * runtime.config.contextWindowTokens;
|
|
769
|
+
const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * runtime.config.contextWindowTokens;
|
|
763
770
|
if (!hardCap && est < pressureLine)
|
|
764
771
|
return;
|
|
765
772
|
}
|
|
766
773
|
}
|
|
767
774
|
const r = await compactHistory(history, {
|
|
768
|
-
window: config.contextWindowTokens,
|
|
775
|
+
window: runtime.config.contextWindowTokens,
|
|
769
776
|
threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
|
|
770
777
|
focus: manualOpts?.focus,
|
|
771
778
|
manual: isManual,
|
|
@@ -773,6 +780,7 @@ signal) {
|
|
|
773
780
|
force: manualOpts?.force === true || hardCap,
|
|
774
781
|
tools: activeTools,
|
|
775
782
|
contextState: state,
|
|
783
|
+
runtime,
|
|
776
784
|
signal,
|
|
777
785
|
});
|
|
778
786
|
return r;
|
package/dist/session/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
|
|
|
11
11
|
// repl /compact 命令调 manualCompact(history, focus?):与自动路径共享决策,focus 透传摘要 prompt。
|
|
12
12
|
export { runScheduler, manualCompact, createBudgetScheduler } from './scheduler.js';
|
|
13
13
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir } from './persist.js';
|
|
14
|
+
export { SessionStore, defaultSessionStore, getActiveSessionStore, withSessionStore } from './store.js';
|
|
14
15
|
export { appendCurrentSessionTrace, appendCurrentSessionTraceEvent, appendCurrentSessionRuntimeEvent, createTraceEvent, } from './trace.js';
|
|
15
16
|
export { reduceTraceMetrics, readTraceEvents, readTraceMetrics } from './trace-metrics.js';
|
|
16
17
|
export { summarizeToolArguments, hashTraceValue, safeProviderId } from './trace-sanitize.js';
|
package/dist/session/persist.js
CHANGED
|
@@ -1,157 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { config, getActiveModel } from '../config/index.js';
|
|
4
|
-
import { isToolRouteGroupName } from '../config/profiles.js';
|
|
5
|
-
import { truncateDisplay } from '../ui/render.js';
|
|
1
|
+
import { defaultSessionStore } from './store.js';
|
|
6
2
|
/** 会话目录(确保存在)。 */
|
|
7
3
|
export function sessionDir() {
|
|
8
|
-
|
|
9
|
-
return config.sessionDir;
|
|
4
|
+
return defaultSessionStore.sessionDir();
|
|
10
5
|
}
|
|
11
|
-
/** 新会话 id
|
|
6
|
+
/** 新会话 id:可排序时间前缀 + 碰撞防护后缀。 */
|
|
12
7
|
export function newSessionId() {
|
|
13
|
-
|
|
14
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
15
|
-
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
8
|
+
return defaultSessionStore.createId();
|
|
16
9
|
}
|
|
17
|
-
/**
|
|
18
|
-
function idToIso(id) {
|
|
19
|
-
const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/.exec(id);
|
|
20
|
-
if (!m)
|
|
21
|
-
return id;
|
|
22
|
-
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
|
|
23
|
-
}
|
|
24
|
-
function toText(content) {
|
|
25
|
-
if (content == null)
|
|
26
|
-
return '';
|
|
27
|
-
if (typeof content === 'string')
|
|
28
|
-
return content;
|
|
29
|
-
try {
|
|
30
|
-
return JSON.stringify(content);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return String(content);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function firstUserOf(history) {
|
|
37
|
-
for (const m of history) {
|
|
38
|
-
if (m.role === 'user') {
|
|
39
|
-
const text = toText(m.content)
|
|
40
|
-
.replace(/\n/g, ' ')
|
|
41
|
-
.trim();
|
|
42
|
-
return truncateDisplay(text, 40);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return '';
|
|
46
|
-
}
|
|
47
|
-
function sessionPath(id) {
|
|
48
|
-
return path.join(config.sessionDir, id, 'session.json');
|
|
49
|
-
}
|
|
50
|
-
/** 保存会话到磁盘。全新且没有 query 的会话不创建文件;已有会话即使回滚为空也必须覆盖旧记录。 */
|
|
10
|
+
/** 保存会话;保留旧同步 API 与磁盘格式。 */
|
|
51
11
|
export function saveSession(history, id, queryHistory = [], lastToolGroups = []) {
|
|
52
|
-
|
|
53
|
-
id,
|
|
54
|
-
createdAt: idToIso(id),
|
|
55
|
-
model: getActiveModel(),
|
|
56
|
-
firstUser: history.length > 1
|
|
57
|
-
? firstUserOf(history)
|
|
58
|
-
: truncateDisplay((queryHistory[0] ?? '').replace(/\n/g, ' ').trim(), 40),
|
|
59
|
-
};
|
|
60
|
-
const currentPath = sessionPath(id);
|
|
61
|
-
const legacyPath = path.join(config.sessionDir, `${id}.json`);
|
|
62
|
-
if (history.length <= 1 && queryHistory.length === 0 && !existsSync(currentPath) && !existsSync(legacyPath)) {
|
|
63
|
-
return meta;
|
|
64
|
-
}
|
|
65
|
-
const dir = path.join(config.sessionDir, id);
|
|
66
|
-
mkdirSync(dir, { recursive: true });
|
|
67
|
-
const record = {
|
|
68
|
-
...meta,
|
|
69
|
-
history,
|
|
70
|
-
queryHistory: [...queryHistory],
|
|
71
|
-
lastToolGroups: [...lastToolGroups],
|
|
72
|
-
};
|
|
73
|
-
writeFileSync(currentPath, JSON.stringify(record), 'utf8');
|
|
74
|
-
// 一旦写入新式目录,删除旧式扁平副本,避免已回滚消息仍残留在磁盘。
|
|
75
|
-
if (existsSync(legacyPath))
|
|
76
|
-
unlinkSync(legacyPath);
|
|
77
|
-
return meta;
|
|
12
|
+
return defaultSessionStore.save(history, id, queryHistory, lastToolGroups);
|
|
78
13
|
}
|
|
79
|
-
/**
|
|
14
|
+
/** 加载会话;不存在或损坏时返回 null。 */
|
|
80
15
|
export function loadSession(id) {
|
|
81
|
-
|
|
82
|
-
const newPath = path.join(config.sessionDir, id, 'session.json');
|
|
83
|
-
// 旧式: .mocode/sessions/<id>.json
|
|
84
|
-
const oldPath = path.join(config.sessionDir, `${id}.json`);
|
|
85
|
-
const p = existsSync(newPath) ? newPath : oldPath;
|
|
86
|
-
if (!existsSync(p))
|
|
87
|
-
return null;
|
|
88
|
-
try {
|
|
89
|
-
const raw = readFileSync(p, 'utf8');
|
|
90
|
-
const rec = JSON.parse(raw);
|
|
91
|
-
if (!rec || !Array.isArray(rec.history))
|
|
92
|
-
return null;
|
|
93
|
-
return {
|
|
94
|
-
id: rec.id,
|
|
95
|
-
createdAt: rec.createdAt ?? idToIso(rec.id ?? id),
|
|
96
|
-
model: rec.model ?? '',
|
|
97
|
-
firstUser: rec.firstUser ?? '',
|
|
98
|
-
history: rec.history,
|
|
99
|
-
queryHistory: Array.isArray(rec.queryHistory)
|
|
100
|
-
? rec.queryHistory.filter((query) => typeof query === 'string')
|
|
101
|
-
: undefined,
|
|
102
|
-
lastToolGroups: Array.isArray(rec.lastToolGroups) ? rec.lastToolGroups.filter(isToolRouteGroupName) : undefined,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
16
|
+
return defaultSessionStore.load(id);
|
|
108
17
|
}
|
|
109
|
-
/**
|
|
110
|
-
* - limit?: 仅返回前 N 条。会话目录名是 YYYYMMDD-HHmmss,字典序=时间序;
|
|
111
|
-
* 按目录名降序逐个解析,收集到 N 个有效会话就停止,避免 /resume 在
|
|
112
|
-
* sessions 目录堆了几百个子目录时全量 JSON.parse;同时不让只有笔记或
|
|
113
|
-
* 快照、没有 session.json 的目录占掉最近 N 条的名额。
|
|
114
|
-
* - 不传 limit 时读全部(向后兼容,供裸 --resume 列全表用)。
|
|
115
|
-
* - 向后兼容:同时扫描旧式 <id>.json 文件(扁平结构),优先读新式目录。
|
|
116
|
-
*/
|
|
18
|
+
/** 列出最近会话,按 createdAt 降序。 */
|
|
117
19
|
export function listSessions(limit) {
|
|
118
|
-
|
|
119
|
-
return [];
|
|
120
|
-
const entries = readdirSync(config.sessionDir, { withFileTypes: true });
|
|
121
|
-
const ids = [];
|
|
122
|
-
for (const e of entries) {
|
|
123
|
-
if (e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name)) {
|
|
124
|
-
ids.push(e.name);
|
|
125
|
-
}
|
|
126
|
-
else if (e.isFile() && e.name.endsWith('.json') && !e.name.endsWith('.snapshots.json')) {
|
|
127
|
-
// 旧式扁平文件:兼容读取
|
|
128
|
-
ids.push(e.name.replace(/\.json$/, ''));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const all = ids.sort().reverse(); // 降序:最新在前
|
|
132
|
-
const maxResults = typeof limit === 'number' ? Math.max(0, limit) : Infinity;
|
|
133
|
-
const out = [];
|
|
134
|
-
for (const id of all) {
|
|
135
|
-
if (out.length >= maxResults)
|
|
136
|
-
break;
|
|
137
|
-
try {
|
|
138
|
-
// 优先新式目录,回退旧式文件
|
|
139
|
-
const newPath = path.join(config.sessionDir, id, 'session.json');
|
|
140
|
-
const oldPath = path.join(config.sessionDir, `${id}.json`);
|
|
141
|
-
const p = existsSync(newPath) ? newPath : oldPath;
|
|
142
|
-
const rec = JSON.parse(readFileSync(p, 'utf8'));
|
|
143
|
-
if (rec && typeof rec.id === 'string') {
|
|
144
|
-
out.push({
|
|
145
|
-
id: rec.id,
|
|
146
|
-
createdAt: rec.createdAt ?? idToIso(rec.id),
|
|
147
|
-
model: rec.model ?? '',
|
|
148
|
-
firstUser: rec.firstUser ?? '',
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// 跳过损坏文件
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return out;
|
|
20
|
+
return defaultSessionStore.list(limit);
|
|
157
21
|
}
|