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,40 +1,134 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentRuntimeContext:runAgentCore 的运行时依赖注入接缝。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
package/dist/agent/spawn.js
CHANGED
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
// - 主屏渲染可选:TUI 激活时把子 agent 内部工具调用实时写入主内容区并复用 batch 折叠;
|
|
11
11
|
// TUI 未激活(host 嵌入 / 非 TTY)时纯静默,中间过程只缓冲进 transcript。
|
|
12
12
|
// - 独立 history 分支:子任务的工具噪声不回灌主对话,只有最终摘要回灌。
|
|
13
|
-
import {
|
|
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 {
|
|
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
|
-
:
|
|
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
|
|
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,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
|
+
}
|