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,40 +1,134 @@
1
1
  /**
2
2
  * AgentRuntimeContext:runAgentCore 的运行时依赖注入接缝。
3
3
  *
4
- * 2.0 框架化第一步(Context 参数化):把 core.ts 直读的模块级单例(config / agentMode /
5
- * sessionId / sandboxRoot / token 校准 / notes mtime)收敛成一个显式接口。默认实现
6
- * defaultAgentRuntimeContext 原样绑定全局单例,行为与改造前完全一致;宿主(TUI / stdio host /
7
- * agent / 未来多 runtime)可注入自定义实现,在同一进程内获得互不干扰的运行时视图。
8
- *
9
- * 本模块是叶子:只依赖各单例所在模块,不反向 import core.ts,依赖方向无环。
10
- *
11
- * 取舍说明:
12
- * - 用方法(getter)而非裸值字段:config.model 等会被 /model 热切,方法保证每次读取取最新值,
13
- * 与改造前「每次直读全局」的语义一致。
14
- * - jailResolve / getCurrentTurnMutationState 等涉及真实 I/O 或可变返回,保持函数形态原样透传。
15
- * - 暂未覆盖 executeToolOutcome / findTool(tools registry 单例)与 checkPermission:它们属
16
- * 工具系统(步骤 4 抽包时处理),本轮只收敛 agent 运行时自身的单例。
4
+ * 独立 runtime 通过 createAgentRuntimeContext 装配自己的 config、工具表、模型 transport、
5
+ * mode sandbox scope;默认上下文则继续显式绑定进程级单例,保持现有 TUI/host 行为。
6
+ * 本模块不 import builtins:默认工具包只从 defaultToolRuntime.builtinTools 复制,避免
7
+ * registry builtins core → runtime-context 的模块循环。
17
8
  */
18
- import { config, getActiveModel, extractActivePlanSection, buildSessionStateReminder } from '../config/index.js';
19
- import { getAgentMode, setAgentMode } from './mode.js';
20
- import { getCurrentSessionId } from '../session/state.js';
21
- import { getCurrentTurnId, getCurrentTurnMutationState } from '../rollback/index.js';
22
- import { getNotesMtime } from '../session/notes.js';
23
- import { jailResolve } from '../sandbox/index.js';
9
+ import { AsyncLocalStorage } from 'node:async_hooks';
10
+ import { resolve } from 'node:path';
11
+ import { buildSessionStateReminder, config, createConfigSnapshot, extractActivePlanSection, getActiveModel, } from '../config/index.js';
24
12
  import { getTokenCalibration, updateTokenCalibration } from '../context/token-calibration.js';
13
+ import { chat, createChatClientState, createChatTransport, } from '../llm/index.js';
14
+ import { checkPermission, createPermissionChecker } from '../permissions/index.js';
15
+ import { defaultRollbackStore, RollbackStore, withRollbackStore, } from '../rollback/index.js';
16
+ import { getSandboxRoot, jailResolve, withSandboxRoot } from '../sandbox/index.js';
17
+ import { getNotesMtime } from '../session/notes.js';
18
+ import { defaultSessionStore, SessionStore, withSessionStore } from '../session/store.js';
25
19
  import { safeProviderId } from '../session/trace-sanitize.js';
20
+ import { defaultToolRuntime, ToolRuntime } from '../tools/registry.js';
21
+ import { getAgentMode, setAgentMode } from './mode.js';
22
+ const activeRuntimeContexts = new AsyncLocalStorage();
23
+ /** 当前异步 agent 树绑定的 RuntimeContext;编排工具据此让子 agent 继承父 runtime。 */
24
+ export function getActiveAgentRuntimeContext() {
25
+ return activeRuntimeContexts.getStore();
26
+ }
27
+ /** 仅由 agent composition root 调用;嵌套异步任务自动继承且并发树互不污染。 */
28
+ export function withAgentRuntimeContext(runtimeContext, run) {
29
+ return activeRuntimeContexts.run(runtimeContext, run);
30
+ }
31
+ /**
32
+ * 创建一个基础独立 runtime。构造过程不读取 env/config 文件,不加载 builtins 模块,
33
+ * 也不修改全局 mode、sandbox root、工具表或模型客户端。
34
+ */
35
+ export function createAgentRuntimeContext(init = {}) {
36
+ if (init.toolRuntime && init.tools) {
37
+ throw new TypeError('toolRuntime and tools are mutually exclusive');
38
+ }
39
+ const runtimeConfig = createConfigSnapshot(init.configOverrides, init.config ?? config);
40
+ const runtimeSandboxRoot = resolve(init.sandboxRoot ?? runtimeConfig.sandboxRoot ?? getSandboxRoot() ?? process.cwd());
41
+ const hasExplicitSessionsRoot = init.config !== undefined || init.configOverrides?.sessionDir !== undefined;
42
+ const runtimeSessionsRoot = hasExplicitSessionsRoot
43
+ ? runtimeConfig.sessionDir
44
+ : resolve(runtimeSandboxRoot, '.mocode', 'sessions');
45
+ if (!hasExplicitSessionsRoot)
46
+ runtimeConfig.sessionDir = runtimeSessionsRoot;
47
+ const services = init.services ?? {};
48
+ const runtimeGetActiveModel = services.getActiveModel ?? (() => runtimeConfig.model);
49
+ const runtimeSessionStore = init.sessionStore ??
50
+ new SessionStore({
51
+ sessionsRoot: runtimeSessionsRoot,
52
+ workspaceRoot: runtimeSandboxRoot,
53
+ getModel: runtimeGetActiveModel,
54
+ });
55
+ const runtimeRollbackStore = init.rollbackStore ?? new RollbackStore(runtimeSandboxRoot, runtimeSessionStore.sessionsRoot);
56
+ const runtimeToolRuntime = init.toolRuntime ??
57
+ new ToolRuntime({
58
+ beginPathMutation: runtimeRollbackStore.beginPathMutation.bind(runtimeRollbackStore),
59
+ endPathMutation: runtimeRollbackStore.endPathMutation.bind(runtimeRollbackStore),
60
+ beginWorkspaceMutation: runtimeRollbackStore.beginWorkspaceMutation.bind(runtimeRollbackStore),
61
+ endWorkspaceMutation: runtimeRollbackStore.endWorkspaceMutation.bind(runtimeRollbackStore),
62
+ getCurrentTurnMutationState: runtimeRollbackStore.getCurrentTurnMutationState.bind(runtimeRollbackStore),
63
+ });
64
+ if (!init.toolRuntime) {
65
+ runtimeToolRuntime.installBuiltinTools([...(init.tools ?? defaultToolRuntime.builtinTools)]);
66
+ }
67
+ let runtimeMode = init.initialMode ?? 'auto';
68
+ const localGetAgentMode = () => runtimeMode;
69
+ const localSetAgentMode = (mode) => {
70
+ const previous = runtimeMode;
71
+ runtimeMode = mode;
72
+ return previous;
73
+ };
74
+ const runtimeModelTransport = init.modelTransport ??
75
+ createChatTransport({
76
+ config: runtimeConfig,
77
+ getModel: runtimeGetActiveModel,
78
+ clientState: createChatClientState(runtimeConfig, init.modelClientOverrides),
79
+ });
80
+ return {
81
+ config: runtimeConfig,
82
+ toolRuntime: runtimeToolRuntime,
83
+ modelTransport: runtimeModelTransport,
84
+ sessionStore: runtimeSessionStore,
85
+ rollbackStore: runtimeRollbackStore,
86
+ sandboxRoot: runtimeSandboxRoot,
87
+ runInScope: (fn) => withSessionStore(runtimeSessionStore, () => withRollbackStore(runtimeRollbackStore, () => withSandboxRoot(runtimeSandboxRoot, fn))),
88
+ checkPermission: services.checkPermission ?? createPermissionChecker(runtimeConfig, runtimeSandboxRoot),
89
+ getActiveModel: runtimeGetActiveModel,
90
+ getAgentMode: services.getAgentMode ?? localGetAgentMode,
91
+ setAgentMode: services.setAgentMode ?? localSetAgentMode,
92
+ beginTurn: services.beginTurn ?? runtimeRollbackStore.beginTurn.bind(runtimeRollbackStore),
93
+ getCurrentSessionId: services.getCurrentSessionId ?? runtimeSessionStore.getCurrentSessionId.bind(runtimeSessionStore),
94
+ getCurrentTurnId: services.getCurrentTurnId ?? runtimeRollbackStore.getCurrentTurnId.bind(runtimeRollbackStore),
95
+ getCurrentTurnMutationState: services.getCurrentTurnMutationState ??
96
+ runtimeRollbackStore.getCurrentTurnMutationState.bind(runtimeRollbackStore),
97
+ buildSessionStateReminder: services.buildSessionStateReminder ??
98
+ (() => buildSessionStateReminder(runtimeSessionStore.getCurrentSessionId())),
99
+ extractActivePlanSection: services.extractActivePlanSection ?? (() => extractActivePlanSection(runtimeSessionStore.getCurrentSessionId())),
100
+ getNotesMtime: services.getNotesMtime ?? (() => getNotesMtime(runtimeSessionStore.getCurrentSessionId())),
101
+ jailResolve: services.jailResolve ?? jailResolve,
102
+ getTokenCalibration: services.getTokenCalibration ?? getTokenCalibration,
103
+ updateTokenCalibration: services.updateTokenCalibration ?? updateTokenCalibration,
104
+ safeProviderId: services.safeProviderId ?? safeProviderId,
105
+ };
106
+ }
107
+ function currentGlobalSandboxRoot() {
108
+ return resolve(getSandboxRoot() ?? config.sandboxRoot ?? process.cwd());
109
+ }
26
110
  /**
27
- * 默认运行时上下文:原样绑定全局单例,与改造前 runAgentCore 的直读行为完全一致。
28
- * 每个方法都是对应单例函数的透传;config 是同一对象引用(字段读取热生效)。
111
+ * 默认运行时上下文:显式绑定全部旧全局单例。sandboxRoot 使用 getter,确保 REPL 在模块加载后
112
+ * 调用 setSandboxRoot 仍能即时生效;runInScope 在每次调用时捕获当前全局根。
29
113
  */
30
114
  export const defaultAgentRuntimeContext = {
31
115
  config,
116
+ toolRuntime: defaultToolRuntime,
117
+ modelTransport: chat,
118
+ sessionStore: defaultSessionStore,
119
+ rollbackStore: defaultRollbackStore,
120
+ get sandboxRoot() {
121
+ return currentGlobalSandboxRoot();
122
+ },
123
+ runInScope: (fn) => withSessionStore(defaultSessionStore, () => withRollbackStore(defaultRollbackStore, () => withSandboxRoot(currentGlobalSandboxRoot(), fn))),
124
+ checkPermission,
32
125
  getActiveModel,
33
126
  getAgentMode,
34
127
  setAgentMode,
35
- getCurrentSessionId,
36
- getCurrentTurnId,
37
- getCurrentTurnMutationState,
128
+ beginTurn: defaultRollbackStore.beginTurn.bind(defaultRollbackStore),
129
+ getCurrentSessionId: defaultSessionStore.getCurrentSessionId.bind(defaultSessionStore),
130
+ getCurrentTurnId: defaultRollbackStore.getCurrentTurnId.bind(defaultRollbackStore),
131
+ getCurrentTurnMutationState: defaultRollbackStore.getCurrentTurnMutationState.bind(defaultRollbackStore),
38
132
  buildSessionStateReminder,
39
133
  extractActivePlanSection,
40
134
  getNotesMtime,
@@ -10,8 +10,7 @@
10
10
  // - 主屏渲染可选:TUI 激活时把子 agent 内部工具调用实时写入主内容区并复用 batch 折叠;
11
11
  // TUI 未激活(host 嵌入 / 非 TTY)时纯静默,中间过程只缓冲进 transcript。
12
12
  // - 独立 history 分支:子任务的工具噪声不回灌主对话,只有最终摘要回灌。
13
- import { chatTools } from '../llm/index.js';
14
- import { buildMocodeCorePrompt, config, isSubAgentHardDisabled } from '../config/index.js';
13
+ import { buildMocodeCorePrompt, isSubAgentHardDisabled } from '../config/index.js';
15
14
  import { getToolChatSchema } from '../tools/policy.js';
16
15
  import { effectiveSystemPrompt } from '../skills/index.js';
17
16
  import { ui } from '../ui/theme.js';
@@ -19,10 +18,11 @@ import * as layout from '../ui/layout.js';
19
18
  import { isTuiActive } from '../ui/layout.js';
20
19
  import * as batch from '../ui/batch.js';
21
20
  import { isToolErrorOutput } from '../tools/result.js';
22
- import { runAgentCore } from './core.js';
21
+ import { createRuntime, getActiveRuntime } from '../runtime/index.js';
23
22
  import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
24
23
  import { t } from '../i18n/index.js';
25
24
  import { createContextState } from '../session/compact.js';
25
+ import { defaultAgentRuntimeContext, getActiveAgentRuntimeContext, } from './runtime-context.js';
26
26
  /** 子 agent 系统提示后缀(仅 legacy 直接调用路径用;共享前缀路径的系统提示直接复用父 agent)。 */
27
27
  const SUBAGENT_SUFFIX = `
28
28
 
@@ -48,6 +48,9 @@ You are executing one delegated sub-task with the same engineering standards and
48
48
  * 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
49
49
  */
50
50
  export async function spawnAgent(opts) {
51
+ const activeRuntime = opts.runtime ?? getActiveRuntime();
52
+ const runtimeContext = opts.runtimeContext ?? activeRuntime?.context ?? getActiveAgentRuntimeContext() ?? defaultAgentRuntimeContext;
53
+ const runtime = activeRuntime?.context === runtimeContext ? activeRuntime : createRuntime({ context: runtimeContext });
51
54
  if (isSubAgentHardDisabled()) {
52
55
  return {
53
56
  summary: null,
@@ -57,7 +60,7 @@ export async function spawnAgent(opts) {
57
60
  usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
58
61
  };
59
62
  }
60
- const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps;
63
+ const maxSteps = opts.maxSteps ?? runtimeContext.config.subAgentMaxSteps;
61
64
  const requested = opts.tools === undefined ? null : new Set(opts.tools);
62
65
  const shared = opts.delegation && opts.delegation.history.length > 0 && opts.delegation.history[0]?.role === 'system'
63
66
  ? opts.delegation
@@ -108,10 +111,10 @@ export async function spawnAgent(opts) {
108
111
  // chatTools 为 baseline;显式 tools:[] 必须保持零工具,不能误当成"未限制"。
109
112
  const parentNames = opts.parentAllowedToolNames
110
113
  ? [...new Set(opts.parentAllowedToolNames)]
111
- : chatTools.map((tool) => tool.function.name);
114
+ : runtimeContext.toolRuntime.tools.map((tool) => tool.name);
112
115
  const effectiveNames = parentNames.filter((name) => !requested || requested.has(name));
113
116
  toolsOverride = effectiveNames.flatMap((name) => {
114
- const schema = getToolChatSchema(name);
117
+ const schema = getToolChatSchema(name, runtimeContext.toolRuntime.tools);
115
118
  return schema ? [schema] : [];
116
119
  });
117
120
  runtimeAllowedToolNames = new Set(toolsOverride.map((tool) => tool.function.name));
@@ -282,7 +285,8 @@ export async function spawnAgent(opts) {
282
285
  // 与主 agent 完全同源:写操作直接落在工作区,进入主 agent 当前轮次的同一回滚事务
283
286
  // (spawn 不调 beginTurn)。没有 overlay 拷贝/ChangeSet 合并这一步——那是旧 read/write
284
287
  // 双模式的产物,子 agent 不再受限,也就不需要"先隔离再合并"。
285
- const result = await runAgentCore({
288
+ const result = await runtime.run({
289
+ turn: 'inherit',
286
290
  history,
287
291
  userInput,
288
292
  signal: opts.signal,
@@ -0,0 +1,63 @@
1
+ import { maybeCompact } from '../../session/index.js';
2
+ import { defaultCompactionRuntime } from '../../session/compact.js';
3
+ class LegacyCompatibleContextTrimmer {
4
+ implementation;
5
+ init;
6
+ constructor(implementation, init) {
7
+ this.implementation = implementation;
8
+ this.init = init;
9
+ }
10
+ async trim(request) {
11
+ if (request.mode === 'scheduled')
12
+ return this.trimScheduled(request);
13
+ const result = await this.init.historyManager.withLegacyMutableHistory((history) => maybeCompact(history, undefined, request.mode === 'overflow' ? { manual: true, force: true } : undefined, this.init.contextState, request.tools, request.signal, this.init.runtime ?? defaultCompactionRuntime));
14
+ if (!result)
15
+ return { kind: 'none', stats: {} };
16
+ const stats = {
17
+ reason: result.reason,
18
+ compacted: result.compacted,
19
+ estimateBefore: result.estimateBefore,
20
+ estimateAfter: result.estimateAfter,
21
+ };
22
+ if (result.historyRebuilt) {
23
+ return { kind: 'rebuild', history: this.compactedHistory(), stats };
24
+ }
25
+ return { kind: result.compacted ? 'content' : 'none', stats };
26
+ }
27
+ async trimScheduled(request) {
28
+ const scheduler = this.init.scheduler;
29
+ if (!scheduler)
30
+ throw new Error('Scheduled context trim requires a budget scheduler.');
31
+ await this.init.historyManager.withLegacyMutableHistory((history) => scheduler.runStep(history, request.step, request.tools, request.ephemeralTokens, request.signal));
32
+ const log = scheduler.lastRunLog;
33
+ const stats = {
34
+ reason: log?.compactHistoryCalled ? 'scheduled' : undefined,
35
+ compactHistoryCalled: log?.compactHistoryCalled,
36
+ estimateBefore: log?.report.total,
37
+ estimateAfter: log?.pressure.after,
38
+ };
39
+ if (log?.historyMutation === 'rebuild') {
40
+ return { kind: 'rebuild', history: this.compactedHistory(), stats };
41
+ }
42
+ return { kind: log?.historyMutation === 'content' ? 'content' : 'none', stats };
43
+ }
44
+ compactedHistory() {
45
+ return { messages: this.init.historyManager.snapshot().messages };
46
+ }
47
+ }
48
+ class LegacyContextTrimmer extends LegacyCompatibleContextTrimmer {
49
+ constructor(init) {
50
+ super('legacy', init);
51
+ }
52
+ }
53
+ class StagedContextTrimmer extends LegacyCompatibleContextTrimmer {
54
+ constructor(init) {
55
+ super('staged', init);
56
+ }
57
+ }
58
+ export function createLegacyContextTrimmer(init) {
59
+ return new LegacyContextTrimmer(init);
60
+ }
61
+ export function createStagedContextTrimmer(init) {
62
+ return new StagedContextTrimmer(init);
63
+ }
@@ -0,0 +1,12 @@
1
+ /** Stable stage identities used by assembly and per-stage rollback. */
2
+ export const AGENT_STAGE_NAMES = [
3
+ 'history',
4
+ 'model',
5
+ 'tools',
6
+ 'context',
7
+ 'trace',
8
+ 'usage',
9
+ 'cancellation',
10
+ 'termination',
11
+ 'capabilities',
12
+ ];
@@ -0,0 +1,178 @@
1
+ function replaceMessages(backing, messages) {
2
+ const replacement = messages.slice();
3
+ backing.length = 0;
4
+ backing.push(...replacement);
5
+ }
6
+ function assistantMessage(turn) {
7
+ const message = {
8
+ role: 'assistant',
9
+ content: turn.content,
10
+ };
11
+ if (turn.toolCalls.length > 0) {
12
+ message.tool_calls = turn.toolCalls.map((call) => ({
13
+ id: call.id,
14
+ type: 'function',
15
+ function: { name: call.name, arguments: call.arguments },
16
+ }));
17
+ }
18
+ return message;
19
+ }
20
+ function validateCalls(calls) {
21
+ const seen = new Set();
22
+ for (const call of calls) {
23
+ if (!call.id)
24
+ throw new Error('History tool batch contains an empty tool_call id.');
25
+ if (seen.has(call.id))
26
+ throw new Error(`History tool batch contains duplicate tool_call id: ${call.id}.`);
27
+ seen.add(call.id);
28
+ }
29
+ }
30
+ function validateAssistantBatch(backing, calls) {
31
+ const message = backing.at(-1);
32
+ if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls)) {
33
+ throw new Error('History tool batch must follow an assistant tool_calls message.');
34
+ }
35
+ if (message.tool_calls.length !== calls.length) {
36
+ throw new Error(`History assistant declared ${message.tool_calls.length} tool call(s), transaction received ${calls.length}.`);
37
+ }
38
+ for (let index = 0; index < calls.length; index++) {
39
+ const actual = message.tool_calls[index];
40
+ const expected = calls[index];
41
+ if (actual.id !== expected.id ||
42
+ actual.function?.name !== expected.name ||
43
+ actual.function?.arguments !== expected.arguments) {
44
+ throw new Error(`History tool batch call ${index} does not match the preceding assistant message.`);
45
+ }
46
+ }
47
+ }
48
+ function validateToolResults(calls, messages, resultStartIndex) {
49
+ const results = messages.slice(resultStartIndex);
50
+ if (results.length !== calls.length) {
51
+ throw new Error(`History tool batch expected ${calls.length} result(s), received ${results.length}.`);
52
+ }
53
+ const seen = new Set();
54
+ for (let index = 0; index < calls.length; index++) {
55
+ const message = results[index];
56
+ if (message.role !== 'tool') {
57
+ throw new Error(`History tool batch result ${index} must be a tool message.`);
58
+ }
59
+ const actualId = message.tool_call_id ?? '';
60
+ const expectedId = calls[index].id;
61
+ if (seen.has(actualId)) {
62
+ throw new Error(`History tool batch contains duplicate result id: ${actualId || '<empty>'}.`);
63
+ }
64
+ seen.add(actualId);
65
+ if (actualId !== expectedId) {
66
+ throw new Error(`History tool batch result ${index} expected id ${expectedId}, received ${actualId || '<empty>'}.`);
67
+ }
68
+ }
69
+ }
70
+ function validateAttachment(attachment) {
71
+ if (attachment && attachment.role !== 'user') {
72
+ throw new Error('History tool batch attachment must be a user message.');
73
+ }
74
+ }
75
+ class DefaultHistoryManager {
76
+ backing;
77
+ options;
78
+ revision = 0;
79
+ activeBatch = false;
80
+ activeBatchRollback;
81
+ constructor(backing, options) {
82
+ this.backing = backing;
83
+ this.options = options;
84
+ }
85
+ snapshot() {
86
+ return { revision: this.revision, messages: this.backing.slice() };
87
+ }
88
+ appendUserTurn(content) {
89
+ this.assertNoActiveBatch('append a user turn');
90
+ this.backing.push({ role: 'user', content });
91
+ this.revision++;
92
+ }
93
+ appendAssistantTurn(turn) {
94
+ this.assertNoActiveBatch('append an assistant turn');
95
+ if (turn.toolCalls.length > 0)
96
+ validateCalls(turn.toolCalls);
97
+ this.backing.push(assistantMessage(turn));
98
+ this.revision++;
99
+ }
100
+ beginToolBatch(calls) {
101
+ this.assertNoActiveBatch('begin another tool batch');
102
+ validateCalls(calls);
103
+ validateAssistantBatch(this.backing, calls);
104
+ this.activeBatch = true;
105
+ const resultStartIndex = this.backing.length;
106
+ const workingMessages = this.options.stagedToolBatches ? this.backing.slice() : this.backing;
107
+ let settled = false;
108
+ const settle = () => {
109
+ settled = true;
110
+ this.activeBatch = false;
111
+ this.activeBatchRollback = undefined;
112
+ };
113
+ const rollback = () => {
114
+ if (settled)
115
+ return;
116
+ // staged has only a shadow to discard; legacy deliberately leaves its direct working view unchanged.
117
+ settle();
118
+ };
119
+ this.activeBatchRollback = rollback;
120
+ return {
121
+ workingMessages,
122
+ commit: (attachment) => {
123
+ if (settled)
124
+ throw new Error('History tool batch transaction is already settled.');
125
+ try {
126
+ validateAttachment(attachment);
127
+ validateToolResults(calls, workingMessages, resultStartIndex);
128
+ if (this.options.stagedToolBatches) {
129
+ this.backing.push(...workingMessages.slice(resultStartIndex));
130
+ }
131
+ if (attachment)
132
+ this.backing.push(attachment);
133
+ this.revision++;
134
+ settle();
135
+ }
136
+ catch (error) {
137
+ // Validation is pre-publication in staged mode. Legacy keeps its direct-view failure state for rollback parity.
138
+ settle();
139
+ throw error;
140
+ }
141
+ },
142
+ rollback,
143
+ };
144
+ }
145
+ replaceAfterCompaction(result) {
146
+ this.assertNoActiveBatch('replace history after compaction');
147
+ replaceMessages(this.backing, result.messages);
148
+ this.revision++;
149
+ }
150
+ createCheckpoint() {
151
+ return { revision: this.revision, messages: this.backing.slice() };
152
+ }
153
+ restore(checkpoint) {
154
+ this.activeBatchRollback?.();
155
+ replaceMessages(this.backing, checkpoint.messages);
156
+ this.revision++;
157
+ }
158
+ async withLegacyMutableHistory(operation) {
159
+ this.assertNoActiveBatch('run a legacy history mutation');
160
+ try {
161
+ return await operation(this.backing);
162
+ }
163
+ finally {
164
+ // The bridge deliberately assumes mutation: compact may rewrite message content without changing array shape.
165
+ this.revision++;
166
+ }
167
+ }
168
+ assertNoActiveBatch(action) {
169
+ if (this.activeBatch)
170
+ throw new Error(`Cannot ${action} while a history tool batch is active.`);
171
+ }
172
+ }
173
+ export function createLegacyHistoryManager(input) {
174
+ return new DefaultHistoryManager(input.messages, { stagedToolBatches: false });
175
+ }
176
+ export function createStagedHistoryManager(input) {
177
+ return new DefaultHistoryManager(input.messages, { stagedToolBatches: true });
178
+ }
@@ -0,0 +1,19 @@
1
+ import { AGENT_STAGE_NAMES, } from './contracts.js';
2
+ /**
3
+ * Untouched stages keep every business rule inside the existing coordinator. These adapters are explicit rollback
4
+ * boundaries, not fake partial implementations of HistoryManager/ToolDispatcher/etc. Each migration stage replaces
5
+ * one descriptor with a real port while the remaining stages continue through the same legacy coordinator.
6
+ */
7
+ export function createLegacyStageAdapters(overrides = {}) {
8
+ return Object.freeze(Object.fromEntries(AGENT_STAGE_NAMES.map((name) => [
9
+ name,
10
+ Object.freeze({ name, implementation: overrides[name] ?? 'legacy' }),
11
+ ])));
12
+ }
13
+ /** One adapter per run: state and future staged replacements must never leak across agent runs. */
14
+ export function createLegacyCoordinatorAdapter(coordinator) {
15
+ return Object.freeze({
16
+ implementation: 'legacy',
17
+ run: (options, historyManager, stages) => coordinator(options, historyManager, stages),
18
+ });
19
+ }
@@ -0,0 +1,29 @@
1
+ import { chat } from '../../llm/index.js';
2
+ class ChatModelRunner {
3
+ implementation;
4
+ transport;
5
+ constructor(implementation, transport) {
6
+ this.implementation = implementation;
7
+ this.transport = transport;
8
+ }
9
+ run(request, signal) {
10
+ return this.transport(request.history.slice(), request.handlers, signal, request.tools.slice());
11
+ }
12
+ }
13
+ class LegacyChatModelRunner extends ChatModelRunner {
14
+ constructor(transport) {
15
+ super('legacy', transport);
16
+ }
17
+ }
18
+ class StagedChatModelRunner extends ChatModelRunner {
19
+ constructor(transport) {
20
+ super('staged', transport);
21
+ }
22
+ }
23
+ /** Legacy and staged bindings are separate rollback seams over the same frozen single-call transport contract. */
24
+ export function createLegacyModelRunner(transport = chat) {
25
+ return new LegacyChatModelRunner(transport);
26
+ }
27
+ export function createStagedModelRunner(transport = chat) {
28
+ return new StagedChatModelRunner(transport);
29
+ }
@@ -0,0 +1,73 @@
1
+ class DefaultCapabilityResolver {
2
+ implementation;
3
+ constructor(implementation) {
4
+ this.implementation = implementation;
5
+ }
6
+ resolve(request) {
7
+ const configured = request.toolsOverride ?? request.toolPolicy?.tools ?? request.defaultTools;
8
+ const bounded = request.runtimeAllowedToolNames
9
+ ? configured.filter((tool) => request.runtimeAllowedToolNames?.has(tool.function.name))
10
+ : configured.slice();
11
+ const legacyDisabled = request.useLegacyDisabledFallback ? request.legacyDisabledToolNames : new Set();
12
+ const tools = bounded.filter((tool) => !request.skillDisabledToolNames.has(tool.function.name) && !legacyDisabled.has(tool.function.name));
13
+ return Object.freeze({
14
+ mode: request.mode,
15
+ tools: Object.freeze(tools),
16
+ allowedToolNames: new Set(tools.map((tool) => tool.function.name)),
17
+ ...(request.toolPolicy ? { toolPolicy: request.toolPolicy } : {}),
18
+ reminder: request.reminder,
19
+ });
20
+ }
21
+ }
22
+ class PhaseAwareTerminationPolicy {
23
+ implementation;
24
+ constructor(implementation) {
25
+ this.implementation = implementation;
26
+ }
27
+ decide(input) {
28
+ if (input.phase === 'step_start')
29
+ return input.aborted ? { kind: 'aborted' } : { kind: 'continue' };
30
+ if (input.phase === 'model_result') {
31
+ if (!input.modelResult)
32
+ throw new Error('model_result termination requires a model result.');
33
+ return input.modelResult.toolCalls.length === 0
34
+ ? { kind: 'completed', finalText: input.modelResult.content }
35
+ : { kind: 'continue' };
36
+ }
37
+ if (input.phase === 'tool_batch_committed')
38
+ return { kind: 'continue' };
39
+ return { kind: 'max_steps' };
40
+ }
41
+ }
42
+ class LegacyCapabilityResolver extends DefaultCapabilityResolver {
43
+ constructor() {
44
+ super('legacy');
45
+ }
46
+ }
47
+ class StagedCapabilityResolver extends DefaultCapabilityResolver {
48
+ constructor() {
49
+ super('staged');
50
+ }
51
+ }
52
+ class LegacyTerminationPolicy extends PhaseAwareTerminationPolicy {
53
+ constructor() {
54
+ super('legacy');
55
+ }
56
+ }
57
+ class StagedTerminationPolicy extends PhaseAwareTerminationPolicy {
58
+ constructor() {
59
+ super('staged');
60
+ }
61
+ }
62
+ export function createLegacyCapabilityResolver() {
63
+ return new LegacyCapabilityResolver();
64
+ }
65
+ export function createStagedCapabilityResolver() {
66
+ return new StagedCapabilityResolver();
67
+ }
68
+ export function createLegacyTerminationPolicy() {
69
+ return new LegacyTerminationPolicy();
70
+ }
71
+ export function createStagedTerminationPolicy() {
72
+ return new StagedTerminationPolicy();
73
+ }