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
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// rewritten only after real occupancy reaches the shared pressure threshold.
|
|
3
3
|
import { evaluateBudget, scheduleActions, formatReport, DEFAULT_BUDGET_POLICY, } from '../context/budget.js';
|
|
4
4
|
import { chatTools } from '../llm/index.js';
|
|
5
|
-
import {
|
|
6
|
-
import { maybeCompact, contextState } from './compact.js';
|
|
5
|
+
import { defaultCompactionRuntime, maybeCompact, contextState, } from './compact.js';
|
|
7
6
|
import { pruneStaleArtifacts, refreshArtifactFreshness } from '../context/artifacts.js';
|
|
8
7
|
import { pruneSuperseded } from '../context/relevance.js';
|
|
9
8
|
import { createAgeAwareEncodingState } from '../context/age-aware.js';
|
|
@@ -21,8 +20,8 @@ function emptyPressure(report) {
|
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
22
|
/** One scheduler instance is owned by one agent run. */
|
|
24
|
-
export function createBudgetScheduler(state = contextState) {
|
|
25
|
-
const evaluate = (history, step, activeTools, ephemeralTokens) => evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools, ephemeralTokens);
|
|
23
|
+
export function createBudgetScheduler(state = contextState, runtime = defaultCompactionRuntime) {
|
|
24
|
+
const evaluate = (history, step, activeTools, ephemeralTokens) => evaluateBudget(history, runtime.config.contextWindowTokens, step, state.correction, activeTools, ephemeralTokens);
|
|
26
25
|
const scheduler = {
|
|
27
26
|
lastRunLog: null,
|
|
28
27
|
async runStep(history, step, activeTools = chatTools, ephemeralTokens = 0, signal) {
|
|
@@ -35,11 +34,11 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
35
34
|
// A single 80% pressure event owns every history rewrite. Run all enabled
|
|
36
35
|
// low-cost cleanup first, then always compact; do not introduce per-stage
|
|
37
36
|
// thresholds or stop early when one stage happens to cross below 80%.
|
|
38
|
-
if (config.contextRelprune) {
|
|
37
|
+
if (runtime.config.contextRelprune) {
|
|
39
38
|
pressure.superseded = pruneSuperseded(history, report.hotBoundary);
|
|
40
39
|
}
|
|
41
40
|
pressure.staleArtifacts = pruneStaleArtifacts(state, history, report.hotBoundary);
|
|
42
|
-
if (config.contextOptimize) {
|
|
41
|
+
if (runtime.config.contextOptimize) {
|
|
43
42
|
const ageAware = createAgeAwareEncodingState(history);
|
|
44
43
|
pressure.encodedLogsAndSearches = ageAware.sweepPressure(history, report.hotBoundary);
|
|
45
44
|
}
|
|
@@ -50,10 +49,12 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
50
49
|
const actions = scheduleActions(report);
|
|
51
50
|
let compactHistoryCalled = false;
|
|
52
51
|
let historyRebuilt = false;
|
|
52
|
+
let contentMutated = pressure.superseded > 0 || pressure.staleArtifacts > 0 || pressure.encodedLogsAndSearches > 0;
|
|
53
53
|
for (const _action of actions) {
|
|
54
|
-
const result = await maybeCompact(history, report, undefined, state, activeTools, signal);
|
|
54
|
+
const result = await maybeCompact(history, report, undefined, state, activeTools, signal, runtime);
|
|
55
55
|
compactHistoryCalled = true;
|
|
56
56
|
historyRebuilt ||= result?.historyRebuilt === true;
|
|
57
|
+
contentMutated ||= result?.compacted === true;
|
|
57
58
|
}
|
|
58
59
|
const log = {
|
|
59
60
|
step,
|
|
@@ -61,6 +62,7 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
61
62
|
actions,
|
|
62
63
|
pressure,
|
|
63
64
|
compactHistoryCalled,
|
|
65
|
+
historyMutation: historyRebuilt ? 'rebuild' : contentMutated ? 'content' : 'none',
|
|
64
66
|
ts: Date.now(),
|
|
65
67
|
};
|
|
66
68
|
scheduler.lastRunLog = log;
|
|
@@ -70,28 +72,35 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
70
72
|
};
|
|
71
73
|
return scheduler;
|
|
72
74
|
}
|
|
73
|
-
export async function runScheduler(history, step, state = contextState, activeTools = chatTools) {
|
|
74
|
-
return createBudgetScheduler(state).runStep(history, step, activeTools);
|
|
75
|
+
export async function runScheduler(history, step, state = contextState, activeTools = chatTools, runtime = defaultCompactionRuntime) {
|
|
76
|
+
return createBudgetScheduler(state, runtime).runStep(history, step, activeTools);
|
|
75
77
|
}
|
|
76
78
|
/** User-requested compaction bypasses automatic pressure gating. */
|
|
77
79
|
export async function manualCompact(history, focus, opts) {
|
|
78
80
|
const signal = opts?.signal;
|
|
79
|
-
|
|
81
|
+
const runtime = opts?.runtime ?? defaultCompactionRuntime;
|
|
82
|
+
const state = opts?.contextState ?? contextState;
|
|
83
|
+
const activeTools = opts?.activeTools ?? chatTools;
|
|
84
|
+
if (runtime.config.contextBudget === false) {
|
|
80
85
|
const result = await import('./compact.js').then(({ compactHistory }) => compactHistory(history, {
|
|
81
|
-
window: config.contextWindowTokens,
|
|
86
|
+
window: runtime.config.contextWindowTokens,
|
|
82
87
|
threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
|
|
83
88
|
focus,
|
|
84
89
|
manual: true,
|
|
85
90
|
force: opts?.force,
|
|
91
|
+
tools: activeTools,
|
|
92
|
+
contextState: state,
|
|
93
|
+
runtime,
|
|
86
94
|
signal,
|
|
87
95
|
}));
|
|
88
|
-
const report = evaluateBudget(history, config.contextWindowTokens, -1,
|
|
96
|
+
const report = evaluateBudget(history, runtime.config.contextWindowTokens, -1, state.correction, activeTools);
|
|
89
97
|
const log = {
|
|
90
98
|
step: -1,
|
|
91
99
|
report,
|
|
92
100
|
actions: [{ kind: 'compact_history', focus }],
|
|
93
101
|
pressure: emptyPressure(report),
|
|
94
102
|
compactHistoryCalled: true,
|
|
103
|
+
historyMutation: result.historyRebuilt ? 'rebuild' : result.compacted ? 'content' : 'none',
|
|
95
104
|
ts: Date.now(),
|
|
96
105
|
compactDetail: {
|
|
97
106
|
reason: result.reason,
|
|
@@ -102,10 +111,10 @@ export async function manualCompact(history, focus, opts) {
|
|
|
102
111
|
focus,
|
|
103
112
|
},
|
|
104
113
|
};
|
|
105
|
-
|
|
114
|
+
state.schedulerLog = log;
|
|
106
115
|
return log;
|
|
107
116
|
}
|
|
108
|
-
const report = evaluateBudget(history, config.contextWindowTokens, -1,
|
|
117
|
+
const report = evaluateBudget(history, runtime.config.contextWindowTokens, -1, state.correction, activeTools);
|
|
109
118
|
let actions = scheduleActions(report);
|
|
110
119
|
if (!actions.some((action) => action.kind === 'compact_history')) {
|
|
111
120
|
actions = [...actions, { kind: 'compact_history', focus }];
|
|
@@ -114,6 +123,7 @@ export async function manualCompact(history, focus, opts) {
|
|
|
114
123
|
actions = actions.map((action) => (action.kind === 'compact_history' ? { ...action, focus } : action));
|
|
115
124
|
}
|
|
116
125
|
let compactHistoryCalled = false;
|
|
126
|
+
let historyMutation = 'none';
|
|
117
127
|
let compactDetail;
|
|
118
128
|
for (const action of actions) {
|
|
119
129
|
if (action.kind !== 'compact_history')
|
|
@@ -122,9 +132,10 @@ export async function manualCompact(history, focus, opts) {
|
|
|
122
132
|
manual: true,
|
|
123
133
|
force: opts?.force,
|
|
124
134
|
focus: action.focus,
|
|
125
|
-
},
|
|
135
|
+
}, state, activeTools, signal, runtime);
|
|
126
136
|
compactHistoryCalled = true;
|
|
127
137
|
if (result) {
|
|
138
|
+
historyMutation = result.historyRebuilt ? 'rebuild' : result.compacted ? 'content' : historyMutation;
|
|
128
139
|
compactDetail = {
|
|
129
140
|
reason: result.reason,
|
|
130
141
|
estimateBefore: result.estimateBefore,
|
|
@@ -141,10 +152,11 @@ export async function manualCompact(history, focus, opts) {
|
|
|
141
152
|
actions,
|
|
142
153
|
pressure: emptyPressure(report),
|
|
143
154
|
compactHistoryCalled,
|
|
155
|
+
historyMutation,
|
|
144
156
|
ts: Date.now(),
|
|
145
157
|
compactDetail,
|
|
146
158
|
};
|
|
147
|
-
|
|
159
|
+
state.schedulerLog = log;
|
|
148
160
|
return log;
|
|
149
161
|
}
|
|
150
162
|
export { evaluateBudget, scheduleActions, formatReport };
|
package/dist/session/state.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
// Default-runtime session identity stays free of config/store imports to avoid initialization cycles.
|
|
3
|
+
let defaultCurrentSessionId;
|
|
4
|
+
const activeSessionIdProviders = new AsyncLocalStorage();
|
|
5
|
+
/** 获取当前异步 runtime 的会话 ID;无 scope 时读取默认进程 runtime。 */
|
|
6
6
|
export function getCurrentSessionId() {
|
|
7
|
-
return
|
|
7
|
+
return activeSessionIdProviders.getStore()?.() ?? defaultCurrentSessionId;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*/
|
|
9
|
+
/** SessionStore 默认兼容实例专用:绕过异步 scope,避免 provider 自递归。 */
|
|
10
|
+
export function getDefaultCurrentSessionId() {
|
|
11
|
+
return defaultCurrentSessionId;
|
|
12
|
+
}
|
|
13
|
+
/** 设置默认进程 runtime 的当前活跃会话 ID。 */
|
|
14
14
|
export function setCurrentSessionId(id, cwd) {
|
|
15
|
-
|
|
15
|
+
defaultCurrentSessionId = id;
|
|
16
16
|
void cwd;
|
|
17
17
|
}
|
|
18
|
+
/** 让 notes/config 等旧读取入口在异步 runtime 树内看到实例会话身份。 */
|
|
19
|
+
export function withCurrentSessionIdProvider(provider, run) {
|
|
20
|
+
return activeSessionIdProviders.run(provider, run);
|
|
21
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { config, getActiveModel } from '../config/index.js';
|
|
6
|
+
import { isToolRouteGroupName } from '../config/profiles.js';
|
|
7
|
+
import { truncateDisplay } from '../ui/render.js';
|
|
8
|
+
import { getDefaultCurrentSessionId, setCurrentSessionId as setDefaultCurrentSessionId, withCurrentSessionIdProvider, } from './state.js';
|
|
9
|
+
/** 新会话 id: 时间前缀保持可排序,毫秒与随机段避免同进程/跨进程碰撞。 */
|
|
10
|
+
function createTimestampId() {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
const p = (n, width = 2) => String(n).padStart(width, '0');
|
|
13
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}-${randomUUID().slice(0, 8)}`;
|
|
14
|
+
}
|
|
15
|
+
/** 时间前缀 → ISO 字符串;兼容旧秒级 ID,解析失败回退原 id。 */
|
|
16
|
+
function idToIso(id) {
|
|
17
|
+
const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(?:-(\d{3})-[a-f0-9]{8})?$/.exec(id);
|
|
18
|
+
if (!m)
|
|
19
|
+
return id;
|
|
20
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}${m[7] ? `.${m[7]}` : ''}`;
|
|
21
|
+
}
|
|
22
|
+
function toText(content) {
|
|
23
|
+
if (content == null)
|
|
24
|
+
return '';
|
|
25
|
+
if (typeof content === 'string')
|
|
26
|
+
return content;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(content);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return String(content);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function firstUserOf(history) {
|
|
35
|
+
for (const message of history) {
|
|
36
|
+
if (message.role !== 'user')
|
|
37
|
+
continue;
|
|
38
|
+
const text = toText(message.content)
|
|
39
|
+
.replace(/\n/g, ' ')
|
|
40
|
+
.trim();
|
|
41
|
+
return truncateDisplay(text, 40);
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Runtime-local session persistence and identity.
|
|
47
|
+
*
|
|
48
|
+
* Disk format remains compatible with the historical helpers: the current format is
|
|
49
|
+
* `<sessionsRoot>/<id>/session.json`, with `<sessionsRoot>/<id>.json` as a read fallback.
|
|
50
|
+
*/
|
|
51
|
+
export class SessionStore {
|
|
52
|
+
workspaceRoot;
|
|
53
|
+
sessionsRootProvider;
|
|
54
|
+
getModel;
|
|
55
|
+
currentSessionIdProvider;
|
|
56
|
+
currentSessionIdSetter;
|
|
57
|
+
currentSessionId;
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.workspaceRoot = path.resolve(options.workspaceRoot ?? process.cwd());
|
|
60
|
+
if (typeof options.sessionsRoot === 'function') {
|
|
61
|
+
const provider = options.sessionsRoot;
|
|
62
|
+
this.sessionsRootProvider = () => path.resolve(provider());
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const fixedRoot = path.resolve(options.sessionsRoot ?? config.sessionDir);
|
|
66
|
+
this.sessionsRootProvider = () => fixedRoot;
|
|
67
|
+
}
|
|
68
|
+
this.getModel = options.getModel ?? getActiveModel;
|
|
69
|
+
this.currentSessionIdProvider = options.getCurrentSessionId;
|
|
70
|
+
this.currentSessionIdSetter = options.setCurrentSessionId;
|
|
71
|
+
}
|
|
72
|
+
get sessionsRoot() {
|
|
73
|
+
return this.sessionsRootProvider();
|
|
74
|
+
}
|
|
75
|
+
sessionDir() {
|
|
76
|
+
const root = this.sessionsRoot;
|
|
77
|
+
mkdirSync(root, { recursive: true });
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
createId() {
|
|
81
|
+
return createTimestampId();
|
|
82
|
+
}
|
|
83
|
+
getCurrentSessionId() {
|
|
84
|
+
return this.currentSessionIdProvider?.() ?? this.currentSessionId;
|
|
85
|
+
}
|
|
86
|
+
setCurrentSessionId(id) {
|
|
87
|
+
if (this.currentSessionIdSetter)
|
|
88
|
+
this.currentSessionIdSetter(id);
|
|
89
|
+
else
|
|
90
|
+
this.currentSessionId = id;
|
|
91
|
+
}
|
|
92
|
+
sessionPath(id) {
|
|
93
|
+
return path.join(this.sessionsRoot, id, 'session.json');
|
|
94
|
+
}
|
|
95
|
+
artifactPath(id, filename) {
|
|
96
|
+
return path.join(this.sessionsRoot, id, filename);
|
|
97
|
+
}
|
|
98
|
+
appendTrace(id, value) {
|
|
99
|
+
try {
|
|
100
|
+
const dir = path.join(this.sessionsRoot, id);
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
103
|
+
const tracePath = path.join(dir, 'trace.jsonl');
|
|
104
|
+
writeFileSync(tracePath, line, { encoding: 'utf8', flag: 'a' });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Observability is best-effort and cannot block coding work.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
save(history, id, queryHistory = [], lastToolGroups = []) {
|
|
111
|
+
const meta = {
|
|
112
|
+
id,
|
|
113
|
+
createdAt: idToIso(id),
|
|
114
|
+
model: this.getModel(),
|
|
115
|
+
firstUser: history.length > 1
|
|
116
|
+
? firstUserOf(history)
|
|
117
|
+
: truncateDisplay((queryHistory[0] ?? '').replace(/\n/g, ' ').trim(), 40),
|
|
118
|
+
};
|
|
119
|
+
const currentPath = this.sessionPath(id);
|
|
120
|
+
const legacyPath = path.join(this.sessionsRoot, `${id}.json`);
|
|
121
|
+
if (history.length <= 1 && queryHistory.length === 0 && !existsSync(currentPath) && !existsSync(legacyPath)) {
|
|
122
|
+
return meta;
|
|
123
|
+
}
|
|
124
|
+
mkdirSync(path.join(this.sessionsRoot, id), { recursive: true });
|
|
125
|
+
const record = {
|
|
126
|
+
...meta,
|
|
127
|
+
history,
|
|
128
|
+
queryHistory: [...queryHistory],
|
|
129
|
+
lastToolGroups: [...lastToolGroups],
|
|
130
|
+
};
|
|
131
|
+
writeFileSync(currentPath, JSON.stringify(record), 'utf8');
|
|
132
|
+
if (existsSync(legacyPath))
|
|
133
|
+
unlinkSync(legacyPath);
|
|
134
|
+
return meta;
|
|
135
|
+
}
|
|
136
|
+
load(id) {
|
|
137
|
+
const currentPath = this.sessionPath(id);
|
|
138
|
+
const legacyPath = path.join(this.sessionsRoot, `${id}.json`);
|
|
139
|
+
const source = existsSync(currentPath) ? currentPath : legacyPath;
|
|
140
|
+
if (!existsSync(source))
|
|
141
|
+
return null;
|
|
142
|
+
try {
|
|
143
|
+
const rec = JSON.parse(readFileSync(source, 'utf8'));
|
|
144
|
+
if (!rec || !Array.isArray(rec.history))
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
id: rec.id,
|
|
148
|
+
createdAt: rec.createdAt ?? idToIso(rec.id ?? id),
|
|
149
|
+
model: rec.model ?? '',
|
|
150
|
+
firstUser: rec.firstUser ?? '',
|
|
151
|
+
history: rec.history,
|
|
152
|
+
queryHistory: Array.isArray(rec.queryHistory)
|
|
153
|
+
? rec.queryHistory.filter((query) => typeof query === 'string')
|
|
154
|
+
: undefined,
|
|
155
|
+
lastToolGroups: Array.isArray(rec.lastToolGroups) ? rec.lastToolGroups.filter(isToolRouteGroupName) : undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
list(limit) {
|
|
163
|
+
const root = this.sessionsRoot;
|
|
164
|
+
if (!existsSync(root))
|
|
165
|
+
return [];
|
|
166
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
167
|
+
const ids = [];
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (entry.isDirectory() && /^\d{8}-\d{6}(?:-\d{3}-[a-f0-9]{8})?$/.test(entry.name)) {
|
|
170
|
+
ids.push(entry.name);
|
|
171
|
+
}
|
|
172
|
+
else if (entry.isFile() && entry.name.endsWith('.json') && !entry.name.endsWith('.snapshots.json')) {
|
|
173
|
+
ids.push(entry.name.replace(/\.json$/, ''));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const maxResults = typeof limit === 'number' ? Math.max(0, limit) : Infinity;
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const id of ids.sort().reverse()) {
|
|
179
|
+
if (out.length >= maxResults)
|
|
180
|
+
break;
|
|
181
|
+
const currentPath = this.sessionPath(id);
|
|
182
|
+
const legacyPath = path.join(root, `${id}.json`);
|
|
183
|
+
const source = existsSync(currentPath) ? currentPath : legacyPath;
|
|
184
|
+
try {
|
|
185
|
+
const rec = JSON.parse(readFileSync(source, 'utf8'));
|
|
186
|
+
if (!rec || typeof rec.id !== 'string')
|
|
187
|
+
continue;
|
|
188
|
+
out.push({
|
|
189
|
+
id: rec.id,
|
|
190
|
+
createdAt: rec.createdAt ?? idToIso(rec.id),
|
|
191
|
+
model: rec.model ?? '',
|
|
192
|
+
firstUser: rec.firstUser ?? '',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// 跳过损坏文件。
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** 旧函数 API 的进程级兼容实例;session root 每次读取 config,保留测试和运行时切换语义。 */
|
|
203
|
+
export const defaultSessionStore = new SessionStore({
|
|
204
|
+
sessionsRoot: () => config.sessionDir,
|
|
205
|
+
workspaceRoot: process.cwd(),
|
|
206
|
+
getModel: getActiveModel,
|
|
207
|
+
getCurrentSessionId: getDefaultCurrentSessionId,
|
|
208
|
+
setCurrentSessionId: (id) => setDefaultCurrentSessionId(id, process.cwd()),
|
|
209
|
+
});
|
|
210
|
+
const activeSessionStores = new AsyncLocalStorage();
|
|
211
|
+
/** 当前异步 runtime 树使用的 session store;无 scope 时回退默认兼容实例。 */
|
|
212
|
+
export function getActiveSessionStore() {
|
|
213
|
+
return activeSessionStores.getStore() ?? defaultSessionStore;
|
|
214
|
+
}
|
|
215
|
+
/** 让旧 session/trace 入口在异步 runtime 树内自动使用对应实例。 */
|
|
216
|
+
export function withSessionStore(store, run) {
|
|
217
|
+
return activeSessionStores.run(store, () => withCurrentSessionIdProvider(() => store.getCurrentSessionId(), run));
|
|
218
|
+
}
|
package/dist/session/trace.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { config } from '../config/index.js';
|
|
5
2
|
import { getCurrentTurnId } from '../rollback/index.js';
|
|
6
|
-
import {
|
|
3
|
+
import { getActiveSessionStore } from './store.js';
|
|
7
4
|
export function createTraceEvent(input) {
|
|
8
5
|
return {
|
|
9
6
|
schemaVersion: 1,
|
|
@@ -13,32 +10,25 @@ export function createTraceEvent(input) {
|
|
|
13
10
|
};
|
|
14
11
|
}
|
|
15
12
|
function appendTraceLine(sessionId, value) {
|
|
16
|
-
|
|
17
|
-
const dir = path.join(config.sessionDir, sessionId);
|
|
18
|
-
mkdirSync(dir, { recursive: true });
|
|
19
|
-
appendFileSync(path.join(dir, 'trace.jsonl'), `${JSON.stringify(value)}\n`, 'utf8');
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// Observability is best-effort and cannot block coding work.
|
|
23
|
-
}
|
|
13
|
+
getActiveSessionStore().appendTrace(sessionId, value);
|
|
24
14
|
}
|
|
25
15
|
/** Persists a typed event in the current session's append-only black-box log. */
|
|
26
16
|
export function appendCurrentSessionTraceEvent(event) {
|
|
27
|
-
const sessionId = getCurrentSessionId();
|
|
17
|
+
const sessionId = getActiveSessionStore().getCurrentSessionId();
|
|
28
18
|
if (!sessionId)
|
|
29
19
|
return;
|
|
30
20
|
appendTraceLine(sessionId, { ...event, sessionId });
|
|
31
21
|
}
|
|
32
22
|
/** Records events initiated outside runAgentCore, such as Ctrl+C, /compact, and /rollback. */
|
|
33
23
|
export function appendCurrentSessionRuntimeEvent(type, data, turnId = getCurrentTurnId()) {
|
|
34
|
-
const sessionId = getCurrentSessionId();
|
|
24
|
+
const sessionId = getActiveSessionStore().getCurrentSessionId();
|
|
35
25
|
if (!sessionId)
|
|
36
26
|
return;
|
|
37
27
|
appendTraceLine(sessionId, createTraceEvent({ sessionId, turnId, type, data }));
|
|
38
28
|
}
|
|
39
29
|
/** Legacy turn-summary sink retained for API compatibility. New production code writes events. */
|
|
40
30
|
export function appendCurrentSessionTrace(trace) {
|
|
41
|
-
const sessionId = getCurrentSessionId();
|
|
31
|
+
const sessionId = getActiveSessionStore().getCurrentSessionId();
|
|
42
32
|
if (!sessionId)
|
|
43
33
|
return;
|
|
44
34
|
appendTraceLine(sessionId, { ...trace, sessionId });
|
package/dist/tools/policy.js
CHANGED
|
@@ -5,16 +5,16 @@ const clampConfidence = (value) => (Number.isFinite(value) ? Math.max(0, Math.mi
|
|
|
5
5
|
function envGateAllows(name) {
|
|
6
6
|
return !name || process.env[name] !== 'false';
|
|
7
7
|
}
|
|
8
|
-
function registeredNames() {
|
|
9
|
-
return
|
|
8
|
+
function registeredNames(catalog = tools) {
|
|
9
|
+
return catalog.map((tool) => tool.name);
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
export function getAvailableToolRouteGroups() {
|
|
13
|
-
const names = registeredNames();
|
|
11
|
+
/** 当前 runtime 真正可路由的簇;旧 env=false 只作为硬 veto,不再强制把簇常驻 schema。 */
|
|
12
|
+
export function getAvailableToolRouteGroups(catalog = tools, gateAllows = envGateAllows) {
|
|
13
|
+
const names = registeredNames(catalog);
|
|
14
14
|
const registered = new Set(names);
|
|
15
15
|
return TOOL_ROUTE_GROUP_NAMES.filter((group) => {
|
|
16
16
|
const definition = TOOL_ROUTE_GROUPS[group];
|
|
17
|
-
if (!
|
|
17
|
+
if (!gateAllows(definition.gateEnv))
|
|
18
18
|
return false;
|
|
19
19
|
const groupNames = getToolRouteGroupNames(group, names);
|
|
20
20
|
if (group === 'mcp')
|
|
@@ -22,8 +22,8 @@ export function getAvailableToolRouteGroups() {
|
|
|
22
22
|
return groupNames.length > 0 && groupNames.every((name) => registered.has(name));
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
export function toolRouteCatalog(groups = getAvailableToolRouteGroups()) {
|
|
26
|
-
const names = registeredNames();
|
|
25
|
+
export function toolRouteCatalog(groups = getAvailableToolRouteGroups(), catalog = tools) {
|
|
26
|
+
const names = registeredNames(catalog);
|
|
27
27
|
return groups
|
|
28
28
|
.map((group) => {
|
|
29
29
|
const members = getToolRouteGroupNames(group, names);
|
|
@@ -31,8 +31,8 @@ export function toolRouteCatalog(groups = getAvailableToolRouteGroups()) {
|
|
|
31
31
|
})
|
|
32
32
|
.join('\n');
|
|
33
33
|
}
|
|
34
|
-
export function getToolChatSchema(name) {
|
|
35
|
-
const tool =
|
|
34
|
+
export function getToolChatSchema(name, catalog = tools) {
|
|
35
|
+
const tool = catalog.find((candidate) => candidate.name === name);
|
|
36
36
|
if (!tool)
|
|
37
37
|
return null;
|
|
38
38
|
return {
|
|
@@ -87,12 +87,16 @@ export class ToolPolicyController {
|
|
|
87
87
|
confidence;
|
|
88
88
|
autoCache = null;
|
|
89
89
|
planCache = null;
|
|
90
|
+
catalog;
|
|
91
|
+
gateAllows;
|
|
90
92
|
constructor(init = {}) {
|
|
93
|
+
this.catalog = init.tools ?? tools;
|
|
94
|
+
this.gateAllows = init.gateAllows ?? envGateAllows;
|
|
91
95
|
this.id = init.id ?? `route-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
92
96
|
this.maxExpansions = Math.max(0, init.maxExpansions ?? 3);
|
|
93
97
|
this.reason = init.reason?.trim() || 'LLM router selected common tools only.';
|
|
94
98
|
this.confidence = clampConfidence(init.confidence ?? 0);
|
|
95
|
-
const available = new Set(getAvailableToolRouteGroups());
|
|
99
|
+
const available = new Set(getAvailableToolRouteGroups(this.catalog, this.gateAllows));
|
|
96
100
|
const requested = process.env.MOCODE_TOOL_POLICY === 'full' ? available : new Set(init.groups ?? []);
|
|
97
101
|
for (const group of requested) {
|
|
98
102
|
if (available.has(group))
|
|
@@ -106,7 +110,7 @@ export class ToolPolicyController {
|
|
|
106
110
|
return this.expansionCount < this.maxExpansions && this.remainingGroups().length > 0;
|
|
107
111
|
}
|
|
108
112
|
remainingGroups() {
|
|
109
|
-
const available = new Set(getAvailableToolRouteGroups());
|
|
113
|
+
const available = new Set(getAvailableToolRouteGroups(this.catalog, this.gateAllows));
|
|
110
114
|
return TOOL_ROUTE_GROUP_NAMES.filter((group) => available.has(group) && !this.selected.has(group));
|
|
111
115
|
}
|
|
112
116
|
snapshot(planMode = false) {
|
|
@@ -126,7 +130,7 @@ export class ToolPolicyController {
|
|
|
126
130
|
const remaining = this.canExpand ? this.remainingGroups() : [];
|
|
127
131
|
if (remaining.length > 0)
|
|
128
132
|
append(ADD_TOOL_GROUPS_TOOL_NAME);
|
|
129
|
-
const registered = registeredNames();
|
|
133
|
+
const registered = registeredNames(this.catalog);
|
|
130
134
|
for (const group of TOOL_ROUTE_GROUP_NAMES) {
|
|
131
135
|
if (!this.selected.has(group))
|
|
132
136
|
continue;
|
|
@@ -139,7 +143,7 @@ export class ToolPolicyController {
|
|
|
139
143
|
const chatTools = visibleNames.flatMap((name) => {
|
|
140
144
|
if (name === ADD_TOOL_GROUPS_TOOL_NAME)
|
|
141
145
|
return [addToolGroupsSchema(remaining)];
|
|
142
|
-
const schema = getToolChatSchema(name);
|
|
146
|
+
const schema = getToolChatSchema(name, this.catalog);
|
|
143
147
|
return schema ? [schema] : [];
|
|
144
148
|
});
|
|
145
149
|
const snapshot = {
|
|
@@ -168,7 +172,7 @@ export class ToolPolicyController {
|
|
|
168
172
|
snapshot: this.snapshot(false),
|
|
169
173
|
};
|
|
170
174
|
}
|
|
171
|
-
const available = new Set(getAvailableToolRouteGroups());
|
|
175
|
+
const available = new Set(getAvailableToolRouteGroups(this.catalog, this.gateAllows));
|
|
172
176
|
for (const value of rawGroups) {
|
|
173
177
|
if (!isToolRouteGroupName(value)) {
|
|
174
178
|
rejected.push(`${String(value)}: unknown group`);
|