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.
Files changed (47) hide show
  1. package/README.md +13 -1
  2. package/dist/agent/core.js +14 -936
  3. package/dist/agent/index.js +37 -13
  4. package/dist/agent/model-turn.js +218 -0
  5. package/dist/agent/pipeline.js +18 -0
  6. package/dist/agent/run-contracts.js +1 -0
  7. package/dist/agent/run-coordinator.js +758 -0
  8. package/dist/agent/runtime-context.js +118 -24
  9. package/dist/agent/spawn.js +11 -7
  10. package/dist/agent/stages/context-trimmer.js +63 -0
  11. package/dist/agent/stages/contracts.js +12 -0
  12. package/dist/agent/stages/history-manager.js +178 -0
  13. package/dist/agent/stages/legacy-adapters.js +19 -0
  14. package/dist/agent/stages/model-runner.js +29 -0
  15. package/dist/agent/stages/run-policy.js +73 -0
  16. package/dist/agent/stages/tool-dispatcher.js +341 -0
  17. package/dist/agent/tool-helpers.js +12 -12
  18. package/dist/agent/tool-turn.js +87 -0
  19. package/dist/agent/trace-state.js +97 -101
  20. package/dist/agent/turn-lifecycle.js +110 -0
  21. package/dist/config/index.js +14 -0
  22. package/dist/host/stdio.js +101 -40
  23. package/dist/llm/index.js +51 -35
  24. package/dist/llm/providers/anthropic.js +16 -10
  25. package/dist/llm/runtime.js +1 -0
  26. package/dist/permissions/index.js +21 -5
  27. package/dist/repl/commands/compact.js +2 -2
  28. package/dist/repl/commands/session.js +3 -12
  29. package/dist/repl/message-format.js +5 -0
  30. package/dist/repl/runtime.js +95 -55
  31. package/dist/rollback/index.js +29 -624
  32. package/dist/rollback/store.js +593 -0
  33. package/dist/runtime/index.js +1 -0
  34. package/dist/runtime/runtime.js +307 -0
  35. package/dist/session/compact.js +22 -14
  36. package/dist/session/index.js +1 -0
  37. package/dist/session/persist.js +10 -146
  38. package/dist/session/scheduler.js +28 -16
  39. package/dist/session/state.js +16 -12
  40. package/dist/session/store.js +218 -0
  41. package/dist/session/trace.js +5 -15
  42. package/dist/tools/policy.js +19 -15
  43. package/dist/tools/registry.js +21 -229
  44. package/dist/tools/router.js +5 -3
  45. package/dist/tools/tool-runtime.js +267 -0
  46. package/dist/ui/layout-internal/content-write.js +4 -0
  47. package/package.json +7 -3
@@ -1,118 +1,114 @@
1
- /**
2
- * agent 单轮的 trace / token-usage 状态聚合(从 core.ts 提取,2.0 步骤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(init) {
28
- this.sessionId = init.sessionId;
29
- this.turnId = init.turnId;
30
- this.onTraceEvent = init.onTraceEvent;
4
+ constructor(implementation, onTraceEvent) {
5
+ this.implementation = implementation;
6
+ this.onTraceEvent = onTraceEvent;
31
7
  }
32
- /** 记一条 trace 事件。best-effort:回调抛错被吞,绝不改变执行路径。 */
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?.(createTraceEvent({
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
- /** 累加一次 chat 返回的真实 usage 到本轮 turnUsage。 */
57
- addUsage(u) {
58
- if (!u)
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.turnUsage = this.turnUsage
26
+ this.usage = this.usage
61
27
  ? {
62
- promptTokens: this.turnUsage.promptTokens + u.promptTokens,
63
- completionTokens: this.turnUsage.completionTokens + u.completionTokens,
64
- totalTokens: this.turnUsage.totalTokens + u.totalTokens,
65
- cachedTokens: this.turnUsage.cachedTokens + u.cachedTokens,
66
- cacheCreationTokens: (this.turnUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0),
67
- reasoningTokens: this.turnUsage.reasoningTokens + u.reasoningTokens,
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
- : u;
35
+ : next;
70
36
  }
71
- /**
72
- * 实时用量上报:onLiveUsage 推送「已完成步实测(turnUsage) + 当前步(prompt 估算/实测 + 流式 completion)」。
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
- * 中断还原:记一次 abort 事件(幂等)→ onAbort 钩子 → history 回滚到快照 → 模式还原。
91
- * 与 core.ts 原 abortRestore 闭包逐字一致(含调用顺序);traceStatus 赋值留在 core
92
- * (它是 core 局部 let,turn_end 埋点还要读)。
93
- */
94
- abortRestore(deps) {
95
- if (!this.abortTraced) {
96
- this.emit('abort', { phase: 'observed', reason: 'signal' });
97
- this.abortTraced = true;
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
- deps.hooks.onAbort?.();
100
- deps.history.length = 0;
101
- deps.history.push(...deps.savedHistory);
102
- deps.ctx.setAgentMode(deps.savedMode);
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
- * 构造中断返回值(completed=false / terminationReason='aborted' / finalText=null)。
106
- * core.ts 两处 aborted-return 块逐字节相同,收敛到此处;mutation 由调用方现取
107
- * (ctx.getCurrentTurnMutationState()),保证读取时点与原内联代码一致。
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
+ }
@@ -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——
@@ -1,30 +1,34 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import readline from 'node:readline';
3
- import { runAgentCore } from '../agent/core.js';
4
- import { setAgentMode } from '../agent/mode.js';
5
- import { buildBasePrompt, config } from '../config/index.js';
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 { setSandboxRoot } from '../sandbox/index.js';
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
- setSandboxRoot(process.cwd());
42
- setAgentMode('auto');
45
+ if (closing)
46
+ return;
43
47
  await initializeAllMcp();
44
- registerToolsExtension('mcp', getMcpTools());
45
- refreshChatTools();
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: process.cwd(),
48
- provider: config.provider,
49
- promptCache: config.provider === 'anthropic' && config.anthropicPromptCache,
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
- sessionId = newSessionId();
60
- setCurrentSessionId(sessionId, process.cwd());
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 loaded = loadSession(id);
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
- setCurrentSessionId(sessionId, process.cwd());
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 turnId = queryHistory.length;
176
- const result = await runAgentCore({
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, turnId },
212
+ traceContext: { sessionId },
185
213
  onTraceEvent: appendCurrentSessionTraceEvent,
186
214
  permissionPrompt: (request) => waitForApproval(command.id, request),
187
215
  });
188
216
  lastToolGroups = toolPolicy.groupNames;
189
- saveSession(history, sessionId, queryHistory, lastToolGroups);
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
- saveSession(history, sessionId, queryHistory, lastToolGroups);
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 log = await manualCompact(history, command.focus, { force: true });
257
- saveSession(history, sessionId, queryHistory, lastToolGroups);
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
- void initializeRuntime().catch((cause) => error(cause instanceof Error ? cause.message : String(cause)));
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
- else
298
- void handle(command);
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);