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
|
@@ -1,118 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* runAgentCore 原本把 emitTrace / addUsage / reportLive 写成函数内闭包,捕获
|
|
5
|
-
* traceSessionId / traceTurnId / currentTraceStep / abortTraced / turnUsage 等一坨可变局部量。
|
|
6
|
-
* 这里把这坨状态收敛进一个对象,runAgentCore 持有实例并调用方法,行为与字节级事件 payload 不变。
|
|
7
|
-
*
|
|
8
|
-
* 设计约束:
|
|
9
|
-
* - emit() 的 currentTraceStep 三元逻辑逐字保留——trace 事件落盘格式是观测契约,不能漂移。
|
|
10
|
-
* - addUsage() 的逐字段累加(含 cacheCreationTokens 的 ?? 0 兜底)逐字保留。
|
|
11
|
-
* - reportLive() 的 onLiveUsage 上报警文结构与 turnUsage 闭包累加口径一致(实时 chip 显示)。
|
|
12
|
-
* - turnId 类型为 number | undefined:traceTurnId 兜底链可能产出 undefined(--no-session)。
|
|
13
|
-
*/
|
|
14
|
-
import { createTraceEvent } from '../session/index.js';
|
|
15
|
-
export class TurnTraceState {
|
|
16
|
-
sessionId;
|
|
17
|
-
turnId;
|
|
18
|
-
/** 当前步序号;循环每步开头赋值,finally 归 undefined。stepId 由此拼。 */
|
|
19
|
-
currentTraceStep;
|
|
20
|
-
/** abort 事件只记一次的幂等旗标。 */
|
|
21
|
-
abortTraced = false;
|
|
22
|
-
/** 本轮工具调用总数(turn_end 摘要用)。 */
|
|
23
|
-
toolCallCount = 0;
|
|
24
|
-
/** 本轮 token 累计;未开启 include_usage 或全失败时为 undefined。 */
|
|
25
|
-
turnUsage;
|
|
1
|
+
class CallbackTraceSink {
|
|
2
|
+
implementation;
|
|
26
3
|
onTraceEvent;
|
|
27
|
-
constructor(
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
this.onTraceEvent = init.onTraceEvent;
|
|
4
|
+
constructor(implementation, onTraceEvent) {
|
|
5
|
+
this.implementation = implementation;
|
|
6
|
+
this.onTraceEvent = onTraceEvent;
|
|
31
7
|
}
|
|
32
|
-
|
|
33
|
-
emit(type, data = {}, ids = {}) {
|
|
34
|
-
const turnId = this.turnId;
|
|
35
|
-
if (turnId === undefined)
|
|
36
|
-
return; // 无会话上下文(--no-session)不产生事件,与原闭包行为一致
|
|
8
|
+
emit(event) {
|
|
37
9
|
try {
|
|
38
|
-
this.onTraceEvent?.(
|
|
39
|
-
sessionId: this.sessionId,
|
|
40
|
-
turnId,
|
|
41
|
-
type,
|
|
42
|
-
...(this.currentTraceStep === undefined
|
|
43
|
-
? {}
|
|
44
|
-
: {
|
|
45
|
-
step: this.currentTraceStep,
|
|
46
|
-
stepId: `${turnId}:step:${this.currentTraceStep}`,
|
|
47
|
-
}),
|
|
48
|
-
...ids,
|
|
49
|
-
data,
|
|
50
|
-
}));
|
|
10
|
+
this.onTraceEvent?.(event);
|
|
51
11
|
}
|
|
52
12
|
catch {
|
|
53
13
|
// Trace is best-effort and must never alter execution.
|
|
54
14
|
}
|
|
55
15
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
16
|
+
}
|
|
17
|
+
class TurnUsageMeter {
|
|
18
|
+
implementation;
|
|
19
|
+
usage;
|
|
20
|
+
constructor(implementation) {
|
|
21
|
+
this.implementation = implementation;
|
|
22
|
+
}
|
|
23
|
+
add(next) {
|
|
24
|
+
if (!next)
|
|
59
25
|
return;
|
|
60
|
-
this.
|
|
26
|
+
this.usage = this.usage
|
|
61
27
|
? {
|
|
62
|
-
promptTokens: this.
|
|
63
|
-
completionTokens: this.
|
|
64
|
-
totalTokens: this.
|
|
65
|
-
cachedTokens: this.
|
|
66
|
-
cacheCreationTokens: (this.
|
|
67
|
-
reasoningTokens: this.
|
|
28
|
+
promptTokens: this.usage.promptTokens + next.promptTokens,
|
|
29
|
+
completionTokens: this.usage.completionTokens + next.completionTokens,
|
|
30
|
+
totalTokens: this.usage.totalTokens + next.totalTokens,
|
|
31
|
+
cachedTokens: this.usage.cachedTokens + next.cachedTokens,
|
|
32
|
+
cacheCreationTokens: (this.usage.cacheCreationTokens ?? 0) + (next.cacheCreationTokens ?? 0),
|
|
33
|
+
reasoningTokens: this.usage.reasoningTokens + next.reasoningTokens,
|
|
68
34
|
}
|
|
69
|
-
:
|
|
35
|
+
: next;
|
|
70
36
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* turnUsage 在本对象内被 addUsage 原地累加,这里每次读最新值,口径与轮末摘要一致。
|
|
74
|
-
*/
|
|
75
|
-
reportLive(hooks, stepPromptEst, lastStepPromptTokens, providerCacheSeen, p) {
|
|
76
|
-
// 当前步 prompt:末尾 usage chunk 到达后用实测,流式期间用估算(含校准)。
|
|
77
|
-
// 当前步 cache 命中同理:上报即用实测;流式期间按前缀缓存估算 ≈ 上一步实测 prompt
|
|
78
|
-
// (当前 prompt 总含其为前缀),不超过当前步 prompt;后端从不报 cache 时不估算。
|
|
79
|
-
// 口径与轮末摘要一致:chip ↑ 显计费 prompt(裸 - cached),↓/↻ 同。
|
|
80
|
-
const curPrompt = p.promptTokens ?? stepPromptEst;
|
|
81
|
-
const curCached = p.cachedTokens ?? (providerCacheSeen ? Math.min(lastStepPromptTokens, curPrompt) : 0);
|
|
82
|
-
hooks.onLiveUsage?.({
|
|
83
|
-
promptTokens: (this.turnUsage?.promptTokens ?? 0) + curPrompt,
|
|
84
|
-
completionTokens: (this.turnUsage?.completionTokens ?? 0) + p.completionTokens,
|
|
85
|
-
totalTokens: (this.turnUsage?.totalTokens ?? 0) + curPrompt + p.completionTokens,
|
|
86
|
-
cachedTokens: (this.turnUsage?.cachedTokens ?? 0) + curCached,
|
|
87
|
-
});
|
|
37
|
+
snapshot() {
|
|
38
|
+
return this.usage;
|
|
88
39
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
40
|
+
}
|
|
41
|
+
export class RunCancellationLifecycle {
|
|
42
|
+
implementation;
|
|
43
|
+
init;
|
|
44
|
+
savedHistory;
|
|
45
|
+
observed = false;
|
|
46
|
+
constructor(implementation, init) {
|
|
47
|
+
this.implementation = implementation;
|
|
48
|
+
this.init = init;
|
|
49
|
+
}
|
|
50
|
+
checkpoint() {
|
|
51
|
+
this.savedHistory = this.init.historyManager.createCheckpoint();
|
|
52
|
+
}
|
|
53
|
+
restore() {
|
|
54
|
+
if (!this.observed) {
|
|
55
|
+
this.init.onObserved();
|
|
56
|
+
this.observed = true;
|
|
98
57
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
58
|
+
this.init.onAbort();
|
|
59
|
+
if (!this.savedHistory)
|
|
60
|
+
throw new Error('Cancellation restore requires an initialized history checkpoint.');
|
|
61
|
+
this.init.historyManager.restore(this.savedHistory);
|
|
62
|
+
this.init.onHistoryRestored();
|
|
63
|
+
this.init.restoreMode();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
class LegacyTraceSink extends CallbackTraceSink {
|
|
67
|
+
constructor(init) {
|
|
68
|
+
super('legacy', init.onTraceEvent);
|
|
103
69
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
buildAbortedResult(mutation) {
|
|
110
|
-
return {
|
|
111
|
-
completed: false,
|
|
112
|
-
terminationReason: 'aborted',
|
|
113
|
-
finalText: null,
|
|
114
|
-
usage: this.turnUsage,
|
|
115
|
-
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
116
|
-
};
|
|
70
|
+
}
|
|
71
|
+
class StagedTraceSink extends CallbackTraceSink {
|
|
72
|
+
constructor(init) {
|
|
73
|
+
super('staged', init.onTraceEvent);
|
|
117
74
|
}
|
|
118
75
|
}
|
|
76
|
+
class LegacyUsageMeter extends TurnUsageMeter {
|
|
77
|
+
constructor() {
|
|
78
|
+
super('legacy');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
class StagedUsageMeter extends TurnUsageMeter {
|
|
82
|
+
constructor() {
|
|
83
|
+
super('staged');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
class LegacyCancellationLifecycle extends RunCancellationLifecycle {
|
|
87
|
+
constructor(init) {
|
|
88
|
+
super('legacy', init);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class StagedCancellationLifecycle extends RunCancellationLifecycle {
|
|
92
|
+
constructor(init) {
|
|
93
|
+
super('staged', init);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Separate factories preserve a per-stage rollback seam while sharing the frozen protocol implementation. */
|
|
97
|
+
export function createLegacyTraceSink(init) {
|
|
98
|
+
return new LegacyTraceSink(init);
|
|
99
|
+
}
|
|
100
|
+
export function createStagedTraceSink(init) {
|
|
101
|
+
return new StagedTraceSink(init);
|
|
102
|
+
}
|
|
103
|
+
export function createLegacyUsageMeter() {
|
|
104
|
+
return new LegacyUsageMeter();
|
|
105
|
+
}
|
|
106
|
+
export function createStagedUsageMeter() {
|
|
107
|
+
return new StagedUsageMeter();
|
|
108
|
+
}
|
|
109
|
+
export function createLegacyCancellationLifecycle(init) {
|
|
110
|
+
return new LegacyCancellationLifecycle(init);
|
|
111
|
+
}
|
|
112
|
+
export function createStagedCancellationLifecycle(init) {
|
|
113
|
+
return new StagedCancellationLifecycle(init);
|
|
114
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createTraceEvent } from '../session/index.js';
|
|
2
|
+
import { createLegacyCancellationLifecycle, createLegacyTraceSink, createLegacyUsageMeter, createStagedCancellationLifecycle, createStagedTraceSink, createStagedUsageMeter, } from './trace-state.js';
|
|
3
|
+
/** Owns turn-scoped trace, usage, cancellation and outer-finally state without changing their observable order. */
|
|
4
|
+
export function createTurnLifecycle(opts, ctx, stages, savedMode) {
|
|
5
|
+
const startedAt = Date.now();
|
|
6
|
+
const traceSessionId = opts.traceContext?.sessionId ?? ctx.getCurrentSessionId() ?? `ephemeral-${process.pid}`;
|
|
7
|
+
const traceTurnId = opts.traceContext?.turnId ?? ctx.getCurrentTurnId();
|
|
8
|
+
const traceSink = stages.trace.implementation === 'staged'
|
|
9
|
+
? createStagedTraceSink({ onTraceEvent: opts.onTraceEvent })
|
|
10
|
+
: createLegacyTraceSink({ onTraceEvent: opts.onTraceEvent });
|
|
11
|
+
const usageMeter = stages.usage.implementation === 'staged' ? createStagedUsageMeter() : createLegacyUsageMeter();
|
|
12
|
+
let currentTraceStep;
|
|
13
|
+
let toolCallCount = 0;
|
|
14
|
+
let done = false;
|
|
15
|
+
let traceStatus = 'error';
|
|
16
|
+
const emitTrace = (type, data = {}, ids = {}) => {
|
|
17
|
+
if (traceTurnId === undefined)
|
|
18
|
+
return;
|
|
19
|
+
traceSink.emit(createTraceEvent({
|
|
20
|
+
sessionId: traceSessionId,
|
|
21
|
+
turnId: traceTurnId,
|
|
22
|
+
type,
|
|
23
|
+
...(currentTraceStep === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: {
|
|
26
|
+
step: currentTraceStep,
|
|
27
|
+
stepId: `${traceTurnId}:step:${currentTraceStep}`,
|
|
28
|
+
}),
|
|
29
|
+
...ids,
|
|
30
|
+
data,
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
emitTrace('turn_start', { mode: ctx.getAgentMode() });
|
|
34
|
+
if (opts.initialToolRoute)
|
|
35
|
+
emitTrace('tool_route', opts.initialToolRoute);
|
|
36
|
+
return {
|
|
37
|
+
usageMeter,
|
|
38
|
+
traceSessionId,
|
|
39
|
+
traceTurnId,
|
|
40
|
+
startedAt,
|
|
41
|
+
emitTrace,
|
|
42
|
+
setCurrentStep: (step) => {
|
|
43
|
+
currentTraceStep = step;
|
|
44
|
+
},
|
|
45
|
+
addToolCalls: (count) => {
|
|
46
|
+
toolCallCount += count;
|
|
47
|
+
},
|
|
48
|
+
markCompleted: () => {
|
|
49
|
+
done = true;
|
|
50
|
+
traceStatus = 'completed';
|
|
51
|
+
},
|
|
52
|
+
markAborted: () => {
|
|
53
|
+
traceStatus = 'aborted';
|
|
54
|
+
},
|
|
55
|
+
markMaxSteps: () => {
|
|
56
|
+
done = true;
|
|
57
|
+
traceStatus = 'max_steps';
|
|
58
|
+
},
|
|
59
|
+
createCancellation: (historyManager, rebuildHistoryIndexes) => {
|
|
60
|
+
const init = {
|
|
61
|
+
historyManager,
|
|
62
|
+
onObserved: () => emitTrace('abort', { phase: 'observed', reason: 'signal' }),
|
|
63
|
+
onAbort: () => opts.hooks.onAbort?.(),
|
|
64
|
+
onHistoryRestored: rebuildHistoryIndexes,
|
|
65
|
+
restoreMode: () => ctx.setAgentMode(savedMode),
|
|
66
|
+
};
|
|
67
|
+
return stages.cancellation.implementation === 'staged'
|
|
68
|
+
? createStagedCancellationLifecycle(init)
|
|
69
|
+
: createLegacyCancellationLifecycle(init);
|
|
70
|
+
},
|
|
71
|
+
buildAbortedResult: () => {
|
|
72
|
+
const mutation = ctx.getCurrentTurnMutationState();
|
|
73
|
+
return {
|
|
74
|
+
completed: false,
|
|
75
|
+
terminationReason: 'aborted',
|
|
76
|
+
finalText: null,
|
|
77
|
+
usage: usageMeter.snapshot(),
|
|
78
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
finalize: () => {
|
|
82
|
+
const finalMutation = ctx.getCurrentTurnMutationState();
|
|
83
|
+
currentTraceStep = undefined;
|
|
84
|
+
emitTrace('turn_end', {
|
|
85
|
+
status: traceStatus,
|
|
86
|
+
durationMs: Date.now() - startedAt,
|
|
87
|
+
toolCalls: toolCallCount,
|
|
88
|
+
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
89
|
+
totalTokens: usageMeter.snapshot()?.totalTokens,
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
opts.onTrace?.({
|
|
93
|
+
ts: new Date().toISOString(),
|
|
94
|
+
sessionId: traceSessionId,
|
|
95
|
+
turnId: traceTurnId,
|
|
96
|
+
status: traceStatus,
|
|
97
|
+
durationMs: Date.now() - startedAt,
|
|
98
|
+
toolCalls: toolCallCount,
|
|
99
|
+
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
100
|
+
usage: usageMeter.snapshot(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Trace is best-effort and must not change the turn result.
|
|
105
|
+
}
|
|
106
|
+
if (done)
|
|
107
|
+
opts.hooks.onDone?.(Date.now() - startedAt, usageMeter.snapshot());
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/config/index.js
CHANGED
|
@@ -530,6 +530,20 @@ export const config = {
|
|
|
530
530
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
531
531
|
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
532
532
|
};
|
|
533
|
+
/**
|
|
534
|
+
* 创建一份独立 Config 快照,不重新读取环境变量、配置文件或 preset。
|
|
535
|
+
*
|
|
536
|
+
* `source.systemPrompt` 若是 getter(全局 config 即如此)会在创建时求值并物化为字符串,
|
|
537
|
+
* 使快照不会继续隐式依赖全局 config;调用方也可通过 overrides 显式替换它。
|
|
538
|
+
* 当前 Config 的可变容器字段 llmKeysFromShell 始终复制,避免 runtime 间共享数组引用。
|
|
539
|
+
*/
|
|
540
|
+
export function createConfigSnapshot(overrides = {}, source = config) {
|
|
541
|
+
const snapshot = { ...source, ...overrides };
|
|
542
|
+
return {
|
|
543
|
+
...snapshot,
|
|
544
|
+
llmKeysFromShell: [...snapshot.llmKeysFromShell],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
533
547
|
/**
|
|
534
548
|
* 会话钉死模型:窗口/会话启动时由 pinSessionModel() 捕获一次。
|
|
535
549
|
* 运行中 agent 一律经 getActiveModel() 取模型,而非热切的 config.model——
|
package/dist/host/stdio.js
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import readline from 'node:readline';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { refreshChatTools, estimateMessagesTokens } from '../llm/index.js';
|
|
3
|
+
import { createRuntime } from '../runtime/index.js';
|
|
4
|
+
import { buildBasePrompt } from '../config/index.js';
|
|
5
|
+
import { estimateMessagesTokens } from '../llm/index.js';
|
|
7
6
|
import { initializeAllMcp, getMcpTools, getMcpWarnings, closeAllMcp } from '../mcp/index.js';
|
|
8
|
-
import {
|
|
9
|
-
import { appendCurrentSessionTraceEvent, createContextState, loadSession, newSessionId, saveSession, } from '../session/index.js';
|
|
10
|
-
import { setCurrentSessionId } from '../session/state.js';
|
|
11
|
-
import { manualCompact } from '../session/scheduler.js';
|
|
7
|
+
import { appendCurrentSessionTraceEvent, createContextState } from '../session/index.js';
|
|
12
8
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
13
9
|
import { clearSkillActivation } from '../skills/activation.js';
|
|
14
|
-
import { registerToolsExtension } from '../tools/registry.js';
|
|
15
10
|
import { ToolPolicyController } from '../tools/policy.js';
|
|
16
11
|
import { routeToolGroups } from '../tools/router.js';
|
|
17
12
|
// 装配官方默认工具包:registry 不再顶层 import builtins(破模块循环),host 入口须显式装配。
|
|
18
13
|
import '../tools/builtins/index.js';
|
|
19
14
|
import { parseCommand } from './protocol.js';
|
|
20
15
|
let initialized = null;
|
|
16
|
+
let runtimeFacade = null;
|
|
21
17
|
let activeRun = null;
|
|
18
|
+
let closing = false;
|
|
19
|
+
const hostClosedReason = new DOMException('Host input closed.', 'AbortError');
|
|
20
|
+
const shutdownController = new AbortController();
|
|
22
21
|
let sessionId = '';
|
|
23
22
|
let history = [];
|
|
24
23
|
let queryHistory = [];
|
|
25
24
|
let lastToolGroups = [];
|
|
26
25
|
let contextState = createContextState();
|
|
27
26
|
const approvals = new Map();
|
|
27
|
+
function runtime() {
|
|
28
|
+
if (!runtimeFacade)
|
|
29
|
+
throw new Error('Runtime is not initialized.');
|
|
30
|
+
return runtimeFacade;
|
|
31
|
+
}
|
|
28
32
|
function write(envelope) {
|
|
29
33
|
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
30
34
|
}
|
|
@@ -38,15 +42,22 @@ async function initializeRuntime() {
|
|
|
38
42
|
if (initialized)
|
|
39
43
|
return initialized;
|
|
40
44
|
initialized = (async () => {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
if (closing)
|
|
46
|
+
return;
|
|
43
47
|
await initializeAllMcp();
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if (closing)
|
|
49
|
+
return;
|
|
50
|
+
const createdRuntime = createRuntime({ sandboxRoot: process.cwd(), initialMode: 'auto' });
|
|
51
|
+
runtimeFacade = createdRuntime;
|
|
52
|
+
createdRuntime.context.toolRuntime.registerToolsExtension('mcp', getMcpTools());
|
|
53
|
+
await createdRuntime.start();
|
|
54
|
+
if (closing)
|
|
55
|
+
return;
|
|
56
|
+
const runtimeConfig = createdRuntime.context.config;
|
|
46
57
|
emit('runtime_ready', {
|
|
47
|
-
projectRoot:
|
|
48
|
-
provider:
|
|
49
|
-
promptCache:
|
|
58
|
+
projectRoot: createdRuntime.context.sandboxRoot,
|
|
59
|
+
provider: runtimeConfig.provider,
|
|
60
|
+
promptCache: runtimeConfig.provider === 'anthropic' && runtimeConfig.anthropicPromptCache,
|
|
50
61
|
warnings: getMcpWarnings(),
|
|
51
62
|
});
|
|
52
63
|
})();
|
|
@@ -56,21 +67,21 @@ function systemMessage() {
|
|
|
56
67
|
return effectiveSystemPrompt(buildBasePrompt(sessionId));
|
|
57
68
|
}
|
|
58
69
|
function createSession() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setAgentMode('auto');
|
|
70
|
+
const activeRuntime = runtime();
|
|
71
|
+
sessionId = activeRuntime.session.create();
|
|
72
|
+
activeRuntime.context.setAgentMode('auto');
|
|
62
73
|
history = [{ role: 'system', content: systemMessage() }];
|
|
63
74
|
queryHistory = [];
|
|
64
75
|
lastToolGroups = [];
|
|
65
76
|
contextState = createContextState();
|
|
66
77
|
}
|
|
67
78
|
function restoreSession(id) {
|
|
68
|
-
const
|
|
79
|
+
const activeRuntime = runtime();
|
|
80
|
+
const loaded = activeRuntime.session.resume(id);
|
|
69
81
|
if (!loaded?.history.length)
|
|
70
82
|
return false;
|
|
71
83
|
sessionId = loaded.id;
|
|
72
|
-
|
|
73
|
-
setAgentMode('auto');
|
|
84
|
+
activeRuntime.context.setAgentMode('auto');
|
|
74
85
|
history = [...loaded.history];
|
|
75
86
|
if (history[0]?.role === 'system')
|
|
76
87
|
history[0] = { role: 'system', content: systemMessage() };
|
|
@@ -81,6 +92,11 @@ function restoreSession(id) {
|
|
|
81
92
|
contextState = createContextState();
|
|
82
93
|
return true;
|
|
83
94
|
}
|
|
95
|
+
function persistSession() {
|
|
96
|
+
if (!sessionId)
|
|
97
|
+
return;
|
|
98
|
+
runtime().session.save(history, sessionId, queryHistory, lastToolGroups);
|
|
99
|
+
}
|
|
84
100
|
function prepareSession(requestedSessionId) {
|
|
85
101
|
if (!requestedSessionId) {
|
|
86
102
|
if (!sessionId)
|
|
@@ -92,6 +108,8 @@ function prepareSession(requestedSessionId) {
|
|
|
92
108
|
return restoreSession(requestedSessionId) ? true : null;
|
|
93
109
|
}
|
|
94
110
|
function waitForApproval(runId, request) {
|
|
111
|
+
if (closing)
|
|
112
|
+
return Promise.resolve({ action: 'cancelled' });
|
|
95
113
|
const approvalId = randomUUID();
|
|
96
114
|
emit('approval_requested', {
|
|
97
115
|
approvalId,
|
|
@@ -114,6 +132,8 @@ function hooksFor(runId) {
|
|
|
114
132
|
};
|
|
115
133
|
}
|
|
116
134
|
async function run(command) {
|
|
135
|
+
if (closing)
|
|
136
|
+
return;
|
|
117
137
|
if (activeRun)
|
|
118
138
|
return error('An agent run is already active for this project.', command.id);
|
|
119
139
|
if (!command.prompt.trim())
|
|
@@ -121,6 +141,8 @@ async function run(command) {
|
|
|
121
141
|
let toolPolicy;
|
|
122
142
|
try {
|
|
123
143
|
await initializeRuntime();
|
|
144
|
+
if (closing)
|
|
145
|
+
return;
|
|
124
146
|
const resumed = prepareSession(command.sessionId);
|
|
125
147
|
if (resumed === null)
|
|
126
148
|
return error(`Session ${command.sessionId} could not be restored.`, command.id);
|
|
@@ -142,8 +164,8 @@ async function run(command) {
|
|
|
142
164
|
sessionId,
|
|
143
165
|
projectRoot: process.cwd(),
|
|
144
166
|
resumed,
|
|
145
|
-
provider: config.provider,
|
|
146
|
-
promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
|
|
167
|
+
provider: runtime().context.config.provider,
|
|
168
|
+
promptCache: runtime().context.config.provider === 'anthropic' && runtime().context.config.anthropicPromptCache,
|
|
147
169
|
attachments: command.attachments?.map((attachment) => attachment.name) ?? [],
|
|
148
170
|
}, command.id);
|
|
149
171
|
const previousGroups = [...lastToolGroups];
|
|
@@ -153,11 +175,16 @@ async function run(command) {
|
|
|
153
175
|
planMode: false,
|
|
154
176
|
attachmentNames: command.attachments?.map((attachment) => attachment.name),
|
|
155
177
|
signal: controller.signal,
|
|
178
|
+
transport: runtime().context.modelTransport,
|
|
179
|
+
tools: runtime().context.toolRuntime.tools,
|
|
156
180
|
});
|
|
181
|
+
if (closing)
|
|
182
|
+
throw hostClosedReason;
|
|
157
183
|
toolPolicy = new ToolPolicyController({
|
|
158
184
|
groups: decision.groups,
|
|
159
185
|
reason: decision.reason,
|
|
160
186
|
confidence: decision.confidence,
|
|
187
|
+
tools: runtime().context.toolRuntime.tools,
|
|
161
188
|
});
|
|
162
189
|
lastToolGroups = toolPolicy.groupNames;
|
|
163
190
|
const initialToolRoute = {
|
|
@@ -172,8 +199,9 @@ async function run(command) {
|
|
|
172
199
|
planMode: false,
|
|
173
200
|
};
|
|
174
201
|
emit('tool_route', initialToolRoute, command.id);
|
|
175
|
-
const
|
|
176
|
-
|
|
202
|
+
const result = await runtime().run({
|
|
203
|
+
turn: 'new',
|
|
204
|
+
turnLabel: command.prompt.split('\n')[0]?.slice(0, 40) ?? command.prompt.slice(0, 40),
|
|
177
205
|
history,
|
|
178
206
|
userInput,
|
|
179
207
|
signal: controller.signal,
|
|
@@ -181,22 +209,22 @@ async function run(command) {
|
|
|
181
209
|
contextState,
|
|
182
210
|
toolPolicy,
|
|
183
211
|
initialToolRoute,
|
|
184
|
-
traceContext: { sessionId
|
|
212
|
+
traceContext: { sessionId },
|
|
185
213
|
onTraceEvent: appendCurrentSessionTraceEvent,
|
|
186
214
|
permissionPrompt: (request) => waitForApproval(command.id, request),
|
|
187
215
|
});
|
|
188
216
|
lastToolGroups = toolPolicy.groupNames;
|
|
189
|
-
|
|
217
|
+
persistSession();
|
|
190
218
|
emit('run_completed', {
|
|
191
219
|
sessionId,
|
|
192
220
|
completed: result.completed,
|
|
193
221
|
terminationReason: result.terminationReason,
|
|
194
222
|
changedFiles: result.changedFiles ?? [],
|
|
195
223
|
usage: result.usage,
|
|
196
|
-
provider: config.provider,
|
|
197
|
-
promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
|
|
224
|
+
provider: runtime().context.config.provider,
|
|
225
|
+
promptCache: runtime().context.config.provider === 'anthropic' && runtime().context.config.anthropicPromptCache,
|
|
198
226
|
usagePercent: Math.round(contextUsagePercent() * 100),
|
|
199
|
-
contextWindow: config.contextWindowTokens,
|
|
227
|
+
contextWindow: runtime().context.config.contextWindowTokens,
|
|
200
228
|
}, command.id);
|
|
201
229
|
}
|
|
202
230
|
catch (cause) {
|
|
@@ -205,7 +233,7 @@ async function run(command) {
|
|
|
205
233
|
lastToolGroups = toolPolicy.groupNames;
|
|
206
234
|
try {
|
|
207
235
|
if (sessionId)
|
|
208
|
-
|
|
236
|
+
persistSession();
|
|
209
237
|
}
|
|
210
238
|
catch {
|
|
211
239
|
/* Preserve the runtime error. */
|
|
@@ -231,6 +259,7 @@ function cancel(command) {
|
|
|
231
259
|
if (!activeRun)
|
|
232
260
|
return emit('run_idle', {}, command.id);
|
|
233
261
|
activeRun.controller.abort();
|
|
262
|
+
runtimeFacade?.cancel();
|
|
234
263
|
for (const [approvalId, waiter] of approvals) {
|
|
235
264
|
if (waiter.runId === activeRun.id) {
|
|
236
265
|
waiter.resolve({ action: 'cancelled' });
|
|
@@ -243,25 +272,35 @@ function cancel(command) {
|
|
|
243
272
|
function contextUsagePercent() {
|
|
244
273
|
const dialog = history.filter((m) => m.role !== 'system');
|
|
245
274
|
const est = estimateMessagesTokens(dialog);
|
|
246
|
-
return Math.min(1, est / config.contextWindowTokens);
|
|
275
|
+
return Math.min(1, est / runtime().context.config.contextWindowTokens);
|
|
247
276
|
}
|
|
248
277
|
async function compact(command) {
|
|
278
|
+
if (closing)
|
|
279
|
+
return;
|
|
249
280
|
if (activeRun)
|
|
250
281
|
return error('有正在运行的任务,请先取消后再压缩。', command.id);
|
|
251
282
|
try {
|
|
252
283
|
await initializeRuntime();
|
|
284
|
+
if (closing)
|
|
285
|
+
return;
|
|
253
286
|
if (!sessionId)
|
|
254
287
|
createSession();
|
|
255
288
|
emit('status', { value: 'compacting' }, command.id);
|
|
256
|
-
const
|
|
257
|
-
|
|
289
|
+
const activeRuntime = runtime();
|
|
290
|
+
const log = await activeRuntime.compact(history, {
|
|
291
|
+
focus: command.focus,
|
|
292
|
+
force: true,
|
|
293
|
+
contextState,
|
|
294
|
+
signal: shutdownController.signal,
|
|
295
|
+
});
|
|
296
|
+
persistSession();
|
|
258
297
|
const pct = contextUsagePercent();
|
|
259
298
|
emit('compact_done', {
|
|
260
299
|
compacted: log.compactHistoryCalled,
|
|
261
300
|
beforeTokens: log.compactDetail?.estimateBefore,
|
|
262
301
|
afterTokens: log.compactDetail?.estimateAfter,
|
|
263
302
|
usagePercent: Math.round(pct * 100),
|
|
264
|
-
contextWindow: config.contextWindowTokens,
|
|
303
|
+
contextWindow: runtime().context.config.contextWindowTokens,
|
|
265
304
|
}, command.id);
|
|
266
305
|
}
|
|
267
306
|
catch (cause) {
|
|
@@ -285,20 +324,42 @@ async function handle(command) {
|
|
|
285
324
|
return compact(command);
|
|
286
325
|
resolveApproval(command);
|
|
287
326
|
}
|
|
327
|
+
function requestShutdownCancellation() {
|
|
328
|
+
activeRun?.controller.abort(hostClosedReason);
|
|
329
|
+
runtimeFacade?.cancel(hostClosedReason);
|
|
330
|
+
for (const [approvalId, waiter] of approvals) {
|
|
331
|
+
waiter.resolve({ action: 'cancelled' });
|
|
332
|
+
approvals.delete(approvalId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
288
335
|
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
289
|
-
|
|
336
|
+
const pendingCommands = new Set();
|
|
337
|
+
const startupPromise = initializeRuntime();
|
|
338
|
+
void startupPromise.catch((cause) => error(cause instanceof Error ? cause.message : String(cause)));
|
|
290
339
|
for await (const line of input) {
|
|
291
340
|
if (!line.trim())
|
|
292
341
|
continue;
|
|
293
342
|
try {
|
|
294
343
|
const command = parseCommand(JSON.parse(line));
|
|
295
|
-
if (!command)
|
|
344
|
+
if (!command) {
|
|
296
345
|
error('Invalid Mocode Work host command.');
|
|
297
|
-
|
|
298
|
-
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const pending = Promise.resolve(handle(command));
|
|
349
|
+
pendingCommands.add(pending);
|
|
350
|
+
void pending.then(() => pendingCommands.delete(pending), () => pendingCommands.delete(pending));
|
|
351
|
+
}
|
|
299
352
|
}
|
|
300
353
|
catch {
|
|
301
354
|
error('Invalid JSON command.');
|
|
302
355
|
}
|
|
303
356
|
}
|
|
357
|
+
closing = true;
|
|
358
|
+
shutdownController.abort(hostClosedReason);
|
|
359
|
+
requestShutdownCancellation();
|
|
360
|
+
await startupPromise.catch(() => undefined);
|
|
361
|
+
requestShutdownCancellation();
|
|
362
|
+
await Promise.allSettled([...pendingCommands]);
|
|
363
|
+
const runtimeAtShutdown = runtimeFacade;
|
|
364
|
+
await runtimeAtShutdown?.close().catch(() => undefined);
|
|
304
365
|
await closeAllMcp().catch(() => undefined);
|